Skip to main content

azul_layout/widgets/
number_input.rs

1//! Numeric input widget that wraps `TextInput` with numeric validation.
2//!
3//! Exports `NumberInput`, `NumberInputState`, and callback types
4//! (`NumberInputOnValueChangeCallbackType`, `NumberInputOnFocusLostCallbackType`).
5//! Internally delegates to `TextInput` and validates that the entered text
6//! parses as an `f32` within the configured `min`/`max` range.
7
8use std::string::String;
9
10use azul_core::{
11    callbacks::{CoreCallbackData, Update},
12    dom::Dom,
13    refany::RefAny,
14};
15#[allow(clippy::wildcard_imports)] // widget/render module pulls in the css property/value types it builds with
16use 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
37/// Callback type invoked when the numeric value changes.
38pub 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
60/// Callback type invoked when the number input loses focus.
61pub 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/// A numeric input widget that wraps `TextInput` with `f32` validation.
84#[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/// Wraps `NumberInputState` together with its value-change and focus-lost callbacks.
93#[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/// State of a `NumberInput`: the current and previous value, plus allowed range.
102#[derive(Copy, Debug, Clone, PartialEq)]
103#[repr(C)]
104pub struct NumberInputState {
105    /// The value before the most recent change.
106    pub previous: f32,
107    /// The current numeric value.
108    pub number: f32,
109    /// Minimum allowed value (inclusive).
110    pub min: f32,
111    /// Maximum allowed value (inclusive).
112    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    /// Creates a new `NumberInput` with the given initial value.
128    #[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    // Function called when the input has been parsed as a number
205    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
295extern "C" fn validate_text_input(
296    mut refany: RefAny,
297    info: CallbackInfo,
298    state: TextInputState,
299) -> OnTextInputReturn {
300    let Some(mut refany) = refany.downcast_mut::<NumberInputStateWrapper>() else {
301        return OnTextInputReturn {
302            update: Update::DoNothing,
303            valid: TextInputValid::Yes,
304        };
305    };
306
307    let validated_input: String = state
308        .text
309        .iter()
310        .filter_map(|c| core::char::from_u32(*c))
311        .map(|c| if c == ',' { '.' } else { c })
312        .collect();
313
314    let Ok(validated_f32) = validated_input.parse::<f32>() else {
315        // do not re-layout the entire screen,
316        // but don't handle the character
317        return OnTextInputReturn {
318            update: Update::DoNothing,
319            valid: TextInputValid::No,
320        };
321    };
322
323    let number_input = &mut *refany;
324    let onvaluechange = &mut number_input.on_value_change;
325    let inner = &mut number_input.inner;
326
327    inner.previous = inner.number;
328    let clamped = validated_f32.clamp(inner.min, inner.max);
329    inner.number = clamped;
330    let inner_clone = *inner;
331
332    let update = match onvaluechange.as_mut() {
333        Some(NumberInputOnValueChange { callback, refany }) => {
334            (callback.cb)(refany.clone(), info, inner_clone)
335        }
336        None => Update::DoNothing,
337    };
338
339    OnTextInputReturn {
340        update,
341        valid: TextInputValid::Yes,
342    }
343}