Merge branch 'cosmic-design-system' into sctk-cosmic-design-system

This commit is contained in:
Ashley Wulber 2022-12-06 17:03:31 -05:00
commit 9796fa9f15
No known key found for this signature in database
GPG key ID: 5216D4F46A90A820
34 changed files with 850 additions and 1360 deletions

View file

@ -22,6 +22,7 @@ lazy_static = "1.4.0"
palette = "0.6.1"
cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", default-features = false, optional = true }
sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", optional = true }
static-rc = "0.6.1"
[dependencies.cosmic-theme]
git = "https://github.com/pop-os/cosmic-theme.git"

View file

@ -4,7 +4,7 @@ use cosmic::{
};
mod window;
pub use window::*;
pub use window::Window;
pub fn main() -> cosmic::iced::Result {
let mut settings = settings();

View file

@ -1,22 +1,25 @@
use cosmic::widget::{expander, image_icon, nav_bar, nav_bar_page, nav_bar_section};
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use cosmic::{
iced::widget::{
checkbox, column, container, horizontal_space, pick_list, progress_bar, radio, row, slider,
text,
},
iced::{self, Alignment, Application, Color, Command, Length},
iced_lazy::responsive,
iced_native::window,
list_view, list_view_item, list_view_row, list_view_section, scrollable,
theme::{self, Button, Theme},
widget::{button, header_bar, list_box, list_row, list_view::*, toggler},
iced::widget::{
column, container, horizontal_space, pick_list, progress_bar, radio, row, slider,
},
iced::{self, Alignment, Application, Command, Length},
iced_lazy::responsive,
theme::{self, Theme},
widget::{button, nav_button, nav_bar, nav_bar_page, nav_bar_section, header_bar, settings, scrollable, toggler},
Element,
ElementExt,
};
use iced_sctk::application::SurfaceIdWrapper;
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,
@ -64,6 +67,7 @@ pub enum Message {
Drag,
Minimize,
Maximize,
InputChanged,
}
impl Application for Window {
@ -80,11 +84,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<Self::Message> {
@ -94,43 +99,46 @@ impl Application for Window {
Message::ThemeChanged(theme) => self.theme = theme,
Message::ButtonPressed => {}
Message::SliderChanged(value) => self.slider_value = value,
Message::CheckboxToggled(value) => self.checkbox_value = value,
Message::CheckboxToggled(value) => {
self.checkbox_value = value;
},
Message::TogglerToggled(value) => self.toggler_value = value,
Message::PickListSelected(value) => self.pick_list_selected = Some(value),
Message::Close => self.exit = true,
Message::ToggleSidebar => self.sidebar_toggled = !self.sidebar_toggled,
// Message::Drag => return drag(window::Id::new(0)),
// Message::Minimize => return minimize(window::Id::new(0), true),
// Message::Maximize => return maximize(window::Id::new(0), true),
Message::Drag => todo!(),
Message::Minimize => todo!(),
Message::Maximize => todo!(),
Message::RowSelected(row) => println!("Selected row {row}"),
_ => {}
Message::InputChanged => {},
}
Command::none()
}
fn close_requested(&self, _: SurfaceIdWrapper) -> Self::Message {
unimplemented!()
}
fn view(&self, _: SurfaceIdWrapper) -> Element<Message> {
let mut header: Element<Message> = 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::<Element<Message>>::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.
@ -138,26 +146,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
// }),
// ]
@ -221,49 +229,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",],
@ -271,72 +278,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()
),
list_view_section!(
"image",
button!(image_icon("firefox", 64).unwrap()).style(Button::Transparent)
)
)
))
.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(),
@ -360,4 +325,8 @@ impl Application for Window {
fn theme(&self) -> Theme {
self.theme
}
fn close_requested(&self, id: iced_sctk::application::SurfaceIdWrapper) -> Self::Message {
Message::Close
}
}

View file

@ -1,3 +1,6 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use cosmic::{iced::Application, settings};
mod window;

View file

@ -1,24 +1,25 @@
use cosmic::widget::{expander, nav_bar, nav_bar_page, nav_bar_section};
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use cosmic::{
iced::widget::{
checkbox, column, container, horizontal_space, pick_list, progress_bar, radio, row, slider,
text,
},
iced::{self, Alignment, Application, Color, Command, Length},
iced_lazy::responsive,
iced_native::window,
iced_winit::window::{drag, maximize, minimize},
list_view, list_view_item, list_view_row, list_view_section, scrollable,
iced::widget::{
column, container, horizontal_space, pick_list, progress_bar, radio, row, slider,
},
iced::{self, Alignment, Application, Command, Length},
iced_lazy::responsive,
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 cosmic::widget::widget::text_input::Id as TextInputId;
use cosmic::widget::widget::text_input;
use std::{collections::BTreeMap, vec};
use theme::Button as ButtonTheme;
#[derive(Default)]
pub struct Window {
title: String,
page: u8,
debug: bool,
theme: Theme,
@ -83,11 +84,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<Self::Message> {
@ -99,7 +101,6 @@ impl Application for Window {
Message::SliderChanged(value) => self.slider_value = value,
Message::CheckboxToggled(value) => {
self.checkbox_value = value;
return text_input::focus(TextInputId::new("launcher_entry"));
},
Message::TogglerToggled(value) => self.toggler_value = value,
Message::PickListSelected(value) => self.pick_list_selected = Some(value),
@ -107,7 +108,7 @@ impl Application for Window {
Message::ToggleSidebar => self.sidebar_toggled = !self.sidebar_toggled,
Message::Drag => return drag(window::Id::new(0)),
Message::Minimize => return minimize(window::Id::new(0), true),
Message::Maximize => return maximize(window::Id::new(0), true),
Message::Maximize => return toggle_maximize(window::Id::new(0)),
Message::RowSelected(row) => println!("Selected row {row}"),
Message::InputChanged => {},
@ -117,23 +118,27 @@ impl Application for Window {
}
fn view(&self) -> Element<Message> {
let mut header: Element<Message> = 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::<Element<Message>>::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.
@ -141,26 +146,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
// }),
// ]
@ -224,49 +229,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",],
@ -274,76 +278,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),
text_input(
"Type something...",
"",
|_| Message::InputChanged,
)
.padding(8)
.size(20)
.id(TextInputId::new("launcher_entry"))
),
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(),

19
src/ext.rs Normal file
View file

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

View file

@ -1,3 +1,6 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
pub use iced::Font;
pub const FONT: Font = Font::External {

View file

@ -1,3 +1,6 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
pub use iced;
pub use iced_lazy;
pub use iced_native;
@ -11,19 +14,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<Theme>;
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<Flags: Default>() -> iced::Settings<Flags> {
let mut settings = iced::Settings::default();
settings.default_font = match font::FONT {

View file

@ -0,0 +1,2 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0

View file

@ -1,3 +1,6 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use iced_core::{Background, Color};
/// The appearance of a [`Expander`](crate::native::expander::Expander).

View file

@ -1,6 +1,12 @@
// Copyright 2022 System76 <info@system76.com>
// 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 cosmic_theme::Component;
@ -52,6 +58,7 @@ pub enum Theme {
}
impl Theme {
#[must_use]
pub fn cosmic(self) -> &'static CosmicTheme {
match self {
Self::Dark => &COSMIC_DARK,
@ -59,6 +66,7 @@ impl Theme {
}
}
#[must_use]
pub fn palette(self) -> Palette {
match self {
Self::Dark => Palette::DARK,
@ -66,6 +74,7 @@ impl Theme {
}
}
#[must_use]
pub fn extended_palette(&self) -> &self::palette::Extended {
match self {
Self::Dark => &self::palette::EXTENDED_DARK,
@ -113,10 +122,11 @@ impl application::StyleSheet for Theme {
*/
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Button {
Deactivated,
Destructive,
Positive,
Primary,
Secondary,
Positive,
Destructive,
Text,
Transparent,
}
@ -137,6 +147,7 @@ impl Button {
Button::Destructive => &cosmic.destructive,
Button::Text => &cosmic.secondary.component,
Button::Transparent => &TRANSPARENT_COMPONENT,
Button::Deactivated => &cosmic.secondary.component,
}
}
}
@ -189,7 +200,11 @@ impl Default for Checkbox {
impl checkbox::StyleSheet for Theme {
type Style = Checkbox;
fn active(&self, style: &Self::Style, is_checked: bool) -> checkbox::Appearance {
fn active(
&self,
style: &Self::Style,
is_checked: bool,
) -> checkbox::Appearance {
let palette = self.extended_palette();
match style {
@ -220,7 +235,11 @@ impl checkbox::StyleSheet for Theme {
}
}
fn hovered(&self, style: &Self::Style, is_checked: bool) -> checkbox::Appearance {
fn hovered(
&self,
style: &Self::Style,
is_checked: bool,
) -> checkbox::Appearance {
let palette = self.extended_palette();
match style {
@ -291,7 +310,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),
}
}
@ -324,7 +343,7 @@ impl container::StyleSheet for Theme {
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();
@ -368,7 +387,9 @@ impl slider::StyleSheet for Theme {
fn hovered(&self, style: &Self::Style) -> slider::Appearance {
let mut style = self.active(&style);
style.handle.shape = slider::HandleShape::Circle { radius: 16.0 };
style.handle.shape = slider::HandleShape::Circle {
radius: 16.0
};
style.handle.border_width = 6.0;
style.handle.border_color = match self {
Theme::Dark => Color::from_rgba8(0xFF, 0xFF, 0xFF, 0.1),
@ -444,7 +465,7 @@ impl pick_list::StyleSheet for Theme {
impl radio::StyleSheet for Theme {
type Style = ();
fn active(&self, _style: &Self::Style, _is_checked: bool) -> radio::Appearance {
fn active(&self, _style: &Self::Style, is_selected: bool) -> radio::Appearance {
let palette = self.extended_palette();
radio::Appearance {
@ -456,8 +477,8 @@ impl radio::StyleSheet for Theme {
}
}
fn hovered(&self, style: &Self::Style, is_checked: bool) -> radio::Appearance {
let active = self.active(&style, is_checked);
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 {
@ -474,7 +495,11 @@ impl radio::StyleSheet for Theme {
impl toggler::StyleSheet for Theme {
type Style = ();
fn active(&self, _style: &Self::Style, is_active: bool) -> toggler::Appearance {
fn active(
&self,
_style: &Self::Style,
is_active: bool,
) -> toggler::Appearance {
let palette = self.palette();
toggler::Appearance {
@ -497,7 +522,11 @@ impl toggler::StyleSheet for Theme {
}
}
fn hovered(&self, style: &Self::Style, is_active: bool) -> toggler::Appearance {
fn hovered(
&self,
style: &Self::Style,
is_active: bool,
) -> toggler::Appearance {
//TODO: grab colors from palette
match self {
Theme::Dark => toggler::Appearance {
@ -515,7 +544,7 @@ impl toggler::StyleSheet for Theme {
Color::from_rgb8(0x54, 0x54, 0x54)
},
..self.active(&style, is_active)
},
}
}
}
}
@ -659,23 +688,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<H: Hasher>(&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()),
},
}
}
@ -707,7 +755,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),
}

View file

@ -1,3 +1,6 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//TODO: GET CORRECT PALETTE FROM COSMIC-THEME
use iced_core::Color;
@ -85,6 +88,7 @@ lazy_static! {
}
impl Extended {
#[must_use]
pub fn generate(palette: Palette) -> Self {
Self {
background: Background::new(palette.background, palette.text),
@ -118,6 +122,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);
@ -137,6 +142,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);
@ -156,6 +162,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);
@ -176,6 +183,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);
@ -195,6 +203,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);

9
src/utils.rs Normal file
View file

@ -0,0 +1,9 @@
// Copyright 2022 System76 <info@system76.com>
// 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<T>(value: T) -> (StaticRc<T, 1, 3>, StaticRc<T, 2, 3>) {
StaticRc::split::<1, 2>(StaticRc::<T, 3, 3>::new(value))
}

View file

@ -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 <info@system76.com>
// 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<Message>(style: theme::Button) -> Button<Message> {
Button { style, message: None }
}
pub use button;
/// A button widget with COSMIC styling
pub struct Button<Message> {
style: theme::Button,
message: Option<Message>,
}
impl<Message: 'static> Button<Message> {
/// 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<Message, Renderer> {
self.custom(vec![super::icon(icon, size).style(style).into()])
}
/// A button with text.
pub fn text(self, text: &str) -> widget::Button<Message, Renderer> {
self.custom(vec![text.into()])
}
/// A custom button that has the desired default spacing and padding.
pub fn custom(self, children: Vec<Element<Message>>) -> widget::Button<Message, Renderer> {
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
}
}
}

View file

@ -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<String>,
expansible: bool,
#[setters(skip)]
rows: Option<Vec<ListRow<'a>>>,
#[setters(strip_option)]
on_row_selected: Option<Box<dyn Fn(usize) -> 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<ListRow<'a>>) -> 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<Message, Renderer> for Expander<'a, Message> {
type State = ExpanderState;
type Event = ExpanderEvent;
fn update(&mut self, state: &mut Self::State, event: Self::Event) -> Option<Message> {
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<Self::Event> {
let heading: Element<ExpanderEvent> = {
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<ExpanderEvent> = horizontal_space(Length::Fill).into();
let toggler: Element<ExpanderEvent> = {
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<Element<_>> = 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<ExpanderEvent> = 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<Expander<'a, Message>> 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),
}
}

View file

@ -1,16 +1,28 @@
use crate::{theme, Element, Renderer};
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use apply::Apply;
use derive_setters::*;
use iced::{self, alignment::Vertical, widget, Length};
use iced_lazy::Component;
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<Message> {
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<Message>,
#[setters(strip_option)]
@ -20,133 +32,98 @@ pub struct HeaderBar<Message> {
#[setters(strip_option)]
on_minimize: Option<Message>,
#[setters(strip_option)]
on_sidebar_toggle: Option<Message>,
start: Option<Element<'a, Message>>,
#[setters(strip_option)]
center: Option<Element<'a, Message>>,
#[setters(strip_option)]
end: Option<Element<'a, Message>>
}
pub fn header_bar<Message>() -> HeaderBar<Message> {
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<Element<Message>> = Vec::with_capacity(4);
#[derive(Debug, Clone)]
pub enum HeaderEvent {
Close,
ToggleSidebar,
Drag,
Minimize,
Maximize,
}
impl<Message: Clone> Component<Message, Renderer> for HeaderBar<Message> {
type State = ();
type Event = HeaderEvent;
fn update(&mut self, _state: &mut Self::State, event: Self::Event) -> Option<Message> {
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<Self::Event> {
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<Element<HeaderEvent>> = Vec::with_capacity(3);
/// Creates the widget for window controls.
fn window_controls(&mut self) -> Element<'a, Message> {
let mut widgets: Vec<Element<_>> = 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<HeaderBar<Message>> for Element<'a, Message> {
fn from(header_bar: HeaderBar<Message>) -> Self {
iced_lazy::component(header_bar)
impl<'a, Message: Clone + 'static> From<HeaderBar<'a, Message>> for Element<'a, Message> {
fn from(headerbar: HeaderBar<'a, Message>) -> Self {
headerbar.into_element()
}
}

View file

@ -1,29 +1,32 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Lazily-generated SVG icon widget for Iced.
use iced::{
widget::{svg, Image},
Length,
Length, ContentFit,
};
use std::borrow::Cow;
use std::hash::Hash;
use derive_setters::Setters;
use crate::{Element, Renderer};
pub fn icon<Renderer>(name: &str, size: u16) -> svg::Svg<Renderer>
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))
/// 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<ContentFit>,
#[setters(strip_option)]
width: Option<Length>,
#[setters(strip_option)]
height: Option<Length>,
}
pub fn image_icon(name: &str, size: u16) -> Option<Image> {
@ -37,3 +40,56 @@ pub fn image_icon(name: &str, size: u16) -> Option<Image> {
.height(Length::Units(size))
})
}
/// A lazily-generated SVG icon.
#[must_use]
pub fn icon<'a>(name: impl Into<Cow<'a, str>>, 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<Message: 'static>(self) -> Element<'a, Message> {
let (svg, svg_clone) = crate::utils::static_rc_halves(self);
iced_lazy::lazy(svg_clone, move || -> Element<Message> {
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::<Renderer>::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<Icon<'a>> for Element<'a, Message> {
fn from(icon: Icon<'a>) -> Self {
icon.into_svg::<Message>()
}
}

65
src/widget/list/column.rs Normal file
View file

@ -0,0 +1,65 @@
// Copyright 2022 System76 <info@system76.com>
// 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<Element<'a, Message>>,
}
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<Element<'a, Message>>) -> 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<ListColumn<'a, Message>> 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,
}
}

2
src/widget/list/item.rs Normal file
View file

@ -0,0 +1,2 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0

View file

@ -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,
<Renderer as iced_native::Renderer>::Theme: iced_style::rule::StyleSheet,
{
spacing: u16,
#[setters(into)]
padding: Padding,
width: Length,
height: Length,
max_width: u32,
align_items: Alignment,
style: <Renderer::Theme as StyleSheet>::Style,
children: Vec<Element<'a, Message, Renderer>>,
#[setters(strip_option)]
placeholder: Option<Element<'a, Message, Renderer>>,
show_separators: bool,
on_item_selected: Option<Box<dyn Fn(usize) -> Message + 'a>>,
}
pub fn list_box<'a, Message: 'a, Renderer>() -> ListBox<'a, Message, Renderer>
where
Renderer: iced_native::Renderer + 'a,
<<Renderer as iced_native::Renderer>::Theme as StyleSheet>::Style: From<Container>,
<Renderer as iced_native::Renderer>::Theme: StyleSheet + iced_style::rule::StyleSheet,
<<Renderer as iced_native::Renderer>::Theme as iced_style::rule::StyleSheet>::Style:
From<theme::Rule>,
{
ListBox::new()
}
impl<'a, Message: 'a, Renderer: iced_native::Renderer + 'a> ListBox<'a, Message, Renderer>
where
Renderer::Theme: StyleSheet + iced_style::rule::StyleSheet,
<<Renderer as iced_native::Renderer>::Theme as StyleSheet>::Style: From<Container>,
<<Renderer as iced_native::Renderer>::Theme as iced_style::rule::StyleSheet>::Style:
From<theme::Rule>,
{
/// 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::<Element<Message, Renderer>>::new()).render()
}
/// Creates a new [`ListBox`].
///
/// [`ListBox`]: struct.ListBox.html
pub fn with_children(children: Vec<Element<'a, Message, Renderer>>) -> 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<Element<'a, Message, Renderer>>) -> 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,
<<Renderer as iced_native::Renderer>::Theme as StyleSheet>::Style: From<Container>,
<<Renderer as iced_native::Renderer>::Theme as iced_style::rule::StyleSheet>::Style:
From<theme::Rule>,
{
fn default() -> Self {
Self::new()
}
}
impl<'a, Message, Renderer> Widget<Message, Renderer> for ListBox<'a, Message, Renderer>
where
Renderer: iced_native::Renderer,
<Renderer as 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<Tree> {
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<Message>,
) {
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<overlay::Element<'b, Message, Renderer>> {
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>(
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<ListBox<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
<Renderer as iced_native::Renderer>::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;

View file

@ -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<String>,
}
pub fn list_row<'a>() -> ListRow<'a> {
ListRow {
title: "",
subtitle: None,
icon: None,
}
}

View file

@ -1,146 +0,0 @@
pub use crate::Theme;
pub use iced::{widget, Background, Color};
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 crate::Theme;
use iced::widget;
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;
}

View file

@ -1,8 +1,8 @@
pub mod macros;
pub use macros::*;
// Copyright 2022 System76 <info@system76.com>
// 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};

View file

@ -1,29 +1,34 @@
// Copyright 2022 System76 <info@system76.com>
// 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 mod popup;
pub use popup::*;
pub use separator::{horizontal_rule, vertical_rule};

61
src/widget/nav_button.rs Normal file
View file

@ -0,0 +1,61 @@
// Copyright 2022 System76 <info@system76.com>
// 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<Message>,
}
#[must_use]
pub fn nav_button<Message>(title: &str) -> NavButton<Message> {
NavButton {
title,
sidebar_active: false,
on_sidebar_toggled: None,
}
}
impl<'a, Message: 'static + Clone> From<NavButton<'a, Message>> 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()
}
}

View file

@ -1,3 +1,6 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
pub mod nav_bar {
use crate::Theme;
use iced::{widget, Background, Color};

View file

@ -1,3 +1,6 @@
// Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
pub mod navbar;
pub use navbar::*;

View file

@ -1,14 +1,15 @@
use crate::scrollable;
// Copyright 2022 System76 <info@system76.com>
// 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<Message, Renderer> for NavBar<'a, Message>
where
Renderer: iced_native::Renderer + iced_native::text::Renderer + iced_native::svg::Renderer + 'a,
<Renderer as iced_native::Renderer>::Theme:
container::StyleSheet + button::StyleSheet + text::StyleSheet + scrollable::StyleSheet,
<<Renderer as iced_native::Renderer>::Theme as button::StyleSheet>::Style: From<theme::Button>,
<<Renderer as iced_native::Renderer>::Theme as container::StyleSheet>::Style:
From<theme::Container>,
<<Renderer as iced_native::Renderer>::Theme as text::StyleSheet>::Style: From<theme::Text>,
Renderer::Theme: iced_native::svg::StyleSheet,
{
impl<'a, Message> Component<Message, Renderer> 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<Element<Self::Event, Renderer>> = vec![];
let mut pages: Vec<Element<Self::Event, Renderer>> = vec![];
let mut sections: Vec<Element<'a, Self::Event, Renderer>> = vec![];
let mut pages: Vec<Element<'a, Self::Event, Renderer>> = vec![];
for (section, section_pages) in &self.source {
sections.push(
button(
column(vec![
icon(&section.icon, 20).into(),
text(&section.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,16 +167,16 @@ where
let nav_bar: Element<Self::Event, Renderer> =
container(if self.condensed && state.selected_page.is_some() {
row![container(scrollable!(column(pages)
.spacing(10)
.padding(10)
.max_width(200)
.width(Length::Units(200))
.height(Length::Shrink)))
.height(Length::Fill)
.style(theme::Container::Custom(nav_bar_pages_style))]
row![container(scrollable(column(pages)
.spacing(10)
.padding(10)
.max_width(200)
.width(Length::Units(200))
.height(Length::Shrink)))
.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,16 +210,8 @@ where
}
}
impl<'a, Message: 'a, Renderer> From<NavBar<'a, Message>> for Element<'a, Message, Renderer>
where
Renderer: iced_native::text::Renderer + iced_native::svg::Renderer + 'a,
<Renderer as iced_native::Renderer>::Theme:
container::StyleSheet + button::StyleSheet + text::StyleSheet + scrollable::StyleSheet,
<<Renderer as iced_native::Renderer>::Theme as button::StyleSheet>::Style: From<theme::Button>,
<<Renderer as iced_native::Renderer>::Theme as container::StyleSheet>::Style:
From<theme::Container>,
<<Renderer as iced_native::Renderer>::Theme as text::StyleSheet>::Style: From<theme::Text>,
Renderer::Theme: iced_native::svg::StyleSheet,
impl<'a, Message: 'static> From<NavBar<'a, Message>>
for Element<'a, Message, Renderer>
{
fn from(nav_bar: NavBar<'a, Message>) -> Self {
iced_lazy::component(nav_bar)

View file

@ -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 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
use crate::{Element, Renderer};
use iced::widget;
pub fn scrollable<'a, Message>(element: impl Into<Element<'a, Message>>) -> widget::Scrollable<'a, Message, Renderer> {
widget::scrollable(element)
.scrollbar_width(8)
.scroller_width(8)
}

View file

@ -1,7 +1,25 @@
#[macro_export]
macro_rules! separator {
($size:expr) => {
$crate::iced::widget::horizontal_rule($size)
.style($crate::theme::Rule::Custom($crate::widget::separator_style))
};
// Copyright 2022 System76 <info@system76.com>
// 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<Renderer> {
widget::horizontal_rule(size).style(theme::Rule::Custom(separator_style))
}
#[must_use]
pub fn vertical_rule(size: u16) -> widget::Rule<Renderer> {
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),
}
}

View file

@ -0,0 +1,26 @@
// Copyright 2022 System76 <info@system76.com>
// 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<Element<'a, Message>>) -> 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<Message>(children: Vec<Element<Message>>) -> iced::widget::Row<Message, Renderer> {
iced::widget::row(children)
.align_items(iced::Alignment::Center)
.padding([0, 8])
.spacing(12)
}

View file

@ -0,0 +1,17 @@
// Copyright 2022 System76 <info@system76.com>
// 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<Message: 'static>(children: Vec<Element<Message>>) -> Column<Message, Renderer> {
column(children).spacing(24).padding(24).max_width(600)
}

View file

@ -0,0 +1,39 @@
// Copyright 2022 System76 <info@system76.com>
// 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<Message: 'static>(
title: &str,
) -> Section<Message> {
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<Element<'a, Message>>) -> Self {
self.children = self.children.add(item.into());
self
}
}
impl<'a, Message: 'static> From<Section<'a, Message>> 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()
}
}

View file

@ -1,14 +1,14 @@
// Copyright 2022 System76 <info@system76.com>
// 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<Option<String>>,
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)