feat: implement SegmentedButton widget

This commit is contained in:
Michael Aaron Murphy 2022-12-28 12:42:28 +01:00 committed by Ashley Wulber
parent 01701759c9
commit e97c258422
8 changed files with 740 additions and 244 deletions

View file

@ -24,6 +24,7 @@ lazy_static = "1.4.0"
palette = "0.6.1" palette = "0.6.1"
cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", optional = true, branch = "master_jammy" } cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", optional = true, branch = "master_jammy" }
sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", optional = true, rev = "73346019952f82ec7e4d4d15f5d66841b54e8b61" } sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", optional = true, rev = "73346019952f82ec7e4d4d15f5d66841b54e8b61" }
slotmap = "1.0.6"
[dependencies.cosmic-theme] [dependencies.cosmic-theme]
git = "https://github.com/pop-os/cosmic-theme.git" git = "https://github.com/pop-os/cosmic-theme.git"

View file

@ -1,24 +1,28 @@
/// Copyright 2022 System76 <info@system76.com> /// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
use cosmic::{ use cosmic::{
iced_native,
iced_native::window,
iced::widget::{column, container, horizontal_space, row, text}, iced::widget::{column, container, horizontal_space, row, text},
iced::{self, Application, Command, Length, Subscription}, iced::{self, Application, Command, Length, Subscription},
iced_winit::window::{close, drag, toggle_maximize, minimize}, iced_native,
iced_native::window,
iced_winit::window::{close, drag, minimize, toggle_maximize},
theme::{self, Theme}, theme::{self, Theme},
widget::{icon, list, nav_bar, nav_button, header_bar, settings, scrollable, spin_button::{SpinButtonModel, SpinMessage}}, widget::{
Element, header_bar, icon, list, nav_bar, nav_button, scrollable, segmented_button, settings,
ElementExt, spin_button::{SpinButtonModel, SpinMessage},
},
Element, ElementExt,
};
use std::{
sync::atomic::{AtomicU32, Ordering},
vec,
}; };
use std::{vec, sync::atomic::{AtomicU32, Ordering}};
mod bluetooth; mod bluetooth;
mod demo; mod demo;
use self::desktop::DesktopPage; use self::{demo::DemoView, desktop::DesktopPage};
mod desktop; mod desktop;
use self::input_devices::InputDevicesPage; use self::input_devices::InputDevicesPage;
@ -125,6 +129,7 @@ pub struct Window {
debug: bool, debug: bool,
theme: Theme, theme: Theme,
slider_value: f32, slider_value: f32,
demo_tab_state: segmented_button::State<DemoView>,
spin_button: SpinButtonModel<i32>, spin_button: SpinButtonModel<i32>,
checkbox_value: bool, checkbox_value: bool,
toggler_value: bool, toggler_value: bool,
@ -155,36 +160,34 @@ impl Window {
#[allow(dead_code)] #[allow(dead_code)]
#[derive(Clone, Copy, Debug)] #[derive(Clone, Copy, Debug)]
pub enum Message { pub enum Message {
Page(Page),
Debug(bool),
ThemeChanged(Theme),
ButtonPressed, ButtonPressed,
SliderChanged(f32),
CheckboxToggled(bool), CheckboxToggled(bool),
TogglerToggled(bool), Close,
CondensedViewToggle(()),
Debug(bool),
DemoTabActivate(segmented_button::Key),
Drag,
InputChanged,
Maximize,
Minimize,
Page(Page),
PickListSelected(&'static str), PickListSelected(&'static str),
RowSelected(usize), RowSelected(usize),
Close, SliderChanged(f32),
SpinButton(SpinMessage),
ThemeChanged(Theme),
TogglerToggled(bool),
ToggleSidebar, ToggleSidebar,
ToggleSidebarCondensed, ToggleSidebarCondensed,
Drag,
Minimize,
Maximize,
InputChanged,
SpinButton(SpinMessage),
CondensedViewToggle(()),
} }
impl Window { impl Window {
fn page_title(&self, page: Page) -> Element<Message> { fn page_title(&self, page: Page) -> Element<Message> {
row!( row!(text(page.title()).size(30), horizontal_space(Length::Fill),).into()
text(page.title()).size(30),
horizontal_space(Length::Fill),
).into()
} }
fn is_condensed(&self) -> bool { fn is_condensed(&self) -> bool {
WINDOW_WIDTH.load(Ordering::Relaxed) < BREAK_POINT WINDOW_WIDTH.load(Ordering::Relaxed) < BREAK_POINT
} }
fn parent_page_button(&self, sub_page: impl SubPage) -> Element<Message> { fn parent_page_button(&self, sub_page: impl SubPage) -> Element<Message> {
@ -197,7 +200,6 @@ impl Window {
.padding(0) .padding(0)
.style(theme::Button::Link) .style(theme::Button::Link)
.on_press(Message::Page(page)), .on_press(Message::Page(page)),
row!( row!(
text(sub_page.title()).size(30), text(sub_page.title()).size(30),
horizontal_space(Length::Fill), horizontal_space(Length::Fill),
@ -209,17 +211,26 @@ impl Window {
fn sub_page_button(&self, sub_page: impl SubPage) -> Element<Message> { fn sub_page_button(&self, sub_page: impl SubPage) -> Element<Message> {
iced::widget::Button::new( iced::widget::Button::new(
container(settings::item_row(vec![ container(
icon(sub_page.icon_name(), 20).style(theme::Svg::Symbolic).into(), settings::item_row(vec![
column!( icon(sub_page.icon_name(), 20)
text(sub_page.title()).size(18), .style(theme::Svg::Symbolic)
text(sub_page.description()).size(12), .into(),
).spacing(2).into(), column!(
horizontal_space(iced::Length::Fill).into(), text(sub_page.title()).size(18),
icon("go-next-symbolic", 20).style(theme::Svg::Symbolic).into(), text(sub_page.description()).size(12),
]).spacing(16)) )
.spacing(2)
.into(),
horizontal_space(iced::Length::Fill).into(),
icon("go-next-symbolic", 20)
.style(theme::Svg::Symbolic)
.into(),
])
.spacing(16),
)
.padding([20, 24]) .padding([20, 24])
.style(theme::Container::Custom(list::column::style)) .style(theme::Container::Custom(list::column::style)),
) )
.padding(0) .padding(0)
.style(theme::Button::Transparent) .style(theme::Button::Transparent)
@ -259,6 +270,21 @@ impl Application for Window {
window.title = String::from("COSMIC Design System - Iced"); window.title = String::from("COSMIC Design System - Iced");
window.spin_button.min = -10; window.spin_button.min = -10;
window.spin_button.max = 10; window.spin_button.max = 10;
let key = window
.demo_tab_state
.insert(String::from("Tab A"), DemoView::TabA);
window.demo_tab_state.activate(key);
window
.demo_tab_state
.insert(String::from("Tab B"), DemoView::TabB);
window
.demo_tab_state
.insert(String::from("Tab C"), DemoView::TabC);
(window, Command::none()) (window, Command::none())
} }
@ -267,20 +293,25 @@ impl Application for Window {
} }
fn subscription(&self) -> Subscription<Message> { fn subscription(&self) -> Subscription<Message> {
iced_native::subscription::events_with(|event, _| match event { iced_native::subscription::events_with(|event, _| match event {
cosmic::iced::Event::Window(_window_id, window::Event::Resized{width, height: _}) => { cosmic::iced::Event::Window(
let old_width = WINDOW_WIDTH.load(Ordering::Relaxed); _window_id,
if old_width == 0 window::Event::Resized { width, height: _ },
|| old_width < BREAK_POINT && width > BREAK_POINT ) => {
|| old_width > BREAK_POINT && width < BREAK_POINT { let old_width = WINDOW_WIDTH.load(Ordering::Relaxed);
WINDOW_WIDTH.store(width, Ordering::Relaxed); if old_width == 0
Some(()) || old_width < BREAK_POINT && width > BREAK_POINT
} else { || old_width > BREAK_POINT && width < BREAK_POINT
None {
} WINDOW_WIDTH.store(width, Ordering::Relaxed);
} Some(())
_ => None } else {
}).map(Message::CondensedViewToggle) None
}
}
_ => None,
})
.map(Message::CondensedViewToggle)
} }
fn update(&mut self, message: Message) -> iced::Command<Self::Message> { fn update(&mut self, message: Message) -> iced::Command<Self::Message> {
@ -288,165 +319,199 @@ impl Application for Window {
Message::Page(page) => { Message::Page(page) => {
self.sidebar_toggled_condensed = false; self.sidebar_toggled_condensed = false;
self.page = page; self.page = page;
}, }
Message::Debug(debug) => self.debug = debug, Message::Debug(debug) => self.debug = debug,
Message::ThemeChanged(theme) => self.theme = theme, Message::ThemeChanged(theme) => self.theme = theme,
Message::ButtonPressed => {} Message::ButtonPressed => {}
Message::SliderChanged(value) => self.slider_value = value, Message::SliderChanged(value) => self.slider_value = value,
Message::CheckboxToggled(value) => { Message::CheckboxToggled(value) => {
self.checkbox_value = value; self.checkbox_value = value;
}, }
Message::TogglerToggled(value) => self.toggler_value = value, Message::TogglerToggled(value) => self.toggler_value = value,
Message::PickListSelected(value) => self.pick_list_selected = Some(value), Message::PickListSelected(value) => self.pick_list_selected = Some(value),
Message::ToggleSidebar => self.sidebar_toggled = !self.sidebar_toggled, Message::ToggleSidebar => self.sidebar_toggled = !self.sidebar_toggled,
Message::ToggleSidebarCondensed => self.sidebar_toggled_condensed = !self.sidebar_toggled_condensed, Message::ToggleSidebarCondensed => {
self.sidebar_toggled_condensed = !self.sidebar_toggled_condensed
}
Message::Drag => return drag(window::Id::new(0)), Message::Drag => return drag(window::Id::new(0)),
Message::Close => return close(window::Id::new(0)), Message::Close => return close(window::Id::new(0)),
Message::Minimize => return minimize(window::Id::new(0), true), Message::Minimize => return minimize(window::Id::new(0), true),
Message::Maximize => return toggle_maximize(window::Id::new(0)), Message::Maximize => return toggle_maximize(window::Id::new(0)),
Message::RowSelected(row) => println!("Selected row {row}"), Message::RowSelected(row) => println!("Selected row {row}"),
Message::InputChanged => {}, Message::InputChanged => {}
Message::SpinButton(msg) => self.spin_button.update(msg), Message::SpinButton(msg) => self.spin_button.update(msg),
Message::CondensedViewToggle(_) => {}, Message::CondensedViewToggle(_) => {}
Message::DemoTabActivate(key) => self.demo_tab_state.activate(key),
} }
Command::none() Command::none()
} }
fn view(&self) -> Element<Message> { fn view(&self) -> Element<Message> {
let (sidebar_message, sidebar_toggled) = if self.is_condensed() { let (sidebar_message, sidebar_toggled) = if self.is_condensed() {
(Message::ToggleSidebarCondensed, self.sidebar_toggled_condensed) (
} else { Message::ToggleSidebarCondensed,
(Message::ToggleSidebar, self.sidebar_toggled) self.sidebar_toggled_condensed,
}; )
} else {
(Message::ToggleSidebar, self.sidebar_toggled)
};
let mut header = header_bar() let mut header = header_bar()
.title("COSMIC Design System - Iced") .title("COSMIC Design System - Iced")
.on_close(Message::Close) .on_close(Message::Close)
.on_drag(Message::Drag) .on_drag(Message::Drag)
.start( .start(
nav_button("Settings") nav_button("Settings")
.on_sidebar_toggled(sidebar_message) .on_sidebar_toggled(sidebar_message)
.sidebar_active(sidebar_toggled) .sidebar_active(sidebar_toggled)
.into() .into(),
); );
if self.show_maximize { if self.show_maximize {
header = header.on_maximize(Message::Maximize); header = header.on_maximize(Message::Maximize);
} }
if self.show_minimize { if self.show_minimize {
header = header.on_minimize(Message::Minimize); header = header.on_minimize(Message::Minimize);
} }
let header = Into::<Element<Message>>::into(header).debug(self.debug); let header = Into::<Element<Message>>::into(header).debug(self.debug);
let mut widgets = Vec::with_capacity(2); let mut widgets = Vec::with_capacity(2);
if sidebar_toggled { if sidebar_toggled {
let sidebar_button_complex = |page: Page, active| { let sidebar_button_complex = |page: Page, active| {
cosmic::nav_button!( cosmic::nav_button!(page.icon_name(), page.title(), active)
page.icon_name(), .on_press(Message::Page(page))
page.title(), };
active
)
.on_press(Message::Page(page))
};
let sidebar_button = |page: Page| { let sidebar_button = |page: Page| sidebar_button_complex(page, self.page == page);
sidebar_button_complex(page, self.page == page)
};
let mut sidebar = container(scrollable(column!( let mut sidebar = container(scrollable(
sidebar_button(Page::Demo), column!(
sidebar_button(Page::WiFi), sidebar_button(Page::Demo),
sidebar_button_complex(Page::Networking(None), matches!(self.page, Page::Networking(_))), sidebar_button(Page::WiFi),
sidebar_button(Page::Bluetooth), sidebar_button_complex(
sidebar_button_complex(Page::Desktop(None), matches!(self.page, Page::Desktop(_))), Page::Networking(None),
sidebar_button_complex(Page::InputDevices(None), matches!(self.page, Page::InputDevices(_))), matches!(self.page, Page::Networking(_))
sidebar_button(Page::Displays), ),
sidebar_button(Page::PowerAndBattery), sidebar_button(Page::Bluetooth),
sidebar_button(Page::Sound), sidebar_button_complex(
sidebar_button(Page::PrintersAndScanners), Page::Desktop(None),
sidebar_button(Page::PrivacyAndSecurity), matches!(self.page, Page::Desktop(_))
sidebar_button_complex(Page::SystemAndAccounts(None), matches!(self.page, Page::SystemAndAccounts(_))), ),
sidebar_button(Page::UpdatesAndRecovery), sidebar_button_complex(
sidebar_button_complex(Page::TimeAndLanguage(None), matches!(self.page, Page::TimeAndLanguage(_))), Page::InputDevices(None),
sidebar_button(Page::Accessibility), matches!(self.page, Page::InputDevices(_))
sidebar_button(Page::Applications), ),
).spacing(14))) sidebar_button(Page::Displays),
.height(Length::Fill) sidebar_button(Page::PowerAndBattery),
.padding(8) sidebar_button(Page::Sound),
.style(theme::Container::Custom(nav_bar::nav_bar_sections_style)); sidebar_button(Page::PrintersAndScanners),
sidebar_button(Page::PrivacyAndSecurity),
sidebar_button_complex(
Page::SystemAndAccounts(None),
matches!(self.page, Page::SystemAndAccounts(_))
),
sidebar_button(Page::UpdatesAndRecovery),
sidebar_button_complex(
Page::TimeAndLanguage(None),
matches!(self.page, Page::TimeAndLanguage(_))
),
sidebar_button(Page::Accessibility),
sidebar_button(Page::Applications),
)
.spacing(14),
))
.height(Length::Fill)
.padding(8)
.style(theme::Container::Custom(nav_bar::nav_bar_sections_style));
if ! self.is_condensed() { if !self.is_condensed() {
sidebar = sidebar.max_width(300) sidebar = sidebar.max_width(300)
} }
let sidebar: Element<_> = sidebar.into(); let sidebar: Element<_> = sidebar.into();
widgets.push(sidebar.debug(self.debug)); widgets.push(sidebar.debug(self.debug));
} }
if ! (self.is_condensed() && sidebar_toggled) { if !(self.is_condensed() && sidebar_toggled) {
let content: Element<_> = match self.page { let content: Element<_> = match self.page {
Page::Demo => self.view_demo(), Page::Demo => self.view_demo(),
Page::Networking(None) => settings::view_column(vec![ Page::Networking(None) => settings::view_column(vec![
self.page_title(self.page), self.page_title(self.page),
column!( column!(
self.sub_page_button(NetworkingPage::Wired), self.sub_page_button(NetworkingPage::Wired),
self.sub_page_button(NetworkingPage::OnlineAccounts), self.sub_page_button(NetworkingPage::OnlineAccounts),
).spacing(16).into() )
]).into(), .spacing(16)
Page::Networking(Some(sub_page)) => self.view_unimplemented_sub_page(sub_page), .into(),
Page::Bluetooth => self.view_bluetooth(), ])
Page::Desktop(desktop_page_opt) => self.view_desktop(desktop_page_opt), .into(),
Page::InputDevices(None) => settings::view_column(vec![ Page::Networking(Some(sub_page)) => self.view_unimplemented_sub_page(sub_page),
self.page_title(self.page), Page::Bluetooth => self.view_bluetooth(),
column!( Page::Desktop(desktop_page_opt) => self.view_desktop(desktop_page_opt),
self.sub_page_button(InputDevicesPage::Keyboard), Page::InputDevices(None) => settings::view_column(vec![
self.sub_page_button(InputDevicesPage::Touchpad), self.page_title(self.page),
self.sub_page_button(InputDevicesPage::Mouse), column!(
).spacing(16).into() self.sub_page_button(InputDevicesPage::Keyboard),
]).into(), self.sub_page_button(InputDevicesPage::Touchpad),
Page::InputDevices(Some(sub_page)) => self.view_unimplemented_sub_page(sub_page), self.sub_page_button(InputDevicesPage::Mouse),
Page::SystemAndAccounts(None) => settings::view_column(vec![ )
self.page_title(self.page), .spacing(16)
column!( .into(),
self.sub_page_button(SystemAndAccountsPage::Users), ])
self.sub_page_button(SystemAndAccountsPage::About), .into(),
self.sub_page_button(SystemAndAccountsPage::Firmware), Page::InputDevices(Some(sub_page)) => self.view_unimplemented_sub_page(sub_page),
).spacing(16).into() Page::SystemAndAccounts(None) => settings::view_column(vec![
]).into(), self.page_title(self.page),
Page::SystemAndAccounts(Some(SystemAndAccountsPage::About)) => self.view_system_and_accounts_about(), column!(
Page::SystemAndAccounts(Some(sub_page)) => self.view_unimplemented_sub_page(sub_page), self.sub_page_button(SystemAndAccountsPage::Users),
Page::TimeAndLanguage(None) => settings::view_column(vec![ self.sub_page_button(SystemAndAccountsPage::About),
self.page_title(self.page), self.sub_page_button(SystemAndAccountsPage::Firmware),
column!( )
self.sub_page_button(TimeAndLanguagePage::DateAndTime), .spacing(16)
self.sub_page_button(TimeAndLanguagePage::RegionAndLanguage), .into(),
).spacing(16).into() ])
]).into(), .into(),
Page::TimeAndLanguage(Some(sub_page)) => self.view_unimplemented_sub_page(sub_page), Page::SystemAndAccounts(Some(SystemAndAccountsPage::About)) => {
_ => self.view_unimplemented_page(self.page), self.view_system_and_accounts_about()
}; }
Page::SystemAndAccounts(Some(sub_page)) => {
self.view_unimplemented_sub_page(sub_page)
}
Page::TimeAndLanguage(None) => settings::view_column(vec![
self.page_title(self.page),
column!(
self.sub_page_button(TimeAndLanguagePage::DateAndTime),
self.sub_page_button(TimeAndLanguagePage::RegionAndLanguage),
)
.spacing(16)
.into(),
])
.into(),
Page::TimeAndLanguage(Some(sub_page)) => self.view_unimplemented_sub_page(sub_page),
_ => self.view_unimplemented_page(self.page),
};
widgets.push( widgets.push(
scrollable(row![ scrollable(row![
horizontal_space(Length::Fill), horizontal_space(Length::Fill),
content.debug(self.debug), content.debug(self.debug),
horizontal_space(Length::Fill), horizontal_space(Length::Fill),
]) ])
.into(), .into(),
); );
} }
let content = container(row(widgets)) let content = container(row(widgets))
.padding([0, 8, 8, 8]) .padding([0, 8, 8, 8])
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.into(); .into();
column(vec![header, content]).into() column(vec![header, content]).into()
} }
fn theme(&self) -> Theme { fn theme(&self) -> Theme {

View file

@ -1,13 +1,19 @@
use cosmic::{ use cosmic::{
Element,
iced::{Alignment, Length},
iced::widget::{checkbox, pick_list, progress_bar, radio, row, slider}, iced::widget::{checkbox, pick_list, progress_bar, radio, row, slider},
widget::{button, settings, toggler}, iced::{Alignment, Length},
theme::{Button as ButtonTheme, Theme}, theme::{Button as ButtonTheme, Theme},
widget::{button, segmented_button, settings, toggler},
Element,
}; };
use super::{Message, Page, Window}; use super::{Message, Page, Window};
pub enum DemoView {
TabA,
TabB,
TabC,
}
impl Window { impl Window {
pub(super) fn view_demo(&self) -> Element<Message> { pub(super) fn view_demo(&self) -> Element<Message> {
let choose_theme = [Theme::Light, Theme::Dark].iter().fold( let choose_theme = [Theme::Light, Theme::Dark].iter().fold(
@ -24,77 +30,107 @@ impl Window {
settings::view_column(vec![ settings::view_column(vec![
self.page_title(Page::Demo), self.page_title(Page::Demo),
segmented_button(&self.demo_tab_state)
settings::view_section("Debug") .on_activate(Message::DemoTabActivate)
.add(settings::item("Debug theme", choose_theme))
.add(settings::item(
"Debug layout",
toggler(None, self.debug, Message::Debug)
))
.into(), .into(),
match self.demo_tab_state.active_data() {
settings::view_section("Buttons") None => panic!("no tab is active"),
.add(settings::item_row(vec![ Some(DemoView::TabA) => settings::view_column(vec![
button(ButtonTheme::Primary) settings::view_section("Debug")
.text("Primary") .add(settings::item("Debug theme", choose_theme))
.on_press(Message::ButtonPressed) .add(settings::item(
"Debug layout",
toggler(None, self.debug, Message::Debug),
))
.into(), .into(),
button(ButtonTheme::Secondary) settings::view_section("Buttons")
.text("Secondary") .add(settings::item_row(vec![
.on_press(Message::ButtonPressed) button(ButtonTheme::Primary)
.text("Primary")
.on_press(Message::ButtonPressed)
.into(),
button(ButtonTheme::Secondary)
.text("Secondary")
.on_press(Message::ButtonPressed)
.into(),
button(ButtonTheme::Positive)
.text("Positive")
.on_press(Message::ButtonPressed)
.into(),
button(ButtonTheme::Destructive)
.text("Destructive")
.on_press(Message::ButtonPressed)
.into(),
button(ButtonTheme::Text)
.text("Text")
.on_press(Message::ButtonPressed)
.into(),
]))
.add(settings::item_row(vec![
button(ButtonTheme::Primary).text("Primary").into(),
button(ButtonTheme::Secondary).text("Secondary").into(),
button(ButtonTheme::Positive).text("Positive").into(),
button(ButtonTheme::Destructive).text("Destructive").into(),
button(ButtonTheme::Text).text("Text").into(),
]))
.into(), .into(),
button(ButtonTheme::Positive) settings::view_section("Controls")
.text("Positive") .add(settings::item(
.on_press(Message::ButtonPressed) "Toggler",
toggler(None, self.toggler_value, Message::TogglerToggled),
))
.add(settings::item(
"Pick List (TODO)",
pick_list(
vec!["Option 1", "Option 2", "Option 3", "Option 4"],
self.pick_list_selected,
Message::PickListSelected,
)
.padding([8, 0, 8, 16]),
))
.add(settings::item(
"Slider",
slider(0.0..=100.0, self.slider_value, Message::SliderChanged)
.width(Length::Units(250)),
))
.add(settings::item(
"Progress",
progress_bar(0.0..=100.0, self.slider_value)
.width(Length::Units(250))
.height(Length::Units(4)),
))
.add(settings::item_row(vec![checkbox(
"Checkbox",
self.checkbox_value,
Message::CheckboxToggled,
)
.into()]))
.add(settings::item(
format!(
"Spin Button (Range {}:{})",
self.spin_button.min, self.spin_button.max
),
self.spin_button.view(Message::SpinButton),
))
.into(), .into(),
button(ButtonTheme::Destructive) ])
.text("Destructive") .padding(0)
.on_press(Message::ButtonPressed)
.into(),
button(ButtonTheme::Text)
.text("Text")
.on_press(Message::ButtonPressed)
.into()
]))
.add(settings::item_row(vec![
button(ButtonTheme::Primary).text("Primary").into(),
button(ButtonTheme::Secondary).text("Secondary").into(),
button(ButtonTheme::Positive).text("Positive").into(),
button(ButtonTheme::Destructive).text("Destructive").into(),
button(ButtonTheme::Text).text("Text").into(),
]))
.into(), .into(),
Some(DemoView::TabB) => {
settings::view_section("Controls") settings::view_column(vec![settings::view_section("Tab B")
.add(settings::item("Toggler", toggler(None, self.toggler_value, Message::TogglerToggled))) .add(cosmic::iced::widget::text("Nothing here yet").width(Length::Fill))
.add(settings::item( .into()])
"Pick List (TODO)", .padding(0)
pick_list( .into()
vec!["Option 1", "Option 2", "Option 3", "Option 4",], }
self.pick_list_selected, Some(DemoView::TabC) => {
Message::PickListSelected settings::view_column(vec![settings::view_section("Tab C")
) .add(cosmic::iced::widget::text("Nothing here yet").width(Length::Fill))
.padding([8, 0, 8, 16]) .into()])
)) .padding(0)
.add(settings::item( .into()
"Slider", }
slider(0.0..=100.0, self.slider_value, Message::SliderChanged) },
.width(Length::Units(250))
))
.add(settings::item(
"Progress",
progress_bar(0.0..=100.0, self.slider_value)
.width(Length::Units(250))
.height(Length::Units(4))
))
.add(settings::item_row(vec![
checkbox("Checkbox", self.checkbox_value, Message::CheckboxToggled).into()
]))
.add(settings::item(
format!("Spin Button (Range {}:{})", self.spin_button.min, self.spin_button.max),
self.spin_button.view(Message::SpinButton),
))
.into()
]) ])
.into() .into()
} }

View file

@ -27,6 +27,7 @@ use iced_style::svg;
use iced_style::text; use iced_style::text;
use iced_style::text_input; use iced_style::text_input;
use iced_style::toggler; use iced_style::toggler;
use crate::widget::segmented_button;
use iced_core::{Background, Color}; use iced_core::{Background, Color};
@ -864,3 +865,37 @@ impl text_input::StyleSheet for Theme {
palette.primary.weak.color palette.primary.weak.color
} }
} }
#[derive(Clone, Copy, Default)]
pub enum SegmentedButton {
#[default]
Default,
Custom(fn(&Theme) -> segmented_button::Appearance)
}
impl segmented_button::StyleSheet for Theme {
type Style = SegmentedButton;
fn appearance(&self, style: &Self::Style) -> segmented_button::Appearance {
if let SegmentedButton::Custom(func) = style {
return func(self);
}
let cosmic = self.cosmic();
segmented_button::Appearance {
button_active: segmented_button::ButtonAppearance {
background: Some(Background::Color(cosmic.primary.component.base.into())),
border_bottom: Some((4.0, cosmic.accent.base.into())),
border_radius: BorderRadius::from([8.0, 8.0, 0.0, 0.0]),
text_color: cosmic.accent.base.into(),
},
button_inactive: segmented_button::ButtonAppearance {
background: None,
border_bottom: Some((2.0, cosmic.accent.base.into())),
text_color: cosmic.primary.on.into(),
border_radius: BorderRadius::from(0.0),
}
}
}
}

View file

@ -22,6 +22,9 @@ pub use navigation::*;
mod toggler; mod toggler;
pub use toggler::toggler; pub use toggler::toggler;
pub mod segmented_button;
pub use segmented_button::{SegmentedButton, segmented_button};
pub mod settings; pub mod settings;
mod scrollable; mod scrollable;

View file

@ -0,0 +1,245 @@
/// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
mod state;
mod style;
pub use self::state::{ButtonContent, Key, SecondaryState, State, WidgetState};
pub use self::style::{Appearance, ButtonAppearance, StyleSheet};
use derive_setters::Setters;
use iced::{
alignment::{Horizontal, Vertical},
event, mouse, touch, Background, Color, Element, Event, Length, Point, Rectangle, Size,
};
use iced_core::BorderRadius;
use iced_native::{layout, renderer, widget::Tree, Clipboard, Layout, Shell, Widget};
/// A linear set of options for choosing between.
#[derive(Setters)]
pub struct SegmentedButton<'a, Message, Renderer>
where
Renderer: iced_native::Renderer,
Renderer::Theme: StyleSheet,
{
state: &'a WidgetState,
width: Length,
height: Length,
spacing: u16,
#[setters(into)]
style: <Renderer::Theme as StyleSheet>::Style,
#[setters(skip)]
on_activate: Option<Box<dyn Fn(Key) -> Message>>,
}
impl<'a, Message, Renderer> SegmentedButton<'a, Message, Renderer>
where
Renderer: iced_native::Renderer,
Renderer::Theme: StyleSheet,
{
#[must_use]
pub fn new(state: &'a WidgetState) -> Self {
Self {
state,
height: Length::Units(48),
width: Length::Fill,
spacing: 0,
style: <Renderer::Theme as StyleSheet>::Style::default(),
on_activate: None,
}
}
#[must_use]
pub fn on_activate(mut self, on_activate: impl Fn(Key) -> Message + 'static) -> Self {
self.on_activate = Some(Box::from(on_activate));
self
}
}
#[must_use]
pub fn segmented_button<Message, Renderer, Data>(
state: &State<Data>,
) -> SegmentedButton<Message, Renderer>
where
Renderer: iced_native::Renderer,
Renderer::Theme: StyleSheet,
{
SegmentedButton::new(&state.inner)
}
impl<'a, Message, Renderer> Widget<Message, Renderer> for SegmentedButton<'a, Message, Renderer>
where
Renderer: iced_native::Renderer + iced_native::text::Renderer,
Renderer::Theme: StyleSheet,
Message: 'static + Clone,
{
fn width(&self) -> Length {
self.width
}
fn height(&self) -> Length {
self.height
}
fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node {
let limits = limits.width(self.width).height(self.height);
let bounds = limits.max();
let size = renderer.default_size();
let mut width = 0.0;
let height = bounds.height;
for (_, content) in self.state.buttons.iter() {
let (w, _) = renderer.measure(&content.text, size, Default::default(), bounds);
width += w + f32::from(self.spacing * 2);
}
layout::Node::new(limits.resolve(Size::new(width, height)))
}
fn on_event(
&mut self,
_tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
_renderer: &Renderer,
_clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
) -> event::Status {
let bounds = layout.bounds();
if bounds.contains(cursor_position) {
let button_width = bounds.width / self.state.buttons.len() as f32;
for (num, (key, _)) in self.state.buttons.iter().enumerate() {
let mut bounds = bounds;
bounds.width = button_width;
bounds.x += num as f32 * button_width;
if bounds.contains(cursor_position) {
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;
}
}
}
}
}
event::Status::Ignored
}
fn mouse_interaction(
&self,
_tree: &Tree,
layout: Layout<'_>,
cursor_position: iced::Point,
_viewport: &iced::Rectangle,
_renderer: &Renderer,
) -> iced_native::mouse::Interaction {
if layout.bounds().contains(cursor_position) {
iced_native::mouse::Interaction::Pointer
} else {
iced_native::mouse::Interaction::Idle
}
}
fn draw(
&self,
_tree: &Tree,
renderer: &mut Renderer,
theme: &<Renderer as iced_native::Renderer>::Theme,
_style: &renderer::Style,
layout: Layout<'_>,
_cursor_position: iced::Point,
_viewport: &iced::Rectangle,
) {
let appearance = theme.appearance(&self.style);
let bounds = layout.bounds();
let button_width = bounds.width / self.state.buttons.len() as f32;
for (num, (key, content)) in self.state.buttons.iter().enumerate() {
let mut bounds = bounds;
bounds.width = button_width;
bounds.x += num as f32 * button_width;
let button_appearance = if self.state.active == key {
appearance.button_active
} else {
appearance.button_inactive
};
let x = bounds.center_x();
let y = bounds.center_y();
// Render the background of the button.
if button_appearance.background.is_some() {
renderer.fill_quad(
renderer::Quad {
bounds,
border_radius: button_appearance.border_radius,
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
button_appearance
.background
.unwrap_or(Background::Color(Color::TRANSPARENT)),
);
}
// Render the bottom border.
if let Some((width, background)) = button_appearance.border_bottom {
let mut bounds = bounds;
bounds.y = bounds.y + bounds.height - width;
bounds.height = width;
renderer.fill_quad(
renderer::Quad {
bounds,
border_radius: BorderRadius::from(0.0),
border_width: 0.0,
border_color: Color::TRANSPARENT,
},
background,
);
}
// Render the text.
renderer.fill_text(iced_native::text::Text {
content: &content.text,
size: f32::from(renderer.default_size()),
bounds: Rectangle { x, y, ..bounds },
color: button_appearance.text_color,
font: Default::default(),
horizontal_alignment: Horizontal::Center,
vertical_alignment: Vertical::Center,
});
}
}
fn overlay<'b>(
&'b self,
_tree: &'b mut Tree,
_layout: iced_native::Layout<'_>,
_renderer: &Renderer,
) -> Option<iced_native::overlay::Element<'b, Message, Renderer>> {
None
}
}
impl<'a, Message, Renderer> From<SegmentedButton<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Renderer: iced_native::Renderer + iced_native::text::Renderer + 'a,
Renderer::Theme: StyleSheet,
Message: 'static + Clone,
{
fn from(widget: SegmentedButton<'a, Message, Renderer>) -> Self {
Self::new(widget)
}
}

View file

@ -0,0 +1,82 @@
/// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use slotmap::{SecondaryMap, SlotMap};
slotmap::new_key_type! {
pub struct Key;
}
/// Contains all state for interacting with a [`SegmentedButton`].
pub struct State<Data> {
pub inner: WidgetState,
pub data: SecondaryState<Data>,
}
impl<Data> Default for State<Data> {
fn default() -> Self {
Self {
inner: WidgetState::default(),
data: SecondaryState::default(),
}
}
}
/// State which is most useful to the widget.
#[derive(Default)]
pub struct WidgetState {
pub buttons: SlotMap<Key, ButtonContent>,
pub active: Key,
}
/// State which is most useful to the application.
pub type SecondaryState<Data> = SecondaryMap<Key, Data>;
impl<Data> State<Data> {
/// The ID of the active button.
#[must_use]
pub fn active(&self) -> Key {
self.inner.active
}
/// Get the application data for the active button.
#[must_use]
pub fn active_data(&self) -> Option<&Data> {
self.data(self.active())
}
/// Get the application data for a button.
#[must_use]
pub fn data(&self, key: Key) -> Option<&Data> {
self.data.get(key)
}
/// Insert a new button.
pub fn insert(&mut self, content: impl Into<ButtonContent>, data: Data) -> Key {
let key = self.inner.buttons.insert(content.into());
self.data.insert(key, data);
key
}
/// Removes a button.
pub fn remove(&mut self, key: Key) -> Option<Data> {
self.inner.buttons.remove(key);
self.data.remove(key)
}
/// Activates this button.
pub fn activate(&mut self, key: Key) {
self.inner.active = key;
}
}
/// Data to be drawn in a [`SegmentedButton`] button.
pub struct ButtonContent {
pub text: String,
}
impl From<String> for ButtonContent {
fn from(text: String) -> Self {
ButtonContent { text }
}
}

View file

@ -0,0 +1,29 @@
/// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use iced_core::{Background, BorderRadius, Color};
/// The appearance of a [`SegmentedButton`].
#[derive(Clone, Copy)]
pub struct Appearance {
pub button_active: ButtonAppearance,
pub button_inactive: ButtonAppearance,
}
/// The appearance of a button in the [`SegmentedButton`]
#[derive(Clone, Copy)]
pub struct ButtonAppearance {
pub background: Option<Background>,
pub border_radius: BorderRadius,
pub border_bottom: Option<(f32, Color)>,
pub text_color: Color,
}
/// Defines the [`Appearance`] of a [`SegmentedButton`].
pub trait StyleSheet {
/// The supported style of the [`StyleSheet`].
type Style: Default;
/// The [`Appearance`] of the [`SegmentedButton`].
fn appearance(&self, style: &Self::Style) -> Appearance;
}