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    ///
552    /// Layered with [`on_access_action`](Self::on_access_action) rather than
553    /// replacing it: both fire for the same dispatched action, and it counts as
554    /// handled if either says so.
555    pub fn on_access_action_request(
556        mut self,
557        f: impl FnMut(
558            accesskit::Action,
559            accesskit::NodeId,
560            Option<accesskit::ActionData>,
561            &mut EventContext,
562        ) -> EventResponse
563        + 'static,
564    ) -> Self {
565        self.handlers.on_access_action_request = Some(Box::new(f));
566        self
567    }
568
569    /// Set the focusable flag.
570    pub fn focusable(mut self, focusable: bool) -> Self {
571        self.focusable = Some(focusable);
572        self
573    }
574
575    /// Set the cursor icon.
576    pub fn cursor(mut self, cursor: CursorIcon) -> Self {
577        self.cursor = Some(cursor);
578        self
579    }
580
581    /// Set the clips_children flag.
582    pub fn clips_children(mut self, clips: bool) -> Self {
583        self.clips_children = Some(clips);
584        self
585    }
586
587    /// Declare this node a text-input surface, enabling the OS input method
588    /// (with `ctx`'s purpose) while it is focused. Leaving it unset (the
589    /// default) means no OS IME. The platform reads the focused node's
590    /// descriptor at focus-change time. See [`crate::ime`].
591    pub fn ime_input(mut self, ctx: crate::ime::ImeContext) -> Self {
592        self.ime = Some(ctx);
593        self
594    }
595
596    /// Make the widget invisible to pointer hit-testing. With
597    /// `pass_through = true`, pointer events traverse this node as if
598    /// it were not there — useful for purely decorative overlays that
599    /// must not absorb clicks (the debug inspector's `HighlightLayer`
600    /// and `HoverProbe` use this).
601    pub fn event_pass_through(mut self, pass_through: bool) -> Self {
602        self.event_pass_through = Some(pass_through);
603        self
604    }
605
606    /// Mark this widget's subtree a **gesture dead zone**: a pointer press
607    /// inside it must not arm a drag/swipe recognizer on any ancestor above
608    /// it. Use to let interactive controls (buttons, a `⋮` menu) sit inside a
609    /// draggable / swipeable container (a dock-panel header, a card, a list
610    /// row) without a few px of click jitter starting the ancestor's drag.
611    /// The container's own drag still works everywhere else. Honored by
612    /// `arm_drag_observers`; see the `DeadZone` wrapper widget.
613    pub fn gesture_dead_zone(mut self, dead_zone: bool) -> Self {
614        self.gesture_dead_zone = Some(dead_zone);
615        self
616    }
617
618    /// Mark this widget a **keyboard capture** surface: while it holds
619    /// focus, every `KeyDown` is delivered straight to its `on_key`
620    /// handler, bypassing shortcut → intent → action resolution. Use for
621    /// a terminal emulator that must forward `Ctrl+C` / `Ctrl+W` /
622    /// `Alt+<letter>` to a child process instead of triggering the host
623    /// app's shortcuts, a game viewport, or a modal text surface.
624    ///
625    /// # The escape contract
626    ///
627    /// **`Ctrl+Tab` / `Ctrl+Shift+Tab` are reserved and always move focus
628    /// out.** The dispatcher cycles focus on that chord before the capture
629    /// node is consulted, so a capture surface cannot become a keyboard trap
630    /// (WCAG 2.1.2) however greedily its `on_key` behaves. Do not bind them.
631    ///
632    /// Nothing else is reserved. In particular Escape is **not**: overlay
633    /// back-navigation runs first only while an overlay is actually open, so
634    /// a focused capture surface with no overlay above it does receive
635    /// Escape and may consume it. See
636    /// [`super::arena::WidgetNode::keyboard_capture`].
637    pub fn keyboard_capture(mut self, capture: bool) -> Self {
638        self.keyboard_capture = Some(capture);
639        self
640    }
641
642    /// Make this widget AND its whole subtree invisible to pointer
643    /// hit-testing. Stronger than [`event_pass_through`](Self::event_pass_through):
644    /// that one keeps descendants hittable, this one excludes them too.
645    /// For purely decorative composite overlays (a count badge over a
646    /// button, a watermark) whose own children would otherwise swallow
647    /// the click meant for the control underneath.
648    pub fn hit_transparent(mut self, transparent: bool) -> Self {
649        self.hit_transparent = Some(transparent);
650        self
651    }
652
653    /// Bind a user-owned `Signal<bool>` that the framework will set
654    /// to `true` whenever the focused widget is a *strict descendant*
655    /// of this node, and `false` otherwise. Useful for unified focus
656    /// halos around composite widgets (a chat composer that highlights
657    /// when its `RichTextEditor` or "Send" button is focused, a
658    /// `Panel` wrapping a `SpinBox`, etc).
659    ///
660    /// Strict-ancestors only — a widget that *is* itself focused does
661    /// not also see its own `focus_within` signal flipped to `true`.
662    /// Combine with `on_focus` if you want both behaviours.
663    pub fn focus_within(mut self, signal: crate::signal::Signal<bool>) -> Self {
664        self.focus_within = Some(signal);
665        self
666    }
667
668    /// Bind a user-owned `Signal<bool>` that the framework will set
669    /// to `true` whenever the hovered widget is a *strict descendant*
670    /// of this node. Symmetric to [`focus_within`](Self::focus_within).
671    pub fn hover_within(mut self, signal: crate::signal::Signal<bool>) -> Self {
672        self.hover_within = Some(signal);
673        self
674    }
675
676    /// Bind this node's visibility to a `bool` / `Signal<bool>` / `Prop<bool>`.
677    /// A bound value shows/hides the node reactively (registered at
678    /// `Relayout`). Equivalent to `ctx.visible_when(id, ..)`; exposed as a
679    /// builder method so `teksu!` can write `visible_when: sig` as a property.
680    pub fn visible_when(mut self, state: impl Into<Prop<bool>>) -> Self {
681        self.visible_when = Some(state.into());
682        self
683    }
684
685    /// Set a context-menu factory. See [`ContextMenuFactory`] for the
686    /// full contract: the closure receives the click position
687    /// (widget-local) and a full [`EventContext`], and returns
688    /// `Some(menu)` to mount or `None` to decline (falling through to
689    /// the nearest ancestor with a factory).
690    pub fn context_menu(
691        mut self,
692        factory: impl Fn(Point, &mut EventContext) -> Option<Box<dyn Widget>> + 'static,
693    ) -> Self {
694        self.context_menu_factory = Some(Box::new(factory));
695        self
696    }
697
698    /// Set the drag hover handler. Called when a drag payload hovers over this widget.
699    /// Return `DropFeedback` to indicate acceptance and visual feedback.
700    pub fn on_drag_hover(
701        mut self,
702        f: impl FnMut(
703            &crate::drag_payload::DragPayload,
704            teksilo_canvas::Point,
705            &mut EventContext,
706        ) -> crate::drag_state::DropFeedback
707        + 'static,
708    ) -> Self {
709        self.handlers.on_drag_hover = Some(Box::new(f));
710        self
711    }
712
713    /// Set the drag-leave handler. Fires when a drag that was over this
714    /// widget moves to another target, completes (drop on any target), or
715    /// is cancelled. Widgets that stash transient feedback state in
716    /// `on_drag_hover` must clear it here.
717    pub fn on_drag_leave(mut self, f: impl FnMut(&mut EventContext) + 'static) -> Self {
718        self.handlers.on_drag_leave = Some(Box::new(f));
719        self
720    }
721
722    /// Set the per-frame drag-tick handler. Fires once per frame while a
723    /// drag is active and this widget is the current drop target. The
724    /// closure receives the current pointer position in widget-local
725    /// coordinates. Use for behaviours that must keep running even when
726    /// the pointer is stationary — viewport-edge auto-scroll and
727    /// spring-loaded folders.
728    pub fn on_drag_tick(
729        mut self,
730        f: impl FnMut(teksilo_canvas::Point, &mut EventContext) + 'static,
731    ) -> Self {
732        self.handlers.on_drag_tick = Some(Box::new(f));
733        self
734    }
735
736    /// Set the drop handler. Called when a payload is dropped on this widget.
737    /// Return `true` if the drop was accepted.
738    pub fn on_drop(
739        mut self,
740        f: impl FnMut(
741            crate::drag_payload::DragPayload,
742            teksilo_canvas::Point,
743            &mut EventContext,
744        ) -> bool
745        + 'static,
746    ) -> Self {
747        self.handlers.on_drop = Some(Box::new(f));
748        self
749    }
750
751    /// Set the drag-ended handler on a drag **source**. Fires when a drag
752    /// this widget started ends — dropped on an in-app target, exported to
753    /// another application via the OS (copy / move), or cancelled. Use it to
754    /// react to the outcome, e.g. remove the dragged item on a
755    /// [`DropOutcome::OsMove`](crate::drag_payload::DropOutcome::OsMove).
756    pub fn on_drag_ended(
757        mut self,
758        f: impl FnMut(crate::drag_payload::DropOutcome, &mut EventContext) + 'static,
759    ) -> Self {
760        self.handlers.on_drag_ended = Some(Box::new(f));
761        self
762    }
763}
764
765impl Default for HandlerSet {
766    fn default() -> Self {
767        Self::new()
768    }
769}
770
771impl std::fmt::Debug for HandlerSet {
772    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
773        f.debug_struct("HandlerSet")
774            .field("handlers", &self.handlers)
775            .field("focusable", &self.focusable)
776            .field("tab_index", &self.tab_index)
777            .field("cursor", &self.cursor)
778            .finish()
779    }
780}
781
782// ---------------------------------------------------------------------------
783// WidgetWithHandlers<W> — wrapper storing widget + accumulated handlers
784// ---------------------------------------------------------------------------
785
786/// A widget wrapped with attached event handlers and framework metadata.
787/// Created by calling builder methods from `WidgetBuilder` on any widget.
788pub struct WidgetWithHandlers<W: Widget> {
789    pub(crate) widget: W,
790    pub(crate) handler_set: HandlerSet,
791}
792
793impl<W: Widget> WidgetWithHandlers<W> {
794    fn new(widget: W) -> Self {
795        Self {
796            widget,
797            handler_set: HandlerSet::new(),
798        }
799    }
800
801    /// Take the handler set out, leaving defaults.
802    pub(crate) fn take_handler_set(&mut self) -> HandlerSet {
803        std::mem::take(&mut self.handler_set)
804    }
805
806    // -- Gesture handlers --
807
808    pub fn on_tap(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
809        self.handler_set.handlers.on_tap = Some(Box::new(f));
810        self
811    }
812
813    pub fn on_double_tap(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
814        self.handler_set.handlers.on_double_tap = Some(Box::new(f));
815        self
816    }
817
818    pub fn on_triple_tap(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
819        self.handler_set.handlers.on_triple_tap = Some(Box::new(f));
820        self
821    }
822
823    pub fn on_long_press(mut self, f: impl FnMut(&TapEvent, &mut EventContext) + 'static) -> Self {
824        self.handler_set.handlers.on_long_press = Some(Box::new(f));
825        self
826    }
827
828    /// Restrict (or extend) the set of pointer buttons that fire
829    /// `on_tap`. Default is [`ButtonMask::PRIMARY`].
830    pub fn accept_tap_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
831        self.handler_set.handlers.tap_buttons = Some(mask.into());
832        self
833    }
834
835    /// Restrict (or extend) the set of pointer buttons that fire
836    /// `on_double_tap`. Default [`ButtonMask::PRIMARY`].
837    pub fn accept_double_tap_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
838        self.handler_set.handlers.double_tap_buttons = Some(mask.into());
839        self
840    }
841
842    /// Restrict (or extend) the set of pointer buttons that fire
843    /// `on_triple_tap`. Default [`ButtonMask::PRIMARY`].
844    pub fn accept_triple_tap_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
845        self.handler_set.handlers.triple_tap_buttons = Some(mask.into());
846        self
847    }
848
849    /// Restrict (or extend) the set of pointer buttons that fire
850    /// `on_long_press`. Default [`ButtonMask::PRIMARY`].
851    pub fn accept_long_press_buttons(mut self, mask: impl Into<ButtonMask>) -> Self {
852        self.handler_set.handlers.long_press_buttons = Some(mask.into());
853        self
854    }
855
856    pub fn on_drag(mut self, f: impl FnMut(DragPhase, &mut EventContext) + 'static) -> Self {
857        self.handler_set.handlers.on_drag = Some(Box::new(f));
858        self
859    }
860
861    pub fn on_swipe(
862        mut self,
863        f: impl FnMut(SwipeDirection, f32, &mut EventContext) + 'static,
864    ) -> Self {
865        self.handler_set.handlers.on_swipe = Some(Box::new(f));
866        self
867    }
868
869    pub fn on_pinch(mut self, f: impl FnMut(PinchPhase, &mut EventContext) + 'static) -> Self {
870        self.handler_set.handlers.on_pinch = Some(Box::new(f));
871        self
872    }
873
874    // -- Focus and keyboard --
875
876    pub fn on_focus(mut self, f: impl FnMut(bool, &mut EventContext) + 'static) -> Self {
877        self.handler_set.handlers.on_focus = Some(Box::new(f));
878        self
879    }
880
881    pub fn on_key(
882        mut self,
883        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
884    ) -> Self {
885        self.handler_set.handlers.on_key = Some(Box::new(f));
886        self
887    }
888
889    /// Set the strict-ancestor key preview handler. See
890    /// [`HandlerSet::on_key_preview`].
891    pub fn on_key_preview(
892        mut self,
893        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
894    ) -> Self {
895        self.handler_set.handlers.on_key_preview = Some(Box::new(f));
896        self
897    }
898
899    pub fn focusable(mut self, focusable: bool) -> Self {
900        self.handler_set.focusable = Some(focusable);
901        self
902    }
903
904    pub fn tab_index(mut self, index: i32) -> Self {
905        self.handler_set.tab_index = Some(index);
906        self
907    }
908
909    // -- Pointer (low-level escape hatch) --
910
911    pub fn on_pointer_event(
912        mut self,
913        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
914    ) -> Self {
915        self.handler_set.handlers.on_pointer_event = Some(Box::new(f));
916        self
917    }
918
919    pub fn on_hover(mut self, f: impl FnMut(bool, &mut EventContext) + 'static) -> Self {
920        self.handler_set.handlers.on_hover = Some(Box::new(f));
921        self
922    }
923
924    pub fn cursor(mut self, cursor: CursorIcon) -> Self {
925        self.handler_set.cursor = Some(cursor);
926        self
927    }
928
929    // -- Scroll --
930
931    pub fn on_scroll(
932        mut self,
933        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
934    ) -> Self {
935        self.handler_set.handlers.on_scroll = Some(Box::new(f));
936        self
937    }
938
939    // -- Accessibility actions --
940
941    pub fn on_access_action(
942        mut self,
943        f: impl FnMut(accesskit::Action, &mut EventContext) -> EventResponse + 'static,
944    ) -> Self {
945        self.handler_set.handlers.on_access_action = Some(Box::new(f));
946        self
947    }
948
949    pub fn on_access_action_request(
950        mut self,
951        f: impl FnMut(
952            accesskit::Action,
953            accesskit::NodeId,
954            Option<accesskit::ActionData>,
955            &mut EventContext,
956        ) -> EventResponse
957        + 'static,
958    ) -> Self {
959        self.handler_set.handlers.on_access_action_request = Some(Box::new(f));
960        self
961    }
962
963    // -- Framework-level properties --
964
965    pub fn clips_children(mut self, clips: bool) -> Self {
966        self.handler_set.clips_children = Some(clips);
967        self
968    }
969
970    /// Declare this node a text-input surface, enabling the OS input method
971    /// (with `ctx`'s purpose) while it is focused. See [`crate::ime`].
972    pub fn ime_input(mut self, ctx: crate::ime::ImeContext) -> Self {
973        self.handler_set.ime = Some(ctx);
974        self
975    }
976
977    /// Make the widget invisible to pointer hit-testing. See
978    /// [`HandlerSet::event_pass_through`].
979    pub fn event_pass_through(mut self, pass_through: bool) -> Self {
980        self.handler_set.event_pass_through = Some(pass_through);
981        self
982    }
983
984    /// Mark this widget's subtree a gesture dead zone. See
985    /// [`HandlerSet::gesture_dead_zone`].
986    pub fn gesture_dead_zone(mut self, dead_zone: bool) -> Self {
987        self.handler_set.gesture_dead_zone = Some(dead_zone);
988        self
989    }
990
991    /// Mark this widget a keyboard capture surface: while focused, every
992    /// `KeyDown` bypasses shortcut resolution and reaches its `on_key`
993    /// handler (terminals, game viewports). See
994    /// [`HandlerSet::keyboard_capture`].
995    pub fn keyboard_capture(mut self, capture: bool) -> Self {
996        self.handler_set.keyboard_capture = Some(capture);
997        self
998    }
999
1000    /// Make this widget and its whole subtree invisible to pointer
1001    /// hit-testing. See [`HandlerSet::hit_transparent`].
1002    pub fn hit_transparent(mut self, transparent: bool) -> Self {
1003        self.handler_set.hit_transparent = Some(transparent);
1004        self
1005    }
1006
1007    /// Set a context-menu factory. See
1008    /// [`HandlerSet::context_menu`] for the full contract.
1009    pub fn context_menu(
1010        mut self,
1011        factory: impl Fn(Point, &mut EventContext) -> Option<Box<dyn Widget>> + 'static,
1012    ) -> Self {
1013        self.handler_set.context_menu_factory = Some(Box::new(factory));
1014        self
1015    }
1016
1017    /// Bind a `Signal<bool>` the framework writes when a strict
1018    /// descendant has focus. See [`HandlerSet::focus_within`].
1019    pub fn focus_within(mut self, signal: crate::signal::Signal<bool>) -> Self {
1020        self.handler_set.focus_within = Some(signal);
1021        self
1022    }
1023
1024    /// Bind a `Signal<bool>` the framework writes when a strict
1025    /// descendant is hovered. See [`HandlerSet::hover_within`].
1026    pub fn hover_within(mut self, signal: crate::signal::Signal<bool>) -> Self {
1027        self.handler_set.hover_within = Some(signal);
1028        self
1029    }
1030
1031    /// Bind this node's visibility. See [`HandlerSet::visible_when`].
1032    pub fn visible_when(mut self, state: impl Into<Prop<bool>>) -> Self {
1033        self.handler_set.visible_when = Some(state.into());
1034        self
1035    }
1036
1037    /// Set the drag hover handler. Called when a drag payload hovers over this widget.
1038    pub fn on_drag_hover(
1039        mut self,
1040        f: impl FnMut(
1041            &crate::drag_payload::DragPayload,
1042            teksilo_canvas::Point,
1043            &mut EventContext,
1044        ) -> crate::drag_state::DropFeedback
1045        + 'static,
1046    ) -> Self {
1047        self.handler_set.handlers.on_drag_hover = Some(Box::new(f));
1048        self
1049    }
1050
1051    /// Set the drag-leave handler. See [`HandlerSet::on_drag_leave`].
1052    pub fn on_drag_leave(mut self, f: impl FnMut(&mut EventContext) + 'static) -> Self {
1053        self.handler_set.handlers.on_drag_leave = Some(Box::new(f));
1054        self
1055    }
1056
1057    /// Set the per-frame drag-tick handler. See [`HandlerSet::on_drag_tick`].
1058    pub fn on_drag_tick(
1059        mut self,
1060        f: impl FnMut(teksilo_canvas::Point, &mut EventContext) + 'static,
1061    ) -> Self {
1062        self.handler_set.handlers.on_drag_tick = Some(Box::new(f));
1063        self
1064    }
1065
1066    /// Set the drop handler. Called when a payload is dropped on this widget.
1067    pub fn on_drop(
1068        mut self,
1069        f: impl FnMut(
1070            crate::drag_payload::DragPayload,
1071            teksilo_canvas::Point,
1072            &mut EventContext,
1073        ) -> bool
1074        + 'static,
1075    ) -> Self {
1076        self.handler_set.handlers.on_drop = Some(Box::new(f));
1077        self
1078    }
1079
1080    /// Set the drag-ended handler on a drag source. See
1081    /// [`HandlerSet::on_drag_ended`].
1082    pub fn on_drag_ended(
1083        mut self,
1084        f: impl FnMut(crate::drag_payload::DropOutcome, &mut EventContext) + 'static,
1085    ) -> Self {
1086        self.handler_set.handlers.on_drag_ended = Some(Box::new(f));
1087        self
1088    }
1089
1090    // ── Accessibility overrides ────────────────────────────────────────
1091    //
1092    // The user-visible string methods take `impl Into<Prop<String>>` so
1093    // they stay reactive. With the `i18n` feature, `LocalizedString`
1094    // (produced by `tr!(...)`) provides `From<LocalizedString> for
1095    // Prop<String>`, which yields a locale-observing `Prop::Bound`, so
1096    // `.access_label(tr!(save()))` follows the locale. A bare `&str`
1097    // does NOT convert to `Prop<String>`, so untranslated literals must
1098    // go through `lit!(...)` (downstream crates) or the `_literal`
1099    // twins (which store `Prop::Static` — the only literal path
1100    // reachable from within `teksilo-core`).
1101
1102    /// Override the accessibility label (`Node::label`) of this widget.
1103    /// Replaces whatever the inner widget emitted via `set_name`.
1104    ///
1105    /// Accepts any `impl Into<Prop<String>>`. With the `i18n` feature,
1106    /// `LocalizedString` (produced by `tr!(...)`)
1107    /// implements `From<LocalizedString> for Prop<String>`, so
1108    /// `.access_label(tr!(save()))` stays reactive — the announced
1109    /// value re-resolves on locale change (the accessibility tree
1110    /// re-walks via `sync_accessibility`).
1111    pub fn access_label(mut self, label: impl Into<Prop<String>>) -> Self {
1112        self.handler_set.access_mut().label = Some(label.into());
1113        self
1114    }
1115
1116    /// `#[doc(hidden)]` grep marker for explicitly-untranslated label
1117    /// strings — the same convention as `Button::new_literal`. Stores a
1118    /// `Prop::Static`. The distinct name makes untranslated call sites
1119    /// greppable as a one-pass audit, and it's the literal path
1120    /// reachable from within `teksilo-core` (where `lit!` isn't usable).
1121    #[doc(hidden)]
1122    pub fn access_label_literal(self, label: impl Into<String>) -> Self {
1123        self.access_label(Prop::Static(label.into()))
1124    }
1125
1126    /// Override the accessibility description (`Node::description`).
1127    /// Same conversion rules as `access_label`.
1128    pub fn access_description(mut self, description: impl Into<Prop<String>>) -> Self {
1129        self.handler_set.access_mut().description = Some(description.into());
1130        self
1131    }
1132
1133    #[doc(hidden)]
1134    pub fn access_description_literal(self, description: impl Into<String>) -> Self {
1135        self.access_description(Prop::Static(description.into()))
1136    }
1137
1138    /// Long-form context hint. Alias of `access_description` —
1139    /// AccessKit has no separate hint slot (SwiftUI's split is
1140    /// VoiceOver-specific). Provided for SwiftUI parity.
1141    pub fn access_hint(self, hint: impl Into<Prop<String>>) -> Self {
1142        self.access_description(hint)
1143    }
1144
1145    #[doc(hidden)]
1146    pub fn access_hint_literal(self, hint: impl Into<String>) -> Self {
1147        self.access_description(Prop::Static(hint.into()))
1148    }
1149
1150    /// Override the accessibility value (`Node::value`).
1151    /// Same conversion rules as `access_label`.
1152    pub fn access_value(mut self, value: impl Into<Prop<String>>) -> Self {
1153        self.handler_set.access_mut().value = Some(value.into());
1154        self
1155    }
1156
1157    #[doc(hidden)]
1158    pub fn access_value_literal(self, value: impl Into<String>) -> Self {
1159        self.access_value(Prop::Static(value.into()))
1160    }
1161
1162    /// Override the accessibility role.
1163    pub fn access_role(mut self, role: accesskit::Role) -> Self {
1164        self.handler_set.access_mut().role = Some(role);
1165        self
1166    }
1167
1168    /// Hide (or un-hide) this node from assistive technologies. Accepts a
1169    /// plain `bool`, a `Signal<bool>`, or a `Prop<bool>`: a bound value makes
1170    /// the node appear/disappear from the AT tree reactively (the binding is
1171    /// registered at `AccessibilityOnly`, so the tree re-walks on change).
1172    /// `false` un-sets a hidden state the inner widget may have emitted
1173    /// unconditionally (e.g. `Panel::a11y_presentational`).
1174    pub fn access_hidden(mut self, hidden: impl Into<Prop<bool>>) -> Self {
1175        self.handler_set.access_mut().hidden = Some(hidden.into());
1176        self
1177    }
1178
1179    /// Mark (or un-mark) this widget as disabled for AT. `false`
1180    /// clears both widget-emitted disabled state AND the framework's
1181    /// arena-driven disabled gate at
1182    /// `accessibility_impl::build_accessibility_recursive`.
1183    pub fn access_disabled(mut self, disabled: bool) -> Self {
1184        self.handler_set.access_mut().disabled = Some(disabled);
1185        self
1186    }
1187
1188    /// Stable test/debug identifier (`Node::author_id`). Not
1189    /// user-visible — used by accessibility inspectors and UI tests.
1190    pub fn access_identifier(mut self, id: impl Into<String>) -> Self {
1191        self.handler_set.access_mut().identifier = Some(id.into());
1192        self
1193    }
1194
1195    /// Append a `controls` relationship. The target widget's NodeId
1196    /// is included in this node's `aria-controls`-equivalent list.
1197    pub fn access_controls(mut self, target: WidgetId) -> Self {
1198        self.handler_set.access_mut().controls.push(target);
1199        self
1200    }
1201
1202    /// Append a `described_by` relationship.
1203    pub fn access_described_by(mut self, target: WidgetId) -> Self {
1204        self.handler_set.access_mut().described_by.push(target);
1205        self
1206    }
1207
1208    /// Append a `labelled_by` relationship.
1209    pub fn access_labelled_by(mut self, target: WidgetId) -> Self {
1210        self.handler_set.access_mut().labelled_by.push(target);
1211        self
1212    }
1213
1214    /// Set the live-region politeness (`Node::live`).
1215    pub fn access_live(mut self, mode: accesskit::Live) -> Self {
1216        self.handler_set.access_mut().live = Some(mode);
1217        self
1218    }
1219
1220    /// Mark this node as the current item within its container
1221    /// (`aria-current`).
1222    pub fn access_current(mut self, current: accesskit::AriaCurrent) -> Self {
1223        self.handler_set.access_mut().aria_current = Some(current);
1224        self
1225    }
1226
1227    /// Pre-formatted shortcut announcement (e.g. `"Ctrl+S"`). Used for
1228    /// chords NOT routed through the `Shortcut` system — platform-native
1229    /// keys, app-internal hotkeys not exposed to user rebinding. For
1230    /// `Shortcut`-registered chords prefer
1231    /// [`access_shortcut_id`](Self::access_shortcut_id), which tracks
1232    /// rebinds automatically.
1233    pub fn access_shortcut_literal(mut self, shortcut: impl Into<String>) -> Self {
1234        self.handler_set.access_mut().shortcut = Some(shortcut.into());
1235        self
1236    }
1237
1238    /// Bind the announced shortcut to a registered `Shortcut` id (the
1239    /// same id you pass to `Shortcut::new("app.save")`). The
1240    /// accessibility tree walker resolves the current keystroke from
1241    /// `WidgetTree::shortcut_registry()` at AT-build time, formats it
1242    /// via `KeyStroke::Display` (`"Ctrl+S"`), and writes it to
1243    /// `Node::keyboard_shortcut`. Auto-refreshes on rebind.
1244    ///
1245    /// If the registry has no entry for `id` (no widget registered the
1246    /// shortcut yet), the announcement is omitted — same fallback as
1247    /// `MenuItem::for_shortcut(...)`.
1248    pub fn access_shortcut_id(mut self, id: impl Into<String>) -> Self {
1249        self.handler_set.access_mut().shortcut_id = Some(id.into());
1250        self
1251    }
1252
1253    /// Indicate that activating this widget pops up a menu / listbox /
1254    /// dialog (`aria-haspopup`).
1255    pub fn access_has_popup(mut self, kind: accesskit::HasPopup) -> Self {
1256        self.handler_set.access_mut().has_popup = Some(kind);
1257        self
1258    }
1259
1260    /// Override orientation (`Node::orientation`) — used on sliders,
1261    /// scrollbars, separators.
1262    pub fn access_orientation(mut self, orientation: accesskit::Orientation) -> Self {
1263        self.handler_set.access_mut().orientation = Some(orientation);
1264        self
1265    }
1266
1267    /// Prune all descendants from the accessibility tree. The widget's
1268    /// own AT node is still emitted; only children disappear. Use for
1269    /// purely decorative composites. Flutter's `excludeSemantics: true`.
1270    pub fn access_exclude_subtree(mut self) -> Self {
1271        self.handler_set.access_subtree = Some(AccessSubtreeMode::Exclude);
1272        self
1273    }
1274
1275    /// Lift descendants' labels / descriptions / values / actions into
1276    /// this widget's AT node, then prune the descendants. The whole
1277    /// composite reads as a single AT element. Flutter's
1278    /// `mergeAllDescendants: true` and SwiftUI's
1279    /// `.accessibilityElement(children: .combine)`.
1280    pub fn access_merge_subtree(mut self) -> Self {
1281        self.handler_set.access_subtree = Some(AccessSubtreeMode::Merge);
1282        self
1283    }
1284
1285    /// Set an explicit subtree mode.
1286    pub fn access_subtree(mut self, mode: AccessSubtreeMode) -> Self {
1287        self.handler_set.access_subtree = Some(mode);
1288        self
1289    }
1290
1291    /// Override `Node::numeric_value`.
1292    pub fn access_numeric_value(mut self, value: f64) -> Self {
1293        self.handler_set.access_mut().numeric_value = Some(value);
1294        self
1295    }
1296
1297    /// Override `Node::min_numeric_value` and `max_numeric_value`.
1298    pub fn access_numeric_range(mut self, min: f64, max: f64) -> Self {
1299        let access = self.handler_set.access_mut();
1300        access.min_numeric_value = Some(min);
1301        access.max_numeric_value = Some(max);
1302        self
1303    }
1304
1305    /// Override `Node::numeric_value_step`.
1306    pub fn access_numeric_step(mut self, step: f64) -> Self {
1307        self.handler_set.access_mut().numeric_step = Some(step);
1308        self
1309    }
1310
1311    /// Advertise an accessibility action and the callback that fires
1312    /// when AT software invokes it. Multiple `access_action` calls
1313    /// register separate callbacks for distinct actions; calling twice
1314    /// with the same action records both — they fire in order.
1315    pub fn access_action<F>(mut self, action: accesskit::Action, handler: F) -> Self
1316    where
1317        F: FnMut(&mut EventContext) + 'static,
1318    {
1319        self.handler_set
1320            .access_mut()
1321            .actions
1322            .push((action, Box::new(handler)));
1323        self
1324    }
1325
1326    /// Suppress an action the inner widget emitted (e.g. neutralize
1327    /// `Action::Click` on a Button used purely as a layout shim).
1328    /// Applied after the widget's `accessibility()` runs but before
1329    /// override-advertised actions, so a subsequent `access_action`
1330    /// for the same action re-advertises it with the override-installed
1331    /// callback.
1332    pub fn access_remove_action(mut self, action: accesskit::Action) -> Self {
1333        self.handler_set.access_mut().removed_actions.push(action);
1334        self
1335    }
1336
1337    /// Advertise a custom-named action (SwiftUI parity:
1338    /// `.accessibilityAction(named:_:)`). The label is exposed
1339    /// verbatim by AT software (e.g. VoiceOver's Actions rotor).
1340    /// Accepts `tr!(...)` via `From<LocalizedString> for Prop<String>`
1341    /// in `teksilo-i18n`, so the announced name follows the locale.
1342    pub fn access_custom_action<F>(mut self, label: impl Into<Prop<String>>, handler: F) -> Self
1343    where
1344        F: FnMut(&mut EventContext) + 'static,
1345    {
1346        self.handler_set
1347            .access_mut()
1348            .custom_actions
1349            .push((label.into(), Box::new(handler)));
1350        self
1351    }
1352
1353    #[doc(hidden)]
1354    pub fn access_custom_action_literal<F>(self, label: impl Into<String>, handler: F) -> Self
1355    where
1356        F: FnMut(&mut EventContext) + 'static,
1357    {
1358        self.access_custom_action(Prop::Static(label.into()), handler)
1359    }
1360
1361    /// Final escape hatch — invoked after all typed override setters,
1362    /// with full `&mut AccessNodeBuilder` access (including
1363    /// `inner_mut()`). Use for synthetic-child surgery (rich text
1364    /// paragraphs, text runs) or any AccessKit field the typed
1365    /// surface doesn't cover.
1366    pub fn access_customize<F>(mut self, f: F) -> Self
1367    where
1368        F: Fn(&mut crate::accessibility::AccessNodeBuilder) + 'static,
1369    {
1370        self.handler_set.access_mut().customize = Some(Box::new(f));
1371        self
1372    }
1373}
1374
1375// Delegate all Widget trait methods to the inner widget.
1376impl<W: Widget> std::fmt::Debug for WidgetWithHandlers<W> {
1377    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
1378        f.debug_struct("WidgetWithHandlers")
1379            .field("widget", &self.widget)
1380            .field("handler_set", &self.handler_set)
1381            .finish()
1382    }
1383}
1384
1385impl<W: Widget + 'static> Widget for WidgetWithHandlers<W> {
1386    fn build(
1387        &mut self,
1388        ctx: &mut crate::build_context::BuildContext,
1389    ) -> Vec<crate::widget_id::WidgetId> {
1390        self.widget.build(ctx)
1391    }
1392
1393    fn layout_response(
1394        &self,
1395        proposal: teksilo_canvas::SizeProposal,
1396        ctx: &crate::widget::LayoutContext,
1397    ) -> crate::widget::LayoutResponse {
1398        self.widget.layout_response(proposal, ctx)
1399    }
1400
1401    fn place_children(
1402        &self,
1403        bounds: teksilo_canvas::Rect,
1404        proposal: teksilo_canvas::SizeProposal,
1405        children: &mut [crate::widget::WidgetPlacement],
1406        ctx: &crate::widget::LayoutContext,
1407    ) {
1408        self.widget.place_children(bounds, proposal, children, ctx)
1409    }
1410
1411    fn paint(
1412        &self,
1413        bounds: teksilo_canvas::Rect,
1414        canvas: &mut teksilo_canvas::Canvas,
1415        ctx: &crate::widget::PaintContext,
1416    ) {
1417        self.widget.paint(bounds, canvas, ctx)
1418    }
1419
1420    fn accessibility(&self, builder: &mut crate::accessibility::AccessNodeBuilder) {
1421        self.widget.accessibility(builder)
1422    }
1423
1424    fn children(&self) -> Vec<crate::widget_id::WidgetId> {
1425        self.widget.children()
1426    }
1427
1428    fn as_any(&self) -> Option<&dyn std::any::Any> {
1429        self.widget.as_any()
1430    }
1431
1432    /// Mutable counterpart of [`as_any`](Widget::as_any), forwarded for the
1433    /// same reason it is.
1434    ///
1435    /// A composing container that reads a child's concrete type must see the
1436    /// same widget whether or not a builder method wrapped it. `MenuList` reads
1437    /// a `MenuItem` this way for its mnemonic, its type-ahead label, its radio
1438    /// group and its safe-triangle state; without this forward,
1439    /// `MenuItem::new(..).context_menu(..)` silently stops being a `MenuItem`
1440    /// to its parent — no error, just a row that lost all four.
1441    fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
1442        self.widget.as_any_mut()
1443    }
1444
1445    fn clips_children(&self) -> bool {
1446        self.handler_set
1447            .clips_children
1448            .unwrap_or_else(|| self.widget.clips_children())
1449    }
1450
1451    fn take_handler_set(&mut self) -> Option<HandlerSet> {
1452        Some(self.take_handler_set())
1453    }
1454}
1455
1456// ---------------------------------------------------------------------------
1457// WidgetBuilder trait — the entry point
1458// ---------------------------------------------------------------------------
1459
1460/// Blanket trait providing attached handler methods for all Widget types.
1461/// The first builder method call wraps the widget in `WidgetWithHandlers`.
1462pub trait WidgetBuilder: Widget + Sized + 'static {
1463    fn on_tap(
1464        self,
1465        f: impl FnMut(&TapEvent, &mut EventContext) + 'static,
1466    ) -> WidgetWithHandlers<Self> {
1467        WidgetWithHandlers::new(self).on_tap(f)
1468    }
1469
1470    fn on_double_tap(
1471        self,
1472        f: impl FnMut(&TapEvent, &mut EventContext) + 'static,
1473    ) -> WidgetWithHandlers<Self> {
1474        WidgetWithHandlers::new(self).on_double_tap(f)
1475    }
1476
1477    fn on_triple_tap(
1478        self,
1479        f: impl FnMut(&TapEvent, &mut EventContext) + 'static,
1480    ) -> WidgetWithHandlers<Self> {
1481        WidgetWithHandlers::new(self).on_triple_tap(f)
1482    }
1483
1484    fn on_long_press(
1485        self,
1486        f: impl FnMut(&TapEvent, &mut EventContext) + 'static,
1487    ) -> WidgetWithHandlers<Self> {
1488        WidgetWithHandlers::new(self).on_long_press(f)
1489    }
1490
1491    /// Restrict (or extend) the set of pointer buttons that fire
1492    /// `on_tap`. Default is [`ButtonMask::PRIMARY`].
1493    fn accept_tap_buttons(self, mask: impl Into<ButtonMask>) -> WidgetWithHandlers<Self> {
1494        WidgetWithHandlers::new(self).accept_tap_buttons(mask)
1495    }
1496
1497    /// Restrict (or extend) the set of pointer buttons that fire
1498    /// `on_double_tap`. Default [`ButtonMask::PRIMARY`].
1499    fn accept_double_tap_buttons(self, mask: impl Into<ButtonMask>) -> WidgetWithHandlers<Self> {
1500        WidgetWithHandlers::new(self).accept_double_tap_buttons(mask)
1501    }
1502
1503    /// Restrict (or extend) the set of pointer buttons that fire
1504    /// `on_triple_tap`. Default [`ButtonMask::PRIMARY`].
1505    fn accept_triple_tap_buttons(self, mask: impl Into<ButtonMask>) -> WidgetWithHandlers<Self> {
1506        WidgetWithHandlers::new(self).accept_triple_tap_buttons(mask)
1507    }
1508
1509    /// Restrict (or extend) the set of pointer buttons that fire
1510    /// `on_long_press`. Default [`ButtonMask::PRIMARY`].
1511    fn accept_long_press_buttons(self, mask: impl Into<ButtonMask>) -> WidgetWithHandlers<Self> {
1512        WidgetWithHandlers::new(self).accept_long_press_buttons(mask)
1513    }
1514
1515    /// Dim this widget's subtree to `factor` opacity whenever the host window
1516    /// is **inactive** (not focused / occluded), restoring full opacity when it
1517    /// becomes active again. The opt-in, per-widget layer of the window-active
1518    /// appearance model — for custom content an app wants to fade back when its
1519    /// window isn't the active one. Stock widgets handle their own
1520    /// inactive appearance (caret hiding, selection desaturation) and need no
1521    /// wrapping. Layout- and a11y-transparent; the opacity snaps (no tween),
1522    /// which is correct under `prefers-reduced-motion`. See
1523    /// [`DimWhenInactive`](crate::dim_when_inactive::DimWhenInactive).
1524    fn dim_when_inactive(self, factor: f32) -> crate::dim_when_inactive::DimWhenInactive {
1525        crate::dim_when_inactive::DimWhenInactive::new()
1526            .child(self)
1527            .factor(factor)
1528    }
1529
1530    /// [`dim_when_inactive`](Self::dim_when_inactive) with the default factor
1531    /// ([`DEFAULT_DIM_FACTOR`](crate::dim_when_inactive::DEFAULT_DIM_FACTOR), 70 %).
1532    fn dim_when_inactive_default(self) -> crate::dim_when_inactive::DimWhenInactive {
1533        crate::dim_when_inactive::DimWhenInactive::new().child(self)
1534    }
1535
1536    fn on_drag(
1537        self,
1538        f: impl FnMut(DragPhase, &mut EventContext) + 'static,
1539    ) -> WidgetWithHandlers<Self> {
1540        WidgetWithHandlers::new(self).on_drag(f)
1541    }
1542
1543    fn on_swipe(
1544        self,
1545        f: impl FnMut(SwipeDirection, f32, &mut EventContext) + 'static,
1546    ) -> WidgetWithHandlers<Self> {
1547        WidgetWithHandlers::new(self).on_swipe(f)
1548    }
1549
1550    fn on_pinch(
1551        self,
1552        f: impl FnMut(PinchPhase, &mut EventContext) + 'static,
1553    ) -> WidgetWithHandlers<Self> {
1554        WidgetWithHandlers::new(self).on_pinch(f)
1555    }
1556
1557    fn on_focus(
1558        self,
1559        f: impl FnMut(bool, &mut EventContext) + 'static,
1560    ) -> WidgetWithHandlers<Self> {
1561        WidgetWithHandlers::new(self).on_focus(f)
1562    }
1563
1564    fn on_key(
1565        self,
1566        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
1567    ) -> WidgetWithHandlers<Self> {
1568        WidgetWithHandlers::new(self).on_key(f)
1569    }
1570
1571    /// Strict-ancestor key preview. See [`HandlerSet::on_key_preview`].
1572    fn on_key_preview(
1573        self,
1574        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
1575    ) -> WidgetWithHandlers<Self> {
1576        WidgetWithHandlers::new(self).on_key_preview(f)
1577    }
1578
1579    fn on_pointer_event(
1580        self,
1581        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
1582    ) -> WidgetWithHandlers<Self> {
1583        WidgetWithHandlers::new(self).on_pointer_event(f)
1584    }
1585
1586    fn on_hover(
1587        self,
1588        f: impl FnMut(bool, &mut EventContext) + 'static,
1589    ) -> WidgetWithHandlers<Self> {
1590        WidgetWithHandlers::new(self).on_hover(f)
1591    }
1592
1593    fn on_scroll(
1594        self,
1595        f: impl FnMut(&WidgetEvent, &mut EventContext) -> EventResponse + 'static,
1596    ) -> WidgetWithHandlers<Self> {
1597        WidgetWithHandlers::new(self).on_scroll(f)
1598    }
1599
1600    fn on_access_action(
1601        self,
1602        f: impl FnMut(accesskit::Action, &mut EventContext) -> EventResponse + 'static,
1603    ) -> WidgetWithHandlers<Self> {
1604        WidgetWithHandlers::new(self).on_access_action(f)
1605    }
1606
1607    fn focusable(self, focusable: bool) -> WidgetWithHandlers<Self> {
1608        WidgetWithHandlers::new(self).focusable(focusable)
1609    }
1610
1611    fn tab_index(self, index: i32) -> WidgetWithHandlers<Self> {
1612        WidgetWithHandlers::new(self).tab_index(index)
1613    }
1614
1615    fn cursor(self, cursor: CursorIcon) -> WidgetWithHandlers<Self> {
1616        WidgetWithHandlers::new(self).cursor(cursor)
1617    }
1618
1619    fn clips_children_on(self, clips: bool) -> WidgetWithHandlers<Self> {
1620        WidgetWithHandlers::new(self).clips_children(clips)
1621    }
1622
1623    /// Declare this node a text-input surface, enabling the OS input method
1624    /// (with `ctx`'s purpose) while it is focused. See [`crate::ime`].
1625    fn ime_input(self, ctx: crate::ime::ImeContext) -> WidgetWithHandlers<Self> {
1626        WidgetWithHandlers::new(self).ime_input(ctx)
1627    }
1628
1629    /// Make the widget invisible to pointer hit-testing. See
1630    /// [`HandlerSet::event_pass_through`].
1631    fn event_pass_through(self, pass_through: bool) -> WidgetWithHandlers<Self> {
1632        WidgetWithHandlers::new(self).event_pass_through(pass_through)
1633    }
1634
1635    /// Mark this widget's subtree a gesture dead zone. See
1636    /// [`HandlerSet::gesture_dead_zone`].
1637    fn gesture_dead_zone(self, dead_zone: bool) -> WidgetWithHandlers<Self> {
1638        WidgetWithHandlers::new(self).gesture_dead_zone(dead_zone)
1639    }
1640
1641    /// Mark this widget a keyboard capture surface (terminals, game
1642    /// viewports): while focused, `KeyDown`s bypass shortcut resolution.
1643    /// See [`HandlerSet::keyboard_capture`].
1644    fn keyboard_capture(self, capture: bool) -> WidgetWithHandlers<Self> {
1645        WidgetWithHandlers::new(self).keyboard_capture(capture)
1646    }
1647
1648    /// Make this widget and its whole subtree invisible to pointer
1649    /// hit-testing (decorative overlays). See
1650    /// [`HandlerSet::hit_transparent`].
1651    fn hit_transparent(self, transparent: bool) -> WidgetWithHandlers<Self> {
1652        WidgetWithHandlers::new(self).hit_transparent(transparent)
1653    }
1654
1655    /// Set a context-menu factory. See
1656    /// [`HandlerSet::context_menu`] for the full contract.
1657    fn context_menu(
1658        self,
1659        factory: impl Fn(Point, &mut EventContext) -> Option<Box<dyn Widget>> + 'static,
1660    ) -> WidgetWithHandlers<Self> {
1661        WidgetWithHandlers::new(self).context_menu(factory)
1662    }
1663
1664    /// Bind a `Signal<bool>` the framework writes when a strict
1665    /// descendant has focus. See [`HandlerSet::focus_within`].
1666    fn focus_within(self, signal: crate::signal::Signal<bool>) -> WidgetWithHandlers<Self> {
1667        WidgetWithHandlers::new(self).focus_within(signal)
1668    }
1669
1670    /// Bind a `Signal<bool>` the framework writes when a strict
1671    /// descendant is hovered. See [`HandlerSet::hover_within`].
1672    fn hover_within(self, signal: crate::signal::Signal<bool>) -> WidgetWithHandlers<Self> {
1673        WidgetWithHandlers::new(self).hover_within(signal)
1674    }
1675
1676    /// Bind this node's visibility (`bool` / `Signal<bool>` / `Prop<bool>`) as
1677    /// a builder property, so `teksu!` can write `visible_when: sig`. Equivalent
1678    /// to `ctx.visible_when(id, ..)`. See [`HandlerSet::visible_when`].
1679    fn visible_when(self, state: impl Into<Prop<bool>>) -> WidgetWithHandlers<Self> {
1680        WidgetWithHandlers::new(self).visible_when(state)
1681    }
1682
1683    fn on_drag_hover(
1684        self,
1685        f: impl FnMut(
1686            &crate::drag_payload::DragPayload,
1687            teksilo_canvas::Point,
1688            &mut EventContext,
1689        ) -> crate::drag_state::DropFeedback
1690        + 'static,
1691    ) -> WidgetWithHandlers<Self> {
1692        WidgetWithHandlers::new(self).on_drag_hover(f)
1693    }
1694
1695    fn on_drag_leave(self, f: impl FnMut(&mut EventContext) + 'static) -> WidgetWithHandlers<Self> {
1696        WidgetWithHandlers::new(self).on_drag_leave(f)
1697    }
1698
1699    fn on_drag_tick(
1700        self,
1701        f: impl FnMut(teksilo_canvas::Point, &mut EventContext) + 'static,
1702    ) -> WidgetWithHandlers<Self> {
1703        WidgetWithHandlers::new(self).on_drag_tick(f)
1704    }
1705
1706    fn on_drop(
1707        self,
1708        f: impl FnMut(
1709            crate::drag_payload::DragPayload,
1710            teksilo_canvas::Point,
1711            &mut EventContext,
1712        ) -> bool
1713        + 'static,
1714    ) -> WidgetWithHandlers<Self> {
1715        WidgetWithHandlers::new(self).on_drop(f)
1716    }
1717
1718    /// Set the drag-ended handler on a drag source. See
1719    /// [`HandlerSet::on_drag_ended`].
1720    fn on_drag_ended(
1721        self,
1722        f: impl FnMut(crate::drag_payload::DropOutcome, &mut EventContext) + 'static,
1723    ) -> WidgetWithHandlers<Self> {
1724        WidgetWithHandlers::new(self).on_drag_ended(f)
1725    }
1726
1727    // ── Accessibility overrides ────────────────────────────────────────
1728    //
1729    // Trait-level entry points: each method wraps the widget into a
1730    // `WidgetWithHandlers` (the first builder call in any chain) and
1731    // forwards to the inherent method of the same name. See
1732    // `WidgetWithHandlers` for full rustdoc on each method's semantics.
1733    // For translated strings, `LocalizedString` flows through
1734    // `impl Into<Prop<String>>` via `teksilo-i18n`'s
1735    // `From<LocalizedString> for Prop<String>` impl, staying reactive.
1736
1737    fn access_label(self, label: impl Into<Prop<String>>) -> WidgetWithHandlers<Self> {
1738        WidgetWithHandlers::new(self).access_label(label)
1739    }
1740
1741    #[doc(hidden)]
1742    fn access_label_literal(self, label: impl Into<String>) -> WidgetWithHandlers<Self> {
1743        WidgetWithHandlers::new(self).access_label_literal(label)
1744    }
1745
1746    fn access_description(self, description: impl Into<Prop<String>>) -> WidgetWithHandlers<Self> {
1747        WidgetWithHandlers::new(self).access_description(description)
1748    }
1749
1750    #[doc(hidden)]
1751    fn access_description_literal(
1752        self,
1753        description: impl Into<String>,
1754    ) -> WidgetWithHandlers<Self> {
1755        WidgetWithHandlers::new(self).access_description_literal(description)
1756    }
1757
1758    fn access_hint(self, hint: impl Into<Prop<String>>) -> WidgetWithHandlers<Self> {
1759        WidgetWithHandlers::new(self).access_hint(hint)
1760    }
1761
1762    #[doc(hidden)]
1763    fn access_hint_literal(self, hint: impl Into<String>) -> WidgetWithHandlers<Self> {
1764        WidgetWithHandlers::new(self).access_hint_literal(hint)
1765    }
1766
1767    fn access_value(self, value: impl Into<Prop<String>>) -> WidgetWithHandlers<Self> {
1768        WidgetWithHandlers::new(self).access_value(value)
1769    }
1770
1771    #[doc(hidden)]
1772    fn access_value_literal(self, value: impl Into<String>) -> WidgetWithHandlers<Self> {
1773        WidgetWithHandlers::new(self).access_value_literal(value)
1774    }
1775
1776    fn access_role(self, role: accesskit::Role) -> WidgetWithHandlers<Self> {
1777        WidgetWithHandlers::new(self).access_role(role)
1778    }
1779
1780    fn access_hidden(self, hidden: impl Into<Prop<bool>>) -> WidgetWithHandlers<Self> {
1781        WidgetWithHandlers::new(self).access_hidden(hidden)
1782    }
1783
1784    fn access_disabled(self, disabled: bool) -> WidgetWithHandlers<Self> {
1785        WidgetWithHandlers::new(self).access_disabled(disabled)
1786    }
1787
1788    fn access_identifier(self, id: impl Into<String>) -> WidgetWithHandlers<Self> {
1789        WidgetWithHandlers::new(self).access_identifier(id)
1790    }
1791
1792    fn access_controls(self, target: WidgetId) -> WidgetWithHandlers<Self> {
1793        WidgetWithHandlers::new(self).access_controls(target)
1794    }
1795
1796    fn access_described_by(self, target: WidgetId) -> WidgetWithHandlers<Self> {
1797        WidgetWithHandlers::new(self).access_described_by(target)
1798    }
1799
1800    fn access_labelled_by(self, target: WidgetId) -> WidgetWithHandlers<Self> {
1801        WidgetWithHandlers::new(self).access_labelled_by(target)
1802    }
1803
1804    fn access_live(self, mode: accesskit::Live) -> WidgetWithHandlers<Self> {
1805        WidgetWithHandlers::new(self).access_live(mode)
1806    }
1807
1808    fn access_current(self, current: accesskit::AriaCurrent) -> WidgetWithHandlers<Self> {
1809        WidgetWithHandlers::new(self).access_current(current)
1810    }
1811
1812    fn access_shortcut_literal(self, shortcut: impl Into<String>) -> WidgetWithHandlers<Self> {
1813        WidgetWithHandlers::new(self).access_shortcut_literal(shortcut)
1814    }
1815
1816    fn access_shortcut_id(self, id: impl Into<String>) -> WidgetWithHandlers<Self> {
1817        WidgetWithHandlers::new(self).access_shortcut_id(id)
1818    }
1819
1820    fn access_has_popup(self, kind: accesskit::HasPopup) -> WidgetWithHandlers<Self> {
1821        WidgetWithHandlers::new(self).access_has_popup(kind)
1822    }
1823
1824    fn access_orientation(self, orientation: accesskit::Orientation) -> WidgetWithHandlers<Self> {
1825        WidgetWithHandlers::new(self).access_orientation(orientation)
1826    }
1827
1828    fn access_exclude_subtree(self) -> WidgetWithHandlers<Self> {
1829        WidgetWithHandlers::new(self).access_exclude_subtree()
1830    }
1831
1832    fn access_merge_subtree(self) -> WidgetWithHandlers<Self> {
1833        WidgetWithHandlers::new(self).access_merge_subtree()
1834    }
1835
1836    fn access_subtree(self, mode: AccessSubtreeMode) -> WidgetWithHandlers<Self> {
1837        WidgetWithHandlers::new(self).access_subtree(mode)
1838    }
1839
1840    fn access_numeric_value(self, value: f64) -> WidgetWithHandlers<Self> {
1841        WidgetWithHandlers::new(self).access_numeric_value(value)
1842    }
1843
1844    fn access_numeric_range(self, min: f64, max: f64) -> WidgetWithHandlers<Self> {
1845        WidgetWithHandlers::new(self).access_numeric_range(min, max)
1846    }
1847
1848    fn access_numeric_step(self, step: f64) -> WidgetWithHandlers<Self> {
1849        WidgetWithHandlers::new(self).access_numeric_step(step)
1850    }
1851
1852    fn access_action<F>(self, action: accesskit::Action, handler: F) -> WidgetWithHandlers<Self>
1853    where
1854        F: FnMut(&mut EventContext) + 'static,
1855    {
1856        WidgetWithHandlers::new(self).access_action(action, handler)
1857    }
1858
1859    fn access_remove_action(self, action: accesskit::Action) -> WidgetWithHandlers<Self> {
1860        WidgetWithHandlers::new(self).access_remove_action(action)
1861    }
1862
1863    fn access_custom_action<F>(
1864        self,
1865        label: impl Into<Prop<String>>,
1866        handler: F,
1867    ) -> WidgetWithHandlers<Self>
1868    where
1869        F: FnMut(&mut EventContext) + 'static,
1870    {
1871        WidgetWithHandlers::new(self).access_custom_action(label, handler)
1872    }
1873
1874    #[doc(hidden)]
1875    fn access_custom_action_literal<F>(
1876        self,
1877        label: impl Into<String>,
1878        handler: F,
1879    ) -> WidgetWithHandlers<Self>
1880    where
1881        F: FnMut(&mut EventContext) + 'static,
1882    {
1883        WidgetWithHandlers::new(self).access_custom_action_literal(label, handler)
1884    }
1885
1886    fn access_customize<F>(self, f: F) -> WidgetWithHandlers<Self>
1887    where
1888        F: Fn(&mut crate::accessibility::AccessNodeBuilder) + 'static,
1889    {
1890        WidgetWithHandlers::new(self).access_customize(f)
1891    }
1892}
1893
1894// Blanket implementation for all Widget types.
1895impl<W: Widget + Sized + 'static> WidgetBuilder for W {}
1896
1897#[cfg(test)]
1898mod tests {
1899    use super::*;
1900    use crate::widget::WidgetPlacement;
1901    use crate::widget_id::WidgetId;
1902    use crate::widget_tree::WidgetTree;
1903
1904    #[derive(Debug)]
1905    struct CompositeLeaf {
1906        child_id: Option<WidgetId>,
1907    }
1908
1909    impl CompositeLeaf {
1910        fn new() -> Self {
1911            Self { child_id: None }
1912        }
1913    }
1914
1915    impl Widget for CompositeLeaf {
1916        fn build(&mut self, ctx: &mut crate::build_context::BuildContext) -> Vec<WidgetId> {
1917            let child = ctx.add(crate::test_widgets::FillWidget::new());
1918            self.child_id = Some(child);
1919            vec![child]
1920        }
1921
1922        fn layout_response(
1923            &self,
1924            proposal: teksilo_canvas::SizeProposal,
1925            _ctx: &crate::widget::LayoutContext,
1926        ) -> crate::widget::LayoutResponse {
1927            proposal.resolve(120.0, 40.0).into()
1928        }
1929
1930        fn place_children(
1931            &self,
1932            bounds: teksilo_canvas::Rect,
1933            _proposal: teksilo_canvas::SizeProposal,
1934            children: &mut [WidgetPlacement],
1935            _ctx: &crate::widget::LayoutContext,
1936        ) {
1937            for child in children.iter_mut() {
1938                child.origin = bounds.origin();
1939                child.size = bounds.size();
1940            }
1941        }
1942
1943        fn children(&self) -> Vec<WidgetId> {
1944            self.child_id.into_iter().collect()
1945        }
1946    }
1947
1948    #[test]
1949    fn external_handlers_survive_rebuild() {
1950        // Regression check: handlers attached externally via the
1951        // `WidgetBuilder` builder (e.g. `MyCompositeWidget::new().on_tap(...)`)
1952        // must continue to fire after the widget rebuilds in place.
1953        // My handler-clearing fix in `rebuild_single_widget` wiped
1954        // `node.handlers` to stop accumulation of `apply_self_handlers`
1955        // calls across rebuilds — but the extracted-once-at-insertion
1956        // HandlerSet is gone by rebuild time and would be lost.
1957        use std::cell::Cell;
1958        use std::rc::Rc;
1959
1960        let tap_count = Rc::new(Cell::new(0_u32));
1961        let tc = tap_count.clone();
1962
1963        let mut tree = WidgetTree::new();
1964        let id = tree.add(CompositeLeaf::new().on_tap(move |_pos, _ctx| {
1965            tc.set(tc.get() + 1);
1966        }));
1967        tree.layout(teksilo_canvas::SizeProposal::exact(200.0, 100.0));
1968
1969        // Trip a rebuild of the composite — its child gets torn down &
1970        // rebuilt; node.handlers gets cleared and reset.
1971        tree.arena_mark_needs_rebuild_for_testing(id);
1972        tree.layout(teksilo_canvas::SizeProposal::exact(200.0, 100.0));
1973
1974        // Click through the composite; the externally-attached on_tap
1975        // must still be wired up.
1976        tree.click(id);
1977        assert_eq!(
1978            tap_count.get(),
1979            1,
1980            "externally-attached on_tap must survive a rebuild"
1981        );
1982    }
1983
1984    #[test]
1985    fn wrapped_composite_widget_still_builds_children() {
1986        let mut tree = WidgetTree::new();
1987        let root = tree.add(CompositeLeaf::new().on_tap(|_pos, _ctx| {}));
1988        tree.layout(teksilo_canvas::SizeProposal::exact(200.0, 100.0));
1989
1990        assert_eq!(tree.children(root).len(), 1);
1991    }
1992
1993    /// A widget that exposes both downcast hooks, like every widget a
1994    /// composing container reads its child's concrete type through.
1995    #[derive(Debug)]
1996    struct Reflective {
1997        marker: u32,
1998    }
1999
2000    impl Widget for Reflective {
2001        fn layout_response(
2002            &self,
2003            proposal: teksilo_canvas::SizeProposal,
2004            _ctx: &crate::widget::LayoutContext,
2005        ) -> crate::widget::LayoutResponse {
2006            proposal.resolve(0.0, 0.0).into()
2007        }
2008
2009        fn as_any(&self) -> Option<&dyn std::any::Any> {
2010            Some(self)
2011        }
2012
2013        fn as_any_mut(&mut self) -> Option<&mut dyn std::any::Any> {
2014            Some(self)
2015        }
2016    }
2017
2018    /// Decorating a widget must not hide its concrete type from a parent that
2019    /// reads it. `as_any` was already forwarded; `as_any_mut` was not, so a
2020    /// container reading a child through the mutable hook (`MenuList` does, for
2021    /// mnemonics, the type-ahead label and radio grouping) silently saw nothing
2022    /// the moment any builder method was called on that child.
2023    #[test]
2024    fn both_downcast_hooks_see_through_the_handler_wrapper() {
2025        let mut wrapped = Reflective { marker: 7 }.focusable(true);
2026
2027        let seen = wrapped
2028            .as_any()
2029            .and_then(|a| a.downcast_ref::<Reflective>())
2030            .map(|r| r.marker);
2031        assert_eq!(seen, Some(7), "as_any must forward through the wrapper");
2032
2033        let seen_mut = wrapped
2034            .as_any_mut()
2035            .and_then(|a| a.downcast_mut::<Reflective>())
2036            .map(|r| r.marker);
2037        assert_eq!(
2038            seen_mut,
2039            Some(7),
2040            "as_any_mut must forward through the wrapper too"
2041        );
2042    }
2043}