1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
// SPDX-License-Identifier: MPL-2.0
// SPDX-FileCopyrightText: 2026 FernTech
use teksilo_tokens::PointerKind;
/// How focus was acquired.
///
/// Read by `:focus-visible` — a focus ring belongs to keyboard and assistive
/// navigation, not to a click — and by anything that has to treat a finger
/// differently from a mouse. The pointer arm carries the device that delivered
/// the focus, because "a pointer focused this" is not one behaviour: a mouse
/// focuses on press, a finger and a pen focus on release, and only a release
/// that lands back on the same focusable counts.
///
/// `#[non_exhaustive]`: matches need a wildcard arm. Most call sites want
/// [`is_pointer`](Self::is_pointer) rather than a match at all.
#[non_exhaustive]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum FocusOrigin {
/// Focus gained via Tab/Shift-Tab keyboard navigation, or by any other
/// keystroke a widget routes into focus.
Keyboard,
/// Focus gained by pointing at the widget, with the device that did it.
Pointer(PointerKind),
/// Focus set programmatically by the application. Carries no input
/// modality of its own: a scripted focus leaves the focus ring exactly as
/// the user's last real interaction left it, which is what
/// `:focus-visible` does for `element.focus()`.
Programmatic,
/// Focus moved by assistive technology — a screen reader's
/// [`Action::Focus`](accesskit::Action::Focus), or an automation client
/// standing in for one. Reveals the focus ring: the user is navigating,
/// they are simply not doing it with a key.
Accessibility,
}
impl FocusOrigin {
/// Focus arrived by pointer, from a site that cannot know which device
/// delivered it.
///
/// A control deriving its own origin from hover or from the tree's
/// input-modality signal — rather than from the
/// [`FocusGained`](crate::event::WidgetEvent::FocusGained) it was handed —
/// knows only that the keyboard was not involved. It says so with
/// [`PointerKind::Unknown`] instead of naming a device it never saw, so a
/// consumer reading [`pointer_kind`](Self::pointer_kind) is never told a
/// finger was a mouse.
pub const POINTER: Self = Self::Pointer(PointerKind::Unknown);
/// Whether focus arrived by pointing at the widget.
///
/// The predicate that replaced `== FocusOrigin::Pointer`: a pointer origin
/// now names its device, so equality against a bare variant no longer
/// compiles and equality against one device would silently exclude the
/// others.
pub const fn is_pointer(self) -> bool {
matches!(self, Self::Pointer(_))
}
/// The device that delivered a pointer focus, or `None` for every other
/// origin. [`PointerKind::Unknown`] for a widget-side derivation — see
/// [`POINTER`](Self::POINTER).
pub const fn pointer_kind(self) -> Option<PointerKind> {
match self {
Self::Pointer(kind) => Some(kind),
_ => None,
}
}
/// Whether this origin reveals the focus ring — the `:focus-visible`
/// question, answered in one place so the tree's modality signal and any
/// widget consulting the origin cannot disagree.
///
/// Keyboard and assistive navigation reveal it; a pointer hides it.
/// [`Programmatic`](Self::Programmatic) answers neither: a scripted focus
/// declares no modality, so the tree leaves the signal where the last real
/// interaction put it.
pub const fn focus_visible(self) -> Option<bool> {
match self {
Self::Keyboard | Self::Accessibility => Some(true),
Self::Pointer(_) => Some(false),
Self::Programmatic => None,
}
}
}
/// Policy for a focus **traversal scope**, declared via the `FocusScope`
/// wrapper widget. Controls what Tab / Shift+Tab does when it reaches the
/// scope's ends.
///
/// A scope groups + scopes the `tab_index` numbering of its descendants:
/// two sibling scopes that both number their children `1, 2, 3` never
/// interleave — each scope is an independent, ordered unit within its
/// parent. This is Teksilo's analogue of Flutter `FocusTraversalGroup` /
/// WPF `KeyboardNavigation.TabNavigation`.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TraversalScopePolicy {
/// Tab flows *out* of the scope at its ends into the enclosing scope's
/// next member. The scope groups `tab_index` numbering without trapping
/// focus — use for logical regions in a continuous Tab order (e.g. dock
/// panels, where each panel numbers its own controls without colliding
/// with sibling panels).
Continue,
/// Tab *wraps* within the scope and never exits via keyboard navigation.
/// Use for modal dialogs — the one surface whose pattern (ARIA's Dialog
/// (Modal)) actually calls for containing focus.
///
/// **Not for popovers or menus.** Those implement Disclosure and Menu,
/// which mandate the opposite: Tab is an exit gesture there, and the
/// framework already answers it by dismissing the overlay focus leaves
/// rather than by trapping focus inside it. Wrapping such an overlay in a
/// `Cycle` scope defeats that — focus can no longer leave, so the
/// dismissal never fires and the panel becomes keyboard-inescapable except
/// via Escape.
Cycle,
}