Merge branch 'cosmic-design-system' into sctk-cosmic-design-system
This commit is contained in:
commit
9796fa9f15
34 changed files with 850 additions and 1360 deletions
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright 2022 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use cosmic::{iced::Application, settings};
|
||||
|
||||
mod window;
|
||||
|
|
|
|||
|
|
@ -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
19
src/ext.rs
Normal 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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -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 {
|
||||
|
|
|
|||
17
src/lib.rs
17
src/lib.rs
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -0,0 +1,2 @@
|
|||
// Copyright 2022 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
|
@ -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).
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
9
src/utils.rs
Normal 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))
|
||||
}
|
||||
|
|
@ -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
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
|
|
@ -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()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
65
src/widget/list/column.rs
Normal 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
2
src/widget/list/item.rs
Normal file
|
|
@ -0,0 +1,2 @@
|
|||
// Copyright 2022 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
|
@ -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;
|
||||
|
|
@ -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,
|
||||
}
|
||||
}
|
||||
|
|
@ -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;
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
|
@ -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
61
src/widget/nav_button.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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};
|
||||
|
|
|
|||
|
|
@ -1,3 +1,6 @@
|
|||
// Copyright 2022 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
pub mod navbar;
|
||||
pub use navbar::*;
|
||||
|
||||
|
|
|
|||
|
|
@ -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(§ion.icon, 20).into(),
|
||||
text(§ion.title).size(14).into(),
|
||||
icon(section.icon.clone(), 20).into(),
|
||||
text(section.title.clone()).size(14).into(),
|
||||
])
|
||||
.width(Length::Units(100))
|
||||
.height(Length::Units(50))
|
||||
|
|
@ -179,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)
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
|
|
@ -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),
|
||||
}
|
||||
}
|
||||
26
src/widget/settings/item.rs
Normal file
26
src/widget/settings/item.rs
Normal 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)
|
||||
}
|
||||
|
||||
17
src/widget/settings/mod.rs
Normal file
17
src/widget/settings/mod.rs
Normal 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)
|
||||
}
|
||||
39
src/widget/settings/section.rs
Normal file
39
src/widget/settings/section.rs
Normal 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()
|
||||
}
|
||||
}
|
||||
|
|
@ -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)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue