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

src/widget/model.rs (79.6K)

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