From 37f978d1b3625ed8875386938a3092ab77f3ead6 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 6 Dec 2022 16:12:59 +0100 Subject: [PATCH] wip: Various refactorings and improvements --- Cargo.toml | 1 + examples/cosmic/Cargo.toml | 1 + examples/cosmic/src/main.rs | 3 + examples/cosmic/src/window.rs | 213 +++++++-------- src/ext.rs | 19 ++ src/font.rs | 3 + src/lib.rs | 17 +- src/theme/cosmic.rs | 2 + src/theme/expander.rs | 3 + src/theme/mod.rs | 122 +++++---- src/theme/palette.rs | 9 + src/utils.rs | 9 + src/widget/button.rs | 61 ++++- src/widget/expander.rs | 209 --------------- src/widget/header_bar.rs | 215 +++++++-------- src/widget/icon.rs | 101 +++++-- src/widget/list/column.rs | 65 +++++ src/widget/list/item.rs | 2 + src/widget/list/list_box.rs | 452 -------------------------------- src/widget/list/list_row.rs | 18 -- src/widget/list/macros.rs | 146 ----------- src/widget/list/mod.rs | 12 +- src/widget/mod.rs | 25 +- src/widget/nav_button.rs | 61 +++++ src/widget/navigation/macros.rs | 3 + src/widget/navigation/mod.rs | 3 + src/widget/navigation/navbar.rs | 55 ++-- src/widget/scrollable.rs | 19 +- src/widget/separator.rs | 30 ++- src/widget/settings/item.rs | 26 ++ src/widget/settings/mod.rs | 17 ++ src/widget/settings/section.rs | 39 +++ src/widget/toggler.rs | 12 +- 33 files changed, 744 insertions(+), 1229 deletions(-) create mode 100644 src/ext.rs create mode 100644 src/utils.rs delete mode 100644 src/widget/expander.rs create mode 100644 src/widget/list/column.rs create mode 100644 src/widget/list/item.rs delete mode 100644 src/widget/list/list_box.rs delete mode 100644 src/widget/list/list_row.rs delete mode 100644 src/widget/list/macros.rs create mode 100644 src/widget/nav_button.rs create mode 100644 src/widget/settings/item.rs create mode 100644 src/widget/settings/mod.rs create mode 100644 src/widget/settings/section.rs diff --git a/Cargo.toml b/Cargo.toml index 6f89ed9..1f1f1f6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ apply = "0.3.0" derive_setters = "0.1.5" lazy_static = "1.4.0" palette = "0.6.1" +static-rc = "0.6.1" [dependencies.cosmic-theme] git = "https://github.com/pop-os/cosmic-theme.git" diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index bfa5aed..7030459 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -6,4 +6,5 @@ edition = "2021" publish = false [dependencies] +apply = "0.3.0" libcosmic = { path = "../..", features = ["debug"] } diff --git a/examples/cosmic/src/main.rs b/examples/cosmic/src/main.rs index 46ff584..d289e54 100644 --- a/examples/cosmic/src/main.rs +++ b/examples/cosmic/src/main.rs @@ -1,3 +1,6 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + use cosmic::{iced::Application, settings}; mod window; diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index 1a077de..c3b4247 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -1,21 +1,24 @@ -use cosmic::widget::{expander, nav_bar, nav_bar_page, nav_bar_section}; +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + use cosmic::{ iced::widget::{ - checkbox, column, container, horizontal_space, pick_list, progress_bar, radio, row, slider, - text, + column, container, horizontal_space, pick_list, progress_bar, radio, row, slider, }, - iced::{self, Alignment, Application, Color, Command, Length}, + iced::{self, Alignment, Application, Command, Length}, iced_lazy::responsive, - iced_winit::window::{drag, maximize, minimize}, - list_view, list_view_item, list_view_row, list_view_section, scrollable, + iced_winit::window::{drag, toggle_maximize, minimize}, theme::{self, Theme}, - widget::{button, header_bar, list_box, list_row, list_view::*, toggler}, + widget::{button, nav_button, nav_bar, nav_bar_page, nav_bar_section, header_bar, settings, scrollable, toggler}, Element, + ElementExt, }; -use std::collections::BTreeMap; +use std::{collections::BTreeMap, vec}; +use theme::Button as ButtonTheme; #[derive(Default)] pub struct Window { + title: String, page: u8, debug: bool, theme: Theme, @@ -79,11 +82,12 @@ impl Application for Window { window.slider_value = 50.0; // window.theme = Theme::Light; window.pick_list_selected = Some("Option 1"); + window.title = String::from("COSMIC Design System - Iced"); (window, Command::none()) } fn title(&self) -> String { - String::from("COSMIC Design System - Iced") + self.title.clone() } fn update(&mut self, message: Message) -> iced::Command { @@ -99,8 +103,8 @@ impl Application for Window { Message::Close => self.exit = true, Message::ToggleSidebar => self.sidebar_toggled = !self.sidebar_toggled, Message::Drag => return drag(), - Message::Minimize => return minimize(), - Message::Maximize => return maximize(), + Message::Minimize => return minimize(true), + Message::Maximize => return toggle_maximize(), Message::RowSelected(row) => println!("Selected row {row}"), } @@ -108,23 +112,27 @@ impl Application for Window { } fn view(&self) -> Element { - let mut header: Element = header_bar() - .title(self.title()) - .nav_title(String::from("Settings")) - .sidebar_active(self.sidebar_toggled) - .show_minimize(self.show_minimize) - .show_maximize(self.show_maximize) + let mut header = header_bar() + .title("COSMIC Design System - Iced") .on_close(Message::Close) .on_drag(Message::Drag) - .on_maximize(Message::Maximize) - .on_minimize(Message::Minimize) - .on_sidebar_toggle(Message::ToggleSidebar) - .into(); + .start( + nav_button("Settings") + .on_sidebar_toggled(Message::ToggleSidebar) + .sidebar_active(self.sidebar_toggled) + .into() + ); - if self.debug { - header = header.explain(Color::WHITE); + if self.show_maximize { + header = header.on_maximize(Message::Maximize); } + if self.show_minimize { + header = header.on_minimize(Message::Minimize); + } + + let header = Into::>::into(header).debug(self.debug); + // TODO: Adding responsive makes this regenerate on every size change, and regeneration // involves allocations for many different items. Ideally, we could only make the nav bar // responsive and leave the content to be sized normally. @@ -132,26 +140,26 @@ impl Application for Window { let condensed = size.width < 900.0; // cosmic::navbar![ - // nav_button!("network-wireless", "Network & Wireless", condensed) + // nav_text_button("network-wireless", "Network & Wireless", condensed) // .on_press(Message::Page(0)) // .style(if self.page == 0 { - // theme::Button::Primary + // ButtonTheme::Primary // } else { - // theme::Button::Text + // ButtonTheme::Text // }), - // nav_button!("preferences-desktop", "Bluetooth", condensed) + // nav_text_button("preferences-desktop", "Bluetooth", condensed) // .on_press(Message::Page(1)) // .style(if self.page == 1 { - // theme::Button::Primary + // ButtonTheme::Primary // } else { - // theme::Button::Text + // ButtonTheme::Text // }), - // nav_button!("system-software-update", "Personalization", condensed) + // nav_text_button("system-software-update", "Personalization", condensed) // .on_press(Message::Page(2)) // .style(if self.page == 2 { - // theme::Button::Primary + // ButtonTheme::Primary // } else { - // theme::Button::Text + // ButtonTheme::Text // }), // ] @@ -219,49 +227,48 @@ impl Application for Window { }, ); - let content: Element<_> = list_view!( - list_view_section!( - "Debug", - list_view_item!("Debug theme", choose_theme), - list_view_item!( + let content: Element<_> = settings::view_column(vec![ + settings::view_section("Debug") + .add(settings::item("Debug theme", choose_theme)) + .add(settings::item( "Debug layout", - toggler(String::from("Debug layout"), self.debug, Message::Debug,) - ) - ), - list_view_section!( - "Buttons", - list_view_row!( - button!("Primary") - .style(theme::Button::Primary) - .on_press(Message::ButtonPressed), - button!("Secondary") - .style(theme::Button::Secondary) - .on_press(Message::ButtonPressed), - button!("Positive") - .style(theme::Button::Positive) - .on_press(Message::ButtonPressed), - button!("Destructive") - .style(theme::Button::Destructive) - .on_press(Message::ButtonPressed), - button!("Text") - .style(theme::Button::Text) - .on_press(Message::ButtonPressed), - ), - list_view_row!( - button!("Primary").style(theme::Button::Primary), - button!("Secondary").style(theme::Button::Secondary), - button!("Positive").style(theme::Button::Positive), - button!("Destructive").style(theme::Button::Destructive), - button!("Text").style(theme::Button::Text), - ), - ), - list_view_section!( - "Controls", - list_view_item!( - "Toggler", - toggler(None, self.toggler_value, Message::TogglerToggled) - ), - list_view_item!( + toggler(String::from("Debug layout"), self.debug, Message::Debug) + )) + .into(), + settings::view_section("Buttons") + .add(settings::item_row(vec![ + 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(), + settings::view_section("Controls") + .add(settings::item("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",], @@ -269,68 +276,30 @@ impl Application for Window { Message::PickListSelected ) .padding([8, 0, 8, 16]) - ), - list_view_item!( + )) + .add(settings::item( "Slider", slider(0.0..=100.0, self.slider_value, Message::SliderChanged) .width(Length::Units(250)) - ), - list_view_item!( + )) + .add(settings::item( "Progress", progress_bar(0.0..=100.0, self.slider_value) .width(Length::Units(250)) .height(Length::Units(4)) - ), - checkbox("Checkbox", self.checkbox_value, Message::CheckboxToggled), - ), - list_view_section!( - "Expander", - expander() - .title("Label") - .subtitle("Caption") - .icon(String::from("edit-paste")) - .on_row_selected(Box::new(Message::RowSelected)) - .rows(vec![ - list_row() - .title("Label") - .subtitle("Caption") - .icon(String::from("help-about")), - list_row().subtitle("Caption").title("Label"), - list_row().title("Label") - ]) - ), - list_view_section!( - "List Box", - list_box() - .style(theme::Container::Custom(list_section_style)) - .children(vec![ - cosmic::list_box_row!("Title").into(), - cosmic::list_box_row!("Title", "Subtitle").into(), - cosmic::list_box_row!("Title", "", "edit-paste").into(), - cosmic::list_box_row!("", "Subtitle", "edit-paste").into(), - cosmic::list_box_row!("Title", "Subtitle", "edit-paste").into() - ]) - .render() - ), - ) + )) + .into() + ]) .into(); let mut widgets = Vec::with_capacity(2); - widgets.push(if self.debug { - sidebar.explain(Color::WHITE) - } else { - sidebar - }); + widgets.push(sidebar.debug(self.debug)); widgets.push( - scrollable!(row![ + scrollable(row![ horizontal_space(Length::Fill), - if self.debug { - content.explain(Color::WHITE) - } else { - content - }, + content.debug(self.debug), horizontal_space(Length::Fill), ]) .into(), diff --git a/src/ext.rs b/src/ext.rs new file mode 100644 index 0000000..1897d57 --- /dev/null +++ b/src/ext.rs @@ -0,0 +1,19 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use iced::Color; + +pub trait ElementExt { + #[must_use] + fn debug(self, debug: bool) -> Self; +} + +impl<'a, Message: 'static> ElementExt for crate::Element<'a, Message> { + fn debug(self, debug: bool) -> Self { + if debug { + self.explain(Color::WHITE) + } else { + self + } + } +} \ No newline at end of file diff --git a/src/font.rs b/src/font.rs index c9148df..e3f9698 100644 --- a/src/font.rs +++ b/src/font.rs @@ -1,3 +1,6 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + pub use iced::Font; pub const FONT: Font = Font::External { diff --git a/src/lib.rs b/src/lib.rs index c6dd88f..e941de9 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,3 +1,6 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + pub use iced; pub use iced_lazy; pub use iced_native; @@ -8,19 +11,15 @@ pub mod font; pub mod theme; pub mod widget; +mod ext; +pub use ext::ElementExt; + +mod utils; + pub use theme::Theme; pub type Renderer = iced::Renderer; pub type Element<'a, Message> = iced::Element<'a, Message, Renderer>; -#[derive(Clone, Copy, Debug)] -pub enum WindowMsg { - Close, - Drag, - Minimize, - Maximize, - ToggleSidebar, -} - pub fn settings() -> iced::Settings { let mut settings = iced::Settings::default(); settings.default_font = match font::FONT { diff --git a/src/theme/cosmic.rs b/src/theme/cosmic.rs index e69de29..a016b62 100644 --- a/src/theme/cosmic.rs +++ b/src/theme/cosmic.rs @@ -0,0 +1,2 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 \ No newline at end of file diff --git a/src/theme/expander.rs b/src/theme/expander.rs index d43dea9..b2b736e 100644 --- a/src/theme/expander.rs +++ b/src/theme/expander.rs @@ -1,3 +1,6 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + use iced_core::{Background, Color}; /// The appearance of a [`Expander`](crate::native::expander::Expander). diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 2775f3a..80296ab 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -1,6 +1,12 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + pub mod expander; pub mod palette; +use std::hash::Hash; +use std::hash::Hasher; + pub use self::palette::Palette; use iced_style::application; @@ -39,6 +45,7 @@ pub enum Theme { } impl Theme { + #[must_use] pub fn cosmic(self) -> &'static CosmicTheme { match self { Self::Dark => &COSMIC_DARK, @@ -46,6 +53,7 @@ impl Theme { } } + #[must_use] pub fn palette(self) -> Palette { match self { Self::Dark => Palette::DARK, @@ -53,6 +61,7 @@ impl Theme { } } + #[must_use] pub fn extended_palette(&self) -> &self::palette::Extended { match self { Self::Dark => &self::palette::EXTENDED_DARK, @@ -82,7 +91,7 @@ impl Default for Application { impl application::StyleSheet for Theme { type Style = Application; - fn appearance(&self, style: Self::Style) -> application::Appearance { + fn appearance(&self, style: &Self::Style) -> application::Appearance { let cosmic = self.cosmic(); match style { @@ -100,10 +109,11 @@ impl application::StyleSheet for Theme { */ #[derive(Debug, Clone, Copy, PartialEq, Eq)] pub enum Button { + Deactivated, + Destructive, + Positive, Primary, Secondary, - Positive, - Destructive, Text, } @@ -122,6 +132,7 @@ impl Button { Button::Positive => &cosmic.success, Button::Destructive => &cosmic.destructive, Button::Text => &cosmic.secondary.component, + Button::Deactivated => &cosmic.secondary.component, } } } @@ -129,7 +140,7 @@ impl Button { impl button::StyleSheet for Theme { type Style = Button; - fn active(&self, style: Self::Style) -> button::Appearance { + fn active(&self, style: &Self::Style) -> button::Appearance { let cosmic = style.cosmic(self); button::Appearance { @@ -143,8 +154,8 @@ impl button::StyleSheet for Theme { } } - fn hovered(&self, style: Self::Style) -> button::Appearance { - let active = self.active(style); + fn hovered(&self, style: &Self::Style) -> button::Appearance { + let active = self.active(&style); let cosmic = style.cosmic(self); button::Appearance { @@ -176,7 +187,7 @@ impl checkbox::StyleSheet for Theme { fn active( &self, - style: Self::Style, + style: &Self::Style, is_checked: bool, ) -> checkbox::Appearance { let palette = self.extended_palette(); @@ -211,7 +222,7 @@ impl checkbox::StyleSheet for Theme { fn hovered( &self, - style: Self::Style, + style: &Self::Style, is_checked: bool, ) -> checkbox::Appearance { let palette = self.extended_palette(); @@ -288,7 +299,7 @@ impl expander::StyleSheet for Theme { fn appearance(&self, style: Self::Style) -> expander::Appearance { match style { - Expander::Default => Default::default(), + Expander::Default => expander::Appearance::default(), Expander::Custom(f) => f(self), } } @@ -319,9 +330,9 @@ impl From container::Appearance> for Container { impl container::StyleSheet for Theme { type Style = Container; - fn appearance(&self, style: Self::Style) -> container::Appearance { + fn appearance(&self, style: &Self::Style) -> container::Appearance { match style { - Container::Transparent => Default::default(), + Container::Transparent => container::Appearance::default(), Container::Box => { let palette = self.extended_palette(); @@ -344,7 +355,7 @@ impl container::StyleSheet for Theme { impl slider::StyleSheet for Theme { type Style = (); - fn active(&self, _style: Self::Style) -> slider::Appearance { + fn active(&self, _style: &Self::Style) -> slider::Appearance { let cosmic = self.cosmic(); //TODO: no way to set rail thickness @@ -365,8 +376,8 @@ impl slider::StyleSheet for Theme { } } - fn hovered(&self, style: Self::Style) -> slider::Appearance { - let mut style = self.active(style); + fn hovered(&self, style: &Self::Style) -> slider::Appearance { + let mut style = self.active(&style); style.handle.shape = slider::HandleShape::Circle { radius: 16.0 }; @@ -378,8 +389,8 @@ impl slider::StyleSheet for Theme { style } - fn dragging(&self, style: Self::Style) -> slider::Appearance { - let mut style = self.hovered(style); + fn dragging(&self, style: &Self::Style) -> slider::Appearance { + let mut style = self.hovered(&style); style.handle.border_color = match self { Theme::Dark => Color::from_rgba8(0xFF, 0xFF, 0xFF, 0.2), Theme::Light => Color::from_rgba8(0, 0, 0, 0.2), @@ -394,7 +405,7 @@ impl slider::StyleSheet for Theme { impl menu::StyleSheet for Theme { type Style = (); - fn appearance(&self, _style: Self::Style) -> menu::Appearance { + fn appearance(&self, _style: &Self::Style) -> menu::Appearance { let cosmic = self.cosmic(); menu::Appearance { @@ -415,7 +426,7 @@ impl menu::StyleSheet for Theme { impl pick_list::StyleSheet for Theme { type Style = (); - fn active(&self, _style: ()) -> pick_list::Appearance { + fn active(&self, _style: &()) -> pick_list::Appearance { let cosmic = &self.cosmic().primary.component; pick_list::Appearance { @@ -429,12 +440,12 @@ impl pick_list::StyleSheet for Theme { } } - fn hovered(&self, style: ()) -> pick_list::Appearance { + fn hovered(&self, style: &()) -> pick_list::Appearance { let cosmic = &self.cosmic().primary.component; pick_list::Appearance { background: Background::Color(cosmic.hover.into()), - ..self.active(style) + ..self.active(&style) } } } @@ -445,7 +456,7 @@ impl pick_list::StyleSheet for Theme { impl radio::StyleSheet for Theme { type Style = (); - fn active(&self, _style: Self::Style) -> radio::Appearance { + fn active(&self, _style: &Self::Style, is_selected: bool) -> radio::Appearance { let palette = self.extended_palette(); radio::Appearance { @@ -457,8 +468,8 @@ impl radio::StyleSheet for Theme { } } - fn hovered(&self, style: Self::Style) -> radio::Appearance { - let active = self.active(style); + fn hovered(&self, style: &Self::Style, is_selected: bool) -> radio::Appearance { + let active = self.active(&style, is_selected); let palette = self.extended_palette(); radio::Appearance { @@ -477,7 +488,7 @@ impl toggler::StyleSheet for Theme { fn active( &self, - _style: Self::Style, + _style: &Self::Style, is_active: bool, ) -> toggler::Appearance { let palette = self.palette(); @@ -504,7 +515,7 @@ impl toggler::StyleSheet for Theme { fn hovered( &self, - style: Self::Style, + style: &Self::Style, is_active: bool, ) -> toggler::Appearance { //TODO: grab colors from palette @@ -515,7 +526,7 @@ impl toggler::StyleSheet for Theme { } else { Color::from_rgb8(0xb6, 0xb6, 0xb6) }, - ..self.active(style, is_active) + ..self.active(&style, is_active) }, Theme::Light => toggler::Appearance { background: if is_active { @@ -523,7 +534,7 @@ impl toggler::StyleSheet for Theme { } else { Color::from_rgb8(0x54, 0x54, 0x54) }, - ..self.active(style, is_active) + ..self.active(&style, is_active) } } } @@ -535,7 +546,7 @@ impl toggler::StyleSheet for Theme { impl pane_grid::StyleSheet for Theme { type Style = (); - fn picked_split(&self, _style: Self::Style) -> Option { + fn picked_split(&self, _style: &Self::Style) -> Option { let palette = self.extended_palette(); Some(pane_grid::Line { @@ -544,7 +555,7 @@ impl pane_grid::StyleSheet for Theme { }) } - fn hovered_split(&self, _style: Self::Style) -> Option { + fn hovered_split(&self, _style: &Self::Style) -> Option { let palette = self.extended_palette(); Some(pane_grid::Line { @@ -574,7 +585,7 @@ impl Default for ProgressBar { impl progress_bar::StyleSheet for Theme { type Style = ProgressBar; - fn appearance(&self, style: Self::Style) -> progress_bar::Appearance { + fn appearance(&self, style: &Self::Style) -> progress_bar::Appearance { let palette = self.extended_palette(); let from_palette = |bar: Color| progress_bar::Appearance { @@ -610,7 +621,7 @@ impl Default for Rule { impl rule::StyleSheet for Theme { type Style = Rule; - fn style(&self, style: Self::Style) -> rule::Appearance { + fn appearance(&self, style: &Self::Style) -> rule::Appearance { let palette = self.extended_palette(); match style { @@ -631,7 +642,7 @@ impl rule::StyleSheet for Theme { impl scrollable::StyleSheet for Theme { type Style = (); - fn active(&self, _style: Self::Style) -> scrollable::Scrollbar { + fn active(&self, _style: &Self::Style) -> scrollable::Scrollbar { let palette = self.extended_palette(); scrollable::Scrollbar { @@ -648,7 +659,7 @@ impl scrollable::StyleSheet for Theme { } } - fn hovered(&self, _style: Self::Style) -> scrollable::Scrollbar { + fn hovered(&self, _style: &Self::Style) -> scrollable::Scrollbar { let palette = self.extended_palette(); scrollable::Scrollbar { @@ -669,23 +680,42 @@ impl scrollable::StyleSheet for Theme { #[derive(Default, Clone, Copy)] pub enum Svg { + /// Apply a custom appearance filter Custom(fn(&Theme) -> svg::Appearance), + /// No filtering is applied #[default] Default, - Accent, + /// Icon fill color will match text color + Symbolic, + /// Icon fill color will match accent color + SymbolicActive, +} + +impl Hash for Svg { + fn hash(&self, state: &mut H) { + let id = match self { + Svg::Custom(_) => 0, + Svg::Default => 1, + Svg::Symbolic => 2, + Svg::SymbolicActive => 3 + }; + + id.hash(state); + } } impl svg::StyleSheet for Theme { type Style = Svg; fn appearance(&self, style: Self::Style) -> svg::Appearance { - let cosmic = self.cosmic(); - match style { - Svg::Default => Default::default(), + Svg::Default => svg::Appearance::default(), Svg::Custom(appearance) => appearance(self), - Svg::Accent => svg::Appearance { - fill: Some(cosmic.accent.base.into()), + Svg::Symbolic => svg::Appearance { + fill: Some(self.extended_palette().background.base.text), + }, + Svg::SymbolicActive => svg::Appearance { + fill: Some(self.cosmic().accent.base.into()), }, } } @@ -717,7 +747,7 @@ impl text::StyleSheet for Theme { Text::Accent => text::Appearance { color: Some(self.cosmic().accent.base.into()) }, - Text::Default => Default::default(), + Text::Default => text::Appearance::default(), Text::Color(c) => text::Appearance { color: Some(c) }, Text::Custom(f) => f(self), } @@ -730,7 +760,7 @@ impl text::StyleSheet for Theme { impl text_input::StyleSheet for Theme { type Style = (); - fn active(&self, _style: Self::Style) -> text_input::Appearance { + fn active(&self, _style: &Self::Style) -> text_input::Appearance { let palette = self.extended_palette(); text_input::Appearance { @@ -741,7 +771,7 @@ impl text_input::StyleSheet for Theme { } } - fn hovered(&self, _style: Self::Style) -> text_input::Appearance { + fn hovered(&self, _style: &Self::Style) -> text_input::Appearance { let palette = self.extended_palette(); text_input::Appearance { @@ -752,7 +782,7 @@ impl text_input::StyleSheet for Theme { } } - fn focused(&self, _style: Self::Style) -> text_input::Appearance { + fn focused(&self, _style: &Self::Style) -> text_input::Appearance { let palette = self.extended_palette(); text_input::Appearance { @@ -763,19 +793,19 @@ impl text_input::StyleSheet for Theme { } } - fn placeholder_color(&self, _style: Self::Style) -> Color { + fn placeholder_color(&self, _style: &Self::Style) -> Color { let palette = self.extended_palette(); palette.background.strong.color } - fn value_color(&self, _style: Self::Style) -> Color { + fn value_color(&self, _style: &Self::Style) -> Color { let palette = self.extended_palette(); palette.background.base.text } - fn selection_color(&self, _style: Self::Style) -> Color { + fn selection_color(&self, _style: &Self::Style) -> Color { let palette = self.extended_palette(); palette.primary.weak.color diff --git a/src/theme/palette.rs b/src/theme/palette.rs index 1f8a660..9715c5a 100644 --- a/src/theme/palette.rs +++ b/src/theme/palette.rs @@ -1,3 +1,6 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + //TODO: GET CORRECT PALETTE FROM COSMIC-THEME use iced_core::Color; @@ -86,6 +89,7 @@ lazy_static! { } impl Extended { + #[must_use] pub fn generate(palette: Palette) -> Self { Self { background: Background::new(palette.background, palette.text), @@ -131,6 +135,7 @@ pub struct Background { } impl Background { + #[must_use] pub fn new(base: Color, text: Color) -> Self { let weak = mix(base, text, 0.15); let strong = mix(base, text, 0.40); @@ -150,6 +155,7 @@ pub struct Primary { } impl Primary { + #[must_use] pub fn generate(base: Color, background: Color, text: Color) -> Self { let weak = mix(base, background, 0.4); let strong = deviate(base, 0.1); @@ -169,6 +175,7 @@ pub struct Secondary { } impl Secondary { + #[must_use] pub fn generate(base: Color, text: Color) -> Self { let base = mix(base, text, 0.2); let weak = mix(base, text, 0.1); @@ -189,6 +196,7 @@ pub struct Success { } impl Success { + #[must_use] pub fn generate(base: Color, background: Color, text: Color) -> Self { let weak = mix(base, background, 0.4); let strong = deviate(base, 0.1); @@ -208,6 +216,7 @@ pub struct Danger { } impl Danger { + #[must_use] pub fn generate(base: Color, background: Color, text: Color) -> Self { let weak = mix(base, background, 0.4); let strong = deviate(base, 0.1); diff --git a/src/utils.rs b/src/utils.rs new file mode 100644 index 0000000..6bcfaaf --- /dev/null +++ b/src/utils.rs @@ -0,0 +1,9 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use static_rc::StaticRc; + +/// Uses [`StaticRc`] to create two halves of value with shared ownership, with no runtime reference counting required. +pub(crate) fn static_rc_halves(value: T) -> (StaticRc, StaticRc) { + StaticRc::split::<1, 2>(StaticRc::::new(value)) +} \ No newline at end of file diff --git a/src/widget/button.rs b/src/widget/button.rs index 7a1de22..7193077 100644 --- a/src/widget/button.rs +++ b/src/widget/button.rs @@ -1,13 +1,50 @@ -#[macro_export] -macro_rules! button { - ($($x:expr),+ $(,)?) => ( - $crate::iced::widget::Button::new( - $crate::iced::widget::Row::with_children( - vec![$($crate::iced::Element::from($x)),+] - ) - .spacing(8) - ) - .padding([8, 16]) - ); +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{theme, Element, Renderer}; +use iced::widget; + +/// A button widget with COSMIC styling +#[must_use] +pub const fn button(style: theme::Button) -> Button { + Button { style, message: None } } -pub use button; + +/// A button widget with COSMIC styling +pub struct Button { + style: theme::Button, + message: Option, +} + +impl Button { + /// The message to emit on button press. + #[must_use] + pub fn on_press(mut self, message: Message) -> Self { + self.message = Some(message); + self + } + + /// A button with an icon. + pub fn icon(self, style: theme::Svg, icon: &str, size: u16) -> widget::Button { + self.custom(vec![super::icon(icon, size).style(style).into()]) + } + + /// A button with text. + pub fn text(self, text: &str) -> widget::Button { + self.custom(vec![text.into()]) + } + + /// A custom button that has the desired default spacing and padding. + pub fn custom(self, children: Vec>) -> widget::Button { + let button = widget::button(widget::row(children).spacing(8)) + .style(self.style) + .padding([8, 16]); + + if let Some(message) = self.message { + button.on_press(message) + } else { + button + } + } +} + diff --git a/src/widget/expander.rs b/src/widget/expander.rs deleted file mode 100644 index 5a9cfac..0000000 --- a/src/widget/expander.rs +++ /dev/null @@ -1,209 +0,0 @@ -use std::vec; - -use crate::{list_box_row, separator, theme, widget::ListRow, Element, Renderer, Theme}; -use apply::Apply; -use derive_setters::Setters; -use iced::{ - widget::{self, button, container, horizontal_space, row, text, Column}, - Alignment, Background, Length, -}; -use iced_lazy::Component; -use iced_native::widget::{column, event_container}; - -#[derive(Setters)] -pub struct Expander<'a, Message> { - title: &'a str, - #[setters(strip_option)] - subtitle: Option<&'a str>, - #[setters(strip_option)] - icon: Option, - expansible: bool, - #[setters(skip)] - rows: Option>>, - #[setters(strip_option)] - on_row_selected: Option Message + 'a>>, -} - -pub fn expander<'a, Message>() -> Expander<'a, Message> { - Expander { - title: "", - subtitle: None, - icon: None, - expansible: false, - rows: None, - on_row_selected: None, - } -} - -pub struct ExpanderState { - pub expanded: bool, -} - -impl Default for ExpanderState { - fn default() -> Self { - Self { expanded: true } - } -} - -#[derive(Clone, Copy, Debug)] -pub enum ExpanderEvent { - Expand, - RowSelected(usize), -} - -impl<'a, Message> Expander<'a, Message> { - pub fn rows(mut self, rows: Vec>) -> Self { - self.rows = Some(rows); - self.expansible = true; - self - } - - pub fn push(&mut self, row: ListRow<'a>) { - if self.rows.is_none() { - self.rows = Some(vec![]) - } - self.rows.as_mut().unwrap().push(row); - } -} - -impl<'a, Message: Clone + 'a> Component for Expander<'a, Message> { - type State = ExpanderState; - - type Event = ExpanderEvent; - - fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option { - match event { - ExpanderEvent::Expand => { - state.expanded = !state.expanded; - None - } - ExpanderEvent::RowSelected(index) => self - .on_row_selected - .as_ref() - .map(|on_row_selected| (on_row_selected)(index)), - } - } - - fn view(&self, state: &Self::State) -> Element { - let heading: Element = { - let mut captions = vec![text(&self.title).size(18).into()]; - if let Some(subtitle) = &self.subtitle { - captions.push(text(subtitle).size(16).into()); - } - let text = column(captions); - let space: Element = horizontal_space(Length::Fill).into(); - let toggler: Element = { - let mut icon = super::icon( - if state.expanded { - "go-down-symbolic" - } else { - "go-next-symbolic" - }, - 16, - ) - .apply(button) - .width(Length::Units(25)); - if self.expansible { - icon = icon.on_press(ExpanderEvent::Expand); - } - icon.into() - }; - - let items = if let Some(icon) = &self.icon { - let icon = super::icon(icon.as_str(), 20) - .apply(event_container) - .padding(10); - row![icon, text, space, toggler] - } else { - row![text, space, toggler] - }; - - container(items.align_items(Alignment::Center)) - .style(theme::Container::Custom(expander_heading_style)) - .padding(10) - .into() - }; - - let rows: Vec> = if let Some(rows) = &self.rows { - rows.iter() - .enumerate() - .map(|(index, row)| { - let subtitle = row.subtitle.unwrap_or_default(); - if let Some(icon) = &row.icon { - list_box_row!(row.title, subtitle, icon.as_str()) - .apply(event_container) - .on_press(ExpanderEvent::RowSelected(index)) - .into() - } else { - list_box_row!(row.title, subtitle) - .apply(event_container) - .on_press(ExpanderEvent::RowSelected(index)) - .into() - } - }) - .enumerate() - .flat_map(|(index, child)| { - if index != rows.len() - 1 { - vec![child, separator!(1).into()] - } else { - vec![child] - } - }) - .collect() - } else { - vec![] - }; - - let rows: Element = Column::with_children(rows).into(); - - let mut layout = vec![heading]; - if state.expanded && self.expansible { - layout.push(rows) - } - - column(layout) - .apply(widget::container) - .height(Length::Shrink) - .style(theme::Container::Custom(expander_row_style)) - .into() - } -} - -impl<'a, Message: Clone + 'a> From> for Element<'a, Message> { - fn from(expander: Expander<'a, Message>) -> Self { - iced_lazy::component(expander) - } -} - -pub fn expander_heading_style(theme: &Theme) -> widget::container::Appearance { - let primary = &theme.cosmic().primary; - let accent = &theme.cosmic().accent; - widget::container::Appearance { - text_color: Some(accent.base.into()), - background: Some(Background::Color(primary.divider.into())), - border_radius: 8.0, - border_width: 0.0, - border_color: primary.on.into(), - } -} - -pub fn expander_row_style(theme: &Theme) -> widget::container::Appearance { - let cosmic = &theme.cosmic().primary; - widget::container::Appearance { - text_color: Some(cosmic.on.into()), - background: Some(Background::Color(cosmic.base.into())), - border_radius: 8.0, - border_width: 0.4, - border_color: cosmic.divider.into(), - } -} - -pub fn separator_style(theme: &Theme) -> widget::rule::Appearance { - let cosmic = &theme.cosmic().primary; - widget::rule::Appearance { - color: cosmic.divider.into(), - width: 1, - radius: 0.0, - fill_mode: widget::rule::FillMode::Padded(10), - } -} diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 7fe7381..1c6b26e 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -1,16 +1,28 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + use apply::Apply; -use derive_setters::*; -use iced::{self, alignment::Vertical, widget, Length}; -use iced_lazy::Component; -use crate::{theme, Element, Renderer}; +use derive_setters::Setters; +use iced::{self, widget, Length}; +use crate::{theme, Element}; + +#[must_use] +pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { + HeaderBar { + title: "", + on_close: None, + on_drag: None, + on_maximize: None, + on_minimize: None, + start: None, + center: None, + end: None, + } +} #[derive(Setters)] -pub struct HeaderBar { - title: String, - nav_title: String, - sidebar_active: bool, - show_minimize: bool, - show_maximize: bool, +pub struct HeaderBar<'a, Message> { + title: &'a str, #[setters(strip_option)] on_close: Option, #[setters(strip_option)] @@ -20,133 +32,98 @@ pub struct HeaderBar { #[setters(strip_option)] on_minimize: Option, #[setters(strip_option)] - on_sidebar_toggle: Option, + start: Option>, + #[setters(strip_option)] + center: Option>, + #[setters(strip_option)] + end: Option> } -pub fn header_bar() -> HeaderBar { - HeaderBar { - title: String::default(), - nav_title: String::default(), - sidebar_active: false, - show_minimize: false, - show_maximize: false, - on_sidebar_toggle: None, - on_close: None, - on_drag: None, - on_maximize: None, - on_minimize: None, - } -} +impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { + /// Converts the headerbar builder into an Iced element. + pub fn into_element(mut self) -> Element<'a, Message> { + let mut packed: Vec> = Vec::with_capacity(4); -#[derive(Debug, Clone)] -pub enum HeaderEvent { - Close, - ToggleSidebar, - Drag, - Minimize, - Maximize, -} - -impl Component for HeaderBar { - type State = (); - - type Event = HeaderEvent; - - fn update(&mut self, _state: &mut Self::State, event: Self::Event) -> Option { - match event { - HeaderEvent::Close => self.on_close.clone(), - - HeaderEvent::ToggleSidebar => self.on_sidebar_toggle.clone(), - - HeaderEvent::Drag => self.on_drag.clone(), - - HeaderEvent::Maximize => self.on_maximize.clone(), - - HeaderEvent::Minimize => self.on_minimize.clone(), + if let Some(start) = self.start.take() { + packed.push(widget::container(start).align_x(iced::alignment::Horizontal::Left).into()); } + + packed.push(if let Some(center) = self.center.take() { + widget::container(center).align_x(iced::alignment::Horizontal::Center).into() + } else { + self.title_widget() + }); + + packed.push(if let Some(end) = self.end.take() { + widget::row(vec![end, self.window_controls()]) + .apply(widget::container) + .align_x(iced::alignment::Horizontal::Right) + .into() + } else { + self.window_controls() + }); + + let mut widget = widget::row(packed) + .height(Length::Units(50)) + .padding(10) + .apply(widget::event_container) + .center_y(); + + if let Some(message) = self.on_drag.clone() { + widget = widget.on_press(message); + } + + if let Some(message) = self.on_maximize.clone() { + widget = widget.on_release(message); + } + + widget.into() } - fn view(&self, _state: &Self::State) -> Element { - let nav_button = { - let text = widget::text(&self.nav_title) - .style(theme::Text::Accent) - .vertical_alignment(Vertical::Center) - .width(Length::Shrink) - .height(Length::Fill); - - let icon = super::icon( - if self.sidebar_active { - "go-previous-symbolic" - } else { - "go-next-symbolic" - }, - 24, - ) - .style(theme::Svg::Accent) - .width(Length::Units(24)) - .height(Length::Fill); - - widget::row!(text, icon) - .padding(4) - .spacing(4) - .apply(widget::button) - .style(theme::Button::Secondary) - .on_press(HeaderEvent::ToggleSidebar) - .apply(widget::container) - .center_y() - .height(Length::Fill) - .into() - }; - - let content = widget::container(widget::text(&self.title)) + fn title_widget(&self) -> Element<'a, Message> { + widget::container(widget::text(self.title)) .center_x() .center_y() .width(Length::Fill) .height(Length::Fill) - .into(); + .into() + } - let window_controls = { - let mut widgets: Vec> = Vec::with_capacity(3); + /// Creates the widget for window controls. + fn window_controls(&mut self) -> Element<'a, Message> { + let mut widgets: Vec> = Vec::with_capacity(3); - let icon = |name, size, on_press| { - super::icon(name, size) - .style(crate::theme::Svg::Accent) - .apply(widget::button) - .style(theme::Button::Text) - .on_press(on_press) - }; - - if self.show_minimize { - widgets.push(icon("window-minimize-symbolic", 16, HeaderEvent::Minimize).into()); - } - - if self.show_maximize { - widgets.push(icon("window-maximize-symbolic", 16, HeaderEvent::Maximize).into()); - } - - widgets.push(icon("window-close-symbolic", 16, HeaderEvent::Close).into()); - - widget::row(widgets) - .spacing(8) - .apply(widget::container) - .height(Length::Fill) - .center_y() - .into() + let icon = |name, size, on_press| { + super::icon(name, size) + .style(crate::theme::Svg::SymbolicActive) + .apply(iced::widget::button) + .style(theme::Button::Text) + .on_press(on_press) }; - widget::row(vec![nav_button, content, window_controls]) - .height(Length::Units(50)) - .padding(10) - .apply(widget::event_container) + if let Some(message) = self.on_minimize.take() { + widgets.push(icon("window-minimize-symbolic", 16, message).into()); + } + + if let Some(message) = self.on_maximize.take() { + widgets.push(icon("window-maximize-symbolic", 16, message).into()); + } + + if let Some(message) = self.on_close.take() { + widgets.push(icon("window-close-symbolic", 16, message).into()); + } + + widget::row(widgets) + .spacing(8) + .apply(widget::container) + .height(Length::Fill) .center_y() - .on_press(HeaderEvent::Drag) - .on_release(HeaderEvent::Maximize) .into() } } -impl<'a, Message: Clone + 'a> From> for Element<'a, Message> { - fn from(header_bar: HeaderBar) -> Self { - iced_lazy::component(header_bar) +impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { + fn from(headerbar: HeaderBar<'a, Message>) -> Self { + headerbar.into_element() } } diff --git a/src/widget/icon.rs b/src/widget/icon.rs index 92399ab..b480768 100644 --- a/src/widget/icon.rs +++ b/src/widget/icon.rs @@ -1,24 +1,81 @@ -use iced::{widget::svg, Length}; +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 -pub fn icon(name: &str, size: u16) -> svg::Svg -where - Renderer: iced_native::svg::Renderer, - Renderer::Theme: iced_native::svg::StyleSheet, -{ - let handle = match freedesktop_icons::lookup(name) - .with_size(size) - .with_theme("Pop") - .with_cache() - .force_svg() - .find() - { - Some(path) => svg::Handle::from_path(path), - None => { - eprintln!("icon '{}' size {} not found", name, size); - svg::Handle::from_memory(Vec::new()) - } - }; - svg::Svg::new(handle) - .width(Length::Units(size)) - .height(Length::Units(size)) +//! Lazily-generated SVG icon widget for Iced. + +use std::borrow::Cow; +use std::hash::Hash; +use derive_setters::Setters; +use crate::{Element, Renderer}; +use iced::{widget::svg, Length, ContentFit}; + +/// A lazily-generated SVG icon. +#[derive(Hash, Setters)] +pub struct Icon<'a> { + #[setters(skip)] + name: Cow<'a, str>, + #[setters(into)] + theme: Cow<'a, str>, + style: crate::theme::Svg, + size: u16, + #[setters(strip_option)] + content_fit: Option, + #[setters(strip_option)] + width: Option, + #[setters(strip_option)] + height: Option, } + +/// A lazily-generated SVG icon. +#[must_use] +pub fn icon<'a>(name: impl Into>, size: u16) -> Icon<'a> { + Icon { + content_fit: None, + height: None, + name: name.into(), + size, + style: crate::theme::Svg::default(), + theme: Cow::Borrowed("Pop"), + width: None, + } +} + +impl<'a> Icon<'a> { + #[must_use] + fn into_svg(self) -> Element<'a, Message> { + let (svg, svg_clone) = crate::utils::static_rc_halves(self); + + iced_lazy::lazy(svg_clone, move || -> Element { + let icon = freedesktop_icons::lookup(&svg.name) + .with_size(svg.size) + .with_theme(&svg.theme) + .with_cache() + .force_svg() + .find(); + + let handle = if let Some(path) = icon { + svg::Handle::from_path(path) + } else { + eprintln!("icon '{}' size {} not found", svg.name, svg.size); + svg::Handle::from_memory(Vec::new()) + }; + + let mut widget = svg::Svg::::new(handle) + .style(svg.style) + .width(svg.width.unwrap_or(Length::Units(svg.size))) + .height(svg.height.unwrap_or(Length::Units(svg.size))); + + if let Some(content_fit) = svg.content_fit { + widget = widget.content_fit(content_fit); + } + + widget.into() + }).into() + } +} + +impl<'a, Message: 'static> From> for Element<'a, Message> { + fn from(icon: Icon<'a>) -> Self { + icon.into_svg::() + } +} \ No newline at end of file diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs new file mode 100644 index 0000000..f77f593 --- /dev/null +++ b/src/widget/list/column.rs @@ -0,0 +1,65 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use apply::Apply; +use crate::{Element, theme}; +use crate::widget::horizontal_rule; +use iced::{Background, Color}; + +#[must_use] +pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> { + ListColumn::default() +} + +pub struct ListColumn<'a, Message> { + children: Vec>, +} + +impl<'a, Message: 'static> Default for ListColumn<'a, Message> { + fn default() -> Self { + Self { children: Vec::with_capacity(4) } + } +} + +impl<'a, Message: 'static> ListColumn<'a, Message> { + pub fn new() -> Self { + Self::default() + } + + #[must_use] + pub fn add(mut self, item: impl Into>) -> Self { + if !self.children.is_empty() { + self.children.push(horizontal_rule(12).into()); + } + + self.children.push(item.into()); + self + } + + #[must_use] + pub fn into_element(self) -> Element<'a, Message> { + iced::widget::column(self.children) + .spacing(12) + .apply(iced::widget::container) + .padding([12, 16]) + .style(theme::Container::Custom(style)) + .into() + } +} + +impl<'a, Message: 'static> From> for Element<'a, Message> { + fn from(column: ListColumn<'a, Message>) -> Self { + column.into_element() + } +} + +fn style(theme: &crate::Theme) -> iced::widget::container::Appearance { + let cosmic = &theme.cosmic().primary; + iced::widget::container::Appearance { + text_color: Some(cosmic.on.into()), + background: Some(Background::Color(cosmic.base.into())), + border_radius: 8.0, + border_width: 0.0, + border_color: Color::TRANSPARENT, + } +} \ No newline at end of file diff --git a/src/widget/list/item.rs b/src/widget/list/item.rs new file mode 100644 index 0000000..8eeae95 --- /dev/null +++ b/src/widget/list/item.rs @@ -0,0 +1,2 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 diff --git a/src/widget/list/list_box.rs b/src/widget/list/list_box.rs deleted file mode 100644 index d689a7b..0000000 --- a/src/widget/list/list_box.rs +++ /dev/null @@ -1,452 +0,0 @@ -use crate::separator; -use crate::theme::{self, Container}; -use derive_setters::Setters; -use iced::mouse::Interaction; -use iced::{overlay, Alignment, Length, Padding, Point, Rectangle}; -use iced_native::event::Status; -use iced_native::layout::flex::{resolve, Axis}; -use iced_native::layout::{Limits, Node}; -use iced_native::overlay::from_children; -use iced_native::renderer::Style; -use iced_native::widget::{column, Operation, Tree}; -use iced_native::{ - renderer, row, Background, Clipboard, Color, Element, Event, Layout, Shell, Widget, -}; -use iced_style::container::{Appearance, StyleSheet}; - -#[derive(Setters)] -#[allow(dead_code)] -pub struct ListBox<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - Renderer::Theme: StyleSheet + iced_style::rule::StyleSheet, - ::Theme: iced_style::rule::StyleSheet, -{ - spacing: u16, - #[setters(into)] - padding: Padding, - width: Length, - height: Length, - max_width: u32, - align_items: Alignment, - style: ::Style, - children: Vec>, - #[setters(strip_option)] - placeholder: Option>, - show_separators: bool, - on_item_selected: Option Message + 'a>>, -} - -pub fn list_box<'a, Message: 'a, Renderer>() -> ListBox<'a, Message, Renderer> -where - Renderer: iced_native::Renderer + 'a, - <::Theme as StyleSheet>::Style: From, - ::Theme: StyleSheet + iced_style::rule::StyleSheet, - <::Theme as iced_style::rule::StyleSheet>::Style: - From, -{ - ListBox::new() -} - -impl<'a, Message: 'a, Renderer: iced_native::Renderer + 'a> ListBox<'a, Message, Renderer> -where - Renderer::Theme: StyleSheet + iced_style::rule::StyleSheet, - <::Theme as StyleSheet>::Style: From, - <::Theme as iced_style::rule::StyleSheet>::Style: - From, -{ - /// The default padding of a [`ListBox`] drawn by this renderer. - pub const DEFAULT_PADDING: u16 = 0; - - /// Creates an empty [`ListBox`]. - pub fn new() -> Self { - Self::with_children(Vec::>::new()).render() - } - - /// Creates a new [`ListBox`]. - /// - /// [`ListBox`]: struct.ListBox.html - pub fn with_children(children: Vec>) -> Self { - let list_box = Self { - spacing: 0, - padding: Padding::from(Self::DEFAULT_PADDING), - width: Length::Shrink, - height: Length::Shrink, - max_width: u32::MAX, - align_items: Alignment::Center, - style: Default::default(), - children, - placeholder: None, - show_separators: true, - on_item_selected: None, - }; - list_box.render() - } - - pub fn render(mut self) -> Self { - let children_size = self.children.len(); - self.children = self - .children - .into_iter() - .enumerate() - .map(|(index, child)| { - let row_items = if self.show_separators && index != children_size - 1 { - vec![ - row![child].align_items(self.align_items).into(), - separator!(1).into(), - ] - } else { - vec![row![child].align_items(self.align_items).into()] - }; - column(row_items).spacing(self.spacing).into() - }) - .collect(); - self - } - - /// Adds an element to the [`ListBox`]. - pub fn push(mut self, child: impl Into>) -> Self { - self.children.push(child.into()); - self = self.render(); - self - } -} - -impl<'a, Message: 'a, Renderer: iced_native::Renderer + 'a> std::default::Default - for ListBox<'a, Message, Renderer> -where - Renderer::Theme: StyleSheet + iced_style::rule::StyleSheet, - <::Theme as StyleSheet>::Style: From, - <::Theme as iced_style::rule::StyleSheet>::Style: - From, -{ - fn default() -> Self { - Self::new() - } -} - -impl<'a, Message, Renderer> Widget for ListBox<'a, Message, Renderer> -where - Renderer: iced_native::Renderer, - ::Theme: StyleSheet + iced_style::rule::StyleSheet, -{ - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height - } - - fn layout(&self, renderer: &Renderer, limits: &Limits) -> Node { - let limits = limits - .max_width(self.max_width) - .width(self.width) - .height(self.height); - - if !self.children.is_empty() { - resolve( - Axis::Vertical, - renderer, - &limits, - self.padding, - self.spacing as f32, - self.align_items, - &self.children, - ) - } else if self.placeholder.is_some() { - self.placeholder - .as_ref() - .unwrap() - .as_widget() - .layout(renderer, &limits) - } else { - Node::default() - } - } - - fn draw( - &self, - state: &Tree, - renderer: &mut Renderer, - theme: &Renderer::Theme, - style: &Style, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - ) { - let color_scheme = theme.appearance(self.style); - - draw_background(renderer, &color_scheme, layout.bounds()); - - if !self.children.is_empty() { - for ((child, state), layout) in self - .children - .iter() - .zip(&state.children) - .zip(layout.children()) - { - child.as_widget().draw( - state, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); - } - } else if let Some(placeholder) = &self.placeholder { - placeholder.as_widget().draw( - state, - renderer, - theme, - style, - layout, - cursor_position, - viewport, - ); - } - } - - fn children(&self) -> Vec { - let widgets = if !self.children.is_empty() { - self.children.iter().map(Tree::new).collect() - } else if let Some(placeholder) = &self.placeholder { - vec![Tree::new(placeholder)] - } else { - vec![Tree::empty()] - }; - widgets - } - - fn diff(&self, tree: &mut Tree) { - if !self.children.is_empty() { - tree.diff_children(&self.children); - } else if let Some(placeholder) = &self.placeholder { - tree.diff_children(&[placeholder]); - } - } - - fn operate( - &self, - state: &mut Tree, - layout: Layout<'_>, - operation: &mut dyn Operation, - ) { - if !self.children.is_empty() { - operation.container(None, &mut |operation| { - self.children - .iter() - .zip(&mut state.children) - .zip(layout.children()) - .for_each(|((child, state), layout)| { - child.as_widget().operate(state, layout, operation); - }) - }); - } else if let Some(placeholder) = &self.placeholder { - placeholder.as_widget().operate(state, layout, operation); - } - } - - fn on_event( - &mut self, - state: &mut Tree, - event: Event, - layout: Layout<'_>, - cursor_position: Point, - renderer: &Renderer, - clipboard: &mut dyn Clipboard, - shell: &mut Shell<'_, Message>, - ) -> Status { - if !self.children.is_empty() { - self.children - .iter_mut() - .zip(&mut state.children) - .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget_mut().on_event( - state, - event.clone(), - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - }) - .fold(Status::Ignored, Status::merge) - } else if self.placeholder.is_some() { - self.placeholder.as_mut().unwrap().as_widget_mut().on_event( - &mut state.children[0], - event, - layout, - cursor_position, - renderer, - clipboard, - shell, - ) - } else { - Status::Ignored - } - } - - fn mouse_interaction( - &self, - state: &Tree, - layout: Layout<'_>, - cursor_position: Point, - viewport: &Rectangle, - renderer: &Renderer, - ) -> Interaction { - if !self.children.is_empty() { - self.children - .iter() - .zip(&state.children) - .zip(layout.children()) - .map(|((child, state), layout)| { - child.as_widget().mouse_interaction( - state, - layout, - cursor_position, - viewport, - renderer, - ) - }) - .max() - .unwrap_or_default() - } else if let Some(placeholder) = &self.placeholder { - placeholder.as_widget().mouse_interaction( - &state.children[0], - layout, - cursor_position, - viewport, - renderer, - ) - } else { - Interaction::Idle - } - } - - fn overlay<'b>( - &'b self, - tree: &'b mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - ) -> Option> { - if !self.children.is_empty() { - from_children(&self.children, tree, layout, renderer) - } else if let Some(placeholder) = &self.placeholder { - placeholder - .as_widget() - .overlay(&mut tree.children[0], layout, renderer) - } else { - None - } - } -} - -/// Draws the background of a [`Container`] given its [`Style`] and its `bounds`. -pub fn draw_background( - renderer: &mut Renderer, - appearance: &Appearance, - bounds: Rectangle, -) where - Renderer: iced_native::Renderer, -{ - if appearance.background.is_some() || appearance.border_width > 0.0 { - renderer.fill_quad( - renderer::Quad { - bounds, - border_radius: appearance.border_radius, - border_width: appearance.border_width, - border_color: appearance.border_color, - }, - appearance - .background - .unwrap_or(Background::Color(Color::TRANSPARENT)), - ); - } -} - -impl<'a, Message: 'a, Renderer: iced_native::Renderer + 'a> From> - for Element<'a, Message, Renderer> -where - ::Theme: StyleSheet + iced_style::rule::StyleSheet, -{ - fn from(list_box: ListBox<'a, Message, Renderer>) -> Self { - Self::new(list_box) - } -} - -#[macro_export] -macro_rules! list_box_item { - ($($x:expr),+ $(,)?) => ( - $crate::iced::widget::row![ - column(vec![ - $($x),+ - ]) - ] - ); -} -pub use list_box_item; - -#[macro_export] -macro_rules! list_box_heading { - ($title:expr) => { - $crate::iced::widget::container( - $crate::iced::widget::row![ - text($title).size(18), - $crate::iced::widget::vertical_space(Length::Fill), - $crate::iced::widget::horizontal_space(Length::Fill) - ] - .height(Length::Fill) - .align_items($crate::iced::alignment::Alignment::Center), - ) - .style($crate::iced::theme::Container::Custom( - $crate::widget::expander_heading_style, - )) - .max_height(60) - .padding(10) - }; - ($title:expr, $subtitle:expr) => { - $crate::iced::widget::container( - $crate::iced::widget::row![ - column(vec![ - text($title).size(18).into(), - text($subtitle).size(16).into(), - ]), - $crate::iced::widget::vertical_space(Length::Fill), - $crate::iced::widget::horizontal_space(Length::Fill) - ] - .height(Length::Fill) - .align_items($crate::iced::alignment::Alignment::Center), - ) - .style($crate::iced::theme::Container::Custom( - $crate::widget::expander_heading_style, - )) - .max_height(60) - .padding(10) - }; - ($title:expr, $subtitle:expr, $icon:expr) => { - $crate::iced::widget::container( - $crate::iced::widget::row![ - container($crate::widget::icon($icon, 20)).padding(10), - column(vec![ - text($title).size(18).into(), - text($subtitle).size(16).into(), - ]), - $crate::iced::widget::vertical_space(Length::Fill), - $crate::iced::widget::horizontal_space(Length::Fill) - ] - .height(Length::Fill) - .align_items($crate::iced::alignment::Alignment::Center), - ) - .style($crate::iced::theme::Container::Custom( - $crate::widget::expander_heading_style, - )) - .max_height(60) - .padding(10) - }; -} -pub use list_box_heading; diff --git a/src/widget/list/list_row.rs b/src/widget/list/list_row.rs deleted file mode 100644 index 9046ecb..0000000 --- a/src/widget/list/list_row.rs +++ /dev/null @@ -1,18 +0,0 @@ -use derive_setters::Setters; - -#[derive(Setters, Default, Debug, Clone)] -pub struct ListRow<'a> { - pub(crate) title: &'a str, - #[setters(strip_option)] - pub subtitle: Option<&'a str>, - #[setters(strip_option)] - pub icon: Option, -} - -pub fn list_row<'a>() -> ListRow<'a> { - ListRow { - title: "", - subtitle: None, - icon: None, - } -} diff --git a/src/widget/list/macros.rs b/src/widget/list/macros.rs deleted file mode 100644 index f2f3649..0000000 --- a/src/widget/list/macros.rs +++ /dev/null @@ -1,146 +0,0 @@ -pub use iced::{widget, Background, Color}; -pub use crate::Theme; - -pub mod list_view { - #[macro_export] - macro_rules! list_view { - ($($x:expr),+ $(,)?) => ( - $crate::iced::widget::Column::with_children( - vec![$($crate::iced::Element::from($x)),+] - ) - .spacing(24) - .padding(24) - .max_width(600) - ); - } - - #[macro_export] - macro_rules! list_view_row { - ($($x:expr),+ $(,)?) => ( - $crate::iced::widget::Row::with_children(vec![ - $($crate::iced::Element::from($x)),+ - ]) - .align_items(Alignment::Center) - .padding([0, 8]) - .spacing(12) - ); - } - - #[macro_export] - macro_rules! list_view_section { - ($title:expr, $($x:expr),+ $(,)?) => ( - $crate::iced::widget::Column::with_children(vec![ - $crate::iced::widget::Text::new($title) - .font($crate::font::FONT_SEMIBOLD) - .into() - , - $crate::iced::widget::Container::new({ - let mut children = vec![$($crate::iced::Element::from($x)),+]; - - //TODO: more efficient method for adding separators - let mut i = 1; - while i < children.len() { - children.insert(i, $crate::separator!(12).into()); - i += 2; - } - - $crate::iced::widget::Column::with_children(children) - .spacing(12) - }) - .padding([12, 16]) - .style(theme::Container::Custom( - list_section_style - )) - .into() - ]) - .spacing(8) - ); - } - - #[macro_export] - macro_rules! list_view_item { - ($title:expr, $($x:expr),+ $(,)?) => ( - $crate::list_view_row!( - $crate::iced::widget::Text::new($title), - $crate::iced::widget::horizontal_space( - $crate::iced::Length::Fill - ), - $($x),+ - ) - ); - } - - pub fn list_section_style(theme: &Theme) -> widget::container::Appearance { - let cosmic = &theme.cosmic().primary; - widget::container::Appearance { - text_color: Some(cosmic.on.into()), - background: Some(Background::Color(cosmic.base.into())), - border_radius: 8.0, - border_width: 0.0, - border_color: Color::TRANSPARENT, - } - } - - use crate::widget::{Background, Color}; - use iced::widget; - use crate::Theme; - - pub use list_view; - pub use list_view_item; - pub use list_view_row; - pub use list_view_section; -} - -pub mod list_box { - #[macro_export] - macro_rules! list_box_row { - ($title:expr) => { - $crate::iced::widget::container( - $crate::iced::widget::row![ - text($title).size(18), - $crate::iced::widget::vertical_space(Length::Fill), - $crate::iced::widget::horizontal_space(Length::Fill) - ] - .height(Length::Fill) - .align_items($crate::iced::alignment::Alignment::Center), - ) - .max_height(60) - .padding(10) - }; - ($title:expr, $subtitle:expr) => { - $crate::iced::widget::container( - $crate::iced::widget::row![ - column(vec![ - text($title).size(18).into(), - text($subtitle).size(16).into(), - ]), - $crate::iced::widget::vertical_space(Length::Fill), - $crate::iced::widget::horizontal_space(Length::Fill) - ] - .height(Length::Fill) - .align_items($crate::iced::alignment::Alignment::Center), - ) - .max_height(60) - .padding(10) - }; - ($title:expr, $subtitle:expr, $icon:expr) => { - $crate::iced::widget::container( - $crate::iced::widget::row![ - container($crate::widget::icon($icon, 20)).padding(10), - column(vec![ - text($title).size(18).into(), - text($subtitle).size(16).into(), - ]), - $crate::iced::widget::vertical_space(Length::Fill), - $crate::iced::widget::horizontal_space(Length::Fill) - ] - .height(Length::Fill) - .align_items($crate::iced::alignment::Alignment::Center), - ) - .max_height(60) - .padding(10) - }; - } - - pub use list_box_row; -} diff --git a/src/widget/list/mod.rs b/src/widget/list/mod.rs index d437d30..646d242 100644 --- a/src/widget/list/mod.rs +++ b/src/widget/list/mod.rs @@ -1,8 +1,8 @@ -pub mod macros; -pub use macros::*; +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 -pub mod list_row; -pub use list_row::*; +mod column; +// mod item; -pub mod list_box; -pub use list_box::*; +pub use self::column::{ListColumn, list_column}; +// pub use self::item::{ListItem, list_item}; \ No newline at end of file diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 196a011..f1940c3 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -1,26 +1,31 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + mod button; pub use button::*; mod header_bar; -pub use header_bar::*; +pub use header_bar::{HeaderBar, header_bar}; mod icon; -pub use self::icon::*; +pub use self::icon::{Icon, icon}; + +pub mod list; +pub use self::list::*; + +pub mod nav_button; +pub use self::nav_button::{NavButton, nav_button}; pub mod navigation; pub use navigation::*; mod toggler; -pub use toggler::*; +pub use toggler::toggler; + +pub mod settings; mod scrollable; pub use scrollable::*; -mod expander; -pub use expander::*; - -pub mod list; -pub use list::*; - pub mod separator; -pub use separator::*; +pub use separator::{horizontal_rule, vertical_rule}; diff --git a/src/widget/nav_button.rs b/src/widget/nav_button.rs new file mode 100644 index 0000000..f271184 --- /dev/null +++ b/src/widget/nav_button.rs @@ -0,0 +1,61 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use apply::Apply; +use derive_setters::Setters; +use iced::{alignment::Vertical, Length}; +use crate::{Element, theme}; + +#[derive(Setters)] +pub struct NavButton<'a, Message> { + title: &'a str, + sidebar_active: bool, + #[setters(strip_option)] + on_sidebar_toggled: Option, +} + +#[must_use] +pub fn nav_button(title: &str) -> NavButton { + NavButton { + title, + sidebar_active: false, + on_sidebar_toggled: None, + } +} + +impl<'a, Message: 'static + Clone> From> for Element<'a, Message> { + fn from(nav_button: NavButton<'a, Message>) -> Self { + let text = iced::widget::text(&nav_button.title) + .style(theme::Text::Accent) + .vertical_alignment(Vertical::Center) + .width(Length::Shrink) + .height(Length::Fill); + + let icon = super::icon( + if nav_button.sidebar_active { + "go-previous-symbolic" + } else { + "go-next-symbolic" + }, + 24, + ) + .style(theme::Svg::SymbolicActive) + .width(Length::Units(24)) + .height(Length::Fill); + + let mut widget = iced::widget::row!(text, crate::widget::vertical_rule(4), icon) + .padding(4) + .spacing(4) + .apply(iced::widget::button) + .style(theme::Button::Secondary); + + if let Some(message) = nav_button.on_sidebar_toggled.clone() { + widget = widget.on_press(message); + } + + widget.apply(iced::widget::container) + .center_y() + .height(Length::Fill) + .into() + } +} \ No newline at end of file diff --git a/src/widget/navigation/macros.rs b/src/widget/navigation/macros.rs index 0cd7085..2dc31fa 100644 --- a/src/widget/navigation/macros.rs +++ b/src/widget/navigation/macros.rs @@ -1,3 +1,6 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + pub mod nav_bar { use iced::{widget, Background, Color}; use crate::Theme; diff --git a/src/widget/navigation/mod.rs b/src/widget/navigation/mod.rs index 896b838..f8fcdec 100644 --- a/src/widget/navigation/mod.rs +++ b/src/widget/navigation/mod.rs @@ -1,3 +1,6 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + pub mod navbar; pub use navbar::*; diff --git a/src/widget/navigation/navbar.rs b/src/widget/navigation/navbar.rs index 0fd4cf4..a3d006f 100644 --- a/src/widget/navigation/navbar.rs +++ b/src/widget/navigation/navbar.rs @@ -1,14 +1,15 @@ -use crate::scrollable; +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + use crate::widget::nav_bar::{nav_bar_pages_style, nav_bar_sections_style}; -use crate::widget::{icon, Background}; -use crate::{theme, Theme}; +use crate::widget::{icon, scrollable}; +use crate::{theme, Renderer, Theme}; use derive_setters::Setters; -use iced::Length; +use iced::{Background, Length}; use iced_lazy::Component; use iced_native::widget::{button, column, container, text}; use iced_native::{row, Alignment, Element}; use iced_style::button::Appearance; -use iced_style::scrollable; use std::collections::BTreeMap; #[derive(Setters, Default)] @@ -44,10 +45,7 @@ pub struct NavBarSection { impl NavBarSection { pub fn new() -> Self { - Self { - title: String::new(), - icon: String::new(), - } + Self::default() } } @@ -89,17 +87,7 @@ pub struct NavBarState { page_active: bool, } -impl<'a, Message, Renderer> Component for NavBar<'a, Message> -where - Renderer: iced_native::Renderer + iced_native::text::Renderer + iced_native::svg::Renderer + 'a, - ::Theme: - container::StyleSheet + button::StyleSheet + text::StyleSheet + scrollable::StyleSheet, - <::Theme as button::StyleSheet>::Style: From, - <::Theme as container::StyleSheet>::Style: - From, - <::Theme as text::StyleSheet>::Style: From, - Renderer::Theme: iced_native::svg::StyleSheet -{ +impl<'a, Message> Component for NavBar<'a, Message> { type State = NavBarState; type Event = NavBarEvent; @@ -132,15 +120,15 @@ where fn view(&self, state: &Self::State) -> Element<'a, Self::Event, Renderer> { if self.active { - let mut sections: Vec> = vec![]; - let mut pages: Vec> = vec![]; + let mut sections: Vec> = vec![]; + let mut pages: Vec> = vec![]; for (section, section_pages) in &self.source { sections.push( button( column(vec![ - icon(§ion.icon, 20).into(), - text(§ion.title).size(14).into(), + icon(section.icon.clone(), 20).into(), + text(section.title.clone()).size(14).into(), ]) .width(Length::Units(100)) .height(Length::Units(50)) @@ -179,7 +167,7 @@ where let nav_bar: Element = container(if self.condensed && state.selected_page.is_some() { - row![container(scrollable!(column(pages) + row![container(scrollable(column(pages) .spacing(10) .padding(10) .max_width(200) @@ -188,7 +176,7 @@ where .height(Length::Fill) .style(theme::Container::Custom(nav_bar_pages_style))] } else if !state.section_active || self.condensed && state.selected_page.is_none() { - row![scrollable!(column(sections) + row![scrollable(column(sections) .spacing(10) .padding(10) .max_width(100) @@ -196,13 +184,13 @@ where .height(Length::Shrink))] } else { row![ - scrollable!(column(sections) + scrollable(column(sections) .spacing(10) .padding(10) .max_width(100) .align_items(Alignment::Center) .height(Length::Shrink)), - container(scrollable!(column(pages) + container(scrollable(column(pages) .spacing(10) .padding(10) .max_width(200) @@ -222,17 +210,8 @@ where } } -impl<'a, Message: 'a, Renderer> From> +impl<'a, Message: 'static> From> for Element<'a, Message, Renderer> -where - Renderer: iced_native::text::Renderer + iced_native::svg::Renderer + 'a, - ::Theme: - container::StyleSheet + button::StyleSheet + text::StyleSheet + scrollable::StyleSheet, - <::Theme as button::StyleSheet>::Style: From, - <::Theme as container::StyleSheet>::Style: - From, - <::Theme as text::StyleSheet>::Style: From, - Renderer::Theme: iced_native::svg::StyleSheet { fn from(nav_bar: NavBar<'a, Message>) -> Self { iced_lazy::component(nav_bar) diff --git a/src/widget/scrollable.rs b/src/widget/scrollable.rs index 9b7acef..e205a2e 100644 --- a/src/widget/scrollable.rs +++ b/src/widget/scrollable.rs @@ -1,8 +1,11 @@ -#[macro_export] -macro_rules! scrollable { - ($x:expr) => { - $crate::iced::widget::scrollable($x) - .scrollbar_width(8) - .scroller_width(8) - }; -} +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{Element, Renderer}; +use iced::widget; + +pub fn scrollable<'a, Message>(element: impl Into>) -> widget::Scrollable<'a, Message, Renderer> { + widget::scrollable(element) + .scrollbar_width(8) + .scroller_width(8) +} \ No newline at end of file diff --git a/src/widget/separator.rs b/src/widget/separator.rs index d69fb92..f00bea7 100644 --- a/src/widget/separator.rs +++ b/src/widget/separator.rs @@ -1,7 +1,25 @@ -#[macro_export] -macro_rules! separator { - ($size:expr) => { - $crate::iced::widget::horizontal_rule($size) - .style(theme::Rule::Custom($crate::widget::separator_style)) - }; +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::iced::widget; +use crate::{theme, Renderer, Theme}; + +#[must_use] +pub fn horizontal_rule(size: u16) -> widget::Rule { + widget::horizontal_rule(size).style(theme::Rule::Custom(separator_style)) } + +#[must_use] +pub fn vertical_rule(size: u16) -> widget::Rule { + widget::vertical_rule(size).style(theme::Rule::Custom(separator_style)) +} + +fn separator_style(theme: &Theme) -> widget::rule::Appearance { + let cosmic = &theme.cosmic().primary; + widget::rule::Appearance { + color: cosmic.divider.into(), + width: 1, + radius: 0.0, + fill_mode: widget::rule::FillMode::Padded(10), + } +} \ No newline at end of file diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs new file mode 100644 index 0000000..d244751 --- /dev/null +++ b/src/widget/settings/item.rs @@ -0,0 +1,26 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{Element, Renderer}; + +/// A setting within a settings view section. +#[must_use] +#[allow(clippy::module_name_repetitions)] +pub fn item<'a, Message: 'static>(title: &'a str, widget: impl Into>) -> iced::widget::Row<'a, Message, Renderer> { + item_row(vec![ + iced::widget::text(title).into(), + iced::widget::horizontal_space(iced::Length::Fill).into(), + widget.into() + ]) +} + +/// A settings item aligned in a row +#[must_use] +#[allow(clippy::module_name_repetitions)] +pub fn item_row(children: Vec>) -> iced::widget::Row { + iced::widget::row(children) + .align_items(iced::Alignment::Center) + .padding([0, 8]) + .spacing(12) +} + diff --git a/src/widget/settings/mod.rs b/src/widget/settings/mod.rs new file mode 100644 index 0000000..49ef389 --- /dev/null +++ b/src/widget/settings/mod.rs @@ -0,0 +1,17 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +mod item; +mod section; + +pub use self::item::{item, item_row}; +pub use self::section::{Section, view_section}; + +use crate::{Element, Renderer}; +use iced::widget::{Column, column}; + +/// A column with a predefined style for creating a settings panel +#[must_use] +pub fn view_column(children: Vec>) -> Column { + column(children).spacing(24).padding(24).max_width(600) +} diff --git a/src/widget/settings/section.rs b/src/widget/settings/section.rs new file mode 100644 index 0000000..6f79352 --- /dev/null +++ b/src/widget/settings/section.rs @@ -0,0 +1,39 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{Element}; +use crate::widget::{ListColumn}; +use iced::widget::{column, text}; + +/// A section within a settings view column. +#[must_use] +pub fn view_section( + title: &str, +) -> Section { + Section { title, children: ListColumn::default() } +} + +pub struct Section<'a, Message> { + title: &'a str, + children: ListColumn<'a, Message> +} + +impl<'a, Message: 'static> Section<'a, Message> { + #[must_use] + pub fn add(mut self, item: impl Into>) -> Self { + self.children = self.children.add(item.into()); + self + } +} + +impl<'a, Message: 'static> From> for Element<'a, Message> { + fn from(data: Section<'a, Message>) -> Self { + let title = text(data.title) + .font(crate::font::FONT_SEMIBOLD) + .into(); + + column(vec![title, data.children.into_element()]) + .spacing(8) + .into() + } +} \ No newline at end of file diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 78e16b0..004f81d 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -1,14 +1,14 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::Renderer; use iced::{widget, Length}; -pub fn toggler<'a, Message, Renderer>( +pub fn toggler<'a, Message>( label: impl Into>, is_checked: bool, f: impl Fn(bool) -> Message + 'a, -) -> widget::Toggler<'a, Message, Renderer> -where - Renderer: iced_native::text::Renderer, - Renderer::Theme: widget::toggler::StyleSheet, -{ +) -> widget::Toggler<'a, Message, Renderer> { widget::Toggler::new(is_checked, label, f) .size(24) .spacing(12)