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 #[cfg(feature = "a11y")] name: impl Into<Cow<'a, str>>,
20 value: T,
21 step: T,
22 min: T,
23 max: T,
24 on_press: impl Fn(T) -> M + 'static,
25) -> SpinButton<'a, T, M>
26where
27 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
28{
29 let mut button = SpinButton::new(
30 label,
31 value,
32 step,
33 min,
34 max,
35 Orientation::Horizontal,
36 on_press,
37 );
38
39 #[cfg(feature = "a11y")]
40 {
41 button = button.name(name.into());
42 }
43
44 button
45}
46
47pub fn vertical<'a, T, M>(
49 label: impl Into<Cow<'a, str>>,
50 #[cfg(feature = "a11y")] name: impl Into<Cow<'a, str>>,
51 value: T,
52 step: T,
53 min: T,
54 max: T,
55 on_press: impl Fn(T) -> M + 'static,
56) -> SpinButton<'a, T, M>
57where
58 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
59{
60 let mut button = SpinButton::new(
61 label,
62 value,
63 step,
64 min,
65 max,
66 Orientation::Horizontal,
67 on_press,
68 );
69
70 #[cfg(feature = "a11y")]
71 {
72 button = button.name(name.into());
73 }
74
75 button
76}
77
78#[derive(Clone, Copy)]
79enum Orientation {
80 Horizontal,
81 Vertical,
82}
83
84pub struct SpinButton<'a, T, M>
85where
86 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
87{
88 label: Cow<'a, str>,
90 #[cfg(feature = "a11y")]
92 name: Cow<'a, str>,
93 value: T,
95 step: T,
97 min: T,
99 max: T,
101 orientation: Orientation,
102 on_press: Box<dyn Fn(T) -> M>,
103}
104
105impl<'a, T, M> SpinButton<'a, T, M>
106where
107 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
108{
109 fn new(
111 label: impl Into<Cow<'a, str>>,
112 value: T,
113 step: T,
114 min: T,
115 max: T,
116 orientation: Orientation,
117 on_press: impl Fn(T) -> M + 'static,
118 ) -> Self {
119 Self {
120 label: label.into(),
121 #[cfg(feature = "a11y")]
122 name: Cow::Borrowed(""),
123 step,
124 value: if value < min {
125 min
126 } else if value > max {
127 max
128 } else {
129 value
130 },
131 min,
132 max,
133 orientation,
134 on_press: Box::from(on_press),
135 }
136 }
137
138 #[cfg(feature = "a11y")]
139 pub(self) fn name(mut self, name: Cow<'a, str>) -> Self {
140 self.name = name;
141 self
142 }
143}
144
145fn increment<T>(value: T, step: T, _min: T, max: T) -> T
146where
147 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
148{
149 if value > max - step {
150 max
151 } else {
152 value + step
153 }
154}
155
156fn decrement<T>(value: T, step: T, min: T, _max: T) -> T
157where
158 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
159{
160 if value < min + step {
161 min
162 } else {
163 value - step
164 }
165}
166
167impl<'a, T, Message> From<SpinButton<'a, T, Message>> for Element<'a, Message>
168where
169 Message: Clone + 'static,
170 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
171{
172 fn from(this: SpinButton<'a, T, Message>) -> Self {
173 match this.orientation {
174 Orientation::Horizontal => horizontal_variant(this),
175 Orientation::Vertical => vertical_variant(this),
176 }
177 }
178}
179
180fn make_button<'a, T, Message>(
181 spin_button: &SpinButton<'a, T, Message>,
182 icon: &'static str,
183 #[cfg(feature = "a11y")] name: String,
184 operation: Option<fn(T, T, T, T) -> T>,
185) -> Element<'a, Message>
186where
187 Message: Clone + 'static,
188 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
189{
190 let mut button = icon::from_name(icon).apply(button::icon);
191
192 if let Some(f) = operation {
193 button = button.on_press((spin_button.on_press)(f(
194 spin_button.value,
195 spin_button.step,
196 spin_button.min,
197 spin_button.max,
198 )))
199 };
200
201 #[cfg(feature = "a11y")]
202 {
203 button = button.name(name.clone());
204 }
205
206 button.into()
207}
208
209fn horizontal_variant<T, Message>(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message>
210where
211 Message: Clone + 'static,
212 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
213{
214 let decrement_button = make_button(
215 &spin_button,
216 "list-remove-symbolic",
217 #[cfg(feature = "a11y")]
218 [&spin_button.name, " decrease"].concat(),
219 match spin_button.value == spin_button.min {
220 true => None,
221 false => Some(decrement),
222 },
223 );
224 let increment_button = make_button(
225 &spin_button,
226 "list-add-symbolic",
227 #[cfg(feature = "a11y")]
228 [&spin_button.name, " increase"].concat(),
229 match spin_button.value == spin_button.max {
230 true => None,
231 false => Some(increment),
232 },
233 );
234 let label = text::body(spin_button.label)
235 .apply(container)
236 .center_x(Length::Fixed(48.0))
237 .align_y(Alignment::Center);
238
239 row::with_capacity(3)
240 .push(decrement_button)
241 .push(label)
242 .push(increment_button)
243 .align_y(Alignment::Center)
244 .apply(container)
245 .class(theme::Container::custom(container_style))
246 .into()
247}
248
249fn vertical_variant<T, Message>(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message>
250where
251 Message: Clone + 'static,
252 T: Copy + Sub<Output = T> + Add<Output = T> + PartialOrd,
253{
254 let decrement_button = make_button(
255 &spin_button,
256 "list-remove-symbolic",
257 #[cfg(feature = "a11y")]
258 [&spin_button.label, " decrease"].concat(),
259 match spin_button.value == spin_button.min {
260 true => None,
261 false => Some(decrement),
262 },
263 );
264 let increment_button = make_button(
265 &spin_button,
266 "list-add-symbolic",
267 #[cfg(feature = "a11y")]
268 [&spin_button.label, " increase"].concat(),
269 match spin_button.value == spin_button.max {
270 true => None,
271 false => Some(increment),
272 },
273 );
274
275 let label = text::body(spin_button.label)
276 .apply(container)
277 .center_x(Length::Fixed(48.0))
278 .align_y(Alignment::Center);
279
280 column::with_capacity(3)
281 .push(increment_button)
282 .push(label)
283 .push(decrement_button)
284 .align_x(Alignment::Center)
285 .apply(container)
286 .class(theme::Container::custom(container_style))
287 .into()
288}
289
290#[allow(clippy::trivially_copy_pass_by_ref)]
291fn container_style(theme: &crate::Theme) -> iced_widget::container::Style {
292 let cosmic_theme = &theme.cosmic();
293 let accent = &cosmic_theme.accent;
294 let corners = &cosmic_theme.corner_radii;
295 let current_container = theme.current_container();
296 let border = if theme.theme_type.is_high_contrast() {
297 Border {
298 radius: corners.radius_s.into(),
299 width: 1.,
300 color: current_container.component.border.into(),
301 }
302 } else {
303 Border {
304 radius: corners.radius_s.into(),
305 width: 0.0,
306 color: accent.base.into(),
307 }
308 };
309
310 iced_widget::container::Style {
311 icon_color: Some(current_container.on.into()),
312 text_color: Some(current_container.on.into()),
313 background: None,
314 border,
315 shadow: Shadow::default(),
316 snap: true,
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 #[test]
323 fn decrement() {
324 assert_eq!(super::decrement(0i32, 10, 15, 35), 15);
325 }
326}