Skip to main content

qframe/theme/
selector.rs

1//! Style rule selectors: `widget`, `widget.variant`, `widget:state`, `widget.variant:state:state`.
2
3use std::fmt;
4
5/// An interaction state a style rule can target.
6#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub enum State {
8    /// The pointer is over the widget.
9    Hover,
10    /// The widget has keyboard focus.
11    Focus,
12    /// The widget is the current choice, e.g. the open tab.
13    Active,
14    /// The widget is being pressed right now (the confirmation flash).
15    Pressed,
16    /// The widget cannot be used.
17    Disabled,
18    /// The row or item is selected.
19    Selected,
20    /// A checkbox, switch or radio is on.
21    Checked,
22    /// The value failed validation.
23    Invalid,
24}
25
26impl State {
27    /// Every state, in declaration order.
28    pub const ALL: [Self; 8] = [
29        Self::Hover,
30        Self::Focus,
31        Self::Active,
32        Self::Pressed,
33        Self::Disabled,
34        Self::Selected,
35        Self::Checked,
36        Self::Invalid,
37    ];
38
39    /// The name used in theme files.
40    #[must_use]
41    pub fn name(self) -> &'static str {
42        match self {
43            Self::Hover => "hover",
44            Self::Focus => "focus",
45            Self::Active => "active",
46            Self::Pressed => "pressed",
47            Self::Disabled => "disabled",
48            Self::Selected => "selected",
49            Self::Checked => "checked",
50            Self::Invalid => "invalid",
51        }
52    }
53
54    /// Looks a state up by its theme file name.
55    #[must_use]
56    pub fn from_name(name: &str) -> Option<Self> {
57        Self::ALL.into_iter().find(|state| state.name() == name)
58    }
59}
60
61/// Which widgets, variants and states a style rule applies to.
62#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct Selector {
64    widget: String,
65    variant: Option<String>,
66    states: Vec<State>,
67}
68
69impl Selector {
70    /// Parses a selector such as `button.primary:hover`.
71    ///
72    /// # Errors
73    ///
74    /// Returns a message when a name is empty or uses characters other than `a-z`, `0-9`
75    /// and `-`, or when a state is unknown.
76    pub fn parse(text: &str) -> Result<Self, String> {
77        let mut parts = text.split(':');
78        let head = parts.next().unwrap_or_default();
79        let (widget, variant) = match head.split_once('.') {
80            Some((widget, variant)) => (widget, Some(variant)),
81            None => (head, None),
82        };
83        check_name(widget, "widget", text)?;
84        if let Some(variant) = variant {
85            check_name(variant, "variant", text)?;
86        }
87        let mut states = parts
88            .map(|name| {
89                State::from_name(name).ok_or_else(|| {
90                    let known: Vec<&str> = State::ALL.iter().map(|s| s.name()).collect();
91                    format!("unknown state `:{name}` in `{text}`; known states: {}", known.join(", "))
92                })
93            })
94            .collect::<Result<Vec<_>, _>>()?;
95        states.sort();
96        states.dedup();
97        Ok(Self { widget: widget.to_owned(), variant: variant.map(str::to_owned), states })
98    }
99
100    /// The widget name.
101    #[must_use]
102    pub fn widget(&self) -> &str {
103        &self.widget
104    }
105
106    /// The variant name, if the selector names one.
107    #[must_use]
108    pub fn variant(&self) -> Option<&str> {
109        self.variant.as_deref()
110    }
111
112    /// The states the selector requires, sorted.
113    #[must_use]
114    pub fn states(&self) -> &[State] {
115        &self.states
116    }
117
118    /// Higher wins: states count first, then whether a variant is named.
119    #[must_use]
120    pub fn specificity(&self) -> (usize, bool) {
121        (self.states.len(), self.variant.is_some())
122    }
123
124    /// Whether a widget drawn with `variant` in `states` is targeted by this selector.
125    #[must_use]
126    pub fn matches(&self, widget: &str, variant: Option<&str>, states: &[State]) -> bool {
127        self.widget == widget
128            && self.variant.as_deref().is_none_or(|v| Some(v) == variant)
129            && self.states.iter().all(|state| states.contains(state))
130    }
131}
132
133impl fmt::Display for Selector {
134    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
135        f.write_str(&self.widget)?;
136        if let Some(variant) = &self.variant {
137            write!(f, ".{variant}")?;
138        }
139        for state in &self.states {
140            write!(f, ":{}", state.name())?;
141        }
142        Ok(())
143    }
144}
145
146fn check_name(name: &str, what: &str, selector: &str) -> Result<(), String> {
147    let starts_with_letter = name.chars().next().is_some_and(|c| c.is_ascii_lowercase());
148    let valid_chars = name.chars().all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-');
149    if starts_with_letter && valid_chars {
150        Ok(())
151    } else {
152        Err(format!(
153            "invalid {what} name `{name}` in `{selector}`; use lowercase letters, digits and `-`, starting with a letter"
154        ))
155    }
156}
157
158#[cfg(test)]
159mod tests {
160    use super::*;
161
162    #[test]
163    fn parses_all_parts() {
164        let selector = Selector::parse("button.primary:hover:focus").expect("valid");
165        assert_eq!(selector.widget(), "button");
166        assert_eq!(selector.variant(), Some("primary"));
167        assert_eq!(selector.states(), &[State::Hover, State::Focus]);
168        assert_eq!(selector.to_string(), "button.primary:hover:focus");
169    }
170
171    #[test]
172    fn rejects_bad_names_and_states() {
173        assert!(Selector::parse("list.item.selected").is_err());
174        assert!(Selector::parse("Button").is_err());
175        assert!(Selector::parse("button:glow").is_err());
176        assert!(Selector::parse("").is_err());
177        assert!(Selector::parse("list-item:selected").is_ok());
178    }
179
180    #[test]
181    fn specificity_orders_base_variant_state_both() {
182        let spec = |s: &str| Selector::parse(s).expect("valid").specificity();
183        assert!(spec("button") < spec("button.primary"));
184        assert!(spec("button.primary") < spec("button:hover"));
185        assert!(spec("button:hover") < spec("button.primary:hover"));
186    }
187
188    #[test]
189    fn matching_requires_all_states() {
190        let selector = Selector::parse("button.primary:hover").expect("valid");
191        assert!(selector.matches("button", Some("primary"), &[State::Hover, State::Focus]));
192        assert!(!selector.matches("button", Some("primary"), &[State::Focus]));
193        assert!(!selector.matches("button", None, &[State::Hover]));
194        let base = Selector::parse("button").expect("valid");
195        assert!(base.matches("button", Some("danger"), &[]));
196        assert!(!base.matches("switch", None, &[]));
197    }
198}