Skip to main content

teksilo_core/
widget.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4use teksilo_canvas::{Canvas, Point, Rect, Size, SizeProposal};
5
6use crate::accessibility::AccessNodeBuilder;
7use crate::widget_id::WidgetId;
8
9mod cursor;
10mod event_context;
11mod layout_context;
12mod paint_context;
13
14pub use cursor::CursorIcon;
15pub use event_context::EventContext;
16pub(crate) use event_context::GestureAct;
17pub use layout_context::{LayoutContext, StackAxis};
18pub use paint_context::{PaintContext, WidgetPlacement, WidgetTreeView};
19
20pub(crate) use event_context::{DismissScope, ShortcutMutation, TreeMutation};
21pub(crate) use layout_context::LayoutExtras;
22
23/// A child that is either pre-registered (ID) or waiting to be inserted.
24/// Used by the inline `child()` builder pattern: deferred children are stored
25/// inside the container and resolved recursively when `BuildContext::add()`
26/// inserts the container into the arena.
27pub enum PendingChild {
28    /// Already in the arena — use this ID directly.
29    Id(WidgetId),
30    /// Not yet in the arena — insert during resolution.
31    Deferred(Box<dyn Widget>),
32}
33
34impl std::fmt::Debug for PendingChild {
35    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
36        match self {
37            PendingChild::Id(id) => write!(f, "PendingChild::Id({:?})", id),
38            PendingChild::Deferred(_) => f.write_str("PendingChild::Deferred(..)"),
39        }
40    }
41}
42
43/// A widget's reply to a parent's layout query.
44///
45/// Carries four quantities that together describe how the widget participates
46/// in a stack's space distribution along the layout's main axis:
47///
48/// - `size` — the widget's wanted/ideal size; the floor for **growth**.
49/// - `flex` — positive-slack **grow** weight. `0.0` = rigid (no claim on
50///   surplus); `> 0.0` = wants a share of surplus proportional to its weight.
51/// - `min` — the hard floor for **compression**. A parent must never shrink
52///   the widget below this. Defaults to `size` (i.e. "I do not shrink").
53/// - `shrink` — negative-slack **shrink** weight. `0.0` = will not compress
54///   (the widget overflows before it shrinks); `> 0.0` = absorbs a share of an
55///   over-constraint deficit proportional to its weight, down to `min`.
56///
57/// `flex` and `shrink` are independent — as in CSS flexbox (`flex-grow` vs
58/// `flex-shrink`), a widget may grow but not shrink, or shrink but not grow.
59/// Most widgets just return a `Size`; the `From<Size>` impl wraps it as fully
60/// rigid (`flex = 0`, `shrink = 0`, `min = size`). Grow-bearing widgets
61/// (`Spacer`, `Expand`) use [`LayoutResponse::flexible`]; shrink-bearing ones
62/// (`Shrinkable`, single-line `TextWidget`) use [`LayoutResponse::shrinkable`].
63#[derive(Debug, Clone, Copy, PartialEq)]
64pub struct LayoutResponse {
65    pub size: Size,
66    pub flex: f32,
67    pub min: Size,
68    pub shrink: f32,
69}
70
71impl LayoutResponse {
72    pub const ZERO: Self = Self {
73        size: Size::ZERO,
74        flex: 0.0,
75        min: Size::ZERO,
76        shrink: 0.0,
77    };
78
79    /// A fully rigid response: wanted `size`, no growth, no compression
80    /// (`min == size`).
81    pub fn rigid(size: Size) -> Self {
82        Self {
83            size,
84            flex: 0.0,
85            min: size,
86            shrink: 0.0,
87        }
88    }
89
90    /// A grow-bearing response: wanted `size` with grow weight `flex`. Does
91    /// not shrink (`min == size`).
92    pub fn flexible(size: Size, flex: f32) -> Self {
93        Self {
94            size,
95            flex: flex.max(0.0),
96            min: size,
97            shrink: 0.0,
98        }
99    }
100
101    /// A shrink-bearing response: wanted `size`, compressible down to `min`
102    /// with shrink weight `shrink`. Does not grow (`flex == 0`). `min` is
103    /// clamped componentwise to be no larger than `size`.
104    pub fn shrinkable(size: Size, min: Size, shrink: f32) -> Self {
105        Self {
106            size,
107            flex: 0.0,
108            min: Size::new(min.width.min(size.width), min.height.min(size.height)),
109            shrink: shrink.max(0.0),
110        }
111    }
112
113    /// Builder: set the grow weight on an existing response.
114    pub fn with_flex(mut self, flex: f32) -> Self {
115        self.flex = flex.max(0.0);
116        self
117    }
118
119    /// Builder: set the shrink weight on an existing response.
120    pub fn with_shrink(mut self, shrink: f32) -> Self {
121        self.shrink = shrink.max(0.0);
122        self
123    }
124
125    /// Builder: set the compression floor (clamped componentwise ≤ `size`).
126    pub fn with_min(mut self, min: Size) -> Self {
127        self.min = Size::new(
128            min.width.min(self.size.width),
129            min.height.min(self.size.height),
130        );
131        self
132    }
133}
134
135impl From<Size> for LayoutResponse {
136    fn from(size: Size) -> Self {
137        Self {
138            size,
139            flex: 0.0,
140            min: size,
141            shrink: 0.0,
142        }
143    }
144}
145
146/// The full Widget trait for Level 2 (custom rendering) widgets.
147///
148/// # Adding a method here
149///
150/// A defaulted method added to this trait is **inert for every widget a builder
151/// method has touched** until the framework's same-node wrappers forward it.
152/// [`WidgetWithHandlers`](crate::widget_builder::WidgetWithHandlers) and the
153/// `TeksiBranch{,3,4}` sum types replace the widget at its own arena node, so an
154/// unforwarded method answers this default and the widget loses the behaviour
155/// silently — no compile error, and no test that drives the hook on a bare
156/// struct can see it. Those impls deny `clippy::missing_trait_methods` so the
157/// omission surfaces as a lint on them rather than as a defect in an app.
158pub trait Widget: std::fmt::Debug + std::any::Any {
159    /// Concrete type name of this widget (e.g.
160    /// `"teksilo_widgets::button::Button"`). The default implementation
161    /// resolves at the impl site via `std::any::type_name::<Self>()`,
162    /// so calls through `&dyn Widget` correctly dispatch to the
163    /// monomorphized fn for the concrete type — getting the
164    /// concrete name through the vtable without per-impl boilerplate.
165    ///
166    /// Used by [`crate::widget_tree::WidgetTree::widget_type_histogram`]
167    /// for the `widget.census` telemetry event. Custom
168    /// widgets that wrap their state in a generic struct may
169    /// override to give analytics a stable name independent of the
170    /// generic parameter.
171    fn type_name(&self) -> &'static str {
172        std::any::type_name::<Self>()
173    }
174
175    /// Compose child widgets. Called once after the widget is placed in the
176    /// arena, and again on environment change (theme switch, locale switch).
177    /// Takes `&mut self` — store child IDs, signal handles, any state needed later.
178    /// Returns the list of root child IDs (empty for leaf widgets).
179    fn build(
180        &mut self,
181        _ctx: &mut crate::build_context::BuildContext,
182    ) -> Vec<crate::widget_id::WidgetId> {
183        Vec::new()
184    }
185
186    /// Respond to the parent's size proposal with this widget's wanted size,
187    /// grow/shrink weights, and compression floor (see [`LayoutResponse`]).
188    ///
189    /// Most widgets just return a `Size` (auto-converts via `From<Size>` to a
190    /// fully rigid response). Grow-bearing widgets (`Spacer`, `Expand`) return
191    /// a non-zero `flex`; shrink-bearing widgets (`Shrinkable`, single-line
192    /// `TextWidget`) return a non-zero `shrink` with a `min` floor.
193    ///
194    /// The parent honors `size` as a floor for growth and distributes positive
195    /// slack proportional to `flex`; when over-constrained it distributes the
196    /// deficit proportional to `shrink`, never below `min`.
197    ///
198    /// **Determinism / height-for-width contract.** This must be a *deterministic
199    /// function of the widget's state and the `proposal`*: two calls with the
200    /// same proposal in one layout pass must return the same value. The result
201    /// must be correct *for the proposal given* — in particular a
202    /// height-for-width widget queried with `{width: Some(w), height: None}`
203    /// must return its height *at width `w`*. The framework memoizes results per
204    /// `(widget, proposal)` within a pass (see
205    /// [`cacheable_layout`](Self::cacheable_layout)) to keep negotiation O(n).
206    ///
207    /// Side effects are permitted **as long as they are idempotent** — the cache
208    /// may skip them on a repeat query, so a side effect (e.g. snapshotting
209    /// measured state into a `Signal`) must be safe to run any number of times
210    /// ≥ 1 per pass and leave the same final state. Most measuring widgets
211    /// (`Collapse`, `SceneView`, inspector tabs) satisfy this with a guarded or
212    /// overwriting set. A side effect that must run on *every* call (e.g. a call
213    /// counter) is non-idempotent — opt out via `cacheable_layout`.
214    fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse;
215
216    /// Whether this widget's [`layout_response`](Self::layout_response) may be
217    /// memoized by the per-pass layout cache. Defaults to `true`.
218    ///
219    /// Override to `false` only when `layout_response` has a **non-idempotent**
220    /// side effect that must run on every call (a call counter, a one-shot
221    /// trigger). Idempotent side effects — the common case, e.g. overwriting a
222    /// measured size into a `Signal` — do **not** need to opt out: the cache may
223    /// skip a redundant identical-proposal repeat, but the value was already
224    /// written and the final state is correct. The debug inspector's
225    /// `BoundsTracker` opts out defensively (its whole-tree snapshot is the
226    /// payload, not a by-product of sizing).
227    fn cacheable_layout(&self) -> bool {
228        true
229    }
230
231    /// Position children within the allocated bounds.
232    ///
233    /// Called for **every active widget on every layout pass** — including
234    /// leaves, which receive an empty `children` slice. It is therefore also the
235    /// canonical *"here are your final, parent-assigned bounds"* hook, and the
236    /// only one that runs during layout: `layout_response` sees a *proposal*, not
237    /// the outcome, and `paint` runs too late for anything the renderer consumes
238    /// before it (a node-level transform scope, a text engine's viewport).
239    ///
240    /// A widget whose bounds feed such a thing must read them here, not in
241    /// `paint`. `SceneView` is the motivating case: it folds `bounds.origin` into
242    /// the view transform the render walker pushes *around* its subtree, so a
243    /// scene that learned its origin only at paint time drew its content offset
244    /// by `-bounds.origin` — an error that then scaled with zoom.
245    ///
246    /// **If you mutate interior state here, keep the write and its consequences
247    /// together.** This hook runs *before* `paint`, so writing a field that
248    /// `paint` later uses as a compare-then-act change detector will silently
249    /// blind that detector. (Both text engines were bitten: `place_children` set
250    /// the `viewport_width` that `paint` compared against, so `paint` concluded
251    /// "unchanged" and skipped the `engine.set_viewport` + relayout it owed.)
252    /// Route such writes through one idempotent helper that both hooks call.
253    ///
254    /// **Do not `Signal::set` here at [`BindingLevel::Relayout`] or
255    /// [`Rebuild`](crate::binding::BindingLevel::Rebuild).** This runs inside the
256    /// layout pass, so dirtying the tree from it re-enters layout. A plain
257    /// `Cell`/`RefCell` (what the colour-picker leaves use to cache their bounds
258    /// for hit-testing) or a `RepaintOnly` signal is fine.
259    ///
260    /// [`BindingLevel::Relayout`]: crate::binding::BindingLevel::Relayout
261    fn place_children(
262        &self,
263        _bounds: Rect,
264        _proposal: SizeProposal,
265        _children: &mut [WidgetPlacement],
266        _ctx: &LayoutContext,
267    ) {
268        // Leaf widgets have no children to place.
269    }
270
271    /// Draw the widget's visual representation.
272    fn paint(&self, _bounds: Rect, _canvas: &mut Canvas, _ctx: &PaintContext) {
273        // Default: nothing to paint (layout-only containers).
274    }
275
276    /// Whether this widget wants its [`after_paint`](Self::after_paint)
277    /// hook to fire each frame. Returning `false` (the default) saves
278    /// a virtual call per widget per frame for the vast majority of
279    /// widgets that don't aggregate descendant geometry.
280    ///
281    /// Same opt-in pattern as
282    /// [`wants_descendant_redirects`](Self::wants_descendant_redirects).
283    fn wants_after_paint(&self) -> bool {
284        false
285    }
286
287    /// Called once per frame after this widget's subtree has finished
288    /// painting. Receives a read-only view of the layout-resolved
289    /// arena so a parent can read its descendants' final bounds —
290    /// e.g. `TitleBar` aggregates its drag region and control-button
291    /// rects into a single `HitRegions` payload for the Windows
292    /// backend's `WM_NCHITTEST`.
293    ///
294    /// Walk order is depth-first **post**-order: a parent's
295    /// `after_paint` runs after every descendant's `paint` has
296    /// committed.
297    ///
298    /// Default: empty. Only widgets that override
299    /// [`wants_after_paint`](Self::wants_after_paint) and return `true`
300    /// see this called.
301    fn after_paint(&self, _view: &WidgetTreeView<'_>, _ctx: &PaintContext) {}
302
303    /// Whether this widget wants its [`post_paint`](Self::post_paint)
304    /// hook to fire each frame. Returning `false` (the default) saves a
305    /// virtual call per widget per frame for the vast majority of widgets
306    /// that don't draw a foreground over their children.
307    ///
308    /// Same opt-in pattern as [`wants_after_paint`](Self::wants_after_paint).
309    fn wants_post_paint(&self) -> bool {
310        false
311    }
312
313    /// Draw a foreground layer *over* this widget's children.
314    ///
315    /// The normal [`paint`](Self::paint) emits a widget's draws *before*
316    /// its children — a backdrop. `post_paint` emits *after* the entire
317    /// child subtree, so its draws land on top. This is the supported way
318    /// for a composing widget to paint over its own descendants:
319    /// inset shadows, a focus ring that must overlay content, a scrim, or
320    /// `SceneView`'s "over" lightweight band (selection lasso, highlighted
321    /// connectors).
322    ///
323    /// Runs inside the same clip / transform / opacity / blur scopes as
324    /// the widget and its children, so a foreground decoration pans,
325    /// scales and clips consistently with the subtree it covers. It is
326    /// paint-only — no hit-testing and no accessibility node; for
327    /// interactive overlays that must escape the widget's bounds, use the
328    /// overlay system instead.
329    ///
330    /// Default: empty. Only widgets that override
331    /// [`wants_post_paint`](Self::wants_post_paint) and return `true` see
332    /// this called.
333    fn post_paint(&self, _bounds: Rect, _canvas: &mut Canvas, _ctx: &PaintContext) {}
334
335    /// Declare this widget's accessibility identity.
336    fn accessibility(&self, _builder: &mut AccessNodeBuilder) {}
337
338    /// Whether this widget wants the AT walker to consult its
339    /// [`a11y_redirect_descendant`](Self::a11y_redirect_descendant)
340    /// hook for *every* descendant during AT tree emission, not
341    /// just its direct arena children.
342    ///
343    /// Returning `true` opts this widget into ancestor-chain
344    /// queries: as the walker iterates each descendant's parent
345    /// to decide where the descendant's `NodeId` lands in the AT
346    /// tree, it walks up the arena from that parent and asks
347    /// every ancestor with this flag set. First `Some(_)` wins
348    /// (closest ancestor takes priority — same precedence as a
349    /// CSS-like cascade).
350    ///
351    /// Returning `false` (the default) makes the walker pay the
352    /// O(depth) ancestor walk only for trees that genuinely need
353    /// it. Only opt in if your widget actively places
354    /// non-direct-child descendant `NodeId`s in its own
355    /// `accessibility()` emission — `teksilo_scene::SceneView` is
356    /// the canonical example.
357    ///
358    /// Default: `false`.
359    fn wants_descendant_redirects(&self) -> bool {
360        false
361    }
362
363    /// Optional redirection hook for AT-tree placement of a child.
364    ///
365    /// The accessibility walker consults every ancestor that opts
366    /// in via
367    /// [`wants_descendant_redirects`](Self::wants_descendant_redirects),
368    /// starting at the child's immediate arena parent and walking
369    /// up to the root. An ancestor whose flag is `false` is
370    /// skipped without its hook ever being called, the immediate
371    /// parent included, and the walk carries on past it. First
372    /// `Some(_)` wins, scanned bottom-up (closest opted-in
373    /// ancestor takes priority). Returning
374    /// `Some(_)` tells the walker that this widget has *already*
375    /// placed `descendant`'s `NodeId` somewhere else (typically
376    /// under a synthetic node it emitted in its own
377    /// `accessibility()` call), and the walker should NOT add it
378    /// to its arena parent's children list.
379    ///
380    /// The returned `NodeId` is informational — it identifies the
381    /// new logical parent in case the walker wants to bookkeep
382    /// (e.g., dedupe). The walker does not validate that
383    /// `descendant`'s NodeId is actually in that target's children
384    /// list; it is the implementing widget's responsibility to
385    /// have placed it there during its `accessibility()` emission
386    /// (e.g. via `AccessNodeBuilder::attach_scene_child_under`).
387    ///
388    /// Used by `teksilo_scene::SceneView` to graft heavyweight
389    /// `Widget` items into an app-declared logical AT tree.
390    /// Other layered containers can adopt the same pattern.
391    ///
392    /// Default: `None` — no redirection.
393    fn a11y_redirect_descendant(
394        &self,
395        _self_id: WidgetId,
396        _descendant: WidgetId,
397    ) -> Option<accesskit::NodeId> {
398        None
399    }
400
401    /// Suggest an accessible title to an enclosing container that
402    /// wraps this widget as content — typically a modal / dialog
403    /// shell that wants to propagate the inner content's visible
404    /// title as the shell's own accessible name.
405    ///
406    /// Example: `ModalContainer` wraps a `DialogContent`. The
407    /// container owns the `Role::Dialog` node and needs a name;
408    /// `DialogContent` overrides this method to return its own
409    /// `title` string. The container queries this on its pending
410    /// content at build time and uses the result if set.
411    ///
412    /// Default: `None` — widgets that don't carry a natural
413    /// title don't need to override.
414    fn accessible_title_hint(&self) -> Option<String> {
415        None
416    }
417
418    /// The widget that *paints* this one's title, when it has one.
419    ///
420    /// Preferred over [`accessible_title_hint`](Self::accessible_title_hint)
421    /// where both are available: an enclosing container points at the
422    /// title node through a `labelled_by` relation instead of copying
423    /// its string, so the title keeps its own node and stays reviewable
424    /// by character rather than being announced only as part of the
425    /// container's name.
426    ///
427    /// Queried right after the content is mounted — `build()` runs
428    /// eagerly on insertion, so the title node already exists by then.
429    ///
430    /// Default: `None`.
431    fn accessible_title_node(&self) -> Option<crate::widget_id::WidgetId> {
432        None
433    }
434
435    /// Optional hint that directs initial focus to a specific
436    /// descendant when this widget is the root of a deferred-built
437    /// modal surface.
438    ///
439    /// The modal presentation pipeline consults this after building
440    /// the content subtree, in priority order: the caller's
441    /// `ModalRequest::focus_target` → the content widget's
442    /// `initial_focus_hint` → `first_focusable_descendant`.
443    /// `MessageBox` overrides this to return the widget id of its
444    /// configured default button, so platform-native button orderings
445    /// (Cancel-left + Default-right-but-focused) work without
446    /// forcing the default button to be the first focusable
447    /// descendant in tree-walk order.
448    ///
449    /// Default: `None` — widgets that don't need to direct initial
450    /// focus to a non-first-focusable descendant don't override.
451    fn initial_focus_hint(&self) -> Option<WidgetId> {
452        None
453    }
454
455    /// Which descendant a keyboard request for a context menu should target.
456    ///
457    /// The context-menu key (and Shift+F10) opens the menu of the **focused**
458    /// widget. For a data view that is the wrong node: `ListView`, `TreeView`,
459    /// `TableView`, `TreeTableView` and `GridView` are focusable as a whole and
460    /// their rows deliberately are not — the container owns focus and
461    /// `set_selected` is what tells assistive technology which row is current
462    /// (see `list_item_a11y`). Without this hook the chord would open the
463    /// *list's* menu rather than the selected row's, in exactly the widget
464    /// family where a per-row menu matters most.
465    ///
466    /// Return the widget id of the row (or cell, or tile) the menu should be
467    /// about. The dispatcher then walks up from there, so a view whose rows
468    /// carry no factory of their own still finds the container's.
469    ///
470    /// Default: `None` — the focused widget is the target, which is right for
471    /// every widget that is itself the thing the user is pointing at.
472    fn context_menu_key_target(&self) -> Option<WidgetId> {
473        None
474    }
475
476    /// Return the child widget IDs that this widget manages.
477    fn children(&self) -> Vec<WidgetId> {
478        Vec::new()
479    }
480
481    /// Optional override for the child ORDER presented to assistive
482    /// technology, when it must differ from the paint / z-order child
483    /// order returned by [`children`](Self::children).
484    ///
485    /// Return `None` (the default) to let the accessibility walker use the
486    /// arena's child order — correct for almost every widget. Return
487    /// `Some(ids)` to reorder (or restrict) how children appear in the AT
488    /// tree and in the linear Tab reading order, WITHOUT affecting layout or
489    /// paint. `TableView` / `TreeTableView` use this to read the header
490    /// before the body rows even though they build the body first so it
491    /// paints beneath the header (WCAG 1.3.2 Meaningful Sequence).
492    fn accessibility_children(&self) -> Option<Vec<WidgetId>> {
493        None
494    }
495
496    /// Downcast hook. Default implementation returns `None`; concrete
497    /// widgets override with `Some(self)` when they want to expose
498    /// their concrete type to test-level introspection or reflection.
499    /// The trait already bounds on `std::any::Any` so concrete types
500    /// satisfy the `'static` requirement.
501    fn as_any(&self) -> Option<&dyn std::any::Any> {
502        None
503    }
504
505    /// Mutable counterpart of [`as_any`](Self::as_any). Default
506    /// returns `None`; widgets that want to expose mutable state to
507    /// tests (e.g. so a test can mutate a `Scene` inside a
508    /// `SceneView` post-layout) override with `Some(self)`. Should
509    /// follow the same opt-in pattern as `as_any`: only widgets
510    /// that opt into `&` introspection should opt into `&mut`.
511    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
512        None
513    }
514
515    /// Whether this widget clips its children to its bounds.
516    fn clips_children(&self) -> bool {
517        false
518    }
519
520    /// The rectangle (in **absolute tree coordinates**) that best *represents*
521    /// this widget when the framework reveals it into an ancestor scroll area on
522    /// focus gain. Returning `None` (the default) reveals the widget's whole
523    /// bounds — correct for most controls.
524    ///
525    /// A widget that can be much taller than a viewport — a `RichTextEditor`
526    /// grown inside a page `ScrollArea`, a `ListView` / `TreeView` taller than
527    /// its scroller — should override this to return the sub-rectangle the user
528    /// actually cares about (the caret line, the selected row). Otherwise
529    /// [`scroll_focused_into_view`](crate::widget_tree::WidgetTree) reveals the
530    /// *entire* box, which for a tall widget scrolls the page to its bottom on a
531    /// click that only meant to place the caret near the top. The returned rect
532    /// feeds the same ancestor-only `scroll_rect_into_view` engine the caret /
533    /// selection follow uses (the focused widget itself is excluded), so it
534    /// never double-scrolls against the widget's own internal follow.
535    ///
536    /// `bounds` is this widget's current absolute rectangle as the arena stores
537    /// it, so an override can place its interior rect without depending on a
538    /// paint-set origin.
539    fn focus_reveal_rect(&self, _bounds: Rect) -> Option<Rect> {
540        None
541    }
542
543    /// Whether the point lies inside this widget's *actual* shape, not
544    /// just its rectangular bounds. Consulted by hit-testing right after
545    /// the bounds check: returning `false` for a point that *is* inside
546    /// the bounding box makes the widget transparent to the click there,
547    /// so it **falls through** to whatever sibling is painted underneath
548    /// (the same machinery as a fully pass-through node, but shape-aware).
549    ///
550    /// Both arguments are in the widget's bounds space: `local_point` is
551    /// the point being tested and `bounds` is this widget's rectangle, so
552    /// a non-rectangular widget can test the point against its silhouette.
553    ///
554    /// The default returns `true` for any in-bounds point (a plain
555    /// rectangle), so every existing widget is unaffected. Override for
556    /// irregular shapes — an ellipse / cloud scene node, a circular
557    /// handle — so a click lands on the shape you see, not its bounding
558    /// box, and clicks in the transparent corners reach the node beneath.
559    /// This mirrors the lightweight tier's `SceneItem::shape_contains`.
560    fn hit_shape(&self, _local_point: Point, _bounds: Rect) -> bool {
561        true
562    }
563
564    /// How far **outside** its own bounds this widget still absorbs a press,
565    /// per edge, for the pointer that is asking.
566    ///
567    /// Consulted **inside** the exact hit test: within one parent, children
568    /// that declare an outset are tested against their outset bounds *before*
569    /// the ordinary reverse-sibling walk, so a 6 dp splitter gutter wins over
570    /// the panes it overlaps instead of losing to whichever pane is painted on
571    /// top. This is the mechanism for a thin **grip**, and the only one of the
572    /// three that can beat a competing target.
573    ///
574    /// Three rules make it safe to add to a widget that already works:
575    ///
576    /// * **Hit-only.** No layout moves, nothing repaints differently, and a
577    ///   Compact build renders byte for byte as it did. The outset exists
578    ///   between the pointer and the arena and nowhere else.
579    /// * **It never escapes the parent.** The recursion has already tested the
580    ///   parent's own bounds before it looks at any child, so an outset can
581    ///   only ever claim space the parent already owns — including through a
582    ///   `clips_children` ancestor, whose rectangle gated the descent.
583    /// * **Zero for a precise pointer** unless the widget deliberately says
584    ///   otherwise. A mouse cursor's hot-spot is exact and occludes nothing, so
585    ///   widening its targets steals clicks. Check
586    ///   `kind.is_direct()` (or accept every kind explicitly, as a control with
587    ///   a genuinely undersized mouse grip may) before returning anything
588    ///   non-zero.
589    ///
590    /// The insets are **reading-order**: `leading` is the left edge in an LTR
591    /// UI and the right edge in an RTL one. The default returns
592    /// [`EdgeInsets::ZERO`](teksilo_canvas::EdgeInsets::ZERO), so every existing
593    /// widget is unaffected.
594    ///
595    /// A grip's conventional value is `9 dp` for a direct pointer and `0 dp`
596    /// for a precise one — enough to lift a 6 dp gutter to a 24 dp target.
597    fn hit_outset(
598        &self,
599        _kind: teksilo_tokens::PointerKind,
600        _tokens: &teksilo_tokens::InputTokens,
601    ) -> teksilo_canvas::EdgeInsets {
602        teksilo_canvas::EdgeInsets::ZERO
603    }
604
605    /// This widget's own say in the *miss-only* slop pass, overriding the
606    /// density default for its node.
607    ///
608    /// Third link of the precedence chain — `no_hit_slop` beats a node-level
609    /// `.hit_slop(..)`, which beats this, which beats
610    /// [`HitSlop::for_pointer`]. Return [`HitSlop::NONE`] to opt a widget out
611    /// of re-attribution entirely, or a larger `up_to` to say that a control
612    /// deserves topping up further than the density asks.
613    ///
614    /// `None` (the default) means "no opinion — use the density default".
615    ///
616    /// [`HitSlop::for_pointer`]: crate::pointer::hit_slop::HitSlop::for_pointer
617    /// [`HitSlop::NONE`]: crate::pointer::hit_slop::HitSlop::NONE
618    fn hit_slop(
619        &self,
620        _kind: teksilo_tokens::PointerKind,
621        _tokens: &teksilo_tokens::InputTokens,
622    ) -> Option<crate::pointer::hit_slop::HitSlop> {
623        None
624    }
625
626    /// How far a *missed* press is from this widget's actual silhouette, in the
627    /// widget's own bounds space.
628    ///
629    /// The key to the miss-only slop pass: once the exact pass has found
630    /// nothing eligible, the framework asks every nearby node how far away it
631    /// really is and re-attributes the press to the closest one still inside
632    /// its earned outset.
633    ///
634    /// The default measures to the bounding rectangle, which is right for the
635    /// rectangular majority. A **round or wedge** control overrides it beside
636    /// its existing [`hit_shape`](Self::hit_shape) so the slop follows the
637    /// shape the user aimed at rather than the box it was laid out in — a
638    /// press past the corner of a radio dot's box is further from the dot than
639    /// a press past its edge, and should lose to a neighbour that is nearer.
640    ///
641    /// Returning `None` withdraws the widget from the pass altogether, which is
642    /// the shape-level equivalent of `no_hit_slop`.
643    ///
644    /// This is **never** consulted by the exact pass, so overriding it cannot
645    /// change where an ordinary click lands.
646    fn hit_distance(&self, local_point: Point, bounds: Rect) -> Option<f32> {
647        Some(crate::pointer::hit_slop::rect_distance(bounds, local_point))
648    }
649
650    /// The interactive sub-regions this widget **paints inside its own single
651    /// node** — a scroll bar's thumb, a slider's knob, a header cell's filter
652    /// affordance.
653    ///
654    /// Reporting only: implementing it changes no layout and no hit test by
655    /// itself. It exists because a control that draws several targets on one
656    /// canvas is otherwise opaque — the router cannot route a coarse press to
657    /// the nearest one, and the target-conformance audit cannot see that any of
658    /// them exists, let alone that it clears the floor.
659    ///
660    /// `bounds` is this widget's current rectangle, and the returned rects are
661    /// in the same space. Build them with
662    /// [`partition_targets`](crate::partition::partition_targets) where the
663    /// split is a horizontal division, so the geometry the widget paints and
664    /// the geometry it reports cannot drift apart.
665    ///
666    /// The default returns an empty list: a widget whose node *is* its target
667    /// has nothing to add.
668    fn target_regions(&self, _bounds: Rect) -> Vec<crate::partition::TargetRegion> {
669        Vec::new()
670    }
671
672    /// How `rebuild_single_widget` treats this widget's existing children
673    /// when re-running its `build()`.
674    ///
675    /// **`false` (default) — re-derive.** Rebuild is "tear down and
676    /// reconstruct": every old child subtree is destroyed up front, then
677    /// `build()` produces a fresh set. The right semantic for data-driven
678    /// widgets like `Repeater` / `ListView` that rebuild their children from
679    /// current model state with fresh `WidgetId`s. A `false` widget must NOT
680    /// re-attach an old child id — it has already been destroyed.
681    ///
682    /// **`true` — reconcile.** `build()` re-attaches (by id) the children it
683    /// keeps and drops the rest. The framework keeps every re-attached child's
684    /// subtree intact — focus, scroll offset, text contents, signal
685    /// subscriptions all survive — and destroys only the old children the new
686    /// build dropped *and* did not re-parent elsewhere. This is the mode for
687    /// widgets that memoize stateful children across rebuilds:
688    ///
689    /// * `Switcher` keeps every mounted page alive so switching tabs doesn't
690    ///   wipe the inactive pages' state.
691    /// * `SceneView` re-pushes the same heavyweight scene-widget ids each
692    ///   rebuild (draining drag-to-move / marquee commits) — they must stay
693    ///   attached or the cards "disappear" on every drag end.
694    /// * `TabWidget` / `DockingLayout` / `CompositeTooltip` re-attach memoized
695    ///   panes / a one-shot body widget that cannot be reconstructed.
696    /// * `MenuBar` re-derives its menu triggers fresh each build (the model may
697    ///   have changed — the reconcile reaps the superseded ones) while keeping
698    ///   its memoized leading/trailing slot widgets, so a stateful slot control
699    ///   survives a model-version rebuild.
700    ///
701    /// The reconcile follows **authoritative parent pointers**, so a kept
702    /// subtree that `build()` re-parents *out* of a dropped sibling and into
703    /// the new tree survives — it is not swept via the dropped sibling's now
704    /// stale `children` list. Dropped children are genuinely destroyed (state
705    /// unmounted, arena slots freed), not left as stranded, still-active
706    /// orphans.
707    fn preserves_children_on_rebuild(&self) -> bool {
708        false
709    }
710
711    /// Whether this widget, used as tooltip content, currently has anything
712    /// worth showing.
713    ///
714    /// Consulted by `WidgetTree` just before a dwell matures into an overlay.
715    /// Returning `false` cancels the show — the anchor simply has no tooltip
716    /// this time — so a blank or unresolved string does not pop an empty
717    /// chromed bubble, which reads as a rendering fault rather than as
718    /// "nothing to say here".
719    ///
720    /// Defaults to `true`: content that hosts an arbitrary widget tree (a
721    /// chart, a progress row) is meaningful without any text, and a custom
722    /// content widget must never be suppressed by a check it did not opt into.
723    /// Only widgets whose *whole* payload is a string — `TooltipWidget` — have
724    /// a well-defined notion of being empty.
725    fn tooltip_has_content(&self) -> bool {
726        true
727    }
728
729    /// Declare the rebindable keyboard shortcuts this widget exposes,
730    /// *without* installing handlers. The framework calls this at
731    /// arena insertion time (before `build()`) and at certain lazy
732    /// boundaries (e.g. `Switcher` walks declarations on its
733    /// not-yet-mounted `Pending` slots), so settings UIs and the
734    /// `ShortcutRegistry` see the keystrokes the moment the owning
735    /// container mounts — even if `build()` hasn't run.
736    ///
737    /// Pair this with `BuildContext::register_shortcut` in `build()`
738    /// to install the matching `on_activate` handler: the build-time
739    /// registration *upserts* the declared entry, preserving any user
740    /// override and the declared keystrokes while attaching the
741    /// closure that actually fires.
742    ///
743    /// The returned shortcuts may omit `on_activate` (a metadata-only
744    /// declaration). When matched at dispatch time without a
745    /// registered handler, the framework synthesizes a no-parameter
746    /// intent from the shortcut's id — same path as a build-time
747    /// registration with `on_activate: None`.
748    ///
749    /// Default: empty (no declared shortcuts).
750    fn declare_shortcuts(&self) -> Vec<crate::shortcut::Shortcut> {
751        Vec::new()
752    }
753
754    /// Extract attached handler set from a `WidgetWithHandlers` wrapper.
755    /// Called during arena insertion to transfer handlers to the `WidgetNode`.
756    /// Default: returns `None` (no attached handlers).
757    fn take_handler_set(&mut self) -> Option<crate::widget_builder::HandlerSet> {
758        None
759    }
760}
761
762/// A boxed widget is a widget.
763///
764/// Without this, `Box<dyn Widget>` is the one widget-shaped value that cannot
765/// go where a widget goes: `.child(..)`, a `Vec` of children, a `match` arm.
766/// 238 functions in `teksilo-widgets` alone return it, the `teksu!` macro's own
767/// over-four-arms advice recommends it, and only two containers in the whole
768/// catalog (`Switcher`, `Cycle`) shipped a `child_boxed` to take it. Every
769/// other call site had to invent an adapter widget, which costs a real arena
770/// node per use.
771///
772/// Every method forwards to the inner widget, so the box is invisible to the
773/// arena: it adds no node, no layout pass and no AT element. In particular
774/// `as_any` / `as_any_mut` forward, so `EventContext::with_widget_mut::<W>`
775/// downcasts to the widget that was boxed rather than to the box.
776impl<W: Widget + ?Sized> Widget for Box<W> {
777    fn type_name(&self) -> &'static str {
778        (**self).type_name()
779    }
780
781    fn build(
782        &mut self,
783        ctx: &mut crate::build_context::BuildContext,
784    ) -> Vec<crate::widget_id::WidgetId> {
785        (**self).build(ctx)
786    }
787
788    fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse {
789        (**self).layout_response(proposal, ctx)
790    }
791
792    fn cacheable_layout(&self) -> bool {
793        (**self).cacheable_layout()
794    }
795
796    fn place_children(
797        &self,
798        bounds: Rect,
799        proposal: SizeProposal,
800        children: &mut [WidgetPlacement],
801        ctx: &LayoutContext,
802    ) {
803        (**self).place_children(bounds, proposal, children, ctx)
804    }
805
806    fn paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
807        (**self).paint(bounds, canvas, ctx)
808    }
809
810    fn wants_after_paint(&self) -> bool {
811        (**self).wants_after_paint()
812    }
813
814    fn after_paint(&self, view: &WidgetTreeView<'_>, ctx: &PaintContext) {
815        (**self).after_paint(view, ctx)
816    }
817
818    fn wants_post_paint(&self) -> bool {
819        (**self).wants_post_paint()
820    }
821
822    fn post_paint(&self, bounds: Rect, canvas: &mut Canvas, ctx: &PaintContext) {
823        (**self).post_paint(bounds, canvas, ctx)
824    }
825
826    fn accessibility(&self, builder: &mut AccessNodeBuilder) {
827        (**self).accessibility(builder)
828    }
829
830    fn wants_descendant_redirects(&self) -> bool {
831        (**self).wants_descendant_redirects()
832    }
833
834    fn a11y_redirect_descendant(
835        &self,
836        self_id: WidgetId,
837        descendant: WidgetId,
838    ) -> Option<accesskit::NodeId> {
839        (**self).a11y_redirect_descendant(self_id, descendant)
840    }
841
842    fn accessible_title_hint(&self) -> Option<String> {
843        (**self).accessible_title_hint()
844    }
845
846    fn accessible_title_node(&self) -> Option<crate::widget_id::WidgetId> {
847        (**self).accessible_title_node()
848    }
849
850    fn initial_focus_hint(&self) -> Option<WidgetId> {
851        (**self).initial_focus_hint()
852    }
853
854    fn context_menu_key_target(&self) -> Option<WidgetId> {
855        (**self).context_menu_key_target()
856    }
857
858    fn children(&self) -> Vec<WidgetId> {
859        (**self).children()
860    }
861
862    fn accessibility_children(&self) -> Option<Vec<WidgetId>> {
863        (**self).accessibility_children()
864    }
865
866    fn as_any(&self) -> Option<&dyn std::any::Any> {
867        (**self).as_any()
868    }
869
870    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
871        (**self).as_any_mut()
872    }
873
874    fn clips_children(&self) -> bool {
875        (**self).clips_children()
876    }
877
878    fn focus_reveal_rect(&self, bounds: Rect) -> Option<Rect> {
879        (**self).focus_reveal_rect(bounds)
880    }
881
882    fn hit_shape(&self, local_point: Point, bounds: Rect) -> bool {
883        (**self).hit_shape(local_point, bounds)
884    }
885
886    fn hit_outset(
887        &self,
888        kind: teksilo_tokens::PointerKind,
889        tokens: &teksilo_tokens::InputTokens,
890    ) -> teksilo_canvas::EdgeInsets {
891        (**self).hit_outset(kind, tokens)
892    }
893
894    fn hit_slop(
895        &self,
896        kind: teksilo_tokens::PointerKind,
897        tokens: &teksilo_tokens::InputTokens,
898    ) -> Option<crate::pointer::hit_slop::HitSlop> {
899        (**self).hit_slop(kind, tokens)
900    }
901
902    fn hit_distance(&self, local_point: Point, bounds: Rect) -> Option<f32> {
903        (**self).hit_distance(local_point, bounds)
904    }
905
906    fn target_regions(&self, bounds: Rect) -> Vec<crate::partition::TargetRegion> {
907        (**self).target_regions(bounds)
908    }
909
910    fn preserves_children_on_rebuild(&self) -> bool {
911        (**self).preserves_children_on_rebuild()
912    }
913
914    fn tooltip_has_content(&self) -> bool {
915        (**self).tooltip_has_content()
916    }
917
918    fn declare_shortcuts(&self) -> Vec<crate::shortcut::Shortcut> {
919        (**self).declare_shortcuts()
920    }
921
922    fn take_handler_set(&mut self) -> Option<crate::widget_builder::HandlerSet> {
923        (**self).take_handler_set()
924    }
925}