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

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

View file

@ -31,7 +31,7 @@ desktop = ["process", "dep:freedesktop-desktop-entry", "dep:mime", "dep:shlex"]
# Enables keycode serialization
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"

View file

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

View file

@ -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};
"#}
}

View file

@ -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

View file

@ -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),
],
),
)])

View file

@ -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

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

View file

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

View file

@ -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(),

View file

@ -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) => {

View file

@ -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));
}

View file

@ -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)

View file

@ -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.

View file

@ -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)
}
}

View file

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

View file

@ -29,7 +29,10 @@ pub mod command;
pub mod config;
#[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;

View file

@ -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;
}
}
}
})

View file

@ -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);

View file

@ -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],
]

View file

@ -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,
}

View file

@ -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;

View file

@ -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

View file

@ -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(),

View file

@ -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,

View file

@ -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,

View file

@ -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> {

View file

@ -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>,

View file

@ -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);
}

View file

@ -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,

View file

@ -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>,

View file

@ -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,

View file

@ -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

View file

@ -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 {

View file

@ -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 {

View file

@ -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>,

View file

@ -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)
}

View file

@ -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)
}

View file

@ -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,
)
}

View file

@ -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)
}

View file

@ -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()
}

View file

@ -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,

View file

@ -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()

View file

@ -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
View file

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

View file

@ -59,14 +59,17 @@ pub use action::MenuAction as Action;
mod flex;
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;

View file

@ -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;
}

View file

@ -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);
}
}

View file

@ -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

View file

@ -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),
));
}

View file

@ -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::*;

View file

@ -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()
}
}

View file

@ -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
}

View file

@ -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
}

View file

@ -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))
}
}

View file

@ -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};

View file

@ -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,

View file

@ -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;
}
}
}
}