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