// Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 //! A control for incremental adjustments of a value. use crate::{ theme, widget::{button, column, container, icon, row, text}, Element, }; use apply::Apply; use derive_setters::Setters; use iced::{alignment::Horizontal, Border, Shadow}; use iced::{Alignment, Length}; use std::marker::PhantomData; use std::ops::{Add, Sub}; use std::{borrow::Cow, fmt::Display}; /// Horizontal spin button widget. pub fn spin_button<'a, T, M>( label: impl Into>, value: T, step: T, min: T, max: T, on_press: impl Fn(T) -> M + 'static, ) -> SpinButton<'a, T, M> where T: Copy + Sub + Add + PartialOrd, { SpinButton::new( label, value, step, min, max, Orientation::Horizontal, on_press, ) } /// Vertical spin button widget. pub fn vertical<'a, T, M>( label: impl Into>, value: T, step: T, min: T, max: T, on_press: impl Fn(T) -> M + 'static, ) -> SpinButton<'a, T, M> where T: Copy + Sub + Add + PartialOrd, { SpinButton::new( label, value, step, min, max, Orientation::Vertical, on_press, ) } #[derive(Clone, Copy)] enum Orientation { Horizontal, Vertical, } pub struct SpinButton<'a, T, M> where T: Copy + Sub + Add + PartialOrd, { /// The formatted value of the spin button. label: Cow<'a, str>, /// The current value of the spin button. value: T, /// The amount to increment or decrement the value. step: T, /// The minimum value permitted. min: T, /// The maximum value permitted. max: T, orientation: Orientation, on_press: Box M>, } impl<'a, T, M> SpinButton<'a, T, M> where T: Copy + Sub + Add + PartialOrd, { /// Create a new new button fn new( label: impl Into>, value: T, step: T, min: T, max: T, orientation: Orientation, on_press: impl Fn(T) -> M + 'static, ) -> Self { Self { label: label.into(), step, value: if value < min { min } else if value > max { max } else { value }, min, max, orientation, on_press: Box::from(on_press), } } } fn increment(value: T, step: T, min: T, max: T) -> T where T: Copy + Sub + Add + PartialOrd, { if value > max - step { max } else { value + step } } fn decrement(value: T, step: T, min: T, max: T) -> T where T: Copy + Sub + Add + PartialOrd, { if value < min + step { min } else { value - step } } impl<'a, T, Message> From> for Element<'a, Message> where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, { fn from(this: SpinButton<'a, T, Message>) -> Self { match this.orientation { Orientation::Horizontal => horizontal_variant(this), Orientation::Vertical => vertical_variant(this), } } } fn horizontal_variant<'a, T, Message>( spin_button: SpinButton<'a, T, Message>, ) -> Element<'a, Message> where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, { let decrement_button = icon::from_name("list-remove-symbolic") .apply(button::icon) .on_press((spin_button.on_press)(decrement::( spin_button.value, spin_button.step, spin_button.min, spin_button.max, ))); let increment_button = icon::from_name("list-add-symbolic") .apply(button::icon) .on_press((spin_button.on_press)(increment::( spin_button.value, spin_button.step, spin_button.min, spin_button.max, ))); let label = text::title4(spin_button.label) .apply(container) .center_x(Length::Fixed(48.0)) .align_y(Alignment::Center); row::with_capacity(3) .push(decrement_button) .push(label) .push(increment_button) .align_y(Alignment::Center) .apply(container) .class(theme::Container::custom(container_style)) .into() } fn vertical_variant<'a, T, Message>(spin_button: SpinButton<'a, T, Message>) -> Element<'a, Message> where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, { let decrement_button = icon::from_name("list-remove-symbolic") .apply(button::icon) .on_press((spin_button.on_press)(decrement::( spin_button.value, spin_button.step, spin_button.min, spin_button.max, ))); let increment_button = icon::from_name("list-add-symbolic") .apply(button::icon) .on_press((spin_button.on_press)(increment::( spin_button.value, spin_button.step, spin_button.min, spin_button.max, ))); let label = text::title4(spin_button.label) .apply(container) .center_x(Length::Fixed(48.0)) .align_y(Alignment::Center); column::with_capacity(3) .push(increment_button) .push(label) .push(decrement_button) .align_x(Alignment::Center) .apply(container) .class(theme::Container::custom(container_style)) .into() } #[allow(clippy::trivially_copy_pass_by_ref)] fn container_style(theme: &crate::Theme) -> iced_widget::container::Style { let cosmic_theme = &theme.cosmic(); let mut neutral_10 = cosmic_theme.palette.neutral_10; neutral_10.alpha = 0.1; let accent = &cosmic_theme.accent; let corners = &cosmic_theme.corner_radii; iced_widget::container::Style { icon_color: Some(accent.base.into()), text_color: Some(cosmic_theme.palette.neutral_10.into()), background: None, border: Border { radius: corners.radius_s.into(), width: 0.0, color: accent.base.into(), }, shadow: Shadow::default(), } } #[cfg(test)] mod tests { #[test] fn decrement() { assert_eq!(super::decrement(0i32, 10, 15, 35), 15); } }