frus_widgets/interaction.rs
1//! Widget identity, keyboard keys and interaction state.
2//!
3//! A [`WidgetId`] identifies a widget by its **position** in the tree (the path
4//! root → child indices), stable from one frame to the next for as long as the
5//! structure does not change. It is the founding brick of reconciliation, and
6//! what makes it possible to track hover, press and **focus**.
7
8/// The shape of the **system cursor** a widget can request for a given sub-region
9/// (milestone 205). The shell translates it to the window's cursor; widgets stay
10/// independent of the windowing layer.
11#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
12pub enum Cursor {
13 /// The default arrow.
14 #[default]
15 Default,
16 /// A hand (a clickable element).
17 Pointer,
18 /// A vertical bar (a text entry area).
19 Text,
20}
21
22/// A widget's positional identity: a hash of its path in the tree.
23#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
24pub struct WidgetId(u64);
25
26impl WidgetId {
27 /// The root's identity.
28 pub(crate) const ROOT: WidgetId = WidgetId(0xcbf29ce484222325);
29
30 /// The identity's raw value, used to tag primitives.
31 pub fn as_u64(self) -> u64 {
32 self.0
33 }
34
35 /// Rebuilds an identity from its raw value (the inverse of [`as_u64`]) — to
36 /// route an action coming from an external layer (accessibility).
37 ///
38 /// [`as_u64`]: WidgetId::as_u64
39 pub fn from_u64(raw: u64) -> WidgetId {
40 WidgetId(raw)
41 }
42
43 /// Derives the identity of this widget's `index`-th child (positional).
44 pub(crate) fn child(self, index: usize) -> WidgetId {
45 let mut h = self.0 ^ (index as u64).wrapping_add(0x9e37_79b9_7f4a_7c15);
46 h = h.wrapping_mul(0x0000_0100_0000_01b3);
47 h ^= h >> 29;
48 WidgetId(h)
49 }
50
51 /// Derives the identity of the **dismissal barrier** under an overlay: the
52 /// window-wide region whose press closes it.
53 ///
54 /// It needs one of its own because it is not a widget. It used to borrow the
55 /// overlay's root identity, and a root that registered a target of its own then had
56 /// two different regions under one name — which [`crate::Ui::msg_for`] resolves by
57 /// identity alone, so a press on a menu's own surface read back as the press that
58 /// closes it.
59 pub(crate) fn barrier(self) -> WidgetId {
60 let mut h = self.0 ^ 0x243f_6a88_85a3_08d3;
61 h = h.wrapping_mul(0x0000_0100_0000_01b3);
62 h ^= h >> 27;
63 WidgetId(h)
64 }
65
66 /// Derives a child's identity **by key** (stable whatever its position).
67 /// Distinct from [`WidgetId::child`] (a different constant and shift).
68 pub(crate) fn keyed(self, key: u64) -> WidgetId {
69 let mut h = self.0 ^ key.wrapping_add(0x517c_c1b7_2722_0a95);
70 h = h.wrapping_mul(0x0000_0100_0000_01b3);
71 h ^= h >> 31;
72 WidgetId(h)
73 }
74}
75
76/// A widget's response to a key received during the **leaf→root bubble** (the
77/// focused one first, then its ancestors for as long as the result is `Ignored`).
78#[derive(Clone, Debug, PartialEq)]
79pub enum KeyResponse<Msg = crate::callback::Callback> {
80 /// Not concerned: the key keeps bubbling up.
81 Ignored,
82 /// Consumed (with an optional message to emit): the bubbling stops.
83 Handled(Option<Msg>),
84 /// Not consumed, but the bubbling stops, and no fallback is attempted.
85 Skip,
86}
87
88/// A key passed to the focused widget.
89#[derive(Clone, Debug, PartialEq)]
90pub enum Key {
91 /// Typed text (one or more characters) — also used for pasting.
92 Text(String),
93 /// Backspace (deletes the selection, otherwise the character to the left).
94 Backspace,
95 /// Forward delete (the selection, otherwise the character to the right).
96 Delete,
97 /// Enter.
98 Enter,
99 /// Left arrow (`shift`: extends the selection; `word`: jumps a word, Ctrl).
100 Left {
101 /// Whether Shift is held: the selection is extended.
102 shift: bool,
103 /// Whether Ctrl is held: the caret jumps a word.
104 word: bool,
105 },
106 /// Right arrow (`shift`: extends it; `word`: jumps a word, Ctrl).
107 Right {
108 /// Whether Shift is held: the selection is extended.
109 shift: bool,
110 /// Whether Ctrl is held: the caret jumps a word.
111 word: bool,
112 },
113 /// Escape (close or cancel) — routed leaf→root, never to editing.
114 Escape,
115 /// Start of line (`doc`: the start of the whole **field**, Ctrl).
116 Home {
117 /// Whether Shift is held: the selection is extended.
118 shift: bool,
119 /// Whether Ctrl is held: the caret goes to the start of the whole field.
120 doc: bool,
121 },
122 /// End of line (`doc`: the end of the whole **field**, Ctrl).
123 End {
124 /// Whether Shift is held: the selection is extended.
125 shift: bool,
126 /// Whether Ctrl is held: the caret goes to the end of the whole field.
127 doc: bool,
128 },
129}
130
131/// A widget's visual pointer interaction state.
132#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
133pub enum Interaction {
134 /// Neither hovered nor pressed.
135 #[default]
136 None,
137 /// The pointer is over it.
138 Hovered,
139 /// The pointer is pressed on this widget.
140 ///
141 /// This is the **flag**: true from the first frame the finger is down. What a paint
142 /// wants is almost always [`Status::press_progress`], the same thing as a fade.
143 Pressed,
144}
145
146/// A widget's complete status for one frame: pointer interaction, focus,
147/// caret and selection (for fields), animation progress and opacity.
148#[derive(Copy, Clone, PartialEq, Debug)]
149pub struct Status {
150 /// Whether the pointer is over the widget or pressing it.
151 pub interaction: Interaction,
152 /// Whether the widget holds the keyboard focus.
153 pub focused: bool,
154 /// The caret's (character) index, if this widget is a focused field.
155 pub cursor: Option<usize>,
156 /// The selected `(start, end)` range, in character indices.
157 pub selection: Option<(usize, usize)>,
158 /// The `(start, end)` range **being composed** by the IME (provisional,
159 /// underlined text); `None` outside composition. In character indices.
160 pub composing: Option<(usize, usize)>,
161 /// Does the selection carry **handles**? A selection made with a finger does — a long
162 /// press on a word — and one made with a mouse or the keyboard does not. Set by the
163 /// shell, and only for the focused field.
164 pub handles: bool,
165 /// Is the caret in the hidden half of its blink? Set by the shell, and only for the
166 /// focused field; a field paints no caret while it is.
167 pub caret_hidden: bool,
168 /// Is a drag currently **over this widget**, and would it be accepted? Only
169 /// ever true for a [`crate::DragTarget`]; it is what lets one paint the "drop it
170 /// here" state itself rather than have the shell paint it from outside.
171 pub drag_over: bool,
172 /// The hover transition's progress (`0.0..=1.0`).
173 pub hover_progress: f32,
174 /// The focus transition's progress (`0.0..=1.0`).
175 pub focus_progress: f32,
176 /// The **press** transition's progress (`0.0..=1.0`): the animated form of
177 /// [`Interaction::Pressed`], as `hover_progress` is the animated form of
178 /// [`Interaction::Hovered`].
179 ///
180 /// A press used to be a flag and nothing else, so every state layer and every held
181 /// radius in the crate reached full the instant a finger landed and vanished the
182 /// instant it left. The reference fades its press highlight over 200 ms against the
183 /// 50 ms it gives hover and focus (`ink_well.dart:995`) — the press is the *slower*
184 /// of the two, which is the opposite of what a flag does.
185 ///
186 /// `interaction` stays for the decisions that really are discrete — is this the
187 /// widget being held? — and this is what the **paint** reads.
188 pub press_progress: f32,
189 /// The opacity to apply (a fade-in); `1.0` = opaque.
190 pub opacity: f32,
191 /// The widget's own animated value (a switch's `0 → 1`, for instance), driven
192 /// by `Widget::anim_target`.
193 pub value: f32,
194 /// The time elapsed (in seconds) since start-up — for continuous,
195 /// time-driven animations (a `CircularProgressIndicator`, for instance).
196 pub time: f32,
197 /// The **interpolated** color of an animated background
198 /// (`Container::animated_color`) while in transition; `None` = no animated
199 /// color, and the widget uses its own.
200 pub anim_color: Option<frus_core::Color>,
201 /// The **interpolated** corner radius (`Container::animated_radius`) while in
202 /// transition; `None` = a fixed radius, and the widget uses its own.
203 pub anim_radius: Option<frus_core::BorderRadius>,
204 /// This widget's **retained** vertical scroll (in px), for widgets that scroll
205 /// their own content (a multi-line field); `0` otherwise.
206 pub scroll_y: f32,
207 /// The pointer's **absolute** position when it hovers an **interactive sub-region** of
208 /// this widget (milestone 208); `None` otherwise. The widget brings it back to local
209 /// coordinates through its `bounds` to highlight the targeted zone (a suffix icon…).
210 /// Set by the shell from `cursor_icon`.
211 pub hover_cursor: Option<frus_core::Point>,
212}
213
214impl Status {
215 /// **The states this widget is in**, as a [`WidgetStateProperty`] resolves against
216 /// (`widget_state.dart:168`).
217 ///
218 /// Three of the eight, because three are all a status honestly knows: hovered,
219 /// focused and pressed. `Selected`, `Disabled`, `Error`, `Dragged` and
220 /// `ScrolledUnder` are the **widget's own** to add with
221 /// [`WidgetStates::set`] — nothing outside a checkbox knows whether it is ticked, and
222 /// a status that guessed would be wrong for every widget that never selects anything.
223 ///
224 /// It reads the **flags**, not the fades: a property is a step between values, and
225 /// where the change should be gradual the widget animates between two resolved values
226 /// rather than resolving a fraction. See the module note.
227 ///
228 /// [`WidgetStateProperty`]: crate::WidgetStateProperty
229 /// [`WidgetStates::set`]: crate::WidgetStates::set
230 pub fn states(&self) -> crate::widgetstate::WidgetStates {
231 use crate::widgetstate::{WidgetState, WidgetStates};
232 WidgetStates::EMPTY
233 .set(WidgetState::Hovered, self.interaction != Interaction::None)
234 .set(
235 WidgetState::Pressed,
236 self.interaction == Interaction::Pressed,
237 )
238 .set(WidgetState::Focused, self.focused)
239 }
240}
241
242impl Default for Status {
243 fn default() -> Self {
244 Self {
245 interaction: Interaction::None,
246 focused: false,
247 cursor: None,
248 selection: None,
249 composing: None,
250 handles: false,
251 caret_hidden: false,
252 drag_over: false,
253 hover_progress: 0.0,
254 focus_progress: 0.0,
255 press_progress: 0.0,
256 opacity: 1.0,
257 value: 0.0,
258 time: 0.0,
259 anim_color: None,
260 anim_radius: None,
261 scroll_y: 0.0,
262 hover_cursor: None,
263 }
264 }
265}
266
267/// The input state retained by the runtime, passed to the interface build.
268#[derive(Copy, Clone, Debug, Default)]
269pub struct InputState {
270 /// The widget currently hovered.
271 pub hovered: Option<WidgetId>,
272 /// The widget the pointer is pressed on.
273 pub pressed: Option<WidgetId>,
274 /// The widget that has keyboard focus.
275 pub focused: Option<WidgetId>,
276 /// The pointer's absolute position when it hovers an interactive sub-region of the
277 /// hovered widget (milestone 208); `None` otherwise. Set by the shell (see
278 /// `Status::hover_cursor`).
279 pub hover_cursor: Option<frus_core::Point>,
280}
281
282impl InputState {
283 /// A given widget's status.
284 pub(crate) fn status_for(&self, id: WidgetId) -> Status {
285 let interaction = if self.pressed == Some(id) && self.hovered == Some(id) {
286 Interaction::Pressed
287 } else if self.hovered == Some(id) {
288 Interaction::Hovered
289 } else {
290 Interaction::None
291 };
292 Status {
293 interaction,
294 focused: self.focused == Some(id),
295 cursor: None,
296 selection: None,
297 composing: None,
298 handles: false,
299 caret_hidden: false,
300 drag_over: false,
301 hover_progress: 0.0,
302 focus_progress: 0.0,
303 press_progress: 0.0,
304 opacity: 1.0,
305 value: 0.0,
306 time: 0.0,
307 anim_color: None,
308 anim_radius: None,
309 scroll_y: 0.0,
310 // The pointer's position only for the hovered widget (otherwise `None`).
311 hover_cursor: if self.hovered == Some(id) {
312 self.hover_cursor
313 } else {
314 None
315 },
316 }
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[test]
325 fn same_path_yields_same_id() {
326 assert_eq!(
327 WidgetId::ROOT.child(0).child(2),
328 WidgetId::ROOT.child(0).child(2)
329 );
330 }
331
332 #[test]
333 fn different_paths_differ() {
334 assert_ne!(WidgetId::ROOT.child(0), WidgetId::ROOT.child(1));
335 assert_ne!(
336 WidgetId::ROOT.child(0).child(1),
337 WidgetId::ROOT.child(1).child(0)
338 );
339 assert_ne!(WidgetId::ROOT, WidgetId::ROOT.child(0));
340 }
341
342 #[test]
343 fn keyed_is_stable_and_distinct() {
344 // The same key under the same parent → the same identity (position-independent).
345 assert_eq!(WidgetId::ROOT.keyed(7), WidgetId::ROOT.keyed(7));
346 // Different keys → different identities.
347 assert_ne!(WidgetId::ROOT.keyed(7), WidgetId::ROOT.keyed(8));
348 // A key does not collide with a positional index of the same value.
349 assert_ne!(WidgetId::ROOT.keyed(0), WidgetId::ROOT.child(0));
350 // Different parents → different identities for the same key.
351 assert_ne!(
352 WidgetId::ROOT.child(0).keyed(7),
353 WidgetId::ROOT.child(1).keyed(7)
354 );
355 }
356
357 #[test]
358 fn status_precedence_and_focus() {
359 let id = WidgetId::ROOT.child(0);
360 let other = WidgetId::ROOT.child(1);
361
362 let hovered = InputState {
363 hovered: Some(id),
364 ..Default::default()
365 };
366 assert_eq!(hovered.status_for(id).interaction, Interaction::Hovered);
367
368 let pressed = InputState {
369 hovered: Some(id),
370 pressed: Some(id),
371 ..Default::default()
372 };
373 assert_eq!(pressed.status_for(id).interaction, Interaction::Pressed);
374
375 // Pressed, but the pointer is elsewhere → not "Pressed".
376 let moved_away = InputState {
377 hovered: Some(other),
378 pressed: Some(id),
379 ..Default::default()
380 };
381 assert_eq!(moved_away.status_for(id).interaction, Interaction::None);
382
383 // Focus is independent of pointer interaction.
384 let focused = InputState {
385 focused: Some(id),
386 ..Default::default()
387 };
388 assert!(focused.status_for(id).focused);
389 assert!(!focused.status_for(other).focused);
390 }
391}