Skip to main content

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}