Skip to main content

ferroui_diagnostics/diagnostics/view_models/
control_layout_view_model.rs

1//! Port of `Diagnostics/ViewModels/ControlLayoutViewModel.cs`.
2
3use super::ViewModelBase;
4use ferroui_base::data::model::{Event, INotifyPropertyChanged};
5use ferroui_base::layout::{HorizontalAlignment, Layoutable, VerticalAlignment};
6use ferroui_base::{
7    ferro_markup_type, FerroObject, FerroPropertyChangedEventArgs, FerroPropertyRegistry, Ref, StyledProperty,
8    Thickness, Visual,
9};
10use ferroui_controls::{Border, Decorator};
11use std::cell::{Cell, RefCell};
12use std::rc::Rc;
13
14/// The layout of a control as the layout explorer shows it: the margin, the border and the
15/// padding (which edit the control), the size and its constraints, and the alignments.
16pub struct ControlLayoutViewModel {
17    base: ViewModelBase,
18    control: Ref<Visual>,
19    border_thickness: RefCell<Thickness>,
20    height: RefCell<f64>,
21    height_constraint: RefCell<Option<String>>,
22    horizontal_alignment: RefCell<HorizontalAlignment>,
23    margin_thickness: RefCell<Thickness>,
24    padding_thickness: RefCell<Thickness>,
25    updating_from_control: Cell<bool>,
26    vertical_alignment: RefCell<VerticalAlignment>,
27    width: RefCell<f64>,
28    width_constraint: RefCell<Option<String>>,
29    has_padding: bool,
30    has_border: bool,
31}
32
33impl PartialEq for ControlLayoutViewModel {
34    fn eq(&self, other: &Self) -> bool {
35        std::ptr::eq(self, other)
36    }
37}
38
39impl INotifyPropertyChanged for ControlLayoutViewModel {
40    fn property_changed(&self) -> &Event<str> {
41        self.base.property_changed()
42    }
43}
44
45/// `Math.Round(value, 2)`: to two decimals, a midpoint to the even neighbour.
46pub(crate) fn round2(value: f64) -> f64 {
47    if !value.is_finite() {
48        return value;
49    }
50    (value * 100.0).round_ties_even() / 100.0
51}
52
53/// A number as the managed original formats it into text (`{0}`): the shortest form that
54/// reads back, infinities as the symbol.
55pub(crate) fn format_number(value: f64) -> String {
56    if value == f64::INFINITY {
57        "\u{221e}".to_string()
58    } else if value == f64::NEG_INFINITY {
59        "-\u{221e}".to_string()
60    } else if value.is_nan() {
61        "NaN".to_string()
62    } else {
63        format!("{value}")
64    }
65}
66
67impl ControlLayoutViewModel {
68    pub fn new(control: &Visual) -> Rc<Self> {
69        let registry = FerroPropertyRegistry::instance();
70        let has_padding = registry.is_registered_for(control, Decorator::padding_property().as_property());
71        let has_border = registry.is_registered_for(control, Border::border_thickness_property().as_property());
72
73        let this = Rc::new(Self {
74            base: ViewModelBase::new(),
75            control: control.to_ref(),
76            border_thickness: RefCell::new(Thickness::default()),
77            height: RefCell::new(0.0),
78            height_constraint: RefCell::new(None),
79            horizontal_alignment: RefCell::new(HorizontalAlignment::Stretch),
80            margin_thickness: RefCell::new(Thickness::default()),
81            padding_thickness: RefCell::new(Thickness::default()),
82            updating_from_control: Cell::new(false),
83            vertical_alignment: RefCell::new(VerticalAlignment::Stretch),
84            width: RefCell::new(0.0),
85            width_constraint: RefCell::new(None),
86            has_padding,
87            has_border,
88        });
89
90        // `control is FerroObject ao`: a visual always is one.
91        {
92            let ao: &FerroObject = &this.control;
93
94            // `try { .. } finally { _updatingFromControl = false; }`: nothing in between can fail.
95            this.updating_from_control.set(true);
96            this.set_margin_thickness(ao.get_value(Layoutable::margin_property()));
97
98            if this.has_padding {
99                this.set_padding_thickness(ao.get_value(Decorator::padding_property()));
100            }
101
102            if this.has_border {
103                this.set_border_thickness(ao.get_value(Border::border_thickness_property()));
104            }
105
106            this.set_horizontal_alignment(ao.get_value(Layoutable::horizontal_alignment_property()));
107            this.set_vertical_alignment(ao.get_value(Layoutable::vertical_alignment_property()));
108            this.updating_from_control.set(false);
109        }
110
111        this.update_size();
112        this.update_size_constraints();
113
114        this
115    }
116
117    pub fn margin_thickness(&self) -> Thickness {
118        *self.margin_thickness.borrow()
119    }
120
121    pub fn set_margin_thickness(&self, value: Thickness) {
122        self.raise_and_set_if_changed(&self.margin_thickness, value, "MarginThickness");
123    }
124
125    pub fn border_thickness(&self) -> Thickness {
126        *self.border_thickness.borrow()
127    }
128
129    pub fn set_border_thickness(&self, value: Thickness) {
130        self.raise_and_set_if_changed(&self.border_thickness, value, "BorderThickness");
131    }
132
133    pub fn padding_thickness(&self) -> Thickness {
134        *self.padding_thickness.borrow()
135    }
136
137    pub fn set_padding_thickness(&self, value: Thickness) {
138        self.raise_and_set_if_changed(&self.padding_thickness, value, "PaddingThickness");
139    }
140
141    pub fn width(&self) -> f64 {
142        *self.width.borrow()
143    }
144
145    fn set_width(&self, value: f64) {
146        self.raise_and_set_if_changed(&self.width, value, "Width");
147    }
148
149    pub fn height(&self) -> f64 {
150        *self.height.borrow()
151    }
152
153    fn set_height(&self, value: f64) {
154        self.raise_and_set_if_changed(&self.height, value, "Height");
155    }
156
157    pub fn width_constraint(&self) -> Option<String> {
158        self.width_constraint.borrow().clone()
159    }
160
161    fn set_width_constraint(&self, value: Option<String>) {
162        self.raise_and_set_if_changed(&self.width_constraint, value, "WidthConstraint");
163    }
164
165    pub fn height_constraint(&self) -> Option<String> {
166        self.height_constraint.borrow().clone()
167    }
168
169    fn set_height_constraint(&self, value: Option<String>) {
170        self.raise_and_set_if_changed(&self.height_constraint, value, "HeightConstraint");
171    }
172
173    pub fn horizontal_alignment(&self) -> HorizontalAlignment {
174        *self.horizontal_alignment.borrow()
175    }
176
177    pub fn set_horizontal_alignment(&self, value: HorizontalAlignment) {
178        self.raise_and_set_if_changed(&self.horizontal_alignment, value, "HorizontalAlignment");
179    }
180
181    pub fn vertical_alignment(&self) -> VerticalAlignment {
182        *self.vertical_alignment.borrow()
183    }
184
185    pub fn set_vertical_alignment(&self, value: VerticalAlignment) {
186        self.raise_and_set_if_changed(&self.vertical_alignment, value, "VerticalAlignment");
187    }
188
189    pub fn has_padding(&self) -> bool {
190        self.has_padding
191    }
192
193    pub fn has_border(&self) -> bool {
194        self.has_border
195    }
196
197    /// `RaiseAndSetIfChanged` of the base class, through this class's `RaisePropertyChanged`,
198    /// which runs the override of `OnPropertyChanged` first.
199    fn raise_and_set_if_changed<T: PartialEq>(&self, field: &RefCell<T>, value: T, property_name: &str) -> bool {
200        if *field.borrow() == value {
201            return false;
202        }
203        *field.borrow_mut() = value;
204        self.raise_property_changed(property_name);
205        true
206    }
207
208    /// `RaisePropertyChanged`: the override of `OnPropertyChanged`, then the handlers.
209    fn raise_property_changed(&self, property_name: &str) {
210        self.on_property_changed(property_name);
211        self.base.raise_property_changed(property_name);
212    }
213
214    fn update_size_constraints(&self) {
215        let ao: &FerroObject = &self.control;
216
217        let create_constraint_info =
218            |min_property: &'static StyledProperty<f64>, max_property: &'static StyledProperty<f64>| -> Option<String> {
219                let has_min = ao.is_set(min_property.as_property());
220                let has_max = ao.is_set(max_property.as_property());
221
222                if has_min || has_max {
223                    let mut builder = String::new();
224
225                    if has_min {
226                        let min_value = ao.get_value(min_property);
227                        builder.push_str(&format!("Min: {}", format_number(round2(min_value))));
228                        // `AppendLine()`.
229                        builder.push('\n');
230                    }
231
232                    if has_max {
233                        let max_value = ao.get_value(max_property);
234                        builder.push_str(&format!("Max: {}", format_number(round2(max_value))));
235                    }
236
237                    return Some(builder);
238                }
239
240                None
241            };
242
243        self.set_width_constraint(create_constraint_info(
244            Layoutable::min_width_property(),
245            Layoutable::max_width_property(),
246        ));
247        self.set_height_constraint(create_constraint_info(
248            Layoutable::min_height_property(),
249            Layoutable::max_height_property(),
250        ));
251    }
252
253    /// The override of `OnPropertyChanged`: an edit of the view model sets the property of the
254    /// control, unless the change comes from the control.
255    fn on_property_changed(&self, property_name: &str) {
256        if self.updating_from_control.get() {
257            return;
258        }
259
260        let ao: &FerroObject = &self.control;
261
262        if property_name == "MarginThickness" {
263            ao.set_value(Layoutable::margin_property(), self.margin_thickness());
264        } else if self.has_padding && property_name == "PaddingThickness" {
265            ao.set_value(Decorator::padding_property(), self.padding_thickness());
266        } else if self.has_border && property_name == "BorderThickness" {
267            ao.set_value(Border::border_thickness_property(), self.border_thickness());
268        } else if property_name == "HorizontalAlignment" {
269            ao.set_value(Layoutable::horizontal_alignment_property(), self.horizontal_alignment());
270        } else if property_name == "VerticalAlignment" {
271            ao.set_value(Layoutable::vertical_alignment_property(), self.vertical_alignment());
272        }
273    }
274
275    /// Follows a change of a property of the control.
276    pub fn control_property_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
277        // `try { .. } finally { _updatingFromControl = false; }`: nothing in between can fail.
278        self.updating_from_control.set(true);
279
280        let property = e.property();
281
282        if property == Visual::bounds_property().as_property() {
283            self.update_size();
284        } else {
285            let ao: &FerroObject = &self.control;
286
287            if property == Layoutable::margin_property().as_property() {
288                self.set_margin_thickness(ao.get_value(Layoutable::margin_property()));
289            } else if property == Decorator::padding_property().as_property() {
290                self.set_padding_thickness(ao.get_value(Decorator::padding_property()));
291            } else if property == Border::border_thickness_property().as_property() {
292                self.set_border_thickness(ao.get_value(Border::border_thickness_property()));
293            } else if property == Layoutable::min_width_property().as_property()
294                || property == Layoutable::max_width_property().as_property()
295                || property == Layoutable::min_height_property().as_property()
296                || property == Layoutable::max_height_property().as_property()
297            {
298                self.update_size_constraints();
299            } else if property == Layoutable::horizontal_alignment_property().as_property() {
300                self.set_horizontal_alignment(ao.get_value(Layoutable::horizontal_alignment_property()));
301            } else if property == Layoutable::vertical_alignment_property().as_property() {
302                self.set_vertical_alignment(ao.get_value(Layoutable::vertical_alignment_property()));
303            }
304        }
305
306        self.updating_from_control.set(false);
307    }
308
309    fn update_size(&self) {
310        let size = self.control.bounds();
311
312        self.set_width(round2(size.width));
313        self.set_height(round2(size.height));
314    }
315}
316
317ferro_markup_type!(class ControlLayoutViewModel {
318    this: Rc<ControlLayoutViewModel>,
319    handles: [ControlLayoutViewModel, Rc<ControlLayoutViewModel>, Option<Rc<ControlLayoutViewModel>>],
320    properties: [
321        MarginThickness: Thickness {
322            get: ControlLayoutViewModel::margin_thickness,
323            set: ControlLayoutViewModel::set_margin_thickness
324        },
325        BorderThickness: Thickness {
326            get: ControlLayoutViewModel::border_thickness,
327            set: ControlLayoutViewModel::set_border_thickness
328        },
329        PaddingThickness: Thickness {
330            get: ControlLayoutViewModel::padding_thickness,
331            set: ControlLayoutViewModel::set_padding_thickness
332        },
333        Width: f64 { get: ControlLayoutViewModel::width },
334        Height: f64 { get: ControlLayoutViewModel::height },
335        WidthConstraint: Option<String> { get: ControlLayoutViewModel::width_constraint },
336        HeightConstraint: Option<String> { get: ControlLayoutViewModel::height_constraint },
337        HorizontalAlignment: HorizontalAlignment {
338            get: ControlLayoutViewModel::horizontal_alignment,
339            set: ControlLayoutViewModel::set_horizontal_alignment
340        },
341        VerticalAlignment: VerticalAlignment {
342            get: ControlLayoutViewModel::vertical_alignment,
343            set: ControlLayoutViewModel::set_vertical_alignment
344        },
345        HasPadding: bool { get: ControlLayoutViewModel::has_padding },
346        HasBorder: bool { get: ControlLayoutViewModel::has_border },
347    ],
348    notify_property_changed: ControlLayoutViewModel,
349});