From e10459fb375d6a84e4ad296df98b1af610fcc531 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 10 Feb 2026 15:37:41 -0500 Subject: [PATCH 001/116] wip rebase updates --- Cargo.toml | 21 +- cosmic-config/src/dbus.rs | 335 ++++++++++--------- cosmic-config/src/subscription.rs | 51 ++- examples/application/Cargo.toml | 1 + iced | 2 +- src/app/action.rs | 2 - src/app/cosmic.rs | 15 +- src/app/mod.rs | 96 ++++-- src/app/multi_window.rs | 244 -------------- src/applet/column.rs | 46 +-- src/applet/mod.rs | 37 +- src/applet/row.rs | 46 +-- src/applet/token/subscription.rs | 7 +- src/command.rs | 2 +- src/dbus_activation.rs | 121 +++---- src/executor/multi.rs | 4 + src/executor/single.rs | 4 + src/theme/portal.rs | 9 +- src/theme/style/iced.rs | 158 +++++++-- src/widget/about.rs | 4 +- src/widget/aspect_ratio.rs | 20 +- src/widget/autosize.rs | 31 +- src/widget/button/widget.rs | 89 +++-- src/widget/calendar.rs | 4 +- src/widget/color_picker/mod.rs | 64 ++-- src/widget/context_drawer/overlay.rs | 29 +- src/widget/context_drawer/widget.rs | 23 +- src/widget/context_menu.rs | 22 +- src/widget/dialog.rs | 6 +- src/widget/dnd_destination.rs | 108 +++--- src/widget/dnd_source.rs | 65 ++-- src/widget/dropdown/menu/mod.rs | 81 +++-- src/widget/dropdown/mod.rs | 16 +- src/widget/dropdown/multi/menu.rs | 63 ++-- src/widget/dropdown/multi/widget.rs | 61 ++-- src/widget/dropdown/operation.rs | 96 +++--- src/widget/dropdown/widget.rs | 111 +++--- src/widget/flex_row/layout.rs | 10 +- src/widget/flex_row/widget.rs | 37 +- src/widget/frames.rs | 70 ++-- src/widget/grid/layout.rs | 14 +- src/widget/grid/widget.rs | 37 +- src/widget/header_bar.rs | 27 +- src/widget/icon/mod.rs | 17 +- src/widget/id_container.rs | 29 +- src/widget/layer_container.rs | 18 +- src/widget/list/column.rs | 4 +- src/widget/menu/flex.rs | 34 +- src/widget/menu/menu_bar.rs | 39 +-- src/widget/menu/menu_inner.rs | 124 +++---- src/widget/menu/menu_tree.rs | 29 +- src/widget/mod.rs | 18 +- src/widget/nav_bar.rs | 1 + src/widget/popover.rs | 87 +++-- src/widget/radio.rs | 31 +- src/widget/rectangle_tracker/mod.rs | 18 +- src/widget/rectangle_tracker/subscription.rs | 8 +- src/widget/responsive_container.rs | 31 +- src/widget/segmented_button/widget.rs | 164 +++++---- src/widget/settings/item.rs | 5 +- src/widget/spin_button.rs | 1 + src/widget/table/widget/compact.rs | 1 + src/widget/table/widget/standard.rs | 1 + src/widget/text_input/input.rs | 230 +++++++------ src/widget/toaster/widget.rs | 53 +-- src/widget/warning.rs | 1 + src/widget/wayland/tooltip/widget.rs | 65 ++-- src/widget/wrapper.rs | 22 +- 68 files changed, 1776 insertions(+), 1544 deletions(-) delete mode 100644 src/app/multi_window.rs diff --git a/Cargo.toml b/Cargo.toml index 4aaf9d0a..62b8ee7c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,13 +8,27 @@ rust-version = "1.90" name = "cosmic" [features] -default = ["dbus-config", "multi-window", "a11y"] +# default = ["dbus-config", "multi-window", "a11y"] +default = [ "debug", + "winit", + "tokio", + # "xdg-portal", + "a11y", + "wgpu", + "single-instance", + "surface-message", + "dbus-config", + "x11", + "wayland", + "multi-window", + "about","animated-image","autosize", "dbus-config", "pipewire", "process", "rfd", "desktop", "desktop-systemd-scope", "serde-keycode", "qr_code", "markdown", "highlighter" +] # Accessibility support a11y = ["iced/a11y", "iced_accessibility"] # Enable about widget about = [] # Builds support for animated images -animated-image = ["dep:async-fs", "image/gif", "tokio?/io-util", "tokio?/fs"] +animated-image = ["dep:async-fs", "image/gif", "image/webp", "image/png", "tokio?/io-util", "tokio?/fs"] # XXX autosize should not be used on winit windows unless dialogs autosize = [] applet = [ @@ -76,7 +90,7 @@ wayland = [ ] surface-message = [] # multi-window support -multi-window = ["iced/multi-window"] +multi-window = [] # Render with wgpu wgpu = ["iced/wgpu", "iced_wgpu"] # X11 window support via winit @@ -96,6 +110,7 @@ async-std = [ "zbus?/async-io", "iced/async-std", ] +x11 = ["iced/x11", "iced_winit/x11"] [dependencies] apply = "0.3.0" diff --git a/cosmic-config/src/dbus.rs b/cosmic-config/src/dbus.rs index e9e3395c..da7bcb68 100644 --- a/cosmic-config/src/dbus.rs +++ b/cosmic-config/src/dbus.rs @@ -1,11 +1,11 @@ -use std::ops::Deref; +use std::{any::TypeId, ops::Deref}; use crate::{CosmicConfigEntry, Update}; use cosmic_settings_daemon::{Changed, ConfigProxy, CosmicSettingsDaemonProxy}; use futures_util::SinkExt; use iced_futures::{ Subscription, - futures::{self, Stream, StreamExt, future::pending}, + futures::{self, StreamExt, future::pending}, stream, }; @@ -57,6 +57,20 @@ impl Watcher { } } +#[derive(Clone)] +struct Wrapper( + TypeId, + CosmicSettingsDaemonProxy<'static>, + &'static str, + bool, +); + +impl std::hash::Hash for Wrapper { + fn hash(&self, state: &mut H) { + self.0.hash(state); + } +} + #[allow(clippy::too_many_lines)] pub fn watcher_subscription( settings_daemon: CosmicSettingsDaemonProxy<'static>, @@ -64,166 +78,185 @@ pub fn watcher_subscription iced_futures::Subscription> { let id = std::any::TypeId::of::(); - Subscription::run_with_id( - (id, config_id), - watcher_stream(settings_daemon, config_id, is_state), - ) -} + Subscription::run_with( + Wrapper(id, settings_daemon, config_id, is_state), + |&Wrapper(_, ref settings_daemon, ref config_id, ref is_state)| { + let is_state = *is_state; + let config_id = *config_id; + let settings_daemon = settings_daemon.clone(); + enum Change { + Changes(Changed), + OwnerChanged(bool), + } + stream::channel( + 5, + move |mut tx: futures::channel::mpsc::Sender>| async move { + let version = T::VERSION; -fn watcher_stream( - settings_daemon: CosmicSettingsDaemonProxy<'static>, - config_id: &'static str, - is_state: bool, -) -> impl Stream> { - enum Change { - Changes(Changed), - OwnerChanged(bool), - } - stream::channel(5, move |mut tx| async move { - let version = T::VERSION; + let Ok(cosmic_config) = (if is_state { + crate::Config::new_state(config_id, version) + } else { + crate::Config::new(config_id, version) + }) else { + pending::<()>().await; + unreachable!(); + }; - let Ok(cosmic_config) = (if is_state { - crate::Config::new_state(config_id, version) - } else { - crate::Config::new(config_id, version) - }) else { - pending::<()>().await; - unreachable!(); - }; + let mut attempts = 0; - let mut attempts = 0; + loop { + let watcher = if is_state { + Watcher::new_state(&settings_daemon, config_id, version).await + } else { + Watcher::new_config(&settings_daemon, config_id, version).await + }; + let Ok(watcher) = watcher else { + tracing::error!("Failed to create watcher for {config_id}"); - loop { - let watcher = if is_state { - Watcher::new_state(&settings_daemon, config_id, version).await - } else { - Watcher::new_config(&settings_daemon, config_id, version).await - }; - let Ok(watcher) = watcher else { - tracing::error!("Failed to create watcher for {config_id}"); + #[cfg(feature = "tokio")] + ::tokio::time::sleep(::tokio::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(feature = "async-std")] + async_std::task::sleep(std::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(not(any(feature = "tokio", feature = "async-std")))] + { + pending::<()>().await; + unreachable!(); + } + attempts += 1; + // The settings daemon has exited + continue; + }; + let Ok(changes) = watcher.receive_changed().await else { + tracing::error!("Failed to listen for changes for {config_id}"); - #[cfg(feature = "tokio")] - ::tokio::time::sleep(::tokio::time::Duration::from_secs(2_u64.pow(attempts))).await; - #[cfg(feature = "async-std")] - async_std::task::sleep(std::time::Duration::from_secs(2_u64.pow(attempts))).await; - #[cfg(not(any(feature = "tokio", feature = "async-std")))] - { - pending::<()>().await; - unreachable!(); - } - attempts += 1; - // The settings daemon has exited - continue; - }; - let Ok(changes) = watcher.receive_changed().await else { - tracing::error!("Failed to listen for changes for {config_id}"); + #[cfg(feature = "tokio")] + ::tokio::time::sleep(::tokio::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(feature = "async-std")] + async_std::task::sleep(std::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(not(any(feature = "tokio", feature = "async-std")))] + { + pending::<()>().await; + unreachable!(); + } + attempts += 1; + // The settings daemon has exited + continue; + }; - #[cfg(feature = "tokio")] - ::tokio::time::sleep(::tokio::time::Duration::from_secs(2_u64.pow(attempts))).await; - #[cfg(feature = "async-std")] - async_std::task::sleep(std::time::Duration::from_secs(2_u64.pow(attempts))).await; - #[cfg(not(any(feature = "tokio", feature = "async-std")))] - { - pending::<()>().await; - unreachable!(); - } - attempts += 1; - // The settings daemon has exited - continue; - }; + let mut changes = changes.map(Change::Changes).fuse(); - let mut changes = changes.map(Change::Changes).fuse(); + let Ok(owner_changed) = watcher.inner().receive_owner_changed().await + else { + tracing::error!("Failed to listen for owner changes for {config_id}"); + #[cfg(feature = "tokio")] + ::tokio::time::sleep(::tokio::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(feature = "async-std")] + async_std::task::sleep(std::time::Duration::from_secs( + 2_u64.pow(attempts), + )) + .await; + #[cfg(not(any(feature = "tokio", feature = "async-std")))] + { + pending::<()>().await; + unreachable!(); + } + attempts += 1; + // The settings daemon has exited + continue; + }; + let mut owner_changed = owner_changed + .map(|c| Change::OwnerChanged(c.is_some())) + .fuse(); - let Ok(owner_changed) = watcher.inner().receive_owner_changed().await else { - tracing::error!("Failed to listen for owner changes for {config_id}"); - #[cfg(feature = "tokio")] - ::tokio::time::sleep(::tokio::time::Duration::from_secs(2_u64.pow(attempts))).await; - #[cfg(feature = "async-std")] - async_std::task::sleep(std::time::Duration::from_secs(2_u64.pow(attempts))).await; - #[cfg(not(any(feature = "tokio", feature = "async-std")))] - { - pending::<()>().await; - unreachable!(); - } - attempts += 1; - // The settings daemon has exited - continue; - }; - let mut owner_changed = owner_changed - .map(|c| Change::OwnerChanged(c.is_some())) - .fuse(); + // update now, just in case we missed changes while setting up stream + let mut config = match T::get_entry(&cosmic_config) { + Ok(config) => config, + Err((errors, default)) => { + for why in &errors { + if why.is_err() { + if let crate::Error::GetKey(_, err) = &why { + if err.kind() == std::io::ErrorKind::NotFound { + // No system default config installed; don't error + continue; + } + } + tracing::error!("error getting config: {config_id} {why}"); + } + } + default + } + }; - // update now, just in case we missed changes while setting up stream - let mut config = match T::get_entry(&cosmic_config) { - Ok(config) => config, - Err((errors, default)) => { - for why in &errors { - if why.is_err() { - if let crate::Error::GetKey(_, err) = &why { - if err.kind() == std::io::ErrorKind::NotFound { - // No system default config installed; don't error - continue; + if let Err(err) = tx + .send(Update { + errors: Vec::new(), + keys: Vec::new(), + config: config.clone(), + }) + .await + { + tracing::error!("Failed to send config: {err}"); + } + + loop { + let change: Changed = futures::select! { + c = changes.next() => { + let Some(Change::Changes(c)) = c else { + break; + }; + c + } + c = owner_changed.next() => { + let Some(Change::OwnerChanged(cont)) = c else { + break; + }; + if cont { + continue; + } else { + // The settings daemon has exited + break; + } + }, + }; + + // Reset the attempts counter if we received a change + attempts = 0; + let Ok(args) = change.args() else { + // The settings daemon has exited + break; + }; + let (errors, keys) = config.update_keys(&cosmic_config, &[args.key]); + if !keys.is_empty() { + if let Err(err) = tx + .send(Update { + errors, + keys, + config: config.clone(), + }) + .await + { + tracing::error!("Failed to send config update: {err}"); } } - tracing::error!("error getting config: {config_id} {why}"); } } - default - } - }; - - if let Err(err) = tx - .send(Update { - errors: Vec::new(), - keys: Vec::new(), - config: config.clone(), - }) - .await - { - tracing::error!("Failed to send config: {err}"); - } - - loop { - let change: Changed = futures::select! { - c = changes.next() => { - let Some(Change::Changes(c)) = c else { - break; - }; - c - } - c = owner_changed.next() => { - let Some(Change::OwnerChanged(cont)) = c else { - break; - }; - if cont { - continue; - } else { - // The settings daemon has exited - break; - } - }, - }; - - // Reset the attempts counter if we received a change - attempts = 0; - let Ok(args) = change.args() else { - // The settings daemon has exited - break; - }; - let (errors, keys) = config.update_keys(&cosmic_config, &[args.key]); - if !keys.is_empty() { - if let Err(err) = tx - .send(Update { - errors, - keys, - config: config.clone(), - }) - .await - { - tracing::error!("Failed to send config update: {err}"); - } - } - } - } - }) + }, + ) + }, + ) } diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs index 45e021fe..d16b9b65 100644 --- a/cosmic-config/src/subscription.rs +++ b/cosmic-config/src/subscription.rs @@ -25,7 +25,24 @@ pub fn config_subscription< config_id: Cow<'static, str>, config_version: u64, ) -> iced_futures::Subscription> { - iced_futures::Subscription::run_with_id(id, watcher_stream(config_id, config_version, false)) + iced_futures::Subscription::run_with( + (id, config_id, config_version, false), + // FIXME there are type issues related to the 'static lifetime of the Cow if this is extracted to a named function... + |(_, config_id, config_version, is_state)| { + let config_id = config_id.clone(); + let config_version = *config_version; + let is_state = *is_state; + + stream::channel(100, move |mut output| async move { + let config_id = config_id.clone(); + let mut state = ConfigState::Init(config_id, config_version, is_state); + + loop { + state = start_listening::(state, &mut output).await; + } + }) + }, + ) } #[cold] @@ -37,25 +54,23 @@ pub fn config_state_subscription< config_id: Cow<'static, str>, config_version: u64, ) -> iced_futures::Subscription> { - iced_futures::Subscription::run_with_id(id, watcher_stream(config_id, config_version, true)) -} - -fn watcher_stream( - config_id: Cow<'static, str>, - config_version: u64, - is_state: bool, -) -> impl Stream> { - stream::channel(100, move |mut output| { - let config_id = config_id.clone(); - async move { + iced_futures::Subscription::run_with( + (id, config_id, config_version, true), + |(_, config_id, config_version, is_state)| { let config_id = config_id.clone(); - let mut state = ConfigState::Init(config_id, config_version, is_state); + let config_version = *config_version; + let is_state = *is_state; - loop { - state = start_listening::(state, &mut output).await; - } - } - }) + stream::channel(100, move |mut output| async move { + let config_id = config_id.clone(); + let mut state = ConfigState::Init(config_id, config_version, is_state); + + loop { + state = start_listening::(state, &mut output).await; + } + }) + }, + ) } async fn start_listening( diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index f05c0418..35ff3d30 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -23,4 +23,5 @@ features = [ "wgpu", "single-instance", "surface-message", + "multi-window", ] diff --git a/iced b/iced index d36e4df4..73369a18 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit d36e4df47f2e277fafcd3505229d53438c7f128d +Subproject commit 73369a18eb4069f3f3d1916fd1e17537ee87a587 diff --git a/src/app/action.rs b/src/app/action.rs index cbdd1a55..05fc7cbe 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -8,8 +8,6 @@ use crate::{config::CosmicTk, keyboard_nav}; #[cfg(feature = "wayland")] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; -#[cfg(not(any(feature = "multi-window", feature = "wayland")))] -use iced::Application as IcedApplication; /// A message managed internally by COSMIC. #[derive(Clone, Debug)] diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index bfda4a1d..edd7b157 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -15,7 +15,7 @@ use cosmic_theme::ThemeMode; use iced::Application as IcedApplication; #[cfg(feature = "wayland")] use iced::event::wayland; -use iced::{Task, window}; +use iced::{Task, theme, window}; use iced_futures::event::listen_with; #[cfg(feature = "wayland")] use iced_winit::SurfaceIdWrapper; @@ -397,15 +397,16 @@ where f64::from(self.app.core().scale_factor()) } - pub fn style(&self, theme: &Theme) -> iced_runtime::Appearance { + pub fn style(&self, theme: &Theme) -> theme::Style { if let Some(style) = self.app.style() { style } else if self.app.core().window.is_maximized { let theme = THEME.lock().unwrap(); - crate::style::iced::application::appearance(theme.borrow()) + crate::style::iced::application::style(theme.borrow()) } else { let theme = THEME.lock().unwrap(); - iced_runtime::Appearance { + + theme::Style { background_color: iced_core::Color::TRANSPARENT, icon_color: theme.cosmic().on_bg_color().into(), text_color: theme.cosmic().on_bg_color().into(), @@ -635,7 +636,7 @@ impl Cosmic { self.app.on_window_resize(id, width, height); //TODO: more efficient test of maximized (winit has no event for maximize if set by the OS) - return iced::window::get_maximized(id).map(move |maximized| { + return iced::window::is_maximized(id).map(move |maximized| { crate::Action::Cosmic(Action::WindowMaximized(id, maximized)) }); } @@ -711,10 +712,10 @@ impl Cosmic { Action::KeyboardNav(message) => match message { keyboard_nav::Action::FocusNext => { - return iced::widget::focus_next().map(crate::Action::Cosmic); + return iced::widget::operation::focus_next().map(crate::Action::Cosmic); } keyboard_nav::Action::FocusPrevious => { - return iced::widget::focus_previous().map(crate::Action::Cosmic); + return iced::widget::operation::focus_previous().map(crate::Action::Cosmic); } keyboard_nav::Action::Escape => return self.app.on_escape(), keyboard_nav::Action::Search => return self.app.on_search(), diff --git a/src/app/mod.rs b/src/app/mod.rs index 67636dac..1287dc27 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -11,9 +11,8 @@ pub use action::Action; use cosmic_config::CosmicConfigEntry; pub mod context_drawer; pub use context_drawer::{ContextDrawer, context_drawer}; +use iced::application::BootFn; pub mod cosmic; -#[cfg(all(feature = "winit", feature = "multi-window"))] -pub(crate) mod multi_window; pub mod settings; pub type Task = iced::Task>; @@ -21,12 +20,13 @@ pub type Task = iced::Task>; pub use crate::Core; use crate::prelude::*; use crate::theme::THEME; -use crate::widget::{container, horizontal_space, id_container, menu, nav_bar, popover}; +use crate::widget::{container, id_container, menu, nav_bar, popover, space}; use apply::Apply; -use iced::window; use iced::{Length, Subscription}; +use iced::{theme, window}; pub use settings::Settings; use std::borrow::Cow; +use std::{cell::RefCell, rc::Rc}; #[cold] pub(crate) fn iced_settings( @@ -72,7 +72,7 @@ pub(crate) fn iced_settings( core.exit_on_main_window_closed = exit_on_close; if let Some(border_size) = settings.resizable { - window_settings.resize_border = border_size as u32; + // window_settings.resize_border = border_size as u32; window_settings.resizable = true; } window_settings.decorations = !settings.client_decorations; @@ -82,7 +82,7 @@ pub(crate) fn iced_settings( window_settings.min_size = Some(min_size); } let max_size = settings.size_limits.max(); - if max_size != iced::Size::INFINITY { + if max_size != iced::Size::INFINITE { window_settings.max_size = Some(max_size); } @@ -90,6 +90,22 @@ pub(crate) fn iced_settings( (iced, (core, flags), window_settings) } +pub(crate) struct BootDataInner { + pub flags: A::Flags, + pub core: Core, +} + +pub(crate) struct BootData(pub Rc>>>); + +impl BootFn, crate::Action> + for BootData +{ + fn boot(&self) -> (cosmic::Cosmic, iced::Task>) { + let mut data = self.0.borrow_mut(); + let data = data.take().unwrap(); + cosmic::Cosmic::::init((data.core, data.flags)) + } +} /// Launch a COSMIC application with the given [`Settings`]. /// /// # Errors @@ -102,39 +118,50 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res } let default_font = settings.default_font; - let (settings, mut flags, window_settings) = iced_settings::(settings, flags); + let (settings, (mut core, flags), window_settings) = iced_settings::(settings, flags); #[cfg(not(feature = "multi-window"))] { - flags.0.main_window = Some(iced::window::Id::RESERVED); + core.main_window = Some(iced::window::Id::RESERVED); + iced::application( - cosmic::Cosmic::title, + BootData(Rc::new(RefCell::new(Some(BootDataInner:: { + flags, + core, + })))), cosmic::Cosmic::update, cosmic::Cosmic::view, ) .subscription(cosmic::Cosmic::subscription) + .title(cosmic::Cosmic::title) .style(cosmic::Cosmic::style) .theme(cosmic::Cosmic::theme) .window_size((500.0, 800.0)) .settings(settings) .window(window_settings) - .run_with(move || cosmic::Cosmic::::init(flags)) + .run() } #[cfg(feature = "multi-window")] { - let mut app = multi_window::multi_window::<_, _, _, _, App::Executor>( - cosmic::Cosmic::title, + let no_main_window = core.main_window.is_none(); + if no_main_window { + // app = app.window(window_settings); + core.main_window = Some(iced_core::window::Id::RESERVED); + } + let mut app = iced::daemon( + BootData(Rc::new(RefCell::new(Some(BootDataInner:: { + flags, + core, + })))), cosmic::Cosmic::update, cosmic::Cosmic::view, ); - if flags.0.main_window.is_none() { - app = app.window(window_settings); - flags.0.main_window = Some(iced_core::window::Id::RESERVED); - } + app.subscription(cosmic::Cosmic::subscription) + .title(cosmic::Cosmic::title) .style(cosmic::Cosmic::style) .theme(cosmic::Cosmic::theme) .settings(settings) - .run_with(move || cosmic::Cosmic::::init(flags)) + .run() } } @@ -204,13 +231,16 @@ where tracing::info!("Another instance is running"); Ok(()) } else { - let (settings, mut flags, window_settings) = iced_settings::(settings, flags); - flags.0.single_instance = true; + let (settings, (mut core, flags), window_settings) = iced_settings::(settings, flags); + core.single_instance = true; #[cfg(not(feature = "multi-window"))] { iced::application( - cosmic::Cosmic::title, + BootData(Rc::new(RefCell::new(Some(BootDataInner:: { + flags, + core, + })))), cosmic::Cosmic::update, cosmic::Cosmic::view, ) @@ -220,24 +250,30 @@ where .window_size((500.0, 800.0)) .settings(settings) .window(window_settings) - .run_with(move || cosmic::Cosmic::::init(flags)) + .run() } #[cfg(feature = "multi-window")] { - let mut app = multi_window::multi_window::<_, _, _, _, App::Executor>( - cosmic::Cosmic::title, + let no_main_window = core.main_window.is_none(); + if no_main_window { + // app = app.window(window_settings); + core.main_window = Some(iced_core::window::Id::RESERVED); + } + let mut app = iced::daemon( + BootData(Rc::new(RefCell::new(Some(BootDataInner:: { + flags, + core, + })))), cosmic::Cosmic::update, cosmic::Cosmic::view, ); - if flags.0.main_window.is_none() { - app = app.window(window_settings); - flags.0.main_window = Some(iced_core::window::Id::RESERVED); - } + app.subscription(cosmic::Cosmic::subscription) .style(cosmic::Cosmic::style) + .title(cosmic::Cosmic::title) .theme(cosmic::Cosmic::theme) .settings(settings) - .run_with(move || cosmic::Cosmic::::init(flags)) + .run() } } } @@ -428,7 +464,7 @@ where } /// Overrides the default style for applications - fn style(&self) -> Option { + fn style(&self) -> Option { None } @@ -667,7 +703,7 @@ impl ApplicationExt for App { ) } else { //TODO: this element is added to workaround state issues - widgets.push(horizontal_space().width(Length::Shrink).into()); + widgets.push(space::horizontal().width(Length::Shrink).into()); } } } diff --git a/src/app/multi_window.rs b/src/app/multi_window.rs deleted file mode 100644 index 65ac61f7..00000000 --- a/src/app/multi_window.rs +++ /dev/null @@ -1,244 +0,0 @@ -// Copyright 2024 System76 -// SPDX-License-Identifier: MPL-2.0 - -//! Create and run daemons that run in the background. -//! Copied from iced 0.13, but adds optional initial window - -use iced::application; -use iced::window; -use iced::{ - self, Program, - program::{self, with_style, with_subscription, with_theme, with_title}, - runtime::{Appearance, DefaultStyle}, -}; -use iced::{Element, Result, Settings, Subscription, Task}; - -use std::marker::PhantomData; - -pub(crate) struct Instance { - update: Update, - view: View, - _state: PhantomData, - _message: PhantomData, - _theme: PhantomData, - _renderer: PhantomData, - _executor: PhantomData, -} - -/// Creates an iced [`MultiWindow`] given its title, update, and view logic. -pub fn multi_window( - title: impl Title, - update: impl application::Update, - view: impl for<'a> self::View<'a, State, Message, Theme, Renderer>, -) -> MultiWindow> -where - State: 'static, - Message: Send + std::fmt::Debug + 'static, - Theme: Default + DefaultStyle, - Renderer: program::Renderer, - Executor: iced::Executor, -{ - use std::marker::PhantomData; - - impl Program - for Instance - where - Message: Send + std::fmt::Debug + 'static, - Theme: Default + DefaultStyle, - Renderer: program::Renderer, - Update: application::Update, - View: for<'a> self::View<'a, State, Message, Theme, Renderer>, - Executor: iced::Executor, - { - type State = State; - type Message = Message; - type Theme = Theme; - type Renderer = Renderer; - type Executor = Executor; - - fn update(&self, state: &mut Self::State, message: Self::Message) -> Task { - self.update.update(state, message).into() - } - - fn view<'a>( - &self, - state: &'a Self::State, - window: window::Id, - ) -> Element<'a, Self::Message, Self::Theme, Self::Renderer> { - self.view.view(state, window).into() - } - } - - MultiWindow { - raw: Instance { - update, - view, - _state: PhantomData, - _message: PhantomData, - _theme: PhantomData, - _renderer: PhantomData, - _executor: PhantomData::, - }, - settings: Settings::default(), - window: None, - } - .title(title) -} - -/// The underlying definition and configuration of an iced daemon. -/// -/// You can use this API to create and run iced applications -/// step by step—without coupling your logic to a trait -/// or a specific type. -/// -/// You can create a [`MultiWindow`] with the [`daemon`] helper. -#[derive(Debug)] -pub struct MultiWindow { - raw: P, - settings: Settings, - window: Option, -} - -impl MultiWindow

{ - #[cfg(any(feature = "winit", feature = "wayland"))] - /// Runs the [`MultiWindow`]. - /// - /// The state of the [`MultiWindow`] must implement [`Default`]. - /// If your state does not implement [`Default`], use [`run_with`] - /// instead. - /// - /// [`run_with`]: Self::run_with - pub fn run(self) -> Result - where - Self: 'static, - P::State: Default, - { - self.raw.run(self.settings, self.window) - } - - #[cfg(any(feature = "winit", feature = "wayland"))] - /// Runs the [`MultiWindow`] with a closure that creates the initial state. - pub fn run_with(self, initialize: I) -> Result - where - Self: 'static, - I: FnOnce() -> (P::State, Task) + 'static, - { - self.raw.run_with(self.settings, self.window, initialize) - } - - /// Sets the [`Settings`] that will be used to run the [`MultiWindow`]. - pub fn settings(self, settings: Settings) -> Self { - Self { settings, ..self } - } - - /// Sets the [`Title`] of the [`MultiWindow`]. - pub(crate) fn title( - self, - title: impl Title, - ) -> MultiWindow> { - MultiWindow { - raw: with_title(self.raw, move |state, window| title.title(state, window)), - settings: self.settings, - window: self.window, - } - } - - /// Sets the subscription logic of the [`MultiWindow`]. - pub fn subscription( - self, - f: impl Fn(&P::State) -> Subscription, - ) -> MultiWindow> { - MultiWindow { - raw: with_subscription(self.raw, f), - settings: self.settings, - window: self.window, - } - } - - /// Sets the theme logic of the [`MultiWindow`]. - pub fn theme( - self, - f: impl Fn(&P::State, window::Id) -> P::Theme, - ) -> MultiWindow> { - MultiWindow { - raw: with_theme(self.raw, f), - settings: self.settings, - window: self.window, - } - } - - /// Sets the style logic of the [`MultiWindow`]. - pub fn style( - self, - f: impl Fn(&P::State, &P::Theme) -> Appearance, - ) -> MultiWindow> { - MultiWindow { - raw: with_style(self.raw, f), - settings: self.settings, - window: self.window, - } - } - - /// Sets the window settings of the [`MultiWindow`]. - pub fn window(self, window: window::Settings) -> Self { - Self { - raw: self.raw, - settings: self.settings, - window: Some(window), - } - } -} - -/// The title logic of some [`MultiWindow`]. -/// -/// This trait is implemented both for `&static str` and -/// any closure `Fn(&State, window::Id) -> String`. -/// -/// This trait allows the [`daemon`] builder to take any of them. -pub trait Title { - /// Produces the title of the [`MultiWindow`]. - fn title(&self, state: &State, window: window::Id) -> String; -} - -impl Title for &'static str { - fn title(&self, _state: &State, _window: window::Id) -> String { - (*self).to_string() - } -} - -impl Title for T -where - T: Fn(&State, window::Id) -> String, -{ - fn title(&self, state: &State, window: window::Id) -> String { - self(state, window) - } -} - -/// The view logic of some [`MultiWindow`]. -/// -/// This trait allows the [`daemon`] builder to take any closure that -/// returns any `Into>`. -pub trait View<'a, State, Message, Theme, Renderer> { - /// Produces the widget of the [`MultiWindow`]. - fn view( - &self, - state: &'a State, - window: window::Id, - ) -> impl Into>; -} - -impl<'a, T, State, Message, Theme, Renderer, Widget> View<'a, State, Message, Theme, Renderer> for T -where - T: Fn(&'a State, window::Id) -> Widget, - State: 'static, - Widget: Into>, -{ - fn view( - &self, - state: &'a State, - window: window::Id, - ) -> impl Into> { - self(state, window) - } -} diff --git a/src/applet/column.rs b/src/applet/column.rs index 8fa2fa9f..8b3c68e9 100644 --- a/src/applet/column.rs +++ b/src/applet/column.rs @@ -217,7 +217,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -233,25 +233,26 @@ where self.padding, self.spacing, self.align, - &self.children, + &mut self.children, &mut tree.children, ) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.children - .iter() + .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .for_each(|((child, state), c_layout)| { - child.as_widget().operate( + child.as_widget_mut().operate( state, c_layout.with_virtual_offset(layout.virtual_offset()), renderer, @@ -261,17 +262,17 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let my_state = tree.state.downcast_mut::(); if let Some(hovered) = my_state.hovered { @@ -285,7 +286,7 @@ where e, mouse::Event::CursorLeft | mouse::Event::ButtonReleased { .. } ) { - return self.children[hovered].as_widget_mut().on_event( + return self.children[hovered].as_widget_mut().update( &mut tree.children[hovered], event, child_layout.with_virtual_offset(layout.virtual_offset()), @@ -302,7 +303,7 @@ where iced::core::touch::Event::FingerLifted { .. } | iced::core::touch::Event::FingerLost { .. } ) { - return self.children[hovered].as_widget_mut().on_event( + return self.children[hovered].as_widget_mut().update( &mut tree.children[hovered], event, child_layout.with_virtual_offset(layout.virtual_offset()), @@ -336,9 +337,9 @@ where ) && cursor.is_over(c_layout.bounds()) { my_state.hovered = Some(i); - return child.as_widget_mut().on_event( + return child.as_widget_mut().update( state, - event.clone(), + &event, c_layout.with_virtual_offset(layout.virtual_offset()), cursor_virtual, renderer, @@ -350,9 +351,9 @@ where cursor_virtual = mouse::Cursor::Unavailable; } - child.as_widget_mut().on_event( + child.as_widget_mut().update( state, - event.clone(), + &event, c_layout.with_virtual_offset(layout.virtual_offset()), cursor_virtual, renderer, @@ -360,8 +361,7 @@ where shell, viewport, ) - }) - .fold(event::Status::Ignored, event::Status::merge) + }); } fn mouse_interaction( @@ -436,11 +436,19 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer, translation) + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + viewport, + translation, + ) } #[cfg(feature = "a11y")] diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 0ab18817..ff376aab 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -1,7 +1,7 @@ #[cfg(feature = "applet-token")] pub mod token; -use crate::app::cosmic; +use crate::app::{BootData, BootDataInner, cosmic}; use crate::{ Application, Element, Renderer, app::iced_settings, @@ -18,17 +18,19 @@ use crate::{ self, autosize::{self, Autosize, autosize}, column::Column, - horizontal_space, layer_container, + layer_container, row::Row, - vertical_space, + space::horizontal, + space::vertical, }, }; pub use cosmic_panel_config; use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize}; use iced_core::{Padding, Shadow}; +use iced_runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; use iced_widget::Text; -use iced_widget::runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity}; +use std::cell::RefCell; use std::{borrow::Cow, num::NonZeroU32, rc::Rc, sync::LazyLock, time::Duration}; use tracing::info; @@ -386,6 +388,7 @@ impl Context { }, shadow: Shadow::default(), icon_color: Some(cosmic.background.on.into()), + snap: true, } }), ) @@ -567,30 +570,36 @@ pub fn run(flags: App::Flags) -> iced::Result { window_settings.decorations = false; window_settings.exit_on_close_request = true; window_settings.resizable = false; - window_settings.resize_border = 0; + // window_settings.resize_border = 0; // TODO make multi-window not mandatory - let mut app = super::app::multi_window::multi_window::<_, _, _, _, App::Executor>( - cosmic::Cosmic::title, + let no_main_window = core.main_window.is_none(); + if no_main_window { + // TODO still apply window settings? + // window_settings = window_settings.clone(); + core.main_window = Some(iced_core::window::Id::RESERVED); + } + let mut app = iced::daemon( + BootData(Rc::new(RefCell::new(Some(BootDataInner:: { + flags, + core, + })))), cosmic::Cosmic::update, cosmic::Cosmic::view, ); - if core.main_window.is_none() { - app = app.window(window_settings.clone()); - core.main_window = Some(iced_core::window::Id::RESERVED); - } + app.subscription(cosmic::Cosmic::subscription) .style(cosmic::Cosmic::style) .theme(cosmic::Cosmic::theme) .settings(iced_settings) - .run_with(move || cosmic::Cosmic::::init((core, flags))) + .run() } #[must_use] -pub fn style() -> iced_runtime::Appearance { +pub fn style() -> iced::theme::Style { let theme = crate::theme::THEME.lock().unwrap(); - iced_runtime::Appearance { + iced::theme::Style { background_color: Color::from_rgba(0.0, 0.0, 0.0, 0.0), text_color: theme.cosmic().on_bg_color().into(), icon_color: theme.cosmic().on_bg_color().into(), diff --git a/src/applet/row.rs b/src/applet/row.rs index b5cf851f..2a770503 100644 --- a/src/applet/row.rs +++ b/src/applet/row.rs @@ -208,7 +208,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -222,25 +222,26 @@ where self.padding, self.spacing, self.align, - &self.children, + &mut self.children, &mut tree.children, ) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { self.children - .iter() + .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .for_each(|((child, state), c_layout)| { - child.as_widget().operate( + child.as_widget_mut().operate( state, c_layout.with_virtual_offset(layout.virtual_offset()), renderer, @@ -250,17 +251,17 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let my_state = tree.state.downcast_mut::(); if let Some(hovered) = my_state.hovered { @@ -274,7 +275,7 @@ where e, mouse::Event::CursorLeft | mouse::Event::ButtonReleased { .. } ) { - return self.children[hovered].as_widget_mut().on_event( + return self.children[hovered].as_widget_mut().update( &mut tree.children[hovered], event, child_layout.with_virtual_offset(layout.virtual_offset()), @@ -291,7 +292,7 @@ where iced::core::touch::Event::FingerLifted { .. } | iced::core::touch::Event::FingerLost { .. } ) { - return self.children[hovered].as_widget_mut().on_event( + return self.children[hovered].as_widget_mut().update( &mut tree.children[hovered], event, child_layout.with_virtual_offset(layout.virtual_offset()), @@ -326,9 +327,9 @@ where ) && cursor.is_over(c_layout.bounds()) { my_state.hovered = Some(i); - return child.as_widget_mut().on_event( + return child.as_widget_mut().update( state, - event.clone(), + &event, c_layout.with_virtual_offset(layout.virtual_offset()), cursor_virtual, renderer, @@ -340,9 +341,9 @@ where cursor_virtual = mouse::Cursor::Unavailable; } - child.as_widget_mut().on_event( + child.as_widget_mut().update( state, - event.clone(), + &event, c_layout.with_virtual_offset(layout.virtual_offset()), cursor_virtual, renderer, @@ -350,8 +351,7 @@ where shell, viewport, ) - }) - .fold(event::Status::Ignored, event::Status::merge) + }); } fn mouse_interaction( @@ -426,11 +426,19 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer, translation) + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + viewport, + translation, + ) } #[cfg(feature = "a11y")] diff --git a/src/applet/token/subscription.rs b/src/applet/token/subscription.rs index 706c0301..82763303 100644 --- a/src/applet/token/subscription.rs +++ b/src/applet/token/subscription.rs @@ -14,16 +14,15 @@ use super::wayland_handler::wayland_handler; pub fn activation_token_subscription( id: I, ) -> iced::Subscription { - Subscription::run_with_id( - id, + Subscription::run_with(id, |_| { stream::channel(50, move |mut output| async move { let mut state = State::Ready; loop { state = start_listening(state, &mut output).await; } - }), - ) + }) + }) } pub enum State { diff --git a/src/command.rs b/src/command.rs index 00684e55..1d6f635c 100644 --- a/src/command.rs +++ b/src/command.rs @@ -39,7 +39,7 @@ pub fn set_theme(theme: crate::Theme) -> iced::Task(id: window::Id) -> iced::Task> { - iced_runtime::window::change_mode(id, window::Mode::Windowed) + iced_runtime::window::set_mode(id, window::Mode::Windowed) } /// Toggles the windows' maximize state. diff --git a/src/dbus_activation.rs b/src/dbus_activation.rs index c8931dd4..99e2f9f0 100644 --- a/src/dbus_activation.rs +++ b/src/dbus_activation.rs @@ -16,75 +16,80 @@ use { #[cold] pub fn subscription() -> Subscription> { use iced_futures::futures::StreamExt; - iced_futures::Subscription::run_with_id( - TypeId::of::(), - iced::stream::channel(10, move |mut output| async move { - let mut single_instance: DbusActivation = DbusActivation::new(); - let mut rx = single_instance.rx(); - if let Ok(builder) = zbus::connection::Builder::session() { - let path: String = format!("/{}", App::APP_ID.replace('.', "/")); - if let Ok(conn) = builder.build().await { - // XXX Setup done this way seems to be more reliable. - // - // the docs for serve_at seem to imply it will replace the - // existing interface at the requested path, but it doesn't - // seem to work that way all the time. The docs for - // object_server().at() imply it won't replace the existing - // interface. - // - // request_name is used either way, with the builder or - // with the connection, but it must be done after the - // object server is setup. - if conn.object_server().at(path, single_instance).await != Ok(true) { - tracing::error!("Failed to serve dbus"); - std::process::exit(1); - } - if conn.request_name(App::APP_ID).await.is_err() { - tracing::error!("Failed to serve dbus"); - std::process::exit(1); - } + iced_futures::Subscription::run_with(TypeId::of::(), |_| { + iced::stream::channel( + 10, + move |mut output: Sender>| async move { + let mut single_instance: DbusActivation = DbusActivation::new(); + let mut rx = single_instance.rx(); + if let Ok(builder) = zbus::connection::Builder::session() { + let path: String = format!("/{}", App::APP_ID.replace('.', "/")); + if let Ok(conn) = builder.build().await { + // XXX Setup done this way seems to be more reliable. + // + // the docs for serve_at seem to imply it will replace the + // existing interface at the requested path, but it doesn't + // seem to work that way all the time. The docs for + // object_server().at() imply it won't replace the existing + // interface. + // + // request_name is used either way, with the builder or + // with the connection, but it must be done after the + // object server is setup. + if conn.object_server().at(path, single_instance).await != Ok(true) { + tracing::error!("Failed to serve dbus"); + std::process::exit(1); + } + if conn.request_name(App::APP_ID).await.is_err() { + tracing::error!("Failed to serve dbus"); + std::process::exit(1); + } - output - .send(crate::Action::Cosmic(crate::app::Action::DbusConnection( - conn.clone(), - ))) - .await; + output + .send(crate::Action::Cosmic(crate::app::Action::DbusConnection( + conn.clone(), + ))) + .await; - #[cfg(feature = "smol")] - let handle = { - std::thread::spawn(move || { - let conn_clone = _conn.clone(); + #[cfg(feature = "smol")] + let handle = { + std::thread::spawn(move || { + let conn_clone = _conn.clone(); - zbus::block_on(async move { - loop { - conn_clone.executor().tick().await; - } + zbus::block_on(async move { + loop { + conn_clone.executor().tick().await; + } + }) }) - }) - }; - while let Some(mut msg) = rx.next().await { - if let Some(token) = msg.activation_token.take() { - if let Err(err) = output - .send(crate::Action::Cosmic(crate::app::Action::Activate(token))) - .await + }; + while let Some(mut msg) = rx.next().await { + if let Some(token) = msg.activation_token.take() { + if let Err(err) = output + .send(crate::Action::Cosmic(crate::app::Action::Activate( + token, + ))) + .await + { + tracing::error!(?err, "Failed to send message"); + } + } + if let Err(err) = output.send(crate::Action::DbusActivation(msg)).await { tracing::error!(?err, "Failed to send message"); } } - if let Err(err) = output.send(crate::Action::DbusActivation(msg)).await { - tracing::error!(?err, "Failed to send message"); - } } + } else { + tracing::warn!("Failed to connect to dbus for single instance"); } - } else { - tracing::warn!("Failed to connect to dbus for single instance"); - } - loop { - iced::futures::pending!(); - } - }), - ) + loop { + iced::futures::pending!(); + } + }, + ) + }) } #[derive(Debug, Clone)] diff --git a/src/executor/multi.rs b/src/executor/multi.rs index 50aa111e..5536db54 100644 --- a/src/executor/multi.rs +++ b/src/executor/multi.rs @@ -26,4 +26,8 @@ impl iced::Executor for Executor { let _guard = self.0.enter(); f() } + + fn block_on(&self, future: impl Future) -> T { + self.0.block_on(future) + } } diff --git a/src/executor/single.rs b/src/executor/single.rs index aaa4f9f5..7c42ae84 100644 --- a/src/executor/single.rs +++ b/src/executor/single.rs @@ -30,4 +30,8 @@ impl iced::Executor for Executor { let _guard = self.0.enter(); f() } + + fn block_on(&self, future: impl Future) -> T { + self.0.block_on(future) + } } diff --git a/src/theme/portal.rs b/src/theme/portal.rs index f0c88c01..0154ff58 100644 --- a/src/theme/portal.rs +++ b/src/theme/portal.rs @@ -13,9 +13,8 @@ pub enum Desktop { #[cold] pub fn desktop_settings() -> iced_futures::Subscription { - iced_futures::Subscription::run_with_id( - std::any::TypeId::of::(), - stream::channel(10, |mut tx| { + iced_futures::Subscription::run(|| { + stream::channel(10, |mut tx: futures::channel::mpsc::Sender| { async move { let mut attempts = 0; loop { @@ -99,6 +98,6 @@ pub fn desktop_settings() -> iced_futures::Subscription { } } } - }), - ) + }) + }) } diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 937ee388..4633477d 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -7,6 +7,7 @@ use crate::theme::{CosmicComponent, TRANSPARENT_COMPONENT, Theme}; use cosmic_theme::composite::over; use iced::{ overlay::menu, + theme::Base, widget::{ button as iced_button, checkbox as iced_checkbox, combo_box, container as iced_container, pane_grid, pick_list, progress_bar, radio, rule, scrollable, @@ -15,7 +16,7 @@ use iced::{ }, }; use iced_core::{Background, Border, Color, Shadow, Vector}; -use iced_widget::{pane_grid::Highlight, text_editor, text_input}; +use iced_widget::{pane_grid::Highlight, scrollable::AutoScroll, text_editor, text_input}; use palette::WithAlpha; use std::rc::Rc; @@ -36,13 +37,13 @@ pub mod application { } } - pub fn appearance(theme: &Theme) -> Appearance { + pub fn style(theme: &Theme) -> iced::theme::Style { let cosmic = theme.cosmic(); - Appearance { - icon_color: cosmic.bg_color().into(), + iced::theme::Style { background_color: cosmic.bg_color().into(), text_color: cosmic.on_bg_color().into(), + icon_color: cosmic.bg_color().into(), } } } @@ -422,6 +423,7 @@ impl<'a> Container<'a> { ..Default::default() }, shadow: Shadow::default(), + snap: true, } } @@ -436,6 +438,7 @@ impl<'a> Container<'a> { ..Default::default() }, shadow: Shadow::default(), + snap: true, } } @@ -450,6 +453,7 @@ impl<'a> Container<'a> { ..Default::default() }, shadow: Shadow::default(), + snap: true, } } } @@ -493,6 +497,7 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), + snap: true, }, Container::List => { @@ -506,6 +511,7 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), + snap: true, } } @@ -552,6 +558,7 @@ impl iced_container::Catalog for Theme { .into(), ..Default::default() }, + snap: true, shadow: Shadow::default(), } } @@ -582,6 +589,7 @@ impl iced_container::Catalog for Theme { radius: cosmic.corner_radii.radius_s.into(), }, shadow: Shadow::default(), + snap: true, }, Container::Tooltip => iced_container::Style { @@ -593,6 +601,7 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), + snap: true, }, Container::Card => { @@ -610,6 +619,7 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), + snap: true, }, cosmic_theme::Layer::Primary => iced_container::Style { icon_color: Some(Color::from(cosmic.primary.component.on)), @@ -622,6 +632,7 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), + snap: true, }, cosmic_theme::Layer::Secondary => iced_container::Style { icon_color: Some(Color::from(cosmic.secondary.component.on)), @@ -634,6 +645,7 @@ impl iced_container::Catalog for Theme { ..Default::default() }, shadow: Shadow::default(), + snap: true, }, } } @@ -652,6 +664,7 @@ impl iced_container::Catalog for Theme { offset: Vector::new(0.0, 4.0), blur_radius: 16.0, }, + snap: true, }, } } @@ -791,6 +804,7 @@ impl menu::Catalog for Theme { }, selected_text_color: cosmic.accent_text_color().into(), selected_background: Background::Color(cosmic.background.component.hover.into()), + shadow: Default::default(), } } } @@ -830,7 +844,7 @@ impl pick_list::Catalog for Theme { background: Background::Color(cosmic.background.base.into()), ..appearance }, - pick_list::Status::Opened => appearance, + pick_list::Status::Opened { is_hovered: _ } => appearance, } } } @@ -920,6 +934,8 @@ impl toggler::Catalog for Theme { background_border_color: Color::TRANSPARENT, foreground_border_width: 0.0, foreground_border_color: Color::TRANSPARENT, + text_color: None, + padding_ratio: 0.0, }; match status { toggler::Status::Active { is_toggled } => active, @@ -942,9 +958,9 @@ impl toggler::Catalog for Theme { ..active } } - toggler::Status::Disabled => { - active.background.a /= 2.; - active.foreground.a /= 2.; + toggler::Status::Disabled { is_toggled } => { + active.background = active.background.scale_alpha(0.5); + active.foreground = active.foreground.scale_alpha(0.5); active } } @@ -1086,21 +1102,21 @@ impl rule::Catalog for Theme { match class { Rule::Default => rule::Style { color: self.current_container().divider.into(), - width: 1, radius: 0.0.into(), fill_mode: rule::FillMode::Full, + snap: true, }, Rule::LightDivider => rule::Style { color: self.current_container().divider.into(), - width: 1, radius: 0.0.into(), fill_mode: rule::FillMode::Padded(8), + snap: true, }, Rule::HeavyDivider => rule::Style { color: self.current_container().divider.into(), - width: 4, radius: 2.0.into(), fill_mode: rule::FillMode::Full, + snap: true, }, Rule::Custom(f) => f(self), } @@ -1126,7 +1142,10 @@ impl scrollable::Catalog for Theme { fn style(&self, class: &Self::Class<'_>, status: scrollable::Status) -> scrollable::Style { match status { - scrollable::Status::Active => { + scrollable::Status::Active { + is_horizontal_scrollbar_disabled, + is_vertical_scrollbar_disabled, + } => { let cosmic = self.cosmic(); let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); let neutral_6 = cosmic.palette.neutral_6.with_alpha(0.7); @@ -1139,7 +1158,7 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - color: if cosmic.is_dark { + background: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() @@ -1157,7 +1176,7 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - color: if cosmic.is_dark { + background: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() @@ -1169,6 +1188,13 @@ impl scrollable::Catalog for Theme { }, }, gap: None, + // TODO: what is auto scroll? + auto_scroll: AutoScroll { + background: Color::TRANSPARENT.into(), + border: Border::default(), + shadow: Shadow::default(), + icon: Color::TRANSPARENT.into(), + }, }; let small_widget_container = self.current_container().small_widget.with_alpha(0.7); @@ -1200,7 +1226,7 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - color: if cosmic.is_dark { + background: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() @@ -1218,7 +1244,7 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - color: if cosmic.is_dark { + background: if cosmic.is_dark { neutral_6.into() } else { neutral_5.into() @@ -1230,6 +1256,13 @@ impl scrollable::Catalog for Theme { }, }, gap: None, + // TODO: what is auto scroll? + auto_scroll: AutoScroll { + background: Color::TRANSPARENT.into(), + border: Border::default(), + shadow: Shadow::default(), + icon: Color::TRANSPARENT.into(), + }, }; if matches!(class, Scrollable::Permanent) { @@ -1400,7 +1433,7 @@ impl text_input::Catalog for Theme { }, } } - text_input::Status::Focused => { + text_input::Status::Focused { is_hovered } => { let bg = self.current_container().small_widget.with_alpha(0.25); match class { @@ -1477,7 +1510,8 @@ impl iced_widget::text_editor::Catalog for Theme { let selection = cosmic.accent.base.into(); let value = cosmic.palette.neutral_9.into(); let placeholder = cosmic.palette.neutral_9.with_alpha(0.7).into(); - let icon = cosmic.background.on.into(); + let icon: Color = cosmic.background.on.into(); + // TODO do we need to add icon color back? match status { iced_widget::text_editor::Status::Active @@ -1489,23 +1523,23 @@ impl iced_widget::text_editor::Catalog for Theme { width: f32::from(cosmic.space_xxxs()), color: iced::Color::from(cosmic.bg_divider()), }, - icon, - placeholder, - value, - selection, - }, - iced_widget::text_editor::Status::Focused => iced_widget::text_editor::Style { - background: iced::Color::from(cosmic.bg_color()).into(), - border: Border { - radius: cosmic.corner_radii.radius_0.into(), - width: f32::from(cosmic.space_xxxs()), - color: iced::Color::from(cosmic.accent.base), - }, - icon, placeholder, value, selection, }, + iced_widget::text_editor::Status::Focused { is_hovered } => { + iced_widget::text_editor::Style { + background: iced::Color::from(cosmic.bg_color()).into(), + border: Border { + radius: cosmic.corner_radii.radius_0.into(), + width: f32::from(cosmic.space_xxxs()), + color: iced::Color::from(cosmic.accent.base), + }, + placeholder, + value, + selection, + } + } } } } @@ -1522,6 +1556,21 @@ impl iced_widget::markdown::Catalog for Theme { } } +impl iced_widget::table::Catalog for Theme { + type Class<'a> = iced_widget::table::StyleFn<'a, Self>; + + fn default<'a>() -> Self::Class<'a> { + Box::new(|theme| iced_widget::table::Style { + separator_x: theme.current_container().divider.into(), + separator_y: theme.current_container().divider.into(), + }) + } + + fn style(&self, class: &Self::Class<'_>) -> iced_widget::table::Style { + class(self) + } +} + #[cfg(feature = "qr_code")] impl iced_widget::qr_code::Catalog for Theme { type Class<'a> = iced_widget::qr_code::StyleFn<'a, Self>; @@ -1539,3 +1588,50 @@ impl iced_widget::qr_code::Catalog for Theme { } impl combo_box::Catalog for Theme {} + +impl Base for Theme { + fn default(preference: iced::theme::Mode) -> Self { + match preference { + iced::theme::Mode::Light => Theme::light(), + iced::theme::Mode::Dark | iced::theme::Mode::None => Theme::dark(), + } + } + + fn mode(&self) -> iced::theme::Mode { + if self.theme_type.is_dark() { + iced::theme::Mode::Dark + } else { + iced::theme::Mode::Light + } + } + + fn base(&self) -> iced::theme::Style { + iced::theme::Style { + background_color: self.cosmic().bg_color().into(), + text_color: self.cosmic().on_bg_color().into(), + icon_color: self.cosmic().on_bg_color().into(), + } + } + + fn palette(&self) -> Option { + Some(iced::theme::Palette { + primary: self.cosmic().accent.base.into(), + success: self.cosmic().success.base.into(), + warning: self.cosmic().warning.base.into(), + danger: self.cosmic().destructive.base.into(), + background: iced::Color::from(self.cosmic().bg_color()), + text: iced::Color::from(self.cosmic().on_bg_color()), + }) + } + + fn name(&self) -> &str { + match &self.theme_type { + crate::theme::ThemeType::Dark => "Cosmic Dark Theme", + crate::theme::ThemeType::Light => "Cosmic Light Theme", + crate::theme::ThemeType::HighContrastDark => "Cosmic High Contrast Dark Theme", + crate::theme::ThemeType::HighContrastLight => "Cosmic High Contrast Light Theme", + crate::theme::ThemeType::Custom(theme) => "Custom Cosmic Theme", + crate::theme::ThemeType::System { prefer_dark, theme } => &theme.name, + } + } +} diff --git a/src/widget/about.rs b/src/widget/about.rs index 384aee4a..ba88e03a 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -1,7 +1,7 @@ use crate::{ Apply, Element, fl, iced::{Alignment, Length}, - widget::{self, horizontal_space}, + widget::{self, space}, }; #[derive(Debug, Default, Clone, derive_setters::Setters)] @@ -99,7 +99,7 @@ pub fn about<'a, Message: Clone + 'static>( let section_button = |name: &'a str, url: &'a str| -> Element<'a, Message> { widget::row() .push(widget::text(name)) - .push(horizontal_space()) + .push(space::horizontal()) .push_maybe( (!url.is_empty()).then_some(crate::widget::icon::from_name("link-symbolic").icon()), ) diff --git a/src/widget/aspect_ratio.rs b/src/widget/aspect_ratio.rs index e66c14d0..577bea95 100644 --- a/src/widget/aspect_ratio.rs +++ b/src/widget/aspect_ratio.rs @@ -2,7 +2,7 @@ use iced::Size; use iced::widget::Container; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::layout; use iced_core::mouse; use iced_core::overlay; @@ -172,7 +172,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -186,7 +186,7 @@ where } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -195,18 +195,18 @@ where self.container.operate(tree, layout, renderer, operation); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.container.on_event( + ) { + self.container.update( tree, event, layout, @@ -254,11 +254,13 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container.overlay(tree, layout, renderer, translation) + self.container + .overlay(tree, layout, renderer, viewport, translation) } #[cfg(feature = "a11y")] diff --git a/src/widget/autosize.rs b/src/widget/autosize.rs index 172d505f..6a1e6060 100644 --- a/src/widget/autosize.rs +++ b/src/widget/autosize.rs @@ -5,7 +5,7 @@ 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::widget::{Id, Operation, Tree}; use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; pub use iced_widget::container::{Catalog, Style}; @@ -115,7 +115,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -131,22 +131,23 @@ where } let node = self .content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, &my_limits); let size = node.size(); layout::Node::with_children(size, vec![node]) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, + operation: &mut dyn Operation, ) { - operation.container(Some(&self.id), layout.bounds(), &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], + operation.container(Some(&self.id), layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( + tree, layout .children() .next() @@ -158,17 +159,17 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { #[cfg(feature = "wayland")] if matches!( event, @@ -179,9 +180,9 @@ where let bounds = layout.bounds().size(); clipboard.request_logical_window_size(bounds.width.max(1.), bounds.height.max(1.)); } - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut tree.children[0], - event.clone(), + event, layout .children() .next() @@ -238,8 +239,9 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( @@ -250,6 +252,7 @@ where .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, + viewport, translation, ) } diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 87233330..54e29786 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -318,7 +318,7 @@ impl<'a, Message: 'a + Clone> Widget } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -331,21 +331,22 @@ impl<'a, Message: 'a + Clone> Widget self.padding, |renderer, limits| { self.content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) }, ) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn Operation<()>, ) { - operation.container(None, layout.bounds(), &mut |operation| { - self.content.as_widget().operate( + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( &mut tree.children[0], layout .children() @@ -357,20 +358,19 @@ impl<'a, Message: 'a + Clone> Widget ); }); let state = tree.state.downcast_mut::(); - operation.focusable(state, Some(&self.id)); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { if let Variant::Image { on_remove: Some(on_remove), .. @@ -383,7 +383,8 @@ impl<'a, Message: 'a + Clone> Widget if let Some(position) = cursor.position() { if removal_bounds(layout.bounds(), 4.0).contains(position) { shell.publish(on_remove.clone()); - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -391,10 +392,9 @@ impl<'a, Message: 'a + Clone> Widget _ => (), } } - - if self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut tree.children[0], - event.clone(), + event, layout .children() .next() @@ -405,9 +405,9 @@ impl<'a, Message: 'a + Clone> Widget clipboard, shell, viewport, - ) == event::Status::Captured - { - return event::Status::Captured; + ); + if shell.is_event_captured() { + return; } update( @@ -541,6 +541,7 @@ impl<'a, Message: 'a + Clone> Widget ..Default::default() }, shadow: Shadow::default(), + snap: true, }, selection_background, ); @@ -554,7 +555,7 @@ impl<'a, Message: 'a + Clone> Widget y: bounds.y + (bounds.height - 18.0 - styling.border_width), }; if bounds.intersects(viewport) { - iced_core::svg::Renderer::draw_svg(renderer, svg_handle, bounds); + iced_core::svg::Renderer::draw_svg(renderer, svg_handle, bounds, bounds); } } @@ -570,6 +571,7 @@ impl<'a, Message: 'a + Clone> Widget radius: c_rad.radius_m.into(), ..Default::default() }, + snap: true, }, selection_background, ); @@ -583,6 +585,12 @@ impl<'a, Message: 'a + Clone> Widget x: bounds.x + 4.0, y: bounds.y + 4.0, }, + Rectangle { + width: 16.0, + height: 16.0, + x: bounds.x + 4.0, + y: bounds.y + 4.0, + }, ); } } @@ -609,8 +617,9 @@ impl<'a, Message: 'a + Clone> Widget fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, mut translation: Vector, ) -> Option> { let position = layout.bounds().position(); @@ -624,6 +633,7 @@ impl<'a, Message: 'a + Clone> Widget .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, + viewport, translation, ) } @@ -638,7 +648,7 @@ impl<'a, Message: 'a + Clone> Widget ) -> iced_accessibility::A11yTree { use iced_accessibility::{ A11yNode, A11yTree, - accesskit::{Action, DefaultActionVerb, NodeBuilder, NodeId, Rect, Role}, + accesskit::{Action, Node, NodeId, Rect, Role}, }; // TODO why is state None sometimes? if matches!(state.state, iced_core::widget::tree::State::None) { @@ -658,12 +668,12 @@ impl<'a, Message: 'a + Clone> Widget let bounds = Rect::new(x as f64, y as f64, (x + width) as f64, (y + height) as f64); let is_hovered = state.state.downcast_ref::().is_hovered; - let mut node = NodeBuilder::new(Role::Button); + let mut node = Node::new(Role::Button); node.add_action(Action::Focus); - node.add_action(Action::Default); + node.add_action(Action::Click); node.set_bounds(bounds); if let Some(name) = self.name.as_ref() { - node.set_name(name.clone()); + node.set_label(name.clone()); } match self.description.as_ref() { Some(iced_accessibility::Description::Id(id)) => { @@ -682,10 +692,10 @@ impl<'a, Message: 'a + Clone> Widget if self.on_press.is_none() { node.set_disabled(); } - if is_hovered { - node.set_hovered(); - } - node.set_default_action_verb(DefaultActionVerb::Click); + // TODO hover + // if is_hovered { + // node.set_hovered(); + // } if let Some(child_tree) = child_tree.map(|child_tree| { self.content.as_widget().a11y_nodes( @@ -761,14 +771,14 @@ impl State { #[allow(clippy::needless_pass_by_value, clippy::too_many_arguments)] pub fn update<'a, Message: Clone>( _id: Id, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, on_press: Option<&dyn Fn(Vector, Rectangle) -> Message>, on_press_down: Option<&dyn Fn(Vector, Rectangle) -> Message>, state: impl FnOnce() -> &'a mut State, -) -> event::Status { +) { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -787,7 +797,8 @@ pub fn update<'a, Message: Clone>( shell.publish(msg); } - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -806,7 +817,8 @@ pub fn update<'a, Message: Clone>( shell.publish(msg); } - return event::Status::Captured; + shell.capture_event(); + return; } } else if on_press_down.is_some() { let state = state(); @@ -816,7 +828,7 @@ pub fn update<'a, Message: Clone>( #[cfg(feature = "a11y")] Event::A11y(event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => { let state = state(); - if let Some(on_press) = matches!(action, iced_accessibility::accesskit::Action::Default) + if let Some(on_press) = matches!(action, iced_accessibility::accesskit::Action::Click) .then_some(on_press) .flatten() { @@ -825,17 +837,19 @@ pub fn update<'a, Message: Clone>( shell.publish(msg); } - return event::Status::Captured; + shell.capture_event(); + return; } Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { if let Some(on_press) = on_press { let state = state(); - if state.is_focused && key == keyboard::Key::Named(keyboard::key::Named::Enter) { + if state.is_focused && *key == keyboard::Key::Named(keyboard::key::Named::Enter) { state.is_pressed = true; let msg = (on_press)(layout.virtual_offset(), layout.bounds()); shell.publish(msg); - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -846,8 +860,6 @@ pub fn update<'a, Message: Clone>( } _ => {} } - - event::Status::Ignored } #[allow(clippy::too_many_arguments)] @@ -879,6 +891,7 @@ pub fn draw( radius: styling.border_radius, }, shadow: Shadow::default(), + snap: true, }, Color::TRANSPARENT, ); @@ -900,6 +913,7 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), + snap: true, }, Background::Color([0.0, 0.0, 0.0, 0.5].into()), ); @@ -915,6 +929,7 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), + snap: true, }, background, ); @@ -930,6 +945,7 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), + snap: true, }, overlay, ); @@ -953,6 +969,7 @@ pub fn draw( radius: styling.border_radius, }, shadow: Shadow::default(), + snap: true, }, Color::TRANSPARENT, ); diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index ea10fddb..7c09d39c 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -213,7 +213,9 @@ where let content_list = column::with_children([ row::with_children([ column().push(date).push(day).into(), - crate::widget::Space::with_width(Length::Fill).into(), + crate::widget::space::horizontal() + .width(Length::Fill) + .into(), month_controls.into(), ]) .align_y(Vertical::Center) diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 40a4a940..d484bb62 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -26,7 +26,10 @@ use iced_core::{ }; use iced_widget::slider::HandleShape; -use iced_widget::{Row, canvas, column, horizontal_space, row, scrollable, vertical_space}; +use iced_widget::{ + Row, canvas, column, row, scrollable, + space::{horizontal, vertical}, +}; use palette::{FromColor, RgbHue}; use super::divider::horizontal; @@ -334,7 +337,7 @@ where .width(self.width), // canvas with gradient for the current color // still needs the canvas and the handle to be drawn on it - container(vertical_space().height(self.height)) + container(vertical().height(self.height)) .width(self.width) .height(self.height), slider( @@ -548,13 +551,13 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, ) -> layout::Node { self.inner - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } @@ -657,6 +660,7 @@ where radius: (1.0 + handle_radius).into(), }, shadow: Shadow::default(), + snap: true, }, Color::TRANSPARENT, ); @@ -674,6 +678,7 @@ where radius: handle_radius.into(), }, shadow: Shadow::default(), + snap: true, }, Color::TRANSPARENT, ); @@ -684,26 +689,31 @@ where fn overlay<'b>( &'b mut self, state: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.inner - .as_widget_mut() - .overlay(&mut state.children[0], layout, renderer, translation) + self.inner.as_widget_mut().overlay( + &mut state.children[0], + layout, + renderer, + viewport, + translation, + ) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { // if the pointer is performing a drag, intercept pointer motion and button events // else check if event is handled by child elements // if the event is not handled by a child element, check if it is over the canvas when pressing a button @@ -732,24 +742,26 @@ where shell.publish((self.on_update)(ColorPickerUpdate::ActionFinished)); state.dragging = false; } - _ => return event::Status::Ignored, + _ => return, }; - return event::Status::Captured; + shell.capture_event(); + return; } let column_tree = &mut tree.children[0]; - if self.inner.as_widget_mut().on_event( + self.inner.as_widget_mut().update( column_tree, - event.clone(), + &event, column_layout, cursor, renderer, clipboard, shell, viewport, - ) == event::Status::Captured - { - return event::Status::Captured; + ); + if shell.is_event_captured() { + shell.capture_event(); + return; } match event { @@ -764,12 +776,10 @@ where state.dragging = true; let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v); shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv))); - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } - _ => event::Status::Ignored, + _ => {} } } @@ -812,12 +822,12 @@ pub fn color_button<'a, Message: Clone + 'static>( let spacing = THEME.lock().unwrap().cosmic().spacing; button::custom(if color.is_some() { - Element::from(vertical_space().height(Length::Fixed(f32::from(spacing.space_s)))) + Element::from(vertical().height(Length::Fixed(f32::from(spacing.space_s)))) } else { Element::from(column![ - vertical_space().height(Length::FillPortion(6)), + vertical().height(Length::FillPortion(6)), row![ - horizontal_space().width(Length::FillPortion(6)), + horizontal().width(Length::FillPortion(6)), Icon::from( icon::from_name("list-add-symbolic") .prefer_svg(true) @@ -827,11 +837,11 @@ pub fn color_button<'a, Message: Clone + 'static>( .width(icon_portion) .height(Length::Fill) .content_fit(iced_core::ContentFit::Contain), - horizontal_space().width(Length::FillPortion(6)), + horizontal().width(Length::FillPortion(6)), ] .height(icon_portion) .width(Length::Fill), - vertical_space().height(Length::FillPortion(6)), + vertical().height(Length::FillPortion(6)), ]) }) .width(Length::Fixed(f32::from(spacing.space_s))) diff --git a/src/widget/context_drawer/overlay.rs b/src/widget/context_drawer/overlay.rs index 4f72e113..eef9183b 100644 --- a/src/widget/context_drawer/overlay.rs +++ b/src/widget/context_drawer/overlay.rs @@ -7,7 +7,7 @@ use iced::advanced::layout::{self, Layout}; use iced::advanced::widget::{self, Operation}; use iced::advanced::{Clipboard, Shell}; use iced::advanced::{overlay, renderer}; -use iced::{Event, Point, Rectangle, Size, event, mouse}; +use iced::{Event, Point, Size, mouse}; use iced_core::Renderer; pub(super) struct Overlay<'a, 'b, Message> { @@ -29,7 +29,7 @@ where let node = self .content - .as_widget() + .as_widget_mut() .layout(self.tree, renderer, &limits); let node_size = node.size(); @@ -47,16 +47,16 @@ where }) } - fn on_event( + fn update( &mut self, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.content.as_widget_mut().on_event( + ) { + self.content.as_widget_mut().update( self.tree, event, layout, @@ -104,9 +104,10 @@ where &self, layout: Layout<'_>, cursor: mouse::Cursor, - viewport: &Rectangle, renderer: &crate::Renderer, ) -> mouse::Interaction { + // TODO how to handle viewport here? + let viewport = &layout.bounds(); self.content .as_widget() .mouse_interaction(self.tree, layout, cursor, viewport, renderer) @@ -114,11 +115,17 @@ where fn overlay<'c>( &'c mut self, - layout: Layout<'_>, + layout: Layout<'c>, renderer: &crate::Renderer, ) -> Option> { - self.content - .as_widget_mut() - .overlay(self.tree, layout, renderer, iced::Vector::default()) + let viewport = &layout.bounds(); + + self.content.as_widget_mut().overlay( + self.tree, + layout, + renderer, + viewport, + iced::Vector::default(), + ) } } diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index 5366832f..e7ca5dab 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -7,7 +7,7 @@ use crate::{Apply, Element, Renderer, Theme, fl}; use std::borrow::Cow; use iced_core::Alignment; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::{Operation, Tree}; use iced_core::{ Clipboard, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, @@ -65,7 +65,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { } else { let title = title .map(|title| text::title4(title).width(Length::Fill).apply(Element::from)) - .unwrap_or_else(|| widget::horizontal_space().apply(Element::from)); + .unwrap_or_else(|| widget::space::horizontal().apply(Element::from)); (title, None) }; @@ -196,40 +196,40 @@ impl Widget for ContextDrawer<' } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { self.content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation<()>, ) { self.content - .as_widget() + .as_widget_mut() .operate(&mut tree.children[0], layout, renderer, operation); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.content.as_widget_mut().on_event( + ) { + self.content.as_widget_mut().update( &mut tree.children[0], event, layout, @@ -282,8 +282,9 @@ impl Widget for ContextDrawer<' fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, _renderer: &Renderer, + _viewport: &Rectangle, translation: Vector, ) -> Option> { let bounds = layout.bounds(); diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index d9dc529a..008660a7 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -270,13 +270,13 @@ impl Widget } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &iced_core::layout::Limits, ) -> iced_core::layout::Node { self.content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } @@ -302,29 +302,29 @@ impl Widget } fn operate( - &self, + &mut self, tree: &mut Tree, layout: iced_core::Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { self.content - .as_widget() + .as_widget_mut() .operate(&mut tree.children[0], layout, renderer, operation); } #[allow(clippy::too_many_lines)] - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: iced::Event, + event: &iced::Event, layout: iced_core::Layout<'_>, cursor: iced_core::mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn iced_core::Clipboard, shell: &mut iced_core::Shell<'_, Message>, viewport: &iced::Rectangle, - ) -> iced_core::event::Status { + ) { let state = tree.state.downcast_mut::(); let bounds = layout.bounds(); @@ -384,7 +384,7 @@ impl Widget match event { Event::Touch(touch::Event::FingerPressed { id, .. }) => { - state.fingers_pressed.insert(id); + state.fingers_pressed.insert(*id); } Event::Touch(touch::Event::FingerLifted { id, .. }) => { @@ -410,7 +410,8 @@ impl Widget self.create_popup(layout, cursor, renderer, shell, viewport, state); } - return event::Status::Captured; + shell.capture_event(); + return; } else if !was_open && right_button_released(&event) || (touch_lifted(&event)) || left_button_released(&event) @@ -440,7 +441,7 @@ impl Widget }); } } - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut tree.children[0], event, layout, @@ -457,6 +458,7 @@ impl Widget tree: &'b mut Tree, layout: iced_core::Layout<'_>, _renderer: &crate::Renderer, + viewport: &iced::Rectangle, translation: Vector, ) -> Option> { #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] diff --git a/src/widget/dialog.rs b/src/widget/dialog.rs index ba5b55e2..7d084626 100644 --- a/src/widget/dialog.rs +++ b/src/widget/dialog.rs @@ -123,7 +123,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes if let Some(body) = dialog.body { if should_space { content_col = content_col - .push(widget::vertical_space().height(Length::Fixed(space_xxs.into()))); + .push(widget::space::vertical().height(Length::Fixed(space_xxs.into()))); } content_col = content_col.push( widget::container(widget::scrollable(widget::text::body(body))).max_height(300.), @@ -133,7 +133,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes for control in dialog.controls { if should_space { content_col = content_col - .push(widget::vertical_space().height(Length::Fixed(space_s.into()))); + .push(widget::space::vertical().height(Length::Fixed(space_s.into()))); } content_col = content_col.push(control); should_space = true; @@ -149,7 +149,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes if let Some(button) = dialog.tertiary_action { button_row = button_row.push(button); } - button_row = button_row.push(widget::horizontal_space()); + button_row = button_row.push(widget::space::horizontal()); if let Some(button) = dialog.secondary_action { button_row = button_row.push(button); } diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 7225e917..9faa2605 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -303,43 +303,43 @@ impl Widget } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, ) -> layout::Node { self.container - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: layout::Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { self.container - .as_widget() + .as_widget_mut() .operate(&mut tree.children[0], layout, renderer, operation); } #[allow(clippy::too_many_lines)] - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: layout::Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - let s = self.container.as_widget_mut().on_event( + ) { + let s = self.container.as_widget_mut().update( &mut tree.children[0], - event.clone(), + event, layout, cursor, renderer, @@ -347,8 +347,8 @@ impl Widget shell, viewport, ); - if matches!(s, event::Status::Captured) { - return event::Status::Captured; + if shell.is_event_captured() { + return; } let state = tree.state.downcast_mut::>(); @@ -367,23 +367,23 @@ impl Widget OfferEvent::Enter { x, y, mime_types, .. }, - )) if id == Some(my_id) => { + )) if *id == Some(my_id) => { if !self.mime_matches(&mime_types) { log::trace!( target: DND_DEST_LOG_TARGET, "offer enter id={my_id:?} ignored (mimes={mime_types:?} not in {:?})", self.mime_types ); - return event::Status::Ignored; + return; } log::trace!( target: DND_DEST_LOG_TARGET, "offer enter id={my_id:?} coords=({x},{y}) mimes={mime_types:?}" ); if let Some(msg) = state.on_enter( - x, - y, - mime_types, + *x, + *y, + mime_types.clone(), self.on_enter.as_ref().map(std::convert::AsRef::as_ref), (), ) { @@ -391,13 +391,13 @@ impl Widget } if self.forward_drag_as_cursor { #[allow(clippy::cast_possible_truncation)] - let drag_cursor = mouse::Cursor::Available((x as f32, y as f32).into()); + let drag_cursor = mouse::Cursor::Available((*x as f32, *y as f32).into()); let event = Event::Mouse(mouse::Event::CursorMoved { position: drag_cursor.position().unwrap(), }); - self.container.as_widget_mut().on_event( + self.container.as_widget_mut().update( &mut tree.children[0], - event, + &event, layout, drag_cursor, renderer, @@ -406,7 +406,8 @@ impl Widget viewport, ); } - return event::Status::Captured; + shell.capture_event(); + return; } Event::Dnd(DndEvent::Offer(_, OfferEvent::Leave)) => { log::trace!( @@ -423,9 +424,9 @@ impl Widget if self.forward_drag_as_cursor { let drag_cursor = mouse::Cursor::Unavailable; let event = Event::Mouse(mouse::Event::CursorLeft); - self.container.as_widget_mut().on_event( + self.container.as_widget_mut().update( &mut tree.children[0], - event, + &event, layout, drag_cursor, renderer, @@ -434,16 +435,16 @@ impl Widget viewport, ); } - return event::Status::Ignored; + return; } - Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if id == Some(my_id) => { + Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if *id == Some(my_id) => { log::trace!( target: DND_DEST_LOG_TARGET, "offer motion id={my_id:?} coords=({x},{y})" ); if let Some(msg) = state.on_motion( - x, - y, + *x, + *y, self.on_motion.as_ref().map(std::convert::AsRef::as_ref), self.on_enter.as_ref().map(std::convert::AsRef::as_ref), (), @@ -453,13 +454,13 @@ impl Widget if self.forward_drag_as_cursor { #[allow(clippy::cast_possible_truncation)] - let drag_cursor = mouse::Cursor::Available((x as f32, y as f32).into()); + let drag_cursor = mouse::Cursor::Available((*x as f32, *y as f32).into()); let event = Event::Mouse(mouse::Event::CursorMoved { position: drag_cursor.position().unwrap(), }); - self.container.as_widget_mut().on_event( + self.container.as_widget_mut().update( &mut tree.children[0], - event, + &event, layout, drag_cursor, renderer, @@ -468,7 +469,8 @@ impl Widget viewport, ); } - return event::Status::Captured; + shell.capture_event(); + return; } Event::Dnd(DndEvent::Offer(_, OfferEvent::LeaveDestination)) => { log::trace!( @@ -481,9 +483,9 @@ impl Widget { shell.publish(msg); } - return event::Status::Ignored; + return; } - Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if id == Some(my_id) => { + Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if *id == Some(my_id) => { log::trace!( target: DND_DEST_LOG_TARGET, "offer drop id={my_id:?}" @@ -493,27 +495,29 @@ impl Widget { shell.publish(msg); } - return event::Status::Captured; + shell.capture_event(); + return; } Event::Dnd(DndEvent::Offer(id, OfferEvent::SelectedAction(action))) - if id == Some(my_id) => + if *id == Some(my_id) => { log::trace!( target: DND_DEST_LOG_TARGET, "offer selected-action id={my_id:?} action={action:?}" ); if let Some(msg) = state.on_action_selected( - action, + *action, self.on_action_selected .as_ref() .map(std::convert::AsRef::as_ref), ) { shell.publish(msg); } - return event::Status::Captured; + shell.capture_event(); + return; } Event::Dnd(DndEvent::Offer(id, OfferEvent::Data { data, mime_type })) - if id == Some(my_id) => + if *id == Some(my_id) => { log::trace!( target: DND_DEST_LOG_TARGET, @@ -531,21 +535,28 @@ impl Widget } if let (Some(msg), ret) = state.on_data_received( - mime_type, - data, + mime_type.clone(), + data.clone(), self.on_data_received .as_ref() .map(std::convert::AsRef::as_ref), self.on_finish.as_ref().map(std::convert::AsRef::as_ref), ) { shell.publish(msg); - return ret; + if ret == event::Status::Captured { + log::trace!( + target: DND_DEST_LOG_TARGET, + "offer data id={my_id:?} captured" + ); + shell.capture_event(); + } + return; } - return event::Status::Captured; + shell.capture_event(); + return; } _ => {} } - event::Status::Ignored } fn mouse_interaction( @@ -589,13 +600,18 @@ impl Widget fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: layout::Layout<'_>, + layout: layout::Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container - .as_widget_mut() - .overlay(&mut tree.children[0], layout, renderer, translation) + self.container.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + viewport, + translation, + ) } fn drag_destinations( diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index f21f9670..c8627482 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -1,6 +1,6 @@ use std::any::Any; -use iced_core::window; +use iced_core::{widget::Operation, window}; use crate::{ Element, @@ -176,7 +176,7 @@ impl(); let node = self .container - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits); state.cached_bounds = node.bounds(); node } fn operate( - &self, + &mut self, tree: &mut Tree, layout: layout::Layout<'_>, renderer: &crate::Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, + operation: &mut dyn Operation, ) { - operation.custom((&mut tree.state) as &mut dyn Any, Some(&self.id)); - operation.container(Some(&self.id), layout.bounds(), &mut |operation| { - self.container - .as_widget() - .operate(&mut tree.children[0], layout, renderer, operation) + operation.container(Some(&self.id), layout.bounds()); + operation.traverse(&mut |operation| { + self.container.as_widget_mut().operate( + tree, + layout + .children() + .next() + .unwrap() + .with_virtual_offset(layout.virtual_offset()), + renderer, + operation, + ); }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: layout::Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - let ret = self.container.as_widget_mut().on_event( + ) { + let ret = self.container.as_widget_mut().update( &mut tree.children[0], - event.clone(), + &event, layout, cursor, renderer, @@ -238,14 +245,16 @@ impl { state.left_pressed_position = None; - return event::Status::Captured; + shell.capture_event(); + return; } mouse::Event::CursorMoved { .. } => { if let Some(position) = cursor.position() { @@ -277,7 +286,8 @@ impl return ret, @@ -288,7 +298,8 @@ impl( &'b mut self, tree: &'b mut Tree, - layout: layout::Layout<'_>, + layout: layout::Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container - .as_widget_mut() - .overlay(&mut tree.children[0], layout, renderer, translation) + self.container.as_widget_mut().overlay( + &mut tree.children[0], + layout, + renderer, + viewport, + translation, + ) } fn drag_destinations( diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index 3fd099b3..0c96c1c6 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -213,7 +213,7 @@ impl<'a, Message: Clone + 'a> Overlay<'a, Message> { } } - fn _layout(&self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { + fn _layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> layout::Node { let space_below = bounds.height - (self.position.y + self.target_height); let space_above = self.position.y; @@ -242,19 +242,19 @@ impl<'a, Message: Clone + 'a> Overlay<'a, Message> { }) } - fn _on_event( + fn _update( &mut self, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + ) { let bounds = layout.bounds(); self.state.with_data_mut(|tree| { - self.container.on_event( + self.container.update( tree, event, layout, cursor, renderer, clipboard, shell, &bounds, ) }) @@ -293,6 +293,7 @@ impl<'a, Message: Clone + 'a> Overlay<'a, Message> { radius: appearance.border_radius, }, shadow: Shadow::default(), + snap: true, }, appearance.background, ); @@ -311,26 +312,25 @@ impl<'a, Message: Clone + 'a> iced_core::Overlay, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { - self._on_event(event, layout, cursor, renderer, clipboard, shell) + ) { + self._update(event, layout, cursor, renderer, clipboard, shell) } fn mouse_interaction( &self, layout: Layout<'_>, cursor: mouse::Cursor, - viewport: &Rectangle, renderer: &crate::Renderer, ) -> mouse::Interaction { - self._mouse_interaction(layout, cursor, viewport, renderer) + self._mouse_interaction(layout, cursor, &layout.bounds(), renderer) } fn draw( @@ -353,7 +353,7 @@ impl<'a, Message: Clone + 'a> crate::widget::Widget crate::widget::Widget, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { - self._on_event(event, layout, cursor, renderer, clipboard, shell) + ) { + self._update(event, layout, cursor, renderer, clipboard, shell) } fn draw( @@ -435,7 +435,7 @@ where } fn layout( - &self, + &mut self, _tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -452,7 +452,7 @@ where let size = { let intrinsic = Size::new( 0.0, - (f32::from(text_line_height) + self.padding.vertical()) * self.options.len() as f32, + (f32::from(text_line_height) + self.padding.y()) * self.options.len() as f32, ); limits.resolve(Length::Fill, Length::Shrink, intrinsic) @@ -461,17 +461,17 @@ where layout::Node::new(size) } - fn on_event( + fn update( &mut self, _state: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { let hovered_guard = self.hovered_option.lock().unwrap(); @@ -481,7 +481,8 @@ where if let Some(close_on_selected) = self.close_on_selected.as_ref() { shell.publish(close_on_selected.clone()); } - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -493,7 +494,7 @@ where let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) - + self.padding.vertical(); + + self.padding.y(); let new_hovered_option = (cursor_position.y / option_height) as usize; let mut hovered_guard = self.hovered_option.lock().unwrap(); @@ -515,7 +516,7 @@ where let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) - + self.padding.vertical(); + + self.padding.y(); let mut hovered_guard = self.hovered_option.lock().unwrap(); *hovered_guard = Some((cursor_position.y / option_height) as usize); @@ -525,14 +526,13 @@ where if let Some(close_on_selected) = self.close_on_selected.as_ref() { shell.publish(close_on_selected.clone()); } - return event::Status::Captured; + shell.capture_event(); + return; } } } _ => {} } - - event::Status::Ignored } fn mouse_interaction( @@ -568,8 +568,8 @@ where let text_size = self .text_size .unwrap_or_else(|| text::Renderer::default_size(renderer).0); - let option_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))) - + self.padding.vertical(); + let option_height = + f32::from(self.text_line_height.to_absolute(Pixels(text_size))) + self.padding.y(); let offset = viewport.y - bounds.y; let start = (offset / option_height) as usize; @@ -605,6 +605,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, appearance.selected_background, ); @@ -614,16 +615,13 @@ where .color(appearance.selected_text_color) .border_radius(appearance.border_radius); - svg::Renderer::draw_svg( - renderer, - svg_handle, - Rectangle { - x: item_x + item_width - 16.0 - 8.0, - y: bounds.y + (bounds.height / 2.0 - 8.0), - width: 16.0, - height: 16.0, - }, - ); + let bounds = Rectangle { + x: item_x + item_width - 16.0 - 8.0, + y: bounds.y + (bounds.height / 2.0 - 8.0), + width: 16.0, + height: 16.0, + }; + svg::Renderer::draw_svg(renderer, svg_handle, bounds, bounds); (appearance.selected_text_color, crate::font::semibold()) } else if *hovered_guard == Some(i) { @@ -642,6 +640,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, appearance.hovered_background, ); @@ -678,8 +677,8 @@ where size: Pixels(text_size), line_height: self.text_line_height, font, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), diff --git a/src/widget/dropdown/mod.rs b/src/widget/dropdown/mod.rs index fa4184c4..b2d3fbed 100644 --- a/src/widget/dropdown/mod.rs +++ b/src/widget/dropdown/mod.rs @@ -56,12 +56,12 @@ pub fn popup_dropdown< dropdown } -/// Produces a [`Task`] that closes the [`Dropdown`]. -pub fn close(id: Id) -> iced_runtime::Task { - iced_runtime::task::effect(iced_runtime::Action::Widget(Box::new(operation::close(id)))) -} +// /// Produces a [`Task`] that closes the [`Dropdown`]. +// pub fn close(id: Id) -> iced_runtime::Task { +// iced_runtime::task::effect(iced_runtime::Action::Widget(Box::new(operation::close(id)))) +// } -/// Produces a [`Task`] that opens the [`Dropdown`]. -pub fn open(id: Id) -> iced_runtime::Task { - iced_runtime::task::effect(iced_runtime::Action::Widget(Box::new(operation::open(id)))) -} +// /// Produces a [`Task`] that opens the [`Dropdown`]. +// pub fn open(id: Id) -> iced_runtime::Task { +// iced_runtime::task::effect(iced_runtime::Action::Widget(Box::new(operation::open(id)))) +// } diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index 39e89ee2..0a761097 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -209,18 +209,18 @@ impl iced_core::Overlay for Ove }) } - fn on_event( + fn update( &mut self, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + ) { let bounds = layout.bounds(); - self.container.on_event( + self.container.update( self.state, event, layout, cursor, renderer, clipboard, shell, &bounds, ) } @@ -229,11 +229,10 @@ impl iced_core::Overlay for Ove &self, layout: Layout<'_>, cursor: mouse::Cursor, - viewport: &Rectangle, renderer: &crate::Renderer, ) -> mouse::Interaction { self.container - .mouse_interaction(self.state, layout, cursor, viewport, renderer) + .mouse_interaction(self.state, layout, cursor, &layout.bounds(), renderer) } fn draw( @@ -256,6 +255,7 @@ impl iced_core::Overlay for Ove radius: appearance.border_radius, }, shadow: Shadow::default(), + snap: true, }, appearance.background, ); @@ -287,7 +287,7 @@ where } fn layout( - &self, + &mut self, _tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -309,7 +309,7 @@ where ) }); - let vertical_padding = self.padding.vertical(); + let vertical_padding = self.padding.y(); let text_line_height = f32::from(text_line_height); let size = { @@ -328,17 +328,17 @@ where layout::Node::new(size) } - fn on_event( + fn update( &mut self, _state: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { let bounds = layout.bounds(); match event { @@ -346,7 +346,8 @@ where if cursor.is_over(bounds) { if let Some(item) = self.hovered_option.as_ref() { shell.publish((self.on_selected)(item.clone())); - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -361,7 +362,7 @@ where let heights = self .options - .element_heights(self.padding.vertical(), text_line_height); + .element_heights(self.padding.y(), text_line_height); let mut current_offset = 0.0; @@ -408,7 +409,7 @@ where let heights = self .options - .element_heights(self.padding.vertical(), text_line_height); + .element_heights(self.padding.y(), text_line_height); let mut current_offset = 0.0; @@ -446,8 +447,6 @@ where } _ => {} } - - event::Status::Ignored } fn mouse_interaction( @@ -490,7 +489,7 @@ where let text_line_height = f32::from(self.text_line_height.to_absolute(Pixels(text_size))); let visible_options = self.options.visible_options( - self.padding.vertical(), + self.padding.y(), text_line_height, offset, viewport.height, @@ -528,24 +527,23 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, appearance.selected_background, ); + let svg_bounds = Rectangle { + x: item_x + item_width - 16.0 - 8.0, + y: bounds.y + (bounds.height / 2.0 - 8.0), + width: 16.0, + height: 16.0, + }; + let svg_handle = svg::Svg::new(crate::widget::common::object_select().clone()) .color(appearance.selected_text_color) .border_radius(appearance.border_radius); - svg::Renderer::draw_svg( - renderer, - svg_handle, - Rectangle { - x: item_x + item_width - 16.0 - 8.0, - y: bounds.y + (bounds.height / 2.0 - 8.0), - width: 16.0, - height: 16.0, - }, - ); + svg::Renderer::draw_svg(renderer, svg_handle, svg_bounds, svg_bounds); (appearance.selected_text_color, crate::font::semibold()) } else if self.hovered_option.as_ref() == Some(item) { @@ -566,6 +564,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, appearance.hovered_background, ); @@ -590,8 +589,8 @@ where size: iced::Pixels(text_size), line_height: self.text_line_height, font, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), @@ -611,7 +610,7 @@ where }) .move_to(Point { x: bounds.x, - y: bounds.y + (self.padding.vertical() / 2.0) - 4.0, + y: bounds.y + (self.padding.y() / 2.0) - 4.0, }); Widget::::draw( @@ -640,8 +639,8 @@ where size: iced::Pixels(text_size), line_height: text::LineHeight::Absolute(Pixels(text_line_height + 4.0)), font: crate::font::default(), - horizontal_alignment: alignment::Horizontal::Center, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Center, + align_y: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index 43a0836f..a46c6dcc 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -78,7 +78,7 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -116,17 +116,17 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> ) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, _renderer: &crate::Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { update( &event, layout, @@ -183,8 +183,9 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &crate::Renderer, + _viewport: &Rectangle, translation: Vector, ) -> Option> { let state = tree.state.downcast_mut::>(); @@ -275,8 +276,8 @@ pub fn layout( size: iced::Pixels(text_size), line_height: text_line_height, font: font.unwrap_or_else(crate::font::default), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), @@ -314,7 +315,7 @@ pub fn update<'a, S: AsRef, Message, Item: Clone + PartialEq + 'static + 'a on_selected: &dyn Fn(Item) -> Message, selections: &super::Model, state: impl FnOnce() -> &'a mut State, -) -> event::Status { +) { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -325,14 +326,12 @@ pub fn update<'a, S: AsRef, Message, Item: Clone + PartialEq + 'static + 'a // bounds or on the drop-down, either way we close the overlay. state.is_open = false; - event::Status::Captured + shell.capture_event(); } else if cursor.is_over(layout.bounds()) { state.is_open = true; state.hovered_option = selections.selected.clone(); - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } Event::Mouse(mouse::Event::WheelScrolled { @@ -348,19 +347,15 @@ pub fn update<'a, S: AsRef, Message, Item: Clone + PartialEq + 'static + 'a shell.publish((on_selected)(option.1.clone())); } - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { let state = state(); state.keyboard_modifiers = *modifiers; - - event::Status::Ignored } - _ => event::Status::Ignored, + _ => {} } } @@ -420,8 +415,8 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static size: iced::Pixels(text_size), line_height, font: font.unwrap_or_else(crate::font::default), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), @@ -430,7 +425,7 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static }; let mut desc_count = 0; - padding.horizontal().mul_add( + padding.x().mul_add( 2.0, selections .elements() @@ -517,22 +512,20 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( bounds, border: style.border, shadow: Shadow::default(), + snap: true, }, style.background, ); if let Some(handle) = state.icon.as_ref() { let svg_handle = iced_core::Svg::new(handle.clone()).color(style.text_color); - svg::Renderer::draw_svg( - renderer, - svg_handle, - Rectangle { - x: bounds.x + bounds.width - gap - 16.0, - y: bounds.center_y() - 8.0, - width: 16.0, - height: 16.0, - }, - ); + let svg_bounds = Rectangle { + x: bounds.x + bounds.width - gap - 16.0, + y: bounds.center_y() - 8.0, + width: 16.0, + height: 16.0, + }; + svg::Renderer::draw_svg(renderer, svg_handle, svg_bounds, svg_bounds); } if let Some(content) = selected.map(AsRef::as_ref) { @@ -541,7 +534,7 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( let bounds = Rectangle { x: bounds.x + padding.left, y: bounds.center_y(), - width: bounds.width - padding.horizontal(), + width: bounds.width - padding.x(), height: f32::from(text_line_height.to_absolute(Pixels(text_size))), }; @@ -553,8 +546,8 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( line_height: text_line_height, font, bounds: bounds.size(), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), diff --git a/src/widget/dropdown/operation.rs b/src/widget/dropdown/operation.rs index 8cea4566..1a4e1a9f 100644 --- a/src/widget/dropdown/operation.rs +++ b/src/widget/dropdown/operation.rs @@ -11,62 +11,62 @@ pub trait Dropdown { fn open(&mut self); } -/// Produces a [`Task`] that closes a [`Dropdown`] popup. -pub fn close(id: Id) -> impl Operation { - struct Close(Id); +// /// Produces a [`Task`] that closes a [`Dropdown`] popup. +// pub fn close(id: Id) -> impl Operation { +// struct Close(Id); - impl Operation for Close { - fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) { - if id.map_or(true, |id| id != &self.0) { - return; - } +// impl Operation for Close { +// fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) { +// if id.map_or(true, |id| id != &self.0) { +// return; +// } - let Some(state) = state.downcast_mut::() else { - return; - }; +// let Some(state) = state.downcast_mut::() else { +// return; +// }; - state.close(); - } +// state.close(); +// } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } +// fn container( +// &mut self, +// _id: Option<&Id>, +// _bounds: Rectangle, +// operate_on_children: &mut dyn FnMut(&mut dyn Operation), +// ) { +// operate_on_children(self) +// } +// } - Close(id) -} +// Close(id) +// } -/// Produces a [`Task`] that opens a [`Dropdown`] popup. -pub fn open(id: Id) -> impl Operation { - struct Open(Id); +// /// Produces a [`Task`] that opens a [`Dropdown`] popup. +// pub fn open(id: Id) -> impl Operation { +// struct Open(Id); - impl Operation for Open { - fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) { - if id.map_or(true, |id| id != &self.0) { - return; - } +// impl Operation for Open { +// fn custom(&mut self, state: &mut dyn std::any::Any, id: Option<&Id>) { +// if id.map_or(true, |id| id != &self.0) { +// return; +// } - let Some(state) = state.downcast_mut::() else { - return; - }; +// let Some(state) = state.downcast_mut::() else { +// return; +// }; - state.open(); - } +// state.open(); +// } - fn container( - &mut self, - _id: Option<&Id>, - _bounds: Rectangle, - operate_on_children: &mut dyn FnMut(&mut dyn Operation), - ) { - operate_on_children(self) - } - } +// fn container( +// &mut self, +// _id: Option<&Id>, +// _bounds: Rectangle, +// operate_on_children: &mut dyn FnMut(&mut dyn Operation), +// ) { +// operate_on_children(self) +// } +// } - Open(id) -} +// Open(id) +// } diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 67101d26..b6244c07 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -203,13 +203,13 @@ where state.hashes[i] = text_hash; state.selections[i].update(Text { content: selection.as_ref(), - bounds: Size::INFINITY, + bounds: Size::INFINITE, // TODO use the renderer default size size: iced::Pixels(self.text_size.unwrap_or(14.0)), line_height: self.text_line_height, font: self.font.unwrap_or_else(crate::font::default), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), @@ -227,7 +227,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -252,17 +252,17 @@ where ) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, _renderer: &crate::Renderer, _clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &Rectangle, - ) -> event::Status { + ) { update::( &event, layout, @@ -327,21 +327,23 @@ where } fn operate( - &self, + &mut self, tree: &mut Tree, _layout: Layout<'_>, _renderer: &crate::Renderer, operation: &mut dyn iced_core::widget::Operation, ) { - let state = tree.state.downcast_mut::(); - operation.custom(state, self.id.as_ref()); + // TODO: double check operation handling + // let state = tree.state.downcast_mut::(); + // operation.custom(state, self.id.as_ref()); } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { #[cfg(all(feature = "winit", feature = "wayland"))] @@ -469,24 +471,38 @@ pub fn layout( let max_width = match width { Length::Shrink => { let measure = move |(label, paragraph): (_, Option<&mut crate::Plain>)| -> f32 { - let text = Text { - content: label, - bounds: Size::new(f32::MAX, f32::MAX), - size: iced::Pixels(text_size), - line_height: text_line_height, - font: font.unwrap_or_else(crate::font::default), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, - shaping: text::Shaping::Advanced, - wrapping: text::Wrapping::default(), - ellipsize: text::Ellipsize::default(), - }; let paragraph = match paragraph { Some(p) => { + let text = Text { + content: label, + bounds: Size::new(f32::MAX, f32::MAX), + size: iced::Pixels(text_size), + line_height: text_line_height, + font: font.unwrap_or_else(crate::font::default), + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), + }; p.update(text); p } - None => &mut crate::Plain::new(text), + None => { + let text = Text { + content: label.to_string(), + bounds: Size::new(f32::MAX, f32::MAX), + size: iced::Pixels(text_size), + line_height: text_line_height, + font: font.unwrap_or_else(crate::font::default), + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, + shaping: text::Shaping::Advanced, + wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), + }; + &mut crate::Plain::new(text) + } }; paragraph.min_width().round() }; @@ -544,7 +560,7 @@ pub fn update< text_size: Option, font: Option, selected_option: Option, -) -> event::Status { +) { let state = state(); let open = |shell: &mut Shell<'_, Message>, @@ -575,7 +591,7 @@ pub fn update< let measure = |_label: &str, selection_paragraph: &crate::Paragraph| -> f32 { selection_paragraph.min_width().round() }; - let pad_width = padding.horizontal().mul_add(2.0, 16.0); + let pad_width = padding.x().mul_add(2.0, 16.0); let selections_width = selections .iter() @@ -669,12 +685,10 @@ pub fn update< if let Some(on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } - event::Status::Captured + shell.capture_event(); } else if cursor.is_over(layout.bounds()) { open(shell, state, on_selected); - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } Event::Mouse(mouse::Event::WheelScrolled { @@ -689,17 +703,13 @@ pub fn update< shell.publish((on_selected)(next_index)); } - event::Status::Captured - } else { - event::Status::Ignored + shell.capture_event(); } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { state.keyboard_modifiers = *modifiers; - - event::Status::Ignored } - _ => event::Status::Ignored, + _ => {} } } @@ -746,7 +756,7 @@ where .zip(state.selections.iter()) .map(|(label, selection)| measure(label.as_ref(), selection.raw())) .fold(0.0, |next, current| current.max(next)); - let pad_width = padding.horizontal().mul_add(2.0, 16.0); + let pad_width = padding.x().mul_add(2.0, 16.0); let width = selections_width + gap + pad_width + icon_width; let is_open = state.is_open.clone(); @@ -822,7 +832,7 @@ where selection_paragraph.min_width().round() }; - let pad_width = padding.horizontal().mul_add(2.0, 16.0); + let pad_width = padding.x().mul_add(2.0, 16.0); let icon_width = if icons.is_empty() { 0.0 } else { 24.0 }; @@ -883,23 +893,20 @@ pub fn draw<'a, S>( bounds, border: style.border, shadow: Shadow::default(), + snap: true, }, style.background, ); if let Some(handle) = state.icon.clone() { let svg_handle = svg::Svg::new(handle).color(style.text_color); - - svg::Renderer::draw_svg( - renderer, - svg_handle, - Rectangle { - x: bounds.x + bounds.width - gap - 16.0, - y: bounds.center_y() - 8.0, - width: 16.0, - height: 16.0, - }, - ); + let bounds = Rectangle { + x: bounds.x + bounds.width - gap - 16.0, + y: bounds.center_y() - 8.0, + width: 16.0, + height: 16.0, + }; + svg::Renderer::draw_svg(renderer, svg_handle, bounds, bounds); } if let Some(content) = selected.map(AsRef::as_ref).or(placeholder) { @@ -908,7 +915,7 @@ pub fn draw<'a, S>( let mut bounds = Rectangle { x: bounds.x + padding.left, y: bounds.center_y(), - width: bounds.width - padding.horizontal(), + width: bounds.width - padding.x(), height: f32::from(text_line_height.to_absolute(Pixels(text_size))), }; @@ -932,8 +939,8 @@ pub fn draw<'a, S>( line_height: text_line_height, font, bounds: bounds.size(), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), ellipsize: text::Ellipsize::default(), diff --git a/src/widget/flex_row/layout.rs b/src/widget/flex_row/layout.rs index 744b607d..ae0c28d6 100644 --- a/src/widget/flex_row/layout.rs +++ b/src/widget/flex_row/layout.rs @@ -15,7 +15,7 @@ use taffy::{AlignContent, TaffyTree}; pub fn resolve( renderer: &Renderer, limits: &Limits, - items: &[Element<'_, Message>], + items: &mut [Element<'_, Message>], padding: Padding, column_spacing: f32, row_spacing: f32, @@ -61,8 +61,8 @@ pub fn resolve( ..taffy::Style::default() }; - for (child, tree) in items.iter().zip(tree.iter_mut()) { - let child_widget = child.as_widget(); + for (child, tree) in items.iter_mut().zip(tree.iter_mut()) { + let child_widget = child.as_widget_mut(); let child_node = child_widget.layout(tree, renderer, limits); let size = child_node.size(); @@ -138,7 +138,7 @@ pub fn resolve( leafs .into_iter() - .zip(items.iter()) + .zip(items.iter_mut()) .zip(nodes.iter_mut()) .zip(tree) .for_each(|(((leaf, child), node), tree)| { @@ -146,7 +146,7 @@ pub fn resolve( return; }; - let child_widget = child.as_widget(); + let child_widget = child.as_widget_mut(); let c_size = child_widget.size(); match c_size.width { Length::Fill | Length::FillPortion(_) => { diff --git a/src/widget/flex_row/widget.rs b/src/widget/flex_row/widget.rs index 264201c1..f7b90f66 100644 --- a/src/widget/flex_row/widget.rs +++ b/src/widget/flex_row/widget.rs @@ -100,7 +100,7 @@ impl Widget for FlexR } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -114,7 +114,7 @@ impl Widget for FlexR super::layout::resolve( renderer, &limits, - &self.children, + &mut self.children, self.padding, f32::from(self.column_spacing), f32::from(self.row_spacing), @@ -127,19 +127,19 @@ impl Widget for FlexR } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation<()>, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.traverse(&mut |operation| { self.children - .iter() + .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .for_each(|((child, state), c_layout)| { - child.as_widget().operate( + child.as_widget_mut().operate( state, c_layout.with_virtual_offset(layout.virtual_offset()), renderer, @@ -149,25 +149,25 @@ impl Widget for FlexR }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { self.children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .map(|((child, state), c_layout)| { - child.as_widget_mut().on_event( + child.as_widget_mut().update( state, - event.clone(), + event, c_layout.with_virtual_offset(layout.virtual_offset()), cursor, renderer, @@ -175,8 +175,7 @@ impl Widget for FlexR shell, viewport, ) - }) - .fold(event::Status::Ignored, event::Status::merge) + }); } fn mouse_interaction( @@ -235,11 +234,19 @@ impl Widget for FlexR fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer, translation) + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + viewport, + translation, + ) } #[cfg(feature = "a11y")] diff --git a/src/widget/frames.rs b/src/widget/frames.rs index 1c379ac1..056a55ba 100644 --- a/src/widget/frames.rs +++ b/src/widget/frames.rs @@ -8,6 +8,8 @@ use std::path::Path; use std::time::{Duration, Instant}; use ::image as image_rs; +use iced::Task; +use iced::mouse; use iced_core::image::Renderer as ImageRenderer; use iced_core::mouse::Cursor; use iced_core::widget::{Tree, tree}; @@ -15,7 +17,6 @@ use iced_core::{ Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector, Widget, event, layout, renderer, window, }; -use iced_runtime::Command; use iced_widget::image::{self, Handle}; use image_rs::AnimationDecoder; use image_rs::codecs::gif::GifDecoder; @@ -27,7 +28,7 @@ use iced_futures::futures::{AsyncRead, AsyncReadExt}; #[cfg(feature = "tokio")] use tokio::io::{AsyncRead, AsyncReadExt}; -use super::icon::load_icon; +use crate::widget::icon; #[must_use] /// Creates a new [`AnimatedImage`] with the given [`animated_image::Frames`] @@ -74,13 +75,13 @@ impl Frames { size: u16, theme: Option<&str>, default_fallbacks: bool, - ) -> Command> { + ) -> Task> { let mut name_path_buffer = None; - if let Some(path) = load_icon(name, size, theme) { + if let Some(path) = icon::Named::new(name).size(size).path() { name_path_buffer = Some(path); } else if default_fallbacks { for name in name.rmatch_indices('-').map(|(pos, _)| &name[..pos]) { - if let Some(path) = load_icon(name, size, theme) { + if let Some(path) = icon::Named::new(name).size(size).path() { name_path_buffer = Some(path); break; } @@ -90,14 +91,14 @@ impl Frames { if let Some(name_path_buffer) = name_path_buffer { Self::load_from_path(name_path_buffer) } else { - Command::perform(async { Err(Error::Missing) }, std::convert::identity) + Task::perform(async { Err(Error::Missing) }, std::convert::identity) } } /// Load [`Frames`] from the supplied path - pub fn load_from_path(path: impl AsRef) -> Command> { + pub fn load_from_path(path: impl AsRef) -> Task> { #[inline(never)] - fn inner(path: &Path) -> Command> { + fn inner(path: &Path) -> Task> { #[cfg(feature = "tokio")] use tokio::fs::File; #[cfg(feature = "tokio")] @@ -108,7 +109,7 @@ impl Frames { #[cfg(not(feature = "tokio"))] use iced_futures::futures::io::BufReader; - let path = path.as_ref().to_path_buf(); + let path = path.to_path_buf(); let f = async move { let image_type = match &path.extension() { @@ -119,10 +120,10 @@ impl Frames { }; let reader = BufReader::new(File::open(path).await?); - Self::from_reader(reader, image_type).await + Frames::from_reader(reader, image_type).await }; - Command::perform(f, std::convert::identity) + Task::perform(f, std::convert::identity) } inner(path.as_ref()) @@ -168,9 +169,9 @@ impl Frames { let total_bytes = frames .iter() .map(|f| match f.handle.data() { - iced_core::image::Data::Path(_) => 0, - iced_core::image::Data::Bytes(b) => b.len(), - iced_core::image::Data::Rgba { pixels, .. } => pixels.len(), + iced_core::image::Handle::Path(..) => 0, + iced_core::image::Handle::Bytes(_, b) => b.len(), + iced_core::image::Handle::Rgba { pixels, .. } => pixels.len(), }) .sum::() .try_into() @@ -195,7 +196,7 @@ impl From for Frame { let delay = frame.delay().into(); - let handle = image::Handle::from_pixels(width, height, frame.into_buffer().into_vec()); + let handle = image::Handle::from_rgba(width, height, frame.into_buffer().into_vec()); Self { delay, handle } } @@ -278,12 +279,8 @@ impl<'a, Message, Renderer> Widget for Animated where Renderer: ImageRenderer, { - fn width(&self) -> Length { - self.width - } - - fn height(&self) -> Length { - self.height + fn size(&self) -> Size { + Size::new(self.width.into(), self.height.into()) } fn tag(&self) -> tree::Tag { @@ -315,7 +312,12 @@ where } } - fn layout(&self, renderer: &Renderer, limits: &layout::Limits) -> layout::Node { + fn layout( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { iced_widget::image::layout( renderer, limits, @@ -326,19 +328,20 @@ where ) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, - _layout: Layout<'_>, - _cursor_position: Cursor, - _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, + event: &Event, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + viewport: &Rectangle, + ) { let state = tree.state.downcast_mut::(); - if let Event::Window(_, window::Event::RedrawRequested(now)) = event { + if let Event::Window(window::Event::RedrawRequested(now)) = event { let elapsed = now.duration_since(state.current.started); if elapsed > state.current.frame.delay { @@ -346,15 +349,14 @@ where state.current = self.frames.frames[state.index].clone().into(); - shell.request_redraw(window::RedrawRequest::At(now + state.current.frame.delay)); + shell + .request_redraw_at(window::RedrawRequest::At(*now + state.current.frame.delay)); } else { let remaining = state.current.frame.delay - elapsed; - shell.request_redraw(window::RedrawRequest::At(now + remaining)); + shell.request_redraw_at(window::RedrawRequest::At(*now + remaining)); } } - - event::Status::Ignored } fn draw( diff --git a/src/widget/grid/layout.rs b/src/widget/grid/layout.rs index a7e42759..8ed4c0ec 100644 --- a/src/widget/grid/layout.rs +++ b/src/widget/grid/layout.rs @@ -17,7 +17,7 @@ use taffy::{AlignContent, TaffyTree}; pub fn resolve( renderer: &Renderer, limits: &Limits, - items: &[Element<'_, Message>], + items: &mut [Element<'_, Message>], assignments: &[Assignment], width: Length, height: Length, @@ -37,9 +37,13 @@ pub fn resolve( let mut taffy = TaffyTree::<()>::with_capacity(items.len() + 1); // Attach widgets as child nodes. - for ((child, assignment), tree) in items.iter().zip(assignments.iter()).zip(tree.iter_mut()) { + for ((child, assignment), tree) in items + .iter_mut() + .zip(assignments.iter()) + .zip(tree.iter_mut()) + { // Calculate the dimensions of the item. - let child_widget = child.as_widget(); + let child_widget = child.as_widget_mut(); let child_node = child_widget.layout(tree, renderer, limits); let size = child_node.size(); @@ -172,12 +176,12 @@ pub fn resolve( for (((leaf, child), node), tree) in leafs .into_iter() - .zip(items.iter()) + .zip(items.iter_mut()) .zip(nodes.iter_mut()) .zip(tree) { if let Ok(leaf_layout) = taffy.layout(leaf) { - let child_widget = child.as_widget(); + let child_widget = child.as_widget_mut(); let c_size = child_widget.size(); match c_size.width { Length::Fill | Length::FillPortion(_) => { diff --git a/src/widget/grid/widget.rs b/src/widget/grid/widget.rs index 0aca7943..f88dfc2a 100644 --- a/src/widget/grid/widget.rs +++ b/src/widget/grid/widget.rs @@ -127,7 +127,7 @@ impl Widget for Grid< } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -141,7 +141,7 @@ impl Widget for Grid< super::layout::resolve( renderer, &limits, - &self.children, + &mut self.children, &self.assignments, self.width, self.height, @@ -156,19 +156,19 @@ impl Widget for Grid< } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation<()>, ) { - operation.container(None, layout.bounds(), &mut |operation| { + operation.traverse(&mut |operation| { self.children - .iter() + .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .for_each(|((child, state), c_layout)| { - child.as_widget().operate( + child.as_widget_mut().operate( state, c_layout.with_virtual_offset(layout.virtual_offset()), renderer, @@ -178,25 +178,25 @@ impl Widget for Grid< }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { self.children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .map(|((child, state), c_layout)| { - child.as_widget_mut().on_event( + child.as_widget_mut().update( state, - event.clone(), + event, c_layout.with_virtual_offset(layout.virtual_offset()), cursor, renderer, @@ -204,8 +204,7 @@ impl Widget for Grid< shell, viewport, ) - }) - .fold(event::Status::Ignored, event::Status::merge) + }); } fn mouse_interaction( @@ -264,11 +263,19 @@ impl Widget for Grid< fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - overlay::from_children(&mut self.children, tree, layout, renderer, translation) + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + viewport, + translation, + ) } #[cfg(feature = "a11y")] diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index b0957d68..695c8405 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -157,7 +157,7 @@ impl Widget } fn layout( - &self, + &mut self, tree: &mut tree::Tree, renderer: &crate::Renderer, limits: &iced_core::layout::Limits, @@ -165,7 +165,7 @@ impl Widget let child_tree = &mut tree.children[0]; let child = self .header_bar_inner - .as_widget() + .as_widget_mut() .layout(child_tree, renderer, limits); iced_core::layout::Node::with_children(child.size(), vec![child]) } @@ -193,20 +193,20 @@ impl Widget ); } - fn on_event( + fn update( &mut self, state: &mut tree::Tree, - event: iced_core::Event, + event: &iced_core::Event, layout: iced_core::Layout<'_>, cursor: iced_core::mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn iced_core::Clipboard, shell: &mut iced_core::Shell<'_, Message>, viewport: &iced_core::Rectangle, - ) -> iced_core::event::Status { + ) { let child_state = &mut state.children[0]; let child_layout = layout.children().next().unwrap(); - self.header_bar_inner.as_widget_mut().on_event( + self.header_bar_inner.as_widget_mut().update( child_state, event, child_layout, @@ -238,7 +238,7 @@ impl Widget } fn operate( - &self, + &mut self, state: &mut tree::Tree, layout: iced_core::Layout<'_>, renderer: &crate::Renderer, @@ -246,16 +246,20 @@ impl Widget ) { let child_tree = &mut state.children[0]; let child_layout = layout.children().next().unwrap(); - self.header_bar_inner - .as_widget() - .operate(child_tree, child_layout, renderer, operation); + self.header_bar_inner.as_widget_mut().operate( + child_tree, + child_layout, + renderer, + operation, + ); } fn overlay<'b>( &'b mut self, state: &'b mut tree::Tree, - layout: iced_core::Layout<'_>, + layout: iced_core::Layout<'b>, renderer: &crate::Renderer, + viewport: &iced_core::Rectangle, translation: Vector, ) -> Option> { let child_tree = &mut state.children[0]; @@ -264,6 +268,7 @@ impl Widget child_tree, child_layout, renderer, + viewport, translation, ) } diff --git a/src/widget/icon/mod.rs b/src/widget/icon/mod.rs index 6c6a9f08..031b4b0c 100644 --- a/src/widget/icon/mod.rs +++ b/src/widget/icon/mod.rs @@ -15,7 +15,7 @@ pub use handle::{Data, Handle, from_path, from_raster_bytes, from_raster_pixels, use crate::Element; use derive_setters::Setters; use iced::widget::{Image, Svg}; -use iced::{ContentFit, Length, Rectangle}; +use iced::{ContentFit, Length, Radians, Rectangle}; use iced_core::Rotation; /// Create an [`Icon`] from a pre-existing [`Handle`] @@ -125,17 +125,22 @@ pub fn draw(renderer: &mut crate::Renderer, handle: &Handle, icon_bounds: Rectan renderer, iced_core::svg::Svg::new(handle), icon_bounds, + icon_bounds, ), Data::Image(handle) => { iced_core::image::Renderer::draw_image( renderer, - handle, - iced_core::image::FilterMethod::Linear, + iced_core::Image { + handle, + filter_method: iced_core::image::FilterMethod::Linear, + rotation: Radians(0.), + border_radius: [0.0; 4].into(), + opacity: 1.0, + snap: true, + }, + icon_bounds, icon_bounds, - iced_core::Radians::from(0), - 1.0, - [0.0; 4], ); } } diff --git a/src/widget/id_container.rs b/src/widget/id_container.rs index 3d468b20..c8e49e04 100644 --- a/src/widget/id_container.rs +++ b/src/widget/id_container.rs @@ -3,7 +3,7 @@ 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::widget::{Id, Operation, Tree}; use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; pub use iced_widget::container::{Catalog, Style}; @@ -65,29 +65,30 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { let node = self .content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits); let size = node.size(); layout::Node::with_children(size, vec![node]) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, + operation: &mut dyn Operation, ) { - operation.container(Some(&self.id), layout.bounds(), &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], + operation.container(Some(&self.id), layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( + tree, layout .children() .next() @@ -99,18 +100,18 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + 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( + ) { + self.content.as_widget_mut().update( &mut tree.children[0], event, layout @@ -169,8 +170,9 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( @@ -181,6 +183,7 @@ where .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, + viewport, translation, ) } diff --git a/src/widget/layer_container.rs b/src/widget/layer_container.rs index 74521b3d..110af518 100644 --- a/src/widget/layer_container.rs +++ b/src/widget/layer_container.rs @@ -172,7 +172,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -181,7 +181,7 @@ where } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -190,18 +190,18 @@ where self.container.operate(tree, layout, renderer, operation); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.container.on_event( + ) { + self.container.update( tree, event, layout, @@ -257,11 +257,13 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container.overlay(tree, layout, renderer, translation) + self.container + .overlay(tree, layout, renderer, viewport, translation) } fn drag_destinations( diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs index a3dedd96..49df998a 100644 --- a/src/widget/list/column.rs +++ b/src/widget/list/column.rs @@ -6,7 +6,7 @@ use iced_widget::container::Catalog; use crate::{ Apply, Element, theme, - widget::{container, divider, vertical_space}, + widget::{container, divider, space::vertical}, }; #[inline] @@ -65,7 +65,7 @@ impl<'a, Message: 'static> ListColumn<'a, Message> { // Ensure a minimum height of 32. let list_item = iced::widget::row![ container(item).align_y(iced::Alignment::Center), - vertical_space().height(iced::Length::Fixed(32.)) + vertical().height(iced::Length::Fixed(32.)) ] .padding(this.list_item_padding) .align_y(iced::Alignment::Center); diff --git a/src/widget/menu/flex.rs b/src/widget/menu/flex.rs index 8eb08d4e..4a58f13a 100644 --- a/src/widget/menu/flex.rs +++ b/src/widget/menu/flex.rs @@ -57,11 +57,11 @@ pub fn resolve<'a, E, Message, Renderer>( padding: Padding, spacing: f32, align_items: Alignment, - items: &[E], + items: &mut [E], tree: &mut [&mut Tree], ) -> Node where - E: std::borrow::Borrow>, + E: std::borrow::BorrowMut>, Renderer: renderer::Renderer, { let limits = limits.shrink(padding); @@ -69,7 +69,7 @@ where let max_cross = axis.cross(limits.max()); let mut fill_sum = 0; - let mut cross = axis.cross(limits.min()).max(axis.cross(Size::INFINITY)); + let mut cross = axis.cross(limits.min()).max(axis.cross(Size::INFINITE)); let mut available = axis.main(limits.max()) - total_spacing; let mut nodes: Vec = Vec::with_capacity(items.len()); @@ -78,8 +78,8 @@ where if align_items == Alignment::Center { let mut fill_cross = axis.cross(limits.min()); - for (child, tree) in items.iter().zip(tree.iter_mut()) { - let child = child.borrow(); + for (child, tree) in items.iter_mut().zip(tree.iter_mut()) { + let child = child.borrow_mut(); let c_size = child.as_widget().size(); let cross_fill_factor = match axis { Axis::Horizontal => c_size.height, @@ -92,7 +92,7 @@ where let child_limits = Limits::new(Size::ZERO, Size::new(max_width, max_height)); - let layout = child.as_widget().layout(tree, renderer, &child_limits); + let layout = child.as_widget_mut().layout(tree, renderer, &child_limits); let size = layout.size(); fill_cross = fill_cross.max(axis.cross(size)); @@ -102,8 +102,8 @@ where cross = fill_cross; } - for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { - let child = child.borrow(); + for (i, (child, tree)) in items.iter_mut().zip(tree.iter_mut()).enumerate() { + let child = child.borrow_mut(); let c_size = child.as_widget().size(); let fill_factor = match axis { Axis::Horizontal => c_size.width, @@ -129,7 +129,7 @@ where Size::new(max_width, max_height), ); - let layout = child.as_widget().layout(tree, renderer, &child_limits); + let layout = child.as_widget_mut().layout(tree, renderer, &child_limits); let size = layout.size(); available -= axis.main(size); @@ -146,8 +146,8 @@ where let remaining = available.max(0.0); - for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { - let child = child.borrow(); + for (i, (child, tree)) in items.iter_mut().zip(tree.iter_mut()).enumerate() { + let child = child.borrow_mut(); let c_size = child.as_widget().size(); let fill_factor = match axis { Axis::Horizontal => c_size.width, @@ -180,7 +180,7 @@ where Size::new(max_width, max_height), ); - let layout = child.as_widget().layout(tree, renderer, &child_limits); + let layout = child.as_widget_mut().layout(tree, renderer, &child_limits); if align_items != Alignment::Center { cross = cross.max(axis.cross(layout.size())); @@ -231,7 +231,7 @@ pub fn resolve_wrapper<'a, Message>( padding: Padding, spacing: f32, align_items: Alignment, - items: &[&RcElementWrapper], + items: &mut [&mut RcElementWrapper], tree: &mut [&mut Tree], ) -> Node { let limits = limits.shrink(padding); @@ -239,7 +239,7 @@ pub fn resolve_wrapper<'a, Message>( let max_cross = axis.cross(limits.max()); let mut fill_sum = 0; - let mut cross = axis.cross(limits.min()).max(axis.cross(Size::INFINITY)); + let mut cross = axis.cross(limits.min()).max(axis.cross(Size::INFINITE)); let mut available = axis.main(limits.max()) - total_spacing; let mut nodes: Vec = Vec::with_capacity(items.len()); @@ -248,7 +248,7 @@ pub fn resolve_wrapper<'a, Message>( if align_items == Alignment::Center { let mut fill_cross = axis.cross(limits.min()); - for (child, tree) in items.iter().zip(tree.iter_mut()) { + for (child, tree) in items.into_iter().zip(tree.iter_mut()) { let c_size = child.size(); let cross_fill_factor = match axis { Axis::Horizontal => c_size.height, @@ -271,7 +271,7 @@ pub fn resolve_wrapper<'a, Message>( cross = fill_cross; } - for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { + for (i, (child, tree)) in items.into_iter().zip(tree.iter_mut()).enumerate() { let c_size = child.size(); let fill_factor = match axis { Axis::Horizontal => c_size.width, @@ -314,7 +314,7 @@ pub fn resolve_wrapper<'a, Message>( let remaining = available.max(0.0); - for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { + for (i, (child, tree)) in items.into_iter().zip(tree.iter_mut()).enumerate() { let c_size = child.size(); let fill_factor = match axis { Axis::Horizontal => c_size.width, diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index bbbb4a2b..05fcc133 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -26,7 +26,7 @@ use crate::{ }, }; -use iced::{Point, Shadow, Vector, window}; +use iced::{Point, Shadow, Vector, event::Status, window}; use iced_core::Border; use iced_widget::core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, event, @@ -533,14 +533,14 @@ where menu_roots_children(&self.menu_roots) } - fn layout(&self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { + fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node { use super::flex; let limits = limits.width(self.width).height(self.height); - let children = self + let mut children = self .menu_roots - .iter() - .map(|root| &root.item) + .iter_mut() + .map(|root| &mut root.item) .collect::>(); // the first children of the tree are the menu roots items let mut tree_children = tree @@ -555,28 +555,28 @@ where self.padding, self.spacing, Alignment::Center, - &children, + &mut children, &mut tree_children, ) } #[allow(clippy::too_many_lines)] - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: event::Event, + event: &event::Event, layout: Layout<'_>, view_cursor: Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { use event::Event::{Mouse, Touch}; use mouse::{Button::Left, Event::ButtonReleased}; use touch::Event::{FingerLifted, FingerLost}; - let root_status = process_root_events( + process_root_events( &mut self.menu_roots, view_cursor, tree, @@ -638,7 +638,7 @@ where }); if !create_popup { - return event::Status::Ignored; + return; } #[cfg(all( feature = "multi-window", @@ -665,8 +665,6 @@ where } _ => (), } - - root_status } fn draw( @@ -704,6 +702,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }; renderer.fill_quad(path_quad, styling.path); @@ -731,8 +730,9 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, _renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { #[cfg(all( @@ -799,18 +799,16 @@ fn process_root_events( clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, -) -> event::Status -where -{ +) { menu_roots .iter_mut() .zip(&mut tree.children) .zip(layout.children()) .map(|((root, t), lo)| { // assert!(t.tag == tree::Tag::stateless()); - root.item.on_event( + root.item.update( &mut t.children[root.index], - event.clone(), + event, lo, view_cursor, renderer, @@ -818,6 +816,5 @@ where shell, viewport, ) - }) - .fold(event::Status::Ignored, event::Status::merge) + }); } diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index c455cd13..d52c929d 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -310,7 +310,7 @@ pub(crate) struct MenuState { } impl MenuState { pub(super) fn layout( - &self, + &mut self, overlay_offset: Vector, slice: MenuSlice, renderer: &crate::Renderer, @@ -329,8 +329,8 @@ impl MenuState { // viewport space children bounds let children_bounds = self.menu_bounds.children_bounds + overlay_offset; let child_nodes = self.menu_bounds.child_positions[start_index..=end_index] - .iter() - .zip(self.menu_bounds.child_sizes[start_index..=end_index].iter()) + .iter_mut() + .zip(self.menu_bounds.child_sizes[start_index..=end_index].iter_mut()) .zip(menu_tree[start_index..=end_index].iter()) .map(|((cp, size), mt)| { let mut position = *cp; @@ -347,7 +347,11 @@ impl MenuState { let limits = Limits::new(size, size); mt.item - .layout(&mut tree[mt.index], renderer, &limits) + .element + .with_data_mut(|e| { + e.as_widget_mut() + .layout(&mut tree[mt.index], renderer, &limits) + }) .move_to(Point::new(0.0, position + self.scroll_offset)) }) .collect::>(); @@ -360,7 +364,7 @@ impl MenuState { overlay_offset: Vector, index: usize, renderer: &crate::Renderer, - menu_tree: &MenuTree, + menu_tree: &mut MenuTree, tree: &mut Tree, ) -> Node { // viewport space children bounds @@ -499,7 +503,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } else { self.depth }] - .iter() + .iter_mut() .enumerate() .filter(|ms| self.is_overlay || ms.0 < 1) .fold( @@ -545,15 +549,15 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } #[allow(clippy::too_many_lines)] - fn on_event( + fn update( &mut self, - event: event::Event, + event: &event::Event, layout: Layout<'_>, view_cursor: Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> (Option<(usize, MenuState)>, event::Status) { + ) -> Option<(usize, MenuState)> { use event::{ Event::{Mouse, Touch}, Status::{Captured, Ignored}, @@ -569,7 +573,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { .inner .with_data(|data| data.open || data.active_root.len() <= self.depth) { - return (None, Ignored); + return None; } let viewport = layout.bounds(); @@ -583,7 +587,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { }; let menu_status = process_menu_events( self, - event.clone(), + &event, view_cursor, renderer, clipboard, @@ -602,25 +606,28 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { self.main_offset as f32, ); - let ret = match event { - Mouse(WheelScrolled { delta }) => { - process_scroll_events(self, delta, overlay_cursor, viewport_size, overlay_offset) - .merge(menu_status) - } + match event { + Mouse(WheelScrolled { delta }) => process_scroll_events( + self, + shell, + *delta, + overlay_cursor, + viewport_size, + overlay_offset, + ), Mouse(ButtonPressed(Left)) | Touch(FingerPressed { .. }) => { self.tree.inner.with_data_mut(|data| { data.pressed = true; data.view_cursor = view_cursor; }); - Captured } Mouse(CursorMoved { position }) | Touch(FingerMoved { position, .. }) => { - let view_cursor = Cursor::Available(position); + let view_cursor = Cursor::Available(*position); let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset; if !self.is_overlay && !view_cursor.is_over(viewport) { - return (None, menu_status); + return None; } let (new_root, status) = process_overlay_events( @@ -634,7 +641,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { shell, ); - return (new_root, status.merge(menu_status)); + return new_root; } Mouse(ButtonReleased(_)) | Touch(FingerLifted { .. }) => { @@ -694,23 +701,19 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } state.reset(); - return Captured; } } // close all menus when clicking inside the menu bar if self.bar_bounds.contains(overlay_cursor) { state.reset(); - Captured - } else { - menu_status } }) } - _ => menu_status, + _ => {} }; - (None, ret) + None } #[allow(unused_results, clippy::too_many_lines)] @@ -734,7 +737,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { let render_bounds = if self.is_overlay { Rectangle::new(Point::ORIGIN, viewport.size()) } else { - Rectangle::new(Point::ORIGIN, Size::INFINITY) + Rectangle::new(Point::ORIGIN, Size::INFINITE) }; let styling = theme.appearance(&self.style); @@ -796,6 +799,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { color: styling.border_color, }, shadow: Shadow::default(), + snap: true, }; let menu_color = styling.background; r.fill_quad(menu_quad, menu_color); @@ -815,6 +819,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { ..Default::default() }, shadow: Shadow::default(), + snap: true, }; r.fill_quad(path_quad, styling.path); @@ -867,17 +872,16 @@ impl overlay::Overlay, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { - self.on_event(event, layout, cursor, renderer, clipboard, shell) - .1 + ) { + self.update(event, layout, cursor, renderer, clipboard, shell); } fn draw( @@ -903,7 +907,7 @@ impl Widget Widget, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - let (new_root, status) = self.on_event(event, layout, cursor, renderer, clipboard, shell); + ) { + let new_root = self.update(event, layout, cursor, renderer, clipboard, shell); #[cfg(all( feature = "multi-window", @@ -997,7 +1001,7 @@ impl Widget Widget Widget Rectangle { Rectangle { x: rect.x - padding.left, y: rect.y - padding.top, - width: rect.width + padding.horizontal(), - height: rect.height + padding.vertical(), + width: rect.width + padding.x(), + height: rect.height + padding.y(), } } @@ -1274,15 +1277,13 @@ pub(super) fn init_root_popup_menu( #[allow(clippy::too_many_arguments)] fn process_menu_events( menu: &mut Menu, - event: event::Event, + event: &event::Event, view_cursor: Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, overlay_offset: Vector, -) -> event::Status { - use event::Status; - +) { let my_state = &mut menu.tree; let menu_roots = match &mut menu.menu_roots { Cow::Borrowed(_) => panic!(), @@ -1290,15 +1291,15 @@ fn process_menu_events( }; my_state.inner.with_data_mut(|state| { if state.active_root.len() <= menu.depth { - return event::Status::Ignored; + return; } let Some(hover) = state.menu_states.last_mut() else { - return Status::Ignored; + return; }; let Some(hover_index) = hover.index else { - return Status::Ignored; + return; }; let mt = state.active_root.iter().skip(1).fold( @@ -1321,7 +1322,7 @@ fn process_menu_events( let child_layout = Layout::new(&child_node); // process only the last widget - mt.item.on_event( + mt.item.update( tree, event, child_layout, @@ -1330,7 +1331,7 @@ fn process_menu_events( clipboard, shell, &Rectangle::default(), - ) + ); }) } @@ -1561,12 +1562,12 @@ where fn process_scroll_events( menu: &mut Menu<'_, Message>, + shell: &mut Shell<'_, Message>, delta: mouse::ScrollDelta, overlay_cursor: Point, viewport_size: Size, overlay_offset: Vector, -) -> event::Status -where +) where Message: Clone, { use event::Status::{Captured, Ignored}; @@ -1590,12 +1591,12 @@ where // update if state.menu_states.is_empty() { - return Ignored; + return; } else if state.menu_states.len() == 1 { let last_ms = &mut state.menu_states[0]; if last_ms.index.is_none() { - return Captured; + return; } let (max_offset, min_offset) = calc_offset_bounds(last_ms, viewport_size); @@ -1616,7 +1617,8 @@ where .children_bounds .contains(overlay_cursor) { - return Captured; + shell.capture_event(); + return; } // scroll the second last one @@ -1632,8 +1634,8 @@ where last_two[1].menu_bounds.check_bounds.y += clamped_delta_y; } } - Captured - }) + shell.capture_event(); + }); } #[allow(clippy::pedantic)] @@ -1666,11 +1668,11 @@ fn get_children_layout( .map(|mt| { mt.item .element - .with_data(|w| match w.as_widget().size().height { + .with_data_mut(|w| match w.as_widget_mut().size().height { Length::Fixed(f) => Size::new(width, f), Length::Shrink => { let l_height = w - .as_widget() + .as_widget_mut() .layout( &mut tree[mt.index], renderer, diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 15dd5810..bd182b9c 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -253,13 +253,16 @@ pub fn menu_items< let key = find_key(&action, key_binds); let mut items = vec![ widget::text(l).into(), - widget::horizontal_space().into(), + widget::space::horizontal().into(), widget::text(key).class(key_class).into(), ]; if let Some(icon) = icon { items.insert(0, widget::icon::icon(icon).size(14).into()); - items.insert(1, widget::Space::with_width(spacing.space_xxs).into()); + items.insert( + 1, + widget::space::horizontal().width(spacing.space_xxs).into(), + ); } let menu_button = menu_button(items).on_press(action.message()); @@ -273,13 +276,16 @@ pub fn menu_items< let mut items = vec![ widget::text(l).into(), - widget::horizontal_space().into(), + widget::space::horizontal().into(), widget::text(key).class(key_class).into(), ]; if let Some(icon) = icon { items.insert(0, widget::icon::icon(icon).size(14).into()); - items.insert(1, widget::Space::with_width(spacing.space_xxs).into()); + items.insert( + 1, + widget::space::horizontal().width(spacing.space_xxs).into(), + ); } let menu_button = menu_button(items); @@ -301,16 +307,21 @@ pub fn menu_items< .width(Length::Fixed(16.0)) .into() } else { - widget::Space::with_width(Length::Fixed(16.0)).into() + widget::space::horizontal() + .width(Length::Fixed(16.0)) + .into() }, - widget::Space::with_width(spacing.space_xxs).into(), + widget::space::horizontal().width(spacing.space_xxs).into(), widget::text(label).align_x(iced::Alignment::Start).into(), - widget::horizontal_space().into(), + widget::space::horizontal().into(), widget::text(key).class(key_class).into(), ]; if let Some(icon) = icon { - items.insert(1, widget::Space::with_width(spacing.space_xxs).into()); + items.insert( + 1, + widget::space::horizontal().width(spacing.space_xxs).into(), + ); items.insert(2, widget::icon::icon(icon).size(14).into()); } @@ -325,7 +336,7 @@ pub fn menu_items< RcElementWrapper::new(crate::Element::from( menu_button::<'static, _>(vec![ widget::text(l.clone()).into(), - widget::horizontal_space().into(), + widget::space::horizontal().into(), widget::icon::from_name("pan-end-symbolic") .size(16) .icon() diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 202173ef..30b75a10 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -60,7 +60,7 @@ pub use iced::widget::{ComboBox, combo_box}; pub use iced::widget::{Container, container}; #[doc(inline)] -pub use iced::widget::{Space, horizontal_space, vertical_space}; +pub use iced::widget::{Space, space}; #[doc(inline)] pub use iced::widget::{Image, image}; @@ -175,47 +175,47 @@ pub use dialog::{Dialog, dialog}; pub mod divider { /// Horizontal variant of a divider. pub mod horizontal { - use iced::widget::{Rule, horizontal_rule}; + use iced::{widget::Rule, widget::rule}; /// Horizontal divider with default thickness #[must_use] pub fn default<'a>() -> Rule<'a, crate::Theme> { - horizontal_rule(1).class(crate::theme::Rule::Default) + rule::horizontal(1).class(crate::theme::Rule::Default) } /// Horizontal divider with light thickness #[must_use] pub fn light<'a>() -> Rule<'a, crate::Theme> { - horizontal_rule(1).class(crate::theme::Rule::LightDivider) + rule::horizontal(1).class(crate::theme::Rule::LightDivider) } /// Horizontal divider with heavy thickness. #[must_use] pub fn heavy<'a>() -> Rule<'a, crate::Theme> { - horizontal_rule(4).class(crate::theme::Rule::HeavyDivider) + rule::horizontal(4).class(crate::theme::Rule::HeavyDivider) } } /// Vertical variant of a divider. pub mod vertical { - use iced::widget::{Rule, vertical_rule}; + use iced::widget::{Rule, rule}; /// Vertical divider with default thickness #[must_use] pub fn default<'a>() -> Rule<'a, crate::Theme> { - vertical_rule(1).class(crate::theme::Rule::Default) + rule::vertical(1).class(crate::theme::Rule::Default) } /// Vertical divider with light thickness #[must_use] pub fn light<'a>() -> Rule<'a, crate::Theme> { - vertical_rule(4).class(crate::theme::Rule::LightDivider) + rule::vertical(4).class(crate::theme::Rule::LightDivider) } /// Vertical divider with heavy thickness. #[must_use] pub fn heavy<'a>() -> Rule<'a, crate::Theme> { - vertical_rule(10).class(crate::theme::Rule::HeavyDivider) + rule::vertical(10).class(crate::theme::Rule::HeavyDivider) } } } diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index 140385bc..ad6f9206 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -180,5 +180,6 @@ pub fn nav_bar_style(theme: &Theme) -> iced_widget::container::Style { radius: cosmic.corner_radii.radius_s.into(), }, shadow: Shadow::default(), + snap: true, } } diff --git a/src/widget/popover.rs b/src/widget/popover.rs index ddc31455..951b3757 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -3,6 +3,7 @@ //! A container which displays an overlay when a popup widget is attached. +use iced::widget; use iced_core::event::{self, Event}; use iced_core::layout; use iced_core::mouse; @@ -33,6 +34,7 @@ pub enum Position { /// A container which displays overlays when a popup widget is assigned. #[must_use] pub struct Popover<'a, Message, Renderer> { + id: widget::Id, content: Element<'a, Message, crate::Theme, Renderer>, modal: bool, popup: Option>, @@ -43,6 +45,7 @@ pub struct Popover<'a, Message, Renderer> { impl<'a, Message, Renderer> Popover<'a, Message, Renderer> { pub fn new(content: impl Into>) -> Self { Self { + id: widget::Id::unique(), content: content.into(), modal: false, popup: None, @@ -51,6 +54,13 @@ impl<'a, Message, Renderer> Popover<'a, Message, Renderer> { } } + /// Set the Id + #[inline] + pub fn id(mut self, id: widget::Id) -> Self { + self.id = id; + self + } + /// A modal popup intercepts user inputs while a popup is active. #[inline] pub fn modal(mut self, modal: bool) -> Self { @@ -83,6 +93,14 @@ impl Widget where Renderer: iced_core::Renderer, { + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: widget::Id) { + self.id = id; + } + fn children(&self) -> Vec { if let Some(popup) = &self.popup { vec![Tree::new(&self.content), Tree::new(popup)] @@ -104,42 +122,53 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { let tree = content_tree_mut(tree); - self.content.as_widget().layout(tree, renderer, limits) + self.content.as_widget_mut().layout(tree, renderer, limits) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn Operation<()>, + operation: &mut dyn Operation, ) { - self.content - .as_widget() - .operate(content_tree_mut(tree), layout, renderer, operation); + operation.container(Some(&self.id), layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( + tree, + layout + .children() + .next() + .unwrap() + .with_virtual_offset(layout.virtual_offset()), + renderer, + operation, + ); + }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { if self.popup.is_some() { if self.modal { if matches!(event, Event::Mouse(_) | Event::Touch(_)) { - return event::Status::Captured; + shell.capture_event(); + return; } } else if let Some(on_close) = self.on_close.as_ref() { if matches!( @@ -153,7 +182,7 @@ where } } - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( content_tree_mut(tree), event, layout, @@ -209,8 +238,9 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, mut translation: Vector, ) -> Option> { if let Some(popup) = &mut self.popup { @@ -248,6 +278,7 @@ where content_tree_mut(tree), layout, renderer, + viewport, translation, ) } @@ -312,7 +343,7 @@ where let limits = layout::Limits::new(Size::UNIT, bounds); let node = self .content - .as_widget() + .as_widget_mut() .layout(self.tree, renderer, &limits); match self.position { Position::Center => { @@ -353,27 +384,28 @@ where operation: &mut dyn Operation<()>, ) { self.content - .as_widget() + .as_widget_mut() .operate(self.tree, layout, renderer, operation); } - fn on_event( + fn update( &mut self, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + ) { if self.modal && matches!(event, Event::Mouse(_) | Event::Touch(_)) && !cursor_position.is_over(layout.bounds()) { - return event::Status::Captured; + shell.capture_event(); + return; } - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( self.tree, event, layout, @@ -389,7 +421,6 @@ where &self, layout: Layout<'_>, cursor_position: mouse::Cursor, - viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { if self.modal && !cursor_position.is_over(layout.bounds()) { @@ -400,7 +431,7 @@ where self.tree, layout, cursor_position, - viewport, + &layout.bounds(), renderer, ) } @@ -427,12 +458,16 @@ where fn overlay<'c>( &'c mut self, - layout: Layout<'_>, + layout: Layout<'c>, renderer: &Renderer, ) -> Option> { - self.content - .as_widget_mut() - .overlay(self.tree, layout, renderer, Default::default()) + self.content.as_widget_mut().overlay( + self.tree, + layout, + renderer, + &layout.bounds(), + Default::default(), + ) } } diff --git a/src/widget/radio.rs b/src/widget/radio.rs index ebb75ee2..831e9460 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -175,7 +175,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -186,20 +186,20 @@ where |_| layout::Node::new(Size::new(self.size, self.size)), |limits| { self.label - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) }, ) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { - self.label.as_widget().operate( + self.label.as_widget_mut().operate( &mut tree.children[0], layout.children().nth(1).unwrap(), renderer, @@ -207,20 +207,20 @@ where ); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - let status = self.label.as_widget_mut().on_event( + ) { + self.label.as_widget_mut().update( &mut tree.children[0], - event.clone(), + event, layout.children().nth(1).unwrap(), cursor, renderer, @@ -229,22 +229,19 @@ where viewport, ); - if status == event::Status::Ignored { + if !shell.is_event_captured() { match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { if cursor.is_over(layout.bounds()) { shell.publish(self.on_click.clone()); - return event::Status::Captured; + shell.capture_event(); + return; } } _ => {} } - - event::Status::Ignored - } else { - status } } @@ -359,14 +356,16 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { self.label.as_widget_mut().overlay( &mut tree.children[0], layout.children().nth(1).unwrap(), renderer, + viewport, translation, ) } diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index 632578ff..b3066ecb 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -204,7 +204,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -221,7 +221,7 @@ where } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, @@ -230,18 +230,18 @@ where self.container.operate(tree, layout, renderer, operation); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &iced_core::Rectangle, - ) -> event::Status { - self.container.on_event( + ) { + self.container.update( tree, event, layout, @@ -290,11 +290,13 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { - self.container.overlay(tree, layout, renderer, translation) + self.container + .overlay(tree, layout, renderer, viewport, translation) } fn drag_destinations( diff --git a/src/widget/rectangle_tracker/subscription.rs b/src/widget/rectangle_tracker/subscription.rs index 541862cd..02fa4329 100644 --- a/src/widget/rectangle_tracker/subscription.rs +++ b/src/widget/rectangle_tracker/subscription.rs @@ -18,10 +18,10 @@ pub fn rectangle_tracker_subscription< >( id: I, ) -> Subscription<(I, RectangleUpdate)> { - Subscription::run_with_id( - id, - stream::unfold(State::Ready, move |state| start_listening(id, state)), - ) + Subscription::run_with(id, |id| { + let id = *id; + stream::unfold(State::Ready, move |state| start_listening(id, state)) + }) } pub enum State { diff --git a/src/widget/responsive_container.rs b/src/widget/responsive_container.rs index fbc2df9e..0c7fbad3 100644 --- a/src/widget/responsive_container.rs +++ b/src/widget/responsive_container.rs @@ -6,7 +6,7 @@ use iced_core::layout; use iced_core::mouse; use iced_core::overlay; use iced_core::renderer; -use iced_core::widget::{Id, Tree, tree}; +use iced_core::widget::{Id, Operation, Tree, tree}; use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; pub(crate) fn responsive_container<'a, Message: 'static, Theme, E>( @@ -89,7 +89,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -98,7 +98,7 @@ where let unrestricted_size = self.size.unwrap_or_else(|| { let node = self.content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, &Limits::NONE); node.size() }); @@ -115,22 +115,23 @@ where let node = self .content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits); let size = node.size(); layout::Node::with_children(size, vec![node]) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation<()>, + operation: &mut dyn Operation, ) { - operation.container(Some(&self.id), layout.bounds(), &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], + operation.container(Some(&self.id), layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( + tree, layout .children() .next() @@ -142,17 +143,17 @@ where }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let state = tree.state.downcast_mut::(); if state.needs_update { @@ -166,7 +167,7 @@ where state.needs_update = false; } - self.content.as_widget_mut().on_event( + self.content.as_widget_mut().update( &mut tree.children[0], event, layout @@ -225,8 +226,9 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { self.content.as_widget_mut().overlay( @@ -237,6 +239,7 @@ where .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, + viewport, translation, ) } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 0e1af1d0..162d1d21 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -23,7 +23,7 @@ use iced::{ event, keyboard, mouse, touch, window, }; use iced_core::mouse::ScrollDelta; -use iced_core::text::{Ellipsize, LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; +use iced_core::text::{self, Ellipsize, LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; use iced_core::widget::operation::Focusable; use iced_core::widget::{self, operation, tree}; use iced_core::{Border, Point, Renderer as IcedRenderer, Shadow, Text}; @@ -265,22 +265,33 @@ where } } - let text = Text { - content: text.as_ref(), - size: iced::Pixels(self.font_size), - bounds: Size::INFINITY, - font, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, - shaping: Shaping::Advanced, - wrapping: Wrapping::None, - ellipsize: Ellipsize::None, - line_height: self.line_height, - }; - if let Some(paragraph) = state.paragraphs.get_mut(key) { + let text = Text { + content: text.as_ref(), + size: iced::Pixels(self.font_size), + bounds: Size::INFINITE, + font, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, + shaping: Shaping::Advanced, + wrapping: Wrapping::None, + line_height: self.line_height, + ellipsize: Ellipsize::default(), + }; paragraph.update(text); } else { + let text = Text { + content: text.to_string(), + size: iced::Pixels(self.font_size), + bounds: Size::INFINITE, + font, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, + shaping: Shaping::Advanced, + wrapping: Wrapping::None, + line_height: self.line_height, + ellipsize: Ellipsize::default(), + }; state.paragraphs.insert(key, crate::Plain::new(text)); } } @@ -441,7 +452,7 @@ where } /// Item the previous item in the widget. - fn focus_previous(&mut self, state: &mut LocalState) -> event::Status { + fn focus_previous(&mut self, state: &mut LocalState, shell: &mut Shell<'_, Message>) { match state.focused_item { Item::Tab(entity) => { let mut keys = self.iterate_visible_tabs(state).rev(); @@ -455,7 +466,8 @@ where } state.focused_item = Item::Tab(key); - return event::Status::Captured; + shell.capture_event(); + return; } break; @@ -464,24 +476,28 @@ where if self.prev_tab_sensitive(state) { state.focused_item = Item::PrevButton; - return event::Status::Captured; + shell.capture_event(); + return; } } Item::NextButton => { if let Some(last) = self.last_tab(state) { state.focused_item = Item::Tab(last); - return event::Status::Captured; + shell.capture_event(); + return; } } Item::None => { if self.next_tab_sensitive(state) { state.focused_item = Item::NextButton; - return event::Status::Captured; + shell.capture_event(); + return; } else if let Some(last) = self.last_tab(state) { state.focused_item = Item::Tab(last); - return event::Status::Captured; + shell.capture_event(); + return; } } @@ -489,11 +505,10 @@ where } state.focused_item = Item::None; - event::Status::Ignored } /// Item the next item in the widget. - fn focus_next(&mut self, state: &mut LocalState) -> event::Status { + fn focus_next(&mut self, state: &mut LocalState, shell: &mut Shell<'_, Message>) { match state.focused_item { Item::Tab(entity) => { let mut keys = self.iterate_visible_tabs(state); @@ -506,7 +521,8 @@ where } state.focused_item = Item::Tab(key); - return event::Status::Captured; + shell.capture_event(); + return; } break; @@ -515,24 +531,28 @@ where if self.next_tab_sensitive(state) { state.focused_item = Item::NextButton; - return event::Status::Captured; + shell.capture_event(); + return; } } Item::PrevButton => { if let Some(first) = self.first_tab(state) { state.focused_item = Item::Tab(first); - return event::Status::Captured; + shell.capture_event(); + return; } } Item::None => { if self.prev_tab_sensitive(state) { state.focused_item = Item::PrevButton; - return event::Status::Captured; + shell.capture_event(); + return; } else if let Some(first) = self.first_tab(state) { state.focused_item = Item::Tab(first); - return event::Status::Captured; + shell.capture_event(); + return; } } @@ -540,7 +560,6 @@ where } state.focused_item = Item::None; - event::Status::Ignored } fn iterate_visible_tabs<'b>( @@ -595,12 +614,12 @@ where icon_spacing = f32::from(self.button_spacing); let paragraph = entry.or_insert_with(|| { crate::Plain::new(Text { - content: text.as_ref(), + content: text.to_string(), // TODO should we just use String at this point? size: iced::Pixels(self.font_size), - bounds: Size::INFINITY, + bounds: Size::INFINITE, font, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, shaping: Shaping::Advanced, wrapping: Wrapping::default(), ellipsize: Ellipsize::default(), @@ -888,7 +907,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, @@ -902,17 +921,17 @@ where } #[allow(clippy::too_many_lines)] - fn on_event( + fn update( &mut self, tree: &mut Tree, - mut event: Event, + mut event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, _renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &iced::Rectangle, - ) -> event::Status { + ) { let my_bounds = layout.bounds(); let state = tree.state.downcast_mut::(); @@ -941,7 +960,8 @@ where "tab drag source finished id={:?}", my_id ); - return event::Status::Captured; + shell.capture_event(); + return; } } DndEvent::Offer( @@ -1137,8 +1157,8 @@ where }); let (maybe_msg, ret) = state.dnd_state.on_data_received( - mem::take(mime_type), - mem::take(data), + mime_type.clone(), + data.clone(), None:: Message>, on_drop, ); @@ -1160,10 +1180,11 @@ where } if let Some(on_reorder) = self.on_reorder.as_ref() { shell.publish(on_reorder(event)); - return event::Status::Captured; + shell.capture_event(); + return; } } - return ret; + return; } } _ => {} @@ -1175,7 +1196,7 @@ where match event { Event::Touch(touch::Event::FingerPressed { id, .. }) => { - state.fingers_pressed.insert(id); + state.fingers_pressed.insert(*id); } Event::Touch(touch::Event::FingerLifted { id, .. }) => { @@ -1252,7 +1273,8 @@ where || (touch_lifted(&event) && fingers_pressed == 1)) { shell.publish(on_close(key)); - return event::Status::Captured; + shell.capture_event(); + return; } if self.on_middle_press.is_none() { @@ -1263,7 +1285,8 @@ where { if state.middle_clicked == Some(Item::Tab(key)) { shell.publish(on_close(key)); - return event::Status::Captured; + shell.capture_event(); + return; } state.middle_clicked = None; @@ -1315,7 +1338,8 @@ where state.set_focused(); state.focused_item = Item::Tab(key); state.pressed_item = None; - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -1336,7 +1360,8 @@ where }); shell.publish(on_context(key)); - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -1347,7 +1372,8 @@ where state.middle_clicked = Some(Item::Tab(key)); if let Some(on_middle_press) = self.on_middle_press.as_ref() { shell.publish(on_middle_press(key)); - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -1374,7 +1400,7 @@ where ScrollDelta::Lines { y, .. } | ScrollDelta::Pixels { y, .. } => { let mut activate_key = None; - if y < 0.0 { + if *y < 0.0 { let mut prev_key = Entity::null(); for key in self.model.order.iter().copied() { @@ -1386,7 +1412,7 @@ where prev_key = key; } } - } else if y > 0.0 { + } else if *y > 0.0 { let mut buttons = self.model.order.iter().copied(); while let Some(key) = buttons.next() { if self.model.is_active(key) { @@ -1405,7 +1431,8 @@ where shell.publish(on_activate(key)); state.set_focused(); state.focused_item = Item::Tab(key); - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -1424,7 +1451,7 @@ where if is_pressed(&event) { state.unfocus(); state.pressed_item = None; - return event::Status::Ignored; + return; } } else if is_lifted(&event) { state.pressed_item = None; @@ -1452,7 +1479,8 @@ where position, clipboard, ) { - return event::Status::Captured; + shell.capture_event(); + return; } } } @@ -1475,12 +1503,10 @@ where }) = event { state.focused_visible = true; - return if modifiers == keyboard::Modifiers::SHIFT { - self.focus_previous(state) + return if *modifiers == keyboard::Modifiers::SHIFT { + self.focus_previous(state, shell) } else if modifiers.is_empty() { - self.focus_next(state) - } else { - event::Status::Ignored + self.focus_next(state, shell) }; } @@ -1524,24 +1550,23 @@ where Item::None | Item::Set => (), } - return event::Status::Captured; + shell.capture_event(); + return; } } } - - event::Status::Ignored } fn operate( - &self, + &mut self, tree: &mut Tree, - _layout: Layout<'_>, + layout: Layout<'_>, _renderer: &Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { let state = tree.state.downcast_mut::(); - operation.focusable(state, Some(&self.id.0)); - operation.custom(state, Some(&self.id.0)); + operation.focusable(Some(&self.id.0), layout.bounds(), state); + operation.custom(Some(&self.id.0), layout.bounds(), state); if let Item::Set = state.focused_item { if self.prev_tab_sensitive(state) { @@ -1616,6 +1641,7 @@ where bounds, border: appearance.border, shadow: Shadow::default(), + snap: true, }, background, ); @@ -1644,6 +1670,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, background_appearance .background @@ -1692,6 +1719,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, background_appearance .background @@ -1747,6 +1775,7 @@ where bounds, border: Border::default(), shadow: Shadow::default(), + snap: true, }, { let theme = crate::theme::active(); @@ -1842,6 +1871,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, appearance.active.text_color, ); @@ -1878,6 +1908,7 @@ where ..Default::default() }, shadow: Shadow::default(), + snap: true, }, divider_background, ); @@ -1910,6 +1941,7 @@ where button_appearance.border }, shadow: Shadow::default(), + snap: true, }, status_appearance .background @@ -2069,8 +2101,9 @@ where fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: iced_core::Layout<'_>, + layout: iced_core::Layout<'b>, _renderer: &Renderer, + viewport: &iced_core::Rectangle, translation: Vector, ) -> Option> { let state = tree.state.downcast_mut::(); @@ -2662,6 +2695,7 @@ fn draw_drop_indicator( ..Default::default() }, shadow: Shadow::default(), + snap: true, }, Background::Color(color), ); diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index d62bbc99..a17f2071 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -5,10 +5,11 @@ use std::borrow::Cow; use crate::{ Element, theme, - widget::{FlexRow, Row, column, container, flex_row, horizontal_space, row, text}, + widget::{FlexRow, Row, column, container, flex_row, row, text}, }; use derive_setters::Setters; use iced_core::{Length, text::Wrapping}; +use iced_widget::space; use taffy::AlignContent; /// A settings item aligned in a row @@ -25,7 +26,7 @@ pub fn item<'a, Message: 'static>( ) -> Row<'a, Message> { item_row(vec![ text(title).wrapping(Wrapping::Word).into(), - horizontal_space().into(), + space::horizontal().into(), widget, ]) } diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index 9ad81b4d..833e90b8 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -313,6 +313,7 @@ fn container_style(theme: &crate::Theme) -> iced_widget::container::Style { background: None, border, shadow: Shadow::default(), + snap: true, } } diff --git a/src/widget/table/widget/compact.rs b/src/widget/table/widget/compact.rs index 0ad92166..85b5cfce 100644 --- a/src/widget/table/widget/compact.rs +++ b/src/widget/table/widget/compact.rs @@ -131,6 +131,7 @@ where ..Default::default() }, shadow: Default::default(), + snap: true, } })) .apply(widget::mouse_area) diff --git a/src/widget/table/widget/standard.rs b/src/widget/table/widget/standard.rs index c0207f06..79107074 100644 --- a/src/widget/table/widget/standard.rs +++ b/src/widget/table/widget/standard.rs @@ -192,6 +192,7 @@ where ..Default::default() }, shadow: Default::default(), + snap: true, } })) .apply(widget::mouse_area) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index e98d4cfa..5b6a53f3 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -699,7 +699,7 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -711,7 +711,7 @@ where let size = self.size.unwrap_or_else(|| renderer.default_size().0); - let bounds = limits.resolve(Length::Shrink, Length::Fill, Size::INFINITY); + let bounds = limits.resolve(Length::Shrink, Length::Fill, Size::INFINITE); let value_paragraph = &mut state.value; let v = self.value.to_string(); value_paragraph.update(Text { @@ -723,8 +723,8 @@ where font, bounds, size: iced::Pixels(size), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, line_height: text::LineHeight::default(), shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -743,8 +743,8 @@ where self.width, self.padding, self.size, - self.leading_icon.as_ref(), - self.trailing_icon.as_ref(), + self.leading_icon.as_mut(), + self.trailing_icon.as_mut(), self.line_height, self.label.as_deref(), self.helper_text.as_deref(), @@ -780,24 +780,25 @@ where } fn operate( - &self, + &mut self, tree: &mut Tree, - _layout: Layout<'_>, - _renderer: &crate::Renderer, - operation: &mut dyn Operation<()>, + layout: Layout<'_>, + renderer: &crate::Renderer, + operation: &mut dyn Operation, ) { + operation.container(Some(&self.id), layout.bounds()); let state = tree.state.downcast_mut::(); - operation.custom(state, Some(&self.id)); - operation.focusable(state, Some(&self.id)); - operation.text_input(state, Some(&self.id)); + operation.focusable(Some(&self.id), layout.bounds(), state); + operation.text_input(Some(&self.id), layout.bounds(), state); } fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { let mut layout_ = Vec::with_capacity(2); @@ -823,24 +824,24 @@ where .filter_map(|((child, state), layout)| { child .as_widget_mut() - .overlay(state, layout, renderer, translation) + .overlay(state, layout, renderer, viewport, translation) }) .collect::>(); (!children.is_empty()).then(|| Group::with_children(children).overlay()) } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let text_layout = self.text_layout(layout); let mut trailing_icon_layout = None; let font = self.font.unwrap_or_else(|| renderer.default_font()); @@ -877,9 +878,9 @@ where // Enable custom buttons defined on the trailing icon position to be handled. if !self.is_editable_variant { if let Some(trailing_layout) = trailing_icon_layout { - let res = trailing_icon.as_widget_mut().on_event( + let res = trailing_icon.as_widget_mut().update( tree, - event.clone(), + event, trailing_layout, cursor_position, renderer, @@ -888,8 +889,8 @@ where viewport, ); - if res == event::Status::Captured { - return res; + if shell.is_event_captured() { + return; } } } @@ -1133,8 +1134,8 @@ pub fn layout( width: Length, padding: Padding, size: Option, - leading_icon: Option<&Element<'_, Message, crate::Theme, crate::Renderer>>, - trailing_icon: Option<&Element<'_, Message, crate::Theme, crate::Renderer>>, + leading_icon: Option<&mut Element<'_, Message, crate::Theme, crate::Renderer>>, + trailing_icon: Option<&mut Element<'_, Message, crate::Theme, crate::Renderer>>, line_height: text::LineHeight, label: Option<&str>, helper_text: Option<&str>, @@ -1148,7 +1149,7 @@ pub fn layout( let mut nodes = Vec::with_capacity(3); let text_pos = if let Some(label) = label { - let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITY); + let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITE); let state = tree.state.downcast_mut::(); let label_paragraph = &mut state.label; label_paragraph.update(Text { @@ -1156,8 +1157,8 @@ pub fn layout( font, bounds: text_bounds, size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -1186,7 +1187,7 @@ pub fn layout( let (leading_icon_width, mut leading_icon) = if let Some((icon, tree)) = leading_icon.zip(children.get_mut(c_i)) { let size = icon.as_widget().size(); - let icon_node = icon.as_widget().layout( + let icon_node = icon.as_widget_mut().layout( tree, renderer, &Limits::NONE.width(size.width).height(size.height), @@ -1201,7 +1202,7 @@ pub fn layout( let (trailing_icon_width, mut trailing_icon) = if let Some((icon, tree)) = trailing_icon.zip(children.get_mut(c_i)) { let size = icon.as_widget().size(); - let icon_node = icon.as_widget().layout( + let icon_node = icon.as_widget_mut().layout( tree, renderer, &Limits::NONE.width(size.width).height(size.height), @@ -1214,7 +1215,7 @@ pub fn layout( let text_limits = limits .width(width) .height(line_height.to_absolute(text_size.into())); - let text_bounds = text_limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITY); + let text_bounds = text_limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITE); let text_node = layout::Node::new( text_bounds - Size::new(leading_icon_width + trailing_icon_width, 0.0), ) @@ -1266,9 +1267,9 @@ pub fn layout( } else { let limits = limits .width(width) - .height(text_input_height + padding.vertical()) + .height(text_input_height + padding.y()) .shrink(padding); - let text_bounds = limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITY); + let text_bounds = limits.resolve(Length::Shrink, Length::Shrink, Size::INFINITE); let text = layout::Node::new(text_bounds).move_to(Point::new(padding.left, padding.top)); @@ -1286,7 +1287,7 @@ pub fn layout( .width(width) .shrink(padding) .height(helper_text_line_height.to_absolute(helper_text_size.into())); - let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITY); + let text_bounds = limits.resolve(width, Length::Shrink, Size::INFINITE); let state = tree.state.downcast_mut::(); let helper_text_paragraph = &mut state.helper_text; helper_text_paragraph.update(Text { @@ -1294,8 +1295,8 @@ pub fn layout( font, bounds: text_bounds, size: iced::Pixels(helper_text_size), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, line_height: helper_text_line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -1332,7 +1333,7 @@ pub fn layout( #[allow(clippy::cast_possible_truncation)] pub fn update<'a, Message: Clone + 'static>( id: Option, - event: Event, + event: &Event, text_layout: Layout<'_>, edit_button_layout: Option>, cursor: mouse::Cursor, @@ -1357,7 +1358,7 @@ pub fn update<'a, Message: Clone + 'static>( layout: Layout<'_>, manage_value: bool, drag_threshold: f32, -) -> event::Status { +) { let update_cache = |state, value| { replace_paragraph( state, @@ -1420,7 +1421,8 @@ pub fn update<'a, Message: Clone + 'static>( }); } - return event::Status::Captured; + shell.capture_event(); + return; } let target = cursor_position.x - text_layout.bounds().x; @@ -1461,13 +1463,15 @@ pub fn update<'a, Message: Clone + 'static>( if cursor.is_over(selection_bounds) && (on_input.is_some() || manage_value) { state.dragging_state = Some(DraggingState::PrepareDnd(cursor_position)); - return event::Status::Captured; + shell.capture_event(); + return; } // clear selection and place cursor at click position update_cache(state, value); state.setting_selection(value, text_layout.bounds(), target); state.dragging_state = None; - return event::Status::Captured; + shell.capture_event(); + return; } (None, click::Kind::Single, _) => { state.setting_selection(value, text_layout.bounds(), target); @@ -1528,7 +1532,8 @@ pub fn update<'a, Message: Clone + 'static>( state.last_click = Some(click); - return event::Status::Captured; + shell.capture_event(); + return; } else { state.unfocus(); @@ -1551,12 +1556,10 @@ pub fn update<'a, Message: Clone + 'static>( } } state.dragging_state = None; - - return if cursor.is_over(layout.bounds()) { - event::Status::Captured - } else { - event::Status::Ignored - }; + if cursor.is_over(layout.bounds()) { + shell.capture_event(); + } + return; } Event::Mouse(mouse::Event::CursorMoved { position }) | Event::Touch(touch::Event::FingerMoved { position, .. }) => { @@ -1573,7 +1576,8 @@ pub fn update<'a, Message: Clone + 'static>( .cursor .select_range(state.cursor.start(value), position); - return event::Status::Captured; + shell.capture_event(); + return; } #[cfg(feature = "wayland")] if let Some(DraggingState::PrepareDnd(start_position)) = state.dragging_state { @@ -1583,7 +1587,7 @@ pub fn update<'a, Message: Clone + 'static>( if distance >= drag_threshold { if is_secure { - return event::Status::Ignored; + return; } let input_text = state.selected_text(&value.to_string()).unwrap_or_default(); @@ -1625,7 +1629,8 @@ pub fn update<'a, Message: Clone + 'static>( state.dragging_state = Some(DraggingState::PrepareDnd(start_position)); } - return event::Status::Captured; + shell.capture_event(); + return; } } Event::Keyboard(keyboard::Event::KeyPressed { @@ -1636,11 +1641,11 @@ pub fn update<'a, Message: Clone + 'static>( .. }) => { let state = state(); - state.keyboard_modifiers = modifiers; + state.keyboard_modifiers = *modifiers; if let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) { if state.is_read_only || (!manage_value && on_input.is_none()) { - return event::Status::Ignored; + return; }; let modifiers = state.keyboard_modifiers; focus.updated_at = Instant::now(); @@ -1724,12 +1729,14 @@ pub fn update<'a, Message: Clone + 'static>( }; update_cache(state, &value); - return event::Status::Captured; + shell.capture_event(); + return; } keyboard::Key::Character("a") | keyboard::Key::Character("A") => { state.cursor.select_all(value); - return event::Status::Captured; + shell.capture_event(); + return; } _ => {} @@ -1737,9 +1744,12 @@ pub fn update<'a, Message: Clone + 'static>( } // Capture keyboard inputs that should be submitted. - if let Some(c) = text.and_then(|t| t.chars().next().filter(|c| !c.is_control())) { + if let Some(c) = text + .as_ref() + .and_then(|t| t.chars().next().filter(|c| !c.is_control())) + { if state.is_read_only || (!manage_value && on_input.is_none()) { - return event::Status::Ignored; + return; }; state.is_pasting = None; @@ -1769,7 +1779,8 @@ pub fn update<'a, Message: Clone + 'static>( update_cache(state, &value); - return event::Status::Captured; + shell.capture_event(); + return; } } @@ -1902,19 +1913,20 @@ pub fn update<'a, Message: Clone + 'static>( shell.publish(on_unfocus.clone()); } - return event::Status::Ignored; + return; }; } keyboard::Key::Named( keyboard::key::Named::ArrowUp | keyboard::key::Named::ArrowDown, ) => { - return event::Status::Ignored; + return; } _ => {} } - return event::Status::Captured; + shell.capture_event(); + return; } } Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { @@ -1928,31 +1940,30 @@ pub fn update<'a, Message: Clone + 'static>( keyboard::Key::Named(keyboard::key::Named::Tab) | keyboard::Key::Named(keyboard::key::Named::ArrowUp) | keyboard::Key::Named(keyboard::key::Named::ArrowDown) => { - return event::Status::Ignored; + return; } _ => {} } - return event::Status::Captured; + shell.capture_event(); + return; } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { let state = state(); - state.keyboard_modifiers = modifiers; + state.keyboard_modifiers = *modifiers; } Event::Window(window::Event::RedrawRequested(now)) => { let state = state(); if let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) { - focus.now = now; + focus.now = *now; let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS - - (now - focus.updated_at).as_millis() % CURSOR_BLINK_INTERVAL_MILLIS; + - (*now - focus.updated_at).as_millis() % CURSOR_BLINK_INTERVAL_MILLIS; - shell.request_redraw(window::RedrawRequest::At( - now + Duration::from_millis(u64::try_from(millis_until_redraw).unwrap()), - )); + shell.request_redraw(); } } #[cfg(feature = "wayland")] @@ -1962,7 +1973,8 @@ pub fn update<'a, Message: Clone + 'static>( if matches!(state.dragging_state, Some(DraggingState::Dnd(..))) { // TODO: restore value in text input state.dragging_state = None; - return event::Status::Captured; + shell.capture_event(); + return; } } #[cfg(feature = "wayland")] @@ -1974,23 +1986,23 @@ pub fn update<'a, Message: Clone + 'static>( mime_types, surface, }, - )) if rectangle == Some(dnd_id) => { + )) if *rectangle == Some(dnd_id) => { cold(); let state = state(); let is_clicked = text_layout.bounds().contains(Point { - x: x as f32, - y: y as f32, + x: *x as f32, + y: *y as f32, }); let mut accepted = false; - for m in &mime_types { + for m in mime_types { if SUPPORTED_TEXT_MIME_TYPES.contains(&m.as_str()) { let clone = m.clone(); accepted = true; } } if accepted { - let target = x as f32 - text_layout.bounds().x; + let target = *x as f32 - text_layout.bounds().x; state.dnd_offer = DndOfferState::HandlingOffer(mime_types.clone(), DndAction::empty()); // existing logic for setting the selection @@ -2002,16 +2014,17 @@ pub fn update<'a, Message: Clone + 'static>( }; state.cursor.move_to(position.unwrap_or(0)); - return event::Status::Captured; + shell.capture_event(); + return; } } #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y })) - if rectangle == Some(dnd_id) => + if *rectangle == Some(dnd_id) => { let state = state(); - let target = x as f32 - text_layout.bounds().x; + let target = *x as f32 - text_layout.bounds().x; // existing logic for setting the selection let position = if target > 0.0 { update_cache(state, value); @@ -2021,10 +2034,11 @@ pub fn update<'a, Message: Clone + 'static>( }; state.cursor.move_to(position.unwrap_or(0)); - return event::Status::Captured; + shell.capture_event(); + return; } #[cfg(feature = "wayland")] - Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if rectangle == Some(dnd_id) => { + Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if *rectangle == Some(dnd_id) => { cold(); let state = state(); if let DndOfferState::HandlingOffer(mime_types, _action) = state.dnd_offer.clone() { @@ -2033,15 +2047,16 @@ pub fn update<'a, Message: Clone + 'static>( .find(|&&m| mime_types.iter().any(|t| t == m)) else { state.dnd_offer = DndOfferState::None; - return event::Status::Captured; + shell.capture_event(); + return; }; state.dnd_offer = DndOfferState::Dropped; } - return event::Status::Ignored; + return; } #[cfg(feature = "wayland")] - Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != id => {} + Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != *id => {} #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Offer( rectangle, @@ -2057,21 +2072,24 @@ pub fn update<'a, Message: Clone + 'static>( state.dnd_offer = DndOfferState::None; } }; - return event::Status::Captured; + shell.capture_event(); + return; } #[cfg(feature = "wayland")] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type })) - if rectangle == Some(dnd_id) => + if *rectangle == Some(dnd_id) => { cold(); let state = state(); if matches!(&state.dnd_offer, DndOfferState::Dropped) { state.dnd_offer = DndOfferState::None; if !SUPPORTED_TEXT_MIME_TYPES.contains(&mime_type.as_str()) || data.is_empty() { - return event::Status::Captured; + shell.capture_event(); + return; } - let Ok(content) = String::from_utf8(data) else { - return event::Status::Captured; + let Ok(content) = String::from_utf8(data.clone()) else { + shell.capture_event(); + return; }; let mut editor = Editor::new(unsecured_value, &mut state.cursor); @@ -2091,14 +2109,13 @@ pub fn update<'a, Message: Clone + 'static>( unsecured_value }; update_cache(state, &value); - return event::Status::Captured; + shell.capture_event(); + return; } - return event::Status::Ignored; + return; } _ => {} } - - event::Status::Ignored } /// Draws the [`TextInput`] with the given [`Renderer`], overriding its @@ -2212,6 +2229,7 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, + snap: true, }, appearance.background, ); @@ -2228,6 +2246,7 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, + snap: true, }, Background::Color(Color::TRANSPARENT), ); @@ -2245,6 +2264,7 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, + snap: true, }, appearance.background, ); @@ -2258,8 +2278,8 @@ pub fn draw<'a, Message>( size: iced::Pixels(size.unwrap_or_else(|| renderer.default_size().0)), font: font.unwrap_or_else(|| renderer.default_font()), bounds: label_layout.bounds().size(), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -2353,6 +2373,7 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, + snap: true, }, text_color, )), @@ -2403,6 +2424,7 @@ pub fn draw<'a, Message>( color: Color::TRANSPARENT, blur_radius: 0.0, }, + snap: true, }, appearance.selected_fill, )), @@ -2448,8 +2470,8 @@ pub fn draw<'a, Message>( font, bounds: bounds.size(), size: iced::Pixels(size), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Center, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, line_height: text::LineHeight::default(), shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -2497,8 +2519,8 @@ pub fn draw<'a, Message>( size: iced::Pixels(helper_text_size), font, bounds: helper_text_layout.bounds().size(), - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, line_height: helper_line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -2811,6 +2833,14 @@ impl operation::TextInput for State { fn select_all(&mut self) { Self::select_all(self); } + + fn text(&self) -> &str { + todo!() + } + + fn select_range(&mut self, start: usize, end: usize) { + todo!() + } } #[inline(never)] @@ -2876,11 +2906,11 @@ fn replace_paragraph( state.value = crate::Plain::new(Text { font, line_height, - content: &value.to_string(), + content: value.to_string(), bounds, size: text_size, - horizontal_alignment: alignment::Horizontal::Left, - vertical_alignment: alignment::Vertical::Top, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, ellipsize: text::Ellipsize::None, diff --git a/src/widget/toaster/widget.rs b/src/widget/toaster/widget.rs index 52604592..240e4867 100644 --- a/src/widget/toaster/widget.rs +++ b/src/widget/toaster/widget.rs @@ -45,13 +45,13 @@ where } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { self.content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) } @@ -85,29 +85,29 @@ where } fn operate<'b>( - &'b self, + &'b mut self, state: &'b mut Tree, layout: Layout<'_>, renderer: &Renderer, operation: &mut dyn Operation<()>, ) { self.content - .as_widget() + .as_widget_mut() .operate(&mut state.children[0], layout, renderer, operation); } - fn on_event( + fn update( &mut self, state: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { - self.content.as_widget_mut().on_event( + ) { + self.content.as_widget_mut().update( &mut state.children[0], event, layout, @@ -139,8 +139,9 @@ where fn overlay<'b>( &'b mut self, state: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &Renderer, + viewport: &Rectangle, translation: Vector, ) -> Option> { //TODO: this hides the overlay of the content during the toast @@ -149,6 +150,7 @@ where &mut state.children[0], layout, renderer, + viewport, translation, ) } else { @@ -201,7 +203,7 @@ where let node = self .element - .as_widget() + .as_widget_mut() .layout(self.state, renderer, &limits); let offset = 15.; @@ -228,16 +230,16 @@ where .draw(self.state, renderer, theme, style, layout, cursor, &bounds); } - fn on_event( + fn update( &mut self, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell, - ) -> event::Status { - self.element.as_widget_mut().on_event( + ) { + self.element.as_widget_mut().update( self.state, event, layout, @@ -253,22 +255,29 @@ where &self, layout: Layout<'_>, cursor: mouse::Cursor, - viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - self.element - .as_widget() - .mouse_interaction(self.state, layout, cursor, viewport, renderer) + self.element.as_widget().mouse_interaction( + self.state, + layout, + cursor, + &layout.bounds(), + renderer, + ) } fn overlay<'c>( &'c mut self, - layout: Layout<'_>, + layout: Layout<'c>, renderer: &Renderer, ) -> Option> { - self.element - .as_widget_mut() - .overlay(self.state, layout, renderer, Default::default()) + self.element.as_widget_mut().overlay( + self.state, + layout, + renderer, + &layout.bounds(), + Default::default(), + ) } } diff --git a/src/widget/warning.rs b/src/widget/warning.rs index 942ffb8b..4153d647 100644 --- a/src/widget/warning.rs +++ b/src/widget/warning.rs @@ -73,5 +73,6 @@ pub fn warning_container(theme: &Theme) -> widget::container::Style { offset: iced::Vector::new(0.0, 0.0), blur_radius: 0.0, }, + snap: true, } } diff --git a/src/widget/wayland/tooltip/widget.rs b/src/widget/wayland/tooltip/widget.rs index 5194d5c7..ceb234a9 100644 --- a/src/widget/wayland/tooltip/widget.rs +++ b/src/widget/wayland/tooltip/widget.rs @@ -211,7 +211,7 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> } fn layout( - &self, + &mut self, tree: &mut Tree, renderer: &crate::Renderer, limits: &layout::Limits, @@ -224,22 +224,23 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> self.padding, |renderer, limits| { self.content - .as_widget() + .as_widget_mut() .layout(&mut tree.children[0], renderer, limits) }, ) } fn operate( - &self, + &mut self, tree: &mut Tree, layout: Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn Operation<()>, ) { - operation.container(None, layout.bounds(), &mut |operation| { - self.content.as_widget().operate( - &mut tree.children[0], + operation.container(Some(&self.id), layout.bounds()); + operation.traverse(&mut |operation| { + self.content.as_widget_mut().operate( + tree, layout .children() .next() @@ -251,17 +252,17 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> }); } - fn on_event( + fn update( &mut self, tree: &mut Tree, - event: Event, + event: &Event, layout: Layout<'_>, cursor: mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, viewport: &Rectangle, - ) -> event::Status { + ) { let status = update( self.id.clone(), event.clone(), @@ -275,22 +276,21 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> &self.on_surface_action, || tree.state.downcast_mut::(), ); - status.merge( - self.content.as_widget_mut().on_event( - &mut tree.children[0], - event, - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ), - ) + + self.content.as_widget_mut().update( + &mut tree.children[0], + event, + layout + .children() + .next() + .unwrap() + .with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); } #[allow(clippy::too_many_lines)] @@ -359,8 +359,9 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> fn overlay<'b>( &'b mut self, tree: &'b mut Tree, - layout: Layout<'_>, + layout: Layout<'b>, renderer: &crate::Renderer, + viewport: &Rectangle, mut translation: Vector, ) -> Option> { let position = layout.bounds().position(); @@ -374,6 +375,7 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> .unwrap() .with_virtual_offset(layout.virtual_offset()), renderer, + viewport, translation, ) } @@ -451,7 +453,7 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( on_leave: &Message, on_surface_action: &dyn Fn(crate::surface::Action) -> Message, state: impl FnOnce() -> &'a mut State, -) -> event::Status { +) { match event { Event::Touch(touch::Event::FingerLifted { .. }) => { let state = state(); @@ -461,7 +463,8 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( shell.publish(on_leave.clone()); - return event::Status::Captured; + shell.capture_event(); + return; } } @@ -579,8 +582,6 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( } _ => {} } - - event::Status::Ignored } #[allow(clippy::too_many_arguments)] @@ -611,6 +612,7 @@ pub fn draw( radius: styling.border_radius, }, shadow: Shadow::default(), + snap: true, }, Color::TRANSPARENT, ); @@ -632,6 +634,7 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), + snap: true, }, Background::Color([0.0, 0.0, 0.0, 0.5].into()), ); @@ -647,6 +650,7 @@ pub fn draw( ..Default::default() }, shadow: Shadow::default(), + snap: true, }, background, ); @@ -669,6 +673,7 @@ pub fn draw( radius: styling.border_radius, }, shadow: Shadow::default(), + snap: true, }, Color::TRANSPARENT, ); diff --git a/src/widget/wrapper.rs b/src/widget/wrapper.rs index 59c0a376..73e476fa 100644 --- a/src/widget/wrapper.rs +++ b/src/widget/wrapper.rs @@ -90,7 +90,7 @@ impl Widget for RcElementWrapper { } fn layout( - &self, + &mut self, tree: &mut tree::Tree, renderer: &crate::Renderer, limits: &crate::iced_core::layout::Limits, @@ -132,30 +132,31 @@ impl Widget for RcElementWrapper { } fn operate( - &self, + &mut self, state: &mut tree::Tree, layout: crate::iced_core::Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn widget::Operation, ) { - self.element.with_data(|e| { - e.as_widget().operate(state, layout, renderer, operation); + self.element.with_data_mut(|e| { + e.as_widget_mut() + .operate(state, layout, renderer, operation); }); } - fn on_event( + fn update( &mut self, state: &mut tree::Tree, - event: crate::iced::Event, + event: &crate::iced::Event, layout: crate::iced_core::Layout<'_>, cursor: crate::iced_core::mouse::Cursor, renderer: &crate::Renderer, clipboard: &mut dyn crate::iced_core::Clipboard, shell: &mut crate::iced_core::Shell<'_, M>, viewport: &Rectangle, - ) -> event::Status { + ) { self.element.with_data_mut(|e| { - e.as_widget_mut().on_event( + e.as_widget_mut().update( state, event, layout, cursor, renderer, clipboard, shell, viewport, ) }) @@ -178,15 +179,16 @@ impl Widget for RcElementWrapper { fn overlay<'a>( &'a mut self, state: &'a mut tree::Tree, - layout: crate::iced_core::Layout<'_>, + layout: crate::iced_core::Layout<'a>, renderer: &crate::Renderer, + viewport: &Rectangle, translation: crate::iced_core::Vector, ) -> Option> { assert_eq!(self.element.thread_id, thread::current().id()); Rc::get_mut(&mut self.element.data).and_then(|e| { e.get_mut() .as_widget_mut() - .overlay(state, layout, renderer, translation) + .overlay(state, layout, renderer, viewport, translation) }) } From e8d53b14ea348bd42223ddc40bd2e463f87bf401 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 19 Feb 2026 18:15:22 -0500 Subject: [PATCH 002/116] chore: various fixes and some cleanup --- Cargo.toml | 22 +- examples/applet/Cargo.toml | 2 + examples/applet/src/window.rs | 15 +- examples/application/Cargo.toml | 7 +- examples/application/src/main.rs | 9 +- src/anim.rs | 51 +++ src/app/mod.rs | 30 +- src/applet/mod.rs | 3 +- src/lib.rs | 2 + src/widget/autosize.rs | 6 +- src/widget/button/widget.rs | 1 - src/widget/cards.rs | 587 ++++++++++++++++++++++++++ src/widget/context_menu.rs | 2 +- src/widget/dnd_destination.rs | 4 +- src/widget/dnd_source.rs | 48 +-- src/widget/header_bar.rs | 6 +- src/widget/id_container.rs | 6 +- src/widget/list/column.rs | 1 + src/widget/menu/menu_bar.rs | 12 +- src/widget/menu/menu_inner.rs | 3 +- src/widget/mod.rs | 4 + src/widget/popover.rs | 23 +- src/widget/radio.rs | 2 +- src/widget/responsive_container.rs | 4 +- src/widget/segmented_button/widget.rs | 7 +- src/widget/toggler.rs | 429 ++++++++++++++++++- src/widget/wayland/tooltip/widget.rs | 2 +- 27 files changed, 1181 insertions(+), 107 deletions(-) create mode 100644 src/anim.rs create mode 100644 src/widget/cards.rs diff --git a/Cargo.toml b/Cargo.toml index 62b8ee7c..01b50733 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,27 +8,28 @@ rust-version = "1.90" name = "cosmic" [features] -# default = ["dbus-config", "multi-window", "a11y"] -default = [ "debug", +default = [ "winit", "tokio", - # "xdg-portal", - "a11y", - "wgpu", - "single-instance", - "surface-message", + "a11y", "dbus-config", "x11", "wayland", "multi-window", - "about","animated-image","autosize", "dbus-config", "pipewire", "process", "rfd", "desktop", "desktop-systemd-scope", "serde-keycode", "qr_code", "markdown", "highlighter" -] +] # default = ["dbus-config", "multi-window", "a11y"] # Accessibility support a11y = ["iced/a11y", "iced_accessibility"] # Enable about widget about = [] # Builds support for animated images -animated-image = ["dep:async-fs", "image/gif", "image/webp", "image/png", "tokio?/io-util", "tokio?/fs"] +animated-image = [ + "dep:async-fs", + "image/gif", + "image/webp", + "image/png", + "tokio?/io-util", + "tokio?/fs", +] # XXX autosize should not be used on winit windows unless dialogs autosize = [] applet = [ @@ -155,6 +156,7 @@ tracing = "0.1.44" unicode-segmentation = "1.12" url = "2.5.8" zbus = { version = "5.13.2", default-features = false, optional = true } +float-cmp = "0.10.0" # Enable DBus feature on Linux targets [target.'cfg(target_os = "linux")'.dependencies] diff --git a/examples/applet/Cargo.toml b/examples/applet/Cargo.toml index f97bff44..844ad8ff 100644 --- a/examples/applet/Cargo.toml +++ b/examples/applet/Cargo.toml @@ -13,6 +13,8 @@ env_logger = "0.10.2" log = "0.4.29" [dependencies.libcosmic] +# path = "../../" +branch = "iced-rebase" git = "https://github.com/pop-os/libcosmic" default-features = false features = ["applet-token"] diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 66b2040a..547863f2 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -13,6 +13,7 @@ pub struct Window { core: Core, popup: Option, example_row: bool, + toggle: bool, selected: Option, } @@ -22,6 +23,7 @@ impl Default for Window { core: Core::default(), popup: None, example_row: false, + toggle: false, selected: None, } } @@ -33,6 +35,7 @@ pub enum Message { ToggleExampleRow(bool), Selected(usize), Surface(cosmic::surface::Action), + Toggle(bool), } impl cosmic::Application for Window { @@ -71,7 +74,6 @@ impl cosmic::Application for Window { Message::ToggleExampleRow(toggled) => { self.example_row = toggled; } - Message::Surface(a) => { return cosmic::task::message(cosmic::Action::Cosmic( cosmic::app::Action::Surface(a), @@ -80,6 +82,9 @@ impl cosmic::Application for Window { Message::Selected(i) => { self.selected = Some(i); } + Message::Toggle(v) => { + self.toggle = v; + } }; Task::none() } @@ -123,9 +128,9 @@ impl cosmic::Application for Window { "Example row", cosmic::widget::container( toggler(state.example_row) - .on_toggle(|value| Message::ToggleExampleRow(value)), - ) - .height(Length::Fixed(50.)), + .on_toggle(Message::ToggleExampleRow) + .width(Length::Fill), + ), )) .add(popup_dropdown( &["1", "asdf", "hello", "test"], @@ -155,7 +160,7 @@ impl cosmic::Application for Window { "oops".into() } - fn style(&self) -> Option { + fn style(&self) -> Option { Some(cosmic::applet::style()) } } diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index 35ff3d30..b1ac1242 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -8,12 +8,11 @@ default = ["wayland"] wayland = ["libcosmic/wayland"] [dependencies] -tracing = "0.1.44" -tracing-subscriber = "0.3.22" -tracing-log = "0.2.0" +env_logger = "0.11" [dependencies.libcosmic] -path = "../../" +git = "https://github.com/pop-os/libcosmic" +branch = "iced-rebase" features = [ "debug", "winit", diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 45805579..831a47f1 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -54,8 +54,9 @@ impl widget::menu::Action for Action { /// Runs application with these settings #[rustfmt::skip] fn main() -> Result<(), Box> { - // tracing_subscriber::fmt::init(); - // let _ = tracing_log::LogTracer::init(); + + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); + let input = vec![ (Page::Page1, "🖖 Hello from libcosmic.".into()), @@ -66,9 +67,7 @@ fn main() -> Result<(), Box> { let settings = Settings::default() .size(Size::new(1024., 768.)); - - cosmic::app::run::(settings, input)?; - + cosmic::app::run::(settings, input).unwrap(); Ok(()) } diff --git a/src/anim.rs b/src/anim.rs new file mode 100644 index 00000000..3186ff2e --- /dev/null +++ b/src/anim.rs @@ -0,0 +1,51 @@ +use std::time::{Duration, Instant}; + +/// A simple linear interpolation calculation function. +/// p = `percent_complete` in decimal form +#[must_use] +pub fn lerp(start: f32, end: f32, p: f32) -> f32 { + (1.0 - p) * start + p * end +} + +/// A fast smooth interpolation calculation function. +/// p = `percent_complete` in decimal form +#[must_use] +pub fn slerp(start: f32, end: f32, p: f32) -> f32 { + let t = smootherstep(p); + (1.0 - t) * start + t * end +} + +/// utility function which maps a value [0, 1] -> [0, 1] using the smootherstep function +pub fn smootherstep(t: f32) -> f32 { + (6.0 * t.powi(5) - 15.0 * t.powi(4) + 10.0 * t.powi(3)).clamp(0.0, 1.0) +} + +#[derive(Default, Debug)] +pub struct State { + pub last_change: Option, +} + +impl State { + pub fn changed(&mut self, dur: Duration) { + let t = self.t(dur, false); + let diff = dur.mul_f32(t.abs()); + let now = Instant::now(); + self.last_change = Some(now.checked_sub(diff).unwrap_or(now)); + } + + pub fn anim_done(&mut self, dur: Duration) { + if self + .last_change + .is_some_and(|t| Instant::now().duration_since(t) > dur) + { + self.last_change = None; + } + } + + pub fn t(&self, dur: Duration, forward: bool) -> f32 { + let res = self.last_change.map_or(1., |t| { + Instant::now().duration_since(t).as_millis() as f32 / dur.as_millis() as f32 + }); + if forward { res } else { 1. - res } + } +} diff --git a/src/app/mod.rs b/src/app/mod.rs index 1287dc27..abda71c1 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -12,6 +12,7 @@ use cosmic_config::CosmicConfigEntry; pub mod context_drawer; pub use context_drawer::{ContextDrawer, context_drawer}; use iced::application::BootFn; +use iced_core::Widget; pub mod cosmic; pub mod settings; @@ -93,6 +94,7 @@ pub(crate) fn iced_settings( pub(crate) struct BootDataInner { pub flags: A::Flags, pub core: Core, + pub settings: window::Settings, } pub(crate) struct BootData(pub Rc>>>); @@ -102,8 +104,23 @@ impl BootFn, crate::Action (cosmic::Cosmic, iced::Task>) { let mut data = self.0.borrow_mut(); - let data = data.take().unwrap(); - cosmic::Cosmic::::init((data.core, data.flags)) + let mut data = data.take().unwrap(); + let mut tasks = Vec::new(); + #[cfg(feature = "multi-window")] + if data.core.main_window_id().is_some() { + let window_task = iced_runtime::task::oneshot(|channel| { + iced_runtime::Action::Window(iced_runtime::window::Action::Open( + window::Id::RESERVED, + data.settings, + channel, + )) + }); + data.core.set_main_window_id(Some(window::Id::RESERVED)); + tasks.push(window_task.discard()); + } + let (a, t) = cosmic::Cosmic::::init((data.core, data.flags)); + tasks.push(t); + (a, Task::batch(tasks)) } } /// Launch a COSMIC application with the given [`Settings`]. @@ -127,6 +144,7 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res BootData(Rc::new(RefCell::new(Some(BootDataInner:: { flags, core, + settings: window_settings.clone(), })))), cosmic::Cosmic::update, cosmic::Cosmic::view, @@ -147,10 +165,11 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res // app = app.window(window_settings); core.main_window = Some(iced_core::window::Id::RESERVED); } - let mut app = iced::daemon( + let app = iced::daemon( BootData(Rc::new(RefCell::new(Some(BootDataInner:: { flags, core, + settings: window_settings, })))), cosmic::Cosmic::update, cosmic::Cosmic::view, @@ -240,6 +259,7 @@ where BootData(Rc::new(RefCell::new(Some(BootDataInner:: { flags, core, + settings: window_settings.clone(), })))), cosmic::Cosmic::update, cosmic::Cosmic::view, @@ -263,6 +283,7 @@ where BootData(Rc::new(RefCell::new(Some(BootDataInner:: { flags, core, + settings: window_settings, })))), cosmic::Cosmic::update, cosmic::Cosmic::view, @@ -700,7 +721,7 @@ impl ApplicationExt for App { [0, 0, 0, 0] }) .into(), - ) + ); } else { //TODO: this element is added to workaround state issues widgets.push(space::horizontal().width(Length::Shrink).into()); @@ -710,6 +731,7 @@ impl ApplicationExt for App { widgets }); + let content_col = crate::widget::column::with_capacity(2) .push(content_row) .push_maybe(self.footer().map(|footer| { diff --git a/src/applet/mod.rs b/src/applet/mod.rs index ff376aab..f7fa5b62 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -392,7 +392,7 @@ impl Context { } }), ) - .width(Length::Shrink) + .width(Length::Fill) .height(Length::Shrink) .align_x(horizontal_align) .align_y(vertical_align), @@ -584,6 +584,7 @@ pub fn run(flags: App::Flags) -> iced::Result { BootData(Rc::new(RefCell::new(Some(BootDataInner:: { flags, core, + settings: window_settings, })))), cosmic::Cosmic::update, cosmic::Cosmic::view, diff --git a/src/lib.rs b/src/lib.rs index 7e61730b..1a579f96 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -18,6 +18,8 @@ pub use apply::{Also, Apply}; pub mod action; pub use action::Action; +pub mod anim; + #[cfg(feature = "winit")] pub mod app; #[cfg(feature = "winit")] diff --git a/src/widget/autosize.rs b/src/widget/autosize.rs index 6a1e6060..937aabf9 100644 --- a/src/widget/autosize.rs +++ b/src/widget/autosize.rs @@ -107,7 +107,7 @@ where } fn diff(&mut self, tree: &mut Tree) { - tree.children[0].diff(&mut self.content); + tree.diff_children(std::slice::from_mut(&mut self.content)); } fn size(&self) -> iced_core::Size { @@ -147,7 +147,7 @@ where operation.container(Some(&self.id), layout.bounds()); operation.traverse(&mut |operation| { self.content.as_widget_mut().operate( - tree, + &mut tree.children[0], layout .children() .next() @@ -193,7 +193,7 @@ where clipboard, shell, viewport, - ) + ); } fn mouse_interaction( diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 54e29786..a4e32378 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -357,7 +357,6 @@ impl<'a, Message: 'a + Clone> Widget operation, ); }); - let state = tree.state.downcast_mut::(); } fn update( diff --git a/src/widget/cards.rs b/src/widget/cards.rs new file mode 100644 index 00000000..b8e17636 --- /dev/null +++ b/src/widget/cards.rs @@ -0,0 +1,587 @@ +//! An expandable stack of cards +use std::time::Duration; + +use self::iced_core::{ + Element, Event, Length, Size, Vector, Widget, border::Radius, id::Id, layout::Node, + renderer::Quad, widget::Tree, +}; +use crate::{ + anim, + iced_core::{self, Border, Shadow}, + widget::{ + button, + card::style::Style, + column, + icon::{self, Handle}, + row, text, + }, +}; +use float_cmp::approx_eq; +use iced::widget; +use iced_core::{widget::tree, window}; + +const ICON_SIZE: u16 = 16; +const TOP_SPACING: u16 = 4; +const VERTICAL_SPACING: f32 = 8.0; +const PADDING: u16 = 16; +const BG_CARD_VISIBLE_HEIGHT: f32 = 4.0; +const BG_CARD_BORDER_RADIUS: f32 = 8.0; +const BG_CARD_MARGIN_STEP: f32 = 8.0; + +/// get an expandable stack of cards +#[allow(clippy::too_many_arguments)] +pub fn cards<'a, Message, F, G>( + id: widget::Id, + card_inner_elements: Vec>, + on_clear_all: Message, + on_show_more: Option, + on_activate: Option, + show_more_label: &'a str, + show_less_label: &'a str, + clear_all_label: &'a str, + show_less_icon: Option, + expanded: bool, +) -> Cards<'a, Message, crate::Renderer> +where + Message: 'static + Clone, + F: 'a + Fn(bool) -> Message, + G: 'a + Fn(usize) -> Message, +{ + Cards::new( + id, + card_inner_elements, + on_clear_all, + on_show_more, + on_activate, + show_more_label, + show_less_label, + clear_all_label, + show_less_icon, + expanded, + ) +} + +impl<'a, Message, Renderer> Cards<'a, Message, Renderer> +where + Renderer: iced_core::text::Renderer, +{ + fn fully_expanded(&self, t: f32) -> bool { + self.expanded && self.elements.len() > 1 && self.can_show_more && approx_eq!(f32, t, 1.0) + } + + fn fully_unexpanded(&self, t: f32) -> bool { + self.elements.len() == 1 + || (!self.expanded && (!self.can_show_more || approx_eq!(f32, t, 0.0))) + } +} + +/// An expandable stack of cards. +#[allow(missing_debug_implementations)] +pub struct Cards<'a, Message, Renderer = crate::Renderer> +where + Renderer: iced_core::text::Renderer, +{ + id: Id, + show_less_button: Element<'a, Message, crate::Theme, Renderer>, + clear_all_button: Element<'a, Message, crate::Theme, Renderer>, + elements: Vec>, + expanded: bool, + can_show_more: bool, + width: Length, + anim_multiplier: f32, + duration: Duration, +} + +impl<'a, Message> Cards<'a, Message, crate::Renderer> +where + Message: Clone + 'static, +{ + /// Get an expandable stack of cards + #[allow(clippy::too_many_arguments)] + pub fn new( + id: widget::Id, + card_inner_elements: Vec>, + on_clear_all: Message, + on_show_more: Option, + on_activate: Option, + show_more_label: &'a str, + show_less_label: &'a str, + clear_all_label: &'a str, + show_less_icon: Option, + expanded: bool, + ) -> Self + where + F: 'a + Fn(bool) -> Message, + G: 'a + Fn(usize) -> Message, + { + let can_show_more = card_inner_elements.len() > 1 && on_show_more.is_some(); + + Self { + can_show_more, + id: Id::unique(), + show_less_button: { + let mut show_less_children = Vec::with_capacity(3); + if let Some(source) = show_less_icon { + show_less_children.push(icon::icon(source).size(ICON_SIZE).into()); + } + show_less_children.push(text::body(show_less_label).width(Length::Shrink).into()); + show_less_children.push( + icon::from_name("pan-up-symbolic") + .size(ICON_SIZE) + .icon() + .into(), + ); + + let button_content = row::with_children(show_less_children) + .align_y(iced_core::Alignment::Center) + .spacing(TOP_SPACING) + .width(Length::Shrink); + + Element::from( + button::custom(button_content) + .class(crate::theme::Button::Text) + .width(Length::Shrink) + .on_press_maybe(on_show_more.as_ref().map(|f| f(false))) + .padding([PADDING / 2, PADDING]), + ) + }, + clear_all_button: Element::from( + button::custom(text(clear_all_label)) + .class(crate::theme::Button::Text) + .width(Length::Shrink) + .on_press(on_clear_all) + .padding([PADDING / 2, PADDING]), + ), + elements: card_inner_elements + .into_iter() + .enumerate() + .map(|(i, w)| { + let custom_content = if i == 0 && !expanded && can_show_more { + column::with_capacity(2) + .push(w) + .push(text::caption(show_more_label)) + .spacing(VERTICAL_SPACING) + .align_x(iced_core::Alignment::Center) + .into() + } else { + w + }; + + let b = crate::iced::widget::button(custom_content) + .class(crate::theme::iced::Button::Card) + .padding(PADDING); + if i == 0 && !expanded && can_show_more { + b.on_press_maybe(on_show_more.as_ref().map(|f| f(true))) + } else { + b.on_press_maybe(on_activate.as_ref().map(|f| f(i))) + } + .into() + }) + // we will set the width of the container to shrink, then when laying out the top bar + // we will set the fill limit to the max of the shrink top bar width and the max shrink width of the + // cards + .collect(), + width: Length::Shrink, + anim_multiplier: 1.0, + expanded, + duration: Duration::from_millis(200), + } + } + + /// Set the width of the cards stack + #[must_use] + pub fn width(mut self, width: Length) -> Self { + self.width = width; + self + } + + #[must_use] + /// The default animation time is 100ms, to speed up the toggle + /// animation use a value less than 1.0, and to slow down the + /// animation use a value greater than 1.0. + pub fn anim_multiplier(mut self, multiplier: f32) -> Self { + self.anim_multiplier = multiplier; + self + } + + pub fn duration(mut self, dur: Duration) -> Self { + self.duration = dur; + self + } + + pub fn id(mut self, id: Id) -> Self { + self.id = id; + self + } +} + +impl<'a, Message, Renderer> Widget for Cards<'a, Message, Renderer> +where + Message: 'a + Clone, + Renderer: 'a + iced_core::Renderer + iced_core::text::Renderer, +{ + fn children(&self) -> Vec { + [&self.show_less_button, &self.clear_all_button] + .iter() + .map(|w| Tree::new(w.as_widget())) + .chain(self.elements.iter().map(|w| Tree::new(w.as_widget()))) + .collect() + } + + fn diff(&mut self, tree: &mut Tree) { + let mut children: Vec<_> = vec![ + self.show_less_button.as_widget_mut(), + self.clear_all_button.as_widget_mut(), + ] + .into_iter() + .chain( + self.elements + .iter_mut() + .map(iced_core::Element::as_widget_mut), + ) + .collect(); + + tree.diff_children(children.as_mut_slice()); + } + + #[allow(clippy::too_many_lines)] + fn layout( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + limits: &iced_core::layout::Limits, + ) -> iced_core::layout::Node { + let my_state = tree.state.downcast_ref::(); + + let mut children = Vec::with_capacity(1 + self.elements.len()); + let mut size = Size::new(0.0, 0.0); + let tree_children = &mut tree.children; + let count = self.elements.len(); + if self.elements.is_empty() { + return Node::with_children(Size::new(1., 1.), children); + } + let s = anim::smootherstep(my_state.anim.t(self.duration, self.expanded)); + let fully_expanded: bool = self.fully_expanded(s); + let fully_unexpanded: bool = self.fully_unexpanded(s); + + let show_less = &mut self.show_less_button; + let clear_all = &mut self.clear_all_button; + + let show_less_node = if self.can_show_more { + show_less + .as_widget_mut() + .layout(&mut tree_children[0], renderer, limits) + } else { + Node::new(Size::default()) + }; + let clear_all_node = + clear_all + .as_widget_mut() + .layout(&mut tree_children[1], renderer, limits); + size.width += show_less_node.size().width + clear_all_node.size().width; + + let custom_limits = limits.min_width(size.width); + for (c, t) in self.elements.iter_mut().zip(tree_children[2..].iter_mut()) { + let card_node = c.as_widget_mut().layout(t, renderer, &custom_limits); + size.width = size.width.max(card_node.size().width); + } + + if fully_expanded { + let show_less = &mut self.show_less_button; + let clear_all = &mut self.clear_all_button; + + let show_less_node = if self.can_show_more { + show_less + .as_widget_mut() + .layout(&mut tree_children[0], renderer, limits) + } else { + Node::new(Size::default()) + }; + let clear_all_node = if self.can_show_more { + let mut n = + clear_all + .as_widget_mut() + .layout(&mut tree_children[1], renderer, limits); + let clear_all_node_size = n.size(); + n = clear_all_node + .translate(Vector::new(size.width - clear_all_node_size.width, 0.0)); + size.height += show_less_node.size().height.max(n.size().height) + VERTICAL_SPACING; + n + } else { + Node::new(Size::default()) + }; + + children.push(show_less_node); + children.push(clear_all_node); + } + + let custom_limits = limits + .min_width(size.width) + .max_width(size.width) + .width(Length::Fixed(size.width)); + + for (i, (c, t)) in self + .elements + .iter_mut() + .zip(tree_children[2..].iter_mut()) + .enumerate() + { + let progress = s * size.height; + let card_node = c + .as_widget_mut() + .layout(t, renderer, &custom_limits) + .translate(Vector::new(0.0, progress)); + + size.height = size.height.max(progress + card_node.size().height); + + children.push(card_node); + + if fully_unexpanded { + let width = children.last().unwrap().bounds().width; + + // push the background card nodes + for i in 1..self.elements.len().min(3) { + // height must be 16px for 8px padding + // but we only want 4px visible + + let margin = f32::from(u8::try_from(i).unwrap()) * BG_CARD_MARGIN_STEP; + let node = + Node::new(Size::new(width - 2.0 * margin, BG_CARD_BORDER_RADIUS * 2.0)) + .translate(Vector::new( + margin, + size.height - BG_CARD_BORDER_RADIUS * 2.0 + BG_CARD_VISIBLE_HEIGHT, + )); + size.height += BG_CARD_VISIBLE_HEIGHT; + children.push(node); + } + break; + } + + if i + 1 < count { + size.height += VERTICAL_SPACING; + } + } + + Node::with_children(size, children) + } + + fn draw( + &self, + state: &iced_core::widget::Tree, + renderer: &mut Renderer, + theme: &crate::Theme, + style: &iced_core::renderer::Style, + layout: iced_core::Layout<'_>, + cursor: iced_core::mouse::Cursor, + viewport: &iced_core::Rectangle, + ) { + let my_state = state.state.downcast_ref::(); + + // there are 4 cases for drawing + // 1. empty entries list + // Nothing to draw + // 2. un-expanded + // go through the layout, draw the card, the inner card, and the bg cards + // 3. expanding / unexpanding + // go through the layout. draw each card and its inner card + // 4. expanded => + // go through the layout. draw the top bar, and do all of 3 + // cards may be hovered + // any buttons may have a hover state as well + if self.elements.is_empty() { + return; + } + + let t = my_state.anim.t(self.duration, self.expanded); + let fully_unexpanded = self.fully_unexpanded(t); + let fully_expanded = self.fully_expanded(t); + + let mut layout = layout.children(); + let mut tree_children = state.children.iter(); + + if fully_expanded { + let show_less = &self.show_less_button; + let clear_all = &self.clear_all_button; + + let show_less_layout = layout.next().unwrap(); + let clear_all_layout = layout.next().unwrap(); + + show_less.as_widget().draw( + tree_children.next().unwrap(), + renderer, + theme, + style, + show_less_layout, + cursor, + viewport, + ); + + clear_all.as_widget().draw( + tree_children.next().unwrap(), + renderer, + theme, + style, + clear_all_layout, + cursor, + viewport, + ); + } else { + _ = tree_children.next(); + _ = tree_children.next(); + } + + // Draw first to appear behind + if fully_unexpanded { + let card_layout = layout.next().unwrap(); + let appearance = Style::default(); + let bg_layout = layout.collect::>(); + for (i, layout) in (0..2).zip(bg_layout.into_iter()).rev() { + renderer.fill_quad( + Quad { + bounds: layout.bounds(), + border: Border { + radius: Radius::from([ + 0.0, + 0.0, + BG_CARD_BORDER_RADIUS, + BG_CARD_BORDER_RADIUS, + ]), + ..Default::default() + }, + shadow: Shadow::default(), + snap: true, + }, + if i == 0 { + appearance.card_1 + } else { + appearance.card_2 + }, + ); + } + self.elements[0].as_widget().draw( + tree_children.next().unwrap(), + renderer, + theme, + style, + card_layout, + cursor, + viewport, + ); + } else { + let layout = layout.collect::>(); + // draw in reverse order so later cards appear behind earlier cards + for ((inner, layout), c_state) in self + .elements + .iter() + .rev() + .zip(layout.into_iter().rev()) + .zip(tree_children.rev()) + { + inner + .as_widget() + .draw(c_state, renderer, theme, style, layout, cursor, viewport); + } + } + } + + fn update( + &mut self, + state: &mut Tree, + event: &iced_core::Event, + layout: iced_core::Layout<'_>, + cursor: iced_core::mouse::Cursor, + renderer: &Renderer, + clipboard: &mut dyn iced_core::Clipboard, + shell: &mut iced_core::Shell<'_, Message>, + viewport: &iced_core::Rectangle, + ) { + if self.elements.is_empty() { + return; + } + + if let Event::Window(window::Event::RedrawRequested(_)) = event { + let state = state.state.downcast_mut::(); + + state.anim.anim_done(self.duration); + if state.anim.last_change.is_some() { + shell.request_redraw(); + shell.invalidate_layout(); + } + } + + let my_state = state.state.downcast_ref::(); + + let mut layout = layout.children(); + let mut tree_children = state.children.iter_mut(); + let t = my_state.anim.t(self.duration, self.expanded); + let fully_expanded = self.fully_expanded(t); + let fully_unexpanded = self.fully_unexpanded(t); + let show_less_state = tree_children.next(); + let clear_all_state = tree_children.next(); + + if fully_expanded { + let c_layout = layout.next().unwrap(); + let state = show_less_state.unwrap(); + self.show_less_button.as_widget_mut().update( + state, event, c_layout, cursor, renderer, clipboard, shell, viewport, + ); + + if shell.is_event_captured() { + return; + } + + let c_layout = layout.next().unwrap(); + let state = clear_all_state.unwrap(); + self.clear_all_button.as_widget_mut().update( + state, &event, c_layout, cursor, renderer, clipboard, shell, viewport, + ); + } + + if shell.is_event_captured() { + return; + } + + for ((inner, layout), c_state) in self.elements.iter_mut().zip(layout).zip(tree_children) { + inner.as_widget_mut().update( + c_state, &event, layout, cursor, renderer, clipboard, shell, viewport, + ); + if shell.is_event_captured() || fully_unexpanded { + break; + } + } + } + + fn size(&self) -> Size { + Size::new(self.width, Length::Shrink) + } + + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } +} + +impl<'a, Message> From> for Element<'a, Message, crate::Theme, crate::Renderer> +where + Message: Clone + 'a, +{ + fn from(cards: Cards<'a, Message>) -> Self { + Self::new(cards) + } +} + +#[derive(Debug, Default)] +pub struct State { + anim: anim::State, +} diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 008660a7..143a78b8 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -249,7 +249,7 @@ impl Widget } fn diff(&mut self, tree: &mut Tree) { - tree.children[0].diff(self.content.as_widget_mut()); + tree.diff_children(std::slice::from_mut(&mut self.content)); let state = tree.state.downcast_mut::(); state.menu_bar_state.inner.with_data_mut(|inner| { menu_roots_diff(self.context_menu.as_mut().unwrap(), &mut inner.tree); diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 9faa2605..b0a23fad 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -291,7 +291,7 @@ impl Widget } fn diff(&mut self, tree: &mut Tree) { - tree.children[0].diff(self.container.as_widget_mut()); + tree.diff_children(std::slice::from_mut(&mut self.container)); } fn state(&self) -> iced_core::widget::tree::State { @@ -337,7 +337,7 @@ impl Widget shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - let s = self.container.as_widget_mut().update( + self.container.as_widget_mut().update( &mut tree.children[0], event, layout, diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index c8627482..07b448a5 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -131,21 +131,25 @@ impl< ); } + #[must_use] pub fn on_start(mut self, on_start: Option) -> Self { self.on_start = on_start; self } + #[must_use] pub fn on_cancel(mut self, on_cancelled: Option) -> Self { self.on_cancelled = on_cancelled; self } + #[must_use] pub fn on_finish(mut self, on_finish: Option) -> Self { self.on_finish = on_finish; self } + #[must_use] pub fn window(mut self, window: window::Id) -> Self { self.window = Some(window); self @@ -164,7 +168,7 @@ impl iced_core::widget::tree::State { @@ -197,19 +201,15 @@ impl, viewport: &Rectangle, ) { - let ret = self.container.as_widget_mut().update( + self.container.as_widget_mut().update( &mut tree.children[0], - &event, + event, layout, cursor, renderer, @@ -241,12 +241,11 @@ impl { if let Some(position) = cursor.position() { if !state.hovered { - return ret; + return; } state.left_pressed_position = Some(position); shell.capture_event(); - return; } } mouse::Event::ButtonReleased(mouse::Button::Left) @@ -254,7 +253,6 @@ impl { if let Some(position) = cursor.position() { @@ -262,7 +260,7 @@ impl self.drag_threshold { @@ -281,16 +279,15 @@ impl return ret, + _ => (), }, Event::Dnd(DndEvent::Source(SourceEvent::Cancelled)) => { if state.is_dragging { @@ -301,7 +298,6 @@ impl { if state.is_dragging { @@ -312,11 +308,9 @@ impl return ret, + _ => (), } - ret } fn mouse_interaction( diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 695c8405..1465a9d7 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -5,7 +5,7 @@ use crate::cosmic_theme::{Density, Spacing}; use crate::{Element, theme, widget}; use apply::Apply; use derive_setters::Setters; -use iced::Length; +use iced::{Length, mouse}; use iced_core::{Vector, Widget, widget::tree}; use std::{borrow::Cow, cmp}; @@ -206,6 +206,7 @@ impl Widget ) { let child_state = &mut state.children[0]; let child_layout = layout.children().next().unwrap(); + self.header_bar_inner.as_widget_mut().update( child_state, event, @@ -215,7 +216,7 @@ impl Widget clipboard, shell, viewport, - ) + ); } fn mouse_interaction( @@ -435,6 +436,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { if let Some(message) = self.on_maximize.clone() { widget = widget.on_release(message); } + if let Some(message) = self.on_double_click.clone() { widget = widget.on_double_press(message); } diff --git a/src/widget/id_container.rs b/src/widget/id_container.rs index c8e49e04..716ee138 100644 --- a/src/widget/id_container.rs +++ b/src/widget/id_container.rs @@ -57,7 +57,7 @@ where } fn diff(&mut self, tree: &mut Tree) { - tree.children[0].diff(&mut self.content); + tree.diff_children(std::slice::from_mut(&mut self.content)); } fn size(&self) -> iced_core::Size { @@ -88,7 +88,7 @@ where operation.container(Some(&self.id), layout.bounds()); operation.traverse(&mut |operation| { self.content.as_widget_mut().operate( - tree, + &mut tree.children[0], layout .children() .next() @@ -124,7 +124,7 @@ where clipboard, shell, viewport, - ) + ); } fn mouse_interaction( diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs index 49df998a..136b49ea 100644 --- a/src/widget/list/column.rs +++ b/src/widget/list/column.rs @@ -112,6 +112,7 @@ impl<'a, Message: 'static> ListColumn<'a, Message> { crate::widget::column::with_children(self.children) .spacing(self.spacing) .padding(self.padding) + .width(iced::Length::Fill) .apply(container) .padding([self.spacing, 0]) .class(self.style) diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 05fcc133..9d4b09b0 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -580,7 +580,7 @@ where &mut self.menu_roots, view_cursor, tree, - &event, + event, layout, renderer, clipboard, @@ -609,6 +609,13 @@ where }); match event { + Mouse(mouse::Event::ButtonPressed(Left)) + | Touch(touch::Event::FingerPressed { .. }) + if view_cursor.is_over(layout.bounds()) => + { + // TODO should we track that it has been pressed? + shell.capture_event(); + } Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. } | FingerLost { .. }) => { let create_popup = my_state.inner.with_data_mut(|state| { let mut create_popup = false; @@ -627,6 +634,7 @@ where ))] { let surface_action = self.on_surface_action.as_ref().unwrap(); + shell.capture_event(); shell.publish(surface_action(crate::surface::action::destroy_popup( _id, @@ -640,6 +648,7 @@ where if !create_popup { return; } + shell.capture_event(); #[cfg(all( feature = "multi-window", feature = "wayland", @@ -653,6 +662,7 @@ where Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) if open && view_cursor.is_over(layout.bounds()) => { + shell.capture_event(); #[cfg(all( feature = "multi-window", feature = "wayland", diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index d52c929d..4f97d30e 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -1008,7 +1008,7 @@ impl Widget( menu_bounds, }; state.menu_states.push(ms); - // Hack to ensure menu opens properly shell.invalidate_layout(); diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 30b75a10..f63cdc37 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -127,6 +127,10 @@ pub use color_picker::{ColorPicker, ColorPickerModel}; #[doc(inline)] pub use iced::widget::qr_code; +mod cards; +#[doc(inline)] +pub use cards::cards; + pub mod context_drawer; #[doc(inline)] pub use context_drawer::{ContextDrawer, context_drawer}; diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 951b3757..7a82cd86 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -127,7 +127,7 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - let tree = content_tree_mut(tree); + let tree = &mut tree.children[0]; self.content.as_widget_mut().layout(tree, renderer, limits) } @@ -138,19 +138,9 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { - operation.container(Some(&self.id), layout.bounds()); - operation.traverse(&mut |operation| { - self.content.as_widget_mut().operate( - tree, - layout - .children() - .next() - .unwrap() - .with_virtual_offset(layout.virtual_offset()), - renderer, - operation, - ); - }); + self.content + .as_widget_mut() + .operate(content_tree_mut(tree), layout, renderer, operation); } fn update( @@ -183,7 +173,7 @@ where } self.content.as_widget_mut().update( - content_tree_mut(tree), + &mut tree.children[0], event, layout, cursor_position, @@ -265,7 +255,6 @@ where overlay_position.y = overlay_position.y.round(); translation.x += overlay_position.x; translation.y += overlay_position.y; - Some(overlay::Element::new(Box::new(Overlay { tree: &mut tree.children[1], content: popup, @@ -275,7 +264,7 @@ where }))) } else { self.content.as_widget_mut().overlay( - content_tree_mut(tree), + &mut tree.children[0], layout, renderer, viewport, diff --git a/src/widget/radio.rs b/src/widget/radio.rs index 831e9460..338c0a4e 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -165,7 +165,7 @@ where } fn diff(&mut self, tree: &mut Tree) { - tree.children[0].diff(&mut self.label); + tree.diff_children(std::slice::from_mut(&mut self.label)); } fn size(&self) -> Size { Size { diff --git a/src/widget/responsive_container.rs b/src/widget/responsive_container.rs index 0c7fbad3..3bb44276 100644 --- a/src/widget/responsive_container.rs +++ b/src/widget/responsive_container.rs @@ -81,7 +81,7 @@ where } fn diff(&mut self, tree: &mut Tree) { - tree.children[0].diff(&mut self.content); + tree.diff_children(std::slice::from_mut(&mut self.content)); } fn size(&self) -> iced_core::Size { @@ -131,7 +131,7 @@ where operation.container(Some(&self.id), layout.bounds()); operation.traverse(&mut |operation| { self.content.as_widget_mut().operate( - tree, + &mut tree.children[0], layout .children() .next() diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 162d1d21..f6de999e 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -2047,6 +2047,9 @@ where bounds.y = center_y; if self.model.text(key).is_some_and(|text| !text.is_empty()) { + // FIXME why has this behavior changed? Does the center alignment not work with infinite bounds now? + bounds.y -= state.paragraphs[key].min_height() / 2.; + // Draw the text for this segmented button or tab. renderer.fill_paragraph( state.paragraphs[key].raw(), @@ -2055,7 +2058,9 @@ where Rectangle { x: bounds.x, width: bounds.width, - ..original_bounds + height: original_bounds.height, + y: bounds.y, + // ..original_bounds, }, ); } diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 65179d99..fafc6d70 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -1,17 +1,418 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 +//! Show toggle controls using togglers. -use iced::{Length, widget}; -use iced_core::text; +use std::time::{Duration, Instant}; -pub fn toggler<'a, Message, Theme: iced_widget::toggler::Catalog, Renderer>( - is_checked: bool, -) -> widget::Toggler<'a, Message, Theme, Renderer> -where - Renderer: iced_core::Renderer + text::Renderer, -{ - widget::Toggler::new(is_checked) - .size(24) - .spacing(0) - .width(Length::Shrink) +use crate::{Element, anim, iced_core::Border, iced_widget::toggler::Status}; +use iced_core::{ + Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, event, + layout, mouse, + renderer::{self, Renderer}, + text, + widget::{self, Tree, tree}, + window, +}; +use iced_widget::Id; + +pub use crate::iced_widget::toggler::{Catalog, Style}; + +pub fn toggler<'a, Message>(is_checked: bool) -> Toggler<'a, Message> { + Toggler::new(is_checked) +} +/// A toggler widget. +#[allow(missing_debug_implementations)] +pub struct Toggler<'a, Message> { + id: Id, + is_toggled: bool, + on_toggle: Option Message + 'a>>, + label: Option, + width: Length, + size: f32, + text_size: Option, + text_line_height: text::LineHeight, + text_alignment: text::Alignment, + text_shaping: text::Shaping, + spacing: f32, + font: Option, + duration: Duration, +} + +impl<'a, Message> Toggler<'a, Message> { + /// The default size of a [`Toggler`]. + pub const DEFAULT_SIZE: f32 = 24.0; + + /// Creates a new [`Toggler`]. + /// + /// It expects: + /// * a boolean describing whether the [`Toggler`] is checked or not + /// * An optional label for the [`Toggler`] + /// * a function that will be called when the [`Toggler`] is toggled. It + /// will receive the new state of the [`Toggler`] and must produce a + /// `Message`. + pub fn new(is_toggled: bool) -> Self { + Toggler { + id: Id::unique(), + is_toggled, + on_toggle: None, + label: None, + width: Length::Fill, + size: Self::DEFAULT_SIZE, + text_size: None, + text_line_height: text::LineHeight::default(), + text_alignment: text::Alignment::Left, + text_shaping: text::Shaping::Advanced, + spacing: 0.0, + font: None, + duration: Duration::from_millis(200), + } + } + + /// Sets the size of the [`Toggler`]. + pub fn size(mut self, size: impl Into) -> Self { + self.size = size.into().0; + self + } + + /// Sets the width of the [`Toggler`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the text size o the [`Toggler`]. + pub fn text_size(mut self, text_size: impl Into) -> Self { + self.text_size = Some(text_size.into().0); + self + } + + /// Sets the text [`LineHeight`] of the [`Toggler`]. + pub fn text_line_height(mut self, line_height: impl Into) -> Self { + self.text_line_height = line_height.into(); + self + } + + /// Sets the horizontal alignment of the text of the [`Toggler`] + pub fn text_alignment(mut self, alignment: text::Alignment) -> Self { + self.text_alignment = alignment; + self + } + + /// Sets the [`text::Shaping`] strategy of the [`Toggler`]. + pub fn text_shaping(mut self, shaping: text::Shaping) -> Self { + self.text_shaping = shaping; + self + } + + /// Sets the spacing between the [`Toggler`] and the text. + pub fn spacing(mut self, spacing: impl Into) -> Self { + self.spacing = spacing.into().0; + self + } + + /// Sets the [`Font`] of the text of the [`Toggler`] + /// + /// [`Font`]: cosmic::iced::text::Renderer::Font + pub fn font(mut self, font: impl Into) -> Self { + self.font = Some(font.into()); + self + } + + pub fn id(mut self, id: Id) -> Self { + self.id = id; + self + } + + pub fn duration(mut self, dur: Duration) -> Self { + self.duration = dur; + self + } + + pub fn on_toggle(mut self, on_toggle: impl Fn(bool) -> Message + 'a) -> Self { + self.on_toggle = Some(Box::new(on_toggle)); + self + } + + /// Sets the label of the [`Button`]. + pub fn label(mut self, label: impl Into>) -> Self { + self.label = label.into(); + self + } +} + +impl<'a, Message> Widget for Toggler<'a, Message> { + fn size(&self) -> Size { + Size::new(self.width, Length::Shrink) + } + + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn id(&self) -> Option { + Some(self.id.clone()) + } + + fn set_id(&mut self, id: Id) { + self.id = id; + } + + fn layout( + &mut self, + tree: &mut Tree, + renderer: &crate::Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.width(self.width); + + let res = next_to_each_other( + &limits, + self.spacing, + |limits| { + if let Some(label) = self.label.as_deref() { + let state = tree.state.downcast_mut::(); + let node = iced_core::widget::text::layout( + &mut state.text, + renderer, + limits, + label, + widget::text::Format { + width: self.width, + height: Length::Shrink, + line_height: self.text_line_height, + size: self.text_size.map(iced::Pixels), + font: self.font, + align_x: self.text_alignment, + align_y: alignment::Vertical::Top, + shaping: self.text_shaping, + wrapping: crate::iced_core::text::Wrapping::default(), + }, + ); + match self.width { + Length::Fill => { + let size = node.size(); + layout::Node::with_children( + Size::new(limits.width(Length::Fill).max().width, size.height), + vec![node], + ) + } + _ => node, + } + } else { + layout::Node::new(iced_core::Size::ZERO) + } + }, + |_| layout::Node::new(Size::new(48., 24.)), + ); + res + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + _renderer: &crate::Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) { + let Some(on_toggle) = self.on_toggle.as_ref() else { + return; + }; + let state = tree.state.downcast_mut::(); + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + let mouse_over = cursor_position.is_over(layout.bounds()); + + if mouse_over { + shell.publish((on_toggle)(!self.is_toggled)); + state.anim.changed(self.duration); + shell.capture_event(); + } + } + Event::Window(window::Event::RedrawRequested(now)) => { + state.anim.anim_done(self.duration); + if state.anim.last_change.is_some() { + shell.request_redraw(); + } + } + _ => {} + } + } + + fn mouse_interaction( + &self, + _state: &Tree, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &crate::Renderer, + ) -> mouse::Interaction { + if cursor_position.is_over(layout.bounds()) { + mouse::Interaction::Pointer + } else { + mouse::Interaction::default() + } + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor_position: mouse::Cursor, + viewport: &Rectangle, + ) { + let state = tree.state.downcast_ref::(); + + let mut children = layout.children(); + let label_layout = children.next().unwrap(); + + if let Some(_label) = &self.label { + let state: &State = tree.state.downcast_ref(); + iced_widget::text::draw( + renderer, + style, + label_layout.bounds(), + state.text.raw(), + iced_widget::text::Style::default(), + viewport, + ); + } + + let toggler_layout = children.next().unwrap(); + let bounds = toggler_layout.bounds(); + + let is_mouse_over = cursor_position.is_over(bounds); + + // let style = blend_appearances( + // theme.style( + // &(), + // if is_mouse_over { + // Status::Hovered { is_toggled: false } + // } else { + // Status::Active { is_toggled: false } + // }, + // ), + // theme.style( + // &(), + // if is_mouse_over { + // Status::Hovered { is_toggled: true } + // } else { + // Status::Active { is_toggled: true } + // }, + // ), + // percent, + // ); + + let style = theme.style( + &(), + if is_mouse_over { + Status::Hovered { + is_toggled: self.is_toggled, + } + } else { + Status::Active { + is_toggled: self.is_toggled, + } + }, + ); + + let space = style.handle_margin; + + let toggler_background_bounds = Rectangle { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }; + + renderer.fill_quad( + renderer::Quad { + bounds: toggler_background_bounds, + border: Border { + radius: style.border_radius, + ..Default::default() + }, + ..renderer::Quad::default() + }, + style.background, + ); + let mut t = state.anim.t(self.duration, self.is_toggled); + + let toggler_foreground_bounds = Rectangle { + x: bounds.x + + anim::slerp( + space, + bounds.width - space - (bounds.height - (2.0 * space)), + t, + ), + + y: bounds.y + space, + width: bounds.height - (2.0 * space), + height: bounds.height - (2.0 * space), + }; + + renderer.fill_quad( + renderer::Quad { + bounds: toggler_foreground_bounds, + border: Border { + radius: style.handle_radius, + ..Default::default() + }, + ..renderer::Quad::default() + }, + style.foreground, + ); + } +} + +impl<'a, Message: 'static> From> for Element<'a, Message> { + fn from(toggler: Toggler<'a, Message>) -> Element<'a, Message> { + Element::new(toggler) + } +} + +/// Produces a [`Node`] with two children nodes one right next to each other. +pub fn next_to_each_other( + limits: &iced::Limits, + spacing: f32, + left: impl FnOnce(&iced::Limits) -> iced_core::layout::Node, + right: impl FnOnce(&iced::Limits) -> iced_core::layout::Node, +) -> iced_core::layout::Node { + let mut right_node = right(limits); + let right_size = right_node.size(); + + let left_limits = limits.shrink(Size::new(right_size.width + spacing, 0.0)); + let mut left_node = left(&left_limits); + let left_size = left_node.size(); + + let (left_y, right_y) = if left_size.height > right_size.height { + (0.0, (left_size.height - right_size.height) / 2.0) + } else { + ((right_size.height - left_size.height) / 2.0, 0.0) + }; + + left_node = left_node.move_to(iced::Point::new(0.0, left_y)); + right_node = right_node.move_to(iced::Point::new(left_size.width + spacing, right_y)); + + iced_core::layout::Node::with_children( + Size::new( + left_size.width + spacing + right_size.width, + left_size.height.max(right_size.height), + ), + vec![left_node, right_node], + ) +} + +#[derive(Debug, Default)] +pub struct State { + text: widget::text::State<::Paragraph>, + anim: anim::State, } diff --git a/src/widget/wayland/tooltip/widget.rs b/src/widget/wayland/tooltip/widget.rs index ceb234a9..b16720cd 100644 --- a/src/widget/wayland/tooltip/widget.rs +++ b/src/widget/wayland/tooltip/widget.rs @@ -240,7 +240,7 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> operation.container(Some(&self.id), layout.bounds()); operation.traverse(&mut |operation| { self.content.as_widget_mut().operate( - tree, + &mut tree.children[0], layout .children() .next() From e6fe1a68115fe8d62766cc6665c52f7bb4e4c340 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 19 Feb 2026 22:54:08 -0500 Subject: [PATCH 003/116] fix: ellipsize --- src/widget/toggler.rs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index fafc6d70..312b50d5 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -34,6 +34,7 @@ pub struct Toggler<'a, Message> { spacing: f32, font: Option, duration: Duration, + ellipsize: text::Ellipsize, } impl<'a, Message> Toggler<'a, Message> { @@ -63,6 +64,7 @@ impl<'a, Message> Toggler<'a, Message> { spacing: 0.0, font: None, duration: Duration::from_millis(200), + ellipsize: text::Ellipsize::None, } } @@ -108,6 +110,12 @@ impl<'a, Message> Toggler<'a, Message> { self } + /// Sets the [`text::Ellipsize`] strategy of the [`Toggler`]. + pub fn ellipsize(mut self, ellipsize: text::Ellipsize) -> Self { + self.ellipsize = ellipsize; + self + } + /// Sets the [`Font`] of the text of the [`Toggler`] /// /// [`Font`]: cosmic::iced::text::Renderer::Font @@ -188,6 +196,7 @@ impl<'a, Message> Widget for Toggler<'a, align_y: alignment::Vertical::Top, shaping: self.text_shaping, wrapping: crate::iced_core::text::Wrapping::default(), + ellipsize: self.ellipsize, }, ); match self.width { From 0d37dc69e3fae08acc14a91f6e491d7a6c5feaf6 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Sun, 22 Feb 2026 20:47:15 -0500 Subject: [PATCH 004/116] fix: applet popup width --- src/applet/mod.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/applet/mod.rs b/src/applet/mod.rs index f7fa5b62..0cbcacab 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -392,7 +392,6 @@ impl Context { } }), ) - .width(Length::Fill) .height(Length::Shrink) .align_x(horizontal_align) .align_y(vertical_align), From 71e2c7c99eafd20cd2f11a0b8a3c4fb7d2c9eda5 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Sun, 22 Feb 2026 20:48:11 -0500 Subject: [PATCH 005/116] fix: responsive menu layout --- examples/application/Cargo.toml | 5 ++-- src/applet/mod.rs | 2 ++ src/widget/responsive_container.rs | 44 +++++++++++++++++++++------- src/widget/toggler.rs | 5 ++++ src/widget/wayland/tooltip/widget.rs | 2 +- 5 files changed, 45 insertions(+), 13 deletions(-) diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index b1ac1242..f4b62cdb 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -11,8 +11,9 @@ wayland = ["libcosmic/wayland"] env_logger = "0.11" [dependencies.libcosmic] -git = "https://github.com/pop-os/libcosmic" -branch = "iced-rebase" +# git = "https://github.com/pop-os/libcosmic" +# branch = "iced-rebase" +path = "../.." features = [ "debug", "winit", diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 0cbcacab..e18c9aad 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -234,6 +234,7 @@ impl Context { }) .width(Length::Fixed(suggested.0 as f32)) .height(Length::Fixed(suggested.1 as f32)); + dbg!(suggested); self.button_from_element(icon, symbolic) } @@ -250,6 +251,7 @@ impl Context { (applet_padding_minor_axis, applet_padding_major_axis) }; + dbg!(suggested.0 + 2 * horizontal_padding); crate::widget::button::custom(layer_container(content).center(Length::Fill)) .width(Length::Fixed((suggested.0 + 2 * horizontal_padding) as f32)) .height(Length::Fixed((suggested.1 + 2 * vertical_padding) as f32)) diff --git a/src/widget/responsive_container.rs b/src/widget/responsive_container.rs index 3bb44276..b9b6a289 100644 --- a/src/widget/responsive_container.rs +++ b/src/widget/responsive_container.rs @@ -95,7 +95,7 @@ where limits: &layout::Limits, ) -> layout::Node { let state = tree.state.downcast_mut::(); - let unrestricted_size = self.size.unwrap_or_else(|| { + let mut unrestricted_size = self.size.unwrap_or_else(|| { let node = self.content .as_widget_mut() @@ -103,21 +103,45 @@ where node.size() }); - let max_size = limits.max(); - let old_max = state.limits.max(); - state.needs_update = (unrestricted_size.width > max_size.width) - ^ (state.size.width > old_max.width) - || (unrestricted_size.height > max_size.height) ^ (state.size.height > old_max.height); - if state.needs_update { - state.limits = *limits; - state.size = unrestricted_size; - } + let cur_unrestricted_size = { + let node = + self.content + .as_widget_mut() + .layout(&mut tree.children[0], renderer, &Limits::NONE); + node.size() + }; + let max_size = limits.max(); + + let old_max = state.limits.max(); + + state.needs_update = (cur_unrestricted_size.width > max_size.width) + || (cur_unrestricted_size.width > old_max.width) + || (cur_unrestricted_size.height > max_size.height) + || (cur_unrestricted_size.height > old_max.height) + || ((unrestricted_size.width <= max_size.width) + && (unrestricted_size.height <= max_size.height) + && (unrestricted_size.width - cur_unrestricted_size.width > 1. + || unrestricted_size.height - cur_unrestricted_size.height > 1.)); + + if unrestricted_size.width < cur_unrestricted_size.width { + state.needs_update = true; + unrestricted_size.width = cur_unrestricted_size.width; + } else if unrestricted_size.height < cur_unrestricted_size.height { + state.needs_update = true; + unrestricted_size.height = cur_unrestricted_size.height; + } let node = self .content .as_widget_mut() .layout(&mut tree.children[0], renderer, limits); let size = node.size(); + + if state.needs_update { + state.limits = *limits; + state.size = unrestricted_size; + } + layout::Node::with_children(size, vec![node]) } diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 312b50d5..2cd6a785 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -139,6 +139,11 @@ impl<'a, Message> Toggler<'a, Message> { self } + pub fn on_toggle_maybe(mut self, on_toggle: Option Message + 'a>) -> Self { + self.on_toggle = on_toggle.map(|t| Box::new(t) as _); + self + } + /// Sets the label of the [`Button`]. pub fn label(mut self, label: impl Into>) -> Self { self.label = label.into(); diff --git a/src/widget/wayland/tooltip/widget.rs b/src/widget/wayland/tooltip/widget.rs index b16720cd..7bf0991a 100644 --- a/src/widget/wayland/tooltip/widget.rs +++ b/src/widget/wayland/tooltip/widget.rs @@ -263,7 +263,7 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - let status = update( + update( self.id.clone(), event.clone(), layout, From 7554540b78e093ad1f12f293535c903f3889c350 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Sun, 22 Feb 2026 21:57:15 -0500 Subject: [PATCH 006/116] fix: update for applet widgets and grid --- src/applet/column.rs | 61 ++++++++++++++++++++------------------- src/applet/row.rs | 61 ++++++++++++++++++++------------------- src/widget/grid/widget.rs | 27 ++++++++--------- 3 files changed, 76 insertions(+), 73 deletions(-) diff --git a/src/applet/column.rs b/src/applet/column.rs index 8b3c68e9..9657b566 100644 --- a/src/applet/column.rs +++ b/src/applet/column.rs @@ -320,38 +320,25 @@ where } } - self.children + for (((i, child), state), c_layout) in self + .children .iter_mut() .enumerate() .zip(&mut tree.children) .zip(layout.children()) - .map(|(((i, child), state), c_layout)| { - let mut cursor_virtual = cursor; - if matches!( - event, - Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) - | Event::Touch( - iced_core::touch::Event::FingerMoved { .. } - | iced_core::touch::Event::FingerPressed { .. } - ) - ) && cursor.is_over(c_layout.bounds()) - { - my_state.hovered = Some(i); - return child.as_widget_mut().update( - state, - &event, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor_virtual, - renderer, - clipboard, - shell, - viewport, - ); - } else if my_state.hovered.is_some_and(|h| i != h) { - cursor_virtual = mouse::Cursor::Unavailable; - } - - child.as_widget_mut().update( + { + let mut cursor_virtual = cursor; + if matches!( + event, + Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) + | Event::Touch( + iced_core::touch::Event::FingerMoved { .. } + | iced_core::touch::Event::FingerPressed { .. } + ) + ) && cursor.is_over(c_layout.bounds()) + { + my_state.hovered = Some(i); + return child.as_widget_mut().update( state, &event, c_layout.with_virtual_offset(layout.virtual_offset()), @@ -360,8 +347,22 @@ where clipboard, shell, viewport, - ) - }); + ); + } else if my_state.hovered.is_some_and(|h| i != h) { + cursor_virtual = mouse::Cursor::Unavailable; + } + + child.as_widget_mut().update( + state, + &event, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor_virtual, + renderer, + clipboard, + shell, + viewport, + ); + } } fn mouse_interaction( diff --git a/src/applet/row.rs b/src/applet/row.rs index 2a770503..a6745d1c 100644 --- a/src/applet/row.rs +++ b/src/applet/row.rs @@ -309,39 +309,26 @@ where } } - self.children + for (((i, child), state), c_layout) in self + .children .iter_mut() .enumerate() .zip(&mut tree.children) .zip(layout.children()) - .map(|(((i, child), state), c_layout)| { - let mut cursor_virtual = cursor; + { + let mut cursor_virtual = cursor; - if matches!( - event, - Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) - | Event::Touch( - iced_core::touch::Event::FingerMoved { .. } - | iced_core::touch::Event::FingerPressed { .. } - ) - ) && cursor.is_over(c_layout.bounds()) - { - my_state.hovered = Some(i); - return child.as_widget_mut().update( - state, - &event, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor_virtual, - renderer, - clipboard, - shell, - viewport, - ); - } else if my_state.hovered.is_some_and(|h| i != h) { - cursor_virtual = mouse::Cursor::Unavailable; - } - - child.as_widget_mut().update( + if matches!( + event, + Event::Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) + | Event::Touch( + iced_core::touch::Event::FingerMoved { .. } + | iced_core::touch::Event::FingerPressed { .. } + ) + ) && cursor.is_over(c_layout.bounds()) + { + my_state.hovered = Some(i); + return child.as_widget_mut().update( state, &event, c_layout.with_virtual_offset(layout.virtual_offset()), @@ -350,8 +337,22 @@ where clipboard, shell, viewport, - ) - }); + ); + } else if my_state.hovered.is_some_and(|h| i != h) { + cursor_virtual = mouse::Cursor::Unavailable; + } + + child.as_widget_mut().update( + state, + &event, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor_virtual, + renderer, + clipboard, + shell, + viewport, + ); + } } fn mouse_interaction( diff --git a/src/widget/grid/widget.rs b/src/widget/grid/widget.rs index f88dfc2a..e59ba90d 100644 --- a/src/widget/grid/widget.rs +++ b/src/widget/grid/widget.rs @@ -189,22 +189,23 @@ impl Widget for Grid< shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - self.children + for ((child, state), c_layout) in self + .children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - .map(|((child, state), c_layout)| { - child.as_widget_mut().update( - state, - event, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ) - }); + { + child.as_widget_mut().update( + state, + event, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } } fn mouse_interaction( From 89ee66f25113dac0f8f06e734c6453323b508297 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Sun, 22 Feb 2026 22:47:28 -0500 Subject: [PATCH 007/116] fix: menu bar and flex row event handling --- src/widget/flex_row/widget.rs | 27 ++++++++++++++------------- src/widget/menu/menu_bar.rs | 28 ++++++++++++++-------------- 2 files changed, 28 insertions(+), 27 deletions(-) diff --git a/src/widget/flex_row/widget.rs b/src/widget/flex_row/widget.rs index f7b90f66..b891c170 100644 --- a/src/widget/flex_row/widget.rs +++ b/src/widget/flex_row/widget.rs @@ -160,22 +160,23 @@ impl Widget for FlexR shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - self.children + for ((child, state), c_layout) in self + .children .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - .map(|((child, state), c_layout)| { - child.as_widget_mut().update( - state, - event, - c_layout.with_virtual_offset(layout.virtual_offset()), - cursor, - renderer, - clipboard, - shell, - viewport, - ) - }); + { + child.as_widget_mut().update( + state, + event, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } } fn mouse_interaction( diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 9d4b09b0..7007befb 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -810,21 +810,21 @@ fn process_root_events( shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - menu_roots + for ((root, t), lo) in menu_roots .iter_mut() .zip(&mut tree.children) .zip(layout.children()) - .map(|((root, t), lo)| { - // assert!(t.tag == tree::Tag::stateless()); - root.item.update( - &mut t.children[root.index], - event, - lo, - view_cursor, - renderer, - clipboard, - shell, - viewport, - ) - }); + { + // assert!(t.tag == tree::Tag::stateless()); + root.item.update( + &mut t.children[root.index], + event, + lo, + view_cursor, + renderer, + clipboard, + shell, + viewport, + ); + } } From fb1a7d36407d863c80ee4be1e718969451697f58 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Sun, 22 Feb 2026 22:48:07 -0500 Subject: [PATCH 008/116] fix: open-dialog example --- examples/open-dialog/src/main.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/examples/open-dialog/src/main.rs b/examples/open-dialog/src/main.rs index 10e46315..29061534 100644 --- a/examples/open-dialog/src/main.rs +++ b/examples/open-dialog/src/main.rs @@ -207,7 +207,7 @@ impl cosmic::Application for App { ); content.push( - iced::widget::vertical_space() + iced::widget::space::vertical() .height(Length::Fixed(12.0)) .into(), ); From 442ce6ad0c3c74d878d4a2d3ff467daa63275f11 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Sun, 22 Feb 2026 23:32:38 -0500 Subject: [PATCH 009/116] fix: context-menu when a popup is created and a focus event is received, we shouldn't close the popups, because it may be a focus event for a popup --- src/widget/context_menu.rs | 63 ++++++++++++++++++-------------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 143a78b8..25953639 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -13,7 +13,7 @@ use derive_setters::Setters; use iced::touch::Finger; use iced::{Event, Vector, keyboard, window}; use iced_core::widget::{Tree, Widget, tree}; -use iced_core::{Length, Point, Size, event, mouse, touch}; +use iced_core::{Length, Point, Size, mouse, touch}; use std::collections::HashSet; use std::sync::Arc; @@ -85,6 +85,7 @@ impl ContextMenu<'_, Message> { // close existing popups state.menu_states.clear(); state.active_root.clear(); + dbg!("closing existing popups"); shell.publish(self.on_surface_action.as_ref().unwrap()(destroy_popup(id))); state.view_cursor = view_cursor; ( @@ -336,13 +337,12 @@ impl Widget .with_data(|d| !d.open && !d.active_root.is_empty()); let open = state.menu_bar_state.inner.with_data_mut(|state| { - if reset { - if let Some(popup_id) = state.popup_id.get(&self.window_id).copied() { - if let Some(handler) = self.on_surface_action.as_ref() { - shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id))); - state.reset(); - } - } + if reset + && let Some(popup_id) = state.popup_id.get(&self.window_id).copied() + && let Some(handler) = self.on_surface_action.as_ref() + { + shell.publish((handler)(crate::surface::Action::DestroyPopup(popup_id))); + state.reset(); } state.open }); @@ -356,7 +356,6 @@ impl Widget mouse::Button::Right | mouse::Button::Left, )) | Event::Touch(touch::Event::FingerPressed { .. }) - | Event::Window(window::Event::Focused) if open ) { state.menu_bar_state.inner.with_data_mut(|state| { @@ -366,15 +365,14 @@ impl Widget state.open = false; #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { - if let Some(id) = state.popup_id.remove(&self.window_id) { - { - let surface_action = self.on_surface_action.as_ref().unwrap(); - shell - .publish(surface_action(crate::surface::action::destroy_popup(id))); - } - state.view_cursor = cursor; + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) + && let Some(id) = state.popup_id.remove(&self.window_id) + { + { + let surface_action = self.on_surface_action.as_ref().unwrap(); + shell.publish(surface_action(crate::surface::action::destroy_popup(id))); } + state.view_cursor = cursor; } }); } @@ -388,7 +386,7 @@ impl Widget } Event::Touch(touch::Event::FingerLifted { id, .. }) => { - state.fingers_pressed.remove(&id); + state.fingers_pressed.remove(id); } _ => (), @@ -397,7 +395,7 @@ impl Widget // Present a context menu on a right click event. if !was_open && self.context_menu.is_some() - && (right_button_released(&event) || (touch_lifted(&event) && fingers_pressed == 2)) + && (right_button_released(event) || (touch_lifted(event) && fingers_pressed == 2)) { state.context_cursor = cursor.position().unwrap_or_default(); let state = tree.state.downcast_mut::(); @@ -412,9 +410,9 @@ impl Widget shell.capture_event(); return; - } else if !was_open && right_button_released(&event) - || (touch_lifted(&event)) - || left_button_released(&event) + } else if !was_open && right_button_released(event) + || (touch_lifted(event)) + || left_button_released(event) { state.menu_bar_state.inner.with_data_mut(|state| { was_open = true; @@ -427,16 +425,15 @@ impl Widget feature = "winit", feature = "surface-message" ))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { - if let Some(id) = state.popup_id.remove(&self.window_id) { - { - let surface_action = self.on_surface_action.as_ref().unwrap(); - shell.publish(surface_action( - crate::surface::action::destroy_popup(id), - )); - } - state.view_cursor = cursor; + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) + && let Some(id) = state.popup_id.remove(&self.window_id) + { + { + let surface_action = self.on_surface_action.as_ref().unwrap(); + shell + .publish(surface_action(crate::surface::action::destroy_popup(id))); } + state.view_cursor = cursor; } }); } @@ -450,7 +447,7 @@ impl Widget clipboard, shell, viewport, - ) + ); } fn overlay<'b>( @@ -458,7 +455,7 @@ impl Widget tree: &'b mut Tree, layout: iced_core::Layout<'_>, _renderer: &crate::Renderer, - viewport: &iced::Rectangle, + _viewport: &iced::Rectangle, translation: Vector, ) -> Option> { #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] From bee2d591db0428d7fe516ed9caddcee728a27b31 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 23 Feb 2026 14:50:52 -0500 Subject: [PATCH 010/116] chore: update iced --- examples/application/Cargo.toml | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index f4b62cdb..b1ac1242 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -11,9 +11,8 @@ wayland = ["libcosmic/wayland"] env_logger = "0.11" [dependencies.libcosmic] -# git = "https://github.com/pop-os/libcosmic" -# branch = "iced-rebase" -path = "../.." +git = "https://github.com/pop-os/libcosmic" +branch = "iced-rebase" features = [ "debug", "winit", From 904133397b1de0db13b20ad6454d34a7e82fb61f Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 25 Feb 2026 11:10:34 -0500 Subject: [PATCH 011/116] fix: toggler width fixes & cleanup --- examples/applet/src/window.rs | 3 +-- src/applet/mod.rs | 2 -- src/widget/context_menu.rs | 2 +- src/widget/mod.rs | 2 +- src/widget/settings/item.rs | 10 ++++++++-- src/widget/toggler.rs | 2 +- 6 files changed, 12 insertions(+), 9 deletions(-) diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 547863f2..4e05c70a 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -128,8 +128,7 @@ impl cosmic::Application for Window { "Example row", cosmic::widget::container( toggler(state.example_row) - .on_toggle(Message::ToggleExampleRow) - .width(Length::Fill), + .on_toggle(Message::ToggleExampleRow), ), )) .add(popup_dropdown( diff --git a/src/applet/mod.rs b/src/applet/mod.rs index e18c9aad..0cbcacab 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -234,7 +234,6 @@ impl Context { }) .width(Length::Fixed(suggested.0 as f32)) .height(Length::Fixed(suggested.1 as f32)); - dbg!(suggested); self.button_from_element(icon, symbolic) } @@ -251,7 +250,6 @@ impl Context { (applet_padding_minor_axis, applet_padding_major_axis) }; - dbg!(suggested.0 + 2 * horizontal_padding); crate::widget::button::custom(layer_container(content).center(Length::Fill)) .width(Length::Fixed((suggested.0 + 2 * horizontal_padding) as f32)) .height(Length::Fixed((suggested.1 + 2 * vertical_padding) as f32)) diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 25953639..200021c3 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -85,7 +85,7 @@ impl ContextMenu<'_, Message> { // close existing popups state.menu_states.clear(); state.active_root.clear(); - dbg!("closing existing popups"); + shell.publish(self.on_surface_action.as_ref().unwrap()(destroy_popup(id))); state.view_cursor = view_cursor; ( diff --git a/src/widget/mod.rs b/src/widget/mod.rs index f63cdc37..eae255bc 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -350,7 +350,7 @@ pub use toaster::{Toast, ToastId, Toasts, toaster}; mod toggler; #[doc(inline)] -pub use toggler::toggler; +pub use toggler::{Toggler, toggler}; #[doc(inline)] pub use tooltip::{Tooltip, tooltip}; diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index a17f2071..110ab7b7 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -41,6 +41,7 @@ pub fn item_row(children: Vec>) -> Row { row::with_children(children) .spacing(theme::spacing().space_xs) .align_y(iced::Alignment::Center) + .width(Length::Fill) } /// A settings item aligned in a flex row @@ -59,8 +60,9 @@ pub fn flex_item<'a, Message: 'static>( .wrapping(Wrapping::Word) .width(Length::Fill) .into(), - container(widget).into(), + container(widget).width(Length::Shrink).into(), ]) + .width(Length::Fill) } inner(title.into(), widget.into()) @@ -141,6 +143,10 @@ impl<'a, Message: 'static> Item<'a, Message> { is_checked: bool, message: impl Fn(bool) -> Message + 'static, ) -> Row<'a, Message> { - self.control(crate::widget::toggler(is_checked).on_toggle(message)) + self.control( + crate::widget::toggler(is_checked) + .width(Length::Shrink) + .on_toggle(message), + ) } } diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 2cd6a785..12bb8950 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -55,7 +55,7 @@ impl<'a, Message> Toggler<'a, Message> { is_toggled, on_toggle: None, label: None, - width: Length::Fill, + width: Length::Shrink, size: Self::DEFAULT_SIZE, text_size: None, text_line_height: text::LineHeight::default(), From 0298487096e03abc6bc31eec3e8d1339530f2714 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 25 Feb 2026 13:26:38 -0500 Subject: [PATCH 012/116] fix: overlay event handling and mouse interaction --- src/app/mod.rs | 1 - src/widget/context_drawer/overlay.rs | 30 +++++++++++++++++++++++----- src/widget/context_drawer/widget.rs | 2 +- 3 files changed, 26 insertions(+), 7 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index abda71c1..e11ed7ae 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -12,7 +12,6 @@ use cosmic_config::CosmicConfigEntry; pub mod context_drawer; pub use context_drawer::{ContextDrawer, context_drawer}; use iced::application::BootFn; -use iced_core::Widget; pub mod cosmic; pub mod settings; diff --git a/src/widget/context_drawer/overlay.rs b/src/widget/context_drawer/overlay.rs index eef9183b..39b34217 100644 --- a/src/widget/context_drawer/overlay.rs +++ b/src/widget/context_drawer/overlay.rs @@ -8,7 +8,7 @@ use iced::advanced::widget::{self, Operation}; use iced::advanced::{Clipboard, Shell}; use iced::advanced::{overlay, renderer}; use iced::{Event, Point, Size, mouse}; -use iced_core::Renderer; +use iced_core::{Renderer, touch}; pub(super) struct Overlay<'a, 'b, Message> { pub(crate) position: Point, @@ -65,7 +65,20 @@ where clipboard, shell, &layout.bounds(), - ) + ); + match event { + Event::Mouse(e) if !matches!(e, mouse::Event::CursorLeft) => { + if cursor.is_over(layout.bounds()) { + shell.capture_event(); + } + } + Event::Touch(e) if !matches!(e, touch::Event::FingerLost { .. }) => { + if cursor.is_over(layout.bounds()) { + shell.capture_event(); + } + } + _ => {} + } } fn draw( @@ -86,7 +99,7 @@ where cursor, &layout.bounds(), ); - }) + }); } fn operate( @@ -108,9 +121,16 @@ where ) -> mouse::Interaction { // TODO how to handle viewport here? let viewport = &layout.bounds(); - self.content + let interaction = self + .content .as_widget() - .mouse_interaction(self.tree, layout, cursor, viewport, renderer) + .mouse_interaction(self.tree, layout, cursor, viewport, renderer); + if let mouse::Interaction::None = interaction + && cursor.is_over(layout.bounds()) + { + return mouse::Interaction::Idle; + } + interaction } fn overlay<'c>( diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index e7ca5dab..7420738c 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -238,7 +238,7 @@ impl Widget for ContextDrawer<' clipboard, shell, viewport, - ) + ); } fn mouse_interaction( From 3d8596287c34f6151ab591acc23d30b971f7d805 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 25 Feb 2026 15:26:08 -0500 Subject: [PATCH 013/116] fix: missed event status after rebase --- src/widget/dnd_destination.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index b0a23fad..a77101b9 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -531,7 +531,8 @@ impl Widget && let Ok(s) = String::from_utf8(data[..data.len() - 1].to_vec()) { shell.publish(f(s)); - return event::Status::Captured; + shell.capture_event(); + return; } if let (Some(msg), ret) = state.on_data_received( From 89d31e988da8cf4aa1737767d7e41e6476234277 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 25 Feb 2026 17:47:58 -0500 Subject: [PATCH 014/116] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 73369a18..59fbf68c 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 73369a18eb4069f3f3d1916fd1e17537ee87a587 +Subproject commit 59fbf68c541758197204aa52ceca9f89d63d1611 From 0e1a9d46eb09ed2c752ad6c6467f2d3437cd25ca Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 27 Feb 2026 17:24:18 -0500 Subject: [PATCH 015/116] chore: update iced & cleanup text input --- iced | 2 +- src/widget/text_input/input.rs | 26 +++++++++++++++----------- 2 files changed, 16 insertions(+), 12 deletions(-) diff --git a/iced b/iced index 59fbf68c..f7dc1803 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 59fbf68c541758197204aa52ceca9f89d63d1611 +Subproject commit f7dc18037113719633f450e549d9a6428b5c84b9 diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 5b6a53f3..3960cee1 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -651,11 +651,11 @@ where // if the previous state was at the end of the text, keep it there let old_value = Value::new(&old_value); - if state.is_focused() { - if let cursor::State::Index(index) = state.cursor.state(&old_value) { - if index == old_value.len() { - state.cursor.move_to(self.value.len()); - } + if state.is_focused() + && let cursor::State::Index(index) = state.cursor.state(&old_value) + { + if index == old_value.len() { + state.cursor.move_to(self.value.len()); } } @@ -935,7 +935,8 @@ where layout, self.manage_value, self.drag_threshold, - ) + self.always_active, + ); } #[inline] @@ -1358,6 +1359,7 @@ pub fn update<'a, Message: Clone + 'static>( layout: Layout<'_>, manage_value: bool, drag_threshold: f32, + always_active: bool, ) { let update_cache = |state, value| { replace_paragraph( @@ -1962,7 +1964,11 @@ pub fn update<'a, Message: Clone + 'static>( let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS - (*now - focus.updated_at).as_millis() % CURSOR_BLINK_INTERVAL_MILLIS; - + shell.request_redraw_at(window::RedrawRequest::At( + now.checked_add(Duration::from_millis(millis_until_redraw as u64)) + .unwrap_or(*now), + )); + } else if always_active { shell.request_redraw(); } } @@ -2340,11 +2346,9 @@ pub fn draw<'a, Message>( cursor::State::Index(position) => { let (text_value_width, offset) = measure_cursor_and_scroll_offset(state.value.raw(), text_bounds, position); - let is_cursor_visible = handling_dnd_offer || ((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS) - % 2 - == 0; + .is_multiple_of(2); if is_cursor_visible { if dnd_icon { (None, 0.0) @@ -2479,7 +2483,7 @@ pub fn draw<'a, Message>( }, bounds.position(), color, - *viewport, + text_bounds, ); }; From 925cc9a39f36e09e71e29da37cfeea841cb258b4 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 2 Mar 2026 10:35:53 -0500 Subject: [PATCH 016/116] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index f7dc1803..0df654c1 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit f7dc18037113719633f450e549d9a6428b5c84b9 +Subproject commit 0df654c14aa811e01362275a22201a9b9eff9ae3 From 5432fee1120155248c8689f33a462f2751519060 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 2 Mar 2026 10:56:14 -0500 Subject: [PATCH 017/116] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 0df654c1..b479f3e8 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 0df654c14aa811e01362275a22201a9b9eff9ae3 +Subproject commit b479f3e87fd54b9e80a95cf1f4d7767f9dcfbccf From 0bfda2e28cc406411368600e112c2a813125ef2c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 2 Mar 2026 13:36:07 -0500 Subject: [PATCH 018/116] chore: update deps and test fixes --- .github/workflows/ci.yml | 2 +- Cargo.toml | 4 +-- iced | 2 +- src/desktop.rs | 53 ++++++++++++++++++++++++---------------- 4 files changed, 36 insertions(+), 25 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 50a62a50..a822642e 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -66,7 +66,7 @@ jobs: - name: Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Test features - run: cargo test --no-default-features --features "${{ matrix.features }}" + run: cargo test --no-default-features --features "${{ matrix.features }}" -- --test-threads=1 env: RUST_BACKTRACE: full diff --git a/Cargo.toml b/Cargo.toml index 01b50733..ecb84bb5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,7 +11,7 @@ name = "cosmic" default = [ "winit", "tokio", - "a11y", + "a11y", "dbus-config", "x11", "wayland", @@ -119,7 +119,7 @@ ashpd = { version = "0.12.1", default-features = false, optional = true } async-fs = { version = "2.2", optional = true } async-std = { version = "1.13", optional = true } auto_enums = "0.8.7" -cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "d0e95be", optional = true } +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true } jiff = "0.2" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } diff --git a/iced b/iced index b479f3e8..4516691f 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit b479f3e87fd54b9e80a95cf1f4d7767f9dcfbccf +Subproject commit 4516691f3582a2a8c31f886b8e6090a235f6e72c diff --git a/src/desktop.rs b/src/desktop.rs index 0d3dbb52..fe32f286 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -416,7 +416,6 @@ fn match_exec_basename( }; let basename_lower = basename.to_ascii_lowercase(); - if normalized .iter() .any(|candidate| candidate == &basename_lower) @@ -440,8 +439,7 @@ fn fallback_entry(context: &DesktopLookupContext<'_>) -> fde::DesktopEntry { let name = context .title .as_ref() - .map(|title| title.to_string()) - .unwrap_or_else(|| context.app_id.to_string()); + .map_or_else(|| context.app_id.to_string(), |title| title.to_string()); entry.add_desktop_entry("Name".to_string(), name); entry } @@ -458,7 +456,9 @@ fn proton_or_wine_fallback( ) -> Option { let app_id = context.app_id.as_ref(); let is_proton_game = app_id == "steam_app_default"; - let is_wine_entry = app_id.ends_with(".exe"); + let is_wine_entry = std::path::Path::new(app_id) + .extension() + .is_some_and(|ext| ext.eq_ignore_ascii_case("exe")); if !is_proton_game && !is_wine_entry { return None; @@ -487,10 +487,6 @@ fn proton_or_wine_fallback( #[cfg(not(windows))] fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec { - const SUFFIXES: &[&str] = &[".desktop", ".Desktop", ".DESKTOP"]; - let mut ordered = Vec::new(); - let mut seen = HashSet::new(); - fn push_candidate(seen: &mut HashSet, ordered: &mut Vec, candidate: &str) { let trimmed = candidate.trim(); if trimmed.is_empty() { @@ -531,11 +527,11 @@ fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec { } } - if trimmed.contains('.') { - if let Some(last) = trimmed.rsplit('.').next() { - if last.len() >= 2 { - push_candidate(seen, ordered, last); - } + if trimmed.contains('.') + && let Some(last) = trimmed.rsplit('.').next() + { + if last.len() >= 2 { + push_candidate(seen, ordered, last); } } @@ -546,13 +542,20 @@ fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec { push_candidate(seen, ordered, &trimmed.replace('_', "-")); } - for token in trimmed.split(|c: char| matches!(c, '.' | '-' | '_' | '@' | ' ')) { + for token in + trimmed.split(|c: char| matches!(c, '.' | '-' | '_' | '@') || c.is_whitespace()) + { if token.len() >= 2 && token != trimmed { push_candidate(seen, ordered, token); } } } + const SUFFIXES: &[&str] = &[".desktop", ".Desktop", ".DESKTOP"]; + + let mut ordered = Vec::new(); + let mut seen = HashSet::new(); + add_variants( &mut seen, &mut ordered, @@ -915,12 +918,20 @@ mod tests { let candidates = candidate_desktop_ids(&ctx); assert_eq!(candidates.first().unwrap(), "com.example.App.desktop"); - assert!(candidates.contains(&"com.example.App".to_string())); - assert!(candidates.contains(&"com-example-App".to_string())); - assert!(candidates.contains(&"com_example_App".to_string())); - assert!(candidates.contains(&"Example App".to_string())); - assert!(candidates.contains(&"Example".to_string())); - assert!(candidates.contains(&"App".to_string())); + for test in [ + "com.example.App", + "com-example-App", + "com_example_App", + "Example App", + "Example", + "App", + ] { + assert!( + candidates + .iter() + .any(|c| c.to_ascii_lowercase() == test.to_ascii_lowercase()), + ); + } } #[test] @@ -985,7 +996,7 @@ Icon=vmware-workstation\n\ let resolved = resolve_desktop_entry(&mut cache, &ctx, &DesktopResolveOptions::default()); - assert_eq!(resolved.id(), "vmware-workstation.desktop"); + assert_eq!(resolved.id(), "vmware-workstation"); } #[test] From 976e0e214f90aafae5913099475f4695e6b2841d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 3 Mar 2026 01:45:04 -0500 Subject: [PATCH 019/116] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 4516691f..14cefe03 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 4516691f3582a2a8c31f886b8e6090a235f6e72c +Subproject commit 14cefe034e57a189f53a10939b94d2b1d1cfdad3 From 8795c506fa9817faba87c5d0088c7a83aaab6c72 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 4 Mar 2026 10:44:42 -0500 Subject: [PATCH 020/116] chore: update iced should fix responsive widgets --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 14cefe03..40b6bfe9 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 14cefe034e57a189f53a10939b94d2b1d1cfdad3 +Subproject commit 40b6bfe9cabcaa932584f30f0710f8f69d6eb95d From ad65416551975cded91007b491b46556a45e059a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 4 Mar 2026 13:12:28 -0500 Subject: [PATCH 021/116] fix: resize border --- iced | 2 +- src/app/mod.rs | 2 +- src/applet/mod.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/iced b/iced index 40b6bfe9..fb1d5b2e 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 40b6bfe9cabcaa932584f30f0710f8f69d6eb95d +Subproject commit fb1d5b2ed88f8e56b8637b777cedb135a04098d4 diff --git a/src/app/mod.rs b/src/app/mod.rs index e11ed7ae..e137042e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -72,7 +72,7 @@ pub(crate) fn iced_settings( core.exit_on_main_window_closed = exit_on_close; if let Some(border_size) = settings.resizable { - // window_settings.resize_border = border_size as u32; + window_settings.resize_border = border_size as u32; window_settings.resizable = true; } window_settings.decorations = !settings.client_decorations; diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 0cbcacab..a3f5228b 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -569,7 +569,7 @@ pub fn run(flags: App::Flags) -> iced::Result { window_settings.decorations = false; window_settings.exit_on_close_request = true; window_settings.resizable = false; - // window_settings.resize_border = 0; + window_settings.resize_border = 0; // TODO make multi-window not mandatory From 1810bedfa5db1d2d8587ec9104bb3b527d9166f8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Mar=C3=ADn?= <62134857+mariinkys@users.noreply.github.com> Date: Thu, 5 Mar 2026 15:07:26 +0100 Subject: [PATCH 022/116] fix(navbar): fill height of panel instead of shrinking --- src/app/mod.rs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index e137042e..b36ec4f6 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -385,9 +385,8 @@ where .on_context(|id| crate::Action::Cosmic(Action::NavBarContext(id))) .context_menu(self.nav_context_menu(self.core().nav_bar_context())) .into_container() - // XXX both must be shrink to avoid flex layout from ignoring it .width(iced::Length::Shrink) - .height(iced::Length::Shrink); + .height(iced::Length::Fill); if !self.core().is_condensed() { nav = nav.max_width(280); From 197049945935f0e0c2d1e42e5c0ff136dd4146ce Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 5 Mar 2026 15:38:28 -0500 Subject: [PATCH 023/116] fix: capture mouse motion and mouse interactions in overlay --- src/widget/dropdown/multi/widget.rs | 2 +- src/widget/menu/menu_inner.rs | 338 +++++++++--------- src/widget/segmented_button/widget.rs | 482 +++++++++++++------------- src/widget/toaster/widget.rs | 2 +- 4 files changed, 417 insertions(+), 407 deletions(-) diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index a46c6dcc..779c6d00 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -135,7 +135,7 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> self.on_selected.as_ref(), self.selections, || tree.state.downcast_mut::>(), - ) + ); } fn mouse_interaction( diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 4f97d30e..d23a1599 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -585,9 +585,9 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { Cow::Borrowed(_) => panic!(), Cow::Owned(o) => o.as_mut_slice(), }; - let menu_status = process_menu_events( + process_menu_events( self, - &event, + event, view_cursor, renderer, clipboard, @@ -629,8 +629,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { if !self.is_overlay && !view_cursor.is_over(viewport) { return None; } - - let (new_root, status) = process_overlay_events( + let new_root = process_overlay_events( self, renderer, viewport_size, @@ -641,6 +640,10 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { shell, ); + if self.is_overlay && view_cursor.is_over(viewport) { + shell.capture_event(); + } + return new_root; } @@ -680,24 +683,23 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { feature = "winit", feature = "surface-message" ))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { - if let Some(handler) = self.on_surface_action.as_ref() { - let mut root = self.window_id; - let mut depth = self.depth; - while let Some(parent) = - state.popup_id.iter().find(|(_, v)| **v == root) - { - // parent of root popup is the window, so we stop. - if depth == 0 { - break; - } - root = *parent.0; - depth = depth.saturating_sub(1); + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) + && let Some(handler) = self.on_surface_action.as_ref() + { + let mut root = self.window_id; + let mut depth = self.depth; + while let Some(parent) = + state.popup_id.iter().find(|(_, v)| **v == root) + { + // parent of root popup is the window, so we stop. + if depth == 0 { + break; } - shell.publish((handler)(crate::surface::Action::DestroyPopup( - root, - ))); + root = *parent.0; + depth = depth.saturating_sub(1); } + shell + .publish((handler)(crate::surface::Action::DestroyPopup(root))); } state.reset(); @@ -708,7 +710,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { if self.bar_bounds.contains(overlay_cursor) { state.reset(); } - }) + }); } _ => {} @@ -804,26 +806,25 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { let menu_color = styling.background; r.fill_quad(menu_quad, menu_color); // draw path hightlight - if let (true, Some(active)) = (draw_path, ms.index) { - if let Some(active_layout) = children_layout + if let (true, Some(active)) = (draw_path, ms.index) + && let Some(active_layout) = children_layout .children() .nth(active.saturating_sub(start_index)) - { - let path_quad = renderer::Quad { - bounds: active_layout - .bounds() - .intersection(&viewport) - .unwrap_or_default(), - border: Border { - radius: styling.menu_border_radius.into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }; + { + let path_quad = renderer::Quad { + bounds: active_layout + .bounds() + .intersection(&viewport) + .unwrap_or_default(), + border: Border { + radius: styling.menu_border_radius.into(), + ..Default::default() + }, + shadow: Shadow::default(), + snap: true, + }; - r.fill_quad(path_quad, styling.path); - } + r.fill_quad(path_quad, styling.path); } if start_index < menu_roots.len() { // draw item @@ -894,6 +895,19 @@ impl overlay::Overlay, + cursor: mouse::Cursor, + _renderer: &crate::Renderer, + ) -> mouse::Interaction { + if cursor.is_over(layout.bounds()) { + mouse::Interaction::Idle + } else { + mouse::Interaction::None + } + } } impl Widget @@ -948,73 +962,74 @@ impl Widget Widget, + cursor: mouse::Cursor, + _viewport: &Rectangle, + _renderer: &crate::Renderer, + ) -> mouse::Interaction { + if cursor.is_over(layout.bounds()) { + mouse::Interaction::Idle + } else { + mouse::Interaction::None } } } @@ -1331,7 +1358,7 @@ fn process_menu_events( shell, &Rectangle::default(), ); - }) + }); } #[allow(unused_results, clippy::too_many_lines, clippy::too_many_arguments)] @@ -1343,12 +1370,11 @@ fn process_overlay_events( view_cursor: Cursor, overlay_cursor: Point, cross_offset: f32, - _shell: &mut Shell<'_, Message>, -) -> (Option<(usize, MenuState)>, event::Status) + shell: &mut Shell<'_, Message>, +) -> Option<(usize, MenuState)> where Message: std::clone::Clone, { - use event::Status::{Captured, Ignored}; /* if no active root || pressed: return @@ -1431,8 +1457,8 @@ where state.open = false; } } - - return (new_menu_root, Captured); + shell.capture_event(); + return new_menu_root; }; let last_menu_bounds = &last_menu_state.menu_bounds; @@ -1446,7 +1472,8 @@ where { last_menu_state.index = None; - return (new_menu_root, Captured); + shell.capture_event(); + return new_menu_root; } // calc new index @@ -1461,7 +1488,7 @@ where }; if state.pressed { - return (new_menu_root, Ignored); + return new_menu_root; } let roots = active_root.iter().skip(1).fold( &menu.menu_roots[active_root[0]].children, @@ -1494,7 +1521,7 @@ where if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && remove { if let Some(id) = state.popup_id.remove(&menu.window_id) { state.active_root.truncate(menu.depth + 1); - _shell.publish((menu.on_surface_action.as_ref().unwrap())({ + shell.publish((menu.on_surface_action.as_ref().unwrap())({ crate::surface::action::destroy_popup(id) })); } @@ -1555,7 +1582,8 @@ where state.menu_states.truncate(menu.depth + 1); } - (new_menu_root, Captured) + shell.capture_event(); + new_menu_root }) } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index f6de999e..857d6371 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -20,7 +20,7 @@ use iced::clipboard::mime::AllowedMimeTypes; use iced::touch::Finger; use iced::{ Alignment, Background, Color, Event, Length, Padding, Rectangle, Size, Task, Vector, alignment, - event, keyboard, mouse, touch, window, + keyboard, mouse, touch, window, }; use iced_core::mouse::ScrollDelta; use iced_core::text::{self, Ellipsize, LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; @@ -36,7 +36,6 @@ use std::collections::HashSet; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use std::marker::PhantomData; -use std::mem; use std::time::{Duration, Instant}; thread_local! { @@ -609,27 +608,26 @@ where .text .get(button) .zip(state.paragraphs.entry(button)) + && !text.is_empty() { - if !text.is_empty() { - icon_spacing = f32::from(self.button_spacing); - let paragraph = entry.or_insert_with(|| { - crate::Plain::new(Text { - content: text.to_string(), // TODO should we just use String at this point? - size: iced::Pixels(self.font_size), - bounds: Size::INFINITE, - font, - align_x: text::Alignment::Left, - align_y: alignment::Vertical::Center, - shaping: Shaping::Advanced, - wrapping: Wrapping::default(), - ellipsize: Ellipsize::default(), - line_height: self.line_height, - }) - }); + icon_spacing = f32::from(self.button_spacing); + let paragraph = entry.or_insert_with(|| { + crate::Plain::new(Text { + content: text.to_string(), // TODO should we just use String at this point? + size: iced::Pixels(self.font_size), + bounds: Size::INFINITE, + font, + align_x: text::Alignment::Left, + align_y: alignment::Vertical::Center, + shaping: Shaping::Advanced, + wrapping: Wrapping::default(), + ellipsize: Ellipsize::default(), + line_height: self.line_height, + }) + }); - let size = paragraph.min_bounds(); - width += size.width; - } + let size = paragraph.min_bounds(); + width += size.width; } // Add indent to measurement if found. @@ -895,10 +893,10 @@ where } // Unfocus if another segmented control was focused. - if let Some(f) = state.focused.as_ref() { - if f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get()) { - state.unfocus(); - } + if let Some(f) = state.focused.as_ref() + && f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get()) + { + state.unfocus(); } } @@ -1162,6 +1160,9 @@ where None:: Message>, on_drop, ); + if matches!(ret, iced::event::Status::Captured) { + shell.capture_event(); + } if let Some(msg) = maybe_msg { log::trace!( target: TAB_REORDER_LOG_TARGET, @@ -1200,9 +1201,8 @@ where } Event::Touch(touch::Event::FingerLifted { id, .. }) => { - state.fingers_pressed.remove(&id); + state.fingers_pressed.remove(id); } - _ => (), } @@ -1301,27 +1301,26 @@ where Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) ) && !over_close_button + && let Some(position) = cursor_position.position() { - if let Some(position) = cursor_position.position() { - state.tab_drag_candidate = Some(TabDragCandidate { - entity: key, - bounds, - origin: position, - }); - if let Some(tab_drag) = self.tab_drag.as_ref() { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "tab drag candidate entity={:?} origin=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}", - key, - position.x, - position.y, - bounds.x, - bounds.y, - bounds.width, - bounds.height, - tab_drag.threshold - ); - } + state.tab_drag_candidate = Some(TabDragCandidate { + entity: key, + bounds, + origin: position, + }); + if let Some(tab_drag) = self.tab_drag.as_ref() { + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "tab drag candidate entity={:?} origin=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}", + key, + position.x, + position.y, + bounds.x, + bounds.y, + bounds.width, + bounds.height, + tab_drag.threshold + ); } } @@ -1330,40 +1329,35 @@ where } if let Some(on_activate) = self.on_activate.as_ref() { - if is_pressed(&event) { + if is_pressed(event) { state.pressed_item = Some(Item::Tab(key)); - } else if is_lifted(&event) { - if self.button_is_pressed(state, key) { - shell.publish(on_activate(key)); - state.set_focused(); - state.focused_item = Item::Tab(key); - state.pressed_item = None; - shell.capture_event(); - return; - } + } else if is_lifted(&event) && self.button_is_pressed(state, key) { + shell.publish(on_activate(key)); + state.set_focused(); + state.focused_item = Item::Tab(key); + state.pressed_item = None; + shell.capture_event(); + return; } } // Present a context menu on a right click event. - if self.context_menu.is_some() { - if let Some(on_context) = self.on_context.as_ref() { - if right_button_released(&event) - || (touch_lifted(&event) && fingers_pressed == 2) - { - state.show_context = Some(key); - state.context_cursor = - cursor_position.position().unwrap_or_default(); + if self.context_menu.is_some() + && let Some(on_context) = self.on_context.as_ref() + && (right_button_released(&event) + || (touch_lifted(&event) && fingers_pressed == 2)) + { + state.show_context = Some(key); + state.context_cursor = cursor_position.position().unwrap_or_default(); - state.menu_state.inner.with_data_mut(|data| { - data.open = true; - data.view_cursor = cursor_position; - }); + state.menu_state.inner.with_data_mut(|data| { + data.open = true; + data.view_cursor = cursor_position; + }); - shell.publish(on_context(key)); - shell.capture_event(); - return; - } - } + shell.publish(on_context(key)); + shell.capture_event(); + return; } if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) = @@ -1385,57 +1379,56 @@ where } } - if self.scrollable_focus { - if let Some(on_activate) = self.on_activate.as_ref() { - if let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event { - let current = Instant::now(); + if self.scrollable_focus + && let Some(on_activate) = self.on_activate.as_ref() + && let Event::Mouse(mouse::Event::WheelScrolled { delta }) = event + { + let current = Instant::now(); - // Permit successive scroll wheel events only after a given delay. - if state.wheel_timestamp.is_none_or(|previous| { - current.duration_since(previous) > Duration::from_millis(250) - }) { - state.wheel_timestamp = Some(current); + // Permit successive scroll wheel events only after a given delay. + if state.wheel_timestamp.is_none_or(|previous| { + current.duration_since(previous) > Duration::from_millis(250) + }) { + state.wheel_timestamp = Some(current); - match delta { - ScrollDelta::Lines { y, .. } | ScrollDelta::Pixels { y, .. } => { - let mut activate_key = None; + match delta { + ScrollDelta::Lines { y, .. } | ScrollDelta::Pixels { y, .. } => { + let mut activate_key = None; - if *y < 0.0 { - let mut prev_key = Entity::null(); + if *y < 0.0 { + let mut prev_key = Entity::null(); - for key in self.model.order.iter().copied() { - if self.model.is_active(key) && !prev_key.is_null() { - activate_key = Some(prev_key); - } + for key in self.model.order.iter().copied() { + if self.model.is_active(key) && !prev_key.is_null() { + activate_key = Some(prev_key); + } + if self.model.is_enabled(key) { + prev_key = key; + } + } + } else if *y > 0.0 { + let mut buttons = self.model.order.iter().copied(); + while let Some(key) = buttons.next() { + if self.model.is_active(key) { + for key in buttons { if self.model.is_enabled(key) { - prev_key = key; - } - } - } else if *y > 0.0 { - let mut buttons = self.model.order.iter().copied(); - while let Some(key) = buttons.next() { - if self.model.is_active(key) { - for key in buttons { - if self.model.is_enabled(key) { - activate_key = Some(key); - break; - } - } + activate_key = Some(key); break; } } - } - - if let Some(key) = activate_key { - shell.publish(on_activate(key)); - state.set_focused(); - state.focused_item = Item::Tab(key); - shell.capture_event(); - return; + break; } } } + + if let Some(key) = activate_key { + shell.publish(on_activate(key)); + state.set_focused(); + state.focused_item = Item::Tab(key); + shell.capture_event(); + return; + } } } } @@ -1460,31 +1453,27 @@ where if let (Some(tab_drag), Some(candidate)) = (self.tab_drag.as_ref(), state.tab_drag_candidate) + && let Event::Mouse(mouse::Event::CursorMoved { .. }) = event + && let Some(position) = cursor_position.position() + && position.distance(candidate.origin) >= tab_drag.threshold + && let Some(candidate) = state.tab_drag_candidate.take() { - if let Event::Mouse(mouse::Event::CursorMoved { .. }) = event { - if let Some(position) = cursor_position.position() { - if position.distance(candidate.origin) >= tab_drag.threshold { - if let Some(candidate) = state.tab_drag_candidate.take() { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "tab drag threshold met entity={:?} distance={:.2} threshold={}", - candidate.entity, - position.distance(candidate.origin), - tab_drag.threshold - ); - if self.start_tab_drag( - state, - candidate.entity, - candidate.bounds, - position, - clipboard, - ) { - shell.capture_event(); - return; - } - } - } - } + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "tab drag threshold met entity={:?} distance={:.2} threshold={}", + candidate.entity, + position.distance(candidate.origin), + tab_drag.threshold + ); + if self.start_tab_drag( + state, + candidate.entity, + candidate.bounds, + position, + clipboard, + ) { + shell.capture_event(); + return; } } @@ -1504,55 +1493,53 @@ where { state.focused_visible = true; return if *modifiers == keyboard::Modifiers::SHIFT { - self.focus_previous(state, shell) + self.focus_previous(state, shell); } else if modifiers.is_empty() { - self.focus_next(state, shell) + self.focus_next(state, shell); }; } - if let Some(on_activate) = self.on_activate.as_ref() { - if let Event::Keyboard(keyboard::Event::KeyReleased { + if let Some(on_activate) = self.on_activate.as_ref() + && let Event::Keyboard(keyboard::Event::KeyReleased { key: keyboard::Key::Named(keyboard::key::Named::Enter), .. }) = event - { - match state.focused_item { - Item::Tab(entity) => { - shell.publish(on_activate(entity)); - } - - Item::PrevButton => { - if self.prev_tab_sensitive(state) { - state.buttons_offset -= 1; - - // If the change would cause it to be insensitive, focus the first tab. - if !self.prev_tab_sensitive(state) { - if let Some(first) = self.first_tab(state) { - state.focused_item = Item::Tab(first); - } - } - } - } - - Item::NextButton => { - if self.next_tab_sensitive(state) { - state.buttons_offset += 1; - - // If the change would cause it to be insensitive, focus the last tab. - if !self.next_tab_sensitive(state) { - if let Some(last) = self.last_tab(state) { - state.focused_item = Item::Tab(last); - } - } - } - } - - Item::None | Item::Set => (), + { + match state.focused_item { + Item::Tab(entity) => { + shell.publish(on_activate(entity)); } - shell.capture_event(); - return; + Item::PrevButton => { + if self.prev_tab_sensitive(state) { + state.buttons_offset -= 1; + + // If the change would cause it to be insensitive, focus the first tab. + if !self.prev_tab_sensitive(state) + && let Some(first) = self.first_tab(state) + { + state.focused_item = Item::Tab(first); + } + } + } + + Item::NextButton => { + if self.next_tab_sensitive(state) { + state.buttons_offset += 1; + + // If the change would cause it to be insensitive, focus the last tab. + if !self.next_tab_sensitive(state) + && let Some(last) = self.last_tab(state) + { + state.focused_item = Item::Tab(last); + } + } + } + + Item::None | Item::Set => (), } + + shell.capture_event(); } } } @@ -1794,22 +1781,22 @@ where let original_bounds = bounds; let center_y = bounds.center_y(); - if show_drop_hint_marker { - if matches!( + if show_drop_hint_marker + && matches!( drop_hint_marker, Some(DropHint { entity, side: DropSide::Before }) if entity == key - ) { - draw_drop_indicator( - renderer, - original_bounds, - DropSide::Before, - Self::VERTICAL, - appearance.active.text_color, - ); - } + ) + { + draw_drop_indicator( + renderer, + original_bounds, + DropSide::Before, + Self::VERTICAL, + appearance.active.text_color, + ); } let menu_open = || { @@ -1882,41 +1869,41 @@ where let mut indent_padding = 0.0; // Adjust bounds by indent - if let Some(indent) = self.model.indent(key) { - if indent > 0 { - let adjustment = f32::from(indent) * f32::from(self.indent_spacing); - bounds.x += adjustment; - bounds.width -= adjustment; + if let Some(indent) = self.model.indent(key) + && indent > 0 + { + let adjustment = f32::from(indent) * f32::from(self.indent_spacing); + bounds.x += adjustment; + bounds.width -= adjustment; - // Draw indent line - if let crate::theme::SegmentedButton::FileNav = self.style { - if indent > 1 { - indent_padding = 7.0; + // Draw indent line + if let crate::theme::SegmentedButton::FileNav = self.style + && indent > 1 + { + indent_padding = 7.0; - for level in 1..indent { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: (level as f32) - .mul_add(-(self.indent_spacing as f32), bounds.x) - + indent_padding, - width: 1.0, - ..bounds - }, - border: Border { - radius: rad_0.into(), - ..Default::default() - }, - shadow: Shadow::default(), - snap: true, - }, - divider_background, - ); - } - - indent_padding += 4.0; - } + for level in 1..indent { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: (level as f32) + .mul_add(-(self.indent_spacing as f32), bounds.x) + + indent_padding, + width: 1.0, + ..bounds + }, + border: Border { + radius: rad_0.into(), + ..Default::default() + }, + shadow: Shadow::default(), + snap: true, + }, + divider_background, + ); } + + indent_padding += 4.0; } } @@ -1990,40 +1977,35 @@ where bounds.x += offset; } else { // Draw the selection indicator if widget is a segmented selection, and the item is selected. - if key_is_active { - if let crate::theme::SegmentedButton::Control = self.style { - let mut image_bounds = bounds; - image_bounds.y = center_y - 8.0; + if key_is_active && let crate::theme::SegmentedButton::Control = self.style { + let mut image_bounds = bounds; + image_bounds.y = center_y - 8.0; - draw_icon::( - renderer, - theme, - style, - cursor, - viewport, - status_appearance.text_color, - Rectangle { - width: 16.0, - height: 16.0, - ..image_bounds - }, - crate::widget::icon( - match crate::widget::common::object_select().data() { - crate::iced_core::svg::Data::Bytes(bytes) => { - crate::widget::icon::from_svg_bytes(bytes.as_ref()) - .symbolic(true) - } - crate::iced_core::svg::Data::Path(path) => { - crate::widget::icon::from_path(path.clone()) - } - }, - ), - ); + draw_icon::( + renderer, + theme, + style, + cursor, + viewport, + status_appearance.text_color, + Rectangle { + width: 16.0, + height: 16.0, + ..image_bounds + }, + crate::widget::icon(match crate::widget::common::object_select().data() { + crate::iced_core::svg::Data::Bytes(bytes) => { + crate::widget::icon::from_svg_bytes(bytes.as_ref()).symbolic(true) + } + crate::iced_core::svg::Data::Path(path) => { + crate::widget::icon::from_path(path.clone()) + } + }), + ); - let offset = 16.0 + f32::from(self.button_spacing); + let offset = 16.0 + f32::from(self.button_spacing); - bounds.x += offset; - } + bounds.x += offset; } } diff --git a/src/widget/toaster/widget.rs b/src/widget/toaster/widget.rs index 240e4867..de47a9bd 100644 --- a/src/widget/toaster/widget.rs +++ b/src/widget/toaster/widget.rs @@ -248,7 +248,7 @@ where clipboard, shell, &layout.bounds(), - ) + ); } fn mouse_interaction( From 14a5d0c0ba60b95e5b244414da041df36148edc8 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Fri, 6 Mar 2026 11:55:53 -0500 Subject: [PATCH 024/116] fix(iced): reversed scroll direction --- examples/applet/Cargo.toml | 2 -- examples/application/Cargo.toml | 1 - iced | 2 +- 3 files changed, 1 insertion(+), 4 deletions(-) diff --git a/examples/applet/Cargo.toml b/examples/applet/Cargo.toml index 844ad8ff..f97bff44 100644 --- a/examples/applet/Cargo.toml +++ b/examples/applet/Cargo.toml @@ -13,8 +13,6 @@ env_logger = "0.10.2" log = "0.4.29" [dependencies.libcosmic] -# path = "../../" -branch = "iced-rebase" git = "https://github.com/pop-os/libcosmic" default-features = false features = ["applet-token"] diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index b1ac1242..c842c79f 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -12,7 +12,6 @@ env_logger = "0.11" [dependencies.libcosmic] git = "https://github.com/pop-os/libcosmic" -branch = "iced-rebase" features = [ "debug", "winit", diff --git a/iced b/iced index fb1d5b2e..b22d363f 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit fb1d5b2ed88f8e56b8637b777cedb135a04098d4 +Subproject commit b22d363f2c7d6485a3eddc6a54b2a652b7ded916 From 79f8337634071ac4fc32e063a5ecfc173dda4c6c Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Fri, 6 Mar 2026 13:21:34 -0500 Subject: [PATCH 025/116] fix(iced): space key is now handled differently in iced-winit --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index b22d363f..02149769 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit b22d363f2c7d6485a3eddc6a54b2a652b7ded916 +Subproject commit 02149769ec61d485ddc0bf4f07e98f0ec700420f From 3d2c018cd1df25697fa2825dbc5d040cff39389a Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Fri, 6 Mar 2026 14:37:56 -0500 Subject: [PATCH 026/116] fix(dnd_source): rely on current cursor position for hover state --- iced | 2 +- src/widget/dnd_source.rs | 51 +++++++++++++++++----------------------- 2 files changed, 22 insertions(+), 31 deletions(-) diff --git a/iced b/iced index 02149769..ac24bbe8 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 02149769ec61d485ddc0bf4f07e98f0ec700420f +Subproject commit ac24bbe80dd16ea586b8a0b5816066e3ba1b48fa diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index 07b448a5..25900a66 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -240,7 +240,7 @@ impl match mouse_event { mouse::Event::ButtonPressed(mouse::Button::Left) => { if let Some(position) = cursor.position() { - if !state.hovered { + if !cursor.is_over(layout.bounds()) { return; } @@ -256,33 +256,27 @@ impl { if let Some(position) = cursor.position() { - if state.hovered { - // We ignore motion if we do not possess drag content by now. - if self.drag_content.is_none() { - state.left_pressed_position = None; - return; + // We ignore motion if we do not possess drag content by now. + if self.drag_content.is_none() { + state.left_pressed_position = None; + return; + } + if let Some(left_pressed_position) = state.left_pressed_position + && position.distance(left_pressed_position) > self.drag_threshold + { + if let Some(on_start) = self.on_start.as_ref() { + shell.publish(on_start.clone()); } - if let Some(left_pressed_position) = state.left_pressed_position { - if position.distance(left_pressed_position) > self.drag_threshold { - if let Some(on_start) = self.on_start.as_ref() { - shell.publish(on_start.clone()) - } - let offset = Vector::new( - left_pressed_position.x - layout.bounds().x, - left_pressed_position.y - layout.bounds().y, - ); - self.start_dnd(clipboard, state.cached_bounds, offset); - state.is_dragging = true; - state.left_pressed_position = None; - } - } - if !cursor.is_over(layout.bounds()) { - state.hovered = false; - - return; - } - } else if cursor.is_over(layout.bounds()) { - state.hovered = true; + let offset = Vector::new( + left_pressed_position.x - layout.bounds().x, + left_pressed_position.y - layout.bounds().y, + ); + self.start_dnd(clipboard, state.cached_bounds, offset); + state.is_dragging = true; + state.left_pressed_position = None; + } + if !cursor.is_over(layout.bounds()) { + return; } shell.capture_event(); } @@ -296,7 +290,6 @@ impl { @@ -306,7 +299,6 @@ impl (), @@ -422,7 +414,6 @@ impl< /// Local state of the [`MouseListener`]. #[derive(Debug, Default)] struct State { - hovered: bool, left_pressed_position: Option, is_dragging: bool, cached_bounds: Rectangle, From 03d0171bbe36664b4749c2b70297a288f35df59f Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 6 Mar 2026 16:45:19 -0500 Subject: [PATCH 027/116] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index ac24bbe8..5f97135c 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit ac24bbe80dd16ea586b8a0b5816066e3ba1b48fa +Subproject commit 5f97135c3dd558cde27334a534df6f0b55ab02fa From 5eec82061592fdb5749045b71b1b2c3dbe24ee49 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sun, 8 Mar 2026 10:09:55 +0100 Subject: [PATCH 028/116] i18n: translation updates from weblate Co-authored-by: Aman Alam Co-authored-by: Ettore Atalan Co-authored-by: Hosted Weblate Co-authored-by: Vilius Paliokas Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/de/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/lt/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/pa/ Translation: Pop OS/libcosmic --- i18n/de/libcosmic.ftl | 6 ++++-- i18n/lt/libcosmic.ftl | 21 ++++++++++++++------- i18n/pa/libcosmic.ftl | 34 ++++++++++++++++++++++++++++++++++ 3 files changed, 52 insertions(+), 9 deletions(-) diff --git a/i18n/de/libcosmic.ftl b/i18n/de/libcosmic.ftl index 1f17c924..238000f5 100644 --- a/i18n/de/libcosmic.ftl +++ b/i18n/de/libcosmic.ftl @@ -21,8 +21,8 @@ september = September { $year } october = Oktober { $year } november = November { $year } december = Dezember { $year } -monday = Mo -tuesday = Di +monday = Montag +tuesday = Dienstag wednesday = Mittwoch thursday = Donnerstag friday = Freitag @@ -33,3 +33,5 @@ thu = Do fri = Fr sat = Sa sun = So +tue = Di +mon = Mo diff --git a/i18n/lt/libcosmic.ftl b/i18n/lt/libcosmic.ftl index 6472cbd3..097b3219 100644 --- a/i18n/lt/libcosmic.ftl +++ b/i18n/lt/libcosmic.ftl @@ -2,26 +2,33 @@ february = Vasaris { $year } close = Uždaryti documenters = Dokumentuotojai november = Lapkritis { $year } -friday = Penk -tuesday = Antr +friday = Penktadienis +tuesday = Antradienis may = Gegužė { $year } -wednesday = Treč +wednesday = Trečiadienis april = Balandis { $year } -monday = Pirm +monday = Pirmadienis translators = Vertėjai artists = Menininkai license = Licencija december = Gruodis { $year } -sunday = Sekm +sunday = Sekmadienis links = Nuorodos march = Kovas { $year } june = Birželis { $year } -saturday = Šešt +saturday = Šeštadienis august = Rugpjūtis { $year } developers = Kūrėjai july = Liepa { $year } -thursday = Ketv +thursday = Ketvirtadienis september = Rugsėjis { $year } designers = Dizaineriai october = Spalis { $year } january = Sausis { $year } +mon = Pirm +tue = Antr +wed = Treč +thu = Ketv +fri = Penkt +sat = Šešt +sun = Sekm diff --git a/i18n/pa/libcosmic.ftl b/i18n/pa/libcosmic.ftl index e69de29b..83d82608 100644 --- a/i18n/pa/libcosmic.ftl +++ b/i18n/pa/libcosmic.ftl @@ -0,0 +1,34 @@ +close = ਬੰਦ ਕਰੋ +license = ਲਸੰਸ +links = ਲਿੰਕ +developers = ਡਿਵੈਲਪਰ +designers = ਡਿਜ਼ਾਇਨਰ +artists = ਕਲਾਕਾਰ +translators = ਅਨੁਵਾਦਕ +documenters = ਦਸਤਾਵੇਜ਼ ਤਿਆਰ ਕਰਤਾ +january = ਜਨਵਰੀ { $year } +february = ਫਰਵਰੀ { $year } +march = ਮਾਰਚ { $year } +april = ਅਪਰੈਲ { $year } +may = ਮਈ { $year } +june = ਜੂਨ { $year } +july = ਜੁਲਾਈ { $year } +august = ਅਗਸਤ { $year } +september = ਸਤੰਬਰ { $year } +october = ਅਕਤੂਬਰ { $year } +november = ਨਵੰਬਰ { $year } +december = ਦਸੰਬਰ { $year } +monday = ਸੋਮਵਾਰ +mon = ਸੋਮ +tuesday = ਮੰਗਲਵਾਰ +tue = ਮੰਗਲ +wednesday = ਬੁੱਧਵਾਰ +wed = ਬੁੱਧ +thursday = ਵੀਰਵਾਰ +thu = ਵੀਰ +friday = ਸ਼ੁੱਕਰਵਾਰ +fri = ਸ਼ੁੱਕਰ +saturday = ਸ਼ਨਿੱਚਰਵਾਰ +sat = ਸ਼ਨਿੱਚਰ +sunday = ਐਤਵਾਰ +sun = ਐਤ From 4b92ee5f80dbc3cc6d64cf1411ade88ded8ff741 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 9 Mar 2026 16:15:03 -0400 Subject: [PATCH 029/116] chore: update iced includes fix for virtual offsets --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 5f97135c..99bc4551 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 5f97135c3dd558cde27334a534df6f0b55ab02fa +Subproject commit 99bc45511804ce94ddba880d4913e983a5f64a7f From 26f40869313f0843c436c0d8a59a05703330d571 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Tue, 10 Mar 2026 12:33:00 -0400 Subject: [PATCH 030/116] fix(iced): fix touch event handling --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 99bc4551..1e419b2b 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 99bc45511804ce94ddba880d4913e983a5f64a7f +Subproject commit 1e419b2bc6ba5bdbb7923b1798f5292815a8c2c3 From 242fe6c4ac2229f4d1aa233e09d34234883941d0 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 11 Mar 2026 10:15:30 -0400 Subject: [PATCH 031/116] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 1e419b2b..d8315860 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 1e419b2bc6ba5bdbb7923b1798f5292815a8c2c3 +Subproject commit d8315860b378536dc5b2fe821b9da54934d96ff3 From b4533e3a5621edfff6cdc0c064bca61555f832b1 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 11 Mar 2026 10:38:51 -0400 Subject: [PATCH 032/116] chore: update deps --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index d8315860..f0899a2a 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit d8315860b378536dc5b2fe821b9da54934d96ff3 +Subproject commit f0899a2a8192ed66f9d9bf00e3643194820239e6 From ce9e8b520579f8d70627093ebee74eb07c41055e Mon Sep 17 00:00:00 2001 From: Dryadxon <81884588+Dryadxon@users.noreply.github.com> Date: Wed, 11 Mar 2026 00:15:14 +0100 Subject: [PATCH 033/116] fix(flex_row): layout::resolve swap align_items with justify_items --- src/widget/flex_row/widget.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/flex_row/widget.rs b/src/widget/flex_row/widget.rs index b891c170..0b2e6e13 100644 --- a/src/widget/flex_row/widget.rs +++ b/src/widget/flex_row/widget.rs @@ -119,8 +119,8 @@ impl Widget for FlexR f32::from(self.column_spacing), f32::from(self.row_spacing), self.min_item_width, - self.align_items, self.justify_items, + self.align_items, self.justify_content, &mut tree.children, ) From 1dc9aa37ed8b8670217f7b9f82d40b3476204ad1 Mon Sep 17 00:00:00 2001 From: Dryadxon <81884588+Dryadxon@users.noreply.github.com> Date: Wed, 11 Mar 2026 17:25:02 +0100 Subject: [PATCH 034/116] feat(flex_row): re-export JustifyItems --- src/widget/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/mod.rs b/src/widget/mod.rs index eae255bc..73004597 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -259,7 +259,7 @@ pub use id_container::{IdContainer, id_container}; #[cfg(feature = "animated-image")] pub mod frames; -pub use taffy::JustifyContent; +pub use taffy::{JustifyContent, JustifyItems}; pub mod list; #[doc(inline)] From 01e5593741c7aa7eedf0692c6087ee3fb97feeb8 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 11 Mar 2026 22:35:31 -0400 Subject: [PATCH 035/116] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index f0899a2a..88f3b00d 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit f0899a2a8192ed66f9d9bf00e3643194820239e6 +Subproject commit 88f3b00d9625a3dd08ebaadb328fd119957fcd85 From c52ef976500c270b1f9b5fe488dbe5e153022ad3 Mon Sep 17 00:00:00 2001 From: Jonathan Wingrove Date: Sat, 14 Mar 2026 22:07:58 +0000 Subject: [PATCH 036/116] fix(table): Use on_item_mb_double for double-click handler instead of on_item_mb_left --- src/widget/table/widget/compact.rs | 2 +- src/widget/table/widget/standard.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widget/table/widget/compact.rs b/src/widget/table/widget/compact.rs index 85b5cfce..db71a1af 100644 --- a/src/widget/table/widget/compact.rs +++ b/src/widget/table/widget/compact.rs @@ -145,7 +145,7 @@ where }) // Double click .apply(|mouse_area| { - if let Some(ref on_item_mb) = val.on_item_mb_left { + if let Some(ref on_item_mb) = val.on_item_mb_double { mouse_area.on_double_click((on_item_mb)(entity)) } else { mouse_area diff --git a/src/widget/table/widget/standard.rs b/src/widget/table/widget/standard.rs index 79107074..1fa611f3 100644 --- a/src/widget/table/widget/standard.rs +++ b/src/widget/table/widget/standard.rs @@ -206,7 +206,7 @@ where }) // Double click .apply(|mouse_area| { - if let Some(ref on_item_mb) = val.on_item_mb_left { + if let Some(ref on_item_mb) = val.on_item_mb_double { mouse_area.on_double_click((on_item_mb)(entity)) } else { mouse_area From 12cc536cd54d2b4c99f4cf8803beb260ec40dc63 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 16 Mar 2026 14:24:46 -0400 Subject: [PATCH 037/116] chore: update iced fix for tiny-skia rotation --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 88f3b00d..d79181f4 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 88f3b00d9625a3dd08ebaadb328fd119957fcd85 +Subproject commit d79181f44325e63e35ef9e9653543b4bc09976bb From 9602dfd2f12b667e0afacdccd0e403d8152dde5a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 16 Mar 2026 15:59:34 -0400 Subject: [PATCH 038/116] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index d79181f4..7491547d 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit d79181f44325e63e35ef9e9653543b4bc09976bb +Subproject commit 7491547d7078c8bad54cf350b1276c7f32e50df5 From adb6e304052857392b596f5b1f9732af2955a0d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Tue, 17 Mar 2026 16:23:31 +0100 Subject: [PATCH 039/116] feat(header_bar): use custom widget for layout --- src/app/mod.rs | 6 +- src/widget/header_bar.rs | 434 +++++++++++++++++++-------------------- 2 files changed, 213 insertions(+), 227 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index b36ec4f6..47900107 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -742,9 +742,6 @@ impl ApplicationExt for App { })); let content: Element<_> = if content_container { content_col - .apply(container) - .width(iced::Length::Fill) - .height(iced::Length::Fill) .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container"))) .into() } else { @@ -772,8 +769,7 @@ impl ApplicationExt for App { .title(&core.window.header_title) .on_drag(crate::Action::Cosmic(Action::Drag)) .on_right_click(crate::Action::Cosmic(Action::ShowWindowMenu)) - .on_double_click(crate::Action::Cosmic(Action::Maximize)) - .is_condensed(is_condensed); + .on_double_click(crate::Action::Cosmic(Action::Maximize)); if self.nav_model().is_some() { let toggle = crate::widget::nav_bar_toggle() diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 1465a9d7..9ab6ff15 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -5,9 +5,8 @@ use crate::cosmic_theme::{Density, Spacing}; use crate::{Element, theme, widget}; use apply::Apply; use derive_setters::Setters; -use iced::{Length, mouse}; -use iced_core::{Vector, Widget, widget::tree}; -use std::{borrow::Cow, cmp}; +use iced_core::{Length, Size, Vector, Widget, layout, text, widget::tree}; +use std::borrow::Cow; #[must_use] pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { @@ -27,7 +26,6 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { sharp_corners: false, is_ssd: false, on_double_click: None, - is_condensed: false, transparent: false, } } @@ -91,9 +89,6 @@ pub struct HeaderBar<'a, Message> { /// HeaderBar used for server-side decorations is_ssd: bool, - /// Whether the headerbar should be compact - is_condensed: bool, - /// Whether the headerbar should be transparent transparent: bool, } @@ -126,48 +121,120 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { self.end.push(widget.into()); self } - - /// Build the widget - #[must_use] - #[inline] - pub fn build(self) -> HeaderBarWidget<'a, Message> { - HeaderBarWidget { - header_bar_inner: self.view(), - } - } } pub struct HeaderBarWidget<'a, Message> { - header_bar_inner: Element<'a, Message>, + start: Element<'a, Message>, + center: Option>, + end: Element<'a, Message>, } -impl Widget - for HeaderBarWidget<'_, Message> +impl<'a, Message> HeaderBarWidget<'a, Message> { + pub fn new( + start: Element<'a, Message>, + center: Option>, + end: Element<'a, Message>, + ) -> Self { + Self { start, center, end } + } + + fn elems(&self) -> impl Iterator> { + std::iter::once(&self.start) + .chain(std::iter::once(&self.end)) + .chain(self.center.as_ref()) + } + + fn elems_mut(&mut self) -> impl Iterator> { + std::iter::once(&mut self.start) + .chain(std::iter::once(&mut self.end)) + .chain(self.center.as_mut()) + } +} + +impl<'a, Message: Clone + 'static> Widget + for HeaderBarWidget<'a, Message> { fn diff(&mut self, tree: &mut tree::Tree) { - tree.diff_children(&mut [&mut self.header_bar_inner]); + if let Some(center) = &mut self.center { + tree.diff_children(&mut [&mut self.start, &mut self.end, center]); + } else { + tree.diff_children(&mut [&mut self.start, &mut self.end]); + } } fn children(&self) -> Vec { - vec![tree::Tree::new(&self.header_bar_inner)] + self.elems().map(tree::Tree::new).collect() } - fn size(&self) -> iced_core::Size { - self.header_bar_inner.as_widget().size() + fn size(&self) -> Size { + Size { + width: Length::Fill, + height: Length::Shrink, + } } fn layout( &mut self, tree: &mut tree::Tree, renderer: &crate::Renderer, - limits: &iced_core::layout::Limits, - ) -> iced_core::layout::Node { - let child_tree = &mut tree.children[0]; - let child = self - .header_bar_inner - .as_widget_mut() - .layout(child_tree, renderer, limits); - iced_core::layout::Node::with_children(child.size(), vec![child]) + limits: &layout::Limits, + ) -> layout::Node { + let width = limits.max().width; + let height = limits.max().height; + let gap = 8.0; + + let end_node = + self.end + .as_widget_mut() + .layout(&mut tree.children[1], renderer, &limits.loose()); + let end_width = end_node.size().width; + + let start_available = (width - end_width - gap).max(0.0); + let start_node = self.start.as_widget_mut().layout( + &mut tree.children[0], + renderer, + &layout::Limits::new(Size::ZERO, Size::new(start_available, height)), + ); + let start_width = start_node.size().width; + + let (center_node, center_x) = if let Some(center) = &mut self.center { + let slot_start = start_width + gap; + let slot_end = (width - end_width - gap).max(slot_start); + let slot_width = slot_end - slot_start; + // this instead of `node.size().width` prevents center jitter as text ellipsizes + let natural_width = center + .as_widget_mut() + .layout(&mut tree.children[2], renderer, &limits.loose()) + .size() + .width; + + let node = center.as_widget_mut().layout( + &mut tree.children[2], + renderer, + &layout::Limits::new(Size::ZERO, Size::new(slot_width, height)), + ); + + let ideal_x = (width - natural_width) / 2.0; + let max_x = (width - end_width - gap - natural_width).max(slot_start); + let center_x = ideal_x.clamp(slot_start, max_x); + (Some(node), center_x) + } else { + (None, 0.0) + }; + + let vcenter = |node: layout::Node, x: f32| -> layout::Node { + let dy = ((height - node.size().height) / 2.0).max(0.0); + node.translate(Vector::new(x, dy)) + }; + + let mut child_nodes = Vec::with_capacity(3); + child_nodes.push(vcenter(start_node, 0.0)); + child_nodes.push(vcenter(end_node, width - end_width)); + if let Some(cn) = center_node { + child_nodes.push(vcenter(cn, center_x)); + } + + layout::Node::with_children(Size::new(width, height), child_nodes) } fn draw( @@ -180,17 +247,10 @@ impl Widget cursor: iced_core::mouse::Cursor, viewport: &iced_core::Rectangle, ) { - let layout_children = layout.children().next().unwrap(); - let state_children = &tree.children[0]; - self.header_bar_inner.as_widget().draw( - state_children, - renderer, - theme, - style, - layout_children, - cursor, - viewport, - ); + for ((e, s), l) in self.elems().zip(&tree.children).zip(layout.children()) { + e.as_widget() + .draw(s, renderer, theme, style, l, cursor, viewport); + } } fn update( @@ -204,19 +264,14 @@ impl Widget shell: &mut iced_core::Shell<'_, Message>, viewport: &iced_core::Rectangle, ) { - let child_state = &mut state.children[0]; - let child_layout = layout.children().next().unwrap(); - - self.header_bar_inner.as_widget_mut().update( - child_state, - event, - child_layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ); + for ((e, s), l) in self + .elems_mut() + .zip(&mut state.children) + .zip(layout.children()) + { + e.as_widget_mut() + .update(s, event, l, cursor, renderer, clipboard, shell, viewport); + } } fn mouse_interaction( @@ -227,15 +282,15 @@ impl Widget viewport: &iced_core::Rectangle, renderer: &crate::Renderer, ) -> iced_core::mouse::Interaction { - let child_tree = &state.children[0]; - let child_layout = layout.children().next().unwrap(); - self.header_bar_inner.as_widget().mouse_interaction( - child_tree, - child_layout, - cursor, - viewport, - renderer, - ) + self.elems() + .zip(&state.children) + .zip(layout.children()) + .map(|((e, s), l)| { + e.as_widget() + .mouse_interaction(s, l, cursor, viewport, renderer) + }) + .max() + .unwrap_or(iced_core::mouse::Interaction::None) } fn operate( @@ -245,14 +300,13 @@ impl Widget renderer: &crate::Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { - let child_tree = &mut state.children[0]; - let child_layout = layout.children().next().unwrap(); - self.header_bar_inner.as_widget_mut().operate( - child_tree, - child_layout, - renderer, - operation, - ); + for ((e, s), l) in self + .elems_mut() + .zip(&mut state.children) + .zip(layout.children()) + { + e.as_widget_mut().operate(s, l, renderer, operation); + } } fn overlay<'b>( @@ -263,15 +317,27 @@ impl Widget viewport: &iced_core::Rectangle, translation: Vector, ) -> Option> { - let child_tree = &mut state.children[0]; - let child_layout = layout.children().next().unwrap(); - self.header_bar_inner.as_widget_mut().overlay( - child_tree, - child_layout, - renderer, - viewport, - translation, - ) + let mut layouts = layout.children(); + let mut try_overlay = |elem: &'b mut Element<'a, Message>, + state: &'b mut tree::Tree| + -> Option< + iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>, + > { + elem.as_widget_mut() + .overlay(state, layouts.next()?, renderer, viewport, translation) + }; + + if let Some(center) = &mut self.center { + let (start_slice, end_center) = state.children.split_at_mut(1); + let (end_slice, center_slice) = end_center.split_at_mut(1); + try_overlay(&mut self.start, &mut start_slice[0]) + .or_else(|| try_overlay(&mut self.end, &mut end_slice[0])) + .or_else(|| try_overlay(center, &mut center_slice[0])) + } else { + let (start_slice, end_slice) = state.children.split_at_mut(1); + try_overlay(&mut self.start, &mut start_slice[0]) + .or_else(|| try_overlay(&mut self.end, &mut end_slice[0])) + } } fn drag_destinations( @@ -281,15 +347,9 @@ impl Widget renderer: &crate::Renderer, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { - if let Some((child_tree, child_layout)) = - state.children.iter().zip(layout.children()).next() - { - self.header_bar_inner.as_widget().drag_destinations( - child_tree, - child_layout, - renderer, - dnd_rectangles, - ); + for ((e, s), l) in self.elems().zip(&state.children).zip(layout.children()) { + e.as_widget() + .drag_destinations(s, l, renderer, dnd_rectangles); } } @@ -301,16 +361,22 @@ impl Widget state: &tree::Tree, p: iced::mouse::Cursor, ) -> iced_accessibility::A11yTree { - let c_layout = layout.children().next().unwrap(); - let c_state = &state.children[0]; - self.header_bar_inner - .as_widget() - .a11y_nodes(c_layout, c_state, p) + iced_accessibility::A11yTree::join( + self.elems() + .zip(&state.children) + .zip(layout.children()) + .map(|((e, s), l)| e.as_widget().a11y_nodes(l, s, p)), + ) + } +} + +impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { + fn from(w: HeaderBarWidget<'a, Message>) -> Self { + Element::new(w) } } impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { - #[allow(clippy::too_many_lines)] /// Converts the headerbar builder into an Iced element. pub fn view(mut self) -> Element<'a, Message> { let Spacing { @@ -324,154 +390,85 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { let center = std::mem::take(&mut self.center); let mut end = std::mem::take(&mut self.end); - let window_control_cnt = self.on_close.is_some() as usize - + self.on_maximize.is_some() as usize - + self.on_minimize.is_some() as usize; // Also packs the window controls at the very end. - end.push(self.window_controls()); + end.push(self.window_controls(space_xxs)); - // Center content depending on window border - let padding = match self.density.unwrap_or_else(crate::config::header_size) { - Density::Compact => { - if self.maximized { - [4, 8, 4, 8] - } else { - [3, 7, 4, 7] - } - } - _ => { - if self.maximized { - [8, 8, 8, 8] - } else { - [7, 7, 8, 7] - } + let padding = if self.is_ssd { + [0, 8, 0, 8] + } else { + match ( + self.density.unwrap_or_else(crate::config::header_size), + // Center content depending on window border + self.maximized, + ) { + (Density::Compact, true) => [4, 8, 4, 8], + (Density::Compact, false) => [3, 7, 4, 7], + (_, true) => [8, 8, 8, 8], + (_, false) => [7, 7, 8, 7], } }; - let acc_count = |v: &[Element<'a, Message>]| { - v.iter().fold(0, |acc, e| { - acc + match e.as_widget().size().width { - Length::Fixed(w) if w > 30. => (w / 30.0).ceil() as usize, - _ => 1, - } - }) - }; - - let left_len = acc_count(&start); - let right_len = acc_count(&end); - - let portion = ((left_len.max(right_len + window_control_cnt) as f32 - / center.len().max(1) as f32) - .round() as u16) - .max(1); - let (left_portion, right_portion) = - if center.is_empty() && (self.title.is_empty() || self.is_condensed) { - let left_to_right_ratio = left_len as f32 / right_len.max(1) as f32; - let right_to_left_ratio = right_len as f32 / left_len.max(1) as f32; - if right_to_left_ratio > 2. || left_len < 1 { - (1, 2) - } else if left_to_right_ratio > 2. || right_len < 1 { - (2, 1) - } else { - (left_len as u16, (right_len + window_control_cnt) as u16) - } - } else { - (portion, portion) - }; - let title_portion = cmp::max(left_portion, right_portion) * 2; - // Creates the headerbar widget. - let mut widget = widget::row::with_capacity(3) - // If elements exist in the start region, append them here. - .push( - widget::row::with_children(start) + let start = widget::row::with_children(start) + .spacing(space_xxxs) + .align_y(iced::Alignment::Center) + .into(); + let center = if !center.is_empty() { + Some( + widget::row::with_children(center) .spacing(space_xxxs) .align_y(iced::Alignment::Center) - .apply(widget::container) - .align_x(iced::Alignment::Start) - .width(Length::FillPortion(left_portion)), + .into(), ) - // If elements exist in the center region, use them here. - // This will otherwise use the title as a widget if a title was defined. - .push_maybe(if !center.is_empty() { - Some( - widget::row::with_children(center) - .spacing(space_xxxs) - .align_y(iced::Alignment::Center) - .apply(widget::container) - .center_x(Length::Fill) - .into(), - ) - } else if !self.title.is_empty() && !self.is_condensed { - Some(self.title_widget(title_portion)) - } else { - None - }) - .push( - widget::row::with_children(end) - .spacing(space_xxs) - .align_y(iced::Alignment::Center) - .apply(widget::container) - .align_x(iced::Alignment::End) - .width(Length::FillPortion(right_portion)), + } else if !self.title.is_empty() { + Some( + widget::text::heading(self.title) + .wrapping(text::Wrapping::None) + .ellipsize(text::Ellipsize::End(text::EllipsizeHeightLimit::Lines(1))) + .into(), ) + } else { + None + }; + let end = widget::row::with_children(end) + .spacing(space_xxs) .align_y(iced::Alignment::Center) - .height(Length::Fixed(32.0 + padding[0] as f32 + padding[2] as f32)) - .padding(if self.is_ssd { [0, 8, 0, 8] } else { padding }) - .spacing(8) + .into(); + + let mut widget = HeaderBarWidget::new(start, center, end) .apply(widget::container) .class(crate::theme::Container::HeaderBar { focused: self.focused, sharp_corners: self.sharp_corners, transparent: self.transparent, }) - .center_y(Length::Shrink) + .height(Length::Fixed(32.0 + padding[0] as f32 + padding[2] as f32)) + .padding(padding) .apply(widget::mouse_area); - // Assigns a message to emit when the headerbar is dragged. - if let Some(message) = self.on_drag.clone() { + if let Some(message) = self.on_drag { widget = widget.on_drag(message); } - - // Assigns a message to emit when the headerbar is double-clicked. - if let Some(message) = self.on_maximize.clone() { + if let Some(message) = self.on_maximize { widget = widget.on_release(message); } - - if let Some(message) = self.on_double_click.clone() { + if let Some(message) = self.on_double_click { widget = widget.on_double_press(message); } - if let Some(message) = self.on_right_click.clone() { + if let Some(message) = self.on_right_click { widget = widget.on_right_press(message); } widget.into() } - fn title_widget(&mut self, title_portion: u16) -> Element<'a, Message> { - let mut title = Cow::default(); - std::mem::swap(&mut title, &mut self.title); - - widget::text::heading(title) - .wrapping(iced_core::text::Wrapping::None) - .ellipsize(iced_core::text::Ellipsize::End( - iced_core::text::EllipsizeHeightLimit::Lines(1), - )) - .apply(widget::container) - .center(Length::FillPortion(title_portion)) - .into() - } - /// Creates the widget for window controls. - fn window_controls(&mut self) -> Element<'a, Message> { + fn window_controls(&mut self, spacing: u16) -> Element<'a, Message> { macro_rules! icon { ($name:expr, $size:expr, $on_press:expr) => {{ - let icon = { - widget::icon::from_name($name) - .apply(widget::button::icon) - .padding(8) - }; - - icon.class(crate::theme::Button::HeaderBar) + widget::icon::from_name($name) + .apply(widget::button::icon) + .padding(8) + .class(crate::theme::Button::HeaderBar) .selected(self.focused) .icon_size($size) .on_press($on_press) @@ -482,7 +479,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .push_maybe( self.on_minimize .take() - .map(|m: Message| icon!("window-minimize-symbolic", 16, m)), + .map(|m| icon!("window-minimize-symbolic", 16, m)), ) .push_maybe(self.on_maximize.take().map(|m| { if self.maximized { @@ -496,21 +493,14 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .take() .map(|m| icon!("window-close-symbolic", 16, m)), ) - .spacing(theme::spacing().space_xxs) - .apply(widget::container) - .center_y(Length::Fill) + .spacing(spacing) + .align_y(iced::Alignment::Center) .into() } } impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { fn from(headerbar: HeaderBar<'a, Message>) -> Self { - Element::new(headerbar.build()) - } -} - -impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { - fn from(headerbar: HeaderBarWidget<'a, Message>) -> Self { - Element::new(headerbar) + headerbar.view() } } From 0bb006c5bbf7eb89491891d45bfc8f21f8eb1305 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Tue, 17 Mar 2026 17:13:46 +0100 Subject: [PATCH 040/116] fix(header_bar): add vertical SSD padding Prevents SSDs from having a gap after the rebase. --- src/widget/header_bar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 9ab6ff15..11b00e09 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -394,7 +394,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { end.push(self.window_controls(space_xxs)); let padding = if self.is_ssd { - [0, 8, 0, 8] + [2, 8, 2, 8] } else { match ( self.density.unwrap_or_else(crate::config::header_size), From c7ac9cfd31c8c5095d46f9322adc3e7c3208c94e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 17 Mar 2026 15:18:09 -0400 Subject: [PATCH 041/116] fix: if not in bounds, return default mouse interaction --- src/widget/segmented_button/widget.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 857d6371..059d8387 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -1596,7 +1596,7 @@ where } } - iced_core::mouse::Interaction::Idle + iced_core::mouse::Interaction::default() } #[allow(clippy::too_many_lines)] From 6c6d16d34a3572b96eabb40a86872c230d0cdd93 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Wed, 18 Mar 2026 10:53:09 -0400 Subject: [PATCH 042/116] fix(iced): scaling issue in the cosmic-greeter lock screen --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 7491547d..2d412482 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 7491547d7078c8bad54cf350b1276c7f32e50df5 +Subproject commit 2d412482884ff36b30aeca656c8c43043a9f3e20 From 54bcb9ec128e86b222a3435b7c90b8c660b769bc Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Wed, 18 Mar 2026 15:54:07 +0100 Subject: [PATCH 043/116] chore: update dependencies and examples --- Cargo.toml | 19 ++++----- cosmic-config/Cargo.toml | 8 ++-- cosmic-theme/Cargo.toml | 2 +- examples/cosmic/src/window/bluetooth.rs | 5 ++- examples/cosmic/src/window/demo.rs | 40 +++++++++--------- examples/cosmic/src/window/desktop.rs | 41 +++++++++---------- .../cosmic/src/window/system_and_accounts.rs | 9 ++-- src/app/cosmic.rs | 4 +- src/widget/header_bar.rs | 29 ++++++------- src/widget/settings/section.rs | 6 --- 10 files changed, 76 insertions(+), 87 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index ecb84bb5..23483a1d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -115,10 +115,10 @@ x11 = ["iced/x11", "iced_winit/x11"] [dependencies] apply = "0.3.0" -ashpd = { version = "0.12.1", default-features = false, optional = true } +ashpd = { version = "0.12.3", default-features = false, optional = true } async-fs = { version = "2.2", optional = true } async-std = { version = "1.13", optional = true } -auto_enums = "0.8.7" +auto_enums = "0.8.8" cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true } jiff = "0.2" cosmic-config = { path = "cosmic-config" } @@ -131,17 +131,16 @@ i18n-embed = { version = "0.16.0", features = [ i18n-embed-fl = "0.10" rust-embed = "8.11.0" css-color = "0.2.8" -derive_setters = "0.1.8" +derive_setters = "0.1.9" futures = "0.3" -image = { version = "0.25.9", default-features = false, features = [ +image = { version = "0.25.10", default-features = false, features = [ "jpeg", "png", ] } -libc = { version = "0.2.180", optional = true } +libc = { version = "0.2.183", optional = true } log = "0.4" mime = { version = "0.3.17", optional = true } palette = "0.7.6" -raw-window-handle = "0.6" rfd = { version = "0.16.0", default-features = false, features = [ "xdg-portal", ], optional = true } @@ -151,18 +150,18 @@ slotmap = "1.1.1" smol = { version = "2.0.2", optional = true } thiserror = "2.0.18" taffy = { version = "0.9.2", features = ["grid"] } -tokio = { version = "1.49.0", optional = true } +tokio = { version = "1.50.0", optional = true } tracing = "0.1.44" unicode-segmentation = "1.12" url = "2.5.8" -zbus = { version = "5.13.2", default-features = false, optional = true } +zbus = { version = "5.14.0", default-features = false, optional = true } float-cmp = "0.10.0" # Enable DBus feature on Linux targets [target.'cfg(target_os = "linux")'.dependencies] cosmic-config = { path = "cosmic-config", features = ["dbus"] } cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" } -zbus = { version = "5.13.2", default-features = false } +zbus = { version = "5.14.0", default-features = false } [target.'cfg(unix)'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } @@ -242,4 +241,4 @@ exclude = ["iced"] dirs = "6.0.0" [dev-dependencies] -tempfile = "3.24.0" +tempfile = "3.27.0" diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 6103c15e..0a7653e0 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -11,9 +11,9 @@ subscription = ["iced_futures"] [dependencies] cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } -zbus = { version = "5.13.2", default-features = false, optional = true } +zbus = { version = "5.14.0", default-features = false, optional = true } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } -calloop = { version = "0.14.3", optional = true } +calloop = { version = "0.14.4", optional = true } notify = "8.2.0" ron = "0.12.0" serde = "1.0.228" @@ -22,7 +22,7 @@ iced = { path = "../iced/", default-features = false, optional = true } iced_futures = { path = "../iced/futures/", default-features = false, optional = true } futures-util = { version = "0.3", optional = true } dirs.workspace = true -tokio = { version = "1.49", optional = true, features = ["time"] } +tokio = { version = "1.50", optional = true, features = ["time"] } async-std = { version = "1.13", optional = true } tracing = "0.1" @@ -30,4 +30,4 @@ tracing = "0.1" xdg = "3.0" [target.'cfg(windows)'.dependencies] -known-folders = "1.4.0" +known-folders = "1.4.2" diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 80f4805d..1d64912a 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -22,7 +22,7 @@ serde_json = { version = "1.0.149", optional = true, features = [ "preserve_order", ] } ron = "0.12.0" -csscolorparser = { version = "0.8.1", features = ["serde"] } +csscolorparser = { version = "0.8.3", features = ["serde"] } cosmic-config = { path = "../cosmic-config/", default-features = false, features = [ "subscription", "macro", diff --git a/examples/cosmic/src/window/bluetooth.rs b/examples/cosmic/src/window/bluetooth.rs index 44fe7d6c..1b5892f6 100644 --- a/examples/cosmic/src/window/bluetooth.rs +++ b/examples/cosmic/src/window/bluetooth.rs @@ -28,13 +28,14 @@ impl State { column!( list_column().add(settings::item( "Bluetooth", - toggler(None, self.enabled, Message::Enable) + toggler(self.enabled).on_toggle(Message::Enable) )), text("Now visible as \"TODO\", just kidding") ) .spacing(8) .into(), - settings::view_section("Devices") + settings::section() + .title("Devices") .add(settings::item("No devices found", text(""))) .into(), ]) diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 9ca84ef7..0d31fa93 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -258,12 +258,13 @@ impl State { match self.tab_bar.active_data() { None => panic!("no tab is active"), Some(DemoView::TabA) => settings::view_column(vec![ - settings::view_section("Debug") + settings::section() + .title("Debug") .add(settings::item("Debug theme", choose_theme)) .add(settings::item("Debug icon theme", choose_icon_theme)) .add(settings::item( "Debug layout", - toggler(None, window.debug, Message::Debug), + toggler(window.debug).on_toggle(Message::Debug), )) .add(settings::item( "Scaling Factor", @@ -276,10 +277,11 @@ impl State { .into(), ])) .into(), - settings::view_section("Controls") + settings::section() + .title("Controls") .add(settings::item( "Toggler", - toggler(None, self.toggler_value, Message::TogglerToggled), + toggler(self.toggler_value).on_toggle(Message::TogglerToggled), )) .add(settings::item( "Pick List (TODO)", @@ -299,15 +301,13 @@ impl State { .add(settings::item( "Progress", progress_bar(0.0..=100.0, self.slider_value) - .width(Length::Fixed(250.0)) - .height(Length::Fixed(4.0)), + .length(Length::Fixed(250.0)) + .girth(Length::Fixed(4.0)), )) - .add(settings::item_row(vec![checkbox( - "Checkbox", - self.checkbox_value, - Message::CheckboxToggled, - ) - .into()])) + .add(settings::item_row(vec![checkbox(self.checkbox_value) + .label("Checkbox") + .on_toggle(Message::CheckboxToggled) + .into()])) .add(settings::item( format!( "Spin Button (Range {}:{})", @@ -354,8 +354,7 @@ impl State { .width(Length::Shrink) .on_activate(Message::MultiSelection) .apply(container) - .center_x() - .width(Length::Fill) + .center_x(Length::Fill) .into(), text("Vertical With Spacing").into(), cosmic::iced::widget::row(vec![ @@ -424,13 +423,12 @@ impl State { ]) .padding(0) .into(), - Some(DemoView::TabC) => { - settings::view_column(vec![settings::view_section("Tab C") - .add(text("Nothing here yet").width(Length::Fill)) - .into()]) - .padding(0) - .into() - } + Some(DemoView::TabC) => settings::view_column(vec![settings::section() + .title("Tab C") + .add(text("Nothing here yet").width(Length::Fill)) + .into()]) + .padding(0) + .into(), }, container(text("Background container with some text").size(24)) .layer(cosmic_theme::Layer::Background) diff --git a/examples/cosmic/src/window/desktop.rs b/examples/cosmic/src/window/desktop.rs index 4fa726d8..46a4e5b8 100644 --- a/examples/cosmic/src/window/desktop.rs +++ b/examples/cosmic/src/window/desktop.rs @@ -147,7 +147,8 @@ impl State { fn view_desktop_options<'a>(&'a self, window: &'a Window) -> Element<'a, Message> { settings::view_column(vec![ window.parent_page_button(DesktopPage::DesktopOptions), - settings::view_section("Super Key Action") + settings::section() + .title("Super Key Action") .add(settings::item("Launcher", horizontal_space(Length::Fill))) .add(settings::item("Workspaces", horizontal_space(Length::Fill))) .add(settings::item( @@ -155,38 +156,34 @@ impl State { horizontal_space(Length::Fill), )) .into(), - settings::view_section("Hot Corner") + settings::section() + .title("Hot Corner") .add(settings::item( "Enable top-left hot corner for Workspaces", - toggler(None, self.top_left_hot_corner, Message::TopLeftHotCorner), + toggler(self.top_left_hot_corner).on_toggle(Message::TopLeftHotCorner), )) .into(), - settings::view_section("Top Panel") + settings::section() + .title("Top Panel") .add(settings::item( "Show Workspaces Button", - toggler( - None, - self.show_workspaces_button, - Message::ShowWorkspacesButton, - ), + toggler(self.show_workspaces_button).on_toggle(Message::ShowWorkspacesButton), )) .add(settings::item( "Show Applications Button", - toggler( - None, - self.show_applications_button, - Message::ShowApplicationsButton, - ), + toggler(self.show_applications_button) + .on_toggle(Message::ShowApplicationsButton), )) .into(), - settings::view_section("Window Controls") + settings::section() + .title("Window Controls") .add(settings::item( "Show Minimize Button", - toggler(None, self.show_minimize_button, Message::ShowMinimizeButton), + toggler(self.show_minimize_button).on_toggle(Message::ShowMinimizeButton), )) .add(settings::item( "Show Maximize Button", - toggler(None, self.show_maximize_button, Message::ShowMaximizeButton), + toggler(self.show_maximize_button).on_toggle(Message::ShowMaximizeButton), )) .into(), ]) @@ -245,12 +242,12 @@ impl State { list_column() .add(settings::item( "Same background on all displays", - toggler(None, self.same_background, Message::SameBackground), + toggler(self.same_background).on_toggle(Message::SameBackground), )) .add(settings::item("Background fit", text("TODO"))) .add(settings::item( "Slideshow", - toggler(None, self.slideshow, Message::Slideshow), + toggler(self.slideshow).on_toggle(Message::Slideshow), )) .into(), column(image_column).spacing(16).into(), @@ -261,7 +258,8 @@ impl State { fn view_desktop_workspaces<'a>(&'a self, window: &'a Window) -> Element<'a, Message> { settings::view_column(vec![ window.parent_page_button(DesktopPage::Wallpaper), - settings::view_section("Workspace Behavior") + settings::section() + .title("Workspace Behavior") .add(settings::item( "Dynamic workspaces", horizontal_space(Length::Fill), @@ -271,7 +269,8 @@ impl State { horizontal_space(Length::Fill), )) .into(), - settings::view_section("Multi-monitor Behavior") + settings::section() + .title("Multi-monitor Behavior") .add(settings::item( "Workspaces Span Displays", horizontal_space(Length::Fill), diff --git a/examples/cosmic/src/window/system_and_accounts.rs b/examples/cosmic/src/window/system_and_accounts.rs index e42e643c..ed1bd004 100644 --- a/examples/cosmic/src/window/system_and_accounts.rs +++ b/examples/cosmic/src/window/system_and_accounts.rs @@ -69,14 +69,16 @@ impl State { list_column() .add(settings::item("Device name", text("TODO"))) .into(), - settings::view_section("Hardware") + settings::section() + .title("Hardware") .add(settings::item("Hardware model", text("TODO"))) .add(settings::item("Memory", text("TODO"))) .add(settings::item("Processor", text("TODO"))) .add(settings::item("Graphics", text("TODO"))) .add(settings::item("Disk Capacity", text("TODO"))) .into(), - settings::view_section("Operating System") + settings::section() + .title("Operating System") .add(settings::item("Operating system", text("TODO"))) .add(settings::item( "Operating system architecture", @@ -85,7 +87,8 @@ impl State { .add(settings::item("Desktop environment", text("TODO"))) .add(settings::item("Windowing system", text("TODO"))) .into(), - settings::view_section("Related settings") + settings::section() + .title("Related settings") .add(settings::item("Get support", text("TODO"))) .into(), ]) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index edd7b157..9566403a 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -49,8 +49,8 @@ pub fn windowing_system() -> Option { WINDOWING_SYSTEM.get().copied() } -fn init_windowing_system(handle: raw_window_handle::WindowHandle) -> crate::Action { - let raw: &raw_window_handle::RawWindowHandle = handle.as_ref(); +fn init_windowing_system(handle: window::raw_window_handle::WindowHandle) -> crate::Action { + let raw = handle.as_ref(); let system = match raw { window::raw_window_handle::RawWindowHandle::UiKit(_) => WindowingSystem::UiKit, window::raw_window_handle::RawWindowHandle::AppKit(_) => WindowingSystem::AppKit, diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 11b00e09..1c0ca2c0 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -197,7 +197,16 @@ impl<'a, Message: Clone + 'static> Widget layout::Node { + let dy = ((height - node.size().height) / 2.0).max(0.0); + node.translate(Vector::new(x, dy)) + }; + + let mut child_nodes = Vec::with_capacity(3); + child_nodes.push(vcenter(start_node, 0.0)); + child_nodes.push(vcenter(end_node, width - end_width)); + + if let Some(center) = &mut self.center { let slot_start = start_width + gap; let slot_end = (width - end_width - gap).max(slot_start); let slot_width = slot_end - slot_start; @@ -217,21 +226,8 @@ impl<'a, Message: Clone + 'static> Widget layout::Node { - let dy = ((height - node.size().height) / 2.0).max(0.0); - node.translate(Vector::new(x, dy)) - }; - - let mut child_nodes = Vec::with_capacity(3); - child_nodes.push(vcenter(start_node, 0.0)); - child_nodes.push(vcenter(end_node, width - end_width)); - if let Some(cn) = center_node { - child_nodes.push(vcenter(cn, center_x)); + child_nodes.push(vcenter(node, center_x)) } layout::Node::with_children(Size::new(width, height), child_nodes) @@ -398,8 +394,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { } else { match ( self.density.unwrap_or_else(crate::config::header_size), - // Center content depending on window border - self.maximized, + self.maximized, // window border handling ) { (Density::Compact, true) => [4, 8, 4, 8], (Density::Compact, false) => [3, 7, 4, 7], diff --git a/src/widget/settings/section.rs b/src/widget/settings/section.rs index 899826dc..ab95b5ad 100644 --- a/src/widget/settings/section.rs +++ b/src/widget/settings/section.rs @@ -5,12 +5,6 @@ use crate::Element; use crate::widget::{ListColumn, column, text}; use std::borrow::Cow; -/// A section within a settings view column. -#[deprecated(note = "use `settings::section().title()` instead")] -pub fn view_section<'a, Message: 'static>(title: impl Into>) -> Section<'a, Message> { - section().title(title) -} - /// A section within a settings view column. pub fn section<'a, Message: 'static>() -> Section<'a, Message> { with_column(ListColumn::default()) From 3da55e807440a99f6ed62edc2e7a84ca4be9b844 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Tue, 17 Mar 2026 16:45:39 -0600 Subject: [PATCH 044/116] fix(flex_row): calculate height based on nodes --- src/widget/flex_row/layout.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/widget/flex_row/layout.rs b/src/widget/flex_row/layout.rs index ae0c28d6..166b47f4 100644 --- a/src/widget/flex_row/layout.rs +++ b/src/widget/flex_row/layout.rs @@ -162,9 +162,14 @@ pub fn resolve( }); }); + let actual_height = nodes + .iter() + .map(|node| node.bounds().y + node.bounds().height) + .fold(0.0f32, f32::max); + let size = Size { width: flex_layout.content_size.width, - height: flex_layout.content_size.height, + height: actual_height.max(flex_layout.content_size.height), }; Node::with_children(size, nodes) From 36cba695d2e4b12e4172ed8855811f6bf96223f6 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 19 Mar 2026 18:25:11 -0400 Subject: [PATCH 045/116] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 2d412482..a3a434ac 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 2d412482884ff36b30aeca656c8c43043a9f3e20 +Subproject commit a3a434ac924cb0d8f0c30ff704a01f01031c7fbb From 7a5676242259c1c743387b7a23df12bd8be1e53f Mon Sep 17 00:00:00 2001 From: Hojjat Date: Fri, 20 Mar 2026 14:33:40 -0600 Subject: [PATCH 046/116] fix: restore width and height fill for app content --- src/app/mod.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/app/mod.rs b/src/app/mod.rs index 47900107..5c0e95e4 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -742,6 +742,8 @@ impl ApplicationExt for App { })); let content: Element<_> = if content_container { content_col + .width(iced::Length::Fill) + .height(iced::Length::Fill) .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container"))) .into() } else { From dc3ebaa38e6b09c5f9489d2dabc7dd31012caf40 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Thu, 19 Mar 2026 12:18:13 -0600 Subject: [PATCH 047/116] feat(segmented_button): add ellipsize support --- src/widget/segmented_button/horizontal.rs | 12 +++++ src/widget/segmented_button/vertical.rs | 9 +++- src/widget/segmented_button/widget.rs | 53 +++++++++++++++++++++-- 3 files changed, 69 insertions(+), 5 deletions(-) diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 3e46dd5e..5fd67649 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -213,6 +213,18 @@ where state.buttons_offset = num - state.buttons_visible; } + // Resize paragraph bounds so that text ellipsis can take effect. + if !matches!(self.width, Length::Shrink) || state.collapsed { + let num = state.buttons_visible.max(1) as f32; + let spacing = f32::from(self.spacing); + let mut width_offset = 0.0; + if state.collapsed { + width_offset = f32::from(self.button_height) * 2.0; + } + let button_width = ((num).mul_add(-spacing, size.width - width_offset) + spacing) / num; + self.resize_paragraphs(state, button_width); + } + size } } diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 7963e9c8..5458cd0a 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -117,10 +117,15 @@ where height += item_height; } - limits.height(Length::Fixed(height)).resolve( + let size = limits.height(Length::Fixed(height)).resolve( self.width, self.height, Size::new(width, height), - ) + ); + + // Resize paragraph bounds so that text ellipsis can take effect. + self.resize_paragraphs(state, size.width); + + size } } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 059d8387..bdce1324 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -156,6 +156,8 @@ where pub(super) spacing: u16, /// LineHeight of the font. pub(super) line_height: LineHeight, + /// Ellipsize strategy for button text. + pub(super) ellipsize: Ellipsize, /// Style to draw the widget in. #[setters(into)] pub(super) style: Style, @@ -223,6 +225,7 @@ where width: Length::Fill, spacing: 0, line_height: LineHeight::default(), + ellipsize: Ellipsize::default(), style: Style::default(), context_menu: None, on_activate: None, @@ -275,7 +278,7 @@ where shaping: Shaping::Advanced, wrapping: Wrapping::None, line_height: self.line_height, - ellipsize: Ellipsize::default(), + ellipsize: self.ellipsize, }; paragraph.update(text); } else { @@ -289,7 +292,7 @@ where shaping: Shaping::Advanced, wrapping: Wrapping::None, line_height: self.line_height, - ellipsize: Ellipsize::default(), + ellipsize: self.ellipsize, }; state.paragraphs.insert(key, crate::Plain::new(text)); } @@ -621,7 +624,7 @@ where align_y: alignment::Vertical::Center, shaping: Shaping::Advanced, wrapping: Wrapping::default(), - ellipsize: Ellipsize::default(), + ellipsize: self.ellipsize, line_height: self.line_height, }) }); @@ -657,6 +660,50 @@ where (width, f32::from(self.button_height)) } + /// Resizes paragraph bounds based on the actual available button width so that + /// text ellipsis can take effect. Call this after `variant_layout` has populated + /// `state.internal_layout` with final button sizes. + pub(super) fn resize_paragraphs(&self, state: &mut LocalState, available_width: f32) { + if matches!(self.ellipsize, Ellipsize::None) { + return; + } + + for (nth, key) in self.model.order.iter().copied().enumerate() { + if self.model.text(key).is_some_and(|text| !text.is_empty()) { + let mut non_text_width = + f32::from(self.button_padding[0]) + f32::from(self.button_padding[2]); + + if let Some(icon) = self.model.icon(key) { + non_text_width += f32::from(icon.size) + f32::from(self.button_spacing); + } else if self.model.is_active(key) { + if let crate::theme::SegmentedButton::Control = self.style { + non_text_width += 16.0 + f32::from(self.button_spacing); + } + } + + if self.model.is_closable(key) { + non_text_width += + f32::from(self.close_icon.size) + f32::from(self.button_spacing); + } + + let text_width = (available_width - non_text_width).max(0.0); + + if let Some(paragraph) = state.paragraphs.get_mut(key) { + paragraph.resize(Size::new(text_width, f32::INFINITY)); + + // Update internal_layout actual content width so that + // button_alignment centering uses the ellipsized size. + let content_width = paragraph.min_bounds().width + non_text_width + - f32::from(self.button_padding[0]) + - f32::from(self.button_padding[2]); + if let Some(entry) = state.internal_layout.get_mut(nth) { + entry.1.width = content_width; + } + } + } + } + } + pub(super) fn max_button_dimensions( &self, state: &mut LocalState, From c804d3851d28ec4ecea38a430fc66d7858af6ce1 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Fri, 20 Mar 2026 15:07:11 -0600 Subject: [PATCH 048/116] fix: don't ever draw glyphs outside of the bounds --- src/widget/segmented_button/widget.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index bdce1324..76c74f3b 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -1985,7 +1985,9 @@ where // Align contents of the button to the requested `button_alignment`. { - let actual_width = state.internal_layout[nth].1.width; + // Avoid shifting content outside the left edge when the measured content is + // wider than the available button bounds (for example, non-ellipsized text). + let actual_width = state.internal_layout[nth].1.width.min(bounds.width); let offset = match self.button_alignment { Alignment::Start => None, From 141261b9bfdae30bdfd96feaf57d8ae6a48db55f Mon Sep 17 00:00:00 2001 From: Hojjat Date: Fri, 20 Mar 2026 16:25:10 -0600 Subject: [PATCH 049/116] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index a3a434ac..70f54c99 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit a3a434ac924cb0d8f0c30ff704a01f01031c7fbb +Subproject commit 70f54c994acb17aa247284366edc630d8514e23d From d7fd880ac6e3ea03b421541837d654bd036437ea Mon Sep 17 00:00:00 2001 From: Frederic Laing Date: Mon, 23 Mar 2026 01:11:11 +0100 Subject: [PATCH 050/116] fix(toggler): add touch input support --- src/widget/toggler.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 12bb8950..9d31ca1e 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -7,7 +7,7 @@ use iced_core::{ Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, event, layout, mouse, renderer::{self, Renderer}, - text, + text, touch, widget::{self, Tree, tree}, window, }; @@ -239,7 +239,8 @@ impl<'a, Message> Widget for Toggler<'a, }; let state = tree.state.downcast_mut::(); match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) => { let mouse_over = cursor_position.is_over(layout.bounds()); if mouse_over { From 8e439c842ccc37a5df0821141c61766aef10c53e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 23 Mar 2026 20:17:53 -0400 Subject: [PATCH 051/116] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 70f54c99..f59d5354 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 70f54c994acb17aa247284366edc630d8514e23d +Subproject commit f59d5354bfc433d636c6987a60b61bc8f7a25d68 From adb3e341fc35282c0cee108b73740dcb28d4efe1 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 25 Mar 2026 12:04:00 -0400 Subject: [PATCH 052/116] fix(theme): bright colors for success, warn, destructive --- cosmic-theme/src/model/theme.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 8e1cd9f7..5db0f32c 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -986,19 +986,19 @@ impl ThemeBuilder { let success = if let Some(success) = success { success.into_color() } else { - palette.as_ref().accent_green + palette.as_ref().bright_green }; let warning = if let Some(warning) = warning { warning.into_color() } else { - palette.as_ref().accent_yellow + palette.as_ref().bright_orange }; let destructive = if let Some(destructive) = destructive { destructive.into_color() } else { - palette.as_ref().accent_red + palette.as_ref().bright_red }; let text_steps_array = text_tint.map(|c| steps(c, NonZeroUsize::new(100).unwrap())); From 763f0da64cea86422150f522b6f0503653529a2e Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Thu, 26 Mar 2026 17:19:39 -0400 Subject: [PATCH 053/116] fix(iced): RTL text fix --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index f59d5354..a11b8282 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit f59d5354bfc433d636c6987a60b61bc8f7a25d68 +Subproject commit a11b828280ccded9dd2c5d52fb4c71dc9a999e3d From a38a6f5d73294441f6ee9f141dffb541a83a8fb0 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Thu, 26 Mar 2026 18:02:10 -0600 Subject: [PATCH 054/116] fix(ci): install dependencies --- .github/workflows/pages.yml | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 46d53ad2..4229839e 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -15,6 +15,8 @@ jobs: uses: actions/checkout@v3 with: submodules: recursive + - name: System dependencies + run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Build documentation run: cargo doc --verbose --features tokio,winit - name: Deploy documentation From e63f3196e2ae7e9a581829675d61c3e32ce1a194 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 27 Mar 2026 15:06:22 -0400 Subject: [PATCH 055/116] fix: MenuActive path highlight --- src/widget/menu/menu_inner.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index d23a1599..596e148e 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -765,7 +765,13 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { PathHighlight::OmitActive => { !indices.is_empty() && i < indices.len() - 1 } - PathHighlight::MenuActive => self.depth == state.active_root.len() - 1, + PathHighlight::MenuActive => { + !indices.is_empty() + && i < indices.len() + && menu_roots.len() > indices[i] + && (i < indices.len() - 1 + || !menu_roots[indices[i]].children.is_empty()) + } }); // react only to the last menu From 254c13cfc486833bf24dabf35038dd5991b1862d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 27 Mar 2026 14:37:35 -0400 Subject: [PATCH 056/116] fix: ellipsize text in menu items --- src/widget/menu/menu_tree.rs | 46 ++++++++++++++++++++++++++++++------ 1 file changed, 39 insertions(+), 7 deletions(-) diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index bd182b9c..047df0ed 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -252,9 +252,18 @@ pub fn menu_items< let l: Cow<'static, str> = label.into(); let key = find_key(&action, key_binds); let mut items = vec![ - widget::text(l).into(), + widget::text(l) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .into(), widget::space::horizontal().into(), - widget::text(key).class(key_class).into(), + widget::text(key) + .class(key_class) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .into(), ]; if let Some(icon) = icon { @@ -275,9 +284,18 @@ pub fn menu_items< let key = find_key(&action, key_binds); let mut items = vec![ - widget::text(l).into(), + widget::text(l) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .into(), widget::space::horizontal().into(), - widget::text(key).class(key_class).into(), + widget::text(key) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .class(key_class) + .into(), ]; if let Some(icon) = icon { @@ -312,9 +330,19 @@ pub fn menu_items< .into() }, widget::space::horizontal().width(spacing.space_xxs).into(), - widget::text(label).align_x(iced::Alignment::Start).into(), + widget::text(label) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .align_x(iced::Alignment::Start) + .into(), widget::space::horizontal().into(), - widget::text(key).class(key_class).into(), + widget::text(key) + .class(key_class) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .into(), ]; if let Some(icon) = icon { @@ -335,7 +363,11 @@ pub fn menu_items< trees.push(MenuTree::::with_children( RcElementWrapper::new(crate::Element::from( menu_button::<'static, _>(vec![ - widget::text(l.clone()).into(), + widget::text(l.clone()) + .ellipsize(iced_core::text::Ellipsize::Middle( + iced_core::text::EllipsizeHeightLimit::Lines(1), + )) + .into(), widget::space::horizontal().into(), widget::icon::from_name("pan-end-symbolic") .size(16) From 380b341bdc57c28b8e46da13a1baf4ec996ea6e8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=C3=8Dtalo=20Dell=20Areti?= Date: Thu, 19 Feb 2026 13:18:02 -0300 Subject: [PATCH 057/116] feat(text_input): add select_range method and Task function --- src/widget/text_input/input.rs | 17 ++++++++++++++++- 1 file changed, 16 insertions(+), 1 deletion(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 3960cee1..43db6a4d 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -1125,6 +1125,14 @@ pub fn select_all(id: Id) -> Task { task::effect(Action::widget(operation::text_input::select_all(id))) } +/// Produces a [`Task`] that selects a range of the content of the [`TextInput`] with the given +/// [`Id`]. +pub fn select_range(id: Id, start: usize, end: usize) -> Task { + task::effect(Action::widget(operation::text_input::select_range( + id, start, end, + ))) +} + /// Computes the layout of a [`TextInput`]. #[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_arguments)] @@ -2782,6 +2790,12 @@ impl State { self.cursor.select_range(0, usize::MAX); } + /// Selects a range of the content of the [`TextInput`]. + #[inline] + pub fn select_range(&mut self, start: usize, end: usize) { + self.cursor.select_range(start, end); + } + pub(super) fn setting_selection(&mut self, value: &Value, bounds: Rectangle, target: f32) { let position = if target > 0.0 { find_cursor_position(bounds, value, self, target) @@ -2842,8 +2856,9 @@ impl operation::TextInput for State { todo!() } + #[inline] fn select_range(&mut self, start: usize, end: usize) { - todo!() + Self::select_range(self, start, end); } } From 413e63f62a84ee9833eb13fa33ff44b27280f12a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 30 Mar 2026 18:51:33 -0400 Subject: [PATCH 058/116] chore: update features and feature gates --- Cargo.toml | 13 +++++-- iced | 2 +- src/app/action.rs | 6 +-- src/app/cosmic.rs | 63 ++++++++++++++++--------------- src/app/settings.rs | 4 +- src/core.rs | 8 ++-- src/lib.rs | 2 +- src/surface/action.rs | 18 ++++----- src/theme/style/mod.rs | 4 +- src/widget/autosize.rs | 2 +- src/widget/context_menu.rs | 36 +++++++++++++++--- src/widget/dropdown/mod.rs | 2 +- src/widget/dropdown/widget.rs | 22 +++++------ src/widget/menu/menu_bar.rs | 27 +++++++++++-- src/widget/menu/menu_inner.rs | 8 +++- src/widget/mod.rs | 2 +- src/widget/responsive_menu_bar.rs | 4 +- src/widget/text_input/input.rs | 38 +++++++++---------- 18 files changed, 159 insertions(+), 102 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 23483a1d..35d048ee 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ default = [ "a11y", "dbus-config", "x11", - "wayland", + "iced-wayland", "multi-window", ] # default = ["dbus-config", "multi-window", "a11y"] # Accessibility support @@ -80,15 +80,20 @@ tokio = [ ] # Tokio async runtime # Wayland window support -wayland = [ +iced-wayland = [ "ashpd?/wayland", "autosize", - "iced_runtime/wayland", "iced/wayland", "iced_winit/wayland", - "cctk", "surface-message", ] +wayland = [ + "iced-wayland", + "iced_runtime/cctk", + "iced_winit/cctk", + "iced/cctk", + "dep:cctk", +] surface-message = [] # multi-window support multi-window = [] diff --git a/iced b/iced index a11b8282..1fdd24ab 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit a11b828280ccded9dd2c5d52fb4c71dc9a999e3d +Subproject commit 1fdd24ab995a4d65ba83cc1957e992b57cc37fcd diff --git a/src/app/action.rs b/src/app/action.rs index 05fc7cbe..fb982acb 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -5,7 +5,7 @@ use crate::surface; use crate::theme::Theme; use crate::widget::nav_bar; use crate::{config::CosmicTk, keyboard_nav}; -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; @@ -69,10 +69,10 @@ pub enum Action { /// Updates the tracked window geometry. WindowResize(iced::window::Id, f32, f32), /// Tracks updates to window state. - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] WindowState(iced::window::Id, WindowState), /// Capabilities the window manager supports - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] WmCapabilities(iced::window::Id, WindowManagerCapabilities), #[cfg(feature = "xdg-portal")] DesktopSettings(crate::theme::portal::Desktop), diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 9566403a..b732eee9 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -8,16 +8,16 @@ use std::sync::Arc; use super::{Action, Application, ApplicationExt, Subscription}; use crate::theme::{THEME, Theme, ThemeType}; use crate::{Core, Element, keyboard_nav}; -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; -#[cfg(not(any(feature = "multi-window", feature = "wayland")))] +#[cfg(not(any(feature = "multi-window", feature = "wayland", target_os = "linux")))] use iced::Application as IcedApplication; -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] use iced::event::wayland; use iced::{Task, theme, window}; use iced_futures::event::listen_with; -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] use iced_winit::SurfaceIdWrapper; use palette::color_difference::EuclideanDistance; @@ -83,7 +83,7 @@ fn init_windowing_system(handle: window::raw_window_handle::WindowHandle) -> #[derive(Default)] pub struct Cosmic { pub app: App, - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] pub surface_views: HashMap< window::Id, ( @@ -138,7 +138,7 @@ where ) -> iced::Task> { #[cfg(feature = "surface-message")] match _surface_message { - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::AppSubsurface(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -168,7 +168,7 @@ where iced_winit::commands::subsurface::get_subsurface(settings(&mut self.app)) } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::Subsurface(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -196,7 +196,7 @@ where iced_winit::commands::subsurface::get_subsurface(settings()) } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::AppPopup(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -225,15 +225,15 @@ where iced_winit::commands::popup::get_popup(settings(&mut self.app)) } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::DestroyPopup(id) => { iced_winit::commands::popup::destroy_popup(id) } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::DestroySubsurface(id) => { iced_winit::commands::subsurface::destroy_subsurface(id) } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::DestroyWindow(id) => iced::window::close(id), crate::surface::Action::ResponsiveMenuBar { menu_bar, @@ -244,7 +244,7 @@ where core.menu_bars.insert(menu_bar, (limits, size)); iced::Task::none() } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::Popup(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -271,7 +271,7 @@ where iced_winit::commands::popup::get_popup(settings()) } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::AppWindow(id, settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { s.downcast:: iced::window::Settings + Send + Sync>>() @@ -310,7 +310,7 @@ where .discard() } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::Window(id, settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { s.downcast:: iced::window::Settings + Send + Sync>>() @@ -430,7 +430,7 @@ where } iced::Event::Window(window::Event::Focused) => return Some(Action::Focus(id)), iced::Event::Window(window::Event::Unfocused) => return Some(Action::Unfocus(id)), - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => { match event { wayland::Event::Popup(wayland::PopupEvent::Done, _, id) @@ -443,7 +443,7 @@ where ) => { return Some(Action::SuggestedBounds(b)); } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] wayland::Event::Window(iced::event::wayland::WindowEvent::WindowState( s, )) => { @@ -560,7 +560,7 @@ where #[cfg(feature = "multi-window")] pub fn view(&self, id: window::Id) -> Element<'_, crate::Action> { - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] if let Some((_, _, v)) = self.surface_views.get(&id) { return v(&self.app); } @@ -611,7 +611,7 @@ impl Cosmic { fn cosmic_update(&mut self, message: Action) -> iced::Task> { match message { Action::WindowMaximized(id, maximized) => { - #[cfg(not(feature = "wayland"))] + #[cfg(not(all(feature = "wayland", target_os = "linux")))] if self .app .core() @@ -641,7 +641,7 @@ impl Cosmic { }); } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Action::WindowState(id, state) => { if self .app @@ -693,7 +693,7 @@ impl Cosmic { } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Action::WmCapabilities(id, capabilities) => { if self .app @@ -800,7 +800,7 @@ impl Cosmic { new_theme.theme_type.prefer_dark(prefer_dark); cosmic_theme.set_theme(new_theme.theme_type); - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] if self.app.core().sync_window_border_radii_to_theme() { use iced_runtime::platform_specific::wayland::CornerRadius; use iced_winit::platform_specific::commands::corner_radius::corner_radius; @@ -946,7 +946,7 @@ impl Cosmic { // Only apply update if the theme is set to load a system theme if let ThemeType::System { .. } = cosmic_theme.theme_type { cosmic_theme.set_theme(new_theme.theme_type); - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] if self.app.core().sync_window_border_radii_to_theme() { use iced_runtime::platform_specific::wayland::CornerRadius; use iced_winit::platform_specific::commands::corner_radius::corner_radius; @@ -1040,7 +1040,7 @@ impl Cosmic { // Unminimize window before requesting to activate it. let mut task = iced_runtime::window::minimize(id, false); - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] { task = task.chain( iced_winit::platform_specific::commands::activation::activate( @@ -1051,7 +1051,7 @@ impl Cosmic { ) } - #[cfg(not(feature = "wayland"))] + #[cfg(not(all(feature = "wayland", target_os = "linux")))] { task = task.chain(iced_runtime::window::gain_focus(id)); } @@ -1068,7 +1068,7 @@ impl Cosmic { *v == 0 }) { self.opened_surfaces.remove(&id); - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] self.surface_views.remove(&id); self.tracked_windows.remove(&id); } @@ -1190,7 +1190,8 @@ impl Cosmic { #[cfg(all( feature = "wayland", feature = "multi-window", - feature = "surface-message" + feature = "surface-message", + target_os = "linux" ))] if let Some(( parent, @@ -1235,7 +1236,7 @@ impl Cosmic { core.applet.suggested_bounds = b; } Action::Opened(id) => { - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] if self.app.core().sync_window_border_radii_to_theme() { use iced_runtime::platform_specific::wayland::CornerRadius; use iced_winit::platform_specific::commands::corner_radius::corner_radius; @@ -1284,14 +1285,14 @@ impl Cosmic { pub fn new(app: App) -> Self { Self { app, - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] surface_views: HashMap::new(), tracked_windows: HashSet::new(), opened_surfaces: HashMap::new(), } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] /// Create a subsurface pub fn get_subsurface( &mut self, @@ -1314,7 +1315,7 @@ impl Cosmic { get_subsurface(settings) } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] /// Create a subsurface pub fn get_popup( &mut self, @@ -1336,7 +1337,7 @@ impl Cosmic { get_popup(settings) } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] /// Create a window surface pub fn get_window( &mut self, diff --git a/src/app/settings.rs b/src/app/settings.rs index 926181e1..5c903f09 100644 --- a/src/app/settings.rs +++ b/src/app/settings.rs @@ -16,7 +16,7 @@ pub struct Settings { pub(crate) antialiasing: bool, /// Autosize the window to fit its contents - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] pub(crate) autosize: bool, /// Set the application to not create a main window @@ -80,7 +80,7 @@ impl Default for Settings { fn default() -> Self { Self { antialiasing: true, - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] autosize: false, no_main_window: false, client_decorations: true, diff --git a/src/core.rs b/src/core.rs index 4d50e764..970a5351 100644 --- a/src/core.rs +++ b/src/core.rs @@ -99,7 +99,7 @@ pub struct Core { pub(crate) menu_bars: HashMap, - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] pub(crate) sync_window_border_radii_to_theme: bool, } @@ -159,7 +159,7 @@ impl Default for Core { main_window: None, exit_on_main_window_closed: true, menu_bars: HashMap::new(), - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] sync_window_border_radii_to_theme: true, } } @@ -493,12 +493,12 @@ impl Core { } // TODO should we emit tasks setting the corner radius or unsetting it if this is changed? - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] pub fn set_sync_window_border_radii_to_theme(&mut self, sync: bool) { self.sync_window_border_radii_to_theme = sync; } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] pub fn sync_window_border_radii_to_theme(&self) -> bool { self.sync_window_border_radii_to_theme } diff --git a/src/lib.rs b/src/lib.rs index 1a579f96..aa3b7db2 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -100,7 +100,7 @@ pub(crate) mod malloc; #[cfg(all(feature = "process", not(windows)))] pub mod process; -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] pub use cctk; pub mod surface; diff --git a/src/surface/action.rs b/src/surface/action.rs index 3a078ca3..50e2b4a9 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -9,25 +9,25 @@ use iced::window; use std::{any::Any, sync::Arc}; /// Used to produce a destroy popup message from within a widget. -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn destroy_popup(id: iced_core::window::Id) -> Action { Action::DestroyPopup(id) } -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn destroy_subsurface(id: iced_core::window::Id) -> Action { Action::DestroySubsurface(id) } -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn destroy_window(id: iced_core::window::Id) -> Action { Action::DestroyWindow(id) } -#[cfg(all(feature = "wayland", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn app_window( settings: impl Fn(&mut App) -> window::Settings + Send + Sync + 'static, @@ -60,7 +60,7 @@ pub fn app_window( } /// Used to create a window message from within a widget. -#[cfg(all(feature = "wayland", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn simple_window( settings: impl Fn() -> window::Settings + Send + Sync + 'static, @@ -92,7 +92,7 @@ pub fn simple_window( ) } -#[cfg(all(feature = "wayland", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn app_popup( settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings @@ -126,7 +126,7 @@ pub fn app_popup( } /// Used to create a subsurface message from within a widget. -#[cfg(all(feature = "wayland", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn simple_subsurface( settings: impl Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings @@ -155,7 +155,7 @@ pub fn simple_subsurface( } /// Used to create a popup message from within a widget. -#[cfg(all(feature = "wayland", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn simple_popup( settings: impl Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings @@ -186,7 +186,7 @@ pub fn simple_popup( ) } -#[cfg(all(feature = "wayland", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn subsurface( settings: impl Fn( diff --git a/src/theme/style/mod.rs b/src/theme/style/mod.rs index a187374c..bc648a73 100644 --- a/src/theme/style/mod.rs +++ b/src/theme/style/mod.rs @@ -32,7 +32,7 @@ mod text_input; #[doc(inline)] pub use self::text_input::TextInput; -#[cfg(all(feature = "wayland", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] pub mod tooltip; -#[cfg(all(feature = "wayland", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] pub use tooltip::Tooltip; diff --git a/src/widget/autosize.rs b/src/widget/autosize.rs index 937aabf9..69fd9c83 100644 --- a/src/widget/autosize.rs +++ b/src/widget/autosize.rs @@ -170,7 +170,7 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] if matches!( event, Event::PlatformSpecific(event::PlatformSpecific::Wayland( diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 200021c3..918d4da2 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -3,7 +3,12 @@ //! A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation. -#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] +#[cfg(all( + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" +))] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::widget::menu::{ self, CloseCondition, Direction, ItemHeight, ItemWidth, MenuBarState, PathHighlight, @@ -59,7 +64,12 @@ pub struct ContextMenu<'a, Message> { } impl ContextMenu<'_, Message> { - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all( + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] #[allow(clippy::too_many_lines)] fn create_popup( &mut self, @@ -364,7 +374,12 @@ impl Widget state.active_root.clear(); state.open = false; - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all( + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && let Some(id) = state.popup_id.remove(&self.window_id) { @@ -403,7 +418,12 @@ impl Widget state.open = true; state.view_cursor = cursor; }); - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all( + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { self.create_popup(layout, cursor, renderer, shell, viewport, state); } @@ -422,6 +442,7 @@ impl Widget #[cfg(all( feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -458,7 +479,12 @@ impl Widget _viewport: &iced::Rectangle, translation: Vector, ) -> Option> { - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all( + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && self.window_id != window::Id::NONE && self.on_surface_action.is_some() diff --git a/src/widget/dropdown/mod.rs b/src/widget/dropdown/mod.rs index b2d3fbed..b5fd4c06 100644 --- a/src/widget/dropdown/mod.rs +++ b/src/widget/dropdown/mod.rs @@ -50,7 +50,7 @@ pub fn popup_dropdown< let dropdown: Dropdown<'_, S, Message, AppMessage> = Dropdown::new(selections.into(), selected, on_selected); - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] let dropdown = dropdown.with_popup(_parent_id, _on_surface_action, _map_action); dropdown diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index b6244c07..2ff9c92f 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -60,7 +60,7 @@ where action_map: Option AppMessage + 'static + Send + Sync>>, #[setters(strip_option)] window_id: Option, - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, } @@ -96,14 +96,14 @@ where text_line_height: text::LineHeight::Relative(1.2), font: None, window_id: None, - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), on_surface_action: None, action_map: None, } } - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] /// Handle dropdown requests for popup creation. /// Intended to be used with [`crate::app::message::get_popup`] pub fn with_popup( @@ -154,7 +154,7 @@ where self } - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -268,7 +268,7 @@ where layout, cursor, shell, - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] self.positioner.clone(), self.on_selected.clone(), self.selected, @@ -346,7 +346,7 @@ where viewport: &Rectangle, translation: Vector, ) -> Option> { - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] if self.window_id.is_some() || self.on_surface_action.is_some() { return None; } @@ -545,7 +545,7 @@ pub fn update< layout: Layout<'_>, cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, on_selected: Arc Message + Send + Sync + 'static>, selected: Option, @@ -571,7 +571,7 @@ pub fn update< *hovered_guard = selected; let id = window::Id::unique(); state.popup_id = id; - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] if let Some(((on_surface_action, parent), action_map)) = on_surface_action .as_ref() .zip(_window_id) @@ -658,7 +658,7 @@ pub fn update< state.close_operation = false; state.is_open.store(false, Ordering::SeqCst); if is_open { - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] if let Some(ref on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } @@ -681,7 +681,7 @@ pub fn update< // Event wasn't processed by overlay, so cursor was clicked either outside it's // bounds or on the drop-down, either way we close the overlay. state.is_open.store(false, Ordering::Relaxed); - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] if let Some(on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } @@ -726,7 +726,7 @@ pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::In } } -#[cfg(all(feature = "winit", feature = "wayland"))] +#[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] /// Returns the current menu widget of a [`Dropdown`]. #[allow(clippy::too_many_arguments)] pub fn menu_widget< diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 7007befb..981446e8 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -12,6 +12,7 @@ use super::{ #[cfg(all( feature = "multi-window", feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -195,7 +196,12 @@ pub struct MenuBar { menu_roots: Vec>, style: ::Style, window_id: window::Id, - #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + target_os = "linux" + ))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, pub(crate) on_surface_action: Option Message + Send + Sync + 'static>>, @@ -230,7 +236,12 @@ where menu_roots, style: ::Style::default(), window_id: window::Id::NONE, - #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + target_os = "linux" + ))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), on_surface_action: None, } @@ -324,7 +335,12 @@ where self } - #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + target_os = "linux" + ))] pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -359,6 +375,7 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -629,6 +646,7 @@ where state.open = false; #[cfg(all( feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -652,6 +670,7 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -666,6 +685,7 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -748,6 +768,7 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 596e148e..74afe60f 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -7,6 +7,7 @@ use super::{menu_bar::MenuBarState, menu_tree::MenuTree}; #[cfg(all( feature = "multi-window", feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -680,6 +681,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { #[cfg(all( feature = "multi-window", feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -966,7 +968,8 @@ impl Widget( #[cfg(all( feature = "multi-window", feature = "wayland", + target_os = "linux", feature = "winit", feature = "surface-message" ))] @@ -1523,7 +1527,7 @@ where .as_ref() .is_some_and(|i| *i != new_index && !active_menu[*i].children.is_empty()); - #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all(feature = "multi-window", feature = "wayland",target_os = "linux", feature = "winit", feature = "surface-message"))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && remove { if let Some(id) = state.popup_id.remove(&menu.window_id) { state.active_root.truncate(menu.depth + 1); diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 73004597..0f607240 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -355,7 +355,7 @@ pub use toggler::{Toggler, toggler}; #[doc(inline)] pub use tooltip::{Tooltip, tooltip}; -#[cfg(all(feature = "wayland", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] pub mod wayland; pub mod tooltip { diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index 5f855260..b5dd556d 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -25,7 +25,7 @@ impl Default for ResponsiveMenuBar { fn default() -> ResponsiveMenuBar { ResponsiveMenuBar { collapsed_item_width: { - #[cfg(all(feature = "winit", feature = "wayland"))] + #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] if matches!( crate::app::cosmic::WINDOWING_SYSTEM.get(), Some(crate::app::cosmic::WindowingSystem::Wayland) @@ -34,7 +34,7 @@ impl Default for ResponsiveMenuBar { } else { ItemWidth::Static(84) } - #[cfg(not(all(feature = "winit", feature = "wayland")))] + #[cfg(not(all(feature = "winit", feature = "wayland", target_os = "linux")))] { ItemWidth::Static(84) } diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 43db6a4d..8f6fb329 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -513,7 +513,7 @@ where } /// Sets the start dnd handler of the [`TextInput`]. - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] pub fn on_start_dnd(mut self, on_start_dnd: impl Fn(State) -> Message + 'a) -> Self { self.on_create_dnd_source = Some(Box::new(on_start_dnd)); self @@ -1445,7 +1445,7 @@ pub fn update<'a, Message: Clone + 'static>( click.kind(), state.cursor().state(value), ) { - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] (None, click::Kind::Single, cursor::State::Selection { start, end }) => { let left = start.min(end); let right = end.max(start); @@ -1556,7 +1556,7 @@ pub fn update<'a, Message: Clone + 'static>( | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => { cold(); let state = state(); - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] if matches!(state.dragging_state, Some(DraggingState::PrepareDnd(_))) { // clear selection and place cursor at click position update_cache(state, value); @@ -1589,7 +1589,7 @@ pub fn update<'a, Message: Clone + 'static>( shell.capture_event(); return; } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] if let Some(DraggingState::PrepareDnd(start_position)) = state.dragging_state { let distance = ((position.x - start_position.x).powi(2) + (position.y - start_position.y).powi(2)) @@ -1980,7 +1980,7 @@ pub fn update<'a, Message: Clone + 'static>( shell.request_redraw(); } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Source(SourceEvent::Finished | SourceEvent::Cancelled)) => { cold(); let state = state(); @@ -1991,7 +1991,7 @@ pub fn update<'a, Message: Clone + 'static>( return; } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer( rectangle, OfferEvent::Enter { @@ -2032,7 +2032,7 @@ pub fn update<'a, Message: Clone + 'static>( return; } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y })) if *rectangle == Some(dnd_id) => { @@ -2051,7 +2051,7 @@ pub fn update<'a, Message: Clone + 'static>( shell.capture_event(); return; } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if *rectangle == Some(dnd_id) => { cold(); let state = state(); @@ -2069,9 +2069,9 @@ pub fn update<'a, Message: Clone + 'static>( return; } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != *id => {} - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer( rectangle, OfferEvent::Leave | OfferEvent::LeaveDestination, @@ -2089,7 +2089,7 @@ pub fn update<'a, Message: Clone + 'static>( shell.capture_event(); return; } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type })) if *rectangle == Some(dnd_id) => { @@ -2336,9 +2336,9 @@ pub fn draw<'a, Message>( let actual_width = text_width.max(text_bounds.width); let radius_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0.into(); - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None); - #[cfg(not(feature = "wayland"))] + #[cfg(not(all(feature = "wayland", target_os = "linux")))] let handling_dnd_offer = false; let (cursor, offset) = if let Some(focus) = state.is_focused.filter(|f| f.focused).or_else(|| { @@ -2567,7 +2567,7 @@ pub fn mouse_interaction( #[derive(Debug, Clone)] pub struct TextInputString(pub String); -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] impl AsMimeTypes for TextInputString { fn available(&self) -> Cow<'static, [String]> { Cow::Owned( @@ -2591,13 +2591,13 @@ impl AsMimeTypes for TextInputString { #[derive(Debug, Clone, PartialEq)] pub(crate) enum DraggingState { Selection, - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] PrepareDnd(Point), - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] Dnd(DndAction, String), } -#[cfg(feature = "wayland")] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[derive(Debug, Default, Clone)] pub(crate) enum DndOfferState { #[default] @@ -2606,7 +2606,7 @@ pub(crate) enum DndOfferState { Dropped, } #[derive(Debug, Default, Clone)] -#[cfg(not(feature = "wayland"))] +#[cfg(not(all(feature = "wayland", target_os = "linux")))] pub(crate) struct DndOfferState; /// The state of a [`TextInput`]. @@ -2680,7 +2680,7 @@ impl State { } } - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", target_os = "linux"))] /// Returns the current value of the dragged text in the [`TextInput`]. #[must_use] pub fn dragged_text(&self) -> Option { From f06d15ae35204cb3bcef5a3188b5ec59a1cc9bfd Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Tue, 31 Mar 2026 16:02:52 +0100 Subject: [PATCH 059/116] feat(cosmic-theme): produce QPalette ini for more compatibility --- cosmic-theme/src/output/mod.rs | 4 + cosmic-theme/src/output/qt56ct_output.rs | 281 ++++++++++++++++++++++- cosmic-theme/src/output/qt_output.rs | 38 +-- 3 files changed, 303 insertions(+), 20 deletions(-) diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index b2474dc1..19f7bc5b 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -46,8 +46,10 @@ impl Theme { pub fn write_exports(&self) -> Result<(), OutputError> { let gtk_res = self.write_gtk4(); let qt_res = self.write_qt(); + let qt56ct_res = self.write_qt56ct(); gtk_res?; qt_res?; + qt56ct_res?; Ok(()) } @@ -56,8 +58,10 @@ impl Theme { pub fn reset_exports() -> Result<(), OutputError> { let gtk_res = Theme::reset_gtk(); let qt_res = Theme::reset_qt(); + let qt56ct_res = Theme::reset_qt56ct(); gtk_res?; qt_res?; + qt56ct_res?; Ok(()) } } diff --git a/cosmic-theme/src/output/qt56ct_output.rs b/cosmic-theme/src/output/qt56ct_output.rs index 552e7fec..eccfc846 100644 --- a/cosmic-theme/src/output/qt56ct_output.rs +++ b/cosmic-theme/src/output/qt56ct_output.rs @@ -1,8 +1,11 @@ use crate::Theme; use configparser::ini::Ini; +use palette::{Mix, Srgba, WithAlpha, blend::Compose, rgb::Rgba}; use std::{ fs::{self, File}, + io::Write, path::PathBuf, + vec, }; use super::{OutputError, qt_settings_ini_style}; @@ -15,7 +18,117 @@ impl Theme { /// Increment this value when changes to qt{5,6}ct.conf are needed. /// If the config's version is outdated, we update several sections. /// Otherwise, only the light/dark mode is updated. - const COSMIC_QT_VERSION: u64 = 1; + const COSMIC_QT_VERSION: u64 = 2; + + /// Produces a QPalette ini file for qt5ct and qt6ct. + /// + /// Example file: https://github.com/trialuser02/qt6ct/blob/master/colors/airy.conf + #[must_use] + #[cold] + pub fn as_qpalette(&self) -> String { + let lightest = if self.is_dark { + self.background.on + } else { + self.background.base + }; + let darkest = if self.is_dark { + self.background.base + } else { + self.background.on + }; + let active = QPaletteGroup { + window_text: self.background.on, + button: self.button.base, + light: self.button.base.mix(lightest, 0.1), + midlight: self.button.base.mix(lightest, 0.05), + dark: self.button.base.mix(darkest, 0.1), + mid: self.button.base.mix(darkest, 0.05), + text: self.background.component.on, + bright_text: lightest, + button_text: self.button.on, + base: self.background.component.base, + window: self.background.base, + shadow: darkest, + // selection colors are swapped to fix menu bar contrast + highlight: self.background.component.selected_text, + highlighted_text: self.background.component.selected, + link: self.link_button.on, + link_visited: self.link_button.on.mix(self.secondary.component.base, 0.2), + alternate_base: self.background.base.mix(self.accent.base, 0.05), + no_role: self.background.component.disabled, + tool_tip_base: self.background.component.base, + tool_tip_text: self.background.component.on, + placeholder_text: self.background.component.on.with_alpha(0.5), + }; + let inactive = QPaletteGroup { + window_text: active.window_text.with_alpha(0.8), + text: active.text.with_alpha(0.8), + highlighted_text: active.highlighted_text.with_alpha(0.8), + tool_tip_text: active.tool_tip_text.with_alpha(0.8), + ..active + }; + let disabled = QPaletteGroup { + button: self.button.disabled, + text: self.background.component.on_disabled, + button_text: self.button.on_disabled, + base: self.background.component.disabled, + highlighted_text: active.highlighted_text.with_alpha(0.5), + link: self.link_button.on_disabled, + link_visited: self + .link_button + .on_disabled + .mix(self.secondary.component.disabled, 0.2), + alternate_base: self.background.base.mix(self.accent.disabled, 0.05), + tool_tip_base: self.background.component.disabled, + tool_tip_text: self.background.component.on_disabled, + placeholder_text: self.background.component.on_disabled.with_alpha(0.5), + ..inactive + }; + + format!( + r#"# GENERATED BY COSMIC + +[ColorScheme] +active_colors={} +disabled_colors={} +inactive_colors={} +"#, + active.as_list(), + disabled.as_list(), + inactive.as_list(), + ) + } + + /// Writes the QPalette ini files to: + /// - `~/.config/qt6ct/colors/` + /// - `~/.config/qt5ct/colors/` + #[cold] + pub fn write_qt56ct(&self) -> Result<(), OutputError> { + let qpalette = self.as_qpalette(); + let qt5ct_res = self.write_ct("qt5ct", &qpalette); + let qt6ct_res = self.write_ct("qt6ct", &qpalette); + qt5ct_res?; + qt6ct_res?; + Ok(()) + } + #[must_use] + #[cold] + fn write_ct(&self, ct: &str, qpalette: &str) -> Result<(), OutputError> { + let file_path = Self::get_qpalette_path(ct, self.is_dark)?; + let tmp_file_path = file_path.with_extension("conf.new"); + + let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?; + let res = tmp_file + .write_all(qpalette.as_bytes()) + .and_then(|_| tmp_file.flush()) + .and_then(|_| std::fs::rename(&tmp_file_path, file_path)); + if let Err(e) = res { + _ = std::fs::remove_file(&tmp_file_path); + return Err(OutputError::Io(e)); + } + + Ok(()) + } /// Edits qt{5,6}ct.conf to use COSMIC styles if needed. #[cold] @@ -39,7 +152,7 @@ impl Theme { .map_err(OutputError::Ini)? .unwrap_or_default(); - let color_scheme_path = Self::get_qt_colors_path(is_dark)?; + let color_scheme_path = Self::get_qpalette_path(ct, is_dark)?; let icon_theme = if is_dark { "breeze-dark" } else { "breeze" }; ini.set( @@ -91,11 +204,48 @@ impl Theme { Ok(()) } + /// Reset the applied qt56ct config by removing COSMIC-specific entries from the config file. + #[cold] + pub fn reset_qt56ct() -> Result<(), OutputError> { + let qt5ct_res = Self::reset_ct("qt5ct"); + let qt6ct_res = Self::reset_ct("qt6ct"); + qt5ct_res?; + qt6ct_res?; + Ok(()) + } + #[must_use] + #[cold] + fn reset_ct(ct: &str) -> Result<(), OutputError> { + let path = Self::get_conf_path(ct)?; + let file_content = fs::read_to_string(&path).map_err(OutputError::Io)?; + let mut ini = Ini::new_cs(); + ini.read(file_content).map_err(OutputError::Ini)?; + + let old_version = ini + .getuint("Appearance", "cosmic_qt_version") + .map_err(OutputError::Ini)? + .unwrap_or_default(); + if old_version == 0 { + return Ok(()); + } + + ini.remove_key("Appearance", "cosmic_qt_version"); + ini.remove_key("Appearance", "color_scheme_path"); + ini.remove_key("Appearance", "icon_theme"); + + ini.pretty_write(path, &qt_settings_ini_style()) + .map_err(OutputError::Io)?; + Ok(()) + } + /// Returns the file paths of the form `~/.config/ct/ct.conf`: /// e.g. `~/.config/qt6ct/qt6ct.conf`. /// /// The file and its parent directory are created if they don't exist. + #[cold] fn get_conf_path(ct: &str) -> Result { + assert!(ct == "qt5ct" || ct == "qt6ct"); + let Some(mut config_dir) = dirs::config_dir() else { return Err(OutputError::MissingConfigDir); }; @@ -111,4 +261,131 @@ impl Theme { Ok(file_path) } + + /// Gets a path like `~/.config/qt6ct/colors/CosmicDark.conf` + /// + /// Its parent directory is created if it doesn't exist. + #[cold] + fn get_qpalette_path(ct: &str, is_dark: bool) -> Result { + assert!(ct == "qt5ct" || ct == "qt6ct"); + + let Some(mut config_dir) = dirs::config_dir() else { + return Err(OutputError::MissingConfigDir); + }; + config_dir.push(&ct); + config_dir.push("colors"); + if !config_dir.exists() { + fs::create_dir_all(&config_dir).map_err(OutputError::Io)?; + } + + let file_name = if is_dark { + "CosmicDark.conf" + } else { + "CosmicLight.conf" + }; + + Ok(config_dir.join(file_name)) + } +} + +/// Defines the different symbolic color roles used in current GUIs. +/// +/// qt5ct and qt6ct consume this as a list of colors, ordered by ColorRole: +/// - https://doc.qt.io/qt-6/qpalette.html#ColorRole-enum +/// - https://doc.qt.io/archives/qt-5.15/qpalette.html#ColorRole-enum +struct QPaletteGroup { + /// A general foreground color. + window_text: Srgba, + /// The general button background color. + button: Srgba, + /// Lighter than [button] color, used mostly for 3D bevel and shadow effects. + light: Srgba, + /// Between [button] and [light], used mostly for 3D bevel and shadow effects. + midlight: Srgba, + /// Darker than [button], used mostly for 3D bevel and shadow effects. + dark: Srgba, + /// Between [button] and [dark], used mostly for 3D bevel and shadow effects. + mid: Srgba, + /// The foreground color used with [base]. + text: Srgba, + /// A text color that is very different from [window_text], and contrasts well with e.g. [dark]. + /// Typically used for text that needs to be drawn where [text] or [window_text] would give poor contrast, such as on pressed push buttons. + bright_text: Srgba, + /// A foreground color used with the [button] color. + button_text: Srgba, + /// Used mostly as the background color for text entry widgets, but can also be used for other painting - + /// such as the background of combobox drop down lists and toolbar handles. + base: Srgba, + /// A general background color. + window: Srgba, + /// A very dark color, used mostly for 3D bevel and shadow effects. + /// Opaque black by default. + shadow: Srgba, + /// A color to indicate a selected item or the current item. + highlight: Srgba, + /// A text color that contrasts with [highlight]. + highlighted_text: Srgba, + /// A text color used for unvisited hyperlinks. + link: Srgba, + /// A text color used for already visited hyperlinks. + link_visited: Srgba, + /// Used as the alternate background color in views with alternating row colors. + alternate_base: Srgba, + /// No role; this special role is often used to indicate that a role has not been assigned. + no_role: Srgba, + /// Used as the background color for QToolTip and QWhatsThis. + /// Tool tips use the inactive color group of QPalette, because tool tips are not active windows. + tool_tip_base: Srgba, + /// Used as the foreground color for QToolTip and QWhatsThis. + /// Tool tips use the inactive color group of QPalette, because tool tips are not active windows. + tool_tip_text: Srgba, + /// Used as the placeholder color for various text input widgets. + placeholder_text: Srgba, + // /// [accent] only exists since Qt 6.6. Including it here breaks qt5ct. + // /// When omitted, it defaults to [highlight]. + // accent: Srgba, +} + +impl QPaletteGroup { + /// Returns a comma-separated list of the colors as hex codes. + /// E.g. `#ff000000, #ffdcdcdc, ...` + /// + /// Any transparent colors are flattened with [base] to avoid issues with + /// the Fusion style. + fn as_list(&self) -> String { + let colors = vec![ + to_argb_hex(self.window_text.over(self.base)), + to_argb_hex(self.button.over(self.base)), + to_argb_hex(self.light.over(self.base)), + to_argb_hex(self.midlight.over(self.base)), + to_argb_hex(self.dark.over(self.base)), + to_argb_hex(self.mid.over(self.base)), + to_argb_hex(self.text.over(self.base)), + to_argb_hex(self.bright_text.over(self.base)), + to_argb_hex(self.button_text.over(self.base)), + to_argb_hex(self.base.over(self.base)), + to_argb_hex(self.window.over(self.base)), + to_argb_hex(self.shadow.over(self.base)), + to_argb_hex(self.highlight.over(self.base)), + to_argb_hex(self.highlighted_text.over(self.base)), + to_argb_hex(self.link.over(self.base)), + to_argb_hex(self.link_visited.over(self.base)), + to_argb_hex(self.alternate_base.over(self.base)), + to_argb_hex(self.no_role.over(self.base)), + to_argb_hex(self.tool_tip_base.over(self.base)), + to_argb_hex(self.tool_tip_text.over(self.base)), + to_argb_hex(self.placeholder_text.over(self.base)), + ]; + colors.join(", ") + } +} + +/// Converts a color to a hex string in the format `#AARRGGBB`. +/// Do not use [to_hex] since that uses the format `RRGGBBAA`. +fn to_argb_hex(c: Srgba) -> String { + let c_u8: Rgba = c.into_format(); + format!( + "#{:02x}{:02x}{:02x}{:02x}", + c_u8.alpha, c_u8.red, c_u8.green, c_u8.blue + ) } diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index 9bca3d18..86f7ac13 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -14,10 +14,11 @@ impl Theme { /// Produces a color scheme ini file for Qt. /// /// Some high-level documentation for this file can be found at: - /// https://web.archive.org/web/20250402234329/https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/colors/ + /// - https://api.kde.org/kcolorscheme.html + /// - https://web.archive.org/web/20250402234329/https://docs.kde.org/stable5/en/plasma-workspace/kcontrol/colors/ #[must_use] #[cold] - pub fn as_qt(&self) -> String { + pub fn as_kcolorscheme(&self) -> String { // Usually, disabled elements will have strongly reduced contrast and are often notably darker or lighter let disabled_color_effects = IniColorEffects { color: self.button.disabled, @@ -41,7 +42,7 @@ impl Theme { let bg = self.background.base; // the background container - let view_colors = IniColors { + let window_colors = IniColors { background_alternate: bg.mix(self.accent.base, 0.05), background_normal: bg, decoration_focus: self.accent_text_color(), @@ -56,16 +57,17 @@ impl Theme { foreground_visited: self.accent_text_color(), }; // components inside the background container - let window_colors = IniColors { + let view_colors = IniColors { background_alternate: self.background.component.base.mix(self.accent.base, 0.05), background_normal: self.background.component.base, - ..view_colors + ..window_colors }; // selected text and items let selection_colors = { - let selected = self.background.component.selected; - let selected_text = self.background.component.selected_text; + // selection colors are swapped to fix menu bar contrast + let selected = self.background.component.selected_text; + let selected_text = self.background.component.selected; IniColors { background_alternate: selected.mix(bg, 0.5), background_normal: selected, @@ -116,10 +118,10 @@ impl Theme { }; // headers in cosmic don't have a background - let header_colors = &view_colors; - let header_colors_inactive = &view_colors; + let header_colors = &window_colors; + let header_colors_inactive = &window_colors; // tool tips, "What's This" tips, and similar elements - let tooltip_colors = &window_colors; + let tooltip_colors = &view_colors; let general_color_scheme = if self.is_dark { "CosmicDark" @@ -198,7 +200,7 @@ widgetStyle=qt6ct-style format_ini_colors(&tooltip_colors, bg), format_ini_colors(&view_colors, bg), format_ini_colors(&window_colors, bg), - format_ini_wm_colors(&view_colors, self.is_dark), + format_ini_wm_colors(&window_colors, self.is_dark), ) } @@ -212,14 +214,14 @@ widgetStyle=qt6ct-style /// Returns an `OutputError` if there is an error writing the colors file. #[cold] pub fn write_qt(&self) -> Result<(), OutputError> { - let colors = self.as_qt(); - let file_path = Self::get_qt_colors_path(self.is_dark)?; + let kcolorscheme = self.as_kcolorscheme(); + let file_path = Self::get_kcolorscheme_path(self.is_dark)?; let tmp_file_path = file_path.with_extension("colors.new"); // Write to tmp_file_path first, then move it to file_path let mut tmp_file = File::create(&tmp_file_path).map_err(OutputError::Io)?; let res = tmp_file - .write_all(colors.as_bytes()) + .write_all(kcolorscheme.as_bytes()) .and_then(|_| tmp_file.flush()) .and_then(|_| std::fs::rename(&tmp_file_path, file_path)); if let Err(e) = res { @@ -245,7 +247,7 @@ widgetStyle=qt6ct-style let kdeglobals_file = config_dir.join("kdeglobals"); let mut kdeglobals_ini = Self::read_ini(&kdeglobals_file)?; - let src_file = Self::get_qt_colors_path(is_dark)?; + let src_file = Self::get_kcolorscheme_path(is_dark)?; let src_ini = Self::read_ini(&src_file)?; Self::backup_non_cosmic_kdeglobals(&kdeglobals_ini, &kdeglobals_file) @@ -288,7 +290,7 @@ widgetStyle=qt6ct-style } let is_dark = false; // doesn't matter since we're only reading keys - let src_file = Self::get_qt_colors_path(is_dark)?; + let src_file = Self::get_kcolorscheme_path(is_dark)?; let src_ini = Self::read_ini(&src_file)?; for (section, key_value) in src_ini.get_map_ref() { @@ -303,8 +305,8 @@ widgetStyle=qt6ct-style Ok(()) } - /// Gets a path like `~/.config/color-schemes/CosmicDark.colors` - pub fn get_qt_colors_path(is_dark: bool) -> Result { + /// Gets a path like `~/.local/share/color-schemes/CosmicDark.colors` + fn get_kcolorscheme_path(is_dark: bool) -> Result { let Some(mut data_dir) = dirs::data_dir() else { return Err(OutputError::MissingDataDir); }; From 1433b89e407a2f2676ceec1090224b7e27f155f7 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 31 Mar 2026 14:58:46 -0400 Subject: [PATCH 060/116] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 1fdd24ab..be453292 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 1fdd24ab995a4d65ba83cc1957e992b57cc37fcd +Subproject commit be453292c69f3bf103b93ea27e38f57386450085 From 4541c6a275bd90f14b91bbce875212825702c9dd Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 31 Mar 2026 15:09:07 -0400 Subject: [PATCH 061/116] fix: example deps --- examples/applet/Cargo.toml | 2 +- examples/application/Cargo.toml | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/examples/applet/Cargo.toml b/examples/applet/Cargo.toml index f97bff44..13eff684 100644 --- a/examples/applet/Cargo.toml +++ b/examples/applet/Cargo.toml @@ -13,6 +13,6 @@ env_logger = "0.10.2" log = "0.4.29" [dependencies.libcosmic] -git = "https://github.com/pop-os/libcosmic" +path = "../../" default-features = false features = ["applet-token"] diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index c842c79f..bc037ec0 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -11,7 +11,7 @@ wayland = ["libcosmic/wayland"] env_logger = "0.11" [dependencies.libcosmic] -git = "https://github.com/pop-os/libcosmic" +path = "../../" features = [ "debug", "winit", From d631f9d6d789304a1f01806cf2c1a0c5e93df58a Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Wed, 1 Apr 2026 17:21:27 -0400 Subject: [PATCH 062/116] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index be453292..e4da5002 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit be453292c69f3bf103b93ea27e38f57386450085 +Subproject commit e4da5002ae4e9d68cc4ac777ed77b4a225659440 From 8b52592f2d6c0915d11a96af25915698e351800e Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 1 Apr 2026 17:11:42 +0100 Subject: [PATCH 063/116] ci: test cosmic-theme --- .github/workflows/ci.yml | 25 +++++++++++++------------ 1 file changed, 13 insertions(+), 12 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a822642e..7897eb01 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -33,16 +33,17 @@ jobs: strategy: fail-fast: false matrix: - features: - - "" # for cosmic-comp, don't remove! - - 'winit_debug' - - 'winit_tokio' - - winit - - winit_wgpu - - wayland - - applet - - desktop,smol - - desktop,tokio + test_args: + - --no-default-features --features "" # for cosmic-comp, don't remove! + - --no-default-features --features "winit_debug" + - --no-default-features --features "winit_tokio" + - --no-default-features --features "winit" + - --no-default-features --features "winit_wgpu" + - --no-default-features --features "wayland" + - --no-default-features --features "applet" + - --no-default-features --features "desktop,smol" + - --no-default-features --features "desktop,tokio" + - -p cosmic-theme runs-on: ubuntu-22.04 steps: - name: Checkout sources @@ -66,7 +67,7 @@ jobs: - name: Rust toolchain uses: dtolnay/rust-toolchain@stable - name: Test features - run: cargo test --no-default-features --features "${{ matrix.features }}" -- --test-threads=1 + run: cargo test ${{ matrix.test_args }} -- --test-threads=1 env: RUST_BACKTRACE: full @@ -103,7 +104,7 @@ jobs: run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Rust toolchain uses: dtolnay/rust-toolchain@stable - - name: Test example + - name: Check example run: cargo check -p "${{ matrix.examples }}" env: RUST_BACKTRACE: full From 672f9047a2666eee371ae11082800aafd1d51dd8 Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 1 Apr 2026 17:02:14 +0100 Subject: [PATCH 064/116] test: use almost::zero instead of almost::equal as per documentation "Do not use this to compare a value with a constant zero. Instead, for this you should use almost::zero." --- cosmic-theme/src/steps.rs | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs index 143cf532..00a002c9 100644 --- a/cosmic-theme/src/steps.rs +++ b/cosmic-theme/src/steps.rs @@ -145,7 +145,6 @@ pub fn is_valid_srgb(c: Srgba) -> bool { #[cfg(test)] mod tests { - use almost::equal; use palette::{OklabHue, Srgba}; use super::{is_valid_srgb, oklch_to_srgba_nearest_chroma}; @@ -173,16 +172,16 @@ mod tests { fn test_conversion_boundaries() { let c1 = palette::Oklcha::new(0.0, 0.288, OklabHue::from_degrees(0.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1); - equal(srgb.red, 0.0); - equal(srgb.blue, 0.0); - equal(srgb.green, 0.0); + almost::zero(srgb.red); + almost::zero(srgb.blue); + almost::zero(srgb.green); let c1 = palette::Oklcha::new(1.0, 0.288, OklabHue::from_degrees(0.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1); - equal(srgb.red, 1.0); - equal(srgb.blue, 1.0); - equal(srgb.green, 1.0); + almost::equal(srgb.red, 1.0); + almost::equal(srgb.blue, 1.0); + almost::equal(srgb.green, 1.0); } #[test] From e86304cf3ffaf9ea4a6f60c87898d993b4196942 Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 1 Apr 2026 16:59:38 +0100 Subject: [PATCH 065/116] ref: use assert_eq not assert This way, the test log can show the expected and actual result if it fails. thread 'steps::tests::test_conversion_fallback_colors' (61338) panicked at cosmic-theme/src/steps.rs:213:9: assertion `left == right` failed left: 102 right: 103 note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace --- cosmic-theme/src/steps.rs | 36 ++++++++++++++++++------------------ 1 file changed, 18 insertions(+), 18 deletions(-) diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs index 00a002c9..4c3ab3d7 100644 --- a/cosmic-theme/src/steps.rs +++ b/cosmic-theme/src/steps.rs @@ -188,41 +188,41 @@ mod tests { fn test_conversion_colors() { let c1 = palette::Oklcha::new(0.4608, 0.11111, OklabHue::new(57.31), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert!(srgb.red == 133); - assert!(srgb.green == 69); - assert!(srgb.blue == 0); + assert_eq!(srgb.red, 133); + assert_eq!(srgb.green, 69); + assert_eq!(srgb.blue, 0); let c1 = palette::Oklcha::new(0.30, 0.08, OklabHue::new(35.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert!(srgb.red == 78); - assert!(srgb.green == 27); - assert!(srgb.blue == 15); + assert_eq!(srgb.red, 78); + assert_eq!(srgb.green, 27); + assert_eq!(srgb.blue, 15); let c1 = palette::Oklcha::new(0.757, 0.146, OklabHue::new(301.2), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert!(srgb.red == 192); - assert!(srgb.green == 153); - assert!(srgb.blue == 253); + assert_eq!(srgb.red, 192); + assert_eq!(srgb.green, 153); + assert_eq!(srgb.blue, 253); } #[test] fn test_conversion_fallback_colors() { let c1 = palette::Oklcha::new(0.70, 0.284, OklabHue::new(35.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert!(srgb.red == 255); - assert!(srgb.green == 103); - assert!(srgb.blue == 65); + assert_eq!(srgb.red, 255); + assert_eq!(srgb.green, 103); + assert_eq!(srgb.blue, 65); let c1 = palette::Oklcha::new(0.757, 0.239, OklabHue::new(301.2), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert!(srgb.red == 193); - assert!(srgb.green == 152); - assert!(srgb.blue == 255); + assert_eq!(srgb.red, 193); + assert_eq!(srgb.green, 152); + assert_eq!(srgb.blue, 255); let c1 = palette::Oklcha::new(0.163, 0.333, OklabHue::new(141.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); - assert!(srgb.red == 1); - assert!(srgb.green == 19); - assert!(srgb.blue == 0); + assert_eq!(srgb.red, 1); + assert_eq!(srgb.green, 19); + assert_eq!(srgb.blue, 0); } } From f734ccbbdeb68a5844465b2bb36a9052b54c288b Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 1 Apr 2026 17:17:19 +0100 Subject: [PATCH 066/116] test: fix expected color value --- cosmic-theme/src/steps.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs index 4c3ab3d7..6ebf1015 100644 --- a/cosmic-theme/src/steps.rs +++ b/cosmic-theme/src/steps.rs @@ -210,7 +210,7 @@ mod tests { let c1 = palette::Oklcha::new(0.70, 0.284, OklabHue::new(35.0), 1.0); let srgb = oklch_to_srgba_nearest_chroma(c1).into_format::(); assert_eq!(srgb.red, 255); - assert_eq!(srgb.green, 103); + assert_eq!(srgb.green, 102); assert_eq!(srgb.blue, 65); let c1 = palette::Oklcha::new(0.757, 0.239, OklabHue::new(301.2), 1.0); From 39e8300d90f7aab7a8b28e216d6631985d2de801 Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 1 Apr 2026 16:43:32 +0100 Subject: [PATCH 067/116] test: snapshots of kcolorscheme and qpalette AI disclosure: I asked GitHub Copilot (Claude Haiku 4.5) "What's the best way to add tests for my recently merged qt theming contributions?" It suggested the insta crate for golden testing the output strings as well as some unit tests. I implemented it myself. --- cosmic-theme/Cargo.toml | 7 + cosmic-theme/src/output/qt56ct_output.rs | 24 +++ cosmic-theme/src/output/qt_output.rs | 41 +++++ ..._output__tests__dark_default_qpalette.snap | 10 ++ ...output__tests__light_default_qpalette.snap | 10 ++ ...put__tests__dark_default_kcolorscheme.snap | 157 ++++++++++++++++++ ...ut__tests__light_default_kcolorscheme.snap | 157 ++++++++++++++++++ 7 files changed, 406 insertions(+) create mode 100644 cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__dark_default_qpalette.snap create mode 100644 cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__light_default_qpalette.snap create mode 100644 cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap create mode 100644 cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 1d64912a..7e408d8d 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -30,3 +30,10 @@ cosmic-config = { path = "../cosmic-config/", default-features = false, features configparser = "3.1.0" dirs.workspace = true thiserror = "2.0.18" + +[dev-dependencies] +insta = "1.47.2" + +[profile.dev.package] +insta.opt-level = 3 +similar.opt-level = 3 diff --git a/cosmic-theme/src/output/qt56ct_output.rs b/cosmic-theme/src/output/qt56ct_output.rs index eccfc846..43a45470 100644 --- a/cosmic-theme/src/output/qt56ct_output.rs +++ b/cosmic-theme/src/output/qt56ct_output.rs @@ -389,3 +389,27 @@ fn to_argb_hex(c: Srgba) -> String { c_u8.alpha, c_u8.red, c_u8.green, c_u8.blue ) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_color_to_argb_hex() { + let color = Srgba::new(0x33, 0x55, 0x77, 0xff); + let argb = to_argb_hex(color.into()); + assert_eq!(argb, "#ff335577"); + } + + #[test] + fn test_light_default_qpalette() { + let light_default_qpalette = Theme::light_default().as_qpalette(); + insta::assert_snapshot!(light_default_qpalette); + } + + #[test] + fn test_dark_default_qpalette() { + let dark_default_qpalette = Theme::dark_default().as_qpalette(); + insta::assert_snapshot!(dark_default_qpalette); + } +} diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index 86f7ac13..cd66e865 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -522,3 +522,44 @@ impl ColorEffect { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_opaque_color_to_rgb() { + let color = Srgba::new(30.0 / 255.0, 50.0 / 255.0, 70.0 / 255.0, 1.0); + let bg = Srgba::new(1.0, 1.0, 1.0, 1.0); + let result = to_rgb(color, bg); + assert_eq!(result, "30,50,70"); + } + + #[test] + fn test_transparent_color_to_rgb() { + let color = Srgba::new(0.0, 0.0, 0.0, 0.0); + let bg = Srgba::new(1.0, 1.0, 1.0, 1.0); + let result = to_rgb(color, bg); + assert_eq!(result, "255,255,255"); + } + + #[test] + fn test_translucent_color_to_rgb() { + let color = Srgba::new(0.0, 0.0, 0.0, 0.9); + let bg = Srgba::new(1.0, 1.0, 1.0, 1.0); + let result = to_rgb(color, bg); + assert_eq!(result, "26,26,26"); + } + + #[test] + fn test_light_default_kcolorscheme() { + let light_default_kcolorscheme = Theme::light_default().as_kcolorscheme(); + insta::assert_snapshot!(light_default_kcolorscheme); + } + + #[test] + fn test_dark_default_kcolorscheme() { + let dark_default_kcolorscheme = Theme::dark_default().as_kcolorscheme(); + insta::assert_snapshot!(dark_default_kcolorscheme); + } +} diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__dark_default_qpalette.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__dark_default_qpalette.snap new file mode 100644 index 00000000..15746fd0 --- /dev/null +++ b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__dark_default_qpalette.snap @@ -0,0 +1,10 @@ +--- +source: cosmic-theme/src/output/qt56ct_output.rs +expression: dark_default_qpalette +--- +# GENERATED BY COSMIC + +[ColorScheme] +active_colors=#ffe7e7e7, #ff4a4a4a, #ff555555, #ff505050, #ff4f4f4f, #ff4d4d4d, #ffc0c0c0, #ffe7e7e7, #ffc0c0c0, #ff2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #ff434343, #ff63d0df, #ff5bb2be, #ff1f2425, #ff2e2e2e, #ff2e2e2e, #ffc0c0c0, #ff777777 +disabled_colors=#e6d3d3d3, #8f474747, #a9696969, #a4626262, #a95f5f5f, #a45d5d5d, #d2a1a1a1, #ffe7e7e7, #d2a1a1a1, #bf2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #bf3c3c3c, #bf30555a, #bf324f53, #ff1f2425, #bf2e2e2e, #bf2e2e2e, #d2a1a1a1, #bf909090 +inactive_colors=#ffc2c2c2, #ff4a4a4a, #ff555555, #ff505050, #ff4f4f4f, #ff4d4d4d, #ffa3a3a3, #ffe7e7e7, #ffc0c0c0, #ff2e2e2e, #ff1b1b1b, #ff1b1b1b, #ff63d0df, #ff3f3f3f, #ff63d0df, #ff5bb2be, #ff1f2425, #ff2e2e2e, #ff2e2e2e, #ffa3a3a3, #ff777777 diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__light_default_qpalette.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__light_default_qpalette.snap new file mode 100644 index 00000000..c79b2c55 --- /dev/null +++ b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt56ct_output__tests__light_default_qpalette.snap @@ -0,0 +1,10 @@ +--- +source: cosmic-theme/src/output/qt56ct_output.rs +expression: light_default_qpalette +--- +# GENERATED BY COSMIC + +[ColorScheme] +active_colors=#ff121212, #ffc3c3c3, #ffbababa, #ffbebebe, #ffb3b3b3, #ffbbbbbb, #ff272727, #ffd7d7d7, #ff272727, #fff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #fff6f6f6, #ff00525a, #ff317379, #ffccd0d1, #fff5f5f5, #fff5f5f5, #ff272727, #ff8e8e8e +disabled_colors=#e62b2b2b, #8fc9c9c9, #a99b9b9b, #a4a0a0a0, #a9929292, #a49b9b9b, #d2535353, #ffd7d7d7, #d2535353, #bff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #bff6f6f6, #bf526d70, #bf72888a, #ffccd0d1, #bff5f5f5, #bff5f5f5, #d2535353, #bf6c6c6c +inactive_colors=#ff3f3f3f, #ffc3c3c3, #ffbababa, #ffbebebe, #ffb3b3b3, #ffbbbbbb, #ff505050, #ffd7d7d7, #ff272727, #fff5f5f5, #ffd7d7d7, #ff121212, #ff00525a, #fff6f6f6, #ff00525a, #ff317379, #ffccd0d1, #fff5f5f5, #fff5f5f5, #ff505050, #ff8e8e8e diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap new file mode 100644 index 00000000..c50f95dc --- /dev/null +++ b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap @@ -0,0 +1,157 @@ +--- +source: cosmic-theme/src/output/qt_output.rs +expression: dark_default_kcolorscheme +--- +# GENERATED BY COSMIC + +[ColorEffects:Disabled] +Color=43,43,43 +ColorAmount=0 +ColorEffect=0 +ContrastAmount=0.65 +ContrastEffect=1 +IntensityAmount=0.1 +IntensityEffect=2 + +[ColorEffects:Inactive] +ChangeSelectionColor=false +Enable=false +Color=27,27,27 +ColorAmount=0.025 +ColorEffect=2 +ContrastAmount=0.1 +ContrastEffect=2 +IntensityAmount=0 +IntensityEffect=0 + +[Colors:Button] +BackgroundAlternate=99,208,223 +BackgroundNormal=60,60,60 +DecorationFocus=99,208,223 +DecorationHover=99,208,223 +ForegroundActive=99,208,223 +ForegroundInactive=211,211,211 +ForegroundLink=99,208,223 +ForegroundNegative=255,160,154 +ForegroundNeutral=255,163,125 +ForegroundNormal=231,231,231 +ForegroundPositive=94,219,140 +ForegroundVisited=99,208,223 + +[Colors:Complementary] +BackgroundAlternate=99,208,223 +BackgroundNormal=27,27,27 +DecorationFocus=99,208,223 +DecorationHover=99,208,223 +ForegroundActive=99,208,223 +ForegroundInactive=211,211,211 +ForegroundLink=99,208,223 +ForegroundNegative=255,160,154 +ForegroundNeutral=255,163,125 +ForegroundNormal=231,231,231 +ForegroundPositive=94,219,140 +ForegroundVisited=99,208,223 + +[Colors:Header] +BackgroundAlternate=31,36,37 +BackgroundNormal=27,27,27 +DecorationFocus=99,208,223 +DecorationHover=99,208,223 +ForegroundActive=99,208,223 +ForegroundInactive=211,211,211 +ForegroundLink=99,208,223 +ForegroundNegative=255,160,154 +ForegroundNeutral=255,163,125 +ForegroundNormal=231,231,231 +ForegroundPositive=94,219,140 +ForegroundVisited=99,208,223 + +[Colors:Header][Inactive] +BackgroundAlternate=31,36,37 +BackgroundNormal=27,27,27 +DecorationFocus=99,208,223 +DecorationHover=99,208,223 +ForegroundActive=99,208,223 +ForegroundInactive=211,211,211 +ForegroundLink=99,208,223 +ForegroundNegative=255,160,154 +ForegroundNeutral=255,163,125 +ForegroundNormal=231,231,231 +ForegroundPositive=94,219,140 +ForegroundVisited=99,208,223 + +[Colors:Selection] +BackgroundAlternate=63,118,125 +BackgroundNormal=99,208,223 +DecorationFocus=99,208,223 +DecorationHover=99,208,223 +ForegroundActive=67,67,67 +ForegroundInactive=83,138,145 +ForegroundLink=27,27,27 +ForegroundNegative=255,160,154 +ForegroundNeutral=255,163,125 +ForegroundNormal=67,67,67 +ForegroundPositive=94,219,140 +ForegroundVisited=99,208,223 + +[Colors:Tooltip] +BackgroundAlternate=49,55,55 +BackgroundNormal=46,46,46 +DecorationFocus=99,208,223 +DecorationHover=99,208,223 +ForegroundActive=99,208,223 +ForegroundInactive=211,211,211 +ForegroundLink=99,208,223 +ForegroundNegative=255,160,154 +ForegroundNeutral=255,163,125 +ForegroundNormal=231,231,231 +ForegroundPositive=94,219,140 +ForegroundVisited=99,208,223 + +[Colors:View] +BackgroundAlternate=49,55,55 +BackgroundNormal=46,46,46 +DecorationFocus=99,208,223 +DecorationHover=99,208,223 +ForegroundActive=99,208,223 +ForegroundInactive=211,211,211 +ForegroundLink=99,208,223 +ForegroundNegative=255,160,154 +ForegroundNeutral=255,163,125 +ForegroundNormal=231,231,231 +ForegroundPositive=94,219,140 +ForegroundVisited=99,208,223 + +[Colors:Window] +BackgroundAlternate=31,36,37 +BackgroundNormal=27,27,27 +DecorationFocus=99,208,223 +DecorationHover=99,208,223 +ForegroundActive=99,208,223 +ForegroundInactive=211,211,211 +ForegroundLink=99,208,223 +ForegroundNegative=255,160,154 +ForegroundNeutral=255,163,125 +ForegroundNormal=231,231,231 +ForegroundPositive=94,219,140 +ForegroundVisited=99,208,223 + +[General] +ColorScheme=CosmicDark +Name=COSMIC Dark +shadeSortColumn=true + +[Icons] +Theme=breeze-dark + +[KDE] +contrast=4 +widgetStyle=qt6ct-style + +[WM] +activeBackground=27,27,27 +activeBlend=99,208,223 +activeForeground=99,208,223 +inactiveBackground=27,27,27 +inactiveBlend=99,208,223 +inactiveForeground=99,208,223 diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap new file mode 100644 index 00000000..40aacf01 --- /dev/null +++ b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap @@ -0,0 +1,157 @@ +--- +source: cosmic-theme/src/output/qt_output.rs +expression: light_default_kcolorscheme +--- +# GENERATED BY COSMIC + +[ColorEffects:Disabled] +Color=194,194,194 +ColorAmount=0 +ColorEffect=0 +ContrastAmount=0.65 +ContrastEffect=1 +IntensityAmount=0.1 +IntensityEffect=2 + +[ColorEffects:Inactive] +ChangeSelectionColor=false +Enable=false +Color=215,215,215 +ColorAmount=0.025 +ColorEffect=2 +ContrastAmount=0.1 +ContrastEffect=2 +IntensityAmount=0 +IntensityEffect=0 + +[Colors:Button] +BackgroundAlternate=0,82,90 +BackgroundNormal=173,173,173 +DecorationFocus=0,82,90 +DecorationHover=0,82,90 +ForegroundActive=0,82,90 +ForegroundInactive=38,38,38 +ForegroundLink=0,82,90 +ForegroundNegative=137,4,24 +ForegroundNeutral=121,44,0 +ForegroundNormal=18,18,18 +ForegroundPositive=0,87,44 +ForegroundVisited=0,82,90 + +[Colors:Complementary] +BackgroundAlternate=24,85,41 +BackgroundNormal=203,221,173 +DecorationFocus=24,85,41 +DecorationHover=24,85,41 +ForegroundActive=24,85,41 +ForegroundInactive=34,36,31 +ForegroundLink=24,85,41 +ForegroundNegative=120,41,46 +ForegroundNeutral=83,72,0 +ForegroundNormal=16,16,16 +ForegroundPositive=24,85,41 +ForegroundVisited=24,85,41 + +[Colors:Header] +BackgroundAlternate=204,208,209 +BackgroundNormal=215,215,215 +DecorationFocus=0,82,90 +DecorationHover=0,82,90 +ForegroundActive=0,82,90 +ForegroundInactive=38,38,38 +ForegroundLink=0,82,90 +ForegroundNegative=137,4,24 +ForegroundNeutral=121,44,0 +ForegroundNormal=18,18,18 +ForegroundPositive=0,87,44 +ForegroundVisited=0,82,90 + +[Colors:Header][Inactive] +BackgroundAlternate=204,208,209 +BackgroundNormal=215,215,215 +DecorationFocus=0,82,90 +DecorationHover=0,82,90 +ForegroundActive=0,82,90 +ForegroundInactive=38,38,38 +ForegroundLink=0,82,90 +ForegroundNegative=137,4,24 +ForegroundNeutral=121,44,0 +ForegroundNormal=18,18,18 +ForegroundPositive=0,87,44 +ForegroundVisited=0,82,90 + +[Colors:Selection] +BackgroundAlternate=108,149,152 +BackgroundNormal=0,82,90 +DecorationFocus=0,82,90 +DecorationHover=0,82,90 +ForegroundActive=246,246,246 +ForegroundInactive=123,164,168 +ForegroundLink=215,215,215 +ForegroundNegative=137,4,24 +ForegroundNeutral=121,44,0 +ForegroundNormal=246,246,246 +ForegroundPositive=0,87,44 +ForegroundVisited=0,82,90 + +[Colors:Tooltip] +BackgroundAlternate=233,237,237 +BackgroundNormal=245,245,245 +DecorationFocus=0,82,90 +DecorationHover=0,82,90 +ForegroundActive=0,82,90 +ForegroundInactive=38,38,38 +ForegroundLink=0,82,90 +ForegroundNegative=137,4,24 +ForegroundNeutral=121,44,0 +ForegroundNormal=18,18,18 +ForegroundPositive=0,87,44 +ForegroundVisited=0,82,90 + +[Colors:View] +BackgroundAlternate=233,237,237 +BackgroundNormal=245,245,245 +DecorationFocus=0,82,90 +DecorationHover=0,82,90 +ForegroundActive=0,82,90 +ForegroundInactive=38,38,38 +ForegroundLink=0,82,90 +ForegroundNegative=137,4,24 +ForegroundNeutral=121,44,0 +ForegroundNormal=18,18,18 +ForegroundPositive=0,87,44 +ForegroundVisited=0,82,90 + +[Colors:Window] +BackgroundAlternate=204,208,209 +BackgroundNormal=215,215,215 +DecorationFocus=0,82,90 +DecorationHover=0,82,90 +ForegroundActive=0,82,90 +ForegroundInactive=38,38,38 +ForegroundLink=0,82,90 +ForegroundNegative=137,4,24 +ForegroundNeutral=121,44,0 +ForegroundNormal=18,18,18 +ForegroundPositive=0,87,44 +ForegroundVisited=0,82,90 + +[General] +ColorScheme=CosmicLight +Name=COSMIC Light +shadeSortColumn=true + +[Icons] +Theme=breeze + +[KDE] +contrast=4 +widgetStyle=qt6ct-style + +[WM] +activeBackground=215,215,215 +activeBlend=215,215,215 +activeForeground=0,82,90 +inactiveBackground=215,215,215 +inactiveBlend=215,215,215 +inactiveForeground=0,82,90 From 9a72fe6c2da372ea940e0669ea713b56e7311133 Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 1 Apr 2026 17:49:54 +0100 Subject: [PATCH 068/116] fix: complementary should be dark not light --- cosmic-theme/src/output/qt_output.rs | 2 +- ...ut__tests__light_default_kcolorscheme.snap | 24 +++++++++---------- 2 files changed, 13 insertions(+), 13 deletions(-) diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index cd66e865..5b369719 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -95,7 +95,7 @@ impl Theme { let dark = if self.is_dark { self.clone() } else { - Theme::light_config() + Theme::dark_config() .ok() .as_ref() .and_then(|conf| Theme::get_entry(conf).ok()) diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap index 40aacf01..12c511fa 100644 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap +++ b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap @@ -39,18 +39,18 @@ ForegroundPositive=0,87,44 ForegroundVisited=0,82,90 [Colors:Complementary] -BackgroundAlternate=24,85,41 -BackgroundNormal=203,221,173 -DecorationFocus=24,85,41 -DecorationHover=24,85,41 -ForegroundActive=24,85,41 -ForegroundInactive=34,36,31 -ForegroundLink=24,85,41 -ForegroundNegative=120,41,46 -ForegroundNeutral=83,72,0 -ForegroundNormal=16,16,16 -ForegroundPositive=24,85,41 -ForegroundVisited=24,85,41 +BackgroundAlternate=129,196,88 +BackgroundNormal=12,17,6 +DecorationFocus=129,196,88 +DecorationHover=129,196,88 +ForegroundActive=129,196,88 +ForegroundInactive=191,198,186 +ForegroundLink=129,196,88 +ForegroundNegative=253,161,160 +ForegroundNeutral=247,224,98 +ForegroundNormal=211,218,206 +ForegroundPositive=146,207,156 +ForegroundVisited=129,196,88 [Colors:Header] BackgroundAlternate=204,208,209 From c33455e9ad7e1d748f755766b6e5688c90f5f602 Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 1 Apr 2026 18:01:16 +0100 Subject: [PATCH 069/116] test: use default dark theme, not real system theme --- cosmic-theme/src/output/qt_output.rs | 3 +++ ...ut__tests__light_default_kcolorscheme.snap | 24 +++++++++---------- 2 files changed, 15 insertions(+), 12 deletions(-) diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index 5b369719..d42d553b 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -94,6 +94,9 @@ impl Theme { let complementary_colors = { let dark = if self.is_dark { self.clone() + } else if cfg!(test) { + // For reproducible results in tests, use the default dark theme + Theme::dark_default() } else { Theme::dark_config() .ok() diff --git a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap index 12c511fa..ae2bcb66 100644 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap +++ b/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap @@ -39,18 +39,18 @@ ForegroundPositive=0,87,44 ForegroundVisited=0,82,90 [Colors:Complementary] -BackgroundAlternate=129,196,88 -BackgroundNormal=12,17,6 -DecorationFocus=129,196,88 -DecorationHover=129,196,88 -ForegroundActive=129,196,88 -ForegroundInactive=191,198,186 -ForegroundLink=129,196,88 -ForegroundNegative=253,161,160 -ForegroundNeutral=247,224,98 -ForegroundNormal=211,218,206 -ForegroundPositive=146,207,156 -ForegroundVisited=129,196,88 +BackgroundAlternate=99,208,223 +BackgroundNormal=27,27,27 +DecorationFocus=99,208,223 +DecorationHover=99,208,223 +ForegroundActive=99,208,223 +ForegroundInactive=211,211,211 +ForegroundLink=99,208,223 +ForegroundNegative=255,160,154 +ForegroundNeutral=255,163,125 +ForegroundNormal=231,231,231 +ForegroundPositive=94,219,140 +ForegroundVisited=99,208,223 [Colors:Header] BackgroundAlternate=204,208,209 From 2299fba69b0116d7dc970895a78f24ebe40746a8 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 1 Apr 2026 11:44:58 -0600 Subject: [PATCH 070/116] fix(text_input): RTL text cursor and highlight fixes --- src/widget/text_input/cursor.rs | 41 ++- src/widget/text_input/input.rs | 448 +++++++++++++++++++++----------- src/widget/text_input/value.rs | 33 ++- 3 files changed, 365 insertions(+), 157 deletions(-) diff --git a/src/widget/text_input/cursor.rs b/src/widget/text_input/cursor.rs index 42f52da1..3ffb535c 100644 --- a/src/widget/text_input/cursor.rs +++ b/src/widget/text_input/cursor.rs @@ -3,16 +3,19 @@ // SPDX-License-Identifier: MIT //! Track the cursor of a text input. +use iced_core::text::Affinity; + use super::value::Value; /// The cursor of a text input. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub struct Cursor { state: State, + affinity: Affinity, } /// The state of a [`Cursor`]. -#[derive(Debug, Copy, Clone)] +#[derive(Debug, Copy, Clone, PartialEq, Eq)] pub enum State { /// Cursor without a selection Index(usize), @@ -31,6 +34,7 @@ impl Default for Cursor { fn default() -> Self { Self { state: State::Index(0), + affinity: Affinity::Before, } } } @@ -193,4 +197,37 @@ impl Cursor { State::Selection { start, end } => start.max(end), } } + + /// Returns the current cursor [`Affinity`]. + #[must_use] + pub fn affinity(&self) -> Affinity { + self.affinity + } + + /// Sets the cursor [`Affinity`]. + pub fn set_affinity(&mut self, affinity: Affinity) { + self.affinity = affinity; + } + + /// Moves the cursor in a visual direction, accounting for RTL text. + /// + /// `forward` = `true` is visually rightward. + pub fn move_visual(&mut self, forward: bool, by_words: bool, rtl: bool, value: &Value) { + match (forward ^ rtl, by_words) { + (true, false) => self.move_right(value), + (true, true) => self.move_right_by_words(value), + (false, false) => self.move_left(value), + (false, true) => self.move_left_by_words(value), + } + } + + /// Extends the selection in a visual direction, accounting for RTL text. + pub fn select_visual(&mut self, forward: bool, by_words: bool, rtl: bool, value: &Value) { + match (forward ^ rtl, by_words) { + (true, false) => self.select_right(value), + (true, true) => self.select_right_by_words(value), + (false, false) => self.select_left(value), + (false, true) => self.select_left_by_words(value), + } + } } diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 8f6fb329..ffb08c8b 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -937,6 +937,18 @@ where self.drag_threshold, self.always_active, ); + + let state = tree.state.downcast_mut::(); + let value = if self.is_secure { + self.value.secure() + } else { + self.value.clone() + }; + state.scroll_offset = offset( + text_layout.children().next().unwrap().bounds(), + &value, + state, + ); } #[inline] @@ -1435,7 +1447,17 @@ pub fn update<'a, Message: Clone + 'static>( return; } - let target = cursor_position.x - text_layout.bounds().x; + let target = { + let text_bounds = text_layout.bounds(); + + let alignment_offset = alignment_offset( + text_bounds.width, + state.value.raw().min_width(), + effective_alignment(state.value.raw()), + ); + + cursor_position.x - text_bounds.x - alignment_offset + }; let click = mouse::Click::new(cursor_position, mouse::Button::Left, state.last_click); @@ -1454,17 +1476,30 @@ pub fn update<'a, Message: Clone + 'static>( state.value.raw(), text_layout.bounds(), left, + value, + state.cursor.affinity(), + state.scroll_offset, ); let (right_position, _right_offset) = measure_cursor_and_scroll_offset( state.value.raw(), text_layout.bounds(), right, + value, + state.cursor.affinity(), + state.scroll_offset, ); - let width = right_position - left_position; + let selection_start = left_position.min(right_position); + let width = (right_position - left_position).abs(); + let alignment_offset = alignment_offset( + text_layout.bounds().width, + state.value.raw().min_width(), + effective_alignment(state.value.raw()), + ); let selection_bounds = Rectangle { - x: text_layout.bounds().x + left_position, + x: text_layout.bounds().x + alignment_offset + selection_start + - state.scroll_offset, y: text_layout.bounds().y, width, height: text_layout.bounds().height, @@ -1492,10 +1527,11 @@ pub fn update<'a, Message: Clone + 'static>( if is_secure { state.cursor.select_all(value); } else { - let position = + let (position, affinity) = find_cursor_position(text_layout.bounds(), value, state, target) - .unwrap_or(0); + .unwrap_or((0, text::Affinity::Before)); + state.cursor.set_affinity(affinity); state.cursor.select_range( value.previous_start_of_word(position), value.next_end_of_word(position), @@ -1561,7 +1597,17 @@ pub fn update<'a, Message: Clone + 'static>( // clear selection and place cursor at click position update_cache(state, value); if let Some(position) = cursor.position_over(layout.bounds()) { - let target = position.x - text_layout.bounds().x; + let target = { + let text_bounds = text_layout.bounds(); + + let alignment_offset = alignment_offset( + text_bounds.width, + state.value.raw().min_width(), + effective_alignment(state.value.raw()), + ); + + position.x - text_bounds.x - alignment_offset + }; state.setting_selection(value, text_layout.bounds(), target); } } @@ -1576,12 +1622,24 @@ pub fn update<'a, Message: Clone + 'static>( let state = state(); if matches!(state.dragging_state, Some(DraggingState::Selection)) { - let target = position.x - text_layout.bounds().x; + let target = { + let text_bounds = text_layout.bounds(); + + let alignment_offset = alignment_offset( + text_bounds.width, + state.value.raw().min_width(), + effective_alignment(state.value.raw()), + ); + + position.x - text_bounds.x - alignment_offset + }; update_cache(state, value); - let position = - find_cursor_position(text_layout.bounds(), value, state, target).unwrap_or(0); + let (position, affinity) = + find_cursor_position(text_layout.bounds(), value, state, target) + .unwrap_or((0, text::Affinity::Before)); + state.cursor.set_affinity(affinity); state .cursor .select_range(state.cursor.start(value), position); @@ -1860,29 +1918,23 @@ pub fn update<'a, Message: Clone + 'static>( update_cache(state, &value); } keyboard::Key::Named(keyboard::key::Named::ArrowLeft) => { - if platform::is_jump_modifier_pressed(modifiers) && !is_secure { - if modifiers.shift() { - state.cursor.select_left_by_words(value); - } else { - state.cursor.move_left_by_words(value); - } - } else if modifiers.shift() { - state.cursor.select_left(value); + let rtl = state.value.raw().is_rtl(0).unwrap_or(false); + let by_words = platform::is_jump_modifier_pressed(modifiers) && !is_secure; + + if modifiers.shift() { + state.cursor.select_visual(false, by_words, rtl, value); } else { - state.cursor.move_left(value); + state.cursor.move_visual(false, by_words, rtl, value); } } keyboard::Key::Named(keyboard::key::Named::ArrowRight) => { - if platform::is_jump_modifier_pressed(modifiers) && !is_secure { - if modifiers.shift() { - state.cursor.select_right_by_words(value); - } else { - state.cursor.move_right_by_words(value); - } - } else if modifiers.shift() { - state.cursor.select_right(value); + let rtl = state.value.raw().is_rtl(0).unwrap_or(false); + let by_words = platform::is_jump_modifier_pressed(modifiers) && !is_secure; + + if modifiers.shift() { + state.cursor.select_visual(true, by_words, rtl, value); } else { - state.cursor.move_right(value); + state.cursor.move_visual(true, by_words, rtl, value); } } keyboard::Key::Named(keyboard::key::Named::Home) => { @@ -2016,18 +2068,27 @@ pub fn update<'a, Message: Clone + 'static>( } } if accepted { - let target = *x as f32 - text_layout.bounds().x; + let target = { + let text_bounds = text_layout.bounds(); + + let alignment_offset = alignment_offset( + text_bounds.width, + state.value.raw().min_width(), + effective_alignment(state.value.raw()), + ); + + *x as f32 - text_bounds.x - alignment_offset + }; state.dnd_offer = DndOfferState::HandlingOffer(mime_types.clone(), DndAction::empty()); // existing logic for setting the selection - let position = if target > 0.0 { - update_cache(state, value); + update_cache(state, value); + let (position, affinity) = find_cursor_position(text_layout.bounds(), value, state, target) - } else { - None - }; + .unwrap_or((0, text::Affinity::Before)); - state.cursor.move_to(position.unwrap_or(0)); + state.cursor.set_affinity(affinity); + state.cursor.move_to(position); shell.capture_event(); return; } @@ -2038,16 +2099,25 @@ pub fn update<'a, Message: Clone + 'static>( { let state = state(); - let target = *x as f32 - text_layout.bounds().x; - // existing logic for setting the selection - let position = if target > 0.0 { - update_cache(state, value); - find_cursor_position(text_layout.bounds(), value, state, target) - } else { - None - }; + let target = { + let text_bounds = text_layout.bounds(); - state.cursor.move_to(position.unwrap_or(0)); + let alignment_offset = alignment_offset( + text_bounds.width, + state.value.raw().min_width(), + effective_alignment(state.value.raw()), + ); + + *x as f32 - text_bounds.x - alignment_offset + }; + // existing logic for setting the selection + update_cache(state, value); + let (position, affinity) = + find_cursor_position(text_layout.bounds(), value, state, target) + .unwrap_or((0, text::Affinity::Before)); + + state.cursor.set_affinity(affinity); + state.cursor.move_to(position); shell.capture_event(); return; } @@ -2340,7 +2410,7 @@ pub fn draw<'a, Message>( let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None); #[cfg(not(all(feature = "wayland", target_os = "linux")))] let handling_dnd_offer = false; - let (cursor, offset) = if let Some(focus) = + let (cursors, offset, is_selecting) = if let Some(focus) = state.is_focused.filter(|f| f.focused).or_else(|| { let now = Instant::now(); handling_dnd_offer.then_some(Focus { @@ -2352,78 +2422,26 @@ pub fn draw<'a, Message>( }) { match state.cursor.state(value) { cursor::State::Index(position) => { - let (text_value_width, offset) = - measure_cursor_and_scroll_offset(state.value.raw(), text_bounds, position); + let (text_value_width, _) = measure_cursor_and_scroll_offset( + state.value.raw(), + text_bounds, + position, + value, + state.cursor.affinity(), + state.scroll_offset, + ); let is_cursor_visible = handling_dnd_offer || ((focus.now - focus.updated_at).as_millis() / CURSOR_BLINK_INTERVAL_MILLIS) .is_multiple_of(2); - if is_cursor_visible { - if dnd_icon { - (None, 0.0) - } else { - ( - Some(( - renderer::Quad { - bounds: Rectangle { - x: text_bounds.x + text_value_width - offset - + if text_value_width < 0. { - actual_width - } else { - 0. - }, - y: text_bounds.y, - width: 1.0, - height: text_bounds.height, - }, - border: Border { - width: 0.0, - color: Color::TRANSPARENT, - radius: radius_0, - }, - shadow: Shadow { - offset: Vector::ZERO, - color: Color::TRANSPARENT, - blur_radius: 0.0, - }, - snap: true, - }, - text_color, - )), - offset, - ) - } - } else { - (None, offset) - } - } - cursor::State::Selection { start, end } => { - let left = start.min(end); - let right = end.max(start); - let value_paragraph = &state.value; - let (left_position, left_offset) = - measure_cursor_and_scroll_offset(value_paragraph.raw(), text_bounds, left); - - let (right_position, right_offset) = - measure_cursor_and_scroll_offset(value_paragraph.raw(), text_bounds, right); - - let width = right_position - left_position; - if dnd_icon { - (None, 0.0) - } else { + if is_cursor_visible && !dnd_icon { ( - Some(( + vec![( renderer::Quad { bounds: Rectangle { - x: text_bounds.x - + left_position - + if left_position < 0. || right_position < 0. { - actual_width - } else { - 0. - }, + x: (text_bounds.x + text_value_width).floor(), y: text_bounds.y, - width, + width: 1.0, height: text_bounds.height, }, border: Border { @@ -2438,30 +2456,101 @@ pub fn draw<'a, Message>( }, snap: true, }, - appearance.selected_fill, - )), - if end == right { - right_offset - } else { - left_offset - }, + text_color, + )], + state.scroll_offset, + false, ) + } else { + ( + Vec::<(renderer::Quad, Color)>::new(), + if dnd_icon { 0.0 } else { state.scroll_offset }, + false, + ) + } + } + cursor::State::Selection { start, end } => { + let left = start.min(end); + let right = end.max(start); + + if dnd_icon { + (Vec::<(renderer::Quad, Color)>::new(), 0.0, true) + } else { + let lo_byte = value.byte_index_at_grapheme(left); + let hi_byte = value.byte_index_at_grapheme(right); + + let rects = state.value.raw().highlight( + 0, + (lo_byte, text::Affinity::After), + (hi_byte, text::Affinity::Before), + ); + + let cursors: Vec<(renderer::Quad, Color)> = rects + .into_iter() + .map(|r| { + ( + renderer::Quad { + bounds: Rectangle { + x: text_bounds.x + r.x, + y: text_bounds.y, + width: r.width, + height: text_bounds.height, + }, + border: Border { + width: 0.0, + color: Color::TRANSPARENT, + radius: radius_0, + }, + shadow: Shadow { + offset: Vector::ZERO, + color: Color::TRANSPARENT, + blur_radius: 0.0, + }, + snap: true, + }, + appearance.selected_fill, + ) + }) + .collect(); + + (cursors, state.scroll_offset, true) } } } } else { - (None, 0.0) + let unfocused_offset = match effective_alignment(state.value.raw()) { + alignment::Horizontal::Right => { + (state.value.raw().min_width() - text_bounds.width).max(0.0) + } + _ => 0.0, + }; + + ( + Vec::<(renderer::Quad, Color)>::new(), + unfocused_offset, + false, + ) }; let render = |renderer: &mut crate::Renderer| { - if let Some((cursor, color)) = cursor { - renderer.fill_quad(cursor, color); + let alignment_offset = alignment_offset( + text_bounds.width, + state.value.raw().min_width(), + effective_alignment(state.value.raw()), + ); + + if !cursors.is_empty() { + renderer.with_translation(Vector::new(alignment_offset - offset, 0.0), |renderer| { + for (quad, color) in &cursors { + renderer.fill_quad(*quad, *color); + } + }); } else { renderer.with_translation(Vector::ZERO, |_| {}); } let bounds = Rectangle { - x: text_bounds.x - offset, + x: text_bounds.x + alignment_offset - offset, y: text_bounds.center_y(), width: actual_width, ..text_bounds @@ -2482,7 +2571,7 @@ pub fn draw<'a, Message>( font, bounds: bounds.size(), size: iced::Pixels(size), - align_x: text::Alignment::Left, + align_x: text::Alignment::Default, align_y: alignment::Vertical::Center, line_height: text::LineHeight::default(), shaping: text::Shaping::Advanced, @@ -2495,7 +2584,11 @@ pub fn draw<'a, Message>( ); }; - renderer.with_layer(text_bounds, render); + if is_selecting { + renderer.with_layer(bounds, render); + } else { + render(renderer); + } let trailing_icon_tree = children.get(child_index); @@ -2630,7 +2723,7 @@ pub struct State { last_click: Option, cursor: Cursor, keyboard_modifiers: keyboard::Modifiers, - // TODO: Add stateful horizontal scrolling offset + scroll_offset: f32, } #[derive(Debug, Clone, Copy)] @@ -2709,6 +2802,7 @@ impl State { last_click: None, cursor: Cursor::default(), keyboard_modifiers: keyboard::Modifiers::default(), + scroll_offset: 0.0, dirty: false, } } @@ -2797,13 +2891,11 @@ impl State { } pub(super) fn setting_selection(&mut self, value: &Value, bounds: Rectangle, target: f32) { - let position = if target > 0.0 { - find_cursor_position(bounds, value, self, target) - } else { - None - }; + let (position, affinity) = find_cursor_position(bounds, value, self, target) + .unwrap_or((0, text::Affinity::Before)); - self.cursor.move_to(position.unwrap_or(0)); + self.cursor.set_affinity(affinity); + self.cursor.move_to(position); self.dragging_state = Some(DraggingState::Selection); } } @@ -2867,14 +2959,33 @@ fn measure_cursor_and_scroll_offset( paragraph: &impl text::Paragraph, text_bounds: Rectangle, cursor_index: usize, + value: &Value, + affinity: text::Affinity, + current_offset: f32, ) -> (f32, f32) { - let grapheme_position = paragraph - .grapheme_position(0, cursor_index) + let byte_index = value.byte_index_at_grapheme(cursor_index); + let position = paragraph + .cursor_position(0, byte_index, affinity) .unwrap_or(Point::ORIGIN); - let offset = ((grapheme_position.x + 5.0) - text_bounds.width).max(0.0); + // The visible window in paragraph coordinates is: + // [current_offset, current_offset + text_bounds.width] + // Keep the cursor visible with a 5px margin on each side. + let offset = if position.x > current_offset + text_bounds.width - 5.0 { + // Cursor past right edge of visible window → scroll left + (position.x + 5.0) - text_bounds.width + } else if position.x < current_offset + 5.0 { + // Cursor past left edge of visible window → scroll right + position.x - 5.0 + } else { + // Cursor is within visible window → keep current scroll + current_offset + }; - (grapheme_position.x, offset) + let max_offset = (paragraph.min_width() - text_bounds.width).max(0.0); + let offset = offset.clamp(0.0, max_offset); + + (position.x, offset) } /// Computes the position of the text cursor at the given X coordinate of @@ -2885,23 +2996,23 @@ fn find_cursor_position( value: &Value, state: &State, x: f32, -) -> Option { - let offset = offset(text_bounds, value, state); - let value = value.to_string(); +) -> Option<(usize, text::Affinity)> { + let value_str = value.to_string(); - let char_offset = state - .value - .raw() - .hit_test(Point::new(x + offset, text_bounds.height / 2.0)) - .map(text::Hit::cursor)?; + let hit = state.value.raw().hit_test(Point::new( + x + state.scroll_offset, + text_bounds.height / 2.0, + ))?; + let char_offset = hit.cursor(); + let affinity = hit.affinity(); - Some( - unicode_segmentation::UnicodeSegmentation::graphemes( - &value[..char_offset.min(value.len())], - true, - ) - .count(), + let grapheme_count = unicode_segmentation::UnicodeSegmentation::graphemes( + &value_str[..char_offset.min(value_str.len())], + true, ) + .count(); + + Some((grapheme_count, affinity)) } #[inline(never)] @@ -2928,7 +3039,7 @@ fn replace_paragraph( content: value.to_string(), bounds, size: text_size, - align_x: text::Alignment::Left, + align_x: text::Alignment::Default, align_y: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, @@ -2961,11 +3072,48 @@ fn offset(text_bounds: Rectangle, value: &Value, state: &State) -> f32 { cursor::State::Selection { end, .. } => end, }; - let (_, offset) = - measure_cursor_and_scroll_offset(state.value.raw(), text_bounds, focus_position); + let (_, offset) = measure_cursor_and_scroll_offset( + state.value.raw(), + text_bounds, + focus_position, + value, + state.cursor().affinity(), + state.scroll_offset, + ); offset } else { - 0.0 + match effective_alignment(state.value.raw()) { + alignment::Horizontal::Right => { + (state.value.raw().min_width() - text_bounds.width).max(0.0) + } + _ => 0.0, + } + } +} + +#[inline(never)] +fn alignment_offset( + text_bounds_width: f32, + text_min_width: f32, + alignment: alignment::Horizontal, +) -> f32 { + if text_min_width > text_bounds_width { + 0.0 + } else { + match alignment { + alignment::Horizontal::Left => 0.0, + alignment::Horizontal::Center => (text_bounds_width - text_min_width) / 2.0, + alignment::Horizontal::Right => text_bounds_width - text_min_width, + } + } +} + +#[inline(never)] +fn effective_alignment(paragraph: &impl text::Paragraph) -> alignment::Horizontal { + if paragraph.is_rtl(0).unwrap_or(false) { + alignment::Horizontal::Right + } else { + alignment::Horizontal::Left } } diff --git a/src/widget/text_input/value.rs b/src/widget/text_input/value.rs index 900aac0f..9faff4ac 100644 --- a/src/widget/text_input/value.rs +++ b/src/widget/text_input/value.rs @@ -132,11 +132,34 @@ impl Value { graphemes: std::iter::repeat_n(String::from("•"), self.graphemes.len()).collect(), } } -} -impl ToString for Value { - #[inline] - fn to_string(&self) -> String { - self.graphemes.concat() + /// Converts a grapheme index to a byte index in the underlying string. + #[must_use] + pub fn byte_index_at_grapheme(&self, grapheme_index: usize) -> usize { + self.graphemes[..grapheme_index.min(self.graphemes.len())] + .iter() + .map(|g| g.len()) + .sum() + } + + /// Converts a byte index to a grapheme index. + #[must_use] + pub fn grapheme_index_at_byte(&self, byte_index: usize) -> usize { + let mut bytes = 0; + for (i, g) in self.graphemes.iter().enumerate() { + if bytes >= byte_index { + return i; + } + bytes += g.len(); + } + + self.graphemes.len() + } +} + +impl std::fmt::Display for Value { + #[inline] + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.write_str(&self.graphemes.concat()) } } From e1738d2ea7c3a2df2584a7cf7f098681c2b34c86 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 1 Apr 2026 11:49:12 -0600 Subject: [PATCH 071/116] fix(text_input): keyboard shortcuts when keyboard is a different language Matches what Iced does --- src/widget/text_input/input.rs | 17 +++++++---------- 1 file changed, 7 insertions(+), 10 deletions(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index ffb08c8b..a86e4b6e 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -1719,13 +1719,10 @@ pub fn update<'a, Message: Clone + 'static>( focus.updated_at = Instant::now(); LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at)); - // Check if Ctrl+A/C/V/X was pressed. - if state.keyboard_modifiers == keyboard::Modifiers::COMMAND - || state.keyboard_modifiers - == keyboard::Modifiers::COMMAND | keyboard::Modifiers::CAPS_LOCK - { - match key.as_ref() { - keyboard::Key::Character("c") | keyboard::Key::Character("C") => { + // Check if Ctrl/Command+A/C/V/X was pressed. + if state.keyboard_modifiers.command() { + match key.to_latin(*physical_key) { + Some('c') => { if !is_secure { if let Some((start, end)) = state.cursor.selection(value) { clipboard.write( @@ -1737,7 +1734,7 @@ pub fn update<'a, Message: Clone + 'static>( } // XXX if we want to allow cutting of secure text, we need to // update the cache and decide which value to cut - keyboard::Key::Character("x") | keyboard::Key::Character("X") => { + Some('x') => { if !is_secure { if let Some((start, end)) = state.cursor.selection(value) { clipboard.write( @@ -1756,7 +1753,7 @@ pub fn update<'a, Message: Clone + 'static>( } } } - keyboard::Key::Character("v") | keyboard::Key::Character("V") => { + Some('v') => { let content = if let Some(content) = state.is_pasting.take() { content } else { @@ -1801,7 +1798,7 @@ pub fn update<'a, Message: Clone + 'static>( return; } - keyboard::Key::Character("a") | keyboard::Key::Character("A") => { + Some('a') => { state.cursor.select_all(value); shell.capture_event(); return; From 22661fd76459a279fc5837ed61abb56866e2f988 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 1 Apr 2026 15:25:10 -0600 Subject: [PATCH 072/116] chore: udpate iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index e4da5002..84f32108 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit e4da5002ae4e9d68cc4ac777ed77b4a225659440 +Subproject commit 84f3210819c03f5393fe4dcc404ab9532b941c70 From aef328238fcdaaa17e05f67fc53615ce64a547e0 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 1 Apr 2026 13:30:36 -0600 Subject: [PATCH 073/116] fix(editable): the UX is closer to design now This fixes the unresponsive trailing icon and changes the behavior to be closer to the UI/UX design. --- src/widget/text_input/input.rs | 146 ++++++++++++++++++++++++--------- 1 file changed, 108 insertions(+), 38 deletions(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index a86e4b6e..2c788235 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -66,18 +66,20 @@ pub fn editable_input<'a, Message: Clone + 'static>( editing: bool, on_toggle_edit: impl Fn(bool) -> Message + 'a, ) -> TextInput<'a, Message> { - let icon = crate::widget::icon::from_name(if editing { - "edit-clear-symbolic" - } else { - "edit-symbolic" - }); - + // The trailing icon is a placeholder; diff() rebuilds it reactively + // based on the current is_read_only state and value content. TextInput::new(placeholder, text) .style(crate::theme::TextInput::EditableText) .editable() .editing(editing) .on_toggle_edit(on_toggle_edit) - .trailing_icon(icon.size(16).into()) + .trailing_icon( + crate::widget::icon::from_name("edit-symbolic") + .size(16) + .apply(crate::widget::container) + .padding(8) + .into(), + ) } /// Creates a new search [`TextInput`]. @@ -666,7 +668,36 @@ where } } - self.is_read_only = state.is_read_only; + if self.is_editable_variant { + if !state.is_focused() { + // Not yet interacted, use the widget's value + state.is_read_only = self.is_read_only; + } else { + // Already interacted, use the state + self.is_read_only = state.is_read_only; + } + + let editing = !self.is_read_only; + let icon_name = if editing { + if self.value.is_empty() { + "window-close-symbolic" + } else { + "edit-clear-symbolic" + } + } else { + "edit-symbolic" + }; + + self.trailing_icon = Some( + crate::widget::icon::from_name(icon_name) + .size(16) + .apply(crate::widget::container) + .padding(8) + .into(), + ); + } else { + self.is_read_only = state.is_read_only; + } // Stop pasting if input becomes disabled if !self.manage_value && self.on_input.is_none() { @@ -855,9 +886,6 @@ where if !state.is_read_only && state.is_focused.is_some_and(|f| !f.focused) { state.is_read_only = true; shell.publish((on_edit)(false)); - } else if state.is_focused() && state.is_read_only { - state.is_read_only = false; - shell.publish((on_edit)(true)); } else if let Some(f) = state.is_focused.as_mut().filter(|f| f.needs_update) { // TODO do we want to just move this to on_focus or on_unfocus for all inputs? f.needs_update = false; @@ -1018,9 +1046,7 @@ where index += 1; } - if let (Some(trailing_icon), Some(tree)) = - (self.trailing_icon.as_ref(), state.children.get(index)) - { + if self.trailing_icon.is_some() { let mut children = layout.children(); children.next(); // skip if there is no leading icon @@ -1030,13 +1056,21 @@ where let trailing_icon_layout = children.next().unwrap(); if cursor_position.is_over(trailing_icon_layout.bounds()) { - return trailing_icon.as_widget().mouse_interaction( - tree, - layout, - cursor_position, - viewport, - renderer, - ); + if self.is_editable_variant { + return mouse::Interaction::Pointer; + } + + if let Some((trailing_icon, tree)) = + self.trailing_icon.as_ref().zip(state.children.get(index)) + { + return trailing_icon.as_widget().mouse_interaction( + tree, + layout, + cursor_position, + viewport, + renderer, + ); + } } } let mut children = layout.children(); @@ -1426,21 +1460,54 @@ pub fn update<'a, Message: Clone + 'static>( && edit_button_layout.is_some_and(|l| cursor.is_over(l.bounds())) { if is_editable_variant { - state.is_read_only = !state.is_read_only; - state.move_cursor_to_end(); + let has_content = !unsecured_value.is_empty(); + let is_editing = !state.is_read_only; - if let Some(on_toggle_edit) = on_toggle_edit { - shell.publish(on_toggle_edit(!state.is_read_only)); + if is_editing && has_content { + if let Some(on_input) = on_input { + shell.publish((on_input)(String::new())); + } + + if manage_value { + *unsecured_value = Value::new(""); + state.tracked_value = unsecured_value.clone(); + + let cleared_value = if is_secure { + unsecured_value.secure() + } else { + unsecured_value.clone() + }; + + update_cache(state, &cleared_value); + } + + state.move_cursor_to_end(); + } else if is_editing { + // Close: toggle back to read-only and unfocus. + state.is_read_only = true; + state.unfocus(); + + if let Some(on_toggle_edit) = on_toggle_edit { + shell.publish(on_toggle_edit(false)); + } + } else { + // Edit: toggle to editing, select all, and focus. + state.is_read_only = false; + state.cursor.select_range(0, value.len()); + + if let Some(on_toggle_edit) = on_toggle_edit { + shell.publish(on_toggle_edit(true)); + } + + let now = Instant::now(); + LAST_FOCUS_UPDATE.with(|x| x.set(now)); + state.is_focused = Some(Focus { + updated_at: now, + now, + focused: true, + needs_update: false, + }); } - - let now = Instant::now(); - LAST_FOCUS_UPDATE.with(|x| x.set(now)); - state.is_focused = Some(Focus { - updated_at: now, - now, - focused: true, - needs_update: false, - }); } shell.capture_event(); @@ -1550,15 +1617,18 @@ pub fn update<'a, Message: Clone + 'static>( } // Focus on click of the text input, and ensure that the input is writable. - if !state.is_focused() - && matches!(state.dragging_state, None | Some(DraggingState::Selection)) + if matches!(state.dragging_state, None | Some(DraggingState::Selection)) + && (!state.is_focused() || (is_editable_variant && state.is_read_only)) { - if let Some(on_focus) = on_focus { - shell.publish(on_focus.clone()); + if !state.is_focused() { + if let Some(on_focus) = on_focus { + shell.publish(on_focus.clone()); + } } if state.is_read_only { state.is_read_only = false; + state.cursor.select_range(0, value.len()); if let Some(on_toggle_edit) = on_toggle_edit { let message = (on_toggle_edit)(true); shell.publish(message); From 0ba668eb52908e5b10f65971d7a4b6395dc194ae Mon Sep 17 00:00:00 2001 From: TobyDig <53296459+TobyDig@users.noreply.github.com> Date: Thu, 2 Apr 2026 08:32:36 +1100 Subject: [PATCH 074/116] fix(desktop): use `-e` argument for spawning desktop entries with a terminal --- src/desktop.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/desktop.rs b/src/desktop.rs index fe32f286..98ce7d4b 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -789,7 +789,7 @@ pub async fn spawn_desktop_exec( }) .unwrap_or_else(|| String::from("cosmic-term")); - term_exec = format!("{term} -- {}", exec.as_ref()); + term_exec = format!("{term} -e {}", exec.as_ref()); &term_exec } else { exec.as_ref() From f6eb314606f77adfa7199338a89b17f3d17d136c Mon Sep 17 00:00:00 2001 From: KENZ Date: Thu, 2 Apr 2026 07:35:57 +0900 Subject: [PATCH 075/116] feat(text_input): minimal IME support for COSMIC specific text widgets --- src/widget/text_input/input.rs | 104 ++++++++++++++++++++++++++++++++- 1 file changed, 103 insertions(+), 1 deletion(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 2c788235..cd93a7d7 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -22,10 +22,11 @@ use iced::Limits; use iced::clipboard::dnd::{DndAction, DndEvent, OfferEvent, SourceEvent}; use iced::clipboard::mime::AsMimeTypes; use iced_core::event::{self, Event}; +use iced_core::input_method::{self, InputMethod, Preedit}; use iced_core::mouse::{self, click}; use iced_core::overlay::Group; use iced_core::renderer::{self, Renderer as CoreRenderer}; -use iced_core::text::{self, Paragraph, Renderer, Text}; +use iced_core::text::{self, Affinity, Paragraph, Renderer, Text}; use iced_core::time::{Duration, Instant}; use iced_core::touch; use iced_core::widget::Id; @@ -2083,6 +2084,66 @@ pub fn update<'a, Message: Clone + 'static>( state.keyboard_modifiers = *modifiers; } + Event::InputMethod(event) => { + let state = state(); + + match event { + input_method::Event::Opened | input_method::Event::Closed => { + state.preedit = matches!(event, input_method::Event::Opened) + .then(input_method::Preedit::new); + shell.capture_event(); + return; + } + input_method::Event::Preedit(content, selection) => { + if state.is_focused.is_some() { + state.preedit = Some(input_method::Preedit { + content: content.to_owned(), + selection: selection.clone(), + text_size: Some(size.into()), + }); + shell.capture_event(); + return; + } + } + input_method::Event::Commit(text) => { + let Some(focus) = &mut state.is_focused else { + return; + }; + let Some(on_input) = on_input else { + return; + }; + if state.is_read_only { + return; + } + + focus.updated_at = Instant::now(); + LAST_FOCUS_UPDATE.with(|x| x.set(focus.updated_at)); + + let mut editor = Editor::new(unsecured_value, &mut state.cursor); + editor.paste(Value::new(&text)); + + let contents = editor.contents(); + let unsecured_value = Value::new(&contents); + let message = if let Some(paste) = &on_paste { + (paste)(contents) + } else { + (on_input)(contents) + }; + shell.publish(message); + + state.is_pasting = None; + let value = if is_secure { + unsecured_value.secure() + } else { + unsecured_value + }; + + update_cache(state, &value); + shell.capture_event(); + return; + } + } + } Event::Window(window::Event::RedrawRequested(now)) => { let state = state(); @@ -2095,6 +2156,8 @@ pub fn update<'a, Message: Clone + 'static>( now.checked_add(Duration::from_millis(millis_until_redraw as u64)) .unwrap_or(*now), )); + + shell.request_input_method(&input_method(state, text_layout, unsecured_value)); } else if always_active { shell.request_redraw(); } @@ -2269,6 +2332,43 @@ pub fn update<'a, Message: Clone + 'static>( } } +fn input_method<'b>( + state: &'b State, + text_layout: Layout<'_>, + value: &Value, +) -> InputMethod<&'b str> { + if state.is_focused() { + } else { + return InputMethod::Disabled; + }; + + let text_bounds = text_layout.bounds(); + let cursor_index = match state.cursor.state(value) { + cursor::State::Index(position) => position, + cursor::State::Selection { start, end } => start.min(end), + }; + let (cursor, offset) = measure_cursor_and_scroll_offset( + state.value.raw(), + text_bounds, + cursor_index, + value, + state.cursor.affinity(), + state.scroll_offset, + ); + InputMethod::Enabled { + cursor: Rectangle::new( + Point::new(text_bounds.x + cursor - offset, text_bounds.y), + Size::new(1.0, text_bounds.height), + ), + purpose: if state.is_secure { + input_method::Purpose::Secure + } else { + input_method::Purpose::Normal + }, + preedit: state.preedit.as_ref().map(input_method::Preedit::as_ref), + } +} + /// Draws the [`TextInput`] with the given [`Renderer`], overriding its /// [`Value`] if provided. /// @@ -2789,6 +2889,7 @@ pub struct State { is_pasting: Option, last_click: Option, cursor: Cursor, + preedit: Option, keyboard_modifiers: keyboard::Modifiers, scroll_offset: f32, } @@ -2868,6 +2969,7 @@ impl State { is_pasting: None, last_click: None, cursor: Cursor::default(), + preedit: None, keyboard_modifiers: keyboard::Modifiers::default(), scroll_offset: 0.0, dirty: false, From 12be83a8ef58019a1bd31ead100040244bd05f16 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 1 Apr 2026 20:12:12 -0600 Subject: [PATCH 076/116] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 84f32108..42e3afb5 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 84f3210819c03f5393fe4dcc404ab9532b941c70 +Subproject commit 42e3afb5686eff08c78c9292bb83c36d5c8f5146 From 61e5d882ae877f39b3389e17da48f516fdcc4582 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Thu, 26 Mar 2026 19:27:50 -0600 Subject: [PATCH 077/116] fix(ci): only document libcosmic, no dependency --- .github/workflows/pages.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 4229839e..e48570ba 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -18,7 +18,7 @@ jobs: - name: System dependencies run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Build documentation - run: cargo doc --verbose --features tokio,winit + run: cargo doc --no-deps --verbose --features tokio,winit - name: Deploy documentation uses: peaceiris/actions-gh-pages@v3 with: From 7a02c9a296c10469d9061391657f71aa33b3936b Mon Sep 17 00:00:00 2001 From: GroobleDierne Date: Fri, 30 Jan 2026 23:33:52 +0100 Subject: [PATCH 078/116] fix(color palette): avoid duplicates --- src/widget/color_picker/mod.rs | 34 +++++++++++++--------------------- 1 file changed, 13 insertions(+), 21 deletions(-) diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index d484bb62..318e943b 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -4,7 +4,6 @@ //! Widgets for selecting colors with a color picker. use std::borrow::Cow; -use std::iter; use std::rc::Rc; use std::sync::LazyLock; use std::sync::atomic::{AtomicBool, Ordering}; @@ -93,8 +92,6 @@ pub struct ColorPickerModel { #[setters(skip)] active_color: palette::Hsv, #[setters(skip)] - save_next: Option, - #[setters(skip)] input_color: String, #[setters(skip)] applied_color: Option, @@ -128,7 +125,6 @@ impl ColorPickerModel { .insert(move |b| b.text(rgb.clone())) .build(), active_color: hsv, - save_next: None, input_color: color_to_string(hsv, true), applied_color: initial, fallback_color, @@ -159,22 +155,26 @@ impl ColorPickerModel { ) } + fn update_recent_colors(&mut self, new_color: Color) { + if let Some(pos) = self.recent_colors.iter().position(|c| *c == new_color) { + self.recent_colors.remove(pos); + } + self.recent_colors.insert(0, new_color); + self.recent_colors.truncate(MAX_RECENT); + } + pub fn update(&mut self, update: ColorPickerUpdate) -> Task { match update { ColorPickerUpdate::ActiveColor(c) => { self.must_clear_cache.store(true, Ordering::SeqCst); self.input_color = color_to_string(c, self.is_hex()); - if let Some(to_save) = self.save_next.take() { - self.recent_colors.insert(0, to_save); - self.recent_colors.truncate(MAX_RECENT); - } self.active_color = c; self.copied_at = None; } - ColorPickerUpdate::AppliedColor => { + ColorPickerUpdate::AppliedColor | ColorPickerUpdate::ActionFinished => { let srgb = palette::Srgb::from_color(self.active_color); if let Some(applied_color) = self.applied_color.take() { - self.recent_colors.push(applied_color); + self.update_recent_colors(applied_color); } self.applied_color = Some(Color::from(srgb)); self.active = false; @@ -215,21 +215,12 @@ impl ColorPickerModel { palette::Hsv::from_color(palette::Srgb::new(c.red, c.green, c.blue)); } } - ColorPickerUpdate::ActionFinished => { - let srgb = palette::Srgb::from_color(self.active_color); - if let Some(applied_color) = self.applied_color.take() { - self.recent_colors.push(applied_color); - } - self.applied_color = Some(Color::from(srgb)); - self.active = false; - self.save_next = Some(Color::from(srgb)); - } ColorPickerUpdate::ToggleColorPicker => { self.must_clear_cache.store(true, Ordering::SeqCst); self.active = !self.active; self.copied_at = None; } - }; + } Task::none() } @@ -395,7 +386,8 @@ where text_input("", self.input_color) .on_input(move |s| on_update(ColorPickerUpdate::Input(s))) .on_paste(move |s| on_update(ColorPickerUpdate::Input(s))) - .on_submit(move |_| on_update(ColorPickerUpdate::AppliedColor)) + .on_submit(move |_| on_update(ColorPickerUpdate::ActionFinished)) + // .on_unfocus(on_update(ColorPickerUpdate::ActionFinished)) Somehow this is called even when the field wasn't previously focused .leading_icon( color_button( None, From 24464908f6503e0f7923357a19a578151f18f50a Mon Sep 17 00:00:00 2001 From: Hojjat Date: Thu, 2 Apr 2026 18:15:41 -0600 Subject: [PATCH 079/116] fix: buttons are focusable again --- src/widget/button/widget.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index a4e32378..4acf3f2d 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -357,6 +357,8 @@ impl<'a, Message: 'a + Clone> Widget operation, ); }); + let state = tree.state.downcast_mut::(); + operation.focusable(Some(&self.id), layout.bounds(), state); } fn update( From 97a805e5a184c122364e47d7eceac763987fb491 Mon Sep 17 00:00:00 2001 From: Hendrik Hamerlinck Date: Wed, 11 Feb 2026 22:34:22 +0100 Subject: [PATCH 080/116] feat(applets): add destroy tooltip popup action This commit adds a new surface action to explicitly destroy the tooltip popup on `TOOLTIP_WINDOW_ID`, allowing proper cleanup when minimizing applets. --- src/app/cosmic.rs | 11 +++++++++++ src/applet/mod.rs | 2 +- src/surface/mod.rs | 3 +++ 3 files changed, 15 insertions(+), 1 deletion(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index b732eee9..030ed041 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -230,6 +230,17 @@ where iced_winit::commands::popup::destroy_popup(id) } #[cfg(all(feature = "wayland", target_os = "linux"))] + crate::surface::Action::DestroyTooltipPopup => { + #[cfg(feature = "applet")] + { + iced_winit::commands::popup::destroy_popup(*crate::applet::TOOLTIP_WINDOW_ID) + } + #[cfg(not(feature = "applet"))] + { + Task::none() + } + } + #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::DestroySubsurface(id) => { iced_winit::commands::subsurface::destroy_subsurface(id) } diff --git a/src/applet/mod.rs b/src/applet/mod.rs index a3f5228b..a7fc4069 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -42,7 +42,7 @@ static AUTOSIZE_ID: LazyLock = static AUTOSIZE_MAIN_ID: LazyLock = LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize-main")); static TOOLTIP_ID: LazyLock = LazyLock::new(|| iced::id::Id::new("subsurface")); -static TOOLTIP_WINDOW_ID: LazyLock = LazyLock::new(window::Id::unique); +pub(crate) static TOOLTIP_WINDOW_ID: LazyLock = LazyLock::new(window::Id::unique); #[derive(Debug, Clone)] pub struct Context { diff --git a/src/surface/mod.rs b/src/surface/mod.rs index 4598ac7c..0dad6459 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -36,6 +36,8 @@ pub enum Action { ), /// Destroy a subsurface with a view function DestroyPopup(iced::window::Id), + /// Destroys the global tooltip popup subsurface + DestroyTooltipPopup, /// Create a window with a view function accepting the App as a parameter AppWindow( @@ -85,6 +87,7 @@ impl std::fmt::Debug for Action { } Self::Popup(arg0, arg1) => f.debug_tuple("Popup").field(arg0).field(arg1).finish(), Self::DestroyPopup(arg0) => f.debug_tuple("DestroyPopup").field(arg0).finish(), + Self::DestroyTooltipPopup => f.debug_tuple("DestroyTooltipPopup").finish(), Self::ResponsiveMenuBar { menu_bar, limits, From b0f4e931f2c1d7d30e4fd0dee8f5b45b9b5038f9 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 3 Apr 2026 08:25:01 -0400 Subject: [PATCH 081/116] fix: font issues some fonts are not falling back when a glyph is missing for a selected font and weight --- Cargo.toml | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 35d048ee..bdbc141b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,6 +9,7 @@ name = "cosmic" [features] default = [ + "advanced-shaping", "winit", "tokio", "a11y", @@ -16,7 +17,8 @@ default = [ "x11", "iced-wayland", "multi-window", -] # default = ["dbus-config", "multi-window", "a11y"] +] +advanced-shaping = ["iced/advanced-shaping"] # Accessibility support a11y = ["iced/a11y", "iced_accessibility"] # Enable about widget From cdd825b953b528b19907e185957f8bc203514d3d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 3 Apr 2026 08:25:12 -0400 Subject: [PATCH 082/116] fix: update iced softbuffer released version doesn't support transparency yet --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 42e3afb5..2d4ede15 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 42e3afb5686eff08c78c9292bb83c36d5c8f5146 +Subproject commit 2d4ede1597860db0bfaccfbc0166ee89ac353fc2 From 34219d1fd4171fb14ea7b4f6f572f9cbd4952150 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 3 Apr 2026 14:12:58 -0400 Subject: [PATCH 083/116] chore: wgpu cctk feature for wayland --- Cargo.toml | 1 + 1 file changed, 1 insertion(+) diff --git a/Cargo.toml b/Cargo.toml index bdbc141b..78922132 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -93,6 +93,7 @@ wayland = [ "iced-wayland", "iced_runtime/cctk", "iced_winit/cctk", + "iced_wgpu/cctk", "iced/cctk", "dep:cctk", ] From a9e0671075093ef417a9bae8d0ec39ac44a8c035 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Fri, 3 Apr 2026 03:58:26 +0200 Subject: [PATCH 084/116] fix(segmented_button): hover text style --- src/widget/segmented_button/widget.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 76c74f3b..203fbc2e 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -218,7 +218,7 @@ where maximum_button_width: u16::MAX, indent_spacing: 16, font_active: crate::font::semibold(), - font_hovered: crate::font::semibold(), + font_hovered: crate::font::default(), font_inactive: crate::font::default(), font_size: 14.0, height: Length::Shrink, From fdf3369cea2f772aabb5f7c4e5cdf6406780f6ab Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Wed, 1 Apr 2026 23:24:53 +0200 Subject: [PATCH 085/116] chore: re-export iced row and column This removes the custom row and column implementations and uses the iced ones directly. --- examples/about/src/main.rs | 2 +- examples/application/src/main.rs | 4 +- examples/calendar/src/main.rs | 6 +-- examples/image-button/src/main.rs | 2 +- examples/subscriptions/src/main.rs | 2 +- examples/text-input/src/main.rs | 4 +- src/ext.rs | 66 ------------------------ src/widget/about.rs | 56 +++++++++++--------- src/widget/button/icon.rs | 7 +-- src/widget/calendar.rs | 2 +- src/widget/context_menu.rs | 2 +- src/widget/header_bar.rs | 74 ++++++++++++--------------- src/widget/list/column.rs | 2 +- src/widget/mod.rs | 65 +++-------------------- src/widget/segmented_button/widget.rs | 6 +-- src/widget/settings/item.rs | 12 ++--- src/widget/settings/mod.rs | 4 +- src/widget/table/widget/compact.rs | 6 +-- src/widget/table/widget/standard.rs | 4 +- src/widget/toaster/mod.rs | 4 +- 20 files changed, 103 insertions(+), 227 deletions(-) diff --git a/examples/about/src/main.rs b/examples/about/src/main.rs index 50f25da4..c25a9b9a 100644 --- a/examples/about/src/main.rs +++ b/examples/about/src/main.rs @@ -132,7 +132,7 @@ impl cosmic::Application for App { fn view(&self) -> Element<'_, Self::Message> { let show_about_button = widget::button::text("Show about").on_press(Message::ToggleAbout); let centered = cosmic::widget::container( - widget::column() + widget::column::with_capacity(1) .push(show_about_button) .width(Length::Fill) .height(Length::Shrink) diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 831a47f1..53f1c28e 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -54,7 +54,7 @@ impl widget::menu::Action for Action { /// Runs application with these settings #[rustfmt::skip] fn main() -> Result<(), Box> { - + env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("warn")).init(); @@ -190,7 +190,7 @@ impl cosmic::Application for App { .map_or("No page selected", String::as_str); let centered = widget::container( - widget::column() + widget::column::with_capacity(5) .push(widget::text::body(page_content)) .push( widget::text_input::text_input("", &self.input_1) diff --git a/examples/calendar/src/main.rs b/examples/calendar/src/main.rs index 240684c6..494087d1 100644 --- a/examples/calendar/src/main.rs +++ b/examples/calendar/src/main.rs @@ -85,8 +85,6 @@ impl cosmic::Application for App { /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { - let mut content = cosmic::widget::column().spacing(12); - let calendar = cosmic::widget::calendar( &self.calendar_model, |date| Message::DateSelected(date), @@ -95,9 +93,7 @@ impl cosmic::Application for App { Weekday::Sunday, ); - content = content.push(calendar); - - let centered = cosmic::widget::container(content) + let centered = cosmic::widget::container(calendar) .width(iced::Length::Fill) .height(iced::Length::Shrink) .align_x(iced::Alignment::Center) diff --git a/examples/image-button/src/main.rs b/examples/image-button/src/main.rs index 0ac906ca..c68c7070 100644 --- a/examples/image-button/src/main.rs +++ b/examples/image-button/src/main.rs @@ -80,7 +80,7 @@ impl cosmic::Application for App { /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { - let mut content = cosmic::widget::column().spacing(12); + let mut content = cosmic::widget::column::with_capacity(self.images.len()).spacing(12); for (id, image) in self.images.iter().enumerate() { content = content.push( diff --git a/examples/subscriptions/src/main.rs b/examples/subscriptions/src/main.rs index 47bd3772..17e630aa 100644 --- a/examples/subscriptions/src/main.rs +++ b/examples/subscriptions/src/main.rs @@ -64,7 +64,7 @@ impl cosmic::Application for App { /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { - widget::row().into() + widget::Row::new().into() } } diff --git a/examples/text-input/src/main.rs b/examples/text-input/src/main.rs index ea99666c..c17fcd5c 100644 --- a/examples/text-input/src/main.rs +++ b/examples/text-input/src/main.rs @@ -99,7 +99,9 @@ impl cosmic::Application for App { let inline = cosmic::widget::inline_input("", &self.input).on_input(Message::Input); - let column = cosmic::widget::column().push(editable).push(inline); + let column = cosmic::widget::column::with_capacity(2) + .push(editable) + .push(inline); let centered = cosmic::widget::container(column.width(200)) .width(iced::Length::Fill) diff --git a/src/ext.rs b/src/ext.rs index c85e6e86..8eb749e5 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -19,72 +19,6 @@ impl ElementExt for crate::Element<'_, Message> { } } -/// Additional methods for the [`Column`] and [`Row`] widgets. -pub trait CollectionWidget<'a, Message: 'a>: - Widget -where - Self: Sized, -{ - /// Moves all the elements of `other` into `self`, leaving `other` empty. - #[must_use] - fn append(self, other: &mut Vec) -> Self - where - E: Into>; - - /// Appends all elements in an iterator to the widget. - #[must_use] - fn extend(mut self, iterator: impl Iterator) -> Self - where - E: Into>, - { - for item in iterator { - self = self.push(item.into()); - } - - self - } - - /// Pushes an element into the widget. - #[must_use] - fn push(self, element: impl Into>) -> Self; - - /// Conditionally pushes an element to the widget. - #[must_use] - fn push_maybe(self, element: Option>>) -> Self { - if let Some(element) = element { - self.push(element.into()) - } else { - self - } - } -} - -impl<'a, Message: 'a> CollectionWidget<'a, Message> for crate::widget::Column<'a, Message> { - fn append(self, other: &mut Vec) -> Self - where - E: Into>, - { - self.extend(other.drain(..).map(Into::into)) - } - - fn push(self, element: impl Into>) -> Self { - self.push(element) - } -} - -impl<'a, Message: 'a> CollectionWidget<'a, Message> for crate::widget::Row<'a, Message> { - fn append(self, other: &mut Vec) -> Self - where - E: Into>, - { - self.extend(other.drain(..).map(Into::into)) - } - - fn push(self, element: impl Into>) -> Self { - self.push(element) - } -} - pub trait ColorExt { /// Combines color with background to create appearance of transparency. #[must_use] diff --git a/src/widget/about.rs b/src/widget/about.rs index ba88e03a..148af02a 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -47,32 +47,40 @@ pub struct About { fn add_contributors(contributors: Vec<(&str, &str)>) -> Vec<(String, String)> { contributors .into_iter() - .map(|(name, email)| (name.to_string(), format!("mailto:{email}"))) + .map(|(name, email)| (name.into(), format!("mailto:{email}"))) .collect() } -macro_rules! set_contributors { - ($field:ident, $doc:expr) => { - #[doc = $doc] - pub fn $field(mut self, contributors: impl Into>) -> Self { - self.$field = add_contributors(contributors.into()); - self - } - }; -} - impl<'a> About { - set_contributors!(artists, "Artists who contributed to the application."); - set_contributors!(designers, "Designers who contributed to the application."); - set_contributors!(developers, "Developers who contributed to the application."); - set_contributors!( - documenters, - "Documenters who contributed to the application." - ); - set_contributors!( - translators, - "Translators who contributed to the application." - ); + /// Artists who contributed to the application. + pub fn artists(mut self, contributors: impl Into>) -> Self { + self.artists = add_contributors(contributors.into()); + self + } + + /// Designers who contributed to the application. + pub fn designers(mut self, contributors: impl Into>) -> Self { + self.designers = add_contributors(contributors.into()); + self + } + + /// Developers who contributed to the application. + pub fn developers(mut self, contributors: impl Into>) -> Self { + self.developers = add_contributors(contributors.into()); + self + } + + /// Documenters who contributed to the application. + pub fn documenters(mut self, contributors: impl Into>) -> Self { + self.documenters = add_contributors(contributors.into()); + self + } + + /// Translators who contributed to the application. + pub fn translators(mut self, contributors: impl Into>) -> Self { + self.translators = add_contributors(contributors.into()); + self + } /// Links associated with the application. pub fn links, V: Into>( @@ -97,7 +105,7 @@ pub fn about<'a, Message: Clone + 'static>( } = crate::theme::spacing(); let section_button = |name: &'a str, url: &'a str| -> Element<'a, Message> { - widget::row() + widget::row::with_capacity(3) .push(widget::text(name)) .push(space::horizontal()) .push_maybe( @@ -158,7 +166,7 @@ pub fn about<'a, Message: Clone + 'static>( let copyright = about.copyright.as_ref().map(widget::text::body); let comments = about.comments.as_ref().map(widget::text::body); - widget::column() + widget::column::with_capacity(10) .push_maybe(header) .push_maybe(links_section) .push_maybe(developers_section) diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs index edb54272..04d2bdd5 100644 --- a/src/widget/button/icon.rs +++ b/src/widget/button/icon.rs @@ -3,10 +3,7 @@ use super::{Builder, ButtonClass}; use crate::Element; -use crate::widget::{ - icon::{self, Handle}, - tooltip, -}; +use crate::widget::{icon::Handle, tooltip}; use apply::Apply; use iced_core::{Alignment, Length, Padding, font::Weight, text::LineHeight, widget::Id}; use std::borrow::Cow; @@ -133,7 +130,7 @@ impl Button<'_, Message> { } impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { - fn from(mut builder: Button<'a, Message>) -> Element<'a, Message> { + fn from(builder: Button<'a, Message>) -> Element<'a, Message> { let mut content = Vec::with_capacity(2); content.push( diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index 7c09d39c..19758472 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -212,7 +212,7 @@ where let content_list = column::with_children([ row::with_children([ - column().push(date).push(day).into(), + column([date.into(), day.into()]).into(), crate::widget::space::horizontal() .width(Length::Fill) .into(), diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 918d4da2..3f35f04a 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -32,7 +32,7 @@ pub fn context_menu<'a, Message: 'static + Clone>( content: content.into(), context_menu: context_menu.map(|menus| { vec![menu::Tree::with_children( - crate::Element::from(crate::widget::row::<'static, Message>()), + crate::Element::from(crate::widget::Row::new()), menus, )] }), diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 1c0ca2c0..a772f7d2 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -243,10 +243,13 @@ impl<'a, Message: Clone + 'static> Widget Widget, viewport: &iced_core::Rectangle, ) { - for ((e, s), l) in self - .elems_mut() + self.elems_mut() .zip(&mut state.children) .zip(layout.children()) - { - e.as_widget_mut() - .update(s, event, l, cursor, renderer, clipboard, shell, viewport); - } + .for_each(|((e, s), l)| { + e.as_widget_mut() + .update(s, event, l, cursor, renderer, clipboard, shell, viewport); + }); } fn mouse_interaction( @@ -296,13 +298,12 @@ impl<'a, Message: Clone + 'static> Widget, ) { - for ((e, s), l) in self - .elems_mut() + self.elems_mut() .zip(&mut state.children) .zip(layout.children()) - { - e.as_widget_mut().operate(s, l, renderer, operation); - } + .for_each(|((e, s), l)| { + e.as_widget_mut().operate(s, l, renderer, operation); + }); } fn overlay<'b>( @@ -313,27 +314,13 @@ impl<'a, Message: Clone + 'static> Widget Option> { - let mut layouts = layout.children(); - let mut try_overlay = |elem: &'b mut Element<'a, Message>, - state: &'b mut tree::Tree| - -> Option< - iced_core::overlay::Element<'b, Message, crate::Theme, crate::Renderer>, - > { - elem.as_widget_mut() - .overlay(state, layouts.next()?, renderer, viewport, translation) - }; - - if let Some(center) = &mut self.center { - let (start_slice, end_center) = state.children.split_at_mut(1); - let (end_slice, center_slice) = end_center.split_at_mut(1); - try_overlay(&mut self.start, &mut start_slice[0]) - .or_else(|| try_overlay(&mut self.end, &mut end_slice[0])) - .or_else(|| try_overlay(center, &mut center_slice[0])) - } else { - let (start_slice, end_slice) = state.children.split_at_mut(1); - try_overlay(&mut self.start, &mut start_slice[0]) - .or_else(|| try_overlay(&mut self.end, &mut end_slice[0])) - } + self.elems_mut() + .zip(&mut state.children) + .zip(layout.children()) + .find_map(|((e, s), l)| { + e.as_widget_mut() + .overlay(s, l, renderer, viewport, translation) + }) } fn drag_destinations( @@ -343,10 +330,13 @@ impl<'a, Message: Clone + 'static> Widget HeaderBar<'a, Message> { let mut widget = HeaderBarWidget::new(start, center, end) .apply(widget::container) - .class(crate::theme::Container::HeaderBar { + .class(theme::Container::HeaderBar { focused: self.focused, sharp_corners: self.sharp_corners, transparent: self.transparent, @@ -463,7 +453,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { widget::icon::from_name($name) .apply(widget::button::icon) .padding(8) - .class(crate::theme::Button::HeaderBar) + .class(theme::Button::HeaderBar) .selected(self.focused) .icon_size($size) .on_press($on_press) diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs index 136b49ea..945b9140 100644 --- a/src/widget/list/column.rs +++ b/src/widget/list/column.rs @@ -63,7 +63,7 @@ impl<'a, Message: 'static> ListColumn<'a, Message> { } // Ensure a minimum height of 32. - let list_item = iced::widget::row![ + let list_item = crate::widget::row![ container(item).align_y(iced::Alignment::Center), vertical().height(iced::Length::Fixed(32.)) ] diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 0f607240..ef212dab 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -24,7 +24,7 @@ //! .on_press(Message::LaunchUrl(REPOSITORY)) //! .padding(0); //! -//! let content = widget::column() +//! let content = widget::column::with_capacity(3) //! .push(widget::icon::from_name("my-app-icon")) //! .push(widget::text::title3("My App Name")) //! .push(link) @@ -53,6 +53,9 @@ pub use iced::widget::{Canvas, canvas}; #[doc(inline)] pub use iced::widget::{Checkbox, checkbox}; +#[doc(inline)] +pub use iced::widget::{Column, column}; + #[doc(inline)] pub use iced::widget::{ComboBox, combo_box}; @@ -80,6 +83,9 @@ pub use iced::widget::{ProgressBar, progress_bar}; #[doc(inline)] pub use iced::widget::{Responsive, responsive}; +#[doc(inline)] +pub use iced::widget::{Row, row}; + #[doc(inline)] pub use iced::widget::{Slider, VerticalSlider, slider, vertical_slider}; @@ -135,34 +141,6 @@ pub mod context_drawer; #[doc(inline)] pub use context_drawer::{ContextDrawer, context_drawer}; -#[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_capacity(capacity) - } - - #[must_use] - /// A [`column`] that will be assigned an [`Iterator`] of children. - pub fn with_children<'a, Message>( - children: impl IntoIterator>, - ) -> Column<'a, Message> { - Column::with_children(children) - } -} - pub mod layer_container; #[doc(inline)] pub use layer_container::{LayerContainer, layer_container}; @@ -287,35 +265,6 @@ pub mod rectangle_tracker; #[doc(inline)] pub use rectangle_tracker::{RectangleTracker, rectangle_tracking_container}; -#[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_capacity(capacity) - } - - #[must_use] - /// A [`row`] that will be assigned an [`Iterator`] of children. - pub fn with_children<'a, Message>( - children: impl IntoIterator>, - ) -> Row<'a, Message> { - Row::with_children(children) - } -} - pub mod scrollable; #[doc(inline)] pub use scrollable::scrollable; diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 203fbc2e..b9d1000e 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -305,7 +305,7 @@ where { self.context_menu = context_menu.map(|menus| { vec![menu::Tree::with_children( - crate::Element::from(crate::widget::row::<'static, Message>()), + crate::Element::from(crate::widget::Row::new()), menus, )] }); @@ -1481,7 +1481,7 @@ where } } } else { - if let Item::Tab(key) = std::mem::replace(&mut state.hovered, Item::None) { + if let Item::Tab(_key) = std::mem::replace(&mut state.hovered, Item::None) { for key in self.model.order.iter().copied() { self.update_entity_paragraph(state, key); } @@ -2139,7 +2139,7 @@ where tree: &'b mut Tree, layout: iced_core::Layout<'b>, _renderer: &Renderer, - viewport: &iced_core::Rectangle, + _viewport: &iced_core::Rectangle, translation: Vector, ) -> Option> { let state = tree.state.downcast_mut::(); diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index 110ab7b7..349d93d8 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -4,7 +4,7 @@ use std::borrow::Cow; use crate::{ - Element, theme, + Element, Theme, theme, widget::{FlexRow, Row, column, container, flex_row, row, text}, }; use derive_setters::Setters; @@ -18,12 +18,12 @@ use taffy::AlignContent; pub fn item<'a, Message: 'static>( title: impl Into> + 'a, widget: impl Into> + 'a, -) -> Row<'a, Message> { +) -> Row<'a, Message, Theme> { #[inline(never)] fn inner<'a, Message: 'static>( title: Cow<'a, str>, widget: Element<'a, Message>, - ) -> Row<'a, Message> { + ) -> Row<'a, Message, Theme> { item_row(vec![ text(title).wrapping(Wrapping::Word).into(), space::horizontal().into(), @@ -37,7 +37,7 @@ pub fn item<'a, Message: 'static>( /// A settings item aligned in a row #[must_use] #[allow(clippy::module_name_repetitions)] -pub fn item_row(children: Vec>) -> Row { +pub fn item_row(children: Vec>) -> Row { row::with_children(children) .spacing(theme::spacing().space_xs) .align_y(iced::Alignment::Center) @@ -105,7 +105,7 @@ 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> { + pub fn control(self, widget: impl Into>) -> Row<'a, Message, Theme> { item_row(self.control_(widget.into())) } @@ -142,7 +142,7 @@ impl<'a, Message: 'static> Item<'a, Message> { self, is_checked: bool, message: impl Fn(bool) -> Message + 'static, - ) -> Row<'a, Message> { + ) -> Row<'a, Message, Theme> { self.control( crate::widget::toggler(is_checked) .width(Length::Shrink) diff --git a/src/widget/settings/mod.rs b/src/widget/settings/mod.rs index 597d9bdd..79d81697 100644 --- a/src/widget/settings/mod.rs +++ b/src/widget/settings/mod.rs @@ -8,10 +8,10 @@ pub use self::item::{flex_item, flex_item_row, item, item_row}; pub use self::section::{Section, section}; use crate::widget::{Column, column}; -use crate::{Element, theme}; +use crate::{Element, Theme, theme}; /// A column with a predefined style for creating a settings panel #[must_use] -pub fn view_column(children: Vec>) -> Column { +pub fn view_column(children: Vec>) -> Column { column::with_children(children).spacing(theme::spacing().space_m) } diff --git a/src/widget/table/widget/compact.rs b/src/widget/table/widget/compact.rs index db71a1af..65ac9058 100644 --- a/src/widget/table/widget/compact.rs +++ b/src/widget/table/widget/compact.rs @@ -65,7 +65,7 @@ where let selected = val.model.is_active(entity); let context_menu = (val.item_context_builder)(item); - widget::column() + widget::column::with_capacity(2) .spacing(val.item_spacing) .push( widget::divider::horizontal::default() @@ -73,7 +73,7 @@ where .padding(val.divider_padding), ) .push( - widget::row() + widget::row::with_capacity(2) .spacing(space_xxxs) .align_y(Alignment::Center) .push_maybe( @@ -81,7 +81,7 @@ where .map(|icon| icon.size(val.icon_size)), ) .push( - widget::column() + widget::column::with_capacity(2) .push(widget::text::body(item.get_text(Category::default()))) .push({ let mut elements = val diff --git a/src/widget/table/widget/standard.rs b/src/widget/table/widget/standard.rs index 1fa611f3..9ab76c9d 100644 --- a/src/widget/table/widget/standard.rs +++ b/src/widget/table/widget/standard.rs @@ -99,7 +99,7 @@ where }; // Build the category header - widget::row() + widget::row::with_capacity(2) .spacing(val.icon_spacing) .push(widget::text::heading(category.to_string())) .push_maybe(match sort_state { @@ -152,7 +152,7 @@ where categories .iter() .map(|category| { - widget::row() + widget::row::with_capacity(2) .spacing(val.icon_spacing) .push_maybe( item.get_icon(*category) diff --git a/src/widget/toaster/mod.rs b/src/widget/toaster/mod.rs index efd93a9d..bafaa9f9 100644 --- a/src/widget/toaster/mod.rs +++ b/src/widget/toaster/mod.rs @@ -34,10 +34,10 @@ pub fn toaster<'a, Message: Clone + 'static>( } = theme.cosmic().spacing; let make_toast = move |(id, toast): (ToastId, &'a Toast)| { - let row = row() + let row = row::with_capacity(2) .push(text(&toast.message)) .push( - row() + row::with_capacity(2) .push_maybe(toast.action.as_ref().map(|action| { button::text(&action.description).on_press((action.message)(id)) })) From 1d01054993862f615adb029379307cd5501c79f7 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Fri, 3 Apr 2026 17:05:24 -0600 Subject: [PATCH 086/116] chore: update iced pulls in fixes for cycling focus --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 2d4ede15..ed9ad80e 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 2d4ede1597860db0bfaccfbc0166ee89ac353fc2 +Subproject commit ed9ad80e18fdaa442a60f9cfce5b8841e19e9ef3 From 8e3672a7dd6aa2fb8d663b1379fa80afdd1ab75b Mon Sep 17 00:00:00 2001 From: KENZ Date: Sun, 5 Apr 2026 14:33:23 +0900 Subject: [PATCH 087/116] fix: focus detecting in IME logic --- src/widget/text_input/input.rs | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index cd93a7d7..12fd731b 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -2095,7 +2095,7 @@ pub fn update<'a, Message: Clone + 'static>( return; } input_method::Event::Preedit(content, selection) => { - if state.is_focused.is_some() { + if state.is_focused() { state.preedit = Some(input_method::Preedit { content: content.to_owned(), selection: selection.clone(), @@ -2106,7 +2106,7 @@ pub fn update<'a, Message: Clone + 'static>( } } input_method::Event::Commit(text) => { - let Some(focus) = &mut state.is_focused else { + let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) else { return; }; let Some(on_input) = on_input else { @@ -2337,8 +2337,7 @@ fn input_method<'b>( text_layout: Layout<'_>, value: &Value, ) -> InputMethod<&'b str> { - if state.is_focused() { - } else { + if !state.is_focused() { return InputMethod::Disabled; }; From ab3eedd0f2e2ed7de9108ba6728261d8bae9e48d Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 6 Apr 2026 09:40:43 -0600 Subject: [PATCH 088/116] chore: update iced This pulls in the fix in cosmic-text to fallback to the default SansSerif if there are missing glyphs in basic shaping. Also removes advanced-shaping from the default features list. --- Cargo.toml | 1 - iced | 2 +- 2 files changed, 1 insertion(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 78922132..83fe90f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -9,7 +9,6 @@ name = "cosmic" [features] default = [ - "advanced-shaping", "winit", "tokio", "a11y", diff --git a/iced b/iced index ed9ad80e..7fd263d9 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit ed9ad80e18fdaa442a60f9cfce5b8841e19e9ef3 +Subproject commit 7fd263d99e6ae1b07e51f25bda3367f7463806b1 From 9aa87cd66b94b3d7d4dc2047e9c85b93f968d1d0 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 6 Apr 2026 18:16:32 -0400 Subject: [PATCH 089/116] fix(segmented_button): active font for context menu & prioritize active font over hover --- src/widget/segmented_button/widget.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index b9d1000e..a2efdfb8 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -246,12 +246,13 @@ where fn update_entity_paragraph(&mut self, state: &mut LocalState, key: Entity) { if let Some(text) = self.model.text.get(key) { - let font = if self.button_is_focused(state, key) { + let font = if self.button_is_focused(state, key) + || state.show_context == Some(key) + || self.model.is_active(key) + { self.font_active - } else if state.show_context == Some(key) || self.button_is_hovered(state, key) { + } else if self.button_is_hovered(state, key) { self.font_hovered - } else if self.model.is_active(key) { - self.font_active } else { self.font_inactive }; From 1f87cbc88320e540db408d65b763a4bed675b93e Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 6 Apr 2026 23:04:49 -0600 Subject: [PATCH 090/116] fix: do not allow cursor or keyboard activity when popup is open traps Tab from escaping, and won't allow elements in the background to react to hover --- src/widget/popover.rs | 20 ++++++++++++++++++-- 1 file changed, 18 insertions(+), 2 deletions(-) diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 7a82cd86..af5370a8 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -138,6 +138,10 @@ where renderer: &Renderer, operation: &mut dyn Operation, ) { + // Skip operating on background content, prevents Tab from escaping + if self.modal && self.popup.is_some() { + return; + } self.content .as_widget_mut() .operate(content_tree_mut(tree), layout, renderer, operation); @@ -172,11 +176,17 @@ where } } + // Hide cursor from background content when modal popup is active + let cursor = if self.modal && self.popup.is_some() { + mouse::Cursor::Unavailable + } else { + cursor_position + }; self.content.as_widget_mut().update( &mut tree.children[0], event, layout, - cursor_position, + cursor, renderer, clipboard, shell, @@ -214,13 +224,19 @@ where cursor_position: mouse::Cursor, viewport: &Rectangle, ) { + // Hide cursor from background content when a modal popup is active + let cursor = if self.modal && self.popup.is_some() { + mouse::Cursor::Unavailable + } else { + cursor_position + }; self.content.as_widget().draw( content_tree(tree), renderer, theme, renderer_style, layout, - cursor_position, + cursor, viewport, ); } From 724351727a191516ca1b2f2f90a00b7d211c7e1f Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 6 Apr 2026 22:56:18 -0600 Subject: [PATCH 091/116] feat: select until char and double click select delimiter adds a feature to select from the start of the sentence until the last occurrence of a character. This can be used to select until the extension in cosmic-files save dialog or rename pop up. Also, it adds a feature to select until the last occurrence of a character on double-click. --- src/widget/text_input/input.rs | 45 +++++++++++++++++++++++++++++++--- src/widget/text_input/value.rs | 8 ++++++ 2 files changed, 49 insertions(+), 4 deletions(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 12fd731b..806ceda0 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -188,6 +188,7 @@ pub struct TextInput<'a, Message> { is_editable_variant: bool, is_read_only: bool, select_on_focus: bool, + double_click_select_delimiter: Option, font: Option<::Font>, width: Length, padding: Padding, @@ -238,6 +239,7 @@ where is_editable_variant: false, is_read_only: false, select_on_focus: false, + double_click_select_delimiter: None, font: None, width: Length::Fill, padding: spacing.into(), @@ -343,6 +345,17 @@ where self } + /// Sets a delimiter character for double-click selection behavior. + /// + /// When set, double-clicking before the last occurrence of this character + /// selects from the start to that character. Double-clicking after the + /// delimiter uses normal word selection. + #[inline] + pub const fn double_click_select_delimiter(mut self, delimiter: char) -> Self { + self.double_click_select_delimiter = Some(delimiter); + self + } + /// Emits a message when an unfocused text input has been focused by click. /// /// This will not trigger if the input was focused externally by the application. @@ -598,6 +611,7 @@ where self.value = state.tracked_value.clone(); // std::mem::swap(&mut state.tracked_value, &mut self.value); } + state.double_click_select_delimiter = self.double_click_select_delimiter; // Unfocus text input if it becomes disabled if self.on_input.is_none() && !self.manage_value { state.last_click = None; @@ -1180,6 +1194,14 @@ pub fn select_range(id: Id, start: usize, end: usize) -> Task< ))) } +/// Produces a [`Task`] that selects from the front to the last occurrence of the given character +/// in the [`TextInput`] with the given [`Id`], or selects all if not found. +pub fn select_until_last(id: Id, value: &str, ch: char) -> Task { + let v = Value::new(value); + let end = v.rfind_char(ch).unwrap_or(v.len()); + select_range(id, 0, end) +} + /// Computes the layout of a [`TextInput`]. #[allow(clippy::cast_precision_loss)] #[allow(clippy::too_many_arguments)] @@ -1600,10 +1622,23 @@ pub fn update<'a, Message: Clone + 'static>( .unwrap_or((0, text::Affinity::Before)); state.cursor.set_affinity(affinity); - state.cursor.select_range( - value.previous_start_of_word(position), - value.next_end_of_word(position), - ); + + if let Some(delimiter) = state.double_click_select_delimiter { + if let Some(delim_pos) = value.rfind_char(delimiter) { + if position <= delim_pos { + state.cursor.select_range(0, delim_pos); + } else { + state.cursor.select_range(delim_pos + 1, value.len()); + } + } else { + state.cursor.select_all(value); + } + } else { + state.cursor.select_range( + value.previous_start_of_word(position), + value.next_end_of_word(position), + ); + } } state.dragging_state = Some(DraggingState::Selection); } @@ -2882,6 +2917,7 @@ pub struct State { pub is_read_only: bool, pub emit_unfocus: bool, select_on_focus: bool, + double_click_select_delimiter: Option, is_focused: Option, dragging_state: Option, dnd_offer: DndOfferState, @@ -2963,6 +2999,7 @@ impl State { emit_unfocus: false, is_focused: None, select_on_focus: false, + double_click_select_delimiter: None, dragging_state: None, dnd_offer: DndOfferState::default(), is_pasting: None, diff --git a/src/widget/text_input/value.rs b/src/widget/text_input/value.rs index 9faff4ac..3f7b8d73 100644 --- a/src/widget/text_input/value.rs +++ b/src/widget/text_input/value.rs @@ -142,6 +142,14 @@ impl Value { .sum() } + /// Returns the grapheme index of the last occurrence of the given character, + /// searching from the end. + #[must_use] + pub fn rfind_char(&self, ch: char) -> Option { + let needle = ch.to_string(); + self.graphemes.iter().rposition(|g| g == &needle) + } + /// Converts a byte index to a grapheme index. #[must_use] pub fn grapheme_index_at_byte(&self, byte_index: usize) -> usize { From b963fbfea9a94316dd1a0d99e84a5116cf696853 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Tue, 7 Apr 2026 11:02:58 -0400 Subject: [PATCH 092/116] feat(widget): progress bars --- examples/application/src/main.rs | 50 +++ src/widget/mod.rs | 9 +- src/widget/progress_bar/circular.rs | 453 ++++++++++++++++++++++++++ src/widget/progress_bar/linear.rs | 306 +++++++++++++++++ src/widget/progress_bar/mod.rs | 11 + src/widget/progress_bar/style.rs | 105 ++++++ src/widget/segmented_button/widget.rs | 9 +- 7 files changed, 935 insertions(+), 8 deletions(-) create mode 100644 src/widget/progress_bar/circular.rs create mode 100644 src/widget/progress_bar/linear.rs create mode 100644 src/widget/progress_bar/mod.rs create mode 100644 src/widget/progress_bar/style.rs diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 53f1c28e..bceece6e 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -82,6 +82,7 @@ pub enum Message { Hi, Hi2, Hi3, + Tick, } /// The [`App`] stores application-specific state. @@ -92,6 +93,7 @@ pub struct App { input_2: String, hidden: bool, keybinds: HashMap, + progress: f32, } /// Implement [`cosmic::Application`] to integrate with COSMIC. @@ -133,6 +135,7 @@ impl cosmic::Application for App { input_2: String::new(), hidden: true, keybinds: HashMap::new(), + progress: 0.0, }; let command = app.update_title(); @@ -178,10 +181,17 @@ impl cosmic::Application for App { Message::Hi3 => { dbg!("hi 3"); } + Message::Tick => { + self.progress = (self.progress + 0.01) % 1.0; + } } Task::none() } + fn subscription(&self) -> iced::Subscription { + iced::time::every(std::time::Duration::from_millis(64)).map(|_| Message::Tick) + } + /// Creates a view after each update. fn view(&self) -> Element<'_, Self::Message> { let page_content = self @@ -212,6 +222,46 @@ impl cosmic::Application for App { .on_input(Message::Input2) .on_clear(Message::Ignore), ) + .push(widget::progress_bar::circular::Circular::new().size(50.0)) + .push( + widget::progress_bar::linear::Linear::new() + .girth(10.0) + .width(Length::Fill), + ) + .push( + widget::progress_bar::circular::Circular::new() + .bar_height(10.0) + .size(50.0) + .progress(self.progress), + ) + .push( + widget::progress_bar::linear::Linear::new() + .girth(10.0) + .progress(self.progress) + .width(Length::Fill), + ) + .push( + widget::progress_bar::circular::Circular::new() + .size(50.0) + .progress(0.0), + ) + .push( + widget::progress_bar::linear::Linear::new() + .girth(10.0) + .progress(0.0) + .width(Length::Fill), + ) + .push( + widget::progress_bar::circular::Circular::new() + .size(50.0) + .progress(1.0), + ) + .push( + widget::progress_bar::linear::Linear::new() + .girth(10.0) + .progress(1.0) + .width(Length::Fill), + ) .spacing(cosmic::theme::spacing().space_s) .width(Length::Fill) .height(Length::Shrink) diff --git a/src/widget/mod.rs b/src/widget/mod.rs index ef212dab..7dcfa233 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -77,9 +77,6 @@ pub use iced::widget::{MouseArea, mouse_area}; #[doc(inline)] pub use iced::widget::{PaneGrid, pane_grid}; -#[doc(inline)] -pub use iced::widget::{ProgressBar, progress_bar}; - #[doc(inline)] pub use iced::widget::{Responsive, responsive}; @@ -257,6 +254,12 @@ pub mod popover; #[doc(inline)] pub use popover::{Popover, popover}; +pub mod progress_bar; +#[doc(inline)] +pub use progress_bar::{ + circular, circular::Circular, circular_progress, linear, linear::Linear, linear_progress, style, +}; + pub mod radio; #[doc(inline)] pub use radio::{Radio, radio}; diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs new file mode 100644 index 00000000..7e8177d6 --- /dev/null +++ b/src/widget/progress_bar/circular.rs @@ -0,0 +1,453 @@ +//! Show a circular progress indicator. +use super::style::StyleSheet; +use crate::anim::smootherstep; +use iced::advanced::layout; +use iced::advanced::renderer; +use iced::advanced::widget::tree::{self, Tree}; +use iced::advanced::{self, Clipboard, Layout, Shell, Widget}; +use iced::mouse; +use iced::time::Instant; +use iced::widget::canvas; +use iced::window; +use iced::{Element, Event, Length, Radians, Rectangle, Renderer, Size, Vector}; + +use std::f32::consts::PI; +use std::time::Duration; + +const MIN_ANGLE: Radians = Radians(PI / 8.0); +const WRAP_ANGLE: Radians = Radians(2.0 * PI - PI / 4.0); +const BASE_ROTATION_SPEED: u32 = u32::MAX / 80; + +#[must_use] +pub struct Circular +where + Theme: StyleSheet, +{ + size: f32, + bar_height: f32, + style: ::Style, + cycle_duration: Duration, + rotation_duration: Duration, + progress: Option, +} + +impl Circular +where + Theme: StyleSheet, +{ + /// Creates a new [`Circular`] with the given content. + pub fn new() -> Self { + Circular { + size: 40.0, + bar_height: 4.0, + style: ::Style::default(), + cycle_duration: Duration::from_millis(1500), + rotation_duration: Duration::from_secs(2), + progress: None, + } + } + + /// Sets the size of the [`Circular`]. + pub fn size(mut self, size: f32) -> Self { + self.size = size; + self + } + + /// Sets the bar height of the [`Circular`]. + pub fn bar_height(mut self, bar_height: f32) -> Self { + self.bar_height = bar_height; + self + } + + /// Sets the style variant of this [`Circular`]. + pub fn style(mut self, style: ::Style) -> Self { + self.style = style; + self + } + + /// Sets the cycle duration of this [`Circular`]. + pub fn cycle_duration(mut self, duration: Duration) -> Self { + self.cycle_duration = duration / 2; + self + } + + /// Sets the base rotation duration of this [`Circular`]. This is the duration that a full + /// rotation would take if the cycle rotation were set to 0.0 (no expanding or contracting) + pub fn rotation_duration(mut self, duration: Duration) -> Self { + self.rotation_duration = duration; + self + } + + /// Override the default behavior by providing a determinate progress value between `0.0` and `1.0`. + pub fn progress(mut self, progress: f32) -> Self { + self.progress = Some(progress.clamp(0.0, 1.0)); + self + } +} + +impl Default for Circular +where + Theme: StyleSheet, +{ + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Copy)] +enum Animation { + Expanding { + start: Instant, + progress: f32, + rotation: u32, + last: Instant, + }, + Contracting { + start: Instant, + progress: f32, + rotation: u32, + last: Instant, + }, +} + +impl Default for Animation { + fn default() -> Self { + Self::Expanding { + start: Instant::now(), + progress: 0.0, + rotation: 0, + last: Instant::now(), + } + } +} + +impl Animation { + fn next(&self, additional_rotation: u32, now: Instant) -> Self { + match self { + Self::Expanding { rotation, .. } => Self::Contracting { + start: now, + progress: 0.0, + rotation: rotation.wrapping_add(additional_rotation), + last: now, + }, + Self::Contracting { rotation, .. } => Self::Expanding { + start: now, + progress: 0.0, + rotation: rotation.wrapping_add(BASE_ROTATION_SPEED.wrapping_add( + (f64::from(WRAP_ANGLE / (2.0 * Radians::PI)) * f64::from(u32::MAX)) as u32, + )), + last: now, + }, + } + } + + fn start(&self) -> Instant { + match self { + Self::Expanding { start, .. } | Self::Contracting { start, .. } => *start, + } + } + + fn last(&self) -> Instant { + match self { + Self::Expanding { last, .. } | Self::Contracting { last, .. } => *last, + } + } + + fn timed_transition( + &self, + cycle_duration: Duration, + rotation_duration: Duration, + now: Instant, + ) -> Self { + let elapsed = now.duration_since(self.start()); + let additional_rotation = ((now - self.last()).as_secs_f32() + / rotation_duration.as_secs_f32() + * (u32::MAX) as f32) as u32; + + match elapsed { + elapsed if elapsed > cycle_duration => self.next(additional_rotation, now), + _ => self.with_elapsed(cycle_duration, additional_rotation, elapsed, now), + } + } + + fn with_elapsed( + &self, + cycle_duration: Duration, + additional_rotation: u32, + elapsed: Duration, + now: Instant, + ) -> Self { + let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32(); + match self { + Self::Expanding { + start, rotation, .. + } => Self::Expanding { + start: *start, + progress, + rotation: rotation.wrapping_add(additional_rotation), + last: now, + }, + Self::Contracting { + start, rotation, .. + } => Self::Contracting { + start: *start, + progress, + rotation: rotation.wrapping_add(additional_rotation), + last: now, + }, + } + } + + fn rotation(&self) -> f32 { + match self { + Self::Expanding { rotation, .. } | Self::Contracting { rotation, .. } => { + *rotation as f32 / u32::MAX as f32 + } + } + } +} + +#[derive(Default)] +struct State { + animation: Animation, + cache: canvas::Cache, + progress: Option, +} + +impl Widget for Circular +where + Message: Clone, + Theme: StyleSheet, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn size(&self) -> Size { + Size { + width: Length::Fixed(self.size), + height: Length::Fixed(self.size), + } + } + + fn layout( + &mut self, + _tree: &mut Tree, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout::atomic(limits, self.size, self.size) + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + _layout: Layout<'_>, + _cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) { + let state = tree.state.downcast_mut::(); + if self.progress.is_some() { + if !float_cmp::approx_eq!( + f32, + state.progress.unwrap_or_default(), + self.progress.unwrap_or_default() + ) { + state.progress = self.progress; + state.cache.clear(); + } + return; + } + if let Event::Window(window::Event::RedrawRequested(now)) = event { + state.animation = + state + .animation + .timed_transition(self.cycle_duration, self.rotation_duration, *now); + + state.cache.clear(); + shell.request_redraw(); + } + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + use advanced::Renderer as _; + + let state = tree.state.downcast_ref::(); + let bounds = layout.bounds(); + let custom_style = + ::appearance(theme, &self.style, self.progress.is_some(), true); + + let geometry = state.cache.draw(renderer, bounds.size(), |frame| { + let track_radius = frame.width() / 2.0 - self.bar_height; + let track_path = canvas::Path::circle(frame.center(), track_radius); + + frame.stroke( + &track_path, + canvas::Stroke::default() + .with_color(custom_style.track_color) + .with_width(self.bar_height), + ); + + if let Some(progress) = self.progress { + // outer border + if let Some(border_color) = custom_style.border_color { + let border_path = + canvas::Path::circle(frame.center(), track_radius + self.bar_height / 2.0); + + frame.stroke( + &border_path, + canvas::Stroke::default() + .with_color(border_color) + .with_width(1.0), + ); + } + + // inner border + if let Some(border_color) = custom_style.border_color { + let border_path = + canvas::Path::circle(frame.center(), track_radius - self.bar_height / 2.0); + + frame.stroke( + &border_path, + canvas::Stroke::default() + .with_color(border_color) + .with_width(1.0), + ); + } + + // bar + let mut builder = canvas::path::Builder::new(); + + builder.arc(canvas::path::Arc { + center: frame.center(), + radius: track_radius, + start_angle: Radians(-PI / 2.0), + end_angle: Radians(-PI / 2.0 + progress * 2.0 * PI), + }); + + let bar_path = builder.build(); + + frame.stroke( + &bar_path, + canvas::Stroke::default() + .with_color(custom_style.bar_color) + .with_width(self.bar_height), + ); + + let mut builder = canvas::path::Builder::new(); + + // get center of end of arc for rounded cap + let end_angle = -PI / 2.0 + progress * 2.0 * PI; + let end_center = + frame.center() + Vector::new(end_angle.cos(), end_angle.sin()) * track_radius; + builder.arc(canvas::path::Arc { + center: end_center, + radius: self.bar_height / 2.0, + start_angle: Radians(end_angle), + end_angle: Radians(end_angle + PI), + }); + + // get center of start of arc for rounded cap + let start_angle = -PI / 2.0; + let start_center = frame.center() + + Vector::new(start_angle.cos(), start_angle.sin()) * track_radius; + builder.arc(canvas::path::Arc { + center: start_center, + radius: self.bar_height / 2.0, + start_angle: Radians(start_angle - PI), + end_angle: Radians(start_angle), + }); + + let cap_path = builder.build(); + frame.fill(&cap_path, custom_style.bar_color); + } else { + let mut builder = canvas::path::Builder::new(); + + let start = Radians(state.animation.rotation() * 2.0 * PI); + let (start_angle, end_angle) = match state.animation { + Animation::Expanding { progress, .. } => ( + start, + start + MIN_ANGLE + WRAP_ANGLE * (smootherstep(progress)), + ), + Animation::Contracting { progress, .. } => ( + start + WRAP_ANGLE * (smootherstep(progress)), + start + MIN_ANGLE + WRAP_ANGLE, + ), + }; + builder.arc(canvas::path::Arc { + center: frame.center(), + radius: track_radius, + start_angle, + end_angle, + }); + + let bar_path = builder.build(); + + frame.stroke( + &bar_path, + canvas::Stroke::default() + .with_color(custom_style.bar_color) + .with_width(self.bar_height), + ); + + let mut builder = canvas::path::Builder::new(); + + // get center of end of arc for rounded cap + let end_center = frame.center() + + Vector::new(end_angle.0.cos(), end_angle.0.sin()) * track_radius; + builder.arc(canvas::path::Arc { + center: end_center, + radius: self.bar_height / 2.0, + start_angle: Radians(end_angle.0), + end_angle: Radians(end_angle.0 + PI), + }); + + // get center of start of arc for rounded cap + let start_center = frame.center() + + Vector::new(start_angle.0.cos(), start_angle.0.sin()) * track_radius; + builder.arc(canvas::path::Arc { + center: start_center, + radius: self.bar_height / 2.0, + start_angle: Radians(start_angle.0 - PI), + end_angle: Radians(start_angle.0), + }); + + let cap_path = builder.build(); + frame.fill(&cap_path, custom_style.bar_color); + } + }); + + renderer.with_translation(Vector::new(bounds.x, bounds.y), |renderer| { + use iced::advanced::graphics::geometry::Renderer as _; + + renderer.draw_geometry(geometry); + }); + } +} + +impl<'a, Message, Theme> From> for Element<'a, Message, Theme, Renderer> +where + Message: Clone + 'a, + Theme: StyleSheet + 'a, +{ + fn from(circular: Circular) -> Self { + Self::new(circular) + } +} diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs new file mode 100644 index 00000000..226b2b5f --- /dev/null +++ b/src/widget/progress_bar/linear.rs @@ -0,0 +1,306 @@ +//! Show a linear progress indicator. +use iced::advanced::layout; +use iced::advanced::renderer::{self, Quad}; +use iced::advanced::widget::tree::{self, Tree}; +use iced::advanced::{self, Clipboard, Layout, Shell, Widget}; +use iced::mouse; +use iced::time::Instant; +use iced::window; +use iced::{Background, Element, Event, Length, Rectangle, Size}; + +use crate::anim::smootherstep; + +use super::style::StyleSheet; + +use std::time::Duration; + +#[must_use] +pub struct Linear +where + Theme: StyleSheet, +{ + width: Length, + girth: Length, + style: Theme::Style, + cycle_duration: Duration, + progress: Option, +} + +impl Linear +where + Theme: StyleSheet, +{ + /// Creates a new [`Linear`] with the given content. + pub fn new() -> Self { + Linear { + width: Length::Fixed(100.0), + girth: Length::Fixed(4.0), + style: Theme::Style::default(), + cycle_duration: Duration::from_millis(1500), + progress: None, + } + } + + /// Sets the width of the [`Linear`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the girth of the [`Linear`]. + pub fn girth(mut self, girth: impl Into) -> Self { + self.girth = girth.into(); + self + } + + /// Sets the style variant of this [`Linear`]. + pub fn style(mut self, style: impl Into) -> Self { + self.style = style.into(); + self + } + + /// Sets the cycle duration of this [`Linear`]. + pub fn cycle_duration(mut self, duration: Duration) -> Self { + self.cycle_duration = duration / 2; + self + } + + /// Override the default behavior by providing a determinate progress value between `0.0` and `1.0`. + pub fn progress(mut self, progress: f32) -> Self { + self.progress = Some(progress.clamp(0.0, 1.0)); + self + } +} + +impl Default for Linear +where + Theme: StyleSheet, +{ + fn default() -> Self { + Self::new() + } +} + +#[derive(Clone, Copy)] +enum State { + Expanding { start: Instant, progress: f32 }, + Contracting { start: Instant, progress: f32 }, +} + +impl Default for State { + fn default() -> Self { + Self::Expanding { + start: Instant::now(), + progress: 0.0, + } + } +} + +impl State { + fn next(&self, now: Instant) -> Self { + match self { + Self::Expanding { .. } => Self::Contracting { + start: now, + progress: 0.0, + }, + Self::Contracting { .. } => Self::Expanding { + start: now, + progress: 0.0, + }, + } + } + + fn start(&self) -> Instant { + match self { + Self::Expanding { start, .. } | Self::Contracting { start, .. } => *start, + } + } + + fn timed_transition(&self, cycle_duration: Duration, now: Instant) -> Self { + let elapsed = now.duration_since(self.start()); + + match elapsed { + elapsed if elapsed > cycle_duration => self.next(now), + _ => self.with_elapsed(cycle_duration, elapsed), + } + } + + fn with_elapsed(&self, cycle_duration: Duration, elapsed: Duration) -> Self { + let progress = elapsed.as_secs_f32() / cycle_duration.as_secs_f32(); + match self { + Self::Expanding { start, .. } => Self::Expanding { + start: *start, + progress, + }, + Self::Contracting { start, .. } => Self::Contracting { + start: *start, + progress, + }, + } + } +} + +impl Widget for Linear +where + Message: Clone, + Theme: StyleSheet, + Renderer: advanced::Renderer, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::() + } + + fn state(&self) -> tree::State { + tree::State::new(State::default()) + } + + fn size(&self) -> Size { + Size { + width: self.width, + height: self.girth, + } + } + + fn layout( + &mut self, + _tree: &mut Tree, + _renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout::atomic(limits, self.width, self.girth) + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + _layout: Layout<'_>, + _cursor: mouse::Cursor, + _renderer: &Renderer, + _clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + _viewport: &Rectangle, + ) { + if self.progress.is_some() { + return; + } + + let state = tree.state.downcast_mut::(); + + if let Event::Window(window::Event::RedrawRequested(now)) = event { + *state = state.timed_transition(self.cycle_duration, *now); + + shell.request_redraw(); + } + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + _style: &renderer::Style, + layout: Layout<'_>, + _cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let custom_style = theme.appearance(&self.style, self.progress.is_some(), false); + let state = tree.state.downcast_ref::(); + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }, + border: iced::Border { + width: if custom_style.border_color.is_some() { + 1.0 + } else { + 0.0 + }, + color: custom_style.border_color.unwrap_or(custom_style.bar_color), + radius: custom_style.border_radius.into(), + }, + snap: true, + ..renderer::Quad::default() + }, + Background::Color(custom_style.track_color), + ); + + if let Some(progress) = self.progress { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: progress * bounds.width, + height: bounds.height, + }, + border: iced::Border { + width: 0., + color: iced::Color::TRANSPARENT, + radius: custom_style.border_radius.into(), + }, + snap: true, + ..renderer::Quad::default() + }, + Background::Color(custom_style.bar_color), + ); + } else { + match state { + State::Expanding { progress, .. } => renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: smootherstep(*progress) * bounds.width, + height: bounds.height, + }, + border: iced::Border { + width: 0., + color: iced::Color::TRANSPARENT, + radius: custom_style.border_radius.into(), + }, + snap: true, + ..renderer::Quad::default() + }, + Background::Color(custom_style.bar_color), + ), + + State::Contracting { progress, .. } => renderer.fill_quad( + Quad { + bounds: Rectangle { + x: bounds.x + smootherstep(*progress) * bounds.width, + y: bounds.y, + width: (1.0 - smootherstep(*progress)) * bounds.width, + height: bounds.height, + }, + border: iced::Border { + width: 0., + color: iced::Color::TRANSPARENT, + radius: custom_style.border_radius.into(), + }, + snap: true, + ..renderer::Quad::default() + }, + Background::Color(custom_style.bar_color), + ), + } + } + } +} + +impl<'a, Message, Theme, Renderer> From> for Element<'a, Message, Theme, Renderer> +where + Message: Clone + 'a, + Theme: StyleSheet + 'a, + Renderer: iced::advanced::Renderer + 'a, +{ + fn from(linear: Linear) -> Self { + Self::new(linear) + } +} diff --git a/src/widget/progress_bar/mod.rs b/src/widget/progress_bar/mod.rs new file mode 100644 index 00000000..c1230961 --- /dev/null +++ b/src/widget/progress_bar/mod.rs @@ -0,0 +1,11 @@ +pub mod circular; +pub mod linear; +pub mod style; + +pub fn circular_progress() -> circular::Circular { + circular::Circular::new() +} + +pub fn linear_progress() -> linear::Linear { + linear::Linear::new() +} diff --git a/src/widget/progress_bar/style.rs b/src/widget/progress_bar/style.rs new file mode 100644 index 00000000..db2fe64d --- /dev/null +++ b/src/widget/progress_bar/style.rs @@ -0,0 +1,105 @@ +use iced::Color; + +#[derive(Debug, Clone, Copy)] +pub struct Appearance { + /// The track [`Color`] of the progress indicator. + pub track_color: Color, + /// The bar [`Color`] of the progress indicator. + pub bar_color: Color, + /// The border [`Color`] of the progress indicator. + pub border_color: Option, + /// The border radius of the progress indicator. + pub border_radius: f32, +} + +impl std::default::Default for Appearance { + fn default() -> Self { + Self { + track_color: Color::TRANSPARENT, + bar_color: Color::BLACK, + border_color: None, + border_radius: 0.0, + } + } +} + +/// A set of rules that dictate the style of an indicator. +pub trait StyleSheet { + /// The supported style of the [`StyleSheet`]. + type Style: Default; + + /// Produces the active [`Appearance`] of a indicator. + fn appearance( + &self, + style: &Self::Style, + is_determinate: bool, + is_circular: bool, + ) -> Appearance; +} + +impl StyleSheet for iced::Theme { + type Style = (); + + fn appearance( + &self, + _style: &Self::Style, + _is_determinate: bool, + _is_circular: bool, + ) -> Appearance { + let palette = self.extended_palette(); + + Appearance { + track_color: palette.background.weak.color, + bar_color: palette.primary.base.color, + border_color: None, + border_radius: 0.0, + } + } +} + +impl StyleSheet for crate::Theme { + type Style = (); + + fn appearance( + &self, + _style: &Self::Style, + is_determinate: bool, + is_circular: bool, + ) -> Appearance { + let cur = self.current_container(); + let mut cur_divider = cur.divider; + cur_divider.alpha = 0.5; + let theme = self.cosmic(); + + let (mut track_color, bar_color) = if theme.is_dark && theme.is_high_contrast { + ( + theme.palette.neutral_6.into(), + theme.accent_text_color().into(), + ) + } else if theme.is_dark { + (theme.palette.neutral_5.into(), theme.accent_color().into()) + } else if theme.is_high_contrast { + ( + theme.palette.neutral_4.into(), + theme.accent_text_color().into(), + ) + } else { + (theme.palette.neutral_3.into(), theme.accent_color().into()) + }; + + if !is_determinate && is_circular { + track_color = Color::TRANSPARENT; + } + + Appearance { + track_color, + bar_color, + border_color: if is_determinate && theme.is_high_contrast { + Some(cur_divider.into()) + } else { + None + }, + border_radius: theme.corner_radii.radius_xl[0], + } + } +} diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index a2efdfb8..5d862e9f 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -262,10 +262,10 @@ where font.hash(&mut hasher); let text_hash = hasher.finish(); - if let Some(prev_hash) = state.text_hashes.insert(key, text_hash) { - if prev_hash == text_hash { - return; - } + if let Some(prev_hash) = state.text_hashes.insert(key, text_hash) + && prev_hash == text_hash + { + return; } if let Some(paragraph) = state.paragraphs.get_mut(key) { @@ -928,7 +928,6 @@ where fn diff(&mut self, tree: &mut Tree) { let state = tree.state.downcast_mut::(); - for key in self.model.order.iter().copied() { self.update_entity_paragraph(state, key); } From d9121d6f0dfff4116eea096459151d051befe1da Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 7 Apr 2026 15:37:13 -0400 Subject: [PATCH 093/116] refactor: better helpers for the progress_bar --- src/widget/mod.rs | 3 ++- src/widget/progress_bar/mod.rs | 16 ++++++++++++++-- 2 files changed, 16 insertions(+), 3 deletions(-) diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 7dcfa233..f442b0da 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -257,7 +257,8 @@ pub use popover::{Popover, popover}; pub mod progress_bar; #[doc(inline)] pub use progress_bar::{ - circular, circular::Circular, circular_progress, linear, linear::Linear, linear_progress, style, + circular, circular::Circular, determinate_circular, determinate_linear, indeterminate_circular, + indeterminate_linear, linear, linear::Linear, style, }; pub mod radio; diff --git a/src/widget/progress_bar/mod.rs b/src/widget/progress_bar/mod.rs index c1230961..ea069ffc 100644 --- a/src/widget/progress_bar/mod.rs +++ b/src/widget/progress_bar/mod.rs @@ -2,10 +2,22 @@ pub mod circular; pub mod linear; pub mod style; -pub fn circular_progress() -> circular::Circular { +/// A spinner / throbber widget that can be used to indicate that some operation is in progress. +pub fn indeterminate_circular() -> circular::Circular { circular::Circular::new() } -pub fn linear_progress() -> linear::Linear { +/// A linear throbber widget that can be used to indicate that some operation is in progress. +pub fn indeterminate_linear() -> linear::Linear { linear::Linear::new() } + +/// A circular progress spinner widget that can be used to indicate the progress of some operation. +pub fn determinate_circular(progress: f32) -> circular::Circular { + circular::Circular::new().progress(progress) +} + +/// A linear progress bar widget that can be used to indicate the progress of some operation. +pub fn determinate_linear(progress: f32) -> linear::Linear { + linear::Linear::new().progress(progress) +} From 5d1dfc4c54eba5d1037293fc0bc2ebfdc78eaab3 Mon Sep 17 00:00:00 2001 From: Adam Cosner <160804448+Adam-Cosner@users.noreply.github.com> Date: Wed, 8 Apr 2026 01:12:10 +0000 Subject: [PATCH 094/116] refactor!: remove `cosmic::iced_*` re-exports --- examples/applet/src/window.rs | 4 +-- examples/context-menu/src/main.rs | 2 +- examples/menu/src/main.rs | 6 ++--- examples/multi-window/src/window.rs | 4 +-- examples/nav-context/src/main.rs | 2 +- examples/open-dialog/src/main.rs | 2 +- examples/table-view/src/main.rs | 2 +- src/applet/mod.rs | 16 ++++++------ src/applet/token/subscription.rs | 2 +- src/lib.rs | 23 ----------------- src/widget/calendar.rs | 2 +- src/widget/cards.rs | 9 +++---- src/widget/dnd_destination.rs | 29 ++++++++++----------- src/widget/dnd_source.rs | 18 +++++++------- src/widget/menu/menu_tree.rs | 2 +- src/widget/segmented_button/widget.rs | 6 ++--- src/widget/toggler.rs | 12 ++++----- src/widget/wrapper.rs | 36 +++++++++++++-------------- 18 files changed, 77 insertions(+), 100 deletions(-) diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 4e05c70a..22903eac 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -1,8 +1,8 @@ use cosmic::app::{Core, Task}; +use cosmic::iced::core::window; use cosmic::iced::window::Id; use cosmic::iced::{Length, Rectangle}; -use cosmic::iced_runtime::core::window; use cosmic::surface::action::{app_popup, destroy_popup}; use cosmic::widget::{dropdown::popup_dropdown, list_column, settings, toggler}; use cosmic::Element; @@ -159,7 +159,7 @@ impl cosmic::Application for Window { "oops".into() } - fn style(&self) -> Option { + fn style(&self) -> Option { Some(cosmic::applet::style()) } } diff --git a/examples/context-menu/src/main.rs b/examples/context-menu/src/main.rs index db66ba1b..e5ca5878 100644 --- a/examples/context-menu/src/main.rs +++ b/examples/context-menu/src/main.rs @@ -4,7 +4,7 @@ //! Application API example use cosmic::app::{Core, Settings, Task}; -use cosmic::iced_core::Size; +use cosmic::iced::Size; use cosmic::widget::menu; use cosmic::{executor, iced, ApplicationExt, Element}; use std::collections::HashMap; diff --git a/examples/menu/src/main.rs b/examples/menu/src/main.rs index 8b5a1cb7..da0c3231 100644 --- a/examples/menu/src/main.rs +++ b/examples/menu/src/main.rs @@ -7,10 +7,10 @@ use std::collections::HashMap; use std::{env, process}; use cosmic::app::{Core, Settings, Task}; +use cosmic::iced::alignment::{Horizontal, Vertical}; +use cosmic::iced::keyboard::Key; use cosmic::iced::window; -use cosmic::iced_core::alignment::{Horizontal, Vertical}; -use cosmic::iced_core::keyboard::Key; -use cosmic::iced_core::{Length, Size}; +use cosmic::iced::{Length, Size}; use cosmic::widget::menu::action::MenuAction; use cosmic::widget::menu::key_bind::KeyBind; use cosmic::widget::menu::key_bind::Modifier; diff --git a/examples/multi-window/src/window.rs b/examples/multi-window/src/window.rs index 74ab5386..754a0d86 100644 --- a/examples/multi-window/src/window.rs +++ b/examples/multi-window/src/window.rs @@ -2,9 +2,9 @@ use std::collections::HashMap; use cosmic::{ app::Core, + iced::core::{id, Alignment, Length, Point}, + iced::widget::{column, container, scrollable, text}, iced::{self, event, window, Subscription}, - iced_core::{id, Alignment, Length, Point}, - iced_widget::{column, container, scrollable, text}, prelude::*, widget::{button, header_bar}, }; diff --git a/examples/nav-context/src/main.rs b/examples/nav-context/src/main.rs index fdfb90f9..1992066f 100644 --- a/examples/nav-context/src/main.rs +++ b/examples/nav-context/src/main.rs @@ -6,7 +6,7 @@ use std::collections::HashMap; use cosmic::app::{Core, Settings, Task}; -use cosmic::iced_core::Size; +use cosmic::iced::Size; use cosmic::widget::{menu, nav_bar}; use cosmic::{executor, iced, ApplicationExt, Element}; diff --git a/examples/open-dialog/src/main.rs b/examples/open-dialog/src/main.rs index 29061534..b4b5343f 100644 --- a/examples/open-dialog/src/main.rs +++ b/examples/open-dialog/src/main.rs @@ -6,7 +6,7 @@ use apply::Apply; use cosmic::app::{Core, Settings, Task}; use cosmic::dialog::file_chooser::{self, FileFilter}; -use cosmic::iced_core::Length; +use cosmic::iced::Length; use cosmic::widget::button; use cosmic::{executor, iced, ApplicationExt, Element}; use std::sync::Arc; diff --git a/examples/table-view/src/main.rs b/examples/table-view/src/main.rs index bbd9cf5b..d2478429 100644 --- a/examples/table-view/src/main.rs +++ b/examples/table-view/src/main.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use chrono::Datelike; use cosmic::app::{Core, Settings, Task}; -use cosmic::iced_core::Size; +use cosmic::iced::Size; use cosmic::prelude::*; use cosmic::widget::table; use cosmic::widget::{self, nav_bar}; diff --git a/src/applet/mod.rs b/src/applet/mod.rs index a7fc4069..48721e1c 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -6,13 +6,6 @@ use crate::{ Application, Element, Renderer, app::iced_settings, cctk::sctk, - iced::{ - self, Color, Length, Limits, Rectangle, - alignment::{Alignment, Horizontal, Vertical}, - widget::Container, - window, - }, - iced_widget, theme::{self, Button, THEME, system_dark, system_light}, widget::{ self, @@ -24,8 +17,15 @@ use crate::{ space::vertical, }, }; + pub use cosmic_panel_config; use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize}; +use iced::{ + self, Color, Length, Limits, Rectangle, + alignment::{Alignment, Horizontal, Vertical}, + widget::Container, + window, +}; use iced_core::{Padding, Shadow}; use iced_runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; use iced_widget::Text; @@ -226,7 +226,7 @@ impl Context { let symbolic = icon.symbolic; let icon = widget::icon(icon) .class(if symbolic { - theme::Svg::Custom(Rc::new(|theme| crate::iced_widget::svg::Style { + theme::Svg::Custom(Rc::new(|theme| iced_widget::svg::Style { color: Some(theme.cosmic().background.on.into()), })) } else { diff --git a/src/applet/token/subscription.rs b/src/applet/token/subscription.rs index 82763303..07c528ea 100644 --- a/src/applet/token/subscription.rs +++ b/src/applet/token/subscription.rs @@ -1,11 +1,11 @@ use crate::iced; -use crate::iced_futures::futures; use cctk::sctk::reexports::calloop; use futures::{ SinkExt, StreamExt, channel::mpsc::{UnboundedReceiver, unbounded}, }; use iced::Subscription; +use iced_futures::futures; use iced_futures::stream; use std::{fmt::Debug, hash::Hash, thread::JoinHandle}; diff --git a/src/lib.rs b/src/lib.rs index aa3b7db2..e04f1609 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -66,29 +66,6 @@ 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; - -#[doc(inline)] -pub use iced_widget; - -#[doc(inline)] -#[cfg(feature = "winit")] -pub use iced_winit; - -#[doc(inline)] -#[cfg(feature = "wgpu")] -pub use iced_wgpu; - pub mod icon_theme; pub mod keyboard_nav; diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index 19758472..91c601d3 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -4,10 +4,10 @@ //! A widget that displays an interactive calendar. use crate::fl; -use crate::iced_core::{Alignment, Length}; use crate::widget::{button, column, grid, icon, row, text}; use apply::Apply; use iced::alignment::Vertical; +use iced_core::{Alignment, Length}; use jiff::{ ToSpan, civil::{Date, Weekday}, diff --git a/src/widget/cards.rs b/src/widget/cards.rs index b8e17636..66267a73 100644 --- a/src/widget/cards.rs +++ b/src/widget/cards.rs @@ -1,13 +1,8 @@ //! An expandable stack of cards use std::time::Duration; -use self::iced_core::{ - Element, Event, Length, Size, Vector, Widget, border::Radius, id::Id, layout::Node, - renderer::Quad, widget::Tree, -}; use crate::{ anim, - iced_core::{self, Border, Shadow}, widget::{ button, card::style::Style, @@ -18,6 +13,10 @@ use crate::{ }; use float_cmp::approx_eq; use iced::widget; +use iced_core::{ + Border, Element, Event, Length, Shadow, Size, Vector, Widget, border::Radius, id::Id, + layout::Node, renderer::Quad, widget::Tree, +}; use iced_core::{widget::tree, window}; const ICON_SIZE: u16 = 16; diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index a77101b9..10bf7a8b 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -7,23 +7,24 @@ use iced::Vector; use crate::{ Element, - iced::{ - Event, Length, Rectangle, - clipboard::{ - dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent}, - mime::AllowedMimeTypes, - }, - event, - id::Internal, - mouse, overlay, - }, - iced_core::{ - self, Clipboard, Shell, layout, - widget::{Tree, tree}, - }, widget::{Id, Widget}, }; +use iced::{ + Event, Length, Rectangle, + clipboard::{ + dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent}, + mime::AllowedMimeTypes, + }, + event, + id::Internal, + mouse, overlay, +}; +use iced_core::{ + self, Clipboard, Shell, layout, + widget::{Tree, tree}, +}; + pub fn dnd_destination<'a, Message: 'static>( child: impl Into>, mimes: Vec>, diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index 25900a66..980723e3 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -4,17 +4,17 @@ use iced_core::{widget::Operation, window}; use crate::{ Element, - iced::{ - Event, Length, Point, Rectangle, Vector, - clipboard::dnd::{DndAction, DndEvent, SourceEvent}, - event, mouse, overlay, - }, - iced_core::{ - self, Clipboard, Shell, layout, renderer, - widget::{Tree, tree}, - }, widget::{Id, Widget, container}, }; +use iced::{ + Event, Length, Point, Rectangle, Vector, + clipboard::dnd::{DndAction, DndEvent, SourceEvent}, + event, mouse, overlay, +}; +use iced_core::{ + self, Clipboard, Shell, layout, renderer, + widget::{Tree, tree}, +}; pub fn dnd_source< 'a, diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 047df0ed..41cf1dff 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -9,11 +9,11 @@ use std::rc::Rc; use iced::advanced::widget::text::Style as TextStyle; use iced_widget::core::{Element, renderer}; -use crate::iced_core::{Alignment, Length}; use crate::widget::menu::action::MenuAction; use crate::widget::menu::key_bind::KeyBind; use crate::widget::{Button, RcElementWrapper, icon}; use crate::{theme, widget}; +use iced_core::{Alignment, Length}; /// Nested menu is essentially a tree of items, a menu is a collection of items /// a menu itself can also be an item of another menu. diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 5d862e9f..44ca8574 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -3,7 +3,6 @@ use super::model::{Entity, Model, Selectable}; use super::{InsertPosition, ReorderEvent}; -use crate::iced_core::id::Internal; use crate::theme::{SegmentedButton as Style, THEME}; use crate::widget::dnd_destination::DragId; use crate::widget::menu::{ @@ -22,6 +21,7 @@ use iced::{ Alignment, Background, Color, Event, Length, Padding, Rectangle, Size, Task, Vector, alignment, keyboard, mouse, touch, window, }; +use iced_core::id::Internal; use iced_core::mouse::ScrollDelta; use iced_core::text::{self, Ellipsize, LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; use iced_core::widget::operation::Focusable; @@ -2043,10 +2043,10 @@ where ..image_bounds }, crate::widget::icon(match crate::widget::common::object_select().data() { - crate::iced_core::svg::Data::Bytes(bytes) => { + iced_core::svg::Data::Bytes(bytes) => { crate::widget::icon::from_svg_bytes(bytes.as_ref()).symbolic(true) } - crate::iced_core::svg::Data::Path(path) => { + iced_core::svg::Data::Path(path) => { crate::widget::icon::from_path(path.clone()) } }), diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 9d31ca1e..05371a17 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -2,18 +2,18 @@ use std::time::{Duration, Instant}; -use crate::{Element, anim, iced_core::Border, iced_widget::toggler::Status}; +use crate::{Element, anim}; use iced_core::{ - Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, event, - layout, mouse, + Border, Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, + event, layout, mouse, renderer::{self, Renderer}, text, touch, widget::{self, Tree, tree}, window, }; -use iced_widget::Id; +use iced_widget::{Id, toggler::Status}; -pub use crate::iced_widget::toggler::{Catalog, Style}; +pub use iced_widget::toggler::{Catalog, Style}; pub fn toggler<'a, Message>(is_checked: bool) -> Toggler<'a, Message> { Toggler::new(is_checked) @@ -200,7 +200,7 @@ impl<'a, Message> Widget for Toggler<'a, align_x: self.text_alignment, align_y: alignment::Vertical::Top, shaping: self.text_shaping, - wrapping: crate::iced_core::text::Wrapping::default(), + wrapping: iced_core::text::Wrapping::default(), ellipsize: self.ellipsize, }, ); diff --git a/src/widget/wrapper.rs b/src/widget/wrapper.rs index 73e476fa..133f9b87 100644 --- a/src/widget/wrapper.rs +++ b/src/widget/wrapper.rs @@ -93,8 +93,8 @@ impl Widget for RcElementWrapper { &mut self, tree: &mut tree::Tree, renderer: &crate::Renderer, - limits: &crate::iced_core::layout::Limits, - ) -> crate::iced_core::layout::Node { + limits: &iced_core::layout::Limits, + ) -> iced_core::layout::Node { self.element .with_data_mut(|e| e.as_widget_mut().layout(tree, renderer, limits)) } @@ -104,9 +104,9 @@ impl Widget for RcElementWrapper { tree: &tree::Tree, renderer: &mut crate::Renderer, theme: &crate::Theme, - style: &crate::iced_core::renderer::Style, - layout: crate::iced_core::Layout<'_>, - cursor: crate::iced_core::mouse::Cursor, + style: &iced_core::renderer::Style, + layout: iced_core::Layout<'_>, + cursor: iced_core::mouse::Cursor, viewport: &Rectangle, ) { self.element.with_data(move |e| { @@ -134,7 +134,7 @@ impl Widget for RcElementWrapper { fn operate( &mut self, state: &mut tree::Tree, - layout: crate::iced_core::Layout<'_>, + layout: iced_core::Layout<'_>, renderer: &crate::Renderer, operation: &mut dyn widget::Operation, ) { @@ -148,11 +148,11 @@ impl Widget for RcElementWrapper { &mut self, state: &mut tree::Tree, event: &crate::iced::Event, - layout: crate::iced_core::Layout<'_>, - cursor: crate::iced_core::mouse::Cursor, + layout: iced_core::Layout<'_>, + cursor: iced_core::mouse::Cursor, renderer: &crate::Renderer, - clipboard: &mut dyn crate::iced_core::Clipboard, - shell: &mut crate::iced_core::Shell<'_, M>, + clipboard: &mut dyn iced_core::Clipboard, + shell: &mut iced_core::Shell<'_, M>, viewport: &Rectangle, ) { self.element.with_data_mut(|e| { @@ -165,11 +165,11 @@ impl Widget for RcElementWrapper { fn mouse_interaction( &self, state: &tree::Tree, - layout: crate::iced_core::Layout<'_>, - cursor: crate::iced_core::mouse::Cursor, + layout: iced_core::Layout<'_>, + cursor: iced_core::mouse::Cursor, viewport: &Rectangle, renderer: &crate::Renderer, - ) -> crate::iced_core::mouse::Interaction { + ) -> iced_core::mouse::Interaction { self.element.with_data(|e| { e.as_widget() .mouse_interaction(state, layout, cursor, viewport, renderer) @@ -179,11 +179,11 @@ impl Widget for RcElementWrapper { fn overlay<'a>( &'a mut self, state: &'a mut tree::Tree, - layout: crate::iced_core::Layout<'a>, + layout: iced_core::Layout<'a>, renderer: &crate::Renderer, viewport: &Rectangle, - translation: crate::iced_core::Vector, - ) -> Option> { + translation: iced_core::Vector, + ) -> Option> { assert_eq!(self.element.thread_id, thread::current().id()); Rc::get_mut(&mut self.element.data).and_then(|e| { e.get_mut() @@ -203,9 +203,9 @@ impl Widget for RcElementWrapper { fn drag_destinations( &self, state: &tree::Tree, - layout: crate::iced_core::Layout<'_>, + layout: iced_core::Layout<'_>, renderer: &crate::Renderer, - dnd_rectangles: &mut crate::iced_core::clipboard::DndDestinationRectangles, + dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { self.element.with_data_mut(|e| { e.as_widget_mut() From e5955b568de23b653963cfc8a0ed444633e1795b Mon Sep 17 00:00:00 2001 From: Adam Cosner Date: Tue, 7 Apr 2026 22:12:36 -0400 Subject: [PATCH 095/116] ci: Updated pages.yml workflow Use nightly channel to enable docs generating feature badges, plus enabled more features in the docs build, and building the cctk docs also --- .github/workflows/pages.yml | 33 ++++++++++++++++++--------------- src/lib.rs | 1 + 2 files changed, 19 insertions(+), 15 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index e48570ba..419c99d0 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -7,21 +7,24 @@ on: jobs: pages: - runs-on: ubuntu-latest steps: - - name: Checkout sources - uses: actions/checkout@v3 - with: - submodules: recursive - - name: System dependencies - run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - - name: Build documentation - run: cargo doc --no-deps --verbose --features tokio,winit - - name: Deploy documentation - uses: peaceiris/actions-gh-pages@v3 - with: - github_token: ${{ secrets.GITHUB_TOKEN }} - publish_dir: ./target/doc - force_orphan: true + - name: Checkout sources + uses: actions/checkout@v3 + with: + submodules: recursive + - name: Install Rust nightly + uses: dtolnay/rust-toolchain@master + with: + toolchain: nightly-2025-07-31 + - name: System dependencies + run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev + - name: Build documentation + run: RUSTDOCFLAGS="--cfg docsrs" cargo +nightly-2025-07-31 doc --no-deps -p cosmic-client-toolkit -p libcosmic --verbose --features tokio,winit,wayland,process,desktop,single-instance + - name: Deploy documentation + uses: peaceiris/actions-gh-pages@v3 + with: + github_token: ${{ secrets.GITHUB_TOKEN }} + publish_dir: ./target/doc + force_orphan: true diff --git a/src/lib.rs b/src/lib.rs index e04f1609..f3873443 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,6 +3,7 @@ #![allow(clippy::module_name_repetitions)] #![cfg_attr(target_os = "redox", feature(lazy_cell))] +#![cfg_attr(docsrs, feature(doc_auto_cfg))] /// Recommended default imports. pub mod prelude { From 12d2233c6b5f0315e3feb99705d9046f38729a94 Mon Sep 17 00:00:00 2001 From: Adam Cosner Date: Tue, 7 Apr 2026 22:25:25 -0400 Subject: [PATCH 096/116] fix(ci): Added an inline doc to cctk reexport --- src/lib.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/lib.rs b/src/lib.rs index f3873443..02623799 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -78,6 +78,7 @@ pub(crate) mod malloc; #[cfg(all(feature = "process", not(windows)))] pub mod process; +#[doc(inline)] #[cfg(all(feature = "wayland", target_os = "linux"))] pub use cctk; From 6df3f76a33f55de94670e16d1c7e57a1de2fe7f3 Mon Sep 17 00:00:00 2001 From: Adam Cosner Date: Tue, 7 Apr 2026 22:50:13 -0400 Subject: [PATCH 097/116] ci: Added a few more enabled dependency docs --- .github/workflows/pages.yml | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 419c99d0..a15b99b5 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -21,7 +21,15 @@ jobs: - name: System dependencies run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Build documentation - run: RUSTDOCFLAGS="--cfg docsrs" cargo +nightly-2025-07-31 doc --no-deps -p cosmic-client-toolkit -p libcosmic --verbose --features tokio,winit,wayland,process,desktop,single-instance + run: RUSTDOCFLAGS="--cfg docsrs" \ + cargo +nightly-2025-07-31 doc --no-deps \ + -p cosmic-client-toolkit \ + -p cosmic-protocols \ + -p smithay-client-toolkit \ + -p wayland-protocols \ + -p wayland-client \ + -p libcosmic \ + --verbose --features tokio,winit,wayland,desktop,single-instance,applet,xdg-portal,multi-window - name: Deploy documentation uses: peaceiris/actions-gh-pages@v3 with: From 77b37f22466ddb581407e0f683c733cf8b7e6891 Mon Sep 17 00:00:00 2001 From: Adam Cosner Date: Tue, 7 Apr 2026 23:01:10 -0400 Subject: [PATCH 098/116] fix(ci) removed the smithay and wayland protocol docs builds --- .github/workflows/pages.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index a15b99b5..34e4d0cf 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -25,9 +25,6 @@ jobs: cargo +nightly-2025-07-31 doc --no-deps \ -p cosmic-client-toolkit \ -p cosmic-protocols \ - -p smithay-client-toolkit \ - -p wayland-protocols \ - -p wayland-client \ -p libcosmic \ --verbose --features tokio,winit,wayland,desktop,single-instance,applet,xdg-portal,multi-window - name: Deploy documentation From c7093beca323e555f169fe65ac1118e925bd4e75 Mon Sep 17 00:00:00 2001 From: Adam Cosner Date: Wed, 8 Apr 2026 01:22:23 -0400 Subject: [PATCH 099/116] fix(ci): cargo now running properly --- .github/workflows/pages.yml | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 34e4d0cf..3e3a042e 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -21,7 +21,8 @@ jobs: - name: System dependencies run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Build documentation - run: RUSTDOCFLAGS="--cfg docsrs" \ + run: | + RUSTDOCFLAGS="--cfg docsrs" \ cargo +nightly-2025-07-31 doc --no-deps \ -p cosmic-client-toolkit \ -p cosmic-protocols \ From 47ab72be502378d18b931f0e10e8ba94619dd607 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Wed, 8 Apr 2026 01:38:18 -0400 Subject: [PATCH 100/116] fix!(progress_bar): remove unused generic Message type --- src/widget/progress_bar/mod.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/widget/progress_bar/mod.rs b/src/widget/progress_bar/mod.rs index ea069ffc..4e277b0a 100644 --- a/src/widget/progress_bar/mod.rs +++ b/src/widget/progress_bar/mod.rs @@ -3,21 +3,21 @@ pub mod linear; pub mod style; /// A spinner / throbber widget that can be used to indicate that some operation is in progress. -pub fn indeterminate_circular() -> circular::Circular { +pub fn indeterminate_circular() -> circular::Circular { circular::Circular::new() } /// A linear throbber widget that can be used to indicate that some operation is in progress. -pub fn indeterminate_linear() -> linear::Linear { +pub fn indeterminate_linear() -> linear::Linear { linear::Linear::new() } /// A circular progress spinner widget that can be used to indicate the progress of some operation. -pub fn determinate_circular(progress: f32) -> circular::Circular { +pub fn determinate_circular(progress: f32) -> circular::Circular { circular::Circular::new().progress(progress) } /// A linear progress bar widget that can be used to indicate the progress of some operation. -pub fn determinate_linear(progress: f32) -> linear::Linear { +pub fn determinate_linear(progress: f32) -> linear::Linear { linear::Linear::new().progress(progress) } From a44cff8011d81209e18de86f24da248c88b5a28d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 8 Apr 2026 09:58:20 -0400 Subject: [PATCH 101/116] fix(text_input): always clip input text with the text bounds this issue seems unique to tiny-skia --- examples/application/Cargo.toml | 1 - src/widget/text_input/input.rs | 14 ++++++-------- 2 files changed, 6 insertions(+), 9 deletions(-) diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index bc037ec0..c494238f 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -18,7 +18,6 @@ features = [ "tokio", "xdg-portal", "a11y", - "wgpu", "single-instance", "surface-message", "multi-window", diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 806ceda0..4336c757 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -2740,14 +2740,14 @@ pub fn draw<'a, Message>( effective_alignment(state.value.raw()), ); - if !cursors.is_empty() { + if cursors.is_empty() { + renderer.with_translation(Vector::ZERO, |_| {}); + } else { renderer.with_translation(Vector::new(alignment_offset - offset, 0.0), |renderer| { for (quad, color) in &cursors { renderer.fill_quad(*quad, *color); } }); - } else { - renderer.with_translation(Vector::ZERO, |_| {}); } let bounds = Rectangle { @@ -2785,11 +2785,9 @@ pub fn draw<'a, Message>( ); }; - if is_selecting { - renderer.with_layer(bounds, render); - } else { - render(renderer); - } + // FIXME: we always must clip with a layer because of what appears to be a tiny-skia text clipping issue. + // Otherwise overflowing text escapes the bounds of the input. + renderer.with_layer(text_bounds, render); let trailing_icon_tree = children.get(child_index); From 6caccaba337ed9bab21c5fe3c2aa7392e322e89c Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 8 Apr 2026 16:13:31 -0600 Subject: [PATCH 102/116] fix: icon color when window is maximized --- src/theme/mod.rs | 2 +- src/theme/style/iced.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/theme/mod.rs b/src/theme/mod.rs index b7e85237..093bac05 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -307,7 +307,7 @@ impl DefaultStyle for Theme { fn default_style(&self) -> Appearance { let cosmic = self.cosmic(); Appearance { - icon_color: cosmic.bg_color().into(), + icon_color: cosmic.on_bg_color().into(), background_color: cosmic.bg_color().into(), text_color: cosmic.on_bg_color().into(), } diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 4633477d..aa6f4b33 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -43,7 +43,7 @@ pub mod application { iced::theme::Style { background_color: cosmic.bg_color().into(), text_color: cosmic.on_bg_color().into(), - icon_color: cosmic.bg_color().into(), + icon_color: cosmic.on_bg_color().into(), } } } From e287a789c1f33459d4a7ac737c2e7d4004e7e0e4 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Fri, 10 Apr 2026 20:53:43 -0600 Subject: [PATCH 103/116] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 7fd263d9..fc6b4634 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 7fd263d99e6ae1b07e51f25bda3367f7463806b1 +Subproject commit fc6b46342b365ca4f120a830b66204c2517945c8 From 0e72508dcca7161376e86167242b24e0469e53ee Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sun, 12 Apr 2026 18:50:19 +0200 Subject: [PATCH 104/116] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Amadɣas Co-authored-by: Asier Saratsua Garmendia Co-authored-by: ButterflyOfFire Co-authored-by: Ettore Atalan Co-authored-by: Geeson Wan Co-authored-by: Hosted Weblate Co-authored-by: 麋麓 BigELK176 Co-authored-by: 김유빈 Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/de/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/kab/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ko/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/zh_Hant/ Translation: Pop OS/libcosmic --- i18n/de/libcosmic.ftl | 2 +- i18n/eu/libcosmic.ftl | 0 i18n/kab/libcosmic.ftl | 33 +++++++++++++++++++++++++++++++++ i18n/ko/libcosmic.ftl | 21 ++++++++++++++------- i18n/zh-Hant/libcosmic.ftl | 34 ++++++++++++++++++++++++++++++++++ 5 files changed, 82 insertions(+), 8 deletions(-) create mode 100644 i18n/eu/libcosmic.ftl diff --git a/i18n/de/libcosmic.ftl b/i18n/de/libcosmic.ftl index 238000f5..2d3704a6 100644 --- a/i18n/de/libcosmic.ftl +++ b/i18n/de/libcosmic.ftl @@ -6,7 +6,7 @@ links = Links developers = Entwickler(innen) designers = Designer(innen) artists = Künstler(innen) -translators = Übersetzer*innen +translators = Übersetzer(innen) documenters = Dokumentierer(innen) # Calendar january = Januar { $year } diff --git a/i18n/eu/libcosmic.ftl b/i18n/eu/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/kab/libcosmic.ftl b/i18n/kab/libcosmic.ftl index e69de29b..6eac2bc7 100644 --- a/i18n/kab/libcosmic.ftl +++ b/i18n/kab/libcosmic.ftl @@ -0,0 +1,33 @@ +close = Mdel +license = Turagt +links = Iseɣwan +developers = Ineflayen +artists = Inaẓuren +translators = Imsuqlen +january = Yennayer { $year } +february = Fuṛar { $year } +march = Meɣres { $year } +april = Yebrir { $year } +may = Mayyu { $year } +june = Yunyu { $year } +july = Yulyu { $year } +august = Ɣuct { $year } +september = Ctembeṛ { $year } +october = Tubeṛ { $year } +november = Wambeṛ { $year } +december = Dujembeṛ { $year } +documenters = Imeskaren +monday = Arim +mon = Ari +tuesday = Aram +tue = Ara +wednesday = Ahad +wed = Aha +thursday = Amhad +thu = Amh +friday = Sem +fri = Sm +saturday = Sed +sat = Sd +sunday = Acer +sun = Ace diff --git a/i18n/ko/libcosmic.ftl b/i18n/ko/libcosmic.ftl index 8d499756..6cc0adbc 100644 --- a/i18n/ko/libcosmic.ftl +++ b/i18n/ko/libcosmic.ftl @@ -2,26 +2,33 @@ february = { $year }년 2월 close = 닫기 documenters = 문서 작성자 november = { $year }년 11월 -friday = 금 -tuesday = 화 +friday = 금요일 +tuesday = 화요일 may = { $year }년 5월 -wednesday = 수 +wednesday = 수요일 april = { $year }년 4월 -monday = 월 +monday = 월요일 translators = 번역가 artists = 아티스트 license = 라이선스 december = { $year }년 12월 -sunday = 일 +sunday = 일요일 links = 링크 march = { $year }년 3월 june = { $year }년 6월 -saturday = 토 +saturday = 토요일 august = { $year }년 8월 developers = 개발자 july = { $year }년 7월 -thursday = 목 +thursday = 목요일 september = { $year }년 9월 designers = 디자이너 october = { $year }년 10월 january = { $year }년 1월 +mon = 월 +tue = 화 +wed = 수 +thu = 목 +fri = 금 +sat = 토 +sun = 일 diff --git a/i18n/zh-Hant/libcosmic.ftl b/i18n/zh-Hant/libcosmic.ftl index e69de29b..8c9b201c 100644 --- a/i18n/zh-Hant/libcosmic.ftl +++ b/i18n/zh-Hant/libcosmic.ftl @@ -0,0 +1,34 @@ +close = 關閉 +developers = 開發人員 +designers = 設計人員 +artists = 美編設計 +translators = 翻譯人員 +documenters = 文件編輯人員 +january = { $year } 年 1 月 +monday = 星期一 +tuesday = 星期二 +wednesday = 星期三 +thursday = 星期四 +friday = 星期五 +saturday = 星期六 +sunday = 星期日 +mon = 週一 +tue = 週二 +wed = 週三 +thu = 週四 +fri = 週五 +sat = 週六 +sun = 週日 +license = 授權 +links = 連結 +february = { $year } 年 2 月 +march = { $year } 年 3 月 +april = { $year } 年 4 月 +may = { $year } 年 5 月 +june = { $year } 年 6 月 +july = { $year } 年 7 月 +august = { $year } 年 8 月 +september = { $year } 年 9 月 +october = { $year } 年 10 月 +november = { $year } 年 11 月 +december = { $year } 年 12 月 From 52116d2f36972c422a0953a4699e32d5eb30cdac Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 13 Apr 2026 14:07:31 -0600 Subject: [PATCH 105/116] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index fc6b4634..78caabba 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit fc6b46342b365ca4f120a830b66204c2517945c8 +Subproject commit 78caabba7ef91cd1030da6f70b41d266704ffece From 46d9f0c3442189b446ffeff452c314fa6592da7e Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Tue, 14 Apr 2026 11:53:33 -0700 Subject: [PATCH 106/116] widget/icon: Bundle icons on macOS, not just Windows --- Cargo.toml | 4 ++-- build.rs | 4 +++- src/widget/icon/bundle.rs | 6 +++--- src/widget/icon/named.rs | 4 ++-- 4 files changed, 10 insertions(+), 8 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 83fe90f0..e090ad21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -170,12 +170,12 @@ cosmic-config = { path = "cosmic-config", features = ["dbus"] } cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" } zbus = { version = "5.14.0", default-features = false } -[target.'cfg(unix)'.dependencies] +[target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } freedesktop-desktop-entry = { version = "0.8.1", optional = true } shlex = { version = "1.3.0", optional = true } -[target.'cfg(not(unix))'.dependencies] +[target.'cfg(any(not(unix), target_os = "macos"))'.dependencies] # Used to embed bundled icons for non-unix platforms. phf = { version = "0.13.1", features = ["macros"] } diff --git a/build.rs b/build.rs index c69feaf5..4ce0aa9e 100644 --- a/build.rs +++ b/build.rs @@ -3,7 +3,9 @@ use std::env; fn main() { println!("cargo::rerun-if-changed=build.rs"); - if env::var_os("CARGO_CFG_UNIX").is_none() { + if env::var_os("CARGO_CFG_UNIX").is_none() + || env::var("CARGO_CFG_TARGET_OS").as_deref() == Ok("macos") + { generate_bundled_icons(); } } diff --git a/src/widget/icon/bundle.rs b/src/widget/icon/bundle.rs index 9d0877d0..bb6ce244 100644 --- a/src/widget/icon/bundle.rs +++ b/src/widget/icon/bundle.rs @@ -4,12 +4,12 @@ //! Embedded icons for platforms which do not support icon themes yet. /// Icon bundling is not enabled on unix platforms. -#[cfg(unix)] +#[cfg(all(unix, not(target_os = "macos")))] pub fn get(icon_name: &str) -> Option { None } -#[cfg(not(unix))] +#[cfg(any(not(unix), target_os = "macos"))] /// Get a bundled icon on non-unix platforms. pub fn get(icon_name: &str) -> Option { ICONS @@ -17,5 +17,5 @@ pub fn get(icon_name: &str) -> Option { .map(|bytes| super::Data::Svg(crate::iced::widget::svg::Handle::from_memory(*bytes))) } -#[cfg(not(unix))] +#[cfg(any(not(unix), target_os = "macos"))] include!(concat!(env!("OUT_DIR"), "/bundled_icons.rs")); diff --git a/src/widget/icon/named.rs b/src/widget/icon/named.rs index 8405e080..dfd66cf5 100644 --- a/src/widget/icon/named.rs +++ b/src/widget/icon/named.rs @@ -52,7 +52,7 @@ impl Named { } } - #[cfg(not(windows))] + #[cfg(all(unix, not(target_os = "macos")))] #[must_use] pub fn path(self) -> Option { let name = &*self.name; @@ -107,7 +107,7 @@ impl Named { result } - #[cfg(windows)] + #[cfg(any(not(unix), target_os = "macos"))] #[must_use] pub fn path(self) -> Option { //TODO: implement icon lookup for Windows From 3d8d8915be516229bd215403e0a800ea80f618ae Mon Sep 17 00:00:00 2001 From: Hojjat Date: Tue, 14 Apr 2026 23:14:41 -0600 Subject: [PATCH 107/116] chore: enable ico and xpm image support for desktop feature --- Cargo.toml | 6 ++++++ src/app/mod.rs | 3 +++ 2 files changed, 9 insertions(+) diff --git a/Cargo.toml b/Cargo.toml index e090ad21..d73da2dc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -58,6 +58,7 @@ desktop = [ "process", "dep:cosmic-settings-config", "dep:freedesktop-desktop-entry", + "dep:image-extras", "dep:mime", "dep:shlex", "tokio?/io-util", @@ -141,9 +142,14 @@ css-color = "0.2.8" derive_setters = "0.1.9" futures = "0.3" image = { version = "0.25.10", default-features = false, features = [ + "ico", "jpeg", "png", ] } +image-extras = { version = "0.1.0", default-features = false, features = [ + "xpm", + "xbm", +], optional = true } libc = { version = "0.2.183", optional = true } log = "0.4" mime = { version = "0.3.17", optional = true } diff --git a/src/app/mod.rs b/src/app/mod.rs index 5c0e95e4..42fa4b1b 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -128,6 +128,9 @@ impl BootFn, crate::Action(settings: Settings, flags: App::Flags) -> iced::Result { + #[cfg(feature = "desktop")] + image_extras::register(); + #[cfg(all(target_env = "gnu", not(target_os = "windows")))] if let Some(threshold) = settings.default_mmap_threshold { crate::malloc::limit_mmap_threshold(threshold); From 0fc4638af38d8edecf4b0bdc4e17e8e2bd2a2c22 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 15 Apr 2026 14:45:20 -0600 Subject: [PATCH 108/116] fix: register image_extras in run_single_instance too --- src/app/mod.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/app/mod.rs b/src/app/mod.rs index 42fa4b1b..f78beac7 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -197,6 +197,9 @@ where App::Flags: CosmicFlags, App::Message: Clone + std::fmt::Debug + Send + 'static, { + #[cfg(feature = "desktop")] + image_extras::register(); + use std::collections::HashMap; let activation_token = std::env::var("XDG_ACTIVATION_TOKEN").ok(); From 9cac422c245777e492094177b21b8a8be4ab7bb0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Sun, 5 Apr 2026 17:03:47 +0200 Subject: [PATCH 109/116] fix(toggler): animate external changes --- src/widget/toggler.rs | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 05371a17..b95b596e 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -161,7 +161,10 @@ impl<'a, Message> Widget for Toggler<'a, } fn state(&self) -> tree::State { - tree::State::new(State::default()) + tree::State::new(State { + prev_toggled: self.is_toggled, + ..State::default() + }) } fn id(&self) -> Option { @@ -238,6 +241,14 @@ impl<'a, Message> Widget for Toggler<'a, return; }; let state = tree.state.downcast_mut::(); + + // animate external changes + if state.prev_toggled != self.is_toggled { + state.anim.changed(self.duration); + shell.request_redraw(); + state.prev_toggled = self.is_toggled; + } + match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { @@ -246,6 +257,7 @@ impl<'a, Message> Widget for Toggler<'a, if mouse_over { shell.publish((on_toggle)(!self.is_toggled)); state.anim.changed(self.duration); + state.prev_toggled = !self.is_toggled; shell.capture_event(); } } @@ -430,4 +442,5 @@ pub fn next_to_each_other( pub struct State { text: widget::text::State<::Paragraph>, anim: anim::State, + prev_toggled: bool, } From 9b465a8b5c4d3bb75389bba49d6ee1cec8c26d9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Sat, 4 Apr 2026 16:34:25 +0200 Subject: [PATCH 110/116] feat(list_column): button list items --- src/theme/style/button.rs | 30 ++++-- src/widget/list/column.rs | 128 ---------------------- src/widget/list/list_column.rs | 188 +++++++++++++++++++++++++++++++++ src/widget/list/mod.rs | 4 +- src/widget/settings/item.rs | 94 +++++++++++++---- src/widget/settings/section.rs | 17 +-- 6 files changed, 298 insertions(+), 163 deletions(-) delete mode 100644 src/widget/list/column.rs create mode 100644 src/widget/list/list_column.rs diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index 0575ce67..bb52d9a6 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -27,7 +27,7 @@ pub enum Button { IconVertical, Image, Link, - ListItem, + ListItem([f32; 4]), MenuFolder, MenuItem, MenuRoot, @@ -148,8 +148,8 @@ pub fn appearance( appearance.text_color = Some(component.on.into()); corner_radii = &cosmic.corner_radii.radius_s; } - Button::ListItem => { - corner_radii = &[0.0; 4]; + Button::ListItem(radii) => { + corner_radii = radii; let (background, text, icon) = color(&cosmic.background.component); if selected { @@ -197,7 +197,7 @@ impl Catalog for crate::Theme { return active(focused, self); } - appearance(self, focused, selected, false, style, move |component| { + let mut s = appearance(self, focused, selected, false, style, move |component| { let text_color = if matches!( style, Button::Icon | Button::IconVertical | Button::HeaderBar @@ -209,7 +209,15 @@ impl Catalog for crate::Theme { }; (component.base.into(), text_color, text_color) - }) + }); + + if let Button::ListItem(_) = style { + if !selected { + s.background = None; + } + } + + s } fn disabled(&self, style: &Self::Class) -> Style { @@ -237,7 +245,7 @@ impl Catalog for crate::Theme { return hovered(focused, self); } - appearance( + let mut s = appearance( self, focused || matches!(style, Button::Image), selected, @@ -256,7 +264,15 @@ impl Catalog for crate::Theme { (component.hover.into(), text_color, text_color) }, - ) + ); + + if let Button::ListItem(_) = style { + if !selected { + s.background = None; + } + } + + s } fn pressed(&self, focused: bool, selected: bool, style: &Self::Class) -> Style { diff --git a/src/widget/list/column.rs b/src/widget/list/column.rs deleted file mode 100644 index 945b9140..00000000 --- a/src/widget/list/column.rs +++ /dev/null @@ -1,128 +0,0 @@ -// Copyright 2022 System76 -// SPDX-License-Identifier: MPL-2.0 - -use iced_core::Padding; -use iced_widget::container::Catalog; - -use crate::{ - Apply, Element, theme, - widget::{container, divider, space::vertical}, -}; - -#[inline] -pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> { - ListColumn::default() -} - -#[must_use] -pub struct ListColumn<'a, Message> { - spacing: u16, - padding: Padding, - list_item_padding: Padding, - divider_padding: u16, - style: theme::Container<'a>, - children: Vec>, -} - -impl Default for ListColumn<'_, Message> { - fn default() -> Self { - let cosmic_theme::Spacing { - space_xxs, space_m, .. - } = theme::spacing(); - - Self { - spacing: 0, - padding: Padding::from(0), - divider_padding: 16, - list_item_padding: [space_xxs, space_m].into(), - style: theme::Container::List, - children: Vec::with_capacity(4), - } - } -} - -impl<'a, Message: 'static> ListColumn<'a, Message> { - #[inline] - pub fn new() -> Self { - Self::default() - } - - #[allow(clippy::should_implement_trait)] - pub fn add(self, item: impl Into>) -> Self { - #[inline(never)] - fn inner<'a, Message: 'static>( - mut this: ListColumn<'a, Message>, - item: Element<'a, Message>, - ) -> ListColumn<'a, Message> { - if !this.children.is_empty() { - this.children.push( - container(divider::horizontal::default()) - .padding([0, this.divider_padding]) - .into(), - ); - } - - // Ensure a minimum height of 32. - let list_item = crate::widget::row![ - container(item).align_y(iced::Alignment::Center), - vertical().height(iced::Length::Fixed(32.)) - ] - .padding(this.list_item_padding) - .align_y(iced::Alignment::Center); - - this.children.push(list_item.into()); - this - } - - inner(self, item.into()) - } - - #[inline] - pub fn spacing(mut self, spacing: u16) -> Self { - self.spacing = spacing; - self - } - - /// Sets the style variant of this [`Circular`]. - #[inline] - pub fn style(mut self, style: ::Class<'a>) -> Self { - self.style = style; - self - } - - #[inline] - pub fn padding(mut self, padding: impl Into) -> Self { - self.padding = padding.into(); - self - } - - #[inline] - pub fn divider_padding(mut self, padding: u16) -> Self { - self.divider_padding = padding; - self - } - - pub fn list_item_padding(mut self, padding: impl Into) -> Self { - self.list_item_padding = padding.into(); - self - } - - #[must_use] - pub fn into_element(self) -> Element<'a, Message> { - crate::widget::column::with_children(self.children) - .spacing(self.spacing) - .padding(self.padding) - .width(iced::Length::Fill) - .apply(container) - .padding([self.spacing, 0]) - .class(self.style) - .width(iced::Length::Fill) - .into() - } -} - -impl<'a, Message: 'static> From> for Element<'a, Message> { - fn from(column: ListColumn<'a, Message>) -> Self { - column.into_element() - } -} diff --git a/src/widget/list/list_column.rs b/src/widget/list/list_column.rs new file mode 100644 index 00000000..89a87063 --- /dev/null +++ b/src/widget/list/list_column.rs @@ -0,0 +1,188 @@ +// Copyright 2022 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::widget::container::Catalog; +use crate::widget::{button, column, container, divider, row, space::vertical}; +use crate::{Apply, Element, theme}; +use iced::{Length, Padding}; + +/// A button list item for use in a [`ListColumn`]. +pub struct ListButton<'a, Message> { + content: Element<'a, Message>, + on_press: Option, + selected: bool, +} + +/// Creates a [`ListButton`] with the given content. +pub fn button<'a, Message>(content: impl Into>) -> ListButton<'a, Message> { + ListButton { + content: content.into(), + on_press: None, + selected: false, + } +} + +impl<'a, Message: 'static> ListButton<'a, Message> { + pub fn on_press(mut self, on_press: Message) -> Self { + self.on_press = Some(on_press); + self + } + + pub fn on_press_maybe(mut self, on_press: Option) -> Self { + self.on_press = on_press; + self + } + + pub fn selected(mut self, selected: bool) -> Self { + self.selected = selected; + self + } +} + +pub enum ListItem<'a, Message> { + Element(Element<'a, Message>), + Button(ListButton<'a, Message>), +} + +/// A trait for types that can be added to a [`ListColumn`]. +pub trait IntoListItem<'a, Message> { + fn into_list_item(self) -> ListItem<'a, Message>; +} + +impl<'a, Message, T> IntoListItem<'a, Message> for T +where + T: Into>, +{ + fn into_list_item(self) -> ListItem<'a, Message> { + ListItem::Element(self.into()) + } +} + +impl<'a, Message> IntoListItem<'a, Message> for ListButton<'a, Message> { + fn into_list_item(self) -> ListItem<'a, Message> { + ListItem::Button(self) + } +} + +#[must_use] +pub struct ListColumn<'a, Message> { + list_item_padding: Padding, + style: theme::Container<'a>, + children: Vec>, +} + +#[inline] +pub fn list_column<'a, Message: 'static>() -> ListColumn<'a, Message> { + ListColumn::default() +} + +pub fn with_capacity<'a, Message: 'static>(capacity: usize) -> ListColumn<'a, Message> { + let cosmic_theme::Spacing { + space_xxs, space_m, .. + } = theme::spacing(); + + ListColumn { + list_item_padding: [space_xxs, space_m].into(), + style: theme::Container::List, + children: Vec::with_capacity(capacity), + } +} + +impl Default for ListColumn<'_, Message> { + fn default() -> Self { + with_capacity(4) + } +} + +impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { + #[inline] + pub fn new() -> Self { + Self::default() + } + + /// Adds an element to the list column. + #[allow(clippy::should_implement_trait)] + pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self { + self.children.push(item.into_list_item()); + self + } + + /// Sets the style variant of this [`ListColumn`]. + #[inline] + pub fn style(mut self, style: ::Class<'a>) -> Self { + self.style = style; + self + } + + pub fn list_item_padding(mut self, padding: impl Into) -> Self { + self.list_item_padding = padding.into(); + self + } + + #[must_use] + pub fn into_element(self) -> Element<'a, Message> { + let padding = self.list_item_padding; + let count = self.children.len(); + let last_index = count.saturating_sub(1); + let radius_s = theme::active().cosmic().radius_s(); + + // Ensure minimum height of 32 + let content_row = |content| { + row![container(content), vertical().height(32)].align_y(iced::Alignment::Center) + }; + + self.children + .into_iter() + .enumerate() + .fold( + column::with_capacity((2 * count).saturating_sub(1)), + |mut col, (i, item)| { + if i > 0 { + col = col.push(divider::horizontal::default()); + } + + match item { + ListItem::Element(content) => { + col.push(content_row(content).padding(padding).width(Length::Fill)) + } + ListItem::Button(ListButton { + content, + on_press, + selected, + }) => col.push( + content_row(content) + .apply(button::custom) + .padding(padding) + .width(Length::Fill) + .on_press_maybe(on_press) + .selected(selected) + .class(theme::Button::ListItem(get_radius( + radius_s, + i == 0, + i == last_index, + ))), + ), + } + }, + ) + .width(Length::Fill) + .apply(container) + .class(self.style) + .into() + } +} + +impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { + fn from(column: ListColumn<'a, Message>) -> Self { + column.into_element() + } +} + +fn get_radius(radius: [f32; 4], first: bool, last: bool) -> [f32; 4] { + match (first, last) { + (true, true) => radius, + (true, false) => [radius[0], radius[1], 0.0, 0.0], + (false, true) => [0.0, 0.0, radius[2], radius[3]], + (false, false) => [0.0, 0.0, 0.0, 0.0], + } +} diff --git a/src/widget/list/mod.rs b/src/widget/list/mod.rs index c6e2051c..71eda086 100644 --- a/src/widget/list/mod.rs +++ b/src/widget/list/mod.rs @@ -1,6 +1,6 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -pub mod column; +pub mod list_column; -pub use self::column::{ListColumn, list_column}; +pub use self::list_column::{ListButton, ListColumn, button, list_column}; diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index 349d93d8..a4092093 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -5,7 +5,7 @@ use std::borrow::Cow; use crate::{ Element, Theme, theme, - widget::{FlexRow, Row, column, container, flex_row, row, text}, + widget::{FlexRow, Row, column, container, flex_row, list, row, text}, }; use derive_setters::Setters; use iced_core::{Length, text::Wrapping}; @@ -114,39 +114,95 @@ impl<'a, Message: 'static> Item<'a, Message> { flex_item_row(self.control_(widget.into())) } - #[inline(never)] - fn control_(self, widget: Element<'a, Message>) -> Vec> { - let mut contents = Vec::with_capacity(4); - - if let Some(icon) = self.icon { - contents.push(icon); - } - + fn label(self) -> Element<'a, Message> { if let Some(description) = self.description { - let column = column::with_capacity(2) + column::with_capacity(2) .spacing(2) .push(text::body(self.title).wrapping(Wrapping::Word)) .push(text::caption(description).wrapping(Wrapping::Word)) - .width(Length::Fill); - - contents.push(column.into()); + .width(Length::Fill) + .into() } else { - contents.push(text(self.title).width(Length::Fill).into()); + text(self.title).width(Length::Fill).into() } + } + #[inline(never)] + fn control_(mut self, widget: Element<'a, Message>) -> Vec> { + let mut contents = Vec::with_capacity(3); + if let Some(icon) = self.icon.take() { + contents.push(icon); + } + contents.push(self.label()); contents.push(widget); contents } + fn control_start(self, widget: impl Into>) -> Row<'a, Message, Theme> { + item_row(vec![widget.into(), self.label()]) + } + pub fn toggler( self, is_checked: bool, message: impl Fn(bool) -> Message + 'static, - ) -> Row<'a, Message, Theme> { - self.control( - crate::widget::toggler(is_checked) - .width(Length::Shrink) - .on_toggle(message), + ) -> list::ListButton<'a, Message> { + let on_press = message(!is_checked); + list::button( + self.control( + crate::widget::toggler(is_checked) + .width(Length::Shrink) + .on_toggle(message), + ), ) + .on_press(on_press) + } + + pub fn toggler_maybe( + self, + is_checked: bool, + message: Option Message + 'static>, + ) -> list::ListButton<'a, Message> { + let on_press = message.as_ref().map(|f| f(!is_checked)); + list::button( + self.control( + crate::widget::toggler(is_checked) + .width(Length::Shrink) + .on_toggle_maybe(message), + ), + ) + .on_press_maybe(on_press) + } + + pub fn checkbox( + self, + is_checked: bool, + message: impl Fn(bool) -> Message + 'static, + ) -> list::ListButton<'a, Message> { + let on_press = message(!is_checked); + list::button( + self.control_start( + crate::widget::checkbox(is_checked) + .width(Length::Shrink) + .on_toggle(message), + ), + ) + .on_press(on_press) + } + + pub fn checkbox_maybe( + self, + is_checked: bool, + message: Option Message + 'static>, + ) -> list::ListButton<'a, Message> { + let on_press = message.as_ref().map(|f| f(!is_checked)); + list::button( + self.control_start( + crate::widget::checkbox(is_checked) + .width(Length::Shrink) + .on_toggle_maybe(message), + ), + ) + .on_press_maybe(on_press) } } diff --git a/src/widget/settings/section.rs b/src/widget/settings/section.rs index ab95b5ad..ee07c76d 100644 --- a/src/widget/settings/section.rs +++ b/src/widget/settings/section.rs @@ -2,16 +2,19 @@ // SPDX-License-Identifier: MPL-2.0 use crate::Element; +use crate::widget::list_column::IntoListItem; use crate::widget::{ListColumn, column, text}; use std::borrow::Cow; /// A section within a settings view column. -pub fn section<'a, Message: 'static>() -> Section<'a, Message> { +pub fn section<'a, Message: Clone + 'static>() -> Section<'a, Message> { with_column(ListColumn::default()) } /// A section with a pre-defined list column. -pub fn with_column(children: ListColumn<'_, Message>) -> Section<'_, Message> { +pub fn with_column( + children: ListColumn<'_, Message>, +) -> Section<'_, Message> { Section { header: None, children, @@ -24,9 +27,9 @@ pub struct Section<'a, Message> { children: ListColumn<'a, Message>, } -impl<'a, Message: 'static> Section<'a, Message> { +impl<'a, Message: Clone + 'static> Section<'a, Message> { /// Define an optional title for the section. - pub fn title(mut self, title: impl Into>) -> Self { + pub fn title(self, title: impl Into>) -> Self { self.header(text::heading(title.into())) } @@ -38,8 +41,8 @@ impl<'a, Message: 'static> Section<'a, Message> { /// Add a child element to the section's list column. #[allow(clippy::should_implement_trait)] - pub fn add(mut self, item: impl Into>) -> Self { - self.children = self.children.add(item.into()); + pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self { + self.children = self.children.add(item); self } @@ -61,7 +64,7 @@ impl<'a, Message: 'static> Section<'a, Message> { } } -impl<'a, Message: 'static> From> for Element<'a, Message> { +impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { fn from(data: Section<'a, Message>) -> Self { column::with_capacity(2) .spacing(8) From 917af9fda204d027ad55380521041b6691f17895 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Thu, 16 Apr 2026 15:59:37 +0200 Subject: [PATCH 111/116] feat(radio): internal method for radio without label Also adds the related settings item builder. --- src/widget/radio.rs | 165 ++++++++++++++++++++++-------------- src/widget/settings/item.rs | 16 +++- 2 files changed, 116 insertions(+), 65 deletions(-) diff --git a/src/widget/radio.rs b/src/widget/radio.rs index 338c0a4e..c3f115c0 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -1,5 +1,5 @@ //! Create choices using radio buttons. -use crate::Theme; +use crate::{Theme, theme}; use iced::border; use iced_core::event::{self, Event}; use iced_core::layout; @@ -92,7 +92,7 @@ where { is_selected: bool, on_click: Message, - label: Element<'a, Message, Theme, Renderer>, + label: Option>, width: Length, size: f32, spacing: f32, @@ -106,9 +106,6 @@ where /// The default size of a [`Radio`] button. pub const DEFAULT_SIZE: f32 = 16.0; - /// The default spacing of a [`Radio`] button. - pub const DEFAULT_SPACING: f32 = 8.0; - /// Creates a new [`Radio`] button. /// /// It expects: @@ -126,10 +123,29 @@ where Radio { is_selected: Some(value) == selected, on_click: f(value), - label: label.into(), + label: Some(label.into()), width: Length::Shrink, size: Self::DEFAULT_SIZE, - spacing: Self::DEFAULT_SPACING, + spacing: theme::spacing().space_xs as f32, + } + } + + /// Creates a new [`Radio`] button without a label. + /// + /// This is intended for internal use with the settings item builder, + /// where the label comes from the settings item title instead. + pub(crate) fn new_no_label(value: V, selected: Option, f: F) -> Self + where + V: Eq + Copy, + F: FnOnce(V) -> Message, + { + Radio { + is_selected: Some(value) == selected, + on_click: f(value), + label: None, + width: Length::Shrink, + size: Self::DEFAULT_SIZE, + spacing: theme::spacing().space_xs as f32, } } @@ -161,11 +177,17 @@ where Renderer: iced_core::Renderer, { fn children(&self) -> Vec { - vec![Tree::new(&self.label)] + if let Some(label) = &self.label { + vec![Tree::new(label)] + } else { + vec![] + } } fn diff(&mut self, tree: &mut Tree) { - tree.diff_children(std::slice::from_mut(&mut self.label)); + if let Some(label) = &mut self.label { + tree.diff_children(std::slice::from_mut(label)); + } } fn size(&self) -> Size { Size { @@ -180,16 +202,20 @@ where renderer: &Renderer, limits: &layout::Limits, ) -> layout::Node { - layout::next_to_each_other( - &limits.width(self.width), - self.spacing, - |_| layout::Node::new(Size::new(self.size, self.size)), - |limits| { - self.label - .as_widget_mut() - .layout(&mut tree.children[0], renderer, limits) - }, - ) + if let Some(label) = &mut self.label { + layout::next_to_each_other( + &limits.width(self.width), + self.spacing, + |_| layout::Node::new(Size::new(self.size, self.size)), + |limits| { + label + .as_widget_mut() + .layout(&mut tree.children[0], renderer, limits) + }, + ) + } else { + layout::Node::new(Size::new(self.size, self.size)) + } } fn operate( @@ -199,12 +225,14 @@ where renderer: &Renderer, operation: &mut dyn iced_core::widget::Operation<()>, ) { - self.label.as_widget_mut().operate( - &mut tree.children[0], - layout.children().nth(1).unwrap(), - renderer, - operation, - ); + if let Some(label) = &mut self.label { + label.as_widget_mut().operate( + &mut tree.children[0], + layout.children().nth(1).unwrap(), + renderer, + operation, + ); + } } fn update( @@ -218,24 +246,25 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - self.label.as_widget_mut().update( - &mut tree.children[0], - event, - layout.children().nth(1).unwrap(), - cursor, - renderer, - clipboard, - shell, - viewport, - ); + if let Some(label) = &mut self.label { + label.as_widget_mut().update( + &mut tree.children[0], + event, + layout.children().nth(1).unwrap(), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } if !shell.is_event_captured() { match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) => { + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerLifted { .. }) => { if cursor.is_over(layout.bounds()) { shell.publish(self.on_click.clone()); - shell.capture_event(); return; } @@ -253,13 +282,17 @@ where viewport: &Rectangle, renderer: &Renderer, ) -> mouse::Interaction { - let interaction = self.label.as_widget().mouse_interaction( - &tree.children[0], - layout.children().nth(1).unwrap(), - cursor, - viewport, - renderer, - ); + let interaction = if let Some(label) = &self.label { + label.as_widget().mouse_interaction( + &tree.children[0], + layout.children().nth(1).unwrap(), + cursor, + viewport, + renderer, + ) + } else { + mouse::Interaction::default() + }; if interaction == mouse::Interaction::default() { if cursor.is_over(layout.bounds()) { @@ -284,8 +317,6 @@ where ) { let is_mouse_over = cursor.is_over(layout.bounds()); - let mut children = layout.children(); - let custom_style = if is_mouse_over { theme.style( &(), @@ -302,16 +333,21 @@ where ) }; - { - let layout = children.next().unwrap(); - let bounds = layout.bounds(); + let (dot_bounds, label_layout) = if self.label.is_some() { + let mut children = layout.children(); + let dot_bounds = children.next().unwrap().bounds(); + (dot_bounds, children.next()) + } else { + (layout.bounds(), None) + }; - let size = bounds.width; + { + let size = dot_bounds.width; let dot_size = 6.0; renderer.fill_quad( renderer::Quad { - bounds, + bounds: dot_bounds, border: Border { radius: (size / 2.0).into(), width: custom_style.border_width, @@ -326,8 +362,8 @@ where renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: bounds.x + (size - dot_size) / 2.0, - y: bounds.y + (size - dot_size) / 2.0, + x: dot_bounds.x + (size - dot_size) / 2.0, + y: dot_bounds.y + (size - dot_size) / 2.0, width: dot_size, height: dot_size, }, @@ -339,9 +375,8 @@ where } } - { - let label_layout = children.next().unwrap(); - self.label.as_widget().draw( + if let (Some(label), Some(label_layout)) = (&self.label, label_layout) { + label.as_widget().draw( &tree.children[0], renderer, theme, @@ -361,7 +396,7 @@ where viewport: &Rectangle, translation: Vector, ) -> Option> { - self.label.as_widget_mut().overlay( + self.label.as_mut()?.as_widget_mut().overlay( &mut tree.children[0], layout.children().nth(1).unwrap(), renderer, @@ -377,12 +412,14 @@ where renderer: &Renderer, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { - self.label.as_widget().drag_destinations( - &state.children[0], - layout.children().nth(1).unwrap(), - renderer, - dnd_rectangles, - ); + if let Some(label) = &self.label { + label.as_widget().drag_destinations( + &state.children[0], + layout.children().nth(1).unwrap(), + renderer, + dnd_rectangles, + ); + } } } diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index a4092093..11821335 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -103,7 +103,7 @@ pub struct Item<'a, Message> { icon: Option>, } -impl<'a, Message: 'static> Item<'a, Message> { +impl<'a, Message: Clone + 'static> Item<'a, Message> { /// Assigns a control to the item. pub fn control(self, widget: impl Into>) -> Row<'a, Message, Theme> { item_row(self.control_(widget.into())) @@ -205,4 +205,18 @@ impl<'a, Message: 'static> Item<'a, Message> { ) .on_press_maybe(on_press) } + + pub fn radio(self, value: V, selected: Option, f: F) -> list::ListButton<'a, Message> + where + V: Eq + Copy + 'static, + F: Fn(V) -> Message + 'static, + { + let on_press = f(value); + list::button( + self.control_start(crate::widget::radio::Radio::new_no_label( + value, selected, f, + )), + ) + .on_press(on_press) + } } From 3f9e93067b31d9ba81a4e3a28653b3380c61c352 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Thu, 16 Apr 2026 18:08:42 +0200 Subject: [PATCH 112/116] fix(item builder): remove unnecessary lifetime bound for radio --- src/widget/settings/item.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index 11821335..5abb464c 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -208,8 +208,8 @@ impl<'a, Message: Clone + 'static> Item<'a, Message> { pub fn radio(self, value: V, selected: Option, f: F) -> list::ListButton<'a, Message> where - V: Eq + Copy + 'static, - F: Fn(V) -> Message + 'static, + V: Eq + Copy, + F: Fn(V) -> Message, { let on_press = f(value); list::button( From c162a1f24a2b7fdf29286dfa807c4a1b4813ab7c Mon Sep 17 00:00:00 2001 From: Hojjat Date: Thu, 9 Apr 2026 18:32:14 -0600 Subject: [PATCH 113/116] fix(animated-image): update frames and fix compilation errors --- src/widget/frames.rs | 63 +++++++++++++++++--------------------------- 1 file changed, 24 insertions(+), 39 deletions(-) diff --git a/src/widget/frames.rs b/src/widget/frames.rs index 056a55ba..a542cec6 100644 --- a/src/widget/frames.rs +++ b/src/widget/frames.rs @@ -14,10 +14,10 @@ use iced_core::image::Renderer as ImageRenderer; use iced_core::mouse::Cursor; use iced_core::widget::{Tree, tree}; use iced_core::{ - Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Shell, Size, Vector, Widget, - event, layout, renderer, window, + Clipboard, ContentFit, Element, Event, Layout, Length, Rectangle, Rotation, Shell, Size, + Widget, event, layout, renderer, window, }; -use iced_widget::image::{self, Handle}; +use iced_widget::image::{self, FilterMethod, Handle}; use image_rs::AnimationDecoder; use image_rs::codecs::gif::GifDecoder; use image_rs::codecs::png::PngDecoder; @@ -146,7 +146,7 @@ impl Frames { match image_type { ImageType::Gif => Self::from_decoder(GifDecoder::new(io::Cursor::new(bytes))?), - ImageType::Apng => Self::from_decoder(PngDecoder::new(io::Cursor::new(bytes))?.apng()), + ImageType::Apng => Self::from_decoder(PngDecoder::new(io::Cursor::new(bytes))?.apng()?), ImageType::WebP => Self::from_decoder(WebPDecoder::new(io::Cursor::new(bytes))?), } } @@ -168,10 +168,10 @@ impl Frames { let first = frames.first().cloned().unwrap(); let total_bytes = frames .iter() - .map(|f| match f.handle.data() { - iced_core::image::Handle::Path(..) => 0, - iced_core::image::Handle::Bytes(_, b) => b.len(), - iced_core::image::Handle::Rgba { pixels, .. } => pixels.len(), + .map(|f| match &f.handle { + Handle::Path(..) => 0, + Handle::Bytes(_, b) => b.len(), + Handle::Rgba { pixels, .. } => pixels.len(), }) .sum::() .try_into() @@ -324,7 +324,11 @@ where &self.frames.first.handle, self.width, self.height, + None, self.content_fit, + Rotation::default(), + false, + [0.0; 4], ) } @@ -371,37 +375,18 @@ where ) { let state = tree.state.downcast_ref::(); - // Pulled from iced_native::widget::::draw - // - // TODO: export iced_native::widget::image::draw as standalone function - { - let Size { width, height } = renderer.dimensions(&state.current.frame.handle); - let image_size = Size::new(width as f32, height as f32); - - let bounds = layout.bounds(); - let adjusted_fit = self.content_fit.fit(image_size, bounds.size()); - - let render = |renderer: &mut Renderer| { - let offset = Vector::new( - (bounds.width - adjusted_fit.width).max(0.0) / 2.0, - (bounds.height - adjusted_fit.height).max(0.0) / 2.0, - ); - - let drawing_bounds = Rectangle { - width: adjusted_fit.width, - height: adjusted_fit.height, - ..bounds - }; - - renderer.draw(state.current.frame.handle.clone(), drawing_bounds + offset); - }; - - if adjusted_fit.width > bounds.width || adjusted_fit.height > bounds.height { - renderer.with_layer(bounds, render); - } else { - render(renderer); - } - } + iced_widget::image::draw( + renderer, + layout, + &state.current.frame.handle, + None, + iced_core::border::Radius::default(), + self.content_fit, + FilterMethod::default(), + Rotation::default(), + 1.0, + 1.0, + ); } } From 8d7bcab258ba61dc8184d85b63a0e689aefd085c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Fri, 17 Apr 2026 00:45:33 +0200 Subject: [PATCH 114/116] fix(list_column): add back `divider_padding` Also matches previous behavior of both paddings being applied to subsequent items, rather than globally. --- src/widget/list/list_column.rs | 101 ++++++++++++++++++++------------- 1 file changed, 63 insertions(+), 38 deletions(-) diff --git a/src/widget/list/list_column.rs b/src/widget/list/list_column.rs index 89a87063..4ef3fc01 100644 --- a/src/widget/list/list_column.rs +++ b/src/widget/list/list_column.rs @@ -64,11 +64,19 @@ impl<'a, Message> IntoListItem<'a, Message> for ListButton<'a, Message> { } } +// Snapshots the padding values at the moment an item is added +struct ListEntry<'a, Message> { + item: ListItem<'a, Message>, + item_padding: Padding, + divider_padding: u16, +} + #[must_use] pub struct ListColumn<'a, Message> { list_item_padding: Padding, + divider_padding: u16, style: theme::Container<'a>, - children: Vec>, + children: Vec>, } #[inline] @@ -83,6 +91,7 @@ pub fn with_capacity<'a, Message: 'static>(capacity: usize) -> ListColumn<'a, Me ListColumn { list_item_padding: [space_xxs, space_m].into(), + divider_padding: 0, style: theme::Container::List, children: Vec::with_capacity(capacity), } @@ -100,10 +109,14 @@ impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { Self::default() } - /// Adds an element to the list column. + /// Adds a [`ListItem`] to the [`ListColumn`]. #[allow(clippy::should_implement_trait)] pub fn add(mut self, item: impl IntoListItem<'a, Message>) -> Self { - self.children.push(item.into_list_item()); + self.children.push(ListEntry { + item: item.into_list_item(), + item_padding: self.list_item_padding, + divider_padding: self.divider_padding, + }); self } @@ -119,53 +132,65 @@ impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { self } + #[inline] + pub fn divider_padding(mut self, padding: u16) -> Self { + self.divider_padding = padding; + self + } + #[must_use] pub fn into_element(self) -> Element<'a, Message> { - let padding = self.list_item_padding; let count = self.children.len(); let last_index = count.saturating_sub(1); let radius_s = theme::active().cosmic().radius_s(); + let mut col = column::with_capacity((2 * count).saturating_sub(1)); // Ensure minimum height of 32 let content_row = |content| { row![container(content), vertical().height(32)].align_y(iced::Alignment::Center) }; - self.children - .into_iter() - .enumerate() - .fold( - column::with_capacity((2 * count).saturating_sub(1)), - |mut col, (i, item)| { - if i > 0 { - col = col.push(divider::horizontal::default()); - } + for ( + i, + ListEntry { + item, + item_padding, + divider_padding, + }, + ) in self.children.into_iter().enumerate() + { + if i > 0 { + col = col + .push(container(divider::horizontal::default()).padding([0, divider_padding])); + } - match item { - ListItem::Element(content) => { - col.push(content_row(content).padding(padding).width(Length::Fill)) - } - ListItem::Button(ListButton { - content, - on_press, - selected, - }) => col.push( - content_row(content) - .apply(button::custom) - .padding(padding) - .width(Length::Fill) - .on_press_maybe(on_press) - .selected(selected) - .class(theme::Button::ListItem(get_radius( - radius_s, - i == 0, - i == last_index, - ))), - ), - } - }, - ) - .width(Length::Fill) + col = match item { + ListItem::Element(content) => col.push( + content_row(content) + .padding(item_padding) + .width(Length::Fill), + ), + ListItem::Button(ListButton { + content, + on_press, + selected, + }) => col.push( + content_row(content) + .apply(button::custom) + .padding(item_padding) + .width(Length::Fill) + .on_press_maybe(on_press) + .selected(selected) + .class(theme::Button::ListItem(get_radius( + radius_s, + i == 0, + i == last_index, + ))), + ), + }; + } + + col.width(Length::Fill) .apply(container) .class(self.style) .into() From c423ad1bfc25057922406c687f2ddc75ead5ab67 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Fri, 17 Apr 2026 13:19:23 +0200 Subject: [PATCH 115/116] improv(about): use `ListButton` --- src/widget/about.rs | 25 +++++++++++++++---------- src/widget/settings/section.rs | 11 ++++++++--- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/widget/about.rs b/src/widget/about.rs index 148af02a..9b21e93a 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -1,8 +1,9 @@ use crate::{ Apply, Element, fl, iced::{Alignment, Length}, - widget::{self, space}, + widget::{self, list}, }; +use std::rc::Rc; #[derive(Debug, Default, Clone, derive_setters::Setters)] #[setters(into, strip_option)] @@ -104,19 +105,23 @@ pub fn about<'a, Message: Clone + 'static>( space_xxs, space_m, .. } = crate::theme::spacing(); - let section_button = |name: &'a str, url: &'a str| -> Element<'a, Message> { - widget::row::with_capacity(3) - .push(widget::text(name)) - .push(space::horizontal()) + let svg_accent = Rc::new(|theme: &crate::Theme| widget::svg::Style { + color: Some(theme.cosmic().accent_text_color().into()), + }); + + let section_button = |name: &'a str, url: &'a str| -> list::ListButton<'a, Message> { + widget::row::with_capacity(2) + .push(widget::text::body(name).width(Length::Fill)) .push_maybe( - (!url.is_empty()).then_some(crate::widget::icon::from_name("link-symbolic").icon()), + (!url.is_empty()).then_some( + widget::icon::from_name("link-symbolic") + .icon() + .class(crate::theme::Svg::Custom(svg_accent.clone())), + ), ) .align_y(Alignment::Center) - .apply(widget::button::custom) - .class(crate::theme::Button::Link) + .apply(list::button) .on_press(on_url_press(url)) - .width(Length::Fill) - .into() }; let section = |list: &'a Vec<(String, String)>, title: String| { diff --git a/src/widget/settings/section.rs b/src/widget/settings/section.rs index ee07c76d..3dddb1a1 100644 --- a/src/widget/settings/section.rs +++ b/src/widget/settings/section.rs @@ -3,7 +3,7 @@ use crate::Element; use crate::widget::list_column::IntoListItem; -use crate::widget::{ListColumn, column, text}; +use crate::widget::{ListColumn, column, list_column, text}; use std::borrow::Cow; /// A section within a settings view column. @@ -11,6 +11,11 @@ pub fn section<'a, Message: Clone + 'static>() -> Section<'a, Message> { with_column(ListColumn::default()) } +/// A section with a pre-defined list column of a given capacity. +pub fn with_capacity<'a, Message: Clone + 'static>(capacity: usize) -> Section<'a, Message> { + with_column(list_column::with_capacity(capacity)) +} + /// A section with a pre-defined list column. pub fn with_column( children: ListColumn<'_, Message>, @@ -47,7 +52,7 @@ impl<'a, Message: Clone + 'static> Section<'a, Message> { } /// Add a child element to the section's list column, if `Some`. - pub fn add_maybe(self, item: Option>>) -> Self { + pub fn add_maybe(self, item: Option>) -> Self { if let Some(item) = item { self.add(item) } else { @@ -58,7 +63,7 @@ impl<'a, Message: Clone + 'static> Section<'a, Message> { /// Extends the [`Section`] with the given children. pub fn extend( self, - children: impl IntoIterator>>, + children: impl IntoIterator>, ) -> Self { children.into_iter().fold(self, Self::add) } From 95756b1a576cf6dc9f6135cf1c66e1283bfc487f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= <150025636+git-f0x@users.noreply.github.com> Date: Sat, 18 Apr 2026 16:33:57 +0200 Subject: [PATCH 116/116] improv(circular): prevent caps from touching --- examples/application/Cargo.toml | 1 + examples/application/src/main.rs | 3 +- src/widget/progress_bar/circular.rs | 57 +++++++++++++++++------------ 3 files changed, 36 insertions(+), 25 deletions(-) diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index c494238f..7a6083e0 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -21,4 +21,5 @@ features = [ "single-instance", "surface-message", "multi-window", + "wgpu", ] diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index bceece6e..f6e571e0 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -200,7 +200,7 @@ impl cosmic::Application for App { .map_or("No page selected", String::as_str); let centered = widget::container( - widget::column::with_capacity(5) + widget::column::with_capacity(14) .push(widget::text::body(page_content)) .push( widget::text_input::text_input("", &self.input_1) @@ -223,6 +223,7 @@ impl cosmic::Application for App { .on_clear(Message::Ignore), ) .push(widget::progress_bar::circular::Circular::new().size(50.0)) + .push(widget::progress_bar::circular::Circular::new().size(20.0)) .push( widget::progress_bar::linear::Linear::new() .girth(10.0) diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs index 7e8177d6..fa8c38fe 100644 --- a/src/widget/progress_bar/circular.rs +++ b/src/widget/progress_bar/circular.rs @@ -15,8 +15,6 @@ use std::f32::consts::PI; use std::time::Duration; const MIN_ANGLE: Radians = Radians(PI / 8.0); -const WRAP_ANGLE: Radians = Radians(2.0 * PI - PI / 4.0); -const BASE_ROTATION_SPEED: u32 = u32::MAX / 80; #[must_use] pub struct Circular @@ -83,6 +81,12 @@ where self.progress = Some(progress.clamp(0.0, 1.0)); self } + + fn min_wrap_angle(&self, track_radius: f32) -> (f32, f32) { + let cap_angle = self.bar_height / track_radius; + let gap = MIN_ANGLE.0.max(cap_angle); + (gap - cap_angle, 2.0 * PI - gap * 2.0) + } } impl Default for Circular @@ -122,7 +126,7 @@ impl Default for Animation { } impl Animation { - fn next(&self, additional_rotation: u32, now: Instant) -> Self { + fn next(&self, additional_rotation: u32, wrap_angle: f32, now: Instant) -> Self { match self { Self::Expanding { rotation, .. } => Self::Contracting { start: now, @@ -133,9 +137,9 @@ impl Animation { Self::Contracting { rotation, .. } => Self::Expanding { start: now, progress: 0.0, - rotation: rotation.wrapping_add(BASE_ROTATION_SPEED.wrapping_add( - (f64::from(WRAP_ANGLE / (2.0 * Radians::PI)) * f64::from(u32::MAX)) as u32, - )), + rotation: rotation.wrapping_add( + (f64::from((wrap_angle) / (2.0 * PI)) * f64::from(u32::MAX)) as u32, + ), last: now, }, } @@ -157,6 +161,7 @@ impl Animation { &self, cycle_duration: Duration, rotation_duration: Duration, + wrap_angle: f32, now: Instant, ) -> Self { let elapsed = now.duration_since(self.start()); @@ -165,7 +170,7 @@ impl Animation { * (u32::MAX) as f32) as u32; match elapsed { - elapsed if elapsed > cycle_duration => self.next(additional_rotation, now), + elapsed if elapsed > cycle_duration => self.next(additional_rotation, wrap_angle, now), _ => self.with_elapsed(cycle_duration, additional_rotation, elapsed, now), } } @@ -267,10 +272,13 @@ where return; } if let Event::Window(window::Event::RedrawRequested(now)) = event { - state.animation = - state - .animation - .timed_transition(self.cycle_duration, self.rotation_duration, *now); + let (_, wrap_angle) = self.min_wrap_angle(self.size / 2.0 - self.bar_height); + state.animation = state.animation.timed_transition( + self.cycle_duration, + self.rotation_duration, + wrap_angle, + *now, + ); state.cache.clear(); shell.request_redraw(); @@ -380,22 +388,23 @@ where } else { let mut builder = canvas::path::Builder::new(); - let start = Radians(state.animation.rotation() * 2.0 * PI); + let start = state.animation.rotation() * 2.0 * PI; + let (min_angle, wrap_angle) = self.min_wrap_angle(track_radius); let (start_angle, end_angle) = match state.animation { Animation::Expanding { progress, .. } => ( start, - start + MIN_ANGLE + WRAP_ANGLE * (smootherstep(progress)), + start + min_angle + wrap_angle * smootherstep(progress), ), Animation::Contracting { progress, .. } => ( - start + WRAP_ANGLE * (smootherstep(progress)), - start + MIN_ANGLE + WRAP_ANGLE, + start + wrap_angle * smootherstep(progress), + start + min_angle + wrap_angle, ), }; builder.arc(canvas::path::Arc { center: frame.center(), radius: track_radius, - start_angle, - end_angle, + start_angle: Radians(start_angle), + end_angle: Radians(end_angle), }); let bar_path = builder.build(); @@ -410,23 +419,23 @@ where let mut builder = canvas::path::Builder::new(); // get center of end of arc for rounded cap - let end_center = frame.center() - + Vector::new(end_angle.0.cos(), end_angle.0.sin()) * track_radius; + let end_center = + frame.center() + Vector::new(end_angle.cos(), end_angle.sin()) * track_radius; builder.arc(canvas::path::Arc { center: end_center, radius: self.bar_height / 2.0, - start_angle: Radians(end_angle.0), - end_angle: Radians(end_angle.0 + PI), + start_angle: Radians(end_angle), + end_angle: Radians(end_angle + PI), }); // get center of start of arc for rounded cap let start_center = frame.center() - + Vector::new(start_angle.0.cos(), start_angle.0.sin()) * track_radius; + + Vector::new(start_angle.cos(), start_angle.sin()) * track_radius; builder.arc(canvas::path::Arc { center: start_center, radius: self.bar_height / 2.0, - start_angle: Radians(start_angle.0 - PI), - end_angle: Radians(start_angle.0), + start_angle: Radians(start_angle - PI), + end_angle: Radians(start_angle), }); let cap_path = builder.build();