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
32struct Reset<'a>(&'a Cell<bool>);
34
35impl Drop for Reset<'_> {
36 fn drop(&mut self) {
37 self.0.set(false);
38 }
39}
40
41struct ConversionError(String);
44
45impl ConversionError {
46 fn format() -> Self {
47 ConversionError("Input string was not in a correct format.".to_string())
48 }
49}
50
51type Part<T> = (Ref<T>, RoutedEventHandlerToken);
53
54#[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 spinner: RefCell<Option<Part<Spinner>>>,
66 text_box: RefCell<Option<Part<TextBox>>>,
68 spinned: HandlerList<dyn Fn(&SpinEventArgs)>,
69}
70
71ferro_class! {
72 NumericUpDown: TemplatedControl, virtuals NumericUpDownImpl: TemplatedControlImpl {
73 fn on_number_format_changed(
75 this,
76 old_value: Option<Rc<NumberFormatInfo>>,
77 new_value: Option<Rc<NumberFormatInfo>>
78 );
79 fn on_format_string_changed(this, old_value: &str, new_value: &str);
81 fn on_increment_changed(this, old_value: Decimal, new_value: Decimal);
83 fn on_is_read_only_changed(this, old_value: bool, new_value: bool);
85 fn on_maximum_changed(this, old_value: Decimal, new_value: Decimal);
87 fn on_minimum_changed(this, old_value: Decimal, new_value: Decimal);
89 fn on_text_changed(this, old_value: Option<&str>, new_value: Option<&str>);
91 fn on_text_converter_changed(
93 this,
94 old_value: Option<Rc<dyn IValueConverter>>,
95 new_value: Option<Rc<dyn IValueConverter>>
96 );
97 fn on_value_changed(this, old_value: Option<Decimal>, new_value: Option<Decimal>);
99 fn on_coerce_increment(this, base_value: Decimal) -> Decimal;
101 fn on_coerce_maximum(this, base_value: Decimal) -> Decimal;
103 fn on_coerce_minimum(this, base_value: Decimal) -> Decimal;
105 fn on_coerce_value(this, base_value: Option<Decimal>) -> Option<Decimal>;
107 fn on_spin(this, e: &SpinEventArgs);
110 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 pub fn allow_spin_property() -> StyledProperty<bool> {
335 ButtonSpinner::allow_spin_property().add_owner::<NumericUpDown>()
336 }
337
338 pub fn button_spinner_location_property() -> StyledProperty<Location> {
340 ButtonSpinner::button_spinner_location_property().add_owner::<NumericUpDown>()
341 }
342
343 pub fn show_button_spinner_property() -> StyledProperty<bool> {
345 ButtonSpinner::show_button_spinner_property().add_owner::<NumericUpDown>()
346 }
347
348 pub fn clip_value_to_min_max_property() -> StyledProperty<bool> {
350 FerroProperty::register::<NumericUpDown, _>("ClipValueToMinMax", false)
351 }
352
353 pub fn number_format_property() -> StyledProperty<Option<Rc<NumberFormatInfo>>> {
355 FerroProperty::register::<NumericUpDown, _>("NumberFormat", Some(NumberFormatInfo::current_info()))
356 }
357
358 pub fn format_string_property() -> StyledProperty<String> {
360 FerroProperty::register::<NumericUpDown, _>("FormatString", String::new())
361 }
362
363 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 pub fn is_read_only_property() -> StyledProperty<bool> {
378 FerroProperty::register::<NumericUpDown, _>("IsReadOnly", false)
379 }
380
381 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 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 pub fn parsing_number_style_property() -> StyledProperty<NumberStyles> {
409 FerroProperty::register::<NumericUpDown, _>("ParsingNumberStyle", NumberStyles::ANY)
410 }
411
412 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 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 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 pub fn placeholder_text_property() -> StyledProperty<Option<String>> {
448 TextBox::placeholder_text_property().add_owner::<NumericUpDown>()
449 }
450
451 pub fn placeholder_foreground_property() -> StyledProperty<Option<Rc<dyn IBrush>>> {
453 TextBox::placeholder_foreground_property().add_owner::<NumericUpDown>()
454 }
455
456 pub fn horizontal_content_alignment_property() -> StyledProperty<HorizontalAlignment> {
458 ContentControl::horizontal_content_alignment_property().add_owner::<NumericUpDown>()
459 }
460
461 pub fn vertical_content_alignment_property() -> StyledProperty<VerticalAlignment> {
463 ContentControl::vertical_content_alignment_property().add_owner::<NumericUpDown>()
464 }
465
466 pub fn text_alignment_property() -> AttachedProperty<TextAlignment> {
468 TextBox::text_alignment_property().add_owner::<NumericUpDown>()
469 }
470
471 pub fn inner_left_content_property() -> StyledProperty<Option<BoxedValue>> {
473 TextBox::inner_left_content_property().add_owner::<NumericUpDown>()
474 }
475
476 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 #[deprecated(note = "Use placeholder_text_property instead.")]
486 pub fn watermark_property() -> &'static StyledProperty<Option<String>> {
487 Self::placeholder_text_property()
488 }
489
490 #[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 pub fn value_changed_event() -> RoutedEvent<NumericUpDownValueChangedEventArgs> {
499 RoutedEvent::register::<NumericUpDown, _>("ValueChanged", RoutingStrategies::BUBBLE)
500 }
501 );
502
503 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 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 pub fn new() -> Ref<Self> {
565 instantiate(Self::construct())
566 }
567
568 fn spinner(&self) -> Option<Ref<Spinner>> {
570 self.spinner.borrow().as_ref().map(|(spinner, _)| spinner.clone())
571 }
572
573 fn text_box(&self) -> Option<Ref<TextBox>> {
575 self.text_box.borrow().as_ref().map(|(text_box, _)| text_box.clone())
576 }
577
578 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 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 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 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 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 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 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 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 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 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 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 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 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 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 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 #[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 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 #[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 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 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 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 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 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 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 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 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 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 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 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 fn on_increment(&self) {
877 let result = match self.value() {
878 Some(value) => value.checked_add(self.increment()).unwrap_or(saturated(self.increment())),
879 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 fn on_decrement(&self) {
893 let result = match self.value() {
894 Some(value) => value.checked_sub(self.increment()).unwrap_or(saturated(-self.increment())),
895 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 fn set_valid_spin_direction(&self) {
908 let mut valid_directions = ValidSpinDirections::NONE;
909
910 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 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 if !self.is_text_changed_from_ui.get() {
1023 if force_text_update {
1024 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 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 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 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 if let Some(current_value_text) = current_value_text.filter(|text| !text.is_empty()) {
1102 if parse(current_value_text)?.is_none() {
1103 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 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 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 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
1166fn 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
1178fn 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
1192fn saturated(step: Decimal) -> Decimal {
1195 if step.is_sign_negative() {
1196 Decimal::MIN_VALUE
1197 } else {
1198 Decimal::MAX_VALUE
1199 }
1200}
1201
1202fn 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}