ferroui_diagnostics/diagnostics/view_models/
control_layout_view_model.rs1use 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
14pub 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
45pub(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
53pub(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 {
92 let ao: &FerroObject = &this.control;
93
94 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 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 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 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 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 pub fn control_property_changed(&self, e: &FerroPropertyChangedEventArgs<'_>) {
277 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});