Draw an emoji-presentation character from the color face #31

Merged
natsukium merged 1 commit from fix/emoji-presentation-face into main 2026-10-06 13:48:59 +09:00
Owner

Closes #14.

A character whose default presentation is emoji (Emoji_Presentation=Yes, such as ⭐ ⚡ ☕ ⌚ 🀄) now takes the first color face covering it, the primary included, instead of the first face of any kind. Before, the symbol faces that precede the emoji face in the fallback chain drew these characters small and monochrome in their two-cell footprint. kitty's font_for_cell makes the same choice.

  • crates/felis-shaping/src/presentation.rs: the Emoji_Presentation table (Unicode 17.0.0 emoji-data.txt), tested against unicode-width and against representative inclusions and exclusions.
  • FontStack::resolve / resolve_styled_index consult the property, so the char path and the cluster path agree. An explicit font.fallback list keeps its declared order for every other character.
  • Shaper::shape_cluster: a VS15 directly after the base (⭐︎, 🀄︎) keeps the base on the first covering face. Otherwise the existing rules apply: the ZWJ/modifier anchor first, then VS16.
  • Text-presentation characters (⏸ ✳ ❤ ☺) and VS16/ZWJ sequences are unchanged.

Before / after (offscreen render with Moralerspace Neon HW):

before and after

Closes #14. A character whose default presentation is emoji (`Emoji_Presentation=Yes`, such as ⭐ ⚡ ☕ ⌚ 🀄) now takes the first color face covering it, the primary included, instead of the first face of any kind. Before, the symbol faces that precede the emoji face in the fallback chain drew these characters small and monochrome in their two-cell footprint. kitty's `font_for_cell` makes the same choice. - `crates/felis-shaping/src/presentation.rs`: the `Emoji_Presentation` table (Unicode 17.0.0 `emoji-data.txt`), tested against unicode-width and against representative inclusions and exclusions. - `FontStack::resolve` / `resolve_styled_index` consult the property, so the char path and the cluster path agree. An explicit `font.fallback` list keeps its declared order for every other character. - `Shaper::shape_cluster`: a VS15 directly after the base (`⭐︎`, `🀄︎`) keeps the base on the first covering face. Otherwise the existing rules apply: the ZWJ/modifier anchor first, then VS16. - Text-presentation characters (⏸ ✳ ❤ ☺) and VS16/ZWJ sequences are unchanged. Before / after (offscreen render with Moralerspace Neon HW): ![before and after](https://git.natsukium.com/attachments/ff8c2127-3f86-4df4-9609-dae667d9c060)
shaping: draw an emoji-presentation character from the color face
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 9s
darwin / darwin docs-only no-op (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 11s
windows / windows docs-only no-op (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 2m23s
bench / Criterion regression gate (pull_request) Failing after 4m9s
windows / cargo nextest (Windows) (pull_request) Failing after 3m28s
windows / frontend smoke (Windows) (pull_request) Has been skipped
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m41s
pr / nix flake check (pull_request) Successful in 1m35s
pr / cargo build / clippy / test / deny (pull_request) Successful in 2m26s
pr / wire schema is compatible with the base (pull_request) Successful in 3s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 26s
windows / cargo clippy (Windows cross) (pull_request) Successful in 14s
05486a1249
The fallback chain puts the symbol faces before the emoji face so that a
text-presentation symbol such as ⏸ or ✳ resolves mono. A character whose
default presentation is emoji (⭐, ⚡, ☕, ⌚, 🀄) went through the same
first-coverage walk, so wherever a symbol face also covered it, it was
drawn as a small monochrome glyph in its two cells instead of the emoji
the application printed.

Such a character now takes the first color face that covers it, the
primary and any explicit `font.fallback` order notwithstanding, and
falls back to first coverage only when no color face covers it. This is
what kitty's font_for_cell does. A VS15 after it asks for text, so the
cluster path resolves that base by first coverage again.

The default presentation is a property of the scalar alone, so it is
decided in single-codepoint resolution, from an Emoji_Presentation table
generated from Unicode 17.0.0 emoji-data.txt, the version unicode-width
encodes; a test checks that every scalar in it but a lone regional
indicator is wide.

Rejected: keep first coverage and center the glyph. It fixes the
alignment but leaves the emoji monochrome, and makes color depend on
which symbol fonts a host has installed.

Assisted-by: Claude Code
natsukium force-pushed fix/emoji-presentation-face from 05486a1249
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 9s
darwin / darwin docs-only no-op (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 11s
windows / windows docs-only no-op (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 2m23s
bench / Criterion regression gate (pull_request) Failing after 4m9s
windows / cargo nextest (Windows) (pull_request) Failing after 3m28s
windows / frontend smoke (Windows) (pull_request) Has been skipped
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m41s
pr / nix flake check (pull_request) Successful in 1m35s
pr / cargo build / clippy / test / deny (pull_request) Successful in 2m26s
pr / wire schema is compatible with the base (pull_request) Successful in 3s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 26s
windows / cargo clippy (Windows cross) (pull_request) Successful in 14s
to c3ed6b0e4d
Some checks failed
bench / Criterion full-suite snapshot (pull_request) Has been skipped
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 8s
windows / detect relevant changes (pull_request) Successful in 10s
darwin / darwin docs-only no-op (pull_request) Has been skipped
windows / windows docs-only no-op (pull_request) Has been skipped
windows / frontend smoke (Windows) (pull_request) Successful in 35s
windows / package felis (x86_64-pc-windows-msvc) (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 1m5s
windows / cargo nextest (Windows) (pull_request) Successful in 3m17s
bench / Criterion regression gate (pull_request) Failing after 1m43s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 1m6s
pr / nix flake check (pull_request) Successful in 38s
pr / cargo build / clippy / test / deny (pull_request) Successful in 2m19s
pr / wire schema is compatible with the base (pull_request) Successful in 3s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 26s
windows / cargo clippy (Windows cross) (pull_request) Successful in 13s
2026-10-06 13:10:59 +09:00
Compare
Author
Owner

The Criterion gate failure is unrelated to this change: the benched crate (felis-client-core) does not depend on felis-shaping, the only crate the change touches besides felis-render-wgpu tests. The two runs flagged different benches (daemon_side/shell_prompt +64%, then multi_cycle_end_to_end/plaintext_scroll_individual +15.3%), both while the shared runner host was loaded. All other checks pass.

The Criterion gate failure is unrelated to this change: the benched crate (felis-client-core) does not depend on felis-shaping, the only crate the change touches besides felis-render-wgpu tests. The two runs flagged different benches (daemon_side/shell_prompt +64%, then multi_cycle_end_to_end/plaintext_scroll_individual +15.3%), both while the shared runner host was loaded. All other checks pass.
natsukium deleted branch fix/emoji-presentation-face 2026-10-06 13:48:59 +09:00
Sign in to join this conversation.
No reviewers
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set

Reference
felis-terminal/felis!31
No description provided.