Merge branch 'pop-os:master' into new-match-algo

This commit is contained in:
wiiznokes 2024-05-31 03:04:47 +02:00 • committed by GitHub
commit 01a3789a19
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
58 changed files with 1360 additions and 567 deletions

View file

@ -31,7 +31,7 @@ desktop = ["process", "dep:freedesktop-desktop-entry", "dep:mime", "dep:shlex"]
# Enables keycode serialization # Enables keycode serialization
serde-keycode = ["iced_core/serde"] serde-keycode = ["iced_core/serde"]
# Prevents multiple separate process instances. # Prevents multiple separate process instances.
single-instance = ["dep:zbus", "serde", "ron"] single-instance = ["dep:zbus", "ron"]
# smol async runtime # smol async runtime
smol = ["iced/smol", "zbus?/async-io"] smol = ["iced/smol", "zbus?/async-io"]
tokio = [ tokio = [
@ -80,7 +80,7 @@ mime = { version = "0.3.17", optional = true }
nix = { version = "0.27", features = ["process"], optional = true } nix = { version = "0.27", features = ["process"], optional = true }
palette = "0.7.3" palette = "0.7.3"
rfd = { version = "0.14.0", optional = true } rfd = { version = "0.14.0", optional = true }
serde = { version = "1.0.180", optional = true } serde = { version = "1.0.180", features = ["derive"]}
slotmap = "1.0.6" slotmap = "1.0.6"
textdistance = "1.0.2" textdistance = "1.0.2"
thiserror = "1.0.44" thiserror = "1.0.44"

View file

@ -1,3 +1,5 @@
//! Integrations for cosmic-config — the cosmic configuration system.
use notify::{ use notify::{
event::{EventKind, ModifyKind}, event::{EventKind, ModifyKind},
Watcher, Watcher,

View file

@ -6,7 +6,7 @@ use std::{
num::NonZeroUsize, num::NonZeroUsize,
}; };
use super::{to_hex, OutputError}; use super::{to_rgba, OutputError};
impl Theme { impl Theme {
#[must_use] #[must_use]
@ -24,95 +24,169 @@ impl Theme {
.. ..
} = self; } = self;
let window_bg = to_hex(background.base); let window_bg = to_rgba(background.base);
let window_fg = to_hex(background.on); let window_fg = to_rgba(background.on);
let view_bg = to_hex(primary.base); let view_bg = to_rgba(primary.base);
let view_fg = to_hex(primary.on); let view_fg = to_rgba(primary.on);
let headerbar_bg = to_hex(background.base); let headerbar_bg = to_rgba(background.base);
let headerbar_fg = to_hex(background.on); let headerbar_fg = to_rgba(background.on);
let headerbar_border_color = to_hex(background.divider); let headerbar_border_color = to_rgba(background.divider);
let sidebar_bg = to_hex(primary.base); let sidebar_bg = to_rgba(primary.base);
let sidebar_fg = to_hex(primary.on); let sidebar_fg = to_rgba(primary.on);
let sidebar_shade = to_hex(if self.is_dark { let sidebar_shade = to_rgba(if self.is_dark {
Rgba::new(0.0, 0.0, 0.0, 0.08) Rgba::new(0.0, 0.0, 0.0, 0.08)
} else { } else {
Rgba::new(0.0, 0.0, 0.0, 0.32) Rgba::new(0.0, 0.0, 0.0, 0.32)
}); });
let backdrop_overlay = Srgba::new(1.0, 1.0, 1.0, if self.is_dark { 0.08 } else { 0.32 }); let backdrop_overlay = Srgba::new(1.0, 1.0, 1.0, if self.is_dark { 0.08 } else { 0.32 });
let sidebar_backdrop = to_hex(over(backdrop_overlay, primary.base)); let sidebar_backdrop = to_rgba(over(backdrop_overlay, primary.base));
let secondary_sidebar_bg = to_hex(secondary.base); let secondary_sidebar_bg = to_rgba(secondary.base);
let secondary_sidebar_fg = to_hex(secondary.on); let secondary_sidebar_fg = to_rgba(secondary.on);
let secondary_sidebar_shade = to_hex(if self.is_dark { let secondary_sidebar_shade = to_rgba(if self.is_dark {
Rgba::new(0.0, 0.0, 0.0, 0.08) Rgba::new(0.0, 0.0, 0.0, 0.08)
} else { } else {
Rgba::new(0.0, 0.0, 0.0, 0.32) Rgba::new(0.0, 0.0, 0.0, 0.32)
}); });
let secondary_sidebar_backdrop = to_hex(over(backdrop_overlay, secondary.base)); let secondary_sidebar_backdrop = to_rgba(over(backdrop_overlay, secondary.base));
let headerbar_backdrop = to_hex(background.base); let headerbar_backdrop = to_rgba(background.base);
let card_bg = to_hex(background.component.base); let card_bg = to_rgba(background.component.base);
let card_fg = to_hex(background.component.on); let card_fg = to_rgba(background.component.on);
let thumbnail_bg = to_hex(background.component.base); let thumbnail_bg = to_rgba(background.component.base);
let thumbnail_fg = to_hex(background.component.on); let thumbnail_fg = to_rgba(background.component.on);
let dialog_bg = to_hex(primary.base); let dialog_bg = to_rgba(primary.base);
let dialog_fg = to_hex(primary.on); let dialog_fg = to_rgba(primary.on);
let popover_bg = to_hex(background.component.base); let popover_bg = to_rgba(background.component.base);
let popover_fg = to_hex(background.component.on); let popover_fg = to_rgba(background.component.on);
let shade = to_hex(if self.is_dark { let shade = to_rgba(if self.is_dark {
Rgba::new(0.0, 0.0, 0.0, 0.32) Rgba::new(0.0, 0.0, 0.0, 0.32)
} else { } else {
Rgba::new(0.0, 0.0, 0.0, 0.08) Rgba::new(0.0, 0.0, 0.0, 0.08)
}); });
let window_control_hover_bg = to_rgba(if self.is_dark {
Rgba::new(255.0, 255.0, 255.0, 0.12)
} else {
Rgba::new(0.0, 0.0, 0.0, 0.12)
});
let corner_radius = self.corner_radii.radius_s[0];
let mut inverted_bg_divider = background.base; let mut inverted_bg_divider = background.base;
inverted_bg_divider.alpha = 0.5; inverted_bg_divider.alpha = 0.5;
let scrollbar_outline = to_hex(inverted_bg_divider); let scrollbar_outline = to_rgba(inverted_bg_divider);
let mut css = format! {r#" let mut css = format! {r#"
@define-color window_bg_color #{window_bg}; @define-color window_bg_color {window_bg};
@define-color window_fg_color #{window_fg}; @define-color window_fg_color {window_fg};
@define-color view_bg_color #{view_bg}; @define-color view_bg_color {view_bg};
@define-color view_fg_color #{view_fg}; @define-color view_fg_color {view_fg};
@define-color headerbar_bg_color #{headerbar_bg}; @define-color headerbar_bg_color {headerbar_bg};
@define-color headerbar_fg_color #{headerbar_fg}; @define-color headerbar_fg_color {headerbar_fg};
@define-color headerbar_border_color_color #{headerbar_border_color}; @define-color headerbar_border_color_color {headerbar_border_color};
@define-color headerbar_backdrop_color #{headerbar_backdrop}; @define-color headerbar_backdrop_color {headerbar_backdrop};
@define-color sidebar_bg_color #{sidebar_bg}; @define-color sidebar_bg_color {sidebar_bg};
@define-color sidebar_fg_color #{sidebar_fg}; @define-color sidebar_fg_color {sidebar_fg};
@define-color sidebar_shade_color #{sidebar_shade}; @define-color sidebar_shade_color {sidebar_shade};
@define-color sidebar_backdrop_color #{sidebar_backdrop}; @define-color sidebar_backdrop_color {sidebar_backdrop};
@define-color secondary_sidebar_bg_color #{secondary_sidebar_bg}; @define-color secondary_sidebar_bg_color {secondary_sidebar_bg};
@define-color secondary_sidebar_fg_color #{secondary_sidebar_fg}; @define-color secondary_sidebar_fg_color {secondary_sidebar_fg};
@define-color secondary_sidebar_shade_color #{secondary_sidebar_shade}; @define-color secondary_sidebar_shade_color {secondary_sidebar_shade};
@define-color secondary_sidebar_backdrop_color #{secondary_sidebar_backdrop}; @define-color secondary_sidebar_backdrop_color {secondary_sidebar_backdrop};
@define-color card_bg_color #{card_bg}; @define-color card_bg_color {card_bg};
@define-color card_fg_color #{card_fg}; @define-color card_fg_color {card_fg};
@define-color thumbnail_bg_color #{thumbnail_bg}; @define-color thumbnail_bg_color {thumbnail_bg};
@define-color thumbnail_fg_color #{thumbnail_fg}; @define-color thumbnail_fg_color {thumbnail_fg};
@define-color dialog_bg_color #{dialog_bg}; @define-color dialog_bg_color {dialog_bg};
@define-color dialog_fg_color #{dialog_fg}; @define-color dialog_fg_color {dialog_fg};
@define-color popover_bg_color #{popover_bg}; @define-color popover_bg_color {popover_bg};
@define-color popover_fg_color #{popover_fg}; @define-color popover_fg_color {popover_fg};
@define-color shade_color #{shade}; @define-color shade_color {shade};
@define-color scrollbar_outline_color #{scrollbar_outline}; @define-color scrollbar_outline_color {scrollbar_outline};
.close, .maximize, .minimize {{
background: transparent;
}}
.close:not(:hover) > image,
.maximize:not(:hover) > image,
.minimize:not(:hover) > image {{
background: transparent;
}}
.close > image,
.maximize > image,
.minimize > image {{
transition: 0;
color: @accent_bg_color;
border-radius: 100%;
}}
.close:backdrop > image,
.maximize:backdrop > image,
.minimize:backdrop > image {{
filter: grayscale(1) brightness(1.7);
}}
.close:hover > image,
.maximize:hover > image,
.minimize:hover > image {{
background: {window_control_hover_bg};
}}
.image-button {{
transition: 0;
}}
.image-button > image,
.image-button > box,
.popup > image,
.toggle > image,
.toggle > arrow,
.toggle > box > contents > image,
button > image,
button > widget > box > image,
stack > box > image
{{
color: @accent_bg_color;
}}
.image-button:insensitive > image,
.image-button:insensitive > box,
.popup:insensitive > image,
.toggle:insensitive > image,
.toggle:insensitive > box > contents > image,
button:insensitive > image,
button:insensitive > widget > box > image,
stack:insensitive > box > image {{
opacity: 0.5;
}}
.image-button:backdrop > image,
.image-button:backdrop > box,
.popup:backdrop > image,
.toggle:backdrop > image,
.toggle:backdrop > arrow,
button:backdrop > image,
button:backdrop > widget > box > image,
stack:backdrop > box > image,
.toggle:backdrop > box > contents > image {{
filter: grayscale(1) brightness(1.7);
}}
window {{ background: @window_bg_color }}
window:not(.maximized) {{
border-radius: {corner_radius:.0}px;
}}
window, statuspage.view {{ background: @window_bg_color }}
"#}; "#};
css.push_str(&component_gtk4_css("accent", accent)); css.push_str(&component_gtk4_css("accent", accent));
@ -130,10 +204,10 @@ impl Theme {
css.push_str(&color_css("purple", palette.ext_purple)); css.push_str(&color_css("purple", palette.ext_purple));
let neutral_steps = steps(palette.neutral_5, NonZeroUsize::new(10).unwrap()); let neutral_steps = steps(palette.neutral_5, NonZeroUsize::new(10).unwrap());
for (i, c) in neutral_steps[..5].iter().enumerate() { for (i, c) in neutral_steps[..5].iter().enumerate() {
css.push_str(&format!("@define-color light_{i} #{};\n", to_hex(*c))); css.push_str(&format!("@define-color light_{i} {};\n", to_rgba(*c)));
} }
for (i, c) in neutral_steps[5..].iter().enumerate() { for (i, c) in neutral_steps[5..].iter().enumerate() {
css.push_str(&format!("@define-color dark_{i} #{};\n", to_hex(*c))); css.push_str(&format!("@define-color dark_{i} {};\n", to_rgba(*c)));
} }
css css
} }
@ -232,13 +306,13 @@ impl Theme {
fn component_gtk4_css(prefix: &str, c: &Component) -> String { fn component_gtk4_css(prefix: &str, c: &Component) -> String {
format!( format!(
r#" r#"
@define-color {prefix}_color #{}; @define-color {prefix}_color {};
@define-color {prefix}_bg_color #{}; @define-color {prefix}_bg_color {};
@define-color {prefix}_fg_color #{}; @define-color {prefix}_fg_color {};
"#, "#,
to_hex(c.base), to_rgba(c.base),
to_hex(c.base), to_rgba(c.base),
to_hex(c.on), to_rgba(c.on),
) )
} }
@ -248,17 +322,17 @@ fn color_css(prefix: &str, c_3: Srgba) -> String {
let c_1: Srgba = oklch.lighten(0.2).into_color(); let c_1: Srgba = oklch.lighten(0.2).into_color();
let c_4: Srgba = oklch.darken(0.1).into_color(); let c_4: Srgba = oklch.darken(0.1).into_color();
let c_5: Srgba = oklch.darken(0.2).into_color(); let c_5: Srgba = oklch.darken(0.2).into_color();
let c_1 = to_hex(c_1); let c_1 = to_rgba(c_1);
let c_2 = to_hex(c_2); let c_2 = to_rgba(c_2);
let c_3 = to_hex(c_3); let c_3 = to_rgba(c_3);
let c_4 = to_hex(c_4); let c_4 = to_rgba(c_4);
let c_5 = to_hex(c_5); let c_5 = to_rgba(c_5);
format! {r#" format! {r#"
@define-color {prefix}_1 #{c_1}; @define-color {prefix}_1 {c_1};
@define-color {prefix}_2 #{c_2}; @define-color {prefix}_2 {c_2};
@define-color {prefix}_3 #{c_3}; @define-color {prefix}_3 {c_3};
@define-color {prefix}_4 #{c_4}; @define-color {prefix}_4 {c_4};
@define-color {prefix}_5 #{c_5}; @define-color {prefix}_5 {c_5};
"#} "#}
} }

View file

@ -42,5 +42,16 @@ impl Theme {
pub fn to_hex(c: Srgba) -> String { pub fn to_hex(c: Srgba) -> String {
let c_u8: Rgba<palette::encoding::Srgb, u8> = c.into_format(); let c_u8: Rgba<palette::encoding::Srgb, u8> = c.into_format();
format!("{:02x}{:02x}{:02x}", c_u8.red, c_u8.green, c_u8.blue) format!(
"{:02x}{:02x}{:02x}{:02x}",
c_u8.red, c_u8.green, c_u8.blue, c_u8.alpha
)
}
pub fn to_rgba(c: Srgba) -> String {
let c_u8: Rgba<palette::encoding::Srgb, u8> = c.into_format();
format!(
"rgba({}, {}, {}, {:1.2})",
c_u8.red, c_u8.green, c_u8.blue, c.alpha
)
} }

@ -1 +0,0 @@
Subproject commit 493e17a0105c7523fb7ff5fd7221ec586ac9010f

View file

@ -14,9 +14,7 @@ use cosmic::iced_core::{Length, Size};
use cosmic::widget::menu::action::MenuAction; use cosmic::widget::menu::action::MenuAction;
use cosmic::widget::menu::key_bind::KeyBind; use cosmic::widget::menu::key_bind::KeyBind;
use cosmic::widget::menu::key_bind::Modifier; use cosmic::widget::menu::key_bind::Modifier;
use cosmic::widget::menu::menu_tree::{menu_items, menu_root, MenuItem}; use cosmic::widget::menu::{self, ItemHeight, ItemWidth};
use cosmic::widget::menu::{ItemHeight, ItemWidth, MenuBar, MenuTree};
use cosmic::widget::segmented_button::Entity;
use cosmic::{executor, Element}; use cosmic::{executor, Element};
/// Runs application with these settings /// Runs application with these settings
@ -67,7 +65,7 @@ pub enum Action {
impl MenuAction for Action { impl MenuAction for Action {
type Message = Message; type Message = Message;
fn message(&self, _entity_opt: Option<Entity>) -> Self::Message { fn message(&self) -> Self::Message {
match self { match self {
Action::WindowClose => Message::WindowClose, Action::WindowClose => Message::WindowClose,
Action::ToggleHideContent => Message::ToggleHideContent, Action::ToggleHideContent => Message::ToggleHideContent,
@ -156,23 +154,23 @@ impl cosmic::Application for App {
} }
pub fn menu_bar<'a>(config: &Config, key_binds: &HashMap<KeyBind, Action>) -> Element<'a, Message> { pub fn menu_bar<'a>(config: &Config, key_binds: &HashMap<KeyBind, Action>) -> Element<'a, Message> {
MenuBar::new(vec![MenuTree::with_children( menu::bar(vec![menu::Tree::with_children(
menu_root("File"), menu::root("File"),
menu_items( menu::items(
key_binds, key_binds,
vec![ vec![
MenuItem::Button("New window", Action::WindowNew), menu::Item::Button("New window", Action::WindowNew),
MenuItem::Divider, menu::Item::Divider,
MenuItem::Folder( menu::Item::Folder(
"View", "View",
vec![MenuItem::CheckBox( vec![menu::Item::CheckBox(
"Hide content", "Hide content",
config.hide_content, config.hide_content,
Action::ToggleHideContent, Action::ToggleHideContent,
)], )],
), ),
MenuItem::Divider, menu::Item::Divider,
MenuItem::Button("Quit", Action::WindowClose), menu::Item::Button("Quit", Action::WindowClose),
], ],
), ),
)]) )])

View file

@ -139,8 +139,7 @@ impl cosmic::Application for App {
// Send this back to the application. // Send this back to the application.
Message::FileRead(url, contents) Message::FileRead(url, contents)
}) });
.map(cosmic::app::message::app);
} }
// Creates a new open dialog. // Creates a new open dialog.
@ -167,8 +166,7 @@ impl cosmic::Application for App {
Err(why) => Message::OpenError(Arc::new(why)), Err(why) => Message::OpenError(Arc::new(why)),
} }
}) });
.map(cosmic::app::Message::App);
} }
// Displays an error in the application's warning bar. // Displays an error in the application's warning bar.

2
iced

@ -1 +1 @@
Subproject commit 7c58b8eda9885083e019a5ea14edf4652740e154 Subproject commit 2dcae59258c9cee6862c7fb2193066a1cb1e30c2

View file

@ -1,4 +1,4 @@
examples := 'applet application config cosmic cosmic-design-demo multi-window open-dialog' examples := 'applet application calendar config context-menu cosmic image-button menu multi-window nav-context open-dialog'
clippy_args := '-W clippy::all -W clippy::pedantic' clippy_args := '-W clippy::all -W clippy::pedantic'
# Check for errors and linter warnings # Check for errors and linter warnings

View file

@ -73,9 +73,6 @@ pub struct Core {
/// Configured theme mode /// Configured theme mode
pub(super) system_theme_mode: ThemeMode, pub(super) system_theme_mode: ThemeMode,
/// Libcosmic toolkit configuration.
pub(super) toolkit_config: CosmicTk,
pub(super) portal_is_dark: Option<bool>, pub(super) portal_is_dark: Option<bool>,
pub(super) portal_accent: Option<Srgba>, pub(super) portal_accent: Option<Srgba>,
@ -107,7 +104,7 @@ impl Default for Core {
active: true, active: true,
context_id: crate::widget::nav_bar::Id::null(), context_id: crate::widget::nav_bar::Id::null(),
toggled: true, toggled: true,
toggled_condensed: true, toggled_condensed: false,
}, },
scale_factor: 1.0, scale_factor: 1.0,
title: HashMap::new(), title: HashMap::new(),
@ -123,16 +120,6 @@ impl Default for Core {
}) })
}) })
.unwrap_or_default(), .unwrap_or_default(),
toolkit_config: CosmicTk::config()
.map(|c| {
CosmicTk::get_entry(&c).unwrap_or_else(|(errors, mode)| {
for why in errors {
tracing::error!(?why, "CosmicTk config entry error");
}
mode
})
})
.unwrap_or_default(),
window: Window { window: Window {
context_title: String::new(), context_title: String::new(),
header_title: String::new(), header_title: String::new(),

View file

@ -156,6 +156,7 @@ where
} }
} }
#[allow(clippy::too_many_lines)]
fn subscription(&self) -> Subscription<Self::Message> { fn subscription(&self) -> Subscription<Self::Message> {
let window_events = listen_with(|event, _| { let window_events = listen_with(|event, _| {
match event { match event {
@ -197,7 +198,7 @@ where
self.app.subscription().map(super::Message::App), self.app.subscription().map(super::Message::App),
self.app self.app
.core() .core()
.watch_config::<crate::config::CosmicTk>(crate::config::toolkit::ID) .watch_config::<crate::config::CosmicTk>(crate::config::ID)
.map(|update| { .map(|update| {
for why in update.errors { for why in update.errors {
tracing::error!(?why, "cosmic toolkit config update error"); tracing::error!(?why, "cosmic toolkit config update error");
@ -408,11 +409,9 @@ impl<T: Application> Cosmic<T> {
} }
Message::AppThemeChange(mut theme) => { Message::AppThemeChange(mut theme) => {
// Apply last-known system theme if the system theme is preferred.
if let ThemeType::System { theme: _, .. } = theme.theme_type { if let ThemeType::System { theme: _, .. } = theme.theme_type {
self.app.core_mut().theme_sub_counter += 1; self.app.core_mut().theme_sub_counter += 1;
theme = self.app.core().system_theme.clone();
let portal_accent = self.app.core().portal_accent; let portal_accent = self.app.core().portal_accent;
if let Some(a) = portal_accent { if let Some(a) = portal_accent {
let t_inner = theme.cosmic(); let t_inner = theme.cosmic();
@ -429,6 +428,11 @@ impl<T: Application> Cosmic<T> {
} }
Message::SystemThemeChange(keys, theme) => { Message::SystemThemeChange(keys, theme) => {
let cur_is_dark = THEME.with(|t| t.borrow().theme_type.is_dark());
// Ignore updates if the current theme mode does not match.
if cur_is_dark != theme.cosmic().is_dark {
return iced::Command::none();
}
let cmd = self.app.system_theme_update(&keys, theme.cosmic()); let cmd = self.app.system_theme_update(&keys, theme.cosmic());
// Record the last-known system theme in event that the current theme is custom. // Record the last-known system theme in event that the current theme is custom.
self.app.core_mut().system_theme = theme.clone(); self.app.core_mut().system_theme = theme.clone();
@ -472,6 +476,9 @@ impl<T: Application> Cosmic<T> {
}; };
} }
Message::SystemThemeModeChange(keys, mode) => { Message::SystemThemeModeChange(keys, mode) => {
if !keys.contains(&"is_dark") {
return iced::Command::none();
}
if THEME.with(|t| match t.borrow().theme_type { if THEME.with(|t| match t.borrow().theme_type {
ThemeType::System { ThemeType::System {
theme: _, theme: _,
@ -619,7 +626,7 @@ impl<T: Application> Cosmic<T> {
crate::icon_theme::set_default(config.icon_theme.clone()); crate::icon_theme::set_default(config.icon_theme.clone());
} }
self.app.core_mut().toolkit_config = config; crate::config::COSMIC_TK.with(|tk| *tk.borrow_mut() = config);
} }
Message::Focus(f) => { Message::Focus(f) => {

View file

@ -37,6 +37,12 @@ pub mod message {
pub const fn none<M>() -> Message<M> { pub const fn none<M>() -> Message<M> {
Message::None Message::None
} }
impl<M> From<M> for Message<M> {
fn from(value: M) -> Self {
Self::App(value)
}
}
} }
pub use self::command::Command; pub use self::command::Command;
@ -45,7 +51,7 @@ pub use self::settings::Settings;
use crate::config::CosmicTk; use crate::config::CosmicTk;
use crate::prelude::*; use crate::prelude::*;
use crate::theme::THEME; use crate::theme::THEME;
use crate::widget::{context_drawer, menu, nav_bar, popover}; use crate::widget::{context_drawer, id_container, menu, nav_bar, popover};
use apply::Apply; use apply::Apply;
use iced::Subscription; use iced::Subscription;
#[cfg(all(feature = "winit", feature = "multi-window"))] #[cfg(all(feature = "winit", feature = "multi-window"))]
@ -78,7 +84,7 @@ pub(crate) fn iced_settings<App: Application>(
if let Some(icon_theme) = settings.default_icon_theme { if let Some(icon_theme) = settings.default_icon_theme {
crate::icon_theme::set_default(icon_theme); crate::icon_theme::set_default(icon_theme);
} else { } else {
crate::icon_theme::set_default(core.toolkit_config.icon_theme.clone()); crate::icon_theme::set_default(crate::config::icon_theme());
} }
THEME.with(move |t| { THEME.with(move |t| {
@ -92,7 +98,11 @@ pub(crate) fn iced_settings<App: Application>(
iced.default_font = settings.default_font; iced.default_font = settings.default_font;
iced.default_text_size = iced::Pixels(settings.default_text_size); iced.default_text_size = iced::Pixels(settings.default_text_size);
iced.exit_on_close_request = settings.exit_on_close; iced.exit_on_close_request = settings.exit_on_close;
// iced.window.exit_on_close_request = settings.exit_on_close; #[cfg(not(feature = "wayland"))]
{
let exit_on_close = settings.exit_on_close;
iced.window.exit_on_close_request = exit_on_close;
}
iced.id = Some(App::APP_ID.to_owned()); iced.id = Some(App::APP_ID.to_owned());
#[cfg(all(not(feature = "wayland"), target_os = "linux"))] #[cfg(all(not(feature = "wayland"), target_os = "linux"))]
{ {
@ -149,40 +159,11 @@ pub(crate) fn iced_settings<App: Application>(
/// ///
/// Returns error on application failure. /// Returns error on application failure.
pub fn run<App: Application>(settings: Settings, flags: App::Flags) -> iced::Result { pub fn run<App: Application>(settings: Settings, flags: App::Flags) -> iced::Result {
#[cfg(feature = "wgpu")]
wgpu_power_pref();
let settings = iced_settings::<App>(settings, flags); let settings = iced_settings::<App>(settings, flags);
cosmic::Cosmic::<App>::run(settings) cosmic::Cosmic::<App>::run(settings)
} }
/// Default to rendering the application with the low power GPU preference.
#[cfg(feature = "wgpu")]
fn wgpu_power_pref() {
fn is_desktop() -> bool {
let chassis = std::fs::read_to_string("/sys/class/dmi/id/chassis_type").unwrap_or_default();
chassis.trim() == "3"
}
// Ignore if the system is a desktop.
if is_desktop() {
return;
}
// Ignore if requested to run on NVIDIA GPU
if std::env::var("__NV_PRIME_RENDER_OFFLOAD").ok().as_deref() == Some("1") {
return;
}
#[allow(clippy::items_after_statements)]
const VAR: &str = "WGPU_POWER_PREF";
if std::env::var(VAR).is_err() {
std::env::set_var(VAR, "low");
}
}
#[cfg(feature = "single-instance")] #[cfg(feature = "single-instance")]
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct DbusActivationMessage<Action = String, Args = Vec<String>> { pub struct DbusActivationMessage<Action = String, Args = Vec<String>> {
@ -429,7 +410,7 @@ where
fn core_mut(&mut self) -> &mut Core; fn core_mut(&mut self) -> &mut Core;
/// Creates the application, and optionally emits command on initialize. /// Creates the application, and optionally emits command on initialize.
fn init(core: Core, flags: Self::Flags) -> (Self, iced::Command<Message<Self::Message>>); fn init(core: Core, flags: Self::Flags) -> (Self, Command<Self::Message>);
/// Displays a context drawer on the side of the application window when `Some`. /// Displays a context drawer on the side of the application window when `Some`.
fn context_drawer(&self) -> Option<Element<Self::Message>> { fn context_drawer(&self) -> Option<Element<Self::Message>> {
@ -506,28 +487,28 @@ where
} }
// Called when context drawer is toggled // Called when context drawer is toggled
fn on_context_drawer(&mut self) -> iced::Command<Message<Self::Message>> { fn on_context_drawer(&mut self) -> Command<Self::Message> {
iced::Command::none() Command::none()
} }
/// Called when the escape key is pressed. /// Called when the escape key is pressed.
fn on_escape(&mut self) -> iced::Command<Message<Self::Message>> { fn on_escape(&mut self) -> Command<Self::Message> {
iced::Command::none() Command::none()
} }
/// Called when a navigation item is selected. /// Called when a navigation item is selected.
fn on_nav_select(&mut self, id: nav_bar::Id) -> iced::Command<Message<Self::Message>> { fn on_nav_select(&mut self, id: nav_bar::Id) -> Command<Self::Message> {
iced::Command::none() Command::none()
} }
/// Called when a context menu is requested for a navigation item. /// Called when a context menu is requested for a navigation item.
fn on_nav_context(&mut self, id: nav_bar::Id) -> iced::Command<Message<Self::Message>> { fn on_nav_context(&mut self, id: nav_bar::Id) -> Command<Self::Message> {
iced::Command::none() Command::none()
} }
/// Called when the search function is requested. /// Called when the search function is requested.
fn on_search(&mut self) -> iced::Command<Message<Self::Message>> { fn on_search(&mut self) -> Command<Self::Message> {
iced::Command::none() Command::none()
} }
/// Called when a window is resized. /// Called when a window is resized.
@ -539,8 +520,8 @@ where
} }
/// Respond to an application-specific message. /// Respond to an application-specific message.
fn update(&mut self, message: Self::Message) -> iced::Command<Message<Self::Message>> { fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
iced::Command::none() Command::none()
} }
/// Respond to a system theme change /// Respond to a system theme change
@ -548,8 +529,8 @@ where
&mut self, &mut self,
keys: &[&'static str], keys: &[&'static str],
new_theme: &cosmic_theme::Theme, new_theme: &cosmic_theme::Theme,
) -> iced::Command<Message<Self::Message>> { ) -> Command<Self::Message> {
iced::Command::none() Command::none()
} }
/// Respond to a system theme mode change /// Respond to a system theme mode change
@ -557,8 +538,8 @@ where
&mut self, &mut self,
keys: &[&'static str], keys: &[&'static str],
new_theme: &cosmic_theme::ThemeMode, new_theme: &cosmic_theme::ThemeMode,
) -> iced::Command<Message<Self::Message>> { ) -> Command<Self::Message> {
iced::Command::none() Command::none()
} }
/// Constructs the view for the main window. /// Constructs the view for the main window.
@ -576,24 +557,21 @@ where
/// Handles dbus activation messages /// Handles dbus activation messages
#[cfg(feature = "single-instance")] #[cfg(feature = "single-instance")]
fn dbus_activation( fn dbus_activation(&mut self, msg: DbusActivationMessage) -> Command<Self::Message> {
&mut self, Command::none()
msg: DbusActivationMessage,
) -> iced::Command<Message<Self::Message>> {
iced::Command::none()
} }
} }
/// Methods automatically derived for all types implementing [`Application`]. /// Methods automatically derived for all types implementing [`Application`].
pub trait ApplicationExt: Application { pub trait ApplicationExt: Application {
/// Initiates a window drag. /// Initiates a window drag.
fn drag(&mut self) -> iced::Command<Message<Self::Message>>; fn drag(&mut self) -> Command<Self::Message>;
/// Maximizes the window. /// Maximizes the window.
fn maximize(&mut self) -> iced::Command<Message<Self::Message>>; fn maximize(&mut self) -> Command<Self::Message>;
/// Minimizes the window. /// Minimizes the window.
fn minimize(&mut self) -> iced::Command<Message<Self::Message>>; fn minimize(&mut self) -> Command<Self::Message>;
/// Get the title of the main window. /// Get the title of the main window.
#[cfg(not(any(feature = "multi-window", feature = "wayland")))] #[cfg(not(any(feature = "multi-window", feature = "wayland")))]
@ -615,30 +593,26 @@ pub trait ApplicationExt: Application {
#[cfg(not(any(feature = "multi-window", feature = "wayland")))] #[cfg(not(any(feature = "multi-window", feature = "wayland")))]
/// Set the title of the main window. /// Set the title of the main window.
fn set_window_title(&mut self, title: String) -> iced::Command<Message<Self::Message>>; fn set_window_title(&mut self, title: String) -> Command<Self::Message>;
#[cfg(any(feature = "multi-window", feature = "wayland"))] #[cfg(any(feature = "multi-window", feature = "wayland"))]
/// Set the title of a window. /// Set the title of a window.
fn set_window_title( fn set_window_title(&mut self, title: String, id: window::Id) -> Command<Self::Message>;
&mut self,
title: String,
id: window::Id,
) -> iced::Command<Message<Self::Message>>;
/// View template for the main window. /// View template for the main window.
fn view_main(&self) -> Element<Message<Self::Message>>; fn view_main(&self) -> Element<Message<Self::Message>>;
} }
impl<App: Application> ApplicationExt for App { impl<App: Application> ApplicationExt for App {
fn drag(&mut self) -> iced::Command<Message<Self::Message>> { fn drag(&mut self) -> Command<Self::Message> {
command::drag(Some(self.main_window_id())) command::drag(Some(self.main_window_id()))
} }
fn maximize(&mut self) -> iced::Command<Message<Self::Message>> { fn maximize(&mut self) -> Command<Self::Message> {
command::maximize(Some(self.main_window_id()), true) command::maximize(Some(self.main_window_id()), true)
} }
fn minimize(&mut self) -> iced::Command<Message<Self::Message>> { fn minimize(&mut self) -> Command<Self::Message> {
command::minimize(Some(self.main_window_id())) command::minimize(Some(self.main_window_id()))
} }
@ -656,23 +630,20 @@ impl<App: Application> ApplicationExt for App {
} }
#[cfg(any(feature = "multi-window", feature = "wayland"))] #[cfg(any(feature = "multi-window", feature = "wayland"))]
fn set_window_title( fn set_window_title(&mut self, title: String, id: window::Id) -> Command<Self::Message> {
&mut self,
title: String,
id: window::Id,
) -> iced::Command<Message<Self::Message>> {
self.core_mut().title.insert(id, title.clone()); self.core_mut().title.insert(id, title.clone());
command::set_title(Some(id), title) command::set_title(Some(id), title)
} }
#[cfg(not(any(feature = "multi-window", feature = "wayland")))] #[cfg(not(any(feature = "multi-window", feature = "wayland")))]
fn set_window_title(&mut self, title: String) -> iced::Command<Message<Self::Message>> { fn set_window_title(&mut self, title: String) -> Command<Self::Message> {
let id = self.main_window_id(); let id = self.main_window_id();
self.core_mut().title.insert(id, title.clone()); self.core_mut().title.insert(id, title.clone());
iced::Command::none() Command::none()
} }
#[allow(clippy::too_many_lines)]
/// Creates the view for the main window. /// Creates the view for the main window.
fn view_main(&self) -> Element<Message<Self::Message>> { fn view_main(&self) -> Element<Message<Self::Message>> {
let core = self.core(); let core = self.core();
@ -685,8 +656,11 @@ impl<App: Application> ApplicationExt for App {
let mut widgets = Vec::with_capacity(2); let mut widgets = Vec::with_capacity(2);
// Insert nav bar onto the left side of the window. // Insert nav bar onto the left side of the window.
if let Some(nav) = self.nav_bar() { if let Some(nav) = self
widgets.push(nav); .nav_bar()
.map(|nav| id_container(nav, iced_core::id::Id::new("COSMIC_nav_bar")))
{
widgets.push(nav.into());
} }
if self.nav_model().is_none() || core.show_content() { if self.nav_model().is_none() || core.show_content() {
@ -699,7 +673,12 @@ impl<App: Application> ApplicationExt for App {
main_content, main_content,
context.map(Message::App), context.map(Message::App),
) )
.into() .apply(|drawer| {
Element::from(id_container(
drawer,
iced_core::id::Id::new("COSMIC_context_drawer"),
))
})
} else { } else {
main_content main_content
}); });
@ -715,6 +694,7 @@ impl<App: Application> ApplicationExt for App {
.width(iced::Length::Fill) .width(iced::Length::Fill)
.height(iced::Length::Fill) .height(iced::Length::Fill)
.style(crate::theme::Container::WindowBackground) .style(crate::theme::Container::WindowBackground)
.apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container")))
.into() .into()
} else { } else {
content_row.into() content_row.into()
@ -744,11 +724,11 @@ impl<App: Application> ApplicationExt for App {
header = header.start(toggle); header = header.start(toggle);
} }
if core.window.show_maximize && core.toolkit_config.show_maximize { if core.window.show_maximize && crate::config::show_maximize() {
header = header.on_maximize(Message::Cosmic(cosmic::Message::Maximize)); header = header.on_maximize(Message::Cosmic(cosmic::Message::Maximize));
} }
if core.window.show_minimize && core.toolkit_config.show_minimize { if core.window.show_minimize && crate::config::show_minimize() {
header = header.on_minimize(Message::Cosmic(cosmic::Message::Minimize)); header = header.on_minimize(Message::Cosmic(cosmic::Message::Minimize));
} }
@ -764,7 +744,7 @@ impl<App: Application> ApplicationExt for App {
header = header.end(element.map(Message::App)); header = header.end(element.map(Message::App));
} }
header header.apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_header")))
}) })
} else { } else {
None None
@ -775,7 +755,10 @@ impl<App: Application> ApplicationExt for App {
// Show any current dialog on top and centered over the view content // Show any current dialog on top and centered over the view content
// We have to use a popover even without a dialog to keep the tree from changing // We have to use a popover even without a dialog to keep the tree from changing
let mut popover = popover(view_column).modal(true); let mut popover = popover(view_column).modal(true);
if let Some(dialog) = self.dialog() { if let Some(dialog) = self
.dialog()
.map(|w| Element::from(id_container(w, iced_core::id::Id::new("COSMIC_dialog"))))
{
popover = popover.popup(dialog.map(Message::App)); popover = popover.popup(dialog.map(Message::App));
} }

View file

@ -372,7 +372,7 @@ pub fn style() -> <crate::Theme as iced_style::application::StyleSheet>::Style {
pub fn menu_button<'a, Message>( pub fn menu_button<'a, Message>(
content: impl Into<Element<'a, Message>>, content: impl Into<Element<'a, Message>>,
) -> crate::widget::Button<'a, Message> { ) -> crate::widget::Button<'a, Message> {
crate::widget::Button::new(content) crate::widget::button(content)
.style(Button::AppletMenu) .style(Button::AppletMenu)
.padding(menu_control_padding()) .padding(menu_control_padding())
.width(Length::Fill) .width(Length::Fill)

View file

@ -16,18 +16,20 @@ use iced_runtime::command::Action;
use std::future::Future; use std::future::Future;
/// Yields a command which contains a batch of commands. /// Yields a command which contains a batch of commands.
pub fn batch<M>(commands: impl IntoIterator<Item = Command<M>>) -> Command<M> { pub fn batch<X: 'static + Into<Y>, Y: 'static>(
Command::batch(commands) commands: impl IntoIterator<Item = Command<X>>,
) -> Command<Y> {
Command::batch(commands).map(Into::into)
} }
/// Yields a command which will run the future on the runtime executor. /// Yields a command which will run the future on the runtime executor.
pub fn future<M>(future: impl Future<Output = M> + Send + 'static) -> Command<M> { pub fn future<X: Into<Y>, Y>(future: impl Future<Output = X> + Send + 'static) -> Command<Y> {
Command::single(Action::Future(Box::pin(future))) Command::single(Action::Future(Box::pin(async move { future.await.into() })))
} }
/// Yields a command which will return a message. /// Yields a command which will return a message.
pub fn message<M: Send + 'static>(message: M) -> Command<M> { pub fn message<X: Send + 'static + Into<Y>, Y>(message: X) -> Command<Y> {
future(async move { message }) future(async move { message.into() })
} }
/// Initiates a window drag. /// Initiates a window drag.

View file

@ -3,5 +3,102 @@
//! Configurations available to libcosmic applications. //! Configurations available to libcosmic applications.
pub mod toolkit; use cosmic_config::cosmic_config_derive::CosmicConfigEntry;
pub use toolkit::CosmicTk; use cosmic_config::{Config, CosmicConfigEntry};
use serde::{Deserialize, Serialize};
use std::cell::RefCell;
use std::rc::Rc;
/// ID for the `CosmicTk` config.
pub const ID: &str = "com.system76.CosmicTk";
thread_local! {
pub static COSMIC_TK: RefCell<CosmicTk> = RefCell::new(CosmicTk::config()
.map(|c| {
CosmicTk::get_entry(&c).unwrap_or_else(|(errors, mode)| {
for why in errors {
tracing::error!(?why, "CosmicTk config entry error");
}
mode
})
})
.unwrap_or_default())
}
/// Apply the theme to other toolkits.
pub fn apply_theme_global() -> bool {
COSMIC_TK.with(|tk| tk.borrow().apply_theme_global)
}
/// Show minimize button in window header.
pub fn show_minimize() -> bool {
COSMIC_TK.with(|tk| tk.borrow().show_minimize)
}
/// Show maximize button in window header.
pub fn show_maximize() -> bool {
COSMIC_TK.with(|tk| tk.borrow().show_maximize)
}
/// Preferred icon theme.
pub fn icon_theme() -> String {
COSMIC_TK.with(|tk| tk.borrow().icon_theme.clone())
}
/// Density of CSD/SSD header bars.
pub fn header_size() -> Density {
COSMIC_TK.with(|tk| tk.borrow().header_size)
}
/// Interface density.
pub fn interface_density() -> Density {
COSMIC_TK.with(|tk| tk.borrow().interface_density)
}
#[derive(Clone, CosmicConfigEntry, Debug, Eq, PartialEq)]
#[version = 1]
pub struct CosmicTk {
/// Apply the theme to other toolkits.
pub apply_theme_global: bool,
/// Show minimize button in window header.
pub show_minimize: bool,
/// Show maximize button in window header.
pub show_maximize: bool,
/// Preferred icon theme.
pub icon_theme: String,
/// Density of CSD/SSD header bars.
pub header_size: Density,
/// Interface density.
pub interface_density: Density,
}
#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)]
pub enum Density {
Compact,
#[default]
Standard,
}
impl Default for CosmicTk {
fn default() -> Self {
Self {
apply_theme_global: false,
show_minimize: true,
show_maximize: true,
icon_theme: String::from("Cosmic"),
header_size: Density::Standard,
interface_density: Density::Standard,
}
}
}
impl CosmicTk {
pub fn config() -> Result<Config, cosmic_config::Error> {
Config::new(ID, Self::VERSION)
}
}

View file

@ -1,43 +0,0 @@
// Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0
//! Configurations for the libcosmic toolkit.
use cosmic_config::cosmic_config_derive::CosmicConfigEntry;
use cosmic_config::{Config, CosmicConfigEntry};
/// ID for the `CosmicTk` config.
pub const ID: &str = "com.system76.CosmicTk";
#[derive(Clone, CosmicConfigEntry, Debug, Eq, PartialEq)]
#[version = 1]
pub struct CosmicTk {
/// Show minimize button in window header.
pub show_minimize: bool,
/// Show maximize button in window header.
pub show_maximize: bool,
/// Preferred icon theme.
pub icon_theme: String,
/// Apply the theme to other toolkits.
pub apply_theme_global: bool,
}
impl Default for CosmicTk {
fn default() -> Self {
Self {
show_minimize: true,
show_maximize: true,
icon_theme: String::from("Cosmic"),
apply_theme_global: false,
}
}
}
impl CosmicTk {
pub fn config() -> Result<Config, cosmic_config::Error> {
Config::new(ID, Self::VERSION)
}
}

View file

@ -29,7 +29,10 @@ pub mod command;
pub mod config; pub mod config;
#[doc(inline)]
pub use cosmic_config; pub use cosmic_config;
#[doc(inline)]
pub use cosmic_theme; pub use cosmic_theme;
#[cfg(any(feature = "xdg-portal", feature = "rfd"))] #[cfg(any(feature = "xdg-portal", feature = "rfd"))]
@ -43,15 +46,31 @@ mod ext;
pub mod font; pub mod font;
#[doc(inline)]
pub use iced; pub use iced;
#[doc(inline)]
pub use iced_core; pub use iced_core;
#[doc(inline)]
pub use iced_futures; pub use iced_futures;
#[doc(inline)]
pub use iced_renderer; pub use iced_renderer;
#[doc(inline)]
pub use iced_runtime; pub use iced_runtime;
#[cfg(feature = "wayland")] #[cfg(feature = "wayland")]
pub use iced_sctk; pub use iced_sctk;
#[doc(inline)]
pub use iced_style; pub use iced_style;
#[doc(inline)]
pub use iced_widget; pub use iced_widget;
#[doc(inline)]
#[cfg(feature = "winit")] #[cfg(feature = "winit")]
pub use iced_winit; pub use iced_winit;
@ -67,6 +86,8 @@ pub mod process;
pub use cctk; pub use cctk;
pub mod theme; pub mod theme;
#[doc(inline)]
pub use theme::{style, Theme}; pub use theme::{style, Theme};
pub mod widget; pub mod widget;

View file

@ -1,7 +1,6 @@
use ashpd::desktop::settings::{ColorScheme, Contrast}; use ashpd::desktop::settings::{ColorScheme, Contrast};
use ashpd::desktop::Color; use ashpd::desktop::Color;
use iced::futures::{self, select, FutureExt, SinkExt, StreamExt}; use iced::futures::{self, select, FutureExt, SinkExt, StreamExt};
use iced::Subscription;
use iced_futures::subscription; use iced_futures::subscription;
use tracing::error; use tracing::error;
@ -15,68 +14,83 @@ pub enum Desktop {
pub fn desktop_settings() -> iced_futures::Subscription<Desktop> { pub fn desktop_settings() -> iced_futures::Subscription<Desktop> {
subscription::channel(std::any::TypeId::of::<Desktop>(), 10, |mut tx| { subscription::channel(std::any::TypeId::of::<Desktop>(), 10, |mut tx| {
async move { async move {
let Ok(settings) = ashpd::desktop::settings::Settings::new().await else { let mut attempts = 0;
// wait forever
error!("Failed to create the settings proxy");
futures::future::pending::<()>().await;
unreachable!()
};
match settings.color_scheme().await {
Ok(color_scheme) => {
let _ = tx.send(Desktop::ColorScheme(color_scheme)).await;
}
Err(err) => error!("Failed to get the color scheme {err:?}"),
};
match settings.contrast().await {
Ok(contrast) => {
let _ = tx.send(Desktop::Contrast(contrast)).await;
}
Err(err) => error!("Failed to get the contrast {err:?}"),
};
let mut color_scheme_stream = settings.receive_color_scheme_changed().await.ok();
if color_scheme_stream.is_none() {
error!("Failed to receive color scheme changes");
}
let mut contrast_stream = settings.receive_contrast_changed().await.ok();
if contrast_stream.is_none() {
error!("Failed to receive contrast changes");
}
loop { loop {
let next_color_scheme = async { let Ok(settings) = ashpd::desktop::settings::Settings::new().await else {
if let Some(s) = color_scheme_stream.as_mut() { error!("Failed to create the settings proxy");
return s.next().await; #[cfg(feature = "tokio")]
::tokio::time::sleep(::tokio::time::Duration::from_secs(2_u64.pow(attempts)))
.await;
#[cfg(not(feature = "tokio"))]
{
pending::<()>().await;
unreachable!();
} }
futures::future::pending().await attempts += 1;
continue;
}; };
let next_contrast = async { match settings.color_scheme().await {
if let Some(s) = contrast_stream.as_mut() { Ok(color_scheme) => {
return s.next().await; let _ = tx.send(Desktop::ColorScheme(color_scheme)).await;
} }
futures::future::pending().await Err(err) => error!("Failed to get the color scheme {err:?}"),
};
match settings.contrast().await {
Ok(contrast) => {
let _ = tx.send(Desktop::Contrast(contrast)).await;
}
Err(err) => error!("Failed to get the contrast {err:?}"),
}; };
select! { let mut color_scheme_stream = settings.receive_color_scheme_changed().await.ok();
s = next_color_scheme.fuse() => { if color_scheme_stream.is_none() {
if let Some(s) = s { error!("Failed to receive color scheme changes");
_ = tx.send(Desktop::ColorScheme(s)).await; }
} else {
color_scheme_stream = None; let mut contrast_stream = settings.receive_contrast_changed().await.ok();
if contrast_stream.is_none() {
error!("Failed to receive contrast changes");
}
loop {
if color_scheme_stream.is_none() && contrast_stream.is_none() {
break;
}
let next_color_scheme = async {
if let Some(s) = color_scheme_stream.as_mut() {
return s.next().await;
} }
}, futures::future::pending().await
};
c = next_contrast.fuse() => { let next_contrast = async {
if let Some(c) = c { if let Some(s) = contrast_stream.as_mut() {
_ = tx.send(Desktop::Contrast(c)).await; return s.next().await;
} else {
contrast_stream = None;
} }
} futures::future::pending().await
}; };
select! {
s = next_color_scheme.fuse() => {
if let Some(s) = s {
_ = tx.send(Desktop::ColorScheme(s)).await;
} else {
color_scheme_stream = None;
}
},
c = next_contrast.fuse() => {
if let Some(c) = c {
_ = tx.send(Desktop::Contrast(c)).await;
} else {
contrast_stream = None;
}
}
};
// Reset the attempts counter if we successfully received a change
attempts = 0;
}
} }
} }
}) })

View file

@ -120,8 +120,8 @@ pub fn appearance(
} }
Button::MenuRoot => { Button::MenuRoot => {
appearance.background = None; appearance.background = None;
appearance.icon_color = Some(cosmic.accent.base.into()); appearance.icon_color = None;
appearance.text_color = Some(cosmic.accent.base.into()); appearance.text_color = None;
} }
Button::MenuItem => { Button::MenuItem => {
let (background, _, _) = color(&cosmic.background.component); let (background, _, _) = color(&cosmic.background.component);

View file

@ -452,8 +452,8 @@ impl container::StyleSheet for Theme {
radius: [ radius: [
cosmic.corner_radii.radius_0[0], cosmic.corner_radii.radius_0[0],
cosmic.corner_radii.radius_0[1], cosmic.corner_radii.radius_0[1],
cosmic.corner_radii.radius_xs[2], cosmic.corner_radii.radius_s[2],
cosmic.corner_radii.radius_xs[3], cosmic.corner_radii.radius_s[3],
] ]
.into(), .into(),
..Default::default() ..Default::default()
@ -494,8 +494,8 @@ impl container::StyleSheet for Theme {
background: Some(iced::Background::Color(cosmic.background.base.into())), background: Some(iced::Background::Color(cosmic.background.base.into())),
border: Border { border: Border {
radius: [ radius: [
cosmic.corner_radii.radius_xs[0], cosmic.corner_radii.radius_s[0],
cosmic.corner_radii.radius_xs[1], cosmic.corner_radii.radius_s[1],
cosmic.corner_radii.radius_0[2], cosmic.corner_radii.radius_0[2],
cosmic.corner_radii.radius_0[3], cosmic.corner_radii.radius_0[3],
] ]

View file

@ -19,7 +19,7 @@ pub struct Appearance {
pub border_color: Color, pub border_color: Color,
/// The expand value of the menus' background /// The expand value of the menus' background
pub background_expand: [u16; 4], pub background_expand: [u16; 4],
/// The highlighted path [`Color`] of the the menu bar and its menus. // /// The highlighted path [`Color`] of the the menu bar and its menus.
pub path: Color, pub path: Color,
} }

View file

@ -9,18 +9,27 @@ pub use self::button::Button;
mod dropdown; mod dropdown;
pub mod iced; pub mod iced;
#[doc(inline)]
pub use self::iced::Application; pub use self::iced::Application;
#[doc(inline)]
pub use self::iced::Checkbox; pub use self::iced::Checkbox;
#[doc(inline)]
pub use self::iced::Container; pub use self::iced::Container;
#[doc(inline)]
pub use self::iced::ProgressBar; pub use self::iced::ProgressBar;
#[doc(inline)]
pub use self::iced::Rule; pub use self::iced::Rule;
#[doc(inline)]
pub use self::iced::Svg; pub use self::iced::Svg;
#[doc(inline)]
pub use self::iced::Text; pub use self::iced::Text;
pub mod menu_bar; pub mod menu_bar;
mod segmented_button; mod segmented_button;
#[doc(inline)]
pub use self::segmented_button::SegmentedButton; pub use self::segmented_button::SegmentedButton;
mod text_input; mod text_input;
#[doc(inline)]
pub use self::text_input::TextInput; pub use self::text_input::TextInput;

View file

@ -1,3 +1,5 @@
//! A container which constraints itself to a specific aspect ratio.
use iced::widget::Container; use iced::widget::Container;
use iced::Size; use iced::Size;
use iced_core::alignment; use iced_core::alignment;
@ -21,9 +23,7 @@ where
AspectRatio::new(content, ratio) AspectRatio::new(content, ratio)
} }
/// An element decorating some content. /// A container which constraints itself to a specific aspect ratio.
///
/// It is normally used for alignment purposes.
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct AspectRatio<'a, Message, Renderer> pub struct AspectRatio<'a, Message, Renderer>
where where

View file

@ -13,12 +13,14 @@ use std::borrow::Cow;
pub type Button<'a, Message> = Builder<'a, Message, Icon>; pub type Button<'a, Message> = Builder<'a, Message, Icon>;
/// The icon variant of a button.
pub struct Icon { pub struct Icon {
handle: Handle, handle: Handle,
vertical: bool, vertical: bool,
selected: bool, selected: bool,
} }
/// A button constructed from an icon handle, using icon button styling.
pub fn icon<'a, Message>(handle: impl Into<Handle>) -> Button<'a, Message> { pub fn icon<'a, Message>(handle: impl Into<Handle>) -> Button<'a, Message> {
Button::new(Icon { Button::new(Icon {
handle: handle.into(), handle: handle.into(),

View file

@ -11,6 +11,7 @@ use std::borrow::Cow;
pub type Button<'a, Message> = Builder<'a, Message, Image<'a, Handle, Message>>; pub type Button<'a, Message> = Builder<'a, Message, Image<'a, Handle, Message>>;
/// A button constructed from an image handle, using image button styling.
pub fn image<'a, Message>(handle: impl Into<Handle> + 'a) -> Button<'a, Message> { pub fn image<'a, Message>(handle: impl Into<Handle> + 'a) -> Button<'a, Message> {
Button::new(Image { Button::new(Image {
image: widget::image(handle).border_radius([9.0; 4]), image: widget::image(handle).border_radius([9.0; 4]),
@ -19,6 +20,7 @@ pub fn image<'a, Message>(handle: impl Into<Handle> + 'a) -> Button<'a, Message>
}) })
} }
/// The image variant of a button.
pub struct Image<'a, Handle, Message> { pub struct Image<'a, Handle, Message> {
image: widget::Image<'a, Handle>, image: widget::Image<'a, Handle>,
selected: bool, selected: bool,

View file

@ -1,6 +1,8 @@
// Copyright 2023 System76 <info@system76.com> // Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
//! Hyperlink button widget
use super::Builder; use super::Builder;
use super::Style; use super::Style;
use crate::prelude::*; use crate::prelude::*;
@ -17,6 +19,7 @@ pub struct Hyperlink {
trailing_icon: bool, trailing_icon: bool,
} }
/// A hyperlink button.
pub fn link<'a, Message>(label: impl Into<Cow<'a, str>> + 'static) -> Button<'a, Message> { pub fn link<'a, Message>(label: impl Into<Cow<'a, str>> + 'static) -> Button<'a, Message> {
Button::new( Button::new(
label, label,

View file

@ -1,29 +1,41 @@
// Copyright 2023 System76 <info@system76.com> // Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
//! Button widgets for COSMIC applications.
pub use crate::theme::Button as Style; pub use crate::theme::Button as Style;
pub mod link; pub mod link;
use derive_setters::Setters; use derive_setters::Setters;
#[doc(inline)]
pub use link::link; pub use link::link;
#[doc(inline)]
pub use link::Button as LinkButton; pub use link::Button as LinkButton;
mod icon; mod icon;
#[doc(inline)]
pub use icon::icon; pub use icon::icon;
#[doc(inline)]
pub use icon::Button as IconButton; pub use icon::Button as IconButton;
mod image; mod image;
#[doc(inline)]
pub use image::image; pub use image::image;
#[doc(inline)]
pub use image::Button as ImageButton; pub use image::Button as ImageButton;
mod style; mod style;
#[doc(inline)]
pub use style::{Appearance, StyleSheet}; pub use style::{Appearance, StyleSheet};
mod text; mod text;
#[doc(inline)]
pub use text::Button as TextButton; pub use text::Button as TextButton;
#[doc(inline)]
pub use text::{destructive, standard, suggested, text}; pub use text::{destructive, standard, suggested, text};
mod widget; mod widget;
#[doc(inline)]
pub use widget::{draw, focus, layout, mouse_interaction, Button}; pub use widget::{draw, focus, layout, mouse_interaction, Button};
use iced_core::font::Weight; use iced_core::font::Weight;
@ -31,10 +43,12 @@ use iced_core::widget::Id;
use iced_core::{Length, Padding}; use iced_core::{Length, Padding};
use std::borrow::Cow; use std::borrow::Cow;
/// A button with the default style, which may contain any widget as its content.
pub fn button<'a, Message>(content: impl Into<crate::Element<'a, Message>>) -> Button<'a, Message> { pub fn button<'a, Message>(content: impl Into<crate::Element<'a, Message>>) -> Button<'a, Message> {
Button::new(content) Button::new(content)
} }
/// An image button which may contain any widget as its content.
pub fn custom_image_button<'a, Message>( pub fn custom_image_button<'a, Message>(
content: impl Into<crate::Element<'a, Message>>, content: impl Into<crate::Element<'a, Message>>,
on_remove: Option<Message>, on_remove: Option<Message>,
@ -42,6 +56,7 @@ pub fn custom_image_button<'a, Message>(
Button::new_image(content, on_remove) Button::new_image(content, on_remove)
} }
/// A builder for constructing a custom [`Button`].
#[must_use] #[must_use]
#[derive(Setters)] #[derive(Setters)]
pub struct Builder<'a, Message, Variant> { pub struct Builder<'a, Message, Variant> {

View file

@ -10,26 +10,31 @@ use std::borrow::Cow;
pub type Button<'a, Message> = Builder<'a, Message, Text>; pub type Button<'a, Message> = Builder<'a, Message, Text>;
/// A text button with the destructive style
pub fn destructive<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> { pub fn destructive<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> {
Button::new(Text::new()) Button::new(Text::new())
.label(label) .label(label)
.style(Style::Destructive) .style(Style::Destructive)
} }
/// A text button with the suggested style
pub fn suggested<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> { pub fn suggested<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> {
Button::new(Text::new()) Button::new(Text::new())
.label(label) .label(label)
.style(Style::Suggested) .style(Style::Suggested)
} }
/// A text button with the standard style
pub fn standard<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> { pub fn standard<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> {
Button::new(Text::new()).label(label) Button::new(Text::new()).label(label)
} }
/// A text button with the text style
pub fn text<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> { pub fn text<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> {
Button::new(Text::new()).label(label).style(Style::Text) Button::new(Text::new()).label(label).style(Style::Text)
} }
/// The text variant of a button.
pub struct Text { pub struct Text {
pub(super) leading_icon: Option<icon::Handle>, pub(super) leading_icon: Option<icon::Handle>,
pub(super) trailing_icon: Option<icon::Handle>, pub(super) trailing_icon: Option<icon::Handle>,

View file

@ -59,7 +59,7 @@ pub struct Button<'a, Message> {
impl<'a, Message> Button<'a, Message> { impl<'a, Message> Button<'a, Message> {
/// Creates a new [`Button`] with the given content. /// Creates a new [`Button`] with the given content.
pub fn new(content: impl Into<crate::Element<'a, Message>>) -> Self { pub(super) fn new(content: impl Into<crate::Element<'a, Message>>) -> Self {
Self { Self {
id: Id::unique(), id: Id::unique(),
#[cfg(feature = "a11y")] #[cfg(feature = "a11y")]
@ -375,7 +375,13 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
}; };
let mut icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color); let mut icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color);
let mut text_color = styling.text_color.unwrap_or(renderer_style.text_color);
// Menu roots should share the accent color that icons get in the header.
let mut text_color = if matches!(self.style, crate::theme::Button::MenuRoot) {
icon_color
} else {
styling.text_color.unwrap_or(renderer_style.text_color)
};
if let Some(alpha) = headerbar_alpha { if let Some(alpha) = headerbar_alpha {
icon_color.a = alpha; icon_color.a = alpha;
@ -403,77 +409,105 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
on_remove, on_remove,
} = &self.variant } = &self.variant
{ {
let selection_background = theme.selection_background(); let mut parent_bounds = bounds;
parent_bounds.y -= 8.0;
parent_bounds.width += 16.0;
parent_bounds.height += 16.0;
let c_rad = THEME.with(|t| t.borrow().cosmic().corner_radii); renderer.with_layer(parent_bounds, |renderer| {
let selection_background = theme.selection_background();
if self.selected { let c_rad = THEME.with(|t| t.borrow().cosmic().corner_radii);
renderer.fill_quad(
Quad { // NOTE: Workaround to round the border of the unselected, unhovered image.
bounds: Rectangle { if !self.selected && !is_mouse_over {
width: 24.0, let mut bounds = bounds;
height: 20.0, bounds.x -= 2.0;
x: bounds.x + styling.border_width, bounds.y -= 2.0;
y: bounds.y + (bounds.height - 20.0 - styling.border_width), bounds.width += 4.0;
bounds.height += 4.0;
renderer.fill_quad(
renderer::Quad {
bounds,
border: Border {
width: 2.0,
color: crate::theme::active().current_container().base.into(),
radius: 9.0.into(),
},
shadow: Shadow::default(),
}, },
border: Border { Color::TRANSPARENT,
radius: [ );
c_rad.radius_0[0], }
c_rad.radius_s[1],
c_rad.radius_0[2], if self.selected {
c_rad.radius_s[3], renderer.fill_quad(
] Quad {
.into(), bounds: Rectangle {
..Default::default() width: 24.0,
height: 20.0,
x: bounds.x + styling.border_width,
y: bounds.y + (bounds.height - 20.0 - styling.border_width),
},
border: Border {
radius: [
c_rad.radius_0[0],
c_rad.radius_s[1],
c_rad.radius_0[2],
c_rad.radius_s[3],
]
.into(),
..Default::default()
},
shadow: Shadow::default(),
}, },
shadow: Shadow::default(), selection_background,
}, );
selection_background,
);
iced_core::svg::Renderer::draw( iced_core::svg::Renderer::draw(
renderer, renderer,
crate::widget::common::object_select().clone(), crate::widget::common::object_select().clone(),
Some(icon_color), Some(icon_color),
Rectangle { Rectangle {
width: 16.0, width: 16.0,
height: 16.0, height: 16.0,
x: bounds.x + 5.0 + styling.border_width, x: bounds.x + 5.0 + styling.border_width,
y: bounds.y + (bounds.height - 18.0 - styling.border_width), y: bounds.y + (bounds.height - 18.0 - styling.border_width),
}, },
); );
} }
if on_remove.is_some() { if on_remove.is_some() {
if let Some(position) = cursor.position() { if let Some(position) = cursor.position() {
if bounds.contains(position) { if bounds.contains(position) {
let bounds = removal_bounds(layout.bounds(), 4.0); let bounds = removal_bounds(layout.bounds(), 4.0);
renderer.fill_quad( renderer.fill_quad(
renderer::Quad { renderer::Quad {
bounds, bounds,
shadow: Shadow::default(), shadow: Shadow::default(),
border: Border { border: Border {
radius: c_rad.radius_m.into(), radius: c_rad.radius_m.into(),
..Default::default() ..Default::default()
},
}, },
}, selection_background,
selection_background, );
);
iced_core::svg::Renderer::draw( iced_core::svg::Renderer::draw(
renderer, renderer,
close_icon.clone(), close_icon.clone(),
Some(icon_color), Some(icon_color),
Rectangle { Rectangle {
width: 16.0, width: 16.0,
height: 16.0, height: 16.0,
x: bounds.x + 4.0, x: bounds.x + 4.0,
y: bounds.y + 4.0, y: bounds.y + 4.0,
}, },
); );
}
} }
} }
} });
} }
} }
@ -519,7 +553,7 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
let child_tree = self let child_tree = self
.content .content
.as_widget() .as_widget()
.a11y_nodes(child_layout, &child_tree, p); .a11y_nodes(child_layout, child_tree, p);
let Rectangle { let Rectangle {
x, x,
@ -775,19 +809,38 @@ pub fn draw<Renderer: iced_core::Renderer, Theme>(
// Then draw the button contents onto the background. // Then draw the button contents onto the background.
draw_contents(renderer, styling); draw_contents(renderer, styling);
// Finish by drawing the border above the contents. let mut clipped_bounds = bounds;
renderer.fill_quad( clipped_bounds.height += styling.border_width;
renderer::Quad {
bounds, renderer.with_layer(clipped_bounds, |renderer| {
border: Border { // NOTE: Workaround to round the border of the hovered/selected image.
width: styling.border_width, renderer.fill_quad(
color: styling.border_color, renderer::Quad {
radius: styling.border_radius, bounds,
border: Border {
width: styling.border_width,
color: crate::theme::active().current_container().base.into(),
radius: 0.0.into(),
},
shadow: Shadow::default(),
}, },
shadow: Shadow::default(), Color::TRANSPARENT,
}, );
Color::TRANSPARENT,
); // Finish by drawing the border above the contents.
renderer.fill_quad(
renderer::Quad {
bounds,
border: Border {
width: styling.border_width,
color: styling.border_color,
radius: styling.border_radius,
},
shadow: Shadow::default(),
},
Color::TRANSPARENT,
);
});
} else { } else {
draw_contents(renderer, styling); draw_contents(renderer, styling);
} }

View file

@ -1,6 +1,8 @@
// Copyright 2024 System76 <info@system76.com> // Copyright 2024 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
//! A widget that displays an interactive calendar.
use std::cmp; use std::cmp;
use crate::iced_core::{Length, Padding}; use crate::iced_core::{Length, Padding};
@ -8,6 +10,7 @@ use crate::widget::{button, column, grid, icon, row, text, Grid};
use chrono::{Datelike, Days, Months, NaiveDate, Weekday}; use chrono::{Datelike, Days, Months, NaiveDate, Weekday};
use iced::alignment::{Horizontal, Vertical}; use iced::alignment::{Horizontal, Vertical};
/// A widget that displays an interactive calendar.
pub fn calendar<M>( pub fn calendar<M>(
selected: &NaiveDate, selected: &NaiveDate,
on_select: impl Fn(NaiveDate) -> M + 'static, on_select: impl Fn(NaiveDate) -> M + 'static,

View file

@ -1,6 +1,8 @@
// Copyright 2023 System76 <info@system76.com> // Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
//! Widgets for selecting colors with a color picker.
use std::borrow::Cow; use std::borrow::Cow;
use std::rc::Rc; use std::rc::Rc;
use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::atomic::{AtomicBool, Ordering};
@ -29,9 +31,12 @@ use palette::{FromColor, RgbHue};
use super::button::StyleSheet; use super::button::StyleSheet;
use super::divider::horizontal; use super::divider::horizontal;
use super::icon::{self, from_name}; use super::icon::{self, from_name};
use super::segmented_button::{self, Model, SingleSelect}; use super::segmented_button::{self, SingleSelect};
use super::{button, segmented_control, text, text_input, tooltip, Icon}; use super::{button, segmented_control, text, text_input, tooltip, Icon};
#[doc(inline)]
pub use ColorPickerModel as Model;
// TODO is this going to look correct enough? // TODO is this going to look correct enough?
lazy_static! { lazy_static! {
pub static ref HSV_RAINBOW: Vec<ColorStop> = (0u16..8) pub static ref HSV_RAINBOW: Vec<ColorStop> = (0u16..8)
@ -64,7 +69,7 @@ pub enum ColorPickerUpdate {
#[derive(Setters)] #[derive(Setters)]
pub struct ColorPickerModel { pub struct ColorPickerModel {
#[setters(skip)] #[setters(skip)]
segmented_model: Model<SingleSelect>, segmented_model: segmented_button::Model<SingleSelect>,
#[setters(skip)] #[setters(skip)]
active_color: palette::Hsv, active_color: palette::Hsv,
#[setters(skip)] #[setters(skip)]
@ -246,7 +251,7 @@ impl ColorPickerModel {
#[derive(Setters, Clone)] #[derive(Setters, Clone)]
pub struct ColorPickerBuilder<'a, Message> { pub struct ColorPickerBuilder<'a, Message> {
#[setters(skip)] #[setters(skip)]
model: &'a Model<SingleSelect>, model: &'a segmented_button::Model<SingleSelect>,
#[setters(skip)] #[setters(skip)]
active_color: palette::Hsv, active_color: palette::Hsv,
#[setters(skip)] #[setters(skip)]
@ -757,6 +762,7 @@ fn color_to_string(c: palette::Hsv, is_hex: bool) -> String {
} }
} }
/// A button for selecting a color from a color picker.
pub fn color_button<'a, Message: 'static>( pub fn color_button<'a, Message: 'static>(
on_press: Option<Message>, on_press: Option<Message>,
color: Option<Color>, color: Option<Color>,

View file

@ -1,6 +1,8 @@
// Copyright 2023 System76 <info@system76.com> // Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
//! An overlayed widget that attaches a toggleable context drawer to the view.
mod overlay; mod overlay;
mod widget; mod widget;
@ -8,6 +10,7 @@ pub use widget::ContextDrawer;
use crate::Element; use crate::Element;
/// An overlayed widget that attaches a toggleable context drawer to the view.
pub fn context_drawer<'a, Message: Clone + 'static, Content, Drawer>( pub fn context_drawer<'a, Message: Clone + 'static, Content, Drawer>(
header: &'a str, header: &'a str,
on_close: Message, on_close: Message,

View file

@ -91,7 +91,7 @@ impl<'a, Message: Clone> Widget<Message, crate::Theme, crate::Renderer>
} }
fn diff(&mut self, tree: &mut Tree) { fn diff(&mut self, tree: &mut Tree) {
self.content.as_widget_mut().diff(&mut tree.children[0]); tree.children[0].diff(self.content.as_widget_mut());
// if let Some(ref mut context_menus) = self.context_menu { // if let Some(ref mut context_menus) = self.context_menu {
// for (menu, tree) in context_menus // for (menu, tree) in context_menus

View file

@ -253,7 +253,7 @@ impl<'a, Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
} }
fn diff(&mut self, tree: &mut Tree) { fn diff(&mut self, tree: &mut Tree) {
self.container.as_widget_mut().diff(&mut tree.children[0]); tree.children[0].diff(self.container.as_widget_mut());
} }
fn state(&self) -> iced_core::widget::tree::State { fn state(&self) -> iced_core::widget::tree::State {

View file

@ -132,7 +132,7 @@ impl<
} }
fn diff(&mut self, tree: &mut Tree) { fn diff(&mut self, tree: &mut Tree) {
self.container.as_widget_mut().diff(&mut tree.children[0]); tree.children[0].diff(self.container.as_widget_mut());
} }
fn state(&self) -> iced_core::widget::tree::State { fn state(&self) -> iced_core::widget::tree::State {

View file

@ -2,6 +2,8 @@
// Copyright 2019 Héctor Ramón, Iced contributors // Copyright 2019 Héctor Ramón, Iced contributors
// SPDX-License-Identifier: MPL-2.0 AND MIT // SPDX-License-Identifier: MPL-2.0 AND MIT
//! Displays a list of options in a popover menu on select.
pub mod menu; pub mod menu;
pub use menu::Menu; pub use menu::Menu;
@ -10,6 +12,7 @@ pub mod multi;
mod widget; mod widget;
pub use widget::*; pub use widget::*;
/// Displays a list of options in a popover menu on select.
pub fn dropdown<'a, S: AsRef<str>, Message: 'a>( pub fn dropdown<'a, S: AsRef<str>, Message: 'a>(
selections: &'a [S], selections: &'a [S],
selected: Option<usize>, selected: Option<usize>,

View file

@ -4,8 +4,14 @@
use crate::{Element, Renderer}; use crate::{Element, Renderer};
use iced_core::layout::{Limits, Node}; use iced_core::layout::{Limits, Node};
use iced_core::widget::Tree; use iced_core::widget::Tree;
use iced_core::{Padding, Point, Size}; use iced_core::{Length, Padding, Point, Size};
use taffy::geometry::Rect;
use taffy::style::{AlignItems, Dimension, Display, Style};
use taffy::style_helpers::length;
use taffy::{AlignContent, TaffyTree};
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_lines)]
pub fn resolve<Message>( pub fn resolve<Message>(
renderer: &Renderer, renderer: &Renderer,
limits: &Limits, limits: &Limits,
@ -13,79 +19,153 @@ pub fn resolve<Message>(
padding: Padding, padding: Padding,
column_spacing: f32, column_spacing: f32,
row_spacing: f32, row_spacing: f32,
min_item_width: Option<f32>,
justify_items: Option<AlignItems>,
align_items: Option<AlignItems>,
justify_content: Option<AlignContent>,
tree: &mut [Tree], tree: &mut [Tree],
) -> Node { ) -> Node {
let limits = limits.shrink(padding); let max_size = limits.max();
let mut leafs = Vec::with_capacity(items.len());
let mut nodes = Vec::with_capacity(items.len()); let mut nodes = Vec::with_capacity(items.len());
let max_flex_width = limits.max().width; let mut taffy_tree = TaffyTree::<()>::with_capacity(items.len() + 1);
let mut flex_width = 0.0f32;
let mut flex_height = 0.0f32;
let mut current_row_width = 0.0f32; let style = taffy::Style {
let mut current_row_height = 0.0f32; display: Display::Flex,
flex_direction: taffy::FlexDirection::Row,
flex_wrap: taffy::FlexWrap::Wrap,
let mut row_buffer = Vec::<Node>::with_capacity(8); gap: taffy::geometry::Size {
width: length(column_spacing),
height: length(row_spacing),
},
min_size: taffy::geometry::Size {
width: length(max_size.width),
height: Dimension::Auto,
},
align_items,
justify_items,
justify_content,
padding: Rect {
left: length(padding.left),
right: length(padding.right),
top: length(padding.top),
bottom: length(padding.bottom),
},
..taffy::Style::default()
};
for (child, tree) in items.iter().zip(tree.iter_mut()) { for (child, tree) in items.iter().zip(tree.iter_mut()) {
// Calculate the dimensions of the item. let child_widget = child.as_widget();
let child_node = child.as_widget().layout(tree, renderer, &limits); let child_node = child_widget.layout(tree, renderer, limits);
let size = child_node.size(); let size = child_node.size();
// Calculate the required additional width to fit the item into the current row. nodes.push(child_node);
let mut required_width = size.width
+ if row_buffer.is_empty() { let c_size = child_widget.size();
0.0 let (width, flex_grow, justify_self) = match c_size.width {
} else { Length::Fill | Length::FillPortion(_) => {
row_spacing (Dimension::Auto, 1.0, Some(AlignItems::Stretch))
}
_ => (length(size.width), 0.0, None),
};
let child_style = Style {
flex_grow,
min_size: taffy::geometry::Size {
width: match min_item_width {
Some(width) => length(size.width.min(width)),
None => Dimension::Auto,
},
height: Dimension::Auto,
},
size: taffy::geometry::Size {
width,
height: match c_size.height {
Length::Fill | Length::FillPortion(_) => Dimension::Auto,
_ => length(size.height),
},
},
justify_self,
..Style::default()
};
leafs.push(match taffy_tree.new_leaf(child_style) {
Ok(leaf) => leaf,
Err(why) => {
tracing::error!(?why, "failed to add child element to flex row");
continue;
}
});
}
let root = match taffy_tree.new_with_children(style, &leafs) {
Ok(root) => root,
Err(why) => {
tracing::error!(?why, "flex row style is invalid");
return Node::new(Size::ZERO);
}
};
if let Err(why) = taffy_tree.compute_layout(
root,
taffy::geometry::Size {
width: length(max_size.width),
height: length(max_size.height),
},
) {
tracing::error!(?why, "flex row layout invalid");
return Node::new(Size::ZERO);
}
let flex_layout = match taffy_tree.layout(root) {
Ok(layout) => layout,
Err(why) => {
tracing::error!(?why, "cannot get flex row layout");
return Node::new(Size::ZERO);
}
};
leafs
.into_iter()
.zip(items.iter())
.zip(nodes.iter_mut())
.zip(tree)
.for_each(|(((leaf, child), node), tree)| {
let Ok(leaf_layout) = taffy_tree.layout(leaf) else {
return;
}; };
// If it fits, add it to the current row, or create a new one. let child_widget = child.as_widget();
if current_row_width + required_width > max_flex_width { let c_size = child_widget.size();
if flex_height != 0.0f32 { match c_size.width {
flex_height += column_spacing; Length::Fill | Length::FillPortion(_) => {
*node =
child_widget.layout(tree, renderer, &limits.width(leaf_layout.size.width));
}
_ => (),
} }
let mut pos_x = 0.0f32; *node = node.clone().move_to(Point {
let pos_y = flex_height; x: leaf_layout.location.x,
y: leaf_layout.location.y,
});
});
for mut child_node in row_buffer.drain(..) { let size = Size {
child_node = child_node.move_to(Point::new(pos_x, pos_y)); width: flex_layout.content_size.width,
pos_x += row_spacing + child_node.size().width; height: flex_layout.content_size.height,
nodes.push(child_node); };
}
flex_height += current_row_height; Node::with_children(size, nodes)
flex_width = flex_width.max(current_row_width);
required_width -= row_spacing;
current_row_width = 0.0;
}
current_row_width += required_width;
current_row_height = current_row_height.max(size.height);
row_buffer.push(child_node);
}
if !row_buffer.is_empty() {
if flex_height != 0.0f32 {
flex_height += column_spacing;
}
let mut pos_x = 0.0f32;
let pos_y = flex_height;
for mut child_node in row_buffer.drain(..) {
child_node = child_node.move_to(Point::new(pos_x, pos_y));
pos_x += row_spacing + child_node.size().width;
nodes.push(child_node);
}
flex_height += current_row_height;
flex_width = flex_width.max(current_row_width);
}
let flex_size = limits.resolve(flex_width, flex_height, Size::new(flex_width, flex_height));
Node::with_children(flex_size.expand(padding), nodes)
} }

View file

@ -1,6 +1,8 @@
// Copyright 2023 System76 <info@system76.com> // Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
//! Responsively generates rows of widgets based on the dimensions of its children.
pub mod layout; pub mod layout;
pub mod widget; pub mod widget;
@ -8,7 +10,7 @@ pub use widget::FlexRow;
use crate::Element; use crate::Element;
/// Responsively generates rows and columns of widgets based on its dimmensions. /// Responsively generates rows of widgets based on the dimensions of its children.
pub const fn flex_row<Message>(children: Vec<Element<Message>>) -> FlexRow<Message> { pub const fn flex_row<Message>(children: Vec<Element<Message>>) -> FlexRow<Message> {
FlexRow::new(children) FlexRow::new(children)
} }

View file

@ -17,6 +17,7 @@ pub struct FlexRow<'a, Message> {
#[setters(skip)] #[setters(skip)]
children: Vec<Element<'a, Message>>, children: Vec<Element<'a, Message>>,
/// Sets the padding around the widget. /// Sets the padding around the widget.
#[setters(into)]
padding: Padding, padding: Padding,
/// Sets the space between each column of items. /// Sets the space between each column of items.
column_spacing: u16, column_spacing: u16,
@ -24,21 +25,64 @@ pub struct FlexRow<'a, Message> {
row_spacing: u16, row_spacing: u16,
/// Sets the width. /// Sets the width.
width: Length, width: Length,
/// Sets minimum width of items that grow.
#[setters(into)]
min_item_width: Option<f32>,
/// Sets the max width /// Sets the max width
max_width: f32, max_width: f32,
/// Defines how content will be aligned horizontally.
#[setters(skip)]
align_items: Option<taffy::AlignItems>,
/// Defines how content will be aligned vertically.
#[setters(skip)]
justify_items: Option<taffy::AlignItems>,
/// Defines how the content will be justified.
#[setters(into)]
justify_content: Option<crate::widget::JustifyContent>,
} }
impl<'a, Message> FlexRow<'a, Message> { impl<'a, Message> FlexRow<'a, Message> {
pub const fn new(children: Vec<Element<'a, Message>>) -> Self { pub(crate) const fn new(children: Vec<Element<'a, Message>>) -> Self {
Self { Self {
children, children,
padding: Padding::ZERO, padding: Padding::ZERO,
column_spacing: 4, column_spacing: 4,
row_spacing: 4, row_spacing: 4,
width: Length::Shrink, width: Length::Shrink,
min_item_width: None,
max_width: f32::INFINITY, max_width: f32::INFINITY,
align_items: None,
justify_items: None,
justify_content: None,
} }
} }
/// Defines how content will be aligned horizontally.
pub fn align_items(mut self, alignment: iced::Alignment) -> Self {
self.align_items = Some(match alignment {
iced::Alignment::Center => taffy::AlignItems::Center,
iced::Alignment::Start => taffy::AlignItems::Start,
iced::Alignment::End => taffy::AlignItems::End,
});
self
}
/// Defines how content will be aligned vertically.
pub fn justify_items(mut self, alignment: iced::Alignment) -> Self {
self.justify_items = Some(match alignment {
iced::Alignment::Center => taffy::AlignItems::Center,
iced::Alignment::Start => taffy::AlignItems::Start,
iced::Alignment::End => taffy::AlignItems::End,
});
self
}
/// Sets the space between each column and row.
pub const fn spacing(mut self, spacing: u16) -> Self {
self.column_spacing = spacing;
self.row_spacing = spacing;
self
}
} }
impl<'a, Message: 'static + Clone> Widget<Message, crate::Theme, Renderer> impl<'a, Message: 'static + Clone> Widget<Message, crate::Theme, Renderer>
@ -75,6 +119,10 @@ impl<'a, Message: 'static + Clone> Widget<Message, crate::Theme, Renderer>
self.padding, self.padding,
f32::from(self.column_spacing), f32::from(self.column_spacing),
f32::from(self.row_spacing), f32::from(self.row_spacing),
self.min_item_width,
self.align_items,
self.justify_items,
self.justify_content,
&mut tree.children, &mut tree.children,
) )
} }

View file

@ -10,8 +10,9 @@ use iced_core::{Alignment, Length, Padding, Point, Size};
use taffy::geometry::{Line, Rect}; use taffy::geometry::{Line, Rect};
use taffy::style::{AlignItems, Dimension, Display, GridPlacement, Style}; use taffy::style::{AlignItems, Dimension, Display, GridPlacement, Style};
use taffy::style_helpers::{auto, length}; use taffy::style_helpers::{auto, length};
use taffy::TaffyTree; use taffy::{AlignContent, TaffyTree};
#[allow(clippy::too_many_arguments)]
#[allow(clippy::too_many_lines)] #[allow(clippy::too_many_lines)]
pub fn resolve<Message>( pub fn resolve<Message>(
renderer: &Renderer, renderer: &Renderer,
@ -23,6 +24,7 @@ pub fn resolve<Message>(
padding: Padding, padding: Padding,
column_alignment: Alignment, column_alignment: Alignment,
row_alignment: Alignment, row_alignment: Alignment,
justify_content: Option<AlignContent>,
column_spacing: f32, column_spacing: f32,
row_spacing: f32, row_spacing: f32,
tree: &mut [Tree], tree: &mut [Tree],
@ -44,23 +46,29 @@ pub fn resolve<Message>(
nodes.push(child_node); nodes.push(child_node);
let c_size = child_widget.size(); let c_size = child_widget.size();
let (width, justify_self) = match c_size.width { let (width, flex_grow, justify_self) = match c_size.width {
Length::Fill | Length::FillPortion(_) => (Dimension::Auto, Some(AlignItems::Stretch)), Length::Fill | Length::FillPortion(_) => {
_ => (length(size.width), None), (Dimension::Auto, 1.0, Some(AlignItems::Stretch))
}
_ => (length(size.width), 0.0, None),
}; };
// Attach widget as leaf to be later assigned to grid. // Attach widget as leaf to be later assigned to grid.
let leaf = taffy.new_leaf(Style { let leaf = taffy.new_leaf(Style {
flex_grow,
grid_column: Line { grid_column: Line {
start: GridPlacement::Line((assignment.column as i16).into()), start: GridPlacement::Line((assignment.column as i16).into()),
end: GridPlacement::Line( end: GridPlacement::Line(
(assignment.column as i16 + assignment.width as i16).into(), (assignment.column as i16 + assignment.width as i16).into(),
), ),
}, },
grid_row: Line { grid_row: Line {
start: GridPlacement::Line((assignment.row as i16).into()), start: GridPlacement::Line((assignment.row as i16).into()),
end: GridPlacement::Line((assignment.row as i16 + assignment.height as i16).into()), end: GridPlacement::Line((assignment.row as i16 + assignment.height as i16).into()),
}, },
size: taffy::geometry::Size { size: taffy::geometry::Size {
width, width,
height: match c_size.height { height: match c_size.height {
@ -68,14 +76,16 @@ pub fn resolve<Message>(
_ => length(size.height), _ => length(size.height),
}, },
}, },
justify_self, justify_self,
..Style::default() ..Style::default()
}); });
match leaf { match leaf {
Ok(leaf) => leafs.push(leaf), Ok(leaf) => leafs.push(leaf),
Err(why) => { Err(why) => {
tracing::error!(%why, "cannot add leaf node to grid"); tracing::error!(?why, "cannot add leaf node to grid");
continue; continue;
} }
} }
@ -108,6 +118,8 @@ pub fn resolve<Message>(
}, },
}), }),
justify_content,
padding: Rect { padding: Rect {
left: length(padding.left), left: length(padding.left),
right: length(padding.right), right: length(padding.right),
@ -134,7 +146,7 @@ pub fn resolve<Message>(
let root = match root { let root = match root {
Ok(root) => root, Ok(root) => root,
Err(why) => { Err(why) => {
tracing::error!(%why, "grid root style invalid"); tracing::error!(?why, "grid root style invalid");
return Node::new(Size::ZERO); return Node::new(Size::ZERO);
} }
}; };
@ -146,14 +158,14 @@ pub fn resolve<Message>(
height: length(max_size.height), height: length(max_size.height),
}, },
) { ) {
tracing::error!(%why, "grid layout did not compute"); tracing::error!(?why, "grid layout did not compute");
return Node::new(Size::ZERO); return Node::new(Size::ZERO);
} }
let grid_layout = match taffy.layout(root) { let grid_layout = match taffy.layout(root) {
Ok(layout) => layout, Ok(layout) => layout,
Err(why) => { Err(why) => {
tracing::error!(%why, "cannot get layout of grid"); tracing::error!(?why, "cannot get layout of grid");
return Node::new(Size::ZERO); return Node::new(Size::ZERO);
} }
}; };
@ -183,9 +195,9 @@ pub fn resolve<Message>(
} }
let grid_size = Size { let grid_size = Size {
width: grid_layout.size.width, width: grid_layout.content_size.width,
height: grid_layout.size.height, height: grid_layout.content_size.height,
}; };
Node::with_children(grid_size.expand(padding), nodes) Node::with_children(grid_size, nodes)
} }

View file

@ -1,12 +1,14 @@
// Copyright 2023 System76 <info@system76.com> // Copyright 2023 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
//! Arrange widgets with a grid layout.
pub mod layout; pub mod layout;
pub mod widget; pub mod widget;
pub use widget::Grid; pub use widget::Grid;
/// Responsively generates rows and columns of widgets based on its dimmensions. /// Arrange widgets with a grid layout.
pub const fn grid<'a, Message>() -> Grid<'a, Message> { pub const fn grid<'a, Message>() -> Grid<'a, Message> {
Grid::new() Grid::new()
} }

View file

@ -26,6 +26,9 @@ pub struct Grid<'a, Message> {
column_alignment: Alignment, column_alignment: Alignment,
/// Alignment across rows /// Alignment across rows
row_alignment: Alignment, row_alignment: Alignment,
/// Defines how the content will be justified.
#[setters(into, strip_option)]
justify_content: Option<crate::widget::JustifyContent>,
/// Sets the space between each column of items. /// Sets the space between each column of items.
column_spacing: u16, column_spacing: u16,
/// Sets the space between each item in a row. /// Sets the space between each item in a row.
@ -50,6 +53,7 @@ impl<'a, Message> Grid<'a, Message> {
padding: Padding::ZERO, padding: Padding::ZERO,
column_alignment: Alignment::Start, column_alignment: Alignment::Start,
row_alignment: Alignment::Start, row_alignment: Alignment::Start,
justify_content: None,
column_spacing: 4, column_spacing: 4,
row_spacing: 4, row_spacing: 4,
width: Length::Shrink, width: Length::Shrink,
@ -138,6 +142,7 @@ impl<'a, Message: 'static + Clone> Widget<Message, crate::Theme, Renderer> for G
self.padding, self.padding,
self.column_alignment, self.column_alignment,
self.row_alignment, self.row_alignment,
self.justify_content,
f32::from(self.column_spacing), f32::from(self.column_spacing),
f32::from(self.row_spacing), f32::from(self.row_spacing),
&mut tree.children, &mut tree.children,

View file

@ -1,10 +1,11 @@
// Copyright 2022 System76 <info@system76.com> // Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
use crate::config::Density;
use crate::{ext::CollectionWidget, widget, Element}; use crate::{ext::CollectionWidget, widget, Element};
use apply::Apply; use apply::Apply;
use derive_setters::Setters; use derive_setters::Setters;
use iced::Length; use iced::{Length, Padding};
use iced_core::{widget::tree, Widget}; use iced_core::{widget::tree, Widget};
use std::borrow::Cow; use std::borrow::Cow;
@ -20,6 +21,7 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> {
start: Vec::new(), start: Vec::new(),
center: Vec::new(), center: Vec::new(),
end: Vec::new(), end: Vec::new(),
density: None,
focused: false, focused: false,
} }
} }
@ -50,9 +52,6 @@ pub struct HeaderBar<'a, Message> {
#[setters(strip_option)] #[setters(strip_option)]
on_right_click: Option<Message>, on_right_click: Option<Message>,
/// Focused state of the window
focused: bool,
/// Elements packed at the start of the headerbar. /// Elements packed at the start of the headerbar.
#[setters(skip)] #[setters(skip)]
start: Vec<Element<'a, Message>>, start: Vec<Element<'a, Message>>,
@ -64,6 +63,13 @@ pub struct HeaderBar<'a, Message> {
/// Elements packed at the end of the headerbar. /// Elements packed at the end of the headerbar.
#[setters(skip)] #[setters(skip)]
end: Vec<Element<'a, Message>>, end: Vec<Element<'a, Message>>,
/// Controls the density of the headerbar.
#[setters(strip_option)]
density: Option<Density>,
/// Focused state of the window
focused: bool,
} }
impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
@ -99,7 +105,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
#[must_use] #[must_use]
pub fn build(self) -> HeaderBarWidget<'a, Message> { pub fn build(self) -> HeaderBarWidget<'a, Message> {
HeaderBarWidget { HeaderBarWidget {
header_bar_inner: self.into_element(), header_bar_inner: self.view(),
} }
} }
} }
@ -253,7 +259,7 @@ impl<'a, Message: Clone + 'static> Widget<Message, crate::Theme, crate::Renderer
impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
/// Converts the headerbar builder into an Iced element. /// Converts the headerbar builder into an Iced element.
pub fn into_element(mut self) -> Element<'a, Message> { pub fn view(mut self) -> Element<'a, Message> {
// Take ownership of the regions to be packed. // Take ownership of the regions to be packed.
let start = std::mem::take(&mut self.start); let start = std::mem::take(&mut self.start);
let center = std::mem::take(&mut self.center); let center = std::mem::take(&mut self.center);
@ -263,6 +269,11 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
end.push(widget::horizontal_space(Length::Fixed(12.0)).into()); end.push(widget::horizontal_space(Length::Fixed(12.0)).into());
end.push(self.window_controls()); end.push(self.window_controls());
let (height, padding) = match self.density.unwrap_or_else(crate::config::header_size) {
crate::config::Density::Compact => (36.0, 2.0),
crate::config::Density::Standard => (48.0, 8.0),
};
// Creates the headerbar widget. // Creates the headerbar widget.
let mut widget = widget::row::with_capacity(4) let mut widget = widget::row::with_capacity(4)
// If elements exist in the start region, append them here. // If elements exist in the start region, append them here.
@ -295,9 +306,9 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
.width(Length::Shrink), .width(Length::Shrink),
) )
.align_items(iced::Alignment::Center) .align_items(iced::Alignment::Center)
.height(Length::Fixed(50.0)) .height(Length::Fixed(height))
.padding(8) .padding(padding)
.spacing(8) .spacing(padding)
.apply(widget::container) .apply(widget::container)
.style(crate::theme::Container::HeaderBar { .style(crate::theme::Container::HeaderBar {
focused: self.focused, focused: self.focused,
@ -326,9 +337,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
let mut title = Cow::default(); let mut title = Cow::default();
std::mem::swap(&mut title, &mut self.title); std::mem::swap(&mut title, &mut self.title);
widget::text(title) widget::text::heading(title)
.size(16)
.font(crate::font::FONT_SEMIBOLD)
.apply(widget::container) .apply(widget::container)
.center_x() .center_x()
.center_y() .center_y()
@ -349,6 +358,13 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
.on_press(on_press) .on_press(on_press)
}; };
let density = self.density.unwrap_or_else(crate::config::header_size);
let spacing = if matches!(density, Density::Compact) {
2
} else {
8
};
widget::row::with_capacity(3) widget::row::with_capacity(3)
.push_maybe(self.on_minimize.take().map(|m| { .push_maybe(self.on_minimize.take().map(|m| {
icon( icon(
@ -371,7 +387,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
m, m,
) )
})) }))
.spacing(8) .spacing(spacing)
.apply(widget::container) .apply(widget::container)
.height(Length::Fill) .height(Length::Fill)
.center_y() .center_y()

View file

@ -69,7 +69,7 @@ impl Icon {
} }
#[must_use] #[must_use]
fn into_element<Message: 'static>(self) -> Element<'static, Message> { fn view<'a, Message: 'a>(self) -> Element<'a, Message> {
let from_image = |handle| { let from_image = |handle| {
Image::new(handle) Image::new(handle)
.width( .width(
@ -120,8 +120,8 @@ impl Icon {
} }
} }
impl<Message: 'static> From<Icon> for Element<'static, Message> { impl<'a, Message: 'a> From<Icon> for Element<'a, Message> {
fn from(icon: Icon) -> Self { fn from(icon: Icon) -> Self {
icon.into_element::<Message>() icon.view::<Message>()
} }
} }

207
src/widget/id_container.rs Normal file
View file

@ -0,0 +1,207 @@
use iced_core::event::{self, Event};
use iced_core::layout;
use iced_core::mouse;
use iced_core::overlay;
use iced_core::renderer;
use iced_core::widget::{Id, Tree};
use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Widget};
pub use iced_style::container::{Appearance, StyleSheet};
pub fn id_container<'a, Message: 'static, Theme, E>(
content: E,
id: Id,
) -> IdContainer<'a, Message, Theme, crate::Renderer>
where
E: Into<Element<'a, Message, Theme, crate::Renderer>>,
Theme: iced_style::container::StyleSheet,
<Theme as iced_widget::container::StyleSheet>::Style: From<crate::theme::Container>,
{
IdContainer::new(content, id)
}
/// An element decorating some content.
///
/// It is normally used for alignment purposes.
#[allow(missing_debug_implementations)]
pub struct IdContainer<'a, Message, Theme, Renderer>
where
Renderer: iced_core::Renderer,
{
content: Element<'a, Message, Theme, Renderer>,
id: Id,
}
impl<'a, Message, Theme, Renderer> IdContainer<'a, Message, Theme, Renderer>
where
Renderer: iced_core::Renderer,
{
/// Creates an empty [`IdContainer`].
pub(crate) fn new<T>(content: T, id: Id) -> Self
where
T: Into<Element<'a, Message, Theme, Renderer>>,
{
IdContainer {
content: content.into(),
id,
}
}
}
impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
for IdContainer<'a, Message, Theme, Renderer>
where
Renderer: iced_core::Renderer,
{
fn children(&self) -> Vec<Tree> {
vec![Tree::new(&self.content)]
}
fn diff(&mut self, tree: &mut Tree) {
tree.children[0].diff(&mut self.content);
}
fn size(&self) -> iced_core::Size<Length> {
self.content.as_widget().size()
}
fn layout(
&self,
tree: &mut Tree,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let node = self
.content
.as_widget()
.layout(&mut tree.children[0], renderer, limits);
let size = node.size();
layout::Node::with_children(size, vec![node])
}
fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn iced_core::widget::Operation<
iced_core::widget::OperationOutputWrapper<Message>,
>,
) {
self.content.as_widget().operate(
&mut tree.children[0],
layout.children().next().unwrap(),
renderer,
operation,
);
}
fn on_event(
&mut self,
tree: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<'_, Message>,
viewport: &Rectangle,
) -> event::Status {
self.content.as_widget_mut().on_event(
&mut tree.children[0],
event.clone(),
layout.children().next().unwrap(),
cursor_position,
renderer,
clipboard,
shell,
viewport,
)
}
fn mouse_interaction(
&self,
tree: &Tree,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
viewport: &Rectangle,
renderer: &Renderer,
) -> mouse::Interaction {
let content_layout = layout.children().next().unwrap();
self.content.as_widget().mouse_interaction(
&tree.children[0],
content_layout,
cursor_position,
viewport,
renderer,
)
}
fn draw(
&self,
tree: &Tree,
renderer: &mut Renderer,
theme: &Theme,
renderer_style: &renderer::Style,
layout: Layout<'_>,
cursor_position: mouse::Cursor,
viewport: &Rectangle,
) {
let content_layout = layout.children().next().unwrap();
self.content.as_widget().draw(
&tree.children[0],
renderer,
theme,
renderer_style,
content_layout,
cursor_position,
viewport,
);
}
fn overlay<'b>(
&'b mut self,
tree: &'b mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
self.content.as_widget_mut().overlay(
&mut tree.children[0],
layout.children().next().unwrap(),
renderer,
)
}
fn drag_destinations(
&self,
state: &Tree,
layout: Layout<'_>,
dnd_rectangles: &mut iced_style::core::clipboard::DndDestinationRectangles,
) {
let content_layout = layout.children().next().unwrap();
self.content.as_widget().drag_destinations(
&state.children[0],
content_layout,
dnd_rectangles,
);
}
fn id(&self) -> Option<crate::widget::Id> {
Some(self.id.clone())
}
fn set_id(&mut self, id: crate::widget::Id) {
self.id = id;
}
}
impl<'a, Message, Theme, Renderer> From<IdContainer<'a, Message, Theme, Renderer>>
for Element<'a, Message, Theme, Renderer>
where
Message: 'a,
Renderer: 'a + iced_core::Renderer,
Theme: 'a,
{
fn from(c: IdContainer<'a, Message, Theme, Renderer>) -> Element<'a, Message, Theme, Renderer> {
Element::new(c)
}
}

View file

@ -59,14 +59,17 @@ pub use action::MenuAction as Action;
mod flex; mod flex;
pub mod key_bind; pub mod key_bind;
pub use key_bind::KeyBind;
mod menu_bar; mod menu_bar;
pub use menu_bar::MenuBar;
pub(crate) use menu_bar::MenuBarState; pub(crate) use menu_bar::MenuBarState;
pub use menu_bar::{menu_bar as bar, MenuBar};
mod menu_inner; mod menu_inner;
mod menu_tree; mod menu_tree;
pub use menu_tree::{menu_items as items, menu_root as root, MenuItem as Item, MenuTree as Tree}; pub use menu_tree::{
menu_button, menu_items as items, menu_root as root, MenuItem as Item, MenuTree as Tree,
};
pub use crate::style::menu_bar::{Appearance, StyleSheet}; pub use crate::style::menu_bar::{Appearance, StyleSheet};
pub(crate) use menu_inner::Menu; pub(crate) use menu_inner::Menu;

View file

@ -1,5 +1,3 @@
use crate::widget::segmented_button::Entity;
/// `MenuAction` is a trait that represents an action in a menu. /// `MenuAction` is a trait that represents an action in a menu.
/// ///
/// It is used to define the behavior of menu items when they are activated. /// It is used to define the behavior of menu items when they are activated.
@ -31,7 +29,7 @@ use crate::widget::segmented_button::Entity;
/// impl MenuAction for MyAction { /// impl MenuAction for MyAction {
/// type Message = MyMessage; /// type Message = MyMessage;
/// ///
/// fn message(&self, entity: Option<Entity>) -> Self::Message { /// fn message(&self) -> Self::Message {
/// match self { /// match self {
/// MyAction::Open => MyMessage::Open, /// MyAction::Open => MyMessage::Open,
/// MyAction::Save => MyMessage::Save, /// MyAction::Save => MyMessage::Save,
@ -46,12 +44,8 @@ pub trait MenuAction: Clone + Copy + Eq + PartialEq {
/// Returns a message of type `Self::Message` when the action is triggered. /// Returns a message of type `Self::Message` when the action is triggered.
/// ///
/// # Arguments
///
/// * `entity` - An optional `Entity` that may be associated with the action.
///
/// # Returns /// # Returns
/// ///
/// * `Self::Message` - The message that is produced when the action is triggered. /// * `Self::Message` - The message that is produced when the action is triggered.
fn message(&self, entity: Option<Entity>) -> Self::Message; fn message(&self) -> Self::Message;
} }

View file

@ -19,6 +19,13 @@ use iced_widget::core::{
Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget,
}; };
/// A `MenuBar` collects `MenuTree`s and handles all the layout, event processing, and drawing.
pub fn menu_bar<Message, Renderer: iced_core::Renderer>(
menu_roots: Vec<MenuTree<Message, Renderer>>,
) -> MenuBar<Message, Renderer> {
MenuBar::new(menu_roots)
}
pub(crate) struct MenuBarState { pub(crate) struct MenuBarState {
pub(crate) pressed: bool, pub(crate) pressed: bool,
pub(crate) view_cursor: Cursor, pub(crate) view_cursor: Cursor,
@ -56,8 +63,7 @@ impl Default for MenuBarState {
} }
} }
/// A `MenuBar` collects `MenuTree`s and handles /// A `MenuBar` collects `MenuTree`s and handles all the layout, event processing, and drawing.
/// all the layout, event processing and drawing
#[allow(missing_debug_implementations)] #[allow(missing_debug_implementations)]
pub struct MenuBar<'a, Message, Renderer = crate::Renderer> pub struct MenuBar<'a, Message, Renderer = crate::Renderer>
where where
@ -384,8 +390,8 @@ where
}, },
shadow: Default::default(), shadow: Default::default(),
}; };
let path_color = styling.path;
renderer.fill_quad(path_quad, path_color); renderer.fill_quad(path_quad, styling.path);
} }
} }

View file

@ -709,8 +709,8 @@ where
}, },
shadow: Shadow::default(), shadow: Shadow::default(),
}; };
let path_color = styling.path;
r.fill_quad(path_quad, path_color); r.fill_quad(path_quad, styling.path);
} }
// draw item // draw item

View file

@ -141,21 +141,19 @@ where
} }
} }
/// This macro creates a button for a MenuTree. pub fn menu_button<'a, Message: 'a>(
#[macro_export] children: Vec<crate::Element<'a, Message>>,
macro_rules! menu_button { ) -> crate::widget::Button<'a, Message> {
($($x:expr),+ $(,)?) => ( widget::button(
widget::button( widget::Row::with_children(children)
widget::Row::with_children(vec![$($x.into()),+])
.align_items(Alignment::Center) .align_items(Alignment::Center)
.height(Length::Fill) .height(Length::Fill)
.width(Length::Fill), .width(Length::Fill),
) )
.height(Length::Fixed(36.0)) .height(Length::Fixed(36.0))
.padding([4, 16]) .padding([4, 16])
.width(Length::Fill) .width(Length::Fill)
.style(theme::Button::MenuItem) .style(theme::Button::MenuItem)
);
} }
/// Represents a menu item that performs an action when selected or a separator between menu items. /// Represents a menu item that performs an action when selected or a separator between menu items.
@ -225,7 +223,7 @@ where
Element<'a, Message, crate::Theme, Renderer>: From<widget::button::Button<'a, Message>>, Element<'a, Message, crate::Theme, Renderer>: From<widget::button::Button<'a, Message>>,
{ {
fn find_key<A: MenuAction>(action: &A, key_binds: &HashMap<KeyBind, A>) -> String { fn find_key<A: MenuAction>(action: &A, key_binds: &HashMap<KeyBind, A>) -> String {
for (key_bind, key_action) in key_binds.iter() { for (key_bind, key_action) in key_binds {
if action == key_action { if action == key_action {
return key_bind.to_string(); return key_bind.to_string();
} }
@ -243,41 +241,48 @@ where
match item { match item {
MenuItem::Button(label, action) => { MenuItem::Button(label, action) => {
let key = find_key(&action, key_binds); let key = find_key(&action, key_binds);
let menu_button = menu_button!( let menu_button = menu_button(vec![
widget::text(label), widget::text(label).into(),
widget::horizontal_space(Length::Fill), widget::horizontal_space(Length::Fill).into(),
widget::text(key), widget::text(key).into(),
) ])
.on_press(action.message(None)); .on_press(action.message());
trees.push(MenuTree::<Message, Renderer>::new(menu_button)); trees.push(MenuTree::<Message, Renderer>::new(menu_button));
} }
MenuItem::CheckBox(label, value, action) => { MenuItem::CheckBox(label, value, action) => {
let key = find_key(&action, &key_binds); let key = find_key(&action, key_binds);
trees.push(MenuTree::new( trees.push(MenuTree::new(
menu_button!( menu_button(vec![
if value { if value {
// TODO: add a object-select-symbolic icon, `Message: 'static` is required when using an icon widget. widget::icon::from_name("object-select-symbolic")
widget::container(widget::text("✓")) .size(16)
.icon()
.width(Length::Fixed(16.0))
.into()
} else { } else {
widget::container(widget::Space::with_width(Length::Fixed(16.0))) widget::Space::with_width(Length::Fixed(17.0)).into()
}, },
widget::Space::with_width(Length::Fixed(8.0)), widget::Space::with_width(Length::Fixed(8.0)).into(),
widget::text(label), widget::text(label)
widget::horizontal_space(Length::Fill), .horizontal_alignment(iced::alignment::Horizontal::Left)
widget::text(key) .into(),
) widget::horizontal_space(Length::Fill).into(),
.on_press(action.message(None)), widget::text(key).into(),
])
.on_press(action.message()),
)); ));
} }
MenuItem::Folder(label, children) => { MenuItem::Folder(label, children) => {
trees.push(MenuTree::<Message, Renderer>::with_children( trees.push(MenuTree::<Message, Renderer>::with_children(
menu_button!( menu_button(vec![
widget::text(label), widget::text(label).into(),
widget::horizontal_space(Length::Fill), widget::horizontal_space(Length::Fill).into(),
// TODO: add a pan-end-symbolic icon, `Message: 'static` is required when using an icon widget. widget::icon::from_name("pan-end-symbolic")
widget::text("▶"), .size(16)
), .icon()
.into(),
]),
menu_items(key_binds, children), menu_items(key_binds, children),
)); ));
} }

View file

@ -1,70 +1,160 @@
// Copyright 2022 System76 <info@system76.com> // Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
//! Cosmic-themed widget implementations. //! The COSMIC widget library
//!
//! This module contains a wide variety of widgets used throughout the COSMIC app ecosystem.
//!
//! # Overview
//!
//! Add widgets to your application view by calling the modules and functions below.
//! Widgets are constructed by chaining their property methods using a functional paradigm.
//! Modules may contain additional functions for constructing different variations of a widget.
//! Each module will typically have one widget with the same name as the module, which will be re-exported here.
//!
//! ```no_run,ignore
//! use cosmic::prelude::*;
//! use cosmic::{cosmic_theme, theme, widget};
//!
//! const REPOSITORY: &str = "https://github.com/pop-os/libcosmic";
//!
//! let cosmic_theme::Spacing { space_xxs, .. } = theme::active().cosmic().spacing;
//!
//! let link = widget::button::link(REPOSITORY)
//! .on_press(Message::LaunchUrl(REPOSITORY))
//! .padding(0);
//!
//! let content = widget::column()
//! .push(widget::icon::from_name("my-app-icon"))
//! .push(widget::text::title3("My App Name"))
//! .push(link)
//! .align_items(Alignment::Center)
//! .spacing(space_xxs);
//! ```
//!
//! Widgets may borrow data from your application struct, and should do so to avoid allocating.
//!
//! ```no_run,ignore
//! let text = widget::text::body(&self.cached_text);
//! ```
//!
//! Use the [`cosmic::Apply`](crate::Apply) trait to embed widgets into other widgets which accept them.
//!
//! ```no_run,ignore
//! let button = widget::icon::from_name("printer-symbolic")
//! .apply(widget::button::icon)
//! .on_press(Message::Print);
//! ```
// Re-exports from Iced // Re-exports from Iced
#[doc(inline)]
pub use iced::widget::{canvas, Canvas};
#[doc(inline)]
pub use iced::widget::{checkbox, Checkbox}; pub use iced::widget::{checkbox, Checkbox};
#[doc(inline)]
pub use iced::widget::{combo_box, ComboBox}; pub use iced::widget::{combo_box, ComboBox};
#[doc(inline)]
pub use iced::widget::{container, Container}; pub use iced::widget::{container, Container};
#[doc(inline)]
pub use iced::widget::{horizontal_space, space, vertical_space, Space}; pub use iced::widget::{horizontal_space, space, vertical_space, Space};
#[doc(inline)]
pub use iced::widget::{image, Image}; pub use iced::widget::{image, Image};
#[doc(inline)]
pub use iced::widget::{lazy, Lazy}; pub use iced::widget::{lazy, Lazy};
#[doc(inline)]
pub use iced::widget::{mouse_area, MouseArea}; pub use iced::widget::{mouse_area, MouseArea};
#[doc(inline)]
pub use iced::widget::{pane_grid, PaneGrid}; pub use iced::widget::{pane_grid, PaneGrid};
#[doc(inline)]
pub use iced::widget::{progress_bar, ProgressBar}; pub use iced::widget::{progress_bar, ProgressBar};
#[doc(inline)]
pub use iced::widget::{radio, Radio}; pub use iced::widget::{radio, Radio};
#[doc(inline)]
pub use iced::widget::{responsive, Responsive}; pub use iced::widget::{responsive, Responsive};
#[doc(inline)]
pub use iced::widget::{slider, vertical_slider, Slider, VerticalSlider}; pub use iced::widget::{slider, vertical_slider, Slider, VerticalSlider};
#[doc(inline)]
pub use iced::widget::{svg, Svg}; pub use iced::widget::{svg, Svg};
#[doc(inline)]
pub use iced::widget::{text_editor, TextEditor};
#[doc(inline)]
pub use iced_core::widget::{Id, Operation, Widget}; pub use iced_core::widget::{Id, Operation, Widget};
pub mod aspect_ratio; pub mod aspect_ratio;
pub mod button; pub mod button;
#[doc(inline)]
pub use button::{button, Button, IconButton, LinkButton, TextButton}; pub use button::{button, Button, IconButton, LinkButton, TextButton};
pub(crate) mod common; pub(crate) mod common;
pub mod calendar; pub mod calendar;
#[doc(inline)]
pub use calendar::{calendar, Calendar}; pub use calendar::{calendar, Calendar};
pub mod card; pub mod card;
#[doc(inline)]
pub use card::*; pub use card::*;
pub mod color_picker; pub mod color_picker;
#[doc(inline)]
pub use color_picker::{ColorPicker, ColorPickerModel}; pub use color_picker::{ColorPicker, ColorPickerModel};
pub mod context_drawer; pub mod context_drawer;
#[doc(inline)]
pub use context_drawer::{context_drawer, ContextDrawer}; pub use context_drawer::{context_drawer, ContextDrawer};
#[doc(inline)]
pub use column::{column, Column}; pub use column::{column, Column};
pub mod column { pub mod column {
//! A container which aligns its children in a column.
pub type Column<'a, Message> = iced::widget::Column<'a, Message, crate::Theme, crate::Renderer>; pub type Column<'a, Message> = iced::widget::Column<'a, Message, crate::Theme, crate::Renderer>;
#[must_use] #[must_use]
/// A container which aligns its children in a column.
pub fn column<'a, Message>() -> Column<'a, Message> { pub fn column<'a, Message>() -> Column<'a, Message> {
Column::new() Column::new()
} }
#[must_use] #[must_use]
/// A pre-allocated [`column`].
pub fn with_capacity<'a, Message>(capacity: usize) -> Column<'a, Message> { pub fn with_capacity<'a, Message>(capacity: usize) -> Column<'a, Message> {
Column::with_children(Vec::with_capacity(capacity)) Column::with_children(Vec::with_capacity(capacity))
} }
#[must_use] #[must_use]
/// A [`column`] that will be assigned a [`Vec`] of children.
pub fn with_children<Message>(children: Vec<crate::Element<Message>>) -> Column<Message> { pub fn with_children<Message>(children: Vec<crate::Element<Message>>) -> Column<Message> {
Column::with_children(children) Column::with_children(children)
} }
} }
pub mod layer_container; pub mod layer_container;
#[doc(inline)]
pub use layer_container::{layer_container, LayerContainer}; pub use layer_container::{layer_container, LayerContainer};
pub mod context_menu; pub mod context_menu;
#[doc(inline)]
pub use context_menu::{context_menu, ContextMenu}; pub use context_menu::{context_menu, ContextMenu};
pub mod dialog; pub mod dialog;
#[doc(inline)]
pub use dialog::{dialog, Dialog}; pub use dialog::{dialog, Dialog};
/// An element to distinguish a boundary between two elements. /// An element to distinguish a boundary between two elements.
@ -117,67 +207,93 @@ pub mod divider {
} }
pub mod dnd_destination; pub mod dnd_destination;
#[doc(inline)]
pub use dnd_destination::{dnd_destination, DndDestination}; pub use dnd_destination::{dnd_destination, DndDestination};
pub mod dnd_source; pub mod dnd_source;
#[doc(inline)]
pub use dnd_source::{dnd_source, DndSource}; pub use dnd_source::{dnd_source, DndSource};
pub mod dropdown; pub mod dropdown;
#[doc(inline)]
pub use dropdown::{dropdown, Dropdown}; pub use dropdown::{dropdown, Dropdown};
pub mod flex_row; pub mod flex_row;
#[doc(inline)]
pub use flex_row::{flex_row, FlexRow}; pub use flex_row::{flex_row, FlexRow};
pub mod grid; pub mod grid;
#[doc(inline)]
pub use grid::{grid, Grid}; pub use grid::{grid, Grid};
mod header_bar; mod header_bar;
#[doc(inline)]
pub use header_bar::{header_bar, HeaderBar}; pub use header_bar::{header_bar, HeaderBar};
pub mod icon; pub mod icon;
#[doc(inline)]
pub use icon::{icon, Icon}; pub use icon::{icon, Icon};
pub mod id_container;
#[doc(inline)]
pub use id_container::{id_container, IdContainer};
#[cfg(feature = "animated-image")] #[cfg(feature = "animated-image")]
pub mod frames; pub mod frames;
pub use taffy::JustifyContent;
pub mod list; pub mod list;
pub use list::*; #[doc(inline)]
pub use list::{list_column, ListColumn};
pub mod menu; pub mod menu;
pub mod nav_bar; pub mod nav_bar;
#[doc(inline)]
pub use nav_bar::{nav_bar, nav_bar_dnd}; pub use nav_bar::{nav_bar, nav_bar_dnd};
pub mod nav_bar_toggle; pub mod nav_bar_toggle;
#[doc(inline)]
pub use nav_bar_toggle::{nav_bar_toggle, NavBarToggle}; pub use nav_bar_toggle::{nav_bar_toggle, NavBarToggle};
pub mod popover; pub mod popover;
#[doc(inline)]
pub use popover::{popover, Popover}; pub use popover::{popover, Popover};
pub mod rectangle_tracker; pub mod rectangle_tracker;
#[doc(inline)]
pub use rectangle_tracker::{rectangle_tracker, RectangleTracker}; pub use rectangle_tracker::{rectangle_tracker, RectangleTracker};
#[doc(inline)]
pub use row::{row, Row}; pub use row::{row, Row};
pub mod row { pub mod row {
//! A container which aligns its children in a row.
pub type Row<'a, Message> = iced::widget::Row<'a, Message, crate::Theme, crate::Renderer>; pub type Row<'a, Message> = iced::widget::Row<'a, Message, crate::Theme, crate::Renderer>;
#[must_use] #[must_use]
/// A container which aligns its children in a row.
pub fn row<'a, Message>() -> Row<'a, Message> { pub fn row<'a, Message>() -> Row<'a, Message> {
Row::new() Row::new()
} }
#[must_use] #[must_use]
/// A pre-allocated [`row`].
pub fn with_capacity<'a, Message>(capacity: usize) -> Row<'a, Message> { pub fn with_capacity<'a, Message>(capacity: usize) -> Row<'a, Message> {
Row::with_children(Vec::with_capacity(capacity)) Row::with_children(Vec::with_capacity(capacity))
} }
#[must_use] #[must_use]
/// A [`row`] that will be assigned a [`Vec`] of children.
pub fn with_children<Message>(children: Vec<crate::Element<Message>>) -> Row<Message> { pub fn with_children<Message>(children: Vec<crate::Element<Message>>) -> Row<Message> {
Row::with_children(children) Row::with_children(children)
} }
} }
mod scrollable; mod scrollable;
#[doc(inline)]
pub use scrollable::*; pub use scrollable::*;
pub mod segmented_button; pub mod segmented_button;
@ -186,19 +302,26 @@ pub mod segmented_control;
pub mod settings; pub mod settings;
pub mod spin_button; pub mod spin_button;
#[doc(inline)]
pub use spin_button::{spin_button, SpinButton}; pub use spin_button::{spin_button, SpinButton};
pub mod tab_bar; pub mod tab_bar;
pub mod text; pub mod text;
#[doc(inline)]
pub use text::{text, Text}; pub use text::{text, Text};
pub mod text_input; pub mod text_input;
pub use text_input::*; #[doc(inline)]
pub use text_input::{
editable_input, inline_input, search_input, secure_input, text_input, TextInput,
};
mod toggler; mod toggler;
#[doc(inline)]
pub use toggler::toggler; pub use toggler::toggler;
#[doc(inline)]
pub use tooltip::{tooltip, Tooltip}; pub use tooltip::{tooltip, Tooltip};
pub mod tooltip { pub mod tooltip {
use crate::Element; use crate::Element;
@ -224,4 +347,5 @@ pub mod tooltip {
} }
pub mod warning; pub mod warning;
#[doc(inline)]
pub use warning::*; pub use warning::*;

View file

@ -42,13 +42,9 @@ impl<'a, Message: 'static + Clone> From<NavBarToggle<Message>> for Element<'a, M
}; };
widget::button::icon(icon) widget::button::icon(icon)
.padding([8, 16, 8, 16])
.on_press_maybe(nav_bar_toggle.on_toggle) .on_press_maybe(nav_bar_toggle.on_toggle)
.selected(nav_bar_toggle.selected) .selected(nav_bar_toggle.selected)
.style(nav_bar_toggle.style) .style(nav_bar_toggle.style)
.apply(widget::container)
.center_y()
.height(Length::Fill)
.into() .into()
} }
} }

View file

@ -103,8 +103,8 @@ where
/// .build() /// .build()
/// ``` /// ```
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
pub fn icon(mut self, icon: Icon) -> Self { pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
self.model.0.icon_set(self.id, icon); self.model.0.icon_set(self.id, icon.into());
self self
} }

View file

@ -89,8 +89,8 @@ where
/// model.insert().text("Item A").icon(IconSource::from("icon-a")); /// model.insert().text("Item A").icon(IconSource::from("icon-a"));
/// ``` /// ```
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)] #[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
pub fn icon(self, icon: Icon) -> Self { pub fn icon(self, icon: impl Into<Icon>) -> Self {
self.model.icon_set(self.id, icon); self.model.icon_set(self.id, icon.into());
self self
} }

View file

@ -4,11 +4,13 @@
use std::borrow::Cow; use std::borrow::Cow;
use crate::{ use crate::{
widget::{column, horizontal_space, row, text, Row}, widget::{column, container, flex_row, horizontal_space, row, text, FlexRow, Row},
Element, Element,
}; };
use derive_setters::Setters; use derive_setters::Setters;
use iced::alignment::Horizontal;
use iced_core::Length; use iced_core::Length;
use taffy::AlignContent;
/// A settings item aligned in a row /// A settings item aligned in a row
#[must_use] #[must_use]
@ -29,9 +31,33 @@ pub fn item<'a, Message: 'static>(
#[allow(clippy::module_name_repetitions)] #[allow(clippy::module_name_repetitions)]
pub fn item_row<Message>(children: Vec<Element<Message>>) -> Row<Message> { pub fn item_row<Message>(children: Vec<Element<Message>>) -> Row<Message> {
row::with_children(children) row::with_children(children)
.spacing(12)
.align_items(iced::Alignment::Center) .align_items(iced::Alignment::Center)
.padding([0, 18]) .padding([0, 18])
}
/// A settings item aligned in a flex row
#[allow(clippy::module_name_repetitions)]
pub fn flex_item<'a, Message: 'static>(
title: impl Into<Cow<'a, str>> + 'a,
widget: impl Into<Element<'a, Message>> + 'a,
) -> FlexRow<'a, Message> {
flex_item_row(vec![
text(title).width(Length::Fill).into(),
container(widget).into(),
])
}
/// A settings item aligned in a flex row
#[allow(clippy::module_name_repetitions)]
pub fn flex_item_row<Message>(children: Vec<Element<Message>>) -> FlexRow<Message> {
flex_row(children)
.padding([0, 18])
.spacing(12) .spacing(12)
.min_item_width(200.0)
.justify_items(iced::Alignment::Center)
.justify_content(AlignContent::SpaceBetween)
.width(Length::Fill)
} }
/// Creates a builder for an item, beginning with the title. /// Creates a builder for an item, beginning with the title.
@ -61,6 +87,15 @@ pub struct Item<'a, Message> {
impl<'a, Message: 'static> Item<'a, Message> { impl<'a, Message: 'static> Item<'a, Message> {
/// Assigns a control to the item. /// Assigns a control to the item.
pub fn control(self, widget: impl Into<Element<'a, Message>>) -> Row<'a, Message> { pub fn control(self, widget: impl Into<Element<'a, Message>>) -> Row<'a, Message> {
item_row(self.control_(widget))
}
/// Assigns a control which flexes.
pub fn flex_control(self, widget: impl Into<Element<'a, Message>>) -> FlexRow<'a, Message> {
flex_item_row(self.control_(widget))
}
fn control_(self, widget: impl Into<Element<'a, Message>>) -> Vec<Element<'a, Message>> {
let mut contents = Vec::with_capacity(4); let mut contents = Vec::with_capacity(4);
if let Some(icon) = self.icon { if let Some(icon) = self.icon {
@ -80,15 +115,14 @@ impl<'a, Message: 'static> Item<'a, Message> {
} }
contents.push(widget.into()); contents.push(widget.into());
contents
item_row(contents)
} }
pub fn toggler( pub fn toggler(
self, self,
is_checked: bool, is_checked: bool,
message: impl Fn(bool) -> Message + 'static, message: impl Fn(bool) -> Message + 'static,
) -> Row<'a, Message> { ) -> FlexRow<'a, Message> {
self.control(crate::widget::toggler(None, is_checked, message)) self.flex_control(crate::widget::toggler(None, is_checked, message))
} }
} }

View file

@ -4,7 +4,7 @@
pub mod item; pub mod item;
mod section; mod section;
pub use self::item::{item, item_row}; pub use self::item::{flex_item, flex_item_row, item, item_row};
pub use self::section::{view_section, Section}; pub use self::section::{view_section, Section};
use crate::widget::{column, Column}; use crate::widget::{column, Column};

View file

@ -1,6 +1,8 @@
// Copyright 2022 System76 <info@system76.com> // Copyright 2022 System76 <info@system76.com>
// SPDX-License-Identifier: MPL-2.0 // SPDX-License-Identifier: MPL-2.0
//! A control for incremental adjustments of a value.
mod model; mod model;
use std::borrow::Cow; use std::borrow::Cow;
@ -20,6 +22,7 @@ pub struct SpinButton<'a, Message> {
on_change: Box<dyn Fn(model::Message) -> Message + 'static>, on_change: Box<dyn Fn(model::Message) -> Message + 'static>,
} }
/// A control for incremental adjustments of a value.
pub fn spin_button<'a, Message: 'static>( pub fn spin_button<'a, Message: 'static>(
label: impl Into<Cow<'a, str>>, label: impl Into<Cow<'a, str>>,
on_change: impl Fn(model::Message) -> Message + 'static, on_change: impl Fn(model::Message) -> Message + 'static,

View file

@ -729,28 +729,30 @@ where
} }
} }
let index = tree.children.len() - 1; if tree.children.len() > 0 {
if let (Some(trailing_icon), Some(tree)) = let index = tree.children.len() - 1;
(self.trailing_icon.as_mut(), tree.children.get_mut(index)) if let (Some(trailing_icon), Some(tree)) =
{ (self.trailing_icon.as_mut(), tree.children.get_mut(index))
let children = text_layout.children(); {
trailing_icon_layout = Some(children.last().unwrap()); let children = text_layout.children();
trailing_icon_layout = Some(children.last().unwrap());
if let Some(trailing_layout) = trailing_icon_layout { if let Some(trailing_layout) = trailing_icon_layout {
if cursor_position.is_over(trailing_layout.bounds()) { if cursor_position.is_over(trailing_layout.bounds()) {
let res = trailing_icon.as_widget_mut().on_event( let res = trailing_icon.as_widget_mut().on_event(
tree, tree,
event.clone(), event.clone(),
trailing_layout, trailing_layout,
cursor_position, cursor_position,
renderer, renderer,
clipboard, clipboard,
shell, shell,
viewport, viewport,
); );
if res == event::Status::Captured { if res == event::Status::Captured {
return res; return res;
}
} }
} }
} }