Summary
U+270E ✎ (LOWER RIGHT PENCIL) paints as tofu in the Wasm harness. It must render on all three product surfaces — not just the queue-row button:
| Surface |
Path today |
Why it tofus |
| Queue-row ✎ button |
queue_band.zig dvui.button + composerIconFont() → DejaVu-only |
Widget may not honor .font; if it falls through to mixed/faceFor, emoji wins |
| Rich markdown (paragraph / emphasis / headings) |
paint_text.zig → addTextMixed(…, body) |
faceFor(0x270E) == .emoji via isEmojiRelated 0x2600…0x2712. OpenMoji subset does not ship U+270E (it has ✏ U+270F). .notdef |
| Code / fence / inline code |
paint_code.zig / fence runs → addTextMixed(…, .theme(.mono)) |
Same faceFor → OpenMoji. Vera does not have U+270E either. A mono-only path (no mixed) would also tofu |
dvui has no per-glyph fallback. One wrong faceFor hits body and mono because both go through addTextMixed.
Shipped as the queue edit mark in #666. Operators also paste ✎ into prompts / model output / fenced snippets.
Coverage (locked)
Grounded facts
| Face |
U+270E |
| DejaVu symbols subset |
yes |
| Noto Sans Regular |
no |
| OpenMoji subset |
no (has U+270F ✏) |
| Vera Mono |
no |
unicode_face.zig: isEmojiRelated includes 0x2600…0x2712. That range already had ✓/✗ (U+2713 / U+2717) carved out for the same reason — OpenMoji claimed the block and did not ship the glyph. U+270E is still inside the emoji span.
addTextMixed (.symbols): paints fontSymbols() at base.size — so base = mono already sizes DejaVu to the fence. No new paint helper. Routing is the bug.
Suggested fix (not locked — direction)
Carve U+270E out of isEmojiRelated (same pattern as 0x2713 / 0x2717 in the 0x2600…0x2712 hole) so faceFor(0x270E) == .symbols. That one change is what rich markdown and code blocks need.
Queue button: keep pinning composerIconFont() (DejaVu). After the carve-out, a mixed fallthrough also works.
Do not switch the product glyph to ✏ U+270F unless every path is the emoji face — DejaVu has both, OpenMoji only 270F, and we are not rewriting operator/model text.
Tests
| # |
Case |
Layer |
| 1 |
faceFor(0x270E) == .symbols and !isEmojiRelated(0x270E) |
unicode_face / test-rich |
| 2 |
Neighbor dingbats that OpenMoji actually ships stay emoji (e.g. U+270F if still claimed, U+2764) |
unit |
| 3 |
Operator: paragraph containing ✎ |
harness preview |
| 4 |
Operator: fenced block + inline `✎` |
harness preview |
| 5 |
Operator: queue-row ✎ button |
harness preview |
Related (same change, not a substitute)
Cancel "×" U+00D7 on the queue row also uses composerIconFont(). U+00D7 is in Noto, so the DejaVu subset excludes it → symbols-only button can tofu. Prefer ✕ U+2715 (in DejaVu) or the body face. Does not replace the U+270E contract above.
Non-goals
Docs
If the carve-out lands: one timeless sentence in docs/harness-limits.md / fonts/README.md (U+270E → DejaVu, including fences). No issue numbers.
Refs
- #666
native/harness/src/rich/unicode_face.zig — isEmojiRelated / faceFor
native/harness/src/rich/mixed_text.zig — addTextMixed symbols @ base.size
native/harness/src/rich/paint_code.zig — fences / inline
native/harness/src/ui/queue_band.zig — queue-row button
native/harness/src/fonts/README.md
Summary
U+270E
✎(LOWER RIGHT PENCIL) paints as tofu in the Wasm harness. It must render on all three product surfaces — not just the queue-row button:queue_band.zigdvui.button+composerIconFont()→ DejaVu-only.font; if it falls through to mixed/faceFor, emoji winspaint_text.zig→addTextMixed(…, body)faceFor(0x270E) == .emojiviaisEmojiRelated0x2600…0x2712. OpenMoji subset does not ship U+270E (it has ✏ U+270F)..notdefpaint_code.zig/ fence runs →addTextMixed(…, .theme(.mono))faceFor→ OpenMoji. Vera does not have U+270E either. A mono-only path (no mixed) would also tofudvui has no per-glyph fallback. One wrong
faceForhits body and mono because both go throughaddTextMixed.Shipped as the queue edit mark in #666. Operators also paste
✎into prompts / model output / fenced snippets.Coverage (locked)
✎in a normal assistant/user paragraph is a pencil, not tofu✎inside a fenced code block / inline code is a pencil, not tofu (DejaVu at mono size, same as arrows in fences)Grounded facts
unicode_face.zig:isEmojiRelatedincludes0x2600…0x2712. That range already had ✓/✗ (U+2713/U+2717) carved out for the same reason — OpenMoji claimed the block and did not ship the glyph. U+270E is still inside the emoji span.addTextMixed(.symbols): paintsfontSymbols()atbase.size— sobase = monoalready sizes DejaVu to the fence. No new paint helper. Routing is the bug.Suggested fix (not locked — direction)
Carve U+270E out of
isEmojiRelated(same pattern as0x2713/0x2717in the0x2600…0x2712hole) sofaceFor(0x270E) == .symbols. That one change is what rich markdown and code blocks need.Queue button: keep pinning
composerIconFont()(DejaVu). After the carve-out, a mixed fallthrough also works.Do not switch the product glyph to ✏ U+270F unless every path is the emoji face — DejaVu has both, OpenMoji only 270F, and we are not rewriting operator/model text.
Tests
faceFor(0x270E) == .symbolsand!isEmojiRelated(0x270E)unicode_face/test-rich✎`✎`Related (same change, not a substitute)
Cancel
"×"U+00D7 on the queue row also usescomposerIconFont(). U+00D7 is in Noto, so the DejaVu subset excludes it → symbols-only button can tofu. Prefer✕U+2715 (in DejaVu) or the body face. Does not replace the U+270E contract above.Non-goals
Docs
If the carve-out lands: one timeless sentence in
docs/harness-limits.md/fonts/README.md(U+270E → DejaVu, including fences). No issue numbers.Refs
native/harness/src/rich/unicode_face.zig—isEmojiRelated/faceFornative/harness/src/rich/mixed_text.zig—addTextMixedsymbols @base.sizenative/harness/src/rich/paint_code.zig— fences / inlinenative/harness/src/ui/queue_band.zig— queue-row buttonnative/harness/src/fonts/README.md