render: resolve cell appearance per row of cells, shared with the painter #176

Merged
natsukium merged 4 commits from render/cell-appearance into main 2026-10-18 22:48:28 +09:00
Owner

The cell painter decides how a cell looks (reverse, cursor and its color, selection, the bidi warning, faint, SGR 58, link underline, CONCEAL, preedit cover, reserved rows), but only over a ScreenBuffer. felis-web-component re-derives those rules in TypeScript and already disagrees with native (faint ×0.6 toward black vs 50/50 toward the cell bg; no selection, block-cursor, bidi-warning or SGR 58 rules), and its scrollback rows exist only as decoded packed rows, never in a ScreenBuffer. This lifts the per-cell rules into a row-level resolver that the painter itself calls, and makes it buildable for wasm32.

  • felis_render_core::appearance: RowCells (a cell slice + StyleTable + ClusterTable, blank past the slice; of_screen builds it from a screen row), FrameState → per-row RowFrame (plain for scrollback), resolve_cell/resolve_row → CellAppearance (fg/bg, bg_is_ground, glyph_visible, disposition, cursor marker with its color, link underline, underline/strikethrough/overline each with its effective color after suppression). The painter keeps only pixel math.
  • glyphs feature (default on) gates painter, glyphs, box_drawing and the felis-shaping/foldhash edges; appearance, palette, display_list, overlay, atlas build without them. just check-portable (pr.yml, release.yml) now runs cargo clippy -p felis-render-core --no-default-features --target wasm32-unknown-unknown.
  • palette::linear_to_srgb_byte / linear_rgba_to_srgb8 so a DOM client writes the bytes the native sRGB surface would.

Evidence

  • #170's golden fixtures pass without regeneration; existing painter tests unchanged except imports.
  • Proptest: random SGR/wide/bidi/combining/OSC 8/erase rows, encode_row → decode_row into a fresh StyleTable (the browser's scrollback path) resolve identically to the live row, under a plain frame and a live cursor + selection frame. Targeted tests for the suppression rules, preedit Skip, stale tail past occupancy, short slices; exhaustive sRGB round trip.
  • paint_frame (full runs, alternating, base = main): full 200x60 −27%, one dirty row −25%, cursor move −25%; cells now come from the row slice instead of a per-access ScreenBuffer::cell, and char_span runs only when a selection or preedit is on the row.
  • just check and just check-windows green locally; sol review PASS (its one non-blocking doc finding fixed in the last commit).

The web client adopts it in its own repository next: first a pin bump to this merge, then emitting resolved colors in its row JSON.

The cell painter decides how a cell looks (reverse, cursor and its color, selection, the bidi warning, faint, SGR 58, link underline, CONCEAL, preedit cover, reserved rows), but only over a `ScreenBuffer`. felis-web-component re-derives those rules in TypeScript and already disagrees with native (faint ×0.6 toward black vs 50/50 toward the cell bg; no selection, block-cursor, bidi-warning or SGR 58 rules), and its scrollback rows exist only as decoded packed rows, never in a `ScreenBuffer`. This lifts the per-cell rules into a row-level resolver that the painter itself calls, and makes it buildable for wasm32. - **`felis_render_core::appearance`**: `RowCells` (a cell slice + `StyleTable` + `ClusterTable`, blank past the slice; `of_screen` builds it from a screen row), `FrameState` → per-row `RowFrame` (`plain` for scrollback), `resolve_cell`/`resolve_row` → `CellAppearance` (fg/bg, `bg_is_ground`, `glyph_visible`, `disposition`, cursor marker with its color, link underline, underline/strikethrough/overline each with its effective color after suppression). The painter keeps only pixel math. - **`glyphs` feature** (default on) gates `painter`, `glyphs`, `box_drawing` and the felis-shaping/foldhash edges; `appearance`, `palette`, `display_list`, `overlay`, `atlas` build without them. `just check-portable` (pr.yml, release.yml) now runs `cargo clippy -p felis-render-core --no-default-features --target wasm32-unknown-unknown`. - **`palette::linear_to_srgb_byte` / `linear_rgba_to_srgb8`** so a DOM client writes the bytes the native sRGB surface would. **Evidence** - #170's golden fixtures pass without regeneration; existing painter tests unchanged except imports. - Proptest: random SGR/wide/bidi/combining/OSC 8/erase rows, `encode_row` → `decode_row` into a fresh `StyleTable` (the browser's scrollback path) resolve identically to the live row, under a plain frame and a live cursor + selection frame. Targeted tests for the suppression rules, preedit `Skip`, stale tail past occupancy, short slices; exhaustive sRGB round trip. - `paint_frame` (full runs, alternating, base = main): full 200x60 −27%, one dirty row −25%, cursor move −25%; cells now come from the row slice instead of a per-access `ScreenBuffer::cell`, and `char_span` runs only when a selection or preedit is on the row. - `just check` and `just check-windows` green locally; sol review PASS (its one non-blocking doc finding fixed in the last commit). The web client adopts it in its own repository next: first a pin bump to this merge, then emitting resolved colors in its row JSON.
A browser client wants the palette, the display-list types and the
cell-appearance rules without the glyph rasterizer, which needs
felis-shaping and its system font provider; felis-shaping does not
build for wasm32. The default-on `glyphs` feature gates the three
modules that import it, so a `default-features = false` consumer links
only felis-grid and felis-protocol. A wasm arm in felis-shaping was
rejected: it would commit that crate to a target with no font provider
and pull its rasterizer stack into a tree that never calls it.

check-portable lints the gated build on wasm32 so a stray import of a
glyphs-only item fails CI rather than the downstream web build.

Assisted-by: Claude Code
A browser client renders the same cells as the painter but re-derives
how each looks, and its copy has drifted (faint toward black, no
conceal, no DECSCNM, no bidi warning, no SGR 58). The rules now live in
`appearance::resolve_cell`, which the painter calls and a DOM client
can call over wasm, so there is one spelling of the precedence chain.

The input is a slice of cells plus a style and cluster table rather
than a `ScreenBuffer`: every rule is row-local, and a browser's
scrollback rows exist only as decoded packed rows. Colors are resolved
even for a skipped pre-edit column because the painter slices a glyph
inking into it by that column's fg. `linear_to_srgb_byte` lets a DOM
client mix faint in linear light and still write the bytes the native
sRGB surface would.

Assisted-by: Claude Code
The crate map, the browser reuse path and the CI description must say
what a wasm32 build of felis-render-core contains, since the browser
client now has a renderer crate to link. The explanation pages record
why the resolver takes a row of cells instead of a ScreenBuffer: the
screen-shaped signature is the one a contributor would reach for, and
nothing in the signature shows that a browser's scrollback rows exist
only as decoded packed rows.

Assisted-by: Claude Code
docs: describe the resolver's browser use as a capability
All checks were successful
fuzz / cargo fuzz nightly long-run (pull_request) Has been skipped
darwin / detect relevant changes (pull_request) Successful in 10s
darwin / darwin docs-only no-op (pull_request) Has been skipped
windows / detect relevant changes (pull_request) Successful in 19s
windows / windows docs-only no-op (pull_request) Has been skipped
darwin / build felis (aarch64-darwin) (pull_request) Successful in 2m37s
bench / instruction-count regression gate (pull_request) Successful in 46s
fuzz / cargo fuzz smoke (per target) (pull_request) Successful in 51s
windows / cargo nextest (Windows) (pull_request) Successful in 2m36s
windows / frontend smoke (Windows) (pull_request) Successful in 48s
pr / nix flake check (pull_request) Successful in 1m19s
pr / cargo build / clippy / test / deny (pull_request) Successful in 1m31s
pr / wire schema is compatible with the base (pull_request) Successful in 4s
pr / frontend smoke (x86_64-linux) (pull_request) Successful in 27s
windows / cargo clippy (Windows cross) (pull_request) Successful in 15s
ddc3841db3
felis-web-component adopts the resolver in its own repository later;
until then the reuse-paths table keeps listing what it links today, and
the pipeline page says what a font-drawing client can call instead of
claiming the browser client already does.

Assisted-by: Claude Code
natsukium deleted branch render/cell-appearance 2026-10-18 22:48:28 +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!176
No description provided.