#472

feat(canvas): letter spacing, authored text size and line height, and rounded content clips

Openrdevwants to merge intofrom

Summary

  • Letter spacing. Paragraphs take a tracking option (Widget.text_tracking), applied as one advance quantum per cluster at the span measurement seam, so intrinsic size, line breaking, and the painted run agree. DrawText carries it: AppKit applies it as NSKern, Direct2D as IDWriteTextLayout1 character spacing, and the reference renderer in its per-cluster walk. Ink and metric bounds include the positive tracking extent, so a tracked run's tail is not clipped.
  • Authored text size. WidgetStyle.text_size is an optional exact point size, used ahead of the theme's size rung, so layout, caret and selection metrics, and paint agree on one size. Invalid values fall back to the rung, and buttons measure their labels at it.
  • Authored line advance. Paragraphs take a line_height option (Widget.text_line_height) that sets the span layout's line advance. 0 keeps the natural height.
  • Rounded content clips on every backend. RenderPlanner.pushClip kept only the clip rect, so a rounded clip_content surface cut its children square on every backend. The planner now carries a RenderClip (rect plus radii) through its state stack and composes nested clips exactly. A command receives radii only when its own geometry reaches an arc, so everything else keeps the plain-rect path.
    • The reference rasterizer applies per-pixel coverage.
    • AppKit clips to a rounded path. The Metal solid-quad fast path declines rounded clips.
    • Direct2D pushes a geometric-mask layer.
    • Content clipping follows the authored radius over the per-kind default. Cover-fit images and revealing accordions use the same rounded clip.

Notes for review

  • Wire format v6 covers both tracking (an f32 on text records) and clip radii (a presence byte, then four radii when set). JSON emits tracking and clipRadius only when non-zero, so existing goldens stay byte-identical. The build.zig wire-format check now also pins the AppKit and Windows decoder version gates, not just their prose.
  • The retained Widget footprint goes from 776 to 792 bytes.

Verification

  • zig build test on macOS with Zig 0.16.0: passed, including at every intermediate commit through zig build test-canvas.
  • The Windows Direct2D renderer compiles through zig build test-windows-web-layer-audit, which cross-compiles ui-inbox and webview. It has not been run on Windows.
  • 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.
  • New tests: a rounded content clip rounds what it clips, a rounded clip only costs the commands that reach a corner, nested clips keep each corner's own radius, letter spacing widens a span paragraph by one quantum per cluster, display list omits tracking at the default and serializes it otherwise, and cover images and revealing accordions clip at their authored radius. The last one fails without its fix.

Activity

9
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:

Frame diff in widgetChange never compares the new text_tracking/text_line_height fields, so a frame changing only letter spacing or line height is treated as unchanged and neither re-laid-out nor repainted (stale render).