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