1use std::string::String;
9
10use azul_core::{
11 callbacks::{CoreCallbackData, Update},
12 dom::Dom,
13 refany::RefAny,
14};
15#[allow(clippy::wildcard_imports)] use azul_css::{
17 dynamic_selector::CssPropertyWithConditionsVec,
18 props::{
19 basic::*,
20 layout::*,
21 property::{CssProperty, *},
22 style::*,
23 },
24 *,
25};
26
27use crate::{
28 callbacks::{Callback, CallbackInfo},
29 widgets::text_input::{
30 OnTextInputReturn, TextInput, TextInputOnFocusLostCallback,
31 TextInputOnFocusLostCallbackType, TextInputOnTextInputCallback,
32 TextInputOnTextInputCallbackType, TextInputOnVirtualKeyDownCallback,
33 TextInputOnVirtualKeyDownCallbackType, TextInputState, TextInputValid,
34 },
35};
36
37pub type NumberInputOnValueChangeCallbackType =
39 extern "C" fn(RefAny, CallbackInfo, NumberInputState) -> Update;
40impl_widget_callback!(
41 NumberInputOnValueChange,
42 OptionNumberInputOnValueChange,
43 NumberInputOnValueChangeCallback,
44 NumberInputOnValueChangeCallbackType
45);
46
47azul_core::impl_managed_callback! {
48 wrapper: NumberInputOnValueChangeCallback,
49 info_ty: CallbackInfo,
50 return_ty: Update,
51 default_ret: Update::DoNothing,
52 invoker_static: NUMBER_INPUT_ON_VALUE_CHANGE_INVOKER,
53 invoker_ty: AzNumberInputOnValueChangeCallbackInvoker,
54 thunk_fn: az_number_input_on_value_change_callback_thunk,
55 setter_fn: AzApp_setNumberInputOnValueChangeCallbackInvoker,
56 from_handle_fn: AzNumberInputOnValueChangeCallback_createFromHostHandle,
57 extra_args: [ state: NumberInputState ],
58}
59
60pub type NumberInputOnFocusLostCallbackType =
62 extern "C" fn(RefAny, CallbackInfo, NumberInputState) -> Update;
63impl_widget_callback!(
64 NumberInputOnFocusLost,
65 OptionNumberInputOnFocusLost,
66 NumberInputOnFocusLostCallback,
67 NumberInputOnFocusLostCallbackType
68);
69
70azul_core::impl_managed_callback! {
71 wrapper: NumberInputOnFocusLostCallback,
72 info_ty: CallbackInfo,
73 return_ty: Update,
74 default_ret: Update::DoNothing,
75 invoker_static: NUMBER_INPUT_ON_FOCUS_LOST_INVOKER,
76 invoker_ty: AzNumberInputOnFocusLostCallbackInvoker,
77 thunk_fn: az_number_input_on_focus_lost_callback_thunk,
78 setter_fn: AzApp_setNumberInputOnFocusLostCallbackInvoker,
79 from_handle_fn: AzNumberInputOnFocusLostCallback_createFromHostHandle,
80 extra_args: [ state: NumberInputState ],
81}
82
83#[derive(Debug, Default, Clone, PartialEq)]
85#[repr(C)]
86pub struct NumberInput {
87 pub number_input_state: NumberInputStateWrapper,
88 pub text_input: TextInput,
89 pub style: CssPropertyWithConditionsVec,
90}
91
92#[derive(Debug, Default, Clone, PartialEq)]
94#[repr(C)]
95pub struct NumberInputStateWrapper {
96 pub inner: NumberInputState,
97 pub on_value_change: OptionNumberInputOnValueChange,
98 pub on_focus_lost: OptionNumberInputOnFocusLost,
99}
100
101#[derive(Copy, Debug, Clone, PartialEq)]
103#[repr(C)]
104pub struct NumberInputState {
105 pub previous: f32,
107 pub number: f32,
109 pub min: f32,
111 pub max: f32,
113}
114
115impl Default for NumberInputState {
116 fn default() -> Self {
117 Self {
118 previous: 0.0,
119 number: 0.0,
120 min: core::f32::MIN,
121 max: core::f32::MAX,
122 }
123 }
124}
125
126impl NumberInput {
127 #[must_use] pub fn create(input: f32) -> Self {
129 Self {
130 number_input_state: NumberInputStateWrapper {
131 inner: NumberInputState {
132 number: input,
133 ..Default::default()
134 },
135 ..Default::default()
136 },
137 ..Default::default()
138 }
139 }
140
141 pub fn set_on_text_input<C: Into<TextInputOnTextInputCallback>>(
142 &mut self,
143 refany: RefAny,
144 callback: C,
145 ) {
146 self.text_input.set_on_text_input(refany, callback);
147 }
148
149 #[must_use]
150 pub fn with_on_text_input<C: Into<TextInputOnTextInputCallback>>(
151 mut self,
152 refany: RefAny,
153 callback: C,
154 ) -> Self {
155 self.set_on_text_input(refany, callback);
156 self
157 }
158
159 pub fn set_on_virtual_key_down<C: Into<TextInputOnVirtualKeyDownCallback>>(
160 &mut self,
161 refany: RefAny,
162 callback: C,
163 ) {
164 self.text_input.set_on_virtual_key_down(refany, callback);
165 }
166
167 #[must_use]
168 pub fn with_on_virtual_key_down<C: Into<TextInputOnVirtualKeyDownCallback>>(
169 mut self,
170 refany: RefAny,
171 callback: C,
172 ) -> Self {
173 self.set_on_virtual_key_down(refany, callback);
174 self
175 }
176
177 pub fn set_placeholder_style(&mut self, style: CssPropertyWithConditionsVec) {
178 self.text_input.placeholder_style = style;
179 }
180
181 #[must_use] pub fn with_placeholder_style(mut self, style: CssPropertyWithConditionsVec) -> Self {
182 self.set_placeholder_style(style);
183 self
184 }
185
186 pub fn set_container_style(&mut self, style: CssPropertyWithConditionsVec) {
187 self.text_input.container_style = style;
188 }
189
190 #[must_use] pub fn with_container_style(mut self, style: CssPropertyWithConditionsVec) -> Self {
191 self.set_container_style(style);
192 self
193 }
194
195 pub fn set_label_style(&mut self, style: CssPropertyWithConditionsVec) {
196 self.text_input.label_style = style;
197 }
198
199 #[must_use] pub fn with_label_style(mut self, style: CssPropertyWithConditionsVec) -> Self {
200 self.set_label_style(style);
201 self
202 }
203
204 pub fn set_on_value_change<C: Into<NumberInputOnValueChangeCallback>>(
206 &mut self,
207 refany: RefAny,
208 callback: C,
209 ) {
210 self.number_input_state.on_value_change = Some(NumberInputOnValueChange {
211 callback: callback.into(),
212 refany,
213 })
214 .into();
215 }
216
217 #[must_use]
218 pub fn with_on_value_change<C: Into<NumberInputOnValueChangeCallback>>(
219 mut self,
220 refany: RefAny,
221 callback: C,
222 ) -> Self {
223 self.set_on_value_change(refany, callback);
224 self
225 }
226
227 pub fn set_on_focus_lost<C: Into<NumberInputOnFocusLostCallback>>(
228 &mut self,
229 refany: RefAny,
230 callback: C,
231 ) {
232 self.number_input_state.on_focus_lost = Some(NumberInputOnFocusLost {
233 callback: callback.into(),
234 refany,
235 })
236 .into();
237 }
238
239 #[must_use]
240 pub fn with_on_focus_lost<C: Into<NumberInputOnFocusLostCallback>>(
241 mut self,
242 refany: RefAny,
243 callback: C,
244 ) -> Self {
245 self.set_on_focus_lost(refany, callback);
246 self
247 }
248
249 #[must_use]
250 pub fn swap_with_default(&mut self) -> Self {
251 let mut s = Self::create(0.0);
252 core::mem::swap(&mut s, self);
253 s
254 }
255
256 #[must_use] pub fn dom(mut self) -> Dom {
257 let number_string = format!("{}", self.number_input_state.inner.number);
258 self.text_input.text_input_state.inner.text = number_string
259 .chars()
260 .map(|s| s as u32)
261 .collect::<Vec<_>>()
262 .into();
263
264 let state = RefAny::new(self.number_input_state);
265
266 let validate: TextInputOnTextInputCallbackType = validate_text_input;
267 self.text_input.set_on_text_input(state.clone(), validate);
268 let focus_lost: TextInputOnFocusLostCallbackType = on_focus_lost;
269 self.text_input.set_on_focus_lost(state, focus_lost);
270 self.text_input.dom()
271 }
272}
273
274extern "C" fn on_focus_lost(
275 mut refany: RefAny,
276 info: CallbackInfo,
277 _state: TextInputState,
278) -> Update {
279 let Some(mut refany) = refany.downcast_mut::<NumberInputStateWrapper>() else {
280 return Update::DoNothing;
281 };
282
283 let number_input = &mut *refany;
284 let onfocuslost = &mut number_input.on_focus_lost;
285 let inner = number_input.inner;
286
287 match onfocuslost.as_mut() {
288 Some(NumberInputOnFocusLost { callback, refany }) => {
289 (callback.cb)(refany.clone(), info, inner)
290 }
291 None => Update::DoNothing,
292 }
293}
294
295fn clamp_to_range(value: f32, min: f32, max: f32) -> f32 {
301 let (lo, hi) = match (min.is_nan(), max.is_nan()) {
302 (true, true) => return value,
303 (true, false) => (max, max),
304 (false, true) => (min, min),
305 (false, false) if min <= max => (min, max),
306 (false, false) => (max, min),
307 };
308 value.clamp(lo, hi)
309}
310
311extern "C" fn validate_text_input(
312 mut refany: RefAny,
313 info: CallbackInfo,
314 state: TextInputState,
315) -> OnTextInputReturn {
316 let Some(mut refany) = refany.downcast_mut::<NumberInputStateWrapper>() else {
317 return OnTextInputReturn {
318 update: Update::DoNothing,
319 valid: TextInputValid::Yes,
320 };
321 };
322
323 let validated_input: String = state
324 .text
325 .iter()
326 .filter_map(|c| core::char::from_u32(*c))
327 .map(|c| if c == ',' { '.' } else { c })
328 .collect();
329
330 let Ok(validated_f32) = validated_input.parse::<f32>() else {
331 return OnTextInputReturn {
334 update: Update::DoNothing,
335 valid: TextInputValid::No,
336 };
337 };
338
339 let number_input = &mut *refany;
340 let onvaluechange = &mut number_input.on_value_change;
341 let inner = &mut number_input.inner;
342
343 inner.previous = inner.number;
344 let clamped = clamp_to_range(validated_f32, inner.min, inner.max);
345 inner.number = clamped;
346 let inner_clone = *inner;
347
348 let update = match onvaluechange.as_mut() {
349 Some(NumberInputOnValueChange { callback, refany }) => {
350 (callback.cb)(refany.clone(), info, inner_clone)
351 }
352 None => Update::DoNothing,
353 };
354
355 OnTextInputReturn {
356 update,
357 valid: TextInputValid::Yes,
358 }
359}
360
361#[cfg(all(test, feature = "std"))]
362#[allow(clippy::float_cmp, clippy::too_many_lines)]
363mod autotest_generated {
364 use std::{
365 collections::BTreeMap,
366 panic::{catch_unwind, AssertUnwindSafe},
367 sync::{Arc, Mutex},
368 };
369
370 use azul_core::{
371 dom::{DomId, DomNodeId},
372 geom::OptionLogicalPosition,
373 gl::OptionGlContextPtr,
374 hit_test::ScrollPosition,
375 refany::OptionRefAny,
376 resources::RendererResources,
377 styled_dom::NodeHierarchyItemId,
378 window::{MonitorVec, RawWindowHandle},
379 };
380 use azul_css::dynamic_selector::CssPropertyWithConditions;
381 use rust_fontconfig::FcFontCache;
382
383 use super::*;
384 #[cfg(feature = "icu")]
385 use crate::icu::IcuLocalizerHandle;
386 use crate::{
387 callbacks::{CallbackChange, CallbackInfoRefData, ExternalSystemCallbacks},
388 widgets::text_input::TextInputStateWrapper,
389 window::LayoutWindow,
390 window_state::FullWindowState,
391 };
392
393 fn finite_samples() -> [f32; 16] {
402 [
403 0.0,
404 -0.0,
405 1.0,
406 -1.0,
407 0.5,
408 -0.5,
409 42.25,
410 -2.5,
411 0.1,
412 16_777_216.0,
413 f32::MIN,
414 f32::MAX,
415 f32::MIN_POSITIVE,
416 -f32::MIN_POSITIVE,
417 f32::EPSILON,
418 f32::from_bits(1),
421 ]
422 }
423
424 const MALFORMED: [&str; 26] = [
427 "", " ", " 1",
430 "1 ",
431 "\t1",
432 "\n",
433 "abc",
434 "e",
435 "e5",
436 "E",
437 "+",
438 "-",
439 ".",
440 "..",
441 "--1",
442 "1.2.3",
443 "1e",
444 "1e+",
445 "0x10", "0b1",
447 "1_000", "1/2",
449 "1%",
450 "½", "∞", "1\u{200b}0", ];
454
455 const NON_ASCII_DIGITS: [&str; 6] = [
457 "١٢٣", "١٫٥", "123", "𝟏", "٣.5", "Ⅻ", ];
464
465 const ACCEPTED: [(&str, f32); 17] = [
469 ("0", 0.0),
470 ("-0", -0.0),
471 ("+1", 1.0),
472 ("1.", 1.0),
473 (".5", 0.5),
474 ("-.5", -0.5),
475 ("1e3", 1000.0),
476 ("1E3", 1000.0),
477 ("1e+3", 1000.0),
478 ("1e-3", 0.001),
479 ("00042.2500", 42.25),
480 ("inf", f32::MAX),
481 ("infinity", f32::MAX),
482 ("-inf", f32::MIN),
483 ("nan", f32::NAN),
484 ("NaN", f32::NAN),
485 ("NAN", f32::NAN),
486 ];
487
488 fn same(a: f32, b: f32) -> bool {
497 if a.is_nan() || b.is_nan() {
498 a.is_nan() && b.is_nan()
499 } else {
500 a.to_bits() == b.to_bits()
501 }
502 }
503
504 fn wrapper(number: f32, min: f32, max: f32) -> NumberInputStateWrapper {
507 NumberInputStateWrapper {
508 inner: NumberInputState {
509 previous: 0.0,
510 number,
511 min,
512 max,
513 },
514 on_value_change: OptionNumberInputOnValueChange::None,
515 on_focus_lost: OptionNumberInputOnFocusLost::None,
516 }
517 }
518
519 fn text_state(text: &str) -> TextInputState {
521 TextInputState {
522 text: text.chars().map(|c| c as u32).collect::<Vec<_>>().into(),
523 ..TextInputState::default()
524 }
525 }
526
527 fn raw_text_state(units: &[u32]) -> TextInputState {
530 TextInputState {
531 text: units.to_vec().into(),
532 ..TextInputState::default()
533 }
534 }
535
536 fn read(state: &RefAny) -> NumberInputState {
538 let mut state = state.clone();
539 let wrapper = state
540 .downcast_ref::<NumberInputStateWrapper>()
541 .expect("the payload must still be a NumberInputStateWrapper");
542 wrapper.inner
543 }
544
545 fn reset(state: &RefAny, number: f32) {
548 let mut state = state.clone();
549 let mut wrapper = state
550 .downcast_mut::<NumberInputStateWrapper>()
551 .expect("the payload must still be a NumberInputStateWrapper");
552 wrapper.inner.number = number;
553 wrapper.inner.previous = 0.0;
554 }
555
556 fn style(n: usize) -> CssPropertyWithConditionsVec {
559 let all: Vec<CssPropertyWithConditions> =
560 TextInput::default().container_style.as_ref().to_vec();
561 assert!(n <= all.len(), "not enough default properties to slice");
562 CssPropertyWithConditionsVec::from_vec(all.into_iter().take(n).collect())
563 }
564
565 struct Recorder {
569 seen: Vec<NumberInputState>,
570 ret: Update,
571 }
572
573 impl Recorder {
574 fn new(ret: Update) -> Self {
575 Self {
576 seen: Vec::new(),
577 ret,
578 }
579 }
580 }
581
582 extern "C" fn record_value_change(
583 mut data: RefAny,
584 _: CallbackInfo,
585 state: NumberInputState,
586 ) -> Update {
587 let Some(mut log) = data.downcast_mut::<Recorder>() else {
588 return Update::DoNothing;
589 };
590 log.seen.push(state);
591 log.ret
592 }
593
594 extern "C" fn record_focus_lost(
598 mut data: RefAny,
599 _: CallbackInfo,
600 state: NumberInputState,
601 ) -> Update {
602 match data.downcast_mut::<Recorder>() {
603 Some(mut log) => {
604 log.seen.push(state);
605 log.ret
606 }
607 None => Update::DoNothing,
608 }
609 }
610
611 extern "C" fn accept_everything(
614 _: RefAny,
615 _: CallbackInfo,
616 _: TextInputState,
617 ) -> OnTextInputReturn {
618 OnTextInputReturn {
619 update: Update::RefreshDomAllWindows,
620 valid: TextInputValid::Yes,
621 }
622 }
623
624 extern "C" fn reject_everything(
626 _: RefAny,
627 _: CallbackInfo,
628 _: TextInputState,
629 ) -> OnTextInputReturn {
630 OnTextInputReturn {
631 update: Update::RefreshDomAllWindows,
632 valid: TextInputValid::No,
633 }
634 }
635
636 fn recorded(recorder: &RefAny) -> Vec<NumberInputState> {
637 let mut recorder = recorder.clone();
638 let log = recorder
639 .downcast_ref::<Recorder>()
640 .expect("the payload must still be a Recorder");
641 log.seen.clone()
642 }
643
644 fn wrapper_with_value_hook(
645 number: f32,
646 min: f32,
647 max: f32,
648 recorder: &RefAny,
649 ) -> NumberInputStateWrapper {
650 NumberInputStateWrapper {
651 on_value_change: Some(NumberInputOnValueChange {
652 refany: recorder.clone(),
653 callback: (record_value_change as NumberInputOnValueChangeCallbackType).into(),
654 })
655 .into(),
656 ..wrapper(number, min, max)
657 }
658 }
659
660 fn wrapper_with_focus_hook(
661 number: f32,
662 min: f32,
663 max: f32,
664 recorder: &RefAny,
665 ) -> NumberInputStateWrapper {
666 NumberInputStateWrapper {
667 on_focus_lost: Some(NumberInputOnFocusLost {
668 refany: recorder.clone(),
669 callback: (record_focus_lost as NumberInputOnFocusLostCallbackType).into(),
670 })
671 .into(),
672 ..wrapper(number, min, max)
673 }
674 }
675
676 fn with_info<R>(f: impl FnOnce(CallbackInfo) -> R) -> R {
684 let layout_window =
685 LayoutWindow::new(FcFontCache::default()).expect("LayoutWindow::new failed");
686 let renderer_resources = RendererResources::default();
687 let previous_window_state: Option<FullWindowState> = None;
688 let current_window_state = FullWindowState::default();
689 let gl_context = OptionGlContextPtr::None;
690 let scroll_states: BTreeMap<DomId, BTreeMap<NodeHierarchyItemId, ScrollPosition>> =
691 BTreeMap::new();
692 let window_handle = RawWindowHandle::Unsupported;
693 let system_callbacks = ExternalSystemCallbacks::rust_internal();
694
695 let ref_data = CallbackInfoRefData {
696 layout_window: &layout_window,
697 renderer_resources: &renderer_resources,
698 previous_window_state: &previous_window_state,
699 current_window_state: ¤t_window_state,
700 gl_context: &gl_context,
701 current_scroll_manager: &scroll_states,
702 current_window_handle: &window_handle,
703 system_callbacks: &system_callbacks,
704 system_style: Arc::new(azul_css::system::SystemStyle::default()),
705 monitors: Arc::new(Mutex::new(MonitorVec::from_const_slice(&[]))),
706 #[cfg(feature = "icu")]
707 icu_localizer: IcuLocalizerHandle::default(),
708 ctx: OptionRefAny::None,
709 };
710
711 let changes: Arc<Mutex<Vec<CallbackChange>>> = Arc::new(Mutex::new(Vec::new()));
712
713 let info = CallbackInfo::new(
714 &ref_data,
715 &changes,
716 DomNodeId {
717 dom: DomId::ROOT_ID,
718 node: NodeHierarchyItemId::NONE,
719 },
720 OptionLogicalPosition::None,
721 OptionLogicalPosition::None,
722 );
723
724 f(info)
725 }
726
727 fn validate_one(state: &RefAny, text: &str) -> (OnTextInputReturn, NumberInputState) {
730 with_info(|info| {
731 let r = validate_text_input(state.clone(), info, text_state(text));
732 (r, read(state))
733 })
734 }
735
736 fn validate_raw(state: &RefAny, units: &[u32]) -> (OnTextInputReturn, NumberInputState) {
738 with_info(|info| {
739 let r = validate_text_input(state.clone(), info, raw_text_state(units));
740 (r, read(state))
741 })
742 }
743
744 fn focus_lost(state: &RefAny, text: &str) -> Update {
745 with_info(|info| on_focus_lost(state.clone(), info, text_state(text)))
746 }
747
748 const PLACEHOLDER: usize = 0;
752 const LABEL: usize = 1;
753
754 fn dataset_of(dom: &Dom) -> RefAny {
755 dom.root
756 .get_dataset()
757 .cloned()
758 .expect("TextInput::dom must attach its state as the node's dataset")
759 }
760
761 fn buffer_text(dom: &Dom) -> String {
763 let mut dataset = dataset_of(dom);
764 let wrapper = dataset
765 .downcast_ref::<TextInputStateWrapper>()
766 .expect("the dataset must be a TextInputStateWrapper");
767 wrapper.inner.get_text()
768 }
769
770 fn displayed_text(dom: &Dom) -> String {
772 dom.children.as_ref()[LABEL]
773 .root
774 .get_node_type()
775 .format()
776 .expect("the label child must be a text node")
777 }
778
779 fn cursor_pos(dom: &Dom) -> usize {
780 let mut dataset = dataset_of(dom);
781 let wrapper = dataset
782 .downcast_ref::<TextInputStateWrapper>()
783 .expect("the dataset must be a TextInputStateWrapper");
784 wrapper.inner.cursor_pos
785 }
786
787 fn number_state_of(dom: &Dom) -> RefAny {
791 let mut dataset = dataset_of(dom);
792 let wrapper = dataset
793 .downcast_ref::<TextInputStateWrapper>()
794 .expect("the dataset must be a TextInputStateWrapper");
795 wrapper
796 .on_text_input
797 .as_ref()
798 .expect("NumberInput::dom must install a text-input hook")
799 .refany
800 .clone()
801 }
802
803 fn drive_text_input(dom: &Dom, text: &str) -> OnTextInputReturn {
805 let mut dataset = dataset_of(dom);
806 let hook = dataset
807 .downcast_ref::<TextInputStateWrapper>()
808 .expect("the dataset must be a TextInputStateWrapper")
809 .on_text_input
810 .as_ref()
811 .expect("NumberInput::dom must install a text-input hook")
812 .clone();
813 with_info(|info| (hook.callback.cb)(hook.refany.clone(), info, text_state(text)))
814 }
815
816 fn drive_virtual_key_down(dom: &Dom) -> Option<OnTextInputReturn> {
818 let mut dataset = dataset_of(dom);
819 let hook = dataset
820 .downcast_ref::<TextInputStateWrapper>()
821 .expect("the dataset must be a TextInputStateWrapper")
822 .on_virtual_key_down
823 .as_ref()
824 .cloned()?;
825 Some(with_info(|info| {
826 (hook.callback.cb)(hook.refany.clone(), info, text_state(""))
827 }))
828 }
829
830 #[test]
835 fn create_zero_is_exactly_the_default_widget() {
836 assert_eq!(
837 NumberInput::create(0.0),
838 NumberInput::default(),
839 "create(0.0) must not perturb anything Default already set",
840 );
841 }
842
843 #[test]
844 fn create_preserves_every_sample_value_bit_exactly() {
845 for v in finite_samples() {
846 let state = NumberInput::create(v).number_input_state.inner;
847 assert!(
848 same(state.number, v),
849 "create({v:?}) stored {:?}",
850 state.number,
851 );
852 assert!(
853 same(state.previous, 0.0),
854 "create({v:?}) must start with no history, got previous = {:?}",
855 state.previous,
856 );
857 assert!(
858 same(state.min, f32::MIN) && same(state.max, f32::MAX),
859 "create({v:?}) must leave the range wide open, got [{}, {}]",
860 state.min,
861 state.max,
862 );
863 }
864 }
865
866 #[test]
867 fn create_accepts_nan_and_infinities_without_panicking() {
868 for v in [f32::NAN, f32::INFINITY, f32::NEG_INFINITY] {
869 let state = NumberInput::create(v).number_input_state.inner;
870 assert!(
871 same(state.number, v),
872 "create({v:?}) stored {:?}",
873 state.number,
874 );
875 }
876 assert!(NumberInput::create(f32::NAN)
877 .number_input_state
878 .inner
879 .number
880 .is_nan());
881 }
882
883 #[test]
884 fn create_never_clamps_its_argument() {
885 let state = NumberInput::create(f32::INFINITY).number_input_state.inner;
889 assert!(
890 state.number.is_infinite(),
891 "create must store the value verbatim, got {}",
892 state.number,
893 );
894 assert!(state.number > state.max, "…even outside its own range");
895 }
896
897 #[test]
898 fn the_default_range_leaves_every_finite_value_untouched() {
899 let d = NumberInputState::default();
900 assert!(
901 d.min <= d.max,
902 "the default range must be non-empty — f32::clamp panics otherwise",
903 );
904 assert!(same(d.min, f32::MIN) && same(d.max, f32::MAX));
905 assert!(same(d.number, 0.0) && same(d.previous, 0.0));
906 for v in finite_samples() {
907 assert!(
908 same(v.clamp(d.min, d.max), v),
909 "{v:?} must pass through the default range untouched",
910 );
911 }
912 }
913
914 #[test]
915 fn number_input_state_is_a_value_type() {
916 let a = NumberInputState::default();
917 let mut b = a;
918 b.number = 5.0;
919 assert!(
920 same(a.number, 0.0),
921 "NumberInputState is Copy — mutating a copy must not alias the original",
922 );
923 assert_ne!(a, b);
924 }
925
926 #[test]
927 fn a_fresh_wrapper_has_no_hooks() {
928 let w = NumberInputStateWrapper::default();
929 assert!(w.on_value_change.as_ref().is_none());
930 assert!(w.on_focus_lost.as_ref().is_none());
931 assert_eq!(w.inner, NumberInputState::default());
932 }
933
934 #[test]
939 fn with_and_set_style_pairs_are_equivalent() {
940 for n in 0..4 {
941 let s = style(n);
942
943 let a = NumberInput::create(1.0).with_placeholder_style(s.clone());
944 let mut b = NumberInput::create(1.0);
945 b.set_placeholder_style(s.clone());
946 assert_eq!(a, b, "with_placeholder_style != set_placeholder_style ({n})");
947
948 let a = NumberInput::create(1.0).with_container_style(s.clone());
949 let mut b = NumberInput::create(1.0);
950 b.set_container_style(s.clone());
951 assert_eq!(a, b, "with_container_style != set_container_style ({n})");
952
953 let a = NumberInput::create(1.0).with_label_style(s.clone());
954 let mut b = NumberInput::create(1.0);
955 b.set_label_style(s);
956 assert_eq!(a, b, "with_label_style != set_label_style ({n})");
957 }
958 }
959
960 #[test]
961 fn style_setters_write_to_disjoint_fields() {
962 let placeholder = style(1);
963 let container = style(2);
964 let label = style(3);
965 assert_ne!(placeholder, container, "the fixture must be distinguishable");
966 assert_ne!(container, label, "the fixture must be distinguishable");
967
968 let input = NumberInput::create(0.0)
969 .with_placeholder_style(placeholder.clone())
970 .with_container_style(container.clone())
971 .with_label_style(label.clone());
972
973 assert_eq!(input.text_input.placeholder_style, placeholder);
974 assert_eq!(input.text_input.container_style, container);
975 assert_eq!(input.text_input.label_style, label);
976 assert_eq!(
977 input.style,
978 NumberInput::default().style,
979 "NumberInput::style is not a dumping ground for the TextInput styles",
980 );
981 }
982
983 #[test]
984 fn with_and_set_callback_pairs_are_equivalent() {
985 let data = RefAny::new(0u32);
988
989 let a = NumberInput::create(1.0).with_on_value_change(
990 data.clone(),
991 record_value_change as NumberInputOnValueChangeCallbackType,
992 );
993 let mut b = NumberInput::create(1.0);
994 b.set_on_value_change(
995 data.clone(),
996 record_value_change as NumberInputOnValueChangeCallbackType,
997 );
998 assert_eq!(a, b, "with_on_value_change != set_on_value_change");
999
1000 let a = NumberInput::create(1.0).with_on_focus_lost(
1001 data.clone(),
1002 record_focus_lost as NumberInputOnFocusLostCallbackType,
1003 );
1004 let mut b = NumberInput::create(1.0);
1005 b.set_on_focus_lost(
1006 data.clone(),
1007 record_focus_lost as NumberInputOnFocusLostCallbackType,
1008 );
1009 assert_eq!(a, b, "with_on_focus_lost != set_on_focus_lost");
1010
1011 let a = NumberInput::create(1.0).with_on_text_input(
1012 data.clone(),
1013 accept_everything as TextInputOnTextInputCallbackType,
1014 );
1015 let mut b = NumberInput::create(1.0);
1016 b.set_on_text_input(
1017 data.clone(),
1018 accept_everything as TextInputOnTextInputCallbackType,
1019 );
1020 assert_eq!(a, b, "with_on_text_input != set_on_text_input");
1021
1022 let a = NumberInput::create(1.0).with_on_virtual_key_down(
1023 data.clone(),
1024 reject_everything as TextInputOnVirtualKeyDownCallbackType,
1025 );
1026 let mut b = NumberInput::create(1.0);
1027 b.set_on_virtual_key_down(data, reject_everything as TextInputOnVirtualKeyDownCallbackType);
1028 assert_eq!(a, b, "with_on_virtual_key_down != set_on_virtual_key_down");
1029 }
1030
1031 #[test]
1032 fn setting_a_hook_twice_keeps_the_last_one() {
1033 let first = RefAny::new(1u32);
1034 let second = RefAny::new(2u32);
1035
1036 let mut input = NumberInput::create(0.0);
1037 input.set_on_value_change(
1038 first.clone(),
1039 record_value_change as NumberInputOnValueChangeCallbackType,
1040 );
1041 input.set_on_value_change(
1042 second.clone(),
1043 record_value_change as NumberInputOnValueChangeCallbackType,
1044 );
1045
1046 let stored = input
1047 .number_input_state
1048 .on_value_change
1049 .as_ref()
1050 .expect("the hook must be set");
1051 assert_eq!(stored.refany, second, "the last hook must win");
1052 assert_ne!(stored.refany, first, "the first hook must be released");
1053 }
1054
1055 #[test]
1056 fn swap_with_default_hands_back_the_original_and_leaves_a_fresh_widget() {
1057 let data = RefAny::new(0u32);
1058 let mut input = NumberInput::create(7.5)
1059 .with_on_value_change(
1060 data,
1061 record_value_change as NumberInputOnValueChangeCallbackType,
1062 )
1063 .with_label_style(style(2));
1064 let original = input.clone();
1065
1066 let taken = input.swap_with_default();
1067 assert_eq!(taken, original, "swap_with_default must return the original");
1068 assert_eq!(
1069 input,
1070 NumberInput::create(0.0),
1071 "the receiver must be left as a fresh 0.0 widget",
1072 );
1073 assert_eq!(
1074 input,
1075 NumberInput::default(),
1076 "…which is also exactly the Default widget",
1077 );
1078
1079 let second = input.swap_with_default();
1081 assert_eq!(second, NumberInput::default());
1082 assert_eq!(input, NumberInput::default());
1083 }
1084
1085 #[test]
1090 fn dom_wires_the_text_input_and_parks_the_cursor_at_the_end() {
1091 let dom = NumberInput::create(-12.5).dom();
1092 assert_eq!(
1093 dom.children.as_ref().len(),
1094 2,
1095 "TextInput renders a placeholder node and a label node",
1096 );
1097 assert_eq!(
1098 dom.root.callbacks.as_ref().len(),
1099 5,
1100 "focus received/lost, text input, virtual key down, hover",
1101 );
1102 assert!(
1103 dom.children.as_ref()[PLACEHOLDER]
1104 .root
1105 .get_node_type()
1106 .format()
1107 .is_some(),
1108 "the placeholder child must be a text node",
1109 );
1110 assert_eq!(buffer_text(&dom), "-12.5");
1111 assert_eq!(displayed_text(&dom), "-12.5");
1112 assert_eq!(
1113 cursor_pos(&dom),
1114 "-12.5".chars().count(),
1115 "the cursor must sit at the end of the rendered number",
1116 );
1117 }
1118
1119 #[test]
1120 fn dom_text_round_trips_back_to_the_same_f32() {
1121 for v in finite_samples() {
1122 let dom = NumberInput::create(v).dom();
1123 let text = buffer_text(&dom);
1124 let parsed: f32 = text.parse().unwrap_or_else(|e| {
1125 panic!("the widget rendered {v:?} as {text:?}, which is not a float: {e}")
1126 });
1127 assert!(
1128 same(parsed, v),
1129 "{v:?} was rendered as {text:?} and read back as {parsed:?}",
1130 );
1131 assert_eq!(
1132 displayed_text(&dom),
1133 text,
1134 "the visible label and the edit buffer must agree for {v:?}",
1135 );
1136 }
1137 }
1138
1139 #[test]
1140 fn dom_renders_the_shortest_round_trip_form() {
1141 for (value, expected) in [
1142 (0.0f32, "0"),
1143 (1.0, "1"),
1144 (-1.5, "-1.5"),
1145 (42.25, "42.25"),
1146 (0.5, "0.5"),
1147 ] {
1148 assert_eq!(buffer_text(&NumberInput::create(value).dom()), expected);
1149 }
1150 }
1151
1152 #[test]
1153 fn dom_renders_non_finite_values_as_inf_and_nan() {
1154 assert_eq!(buffer_text(&NumberInput::create(f32::INFINITY).dom()), "inf");
1155 assert_eq!(
1156 buffer_text(&NumberInput::create(f32::NEG_INFINITY).dom()),
1157 "-inf",
1158 );
1159 assert_eq!(buffer_text(&NumberInput::create(f32::NAN).dom()), "NaN");
1160 assert_eq!(
1161 buffer_text(&NumberInput::create(-f32::NAN).dom()),
1162 "NaN",
1163 "the sign of a NaN is not rendered, so it cannot round-trip",
1164 );
1165 }
1166
1167 #[test]
1168 fn every_string_the_widget_renders_is_accepted_by_its_own_validator() {
1169 let mut values = finite_samples().to_vec();
1170 values.extend_from_slice(&[f32::INFINITY, f32::NEG_INFINITY, f32::NAN]);
1171
1172 for v in values {
1173 let text = buffer_text(&NumberInput::create(v).dom());
1174 let state = RefAny::new(wrapper(0.0, f32::MIN, f32::MAX));
1175 let (r, _) = validate_one(&state, &text);
1176 assert_eq!(
1177 r.valid,
1178 TextInputValid::Yes,
1179 "the widget renders {v:?} as {text:?} but then refuses to parse it back",
1180 );
1181 }
1182 }
1183
1184 #[test]
1185 fn dom_renders_a_value_that_is_outside_its_own_range() {
1186 let mut input = NumberInput::create(1000.0);
1190 input.number_input_state.inner.min = 0.0;
1191 input.number_input_state.inner.max = 10.0;
1192 assert_eq!(buffer_text(&input.dom()), "1000");
1193 }
1194
1195 #[test]
1196 fn dom_replaces_a_user_supplied_text_input_hook_with_the_numeric_validator() {
1197 let dom = NumberInput::create(1.0)
1201 .with_on_text_input(
1202 RefAny::new(0u32),
1203 accept_everything as TextInputOnTextInputCallbackType,
1204 )
1205 .dom();
1206
1207 let r = drive_text_input(&dom, "abc");
1208 assert_eq!(
1209 r.valid,
1210 TextInputValid::No,
1211 "the numeric validator must own the text-input hook after dom()",
1212 );
1213 assert_eq!(r.update, Update::DoNothing);
1214 }
1215
1216 #[test]
1217 fn dom_keeps_a_user_supplied_virtual_key_hook() {
1218 let dom = NumberInput::create(1.0)
1219 .with_on_virtual_key_down(
1220 RefAny::new(0u32),
1221 reject_everything as TextInputOnVirtualKeyDownCallbackType,
1222 )
1223 .dom();
1224
1225 let r = drive_virtual_key_down(&dom)
1226 .expect("with_on_virtual_key_down must survive rendering");
1227 assert_eq!(r.update, Update::RefreshDomAllWindows);
1228 assert_eq!(r.valid, TextInputValid::No);
1229 }
1230
1231 #[test]
1232 fn dom_wires_the_value_change_hook_through_the_rendered_widget() {
1233 let recorder = RefAny::new(Recorder::new(Update::RefreshDom));
1234 let dom = NumberInput::create(0.0)
1235 .with_on_value_change(
1236 recorder.clone(),
1237 record_value_change as NumberInputOnValueChangeCallbackType,
1238 )
1239 .dom();
1240
1241 let r = drive_text_input(&dom, "12,5");
1242 assert_eq!(r.valid, TextInputValid::Yes);
1243 assert_eq!(
1244 r.update,
1245 Update::RefreshDom,
1246 "validate must return whatever the user's hook returned",
1247 );
1248
1249 let seen = recorded(&recorder);
1250 assert_eq!(seen.len(), 1, "the hook must fire exactly once per edit");
1251 assert!(same(seen[0].number, 12.5));
1252 assert!(same(seen[0].previous, 0.0));
1253
1254 assert!(
1255 same(read(&number_state_of(&dom)).number, 12.5),
1256 "the state behind the rendered DOM must have been updated too",
1257 );
1258 }
1259
1260 #[test]
1265 fn validate_rejects_malformed_input_without_touching_the_state() {
1266 let state = RefAny::new(wrapper(7.5, -100.0, 100.0));
1268 with_info(|info| {
1269 for text in MALFORMED {
1270 let r = validate_text_input(state.clone(), info, text_state(text));
1271 assert_eq!(r.valid, TextInputValid::No, "{text:?} must be rejected");
1272 assert_eq!(
1273 r.update,
1274 Update::DoNothing,
1275 "a rejected edit must not trigger a relayout ({text:?})",
1276 );
1277 let after = read(&state);
1278 assert!(
1279 same(after.number, 7.5),
1280 "{text:?} changed the value to {}",
1281 after.number,
1282 );
1283 assert!(
1284 same(after.previous, 0.0),
1285 "{text:?} touched `previous` ({})",
1286 after.previous,
1287 );
1288 }
1289 });
1290 }
1291
1292 #[test]
1293 fn validate_rejects_digits_that_are_not_ascii_digits() {
1294 let state = RefAny::new(wrapper(3.0, f32::MIN, f32::MAX));
1295 with_info(|info| {
1296 for text in NON_ASCII_DIGITS {
1297 let r = validate_text_input(state.clone(), info, text_state(text));
1298 assert_eq!(r.valid, TextInputValid::No, "{text:?} must be rejected");
1299 assert!(
1300 same(read(&state).number, 3.0),
1301 "{text:?} must not change the value",
1302 );
1303 }
1304 });
1305 }
1306
1307 #[test]
1308 fn validate_accepts_every_form_the_rust_float_parser_accepts() {
1309 let state = RefAny::new(wrapper(-1.0, f32::MIN, f32::MAX));
1310 with_info(|info| {
1311 for (text, expected) in ACCEPTED {
1312 reset(&state, -1.0);
1313 let r = validate_text_input(state.clone(), info, text_state(text));
1314 assert_eq!(r.valid, TextInputValid::Yes, "{text:?} must be accepted");
1315 assert_eq!(
1316 r.update,
1317 Update::DoNothing,
1318 "no hook is installed, so there is nothing to redraw ({text:?})",
1319 );
1320 let after = read(&state);
1321 assert!(
1322 same(after.number, expected),
1323 "{text:?} stored {} (expected {expected})",
1324 after.number,
1325 );
1326 assert!(
1327 same(after.previous, -1.0),
1328 "{text:?} must push the old value into `previous`, got {}",
1329 after.previous,
1330 );
1331 }
1332 });
1333 }
1334
1335 #[test]
1336 fn validate_reads_a_comma_as_a_decimal_point() {
1337 with_info(|info| {
1338 for (text, expected) in [
1339 ("1,5", 1.5f32),
1340 ("-1,5", -1.5),
1341 (",5", 0.5),
1342 ("1,", 1.0),
1343 ("1,25e2", 125.0),
1344 ] {
1345 let state = RefAny::new(wrapper(0.0, f32::MIN, f32::MAX));
1346 let r = validate_text_input(state.clone(), info, text_state(text));
1347 assert_eq!(r.valid, TextInputValid::Yes, "{text:?} must be accepted");
1348 assert!(
1349 same(read(&state).number, expected),
1350 "{text:?} stored {} (expected {expected})",
1351 read(&state).number,
1352 );
1353 }
1354
1355 for text in [",", ",,", "1,,5", "1,5,5"] {
1357 let state = RefAny::new(wrapper(0.0, f32::MIN, f32::MAX));
1358 let r = validate_text_input(state.clone(), info, text_state(text));
1359 assert_eq!(r.valid, TextInputValid::No, "{text:?} must be rejected");
1360 }
1361 });
1362 }
1363
1364 #[test]
1365 fn validate_reads_a_thousands_separator_as_a_decimal_point() {
1366 let state = RefAny::new(wrapper(0.0, f32::MIN, f32::MAX));
1371 let (r, after) = validate_one(&state, "1,000");
1372 assert_eq!(r.valid, TextInputValid::Yes);
1373 assert!(
1374 same(after.number, 1.0),
1375 "\"1,000\" is read as {} (the comma is a decimal point here)",
1376 after.number,
1377 );
1378
1379 let (r, _) = validate_one(&state, "1,000,000");
1380 assert_eq!(
1381 r.valid,
1382 TextInputValid::No,
1383 "a second group makes it un-parseable rather than ambiguous",
1384 );
1385 }
1386
1387 #[test]
1388 fn validate_silently_drops_code_units_that_are_not_unicode_scalars() {
1389 let state = RefAny::new(wrapper(0.0, f32::MIN, f32::MAX));
1394
1395 let (r, after) = validate_raw(&state, &[0x31, 0xD800, 0x35]);
1396 assert_eq!(r.valid, TextInputValid::Yes);
1397 assert!(
1398 same(after.number, 15.0),
1399 "a non-scalar code unit between two digits is dropped, got {}",
1400 after.number,
1401 );
1402
1403 let (r, after) = validate_raw(&state, &[0xD800, 0xDFFF, 0x0011_0000]);
1406 assert_eq!(r.valid, TextInputValid::No);
1407 assert!(
1408 same(after.number, 15.0),
1409 "a rejected edit must not change the value",
1410 );
1411 }
1412
1413 #[test]
1414 fn validate_survives_pathologically_long_input() {
1415 let state = RefAny::new(wrapper(0.0, f32::MIN, f32::MAX));
1416
1417 for (text, expected) in [
1418 ("9".repeat(10_000), f32::MAX), (format!("-{}", "9".repeat(10_000)), f32::MIN),
1420 (format!("{}1", "0".repeat(10_000)), 1.0), (format!("0.{}", "0".repeat(10_000)), 0.0),
1422 ("1e999999999".to_string(), f32::MAX), ("1e-999999999".to_string(), 0.0), ] {
1425 let (r, after) = validate_one(&state, &text);
1426 assert_eq!(
1427 r.valid,
1428 TextInputValid::Yes,
1429 "a {}-char input must parse, not error",
1430 text.len(),
1431 );
1432 assert!(
1433 same(after.number, expected),
1434 "a {}-char input stored {} (expected {expected})",
1435 text.len(),
1436 after.number,
1437 );
1438 }
1439 }
1440
1441 #[test]
1446 fn validate_saturates_overflow_at_the_configured_bounds() {
1447 let state = RefAny::new(wrapper(0.0, f32::MIN, f32::MAX));
1448
1449 let (r, after) = validate_one(&state, "1e39");
1452 assert_eq!(r.valid, TextInputValid::Yes);
1453 assert!(
1454 same(after.number, f32::MAX),
1455 "an overflowing value must saturate at `max`, got {}",
1456 after.number,
1457 );
1458
1459 let (_, after) = validate_one(&state, "-1e39");
1460 assert!(
1461 same(after.number, f32::MIN),
1462 "…and at `min` on the negative side, got {}",
1463 after.number,
1464 );
1465 }
1466
1467 #[test]
1468 fn validate_underflow_keeps_the_sign_of_zero() {
1469 let state = RefAny::new(wrapper(1.0, f32::MIN, f32::MAX));
1470
1471 let (_, after) = validate_one(&state, "1e-46");
1472 assert!(
1473 same(after.number, 0.0),
1474 "a positive underflow must land on +0.0, got {}",
1475 after.number,
1476 );
1477
1478 let (_, after) = validate_one(&state, "-1e-46");
1479 assert!(
1480 same(after.number, -0.0),
1481 "a negative underflow must land on -0.0, got {}",
1482 after.number,
1483 );
1484 }
1485
1486 #[test]
1487 fn validate_clamps_into_range() {
1488 with_info(|info| {
1489 for (min, max, text, expected) in [
1490 (-10.0f32, 10.0f32, "1000", 10.0f32),
1491 (-10.0, 10.0, "-1000", -10.0),
1492 (-10.0, 10.0, "inf", 10.0),
1493 (-10.0, 10.0, "-inf", -10.0),
1494 (-10.0, 10.0, "10", 10.0), (-10.0, 10.0, "-10", -10.0),
1496 (-10.0, 10.0, "10.000001", 10.0), (-10.0, 10.0, "0", 0.0),
1498 (0.0, 0.0, "5", 0.0), (0.0, 0.0, "-5", 0.0),
1500 (5.0, 5.0, "0", 5.0),
1501 ] {
1502 let state = RefAny::new(wrapper(0.0, min, max));
1503 let r = validate_text_input(state.clone(), info, text_state(text));
1504 assert_eq!(r.valid, TextInputValid::Yes, "{text:?} must be accepted");
1505 let after = read(&state);
1506 assert!(
1507 same(after.number, expected),
1508 "{text:?} in [{min}, {max}] stored {} (expected {expected})",
1509 after.number,
1510 );
1511 assert!(
1512 after.number >= min && after.number <= max,
1513 "{text:?} escaped [{min}, {max}] as {}",
1514 after.number,
1515 );
1516 }
1517 });
1518 }
1519
1520 #[test]
1521 fn validate_stores_nan_unclamped() {
1522 let state = RefAny::new(wrapper(0.0, -1.0, 1.0));
1527 let (r, after) = validate_one(&state, "NaN");
1528 assert_eq!(
1529 r.valid,
1530 TextInputValid::Yes,
1531 "the Rust float parser accepts \"NaN\", so the widget does too",
1532 );
1533 assert!(
1534 after.number.is_nan(),
1535 "NaN survives the clamp untouched, got {}",
1536 after.number,
1537 );
1538 }
1539
1540 #[test]
1541 fn validate_tracks_previous_as_the_last_accepted_value() {
1542 let state = RefAny::new(wrapper(0.0, 0.0, 10.0));
1543 with_info(|info| {
1544 for (text, previous, number) in [
1545 ("1", 0.0f32, 1.0f32),
1546 ("2", 1.0, 2.0),
1547 ("100", 2.0, 10.0), ("200", 10.0, 10.0), ("abc", 10.0, 10.0), ("-5", 10.0, 0.0),
1551 ] {
1552 let _ = validate_text_input(state.clone(), info, text_state(text));
1553 let after = read(&state);
1554 assert!(
1555 same(after.previous, previous),
1556 "after {text:?}: previous = {} (expected {previous})",
1557 after.previous,
1558 );
1559 assert!(
1560 same(after.number, number),
1561 "after {text:?}: number = {} (expected {number})",
1562 after.number,
1563 );
1564 }
1565 });
1566 }
1567
1568 #[test]
1576 fn validate_with_a_degenerate_range_must_not_panic() {
1577 let degenerate: [(f32, f32); 5] = [
1578 (10.0, 5.0),
1579 (1.0, -1.0),
1580 (f32::NAN, 10.0),
1581 (0.0, f32::NAN),
1582 (f32::NAN, f32::NAN),
1583 ];
1584
1585 let panicked: Vec<(f32, f32)> = degenerate
1586 .iter()
1587 .copied()
1588 .filter(|&(min, max)| {
1589 let state = RefAny::new(wrapper(0.0, min, max));
1590 catch_unwind(AssertUnwindSafe(|| {
1591 let _ = validate_one(&state, "1");
1592 }))
1593 .is_err()
1594 })
1595 .collect();
1596
1597 assert!(
1598 panicked.is_empty(),
1599 "typing a digit into a NumberInput whose [min, max] range is inverted or \
1600 NaN-bounded panics (f32::clamp asserts min <= max) instead of rejecting \
1601 the input; offending ranges: {panicked:?}",
1602 );
1603 }
1604
1605 #[test]
1610 fn validate_with_a_foreign_payload_accepts_the_edit_unchanged() {
1611 let state = RefAny::new(0u32);
1614 let r = with_info(|info| {
1615 validate_text_input(state.clone(), info, text_state("not a number"))
1616 });
1617 assert_eq!(r.update, Update::DoNothing);
1618 assert_eq!(r.valid, TextInputValid::Yes);
1619
1620 let mut state = state;
1621 assert_eq!(
1622 *state
1623 .downcast_ref::<u32>()
1624 .expect("the foreign payload must be left alone"),
1625 0,
1626 );
1627 }
1628
1629 #[test]
1630 fn validate_does_not_invoke_the_value_change_hook_for_rejected_input() {
1631 let recorder = RefAny::new(Recorder::new(Update::RefreshDom));
1632 let state = RefAny::new(wrapper_with_value_hook(
1633 1.0,
1634 f32::MIN,
1635 f32::MAX,
1636 &recorder,
1637 ));
1638
1639 with_info(|info| {
1640 for text in MALFORMED {
1641 let _ = validate_text_input(state.clone(), info, text_state(text));
1642 }
1643 });
1644 assert!(
1645 recorded(&recorder).is_empty(),
1646 "a rejected edit must not reach the user's hook",
1647 );
1648
1649 let (r, _) = validate_one(&state, "2");
1651 assert_eq!(r.update, Update::RefreshDom);
1652 assert_eq!(recorded(&recorder).len(), 1);
1653 }
1654
1655 #[test]
1656 fn validate_hands_the_hook_the_clamped_state_and_returns_its_update() {
1657 let recorder = RefAny::new(Recorder::new(Update::RefreshDomAllWindows));
1658 let state = RefAny::new(wrapper_with_value_hook(4.0, 0.0, 10.0, &recorder));
1659
1660 let (r, after) = validate_one(&state, "1000");
1661 assert_eq!(
1662 r.update,
1663 Update::RefreshDomAllWindows,
1664 "validate must forward the hook's Update verbatim",
1665 );
1666 assert_eq!(r.valid, TextInputValid::Yes);
1667
1668 let seen = recorded(&recorder);
1669 assert_eq!(seen.len(), 1);
1670 assert!(
1671 same(seen[0].number, 10.0),
1672 "the hook must see the clamped value, not the raw 1000, got {}",
1673 seen[0].number,
1674 );
1675 assert!(same(seen[0].previous, 4.0), "…and the previous value");
1676 assert_eq!(
1677 seen[0], after,
1678 "the hook's copy and the stored state must agree",
1679 );
1680 }
1681
1682 #[test]
1687 fn focus_lost_with_a_foreign_payload_does_nothing() {
1688 let state = RefAny::new(0u32);
1689 assert_eq!(focus_lost(&state, "123"), Update::DoNothing);
1690
1691 let mut state = state;
1692 assert_eq!(
1693 *state
1694 .downcast_ref::<u32>()
1695 .expect("the foreign payload must be left alone"),
1696 0,
1697 );
1698 }
1699
1700 #[test]
1701 fn focus_lost_without_a_hook_does_nothing() {
1702 let state = RefAny::new(wrapper(1.5, 0.0, 10.0));
1703 assert_eq!(focus_lost(&state, "123"), Update::DoNothing);
1704 let after = read(&state);
1705 assert!(same(after.number, 1.5) && same(after.previous, 0.0));
1706 }
1707
1708 #[test]
1709 fn focus_lost_reports_the_stored_number_and_ignores_the_text_buffer() {
1710 let recorder = RefAny::new(Recorder::new(Update::RefreshDom));
1714 let state = RefAny::new(wrapper_with_focus_hook(1.5, 0.0, 10.0, &recorder));
1715
1716 assert_eq!(
1717 focus_lost(&state, "999"),
1718 Update::RefreshDom,
1719 "the hook's Update must be forwarded verbatim",
1720 );
1721
1722 let seen = recorded(&recorder);
1723 assert_eq!(seen.len(), 1, "the hook must fire exactly once");
1724 assert!(
1725 same(seen[0].number, 1.5),
1726 "the hook saw {} — the text buffer must not be re-parsed",
1727 seen[0].number,
1728 );
1729 assert!(same(seen[0].previous, 0.0));
1730 }
1731
1732 #[test]
1733 fn focus_lost_neither_mutates_nor_clamps() {
1734 let recorder = RefAny::new(Recorder::new(Update::DoNothing));
1737 let state = RefAny::new(wrapper_with_focus_hook(1000.0, 0.0, 10.0, &recorder));
1738
1739 assert_eq!(focus_lost(&state, ""), Update::DoNothing);
1740
1741 let seen = recorded(&recorder);
1742 assert_eq!(seen.len(), 1);
1743 assert!(
1744 same(seen[0].number, 1000.0),
1745 "focus-lost must not clamp, got {}",
1746 seen[0].number,
1747 );
1748
1749 let after = read(&state);
1750 assert!(
1751 same(after.number, 1000.0) && same(after.previous, 0.0),
1752 "focus-lost must not mutate the state",
1753 );
1754 }
1755
1756 #[test]
1757 fn focus_lost_is_repeatable() {
1758 let recorder = RefAny::new(Recorder::new(Update::DoNothing));
1759 let state = RefAny::new(wrapper_with_focus_hook(2.5, 0.0, 10.0, &recorder));
1760
1761 for _ in 0..8 {
1762 assert_eq!(focus_lost(&state, "2.5"), Update::DoNothing);
1763 }
1764
1765 let seen = recorded(&recorder);
1766 assert_eq!(seen.len(), 8, "every focus loss must reach the hook");
1767 assert!(
1768 seen.iter().all(|s| same(s.number, 2.5)),
1769 "repeated focus losses must keep reporting the same value",
1770 );
1771 }
1772}