diff --git a/Cargo.toml b/Cargo.toml index 98f7cceb..03f82ff9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -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" diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index b0db5828..48a18a52 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -1,3 +1,5 @@ +//! Integrations for cosmic-config — the cosmic configuration system. + use notify::{ event::{EventKind, ModifyKind}, Watcher, diff --git a/cosmic-theme/src/output/gtk4_output.rs b/cosmic-theme/src/output/gtk4_output.rs index db21b8d1..e9cbb508 100644 --- a/cosmic-theme/src/output/gtk4_output.rs +++ b/cosmic-theme/src/output/gtk4_output.rs @@ -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}; "#} } diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index 31b0f773..7344fabe 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -42,5 +42,16 @@ impl Theme { pub fn to_hex(c: Srgba) -> String { let c_u8: Rgba = 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 = c.into_format(); + format!( + "rgba({}, {}, {}, {:1.2})", + c_u8.red, c_u8.green, c_u8.blue, c.alpha + ) } diff --git a/examples/design-demo b/examples/design-demo deleted file mode 160000 index 493e17a0..00000000 --- a/examples/design-demo +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 493e17a0105c7523fb7ff5fd7221ec586ac9010f diff --git a/examples/menu/src/main.rs b/examples/menu/src/main.rs index dbd8de07..ab668cb6 100644 --- a/examples/menu/src/main.rs +++ b/examples/menu/src/main.rs @@ -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) -> 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) -> 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), ], ), )]) diff --git a/examples/open-dialog/src/main.rs b/examples/open-dialog/src/main.rs index f8b95360..c51d2f6c 100644 --- a/examples/open-dialog/src/main.rs +++ b/examples/open-dialog/src/main.rs @@ -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. diff --git a/iced b/iced index 7c58b8ed..2dcae592 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 7c58b8eda9885083e019a5ea14edf4652740e154 +Subproject commit 2dcae59258c9cee6862c7fb2193066a1cb1e30c2 diff --git a/justfile b/justfile index dcddf7e7..0280da7c 100644 --- a/justfile +++ b/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 diff --git a/src/app/core.rs b/src/app/core.rs index 6594f4a7..1a57de66 100644 --- a/src/app/core.rs +++ b/src/app/core.rs @@ -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, pub(super) portal_accent: Option, @@ -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(), diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 1560c56f..30adc37d 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -156,6 +156,7 @@ where } } + #[allow(clippy::too_many_lines)] fn subscription(&self) -> Subscription { 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::toolkit::ID) + .watch_config::(crate::config::ID) .map(|update| { for why in update.errors { tracing::error!(?why, "cosmic toolkit config update error"); @@ -408,11 +409,9 @@ impl Cosmic { } 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 Cosmic { } 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 Cosmic { }; } 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 Cosmic { 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) => { diff --git a/src/app/mod.rs b/src/app/mod.rs index e9bc84ed..275578af 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -37,6 +37,12 @@ pub mod message { pub const fn none() -> Message { Message::None } + + impl From for Message { + 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( 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( 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( /// /// Returns error on application failure. pub fn run(settings: Settings, flags: App::Flags) -> iced::Result { - #[cfg(feature = "wgpu")] - wgpu_power_pref(); - let settings = iced_settings::(settings, flags); cosmic::Cosmic::::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> { @@ -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>); + fn init(core: Core, flags: Self::Flags) -> (Self, Command); /// Displays a context drawer on the side of the application window when `Some`. fn context_drawer(&self) -> Option> { @@ -506,28 +487,28 @@ where } // Called when context drawer is toggled - fn on_context_drawer(&mut self) -> iced::Command> { - iced::Command::none() + fn on_context_drawer(&mut self) -> Command { + Command::none() } /// Called when the escape key is pressed. - fn on_escape(&mut self) -> iced::Command> { - iced::Command::none() + fn on_escape(&mut self) -> Command { + Command::none() } /// Called when a navigation item is selected. - fn on_nav_select(&mut self, id: nav_bar::Id) -> iced::Command> { - iced::Command::none() + fn on_nav_select(&mut self, id: nav_bar::Id) -> Command { + 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> { - iced::Command::none() + fn on_nav_context(&mut self, id: nav_bar::Id) -> Command { + Command::none() } /// Called when the search function is requested. - fn on_search(&mut self) -> iced::Command> { - iced::Command::none() + fn on_search(&mut self) -> Command { + 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> { - iced::Command::none() + fn update(&mut self, message: Self::Message) -> Command { + 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> { - iced::Command::none() + ) -> Command { + 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> { - iced::Command::none() + ) -> Command { + 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> { - iced::Command::none() + fn dbus_activation(&mut self, msg: DbusActivationMessage) -> Command { + Command::none() } } /// Methods automatically derived for all types implementing [`Application`]. pub trait ApplicationExt: Application { /// Initiates a window drag. - fn drag(&mut self) -> iced::Command>; + fn drag(&mut self) -> Command; /// Maximizes the window. - fn maximize(&mut self) -> iced::Command>; + fn maximize(&mut self) -> Command; /// Minimizes the window. - fn minimize(&mut self) -> iced::Command>; + fn minimize(&mut self) -> Command; /// 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>; + fn set_window_title(&mut self, title: String) -> Command; #[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>; + fn set_window_title(&mut self, title: String, id: window::Id) -> Command; /// View template for the main window. fn view_main(&self) -> Element>; } impl ApplicationExt for App { - fn drag(&mut self) -> iced::Command> { + fn drag(&mut self) -> Command { command::drag(Some(self.main_window_id())) } - fn maximize(&mut self) -> iced::Command> { + fn maximize(&mut self) -> Command { command::maximize(Some(self.main_window_id()), true) } - fn minimize(&mut self) -> iced::Command> { + fn minimize(&mut self) -> Command { command::minimize(Some(self.main_window_id())) } @@ -656,23 +630,20 @@ impl ApplicationExt for App { } #[cfg(any(feature = "multi-window", feature = "wayland"))] - fn set_window_title( - &mut self, - title: String, - id: window::Id, - ) -> iced::Command> { + fn set_window_title(&mut self, title: String, id: window::Id) -> Command { 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> { + fn set_window_title(&mut self, title: String) -> Command { 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> { let core = self.core(); @@ -685,8 +656,11 @@ impl 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 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 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 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 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 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)); } diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 07a9c272..2c60cc62 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -372,7 +372,7 @@ pub fn style() -> ::Style { pub fn menu_button<'a, Message>( content: impl Into>, ) -> crate::widget::Button<'a, Message> { - crate::widget::Button::new(content) + crate::widget::button(content) .style(Button::AppletMenu) .padding(menu_control_padding()) .width(Length::Fill) diff --git a/src/command/mod.rs b/src/command/mod.rs index 24776e43..10e32a28 100644 --- a/src/command/mod.rs +++ b/src/command/mod.rs @@ -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(commands: impl IntoIterator>) -> Command { - Command::batch(commands) +pub fn batch, Y: 'static>( + commands: impl IntoIterator>, +) -> Command { + Command::batch(commands).map(Into::into) } /// Yields a command which will run the future on the runtime executor. -pub fn future(future: impl Future + Send + 'static) -> Command { - Command::single(Action::Future(Box::pin(future))) +pub fn future, Y>(future: impl Future + Send + 'static) -> Command { + Command::single(Action::Future(Box::pin(async move { future.await.into() }))) } /// Yields a command which will return a message. -pub fn message(message: M) -> Command { - future(async move { message }) +pub fn message, Y>(message: X) -> Command { + future(async move { message.into() }) } /// Initiates a window drag. diff --git a/src/config/mod.rs b/src/config/mod.rs index 78bf068a..15eb0bf5 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -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 = 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::new(ID, Self::VERSION) + } +} diff --git a/src/config/toolkit.rs b/src/config/toolkit.rs deleted file mode 100644 index 2f738cb9..00000000 --- a/src/config/toolkit.rs +++ /dev/null @@ -1,43 +0,0 @@ -// Copyright 2024 System76 -// 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::new(ID, Self::VERSION) - } -} diff --git a/src/lib.rs b/src/lib.rs index fe23fb5a..ee29b3e5 100644 --- a/src/lib.rs +++ b/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; diff --git a/src/theme/portal.rs b/src/theme/portal.rs index 277abd7b..b0fc5f84 100644 --- a/src/theme/portal.rs +++ b/src/theme/portal.rs @@ -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 { subscription::channel(std::any::TypeId::of::(), 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; + } } } }) diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index 0509d62f..e79496cd 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -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); diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 94dee4c5..6f3388c4 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -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], ] diff --git a/src/theme/style/menu_bar.rs b/src/theme/style/menu_bar.rs index 1b17c527..18b983fd 100644 --- a/src/theme/style/menu_bar.rs +++ b/src/theme/style/menu_bar.rs @@ -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, } diff --git a/src/theme/style/mod.rs b/src/theme/style/mod.rs index a0108cbe..0469673a 100644 --- a/src/theme/style/mod.rs +++ b/src/theme/style/mod.rs @@ -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; diff --git a/src/widget/aspect_ratio.rs b/src/widget/aspect_ratio.rs index 32ef16e5..c918f8c7 100644 --- a/src/widget/aspect_ratio.rs +++ b/src/widget/aspect_ratio.rs @@ -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 diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs index d1e10d02..c22c8cb6 100644 --- a/src/widget/button/icon.rs +++ b/src/widget/button/icon.rs @@ -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) -> Button<'a, Message> { Button::new(Icon { handle: handle.into(), diff --git a/src/widget/button/image.rs b/src/widget/button/image.rs index 9bfa2880..d93844cd 100644 --- a/src/widget/button/image.rs +++ b/src/widget/button/image.rs @@ -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 + '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 + 'a) -> Button<'a, Message> }) } +/// The image variant of a button. pub struct Image<'a, Handle, Message> { image: widget::Image<'a, Handle>, selected: bool, diff --git a/src/widget/button/link.rs b/src/widget/button/link.rs index d1d43e90..52cc44b1 100644 --- a/src/widget/button/link.rs +++ b/src/widget/button/link.rs @@ -1,6 +1,8 @@ // Copyright 2023 System76 // 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> + 'static) -> Button<'a, Message> { Button::new( label, diff --git a/src/widget/button/mod.rs b/src/widget/button/mod.rs index 9ba5dc2f..c9cf555a 100644 --- a/src/widget/button/mod.rs +++ b/src/widget/button/mod.rs @@ -1,29 +1,41 @@ // Copyright 2023 System76 // 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>) -> 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>, on_remove: Option, @@ -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> { diff --git a/src/widget/button/text.rs b/src/widget/button/text.rs index 134e0623..f58b4374 100644 --- a/src/widget/button/text.rs +++ b/src/widget/button/text.rs @@ -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>) -> 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>) -> 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>) -> Button<'a, Message> { Button::new(Text::new()).label(label) } +/// A text button with the text style pub fn text<'a, Message>(label: impl Into>) -> 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, pub(super) trailing_icon: Option, diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 498a94d9..427a1917 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -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>) -> Self { + pub(super) fn new(content: impl Into>) -> Self { Self { id: Id::unique(), #[cfg(feature = "a11y")] @@ -375,7 +375,13 @@ impl<'a, Message: 'a + Clone> Widget }; 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 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 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( // 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); } diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index fc47f5f3..61825ff9 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -1,6 +1,8 @@ // Copyright 2024 System76 // 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( selected: &NaiveDate, on_select: impl Fn(NaiveDate) -> M + 'static, diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 07e255cd..ff2894b1 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -1,6 +1,8 @@ // Copyright 2023 System76 // 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 = (0u16..8) @@ -64,7 +69,7 @@ pub enum ColorPickerUpdate { #[derive(Setters)] pub struct ColorPickerModel { #[setters(skip)] - segmented_model: Model, + segmented_model: segmented_button::Model, #[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, + model: &'a segmented_button::Model, #[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, color: Option, diff --git a/src/widget/context_drawer/mod.rs b/src/widget/context_drawer/mod.rs index 3a3982fd..258b4f88 100644 --- a/src/widget/context_drawer/mod.rs +++ b/src/widget/context_drawer/mod.rs @@ -1,6 +1,8 @@ // Copyright 2023 System76 // 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, diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index dfec06d1..261779f6 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -91,7 +91,7 @@ impl<'a, Message: Clone> Widget } 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 diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 57a52768..0aca0d79 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -253,7 +253,7 @@ impl<'a, Message: 'static> Widget } 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 { diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index e1699ed4..5a378209 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -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 { diff --git a/src/widget/dropdown/mod.rs b/src/widget/dropdown/mod.rs index f91acb6f..6e3db648 100644 --- a/src/widget/dropdown/mod.rs +++ b/src/widget/dropdown/mod.rs @@ -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, Message: 'a>( selections: &'a [S], selected: Option, diff --git a/src/widget/flex_row/layout.rs b/src/widget/flex_row/layout.rs index 22d679f7..d781e4f9 100644 --- a/src/widget/flex_row/layout.rs +++ b/src/widget/flex_row/layout.rs @@ -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( renderer: &Renderer, limits: &Limits, @@ -13,79 +19,153 @@ pub fn resolve( padding: Padding, column_spacing: f32, row_spacing: f32, + min_item_width: Option, + justify_items: Option, + align_items: Option, + justify_content: Option, 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::::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) } diff --git a/src/widget/flex_row/mod.rs b/src/widget/flex_row/mod.rs index 95161090..4f546527 100644 --- a/src/widget/flex_row/mod.rs +++ b/src/widget/flex_row/mod.rs @@ -1,6 +1,8 @@ // Copyright 2023 System76 // 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(children: Vec>) -> FlexRow { FlexRow::new(children) } diff --git a/src/widget/flex_row/widget.rs b/src/widget/flex_row/widget.rs index 02873e6e..b89397f0 100644 --- a/src/widget/flex_row/widget.rs +++ b/src/widget/flex_row/widget.rs @@ -17,6 +17,7 @@ pub struct FlexRow<'a, Message> { #[setters(skip)] children: Vec>, /// 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, /// Sets the max width max_width: f32, + /// Defines how content will be aligned horizontally. + #[setters(skip)] + align_items: Option, + /// Defines how content will be aligned vertically. + #[setters(skip)] + justify_items: Option, + /// Defines how the content will be justified. + #[setters(into)] + justify_content: Option, } impl<'a, Message> FlexRow<'a, Message> { - pub const fn new(children: Vec>) -> Self { + pub(crate) const fn new(children: Vec>) -> 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 @@ -75,6 +119,10 @@ impl<'a, Message: 'static + Clone> Widget 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, ) } diff --git a/src/widget/grid/layout.rs b/src/widget/grid/layout.rs index 9a193010..6423c377 100644 --- a/src/widget/grid/layout.rs +++ b/src/widget/grid/layout.rs @@ -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( renderer: &Renderer, @@ -23,6 +24,7 @@ pub fn resolve( padding: Padding, column_alignment: Alignment, row_alignment: Alignment, + justify_content: Option, column_spacing: f32, row_spacing: f32, tree: &mut [Tree], @@ -44,23 +46,29 @@ pub fn resolve( 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( _ => 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( }, }), + justify_content, + padding: Rect { left: length(padding.left), right: length(padding.right), @@ -134,7 +146,7 @@ pub fn resolve( 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( 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( } 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) } diff --git a/src/widget/grid/mod.rs b/src/widget/grid/mod.rs index e561a22a..f4c8c652 100644 --- a/src/widget/grid/mod.rs +++ b/src/widget/grid/mod.rs @@ -1,12 +1,14 @@ // Copyright 2023 System76 // 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() } diff --git a/src/widget/grid/widget.rs b/src/widget/grid/widget.rs index 6c16b79f..7ba1cfcc 100644 --- a/src/widget/grid/widget.rs +++ b/src/widget/grid/widget.rs @@ -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, /// 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 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, diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 6d47d2d5..2fd0279a 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -1,10 +1,11 @@ // Copyright 2022 System76 // 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, - /// Focused state of the window - focused: bool, - /// Elements packed at the start of the headerbar. #[setters(skip)] start: Vec>, @@ -64,6 +63,13 @@ pub struct HeaderBar<'a, Message> { /// Elements packed at the end of the headerbar. #[setters(skip)] end: Vec>, + + /// Controls the density of the headerbar. + #[setters(strip_option)] + density: Option, + + /// 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 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() diff --git a/src/widget/icon/mod.rs b/src/widget/icon/mod.rs index 783e47ca..752125fe 100644 --- a/src/widget/icon/mod.rs +++ b/src/widget/icon/mod.rs @@ -69,7 +69,7 @@ impl Icon { } #[must_use] - fn into_element(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 From for Element<'static, Message> { +impl<'a, Message: 'a> From for Element<'a, Message> { fn from(icon: Icon) -> Self { - icon.into_element::() + icon.view::() } } diff --git a/src/widget/id_container.rs b/src/widget/id_container.rs new file mode 100644 index 00000000..90511ff3 --- /dev/null +++ b/src/widget/id_container.rs @@ -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>, + Theme: iced_style::container::StyleSheet, + ::Style: From, +{ + 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(content: T, id: Id) -> Self + where + T: Into>, + { + IdContainer { + content: content.into(), + id, + } + } +} + +impl<'a, Message, Theme, Renderer> Widget + for IdContainer<'a, Message, Theme, Renderer> +where + Renderer: iced_core::Renderer, +{ + fn children(&self) -> Vec { + 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 { + 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, + >, + ) { + 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> { + 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 { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: crate::widget::Id) { + self.id = id; + } +} + +impl<'a, Message, Theme, Renderer> From> + 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) + } +} diff --git a/src/widget/menu.rs b/src/widget/menu.rs index cd30a17c..a4deb345 100644 --- a/src/widget/menu.rs +++ b/src/widget/menu.rs @@ -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; diff --git a/src/widget/menu/action.rs b/src/widget/menu/action.rs index 94da8639..1b70209a 100644 --- a/src/widget/menu/action.rs +++ b/src/widget/menu/action.rs @@ -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) -> 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) -> Self::Message; + fn message(&self) -> Self::Message; } diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 14192b40..c191ac0d 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -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( + menu_roots: Vec>, +) -> MenuBar { + 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); } } diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 74c44606..e0d22757 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -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 diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 51870c92..2da9bae1 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -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::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>, { fn find_key(action: &A, key_binds: &HashMap) -> 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::::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::::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), )); } diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 501eb351..a1b6cb51 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -1,70 +1,160 @@ // Copyright 2022 System76 // 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(children: Vec>) -> Column { 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(children: Vec>) -> Row { 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::*; diff --git a/src/widget/nav_bar_toggle.rs b/src/widget/nav_bar_toggle.rs index cd1ec67f..e39144ca 100644 --- a/src/widget/nav_bar_toggle.rs +++ b/src/widget/nav_bar_toggle.rs @@ -42,13 +42,9 @@ impl<'a, Message: 'static + Clone> From> 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() } } diff --git a/src/widget/segmented_button/model/builder.rs b/src/widget/segmented_button/model/builder.rs index 87d25adb..df59bd4d 100644 --- a/src/widget/segmented_button/model/builder.rs +++ b/src/widget/segmented_button/model/builder.rs @@ -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) -> Self { + self.model.0.icon_set(self.id, icon.into()); self } diff --git a/src/widget/segmented_button/model/entity.rs b/src/widget/segmented_button/model/entity.rs index c9f0997d..f23f7130 100644 --- a/src/widget/segmented_button/model/entity.rs +++ b/src/widget/segmented_button/model/entity.rs @@ -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) -> Self { + self.model.icon_set(self.id, icon.into()); self } diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index 73476cbf..d515624c 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -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(children: Vec>) -> Row { 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> + 'a, + widget: impl Into> + '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(children: Vec>) -> FlexRow { + 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>) -> Row<'a, Message> { + item_row(self.control_(widget)) + } + + /// Assigns a control which flexes. + pub fn flex_control(self, widget: impl Into>) -> FlexRow<'a, Message> { + flex_item_row(self.control_(widget)) + } + + fn control_(self, widget: impl Into>) -> Vec> { 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)) } } diff --git a/src/widget/settings/mod.rs b/src/widget/settings/mod.rs index b6c1de45..dedbc886 100644 --- a/src/widget/settings/mod.rs +++ b/src/widget/settings/mod.rs @@ -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}; diff --git a/src/widget/spin_button/mod.rs b/src/widget/spin_button/mod.rs index 62c1e12d..b8806d6e 100644 --- a/src/widget/spin_button/mod.rs +++ b/src/widget/spin_button/mod.rs @@ -1,6 +1,8 @@ // Copyright 2022 System76 // 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 Message + 'static>, } +/// A control for incremental adjustments of a value. pub fn spin_button<'a, Message: 'static>( label: impl Into>, on_change: impl Fn(model::Message) -> Message + 'static, diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index b8c0c839..5bf4f27a 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -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; + } } } }