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 }