Skip to main content

fyrox_ui/
numeric.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! A widget that handles numbers of any machine type. See [`NumericUpDown`] docs for more info and usage examples.
22
23#![warn(missing_docs)]
24
25use crate::style::resource::StyleResourceExt;
26use crate::style::Style;
27use crate::{
28    border::BorderBuilder,
29    button::{ButtonBuilder, ButtonMessage},
30    core::{
31        num_traits::{clamp, Bounded, NumAssign, NumCast, NumOps},
32        pool::Handle,
33        reflect::{prelude::*, Reflect},
34        type_traits::prelude::*,
35        visitor::prelude::*,
36    },
37    decorator::DecoratorBuilder,
38    grid::{Column, GridBuilder, Row},
39    message::{KeyCode, MessageDirection, MouseButton, UiMessage},
40    text::TextMessage,
41    text_box::{TextBox, TextBoxBuilder, TextCommitMode},
42    utils::{make_arrow, ArrowDirection},
43    widget::{Widget, WidgetBuilder, WidgetMessage},
44    BuildContext, Control, HorizontalAlignment, Thickness, UiNode, UserInterface,
45    VerticalAlignment,
46};
47
48use crate::button::Button;
49use crate::message::MessageData;
50use fyrox_core::variable::InheritableVariable;
51use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
52use fyrox_graph::SceneGraph;
53use std::{
54    cmp::Ordering,
55    fmt::{Debug, Display},
56    ops::{Deref, DerefMut},
57    str::FromStr,
58};
59
60/// Numeric type is a trait, that has all required traits of a number type. It is used as a useful abstraction over
61/// all machine numeric types.
62pub trait NumericType:
63    NumAssign
64    + FromStr
65    + Clone
66    + Copy
67    + NumOps
68    + PartialOrd
69    + Display
70    + Bounded
71    + Debug
72    + Send
73    + Sync
74    + NumCast
75    + Default
76    + Reflect
77    + Visit
78    + TypeUuidProvider
79    + 'static
80{
81}
82
83impl<T> NumericType for T where
84    T: NumAssign
85        + FromStr
86        + Clone
87        + Copy
88        + NumOps
89        + PartialOrd
90        + Bounded
91        + Display
92        + Debug
93        + Send
94        + Sync
95        + NumCast
96        + Default
97        + Reflect
98        + Visit
99        + TypeUuidProvider
100        + 'static
101{
102}
103
104/// A set of messages that can be used to modify [`NumericUpDown`] widget state (with [`MessageDirection::ToWidget`], or to
105/// fetch changes from it (with [`MessageDirection::FromWidget`]).
106#[derive(Debug, Clone, PartialEq, Eq)]
107pub enum NumericUpDownMessage<T: NumericType> {
108    /// Used to set new value of the [`NumericUpDown`] widget (with [`MessageDirection::ToWidget`] direction). Also emitted by the widget
109    /// automatically when the new value is set (with [`MessageDirection::FromWidget`]).
110    Value(T),
111    /// Used to set min value of the [`NumericUpDown`] widget (with [`MessageDirection::ToWidget`] direction). Also emitted by the widget
112    /// automatically when the new min value is set (with [`MessageDirection::FromWidget`]).
113    MinValue(T),
114    /// Used to set max value of the [`NumericUpDown`] widget (with [`MessageDirection::ToWidget`] direction). Also emitted by the widget
115    /// automatically when the new max value is set (with [`MessageDirection::FromWidget`]).
116    MaxValue(T),
117    /// Used to set new step of the [`NumericUpDown`] widget (with [`MessageDirection::ToWidget`] direction). Also emitted by the widget
118    /// automatically when the new step is set (with [`MessageDirection::FromWidget`]).
119    Step(T),
120    /// Used to set new precision of the [`NumericUpDown`] widget (with [`MessageDirection::ToWidget`] direction). Also emitted by the widget
121    /// automatically when the new precision is set (with [`MessageDirection::FromWidget`]).
122    Precision(usize),
123}
124impl<T: NumericType> MessageData for NumericUpDownMessage<T> {}
125
126/// Used to store drag info when dragging the cursor on the up/down buttons.
127#[derive(Clone, Debug)]
128pub enum DragContext<T: NumericType> {
129    /// Dragging is just started.
130    PreDrag {
131        /// Initial mouse position in Y axis.
132        start_mouse_pos: f32,
133    },
134    /// Dragging is active.
135    Dragging {
136        /// Start value of the [`NumericUpDown`] widget.
137        start_value: T,
138        /// Initial mouse position in Y axis.
139        start_mouse_pos: f32,
140    },
141}
142
143/// A widget that handles numbers of any machine type. Use this widget if you need to provide an input field for a numeric
144/// type.
145///
146/// ## How to create
147///
148/// Use [`NumericUpDownBuilder`] to create a new instance of the [`NumericUpDown`] widget:
149///
150/// ```rust
151/// # use fyrox_ui::{
152/// #     core::pool::Handle, numeric::NumericUpDownBuilder, widget::WidgetBuilder, BuildContext,
153/// #     UiNode,
154/// # };
155/// # use fyrox_ui::numeric::NumericUpDown;
156///
157/// fn create_numeric_widget(ctx: &mut BuildContext) -> Handle<NumericUpDown<f32>> {
158///     NumericUpDownBuilder::new(WidgetBuilder::new())
159///         .with_value(123.0f32)
160///         .build(ctx)
161/// }
162/// ```
163///
164/// Keep in mind, that this widget is generic and can work with any numeric types. Sometimes you might get an "unknown type"
165/// error message from the compiler (especially if you use `123.0` ambiguous numeric literals), in this case, you need to
166/// specify the type explicitly (`NumericUpDownBuilder::<f32>::new...`).
167///
168/// ## Limits
169///
170/// This widget supports lower and upper limits for the values. It can be specified by [`NumericUpDownBuilder::with_min_value`]
171/// and [`NumericUpDownBuilder::with_max_value`] (or changed at runtime using [`NumericUpDownMessage::MinValue`] and [`NumericUpDownMessage::MaxValue`]
172/// messages):
173///
174/// ```rust
175/// # use fyrox_ui::{
176/// #     core::pool::Handle, numeric::NumericUpDownBuilder, widget::WidgetBuilder, BuildContext,
177/// #     UiNode,
178/// # };
179/// # use fyrox_ui::numeric::NumericUpDown;
180///
181/// fn create_numeric_widget(ctx: &mut BuildContext) -> Handle<NumericUpDown<f32>> {
182///     NumericUpDownBuilder::new(WidgetBuilder::new())
183///         .with_value(123.0f32)
184///         .with_min_value(42.0)
185///         .with_max_value(666.0)
186///         .build(ctx)
187/// }
188/// ```
189///
190/// The default limits for min and max are [NumericType::min_value] and [NumericType::max_value] respectively.
191///
192/// [NumericType::min_value]: Bounded::min_value
193/// [NumericType::max_value]: Bounded::max_value
194///
195/// ## Step
196///
197/// Since the value of the widget can be changed via up/down arrow buttons (also by dragging the cursor up or down on them), the widget
198/// provides a way to set the step of the value (for increment and decrement at the same time):
199///
200/// ```rust
201/// # use fyrox_ui::{
202/// #     core::pool::Handle, numeric::NumericUpDownBuilder, widget::WidgetBuilder, BuildContext,
203/// #     UiNode,
204/// # };
205/// # use fyrox_ui::numeric::NumericUpDown;
206///
207/// fn create_numeric_widget(ctx: &mut BuildContext) -> Handle<NumericUpDown<f32>> {
208///     NumericUpDownBuilder::new(WidgetBuilder::new())
209///         .with_value(125.0f32)
210///         .with_step(5.0)
211///         .build(ctx)
212/// }
213/// ```
214///
215/// The default value of the step is [NumericType::one].
216///
217/// [NumericType::one]: crate::core::num_traits::One::one
218///
219/// ## Precision
220///
221/// It is possible to specify **visual** rounding of the value up to desired decimal place (it does not change the way how
222/// the actual value is rounded). For example, in some cases, you might get irrational values such as `1/3 ~= 0.33333333`,
223/// but you are interested in only the first two decimal places. In this case you can set the precision to `2`:
224///
225/// ```rust
226/// # use fyrox_ui::{
227/// #     core::pool::Handle, numeric::NumericUpDownBuilder, widget::WidgetBuilder, BuildContext,
228/// #     UiNode,
229/// # };
230/// # use fyrox_ui::numeric::NumericUpDown;
231///
232/// fn create_numeric_widget(ctx: &mut BuildContext) -> Handle<NumericUpDown<f32>> {
233///     NumericUpDownBuilder::new(WidgetBuilder::new())
234///         .with_value(0.3333333f32)
235///         .with_precision(2)
236///         .build(ctx)
237/// }
238/// ```
239#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
240#[reflect(derived_type = "UiNode")]
241pub struct NumericUpDown<T: NumericType> {
242    /// Base widget of the [`NumericUpDown`] widget.
243    pub widget: Widget,
244    /// A handle of the input field (usually a [`TextBox`] instance).
245    pub field: InheritableVariable<Handle<TextBox>>,
246    /// A handle of the increase button.
247    pub increase: InheritableVariable<Handle<Button>>,
248    /// A handle of the decrease button.
249    pub decrease: InheritableVariable<Handle<Button>>,
250    /// Current value of the widget.
251    pub value: InheritableVariable<T>,
252    /// Value of the widget with formatting applied.
253    /// This value comes from parsing the result of format! so it has limited precision
254    /// and is used to determine if the value has been changed by text editing.
255    #[visit(skip)]
256    #[reflect(hidden)]
257    formatted_value: T,
258    /// Step value of the widget.
259    pub step: InheritableVariable<T>,
260    /// Min value of the widget.
261    pub min_value: InheritableVariable<T>,
262    /// Max value of the widget.
263    pub max_value: InheritableVariable<T>,
264    /// Current precision of the widget in decimal places.
265    pub precision: InheritableVariable<usize>,
266    /// Internal dragging context.
267    #[visit(skip)]
268    #[reflect(hidden)]
269    pub drag_context: Option<DragContext<T>>,
270    /// Defines how movement in Y axis will be translated in the actual value change. It is some sort of scaling modifier.
271    pub drag_value_scaling: InheritableVariable<f32>,
272}
273
274impl<T: NumericType> ConstructorProvider<UiNode, UserInterface> for NumericUpDown<T> {
275    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
276        GraphNodeConstructor::new::<Self>()
277            .with_variant(
278                format!("Numeric Up Down<{}>", std::any::type_name::<T>()),
279                |ui| {
280                    NumericUpDownBuilder::<T>::new(
281                        WidgetBuilder::new().with_name("Numeric Up Down"),
282                    )
283                    .build(&mut ui.build_ctx())
284                    .to_base()
285                    .into()
286                },
287            )
288            .with_group("Numeric")
289    }
290}
291
292impl<T: NumericType> Deref for NumericUpDown<T> {
293    type Target = Widget;
294
295    fn deref(&self) -> &Self::Target {
296        &self.widget
297    }
298}
299
300impl<T: NumericType> DerefMut for NumericUpDown<T> {
301    fn deref_mut(&mut self) -> &mut Self::Target {
302        &mut self.widget
303    }
304}
305
306impl<T: NumericType> NumericUpDown<T> {
307    fn clamp_value(&self, value: T) -> T {
308        clamp(value, *self.min_value, *self.max_value)
309    }
310
311    fn sync_text_field(&mut self, ui: &UserInterface) {
312        let text = format!("{:.1$}", *self.value, *self.precision);
313        self.formatted_value = text.parse::<T>().unwrap_or(*self.value);
314        let msg = UiMessage::for_widget(
315            *self.field,
316            TextMessage::Text(format!("{:.1$}", *self.value, *self.precision)),
317        );
318        msg.set_handled(true);
319        ui.send_message(msg);
320    }
321
322    fn sync_value_to_bounds_if_needed(&self, ui: &UserInterface) {
323        let clamped = self.clamp_value(*self.value);
324        if *self.value != clamped {
325            ui.send(self.handle, NumericUpDownMessage::Value(clamped));
326        }
327    }
328
329    fn try_parse_value(&mut self, ui: &UserInterface) {
330        // Parse input only when focus is lost from text field.
331        if let Ok(field) = ui.try_get(*self.field) {
332            if let Ok(value) = field.text().parse::<T>() {
333                // If the value we got from the text box has changed since the last time
334                // we parsed it, then the value has been edited through the text box,
335                // and the change was meaningful enough to change the result of parsing.
336                if value != self.formatted_value {
337                    self.formatted_value = value;
338                    let value = self.clamp_value(value);
339                    ui.send(self.handle(), NumericUpDownMessage::Value(value));
340                }
341            } else {
342                // Inform the user that parsing failed by re-establishing a valid value.
343                self.sync_text_field(ui);
344            }
345        }
346    }
347}
348
349fn saturating_sub<T>(a: T, b: T) -> T
350where
351    T: NumericType,
352{
353    assert!(b >= T::zero());
354
355    if a >= b + T::min_value() {
356        a - b
357    } else {
358        T::min_value()
359    }
360}
361
362fn saturating_add<T>(a: T, b: T) -> T
363where
364    T: NumericType,
365{
366    assert!(b >= T::zero());
367
368    if a < T::max_value() - b {
369        a + b
370    } else {
371        T::max_value()
372    }
373}
374
375fn calculate_value_by_offset<T: NumericType>(
376    start_value: T,
377    offset: i32,
378    step: T,
379    min: T,
380    max: T,
381) -> T {
382    let mut new_value = start_value;
383    match offset.cmp(&0) {
384        Ordering::Less => {
385            for _ in 0..(-offset) {
386                new_value = saturating_sub(new_value, step);
387            }
388        }
389        Ordering::Equal => {}
390        Ordering::Greater => {
391            for _ in 0..offset {
392                new_value = saturating_add(new_value, step);
393            }
394        }
395    }
396    new_value = clamp(new_value, min, max);
397    new_value
398}
399
400impl<T> TypeUuidProvider for NumericUpDown<T>
401where
402    T: NumericType,
403{
404    fn type_uuid() -> Uuid {
405        combine_uuids(
406            uuid!("f852eda4-18e5-4480-83ae-a607ce1c26f7"),
407            T::type_uuid(),
408        )
409    }
410}
411
412impl<T: NumericType> Control for NumericUpDown<T> {
413    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
414        self.widget.handle_routed_message(ui, message);
415
416        if let Some(TextMessage::Text(_)) = message.data() {
417            if message.destination() == *self.field
418                && message.direction == MessageDirection::FromWidget
419                && !message.handled()
420            {
421                self.try_parse_value(ui);
422            }
423        } else if let Some(msg) = message.data::<WidgetMessage>() {
424            match msg {
425                WidgetMessage::MouseDown { button, pos, .. } => {
426                    // We can activate dragging either by clicking on increase or decrease buttons.
427                    if *button == MouseButton::Left
428                        && (ui[*self.increase].has_descendant(message.destination(), ui)
429                            || ui[*self.decrease].has_descendant(message.destination(), ui))
430                    {
431                        self.drag_context = Some(DragContext::PreDrag {
432                            start_mouse_pos: pos.y,
433                        });
434                    }
435                }
436                WidgetMessage::MouseMove { pos, .. } => {
437                    if let Some(drag_context) = self.drag_context.as_ref() {
438                        match drag_context {
439                            DragContext::PreDrag { start_mouse_pos } => {
440                                if (pos.y - start_mouse_pos).abs() >= 5.0 {
441                                    self.drag_context = Some(DragContext::Dragging {
442                                        start_value: *self.value,
443                                        start_mouse_pos: *start_mouse_pos,
444                                    });
445                                }
446                            }
447                            DragContext::Dragging {
448                                start_value,
449                                start_mouse_pos,
450                            } => {
451                                // Just change visual value while dragging; do not touch actual value.
452                                ui.send(
453                                    *self.field,
454                                    TextMessage::Text(format!(
455                                        "{:.1$}",
456                                        calculate_value_by_offset(
457                                            *start_value,
458                                            ((*start_mouse_pos - pos.y) * *self.drag_value_scaling)
459                                                as i32,
460                                            *self.step,
461                                            *self.min_value,
462                                            *self.max_value
463                                        ),
464                                        *self.precision
465                                    )),
466                                );
467                            }
468                        }
469                    }
470                }
471                WidgetMessage::KeyDown(key_code) => match *key_code {
472                    KeyCode::ArrowUp => {
473                        ui.post(*self.increase, ButtonMessage::Click);
474                    }
475                    KeyCode::ArrowDown => {
476                        ui.post(*self.decrease, ButtonMessage::Click);
477                    }
478                    _ => (),
479                },
480                _ => {}
481            }
482        } else if let Some(msg) = message.data_for::<NumericUpDownMessage<T>>(self.handle()) {
483            match msg {
484                NumericUpDownMessage::Value(value) => {
485                    // Stupid clippy does not understand that NaN != NaN by definition.
486                    #[allow(clippy::eq_op)]
487                    fn nan_aware_eq<N: NumericType>(lhs: N, rhs: N) -> bool {
488                        let lhs_nan = !(lhs == lhs);
489                        let rhs_nan = !(rhs == rhs);
490                        if lhs_nan && rhs_nan {
491                            true
492                        } else if (lhs_nan && !rhs_nan) || (!lhs_nan && rhs_nan) {
493                            false
494                        } else {
495                            lhs == rhs
496                        }
497                    }
498
499                    let clamped = self.clamp_value(*value);
500                    if !nan_aware_eq(*self.value, clamped) {
501                        self.value.set_value_and_mark_modified(clamped);
502
503                        self.sync_text_field(ui);
504
505                        let mut msg = UiMessage::from_widget(
506                            self.handle,
507                            NumericUpDownMessage::Value(*self.value),
508                        );
509                        // We must maintain flags
510                        msg.set_handled(message.handled());
511                        msg.flags = message.flags;
512                        msg.delivery_mode = message.delivery_mode;
513                        ui.send_message(msg);
514                    }
515                }
516                NumericUpDownMessage::MinValue(min_value) => {
517                    if (*self.min_value).ne(min_value) {
518                        self.min_value.set_value_and_mark_modified(*min_value);
519                        ui.send_message(message.reverse());
520                        self.sync_value_to_bounds_if_needed(ui);
521                    }
522                }
523                NumericUpDownMessage::MaxValue(max_value) => {
524                    if (*self.max_value).ne(max_value) {
525                        self.max_value.set_value_and_mark_modified(*max_value);
526                        ui.send_message(message.reverse());
527                        self.sync_value_to_bounds_if_needed(ui);
528                    }
529                }
530                NumericUpDownMessage::Step(step) => {
531                    if (*self.step).ne(step) {
532                        self.step.set_value_and_mark_modified(*step);
533                        ui.send_message(message.reverse());
534                        self.sync_text_field(ui);
535                    }
536                }
537                NumericUpDownMessage::Precision(precision) => {
538                    if (*self.precision).ne(precision) {
539                        self.precision.set_value_and_mark_modified(*precision);
540                        ui.send_message(message.reverse());
541                        self.sync_text_field(ui);
542                    }
543                }
544            }
545        } else if let Some(ButtonMessage::Click) = message.data::<ButtonMessage>() {
546            if message.destination() == *self.decrease || message.destination() == *self.increase {
547                if let Some(DragContext::Dragging {
548                    start_value,
549                    start_mouse_pos,
550                }) = self.drag_context.take()
551                {
552                    ui.send(
553                        self.handle,
554                        NumericUpDownMessage::Value(calculate_value_by_offset(
555                            start_value,
556                            ((start_mouse_pos - ui.cursor_position().y) * *self.drag_value_scaling)
557                                as i32,
558                            *self.step,
559                            *self.min_value,
560                            *self.max_value,
561                        )),
562                    );
563                } else if message.destination() == *self.decrease {
564                    let value = self.clamp_value(saturating_sub(*self.value, *self.step));
565                    ui.send(self.handle(), NumericUpDownMessage::Value(value));
566                } else if message.destination() == *self.increase {
567                    let value = self.clamp_value(saturating_add(*self.value, *self.step));
568                    ui.send(self.handle(), NumericUpDownMessage::Value(value));
569                }
570            }
571        }
572    }
573}
574
575/// This builder creates new instances of [`NumericUpDown`] widget and adds them to the user interface.
576pub struct NumericUpDownBuilder<T: NumericType> {
577    widget_builder: WidgetBuilder,
578    value: T,
579    step: T,
580    min_value: T,
581    max_value: T,
582    precision: usize,
583    editable: bool,
584    drag_value_scaling: f32,
585}
586
587fn make_button(
588    ctx: &mut BuildContext,
589    arrow: ArrowDirection,
590    row: usize,
591    editable: bool,
592) -> Handle<Button> {
593    let handle = ButtonBuilder::new(
594        WidgetBuilder::new()
595            .with_enabled(editable)
596            .with_margin(Thickness::right(1.0))
597            .on_row(row),
598    )
599    .with_back(
600        DecoratorBuilder::new(
601            BorderBuilder::new(
602                WidgetBuilder::new().with_foreground(ctx.style.property(Style::BRUSH_LIGHTER)),
603            )
604            .with_corner_radius(2.0f32.into())
605            .with_pad_by_corner_radius(false),
606        )
607        .with_normal_brush(ctx.style.property(Style::BRUSH_PRIMARY))
608        .with_hover_brush(ctx.style.property(Style::BRUSH_LIGHT))
609        .with_pressed_brush(ctx.style.property(Style::BRUSH_BRIGHT_BLUE))
610        .build(ctx),
611    )
612    .with_content(make_arrow(ctx, arrow, 6.0))
613    .build(ctx);
614
615    // Disable unwanted potential tab navigation for the buttons.
616    ctx[handle].accepts_input = false;
617
618    handle
619}
620
621impl<T: NumericType> NumericUpDownBuilder<T> {
622    /// Creates new builder instance with the base widget builder specified.
623    pub fn new(widget_builder: WidgetBuilder) -> Self {
624        Self {
625            widget_builder,
626            value: T::zero(),
627            step: T::one(),
628            min_value: T::min_value(),
629            max_value: T::max_value(),
630            precision: 3,
631            editable: true,
632            drag_value_scaling: 0.1,
633        }
634    }
635
636    fn set_value(&mut self, value: T) {
637        self.value = clamp(value, self.min_value, self.max_value);
638    }
639
640    /// Sets the desired min value.
641    pub fn with_min_value(mut self, value: T) -> Self {
642        self.min_value = value;
643        self.set_value(self.value);
644        self
645    }
646
647    /// Sets the desired max value.
648    pub fn with_max_value(mut self, value: T) -> Self {
649        self.max_value = value;
650        self.set_value(self.value);
651        self
652    }
653
654    /// Sets the desired value.
655    pub fn with_value(mut self, value: T) -> Self {
656        self.value = value;
657        self.set_value(value);
658        self
659    }
660
661    /// Sets the desired step.
662    pub fn with_step(mut self, step: T) -> Self {
663        assert!(step >= T::zero());
664
665        self.step = step;
666        self
667    }
668
669    /// Sets the desired precision.
670    pub fn with_precision(mut self, precision: usize) -> Self {
671        self.precision = precision;
672        self
673    }
674
675    /// Enables or disables editing of the widget.
676    pub fn with_editable(mut self, editable: bool) -> Self {
677        self.editable = editable;
678        self
679    }
680
681    /// Sets the desired value scaling when dragging. It scales cursor movement value (along Y axis) and multiplies it to get
682    /// the new value.
683    pub fn with_drag_value_scaling(mut self, drag_value_scaling: f32) -> Self {
684        self.drag_value_scaling = drag_value_scaling;
685        self
686    }
687
688    /// Finishes [`NumericUpDown`] widget creation and adds the new instance to the user interface and returns a handle to it.
689    pub fn build(self, ctx: &mut BuildContext) -> Handle<NumericUpDown<T>> {
690        let increase;
691        let decrease;
692        let field;
693        let back = BorderBuilder::new(
694            WidgetBuilder::new()
695                .with_background(ctx.style.property(Style::BRUSH_DARK))
696                .with_foreground(ctx.style.property(Style::BRUSH_LIGHT)),
697        )
698        .with_corner_radius(4.0f32.into())
699        .with_pad_by_corner_radius(false)
700        .with_stroke_thickness(Thickness::uniform(1.0).into())
701        .build(ctx);
702
703        let text = format!("{:.1$}", self.value, self.precision);
704        let formatted_value = text.parse::<T>().unwrap_or(self.value);
705        let grid = GridBuilder::new(
706            WidgetBuilder::new()
707                .with_child({
708                    field = TextBoxBuilder::new(
709                        WidgetBuilder::new()
710                            .on_row(0)
711                            .on_column(0)
712                            .with_margin(Thickness::left(2.0)),
713                    )
714                    .with_vertical_text_alignment(VerticalAlignment::Center)
715                    .with_horizontal_text_alignment(HorizontalAlignment::Left)
716                    .with_text_commit_mode(TextCommitMode::Changed)
717                    .with_text(text)
718                    .with_editable(self.editable)
719                    .build(ctx);
720                    field
721                })
722                .with_child(
723                    GridBuilder::new(
724                        WidgetBuilder::new()
725                            .on_column(1)
726                            .with_child({
727                                increase = make_button(ctx, ArrowDirection::Top, 0, self.editable);
728                                increase
729                            })
730                            .with_child({
731                                decrease =
732                                    make_button(ctx, ArrowDirection::Bottom, 1, self.editable);
733                                decrease
734                            }),
735                    )
736                    .add_column(Column::auto())
737                    .add_row(Row::stretch())
738                    .add_row(Row::stretch())
739                    .build(ctx),
740                ),
741        )
742        .add_row(Row::stretch())
743        .add_column(Column::stretch())
744        .add_column(Column::auto())
745        .build(ctx);
746
747        ctx.link(grid, back);
748
749        let node = NumericUpDown {
750            widget: self.widget_builder.with_child(back).build(ctx),
751            increase: increase.into(),
752            decrease: decrease.into(),
753            field: field.into(),
754            value: self.value.into(),
755            formatted_value,
756            step: self.step.into(),
757            min_value: self.min_value.into(),
758            max_value: self.max_value.into(),
759            precision: self.precision.into(),
760            drag_context: None,
761            drag_value_scaling: self.drag_value_scaling.into(),
762        };
763
764        ctx.add(node)
765    }
766}
767
768#[cfg(test)]
769mod test {
770
771    use crate::numeric::NumericUpDownBuilder;
772    use crate::{
773        numeric::{saturating_add, saturating_sub},
774        test::test_widget_deletion,
775        widget::WidgetBuilder,
776    };
777
778    #[test]
779    fn test_saturating_add() {
780        // i32
781        assert_eq!(saturating_add(0, 1), 1);
782        assert_eq!(saturating_add(1, 0), 1);
783        assert_eq!(saturating_add(0, 0), 0);
784        assert_eq!(saturating_add(1, 1), 2);
785        assert_eq!(saturating_add(i32::MAX, 1), i32::MAX);
786        assert_eq!(saturating_add(i32::MIN, 1), i32::MIN + 1);
787
788        // f32
789        assert_eq!(saturating_add(0.0, 1.0), 1.0);
790        assert_eq!(saturating_add(1.0, 0.0), 1.0);
791        assert_eq!(saturating_add(f32::MAX, 1.0), f32::MAX);
792        assert_eq!(saturating_add(f32::MIN, 1.0), f32::MIN + 1.0);
793    }
794
795    #[test]
796    fn test_saturating_sub() {
797        // i32
798        assert_eq!(saturating_sub(0, 0), 0);
799        assert_eq!(saturating_sub(0, 1), -1);
800        assert_eq!(saturating_sub(1, 1), 0);
801        assert_eq!(saturating_sub(1, 0), 1);
802        assert_eq!(saturating_sub(10, 10), 0);
803        assert_eq!(saturating_sub(i32::MIN, 1), i32::MIN);
804        assert_eq!(saturating_sub(i32::MAX, 1), i32::MAX - 1);
805
806        // u32
807        assert_eq!(saturating_sub(0u32, 0u32), 0u32);
808        assert_eq!(saturating_sub(0u32, 1u32), 0u32);
809        assert_eq!(saturating_sub(1u32, 1u32), 0u32);
810        assert_eq!(saturating_sub(1u32, 0u32), 1u32);
811        assert_eq!(saturating_sub(10u32, 10u32), 0u32);
812        assert_eq!(saturating_sub(u32::MIN, 1u32), u32::MIN);
813        assert_eq!(saturating_sub(u32::MAX, 1u32), u32::MAX - 1);
814
815        // f32
816        assert_eq!(saturating_sub(0.0, 1.0), -1.0);
817        assert_eq!(saturating_sub(1.0, 0.0), 1.0);
818        assert_eq!(saturating_sub(1.0, 1.0), 0.0);
819        assert_eq!(saturating_sub(f32::MIN, 1.0), f32::MIN);
820        assert_eq!(saturating_sub(f32::MAX, 1.0), f32::MAX - 1.0);
821    }
822
823    #[test]
824    fn test_deletion() {
825        test_widget_deletion(|ctx| {
826            NumericUpDownBuilder::<f32>::new(WidgetBuilder::new()).build(ctx)
827        });
828    }
829}