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