Skip to main content

ferroui_controls/numeric_up_down/
numeric_up_down.rs

1use super::NumericUpDownValueChangedEventArgs;
2use crate::primitives::{TemplateAppliedEventArgs, TemplatedControl, TemplatedControlImpl};
3use crate::{
4    ButtonSpinner, ContentControl, ControlImpl, Location, SpinDirection, SpinEventArgs, Spinner, TextBox,
5    ValidSpinDirections,
6};
7use ferroui_base::data::converters::IValueConverter;
8use ferroui_base::data::core::{ValueType, ValueTypes};
9use ferroui_base::data::BindingMode;
10use ferroui_base::input::{
11    FocusChangedEventArgs, InputElement, InputElementImpl, InputElementImplExt, Key, KeyEventArgs, KeyboardNavigation,
12    KeyboardNavigationMode, PointerPressedEventArgs,
13};
14use ferroui_base::interactivity::{
15    Interactive, InteractiveImpl, RoutedEvent, RoutedEventHandlerToken, RoutingStrategies,
16};
17use ferroui_base::layout::{HorizontalAlignment, LayoutableImpl, VerticalAlignment};
18use ferroui_base::media::text_formatting::unicode::{Codepoint, GeneralCategory};
19use ferroui_base::media::{IBrush, TextAlignment};
20use ferroui_base::reactive::{Disposable, IDisposable, ObservableExt};
21use ferroui_base::threading::{Dispatcher, DispatcherPriority};
22use ferroui_base::utilities::number_format::format_decimal_composite;
23use ferroui_base::utilities::{CultureInfo, Decimal, HandlerList, NumberFormatInfo, NumberParseError, NumberStyles};
24use ferroui_base::{
25    ferro_class, ferro_class_info, ferro_impl_classes, ferro_properties, ferro_routed_event, instantiate,
26    AttachedProperty, BoxedValue, FerroObject, FerroObjectExtensions, FerroObjectImpl, FerroObjectImplExt,
27    FerroProperty, Ref, StyledElementImpl, StyledProperty, StyledPropertyOptions, VisualImpl,
28};
29use std::cell::{Cell, RefCell};
30use std::rc::Rc;
31
32/// Resets a flag when dropped.
33struct Reset<'a>(&'a Cell<bool>);
34
35impl Drop for Reset<'_> {
36    fn drop(&mut self) {
37        self.0.set(false);
38    }
39}
40
41/// The failure of a conversion between the text and the value: what the
42/// reference reports by throwing.
43struct ConversionError(String);
44
45impl ConversionError {
46    fn format() -> Self {
47        ConversionError("Input string was not in a correct format.".to_string())
48    }
49}
50
51/// A template part and the handler attached to one of its events.
52type Part<T> = (Ref<T>, RoutedEventHandlerToken);
53
54/// Control that represents a text box with button spinners that allow
55/// incrementing and decrementing numeric values.
56#[repr(C)]
57pub struct NumericUpDown {
58    base: TemplatedControl,
59    text_box_text_changed_subscription: RefCell<Option<Rc<dyn IDisposable>>>,
60    internal_value_set: Cell<bool>,
61    is_syncing_text_and_value_properties: Cell<bool>,
62    is_text_changed_from_ui: Cell<bool>,
63    is_focused: Cell<bool>,
64    // The spinner template part and its spin handler.
65    spinner: RefCell<Option<Part<Spinner>>>,
66    // The text box template part and its pointer pressed handler.
67    text_box: RefCell<Option<Part<TextBox>>>,
68    spinned: HandlerList<dyn Fn(&SpinEventArgs)>,
69}
70
71ferro_class! {
72    NumericUpDown: TemplatedControl, virtuals NumericUpDownImpl: TemplatedControlImpl {
73        /// Called when the `NumberFormat` property value changed.
74        fn on_number_format_changed(
75            this,
76            old_value: Option<Rc<NumberFormatInfo>>,
77            new_value: Option<Rc<NumberFormatInfo>>
78        );
79        /// Called when the `FormatString` property value changed.
80        fn on_format_string_changed(this, old_value: &str, new_value: &str);
81        /// Called when the `Increment` property value changed.
82        fn on_increment_changed(this, old_value: Decimal, new_value: Decimal);
83        /// Called when the `IsReadOnly` property value changed.
84        fn on_is_read_only_changed(this, old_value: bool, new_value: bool);
85        /// Called when the `Maximum` property value changed.
86        fn on_maximum_changed(this, old_value: Decimal, new_value: Decimal);
87        /// Called when the `Minimum` property value changed.
88        fn on_minimum_changed(this, old_value: Decimal, new_value: Decimal);
89        /// Called when the `Text` property value changed.
90        fn on_text_changed(this, old_value: Option<&str>, new_value: Option<&str>);
91        /// Called when the `TextConverter` property value changed.
92        fn on_text_converter_changed(
93            this,
94            old_value: Option<Rc<dyn IValueConverter>>,
95            new_value: Option<Rc<dyn IValueConverter>>
96        );
97        /// Called when the `Value` property value changed.
98        fn on_value_changed(this, old_value: Option<Decimal>, new_value: Option<Decimal>);
99        /// Called when the `Increment` property has to be coerced.
100        fn on_coerce_increment(this, base_value: Decimal) -> Decimal;
101        /// Called when the `Maximum` property has to be coerced.
102        fn on_coerce_maximum(this, base_value: Decimal) -> Decimal;
103        /// Called when the `Minimum` property has to be coerced.
104        fn on_coerce_minimum(this, base_value: Decimal) -> Decimal;
105        /// Called when the `Value` property has to be coerced.
106        fn on_coerce_value(this, base_value: Option<Decimal>) -> Option<Decimal>;
107        /// Raises the `Spinned` event when spinning is initiated by the
108        /// end-user.
109        fn on_spin(this, e: &SpinEventArgs);
110        /// Raises the `ValueChanged` event.
111        fn raise_value_changed_event(this, old_value: Option<Decimal>, new_value: Option<Decimal>);
112    }
113}
114
115ferro_class_info!(NumericUpDown {
116    new: NumericUpDown::new,
117    markup: {
118        attributes: [
119            TemplatePart("PART_Spinner", type(Ref<Spinner>)),
120            TemplatePart("PART_TextBox", type(Ref<TextBox>), IsRequired = true),
121        ],
122    },
123});
124
125ferro_impl_classes!(NumericUpDown: StyledElementImpl, VisualImpl, LayoutableImpl, InteractiveImpl);
126
127impl ControlImpl for NumericUpDown {
128    fn on_create_automation_peer(this: &Self) -> Ref<crate::automation::peers::AutomationPeer> {
129        crate::automation::peers::NumericUpDownAutomationPeer::new(this).upcast()
130    }
131}
132
133impl FerroObjectImpl for NumericUpDown {
134    fn constructed(this: &Self) {
135        Self::parent_constructed(this);
136
137        let weak = this.to_ref().downgrade();
138        this.initialized(move || {
139            if let Some(this) = weak.upgrade() {
140                if !this.internal_value_set.get() && this.is_initialized() {
141                    this.sync_text_and_value_properties(false, None, true);
142                }
143
144                this.set_valid_spin_direction();
145            }
146        });
147    }
148}
149
150impl InputElementImpl for NumericUpDown {
151    fn on_got_focus(this: &Self, e: &FocusChangedEventArgs) {
152        Self::parent_on_got_focus(this, e);
153        this.focus_changed(this.is_keyboard_focus_within());
154    }
155
156    fn on_lost_focus(this: &Self, e: &FocusChangedEventArgs) {
157        this.commit_input(true);
158        Self::parent_on_lost_focus(this, e);
159        this.focus_changed(this.is_keyboard_focus_within());
160    }
161
162    fn on_key_down(this: &Self, e: &KeyEventArgs) {
163        if e.key == Key::Enter {
164            let commit_success = this.commit_input(false);
165            e.set_handled(!commit_success);
166        }
167    }
168}
169
170impl TemplatedControlImpl for NumericUpDown {
171    fn on_apply_template(this: &Self, e: &TemplateAppliedEventArgs) {
172        if let Some((text_box, token)) = this.text_box.take() {
173            text_box.remove_handler(InputElement::pointer_pressed_event(), token);
174            if let Some(subscription) = this.text_box_text_changed_subscription.take() {
175                subscription.dispose();
176            }
177        }
178
179        if let Some(text_box) = e.name_scope().find_as::<TextBox>("PART_TextBox") {
180            text_box.set_text(this.text().as_deref());
181
182            let tab_index = InputElement::tab_index_property().as_property();
183            text_box.bind_indexer(&tab_index.bind(), &this.indexer(&tab_index.bind()));
184
185            let weak = this.to_ref().downgrade();
186            let token = text_box.add_handler(InputElement::pointer_pressed_event(), move |_, e| {
187                if let Some(this) = weak.upgrade() {
188                    this.text_box_on_pointer_pressed(e);
189                }
190            });
191            *this.text_box.borrow_mut() = Some((text_box.clone(), token));
192
193            let weak = this.to_ref().downgrade();
194            let object: &FerroObject = &text_box;
195            let subscription = FerroObjectExtensions::get_observable(object, TextBox::text_property())
196                .subscribe_fn(move |_| {
197                    if let Some(this) = weak.upgrade() {
198                        this.text_box_on_text_changed();
199                    }
200                });
201            *this.text_box_text_changed_subscription.borrow_mut() = Some(subscription);
202        }
203
204        if let Some((spinner, token)) = this.spinner.take() {
205            spinner.remove_handler(Spinner::spin_event(), token);
206        }
207
208        if let Some(spinner) = e.name_scope().find_as::<Spinner>("PART_Spinner") {
209            let weak = this.to_ref().downgrade();
210            let token = spinner.spin(move |_, e| {
211                if let Some(this) = weak.upgrade() {
212                    this.on_spinner_spin(e);
213                }
214            });
215            *this.spinner.borrow_mut() = Some((spinner, token));
216        }
217
218        this.set_valid_spin_direction();
219    }
220}
221
222impl NumericUpDownImpl for NumericUpDown {
223    fn on_number_format_changed(
224        this: &Self,
225        _old_value: Option<Rc<NumberFormatInfo>>,
226        _new_value: Option<Rc<NumberFormatInfo>>,
227    ) {
228        if this.is_initialized() {
229            this.sync_text_and_value_properties(false, None, true);
230        }
231    }
232
233    fn on_format_string_changed(this: &Self, _old_value: &str, _new_value: &str) {
234        if this.is_initialized() {
235            this.sync_text_and_value_properties(false, None, true);
236        }
237    }
238
239    fn on_increment_changed(this: &Self, _old_value: Decimal, _new_value: Decimal) {
240        if this.is_initialized() {
241            this.set_valid_spin_direction();
242        }
243    }
244
245    fn on_is_read_only_changed(this: &Self, _old_value: bool, _new_value: bool) {
246        this.set_valid_spin_direction();
247    }
248
249    fn on_maximum_changed(this: &Self, _old_value: Decimal, _new_value: Decimal) {
250        if this.is_initialized() {
251            this.set_valid_spin_direction();
252        }
253        if this.clip_value_to_min_max() {
254            if let Some(value) = this.value() {
255                this.set_current_value(Self::value_property(), Some(clamp(value, this.minimum(), this.maximum())));
256            }
257        }
258    }
259
260    fn on_minimum_changed(this: &Self, _old_value: Decimal, _new_value: Decimal) {
261        if this.is_initialized() {
262            this.set_valid_spin_direction();
263        }
264        if this.clip_value_to_min_max() {
265            if let Some(value) = this.value() {
266                this.set_current_value(Self::value_property(), Some(clamp(value, this.minimum(), this.maximum())));
267            }
268        }
269    }
270
271    fn on_text_changed(this: &Self, _old_value: Option<&str>, _new_value: Option<&str>) {
272        if this.is_initialized() {
273            this.sync_text_and_value_properties(true, this.text().as_deref(), false);
274        }
275    }
276
277    fn on_text_converter_changed(
278        this: &Self,
279        _old_value: Option<Rc<dyn IValueConverter>>,
280        _new_value: Option<Rc<dyn IValueConverter>>,
281    ) {
282        if this.is_initialized() {
283            this.sync_text_and_value_properties(false, None, true);
284        }
285    }
286
287    fn on_value_changed(this: &Self, old_value: Option<Decimal>, new_value: Option<Decimal>) {
288        if !this.internal_value_set.get() && this.is_initialized() {
289            this.sync_text_and_value_properties(false, None, true);
290        }
291
292        this.set_valid_spin_direction();
293
294        this.raise_value_changed_event(old_value, new_value);
295    }
296
297    fn on_coerce_increment(_this: &Self, base_value: Decimal) -> Decimal {
298        base_value
299    }
300
301    fn on_coerce_maximum(this: &Self, base_value: Decimal) -> Decimal {
302        base_value.max(this.minimum())
303    }
304
305    fn on_coerce_minimum(this: &Self, base_value: Decimal) -> Decimal {
306        base_value.min(this.maximum())
307    }
308
309    fn on_coerce_value(_this: &Self, base_value: Option<Decimal>) -> Option<Decimal> {
310        base_value
311    }
312
313    fn on_spin(this: &Self, e: &SpinEventArgs) {
314        if e.direction() == SpinDirection::Increase {
315            this.do_increment();
316        } else {
317            this.do_decrement();
318        }
319
320        for (_, handler) in this.spinned.snapshot().iter() {
321            handler(e);
322        }
323    }
324
325    fn raise_value_changed_event(this: &Self, old_value: Option<Decimal>, new_value: Option<Decimal>) {
326        let e = NumericUpDownValueChangedEventArgs::new(Some(Self::value_changed_event()), old_value, new_value);
327        this.raise_event(&e);
328    }
329}
330
331ferro_properties! {
332    impl NumericUpDown {
333        /// Defines the `AllowSpin` property.
334        pub fn allow_spin_property() -> StyledProperty<bool> {
335            ButtonSpinner::allow_spin_property().add_owner::<NumericUpDown>()
336        }
337
338        /// Defines the `ButtonSpinnerLocation` property.
339        pub fn button_spinner_location_property() -> StyledProperty<Location> {
340            ButtonSpinner::button_spinner_location_property().add_owner::<NumericUpDown>()
341        }
342
343        /// Defines the `ShowButtonSpinner` property.
344        pub fn show_button_spinner_property() -> StyledProperty<bool> {
345            ButtonSpinner::show_button_spinner_property().add_owner::<NumericUpDown>()
346        }
347
348        /// Defines the `ClipValueToMinMax` property.
349        pub fn clip_value_to_min_max_property() -> StyledProperty<bool> {
350            FerroProperty::register::<NumericUpDown, _>("ClipValueToMinMax", false)
351        }
352
353        /// Defines the `NumberFormat` property.
354        pub fn number_format_property() -> StyledProperty<Option<Rc<NumberFormatInfo>>> {
355            FerroProperty::register::<NumericUpDown, _>("NumberFormat", Some(NumberFormatInfo::current_info()))
356        }
357
358        /// Defines the `FormatString` property.
359        pub fn format_string_property() -> StyledProperty<String> {
360            FerroProperty::register::<NumericUpDown, _>("FormatString", String::new())
361        }
362
363        /// Defines the `Increment` property.
364        pub fn increment_property() -> StyledProperty<Decimal> {
365            FerroProperty::register_with::<NumericUpDown, _>(
366                "Increment",
367                StyledPropertyOptions::new(Decimal::ONE).coerce(|instance, value| {
368                    match instance.downcast_ref::<NumericUpDown>() {
369                        Some(up_down) => up_down.on_coerce_increment(value),
370                        None => value,
371                    }
372                }),
373            )
374        }
375
376        /// Defines the `IsReadOnly` property.
377        pub fn is_read_only_property() -> StyledProperty<bool> {
378            FerroProperty::register::<NumericUpDown, _>("IsReadOnly", false)
379        }
380
381        /// Defines the `Maximum` property.
382        pub fn maximum_property() -> StyledProperty<Decimal> {
383            FerroProperty::register_with::<NumericUpDown, _>(
384                "Maximum",
385                StyledPropertyOptions::new(Decimal::MAX_VALUE).coerce(|instance, value| {
386                    match instance.downcast_ref::<NumericUpDown>() {
387                        Some(up_down) => up_down.on_coerce_maximum(value),
388                        None => value,
389                    }
390                }),
391            )
392        }
393
394        /// Defines the `Minimum` property.
395        pub fn minimum_property() -> StyledProperty<Decimal> {
396            FerroProperty::register_with::<NumericUpDown, _>(
397                "Minimum",
398                StyledPropertyOptions::new(Decimal::MIN_VALUE).coerce(|instance, value| {
399                    match instance.downcast_ref::<NumericUpDown>() {
400                        Some(up_down) => up_down.on_coerce_minimum(value),
401                        None => value,
402                    }
403                }),
404            )
405        }
406
407        /// Defines the `ParsingNumberStyle` property.
408        pub fn parsing_number_style_property() -> StyledProperty<NumberStyles> {
409            FerroProperty::register::<NumericUpDown, _>("ParsingNumberStyle", NumberStyles::ANY)
410        }
411
412        /// Defines the `Text` property.
413        pub fn text_property() -> StyledProperty<Option<String>> {
414            FerroProperty::register_with::<NumericUpDown, _>(
415                "Text",
416                StyledPropertyOptions::new(None)
417                    .default_binding_mode(BindingMode::TwoWay)
418                    .enable_data_validation(true),
419            )
420        }
421
422        /// Defines the `TextConverter` property.
423        pub fn text_converter_property() -> StyledProperty<Option<Rc<dyn IValueConverter>>> {
424            FerroProperty::register_with::<NumericUpDown, _>(
425                "TextConverter",
426                StyledPropertyOptions::new(None).default_binding_mode(BindingMode::OneWay),
427            )
428        }
429
430        /// Defines the `Value` property.
431        pub fn value_property() -> StyledProperty<Option<Decimal>> {
432            FerroProperty::register_with::<NumericUpDown, _>(
433                "Value",
434                StyledPropertyOptions::new(None)
435                    .coerce(|instance, value| {
436                        instance
437                            .downcast_ref::<NumericUpDown>()
438                            .expect("The owner of the Value property is a NumericUpDown.")
439                            .on_coerce_value(value)
440                    })
441                    .default_binding_mode(BindingMode::TwoWay)
442                    .enable_data_validation(true),
443            )
444        }
445
446        /// Defines the `PlaceholderText` property.
447        pub fn placeholder_text_property() -> StyledProperty<Option<String>> {
448            TextBox::placeholder_text_property().add_owner::<NumericUpDown>()
449        }
450
451        /// Defines the `PlaceholderForeground` property.
452        pub fn placeholder_foreground_property() -> StyledProperty<Option<Rc<dyn IBrush>>> {
453            TextBox::placeholder_foreground_property().add_owner::<NumericUpDown>()
454        }
455
456        /// Defines the `HorizontalContentAlignment` property.
457        pub fn horizontal_content_alignment_property() -> StyledProperty<HorizontalAlignment> {
458            ContentControl::horizontal_content_alignment_property().add_owner::<NumericUpDown>()
459        }
460
461        /// Defines the `VerticalContentAlignment` property.
462        pub fn vertical_content_alignment_property() -> StyledProperty<VerticalAlignment> {
463            ContentControl::vertical_content_alignment_property().add_owner::<NumericUpDown>()
464        }
465
466        /// Defines the `TextAlignment` property.
467        pub fn text_alignment_property() -> AttachedProperty<TextAlignment> {
468            TextBox::text_alignment_property().add_owner::<NumericUpDown>()
469        }
470
471        /// Defines the `InnerLeftContent` property.
472        pub fn inner_left_content_property() -> StyledProperty<Option<BoxedValue>> {
473            TextBox::inner_left_content_property().add_owner::<NumericUpDown>()
474        }
475
476        /// Defines the `InnerRightContent` property.
477        pub fn inner_right_content_property() -> StyledProperty<Option<BoxedValue>> {
478            TextBox::inner_right_content_property().add_owner::<NumericUpDown>()
479        }
480    }
481}
482
483impl NumericUpDown {
484    /// Defines the `Watermark` property.
485    #[deprecated(note = "Use placeholder_text_property instead.")]
486    pub fn watermark_property() -> &'static StyledProperty<Option<String>> {
487        Self::placeholder_text_property()
488    }
489
490    /// Defines the `WatermarkForeground` property.
491    #[deprecated(note = "Use placeholder_foreground_property instead.")]
492    pub fn watermark_foreground_property() -> &'static StyledProperty<Option<Rc<dyn IBrush>>> {
493        Self::placeholder_foreground_property()
494    }
495
496    ferro_routed_event!(
497        /// Defines the `ValueChanged` event.
498        pub fn value_changed_event() -> RoutedEvent<NumericUpDownValueChangedEventArgs> {
499            RoutedEvent::register::<NumericUpDown, _>("ValueChanged", RoutingStrategies::BUBBLE)
500        }
501    );
502
503    /// Initializes the static members of the class.
504    fn static_constructor() {
505        Self::number_format_property().changed().add_class_handler::<NumericUpDown>(|up_down, e| {
506            let (old_value, new_value) = e.get_old_and_new_value::<Option<Rc<NumberFormatInfo>>>();
507            up_down.on_number_format_changed(old_value, new_value);
508        });
509        Self::format_string_property().changed().add_class_handler::<NumericUpDown>(|up_down, e| {
510            let (old_value, new_value) = e.get_old_and_new_value::<String>();
511            up_down.on_format_string_changed(&old_value, &new_value);
512        });
513        Self::increment_property().changed().add_class_handler::<NumericUpDown>(|up_down, e| {
514            let (old_value, new_value) = e.get_old_and_new_value::<Decimal>();
515            up_down.on_increment_changed(old_value, new_value);
516        });
517        Self::is_read_only_property().changed().add_class_handler::<NumericUpDown>(|up_down, e| {
518            let (old_value, new_value) = e.get_old_and_new_value::<bool>();
519            up_down.on_is_read_only_changed(old_value, new_value);
520        });
521        Self::maximum_property().changed().add_class_handler::<NumericUpDown>(|up_down, e| {
522            let (old_value, new_value) = e.get_old_and_new_value::<Decimal>();
523            up_down.on_maximum_changed(old_value, new_value);
524        });
525        Self::minimum_property().changed().add_class_handler::<NumericUpDown>(|up_down, e| {
526            let (old_value, new_value) = e.get_old_and_new_value::<Decimal>();
527            up_down.on_minimum_changed(old_value, new_value);
528        });
529        Self::text_property().changed().add_class_handler::<NumericUpDown>(|up_down, e| {
530            let (old_value, new_value) = e.get_old_and_new_value::<Option<String>>();
531            up_down.on_text_changed(old_value.as_deref(), new_value.as_deref());
532        });
533        Self::text_converter_property().changed().add_class_handler::<NumericUpDown>(|up_down, e| {
534            let (old_value, new_value) = e.get_old_and_new_value::<Option<Rc<dyn IValueConverter>>>();
535            up_down.on_text_converter_changed(old_value, new_value);
536        });
537        Self::value_property().changed().add_class_handler::<NumericUpDown>(|up_down, e| {
538            let (old_value, new_value) = e.get_old_and_new_value::<Option<Decimal>>();
539            up_down.on_value_changed(old_value, new_value);
540        });
541
542        InputElement::focusable_property().override_default_value::<NumericUpDown>(true);
543        InputElement::is_tab_stop_property().override_default_value::<NumericUpDown>(false);
544        KeyboardNavigation::tab_navigation_property()
545            .override_default_value::<NumericUpDown>(KeyboardNavigationMode::Local);
546    }
547
548    /// Creates the class data; see [`ferroui_base::FerroObject::construct`].
549    pub fn construct() -> Self {
550        Self {
551            base: TemplatedControl::construct(),
552            text_box_text_changed_subscription: RefCell::new(None),
553            internal_value_set: Cell::new(false),
554            is_syncing_text_and_value_properties: Cell::new(false),
555            is_text_changed_from_ui: Cell::new(false),
556            is_focused: Cell::new(false),
557            spinner: RefCell::new(None),
558            text_box: RefCell::new(None),
559            spinned: HandlerList::new(),
560        }
561    }
562
563    /// Creates a numeric up-down control.
564    pub fn new() -> Ref<Self> {
565        instantiate(Self::construct())
566    }
567
568    /// The spinner template part.
569    fn spinner(&self) -> Option<Ref<Spinner>> {
570        self.spinner.borrow().as_ref().map(|(spinner, _)| spinner.clone())
571    }
572
573    /// The text box template part.
574    fn text_box(&self) -> Option<Ref<TextBox>> {
575        self.text_box.borrow().as_ref().map(|(text_box, _)| text_box.clone())
576    }
577
578    /// Whether increment/decrement operations via the keyboard, the button
579    /// spinners or the mouse wheel are possible.
580    pub fn allow_spin(&self) -> bool {
581        self.get_value(Self::allow_spin_property())
582    }
583
584    pub fn set_allow_spin(&self, value: bool) {
585        self.set_value(Self::allow_spin_property(), value)
586    }
587
588    /// The current location of the button spinner.
589    pub fn button_spinner_location(&self) -> Location {
590        self.get_value(Self::button_spinner_location_property())
591    }
592
593    pub fn set_button_spinner_location(&self, value: Location) {
594        self.set_value(Self::button_spinner_location_property(), value)
595    }
596
597    /// Whether the spin buttons should be shown.
598    pub fn show_button_spinner(&self) -> bool {
599        self.get_value(Self::show_button_spinner_property())
600    }
601
602    pub fn set_show_button_spinner(&self, value: bool) {
603        self.set_value(Self::show_button_spinner_property(), value)
604    }
605
606    /// Whether the value should be clipped when the minimum/maximum is
607    /// reached.
608    pub fn clip_value_to_min_max(&self) -> bool {
609        self.get_value(Self::clip_value_to_min_max_property())
610    }
611
612    pub fn set_clip_value_to_min_max(&self, value: bool) {
613        self.set_value(Self::clip_value_to_min_max_property(), value)
614    }
615
616    /// The current number format information.
617    pub fn number_format(&self) -> Option<Rc<NumberFormatInfo>> {
618        self.get_value(Self::number_format_property())
619    }
620
621    pub fn set_number_format(&self, value: Option<Rc<NumberFormatInfo>>) {
622        self.set_value(Self::number_format_property(), value)
623    }
624
625    /// The display format of the `Value`.
626    pub fn format_string(&self) -> String {
627        self.get_value(Self::format_string_property())
628    }
629
630    pub fn set_format_string(&self, value: &str) {
631        self.set_value(Self::format_string_property(), value.to_string())
632    }
633
634    /// The amount in which to increment the `Value`.
635    pub fn increment(&self) -> Decimal {
636        self.get_value(Self::increment_property())
637    }
638
639    pub fn set_increment(&self, value: Decimal) {
640        self.set_value(Self::increment_property(), value)
641    }
642
643    /// Whether the control is read only.
644    pub fn is_read_only(&self) -> bool {
645        self.get_value(Self::is_read_only_property())
646    }
647
648    pub fn set_is_read_only(&self, value: bool) {
649        self.set_value(Self::is_read_only_property(), value)
650    }
651
652    /// The maximum allowed value.
653    pub fn maximum(&self) -> Decimal {
654        self.get_value(Self::maximum_property())
655    }
656
657    pub fn set_maximum(&self, value: Decimal) {
658        self.set_value(Self::maximum_property(), value)
659    }
660
661    /// The minimum allowed value.
662    pub fn minimum(&self) -> Decimal {
663        self.get_value(Self::minimum_property())
664    }
665
666    pub fn set_minimum(&self, value: Decimal) {
667        self.set_value(Self::minimum_property(), value)
668    }
669
670    /// The parsing style (`ALLOW_LEADING_WHITE`, `FLOAT`, ...). By default,
671    /// `ANY`. Note that the hexadecimal styles do not work with decimals:
672    /// for a hexadecimal display, use the `TextConverter`.
673    pub fn parsing_number_style(&self) -> NumberStyles {
674        self.get_value(Self::parsing_number_style_property())
675    }
676
677    pub fn set_parsing_number_style(&self, value: NumberStyles) {
678        self.set_value(Self::parsing_number_style_property(), value)
679    }
680
681    /// The formatted string representation of the value.
682    pub fn text(&self) -> Option<String> {
683        self.get_value(Self::text_property())
684    }
685
686    pub fn set_text(&self, value: Option<&str>) {
687        self.set_value(Self::text_property(), value.map(str::to_string))
688    }
689
690    /// The custom bidirectional text-value converter. A converter overrides
691    /// the `ParsingNumberStyle`, providing finer control over the string
692    /// representation of the underlying value.
693    pub fn text_converter(&self) -> Option<Rc<dyn IValueConverter>> {
694        self.get_value(Self::text_converter_property())
695    }
696
697    pub fn set_text_converter(&self, value: Option<Rc<dyn IValueConverter>>) {
698        self.set_value(Self::text_converter_property(), value)
699    }
700
701    /// The value.
702    pub fn value(&self) -> Option<Decimal> {
703        self.get_value(Self::value_property())
704    }
705
706    pub fn set_numeric_value(&self, value: Option<Decimal>) {
707        self.set_value(Self::value_property(), value)
708    }
709
710    /// The text to use as a placeholder if the `Value` is null.
711    pub fn placeholder_text(&self) -> Option<String> {
712        self.get_value(Self::placeholder_text_property())
713    }
714
715    pub fn set_placeholder_text(&self, value: Option<&str>) {
716        self.set_value(Self::placeholder_text_property(), value.map(str::to_string))
717    }
718
719    /// The text to use as a placeholder if the `Value` is null.
720    #[deprecated(note = "Use placeholder_text instead.")]
721    pub fn watermark(&self) -> Option<String> {
722        self.placeholder_text()
723    }
724
725    #[deprecated(note = "Use set_placeholder_text instead.")]
726    pub fn set_watermark(&self, value: Option<&str>) {
727        self.set_placeholder_text(value)
728    }
729
730    /// The brush used for the foreground color of the placeholder text.
731    pub fn placeholder_foreground(&self) -> Option<Rc<dyn IBrush>> {
732        self.get_value(Self::placeholder_foreground_property())
733    }
734
735    pub fn set_placeholder_foreground(&self, value: Option<Rc<dyn IBrush>>) {
736        self.set_value(Self::placeholder_foreground_property(), value)
737    }
738
739    /// The brush used for the foreground color of the placeholder text.
740    #[deprecated(note = "Use placeholder_foreground instead.")]
741    pub fn watermark_foreground(&self) -> Option<Rc<dyn IBrush>> {
742        self.placeholder_foreground()
743    }
744
745    #[deprecated(note = "Use set_placeholder_foreground instead.")]
746    pub fn set_watermark_foreground(&self, value: Option<Rc<dyn IBrush>>) {
747        self.set_placeholder_foreground(value)
748    }
749
750    /// The horizontal alignment of the content within the control.
751    pub fn horizontal_content_alignment(&self) -> HorizontalAlignment {
752        self.get_value(Self::horizontal_content_alignment_property())
753    }
754
755    pub fn set_horizontal_content_alignment(&self, value: HorizontalAlignment) {
756        self.set_value(Self::horizontal_content_alignment_property(), value)
757    }
758
759    /// The vertical alignment of the content within the control.
760    pub fn vertical_content_alignment(&self) -> VerticalAlignment {
761        self.get_value(Self::vertical_content_alignment_property())
762    }
763
764    pub fn set_vertical_content_alignment(&self, value: VerticalAlignment) {
765        self.set_value(Self::vertical_content_alignment_property(), value)
766    }
767
768    /// The text alignment of the control.
769    pub fn text_alignment(&self) -> TextAlignment {
770        self.get_value(Self::text_alignment_property())
771    }
772
773    pub fn set_text_alignment(&self, value: TextAlignment) {
774        self.set_value(Self::text_alignment_property(), value)
775    }
776
777    /// Custom content that is positioned on the left side of the text
778    /// layout box.
779    pub fn inner_left_content(&self) -> Option<BoxedValue> {
780        self.get_value(Self::inner_left_content_property())
781    }
782
783    pub fn set_inner_left_content(&self, value: Option<BoxedValue>) {
784        self.set_value(Self::inner_left_content_property(), value)
785    }
786
787    /// Custom content that is positioned on the right side of the text
788    /// layout box.
789    pub fn inner_right_content(&self) -> Option<BoxedValue> {
790        self.get_value(Self::inner_right_content_property())
791    }
792
793    pub fn set_inner_right_content(&self, value: Option<BoxedValue>) {
794        self.set_value(Self::inner_right_content_property(), value)
795    }
796
797    /// Occurs when spinning is initiated by the end-user.
798    pub fn spinned(&self, handler: impl Fn(&SpinEventArgs) + 'static) -> Rc<dyn IDisposable> {
799        let token = self.spinned.add(Rc::new(handler));
800        let weak = self.to_ref().downgrade();
801        Disposable::create(move || {
802            if let Some(this) = weak.upgrade() {
803                this.spinned.remove(token);
804            }
805        })
806    }
807
808    /// Raised when the `Value` changes.
809    pub fn value_changed(
810        &self,
811        handler: impl Fn(&Interactive, &NumericUpDownValueChangedEventArgs) + 'static,
812    ) -> RoutedEventHandlerToken {
813        self.add_handler(Self::value_changed_event(), handler)
814    }
815
816    /// Converts the formatted text to a value.
817    fn convert_text_to_value(&self, text: Option<&str>) -> Result<Option<Decimal>, ConversionError> {
818        let Some(text) = text.filter(|text| !text.is_empty()) else {
819            return Ok(None);
820        };
821
822        // Since the conversion from the value to text using a format string
823        // may not be parsable, we verify that the already existing text is
824        // not the exact same value.
825        let current_value_text = self.convert_value_to_text()?;
826        if current_value_text.as_deref() == Some(text) {
827            return Ok(self.value());
828        }
829
830        let result = self.convert_text_to_value_core(current_value_text.as_deref(), text)?;
831
832        if self.clip_value_to_min_max() {
833            if let Some(result) = result {
834                return Ok(Some(clamp(result, self.minimum(), self.maximum())));
835            }
836        }
837
838        self.validate_min_max(result)?;
839
840        Ok(result)
841    }
842
843    /// Converts the value to formatted text.
844    fn convert_value_to_text(&self) -> Result<Option<String>, ConversionError> {
845        let value = self.value();
846
847        if let Some(converter) = self.text_converter() {
848            let boxed = value.map(|value| Rc::new(value) as BoxedValue);
849            let text = converter
850                .convert_back(boxed.as_ref(), ValueType::of::<String>(), None, &CultureInfo::current_culture())
851                .map_err(|error| ConversionError(error.to_string()))?;
852            return Ok(text.map(|text| ValueTypes::to_display_string(Some(&text))));
853        }
854
855        let number_format = NumberFormatInfo::get_instance(self.number_format().as_ref());
856        let format_string = self.format_string();
857
858        // Manage a format string of type "{0:N2} °".
859        if format_string.contains("{0") {
860            return format_decimal_composite(&format_string, &number_format, value)
861                .map(Some)
862                .map_err(|error| ConversionError(error.to_string()));
863        }
864
865        match value {
866            Some(value) => value
867                .to_string_with(&format_string, Some(&number_format))
868                .map(Some)
869                .map_err(|error| ConversionError(error.to_string())),
870            None => Ok(None),
871        }
872    }
873
874    /// Called by `on_spin` when the spin direction is
875    /// `SpinDirection::Increase`.
876    fn on_increment(&self) {
877        let result = match self.value() {
878            Some(value) => value.checked_add(self.increment()).unwrap_or(saturated(self.increment())),
879            // If the minimum is set we set the value to the minimum on
880            // increment, otherwise to 0. It will be clamped to be between
881            // the minimum and the maximum later, so we don't need to do it
882            // here.
883            None if self.is_set(Self::minimum_property().as_property()) => self.minimum(),
884            None => Decimal::ZERO,
885        };
886
887        self.set_current_value(Self::value_property(), Some(clamp(result, self.minimum(), self.maximum())));
888    }
889
890    /// Called by `on_spin` when the spin direction is
891    /// `SpinDirection::Decrease`.
892    fn on_decrement(&self) {
893        let result = match self.value() {
894            Some(value) => value.checked_sub(self.increment()).unwrap_or(saturated(-self.increment())),
895            // If the maximum is set we set the value to the maximum on
896            // decrement, otherwise to 0. It will be clamped to be between
897            // the minimum and the maximum later, so we don't need to do it
898            // here.
899            None if self.is_set(Self::maximum_property().as_property()) => self.maximum(),
900            None => Decimal::ZERO,
901        };
902
903        self.set_current_value(Self::value_property(), Some(clamp(result, self.minimum(), self.maximum())));
904    }
905
906    /// Sets the valid spin directions.
907    fn set_valid_spin_direction(&self) {
908        let mut valid_directions = ValidSpinDirections::NONE;
909
910        // Zero increment always prevents spin.
911        if !self.increment().is_zero() && !self.is_read_only() {
912            let value = self.value();
913            if value.is_none() {
914                valid_directions = ValidSpinDirections::INCREASE | ValidSpinDirections::DECREASE;
915            }
916
917            if value.is_some_and(|value| value < self.maximum()) {
918                valid_directions |= ValidSpinDirections::INCREASE;
919            }
920
921            if value.is_some_and(|value| value > self.minimum()) {
922                valid_directions |= ValidSpinDirections::DECREASE;
923            }
924        }
925
926        if let Some(spinner) = self.spinner() {
927            spinner.set_valid_spin_direction(valid_directions);
928        }
929    }
930
931    fn set_value_internal(&self, value: Option<Decimal>) {
932        self.internal_value_set.set(true);
933        let _reset = Reset(&self.internal_value_set);
934        self.set_current_value(Self::value_property(), value);
935    }
936
937    fn text_box_on_text_changed(&self) {
938        self.is_text_changed_from_ui.set(true);
939        let _reset = Reset(&self.is_text_changed_from_ui);
940        if let Some(text_box) = self.text_box() {
941            self.set_current_value(Self::text_property(), text_box.text());
942        }
943    }
944
945    fn on_spinner_spin(&self, e: &SpinEventArgs) {
946        if self.allow_spin() && !self.is_read_only() {
947            let mut spin = !e.using_mouse_wheel();
948            spin |= self.text_box().is_some_and(|text_box| text_box.is_focused());
949
950            if spin {
951                e.set_handled(true);
952                self.on_spin(e);
953            }
954        }
955    }
956
957    fn do_decrement(&self) {
958        if self.spinner().is_none_or(|spinner| spinner.valid_spin_direction().contains(ValidSpinDirections::DECREASE)) {
959            self.on_decrement();
960        }
961    }
962
963    fn do_increment(&self) {
964        if self.spinner().is_none_or(|spinner| spinner.valid_spin_direction().contains(ValidSpinDirections::INCREASE)) {
965            self.on_increment();
966        }
967    }
968
969    fn text_box_on_pointer_pressed(&self, e: &PointerPressedEventArgs) {
970        let captured = e.pointer().captured();
971        let is_spinner = match (&captured, self.spinner()) {
972            (Some(captured), Some(spinner)) => captured.ptr_eq(&spinner),
973            (None, None) => true,
974            _ => false,
975        };
976        if !is_spinner {
977            let pointer = e.pointer().clone();
978            let this = self.to_ref();
979            Dispatcher::ui_thread().invoke_async_local_with_priority(
980                move || {
981                    let spinner = this.spinner().map(|spinner| spinner.upcast::<InputElement>());
982                    pointer.capture(spinner.as_ref());
983                },
984                DispatcherPriority::INPUT,
985            );
986        }
987    }
988
989    fn commit_input(&self, force_text_update: bool) -> bool {
990        self.sync_text_and_value_properties(true, self.text().as_deref(), force_text_update)
991    }
992
993    /// Synchronizes the `Text` and `Value` properties: optionally updates
994    /// the value from the text, and optionally forces the text to be the
995    /// text of the value. Returns whether the text was a valid value.
996    fn sync_text_and_value_properties(
997        &self,
998        update_value_from_text: bool,
999        text: Option<&str>,
1000        force_text_update: bool,
1001    ) -> bool {
1002        if self.is_syncing_text_and_value_properties.get() {
1003            return true;
1004        }
1005
1006        self.is_syncing_text_and_value_properties.set(true);
1007        let _reset = Reset(&self.is_syncing_text_and_value_properties);
1008        let mut parsed_text_is_valid = true;
1009
1010        if update_value_from_text {
1011            match self.convert_text_to_value(text) {
1012                Ok(new_value) => {
1013                    if new_value != self.value() {
1014                        self.set_value_internal(new_value);
1015                    }
1016                }
1017                Err(_) => parsed_text_is_valid = false,
1018            }
1019        }
1020
1021        // Do not touch the ongoing text input from the user.
1022        if !self.is_text_changed_from_ui.get() {
1023            if force_text_update {
1024                // A format string that is not valid is an error of the
1025                // developer: the reference throws here.
1026                let new_text = self.convert_value_to_text().unwrap_or_else(|error| panic!("{}", error.0));
1027                if self.text() != new_text {
1028                    self.set_current_value(Self::text_property(), new_text);
1029                }
1030            }
1031
1032            // Sync the text and the text box.
1033            if let Some(text_box) = self.text_box() {
1034                text_box.set_text(self.text().as_deref());
1035            }
1036        }
1037
1038        if self.is_text_changed_from_ui.get() && !parsed_text_is_valid {
1039            // The text input was made by the user and the text represents
1040            // an invalid value. Disable the spinner in this case.
1041            if let Some(spinner) = self.spinner() {
1042                spinner.set_valid_spin_direction(ValidSpinDirections::NONE);
1043            }
1044        } else {
1045            self.set_valid_spin_direction();
1046        }
1047
1048        parsed_text_is_valid
1049    }
1050
1051    fn convert_text_to_value_core(
1052        &self,
1053        current_value_text: Option<&str>,
1054        text: &str,
1055    ) -> Result<Option<Decimal>, ConversionError> {
1056        if text.is_empty() {
1057            return Ok(None);
1058        }
1059
1060        if let Some(converter) = self.text_converter() {
1061            let boxed: BoxedValue = Rc::new(text.to_string());
1062            let value_from_text = converter
1063                .convert(Some(&boxed), ValueType::of::<Option<Decimal>>(), None, &CultureInfo::current_culture())
1064                .map_err(|error| ConversionError(error.to_string()))?;
1065            return match value_from_text {
1066                None => Ok(None),
1067                Some(value) => {
1068                    if let Some(value) = value.downcast_ref::<Decimal>() {
1069                        Ok(Some(*value))
1070                    } else if let Some(value) = value.downcast_ref::<Option<Decimal>>() {
1071                        Ok(*value)
1072                    } else {
1073                        Err(ConversionError("Specified cast is not valid.".to_string()))
1074                    }
1075                }
1076            };
1077        }
1078
1079        let number_format = self.number_format();
1080        let number_format = number_format.as_deref();
1081
1082        if self.is_percent(&self.format_string()) {
1083            return parse_percent(text, number_format).map(Some);
1084        }
1085
1086        let style = self.parsing_number_style();
1087        let parse = |text: &str| match Decimal::parse_with(text, style, number_format) {
1088            Ok(value) => Ok(Some(value)),
1089            // The styles are not valid for decimals: the reference throws.
1090            Err(NumberParseError::InvalidStyle) => Err(ConversionError(NumberParseError::InvalidStyle.to_string())),
1091            Err(_) => Ok(None),
1092        };
1093
1094        if let Some(value) = parse(text)? {
1095            return Ok(Some(value));
1096        }
1097
1098        // There was a problem while converting the new text. Check if the
1099        // current value text is also failing: it then also contains special
1100        // characters, e.g. "90°".
1101        if let Some(current_value_text) = current_value_text.filter(|text| !text.is_empty()) {
1102            if parse(current_value_text)?.is_none() {
1103                // The same non-digit characters are in the current value
1104                // text and in the new text: remove them from the new text
1105                // to parse it again.
1106                let same_special_characters = current_value_text
1107                    .chars()
1108                    .filter(|c| !is_digit(*c))
1109                    .all(|c| text.chars().any(|other| other == c));
1110                if same_special_characters {
1111                    let digits: String = text.chars().filter(|c| is_digit(*c)).collect();
1112                    // If without the special characters the parsing is
1113                    // good, do not fail.
1114                    if let Some(value) = parse(&digits)? {
1115                        return Ok(Some(value));
1116                    }
1117                }
1118            }
1119        }
1120
1121        Err(ConversionError::format())
1122    }
1123
1124    fn validate_min_max(&self, value: Option<Decimal>) -> Result<(), ConversionError> {
1125        let Some(value) = value else {
1126            return Ok(());
1127        };
1128
1129        let minimum = self.minimum();
1130        let maximum = self.maximum();
1131        if value < minimum {
1132            Err(ConversionError(format!("Value must be greater than Minimum value of {minimum}")))
1133        } else if value > maximum {
1134            Err(ConversionError(format!("Value must be less than Maximum value of {maximum}")))
1135        } else {
1136            Ok(())
1137        }
1138    }
1139
1140    fn is_percent(&self, string_to_test: &str) -> bool {
1141        if let Some(p_index) = string_to_test.find('P') {
1142            // A "P" between two "'" is considered as text, not percent.
1143            let is_text = string_to_test[..p_index].contains('\'') && string_to_test[p_index..].contains('\'');
1144
1145            return !is_text;
1146        }
1147        false
1148    }
1149
1150    fn focus_changed(&self, has_focus: bool) {
1151        // The got focus and lost focus notifications are asynchronous and
1152        // cannot reliably tell that the control has the focus: all they do
1153        // is let it know that the focus changed sometime in the past.
1154
1155        let was_focused = self.is_focused.replace(has_focus);
1156
1157        if has_focus && !was_focused {
1158            if let Some(text_box) = self.text_box() {
1159                text_box.focus();
1160                text_box.select_all();
1161            }
1162        }
1163    }
1164}
1165
1166/// Whether the character is a decimal digit of any script (the general
1167/// category Nd), as the reference tests the characters of the text. Outside
1168/// the basic plane the reference sees surrogates, which are not digits.
1169fn is_digit(c: char) -> bool {
1170    if c.is_ascii() {
1171        return c.is_ascii_digit();
1172    }
1173
1174    let value = c as u32;
1175    value <= 0xFFFF && Codepoint::new(value).general_category() == GeneralCategory::DecimalNumber
1176}
1177
1178/// The value restricted to the range of `min` to `max` (the decimal form of
1179/// the reference's clamp utility).
1180fn clamp(value: Decimal, min: Decimal, max: Decimal) -> Decimal {
1181    assert!(min <= max, "{min} cannot be greater than {max}.");
1182
1183    if value < min {
1184        min
1185    } else if value > max {
1186        max
1187    } else {
1188        value
1189    }
1190}
1191
1192/// The end of the value range in the direction of `step`: the result of a
1193/// spin that would leave the range of decimals.
1194fn saturated(step: Decimal) -> Decimal {
1195    if step.is_sign_negative() {
1196        Decimal::MIN_VALUE
1197    } else {
1198        Decimal::MAX_VALUE
1199    }
1200}
1201
1202/// Parses percent format text.
1203fn parse_percent(text: &str, number_format: Option<&NumberFormatInfo>) -> Result<Decimal, ConversionError> {
1204    let current;
1205    let info = match number_format {
1206        Some(info) => info,
1207        None => {
1208            current = CultureInfo::current_culture().number_format();
1209            &*current
1210        }
1211    };
1212
1213    if info.percent_symbol().is_empty() {
1214        return Err(ConversionError("String cannot be of zero length.".to_string()));
1215    }
1216    let text = text.replace(info.percent_symbol(), "");
1217    let result = Decimal::parse_with(&text, NumberStyles::ANY, Some(info))
1218        .map_err(|error| ConversionError(error.to_string()))?;
1219    Ok(result / Decimal::from(100))
1220}