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}