From fa13116dd6248fc93aec52a7c2b8b4e9fcbbae64 Mon Sep 17 00:00:00 2001 From: Bryan Hyland Date: Tue, 19 Nov 2024 08:17:40 -0800 Subject: [PATCH] feat!(dropdown): refactor and support vertical widget variant --- examples/spin-button/Cargo.toml | 9 + examples/spin-button/src/main.rs | 175 +++++++++++++++++ src/widget/mod.rs | 2 +- src/widget/spin_button/mod.rs | 315 +++++++++++++++++++++++-------- src/widget/spin_button/model.rs | 156 --------------- 5 files changed, 424 insertions(+), 233 deletions(-) create mode 100644 examples/spin-button/Cargo.toml create mode 100644 examples/spin-button/src/main.rs delete mode 100644 src/widget/spin_button/model.rs diff --git a/examples/spin-button/Cargo.toml b/examples/spin-button/Cargo.toml new file mode 100644 index 00000000000..277daeea617 --- /dev/null +++ b/examples/spin-button/Cargo.toml @@ -0,0 +1,9 @@ +[package] +name = "spin-button" +version = "0.1.0" +edition = "2021" + +[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..7b2980a528b --- /dev/null +++ b/examples/spin-button/src/main.rs @@ -0,0 +1,175 @@ +use cosmic::widget::{button, container, spin_button, text}; +use cosmic::{ + app::{Core, Task}, + iced::{ + alignment::{Horizontal, Vertical}, + widget::{column, row, vertical_space}, + Alignment, Size, + }, + Application, Element, +}; + +pub struct SpinButtonExamplApp { + core: Core, + i8_num: i8, + i16_num: i16, + i32_num: i32, + i64_num: i64, + i128_num: i128, + f32_num: f32, + f64_num: f64, + spinner_msg: String, +} + +#[derive(Debug, Clone)] +pub enum SpinBtnMessages { + UpdateI8Num(i8), + UpdateI16Num(i16), + UpdateI32Num(i32), + UpdateI64Num(i64), + UpdateI128Num(i128), + UpdateF32Num(f32), + UpdateF64Num(f64), + UpdateSpinnerMsg, +} + +impl Application for SpinButtonExamplApp { + type Executor = cosmic::executor::Default; + type Flags = (); + type Message = SpinBtnMessages; + + 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, + i16_num: 0, + i32_num: 0, + i64_num: 0, + i128_num: 0, + f32_num: 0., + f64_num: 0., + spinner_msg: String::new(), + }, + Task::none(), + ) + } + + fn update(&mut self, message: Self::Message) -> Task { + match message { + SpinBtnMessages::UpdateI8Num(new_i8) => self.i8_num = new_i8, + SpinBtnMessages::UpdateI16Num(new_i16) => self.i16_num = new_i16, + SpinBtnMessages::UpdateI32Num(new_i32) => self.i32_num = new_i32, + SpinBtnMessages::UpdateI64Num(new_i64) => self.i64_num = new_i64, + SpinBtnMessages::UpdateI128Num(new_i128) => self.i128_num = new_i128, + SpinBtnMessages::UpdateF32Num(new_f32) => self.f32_num = new_f32, + SpinBtnMessages::UpdateF64Num(new_f64) => self.f64_num = new_f64, + SpinBtnMessages::UpdateSpinnerMsg => { + self.spinner_msg = format!( + "i8: {}, i16: {}, i32: {}, i64: {}, i128: {}\nf32: {}, f64: {}", + self.i8_num, + self.i16_num, + self.i32_num, + self.i64_num, + self.i128_num, + self.f32_num, + self.f64_num + ); + } + } + + Task::none() + } + + fn view(&self) -> Element { + let vert_spinner_row = row![ + spin_button::vertical( + "i8", // label: displayed above the widget no matter the orientation + 1, // step: how much to increment/decrement by + self.i8_num, // current value, this is also what's displayed in the center of the widget + -5, // minimum value, if decremented below this the widget's current value rolls to the max value + 5, // maximum value, if incremented above this the widget's current value rolls to the min value + SpinBtnMessages::UpdateI8Num // message to send to the application's update function + ), + spin_button::vertical("i16", 1, self.i16_num, 0, 10, SpinBtnMessages::UpdateI16Num), + spin_button::vertical("i32", 1, self.i32_num, 0, 12, SpinBtnMessages::UpdateI32Num), + spin_button::vertical( + "i64", + 10, + self.i64_num, + 15, + 35, + SpinBtnMessages::UpdateI64Num + ), + ] + .align_y(Vertical::Center); + + let horiz_spinner_row = column![ + row![ + // This function can be called instead if a Horizontal Spin Button is needed. + spin_button( + "i128", + 100, + self.i128_num, + -1000, + 500, + SpinBtnMessages::UpdateI128Num + ), + ], + vertical_space().height(5), + row![spin_button( + "f32", + 1.3, + self.f32_num, + -35.3, + 12.3, + SpinBtnMessages::UpdateF32Num + )], + vertical_space().height(5), + row![spin_button( + "f64", + 1.3, + self.f64_num, + 0.0, + 3.0, + SpinBtnMessages::UpdateF64Num + )], + ] + .align_x(Alignment::Center); + + let status_row = row![text(self.spinner_msg.clone()),]; + + let final_col = column![ + vert_spinner_row, + vertical_space().height(5), + horiz_spinner_row, + button::standard("Show Spinner Values Passed") + .on_press(SpinBtnMessages::UpdateSpinnerMsg), + vertical_space().height(10), + status_row, + ] + .align_x(Alignment::Center); + + container(final_col) + .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/mod.rs b/src/widget/spin_button/mod.rs index 9da2939496c..3eac1629ead 100644 --- a/src/widget/spin_button/mod.rs +++ b/src/widget/spin_button/mod.rs @@ -3,106 +3,269 @@ //! 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 crate::{ + widget::{button, column, container, icon, row, text}, + Element, +}; use apply::Apply; +use iced::alignment::Horizontal; use iced::{Alignment, Length}; -use iced_core::{Border, Shadow}; +use std::fmt::Display; +use std::marker::PhantomData; +use std::ops::{Add, Sub}; -pub struct SpinButton<'a, Message> { - label: Cow<'a, str>, - on_change: Box Message + 'static>, +#[derive(Clone, Copy)] +enum Orientation { + Horizontal, + Vertical, } -/// 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) +pub struct SpinButton<'a, T, M> +where + T: Add + Sub + PartialEq + PartialOrd + Display + Copy, +{ + /// The label that the spin button widget will have. + /// It is placed on the top of and centered on the spin button widget itself. + label: String, + /// The amount to increment or decrement the value. + step: T, + /// The current value of the spin button. + /// It is displayed in the center of the spin button widget, no matter the orientation. + value: T, + /// The minimum value permitted. + /// If the value is decremented below this value the current value will rollover to the max value. + min: T, + /// The maximum value permitted. + /// If the value is incremented above this value the current value will rollover to the min value. + max: T, + /// The direction that the spin button is laid out; Orientation::Horizontal or Orientation::Vertical + orientation: Orientation, + /// The message that the spin button emits to the application's update function. + on_press: Box M>, + phantom_data: PhantomData<&'a M>, } -impl<'a, Message: 'static> SpinButton<'a, Message> { - pub fn new( - label: impl Into>, - on_change: impl Fn(model::Message) -> Message + 'static, +impl<'a, T, M> SpinButton<'a, T, M> +where + T: Add + Sub + PartialEq + PartialOrd + Display + Copy, +{ + /// Create a new new button + fn new( + label: impl Into, + step: T, + value: T, + min: T, + max: T, + orientation: Orientation, + on_press: impl Fn(T) -> M + 'static, ) -> Self { Self { - on_change: Box::from(on_change), label: label.into(), + step, + value, + min, + max, + orientation, + on_press: Box::from(on_press), + phantom_data: PhantomData, } } +} + +/// Shorthand function to create a new spin button +pub fn spin_button<'a, T, M>( + label: impl Into, + step: T, + value: T, + min: T, + max: T, + on_press: impl Fn(T) -> M + 'static, +) -> SpinButton<'a, T, M> +where + T: Add + Sub + PartialEq + PartialOrd + Display + Copy, +{ + SpinButton::new( + label, + step, + value, + min, + max, + Orientation::Horizontal, + on_press, + ) +} + +/// Shorthand to create a standard (horizontal) spin button widget +pub fn vertical<'a, T, M>( + label: impl Into, + step: T, + value: T, + min: T, + max: T, + on_press: impl Fn(T) -> M + 'static, +) -> SpinButton<'a, T, M> +where + T: Add + Sub + PartialEq + PartialOrd + Display + Copy, +{ + SpinButton::new( + label, + step, + value, + min, + max, + Orientation::Vertical, + on_press, + ) +} + +fn increment(step: T, value: T, min: T, max: T) -> T +where + T: Add + Sub + PartialEq + PartialOrd + Display + Copy, +{ + if value + step > max { + max + } else { + value + step + } +} - #[must_use] - pub fn into_element(self) -> Element<'a, Message> { - let Self { on_change, label } = self; - container( +fn decrement(step: T, value: T, min: T, max: T) -> T +where + T: Add + Sub + PartialEq + PartialOrd + Display + Copy, +{ + if value - step < min { + min + } else { + value - step + } +} + +impl<'a, T, Message> From> for Element<'a, Message> +where + Message: Clone + 'static, + T: Add + Sub + PartialEq + PartialOrd + Display + Copy, +{ + fn from(this: SpinButton<'a, T, Message>) -> Self { + // Matching on the direction enum given by the developer when the + // widget is initially created in the application's view function. + match this.orientation { + Orientation::Horizontal => create_horizontal_spin_button(&this), + Orientation::Vertical => create_vertical_spin_button(&this), + } + } +} + +// Helper Functions +// Create a horizontal spin button +// Implemented to make the creation easier to read in the from function for Element implementation. +fn create_horizontal_spin_button<'a, T, Message>( + spin_btn: &SpinButton, +) -> Element<'a, Message> +where + Message: Clone + 'static, + T: Add + Sub + PartialEq + PartialOrd + Display + Copy, +{ + // Create a spinner container variable that contains the row with all of + // the combined widgets that make up the widget. + let spinner_container = column::with_capacity(2) + .push(row::with_children(vec![ + // Using the title4 variant of text, just like the original spin button did. + text::title4(spin_btn.label.clone()) + .apply(container) + .center_x(Length::Fill) + .align_y(Alignment::Center) + .into(), + ])) + .push( 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) + // Using an button instead of an icon for the decrement functionality. + button::icon(icon::from_name("list-remove-symbolic")) + .padding([0, 12]) + .on_press((spin_btn.on_press)(decrement::( + spin_btn.step, + spin_btn.value, + spin_btn.min, + spin_btn.max, + ))) .into(), - text::title4(label) + // Using the title4 variant of text for consistency. + text::title4(format!("{}", spin_btn.value)) .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) + // Using another button for the increment functionality. + button::icon(icon::from_name("list-add-symbolic")) + .padding([0, 12]) + .on_press((spin_btn.on_press)(increment::( + spin_btn.step, + spin_btn.value, + spin_btn.min, + spin_btn.max, + ))) .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) - } -} + .align_x(Alignment::Center); -impl<'a, Message: 'static> From> for Element<'a, Message> { - fn from(spin_button: SpinButton<'a, Message>) -> Self { - spin_button.into_element() - } + // Return the horizontal spin button from the match statement. + Element::new(spinner_container) } -#[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(), - } +// Used to create a vertical spin button widget. +// Implemented to make the creation easier to read in the from function for Element implementation. +fn create_vertical_spin_button<'a, T, Message>( + spin_btn: &SpinButton, +) -> Element<'a, Message> +where + Message: Clone + 'static, + T: Add + Sub + PartialEq + PartialOrd + Display + Copy, +{ + // Create a text widget that holds the value + let val_text = text::title4(format!("{}", spin_btn.value)); + // Create a spinner container variable that contains the column with all of + // the combined widgets that make up the widget. + let spinner_container = column::with_capacity(3) + .push( + // Use a button for the increment functionality + button::icon(icon::from_name("list-add-symbolic")) + .padding([5, 0]) + .on_press((spin_btn.on_press)(increment::( + spin_btn.step, + spin_btn.value, + spin_btn.min, + spin_btn.max, + ))), + ) + // Add the text widget that holds the current value + .push(val_text) + .push( + // Use a button for the decrement functionality + button::icon(icon::from_name("list-remove-symbolic")) + .padding([5, 0]) + .on_press((spin_btn.on_press)(decrement::( + spin_btn.step, + spin_btn.value, + spin_btn.min, + spin_btn.max, + ))), + ) + .align_x(Horizontal::Center); + + // Create a column that contains two rows: + // First Row -> The label/title for the spin button. + // Second Row -> The spin button container from above. + let content_list = column::with_children(vec![ + row::with_capacity(1) + .push(text::title4(spin_btn.label.clone())) + .into(), + row::with_children(vec![Element::from(spinner_container)]).into(), + ]) + .width(75) + .padding([8, 0]) + .align_x(Alignment::Center); + + // Return the vertical spin button from the match statement. + Element::new(content_list) } 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(), - } - } -}