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}
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}