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

            
8
use std::string::String;
9

            
10
use azul_core::{
11
    callbacks::{CoreCallbackData, Update},
12
    dom::Dom,
13
    refany::RefAny,
14
};
15
use azul_css::{
16
    dynamic_selector::CssPropertyWithConditionsVec,
17
    props::{
18
        basic::*,
19
        layout::*,
20
        property::{CssProperty, *},
21
        style::*,
22
    },
23
    *,
24
};
25

            
26
use 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.
37
pub type NumberInputOnValueChangeCallbackType =
38
    extern "C" fn(RefAny, CallbackInfo, NumberInputState) -> Update;
39
impl_widget_callback!(
40
    NumberInputOnValueChange,
41
    OptionNumberInputOnValueChange,
42
    NumberInputOnValueChangeCallback,
43
    NumberInputOnValueChangeCallbackType
44
);
45

            
46
azul_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.
60
pub type NumberInputOnFocusLostCallbackType =
61
    extern "C" fn(RefAny, CallbackInfo, NumberInputState) -> Update;
62
impl_widget_callback!(
63
    NumberInputOnFocusLost,
64
    OptionNumberInputOnFocusLost,
65
    NumberInputOnFocusLostCallback,
66
    NumberInputOnFocusLostCallbackType
67
);
68

            
69
azul_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)]
85
pub 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)]
94
pub 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)]
103
pub 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

            
114
impl 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

            
125
impl 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

            
270
extern "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

            
292
extern "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
}