From b3d550cc5e2758090e0f0551270ceed3829870d7 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 17 Jan 2023 18:49:40 +0100 Subject: [PATCH] feat!(segmented-button): improved interfaces and documentation BREAKING CHANGE: Various type and function names have changed to reflect themselves better in documentation. Code has been reorganized into separate modules with a better placement in libcosmic. Most of the functions, types, and modules now have documentation and examples. These changes no longer require the `Model` type to define the data/component type that it stores. The component functionality is now optional, and it's also possible to associate many components to an item with one component per type. This has had a side effect of simplifying a lot of the type signatures in the implementation. Before, to insert an item into the model, you had to define a `SegmentedItem` and a `Component` on insert, and get back an ID for that item. Which makes it difficult to define an item that contains only an icon or has no components. And requires an extra insert function to activate the item on insert. Now, there is a flexible builder-style API for configuring newly-inserted items in the model. So the complexity for inserting and retrieving values from the model has decreased significantly --- examples/cosmic-sctk/src/window.rs | 27 +- examples/cosmic/src/window.rs | 32 +- examples/cosmic/src/window/demo.rs | 100 +++-- src/theme/segmented_button.rs | 106 +++--- src/widget/mod.rs | 23 +- src/widget/nav_bar.rs | 20 +- src/widget/nav_bar_toggle.rs | 2 + src/widget/segmented_button/cosmic.rs | 75 ---- src/widget/segmented_button/horizontal.rs | 16 +- src/widget/segmented_button/item.rs | 57 --- src/widget/segmented_button/mod.rs | 75 +++- src/widget/segmented_button/model.rs | 243 ------------ src/widget/segmented_button/model/builder.rs | 131 +++++++ src/widget/segmented_button/model/entity.rs | 127 ++++++ src/widget/segmented_button/model/mod.rs | 360 ++++++++++++++++++ .../segmented_button/model/selection.rs | 106 ++++++ .../segmented_button/selection_modes.rs | 59 --- src/widget/segmented_button/style.rs | 23 +- src/widget/segmented_button/vertical.rs | 15 +- src/widget/segmented_button/widget.rs | 77 ++-- src/widget/segmented_selection.rs | 49 +++ src/widget/view_switcher.rs | 49 +++ 22 files changed, 1097 insertions(+), 675 deletions(-) delete mode 100644 src/widget/segmented_button/cosmic.rs delete mode 100644 src/widget/segmented_button/item.rs delete mode 100644 src/widget/segmented_button/model.rs create mode 100644 src/widget/segmented_button/model/builder.rs create mode 100644 src/widget/segmented_button/model/entity.rs create mode 100644 src/widget/segmented_button/model/mod.rs create mode 100644 src/widget/segmented_button/model/selection.rs delete mode 100644 src/widget/segmented_button/selection_modes.rs create mode 100644 src/widget/segmented_selection.rs create mode 100644 src/widget/view_switcher.rs diff --git a/examples/cosmic-sctk/src/window.rs b/examples/cosmic-sctk/src/window.rs index b20b6be0..6ccf5a19 100644 --- a/examples/cosmic-sctk/src/window.rs +++ b/examples/cosmic-sctk/src/window.rs @@ -115,7 +115,7 @@ pub struct Window { checkbox_value: bool, toggler_value: bool, pick_list_selected: Option<&'static str>, - nav_bar_pages: segmented_button::SingleSelectModel, + nav_bar_pages: segmented_button::SingleSelectModel, nav_bar_toggled_condensed: bool, nav_bar_toggled: bool, show_minimize: bool, @@ -126,18 +126,12 @@ pub struct Window { impl Window { /// Adds a page to the model we use for the navigation bar. - fn insert_page(&mut self, page: Page) -> segmented_button::Key { - self.nav_bar_pages.insert( - segmented_button::item() - .text(page.title()) - .icon(IconSource::Name(page.icon_name().into())), - page, - ) - } - - /// Activates the page by its key. - fn activate_page(&mut self, page: segmented_button::Key) { - self.nav_bar_pages.activate(page); + fn insert_page(&mut self, page: Page) -> segmented_button::SingleSelectEntityMut { + self.nav_bar_pages + .insert() + .text(page.title()) + .icon(IconSource::Name(page.icon_name().into())) + .data(page) } fn is_condensed(&self) -> bool { @@ -185,7 +179,7 @@ pub enum Message { Maximize, InputChanged, Rectangle(RectangleUpdate), - NavBar(segmented_button::Key), + NavBar(segmented_button::Entity), } impl Application for Window { @@ -208,7 +202,7 @@ impl Application for Window { window.insert_page(Page::WiFi); window.insert_page(Page::Networking); window.insert_page(Page::Bluetooth); - let key = window.insert_page(Page::Desktop); + window.insert_page(Page::Desktop).activate(); window.insert_page(Page::InputDevices); window.insert_page(Page::Displays); window.insert_page(Page::PowerAndBattery); @@ -219,7 +213,6 @@ impl Application for Window { window.insert_page(Page::TimeAndLanguage); window.insert_page(Page::Accessibility); window.insert_page(Page::Applications); - window.activate_page(key); (window, Command::none()) } @@ -231,7 +224,7 @@ impl Application for Window { fn update(&mut self, message: Message) -> iced::Command { match message { Message::NavBar(key) => { - if let Some(page) = self.nav_bar_pages.component(key).cloned() { + if let Some(page) = self.nav_bar_pages.data::(key).cloned() { self.nav_bar_pages.activate(key); self.page(page); } diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index e23ceeea..b6ef6d45 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -131,7 +131,8 @@ pub struct Window { debug: bool, demo: demo::State, desktop: desktop::State, - nav_bar_pages: segmented_button::SingleSelectModel, + nav_bar: segmented_button::SingleSelectModel, + nav_id_to_page: segmented_button::SecondaryMap, nav_bar_toggled_condensed: bool, nav_bar_toggled: bool, page: Page, @@ -178,7 +179,7 @@ pub enum Message { KeyboardNav(keyboard_nav::Message), Maximize, Minimize, - NavBar(segmented_button::Key), + NavBar(segmented_button::Entity), Page(Page), ToggleNavBar, ToggleNavBarCondensed, @@ -193,18 +194,12 @@ impl From for Message { impl Window { /// Adds a page to the model we use for the navigation bar. - fn insert_page(&mut self, page: Page) -> segmented_button::Key { - self.nav_bar_pages.insert( - segmented_button::item() - .text(page.title()) - .icon(IconSource::Name(page.icon_name().into())), - page, - ) - } - - /// Activates the page by its key. - fn activate_page(&mut self, page: segmented_button::Key) { - self.nav_bar_pages.activate(page); + fn insert_page(&mut self, page: Page) -> segmented_button::SingleSelectEntityMut { + self.nav_bar + .insert() + .text(page.title()) + .icon(IconSource::Name(page.icon_name().into())) + .secondary(&mut self.nav_id_to_page, page) } fn page_title(&self, page: Page) -> Element { @@ -313,7 +308,7 @@ impl Application for Window { window.insert_page(Page::WiFi); window.insert_page(Page::Networking(None)); window.insert_page(Page::Bluetooth); - let key = window.insert_page(Page::Desktop(None)); + window.insert_page(Page::Desktop(None)).activate(); window.insert_page(Page::InputDevices(None)); window.insert_page(Page::Displays); window.insert_page(Page::PowerAndBattery); @@ -324,7 +319,6 @@ impl Application for Window { window.insert_page(Page::TimeAndLanguage(None)); window.insert_page(Page::Accessibility); window.insert_page(Page::Applications); - window.activate_page(key); (window, Command::none()) } @@ -363,8 +357,8 @@ impl Application for Window { let mut ret = Command::none(); match message { Message::NavBar(key) => { - if let Some(page) = self.nav_bar_pages.component(key).cloned() { - self.nav_bar_pages.activate(key); + if let Some(page) = self.nav_id_to_page.get(key).copied() { + self.nav_bar.activate(key); self.page(page); } } @@ -437,7 +431,7 @@ impl Application for Window { let mut widgets = Vec::with_capacity(2); if nav_bar_toggled { - let mut nav_bar = nav_bar(&self.nav_bar_pages, Message::NavBar); + let mut nav_bar = nav_bar(&self.nav_bar, Message::NavBar); if !self.is_condensed() { nav_bar = nav_bar.max_width(300); diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 25f32a9c..0102b54d 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -4,17 +4,9 @@ use cosmic::{ iced::{widget::container, Alignment, Length}, theme::{Button as ButtonTheme, Theme}, widget::{ - button, - segmented_button::{ - self, - cosmic::{ - horizontal_segmented_selection, horizontal_view_switcher, - vertical_segmented_selection, vertical_view_switcher, - }, - }, - settings, + button, segmented_button, segmented_selection, settings, spin_button::{SpinButtonModel, SpinMessage}, - toggler, + toggler, view_switcher, }, Element, }; @@ -41,16 +33,16 @@ pub enum Message { ButtonPressed, CheckboxToggled(bool), Debug(bool), - IconTheme(segmented_button::Key), - MultiSelection(segmented_button::Key), + IconTheme(segmented_button::Entity), + MultiSelection(segmented_button::Entity), PickListSelected(&'static str), RowSelected(usize), - Selection(segmented_button::Key), + Selection(segmented_button::Entity), SliderChanged(f32), SpinButton(SpinMessage), ThemeChanged(Theme), TogglerToggled(bool), - ViewSwitcher(segmented_button::Key), + ViewSwitcher(segmented_button::Entity), } pub enum Output { @@ -60,15 +52,15 @@ pub enum Output { pub struct State { pub checkbox_value: bool, - pub icon_theme: segmented_button::SingleSelectModel<&'static str>, - pub multi_selection: segmented_button::MultiSelectModel, + pub icon_themes: segmented_button::SingleSelectModel, + pub multi_selection: segmented_button::MultiSelectModel, pub pick_list_selected: Option<&'static str>, pub pick_list_options: Vec<&'static str>, - pub selection: segmented_button::SingleSelectModel<()>, + pub selection: segmented_button::SingleSelectModel, pub slider_value: f32, pub spin_button: SpinButtonModel, pub toggler_value: bool, - pub view_switcher: segmented_button::SingleSelectModel, + pub view_switcher: segmented_button::SingleSelectModel, } impl Default for State { @@ -80,26 +72,26 @@ impl Default for State { slider_value: 50.0, spin_button: SpinButtonModel::default().min(-10).max(10), toggler_value: false, - icon_theme: segmented_button::Model::builder() - .insert_active("Pop", "Pop") - .insert("Adwaita", "Adwaita") + icon_themes: segmented_button::Model::builder() + .insert(|b| b.text("Pop").activate()) + .insert(|b| b.text("Adwaita")) .build(), selection: segmented_button::Model::builder() - .insert_active("Choice A", ()) - .insert("Choice B", ()) - .insert("Choice C", ()) + .insert(|b| b.text("Choice A").activate()) + .insert(|b| b.text("Choice B")) + .insert(|b| b.text("Choice C")) .build(), multi_selection: segmented_button::Model::builder() - .insert_active("Option A", MultiOption::OptionA) - .insert("Option B", MultiOption::OptionB) - .insert("Option C", MultiOption::OptionB) - .insert("Option D", MultiOption::OptionC) - .insert("Option E", MultiOption::OptionE) + .insert(|b| b.text("Option A").data(MultiOption::OptionA).activate()) + .insert(|b| b.text("Option B").data(MultiOption::OptionB)) + .insert(|b| b.text("Option C").data(MultiOption::OptionC)) + .insert(|b| b.text("Option D").data(MultiOption::OptionD)) + .insert(|b| b.text("Option E").data(MultiOption::OptionE)) .build(), view_switcher: segmented_button::Model::builder() - .insert_active("Controls", DemoView::TabA) - .insert("Segmented Button", DemoView::TabB) - .insert("Tab C", DemoView::TabC) + .insert(|b| b.text("Controls").data(DemoView::TabA).activate()) + .insert(|b| b.text("Segmented Button").data(DemoView::TabB)) + .insert(|b| b.text("Tab C").data(DemoView::TabC)) .build(), } } @@ -121,9 +113,9 @@ impl State { Message::TogglerToggled(value) => self.toggler_value = value, Message::ViewSwitcher(key) => self.view_switcher.activate(key), Message::IconTheme(key) => { - self.icon_theme.activate(key); - if let Some(theme) = self.icon_theme.component(key) { - cosmic::settings::set_default_icon_theme(*theme); + self.icon_themes.activate(key); + if let Some(theme) = self.icon_themes.text(key) { + cosmic::settings::set_default_icon_theme(theme); } } } @@ -145,14 +137,14 @@ impl State { ); let choose_icon_theme = - horizontal_segmented_selection(&self.icon_theme).on_activate(Message::IconTheme); + segmented_selection::horizontal(&self.icon_themes).on_activate(Message::IconTheme); settings::view_column(vec![ window.page_title(Page::Demo), - horizontal_view_switcher(&self.view_switcher) + view_switcher::horizontal(&self.view_switcher) .on_activate(Message::ViewSwitcher) .into(), - match self.view_switcher.active_component() { + match self.view_switcher.active_data() { None => panic!("no tab is active"), Some(DemoView::TabA) => settings::view_column(vec![ settings::view_section("Debug") @@ -241,25 +233,25 @@ impl State { .font(cosmic::font::FONT_SEMIBOLD) .into(), cosmic::iced::widget::text("Horizontal").into(), - horizontal_segmented_selection(&self.selection) + segmented_selection::horizontal(&self.selection) .on_activate(Message::Selection) .into(), cosmic::iced::widget::text("Horizontal With Spacing").into(), - horizontal_segmented_selection(&self.selection) + segmented_selection::horizontal(&self.selection) .spacing(8) .on_activate(Message::Selection) .into(), cosmic::iced::widget::text("Horizontal Multi-Select").into(), - horizontal_segmented_selection(&self.multi_selection) + segmented_selection::horizontal(&self.multi_selection) .spacing(8) .on_activate(Message::MultiSelection) .into(), cosmic::iced::widget::text("Vertical").into(), - vertical_segmented_selection(&self.selection) + segmented_selection::vertical(&self.selection) .on_activate(Message::Selection) .into(), cosmic::iced::widget::text("Vertical Multi-Select Shrunk").into(), - vertical_segmented_selection(&self.multi_selection) + segmented_selection::vertical(&self.multi_selection) .width(Length::Shrink) .on_activate(Message::MultiSelection) .apply(container) @@ -268,17 +260,17 @@ impl State { .into(), cosmic::iced::widget::text("Vertical With Spacing").into(), cosmic::iced::widget::row(vec![ - vertical_segmented_selection(&self.selection) + segmented_selection::vertical(&self.selection) .spacing(8) .on_activate(Message::Selection) .width(Length::FillPortion(1)) .into(), - vertical_segmented_selection(&self.selection) + segmented_selection::vertical(&self.selection) .spacing(8) .on_activate(Message::Selection) .width(Length::FillPortion(1)) .into(), - vertical_segmented_selection(&self.selection) + segmented_selection::vertical(&self.selection) .spacing(8) .on_activate(Message::Selection) .width(Length::FillPortion(1)) @@ -291,39 +283,39 @@ impl State { .font(cosmic::font::FONT_SEMIBOLD) .into(), cosmic::iced::widget::text("Horizontal").into(), - horizontal_view_switcher(&self.selection) + view_switcher::horizontal(&self.selection) .on_activate(Message::Selection) .into(), cosmic::iced::widget::text("Horizontal Multi-Select").into(), - horizontal_view_switcher(&self.multi_selection) + view_switcher::horizontal(&self.multi_selection) .on_activate(Message::MultiSelection) .into(), cosmic::iced::widget::text("Horizontal With Spacing").into(), - horizontal_view_switcher(&self.selection) + view_switcher::horizontal(&self.selection) .spacing(8) .on_activate(Message::Selection) .into(), cosmic::iced::widget::text("Vertical").into(), - vertical_view_switcher(&self.selection) + view_switcher::vertical(&self.selection) .on_activate(Message::Selection) .into(), cosmic::iced::widget::text("Vertical Multi-Select").into(), - vertical_view_switcher(&self.multi_selection) + view_switcher::vertical(&self.multi_selection) .on_activate(Message::MultiSelection) .into(), cosmic::iced::widget::text("Vertical With Spacing").into(), cosmic::iced::widget::row(vec![ - vertical_view_switcher(&self.selection) + view_switcher::vertical(&self.selection) .spacing(8) .on_activate(Message::Selection) .width(Length::FillPortion(1)) .into(), - vertical_view_switcher(&self.selection) + view_switcher::vertical(&self.selection) .spacing(8) .on_activate(Message::Selection) .width(Length::FillPortion(1)) .into(), - vertical_view_switcher(&self.selection) + view_switcher::vertical(&self.selection) .spacing(8) .on_activate(Message::Selection) .width(Length::FillPortion(1)) diff --git a/src/theme/segmented_button.rs b/src/theme/segmented_button.rs index d79a9298..56fb6b97 100644 --- a/src/theme/segmented_button.rs +++ b/src/theme/segmented_button.rs @@ -1,8 +1,8 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -use crate::theme::Theme; -use crate::widget::segmented_button; +use crate::widget::segmented_button::{Appearance, ItemAppearance, StyleSheet}; +use crate::{theme::Theme, widget::segmented_button::ItemStatusAppearance}; use iced_core::{Background, BorderRadius}; use palette::{rgb::Rgb, Alpha}; @@ -14,33 +14,33 @@ pub enum SegmentedButton { /// A widget for multiple choice selection. Selection, /// Or implement any custom theme of your liking. - Custom(fn(&Theme) -> segmented_button::Appearance), + Custom(fn(&Theme) -> Appearance), } -impl segmented_button::StyleSheet for Theme { +impl StyleSheet for Theme { type Style = SegmentedButton; #[allow(clippy::too_many_lines)] - fn horizontal(&self, style: &Self::Style) -> segmented_button::Appearance { + fn horizontal(&self, style: &Self::Style) -> Appearance { match style { SegmentedButton::ViewSwitcher => { let cosmic = self.cosmic(); let active = horizontal::view_switcher_active(cosmic); - segmented_button::Appearance { + Appearance { border_radius: BorderRadius::from(0.0), - inactive: segmented_button::ButtonStatusAppearance { + inactive: ItemStatusAppearance { background: None, - first: segmented_button::ButtonAppearance { + first: ItemAppearance { border_radius: BorderRadius::from(0.0), border_bottom: Some((1.0, cosmic.accent.base.into())), ..Default::default() }, - middle: segmented_button::ButtonAppearance { + middle: ItemAppearance { border_radius: BorderRadius::from(0.0), border_bottom: Some((1.0, cosmic.accent.base.into())), ..Default::default() }, - last: segmented_button::ButtonAppearance { + last: ItemAppearance { border_radius: BorderRadius::from(0.0), border_bottom: Some((1.0, cosmic.accent.base.into())), ..Default::default() @@ -56,19 +56,19 @@ impl segmented_button::StyleSheet for Theme { SegmentedButton::Selection => { let cosmic = self.cosmic(); let active = horizontal::selection_active(cosmic); - segmented_button::Appearance { + Appearance { border_radius: BorderRadius::from(0.0), - inactive: segmented_button::ButtonStatusAppearance { + inactive: ItemStatusAppearance { background: Some(Background::Color(cosmic.secondary.component.base.into())), - first: segmented_button::ButtonAppearance { + first: ItemAppearance { border_radius: BorderRadius::from([24.0, 0.0, 0.0, 24.0]), ..Default::default() }, - middle: segmented_button::ButtonAppearance { + middle: ItemAppearance { border_radius: BorderRadius::from(0.0), ..Default::default() }, - last: segmented_button::ButtonAppearance { + last: ItemAppearance { border_radius: BorderRadius::from([0.0, 24.0, 24.0, 0.0]), ..Default::default() }, @@ -85,14 +85,14 @@ impl segmented_button::StyleSheet for Theme { } #[allow(clippy::too_many_lines)] - fn vertical(&self, style: &Self::Style) -> segmented_button::Appearance { + fn vertical(&self, style: &Self::Style) -> Appearance { match style { SegmentedButton::ViewSwitcher => { let cosmic = self.cosmic(); let active = vertical::view_switcher_active(cosmic); - segmented_button::Appearance { + Appearance { border_radius: BorderRadius::from(0.0), - inactive: segmented_button::ButtonStatusAppearance { + inactive: ItemStatusAppearance { background: None, text_color: cosmic.primary.on.into(), ..active @@ -106,19 +106,19 @@ impl segmented_button::StyleSheet for Theme { SegmentedButton::Selection => { let cosmic = self.cosmic(); let active = vertical::selection_active(cosmic); - segmented_button::Appearance { + Appearance { border_radius: BorderRadius::from(0.0), - inactive: segmented_button::ButtonStatusAppearance { + inactive: ItemStatusAppearance { background: Some(Background::Color(cosmic.secondary.component.base.into())), - first: segmented_button::ButtonAppearance { + first: ItemAppearance { border_radius: BorderRadius::from([24.0, 24.0, 0.0, 0.0]), ..Default::default() }, - middle: segmented_button::ButtonAppearance { + middle: ItemAppearance { border_radius: BorderRadius::from(0.0), ..Default::default() }, - last: segmented_button::ButtonAppearance { + last: ItemAppearance { border_radius: BorderRadius::from([0.0, 0.0, 24.0, 24.0]), ..Default::default() }, @@ -136,24 +136,22 @@ impl segmented_button::StyleSheet for Theme { } mod horizontal { - use crate::widget::segmented_button; + use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; use iced_core::{Background, BorderRadius}; use palette::{rgb::Rgb, Alpha}; - pub fn selection_active( - cosmic: &cosmic_theme::Theme>, - ) -> segmented_button::ButtonStatusAppearance { - segmented_button::ButtonStatusAppearance { + pub fn selection_active(cosmic: &cosmic_theme::Theme>) -> ItemStatusAppearance { + ItemStatusAppearance { background: Some(Background::Color(cosmic.secondary.component.divider.into())), - first: segmented_button::ButtonAppearance { + first: ItemAppearance { border_radius: BorderRadius::from([24.0, 0.0, 0.0, 24.0]), ..Default::default() }, - middle: segmented_button::ButtonAppearance { + middle: ItemAppearance { border_radius: BorderRadius::from(0.0), ..Default::default() }, - last: segmented_button::ButtonAppearance { + last: ItemAppearance { border_radius: BorderRadius::from([0.0, 24.0, 24.0, 0.0]), ..Default::default() }, @@ -163,20 +161,20 @@ mod horizontal { pub fn view_switcher_active( cosmic: &cosmic_theme::Theme>, - ) -> segmented_button::ButtonStatusAppearance { - segmented_button::ButtonStatusAppearance { + ) -> ItemStatusAppearance { + ItemStatusAppearance { background: Some(Background::Color(cosmic.primary.component.base.into())), - first: segmented_button::ButtonAppearance { + first: ItemAppearance { border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), border_bottom: Some((4.0, cosmic.accent.base.into())), ..Default::default() }, - middle: segmented_button::ButtonAppearance { + middle: ItemAppearance { border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), border_bottom: Some((4.0, cosmic.accent.base.into())), ..Default::default() }, - last: segmented_button::ButtonAppearance { + last: ItemAppearance { border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), border_bottom: Some((4.0, cosmic.accent.base.into())), ..Default::default() @@ -188,9 +186,9 @@ mod horizontal { pub fn focus( cosmic: &cosmic_theme::Theme>, - default: &segmented_button::ButtonStatusAppearance, -) -> segmented_button::ButtonStatusAppearance { - segmented_button::ButtonStatusAppearance { + default: &ItemStatusAppearance, +) -> ItemStatusAppearance { + ItemStatusAppearance { background: Some(Background::Color(cosmic.primary.component.focus.into())), text_color: cosmic.primary.base.into(), ..*default @@ -199,9 +197,9 @@ pub fn focus( pub fn hover( cosmic: &cosmic_theme::Theme>, - default: &segmented_button::ButtonStatusAppearance, -) -> segmented_button::ButtonStatusAppearance { - segmented_button::ButtonStatusAppearance { + default: &ItemStatusAppearance, +) -> ItemStatusAppearance { + ItemStatusAppearance { background: Some(Background::Color(cosmic.primary.component.hover.into())), text_color: cosmic.accent.base.into(), ..*default @@ -209,24 +207,22 @@ pub fn hover( } mod vertical { - use crate::widget::segmented_button; + use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; use iced_core::{Background, BorderRadius}; use palette::{rgb::Rgb, Alpha}; - pub fn selection_active( - cosmic: &cosmic_theme::Theme>, - ) -> segmented_button::ButtonStatusAppearance { - segmented_button::ButtonStatusAppearance { + pub fn selection_active(cosmic: &cosmic_theme::Theme>) -> ItemStatusAppearance { + ItemStatusAppearance { background: Some(Background::Color(cosmic.secondary.component.divider.into())), - first: segmented_button::ButtonAppearance { + first: ItemAppearance { border_radius: BorderRadius::from([24.0, 24.0, 0.0, 0.0]), ..Default::default() }, - middle: segmented_button::ButtonAppearance { + middle: ItemAppearance { border_radius: BorderRadius::from(0.0), ..Default::default() }, - last: segmented_button::ButtonAppearance { + last: ItemAppearance { border_radius: BorderRadius::from([0.0, 0.0, 24.0, 24.0]), ..Default::default() }, @@ -236,18 +232,18 @@ mod vertical { pub fn view_switcher_active( cosmic: &cosmic_theme::Theme>, - ) -> segmented_button::ButtonStatusAppearance { - segmented_button::ButtonStatusAppearance { + ) -> ItemStatusAppearance { + ItemStatusAppearance { background: Some(Background::Color(cosmic.secondary.component.divider.into())), - first: segmented_button::ButtonAppearance { + first: ItemAppearance { border_radius: BorderRadius::from(24.0), ..Default::default() }, - middle: segmented_button::ButtonAppearance { + middle: ItemAppearance { border_radius: BorderRadius::from(24.0), ..Default::default() }, - last: segmented_button::ButtonAppearance { + last: ItemAppearance { border_radius: BorderRadius::from(24.0), ..Default::default() }, diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 9682fb2b..e8964f4e 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -1,31 +1,36 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 +//! Cosmic-themed widget implementations. + mod button; pub use button::*; mod header_bar; pub use header_bar::{header_bar, HeaderBar}; -mod icon; -pub use self::icon::{icon, Icon, IconSource}; +pub mod icon; +pub use icon::{icon, Icon, IconSource}; pub mod list; -pub use self::list::*; +pub use list::*; pub mod nav_bar; pub use nav_bar::nav_bar; pub mod nav_bar_toggle; -pub use self::nav_bar_toggle::{nav_bar_toggle, NavBarToggle}; +pub use nav_bar_toggle::{nav_bar_toggle, NavBarToggle}; mod toggler; pub use toggler::toggler; pub mod segmented_button; -pub use segmented_button::{ - horizontal_segmented_button, vertical_segmented_button, HorizontalSegmentedButton, -}; +pub use segmented_button::horizontal as horizontal_segmented_button; +pub use segmented_button::vertical as vertical_segmented_button; + +pub mod segmented_selection; +pub use segmented_selection::horizontal as horizontal_segmented_selection; +pub use segmented_selection::vertical as vertical_segmented_selection; pub mod settings; @@ -42,5 +47,9 @@ pub mod rectangle_tracker; pub mod aspect_ratio; +pub mod view_switcher; +pub use view_switcher::horizontal as horiontal_view_switcher; +pub use view_switcher::vertical as vertical_view_switcher; + pub mod warning; pub use warning::*; diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index 6809ed74..6b361ed0 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -1,6 +1,10 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 +//! Navigation side panel for switching between views. +//! +//! For details on the model, see the [`segmented_button`] module for more details. + use apply::Apply; use iced::{ widget::{container, scrollable}, @@ -8,19 +12,19 @@ use iced::{ }; use iced_core::Color; -use crate::{theme, Theme}; +use crate::{theme, widget::segmented_button, Theme}; -use super::segmented_button::{self, vertical_segmented_button}; - -/// A container holding a vertical view switcher with the n style -pub fn nav_bar( - model: &segmented_button::SingleSelectModel, - on_activate: impl Fn(segmented_button::Key) -> Message + 'static, +/// Navigation side panel for switching between views. +/// +/// For details on the model, see the [`segmented_button`] module for more details. +pub fn nav_bar( + model: &segmented_button::SingleSelectModel, + on_activate: impl Fn(segmented_button::Entity) -> Message + 'static, ) -> iced::widget::Container where Message: Clone + 'static, { - vertical_segmented_button(model) + segmented_button::vertical(model) .button_height(32) .button_padding([16, 10, 16, 10]) .button_spacing(8) diff --git a/src/widget/nav_bar_toggle.rs b/src/widget/nav_bar_toggle.rs index 4871d287..9a9c3220 100644 --- a/src/widget/nav_bar_toggle.rs +++ b/src/widget/nav_bar_toggle.rs @@ -1,6 +1,8 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 +//! A button for toggling the navigation side panel. + use crate::{theme, Element}; use apply::Apply; use derive_setters::Setters; diff --git a/src/widget/segmented_button/cosmic.rs b/src/widget/segmented_button/cosmic.rs deleted file mode 100644 index 55ff232f..00000000 --- a/src/widget/segmented_button/cosmic.rs +++ /dev/null @@ -1,75 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -use super::{ - horizontal_segmented_button, HorizontalSegmentedButton, Model, SegmentedButton, Selectable, - VerticalSegmentedButton, -}; - -/// Appears as a collection of tabs for developing a tabbed interface. -/// -/// The data for the widget comes from a model supplied by the application. -#[must_use] -pub fn horizontal_view_switcher( - model: &Model, -) -> HorizontalSegmentedButton -where - SelectionMode: Selectable, -{ - horizontal_segmented_button(model) - .button_padding([16, 0, 16, 0]) - .button_height(48) - .style(crate::theme::SegmentedButton::ViewSwitcher) - .font_active(crate::font::FONT_SEMIBOLD) -} - -/// Appears as a selection of choices for choosing between. -/// -/// The data for the widget comes from a model that is maintained the application. -#[must_use] -pub fn horizontal_segmented_selection( - model: &Model, -) -> HorizontalSegmentedButton -where - SelectionMode: Selectable, -{ - SegmentedButton::new(model) - .button_padding([16, 0, 16, 0]) - .button_height(32) - .style(crate::theme::SegmentedButton::Selection) - .font_active(crate::font::FONT_SEMIBOLD) -} - -/// Appears as a selection of choices for choosing between. -/// -/// The data for the widget comes from a model that is maintained the application. -#[must_use] -pub fn vertical_segmented_selection( - model: &Model, -) -> VerticalSegmentedButton -where - SelectionMode: Selectable, -{ - SegmentedButton::new(model) - .button_padding([16, 0, 16, 0]) - .button_height(32) - .style(crate::theme::SegmentedButton::Selection) - .font_active(crate::font::FONT_SEMIBOLD) -} - -/// Appears as a collection of tabs for developing a tabbed interface. -/// -/// The data for the widget comes from a model that is maintained the application. -#[must_use] -pub fn vertical_view_switcher( - model: &Model, -) -> VerticalSegmentedButton -where - SelectionMode: Selectable, -{ - SegmentedButton::new(model) - .button_padding([16, 0, 16, 0]) - .button_height(48) - .style(crate::theme::SegmentedButton::ViewSwitcher) - .font_active(crate::font::FONT_SEMIBOLD) -} diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index fca097ae..e51d6036 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -3,8 +3,7 @@ //! Implementation details for the horizontal layout of a segmented button. -use super::model::Model; -use super::selection_modes::Selectable; +use super::model::{Model, Selectable}; use super::style::StyleSheet; use super::widget::{SegmentedButton, SegmentedVariant}; @@ -18,10 +17,12 @@ pub type HorizontalSegmentedButton<'a, SelectionMode, Message, Renderer> = /// A type marker defining the horizontal variant of a [`SegmentedButton`]. pub struct Horizontal; -/// Row implementation of the [`SegmentedButton`]. +/// Horizontal implementation of the [`SegmentedButton`]. +/// +/// For details on the model, see the [`segmented_button`](super) module for more details. #[must_use] -pub fn horizontal_segmented_button( - model: &Model, +pub fn horizontal( + model: &Model, ) -> SegmentedButton where Renderer: iced_native::Renderer @@ -29,7 +30,7 @@ where + iced_native::image::Renderer + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, - SelectionMode: Selectable, + Model: Selectable, { SegmentedButton::new(model) } @@ -42,7 +43,8 @@ where + iced_native::image::Renderer + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, - SelectionMode: Selectable, + Model: Selectable, + SelectionMode: Default, { type Renderer = Renderer; diff --git a/src/widget/segmented_button/item.rs b/src/widget/segmented_button/item.rs deleted file mode 100644 index 1573c827..00000000 --- a/src/widget/segmented_button/item.rs +++ /dev/null @@ -1,57 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Defines the model that's used by each button in the widget. - -use crate::widget::IconSource; -use derive_setters::Setters; -use std::borrow::Cow; - -/// Describes a button in a segmented button -#[must_use] -pub fn item() -> SegmentedItem { - SegmentedItem::default() -} - -/// Information about a specific button in a segmented button -#[derive(Setters)] -pub struct SegmentedItem { - #[setters(into, strip_option)] - /// The label to display in this button. - pub text: Option>, - - #[setters(into, strip_option)] - /// An optionally-displayed icon beside the label. - pub icon: Option>, - - /// Whether the button is clickable or not. - pub enabled: bool, -} - -impl Default for SegmentedItem { - fn default() -> Self { - Self { - text: None, - icon: None, - enabled: true, - } - } -} - -impl From for SegmentedItem { - fn from(text: String) -> Self { - Self::from(Cow::Owned(text)) - } -} - -impl From<&'static str> for SegmentedItem { - fn from(text: &'static str) -> Self { - Self::from(Cow::Borrowed(text)) - } -} - -impl From> for SegmentedItem { - fn from(text: Cow<'static, str>) -> Self { - SegmentedItem::default().text(text) - } -} diff --git a/src/widget/segmented_button/mod.rs b/src/widget/segmented_button/mod.rs index de331af6..34b6ec74 100644 --- a/src/widget/segmented_button/mod.rs +++ b/src/widget/segmented_button/mod.rs @@ -1,11 +1,11 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -//! A widget providing a conjoined set of linear buttons that function in conjunction with each other. +//! A widget providing a conjoined set of linear items that function in conjunction as a single button. //! //! ## Example //! -//! Add the state and a message variant in your application for handling selections. +//! Add the model and a message variant in your application for handling selections. //! //! ```ignore //! use iced_core::Length; @@ -17,24 +17,35 @@ //! } //! //! struct App { -//! state: segmented_button::SingleSelectModel(), +//! model: segmented_button::SingleSelectModel, //! } //! ``` //! -//! Then add choices to the state, while activating the first. +//! Then add choices to the model, while activating the first. //! //! ```ignore -//! application.model = SingleSelectModel::builder() -//! .insert_activate("Choice A", 0) -//! .insert("Choice B", 1) -//! .insert("Choice C", 2) +//! application.model = segmented_button::Model::builder() +//! .insert(|b| b.text("Choice A").data(0u16)) +//! .insert(|b| b.text("Choice B").data(1u16)) +//! .insert(|b| b.text("Choice C").data(2u16)) //! .build(); //! ``` //! +//! Or incrementally insert items with +//! +//! ```ignore +//! let id = application.model.insert() +//! .text("Choice C") +//! .icon("custom-icon") +//! .data(3u16) +//! .data("custom-meta") +//! .id(); +//! ``` +//! //! Then use it in the view method to create segmented button widgets. //! //! ```ignore -//! let widget = horizontal_segmented_button(&application.model) +//! let widget = segmented_button::horizontal(&application.model) //! .style(theme::SegmentedButton::ViewSeitcher) //! .button_height(32) //! .button_padding([16, 10, 16, 10]) @@ -43,22 +54,46 @@ //! .spacing(8) //! .on_activate(AppMessage::Selected); //! ``` - -/// COSMIC configurations of [`SegmentedButton`]. -pub mod cosmic; +//! +//! And respond to events like so: +//! +//! ```ignore +//! match message { +//! AppMessage::Selected(id) => { +//! application.model.activate(id); +//! +//! if let Some(number) = application.model.data::(id) { +//! println!("activated item with number {number}"); +//! } +//! +//! if let Some(text) = application.text(id) { +//! println!("activated button with text {text}"); +//! } +//! } +//! } +//! ``` mod horizontal; -mod item; mod model; -mod selection_modes; mod style; mod vertical; mod widget; -pub use self::horizontal::{horizontal_segmented_button, HorizontalSegmentedButton}; -pub use self::item::{item, SegmentedItem}; -pub use self::model::{Batch, Key, Model, ModelBuilder, MultiSelectModel, SingleSelectModel}; -pub use self::selection_modes::Selectable; -pub use self::style::{Appearance, ButtonAppearance, ButtonStatusAppearance, StyleSheet}; -pub use self::vertical::{vertical_segmented_button, VerticalSegmentedButton}; +pub use self::horizontal::{horizontal, HorizontalSegmentedButton}; +pub use self::model::{ + BuilderEntity, Entity, EntityMut, Model, ModelBuilder, MultiSelect, MultiSelectEntityMut, + MultiSelectModel, Selectable, SingleSelect, SingleSelectEntityMut, SingleSelectModel, +}; +pub use self::style::{Appearance, ItemAppearance, ItemStatusAppearance, StyleSheet}; +pub use self::vertical::{vertical, VerticalSegmentedButton}; pub use self::widget::{focus, Id, SegmentedButton, SegmentedVariant}; + +/// Associates extra data with an external secondary map. +/// +/// The secondary map internally uses a `Vec`, so should only be used for data that +pub type SecondaryMap = slotmap::SecondaryMap; + +/// Associates extra data with an external sparse secondary map. +/// +/// Sparse maps internally use a `HashMap`, for data that is sparsely associated. +pub type SparseSecondaryMap = slotmap::SparseSecondaryMap; diff --git a/src/widget/segmented_button/model.rs b/src/widget/segmented_button/model.rs deleted file mode 100644 index e57bf0da..00000000 --- a/src/widget/segmented_button/model.rs +++ /dev/null @@ -1,243 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -use std::collections::VecDeque; - -use super::selection_modes::{MultiSelect, Selectable, SingleSelect}; -use super::SegmentedItem; -use slotmap::{SecondaryMap, SlotMap}; - -slotmap::new_key_type! { - /// A unique ID for a segmented button - pub struct Key; -} - -/// A model for single-select button selection. -pub type SingleSelectModel = Model; - -/// A model for multi-select button selection. -pub type MultiSelectModel = Model; - -/// The model held by the application, containing the unique IDs of each item and their respective contents. -#[derive(Default)] -pub struct Model { - pub(super) widget: WidgetModel, - pub(super) app: AppModel, -} - -/// The portion of the model used only by the application. -pub struct AppModel(SecondaryMap); - -impl Default for AppModel { - fn default() -> Self { - Self(SecondaryMap::default()) - } -} - -/// The portion of the model useful to the widget. -#[derive(Default)] -pub struct WidgetModel { - /// The content used for drawing segmented items. - pub(super) items: SlotMap, - - /// Order which the items will be displayed. - pub(super) order: VecDeque, - - /// Manages selections - pub(super) selection: SelectionMode, -} - -impl Model { - /// Activates the item in the model. - pub fn activate(&mut self, key: Key) { - self.widget.selection.active = key; - } - - /// Get an immutable reference to the component associated with the active item. - #[must_use] - pub fn active_component(&self) -> Option<&Component> { - self.component(self.active()) - } - - /// Get a mutable reference to the component associated with the active item. - #[must_use] - pub fn active_component_mut(&mut self) -> Option<&mut Component> { - self.component_mut(self.active()) - } - - /// Deactivates the active item. - pub fn deactivate(&mut self) { - self.widget.selection.active = Key::default(); - } - - /// The ID of the active item. - #[must_use] - pub fn active(&self) -> Key { - self.widget.selection.active - } -} - -impl Model { - /// Activates the item in the model. - pub fn activate(&mut self, key: Key) { - if !self.widget.selection.active.insert(key) { - self.widget.selection.active.remove(&key); - } - } - - /// Deactivates the item in the model. - pub fn deactivate(&mut self, key: Key) { - self.widget.selection.active.remove(&key); - } - - /// The IDs of the active items. - pub fn active(&self) -> impl Iterator + '_ { - self.widget.selection.active.iter().copied() - } -} - -impl Model -where - SelectionMode: Selectable, -{ - /// Creates a builder for initializing a model. - #[must_use] - pub fn builder() -> ModelBuilder { - ModelBuilder(Self { - widget: WidgetModel::default(), - app: AppModel::default(), - }) - } - - /// Convenience method for batching multiple operations - #[must_use] - pub fn batch(&mut self) -> Batch { - Batch(self) - } - - /// Get an immutable reference to an item in the model. - #[must_use] - pub fn content(&self, key: Key) -> Option<&SegmentedItem> { - self.widget.items.get(key) - } - - /// Get a mutable reference to an item in the model. - #[must_use] - pub fn item_mut(&mut self, key: Key) -> Option<&mut SegmentedItem> { - self.widget.items.get_mut(key) - } - - /// Get an immutable reference to a component associated with an item. - pub fn component(&self, key: Key) -> Option<&Component> { - self.app.0.get(key) - } - - /// Get a mutable reference to a component associated with an item. - pub fn component_mut(&mut self, key: Key) -> Option<&mut Component> { - self.app.0.get_mut(key) - } - - /// Insert a new item in the model. - pub fn insert(&mut self, content: impl Into, component: Component) -> Key { - let key = self.widget.items.insert(content.into()); - self.widget.order.push_back(key); - self.app.0.insert(key, component); - key - } - - /// Inserts and activates an item into the model. - pub fn insert_active( - &mut self, - content: impl Into, - component: Component, - ) -> Key { - let key = self.insert(content, component); - self.widget.selection.activate(key); - key - } - - /// Checks if the item is active in the model. - #[must_use] - pub fn is_active(&self, key: Key) -> bool { - self.widget.selection.is_active(key) - } - - /// The position of the item in the model. - pub fn position(&self, key: Key) -> Option { - self.widget.order.iter().position(|k| *k == key) - } - - /// Removes an item from the model. - pub fn remove(&mut self, key: Key) { - self.widget.items.remove(key); - self.widget.selection.deactivate(key); - self.app.0.remove(key); - - if let Some(index) = self.position(key) { - self.widget.order.remove(index); - } - } - - /// Swap the position of two items in the model. - pub fn swap_position(&mut self, first: Key, second: Key) { - let Some(first_index) = self.position(first) else { - return - }; - - let Some(second_index) = self.position(second) else { - return - }; - - self.widget.order.swap(first_index, second_index); - } -} - -pub struct ModelBuilder(Model); - -impl ModelBuilder { - /// Inserts a new item and its associated component into the model. - #[must_use] - pub fn insert(mut self, content: impl Into, component: Component) -> Self { - self.0.insert(content, component); - self - } - - /// Inserts and activates an new item. - #[must_use] - pub fn insert_active( - mut self, - content: impl Into, - component: Component, - ) -> Self { - self.0.insert_active(content, component); - self - } - - pub fn build(self) -> Model { - self.0 - } -} - -/// Convenience type for batching multiple operations -pub struct Batch<'a, SelectionMode, Component>(&'a mut Model); - -impl<'a, SelectionMode: Selectable, Component> Batch<'a, SelectionMode, Component> { - /// Insert a new button. - #[must_use] - pub fn insert(self, content: impl Into, component: Component) -> Self { - self.0.insert(content, component); - self - } - - /// Inserts and activates a button. - #[must_use] - pub fn insert_active(self, content: impl Into, component: Component) -> Self { - self.0.insert_active(content, component); - self - } - - /// Removes a button. - pub fn remove(&mut self, key: Key) { - self.0.remove(key); - } -} diff --git a/src/widget/segmented_button/model/builder.rs b/src/widget/segmented_button/model/builder.rs new file mode 100644 index 00000000..3705bf9c --- /dev/null +++ b/src/widget/segmented_button/model/builder.rs @@ -0,0 +1,131 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use slotmap::{SecondaryMap, SparseSecondaryMap}; + +use super::{Entity, Model, Selectable}; +use crate::widget::IconSource; +use std::borrow::Cow; + +/// A builder for a [`Model`]. +#[derive(Default)] +pub struct ModelBuilder(Model); + +/// Constructs a new item for the [`ModelBuilder`]. +pub struct BuilderEntity { + model: ModelBuilder, + id: Entity, +} + +impl ModelBuilder +where + Model: Selectable, +{ + /// Inserts a new item and its associated data into the model. + #[must_use] + pub fn insert( + mut self, + builder: impl Fn(BuilderEntity) -> BuilderEntity, + ) -> Self { + let id = self.0.insert().id(); + builder(BuilderEntity { model: self, id }).model + } + + /// Consumes the builder and returns the model. + pub fn build(self) -> Model { + self.0 + } +} + +impl BuilderEntity +where + Model: Selectable, +{ + /// Activates the newly-inserted item. + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn activate(mut self) -> Self { + self.model.0.activate(self.id); + self + } + + /// Associates extra data with an external secondary map. + /// + /// The secondary map internally uses a `Vec`, so should only be used for data that + /// is commonly associated. + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn secondary(self, map: &mut SecondaryMap, data: Data) -> Self { + map.insert(self.id, data); + self + } + + /// Associates extra data with an external sparse secondary map. + /// + /// Sparse maps internally use a `HashMap`, for data that is sparsely associated. + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn secondary_sparse( + self, + map: &mut SparseSecondaryMap, + data: Data, + ) -> Self { + map.insert(self.id, data); + self + } + + /// Assigns extra data to the item. + /// + /// There can only be one data component per Rust type. + /// + /// ```ignore + /// enum ViewItem { A } + /// + /// segmented_button::Model::builder() + /// .insert(|b| b.text("Item A").data(ViewItem::A)) + /// .build() + /// ``` + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn data(mut self, data: Data) -> Self { + self.model.0.data_set(self.id, data); + self + } + + /// Defines an icon for the item. + /// + /// ```ignore + /// segmented_button::Model::builder() + /// .insert(|b| b.text("Item A").icon("custom-icon")) + /// .build() + /// ``` + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn icon(mut self, icon: impl Into>) -> Self { + self.model.0.icon_set(self.id, icon); + self + } + + /// Define the position of the newly-inserted item. + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn position(mut self, position: u16) -> Self { + self.model.0.position_set(self.id, position); + self + } + + /// Swap the position with another item in the model. + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn position_swap(mut self, other: Entity) -> Self { + self.model.0.position_swap(self.id, other); + self + } + + /// Defines the text for the item. + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn text(mut self, text: impl Into>) -> Self { + self.model.0.text_set(self.id, text); + self + } + + /// Calls a function with the ID + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn with_id(self, func: impl FnOnce(Entity)) -> Self { + func(self.id); + self + } +} diff --git a/src/widget/segmented_button/model/entity.rs b/src/widget/segmented_button/model/entity.rs new file mode 100644 index 00000000..da575ac6 --- /dev/null +++ b/src/widget/segmented_button/model/entity.rs @@ -0,0 +1,127 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +use std::borrow::Cow; + +use slotmap::{SecondaryMap, SparseSecondaryMap}; + +use crate::widget::IconSource; + +use super::{Entity, Model, Selectable}; + +/// A newly-inserted item which may have additional actions applied to it. +pub struct EntityMut<'a, SelectionMode: Default> { + pub(super) id: Entity, + pub(super) model: &'a mut Model, +} + +impl<'a, SelectionMode: Default> EntityMut<'a, SelectionMode> +where + Model: Selectable, +{ + /// Activates the newly-inserted item. + /// + /// ```ignore + /// model.insert().text("Item A").activate(); + /// ``` + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn activate(self) -> Self { + self.model.activate(self.id); + self + } + + /// Associates extra data with an external secondary map. + /// + /// The secondary map internally uses a `Vec`, so should only be used for data that + /// is commonly associated. + /// + /// ```ignore + /// let mut secondary_data = segmented_button::SecondaryMap::default(); + /// model.insert().text("Item A").secondary(&mut secondary_data, String::new("custom data")); + /// ``` + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn secondary(self, map: &mut SecondaryMap, data: Data) -> Self { + map.insert(self.id, data); + self + } + + /// Associates extra data with an external sparse secondary map. + /// + /// Sparse maps internally use a `HashMap`, for data that is sparsely associated. + /// + /// ```ignore + /// let mut secondary_data = segmented_button::SparseSecondaryMap::default(); + /// model.insert().text("Item A").secondary(&mut secondary_data, String::new("custom data")); + /// ``` + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn secondary_sparse( + self, + map: &mut SparseSecondaryMap, + data: Data, + ) -> Self { + map.insert(self.id, data); + self + } + + /// Associates data with the item. + /// + /// There may only be one data component per Rust type. + /// + /// ```ignore + /// model.insert().text("Item A").data(String::from("custom string")); + /// ``` + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn data(self, data: Data) -> Self { + self.model.data_set(self.id, data); + self + } + + /// Define an icon for the item. + /// + /// ```ignore + /// model.insert().text("Item A").icon(IconSource::Name("icon-a".into())); + /// ``` + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn icon(self, icon: impl Into>) -> Self { + self.model.icon_set(self.id, icon); + self + } + + /// Returns the ID of the item that was inserted. + /// + /// ```ignore + /// let id = model.insert("Item A").id(); + /// ``` + #[must_use] + pub fn id(self) -> Entity { + self.id + } + + /// Define the position of the item. + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn position(self, position: u16) -> Self { + self.model.position_set(self.id, position); + self + } + + /// Swap the position with another item in the model. + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn position_swap(self, other: Entity) -> Self { + self.model.position_swap(self.id, other); + self + } + + /// Defines the text for the item. + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn text(self, text: impl Into>) -> Self { + self.model.text_set(self.id, text); + self + } + + /// Calls a function with the ID without consuming the wrapper. + #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] + pub fn with_id(self, func: impl FnOnce(Entity)) -> Self { + func(self.id); + self + } +} diff --git a/src/widget/segmented_button/model/mod.rs b/src/widget/segmented_button/model/mod.rs new file mode 100644 index 00000000..0fbc8369 --- /dev/null +++ b/src/widget/segmented_button/model/mod.rs @@ -0,0 +1,360 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +mod builder; +pub use self::builder::{BuilderEntity, ModelBuilder}; + +mod entity; +pub use self::entity::EntityMut; + +mod selection; +pub use self::selection::{MultiSelect, Selectable, SingleSelect}; + +use crate::widget::IconSource; +use slotmap::{SecondaryMap, SlotMap}; +use std::any::{Any, TypeId}; +use std::borrow::Cow; +use std::collections::{HashMap, VecDeque}; + +slotmap::new_key_type! { + /// A unique ID for an item in the [`Model`]. + pub struct Entity; +} + +#[derive(Clone, Debug)] +pub struct Settings { + pub enabled: bool, +} + +impl Default for Settings { + fn default() -> Self { + Self { enabled: true } + } +} + +/// A model for single-select button selection. +pub type SingleSelectModel = Model; + +/// Single-select variant of an [`EntityMut`]. +pub type SingleSelectEntityMut<'a> = EntityMut<'a, SingleSelect>; + +/// A model for multi-select button selection. +pub type MultiSelectModel = Model; + +/// Multi-select variant of an [`EntityMut`]. +pub type MultiSelectEntityMut<'a> = EntityMut<'a, MultiSelect>; + +/// The portion of the model used only by the application. +#[derive(Debug, Default)] +pub(super) struct Storage(HashMap>>); + +/// The model held by the application, containing the unique IDs and data of each inserted item. +#[derive(Default, Debug)] +pub struct Model { + /// The content used for drawing segmented items. + pub(super) items: SlotMap, + + /// Icons optionally-defined for each item. + pub(super) icons: SecondaryMap>, + + /// Text optionally-defined for each item. + pub(super) text: SecondaryMap>, + + /// Order which the items will be displayed. + pub(super) order: VecDeque, + + /// Manages selections + pub(super) selection: SelectionMode, + + /// Data managed by the application. + pub(super) storage: Storage, +} + +impl Model +where + Self: Selectable, +{ + /// Activates the item in the model. + /// + /// ```ignore + /// model.activate(id); + /// ``` + pub fn activate(&mut self, id: Entity) { + Selectable::activate(self, id); + } + + /// Creates a builder for initializing a model. + /// + /// ```ignore + /// let model = segmented_button::Model::builder() + /// .insert(|b| b.text("Item A").activate()) + /// .insert(|b| b.text("Item B")) + /// .insert(|b| b.text("Item C")) + /// .build(); + /// ``` + #[must_use] + pub fn builder() -> ModelBuilder { + ModelBuilder::default() + } + + /// Removes all items from the model. + /// + /// Any IDs held elsewhere by the application will no longer be usable with the map. + /// The generation is incremented on removal, so the stale IDs will return `None` for + /// any attempt to get values from the map. + /// + /// ```ignore + /// model.clear(); + /// ``` + pub fn clear(&mut self) { + for entity in self.order.clone() { + self.remove(entity); + } + } + + /// Check if an item exists in the map. + /// + /// ```ignore + /// if model.contains_item(id) { + /// println!("ID is still valid"); + /// } + /// ``` + pub fn contains_item(&self, id: Entity) -> bool { + self.items.contains_key(id) + } + + /// Get an immutable reference to data associated with an item. + /// + /// ```ignore + /// if let Some(data) = model.data::(id) { + /// println!("found string on {:?}: {}", id, data); + /// } + /// ``` + pub fn data(&self, id: Entity) -> Option<&Data> { + self.storage + .0 + .get(&TypeId::of::()) + .and_then(|storage| storage.get(id)) + .and_then(|data| data.downcast_ref()) + } + + /// Get a mutable reference to data associated with an item. + pub fn data_mut(&mut self, id: Entity) -> Option<&mut Data> { + self.storage + .0 + .get_mut(&TypeId::of::()) + .and_then(|storage| storage.get_mut(id)) + .and_then(|data| data.downcast_mut()) + } + + /// Associates data with the item. + /// + /// There may only be one data component per Rust type. + /// + /// ```ignore + /// model.data_set::(id, String::from("custom string")); + /// ``` + pub fn data_set(&mut self, id: Entity, data: Data) { + if self.contains_item(id) { + self.storage + .0 + .entry(TypeId::of::()) + .or_insert_with(SecondaryMap::new) + .insert(id, Box::new(data)); + } + } + + /// Removes a specific data type from the item. + /// + /// ```ignore + /// model.data.remove::(id); + /// ``` + pub fn data_remove(&mut self, id: Entity) { + self.storage + .0 + .get_mut(&TypeId::of::()) + .and_then(|storage| storage.remove(id)); + } + + /// Enable or disable an item. + /// + /// ```ignore + /// model.enable(id, true); + /// ``` + pub fn enable(&mut self, id: Entity, enable: bool) { + if let Some(e) = self.items.get_mut(id) { + e.enabled = enable; + } + } + + /// Immutable reference to the icon associated with the item. + /// + /// ```ignore + /// if let Some(icon) = model.icon(id) { + /// println!("has icon: {:?}", icon); + /// } + /// ``` + pub fn icon(&self, id: Entity) -> Option<&IconSource<'static>> { + self.icons.get(id) + } + + /// Sets a new icon for an item. + /// + /// ```ignore + /// if let Some(old_icon) = model.icon_set(IconSource::Name("new-icon".into())) { + /// println!("previously had icon: {:?}", old_icon); + /// } + /// ``` + pub fn icon_set( + &mut self, + id: Entity, + icon: impl Into>, + ) -> Option> { + if !self.contains_item(id) { + return None; + } + + self.icons.insert(id, icon.into()) + } + + /// Removes the icon from an item. + /// + /// ```ignore + /// if let Some(old_icon) = model.icon_remove(id) { + /// println!("previously had icon: {:?}", old_icon); + /// } + pub fn icon_remove(&mut self, id: Entity) -> Option> { + self.icons.remove(id) + } + + /// Inserts a new item in the model. + /// + /// ```ignore + /// let id = model.insert().text("Item A").icon("custom-icon").id(); + /// ``` + #[must_use] + pub fn insert(&mut self) -> EntityMut { + let id = self.items.insert(Settings::default()); + self.order.push_back(id); + EntityMut { model: self, id } + } + + /// Check if the item is enabled. + /// + /// ```ignore + /// + /// if model.is_enabled(id) { + /// if let Some(text) = model.text(id) { + /// println!("{text} is enabled"); + /// } + /// } + /// ``` + #[must_use] + pub fn is_enabled(&self, id: Entity) -> bool { + self.items.get(id).map_or(false, |e| e.enabled) + } + + /// The position of the item in the model. + /// + /// ```ignore + /// if let Some(position) = model.position(id) { + /// println!("found item at {}", position); + /// } + pub fn position(&self, id: Entity) -> Option { + self.order.iter().position(|k| *k == id) + } + + /// Change the position of an item in the model. + /// + /// ```ignore + /// if let Some(new_position) = model.position_set(id, 0) { + /// println!("placed item at {}", new_position); + /// } + /// ``` + pub fn position_set(&mut self, id: Entity, position: u16) -> Option { + let Some(index) = self.position(id) else { + return None + }; + + let position = self.order.len().min(position as usize); + + self.order.remove(index); + self.order.insert(position, id); + Some(position) + } + + /// Swap the position of two items in the model. + /// + /// Returns false if the swap cannot be performed. + /// + /// ```ignore + /// if model.position_swap(first_id, second_id) { + /// println!("positions swapped"); + /// } + /// ``` + pub fn position_swap(&mut self, first: Entity, second: Entity) -> bool { + let Some(first_index) = self.position(first) else { + return false + }; + + let Some(second_index) = self.position(second) else { + return false + }; + + self.order.swap(first_index, second_index); + true + } + + /// Removes an item from the model. + /// + /// The generation of the slot for the ID will be incremented, so this ID will no + /// longer be usable with the map. Subsequent attempts to get values from the map + /// with this ID will return `None` and failed to assign values. + pub fn remove(&mut self, id: Entity) { + self.items.remove(id); + self.deactivate(id); + + for storage in self.storage.0.values_mut() { + storage.remove(id); + } + + if let Some(index) = self.position(id) { + self.order.remove(index); + } + } + + /// Immutable reference to the text assigned to the item. + /// + /// ```ignore + /// if let Some(text) = model.text(id) { + /// println!("{:?} has text {text}", id); + /// } + /// ``` + pub fn text(&self, id: Entity) -> Option<&str> { + self.text.get(id).map(Cow::as_ref) + } + + /// Sets new text for an item. + /// + /// ```ignore + /// if let Some(old_text) = model.text_set(id, "Item B") { + /// println!("{:?} had text {}", id, old_text) + /// } + /// ``` + pub fn text_set(&mut self, id: Entity, text: impl Into>) -> Option> { + if !self.contains_item(id) { + return None; + } + + self.text.insert(id, text.into()) + } + + /// Removes text from an item. + /// ```ignore + /// if let Some(old_text) = model.text_remove(id) { + /// println!("{:?} had text {}", id, old_text); + /// } + pub fn text_remove(&mut self, id: Entity) -> Option> { + self.text.remove(id) + } +} diff --git a/src/widget/segmented_button/model/selection.rs b/src/widget/segmented_button/model/selection.rs new file mode 100644 index 00000000..59a39cd4 --- /dev/null +++ b/src/widget/segmented_button/model/selection.rs @@ -0,0 +1,106 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Describes logic specific to the single-select and multi-select modes of a model. + +use super::{Entity, Model}; +use std::collections::HashSet; + +/// Describes a type that has selectable items. +pub trait Selectable { + /// Activate an item. + fn activate(&mut self, id: Entity); + + /// Deactivate an item. + fn deactivate(&mut self, id: Entity); + + /// Checks if the item is active. + fn is_active(&self, id: Entity) -> bool; +} + +/// [`Model`] Ensures that only one key may be selected. +#[derive(Debug, Default)] +pub struct SingleSelect { + pub active: Entity, +} + +impl Selectable for Model { + fn activate(&mut self, id: Entity) { + if !self.items.contains_key(id) { + return; + } + + self.selection.active = id; + } + + fn deactivate(&mut self, _id: Entity) { + self.selection.active = Entity::default(); + } + + fn is_active(&self, id: Entity) -> bool { + self.selection.active == id + } +} + +impl Model { + /// Get an immutable reference to the data associated with the active item. + #[must_use] + pub fn active_data(&self) -> Option<&Data> { + self.data(self.active()) + } + + /// Get a mutable reference to the data associated with the active item. + #[must_use] + pub fn active_data_mut(&mut self) -> Option<&mut Data> { + self.data_mut(self.active()) + } + + /// Deactivates the active item. + pub fn deactivate(&mut self) { + Selectable::deactivate(self, Entity::default()); + } + + /// The ID of the active item. + #[must_use] + pub fn active(&self) -> Entity { + self.selection.active + } +} + +/// [`Model`] permits multiple keys to be active at a time. +#[derive(Debug, Default)] +pub struct MultiSelect { + pub active: HashSet, +} + +impl Selectable for Model { + fn activate(&mut self, id: Entity) { + if !self.items.contains_key(id) { + return; + } + + if !self.selection.active.insert(id) { + self.selection.active.remove(&id); + } + } + + fn deactivate(&mut self, id: Entity) { + self.selection.active.remove(&id); + } + + fn is_active(&self, id: Entity) -> bool { + self.selection.active.contains(&id) + } +} + +impl Model { + /// Deactivates the item in the model. + pub fn deactivate(&mut self, id: Entity) { + Selectable::deactivate(self, id); + } + + /// The IDs of the active items. + pub fn active(&self) -> impl Iterator + '_ { + self.selection.active.iter().copied() + } +} diff --git a/src/widget/segmented_button/selection_modes.rs b/src/widget/segmented_button/selection_modes.rs deleted file mode 100644 index 91a31b3f..00000000 --- a/src/widget/segmented_button/selection_modes.rs +++ /dev/null @@ -1,59 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Describes logic specific to the single-select and multi-select modes of a model. - -use super::Key; -use std::collections::HashSet; - -/// Describes a type that has selectable components. -pub trait Selectable: Default { - /// Activate a component. - fn activate(&mut self, key: Key); - - /// Deactivate a component. - fn deactivate(&mut self, key: Key); - - /// Checks if the component is active. - fn is_active(&self, key: Key) -> bool; -} - -/// Ensures that only one key may be selected. -#[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 - } -} - -/// Permits multiple keys to be active at a time. -#[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) - } -} diff --git a/src/widget/segmented_button/style.rs b/src/widget/segmented_button/style.rs index 2e386af2..5cc2e1a7 100644 --- a/src/widget/segmented_button/style.rs +++ b/src/widget/segmented_button/style.rs @@ -3,7 +3,7 @@ use iced_core::{Background, BorderRadius, Color}; -/// The appearance of a segmented button. +/// Appearance of the segmented button. #[derive(Default, Clone, Copy)] pub struct Appearance { pub background: Option, @@ -12,15 +12,15 @@ pub struct Appearance { pub border_end: Option<(f32, Color)>, pub border_start: Option<(f32, Color)>, pub border_top: Option<(f32, Color)>, - pub active: ButtonStatusAppearance, - pub inactive: ButtonStatusAppearance, - pub hover: ButtonStatusAppearance, - pub focus: ButtonStatusAppearance, + pub active: ItemStatusAppearance, + pub inactive: ItemStatusAppearance, + pub hover: ItemStatusAppearance, + pub focus: ItemStatusAppearance, } -/// The appearance of a button in the segmented button +/// Appearance of an item in the segmented button. #[derive(Default, Clone, Copy)] -pub struct ButtonAppearance { +pub struct ItemAppearance { pub border_radius: BorderRadius, pub border_bottom: Option<(f32, Color)>, pub border_end: Option<(f32, Color)>, @@ -28,12 +28,13 @@ pub struct ButtonAppearance { pub border_top: Option<(f32, Color)>, } +/// Appearance of an item based on its status. #[derive(Default, Clone, Copy)] -pub struct ButtonStatusAppearance { +pub struct ItemStatusAppearance { pub background: Option, - pub first: ButtonAppearance, - pub middle: ButtonAppearance, - pub last: ButtonAppearance, + pub first: ItemAppearance, + pub middle: ItemAppearance, + pub last: ItemAppearance, pub text_color: Color, } diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 885fd4fe..ab4897ec 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -3,8 +3,7 @@ //! Implementation details for the vertical layout of a segmented button. -use super::model::Model; -use super::selection_modes::Selectable; +use super::model::{Model, Selectable}; use super::style::StyleSheet; use super::widget::{SegmentedButton, SegmentedVariant}; @@ -19,9 +18,11 @@ pub type VerticalSegmentedButton<'a, SelectionMode, Message, Renderer> = SegmentedButton<'a, Vertical, SelectionMode, Message, Renderer>; /// Vertical implementation of the [`SegmentedButton`]. +/// +/// For details on the model, see the [`segmented_button`](super) module for more details. #[must_use] -pub fn vertical_segmented_button( - model: &Model, +pub fn vertical( + model: &Model, ) -> SegmentedButton where Renderer: iced_native::Renderer @@ -29,7 +30,8 @@ where + iced_native::image::Renderer + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, - SelectionMode: Selectable, + Model: Selectable, + SelectionMode: Default, { SegmentedButton::new(model) } @@ -42,7 +44,8 @@ where + iced_native::image::Renderer + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, - SelectionMode: Selectable, + Model: Selectable, + SelectionMode: Default, { type Renderer = Renderer; diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 1e2fff67..312f5516 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -3,8 +3,7 @@ use std::marker::PhantomData; -use super::model::{Key, Model, WidgetModel}; -use super::selection_modes::Selectable; +use super::model::{Entity, Model, Selectable}; use super::style::StyleSheet; use derive_setters::Setters; @@ -20,13 +19,13 @@ use iced_native::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widg #[derive(Default)] struct LocalState { /// The first focusable key. - first: Key, + first: Entity, /// If the widget is focused or not. focused: bool, /// The key inside the widget that is currently focused. - focused_key: Key, + focused_key: Entity, /// The ID of the button that is being hovered. Defaults to null. - hovered: Key, + hovered: Entity, } impl operation::Focusable for LocalState { @@ -41,7 +40,7 @@ impl operation::Focusable for LocalState { fn unfocus(&mut self) { self.focused = false; - self.focused_key = Key::default(); + self.focused_key = Entity::default(); } } @@ -64,19 +63,22 @@ pub trait SegmentedVariant { fn variant_layout(&self, renderer: &Self::Renderer, limits: &layout::Limits) -> layout::Node; } +/// A conjoined group of items that function together as a button. #[derive(Setters)] -pub struct SegmentedButton<'a, Variant, Selection, Message, Renderer> +pub struct SegmentedButton<'a, Variant, SelectionMode, Message, Renderer> where Renderer: iced_native::Renderer + iced_native::text::Renderer + iced_native::image::Renderer + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, - Selection: Selectable, + Model: Selectable, + SelectionMode: Default, { /// The model borrowed from the application create this widget. #[setters(skip)] - pub(super) model: &'a WidgetModel, + pub(super) model: &'a Model, + /// iced widget ID pub(super) id: Option, /// Padding around a button. pub(super) button_padding: [u16; 4], @@ -103,14 +105,14 @@ where pub(super) style: ::Style, #[setters(skip)] /// Emits the ID of the activated widget on selection. - pub(super) on_activate: Option Message>>, + pub(super) on_activate: Option Message>>, #[setters(skip)] /// Defines the implementation of this struct variant: PhantomData, } -impl<'a, Variant, Selection, Message, Renderer> - SegmentedButton<'a, Variant, Selection, Message, Renderer> +impl<'a, Variant, SelectionMode, Message, Renderer> + SegmentedButton<'a, Variant, SelectionMode, Message, Renderer> where Renderer: iced_native::Renderer + iced_native::text::Renderer @@ -118,12 +120,13 @@ where + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, Self: SegmentedVariant, - Selection: Selectable, + Model: Selectable, + SelectionMode: Default, { #[must_use] - pub fn new(model: &'a Model) -> Self { + pub fn new(model: &'a Model) -> Self { Self { - model: &model.widget, + model, id: None, button_padding: [4, 4, 4, 4], button_height: 32, @@ -142,7 +145,7 @@ where } /// Check if an item is enabled. - fn is_enabled(&self, key: Key) -> bool { + fn is_enabled(&self, key: Entity) -> bool { self.model.items.get(key).map_or(false, |item| item.enabled) } @@ -166,7 +169,7 @@ where } } - state.focused_key = Key::default(); + state.focused_key = Entity::default(); event::Status::Ignored } @@ -190,13 +193,13 @@ where } } - state.focused_key = Key::default(); + state.focused_key = Entity::default(); event::Status::Ignored } /// Emits the ID of the activated widget on selection. #[must_use] - pub fn on_activate(mut self, on_activate: impl Fn(Key) -> Message + 'static) -> Self { + pub fn on_activate(mut self, on_activate: impl Fn(Entity) -> Message + 'static) -> Self { self.on_activate = Some(Box::from(on_activate)); self } @@ -210,12 +213,12 @@ where let mut width = 0.0f32; let mut height = 0.0f32; - for content in self.model.items.values() { + for key in self.model.order.iter().copied() { let mut button_width = 0.0f32; let mut button_height = 0.0f32; // Add text to measurement if text was given. - if let Some(text) = content.text.as_deref() { + if let Some(text) = self.model.text(key) { let (w, h) = renderer.measure(text, text_size, Default::default(), bounds); button_width = w; @@ -223,7 +226,7 @@ where } // Add icon to measurement if icon was given. - if content.icon.is_some() { + if self.model.icon(key).is_some() { button_width += f32::from(self.icon_size) + f32::from(self.button_spacing); button_height = f32::from(self.icon_size); } @@ -241,8 +244,8 @@ where } } -impl<'a, Variant, Selection, Message, Renderer> Widget - for SegmentedButton<'a, Variant, Selection, Message, Renderer> +impl<'a, Variant, SelectionMode, Message, Renderer> Widget + for SegmentedButton<'a, Variant, SelectionMode, Message, Renderer> where Renderer: iced_native::Renderer + iced_native::text::Renderer @@ -250,7 +253,8 @@ where + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, Self: SegmentedVariant, - Selection: Selectable, + Model: Selectable, + SelectionMode: Default, Message: 'static + Clone, { fn tag(&self) -> tree::Tag { @@ -311,7 +315,7 @@ where } } } else { - state.hovered = Key::default(); + state.hovered = Entity::default(); } if state.focused { @@ -412,13 +416,11 @@ where // Draw each of the items in the widget. for (nth, key) in self.model.order.iter().copied().enumerate() { - let content = &self.model.items[key]; - let mut bounds = self.variant_button_bounds(bounds, nth); let (status_appearance, font) = if state.focused_key == key { (appearance.focus, &self.font_active) - } else if self.model.selection.is_active(key) { + } else if self.model.is_active(key) { (appearance.active, &self.font_active) } else if state.hovered == key { (appearance.hover, &self.font_hovered) @@ -470,7 +472,7 @@ where let text_size = renderer.default_size(); // Draw the image beside the text. - let horizontal_alignment = if let Some(icon) = &content.icon { + let horizontal_alignment = if let Some(icon) = self.model.icon(key) { bounds.x += f32::from(self.button_padding[0]); bounds.y += f32::from(self.button_padding[1]); bounds.width -= @@ -510,7 +512,7 @@ where alignment::Horizontal::Center }; - if let Some(text) = content.text.as_deref() { + if let Some(text) = self.model.text(key) { bounds.y = y; // Draw the text in this button. @@ -537,8 +539,8 @@ where } } -impl<'a, Variant, Selection, Message, Renderer> - From> +impl<'a, Variant, SelectionMode, Message, Renderer> + From> for Element<'a, Message, Renderer> where Renderer: iced_native::Renderer @@ -547,13 +549,14 @@ where + iced_native::svg::Renderer + 'a, Renderer::Theme: StyleSheet, - SegmentedButton<'a, Variant, Selection, Message, Renderer>: + SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>: SegmentedVariant, Variant: 'static, - Selection: Selectable, + Model: Selectable, + SelectionMode: Default, Message: 'static + Clone, { - fn from(mut widget: SegmentedButton<'a, Variant, Selection, Message, Renderer>) -> Self { + fn from(mut widget: SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>) -> Self { if widget.model.items.is_empty() { widget.spacing = 0; } @@ -568,7 +571,7 @@ pub fn focus(id: Id) -> Command { Command::widget(operation::focusable::focus(id.0)) } -/// The identifier of a segmented item. +/// The iced identifier of a segmented button. #[derive(Debug, Clone, PartialEq, Eq, Hash)] pub struct Id(widget::Id); diff --git a/src/widget/segmented_selection.rs b/src/widget/segmented_selection.rs new file mode 100644 index 00000000..0b6a2059 --- /dev/null +++ b/src/widget/segmented_selection.rs @@ -0,0 +1,49 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! A selection of multiple choices appearing as a conjoined button. +//! +//! See the [`segmented_button`] module for more details. + +use super::segmented_button::{ + self, HorizontalSegmentedButton, Model, Selectable, VerticalSegmentedButton, +}; + +/// A selection of multiple choices appearing as a conjoined button. +/// +/// The data for the widget comes from a model that is maintained the application. +/// +/// For details on the model, see the [`segmented_button`] module for more details. +#[must_use] +pub fn horizontal( + model: &Model, +) -> HorizontalSegmentedButton +where + Model: Selectable, +{ + segmented_button::horizontal(model) + .button_padding([16, 0, 16, 0]) + .button_height(32) + .style(crate::theme::SegmentedButton::Selection) + .font_active(crate::font::FONT_SEMIBOLD) +} + +/// A selection of multiple choices appearing as a conjoined button. +/// +/// The data for the widget comes from a model that is maintained the application. +/// +/// For details on the model, see the [`segmented_button`] module for more details. +#[must_use] +pub fn vertical( + model: &Model, +) -> VerticalSegmentedButton +where + Model: Selectable, + SelectionMode: Default, +{ + segmented_button::vertical(model) + .button_padding([16, 0, 16, 0]) + .button_height(32) + .style(crate::theme::SegmentedButton::Selection) + .font_active(crate::font::FONT_SEMIBOLD) +} diff --git a/src/widget/view_switcher.rs b/src/widget/view_switcher.rs new file mode 100644 index 00000000..7ff711c7 --- /dev/null +++ b/src/widget/view_switcher.rs @@ -0,0 +1,49 @@ +// Copyright 2023 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! A collection of tabs for developing a tabbed interface. +//! +//! See the [`segmented_button`] module for more details. + +use super::segmented_button::{ + self, HorizontalSegmentedButton, Model, SegmentedButton, Selectable, VerticalSegmentedButton, +}; + +/// A collection of tabs for developing a tabbed interface. +/// +/// The data for the widget comes from a model supplied by the application. +/// +/// For details on the model, see the [`segmented_button`] module for more details. +#[must_use] +pub fn horizontal( + model: &Model, +) -> HorizontalSegmentedButton +where + Model: Selectable, +{ + segmented_button::horizontal(model) + .button_padding([16, 0, 16, 0]) + .button_height(48) + .style(crate::theme::SegmentedButton::ViewSwitcher) + .font_active(crate::font::FONT_SEMIBOLD) +} + +/// A collection of tabs for developing a tabbed interface. +/// +/// The data for the widget comes from a model that is maintained the application. +/// +/// For details on the model, see the [`segmented_button`] module for more details. +#[must_use] +pub fn vertical( + model: &Model, +) -> VerticalSegmentedButton +where + Model: Selectable, + SelectionMode: Default, +{ + SegmentedButton::new(model) + .button_padding([16, 0, 16, 0]) + .button_height(48) + .style(crate::theme::SegmentedButton::ViewSwitcher) + .font_active(crate::font::FONT_SEMIBOLD) +}