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