Skip to main content

ferroui_base/
class_binding_manager.rs

1use crate::data::{BindingBase, BindingExpressionBase};
2use crate::{FerroObject, FerroProperty, Ref, StyledElement};
3use std::cell::RefCell;
4use std::collections::HashMap;
5use std::rc::Rc;
6
7const CLASS_PROPERTY_PREFIX: &str = "__FerroReserved::Classes::";
8
9crate::ferro_thread_local! {
10    static REGISTERED_PROPERTIES: RefCell<HashMap<String, &'static FerroProperty>> = RefCell::new(HashMap::new());
11}
12
13/// Implements bindings to individual style classes of an element
14/// (`Classes.name="{Binding ...}"`): each class gets an intermediate boolean
15/// property whose value adds or removes the class.
16pub struct ClassBindingManager;
17
18impl ClassBindingManager {
19    /// Binds the presence of the class `class_name` on `target` to `source`.
20    pub fn bind(
21        target: &StyledElement,
22        class_name: &str,
23        source: &dyn BindingBase,
24        anchor: Option<&Ref<FerroObject>>,
25    ) -> Rc<dyn BindingExpressionBase> {
26        let property = Self::get_class_property(class_name);
27        target.bind_binding_with_anchor(property, source, anchor)
28    }
29
30    fn register_class_proxy_property(class_name: &str) -> &'static FerroProperty {
31        let property =
32            FerroProperty::register::<StyledElement, bool>(&format!("{CLASS_PROPERTY_PREFIX}{class_name}"), false);
33        let class_name = class_name.to_string();
34        property.changed().subscribe(move |args| {
35            if let Some(element) = args.sender().downcast_ref::<StyledElement>() {
36                let value = args.get_new_value::<bool>();
37                element.classes().set(&class_name, value);
38            }
39        });
40        property
41    }
42
43    /// The intermediate property for the class `class_name`.
44    pub fn get_class_property(class_name: &str) -> &'static FerroProperty {
45        let prefixed = format!("{CLASS_PROPERTY_PREFIX}{class_name}");
46        let existing = REGISTERED_PROPERTIES.with(|p| p.borrow().get(&prefixed).copied());
47        match existing {
48            Some(property) => property,
49            None => {
50                let property = Self::register_class_proxy_property(class_name);
51                REGISTERED_PROPERTIES.with(|p| p.borrow_mut().insert(prefixed, property));
52                property
53            }
54        }
55    }
56
57    /// Whether `property` is the intermediate property of a class binding;
58    /// if so, returns the class name.
59    pub fn is_classes_binding_property(property: &FerroProperty) -> Option<&str> {
60        let name = property.name();
61        let prefix_len = CLASS_PROPERTY_PREFIX.len();
62        if name.len() >= prefix_len && name[..prefix_len].eq_ignore_ascii_case(CLASS_PROPERTY_PREFIX) {
63            // As in the managed implementation, the reported name starts one
64            // character past the prefix.
65            Some(name.get(prefix_len + 1..).unwrap_or(""))
66        } else {
67            None
68        }
69    }
70}
71
72#[cfg(test)]
73mod tests {
74    use super::*;
75
76    #[test]
77    fn get_class_property_should_return_same_instance_for_same_class() {
78        let property1 = ClassBindingManager::get_class_property("Foo");
79        let property2 = ClassBindingManager::get_class_property("Foo");
80        assert!(std::ptr::eq(property1, property2));
81    }
82
83    #[test]
84    fn get_class_property_should_return_different_instances_for_different_classes() {
85        let property1 = ClassBindingManager::get_class_property("Foo");
86        let property2 = ClassBindingManager::get_class_property("Bar");
87        assert!(!std::ptr::eq(property1, property2));
88    }
89}