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" palette = "0.6.1"
cosmic-panel-config = {git = "https://github.com/pop-os/cosmic-panel", default-features = false, optional = true } 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 } sctk = { package = "smithay-client-toolkit", git = "https://github.com/Smithay/client-toolkit", optional = true }
static-rc = "0.6.1"
[dependencies.cosmic-theme] [dependencies.cosmic-theme]
git = "https://github.com/pop-os/cosmic-theme.git" git = "https://github.com/pop-os/cosmic-theme.git"

View file

@ -4,7 +4,7 @@ use cosmic::{
}; };
mod window; mod window;
pub use window::*; pub use window::Window;
pub fn main() -> cosmic::iced::Result { pub fn main() -> cosmic::iced::Result {
let mut settings = settings(); 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::{ 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_native::window,
list_view, list_view_item, list_view_row, list_view_section, scrollable, iced::widget::{
theme::{self, Button, Theme}, column, container, horizontal_space, pick_list, progress_bar, radio, row, slider,
widget::{button, header_bar, list_box, list_row, list_view::*, toggler}, },
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, Element,
ElementExt,
}; };
use iced_sctk::application::SurfaceIdWrapper; use iced_sctk::application::SurfaceIdWrapper;
use std::collections::BTreeMap; use std::{collections::BTreeMap, vec};
use theme::Button as ButtonTheme;
#[derive(Default)] #[derive(Default)]
pub struct Window { pub struct Window {
title: String,
page: u8, page: u8,
debug: bool, debug: bool,
theme: Theme, theme: Theme,
@ -64,6 +67,7 @@ pub enum Message {
Drag, Drag,
Minimize, Minimize,
Maximize, Maximize,
InputChanged,
} }
impl Application for Window { impl Application for Window {
@ -80,11 +84,12 @@ impl Application for Window {
window.slider_value = 50.0; window.slider_value = 50.0;
// window.theme = Theme::Light; // window.theme = Theme::Light;
window.pick_list_selected = Some("Option 1"); window.pick_list_selected = Some("Option 1");
window.title = String::from("COSMIC Design System - Iced");
(window, Command::none()) (window, Command::none())
} }
fn title(&self) -> String { fn title(&self) -> String {
String::from("COSMIC Design System - Iced") self.title.clone()
} }
fn update(&mut self, message: Message) -> iced::Command<Self::Message> { 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::ThemeChanged(theme) => self.theme = theme,
Message::ButtonPressed => {} Message::ButtonPressed => {}
Message::SliderChanged(value) => self.slider_value = value, Message::SliderChanged(value) => self.slider_value = value,
Message::CheckboxToggled(value) => self.checkbox_value = value, Message::CheckboxToggled(value) => {
self.checkbox_value = value;
},
Message::TogglerToggled(value) => self.toggler_value = value, Message::TogglerToggled(value) => self.toggler_value = value,
Message::PickListSelected(value) => self.pick_list_selected = Some(value), Message::PickListSelected(value) => self.pick_list_selected = Some(value),
Message::Close => self.exit = true, Message::Close => self.exit = true,
Message::ToggleSidebar => self.sidebar_toggled = !self.sidebar_toggled, Message::ToggleSidebar => self.sidebar_toggled = !self.sidebar_toggled,
// Message::Drag => return drag(window::Id::new(0)), Message::Drag => todo!(),
// Message::Minimize => return minimize(window::Id::new(0), true), Message::Minimize => todo!(),
// Message::Maximize => return maximize(window::Id::new(0), true), Message::Maximize => todo!(),
Message::RowSelected(row) => println!("Selected row {row}"), Message::RowSelected(row) => println!("Selected row {row}"),
_ => {} Message::InputChanged => {},
} }
Command::none() Command::none()
} }
fn close_requested(&self, _: SurfaceIdWrapper) -> Self::Message {
unimplemented!()
}
fn view(&self, _: SurfaceIdWrapper) -> Element<Message> { fn view(&self, _: SurfaceIdWrapper) -> Element<Message> {
let mut header: Element<Message> = header_bar() let mut header = header_bar()
.title(self.title()) .title("COSMIC Design System - Iced")
.nav_title(String::from("Settings"))
.sidebar_active(self.sidebar_toggled)
.show_minimize(self.show_minimize)
.show_maximize(self.show_maximize)
.on_close(Message::Close) .on_close(Message::Close)
.on_drag(Message::Drag) .on_drag(Message::Drag)
.on_maximize(Message::Maximize) .start(
.on_minimize(Message::Minimize) nav_button("Settings")
.on_sidebar_toggle(Message::ToggleSidebar) .on_sidebar_toggled(Message::ToggleSidebar)
.into(); .sidebar_active(self.sidebar_toggled)
.into()
);
if self.debug { if self.show_maximize {
header = header.explain(Color::WHITE); 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 // 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 // involves allocations for many different items. Ideally, we could only make the nav bar
// responsive and leave the content to be sized normally. // responsive and leave the content to be sized normally.
@ -138,26 +146,26 @@ impl Application for Window {
let condensed = size.width < 900.0; let condensed = size.width < 900.0;
// cosmic::navbar![ // cosmic::navbar![
// nav_button!("network-wireless", "Network & Wireless", condensed) // nav_text_button("network-wireless", "Network & Wireless", condensed)
// .on_press(Message::Page(0)) // .on_press(Message::Page(0))
// .style(if self.page == 0 { // .style(if self.page == 0 {
// theme::Button::Primary // ButtonTheme::Primary
// } else { // } else {
// theme::Button::Text // ButtonTheme::Text
// }), // }),
// nav_button!("preferences-desktop", "Bluetooth", condensed) // nav_text_button("preferences-desktop", "Bluetooth", condensed)
// .on_press(Message::Page(1)) // .on_press(Message::Page(1))
// .style(if self.page == 1 { // .style(if self.page == 1 {
// theme::Button::Primary // ButtonTheme::Primary
// } else { // } 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)) // .on_press(Message::Page(2))
// .style(if self.page == 2 { // .style(if self.page == 2 {
// theme::Button::Primary // ButtonTheme::Primary
// } else { // } else {
// theme::Button::Text // ButtonTheme::Text
// }), // }),
// ] // ]
@ -221,49 +229,48 @@ impl Application for Window {
}, },
); );
let content: Element<_> = list_view!( let content: Element<_> = settings::view_column(vec![
list_view_section!( settings::view_section("Debug")
"Debug", .add(settings::item("Debug theme", choose_theme))
list_view_item!("Debug theme", choose_theme), .add(settings::item(
list_view_item!(
"Debug layout", "Debug layout",
toggler(String::from("Debug layout"), self.debug, Message::Debug,) toggler(String::from("Debug layout"), self.debug, Message::Debug)
) ))
), .into(),
list_view_section!( settings::view_section("Buttons")
"Buttons", .add(settings::item_row(vec![
list_view_row!( button(ButtonTheme::Primary)
button!("Primary") .text("Primary")
.style(theme::Button::Primary) .on_press(Message::ButtonPressed)
.on_press(Message::ButtonPressed), .into(),
button!("Secondary") button(ButtonTheme::Secondary)
.style(theme::Button::Secondary) .text("Secondary")
.on_press(Message::ButtonPressed), .on_press(Message::ButtonPressed)
button!("Positive") .into(),
.style(theme::Button::Positive) button(ButtonTheme::Positive)
.on_press(Message::ButtonPressed), .text("Positive")
button!("Destructive") .on_press(Message::ButtonPressed)
.style(theme::Button::Destructive) .into(),
.on_press(Message::ButtonPressed), button(ButtonTheme::Destructive)
button!("Text") .text("Destructive")
.style(theme::Button::Text) .on_press(Message::ButtonPressed)
.on_press(Message::ButtonPressed), .into(),
), button(ButtonTheme::Text)
list_view_row!( .text("Text")
button!("Primary").style(theme::Button::Primary), .on_press(Message::ButtonPressed)
button!("Secondary").style(theme::Button::Secondary), .into()
button!("Positive").style(theme::Button::Positive), ]))
button!("Destructive").style(theme::Button::Destructive), .add(settings::item_row(vec![
button!("Text").style(theme::Button::Text), button(ButtonTheme::Primary).text("Primary").into(),
), button(ButtonTheme::Secondary).text("Secondary").into(),
), button(ButtonTheme::Positive).text("Positive").into(),
list_view_section!( button(ButtonTheme::Destructive).text("Destructive").into(),
"Controls", button(ButtonTheme::Text).text("Text").into(),
list_view_item!( ]))
"Toggler", .into(),
toggler(None, self.toggler_value, Message::TogglerToggled) settings::view_section("Controls")
), .add(settings::item("Toggler", toggler(None, self.toggler_value, Message::TogglerToggled)))
list_view_item!( .add(settings::item(
"Pick List (TODO)", "Pick List (TODO)",
pick_list( pick_list(
vec!["Option 1", "Option 2", "Option 3", "Option 4",], vec!["Option 1", "Option 2", "Option 3", "Option 4",],
@ -271,72 +278,30 @@ impl Application for Window {
Message::PickListSelected Message::PickListSelected
) )
.padding([8, 0, 8, 16]) .padding([8, 0, 8, 16])
), ))
list_view_item!( .add(settings::item(
"Slider", "Slider",
slider(0.0..=100.0, self.slider_value, Message::SliderChanged) slider(0.0..=100.0, self.slider_value, Message::SliderChanged)
.width(Length::Units(250)) .width(Length::Units(250))
), ))
list_view_item!( .add(settings::item(
"Progress", "Progress",
progress_bar(0.0..=100.0, self.slider_value) progress_bar(0.0..=100.0, self.slider_value)
.width(Length::Units(250)) .width(Length::Units(250))
.height(Length::Units(4)) .height(Length::Units(4))
), ))
checkbox("Checkbox", self.checkbox_value, Message::CheckboxToggled), .into()
), ])
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); let mut widgets = Vec::with_capacity(2);
widgets.push(if self.debug { widgets.push(sidebar.debug(self.debug));
sidebar.explain(Color::WHITE)
} else {
sidebar
});
widgets.push( widgets.push(
scrollable!(row![ scrollable(row![
horizontal_space(Length::Fill), horizontal_space(Length::Fill),
if self.debug { content.debug(self.debug),
content.explain(Color::WHITE)
} else {
content
},
horizontal_space(Length::Fill), horizontal_space(Length::Fill),
]) ])
.into(), .into(),
@ -360,4 +325,8 @@ impl Application for Window {
fn theme(&self) -> Theme { fn theme(&self) -> 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}; use cosmic::{iced::Application, settings};
mod window; 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::{ 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_native::window,
iced_winit::window::{drag, maximize, minimize}, iced::widget::{
list_view, list_view_item, list_view_row, list_view_section, scrollable, 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}, 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, Element,
ElementExt,
}; };
use std::collections::BTreeMap; use std::{collections::BTreeMap, vec};
use cosmic::widget::widget::text_input::Id as TextInputId; use theme::Button as ButtonTheme;
use cosmic::widget::widget::text_input;
#[derive(Default)] #[derive(Default)]
pub struct Window { pub struct Window {
title: String,
page: u8, page: u8,
debug: bool, debug: bool,
theme: Theme, theme: Theme,
@ -83,11 +84,12 @@ impl Application for Window {
window.slider_value = 50.0; window.slider_value = 50.0;
// window.theme = Theme::Light; // window.theme = Theme::Light;
window.pick_list_selected = Some("Option 1"); window.pick_list_selected = Some("Option 1");
window.title = String::from("COSMIC Design System - Iced");
(window, Command::none()) (window, Command::none())
} }
fn title(&self) -> String { fn title(&self) -> String {
String::from("COSMIC Design System - Iced") self.title.clone()
} }
fn update(&mut self, message: Message) -> iced::Command<Self::Message> { 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::SliderChanged(value) => self.slider_value = value,
Message::CheckboxToggled(value) => { Message::CheckboxToggled(value) => {
self.checkbox_value = value; self.checkbox_value = value;
return text_input::focus(TextInputId::new("launcher_entry"));
}, },
Message::TogglerToggled(value) => self.toggler_value = value, Message::TogglerToggled(value) => self.toggler_value = value,
Message::PickListSelected(value) => self.pick_list_selected = Some(value), Message::PickListSelected(value) => self.pick_list_selected = Some(value),
@ -107,7 +108,7 @@ impl Application for Window {
Message::ToggleSidebar => self.sidebar_toggled = !self.sidebar_toggled, Message::ToggleSidebar => self.sidebar_toggled = !self.sidebar_toggled,
Message::Drag => return drag(window::Id::new(0)), Message::Drag => return drag(window::Id::new(0)),
Message::Minimize => return minimize(window::Id::new(0), true), Message::Minimize => return minimize(window::Id::new(0), true),
Message::Maximize => return maximize(window::Id::new(0), true), Message::Maximize => return toggle_maximize(window::Id::new(0)),
Message::RowSelected(row) => println!("Selected row {row}"), Message::RowSelected(row) => println!("Selected row {row}"),
Message::InputChanged => {}, Message::InputChanged => {},
@ -117,23 +118,27 @@ impl Application for Window {
} }
fn view(&self) -> Element<Message> { fn view(&self) -> Element<Message> {
let mut header: Element<Message> = header_bar() let mut header = header_bar()
.title(self.title()) .title("COSMIC Design System - Iced")
.nav_title(String::from("Settings"))
.sidebar_active(self.sidebar_toggled)
.show_minimize(self.show_minimize)
.show_maximize(self.show_maximize)
.on_close(Message::Close) .on_close(Message::Close)
.on_drag(Message::Drag) .on_drag(Message::Drag)
.on_maximize(Message::Maximize) .start(
.on_minimize(Message::Minimize) nav_button("Settings")
.on_sidebar_toggle(Message::ToggleSidebar) .on_sidebar_toggled(Message::ToggleSidebar)
.into(); .sidebar_active(self.sidebar_toggled)
.into()
);
if self.debug { if self.show_maximize {
header = header.explain(Color::WHITE); 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 // 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 // involves allocations for many different items. Ideally, we could only make the nav bar
// responsive and leave the content to be sized normally. // responsive and leave the content to be sized normally.
@ -141,26 +146,26 @@ impl Application for Window {
let condensed = size.width < 900.0; let condensed = size.width < 900.0;
// cosmic::navbar![ // cosmic::navbar![
// nav_button!("network-wireless", "Network & Wireless", condensed) // nav_text_button("network-wireless", "Network & Wireless", condensed)
// .on_press(Message::Page(0)) // .on_press(Message::Page(0))
// .style(if self.page == 0 { // .style(if self.page == 0 {
// theme::Button::Primary // ButtonTheme::Primary
// } else { // } else {
// theme::Button::Text // ButtonTheme::Text
// }), // }),
// nav_button!("preferences-desktop", "Bluetooth", condensed) // nav_text_button("preferences-desktop", "Bluetooth", condensed)
// .on_press(Message::Page(1)) // .on_press(Message::Page(1))
// .style(if self.page == 1 { // .style(if self.page == 1 {
// theme::Button::Primary // ButtonTheme::Primary
// } else { // } 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)) // .on_press(Message::Page(2))
// .style(if self.page == 2 { // .style(if self.page == 2 {
// theme::Button::Primary // ButtonTheme::Primary
// } else { // } else {
// theme::Button::Text // ButtonTheme::Text
// }), // }),
// ] // ]
@ -224,49 +229,48 @@ impl Application for Window {
}, },
); );
let content: Element<_> = list_view!( let content: Element<_> = settings::view_column(vec![
list_view_section!( settings::view_section("Debug")
"Debug", .add(settings::item("Debug theme", choose_theme))
list_view_item!("Debug theme", choose_theme), .add(settings::item(
list_view_item!(
"Debug layout", "Debug layout",
toggler(String::from("Debug layout"), self.debug, Message::Debug,) toggler(String::from("Debug layout"), self.debug, Message::Debug)
) ))
), .into(),
list_view_section!( settings::view_section("Buttons")
"Buttons", .add(settings::item_row(vec![
list_view_row!( button(ButtonTheme::Primary)
button!("Primary") .text("Primary")
.style(theme::Button::Primary) .on_press(Message::ButtonPressed)
.on_press(Message::ButtonPressed), .into(),
button!("Secondary") button(ButtonTheme::Secondary)
.style(theme::Button::Secondary) .text("Secondary")
.on_press(Message::ButtonPressed), .on_press(Message::ButtonPressed)
button!("Positive") .into(),
.style(theme::Button::Positive) button(ButtonTheme::Positive)
.on_press(Message::ButtonPressed), .text("Positive")
button!("Destructive") .on_press(Message::ButtonPressed)
.style(theme::Button::Destructive) .into(),
.on_press(Message::ButtonPressed), button(ButtonTheme::Destructive)
button!("Text") .text("Destructive")
.style(theme::Button::Text) .on_press(Message::ButtonPressed)
.on_press(Message::ButtonPressed), .into(),
), button(ButtonTheme::Text)
list_view_row!( .text("Text")
button!("Primary").style(theme::Button::Primary), .on_press(Message::ButtonPressed)
button!("Secondary").style(theme::Button::Secondary), .into()
button!("Positive").style(theme::Button::Positive), ]))
button!("Destructive").style(theme::Button::Destructive), .add(settings::item_row(vec![
button!("Text").style(theme::Button::Text), button(ButtonTheme::Primary).text("Primary").into(),
), button(ButtonTheme::Secondary).text("Secondary").into(),
), button(ButtonTheme::Positive).text("Positive").into(),
list_view_section!( button(ButtonTheme::Destructive).text("Destructive").into(),
"Controls", button(ButtonTheme::Text).text("Text").into(),
list_view_item!( ]))
"Toggler", .into(),
toggler(None, self.toggler_value, Message::TogglerToggled) settings::view_section("Controls")
), .add(settings::item("Toggler", toggler(None, self.toggler_value, Message::TogglerToggled)))
list_view_item!( .add(settings::item(
"Pick List (TODO)", "Pick List (TODO)",
pick_list( pick_list(
vec!["Option 1", "Option 2", "Option 3", "Option 4",], vec!["Option 1", "Option 2", "Option 3", "Option 4",],
@ -274,76 +278,30 @@ impl Application for Window {
Message::PickListSelected Message::PickListSelected
) )
.padding([8, 0, 8, 16]) .padding([8, 0, 8, 16])
), ))
list_view_item!( .add(settings::item(
"Slider", "Slider",
slider(0.0..=100.0, self.slider_value, Message::SliderChanged) slider(0.0..=100.0, self.slider_value, Message::SliderChanged)
.width(Length::Units(250)) .width(Length::Units(250))
), ))
list_view_item!( .add(settings::item(
"Progress", "Progress",
progress_bar(0.0..=100.0, self.slider_value) progress_bar(0.0..=100.0, self.slider_value)
.width(Length::Units(250)) .width(Length::Units(250))
.height(Length::Units(4)) .height(Length::Units(4))
), ))
checkbox("Checkbox", self.checkbox_value, Message::CheckboxToggled), .into()
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); let mut widgets = Vec::with_capacity(2);
widgets.push(if self.debug { widgets.push(sidebar.debug(self.debug));
sidebar.explain(Color::WHITE)
} else {
sidebar
});
widgets.push( widgets.push(
scrollable!(row![ scrollable(row![
horizontal_space(Length::Fill), horizontal_space(Length::Fill),
if self.debug { content.debug(self.debug),
content.explain(Color::WHITE)
} else {
content
},
horizontal_space(Length::Fill), horizontal_space(Length::Fill),
]) ])
.into(), .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 use iced::Font;
pub const FONT: Font = Font::External { 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;
pub use iced_lazy; pub use iced_lazy;
pub use iced_native; pub use iced_native;
@ -11,19 +14,15 @@ pub mod font;
pub mod theme; pub mod theme;
pub mod widget; pub mod widget;
mod ext;
pub use ext::ElementExt;
mod utils;
pub use theme::Theme; pub use theme::Theme;
pub type Renderer = iced::Renderer<Theme>; pub type Renderer = iced::Renderer<Theme>;
pub type Element<'a, Message> = iced::Element<'a, Message, 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<Flags: Default>() -> iced::Settings<Flags> { pub fn settings<Flags: Default>() -> iced::Settings<Flags> {
let mut settings = iced::Settings::default(); let mut settings = iced::Settings::default();
settings.default_font = match font::FONT { 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}; use iced_core::{Background, Color};
/// The appearance of a [`Expander`](crate::native::expander::Expander). /// 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 expander;
pub mod palette; pub mod palette;
use std::hash::Hash;
use std::hash::Hasher;
pub use self::palette::Palette; pub use self::palette::Palette;
use cosmic_theme::Component; use cosmic_theme::Component;
@ -52,6 +58,7 @@ pub enum Theme {
} }
impl Theme { impl Theme {
#[must_use]
pub fn cosmic(self) -> &'static CosmicTheme { pub fn cosmic(self) -> &'static CosmicTheme {
match self { match self {
Self::Dark => &COSMIC_DARK, Self::Dark => &COSMIC_DARK,
@ -59,6 +66,7 @@ impl Theme {
} }
} }
#[must_use]
pub fn palette(self) -> Palette { pub fn palette(self) -> Palette {
match self { match self {
Self::Dark => Palette::DARK, Self::Dark => Palette::DARK,
@ -66,6 +74,7 @@ impl Theme {
} }
} }
#[must_use]
pub fn extended_palette(&self) -> &self::palette::Extended { pub fn extended_palette(&self) -> &self::palette::Extended {
match self { match self {
Self::Dark => &self::palette::EXTENDED_DARK, Self::Dark => &self::palette::EXTENDED_DARK,
@ -113,10 +122,11 @@ impl application::StyleSheet for Theme {
*/ */
#[derive(Debug, Clone, Copy, PartialEq, Eq)] #[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Button { pub enum Button {
Deactivated,
Destructive,
Positive,
Primary, Primary,
Secondary, Secondary,
Positive,
Destructive,
Text, Text,
Transparent, Transparent,
} }
@ -137,6 +147,7 @@ impl Button {
Button::Destructive => &cosmic.destructive, Button::Destructive => &cosmic.destructive,
Button::Text => &cosmic.secondary.component, Button::Text => &cosmic.secondary.component,
Button::Transparent => &TRANSPARENT_COMPONENT, Button::Transparent => &TRANSPARENT_COMPONENT,
Button::Deactivated => &cosmic.secondary.component,
} }
} }
} }
@ -189,7 +200,11 @@ impl Default for Checkbox {
impl checkbox::StyleSheet for Theme { impl checkbox::StyleSheet for Theme {
type Style = Checkbox; 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(); let palette = self.extended_palette();
match style { 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(); let palette = self.extended_palette();
match style { match style {
@ -291,7 +310,7 @@ impl expander::StyleSheet for Theme {
fn appearance(&self, style: Self::Style) -> expander::Appearance { fn appearance(&self, style: Self::Style) -> expander::Appearance {
match style { match style {
Expander::Default => Default::default(), Expander::Default => expander::Appearance::default(),
Expander::Custom(f) => f(self), Expander::Custom(f) => f(self),
} }
} }
@ -324,7 +343,7 @@ impl container::StyleSheet for Theme {
fn appearance(&self, style: &Self::Style) -> container::Appearance { fn appearance(&self, style: &Self::Style) -> container::Appearance {
match style { match style {
Container::Transparent => Default::default(), Container::Transparent => container::Appearance::default(),
Container::Box => { Container::Box => {
let palette = self.extended_palette(); let palette = self.extended_palette();
@ -368,7 +387,9 @@ impl slider::StyleSheet for Theme {
fn hovered(&self, style: &Self::Style) -> slider::Appearance { fn hovered(&self, style: &Self::Style) -> slider::Appearance {
let mut style = self.active(&style); 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_width = 6.0;
style.handle.border_color = match self { style.handle.border_color = match self {
Theme::Dark => Color::from_rgba8(0xFF, 0xFF, 0xFF, 0.1), 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 { impl radio::StyleSheet for Theme {
type Style = (); 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(); let palette = self.extended_palette();
radio::Appearance { radio::Appearance {
@ -456,8 +477,8 @@ impl radio::StyleSheet for Theme {
} }
} }
fn hovered(&self, style: &Self::Style, is_checked: bool) -> radio::Appearance { fn hovered(&self, style: &Self::Style, is_selected: bool) -> radio::Appearance {
let active = self.active(&style, is_checked); let active = self.active(&style, is_selected);
let palette = self.extended_palette(); let palette = self.extended_palette();
radio::Appearance { radio::Appearance {
@ -474,7 +495,11 @@ impl radio::StyleSheet for Theme {
impl toggler::StyleSheet for Theme { impl toggler::StyleSheet for Theme {
type Style = (); 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(); let palette = self.palette();
toggler::Appearance { 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 //TODO: grab colors from palette
match self { match self {
Theme::Dark => toggler::Appearance { Theme::Dark => toggler::Appearance {
@ -515,7 +544,7 @@ impl toggler::StyleSheet for Theme {
Color::from_rgb8(0x54, 0x54, 0x54) Color::from_rgb8(0x54, 0x54, 0x54)
}, },
..self.active(&style, is_active) ..self.active(&style, is_active)
}, }
} }
} }
} }
@ -659,23 +688,42 @@ impl scrollable::StyleSheet for Theme {
#[derive(Default, Clone, Copy)] #[derive(Default, Clone, Copy)]
pub enum Svg { pub enum Svg {
/// Apply a custom appearance filter
Custom(fn(&Theme) -> svg::Appearance), Custom(fn(&Theme) -> svg::Appearance),
/// No filtering is applied
#[default] #[default]
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 { impl svg::StyleSheet for Theme {
type Style = Svg; type Style = Svg;
fn appearance(&self, style: Self::Style) -> svg::Appearance { fn appearance(&self, style: Self::Style) -> svg::Appearance {
let cosmic = self.cosmic();
match style { match style {
Svg::Default => Default::default(), Svg::Default => svg::Appearance::default(),
Svg::Custom(appearance) => appearance(self), Svg::Custom(appearance) => appearance(self),
Svg::Accent => svg::Appearance { Svg::Symbolic => svg::Appearance {
fill: Some(cosmic.accent.base.into()), 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 { Text::Accent => text::Appearance {
color: Some(self.cosmic().accent.base.into()), 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::Color(c) => text::Appearance { color: Some(c) },
Text::Custom(f) => f(self), 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 //TODO: GET CORRECT PALETTE FROM COSMIC-THEME
use iced_core::Color; use iced_core::Color;
@ -85,6 +88,7 @@ lazy_static! {
} }
impl Extended { impl Extended {
#[must_use]
pub fn generate(palette: Palette) -> Self { pub fn generate(palette: Palette) -> Self {
Self { Self {
background: Background::new(palette.background, palette.text), background: Background::new(palette.background, palette.text),
@ -118,6 +122,7 @@ pub struct Background {
} }
impl Background { impl Background {
#[must_use]
pub fn new(base: Color, text: Color) -> Self { pub fn new(base: Color, text: Color) -> Self {
let weak = mix(base, text, 0.15); let weak = mix(base, text, 0.15);
let strong = mix(base, text, 0.40); let strong = mix(base, text, 0.40);
@ -137,6 +142,7 @@ pub struct Primary {
} }
impl Primary { impl Primary {
#[must_use]
pub fn generate(base: Color, background: Color, text: Color) -> Self { pub fn generate(base: Color, background: Color, text: Color) -> Self {
let weak = mix(base, background, 0.4); let weak = mix(base, background, 0.4);
let strong = deviate(base, 0.1); let strong = deviate(base, 0.1);
@ -156,6 +162,7 @@ pub struct Secondary {
} }
impl Secondary { impl Secondary {
#[must_use]
pub fn generate(base: Color, text: Color) -> Self { pub fn generate(base: Color, text: Color) -> Self {
let base = mix(base, text, 0.2); let base = mix(base, text, 0.2);
let weak = mix(base, text, 0.1); let weak = mix(base, text, 0.1);
@ -176,6 +183,7 @@ pub struct Success {
} }
impl Success { impl Success {
#[must_use]
pub fn generate(base: Color, background: Color, text: Color) -> Self { pub fn generate(base: Color, background: Color, text: Color) -> Self {
let weak = mix(base, background, 0.4); let weak = mix(base, background, 0.4);
let strong = deviate(base, 0.1); let strong = deviate(base, 0.1);
@ -195,6 +203,7 @@ pub struct Danger {
} }
impl Danger { impl Danger {
#[must_use]
pub fn generate(base: Color, background: Color, text: Color) -> Self { pub fn generate(base: Color, background: Color, text: Color) -> Self {
let weak = mix(base, background, 0.4); let weak = mix(base, background, 0.4);
let strong = deviate(base, 0.1); 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] // Copyright 2022 System76 <info@system76.com>
macro_rules! button { // SPDX-License-Identifier: MPL-2.0
($($x:expr),+ $(,)?) => (
$crate::iced::widget::Button::new( use crate::{theme, Element, Renderer};
$crate::iced::widget::Row::with_children( use iced::widget;
vec![$($crate::iced::Element::from($x)),+]
) /// A button widget with COSMIC styling
.spacing(8) #[must_use]
) pub const fn button<Message>(style: theme::Button) -> Button<Message> {
.padding([8, 16]) 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 apply::Apply;
use derive_setters::*; use derive_setters::Setters;
use iced::{self, alignment::Vertical, widget, Length}; use iced::{self, widget, Length};
use iced_lazy::Component; 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)] #[derive(Setters)]
pub struct HeaderBar<Message> { pub struct HeaderBar<'a, Message> {
title: String, title: &'a str,
nav_title: String,
sidebar_active: bool,
show_minimize: bool,
show_maximize: bool,
#[setters(strip_option)] #[setters(strip_option)]
on_close: Option<Message>, on_close: Option<Message>,
#[setters(strip_option)] #[setters(strip_option)]
@ -20,133 +32,98 @@ pub struct HeaderBar<Message> {
#[setters(strip_option)] #[setters(strip_option)]
on_minimize: Option<Message>, on_minimize: Option<Message>,
#[setters(strip_option)] #[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> { impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
HeaderBar { /// Converts the headerbar builder into an Iced element.
title: String::default(), pub fn into_element(mut self) -> Element<'a, Message> {
nav_title: String::default(), let mut packed: Vec<Element<Message>> = Vec::with_capacity(4);
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,
}
}
#[derive(Debug, Clone)] if let Some(start) = self.start.take() {
pub enum HeaderEvent { packed.push(widget::container(start).align_x(iced::alignment::Horizontal::Left).into());
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(),
} }
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> { fn title_widget(&self) -> Element<'a, Message> {
let nav_button = { widget::container(widget::text(self.title))
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))
.center_x() .center_x()
.center_y() .center_y()
.width(Length::Fill) .width(Length::Fill)
.height(Length::Fill) .height(Length::Fill)
.into(); .into()
}
let window_controls = { /// Creates the widget for window controls.
let mut widgets: Vec<Element<HeaderEvent>> = Vec::with_capacity(3); fn window_controls(&mut self) -> Element<'a, Message> {
let mut widgets: Vec<Element<_>> = Vec::with_capacity(3);
let icon = |name, size, on_press| { let icon = |name, size, on_press| {
super::icon(name, size) super::icon(name, size)
.style(crate::theme::Svg::Accent) .style(crate::theme::Svg::SymbolicActive)
.apply(widget::button) .apply(iced::widget::button)
.style(theme::Button::Text) .style(theme::Button::Text)
.on_press(on_press) .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()
}; };
widget::row(vec![nav_button, content, window_controls]) if let Some(message) = self.on_minimize.take() {
.height(Length::Units(50)) widgets.push(icon("window-minimize-symbolic", 16, message).into());
.padding(10) }
.apply(widget::event_container)
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() .center_y()
.on_press(HeaderEvent::Drag)
.on_release(HeaderEvent::Maximize)
.into() .into()
} }
} }
impl<'a, Message: Clone + 'a> From<HeaderBar<Message>> for Element<'a, Message> { impl<'a, Message: Clone + 'static> From<HeaderBar<'a, Message>> for Element<'a, Message> {
fn from(header_bar: HeaderBar<Message>) -> Self { fn from(headerbar: HeaderBar<'a, Message>) -> Self {
iced_lazy::component(header_bar) 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::{ use iced::{
widget::{svg, Image}, 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> /// A lazily-generated SVG icon.
where #[derive(Hash, Setters)]
Renderer: iced_native::svg::Renderer, pub struct Icon<'a> {
Renderer::Theme: iced_native::svg::StyleSheet, #[setters(skip)]
{ name: Cow<'a, str>,
let handle = match freedesktop_icons::lookup(name) #[setters(into)]
.with_size(size) theme: Cow<'a, str>,
.with_theme("Pop") style: crate::theme::Svg,
.with_cache() size: u16,
.force_svg() #[setters(strip_option)]
.find() content_fit: Option<ContentFit>,
{ #[setters(strip_option)]
Some(path) => svg::Handle::from_path(path), width: Option<Length>,
None => { #[setters(strip_option)]
eprintln!("icon '{}' size {} not found", name, size); height: Option<Length>,
svg::Handle::from_memory(Vec::new())
}
};
svg::Svg::new(handle)
.width(Length::Units(size))
.height(Length::Units(size))
} }
pub fn image_icon(name: &str, size: u16) -> Option<Image> { 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)) .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; // Copyright 2022 System76 <info@system76.com>
pub use macros::*; // SPDX-License-Identifier: MPL-2.0
pub mod list_row; mod column;
pub use list_row::*; // mod item;
pub mod list_box; pub use self::column::{ListColumn, list_column};
pub use list_box::*; // 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; mod button;
pub use button::*; pub use button::*;
mod header_bar; mod header_bar;
pub use header_bar::*; pub use header_bar::{HeaderBar, header_bar};
mod icon; 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 mod navigation;
pub use navigation::*; pub use navigation::*;
mod toggler; mod toggler;
pub use toggler::*; pub use toggler::toggler;
pub mod settings;
mod scrollable; mod scrollable;
pub use scrollable::*; pub use scrollable::*;
mod expander;
pub use expander::*;
pub mod list;
pub use list::*;
pub mod separator; pub mod separator;
pub use separator::*;
pub mod popup; pub mod popup;
pub use 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 { pub mod nav_bar {
use crate::Theme; use crate::Theme;
use iced::{widget, Background, Color}; 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 mod navbar;
pub use 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::nav_bar::{nav_bar_pages_style, nav_bar_sections_style};
use crate::widget::{icon, Background}; use crate::widget::{icon, scrollable};
use crate::{theme, Theme}; use crate::{theme, Renderer, Theme};
use derive_setters::Setters; use derive_setters::Setters;
use iced::Length; use iced::{Background, Length};
use iced_lazy::Component; use iced_lazy::Component;
use iced_native::widget::{button, column, container, text}; use iced_native::widget::{button, column, container, text};
use iced_native::{row, Alignment, Element}; use iced_native::{row, Alignment, Element};
use iced_style::button::Appearance; use iced_style::button::Appearance;
use iced_style::scrollable;
use std::collections::BTreeMap; use std::collections::BTreeMap;
#[derive(Setters, Default)] #[derive(Setters, Default)]
@ -44,10 +45,7 @@ pub struct NavBarSection {
impl NavBarSection { impl NavBarSection {
pub fn new() -> Self { pub fn new() -> Self {
Self { Self::default()
title: String::new(),
icon: String::new(),
}
} }
} }
@ -89,17 +87,7 @@ pub struct NavBarState {
page_active: bool, page_active: bool,
} }
impl<'a, Message, Renderer> Component<Message, Renderer> for NavBar<'a, Message> impl<'a, Message> 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,
{
type State = NavBarState; type State = NavBarState;
type Event = NavBarEvent; type Event = NavBarEvent;
@ -132,15 +120,15 @@ where
fn view(&self, state: &Self::State) -> Element<'a, Self::Event, Renderer> { fn view(&self, state: &Self::State) -> Element<'a, Self::Event, Renderer> {
if self.active { if self.active {
let mut sections: Vec<Element<Self::Event, Renderer>> = vec![]; let mut sections: Vec<Element<'a, Self::Event, Renderer>> = vec![];
let mut pages: Vec<Element<Self::Event, Renderer>> = vec![]; let mut pages: Vec<Element<'a, Self::Event, Renderer>> = vec![];
for (section, section_pages) in &self.source { for (section, section_pages) in &self.source {
sections.push( sections.push(
button( button(
column(vec![ column(vec![
icon(&section.icon, 20).into(), icon(section.icon.clone(), 20).into(),
text(&section.title).size(14).into(), text(section.title.clone()).size(14).into(),
]) ])
.width(Length::Units(100)) .width(Length::Units(100))
.height(Length::Units(50)) .height(Length::Units(50))
@ -179,16 +167,16 @@ where
let nav_bar: Element<Self::Event, Renderer> = let nav_bar: Element<Self::Event, Renderer> =
container(if self.condensed && state.selected_page.is_some() { container(if self.condensed && state.selected_page.is_some() {
row![container(scrollable!(column(pages) row![container(scrollable(column(pages)
.spacing(10) .spacing(10)
.padding(10) .padding(10)
.max_width(200) .max_width(200)
.width(Length::Units(200)) .width(Length::Units(200))
.height(Length::Shrink))) .height(Length::Shrink)))
.height(Length::Fill) .height(Length::Fill)
.style(theme::Container::Custom(nav_bar_pages_style))] .style(theme::Container::Custom(nav_bar_pages_style))]
} else if !state.section_active || self.condensed && state.selected_page.is_none() { } else if !state.section_active || self.condensed && state.selected_page.is_none() {
row![scrollable!(column(sections) row![scrollable(column(sections)
.spacing(10) .spacing(10)
.padding(10) .padding(10)
.max_width(100) .max_width(100)
@ -196,13 +184,13 @@ where
.height(Length::Shrink))] .height(Length::Shrink))]
} else { } else {
row![ row![
scrollable!(column(sections) scrollable(column(sections)
.spacing(10) .spacing(10)
.padding(10) .padding(10)
.max_width(100) .max_width(100)
.align_items(Alignment::Center) .align_items(Alignment::Center)
.height(Length::Shrink)), .height(Length::Shrink)),
container(scrollable!(column(pages) container(scrollable(column(pages)
.spacing(10) .spacing(10)
.padding(10) .padding(10)
.max_width(200) .max_width(200)
@ -222,16 +210,8 @@ where
} }
} }
impl<'a, Message: 'a, Renderer> From<NavBar<'a, Message>> for Element<'a, Message, Renderer> impl<'a, Message: 'static> From<NavBar<'a, Message>>
where for Element<'a, Message, Renderer>
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,
{ {
fn from(nav_bar: NavBar<'a, Message>) -> Self { fn from(nav_bar: NavBar<'a, Message>) -> Self {
iced_lazy::component(nav_bar) iced_lazy::component(nav_bar)

View file

@ -1,8 +1,11 @@
#[macro_export] // Copyright 2022 System76 <info@system76.com>
macro_rules! scrollable { // SPDX-License-Identifier: MPL-2.0
($x:expr) => {
$crate::iced::widget::scrollable($x) use crate::{Element, Renderer};
.scrollbar_width(8) use iced::widget;
.scroller_width(8)
}; 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] // Copyright 2022 System76 <info@system76.com>
macro_rules! separator { // SPDX-License-Identifier: MPL-2.0
($size:expr) => {
$crate::iced::widget::horizontal_rule($size) use crate::iced::widget;
.style($crate::theme::Rule::Custom($crate::widget::separator_style)) 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}; use iced::{widget, Length};
pub fn toggler<'a, Message, Renderer>( pub fn toggler<'a, Message>(
label: impl Into<Option<String>>, label: impl Into<Option<String>>,
is_checked: bool, is_checked: bool,
f: impl Fn(bool) -> Message + 'a, f: impl Fn(bool) -> Message + 'a,
) -> widget::Toggler<'a, Message, Renderer> ) -> widget::Toggler<'a, Message, Renderer> {
where
Renderer: iced_native::text::Renderer,
Renderer::Theme: widget::toggler::StyleSheet,
{
widget::Toggler::new(is_checked, label, f) widget::Toggler::new(is_checked, label, f)
.size(24) .size(24)
.spacing(12) .spacing(12)