#473

feat(canvas): widget painters and custom-control seams

Openrdevwants to merge intofrom

Summary

  • Widget painters. Widget.paint is an optional painter that emits a widget's chrome in place of the kind's built-in visuals, with an eight-float paint_data payload for its state. The SDK keeps layout, children (emitted after the painter in both render walks), input, focus, semantics, and invalidation. The painter pointer and payload take part in retained invalidation. Editable kinds keep their text alignment and read a positive paint_data[7] as the painted well's text inset.
  • Nested emission restores tree state. A painter that re-enters emitWidgetTree for a nested widget (a field's editor, an icon in a chip) used to leave the thread-local scrim viewport at the nested frame, so later modal scrims covered that frame instead of the window, and it cleared the culling bounds for the rest of the outer tree. Both are now saved and restored.
  • Painted controls keep built-in semantics. A painted button with authored horizontal padding measures to that padding instead of the 44 pt house floor. A painted button group stamps its children's segments in the static walk too. A painted input group resolves focus-within before its painter runs.
  • controls.container_feedback. Lets a theme whose surfaces own their feedback keep an actionable row's authored background through hover and press.
  • Builder.allocGradientStops. Emit-computed gradient stops get builder-owned storage, like allocPathElements. A stack-local stop array otherwise dies before the display list that references it. The retained per-view gradient stop pool now matches the builder's budget, so views with more than 32 two-stop gradients no longer freeze when scrolled into view.
  • WidgetStyle.cursor. Overrides the cursor a widget's kind implies, for example a divider built from containers or a drag handle. Hit records carry it, and resize_vertical joins resize_horizontal.
  • widget-action <view> <id> hover. Automation can hover widgets, making hover states, cursors, and delayed tooltips testable headlessly.

Notes for review

  • The painter API is the part most worth discussing: a function pointer plus a fixed [8]f32 payload, compared by value for invalidation. It keeps Widget a plain value type, but a typed or tagged payload is a reasonable alternative.
  • The retained Widget footprint goes from 776 to 816 bytes.
  • The cursor override and the hover action don't depend on the painter work and can be split out if the painter discussion takes a while.

Verification

  • zig build test on macOS with Zig 0.16.0: passed.
  • scripts/gate.sh fast: every step passed except examples-mobile, which needs the Android NDK. The NDK isn't installed here, and upstream main fails the same way. surface-claims needed npm ci --include=dev in packages/core and passed when rerun. docs-check passed.
  • New tests: a widget painter replaces the built-in chrome and its children still render, a painter's nested widget tree leaves later modal scrims at the root (fails without the fix), retained gradient controls survive repeated native scroll reversals, and retained gradients support the builder budget and preserve the frame on overflow.

Activity

10
vercel[bot]1mo ago

@rdev is attempting to deploy a commit to the Vercel Labs Team on Vercel.

A member of the Team first needs to authorize it.

vercel[bot]reviewed1mo ago
vercel[bot]1mo ago

Additional Suggestion:

Runtime cursor path returns pointing_hand for a link with an authored style.cursor, ignoring the override, while the canvas layer honors it — the two paths disagree.