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) +}