1use std::fmt;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash)]
7pub enum State {
8 Hover,
10 Focus,
12 Active,
14 Pressed,
16 Disabled,
18 Selected,
20 Checked,
22 Invalid,
24}
25
26impl State {
27 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 #[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 #[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#[derive(Debug, Clone, PartialEq, Eq)]
63pub struct Selector {
64 widget: String,
65 variant: Option<String>,
66 states: Vec<State>,
67}
68
69impl Selector {
70 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 #[must_use]
102 pub fn widget(&self) -> &str {
103 &self.widget
104 }
105
106 #[must_use]
108 pub fn variant(&self) -> Option<&str> {
109 self.variant.as_deref()
110 }
111
112 #[must_use]
114 pub fn states(&self) -> &[State] {
115 &self.states
116 }
117
118 #[must_use]
120 pub fn specificity(&self) -> (usize, bool) {
121 (self.states.len(), self.variant.is_some())
122 }
123
124 #[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}