From b14fde9033e02694e7fa4be6c49c5fcee6d51200 Mon Sep 17 00:00:00 2001 From: Bryan Hyland Date: Tue, 19 Nov 2024 08:17:40 -0800 Subject: [PATCH] feat!(spin_button): refactor and support vertical widget variant --- Cargo.toml | 1 - examples/spin-button/Cargo.toml | 12 ++ examples/spin-button/src/main.rs | 201 ++++++++++++++++++++++++ src/widget/mod.rs | 2 +- src/widget/spin_button.rs | 260 +++++++++++++++++++++++++++++++ src/widget/spin_button/mod.rs | 108 ------------- src/widget/spin_button/model.rs | 156 ------------------- 7 files changed, 474 insertions(+), 266 deletions(-) create mode 100644 examples/spin-button/Cargo.toml create mode 100644 examples/spin-button/src/main.rs create mode 100644 src/widget/spin_button.rs delete mode 100644 src/widget/spin_button/mod.rs delete mode 100644 src/widget/spin_button/model.rs diff --git a/Cargo.toml b/Cargo.toml index 1fced16c502..bc2d4237e72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -95,7 +95,6 @@ cosmic-config = { path = "cosmic-config" } cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } css-color = "0.2.5" derive_setters = "0.1.5" -fraction = "0.15.3" image = { version = "0.25.1", optional = true } lazy_static = "1.4.0" libc = { version = "0.2.155", optional = true } diff --git a/examples/spin-button/Cargo.toml b/examples/spin-button/Cargo.toml new file mode 100644 index 00000000000..3088a313357 --- /dev/null +++ b/examples/spin-button/Cargo.toml @@ -0,0 +1,12 @@ +[package] +name = "spin-button" +version = "0.1.0" +edition = "2021" + +[dependencies] +fraction = "0.15.3" + +[dependencies.libcosmic] +features = ["debug", "multi-window", "wayland", "winit", "desktop", "tokio"] +path = "../.." +default-features = false diff --git a/examples/spin-button/src/main.rs b/examples/spin-button/src/main.rs new file mode 100644 index 00000000000..602f3f4a4a2 --- /dev/null +++ b/examples/spin-button/src/main.rs @@ -0,0 +1,201 @@ +use cosmic::iced::Length; +use cosmic::widget::{column, container, spin_button}; +use cosmic::Apply; +use cosmic::{ + app::{Core, Task}, + iced::{ + self, + alignment::{Horizontal, Vertical}, + Alignment, Size, + }, + Application, Element, +}; +use fraction::Decimal; + +pub struct SpinButtonExamplApp { + core: Core, + i8_num: i8, + i8_str: String, + i16_num: i16, + i16_str: String, + i32_num: i32, + i32_str: String, + i64_num: i64, + i64_str: String, + i128_num: i128, + i128_str: String, + f32_num: f32, + f32_str: String, + f64_num: f64, + f64_str: String, + dec_num: Decimal, + dec_str: String, +} + +#[derive(Debug, Clone)] +pub enum Message { + UpdateI8(i8), + UpdateI16(i16), + UpdateI32(i32), + UpdateI64(i64), + UpdateI128(i128), + UpdateF32(f32), + UpdateF64(f64), + UpdateDec(Decimal), +} + +impl Application for SpinButtonExamplApp { + type Executor = cosmic::executor::Default; + type Flags = (); + type Message = Message; + + const APP_ID: &'static str = "com.system76.SpinButtonExample"; + + fn core(&self) -> &Core { + &self.core + } + + fn core_mut(&mut self) -> &mut Core { + &mut self.core + } + + fn init(core: Core, _flags: Self::Flags) -> (Self, Task) { + ( + Self { + core, + i8_num: 0, + i8_str: 0.to_string(), + i16_num: 0, + i16_str: 0.to_string(), + i32_num: 0, + i32_str: 0.to_string(), + i64_num: 15, + i64_str: 15.to_string(), + i128_num: 0, + i128_str: 0.to_string(), + f32_num: 0., + f32_str: format!("{:.02}", 0.0), + f64_num: 0., + f64_str: format!("{:.02}", 0.0), + dec_num: Decimal::from(0.0), + dec_str: format!("{:.02}", 0.0), + }, + Task::none(), + ) + } + + fn update(&mut self, message: Self::Message) -> Task { + match message { + Message::UpdateI8(value) => { + self.i8_num = value; + self.i8_str = value.to_string(); + } + + Message::UpdateI16(value) => { + self.i16_num = value; + self.i16_str = value.to_string(); + } + + Message::UpdateI32(value) => { + self.i32_num = value; + self.i32_str = value.to_string(); + } + + Message::UpdateI64(value) => { + self.i64_num = value; + self.i64_str = value.to_string(); + } + + Message::UpdateI128(value) => { + self.i128_num = value; + self.i128_str = value.to_string(); + } + + Message::UpdateF32(value) => { + self.f32_num = value; + self.f32_str = format!("{value:.02}"); + } + + Message::UpdateF64(value) => { + self.f64_num = value; + self.f64_str = format!("{value:.02}"); + } + + Message::UpdateDec(value) => { + self.dec_num = value; + self.dec_str = format!("{value:.02}"); + } + } + + Task::none() + } + + fn view(&self) -> Element { + let space_xs = cosmic::theme::active().cosmic().spacing.space_xs; + + let vert_spinner_row = iced::widget::row![ + spin_button::vertical(&self.i8_str, self.i8_num, 1, -5, 5, Message::UpdateI8), + spin_button::vertical(&self.i16_str, self.i16_num, 1, 0, 10, Message::UpdateI16), + spin_button::vertical(&self.i32_str, self.i32_num, 1, 0, 12, Message::UpdateI32), + spin_button::vertical(&self.i64_str, self.i64_num, 10, 15, 35, Message::UpdateI64), + ] + .spacing(space_xs) + .align_y(Vertical::Center); + + let horiz_spinner_row = iced::widget::column![ + spin_button( + &self.i128_str, + self.i128_num, + 100, + -1000, + 500, + Message::UpdateI128 + ), + spin_button( + &self.f32_str, + self.f32_num, + 1.3, + -35.3, + 12.3, + Message::UpdateF32 + ), + spin_button( + &self.f64_str, + self.f64_num, + 1.3, + 0.0, + 3.0, + Message::UpdateF64 + ), + spin_button( + &self.dec_str, + self.dec_num, + Decimal::from(0.25), + Decimal::from(-5.0), + Decimal::from(5.0), + Message::UpdateDec + ), + ] + .spacing(space_xs) + .align_x(Alignment::Center); + + column::with_capacity(3) + .push(vert_spinner_row) + .push(horiz_spinner_row) + .spacing(space_xs) + .align_x(Alignment::Center) + .apply(container) + .width(Length::Fill) + .height(Length::Fill) + .align_x(Horizontal::Center) + .align_y(Vertical::Center) + .into() + } +} + +fn main() -> Result<(), Box> { + let settings = cosmic::app::Settings::default().size(Size::new(550., 1024.)); + cosmic::app::run::(settings, ())?; + + Ok(()) +} diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 52acf06d44c..06dd4e85b45 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -311,7 +311,7 @@ pub mod settings; pub mod spin_button; #[doc(inline)] -pub use spin_button::{spin_button, SpinButton}; +pub use spin_button::{spin_button, vertical as vertical_spin_button, SpinButton}; pub mod tab_bar; diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs new file mode 100644 index 00000000000..6c0df95f310 --- /dev/null +++ b/src/widget/spin_button.rs @@ -0,0 +1,260 @@ +// 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); + } +} diff --git a/src/widget/spin_button/mod.rs b/src/widget/spin_button/mod.rs deleted file mode 100644 index 9da2939496c..00000000000 --- a/src/widget/spin_button/mod.rs +++ /dev/null @@ -1,108 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! A control for incremental adjustments of a value. - -mod model; -use std::borrow::Cow; - -pub use self::model::{Message, Model}; - -use crate::widget::{button, container, icon, row, text}; -use crate::{theme, Element}; -use apply::Apply; -use iced::{Alignment, Length}; -use iced_core::{Border, Shadow}; - -pub struct SpinButton<'a, Message> { - label: Cow<'a, str>, - on_change: Box Message + 'static>, -} - -/// A control for incremental adjustments of a value. -pub fn spin_button<'a, Message: 'static>( - label: impl Into>, - on_change: impl Fn(model::Message) -> Message + 'static, -) -> SpinButton<'a, Message> { - SpinButton::new(label, on_change) -} - -impl<'a, Message: 'static> SpinButton<'a, Message> { - pub fn new( - label: impl Into>, - on_change: impl Fn(model::Message) -> Message + 'static, - ) -> Self { - Self { - on_change: Box::from(on_change), - label: label.into(), - } - } - - #[must_use] - pub fn into_element(self) -> Element<'a, Message> { - let Self { on_change, label } = self; - container( - row::with_children(vec![ - icon::from_name("list-remove-symbolic") - .size(16) - .apply(container) - .center(Length::Fixed(32.0)) - .apply(button::custom) - .width(Length::Fixed(32.0)) - .height(Length::Fixed(32.0)) - .class(theme::Button::Text) - .on_press(model::Message::Decrement) - .into(), - text::title4(label) - .apply(container) - .center_x(Length::Fixed(48.0)) - .align_y(Alignment::Center) - .into(), - icon::from_name("list-add-symbolic") - .size(16) - .apply(container) - .center(Length::Fixed(32.0)) - .apply(button::custom) - .width(Length::Fixed(32.0)) - .height(Length::Fixed(32.0)) - .class(theme::Button::Text) - .on_press(model::Message::Increment) - .into(), - ]) - .width(Length::Shrink) - .height(Length::Fixed(32.0)) - .align_y(Alignment::Center), - ) - .width(Length::Shrink) - .center_y(Length::Fixed(32.0)) - .class(theme::Container::custom(container_style)) - .apply(Element::from) - .map(on_change) - } -} - -impl<'a, Message: 'static> From> for Element<'a, Message> { - fn from(spin_button: SpinButton<'a, Message>) -> Self { - spin_button.into_element() - } -} - -#[allow(clippy::trivially_copy_pass_by_ref)] -fn container_style(theme: &crate::Theme) -> iced_widget::container::Style { - let basic = &theme.cosmic(); - let mut neutral_10 = basic.palette.neutral_10; - neutral_10.alpha = 0.1; - let accent = &basic.accent; - let corners = &basic.corner_radii; - iced_widget::container::Style { - icon_color: Some(basic.palette.neutral_10.into()), - text_color: Some(basic.palette.neutral_10.into()), - background: None, - border: Border { - radius: corners.radius_s.into(), - width: 0.0, - color: accent.base.into(), - }, - shadow: Shadow::default(), - } -} diff --git a/src/widget/spin_button/model.rs b/src/widget/spin_button/model.rs deleted file mode 100644 index e617bc877a9..00000000000 --- a/src/widget/spin_button/model.rs +++ /dev/null @@ -1,156 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -use derive_setters::Setters; -use fraction::{Bounded, Decimal}; -use std::hash::Hash; -use std::ops::{Add, Sub}; - -/// A message emitted by the [`SpinButton`](super) widget. -#[derive(Clone, Copy, Debug, Hash)] -pub enum Message { - Increment, - Decrement, -} - -#[derive(Setters)] -pub struct Model { - /// The current value of the spin button. - #[setters(into)] - pub value: T, - /// The amount to increment the value. - #[setters(into)] - pub step: T, - /// The minimum value permitted. - #[setters(into)] - pub min: T, - /// The maximum value permitted. - #[setters(into)] - pub max: T, -} - -impl Model -where - T: Copy + Hash + Sub + Add + Ord, -{ - pub fn update(&mut self, message: Message) { - self.value = match message { - Message::Increment => { - std::cmp::min(std::cmp::max(self.value + self.step, self.min), self.max) - } - Message::Decrement => { - std::cmp::max(std::cmp::min(self.value - self.step, self.max), self.min) - } - } - } -} - -impl Default for Model { - fn default() -> Self { - Self { - value: 0, - step: 1, - min: i8::MIN, - max: i8::MAX, - } - } -} - -impl Default for Model { - fn default() -> Self { - Self { - value: 0, - step: 1, - min: i16::MIN, - max: i16::MAX, - } - } -} - -impl Default for Model { - fn default() -> Self { - Self { - value: 0, - step: 1, - min: i32::MIN, - max: i32::MAX, - } - } -} - -impl Default for Model { - fn default() -> Self { - Self { - value: 0, - step: 1, - min: isize::MIN, - max: isize::MAX, - } - } -} - -impl Default for Model { - fn default() -> Self { - Self { - value: 0, - step: 1, - min: u8::MIN, - max: u8::MAX, - } - } -} - -impl Default for Model { - fn default() -> Self { - Self { - value: 0, - step: 1, - min: u16::MIN, - max: u16::MAX, - } - } -} - -impl Default for Model { - fn default() -> Self { - Self { - value: 0, - step: 1, - min: u32::MIN, - max: u32::MAX, - } - } -} - -impl Default for Model { - fn default() -> Self { - Self { - value: 0, - step: 1, - min: usize::MIN, - max: usize::MAX, - } - } -} - -impl Default for Model { - fn default() -> Self { - Self { - value: 0, - step: 1, - min: u64::MIN, - max: u64::MAX, - } - } -} - -impl Default for Model { - fn default() -> Self { - Self { - value: Decimal::from(0.0), - step: Decimal::from(0.0), - min: Decimal::min_positive_value(), - max: Decimal::max_value(), - } - } -}