| 54 |
54 |
|
//! needed [`Fill::Sunken`] and [`Depth::Sunken`]: a surface set back by colour
|
| 55 |
55 |
|
//! with no edge, which is neither a well nor level-with.
|
| 56 |
56 |
|
//!
|
|
57 |
+ |
//! 0.7.0 adds [`State`], the interaction axis, closing the gap that adopting
|
|
58 |
+ |
//! against three apps rather than one made visible. The description named
|
|
59 |
+ |
//! rest and, through [`Depth::pressed`], pressed. It named neither focus nor
|
|
60 |
+ |
//! disabled, so `makeover-webview` emitted a hover rule and stopped, and each
|
|
61 |
+ |
//! consumer completed the primitive from outside by out-specifying a rule it
|
|
62 |
+ |
//! did not own: 19 such rules in goingson, 21 in the MNW server, a further set
|
|
63 |
+ |
//! in Balanced Breakfast, and three focus rings that do not match. The axis is
|
|
64 |
+ |
//! deliberately two members wide, because hover and pressed belong where they
|
|
65 |
+ |
//! already are. [`State`]'s own docs carry that argument.
|
|
66 |
+ |
//!
|
| 57 |
67 |
|
//! # Where the description stops
|
| 58 |
68 |
|
//!
|
| 59 |
69 |
|
//! The bespoke widgets, a day-plan timeline and a kanban board and a calendar,
|
| 295 |
305 |
|
}
|
| 296 |
306 |
|
}
|
| 297 |
307 |
|
|
|
308 |
+ |
/// An interaction state a region can be in, beside whatever [`Depth`] it is.
|
|
309 |
+ |
///
|
|
310 |
+ |
/// Orthogonal to depth on purpose. A disabled button is still [`Depth::Raised`]
|
|
311 |
+ |
/// and a disabled field is still a [`Depth::Well`], so folding either member
|
|
312 |
+ |
/// into `Depth` would make [`Depth::bevel`] and [`Depth::fill`] answer for
|
|
313 |
+ |
/// something that is not a depth, and would leave disabled-button and
|
|
314 |
+ |
/// disabled-field sharing one variant that cannot tell them apart.
|
|
315 |
+ |
///
|
|
316 |
+ |
/// # Why hover and pressed are not members
|
|
317 |
+ |
///
|
|
318 |
+ |
/// The line is whether every renderer has the state to express, not whether CSS
|
|
319 |
+ |
/// does. Hover is renderer policy and `makeover-webview` says so in its own
|
|
320 |
+ |
/// header: a terminal and an immediate-mode painter have no pointer hovering
|
|
321 |
+ |
/// over anything, and pressed already arrives through [`Bevel::pressed`] and
|
|
322 |
+ |
/// [`Depth::pressed`], where it belongs, because pressing is a depth inversion
|
|
323 |
+ |
/// rather than a separate condition.
|
|
324 |
+ |
///
|
|
325 |
+ |
/// Focus and disabled are different in kind. A TUI has a focused widget and a
|
|
326 |
+ |
/// greyed-out one; so does egui. Both were unsayable here, so all three webview
|
|
327 |
+ |
/// consumers supplied them from outside the primitive by out-specifying rules
|
|
328 |
+ |
/// they did not own: goingson alone carries 19 of them, and the MNW server
|
|
329 |
+ |
/// another 21. That is the divergence this crate exists to end, arriving one
|
|
330 |
+ |
/// layer down.
|
|
331 |
+ |
///
|
|
332 |
+ |
/// # The principle this encodes
|
|
333 |
+ |
///
|
|
334 |
+ |
/// A primitive owns every state it implies. A renderer that emits a hover rule
|
|
335 |
+ |
/// for a thing owes disabled, focus and the capability answer for that same
|
|
336 |
+ |
/// thing, because anything less exports the completion work to N consumers who
|
|
337 |
+ |
/// will each do it differently.
|
|
338 |
+ |
///
|
|
339 |
+ |
/// `#[non_exhaustive]` for the reason [`Fill`] and [`Depth`] carry it: growth
|
|
340 |
+ |
/// must not be a lockstep event across the three renderers.
|
|
341 |
+ |
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
|
342 |
+ |
#[non_exhaustive]
|
|
343 |
+ |
pub enum State {
|
|
344 |
+ |
/// Keyboard focus, as distinct from the pointer having landed on something.
|
|
345 |
+ |
///
|
|
346 |
+ |
/// One ring, not one per primitive. Where the ring sits is [`Depth`]'s
|
|
347 |
+ |
/// question and not a per-component choice: a well takes it inside its own
|
|
348 |
+ |
/// edge and a raised surface takes it outside. That is one decision with
|
|
349 |
+ |
/// two renderings rather than one decision per component, which is how the
|
|
350 |
+ |
/// three apps ended up with three rings.
|
|
351 |
+ |
Focus,
|
|
352 |
+ |
/// Present, visible, and not answering.
|
|
353 |
+ |
///
|
|
354 |
+ |
/// Not the same as absent, and deliberately not a [`Fill`]: a disabled
|
|
355 |
+ |
/// control keeps the surface it always had and stops responding, so what
|
|
356 |
+ |
/// changes is its content and its interactivity rather than what it is.
|
|
357 |
+ |
Disabled,
|
|
358 |
+ |
}
|
|
359 |
+ |
|
|
360 |
+ |
impl State {
|
|
361 |
+ |
/// Whether a region in this state stops answering the pointer.
|
|
362 |
+ |
///
|
|
363 |
+ |
/// Stated in the description rather than left to each renderer, on the same
|
|
364 |
+ |
/// reasoning as [`Bevel::pressed`]: a cascade carries it for free and an
|
|
365 |
+ |
/// immediate-mode renderer resolves it per call site, so leaving it unsaid
|
|
366 |
+ |
/// means resolving it once per consumer and disagreeing.
|
|
367 |
+ |
#[must_use]
|
|
368 |
+ |
pub const fn suppresses_interaction(self) -> bool {
|
|
369 |
+ |
match self {
|
|
370 |
+ |
Self::Disabled => true,
|
|
371 |
+ |
Self::Focus => false,
|
|
372 |
+ |
}
|
|
373 |
+ |
}
|
|
374 |
+ |
}
|
|
375 |
+ |
|
|
376 |
+ |
impl Intent for State {
|
|
377 |
+ |
fn token(self) -> &'static str {
|
|
378 |
+ |
match self {
|
|
379 |
+ |
// Already derived by `makeover` from `action.primary`, and unused
|
|
380 |
+ |
// until now for the same reason `hover-surface` was: nothing
|
|
381 |
+ |
// emitted the rule that would consume it.
|
|
382 |
+ |
Self::Focus => "focus-ring",
|
|
383 |
+ |
// Reusing the muted content intent rather than minting a
|
|
384 |
+ |
// `disabled` colour. Disabled is a reduction and not a status, and
|
|
385 |
+ |
// `makeover-webview`'s progress rules already record the reading
|
|
386 |
+ |
// that `content-muted` is what disabled looks like.
|
|
387 |
+ |
Self::Disabled => "content-muted",
|
|
388 |
+ |
}
|
|
389 |
+ |
}
|
|
390 |
+ |
}
|
|
391 |
+ |
|
| 298 |
392 |
|
/// What a region is saying, when it is saying something.
|
| 299 |
393 |
|
///
|
| 300 |
394 |
|
/// The one intent family shared by badges, notices and nothing else. Kept
|
| 919 |
1013 |
|
assert_ne!(Depth::Well.fill(), Depth::Raised.fill());
|
| 920 |
1014 |
|
}
|
| 921 |
1015 |
|
|
|
1016 |
+ |
#[test]
|
|
1017 |
+ |
fn state_is_orthogonal_to_depth() {
|
|
1018 |
+ |
// The reason State is its own axis and not a Depth member: a disabled
|
|
1019 |
+ |
// button and a disabled field are both disabled and are not the same
|
|
1020 |
+ |
// shape, which one shared variant could not have said.
|
|
1021 |
+ |
assert_eq!(Depth::Raised.fill(), Some(Fill::Raised));
|
|
1022 |
+ |
assert_eq!(Depth::Well.fill(), Some(Fill::Well));
|
|
1023 |
+ |
assert!(State::Disabled.suppresses_interaction());
|
|
1024 |
+ |
}
|
|
1025 |
+ |
|
|
1026 |
+ |
#[test]
|
|
1027 |
+ |
fn only_disabled_stops_answering() {
|
|
1028 |
+ |
// Focus is a thing you can still click. Getting this backwards is how
|
|
1029 |
+ |
// a focus ring ends up on something inert.
|
|
1030 |
+ |
assert!(!State::Focus.suppresses_interaction());
|
|
1031 |
+ |
assert!(State::Disabled.suppresses_interaction());
|
|
1032 |
+ |
}
|
|
1033 |
+ |
|
|
1034 |
+ |
#[test]
|
|
1035 |
+ |
fn both_states_resolve_against_intents_makeover_already_derives() {
|
|
1036 |
+ |
// Neither needs a new token, so this costs no `makeover` release.
|
|
1037 |
+ |
assert_eq!(State::Focus.token(), "focus-ring");
|
|
1038 |
+ |
assert_eq!(State::Disabled.token(), "content-muted");
|
|
1039 |
+ |
}
|
|
1040 |
+ |
|
| 922 |
1041 |
|
#[test]
|
| 923 |
1042 |
|
fn flat_has_neither_edge_nor_fill() {
|
| 924 |
1043 |
|
assert_eq!(Depth::Flat.bevel(), None);
|
| 1006 |
1125 |
|
Edge::Dark.token(),
|
| 1007 |
1126 |
|
Tone::Danger.token(),
|
| 1008 |
1127 |
|
Tone::Neutral.token(),
|
|
1128 |
+ |
State::Focus.token(),
|
|
1129 |
+ |
State::Disabled.token(),
|
| 1009 |
1130 |
|
] {
|
| 1010 |
1131 |
|
assert!(!t.starts_with('#'), "{t} looks like a value");
|
| 1011 |
1132 |
|
assert!(
|