Skip to main content

frus_widgets/
widgetstate.rs

1//! **A value that depends on what a widget is doing**: [`WidgetState`], [`WidgetStates`],
2//! [`StateFilter`] and [`WidgetStateProperty`] (`widget_state.dart`).
3//!
4//! The reference threads one idea through every Material component: a property is not a
5//! value but a *function of the states the widget is in*. A button's foreground is one
6//! colour at rest, another under a pointer, another while held, another when it cannot be
7//! pressed at all — and the API takes all four in one argument rather than four.
8//!
9//! This framework had no way to say that. Its state rule is [`Theme::state_layer`], one
10//! lerp from a ground towards an ink at three fixed opacities, which is the *right answer
11//! for a state layer* and no answer at all for a caller who wants a particular colour in
12//! a particular state. Since milestone 322 the notes have recorded wanting this.
13//!
14//! # A step, not a fade
15//!
16//! A property resolves from a **set of flags**, so it steps: the value for `Hovered` is
17//! the value the moment the pointer arrives. That is what the reference does too, and
18//! where it wants the change to be gradual it animates *between* two resolved values
19//! rather than resolving a fraction (its scrollbar does exactly this,
20//! `scrollbar.dart:262`). So a property gives the endpoints, and the widget decides
21//! whether to step between them or fade. [`Theme::state_layer`] remains the answer where
22//! the whole point is the fade.
23//!
24//! [`Theme::state_layer`]: crate::Theme::state_layer
25
26/// **One thing a widget can be doing** (`widget_state.dart:168`).
27///
28/// The reference's eight, unchanged. Not all of them are answerable from outside a
29/// widget — see [`Status::states`](crate::Status::states).
30#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
31pub enum WidgetState {
32    /// A pointer is over it.
33    Hovered,
34    /// The keyboard has reached it.
35    Focused,
36    /// It is being held down.
37    Pressed,
38    /// **It** is being dragged from one place to another — not that something is being
39    /// dragged over it, which is the drop target's business.
40    Dragged,
41    /// It is one of a set and it is the chosen one: a tab, a radio, a checked box.
42    Selected,
43    /// It overlaps the content of a scrollable passing beneath it — what an app bar goes
44    /// by to decide it is no longer flush with the page.
45    ScrolledUnder,
46    /// It cannot be interacted with, and so is in none of the four states above.
47    Disabled,
48    /// It holds something invalid.
49    Error,
50}
51
52impl WidgetState {
53    /// This state's bit in a [`WidgetStates`].
54    const fn bit(self) -> u8 {
55        1 << (self as u8)
56    }
57}
58
59/// **The set of states a widget is in**, as a byte rather than a hash set.
60///
61/// The reference passes a `Set<WidgetState>`. Eight states fit in eight bits, and a
62/// `Copy` set with no allocation is what a paint walk that runs every frame wants — it
63/// also means the set can take part in the rebuild hash, which a `HashSet` could not do
64/// cheaply.
65#[derive(Copy, Clone, Debug, Default, PartialEq, Eq, Hash)]
66pub struct WidgetStates(u8);
67
68impl WidgetStates {
69    /// In no state at all: at rest, reachable, valid, unselected.
70    pub const EMPTY: Self = Self(0);
71
72    /// The set holding `state` alone.
73    pub const fn of(state: WidgetState) -> Self {
74        Self(state.bit())
75    }
76
77    /// This set, plus `state`.
78    #[must_use]
79    pub const fn with(self, state: WidgetState) -> Self {
80        Self(self.0 | state.bit())
81    }
82
83    /// This set, less `state`.
84    #[must_use]
85    pub const fn without(self, state: WidgetState) -> Self {
86        Self(self.0 & !state.bit())
87    }
88
89    /// This set, with `state` present or not according to `on` — the shape a widget
90    /// answering for its own selection wants.
91    #[must_use]
92    pub const fn set(self, state: WidgetState, on: bool) -> Self {
93        if on {
94            self.with(state)
95        } else {
96            self.without(state)
97        }
98    }
99
100    /// Is the widget in this state?
101    pub const fn contains(self, state: WidgetState) -> bool {
102        self.0 & state.bit() != 0
103    }
104
105    /// Is it in none at all?
106    pub const fn is_empty(self) -> bool {
107        self.0 == 0
108    }
109}
110
111impl From<WidgetState> for WidgetStates {
112    fn from(state: WidgetState) -> Self {
113        Self::of(state)
114    }
115}
116
117impl std::ops::BitOr for WidgetState {
118    type Output = WidgetStates;
119
120    fn bitor(self, other: WidgetState) -> WidgetStates {
121        WidgetStates::of(self).with(other)
122    }
123}
124
125impl std::ops::BitOr<WidgetState> for WidgetStates {
126    type Output = WidgetStates;
127
128    fn bitor(self, state: WidgetState) -> WidgetStates {
129        self.with(state)
130    }
131}
132
133/// **What a set of states has to look like** for one entry of a
134/// [`WidgetStateProperty`] to answer (`widget_state.dart:27`).
135///
136/// A bare state is satisfied when the set contains it; the three operators build the rest.
137/// The reference spells them `&`, `|` and `~`, and so does this:
138///
139/// ```
140/// use frus_widgets::{StateFilter, WidgetState, WidgetStates};
141///
142/// let held_but_not_broken =
143///     StateFilter::from(WidgetState::Pressed) & !StateFilter::from(WidgetState::Error);
144/// assert!(held_but_not_broken.matches(WidgetStates::of(WidgetState::Pressed)));
145/// assert!(!held_but_not_broken.matches(
146///     WidgetState::Pressed | WidgetState::Error
147/// ));
148/// ```
149#[derive(Clone, Debug, PartialEq, Eq)]
150pub enum StateFilter {
151    /// Satisfied by anything at all, including nothing — the reference's
152    /// `WidgetState.any` (`widget_state.dart:225`), and what a last entry uses to mean
153    /// *otherwise*.
154    Any,
155    /// Satisfied when the set contains this state.
156    Is(WidgetState),
157    /// Both.
158    And(Box<StateFilter>, Box<StateFilter>),
159    /// Either.
160    Or(Box<StateFilter>, Box<StateFilter>),
161    /// The opposite.
162    Not(Box<StateFilter>),
163}
164
165impl StateFilter {
166    /// Do these states satisfy this filter?
167    pub fn matches(&self, states: WidgetStates) -> bool {
168        match self {
169            StateFilter::Any => true,
170            StateFilter::Is(state) => states.contains(*state),
171            StateFilter::And(a, b) => a.matches(states) && b.matches(states),
172            StateFilter::Or(a, b) => a.matches(states) || b.matches(states),
173            StateFilter::Not(inner) => !inner.matches(states),
174        }
175    }
176}
177
178impl From<WidgetState> for StateFilter {
179    fn from(state: WidgetState) -> Self {
180        StateFilter::Is(state)
181    }
182}
183
184impl std::ops::BitAnd for StateFilter {
185    type Output = StateFilter;
186
187    fn bitand(self, other: StateFilter) -> StateFilter {
188        StateFilter::And(Box::new(self), Box::new(other))
189    }
190}
191
192impl std::ops::BitOr for StateFilter {
193    type Output = StateFilter;
194
195    fn bitor(self, other: StateFilter) -> StateFilter {
196        StateFilter::Or(Box::new(self), Box::new(other))
197    }
198}
199
200impl std::ops::Not for StateFilter {
201    type Output = StateFilter;
202
203    fn not(self) -> StateFilter {
204        StateFilter::Not(Box::new(self))
205    }
206}
207
208/// **A value chosen by the states a widget is in** (`widget_state.dart:821`).
209///
210/// Entries are tried in order and the **first match wins**, which is the reference's
211/// `WidgetStateMapper` (`widget_state.dart:1009`) and the reason order is worth
212/// thinking about: put the narrow cases first.
213///
214/// ```
215/// use frus_core::Color;
216/// use frus_widgets::{WidgetState, WidgetStateProperty, WidgetStates};
217///
218/// let ink = WidgetStateProperty::new()
219///     .when(WidgetState::Pressed, Color::rgb8(200, 0, 0))
220///     .when(WidgetState::Hovered, Color::rgb8(240, 120, 120))
221///     .otherwise(Color::TRANSPARENT);
222///
223/// // Held *and* hovered, as a pressed widget always is: the narrower entry answers.
224/// let held = WidgetState::Pressed | WidgetState::Hovered;
225/// assert_eq!(ink.resolve(held), Some(&Color::rgb8(200, 0, 0)));
226/// assert_eq!(ink.resolve(WidgetStates::EMPTY), Some(&Color::TRANSPARENT));
227/// ```
228///
229/// A property built from **closures** — the reference's `resolveWith` — is deliberately
230/// not offered. A map is `Clone`, `Debug` and comparable, which is what a widget tree
231/// that is rebuilt and diffed every frame needs; a boxed closure is none of the three,
232/// and the reference itself documents the map as the form to reach for.
233#[derive(Clone, Debug, PartialEq)]
234pub struct WidgetStateProperty<T> {
235    entries: Vec<(StateFilter, T)>,
236}
237
238impl<T> Default for WidgetStateProperty<T> {
239    fn default() -> Self {
240        Self::new()
241    }
242}
243
244impl<T> WidgetStateProperty<T> {
245    /// A property that answers nothing yet.
246    pub fn new() -> Self {
247        Self {
248            entries: Vec::new(),
249        }
250    }
251
252    /// **The same value whatever the widget is doing** — the reference's
253    /// `WidgetStatePropertyAll` (`widget_state.dart:1077`), and what a caller who has one
254    /// colour and an API that wants a property reaches for.
255    pub fn all(value: T) -> Self {
256        Self::new().otherwise(value)
257    }
258
259    /// The value to use when `filter` is satisfied, tried **after** everything already
260    /// added and **before** everything added later.
261    #[must_use]
262    pub fn when(mut self, filter: impl Into<StateFilter>, value: T) -> Self {
263        self.entries.push((filter.into(), value));
264        self
265    }
266
267    /// The value to use when nothing above matched. Anything added after it is
268    /// unreachable, `Any` being satisfied by every set.
269    #[must_use]
270    pub fn otherwise(self, value: T) -> Self {
271        self.when(StateFilter::Any, value)
272    }
273
274    /// The first value whose filter these states satisfy, or `None` when none does —
275    /// which means *say nothing*, and leaves the widget or the theme to answer, exactly
276    /// as the reference's nullable properties do.
277    pub fn resolve(&self, states: WidgetStates) -> Option<&T> {
278        self.entries
279            .iter()
280            .find(|(filter, _)| filter.matches(states))
281            .map(|(_, value)| value)
282    }
283
284    /// Has anything been said at all?
285    pub fn is_empty(&self) -> bool {
286        self.entries.is_empty()
287    }
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    /// The set is a byte, and it behaves like the reference's set.
295    #[test]
296    fn a_set_of_states_is_eight_bits() {
297        let states = WidgetState::Hovered | WidgetState::Focused;
298        assert!(states.contains(WidgetState::Hovered));
299        assert!(states.contains(WidgetState::Focused));
300        assert!(!states.contains(WidgetState::Pressed));
301        assert!(WidgetStates::EMPTY.is_empty());
302        assert!(!states.is_empty());
303        assert_eq!(
304            states.without(WidgetState::Focused),
305            WidgetState::Hovered.into()
306        );
307        assert_eq!(
308            WidgetStates::EMPTY.set(WidgetState::Selected, true),
309            WidgetStates::of(WidgetState::Selected)
310        );
311        assert_eq!(
312            states.set(WidgetState::Hovered, false),
313            WidgetStates::of(WidgetState::Focused)
314        );
315        assert_eq!(
316            std::mem::size_of::<WidgetStates>(),
317            1,
318            "a set that costs a byte can go in a paint walk and in a hash"
319        );
320    }
321
322    /// **The first match wins** (`widget_state.dart:1009`), so a narrow entry has to come
323    /// before a wide one — a pressed widget is nearly always hovered as well.
324    #[test]
325    fn the_first_entry_that_matches_answers() {
326        let property = WidgetStateProperty::new()
327            .when(WidgetState::Pressed, "held")
328            .when(WidgetState::Hovered, "under a pointer")
329            .otherwise("at rest");
330
331        let held = WidgetState::Pressed | WidgetState::Hovered;
332        assert_eq!(property.resolve(held), Some(&"held"));
333        assert_eq!(
334            property.resolve(WidgetStates::of(WidgetState::Hovered)),
335            Some(&"under a pointer")
336        );
337        assert_eq!(property.resolve(WidgetStates::EMPTY), Some(&"at rest"));
338    }
339
340    /// And a property with nothing to say says **nothing**, rather than a default that
341    /// would silently outrank the widget's own answer.
342    #[test]
343    fn a_property_may_answer_nothing() {
344        let only_held = WidgetStateProperty::new().when(WidgetState::Pressed, 1);
345        assert_eq!(only_held.resolve(WidgetStates::EMPTY), None);
346        assert_eq!(
347            only_held.resolve(WidgetStates::of(WidgetState::Pressed)),
348            Some(&1)
349        );
350        assert!(WidgetStateProperty::<u8>::new().is_empty());
351        assert_eq!(
352            WidgetStateProperty::all(7).resolve(WidgetStates::of(WidgetState::Error)),
353            Some(&7),
354            "and one that says the same thing always says it in every state"
355        );
356    }
357
358    /// The three operators, which are the reference's three (`widget_state.dart:49`).
359    #[test]
360    fn filters_combine_the_way_the_reference_spells_them() {
361        let f = |state| StateFilter::from(state);
362        let both = f(WidgetState::Hovered) & f(WidgetState::Focused);
363        assert!(both.matches(WidgetState::Hovered | WidgetState::Focused));
364        assert!(!both.matches(WidgetStates::of(WidgetState::Hovered)));
365
366        let either = f(WidgetState::Hovered) | f(WidgetState::Focused);
367        assert!(either.matches(WidgetStates::of(WidgetState::Focused)));
368        assert!(!either.matches(WidgetStates::of(WidgetState::Pressed)));
369
370        let reachable = !f(WidgetState::Disabled);
371        assert!(reachable.matches(WidgetStates::EMPTY));
372        assert!(!reachable.matches(WidgetStates::of(WidgetState::Disabled)));
373
374        assert!(StateFilter::Any.matches(WidgetStates::EMPTY));
375    }
376}