1use crate::{
7 Element, theme,
8 widget::{button, column, container, icon, row, text},
9};
10use apply::Apply;
11use iced::{Alignment, Length};
12use iced::{Border, Shadow};
13use std::borrow::Cow;
14use std::ops::{Add, Sub};
15
16pub fn spin_button<'a, T, M>(
18 label: impl Into<Cow<'a, str>>,
19 value: T,
20 step: T,
21 min: T,
22 max: T,
23 on_press: impl Fn(T) -> M + 'static,
24) -> SpinButton<'a, T, M>
25where
26 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
27{
28 SpinButton::new(
29 label,
30 value,
31 step,
32 min,
33 max,
34 Orientation::Horizontal,
35 on_press,
36 )
37}
38
39pub fn vertical<'a, T, M>(
41 label: impl Into<Cow<'a, str>>,
42 value: T,
43 step: T,
44 min: T,
45 max: T,
46 on_press: impl Fn(T) -> M + 'static,
47) -> SpinButton<'a, T, M>
48where
49 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
50{
51 SpinButton::new(
52 label,
53 value,
54 step,
55 min,
56 max,
57 Orientation::Vertical,
58 on_press,
59 )
60}
61
62#[derive(Clone, Copy)]
63enum Orientation {
64 Horizontal,
65 Vertical,
66}
67
68pub struct SpinButton<'a, T, M>
69where
70 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
71{
72 label: Cow<'a, str>,
74 value: T,
76 step: T,
78 min: T,
80 max: T,
82 orientation: Orientation,
83 on_press: Box<dyn Fn(T) -> M>,
84}
85
86impl<'a, T, M> SpinButton<'a, T, M>
87where
88 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
89{
90 fn new(
92 label: impl Into<Cow<'a, str>>,
93 value: T,
94 step: T,
95 min: T,
96 max: T,
97 orientation: Orientation,
98 on_press: impl Fn(T) -> M + 'static,
99 ) -> Self {
100 Self {
101 label: label.into(),
102 step,
103 value: if value < min {
104 min
105 } else if value > max {
106 max
107 } else {
108 value
109 },
110 min,
111 max,
112 orientation,
113 on_press: Box::from(on_press),
114 }
115 }
116}
117
118fn increment<T>(value: T, step: T, min: T, max: T) -> T
119where
120 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
121{
122 if value > max - step {
123 max
124 } else {
125 value + step
126 }
127}
128
129fn decrement<T>(value: T, step: T, min: T, max: T) -> T
130where
131 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
132{
133 if value < min + step {
134 min
135 } else {
136 value - step
137 }
138}
139
140impl<'a, T, Message> From<SpinButton<'a, T, Message>> for Element<'a, Message>
141where
142 Message: Clone + 'static,
143 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
144{
145 fn from(this: SpinButton<'a, T, Message>) -> Self {
146 match this.orientation {
147 Orientation::Horizontal => horizontal_variant(this),
148 Orientation::Vertical => vertical_variant(this),
149 }
150 }
151}
152macro_rules! make_button {
153 ($spin_button:expr, $icon:expr, $operation:expr) => {{
154 #[cfg(target_os = "linux")]
155 let button = icon::from_name($icon);
156
157 #[cfg(not(target_os = "linux"))]
158 let button =
159 icon::from_svg_bytes(include_bytes!(concat!["../../res/icons/", $icon, ".svg"]))
160 .symbolic(true);
161
162 button
163 .apply(button::icon)
164 .on_press(($spin_button.on_press)($operation(
165 $spin_button.value,
166 $spin_button.step,
167 $spin_button.min,
168 $spin_button.max,
169 )))
170 }};
171}
172
173fn horizontal_variant<T, Message>(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message>
174where
175 Message: Clone + 'static,
176 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
177{
178 let decrement_button = make_button!(spin_button, "list-remove-symbolic", decrement);
179 let increment_button = make_button!(spin_button, "list-add-symbolic", increment);
180
181 let label = text::title4(spin_button.label)
182 .apply(container)
183 .center_x(Length::Fixed(48.0))
184 .align_y(Alignment::Center);
185
186 row::with_capacity(3)
187 .push(decrement_button)
188 .push(label)
189 .push(increment_button)
190 .align_y(Alignment::Center)
191 .apply(container)
192 .class(theme::Container::custom(container_style))
193 .into()
194}
195
196fn vertical_variant<T, Message>(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message>
197where
198 Message: Clone + 'static,
199 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
200{
201 let decrement_button = make_button!(spin_button, "list-remove-symbolic", decrement);
202 let increment_button = make_button!(spin_button, "list-add-symbolic", increment);
203
204 let label = text::title4(spin_button.label)
205 .apply(container)
206 .center_x(Length::Fixed(48.0))
207 .align_y(Alignment::Center);
208
209 column::with_capacity(3)
210 .push(increment_button)
211 .push(label)
212 .push(decrement_button)
213 .align_x(Alignment::Center)
214 .apply(container)
215 .class(theme::Container::custom(container_style))
216 .into()
217}
218
219#[allow(clippy::trivially_copy_pass_by_ref)]
220fn container_style(theme: &crate::Theme) -> iced_widget::container::Style {
221 let cosmic_theme = &theme.cosmic();
222 let mut neutral_10 = cosmic_theme.palette.neutral_10;
223 neutral_10.alpha = 0.1;
224 let accent = &cosmic_theme.accent;
225 let corners = &cosmic_theme.corner_radii;
226 let border = if theme.theme_type.is_high_contrast() {
227 let current_container = theme.current_container();
228 Border {
229 radius: corners.radius_s.into(),
230 width: 1.,
231 color: current_container.component.border.into(),
232 }
233 } else {
234 Border {
235 radius: corners.radius_s.into(),
236 width: 0.0,
237 color: accent.base.into(),
238 }
239 };
240
241 iced_widget::container::Style {
242 icon_color: Some(accent.base.into()),
243 text_color: Some(cosmic_theme.palette.neutral_10.into()),
244 background: None,
245 border,
246 shadow: Shadow::default(),
247 }
248}
249
250#[cfg(test)]
251mod tests {
252 #[test]
253 fn decrement() {
254 assert_eq!(super::decrement(0i32, 10, 15, 35), 15);
255 }
256}