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};
15use azul_css::{
16    dynamic_selector::CssPropertyWithConditionsVec,
17    props::{
18        basic::*,
19        layout::*,
20        property::{CssProperty, *},
21        style::*,
22    },
23    *,
24};
25
26use crate::{
27    callbacks::{Callback, CallbackInfo},
28    widgets::text_input::{
29        OnTextInputReturn, TextInput, TextInputOnFocusLostCallback,
30        TextInputOnFocusLostCallbackType, TextInputOnTextInputCallback,
31        TextInputOnTextInputCallbackType, TextInputOnVirtualKeyDownCallback,
32        TextInputOnVirtualKeyDownCallbackType, TextInputState, TextInputValid,
33    },
34};
35
36/// Callback type invoked when the numeric value changes.
37pub type NumberInputOnValueChangeCallbackType =
38    extern "C" fn(RefAny, CallbackInfo, NumberInputState) -> Update;
39impl_widget_callback!(
40    NumberInputOnValueChange,
41    OptionNumberInputOnValueChange,
42    NumberInputOnValueChangeCallback,
43    NumberInputOnValueChangeCallbackType
44);
45
46azul_core::impl_managed_callback! {
47    wrapper:        NumberInputOnValueChangeCallback,
48    info_ty:        CallbackInfo,
49    return_ty:      Update,
50    default_ret:    Update::DoNothing,
51    invoker_static: NUMBER_INPUT_ON_VALUE_CHANGE_INVOKER,
52    invoker_ty:     AzNumberInputOnValueChangeCallbackInvoker,
53    thunk_fn:       az_number_input_on_value_change_callback_thunk,
54    setter_fn:      AzApp_setNumberInputOnValueChangeCallbackInvoker,
55    from_handle_fn: AzNumberInputOnValueChangeCallback_createFromHostHandle,
56    extra_args:     [ state: NumberInputState ],
57}
58
59/// Callback type invoked when the number input loses focus.
60pub type NumberInputOnFocusLostCallbackType =
61    extern "C" fn(RefAny, CallbackInfo, NumberInputState) -> Update;
62impl_widget_callback!(
63    NumberInputOnFocusLost,
64    OptionNumberInputOnFocusLost,
65    NumberInputOnFocusLostCallback,
66    NumberInputOnFocusLostCallbackType
67);
68
69azul_core::impl_managed_callback! {
70    wrapper:        NumberInputOnFocusLostCallback,
71    info_ty:        CallbackInfo,
72    return_ty:      Update,
73    default_ret:    Update::DoNothing,
74    invoker_static: NUMBER_INPUT_ON_FOCUS_LOST_INVOKER,
75    invoker_ty:     AzNumberInputOnFocusLostCallbackInvoker,
76    thunk_fn:       az_number_input_on_focus_lost_callback_thunk,
77    setter_fn:      AzApp_setNumberInputOnFocusLostCallbackInvoker,
78    from_handle_fn: AzNumberInputOnFocusLostCallback_createFromHostHandle,
79    extra_args:     [ state: NumberInputState ],
80}
81
82/// A numeric input widget that wraps `TextInput` with `f32` validation.
83#[derive(Debug, Default, Clone, PartialEq)]
84#[repr(C)]
85pub struct NumberInput {
86    pub number_input_state: NumberInputStateWrapper,
87    pub text_input: TextInput,
88    pub style: CssPropertyWithConditionsVec,
89}
90
91/// Wraps `NumberInputState` together with its value-change and focus-lost callbacks.
92#[derive(Debug, Default, Clone, PartialEq)]
93#[repr(C)]
94pub struct NumberInputStateWrapper {
95    pub inner: NumberInputState,
96    pub on_value_change: OptionNumberInputOnValueChange,
97    pub on_focus_lost: OptionNumberInputOnFocusLost,
98}
99
100/// State of a `NumberInput`: the current and previous value, plus allowed range.
101#[derive(Debug, Clone, PartialEq)]
102#[repr(C)]
103pub struct NumberInputState {
104    /// The value before the most recent change.
105    pub previous: f32,
106    /// The current numeric value.
107    pub number: f32,
108    /// Minimum allowed value (inclusive).
109    pub min: f32,
110    /// Maximum allowed value (inclusive).
111    pub max: f32,
112}
113
114impl Default for NumberInputState {
115    fn default() -> Self {
116        Self {
117            previous: 0.0,
118            number: 0.0,
119            min: core::f32::MIN,
120            max: core::f32::MAX,
121        }
122    }
123}
124
125impl NumberInput {
126    /// Creates a new `NumberInput` with the given initial value.
127    pub fn create(input: f32) -> Self {
128        Self {
129            number_input_state: NumberInputStateWrapper {
130                inner: NumberInputState {
131                    number: input,
132                    ..Default::default()
133                },
134                ..Default::default()
135            },
136            ..Default::default()
137        }
138    }
139
140    pub fn set_on_text_input<C: Into<TextInputOnTextInputCallback>>(
141        &mut self,
142        refany: RefAny,
143        callback: C,
144    ) {
145        self.text_input.set_on_text_input(refany, callback);
146    }
147
148    pub fn with_on_text_input<C: Into<TextInputOnTextInputCallback>>(
149        mut self,
150        refany: RefAny,
151        callback: C,
152    ) -> Self {
153        self.set_on_text_input(refany, callback);
154        self
155    }
156
157    pub fn set_on_virtual_key_down<C: Into<TextInputOnVirtualKeyDownCallback>>(
158        &mut self,
159        refany: RefAny,
160        callback: C,
161    ) {
162        self.text_input.set_on_virtual_key_down(refany, callback);
163    }
164
165    pub fn with_on_virtual_key_down<C: Into<TextInputOnVirtualKeyDownCallback>>(
166        mut self,
167        refany: RefAny,
168        callback: C,
169    ) -> Self {
170        self.set_on_virtual_key_down(refany, callback);
171        self
172    }
173
174    pub fn set_placeholder_style(&mut self, style: CssPropertyWithConditionsVec) {
175        self.text_input.placeholder_style = style;
176    }
177
178    pub fn with_placeholder_style(mut self, style: CssPropertyWithConditionsVec) -> Self {
179        self.set_placeholder_style(style);
180        self
181    }
182
183    pub fn set_container_style(&mut self, style: CssPropertyWithConditionsVec) {
184        self.text_input.container_style = style;
185    }
186
187    pub fn with_container_style(mut self, style: CssPropertyWithConditionsVec) -> Self {
188        self.set_container_style(style);
189        self
190    }
191
192    pub fn set_label_style(&mut self, style: CssPropertyWithConditionsVec) {
193        self.text_input.label_style = style;
194    }
195
196    pub fn with_label_style(mut self, style: CssPropertyWithConditionsVec) -> Self {
197        self.set_label_style(style);
198        self
199    }
200
201    // Function called when the input has been parsed as a number
202    pub fn set_on_value_change<C: Into<NumberInputOnValueChangeCallback>>(
203        &mut self,
204        refany: RefAny,
205        callback: C,
206    ) {
207        self.number_input_state.on_value_change = Some(NumberInputOnValueChange {
208            callback: callback.into(),
209            refany,
210        })
211        .into();
212    }
213
214    pub fn with_on_value_change<C: Into<NumberInputOnValueChangeCallback>>(
215        mut self,
216        refany: RefAny,
217        callback: C,
218    ) -> Self {
219        self.set_on_value_change(refany, callback);
220        self
221    }
222
223    pub fn set_on_focus_lost<C: Into<NumberInputOnFocusLostCallback>>(
224        &mut self,
225        refany: RefAny,
226        callback: C,
227    ) {
228        self.number_input_state.on_focus_lost = Some(NumberInputOnFocusLost {
229            callback: callback.into(),
230            refany,
231        })
232        .into();
233    }
234
235    pub fn with_on_focus_lost<C: Into<NumberInputOnFocusLostCallback>>(
236        mut self,
237        refany: RefAny,
238        callback: C,
239    ) -> Self {
240        self.set_on_focus_lost(refany, callback);
241        self
242    }
243
244    pub fn swap_with_default(&mut self) -> Self {
245        let mut s = Self::create(0.0);
246        core::mem::swap(&mut s, self);
247        s
248    }
249
250    pub fn dom(mut self) -> Dom {
251        let number_string = format!("{}", self.number_input_state.inner.number);
252        self.text_input.text_input_state.inner.text = number_string
253            .chars()
254            .map(|s| s as u32)
255            .collect::<Vec<_>>()
256            .into();
257
258        let state = RefAny::new(self.number_input_state);
259
260        self.text_input.set_on_text_input(
261            state.clone(),
262            validate_text_input as TextInputOnTextInputCallbackType,
263        );
264        self.text_input
265            .set_on_focus_lost(state, on_focus_lost as TextInputOnFocusLostCallbackType);
266        self.text_input.dom()
267    }
268}
269
270extern "C" fn on_focus_lost(
271    mut refany: RefAny,
272    info: CallbackInfo,
273    _state: TextInputState,
274) -> Update {
275    let mut refany = match refany.downcast_mut::<NumberInputStateWrapper>() {
276        Some(s) => s,
277        None => return Update::DoNothing,
278    };
279
280    let number_input = &mut *refany;
281    let onfocuslost = &mut number_input.on_focus_lost;
282    let inner = number_input.inner.clone();
283
284    match onfocuslost.as_mut() {
285        Some(NumberInputOnFocusLost { callback, refany }) => {
286            (callback.cb)(refany.clone(), info.clone(), inner)
287        }
288        None => Update::DoNothing,
289    }
290}
291
292extern "C" fn validate_text_input(
293    mut refany: RefAny,
294    info: CallbackInfo,
295    state: TextInputState,
296) -> OnTextInputReturn {
297    let mut refany = match refany.downcast_mut::<NumberInputStateWrapper>() {
298        Some(s) => s,
299        None => {
300            return OnTextInputReturn {
301                update: Update::DoNothing,
302                valid: TextInputValid::Yes,
303            };
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 validated_f32 = match validated_input.parse::<f32>() {
315        Ok(s) => s,
316        Err(_) => {
317            // do not re-layout the entire screen,
318            // but don't handle the character
319            return OnTextInputReturn {
320                update: Update::DoNothing,
321                valid: TextInputValid::No,
322            };
323        }
324    };
325
326    let number_input = &mut *refany;
327    let onvaluechange = &mut number_input.on_value_change;
328    let inner = &mut number_input.inner;
329
330    inner.previous = inner.number;
331    let clamped = validated_f32.clamp(inner.min, inner.max);
332    inner.number = clamped;
333    let inner_clone = inner.clone();
334
335    let update = match onvaluechange.as_mut() {
336        Some(NumberInputOnValueChange { callback, refany }) => {
337            (callback.cb)(refany.clone(), info.clone(), inner_clone)
338        }
339        None => Update::DoNothing,
340    };
341
342    OnTextInputReturn {
343        update,
344        valid: TextInputValid::Yes,
345    }
346}