Skip to main content

cosmic/widget/
spin_button.rs

1// Copyright 2022 System76 <info@system76.com>
2// SPDX-License-Identifier: MPL-2.0
3
4//! A control for incremental adjustments of a value.
5
6use 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
13/// Horizontal spin button widget.
14pub 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
44/// Vertical spin button widget.
45pub 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    /// The formatted value of the spin button.
86    label: Cow<'a, str>,
87    /// A name for screen reader support.
88    #[cfg(feature = "a11y")]
89    name: Cow<'a, str>,
90    /// The current value of the spin button.
91    value: T,
92    /// The amount to increment or decrement the value.
93    step: T,
94    /// The minimum value permitted.
95    min: T,
96    /// The maximum value permitted.
97    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    /// Create a new new button
107    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}