Skip to main content

ferroui_xaml_interactions_responsive/
adaptive_behavior.rs

1//! Port of `AdaptiveBehavior.cs`.
2
3use crate::AdaptiveClassSetter;
4use ferroui_base::collections::FerroList;
5use ferroui_base::controls::IPseudoClasses;
6use ferroui_base::data::core::ValueTypes;
7use ferroui_base::reactive::{IDisposable, ObservableExt};
8use ferroui_base::{
9    ferro_class, ferro_class_info, ferro_properties, instantiate, DirectProperty, ElementRef, FerroObject,
10    FerroObjectExtensions, FerroObjectImpl, FerroProperty, Nullable, Rect, Ref, StaticType, StyledElementImpl,
11    StyledProperty, Visual,
12};
13use ferroui_controls::Control;
14use ferroui_xaml_interactivity::{
15    ComparisonConditionType, StyledElementBehaviorImpl, StyledElementBehaviorImplExt, StyledElementBehaviorOfT,
16};
17use std::cell::{OnceCell, RefCell};
18use std::ops::Deref;
19use std::rc::Rc;
20
21/// The adaptive class setters of an [`AdaptiveBehavior`]: a list of [`AdaptiveClassSetter`].
22///
23/// Deviation (DEVIATIONS.md, Behaviors): the reference types the collection as the generic list
24/// of the framework of `AdaptiveClassSetter`; here it is a named list (as `ConditionCollection`
25/// of the interactivity crate), so that markup can add the setters of a document to it.
26///
27/// It is a reference object: clones refer to the same collection and compare by identity.
28#[derive(Clone, PartialEq)]
29pub struct AdaptiveClassSetters(FerroList<Ref<AdaptiveClassSetter>>);
30
31impl AdaptiveClassSetters {
32    /// Creates an empty collection.
33    pub fn new() -> Self {
34        Self(FerroList::new())
35    }
36
37    /// The list of the collection.
38    pub fn list(&self) -> &FerroList<Ref<AdaptiveClassSetter>> {
39        &self.0
40    }
41}
42
43impl Default for AdaptiveClassSetters {
44    fn default() -> Self {
45        Self::new()
46    }
47}
48
49impl Deref for AdaptiveClassSetters {
50    type Target = FerroList<Ref<AdaptiveClassSetter>>;
51
52    fn deref(&self) -> &FerroList<Ref<AdaptiveClassSetter>> {
53        &self.0
54    }
55}
56
57impl std::fmt::Debug for AdaptiveClassSetters {
58    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
59        write!(f, "AdaptiveClassSetters({})", self.0.count())
60    }
61}
62
63/// Observes the [`Visual::bounds`] of the associated control, or of the
64/// [`source_control`](Self::source_control), and sets or removes style classes when the
65/// conditions of its [`AdaptiveClassSetter`]s are met.
66#[repr(C)]
67pub struct AdaptiveBehavior {
68    base: StyledElementBehaviorOfT,
69    disposable: RefCell<Option<Rc<dyn IDisposable>>>,
70    setters: OnceCell<AdaptiveClassSetters>,
71}
72
73ferro_class!(AdaptiveBehavior: StyledElementBehaviorOfT);
74ferro_class_info!(AdaptiveBehavior {
75    new: AdaptiveBehavior::new,
76    markup: {
77        content: Setters,
78        property_attributes: [
79            SourceControl: [ResolveByName],
80            TargetControl: [ResolveByName],
81        ],
82    },
83});
84ferroui_base::ferro_impl_classes!(AdaptiveBehavior: FerroObjectImpl, StyledElementImpl);
85
86ferro_properties! {
87    impl AdaptiveBehavior {
88        /// Identifies the `SourceControl` property.
89        ///
90        /// Deviation (DEVIATIONS.md, Behaviors): the behavior does not own its source, so the
91        /// value is an element reference.
92        pub fn source_control_property() -> StyledProperty<Option<ElementRef<Control>>> {
93            ValueTypes::register_element_ref::<Control>();
94            FerroProperty::register::<AdaptiveBehavior, _>("SourceControl", None)
95        }
96
97        /// Identifies the `TargetControl` property (an element reference, as `SourceControl`).
98        pub fn target_control_property() -> StyledProperty<Option<ElementRef<Control>>> {
99            ValueTypes::register_element_ref::<Control>();
100            FerroProperty::register::<AdaptiveBehavior, _>("TargetControl", None)
101        }
102
103        /// Identifies the `Setters` property.
104        pub fn setters_property() -> DirectProperty<AdaptiveBehavior, AdaptiveClassSetters> {
105            FerroProperty::register_direct::<AdaptiveBehavior, _>(
106                "Setters",
107                |behavior| behavior.setters(),
108                None,
109                AdaptiveClassSetters::new(),
110            )
111        }
112    }
113}
114
115ferroui_base::ferro_overrides! {
116    impl StyledElementBehaviorImpl for AdaptiveBehavior {
117        fn on_attached_to_visual_tree(this: &Self) {
118            Self::parent_on_attached_to_visual_tree(this);
119
120            this.stop_observing();
121            this.start_observing();
122        }
123
124        fn on_detached_from_visual_tree(this: &Self) {
125            Self::parent_on_detached_from_visual_tree(this);
126
127            this.stop_observing();
128        }
129    }
130}
131
132impl AdaptiveBehavior {
133    /// Creates the class data; see [`FerroObject::construct`].
134    pub fn construct() -> Self {
135        Self {
136            base: StyledElementBehaviorOfT::construct(<Control as StaticType>::TYPE),
137            disposable: RefCell::new(None),
138            setters: OnceCell::new(),
139        }
140    }
141
142    /// Creates a behavior.
143    pub fn new() -> Ref<Self> {
144        instantiate(Self::construct())
145    }
146
147    /// Gets or sets the source control whose [`Visual::bounds_property`] property is
148    /// observed; if it is not set, the associated object is used. This is a property of the object
149    /// model; markup resolves a name given for it to the named control.
150    pub fn source_control(&self) -> Option<Ref<Control>> {
151        ElementRef::resolve(&self.get_value(Self::source_control_property()))
152    }
153
154    pub fn set_source_control(&self, value: impl Into<Nullable<Control>>) {
155        self.set_value(Self::source_control_property(), ElementRef::from_nullable(value.into().0))
156    }
157
158    /// Gets or sets the target control that class name that should be added or removed when
159    /// triggered, if not set the associated object is used or the
160    /// [`target_control`](AdaptiveClassSetter::target_control) of the [`AdaptiveClassSetter`].
161    /// This is a property of the object model; markup resolves a name given for it to the named
162    /// control.
163    pub fn target_control(&self) -> Option<Ref<Control>> {
164        ElementRef::resolve(&self.get_value(Self::target_control_property()))
165    }
166
167    pub fn set_target_control(&self, value: impl Into<Nullable<Control>>) {
168        self.set_value(Self::target_control_property(), ElementRef::from_nullable(value.into().0))
169    }
170
171    /// Gets adaptive class setters collection. This is a property of the object model, and the
172    /// content of the behavior in markup.
173    pub fn setters(&self) -> AdaptiveClassSetters {
174        self.setters.get_or_init(AdaptiveClassSetters::new).clone()
175    }
176
177    fn start_observing(&self) {
178        let source_control = match self.source_control() {
179            Some(source_control) => Some(source_control),
180            None => self.associated_object_as::<Control>(),
181        };
182
183        if let Some(source_control) = source_control {
184            let disposable = self.observe_bounds(&source_control);
185            *self.disposable.borrow_mut() = Some(disposable);
186        }
187    }
188
189    fn stop_observing(&self) {
190        let disposable = self.disposable.borrow_mut().take();
191        if let Some(disposable) = disposable {
192            disposable.dispose();
193        }
194    }
195
196    fn observe_bounds(&self, source_control: &Ref<Control>) -> Rc<dyn IDisposable> {
197        self.execute(Some(source_control), Some(&self.setters()), source_control.bounds());
198
199        // The behavior and the source are held weakly: the subscription lives in the source.
200        let behavior = self.to_ref().downgrade();
201        let source = source_control.downgrade();
202        let source_object: &FerroObject = source_control;
203        FerroObjectExtensions::get_observable(source_object, Visual::bounds_property()).subscribe_fn(
204            move |bounds: Rect| {
205                if let (Some(behavior), Some(source)) = (behavior.upgrade(), source.upgrade()) {
206                    behavior.execute(Some(&source), Some(&behavior.setters()), bounds);
207                }
208            },
209        )
210    }
211
212    fn execute(&self, source_control: Option<&Ref<Control>>, setters: Option<&AdaptiveClassSetters>, bounds: Rect) {
213        let (Some(_), Some(setters)) = (source_control, setters) else {
214            return;
215        };
216
217        for setter in setters.snapshot().iter() {
218            let is_min_or_max_width_set = setter.is_set(AdaptiveClassSetter::min_width_property().as_property())
219                || setter.is_set(AdaptiveClassSetter::max_width_property().as_property());
220            let width_condition_triggered = Self::get_result(setter.min_width_operator(), bounds.width, setter.min_width())
221                && Self::get_result(setter.max_width_operator(), bounds.width, setter.max_width());
222
223            let is_min_or_max_height_set = setter.is_set(AdaptiveClassSetter::min_height_property().as_property())
224                || setter.is_set(AdaptiveClassSetter::max_height_property().as_property());
225            let height_condition_triggered =
226                Self::get_result(setter.min_height_operator(), bounds.height, setter.min_height())
227                    && Self::get_result(setter.max_height_operator(), bounds.height, setter.max_height());
228
229            let is_add_class_triggered = match (is_min_or_max_width_set, is_min_or_max_height_set) {
230                (true, false) => width_condition_triggered,
231                (false, true) => height_condition_triggered,
232                (true, true) => width_condition_triggered && height_condition_triggered,
233                (false, false) => false,
234            };
235
236            let target_control = match setter.target_control() {
237                Some(target_control) => Some(target_control),
238                None => match self.target_control() {
239                    Some(target_control) => Some(target_control),
240                    None => self.associated_object_as::<Control>(),
241                },
242            };
243
244            let Some(target_control) = target_control else {
245                panic!("Value cannot be null. (Parameter 'targetControl')");
246            };
247
248            let class_name = setter.class_name();
249            let is_pseudo_class = setter.is_pseudo_class();
250
251            if is_add_class_triggered {
252                Self::add(&target_control, class_name.as_deref(), is_pseudo_class);
253            } else {
254                Self::remove(&target_control, class_name.as_deref(), is_pseudo_class);
255            }
256        }
257    }
258
259    fn get_result(comparison_condition_type: ComparisonConditionType, property: f64, value: f64) -> bool {
260        match comparison_condition_type {
261            ComparisonConditionType::Equal => property == value,
262            ComparisonConditionType::NotEqual => property != value,
263            ComparisonConditionType::LessThan => property < value,
264            ComparisonConditionType::LessThanOrEqual => property <= value,
265            ComparisonConditionType::GreaterThan => property > value,
266            ComparisonConditionType::GreaterThanOrEqual => property >= value,
267        }
268    }
269
270    fn add(target_control: &Control, class_name: Option<&str>, is_pseudo_class: bool) {
271        let Some(class_name) = class_name.filter(|class_name| !class_name.is_empty()) else {
272            return;
273        };
274        let classes = target_control.classes();
275        if classes.contains(class_name) {
276            return;
277        }
278
279        if is_pseudo_class {
280            classes.add_pseudo(class_name);
281        } else {
282            classes.add(class_name);
283        }
284    }
285
286    fn remove(target_control: &Control, class_name: Option<&str>, is_pseudo_class: bool) {
287        let Some(class_name) = class_name.filter(|class_name| !class_name.is_empty()) else {
288            return;
289        };
290        let classes = target_control.classes();
291        if !classes.contains(class_name) {
292            return;
293        }
294
295        if is_pseudo_class {
296            classes.remove_pseudo(class_name);
297        } else {
298            classes.remove(class_name);
299        }
300    }
301}