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