Skip to main content

teksilo_core/
widget_builder.rs

1// SPDX-License-Identifier: MPL-2.0
2// SPDX-FileCopyrightText: 2026 FernTech
3
4//! WidgetBuilder trait — blanket-implemented for all Widget types.
5//!
6//! Provides attached event handler methods and framework-level properties.
7//! Each method wraps the widget in a `WidgetWithHandlers<W>` that stores
8//! the handlers and metadata alongside the widget. When the widget is
9//! inserted into the arena, the handler set is extracted and applied to
10//! the `WidgetNode`.
11//!
12//! The four click-style handlers (`on_tap` / `on_double_tap` /
13//! `on_triple_tap` / `on_long_press`) all receive a borrowed
14//! [`crate::gesture::TapEvent`] (position + button + modifiers) and
15//! default to [`crate::event::ButtonMask::PRIMARY`] acceptance. Widen
16//! that filter via the matching `accept_*_buttons(...)` knob — see the
17//! "Event System" section in `docs/events-and-gestures.md` for the
18//! full contract and examples.
19
20use teksilo_canvas::Point;
21
22use crate::event::{ButtonMask, EventResponse, WidgetEvent};
23use crate::event_handlers::EventHandlers;
24use crate::gesture::MultiContact;
25use crate::gesture::{DragPhase, PinchPhase, SwipeDirection, TapEvent};
26use crate::pointer::touch_action::{PanAxes, PanClaim, TouchAction};
27use crate::signal::Prop;
28use crate::widget::{CursorIcon, EventContext, Widget};
29use crate::widget_id::WidgetId;
30
31// ---------------------------------------------------------------------------
32// Accessibility overrides
33// ---------------------------------------------------------------------------
34
35/// Subtree visibility / merge mode applied by the accessibility tree walker.
36///
37/// Set via `WidgetBuilder::access_exclude_subtree()` /
38/// `access_merge_subtree()`. The walker honors the mode after the parent
39/// node has been emitted, before recursing into descendants.
40#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
41pub enum AccessSubtreeMode {
42    /// Normal walk — descendants emitted as their own AT nodes.
43    #[default]
44    Inherit,
45    /// Descendants pruned from the AT tree entirely. Parent node still
46    /// emitted normally. Equivalent to Flutter's `excludeSemantics: true`.
47    Exclude,
48    /// Descendants' labels / descriptions / values / actions are
49    /// concatenated into the parent's emitted node, then descendants are
50    /// pruned. The parent reads as a single AT element. Equivalent to
51    /// Flutter's `mergeAllDescendants: true` and SwiftUI's
52    /// `.accessibilityElement(children: .combine)`.
53    Merge,
54}
55
56/// Builder-level accessibility overrides.
57///
58/// Carried on `HandlerSet` during builder-chain construction, mirrored
59/// onto `WidgetNode::access_overrides` at arena insertion (parallel to
60/// `clips_children` / `cursor` / `focus_within_signal`), then applied by
61/// the accessibility tree walker after the inner widget's
62/// `accessibility(&self, builder)` runs.
63///
64/// User-visible string fields store a `Prop<String>` rather than a
65/// resolved `String`, so they stay reactive to locale changes.
66/// `teksilo-core` can't name `LocalizedString` (that lives in the
67/// downstream `teksilo-i18n` crate), but `Prop<String>` is a core type
68/// and `impl From<LocalizedString> for Prop<String>` in `teksilo-i18n`
69/// yields a `Prop::Bound` over a locale-observing `Signal<String>`. So
70/// `.access_label(tr!(save()))` stores a bound prop; the accessibility
71/// walker reads `.get()` at AT-build time, and `sync_accessibility`
72/// re-walks on locale change so the announced value follows the locale.
73/// The `_literal` builder variants store `Prop::Static` and are the
74/// `#[doc(hidden)]` grep markers for explicitly untranslated call sites
75/// (the only literal path reachable from within `teksilo-core`).
76#[derive(Default)]
77pub struct AccessibilityOverrides {
78    // -- Tier 1: labeling / state -----------------------------------------
79    pub label: Option<Prop<String>>,
80    pub description: Option<Prop<String>>,
81    pub value: Option<Prop<String>>,
82    pub role: Option<accesskit::Role>,
83    /// Reactive hidden-from-AT flag. `Some(prop)` where the prop reads
84    /// `true` hides the node and its whole subtree from assistive
85    /// technologies; `false` un-sets a
86    /// hidden state the inner widget emitted unconditionally. Bound props are
87    /// registered at `AccessibilityOnly` so the AT tree re-walks when they
88    /// flip (see the insertion paths in `widget_tree.rs`).
89    pub hidden: Option<Prop<bool>>,
90    pub disabled: Option<bool>,
91
92    // -- Tier 2: relationships / live / identity --------------------------
93    pub identifier: Option<String>,
94    pub controls: Vec<WidgetId>,
95    pub described_by: Vec<WidgetId>,
96    pub labelled_by: Vec<WidgetId>,
97    pub live: Option<accesskit::Live>,
98    pub aria_current: Option<accesskit::AriaCurrent>,
99    /// Pre-formatted shortcut announcement string (e.g. `"Ctrl+S"`).
100    /// Used by `access_shortcut_literal`. For chords routed through a
101    /// `Shortcut` registration, prefer `access_shortcut_id` (stored
102    /// in `shortcut_id`) so the announcement tracks rebinds.
103    pub shortcut: Option<String>,
104    /// Registered shortcut id (e.g. `"app.save"`). The accessibility
105    /// walker resolves the current keystroke from
106    /// `WidgetTree::shortcut_registry()` at AT-build time and writes
107    /// the formatted string to `Node::keyboard_shortcut`. Refreshes
108    /// automatically when the user rebinds (the registry's `version`
109    /// signal triggers a re-sync).
110    pub shortcut_id: Option<String>,
111    pub has_popup: Option<accesskit::HasPopup>,
112    pub orientation: Option<accesskit::Orientation>,
113
114    // -- Tier 3: numeric / actions / escape hatch -------------------------
115    pub numeric_value: Option<f64>,
116    pub min_numeric_value: Option<f64>,
117    pub max_numeric_value: Option<f64>,
118    pub numeric_step: Option<f64>,
119
120    /// Standard `accesskit::Action` advertisements with their handlers.
121    /// Dispatched by `pointer_router.rs` when handling
122    /// `WidgetEvent::AccessAction`, layered on top of any
123    /// user-installed `on_access_action` / `on_access_action_request`
124    /// handlers (both fire for the same dispatched event).
125    pub actions: Vec<(accesskit::Action, Box<dyn FnMut(&mut EventContext)>)>,
126
127    /// Actions to remove from the widget-emitted action list (called
128    /// after the widget's `accessibility()` runs, before custom-action
129    /// emission).
130    pub removed_actions: Vec<accesskit::Action>,
131
132    /// Custom-named actions (SwiftUI `.accessibilityAction(named:_:)`).
133    /// Each entry pairs a (reactive) description prop with a handler.
134    /// Index in the vec is the stable `i32` `CustomAction::id` exposed
135    /// to AT software.
136    pub custom_actions: Vec<(Prop<String>, Box<dyn FnMut(&mut EventContext)>)>,
137
138    /// Final escape hatch — invoked **last** in `apply()` with full
139    /// `&mut AccessNodeBuilder` access (including `inner_mut()`). Used
140    /// for sub-node surgery (synthetic children) and for cases the
141    /// typed surface doesn't cover.
142    pub customize: Option<Box<dyn Fn(&mut crate::accessibility::AccessNodeBuilder)>>,
143}
144
145impl AccessibilityOverrides {
146    /// Fold `other` into `self`: later scalars win, lists append.
147    ///
148    /// Needed because overrides reach a node from **two** directions — a builder
149    /// chain at construction (`WidgetWithHandlers::access_*`) and a later
150    /// [`BuildContext::apply_handlers`](crate::build_context::BuildContext::apply_handlers)
151    /// on the same id, which is how a widget gives a node it already built one
152    /// more action. Assigning the second block over the first silently dropped
153    /// everything in the first, and nothing else reads an override block, so the
154    /// loss was invisible: a `TreeTableView` row lost `ScrollIntoView` and its
155    /// `Expand` / `Collapse` pair the moment it also gained a move command.
156    ///
157    /// The merge rules are the ones
158    /// [the overrides page](https://github.com/ferntech-eu/teksilo/blob/main/docs/accessibility-overrides.md)
159    /// already documents for a single block: scalars replace when set, lists
160    /// append. `customize` is the exception — it is an escape hatch and both
161    /// halves may matter, so two are **chained** in application order rather
162    /// than one winning.
163    pub(crate) fn merge_from(&mut self, other: Self) {
164        macro_rules! scalar {
165            ($($field:ident),+ $(,)?) => {
166                $(if other.$field.is_some() { self.$field = other.$field; })+
167            };
168        }
169        scalar!(
170            label,
171            description,
172            value,
173            role,
174            hidden,
175            disabled,
176            identifier,
177            live,
178            aria_current,
179            shortcut,
180            shortcut_id,
181            has_popup,
182            orientation,
183            numeric_value,
184            min_numeric_value,
185            max_numeric_value,
186            numeric_step,
187        );
188        self.controls.extend(other.controls);
189        self.described_by.extend(other.described_by);
190        self.labelled_by.extend(other.labelled_by);
191        self.actions.extend(other.actions);
192        self.removed_actions.extend(other.removed_actions);
193        // Appended, so the ids already published for the existing entries keep
194        // pointing at the same handlers.
195        self.custom_actions.extend(other.custom_actions);
196        self.customize = match (self.customize.take(), other.customize) {
197            (Some(first), Some(second)) => Some(Box::new(move |builder| {
198                first(builder);
199                second(builder);
200            })),
201            (first, second) => second.or(first),
202        };
203    }
204}
205
206impl std::fmt::Debug for AccessibilityOverrides {
207    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
208        f.debug_struct("AccessibilityOverrides")
209            .field("label", &self.label)
210            .field("description", &self.description)
211            .field("value", &self.value)
212            .field("role", &self.role)
213            .field("hidden", &self.hidden)
214            .field("disabled", &self.disabled)
215            .field("identifier", &self.identifier)
216            .field("shortcut", &self.shortcut)
217            .field("shortcut_id", &self.shortcut_id)
218            .field("controls_len", &self.controls.len())
219            .field("described_by_len", &self.described_by.len())
220            .field("labelled_by_len", &self.labelled_by.len())
221            .field("actions_len", &self.actions.len())
222            .field("removed_actions", &self.removed_actions)
223            .field("custom_actions_len", &self.custom_actions.len())
224            .finish()
225    }
226}
227
228impl AccessibilityOverrides {
229    /// Apply the override scalar / list fields onto a builder. Called by
230    /// the accessibility tree walker after the inner widget's
231    /// `accessibility(&self, builder)` runs and before the framework
232    /// finalizes the node.
233    pub(crate) fn apply(&self, b: &mut crate::accessibility::AccessNodeBuilder) {
234        use crate::accessibility::widget_id_to_node_id;
235
236        if let Some(ref p) = self.label {
237            b.set_name(p.get());
238        }
239        if let Some(ref p) = self.description {
240            b.set_description(p.get());
241        }
242        if let Some(ref p) = self.value {
243            b.set_value(p.get());
244        }
245        if let Some(role) = self.role {
246            b.set_role(role);
247        }
248        match self.hidden.as_ref().map(|p| p.get()) {
249            Some(true) => b.set_hidden(),
250            Some(false) => b.clear_hidden(),
251            None => {}
252        }
253        match self.disabled {
254            Some(true) => b.set_disabled(),
255            Some(false) => b.clear_disabled(),
256            None => {}
257        }
258        if let Some(ref s) = self.identifier {
259            b.set_author_id(s.clone());
260        }
261        for &id in &self.controls {
262            b.push_controlled(widget_id_to_node_id(id));
263        }
264        for &id in &self.described_by {
265            b.push_described_by(widget_id_to_node_id(id));
266        }
267        for &id in &self.labelled_by {
268            b.push_labelled_by(widget_id_to_node_id(id));
269        }
270        if let Some(live) = self.live {
271            b.set_live(live);
272        }
273        if let Some(c) = self.aria_current {
274            b.set_aria_current(c);
275        }
276        if let Some(ref s) = self.shortcut {
277            b.set_keyboard_shortcut(s.clone());
278        }
279        // `shortcut_id` resolution happens in the accessibility tree
280        // walker (where the `ShortcutRegistry` is reachable) — see
281        // `accessibility_emit_impl::build_accessibility_recursive`.
282        if let Some(p) = self.has_popup {
283            b.set_has_popup(p);
284        }
285        if let Some(o) = self.orientation {
286            b.set_orientation(o);
287        }
288        if let Some(v) = self.numeric_value {
289            b.set_numeric_value(v);
290        }
291        if let Some(v) = self.min_numeric_value {
292            b.set_min_numeric_value(v);
293        }
294        if let Some(v) = self.max_numeric_value {
295            b.set_max_numeric_value(v);
296        }
297        if let Some(v) = self.numeric_step {
298            b.set_numeric_value_step(v);
299        }
300        // Suppression first, then advertisement — so `access_remove_action`
301        // can prune what the widget emitted, but a subsequent
302        // `access_action(same_action, ...)` re-advertises with the
303        // override-installed handler.
304        for &a in &self.removed_actions {
305            b.remove_action(a);
306        }
307        for (action, _) in &self.actions {
308            b.add_action(*action);
309        }
310        if !self.custom_actions.is_empty() {
311            // Appended after the widget's own, which is the list rule every
312            // other override list follows: replacing it took a slider's "Reset
313            // to default" away the moment an application added an action of
314            // its own. The override entries keep their positional ids, which is
315            // what the dispatcher routes them by, so a widget publishing
316            // actions of its own numbers them clear of the low ones.
317            let mut custom: Vec<accesskit::CustomAction> = b.inner_mut().custom_actions().to_vec();
318            custom.extend(
319                self.custom_actions
320                    .iter()
321                    .enumerate()
322                    .map(|(i, (label, _))| accesskit::CustomAction {
323                        id: i as i32,
324                        description: label.get(),
325                    }),
326            );
327            b.set_custom_actions(custom);
328            // A node's custom actions are only reachable when it also
329            // advertises `Action::CustomAction`: an adapter reports the list
330            // through the supported-action gate, not through the list's
331            // emptiness (`accesskit_ios-0.2.0/src/node.rs:109` is the one that
332            // says so in code). Without this the whole vector is decoration.
333            b.add_action(accesskit::Action::CustomAction);
334        }
335        if let Some(ref f) = self.customize {
336            f(b);
337        }
338    }
339}
340
341// ---------------------------------------------------------------------------
342// HandlerSet — temporary storage before arena insertion
343// ---------------------------------------------------------------------------
344
345/// Type alias for a context-menu content factory.
346///
347/// The factory is invoked on every right-click that lands on a widget
348/// owning the factory (or on a descendant whose nearest ancestor with
349/// a factory is this one). It receives:
350///
351/// - `position`: pointer position in **window** coordinates — the factory
352///   runs straight off the right-click `PointerDown`, before the localizing
353///   handler dispatch, and the same point reaches every factory the walk
354///   tries, so subtract the widget's own origin before hit-testing. Useful
355///   when the menu's contents depend on *what* was right-clicked (a row in a
356///   list, a node in a tree, an item under a hit-test, …).
357/// - `ctx`: a full [`EventContext`], so the factory can read window
358///   state, query app state, send intents (e.g. for analytics), or
359///   update Signals before the menu mounts.
360///
361/// The same factory also answers the keyboard
362/// ([`ContextMenuTrigger::Keyboard`]) and an assistive technology's
363/// `ShowContextMenu` action. Neither carries a point, so `position` is then an
364/// anchor the framework picked on the widget the menu is about, not a place
365/// the user chose. [`ctx.context_menu_trigger()`](EventContext::context_menu_trigger)
366/// says which it is: act on the point (move a caret to it, pick the item under
367/// it) only for [`ContextMenuTrigger::Pointer`].
368///
369/// The factory returns:
370///
371/// - `Some(widget)` to mount `widget` as the menu overlay anchored at
372///   the factory-owning widget, placed at `position`.
373/// - `None` to **decline this right-click**. The framework continues
374///   walking up the parent chain looking for the next ancestor with a
375///   factory. This lets a widget conditionally suppress its own menu
376///   without uninstalling the factory.
377pub type ContextMenuFactory = Box<dyn Fn(Point, &mut EventContext) -> Option<Box<dyn Widget>>>;
378
379/// A **section** a node adds to the context menu opened at or below it,
380/// rather than a menu of its own: the Move rows a data view's row adds to the
381/// application's row menu.
382///
383/// When a menu opens, the closest claimer wins, as for factories: walking up
384/// from the target, a node's factory is asked first, and a node with no
385/// factory (or one that declines) but a contribution claims the menu with its
386/// section alone, mounted as the menu. Once a menu is claimed, the
387/// contributions on the owning node and above it are asked, innermost first,
388/// up to and excluding the first focusable node above the owner (a data
389/// view, so a view nested in another view's row adds only its own section,
390/// and a contribution on the view itself is never merged), and each section
391/// is handed to the menu through
392/// [`Widget::append_context_menu_section`]. A menu owned by a focusable
393/// control (a text field, a cell editor) gains no section: it is about that
394/// control, not about the row it sits in. Return `None` to add nothing.
395pub type ContextMenuContribution = Box<dyn Fn(&mut EventContext) -> Option<Box<dyn Widget>>>;
396
397/// What asked for a context menu, as a [`ContextMenuFactory`] reads it from
398/// [`EventContext::context_menu_trigger`].
399///
400/// A factory is handed a point whatever opened the menu, and only a
401/// pointer's point is one the user chose. For the keyboard and for assistive
402/// technology the point is where the menu is drawn: a factory that treated it
403/// as a click moved the caret of a text field to the middle of the field, and
404/// the menu's Paste then wrote where the user had never been.
405#[non_exhaustive]
406#[derive(Debug, Clone, Copy, PartialEq, Eq)]
407pub enum ContextMenuTrigger {
408    /// A secondary click, or a hold with a finger or a pen: the point is
409    /// where the user pointed.
410    Pointer,
411    /// The Menu key, Shift+F10, or on macOS Ctrl+Shift+M, on the focused
412    /// widget.
413    Keyboard,
414    /// An assistive technology's
415    /// [`ShowContextMenu`](accesskit::Action::ShowContextMenu) action.
416    Accessibility,
417}
418
419impl ContextMenuTrigger {
420    /// Whether the factory's point is one the user pointed at.
421    pub const fn is_pointer(self) -> bool {
422        matches!(self, Self::Pointer)
423    }
424}
425
426/// Temporary storage for handlers and metadata accumulated via builder
427/// methods. Transferred to the `WidgetNode` during arena insertion.
428pub struct HandlerSet {
429    pub(crate) handlers: EventHandlers,
430    pub(crate) focusable: Option<bool>,
431    pub(crate) tab_index: Option<i32>,
432    pub(crate) cursor: Option<CursorIcon>,
433    pub(crate) clips_children: Option<bool>,
434    /// When `Some(..)`, declares the node a text-input surface and the OS
435    /// input method is enabled (with this purpose) while it is focused.
436    /// `None` leaves the node default (no OS IME). The platform reads the
437    /// focused node's descriptor at focus-change time. See [`crate::ime`].
438    pub(crate) ime: Option<crate::ime::ImeContext>,
439    /// When `Some(true)`, the widget node is invisible to pointer
440    /// hit-testing — events fall through to whatever sits behind it.
441    /// Used by the debug inspector's overlay widgets.
442    pub(crate) event_pass_through: Option<bool>,
443    /// When `Some(true)`, a press in this node's subtree must not arm a
444    /// drag/swipe on any ancestor above it (a *gesture dead zone*). See
445    /// [`super::arena::WidgetNode::gesture_dead_zone`].
446    pub(crate) gesture_dead_zone: Option<bool>,
447    /// When `Some(true)`, the node is part of the window's own chrome. See
448    /// [`HandlerSet::window_chrome`].
449    pub(crate) window_chrome: Option<bool>,
450    /// When `Some(true)`, the node's subtree is scroll chrome. See
451    /// [`HandlerSet::scroll_chrome`].
452    pub(crate) scroll_chrome: Option<bool>,
453    /// When `Some(false)`, a click or tap activates the node without moving
454    /// keyboard focus to it. See [`HandlerSet::focus_on_click`].
455    pub(crate) focus_on_click: Option<bool>,
456    /// When `Some(..)`, selects what a **hold** on this node's subtree means for
457    /// the tree-owned long-press route. See
458    /// [`super::arena::WidgetNode::long_press_role`].
459    pub(crate) long_press_role: Option<crate::widget_tree::touch_route::LongPressRole>,
460    /// When `Some(..)`, overrides what a direct pointer may do to this
461    /// node's subtree. See [`super::arena::WidgetNode::touch_action`].
462    pub(crate) touch_action: Option<TouchAction>,
463    /// When `Some(..)`, declares this node a pan surface. See
464    /// [`super::arena::WidgetNode::pan_claim`].
465    pub(crate) pan_claim: Option<PanClaim>,
466    pub(crate) overscroll_behavior: Option<crate::OverscrollBehavior>,
467    /// When `Some(..)`, overrides when a drag on this node may begin. See
468    /// [`super::arena::WidgetNode::drag_activation`].
469    pub(crate) drag_activation: Option<teksilo_tokens::DragActivation>,
470
471    /// How many simultaneous contacts this node's recognizers serve. See
472    /// [`super::arena::WidgetNode::multi_contact`].
473    pub(crate) multi_contact: Option<MultiContact>,
474    /// When `Some(true)` and this node holds keyboard focus, a `KeyDown`
475    /// bypasses shortcut resolution and is delivered straight to it (a
476    /// *keyboard capture* surface — terminals, game viewports). See
477    /// [`super::arena::WidgetNode::keyboard_capture`].
478    pub(crate) keyboard_capture: Option<bool>,
479    /// When `Some(..)`, this node is a focus region (an F6 stop) with this
480    /// name. See [`HandlerSet::focus_region`].
481    pub(crate) focus_region: Option<Prop<String>>,
482    /// When `Some(true)`, this node and its WHOLE subtree are invisible
483    /// to pointer hit-testing (decorative overlays — count badges,
484    /// watermarks). See [`super::arena::WidgetNode::hit_transparent`].
485    pub(crate) hit_transparent: Option<bool>,
486    /// Per-node override of the *miss-only* slop this node may earn. Second
487    /// link of the precedence chain. See [`HandlerSet::hit_slop`].
488    pub(crate) hit_slop: Option<crate::pointer::hit_slop::HitSlop>,
489    /// When `Some(true)`, this node is excluded from BOTH hit-widening
490    /// mechanisms. Head of the precedence chain. See
491    /// [`HandlerSet::no_hit_slop`].
492    pub(crate) no_hit_slop: Option<bool>,
493    pub(crate) context_menu_factory: Option<ContextMenuFactory>,
494    pub(crate) context_menu_contribution: Option<ContextMenuContribution>,
495    /// User-bound signal that the framework writes whenever the
496    /// focused widget is a strict descendant of this node. See
497    /// [`HandlerSet::focus_within`].
498    pub(crate) focus_within: Option<crate::signal::Signal<bool>>,
499    /// User-bound signal that the framework writes whenever the
500    /// hovered widget is a strict descendant of this node. See
501    /// [`HandlerSet::hover_within`].
502    pub(crate) hover_within: Option<crate::signal::Signal<bool>>,
503    /// User-bound visibility binding (`bool` / `Signal<bool>` / `Prop<bool>`).
504    /// Applied at insertion via `WidgetTree::visible_when`, exactly like the
505    /// `ctx.visible_when(id, ..)` form, so `teksu!` can write `visible_when: sig`
506    /// as a plain widget property. See [`HandlerSet::visible_when`].
507    pub(crate) visible_when: Option<Prop<bool>>,
508    /// Builder-level accessibility overrides. Mirrored to
509    /// `WidgetNode::access_overrides` at insertion. Action callbacks
510    /// (`actions`, `custom_actions`) are dispatched by
511    /// `pointer_router.rs` when handling
512    /// `WidgetEvent::AccessAction`, in addition to the user's
513    /// `on_access_action` / `on_access_action_request` handlers — so
514    /// builder order doesn't matter.
515    pub(crate) access: Option<Box<AccessibilityOverrides>>,
516    /// Subtree visibility / merge mode. Mirrored to
517    /// `WidgetNode::access_subtree`.
518    pub(crate) access_subtree: Option<AccessSubtreeMode>,
519}
520
521impl HandlerSet {
522    /// Absorb `base`'s declarations wherever `self` is silent.
523    ///
524    /// `self` is the **later** declaration and wins on conflict, so this reads
525    /// left to right like the builder chain that produced the two sets. It is
526    /// Compose's `Modifier.then`, expressed on the value Teksilo already has.
527    ///
528    /// The accessibility block is **merged, not assigned**: an overrides block
529    /// carries lists (`controls`, `described_by`, `custom_actions`, ...) that a
530    /// plain assignment would drop. `WidgetArena` makes the same point at its
531    /// own merge site.
532    pub fn merge_under(&mut self, mut base: HandlerSet) {
533        macro_rules! take_if_empty {
534            ($($field:ident),* $(,)?) => {
535                $( if self.$field.is_none() { self.$field = base.$field.take(); } )*
536            };
537        }
538        take_if_empty!(
539            focusable,
540            tab_index,
541            cursor,
542            clips_children,
543            ime,
544            event_pass_through,
545            gesture_dead_zone,
546            window_chrome,
547            scroll_chrome,
548            focus_on_click,
549            long_press_role,
550            touch_action,
551            pan_claim,
552            overscroll_behavior,
553            drag_activation,
554            multi_contact,
555            keyboard_capture,
556            focus_region,
557            hit_transparent,
558            hit_slop,
559            no_hit_slop,
560            context_menu_factory,
561            context_menu_contribution,
562            focus_within,
563            hover_within,
564            visible_when,
565        );
566        if self.access_subtree.is_none() {
567            self.access_subtree = base.access_subtree.take();
568        }
569        // `base` is the earlier block, `self` the later one, and
570        // `merge_from` lets its argument win — so fold self INTO base and
571        // keep the result.
572        match (base.access.take(), self.access.take()) {
573            (Some(mut earlier), Some(later)) => {
574                earlier.merge_from(*later);
575                self.access = Some(earlier);
576            }
577            (earlier, later) => self.access = later.or(earlier),
578        }
579        self.handlers
580            .merge_under(std::mem::take(&mut base.handlers));
581    }
582
583    /// Create an empty handler set for use in `BuildContext::apply_self_handlers()`.
584    pub fn new() -> Self {
585        Self {
586            handlers: EventHandlers::new(),
587            focusable: None,
588            tab_index: None,
589            cursor: None,
590            clips_children: None,
591            ime: None,
592            event_pass_through: None,
593            gesture_dead_zone: None,
594            window_chrome: None,
595            scroll_chrome: None,
596            focus_on_click: None,
597            long_press_role: None,
598            drag_activation: None,
599            touch_action: None,
600            pan_claim: None,
601            overscroll_behavior: None,
602            multi_contact: None,
603            keyboard_capture: None,
604            focus_region: None,
605            hit_transparent: None,
606            hit_slop: None,
607            no_hit_slop: None,
608            context_menu_factory: None,
609            context_menu_contribution: None,
610            focus_within: None,
611            hover_within: None,
612            visible_when: None,
613            access: None,
614            access_subtree: None,
615        }
616    }
617
618    /// Get a `&mut` to the override block, lazily allocating it on first
619    /// access. Used by all `access_*` builder methods.
620    pub(crate) fn access_mut(&mut self) -> &mut AccessibilityOverrides {
621        self.access
622            .get_or_insert_with(|| Box::new(AccessibilityOverrides::default()))
623    }
624
625    // -- Builder methods (mirror WidgetWithHandlers) --
626
627    /// Set the on_tap handler. The closure receives a borrowed
628    /// [`TapEvent`] carrying the position in
629    /// widget-local coordinates, the finalising mouse button, and the
630    /// modifier state at that moment.
631    ///
632    /// Default acceptance is [`ButtonMask::PRIMARY`] — left-click only.
633    /// Use [`accept_tap_buttons`](Self::accept_tap_buttons) to widen
634    /// the set if you need right-click, middle-click, or auxiliary
635    /// buttons to fire this handler.
636    pub fn on_tap(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
637        self.handlers.on_tap = Some(Box::new(f));
638        self
639    }
640
641    /// Set the on_double_tap handler. See [`on_tap`](Self::on_tap) for
642    /// the callback contract.
643    pub fn on_double_tap(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
644        self.handlers.on_double_tap = Some(Box::new(f));
645        self
646    }
647
648    /// Set the on_triple_tap handler — fires on the third click within the
649    /// recognizer's window (the same `multi_tap_interval` / `multi_tap_slop`
650    /// the pointer's gesture profile gives double tap).
651    /// Runs independently of `on_double_tap` via cooperative gesture
652    /// recognizers (`GestureRecognizer::resets_on_peer_recognition`).
653    pub fn on_triple_tap(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
654        self.handlers.on_triple_tap = Some(Box::new(f));
655        self
656    }
657
658    /// Set the on_long_press handler. The callback receives a borrowed
659    /// [`TapEvent`] whose modifiers are
660    /// captured from the held `Down` (since long-press recognises on a
661    /// timer before any `Up`).
662    pub fn on_long_press(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
663        self.handlers.on_long_press = Some(Box::new(f));
664        self
665    }
666
667    /// Restrict (or extend) the set of pointer buttons that fire
668    /// [`on_tap`](Self::on_tap). Default is [`ButtonMask::PRIMARY`]
669    /// (left-click only). Pass `ButtonMask::ALL` or
670    /// `ButtonMask::PRIMARY | ButtonMask::SECONDARY`, etc.
671    ///
672    /// The mask belongs to the `on_tap` set beside it. A node carries two
673    /// handler sets, the widget's own (from its `build`) and the one this
674    /// builder chain attaches, and each fires only on the buttons in its own
675    /// mask: widening the app's `on_tap` to the secondary button does not make
676    /// a right click run the widget's built-in click (a `Slider` jumping to
677    /// it, say). A mask set where no `on_tap` is attached beside it reaches
678    /// the widget's own instead, unless that one declared a mask of its own:
679    /// `Button::new(..).accept_tap_buttons(PRIMARY | SECONDARY)` is how a
680    /// right click activates a button.
681    pub fn accept_tap_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
682        self.handlers.tap_buttons = Some(mask.into());
683        self
684    }
685
686    /// Restrict (or extend) the set of pointer buttons that fire
687    /// [`on_double_tap`](Self::on_double_tap). Default
688    /// [`ButtonMask::PRIMARY`].
689    ///
690    /// Scoped to the handler set it is attached with, the way
691    /// [`accept_tap_buttons`](Self::accept_tap_buttons) is.
692    pub fn accept_double_tap_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
693        self.handlers.double_tap_buttons = Some(mask.into());
694        self
695    }
696
697    /// Restrict (or extend) the set of pointer buttons that fire
698    /// [`on_triple_tap`](Self::on_triple_tap). Default
699    /// [`ButtonMask::PRIMARY`].
700    ///
701    /// Scoped to the handler set it is attached with, the way
702    /// [`accept_tap_buttons`](Self::accept_tap_buttons) is.
703    pub fn accept_triple_tap_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
704        self.handlers.triple_tap_buttons = Some(mask.into());
705        self
706    }
707
708    /// Restrict (or extend) the set of pointer buttons that fire
709    /// [`on_long_press`](Self::on_long_press). Default
710    /// [`ButtonMask::PRIMARY`].
711    ///
712    /// Scoped to the handler set it is attached with, the way
713    /// [`accept_tap_buttons`](Self::accept_tap_buttons) is.
714    pub fn accept_long_press_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
715        self.handlers.long_press_buttons = Some(mask.into());
716        self
717    }
718
719    /// Set the on_hover handler.
720    pub fn on_hover(mut self, f: impl FnMut(bool, &mut EventContext) + 'static) -> Self {
721        self.handlers.on_hover = Some(Box::new(f));
722        self
723    }
724
725    /// Set the on_key handler.
726    pub fn on_key(
727        mut self,
728        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
729    ) -> Self {
730        self.handlers.on_key = Some(Box::new(f));
731        self
732    }
733
734    /// Set the strict-ancestor key preview handler. Fires on every
735    /// ancestor of the focused widget (root → parent-of-target)
736    /// before the focused widget's `on_key` runs. Return
737    /// `EventResponse::Handled` to consume the event.
738    pub fn on_key_preview(
739        mut self,
740        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
741    ) -> Self {
742        self.handlers.on_key_preview = Some(Box::new(f));
743        self
744    }
745
746    /// Set the on_drag handler (gesture-based drag). The closure receives
747    /// a [`DragPhase`] — `Started`, then zero or more `Moved`, then
748    /// `Ended`.
749    pub fn on_drag(mut self, f: impl FnMut(DragPhase, &mut EventContext) + 'static) -> Self {
750        self.handlers.on_drag = Some(Box::new(f));
751        self
752    }
753
754    /// Set the on_swipe handler. Fires once per swipe with the direction
755    /// and velocity (pixels/second).
756    pub fn on_swipe(
757        mut self,
758        f: impl FnMut(SwipeDirection, f32, &mut EventContext) + 'static,
759    ) -> Self {
760        self.handlers.on_swipe = Some(Box::new(f));
761        self
762    }
763
764    /// Set the on_pinch handler. On desktop the phases are produced from
765    /// OS trackpad gestures (winit `PinchGesture` / `RotationGesture`).
766    pub fn on_pinch(mut self, f: impl FnMut(PinchPhase, &mut EventContext) + 'static) -> Self {
767        self.handlers.on_pinch = Some(Box::new(f));
768        self
769    }
770
771    /// Set the on_focus handler. `f` is called with `true` on focus gain and
772    /// `false` on focus loss.
773    ///
774    /// **WCAG 3.2.1 (On Focus).** Use this only to update *local* visual or
775    /// reactive state. Do NOT open a window, navigate, submit, or otherwise
776    /// change context from here: a context change triggered merely by a control
777    /// receiving focus is a Success Criterion 3.2.1 failure — keyboard users
778    /// tabbing through the UI would trigger it unexpectedly. (A debug-only guard
779    /// warns if `ctx.open_window(...)` / `ctx.focus_window(...)` is called from
780    /// inside focus dispatch.)
781    pub fn on_focus(mut self, f: impl FnMut(bool, &mut EventContext) + 'static) -> Self {
782        self.handlers.on_focus = Some(Box::new(f));
783        self
784    }
785
786    /// Commit the edit this widget holds, as losing focus would, while it
787    /// keeps focus. For a field that commits on blur (a spin box, a date
788    /// field, a text field with a validator). It is not a blur: a callback
789    /// the app asked to run on blur waits for focus to leave.
790    ///
791    /// A click on a control built with
792    /// [`focus_on_click(false)`](Self::focus_on_click) (a toolbar button, a
793    /// menu bar title) blurs nothing, so a value typed and not yet committed
794    /// would be missed by the command it runs. When that click completes,
795    /// before the release that activates the control is dispatched (a press
796    /// dragged off the control or cancelled commits nothing), and before an
797    /// assistive technology's Click on it, the
798    /// framework calls this on the focused widget and on each of its
799    /// ancestors that set one, innermost first: AppKit's `commitEditing`,
800    /// and the fix for Qt's `NoFocus` tool button pitfall. The ancestors are
801    /// those an intent sent from the focused widget walks through: from a
802    /// popover on to the widget it is attached to and that widget's
803    /// ancestors, and no further than an open modal.
804    #[doc(hidden)]
805    pub fn on_commit_edit(mut self, f: impl FnMut(&mut EventContext) + 'static) -> Self {
806        self.handlers.on_commit_edit = Some(Box::new(f));
807        self
808    }
809
810    /// Set the on_pointer_event handler (low-level escape hatch).
811    pub fn on_pointer_event(
812        mut self,
813        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
814    ) -> Self {
815        self.handlers.on_pointer_event = Some(Box::new(f));
816        self
817    }
818
819    /// Set the on_scroll handler.
820    pub fn on_scroll(
821        mut self,
822        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
823    ) -> Self {
824        self.handlers.on_scroll = Some(Box::new(f));
825        self
826    }
827
828    /// Set the on_access_action handler.
829    pub fn on_access_action(
830        mut self,
831        f: impl FnMut(accesskit::Action, &mut EventContext) -> EventResponse + 'static,
832    ) -> Self {
833        self.handlers.on_access_action = Some(Box::new(f));
834        self
835    }
836
837    /// Set the full AccessKit action-request handler. Receives the
838    /// action, target NodeId (may be a synthetic widget-emitted
839    /// child), and optional `ActionData` payload (e.g.
840    /// `SetTextSelection(TextSelection)` or `Value(Box<str>)`).
841    ///
842    /// Layered with [`on_access_action`](Self::on_access_action) rather than
843    /// replacing it: both fire for the same dispatched action, and it counts as
844    /// handled if either says so.
845    pub fn on_access_action_request(
846        mut self,
847        f: impl FnMut(
848            accesskit::Action,
849            accesskit::NodeId,
850            Option<accesskit::ActionData>,
851            &mut EventContext,
852        ) -> EventResponse
853        + 'static,
854    ) -> Self {
855        self.handlers.on_access_action_request = Some(Box::new(f));
856        self
857    }
858
859    /// Set the focusable flag.
860    pub fn focusable(mut self, focusable: bool) -> Self {
861        self.focusable = Some(focusable);
862        self
863    }
864
865    /// Set the cursor icon.
866    pub fn cursor(mut self, cursor: CursorIcon) -> Self {
867        self.cursor = Some(cursor);
868        self
869    }
870
871    /// Set the clips_children flag.
872    pub fn clips_children(mut self, clips: bool) -> Self {
873        self.clips_children = Some(clips);
874        self
875    }
876
877    /// Declare this node a text-input surface, enabling the OS input method
878    /// (with `ctx`'s purpose) while it is focused. Leaving it unset (the
879    /// default) means no OS IME. The platform reads the focused node's
880    /// descriptor at focus-change time. See [`crate::ime`].
881    pub fn ime_input(mut self, ctx: crate::ime::ImeContext) -> Self {
882        self.ime = Some(ctx);
883        self
884    }
885
886    /// Make the widget invisible to pointer hit-testing. With
887    /// `pass_through = true`, pointer events traverse this node as if
888    /// it were not there — useful for purely decorative overlays that
889    /// must not absorb clicks (the debug inspector's `HighlightLayer`
890    /// and `HoverProbe` use this).
891    pub fn event_pass_through(mut self, pass_through: bool) -> Self {
892        self.event_pass_through = Some(pass_through);
893        self
894    }
895
896    /// Mark this widget's subtree a **gesture dead zone**: a pointer press
897    /// inside it must not arm a drag/swipe recognizer on any ancestor above
898    /// it. Use to let interactive controls (buttons, a `⋮` menu) sit inside a
899    /// draggable / swipeable container (a dock-panel header, a card, a list
900    /// row) without a few px of click jitter starting the ancestor's drag.
901    /// The container's own drag still works everywhere else. Honored by
902    /// `PointerSequence` member enrolment; see the `DeadZone` wrapper widget.
903    pub fn gesture_dead_zone(mut self, dead_zone: bool) -> Self {
904        self.gesture_dead_zone = Some(dead_zone);
905        self
906    }
907
908    /// Mark this widget part of the window's own chrome: a title bar's drag
909    /// surface, a window button, a resize edge. While a modal is open in the
910    /// window it stays live under the modal's scrim, as the window system's
911    /// frame stays live above a dialog: a press on it goes to it, even one on
912    /// app content it holds, and one on a gesture dead zone inside it goes
913    /// nowhere, so the app's controls in a title bar stay blocked.
914    ///
915    /// For the title bar's own widgets; an app's widget is never chrome.
916    #[doc(hidden)]
917    pub fn window_chrome(mut self) -> Self {
918        self.window_chrome = Some(true);
919        self
920    }
921
922    /// Mark this widget's subtree **scroll chrome**: a scroll bar's track and
923    /// thumb, a tab strip's scroll arrows. A click on it moves a view and runs
924    /// no command, so, although it takes no focus, it does not have the
925    /// focused text field commit its edit or end its input method's
926    /// composition first, as a click on another control that takes no focus
927    /// does (see [`focus_on_click`](Self::focus_on_click)). Scrolling a
928    /// neighbouring pane leaves the field as it is.
929    ///
930    /// For the stock widgets' scroll chrome.
931    #[doc(hidden)]
932    pub fn scroll_chrome(mut self) -> Self {
933        self.scroll_chrome = Some(true);
934        self
935    }
936
937    /// Whether a click or a tap moves keyboard focus to this widget. Default
938    /// `true`.
939    ///
940    /// With `false`, a click of any pointer (mouse, finger, pen) that would
941    /// focus this widget (one on it, or on a part of it that is not itself
942    /// focusable) activates what it lands on as usual and leaves focus where
943    /// it was: focus moves neither to the widget nor to a focusable ancestor.
944    /// The flag belongs to this node only, as GTK's is: a focusable widget
945    /// inside it (a text field in a toolbar) still takes focus from a click
946    /// unless built with the flag itself. A composite whose focusable node is
947    /// one of its own children passes the flag on to it; `PopoverButton` and
948    /// `PopoverIconButton` do. The widget stays focusable: Tab reaches it, a
949    /// focus request lands on it, and an assistive technology's focus action
950    /// moves to it. This is GTK's `focus-on-click` property and Qt's
951    /// `Qt::TabFocus` policy.
952    ///
953    /// For a control that acts on another widget's content, so that a click
954    /// on it must not take the focus that content holds: a menu bar's
955    /// triggers, a toolbar's buttons. Ordinary push buttons and form controls
956    /// keep the default, as on every desktop. Since such a click blurs
957    /// nothing, the focused widget first commits the edit it holds (a spin
958    /// box's typed number, a field's validator), so the command the
959    /// click runs sees it.
960    ///
961    /// Only the framework's own focus move is affected. A widget that asks for
962    /// focus in its own press handler (a text field placing its caret) still
963    /// gets it.
964    pub fn focus_on_click(mut self, focus_on_click: bool) -> Self {
965        self.focus_on_click = Some(focus_on_click);
966        self
967    }
968
969    /// Select what a **hold** on this widget's subtree means, for the
970    /// tree-owned long-press route.
971    ///
972    /// Only ever consulted for a pointer that cannot hover, and only where the
973    /// widget installs no `on_long_press` of its own — that always wins. See
974    /// [`crate::widget_tree::touch_route`] for the precedence and for what each
975    /// variant selects.
976    pub fn long_press_role(mut self, role: crate::widget_tree::touch_route::LongPressRole) -> Self {
977        self.long_press_role = Some(role);
978        self
979    }
980
981    /// Override what a direct pointer (touch, pen) is permitted to do to
982    /// this widget's subtree — the CSS `touch-action` model. Intersected
983    /// with every ancestor's declaration on the way down; a mouse never
984    /// consults this. See [`super::arena::WidgetNode::touch_action`].
985    pub fn touch_action(mut self, action: TouchAction) -> Self {
986        self.touch_action = Some(action);
987        self
988    }
989
990    /// Declare this widget a **pan surface** on `axes` for direct pointers,
991    /// with kinetic (fling/settle) hand-off on release. Sugar for
992    /// `.pan_claim(PanClaim { axes, devices: PointerKindMask::DIRECT, kinetic: true })`
993    /// — the shape every scrollable declares. See
994    /// [`super::arena::WidgetNode::pan_claim`].
995    pub fn scroll_container(mut self, axes: PanAxes) -> Self {
996        self.pan_claim = Some(PanClaim {
997            axes,
998            devices: teksilo_tokens::PointerKindMask::DIRECT,
999            kinetic: true,
1000        });
1001        self
1002    }
1003
1004    /// Declare this widget a pan surface with an explicit [`PanClaim`] —
1005    /// the escape hatch behind [`scroll_container`](Self::scroll_container)
1006    /// for a claim that isn't kinetic, or that widens/narrows the device
1007    /// mask. See [`super::arena::WidgetNode::pan_claim`].
1008    pub fn pan_claim(mut self, claim: PanClaim) -> Self {
1009        self.pan_claim = Some(claim);
1010        self
1011    }
1012
1013    /// Whether this widget absorbs a scroll it cannot use
1014    /// ([`Contain`](crate::OverscrollBehavior::Contain)) or lets it chain
1015    /// outward at its boundary ([`Chain`](crate::OverscrollBehavior::Chain),
1016    /// the default). The CSS `overscroll-behavior` model, read by the pan
1017    /// claimant chain. See [`super::arena::WidgetNode::overscroll_behavior`].
1018    pub fn overscroll_behavior(mut self, behavior: crate::OverscrollBehavior) -> Self {
1019        self.overscroll_behavior = Some(behavior);
1020        self
1021    }
1022
1023    /// When a drag on this widget may begin relative to the press that starts
1024    /// it.
1025    ///
1026    /// [`DragActivation::Auto`](teksilo_tokens::DragActivation::Auto) — the
1027    /// default — is `Immediate` for a precise pointer, which is exactly
1028    /// today's behaviour, and `AfterLongPress` for a coarse pointer whose axis
1029    /// a pan surface has already claimed. Declare
1030    /// [`Immediate`](teksilo_tokens::DragActivation::Immediate) for a control
1031    /// whose drag *is* the interaction (a slider thumb, a splitter handle) and
1032    /// [`AfterLongPress`](teksilo_tokens::DragActivation::AfterLongPress) for
1033    /// one that must not steal a scroll (a reorderable list row). See
1034    /// [`super::arena::WidgetNode::drag_activation`].
1035    pub fn drag_activation(mut self, activation: teksilo_tokens::DragActivation) -> Self {
1036        self.drag_activation = Some(activation);
1037        self
1038    }
1039
1040    /// How many simultaneous contacts this node serves. Default
1041    /// [`MultiContact::First`]. See
1042    /// [`super::arena::WidgetNode::multi_contact`].
1043    pub fn multi_contact(mut self, policy: MultiContact) -> Self {
1044        self.multi_contact = Some(policy);
1045        self
1046    }
1047
1048    /// Mark this widget a **keyboard capture** surface: while it holds
1049    /// focus, every `KeyDown` is delivered straight to its `on_key`
1050    /// handler, bypassing shortcut → intent → action resolution. Use for
1051    /// a terminal emulator that must forward `Ctrl+C` / `Ctrl+W` /
1052    /// `Alt+<letter>` to a child process instead of triggering the host
1053    /// app's shortcuts, a game viewport, or a modal text surface.
1054    ///
1055    /// # The escape contract
1056    ///
1057    /// **`Ctrl+Tab` / `Ctrl+Shift+Tab` are reserved and always move focus
1058    /// out.** The dispatcher cycles focus on that chord before the capture
1059    /// node is consulted, so a capture surface cannot become a keyboard trap
1060    /// (WCAG 2.1.2) however greedily its `on_key` behaves. Do not bind them.
1061    ///
1062    /// Nothing else is reserved. In particular Escape is **not**: overlay
1063    /// back-navigation runs first only while an overlay is actually open, so
1064    /// a focused capture surface with no overlay above it does receive
1065    /// Escape and may consume it. See
1066    /// [`super::arena::WidgetNode::keyboard_capture`].
1067    pub fn keyboard_capture(mut self, capture: bool) -> Self {
1068        self.keyboard_capture = Some(capture);
1069        self
1070    }
1071
1072    /// Mark this widget a **focus region** named `name`: one of the large
1073    /// parts of a window (a side panel, the centre, a bottom panel, a status
1074    /// bar) that F6 and Shift+F6 move focus between, announcing the name.
1075    ///
1076    /// Not a traversal scope
1077    /// ([`TraversalScopePolicy`](crate::focus::TraversalScopePolicy)): a
1078    /// traversal scope decides the Tab order inside a subtree, a focus region
1079    /// is a stop F6 moves to. F6 lands on the focus the region had last, else on its first
1080    /// Tab stop; a region with no Tab stop of its own is skipped, and a
1081    /// region nested in another is a stop of its own. Mark the node a screen
1082    /// reader lists as a landmark (`Role::Complementary`, `Role::Main`, ...),
1083    /// with the same words, so the landmarks and the F6 stops are one set.
1084    ///
1085    /// The two commands are the shortcuts `focus.next_region` (F6) and
1086    /// `focus.previous_region` (Shift+F6), which every tree registers and a
1087    /// user can rebind; an `Action` for either on the route runs instead of
1088    /// the built-in cycling. In a window with no region they step aside, and
1089    /// F6 reaches the focused widget as before. A keyboard capture surface
1090    /// (a terminal) keeps F6; Ctrl+Tab leaves it.
1091    pub fn focus_region(mut self, name: impl Into<Prop<String>>) -> Self {
1092        self.focus_region = Some(name.into());
1093        self
1094    }
1095
1096    /// Make this widget AND its whole subtree invisible to pointer
1097    /// hit-testing. Stronger than [`event_pass_through`](Self::event_pass_through):
1098    /// that one keeps descendants hittable, this one excludes them too.
1099    /// For purely decorative composite overlays (a count badge over a
1100    /// button, a watermark) whose own children would otherwise swallow
1101    /// the click meant for the control underneath.
1102    pub fn hit_transparent(mut self, transparent: bool) -> Self {
1103        self.hit_transparent = Some(transparent);
1104        self
1105    }
1106
1107    /// Override how far a *missed* press may be re-attributed to this node, and
1108    /// up to what size it is topped up.
1109    ///
1110    /// Second link of the precedence chain: `no_hit_slop` beats this, this
1111    /// beats the widget's own `Widget::hit_slop`, and that beats the density
1112    /// default. Use it for a control the framework cannot recognise as small —
1113    /// a hand-drawn handle, a custom mark in a chart — or to raise `up_to`
1114    /// beyond the density's `target_size` for one especially fiddly target.
1115    ///
1116    /// Hit-only: no layout moves and nothing repaints differently.
1117    pub fn hit_slop(mut self, slop: crate::pointer::hit_slop::HitSlop) -> Self {
1118        self.hit_slop = Some(slop);
1119        self
1120    }
1121
1122    /// Take this node out of **both** hit-widening mechanisms: it earns no slop
1123    /// outset, and its `Widget::hit_outset` is ignored.
1124    ///
1125    /// Head of the precedence chain, and the right switch for a node whose
1126    /// exact rectangle is the contract — a surface hosting foreign content
1127    /// (a `WebView`, an embedded engine) that must receive precisely the
1128    /// presses that land on it and no others, or a modal scrim, which must
1129    /// never re-attribute a press to something under it.
1130    ///
1131    /// Per-node, not per-subtree: descendants may still widen. To take a whole
1132    /// subtree out of hit-testing use
1133    /// [`hit_transparent`](Self::hit_transparent).
1134    pub fn no_hit_slop(mut self) -> Self {
1135        self.no_hit_slop = Some(true);
1136        self
1137    }
1138
1139    /// Bind a user-owned `Signal<bool>` that the framework will set
1140    /// to `true` whenever the focused widget is a *strict descendant*
1141    /// of this node, and `false` otherwise. Useful for unified focus
1142    /// halos around composite widgets (a chat composer that highlights
1143    /// when its `RichTextEditor` or "Send" button is focused, a
1144    /// `Panel` wrapping a `SpinBox`, etc).
1145    ///
1146    /// Strict-ancestors only — a widget that *is* itself focused does
1147    /// not also see its own `focus_within` signal flipped to `true`.
1148    /// Combine with `on_focus` if you want both behaviours.
1149    pub fn focus_within(mut self, signal: crate::signal::Signal<bool>) -> Self {
1150        self.focus_within = Some(signal);
1151        self
1152    }
1153
1154    /// Bind a user-owned `Signal<bool>` that the framework will set
1155    /// to `true` whenever the hovered widget is a *strict descendant*
1156    /// of this node. Symmetric to [`focus_within`](Self::focus_within).
1157    pub fn hover_within(mut self, signal: crate::signal::Signal<bool>) -> Self {
1158        self.hover_within = Some(signal);
1159        self
1160    }
1161
1162    /// Bind this node's visibility to a `bool` / `Signal<bool>` / `Prop<bool>`.
1163    /// A bound value shows/hides the node reactively (registered at
1164    /// `Relayout`). Equivalent to `ctx.visible_when(id, ..)`; exposed as a
1165    /// builder method so `teksu!` can write `visible_when: sig` as a property.
1166    pub fn visible_when(mut self, state: impl Into<Prop<bool>>) -> Self {
1167        self.visible_when = Some(state.into());
1168        self
1169    }
1170
1171    /// Set a context-menu factory. See [`ContextMenuFactory`] for the
1172    /// full contract: the closure receives the click position
1173    /// (window-local) and a full [`EventContext`], and returns
1174    /// `Some(menu)` to mount or `None` to decline (falling through to
1175    /// the nearest ancestor with a factory).
1176    pub fn context_menu(
1177        mut self,
1178        factory: impl Fn(Point, &mut EventContext) -> Option<Box<dyn Widget>> + 'static,
1179    ) -> Self {
1180        self.context_menu_factory = Some(Box::new(factory));
1181        self
1182    }
1183
1184    /// Add a section to the context menu opened at or below this node, rather
1185    /// than a menu of its own. See [`ContextMenuContribution`] for when it is
1186    /// asked and where the section goes.
1187    pub fn context_menu_contribution(
1188        mut self,
1189        contribution: impl Fn(&mut EventContext) -> Option<Box<dyn Widget>> + 'static,
1190    ) -> Self {
1191        self.context_menu_contribution = Some(Box::new(contribution));
1192        self
1193    }
1194
1195    /// Set the drag hover handler. Called when a drag payload hovers over this widget.
1196    /// Return `DropFeedback` to indicate acceptance and visual feedback.
1197    pub fn on_drag_hover(
1198        mut self,
1199        f: impl FnMut(
1200            &crate::drag_payload::DragPayload,
1201            teksilo_canvas::Point,
1202            &mut EventContext,
1203        ) -> crate::drag_state::DropFeedback
1204        + 'static,
1205    ) -> Self {
1206        self.handlers.on_drag_hover = Some(Box::new(f));
1207        self
1208    }
1209
1210    /// Set the drag-leave handler. Fires when a drag that was over this
1211    /// widget moves to another target, completes (drop on any target), or
1212    /// is cancelled. Widgets that stash transient feedback state in
1213    /// `on_drag_hover` must clear it here.
1214    pub fn on_drag_leave(mut self, f: impl FnMut(&mut EventContext) + 'static) -> Self {
1215        self.handlers.on_drag_leave = Some(Box::new(f));
1216        self
1217    }
1218
1219    /// Set the pointer-cancel handler. Fires when a pointer interaction on
1220    /// this widget is taken away — the window lost focus, a modal opened, the
1221    /// subtree was parked, a peer won the arbitration.
1222    ///
1223    /// **Terminal**: no `PointerUp` follows. Release anything the press
1224    /// latched; the framework releases its own state but never widget-owned
1225    /// state.
1226    pub fn on_pointer_cancel(
1227        mut self,
1228        f: impl FnMut(&crate::pointer::PointerInfo, crate::pointer::CancelReason, &mut EventContext)
1229        + 'static,
1230    ) -> Self {
1231        self.handlers.on_pointer_cancel = Some(Box::new(f));
1232        self
1233    }
1234
1235    /// Set the per-frame drag-tick handler. Fires once per frame while a
1236    /// drag is active and this widget is the current drop target. The
1237    /// closure receives the current pointer position in widget-local
1238    /// coordinates. Use for behaviours that must keep running even when
1239    /// the pointer is stationary — viewport-edge auto-scroll and
1240    /// spring-loaded folders.
1241    pub fn on_drag_tick(
1242        mut self,
1243        f: impl FnMut(teksilo_canvas::Point, &mut EventContext) + 'static,
1244    ) -> Self {
1245        self.handlers.on_drag_tick = Some(Box::new(f));
1246        self
1247    }
1248
1249    /// Set the drop handler. Called when a payload is dropped on this widget.
1250    /// Return `true` if the drop was accepted.
1251    pub fn on_drop(
1252        mut self,
1253        f: impl FnMut(
1254            crate::drag_payload::DragPayload,
1255            teksilo_canvas::Point,
1256            &mut EventContext,
1257        ) -> bool
1258        + 'static,
1259    ) -> Self {
1260        self.handlers.on_drop = Some(Box::new(f));
1261        self
1262    }
1263
1264    /// Set the drag-ended handler on a drag **source**. Fires when a drag
1265    /// this widget started ends — dropped on an in-app target, exported to
1266    /// another application via the OS (copy / move), or cancelled. Use it to
1267    /// react to the outcome, e.g. remove the dragged item on a
1268    /// [`DropOutcome::OsMove`](crate::drag_payload::DropOutcome::OsMove).
1269    pub fn on_drag_ended(
1270        mut self,
1271        f: impl FnMut(crate::drag_payload::DropOutcome, &mut EventContext) + 'static,
1272    ) -> Self {
1273        self.handlers.on_drag_ended = Some(Box::new(f));
1274        self
1275    }
1276
1277    /// Advertise a named custom action on this node and register its callback.
1278    ///
1279    /// The [`WidgetWithHandlers`] twin
1280    /// ([`access_custom_action`](WidgetWithHandlers::access_custom_action)) is
1281    /// how an *application* adds one from outside. This is how a **widget**
1282    /// adds one to a node it builds itself — a virtualized row, a rail item,
1283    /// a header cell — where there is no builder chain to hang it on because
1284    /// the node is reached through
1285    /// [`BuildContext::apply_handlers`](crate::build_context::BuildContext::apply_handlers).
1286    ///
1287    /// Actions are dispatched by declaration order, so a node's callbacks and
1288    /// its advertised list cannot drift apart.
1289    pub fn access_custom_action<F>(mut self, label: impl Into<Prop<String>>, handler: F) -> Self
1290    where
1291        F: FnMut(&mut EventContext) + 'static,
1292    {
1293        self.access_mut()
1294            .custom_actions
1295            .push((label.into(), Box::new(handler)));
1296        self
1297    }
1298
1299    /// Finish this node's accessibility with full access to its builder, after
1300    /// the widget's own `accessibility()` and every typed override.
1301    ///
1302    /// The [`WidgetWithHandlers`] twin
1303    /// ([`access_customize`](WidgetWithHandlers::access_customize)) is the
1304    /// application's; this one is for a **widget** writing onto a node it
1305    /// reaches through
1306    /// [`BuildContext::apply_handlers`](crate::build_context::BuildContext::apply_handlers),
1307    /// such as a wrapper handing its popup state to the control it wraps.
1308    /// Chained after any customization the node already carries.
1309    pub fn access_customize<F>(mut self, f: F) -> Self
1310    where
1311        F: Fn(&mut crate::accessibility::AccessNodeBuilder) + 'static,
1312    {
1313        self.access_mut().customize = Some(Box::new(f));
1314        self
1315    }
1316}
1317
1318impl Default for HandlerSet {
1319    fn default() -> Self {
1320        Self::new()
1321    }
1322}
1323
1324impl std::fmt::Debug for HandlerSet {
1325    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1326        f.debug_struct("HandlerSet")
1327            .field("handlers", &self.handlers)
1328            .field("focusable", &self.focusable)
1329            .field("tab_index", &self.tab_index)
1330            .field("cursor", &self.cursor)
1331            .finish()
1332    }
1333}
1334
1335// ---------------------------------------------------------------------------
1336// WidgetWithHandlers<W> — wrapper storing widget + accumulated handlers
1337// ---------------------------------------------------------------------------
1338
1339/// A widget wrapped with attached event handlers and framework metadata.
1340/// Created by calling builder methods from `WidgetBuilder` on any widget.
1341pub struct WidgetWithHandlers<W: Widget> {
1342    pub(crate) widget: W,
1343    pub(crate) handler_set: HandlerSet,
1344    /// Whether [`take_handler_set`](Self::take_handler_set) has run: the
1345    /// handlers (and cursor, focusability, ...) now live on the arena node,
1346    /// and `handler_set` is the empty default left behind. Only `Debug` reads
1347    /// it, so a mounted wrapper does not print an empty set as if it were the
1348    /// widget's declaration.
1349    handlers_taken: bool,
1350}
1351
1352impl<W: Widget> WidgetWithHandlers<W> {
1353    fn new(widget: W) -> Self {
1354        Self {
1355            widget,
1356            handler_set: HandlerSet::new(),
1357            handlers_taken: false,
1358        }
1359    }
1360
1361    /// Take the handler set out, leaving defaults.
1362    pub(crate) fn take_handler_set(&mut self) -> HandlerSet {
1363        self.handlers_taken = true;
1364        std::mem::take(&mut self.handler_set)
1365    }
1366
1367    // -- Gesture handlers --
1368
1369    pub fn on_tap(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
1370        self.handler_set.handlers.on_tap = Some(Box::new(f));
1371        self
1372    }
1373
1374    pub fn on_double_tap(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
1375        self.handler_set.handlers.on_double_tap = Some(Box::new(f));
1376        self
1377    }
1378
1379    pub fn on_triple_tap(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
1380        self.handler_set.handlers.on_triple_tap = Some(Box::new(f));
1381        self
1382    }
1383
1384    pub fn on_long_press(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
1385        self.handler_set.handlers.on_long_press = Some(Box::new(f));
1386        self
1387    }
1388
1389    /// Restrict (or extend) the set of pointer buttons that fire
1390    /// `on_tap`. Default is [`ButtonMask::PRIMARY`].
1391    ///
1392    /// The mask belongs to the `on_tap` set beside it. A node carries two
1393    /// handler sets, the widget's own (from its `build`) and the one this
1394    /// builder chain attaches, and each fires only on the buttons in its own
1395    /// mask: widening the app's `on_tap` to the secondary button does not make
1396    /// a right click run the widget's built-in click (a `Slider` jumping to
1397    /// it, say). A mask set where no `on_tap` is attached beside it reaches
1398    /// the widget's own instead, unless that one declared a mask of its own:
1399    /// `Button::new(..).accept_tap_buttons(PRIMARY | SECONDARY)` is how a
1400    /// right click activates a button.
1401    pub fn accept_tap_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
1402        self.handler_set.handlers.tap_buttons = Some(mask.into());
1403        self
1404    }
1405
1406    /// Restrict (or extend) the set of pointer buttons that fire
1407    /// `on_double_tap`. Default [`ButtonMask::PRIMARY`].
1408    ///
1409    /// Scoped to the handler set it is attached with, the way
1410    /// [`accept_tap_buttons`](Self::accept_tap_buttons) is.
1411    pub fn accept_double_tap_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
1412        self.handler_set.handlers.double_tap_buttons = Some(mask.into());
1413        self
1414    }
1415
1416    /// Restrict (or extend) the set of pointer buttons that fire
1417    /// `on_triple_tap`. Default [`ButtonMask::PRIMARY`].
1418    ///
1419    /// Scoped to the handler set it is attached with, the way
1420    /// [`accept_tap_buttons`](Self::accept_tap_buttons) is.
1421    pub fn accept_triple_tap_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
1422        self.handler_set.handlers.triple_tap_buttons = Some(mask.into());
1423        self
1424    }
1425
1426    /// Restrict (or extend) the set of pointer buttons that fire
1427    /// `on_long_press`. Default [`ButtonMask::PRIMARY`].
1428    ///
1429    /// Scoped to the handler set it is attached with, the way
1430    /// [`accept_tap_buttons`](Self::accept_tap_buttons) is.
1431    pub fn accept_long_press_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
1432        self.handler_set.handlers.long_press_buttons = Some(mask.into());
1433        self
1434    }
1435
1436    pub fn on_drag(mut self, f: impl FnMut(DragPhase, &mut EventContext) + 'static) -> Self {
1437        self.handler_set.handlers.on_drag = Some(Box::new(f));
1438        self
1439    }
1440
1441    pub fn on_swipe(
1442        mut self,
1443        f: impl FnMut(SwipeDirection, f32, &mut EventContext) + 'static,
1444    ) -> Self {
1445        self.handler_set.handlers.on_swipe = Some(Box::new(f));
1446        self
1447    }
1448
1449    pub fn on_pinch(mut self, f: impl FnMut(PinchPhase, &mut EventContext) + 'static) -> Self {
1450        self.handler_set.handlers.on_pinch = Some(Box::new(f));
1451        self
1452    }
1453
1454    // -- Focus and keyboard --
1455
1456    pub fn on_focus(mut self, f: impl FnMut(bool, &mut EventContext) + 'static) -> Self {
1457        self.handler_set.handlers.on_focus = Some(Box::new(f));
1458        self
1459    }
1460
1461    pub fn on_key(
1462        mut self,
1463        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
1464    ) -> Self {
1465        self.handler_set.handlers.on_key = Some(Box::new(f));
1466        self
1467    }
1468
1469    /// Set the strict-ancestor key preview handler. See
1470    /// [`HandlerSet::on_key_preview`].
1471    pub fn on_key_preview(
1472        mut self,
1473        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
1474    ) -> Self {
1475        self.handler_set.handlers.on_key_preview = Some(Box::new(f));
1476        self
1477    }
1478
1479    pub fn focusable(mut self, focusable: bool) -> Self {
1480        self.handler_set.focusable = Some(focusable);
1481        self
1482    }
1483
1484    pub fn tab_index(mut self, index: i32) -> Self {
1485        self.handler_set.tab_index = Some(index);
1486        self
1487    }
1488
1489    // -- Pointer (low-level escape hatch) --
1490
1491    pub fn on_pointer_event(
1492        mut self,
1493        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
1494    ) -> Self {
1495        self.handler_set.handlers.on_pointer_event = Some(Box::new(f));
1496        self
1497    }
1498
1499    pub fn on_hover(mut self, f: impl FnMut(bool, &mut EventContext) + 'static) -> Self {
1500        self.handler_set.handlers.on_hover = Some(Box::new(f));
1501        self
1502    }
1503
1504    pub fn cursor(mut self, cursor: CursorIcon) -> Self {
1505        self.handler_set.cursor = Some(cursor);
1506        self
1507    }
1508
1509    // -- Scroll --
1510
1511    pub fn on_scroll(
1512        mut self,
1513        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
1514    ) -> Self {
1515        self.handler_set.handlers.on_scroll = Some(Box::new(f));
1516        self
1517    }
1518
1519    // -- Accessibility actions --
1520
1521    pub fn on_access_action(
1522        mut self,
1523        f: impl FnMut(accesskit::Action, &mut EventContext) -> EventResponse + 'static,
1524    ) -> Self {
1525        self.handler_set.handlers.on_access_action = Some(Box::new(f));
1526        self
1527    }
1528
1529    pub fn on_access_action_request(
1530        mut self,
1531        f: impl FnMut(
1532            accesskit::Action,
1533            accesskit::NodeId,
1534            Option<accesskit::ActionData>,
1535            &mut EventContext,
1536        ) -> EventResponse
1537        + 'static,
1538    ) -> Self {
1539        self.handler_set.handlers.on_access_action_request = Some(Box::new(f));
1540        self
1541    }
1542
1543    // -- Framework-level properties --
1544
1545    pub fn clips_children(mut self, clips: bool) -> Self {
1546        self.handler_set.clips_children = Some(clips);
1547        self
1548    }
1549
1550    /// `WidgetBuilder::clips_children_on`'s inherent twin.
1551    ///
1552    /// The trait spells it `clips_children_on` because `clips_children` is
1553    /// already taken on `Widget` as a `&self` query. Without this twin the
1554    /// trait method applies to an already-wrapped widget and wraps it a second
1555    /// time; `Widget::take_handler_set` now merges through that, so it is a
1556    /// wasted node rather than lost behaviour, but the twin avoids both.
1557    pub fn clips_children_on(self, clips: bool) -> Self {
1558        self.clips_children(clips)
1559    }
1560
1561    /// Declare this node a text-input surface, enabling the OS input method
1562    /// (with `ctx`'s purpose) while it is focused. See [`crate::ime`].
1563    pub fn ime_input(mut self, ctx: crate::ime::ImeContext) -> Self {
1564        self.handler_set.ime = Some(ctx);
1565        self
1566    }
1567
1568    /// Make the widget invisible to pointer hit-testing. See
1569    /// [`HandlerSet::event_pass_through`].
1570    pub fn event_pass_through(mut self, pass_through: bool) -> Self {
1571        self.handler_set.event_pass_through = Some(pass_through);
1572        self
1573    }
1574
1575    /// Mark this widget's subtree a gesture dead zone. See
1576    /// [`HandlerSet::gesture_dead_zone`].
1577    pub fn gesture_dead_zone(mut self, dead_zone: bool) -> Self {
1578        self.handler_set.gesture_dead_zone = Some(dead_zone);
1579        self
1580    }
1581
1582    /// Whether a click or a tap moves keyboard focus to this widget. See
1583    /// [`HandlerSet::focus_on_click`].
1584    pub fn focus_on_click(mut self, focus_on_click: bool) -> Self {
1585        self.handler_set.focus_on_click = Some(focus_on_click);
1586        self
1587    }
1588
1589    /// Select what a hold on this widget's subtree means. See
1590    /// [`HandlerSet::long_press_role`].
1591    pub fn long_press_role(mut self, role: crate::widget_tree::touch_route::LongPressRole) -> Self {
1592        self.handler_set.long_press_role = Some(role);
1593        self
1594    }
1595
1596    /// Override what a direct pointer may do to this widget's subtree. See
1597    /// [`HandlerSet::touch_action`].
1598    pub fn touch_action(mut self, action: TouchAction) -> Self {
1599        self.handler_set.touch_action = Some(action);
1600        self
1601    }
1602
1603    /// Declare this widget a pan surface on `axes`, kinetic, direct pointers
1604    /// only. See [`HandlerSet::scroll_container`].
1605    pub fn scroll_container(mut self, axes: PanAxes) -> Self {
1606        self.handler_set.pan_claim = Some(PanClaim {
1607            axes,
1608            devices: teksilo_tokens::PointerKindMask::DIRECT,
1609            kinetic: true,
1610        });
1611        self
1612    }
1613
1614    /// Declare this widget a pan surface with an explicit [`PanClaim`]. See
1615    /// [`HandlerSet::pan_claim`].
1616    pub fn pan_claim(mut self, claim: PanClaim) -> Self {
1617        self.handler_set.pan_claim = Some(claim);
1618        self
1619    }
1620
1621    /// Whether this widget absorbs a boundary scroll or chains it outward. See
1622    /// [`HandlerSet::overscroll_behavior`].
1623    pub fn overscroll_behavior(mut self, behavior: crate::OverscrollBehavior) -> Self {
1624        self.handler_set.overscroll_behavior = Some(behavior);
1625        self
1626    }
1627
1628    /// When a drag on this widget may begin. See
1629    /// [`HandlerSet::drag_activation`].
1630    pub fn drag_activation(mut self, activation: teksilo_tokens::DragActivation) -> Self {
1631        self.handler_set.drag_activation = Some(activation);
1632        self
1633    }
1634
1635    /// [`HandlerSet::multi_contact`].
1636    pub fn multi_contact(mut self, policy: MultiContact) -> Self {
1637        self.handler_set.multi_contact = Some(policy);
1638        self
1639    }
1640
1641    /// Mark this widget a keyboard capture surface: while focused, every
1642    /// `KeyDown` bypasses shortcut resolution and reaches its `on_key`
1643    /// handler (terminals, game viewports). See
1644    /// [`HandlerSet::keyboard_capture`].
1645    pub fn keyboard_capture(mut self, capture: bool) -> Self {
1646        self.handler_set.keyboard_capture = Some(capture);
1647        self
1648    }
1649
1650    /// Mark this widget a focus region, an F6 stop named `name`. See
1651    /// [`HandlerSet::focus_region`].
1652    pub fn focus_region(mut self, name: impl Into<Prop<String>>) -> Self {
1653        self.handler_set.focus_region = Some(name.into());
1654        self
1655    }
1656
1657    /// Make this widget and its whole subtree invisible to pointer
1658    /// hit-testing. See [`HandlerSet::hit_transparent`].
1659    pub fn hit_transparent(mut self, transparent: bool) -> Self {
1660        self.handler_set.hit_transparent = Some(transparent);
1661        self
1662    }
1663
1664    /// Override the miss-only hit slop for this node. See
1665    /// [`HandlerSet::hit_slop`].
1666    pub fn hit_slop(mut self, slop: crate::pointer::hit_slop::HitSlop) -> Self {
1667        self.handler_set.hit_slop = Some(slop);
1668        self
1669    }
1670
1671    /// Take this node out of both hit-widening mechanisms. See
1672    /// [`HandlerSet::no_hit_slop`].
1673    pub fn no_hit_slop(mut self) -> Self {
1674        self.handler_set.no_hit_slop = Some(true);
1675        self
1676    }
1677
1678    /// Set a context-menu factory. See
1679    /// [`HandlerSet::context_menu`] for the full contract.
1680    pub fn context_menu(
1681        mut self,
1682        factory: impl Fn(Point, &mut EventContext) -> Option<Box<dyn Widget>> + 'static,
1683    ) -> Self {
1684        self.handler_set.context_menu_factory = Some(Box::new(factory));
1685        self
1686    }
1687
1688    /// Add a section to the context menu opened at or below this node. See
1689    /// [`HandlerSet::context_menu_contribution`].
1690    pub fn context_menu_contribution(
1691        mut self,
1692        contribution: impl Fn(&mut EventContext) -> Option<Box<dyn Widget>> + 'static,
1693    ) -> Self {
1694        self.handler_set.context_menu_contribution = Some(Box::new(contribution));
1695        self
1696    }
1697
1698    /// Bind a `Signal<bool>` the framework writes when a strict
1699    /// descendant has focus. See [`HandlerSet::focus_within`].
1700    pub fn focus_within(mut self, signal: crate::signal::Signal<bool>) -> Self {
1701        self.handler_set.focus_within = Some(signal);
1702        self
1703    }
1704
1705    /// Bind a `Signal<bool>` the framework writes when a strict
1706    /// descendant is hovered. See [`HandlerSet::hover_within`].
1707    pub fn hover_within(mut self, signal: crate::signal::Signal<bool>) -> Self {
1708        self.handler_set.hover_within = Some(signal);
1709        self
1710    }
1711
1712    /// Bind this node's visibility. See [`HandlerSet::visible_when`].
1713    pub fn visible_when(mut self, state: impl Into<Prop<bool>>) -> Self {
1714        self.handler_set.visible_when = Some(state.into());
1715        self
1716    }
1717
1718    /// Set the drag hover handler. Called when a drag payload hovers over this widget.
1719    pub fn on_drag_hover(
1720        mut self,
1721        f: impl FnMut(
1722            &crate::drag_payload::DragPayload,
1723            teksilo_canvas::Point,
1724            &mut EventContext,
1725        ) -> crate::drag_state::DropFeedback
1726        + 'static,
1727    ) -> Self {
1728        self.handler_set.handlers.on_drag_hover = Some(Box::new(f));
1729        self
1730    }
1731
1732    /// Set the drag-leave handler. See [`HandlerSet::on_drag_leave`].
1733    pub fn on_drag_leave(mut self, f: impl FnMut(&mut EventContext) + 'static) -> Self {
1734        self.handler_set.handlers.on_drag_leave = Some(Box::new(f));
1735        self
1736    }
1737
1738    /// Set the pointer-cancel handler. See
1739    /// [`HandlerSet::on_pointer_cancel`].
1740    pub fn on_pointer_cancel(
1741        mut self,
1742        f: impl FnMut(&crate::pointer::PointerInfo, crate::pointer::CancelReason, &mut EventContext)
1743        + 'static,
1744    ) -> Self {
1745        self.handler_set.handlers.on_pointer_cancel = Some(Box::new(f));
1746        self
1747    }
1748
1749    /// Set the per-frame drag-tick handler. See [`HandlerSet::on_drag_tick`].
1750    pub fn on_drag_tick(
1751        mut self,
1752        f: impl FnMut(teksilo_canvas::Point, &mut EventContext) + 'static,
1753    ) -> Self {
1754        self.handler_set.handlers.on_drag_tick = Some(Box::new(f));
1755        self
1756    }
1757
1758    /// Set the drop handler. Called when a payload is dropped on this widget.
1759    pub fn on_drop(
1760        mut self,
1761        f: impl FnMut(
1762            crate::drag_payload::DragPayload,
1763            teksilo_canvas::Point,
1764            &mut EventContext,
1765        ) -> bool
1766        + 'static,
1767    ) -> Self {
1768        self.handler_set.handlers.on_drop = Some(Box::new(f));
1769        self
1770    }
1771
1772    /// Set the drag-ended handler on a drag source. See
1773    /// [`HandlerSet::on_drag_ended`].
1774    pub fn on_drag_ended(
1775        mut self,
1776        f: impl FnMut(crate::drag_payload::DropOutcome, &mut EventContext) + 'static,
1777    ) -> Self {
1778        self.handler_set.handlers.on_drag_ended = Some(Box::new(f));
1779        self
1780    }
1781
1782    // ── Accessibility overrides ────────────────────────────────────────
1783    //
1784    // The user-visible string methods take `impl Into<Prop<String>>` so
1785    // they stay reactive. With the `i18n` feature, `LocalizedString`
1786    // (produced by `tr!(...)`) provides `From<LocalizedString> for
1787    // Prop<String>`, which yields a locale-observing `Prop::Bound`, so
1788    // `.access_label(tr!(save()))` follows the locale. A bare `&str`
1789    // still converts (`impl From<&str> for Prop<String>` in `signal.rs`,
1790    // kept so call sites written against the older `impl Into<String>`
1791    // setters keep compiling), so routing untranslated literals through
1792    // `lit!(...)` (downstream crates) or the `_literal` twins (which
1793    // store `Prop::Static` — the only literal path reachable from within
1794    // `teksilo-core`) is a convention, not a compiler-enforced one. Do it
1795    // anyway: it is what makes an untranslated AT string greppable.
1796
1797    /// Override the accessibility label (`Node::label`) of this widget.
1798    /// Replaces whatever the inner widget emitted via `set_name`.
1799    ///
1800    /// Accepts any `impl Into<Prop<String>>`. With the `i18n` feature,
1801    /// `LocalizedString` (produced by `tr!(...)`)
1802    /// implements `From<LocalizedString> for Prop<String>`, so
1803    /// `.access_label(tr!(save()))` stays reactive — the announced
1804    /// value re-resolves on locale change (the accessibility tree
1805    /// re-walks via `sync_accessibility`).
1806    pub fn access_label(mut self, label: impl Into<Prop<String>>) -> Self {
1807        self.handler_set.access_mut().label = Some(label.into());
1808        self
1809    }
1810
1811    /// `#[doc(hidden)]` grep marker for explicitly-untranslated label
1812    /// strings — the same convention as the `lit!` macro downstream, which
1813    /// replaced the widgets' own `*_literal` constructors. Stores a
1814    /// `Prop::Static`. The distinct name makes untranslated call sites
1815    /// greppable as a one-pass audit, and it's the literal path
1816    /// reachable from within `teksilo-core` (where `lit!` isn't usable).
1817    #[doc(hidden)]
1818    pub fn access_label_literal(self, label: impl Into<String>) -> Self {
1819        self.access_label(Prop::Static(label.into()))
1820    }
1821
1822    /// Override the accessibility description (`Node::description`).
1823    /// Same conversion rules as `access_label`.
1824    pub fn access_description(mut self, description: impl Into<Prop<String>>) -> Self {
1825        self.handler_set.access_mut().description = Some(description.into());
1826        self
1827    }
1828
1829    #[doc(hidden)]
1830    pub fn access_description_literal(self, description: impl Into<String>) -> Self {
1831        self.access_description(Prop::Static(description.into()))
1832    }
1833
1834    /// Long-form context hint. Alias of `access_description` —
1835    /// AccessKit has no separate hint slot (SwiftUI's split is
1836    /// VoiceOver-specific). Provided for SwiftUI parity.
1837    pub fn access_hint(self, hint: impl Into<Prop<String>>) -> Self {
1838        self.access_description(hint)
1839    }
1840
1841    #[doc(hidden)]
1842    pub fn access_hint_literal(self, hint: impl Into<String>) -> Self {
1843        self.access_description(Prop::Static(hint.into()))
1844    }
1845
1846    /// Override the accessibility value (`Node::value`).
1847    /// Same conversion rules as `access_label`.
1848    pub fn access_value(mut self, value: impl Into<Prop<String>>) -> Self {
1849        self.handler_set.access_mut().value = Some(value.into());
1850        self
1851    }
1852
1853    #[doc(hidden)]
1854    pub fn access_value_literal(self, value: impl Into<String>) -> Self {
1855        self.access_value(Prop::Static(value.into()))
1856    }
1857
1858    /// Override the accessibility role.
1859    pub fn access_role(mut self, role: accesskit::Role) -> Self {
1860        self.handler_set.access_mut().role = Some(role);
1861        self
1862    }
1863
1864    /// Hide (or un-hide) this node, and everything under it, from assistive
1865    /// technologies. Accepts a plain `bool`, a `Signal<bool>`, or a
1866    /// `Prop<bool>`: a bound value makes the node appear/disappear from the
1867    /// AT tree reactively (the binding is registered at `AccessibilityOnly`,
1868    /// so the tree re-walks on change). `true` hides the whole subtree, as
1869    /// [`AccessNodeBuilder::set_hidden`](crate::accessibility::AccessNodeBuilder::set_hidden)
1870    /// explains; to drop only this node's own role and keep its content,
1871    /// use `access_role(Role::GenericContainer)` instead. `false` un-sets a
1872    /// hidden state the inner widget emitted on this node, and cannot reveal
1873    /// a node under a hidden ancestor.
1874    pub fn access_hidden(mut self, hidden: impl Into<Prop<bool>>) -> Self {
1875        self.handler_set.access_mut().hidden = Some(hidden.into());
1876        self
1877    }
1878
1879    /// Mark (or un-mark) this widget as disabled for AT. `false`
1880    /// clears both widget-emitted disabled state AND the framework's
1881    /// arena-driven disabled gate at
1882    /// `accessibility_emit_impl::build_accessibility_recursive`.
1883    pub fn access_disabled(mut self, disabled: bool) -> Self {
1884        self.handler_set.access_mut().disabled = Some(disabled);
1885        self
1886    }
1887
1888    /// Stable test/debug identifier (`Node::author_id`). Not
1889    /// user-visible — used by accessibility inspectors and UI tests.
1890    pub fn access_identifier(mut self, id: impl Into<String>) -> Self {
1891        self.handler_set.access_mut().identifier = Some(id.into());
1892        self
1893    }
1894
1895    /// Append a `controls` relationship. The target widget's NodeId
1896    /// is included in this node's `aria-controls`-equivalent list.
1897    pub fn access_controls(mut self, target: WidgetId) -> Self {
1898        self.handler_set.access_mut().controls.push(target);
1899        self
1900    }
1901
1902    /// Append a `described_by` relationship: `target`'s text describes this
1903    /// node, as `aria-describedby` does (WCAG 3.3.1 for a field and its error).
1904    ///
1905    /// AccessKit 0.25 passes the relation to no screen reader, so the tree
1906    /// also writes the target's text into this node's `description`, after
1907    /// any description of its own. That is what a reader says as focus
1908    /// arrives. The text follows the target, except around focus, where it
1909    /// would be said twice: while focus stays, and in the update focus leaves,
1910    /// a text that was not in the description waits (for the next arrival,
1911    /// or for the next update once focus has gone). On Windows a text a live
1912    /// region announces in the update focus arrives in is also left out of
1913    /// that arrival, since NVDA says the announcement; elsewhere the arrival
1914    /// keeps it, since Orca cuts an announcement to read the new focus.
1915    /// A message appearing is for a live region to say, the target itself or
1916    /// an announcement; the description is for coming back.
1917    pub fn access_described_by(mut self, target: WidgetId) -> Self {
1918        self.handler_set.access_mut().described_by.push(target);
1919        self
1920    }
1921
1922    /// Append a `labelled_by` relationship.
1923    pub fn access_labelled_by(mut self, target: WidgetId) -> Self {
1924        self.handler_set.access_mut().labelled_by.push(target);
1925        self
1926    }
1927
1928    /// Set the live-region politeness (`Node::live`).
1929    pub fn access_live(mut self, mode: accesskit::Live) -> Self {
1930        self.handler_set.access_mut().live = Some(mode);
1931        self
1932    }
1933
1934    /// Mark this node as the current item within its container
1935    /// (`aria-current`).
1936    pub fn access_current(mut self, current: accesskit::AriaCurrent) -> Self {
1937        self.handler_set.access_mut().aria_current = Some(current);
1938        self
1939    }
1940
1941    /// Pre-formatted shortcut announcement (e.g. `"Ctrl+S"`). Used for
1942    /// chords NOT routed through the `Shortcut` system — platform-native
1943    /// keys, app-internal hotkeys not exposed to user rebinding. For
1944    /// `Shortcut`-registered chords prefer
1945    /// [`access_shortcut_id`](Self::access_shortcut_id), which tracks
1946    /// rebinds automatically.
1947    pub fn access_shortcut_literal(mut self, shortcut: impl Into<String>) -> Self {
1948        self.handler_set.access_mut().shortcut = Some(shortcut.into());
1949        self
1950    }
1951
1952    /// Bind the announced shortcut to a registered `Shortcut` id (the
1953    /// same id you pass to `Shortcut::new("app.save")`). The
1954    /// accessibility tree walker resolves the current keystroke from
1955    /// `WidgetTree::shortcut_registry()` at AT-build time, formats it
1956    /// via `KeyStroke::Display` (`"Ctrl+S"`), and writes it to
1957    /// `Node::keyboard_shortcut`. Auto-refreshes on rebind.
1958    ///
1959    /// If the registry has no entry for `id` (no widget registered the
1960    /// shortcut yet), the announcement is omitted — same fallback as
1961    /// `MenuItem::for_shortcut(...)`.
1962    pub fn access_shortcut_id(mut self, id: impl Into<String>) -> Self {
1963        self.handler_set.access_mut().shortcut_id = Some(id.into());
1964        self
1965    }
1966
1967    /// Indicate that activating this widget pops up a menu / listbox /
1968    /// dialog (`aria-haspopup`).
1969    pub fn access_has_popup(mut self, kind: accesskit::HasPopup) -> Self {
1970        self.handler_set.access_mut().has_popup = Some(kind);
1971        self
1972    }
1973
1974    /// Override orientation (`Node::orientation`) — used on sliders,
1975    /// scrollbars, separators.
1976    pub fn access_orientation(mut self, orientation: accesskit::Orientation) -> Self {
1977        self.handler_set.access_mut().orientation = Some(orientation);
1978        self
1979    }
1980
1981    /// Prune all descendants from the accessibility tree. The widget's
1982    /// own AT node is still emitted; only children disappear. Use for
1983    /// purely decorative composites. Flutter's `excludeSemantics: true`.
1984    pub fn access_exclude_subtree(mut self) -> Self {
1985        self.handler_set.access_subtree = Some(AccessSubtreeMode::Exclude);
1986        self
1987    }
1988
1989    /// Lift descendants' labels / descriptions / values / actions into
1990    /// this widget's AT node, then prune the descendants. The whole
1991    /// composite reads as a single AT element. Flutter's
1992    /// `mergeAllDescendants: true` and SwiftUI's
1993    /// `.accessibilityElement(children: .combine)`.
1994    pub fn access_merge_subtree(mut self) -> Self {
1995        self.handler_set.access_subtree = Some(AccessSubtreeMode::Merge);
1996        self
1997    }
1998
1999    /// Set an explicit subtree mode.
2000    pub fn access_subtree(mut self, mode: AccessSubtreeMode) -> Self {
2001        self.handler_set.access_subtree = Some(mode);
2002        self
2003    }
2004
2005    /// Override `Node::numeric_value`.
2006    pub fn access_numeric_value(mut self, value: f64) -> Self {
2007        self.handler_set.access_mut().numeric_value = Some(value);
2008        self
2009    }
2010
2011    /// Override `Node::min_numeric_value` and `max_numeric_value`.
2012    pub fn access_numeric_range(mut self, min: f64, max: f64) -> Self {
2013        let access = self.handler_set.access_mut();
2014        access.min_numeric_value = Some(min);
2015        access.max_numeric_value = Some(max);
2016        self
2017    }
2018
2019    /// Override `Node::numeric_value_step`.
2020    pub fn access_numeric_step(mut self, step: f64) -> Self {
2021        self.handler_set.access_mut().numeric_step = Some(step);
2022        self
2023    }
2024
2025    /// Advertise an accessibility action and the callback that fires
2026    /// when AT software invokes it. Multiple `access_action` calls
2027    /// register separate callbacks for distinct actions; calling twice
2028    /// with the same action records both — they fire in order.
2029    pub fn access_action<F>(mut self, action: accesskit::Action, handler: F) -> Self
2030    where
2031        F: FnMut(&mut EventContext) + 'static,
2032    {
2033        self.handler_set
2034            .access_mut()
2035            .actions
2036            .push((action, Box::new(handler)));
2037        self
2038    }
2039
2040    /// Suppress an action the inner widget emitted (e.g. neutralize
2041    /// `Action::Click` on a Button used purely as a layout shim).
2042    /// Applied after the widget's `accessibility()` runs but before
2043    /// override-advertised actions, so a subsequent `access_action`
2044    /// for the same action re-advertises it with the override-installed
2045    /// callback.
2046    pub fn access_remove_action(mut self, action: accesskit::Action) -> Self {
2047        self.handler_set.access_mut().removed_actions.push(action);
2048        self
2049    }
2050
2051    /// Advertise a custom-named action (SwiftUI parity:
2052    /// `.accessibilityAction(named:_:)`). The label is exposed
2053    /// verbatim by AT software (e.g. VoiceOver's Actions rotor).
2054    /// Accepts `tr!(...)` via `From<LocalizedString> for Prop<String>`
2055    /// in `teksilo-i18n`, so the announced name follows the locale.
2056    pub fn access_custom_action<F>(mut self, label: impl Into<Prop<String>>, handler: F) -> Self
2057    where
2058        F: FnMut(&mut EventContext) + 'static,
2059    {
2060        self.handler_set
2061            .access_mut()
2062            .custom_actions
2063            .push((label.into(), Box::new(handler)));
2064        self
2065    }
2066
2067    #[doc(hidden)]
2068    pub fn access_custom_action_literal<F>(self, label: impl Into<String>, handler: F) -> Self
2069    where
2070        F: FnMut(&mut EventContext) + 'static,
2071    {
2072        self.access_custom_action(Prop::Static(label.into()), handler)
2073    }
2074
2075    /// Final escape hatch — invoked after all typed override setters,
2076    /// with full `&mut AccessNodeBuilder` access (including
2077    /// `inner_mut()`). Use for synthetic-child surgery (rich text
2078    /// paragraphs, text runs) or any AccessKit field the typed
2079    /// surface doesn't cover.
2080    pub fn access_customize<F>(mut self, f: F) -> Self
2081    where
2082        F: Fn(&mut crate::accessibility::AccessNodeBuilder) + 'static,
2083    {
2084        self.handler_set.access_mut().customize = Some(Box::new(f));
2085        self
2086    }
2087}
2088
2089// Delegate all Widget trait methods to the inner widget.
2090impl<W: Widget> std::fmt::Debug for WidgetWithHandlers<W> {
2091    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
2092        let mut s = f.debug_struct("WidgetWithHandlers");
2093        s.field("widget", &self.widget);
2094        if self.handlers_taken {
2095            // Mounting moved the set to the node; what is left is the empty
2096            // default, which would read as "no cursor, no handlers".
2097            s.field("handlers", &format_args!("<moved to the tree>"));
2098        } else {
2099            s.field("handler_set", &self.handler_set);
2100        }
2101        s.finish()
2102    }
2103}
2104
2105/// Every method of [`Widget`], forwarded to the wrapped widget.
2106///
2107/// `WidgetWithHandlers<W>` **replaces** `W` at `W`'s own arena node — the tree
2108/// never sees `W` again. So a hook this impl does not forward is not overridden,
2109/// it is gone: the trait's default answers in its place, for every widget any
2110/// builder method has ever touched. Nothing warns, nothing fails to compile, and
2111/// the widget goes on building, painting and hit-testing; only the behaviour
2112/// behind the dropped hook stops, at a call site nowhere near the `.on_tap(..)`
2113/// that silenced it. A test that drives the hook on a bare widget cannot see
2114/// this, which is why every hook is also driven through a builder method in the
2115/// `wrapper_forwarding_tests` module below, and why `missing_trait_methods` is
2116/// denied here: a method added to `Widget` must fail this impl's lint before it
2117/// can quietly fail a user's app.
2118///
2119/// The methods appear in the order the trait declares them, so the two can be
2120/// read side by side.
2121#[deny(clippy::missing_trait_methods)]
2122impl<W: Widget + 'static> Widget for WidgetWithHandlers<W> {
2123    /// Forwarded so the name is the widget's. The wrapper's own name identifies
2124    /// the wrapper, which is never what a census bucket or an inspector row is
2125    /// asking about.
2126    fn type_name(&self) -> &'static str {
2127        self.widget.type_name()
2128    }
2129
2130    fn build(
2131        &mut self,
2132        ctx: &mut crate::build_context::BuildContext,
2133    ) -> Vec<crate::widget_id::WidgetId> {
2134        self.widget.build(ctx)
2135    }
2136
2137    fn layout_response(
2138        &self,
2139        proposal: teksilo_canvas::SizeProposal,
2140        ctx: &crate::widget::LayoutContext,
2141    ) -> crate::widget::LayoutResponse {
2142        self.widget.layout_response(proposal, ctx)
2143    }
2144
2145    /// Forwarded because the opt-out exists to protect a widget whose
2146    /// `layout_response` is not idempotent. Answering the default here caches
2147    /// such a widget anyway, and a memoized non-idempotent measure is wrong in a
2148    /// way no layout assertion localizes.
2149    fn cacheable_layout(&self) -> bool {
2150        self.widget.cacheable_layout()
2151    }
2152
2153    fn place_children(
2154        &self,
2155        bounds: teksilo_canvas::Rect,
2156        proposal: teksilo_canvas::SizeProposal,
2157        children: &mut [crate::widget::WidgetPlacement],
2158        ctx: &crate::widget::LayoutContext,
2159    ) {
2160        self.widget.place_children(bounds, proposal, children, ctx)
2161    }
2162
2163    fn paint(
2164        &self,
2165        bounds: teksilo_canvas::Rect,
2166        canvas: &mut teksilo_canvas::Canvas,
2167        ctx: &crate::widget::PaintContext,
2168    ) {
2169        self.widget.paint(bounds, canvas, ctx)
2170    }
2171
2172    /// The two paint hooks and their `wants_*` gates are forwarded as pairs: the
2173    /// walker consults the gate and skips the hook when it answers `false`, so a
2174    /// forwarded hook behind an unforwarded gate never runs, and the widget
2175    /// reads as having no hook at all rather than as having lost one.
2176    fn wants_after_paint(&self) -> bool {
2177        self.widget.wants_after_paint()
2178    }
2179
2180    fn after_paint(
2181        &self,
2182        view: &crate::widget::WidgetTreeView<'_>,
2183        ctx: &crate::widget::PaintContext,
2184    ) {
2185        self.widget.after_paint(view, ctx)
2186    }
2187
2188    fn wants_post_paint(&self) -> bool {
2189        self.widget.wants_post_paint()
2190    }
2191
2192    fn post_paint(
2193        &self,
2194        bounds: teksilo_canvas::Rect,
2195        canvas: &mut teksilo_canvas::Canvas,
2196        ctx: &crate::widget::PaintContext,
2197    ) {
2198        self.widget.post_paint(bounds, canvas, ctx)
2199    }
2200
2201    fn accessibility(&self, builder: &mut crate::accessibility::AccessNodeBuilder) {
2202        self.widget.accessibility(builder)
2203    }
2204
2205    fn culls_children(&self) -> bool {
2206        self.widget.culls_children()
2207    }
2208
2209    /// Gate and hook again — see [`Widget::wants_after_paint`].
2210    fn wants_descendant_redirects(&self) -> bool {
2211        self.widget.wants_descendant_redirects()
2212    }
2213
2214    fn a11y_redirect_descendant(
2215        &self,
2216        self_id: crate::widget_id::WidgetId,
2217        descendant: crate::widget_id::WidgetId,
2218    ) -> Option<accesskit::NodeId> {
2219        self.widget.a11y_redirect_descendant(self_id, descendant)
2220    }
2221
2222    fn accessible_title_hint(&self) -> Option<String> {
2223        self.widget.accessible_title_hint()
2224    }
2225
2226    fn accessible_title_node(&self) -> Option<crate::widget_id::WidgetId> {
2227        self.widget.accessible_title_node()
2228    }
2229
2230    fn initial_focus_hint(&self) -> Option<crate::widget_id::WidgetId> {
2231        self.widget.initial_focus_hint()
2232    }
2233
2234    fn focus_region_landing(&self) -> Option<crate::widget_id::WidgetId> {
2235        self.widget.focus_region_landing()
2236    }
2237
2238    fn accessibility_proxy(&self) -> Option<crate::widget_id::WidgetId> {
2239        self.widget.accessibility_proxy()
2240    }
2241
2242    fn context_menu_key_target(&self) -> Option<crate::widget_id::WidgetId> {
2243        self.widget.context_menu_key_target()
2244    }
2245
2246    fn append_context_menu_section(
2247        &mut self,
2248        section: Box<dyn Widget>,
2249    ) -> Result<(), Box<dyn Widget>> {
2250        self.widget.append_context_menu_section(section)
2251    }
2252
2253    fn children(&self) -> Vec<crate::widget_id::WidgetId> {
2254        self.widget.children()
2255    }
2256
2257    fn accessibility_children(&self) -> Option<Vec<crate::widget_id::WidgetId>> {
2258        self.widget.accessibility_children()
2259    }
2260
2261    fn as_any(&self) -> Option<&dyn std::any::Any> {
2262        self.widget.as_any()
2263    }
2264
2265    /// Mutable counterpart of [`as_any`](Widget::as_any), forwarded for the
2266    /// same reason it is.
2267    ///
2268    /// A composing container that reads a child's concrete type must see the
2269    /// same widget whether or not a builder method wrapped it. `MenuList` reads
2270    /// a `MenuItem` this way for its mnemonic, its type-ahead label, its radio
2271    /// group and its safe-triangle state; without this forward,
2272    /// `MenuItem::new(..).context_menu(..)` silently stops being a `MenuItem`
2273    /// to its parent — no error, just a row that lost all four.
2274    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
2275        self.widget.as_any_mut()
2276    }
2277
2278    fn clips_children(&self) -> bool {
2279        self.handler_set
2280            .clips_children
2281            .unwrap_or_else(|| self.widget.clips_children())
2282    }
2283
2284    fn focus_reveal_rect(&self, bounds: teksilo_canvas::Rect) -> Option<teksilo_canvas::Rect> {
2285        self.widget.focus_reveal_rect(bounds)
2286    }
2287
2288    /// The hit-shape family, forwarded for the same reason `as_any` is: a
2289    /// widget must not stop being itself to the hit test because a builder
2290    /// method wrapped it.
2291    ///
2292    /// A round control that answers `hit_shape` and `hit_distance` loses both
2293    /// the moment someone writes `.on_tap(..)` on it if these are not
2294    /// forwarded — and loses them silently, which is the worst way to lose a
2295    /// hit test. The node-level `.hit_slop(..)` / `.no_hit_slop()` overrides
2296    /// need no forwarding: they ride the `HandlerSet` onto the node.
2297    fn hit_shape(&self, local_point: teksilo_canvas::Point, bounds: teksilo_canvas::Rect) -> bool {
2298        self.widget.hit_shape(local_point, bounds)
2299    }
2300
2301    fn accepts_child_hit(&self, child: WidgetId, point: teksilo_canvas::Point) -> bool {
2302        self.widget.accepts_child_hit(child, point)
2303    }
2304
2305    fn hit_outset(
2306        &self,
2307        kind: teksilo_tokens::PointerKind,
2308        tokens: &teksilo_tokens::InputTokens,
2309    ) -> teksilo_canvas::EdgeInsets {
2310        self.widget.hit_outset(kind, tokens)
2311    }
2312
2313    fn hit_slop(
2314        &self,
2315        kind: teksilo_tokens::PointerKind,
2316        tokens: &teksilo_tokens::InputTokens,
2317    ) -> Option<crate::pointer::hit_slop::HitSlop> {
2318        // Disambiguated: `WidgetBuilder::hit_slop(self, HitSlop)` — the
2319        // consuming builder method — shares this name, exactly as
2320        // `clips_children` does on both traits.
2321        Widget::hit_slop(&self.widget, kind, tokens)
2322    }
2323
2324    fn hit_distance(
2325        &self,
2326        local_point: teksilo_canvas::Point,
2327        bounds: teksilo_canvas::Rect,
2328    ) -> Option<f32> {
2329        self.widget.hit_distance(local_point, bounds)
2330    }
2331
2332    fn target_regions(&self, bounds: teksilo_canvas::Rect) -> Vec<crate::partition::TargetRegion> {
2333        self.widget.target_regions(bounds)
2334    }
2335
2336    /// Forwarded because the default is the destructive answer. A container that
2337    /// keeps memoized panes across a rebuild loses them to a builder method
2338    /// otherwise, and the loss reads as content that vanishes on an unrelated
2339    /// state change rather than as a dropped forward.
2340    fn preserves_children_on_rebuild(&self) -> bool {
2341        self.widget.preserves_children_on_rebuild()
2342    }
2343
2344    /// Forwarded so tooltip content that knows itself to be empty can still say
2345    /// so. The default is `true`, which shows an empty bubble instead.
2346    fn tooltip_has_content(&self) -> bool {
2347        self.widget.tooltip_has_content()
2348    }
2349
2350    /// Forwarded because a declaration is how the registry and the rebinding UI
2351    /// learn a keystroke exists. Dropped, the shortcut is not overridden by
2352    /// anything — it is simply never registered.
2353    fn declare_shortcuts(&self) -> Vec<crate::shortcut::Shortcut> {
2354        self.widget.declare_shortcuts()
2355    }
2356
2357    /// The one method here that is **not** a forward, deliberately: it exists so
2358    /// the arena can lift off the handlers the builder chain just attached, and
2359    /// those live on the wrapper, not on the widget. Forwarding it would hand
2360    /// the arena the wrapped widget's set instead and the attached handlers
2361    /// would never reach the node — which is the whole point of the wrapper.
2362    ///
2363    /// The call is the inherent `WidgetWithHandlers::take_handler_set`, not this
2364    /// one.
2365    fn take_handler_set(&mut self) -> Option<HandlerSet> {
2366        // Recursive, because a `WidgetBuilder` method with no inherent twin on
2367        // this type wraps an already-wrapped widget. Lifting only the outer set
2368        // silently drops every handler attached before that call.
2369        let mut outer = WidgetWithHandlers::take_handler_set(self);
2370        if let Some(inner) = crate::widget::Widget::take_handler_set(&mut self.widget) {
2371            outer.merge_under(inner);
2372        }
2373        Some(outer)
2374    }
2375}
2376
2377// ---------------------------------------------------------------------------
2378// WidgetBuilder trait — the entry point
2379// ---------------------------------------------------------------------------
2380
2381/// Blanket trait providing attached handler methods for all Widget types.
2382/// The first builder method call wraps the widget in `WidgetWithHandlers`.
2383pub trait WidgetBuilder: Widget + Sized + 'static {
2384    fn on_tap(
2385        self,
2386        f: impl FnMut(&TapEvent, &mut EventContext) + 'static,
2387    ) -> WidgetWithHandlers<Self> {
2388        WidgetWithHandlers::new(self).on_tap(f)
2389    }
2390
2391    fn on_double_tap(
2392        self,
2393        f: impl FnMut(&TapEvent, &mut EventContext) + 'static,
2394    ) -> WidgetWithHandlers<Self> {
2395        WidgetWithHandlers::new(self).on_double_tap(f)
2396    }
2397
2398    fn on_triple_tap(
2399        self,
2400        f: impl FnMut(&TapEvent, &mut EventContext) + 'static,
2401    ) -> WidgetWithHandlers<Self> {
2402        WidgetWithHandlers::new(self).on_triple_tap(f)
2403    }
2404
2405    fn on_long_press(
2406        self,
2407        f: impl FnMut(&TapEvent, &mut EventContext) + 'static,
2408    ) -> WidgetWithHandlers<Self> {
2409        WidgetWithHandlers::new(self).on_long_press(f)
2410    }
2411
2412    /// Restrict (or extend) the set of pointer buttons that fire
2413    /// `on_tap`. Default is [`ButtonMask::PRIMARY`].
2414    ///
2415    /// The mask belongs to the `on_tap` set beside it. A node carries two
2416    /// handler sets, the widget's own (from its `build`) and the one this
2417    /// builder chain attaches, and each fires only on the buttons in its own
2418    /// mask: widening the app's `on_tap` to the secondary button does not make
2419    /// a right click run the widget's built-in click (a `Slider` jumping to
2420    /// it, say). A mask set where no `on_tap` is attached beside it reaches
2421    /// the widget's own instead, unless that one declared a mask of its own:
2422    /// `Button::new(..).accept_tap_buttons(PRIMARY | SECONDARY)` is how a
2423    /// right click activates a button.
2424    fn accept_tap_buttons(self, mask: impl Into<ButtonMask>) -> WidgetWithHandlers<Self> {
2425        WidgetWithHandlers::new(self).accept_tap_buttons(mask)
2426    }
2427
2428    /// Restrict (or extend) the set of pointer buttons that fire
2429    /// `on_double_tap`. Default [`ButtonMask::PRIMARY`].
2430    ///
2431    /// Scoped to the handler set it is attached with, the way
2432    /// [`accept_tap_buttons`](Self::accept_tap_buttons) is.
2433    fn accept_double_tap_buttons(self, mask: impl Into<ButtonMask>) -> WidgetWithHandlers<Self> {
2434        WidgetWithHandlers::new(self).accept_double_tap_buttons(mask)
2435    }
2436
2437    /// Restrict (or extend) the set of pointer buttons that fire
2438    /// `on_triple_tap`. Default [`ButtonMask::PRIMARY`].
2439    ///
2440    /// Scoped to the handler set it is attached with, the way
2441    /// [`accept_tap_buttons`](Self::accept_tap_buttons) is.
2442    fn accept_triple_tap_buttons(self, mask: impl Into<ButtonMask>) -> WidgetWithHandlers<Self> {
2443        WidgetWithHandlers::new(self).accept_triple_tap_buttons(mask)
2444    }
2445
2446    /// Restrict (or extend) the set of pointer buttons that fire
2447    /// `on_long_press`. Default [`ButtonMask::PRIMARY`].
2448    ///
2449    /// Scoped to the handler set it is attached with, the way
2450    /// [`accept_tap_buttons`](Self::accept_tap_buttons) is.
2451    fn accept_long_press_buttons(self, mask: impl Into<ButtonMask>) -> WidgetWithHandlers<Self> {
2452        WidgetWithHandlers::new(self).accept_long_press_buttons(mask)
2453    }
2454
2455    fn on_drag(
2456        self,
2457        f: impl FnMut(DragPhase, &mut EventContext) + 'static,
2458    ) -> WidgetWithHandlers<Self> {
2459        WidgetWithHandlers::new(self).on_drag(f)
2460    }
2461
2462    fn on_swipe(
2463        self,
2464        f: impl FnMut(SwipeDirection, f32, &mut EventContext) + 'static,
2465    ) -> WidgetWithHandlers<Self> {
2466        WidgetWithHandlers::new(self).on_swipe(f)
2467    }
2468
2469    fn on_pinch(
2470        self,
2471        f: impl FnMut(PinchPhase, &mut EventContext) + 'static,
2472    ) -> WidgetWithHandlers<Self> {
2473        WidgetWithHandlers::new(self).on_pinch(f)
2474    }
2475
2476    fn on_focus(
2477        self,
2478        f: impl FnMut(bool, &mut EventContext) + 'static,
2479    ) -> WidgetWithHandlers<Self> {
2480        WidgetWithHandlers::new(self).on_focus(f)
2481    }
2482
2483    fn on_key(
2484        self,
2485        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
2486    ) -> WidgetWithHandlers<Self> {
2487        WidgetWithHandlers::new(self).on_key(f)
2488    }
2489
2490    /// Strict-ancestor key preview. See [`HandlerSet::on_key_preview`].
2491    fn on_key_preview(
2492        self,
2493        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
2494    ) -> WidgetWithHandlers<Self> {
2495        WidgetWithHandlers::new(self).on_key_preview(f)
2496    }
2497
2498    fn on_pointer_event(
2499        self,
2500        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
2501    ) -> WidgetWithHandlers<Self> {
2502        WidgetWithHandlers::new(self).on_pointer_event(f)
2503    }
2504
2505    fn on_hover(
2506        self,
2507        f: impl FnMut(bool, &mut EventContext) + 'static,
2508    ) -> WidgetWithHandlers<Self> {
2509        WidgetWithHandlers::new(self).on_hover(f)
2510    }
2511
2512    fn on_scroll(
2513        self,
2514        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
2515    ) -> WidgetWithHandlers<Self> {
2516        WidgetWithHandlers::new(self).on_scroll(f)
2517    }
2518
2519    fn on_access_action(
2520        self,
2521        f: impl FnMut(accesskit::Action, &mut EventContext) -> EventResponse + 'static,
2522    ) -> WidgetWithHandlers<Self> {
2523        WidgetWithHandlers::new(self).on_access_action(f)
2524    }
2525
2526    fn focusable(self, focusable: bool) -> WidgetWithHandlers<Self> {
2527        WidgetWithHandlers::new(self).focusable(focusable)
2528    }
2529
2530    fn tab_index(self, index: i32) -> WidgetWithHandlers<Self> {
2531        WidgetWithHandlers::new(self).tab_index(index)
2532    }
2533
2534    fn cursor(self, cursor: CursorIcon) -> WidgetWithHandlers<Self> {
2535        WidgetWithHandlers::new(self).cursor(cursor)
2536    }
2537
2538    fn clips_children_on(self, clips: bool) -> WidgetWithHandlers<Self> {
2539        WidgetWithHandlers::new(self).clips_children(clips)
2540    }
2541
2542    /// Declare this node a text-input surface, enabling the OS input method
2543    /// (with `ctx`'s purpose) while it is focused. See [`crate::ime`].
2544    fn ime_input(self, ctx: crate::ime::ImeContext) -> WidgetWithHandlers<Self> {
2545        WidgetWithHandlers::new(self).ime_input(ctx)
2546    }
2547
2548    /// Make the widget invisible to pointer hit-testing. See
2549    /// [`HandlerSet::event_pass_through`].
2550    fn event_pass_through(self, pass_through: bool) -> WidgetWithHandlers<Self> {
2551        WidgetWithHandlers::new(self).event_pass_through(pass_through)
2552    }
2553
2554    /// Mark this widget's subtree a gesture dead zone. See
2555    /// [`HandlerSet::gesture_dead_zone`].
2556    fn gesture_dead_zone(self, dead_zone: bool) -> WidgetWithHandlers<Self> {
2557        WidgetWithHandlers::new(self).gesture_dead_zone(dead_zone)
2558    }
2559
2560    /// Whether a click or a tap moves keyboard focus to this widget. Default
2561    /// `true`. With `false` a click on it activates it and leaves focus where
2562    /// it was, while Tab, a focus request and an assistive technology still
2563    /// focus it (GTK's `focus-on-click`, Qt's `Qt::TabFocus`). For this node
2564    /// only: a focusable widget inside it keeps its own setting. For controls
2565    /// that act on another widget's content, such as menu bar triggers and
2566    /// toolbar buttons. See [`HandlerSet::focus_on_click`].
2567    fn focus_on_click(self, focus_on_click: bool) -> WidgetWithHandlers<Self> {
2568        WidgetWithHandlers::new(self).focus_on_click(focus_on_click)
2569    }
2570
2571    /// Select what a hold on this widget's subtree means. See
2572    /// [`HandlerSet::long_press_role`].
2573    fn long_press_role(
2574        self,
2575        role: crate::widget_tree::touch_route::LongPressRole,
2576    ) -> WidgetWithHandlers<Self> {
2577        WidgetWithHandlers::new(self).long_press_role(role)
2578    }
2579
2580    /// Override what a direct pointer may do to this widget's subtree. See
2581    /// [`HandlerSet::touch_action`].
2582    fn touch_action(self, action: TouchAction) -> WidgetWithHandlers<Self> {
2583        WidgetWithHandlers::new(self).touch_action(action)
2584    }
2585
2586    /// Declare this widget a pan surface on `axes`, kinetic, direct pointers
2587    /// only. See [`HandlerSet::scroll_container`].
2588    fn scroll_container(self, axes: PanAxes) -> WidgetWithHandlers<Self> {
2589        WidgetWithHandlers::new(self).scroll_container(axes)
2590    }
2591
2592    /// Declare this widget a pan surface with an explicit [`PanClaim`]. See
2593    /// [`HandlerSet::pan_claim`].
2594    fn pan_claim(self, claim: PanClaim) -> WidgetWithHandlers<Self> {
2595        WidgetWithHandlers::new(self).pan_claim(claim)
2596    }
2597
2598    /// Whether this widget absorbs a boundary scroll or chains it outward. See
2599    /// [`HandlerSet::overscroll_behavior`].
2600    fn overscroll_behavior(self, behavior: crate::OverscrollBehavior) -> WidgetWithHandlers<Self> {
2601        WidgetWithHandlers::new(self).overscroll_behavior(behavior)
2602    }
2603
2604    /// Declare when a drag on this widget may begin relative to the press that
2605    /// starts it. See [`HandlerSet::drag_activation`].
2606    fn drag_activation(
2607        self,
2608        activation: teksilo_tokens::DragActivation,
2609    ) -> WidgetWithHandlers<Self> {
2610        WidgetWithHandlers::new(self).drag_activation(activation)
2611    }
2612
2613    /// Declare how many simultaneous contacts this widget serves. Default
2614    /// [`MultiContact::First`] — the second finger on a single-contact control
2615    /// is terminated there rather than reaching an ancestor.
2616    fn multi_contact(self, policy: MultiContact) -> WidgetWithHandlers<Self> {
2617        WidgetWithHandlers::new(self).multi_contact(policy)
2618    }
2619
2620    /// Mark this widget a keyboard capture surface (terminals, game
2621    /// viewports): while focused, `KeyDown`s bypass shortcut resolution.
2622    /// See [`HandlerSet::keyboard_capture`].
2623    fn keyboard_capture(self, capture: bool) -> WidgetWithHandlers<Self> {
2624        WidgetWithHandlers::new(self).keyboard_capture(capture)
2625    }
2626
2627    /// Mark this widget a focus region, a part of the window F6 and Shift+F6
2628    /// move focus to, announcing `name`. See [`HandlerSet::focus_region`].
2629    fn focus_region(self, name: impl Into<Prop<String>>) -> WidgetWithHandlers<Self> {
2630        WidgetWithHandlers::new(self).focus_region(name)
2631    }
2632
2633    /// Make this widget and its whole subtree invisible to pointer
2634    /// hit-testing (decorative overlays). See
2635    /// [`HandlerSet::hit_transparent`].
2636    fn hit_transparent(self, transparent: bool) -> WidgetWithHandlers<Self> {
2637        WidgetWithHandlers::new(self).hit_transparent(transparent)
2638    }
2639
2640    /// Override the miss-only hit slop for this node. See
2641    /// [`HandlerSet::hit_slop`].
2642    fn hit_slop(self, slop: crate::pointer::hit_slop::HitSlop) -> WidgetWithHandlers<Self> {
2643        WidgetWithHandlers::new(self).hit_slop(slop)
2644    }
2645
2646    /// Take this node out of both hit-widening mechanisms — no slop outset and
2647    /// no `Widget::hit_outset`. See [`HandlerSet::no_hit_slop`].
2648    fn no_hit_slop(self) -> WidgetWithHandlers<Self> {
2649        WidgetWithHandlers::new(self).no_hit_slop()
2650    }
2651
2652    /// Set a context-menu factory. See
2653    /// [`HandlerSet::context_menu`] for the full contract.
2654    fn context_menu(
2655        self,
2656        factory: impl Fn(Point, &mut EventContext) -> Option<Box<dyn Widget>> + 'static,
2657    ) -> WidgetWithHandlers<Self> {
2658        WidgetWithHandlers::new(self).context_menu(factory)
2659    }
2660
2661    /// Add a section to the context menu opened at or below this node. See
2662    /// [`HandlerSet::context_menu_contribution`].
2663    fn context_menu_contribution(
2664        self,
2665        contribution: impl Fn(&mut EventContext) -> Option<Box<dyn Widget>> + 'static,
2666    ) -> WidgetWithHandlers<Self> {
2667        WidgetWithHandlers::new(self).context_menu_contribution(contribution)
2668    }
2669
2670    /// Bind a `Signal<bool>` the framework writes when a strict
2671    /// descendant has focus. See [`HandlerSet::focus_within`].
2672    fn focus_within(self, signal: crate::signal::Signal<bool>) -> WidgetWithHandlers<Self> {
2673        WidgetWithHandlers::new(self).focus_within(signal)
2674    }
2675
2676    /// Bind a `Signal<bool>` the framework writes when a strict
2677    /// descendant is hovered. See [`HandlerSet::hover_within`].
2678    fn hover_within(self, signal: crate::signal::Signal<bool>) -> WidgetWithHandlers<Self> {
2679        WidgetWithHandlers::new(self).hover_within(signal)
2680    }
2681
2682    /// Bind this node's visibility (`bool` / `Signal<bool>` / `Prop<bool>`) as
2683    /// a builder property, so `teksu!` can write `visible_when: sig`. Equivalent
2684    /// to `ctx.visible_when(id, ..)`. See [`HandlerSet::visible_when`].
2685    fn visible_when(self, state: impl Into<Prop<bool>>) -> WidgetWithHandlers<Self> {
2686        WidgetWithHandlers::new(self).visible_when(state)
2687    }
2688
2689    fn on_drag_hover(
2690        self,
2691        f: impl FnMut(
2692            &crate::drag_payload::DragPayload,
2693            teksilo_canvas::Point,
2694            &mut EventContext,
2695        ) -> crate::drag_state::DropFeedback
2696        + 'static,
2697    ) -> WidgetWithHandlers<Self> {
2698        WidgetWithHandlers::new(self).on_drag_hover(f)
2699    }
2700
2701    fn on_drag_leave(self, f: impl FnMut(&mut EventContext) + 'static) -> WidgetWithHandlers<Self> {
2702        WidgetWithHandlers::new(self).on_drag_leave(f)
2703    }
2704
2705    fn on_drag_tick(
2706        self,
2707        f: impl FnMut(teksilo_canvas::Point, &mut EventContext) + 'static,
2708    ) -> WidgetWithHandlers<Self> {
2709        WidgetWithHandlers::new(self).on_drag_tick(f)
2710    }
2711
2712    /// Attach a pointer-cancel handler. See
2713    /// [`HandlerSet::on_pointer_cancel`].
2714    fn on_pointer_cancel(
2715        self,
2716        f: impl FnMut(&crate::pointer::PointerInfo, crate::pointer::CancelReason, &mut EventContext)
2717        + 'static,
2718    ) -> WidgetWithHandlers<Self> {
2719        WidgetWithHandlers::new(self).on_pointer_cancel(f)
2720    }
2721
2722    fn on_drop(
2723        self,
2724        f: impl FnMut(
2725            crate::drag_payload::DragPayload,
2726            teksilo_canvas::Point,
2727            &mut EventContext,
2728        ) -> bool
2729        + 'static,
2730    ) -> WidgetWithHandlers<Self> {
2731        WidgetWithHandlers::new(self).on_drop(f)
2732    }
2733
2734    /// Set the drag-ended handler on a drag source. See
2735    /// [`HandlerSet::on_drag_ended`].
2736    fn on_drag_ended(
2737        self,
2738        f: impl FnMut(crate::drag_payload::DropOutcome, &mut EventContext) + 'static,
2739    ) -> WidgetWithHandlers<Self> {
2740        WidgetWithHandlers::new(self).on_drag_ended(f)
2741    }
2742
2743    // ── Accessibility overrides ────────────────────────────────────────
2744    //
2745    // Trait-level entry points: each method wraps the widget into a
2746    // `WidgetWithHandlers` (the first builder call in any chain) and
2747    // forwards to the inherent method of the same name. See
2748    // `WidgetWithHandlers` for full rustdoc on each method's semantics.
2749    // For translated strings, `LocalizedString` flows through
2750    // `impl Into<Prop<String>>` via `teksilo-i18n`'s
2751    // `From<LocalizedString> for Prop<String>` impl, staying reactive.
2752
2753    fn access_label(self, label: impl Into<Prop<String>>) -> WidgetWithHandlers<Self> {
2754        WidgetWithHandlers::new(self).access_label(label)
2755    }
2756
2757    #[doc(hidden)]
2758    fn access_label_literal(self, label: impl Into<String>) -> WidgetWithHandlers<Self> {
2759        WidgetWithHandlers::new(self).access_label_literal(label)
2760    }
2761
2762    fn access_description(self, description: impl Into<Prop<String>>) -> WidgetWithHandlers<Self> {
2763        WidgetWithHandlers::new(self).access_description(description)
2764    }
2765
2766    #[doc(hidden)]
2767    fn access_description_literal(
2768        self,
2769        description: impl Into<String>,
2770    ) -> WidgetWithHandlers<Self> {
2771        WidgetWithHandlers::new(self).access_description_literal(description)
2772    }
2773
2774    fn access_hint(self, hint: impl Into<Prop<String>>) -> WidgetWithHandlers<Self> {
2775        WidgetWithHandlers::new(self).access_hint(hint)
2776    }
2777
2778    #[doc(hidden)]
2779    fn access_hint_literal(self, hint: impl Into<String>) -> WidgetWithHandlers<Self> {
2780        WidgetWithHandlers::new(self).access_hint_literal(hint)
2781    }
2782
2783    fn access_value(self, value: impl Into<Prop<String>>) -> WidgetWithHandlers<Self> {
2784        WidgetWithHandlers::new(self).access_value(value)
2785    }
2786
2787    #[doc(hidden)]
2788    fn access_value_literal(self, value: impl Into<String>) -> WidgetWithHandlers<Self> {
2789        WidgetWithHandlers::new(self).access_value_literal(value)
2790    }
2791
2792    fn access_role(self, role: accesskit::Role) -> WidgetWithHandlers<Self> {
2793        WidgetWithHandlers::new(self).access_role(role)
2794    }
2795
2796    fn access_hidden(self, hidden: impl Into<Prop<bool>>) -> WidgetWithHandlers<Self> {
2797        WidgetWithHandlers::new(self).access_hidden(hidden)
2798    }
2799
2800    fn access_disabled(self, disabled: bool) -> WidgetWithHandlers<Self> {
2801        WidgetWithHandlers::new(self).access_disabled(disabled)
2802    }
2803
2804    fn access_identifier(self, id: impl Into<String>) -> WidgetWithHandlers<Self> {
2805        WidgetWithHandlers::new(self).access_identifier(id)
2806    }
2807
2808    fn access_controls(self, target: WidgetId) -> WidgetWithHandlers<Self> {
2809        WidgetWithHandlers::new(self).access_controls(target)
2810    }
2811
2812    fn access_described_by(self, target: WidgetId) -> WidgetWithHandlers<Self> {
2813        WidgetWithHandlers::new(self).access_described_by(target)
2814    }
2815
2816    fn access_labelled_by(self, target: WidgetId) -> WidgetWithHandlers<Self> {
2817        WidgetWithHandlers::new(self).access_labelled_by(target)
2818    }
2819
2820    fn access_live(self, mode: accesskit::Live) -> WidgetWithHandlers<Self> {
2821        WidgetWithHandlers::new(self).access_live(mode)
2822    }
2823
2824    fn access_current(self, current: accesskit::AriaCurrent) -> WidgetWithHandlers<Self> {
2825        WidgetWithHandlers::new(self).access_current(current)
2826    }
2827
2828    fn access_shortcut_literal(self, shortcut: impl Into<String>) -> WidgetWithHandlers<Self> {
2829        WidgetWithHandlers::new(self).access_shortcut_literal(shortcut)
2830    }
2831
2832    fn access_shortcut_id(self, id: impl Into<String>) -> WidgetWithHandlers<Self> {
2833        WidgetWithHandlers::new(self).access_shortcut_id(id)
2834    }
2835
2836    fn access_has_popup(self, kind: accesskit::HasPopup) -> WidgetWithHandlers<Self> {
2837        WidgetWithHandlers::new(self).access_has_popup(kind)
2838    }
2839
2840    fn access_orientation(self, orientation: accesskit::Orientation) -> WidgetWithHandlers<Self> {
2841        WidgetWithHandlers::new(self).access_orientation(orientation)
2842    }
2843
2844    fn access_exclude_subtree(self) -> WidgetWithHandlers<Self> {
2845        WidgetWithHandlers::new(self).access_exclude_subtree()
2846    }
2847
2848    fn access_merge_subtree(self) -> WidgetWithHandlers<Self> {
2849        WidgetWithHandlers::new(self).access_merge_subtree()
2850    }
2851
2852    fn access_subtree(self, mode: AccessSubtreeMode) -> WidgetWithHandlers<Self> {
2853        WidgetWithHandlers::new(self).access_subtree(mode)
2854    }
2855
2856    fn access_numeric_value(self, value: f64) -> WidgetWithHandlers<Self> {
2857        WidgetWithHandlers::new(self).access_numeric_value(value)
2858    }
2859
2860    fn access_numeric_range(self, min: f64, max: f64) -> WidgetWithHandlers<Self> {
2861        WidgetWithHandlers::new(self).access_numeric_range(min, max)
2862    }
2863
2864    fn access_numeric_step(self, step: f64) -> WidgetWithHandlers<Self> {
2865        WidgetWithHandlers::new(self).access_numeric_step(step)
2866    }
2867
2868    fn access_action<F>(self, action: accesskit::Action, handler: F) -> WidgetWithHandlers<Self>
2869    where
2870        F: FnMut(&mut EventContext) + 'static,
2871    {
2872        WidgetWithHandlers::new(self).access_action(action, handler)
2873    }
2874
2875    fn access_remove_action(self, action: accesskit::Action) -> WidgetWithHandlers<Self> {
2876        WidgetWithHandlers::new(self).access_remove_action(action)
2877    }
2878
2879    fn access_custom_action<F>(
2880        self,
2881        label: impl Into<Prop<String>>,
2882        handler: F,
2883    ) -> WidgetWithHandlers<Self>
2884    where
2885        F: FnMut(&mut EventContext) + 'static,
2886    {
2887        WidgetWithHandlers::new(self).access_custom_action(label, handler)
2888    }
2889
2890    #[doc(hidden)]
2891    fn access_custom_action_literal<F>(
2892        self,
2893        label: impl Into<String>,
2894        handler: F,
2895    ) -> WidgetWithHandlers<Self>
2896    where
2897        F: FnMut(&mut EventContext) + 'static,
2898    {
2899        WidgetWithHandlers::new(self).access_custom_action_literal(label, handler)
2900    }
2901
2902    fn access_customize<F>(self, f: F) -> WidgetWithHandlers<Self>
2903    where
2904        F: Fn(&mut crate::accessibility::AccessNodeBuilder) + 'static,
2905    {
2906        WidgetWithHandlers::new(self).access_customize(f)
2907    }
2908}
2909
2910// Blanket implementation for all Widget types.
2911impl<W: Widget + Sized + 'static> WidgetBuilder for W {}
2912
2913#[cfg(test)]
2914mod wrapper_forwarding_tests;
2915
2916#[cfg(test)]
2917mod tests {
2918    use super::*;
2919    use crate::widget::WidgetPlacement;
2920    use crate::widget_id::WidgetId;
2921    use crate::widget_tree::WidgetTree;
2922
2923    #[derive(Debug)]
2924    struct CompositeLeaf {
2925        child_id: Option<WidgetId>,
2926    }
2927
2928    impl CompositeLeaf {
2929        fn new() -> Self {
2930            Self { child_id: None }
2931        }
2932    }
2933
2934    impl Widget for CompositeLeaf {
2935        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
2936            let child = ctx.add(crate::test_widgets::FillWidget::new());
2937            self.child_id = Some(child);
2938            vec![child]
2939        }
2940
2941        fn layout_response(
2942            &self,
2943            proposal: teksilo_canvas::SizeProposal,
2944            _ctx: &crate::widget::LayoutContext,
2945        ) -> crate::widget::LayoutResponse {
2946            proposal.resolve(120.0, 40.0).into()
2947        }
2948
2949        fn place_children(
2950            &self,
2951            bounds: teksilo_canvas::Rect,
2952            _proposal: teksilo_canvas::SizeProposal,
2953            children: &mut [WidgetPlacement],
2954            _ctx: &crate::widget::LayoutContext,
2955        ) {
2956            for child in children.iter_mut() {
2957                child.origin = bounds.origin();
2958                child.size = bounds.size();
2959            }
2960        }
2961
2962        fn children(&self) -> Vec<WidgetId> {
2963            self.child_id.into_iter().collect()
2964        }
2965    }
2966
2967    #[test]
2968    fn external_handlers_survive_rebuild() {
2969        // Regression check: handlers attached externally via the
2970        // `WidgetBuilder` builder (e.g. `MyCompositeWidget::new().on_tap(...)`)
2971        // must continue to fire after the widget rebuilds in place.
2972        // My handler-clearing fix in `rebuild_single_widget` wiped
2973        // `node.handlers` to stop accumulation of `apply_self_handlers`
2974        // calls across rebuilds — but the extracted-once-at-insertion
2975        // HandlerSet is gone by rebuild time and would be lost.
2976        use std::cell::Cell;
2977        use std::rc::Rc;
2978
2979        let tap_count = Rc::new(Cell::new(0_u32));
2980        let tc = tap_count.clone();
2981
2982        let mut tree = WidgetTree::new();
2983        let id = tree.add(CompositeLeaf::new().on_tap(move |_pos, _ctx| {
2984            tc.set(tc.get() + 1);
2985        }));
2986        tree.layout(teksilo_canvas::SizeProposal::exact(200.0, 100.0));
2987
2988        // Trip a rebuild of the composite — its child gets torn down &
2989        // rebuilt; node.handlers gets cleared and reset.
2990        tree.arena_mark_needs_rebuild_for_testing(id);
2991        tree.layout(teksilo_canvas::SizeProposal::exact(200.0, 100.0));
2992
2993        // Click through the composite; the externally-attached on_tap
2994        // must still be wired up.
2995        tree.click(id);
2996        assert_eq!(
2997            tap_count.get(),
2998            1,
2999            "externally-attached on_tap must survive a rebuild"
3000        );
3001    }
3002
3003    #[test]
3004    fn wrapped_composite_widget_still_builds_children() {
3005        let mut tree = WidgetTree::new();
3006        let root = tree.add(CompositeLeaf::new().on_tap(|_pos, _ctx| {}));
3007        tree.layout(teksilo_canvas::SizeProposal::exact(200.0, 100.0));
3008
3009        assert_eq!(tree.children(root).len(), 1);
3010    }
3011
3012    /// A widget that exposes both downcast hooks, like every widget a
3013    /// composing container reads its child's concrete type through.
3014    #[derive(Debug)]
3015    struct Reflective {
3016        marker: u32,
3017    }
3018
3019    impl Widget for Reflective {
3020        fn layout_response(
3021            &self,
3022            proposal: teksilo_canvas::SizeProposal,
3023            _ctx: &crate::widget::LayoutContext,
3024        ) -> crate::widget::LayoutResponse {
3025            proposal.resolve(0.0, 0.0).into()
3026        }
3027
3028        fn as_any(&self) -> Option<&dyn std::any::Any> {
3029            Some(self)
3030        }
3031
3032        fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
3033            Some(self)
3034        }
3035    }
3036
3037    /// Decorating a widget must not hide its concrete type from a parent that
3038    /// reads it. `as_any` was already forwarded; `as_any_mut` was not, so a
3039    /// container reading a child through the mutable hook (`MenuList` does, for
3040    /// mnemonics, the type-ahead label and radio grouping) silently saw nothing
3041    /// the moment any builder method was called on that child.
3042    #[test]
3043    fn both_downcast_hooks_see_through_the_handler_wrapper() {
3044        let mut wrapped = Reflective { marker: 7 }.focusable(true);
3045
3046        let seen = wrapped
3047            .as_any()
3048            .and_then(|a| a.downcast_ref::<Reflective>())
3049            .map(|r| r.marker);
3050        assert_eq!(seen, Some(7), "as_any must forward through the wrapper");
3051
3052        let seen_mut = wrapped
3053            .as_any_mut()
3054            .and_then(|a| a.downcast_mut::<Reflective>())
3055            .map(|r| r.marker);
3056        assert_eq!(
3057            seen_mut,
3058            Some(7),
3059            "as_any_mut must forward through the wrapper too"
3060        );
3061    }
3062
3063    // --- touch-action / pan-claim builder surfaces ---------------------
3064
3065    /// Surface 1: `HandlerSet`'s own builder methods set its fields
3066    /// directly — no arena involved.
3067    #[test]
3068    fn handler_set_surface_sets_touch_action_and_pan_claim() {
3069        let hs = HandlerSet::new()
3070            .touch_action(TouchAction::PAN_Y)
3071            .scroll_container(PanAxes::BOTH);
3072        assert_eq!(hs.touch_action, Some(TouchAction::PAN_Y));
3073        assert_eq!(
3074            hs.pan_claim,
3075            Some(PanClaim {
3076                axes: PanAxes::BOTH,
3077                devices: teksilo_tokens::PointerKindMask::DIRECT,
3078                kinetic: true,
3079            })
3080        );
3081    }
3082
3083    /// Surface 2: `WidgetWithHandlers<W>`'s inherent methods — reached by
3084    /// chaining a second builder call onto an already-wrapped widget, which
3085    /// resolves to the inherent impl rather than the blanket trait default.
3086    #[test]
3087    fn widget_with_handlers_surface_sets_touch_action_and_pan_claim() {
3088        let wrapped = crate::test_widgets::FillWidget::new()
3089            .gesture_dead_zone(false) // promotes to WidgetWithHandlers via the trait
3090            .touch_action(TouchAction::NONE) // now resolves to the inherent method
3091            .pan_claim(PanClaim::horizontal());
3092        assert_eq!(wrapped.handler_set.touch_action, Some(TouchAction::NONE));
3093        assert_eq!(wrapped.handler_set.pan_claim, Some(PanClaim::horizontal()));
3094    }
3095
3096    /// Surface 3: the blanket `WidgetBuilder` trait default method, called
3097    /// directly on a bare `Widget` and verified end-to-end through the tree
3098    /// (the widget-authoring call shape apps actually use). Read back
3099    /// through the two path folds themselves: for a root with no ancestors,
3100    /// `effective_touch_action` / `pan_candidates` reduce to exactly the
3101    /// node's own declaration, so this doubles as a sanity check on those
3102    /// folds' base case.
3103    #[test]
3104    fn widget_builder_surface_sets_the_node_fields() {
3105        let mut tree = WidgetTree::new();
3106        let id = tree.add(
3107            crate::test_widgets::FillWidget::new()
3108                .touch_action(TouchAction::PAN_X)
3109                .scroll_container(PanAxes::Y),
3110        );
3111        tree.layout(teksilo_canvas::SizeProposal::exact(100.0, 100.0));
3112
3113        assert_eq!(tree.effective_touch_action(id), TouchAction::PAN_X);
3114        assert_eq!(
3115            tree.pan_candidates(id, TouchAction::AUTO),
3116            vec![(
3117                id,
3118                PanClaim {
3119                    axes: PanAxes::Y,
3120                    devices: teksilo_tokens::PointerKindMask::DIRECT,
3121                    kinetic: true,
3122                }
3123            )]
3124        );
3125    }
3126}