From 37f07c71545b63d079412e225c4c801b33057a1e Mon Sep 17 00:00:00 2001 From: Bryan Hyland Date: Sat, 16 Nov 2024 13:46:49 -0800 Subject: [PATCH] Implemented a combined horizontal and vertical spin button implementation --- examples/vertical-spin-button/src/main.rs | 60 +++++-- src/widget/mod.rs | 2 +- src/widget/spin_button/mod.rs | 1 + src/widget/spin_button/spin_button.rs | 196 ++++++++++++++++++++++ 4 files changed, 241 insertions(+), 18 deletions(-) create mode 100644 src/widget/spin_button/spin_button.rs diff --git a/examples/vertical-spin-button/src/main.rs b/examples/vertical-spin-button/src/main.rs index 5b0e02ff..46a5de7a 100644 --- a/examples/vertical-spin-button/src/main.rs +++ b/examples/vertical-spin-button/src/main.rs @@ -1,4 +1,4 @@ -use cosmic::widget::{button, container, text, spin_button::vertical_spin_button}; +use cosmic::widget::{button, container, text, spin_button::vertical_spin_button, spin_button::spin_button}; use cosmic::{ app::{Core, Task}, iced::{ @@ -14,21 +14,24 @@ pub struct VertSpinnerApp { hours: i32, mins: i16, secs: i8, + spin_button_num: f32, time_msg: String, + spin_btn_msg: String, } #[derive(Debug, Clone)] -pub enum VertSpinnerMessages { +pub enum SpinBtnMessages { UpdateHours(i32), UpdateMins(i16), UpdateSecs(i8), + UpdateSpinBtnVal(f32), UpdateTimeMessage, } impl Application for VertSpinnerApp { type Executor = cosmic::executor::Default; type Flags = (); - type Message = VertSpinnerMessages; + type Message = SpinBtnMessages; const APP_ID: &'static str = "com.system76.VertSpinnerExample"; @@ -47,7 +50,9 @@ impl Application for VertSpinnerApp { hours: 0, mins: 0, secs: 0, + spin_button_num: 0.0, time_msg: String::new(), + spin_btn_msg: String::new(), }, Task::none(), ) @@ -55,10 +60,11 @@ impl Application for VertSpinnerApp { fn update(&mut self, message: Self::Message) -> Task { match message { - VertSpinnerMessages::UpdateHours(hours) => self.hours = hours, - VertSpinnerMessages::UpdateMins(mins) => self.mins = mins, - VertSpinnerMessages::UpdateSecs(secs) => self.secs = secs, - VertSpinnerMessages::UpdateTimeMessage => { + SpinBtnMessages::UpdateHours(hours) => self.hours = hours, + SpinBtnMessages::UpdateMins(mins) => self.mins = mins, + SpinBtnMessages::UpdateSecs(secs) => self.secs = secs, + SpinBtnMessages::UpdateSpinBtnVal(spin_btn_val) => self.spin_button_num = spin_btn_val, + SpinBtnMessages::UpdateTimeMessage => { self.time_msg = format!( "{}:{}:{}", self.hours, @@ -74,7 +80,9 @@ impl Application for VertSpinnerApp { } else { self.secs.to_string() } - ) + ); + + self.spin_btn_msg = format!("This came from the spin button: {}", self.spin_button_num); } } @@ -83,38 +91,56 @@ impl Application for VertSpinnerApp { fn view(&self) -> Element { let spinner_row = row![ - vertical_spin_button( + spin_button::spin_button( "Hours", 1, self.hours, 0, 12, - VertSpinnerMessages::UpdateHours + spin_button::Direction::Vertical, + SpinBtnMessages::UpdateHours ), - vertical_spin_button( + spin_button::spin_button( "Minutes", 1, self.mins, 0, 59, - VertSpinnerMessages::UpdateMins + spin_button::Direction::Vertical, + SpinBtnMessages::UpdateMins ), - vertical_spin_button( + spin_button::spin_button( "Seconds", 1, self.secs, 0, 59, - VertSpinnerMessages::UpdateSecs + spin_button::Direction::Vertical, + SpinBtnMessages::UpdateSecs ), + spin_button::spin_button( + "Spin Button Demo", + 0.5, + self.spin_button_num, + 0.0, + 10.0, + spin_button::Direction::Horizontal, + SpinBtnMessages::UpdateSpinBtnVal + ) ] .align_y(Vertical::Center); + let status_row = row![ + text(self.time_msg.clone()), + cosmic::widget::horizontal_space().width(10), + text(self.spin_btn_msg.clone()) + ]; + let final_col = column![ spinner_row, - button::standard("Update Time").on_press(VertSpinnerMessages::UpdateTimeMessage), + button::standard("Update Time").on_press(SpinBtnMessages::UpdateTimeMessage), vertical_space().height(10), - text(self.time_msg.clone()), + status_row, ] .align_x(Alignment::Center); @@ -126,7 +152,7 @@ impl Application for VertSpinnerApp { } fn main() -> Result<(), Box> { - let settings = cosmic::app::Settings::default().size(Size::new(260., 240.)); + let settings = cosmic::app::Settings::default().size(Size::new(550., 300.)); cosmic::app::run::(settings, ())?; Ok(()) diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 52acf06d..2bd1a0f1 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::spin_button, SpinButton}; pub mod tab_bar; diff --git a/src/widget/spin_button/mod.rs b/src/widget/spin_button/mod.rs index eb3005f8..110b93c2 100644 --- a/src/widget/spin_button/mod.rs +++ b/src/widget/spin_button/mod.rs @@ -4,6 +4,7 @@ //! A control for incremental adjustments of a value. mod model; +pub mod spin_button; use std::borrow::Cow; pub use self::model::{Message, Model}; diff --git a/src/widget/spin_button/spin_button.rs b/src/widget/spin_button/spin_button.rs new file mode 100644 index 00000000..862be4f2 --- /dev/null +++ b/src/widget/spin_button/spin_button.rs @@ -0,0 +1,196 @@ +use std::borrow::{Borrow, Cow}; +use std::fmt::Display; +use std::ops::{Add, Sub}; +use std::marker::PhantomData; +use apply::Apply; +use iced::alignment::Horizontal; +use iced::{Alignment, Length}; +use crate::{ + Element, + widget::{ + button, + column, + container, + icon, + row, + text, + }, +}; + +pub enum Direction { + Horizontal, + Vertical, +} + +pub struct SpinButton<'a, T, Message> +where + T: Add + Sub + PartialEq + PartialOrd + Display + Copy +{ + label: String, + step: T, + value: T, + min: T, + max: T, + direction: Direction, + on_press: Box Message>, + phantom_data: PhantomData<&'a Message>, +} + +impl<'a, T, Message> SpinButton<'a, T, Message> +where + T: Add + Sub + PartialEq + PartialOrd + Display + Copy +{ + pub fn new( + label: impl Into, + step: T, + value: T, + min: T, + max: T, + direction: Direction, + on_press: impl Fn(T) -> Message + 'static, + ) -> Self { + Self { + label: label.into(), + step, + value, + min, + max, + direction, + on_press: Box::from(on_press), + phantom_data: PhantomData, + } + } +} + +pub fn spin_button<'a, T, Message>( + label: impl Into, + step: T, + value: T, + min: T, + max: T, + direction: Direction, + on_press: impl Fn(T) -> Message + 'static, +) -> SpinButton<'a, T, Message> +where + T: Add + Sub + PartialEq + PartialOrd + Display + Copy +{ + SpinButton::new(label, step, value, min, max, direction, on_press) +} + +fn increment(step: T, value: T, min: T, max: T) -> T +where + T: Add + Sub + PartialEq + PartialOrd + Display + Copy +{ + //! Make it roll over back to min if the increase is too high + if value + step > max { + min + } else { + value + step + } +} + +fn decrement(step: T, value: T, min: T, max: T) -> T +where + T: Add + Sub + PartialEq + PartialOrd + Display + Copy +{ + //! Make it roll over back to max if the decrese is too low + if value - step < min { + max + } 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. + + // NOTE: I can add a bult-in function that can toggle the widget + // enabled or disabled if the feature is requested. However, for + // the first implementation, I didn't want to go into the complexity + // of doing so. + match this.direction { + Direction::Horizontal => { + // Create a spinner container variable that contains the row with all of + // the combined widgets that make up the widget. + let spinner_container = row::with_children(vec![ + // Using the title4 variant of text, just like the original spin button did. + text::title4(this.label.clone()) + .apply(container) + .center_x(Length::Fill) + .align_y(Alignment::Center) + .into(), + // Using an button instead of an icon for the decrement functionality. + button::icon(icon::from_name("list-remove-symbolic")) + .padding([0, 12]) + .on_press((this.on_press)(decrement::( + this.step, this.value, this.min, this.max, + ))) + .into(), + // Using the title4 variant of text for consistency. + text::title4(format!("{}", this.value)) + .apply(container) + .center_x(Length::Fixed(48.0)) + .align_y(Alignment::Center) + .into(), + // Using another button for the increment functionality. + button::icon(icon::from_name("list-add-symbolic")) + .padding([0, 12]) + .on_press((this.on_press)(increment::( + this.step, this.value, this.min, this.max, + ))) + .into(), + ]) + .align_y(Alignment::Center); + + // Return the horizontal spin button from the match statement. + Self::new(spinner_container) + }, + Direction::Vertical => { + // Create a text widget that holds the value + let val_text = text(format!("{}", this.value)).size(14); + // 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([0, 12]) + .on_press((this.on_press)(increment::( + this.step, this.value, this.min, this.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([0, 12]) + .on_press((this.on_press)(decrement::( + this.step, this.value, this.min, this.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(this.label)).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. + Self::new(content_list) + } + } + } +} \ No newline at end of file