1use ferroui_base::{StyledElementImplExt, VisualImplExt};
10use crate::diagnostics::controls::{BrushEditor, CommitTextBox};
11use crate::diagnostics::view_models::{PropertiesViewItem, PropertyViewModelRef};
12use ferroui_base::data::converters::IValueConverter;
13use ferroui_base::data::core::{ValueType, ValueTypes};
14use ferroui_base::data::{BindingError, BindingMode, BindingOperations};
15use ferroui_base::input::{
16 Cursor, InputElement, InputElementImpl, PointerPressedEventArgs, StandardCursorType,
17};
18use ferroui_base::interactivity::{Interactive, InteractiveImpl};
19use ferroui_base::layout::{HorizontalAlignment, LayoutableImpl, Orientation, VerticalAlignment};
20use ferroui_base::media::{Brushes, Color, Geometry, IBrush, IImage, Stretch};
21use ferroui_base::metadata::{MarkupType, MarkupTypeKind};
22use ferroui_base::reactive::{CompositeDisposable, IDisposable};
23use ferroui_base::utilities::{CultureInfo, Decimal, NumberFormatInfo, NumberStyles};
24use ferroui_base::{
25 ferro_class, ferro_class_info, ferro_impl_classes, instantiate, BoxedValue, FerroObjectImpl, FerroProperty,
26 FerroPropertyChangedEventArgs, Ref, StyledElementImpl, VisualImpl, VisualTreeAttachmentEventArgs,
27};
28use ferroui_controls::primitives::{FlyoutBase, SelectingItemsControl, TemplatedControlImpl, ToggleButton};
29use ferroui_controls::shapes::{Ellipse, Path, Shape};
30use ferroui_controls::{
31 Border, CheckBox, ComboBox, ContentControlImpl, Control, ControlImpl, DataValidationErrors, Flyout, Image,
32 ItemsSource, NumericUpDown, Panel, StackPanel, TextBlock, TextBox, ToolTip, UserControl,
33};
34use ferroui_controls_color_picker::{AlphaComponentPosition, ColorView};
35use ferroui_markup::data::Binding;
36use ferroui_markup_xaml::converters::ColorToBrushConverter;
37use std::any::TypeId;
38use std::cell::{Cell, OnceCell, RefCell};
39use std::rc::Rc;
40
41ferroui_base::ferro_thread_local! {
42 static IMAGE_ICON: OnceCell<Ref<Geometry>> = const { OnceCell::new() };
44 static GEOMETRY_ICON: OnceCell<Ref<Geometry>> = const { OnceCell::new() };
46 static COLOR2_BRUSH: OnceCell<Rc<ColorToBrushConverter>> = const { OnceCell::new() };
48}
49
50const IMAGE_ICON_DATA: &str = "M12.25 6C8.79822 6 6 8.79822 6 12.25V35.75C6 37.1059 6.43174 38.3609 7.16525 39.3851L21.5252 25.0251C22.8921 23.6583 25.1081 23.6583 26.475 25.0251L40.8348 39.385C41.5683 38.3608 42 37.1058 42 35.75V12.25C42 8.79822 39.2018 6 35.75 6H12.25ZM34.5 17.5C34.5 19.7091 32.7091 21.5 30.5 21.5C28.2909 21.5 26.5 19.7091 26.5 17.5C26.5 15.2909 28.2909 13.5 30.5 13.5C32.7091 13.5 34.5 15.2909 34.5 17.5ZM39.0024 41.0881L24.7072 26.7929C24.3167 26.4024 23.6835 26.4024 23.293 26.7929L8.99769 41.0882C9.94516 41.6667 11.0587 42 12.25 42H35.75C36.9414 42 38.0549 41.6666 39.0024 41.0881Z";
51
52const GEOMETRY_ICON_DATA: &str = "M23.25 15.5H30.8529C29.8865 8.99258 24.2763 4 17.5 4C10.0442 4 4 10.0442 4 17.5C4 24.2763 8.99258 29.8865 15.5 30.8529V23.25C15.5 18.9698 18.9698 15.5 23.25 15.5ZM23.25 18C20.3505 18 18 20.3505 18 23.25V38.75C18 41.6495 20.3505 44 23.25 44H38.75C41.6495 44 44 41.6495 44 38.75V23.25C44 20.3505 41.6495 18 38.75 18H23.25Z";
53
54fn image_icon() -> Ref<Geometry> {
55 IMAGE_ICON.with(|icon| {
56 icon.get_or_init(|| Geometry::parse(IMAGE_ICON_DATA).expect("the image icon is valid path data")).clone()
57 })
58}
59
60fn geometry_icon() -> Ref<Geometry> {
61 GEOMETRY_ICON.with(|icon| {
62 icon.get_or_init(|| Geometry::parse(GEOMETRY_ICON_DATA).expect("the geometry icon is valid path data")).clone()
63 })
64}
65
66fn color2_brush() -> Rc<ColorToBrushConverter> {
67 COLOR2_BRUSH.with(|converter| converter.get_or_init(ColorToBrushConverter::new).clone())
68}
69
70#[derive(Clone, Copy, PartialEq, Eq)]
72enum EditorKind {
73 CheckBox,
74 Number,
75 Brush,
76 Enum,
77 Text,
78}
79
80#[repr(C)]
94pub struct PropertyValueEditorView {
95 base: UserControl,
96 cleanup: CompositeDisposable,
97 editors: RefCell<Vec<(EditorKind, Ref<Control>)>>,
99 host: OnceCell<Ref<Panel>>,
101 transient: RefCell<Option<Ref<Control>>>,
104 enum_type: Cell<Option<TypeId>>,
106 bound: Cell<bool>,
109}
110
111ferro_class!(PropertyValueEditorView: UserControl);
112ferro_impl_classes!(
113 PropertyValueEditorView: FerroObjectImpl,
114 LayoutableImpl,
115 InteractiveImpl,
116 InputElementImpl,
117 ControlImpl,
118 TemplatedControlImpl,
119 ContentControlImpl
120);
121ferro_class_info!(PropertyValueEditorView { new: PropertyValueEditorView::new });
122
123impl StyledElementImpl for PropertyValueEditorView {
124 fn on_data_context_changed(this: &Self) {
125 Self::parent_on_data_context_changed(this);
126
127 this.refresh();
128 }
129}
130
131impl VisualImpl for PropertyValueEditorView {
132 fn on_attached_to_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
133 Self::parent_on_attached_to_visual_tree(this, e);
134
135 if !this.bound.get() && this.data_context().is_some() {
138 this.refresh();
139 }
140 }
141
142 fn on_detached_from_visual_tree(this: &Self, e: &VisualTreeAttachmentEventArgs) {
143 Self::parent_on_detached_from_visual_tree(this, e);
144
145 this.cleanup.clear();
146 this.bound.set(false);
147 }
148}
149
150fn implements_interface<I: 'static>(type_: ValueType) -> bool {
153 let interface_type = ValueType::of::<I>();
154 let nullable_interface_type = ValueType::of::<Option<I>>();
155 type_ == interface_type || type_ == nullable_interface_type || ValueTypes::is_assignable(type_, interface_type)
156}
157
158fn is_enum(type_: ValueType) -> bool {
160 MarkupType::find_by_handle(type_.id()).is_some_and(|markup| markup.kind == MarkupTypeKind::Enum)
161}
162
163fn is_valid_numeric(type_: ValueType) -> bool {
167 if is_enum(type_) {
168 return false;
169 }
170 type_.is::<u8>()
171 || type_.is::<i16>()
172 || type_.is::<i32>()
173 || type_.is::<i64>()
174 || type_.is::<i8>()
175 || type_.is::<f32>()
176 || type_.is::<u16>()
177 || type_.is::<u32>()
178 || type_.is::<u64>()
179 || type_.is::<Option<u8>>()
180 || type_.is::<Option<i16>>()
181 || type_.is::<Option<i32>>()
182 || type_.is::<Option<i64>>()
183 || type_.is::<Option<i8>>()
184 || type_.is::<Option<f32>>()
185 || type_.is::<Option<u16>>()
186 || type_.is::<Option<u32>>()
187 || type_.is::<Option<u64>>()
188}
189
190fn format_number(value: f64) -> String {
192 if value == f64::INFINITY {
193 "\u{221e}".to_string()
194 } else if value == f64::NEG_INFINITY {
195 "-\u{221e}".to_string()
196 } else {
197 format!("{value}")
198 }
199}
200
201impl PropertyValueEditorView {
202 pub fn construct() -> Self {
203 Self {
204 base: UserControl::construct(),
205 cleanup: CompositeDisposable::new(),
206 editors: RefCell::new(Vec::new()),
207 host: OnceCell::new(),
208 transient: RefCell::new(None),
209 enum_type: Cell::new(None),
210 bound: Cell::new(false),
211 }
212 }
213
214 fn refresh(&self) {
216 let editor = self.update_control();
217 self.bound.set(true);
218 let host = self.host();
219
220 let is_cached = |editor: &Ref<Control>| self.editors.borrow().iter().any(|(_, cached)| cached.ptr_eq(editor));
221 let transient = editor.clone().filter(|editor| !is_cached(editor));
222 let old_transient = self.transient.replace(transient.clone());
223 if let Some(old_transient) = old_transient {
224 if transient.as_ref().is_none_or(|transient| !transient.ptr_eq(&old_transient)) {
225 host.children().remove(&old_transient);
226 }
227 }
228 if let Some(transient) = &transient {
229 if !host.children().contains(transient) {
230 host.children().add(transient.clone());
231 }
232 }
233
234 for (_, cached) in self.editors.borrow().iter() {
235 cached.set_is_visible(editor.as_ref().is_some_and(|editor| editor.ptr_eq(cached)));
236 }
237 }
238
239 fn host(&self) -> Ref<Panel> {
241 self.host
242 .get_or_init(|| {
243 let host = Panel::new();
244 self.set_content(Some(Control::boxed(host.clone())));
245 host
246 })
247 .clone()
248 }
249
250 #[cfg(test)]
252 pub(crate) fn shown_editor(&self) -> Option<Ref<Control>> {
253 self.host.get()?.children().to_vec().into_iter().find(|child| child.is_visible())
254 }
255
256 fn editor<T>(&self, kind: EditorKind, build: impl FnOnce() -> Ref<T>) -> Ref<T>
258 where
259 T: ferroui_base::ObjectType + ferroui_base::Upcast<Control>,
260 {
261 let existing = self.editors.borrow().iter().find(|(k, _)| *k == kind).map(|(_, editor)| editor.clone());
262 if let Some(editor) = existing.and_then(|editor| editor.cast::<T>()) {
263 return editor;
264 }
265 let editor = build();
266 let control: Ref<Control> = editor.clone().upcast();
267 self.editors.borrow_mut().push((kind, control.clone()));
268 self.host().children().add(control);
269 editor
270 }
271
272 pub fn new() -> Ref<Self> {
273 instantiate(Self::construct())
274 }
275
276 fn property(&self) -> Option<PropertyViewModelRef> {
279 let data_context = self.data_context()?;
280 PropertyViewModelRef::from_boxed(&data_context)
281 .or_else(|| PropertiesViewItem::from_boxed(&data_context).and_then(|item| item.property()))
282 }
283
284 fn update_control(&self) -> Option<Ref<Control>> {
285 self.cleanup.clear();
286
287 let property = self.property()?;
288 let property_type = property.property_type();
289
290 if property_type.is::<bool>() || property_type.is::<Option<bool>>() {
291 let check_box = self.editor(EditorKind::CheckBox, || {
292 let check_box = CheckBox::new();
293 check_box.classes().add("editor");
294 check_box
295 });
296 self.create_control(&property, &check_box, ToggleButton::is_checked_property().as_property(), None, None);
297 return Some(check_box.upcast());
298 }
299
300 if is_valid_numeric(property_type) {
302 let n = self.editor(EditorKind::Number, || {
303 let n = NumericUpDown::new();
304 n.classes().add("editor");
305 n.set_increment(Decimal::from(1));
307 let number_format = NumberFormatInfo::new();
308 number_format.set_number_decimal_digits(0);
309 n.set_number_format(Some(Rc::new(number_format)));
310 n.set_parsing_number_style(NumberStyles::INTEGER);
311 n
312 });
313
314 self.create_control(
315 &property,
316 &n,
317 NumericUpDown::value_property().as_property(),
318 Some(Rc::new(ValueToDecimalConverter)),
319 Some(ReadonlyProperty::NumericUpDown),
320 );
321 return Some(n.upcast());
322 }
323
324 if property_type.is::<Color>() {
325 let el = Ellipse::new();
326 el.set_width(12.0);
327 el.set_height(12.0);
328 el.set_vertical_alignment(VerticalAlignment::Center);
329
330 let binding = Binding::with_path("Value")
331 .with_source(Some(property.boxed()))
332 .with_converter(color2_brush() as Rc<dyn IValueConverter>);
333 self.add_cleanup(el.bind_binding(Shape::fill_property().as_property(), &*binding));
334
335 let tbl = TextBlock::new();
336 tbl.set_vertical_alignment(VerticalAlignment::Center);
337
338 let binding = Binding::with_path("Value").with_source(Some(property.boxed()));
339 self.add_cleanup(tbl.bind_binding(TextBlock::text_property().as_property(), &*binding));
340
341 let sp = StackPanel::new();
342 sp.set_orientation(Orientation::Horizontal);
343 sp.set_spacing(2.0);
344 sp.children().add(el);
345 sp.children().add(tbl);
346 sp.set_background(Some(Brushes::transparent() as Rc<dyn IBrush>));
347 sp.set_cursor(Some(Cursor::new(StandardCursorType::Hand)));
348 sp.classes().add("editor");
349 sp.set_is_enabled(!property.is_readonly());
350
351 let cv = ColorView::new();
352 cv.set_hex_input_alpha_position(AlphaComponentPosition::Leading);
354
355 let binding = Binding::with_path_and_mode("Value", BindingMode::TwoWay)
356 .with_source(Some(property.boxed()))
357 .with_converter(color2_brush() as Rc<dyn IValueConverter>);
358 self.add_cleanup(cv.bind_binding(ColorView::color_property().as_property(), &*binding));
359
360 let flyout = Flyout::new();
361 flyout.set_content(Some(Control::boxed(cv)));
362 FlyoutBase::set_attached_flyout(&sp, flyout);
363
364 let weak = sp.downgrade();
365 sp.add_handler(InputElement::pointer_pressed_event(), move |_: &Interactive, _: &PointerPressedEventArgs| {
366 if let Some(sp) = weak.upgrade() {
367 FlyoutBase::show_attached_flyout(&sp);
368 }
369 });
370
371 return Some(sp.upcast());
372 }
373
374 if implements_interface::<Rc<dyn IBrush>>(property_type) {
375 let brush_editor = self.editor(EditorKind::Brush, || {
376 let brush_editor = BrushEditor::new();
377 brush_editor.classes().add("editor");
378 brush_editor
379 });
380 self.create_control(&property, &brush_editor, BrushEditor::brush_property().as_property(), None, None);
381 return Some(brush_editor.upcast());
382 }
383
384 let is_image = implements_interface::<Rc<dyn IImage>>(property_type);
385 let is_geometry = property_type.is::<Ref<Geometry>>() || property_type.is::<Option<Ref<Geometry>>>();
386
387 if is_image || is_geometry {
388 let tbl = TextBlock::new();
389 tbl.set_vertical_alignment(VerticalAlignment::Center);
390
391 let binding = Binding::with_path("Value")
393 .with_source(Some(property.boxed()))
394 .with_converter(Rc::new(SizeTextConverter) as Rc<dyn IValueConverter>);
395 self.add_cleanup(tbl.bind_binding(TextBlock::text_property().as_property(), &*binding));
396
397 let icon = Path::new();
398 icon.set_data(if is_image { image_icon() } else { geometry_icon() });
399 icon.classes().add("value-icon");
402 icon.set_width(12.0);
403 icon.set_height(12.0);
404 icon.set_stretch(Stretch::Uniform);
405 icon.set_vertical_alignment(VerticalAlignment::Center);
406
407 let sp = StackPanel::new();
408 sp.set_background(Some(Brushes::transparent() as Rc<dyn IBrush>));
409 sp.set_orientation(Orientation::Horizontal);
410 sp.set_spacing(2.0);
411 sp.children().add(icon);
412 sp.children().add(tbl);
413 sp.classes().add("editor");
414
415 if is_image {
416 let preview_image = Image::new();
417 preview_image.set_stretch(Stretch::Uniform);
418 preview_image.set_width(300.0);
419 preview_image.set_height(300.0);
420
421 let binding = Binding::with_path("Value").with_source(Some(property.boxed()));
422 self.add_cleanup(preview_image.bind_binding(Image::source_property().as_property(), &*binding));
423
424 ToolTip::set_tip(&sp, Some(Control::boxed(preview_image)));
425 } else {
426 let preview_shape = Path::new();
427 preview_shape.set_stretch(Stretch::Uniform);
428 preview_shape.classes().add("preview-geometry");
432 preview_shape.set_vertical_alignment(VerticalAlignment::Center);
433 preview_shape.set_horizontal_alignment(HorizontalAlignment::Center);
434
435 let binding = Binding::with_path("Value").with_source(Some(property.boxed()));
436 self.add_cleanup(preview_shape.bind_binding(Path::data_property().as_property(), &*binding));
437
438 let border = Border::new();
439 border.set_child(preview_shape);
440 border.set_width(300.0);
441 border.set_height(300.0);
442 ToolTip::set_tip(&sp, Some(Control::boxed(border)));
443 }
444
445 return Some(sp.upcast());
446 }
447
448 if is_enum(property_type) {
449 let c = self.editor(EditorKind::Enum, || {
450 let c = ComboBox::new();
451 c.classes().add("editor");
452 c
453 });
454 if self.enum_type.get() != Some(property_type.id()) {
456 self.enum_type.set(Some(property_type.id()));
457 let members =
458 MarkupType::find_by_handle(property_type.id()).map(|markup| markup.enum_members).unwrap_or(&[]);
459 c.set_items_source(Some(ItemsSource::from_items(members.iter().map(|member| Some((member.get)())))));
460 }
461 self.create_control(
462 &property,
463 &c,
464 SelectingItemsControl::selected_item_property().as_property(),
465 None,
466 None,
467 );
468 return Some(c.upcast());
469 }
470
471 let tb = self.editor(EditorKind::Text, || {
472 let tb = CommitTextBox::new();
473 tb.classes().add("editor");
476 tb.set_placeholder_text(Some("(null)"));
478 tb
479 });
480 self.create_control(
481 &property,
482 &tb,
483 CommitTextBox::committed_text_property().as_property(),
484 Some(Rc::new(TextToValueConverter)),
485 Some(ReadonlyProperty::TextBox),
486 );
487
488 let is_read_only =
489 tb.is_read_only() || property_type.is_object() || !StringConversionHelper::can_convert_from_string(property_type);
490 tb.set_is_read_only(is_read_only);
491 {
493 let text_box: &TextBox = &tb;
494 DataValidationErrors::clear_errors(text_box);
495 }
496
497 if !tb.is_read_only() {
498 let validate = move |tb: &TextBox, t: Option<String>| {
501 let result = match t {
502 Some(t) => StringConversionHelper::from_string(&t, property_type).map(|_| ()),
503 None => Ok(()),
504 };
505 match result {
506 Ok(()) => DataValidationErrors::clear_errors(tb),
507 Err(error) => DataValidationErrors::set_error(tb, Some(&error)),
508 }
509 };
510 let text_box: &TextBox = &tb;
511 validate(text_box, tb.text());
512 let weak = tb.downgrade();
513 self.cleanup.add(tb.property_changed(move |e: &FerroPropertyChangedEventArgs<'_>| {
514 if e.property() == TextBox::text_property().as_property() {
515 if let Some(tb) = weak.upgrade() {
516 let text_box: &TextBox = &tb;
517 validate(text_box, tb.text());
518 }
519 }
520 }));
521 }
522
523 Some(tb.upcast())
524 }
525
526 fn add_cleanup(&self, expression: Rc<dyn ferroui_base::data::BindingExpressionBase>) {
527 self.cleanup.add(expression as Rc<dyn IDisposable>);
528 }
529
530 fn create_control(
535 &self,
536 property: &PropertyViewModelRef,
537 control: &Control,
538 value_property: &'static FerroProperty,
539 converter: Option<Rc<dyn IValueConverter>>,
540 readonly_property: Option<ReadonlyProperty>,
541 ) {
542 let binding_mode = if property.is_readonly() { BindingMode::OneWay } else { BindingMode::TwoWay };
543
544 control.clear_value_untyped(value_property);
547
548 let binding = Binding::with_path_and_mode("Value", binding_mode)
549 .with_source(Some(property.boxed()))
550 .with_converter(converter.unwrap_or_else(|| Rc::new(ValueConverter) as Rc<dyn IValueConverter>))
551 .with_converter_parameter(Some(Rc::new(property.property_type()) as BoxedValue));
552 self.add_cleanup(control.bind_binding(value_property, &*binding));
553
554 match readonly_property {
555 Some(ReadonlyProperty::NumericUpDown) => {
556 control.set_value(NumericUpDown::is_read_only_property(), property.is_readonly())
557 }
558 Some(ReadonlyProperty::TextBox) => control.set_value(TextBox::is_read_only_property(), property.is_readonly()),
559 None => control.set_is_enabled(!property.is_readonly()),
560 }
561 }
562}
563
564#[derive(Clone, Copy)]
566enum ReadonlyProperty {
567 NumericUpDown,
568 TextBox,
569}
570
571fn parameter_type(parameter: Option<&BoxedValue>, target_type: ValueType) -> ValueType {
574 parameter.and_then(|parameter| parameter.downcast_ref::<ValueType>().copied()).unwrap_or(target_type)
575}
576
577struct ValueConverter;
579
580impl IValueConverter for ValueConverter {
581 fn convert(
582 &self,
583 value: Option<&BoxedValue>,
584 _target_type: ValueType,
585 _parameter: Option<&BoxedValue>,
586 _culture: &CultureInfo,
587 ) -> Result<Option<BoxedValue>, BindingError> {
588 Ok(value.cloned())
589 }
590
591 fn convert_back(
592 &self,
593 value: Option<&BoxedValue>,
594 _target_type: ValueType,
595 _parameter: Option<&BoxedValue>,
596 _culture: &CultureInfo,
597 ) -> Result<Option<BoxedValue>, BindingError> {
598 Ok(value.cloned())
599 }
600}
601
602struct ValueToDecimalConverter;
605
606impl ValueToDecimalConverter {
607 fn to_decimal(value: &BoxedValue) -> Option<Decimal> {
609 let value = ValueTypes::normalize(value.clone())?;
610 macro_rules! integers {
611 ($($ty:ty),*) => {
612 $(
613 if let Some(v) = value.downcast_ref::<$ty>() {
614 return Some(Decimal::from(*v));
615 }
616 )*
617 };
618 }
619 integers!(u8, i8, i16, u16, i32, u32, i64, u64);
620 if let Some(v) = value.downcast_ref::<f32>() {
621 return Decimal::try_from_f32(*v);
622 }
623 if let Some(v) = value.downcast_ref::<f64>() {
624 return Decimal::try_from_f64(*v);
625 }
626 value.downcast_ref::<Decimal>().copied()
627 }
628
629 fn change_type(value: Decimal, target_type: ValueType) -> Option<BoxedValue> {
631 let target_type = ValueTypes::nullable_inner(target_type).unwrap_or(target_type);
632 let integer = value.round_to_integer();
633 macro_rules! integers {
634 ($($ty:ty),*) => {
635 $(
636 if target_type.is::<$ty>() {
637 return <$ty>::try_from(integer).ok().map(|v| Rc::new(v) as BoxedValue);
638 }
639 )*
640 };
641 }
642 integers!(u8, i8, i16, u16, i32, u32, i64, u64);
643 if target_type.is::<f32>() {
644 return Some(Rc::new(value.to_f64() as f32));
645 }
646 if target_type.is::<f64>() {
647 return Some(Rc::new(value.to_f64()));
648 }
649 None
650 }
651}
652
653impl IValueConverter for ValueToDecimalConverter {
654 fn convert(
655 &self,
656 value: Option<&BoxedValue>,
657 _target_type: ValueType,
658 _parameter: Option<&BoxedValue>,
659 _culture: &CultureInfo,
660 ) -> Result<Option<BoxedValue>, BindingError> {
661 let decimal = match value {
663 Some(value) => Self::to_decimal(value)
664 .ok_or_else(|| BindingError::message("The value cannot be converted to a decimal."))?,
665 None => Decimal::from(0),
666 };
667 Ok(Some(Rc::new(decimal)))
668 }
669
670 fn convert_back(
671 &self,
672 value: Option<&BoxedValue>,
673 target_type: ValueType,
674 parameter: Option<&BoxedValue>,
675 _culture: &CultureInfo,
676 ) -> Result<Option<BoxedValue>, BindingError> {
677 let target_type = parameter_type(parameter, target_type);
678 let Some(decimal) = value.and_then(Self::to_decimal) else {
679 return Ok(None);
680 };
681 Self::change_type(decimal, target_type)
682 .map(Some)
683 .ok_or_else(|| BindingError::message("The value cannot be converted to the type of the property."))
684 }
685}
686
687struct TextToValueConverter;
690
691impl IValueConverter for TextToValueConverter {
692 fn convert(
693 &self,
694 value: Option<&BoxedValue>,
695 _target_type: ValueType,
696 _parameter: Option<&BoxedValue>,
697 _culture: &CultureInfo,
698 ) -> Result<Option<BoxedValue>, BindingError> {
699 Ok(value.map(|value| Rc::new(StringConversionHelper::to_string(value)) as BoxedValue))
700 }
701
702 fn convert_back(
703 &self,
704 value: Option<&BoxedValue>,
705 target_type: ValueType,
706 parameter: Option<&BoxedValue>,
707 _culture: &CultureInfo,
708 ) -> Result<Option<BoxedValue>, BindingError> {
709 let target_type = parameter_type(parameter, target_type);
710 let Some(s) = value.and_then(|value| value.downcast_ref::<String>()) else {
711 return Ok(None);
712 };
713
714 match StringConversionHelper::from_string(s, target_type) {
715 Ok(value) => Ok(value),
716 Err(_) => Ok(Some(BindingOperations::do_nothing())),
717 }
718 }
719}
720
721struct SizeTextConverter;
724
725impl IValueConverter for SizeTextConverter {
726 fn convert(
727 &self,
728 value: Option<&BoxedValue>,
729 _target_type: ValueType,
730 _parameter: Option<&BoxedValue>,
731 _culture: &CultureInfo,
732 ) -> Result<Option<BoxedValue>, BindingError> {
733 let value = value.and_then(|value| ValueTypes::normalize(value.clone()));
734 let text = match value {
735 Some(value) => {
736 if let Some(img) = value.downcast_ref::<Rc<dyn IImage>>() {
737 let size = img.size();
738 format!("{} x {}", format_number(size.width), format_number(size.height))
739 } else if let Some(geom) = value.downcast_ref::<Ref<Geometry>>() {
740 let bounds = geom.bounds();
741 format!("{} x {}", format_number(bounds.width), format_number(bounds.height))
742 } else {
743 "(null)".to_string()
744 }
745 }
746 None => "(null)".to_string(),
747 };
748 Ok(Some(Rc::new(text)))
749 }
750
751 fn convert_back(
752 &self,
753 _value: Option<&BoxedValue>,
754 _target_type: ValueType,
755 _parameter: Option<&BoxedValue>,
756 _culture: &CultureInfo,
757 ) -> Result<Option<BoxedValue>, BindingError> {
758 Ok(Some(BindingOperations::do_nothing()))
759 }
760}
761
762struct StringConversionHelper;
767
768impl StringConversionHelper {
769 fn can_convert_from_string(type_: ValueType) -> bool {
771 let type_ = ValueTypes::nullable_inner(type_).unwrap_or(type_);
772 if type_.is_string() || type_.is::<Option<String>>() {
773 return true;
774 }
775 if type_.is::<bool>()
776 || type_.is::<char>()
777 || type_.is::<u8>()
778 || type_.is::<i8>()
779 || type_.is::<i16>()
780 || type_.is::<u16>()
781 || type_.is::<i32>()
782 || type_.is::<u32>()
783 || type_.is::<i64>()
784 || type_.is::<u64>()
785 || type_.is::<f32>()
786 || type_.is::<f64>()
787 || type_.is::<Decimal>()
788 {
789 return true;
790 }
791
792 if let Some(class) = ValueTypes::class_of(type_) {
795 let mut current = Some(class);
796 while let Some(class) = current {
797 if class.markup().is_some_and(|markup| markup.parse.is_some()) {
798 return true;
799 }
800 current = class.base_type();
801 }
802 return false;
803 }
804
805 MarkupType::find_by_handle(type_.id())
806 .or_else(|| MarkupType::find_by_nullable_handle(type_.id()))
807 .is_some_and(|markup| markup.parse.is_some() || markup.kind == MarkupTypeKind::Enum)
808 }
809
810 fn to_string(o: &BoxedValue) -> String {
812 ValueTypes::to_display_string(Some(o))
813 }
814
815 fn from_string(s: &str, type_: ValueType) -> Result<Option<BoxedValue>, BindingError> {
817 let text: BoxedValue = Rc::new(s.to_string());
818
819 if let Some(value) = ValueTypes::try_convert(Some(&text), type_) {
820 return Ok(value);
821 }
822 if let Some(value) = ValueTypes::try_parse_text(&text, type_) {
823 return Ok(Some(value));
824 }
825 if let Some(inner) = ValueTypes::nullable_inner(type_) {
826 if let Some(value) = ValueTypes::try_parse_text(&text, inner) {
827 return Ok(ValueTypes::try_convert(Some(&value), type_).flatten().or(Some(value)));
828 }
829 }
830
831 Err(BindingError::message(format!("'{s}' is not a valid value of {}.", type_.name())))
832 }
833}