From 29c7444a3057d288ad4ea00f2bcd86957d3ab4db Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 9 Jan 2023 16:18:02 +0100 Subject: [PATCH] feat: focusable segmented items in segmented button --- examples/cosmic/src/window.rs | 63 ++-- examples/cosmic/src/window/demo.rs | 42 +-- src/theme/segmented_button.rs | 291 +++++++++-------- src/widget/nav_bar.rs | 11 +- src/widget/segmented_button/cosmic.rs | 51 +-- src/widget/segmented_button/horizontal.rs | 35 ++- src/widget/segmented_button/item.rs | 57 ++++ src/widget/segmented_button/mod.rs | 42 +-- src/widget/segmented_button/model.rs | 202 ++++++++++++ .../segmented_button/selection_modes.rs | 59 ++++ src/widget/segmented_button/state.rs | 292 ------------------ src/widget/segmented_button/style.rs | 1 + src/widget/segmented_button/vertical.rs | 29 +- src/widget/segmented_button/widget.rs | 230 +++++++++++--- 14 files changed, 794 insertions(+), 611 deletions(-) create mode 100644 src/widget/segmented_button/item.rs create mode 100644 src/widget/segmented_button/model.rs create mode 100644 src/widget/segmented_button/selection_modes.rs delete mode 100644 src/widget/segmented_button/state.rs diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index c02a014a..88f76bd6 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -12,9 +12,8 @@ use cosmic::{ iced_winit::window::{close, drag, minimize, toggle_maximize}, theme::{self, Theme}, widget::{ - header_bar, icon, list, nav_bar, nav_button, scrollable, - segmented_button::{self, cosmic::vertical_view_switcher, SingleSelect}, - settings, + header_bar, icon, list, nav_bar, nav_button, scrollable, segmented_button, settings, + IconSource, }, Element, ElementExt, }; @@ -136,7 +135,7 @@ pub struct Window { debug: bool, demo: demo::State, desktop: desktop::State, - nav_bar_pages: segmented_button::State, + nav_bar_pages: segmented_button::SingleSelectModel, nav_bar_toggled_condensed: bool, nav_bar_toggled: bool, page: Page, @@ -190,6 +189,21 @@ 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 page_title(&self, page: Page) -> Element { row!(text(page.title()).size(30), horizontal_space(Length::Fill),).into() } @@ -291,29 +305,22 @@ impl Application for Window { window.title = String::from("COSMIC Design System - Iced"); - let mut add_page = |page: Page| { - let content = segmented_button::Content::default() - .text(page.title()) - .icon(page.icon_name()); - window.nav_bar_pages.insert(content, page) - }; - - add_page(Page::Demo); - add_page(Page::WiFi); - add_page(Page::Networking(None)); - add_page(Page::Bluetooth); - let key = add_page(Page::Desktop(None)); - add_page(Page::InputDevices(None)); - add_page(Page::Displays); - add_page(Page::PowerAndBattery); - add_page(Page::Sound); - add_page(Page::PrintersAndScanners); - add_page(Page::PrivacyAndSecurity); - add_page(Page::SystemAndAccounts(None)); - add_page(Page::TimeAndLanguage(None)); - add_page(Page::Accessibility); - add_page(Page::Applications); - window.nav_bar_pages.activate(key); + window.insert_page(Page::Demo); + 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::InputDevices(None)); + window.insert_page(Page::Displays); + window.insert_page(Page::PowerAndBattery); + window.insert_page(Page::Sound); + window.insert_page(Page::PrintersAndScanners); + window.insert_page(Page::PrivacyAndSecurity); + window.insert_page(Page::SystemAndAccounts(None)); + window.insert_page(Page::TimeAndLanguage(None)); + window.insert_page(Page::Accessibility); + window.insert_page(Page::Applications); + window.activate_page(key); (window, Command::none()) } @@ -364,7 +371,7 @@ impl Application for Window { let mut ret = Command::none(); match message { Message::NavBar(key) => { - if let Some(page) = self.nav_bar_pages.data(key).cloned() { + if let Some(page) = self.nav_bar_pages.component(key).cloned() { self.nav_bar_pages.activate(key); self.page(page); } diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 267d64c3..db407932 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -5,7 +5,13 @@ use cosmic::{ theme::{Button as ButtonTheme, Theme}, widget::{ button, - segmented_button::{MultiSelect, SingleSelect}, + segmented_button::{ + self, + cosmic::{ + horizontal_segmented_selection, horizontal_view_switcher, + vertical_segmented_selection, vertical_view_switcher, + }, + }, settings, spin_button::{SpinButtonModel, SpinMessage}, toggler, @@ -14,13 +20,6 @@ use cosmic::{ }; use super::{Page, Window}; -use cosmic::widget::segmented_button::{ - self, - cosmic::{ - horizontal_segmented_selection, horizontal_view_switcher, vertical_segmented_selection, - vertical_view_switcher, - }, -}; pub enum DemoView { TabA, @@ -28,6 +27,7 @@ pub enum DemoView { TabC, } +#[allow(dead_code)] pub enum MultiOption { OptionA, OptionB, @@ -60,14 +60,14 @@ pub enum Output { pub struct State { pub checkbox_value: bool, - pub icon_theme: segmented_button::State, - pub multi_selection: segmented_button::State, + pub icon_theme: segmented_button::SingleSelectModel<&'static str>, + pub multi_selection: segmented_button::MultiSelectModel, pub pick_list_selected: Option<&'static str>, - pub selection: segmented_button::State, + pub selection: segmented_button::SingleSelectModel<()>, pub slider_value: f32, pub spin_button: SpinButtonModel, pub toggler_value: bool, - pub view_switcher: segmented_button::State, + pub view_switcher: segmented_button::SingleSelectModel, } impl Default for State { @@ -78,23 +78,23 @@ impl Default for State { slider_value: 50.0, spin_button: SpinButtonModel::default().min(-10).max(10), toggler_value: false, - icon_theme: segmented_button::State::builder() + icon_theme: segmented_button::Model::builder() .insert_active("Pop", "Pop") .insert("Adwaita", "Adwaita") .build(), - selection: segmented_button::State::builder() + selection: segmented_button::Model::builder() .insert_active("Choice A", ()) .insert("Choice B", ()) .insert("Choice C", ()) .build(), - multi_selection: segmented_button::State::builder() - .insert("Option A", MultiOption::OptionA) + multi_selection: segmented_button::Model::builder() + .insert_active("Option A", MultiOption::OptionA) .insert("Option B", MultiOption::OptionB) - .insert("Option C", MultiOption::OptionC) - .insert("Option D", MultiOption::OptionD) + .insert("Option C", MultiOption::OptionB) + .insert("Option D", MultiOption::OptionC) .insert("Option E", MultiOption::OptionE) .build(), - view_switcher: segmented_button::State::builder() + view_switcher: segmented_button::Model::builder() .insert_active("Controls", DemoView::TabA) .insert("Segmented Button", DemoView::TabB) .insert("Tab C", DemoView::TabC) @@ -120,7 +120,7 @@ impl State { Message::ViewSwitcher(key) => self.view_switcher.activate(key), Message::IconTheme(key) => { self.icon_theme.activate(key); - if let Some(theme) = self.icon_theme.data(key) { + if let Some(theme) = self.icon_theme.component(key) { cosmic::settings::set_default_icon_theme(*theme); } } @@ -150,7 +150,7 @@ impl State { horizontal_view_switcher(&self.view_switcher) .on_activate(Message::ViewSwitcher) .into(), - match self.view_switcher.active_data() { + match self.view_switcher.active_component() { None => panic!("no tab is active"), Some(DemoView::TabA) => settings::view_column(vec![ settings::view_section("Debug") diff --git a/src/theme/segmented_button.rs b/src/theme/segmented_button.rs index d4fb2e1a..d79a9298 100644 --- a/src/theme/segmented_button.rs +++ b/src/theme/segmented_button.rs @@ -4,6 +4,7 @@ use crate::theme::Theme; use crate::widget::segmented_button; use iced_core::{Background, BorderRadius}; +use palette::{rgb::Rgb, Alpha}; #[derive(Clone, Copy, Default)] pub enum SegmentedButton { @@ -24,27 +25,9 @@ impl segmented_button::StyleSheet for Theme { match style { SegmentedButton::ViewSwitcher => { let cosmic = self.cosmic(); + let active = horizontal::view_switcher_active(cosmic); segmented_button::Appearance { border_radius: BorderRadius::from(0.0), - active: segmented_button::ButtonStatusAppearance { - background: Some(Background::Color(cosmic.primary.component.base.into())), - first: segmented_button::ButtonAppearance { - 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 { - 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 { - border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), - border_bottom: Some((4.0, cosmic.accent.base.into())), - ..Default::default() - }, - text_color: cosmic.accent.base.into(), - }, inactive: segmented_button::ButtonStatusAppearance { background: None, first: segmented_button::ButtonAppearance { @@ -64,50 +47,17 @@ impl segmented_button::StyleSheet for Theme { }, text_color: cosmic.primary.on.into(), }, - hover: segmented_button::ButtonStatusAppearance { - background: Some(Background::Color(cosmic.primary.component.hover.into())), - first: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), - border_bottom: Some((1.0, cosmic.accent.base.into())), - ..Default::default() - }, - middle: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), - border_bottom: Some((1.0, cosmic.accent.base.into())), - ..Default::default() - }, - last: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), - border_bottom: Some((1.0, cosmic.accent.base.into())), - ..Default::default() - }, - text_color: cosmic.accent.base.into(), - }, + hover: hover(cosmic, &active), + focus: focus(cosmic, &active), + active, ..Default::default() } } SegmentedButton::Selection => { let cosmic = self.cosmic(); + let active = horizontal::selection_active(cosmic); segmented_button::Appearance { border_radius: BorderRadius::from(0.0), - active: segmented_button::ButtonStatusAppearance { - background: Some(Background::Color( - cosmic.secondary.component.divider.into(), - )), - first: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from([24.0, 0.0, 0.0, 24.0]), - ..Default::default() - }, - middle: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from(0.0), - ..Default::default() - }, - last: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from([0.0, 24.0, 24.0, 0.0]), - ..Default::default() - }, - text_color: cosmic.accent.base.into(), - }, inactive: segmented_button::ButtonStatusAppearance { background: Some(Background::Color(cosmic.secondary.component.base.into())), first: segmented_button::ButtonAppearance { @@ -124,22 +74,9 @@ impl segmented_button::StyleSheet for Theme { }, text_color: cosmic.primary.on.into(), }, - hover: segmented_button::ButtonStatusAppearance { - background: Some(Background::Color(cosmic.primary.component.hover.into())), - first: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from([24.0, 0.0, 0.0, 24.0]), - ..Default::default() - }, - middle: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from(0.0), - ..Default::default() - }, - last: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from([0.0, 24.0, 24.0, 0.0]), - ..Default::default() - }, - text_color: cosmic.accent.base.into(), - }, + hover: hover(cosmic, &active), + focus: focus(cosmic, &active), + active, ..Default::default() } } @@ -152,83 +89,25 @@ impl segmented_button::StyleSheet for Theme { match style { SegmentedButton::ViewSwitcher => { let cosmic = self.cosmic(); + let active = vertical::view_switcher_active(cosmic); segmented_button::Appearance { border_radius: BorderRadius::from(0.0), - active: segmented_button::ButtonStatusAppearance { - background: Some(Background::Color( - cosmic.secondary.component.divider.into(), - )), - first: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from(24.0), - ..Default::default() - }, - middle: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from(24.0), - ..Default::default() - }, - last: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from(24.0), - ..Default::default() - }, - text_color: cosmic.accent.base.into(), - }, inactive: segmented_button::ButtonStatusAppearance { background: None, - first: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from(24.0), - ..Default::default() - }, - middle: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from(24.0), - ..Default::default() - }, - last: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from(24.0), - ..Default::default() - }, text_color: cosmic.primary.on.into(), + ..active }, - hover: segmented_button::ButtonStatusAppearance { - background: Some(Background::Color(cosmic.primary.component.hover.into())), - first: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from(24.0), - ..Default::default() - }, - middle: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from(24.0), - ..Default::default() - }, - last: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from(24.0), - ..Default::default() - }, - text_color: cosmic.accent.base.into(), - }, + hover: hover(cosmic, &active), + focus: focus(cosmic, &active), + active, ..Default::default() } } SegmentedButton::Selection => { let cosmic = self.cosmic(); + let active = vertical::selection_active(cosmic); segmented_button::Appearance { border_radius: BorderRadius::from(0.0), - active: segmented_button::ButtonStatusAppearance { - background: Some(Background::Color( - cosmic.secondary.component.divider.into(), - )), - first: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from([24.0, 24.0, 0.0, 0.0]), - ..Default::default() - }, - middle: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from(0.0), - ..Default::default() - }, - last: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from([0.0, 0.0, 24.0, 24.0]), - ..Default::default() - }, - text_color: cosmic.accent.base.into(), - }, inactive: segmented_button::ButtonStatusAppearance { background: Some(Background::Color(cosmic.secondary.component.base.into())), first: segmented_button::ButtonAppearance { @@ -245,22 +124,9 @@ impl segmented_button::StyleSheet for Theme { }, text_color: cosmic.primary.on.into(), }, - hover: segmented_button::ButtonStatusAppearance { - background: Some(Background::Color(cosmic.primary.component.hover.into())), - first: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from([24.0, 24.0, 0.0, 0.0]), - ..Default::default() - }, - middle: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from(0.0), - ..Default::default() - }, - last: segmented_button::ButtonAppearance { - border_radius: BorderRadius::from([0.0, 0.0, 24.0, 24.0]), - ..Default::default() - }, - text_color: cosmic.accent.base.into(), - }, + hover: hover(cosmic, &active), + focus: focus(cosmic, &active), + active, ..Default::default() } } @@ -268,3 +134,124 @@ impl segmented_button::StyleSheet for Theme { } } } + +mod horizontal { + use crate::widget::segmented_button; + use iced_core::{Background, BorderRadius}; + use palette::{rgb::Rgb, Alpha}; + + pub fn selection_active( + cosmic: &cosmic_theme::Theme>, + ) -> segmented_button::ButtonStatusAppearance { + segmented_button::ButtonStatusAppearance { + background: Some(Background::Color(cosmic.secondary.component.divider.into())), + first: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([24.0, 0.0, 0.0, 24.0]), + ..Default::default() + }, + middle: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(0.0), + ..Default::default() + }, + last: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([0.0, 24.0, 24.0, 0.0]), + ..Default::default() + }, + text_color: cosmic.accent.base.into(), + } + } + + pub fn view_switcher_active( + cosmic: &cosmic_theme::Theme>, + ) -> segmented_button::ButtonStatusAppearance { + segmented_button::ButtonStatusAppearance { + background: Some(Background::Color(cosmic.primary.component.base.into())), + first: segmented_button::ButtonAppearance { + 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 { + 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 { + border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]), + border_bottom: Some((4.0, cosmic.accent.base.into())), + ..Default::default() + }, + text_color: cosmic.accent.base.into(), + } + } +} + +pub fn focus( + cosmic: &cosmic_theme::Theme>, + default: &segmented_button::ButtonStatusAppearance, +) -> segmented_button::ButtonStatusAppearance { + segmented_button::ButtonStatusAppearance { + background: Some(Background::Color(cosmic.primary.component.focus.into())), + text_color: cosmic.primary.base.into(), + ..*default + } +} + +pub fn hover( + cosmic: &cosmic_theme::Theme>, + default: &segmented_button::ButtonStatusAppearance, +) -> segmented_button::ButtonStatusAppearance { + segmented_button::ButtonStatusAppearance { + background: Some(Background::Color(cosmic.primary.component.hover.into())), + text_color: cosmic.accent.base.into(), + ..*default + } +} + +mod vertical { + use crate::widget::segmented_button; + use iced_core::{Background, BorderRadius}; + use palette::{rgb::Rgb, Alpha}; + + pub fn selection_active( + cosmic: &cosmic_theme::Theme>, + ) -> segmented_button::ButtonStatusAppearance { + segmented_button::ButtonStatusAppearance { + background: Some(Background::Color(cosmic.secondary.component.divider.into())), + first: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([24.0, 24.0, 0.0, 0.0]), + ..Default::default() + }, + middle: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(0.0), + ..Default::default() + }, + last: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from([0.0, 0.0, 24.0, 24.0]), + ..Default::default() + }, + text_color: cosmic.accent.base.into(), + } + } + + pub fn view_switcher_active( + cosmic: &cosmic_theme::Theme>, + ) -> segmented_button::ButtonStatusAppearance { + segmented_button::ButtonStatusAppearance { + background: Some(Background::Color(cosmic.secondary.component.divider.into())), + first: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(24.0), + ..Default::default() + }, + middle: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(24.0), + ..Default::default() + }, + last: segmented_button::ButtonAppearance { + border_radius: BorderRadius::from(24.0), + ..Default::default() + }, + text_color: cosmic.accent.base.into(), + } + } +} diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index 7b31cbee..6809ed74 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -10,23 +10,24 @@ use iced_core::Color; use crate::{theme, Theme}; -use super::segmented_button::{self, cosmic::vertical_view_switcher, SingleSelect}; +use super::segmented_button::{self, vertical_segmented_button}; /// A container holding a vertical view switcher with the n style -pub fn nav_bar( - state: &segmented_button::State, +pub fn nav_bar( + model: &segmented_button::SingleSelectModel, on_activate: impl Fn(segmented_button::Key) -> Message + 'static, ) -> iced::widget::Container where Message: Clone + 'static, { - vertical_view_switcher(state) - .on_activate(on_activate) + vertical_segmented_button(model) .button_height(32) .button_padding([16, 10, 16, 10]) .button_spacing(8) .icon_size(16) + .on_activate(on_activate) .spacing(8) + .style(crate::theme::SegmentedButton::ViewSwitcher) .apply(scrollable) .apply(container) .height(Length::Fill) diff --git a/src/widget/segmented_button/cosmic.rs b/src/widget/segmented_button/cosmic.rs index 0f18ec65..55ff232f 100644 --- a/src/widget/segmented_button/cosmic.rs +++ b/src/widget/segmented_button/cosmic.rs @@ -2,20 +2,21 @@ // SPDX-License-Identifier: MPL-2.0 use super::{ - state::Selectable, HorizontalSegmentedButton, SegmentedButton, State, VerticalSegmentedButton, + 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 [`State`] that is maintained the application. +/// The data for the widget comes from a model supplied by the application. #[must_use] -pub fn horizontal_view_switcher( - state: &State, -) -> HorizontalSegmentedButton +pub fn horizontal_view_switcher( + model: &Model, +) -> HorizontalSegmentedButton where - Selection: Selectable, + SelectionMode: Selectable, { - SegmentedButton::new(&state.inner) + horizontal_segmented_button(model) .button_padding([16, 0, 16, 0]) .button_height(48) .style(crate::theme::SegmentedButton::ViewSwitcher) @@ -24,15 +25,15 @@ where /// Appears as a selection of choices for choosing between. /// -/// The data for the widget comes from a [`State`] that is maintained the application. +/// The data for the widget comes from a model that is maintained the application. #[must_use] -pub fn horizontal_segmented_selection( - state: &State, -) -> HorizontalSegmentedButton +pub fn horizontal_segmented_selection( + model: &Model, +) -> HorizontalSegmentedButton where - Selection: Selectable, + SelectionMode: Selectable, { - SegmentedButton::new(&state.inner) + SegmentedButton::new(model) .button_padding([16, 0, 16, 0]) .button_height(32) .style(crate::theme::SegmentedButton::Selection) @@ -41,15 +42,15 @@ where /// Appears as a selection of choices for choosing between. /// -/// The data for the widget comes from a [`State`] that is maintained the application. +/// The data for the widget comes from a model that is maintained the application. #[must_use] -pub fn vertical_segmented_selection( - state: &State, -) -> VerticalSegmentedButton +pub fn vertical_segmented_selection( + model: &Model, +) -> VerticalSegmentedButton where - Selection: Selectable, + SelectionMode: Selectable, { - SegmentedButton::new(&state.inner) + SegmentedButton::new(model) .button_padding([16, 0, 16, 0]) .button_height(32) .style(crate::theme::SegmentedButton::Selection) @@ -58,15 +59,15 @@ where /// Appears as a collection of tabs for developing a tabbed interface. /// -/// The data for the widget comes from a [`State`] that is maintained the application. +/// The data for the widget comes from a model that is maintained the application. #[must_use] -pub fn vertical_view_switcher( - state: &State, -) -> VerticalSegmentedButton +pub fn vertical_view_switcher( + model: &Model, +) -> VerticalSegmentedButton where - Selection: Selectable, + SelectionMode: Selectable, { - SegmentedButton::new(&state.inner) + SegmentedButton::new(model) .button_padding([16, 0, 16, 0]) .button_height(48) .style(crate::theme::SegmentedButton::ViewSwitcher) diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index bcbc66ad..fca097ae 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -1,45 +1,48 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -use super::state::{Selectable, State}; +//! Implementation details for the horizontal layout of a segmented button. + +use super::model::Model; +use super::selection_modes::Selectable; use super::style::StyleSheet; use super::widget::{SegmentedButton, SegmentedVariant}; use iced::{Length, Rectangle, Size}; use iced_native::layout; +/// Horizontal [`SegmentedButton`]. +pub type HorizontalSegmentedButton<'a, SelectionMode, Message, Renderer> = + SegmentedButton<'a, Horizontal, SelectionMode, Message, Renderer>; + /// A type marker defining the horizontal variant of a [`SegmentedButton`]. pub struct Horizontal; -/// Horizontal [`SegmentedButton`]. -pub type HorizontalSegmentedButton<'a, Selection, Message, Renderer> = - SegmentedButton<'a, Horizontal, Selection, Message, Renderer>; - -/// Horizontal implementation of the [`SegmentedButton`]. +/// Row implementation of the [`SegmentedButton`]. #[must_use] -pub fn horizontal_segmented_button( - state: &State, -) -> SegmentedButton +pub fn horizontal_segmented_button( + model: &Model, +) -> SegmentedButton where Renderer: iced_native::Renderer + iced_native::text::Renderer + iced_native::image::Renderer + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, - Selection: Selectable, + SelectionMode: Selectable, { - SegmentedButton::new(&state.inner) + SegmentedButton::new(model) } -impl<'a, Selection, Message, Renderer> SegmentedVariant - for SegmentedButton<'a, Horizontal, Selection, Message, Renderer> +impl<'a, SelectionMode, Message, Renderer> SegmentedVariant + for SegmentedButton<'a, Horizontal, 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, + SelectionMode: Selectable, { type Renderer = Renderer; @@ -52,7 +55,7 @@ where #[allow(clippy::cast_precision_loss)] fn variant_button_bounds(&self, mut bounds: Rectangle, nth: usize) -> Rectangle { - let num = self.state.buttons.len(); + let num = self.model.items.len(); if num != 0 { let spacing = f32::from(self.spacing); bounds.width = (bounds.width - (num as f32 * spacing) + spacing) / num as f32; @@ -74,7 +77,7 @@ where let (mut width, height) = self.max_button_dimensions(renderer, text_size, limits.max()); - let num = self.state.buttons.len(); + let num = self.model.items.len(); let spacing = f32::from(self.spacing); if num != 0 { diff --git a/src/widget/segmented_button/item.rs b/src/widget/segmented_button/item.rs new file mode 100644 index 00000000..1573c827 --- /dev/null +++ b/src/widget/segmented_button/item.rs @@ -0,0 +1,57 @@ +// 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 1d7d77ce..de331af6 100644 --- a/src/widget/segmented_button/mod.rs +++ b/src/widget/segmented_button/mod.rs @@ -1,7 +1,7 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -//! A widget providing a conjoined set of linear buttons for choosing between. +//! A widget providing a conjoined set of linear buttons that function in conjunction with each other. //! //! ## Example //! @@ -17,27 +17,30 @@ //! } //! //! struct App { -//! ... -//! state: segmented_button::State(), -//! ... +//! state: segmented_button::SingleSelectModel(), //! } //! ``` //! //! Then add choices to the state, while activating the first. //! //! ```ignore -//! let first_key = application.state.insert("Choice A", 0); -//! application.state.insert("Choice B", 1); -//! application.state.insert("Choice C", 2); -//! application.state.activate(first_key); +//! application.model = SingleSelectModel::builder() +//! .insert_activate("Choice A", 0) +//! .insert("Choice B", 1) +//! .insert("Choice C", 2) +//! .build(); //! ``` //! //! Then use it in the view method to create segmented button widgets. //! //! ```ignore -//! let widget = horizontal_segmentend_button(&application.state) -//! .style(theme::SegmentedButton::Selection) -//! .height(Length::Units(32)) +//! let widget = horizontal_segmented_button(&application.model) +//! .style(theme::SegmentedButton::ViewSeitcher) +//! .button_height(32) +//! .button_padding([16, 10, 16, 10]) +//! .button_spacing(8) +//! .icon_size(16) +//! .spacing(8) //! .on_activate(AppMessage::Selected); //! ``` @@ -45,16 +48,17 @@ pub mod cosmic; mod horizontal; - -mod state; +mod item; +mod model; +mod selection_modes; mod style; mod vertical; mod widget; -pub use self::horizontal::{horizontal_segmented_button, Horizontal, HorizontalSegmentedButton}; -pub use self::state::{ - Content, Key, MultiSelect, SecondaryState, Selectable, SharedWidgetState, SingleSelect, State, -}; +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, Vertical, VerticalSegmentedButton}; -pub use self::widget::{SegmentedButton, SegmentedVariant}; +pub use self::vertical::{vertical_segmented_button, VerticalSegmentedButton}; +pub use self::widget::{focus, Id, SegmentedButton, SegmentedVariant}; diff --git a/src/widget/segmented_button/model.rs b/src/widget/segmented_button/model.rs new file mode 100644 index 00000000..c4d7caa5 --- /dev/null +++ b/src/widget/segmented_button/model.rs @@ -0,0 +1,202 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +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, + + /// Manages selections + pub(super) selection: SelectionMode, +} + +impl Model { + pub fn activate(&mut self, key: Key) { + self.widget.selection.active = key; + } + + #[must_use] + pub fn active_component(&self) -> Option<&Component> { + self.component(self.active()) + } + + #[must_use] + pub fn active_component_mut(&mut self) -> Option<&mut Component> { + self.component_mut(self.active()) + } + + pub fn deactivate(&mut self) { + self.widget.selection.active = Key::default(); + } + + /// The ID of the active button. + #[must_use] + pub fn active(&self) -> Key { + self.widget.selection.active + } +} + +impl Model { + pub fn activate(&mut self, key: Key) { + if !self.widget.selection.active.insert(key) { + self.widget.selection.active.remove(&key); + } + } + + 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, +{ + #[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) + } + + /// Enables or disables a button + #[must_use] + pub fn content(&self, key: Key) -> Option<&SegmentedItem> { + self.widget.items.get(key) + } + + /// Enables or disables a button + #[must_use] + pub fn content_mut(&mut self, key: Key) -> Option<&mut SegmentedItem> { + self.widget.items.get_mut(key) + } + + pub fn component(&self, key: Key) -> Option<&Component> { + self.app.0.get(key) + } + + pub fn component_mut(&mut self, key: Key) -> Option<&mut Component> { + self.app.0.get_mut(key) + } + + /// Insert a new button. + pub fn insert(&mut self, content: impl Into, component: Component) -> Key { + let key = self.widget.items.insert(content.into()); + self.app.0.insert(key, component); + key + } + + /// Inserts and activates a button. + pub fn insert_active( + &mut self, + content: impl Into, + component: Component, + ) -> Key { + let key = self.insert(content, component); + self.widget.selection.activate(key); + key + } + + #[must_use] + pub fn is_active(&self, key: Key) -> bool { + self.widget.selection.is_active(key) + } + + /// Removes a button. + pub fn remove(&mut self, key: Key) { + self.widget.items.remove(key); + self.widget.selection.deactivate(key); + } +} + +pub struct ModelBuilder(Model); + +impl ModelBuilder { + #[must_use] + pub fn insert(mut self, content: impl Into, component: Component) -> Self { + self.0.insert(content, component); + self + } + + #[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/selection_modes.rs b/src/widget/segmented_button/selection_modes.rs new file mode 100644 index 00000000..91a31b3f --- /dev/null +++ b/src/widget/segmented_button/selection_modes.rs @@ -0,0 +1,59 @@ +// 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/state.rs b/src/widget/segmented_button/state.rs deleted file mode 100644 index d7ebc6b3..00000000 --- a/src/widget/segmented_button/state.rs +++ /dev/null @@ -1,292 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -use derive_setters::Setters; -use slotmap::{SecondaryMap, SlotMap}; -use std::{borrow::Cow, collections::HashSet}; - -use crate::widget::IconSource; - -slotmap::new_key_type! { - /// An ID for a segmented button - pub struct Key; -} - -/// Contains all state for interacting with a segmented button. -pub struct State { - /// State that is shared with widget drawing. - pub inner: SharedWidgetState, - - /// State unique to the application. - pub data: SecondaryState, -} - -impl Default for State { - fn default() -> Self { - Self { - inner: SharedWidgetState::default(), - data: SecondaryState::default(), - } - } -} - -/// State which is most useful to the widget. -#[derive(Default)] -pub struct SharedWidgetState { - /// The content used for drawing segmented buttons. - pub buttons: SlotMap, - - /// Manages selections - pub selection: Variant, -} - -impl State { - pub fn activate(&mut self, key: Key) { - self.inner.selection.activate(key); - } - - pub fn deactivate(&mut self, key: Key) { - self.inner.selection.deactivate(key); - } - - #[must_use] - pub fn is_active(&self, key: Key) -> bool { - self.inner.selection.is_active(key) - } - - /// The ID of the active button. - #[must_use] - pub fn active(&self) -> Key { - self.inner.selection.active - } - - /// Get the application data for the active button. - #[must_use] - pub fn active_data(&self) -> Option<&Data> { - self.data.get(self.inner.selection.active) - } - - /// Mutable application data for the active button. - #[must_use] - pub fn active_data_mut(&mut self) -> Option<&mut Data> { - self.data.get_mut(self.inner.selection.active) - } -} - -impl State { - pub fn activate(&mut self, key: Key) { - if self.inner.selection.is_active(key) { - self.inner.selection.deactivate(key); - } else { - self.inner.selection.activate(key); - } - } - - pub fn deactivate(&mut self, key: Key) { - self.inner.selection.deactivate(key); - } - - #[must_use] - pub fn is_active(&self, key: Key) -> bool { - self.inner.selection.is_active(key) - } - - /// The IDs of the active buttons. - pub fn active(&self) -> impl Iterator + '_ { - self.inner.selection.active.iter().copied() - } - - /// Get the application data for the active buttons. - pub fn active_data(&self) -> impl Iterator { - self.inner.buttons.keys().filter_map(|key| { - if self.inner.selection.is_active(key) { - self.data.get(key).map(|data| (key, data)) - } else { - None - } - }) - } -} - -/// State which is most useful to the application. -pub type SecondaryState = SecondaryMap; - -impl State -where - Selection: Selectable, -{ - #[must_use] - pub fn builder() -> Builder { - Builder(Self::default()) - } - - /// Convenience method for batching multiple operations - #[must_use] - pub fn batch(&mut self) -> Batch { - Batch(self) - } - - /// Enables or disables a button - #[must_use] - pub fn content(&self, key: Key) -> Option<&Content> { - self.inner.buttons.get(key) - } - - /// Enables or disables a button - #[must_use] - pub fn content_mut(&mut self, key: Key) -> Option<&mut Content> { - self.inner.buttons.get_mut(key) - } - - /// Get the application data for a button. - #[must_use] - pub fn data(&self, key: Key) -> Option<&Data> { - self.data.get(key) - } - - /// Get mutable application data for a button. - #[must_use] - pub fn data_mut(&mut self, key: Key) -> Option<&mut Data> { - self.data.get_mut(key) - } - - /// Insert a new button. - pub fn insert(&mut self, content: impl Into, data: Data) -> Key { - let key = self.inner.buttons.insert(content.into()); - self.data.insert(key, data); - key - } - - /// Inserts and activates a button. - pub fn insert_active(&mut self, content: impl Into, data: Data) -> Key { - let key = self.insert(content, data); - self.inner.selection.activate(key); - key - } - - /// Removes a button. - pub fn remove(&mut self, key: Key) -> Option { - self.inner.buttons.remove(key); - self.inner.selection.deactivate(key); - self.data.remove(key) - } -} - -pub trait Selectable: Default { - fn activate(&mut self, key: Key); - - fn deactivate(&mut self, key: Key); - - fn is_active(&self, key: Key) -> bool; -} - -#[derive(Default)] -pub struct SingleSelect { - pub active: Key, -} - -impl Selectable for SingleSelect { - fn activate(&mut self, key: Key) { - self.active = key; - } - - fn deactivate(&mut self, _key: Key) { - self.active = Key::default(); - } - - fn is_active(&self, key: Key) -> bool { - self.active == key - } -} - -#[derive(Default)] -pub struct MultiSelect { - pub active: HashSet, -} - -impl Selectable for MultiSelect { - fn activate(&mut self, key: Key) { - self.active.insert(key); - } - - fn deactivate(&mut self, key: Key) { - self.active.remove(&key); - } - - fn is_active(&self, key: Key) -> bool { - self.active.contains(&key) - } -} - -pub struct Builder(State); - -impl Builder { - pub fn insert(mut self, content: impl Into, data: Data) -> Self { - self.0.insert(content, data); - self - } - - pub fn insert_active(mut self, content: impl Into, data: Data) -> Self { - self.0.insert_active(content, data); - self - } - - pub fn build(self) -> State { - self.0 - } -} - -/// Convenience type for batching multiple operations -pub struct Batch<'a, Selection, Data>(&'a mut State); - -impl<'a, Selection: Selectable, Data> Batch<'a, Selection, Data> { - /// Insert a new button. - pub fn insert(self, content: impl Into, data: Data) -> Self { - self.0.insert(content, data); - self - } - - /// Inserts and activates a button. - pub fn insert_active(self, content: impl Into, data: Data) -> Self { - self.0.insert_active(content, data); - self - } - - /// Removes a button. - pub fn remove(&mut self, key: Key) { - self.0.remove(key); - } -} - -/// Data to be drawn in a segmented button. -#[derive(Default, Setters)] -pub struct Content { - #[setters(strip_option, into)] - /// The label to display in this button. - pub text: Option>, - - #[setters(strip_option, into)] - /// An optionally-displayed icon beside the label. - pub icon: Option>, - - /// Whether the button is clickable or not. - pub enabled: bool, -} - -impl From for Content { - fn from(text: String) -> Self { - Self::from(Cow::Owned(text)) - } -} - -impl From<&'static str> for Content { - fn from(text: &'static str) -> Self { - Self::from(Cow::Borrowed(text)) - } -} - -impl From> for Content { - fn from(text: Cow<'static, str>) -> Self { - Content::default().text(text) - } -} diff --git a/src/widget/segmented_button/style.rs b/src/widget/segmented_button/style.rs index 35316b5d..2e386af2 100644 --- a/src/widget/segmented_button/style.rs +++ b/src/widget/segmented_button/style.rs @@ -15,6 +15,7 @@ pub struct Appearance { pub active: ButtonStatusAppearance, pub inactive: ButtonStatusAppearance, pub hover: ButtonStatusAppearance, + pub focus: ButtonStatusAppearance, } /// The appearance of a button in the segmented button diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index ace21d8b..885fd4fe 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -1,7 +1,10 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -use super::state::{Selectable, State}; +//! Implementation details for the vertical layout of a segmented button. + +use super::model::Model; +use super::selection_modes::Selectable; use super::style::StyleSheet; use super::widget::{SegmentedButton, SegmentedVariant}; @@ -12,34 +15,34 @@ use iced_native::layout; pub struct Vertical; /// Vertical [`SegmentedButton`]. -pub type VerticalSegmentedButton<'a, Selection, Message, Renderer> = - SegmentedButton<'a, Vertical, Selection, Message, Renderer>; +pub type VerticalSegmentedButton<'a, SelectionMode, Message, Renderer> = + SegmentedButton<'a, Vertical, SelectionMode, Message, Renderer>; /// Vertical implementation of the [`SegmentedButton`]. #[must_use] -pub fn vertical_segmented_button( - state: &State, -) -> SegmentedButton +pub fn vertical_segmented_button( + model: &Model, +) -> SegmentedButton where Renderer: iced_native::Renderer + iced_native::text::Renderer + iced_native::image::Renderer + iced_native::svg::Renderer, Renderer::Theme: StyleSheet, - Selection: Selectable, + SelectionMode: Selectable, { - SegmentedButton::new(&state.inner) + SegmentedButton::new(model) } -impl<'a, Selection, Message, Renderer> SegmentedVariant - for SegmentedButton<'a, Vertical, Selection, Message, Renderer> +impl<'a, SelectionMode, Message, Renderer> SegmentedVariant + for SegmentedButton<'a, Vertical, 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, + SelectionMode: Selectable, { type Renderer = Renderer; @@ -52,7 +55,7 @@ where #[allow(clippy::cast_precision_loss)] fn variant_button_bounds(&self, mut bounds: Rectangle, nth: usize) -> Rectangle { - let num = self.state.buttons.len(); + let num = self.model.items.len(); if num != 0 { let spacing = f32::from(self.spacing); bounds.height = (bounds.height - (num as f32 * spacing) + spacing) / num as f32; @@ -74,7 +77,7 @@ where let (width, mut height) = self.max_button_dimensions(renderer, text_size, limits.max()); - let num = self.state.buttons.len(); + let num = self.model.items.len(); let spacing = f32::from(self.spacing); if num != 0 { diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 27971fc7..97469666 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -3,18 +3,48 @@ use std::marker::PhantomData; -use super::state::{Key, Selectable, SharedWidgetState}; +use super::model::{Key, Model, WidgetModel}; +use super::selection_modes::Selectable; use super::style::StyleSheet; use derive_setters::Setters; use iced::{ - alignment, event, mouse, touch, Background, Color, Element, Event, Length, Point, Rectangle, - Size, + alignment, event, keyboard, mouse, touch, Background, Color, Command, Element, Event, Length, + Point, Rectangle, Size, }; use iced_core::BorderRadius; -use iced_native::widget::tree; +use iced_native::widget::{self, operation, tree, Operation}; use iced_native::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget}; +/// State that is maintained by each individual widget. +#[derive(Default)] +struct LocalState { + /// The first focusable key. + first: Key, + /// If the widget is focused or not. + focused: bool, + /// The key inside the widget that is currently focused. + focused_key: Key, + /// The ID of the button that is being hovered. Defaults to null. + hovered: Key, +} + +impl operation::Focusable for LocalState { + fn is_focused(&self) -> bool { + self.focused + } + + fn focus(&mut self) { + self.focused = true; + self.focused_key = self.first; + } + + fn unfocus(&mut self) { + self.focused = false; + self.focused_key = Key::default(); + } +} + /// Isolates variant-specific behaviors from [`SegmentedButton`]. pub trait SegmentedVariant { type Renderer: iced_native::Renderer; @@ -44,9 +74,10 @@ where Renderer::Theme: StyleSheet, Selection: Selectable, { - /// Contains application state also used for drawing. + /// The model borrowed from the application create this widget. #[setters(skip)] - pub(super) state: &'a SharedWidgetState, + pub(super) model: &'a WidgetModel, + pub(super) id: Option, /// Padding around a button. pub(super) button_padding: [u16; 4], /// Desired height of a button. @@ -65,7 +96,7 @@ where pub(super) width: Length, /// Desired height of the widget. pub(super) height: Length, - /// Desired spacing between buttons. + /// Desired spacing between items. pub(super) spacing: u16, /// Style to draw the widget in. #[setters(into)] @@ -90,9 +121,10 @@ where Selection: Selectable, { #[must_use] - pub fn new(state: &'a SharedWidgetState) -> Self { + pub fn new(model: &'a Model) -> Self { Self { - state, + model: &model.widget, + id: None, button_padding: [4, 4, 4, 4], button_height: 32, button_spacing: 4, @@ -109,6 +141,46 @@ where } } + fn focus_previous(&mut self, state: &mut LocalState) -> event::Status { + let mut previous_key: Option = None; + + for key in self.model.items.keys() { + if key == state.focused_key { + return match previous_key { + Some(next_focus) => { + state.focused_key = next_focus; + event::Status::Captured + } + None => break, + }; + } + + previous_key = Some(key); + } + + state.focused_key = Key::default(); + event::Status::Ignored + } + + fn focus_next(&mut self, state: &mut LocalState) -> event::Status { + let mut keys = self.model.items.keys(); + + while let Some(key) = keys.next() { + if key == state.focused_key { + return match keys.next() { + Some(next_focus) => { + state.focused_key = next_focus; + event::Status::Captured + } + None => break, + }; + } + } + + state.focused_key = Key::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 { @@ -125,7 +197,7 @@ where let mut width = 0.0f32; let mut height = 0.0f32; - for (_, content) in self.state.buttons.iter() { + for content in self.model.items.values() { let mut button_width = 0.0f32; let mut button_height = 0.0f32; @@ -169,11 +241,14 @@ where Message: 'static + Clone, { fn tag(&self) -> tree::Tag { - tree::Tag::of::() + tree::Tag::of::() } fn state(&self) -> tree::State { - tree::State::new(UniqueWidgetState::default()) + tree::State::new(LocalState { + first: self.model.items.keys().next().unwrap_or_default(), + ..LocalState::default() + }) } fn width(&self) -> Length { @@ -199,32 +274,72 @@ where shell: &mut Shell<'_, Message>, ) -> event::Status { let bounds = layout.bounds(); - let state = tree.state.downcast_mut::(); + let state = tree.state.downcast_mut::(); if bounds.contains(cursor_position) { - for (nth, (key, _)) in self.state.buttons.iter().enumerate() { + for (nth, (key, content)) in self.model.items.iter().enumerate() { let bounds = self.variant_button_bounds(bounds, nth); if bounds.contains(cursor_position) { - // Record that the mouse is hovering over this button. - state.hovered = key; + if content.enabled { + if let Some(on_activate) = self.on_activate.as_ref() { + if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) = event + { + // Record that the mouse is hovering over this button. + state.hovered = key; - if let Some(on_activate) = self.on_activate.as_ref() { - if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerLifted { .. }) = event - { - shell.publish(on_activate(key)); - return event::Status::Captured; + shell.publish(on_activate(key)); + return event::Status::Captured; + } } } + + break; } } } else { state.hovered = Key::default(); } + if state.focused { + if let Event::Keyboard(keyboard::Event::KeyPressed { + key_code: keyboard::KeyCode::Tab, + modifiers, + .. + }) = event + { + return if modifiers.shift() { + self.focus_previous(state) + } else { + self.focus_next(state) + }; + } + + if let Some(on_activate) = self.on_activate.as_ref() { + if let Event::Keyboard(keyboard::Event::KeyReleased { + key_code: keyboard::KeyCode::Enter, + .. + }) = event + { + shell.publish(on_activate(state.focused_key)); + return event::Status::Captured; + } + } + } + event::Status::Ignored } + fn operate( + &self, + tree: &mut Tree, + _layout: Layout<'_>, + operation: &mut dyn Operation, + ) { + let state = tree.state.downcast_mut::(); + operation.focusable(state, self.id.as_ref().map(|id| &id.0)); + } + fn mouse_interaction( &self, _tree: &Tree, @@ -234,14 +349,23 @@ where _renderer: &Renderer, ) -> iced_native::mouse::Interaction { let bounds = layout.bounds(); - if (0..self.state.buttons.len()).any(|nth| { - self.variant_button_bounds(bounds, nth) - .contains(cursor_position) - }) { - iced_native::mouse::Interaction::Pointer - } else { - iced_native::mouse::Interaction::Idle + + if bounds.contains(cursor_position) { + for (nth, content) in self.model.items.values().enumerate() { + if self + .variant_button_bounds(bounds, nth) + .contains(cursor_position) + { + return if content.enabled { + iced_native::mouse::Interaction::Pointer + } else { + iced_native::mouse::Interaction::Idle + }; + } + } } + + iced_native::mouse::Interaction::Idle } #[allow(clippy::too_many_lines)] @@ -255,10 +379,10 @@ where _cursor_position: iced::Point, _viewport: &iced::Rectangle, ) { - let state = tree.state.downcast_ref::(); + let state = tree.state.downcast_ref::(); let appearance = Self::variant_appearance(theme, &self.style); let bounds = layout.bounds(); - let button_amount = self.state.buttons.len(); + let button_amount = self.model.items.len(); // Draw the background, if a background was defined. if let Some(background) = appearance.background { @@ -273,11 +397,13 @@ where ); } - // Draw each of the buttons in the widget. - for (nth, (key, content)) in self.state.buttons.iter().enumerate() { + // Draw each of the items in the widget. + for (nth, (key, content)) in self.model.items.iter().enumerate() { let mut bounds = self.variant_button_bounds(bounds, nth); - let (status_appearance, font) = if self.state.selection.is_active(key) { + let (status_appearance, font) = if state.focused_key == key { + (appearance.focus, &self.font_active) + } else if self.model.selection.is_active(key) { (appearance.active, &self.font_active) } else if state.hovered == key { (appearance.hover, &self.font_hovered) @@ -413,7 +539,7 @@ where Message: 'static + Clone, { fn from(mut widget: SegmentedButton<'a, Variant, Selection, Message, Renderer>) -> Self { - if widget.state.buttons.is_empty() { + if widget.model.items.is_empty() { widget.spacing = 0; } @@ -421,9 +547,33 @@ where } } -/// State that is maintained by each individual widget. -#[derive(Default)] -struct UniqueWidgetState { - /// The ID of the button that is being hovered. Defaults to null. - hovered: Key, +/// A command that focuses a segmented item stored in a widget. +#[must_use] +pub fn focus(id: Id) -> Command { + Command::widget(operation::focusable::focus(id.0)) +} + +/// The identifier of a segmented item. +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub struct Id(widget::Id); + +impl Id { + /// Creates a custom [`Id`]. + pub fn new(id: impl Into>) -> Self { + Self(widget::Id::new(id)) + } + + /// Creates a unique [`Id`]. + /// + /// This function produces a different [`Id`] every time it is called. + #[must_use] + pub fn unique() -> Self { + Self(widget::Id::unique()) + } +} + +impl From for widget::Id { + fn from(id: Id) -> Self { + id.0 + } }