Skip to main content

ferroui_diagnostics/diagnostics/views/
property_value_editor_view.rs

1//! Port of `Diagnostics/Views/PropertyValueEditorView.cs`.
2//!
3//! The editor of the value of a property in the property grid: a check box, a number box, a
4//! color, a brush, an image or geometry preview, a list of the members of an enumeration, or a
5//! text box, by the type of the property. The managed original asks reflection what a type is
6//! (`IsEnum`, `TypeCode`, implemented interfaces, type converters and `Parse` methods); the port
7//! asks the value types and the markup metadata of the framework the same questions.
8
9use 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    /// `ImageIcon`: parsed on first use, on the UI thread.
43    static IMAGE_ICON: OnceCell<Ref<Geometry>> = const { OnceCell::new() };
44    /// `GeometryIcon`: parsed on first use, on the UI thread.
45    static GEOMETRY_ICON: OnceCell<Ref<Geometry>> = const { OnceCell::new() };
46    /// `Color2Brush`.
47    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/// The kinds of editors the view keeps once built (see [`PropertyValueEditorView`]).
71#[derive(Clone, Copy, PartialEq, Eq)]
72enum EditorKind {
73    CheckBox,
74    Number,
75    Brush,
76    Enum,
77    Text,
78}
79
80/// The editor of the value of a property: its content is the control that edits a value of the
81/// type of the property (its data context), bound to the value of the property.
82///
83/// Deviation (DEVIATIONS.md, Developer tools): the managed original builds a new editor at every
84/// change of the data context. The property grid is a TreeDataGrid, whose rows and cells are
85/// recycled while it scrolls and when another control is selected, and the cells keep their
86/// editor view and give it the next row as its data context; building a text box, a number box or
87/// a combo box (each with its template) for every row made scrolling and selecting slow. The view
88/// keeps the editor of each kind it has built (check box, number box, brush, enumeration, text
89/// box) in a panel that is its content, shows the one of the property and binds it to the
90/// property, as the TreeDataGrid keeps its recycled cells parented and hidden: an editor is built,
91/// attached and styled once, and a recycled row only changes which editor is visible and what it
92/// is bound to. The editors look and behave as the original's.
93#[repr(C)]
94pub struct PropertyValueEditorView {
95    base: UserControl,
96    cleanup: CompositeDisposable,
97    /// The editors built so far, by kind; at most one of each, all children of `host`.
98    editors: RefCell<Vec<(EditorKind, Ref<Control>)>>,
99    /// The content: the panel of the editors.
100    host: OnceCell<Ref<Panel>>,
101    /// The editor built for the current property only (the color, image and geometry editors,
102    /// which few properties use), a child of `host` while it is shown.
103    transient: RefCell<Option<Ref<Control>>>,
104    /// The enumeration whose members the combo box of the enumeration editor lists.
105    enum_type: Cell<Option<TypeId>>,
106    /// Whether the content is bound to the property of the data context (it is not after the
107    /// view left the visual tree, which disposes the bindings).
108    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        // Back in the tree with the same data context: bind the editor again (leaving the tree
136        // disposed its bindings).
137        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
150/// `ImplementsInterface<TInterface>(type)`: the type is the contract (by its handle, or its
151/// nullable handle), or a type assignable to it.
152fn 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
158/// `type.IsEnum`: an enumeration with markup metadata.
159fn is_enum(type_: ValueType) -> bool {
160    MarkupType::find_by_handle(type_.id()).is_some_and(|markup| markup.kind == MarkupTypeKind::Enum)
161}
162
163/// `IsValidNumeric`: an integral number or a single-precision number (`TypeCode` `Byte` to
164/// `UInt64` and `Single`), or the nullable form of one. Doubles and decimals are not edited as
165/// numbers (the managed original notes that infinity and NaN do not work with `NumericUpDown`).
166fn 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
190/// A number as the managed original formats it into text.
191fn 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    /// Binds the content to the property of the data context.
215    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    /// The panel of the editors, the content of the view.
240    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    /// The editor that is shown (the tests' view of the content).
251    #[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    /// The editor of `kind`, built by `build` the first time.
257    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    /// `Property => (PropertyViewModel?)DataContext`: the property of the data context, which is
277    /// a property view model or the row of the property grid of one.
278    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        // TODO (from the original): Infinity, NaN not working with NumericUpDown.
301        if is_valid_numeric(property_type) {
302            let n = self.editor(EditorKind::Number, || {
303                let n = NumericUpDown::new();
304                n.classes().add("editor");
305                // `init`.
306                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            // Always match XAML.
353            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            // `valueObservable.Select(..)`: the text of the size of the image or geometry.
392            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            // Deviation (DEVIATIONS.md, Developer tools): the colour of the icon comes from the
400            // theme (the class `value-icon` of DevToolsStyles.xaml) in place of Gray.
401            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                // Deviation (DEVIATIONS.md, Developer tools): the colour of the preview comes from
429                // the theme (the class `preview-geometry` of DevToolsStyles.xaml) in place of White,
430                // which did not show on the light background of a tool tip.
431                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            // `init`: `Enum.GetValues(propertyType)`, listed again only for another enumeration.
455            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            // Deviation (DEVIATIONS.md, Developer tools): every editor has the class `editor`,
474            // which the sheet of the tools (DevToolsStyles.xaml) gives one compact form.
475            tb.classes().add("editor");
476            // `init`.
477            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        // The text box may have shown another property with an invalid text.
492        {
493            let text_box: &TextBox = &tb;
494            DataValidationErrors::clear_errors(text_box);
495        }
496
497        if !tb.is_read_only() {
498            // `tb.GetObservable(TextBox.TextProperty).Subscribe(..)`: the current text, then every
499            // change of it.
500            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    /// `CreateControl<TControl>`: binds `value_property` of `control` to the value of the
531    /// property (two-way unless the property is read-only), and makes the control read-only or
532    /// disabled for a read-only property. The control is created, and initialized, by the
533    /// caller.
534    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        // A kept editor starts from the state of a new one: the value it showed for the previous
545        // property (its binding is disposed) is cleared before it is bound to this one.
546        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/// The read-only property of a control `CreateControl` sets (`readonlyProperty`).
565#[derive(Clone, Copy)]
566enum ReadonlyProperty {
567    NumericUpDown,
568    TextBox,
569}
570
571/// The type a converter converts back to: the type of the property, the parameter of the
572/// binding (the target type the binding gives is that of the value of the view model).
573fn 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
577/// `ValueConverter`: passes the values through.
578struct 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
602/// `ValueToDecimalConverter`: a number as a decimal, and a decimal back to the type of the
603/// property.
604struct ValueToDecimalConverter;
605
606impl ValueToDecimalConverter {
607    /// `System.Convert.ToDecimal(value)`.
608    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    /// `System.Convert.ChangeType(value, targetType)`: a decimal as a number of `target_type`.
630    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        // `Convert.ToDecimal(null)` is zero.
662        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
687/// `TextToValueConverter`: a value as its text, and text back to a value of the type of the
688/// property (nothing for text that does not convert).
689struct 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
721/// The text of the size of an image or a geometry (`img.Size` / `geom.Bounds`), `(null)` for
722/// anything else.
723struct 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
762/// `StringConversionHelper`: the conversion of values to and from text. The managed original
763/// asks the type converter of a type (`TypeDescriptor.GetConverter`) and its `Parse` methods;
764/// the port asks the value types and the conversion from text the markup metadata of a type
765/// states (`parse:`, the counterpart of both).
766struct StringConversionHelper;
767
768impl StringConversionHelper {
769    /// Whether text converts to a value of `type_`.
770    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        // The conversion of the nearest class that states one (a converter attribute is
793        // inherited).
794        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    /// The text of a value (`ConvertToInvariantString`, or `ToString()`).
811    fn to_string(o: &BoxedValue) -> String {
812        ValueTypes::to_display_string(Some(o))
813    }
814
815    /// The value of `type_` the text `s` converts to; an error for text that does not convert.
816    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}