Merge branch 'pop-os:master' into new-match-algo
This commit is contained in:
commit
01a3789a19
58 changed files with 1360 additions and 567 deletions
|
|
@ -31,7 +31,7 @@ desktop = ["process", "dep:freedesktop-desktop-entry", "dep:mime", "dep:shlex"]
|
|||
# Enables keycode serialization
|
||||
serde-keycode = ["iced_core/serde"]
|
||||
# Prevents multiple separate process instances.
|
||||
single-instance = ["dep:zbus", "serde", "ron"]
|
||||
single-instance = ["dep:zbus", "ron"]
|
||||
# smol async runtime
|
||||
smol = ["iced/smol", "zbus?/async-io"]
|
||||
tokio = [
|
||||
|
|
@ -80,7 +80,7 @@ mime = { version = "0.3.17", optional = true }
|
|||
nix = { version = "0.27", features = ["process"], optional = true }
|
||||
palette = "0.7.3"
|
||||
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"
|
||||
textdistance = "1.0.2"
|
||||
thiserror = "1.0.44"
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
//! Integrations for cosmic-config — the cosmic configuration system.
|
||||
|
||||
use notify::{
|
||||
event::{EventKind, ModifyKind},
|
||||
Watcher,
|
||||
|
|
|
|||
|
|
@ -6,7 +6,7 @@ use std::{
|
|||
num::NonZeroUsize,
|
||||
};
|
||||
|
||||
use super::{to_hex, OutputError};
|
||||
use super::{to_rgba, OutputError};
|
||||
|
||||
impl Theme {
|
||||
#[must_use]
|
||||
|
|
@ -24,95 +24,169 @@ impl Theme {
|
|||
..
|
||||
} = self;
|
||||
|
||||
let window_bg = to_hex(background.base);
|
||||
let window_fg = to_hex(background.on);
|
||||
let window_bg = to_rgba(background.base);
|
||||
let window_fg = to_rgba(background.on);
|
||||
|
||||
let view_bg = to_hex(primary.base);
|
||||
let view_fg = to_hex(primary.on);
|
||||
let view_bg = to_rgba(primary.base);
|
||||
let view_fg = to_rgba(primary.on);
|
||||
|
||||
let headerbar_bg = to_hex(background.base);
|
||||
let headerbar_fg = to_hex(background.on);
|
||||
let headerbar_border_color = to_hex(background.divider);
|
||||
let headerbar_bg = to_rgba(background.base);
|
||||
let headerbar_fg = to_rgba(background.on);
|
||||
let headerbar_border_color = to_rgba(background.divider);
|
||||
|
||||
let sidebar_bg = to_hex(primary.base);
|
||||
let sidebar_fg = to_hex(primary.on);
|
||||
let sidebar_shade = to_hex(if self.is_dark {
|
||||
let sidebar_bg = to_rgba(primary.base);
|
||||
let sidebar_fg = to_rgba(primary.on);
|
||||
let sidebar_shade = to_rgba(if self.is_dark {
|
||||
Rgba::new(0.0, 0.0, 0.0, 0.08)
|
||||
} else {
|
||||
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 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_fg = to_hex(secondary.on);
|
||||
let secondary_sidebar_shade = to_hex(if self.is_dark {
|
||||
let secondary_sidebar_bg = to_rgba(secondary.base);
|
||||
let secondary_sidebar_fg = to_rgba(secondary.on);
|
||||
let secondary_sidebar_shade = to_rgba(if self.is_dark {
|
||||
Rgba::new(0.0, 0.0, 0.0, 0.08)
|
||||
} else {
|
||||
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_fg = to_hex(background.component.on);
|
||||
let card_bg = to_rgba(background.component.base);
|
||||
let card_fg = to_rgba(background.component.on);
|
||||
|
||||
let thumbnail_bg = to_hex(background.component.base);
|
||||
let thumbnail_fg = to_hex(background.component.on);
|
||||
let thumbnail_bg = to_rgba(background.component.base);
|
||||
let thumbnail_fg = to_rgba(background.component.on);
|
||||
|
||||
let dialog_bg = to_hex(primary.base);
|
||||
let dialog_fg = to_hex(primary.on);
|
||||
let dialog_bg = to_rgba(primary.base);
|
||||
let dialog_fg = to_rgba(primary.on);
|
||||
|
||||
let popover_bg = to_hex(background.component.base);
|
||||
let popover_fg = to_hex(background.component.on);
|
||||
let popover_bg = to_rgba(background.component.base);
|
||||
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)
|
||||
} else {
|
||||
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;
|
||||
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#"
|
||||
@define-color window_bg_color #{window_bg};
|
||||
@define-color window_fg_color #{window_fg};
|
||||
@define-color window_bg_color {window_bg};
|
||||
@define-color window_fg_color {window_fg};
|
||||
|
||||
@define-color view_bg_color #{view_bg};
|
||||
@define-color view_fg_color #{view_fg};
|
||||
@define-color view_bg_color {view_bg};
|
||||
@define-color view_fg_color {view_fg};
|
||||
|
||||
@define-color headerbar_bg_color #{headerbar_bg};
|
||||
@define-color headerbar_fg_color #{headerbar_fg};
|
||||
@define-color headerbar_border_color_color #{headerbar_border_color};
|
||||
@define-color headerbar_backdrop_color #{headerbar_backdrop};
|
||||
@define-color headerbar_bg_color {headerbar_bg};
|
||||
@define-color headerbar_fg_color {headerbar_fg};
|
||||
@define-color headerbar_border_color_color {headerbar_border_color};
|
||||
@define-color headerbar_backdrop_color {headerbar_backdrop};
|
||||
|
||||
@define-color sidebar_bg_color #{sidebar_bg};
|
||||
@define-color sidebar_fg_color #{sidebar_fg};
|
||||
@define-color sidebar_shade_color #{sidebar_shade};
|
||||
@define-color sidebar_backdrop_color #{sidebar_backdrop};
|
||||
@define-color sidebar_bg_color {sidebar_bg};
|
||||
@define-color sidebar_fg_color {sidebar_fg};
|
||||
@define-color sidebar_shade_color {sidebar_shade};
|
||||
@define-color sidebar_backdrop_color {sidebar_backdrop};
|
||||
|
||||
@define-color secondary_sidebar_bg_color #{secondary_sidebar_bg};
|
||||
@define-color secondary_sidebar_fg_color #{secondary_sidebar_fg};
|
||||
@define-color secondary_sidebar_shade_color #{secondary_sidebar_shade};
|
||||
@define-color secondary_sidebar_backdrop_color #{secondary_sidebar_backdrop};
|
||||
@define-color secondary_sidebar_bg_color {secondary_sidebar_bg};
|
||||
@define-color secondary_sidebar_fg_color {secondary_sidebar_fg};
|
||||
@define-color secondary_sidebar_shade_color {secondary_sidebar_shade};
|
||||
@define-color secondary_sidebar_backdrop_color {secondary_sidebar_backdrop};
|
||||
|
||||
@define-color card_bg_color #{card_bg};
|
||||
@define-color card_fg_color #{card_fg};
|
||||
@define-color card_bg_color {card_bg};
|
||||
@define-color card_fg_color {card_fg};
|
||||
|
||||
@define-color thumbnail_bg_color #{thumbnail_bg};
|
||||
@define-color thumbnail_fg_color #{thumbnail_fg};
|
||||
@define-color thumbnail_bg_color {thumbnail_bg};
|
||||
@define-color thumbnail_fg_color {thumbnail_fg};
|
||||
|
||||
@define-color dialog_bg_color #{dialog_bg};
|
||||
@define-color dialog_fg_color #{dialog_fg};
|
||||
@define-color dialog_bg_color {dialog_bg};
|
||||
@define-color dialog_fg_color {dialog_fg};
|
||||
|
||||
@define-color popover_bg_color #{popover_bg};
|
||||
@define-color popover_fg_color #{popover_fg};
|
||||
@define-color popover_bg_color {popover_bg};
|
||||
@define-color popover_fg_color {popover_fg};
|
||||
|
||||
@define-color shade_color #{shade};
|
||||
@define-color scrollbar_outline_color #{scrollbar_outline};
|
||||
@define-color shade_color {shade};
|
||||
@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));
|
||||
|
|
@ -130,10 +204,10 @@ impl Theme {
|
|||
css.push_str(&color_css("purple", palette.ext_purple));
|
||||
let neutral_steps = steps(palette.neutral_5, NonZeroUsize::new(10).unwrap());
|
||||
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() {
|
||||
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
|
||||
}
|
||||
|
|
@ -232,13 +306,13 @@ impl Theme {
|
|||
fn component_gtk4_css(prefix: &str, c: &Component) -> String {
|
||||
format!(
|
||||
r#"
|
||||
@define-color {prefix}_color #{};
|
||||
@define-color {prefix}_bg_color #{};
|
||||
@define-color {prefix}_fg_color #{};
|
||||
@define-color {prefix}_color {};
|
||||
@define-color {prefix}_bg_color {};
|
||||
@define-color {prefix}_fg_color {};
|
||||
"#,
|
||||
to_hex(c.base),
|
||||
to_hex(c.base),
|
||||
to_hex(c.on),
|
||||
to_rgba(c.base),
|
||||
to_rgba(c.base),
|
||||
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_4: Srgba = oklch.darken(0.1).into_color();
|
||||
let c_5: Srgba = oklch.darken(0.2).into_color();
|
||||
let c_1 = to_hex(c_1);
|
||||
let c_2 = to_hex(c_2);
|
||||
let c_3 = to_hex(c_3);
|
||||
let c_4 = to_hex(c_4);
|
||||
let c_5 = to_hex(c_5);
|
||||
let c_1 = to_rgba(c_1);
|
||||
let c_2 = to_rgba(c_2);
|
||||
let c_3 = to_rgba(c_3);
|
||||
let c_4 = to_rgba(c_4);
|
||||
let c_5 = to_rgba(c_5);
|
||||
|
||||
format! {r#"
|
||||
@define-color {prefix}_1 #{c_1};
|
||||
@define-color {prefix}_2 #{c_2};
|
||||
@define-color {prefix}_3 #{c_3};
|
||||
@define-color {prefix}_4 #{c_4};
|
||||
@define-color {prefix}_5 #{c_5};
|
||||
@define-color {prefix}_1 {c_1};
|
||||
@define-color {prefix}_2 {c_2};
|
||||
@define-color {prefix}_3 {c_3};
|
||||
@define-color {prefix}_4 {c_4};
|
||||
@define-color {prefix}_5 {c_5};
|
||||
"#}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -42,5 +42,16 @@ impl Theme {
|
|||
|
||||
pub fn to_hex(c: Srgba) -> String {
|
||||
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
|
||||
|
|
@ -14,9 +14,7 @@ use cosmic::iced_core::{Length, Size};
|
|||
use cosmic::widget::menu::action::MenuAction;
|
||||
use cosmic::widget::menu::key_bind::KeyBind;
|
||||
use cosmic::widget::menu::key_bind::Modifier;
|
||||
use cosmic::widget::menu::menu_tree::{menu_items, menu_root, MenuItem};
|
||||
use cosmic::widget::menu::{ItemHeight, ItemWidth, MenuBar, MenuTree};
|
||||
use cosmic::widget::segmented_button::Entity;
|
||||
use cosmic::widget::menu::{self, ItemHeight, ItemWidth};
|
||||
use cosmic::{executor, Element};
|
||||
|
||||
/// Runs application with these settings
|
||||
|
|
@ -67,7 +65,7 @@ pub enum Action {
|
|||
|
||||
impl MenuAction for Action {
|
||||
type Message = Message;
|
||||
fn message(&self, _entity_opt: Option<Entity>) -> Self::Message {
|
||||
fn message(&self) -> Self::Message {
|
||||
match self {
|
||||
Action::WindowClose => Message::WindowClose,
|
||||
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> {
|
||||
MenuBar::new(vec![MenuTree::with_children(
|
||||
menu_root("File"),
|
||||
menu_items(
|
||||
menu::bar(vec![menu::Tree::with_children(
|
||||
menu::root("File"),
|
||||
menu::items(
|
||||
key_binds,
|
||||
vec![
|
||||
MenuItem::Button("New window", Action::WindowNew),
|
||||
MenuItem::Divider,
|
||||
MenuItem::Folder(
|
||||
menu::Item::Button("New window", Action::WindowNew),
|
||||
menu::Item::Divider,
|
||||
menu::Item::Folder(
|
||||
"View",
|
||||
vec![MenuItem::CheckBox(
|
||||
vec![menu::Item::CheckBox(
|
||||
"Hide content",
|
||||
config.hide_content,
|
||||
Action::ToggleHideContent,
|
||||
)],
|
||||
),
|
||||
MenuItem::Divider,
|
||||
MenuItem::Button("Quit", Action::WindowClose),
|
||||
menu::Item::Divider,
|
||||
menu::Item::Button("Quit", Action::WindowClose),
|
||||
],
|
||||
),
|
||||
)])
|
||||
|
|
|
|||
|
|
@ -139,8 +139,7 @@ impl cosmic::Application for App {
|
|||
|
||||
// Send this back to the application.
|
||||
Message::FileRead(url, contents)
|
||||
})
|
||||
.map(cosmic::app::message::app);
|
||||
});
|
||||
}
|
||||
|
||||
// Creates a new open dialog.
|
||||
|
|
@ -167,8 +166,7 @@ impl cosmic::Application for App {
|
|||
|
||||
Err(why) => Message::OpenError(Arc::new(why)),
|
||||
}
|
||||
})
|
||||
.map(cosmic::app::Message::App);
|
||||
});
|
||||
}
|
||||
|
||||
// Displays an error in the application's warning bar.
|
||||
|
|
|
|||
2
iced
2
iced
|
|
@ -1 +1 @@
|
|||
Subproject commit 7c58b8eda9885083e019a5ea14edf4652740e154
|
||||
Subproject commit 2dcae59258c9cee6862c7fb2193066a1cb1e30c2
|
||||
2
justfile
2
justfile
|
|
@ -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'
|
||||
|
||||
# Check for errors and linter warnings
|
||||
|
|
|
|||
|
|
@ -73,9 +73,6 @@ pub struct Core {
|
|||
/// Configured theme mode
|
||||
pub(super) system_theme_mode: ThemeMode,
|
||||
|
||||
/// Libcosmic toolkit configuration.
|
||||
pub(super) toolkit_config: CosmicTk,
|
||||
|
||||
pub(super) portal_is_dark: Option<bool>,
|
||||
|
||||
pub(super) portal_accent: Option<Srgba>,
|
||||
|
|
@ -107,7 +104,7 @@ impl Default for Core {
|
|||
active: true,
|
||||
context_id: crate::widget::nav_bar::Id::null(),
|
||||
toggled: true,
|
||||
toggled_condensed: true,
|
||||
toggled_condensed: false,
|
||||
},
|
||||
scale_factor: 1.0,
|
||||
title: HashMap::new(),
|
||||
|
|
@ -123,16 +120,6 @@ impl Default for Core {
|
|||
})
|
||||
})
|
||||
.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 {
|
||||
context_title: String::new(),
|
||||
header_title: String::new(),
|
||||
|
|
|
|||
|
|
@ -156,6 +156,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::too_many_lines)]
|
||||
fn subscription(&self) -> Subscription<Self::Message> {
|
||||
let window_events = listen_with(|event, _| {
|
||||
match event {
|
||||
|
|
@ -197,7 +198,7 @@ where
|
|||
self.app.subscription().map(super::Message::App),
|
||||
self.app
|
||||
.core()
|
||||
.watch_config::<crate::config::CosmicTk>(crate::config::toolkit::ID)
|
||||
.watch_config::<crate::config::CosmicTk>(crate::config::ID)
|
||||
.map(|update| {
|
||||
for why in update.errors {
|
||||
tracing::error!(?why, "cosmic toolkit config update error");
|
||||
|
|
@ -408,11 +409,9 @@ impl<T: Application> Cosmic<T> {
|
|||
}
|
||||
|
||||
Message::AppThemeChange(mut theme) => {
|
||||
// Apply last-known system theme if the system theme is preferred.
|
||||
if let ThemeType::System { theme: _, .. } = theme.theme_type {
|
||||
self.app.core_mut().theme_sub_counter += 1;
|
||||
|
||||
theme = self.app.core().system_theme.clone();
|
||||
let portal_accent = self.app.core().portal_accent;
|
||||
if let Some(a) = portal_accent {
|
||||
let t_inner = theme.cosmic();
|
||||
|
|
@ -429,6 +428,11 @@ impl<T: Application> Cosmic<T> {
|
|||
}
|
||||
|
||||
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());
|
||||
// Record the last-known system theme in event that the current theme is custom.
|
||||
self.app.core_mut().system_theme = theme.clone();
|
||||
|
|
@ -472,6 +476,9 @@ impl<T: Application> Cosmic<T> {
|
|||
};
|
||||
}
|
||||
Message::SystemThemeModeChange(keys, mode) => {
|
||||
if !keys.contains(&"is_dark") {
|
||||
return iced::Command::none();
|
||||
}
|
||||
if THEME.with(|t| match t.borrow().theme_type {
|
||||
ThemeType::System {
|
||||
theme: _,
|
||||
|
|
@ -619,7 +626,7 @@ impl<T: Application> Cosmic<T> {
|
|||
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) => {
|
||||
|
|
|
|||
143
src/app/mod.rs
143
src/app/mod.rs
|
|
@ -37,6 +37,12 @@ pub mod message {
|
|||
pub const fn none<M>() -> Message<M> {
|
||||
Message::None
|
||||
}
|
||||
|
||||
impl<M> From<M> for Message<M> {
|
||||
fn from(value: M) -> Self {
|
||||
Self::App(value)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub use self::command::Command;
|
||||
|
|
@ -45,7 +51,7 @@ pub use self::settings::Settings;
|
|||
use crate::config::CosmicTk;
|
||||
use crate::prelude::*;
|
||||
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 iced::Subscription;
|
||||
#[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 {
|
||||
crate::icon_theme::set_default(icon_theme);
|
||||
} 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| {
|
||||
|
|
@ -92,7 +98,11 @@ pub(crate) fn iced_settings<App: Application>(
|
|||
iced.default_font = settings.default_font;
|
||||
iced.default_text_size = iced::Pixels(settings.default_text_size);
|
||||
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());
|
||||
#[cfg(all(not(feature = "wayland"), target_os = "linux"))]
|
||||
{
|
||||
|
|
@ -149,40 +159,11 @@ pub(crate) fn iced_settings<App: Application>(
|
|||
///
|
||||
/// Returns error on application failure.
|
||||
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);
|
||||
|
||||
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")]
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct DbusActivationMessage<Action = String, Args = Vec<String>> {
|
||||
|
|
@ -429,7 +410,7 @@ where
|
|||
fn core_mut(&mut self) -> &mut Core;
|
||||
|
||||
/// 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`.
|
||||
fn context_drawer(&self) -> Option<Element<Self::Message>> {
|
||||
|
|
@ -506,28 +487,28 @@ where
|
|||
}
|
||||
|
||||
// Called when context drawer is toggled
|
||||
fn on_context_drawer(&mut self) -> iced::Command<Message<Self::Message>> {
|
||||
iced::Command::none()
|
||||
fn on_context_drawer(&mut self) -> Command<Self::Message> {
|
||||
Command::none()
|
||||
}
|
||||
|
||||
/// Called when the escape key is pressed.
|
||||
fn on_escape(&mut self) -> iced::Command<Message<Self::Message>> {
|
||||
iced::Command::none()
|
||||
fn on_escape(&mut self) -> Command<Self::Message> {
|
||||
Command::none()
|
||||
}
|
||||
|
||||
/// Called when a navigation item is selected.
|
||||
fn on_nav_select(&mut self, id: nav_bar::Id) -> iced::Command<Message<Self::Message>> {
|
||||
iced::Command::none()
|
||||
fn on_nav_select(&mut self, id: nav_bar::Id) -> Command<Self::Message> {
|
||||
Command::none()
|
||||
}
|
||||
|
||||
/// 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>> {
|
||||
iced::Command::none()
|
||||
fn on_nav_context(&mut self, id: nav_bar::Id) -> Command<Self::Message> {
|
||||
Command::none()
|
||||
}
|
||||
|
||||
/// Called when the search function is requested.
|
||||
fn on_search(&mut self) -> iced::Command<Message<Self::Message>> {
|
||||
iced::Command::none()
|
||||
fn on_search(&mut self) -> Command<Self::Message> {
|
||||
Command::none()
|
||||
}
|
||||
|
||||
/// Called when a window is resized.
|
||||
|
|
@ -539,8 +520,8 @@ where
|
|||
}
|
||||
|
||||
/// Respond to an application-specific message.
|
||||
fn update(&mut self, message: Self::Message) -> iced::Command<Message<Self::Message>> {
|
||||
iced::Command::none()
|
||||
fn update(&mut self, message: Self::Message) -> Command<Self::Message> {
|
||||
Command::none()
|
||||
}
|
||||
|
||||
/// Respond to a system theme change
|
||||
|
|
@ -548,8 +529,8 @@ where
|
|||
&mut self,
|
||||
keys: &[&'static str],
|
||||
new_theme: &cosmic_theme::Theme,
|
||||
) -> iced::Command<Message<Self::Message>> {
|
||||
iced::Command::none()
|
||||
) -> Command<Self::Message> {
|
||||
Command::none()
|
||||
}
|
||||
|
||||
/// Respond to a system theme mode change
|
||||
|
|
@ -557,8 +538,8 @@ where
|
|||
&mut self,
|
||||
keys: &[&'static str],
|
||||
new_theme: &cosmic_theme::ThemeMode,
|
||||
) -> iced::Command<Message<Self::Message>> {
|
||||
iced::Command::none()
|
||||
) -> Command<Self::Message> {
|
||||
Command::none()
|
||||
}
|
||||
|
||||
/// Constructs the view for the main window.
|
||||
|
|
@ -576,24 +557,21 @@ where
|
|||
|
||||
/// Handles dbus activation messages
|
||||
#[cfg(feature = "single-instance")]
|
||||
fn dbus_activation(
|
||||
&mut self,
|
||||
msg: DbusActivationMessage,
|
||||
) -> iced::Command<Message<Self::Message>> {
|
||||
iced::Command::none()
|
||||
fn dbus_activation(&mut self, msg: DbusActivationMessage) -> Command<Self::Message> {
|
||||
Command::none()
|
||||
}
|
||||
}
|
||||
|
||||
/// Methods automatically derived for all types implementing [`Application`].
|
||||
pub trait ApplicationExt: Application {
|
||||
/// Initiates a window drag.
|
||||
fn drag(&mut self) -> iced::Command<Message<Self::Message>>;
|
||||
fn drag(&mut self) -> Command<Self::Message>;
|
||||
|
||||
/// Maximizes the window.
|
||||
fn maximize(&mut self) -> iced::Command<Message<Self::Message>>;
|
||||
fn maximize(&mut self) -> Command<Self::Message>;
|
||||
|
||||
/// 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.
|
||||
|
||||
#[cfg(not(any(feature = "multi-window", feature = "wayland")))]
|
||||
|
|
@ -615,30 +593,26 @@ pub trait ApplicationExt: Application {
|
|||
|
||||
#[cfg(not(any(feature = "multi-window", feature = "wayland")))]
|
||||
/// 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"))]
|
||||
/// Set the title of a window.
|
||||
fn set_window_title(
|
||||
&mut self,
|
||||
title: String,
|
||||
id: window::Id,
|
||||
) -> iced::Command<Message<Self::Message>>;
|
||||
fn set_window_title(&mut self, title: String, id: window::Id) -> Command<Self::Message>;
|
||||
|
||||
/// View template for the main window.
|
||||
fn view_main(&self) -> Element<Message<Self::Message>>;
|
||||
}
|
||||
|
||||
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()))
|
||||
}
|
||||
|
||||
fn maximize(&mut self) -> iced::Command<Message<Self::Message>> {
|
||||
fn maximize(&mut self) -> Command<Self::Message> {
|
||||
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()))
|
||||
}
|
||||
|
||||
|
|
@ -656,23 +630,20 @@ impl<App: Application> ApplicationExt for App {
|
|||
}
|
||||
|
||||
#[cfg(any(feature = "multi-window", feature = "wayland"))]
|
||||
fn set_window_title(
|
||||
&mut self,
|
||||
title: String,
|
||||
id: window::Id,
|
||||
) -> iced::Command<Message<Self::Message>> {
|
||||
fn set_window_title(&mut self, title: String, id: window::Id) -> Command<Self::Message> {
|
||||
self.core_mut().title.insert(id, title.clone());
|
||||
command::set_title(Some(id), title)
|
||||
}
|
||||
|
||||
#[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();
|
||||
|
||||
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.
|
||||
fn view_main(&self) -> Element<Message<Self::Message>> {
|
||||
let core = self.core();
|
||||
|
|
@ -685,8 +656,11 @@ impl<App: Application> ApplicationExt for App {
|
|||
let mut widgets = Vec::with_capacity(2);
|
||||
|
||||
// Insert nav bar onto the left side of the window.
|
||||
if let Some(nav) = self.nav_bar() {
|
||||
widgets.push(nav);
|
||||
if let Some(nav) = self
|
||||
.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() {
|
||||
|
|
@ -699,7 +673,12 @@ impl<App: Application> ApplicationExt for App {
|
|||
main_content,
|
||||
context.map(Message::App),
|
||||
)
|
||||
.into()
|
||||
.apply(|drawer| {
|
||||
Element::from(id_container(
|
||||
drawer,
|
||||
iced_core::id::Id::new("COSMIC_context_drawer"),
|
||||
))
|
||||
})
|
||||
} else {
|
||||
main_content
|
||||
});
|
||||
|
|
@ -715,6 +694,7 @@ impl<App: Application> ApplicationExt for App {
|
|||
.width(iced::Length::Fill)
|
||||
.height(iced::Length::Fill)
|
||||
.style(crate::theme::Container::WindowBackground)
|
||||
.apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container")))
|
||||
.into()
|
||||
} else {
|
||||
content_row.into()
|
||||
|
|
@ -744,11 +724,11 @@ impl<App: Application> ApplicationExt for App {
|
|||
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));
|
||||
}
|
||||
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
@ -764,7 +744,7 @@ impl<App: Application> ApplicationExt for App {
|
|||
header = header.end(element.map(Message::App));
|
||||
}
|
||||
|
||||
header
|
||||
header.apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_header")))
|
||||
})
|
||||
} else {
|
||||
None
|
||||
|
|
@ -775,7 +755,10 @@ impl<App: Application> ApplicationExt for App {
|
|||
// 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
|
||||
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));
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -372,7 +372,7 @@ pub fn style() -> <crate::Theme as iced_style::application::StyleSheet>::Style {
|
|||
pub fn menu_button<'a, Message>(
|
||||
content: impl Into<Element<'a, Message>>,
|
||||
) -> crate::widget::Button<'a, Message> {
|
||||
crate::widget::Button::new(content)
|
||||
crate::widget::button(content)
|
||||
.style(Button::AppletMenu)
|
||||
.padding(menu_control_padding())
|
||||
.width(Length::Fill)
|
||||
|
|
|
|||
|
|
@ -16,18 +16,20 @@ use iced_runtime::command::Action;
|
|||
use std::future::Future;
|
||||
|
||||
/// Yields a command which contains a batch of commands.
|
||||
pub fn batch<M>(commands: impl IntoIterator<Item = Command<M>>) -> Command<M> {
|
||||
Command::batch(commands)
|
||||
pub fn batch<X: 'static + Into<Y>, Y: 'static>(
|
||||
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.
|
||||
pub fn future<M>(future: impl Future<Output = M> + Send + 'static) -> Command<M> {
|
||||
Command::single(Action::Future(Box::pin(future)))
|
||||
pub fn future<X: Into<Y>, Y>(future: impl Future<Output = X> + Send + 'static) -> Command<Y> {
|
||||
Command::single(Action::Future(Box::pin(async move { future.await.into() })))
|
||||
}
|
||||
|
||||
/// Yields a command which will return a message.
|
||||
pub fn message<M: Send + 'static>(message: M) -> Command<M> {
|
||||
future(async move { message })
|
||||
pub fn message<X: Send + 'static + Into<Y>, Y>(message: X) -> Command<Y> {
|
||||
future(async move { message.into() })
|
||||
}
|
||||
|
||||
/// Initiates a window drag.
|
||||
|
|
|
|||
|
|
@ -3,5 +3,102 @@
|
|||
|
||||
//! Configurations available to libcosmic applications.
|
||||
|
||||
pub mod toolkit;
|
||||
pub use toolkit::CosmicTk;
|
||||
use cosmic_config::cosmic_config_derive::CosmicConfigEntry;
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
}
|
||||
}
|
||||
21
src/lib.rs
21
src/lib.rs
|
|
@ -29,7 +29,10 @@ pub mod command;
|
|||
|
||||
pub mod config;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use cosmic_config;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use cosmic_theme;
|
||||
|
||||
#[cfg(any(feature = "xdg-portal", feature = "rfd"))]
|
||||
|
|
@ -43,15 +46,31 @@ mod ext;
|
|||
|
||||
pub mod font;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced_core;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced_futures;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced_renderer;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced_runtime;
|
||||
|
||||
#[cfg(feature = "wayland")]
|
||||
pub use iced_sctk;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced_style;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced_widget;
|
||||
|
||||
#[doc(inline)]
|
||||
#[cfg(feature = "winit")]
|
||||
pub use iced_winit;
|
||||
|
||||
|
|
@ -67,6 +86,8 @@ pub mod process;
|
|||
pub use cctk;
|
||||
|
||||
pub mod theme;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use theme::{style, Theme};
|
||||
|
||||
pub mod widget;
|
||||
|
|
|
|||
|
|
@ -1,7 +1,6 @@
|
|||
use ashpd::desktop::settings::{ColorScheme, Contrast};
|
||||
use ashpd::desktop::Color;
|
||||
use iced::futures::{self, select, FutureExt, SinkExt, StreamExt};
|
||||
use iced::Subscription;
|
||||
use iced_futures::subscription;
|
||||
use tracing::error;
|
||||
|
||||
|
|
@ -15,68 +14,83 @@ pub enum Desktop {
|
|||
pub fn desktop_settings() -> iced_futures::Subscription<Desktop> {
|
||||
subscription::channel(std::any::TypeId::of::<Desktop>(), 10, |mut tx| {
|
||||
async move {
|
||||
let Ok(settings) = ashpd::desktop::settings::Settings::new().await else {
|
||||
// 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");
|
||||
}
|
||||
|
||||
let mut attempts = 0;
|
||||
loop {
|
||||
let next_color_scheme = async {
|
||||
if let Some(s) = color_scheme_stream.as_mut() {
|
||||
return s.next().await;
|
||||
let Ok(settings) = ashpd::desktop::settings::Settings::new().await else {
|
||||
error!("Failed to create the settings proxy");
|
||||
#[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 {
|
||||
if let Some(s) = contrast_stream.as_mut() {
|
||||
return s.next().await;
|
||||
match settings.color_scheme().await {
|
||||
Ok(color_scheme) => {
|
||||
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! {
|
||||
s = next_color_scheme.fuse() => {
|
||||
if let Some(s) = s {
|
||||
_ = tx.send(Desktop::ColorScheme(s)).await;
|
||||
} else {
|
||||
color_scheme_stream = None;
|
||||
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 {
|
||||
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() => {
|
||||
if let Some(c) = c {
|
||||
_ = tx.send(Desktop::Contrast(c)).await;
|
||||
} else {
|
||||
contrast_stream = None;
|
||||
let next_contrast = async {
|
||||
if let Some(s) = contrast_stream.as_mut() {
|
||||
return s.next().await;
|
||||
}
|
||||
}
|
||||
};
|
||||
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;
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
|
|
|
|||
|
|
@ -120,8 +120,8 @@ pub fn appearance(
|
|||
}
|
||||
Button::MenuRoot => {
|
||||
appearance.background = None;
|
||||
appearance.icon_color = Some(cosmic.accent.base.into());
|
||||
appearance.text_color = Some(cosmic.accent.base.into());
|
||||
appearance.icon_color = None;
|
||||
appearance.text_color = None;
|
||||
}
|
||||
Button::MenuItem => {
|
||||
let (background, _, _) = color(&cosmic.background.component);
|
||||
|
|
|
|||
|
|
@ -452,8 +452,8 @@ impl container::StyleSheet for Theme {
|
|||
radius: [
|
||||
cosmic.corner_radii.radius_0[0],
|
||||
cosmic.corner_radii.radius_0[1],
|
||||
cosmic.corner_radii.radius_xs[2],
|
||||
cosmic.corner_radii.radius_xs[3],
|
||||
cosmic.corner_radii.radius_s[2],
|
||||
cosmic.corner_radii.radius_s[3],
|
||||
]
|
||||
.into(),
|
||||
..Default::default()
|
||||
|
|
@ -494,8 +494,8 @@ impl container::StyleSheet for Theme {
|
|||
background: Some(iced::Background::Color(cosmic.background.base.into())),
|
||||
border: Border {
|
||||
radius: [
|
||||
cosmic.corner_radii.radius_xs[0],
|
||||
cosmic.corner_radii.radius_xs[1],
|
||||
cosmic.corner_radii.radius_s[0],
|
||||
cosmic.corner_radii.radius_s[1],
|
||||
cosmic.corner_radii.radius_0[2],
|
||||
cosmic.corner_radii.radius_0[3],
|
||||
]
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@ pub struct Appearance {
|
|||
pub border_color: Color,
|
||||
/// The expand value of the menus' background
|
||||
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,
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -9,18 +9,27 @@ pub use self::button::Button;
|
|||
mod dropdown;
|
||||
|
||||
pub mod iced;
|
||||
#[doc(inline)]
|
||||
pub use self::iced::Application;
|
||||
#[doc(inline)]
|
||||
pub use self::iced::Checkbox;
|
||||
#[doc(inline)]
|
||||
pub use self::iced::Container;
|
||||
#[doc(inline)]
|
||||
pub use self::iced::ProgressBar;
|
||||
#[doc(inline)]
|
||||
pub use self::iced::Rule;
|
||||
#[doc(inline)]
|
||||
pub use self::iced::Svg;
|
||||
#[doc(inline)]
|
||||
pub use self::iced::Text;
|
||||
|
||||
pub mod menu_bar;
|
||||
|
||||
mod segmented_button;
|
||||
#[doc(inline)]
|
||||
pub use self::segmented_button::SegmentedButton;
|
||||
|
||||
mod text_input;
|
||||
#[doc(inline)]
|
||||
pub use self::text_input::TextInput;
|
||||
|
|
|
|||
|
|
@ -1,3 +1,5 @@
|
|||
//! A container which constraints itself to a specific aspect ratio.
|
||||
|
||||
use iced::widget::Container;
|
||||
use iced::Size;
|
||||
use iced_core::alignment;
|
||||
|
|
@ -21,9 +23,7 @@ where
|
|||
AspectRatio::new(content, ratio)
|
||||
}
|
||||
|
||||
/// An element decorating some content.
|
||||
///
|
||||
/// It is normally used for alignment purposes.
|
||||
/// A container which constraints itself to a specific aspect ratio.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct AspectRatio<'a, Message, Renderer>
|
||||
where
|
||||
|
|
|
|||
|
|
@ -13,12 +13,14 @@ use std::borrow::Cow;
|
|||
|
||||
pub type Button<'a, Message> = Builder<'a, Message, Icon>;
|
||||
|
||||
/// The icon variant of a button.
|
||||
pub struct Icon {
|
||||
handle: Handle,
|
||||
vertical: 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> {
|
||||
Button::new(Icon {
|
||||
handle: handle.into(),
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ use std::borrow::Cow;
|
|||
|
||||
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> {
|
||||
Button::new(Image {
|
||||
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> {
|
||||
image: widget::Image<'a, Handle>,
|
||||
selected: bool,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//! Hyperlink button widget
|
||||
|
||||
use super::Builder;
|
||||
use super::Style;
|
||||
use crate::prelude::*;
|
||||
|
|
@ -17,6 +19,7 @@ pub struct Hyperlink {
|
|||
trailing_icon: bool,
|
||||
}
|
||||
|
||||
/// A hyperlink button.
|
||||
pub fn link<'a, Message>(label: impl Into<Cow<'a, str>> + 'static) -> Button<'a, Message> {
|
||||
Button::new(
|
||||
label,
|
||||
|
|
|
|||
|
|
@ -1,29 +1,41 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//! Button widgets for COSMIC applications.
|
||||
|
||||
pub use crate::theme::Button as Style;
|
||||
|
||||
pub mod link;
|
||||
use derive_setters::Setters;
|
||||
#[doc(inline)]
|
||||
pub use link::link;
|
||||
#[doc(inline)]
|
||||
pub use link::Button as LinkButton;
|
||||
|
||||
mod icon;
|
||||
#[doc(inline)]
|
||||
pub use icon::icon;
|
||||
#[doc(inline)]
|
||||
pub use icon::Button as IconButton;
|
||||
|
||||
mod image;
|
||||
#[doc(inline)]
|
||||
pub use image::image;
|
||||
#[doc(inline)]
|
||||
pub use image::Button as ImageButton;
|
||||
|
||||
mod style;
|
||||
#[doc(inline)]
|
||||
pub use style::{Appearance, StyleSheet};
|
||||
|
||||
mod text;
|
||||
#[doc(inline)]
|
||||
pub use text::Button as TextButton;
|
||||
#[doc(inline)]
|
||||
pub use text::{destructive, standard, suggested, text};
|
||||
|
||||
mod widget;
|
||||
#[doc(inline)]
|
||||
pub use widget::{draw, focus, layout, mouse_interaction, Button};
|
||||
|
||||
use iced_core::font::Weight;
|
||||
|
|
@ -31,10 +43,12 @@ use iced_core::widget::Id;
|
|||
use iced_core::{Length, Padding};
|
||||
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> {
|
||||
Button::new(content)
|
||||
}
|
||||
|
||||
/// An image button which may contain any widget as its content.
|
||||
pub fn custom_image_button<'a, Message>(
|
||||
content: impl Into<crate::Element<'a, Message>>,
|
||||
on_remove: Option<Message>,
|
||||
|
|
@ -42,6 +56,7 @@ pub fn custom_image_button<'a, Message>(
|
|||
Button::new_image(content, on_remove)
|
||||
}
|
||||
|
||||
/// A builder for constructing a custom [`Button`].
|
||||
#[must_use]
|
||||
#[derive(Setters)]
|
||||
pub struct Builder<'a, Message, Variant> {
|
||||
|
|
|
|||
|
|
@ -10,26 +10,31 @@ use std::borrow::Cow;
|
|||
|
||||
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> {
|
||||
Button::new(Text::new())
|
||||
.label(label)
|
||||
.style(Style::Destructive)
|
||||
}
|
||||
|
||||
/// A text button with the suggested style
|
||||
pub fn suggested<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> {
|
||||
Button::new(Text::new())
|
||||
.label(label)
|
||||
.style(Style::Suggested)
|
||||
}
|
||||
|
||||
/// A text button with the standard style
|
||||
pub fn standard<'a, Message>(label: impl Into<Cow<'a, str>>) -> Button<'a, Message> {
|
||||
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> {
|
||||
Button::new(Text::new()).label(label).style(Style::Text)
|
||||
}
|
||||
|
||||
/// The text variant of a button.
|
||||
pub struct Text {
|
||||
pub(super) leading_icon: Option<icon::Handle>,
|
||||
pub(super) trailing_icon: Option<icon::Handle>,
|
||||
|
|
|
|||
|
|
@ -59,7 +59,7 @@ pub struct Button<'a, Message> {
|
|||
|
||||
impl<'a, Message> Button<'a, Message> {
|
||||
/// 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 {
|
||||
id: Id::unique(),
|
||||
#[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 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 {
|
||||
icon_color.a = alpha;
|
||||
|
|
@ -403,77 +409,105 @@ impl<'a, Message: 'a + Clone> Widget<Message, crate::Theme, crate::Renderer>
|
|||
on_remove,
|
||||
} = &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 {
|
||||
renderer.fill_quad(
|
||||
Quad {
|
||||
bounds: Rectangle {
|
||||
width: 24.0,
|
||||
height: 20.0,
|
||||
x: bounds.x + styling.border_width,
|
||||
y: bounds.y + (bounds.height - 20.0 - styling.border_width),
|
||||
let c_rad = THEME.with(|t| t.borrow().cosmic().corner_radii);
|
||||
|
||||
// NOTE: Workaround to round the border of the unselected, unhovered image.
|
||||
if !self.selected && !is_mouse_over {
|
||||
let mut bounds = bounds;
|
||||
bounds.x -= 2.0;
|
||||
bounds.y -= 2.0;
|
||||
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 {
|
||||
radius: [
|
||||
c_rad.radius_0[0],
|
||||
c_rad.radius_s[1],
|
||||
c_rad.radius_0[2],
|
||||
c_rad.radius_s[3],
|
||||
]
|
||||
.into(),
|
||||
..Default::default()
|
||||
Color::TRANSPARENT,
|
||||
);
|
||||
}
|
||||
|
||||
if self.selected {
|
||||
renderer.fill_quad(
|
||||
Quad {
|
||||
bounds: Rectangle {
|
||||
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(
|
||||
renderer,
|
||||
crate::widget::common::object_select().clone(),
|
||||
Some(icon_color),
|
||||
Rectangle {
|
||||
width: 16.0,
|
||||
height: 16.0,
|
||||
x: bounds.x + 5.0 + styling.border_width,
|
||||
y: bounds.y + (bounds.height - 18.0 - styling.border_width),
|
||||
},
|
||||
);
|
||||
}
|
||||
iced_core::svg::Renderer::draw(
|
||||
renderer,
|
||||
crate::widget::common::object_select().clone(),
|
||||
Some(icon_color),
|
||||
Rectangle {
|
||||
width: 16.0,
|
||||
height: 16.0,
|
||||
x: bounds.x + 5.0 + styling.border_width,
|
||||
y: bounds.y + (bounds.height - 18.0 - styling.border_width),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
if on_remove.is_some() {
|
||||
if let Some(position) = cursor.position() {
|
||||
if bounds.contains(position) {
|
||||
let bounds = removal_bounds(layout.bounds(), 4.0);
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
shadow: Shadow::default(),
|
||||
border: Border {
|
||||
radius: c_rad.radius_m.into(),
|
||||
..Default::default()
|
||||
if on_remove.is_some() {
|
||||
if let Some(position) = cursor.position() {
|
||||
if bounds.contains(position) {
|
||||
let bounds = removal_bounds(layout.bounds(), 4.0);
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
bounds,
|
||||
shadow: Shadow::default(),
|
||||
border: Border {
|
||||
radius: c_rad.radius_m.into(),
|
||||
..Default::default()
|
||||
},
|
||||
},
|
||||
},
|
||||
selection_background,
|
||||
);
|
||||
selection_background,
|
||||
);
|
||||
|
||||
iced_core::svg::Renderer::draw(
|
||||
renderer,
|
||||
close_icon.clone(),
|
||||
Some(icon_color),
|
||||
Rectangle {
|
||||
width: 16.0,
|
||||
height: 16.0,
|
||||
x: bounds.x + 4.0,
|
||||
y: bounds.y + 4.0,
|
||||
},
|
||||
);
|
||||
iced_core::svg::Renderer::draw(
|
||||
renderer,
|
||||
close_icon.clone(),
|
||||
Some(icon_color),
|
||||
Rectangle {
|
||||
width: 16.0,
|
||||
height: 16.0,
|
||||
x: bounds.x + 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
|
||||
.content
|
||||
.as_widget()
|
||||
.a11y_nodes(child_layout, &child_tree, p);
|
||||
.a11y_nodes(child_layout, child_tree, p);
|
||||
|
||||
let Rectangle {
|
||||
x,
|
||||
|
|
@ -775,19 +809,38 @@ pub fn draw<Renderer: iced_core::Renderer, Theme>(
|
|||
// Then draw the button contents onto the background.
|
||||
draw_contents(renderer, styling);
|
||||
|
||||
// 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,
|
||||
let mut clipped_bounds = bounds;
|
||||
clipped_bounds.height += styling.border_width;
|
||||
|
||||
renderer.with_layer(clipped_bounds, |renderer| {
|
||||
// NOTE: Workaround to round the border of the hovered/selected image.
|
||||
renderer.fill_quad(
|
||||
renderer::Quad {
|
||||
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 {
|
||||
draw_contents(renderer, styling);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright 2024 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//! A widget that displays an interactive calendar.
|
||||
|
||||
use std::cmp;
|
||||
|
||||
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 iced::alignment::{Horizontal, Vertical};
|
||||
|
||||
/// A widget that displays an interactive calendar.
|
||||
pub fn calendar<M>(
|
||||
selected: &NaiveDate,
|
||||
on_select: impl Fn(NaiveDate) -> M + 'static,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//! Widgets for selecting colors with a color picker.
|
||||
|
||||
use std::borrow::Cow;
|
||||
use std::rc::Rc;
|
||||
use std::sync::atomic::{AtomicBool, Ordering};
|
||||
|
|
@ -29,9 +31,12 @@ use palette::{FromColor, RgbHue};
|
|||
use super::button::StyleSheet;
|
||||
use super::divider::horizontal;
|
||||
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};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use ColorPickerModel as Model;
|
||||
|
||||
// TODO is this going to look correct enough?
|
||||
lazy_static! {
|
||||
pub static ref HSV_RAINBOW: Vec<ColorStop> = (0u16..8)
|
||||
|
|
@ -64,7 +69,7 @@ pub enum ColorPickerUpdate {
|
|||
#[derive(Setters)]
|
||||
pub struct ColorPickerModel {
|
||||
#[setters(skip)]
|
||||
segmented_model: Model<SingleSelect>,
|
||||
segmented_model: segmented_button::Model<SingleSelect>,
|
||||
#[setters(skip)]
|
||||
active_color: palette::Hsv,
|
||||
#[setters(skip)]
|
||||
|
|
@ -246,7 +251,7 @@ impl ColorPickerModel {
|
|||
#[derive(Setters, Clone)]
|
||||
pub struct ColorPickerBuilder<'a, Message> {
|
||||
#[setters(skip)]
|
||||
model: &'a Model<SingleSelect>,
|
||||
model: &'a segmented_button::Model<SingleSelect>,
|
||||
#[setters(skip)]
|
||||
active_color: palette::Hsv,
|
||||
#[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>(
|
||||
on_press: Option<Message>,
|
||||
color: Option<Color>,
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//! An overlayed widget that attaches a toggleable context drawer to the view.
|
||||
|
||||
mod overlay;
|
||||
|
||||
mod widget;
|
||||
|
|
@ -8,6 +10,7 @@ pub use widget::ContextDrawer;
|
|||
|
||||
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>(
|
||||
header: &'a str,
|
||||
on_close: Message,
|
||||
|
|
|
|||
|
|
@ -91,7 +91,7 @@ impl<'a, Message: Clone> Widget<Message, crate::Theme, crate::Renderer>
|
|||
}
|
||||
|
||||
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 {
|
||||
// for (menu, tree) in context_menus
|
||||
|
|
|
|||
|
|
@ -253,7 +253,7 @@ impl<'a, Message: 'static> Widget<Message, crate::Theme, crate::Renderer>
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -132,7 +132,7 @@ impl<
|
|||
}
|
||||
|
||||
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 {
|
||||
|
|
|
|||
|
|
@ -2,6 +2,8 @@
|
|||
// Copyright 2019 Héctor Ramón, Iced contributors
|
||||
// SPDX-License-Identifier: MPL-2.0 AND MIT
|
||||
|
||||
//! Displays a list of options in a popover menu on select.
|
||||
|
||||
pub mod menu;
|
||||
pub use menu::Menu;
|
||||
|
||||
|
|
@ -10,6 +12,7 @@ pub mod multi;
|
|||
mod widget;
|
||||
pub use widget::*;
|
||||
|
||||
/// Displays a list of options in a popover menu on select.
|
||||
pub fn dropdown<'a, S: AsRef<str>, Message: 'a>(
|
||||
selections: &'a [S],
|
||||
selected: Option<usize>,
|
||||
|
|
|
|||
|
|
@ -4,8 +4,14 @@
|
|||
use crate::{Element, Renderer};
|
||||
use iced_core::layout::{Limits, Node};
|
||||
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>(
|
||||
renderer: &Renderer,
|
||||
limits: &Limits,
|
||||
|
|
@ -13,79 +19,153 @@ pub fn resolve<Message>(
|
|||
padding: Padding,
|
||||
column_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],
|
||||
) -> 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 max_flex_width = limits.max().width;
|
||||
let mut flex_width = 0.0f32;
|
||||
let mut flex_height = 0.0f32;
|
||||
let mut taffy_tree = TaffyTree::<()>::with_capacity(items.len() + 1);
|
||||
|
||||
let mut current_row_width = 0.0f32;
|
||||
let mut current_row_height = 0.0f32;
|
||||
let style = taffy::Style {
|
||||
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()) {
|
||||
// Calculate the dimensions of the item.
|
||||
let child_node = child.as_widget().layout(tree, renderer, &limits);
|
||||
let child_widget = child.as_widget();
|
||||
let child_node = child_widget.layout(tree, renderer, limits);
|
||||
let size = child_node.size();
|
||||
|
||||
// Calculate the required additional width to fit the item into the current row.
|
||||
let mut required_width = size.width
|
||||
+ if row_buffer.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
row_spacing
|
||||
nodes.push(child_node);
|
||||
|
||||
let c_size = child_widget.size();
|
||||
let (width, flex_grow, justify_self) = match c_size.width {
|
||||
Length::Fill | Length::FillPortion(_) => {
|
||||
(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.
|
||||
if current_row_width + required_width > max_flex_width {
|
||||
if flex_height != 0.0f32 {
|
||||
flex_height += column_spacing;
|
||||
let child_widget = child.as_widget();
|
||||
let c_size = child_widget.size();
|
||||
match c_size.width {
|
||||
Length::Fill | Length::FillPortion(_) => {
|
||||
*node =
|
||||
child_widget.layout(tree, renderer, &limits.width(leaf_layout.size.width));
|
||||
}
|
||||
_ => (),
|
||||
}
|
||||
|
||||
let mut pos_x = 0.0f32;
|
||||
let pos_y = flex_height;
|
||||
*node = node.clone().move_to(Point {
|
||||
x: leaf_layout.location.x,
|
||||
y: leaf_layout.location.y,
|
||||
});
|
||||
});
|
||||
|
||||
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);
|
||||
}
|
||||
let size = Size {
|
||||
width: flex_layout.content_size.width,
|
||||
height: flex_layout.content_size.height,
|
||||
};
|
||||
|
||||
flex_height += current_row_height;
|
||||
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)
|
||||
Node::with_children(size, nodes)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//! Responsively generates rows of widgets based on the dimensions of its children.
|
||||
|
||||
pub mod layout;
|
||||
pub mod widget;
|
||||
|
||||
|
|
@ -8,7 +10,7 @@ pub use widget::FlexRow;
|
|||
|
||||
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> {
|
||||
FlexRow::new(children)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,7 @@ pub struct FlexRow<'a, Message> {
|
|||
#[setters(skip)]
|
||||
children: Vec<Element<'a, Message>>,
|
||||
/// Sets the padding around the widget.
|
||||
#[setters(into)]
|
||||
padding: Padding,
|
||||
/// Sets the space between each column of items.
|
||||
column_spacing: u16,
|
||||
|
|
@ -24,21 +25,64 @@ pub struct FlexRow<'a, Message> {
|
|||
row_spacing: u16,
|
||||
/// Sets the width.
|
||||
width: Length,
|
||||
/// Sets minimum width of items that grow.
|
||||
#[setters(into)]
|
||||
min_item_width: Option<f32>,
|
||||
/// Sets the max width
|
||||
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> {
|
||||
pub const fn new(children: Vec<Element<'a, Message>>) -> Self {
|
||||
pub(crate) const fn new(children: Vec<Element<'a, Message>>) -> Self {
|
||||
Self {
|
||||
children,
|
||||
padding: Padding::ZERO,
|
||||
column_spacing: 4,
|
||||
row_spacing: 4,
|
||||
width: Length::Shrink,
|
||||
min_item_width: None,
|
||||
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>
|
||||
|
|
@ -75,6 +119,10 @@ impl<'a, Message: 'static + Clone> Widget<Message, crate::Theme, Renderer>
|
|||
self.padding,
|
||||
f32::from(self.column_spacing),
|
||||
f32::from(self.row_spacing),
|
||||
self.min_item_width,
|
||||
self.align_items,
|
||||
self.justify_items,
|
||||
self.justify_content,
|
||||
&mut tree.children,
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -10,8 +10,9 @@ use iced_core::{Alignment, Length, Padding, Point, Size};
|
|||
use taffy::geometry::{Line, Rect};
|
||||
use taffy::style::{AlignItems, Dimension, Display, GridPlacement, Style};
|
||||
use taffy::style_helpers::{auto, length};
|
||||
use taffy::TaffyTree;
|
||||
use taffy::{AlignContent, TaffyTree};
|
||||
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
#[allow(clippy::too_many_lines)]
|
||||
pub fn resolve<Message>(
|
||||
renderer: &Renderer,
|
||||
|
|
@ -23,6 +24,7 @@ pub fn resolve<Message>(
|
|||
padding: Padding,
|
||||
column_alignment: Alignment,
|
||||
row_alignment: Alignment,
|
||||
justify_content: Option<AlignContent>,
|
||||
column_spacing: f32,
|
||||
row_spacing: f32,
|
||||
tree: &mut [Tree],
|
||||
|
|
@ -44,23 +46,29 @@ pub fn resolve<Message>(
|
|||
nodes.push(child_node);
|
||||
|
||||
let c_size = child_widget.size();
|
||||
let (width, justify_self) = match c_size.width {
|
||||
Length::Fill | Length::FillPortion(_) => (Dimension::Auto, Some(AlignItems::Stretch)),
|
||||
_ => (length(size.width), None),
|
||||
let (width, flex_grow, justify_self) = match c_size.width {
|
||||
Length::Fill | Length::FillPortion(_) => {
|
||||
(Dimension::Auto, 1.0, Some(AlignItems::Stretch))
|
||||
}
|
||||
_ => (length(size.width), 0.0, None),
|
||||
};
|
||||
|
||||
// Attach widget as leaf to be later assigned to grid.
|
||||
let leaf = taffy.new_leaf(Style {
|
||||
flex_grow,
|
||||
|
||||
grid_column: Line {
|
||||
start: GridPlacement::Line((assignment.column as i16).into()),
|
||||
end: GridPlacement::Line(
|
||||
(assignment.column as i16 + assignment.width as i16).into(),
|
||||
),
|
||||
},
|
||||
|
||||
grid_row: Line {
|
||||
start: GridPlacement::Line((assignment.row as i16).into()),
|
||||
end: GridPlacement::Line((assignment.row as i16 + assignment.height as i16).into()),
|
||||
},
|
||||
|
||||
size: taffy::geometry::Size {
|
||||
width,
|
||||
height: match c_size.height {
|
||||
|
|
@ -68,14 +76,16 @@ pub fn resolve<Message>(
|
|||
_ => length(size.height),
|
||||
},
|
||||
},
|
||||
|
||||
justify_self,
|
||||
|
||||
..Style::default()
|
||||
});
|
||||
|
||||
match leaf {
|
||||
Ok(leaf) => leafs.push(leaf),
|
||||
Err(why) => {
|
||||
tracing::error!(%why, "cannot add leaf node to grid");
|
||||
tracing::error!(?why, "cannot add leaf node to grid");
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
|
@ -108,6 +118,8 @@ pub fn resolve<Message>(
|
|||
},
|
||||
}),
|
||||
|
||||
justify_content,
|
||||
|
||||
padding: Rect {
|
||||
left: length(padding.left),
|
||||
right: length(padding.right),
|
||||
|
|
@ -134,7 +146,7 @@ pub fn resolve<Message>(
|
|||
let root = match root {
|
||||
Ok(root) => root,
|
||||
Err(why) => {
|
||||
tracing::error!(%why, "grid root style invalid");
|
||||
tracing::error!(?why, "grid root style invalid");
|
||||
return Node::new(Size::ZERO);
|
||||
}
|
||||
};
|
||||
|
|
@ -146,14 +158,14 @@ pub fn resolve<Message>(
|
|||
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);
|
||||
}
|
||||
|
||||
let grid_layout = match taffy.layout(root) {
|
||||
Ok(layout) => layout,
|
||||
Err(why) => {
|
||||
tracing::error!(%why, "cannot get layout of grid");
|
||||
tracing::error!(?why, "cannot get layout of grid");
|
||||
return Node::new(Size::ZERO);
|
||||
}
|
||||
};
|
||||
|
|
@ -183,9 +195,9 @@ pub fn resolve<Message>(
|
|||
}
|
||||
|
||||
let grid_size = Size {
|
||||
width: grid_layout.size.width,
|
||||
height: grid_layout.size.height,
|
||||
width: grid_layout.content_size.width,
|
||||
height: grid_layout.content_size.height,
|
||||
};
|
||||
|
||||
Node::with_children(grid_size.expand(padding), nodes)
|
||||
Node::with_children(grid_size, nodes)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,12 +1,14 @@
|
|||
// Copyright 2023 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//! Arrange widgets with a grid layout.
|
||||
|
||||
pub mod layout;
|
||||
pub mod widget;
|
||||
|
||||
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> {
|
||||
Grid::new()
|
||||
}
|
||||
|
|
|
|||
|
|
@ -26,6 +26,9 @@ pub struct Grid<'a, Message> {
|
|||
column_alignment: Alignment,
|
||||
/// Alignment across rows
|
||||
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.
|
||||
column_spacing: u16,
|
||||
/// Sets the space between each item in a row.
|
||||
|
|
@ -50,6 +53,7 @@ impl<'a, Message> Grid<'a, Message> {
|
|||
padding: Padding::ZERO,
|
||||
column_alignment: Alignment::Start,
|
||||
row_alignment: Alignment::Start,
|
||||
justify_content: None,
|
||||
column_spacing: 4,
|
||||
row_spacing: 4,
|
||||
width: Length::Shrink,
|
||||
|
|
@ -138,6 +142,7 @@ impl<'a, Message: 'static + Clone> Widget<Message, crate::Theme, Renderer> for G
|
|||
self.padding,
|
||||
self.column_alignment,
|
||||
self.row_alignment,
|
||||
self.justify_content,
|
||||
f32::from(self.column_spacing),
|
||||
f32::from(self.row_spacing),
|
||||
&mut tree.children,
|
||||
|
|
|
|||
|
|
@ -1,10 +1,11 @@
|
|||
// Copyright 2022 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
use crate::config::Density;
|
||||
use crate::{ext::CollectionWidget, widget, Element};
|
||||
use apply::Apply;
|
||||
use derive_setters::Setters;
|
||||
use iced::Length;
|
||||
use iced::{Length, Padding};
|
||||
use iced_core::{widget::tree, Widget};
|
||||
use std::borrow::Cow;
|
||||
|
||||
|
|
@ -20,6 +21,7 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> {
|
|||
start: Vec::new(),
|
||||
center: Vec::new(),
|
||||
end: Vec::new(),
|
||||
density: None,
|
||||
focused: false,
|
||||
}
|
||||
}
|
||||
|
|
@ -50,9 +52,6 @@ pub struct HeaderBar<'a, Message> {
|
|||
#[setters(strip_option)]
|
||||
on_right_click: Option<Message>,
|
||||
|
||||
/// Focused state of the window
|
||||
focused: bool,
|
||||
|
||||
/// Elements packed at the start of the headerbar.
|
||||
#[setters(skip)]
|
||||
start: Vec<Element<'a, Message>>,
|
||||
|
|
@ -64,6 +63,13 @@ pub struct HeaderBar<'a, Message> {
|
|||
/// Elements packed at the end of the headerbar.
|
||||
#[setters(skip)]
|
||||
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> {
|
||||
|
|
@ -99,7 +105,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
|||
#[must_use]
|
||||
pub fn build(self) -> HeaderBarWidget<'a, Message> {
|
||||
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> {
|
||||
/// 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.
|
||||
let start = std::mem::take(&mut self.start);
|
||||
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(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.
|
||||
let mut widget = widget::row::with_capacity(4)
|
||||
// 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),
|
||||
)
|
||||
.align_items(iced::Alignment::Center)
|
||||
.height(Length::Fixed(50.0))
|
||||
.padding(8)
|
||||
.spacing(8)
|
||||
.height(Length::Fixed(height))
|
||||
.padding(padding)
|
||||
.spacing(padding)
|
||||
.apply(widget::container)
|
||||
.style(crate::theme::Container::HeaderBar {
|
||||
focused: self.focused,
|
||||
|
|
@ -326,9 +337,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
|||
let mut title = Cow::default();
|
||||
std::mem::swap(&mut title, &mut self.title);
|
||||
|
||||
widget::text(title)
|
||||
.size(16)
|
||||
.font(crate::font::FONT_SEMIBOLD)
|
||||
widget::text::heading(title)
|
||||
.apply(widget::container)
|
||||
.center_x()
|
||||
.center_y()
|
||||
|
|
@ -349,6 +358,13 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
|||
.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)
|
||||
.push_maybe(self.on_minimize.take().map(|m| {
|
||||
icon(
|
||||
|
|
@ -371,7 +387,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> {
|
|||
m,
|
||||
)
|
||||
}))
|
||||
.spacing(8)
|
||||
.spacing(spacing)
|
||||
.apply(widget::container)
|
||||
.height(Length::Fill)
|
||||
.center_y()
|
||||
|
|
|
|||
|
|
@ -69,7 +69,7 @@ impl Icon {
|
|||
}
|
||||
|
||||
#[must_use]
|
||||
fn into_element<Message: 'static>(self) -> Element<'static, Message> {
|
||||
fn view<'a, Message: 'a>(self) -> Element<'a, Message> {
|
||||
let from_image = |handle| {
|
||||
Image::new(handle)
|
||||
.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 {
|
||||
icon.into_element::<Message>()
|
||||
icon.view::<Message>()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
207
src/widget/id_container.rs
Normal file
207
src/widget/id_container.rs
Normal 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)
|
||||
}
|
||||
}
|
||||
|
|
@ -59,14 +59,17 @@ pub use action::MenuAction as Action;
|
|||
|
||||
mod flex;
|
||||
pub mod key_bind;
|
||||
pub use key_bind::KeyBind;
|
||||
|
||||
mod menu_bar;
|
||||
pub use menu_bar::MenuBar;
|
||||
pub(crate) use menu_bar::MenuBarState;
|
||||
pub use menu_bar::{menu_bar as bar, MenuBar};
|
||||
|
||||
mod menu_inner;
|
||||
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(crate) use menu_inner::Menu;
|
||||
|
|
|
|||
|
|
@ -1,5 +1,3 @@
|
|||
use crate::widget::segmented_button::Entity;
|
||||
|
||||
/// `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.
|
||||
|
|
@ -31,7 +29,7 @@ use crate::widget::segmented_button::Entity;
|
|||
/// impl MenuAction for MyAction {
|
||||
/// type Message = MyMessage;
|
||||
///
|
||||
/// fn message(&self, entity: Option<Entity>) -> Self::Message {
|
||||
/// fn message(&self) -> Self::Message {
|
||||
/// match self {
|
||||
/// MyAction::Open => MyMessage::Open,
|
||||
/// 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.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `entity` - An optional `Entity` that may be associated with the action.
|
||||
///
|
||||
/// # Returns
|
||||
///
|
||||
/// * `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;
|
||||
}
|
||||
|
|
|
|||
|
|
@ -19,6 +19,13 @@ use iced_widget::core::{
|
|||
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) pressed: bool,
|
||||
pub(crate) view_cursor: Cursor,
|
||||
|
|
@ -56,8 +63,7 @@ impl Default for MenuBarState {
|
|||
}
|
||||
}
|
||||
|
||||
/// A `MenuBar` collects `MenuTree`s and handles
|
||||
/// all the layout, event processing and drawing
|
||||
/// A `MenuBar` collects `MenuTree`s and handles all the layout, event processing, and drawing.
|
||||
#[allow(missing_debug_implementations)]
|
||||
pub struct MenuBar<'a, Message, Renderer = crate::Renderer>
|
||||
where
|
||||
|
|
@ -384,8 +390,8 @@ where
|
|||
},
|
||||
shadow: Default::default(),
|
||||
};
|
||||
let path_color = styling.path;
|
||||
renderer.fill_quad(path_quad, path_color);
|
||||
|
||||
renderer.fill_quad(path_quad, styling.path);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -709,8 +709,8 @@ where
|
|||
},
|
||||
shadow: Shadow::default(),
|
||||
};
|
||||
let path_color = styling.path;
|
||||
r.fill_quad(path_quad, path_color);
|
||||
|
||||
r.fill_quad(path_quad, styling.path);
|
||||
}
|
||||
|
||||
// draw item
|
||||
|
|
|
|||
|
|
@ -141,21 +141,19 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
/// This macro creates a button for a MenuTree.
|
||||
#[macro_export]
|
||||
macro_rules! menu_button {
|
||||
($($x:expr),+ $(,)?) => (
|
||||
widget::button(
|
||||
widget::Row::with_children(vec![$($x.into()),+])
|
||||
pub fn menu_button<'a, Message: 'a>(
|
||||
children: Vec<crate::Element<'a, Message>>,
|
||||
) -> crate::widget::Button<'a, Message> {
|
||||
widget::button(
|
||||
widget::Row::with_children(children)
|
||||
.align_items(Alignment::Center)
|
||||
.height(Length::Fill)
|
||||
.width(Length::Fill),
|
||||
)
|
||||
.height(Length::Fixed(36.0))
|
||||
.padding([4, 16])
|
||||
.width(Length::Fill)
|
||||
.style(theme::Button::MenuItem)
|
||||
);
|
||||
)
|
||||
.height(Length::Fixed(36.0))
|
||||
.padding([4, 16])
|
||||
.width(Length::Fill)
|
||||
.style(theme::Button::MenuItem)
|
||||
}
|
||||
|
||||
/// 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>>,
|
||||
{
|
||||
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 {
|
||||
return key_bind.to_string();
|
||||
}
|
||||
|
|
@ -243,41 +241,48 @@ where
|
|||
match item {
|
||||
MenuItem::Button(label, action) => {
|
||||
let key = find_key(&action, key_binds);
|
||||
let menu_button = menu_button!(
|
||||
widget::text(label),
|
||||
widget::horizontal_space(Length::Fill),
|
||||
widget::text(key),
|
||||
)
|
||||
.on_press(action.message(None));
|
||||
let menu_button = menu_button(vec![
|
||||
widget::text(label).into(),
|
||||
widget::horizontal_space(Length::Fill).into(),
|
||||
widget::text(key).into(),
|
||||
])
|
||||
.on_press(action.message());
|
||||
|
||||
trees.push(MenuTree::<Message, Renderer>::new(menu_button));
|
||||
}
|
||||
MenuItem::CheckBox(label, value, action) => {
|
||||
let key = find_key(&action, &key_binds);
|
||||
let key = find_key(&action, key_binds);
|
||||
trees.push(MenuTree::new(
|
||||
menu_button!(
|
||||
menu_button(vec![
|
||||
if value {
|
||||
// TODO: add a object-select-symbolic icon, `Message: 'static` is required when using an icon widget.
|
||||
widget::container(widget::text("✓"))
|
||||
widget::icon::from_name("object-select-symbolic")
|
||||
.size(16)
|
||||
.icon()
|
||||
.width(Length::Fixed(16.0))
|
||||
.into()
|
||||
} 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::text(label),
|
||||
widget::horizontal_space(Length::Fill),
|
||||
widget::text(key)
|
||||
)
|
||||
.on_press(action.message(None)),
|
||||
widget::Space::with_width(Length::Fixed(8.0)).into(),
|
||||
widget::text(label)
|
||||
.horizontal_alignment(iced::alignment::Horizontal::Left)
|
||||
.into(),
|
||||
widget::horizontal_space(Length::Fill).into(),
|
||||
widget::text(key).into(),
|
||||
])
|
||||
.on_press(action.message()),
|
||||
));
|
||||
}
|
||||
MenuItem::Folder(label, children) => {
|
||||
trees.push(MenuTree::<Message, Renderer>::with_children(
|
||||
menu_button!(
|
||||
widget::text(label),
|
||||
widget::horizontal_space(Length::Fill),
|
||||
// TODO: add a pan-end-symbolic icon, `Message: 'static` is required when using an icon widget.
|
||||
widget::text("▶"),
|
||||
),
|
||||
menu_button(vec![
|
||||
widget::text(label).into(),
|
||||
widget::horizontal_space(Length::Fill).into(),
|
||||
widget::icon::from_name("pan-end-symbolic")
|
||||
.size(16)
|
||||
.icon()
|
||||
.into(),
|
||||
]),
|
||||
menu_items(key_binds, children),
|
||||
));
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,70 +1,160 @@
|
|||
// Copyright 2022 System76 <info@system76.com>
|
||||
// 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
|
||||
#[doc(inline)]
|
||||
pub use iced::widget::{canvas, Canvas};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced::widget::{checkbox, Checkbox};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced::widget::{combo_box, ComboBox};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced::widget::{container, Container};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced::widget::{horizontal_space, space, vertical_space, Space};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced::widget::{image, Image};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced::widget::{lazy, Lazy};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced::widget::{mouse_area, MouseArea};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced::widget::{pane_grid, PaneGrid};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced::widget::{progress_bar, ProgressBar};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced::widget::{radio, Radio};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced::widget::{responsive, Responsive};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use iced::widget::{slider, vertical_slider, Slider, VerticalSlider};
|
||||
|
||||
#[doc(inline)]
|
||||
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 mod aspect_ratio;
|
||||
|
||||
pub mod button;
|
||||
#[doc(inline)]
|
||||
pub use button::{button, Button, IconButton, LinkButton, TextButton};
|
||||
|
||||
pub(crate) mod common;
|
||||
|
||||
pub mod calendar;
|
||||
#[doc(inline)]
|
||||
pub use calendar::{calendar, Calendar};
|
||||
|
||||
pub mod card;
|
||||
#[doc(inline)]
|
||||
pub use card::*;
|
||||
|
||||
pub mod color_picker;
|
||||
#[doc(inline)]
|
||||
pub use color_picker::{ColorPicker, ColorPickerModel};
|
||||
|
||||
pub mod context_drawer;
|
||||
#[doc(inline)]
|
||||
pub use context_drawer::{context_drawer, ContextDrawer};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use column::{column, 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>;
|
||||
|
||||
#[must_use]
|
||||
/// A container which aligns its children in a column.
|
||||
pub fn column<'a, Message>() -> Column<'a, Message> {
|
||||
Column::new()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// A pre-allocated [`column`].
|
||||
pub fn with_capacity<'a, Message>(capacity: usize) -> Column<'a, Message> {
|
||||
Column::with_children(Vec::with_capacity(capacity))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// A [`column`] that will be assigned a [`Vec`] of children.
|
||||
pub fn with_children<Message>(children: Vec<crate::Element<Message>>) -> Column<Message> {
|
||||
Column::with_children(children)
|
||||
}
|
||||
}
|
||||
|
||||
pub mod layer_container;
|
||||
#[doc(inline)]
|
||||
pub use layer_container::{layer_container, LayerContainer};
|
||||
|
||||
pub mod context_menu;
|
||||
#[doc(inline)]
|
||||
pub use context_menu::{context_menu, ContextMenu};
|
||||
|
||||
pub mod dialog;
|
||||
#[doc(inline)]
|
||||
pub use dialog::{dialog, Dialog};
|
||||
|
||||
/// An element to distinguish a boundary between two elements.
|
||||
|
|
@ -117,67 +207,93 @@ pub mod divider {
|
|||
}
|
||||
|
||||
pub mod dnd_destination;
|
||||
#[doc(inline)]
|
||||
pub use dnd_destination::{dnd_destination, DndDestination};
|
||||
|
||||
pub mod dnd_source;
|
||||
#[doc(inline)]
|
||||
pub use dnd_source::{dnd_source, DndSource};
|
||||
|
||||
pub mod dropdown;
|
||||
#[doc(inline)]
|
||||
pub use dropdown::{dropdown, Dropdown};
|
||||
|
||||
pub mod flex_row;
|
||||
#[doc(inline)]
|
||||
pub use flex_row::{flex_row, FlexRow};
|
||||
|
||||
pub mod grid;
|
||||
#[doc(inline)]
|
||||
pub use grid::{grid, Grid};
|
||||
|
||||
mod header_bar;
|
||||
#[doc(inline)]
|
||||
pub use header_bar::{header_bar, HeaderBar};
|
||||
|
||||
pub mod icon;
|
||||
#[doc(inline)]
|
||||
pub use icon::{icon, Icon};
|
||||
|
||||
pub mod id_container;
|
||||
#[doc(inline)]
|
||||
pub use id_container::{id_container, IdContainer};
|
||||
|
||||
#[cfg(feature = "animated-image")]
|
||||
pub mod frames;
|
||||
|
||||
pub use taffy::JustifyContent;
|
||||
|
||||
pub mod list;
|
||||
pub use list::*;
|
||||
#[doc(inline)]
|
||||
pub use list::{list_column, ListColumn};
|
||||
|
||||
pub mod menu;
|
||||
|
||||
pub mod nav_bar;
|
||||
#[doc(inline)]
|
||||
pub use nav_bar::{nav_bar, nav_bar_dnd};
|
||||
|
||||
pub mod nav_bar_toggle;
|
||||
#[doc(inline)]
|
||||
pub use nav_bar_toggle::{nav_bar_toggle, NavBarToggle};
|
||||
|
||||
pub mod popover;
|
||||
#[doc(inline)]
|
||||
pub use popover::{popover, Popover};
|
||||
|
||||
pub mod rectangle_tracker;
|
||||
#[doc(inline)]
|
||||
pub use rectangle_tracker::{rectangle_tracker, RectangleTracker};
|
||||
|
||||
#[doc(inline)]
|
||||
pub use row::{row, 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>;
|
||||
|
||||
#[must_use]
|
||||
/// A container which aligns its children in a row.
|
||||
pub fn row<'a, Message>() -> Row<'a, Message> {
|
||||
Row::new()
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// A pre-allocated [`row`].
|
||||
pub fn with_capacity<'a, Message>(capacity: usize) -> Row<'a, Message> {
|
||||
Row::with_children(Vec::with_capacity(capacity))
|
||||
}
|
||||
|
||||
#[must_use]
|
||||
/// A [`row`] that will be assigned a [`Vec`] of children.
|
||||
pub fn with_children<Message>(children: Vec<crate::Element<Message>>) -> Row<Message> {
|
||||
Row::with_children(children)
|
||||
}
|
||||
}
|
||||
|
||||
mod scrollable;
|
||||
#[doc(inline)]
|
||||
pub use scrollable::*;
|
||||
|
||||
pub mod segmented_button;
|
||||
|
|
@ -186,19 +302,26 @@ pub mod segmented_control;
|
|||
pub mod settings;
|
||||
|
||||
pub mod spin_button;
|
||||
#[doc(inline)]
|
||||
pub use spin_button::{spin_button, SpinButton};
|
||||
|
||||
pub mod tab_bar;
|
||||
|
||||
pub mod text;
|
||||
#[doc(inline)]
|
||||
pub use text::{text, Text};
|
||||
|
||||
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;
|
||||
#[doc(inline)]
|
||||
pub use toggler::toggler;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use tooltip::{tooltip, Tooltip};
|
||||
pub mod tooltip {
|
||||
use crate::Element;
|
||||
|
|
@ -224,4 +347,5 @@ pub mod tooltip {
|
|||
}
|
||||
|
||||
pub mod warning;
|
||||
#[doc(inline)]
|
||||
pub use warning::*;
|
||||
|
|
|
|||
|
|
@ -42,13 +42,9 @@ impl<'a, Message: 'static + Clone> From<NavBarToggle<Message>> for Element<'a, M
|
|||
};
|
||||
|
||||
widget::button::icon(icon)
|
||||
.padding([8, 16, 8, 16])
|
||||
.on_press_maybe(nav_bar_toggle.on_toggle)
|
||||
.selected(nav_bar_toggle.selected)
|
||||
.style(nav_bar_toggle.style)
|
||||
.apply(widget::container)
|
||||
.center_y()
|
||||
.height(Length::Fill)
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,8 +103,8 @@ where
|
|||
/// .build()
|
||||
/// ```
|
||||
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
|
||||
pub fn icon(mut self, icon: Icon) -> Self {
|
||||
self.model.0.icon_set(self.id, icon);
|
||||
pub fn icon(mut self, icon: impl Into<Icon>) -> Self {
|
||||
self.model.0.icon_set(self.id, icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -89,8 +89,8 @@ where
|
|||
/// model.insert().text("Item A").icon(IconSource::from("icon-a"));
|
||||
/// ```
|
||||
#[allow(clippy::must_use_candidate, clippy::return_self_not_must_use)]
|
||||
pub fn icon(self, icon: Icon) -> Self {
|
||||
self.model.icon_set(self.id, icon);
|
||||
pub fn icon(self, icon: impl Into<Icon>) -> Self {
|
||||
self.model.icon_set(self.id, icon.into());
|
||||
self
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -4,11 +4,13 @@
|
|||
use std::borrow::Cow;
|
||||
|
||||
use crate::{
|
||||
widget::{column, horizontal_space, row, text, Row},
|
||||
widget::{column, container, flex_row, horizontal_space, row, text, FlexRow, Row},
|
||||
Element,
|
||||
};
|
||||
use derive_setters::Setters;
|
||||
use iced::alignment::Horizontal;
|
||||
use iced_core::Length;
|
||||
use taffy::AlignContent;
|
||||
|
||||
/// A settings item aligned in a row
|
||||
#[must_use]
|
||||
|
|
@ -29,9 +31,33 @@ pub fn item<'a, Message: 'static>(
|
|||
#[allow(clippy::module_name_repetitions)]
|
||||
pub fn item_row<Message>(children: Vec<Element<Message>>) -> Row<Message> {
|
||||
row::with_children(children)
|
||||
.spacing(12)
|
||||
.align_items(iced::Alignment::Center)
|
||||
.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)
|
||||
.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.
|
||||
|
|
@ -61,6 +87,15 @@ pub struct Item<'a, Message> {
|
|||
impl<'a, Message: 'static> Item<'a, Message> {
|
||||
/// Assigns a control to the item.
|
||||
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);
|
||||
|
||||
if let Some(icon) = self.icon {
|
||||
|
|
@ -80,15 +115,14 @@ impl<'a, Message: 'static> Item<'a, Message> {
|
|||
}
|
||||
|
||||
contents.push(widget.into());
|
||||
|
||||
item_row(contents)
|
||||
contents
|
||||
}
|
||||
|
||||
pub fn toggler(
|
||||
self,
|
||||
is_checked: bool,
|
||||
message: impl Fn(bool) -> Message + 'static,
|
||||
) -> Row<'a, Message> {
|
||||
self.control(crate::widget::toggler(None, is_checked, message))
|
||||
) -> FlexRow<'a, Message> {
|
||||
self.flex_control(crate::widget::toggler(None, is_checked, message))
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
pub mod item;
|
||||
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};
|
||||
|
||||
use crate::widget::{column, Column};
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
// Copyright 2022 System76 <info@system76.com>
|
||||
// SPDX-License-Identifier: MPL-2.0
|
||||
|
||||
//! A control for incremental adjustments of a value.
|
||||
|
||||
mod model;
|
||||
use std::borrow::Cow;
|
||||
|
||||
|
|
@ -20,6 +22,7 @@ pub struct SpinButton<'a, Message> {
|
|||
on_change: Box<dyn Fn(model::Message) -> Message + 'static>,
|
||||
}
|
||||
|
||||
/// A control for incremental adjustments of a value.
|
||||
pub fn spin_button<'a, Message: 'static>(
|
||||
label: impl Into<Cow<'a, str>>,
|
||||
on_change: impl Fn(model::Message) -> Message + 'static,
|
||||
|
|
|
|||
|
|
@ -729,28 +729,30 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
let index = tree.children.len() - 1;
|
||||
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());
|
||||
if tree.children.len() > 0 {
|
||||
let index = tree.children.len() - 1;
|
||||
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());
|
||||
|
||||
if let Some(trailing_layout) = trailing_icon_layout {
|
||||
if cursor_position.is_over(trailing_layout.bounds()) {
|
||||
let res = trailing_icon.as_widget_mut().on_event(
|
||||
tree,
|
||||
event.clone(),
|
||||
trailing_layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
if let Some(trailing_layout) = trailing_icon_layout {
|
||||
if cursor_position.is_over(trailing_layout.bounds()) {
|
||||
let res = trailing_icon.as_widget_mut().on_event(
|
||||
tree,
|
||||
event.clone(),
|
||||
trailing_layout,
|
||||
cursor_position,
|
||||
renderer,
|
||||
clipboard,
|
||||
shell,
|
||||
viewport,
|
||||
);
|
||||
|
||||
if res == event::Status::Captured {
|
||||
return res;
|
||||
if res == event::Status::Captured {
|
||||
return res;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue