feat: focusable segmented items in segmented button

This commit is contained in:
Michael Aaron Murphy 2023-01-09 16:18:02 +01:00 committed by Michael Murphy
parent a89ec01297
commit 29c7444a30
14 changed files with 794 additions and 611 deletions

View file

@ -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<SingleSelect, Page>,
nav_bar_pages: segmented_button::SingleSelectModel<Page>,
nav_bar_toggled_condensed: bool,
nav_bar_toggled: bool,
page: Page,
@ -190,6 +189,21 @@ 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 page_title<Message: 'static>(&self, page: Page) -> Element<Message> {
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);
}

View file

@ -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<SingleSelect, &'static str>,
pub multi_selection: segmented_button::State<MultiSelect, MultiOption>,
pub icon_theme: segmented_button::SingleSelectModel<&'static str>,
pub multi_selection: segmented_button::MultiSelectModel<MultiOption>,
pub pick_list_selected: Option<&'static str>,
pub selection: segmented_button::State<SingleSelect, ()>,
pub selection: segmented_button::SingleSelectModel<()>,
pub slider_value: f32,
pub spin_button: SpinButtonModel<i32>,
pub toggler_value: bool,
pub view_switcher: segmented_button::State<SingleSelect, DemoView>,
pub view_switcher: segmented_button::SingleSelectModel<DemoView>,
}
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")

View file

@ -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<Alpha<Rgb, f32>>,
) -> 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<Alpha<Rgb, f32>>,
) -> 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<Alpha<Rgb, f32>>,
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<Alpha<Rgb, f32>>,
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<Alpha<Rgb, f32>>,
) -> 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<Alpha<Rgb, f32>>,
) -> 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(),
}
}
}

View file

@ -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<Data, Message>(
state: &segmented_button::State<SingleSelect, Data>,
pub fn nav_bar<Component, Message>(
model: &segmented_button::SingleSelectModel<Component>,
on_activate: impl Fn(segmented_button::Key) -> Message + 'static,
) -> iced::widget::Container<Message, crate::Renderer>
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)

View file

@ -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<Selection, Message, Data>(
state: &State<Selection, Data>,
) -> HorizontalSegmentedButton<Selection, Message, crate::Renderer>
pub fn horizontal_view_switcher<SelectionMode, Component, Message>(
model: &Model<SelectionMode, Component>,
) -> HorizontalSegmentedButton<SelectionMode, Message, crate::Renderer>
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<Selection, Message, Data>(
state: &State<Selection, Data>,
) -> HorizontalSegmentedButton<Selection, Message, crate::Renderer>
pub fn horizontal_segmented_selection<SelectionMode, Component, Message>(
model: &Model<SelectionMode, Component>,
) -> HorizontalSegmentedButton<SelectionMode, Message, crate::Renderer>
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<Selection, Message, Data>(
state: &State<Selection, Data>,
) -> VerticalSegmentedButton<Selection, Message, crate::Renderer>
pub fn vertical_segmented_selection<SelectionMode, Component, Message>(
model: &Model<SelectionMode, Component>,
) -> VerticalSegmentedButton<SelectionMode, Message, crate::Renderer>
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<Selection, Message, Data>(
state: &State<Selection, Data>,
) -> VerticalSegmentedButton<Selection, Message, crate::Renderer>
pub fn vertical_view_switcher<SelectionMode, Component, Message>(
model: &Model<SelectionMode, Component>,
) -> VerticalSegmentedButton<SelectionMode, Message, crate::Renderer>
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)

View file

@ -1,45 +1,48 @@
// Copyright 2022 System76 <info@system76.com>
// 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<Selection, Message, Renderer, Data>(
state: &State<Selection, Data>,
) -> SegmentedButton<Horizontal, Selection, Message, Renderer>
pub fn horizontal_segmented_button<SelectionMode, Component, Message, Renderer>(
model: &Model<SelectionMode, Component>,
) -> SegmentedButton<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,
{
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 {

View file

@ -0,0 +1,57 @@
// 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,7 +1,7 @@
// Copyright 2022 System76 <info@system76.com>
// 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<u16>(),
//! ...
//! state: segmented_button::SingleSelectModel<u16>(),
//! }
//! ```
//!
//! 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};

View file

@ -0,0 +1,202 @@
// Copyright 2022 System76 <info@system76.com>
// 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<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>,
/// Manages selections
pub(super) selection: SelectionMode,
}
impl<Component> Model<SingleSelect, Component> {
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<Component> Model<MultiSelect, Component> {
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<Item = Key> + '_ {
self.widget.selection.active.iter().copied()
}
}
impl<SelectionMode, Component> Model<SelectionMode, Component>
where
SelectionMode: Selectable,
{
#[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)
}
/// 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<SegmentedItem>, 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<SegmentedItem>,
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<SelectionMode, Component>(Model<SelectionMode, Component>);
impl<SelectionMode: Selectable, Component> ModelBuilder<SelectionMode, Component> {
#[must_use]
pub fn insert(mut self, content: impl Into<SegmentedItem>, component: Component) -> Self {
self.0.insert(content, component);
self
}
#[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,59 @@
// 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

@ -1,292 +0,0 @@
// Copyright 2022 System76 <info@system76.com>
// 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<Selection, Data> {
/// State that is shared with widget drawing.
pub inner: SharedWidgetState<Selection>,
/// State unique to the application.
pub data: SecondaryState<Data>,
}
impl<Selection: Default, Data> Default for State<Selection, Data> {
fn default() -> Self {
Self {
inner: SharedWidgetState::default(),
data: SecondaryState::default(),
}
}
}
/// State which is most useful to the widget.
#[derive(Default)]
pub struct SharedWidgetState<Variant> {
/// The content used for drawing segmented buttons.
pub buttons: SlotMap<Key, Content>,
/// Manages selections
pub selection: Variant,
}
impl<Data> State<SingleSelect, Data> {
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<Data> State<MultiSelect, Data> {
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<Item = Key> + '_ {
self.inner.selection.active.iter().copied()
}
/// Get the application data for the active buttons.
pub fn active_data(&self) -> impl Iterator<Item = (Key, &Data)> {
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<Data> = SecondaryMap<Key, Data>;
impl<Selection, Data> State<Selection, Data>
where
Selection: Selectable,
{
#[must_use]
pub fn builder() -> Builder<Selection, Data> {
Builder(Self::default())
}
/// Convenience method for batching multiple operations
#[must_use]
pub fn batch(&mut self) -> Batch<Selection, Data> {
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<Content>, 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<Content>, 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<Data> {
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<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)
}
}
pub struct Builder<Selection, Data>(State<Selection, Data>);
impl<Selection: Selectable, Data> Builder<Selection, Data> {
pub fn insert(mut self, content: impl Into<Content>, data: Data) -> Self {
self.0.insert(content, data);
self
}
pub fn insert_active(mut self, content: impl Into<Content>, data: Data) -> Self {
self.0.insert_active(content, data);
self
}
pub fn build(self) -> State<Selection, Data> {
self.0
}
}
/// Convenience type for batching multiple operations
pub struct Batch<'a, Selection, Data>(&'a mut State<Selection, Data>);
impl<'a, Selection: Selectable, Data> Batch<'a, Selection, Data> {
/// Insert a new button.
pub fn insert(self, content: impl Into<Content>, data: Data) -> Self {
self.0.insert(content, data);
self
}
/// Inserts and activates a button.
pub fn insert_active(self, content: impl Into<Content>, 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<Cow<'static, str>>,
#[setters(strip_option, into)]
/// An optionally-displayed icon beside the label.
pub icon: Option<IconSource<'static>>,
/// Whether the button is clickable or not.
pub enabled: bool,
}
impl From<String> 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<Cow<'static, str>> for Content {
fn from(text: Cow<'static, str>) -> Self {
Content::default().text(text)
}
}

View file

@ -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

View file

@ -1,7 +1,10 @@
// Copyright 2022 System76 <info@system76.com>
// 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<Selection, Message, Renderer, Data>(
state: &State<Selection, Data>,
) -> SegmentedButton<Vertical, Selection, Message, Renderer>
pub fn vertical_segmented_button<SelectionMode, Component, Message, Renderer>(
model: &Model<SelectionMode, Component>,
) -> SegmentedButton<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,
{
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 {

View file

@ -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<Selection>,
pub(super) model: &'a WidgetModel<Selection>,
pub(super) id: Option<Id>,
/// 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<Selection>) -> Self {
pub fn new<Component>(model: &'a Model<Selection, Component>) -> 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<Key> = 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::<UniqueWidgetState>()
tree::Tag::of::<LocalState>()
}
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::<UniqueWidgetState>();
let state = tree.state.downcast_mut::<LocalState>();
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<Message>,
) {
let state = tree.state.downcast_mut::<LocalState>();
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::<UniqueWidgetState>();
let state = tree.state.downcast_ref::<LocalState>();
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<Message: 'static>(id: Id) -> Command<Message> {
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<std::borrow::Cow<'static, str>>) -> 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<Id> for widget::Id {
fn from(id: Id) -> Self {
id.0
}
}