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