From 8988b25b6a55eaba372fad1c3c8c05a54b757fd5 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 6 Jan 2023 16:18:25 +0100 Subject: [PATCH] feat: MultiSelect support for segmented buttons --- examples/cosmic/src/window.rs | 4 +- examples/cosmic/src/window/demo.rs | 34 +++- src/widget/nav_bar.rs | 6 +- src/widget/segmented_button/cosmic.rs | 40 ++-- src/widget/segmented_button/horizontal.rs | 17 +- src/widget/segmented_button/mod.rs | 5 +- src/widget/segmented_button/state.rs | 223 ++++++++++++++++------ src/widget/segmented_button/vertical.rs | 17 +- src/widget/segmented_button/widget.rs | 29 +-- 9 files changed, 263 insertions(+), 112 deletions(-) diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index a7aff0c7..25d286c5 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -13,7 +13,7 @@ use cosmic::{ theme::{self, Theme}, widget::{ header_bar, icon, list, nav_bar, nav_button, scrollable, - segmented_button::{self, cosmic::vertical_view_switcher}, + segmented_button::{self, cosmic::vertical_view_switcher, Selectable, SingleSelect}, settings, }, Element, ElementExt, @@ -136,7 +136,7 @@ pub struct Window { debug: bool, demo: demo::State, desktop: desktop::State, - nav_bar_pages: segmented_button::State, + nav_bar_pages: segmented_button::State, nav_bar_toggled_condensed: bool, nav_bar_toggled: bool, page: Page, diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index e9755950..4ada6cea 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -3,7 +3,9 @@ use cosmic::{ iced::{Alignment, Length}, theme::{Button as ButtonTheme, Theme}, widget::{ - button, settings, + button, + segmented_button::{MultiSelect, Selectable, SingleSelect}, + settings, spin_button::{SpinButtonModel, SpinMessage}, toggler, }, @@ -25,12 +27,21 @@ pub enum DemoView { TabC, } +pub enum MultiOption { + OptionA, + OptionB, + OptionC, + OptionD, + OptionE, +} + #[derive(Clone, Copy, Debug)] pub enum Message { ButtonPressed, CheckboxToggled(bool), Debug(bool), IconTheme(segmented_button::Key), + MultiSelection(segmented_button::Key), PickListSelected(&'static str), RowSelected(usize), Selection(segmented_button::Key), @@ -48,13 +59,14 @@ pub enum Output { pub struct State { pub checkbox_value: bool, - pub icon_theme: segmented_button::State<&'static str>, + pub icon_theme: segmented_button::State, + pub multi_selection: segmented_button::State, pub pick_list_selected: Option<&'static str>, - pub selection: segmented_button::State<()>, + pub selection: segmented_button::State, pub slider_value: f32, pub spin_button: SpinButtonModel, pub toggler_value: bool, - pub view_switcher: segmented_button::State, + pub view_switcher: segmented_button::State, } impl Default for State { @@ -74,6 +86,13 @@ impl Default for State { .insert("Choice B", ()) .insert("Choice C", ()) .build(), + multi_selection: segmented_button::State::builder() + .insert("Option A", MultiOption::OptionA) + .insert("Option B", MultiOption::OptionB) + .insert("Option C", MultiOption::OptionC) + .insert("Option D", MultiOption::OptionD) + .insert("Option E", MultiOption::OptionE) + .build(), view_switcher: segmented_button::State::builder() .insert_active("Controls", DemoView::TabA) .insert("Segmented Button", DemoView::TabB) @@ -91,6 +110,7 @@ impl State { Message::Debug(value) => return Some(Output::Debug(value)), Message::PickListSelected(value) => self.pick_list_selected = Some(value), Message::RowSelected(row) => println!("Selected row {row}"), + Message::MultiSelection(key) => self.multi_selection.activate(key), Message::Selection(key) => self.selection.activate(key), Message::SliderChanged(value) => self.slider_value = value, Message::SpinButton(msg) => self.spin_button.update(msg), @@ -226,9 +246,9 @@ impl State { .spacing(8) .on_activate(Message::Selection) .into(), - cosmic::iced::widget::text("Vertical").into(), - vertical_segmented_selection(&self.selection) - .on_activate(Message::Selection) + cosmic::iced::widget::text("Vertical (Multi-Select)").into(), + vertical_segmented_selection(&self.multi_selection) + .on_activate(Message::MultiSelection) .into(), cosmic::iced::widget::text("Vertical With Spacing").into(), cosmic::iced::widget::row(vec![ diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index 5889ee05..7b31cbee 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -10,11 +10,11 @@ use iced_core::Color; use crate::{theme, Theme}; -use super::segmented_button::{self, cosmic::vertical_view_switcher}; +use super::segmented_button::{self, cosmic::vertical_view_switcher, SingleSelect}; /// A container holding a vertical view switcher with the n style pub fn nav_bar( - state: &segmented_button::State, + state: &segmented_button::State, on_activate: impl Fn(segmented_button::Key) -> Message + 'static, ) -> iced::widget::Container where @@ -26,7 +26,7 @@ where .button_padding([16, 10, 16, 10]) .button_spacing(8) .icon_size(16) - .spacing(14) + .spacing(8) .apply(scrollable) .apply(container) .height(Length::Fill) diff --git a/src/widget/segmented_button/cosmic.rs b/src/widget/segmented_button/cosmic.rs index 1628774c..0f18ec65 100644 --- a/src/widget/segmented_button/cosmic.rs +++ b/src/widget/segmented_button/cosmic.rs @@ -1,15 +1,20 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -use super::{HorizontalSegmentedButton, SegmentedButton, State, VerticalSegmentedButton}; +use super::{ + state::Selectable, HorizontalSegmentedButton, SegmentedButton, State, VerticalSegmentedButton, +}; /// Appears as a collection of tabs for developing a tabbed interface. /// /// The data for the widget comes from a [`State`] that is maintained the application. #[must_use] -pub fn horizontal_view_switcher( - state: &State, -) -> HorizontalSegmentedButton { +pub fn horizontal_view_switcher( + state: &State, +) -> HorizontalSegmentedButton +where + Selection: Selectable, +{ SegmentedButton::new(&state.inner) .button_padding([16, 0, 16, 0]) .button_height(48) @@ -21,9 +26,12 @@ pub fn horizontal_view_switcher( /// /// The data for the widget comes from a [`State`] that is maintained the application. #[must_use] -pub fn horizontal_segmented_selection( - state: &State, -) -> HorizontalSegmentedButton { +pub fn horizontal_segmented_selection( + state: &State, +) -> HorizontalSegmentedButton +where + Selection: Selectable, +{ SegmentedButton::new(&state.inner) .button_padding([16, 0, 16, 0]) .button_height(32) @@ -35,9 +43,12 @@ pub fn horizontal_segmented_selection( /// /// The data for the widget comes from a [`State`] that is maintained the application. #[must_use] -pub fn vertical_segmented_selection( - state: &State, -) -> VerticalSegmentedButton { +pub fn vertical_segmented_selection( + state: &State, +) -> VerticalSegmentedButton +where + Selection: Selectable, +{ SegmentedButton::new(&state.inner) .button_padding([16, 0, 16, 0]) .button_height(32) @@ -49,9 +60,12 @@ pub fn vertical_segmented_selection( /// /// The data for the widget comes from a [`State`] that is maintained the application. #[must_use] -pub fn vertical_view_switcher( - state: &State, -) -> VerticalSegmentedButton { +pub fn vertical_view_switcher( + state: &State, +) -> VerticalSegmentedButton +where + Selection: Selectable, +{ SegmentedButton::new(&state.inner) .button_padding([16, 0, 16, 0]) .button_height(48) diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index af8beb54..bcbc66ad 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -1,7 +1,7 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -use super::state::State; +use super::state::{Selectable, State}; use super::style::StyleSheet; use super::widget::{SegmentedButton, SegmentedVariant}; @@ -12,31 +12,34 @@ use iced_native::layout; pub struct Horizontal; /// Horizontal [`SegmentedButton`]. -pub type HorizontalSegmentedButton<'a, Message, Renderer> = - SegmentedButton<'a, Horizontal, Message, Renderer>; +pub type HorizontalSegmentedButton<'a, Selection, Message, Renderer> = + SegmentedButton<'a, Horizontal, Selection, Message, Renderer>; /// Horizontal implementation of the [`SegmentedButton`]. #[must_use] -pub fn horizontal_segmented_button( - state: &State, -) -> SegmentedButton +pub fn horizontal_segmented_button( + state: &State, +) -> SegmentedButton where Renderer: iced_native::Renderer + iced_native::text::Renderer + iced_native::image::Renderer + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, + Selection: Selectable, { SegmentedButton::new(&state.inner) } -impl<'a, Message, Renderer> SegmentedVariant for SegmentedButton<'a, Horizontal, Message, Renderer> +impl<'a, Selection, Message, Renderer> SegmentedVariant + for SegmentedButton<'a, Horizontal, Selection, Message, Renderer> where Renderer: iced_native::Renderer + iced_native::text::Renderer + iced_native::image::Renderer + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, + Selection: Selectable, { type Renderer = Renderer; diff --git a/src/widget/segmented_button/mod.rs b/src/widget/segmented_button/mod.rs index ae89b307..1d7d77ce 100644 --- a/src/widget/segmented_button/mod.rs +++ b/src/widget/segmented_button/mod.rs @@ -45,13 +45,16 @@ pub mod cosmic; mod horizontal; + mod state; mod style; mod vertical; mod widget; pub use self::horizontal::{horizontal_segmented_button, Horizontal, HorizontalSegmentedButton}; -pub use self::state::{Content, Key, SecondaryState, SharedWidgetState, State}; +pub use self::state::{ + Content, Key, MultiSelect, SecondaryState, Selectable, SharedWidgetState, SingleSelect, State, +}; pub use self::style::{Appearance, ButtonAppearance, ButtonStatusAppearance, StyleSheet}; pub use self::vertical::{vertical_segmented_button, Vertical, VerticalSegmentedButton}; pub use self::widget::{SegmentedButton, SegmentedVariant}; diff --git a/src/widget/segmented_button/state.rs b/src/widget/segmented_button/state.rs index ad0e8e76..d7ebc6b3 100644 --- a/src/widget/segmented_button/state.rs +++ b/src/widget/segmented_button/state.rs @@ -3,7 +3,7 @@ use derive_setters::Setters; use slotmap::{SecondaryMap, SlotMap}; -use std::borrow::Cow; +use std::{borrow::Cow, collections::HashSet}; use crate::widget::IconSource; @@ -13,15 +13,15 @@ slotmap::new_key_type! { } /// Contains all state for interacting with a segmented button. -pub struct State { +pub struct State { /// State that is shared with widget drawing. - pub inner: SharedWidgetState, + pub inner: SharedWidgetState, /// State unique to the application. pub data: SecondaryState, } -impl Default for State { +impl Default for State { fn default() -> Self { Self { inner: SharedWidgetState::default(), @@ -32,43 +32,97 @@ impl Default for State { /// State which is most useful to the widget. #[derive(Default)] -pub struct SharedWidgetState { +pub struct SharedWidgetState { /// The content used for drawing segmented buttons. pub buttons: SlotMap, - /// The actively-selected segmented button. - pub active: Key, + /// Manages selections + pub selection: Variant, } -/// State which is most useful to the application. -pub type SecondaryState = SecondaryMap; - -impl State { - #[must_use] - pub fn builder() -> Builder { - Builder(Self::default()) +impl State { + pub fn activate(&mut self, key: Key) { + self.inner.selection.activate(key); } - /// Activates this button. - pub fn activate(&mut self, key: Key) { - self.inner.active = key; + pub fn deactivate(&mut self, key: Key) { + self.inner.selection.deactivate(key); + } + + #[must_use] + pub fn is_active(&self, key: Key) -> bool { + self.inner.selection.is_active(key) } /// The ID of the active button. #[must_use] pub fn active(&self) -> Key { - self.inner.active + self.inner.selection.active } /// Get the application data for the active button. #[must_use] pub fn active_data(&self) -> Option<&Data> { - self.data(self.active()) + self.data.get(self.inner.selection.active) + } + + /// Mutable application data for the active button. + #[must_use] + pub fn active_data_mut(&mut self) -> Option<&mut Data> { + self.data.get_mut(self.inner.selection.active) + } +} + +impl State { + pub fn activate(&mut self, key: Key) { + if self.inner.selection.is_active(key) { + self.inner.selection.deactivate(key); + } else { + self.inner.selection.activate(key); + } + } + + pub fn deactivate(&mut self, key: Key) { + self.inner.selection.deactivate(key); + } + + #[must_use] + pub fn is_active(&self, key: Key) -> bool { + self.inner.selection.is_active(key) + } + + /// The IDs of the active buttons. + pub fn active(&self) -> impl Iterator + '_ { + self.inner.selection.active.iter().copied() + } + + /// Get the application data for the active buttons. + pub fn active_data(&self) -> impl Iterator { + self.inner.buttons.keys().filter_map(|key| { + if self.inner.selection.is_active(key) { + self.data.get(key).map(|data| (key, data)) + } else { + None + } + }) + } +} + +/// State which is most useful to the application. +pub type SecondaryState = SecondaryMap; + +impl State +where + Selection: Selectable, +{ + #[must_use] + pub fn builder() -> Builder { + Builder(Self::default()) } /// Convenience method for batching multiple operations #[must_use] - pub fn batch(&mut self) -> Batch { + pub fn batch(&mut self) -> Batch { Batch(self) } @@ -106,17 +160,104 @@ impl State { /// Inserts and activates a button. pub fn insert_active(&mut self, content: impl Into, data: Data) -> Key { let key = self.insert(content, data); - self.activate(key); + self.inner.selection.activate(key); key } /// Removes a button. pub fn remove(&mut self, key: Key) -> Option { self.inner.buttons.remove(key); + self.inner.selection.deactivate(key); self.data.remove(key) } } +pub trait Selectable: Default { + fn activate(&mut self, key: Key); + + fn deactivate(&mut self, key: Key); + + fn is_active(&self, key: Key) -> bool; +} + +#[derive(Default)] +pub struct SingleSelect { + pub active: Key, +} + +impl Selectable for SingleSelect { + fn activate(&mut self, key: Key) { + self.active = key; + } + + fn deactivate(&mut self, _key: Key) { + self.active = Key::default(); + } + + fn is_active(&self, key: Key) -> bool { + self.active == key + } +} + +#[derive(Default)] +pub struct MultiSelect { + pub active: HashSet, +} + +impl Selectable for MultiSelect { + fn activate(&mut self, key: Key) { + self.active.insert(key); + } + + fn deactivate(&mut self, key: Key) { + self.active.remove(&key); + } + + fn is_active(&self, key: Key) -> bool { + self.active.contains(&key) + } +} + +pub struct Builder(State); + +impl Builder { + pub fn insert(mut self, content: impl Into, data: Data) -> Self { + self.0.insert(content, data); + self + } + + pub fn insert_active(mut self, content: impl Into, data: Data) -> Self { + self.0.insert_active(content, data); + self + } + + pub fn build(self) -> State { + self.0 + } +} + +/// Convenience type for batching multiple operations +pub struct Batch<'a, Selection, Data>(&'a mut State); + +impl<'a, Selection: Selectable, Data> Batch<'a, Selection, Data> { + /// Insert a new button. + pub fn insert(self, content: impl Into, data: Data) -> Self { + self.0.insert(content, data); + self + } + + /// Inserts and activates a button. + pub fn insert_active(self, content: impl Into, data: Data) -> Self { + self.0.insert_active(content, data); + self + } + + /// Removes a button. + pub fn remove(&mut self, key: Key) { + self.0.remove(key); + } +} + /// Data to be drawn in a segmented button. #[derive(Default, Setters)] pub struct Content { @@ -149,43 +290,3 @@ impl From> for Content { Content::default().text(text) } } - -pub struct Builder(State); - -impl Builder { - pub fn insert(mut self, content: impl Into, data: Data) -> Self { - self.0.insert(content, data); - self - } - - pub fn insert_active(mut self, content: impl Into, data: Data) -> Self { - self.0.insert_active(content, data); - self - } - - pub fn build(self) -> State { - self.0 - } -} - -/// Convenience type for batching multiple operations -pub struct Batch<'a, Data>(&'a mut State); - -impl<'a, Data> Batch<'a, Data> { - /// Insert a new button. - pub fn insert(self, content: impl Into, data: Data) -> Self { - self.0.insert(content, data); - self - } - - /// Inserts and activates a button. - pub fn insert_active(self, content: impl Into, data: Data) -> Self { - self.0.insert_active(content, data); - self - } - - /// Removes a button. - pub fn remove(&mut self, key: Key) { - self.0.remove(key); - } -} diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 58f170e3..ace21d8b 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -1,7 +1,7 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -use super::state::State; +use super::state::{Selectable, State}; use super::style::StyleSheet; use super::widget::{SegmentedButton, SegmentedVariant}; @@ -12,31 +12,34 @@ use iced_native::layout; pub struct Vertical; /// Vertical [`SegmentedButton`]. -pub type VerticalSegmentedButton<'a, Message, Renderer> = - SegmentedButton<'a, Vertical, Message, Renderer>; +pub type VerticalSegmentedButton<'a, Selection, Message, Renderer> = + SegmentedButton<'a, Vertical, Selection, Message, Renderer>; /// Vertical implementation of the [`SegmentedButton`]. #[must_use] -pub fn vertical_segmented_button( - state: &State, -) -> SegmentedButton +pub fn vertical_segmented_button( + state: &State, +) -> SegmentedButton where Renderer: iced_native::Renderer + iced_native::text::Renderer + iced_native::image::Renderer + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, + Selection: Selectable, { SegmentedButton::new(&state.inner) } -impl<'a, Message, Renderer> SegmentedVariant for SegmentedButton<'a, Vertical, Message, Renderer> +impl<'a, Selection, Message, Renderer> SegmentedVariant + for SegmentedButton<'a, Vertical, Selection, Message, Renderer> where Renderer: iced_native::Renderer + iced_native::text::Renderer + iced_native::image::Renderer + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, + Selection: Selectable, { type Renderer = Renderer; diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index fd3f5431..27971fc7 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -3,7 +3,7 @@ use std::marker::PhantomData; -use super::state::{Key, SharedWidgetState}; +use super::state::{Key, Selectable, SharedWidgetState}; use super::style::StyleSheet; use derive_setters::Setters; @@ -35,17 +35,18 @@ pub trait SegmentedVariant { } #[derive(Setters)] -pub struct SegmentedButton<'a, Variant, Message, Renderer> +pub struct SegmentedButton<'a, Variant, Selection, Message, Renderer> where Renderer: iced_native::Renderer + iced_native::text::Renderer + iced_native::image::Renderer + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, + Selection: Selectable, { /// Contains application state also used for drawing. #[setters(skip)] - pub(super) state: &'a SharedWidgetState, + pub(super) state: &'a SharedWidgetState, /// Padding around a button. pub(super) button_padding: [u16; 4], /// Desired height of a button. @@ -77,7 +78,8 @@ where variant: PhantomData, } -impl<'a, Variant, Message, Renderer> SegmentedButton<'a, Variant, Message, Renderer> +impl<'a, Variant, Selection, Message, Renderer> + SegmentedButton<'a, Variant, Selection, Message, Renderer> where Renderer: iced_native::Renderer + iced_native::text::Renderer @@ -85,9 +87,10 @@ where + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, Self: SegmentedVariant, + Selection: Selectable, { #[must_use] - pub fn new(state: &'a SharedWidgetState) -> Self { + pub fn new(state: &'a SharedWidgetState) -> Self { Self { state, button_padding: [4, 4, 4, 4], @@ -153,8 +156,8 @@ where } } -impl<'a, Variant, Message, Renderer> Widget - for SegmentedButton<'a, Variant, Message, Renderer> +impl<'a, Variant, Selection, Message, Renderer> Widget + for SegmentedButton<'a, Variant, Selection, Message, Renderer> where Renderer: iced_native::Renderer + iced_native::text::Renderer @@ -162,6 +165,7 @@ where + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, Self: SegmentedVariant, + Selection: Selectable, Message: 'static + Clone, { fn tag(&self) -> tree::Tag { @@ -273,7 +277,7 @@ where for (nth, (key, content)) in self.state.buttons.iter().enumerate() { let mut bounds = self.variant_button_bounds(bounds, nth); - let (status_appearance, font) = if self.state.active == key { + let (status_appearance, font) = if self.state.selection.is_active(key) { (appearance.active, &self.font_active) } else if state.hovered == key { (appearance.hover, &self.font_hovered) @@ -392,7 +396,8 @@ where } } -impl<'a, Variant, Message, Renderer> From> +impl<'a, Variant, Selection, Message, Renderer> + From> for Element<'a, Message, Renderer> where Renderer: iced_native::Renderer @@ -401,11 +406,13 @@ where + iced_native::svg::Renderer + 'a, Renderer::Theme: StyleSheet, - SegmentedButton<'a, Variant, Message, Renderer>: SegmentedVariant, + SegmentedButton<'a, Variant, Selection, Message, Renderer>: + SegmentedVariant, Variant: 'static, + Selection: Selectable, Message: 'static + Clone, { - fn from(mut widget: SegmentedButton<'a, Variant, Message, Renderer>) -> Self { + fn from(mut widget: SegmentedButton<'a, Variant, Selection, Message, Renderer>) -> Self { if widget.state.buttons.is_empty() { widget.spacing = 0; }