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 /// No cursor at all: it is hidden while over the region (milestone 583).
21 None,
22 /// Something that cannot be done here — a disabled control, a drop that is refused.
23 Forbidden,
24 /// The application is busy and takes no input.
25 Wait,
26 /// The application is busy, and still takes input.
27 Progress,
28 /// Help is available.
29 Help,
30 /// A precise pick: a crosshair.
31 Precise,
32 /// Something that can be moved.
33 Move,
34 /// Something that can be grabbed and dragged.
35 Grab,
36 /// Something being dragged.
37 Grabbing,
38 /// A boundary that moves left and right: a column's edge.
39 ResizeColumn,
40 /// A boundary that moves up and down: a row's edge.
41 ResizeRow,
42 /// Zoom in.
43 ZoomIn,
44 /// Zoom out.
45 ZoomOut,
46 /// A context menu is available.
47 ContextMenu,
48}
49
50/// A widget's positional identity: a hash of its path in the tree.
51#[derive(Copy, Clone, PartialEq, Eq, Hash, Debug)]
52pub struct WidgetId(u64);
53
54impl WidgetId {
55 /// The root's identity.
56 pub(crate) const ROOT: WidgetId = WidgetId(0xcbf29ce484222325);
57
58 /// The identity's raw value, used to tag primitives.
59 pub fn as_u64(self) -> u64 {
60 self.0
61 }
62
63 /// Rebuilds an identity from its raw value (the inverse of [`as_u64`]) — to
64 /// route an action coming from an external layer (accessibility).
65 ///
66 /// [`as_u64`]: WidgetId::as_u64
67 pub fn from_u64(raw: u64) -> WidgetId {
68 WidgetId(raw)
69 }
70
71 /// Derives the identity of this widget's `index`-th child (positional).
72 pub(crate) fn child(self, index: usize) -> WidgetId {
73 let mut h = self.0 ^ (index as u64).wrapping_add(0x9e37_79b9_7f4a_7c15);
74 h = h.wrapping_mul(0x0000_0100_0000_01b3);
75 h ^= h >> 29;
76 WidgetId(h)
77 }
78
79 /// Derives the identity of the **dismissal barrier** under an overlay: the
80 /// window-wide region whose press closes it.
81 ///
82 /// It needs one of its own because it is not a widget. It used to borrow the
83 /// overlay's root identity, and a root that registered a target of its own then had
84 /// two different regions under one name — which [`crate::Ui::msg_for`] resolves by
85 /// identity alone, so a press on a menu's own surface read back as the press that
86 /// closes it.
87 pub(crate) fn barrier(self) -> WidgetId {
88 let mut h = self.0 ^ 0x243f_6a88_85a3_08d3;
89 h = h.wrapping_mul(0x0000_0100_0000_01b3);
90 h ^= h >> 27;
91 WidgetId(h)
92 }
93
94 /// Derives the identity of the **bar over a text selection** for one of its
95 /// [`ToolbarContext`](crate::ToolbarContext) variants. Each variant is a different
96 /// widget with a different list of buttons, so it needs an identity of its own: the
97 /// layout caches are kept by identity, and two lists sharing one would be measured as
98 /// whichever came first.
99 pub(crate) fn toolbar(self, variant: u8) -> WidgetId {
100 let mut h = self.0 ^ 0x13198a2e_03707344u64.wrapping_add(u64::from(variant));
101 h = h.wrapping_mul(0x0000_0100_0000_01b3);
102 h ^= h >> 31;
103 WidgetId(h)
104 }
105
106 /// Derives a child's identity **by key** (stable whatever its position).
107 /// Distinct from [`WidgetId::child`] (a different constant and shift).
108 pub(crate) fn keyed(self, key: u64) -> WidgetId {
109 let mut h = self.0 ^ key.wrapping_add(0x517c_c1b7_2722_0a95);
110 h = h.wrapping_mul(0x0000_0100_0000_01b3);
111 h ^= h >> 31;
112 WidgetId(h)
113 }
114}
115
116/// A widget's response to a key received during the **leaf→root bubble** (the
117/// focused one first, then its ancestors for as long as the result is `Ignored`).
118#[derive(Clone, Debug, PartialEq)]
119pub enum KeyResponse<Msg = crate::callback::Callback> {
120 /// Not concerned: the key keeps bubbling up.
121 Ignored,
122 /// Consumed (with an optional message to emit): the bubbling stops.
123 Handled(Option<Msg>),
124 /// Not consumed, but the bubbling stops, and no fallback is attempted.
125 Skip,
126}
127
128/// A key passed to the focused widget.
129#[derive(Clone, Debug, PartialEq)]
130pub enum Key {
131 /// Typed text (one or more characters) — also used for pasting.
132 Text(String),
133 /// Backspace (deletes the selection, otherwise the character to the left).
134 Backspace,
135 /// Forward delete (the selection, otherwise the character to the right).
136 Delete,
137 /// Enter.
138 Enter,
139 /// Left arrow (`shift`: extends the selection; `word`: jumps a word, Ctrl).
140 Left {
141 /// Whether Shift is held: the selection is extended.
142 shift: bool,
143 /// Whether Ctrl is held: the caret jumps a word.
144 word: bool,
145 },
146 /// Right arrow (`shift`: extends it; `word`: jumps a word, Ctrl).
147 Right {
148 /// Whether Shift is held: the selection is extended.
149 shift: bool,
150 /// Whether Ctrl is held: the caret jumps a word.
151 word: bool,
152 },
153 /// Escape (close or cancel) — routed leaf→root, never to editing.
154 Escape,
155 /// Start of line (`doc`: the start of the whole **field**, Ctrl).
156 Home {
157 /// Whether Shift is held: the selection is extended.
158 shift: bool,
159 /// Whether Ctrl is held: the caret goes to the start of the whole field.
160 doc: bool,
161 },
162 /// End of line (`doc`: the end of the whole **field**, Ctrl).
163 End {
164 /// Whether Shift is held: the selection is extended.
165 shift: bool,
166 /// Whether Ctrl is held: the caret goes to the end of the whole field.
167 doc: bool,
168 },
169}
170
171/// A widget's visual pointer interaction state.
172#[derive(Copy, Clone, PartialEq, Eq, Debug, Default)]
173pub enum Interaction {
174 /// Neither hovered nor pressed.
175 #[default]
176 None,
177 /// The pointer is over it.
178 Hovered,
179 /// The pointer is pressed on this widget.
180 ///
181 /// This is the **flag**: true from the first frame the finger is down. What a paint
182 /// wants is almost always [`Status::press_progress`], the same thing as a fade.
183 Pressed,
184}
185
186/// A widget's complete status for one frame: pointer interaction, focus,
187/// caret and selection (for fields), animation progress and opacity.
188#[derive(Copy, Clone, PartialEq, Debug)]
189pub struct Status {
190 /// Whether the pointer is over the widget or pressing it.
191 pub interaction: Interaction,
192 /// Whether the widget holds the keyboard focus.
193 pub focused: bool,
194 /// The caret's (character) index, if this widget is a focused field.
195 pub cursor: Option<usize>,
196 /// The selected `(start, end)` range, in character indices.
197 pub selection: Option<(usize, usize)>,
198 /// The `(start, end)` range **being composed** by the IME (provisional,
199 /// underlined text); `None` outside composition. In character indices.
200 pub composing: Option<(usize, usize)>,
201 /// Is the field's **bar** (Cut, Copy, Paste, Select all) showing over its selection?
202 /// `Some(can_paste)` when it is — with whether the clipboard held text when it opened —
203 /// and `None` otherwise. Set by the shell, and only for the focused field.
204 pub toolbar: Option<bool>,
205 /// Does the selection carry **handles**? A selection made with a finger does — a long
206 /// press on a word — and one made with a mouse or the keyboard does not. Set by the
207 /// shell, and only for the focused field.
208 pub handles: bool,
209 /// The `(start, end)` range of this text that a [`SelectionArea`](crate::SelectionArea)'s
210 /// selection covers, in characters. Kept by the shell, for any text in the area whether or
211 /// not it has the focus — which is the difference from `selection`, a field's own.
212 pub region: Option<(usize, usize)>,
213 /// Does the selection a selection area holds show its **bar**? Set for every text the
214 /// selection covers; the first of them in reading order carries the bar.
215 pub region_bar: bool,
216 /// The **handles** of a selection area's touch selection that hang from this text: the
217 /// character where the selection starts, if it starts here, and the boundary where it
218 /// ends, if it ends here.
219 pub region_grips: (Option<usize>, Option<usize>),
220 /// Is the caret in the hidden half of its blink? Set by the shell, and only for the
221 /// focused field; a field paints no caret while it is.
222 pub caret_hidden: bool,
223 /// Is a drag currently **over this widget**, and would it be accepted? Only
224 /// ever true for a [`crate::DragTarget`]; it is what lets one paint the "drop it
225 /// here" state itself rather than have the shell paint it from outside.
226 pub drag_over: bool,
227 /// The hover transition's progress (`0.0..=1.0`).
228 pub hover_progress: f32,
229 /// The focus transition's progress (`0.0..=1.0`).
230 pub focus_progress: f32,
231 /// The **press** transition's progress (`0.0..=1.0`): the animated form of
232 /// [`Interaction::Pressed`], as `hover_progress` is the animated form of
233 /// [`Interaction::Hovered`].
234 ///
235 /// A press used to be a flag and nothing else, so every state layer and every held
236 /// radius in the crate reached full the instant a finger landed and vanished the
237 /// instant it left. The reference fades its press highlight over 200 ms against the
238 /// 50 ms it gives hover and focus (`ink_well.dart:995`) — the press is the *slower*
239 /// of the two, which is the opposite of what a flag does.
240 ///
241 /// `interaction` stays for the decisions that really are discrete — is this the
242 /// widget being held? — and this is what the **paint** reads.
243 pub press_progress: f32,
244 /// The opacity to apply (a fade-in); `1.0` = opaque.
245 pub opacity: f32,
246 /// The widget's own animated value (a switch's `0 → 1`, for instance), driven
247 /// by `Widget::anim_target`.
248 pub value: f32,
249 /// The time elapsed (in seconds) since start-up — for continuous,
250 /// time-driven animations (a `CircularProgressIndicator`, for instance).
251 pub time: f32,
252 /// The **interpolated** color of an animated background
253 /// (`Container::animated_color`) while in transition; `None` = no animated
254 /// color, and the widget uses its own.
255 pub anim_color: Option<frus_core::Color>,
256 /// The **interpolated** corner radius (`Container::animated_radius`) while in
257 /// transition; `None` = a fixed radius, and the widget uses its own.
258 pub anim_radius: Option<frus_core::BorderRadius>,
259 /// This widget's **retained** vertical scroll (in px), for widgets that scroll
260 /// their own content (a multi-line field); `0` otherwise.
261 pub scroll_y: f32,
262 /// The pointer's **absolute** position when it hovers an **interactive sub-region** of
263 /// this widget (milestone 208); `None` otherwise. The widget brings it back to local
264 /// coordinates through its `bounds` to highlight the targeted zone (a suffix icon…).
265 /// Set by the shell from `cursor_icon`.
266 pub hover_cursor: Option<frus_core::Point>,
267 /// Was `value` **placed by a finger** — held by a drag, or settling from where one let
268 /// go (milestone 638)? A widget that curves its value paints it straight while it is:
269 /// the thumb under a finger is where the finger is.
270 pub value_placed: bool,
271 /// Which of this widget's thumbs the **last drag on it took** (milestone 651), on a
272 /// track with two: `0` the start, `1` the end. It stays after the drag ends, as the
273 /// reference's last selection does, so that the thumb last moved stays on top and its
274 /// press fades out on it rather than on its neighbour. Set by the shell.
275 pub thumb: Option<usize>,
276}
277
278impl Status {
279 /// **The states this widget is in**, as a [`WidgetStateProperty`] resolves against
280 /// (`widget_state.dart:168`).
281 ///
282 /// Three of the eight, because three are all a status honestly knows: hovered,
283 /// focused and pressed. `Selected`, `Disabled`, `Error`, `Dragged` and
284 /// `ScrolledUnder` are the **widget's own** to add with
285 /// [`WidgetStates::set`] — nothing outside a checkbox knows whether it is ticked, and
286 /// a status that guessed would be wrong for every widget that never selects anything.
287 ///
288 /// It reads the **flags**, not the fades: a property is a step between values, and
289 /// where the change should be gradual the widget animates between two resolved values
290 /// rather than resolving a fraction. See the module note.
291 ///
292 /// [`WidgetStateProperty`]: crate::WidgetStateProperty
293 /// [`WidgetStates::set`]: crate::WidgetStates::set
294 pub fn states(&self) -> crate::widgetstate::WidgetStates {
295 use crate::widgetstate::{WidgetState, WidgetStates};
296 WidgetStates::EMPTY
297 .set(WidgetState::Hovered, self.interaction != Interaction::None)
298 .set(
299 WidgetState::Pressed,
300 self.interaction == Interaction::Pressed,
301 )
302 .set(WidgetState::Focused, self.focused)
303 }
304}
305
306impl Default for Status {
307 fn default() -> Self {
308 Self {
309 interaction: Interaction::None,
310 focused: false,
311 cursor: None,
312 selection: None,
313 composing: None,
314 toolbar: None,
315 handles: false,
316 region: None,
317 region_bar: false,
318 region_grips: (None, None),
319 caret_hidden: false,
320 drag_over: false,
321 hover_progress: 0.0,
322 focus_progress: 0.0,
323 press_progress: 0.0,
324 opacity: 1.0,
325 value: 0.0,
326 time: 0.0,
327 anim_color: None,
328 anim_radius: None,
329 scroll_y: 0.0,
330 hover_cursor: None,
331 value_placed: false,
332 thumb: None,
333 }
334 }
335}
336
337/// The input state retained by the runtime, passed to the interface build.
338#[derive(Copy, Clone, Debug, Default)]
339pub struct InputState {
340 /// The widget currently hovered.
341 pub hovered: Option<WidgetId>,
342 /// The widget the pointer is pressed on.
343 pub pressed: Option<WidgetId>,
344 /// The widget that has keyboard focus.
345 pub focused: Option<WidgetId>,
346 /// The pointer's absolute position when it hovers an interactive sub-region of the
347 /// hovered widget (milestone 208); `None` otherwise. Set by the shell (see
348 /// `Status::hover_cursor`).
349 pub hover_cursor: Option<frus_core::Point>,
350 /// The widget a drag last took a thumb of, and which thumb (see `Status::thumb`). Set
351 /// by the shell.
352 pub thumb: Option<(WidgetId, usize)>,
353}
354
355impl InputState {
356 /// A given widget's status.
357 pub(crate) fn status_for(&self, id: WidgetId) -> Status {
358 let interaction = if self.pressed == Some(id) && self.hovered == Some(id) {
359 Interaction::Pressed
360 } else if self.hovered == Some(id) {
361 Interaction::Hovered
362 } else {
363 Interaction::None
364 };
365 Status {
366 interaction,
367 focused: self.focused == Some(id),
368 cursor: None,
369 selection: None,
370 composing: None,
371 toolbar: None,
372 handles: false,
373 region: None,
374 region_bar: false,
375 region_grips: (None, None),
376 caret_hidden: false,
377 drag_over: false,
378 hover_progress: 0.0,
379 focus_progress: 0.0,
380 press_progress: 0.0,
381 opacity: 1.0,
382 value: 0.0,
383 time: 0.0,
384 anim_color: None,
385 anim_radius: None,
386 scroll_y: 0.0,
387 // The pointer's position only for the hovered widget (otherwise `None`).
388 hover_cursor: if self.hovered == Some(id) {
389 self.hover_cursor
390 } else {
391 None
392 },
393 value_placed: false,
394 thumb: self
395 .thumb
396 .and_then(|(widget, thumb)| (widget == id).then_some(thumb)),
397 }
398 }
399}
400
401#[cfg(test)]
402mod tests {
403 use super::*;
404
405 /// **The thumb a drag took is that widget's alone** (milestone 651): another widget,
406 /// a range slider beside it, paints its own last selection, not this one's.
407 #[test]
408 fn the_thumb_taken_reaches_only_its_widget() {
409 let taken = WidgetId::ROOT.child(0);
410 let input = InputState {
411 thumb: Some((taken, 0)),
412 ..Default::default()
413 };
414 assert_eq!(input.status_for(taken).thumb, Some(0));
415 assert_eq!(input.status_for(WidgetId::ROOT.child(1)).thumb, None);
416 }
417
418 #[test]
419 fn same_path_yields_same_id() {
420 assert_eq!(
421 WidgetId::ROOT.child(0).child(2),
422 WidgetId::ROOT.child(0).child(2)
423 );
424 }
425
426 #[test]
427 fn different_paths_differ() {
428 assert_ne!(WidgetId::ROOT.child(0), WidgetId::ROOT.child(1));
429 assert_ne!(
430 WidgetId::ROOT.child(0).child(1),
431 WidgetId::ROOT.child(1).child(0)
432 );
433 assert_ne!(WidgetId::ROOT, WidgetId::ROOT.child(0));
434 }
435
436 #[test]
437 fn keyed_is_stable_and_distinct() {
438 // The same key under the same parent → the same identity (position-independent).
439 assert_eq!(WidgetId::ROOT.keyed(7), WidgetId::ROOT.keyed(7));
440 // Different keys → different identities.
441 assert_ne!(WidgetId::ROOT.keyed(7), WidgetId::ROOT.keyed(8));
442 // A key does not collide with a positional index of the same value.
443 assert_ne!(WidgetId::ROOT.keyed(0), WidgetId::ROOT.child(0));
444 // Different parents → different identities for the same key.
445 assert_ne!(
446 WidgetId::ROOT.child(0).keyed(7),
447 WidgetId::ROOT.child(1).keyed(7)
448 );
449 }
450
451 #[test]
452 fn status_precedence_and_focus() {
453 let id = WidgetId::ROOT.child(0);
454 let other = WidgetId::ROOT.child(1);
455
456 let hovered = InputState {
457 hovered: Some(id),
458 ..Default::default()
459 };
460 assert_eq!(hovered.status_for(id).interaction, Interaction::Hovered);
461
462 let pressed = InputState {
463 hovered: Some(id),
464 pressed: Some(id),
465 ..Default::default()
466 };
467 assert_eq!(pressed.status_for(id).interaction, Interaction::Pressed);
468
469 // Pressed, but the pointer is elsewhere → not "Pressed".
470 let moved_away = InputState {
471 hovered: Some(other),
472 pressed: Some(id),
473 ..Default::default()
474 };
475 assert_eq!(moved_away.status_for(id).interaction, Interaction::None);
476
477 // Focus is independent of pointer interaction.
478 let focused = InputState {
479 focused: Some(id),
480 ..Default::default()
481 };
482 assert!(focused.status_for(id).focused);
483 assert!(!focused.status_for(other).focused);
484 }
485}