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 }