Skip to main content

maolan_widgets/
numeric_input.rs

1use iced::{
2    Alignment, Background, Border, Color, Element, Length, Theme,
3    widget::{button, column, container, row, text_input},
4};
5use iced_fonts::lucide::{chevron_down, chevron_up};
6use std::{
7    fmt::Display,
8    ops::{Add, RangeInclusive, Sub},
9    str::FromStr,
10};
11
12fn spinner_button_style(theme: &Theme, status: button::Status) -> button::Style {
13    let palette = theme.extended_palette();
14    let active_bg = palette.primary.strong.color;
15    let hovered_bg = palette.primary.base.color;
16    let disabled_bg = Color {
17        a: active_bg.a * 0.4,
18        ..active_bg
19    };
20    let mut style = button::Style {
21        background: Some(Background::Color(match status {
22            button::Status::Hovered | button::Status::Pressed => hovered_bg,
23            button::Status::Disabled => disabled_bg,
24            _ => active_bg,
25        })),
26        text_color: match status {
27            button::Status::Disabled => Color {
28                a: palette.primary.strong.text.a * 0.45,
29                ..palette.primary.strong.text
30            },
31            _ => palette.primary.strong.text,
32        },
33        ..button::Style::default()
34    };
35    style.border = Border {
36        color: Color::from_rgba(0.0, 0.0, 0.0, 0.0),
37        width: 1.0,
38        radius: 3.0.into(),
39    };
40    style
41}
42
43fn shell_style(_theme: &Theme) -> container::Style {
44    container::Style {
45        text_color: Some(Color::from_rgb(0.92, 0.92, 0.92)),
46        background: Some(Background::Color(Color::from_rgba(0.10, 0.10, 0.10, 1.0))),
47        border: Border {
48            color: Color::from_rgba(0.28, 0.28, 0.28, 1.0),
49            width: 1.0,
50            radius: 2.0.into(),
51        },
52        ..container::Style::default()
53    }
54}
55
56fn input_style(theme: &Theme, status: text_input::Status) -> text_input::Style {
57    let mut style = text_input::default(theme, status);
58    style.background = Background::Color(Color::TRANSPARENT);
59    style.border = Border {
60        color: Color::TRANSPARENT,
61        width: 0.0,
62        radius: 0.0.into(),
63    };
64    style
65}
66
67pub fn number_input<'a, T, Message>(
68    value: &'a T,
69    bounds: RangeInclusive<T>,
70    on_change: impl Fn(T) -> Message + 'a + Copy,
71) -> Element<'a, Message>
72where
73    T: Copy + Display + FromStr + PartialOrd + Add<Output = T> + Sub<Output = T> + From<u8> + 'a,
74    Message: Clone + 'a,
75{
76    let min = *bounds.start();
77    let max = *bounds.end();
78    let current = *value;
79    let step = T::from(1_u8);
80    let dec_value = if current > min + step {
81        current - step
82    } else {
83        min
84    };
85    let inc_value = if current < max - step {
86        current + step
87    } else {
88        max
89    };
90
91    let input = text_input("", &current.to_string())
92        .on_input(move |raw| {
93            raw.parse::<T>()
94                .map(|parsed| {
95                    let clamped = if parsed < min {
96                        min
97                    } else if parsed > max {
98                        max
99                    } else {
100                        parsed
101                    };
102                    on_change(clamped)
103                })
104                .unwrap_or_else(|_| on_change(current))
105        })
106        .style(input_style)
107        .padding([5, 8])
108        .width(Length::Fixed(72.0))
109        .size(14);
110
111    let decrement = button(
112        container(chevron_down().size(14))
113            .center_x(Length::Fill)
114            .center_y(Length::Fill),
115    )
116    .style(spinner_button_style)
117    .padding(0)
118    .width(Length::Fixed(22.0))
119    .height(Length::Fixed(15.0));
120    let decrement = if current > min {
121        decrement.on_press(on_change(dec_value))
122    } else {
123        decrement
124    };
125
126    let increment = button(
127        container(chevron_up().size(14))
128            .center_x(Length::Fill)
129            .center_y(Length::Fill),
130    )
131    .style(spinner_button_style)
132    .padding(0)
133    .width(Length::Fixed(22.0))
134    .height(Length::Fixed(15.0));
135    let increment = if current < max {
136        increment.on_press(on_change(inc_value))
137    } else {
138        increment
139    };
140
141    container(
142        row![
143            container(input)
144                .width(Length::Fixed(72.0))
145                .center_y(Length::Fixed(30.0)),
146            column![increment, decrement]
147                .spacing(0)
148                .width(Length::Fixed(22.0))
149                .align_x(Alignment::Center),
150        ]
151        .spacing(0)
152        .align_y(Alignment::Center),
153    )
154    .style(shell_style)
155    .into()
156}