#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct StateOverlay {
pub hovered: bool,
pub pressed: bool,
pub focused: bool,
pub checked: bool,
}
impl StateOverlay {
pub fn from_base(hovered: bool, pressed: bool, focused: bool) -> Self {
Self { hovered, pressed, focused, checked: false }
}
pub fn is_empty(&self) -> bool {
!(self.hovered || self.pressed || self.focused || self.checked)
}
pub fn fill_blend(&self) -> f32 {
if self.pressed {
0.12
} else if self.hovered {
0.08
} else {
0.0
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
pub enum SemanticState {
#[default]
None,
Error,
Warning,
Success,
}
impl SemanticState {
pub fn state_suffix(self) -> Option<&'static str> {
match self {
Self::None => None,
Self::Error => Some(WidgetState::Error.state_suffix()),
Self::Warning => Some(WidgetState::Warning.state_suffix()),
Self::Success => Some(WidgetState::Success.state_suffix()),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default, PartialOrd, Ord)]
pub enum WidgetState {
#[default]
Normal,
Hover,
Pressed,
Focused,
Disabled,
Checked,
Selected,
Active,
Inactive,
Error,
Warning,
Success,
}
impl WidgetState {
pub const ALL: &'static [WidgetState] = &[
Self::Normal,
Self::Hover,
Self::Pressed,
Self::Focused,
Self::Disabled,
Self::Checked,
Self::Selected,
Self::Active,
Self::Inactive,
Self::Error,
Self::Warning,
Self::Success,
];
pub const fn state_suffix(self) -> &'static str {
match self {
Self::Normal => "normal",
Self::Hover => "hover",
Self::Pressed => "pressed",
Self::Focused => "focused",
Self::Disabled => "disabled",
Self::Checked => "checked",
Self::Selected => "selected",
Self::Active => "active",
Self::Inactive => "inactive",
Self::Error => "error",
Self::Warning => "warning",
Self::Success => "success",
}
}
pub fn from_state_suffix(name: &str) -> Option<Self> {
if let Some(state) = Self::ALL.iter().copied().find(|state| state.state_suffix() == name) {
return Some(state);
}
match name {
"focus" => Some(Self::Focused),
"invalid" => Some(Self::Error),
"valid" => Some(Self::Success),
_ => None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum ThemeMode {
#[default]
Light,
Dark,
Auto,
}
#[cfg(test)]
mod tests {
use super::*;
use crate::compat::Vec;
#[test]
fn the_default_semantic_state_is_no_meaning() {
assert_eq!(SemanticState::default(), SemanticState::None);
}
#[test]
fn each_meaning_addresses_a_theme_key() {
assert_eq!(SemanticState::None.state_suffix(), None);
assert_eq!(SemanticState::Error.state_suffix(), Some("error"));
assert_eq!(SemanticState::Warning.state_suffix(), Some("warning"));
assert_eq!(SemanticState::Success.state_suffix(), Some("success"));
}
#[test]
fn every_state_has_one_round_tripping_suffix() {
let mut seen: Vec<&str> = Vec::new();
for state in WidgetState::ALL {
let suffix = state.state_suffix();
assert!(!seen.contains(&suffix), "`{suffix}` is claimed by more than one state");
seen.push(suffix);
assert_eq!(
WidgetState::from_state_suffix(suffix),
Some(*state),
"`{suffix}` must resolve back to the state that spells it"
);
}
assert_eq!(seen.len(), 12, "the documented vocabulary has twelve states");
assert_eq!(SemanticState::Error.state_suffix(), Some(WidgetState::Error.state_suffix()));
}
#[test]
fn aliases_resolve_and_unknown_names_are_refused() {
assert_eq!(WidgetState::from_state_suffix("focus"), Some(WidgetState::Focused));
assert_eq!(WidgetState::from_state_suffix("invalid"), Some(WidgetState::Error));
assert_eq!(WidgetState::from_state_suffix("valid"), Some(WidgetState::Success));
assert_eq!(WidgetState::from_state_suffix("erorr"), None);
assert_eq!(WidgetState::from_state_suffix("pressed"), Some(WidgetState::Pressed));
assert_eq!(WidgetState::from_state_suffix("active"), Some(WidgetState::Active));
}
#[test]
fn the_semantic_widget_states_are_still_present() {
let _ = [WidgetState::Error, WidgetState::Warning, WidgetState::Success];
}
#[test]
fn overlay_carries_simultaneous_states() {
let overlay = StateOverlay::from_base(true, false, true);
assert!(overlay.hovered && overlay.focused, "focused and hovered at once");
assert!(!overlay.is_empty());
assert_eq!(overlay.fill_blend(), 0.08, "a hover is the lighter step");
}
#[test]
fn overlay_fill_blend_follows_press_over_hover() {
let pressed = StateOverlay { pressed: true, hovered: true, ..StateOverlay::default() };
assert_eq!(pressed.fill_blend(), 0.12, "a press is the firmer step");
assert_eq!(StateOverlay::default().fill_blend(), 0.0);
assert!(StateOverlay::default().is_empty());
}
}