feat: implement SegmentedButton widget
This commit is contained in:
parent
01701759c9
commit
e97c258422
8 changed files with 740 additions and 244 deletions
|
|
@ -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"
|
||||||
|
|
|
||||||
|
|
@ -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 {
|
||||||
|
|
|
||||||
|
|
@ -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()
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -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;
|
||||||
|
|
|
||||||
245
src/widget/segmented_button/mod.rs
Normal file
245
src/widget/segmented_button/mod.rs
Normal 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)
|
||||||
|
}
|
||||||
|
}
|
||||||
82
src/widget/segmented_button/state.rs
Normal file
82
src/widget/segmented_button/state.rs
Normal 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 }
|
||||||
|
}
|
||||||
|
}
|
||||||
29
src/widget/segmented_button/style.rs
Normal file
29
src/widget/segmented_button/style.rs
Normal 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;
|
||||||
|
}
|
||||||
Loading…
Add table
Add a link
Reference in a new issue