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