git.lucas.co / cce-ui
GPU-accelerated UI toolkit (Vulkan)
git clone https://git.lucas.co/cce-ui.git

src/widget/model.rs (92.2K)

   1 //! Narrow, single-concern widget traits + an adapter into the legacy `WidgetHost` tree — Phase 5 of
   2 //! the core rebuild (see `docs/rfc-core-rebuild.md` §3.5 and §5).
   3 //!
   4 //! Phase 5 replaces the ~123-method [`WidgetHost`] god-trait with small traits, one per concern. A
   5 //! *non-breaking supertrait carve-out* of `WidgetHost` is not possible in Rust, for two reasons found
   6 //! by experiment:
   7 //!
   8 //! 1. The structural methods the layout/paint passes need (`rect`, `children`, `set_rect`, …) are
   9 //!    overridden in dozens of widgets across cce-ui **and** the app crates. Moving them off
  10 //!    `WidgetHost` breaks every override; merely *declaring* them on a supertrait breaks every call
  11 //!    site too, because a supertrait method is always in scope on the subtrait — `elem.children()`
  12 //!    on a `&dyn WidgetHost` becomes ambiguous.
  13 //! 2. Trait-object coercion does not offer a way around it: a blanket "view" impl
  14 //!    `impl<T: WidgetHost> Paint for T` does **not** let `&dyn WidgetHost` coerce to `&dyn Paint`
  15 //!    (that coercion only exists for real supertraits).
  16 //!
  17 //! So we take the RFC's recommended **adapter** path. The traits here — [`Layout`] and [`Paint`] —
  18 //! are *independent* of `WidgetHost` (no super/sub relationship). A widget written against them is
  19 //! placed into the existing `*mut dyn WidgetHost` tree by wrapping it in [`Adapted`], whose `WidgetHost`
  20 //! impl forwards each legacy method to the matching narrow-trait method and supplies the
  21 //! [`Widget`] base that `WidgetHost`'s rect/id/dirty machinery reads. Existing `impl WidgetHost` widgets
  22 //! are untouched; new or migrated widgets implement only the concern traits they need; both kinds
  23 //! coexist in one tree. When the last widget is migrated, `WidgetHost` and this adapter are deleted.
  24 //!
  25 //! This commit lands the two concerns the scene passes already consume: [`Layout`] drives
  26 //! [`crate::scene::bridge`] and [`Paint`] drives [`crate::scene::painter`]. The input/event
  27 //! concern follows in its own commit.
  28 
  29 use crate::scene::layout::{Rect, Size};
  30 use crate::scene::paint::{PaintCtx, Prim};
  31 use crate::widget::{
  32     WidgetHost, Event, TextLabel, UiContext, Widget, WidgetId,
  33 };
  34 
  35 /// Layout inputs for the scene layout engine — the RFC's `Widget` concern, named `Layout` here to
  36 /// avoid the existing [`Widget`] base struct.
  37 pub trait Layout {
  38     /// Intrinsic content size of a leaf (e.g. measured text), consumed by the adapter's
  39     /// `measure` (gated on [`Layout::intrinsic_measure_width`]).
  40     fn intrinsic_size(&self) -> Option<Size> {
  41         None
  42     }
  43 
  44     /// Whether this widget's base label IS its content — the text a `Button` face, a
  45     /// `Checkbox` row or a `Label` draws itself — rather than a control label, which
  46     /// the adapter draws detached above the content (the one convention for every
  47     /// labeled control: `layout::control_label_strip` tall, at
  48     /// [`detached_label_inset`](Layout::detached_label_inset)). Inline-label widgets
  49     /// carry no label strip and get no content-rect inset.
  50     fn inline_label(&self) -> bool {
  51         false
  52     }
  53 
  54     /// Horizontal inset of the detached base label: [`crate::layout::DETACHED_LABEL_INSET`]
  55     /// for every control, so a column of labels is one line and the carve-out tabs
  56     /// (which hug the label at this inset) sit under their labels.
  57     fn detached_label_inset(&self) -> f32 {
  58         crate::layout::DETACHED_LABEL_INSET
  59     }
  60 
  61     /// Whether `WidgetHost::measure` should prefer [`intrinsic_size`](Layout::intrinsic_size)'s
  62     /// width over the current rect width (Dropdown's `auto_width` measure override — hosts size
  63     /// it from `measure`, e.g. cce-system-interface' page dropdown). Default: keep the legacy
  64     /// `WidgetHost::measure` width (the current rect's).
  65     fn intrinsic_measure_width(&self) -> bool {
  66         false
  67     }
  68 
  69     /// Whether the adapter's hit test substitutes the base row rect (`row_x`/`row_w`, pushed in
  70     /// by row-layout hosts via `set_row_rect`) — the legacy
  71     /// `WidgetHost::hit_test` default geometry. Migrated controls so far dropped it (accepted
  72     /// drift); TextBox restores it (cce-files' save-name box relies on row hits). Default: off,
  73     /// keeping the other migrated widgets exactly as they shipped.
  74     fn hit_row_rect(&self) -> bool {
  75         false
  76     }
  77 
  78     /// Adjust a row-rect assignment before it lands on the base (`WidgetHost::set_row_rect` —
  79     /// TextBox clamps the row width to its `width`/`max_width`). Default: identity.
  80     fn adjust_row_rect(&self, x: f32, w: f32) -> (f32, f32) {
  81         (x, w)
  82     }
  83 
  84     /// The final base rect landed from a `set_rect`, visible or not — unlike
  85     /// [`arrange_children`](Layout::arrange_children), which the adapter gates on visibility.
  86     /// TextBox caches it (its cursor/scroll math reads the laid-out rect between events) and
  87     /// re-clamps its scroll, the legacy `set_rect` side effect. Default: ignore.
  88     fn rect_assigned(&mut self, _rect: Rect) {}
  89 
  90     // --- Container concern (transitional). Legacy containers own `Vec<*mut dyn WidgetHost>`
  91     // children (child-arranging `set_rect` has no ctx to reach the tree) and every one
  92     // hand-copies the same subtree plumbing: geometry/text aggregation, tick/popover/text-item
  93     // recursion, hit-through-children. A migrated container keeps the pointer Vec in its model
  94     // (exposed through these hooks) and the ADAPTER does the shared plumbing once, filtered by
  95     // `child_visible`. What stays per-widget: child arrangement (`arrange_children` /
  96     // `layout_children_ctx`) and any event proxying (in `on_event`, via `EventCtx::ui`).
  97     // Dies with `WidgetHost`: the arena owns the tree and the scene walk owns recursion.
  98 
  99     /// Whether this widget is a container serving
 100     /// [`container_children`](Layout::container_children). Cheap gate, checked per getter.
 101     fn has_container_children(&self) -> bool {
 102         false
 103     }
 104 
 105     /// The container's child pointers, in stacking order.
 106     fn container_children(&self) -> Vec<*mut (dyn WidgetHost + 'static)> {
 107         Vec::new()
 108     }
 109 
 110     /// Adjust a rect assignment before it lands on the base (Switcher clamps to its parent).
 111     /// Default: identity.
 112     fn adjust_rect(&self, requested: Rect) -> Rect {
 113         requested
 114     }
 115 
 116     /// Position children after a `set_rect` (no ctx available — use the owned pointers).
 117     /// Called only while the widget is visible, matching the legacy overrides. `host` is the
 118     /// adapter's `*mut dyn WidgetHost` — widgets that embed a legacy child (MenuBar's
 119     /// ButtonStrip) parent it back to the host so legacy parent-chain styling walks work.
 120     fn arrange_children(&mut self, _rect: Rect, _host: *mut (dyn WidgetHost + 'static)) {}
 121 
 122     /// Per-child visibility policy for the adapter's subtree plumbing (Switcher exposes only
 123     /// the active child). Default: every child.
 124     fn child_visible(&self, _child: *mut (dyn WidgetHost + 'static)) -> bool {
 125         true
 126     }
 127 
 128     /// Legacy `WidgetHost::z_index` (host render ordering; MenuBar's dropdowns layer at 100+).
 129     fn z_order(&self) -> i32 {
 130         0
 131     }
 132 
 133     /// Republish value-embedded legacy children into the ctx registry (Paginator's ButtonStrip
 134     /// + Pages). Legacy value-owning containers re-registered their children on EVERY `tick` and
 135     /// `layout` because the children's addresses move with the owning struct (host struct moves,
 136     /// `Vec` reallocation) — and the registration is load-bearing: the spatial grid is rebuilt
 137     /// from registered widgets, and it is the registered ButtonStrip (whose
 138     /// `blocks_root_plate_drag` is true) that makes the sidebar block root plate drags. The
 139     /// adapter calls this from `WidgetHost::tick` and `WidgetHost::layout`, mirroring the legacy
 140     /// cadence. `host_id` is the adapter's id, for `link_ids`. Default: nothing embedded.
 141     fn register_embedded_children(&mut self, _host_id: WidgetId, _ctx: &mut UiContext) {}
 142 }
 143 
 144 /// The paint concern — a widget's fill color, its own (non-recursive) geometry emission, and
 145 /// whether it clips its children. Mirrors `WidgetHost::color` / `paint_self` / `clips_children`, but
 146 /// [`paint`](Paint::paint) receives the laid-out `rect` as a parameter (the RFC shape) rather than
 147 /// reading a stored rect, so a narrow widget carries no base of its own.
 148 pub trait Paint {
 149     /// This widget's fill color (RGBA).
 150     fn color(&self) -> [f32; 4];
 151 
 152     /// Emit this node's OWN primitives (non-recursive) into `ctx`, given its final `rect`. The
 153     /// default paints a plain background from [`color`](Paint::color) — the common leaf case.
 154     /// Recursion into children and clipping are the paint walk's job ([`crate::scene::painter`]),
 155     /// not this method's.
 156     fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
 157         let color = self.color();
 158         if color[3].abs() > 0.001 {
 159             ctx.quad(rect, color);
 160         }
 161     }
 162 
 163     /// [`paint`](Paint::paint) with the live [`UiContext`] — what `paint_self` actually calls.
 164     /// The default forwards to `paint`, so ordinary widgets implement only that. Override this
 165     /// for composites whose own geometry aggregates hover/coverage-dependent child chrome that
 166     /// needs the context (ParametersBg): child-holding widgets that never entered the arena tree
 167     /// have no other way to reach it from the paint path.
 168     fn paint_ui(&self, _ui: &UiContext, rect: Rect, ctx: &mut PaintCtx) {
 169         self.paint(rect, ctx);
 170     }
 171 
 172     /// Whether the paint walk clips this widget's children to its `rect` (scroll/root plate
 173     /// containers). Default: no.
 174     fn clips_children(&self) -> bool {
 175         false
 176     }
 177 
 178     /// Corner rounding `(radius, per-corner flags)` of the widget's background, given its
 179     /// laid-out rect (MenuBar's corners depend on where it sits against its parent's edges).
 180     /// **Transitional:** this exists only for legacy render paths that draw widget backgrounds
 181     /// themselves from style properties (`widget_vertices` / `push_widget_vertices` readers of
 182     /// `WidgetHost::corner_radius` + `rounded_corners`) — the widget's real geometry is whatever
 183     /// [`paint`](Paint::paint) emits. Dies with those paths. Default: sharp corners.
 184     fn corner_style(&self, _rect: Rect) -> Option<(f32, (bool, bool, bool, bool))> {
 185         None
 186     }
 187 
 188     /// This widget's OWN popover (dropdown) rect, if one is open — hosts float it above
 189     /// z-ordered siblings (`register_popover` + `render_popovers`). Containers combine this
 190     /// with their children's popovers in the adapter. Default: none.
 191     fn popover(&self, _rect: Rect) -> Option<(f32, f32, f32, f32)> {
 192         None
 193     }
 194 
 195     /// Draw this widget's own popover (legacy `WidgetHost::render_popover`).
 196     fn draw_popover(&self, _rect: Rect, _pc: &mut dyn crate::layout::RenderTarget) {}
 197 
 198     /// Solid border `(color, thickness)` of the widget's background quad. **Transitional**, like
 199     /// [`corner_style`](Paint::corner_style): `render_widget` gives a widget's background quad a
 200     /// border+inset treatment when this is `Some` — `Toggle`'s square mode depends on it.
 201     fn solid_border(&self) -> Option<([f32; 4], f32)> {
 202         None
 203     }
 204 
 205     /// Font for this widget's text on legacy text paths (`render_widget` reads
 206     /// `WidgetHost::widget_font`). **Transitional.**
 207     fn widget_font(&self) -> Option<String> {
 208         None
 209     }
 210 
 211     /// Font for the widget's OWN prim-derived text on the scene paint walk (Phase 6). Defaults
 212     /// to [`widget_font`](Paint::widget_font) — one font for everything the widget draws, which
 213     /// is the legacy tuple-pipeline convention. A widget whose content text deliberately
 214     /// differs from its control font (TextBox with a customized `font_family`/`font_size`)
 215     /// overrides this; the detached base label always renders in `widget_font`. Only the paint
 216     /// walk consults it — the legacy `text_labels_with_font_and_bounds` getters keep serving
 217     /// `widget_font` so unmigrated apps stay byte-identical.
 218     fn text_font(&self) -> Option<String> {
 219         self.widget_font()
 220     }
 221 
 222     /// Receive the control label set on the wrapper via [`Adapted::with_label`] (and legacy
 223     /// `Control::set_label` paths). Widgets that paint their label themselves (inline-label
 224     /// widgets) store it here; the default discards it, leaving label drawing to the adapter's
 225     /// base-label machinery.
 226     fn sync_label(&mut self, _label: &str) {}
 227 
 228     // --- Legacy dual-geometry escape hatch (transitional; Graph is the only user). Legacy
 229     // hosts read DIFFERENT getters: the designer's raw render path draws `extra_quads` as
 230     // PLAIN quads, while `render_widget` and the scene walk consume the rounded view
 231     // (`all_rounded_quads` / `paint_self`). Legacy Graph served both by overriding all three
 232     // getters. A migrated widget emits the rounded view from `paint`; when it also serves a
 233     // plain view, the adapter returns it verbatim from `extra_quads` and empties `all_quads`
 234     // (mirroring legacy Graph's highlight-only override) so render_widget-style hosts that
 235     // read BOTH getters never draw the geometry twice. Dies with `WidgetHost`.
 236 
 237     /// Whether this widget serves [`legacy_plain_quads`](Paint::legacy_plain_quads).
 238     fn serves_legacy_plain_quads(&self) -> bool {
 239         false
 240     }
 241 
 242     /// The plain-quad view of this widget's geometry for legacy `extra_quads` readers.
 243     fn legacy_plain_quads(&self, _rect: Rect) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
 244         Vec::new()
 245     }
 246 
 247     /// Clip rect `[x1, y1, x2, y2]` for this widget's text on the legacy bounded-text paths
 248     /// (`text_labels_with_bounds` / `text_labels_with_font_and_bounds`). `None` (default) keeps
 249     /// the legacy behavior: unbounded, except inside a scroll ancestor. Graph clips its node
 250     /// names to its own rect.
 251     fn text_bounds(&self, _rect: Rect) -> Option<[f32; 4]> {
 252         None
 253     }
 254 
 255     /// Per-frame text shaping against the app's `FontSystem` (legacy `WidgetHost::prepare_text`
 256     /// overrides). TextBox measures its glyph advances here — load-bearing for cursor↔pixel
 257     /// mapping, not just a render cache. Receives the laid-out content rect. Default: nothing
 258     /// to shape.
 259     fn prepare_text(&mut self, _fs: &mut cosmic_text::FontSystem, _rect: Rect) {}
 260 
 261     /// Whether [`paint`](Paint::paint) emits the widget's ENTIRE subtree, so the paint walk
 262     /// must not also descend into its (ctx-linked) children — the legacy
 263     /// `WidgetHost::renders_own_subtree` contract. TreeList: its field widgets stay ctx-linked
 264     /// for event propagation, but their pixels come from `paint`'s own child pass (which
 265     /// gates the add-key popover box on the popover actually being open).
 266     fn paints_own_subtree(&self) -> bool {
 267         false
 268     }
 269 
 270 
 271     /// Whether the adapter re-enables the legacy shared focus/hover highlight overlay
 272     /// (`WidgetHost::highlight_quad`'s default) for this widget. The adapter suppresses it for
 273     /// migrated widgets — matching the `None` overrides most legacy controls carried — but
 274     /// legacy TextBox kept the default: the focused editor gets the primary-highlight tint
 275     /// over its background (data-editor's teal editing wash). Default: suppressed.
 276     fn legacy_focus_highlight(&self) -> bool {
 277         false
 278     }
 279 
 280     /// Legacy container `extra_quads` aggregation: when `true`, the adapter's `extra_quads`
 281     /// serves the visible children's `extra_quads` — and ONLY those, like the legacy container
 282     /// overrides (Paginator returned its strip's + selected page's chrome; its own background
 283     /// quad lived in `all_quads` alone). Hosts that render a container through the plain
 284     /// `extra_quads` getter (cce-mail's and cce-layout-interface's sidebar draw) read exactly
 285     /// this view. The widget's own [`paint`](Paint::paint) prims still reach `all_quads` and
 286     /// the scene walk. Default: off (a leaf's `extra_quads` is its own prims).
 287     fn aggregates_child_extra_quads(&self) -> bool {
 288         false
 289     }
 290 
 291     /// Forward the legacy `WidgetHost::highlight_quad` to somewhere else entirely — Paginator
 292     /// served its ButtonStrip's highlight (the hovered-tab tint cce-layout-interface draws by
 293     /// calling `highlight_quad` directly). Outer `Some` replaces the adapter's highlight logic
 294     /// with the inner value; `None` (default) keeps the standard behavior
 295     /// ([`legacy_focus_highlight`](Paint::legacy_focus_highlight)). A forwarded highlight is
 296     /// served ONLY through the direct `highlight_quad` getter — the adapter keeps it out of
 297     /// `all_quads`/`paint_self`, where the child's own aggregation already carries it (legacy
 298     /// containers likewise excluded it from their `all_quads` overrides).
 299     fn forwarded_highlight(&self, _ctx: &UiContext) -> Option<Option<(f32, f32, f32, f32, [f32; 4])>> {
 300         None
 301     }
 302 
 303     /// Whether this widget serves
 304     /// [`legacy_labels_with_font_and_bounds`](Paint::legacy_labels_with_font_and_bounds) —
 305     /// the text sibling of the dual-geometry escape hatch. The adapter's standard text bridge
 306     /// gives every own label ONE font ([`widget_font`](Paint::widget_font)) and ONE clip rect
 307     /// ([`text_bounds`](Paint::text_bounds)); a widget whose legacy
 308     /// `text_labels_with_font_and_bounds` override assigns them PER LABEL (ParametersBg clips
 309     /// each label to its viewport but its code editor's to the code box, in monospace) serves
 310     /// that view verbatim instead. Transitional — dies when `Prim::Text` carries font+bounds.
 311     fn serves_legacy_labels(&self) -> bool {
 312         false
 313     }
 314 
 315     /// The per-label font+bounds text view for legacy `text_labels_with_font_and_bounds`
 316     /// readers. Served as a FULL replacement: the adapter adds no child aggregation on top, so
 317     /// a container's implementation must include its children (as the legacy overrides did —
 318     /// hence the ctx, which the child recursion needs).
 319     fn legacy_labels_with_font_and_bounds(&self, _rect: Rect, _ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
 320         Vec::new()
 321     }
 322 
 323 }
 324 
 325 /// What an event handler may reach beyond its own state — the RFC §3.5 `EventCtx`, grown as
 326 /// migrated widgets need capabilities: the laid-out content rect, the widget's id (scroll-gesture
 327 /// gating keys on it), focus acquisition, and — transitionally — the raw [`UiContext`] for the
 328 /// legacy shared state some widgets consult (`scroll_gesture_new`, …). `ui` is `None` when the
 329 /// event was synthesized outside a routed path (the `FocusIn`/`FocusOut` from direct
 330 /// `focus()`/`unfocus()` calls).
 331 pub struct EventCtx<'a> {
 332     /// The widget's content rect (detached-label region excluded).
 333     pub rect: Rect,
 334     /// This widget's tree id.
 335     pub id: WidgetId,
 336     /// The routing context, when routed. **Transitional** — narrow widgets should only touch the
 337     /// legacy shared fields (scroll gesture state) until those get typed helpers here.
 338     pub ui: Option<&'a mut UiContext>,
 339     self_ptr: Option<*mut (dyn WidgetHost + 'static)>,
 340 }
 341 
 342 impl EventCtx<'_> {
 343     /// Make this widget the global focus target (legacy `focus::set_focused(self)`).
 344     pub fn request_focus(&mut self) {
 345         if let Some(ptr) = self.self_ptr {
 346             unsafe { crate::widget::focus::set_focused(&mut *ptr, self.ui.as_deref_mut()) };
 347         }
 348     }
 349 
 350     /// Drop this widget's claim on the global focus if it holds it (legacy
 351     /// `focus::clear_if_matches(self)` — MenuBar releases focus when its dropdowns close).
 352     pub fn release_focus(&mut self) {
 353         if let Some(ptr) = self.self_ptr {
 354             unsafe { crate::widget::focus::clear_if_matches(&mut *ptr) };
 355         }
 356     }
 357 
 358     /// Open the shared context menu on this widget (legacy `ctx.handle_right_click(self, …)`),
 359     /// for widgets that must do work *before* the menu opens — Breadcrumb records which segment
 360     /// was right-clicked first, so the menu header can show that segment's path.
 361     /// [`Input::opens_context_menu`] can't express that: the adapter's gate runs instead of
 362     /// `on_event`, not after it. No-op outside a routed path (no ctx or no self pointer).
 363     pub fn open_context_menu(&mut self, px: f32, py: f32) {
 364         if let (Some(ptr), Some(ui)) = (self.self_ptr, self.ui.as_deref_mut()) {
 365             ui.handle_right_click(ptr, px, py);
 366         }
 367     }
 368 
 369     /// The adapter's pointer, for legacy sites that must hand it onward — TreeList makes
 370     /// itself the focus target (`set_focused_ptr`) and the context-menu target
 371     /// (`show_context_menu`) with the pointer hosts registered. Transitional; dies with
 372     /// `WidgetHost`. None outside a routed path.
 373     pub(crate) fn host_ptr(&self) -> Option<*mut (dyn WidgetHost + 'static)> {
 374         self.self_ptr
 375     }
 376 }
 377 
 378 /// The input concern — hit-testing and event handling against the laid-out rect. Mirrors the
 379 /// legacy `WidgetHost::hit_test` / `handle_event` pair, but with the RFC's centralizations: the
 380 /// default hit is plain rect containment (no per-widget address hacks), and pointer-positioned
 381 /// events are hit-gated by the adapter *before* they reach [`on_event`](Input::on_event), so a
 382 /// narrow widget never re-implements the "am I actually under the cursor?" boilerplate that every
 383 /// legacy `mouse_input` override carries.
 384 /// A widget's part in keyboard navigation — see [`Input::focus_role`].
 385 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
 386 pub enum FocusRole {
 387     /// Not a stop: the traversal skips it.
 388     None,
 389     /// A plate — a thing you press. Enter / Space act on it while focused.
 390     Plate,
 391     /// A well — a thing you enter. It opens for typing when focused.
 392     Well,
 393 }
 394 
 395 pub trait Input {
 396     /// Whether the point `(x, y)` hits this widget, given its laid-out `rect`. Override for
 397     /// non-rectangular hit shapes. Default: containment (edges inclusive, matching the legacy
 398     /// `hit_test`).
 399     fn hit(&self, rect: Rect, x: f32, y: f32) -> bool {
 400         x >= rect.x && x <= rect.x + rect.width && y >= rect.y && y <= rect.y + rect.height
 401     }
 402 
 403     /// React to `event`. Return `true` to consume it (the router marks the widget dirty and
 404     /// stops propagation). `MouseButton` *presses* and `MouseWheel` arrive only when
 405     /// [`hit`](Input::hit) passed; *releases* arrive ungated (press-tracking widgets commit or
 406     /// cancel from anywhere); `MouseEnter` / `MouseLeave` are synthesized by the hover machinery.
 407     /// Default: ignore everything.
 408     fn on_event(&mut self, _event: &Event, _ectx: &mut EventCtx) -> bool {
 409         false
 410     }
 411 
 412     /// Whether pressing on this widget blocks dragging the movable root plate under it. Passive
 413     /// display widgets (separators, status dots) return `false` so drags pass through them.
 414     /// Default: `true`, matching the legacy `WidgetHost` default.
 415     fn blocks_root_plate_drag(&self) -> bool {
 416         true
 417     }
 418 
 419     /// Whether a right-click on this widget opens the shared config context menu (the adapter
 420     /// then routes it to `UiContext::handle_right_click`, which `on_event` can't reach — it has
 421     /// no ctx by design). Default: no.
 422     fn opens_context_menu(&self) -> bool {
 423         false
 424     }
 425 
 426     /// What this widget is to keyboard navigation — see "Plates, wells and
 427     /// seams" in `CLAUDE.md`. A [`FocusRole::Plate`] is a thing you press
 428     /// (Enter / Space act on it while focused); a [`FocusRole::Well`] opens
 429     /// for typing when focused. Both are stops for `UiContext::focus_step`.
 430     /// Default: [`FocusRole::None`] — skipped by the traversal. A widget that
 431     /// declares a role must handle `FocusIn` / `FocusOut`.
 432     fn focus_role(&self) -> FocusRole {
 433         FocusRole::None
 434     }
 435 
 436     /// Container hit policy: hit whenever any [`Layout::child_visible`] child hits (Layer,
 437     /// Switcher). The container's own rect is not consulted. Default: own-rect hit.
 438     fn hits_through_children(&self) -> bool {
 439         false
 440     }
 441 
 442     /// Whether the adapter hit-gates `MouseButton` presses before `on_event` (the leaf
 443     /// centralization). Event-proxying containers return `false`: legacy container
 444     /// `mouse_input` overrides saw every press — Switcher unfocuses its active child when a
 445     /// press lands outside it, which a gated `on_event` would never learn about.
 446     fn gates_presses(&self) -> bool {
 447         true
 448     }
 449 
 450     // --- The legacy polling/value-binding surface (`take_click`, `take_change`,
 451     // `get_value_string`/`set_value_string`, `value`) apps read widget state through. Kept on
 452     // `Input` to avoid a fourth trait bound; replaced by typed messages when RFC §3.5's EventCtx
 453     // lands. All default to the inert legacy defaults.
 454 
 455     /// Consume the "was clicked since last asked" flag.
 456     fn take_click(&mut self) -> bool {
 457         false
 458     }
 459 
 460     /// Consume the "value changed since last asked" flag.
 461     fn take_change(&mut self) -> bool {
 462         false
 463     }
 464 
 465     /// The widget's value serialized for the config system.
 466     fn value_string(&self) -> Option<String> {
 467         None
 468     }
 469 
 470     /// Set the widget's value from a config string. Returns whether it parsed and changed.
 471     fn set_value_string(&mut self, _val: &str) -> bool {
 472         false
 473     }
 474 
 475     /// The widget's value as an integer (legacy `WidgetHost::value`).
 476     fn value(&self) -> i32 {
 477         0
 478     }
 479 
 480     // --- Clipboard/selection surface (the context menu's Cut/Copy/Paste/Select-All actions
 481     // call these on their target WidgetHost). The defaults replicate the `WidgetHost` defaults
 482     // byte-for-byte (whole-value copy through the value-string pair), so widgets migrated
 483     // before these hooks existed keep their exact behavior; TextBox overrides with real
 484     // selection-aware implementations.
 485 
 486     fn context_action(&mut self, action: crate::widget::ContextAction) -> bool {
 487         match action {
 488             crate::widget::ContextAction::Cut => {
 489                 if let Some(val) = self.value_string() {
 490                     crate::widget::clipboard::copy_to_clipboard(&val);
 491                     self.set_value_string("")
 492                 } else {
 493                     false
 494                 }
 495             }
 496             crate::widget::ContextAction::Copy => {
 497                 if let Some(val) = self.value_string() {
 498                     crate::widget::clipboard::copy_to_clipboard(&val);
 499                     true
 500                 } else {
 501                     false
 502                 }
 503             }
 504             crate::widget::ContextAction::Paste => {
 505                 if let Some(text) = crate::widget::clipboard::read_from_clipboard() {
 506                     self.set_value_string(&text)
 507                 } else {
 508                     false
 509                 }
 510             }
 511             _ => false,
 512         }
 513     }
 514 
 515     /// Whether direct `focus()`/`unfocus()` calls flip the base `focused` flag. Legacy widgets
 516     /// differ: most set it in their `focus` overrides, but TextBox never did — its detached
 517     /// label must not color as focused. Default: flip it (what every widget migrated so far
 518     /// has shipped with).
 519     fn tracks_base_focus(&self) -> bool {
 520         true
 521     }
 522 
 523     /// Selection state pushed in by list/row hosts (legacy `WidgetHost::set_selected`).
 524     fn set_selected(&mut self, _selected: bool) {}
 525 
 526     // --- Drag surface: legacy hosts (designer, control_panel, parameters_bg, graph, audio…)
 527     // drive drags by calling these directly on the widget, not through events.
 528 
 529     /// Whether a press on this widget starts a host-driven drag. Receives the laid-out rect:
 530     /// scroll widgets (Spreadsheet) are draggable only while their content overflows it.
 531     fn draggable(&self, _rect: Rect) -> bool {
 532         false
 533     }
 534     fn is_dragging(&self) -> bool {
 535         false
 536     }
 537     fn drag_begin(&mut self, _px: f32, _py: f32, _rect: Rect) {}
 538     /// Returns whether the drag changed the widget's value (drives redraw).
 539     fn drag_update(&mut self, _px: f32, _py: f32, _rect: Rect) -> bool {
 540         false
 541     }
 542     /// For self-moving widgets (Panel, Splitter): the new origin this drag step wants, or `None`
 543     /// if unmoved. The adapter applies it to the base rect (the model cannot reach it).
 544     fn drag_reposition(&mut self, _px: f32, _py: f32, _rect: Rect) -> Option<(f32, f32)> {
 545         None
 546     }
 547     fn drag_end(&mut self) {}
 548     /// Movement bounds pushed in by hosts (reached via the inherent `Adapted::set_drag_bounds`).
 549     fn set_drag_bounds(&mut self, _bx: f32, _by: f32, _bw: f32, _bh: f32) {}
 550 
 551     // --- Tick surface: hosts broadcast `WidgetHost::tick(dt)` every frame (the designer's render
 552     // loop) to advance time-based widget state — inertial scroll velocity, here. Transitional:
 553     // §3.6 `Animated<T>` + arena-driven frame requests replace hand-ticked state.
 554 
 555     /// Advance time-based state by `dt` seconds against the laid-out rect. Return whether
 556     /// anything observable changed (drives redraw).
 557     fn tick(&mut self, _dt: f32, _rect: Rect) -> bool {
 558         false
 559     }
 560 
 561     /// Context-carrying tick for legacy stateful containers whose per-frame work needs the
 562     /// routing context — TreeList commits its inline rename editor, re-targets focus, and
 563     /// drains its search box on tick. Runs right after [`tick`](Input::tick) with a routed
 564     /// [`EventCtx`] (ui + the adapter's id/pointer). Transitional, like the capability hooks.
 565     fn tick_ctx(&mut self, _dt: f32, _ectx: &mut EventCtx) -> bool {
 566         false
 567     }
 568 
 569     /// Whether this widget wants `tick` calls from tick-gating hosts (legacy
 570     /// `WidgetHost::wants_tick`; the designer ticks unconditionally and ignores this).
 571     fn wants_tick(&self) -> bool {
 572         false
 573     }
 574 
 575     /// Whether this widget consumes scroll gestures (legacy `WidgetHost::is_scrollable`, read by
 576     /// the router's scroll-gesture gating).
 577     fn scrollable(&self) -> bool {
 578         false
 579     }
 580 
 581     // --- Controller capabilities (transitional, like the polling surface above). The legacy
 582     // tree reaches a widget's typed API through the `WidgetHost::as_*_controller` downcast pairs;
 583     // `WidgetHost` is implemented exactly once (for `Adapted<W>`), so a migrated controller widget
 584     // re-exposes its controller impl through these hooks instead — `Some(self)` when `W`
 585     // implements the trait. Dies with `WidgetHost`: the end state reaches a controller through the
 586     // concrete `Adapted<W>` (or a `&dyn XController` held directly), per RFC §3.5.
 587 
 588 
 589     /// Keyboard modifier state pushed in by hosts before dispatch (legacy
 590     /// `WidgetHost::set_modifiers`).
 591     fn set_modifiers(&mut self, _ctrl: bool, _shift: bool, _alt: bool) {}
 592 
 593     /// The widget's visibility flag changed through `WidgetHost::set_visible` (the adapter owns
 594     /// the flag) — legacy hideable widgets used the setter for side effects (MenuBar closes
 595     /// its dropdowns and invalidates layout).
 596     fn visibility_changed(&mut self, _visible: bool) {}
 597 
 598     /// The widget's `WidgetHost::focused` answer, given the base flag — MenuBar reports focused
 599     /// while any of its dropdowns is open, beyond the flag itself. Default: the flag.
 600     fn is_focused(&self, base_focused: bool) -> bool {
 601         base_focused
 602     }
 603 
 604 }
 605 
 606 /// Wraps a narrow-trait widget `W` so it lives in the legacy `*mut dyn WidgetHost` tree. Carries the
 607 /// [`Widget`] base that `WidgetHost`'s rect / id / dirty machinery needs, and forwards the concern
 608 /// methods to `W`. See the module docs for why this bridge exists rather than a supertrait split.
 609 ///
 610 /// The bounds live on the struct (not just the `WidgetHost` impl) so `Drop` can clear the global
 611 /// focus / context-menu references through `&dyn WidgetHost` — the same guard legacy widgets with
 612 /// `Drop` impls (e.g. the old `Checkbox`) carried.
 613 #[derive(Debug, Clone)]
 614 pub struct Adapted<W: Layout + Paint + Input + 'static> {
 615     base: Widget,
 616     /// The [`Widget`] base carries no visibility, and the legacy `WidgetHost` defaults are a no-op
 617     /// `set_visible` + always-true `visible()` — every hideable legacy widget stores its own
 618     /// flag. The adapter owns it once for all migrated widgets: hosts toggle panes through
 619     /// `WidgetHost::set_visible` (the designer), and the hit-test/render bridges gate on it.
 620     visible: bool,
 621     inner: W,
 622 }
 623 
 624 impl<W: Layout + Paint + Input + 'static> Drop for Adapted<W> {
 625     fn drop(&mut self) {
 626         crate::widget::clear_widget_references(self);
 627     }
 628 }
 629 
 630 /// Plain-data widgets constructed via `Default` (PreviewState in cce-files) keep their
 631 /// construction sites when the wrapper lands.
 632 impl<W: Layout + Paint + Input + Default + 'static> Default for Adapted<W> {
 633     fn default() -> Self {
 634         Adapted::new(W::default())
 635     }
 636 }
 637 
 638 impl<W: Layout + Paint + Input + 'static> Adapted<W> {
 639     /// Wrap `inner` with a fresh [`Widget`] base.
 640     pub fn new(inner: W) -> Self {
 641         Adapted { base: Widget::new(), visible: true, inner }
 642     }
 643 
 644     /// The wrapped widget.
 645     pub fn inner(&self) -> &W {
 646         &self.inner
 647     }
 648 
 649     /// The wrapped widget, mutably.
 650     pub fn inner_mut(&mut self) -> &mut W {
 651         &mut self.inner
 652     }
 653 
 654     /// This widget's tree id (assigned lazily), for registering it in a [`UiContext`].
 655     pub fn id(&self) -> WidgetId {
 656         self.base.id()
 657     }
 658 
 659     /// Attach a control label. Mirrors the `with_label` builders legacy control widgets carry,
 660     /// so construction sites keep their shape when a widget migrates. The label is stored on the
 661     /// base (legacy machinery: label offsets, context-menu titles) *and* pushed into the widget
 662     /// via [`Paint::sync_label`] for widgets that paint it themselves.
 663     pub fn with_label(mut self, label: &str) -> Self {
 664         self.base.label = Some(label.to_string());
 665         self.inner.sync_label(label);
 666         self
 667     }
 668 
 669     /// Bind this widget to a config file/key (right-click context-menu editing). Mirrors the
 670     /// legacy `with_config` builders.
 671     pub fn with_config(mut self, file: &str, key: &str) -> Self {
 672         self.base.config_file = Some(file.to_string());
 673         self.base.config_key = Some(key.to_string());
 674         self
 675     }
 676 
 677     /// Update the control label, keeping the base copy (legacy machinery) and the widget's own
 678     /// copy ([`Paint::sync_label`]) in step. Inherent so it shadows `Control::set_label` — which
 679     /// writes only the base and would leave a self-painting label stale — at every call site,
 680     /// regardless of which traits are in scope.
 681     pub fn set_label(&mut self, label: &str) {
 682         self.base.label = Some(label.to_string());
 683         self.inner.sync_label(label);
 684     }
 685 
 686     // --- The value/polling drains (off `WidgetHost` in the 6bd value shrink): apps read
 687     // widget state through these concrete methods; each forwards to the narrow `Input`
 688     // hook. The last dyn readers went concrete-slot instead (TI roster, cloud JsonControl,
 689     // designer pane-focus sync).
 690 
 691     /// Drain the one-shot click flag (Button-class widgets).
 692     pub fn take_click(&mut self) -> bool {
 693         Input::take_click(&mut self.inner)
 694     }
 695 
 696     /// Drain the one-shot value-changed flag.
 697     pub fn take_change(&mut self) -> bool {
 698         Input::take_change(&mut self.inner)
 699     }
 700 
 701     /// The widget's value serialized to a string (config writes, context-menu Copy).
 702     pub fn get_value_string(&self) -> Option<String> {
 703         Input::value_string(&self.inner)
 704     }
 705 
 706     /// Parse and apply a value string; returns whether the value changed.
 707     pub fn set_value_string(&mut self, val: &str) -> bool {
 708         Input::set_value_string(&mut self.inner, val)
 709     }
 710 
 711     /// The widget's value as an integer.
 712     pub fn value(&self) -> i32 {
 713         Input::value(&self.inner)
 714     }
 715 
 716     /// Selection state pushed in by list/row hosts.
 717     pub fn set_selected(&mut self, selected: bool) {
 718         Input::set_selected(&mut self.inner, selected)
 719     }
 720 
 721     /// Text-content mutation: keep the base copy and the widget's own copy
 722     /// ([`Paint::sync_label`]) in step, like `set_label`.
 723     pub fn set_text(&mut self, text: &str) {
 724         self.base.label = Some(text.to_string());
 725         Paint::sync_label(&mut self.inner, text);
 726     }
 727 
 728     /// The rect the wrapped widget paints into: the widget's rect minus the detached-label
 729     /// strip at the top (zero when there is no label, or when the widget's label is its
 730     /// content — [`Layout::inline_label`]).
 731     fn content_rect(&self) -> Rect {
 732         let top = if Layout::inline_label(&self.inner) { 0.0 } else { self.base.label_offset() };
 733         Rect {
 734             x: self.base.x,
 735             y: self.base.y + top,
 736             width: self.base.w,
 737             // Deliberately NOT clamped at zero: legacy geometry computed `h - label_offset`
 738             // raw, and hosts under-size labeled sliders (label taller than the assigned rect);
 739             // the resulting negative-height quads still rasterize (flipped), which is what
 740             // keeps those tracks visible. Clamping made them vanish — found the hard way.
 741             height: self.base.h - top,
 742         }
 743     }
 744 }
 745 
 746 impl<W: Layout + Paint + Input + 'static> Adapted<W> {
 747     /// This widget as a type-erased host pointer (off the `WidgetHost` trait — the
 748     /// plumbing retype). Registration-bridge material: derived from a live borrow at the
 749     /// call, stored only in the `WidgetTree` registry.
 750     pub fn as_ptr(&self) -> *mut (dyn WidgetHost + 'static) {
 751         self as *const Self as *mut Self as *mut (dyn WidgetHost + 'static)
 752     }
 753 
 754     pub fn as_ptr_mut(&mut self) -> *mut (dyn WidgetHost + 'static) {
 755         self as *mut Self as *mut (dyn WidgetHost + 'static)
 756     }
 757 
 758     /// Whether a press here may start a drag (off `WidgetHost` — the ControlPanel
 759     /// endgame; forwards to the narrow `Input` hook with the laid-out content rect).
 760     pub fn draggable(&self) -> bool {
 761         Input::draggable(&self.inner, self.content_rect())
 762     }
 763 
 764     /// Whether the widget's own drag is live (off `WidgetHost` with `draggable`).
 765     pub fn is_dragging(&self) -> bool {
 766         Input::is_dragging(&self.inner)
 767     }
 768 
 769     /// Movement bounds pushed in by hosts (off the `WidgetHost` trait since 6bd — the one
 770     /// production caller is concrete: designer's network panel).
 771     pub fn set_drag_bounds(&mut self, bx: f32, by: f32, bw: f32, bh: f32) {
 772         Input::set_drag_bounds(&mut self.inner, bx, by, bw, bh)
 773     }
 774 
 775     /// Unlink all tree children (off `WidgetHost` in 6bd batch 2 — every caller is a concrete
 776     /// `Adapted` field).
 777     pub fn clear_children(&mut self, ctx: &mut UiContext) {
 778         ctx.clear_children_ids(self.base.id());
 779     }
 780 
 781     // --- The direct-dispatch entry points, inherent since the 6bd collapse. In-crate
 782     // composites forward to their CONCRETE embedded children through these; dyn callers
 783     // and the router go through `handle_event`, which these forward to (the two paths
 784     // are identical by construction — including Drag*, which handle_event maps onto the
 785     // Input drag hooks).
 786 
 787     pub fn mouse_input(&mut self, button: crate::widget::MouseButton, state: crate::widget::ElementState, px: f32, py: f32, ctx: &mut UiContext) -> bool {
 788         self.handle_event(
 789             &Event::MouseButton { button, state, x: px, y: py, local_x: px, local_y: py },
 790             ctx,
 791         )
 792     }
 793     pub fn mouse_wheel(&mut self, delta: &crate::widget::MouseScrollDelta, px: f32, py: f32, ctx: &mut UiContext) -> bool {
 794         self.handle_event(
 795             &Event::MouseWheel { delta: *delta, x: px, y: py, local_x: px, local_y: py },
 796             ctx,
 797         )
 798     }
 799 
 800     /// [`Self::mouse_wheel`] WITHOUT the adapter's rect hit-gate: straight to the
 801     /// widget's `Input::on_event`. For hosts that already zone-gated the wheel
 802     /// themselves against a capture region LARGER than the widget rect — the
 803     /// band slider's shape-conforming halo extends past the row rect, and the
 804     /// rect gate would clip exactly the fringe the halo exists to catch
 805     /// (`ParametersBg`'s slider forwarding). The widget's own on_event still
 806     /// applies its fine-grained zone test.
 807     pub fn mouse_wheel_ungated(
 808         &mut self,
 809         delta: &crate::widget::MouseScrollDelta,
 810         px: f32,
 811         py: f32,
 812         ctx: &mut UiContext,
 813     ) -> bool {
 814         let rect = self.content_rect();
 815         let id = self.base.id();
 816         let self_ptr = self.as_ptr_mut();
 817         let mut ectx = EventCtx { rect, id, ui: Some(ctx), self_ptr: Some(self_ptr) };
 818         Input::on_event(
 819             &mut self.inner,
 820             &Event::MouseWheel { delta: *delta, x: px, y: py, local_x: px, local_y: py },
 821             &mut ectx,
 822         )
 823     }
 824     pub fn keyboard_input(&mut self, event: &crate::widget::KeyEvent, ctx: &mut UiContext) -> bool {
 825         self.handle_event(&Event::KeyInput(event.clone()), ctx)
 826     }
 827     pub fn drag_begin(&mut self, px: f32, py: f32) {
 828         let rect = self.content_rect();
 829         Input::drag_begin(&mut self.inner, px, py, rect)
 830     }
 831     pub fn drag_update(&mut self, px: f32, py: f32) -> bool {
 832         let rect = self.content_rect();
 833         if let Some((nx, ny)) = Input::drag_reposition(&mut self.inner, px, py, rect) {
 834             self.base.x = nx;
 835             self.base.y = ny;
 836             return true;
 837         }
 838         Input::drag_update(&mut self.inner, px, py, rect)
 839     }
 840     pub fn drag_end(&mut self) {
 841         Input::drag_end(&mut self.inner)
 842     }
 843 
 844     /// The deleted trait default: coverage-gated hover dispatch (an open popover covering
 845     /// the point clears the hover instead of recomputing it).
 846     pub fn cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
 847         ctx.set_cursor_pos(px, py);
 848         if ctx.is_coordinate_covered(self.base.id(), px, py) {
 849             let was = self.base.hovered;
 850             if was {
 851                 self.base.hovered = false;
 852                 self.handle_event(&Event::MouseLeave, ctx);
 853             }
 854             return was;
 855         }
 856         self.on_cursor_moved(px, py, ctx)
 857     }
 858 
 859     /// Ungated pointer moves (no popover-coverage check): offer the raw move to the widget,
 860     /// then fall back to the base hover bookkeeping, mirroring `handle_event`'s `PointerMove`
 861     /// arm. Not routed *through* `handle_event`, because the routed path reaches this method
 862     /// too (via `cursor_moved`) and would recurse; on that path `on_event` sees the same
 863     /// unconsumed move twice, which is fine — a hover recompute is idempotent (anything
 864     /// that changed on the first call consumed it there).
 865     pub fn on_cursor_moved(&mut self, px: f32, py: f32, ctx: &mut UiContext) -> bool {
 866         let rect = self.content_rect();
 867         let id = self.base.id();
 868         let self_ptr = self.as_ptr_mut();
 869         let mut ectx = EventCtx { rect, id, ui: Some(ctx), self_ptr: Some(self_ptr) };
 870         let event = Event::PointerMove { x: px, y: py, local_x: px, local_y: py };
 871         if Input::on_event(&mut self.inner, &event, &mut ectx) {
 872             return true;
 873         }
 874         let was = self.base.hovered;
 875         let is_hit = self.hit_test(px, py, ctx);
 876         self.base.hovered = is_hit;
 877         if was != is_hit {
 878             let transition = if is_hit { Event::MouseEnter } else { Event::MouseLeave };
 879             self.handle_event(&transition, ctx);
 880             true
 881         } else {
 882             false
 883         }
 884     }
 885 
 886     /// Register + link a child under this widget (off `WidgetHost` in 6bd batch 4; dyn callers
 887     /// went to `focus::link_parent_child`/tree ops).
 888     pub fn add_child(&mut self, child: *mut (dyn WidgetHost + 'static), ctx: &mut UiContext) {
 889         // The old WidgetHost default's tree link…
 890         let c_id = unsafe { (*child).base().id() };
 891         let p_id = self.base.id();
 892         let self_ptr = self.as_ptr();
 893         ctx.register_widget(p_id, self_ptr);
 894         ctx.register_widget(c_id, child);
 895         ctx.tree.link(p_id, c_id);
 896         // …plus, for containers, the legacy container extra: parent the child back (Layer,
 897         // Switcher) — the symmetric tree link the child's own set_parent used to make.
 898         if Layout::has_container_children(&self.inner) {
 899             let self_ptr = self.as_ptr_mut();
 900             ctx.register_widget(self.base.id(), self_ptr);
 901             ctx.register_widget(c_id, child);
 902             ctx.tree.set_parent(c_id, Some(self.base.id()));
 903         }
 904     }
 905 
 906     /// Register + (un)link this widget under a parent (off `WidgetHost` in 6bd batch 4).
 907     pub fn set_parent(&mut self, parent: Option<*mut (dyn WidgetHost + 'static)>, ctx: &mut UiContext) {
 908         // Replica of the old WidgetHost default: symmetric tree link.
 909         let id = self.base.id();
 910         if let Some(p_ptr) = parent {
 911             let p_id = unsafe { (*p_ptr).base().id() };
 912             ctx.register_widget(p_id, p_ptr);
 913             let self_ptr = self.as_ptr();
 914             ctx.register_widget(id, self_ptr);
 915             ctx.tree.set_parent(id, Some(p_id));
 916         } else {
 917             ctx.tree.set_parent(id, None);
 918         }
 919     }
 920 
 921     /// The model's intrinsic content size (off the `WidgetHost` trait since 6bd — the concrete
 922     /// callers are fonts'/graph's hand-laid button/dropdown sizing).
 923     pub fn intrinsic_size(&self) -> Option<Size> {
 924         Layout::intrinsic_size(&self.inner)
 925     }
 926 
 927     /// Run the wrapped widget's [`Paint::paint`] against its content rect and return the emitted
 928     /// prims — the shared source for the reverse bridges (`extra_quads`, `all_rounded_quads`,
 929     /// `extra_circles`, `extra_arcs`, prim-derived `text_labels`) that legacy render loops read.
 930     fn painted_prims(&self) -> Vec<Prim> {
 931         let mut pc = PaintCtx::new();
 932         Paint::paint(&self.inner, self.content_rect(), &mut pc);
 933         pc.finish().items.into_iter().map(|item| item.prim).collect()
 934     }
 935 
 936     /// This widget's OWN plain-quad prims from [`Paint::paint`] — the shared source for the
 937     /// `extra_quads`/`all_quads` reverse bridges (kept separate from `extra_quads` itself,
 938     /// which may serve the child aggregation instead —
 939     /// [`Paint::aggregates_child_extra_quads`]).
 940     fn own_plain_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
 941         self.painted_prims()
 942             .into_iter()
 943             .filter_map(|prim| match prim {
 944                 Prim::Quad { rect, color } => Some((rect.x, rect.y, rect.width, rect.height, color)),
 945                 _ => None,
 946             })
 947             .collect()
 948     }
 949 
 950     /// The container's children that pass the [`Layout::child_visible`] policy — the set the
 951     /// adapter's subtree plumbing (aggregation, recursion, hit-through) operates on. Empty for
 952     /// non-containers.
 953     fn visible_children(&self) -> Vec<*mut (dyn WidgetHost + 'static)> {
 954         if !Layout::has_container_children(&self.inner) {
 955             return Vec::new();
 956         }
 957         Layout::container_children(&self.inner)
 958             .into_iter()
 959             .filter(|c| Layout::child_visible(&self.inner, *c))
 960             .collect()
 961     }
 962 
 963     /// This widget's OWN text (prim-derived + detached base label), before any child
 964     /// aggregation — the shared source for the three text getters.
 965     pub(crate) fn own_text_labels(&self) -> Vec<TextLabel> {
 966         if !self.visible() {
 967             return Vec::new();
 968         }
 969         let mut out: Vec<TextLabel> = self
 970             .painted_prims()
 971             .into_iter()
 972             .filter_map(|prim| match prim {
 973                 Prim::Text { text, x, y, font_size, color, .. } => {
 974                     Some(TextLabel { text, x, y, font_size, color })
 975                 }
 976                 _ => None,
 977             })
 978             .collect();
 979         if !Layout::inline_label(&self.inner) {
 980             out.extend(self.base_label_fallback());
 981         }
 982         out
 983     }
 984 
 985     /// The paint-walk view of `own_labels_with_font_and_bounds`: prim-derived text carries the
 986     /// widget's content font ([`Paint::text_font`]); the detached base label is drawn in the
 987     /// configured detached-label font either way (via `own_labels_with_prim_font`).
 988     fn own_labels_for_walk(&self, ctx: &UiContext) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
 989         self.own_labels_with_prim_font(ctx, Paint::text_font(&self.inner))
 990     }
 991 
 992     fn own_labels_with_prim_font(&self, _ctx: &UiContext, prim_font: Option<String>) -> Vec<(TextLabel, Option<String>, Option<[f32; 4]>)> {
 993         // The detached label is the adapter's, not the widget's: one font for every
 994         // control's label — `style.control.label.font_detached`, whose size
 995         // `base_label_fallback` already takes — whatever font the widget's own content
 996         // uses (a TreeList's rows, a Breadcrumb's segments) or does not declare. A
 997         // widget with no `widget_font` used to fall back to the engine's sans default
 998         // here, so half the gallery's labels were in a different face.
 999         let base_font = Some(crate::layout::control_label_font_detached());
1000         let mut fonted: Vec<(TextLabel, Option<String>)> = Vec::new();
1001         if self.visible() {
1002             fonted.extend(
1003                 self.painted_prims()
1004                     .into_iter()
1005                     .filter_map(|prim| match prim {
1006                         Prim::Text { text, x, y, font_size, color, .. } => {
1007                             Some((TextLabel { text, x, y, font_size, color }, prim_font.clone()))
1008                         }
1009                         _ => None,
1010                     }),
1011             );
1012             if !Layout::inline_label(&self.inner) {
1013                 fonted.extend(self.base_label_fallback().into_iter().map(|l| (l, base_font.clone())));
1014             }
1015         }
1016 
1017         if let Some(bounds) = Paint::text_bounds(&self.inner, self.content_rect()) {
1018             return fonted
1019                 .into_iter()
1020                 .map(|(l, font)| (l, font, Some(bounds)))
1021                 .collect();
1022         }
1023 
1024         // The legacy scroll-ancestor clamp ended here: always a no-op since Phase 6av —
1025         // ScrollBox (the last scroll ancestor type) never appeared as a tree parent.
1026         fonted
1027             .into_iter()
1028             .map(|(l, font)| (l, font, None::<[f32; 4]>))
1029             .collect::<Vec<_>>()
1030     }
1031 
1032     /// The base-label text of a *detached*-label widget — a replica of the legacy default
1033     /// `WidgetHost::text_labels` body (which an overriding impl can no longer call).
1034     fn base_label_fallback(&self) -> Vec<TextLabel> {
1035         let b = &self.base;
1036         if let Some(ref label) = b.label {
1037             let (_, font_size) = crate::layout::control_label_font_detached_parsed();
1038             let color = crate::colors::control_label_color_detached_for_state(b.hovered, b.focused);
1039             let inset = Layout::detached_label_inset(&self.inner);
1040             return vec![TextLabel { text: label.clone(), x: b.x + inset, y: b.y, font_size, color }];
1041         }
1042         Vec::new()
1043     }
1044 }
1045 
1046 /// Auto-deref to the wrapped widget, so call sites keep using a migrated widget's own state and
1047 /// methods directly (`dot.status`, `dot.set_status(..)`) without knowing about the wrapper.
1048 /// (By-value builders can't flow through `Deref` — those get mirrored per-widget, like
1049 /// `with_label` here or `UsageBar::with_colors`.)
1050 impl<W: Layout + Paint + Input + 'static> std::ops::Deref for Adapted<W> {
1051     type Target = W;
1052     fn deref(&self) -> &W {
1053         &self.inner
1054     }
1055 }
1056 
1057 impl<W: Layout + Paint + Input + 'static> std::ops::DerefMut for Adapted<W> {
1058     fn deref_mut(&mut self) -> &mut W {
1059         &mut self.inner
1060     }
1061 }
1062 
1063 impl<W: Layout + Paint + Input + 'static> WidgetHost for Adapted<W> {
1064     fn base(&self) -> &Widget {
1065         &self.base
1066     }
1067     fn base_mut(&mut self) -> &mut Widget {
1068         &mut self.base
1069     }
1070     // `as_any` exposes the *inner* widget: legacy code downcasts by concrete widget type
1071     // (`json_layout`'s `downcast_mut::<Checkbox>()`), and the adapter must be transparent to it.
1072     fn as_any(&self) -> &dyn std::any::Any {
1073         &self.inner
1074     }
1075     fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
1076         &mut self.inner
1077     }
1078     fn set_visible(&mut self, visible: bool) {
1079         if self.visible != visible {
1080             self.visible = visible;
1081             Input::visibility_changed(&mut self.inner, visible);
1082         }
1083     }
1084     fn visible(&self) -> bool {
1085         self.visible
1086     }
1087 
1088     // --- Container concern: tree lifecycle, child layout, and subtree recursion. The tree
1089     // itself stays in `ctx.tree` (the WidgetHost defaults' store); a container model additionally
1090     // keeps its own pointer Vec via the `Layout` hooks, because `set_rect`-time arrangement
1091     // has no ctx to reach the tree.
1092 
1093     fn is_child_visible(&self, child_id: WidgetId) -> bool {
1094         if !Layout::has_container_children(&self.inner) {
1095             return true;
1096         }
1097         for child in Layout::container_children(&self.inner) {
1098             if unsafe { (*child).base().id() } == child_id {
1099                 return Layout::child_visible(&self.inner, child);
1100             }
1101         }
1102         false
1103     }
1104 
1105     fn z_index(&self) -> i32 {
1106         Layout::z_order(&self.inner)
1107     }
1108 
1109     fn set_modifiers(&mut self, ctrl: bool, shift: bool, alt: bool) {
1110         Input::set_modifiers(&mut self.inner, ctrl, shift, alt)
1111     }
1112 
1113     fn focused(&self, _ctx: &UiContext) -> bool {
1114         Input::is_focused(&self.inner, self.base.focused)
1115     }
1116 
1117 
1118     fn layout(&mut self, origin: crate::widget::Point, constraints: crate::widget::LayoutConstraints, ctx: &mut UiContext) {
1119         // The WidgetHost default (measure + set_rect), plus recursive child layout for visible
1120         // containers — the ctx-carrying half of the arrangement the model can't do in
1121         // `arrange_children`.
1122         // `origin` is the CONTENT box's top-left and `measure` its height; the detached
1123         // label hangs in the strip above, so the block `set_rect` takes starts `strip`
1124         // higher and is `strip` taller.
1125         let size = self.measure(constraints, ctx);
1126         let strip = self.label_strip();
1127         self.set_rect(origin.x, origin.y - strip, size.width, size.height + strip);
1128         let host_id = self.base.id();
1129         Layout::register_embedded_children(&mut self.inner, host_id, ctx);
1130     }
1131 
1132     fn prepare_text(&mut self, fs: &mut cosmic_text::FontSystem) {
1133         if self.visible() {
1134             let rect = self.content_rect();
1135             Paint::prepare_text(&mut self.inner, fs, rect);
1136             for child in self.visible_children() {
1137                 unsafe { (*child).prepare_text(fs) };
1138             }
1139         }
1140     }
1141 
1142     fn popover_rect(&self) -> Option<(f32, f32, f32, f32)> {
1143         if !self.visible() {
1144             return None;
1145         }
1146         Paint::popover(&self.inner, self.content_rect())
1147             .or_else(|| self.visible_children().into_iter().find_map(|c| unsafe { &*c }.popover_rect()))
1148     }
1149 
1150     fn render_popover(&self, pc: &mut dyn crate::layout::RenderTarget) {
1151         if !self.visible() {
1152             return;
1153         }
1154         Paint::draw_popover(&self.inner, self.content_rect(), pc);
1155         for child in self.visible_children() {
1156             unsafe { &*child }.render_popover(pc);
1157         }
1158     }
1159 
1160     // --- Legacy structural conventions the adapter owns on the widget's behalf ---
1161 
1162     /// The assigned rect is the widget's whole block: the detached label strip (if any)
1163     /// at its top, the content below (`content_rect`). One convention for every
1164     /// control — a caller sizing a labeled widget by hand adds `label_strip` to the
1165     /// content height; `layout` does that for it.
1166     fn set_rect(&mut self, x: f32, y: f32, w: f32, h: f32) {
1167         let r = Layout::adjust_rect(&self.inner, Rect { x, y, width: w, height: h });
1168         self.base.x = r.x;
1169         self.base.y = r.y;
1170         self.base.w = r.width;
1171         self.base.h = r.height;
1172         // Ungated rect notification (TextBox re-clamps scroll on every assignment, hidden or
1173         // not — the legacy `set_rect` side effect).
1174         let landed = Rect { x: self.base.x, y: self.base.y, width: self.base.w, height: self.base.h };
1175         Layout::rect_assigned(&mut self.inner, landed);
1176         // Containers position their children from the assigned rect (legacy `set_rect`
1177         // overrides); hidden containers skip it, like the legacy impls.
1178         if self.visible {
1179             let content = self.content_rect();
1180             let host = self.as_ptr_mut();
1181             Layout::arrange_children(&mut self.inner, content, host);
1182         }
1183     }
1184 
1185     /// The intrinsic content height — the control below the label.
1186     fn preferred_height(&self) -> Option<f32> {
1187         Layout::intrinsic_size(&self.inner).map(|s| s.height)
1188     }
1189 
1190     fn label_strip(&self) -> f32 {
1191         if Layout::inline_label(&self.inner) { 0.0 } else { self.base.label_offset() }
1192     }
1193 
1194     /// The detached label's box, as `base_label_fallback` places the text: at the
1195     /// label inset on the strip above the content, measured in the detached-label font.
1196     fn detached_label_rect(&self) -> Option<Rect> {
1197         if Layout::inline_label(&self.inner) {
1198             return None;
1199         }
1200         let label = self.base.label.as_deref()?;
1201         let (fam, size) = crate::layout::control_label_font_detached_parsed();
1202         let width = crate::widget::display::measure_text_width(label, &fam, size);
1203         Some(Rect { x: self.base.x + Layout::detached_label_inset(&self.inner), y: self.base.y, width, height: self.label_strip() })
1204     }
1205 
1206     /// The `WidgetHost::measure` default, except the width consults the intrinsic size when the
1207     /// widget opts in ([`Layout::intrinsic_measure_width`] — Dropdown's `auto_width`).
1208     fn measure(&self, constraints: crate::widget::LayoutConstraints, _ctx: &UiContext) -> crate::widget::Size {
1209         let (_, _, w, h) = self.rect();
1210         let pref_w = if Layout::intrinsic_measure_width(&self.inner) {
1211             Layout::intrinsic_size(&self.inner).map_or(w, |s| s.width)
1212         } else {
1213             w
1214         };
1215         // Content height: the intrinsic one, else the landed rect less its label strip.
1216         let pref_h = self.preferred_height().unwrap_or(h - self.label_strip());
1217         crate::widget::Size {
1218             width: pref_w.clamp(constraints.min_width, constraints.max_width),
1219             height: pref_h.clamp(constraints.min_height, constraints.max_height),
1220         }
1221     }
1222 
1223     /// Narrow widgets own every pixel they draw through [`Paint::paint`]; the legacy shared
1224     /// hover-highlight overlay is suppressed (matching what most control widgets' `None`
1225     /// overrides do today) — unless the widget opts back in
1226     /// ([`Paint::legacy_focus_highlight`], TextBox), in which case this replicates the
1227     /// `WidgetHost` default byte-for-byte: primary tint when ctx-focused (or active), secondary
1228     /// when hovered, over the row-substituted span.
1229     fn highlight_quad(&self, ctx: &UiContext) -> Option<(f32, f32, f32, f32, [f32; 4])> {
1230         // A forwarding widget (Paginator → its ButtonStrip) serves the forwarded value here —
1231         // and only here; `all_quads`/`paint_self` gate on `legacy_focus_highlight` instead, so
1232         // the forwarded quad is never double-drawn.
1233         if let Some(forwarded) = Paint::forwarded_highlight(&self.inner, ctx) {
1234             return forwarded;
1235         }
1236         if !Paint::legacy_focus_highlight(&self.inner) {
1237             return None;
1238         }
1239         let is_focused = ctx.is_focused_id(self.base.id());
1240         let hc = if is_focused {
1241             crate::colors::highlight_primary_color()
1242         } else if self.base.hovered {
1243             crate::colors::HIGHLIGHT_SECONDARY
1244         } else {
1245             return None;
1246         };
1247         let hx = if self.base.row_w > 0.0 { self.base.row_x } else { self.base.x };
1248         let hw = if self.base.row_w > 0.0 { self.base.row_w } else { self.base.w };
1249         Some((hx, self.base.y, hw, self.base.h, hc))
1250     }
1251 
1252     /// Report the *inner* type's name, not `Adapted<W>`: runtime type-name matching (e.g.
1253     /// `layout.rs`' span-full widget list) must keep seeing the widget it knows.
1254     fn type_name(&self) -> &'static str {
1255         std::any::type_name::<W>().split("::").last().unwrap_or("Widget")
1256     }
1257 
1258     // --- Paint concern -> `Paint` ---
1259     fn color(&self) -> [f32; 4] {
1260         Paint::color(&self.inner)
1261     }
1262     fn clips_children(&self) -> bool {
1263         Paint::clips_children(&self.inner)
1264     }
1265     fn corner_style(&self) -> (f32, (bool, bool, bool, bool)) {
1266         // 12.0 / all-off mirrors the `WidgetHost` default for widgets without a corner style.
1267         Paint::corner_style(&self.inner, self.content_rect())
1268             .unwrap_or((12.0, (false, false, false, false)))
1269     }
1270     fn focus_role(&self) -> FocusRole {
1271         Input::focus_role(&self.inner)
1272     }
1273     fn solid_border(&self) -> Option<([f32; 4], f32)> {
1274         Paint::solid_border(&self.inner)
1275     }
1276     fn widget_font(&self) -> Option<String> {
1277         Paint::widget_font(&self.inner)
1278     }
1279     /// Scene-path emission. Geometry comes from [`Paint::paint`]; its plain `Text` prims are
1280     /// REPLACED by the same font+bounds view the standard text bridges serve
1281     /// (`own_labels_with_font_and_bounds`, or the per-label hatch), so a display list built by
1282     /// the paint walk carries per-widget fonts and clip rects (Phase 6 — text ordering
1283     /// relative to geometry is immaterial: glyphs always render in the later text pass).
1284     fn renders_own_subtree(&self) -> bool {
1285         Paint::paints_own_subtree(&self.inner)
1286     }
1287 
1288     fn paint_self(&self, ui: &UiContext, ctx: &mut PaintCtx) {
1289         let mut tmp = PaintCtx::new();
1290         Paint::paint_ui(&self.inner, ui, self.content_rect(), &mut tmp);
1291         // Subtree painters (paints_own_subtree) author their COMPLETE text in paint() —
1292         // per-child fonts and clip bounds included — so their Text prims pass through
1293         // verbatim and the single-font own-labels re-derivation below is skipped
1294         // (re-deriving would flatten a composite's mixed child fonts to widget_font).
1295         let subtree = Paint::paints_own_subtree(&self.inner);
1296         for item in tmp.finish().items {
1297             // Re-emitting through ctx re-records clip state, so restore the
1298             // circular clip the widget authored the prim under (Ramp's
1299             // foam-cell fills) — it would otherwise be dropped here.
1300             let clip_circle = item.clip_circle;
1301             if let Some(c) = clip_circle {
1302                 ctx.push_clip_circle(c);
1303             }
1304             // `replay` emits every prim but Text and hands Text back — the two
1305             // callers disagree about it. A subtree painter authored its own text
1306             // (per-child fonts and clips) so that passes through verbatim;
1307             // otherwise it is dropped in favour of the own-labels bridge below.
1308             if let Some(Prim::Text { text, x, y, font_size, color, font, bounds, .. }) =
1309                 ctx.replay(item.prim)
1310             {
1311                 if subtree {
1312                     ctx.text_with(text, x, y, font_size, color, font, bounds);
1313                 }
1314             }
1315             if clip_circle.is_some() {
1316                 ctx.pop_clip_circle();
1317             }
1318         }
1319         // The legacy default `paint_self` drained `all_quads`, which carries the focus
1320         // highlight — replicate for opt-in widgets, over the background (same draw order).
1321         // Forwarded highlights stay out: the paint walk reaches the owning child itself.
1322         if Paint::legacy_focus_highlight(&self.inner) {
1323             if let Some((hx, hy, hw, hh, hc)) = WidgetHost::highlight_quad(self, ui) {
1324                 if hc != crate::colors::HIGHLIGHT_SECONDARY {
1325                     ctx.quad(Rect { x: hx, y: hy, width: hw, height: hh }, hc);
1326                 }
1327             }
1328         }
1329         // Own text with per-label font+bounds: the hatch view verbatim for hatched widgets
1330         // (caveat: its contract includes raw container children — those few widgets keep the
1331         // hatch until their hosts adopt the walk), else the standard own-labels bridge (prim
1332         // text + the detached base label, one font, text_bounds or the scroll-ancestor clip).
1333         if subtree {
1334             // The detached label is the adapter's, not the widget's: a subtree painter
1335             // authors its own text but knows nothing of the label strip above its
1336             // content (TreeList, Spreadsheet, Ramp), so the bridge's base-label half
1337             // still runs for it — in the detached-label font, like every control's.
1338             // Skipping it left a labelled tree's strip reserved but blank.
1339             if self.visible() && !Layout::inline_label(&self.inner) {
1340                 let font = Some(crate::layout::control_label_font_detached());
1341                 for tl in self.base_label_fallback() {
1342                     ctx.text_with(tl.text, tl.x, tl.y, tl.font_size, tl.color, font.clone(), None);
1343                 }
1344             }
1345             return;
1346         }
1347         let labels = if Paint::serves_legacy_labels(&self.inner) {
1348             Paint::legacy_labels_with_font_and_bounds(&self.inner, self.content_rect(), ui)
1349         } else {
1350             self.own_labels_for_walk(ui)
1351         };
1352         for (tl, font, bounds) in labels {
1353             ctx.text_with(tl.text, tl.x, tl.y, tl.font_size, tl.color, font, bounds);
1354         }
1355     }
1356 
1357     // The legacy per-widget text getters are deleted from `WidgetHost`: this adapter's text
1358     // reaches the frame through `paint_self` above (prim-derived own labels + the
1359     // detached base label), and composites that need a concrete Adapted child's labels
1360     // call `own_labels_with_font_and_bounds` directly (pub(crate)).
1361 
1362     // --- Reverse bridges: [`Paint::paint`] output converted back to the legacy geometry
1363     // getters external render loops read (cce-test-interface's `all_*` calls, `render_widget`'s
1364     // `all_quads` loop, the demo's `extra_*` loops). Each prim kind maps to the getter legacy
1365     // widgets used for it — plain quads to `extra_quads` (→ `all_quads`), rounded to
1366     // `all_rounded_quads` — so apps that read BOTH getters draw each prim exactly once. Covers
1367     // the widget's OWN geometry only: adapted widgets are leaves for now; recursion belongs to
1368     // `scene::painter`.
1369 
1370     fn all_rounded_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, f32, [f32; 4], (bool, bool, bool, bool))> {
1371         if !self.visible() {
1372             return Vec::new();
1373         }
1374         let mut out: Vec<_> = self
1375             .painted_prims()
1376             .into_iter()
1377             .filter_map(|prim| match prim {
1378                 Prim::RoundedRect { rect, radius, corners, color } => {
1379                     Some((rect.x, rect.y, rect.width, rect.height, radius, color, corners))
1380                 }
1381                 _ => None,
1382             })
1383             .collect();
1384         // Containers recurse, matching the `WidgetHost` default this override replaces.
1385         for child in self.visible_children() {
1386             out.extend(unsafe { &*child }.all_rounded_quads(ctx));
1387         }
1388         out
1389     }
1390 
1391     fn extra_quads(&self) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
1392         if !self.visible() {
1393             return Vec::new();
1394         }
1395         if Paint::serves_legacy_plain_quads(&self.inner) {
1396             return Paint::legacy_plain_quads(&self.inner, self.content_rect());
1397         }
1398         // Legacy container aggregation (Paginator): the plain view is the visible children's
1399         // chrome, and only that — the widget's own background stays in `all_quads`.
1400         if Paint::aggregates_child_extra_quads(&self.inner) {
1401             let mut out = Vec::new();
1402             for child in self.visible_children() {
1403                 out.extend(unsafe { &*child }.extra_quads());
1404             }
1405             return out;
1406         }
1407         self.own_plain_quads()
1408     }
1409 
1410     /// When the widget serves a legacy plain-quad view, its geometry reaches
1411     /// `render_widget`-style hosts (which read BOTH quad getters) through `all_rounded_quads`
1412     /// only — `all_quads` must stay empty or they draw it twice. Mirrors legacy Graph's
1413     /// highlight-only `all_quads` override. Otherwise: the `WidgetHost` default minus the shared
1414     /// highlight (suppressed for all adapted widgets via `highlight_quad -> None`).
1415     fn all_quads(&self, ctx: &UiContext) -> Vec<(f32, f32, f32, f32, [f32; 4])> {
1416         if Paint::serves_legacy_plain_quads(&self.inner) {
1417             return Vec::new();
1418         }
1419         // Own prims directly (NOT `extra_quads`, which may serve the child aggregation — those
1420         // children arrive once, through the recursion below).
1421         let mut quads = if self.visible() { self.own_plain_quads() } else { Vec::new() };
1422         // The `WidgetHost` default's highlight inclusion (secondary/hover tint excluded), live
1423         // only for widgets that opt into the legacy overlay. A forwarded highlight
1424         // ([`Paint::forwarded_highlight`]) is deliberately excluded: its owner's aggregation
1425         // already carries it, matching the legacy container `all_quads` overrides.
1426         if Paint::legacy_focus_highlight(&self.inner) {
1427             if let Some(hq) = WidgetHost::highlight_quad(self, ctx) {
1428                 if hq.4 != crate::colors::HIGHLIGHT_SECONDARY {
1429                     quads.push(hq);
1430                 }
1431             }
1432         }
1433         if self.visible() {
1434             // Container aggregation, replicating the shared legacy loop (Layer, Switcher):
1435             // children contribute their plain quads, except a rounded-cornered child's
1436             // background quad — that one arrives through `all_rounded_quads` instead.
1437             for child in self.visible_children() {
1438                 let widget = unsafe { &*child };
1439                 let (wx, wy, ww, wh) = widget.rect();
1440                 let has_rounded = widget.corner_style().1 != (false, false, false, false);
1441                 for (qx, qy, qw, qh, qc) in widget.all_quads(ctx) {
1442                     if has_rounded
1443                         && (qx - wx).abs() < 0.1
1444                         && (qy - wy).abs() < 0.1
1445                         && (qw - ww).abs() < 0.1
1446                         && (qh - wh).abs() < 0.1
1447                     {
1448                         continue;
1449                     }
1450                     quads.push((qx, qy, qw, qh, qc));
1451                 }
1452             }
1453         }
1454         quads
1455     }
1456 
1457     fn extra_circles(&self) -> Vec<(f32, f32, f32, [f32; 4])> {
1458         if !self.visible() {
1459             return Vec::new();
1460         }
1461         self.painted_prims()
1462             .into_iter()
1463             .filter_map(|prim| match prim {
1464                 Prim::Circle { cx, cy, radius, color } => Some((cx, cy, radius, color)),
1465                 _ => None,
1466             })
1467             .collect()
1468     }
1469 
1470     fn extra_arcs(&self) -> Vec<(f32, f32, f32, f32, f32, f32, [f32; 4])> {
1471         if !self.visible() {
1472             return Vec::new();
1473         }
1474         self.painted_prims()
1475             .into_iter()
1476             .filter_map(|prim| match prim {
1477                 Prim::Arc { cx, cy, radius, thickness, start, end, color } => {
1478                     Some((cx, cy, radius, thickness, start, end, color))
1479                 }
1480                 _ => None,
1481             })
1482             .collect()
1483     }
1484 
1485     // --- Input concern -> `Input` ---
1486     fn blocks_root_plate_drag(&self) -> bool {
1487         Input::blocks_root_plate_drag(&self.inner)
1488     }
1489     fn context_action(&mut self, action: crate::widget::ContextAction) -> bool {
1490         Input::context_action(&mut self.inner, action)
1491     }
1492     /// Row-rect assignment (row-layout hosts): apply the widget's clamp
1493     /// ([`Layout::adjust_row_rect`] — TextBox's `width`/`max_width`), then the base write the
1494     /// `WidgetHost` default does.
1495     fn set_row_rect(&mut self, x: f32, w: f32) {
1496         let (rx, rw) = Layout::adjust_row_rect(&self.inner, x, w);
1497         self.base.row_x = rx;
1498         self.base.row_w = rw;
1499     }
1500     fn tick(&mut self, dt: f32, ctx: &mut UiContext) -> bool {
1501         // Legacy value-owning containers healed their children's registry entries every tick
1502         // (addresses move with the owning struct); same cadence here.
1503         let host_id = self.base.id();
1504         Layout::register_embedded_children(&mut self.inner, host_id, ctx);
1505         let rect = self.content_rect();
1506         let mut changed = Input::tick(&mut self.inner, dt, rect);
1507         {
1508             let self_ptr = self.as_ptr_mut();
1509             let mut ectx = EventCtx { rect, id: host_id, ui: Some(&mut *ctx), self_ptr: Some(self_ptr) };
1510             changed |= Input::tick_ctx(&mut self.inner, dt, &mut ectx);
1511         }
1512         if self.visible {
1513             for child in self.visible_children() {
1514                 changed |= unsafe { &mut *child }.tick(dt, ctx);
1515             }
1516         }
1517         changed
1518     }
1519     fn wants_tick(&self) -> bool {
1520         Input::wants_tick(&self.inner)
1521     }
1522     fn is_scrollable(&self) -> bool {
1523         Input::scrollable(&self.inner)
1524     }
1525     /// Focus set/cleared directly (hosts call `w.focus()`/`w.unfocus()`): keep the base flag
1526     /// (unless the widget opts out — [`Input::tracks_base_focus`], TextBox's legacy `focus`
1527     /// never set it) and tell the widget via the same `FocusIn`/`FocusOut` events the router
1528     /// would send.
1529     fn focus(&mut self) {
1530         if Input::tracks_base_focus(&self.inner) {
1531             self.base.focused = true;
1532         }
1533         let self_ptr = self.as_ptr_mut();
1534         let mut ectx = EventCtx { rect: self.content_rect(), id: self.base.id(), ui: None, self_ptr: Some(self_ptr) };
1535         Input::on_event(&mut self.inner, &Event::FocusIn, &mut ectx);
1536     }
1537     fn unfocus(&mut self) {
1538         if Input::tracks_base_focus(&self.inner) {
1539             self.base.focused = false;
1540         }
1541         let self_ptr = self.as_ptr_mut();
1542         let mut ectx = EventCtx { rect: self.content_rect(), id: self.base.id(), ui: None, self_ptr: Some(self_ptr) };
1543         Input::on_event(&mut self.inner, &Event::FocusOut, &mut ectx);
1544     }
1545 
1546     fn hit_test(&self, px: f32, py: f32, ctx: &UiContext) -> bool {
1547         // Hidden widgets are not hittable. Legacy widgets with a visibility toggle (Spreadsheet)
1548         // carry this gate themselves — and need it: hosts broadcast wheel/press dispatch to
1549         // every widget (the designer) and rely on hidden ones rejecting the hit.
1550         if !self.visible() {
1551             return false;
1552         }
1553         // Containers with a hit-through policy delegate entirely to their visible children
1554         // (each child runs its own coverage check) — the legacy Layer/Switcher pattern, which
1555         // never consulted the container's own rect or coverage.
1556         if Input::hits_through_children(&self.inner) {
1557             return self
1558                 .visible_children()
1559                 .into_iter()
1560                 .any(|c| unsafe { &*c }.hit_test(px, py, ctx));
1561         }
1562         // Preserve the legacy occlusion check (a covering layer swallows the hit), then delegate
1563         // the geometric test to the narrow trait instead of the row/label-offset machinery.
1564         if ctx.is_coordinate_covered(self.base.id(), px, py) {
1565             return false;
1566         }
1567         let (x, y, w, h) = self.rect();
1568         // Row-hit opt-in ([`Layout::hit_row_rect`]): replicate the legacy `hit_test` default's
1569         // geometry — substitute the host-pushed row span — before
1570         // the narrow test. The width<=0 reject also comes from that default.
1571         if Layout::hit_row_rect(&self.inner) {
1572             if w <= 0.0 || h <= 0.0 {
1573                 return false;
1574             }
1575             let (hx, hw) = if self.base.row_w > 0.0 { (self.base.row_x, self.base.row_w) } else { (x, w) };
1576             return Input::hit(&self.inner, Rect { x: hx, y, width: hw, height: h }, px, py);
1577         }
1578         Input::hit(&self.inner, Rect { x, y, width: w, height: h }, px, py)
1579     }
1580 
1581     fn handle_event(&mut self, event: &Event, ctx: &mut UiContext) -> bool {
1582         let rect = self.content_rect();
1583         let id = self.base.id();
1584         let self_ptr = self.as_ptr_mut();
1585         macro_rules! ectx {
1586             () => {
1587                 EventCtx { rect, id, ui: Some(ctx), self_ptr: Some(self_ptr) }
1588             };
1589         }
1590         match event {
1591             // A hit right-press on a context-menu widget routes to the shared config menu —
1592             // `on_event` can't (that policy needs the target's WidgetHost pointer), so the adapter
1593             // owns it.
1594             Event::MouseButton {
1595                 button: crate::widget::MouseButton::Right,
1596                 state: crate::widget::ElementState::Pressed,
1597                 x: px,
1598                 y: py,
1599                 ..
1600             } if Input::opens_context_menu(&self.inner) => {
1601                 if self.hit_test(*px, *py, ctx) {
1602                     ctx.handle_right_click(self_ptr, *px, *py);
1603                     return true;
1604                 }
1605                 false
1606             }
1607             // Hit-gate PRESSES and wheel once, here, so narrow widgets never carry the
1608             // per-widget "check hit_test first" boilerplate legacy `mouse_input` overrides do.
1609             // RELEASES are deliberately NOT gated: a press-tracking widget (Button) must see the
1610             // release wherever the cursor ended up, to commit or cancel — exactly what legacy
1611             // `mouse_input` overrides did by receiving every release. Event-proxying containers
1612             // opt out of the press gate (`Input::gates_presses`): legacy container overrides
1613             // saw every press (Switcher unfocuses its child on an outside press).
1614             Event::MouseButton { state: crate::widget::ElementState::Pressed, x: px, y: py, .. }
1615                 if !Input::gates_presses(&self.inner) =>
1616             {
1617                 let _ = (px, py);
1618                 Input::on_event(&mut self.inner, event, &mut ectx!())
1619             }
1620             Event::MouseButton { state: crate::widget::ElementState::Pressed, x: px, y: py, .. }
1621             | Event::MouseWheel { x: px, y: py, .. } => {
1622                 self.hit_test(*px, *py, ctx) && Input::on_event(&mut self.inner, event, &mut ectx!())
1623             }
1624             Event::MouseButton { state: crate::widget::ElementState::Released, .. } => {
1625                 Input::on_event(&mut self.inner, event, &mut ectx!())
1626             }
1627             // Offer the raw move to the widget; if unconsumed, run the legacy hover bookkeeping
1628             // (base.hovered + MouseEnter/MouseLeave synthesis, which re-enters this method and
1629             // reaches `on_event` through the arm below).
1630             Event::PointerMove { x: px, y: py, .. } => {
1631                 if Input::on_event(&mut self.inner, event, &mut ectx!()) {
1632                     return true;
1633                 }
1634                 let (px, py) = (*px, *py);
1635                 self.cursor_moved(px, py, ctx)
1636             }
1637             // The router's drag lifecycle (recorded drag target → DragStart/DragUpdate/
1638             // DragEnd) maps to the Input drag hooks, exactly like the direct
1639             // `WidgetHost::drag_*` entry points below — `on_event` is offered first, but no
1640             // widget consumes Drag* there today; without these arms the events fell into the
1641             // on_event default and every ROUTED drag was silently dead (the reason each app
1642             // historically kept its own held-drag index and called drag_update directly).
1643             Event::DragStart { start_x, start_y } => {
1644                 if Input::on_event(&mut self.inner, event, &mut ectx!()) {
1645                     return true;
1646                 }
1647                 Input::drag_begin(&mut self.inner, *start_x, *start_y, rect);
1648                 true
1649             }
1650             Event::DragUpdate { x, y, .. } => {
1651                 if Input::on_event(&mut self.inner, event, &mut ectx!()) {
1652                     return true;
1653                 }
1654                 if let Some((nx, ny)) = Input::drag_reposition(&mut self.inner, *x, *y, rect) {
1655                     self.base.x = nx;
1656                     self.base.y = ny;
1657                     return true;
1658                 }
1659                 Input::drag_update(&mut self.inner, *x, *y, rect)
1660             }
1661             Event::DragEnd => {
1662                 if Input::on_event(&mut self.inner, event, &mut ectx!()) {
1663                     return true;
1664                 }
1665                 Input::drag_end(&mut self.inner);
1666                 true
1667             }
1668             // A widget hidden while still holding focus (the designer keys into
1669             // `focused_widget`; hiding a pane doesn't unfocus it) must not consume keys —
1670             // formerly the `keyboard_input` entry point's gate, now on the one funnel
1671             // (which also closes the routed path's missing-gate hole).
1672             Event::KeyInput(_) if !self.visible() => false,
1673             // Everything else (KeyInput, Tick, Enter/Leave, Focus*) forwards directly —
1674             // the legacy default dispatch would route these to leaf handlers Adapted never
1675             // overrides, so there is no behavior to fall back to.
1676             _ => Input::on_event(&mut self.inner, event, &mut ectx!()),
1677         }
1678     }
1679 }
1680 
1681 #[cfg(test)]
1682 mod tests {
1683     /// A subtree painter's own text passes through verbatim, and the adapter still
1684     /// draws the detached label above its content — the widget cannot, it does not
1685     /// know about the label.
1686     #[test]
1687     fn a_subtree_painters_detached_label_is_drawn() {
1688         use crate::scene::paint::PaintCtx;
1689         use crate::widget::{TreeList, WidgetHost};
1690         let ui = crate::context::UiContext::new();
1691         let mut tree = TreeList::new().with_label("TreeList");
1692         let strip = tree.label_strip();
1693         assert!(strip > 0.0);
1694         tree.set_rect(0.0, 0.0, 300.0, 200.0 + strip);
1695         let mut pc = PaintCtx::new();
1696         WidgetHost::paint_self(&tree, &ui, &mut pc);
1697         let texts: Vec<(String, f32)> = pc
1698             .finish()
1699             .items
1700             .into_iter()
1701             .filter_map(|it| match it.prim {
1702                 crate::scene::paint::Prim::Text { text, y, .. } => Some((text, y)),
1703                 _ => None,
1704             })
1705             .collect();
1706         let label = texts.iter().find(|(t, _)| t == "TreeList").expect("the detached label is drawn");
1707         assert_eq!(label.1, 0.0, "on the strip above the content");
1708         assert!(texts.iter().any(|(t, _)| t == "Key"), "the tree's own header text still passes through");
1709     }
1710 
1711     use super::*;
1712     use crate::widget::PathController;
1713     use crate::scene::layout::{Rect, Size};
1714     use crate::scene::paint::Prim;
1715     use crate::scene::painter::paint_tree;
1716     use crate::widget::UiContext;
1717 
1718     /// A leaf that only knows the two narrow concerns — no `WidgetHost` in sight: it reports an
1719     /// intrinsic size ([`Layout`]) and a color ([`Paint`]).
1720     struct Dot {
1721         color: [f32; 4],
1722         size: Size,
1723     }
1724     impl Layout for Dot {
1725         fn intrinsic_size(&self) -> Option<Size> {
1726             Some(self.size)
1727         }
1728     }
1729     impl Paint for Dot {
1730         fn color(&self) -> [f32; 4] {
1731             self.color
1732         }
1733     }
1734     impl Input for Dot {}
1735 
1736     /// A narrow container: it drives a column layout ([`Layout`]) and paints nothing.
1737     struct Col;
1738     impl Layout for Col {}
1739     impl Paint for Col {
1740         fn color(&self) -> [f32; 4] {
1741             [0.0, 0.0, 0.0, 0.0]
1742         }
1743     }
1744     impl Input for Col {}
1745 
1746     fn rect_of(ptr: *mut (dyn WidgetHost + 'static)) -> Rect {
1747         let (x, y, w, h) = unsafe { (*ptr).rect() };
1748         Rect { x, y, width: w, height: h }
1749     }
1750 
1751     /// One rhythm for labeled controls: the preferred height is the CONTENT height
1752     /// for every kind (ProgressBar, Slider, Spinbox, Dropdown), and `layout` places that
1753     /// content at the origin with the label strip hanging above it — so a strategy
1754     /// placing content boxes lines mixed controls up by content, neither squashes a
1755     /// track to the label's leftovers nor lets a label spill into the gap below.
1756     #[test]
1757     fn labeled_controls_land_their_content_at_the_origin_with_the_label_above() {
1758         use crate::widget::{Dropdown, LayoutConstraints, Point, ProgressBar, Slider, Spinbox};
1759         let mut ctx = UiContext::new();
1760         let mut slider = Slider::new().with_label("Gain");
1761         let mut spinbox = Spinbox::new(1, 0, 9, 1).with_label("Count");
1762         let mut dropdown = Dropdown::new(vec!["a".into()], 0).with_label("Pick");
1763         let mut bar = ProgressBar::new(0.5).with_label("Load");
1764         let strip = slider.base.label_offset();
1765         assert!(strip > 0.0, "a detached label has a strip above the content");
1766 
1767         for (name, w, content) in [
1768             ("slider", &mut slider as &mut dyn WidgetHost, crate::layout::slider_height()),
1769             ("spinbox", &mut spinbox, crate::layout::spinbox_height()),
1770             ("dropdown", &mut dropdown, crate::layout::dropdown_height()),
1771             ("progress bar", &mut bar, crate::layout::progressbar_height()),
1772         ] {
1773             let pref = w.preferred_height().expect(name);
1774             assert!((pref - content).abs() < 0.01, "{name}: preferred {pref} is the content height {content}");
1775             assert!((w.label_strip() - strip).abs() < 0.01, "{name}: one label strip");
1776             w.layout(Point { x: 0.0, y: 100.0 }, LayoutConstraints::new(100.0, 100.0, pref, pref), &mut ctx);
1777             let (_, top, _, landed) = w.rect();
1778             assert!((top - (100.0 - strip)).abs() < 0.01, "{name}: the label hangs above the origin (top {top})");
1779             assert!((landed - (content + strip)).abs() < 0.01, "{name}: occupied {landed} = content + strip");
1780             let painted = landed - w.label_strip();
1781             assert!((painted - content).abs() < 0.01, "{name}: content {painted}, wanted {content}");
1782         }
1783     }
1784 
1785     #[test]
1786     fn narrow_widget_lays_out_and_paints_through_the_adapter() {
1787         // A pure narrow-trait widget tree (Col + two Dots), wrapped in `Adapted`, is laid out by
1788         // the existing bridge and painted by the existing painter — proving a widget that never
1789         // touches `WidgetHost` participates in both live passes.
1790         let mut ctx = UiContext::new();
1791         let mut root = Box::new(Adapted::new(Col));
1792         let mut a = Box::new(Adapted::new(Dot { color: [1.0, 0.0, 0.0, 1.0], size: Size::new(10.0, 10.0) }));
1793         let mut b = Box::new(Adapted::new(Dot { color: [0.0, 1.0, 0.0, 1.0], size: Size::new(10.0, 20.0) }));
1794 
1795         let (root_id, root_ptr) = (root.id(), root.as_ptr_mut());
1796         let (a_id, a_ptr) = (a.id(), a.as_ptr_mut());
1797         let (b_id, b_ptr) = (b.id(), b.as_ptr_mut());
1798         ctx.register_widget(root_id, root_ptr);
1799         ctx.register_widget(a_id, a_ptr);
1800         ctx.register_widget(b_id, b_ptr);
1801         ctx.link_ids(root_id, a_id);
1802         ctx.link_ids(root_id, b_id);
1803 
1804         // Layout by hand (the Phase-2b bridge is gone; apps drive the solver directly) —
1805         // the same column-of-two placement the bridge used to compute.
1806         unsafe {
1807             (*root_ptr).set_rect(0.0, 0.0, 100.0, 100.0);
1808             (*a_ptr).set_rect(0.0, 0.0, 10.0, 10.0);
1809             (*b_ptr).set_rect(0.0, 14.0, 10.0, 20.0);
1810         }
1811         assert_eq!(rect_of(a_ptr), Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 });
1812         assert_eq!(rect_of(b_ptr), Rect { x: 0.0, y: 14.0, width: 10.0, height: 20.0 });
1813 
1814         // Paint: each Dot's `Paint::paint` default emits one quad at its laid-out rect, in colour.
1815         let list = paint_tree(&ctx, unsafe { &*root_ptr });
1816         let quads: Vec<_> = list
1817             .items
1818             .iter()
1819             .filter_map(|it| match it.prim {
1820                 Prim::Quad { rect, color } => Some((rect, color)),
1821                 _ => None,
1822             })
1823             .collect();
1824         assert!(
1825             quads.iter().any(|(r, c)| *r == Rect { x: 0.0, y: 0.0, width: 10.0, height: 10.0 } && c[0] == 1.0),
1826             "red Dot painted at its laid-out rect: {quads:?}",
1827         );
1828         assert!(
1829             quads.iter().any(|(r, c)| *r == Rect { x: 0.0, y: 14.0, width: 10.0, height: 20.0 } && c[1] == 1.0),
1830             "green Dot painted at its laid-out rect: {quads:?}",
1831         );
1832     }
1833 
1834     /// A narrow interactive widget: counts left-clicks and records hover transitions — all
1835     /// through [`Input::on_event`], never touching `WidgetHost`.
1836     struct Clicker {
1837         clicks: u32,
1838         entered: u32,
1839         left: u32,
1840     }
1841     impl Layout for Clicker {}
1842     impl Paint for Clicker {
1843         fn color(&self) -> [f32; 4] {
1844             [0.5, 0.5, 0.5, 1.0]
1845         }
1846     }
1847     impl Input for Clicker {
1848         fn on_event(&mut self, event: &Event, _ectx: &mut EventCtx) -> bool {
1849             use crate::widget::{ElementState, MouseButton};
1850             match event {
1851                 Event::MouseButton { button: MouseButton::Left, state: ElementState::Pressed, .. } => {
1852                     self.clicks += 1;
1853                     true
1854                 }
1855                 Event::MouseEnter => {
1856                     self.entered += 1;
1857                     false
1858                 }
1859                 Event::MouseLeave => {
1860                     self.left += 1;
1861                     false
1862                 }
1863                 _ => false,
1864             }
1865         }
1866     }
1867 
1868     #[test]
1869     fn narrow_widget_receives_routed_events_through_the_adapter() {
1870         use crate::widget::{ElementState, MouseButton};
1871         let mut ctx = UiContext::new();
1872         let mut w = Box::new(Adapted::new(Clicker { clicks: 0, entered: 0, left: 0 }));
1873         let (id, ptr) = (w.id(), w.as_ptr_mut());
1874         ctx.register_widget(id, ptr);
1875         unsafe { (*ptr).set_rect(10.0, 10.0, 40.0, 20.0) };
1876 
1877         let click_at = |x: f32, y: f32| Event::MouseButton {
1878             button: MouseButton::Left,
1879             state: ElementState::Pressed,
1880             x,
1881             y,
1882             local_x: x,
1883             local_y: y,
1884         };
1885 
1886         // A click inside the rect is hit-gated in, consumed, and counted.
1887         assert!(ctx.propagate_event(&click_at(20.0, 15.0), id), "in-rect click is consumed");
1888         // A click outside never reaches on_event (the adapter's hit gate rejects it).
1889         assert!(!ctx.propagate_event(&click_at(200.0, 200.0), id), "out-of-rect click passes through");
1890         assert_eq!(w.inner().clicks, 1, "only the in-rect click was counted");
1891 
1892         // Hover: moving inside synthesizes MouseEnter (via the legacy bookkeeping the adapter
1893         // preserves) and sets the base hover flag; moving away synthesizes MouseLeave.
1894         ctx.propagate_event(&Event::PointerMove { x: 20.0, y: 15.0, local_x: 20.0, local_y: 15.0 }, id);
1895         assert_eq!(w.inner().entered, 1, "MouseEnter reached on_event");
1896         assert!(unsafe { (*ptr).base().hovered }, "base hover flag set through the adapter");
1897         ctx.propagate_event(&Event::PointerMove { x: 200.0, y: 200.0, local_x: 200.0, local_y: 200.0 }, id);
1898         assert_eq!(w.inner().left, 1, "MouseLeave reached on_event");
1899         assert!(!unsafe { (*ptr).base().hovered }, "base hover flag cleared");
1900     }
1901 
1902     /// A narrow widget that is also a controller: the controller trait is reached through the
1903     /// concrete `Adapted<W>` by deref (Phase 6aw -- the `WidgetHost::as_*_controller` discovery
1904     /// hooks are deleted).
1905     struct Crumbs {
1906         segs: Vec<String>,
1907         clicked: Option<usize>,
1908     }
1909     impl Layout for Crumbs {}
1910     impl Paint for Crumbs {
1911         fn color(&self) -> [f32; 4] {
1912             [0.0; 4]
1913         }
1914     }
1915     impl Input for Crumbs {
1916     }
1917     impl PathController for Crumbs {
1918         fn set_path(&mut self, segments: &[String]) {
1919             self.segs = segments.to_vec();
1920         }
1921         fn path_click(&mut self) -> Option<usize> {
1922             self.clicked.take()
1923         }
1924     }
1925 
1926     #[test]
1927     fn controller_capability_reached_through_the_concrete_adapter() {
1928         let mut w = Box::new(Adapted::new(Crumbs { segs: Vec::new(), clicked: Some(2) }));
1929 
1930         // The controller trait is reached by deref through the concrete Adapted<W>...
1931         PathController::set_path(&mut **w, &["home".to_string(), "user".to_string()]);
1932         assert_eq!(PathController::path_click(&mut **w), Some(2));
1933 
1934         // ...and lands on the same state the concrete widget sees.
1935         assert_eq!(w.inner().segs, vec!["home".to_string(), "user".to_string()]);
1936         assert_eq!(w.inner().clicked, None, "path_click drained through the deref");
1937     }
1938     /// Phase 6: the paint walk's text prims carry the widget's font and clip rect (what the
1939     /// display-list text path renders), not the bare `Paint::paint` text.
1940     #[test]
1941     fn paint_walk_text_carries_font_and_bounds() {
1942         struct Tag;
1943         impl Layout for Tag {}
1944         impl Paint for Tag {
1945             fn color(&self) -> [f32; 4] {
1946                 [0.0; 4]
1947             }
1948             fn paint(&self, rect: Rect, ctx: &mut PaintCtx) {
1949                 ctx.text("hi", rect.x + 2.0, rect.y + 2.0, 12.0, [1, 2, 3]);
1950             }
1951             fn widget_font(&self) -> Option<String> {
1952                 Some("Mono:12".into())
1953             }
1954             fn text_bounds(&self, rect: Rect) -> Option<[f32; 4]> {
1955                 Some([rect.x, rect.y, rect.x + rect.width, rect.y + rect.height])
1956             }
1957         }
1958         impl Input for Tag {}
1959 
1960         let mut ctx = UiContext::new();
1961         let mut w = Box::new(Adapted::new(Tag));
1962         let (id, ptr) = (w.id(), w.as_ptr_mut());
1963         ctx.register_widget(id, ptr);
1964         unsafe { (*ptr).set_rect(10.0, 20.0, 100.0, 30.0) };
1965 
1966         let list = paint_tree(&ctx, unsafe { &*ptr });
1967         let texts: Vec<_> = list
1968             .items
1969             .iter()
1970             .filter_map(|it| match &it.prim {
1971                 Prim::Text { text, font, bounds, .. } => Some((text.clone(), font.clone(), *bounds)),
1972                 _ => None,
1973             })
1974             .collect();
1975         assert_eq!(texts.len(), 1, "one text prim, no plain duplicate");
1976         assert_eq!(texts[0].0, "hi");
1977         assert_eq!(texts[0].1.as_deref(), Some("Mono:12"), "widget_font attached");
1978         assert_eq!(texts[0].2, Some([10.0, 20.0, 110.0, 50.0]), "text_bounds attached");
1979     }
1980 
1981 }