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 use layout_context::{LayoutContext, StackAxis};
17pub use paint_context::{PaintContext, WidgetPlacement, WidgetTreeView};
18
19pub(crate) use event_context::{DismissScope, ShortcutMutation, TreeMutation};
20pub(crate) use layout_context::LayoutExtras;
21
22/// A child that is either pre-registered (ID) or waiting to be inserted.
23/// Used by the inline `child()` builder pattern: deferred children are stored
24/// inside the container and resolved recursively when `BuildContext::add()`
25/// inserts the container into the arena.
26pub enum PendingChild {
27    /// Already in the arena — use this ID directly.
28    Id(WidgetId),
29    /// Not yet in the arena — insert during resolution.
30    Deferred(Box<dyn Widget>),
31}
32
33impl std::fmt::Debug for PendingChild {
34    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
35        match self {
36            PendingChild::Id(id) => write!(f, "PendingChild::Id({:?})", id),
37            PendingChild::Deferred(_) => f.write_str("PendingChild::Deferred(..)"),
38        }
39    }
40}
41
42/// A widget's reply to a parent's layout query.
43///
44/// Carries four quantities that together describe how the widget participates
45/// in a stack's space distribution along the layout's main axis:
46///
47/// - `size` — the widget's wanted/ideal size; the floor for **growth**.
48/// - `flex` — positive-slack **grow** weight. `0.0` = rigid (no claim on
49///   surplus); `> 0.0` = wants a share of surplus proportional to its weight.
50/// - `min` — the hard floor for **compression**. A parent must never shrink
51///   the widget below this. Defaults to `size` (i.e. "I do not shrink").
52/// - `shrink` — negative-slack **shrink** weight. `0.0` = will not compress
53///   (the widget overflows before it shrinks); `> 0.0` = absorbs a share of an
54///   over-constraint deficit proportional to its weight, down to `min`.
55///
56/// `flex` and `shrink` are independent — as in CSS flexbox (`flex-grow` vs
57/// `flex-shrink`), a widget may grow but not shrink, or shrink but not grow.
58/// Most widgets just return a `Size`; the `From<Size>` impl wraps it as fully
59/// rigid (`flex = 0`, `shrink = 0`, `min = size`). Grow-bearing widgets
60/// (`Spacer`, `Expand`) use [`LayoutResponse::flexible`]; shrink-bearing ones
61/// (`Shrinkable`, single-line `TextWidget`) use [`LayoutResponse::shrinkable`].
62#[derive(Debug, Clone, Copy, PartialEq)]
63pub struct LayoutResponse {
64    pub size: Size,
65    pub flex: f32,
66    pub min: Size,
67    pub shrink: f32,
68}
69
70impl LayoutResponse {
71    pub const ZERO: Self = Self {
72        size: Size::ZERO,
73        flex: 0.0,
74        min: Size::ZERO,
75        shrink: 0.0,
76    };
77
78    /// A fully rigid response: wanted `size`, no growth, no compression
79    /// (`min == size`).
80    pub fn rigid(size: Size) -> Self {
81        Self {
82            size,
83            flex: 0.0,
84            min: size,
85            shrink: 0.0,
86        }
87    }
88
89    /// A grow-bearing response: wanted `size` with grow weight `flex`. Does
90    /// not shrink (`min == size`).
91    pub fn flexible(size: Size, flex: f32) -> Self {
92        Self {
93            size,
94            flex: flex.max(0.0),
95            min: size,
96            shrink: 0.0,
97        }
98    }
99
100    /// A shrink-bearing response: wanted `size`, compressible down to `min`
101    /// with shrink weight `shrink`. Does not grow (`flex == 0`). `min` is
102    /// clamped componentwise to be no larger than `size`.
103    pub fn shrinkable(size: Size, min: Size, shrink: f32) -> Self {
104        Self {
105            size,
106            flex: 0.0,
107            min: Size::new(min.width.min(size.width), min.height.min(size.height)),
108            shrink: shrink.max(0.0),
109        }
110    }
111
112    /// Builder: set the grow weight on an existing response.
113    pub fn with_flex(mut self, flex: f32) -> Self {
114        self.flex = flex.max(0.0);
115        self
116    }
117
118    /// Builder: set the shrink weight on an existing response.
119    pub fn with_shrink(mut self, shrink: f32) -> Self {
120        self.shrink = shrink.max(0.0);
121        self
122    }
123
124    /// Builder: set the compression floor (clamped componentwise ≤ `size`).
125    pub fn with_min(mut self, min: Size) -> Self {
126        self.min = Size::new(
127            min.width.min(self.size.width),
128            min.height.min(self.size.height),
129        );
130        self
131    }
132}
133
134impl From<Size> for LayoutResponse {
135    fn from(size: Size) -> Self {
136        Self {
137            size,
138            flex: 0.0,
139            min: size,
140            shrink: 0.0,
141        }
142    }
143}
144
145/// The full Widget trait for Level 2 (custom rendering) widgets.
146pub trait Widget: std::fmt::Debug + std::any::Any {
147    /// Concrete type name of this widget (e.g.
148    /// `"teksilo_widgets::button::Button"`). The default implementation
149    /// resolves at the impl site via `std::any::type_name::<Self>()`,
150    /// so calls through `&dyn Widget` correctly dispatch to the
151    /// monomorphized fn for the concrete type — getting the
152    /// concrete name through the vtable without per-impl boilerplate.
153    ///
154    /// Used by [`crate::widget_tree::WidgetTree::widget_type_histogram`]
155    /// for the `widget.census` telemetry event. Custom
156    /// widgets that wrap their state in a generic struct may
157    /// override to give analytics a stable name independent of the
158    /// generic parameter.
159    fn type_name(&self) -> &'static str {
160        std::any::type_name::<Self>()
161    }
162
163    /// Compose child widgets. Called once after the widget is placed in the
164    /// arena, and again on environment change (theme switch, locale switch).
165    /// Takes `&mut self` — store child IDs, signal handles, any state needed later.
166    /// Returns the list of root child IDs (empty for leaf widgets).
167    fn build(
168        &mut self,
169        _ctx: &mut crate::build_context::BuildContext,
170    ) -> Vec<crate::widget_id::WidgetId> {
171        Vec::new()
172    }
173
174    /// Respond to the parent's size proposal with this widget's wanted size,
175    /// grow/shrink weights, and compression floor (see [`LayoutResponse`]).
176    ///
177    /// Most widgets just return a `Size` (auto-converts via `From<Size>` to a
178    /// fully rigid response). Grow-bearing widgets (`Spacer`, `Expand`) return
179    /// a non-zero `flex`; shrink-bearing widgets (`Shrinkable`, single-line
180    /// `TextWidget`) return a non-zero `shrink` with a `min` floor.
181    ///
182    /// The parent honors `size` as a floor for growth and distributes positive
183    /// slack proportional to `flex`; when over-constrained it distributes the
184    /// deficit proportional to `shrink`, never below `min`.
185    ///
186    /// **Determinism / height-for-width contract.** This must be a *deterministic
187    /// function of the widget's state and the `proposal`*: two calls with the
188    /// same proposal in one layout pass must return the same value. The result
189    /// must be correct *for the proposal given* — in particular a
190    /// height-for-width widget queried with `{width: Some(w), height: None}`
191    /// must return its height *at width `w`*. The framework memoizes results per
192    /// `(widget, proposal)` within a pass (see
193    /// [`cacheable_layout`](Self::cacheable_layout)) to keep negotiation O(n).
194    ///
195    /// Side effects are permitted **as long as they are idempotent** — the cache
196    /// may skip them on a repeat query, so a side effect (e.g. snapshotting
197    /// measured state into a `Signal`) must be safe to run any number of times
198    /// ≥ 1 per pass and leave the same final state. Most measuring widgets
199    /// (`Collapse`, `SceneView`, inspector tabs) satisfy this with a guarded or
200    /// overwriting set. A side effect that must run on *every* call (e.g. a call
201    /// counter) is non-idempotent — opt out via `cacheable_layout`.
202    fn layout_response(&self, proposal: SizeProposal, ctx: &LayoutContext) -> LayoutResponse;
203
204    /// Whether this widget's [`layout_response`](Self::layout_response) may be
205    /// memoized by the per-pass layout cache. Defaults to `true`.
206    ///
207    /// Override to `false` only when `layout_response` has a **non-idempotent**
208    /// side effect that must run on every call (a call counter, a one-shot
209    /// trigger). Idempotent side effects — the common case, e.g. overwriting a
210    /// measured size into a `Signal` — do **not** need to opt out: the cache may
211    /// skip a redundant identical-proposal repeat, but the value was already
212    /// written and the final state is correct. The debug inspector's
213    /// `BoundsTracker` opts out defensively (its whole-tree snapshot is the
214    /// payload, not a by-product of sizing).
215    fn cacheable_layout(&self) -> bool {
216        true
217    }
218
219    /// Position children within the allocated bounds.
220    ///
221    /// Called for **every active widget on every layout pass** — including
222    /// leaves, which receive an empty `children` slice. It is therefore also the
223    /// canonical *"here are your final, parent-assigned bounds"* hook, and the
224    /// only one that runs during layout: `layout_response` sees a *proposal*, not
225    /// the outcome, and `paint` runs too late for anything the renderer consumes
226    /// before it (a node-level transform scope, a text engine's viewport).
227    ///
228    /// A widget whose bounds feed such a thing must read them here, not in
229    /// `paint`. `SceneView` is the motivating case: it folds `bounds.origin` into
230    /// the view transform the render walker pushes *around* its subtree, so a
231    /// scene that learned its origin only at paint time drew its content offset
232    /// by `-bounds.origin` — an error that then scaled with zoom.
233    ///
234    /// **If you mutate interior state here, keep the write and its consequences
235    /// together.** This hook runs *before* `paint`, so writing a field that
236    /// `paint` later uses as a compare-then-act change detector will silently
237    /// blind that detector. (Both text engines were bitten: `place_children` set
238    /// the `viewport_width` that `paint` compared against, so `paint` concluded
239    /// "unchanged" and skipped the `engine.set_viewport` + relayout it owed.)
240    /// Route such writes through one idempotent helper that both hooks call.
241    ///
242    /// **Do not `Signal::set` here at [`BindingLevel::Relayout`] or
243    /// [`Rebuild`](crate::binding::BindingLevel::Rebuild).** This runs inside the
244    /// layout pass, so dirtying the tree from it re-enters layout. A plain
245    /// `Cell`/`RefCell` (what the colour-picker leaves use to cache their bounds
246    /// for hit-testing) or a `RepaintOnly` signal is fine.
247    ///
248    /// [`BindingLevel::Relayout`]: crate::binding::BindingLevel::Relayout
249    fn place_children(
250        &self,
251        _bounds: Rect,
252        _proposal: SizeProposal,
253        _children: &mut [WidgetPlacement],
254        _ctx: &LayoutContext,
255    ) {
256        // Leaf widgets have no children to place.
257    }
258
259    /// Draw the widget's visual representation.
260    fn paint(&self, _bounds: Rect, _canvas: &mut Canvas, _ctx: &PaintContext) {
261        // Default: nothing to paint (layout-only containers).
262    }
263
264    /// Whether this widget wants its [`after_paint`](Self::after_paint)
265    /// hook to fire each frame. Returning `false` (the default) saves
266    /// a virtual call per widget per frame for the vast majority of
267    /// widgets that don't aggregate descendant geometry.
268    ///
269    /// Same opt-in pattern as
270    /// [`wants_descendant_redirects`](Self::wants_descendant_redirects).
271    fn wants_after_paint(&self) -> bool {
272        false
273    }
274
275    /// Called once per frame after this widget's subtree has finished
276    /// painting. Receives a read-only view of the layout-resolved
277    /// arena so a parent can read its descendants' final bounds —
278    /// e.g. `TitleBar` aggregates its drag region and control-button
279    /// rects into a single `HitRegions` payload for the Windows
280    /// backend's `WM_NCHITTEST`.
281    ///
282    /// Walk order is depth-first **post**-order: a parent's
283    /// `after_paint` runs after every descendant's `paint` has
284    /// committed.
285    ///
286    /// Default: empty. Only widgets that override
287    /// [`wants_after_paint`](Self::wants_after_paint) and return `true`
288    /// see this called.
289    fn after_paint(&self, _view: &WidgetTreeView<'_>, _ctx: &PaintContext) {}
290
291    /// Whether this widget wants its [`post_paint`](Self::post_paint)
292    /// hook to fire each frame. Returning `false` (the default) saves a
293    /// virtual call per widget per frame for the vast majority of widgets
294    /// that don't draw a foreground over their children.
295    ///
296    /// Same opt-in pattern as [`wants_after_paint`](Self::wants_after_paint).
297    fn wants_post_paint(&self) -> bool {
298        false
299    }
300
301    /// Draw a foreground layer *over* this widget's children.
302    ///
303    /// The normal [`paint`](Self::paint) emits a widget's draws *before*
304    /// its children — a backdrop. `post_paint` emits *after* the entire
305    /// child subtree, so its draws land on top. This is the supported way
306    /// for a composing widget to paint over its own descendants:
307    /// inset shadows, a focus ring that must overlay content, a scrim, or
308    /// `SceneView`'s "over" lightweight band (selection lasso, highlighted
309    /// connectors).
310    ///
311    /// Runs inside the same clip / transform / opacity / blur scopes as
312    /// the widget and its children, so a foreground decoration pans,
313    /// scales and clips consistently with the subtree it covers. It is
314    /// paint-only — no hit-testing and no accessibility node; for
315    /// interactive overlays that must escape the widget's bounds, use the
316    /// overlay system instead.
317    ///
318    /// Default: empty. Only widgets that override
319    /// [`wants_post_paint`](Self::wants_post_paint) and return `true` see
320    /// this called.
321    fn post_paint(&self, _bounds: Rect, _canvas: &mut Canvas, _ctx: &PaintContext) {}
322
323    /// Declare this widget's accessibility identity.
324    fn accessibility(&self, _builder: &mut AccessNodeBuilder) {}
325
326    /// Whether this widget wants the AT walker to consult its
327    /// [`a11y_redirect_descendant`](Self::a11y_redirect_descendant)
328    /// hook for *every* descendant during AT tree emission, not
329    /// just its direct arena children.
330    ///
331    /// Returning `true` opts this widget into ancestor-chain
332    /// queries: as the walker iterates each descendant's parent
333    /// to decide where the descendant's `NodeId` lands in the AT
334    /// tree, it walks up the arena from that parent and asks
335    /// every ancestor with this flag set. First `Some(_)` wins
336    /// (closest ancestor takes priority — same precedence as a
337    /// CSS-like cascade).
338    ///
339    /// Returning `false` (the default) makes the walker pay the
340    /// O(depth) ancestor walk only for trees that genuinely need
341    /// it. Only opt in if your widget actively places
342    /// non-direct-child descendant `NodeId`s in its own
343    /// `accessibility()` emission — `teksilo_scene::SceneView` is
344    /// the canonical example.
345    ///
346    /// Default: `false`.
347    fn wants_descendant_redirects(&self) -> bool {
348        false
349    }
350
351    /// Optional redirection hook for AT-tree placement of a child.
352    ///
353    /// The accessibility walker consults every ancestor that opts
354    /// in via
355    /// [`wants_descendant_redirects`](Self::wants_descendant_redirects),
356    /// starting at the child's immediate arena parent and walking
357    /// up to the root. An ancestor whose flag is `false` is
358    /// skipped without its hook ever being called, the immediate
359    /// parent included, and the walk carries on past it. First
360    /// `Some(_)` wins, scanned bottom-up (closest opted-in
361    /// ancestor takes priority). Returning
362    /// `Some(_)` tells the walker that this widget has *already*
363    /// placed `descendant`'s `NodeId` somewhere else (typically
364    /// under a synthetic node it emitted in its own
365    /// `accessibility()` call), and the walker should NOT add it
366    /// to its arena parent's children list.
367    ///
368    /// The returned `NodeId` is informational — it identifies the
369    /// new logical parent in case the walker wants to bookkeep
370    /// (e.g., dedupe). The walker does not validate that
371    /// `descendant`'s NodeId is actually in that target's children
372    /// list; it is the implementing widget's responsibility to
373    /// have placed it there during its `accessibility()` emission
374    /// (e.g. via `AccessNodeBuilder::attach_scene_child_under`).
375    ///
376    /// Used by `teksilo_scene::SceneView` to graft heavyweight
377    /// `Widget` items into an app-declared logical AT tree.
378    /// Other layered containers can adopt the same pattern.
379    ///
380    /// Default: `None` — no redirection.
381    fn a11y_redirect_descendant(
382        &self,
383        _self_id: WidgetId,
384        _descendant: WidgetId,
385    ) -> Option<accesskit::NodeId> {
386        None
387    }
388
389    /// Suggest an accessible title to an enclosing container that
390    /// wraps this widget as content — typically a modal / dialog
391    /// shell that wants to propagate the inner content's visible
392    /// title as the shell's own accessible name.
393    ///
394    /// Example: `ModalContainer` wraps a `DialogContent`. The
395    /// container owns the `Role::Dialog` node and needs a name;
396    /// `DialogContent` overrides this method to return its own
397    /// `title` string. The container queries this on its pending
398    /// content at build time and uses the result if set.
399    ///
400    /// Default: `None` — widgets that don't carry a natural
401    /// title don't need to override.
402    fn accessible_title_hint(&self) -> Option<String> {
403        None
404    }
405
406    /// Optional hint that directs initial focus to a specific
407    /// descendant when this widget is the root of a deferred-built
408    /// modal surface.
409    ///
410    /// The modal presentation pipeline consults this after building
411    /// the content subtree, in priority order: the caller's
412    /// `ModalRequest::focus_target` → the content widget's
413    /// `initial_focus_hint` → `first_focusable_descendant`.
414    /// `MessageBox` overrides this to return the widget id of its
415    /// configured default button, so platform-native button orderings
416    /// (Cancel-left + Default-right-but-focused) work without
417    /// forcing the default button to be the first focusable
418    /// descendant in tree-walk order.
419    ///
420    /// Default: `None` — widgets that don't need to direct initial
421    /// focus to a non-first-focusable descendant don't override.
422    fn initial_focus_hint(&self) -> Option<WidgetId> {
423        None
424    }
425
426    /// Which descendant a keyboard request for a context menu should target.
427    ///
428    /// The context-menu key (and Shift+F10) opens the menu of the **focused**
429    /// widget. For a data view that is the wrong node: `ListView`, `TreeView`,
430    /// `TableView`, `TreeTableView` and `GridView` are focusable as a whole and
431    /// their rows deliberately are not — the container owns focus and
432    /// `set_selected` is what tells assistive technology which row is current
433    /// (see `list_item_a11y`). Without this hook the chord would open the
434    /// *list's* menu rather than the selected row's, in exactly the widget
435    /// family where a per-row menu matters most.
436    ///
437    /// Return the widget id of the row (or cell, or tile) the menu should be
438    /// about. The dispatcher then walks up from there, so a view whose rows
439    /// carry no factory of their own still finds the container's.
440    ///
441    /// Default: `None` — the focused widget is the target, which is right for
442    /// every widget that is itself the thing the user is pointing at.
443    fn context_menu_key_target(&self) -> Option<WidgetId> {
444        None
445    }
446
447    /// Return the child widget IDs that this widget manages.
448    fn children(&self) -> Vec<WidgetId> {
449        Vec::new()
450    }
451
452    /// Optional override for the child ORDER presented to assistive
453    /// technology, when it must differ from the paint / z-order child
454    /// order returned by [`children`](Self::children).
455    ///
456    /// Return `None` (the default) to let the accessibility walker use the
457    /// arena's child order — correct for almost every widget. Return
458    /// `Some(ids)` to reorder (or restrict) how children appear in the AT
459    /// tree and in the linear Tab reading order, WITHOUT affecting layout or
460    /// paint. `TableView` / `TreeTableView` use this to read the header
461    /// before the body rows even though they build the body first so it
462    /// paints beneath the header (WCAG 1.3.2 Meaningful Sequence).
463    fn accessibility_children(&self) -> Option<Vec<WidgetId>> {
464        None
465    }
466
467    /// Downcast hook. Default implementation returns `None`; concrete
468    /// widgets override with `Some(self)` when they want to expose
469    /// their concrete type to test-level introspection or reflection.
470    /// The trait already bounds on `std::any::Any` so concrete types
471    /// satisfy the `'static` requirement.
472    fn as_any(&self) -> Option<&dyn std::any::Any> {
473        None
474    }
475
476    /// Mutable counterpart of [`as_any`](Self::as_any). Default
477    /// returns `None`; widgets that want to expose mutable state to
478    /// tests (e.g. so a test can mutate a `Scene` inside a
479    /// `SceneView` post-layout) override with `Some(self)`. Should
480    /// follow the same opt-in pattern as `as_any`: only widgets
481    /// that opt into `&` introspection should opt into `&mut`.
482    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
483        None
484    }
485
486    /// Whether this widget clips its children to its bounds.
487    fn clips_children(&self) -> bool {
488        false
489    }
490
491    /// The rectangle (in **absolute tree coordinates**) that best *represents*
492    /// this widget when the framework reveals it into an ancestor scroll area on
493    /// focus gain. Returning `None` (the default) reveals the widget's whole
494    /// bounds — correct for most controls.
495    ///
496    /// A widget that can be much taller than a viewport — a `RichTextEditor`
497    /// grown inside a page `ScrollArea`, a `ListView` / `TreeView` taller than
498    /// its scroller — should override this to return the sub-rectangle the user
499    /// actually cares about (the caret line, the selected row). Otherwise
500    /// [`scroll_focused_into_view`](crate::widget_tree::WidgetTree) reveals the
501    /// *entire* box, which for a tall widget scrolls the page to its bottom on a
502    /// click that only meant to place the caret near the top. The returned rect
503    /// feeds the same ancestor-only `scroll_rect_into_view` engine the caret /
504    /// selection follow uses (the focused widget itself is excluded), so it
505    /// never double-scrolls against the widget's own internal follow.
506    ///
507    /// `bounds` is this widget's current absolute rectangle as the arena stores
508    /// it, so an override can place its interior rect without depending on a
509    /// paint-set origin.
510    fn focus_reveal_rect(&self, _bounds: Rect) -> Option<Rect> {
511        None
512    }
513
514    /// Whether the point lies inside this widget's *actual* shape, not
515    /// just its rectangular bounds. Consulted by hit-testing right after
516    /// the bounds check: returning `false` for a point that *is* inside
517    /// the bounding box makes the widget transparent to the click there,
518    /// so it **falls through** to whatever sibling is painted underneath
519    /// (the same machinery as a fully pass-through node, but shape-aware).
520    ///
521    /// Both arguments are in the widget's bounds space: `local_point` is
522    /// the point being tested and `bounds` is this widget's rectangle, so
523    /// a non-rectangular widget can test the point against its silhouette.
524    ///
525    /// The default returns `true` for any in-bounds point (a plain
526    /// rectangle), so every existing widget is unaffected. Override for
527    /// irregular shapes — an ellipse / cloud scene node, a circular
528    /// handle — so a click lands on the shape you see, not its bounding
529    /// box, and clicks in the transparent corners reach the node beneath.
530    /// This mirrors the lightweight tier's `SceneItem::shape_contains`.
531    fn hit_shape(&self, _local_point: Point, _bounds: Rect) -> bool {
532        true
533    }
534
535    /// How `rebuild_single_widget` treats this widget's existing children
536    /// when re-running its `build()`.
537    ///
538    /// **`false` (default) — re-derive.** Rebuild is "tear down and
539    /// reconstruct": every old child subtree is destroyed up front, then
540    /// `build()` produces a fresh set. The right semantic for data-driven
541    /// widgets like `Repeater` / `ListView` that rebuild their children from
542    /// current model state with fresh `WidgetId`s. A `false` widget must NOT
543    /// re-attach an old child id — it has already been destroyed.
544    ///
545    /// **`true` — reconcile.** `build()` re-attaches (by id) the children it
546    /// keeps and drops the rest. The framework keeps every re-attached child's
547    /// subtree intact — focus, scroll offset, text contents, signal
548    /// subscriptions all survive — and destroys only the old children the new
549    /// build dropped *and* did not re-parent elsewhere. This is the mode for
550    /// widgets that memoize stateful children across rebuilds:
551    ///
552    /// * `Switcher` keeps every mounted page alive so switching tabs doesn't
553    ///   wipe the inactive pages' state.
554    /// * `SceneView` re-pushes the same heavyweight scene-widget ids each
555    ///   rebuild (draining drag-to-move / marquee commits) — they must stay
556    ///   attached or the cards "disappear" on every drag end.
557    /// * `TabWidget` / `DockingLayout` / `CompositeTooltip` re-attach memoized
558    ///   panes / a one-shot body widget that cannot be reconstructed.
559    /// * `MenuBar` re-derives its menu triggers fresh each build (the model may
560    ///   have changed — the reconcile reaps the superseded ones) while keeping
561    ///   its memoized leading/trailing slot widgets, so a stateful slot control
562    ///   survives a model-version rebuild.
563    ///
564    /// The reconcile follows **authoritative parent pointers**, so a kept
565    /// subtree that `build()` re-parents *out* of a dropped sibling and into
566    /// the new tree survives — it is not swept via the dropped sibling's now
567    /// stale `children` list. Dropped children are genuinely destroyed (state
568    /// unmounted, arena slots freed), not left as stranded, still-active
569    /// orphans.
570    fn preserves_children_on_rebuild(&self) -> bool {
571        false
572    }
573
574    /// Whether this widget, used as tooltip content, currently has anything
575    /// worth showing.
576    ///
577    /// Consulted by `WidgetTree` just before a dwell matures into an overlay.
578    /// Returning `false` cancels the show — the anchor simply has no tooltip
579    /// this time — so a blank or unresolved string does not pop an empty
580    /// chromed bubble, which reads as a rendering fault rather than as
581    /// "nothing to say here".
582    ///
583    /// Defaults to `true`: content that hosts an arbitrary widget tree (a
584    /// chart, a progress row) is meaningful without any text, and a custom
585    /// content widget must never be suppressed by a check it did not opt into.
586    /// Only widgets whose *whole* payload is a string — `TooltipWidget` — have
587    /// a well-defined notion of being empty.
588    fn tooltip_has_content(&self) -> bool {
589        true
590    }
591
592    /// Declare the rebindable keyboard shortcuts this widget exposes,
593    /// *without* installing handlers. The framework calls this at
594    /// arena insertion time (before `build()`) and at certain lazy
595    /// boundaries (e.g. `Switcher` walks declarations on its
596    /// not-yet-mounted `Pending` slots), so settings UIs and the
597    /// `ShortcutRegistry` see the keystrokes the moment the owning
598    /// container mounts — even if `build()` hasn't run.
599    ///
600    /// Pair this with `BuildContext::register_shortcut` in `build()`
601    /// to install the matching `on_activate` handler: the build-time
602    /// registration *upserts* the declared entry, preserving any user
603    /// override and the declared keystrokes while attaching the
604    /// closure that actually fires.
605    ///
606    /// The returned shortcuts may omit `on_activate` (a metadata-only
607    /// declaration). When matched at dispatch time without a
608    /// registered handler, the framework synthesizes a no-parameter
609    /// intent from the shortcut's id — same path as a build-time
610    /// registration with `on_activate: None`.
611    ///
612    /// Default: empty (no declared shortcuts).
613    fn declare_shortcuts(&self) -> Vec<crate::shortcut::Shortcut> {
614        Vec::new()
615    }
616
617    /// Extract attached handler set from a `WidgetWithHandlers` wrapper.
618    /// Called during arena insertion to transfer handlers to the `WidgetNode`.
619    /// Default: returns `None` (no attached handlers).
620    fn take_handler_set(&mut self) -> Option<crate::widget_builder::HandlerSet> {
621        None
622    }
623}