#475

feat(canvas): side-anchored overlays, docked modals, modal dismissal, menu safe triangle, and composed tooltips

Openrdevwants to merge intofrom

Summary

  • Side anchors. Anchored popovers, menus, and tooltips accept left and right placement and center alignment. Layout flips a side surface that would leave the viewport, then clamps it, so submenus open toward the free side. Markup accepts the new placement strings.
  • Edge-docked modals. Root-relative modals take an explicit edge (WidgetModalEdge) and a fractional height for horizontal panels, so drawers and sheets can dock to any side.
  • Dialog top fraction. WidgetLayoutStyle.modal_top_fraction pins a dialog's top edge at a fraction of the window, so a command palette whose results shrink keeps its top edge instead of recentering on every keystroke.
  • Dismissal fixes. A point-anchored surface (a context menu at the pointer) treated its anchor region as a trigger, so clicks there did nothing. Root-relative modals (dialogs, alerts, drawers, sheets) had no Escape or light dismiss at all. Both now dismiss, with paint order deciding which surface a gesture closes.
  • Menu safe triangle. While the pointer travels from a trigger toward the open anchored menu it owns (the hover triangle), hover holds on the trigger instead of switching to the rows the diagonal crosses. Leaving the triangle, reaching the menu, or the menu closing resumes normal hover at once, with no timer.
  • Composed tooltips. A tooltip's children lay out over its chrome, stacked like a popover's content, so a label beside a shortcut keeps the tooltip's hover intent, focus reveal, and dismissal. A text-only tooltip is unchanged. The tooltip docs and markup vocabulary are updated.
  • Checked native menu items. ContextMenuItem.checked shows the current choice in a native context menu used as a picker. AppKit renders it as the item's on state.

Notes for review

  • The retained Widget footprint goes from 776 to 784 bytes (modal_top_fraction).

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: escape dismisses a root-relative dialog through on_dismiss, an outside click dismisses a root-relative dialog and an inside click does not, menu safe triangle keeps hover on the trigger while the pointer heads for its menu, the menu safe triangle holds only while the pointer travels toward the surface, a tooltip composes child elements over its chrome, and a tooltip with children sizes to them and paints them over its chrome. The dismissal, safe-triangle, and tooltip tests fail without their changes.