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