maolan_widgets/
numeric_input.rs1use 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("", ¤t.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}