feat!(segmented-button): improved interfaces and documentation

BREAKING CHANGE: Various type and function names have changed to reflect
themselves better in documentation. Code has been reorganized into
separate modules with a better placement in libcosmic. Most of the
functions, types, and modules now have documentation and examples.

These changes no longer require the `Model` type to define the
data/component type that it stores. The component functionality is now
optional, and it's also possible to associate many components to an item
with one component per type. This has had a side effect of simplifying a
lot of the type signatures in the implementation.

Before, to insert an item into the model, you had to define a
`SegmentedItem` and a `Component` on insert, and get back an ID for that
item. Which makes it difficult to define an item that contains only an
icon or has no components. And requires an extra insert function to
activate the item on insert.

Now, there is a flexible builder-style API for configuring
newly-inserted items in the model. So the complexity for inserting and
retrieving values from the model has decreased significantly
This commit is contained in:
Michael Aaron Murphy 2023-01-17 18:49:40 +01:00 committed by Michael Murphy
parent 095e4c1acd
commit b3d550cc5e
22 changed files with 1097 additions and 675 deletions

View file

@ -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<Page>,
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<u32>),
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<Self::Message> {
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::<Page>(key).cloned() {
self.nav_bar_pages.activate(key);
self.page(page);
}

View file

@ -131,7 +131,8 @@ pub struct Window {
debug: bool,
demo: demo::State,
desktop: desktop::State,
nav_bar_pages: segmented_button::SingleSelectModel<Page>,
nav_bar: segmented_button::SingleSelectModel,
nav_id_to_page: segmented_button::SecondaryMap<Page>,
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<Page> 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<Message: 'static>(&self, page: Page) -> Element<Message> {
@ -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);

View file

@ -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<MultiOption>,
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<i32>,
pub toggler_value: bool,
pub view_switcher: segmented_button::SingleSelectModel<DemoView>,
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))

View file

@ -1,8 +1,8 @@
// Copyright 2023 System76 <info@system76.com>
// 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<Alpha<Rgb, f32>>,
) -> segmented_button::ButtonStatusAppearance {
segmented_button::ButtonStatusAppearance {
pub fn selection_active(cosmic: &cosmic_theme::Theme<Alpha<Rgb, f32>>) -> 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<Alpha<Rgb, f32>>,
) -> 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<Alpha<Rgb, f32>>,
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<Alpha<Rgb, f32>>,
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<Alpha<Rgb, f32>>,
) -> segmented_button::ButtonStatusAppearance {
segmented_button::ButtonStatusAppearance {
pub fn selection_active(cosmic: &cosmic_theme::Theme<Alpha<Rgb, f32>>) -> 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<Alpha<Rgb, f32>>,
) -> 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()
},

View file

@ -1,31 +1,36 @@
// Copyright 2022 System76 <info@system76.com>
// 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::*;

View file

@ -1,6 +1,10 @@
// Copyright 2022 System76 <info@system76.com>
// 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<Component, Message>(
model: &segmented_button::SingleSelectModel<Component>,
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<Message>(
model: &segmented_button::SingleSelectModel,
on_activate: impl Fn(segmented_button::Entity) -> Message + 'static,
) -> iced::widget::Container<Message, crate::Renderer>
where
Message: Clone + 'static,
{
vertical_segmented_button(model)
segmented_button::vertical(model)
.button_height(32)
.button_padding([16, 10, 16, 10])
.button_spacing(8)

View file

@ -1,6 +1,8 @@
// Copyright 2022 System76 <info@system76.com>
// 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;

View file

@ -1,75 +0,0 @@
// Copyright 2022 System76 <info@system76.com>
// 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<SelectionMode, Component, Message>(
model: &Model<SelectionMode, Component>,
) -> HorizontalSegmentedButton<SelectionMode, Message, crate::Renderer>
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<SelectionMode, Component, Message>(
model: &Model<SelectionMode, Component>,
) -> HorizontalSegmentedButton<SelectionMode, Message, crate::Renderer>
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<SelectionMode, Component, Message>(
model: &Model<SelectionMode, Component>,
) -> VerticalSegmentedButton<SelectionMode, Message, crate::Renderer>
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<SelectionMode, Component, Message>(
model: &Model<SelectionMode, Component>,
) -> VerticalSegmentedButton<SelectionMode, Message, crate::Renderer>
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)
}

View file

@ -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<SelectionMode, Component, Message, Renderer>(
model: &Model<SelectionMode, Component>,
pub fn horizontal<SelectionMode: Default, Message, Renderer>(
model: &Model<SelectionMode>,
) -> SegmentedButton<Horizontal, SelectionMode, Message, Renderer>
where
Renderer: iced_native::Renderer
@ -29,7 +30,7 @@ where
+ iced_native::image::Renderer
+ iced_native::svg::Renderer,
Renderer::Theme: StyleSheet,
SelectionMode: Selectable,
Model<SelectionMode>: Selectable,
{
SegmentedButton::new(model)
}
@ -42,7 +43,8 @@ where
+ iced_native::image::Renderer
+ iced_native::svg::Renderer,
Renderer::Theme: StyleSheet,
SelectionMode: Selectable,
Model<SelectionMode>: Selectable,
SelectionMode: Default,
{
type Renderer = Renderer;

View file

@ -1,57 +0,0 @@
// Copyright 2022 System76 <info@system76.com>
// 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<Cow<'static, str>>,
#[setters(into, strip_option)]
/// An optionally-displayed icon beside the label.
pub icon: Option<IconSource<'static>>,
/// 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<String> 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<Cow<'static, str>> for SegmentedItem {
fn from(text: Cow<'static, str>) -> Self {
SegmentedItem::default().text(text)
}
}

View file

@ -1,11 +1,11 @@
// Copyright 2022 System76 <info@system76.com>
// 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<u16>(),
//! 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::<u16>(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<T> = slotmap::SecondaryMap<Entity, T>;
/// Associates extra data with an external sparse secondary map.
///
/// Sparse maps internally use a `HashMap`, for data that is sparsely associated.
pub type SparseSecondaryMap<T> = slotmap::SparseSecondaryMap<Entity, T>;

View file

@ -1,243 +0,0 @@
// Copyright 2022 System76 <info@system76.com>
// 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<Component> = Model<SingleSelect, Component>;
/// A model for multi-select button selection.
pub type MultiSelectModel<Component> = Model<MultiSelect, Component>;
/// The model held by the application, containing the unique IDs of each item and their respective contents.
#[derive(Default)]
pub struct Model<SelectionMode, Component> {
pub(super) widget: WidgetModel<SelectionMode>,
pub(super) app: AppModel<Component>,
}
/// The portion of the model used only by the application.
pub struct AppModel<Component>(SecondaryMap<Key, Component>);
impl<Component> Default for AppModel<Component> {
fn default() -> Self {
Self(SecondaryMap::default())
}
}
/// The portion of the model useful to the widget.
#[derive(Default)]
pub struct WidgetModel<SelectionMode> {
/// The content used for drawing segmented items.
pub(super) items: SlotMap<Key, SegmentedItem>,
/// Order which the items will be displayed.
pub(super) order: VecDeque<Key>,
/// Manages selections
pub(super) selection: SelectionMode,
}
impl<Component> Model<SingleSelect, Component> {
/// 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<Component> Model<MultiSelect, Component> {
/// 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<Item = Key> + '_ {
self.widget.selection.active.iter().copied()
}
}
impl<SelectionMode, Component> Model<SelectionMode, Component>
where
SelectionMode: Selectable,
{
/// Creates a builder for initializing a model.
#[must_use]
pub fn builder() -> ModelBuilder<SelectionMode, Component> {
ModelBuilder(Self {
widget: WidgetModel::default(),
app: AppModel::default(),
})
}
/// Convenience method for batching multiple operations
#[must_use]
pub fn batch(&mut self) -> Batch<SelectionMode, Component> {
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<SegmentedItem>, 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<SegmentedItem>,
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<usize> {
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<SelectionMode, Component>(Model<SelectionMode, Component>);
impl<SelectionMode: Selectable, Component> ModelBuilder<SelectionMode, Component> {
/// Inserts a new item and its associated component into the model.
#[must_use]
pub fn insert(mut self, content: impl Into<SegmentedItem>, 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<SegmentedItem>,
component: Component,
) -> Self {
self.0.insert_active(content, component);
self
}
pub fn build(self) -> Model<SelectionMode, Component> {
self.0
}
}
/// Convenience type for batching multiple operations
pub struct Batch<'a, SelectionMode, Component>(&'a mut Model<SelectionMode, Component>);
impl<'a, SelectionMode: Selectable, Component> Batch<'a, SelectionMode, Component> {
/// Insert a new button.
#[must_use]
pub fn insert(self, content: impl Into<SegmentedItem>, component: Component) -> Self {
self.0.insert(content, component);
self
}
/// Inserts and activates a button.
#[must_use]
pub fn insert_active(self, content: impl Into<SegmentedItem>, component: Component) -> Self {
self.0.insert_active(content, component);
self
}
/// Removes a button.
pub fn remove(&mut self, key: Key) {
self.0.remove(key);
}
}

View file

@ -0,0 +1,131 @@
// Copyright 2023 System76 <info@system76.com>
// 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<SelectionMode: Default>(Model<SelectionMode>);
/// Constructs a new item for the [`ModelBuilder`].
pub struct BuilderEntity<SelectionMode: Default> {
model: ModelBuilder<SelectionMode>,
id: Entity,
}
impl<SelectionMode: Default> ModelBuilder<SelectionMode>
where
Model<SelectionMode>: Selectable,
{
/// Inserts a new item and its associated data into the model.
#[must_use]
pub fn insert(
mut self,
builder: impl Fn(BuilderEntity<SelectionMode>) -> BuilderEntity<SelectionMode>,
) -> 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<SelectionMode> {
self.0
}
}
impl<SelectionMode: Default> BuilderEntity<SelectionMode>
where
Model<SelectionMode>: 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<Data>(self, map: &mut SecondaryMap<Entity, Data>, 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<Data>(
self,
map: &mut SparseSecondaryMap<Entity, Data>,
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<Data: 'static>(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<IconSource<'static>>) -> 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<Cow<'static, str>>) -> 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
}
}

View file

@ -0,0 +1,127 @@
// Copyright 2023 System76 <info@system76.com>
// 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<SelectionMode>,
}
impl<'a, SelectionMode: Default> EntityMut<'a, SelectionMode>
where
Model<SelectionMode>: 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<Data>(self, map: &mut SecondaryMap<Entity, Data>, 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<Data>(
self,
map: &mut SparseSecondaryMap<Entity, Data>,
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<Data: 'static>(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<IconSource<'static>>) -> 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<Cow<'static, str>>) -> 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
}
}

View file

@ -0,0 +1,360 @@
// Copyright 2023 System76 <info@system76.com>
// 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<SingleSelect>;
/// Single-select variant of an [`EntityMut`].
pub type SingleSelectEntityMut<'a> = EntityMut<'a, SingleSelect>;
/// A model for multi-select button selection.
pub type MultiSelectModel = Model<MultiSelect>;
/// 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<TypeId, SecondaryMap<Entity, Box<dyn Any>>>);
/// The model held by the application, containing the unique IDs and data of each inserted item.
#[derive(Default, Debug)]
pub struct Model<SelectionMode: Default> {
/// The content used for drawing segmented items.
pub(super) items: SlotMap<Entity, Settings>,
/// Icons optionally-defined for each item.
pub(super) icons: SecondaryMap<Entity, IconSource<'static>>,
/// Text optionally-defined for each item.
pub(super) text: SecondaryMap<Entity, Cow<'static, str>>,
/// Order which the items will be displayed.
pub(super) order: VecDeque<Entity>,
/// Manages selections
pub(super) selection: SelectionMode,
/// Data managed by the application.
pub(super) storage: Storage,
}
impl<SelectionMode: Default> Model<SelectionMode>
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<SelectionMode> {
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::<String>(id) {
/// println!("found string on {:?}: {}", id, data);
/// }
/// ```
pub fn data<Data: 'static>(&self, id: Entity) -> Option<&Data> {
self.storage
.0
.get(&TypeId::of::<Data>())
.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<Data: 'static>(&mut self, id: Entity) -> Option<&mut Data> {
self.storage
.0
.get_mut(&TypeId::of::<Data>())
.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::<String>(id, String::from("custom string"));
/// ```
pub fn data_set<Data: 'static>(&mut self, id: Entity, data: Data) {
if self.contains_item(id) {
self.storage
.0
.entry(TypeId::of::<Data>())
.or_insert_with(SecondaryMap::new)
.insert(id, Box::new(data));
}
}
/// Removes a specific data type from the item.
///
/// ```ignore
/// model.data.remove::<String>(id);
/// ```
pub fn data_remove<Data: 'static>(&mut self, id: Entity) {
self.storage
.0
.get_mut(&TypeId::of::<Data>())
.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<IconSource<'static>>,
) -> Option<IconSource<'static>> {
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<IconSource<'static>> {
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<SelectionMode> {
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<usize> {
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<usize> {
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<Cow<'static, str>>) -> Option<Cow<str>> {
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<Cow<'static, str>> {
self.text.remove(id)
}
}

View file

@ -0,0 +1,106 @@
// Copyright 2022 System76 <info@system76.com>
// 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<SingleSelect>`] Ensures that only one key may be selected.
#[derive(Debug, Default)]
pub struct SingleSelect {
pub active: Entity,
}
impl Selectable for Model<SingleSelect> {
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<SingleSelect> {
/// Get an immutable reference to the data associated with the active item.
#[must_use]
pub fn active_data<Data: 'static>(&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<Data: 'static>(&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<MultiSelect>`] permits multiple keys to be active at a time.
#[derive(Debug, Default)]
pub struct MultiSelect {
pub active: HashSet<Entity>,
}
impl Selectable for Model<MultiSelect> {
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<MultiSelect> {
/// 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<Item = Entity> + '_ {
self.selection.active.iter().copied()
}
}

View file

@ -1,59 +0,0 @@
// Copyright 2022 System76 <info@system76.com>
// 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<Key>,
}
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)
}
}

View file

@ -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<Background>,
@ -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<Background>,
pub first: ButtonAppearance,
pub middle: ButtonAppearance,
pub last: ButtonAppearance,
pub first: ItemAppearance,
pub middle: ItemAppearance,
pub last: ItemAppearance,
pub text_color: Color,
}

View file

@ -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<SelectionMode, Component, Message, Renderer>(
model: &Model<SelectionMode, Component>,
pub fn vertical<SelectionMode, Message, Renderer>(
model: &Model<SelectionMode>,
) -> SegmentedButton<Vertical, SelectionMode, Message, Renderer>
where
Renderer: iced_native::Renderer
@ -29,7 +30,8 @@ where
+ iced_native::image::Renderer
+ iced_native::svg::Renderer,
Renderer::Theme: StyleSheet,
SelectionMode: Selectable,
Model<SelectionMode>: 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<SelectionMode>: Selectable,
SelectionMode: Default,
{
type Renderer = Renderer;

View file

@ -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<SelectionMode>: Selectable,
SelectionMode: Default,
{
/// The model borrowed from the application create this widget.
#[setters(skip)]
pub(super) model: &'a WidgetModel<Selection>,
pub(super) model: &'a Model<SelectionMode>,
/// iced widget ID
pub(super) id: Option<Id>,
/// Padding around a button.
pub(super) button_padding: [u16; 4],
@ -103,14 +105,14 @@ where
pub(super) style: <Renderer::Theme as StyleSheet>::Style,
#[setters(skip)]
/// Emits the ID of the activated widget on selection.
pub(super) on_activate: Option<Box<dyn Fn(Key) -> Message>>,
pub(super) on_activate: Option<Box<dyn Fn(Entity) -> Message>>,
#[setters(skip)]
/// Defines the implementation of this struct
variant: PhantomData<Variant>,
}
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<Renderer = Renderer>,
Selection: Selectable,
Model<SelectionMode>: Selectable,
SelectionMode: Default,
{
#[must_use]
pub fn new<Component>(model: &'a Model<Selection, Component>) -> Self {
pub fn new(model: &'a Model<SelectionMode>) -> 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<Message, Renderer>
for SegmentedButton<'a, Variant, Selection, Message, Renderer>
impl<'a, Variant, SelectionMode, Message, Renderer> Widget<Message, Renderer>
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<Renderer = Renderer>,
Selection: Selectable,
Model<SelectionMode>: 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<SegmentedButton<'a, Variant, Selection, Message, Renderer>>
impl<'a, Variant, SelectionMode, Message, Renderer>
From<SegmentedButton<'a, Variant, SelectionMode, Message, Renderer>>
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<Renderer = Renderer>,
Variant: 'static,
Selection: Selectable,
Model<SelectionMode>: 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<Message: 'static>(id: Id) -> Command<Message> {
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);

View file

@ -0,0 +1,49 @@
// Copyright 2023 System76 <info@system76.com>
// 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<SelectionMode: Default, Message>(
model: &Model<SelectionMode>,
) -> HorizontalSegmentedButton<SelectionMode, Message, crate::Renderer>
where
Model<SelectionMode>: 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<SelectionMode, Message>(
model: &Model<SelectionMode>,
) -> VerticalSegmentedButton<SelectionMode, Message, crate::Renderer>
where
Model<SelectionMode>: 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)
}

View file

@ -0,0 +1,49 @@
// Copyright 2023 System76 <info@system76.com>
// 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<SelectionMode: Default, Message>(
model: &Model<SelectionMode>,
) -> HorizontalSegmentedButton<SelectionMode, Message, crate::Renderer>
where
Model<SelectionMode>: 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<SelectionMode, Message>(
model: &Model<SelectionMode>,
) -> VerticalSegmentedButton<SelectionMode, Message, crate::Renderer>
where
Model<SelectionMode>: 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)
}