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::{DragPhase, PinchPhase, SwipeDirection, TapEvent};
25use crate::signal::Prop;
26use crate::widget::{CursorIcon, EventContext, Widget};
27use crate::widget_id::WidgetId;
28
29// ---------------------------------------------------------------------------
30// Accessibility overrides
31// ---------------------------------------------------------------------------
32
33/// Subtree visibility / merge mode applied by the accessibility tree walker.
34///
35/// Set via `WidgetBuilder::access_exclude_subtree()` /
36/// `access_merge_subtree()`. The walker honors the mode after the parent
37/// node has been emitted, before recursing into descendants.
38#[derive(Default, Clone, Copy, PartialEq, Eq, Debug)]
39pub enum AccessSubtreeMode {
40    /// Normal walk — descendants emitted as their own AT nodes.
41    #[default]
42    Inherit,
43    /// Descendants pruned from the AT tree entirely. Parent node still
44    /// emitted normally. Equivalent to Flutter's `excludeSemantics: true`.
45    Exclude,
46    /// Descendants' labels / descriptions / values / actions are
47    /// concatenated into the parent's emitted node, then descendants are
48    /// pruned. The parent reads as a single AT element. Equivalent to
49    /// Flutter's `mergeAllDescendants: true` and SwiftUI's
50    /// `.accessibilityElement(children: .combine)`.
51    Merge,
52}
53
54/// Builder-level accessibility overrides.
55///
56/// Carried on `HandlerSet` during builder-chain construction, mirrored
57/// onto `WidgetNode::access_overrides` at arena insertion (parallel to
58/// `clips_children` / `cursor` / `focus_within_signal`), then applied by
59/// the accessibility tree walker after the inner widget's
60/// `accessibility(&self, builder)` runs.
61///
62/// User-visible string fields store a `Prop<String>` rather than a
63/// resolved `String`, so they stay reactive to locale changes.
64/// `teksilo-core` can't name `LocalizedString` (that lives in the
65/// downstream `teksilo-i18n` crate), but `Prop<String>` is a core type
66/// and `impl From<LocalizedString> for Prop<String>` in `teksilo-i18n`
67/// yields a `Prop::Bound` over a locale-observing `Signal<String>`. So
68/// `.access_label(tr!(save()))` stores a bound prop; the accessibility
69/// walker reads `.get()` at AT-build time, and `sync_accessibility`
70/// re-walks on locale change so the announced value follows the locale.
71/// The `_literal` builder variants store `Prop::Static` and are the
72/// `#[doc(hidden)]` grep markers for explicitly untranslated call sites
73/// (the only literal path reachable from within `teksilo-core`).
74#[derive(Default)]
75pub struct AccessibilityOverrides {
76    // -- Tier 1: labeling / state -----------------------------------------
77    pub label: Option<Prop<String>>,
78    pub description: Option<Prop<String>>,
79    pub value: Option<Prop<String>>,
80    pub role: Option<accesskit::Role>,
81    /// Reactive hidden-from-AT flag. `Some(prop)` where the prop reads
82    /// `true` hides the node from assistive technologies; `false` un-sets a
83    /// hidden state the inner widget emitted unconditionally. Bound props are
84    /// registered at `AccessibilityOnly` so the AT tree re-walks when they
85    /// flip (see the insertion paths in `widget_tree.rs`).
86    pub hidden: Option<Prop<bool>>,
87    pub disabled: Option<bool>,
88
89    // -- Tier 2: relationships / live / identity --------------------------
90    pub identifier: Option<String>,
91    pub controls: Vec<WidgetId>,
92    pub described_by: Vec<WidgetId>,
93    pub labelled_by: Vec<WidgetId>,
94    pub live: Option<accesskit::Live>,
95    pub aria_current: Option<accesskit::AriaCurrent>,
96    /// Pre-formatted shortcut announcement string (e.g. `"Ctrl+S"`).
97    /// Used by `access_shortcut_literal`. For chords routed through a
98    /// `Shortcut` registration, prefer `access_shortcut_id` (stored
99    /// in `shortcut_id`) so the announcement tracks rebinds.
100    pub shortcut: Option<String>,
101    /// Registered shortcut id (e.g. `"app.save"`). The accessibility
102    /// walker resolves the current keystroke from
103    /// `WidgetTree::shortcut_registry()` at AT-build time and writes
104    /// the formatted string to `Node::keyboard_shortcut`. Refreshes
105    /// automatically when the user rebinds (the registry's `version`
106    /// signal triggers a re-sync).
107    pub shortcut_id: Option<String>,
108    pub has_popup: Option<accesskit::HasPopup>,
109    pub orientation: Option<accesskit::Orientation>,
110
111    // -- Tier 3: numeric / actions / escape hatch -------------------------
112    pub numeric_value: Option<f64>,
113    pub min_numeric_value: Option<f64>,
114    pub max_numeric_value: Option<f64>,
115    pub numeric_step: Option<f64>,
116
117    /// Standard `accesskit::Action` advertisements with their handlers.
118    /// Dispatched by `event_dispatch_impl.rs` when handling
119    /// `WidgetEvent::AccessAction`, layered on top of any
120    /// user-installed `on_access_action` / `on_access_action_request`
121    /// handlers (both fire for the same dispatched event).
122    pub actions: Vec<(accesskit::Action, Box<dyn FnMut(&mut EventContext)>)>,
123
124    /// Actions to remove from the widget-emitted action list (called
125    /// after the widget's `accessibility()` runs, before custom-action
126    /// emission).
127    pub removed_actions: Vec<accesskit::Action>,
128
129    /// Custom-named actions (SwiftUI `.accessibilityAction(named:_:)`).
130    /// Each entry pairs a (reactive) description prop with a handler.
131    /// Index in the vec is the stable `i32` `CustomAction::id` exposed
132    /// to AT software.
133    pub custom_actions: Vec<(Prop<String>, Box<dyn FnMut(&mut EventContext)>)>,
134
135    /// Final escape hatch — invoked **last** in `apply()` with full
136    /// `&mut AccessNodeBuilder` access (including `inner_mut()`). Used
137    /// for sub-node surgery (synthetic children) and for cases the
138    /// typed surface doesn't cover.
139    pub customize: Option<Box<dyn Fn(&mut crate::accessibility::AccessNodeBuilder)>>,
140}
141
142impl std::fmt::Debug for AccessibilityOverrides {
143    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
144        f.debug_struct("AccessibilityOverrides")
145            .field("label", &self.label)
146            .field("description", &self.description)
147            .field("value", &self.value)
148            .field("role", &self.role)
149            .field("hidden", &self.hidden)
150            .field("disabled", &self.disabled)
151            .field("identifier", &self.identifier)
152            .field("shortcut", &self.shortcut)
153            .field("shortcut_id", &self.shortcut_id)
154            .field("controls_len", &self.controls.len())
155            .field("described_by_len", &self.described_by.len())
156            .field("labelled_by_len", &self.labelled_by.len())
157            .field("actions_len", &self.actions.len())
158            .field("removed_actions", &self.removed_actions)
159            .field("custom_actions_len", &self.custom_actions.len())
160            .finish()
161    }
162}
163
164impl AccessibilityOverrides {
165    /// Apply the override scalar / list fields onto a builder. Called by
166    /// the accessibility tree walker after the inner widget's
167    /// `accessibility(&self, builder)` runs and before the framework
168    /// finalizes the node.
169    pub(crate) fn apply(&self, b: &mut crate::accessibility::AccessNodeBuilder) {
170        use crate::accessibility::widget_id_to_node_id;
171
172        if let Some(ref p) = self.label {
173            b.set_name(p.get());
174        }
175        if let Some(ref p) = self.description {
176            b.set_description(p.get());
177        }
178        if let Some(ref p) = self.value {
179            b.set_value(p.get());
180        }
181        if let Some(role) = self.role {
182            b.set_role(role);
183        }
184        match self.hidden.as_ref().map(|p| p.get()) {
185            Some(true) => b.set_hidden(),
186            Some(false) => b.clear_hidden(),
187            None => {}
188        }
189        match self.disabled {
190            Some(true) => b.set_disabled(),
191            Some(false) => b.clear_disabled(),
192            None => {}
193        }
194        if let Some(ref s) = self.identifier {
195            b.set_author_id(s.clone());
196        }
197        for &id in &self.controls {
198            b.push_controlled(widget_id_to_node_id(id));
199        }
200        for &id in &self.described_by {
201            b.push_described_by(widget_id_to_node_id(id));
202        }
203        for &id in &self.labelled_by {
204            b.push_labelled_by(widget_id_to_node_id(id));
205        }
206        if let Some(live) = self.live {
207            b.set_live(live);
208        }
209        if let Some(c) = self.aria_current {
210            b.set_aria_current(c);
211        }
212        if let Some(ref s) = self.shortcut {
213            b.set_keyboard_shortcut(s.clone());
214        }
215        // `shortcut_id` resolution happens in the accessibility tree
216        // walker (where the `ShortcutRegistry` is reachable) — see
217        // `accessibility_impl::build_accessibility_recursive`.
218        if let Some(p) = self.has_popup {
219            b.set_has_popup(p);
220        }
221        if let Some(o) = self.orientation {
222            b.set_orientation(o);
223        }
224        if let Some(v) = self.numeric_value {
225            b.set_numeric_value(v);
226        }
227        if let Some(v) = self.min_numeric_value {
228            b.set_min_numeric_value(v);
229        }
230        if let Some(v) = self.max_numeric_value {
231            b.set_max_numeric_value(v);
232        }
233        if let Some(v) = self.numeric_step {
234            b.set_numeric_value_step(v);
235        }
236        // Suppression first, then advertisement — so `access_remove_action`
237        // can prune what the widget emitted, but a subsequent
238        // `access_action(same_action, ...)` re-advertises with the
239        // override-installed handler.
240        for &a in &self.removed_actions {
241            b.remove_action(a);
242        }
243        for (action, _) in &self.actions {
244            b.add_action(*action);
245        }
246        if !self.custom_actions.is_empty() {
247            let custom: Vec<accesskit::CustomAction> = self
248                .custom_actions
249                .iter()
250                .enumerate()
251                .map(|(i, (label, _))| accesskit::CustomAction {
252                    id: i as i32,
253                    description: label.get(),
254                })
255                .collect();
256            b.set_custom_actions(custom);
257        }
258        if let Some(ref f) = self.customize {
259            f(b);
260        }
261    }
262}
263
264// ---------------------------------------------------------------------------
265// HandlerSet — temporary storage before arena insertion
266// ---------------------------------------------------------------------------
267
268/// Type alias for a context-menu content factory.
269///
270/// The factory is invoked on every right-click that lands on a widget
271/// owning the factory (or on a descendant whose nearest ancestor with
272/// a factory is this one). It receives:
273///
274/// - `position`: pointer position in widget-local coordinates of the
275///   factory-owning widget. Useful when the menu's contents depend on
276///   *what* was right-clicked (a row in a list, a node in a tree, an
277///   item under a hit-test, …).
278/// - `ctx`: a full [`EventContext`], so the factory can read window
279///   state, query app state, send intents (e.g. for analytics), or
280///   update Signals before the menu mounts.
281///
282/// The factory returns:
283///
284/// - `Some(widget)` to mount `widget` as the menu overlay anchored at
285///   the factory-owning widget, placed at `position`.
286/// - `None` to **decline this right-click**. The framework continues
287///   walking up the parent chain looking for the next ancestor with a
288///   factory. This lets a widget conditionally suppress its own menu
289///   without uninstalling the factory.
290pub type ContextMenuFactory = Box<dyn Fn(Point, &mut EventContext) -> Option<Box<dyn Widget>>>;
291
292/// Temporary storage for handlers and metadata accumulated via builder
293/// methods. Transferred to the `WidgetNode` during arena insertion.
294pub struct HandlerSet {
295    pub(crate) handlers: EventHandlers,
296    pub(crate) focusable: Option<bool>,
297    pub(crate) tab_index: Option<i32>,
298    pub(crate) cursor: Option<CursorIcon>,
299    pub(crate) clips_children: Option<bool>,
300    /// When `Some(..)`, declares the node a text-input surface and the OS
301    /// input method is enabled (with this purpose) while it is focused.
302    /// `None` leaves the node default (no OS IME). The platform reads the
303    /// focused node's descriptor at focus-change time. See [`crate::ime`].
304    pub(crate) ime: Option<crate::ime::ImeContext>,
305    /// When `Some(true)`, the widget node is invisible to pointer
306    /// hit-testing — events fall through to whatever sits behind it.
307    /// Used by the debug inspector's overlay widgets.
308    pub(crate) event_pass_through: Option<bool>,
309    /// When `Some(true)`, a press in this node's subtree must not arm a
310    /// drag/swipe on any ancestor above it (a *gesture dead zone*). See
311    /// [`super::arena::WidgetNode::gesture_dead_zone`].
312    pub(crate) gesture_dead_zone: Option<bool>,
313    /// When `Some(true)` and this node holds keyboard focus, a `KeyDown`
314    /// bypasses shortcut resolution and is delivered straight to it (a
315    /// *keyboard capture* surface — terminals, game viewports). See
316    /// [`super::arena::WidgetNode::keyboard_capture`].
317    pub(crate) keyboard_capture: Option<bool>,
318    /// When `Some(true)`, this node and its WHOLE subtree are invisible
319    /// to pointer hit-testing (decorative overlays — count badges,
320    /// watermarks). See [`super::arena::WidgetNode::hit_transparent`].
321    pub(crate) hit_transparent: Option<bool>,
322    pub(crate) context_menu_factory: Option<ContextMenuFactory>,
323    /// User-bound signal that the framework writes whenever the
324    /// focused widget is a strict descendant of this node. See
325    /// [`HandlerSet::focus_within`].
326    pub(crate) focus_within: Option<crate::signal::Signal<bool>>,
327    /// User-bound signal that the framework writes whenever the
328    /// hovered widget is a strict descendant of this node. See
329    /// [`HandlerSet::hover_within`].
330    pub(crate) hover_within: Option<crate::signal::Signal<bool>>,
331    /// User-bound visibility binding (`bool` / `Signal<bool>` / `Prop<bool>`).
332    /// Applied at insertion via `WidgetTree::visible_when`, exactly like the
333    /// `ctx.visible_when(id, ..)` form, so `teksu!` can write `visible_when: sig`
334    /// as a plain widget property. See [`HandlerSet::visible_when`].
335    pub(crate) visible_when: Option<Prop<bool>>,
336    /// Builder-level accessibility overrides. Mirrored to
337    /// `WidgetNode::access_overrides` at insertion. Action callbacks
338    /// (`actions`, `custom_actions`) are dispatched by
339    /// `event_dispatch_impl.rs` when handling
340    /// `WidgetEvent::AccessAction`, in addition to the user's
341    /// `on_access_action` / `on_access_action_request` handlers — so
342    /// builder order doesn't matter.
343    pub(crate) access: Option<Box<AccessibilityOverrides>>,
344    /// Subtree visibility / merge mode. Mirrored to
345    /// `WidgetNode::access_subtree`.
346    pub(crate) access_subtree: Option<AccessSubtreeMode>,
347}
348
349impl HandlerSet {
350    /// Create an empty handler set for use in `BuildContext::apply_self_handlers()`.
351    pub fn new() -> Self {
352        Self {
353            handlers: EventHandlers::new(),
354            focusable: None,
355            tab_index: None,
356            cursor: None,
357            clips_children: None,
358            ime: None,
359            event_pass_through: None,
360            gesture_dead_zone: None,
361            keyboard_capture: None,
362            hit_transparent: None,
363            context_menu_factory: None,
364            focus_within: None,
365            hover_within: None,
366            visible_when: None,
367            access: None,
368            access_subtree: None,
369        }
370    }
371
372    /// Get a `&mut` to the override block, lazily allocating it on first
373    /// access. Used by all `access_*` builder methods.
374    pub(crate) fn access_mut(&mut self) -> &mut AccessibilityOverrides {
375        self.access
376            .get_or_insert_with(|| Box::new(AccessibilityOverrides::default()))
377    }
378
379    // -- Builder methods (mirror WidgetWithHandlers) --
380
381    /// Set the on_tap handler. The closure receives a borrowed
382    /// [`TapEvent`] carrying the position in
383    /// widget-local coordinates, the finalising mouse button, and the
384    /// modifier state at that moment.
385    ///
386    /// Default acceptance is [`ButtonMask::PRIMARY`] — left-click only.
387    /// Use [`accept_tap_buttons`](Self::accept_tap_buttons) to widen
388    /// the set if you need right-click, middle-click, or auxiliary
389    /// buttons to fire this handler.
390    pub fn on_tap(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
391        self.handlers.on_tap = Some(Box::new(f));
392        self
393    }
394
395    /// Set the on_double_tap handler. See [`on_tap`](Self::on_tap) for
396    /// the callback contract.
397    pub fn on_double_tap(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
398        self.handlers.on_double_tap = Some(Box::new(f));
399        self
400    }
401
402    /// Set the on_triple_tap handler — fires on the third click within the
403    /// recognizer's window (same 300 ms / 10 px defaults as double tap).
404    /// Runs independently of `on_double_tap` via cooperative gesture
405    /// recognizers (`GestureRecognizer::resets_on_peer_recognition`).
406    pub fn on_triple_tap(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
407        self.handlers.on_triple_tap = Some(Box::new(f));
408        self
409    }
410
411    /// Set the on_long_press handler. The callback receives a borrowed
412    /// [`TapEvent`] whose modifiers are
413    /// captured from the held `Down` (since long-press recognises on a
414    /// timer before any `Up`).
415    pub fn on_long_press(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
416        self.handlers.on_long_press = Some(Box::new(f));
417        self
418    }
419
420    /// Restrict (or extend) the set of pointer buttons that fire
421    /// [`on_tap`](Self::on_tap). Default is [`ButtonMask::PRIMARY`]
422    /// (left-click only). Pass `ButtonMask::ALL` or
423    /// `ButtonMask::PRIMARY | ButtonMask::SECONDARY`, etc.
424    pub fn accept_tap_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
425        self.handlers.tap_buttons = Some(mask.into());
426        self
427    }
428
429    /// Restrict (or extend) the set of pointer buttons that fire
430    /// [`on_double_tap`](Self::on_double_tap). Default
431    /// [`ButtonMask::PRIMARY`].
432    pub fn accept_double_tap_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
433        self.handlers.double_tap_buttons = Some(mask.into());
434        self
435    }
436
437    /// Restrict (or extend) the set of pointer buttons that fire
438    /// [`on_triple_tap`](Self::on_triple_tap). Default
439    /// [`ButtonMask::PRIMARY`].
440    pub fn accept_triple_tap_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
441        self.handlers.triple_tap_buttons = Some(mask.into());
442        self
443    }
444
445    /// Restrict (or extend) the set of pointer buttons that fire
446    /// [`on_long_press`](Self::on_long_press). Default
447    /// [`ButtonMask::PRIMARY`].
448    pub fn accept_long_press_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
449        self.handlers.long_press_buttons = Some(mask.into());
450        self
451    }
452
453    /// Set the on_hover handler.
454    pub fn on_hover(mut self, f: impl FnMut(bool, &mut EventContext) + 'static) -> Self {
455        self.handlers.on_hover = Some(Box::new(f));
456        self
457    }
458
459    /// Set the on_key handler.
460    pub fn on_key(
461        mut self,
462        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
463    ) -> Self {
464        self.handlers.on_key = Some(Box::new(f));
465        self
466    }
467
468    /// Set the strict-ancestor key preview handler. Fires on every
469    /// ancestor of the focused widget (root → parent-of-target)
470    /// before the focused widget's `on_key` runs. Return
471    /// `EventResponse::Handled` to consume the event.
472    pub fn on_key_preview(
473        mut self,
474        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
475    ) -> Self {
476        self.handlers.on_key_preview = Some(Box::new(f));
477        self
478    }
479
480    /// Set the on_drag handler (gesture-based drag). The closure receives
481    /// a [`DragPhase`] — `Started`, then zero or more `Moved`, then
482    /// `Ended`.
483    pub fn on_drag(mut self, f: impl FnMut(DragPhase, &mut EventContext) + 'static) -> Self {
484        self.handlers.on_drag = Some(Box::new(f));
485        self
486    }
487
488    /// Set the on_swipe handler. Fires once per swipe with the direction
489    /// and velocity (pixels/second).
490    pub fn on_swipe(
491        mut self,
492        f: impl FnMut(SwipeDirection, f32, &mut EventContext) + 'static,
493    ) -> Self {
494        self.handlers.on_swipe = Some(Box::new(f));
495        self
496    }
497
498    /// Set the on_pinch handler. On desktop the phases are produced from
499    /// OS trackpad gestures (winit `TouchpadMagnify` / `RotationGesture`).
500    pub fn on_pinch(mut self, f: impl FnMut(PinchPhase, &mut EventContext) + 'static) -> Self {
501        self.handlers.on_pinch = Some(Box::new(f));
502        self
503    }
504
505    /// Set the on_focus handler. `f` is called with `true` on focus gain and
506    /// `false` on focus loss.
507    ///
508    /// **WCAG 3.2.1 (On Focus).** Use this only to update *local* visual or
509    /// reactive state. Do NOT open a window, navigate, submit, or otherwise
510    /// change context from here: a context change triggered merely by a control
511    /// receiving focus is a Success Criterion 3.2.1 failure — keyboard users
512    /// tabbing through the UI would trigger it unexpectedly. (A debug-only guard
513    /// warns if `ctx.open_window(...)` / `ctx.focus_window(...)` is called from
514    /// inside focus dispatch.)
515    pub fn on_focus(mut self, f: impl FnMut(bool, &mut EventContext) + 'static) -> Self {
516        self.handlers.on_focus = Some(Box::new(f));
517        self
518    }
519
520    /// Set the on_pointer_event handler (low-level escape hatch).
521    pub fn on_pointer_event(
522        mut self,
523        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
524    ) -> Self {
525        self.handlers.on_pointer_event = Some(Box::new(f));
526        self
527    }
528
529    /// Set the on_scroll handler.
530    pub fn on_scroll(
531        mut self,
532        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
533    ) -> Self {
534        self.handlers.on_scroll = Some(Box::new(f));
535        self
536    }
537
538    /// Set the on_access_action handler.
539    pub fn on_access_action(
540        mut self,
541        f: impl FnMut(accesskit::Action, &mut EventContext) -> EventResponse + 'static,
542    ) -> Self {
543        self.handlers.on_access_action = Some(Box::new(f));
544        self
545    }
546
547    /// Set the full AccessKit action-request handler. Receives the
548    /// action, target NodeId (may be a synthetic widget-emitted
549    /// child), and optional `ActionData` payload (e.g.
550    /// `SetTextSelection(TextSelection)` or `Value(Box<str>)`).
551    /// When this slot is set it's called INSTEAD of
552    /// `on_access_action` for the same event.
553    pub fn on_access_action_request(
554        mut self,
555        f: impl FnMut(
556            accesskit::Action,
557            accesskit::NodeId,
558            Option<accesskit::ActionData>,
559            &mut EventContext,
560        ) -> EventResponse
561        + 'static,
562    ) -> Self {
563        self.handlers.on_access_action_request = Some(Box::new(f));
564        self
565    }
566
567    /// Set the focusable flag.
568    pub fn focusable(mut self, focusable: bool) -> Self {
569        self.focusable = Some(focusable);
570        self
571    }
572
573    /// Set the cursor icon.
574    pub fn cursor(mut self, cursor: CursorIcon) -> Self {
575        self.cursor = Some(cursor);
576        self
577    }
578
579    /// Set the clips_children flag.
580    pub fn clips_children(mut self, clips: bool) -> Self {
581        self.clips_children = Some(clips);
582        self
583    }
584
585    /// Declare this node a text-input surface, enabling the OS input method
586    /// (with `ctx`'s purpose) while it is focused. Leaving it unset (the
587    /// default) means no OS IME. The platform reads the focused node's
588    /// descriptor at focus-change time. See [`crate::ime`].
589    pub fn ime_input(mut self, ctx: crate::ime::ImeContext) -> Self {
590        self.ime = Some(ctx);
591        self
592    }
593
594    /// Make the widget invisible to pointer hit-testing. With
595    /// `pass_through = true`, pointer events traverse this node as if
596    /// it were not there — useful for purely decorative overlays that
597    /// must not absorb clicks (the debug inspector's `HighlightLayer`
598    /// and `HoverProbe` use this).
599    pub fn event_pass_through(mut self, pass_through: bool) -> Self {
600        self.event_pass_through = Some(pass_through);
601        self
602    }
603
604    /// Mark this widget's subtree a **gesture dead zone**: a pointer press
605    /// inside it must not arm a drag/swipe recognizer on any ancestor above
606    /// it. Use to let interactive controls (buttons, a `⋮` menu) sit inside a
607    /// draggable / swipeable container (a dock-panel header, a card, a list
608    /// row) without a few px of click jitter starting the ancestor's drag.
609    /// The container's own drag still works everywhere else. Honored by
610    /// `arm_drag_observers`; see the `DeadZone` wrapper widget.
611    pub fn gesture_dead_zone(mut self, dead_zone: bool) -> Self {
612        self.gesture_dead_zone = Some(dead_zone);
613        self
614    }
615
616    /// Mark this widget a **keyboard capture** surface: while it holds
617    /// focus, every `KeyDown` is delivered straight to its `on_key`
618    /// handler, bypassing shortcut → intent → action resolution. Use for
619    /// a terminal emulator that must forward `Ctrl+C` / `Ctrl+W` /
620    /// `Alt+<letter>` to a child process instead of triggering the host
621    /// app's shortcuts, a game viewport, or a modal text surface.
622    ///
623    /// # The escape contract
624    ///
625    /// **`Ctrl+Tab` / `Ctrl+Shift+Tab` are reserved and always move focus
626    /// out.** The dispatcher cycles focus on that chord before the capture
627    /// node is consulted, so a capture surface cannot become a keyboard trap
628    /// (WCAG 2.1.2) however greedily its `on_key` behaves. Do not bind them.
629    ///
630    /// Nothing else is reserved. In particular Escape is **not**: overlay
631    /// back-navigation runs first only while an overlay is actually open, so
632    /// a focused capture surface with no overlay above it does receive
633    /// Escape and may consume it. See
634    /// [`super::arena::WidgetNode::keyboard_capture`].
635    pub fn keyboard_capture(mut self, capture: bool) -> Self {
636        self.keyboard_capture = Some(capture);
637        self
638    }
639
640    /// Make this widget AND its whole subtree invisible to pointer
641    /// hit-testing. Stronger than [`event_pass_through`](Self::event_pass_through):
642    /// that one keeps descendants hittable, this one excludes them too.
643    /// For purely decorative composite overlays (a count badge over a
644    /// button, a watermark) whose own children would otherwise swallow
645    /// the click meant for the control underneath.
646    pub fn hit_transparent(mut self, transparent: bool) -> Self {
647        self.hit_transparent = Some(transparent);
648        self
649    }
650
651    /// Bind a user-owned `Signal<bool>` that the framework will set
652    /// to `true` whenever the focused widget is a *strict descendant*
653    /// of this node, and `false` otherwise. Useful for unified focus
654    /// halos around composite widgets (a chat composer that highlights
655    /// when its `RichTextEditor` or "Send" button is focused, a
656    /// `Panel` wrapping a `SpinBox`, etc).
657    ///
658    /// Strict-ancestors only — a widget that *is* itself focused does
659    /// not also see its own `focus_within` signal flipped to `true`.
660    /// Combine with `on_focus` if you want both behaviours.
661    pub fn focus_within(mut self, signal: crate::signal::Signal<bool>) -> Self {
662        self.focus_within = Some(signal);
663        self
664    }
665
666    /// Bind a user-owned `Signal<bool>` that the framework will set
667    /// to `true` whenever the hovered widget is a *strict descendant*
668    /// of this node. Symmetric to [`focus_within`](Self::focus_within).
669    pub fn hover_within(mut self, signal: crate::signal::Signal<bool>) -> Self {
670        self.hover_within = Some(signal);
671        self
672    }
673
674    /// Bind this node's visibility to a `bool` / `Signal<bool>` / `Prop<bool>`.
675    /// A bound value shows/hides the node reactively (registered at
676    /// `Relayout`). Equivalent to `ctx.visible_when(id, ..)`; exposed as a
677    /// builder method so `teksu!` can write `visible_when: sig` as a property.
678    pub fn visible_when(mut self, state: impl Into<Prop<bool>>) -> Self {
679        self.visible_when = Some(state.into());
680        self
681    }
682
683    /// Set a context-menu factory. See [`ContextMenuFactory`] for the
684    /// full contract: the closure receives the click position
685    /// (widget-local) and a full [`EventContext`], and returns
686    /// `Some(menu)` to mount or `None` to decline (falling through to
687    /// the nearest ancestor with a factory).
688    pub fn context_menu(
689        mut self,
690        factory: impl Fn(Point, &mut EventContext) -> Option<Box<dyn Widget>> + 'static,
691    ) -> Self {
692        self.context_menu_factory = Some(Box::new(factory));
693        self
694    }
695
696    /// Set the drag hover handler. Called when a drag payload hovers over this widget.
697    /// Return `DropFeedback` to indicate acceptance and visual feedback.
698    pub fn on_drag_hover(
699        mut self,
700        f: impl FnMut(
701            &crate::drag_payload::DragPayload,
702            teksilo_canvas::Point,
703            &mut EventContext,
704        ) -> crate::drag_state::DropFeedback
705        + 'static,
706    ) -> Self {
707        self.handlers.on_drag_hover = Some(Box::new(f));
708        self
709    }
710
711    /// Set the drag-leave handler. Fires when a drag that was over this
712    /// widget moves to another target, completes (drop on any target), or
713    /// is cancelled. Widgets that stash transient feedback state in
714    /// `on_drag_hover` must clear it here.
715    pub fn on_drag_leave(mut self, f: impl FnMut(&mut EventContext) + 'static) -> Self {
716        self.handlers.on_drag_leave = Some(Box::new(f));
717        self
718    }
719
720    /// Set the per-frame drag-tick handler. Fires once per frame while a
721    /// drag is active and this widget is the current drop target. The
722    /// closure receives the current pointer position in widget-local
723    /// coordinates. Use for behaviours that must keep running even when
724    /// the pointer is stationary — viewport-edge auto-scroll and
725    /// spring-loaded folders.
726    pub fn on_drag_tick(
727        mut self,
728        f: impl FnMut(teksilo_canvas::Point, &mut EventContext) + 'static,
729    ) -> Self {
730        self.handlers.on_drag_tick = Some(Box::new(f));
731        self
732    }
733
734    /// Set the drop handler. Called when a payload is dropped on this widget.
735    /// Return `true` if the drop was accepted.
736    pub fn on_drop(
737        mut self,
738        f: impl FnMut(
739            crate::drag_payload::DragPayload,
740            teksilo_canvas::Point,
741            &mut EventContext,
742        ) -> bool
743        + 'static,
744    ) -> Self {
745        self.handlers.on_drop = Some(Box::new(f));
746        self
747    }
748
749    /// Set the drag-ended handler on a drag **source**. Fires when a drag
750    /// this widget started ends — dropped on an in-app target, exported to
751    /// another application via the OS (copy / move), or cancelled. Use it to
752    /// react to the outcome, e.g. remove the dragged item on a
753    /// [`DropOutcome::OsMove`](crate::drag_payload::DropOutcome::OsMove).
754    pub fn on_drag_ended(
755        mut self,
756        f: impl FnMut(crate::drag_payload::DropOutcome, &mut EventContext) + 'static,
757    ) -> Self {
758        self.handlers.on_drag_ended = Some(Box::new(f));
759        self
760    }
761}
762
763impl Default for HandlerSet {
764    fn default() -> Self {
765        Self::new()
766    }
767}
768
769impl std::fmt::Debug for HandlerSet {
770    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
771        f.debug_struct("HandlerSet")
772            .field("handlers", &self.handlers)
773            .field("focusable", &self.focusable)
774            .field("tab_index", &self.tab_index)
775            .field("cursor", &self.cursor)
776            .finish()
777    }
778}
779
780// ---------------------------------------------------------------------------
781// WidgetWithHandlers<W> — wrapper storing widget + accumulated handlers
782// ---------------------------------------------------------------------------
783
784/// A widget wrapped with attached event handlers and framework metadata.
785/// Created by calling builder methods from `WidgetBuilder` on any widget.
786pub struct WidgetWithHandlers<W: Widget> {
787    pub(crate) widget: W,
788    pub(crate) handler_set: HandlerSet,
789}
790
791impl<W: Widget> WidgetWithHandlers<W> {
792    fn new(widget: W) -> Self {
793        Self {
794            widget,
795            handler_set: HandlerSet::new(),
796        }
797    }
798
799    /// Take the handler set out, leaving defaults.
800    pub(crate) fn take_handler_set(&mut self) -> HandlerSet {
801        std::mem::take(&mut self.handler_set)
802    }
803
804    // -- Gesture handlers --
805
806    pub fn on_tap(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
807        self.handler_set.handlers.on_tap = Some(Box::new(f));
808        self
809    }
810
811    pub fn on_double_tap(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
812        self.handler_set.handlers.on_double_tap = Some(Box::new(f));
813        self
814    }
815
816    pub fn on_triple_tap(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
817        self.handler_set.handlers.on_triple_tap = Some(Box::new(f));
818        self
819    }
820
821    pub fn on_long_press(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
822        self.handler_set.handlers.on_long_press = Some(Box::new(f));
823        self
824    }
825
826    /// Restrict (or extend) the set of pointer buttons that fire
827    /// `on_tap`. Default is [`ButtonMask::PRIMARY`].
828    pub fn accept_tap_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
829        self.handler_set.handlers.tap_buttons = Some(mask.into());
830        self
831    }
832
833    /// Restrict (or extend) the set of pointer buttons that fire
834    /// `on_double_tap`. Default [`ButtonMask::PRIMARY`].
835    pub fn accept_double_tap_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
836        self.handler_set.handlers.double_tap_buttons = Some(mask.into());
837        self
838    }
839
840    /// Restrict (or extend) the set of pointer buttons that fire
841    /// `on_triple_tap`. Default [`ButtonMask::PRIMARY`].
842    pub fn accept_triple_tap_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
843        self.handler_set.handlers.triple_tap_buttons = Some(mask.into());
844        self
845    }
846
847    /// Restrict (or extend) the set of pointer buttons that fire
848    /// `on_long_press`. Default [`ButtonMask::PRIMARY`].
849    pub fn accept_long_press_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
850        self.handler_set.handlers.long_press_buttons = Some(mask.into());
851        self
852    }
853
854    pub fn on_drag(mut self, f: impl FnMut(DragPhase, &mut EventContext) + 'static) -> Self {
855        self.handler_set.handlers.on_drag = Some(Box::new(f));
856        self
857    }
858
859    pub fn on_swipe(
860        mut self,
861        f: impl FnMut(SwipeDirection, f32, &mut EventContext) + 'static,
862    ) -> Self {
863        self.handler_set.handlers.on_swipe = Some(Box::new(f));
864        self
865    }
866
867    pub fn on_pinch(mut self, f: impl FnMut(PinchPhase, &mut EventContext) + 'static) -> Self {
868        self.handler_set.handlers.on_pinch = Some(Box::new(f));
869        self
870    }
871
872    // -- Focus and keyboard --
873
874    pub fn on_focus(mut self, f: impl FnMut(bool, &mut EventContext) + 'static) -> Self {
875        self.handler_set.handlers.on_focus = Some(Box::new(f));
876        self
877    }
878
879    pub fn on_key(
880        mut self,
881        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
882    ) -> Self {
883        self.handler_set.handlers.on_key = Some(Box::new(f));
884        self
885    }
886
887    /// Set the strict-ancestor key preview handler. See
888    /// [`HandlerSet::on_key_preview`].
889    pub fn on_key_preview(
890        mut self,
891        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
892    ) -> Self {
893        self.handler_set.handlers.on_key_preview = Some(Box::new(f));
894        self
895    }
896
897    pub fn focusable(mut self, focusable: bool) -> Self {
898        self.handler_set.focusable = Some(focusable);
899        self
900    }
901
902    pub fn tab_index(mut self, index: i32) -> Self {
903        self.handler_set.tab_index = Some(index);
904        self
905    }
906
907    // -- Pointer (low-level escape hatch) --
908
909    pub fn on_pointer_event(
910        mut self,
911        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
912    ) -> Self {
913        self.handler_set.handlers.on_pointer_event = Some(Box::new(f));
914        self
915    }
916
917    pub fn on_hover(mut self, f: impl FnMut(bool, &mut EventContext) + 'static) -> Self {
918        self.handler_set.handlers.on_hover = Some(Box::new(f));
919        self
920    }
921
922    pub fn cursor(mut self, cursor: CursorIcon) -> Self {
923        self.handler_set.cursor = Some(cursor);
924        self
925    }
926
927    // -- Scroll --
928
929    pub fn on_scroll(
930        mut self,
931        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
932    ) -> Self {
933        self.handler_set.handlers.on_scroll = Some(Box::new(f));
934        self
935    }
936
937    // -- Accessibility actions --
938
939    pub fn on_access_action(
940        mut self,
941        f: impl FnMut(accesskit::Action, &mut EventContext) -> EventResponse + 'static,
942    ) -> Self {
943        self.handler_set.handlers.on_access_action = Some(Box::new(f));
944        self
945    }
946
947    pub fn on_access_action_request(
948        mut self,
949        f: impl FnMut(
950            accesskit::Action,
951            accesskit::NodeId,
952            Option<accesskit::ActionData>,
953            &mut EventContext,
954        ) -> EventResponse
955        + 'static,
956    ) -> Self {
957        self.handler_set.handlers.on_access_action_request = Some(Box::new(f));
958        self
959    }
960
961    // -- Framework-level properties --
962
963    pub fn clips_children(mut self, clips: bool) -> Self {
964        self.handler_set.clips_children = Some(clips);
965        self
966    }
967
968    /// Declare this node a text-input surface, enabling the OS input method
969    /// (with `ctx`'s purpose) while it is focused. See [`crate::ime`].
970    pub fn ime_input(mut self, ctx: crate::ime::ImeContext) -> Self {
971        self.handler_set.ime = Some(ctx);
972        self
973    }
974
975    /// Make the widget invisible to pointer hit-testing. See
976    /// [`HandlerSet::event_pass_through`].
977    pub fn event_pass_through(mut self, pass_through: bool) -> Self {
978        self.handler_set.event_pass_through = Some(pass_through);
979        self
980    }
981
982    /// Mark this widget's subtree a gesture dead zone. See
983    /// [`HandlerSet::gesture_dead_zone`].
984    pub fn gesture_dead_zone(mut self, dead_zone: bool) -> Self {
985        self.handler_set.gesture_dead_zone = Some(dead_zone);
986        self
987    }
988
989    /// Mark this widget a keyboard capture surface: while focused, every
990    /// `KeyDown` bypasses shortcut resolution and reaches its `on_key`
991    /// handler (terminals, game viewports). See
992    /// [`HandlerSet::keyboard_capture`].
993    pub fn keyboard_capture(mut self, capture: bool) -> Self {
994        self.handler_set.keyboard_capture = Some(capture);
995        self
996    }
997
998    /// Make this widget and its whole subtree invisible to pointer
999    /// hit-testing. See [`HandlerSet::hit_transparent`].
1000    pub fn hit_transparent(mut self, transparent: bool) -> Self {
1001        self.handler_set.hit_transparent = Some(transparent);
1002        self
1003    }
1004
1005    /// Set a context-menu factory. See
1006    /// [`HandlerSet::context_menu`] for the full contract.
1007    pub fn context_menu(
1008        mut self,
1009        factory: impl Fn(Point, &mut EventContext) -> Option<Box<dyn Widget>> + 'static,
1010    ) -> Self {
1011        self.handler_set.context_menu_factory = Some(Box::new(factory));
1012        self
1013    }
1014
1015    /// Bind a `Signal<bool>` the framework writes when a strict
1016    /// descendant has focus. See [`HandlerSet::focus_within`].
1017    pub fn focus_within(mut self, signal: crate::signal::Signal<bool>) -> Self {
1018        self.handler_set.focus_within = Some(signal);
1019        self
1020    }
1021
1022    /// Bind a `Signal<bool>` the framework writes when a strict
1023    /// descendant is hovered. See [`HandlerSet::hover_within`].
1024    pub fn hover_within(mut self, signal: crate::signal::Signal<bool>) -> Self {
1025        self.handler_set.hover_within = Some(signal);
1026        self
1027    }
1028
1029    /// Bind this node's visibility. See [`HandlerSet::visible_when`].
1030    pub fn visible_when(mut self, state: impl Into<Prop<bool>>) -> Self {
1031        self.handler_set.visible_when = Some(state.into());
1032        self
1033    }
1034
1035    /// Set the drag hover handler. Called when a drag payload hovers over this widget.
1036    pub fn on_drag_hover(
1037        mut self,
1038        f: impl FnMut(
1039            &crate::drag_payload::DragPayload,
1040            teksilo_canvas::Point,
1041            &mut EventContext,
1042        ) -> crate::drag_state::DropFeedback
1043        + 'static,
1044    ) -> Self {
1045        self.handler_set.handlers.on_drag_hover = Some(Box::new(f));
1046        self
1047    }
1048
1049    /// Set the drag-leave handler. See [`HandlerSet::on_drag_leave`].
1050    pub fn on_drag_leave(mut self, f: impl FnMut(&mut EventContext) + 'static) -> Self {
1051        self.handler_set.handlers.on_drag_leave = Some(Box::new(f));
1052        self
1053    }
1054
1055    /// Set the per-frame drag-tick handler. See [`HandlerSet::on_drag_tick`].
1056    pub fn on_drag_tick(
1057        mut self,
1058        f: impl FnMut(teksilo_canvas::Point, &mut EventContext) + 'static,
1059    ) -> Self {
1060        self.handler_set.handlers.on_drag_tick = Some(Box::new(f));
1061        self
1062    }
1063
1064    /// Set the drop handler. Called when a payload is dropped on this widget.
1065    pub fn on_drop(
1066        mut self,
1067        f: impl FnMut(
1068            crate::drag_payload::DragPayload,
1069            teksilo_canvas::Point,
1070            &mut EventContext,
1071        ) -> bool
1072        + 'static,
1073    ) -> Self {
1074        self.handler_set.handlers.on_drop = Some(Box::new(f));
1075        self
1076    }
1077
1078    /// Set the drag-ended handler on a drag source. See
1079    /// [`HandlerSet::on_drag_ended`].
1080    pub fn on_drag_ended(
1081        mut self,
1082        f: impl FnMut(crate::drag_payload::DropOutcome, &mut EventContext) + 'static,
1083    ) -> Self {
1084        self.handler_set.handlers.on_drag_ended = Some(Box::new(f));
1085        self
1086    }
1087
1088    // ── Accessibility overrides ────────────────────────────────────────
1089    //
1090    // The user-visible string methods take `impl Into<Prop<String>>` so
1091    // they stay reactive. With the `i18n` feature, `LocalizedString`
1092    // (produced by `tr!(...)`) provides `From<LocalizedString> for
1093    // Prop<String>`, which yields a locale-observing `Prop::Bound`, so
1094    // `.access_label(tr!(save()))` follows the locale. A bare `&str`
1095    // does NOT convert to `Prop<String>`, so untranslated literals must
1096    // go through `lit!(...)` (downstream crates) or the `_literal`
1097    // twins (which store `Prop::Static` — the only literal path
1098    // reachable from within `teksilo-core`).
1099
1100    /// Override the accessibility label (`Node::label`) of this widget.
1101    /// Replaces whatever the inner widget emitted via `set_name`.
1102    ///
1103    /// Accepts any `impl Into<Prop<String>>`. With the `i18n` feature,
1104    /// `LocalizedString` (produced by `tr!(...)`)
1105    /// implements `From<LocalizedString> for Prop<String>`, so
1106    /// `.access_label(tr!(save()))` stays reactive — the announced
1107    /// value re-resolves on locale change (the accessibility tree
1108    /// re-walks via `sync_accessibility`).
1109    pub fn access_label(mut self, label: impl Into<Prop<String>>) -> Self {
1110        self.handler_set.access_mut().label = Some(label.into());
1111        self
1112    }
1113
1114    /// `#[doc(hidden)]` grep marker for explicitly-untranslated label
1115    /// strings — the same convention as `Button::new_literal`. Stores a
1116    /// `Prop::Static`. The distinct name makes untranslated call sites
1117    /// greppable as a one-pass audit, and it's the literal path
1118    /// reachable from within `teksilo-core` (where `lit!` isn't usable).
1119    #[doc(hidden)]
1120    pub fn access_label_literal(self, label: impl Into<String>) -> Self {
1121        self.access_label(Prop::Static(label.into()))
1122    }
1123
1124    /// Override the accessibility description (`Node::description`).
1125    /// Same conversion rules as `access_label`.
1126    pub fn access_description(mut self, description: impl Into<Prop<String>>) -> Self {
1127        self.handler_set.access_mut().description = Some(description.into());
1128        self
1129    }
1130
1131    #[doc(hidden)]
1132    pub fn access_description_literal(self, description: impl Into<String>) -> Self {
1133        self.access_description(Prop::Static(description.into()))
1134    }
1135
1136    /// Long-form context hint. Alias of `access_description` —
1137    /// AccessKit has no separate hint slot (SwiftUI's split is
1138    /// VoiceOver-specific). Provided for SwiftUI parity.
1139    pub fn access_hint(self, hint: impl Into<Prop<String>>) -> Self {
1140        self.access_description(hint)
1141    }
1142
1143    #[doc(hidden)]
1144    pub fn access_hint_literal(self, hint: impl Into<String>) -> Self {
1145        self.access_description(Prop::Static(hint.into()))
1146    }
1147
1148    /// Override the accessibility value (`Node::value`).
1149    /// Same conversion rules as `access_label`.
1150    pub fn access_value(mut self, value: impl Into<Prop<String>>) -> Self {
1151        self.handler_set.access_mut().value = Some(value.into());
1152        self
1153    }
1154
1155    #[doc(hidden)]
1156    pub fn access_value_literal(self, value: impl Into<String>) -> Self {
1157        self.access_value(Prop::Static(value.into()))
1158    }
1159
1160    /// Override the accessibility role.
1161    pub fn access_role(mut self, role: accesskit::Role) -> Self {
1162        self.handler_set.access_mut().role = Some(role);
1163        self
1164    }
1165
1166    /// Hide (or un-hide) this node from assistive technologies. Accepts a
1167    /// plain `bool`, a `Signal<bool>`, or a `Prop<bool>`: a bound value makes
1168    /// the node appear/disappear from the AT tree reactively (the binding is
1169    /// registered at `AccessibilityOnly`, so the tree re-walks on change).
1170    /// `false` un-sets a hidden state the inner widget may have emitted
1171    /// unconditionally (e.g. `Panel::a11y_presentational`).
1172    pub fn access_hidden(mut self, hidden: impl Into<Prop<bool>>) -> Self {
1173        self.handler_set.access_mut().hidden = Some(hidden.into());
1174        self
1175    }
1176
1177    /// Mark (or un-mark) this widget as disabled for AT. `false`
1178    /// clears both widget-emitted disabled state AND the framework's
1179    /// arena-driven disabled gate at
1180    /// `accessibility_impl::build_accessibility_recursive`.
1181    pub fn access_disabled(mut self, disabled: bool) -> Self {
1182        self.handler_set.access_mut().disabled = Some(disabled);
1183        self
1184    }
1185
1186    /// Stable test/debug identifier (`Node::author_id`). Not
1187    /// user-visible — used by accessibility inspectors and UI tests.
1188    pub fn access_identifier(mut self, id: impl Into<String>) -> Self {
1189        self.handler_set.access_mut().identifier = Some(id.into());
1190        self
1191    }
1192
1193    /// Append a `controls` relationship. The target widget's NodeId
1194    /// is included in this node's `aria-controls`-equivalent list.
1195    pub fn access_controls(mut self, target: WidgetId) -> Self {
1196        self.handler_set.access_mut().controls.push(target);
1197        self
1198    }
1199
1200    /// Append a `described_by` relationship.
1201    pub fn access_described_by(mut self, target: WidgetId) -> Self {
1202        self.handler_set.access_mut().described_by.push(target);
1203        self
1204    }
1205
1206    /// Append a `labelled_by` relationship.
1207    pub fn access_labelled_by(mut self, target: WidgetId) -> Self {
1208        self.handler_set.access_mut().labelled_by.push(target);
1209        self
1210    }
1211
1212    /// Set the live-region politeness (`Node::live`).
1213    pub fn access_live(mut self, mode: accesskit::Live) -> Self {
1214        self.handler_set.access_mut().live = Some(mode);
1215        self
1216    }
1217
1218    /// Mark this node as the current item within its container
1219    /// (`aria-current`).
1220    pub fn access_current(mut self, current: accesskit::AriaCurrent) -> Self {
1221        self.handler_set.access_mut().aria_current = Some(current);
1222        self
1223    }
1224
1225    /// Pre-formatted shortcut announcement (e.g. `"Ctrl+S"`). Used for
1226    /// chords NOT routed through the `Shortcut` system — platform-native
1227    /// keys, app-internal hotkeys not exposed to user rebinding. For
1228    /// `Shortcut`-registered chords prefer
1229    /// [`access_shortcut_id`](Self::access_shortcut_id), which tracks
1230    /// rebinds automatically.
1231    pub fn access_shortcut_literal(mut self, shortcut: impl Into<String>) -> Self {
1232        self.handler_set.access_mut().shortcut = Some(shortcut.into());
1233        self
1234    }
1235
1236    /// Bind the announced shortcut to a registered `Shortcut` id (the
1237    /// same id you pass to `Shortcut::new("app.save")`). The
1238    /// accessibility tree walker resolves the current keystroke from
1239    /// `WidgetTree::shortcut_registry()` at AT-build time, formats it
1240    /// via `KeyStroke::Display` (`"Ctrl+S"`), and writes it to
1241    /// `Node::keyboard_shortcut`. Auto-refreshes on rebind.
1242    ///
1243    /// If the registry has no entry for `id` (no widget registered the
1244    /// shortcut yet), the announcement is omitted — same fallback as
1245    /// `MenuItem::for_shortcut(...)`.
1246    pub fn access_shortcut_id(mut self, id: impl Into<String>) -> Self {
1247        self.handler_set.access_mut().shortcut_id = Some(id.into());
1248        self
1249    }
1250
1251    /// Indicate that activating this widget pops up a menu / listbox /
1252    /// dialog (`aria-haspopup`).
1253    pub fn access_has_popup(mut self, kind: accesskit::HasPopup) -> Self {
1254        self.handler_set.access_mut().has_popup = Some(kind);
1255        self
1256    }
1257
1258    /// Override orientation (`Node::orientation`) — used on sliders,
1259    /// scrollbars, separators.
1260    pub fn access_orientation(mut self, orientation: accesskit::Orientation) -> Self {
1261        self.handler_set.access_mut().orientation = Some(orientation);
1262        self
1263    }
1264
1265    /// Prune all descendants from the accessibility tree. The widget's
1266    /// own AT node is still emitted; only children disappear. Use for
1267    /// purely decorative composites. Flutter's `excludeSemantics: true`.
1268    pub fn access_exclude_subtree(mut self) -> Self {
1269        self.handler_set.access_subtree = Some(AccessSubtreeMode::Exclude);
1270        self
1271    }
1272
1273    /// Lift descendants' labels / descriptions / values / actions into
1274    /// this widget's AT node, then prune the descendants. The whole
1275    /// composite reads as a single AT element. Flutter's
1276    /// `mergeAllDescendants: true` and SwiftUI's
1277    /// `.accessibilityElement(children: .combine)`.
1278    pub fn access_merge_subtree(mut self) -> Self {
1279        self.handler_set.access_subtree = Some(AccessSubtreeMode::Merge);
1280        self
1281    }
1282
1283    /// Set an explicit subtree mode.
1284    pub fn access_subtree(mut self, mode: AccessSubtreeMode) -> Self {
1285        self.handler_set.access_subtree = Some(mode);
1286        self
1287    }
1288
1289    /// Override `Node::numeric_value`.
1290    pub fn access_numeric_value(mut self, value: f64) -> Self {
1291        self.handler_set.access_mut().numeric_value = Some(value);
1292        self
1293    }
1294
1295    /// Override `Node::min_numeric_value` and `max_numeric_value`.
1296    pub fn access_numeric_range(mut self, min: f64, max: f64) -> Self {
1297        let access = self.handler_set.access_mut();
1298        access.min_numeric_value = Some(min);
1299        access.max_numeric_value = Some(max);
1300        self
1301    }
1302
1303    /// Override `Node::numeric_value_step`.
1304    pub fn access_numeric_step(mut self, step: f64) -> Self {
1305        self.handler_set.access_mut().numeric_step = Some(step);
1306        self
1307    }
1308
1309    /// Advertise an accessibility action and the callback that fires
1310    /// when AT software invokes it. Multiple `access_action` calls
1311    /// register separate callbacks for distinct actions; calling twice
1312    /// with the same action records both — they fire in order.
1313    pub fn access_action<F>(mut self, action: accesskit::Action, handler: F) -> Self
1314    where
1315        F: FnMut(&mut EventContext) + 'static,
1316    {
1317        self.handler_set
1318            .access_mut()
1319            .actions
1320            .push((action, Box::new(handler)));
1321        self
1322    }
1323
1324    /// Suppress an action the inner widget emitted (e.g. neutralize
1325    /// `Action::Click` on a Button used purely as a layout shim).
1326    /// Applied after the widget's `accessibility()` runs but before
1327    /// override-advertised actions, so a subsequent `access_action`
1328    /// for the same action re-advertises it with the override-installed
1329    /// callback.
1330    pub fn access_remove_action(mut self, action: accesskit::Action) -> Self {
1331        self.handler_set.access_mut().removed_actions.push(action);
1332        self
1333    }
1334
1335    /// Advertise a custom-named action (SwiftUI parity:
1336    /// `.accessibilityAction(named:_:)`). The label is exposed
1337    /// verbatim by AT software (e.g. VoiceOver's Actions rotor).
1338    /// Accepts `tr!(...)` via `From<LocalizedString> for Prop<String>`
1339    /// in `teksilo-i18n`, so the announced name follows the locale.
1340    pub fn access_custom_action<F>(mut self, label: impl Into<Prop<String>>, handler: F) -> Self
1341    where
1342        F: FnMut(&mut EventContext) + 'static,
1343    {
1344        self.handler_set
1345            .access_mut()
1346            .custom_actions
1347            .push((label.into(), Box::new(handler)));
1348        self
1349    }
1350
1351    #[doc(hidden)]
1352    pub fn access_custom_action_literal<F>(self, label: impl Into<String>, handler: F) -> Self
1353    where
1354        F: FnMut(&mut EventContext) + 'static,
1355    {
1356        self.access_custom_action(Prop::Static(label.into()), handler)
1357    }
1358
1359    /// Final escape hatch — invoked after all typed override setters,
1360    /// with full `&mut AccessNodeBuilder` access (including
1361    /// `inner_mut()`). Use for synthetic-child surgery (rich text
1362    /// paragraphs, text runs) or any AccessKit field the typed
1363    /// surface doesn't cover.
1364    pub fn access_customize<F>(mut self, f: F) -> Self
1365    where
1366        F: Fn(&mut crate::accessibility::AccessNodeBuilder) + 'static,
1367    {
1368        self.handler_set.access_mut().customize = Some(Box::new(f));
1369        self
1370    }
1371}
1372
1373// Delegate all Widget trait methods to the inner widget.
1374impl<W: Widget> std::fmt::Debug for WidgetWithHandlers<W> {
1375    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1376        f.debug_struct("WidgetWithHandlers")
1377            .field("widget", &self.widget)
1378            .field("handler_set", &self.handler_set)
1379            .finish()
1380    }
1381}
1382
1383impl<W: Widget + 'static> Widget for WidgetWithHandlers<W> {
1384    fn build(
1385        &mut self,
1386        ctx: &mut crate::build_context::BuildContext,
1387    ) -> Vec<crate::widget_id::WidgetId> {
1388        self.widget.build(ctx)
1389    }
1390
1391    fn layout_response(
1392        &self,
1393        proposal: teksilo_canvas::SizeProposal,
1394        ctx: &crate::widget::LayoutContext,
1395    ) -> crate::widget::LayoutResponse {
1396        self.widget.layout_response(proposal, ctx)
1397    }
1398
1399    fn place_children(
1400        &self,
1401        bounds: teksilo_canvas::Rect,
1402        proposal: teksilo_canvas::SizeProposal,
1403        children: &mut [crate::widget::WidgetPlacement],
1404        ctx: &crate::widget::LayoutContext,
1405    ) {
1406        self.widget.place_children(bounds, proposal, children, ctx)
1407    }
1408
1409    fn paint(
1410        &self,
1411        bounds: teksilo_canvas::Rect,
1412        canvas: &mut teksilo_canvas::Canvas,
1413        ctx: &crate::widget::PaintContext,
1414    ) {
1415        self.widget.paint(bounds, canvas, ctx)
1416    }
1417
1418    fn accessibility(&self, builder: &mut crate::accessibility::AccessNodeBuilder) {
1419        self.widget.accessibility(builder)
1420    }
1421
1422    fn children(&self) -> Vec<crate::widget_id::WidgetId> {
1423        self.widget.children()
1424    }
1425
1426    fn as_any(&self) -> Option<&dyn std::any::Any> {
1427        self.widget.as_any()
1428    }
1429
1430    /// Mutable counterpart of [`as_any`](Widget::as_any), forwarded for the
1431    /// same reason it is.
1432    ///
1433    /// A composing container that reads a child's concrete type must see the
1434    /// same widget whether or not a builder method wrapped it. `MenuList` reads
1435    /// a `MenuItem` this way for its mnemonic, its type-ahead label, its radio
1436    /// group and its safe-triangle state; without this forward,
1437    /// `MenuItem::new(..).context_menu(..)` silently stops being a `MenuItem`
1438    /// to its parent — no error, just a row that lost all four.
1439    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
1440        self.widget.as_any_mut()
1441    }
1442
1443    fn clips_children(&self) -> bool {
1444        self.handler_set
1445            .clips_children
1446            .unwrap_or_else(|| self.widget.clips_children())
1447    }
1448
1449    fn take_handler_set(&mut self) -> Option<HandlerSet> {
1450        Some(self.take_handler_set())
1451    }
1452}
1453
1454// ---------------------------------------------------------------------------
1455// WidgetBuilder trait — the entry point
1456// ---------------------------------------------------------------------------
1457
1458/// Blanket trait providing attached handler methods for all Widget types.
1459/// The first builder method call wraps the widget in `WidgetWithHandlers`.
1460pub trait WidgetBuilder: Widget + Sized + 'static {
1461    fn on_tap(
1462        self,
1463        f: impl FnMut(&TapEvent, &mut EventContext) + 'static,
1464    ) -> WidgetWithHandlers<Self> {
1465        WidgetWithHandlers::new(self).on_tap(f)
1466    }
1467
1468    fn on_double_tap(
1469        self,
1470        f: impl FnMut(&TapEvent, &mut EventContext) + 'static,
1471    ) -> WidgetWithHandlers<Self> {
1472        WidgetWithHandlers::new(self).on_double_tap(f)
1473    }
1474
1475    fn on_triple_tap(
1476        self,
1477        f: impl FnMut(&TapEvent, &mut EventContext) + 'static,
1478    ) -> WidgetWithHandlers<Self> {
1479        WidgetWithHandlers::new(self).on_triple_tap(f)
1480    }
1481
1482    fn on_long_press(
1483        self,
1484        f: impl FnMut(&TapEvent, &mut EventContext) + 'static,
1485    ) -> WidgetWithHandlers<Self> {
1486        WidgetWithHandlers::new(self).on_long_press(f)
1487    }
1488
1489    /// Restrict (or extend) the set of pointer buttons that fire
1490    /// `on_tap`. Default is [`ButtonMask::PRIMARY`].
1491    fn accept_tap_buttons(self, mask: impl Into<ButtonMask>) -> WidgetWithHandlers<Self> {
1492        WidgetWithHandlers::new(self).accept_tap_buttons(mask)
1493    }
1494
1495    /// Restrict (or extend) the set of pointer buttons that fire
1496    /// `on_double_tap`. Default [`ButtonMask::PRIMARY`].
1497    fn accept_double_tap_buttons(self, mask: impl Into<ButtonMask>) -> WidgetWithHandlers<Self> {
1498        WidgetWithHandlers::new(self).accept_double_tap_buttons(mask)
1499    }
1500
1501    /// Restrict (or extend) the set of pointer buttons that fire
1502    /// `on_triple_tap`. Default [`ButtonMask::PRIMARY`].
1503    fn accept_triple_tap_buttons(self, mask: impl Into<ButtonMask>) -> WidgetWithHandlers<Self> {
1504        WidgetWithHandlers::new(self).accept_triple_tap_buttons(mask)
1505    }
1506
1507    /// Restrict (or extend) the set of pointer buttons that fire
1508    /// `on_long_press`. Default [`ButtonMask::PRIMARY`].
1509    fn accept_long_press_buttons(self, mask: impl Into<ButtonMask>) -> WidgetWithHandlers<Self> {
1510        WidgetWithHandlers::new(self).accept_long_press_buttons(mask)
1511    }
1512
1513    /// Dim this widget's subtree to `factor` opacity whenever the host window
1514    /// is **inactive** (not focused / occluded), restoring full opacity when it
1515    /// becomes active again. The opt-in, per-widget layer of the window-active
1516    /// appearance model — for custom content an app wants to fade back when its
1517    /// window isn't the active one. Stock widgets handle their own
1518    /// inactive appearance (caret hiding, selection desaturation) and need no
1519    /// wrapping. Layout- and a11y-transparent; the opacity snaps (no tween),
1520    /// which is correct under `prefers-reduced-motion`. See
1521    /// [`DimWhenInactive`](crate::dim_when_inactive::DimWhenInactive).
1522    fn dim_when_inactive(self, factor: f32) -> crate::dim_when_inactive::DimWhenInactive {
1523        crate::dim_when_inactive::DimWhenInactive::new()
1524            .child(self)
1525            .factor(factor)
1526    }
1527
1528    /// [`dim_when_inactive`](Self::dim_when_inactive) with the default factor
1529    /// ([`DEFAULT_DIM_FACTOR`](crate::dim_when_inactive::DEFAULT_DIM_FACTOR), 70 %).
1530    fn dim_when_inactive_default(self) -> crate::dim_when_inactive::DimWhenInactive {
1531        crate::dim_when_inactive::DimWhenInactive::new().child(self)
1532    }
1533
1534    fn on_drag(
1535        self,
1536        f: impl FnMut(DragPhase, &mut EventContext) + 'static,
1537    ) -> WidgetWithHandlers<Self> {
1538        WidgetWithHandlers::new(self).on_drag(f)
1539    }
1540
1541    fn on_swipe(
1542        self,
1543        f: impl FnMut(SwipeDirection, f32, &mut EventContext) + 'static,
1544    ) -> WidgetWithHandlers<Self> {
1545        WidgetWithHandlers::new(self).on_swipe(f)
1546    }
1547
1548    fn on_pinch(
1549        self,
1550        f: impl FnMut(PinchPhase, &mut EventContext) + 'static,
1551    ) -> WidgetWithHandlers<Self> {
1552        WidgetWithHandlers::new(self).on_pinch(f)
1553    }
1554
1555    fn on_focus(
1556        self,
1557        f: impl FnMut(bool, &mut EventContext) + 'static,
1558    ) -> WidgetWithHandlers<Self> {
1559        WidgetWithHandlers::new(self).on_focus(f)
1560    }
1561
1562    fn on_key(
1563        self,
1564        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
1565    ) -> WidgetWithHandlers<Self> {
1566        WidgetWithHandlers::new(self).on_key(f)
1567    }
1568
1569    /// Strict-ancestor key preview. See [`HandlerSet::on_key_preview`].
1570    fn on_key_preview(
1571        self,
1572        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
1573    ) -> WidgetWithHandlers<Self> {
1574        WidgetWithHandlers::new(self).on_key_preview(f)
1575    }
1576
1577    fn on_pointer_event(
1578        self,
1579        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
1580    ) -> WidgetWithHandlers<Self> {
1581        WidgetWithHandlers::new(self).on_pointer_event(f)
1582    }
1583
1584    fn on_hover(
1585        self,
1586        f: impl FnMut(bool, &mut EventContext) + 'static,
1587    ) -> WidgetWithHandlers<Self> {
1588        WidgetWithHandlers::new(self).on_hover(f)
1589    }
1590
1591    fn on_scroll(
1592        self,
1593        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
1594    ) -> WidgetWithHandlers<Self> {
1595        WidgetWithHandlers::new(self).on_scroll(f)
1596    }
1597
1598    fn on_access_action(
1599        self,
1600        f: impl FnMut(accesskit::Action, &mut EventContext) -> EventResponse + 'static,
1601    ) -> WidgetWithHandlers<Self> {
1602        WidgetWithHandlers::new(self).on_access_action(f)
1603    }
1604
1605    fn focusable(self, focusable: bool) -> WidgetWithHandlers<Self> {
1606        WidgetWithHandlers::new(self).focusable(focusable)
1607    }
1608
1609    fn tab_index(self, index: i32) -> WidgetWithHandlers<Self> {
1610        WidgetWithHandlers::new(self).tab_index(index)
1611    }
1612
1613    fn cursor(self, cursor: CursorIcon) -> WidgetWithHandlers<Self> {
1614        WidgetWithHandlers::new(self).cursor(cursor)
1615    }
1616
1617    fn clips_children_on(self, clips: bool) -> WidgetWithHandlers<Self> {
1618        WidgetWithHandlers::new(self).clips_children(clips)
1619    }
1620
1621    /// Declare this node a text-input surface, enabling the OS input method
1622    /// (with `ctx`'s purpose) while it is focused. See [`crate::ime`].
1623    fn ime_input(self, ctx: crate::ime::ImeContext) -> WidgetWithHandlers<Self> {
1624        WidgetWithHandlers::new(self).ime_input(ctx)
1625    }
1626
1627    /// Make the widget invisible to pointer hit-testing. See
1628    /// [`HandlerSet::event_pass_through`].
1629    fn event_pass_through(self, pass_through: bool) -> WidgetWithHandlers<Self> {
1630        WidgetWithHandlers::new(self).event_pass_through(pass_through)
1631    }
1632
1633    /// Mark this widget's subtree a gesture dead zone. See
1634    /// [`HandlerSet::gesture_dead_zone`].
1635    fn gesture_dead_zone(self, dead_zone: bool) -> WidgetWithHandlers<Self> {
1636        WidgetWithHandlers::new(self).gesture_dead_zone(dead_zone)
1637    }
1638
1639    /// Mark this widget a keyboard capture surface (terminals, game
1640    /// viewports): while focused, `KeyDown`s bypass shortcut resolution.
1641    /// See [`HandlerSet::keyboard_capture`].
1642    fn keyboard_capture(self, capture: bool) -> WidgetWithHandlers<Self> {
1643        WidgetWithHandlers::new(self).keyboard_capture(capture)
1644    }
1645
1646    /// Make this widget and its whole subtree invisible to pointer
1647    /// hit-testing (decorative overlays). See
1648    /// [`HandlerSet::hit_transparent`].
1649    fn hit_transparent(self, transparent: bool) -> WidgetWithHandlers<Self> {
1650        WidgetWithHandlers::new(self).hit_transparent(transparent)
1651    }
1652
1653    /// Set a context-menu factory. See
1654    /// [`HandlerSet::context_menu`] for the full contract.
1655    fn context_menu(
1656        self,
1657        factory: impl Fn(Point, &mut EventContext) -> Option<Box<dyn Widget>> + 'static,
1658    ) -> WidgetWithHandlers<Self> {
1659        WidgetWithHandlers::new(self).context_menu(factory)
1660    }
1661
1662    /// Bind a `Signal<bool>` the framework writes when a strict
1663    /// descendant has focus. See [`HandlerSet::focus_within`].
1664    fn focus_within(self, signal: crate::signal::Signal<bool>) -> WidgetWithHandlers<Self> {
1665        WidgetWithHandlers::new(self).focus_within(signal)
1666    }
1667
1668    /// Bind a `Signal<bool>` the framework writes when a strict
1669    /// descendant is hovered. See [`HandlerSet::hover_within`].
1670    fn hover_within(self, signal: crate::signal::Signal<bool>) -> WidgetWithHandlers<Self> {
1671        WidgetWithHandlers::new(self).hover_within(signal)
1672    }
1673
1674    /// Bind this node's visibility (`bool` / `Signal<bool>` / `Prop<bool>`) as
1675    /// a builder property, so `teksu!` can write `visible_when: sig`. Equivalent
1676    /// to `ctx.visible_when(id, ..)`. See [`HandlerSet::visible_when`].
1677    fn visible_when(self, state: impl Into<Prop<bool>>) -> WidgetWithHandlers<Self> {
1678        WidgetWithHandlers::new(self).visible_when(state)
1679    }
1680
1681    fn on_drag_hover(
1682        self,
1683        f: impl FnMut(
1684            &crate::drag_payload::DragPayload,
1685            teksilo_canvas::Point,
1686            &mut EventContext,
1687        ) -> crate::drag_state::DropFeedback
1688        + 'static,
1689    ) -> WidgetWithHandlers<Self> {
1690        WidgetWithHandlers::new(self).on_drag_hover(f)
1691    }
1692
1693    fn on_drag_leave(self, f: impl FnMut(&mut EventContext) + 'static) -> WidgetWithHandlers<Self> {
1694        WidgetWithHandlers::new(self).on_drag_leave(f)
1695    }
1696
1697    fn on_drag_tick(
1698        self,
1699        f: impl FnMut(teksilo_canvas::Point, &mut EventContext) + 'static,
1700    ) -> WidgetWithHandlers<Self> {
1701        WidgetWithHandlers::new(self).on_drag_tick(f)
1702    }
1703
1704    fn on_drop(
1705        self,
1706        f: impl FnMut(
1707            crate::drag_payload::DragPayload,
1708            teksilo_canvas::Point,
1709            &mut EventContext,
1710        ) -> bool
1711        + 'static,
1712    ) -> WidgetWithHandlers<Self> {
1713        WidgetWithHandlers::new(self).on_drop(f)
1714    }
1715
1716    /// Set the drag-ended handler on a drag source. See
1717    /// [`HandlerSet::on_drag_ended`].
1718    fn on_drag_ended(
1719        self,
1720        f: impl FnMut(crate::drag_payload::DropOutcome, &mut EventContext) + 'static,
1721    ) -> WidgetWithHandlers<Self> {
1722        WidgetWithHandlers::new(self).on_drag_ended(f)
1723    }
1724
1725    // ── Accessibility overrides ────────────────────────────────────────
1726    //
1727    // Trait-level entry points: each method wraps the widget into a
1728    // `WidgetWithHandlers` (the first builder call in any chain) and
1729    // forwards to the inherent method of the same name. See
1730    // `WidgetWithHandlers` for full rustdoc on each method's semantics.
1731    // For translated strings, `LocalizedString` flows through
1732    // `impl Into<Prop<String>>` via `teksilo-i18n`'s
1733    // `From<LocalizedString> for Prop<String>` impl, staying reactive.
1734
1735    fn access_label(self, label: impl Into<Prop<String>>) -> WidgetWithHandlers<Self> {
1736        WidgetWithHandlers::new(self).access_label(label)
1737    }
1738
1739    #[doc(hidden)]
1740    fn access_label_literal(self, label: impl Into<String>) -> WidgetWithHandlers<Self> {
1741        WidgetWithHandlers::new(self).access_label_literal(label)
1742    }
1743
1744    fn access_description(self, description: impl Into<Prop<String>>) -> WidgetWithHandlers<Self> {
1745        WidgetWithHandlers::new(self).access_description(description)
1746    }
1747
1748    #[doc(hidden)]
1749    fn access_description_literal(
1750        self,
1751        description: impl Into<String>,
1752    ) -> WidgetWithHandlers<Self> {
1753        WidgetWithHandlers::new(self).access_description_literal(description)
1754    }
1755
1756    fn access_hint(self, hint: impl Into<Prop<String>>) -> WidgetWithHandlers<Self> {
1757        WidgetWithHandlers::new(self).access_hint(hint)
1758    }
1759
1760    #[doc(hidden)]
1761    fn access_hint_literal(self, hint: impl Into<String>) -> WidgetWithHandlers<Self> {
1762        WidgetWithHandlers::new(self).access_hint_literal(hint)
1763    }
1764
1765    fn access_value(self, value: impl Into<Prop<String>>) -> WidgetWithHandlers<Self> {
1766        WidgetWithHandlers::new(self).access_value(value)
1767    }
1768
1769    #[doc(hidden)]
1770    fn access_value_literal(self, value: impl Into<String>) -> WidgetWithHandlers<Self> {
1771        WidgetWithHandlers::new(self).access_value_literal(value)
1772    }
1773
1774    fn access_role(self, role: accesskit::Role) -> WidgetWithHandlers<Self> {
1775        WidgetWithHandlers::new(self).access_role(role)
1776    }
1777
1778    fn access_hidden(self, hidden: impl Into<Prop<bool>>) -> WidgetWithHandlers<Self> {
1779        WidgetWithHandlers::new(self).access_hidden(hidden)
1780    }
1781
1782    fn access_disabled(self, disabled: bool) -> WidgetWithHandlers<Self> {
1783        WidgetWithHandlers::new(self).access_disabled(disabled)
1784    }
1785
1786    fn access_identifier(self, id: impl Into<String>) -> WidgetWithHandlers<Self> {
1787        WidgetWithHandlers::new(self).access_identifier(id)
1788    }
1789
1790    fn access_controls(self, target: WidgetId) -> WidgetWithHandlers<Self> {
1791        WidgetWithHandlers::new(self).access_controls(target)
1792    }
1793
1794    fn access_described_by(self, target: WidgetId) -> WidgetWithHandlers<Self> {
1795        WidgetWithHandlers::new(self).access_described_by(target)
1796    }
1797
1798    fn access_labelled_by(self, target: WidgetId) -> WidgetWithHandlers<Self> {
1799        WidgetWithHandlers::new(self).access_labelled_by(target)
1800    }
1801
1802    fn access_live(self, mode: accesskit::Live) -> WidgetWithHandlers<Self> {
1803        WidgetWithHandlers::new(self).access_live(mode)
1804    }
1805
1806    fn access_current(self, current: accesskit::AriaCurrent) -> WidgetWithHandlers<Self> {
1807        WidgetWithHandlers::new(self).access_current(current)
1808    }
1809
1810    fn access_shortcut_literal(self, shortcut: impl Into<String>) -> WidgetWithHandlers<Self> {
1811        WidgetWithHandlers::new(self).access_shortcut_literal(shortcut)
1812    }
1813
1814    fn access_shortcut_id(self, id: impl Into<String>) -> WidgetWithHandlers<Self> {
1815        WidgetWithHandlers::new(self).access_shortcut_id(id)
1816    }
1817
1818    fn access_has_popup(self, kind: accesskit::HasPopup) -> WidgetWithHandlers<Self> {
1819        WidgetWithHandlers::new(self).access_has_popup(kind)
1820    }
1821
1822    fn access_orientation(self, orientation: accesskit::Orientation) -> WidgetWithHandlers<Self> {
1823        WidgetWithHandlers::new(self).access_orientation(orientation)
1824    }
1825
1826    fn access_exclude_subtree(self) -> WidgetWithHandlers<Self> {
1827        WidgetWithHandlers::new(self).access_exclude_subtree()
1828    }
1829
1830    fn access_merge_subtree(self) -> WidgetWithHandlers<Self> {
1831        WidgetWithHandlers::new(self).access_merge_subtree()
1832    }
1833
1834    fn access_subtree(self, mode: AccessSubtreeMode) -> WidgetWithHandlers<Self> {
1835        WidgetWithHandlers::new(self).access_subtree(mode)
1836    }
1837
1838    fn access_numeric_value(self, value: f64) -> WidgetWithHandlers<Self> {
1839        WidgetWithHandlers::new(self).access_numeric_value(value)
1840    }
1841
1842    fn access_numeric_range(self, min: f64, max: f64) -> WidgetWithHandlers<Self> {
1843        WidgetWithHandlers::new(self).access_numeric_range(min, max)
1844    }
1845
1846    fn access_numeric_step(self, step: f64) -> WidgetWithHandlers<Self> {
1847        WidgetWithHandlers::new(self).access_numeric_step(step)
1848    }
1849
1850    fn access_action<F>(self, action: accesskit::Action, handler: F) -> WidgetWithHandlers<Self>
1851    where
1852        F: FnMut(&mut EventContext) + 'static,
1853    {
1854        WidgetWithHandlers::new(self).access_action(action, handler)
1855    }
1856
1857    fn access_remove_action(self, action: accesskit::Action) -> WidgetWithHandlers<Self> {
1858        WidgetWithHandlers::new(self).access_remove_action(action)
1859    }
1860
1861    fn access_custom_action<F>(
1862        self,
1863        label: impl Into<Prop<String>>,
1864        handler: F,
1865    ) -> WidgetWithHandlers<Self>
1866    where
1867        F: FnMut(&mut EventContext) + 'static,
1868    {
1869        WidgetWithHandlers::new(self).access_custom_action(label, handler)
1870    }
1871
1872    #[doc(hidden)]
1873    fn access_custom_action_literal<F>(
1874        self,
1875        label: impl Into<String>,
1876        handler: F,
1877    ) -> WidgetWithHandlers<Self>
1878    where
1879        F: FnMut(&mut EventContext) + 'static,
1880    {
1881        WidgetWithHandlers::new(self).access_custom_action_literal(label, handler)
1882    }
1883
1884    fn access_customize<F>(self, f: F) -> WidgetWithHandlers<Self>
1885    where
1886        F: Fn(&mut crate::accessibility::AccessNodeBuilder) + 'static,
1887    {
1888        WidgetWithHandlers::new(self).access_customize(f)
1889    }
1890}
1891
1892// Blanket implementation for all Widget types.
1893impl<W: Widget + Sized + 'static> WidgetBuilder for W {}
1894
1895#[cfg(test)]
1896mod tests {
1897    use super::*;
1898    use crate::widget::WidgetPlacement;
1899    use crate::widget_id::WidgetId;
1900    use crate::widget_tree::WidgetTree;
1901
1902    #[derive(Debug)]
1903    struct CompositeLeaf {
1904        child_id: Option<WidgetId>,
1905    }
1906
1907    impl CompositeLeaf {
1908        fn new() -> Self {
1909            Self { child_id: None }
1910        }
1911    }
1912
1913    impl Widget for CompositeLeaf {
1914        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
1915            let child = ctx.add(crate::test_widgets::FillWidget::new());
1916            self.child_id = Some(child);
1917            vec![child]
1918        }
1919
1920        fn layout_response(
1921            &self,
1922            proposal: teksilo_canvas::SizeProposal,
1923            _ctx: &crate::widget::LayoutContext,
1924        ) -> crate::widget::LayoutResponse {
1925            proposal.resolve(120.0, 40.0).into()
1926        }
1927
1928        fn place_children(
1929            &self,
1930            bounds: teksilo_canvas::Rect,
1931            _proposal: teksilo_canvas::SizeProposal,
1932            children: &mut [WidgetPlacement],
1933            _ctx: &crate::widget::LayoutContext,
1934        ) {
1935            for child in children.iter_mut() {
1936                child.origin = bounds.origin();
1937                child.size = bounds.size();
1938            }
1939        }
1940
1941        fn children(&self) -> Vec<WidgetId> {
1942            self.child_id.into_iter().collect()
1943        }
1944    }
1945
1946    #[test]
1947    fn external_handlers_survive_rebuild() {
1948        // Regression check: handlers attached externally via the
1949        // `WidgetBuilder` builder (e.g. `MyCompositeWidget::new().on_tap(...)`)
1950        // must continue to fire after the widget rebuilds in place.
1951        // My handler-clearing fix in `rebuild_single_widget` wiped
1952        // `node.handlers` to stop accumulation of `apply_self_handlers`
1953        // calls across rebuilds — but the extracted-once-at-insertion
1954        // HandlerSet is gone by rebuild time and would be lost.
1955        use std::cell::Cell;
1956        use std::rc::Rc;
1957
1958        let tap_count = Rc::new(Cell::new(0_u32));
1959        let tc = tap_count.clone();
1960
1961        let mut tree = WidgetTree::new();
1962        let id = tree.add(CompositeLeaf::new().on_tap(move |_pos, _ctx| {
1963            tc.set(tc.get() + 1);
1964        }));
1965        tree.layout(teksilo_canvas::SizeProposal::exact(200.0, 100.0));
1966
1967        // Trip a rebuild of the composite — its child gets torn down &
1968        // rebuilt; node.handlers gets cleared and reset.
1969        tree.arena_mark_needs_rebuild_for_testing(id);
1970        tree.layout(teksilo_canvas::SizeProposal::exact(200.0, 100.0));
1971
1972        // Click through the composite; the externally-attached on_tap
1973        // must still be wired up.
1974        tree.click(id);
1975        assert_eq!(
1976            tap_count.get(),
1977            1,
1978            "externally-attached on_tap must survive a rebuild"
1979        );
1980    }
1981
1982    #[test]
1983    fn wrapped_composite_widget_still_builds_children() {
1984        let mut tree = WidgetTree::new();
1985        let root = tree.add(CompositeLeaf::new().on_tap(|_pos, _ctx| {}));
1986        tree.layout(teksilo_canvas::SizeProposal::exact(200.0, 100.0));
1987
1988        assert_eq!(tree.children(root).len(), 1);
1989    }
1990
1991    /// A widget that exposes both downcast hooks, like every widget a
1992    /// composing container reads its child's concrete type through.
1993    #[derive(Debug)]
1994    struct Reflective {
1995        marker: u32,
1996    }
1997
1998    impl Widget for Reflective {
1999        fn layout_response(
2000            &self,
2001            proposal: teksilo_canvas::SizeProposal,
2002            _ctx: &crate::widget::LayoutContext,
2003        ) -> crate::widget::LayoutResponse {
2004            proposal.resolve(0.0, 0.0).into()
2005        }
2006
2007        fn as_any(&self) -> Option<&dyn std::any::Any> {
2008            Some(self)
2009        }
2010
2011        fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
2012            Some(self)
2013        }
2014    }
2015
2016    /// Decorating a widget must not hide its concrete type from a parent that
2017    /// reads it. `as_any` was already forwarded; `as_any_mut` was not, so a
2018    /// container reading a child through the mutable hook (`MenuList` does, for
2019    /// mnemonics, the type-ahead label and radio grouping) silently saw nothing
2020    /// the moment any builder method was called on that child.
2021    #[test]
2022    fn both_downcast_hooks_see_through_the_handler_wrapper() {
2023        let mut wrapped = Reflective { marker: 7 }.focusable(true);
2024
2025        let seen = wrapped
2026            .as_any()
2027            .and_then(|a| a.downcast_ref::<Reflective>())
2028            .map(|r| r.marker);
2029        assert_eq!(seen, Some(7), "as_any must forward through the wrapper");
2030
2031        let seen_mut = wrapped
2032            .as_any_mut()
2033            .and_then(|a| a.downcast_mut::<Reflective>())
2034            .map(|r| r.marker);
2035        assert_eq!(
2036            seen_mut,
2037            Some(7),
2038            "as_any_mut must forward through the wrapper too"
2039        );
2040    }
2041}