feat: rebase onto upstream v0.8.4 and add five Cockpit fixes - #3
Open
phall1 wants to merge 13 commits into
Open
Conversation
* feat(canvas): add context menu policy * fix(canvas): retain secondary gesture ownership * fix(canvas): preserve full terminal viewport * fix(canvas): admit complete terminal fonts * fix(canvas): cost underlined terminal runs safely * chore: follow history-driven changelog workflow * fix(canvas): preserve context menu invariants --------- Co-authored-by: phall <phall@noreply.github.com>
A terminal is the densest widget the toolkit hosts, and the per-view frame budgets were sized for desktop chrome. A terminal row costs one display-list command per contiguous same-background run plus one per contiguous same-foreground run, so a styled 200-column row runs 30-60 commands where a whole three-pane view runs a few hundred. Against the old 2,048-command budget (1,792 after the widget chrome reserve) a 60-row viewport had ~30 commands per row to spend, and the painter degraded from the BOTTOM in silence: a realistically colored 200x60 screen (syntax highlighting, htop meters, a colored build log) painted 34 of its 60 rows and the rest showed bare background, with nothing in any log to say a budget had eaten it. Measured on a 200x60 grid at the widget tier (terminal_grid_tests.zig prints both): realistic styling (50 fg runs/row): 34/60 rows -> 60/60 truecolor (distinct fg+bg per cell): 4/60 rows -> 9/60 Changes: - canvas_limits: max_canvas_commands_per_view 2048 -> 4096 and max_canvas_text_bytes_per_view 32 KiB -> 64 KiB (a 300x100 viewport is ~30 KB of presented text before chrome, and a split holds back a share for its sibling). Both host retained-command caps pin the new number (appkit_host.m, gpu_surface_renderer.cpp), and canvas.max_display_list_text_bytes / the new canvas.max_display_list_commands stay in lockstep by test. Measured cost: ~696 B per command slot across the view's retained mirrors, so RuntimeView 3.44 -> 4.83 MiB and the 32-slot Runtime 110.0 -> 154.5 MiB of fixed-capacity address space. - Truncation is never silent. terminal_grid.paintReport returns the rows painted, the rows handed over, and the store that stopped it; paint() keeps its exact signature and every paint records budget stops on the builder (canvas.DisplayListDegradation), which the runtime turns into one teaching log line on the EDGES of a degradation rather than once per frame. - The widget emit path's frame scratch moves off the stack into the per-thread pool the frame planner already uses. At the new command budget the display list, the chrome copy store and the diff output no longer fit a stack frame under the widget emit recursion — a measured segfault inside the button emitter. What this does NOT fix, deliberately: a viewport with a distinct foreground AND background per cell merges nothing and wants two commands per cell — ~24,000 for 200x60, ~60,000 for 300x100. At ~696 B a slot that is 500 MiB and 1.3 GiB across the view slots, and a trial raise to 8,192 alone (255 MiB) crashed runtime construction. That density is not a number in canvas_limits; it needs a packed cell-grid command the host renderers expand themselves. The budgets now carry realistic styling and say so out loud when they cannot. Also: widening a clip no longer leaves stale pixels behind. The render planner erases push_clip/pop_clip into a per-command clip field, so the retained packet baseline holds no key for a clip and the refined dirty rect could not name pixels a growing clip revealed over unchanged content — the frame kept whatever the host last drew there (the stale columns a terminal pane showed after a split collapsed back to full width). The baseline now carries its clip rects and the next frame adds the difference, in both directions. Frames whose clips did not move, including tweens that move content under a stationary clip, keep their region-scoped patches. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A terminal is the one surface whose content scales with AREA rather than
with design. Painting it as display-list shapes cost one background
command per contiguous same-colour run plus one text command per
contiguous same-foreground run, which merges into nothing on a styled
screen: a 200x60 truecolor viewport wanted ~24,000 commands against a
per-view budget of 4,096 and painted nine rows of sixty, the rest bare
background. No budget raise reaches that shape — one command slot costs
~700 B across the view's retained mirrors, so 60,000 commands is 40 MiB
per view before a pixel exists.
`CanvasCommand.cell_grid` (src/primitives/canvas/cell_grid.zig) carries
the screen instead: a cols x rows lattice of 20-byte cells, each with its
own background, cluster offset, foreground, and style, which every
renderer expands itself. Geometry is implied by the index, so nothing
per-cell is stored but content.
Measured (terminal_grid_tests.zig prints both):
200x60 truecolor, distinct fg+bg per cell:
before 9/60 rows, ~1,600 commands
after 60/60 rows, 4 commands, 12,000 cells, 36 text bytes
300x100 truecolor, distinct fg+bg per cell:
after 100/100 rows, 4 commands, 30,000 cells, 585 KB, 36 text
The command count is now a constant, not a function of styling. Cluster
bytes are INTERNED, so a screen drawn from a 36-character alphabet costs
36 bytes however many cells it has.
Three bug classes close by construction:
- Reflow is safe. A screen is ONE retained key replaced wholesale, so a
row that loses a run cannot orphan a per-run command keyed by its old
start column. That was the stale-column bug — a split collapsing back
to full width left a duplicated prompt line and a truncated hostname
in the revealed columns. Verified gone by running the consuming app
against this commit and driving split/close twice.
- Cell geometry is exact. A cell's position is its index, so a combining
mark or a wide cluster can no longer advance its neighbours out of
their columns whatever the face does.
- Every SGR attribute has somewhere to live. `TerminalCell` gains bold,
italic, strikethrough, overline, six underline styles and an underline
colour; `TerminalCursor` gains `blinking` and `wide`, and
`TerminalCursorShape` gains `block_hollow` so an emulator-requested
hollow block stops colliding with the focus-driven outline. All
additive: the consuming app builds against this commit unmodified.
The reference CPU renderer is the oracle and is complete: two passes
(every background, then every glyph and decoration, so a neighbour's
background cannot erase an overhanging glyph), each cell's ink through
the same `drawGlyphBox` path a text run takes. Decoration geometry lives
in `CellDecoration` so a future host encoder reads the same source.
The GPU packet layer marks `cell_grid` unsupported, which routes terminal
frames to the CPU pixel path — the reference renderer — on every host.
That is correct everywhere from day one and needs no wire-format change;
it is also slower than a native encoder, and closing that is the next
step. Deliberately not shipping an unverified one: automation screenshots
render through the reference path, so a host encoder cannot be validated
here and a wrong one would diverge silently on the user's glass.
Also fixed, all found by the size increase:
- `paintInto` in the terminal tests returned a `Builder` BY VALUE, which
left every builder-owned slice aimed at a dead stack frame. Text runs
were small enough to survive it; a 38-cell grid was not.
- `Builder.initAt` and `CanvasDisplayListScratch.reset` replace
whole-struct assignment on the widget emit path. Both structs carry a
frame's inline storage, so `x.* = .{}` built a megabyte-scale stack
temporary and overflowed the thread.
- `runtimeViewInfo` took a multi-megabyte `RuntimeView` by value.
Memory: RuntimeView 4.83 -> 5.45 MiB (the retained cell array), so the
32-slot Runtime goes 154.5 -> 174.5 MiB of fixed-capacity address space.
The command budget could now come back down to 2048 and give ~45 MiB of
that back, since the terminal is what drove it up; that is a separate
change with its own measurement.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The packed cell grid fixed what a terminal could DRAW and broke how it
PRESENTS. `cell_grid` was unsupported by the GPU packet layer, so one
command dropped the whole view to the CPU pixel path: every frame became
a full-surface upload (~2.8 MB at 1x, 11 MB at 2x) and incremental
dirty-region patching turned off entirely.
Measured on the running app at 1100x640, through the automation
snapshot:
before after
gpu_present_path pixels -> packet
present_mode none -> patch
present_patch_bytes 0 -> 789 (1 upsert on a shell prompt)
present_fallback unsupported_command -> none (0 frames)
gpu_input_latency_ns 19,392,000 -> 5,360,000 (budget 16,666,666)
budget_exceeded 1 -> 0
Three pieces.
ONE GRID COMMAND PER ROW, not per screen. A retained command is the unit
of CHANGE: a screen-wide grid makes a keystroke re-encode and re-upload
every cell, which is the full-surface cost merely moved from the
rasterizer to the wire. A row is the granularity a terminal actually
changes at. Rows are bounded by `max_rows`, so a 300x100 truecolor
screen is 103 commands where per-run painting wanted ~60,000.
Cluster interning moved from per-SCREEN to per-ROW for the same reason,
and this one was measured the hard way: a blob shared across rows put
every row's fingerprint on every other row's characters, so one new
letter re-encoded the screen — 31 upserts and 8,449 bytes per frame.
Per-row it is 1 upsert and ~400-800.
WIRE FORMAT v6 plus the AppKit decoder. `cell_grid` is command kind 14,
its payload implied by the kind (the flag byte is full). Cells encode as
a delta stream — a tag byte per cell whose low bit means "same style as
the previous cell" — so a plain row costs about a byte a column and only
a genuinely per-cell-styled row pays the full 15. Clusters ride inline
per cell, so a row decodes without the rest of the screen.
The host renderer mirrors the reference renderer deliberately, including
its two-pass order (every background, then every glyph and decoration —
one pass lets a neighbour's background erase an overhanging glyph) and
its exact decoration geometry: all six underline styles, underline
colour, strikethrough, overline, wide cells. Stated rather than hidden:
glyph RASTERIZATION differs (CoreText vs the engine's outline filler),
as it already does for every draw_text command; `bold` and `italic` are
carried but not synthesised, matching the reference renderer rather than
getting ahead of it.
Windows moves to v6 and refuses only packets containing a cell grid (an
unknown kind fails validation), so every non-terminal frame keeps the
retained Direct2D path. Not verified on a device — no Windows here.
Also:
- `TerminalCursor.blinking` is real now instead of documented-only. The
runtime arms the same looping opacity animation a text caret uses,
keyed on the new `terminal_grid.cursorCommandId`. A painter has no
clock; blinking is time, so it was never the painter's to stamp.
- The command budget returns to 2048 (the terminal was the only reason
it went to 4096 and now costs ~100 commands), handing back ~45 MiB of
the Runtime's fixed address space. Both host retained caps follow.
- Grid command ids move to `0x60_0000 + row`, out of
`reserved_id_offset` (0x62_0000) where callers layer their focus ring
— the previous commit put the grid and the ring on the same key.
Suite: 2970 pass / 14 skip / 2984 total.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`bold` and `italic` reached `CellFlags` and every renderer and then changed no pixels: `drawCellGrid` built its run with the grid's single `font_id`. So `\x1b[1m` and `\x1b[3m` were carried honestly and drawn identically to regular. A cell grid now carries a FONT FAMILY — the regular face plus bold, italic, and bold-italic companion ids — and both renderers pick per CELL off the style flags. One row mixes weights freely. Where the faces come from: the APP registers them (`Runtime.registerCanvasFont`, ids >= 64) and names them in `DesignTokens.typography.mono_bold_font_id` / `mono_italic_font_id` / `mono_bold_italic_font_id`. The SDK bundles only GeistMono-Regular and does not presume to ship a consumer's type; an app that supplies nothing still gets visible weight, because: SYNTHESIS IS THE FALLBACK, and this commit implements it. Stated plainly rather than dressed up as real faces: a missing companion is faked — bold by drawing the glyph a second time offset by max(1, size/14) px, italic by shearing 0.2 about the baseline. Both renderers read the SAME rules (`cell_grid.CellSynthesis`) because a bold run that renders bold on the oracle and regular on the host is worse than no bold at all. A half family is used for the half it covers: a real bold face with no bold-italic is sheared rather than double-faked. Measured through the reference renderer, "mono" at one size: regular 135 inked pixels, bold 189 (+40%), italic 124 (different shape, not a second pass). Rendered a four-row sheet (regular / bold / italic / bold-italic) and looked at it: four visibly distinct weights, columns aligned identically across all four. CELL GEOMETRY DOES NOT MOVE, which is the invariant the packed cell rests on and the thing most likely to break here. A bold face has different advances; in a lattice that must change nothing, because a cell's position is its INDEX. Pinned by a test that compares every cell rect, the cell width, the baseline, and the command's raster extent across regular/bold/italic rows. Faux bold thickens ink inside the cell and faux italic shears about the baseline — neither touches the pen. Italic overhang is real ink and is protected by the existing two-pass order (all backgrounds, then all glyphs). Pinned by rendering an italic 'H' beside a bright background cell and asserting the lean survives. Wire format v7 carries the three companion ids. Windows follows the version and still refuses only packets containing a cell grid. Also: the terminal cursor blink is checked BEFORE the focus-visible gate. A text caret blinks once focus is VISIBLE (the keyboard ring); a terminal cursor blinks whenever the terminal holds focus, however it got it — a click-focused terminal would otherwise sit steady while the program that asked for `\x1b[1 q` waited. Suite: 2975 pass / 14 skip / 2989 total. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…creen verb
Two things a multi-window terminal app cannot do without.
=== A SECONDARY WINDOW CAN PAINT ITS TERMINALS ===
`rebuild` runs the app's `chrome.build` through `installChromeDisplayList`
— the path that produces every terminal cell. `rebuildWindowSlot`, the
secondary-window path, instead published the widget layout and emitted
with `WithChrome(.{})`: a chrome prefix of ZERO. So a second window
opened, laid out correctly, drew its tab strip, its split divider and
its widget bounds, and painted no cells at all. `ChromeOptions.build`
also took no window label, so even reaching it could not have helped —
it could not know which window it was painting.
- `ChromeOptions.build_window` is the per-window builder, taking a
`ChromeContext` (canvas label, window id, that window's size, tokens,
is_main). A struct rather than more parameters so the next thing a
window needs to know does not break every caller again.
- `installChromeDisplayList` is parameterized by window — label, size,
and the tree-currency flag — instead of reading the main canvas's.
- `rebuildWindowSlot` takes that branch, and so do per-window terminal
sizing (`applyTerminalLayout`) and web panes, which were also
main-canvas-only.
- `handleWindowSlotFrame` calls `on_frame` with the slot's own
`gpuSurfaceFrame`, so each window has a viewport/PTY pump.
ADDITIVE: `build` is untouched and still works. The slot path is gated
on `build_window` being set, not merely on `chrome` — a builder that
cannot name a window would paint the MAIN window's content into a
secondary one, which is a different wrong answer, not a fix. An app that
does not migrate sees no change. Migration is one line:
.build = view.buildChrome,
// becomes
.build_window = myBuildWindowChrome, // (model, builder, context)
Verified by running it: a scratch `windows_fn`/`window_view` spike in the
consuming app, built against this commit, opened a second window and
BOTH windows painted live zsh prompts simultaneously — read from the two
automation screenshots. The spike was reverted.
Also fixed on the way: the install path re-emitted with a zero chrome
prefix right after `rebuildWindowSlot`, which would have erased the
chrome it had just installed.
=== THE FULLSCREEN VERB ===
`PlatformServices` had focus/close/minimize/show and no way to enter or
leave fullscreen, so an app could be told it was fullscreen and never ask
to be. Worse, `WindowState.fullscreen` existed but nothing ever filled
it — even the window-state store persisted a constant false.
- `set_window_fullscreen_fn` on `PlatformServices`, `Runtime.setWindowFullscreen`,
`Effects.setWindowFullscreen(label, bool)` and `toggleFullscreenWindow`.
SET rather than toggle so the call is idempotent and an app can restore
a remembered layout without computing parity; macOS compares the style
mask and only calls `toggleFullScreen:` when it differs.
- The READ half now exists: `WindowInfo.fullscreen`, filled from the
window's style mask on every macOS frame emit, so a transition the USER
started from the green button reports exactly like one the app asked
for. `WindowInfo.state()` finally carries it into `WindowState`.
Chose (a) over (b), deliberately. (b) — appending the stock Enter Full
Screen item when an app supplies custom menus — is a real bug, but the
fix changes "you supplied a menu bar" from "you own it" to "you own it
plus items we inject", which is the wrong default for a framework whose
doctrine is explicitness, and it leaves an app still unable to drive
fullscreen from anything but a menu item it does not control. With (a)
in place a custom-menu app binds its own item in one line, and the
capability also serves shortcuts, buttons, and launch-state restore. (b)
remains worth doing as an opt-outable documented policy; it is not this
commit.
Suite: 2979 pass / 14 skip / 2993 total.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Every glyph the AppKit host draws lands in a CGBitmapContext, and font smoothing - macOS's stem darkening for text - is off by default on the transparent backing all three of those contexts use. The result is that ALL text renders systematically thin, which is what the faint-text report was about. Measured with phux-cockpit's scripts/measure-glyph-smoothing.m against the bundled JetBrains Mono NL, 13pt: scale 2 solid stem pixels 2341 -> 3164 (+35.2%) scale 1 solid stem pixels 635 -> 940 (+48.0%) Filling an opaque ground under the cells was also considered and is NOT included: measured on the same harness it moves solid stem pixels by -0.3% (2341 -> 2334), so it buys no weight while adding a full-screen fill to every frame. The CPU reference renderer never touches CoreText and blends coverage itself, so it does not share the defect and no reference screenshot can catch a regression here. Re-measure rather than eyeball. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
A closed window keeps its runtime table slot until its label or id is re-created -- `removeWindowAt` runs at re-creation, not at close -- so `self.windows[0..window_count]` still carries every window the app has ever declared, with `info.open = false` on the dead ones. The snapshot walked that span unfiltered and published a `window @wN` line for each, while `listViews` and `appendAutomationWidgets` correctly found nothing under them: a ghost window with no views. That misleads exactly the assertion a smoke test wants to make. A test that opens a settings window, closes it, and asserts the window COUNT returned to one passed against a window that is not on screen, and a test asserting the reconciled window set matched saw closed windows in it forever. Filter on `info.open` and advance the output index only for the windows that survive, so the published slice is the windows that are actually up. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`widget-key` dispatched a `key_down` and nothing else, so every key-lifetime latch it armed stayed armed. Those latches exist because a physical chord's release carries different modifier flags than its press (Cmd is often already up by the time G comes back), so the classification is latched on the view at the down and retired at the up -- `consumeCanvasWidgetTabInputFocusEntry`, `consumeCanvasWidgetTerminalPasteKeyLifetime`, and any app-level shortcut latch built the same way. With no release ever arriving, the SECOND drive of the same chord found the latch still held from the first and was swallowed as the missing release: `widget-key canvas cmd+g` twice in a row did the work once. Fixed by synthesizing the release, not by adding an explicit release action. Real hardware has no press without a release, so a harness able to emit an unpaired one is a harness able to reach states no user can; and pairing keeps every existing automation script's wire format AND its meaning intact -- one `widget-key` line is still one keystroke. A new release verb would instead have made correctness opt-in and left every script written before it driving half a keystroke. This is the discipline `widget-drag` (down/drag/up) and `widget-pinch` (begin/change/end) already follow, down to sharing one timestamp across the events because press and release are one gesture. The release carries the chord's modifiers but never `text`: `key_up` is deliberately barren in both text paths (`canvasWidgetTextEditEventFromGpuInput` and the target-less commit fallback), so it cannot double-insert what the press already typed. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`ChromeOptions.build_window` takes a `ChromeContext` naming the window it
is painting; `Options.web_panes` took only the model. But panes are
reconciled PER WINDOW -- `applyWebPanes` runs from the main rebuild, from
every secondary window slot's rebuild, and from the presented-frame
ride-along -- so a hook that could not be told which window it was being
asked about had no choice but to answer with the whole app's pane set
every time.
A webview belongs to exactly one window. Every OTHER window's rebuild
therefore resolved that pane's anchor against a widget tree that does not
contain it and logged
webview pane '<name>': no canvas widget carries semantics label ...
Behaviour was never wrong (the pane is found and snapped in the window
that owns it), but the warning fired on every rebuild of every other
window, which is the kind of noise that trains people to stop reading the
log.
Pass the same `ChromeContext`, built the same way `installChromeDisplayList`
builds it, so an app switches on one context shape whichever per-window
hook it implements: return the panes this window owns, and 0 for a window
that hosts none.
Changed in place rather than added alongside, unlike the `build` ->
`build_window` migration: `build` is a required field that every chrome
app already implemented, while `web_panes` is optional and its every
in-repo implementor is updated here (the ui-app preview test, and the
canvas-preview, workbench, and split-collapse examples). One hook with a
context beats two hooks with a precedence rule.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`semantics.hidden` was the only way to say "keep this out of the accessibility tree", and it also suppresses painting. Authors reaching for it on chrome — a search field's magnifier glyph, a rendered caret — got a blank gap where the glyph should be, and no way to tell the two intents apart. Add `WidgetSemantics.decorative`: the widget and its subtree leave the semantic tree (and with it automation snapshots) and can never take focus, while paint, layout, hit-testing, and event routing are untouched. It is the `aria-hidden` / `role="presentation"` counterpart. `hidden` is unchanged and now documented for what it actually is: a VISIBILITY flag that keeps the layout box — space reserved, siblings not reflowed — while dropping paint, hit-testing, focus, and announcement. That is exactly what an empty fixed-width slot wants (room held for an affordance that is not currently showing) and what `Ui.nav`'s retained inactive pages want, so every existing call site keeps its behavior: the two engine uses that STAMP the flag (nav retain, anchored-tooltip visibility), the paint/hit-test/routing/dismissal walks that read it, and app-side spacers alike. Both flags now answer one predicate, `WidgetSemantics.concealedFromAccessibility`, so the announcement side cannot drift. The a11y audit's `nodePainted` becomes `nodeAnnounced` and skips decorative subtrees — a glyph declared as decoration must not then be reported as an unnamed image — and `widgetSemanticsEqual` gains the field, so flipping `decorative` marks semantics dirty without a repaint. The field fits existing padding: `@sizeOf(Widget)` stays 776. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
`native-sdk.os.openUrl` was a webview bridge command and `openExternalURLIfAllowed:` a webview external-link policy decision, so a Zig core with no web content had no way to ask the OS to open a URL — the platform verb existed on every host (`PlatformServices.openExternalUrl`, NSWorkspace on macOS) with nothing app-facing wired to it. `fx.openUrl(url)` closes that. It follows `showNotification`, the channel's other fire-and-forget platform-service effect: validated on the loop thread, one synchronous platform call, no terminal Msg (the OS owns whether a handler launched, so a success Msg would over-promise), inert under fake execution and session replay. The URL is treated as hostile, because cores build them from terminal output, fetch bodies, and pastes. `validation.validateOpenUrl` refuses whole — never trimmed or coerced into something openable — an empty URL, one past `platform.max_external_url_bytes`, one carrying a NUL or any other control byte / whitespace / DEL, and any scheme outside the allowlist (`http`, `https`, `mailto`, matched case-insensitively so a shouty scheme is honored and a shouty `JavaScript:` still is not). `file:` and `javascript:` are refused by not appearing in it. Unlike `runtime.openExternalUrl`, the effect is not gated on the webview external-link policy: that policy governs links web content follows, while this call comes from the app's own `update`. A test observes it the way the notification effect is observed — bind the null platform's services and read `lastExternalUrl()`, which records the request without opening anything; a refused URL leaves it empty. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
canvasGpuCommandFingerprint decides whether a retained canvas command must be re-encoded and sent to the GPU host. It hashed kind, bounds, opacity, stroke_width, cap, id, clip, transform, shape, paint, image, text and effect -- and never read command.cells. The cell_grid arm of gpu.zig puts ALL terminal content into .cells and leaves shape, paint, text, image and effect empty; CellGrid.bounds() is purely geometric. So a cell_grid row's packet fingerprint was INVARIANT under every possible content change. Downstream, canvas_frame.zig sets upserts[index]=false on a fingerprint match and then skips re-encoding, and appkit_host.m calls rasterCacheRemoveKey only for evicted or upserted keys -- so the host re-blits the raster it already holds. A terminal emits one cell_grid PER ROW under a stable key, so once a retained baseline existed a row's glyphs could never change on glass again. Content only appeared when something else forced a full present. Reported downstream as: terminal output not appearing when it should, an occasional prompt landing at random, and typed text never showing. Confirmed fixed on the real app by the reporter. The cursor is a separate fill_rect whose bounds and paint ARE hashed, so it kept moving over stale pixels -- which is why this read as "the text is the same colour as the background" rather than as a stuck frame. It also defeated every instrument used to chase it: the CPU reference renderer re-rasterizes from the content-aware display list, so reference screenshots always looked correct, and a background colour change alters a fill_rect PAINT, which IS hashed, so that one did reach the glass. The fix mirrors render_fingerprints.zig's cellGridFingerprint, which already hashes the grid identity plus std.mem.sliceAsBytes(cells) over a full screen every frame, so the cost is known-acceptable. canvas_frame_patch_tests.zig now covers it: the fingerprint must differ for a cluster change, a colour change, and a flag change. Seen failing before the fix (exit 1) and passing after. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Brings this fork's
mainup to upstream v0.8.4 and restacks the Cockpit patches on top, plus five new fixes.Targets this fork's own
main— notvercel-labs/native. Nothing here is proposed upstream.zig build testexit 0.scripts/gate.sh fastpassing (docs-install/docs-checkneedpnpm, absent on this machine; run manually they pass).Rebase
Was
cockpit/v0.8.3, based on upstream v0.8.3. Now rebased onto upstreammain(v0.8.4 + 1). One conflict, insrc/runtime/view.zig: upstream added drag fields and Cockpit added secondary-gesture fields at the same struct offset. Resolved as the union of both — neither side's fields were dropped.New in this branch
fix(macos): enable font smoothing so glyphs render at full weight. Every glyph the AppKit host draws lands in aCGBitmapContext, and font smoothing — macOS's stem darkening — is off by default on the transparent backing all three of those contexts use.CGContextSetShouldSmoothFontsappeared zero times in the repo, so all text rendered systematically thin.Measured with the consuming app's harness against the bundled JetBrains Mono NL, 13pt:
An opaque per-cell ground under the glyphs was considered and rejected: on the same harness it moves solid stem pixels by −0.3%, so it buys no weight while adding a full-screen fill to every frame. The CPU reference renderer never touches CoreText and blends coverage itself, so it does not share the defect and no reference screenshot can catch a regression here — re-measure rather than eyeball.
feat(ui-app):web_panestakes aChromeContext, likebuild_window.fix(automation): drop closed windows from the snapshot's window list. A closed window keeps its runtime slot until re-creation, so the snapshot published ghostwindow @wNlines with no views under them — which would quietly pass any smoke test asserting a window count.fix(automation): pairwidget-key's press with its release. The harness emitted onlykey_down, so the shortcut latch swallowed every repeated chord — driving cmd+G twice did nothing the second time. Paired rather than adding a release verb: real hardware has no press without a release, and a new verb would have made correctness opt-in while leaving every existing script driving half a keystroke.feat(canvas): splitdecorativefromhiddenin widget semantics.hiddensuppressed rendering, not just the accessibility node — which is why a search field's magnifier and caret painted nothing until it was removed from both.hiddenkeeps its meaning (and still reserves layout, which downstreamtabAttentionMarkerrelies on);decorativepaints and hides from accessibility instead.feat(effects):fx.openUrl(url). The platform seam already reached[[NSWorkspace sharedWorkspace] openURL:]end to end — the gap was purely that nothing app-facing was wired to it, so the only callers were the webview bridge and a runtime path gated on webview link policy. No ObjC changes were needed. Validated against a scheme allowlist (http, https, mailto) with control bytes and whitespace refused whole rather than trimmed, so ahttps://ok\0javascript:…splice is rejected rather than accepted as its prefix.🤖 Generated with Claude Code