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