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