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::{
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
16/// Horizontal spin button widget.
17pub 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
39/// Vertical spin button widget.
40pub 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    /// The formatted value of the spin button.
73    label: Cow<'a, str>,
74    /// The current value of the spin button.
75    value: T,
76    /// The amount to increment or decrement the value.
77    step: T,
78    /// The minimum value permitted.
79    min: T,
80    /// The maximum value permitted.
81    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    /// Create a new new button
91    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}