ferroui_base/controls/
classes.rs1use crate::collections::{CollectionChangedHandler, FerroList};
2use crate::utilities::HandlerList;
3use std::rc::Rc;
4
5pub trait IPseudoClasses {
7 fn add_pseudo(&self, name: &str);
9 fn remove_pseudo(&self, name: &str) -> bool;
11 fn contains(&self, name: &str) -> bool;
13}
14
15#[derive(Clone, Default)]
24pub struct Classes(Rc<ClassesData>);
25
26#[derive(Default)]
27struct ClassesData {
28 items: FerroList<String>,
29 listeners: HandlerList<dyn Fn()>,
30}
31
32impl PartialEq for Classes {
33 fn eq(&self, other: &Self) -> bool {
34 self.ptr_eq(other)
35 }
36}
37
38impl std::fmt::Debug for Classes {
39 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
40 write!(f, "Classes({})", self.snapshot().join(" "))
41 }
42}
43
44impl Classes {
45 pub fn new() -> Self {
47 Self::default()
48 }
49
50 pub fn from_names<S: Into<String>>(items: impl IntoIterator<Item = S>) -> Self {
52 Self(Rc::new(ClassesData {
53 items: FerroList::from_items(items.into_iter().map(Into::into)),
54 listeners: HandlerList::new(),
55 }))
56 }
57
58 pub fn ptr_eq(&self, other: &Self) -> bool {
60 Rc::ptr_eq(&self.0, &other.0)
61 }
62
63 pub fn parse(s: &str) -> Classes {
65 Self::from_names(s.split(' '))
66 }
67
68 pub fn capacity(&self) -> usize {
71 self.0.items.capacity()
72 }
73
74 pub fn set_capacity(&self, capacity: usize) {
80 self.0.items.set_capacity(capacity);
81 }
82
83 pub fn count(&self) -> usize {
85 self.0.items.count()
86 }
87
88 pub fn get(&self, index: usize) -> String {
90 self.0.items.get(index)
91 }
92
93 pub fn snapshot(&self) -> Rc<Vec<String>> {
95 self.0.items.snapshot()
96 }
97
98 pub fn contains(&self, name: &str) -> bool {
100 self.0.items.snapshot().iter().any(|c| c == name)
101 }
102
103 pub fn add_collection_changed(&self, handler: Rc<CollectionChangedHandler<String>>) -> u64 {
105 self.0.items.add_collection_changed(handler)
106 }
107
108 pub fn remove_collection_changed(&self, token: u64) -> bool {
109 self.0.items.remove_collection_changed(token)
110 }
111
112 pub fn add(&self, name: &str) {
115 Self::panic_if_pseudoclass(name, "added");
116 if !self.contains(name) {
117 self.0.items.add(name.to_string());
118 self.notify_changed();
119 }
120 }
121
122 pub fn add_range<S: AsRef<str>>(&self, names: impl IntoIterator<Item = S>) {
124 let mut to_add: Vec<String> = Vec::new();
125 for name in names {
126 let name = name.as_ref();
127 Self::panic_if_pseudoclass(name, "added");
128 if !self.contains(name) {
129 to_add.push(name.to_string());
130 }
131 }
132 self.0.items.add_range(to_add);
133 self.notify_changed();
134 }
135
136 pub fn clear(&self) {
138 let mut i = self.0.items.count();
139 while i > 0 {
140 i -= 1;
141 if !self.0.items.get(i).starts_with(':') {
142 self.0.items.remove_at(i);
143 }
144 }
145 self.notify_changed();
146 }
147
148 pub fn insert(&self, index: usize, name: &str) {
150 Self::panic_if_pseudoclass(name, "added");
151 if !self.contains(name) {
152 self.0.items.insert(index, name.to_string());
153 self.notify_changed();
154 }
155 }
156
157 pub fn insert_range<S: AsRef<str>>(&self, index: usize, names: impl IntoIterator<Item = S>) {
159 let mut to_insert: Vec<String> = Vec::new();
160 for name in names {
161 let name = name.as_ref();
162 Self::panic_if_pseudoclass(name, "added");
163 if !self.contains(name) {
164 to_insert.push(name.to_string());
165 }
166 }
167 if !to_insert.is_empty() {
168 self.0.items.insert_range(index, to_insert);
169 self.notify_changed();
170 }
171 }
172
173 pub fn remove(&self, name: &str) -> bool {
175 Self::panic_if_pseudoclass(name, "removed");
176 if self.0.items.remove(&name.to_string()) {
177 self.notify_changed();
178 true
179 } else {
180 false
181 }
182 }
183
184 pub fn remove_all<S: AsRef<str>>(&self, names: impl IntoIterator<Item = S>) {
186 let mut to_remove: Vec<String> = Vec::new();
187 for name in names {
188 Self::panic_if_pseudoclass(name.as_ref(), "removed");
189 to_remove.push(name.as_ref().to_string());
190 }
191 if !to_remove.is_empty() {
192 self.0.items.remove_all(to_remove);
193 self.notify_changed();
194 }
195 }
196
197 pub fn remove_at(&self, index: usize) {
199 let name = self.0.items.get(index);
200 Self::panic_if_pseudoclass(&name, "removed");
201 self.0.items.remove_at(index);
202 self.notify_changed();
203 }
204
205 pub fn remove_range(&self, index: usize, count: usize) {
207 self.0.items.remove_range(index, count);
208 self.notify_changed();
209 }
210
211 pub fn replace<S: AsRef<str>>(&self, source: &[S]) {
213 for name in source {
214 Self::panic_if_pseudoclass(name.as_ref(), "added");
215 }
216 let to_remove: Vec<String> = self.0.items.snapshot().iter().filter(|n| !n.starts_with(':')).cloned().collect();
217 if !to_remove.is_empty() {
218 self.0.items.remove_all(to_remove);
219 }
220 self.0.items.add_range(source.iter().map(|s| s.as_ref().to_string()));
221 self.notify_changed();
222 }
223
224 pub fn set(&self, name: &str, value: bool) {
226 if value {
227 if !self.contains(name) {
228 self.add(name);
229 }
230 } else {
231 self.remove(name);
232 }
233 }
234
235 pub fn add_listener(&self, listener: Rc<dyn Fn()>) -> u64 {
238 self.0.listeners.add(listener)
239 }
240
241 pub fn remove_listener(&self, token: u64) -> bool {
242 self.0.listeners.remove(token)
243 }
244
245 pub fn listener_count(&self) -> usize {
247 self.0.listeners.len()
248 }
249
250 fn notify_changed(&self) {
251 if self.0.listeners.is_empty() {
252 return;
253 }
254 for (_, listener) in self.0.listeners.snapshot().iter() {
255 listener();
256 }
257 }
258
259 fn panic_if_pseudoclass(name: &str, operation: &str) {
260 if name.starts_with(':') {
261 panic!("The pseudoclass '{name}' may only be {operation} by the control itself.");
262 }
263 }
264}
265
266impl IPseudoClasses for Classes {
267 fn add_pseudo(&self, name: &str) {
268 if !Classes::contains(self, name) {
269 self.0.items.add(name.to_string());
270 self.notify_changed();
271 }
272 }
273
274 fn remove_pseudo(&self, name: &str) -> bool {
275 let index = self.0.items.snapshot().iter().position(|c| c == name);
278 if let Some(index) = index {
279 self.0.items.remove_at(index);
280 self.notify_changed();
281 true
282 } else {
283 false
284 }
285 }
286
287 fn contains(&self, name: &str) -> bool {
288 Classes::contains(self, name)
289 }
290}
291
292impl dyn IPseudoClasses + '_ {
293 pub fn set(&self, name: &str, value: bool) -> bool {
295 if value {
296 self.add_pseudo(name);
297 } else {
298 self.remove_pseudo(name);
299 }
300 value
301 }
302}
303
304#[cfg(test)]
305mod tests {
306 use super::*;
307 use std::cell::Cell;
308
309 #[test]
310 fn duplicates_should_not_be_added() {
311 let target = Classes::new();
312 target.add("foo");
313 target.add("foo");
314 assert_eq!(*target.snapshot(), vec!["foo".to_string()]);
315 }
316
317 #[test]
318 #[should_panic]
319 fn should_not_be_able_to_add_pseudoclass() {
320 Classes::new().add(":foo");
321 }
322
323 #[test]
324 fn should_be_able_to_add_pseudoclass_through_interface() {
325 let target = Classes::new();
326 target.add_pseudo(":foo");
327 assert_eq!(*target.snapshot(), vec![":foo".to_string()]);
328 }
329
330 #[test]
331 fn clear_should_not_remove_pseudoclasses() {
332 let target = Classes::new();
333 target.add("foo");
334 target.add_pseudo(":bar");
335 target.add("baz");
336 target.clear();
337 assert_eq!(*target.snapshot(), vec![":bar".to_string()]);
338 }
339
340 #[test]
341 fn replace_should_not_replace_pseudoclasses() {
342 let target = Classes::new();
343 target.add("foo");
344 target.add("bar");
345 target.add_pseudo(":baz");
346 target.replace(&["qux"]);
347 assert_eq!(*target.snapshot(), vec![":baz".to_string(), "qux".to_string()]);
348 }
349
350 #[test]
351 fn listeners_are_notified() {
352 let target = Classes::new();
353 let count = Rc::new(Cell::new(0));
354 let c = count.clone();
355 target.add_listener(Rc::new(move || c.set(c.get() + 1)));
356 target.add("foo");
357 target.remove("foo");
358 assert_eq!(count.get(), 2);
359 }
360}