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