iced_aw/widget/
number_input.rs

1//! Display fields that can only be filled with numeric type.
2//!
3//! A [`NumberInput`] has some local [`State`].
4use iced_core::{
5    Alignment, Background, Border, Clipboard, Color, Element, Event, Layout, Length, Padding,
6    Point, Rectangle, Shadow, Shell, Size, Widget,
7    alignment::Vertical,
8    keyboard,
9    layout::{Limits, Node},
10    mouse::{self, Cursor},
11    renderer,
12    widget::{
13        self, Operation, Tree,
14        tree::{State, Tag},
15    },
16};
17use iced_widget::{
18    Column, Container, Row, Text,
19    text::{LineHeight, Wrapping},
20    text_input::{self, Value, cursor},
21};
22use num_traits::{Num, NumAssignOps, bounds::Bounded};
23use std::{
24    fmt::Display,
25    ops::{Bound, RangeBounds},
26    str::FromStr,
27};
28
29use crate::iced_aw_font::advanced_text::{down_open, up_open};
30use crate::style::{self, Status};
31pub use crate::style::{
32    StyleFn,
33    number_input::{self, Catalog, Style},
34};
35use crate::widget::typed_input::TypedInput;
36
37/// The default padding
38const DEFAULT_PADDING: Padding = Padding::new(5.0);
39
40/// A field that can only be filled with numeric type.
41///
42/// # Example
43/// ```ignore
44/// # use iced_aw::NumberInput;
45/// #
46/// #[derive(Debug, Clone)]
47/// enum Message {
48///     NumberInputChanged(u32),
49/// }
50///
51/// let value = 12;
52/// let max = 1275;
53///
54/// let input = NumberInput::new(
55///     value,
56///     0..=max,
57///     Message::NumberInputChanged,
58/// )
59/// .step(2);
60/// ```
61#[allow(missing_debug_implementations)]
62pub struct NumberInput<'a, T, Message, Theme = iced_widget::Theme, Renderer = iced_widget::Renderer>
63where
64    Renderer: iced_core::text::Renderer<Font = iced_core::Font>,
65    Theme: number_input::ExtendedCatalog,
66{
67    /// The current value of the [`NumberInput`].
68    value: T,
69    /// The step for each modify of the [`NumberInput`].
70    step: T,
71    /// The min value of the [`NumberInput`].
72    min: Bound<T>,
73    /// The max value of the [`NumberInput`].
74    max: Bound<T>,
75    /// The content padding of the [`NumberInput`].
76    padding: iced_core::Padding,
77    /// The text size of the [`NumberInput`].
78    size: Option<iced_core::Pixels>,
79    /// The underlying element of the [`NumberInput`].
80    content: TypedInput<'a, T, InternalMessage<T>, Theme, Renderer>,
81    /// The ``on_change`` event of the [`NumberInput`].
82    on_change: Option<Box<dyn 'a + Fn(T) -> Message>>,
83    /// The ``on_submit`` event of the [`NumberInput`].
84    #[allow(clippy::type_complexity)]
85    on_submit: Option<Message>,
86    /// The ``on_paste`` event of the [`NumberInput`]
87    on_paste: Option<Box<dyn 'a + Fn(T) -> Message>>,
88    /// The style of the [`NumberInput`].
89    class: <Theme as style::number_input::Catalog>::Class<'a>,
90    /// The font text of the [`NumberInput`].
91    font: Renderer::Font,
92    // /// The Width to use for the ``NumberBox`` Default is ``Length::Fill``
93    // width: Length,
94    /// Ignore mouse scroll events for the [`NumberInput`] Default is ``false``.
95    ignore_scroll_events: bool,
96    /// Ignore drawing increase and decrease buttons [`NumberInput`] Default is ``false``.
97    ignore_buttons: bool,
98}
99
100#[derive(Debug, Clone, PartialEq)]
101#[allow(clippy::enum_variant_names)]
102enum InternalMessage<T> {
103    OnChange(T),
104    OnSubmit(Result<T, String>),
105    OnPaste(T),
106}
107
108impl<'a, T, Message, Theme, Renderer> NumberInput<'a, T, Message, Theme, Renderer>
109where
110    T: Num + NumAssignOps + PartialOrd + Display + FromStr + Clone + Bounded + 'a,
111    Message: Clone + 'a,
112    Renderer: iced_core::text::Renderer<Font = iced_core::Font>,
113    Theme: number_input::ExtendedCatalog,
114{
115    /// Creates a new [`NumberInput`].
116    ///
117    /// It expects:
118    /// - the current value
119    /// - the bound values
120    /// - a function that produces a message when the [`NumberInput`] changes
121    pub fn new<F>(value: &T, bounds: impl RangeBounds<T>, on_change: F) -> Self
122    where
123        F: 'static + Fn(T) -> Message + Clone,
124        T: 'static,
125    {
126        let padding = DEFAULT_PADDING;
127
128        Self {
129            value: value.clone(),
130            step: T::one(),
131            min: bounds.start_bound().cloned(),
132            max: bounds.end_bound().cloned(),
133            padding,
134            size: None,
135            content: TypedInput::new("", value)
136                .on_input(InternalMessage::OnChange)
137                .padding(padding)
138                .width(Length::Fixed(127.0))
139                .class(Theme::default_input()),
140            on_change: Some(Box::new(on_change)),
141            on_submit: None,
142            on_paste: None,
143            class: <Theme as style::number_input::Catalog>::default(),
144            font: Renderer::Font::default(),
145            // width: Length::Shrink,
146            ignore_scroll_events: false,
147            ignore_buttons: false,
148        }
149    }
150
151    /// Sets the [`Id`](widget::Id) of the underlying [`TextInput`](iced_widget::TextInput).
152    #[must_use]
153    pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
154        self.content = self.content.id(id.into());
155        self
156    }
157
158    /// Sets the message that should be produced when some valid text is typed into [`NumberInput`]
159    ///
160    /// If neither this method nor [`on_submit`](Self::on_submit) is called, the [`NumberInput`] will be disabled
161    #[must_use]
162    pub fn on_input<F>(mut self, callback: F) -> Self
163    where
164        F: 'a + Fn(T) -> Message,
165    {
166        self.content = self.content.on_input(InternalMessage::OnChange);
167        self.on_change = Some(Box::new(callback));
168        self
169    }
170
171    /// Sets the message that should be produced when some text is typed into the [`NumberInput`], if `Some`.
172    ///
173    /// If this is `None`, and there is no [`on_submit`](Self::on_submit) callback, the [`NumberInput`] will be disabled.
174    #[must_use]
175    pub fn on_input_maybe<F>(mut self, callback: Option<F>) -> Self
176    where
177        F: 'a + Fn(T) -> Message,
178    {
179        if let Some(callback) = callback {
180            self.content = self.content.on_input(InternalMessage::OnChange);
181            self.on_change = Some(Box::new(callback));
182        } else {
183            if self.on_submit.is_none() {
184                // Used to give a proper type to None, maybe someone can find a better way
185                #[allow(unused_assignments)]
186                let mut f = Some(InternalMessage::OnChange);
187                f = None;
188                self.content = self.content.on_input_maybe(f);
189            }
190            self.on_change = None;
191        }
192        self
193    }
194
195    /// Sets the message that should be produced when the [`NumberInput`] is
196    /// focused and the enter key is pressed.
197    #[must_use]
198    pub fn on_submit(mut self, message: Message) -> Self {
199        self.content = self.content.on_submit(InternalMessage::OnSubmit);
200        self.on_submit = Some(message);
201        self
202    }
203
204    /// Sets the message that should be produced when the [`NumbertInput`] is
205    /// focused and the enter key is pressed, if `Some`.
206    ///
207    /// If this is `None`, and there is no [`on_change`](Self::on_input) callback, the [`NumberInput`] will be disabled.
208    #[must_use]
209    pub fn on_submit_maybe(mut self, message: Option<Message>) -> Self {
210        if let Some(message) = message {
211            self.content = self.content.on_submit(InternalMessage::OnSubmit);
212            self.on_submit = Some(message);
213        } else {
214            if self.on_change.is_none() {
215                // Used to give a proper type to None, maybe someone can find a better way
216                #[allow(unused_assignments)]
217                let mut f = Some(InternalMessage::OnChange);
218                f = None;
219                self.content = self.content.on_input_maybe(f);
220            }
221            // Used to give a proper type to None, maybe someone can find a better way
222            #[allow(unused_assignments)]
223            let mut f = Some(InternalMessage::OnSubmit);
224            f = None;
225            self.content = self.content.on_submit_maybe(f);
226            self.on_change = None;
227        }
228        self
229    }
230
231    /// Sets the message that should be produced when some text is pasted into the [`NumberInput`], resulting in a valid value
232    #[must_use]
233    pub fn on_paste<F>(mut self, callback: F) -> Self
234    where
235        F: 'a + Fn(T) -> Message,
236    {
237        self.content = self.content.on_paste(InternalMessage::OnPaste);
238        self.on_paste = Some(Box::new(callback));
239        self
240    }
241
242    /// Sets the message that should be produced when some text is pasted into the [`NumberInput`], resulting in a valid value, if `Some`
243    #[must_use]
244    pub fn on_paste_maybe<F>(mut self, callback: Option<F>) -> Self
245    where
246        F: 'a + Fn(T) -> Message,
247    {
248        if let Some(callback) = callback {
249            self.content = self.content.on_paste(InternalMessage::OnPaste);
250            self.on_paste = Some(Box::new(callback));
251        } else {
252            // Used to give a proper type to None, maybe someone can find a better way
253            #[allow(unused_assignments)]
254            let mut f = Some(InternalMessage::OnPaste);
255            f = None;
256            self.content = self.content.on_paste_maybe(f);
257            self.on_paste = None;
258        }
259        self
260    }
261
262    /// Sets the [`Font`] of the [`Text`].
263    ///
264    /// [`Font`]: iced_core::Font
265    /// [`Text`]: iced_widget::Text
266    #[allow(clippy::needless_pass_by_value)]
267    #[must_use]
268    pub fn font(mut self, font: Renderer::Font) -> Self {
269        self.font = font;
270        self.content = self.content.font(font);
271        self
272    }
273
274    /// Sets the [Icon](iced_widget::text_input::Icon) of the [`NumberInput`]
275    #[must_use]
276    pub fn icon(mut self, icon: iced_widget::text_input::Icon<Renderer::Font>) -> Self {
277        self.content = self.content.icon(icon);
278        self
279    }
280
281    /// Sets the width of the [`NumberInput`].
282    #[must_use]
283    pub fn width(mut self, width: impl Into<Length>) -> Self {
284        self.content = self.content.width(width);
285        self
286    }
287
288    /// Sets the width of the [`NumberInput`].
289    #[deprecated(since = "0.11.1", note = "use `width` instead")]
290    #[must_use]
291    pub fn content_width(self, width: impl Into<Length>) -> Self {
292        self.width(width)
293    }
294
295    /// Sets the padding of the [`NumberInput`].
296    #[must_use]
297    pub fn padding(mut self, padding: impl Into<iced_core::Padding>) -> Self {
298        let padding = padding.into();
299        self.padding = padding;
300        self.content = self.content.padding(padding);
301        self
302    }
303
304    /// Sets the text size of the [`NumberInput`].
305    #[must_use]
306    pub fn set_size(mut self, size: impl Into<iced_core::Pixels>) -> Self {
307        let size = size.into();
308        self.size = Some(size);
309        self.content = self.content.size(size);
310        self
311    }
312
313    /// Sets the [`text::LineHeight`](iced_widget::text::LineHeight) of the [`NumberInput`].
314    #[must_use]
315    pub fn line_height(mut self, line_height: impl Into<iced_widget::text::LineHeight>) -> Self {
316        self.content = self.content.line_height(line_height);
317        self
318    }
319
320    /// Sets the horizontal alignment of the [`NumberInput`].
321    #[must_use]
322    pub fn align_x(mut self, alignment: impl Into<iced_core::alignment::Horizontal>) -> Self {
323        self.content = self.content.align_x(alignment);
324        self
325    }
326
327    /// Sets the style of the [`NumberInput`].
328    #[must_use]
329    pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
330    where
331        <Theme as style::number_input::Catalog>::Class<'a>: From<StyleFn<'a, Theme, Style>>,
332    {
333        self.class = (Box::new(style) as StyleFn<'a, Theme, Style>).into();
334        self
335    }
336    /// Sets the style of the input of the [`NumberInput`].
337    #[must_use]
338    pub fn input_style(
339        mut self,
340        style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a,
341    ) -> Self
342    where
343        <Theme as text_input::Catalog>::Class<'a>: From<text_input::StyleFn<'a, Theme>>,
344    {
345        self.content = self.content.style(style);
346        self
347    }
348
349    /// Sets the class of the input of the [`NumberInput`].
350    #[must_use]
351    pub fn class(
352        mut self,
353        class: impl Into<<Theme as style::number_input::Catalog>::Class<'a>>,
354    ) -> Self {
355        self.class = class.into();
356        self
357    }
358
359    /// Sets the minimum & maximum value (bound) of the [`NumberInput`].
360    /// # Example
361    /// ```
362    /// use iced_aw::widget::number_input;
363    /// // Creates a range from -5 till 5.
364    /// let input: iced_aw::NumberInput<'_, _, _, iced_widget::Theme, iced::Renderer> = number_input(&4 /* my_value */, 0..=4, |_| () /* my_message */).bounds(-5..=5);
365    /// ```
366    #[must_use]
367    pub fn bounds(mut self, bounds: impl RangeBounds<T>) -> Self {
368        self.min = bounds.start_bound().cloned();
369        self.max = bounds.end_bound().cloned();
370
371        self
372    }
373
374    /// Sets the step of the [`NumberInput`].
375    #[must_use]
376    pub fn step(mut self, step: T) -> Self {
377        self.step = step;
378        self
379    }
380
381    /// Enable or disable increase and decrease buttons of the [`NumberInput`], by default this is set to
382    /// ``false``.
383    #[must_use]
384    pub fn ignore_buttons(mut self, ignore: bool) -> Self {
385        self.ignore_buttons = ignore;
386        self
387    }
388
389    /// Enable or disable mouse scrolling events of the [`NumberInput`], by default this is set to
390    /// ``false``.
391    #[must_use]
392    pub fn ignore_scroll(mut self, ignore: bool) -> Self {
393        self.ignore_scroll_events = ignore;
394        self
395    }
396
397    /// Decrease current value by step of the [`NumberInput`].
398    fn decrease_value(&mut self, shell: &mut Shell<Message>) {
399        if self.value.clone() > self.min() + self.step.clone()
400            && self.valid(&(self.value.clone() - self.step.clone()))
401        {
402            self.value -= self.step.clone();
403        } else if self.value > self.min() {
404            self.value = self.min();
405        } else {
406            return;
407        }
408        if let Some(on_change) = &self.on_change {
409            shell.publish(on_change(self.value.clone()));
410        }
411    }
412
413    /// Increase current value by step of the [`NumberInput`].
414    fn increase_value(&mut self, shell: &mut Shell<Message>) {
415        if self.value < self.max() - self.step.clone()
416            && self.valid(&(self.value.clone() + self.step.clone()))
417        {
418            self.value += self.step.clone();
419        } else if self.value < self.max() {
420            self.value = self.max();
421        } else {
422            return;
423        }
424        if let Some(on_change) = &self.on_change {
425            shell.publish(on_change(self.value.clone()));
426        }
427    }
428
429    /// Returns the lower value possible
430    /// if the bound is excluded the bound is increased by the step
431    fn min(&self) -> T {
432        match &self.min {
433            Bound::Included(n) => n.clone(),
434            Bound::Excluded(n) => n.clone() + self.step.clone(),
435            Bound::Unbounded => T::min_value(),
436        }
437    }
438
439    /// Returns the higher value possible
440    /// if the bound is excluded the bound is decreased by the step
441    fn max(&self) -> T {
442        match &self.max {
443            Bound::Included(n) => n.clone(),
444            Bound::Excluded(n) => n.clone() - self.step.clone(),
445            Bound::Unbounded => T::max_value(),
446        }
447    }
448
449    /// Checks if the value is within the bounds
450    fn valid(&self, value: &T) -> bool {
451        (match &self.min {
452            Bound::Included(n) if *n > *value => false,
453            Bound::Excluded(n) if *n >= *value => false,
454            _ => true,
455        }) && (match &self.max {
456            Bound::Included(n) if *n < *value => false,
457            Bound::Excluded(n) if *n <= *value => false,
458            _ => true,
459        })
460    }
461
462    /// Checks if the value can be increased by the step
463    fn can_increase(&self) -> bool {
464        (self.value < self.max() - self.step.clone()
465            && self.valid(&(self.value.clone() + self.step.clone())))
466            || self.value < self.max()
467    }
468
469    /// Checks if the value can be decreased by the step
470    fn can_decrease(&self) -> bool {
471        (self.value.clone() > self.min() + self.step.clone()
472            && self.valid(&(self.value.clone() - self.step.clone())))
473            || self.value > self.min()
474    }
475
476    /// Checks if the [`NumberInput`] is disabled
477    /// Meaning that the bounds are too tight for the value to change
478    fn disabled(&self) -> bool {
479        match (&self.min, &self.max) {
480            (Bound::Included(n) | Bound::Excluded(n), Bound::Included(m) | Bound::Excluded(m)) => {
481                *n >= *m
482            }
483            _ => false,
484        }
485    }
486}
487
488impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
489    for NumberInput<'a, T, Message, Theme, Renderer>
490where
491    T: Num + NumAssignOps + PartialOrd + Display + FromStr + ToString + Clone + Bounded + 'a,
492    Message: 'a + Clone,
493    Renderer: 'a + iced_core::text::Renderer<Font = iced_core::Font>,
494    Theme: number_input::ExtendedCatalog,
495{
496    fn tag(&self) -> Tag {
497        Tag::of::<ModifierState>()
498    }
499    fn state(&self) -> State {
500        State::new(ModifierState::default())
501    }
502
503    fn children(&self) -> Vec<Tree> {
504        vec![Tree {
505            tag: self.content.tag(),
506            state: self.content.state(),
507            children: self.content.children(),
508        }]
509    }
510
511    fn diff(&self, tree: &mut Tree) {
512        tree.diff_children_custom(
513            &[&self.content],
514            |state, content| content.diff(state),
515            |content| Tree {
516                tag: content.tag(),
517                state: content.state(),
518                children: content.children(),
519            },
520        );
521    }
522
523    fn size(&self) -> Size<Length> {
524        Widget::size(&self.content)
525    }
526
527    fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
528        let num_size = self.size();
529        let limits = limits.width(num_size.width).height(Length::Shrink);
530        let content = self
531            .content
532            .layout(&mut tree.children[0], renderer, &limits);
533        let limits2 = Limits::new(Size::new(0.0, 0.0), content.size());
534        let txt_size = self.size.unwrap_or_else(|| renderer.default_size());
535
536        let icon_size = txt_size * 2.5 / 4.0;
537        let btn_mod = |c| {
538            Container::<Message, Theme, Renderer>::new(Text::new(format!(" {c} ")).size(icon_size))
539                .center_y(Length::Shrink)
540                .center_x(Length::Shrink)
541        };
542
543        let default_padding = DEFAULT_PADDING;
544
545        let mut element = if self.padding.top < default_padding.top
546            || self.padding.bottom < default_padding.bottom
547            || self.padding.right < default_padding.right
548        {
549            Element::new(
550                Row::<Message, Theme, Renderer>::new()
551                    .spacing(1)
552                    .width(Length::Shrink)
553                    .push(btn_mod('+'))
554                    .push(btn_mod('-')),
555            )
556        } else {
557            Element::new(
558                Column::<Message, Theme, Renderer>::new()
559                    .spacing(1)
560                    .width(Length::Shrink)
561                    .push(btn_mod('▲'))
562                    .push(btn_mod('▼')),
563            )
564        };
565
566        let input_tree = if let Some(child_tree) = tree.children.get_mut(1) {
567            child_tree.diff(element.as_widget_mut());
568            child_tree
569        } else {
570            let child_tree = Tree::new(element.as_widget());
571            tree.children.insert(1, child_tree);
572            &mut tree.children[1]
573        };
574
575        let mut modifier = element
576            .as_widget_mut()
577            .layout(input_tree, renderer, &limits2.loose());
578        let intrinsic = Size::new(
579            content.size().width - 1.0,
580            content.size().height.max(modifier.size().height),
581        );
582        modifier = modifier.align(Alignment::End, Alignment::Center, intrinsic);
583
584        let size = limits.resolve(num_size.width, Length::Shrink, intrinsic);
585        Node::with_children(size, vec![content, modifier])
586    }
587
588    fn operate(
589        &mut self,
590        tree: &mut Tree,
591        layout: Layout<'_>,
592        renderer: &Renderer,
593        operation: &mut dyn Operation<()>,
594    ) {
595        operation.container(None, layout.bounds());
596        operation.traverse(&mut |operation| {
597            self.content.operate(
598                &mut tree.children[0],
599                layout
600                    .children()
601                    .next()
602                    .expect("NumberInput inner child Textbox was not created."),
603                renderer,
604                operation,
605            );
606        });
607    }
608
609    #[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
610    fn update(
611        &mut self,
612        state: &mut Tree,
613        event: &Event,
614        layout: Layout<'_>,
615        cursor: Cursor,
616        renderer: &Renderer,
617        clipboard: &mut dyn Clipboard,
618        shell: &mut Shell<Message>,
619        viewport: &Rectangle,
620    ) {
621        let mut children = layout.children();
622        let content = children.next().expect("fail to get content layout");
623        let mut mod_children = children
624            .next()
625            .expect("fail to get modifiers layout")
626            .children();
627        let inc_bounds = mod_children
628            .next()
629            .expect("fail to get increase mod layout")
630            .bounds();
631        let dec_bounds = mod_children
632            .next()
633            .expect("fail to get decrease mod layout")
634            .bounds();
635
636        if self.disabled() {
637            return;
638        }
639        let can_decrease = self.can_decrease();
640        let can_increase = self.can_increase();
641
642        let cursor_position = cursor.position().unwrap_or_default();
643        let mouse_over_widget = layout.bounds().contains(cursor_position);
644        let mouse_over_inc = inc_bounds.contains(cursor_position);
645        let mouse_over_dec = dec_bounds.contains(cursor_position);
646        let mouse_over_button = mouse_over_inc || mouse_over_dec;
647
648        let modifiers = state.state.downcast_mut::<ModifierState>();
649        let mut value = self.content.text().to_owned();
650
651        let child = state.children.get_mut(0).expect("fail to get child");
652        let text_input = child
653            .state
654            .downcast_mut::<text_input::State<Renderer::Paragraph>>();
655
656        // We use a secondary shell to select handle the event of the underlying [`TypedInput`]
657        let mut messages = Vec::new();
658        let mut sub_shell = Shell::new(&mut messages);
659
660        // Function to forward the event to the underlying [`TypedInput`]
661        let mut forward_to_text = |widget: &mut Self, child, clipboard| {
662            widget.content.update(
663                child,
664                &event.clone(),
665                content,
666                cursor,
667                renderer,
668                clipboard,
669                &mut sub_shell,
670                viewport,
671            );
672        };
673
674        // Check if the value that would result from the input is valid and within bound
675        let supports_negative = self.min() < T::zero();
676        let mut check_value = |value: &str| {
677            if let Ok(value) = T::from_str(value) {
678                self.valid(&value)
679            } else if value.is_empty() || value == "-" && supports_negative {
680                self.value = T::zero();
681                true
682            } else {
683                false
684            }
685        };
686
687        match &event {
688            Event::Keyboard(key) => {
689                if !text_input.is_focused() {
690                    return;
691                }
692
693                match key {
694                    keyboard::Event::ModifiersChanged(_) => forward_to_text(self, child, clipboard),
695                    keyboard::Event::KeyReleased { .. } => return,
696                    keyboard::Event::KeyPressed {
697                        key,
698                        text,
699                        modifiers,
700                        ..
701                    } => {
702                        let cursor = text_input.cursor();
703
704                        // If true, ignore Arrow/Home/End keys - they are coming from numpad and are just
705                        // mislabeled. See the core PR:
706                        // https://github.com/iced-rs/iced/pull/2278
707                        let has_value = !modifiers.command()
708                            && text
709                                .as_ref()
710                                .is_some_and(|t| t.chars().any(|c| !c.is_control()));
711
712                        match key.as_ref() {
713                            // Enter
714                            keyboard::Key::Named(keyboard::key::Named::Enter) => {
715                                forward_to_text(self, child, clipboard);
716                            }
717                            // Copy and selecting all
718                            keyboard::Key::Character("c" | "a") if modifiers.command() => {
719                                forward_to_text(self, child, clipboard);
720                            }
721                            // Cut
722                            keyboard::Key::Character("x") if modifiers.command() => {
723                                // We need a selection to cut
724                                if let Some((start, end)) = cursor.selection(&Value::new(&value)) {
725                                    let _ = value.drain(start..end);
726                                    // We check that once this part is cut, it's still a number
727                                    if check_value(&value) {
728                                        forward_to_text(self, child, clipboard);
729                                    } else {
730                                        return;
731                                    }
732                                } else {
733                                    return;
734                                }
735                            }
736                            // Paste
737                            keyboard::Key::Character("v") if modifiers.command() => {
738                                // We need something to paste
739                                let Some(paste) =
740                                    clipboard.read(iced_core::clipboard::Kind::Standard)
741                                else {
742                                    return;
743                                };
744                                // We replace the selection or paste the text at the cursor
745                                match cursor.state(&Value::new(&value)) {
746                                    cursor::State::Index(idx) => {
747                                        value.insert_str(idx, &paste);
748                                    }
749                                    cursor::State::Selection { start, end } => {
750                                        value.replace_range(sorted_range(start, end), &paste);
751                                    }
752                                }
753
754                                shell.capture_event();
755
756                                // We check if it's now a valid number
757                                if check_value(&value) {
758                                    forward_to_text(self, child, clipboard);
759                                } else {
760                                    return;
761                                }
762                            }
763                            // Backspace
764                            keyboard::Key::Named(keyboard::key::Named::Backspace) => {
765                                // We remove either the selection or the character before the cursor
766                                match cursor.state(&Value::new(&value)) {
767                                    cursor::State::Selection { start, end } => {
768                                        let _ = value.drain(sorted_range(start, end));
769                                    }
770                                    // We need the cursor not at the start
771                                    cursor::State::Index(idx) if idx > 0 => {
772                                        if modifiers.command() {
773                                            // ctrl+backspace erases to the left,
774                                            // including decimal separator but not including
775                                            // minus sign.
776                                            let _ =
777                                                value.drain((value.starts_with('-').into())..idx);
778                                        } else {
779                                            let _ = value.remove(idx - 1);
780                                        }
781                                    }
782                                    cursor::State::Index(_) => return,
783                                }
784
785                                shell.capture_event();
786
787                                // We check if it's now a valid number
788                                if check_value(&value) {
789                                    forward_to_text(self, child, clipboard);
790                                } else {
791                                    return;
792                                }
793                            }
794                            // Delete
795                            keyboard::Key::Named(keyboard::key::Named::Delete) => {
796                                // We remove either the selection or the character after the cursor
797                                match cursor.state(&Value::new(&value)) {
798                                    cursor::State::Selection { start, end } => {
799                                        let _ = value.drain(sorted_range(start, end));
800                                    }
801                                    // We need the cursor not at the end
802                                    cursor::State::Index(idx) if idx < value.len() => {
803                                        if idx == 0 && value.starts_with('-') {
804                                            let _ = value.remove(0);
805                                        } else if modifiers.command() {
806                                            // ctrl+del erases to the right,
807                                            // including decimal separator but not including
808                                            // minus sign.
809                                            let _ = value.drain(idx..);
810                                        } else {
811                                            let _ = value.remove(idx);
812                                        }
813                                    }
814                                    cursor::State::Index(_) => return,
815                                }
816
817                                shell.capture_event();
818
819                                // We check if it's now a valid number
820                                if check_value(&value) {
821                                    forward_to_text(self, child, clipboard);
822                                } else {
823                                    return;
824                                }
825                            }
826                            // Arrow Down, decrease by step
827                            keyboard::Key::Named(keyboard::key::Named::ArrowDown)
828                                if can_decrease && !has_value =>
829                            {
830                                shell.capture_event();
831                                shell.request_redraw();
832                                self.decrease_value(shell);
833                            }
834                            // Arrow Up, increase by step
835                            keyboard::Key::Named(keyboard::key::Named::ArrowUp)
836                                if can_increase && !has_value =>
837                            {
838                                shell.capture_event();
839                                shell.request_redraw();
840
841                                self.increase_value(shell);
842                            }
843                            // Movement of the cursor
844                            keyboard::Key::Named(
845                                keyboard::key::Named::ArrowLeft
846                                | keyboard::key::Named::ArrowRight
847                                | keyboard::key::Named::Home
848                                | keyboard::key::Named::End,
849                            ) if !has_value => forward_to_text(self, child, clipboard),
850                            // Everything else
851                            _ => match text {
852                                // If we are trying to input text
853                                Some(text) => {
854                                    // We replace the selection or insert the text at the cursor
855                                    match cursor.state(&Value::new(&value)) {
856                                        cursor::State::Index(idx) => {
857                                            value.insert_str(idx, text);
858                                        }
859                                        cursor::State::Selection { start, end } => {
860                                            value.replace_range(sorted_range(start, end), text);
861                                        }
862                                    }
863
864                                    shell.capture_event();
865                                    shell.request_redraw();
866
867                                    // We check if it's now a valid number
868                                    if check_value(&value) {
869                                        forward_to_text(self, child, clipboard);
870                                    } else {
871                                        return;
872                                    }
873                                }
874                                // If we are not trying to input text
875                                None => return,
876                            },
877                        }
878                    }
879                }
880            }
881            // Mouse scroll event
882            Event::Mouse(mouse::Event::WheelScrolled { delta })
883                if mouse_over_widget && !self.ignore_scroll_events =>
884            {
885                match delta {
886                    mouse::ScrollDelta::Lines { y, .. } | mouse::ScrollDelta::Pixels { y, .. } => {
887                        if y.is_sign_positive() {
888                            self.increase_value(shell);
889                        } else {
890                            self.decrease_value(shell);
891                        }
892                    }
893                }
894                shell.capture_event();
895                shell.request_redraw();
896            }
897            // Clicking on the buttons up or down
898            Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
899                if mouse_over_button && !self.ignore_buttons =>
900            {
901                if mouse_over_dec {
902                    modifiers.decrease_pressed = true;
903                    self.decrease_value(shell);
904                } else {
905                    modifiers.increase_pressed = true;
906                    self.increase_value(shell);
907                }
908                shell.capture_event();
909                shell.request_redraw();
910            }
911            // Releasing the buttons
912            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
913                if mouse_over_button =>
914            {
915                if mouse_over_dec {
916                    modifiers.decrease_pressed = false;
917                } else {
918                    modifiers.increase_pressed = false;
919                }
920                shell.capture_event();
921                shell.request_redraw();
922            }
923            // Any other event are just forwarded
924            _ => forward_to_text(self, child, clipboard),
925        }
926
927        // We forward the shell of the [`TypedInput`] to the application
928        shell.request_redraw_at(sub_shell.redraw_request());
929
930        if sub_shell.is_layout_invalid() {
931            shell.invalidate_layout();
932        }
933        if sub_shell.are_widgets_invalid() {
934            shell.invalidate_widgets();
935        }
936
937        for message in messages {
938            match message {
939                InternalMessage::OnChange(value) => {
940                    if self.value != value || self.value.is_zero() {
941                        self.value = value.clone();
942                        if let Some(on_change) = &self.on_change {
943                            shell.publish(on_change(value));
944                        }
945                    }
946                    shell.invalidate_layout();
947                }
948                InternalMessage::OnSubmit(result) => {
949                    if let Err(text) = result {
950                        assert!(
951                            text.is_empty(),
952                            "We shouldn't be able to submit a number input with an invalid value"
953                        );
954                    }
955                    if let Some(on_submit) = &self.on_submit {
956                        shell.publish(on_submit.clone());
957                    }
958                    shell.invalidate_layout();
959                }
960                InternalMessage::OnPaste(value) => {
961                    if self.value != value {
962                        self.value = value.clone();
963                        if let Some(on_paste) = &self.on_paste {
964                            shell.publish(on_paste(value));
965                        }
966                    }
967                    shell.invalidate_layout();
968                }
969            }
970        }
971    }
972
973    fn mouse_interaction(
974        &self,
975        _state: &Tree,
976        layout: Layout<'_>,
977        cursor: Cursor,
978        _viewport: &Rectangle,
979        _renderer: &Renderer,
980    ) -> mouse::Interaction {
981        let bounds = layout.bounds();
982        let mut children = layout.children();
983        let _content_layout = children.next().expect("fail to get content layout");
984        let mut mod_children = children
985            .next()
986            .expect("fail to get modifiers layout")
987            .children();
988        let inc_bounds = mod_children
989            .next()
990            .expect("fail to get increase mod layout")
991            .bounds();
992        let dec_bounds = mod_children
993            .next()
994            .expect("fail to get decrease mod layout")
995            .bounds();
996        let is_mouse_over = bounds.contains(cursor.position().unwrap_or_default());
997        let is_decrease_disabled = !self.can_decrease();
998        let is_increase_disabled = !self.can_increase();
999        let mouse_over_decrease = dec_bounds.contains(cursor.position().unwrap_or_default());
1000        let mouse_over_increase = inc_bounds.contains(cursor.position().unwrap_or_default());
1001
1002        if ((mouse_over_decrease && !is_decrease_disabled)
1003            || (mouse_over_increase && !is_increase_disabled))
1004            && !self.ignore_buttons
1005        {
1006            mouse::Interaction::Pointer
1007        } else if is_mouse_over {
1008            mouse::Interaction::Text
1009        } else {
1010            mouse::Interaction::default()
1011        }
1012    }
1013
1014    fn draw(
1015        &self,
1016        state: &Tree,
1017        renderer: &mut Renderer,
1018        theme: &Theme,
1019        style: &renderer::Style,
1020        layout: Layout<'_>,
1021        cursor: Cursor,
1022        viewport: &Rectangle,
1023    ) {
1024        let mut children = layout.children();
1025        let content_layout = children.next().expect("fail to get content layout");
1026        let mut mod_children = children
1027            .next()
1028            .expect("fail to get modifiers layout")
1029            .children();
1030        let inc_bounds = mod_children
1031            .next()
1032            .expect("fail to get increase mod layout")
1033            .bounds();
1034        let dec_bounds = mod_children
1035            .next()
1036            .expect("fail to get decrease mod layout")
1037            .bounds();
1038        self.content.draw(
1039            &state.children[0],
1040            renderer,
1041            theme,
1042            style,
1043            content_layout,
1044            cursor,
1045            viewport,
1046        );
1047        let is_decrease_disabled = !self.can_decrease();
1048        let is_increase_disabled = !self.can_increase();
1049
1050        let decrease_btn_style = if is_decrease_disabled {
1051            style::number_input::Catalog::style(theme, &self.class, Status::Disabled)
1052        } else if state.state.downcast_ref::<ModifierState>().decrease_pressed {
1053            style::number_input::Catalog::style(theme, &self.class, Status::Pressed)
1054        } else {
1055            style::number_input::Catalog::style(theme, &self.class, Status::Active)
1056        };
1057
1058        let increase_btn_style = if is_increase_disabled {
1059            style::number_input::Catalog::style(theme, &self.class, Status::Disabled)
1060        } else if state.state.downcast_ref::<ModifierState>().increase_pressed {
1061            style::number_input::Catalog::style(theme, &self.class, Status::Pressed)
1062        } else {
1063            style::number_input::Catalog::style(theme, &self.class, Status::Active)
1064        };
1065
1066        let txt_size = self.size.unwrap_or_else(|| renderer.default_size());
1067
1068        let icon_size = txt_size * 2.5 / 4.0;
1069
1070        if self.ignore_buttons {
1071            return;
1072        }
1073        // decrease button section
1074        if dec_bounds.intersects(viewport) {
1075            renderer.fill_quad(
1076                renderer::Quad {
1077                    bounds: dec_bounds,
1078                    border: Border {
1079                        radius: (3.0).into(),
1080                        width: 0.0,
1081                        color: Color::TRANSPARENT,
1082                    },
1083                    shadow: Shadow::default(),
1084                    snap: false,
1085                },
1086                decrease_btn_style
1087                    .button_background
1088                    .unwrap_or(Background::Color(Color::TRANSPARENT)),
1089            );
1090        }
1091
1092        let (content, font, shaping) = down_open();
1093        renderer.fill_text(
1094            iced_core::text::Text {
1095                content,
1096                bounds: Size::new(dec_bounds.width, dec_bounds.height),
1097                size: icon_size,
1098                font,
1099                line_height: LineHeight::Relative(1.3),
1100                shaping,
1101                wrapping: Wrapping::default(),
1102                align_x: Alignment::Center.into(),
1103                align_y: Vertical::Center,
1104            },
1105            Point::new(dec_bounds.center_x(), dec_bounds.center_y()),
1106            decrease_btn_style.icon_color,
1107            dec_bounds,
1108        );
1109
1110        // increase button section
1111        if inc_bounds.intersects(viewport) {
1112            renderer.fill_quad(
1113                renderer::Quad {
1114                    bounds: inc_bounds,
1115                    border: Border {
1116                        radius: (3.0).into(),
1117                        width: 0.0,
1118                        color: Color::TRANSPARENT,
1119                    },
1120                    shadow: Shadow::default(),
1121                    snap: false,
1122                },
1123                increase_btn_style
1124                    .button_background
1125                    .unwrap_or(Background::Color(Color::TRANSPARENT)),
1126            );
1127        }
1128
1129        let (content, font, shaping) = up_open();
1130        renderer.fill_text(
1131            iced_core::text::Text {
1132                content,
1133                bounds: Size::new(inc_bounds.width, inc_bounds.height),
1134                size: icon_size,
1135                font,
1136                line_height: LineHeight::Relative(1.3),
1137                shaping,
1138                wrapping: Wrapping::default(),
1139                align_x: Alignment::Center.into(),
1140                align_y: Vertical::Center,
1141            },
1142            Point::new(inc_bounds.center_x(), inc_bounds.center_y()),
1143            increase_btn_style.icon_color,
1144            inc_bounds,
1145        );
1146    }
1147}
1148
1149/// The modifier state of a [`NumberInput`].
1150#[derive(Default, Clone, Debug)]
1151pub struct ModifierState {
1152    /// The state of decrease button on a [`NumberInput`].
1153    pub decrease_pressed: bool,
1154    /// The state of increase button on a [`NumberInput`].
1155    pub increase_pressed: bool,
1156}
1157
1158impl<'a, T, Message, Theme, Renderer> From<NumberInput<'a, T, Message, Theme, Renderer>>
1159    for Element<'a, Message, Theme, Renderer>
1160where
1161    T: 'a + Num + NumAssignOps + PartialOrd + Display + FromStr + Clone + Bounded,
1162    Message: 'a + Clone,
1163    Renderer: 'a + iced_core::text::Renderer<Font = iced_core::Font>,
1164    Theme: 'a + number_input::ExtendedCatalog,
1165{
1166    fn from(num_input: NumberInput<'a, T, Message, Theme, Renderer>) -> Self {
1167        Element::new(num_input)
1168    }
1169}
1170
1171fn sorted_range<T: PartialOrd>(a: T, b: T) -> std::ops::Range<T> {
1172    if a >= b { b..a } else { a..b }
1173}