From 944c6761f73f097552042912780104783f9ca62e Mon Sep 17 00:00:00 2001 From: Josh Megnauth Date: Thu, 29 May 2025 00:40:06 -0400 Subject: [PATCH 001/352] fix(windows): Mingw doesn't support trim Closes: #872 --- src/app/cosmic.rs | 6 +++--- src/app/mod.rs | 2 +- src/applet/mod.rs | 2 +- src/lib.rs | 2 +- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 940030d8..055ff947 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -215,7 +215,7 @@ where crate::Action::DbusActivation(message) => self.app.dbus_activation(message), }; - #[cfg(target_env = "gnu")] + #[cfg(all(target_env = "gnu", not(target_os = "windows")))] crate::malloc::trim(0); message @@ -397,7 +397,7 @@ where self.app.view().map(crate::Action::App) }; - #[cfg(target_env = "gnu")] + #[cfg(all(target_env = "gnu", not(target_os = "windows")))] crate::malloc::trim(0); view @@ -407,7 +407,7 @@ where pub fn view(&self) -> Element> { let view = self.app.view_main(); - #[cfg(target_env = "gnu")] + #[cfg(all(target_env = "gnu", not(target_os = "windows")))] crate::malloc::trim(0); view diff --git a/src/app/mod.rs b/src/app/mod.rs index 31355059..eec9741c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -96,7 +96,7 @@ pub(crate) fn iced_settings( /// /// Returns error on application failure. pub fn run(settings: Settings, flags: App::Flags) -> iced::Result { - #[cfg(target_env = "gnu")] + #[cfg(all(target_env = "gnu", not(target_os = "windows")))] if let Some(threshold) = settings.default_mmap_threshold { crate::malloc::limit_mmap_threshold(threshold); } diff --git a/src/applet/mod.rs b/src/applet/mod.rs index c44bc483..e0ab993c 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -460,7 +460,7 @@ pub fn run(flags: App::Flags) -> iced::Result { let mut settings = helper.window_settings(); settings.resizable = None; - #[cfg(target_env = "gnu")] + #[cfg(all(target_env = "gnu", not(target_os = "windows")))] if let Some(threshold) = settings.default_mmap_threshold { crate::malloc::limit_mmap_threshold(threshold); } diff --git a/src/lib.rs b/src/lib.rs index 119d7af5..e8aeeedd 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -90,7 +90,7 @@ pub use iced_wgpu; pub mod icon_theme; pub mod keyboard_nav; -#[cfg(target_env = "gnu")] +#[cfg(all(target_env = "gnu", not(target_os = "windows")))] pub(crate) mod malloc; #[cfg(all(feature = "process", not(windows)))] From 5b77f37fdeee777e73eec1b7d259ed66739e5510 Mon Sep 17 00:00:00 2001 From: Eduardo Flores Date: Sun, 1 Jun 2025 23:25:09 +0000 Subject: [PATCH 002/352] feat: allow dialog resize --- src/widget/dialog.rs | 62 +++++++++++++++++++++++++++++++++++++------- 1 file changed, 53 insertions(+), 9 deletions(-) diff --git a/src/widget/dialog.rs b/src/widget/dialog.rs index 34ac9bd1..50bf4f1e 100644 --- a/src/widget/dialog.rs +++ b/src/widget/dialog.rs @@ -1,4 +1,8 @@ -use crate::{Element, iced::Length, style, theme, widget}; +use crate::{ + Element, + iced::{Length, Pixels}, + style, theme, widget, +}; use std::borrow::Cow; pub fn dialog<'a, Message>() -> Dialog<'a, Message> { @@ -13,6 +17,10 @@ pub struct Dialog<'a, Message> { primary_action: Option>, secondary_action: Option>, tertiary_action: Option>, + width: Option, + height: Option, + max_width: Option, + max_height: Option, } impl Default for Dialog<'_, Message> { @@ -31,6 +39,10 @@ impl<'a, Message> Dialog<'a, Message> { primary_action: None, secondary_action: None, tertiary_action: None, + width: None, + height: None, + max_width: None, + max_height: None, } } @@ -68,6 +80,26 @@ impl<'a, Message> Dialog<'a, Message> { self.tertiary_action = Some(button.into()); self } + + pub fn width(mut self, width: impl Into) -> Self { + self.width = Some(width.into()); + self + } + + pub fn height(mut self, height: impl Into) -> Self { + self.height = Some(height.into()); + self + } + + pub fn max_height(mut self, max_height: impl Into) -> Self { + self.max_height = Some(max_height.into()); + self + } + + pub fn max_width(mut self, max_width: impl Into) -> Self { + self.max_width = Some(max_width.into()); + self + } } impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { @@ -123,14 +155,26 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes button_row = button_row.push(button); } - Element::from( - widget::container( - widget::column::with_children(vec![content_row.into(), button_row.into()]) - .spacing(space_l), - ) - .class(style::Container::Dialog) - .padding(space_m) - .width(Length::Fixed(570.0)), + let mut container = widget::container( + widget::column::with_children(vec![content_row.into(), button_row.into()]) + .spacing(space_l), ) + .class(style::Container::Dialog) + .padding(space_m) + .width(dialog.width.unwrap_or(Length::Fixed(570.0))); + + if let Some(height) = dialog.height { + container = container.height(height); + } + + if let Some(max_width) = dialog.max_width { + container = container.max_width(max_width); + } + + if let Some(max_height) = dialog.max_height { + container = container.max_height(max_height); + } + + Element::from(container) } } From 92ec78ba29fa32d38287323001c4d6f736680950 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Tue, 10 Jun 2025 12:22:07 -0400 Subject: [PATCH 003/352] feat: menu bar popups --- examples/application/Cargo.toml | 1 + examples/application/src/main.rs | 195 ++- examples/calendar/src/main.rs | 1 + examples/context-menu/Cargo.toml | 10 +- examples/context-menu/src/main.rs | 2 +- examples/menu/src/main.rs | 3 +- examples/table-view/src/main.rs | 4 +- iced | 2 +- src/surface/action.rs | 2 +- src/theme/style/menu_bar.rs | 2 +- src/widget/button/widget.rs | 1 - src/widget/context_menu.rs | 60 +- src/widget/dropdown/widget.rs | 10 +- src/widget/menu/flex.rs | 171 ++- src/widget/menu/menu_bar.rs | 465 +++++-- src/widget/menu/menu_inner.rs | 1631 ++++++++++++++++--------- src/widget/menu/menu_tree.rs | 116 +- src/widget/nav_bar.rs | 2 +- src/widget/responsive_menu_bar.rs | 24 +- src/widget/segmented_button/widget.rs | 43 +- src/widget/table/mod.rs | 12 +- src/widget/table/widget/compact.rs | 6 +- src/widget/table/widget/standard.rs | 13 +- src/widget/wrapper.rs | 13 + 24 files changed, 1861 insertions(+), 928 deletions(-) diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index 3c5ce8e2..e5ae2f30 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -25,4 +25,5 @@ features = [ "wgpu", "single-instance", "multi-window", + "surface-message", ] diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 92c2c242..c70a9d30 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -46,13 +46,19 @@ impl Page { #[derive(Clone, Copy, Debug, Eq, PartialEq)] pub enum Action { Hi, + Hi2, + Hi3, } impl MenuAction for Action { type Message = Message; fn message(&self) -> Message { - Message::Hi + match self { + Action::Hi => Message::Hi, + Action::Hi2 => Message::Hi2, + Action::Hi3 => Message::Hi3, + } } } @@ -86,6 +92,8 @@ pub enum Message { ToggleHide, Surface(cosmic::surface::Action), Hi, + Hi2, + Hi3, } /// The [`App`] stores application-specific state. @@ -176,6 +184,12 @@ impl cosmic::Application for App { Message::Hi => { dbg!("hi"); } + Message::Hi2 => { + dbg!("hi 2"); + } + Message::Hi3 => { + dbg!("hi 3"); + } } Task::none() } @@ -221,119 +235,80 @@ impl cosmic::Application for App { } fn header_start(&self) -> Vec> { - use cosmic::widget::menu::Tree; - #[cfg(not(feature = "wayland"))] - { - vec![cosmic::widget::menu::bar(vec![ - Tree::with_children( - menu::root("hiiiiiiiiiiiiiiiiiii 1"), - menu::items( - &self.keybinds, - vec![menu::Item::Button("hi", None, Action::Hi)], - ), + vec![cosmic::widget::responsive_menu_bar().into_element( + self.core(), + &self.keybinds, + MENU_ID.clone(), + Message::Surface, + vec![ + ( + "hi 1".into(), + vec![ + menu::Item::Button("hi 12", None, Action::Hi), + menu::Item::Button("hi 13", None, Action::Hi2), + ], ), - Tree::with_children( - menu::root("hiiiiiiiiiiiiiiiiii 2"), - menu::items( - &self.keybinds, - vec![menu::Item::Button("hi 2", None, Action::Hi)], - ), + ( + "hi 2".into(), + vec![ + menu::Item::Button("hi 21", None, Action::Hi), + menu::Item::Button("hi 22", None, Action::Hi2), + menu::Item::Folder( + "nest 3 2 >".into(), + vec![ + menu::Item::Button("21", None, Action::Hi), + menu::Item::Button("242", None, Action::Hi2), + menu::Item::Button("2443", None, Action::Hi3), + menu::Item::Folder( + "nest 4 2 >".into(), + vec![ + menu::Item::Button("243", None, Action::Hi2), + menu::Item::Button("2444", None, Action::Hi), + ], + ), + ], + ), + ], ), - Tree::with_children( - menu::root("hiiiiiiiiiiiiiiiiiiiii 3"), - menu::items( - &self.keybinds, - vec![ - menu::Item::Button("hi 3", None, Action::Hi), - menu::Item::Button("hi 3 #2", None, Action::Hi), - ], - ), + ( + "hi 3".into(), + vec![ + menu::Item::Button("hi 31", None, Action::Hi), + menu::Item::Button("hi 332", None, Action::Hi2), + menu::Item::Button("hi 3333", None, Action::Hi3), + menu::Item::Button("hi 33334", None, Action::Hi3), + menu::Item::Button("hi 333335", None, Action::Hi3), + menu::Item::Button("hi 3333336", None, Action::Hi3), + ], ), - Tree::with_children( - menu::root("hi 3"), - menu::items( - &self.keybinds, - vec![ - menu::Item::Button("hi 3", None, Action::Hi), - menu::Item::Button("hi 3 #2", None, Action::Hi), - menu::Item::Button("hi 3 #3", None, Action::Hi), - ], - ), + ( + "hiiiiiiiiiiiiiiiiiii 4".into(), + vec![ + menu::Item::Button("hi 4", None, Action::Hi), + menu::Item::Button("hi 44", None, Action::Hi2), + menu::Item::Button("hi 444", None, Action::Hi3), + menu::Item::Folder( + "nest 4 >".into(), + vec![ + menu::Item::Button("hi 41", None, Action::Hi), + menu::Item::Button("hi 442", None, Action::Hi2), + menu::Item::Folder( + "nest 3 4 >".into(), + vec![ + menu::Item::Button("hi 443", None, Action::Hi2), + menu::Item::Button("hi 4444", None, Action::Hi), + menu::Item::Button("hi 44444", None, Action::Hi3), + menu::Item::Button("hi 444445", None, Action::Hi3), + menu::Item::Button("hi 4444446", None, Action::Hi3), + menu::Item::Button("hi 44444447", None, Action::Hi3), + ], + ), + ], + ), + ], ), - Tree::with_children( - menu::root("hi 4"), - menu::items( - &self.keybinds, - vec![ - menu::Item::Folder( - "hi 41 extra root", - vec![menu::Item::Button("hi 3", None, Action::Hi)], - ), - menu::Item::Button("hi 42", None, Action::Hi), - menu::Item::Button("hi 43", None, Action::Hi), - menu::Item::Button("hi 44", None, Action::Hi), - menu::Item::Button("hi 45", None, Action::Hi), - menu::Item::Button("hi 46", None, Action::Hi), - ], - ), - ), - ]) - .into()] - } - #[cfg(feature = "wayland")] - { - vec![cosmic::widget::responsive_menu_bar().into_element( - self.core(), - &self.keybinds, - MENU_ID.clone(), - Message::Surface, - vec![ - ( - "hiiiiiiiiiiiiiiiiiii 1", - vec![menu::Item::Button("hi 1", None, Action::Hi)], - ), - ( - "hiiiiiiiiiiiiiiiiiii 2".into(), - vec![ - menu::Item::Button("hi 2", None, Action::Hi), - menu::Item::Button("hi 22", None, Action::Hi), - ], - ), - ( - "hiiiiiiiiiiiiiiiiiii 3".into(), - vec![ - menu::Item::Button("hi 3", None, Action::Hi), - menu::Item::Button("hi 33", None, Action::Hi), - menu::Item::Button("hi 333", None, Action::Hi), - ], - ), - ( - "hiiiiiiiiiiiiiiiiiii 4".into(), - vec![ - menu::Item::Button("hi 4", None, Action::Hi), - menu::Item::Button("hi 44", None, Action::Hi), - menu::Item::Button("hi 444", None, Action::Hi), - menu::Item::Folder( - "nest 4".into(), - vec![ - menu::Item::Button("hi 4", None, Action::Hi), - menu::Item::Button("hi 44", None, Action::Hi), - menu::Item::Button("hi 444", None, Action::Hi), - menu::Item::Folder( - "nest 2 4".into(), - vec![ - menu::Item::Button("hi 4", None, Action::Hi), - menu::Item::Button("hi 44", None, Action::Hi), - menu::Item::Button("hi 444", None, Action::Hi), - ], - ), - ], - ), - ], - ), - ], - )] - } + ], + )] } } diff --git a/examples/calendar/src/main.rs b/examples/calendar/src/main.rs index c73c4da7..47549a70 100644 --- a/examples/calendar/src/main.rs +++ b/examples/calendar/src/main.rs @@ -92,6 +92,7 @@ impl cosmic::Application for App { |date| Message::DateSelected(date), || Message::PrevMonth, || Message::NextMonth, + chrono::Weekday::Sun, ); content = content.push(calendar); diff --git a/examples/context-menu/Cargo.toml b/examples/context-menu/Cargo.toml index 5b9ad020..9a24a1c8 100644 --- a/examples/context-menu/Cargo.toml +++ b/examples/context-menu/Cargo.toml @@ -11,4 +11,12 @@ tracing-log = "0.2.0" [dependencies.libcosmic] path = "../../" default-features = false -features = ["debug", "winit", "tokio", "xdg-portal", "multi-window"] +features = [ + "debug", + "winit", + "tokio", + "xdg-portal", + "multi-window", + "surface-message", + "wayland", +] diff --git a/examples/context-menu/src/main.rs b/examples/context-menu/src/main.rs index 840cf865..4a307840 100644 --- a/examples/context-menu/src/main.rs +++ b/examples/context-menu/src/main.rs @@ -93,7 +93,7 @@ impl cosmic::Application for App { /// Creates a view after each update. fn view(&self) -> Element { let widget = cosmic::widget::context_menu( - cosmic::widget::button::text(&self.button_label).on_press(Message::Clicked), + cosmic::widget::button::text(self.button_label.to_string()).on_press(Message::Clicked), self.context_menu(), ); diff --git a/examples/menu/src/main.rs b/examples/menu/src/main.rs index 5b65732e..7037a62c 100644 --- a/examples/menu/src/main.rs +++ b/examples/menu/src/main.rs @@ -15,6 +15,7 @@ use cosmic::widget::menu::action::MenuAction; use cosmic::widget::menu::key_bind::KeyBind; use cosmic::widget::menu::key_bind::Modifier; use cosmic::widget::menu::{self, ItemHeight, ItemWidth}; +use cosmic::widget::RcElementWrapper; use cosmic::{executor, Element}; /// Runs application with these settings @@ -155,7 +156,7 @@ impl cosmic::Application for App { pub fn menu_bar<'a>(config: &Config, key_binds: &HashMap) -> Element<'a, Message> { menu::bar(vec![menu::Tree::with_children( - menu::root("File"), + RcElementWrapper::new(Element::from(menu::root("File"))), menu::items( key_binds, vec![ diff --git a/examples/table-view/src/main.rs b/examples/table-view/src/main.rs index 8b2d4f62..6bd773bc 100644 --- a/examples/table-view/src/main.rs +++ b/examples/table-view/src/main.rs @@ -209,11 +209,11 @@ impl cosmic::Application for App { if size.width < 600.0 { widget::compact_table(&self.table_model) .on_item_left_click(Message::ItemSelect) - .item_context(|item| { + .item_context(move |item| { Some(widget::menu::items( &HashMap::new(), vec![widget::menu::Item::Button( - format!("Action on {}", item.name), + format!("Action on {}", item.name.to_string()), None, Action::None, )], diff --git a/iced b/iced index 70d104a2..717bc5db 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 70d104a28a87f06eb46d76268b6fa18c407ee2c2 +Subproject commit 717bc5dbfbc8f78e367e08e76a9572ee0ebc1f32 diff --git a/src/surface/action.rs b/src/surface/action.rs index e27815eb..fdf2680e 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -85,7 +85,7 @@ pub fn simple_subsurface( /// Used to create a popup message from within a widget. #[cfg(all(feature = "wayland", feature = "winit"))] #[must_use] -pub fn simple_popup( +pub fn simple_popup( settings: impl Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync diff --git a/src/theme/style/menu_bar.rs b/src/theme/style/menu_bar.rs index 7f99a1a5..ed0e657a 100644 --- a/src/theme/style/menu_bar.rs +++ b/src/theme/style/menu_bar.rs @@ -42,7 +42,7 @@ pub enum MenuBarStyle { #[default] Default, /// A [`Theme`] that uses a `Custom` palette. - Custom(Arc>), + Custom(Arc + Send + Sync>), } impl From Appearance> for MenuBarStyle { diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 5a1da458..aa8f0c32 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -460,7 +460,6 @@ impl<'a, Message: 'a + Clone> Widget if !self.selected && matches!(self.style, crate::theme::Button::HeaderBar) { headerbar_alpha = Some(0.8); } - theme.hovered(state.is_focused, self.selected, &self.style) } } else { diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 87122029..6769dff2 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -4,26 +4,26 @@ //! A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation. use crate::widget::menu::{ - self, CloseCondition, ItemHeight, ItemWidth, MenuBarState, PathHighlight, + self, CloseCondition, ItemHeight, ItemWidth, MenuBarState, PathHighlight, menu_roots_diff, }; use derive_setters::Setters; use iced::touch::Finger; -use iced::{Event, Vector}; +use iced::{Event, Vector, window}; use iced_core::widget::{Tree, Widget, tree}; use iced_core::{Length, Point, Size, event, mouse, touch}; use std::collections::HashSet; /// A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation. -pub fn context_menu<'a, Message: 'a>( - content: impl Into> + 'a, +pub fn context_menu( + content: impl Into> + 'static, // on_context: Message, - context_menu: Option>>, -) -> ContextMenu<'a, Message> { + context_menu: Option>>, +) -> ContextMenu<'static, Message> { let mut this = ContextMenu { content: content.into(), context_menu: context_menu.map(|menus| { vec![menu::Tree::with_children( - crate::widget::row::<'static, Message>(), + crate::Element::from(crate::widget::row::<'static, Message>()), menus, )] }), @@ -43,10 +43,12 @@ pub struct ContextMenu<'a, Message> { #[setters(skip)] content: crate::Element<'a, Message>, #[setters(skip)] - context_menu: Option>>, + context_menu: Option>>, } -impl Widget for ContextMenu<'_, Message> { +impl Widget + for ContextMenu<'_, Message> +{ fn tag(&self) -> tree::Tag { tree::Tag::of::() } @@ -56,6 +58,7 @@ impl Widget for ContextM tree::State::new(LocalState { context_cursor: Point::default(), fingers_pressed: Default::default(), + menu_bar_state: Default::default(), }) } @@ -67,7 +70,6 @@ impl Widget for ContextM // Assign the context menu's elements as this widget's children. if let Some(ref context_menu) = self.context_menu { let mut tree = Tree::empty(); - tree.state = tree::State::new(MenuBarState::default()); tree.children = context_menu .iter() .map(|root| { @@ -75,7 +77,7 @@ impl Widget for ContextM let flat = root .flattern() .iter() - .map(|mt| Tree::new(mt.item.as_widget())) + .map(|mt| Tree::new(mt.item.clone())) .collect(); tree.children = flat; tree @@ -90,6 +92,10 @@ impl Widget for ContextM fn diff(&mut self, tree: &mut Tree) { tree.children[0].diff(self.content.as_widget_mut()); + 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); + }); // if let Some(ref mut context_menus) = self.context_menu { // for (menu, tree) in context_menus @@ -183,10 +189,12 @@ impl Widget for ContextM && (right_button_released(&event) || (touch_lifted(&event) && fingers_pressed == 2)) { state.context_cursor = cursor.position().unwrap_or_default(); + let state = tree.state.downcast_mut::(); - let menu_state = tree.children[1].state.downcast_mut::(); - menu_state.open = true; - menu_state.view_cursor = cursor; + state.menu_bar_state.inner.with_data_mut(|state| { + state.open = true; + state.view_cursor = cursor; + }); return event::Status::Captured; } @@ -213,22 +221,19 @@ impl Widget for ContextM ) -> Option> { let state = tree.state.downcast_ref::(); - let Some(context_menu) = self.context_menu.as_mut() else { - return None; - }; + let context_menu = self.context_menu.as_mut()?; - if !tree.children[1].state.downcast_ref::().open { + if !state.menu_bar_state.inner.with_data(|state| state.open) { return None; } let mut bounds = layout.bounds(); bounds.x = state.context_cursor.x; bounds.y = state.context_cursor.y; - Some( crate::widget::menu::Menu { - tree: &mut tree.children[1], - menu_roots: context_menu, + tree: state.menu_bar_state.clone(), + menu_roots: std::borrow::Cow::Owned(context_menu.clone()), bounds_expand: 16, menu_overlays_parent: true, close_condition: CloseCondition { @@ -243,8 +248,12 @@ impl Widget for ContextM cross_offset: 0, root_bounds_list: vec![bounds], path_highlight: Some(PathHighlight::MenuActive), - style: &crate::theme::menu_bar::MenuBarStyle::Default, + style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default), position: Point::new(translation.x, translation.y), + is_overlay: true, + window_id: window::Id::NONE, + depth: 0, + on_surface_action: None, } .overlay(), ) @@ -263,8 +272,10 @@ impl Widget for ContextM } } -impl<'a, Message: Clone + 'a> From> for crate::Element<'a, Message> { - fn from(widget: ContextMenu<'a, Message>) -> Self { +impl<'a, Message: Clone + 'static> From> + for crate::Element<'static, Message> +{ + fn from(widget: ContextMenu<'static, Message>) -> Self { Self::new(widget) } } @@ -283,4 +294,5 @@ fn touch_lifted(event: &Event) -> bool { pub struct LocalState { context_cursor: Point, fingers_pressed: HashSet, + menu_bar_state: MenuBarState, } diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 76ee7b05..d196215d 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -536,15 +536,7 @@ pub fn update< let on_close = surface::action::destroy_popup(id); let on_surface_action_clone = on_surface_action.clone(); let translation = layout.virtual_offset(); - let get_popup_action = surface::action::simple_popup::< - AppMessage, - Box< - dyn Fn() -> Element<'static, crate::Action> - + Send - + Sync - + 'static, - >, - >( + let get_popup_action = surface::action::simple_popup::( move || { SctkPopupSettings { parent, diff --git a/src/widget/menu/flex.rs b/src/widget/menu/flex.rs index c093e802..5eaf3d94 100644 --- a/src/widget/menu/flex.rs +++ b/src/widget/menu/flex.rs @@ -1,12 +1,14 @@ // From iced_aw, license MIT -use iced_core::widget::Tree; +use iced_core::{Widget, widget::Tree}; use iced_widget::core::{ Alignment, Element, Padding, Point, Size, layout::{Limits, Node}, renderer, }; +use crate::widget::RcElementWrapper; + /// The main axis of a flex layout. #[derive(Debug)] pub enum Axis { @@ -217,3 +219,170 @@ where Node::with_children(size.expand(padding), nodes) } + +/// Computes the flex layout with the given axis and limits, applying spacing, +/// padding and alignment to the items as needed. +/// +/// It returns a new layout [`Node`]. +pub fn resolve_wrapper<'a, Message>( + axis: &Axis, + renderer: &crate::Renderer, + limits: &Limits, + padding: Padding, + spacing: f32, + align_items: Alignment, + items: &[&RcElementWrapper], + tree: &mut [&mut Tree], +) -> Node { + let limits = limits.shrink(padding); + let total_spacing = spacing * items.len().saturating_sub(1) as f32; + 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 available = axis.main(limits.max()) - total_spacing; + + let mut nodes: Vec = Vec::with_capacity(items.len()); + nodes.resize(items.len(), Node::default()); + + if align_items == Alignment::Center { + let mut fill_cross = axis.cross(limits.min()); + + for (child, tree) in items.iter().zip(tree.iter_mut()) { + let c_size = child.size(); + let cross_fill_factor = match axis { + Axis::Horizontal => c_size.height, + Axis::Vertical => c_size.width, + } + .fill_factor(); + + if cross_fill_factor == 0 { + let (max_width, max_height) = axis.pack(available, max_cross); + + let child_limits = Limits::new(Size::ZERO, Size::new(max_width, max_height)); + + let layout = child.layout(tree, renderer, &child_limits); + let size = layout.size(); + + fill_cross = fill_cross.max(axis.cross(size)); + } + } + + cross = fill_cross; + } + + for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { + let c_size = child.size(); + let fill_factor = match axis { + Axis::Horizontal => c_size.width, + Axis::Vertical => c_size.height, + } + .fill_factor(); + + if fill_factor == 0 { + let (min_width, min_height) = if align_items == Alignment::Center { + axis.pack(0.0, cross) + } else { + axis.pack(0.0, 0.0) + }; + + let (max_width, max_height) = if align_items == Alignment::Center { + axis.pack(available, cross) + } else { + axis.pack(available, max_cross) + }; + + let child_limits = Limits::new( + Size::new(min_width, min_height), + Size::new(max_width, max_height), + ); + + let layout = child.layout(tree, renderer, &child_limits); + let size = layout.size(); + + available -= axis.main(size); + + if align_items != Alignment::Center { + cross = cross.max(axis.cross(size)); + } + + nodes[i] = layout; + } else { + fill_sum += fill_factor; + } + } + + let remaining = available.max(0.0); + + for (i, (child, tree)) in items.iter().zip(tree.iter_mut()).enumerate() { + let c_size = child.size(); + let fill_factor = match axis { + Axis::Horizontal => c_size.width, + Axis::Vertical => c_size.height, + } + .fill_factor(); + + if fill_factor != 0 { + let max_main = remaining * f32::from(fill_factor) / f32::from(fill_sum); + let min_main = if max_main.is_infinite() { + 0.0 + } else { + max_main + }; + + let (min_width, min_height) = if align_items == Alignment::Center { + axis.pack(min_main, cross) + } else { + axis.pack(min_main, axis.cross(limits.min())) + }; + + let (max_width, max_height) = if align_items == Alignment::Center { + axis.pack(max_main, cross) + } else { + axis.pack(max_main, max_cross) + }; + + let child_limits = Limits::new( + Size::new(min_width, min_height), + Size::new(max_width, max_height), + ); + + let layout = child.layout(tree, renderer, &child_limits); + + if align_items != Alignment::Center { + cross = cross.max(axis.cross(layout.size())); + } + + nodes[i] = layout; + } + } + + let pad = axis.pack(padding.left, padding.top); + let mut main = pad.0; + + for (i, node) in nodes.iter_mut().enumerate() { + if i > 0 { + main += spacing; + } + + let (x, y) = axis.pack(main, pad.1); + + let node_ = node.clone().move_to(Point::new(x, y)); + + let node_ = match axis { + Axis::Horizontal => node_.align(Alignment::Start, align_items, Size::new(0.0, cross)), + Axis::Vertical => node_.align(align_items, Alignment::Start, Size::new(cross, 0.0)), + }; + + let size = node_.bounds().size(); + + *node = node_; + + main += axis.main(size); + } + + let (width, height) = axis.pack(main - pad.0, cross); + let size = limits.resolve(width, height, Size::new(width, height)); + + Node::with_children(size.expand(padding), nodes) +} diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 2584e762..eddc4f3a 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -1,73 +1,98 @@ // From iced_aw, license MIT //! A widget that handles menu trees +use std::{collections::HashMap, sync::Arc}; + use super::{ menu_inner::{ CloseCondition, Direction, ItemHeight, ItemWidth, Menu, MenuState, PathHighlight, }, menu_tree::MenuTree, }; -use crate::style::menu_bar::StyleSheet; +use crate::{ + Renderer, + style::menu_bar::StyleSheet, + widget::{ + RcWrapper, + dropdown::menu::{self, State}, + menu::menu_inner::init_root_menu, + }, +}; -use iced::{Point, Vector}; +use iced::{Point, Shadow, Vector, window}; use iced_core::Border; use iced_widget::core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, event, layout::{Limits, Node}, mouse::{self, Cursor}, - overlay, renderer, touch, + overlay, + renderer::{self, Renderer as IcedRenderer}, + touch, widget::{Tree, tree}, }; /// A `MenuBar` collects `MenuTree`s and handles all the layout, event processing, and drawing. -pub fn menu_bar( - menu_roots: Vec>, -) -> MenuBar { +pub fn menu_bar(menu_roots: Vec>) -> MenuBar +where + Message: Clone + 'static, +{ MenuBar::new(menu_roots) } +#[derive(Clone, Default)] pub(crate) struct MenuBarState { + pub(crate) inner: RcWrapper, +} + +pub(crate) struct MenuBarStateInner { + pub(crate) tree: Tree, + pub(crate) popup_id: HashMap, pub(crate) pressed: bool, + pub(crate) bar_pressed: bool, pub(crate) view_cursor: Cursor, pub(crate) open: bool, - pub(crate) active_root: Option, + pub(crate) active_root: Vec, pub(crate) horizontal_direction: Direction, pub(crate) vertical_direction: Direction, + /// List of all menu states pub(crate) menu_states: Vec, } -impl MenuBarState { - pub(super) fn get_trimmed_indices(&self) -> impl Iterator + '_ { +impl MenuBarStateInner { + /// get the list of indices hovered for the menu + pub(super) fn get_trimmed_indices(&self, index: usize) -> impl Iterator + '_ { self.menu_states .iter() + .skip(index) .take_while(|ms| ms.index.is_some()) .map(|ms| ms.index.expect("No indices were found in the menu state.")) } pub(super) fn reset(&mut self) { self.open = false; - self.active_root = None; + self.active_root = Vec::new(); self.menu_states.clear(); } } -impl Default for MenuBarState { +impl Default for MenuBarStateInner { fn default() -> Self { Self { + tree: Tree::empty(), pressed: false, view_cursor: Cursor::Available([-0.5, -0.5].into()), open: false, - active_root: None, + active_root: Vec::new(), horizontal_direction: Direction::Positive, vertical_direction: Direction::Positive, menu_states: Vec::new(), + popup_id: HashMap::new(), + bar_pressed: false, } } } -pub(crate) fn menu_roots_children( - menu_roots: &Vec>, -) -> Vec +pub(crate) fn menu_roots_children(menu_roots: &Vec>) -> Vec where - Renderer: renderer::Renderer, + Message: Clone + 'static, { /* menu bar @@ -85,7 +110,7 @@ where let flat = root .flattern() .iter() - .map(|mt| Tree::new(mt.item.as_widget())) + .map(|mt| Tree::new(mt.item.clone())) .collect(); tree.children = flat; tree @@ -94,11 +119,9 @@ where } #[allow(invalid_reference_casting)] -pub(crate) fn menu_roots_diff( - menu_roots: &mut Vec>, - tree: &mut Tree, -) where - Renderer: renderer::Renderer, +pub(crate) fn menu_roots_diff(menu_roots: &mut Vec>, tree: &mut Tree) +where + Message: Clone + 'static, { if tree.children.len() > menu_roots.len() { tree.children.truncate(menu_roots.len()); @@ -112,7 +135,7 @@ pub(crate) fn menu_roots_diff( .flattern() .iter() .map(|mt| { - let widget = mt.item.as_widget(); + let widget = &mt.item; let widget_ptr = widget as *const dyn Widget; let widget_ptr_mut = widget_ptr as *mut dyn Widget; @@ -130,7 +153,7 @@ pub(crate) fn menu_roots_diff( let flat = root .flattern() .iter() - .map(|mt| Tree::new(mt.item.as_widget())) + .map(|mt| Tree::new(mt.item.clone())) .collect(); tree.children = flat; tree @@ -139,12 +162,18 @@ pub(crate) fn menu_roots_diff( } } +pub fn get_mut_or_default(vec: &mut Vec, index: usize) -> &mut T { + if index < vec.len() { + &mut vec[index] + } else { + vec.resize_with(index + 1, T::default); + &mut vec[index] + } +} + /// A `MenuBar` collects `MenuTree`s and handles all the layout, event processing, and drawing. #[allow(missing_debug_implementations)] -pub struct MenuBar<'a, Message, Renderer = crate::Renderer> -where - Renderer: renderer::Renderer, -{ +pub struct MenuBar { width: Length, height: Length, spacing: f32, @@ -156,17 +185,22 @@ where item_width: ItemWidth, item_height: ItemHeight, path_highlight: Option, - menu_roots: Vec>, + menu_roots: Vec>, style: ::Style, + window_id: window::Id, + #[cfg(all(feature = "wayland", feature = "winit"))] + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, + pub(crate) on_surface_action: + Option Message + Send + Sync + 'static>>, } -impl<'a, Message, Renderer> MenuBar<'a, Message, Renderer> +impl MenuBar where - Renderer: renderer::Renderer, + Message: Clone + 'static, { /// Creates a new [`MenuBar`] with the given menu roots #[must_use] - pub fn new(menu_roots: Vec>) -> Self { + pub fn new(menu_roots: Vec>) -> Self { let mut menu_roots = menu_roots; menu_roots.iter_mut().for_each(MenuTree::set_index); @@ -188,6 +222,10 @@ where path_highlight: Some(PathHighlight::MenuActive), menu_roots, style: ::Style::default(), + window_id: window::Id::NONE, + #[cfg(all(feature = "wayland", feature = "winit"))] + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), + on_surface_action: None, } } @@ -278,17 +316,196 @@ where self.width = width; self } + + #[cfg(all(feature = "wayland", feature = "winit"))] + pub fn with_positioner( + mut self, + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, + ) -> Self { + self.positioner = positioner; + self + } + + #[must_use] + pub fn window_id(mut self, id: window::Id) -> Self { + self.window_id = id; + self + } + + #[must_use] + pub fn window_id_maybe(mut self, id: Option) -> Self { + if let Some(id) = id { + self.window_id = id; + } + self + } + + #[must_use] + pub fn on_surface_action( + mut self, + handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static, + ) -> Self { + self.on_surface_action = Some(Arc::new(handler)); + self + } + + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[allow(clippy::too_many_lines)] + fn create_popup( + &mut self, + layout: Layout<'_>, + view_cursor: Cursor, + renderer: &Renderer, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + my_state: &mut MenuBarState, + ) { + if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { + use crate::surface::action::destroy_popup; + use iced_runtime::platform_specific::wayland::popup::{ + SctkPopupSettings, SctkPositioner, + }; + + let surface_action = self.on_surface_action.as_ref().unwrap(); + let old_active_root = my_state + .inner + .with_data(|state| state.active_root.get(0).copied()); + + // if position is not on menu bar button skip. + let hovered_root = layout + .children() + .position(|lo| view_cursor.is_over(lo.bounds())); + + if old_active_root + .zip(hovered_root) + .is_some_and(|r| r.0 == r.1) + { + return; + } + let (id, root_list) = my_state.inner.with_data_mut(|state| { + if let Some(id) = state.popup_id.get(&self.window_id).copied() { + // close existing popups + state.menu_states.clear(); + state.active_root.clear(); + shell.publish(surface_action(destroy_popup(id))); + state.view_cursor = view_cursor; + (id, layout.children().map(|lo| lo.bounds()).collect()) + } else { + ( + window::Id::unique(), + layout.children().map(|lo| lo.bounds()).collect(), + ) + } + }); + + let mut popup_menu: Menu<'static, _> = Menu { + tree: my_state.clone(), + menu_roots: std::borrow::Cow::Owned(self.menu_roots.clone()), + bounds_expand: self.bounds_expand, + menu_overlays_parent: false, + close_condition: self.close_condition, + item_width: self.item_width, + item_height: self.item_height, + bar_bounds: layout.bounds(), + main_offset: self.main_offset, + cross_offset: self.cross_offset, + root_bounds_list: root_list, + path_highlight: self.path_highlight, + style: std::borrow::Cow::Owned(self.style.clone()), + position: Point::new(0., 0.), + is_overlay: false, + window_id: id, + depth: 0, + on_surface_action: self.on_surface_action.clone(), + }; + + init_root_menu( + &mut popup_menu, + renderer, + shell, + view_cursor.position().unwrap(), + viewport.size(), + Vector::new(0., 0.), + layout.bounds(), + self.main_offset as f32, + ); + let (anchor_rect, gravity) = my_state.inner.with_data_mut(|state| { + state.popup_id.insert(self.window_id, id); + (state + .menu_states + .iter() + .find(|s| s.index.is_none()) + .map(|s| s.menu_bounds.parent_bounds) + .map_or_else( + || { + let bounds = layout.bounds(); + Rectangle { + x: bounds.x as i32, + y: bounds.y as i32, + width: bounds.width as i32, + height: bounds.height as i32, + } + }, + |r| Rectangle { + x: r.x as i32, + y: r.y as i32, + width: r.width as i32, + height: r.height as i32, + }, + ), match (state.horizontal_direction, state.vertical_direction) { + (Direction::Positive, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, + (Direction::Positive, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopRight, + (Direction::Negative, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomLeft, + (Direction::Negative, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopLeft, + }) + }); + + let menu_node = popup_menu.layout(renderer, Limits::NONE.min_width(1.).min_height(1.)); + let popup_size = menu_node.size(); + let positioner = SctkPositioner { + size: Some(( + popup_size.width.ceil() as u32 + 2, + popup_size.height.ceil() as u32 + 2, + )), + anchor_rect, + anchor: + cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft, + gravity, + reactive: true, + ..Default::default() + }; + let parent = self.window_id; + shell.publish((surface_action)(crate::surface::action::simple_popup( + move || SctkPopupSettings { + parent, + id, + positioner: positioner.clone(), + parent_size: None, + grab: true, + close_with_children: false, + input_zone: None, + }, + Some(move || { + Element::from(crate::widget::container(popup_menu.clone()).center(Length::Fill)) + .map(crate::action::app) + }), + ))); + } + } } -impl Widget for MenuBar<'_, Message, Renderer> +impl Widget for MenuBar where - Renderer: renderer::Renderer, + Message: Clone + 'static, { fn size(&self) -> iced_core::Size { iced_core::Size::new(self.width, self.height) } fn diff(&mut self, tree: &mut Tree) { - menu_roots_diff(&mut self.menu_roots, tree); + let state = tree.state.downcast_mut::(); + state + .inner + .with_data_mut(|inner| menu_roots_diff(&mut self.menu_roots, &mut inner.tree)); } fn tag(&self) -> tree::Tag { @@ -318,7 +535,7 @@ where .iter_mut() .map(|t| &mut t.children[0]) .collect::>(); - flex::resolve( + flex::resolve_wrapper( &flex::Axis::Horizontal, renderer, &limits, @@ -330,6 +547,7 @@ where ) } + #[allow(clippy::too_many_lines)] fn on_event( &mut self, tree: &mut Tree, @@ -357,19 +575,70 @@ where viewport, ); - let state = tree.state.downcast_mut::(); + let my_state = tree.state.downcast_mut::(); + + // XXX this should reset the state if there are no other copies of the state, which implies no dropdown menus open. + let reset = self.window_id != window::Id::NONE + && my_state + .inner + .with_data(|d| !d.open && !d.active_root.is_empty()); + + let open = my_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(); + } + } + } + state.open + }); match event { Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. } | FingerLost { .. }) => { - if state.menu_states.is_empty() && view_cursor.is_over(layout.bounds()) { - state.view_cursor = view_cursor; - state.open = true; - // #[cfg(feature = "wayland")] - // TODO emit Message to open menu + let create_popup = my_state.inner.with_data_mut(|state| { + let mut create_popup = false; + if state.menu_states.is_empty() && view_cursor.is_over(layout.bounds()) { + state.view_cursor = view_cursor; + state.open = true; + create_popup = true; + } else if let Some(_id) = state.popup_id.remove(&self.window_id) { + state.menu_states.clear(); + state.active_root.clear(); + state.open = false; + #[cfg(all( + feature = "wayland", + feature = "winit", + feature = "surface-message" + ))] + { + let surface_action = self.on_surface_action.as_ref().unwrap(); + + shell.publish(surface_action(crate::surface::action::destroy_popup( + _id, + ))); + } + state.view_cursor = view_cursor; + } + create_popup + }); + + if !create_popup { + return event::Status::Ignored; } + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); + } + Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) + if open && view_cursor.is_over(layout.bounds()) => + { + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); } _ => (), } + root_status } @@ -385,49 +654,51 @@ where ) { let state = tree.state.downcast_ref::(); let cursor_pos = view_cursor.position().unwrap_or_default(); - let position = if state.open && (cursor_pos.x < 0.0 || cursor_pos.y < 0.0) { - state.view_cursor - } else { - view_cursor - }; + state.inner.with_data_mut(|state| { + let position = if state.open && (cursor_pos.x < 0.0 || cursor_pos.y < 0.0) { + state.view_cursor + } else { + view_cursor + }; - // draw path highlight - if self.path_highlight.is_some() { - let styling = theme.appearance(&self.style); - if let Some(active) = state.active_root { - let active_bounds = layout - .children() - .nth(active) - .expect("Active child not found in menu?") - .bounds(); - let path_quad = renderer::Quad { - bounds: active_bounds, - border: Border { - radius: styling.bar_border_radius.into(), - ..Default::default() - }, - shadow: Default::default(), - }; + // draw path highlight + if self.path_highlight.is_some() { + let styling = theme.appearance(&self.style); + if let Some(active) = state.active_root.first() { + let active_bounds = layout + .children() + .nth(*active) + .expect("Active child not found in menu?") + .bounds(); + let path_quad = renderer::Quad { + bounds: active_bounds, + border: Border { + radius: styling.bar_border_radius.into(), + ..Default::default() + }, + shadow: Shadow::default(), + }; - renderer.fill_quad(path_quad, styling.path); + renderer.fill_quad(path_quad, styling.path); + } } - } - self.menu_roots - .iter() - .zip(&tree.children) - .zip(layout.children()) - .for_each(|((root, t), lo)| { - root.item.as_widget().draw( - &t.children[root.index], - renderer, - theme, - style, - lo, - position, - viewport, - ); - }); + self.menu_roots + .iter() + .zip(&tree.children) + .zip(layout.children()) + .for_each(|((root, t), lo)| { + root.item.draw( + &t.children[root.index], + renderer, + theme, + style, + lo, + position, + viewport, + ); + }); + }); } fn overlay<'b>( @@ -437,18 +708,18 @@ where _renderer: &Renderer, translation: Vector, ) -> Option> { - // #[cfg(feature = "wayland")] - // return None; + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + return None; let state = tree.state.downcast_ref::(); - if !state.open { + if state.inner.with_data(|state| !state.open) { return None; } Some( Menu { - tree, - menu_roots: &mut self.menu_roots, + tree: state.clone(), + menu_roots: std::borrow::Cow::Owned(self.menu_roots.clone()), bounds_expand: self.bounds_expand, menu_overlays_parent: false, close_condition: self.close_condition, @@ -459,27 +730,30 @@ where cross_offset: self.cross_offset, root_bounds_list: layout.children().map(|lo| lo.bounds()).collect(), path_highlight: self.path_highlight, - style: &self.style, + style: std::borrow::Cow::Borrowed(&self.style), position: Point::new(translation.x, translation.y), + is_overlay: true, + window_id: window::Id::NONE, + depth: 0, + on_surface_action: self.on_surface_action.clone(), } .overlay(), ) } } -impl<'a, Message, Renderer> From> - for Element<'a, Message, crate::Theme, Renderer> + +impl From> for Element<'_, Message, crate::Theme, Renderer> where - Message: 'a, - Renderer: 'a + renderer::Renderer, + Message: Clone + 'static, { - fn from(value: MenuBar<'a, Message, Renderer>) -> Self { + fn from(value: MenuBar) -> Self { Self::new(value) } } #[allow(unused_results, clippy::too_many_arguments)] -fn process_root_events( - menu_roots: &mut [MenuTree<'_, Message, Renderer>], +fn process_root_events( + menu_roots: &mut [MenuTree], view_cursor: Cursor, tree: &mut Tree, event: &event::Event, @@ -490,7 +764,6 @@ fn process_root_events( viewport: &Rectangle, ) -> event::Status where - Renderer: renderer::Renderer, { menu_roots .iter_mut() @@ -498,7 +771,7 @@ where .zip(layout.children()) .map(|((root, t), lo)| { // assert!(t.tag == tree::Tag::stateless()); - root.item.as_widget_mut().on_event( + root.item.on_event( &mut t.children[root.index], event.clone(), lo, diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index f8c0471a..8ebca090 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -1,10 +1,13 @@ // From iced_aw, license MIT //! Menu tree overlay +use std::{borrow::Cow, sync::Arc}; + use super::{menu_bar::MenuBarState, menu_tree::MenuTree}; use crate::style::menu_bar::StyleSheet; -use iced_core::{Border, Shadow}; +use iced::window; +use iced_core::{Border, Renderer as IcedRenderer, Shadow, Widget}; use iced_widget::core::{ Clipboard, Layout, Length, Padding, Point, Rectangle, Shell, Size, Vector, event, layout::{Limits, Node}, @@ -227,20 +230,21 @@ pub(super) struct MenuSlice { pub(super) upper_bound_rel: f32, } +#[derive(Debug, Clone)] /// Menu bounds in overlay space -struct MenuBounds { +pub struct MenuBounds { child_positions: Vec, child_sizes: Vec, children_bounds: Rectangle, - parent_bounds: Rectangle, + pub parent_bounds: Rectangle, check_bounds: Rectangle, offset_bounds: Rectangle, } impl MenuBounds { #[allow(clippy::too_many_arguments)] - fn new( - menu_tree: &MenuTree<'_, Message, Renderer>, - renderer: &Renderer, + fn new( + menu_tree: &MenuTree, + renderer: &crate::Renderer, item_width: ItemWidth, item_height: ItemHeight, viewport_size: Size, @@ -249,10 +253,8 @@ impl MenuBounds { bounds_expand: u16, parent_bounds: Rectangle, tree: &mut [Tree], - ) -> Self - where - Renderer: renderer::Renderer, - { + is_overlay: bool, + ) -> Self { let (children_size, child_positions, child_sizes) = get_children_layout(menu_tree, renderer, item_width, item_height, tree); @@ -262,7 +264,11 @@ impl MenuBounds { // overlay space children position let (children_position, offset_position) = { let (cp, op) = aod.resolve(view_parent_bounds, children_size, viewport_size); - (cp - overlay_offset, op - overlay_offset) + if is_overlay { + (cp - overlay_offset, op - overlay_offset) + } else { + (Point::ORIGIN, op - overlay_offset) + } }; // calc offset bounds @@ -288,23 +294,22 @@ impl MenuBounds { } } +#[derive(Clone)] pub(crate) struct MenuState { + /// The index of the active menu item pub(super) index: Option, scroll_offset: f32, - menu_bounds: MenuBounds, + pub menu_bounds: MenuBounds, } impl MenuState { - pub(super) fn layout( + pub(super) fn layout( &self, overlay_offset: Vector, slice: MenuSlice, - renderer: &Renderer, - menu_tree: &MenuTree<'_, Message, Renderer>, + renderer: &crate::Renderer, + menu_tree: &[MenuTree], tree: &mut [Tree], - ) -> Node - where - Renderer: renderer::Renderer, - { + ) -> Node { let MenuSlice { start_index, end_index, @@ -312,18 +317,14 @@ impl MenuState { upper_bound_rel, } = slice; - assert_eq!( - menu_tree.children.len(), - self.menu_bounds.child_positions.len() - ); + debug_assert_eq!(menu_tree.len(), self.menu_bounds.child_positions.len()); // 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()) - .zip(menu_tree.children[start_index..=end_index].iter()) + .zip(menu_tree[start_index..=end_index].iter()) .map(|((cp, size), mt)| { let mut position = *cp; let mut size = *size; @@ -336,10 +337,9 @@ impl MenuState { size.height = upper_bound_rel - position; } - let limits = Limits::new(Size::ZERO, size); + let limits = Limits::new(size, size); mt.item - .as_widget() .layout(&mut tree[mt.index], renderer, &limits) .move_to(Point::new(0.0, position + self.scroll_offset)) }) @@ -348,30 +348,28 @@ impl MenuState { Node::with_children(children_bounds.size(), child_nodes).move_to(children_bounds.position()) } - fn layout_single( + fn layout_single( &self, overlay_offset: Vector, index: usize, - renderer: &Renderer, - menu_tree: &MenuTree<'_, Message, Renderer>, + renderer: &crate::Renderer, + menu_tree: &MenuTree, tree: &mut Tree, - ) -> Node - where - Renderer: renderer::Renderer, - { + ) -> Node { // viewport space children bounds let children_bounds = self.menu_bounds.children_bounds + overlay_offset; let position = self.menu_bounds.child_positions[index]; let limits = Limits::new(Size::ZERO, self.menu_bounds.child_sizes[index]); let parent_offset = children_bounds.position() - Point::ORIGIN; - let node = menu_tree.item.as_widget().layout(tree, renderer, &limits); + let node = menu_tree.item.layout(tree, renderer, &limits); node.clone().move_to(Point::new( parent_offset.x, parent_offset.y + position + self.scroll_offset, )) } + /// returns a slice of the menu items that are inside the viewport pub(super) fn slice( &self, viewport_size: Size, @@ -426,12 +424,11 @@ impl MenuState { } } -pub(crate) struct Menu<'a, 'b, Message, Renderer> -where - Renderer: renderer::Renderer, -{ - pub(crate) tree: &'b mut Tree, - pub(crate) menu_roots: &'b mut Vec>, +#[derive(Clone)] +pub(crate) struct Menu<'b, Message: std::clone::Clone> { + pub(crate) tree: MenuBarState, + // Flattened menu tree + pub(crate) menu_roots: Cow<'b, Vec>>, pub(crate) bounds_expand: u16, /// Allows menu overlay items to overlap the parent pub(crate) menu_overlays_parent: bool, @@ -443,72 +440,113 @@ where pub(crate) cross_offset: i32, pub(crate) root_bounds_list: Vec, pub(crate) path_highlight: Option, - pub(crate) style: &'b ::Style, + pub(crate) style: Cow<'b, ::Style>, pub(crate) position: Point, + pub(crate) is_overlay: bool, + /// window id for this popup + pub(crate) window_id: window::Id, + pub(crate) depth: usize, + pub(crate) on_surface_action: + Option Message + Send + Sync + 'static>>, } -impl<'b, Message, Renderer> Menu<'_, 'b, Message, Renderer> -where - Renderer: renderer::Renderer, -{ - pub(crate) fn overlay(self) -> overlay::Element<'b, Message, crate::Theme, Renderer> { +impl<'b, Message: Clone + 'static> Menu<'b, Message> { + pub(crate) fn overlay(self) -> overlay::Element<'b, Message, crate::Theme, crate::Renderer> { overlay::Element::new(Box::new(self)) } -} -impl overlay::Overlay - for Menu<'_, '_, Message, Renderer> -where - Renderer: renderer::Renderer, -{ - fn layout(&mut self, renderer: &Renderer, bounds: Size) -> Node { - // layout children - let position = self.position; - let state = self.tree.state.downcast_mut::(); - let overlay_offset = Point::ORIGIN - position; - let tree_children = &mut self.tree.children; - let children = state - .active_root - .map(|active_root| { - let root = &self.menu_roots[active_root]; - let active_tree = &mut tree_children[active_root]; - state.menu_states.iter().enumerate().fold( - (root, Vec::new()), - |(menu_root, mut nodes), (_i, ms)| { - let slice = ms.slice(bounds, overlay_offset, self.item_height); - let _start_index = slice.start_index; - let _end_index = slice.end_index; - let children_node = ms.layout( - overlay_offset, - slice, - renderer, - menu_root, - &mut active_tree.children, - ); - nodes.push(children_node); - // only the last menu can have a None active index - ( - ms.index - .map_or(menu_root, |active| &menu_root.children[active]), - nodes, - ) - }, - ) - }) - .map(|(_, l)| l) - .unwrap_or_default(); - // overlay space viewport rectangle - Node::with_children(bounds, children).translate(Point::ORIGIN - position) + pub(crate) fn layout(&self, renderer: &crate::Renderer, limits: Limits) -> Node { + // layout children; + let position = self.position; + let mut intrinsic_size = Size::ZERO; + + let empty = Vec::new(); + self.tree.inner.with_data_mut(|data| { + if data.active_root.len() < self.depth + 1 || data.menu_states.len() < self.depth + 1 { + return Node::new(limits.min()); + } + + let overlay_offset = Point::ORIGIN - position; + let tree_children: &mut Vec = &mut data.tree.children; + + let children = (if self.is_overlay { 0 } else { self.depth }..=if self.is_overlay { + data.active_root.len() - 1 + } else { + self.depth + }) + .map(|active_root| { + if self.menu_roots.is_empty() { + return (&empty, vec![]); + } + let (active_tree, roots) = + data.active_root[..=active_root].iter().skip(1).fold( + ( + &mut tree_children[data.active_root[0]].children, + &self.menu_roots[data.active_root[0]].children, + ), + |(tree, mt), next_active_root| (tree, &mt[*next_active_root].children), + ); + + data.menu_states[if self.is_overlay { 0 } else { self.depth } + ..=if self.is_overlay { + data.active_root.len() - 1 + } else { + self.depth + }] + .iter() + .enumerate() + .filter(|ms| self.is_overlay || ms.0 < 1) + .fold( + (roots, Vec::new()), + |(menu_root, mut nodes), (_i, ms)| { + let slice = + ms.slice(limits.max(), overlay_offset, self.item_height); + let _start_index = slice.start_index; + let _end_index = slice.end_index; + let children_node = ms.layout( + overlay_offset, + slice, + renderer, + menu_root, + active_tree, + ); + let node_size = children_node.size(); + intrinsic_size.height += node_size.height; + intrinsic_size.width = intrinsic_size.width.max(node_size.width); + + nodes.push(children_node); + // if popup just use len 1? + // only the last menu can have a None active index + ( + ms.index + .map_or(menu_root, |active| &menu_root[active].children), + nodes, + ) + }, + ) + }) + .map(|(_, l)| l) + .next() + .unwrap_or_default(); + + // overlay space viewport rectangle + Node::with_children( + limits.resolve(Length::Shrink, Length::Shrink, intrinsic_size), + children, + ) + .translate(Point::ORIGIN - position) + }) } + #[allow(clippy::too_many_lines)] fn on_event( &mut self, event: event::Event, layout: Layout<'_>, view_cursor: Cursor, - renderer: &Renderer, + renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - ) -> event::Status { + ) -> (Option<(usize, MenuState)>, event::Status) { use event::{ Event::{Mouse, Touch}, Status::{Captured, Ignored}, @@ -519,18 +557,25 @@ where }; use touch::Event::{FingerLifted, FingerMoved, FingerPressed}; - if !self.tree.state.downcast_ref::().open { - return Ignored; - }; + if !self + .tree + .inner + .with_data(|data| data.open || data.active_root.len() <= self.depth) + { + return (None, Ignored); + } let viewport = layout.bounds(); + let viewport_size = viewport.size(); let overlay_offset = Point::ORIGIN - viewport.position(); let overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset; - + let menu_roots = match &mut self.menu_roots { + Cow::Borrowed(_) => panic!(), + Cow::Owned(o) => o.as_mut_slice(), + }; let menu_status = process_menu_events( - self.tree, - self.menu_roots, + self, event.clone(), view_cursor, renderer, @@ -550,23 +595,28 @@ where self.main_offset as f32, ); - match event { + let ret = match event { Mouse(WheelScrolled { delta }) => { process_scroll_events(self, delta, overlay_cursor, viewport_size, overlay_offset) .merge(menu_status) } Mouse(ButtonPressed(Left)) | Touch(FingerPressed { .. }) => { - let state = self.tree.state.downcast_mut::(); - state.pressed = true; - state.view_cursor = view_cursor; + 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 overlay_cursor = view_cursor.position().unwrap_or_default() - overlay_offset; - process_overlay_events( + if !self.is_overlay && !view_cursor.is_over(viewport) { + return (None, menu_status); + } + + let (new_root, status) = process_overlay_events( self, renderer, viewport_size, @@ -574,169 +624,452 @@ where view_cursor, overlay_cursor, self.cross_offset as f32, - ) - .merge(menu_status) + shell, + ); + + return (new_root, status.merge(menu_status)); } Mouse(ButtonReleased(_)) | Touch(FingerLifted { .. }) => { - let state = self.tree.state.downcast_mut::(); - state.pressed = false; + self.tree.inner.with_data_mut(|state| { + state.pressed = false; - // process close condition - if state - .view_cursor - .position() - .unwrap_or_default() - .distance(view_cursor.position().unwrap_or_default()) - < 2.0 - { - let is_inside = state - .menu_states - .iter() - .any(|ms| ms.menu_bounds.check_bounds.contains(overlay_cursor)); - - if self.close_condition.click_inside - && is_inside - && matches!( - event, - Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. }) - ) + // process close condition + if state + .view_cursor + .position() + .unwrap_or_default() + .distance(view_cursor.position().unwrap_or_default()) + < 2.0 { - state.reset(); - return Captured; + let is_inside = state.menu_states[..=if self.is_overlay { + state.active_root.len().saturating_sub(1) + } else { + self.depth + }] + .iter() + .any(|ms| ms.menu_bounds.check_bounds.contains(overlay_cursor)); + let mut needs_reset = false; + needs_reset |= self.close_condition.click_inside + && is_inside + && matches!( + event, + Mouse(ButtonReleased(Left)) | Touch(FingerLifted { .. }) + ); + + needs_reset |= self.close_condition.click_outside && !is_inside; + + if needs_reset { + #[cfg(all( + feature = "wayland", + feature = "winit", + feature = "surface-message" + ))] + 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); + } + shell + .publish((handler)(crate::surface::Action::DestroyPopup(root))); + } + + state.reset(); + return Captured; + } } - if self.close_condition.click_outside && !is_inside { + // close all menus when clicking inside the menu bar + if self.bar_bounds.contains(overlay_cursor) { state.reset(); - return Captured; + Captured + } else { + menu_status } - } - - // 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) } - #[allow(unused_results)] + #[allow(unused_results, clippy::too_many_lines)] fn draw( &self, - renderer: &mut Renderer, + renderer: &mut crate::Renderer, theme: &crate::Theme, style: &renderer::Style, layout: Layout<'_>, view_cursor: Cursor, ) { - let state = self.tree.state.downcast_ref::(); - let Some(active_root) = state.active_root else { - return; - }; + self.tree.inner.with_data(|state| { + if !state.open || state.active_root.len() <= self.depth { + return; + } + let active_root = &state.active_root[..=if self.is_overlay { 0 } else { self.depth }]; + let viewport = layout.bounds(); + let viewport_size = viewport.size(); + let overlay_offset = Point::ORIGIN - viewport.position(); - let viewport = layout.bounds(); - let viewport_size = viewport.size(); - let overlay_offset = Point::ORIGIN - viewport.position(); - let render_bounds = Rectangle::new(Point::ORIGIN, viewport.size()); + let render_bounds = if self.is_overlay { + Rectangle::new(Point::ORIGIN, viewport.size()) + } else { + Rectangle::new(Point::ORIGIN, Size::INFINITY) + }; - let styling = theme.appearance(self.style); + let styling = theme.appearance(&self.style); + let roots = active_root.iter().skip(1).fold( + &self.menu_roots[active_root[0]].children, + |mt, next_active_root| (&mt[*next_active_root].children), + ); + let indices = state.get_trimmed_indices(self.depth).collect::>(); + state.menu_states[if self.is_overlay { 0 } else { self.depth }..=if self.is_overlay { + state.menu_states.len() - 1 + } else { + self.depth + }] + .iter() + .zip(layout.children()) + .enumerate() + .filter(|ms: &(usize, (&MenuState, Layout<'_>))| self.is_overlay || ms.0 < 1) + .fold( + roots, + |menu_roots: &Vec>, (i, (ms, children_layout))| { + let draw_path = self.path_highlight.as_ref().is_some_and(|ph| match ph { + PathHighlight::Full => true, + PathHighlight::OmitActive => { + !indices.is_empty() && i < indices.len() - 1 + } + PathHighlight::MenuActive => self.depth == state.active_root.len() - 1, + }); - let tree = &self.tree.children[active_root].children; - let root = &self.menu_roots[active_root]; - - let indices = state.get_trimmed_indices().collect::>(); - - state - .menu_states - .iter() - .zip(layout.children()) - .enumerate() - .fold(root, |menu_root, (i, (ms, children_layout))| { - let draw_path = self.path_highlight.as_ref().map_or(false, |ph| match ph { - PathHighlight::Full => true, - PathHighlight::OmitActive => !indices.is_empty() && i < indices.len() - 1, - PathHighlight::MenuActive => i < state.menu_states.len() - 1, - }); - - // react only to the last menu - let view_cursor = if i == state.menu_states.len() - 1 { - view_cursor - } else { - Cursor::Available([-1.0; 2].into()) - }; - - let draw_menu = |r: &mut Renderer| { - // calc slice - let slice = ms.slice(viewport_size, overlay_offset, self.item_height); - let start_index = slice.start_index; - let end_index = slice.end_index; - - let children_bounds = children_layout.bounds(); - - // draw menu background - // let bounds = pad_rectangle(children_bounds, styling.background_expand.into()); - // println!("cursor: {:?}", view_cursor); - // println!("bg_bounds: {:?}", bounds); - // println!("color: {:?}\n", styling.background); - let menu_quad = renderer::Quad { - bounds: pad_rectangle(children_bounds, styling.background_expand.into()), - border: Border { - radius: styling.menu_border_radius.into(), - width: styling.border_width, - color: styling.border_color, - }, - shadow: Shadow::default(), - }; - let menu_color = styling.background; - r.fill_quad(menu_quad, menu_color); - - // draw path hightlight - if let (true, Some(active)) = (draw_path, ms.index) { - let active_bounds = children_layout - .children() - .nth(active.saturating_sub(start_index)) - .expect("No active children were found in menu?") - .bounds(); - let path_quad = renderer::Quad { - bounds: active_bounds, - border: Border { - radius: styling.menu_border_radius.into(), - ..Default::default() - }, - shadow: Shadow::default(), + // react only to the last menu + let view_cursor = if self.depth == state.active_root.len() - 1 + || i == state.menu_states.len() - 1 + { + view_cursor + } else { + Cursor::Available([-1.0; 2].into()) }; - r.fill_quad(path_quad, styling.path); - } + let draw_menu = |r: &mut crate::Renderer| { + // calc slice + let slice = ms.slice(viewport_size, overlay_offset, self.item_height); + let start_index = slice.start_index; + let end_index = slice.end_index; - // draw item - menu_root.children[start_index..=end_index] - .iter() - .zip(children_layout.children()) - .for_each(|(mt, clo)| { - mt.item.as_widget().draw( - &tree[mt.index], - r, - theme, - style, - clo, - view_cursor, - &children_layout.bounds(), - ); - }); + let children_bounds = children_layout.bounds(); + + // draw menu background + // let bounds = pad_rectangle(children_bounds, styling.background_expand.into()); + // println!("cursor: {:?}", view_cursor); + // println!("bg_bounds: {:?}", bounds); + // println!("color: {:?}\n", styling.background); + let menu_quad = renderer::Quad { + bounds: pad_rectangle( + children_bounds, + styling.background_expand.into(), + ), + border: Border { + radius: styling.menu_border_radius.into(), + width: styling.border_width, + color: styling.border_color, + }, + shadow: Shadow::default(), + }; + 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 + .children() + .nth(active.saturating_sub(start_index)) + { + let path_quad = renderer::Quad { + bounds: active_layout.bounds(), + border: Border { + radius: styling.menu_border_radius.into(), + ..Default::default() + }, + shadow: Shadow::default(), + }; + + r.fill_quad(path_quad, styling.path); + } + } + if start_index < menu_roots.len() { + // draw item + menu_roots[start_index..=end_index] + .iter() + .zip(children_layout.children()) + .for_each(|(mt, clo)| { + mt.item.draw( + &state.tree.children[active_root[0]].children[mt.index], + r, + theme, + style, + clo, + view_cursor, + &children_layout.bounds(), + ); + }); + } + }; + + renderer.with_layer(render_bounds, draw_menu); + + // only the last menu can have a None active index + ms.index + .map_or(menu_roots, |active| &menu_roots[active].children) + }, + ); + }); + } +} +impl overlay::Overlay + for Menu<'_, Message> +{ + fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> iced_core::layout::Node { + Menu::layout( + self, + renderer, + Limits::NONE + .min_width(bounds.width) + .max_width(bounds.width) + .min_height(bounds.height) + .max_height(bounds.height), + ) + } + + fn on_event( + &mut self, + event: iced::Event, + layout: Layout<'_>, + 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 + } + + fn draw( + &self, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) { + self.draw(renderer, theme, style, layout, cursor); + } +} + +impl Widget + for Menu<'_, Message> +{ + fn size(&self) -> Size { + Size { + width: Length::Shrink, + height: Length::Shrink, + } + } + + fn layout( + &self, + _tree: &mut Tree, + renderer: &crate::Renderer, + limits: &iced_core::layout::Limits, + ) -> iced_core::layout::Node { + Menu::layout(self, renderer, *limits) + } + + fn draw( + &self, + _tree: &Tree, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + _viewport: &Rectangle, + ) { + Menu::draw(self, renderer, theme, style, layout, cursor); + } + + #[allow(clippy::too_many_lines)] + fn on_event( + &mut self, + tree: &mut Tree, + event: iced::Event, + layout: Layout<'_>, + 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); + + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + if let Some((new_root, new_ms)) = new_root { + use iced_runtime::platform_specific::wayland::popup::{ + SctkPopupSettings, SctkPositioner, + }; + let overlay_offset = Point::ORIGIN - viewport.position(); + + let overlay_cursor = cursor.position().unwrap_or_default() - overlay_offset; + + let Some((mut menu, popup_id)) = self.tree.inner.with_data_mut(|state| { + let popup_id = *state + .popup_id + .entry(self.window_id) + .or_insert_with(window::Id::unique); + let active_roots = state + .active_root + .get(self.depth) + .cloned() + .unwrap_or_default(); + + let root_bounds_list = layout + .children() + .next() + .unwrap() + .children() + .map(|lo| lo.bounds()) + .collect(); + + let mut popup_menu = Menu { + tree: self.tree.clone(), + menu_roots: Cow::Owned(Cow::into_owned(self.menu_roots.clone())), + bounds_expand: self.bounds_expand, + menu_overlays_parent: false, + close_condition: self.close_condition, + item_width: self.item_width, + item_height: self.item_height, + bar_bounds: layout.bounds(), + main_offset: self.main_offset, + cross_offset: self.cross_offset, + root_bounds_list, + path_highlight: self.path_highlight, + style: Cow::Owned(Cow::into_owned(self.style.clone())), + position: Point::new(0., 0.), + is_overlay: false, + window_id: popup_id, + depth: self.depth + 1, + on_surface_action: self.on_surface_action.clone(), }; - renderer.with_layer(render_bounds, draw_menu); + state.active_root.push(new_root); - // only the last menu can have a None active index - ms.index - .map_or(menu_root, |active| &menu_root.children[active]) + Some((popup_menu, popup_id)) + }) else { + return status; + }; + // XXX we push a new active root manually instead + init_root_popup_menu( + &mut menu, + renderer, + shell, + cursor.position().unwrap(), + layout.bounds().size(), + Vector::new(0., 0.), + layout.bounds(), + self.main_offset as f32, + ); + let (anchor_rect, gravity) = self.tree.inner.with_data_mut(|state| { + (state + .menu_states + .get(self.depth + 1) + .map(|s| s.menu_bounds.parent_bounds) + .map_or_else( + || { + let bounds = layout.bounds(); + Rectangle { + x: bounds.x as i32, + y: bounds.y as i32, + width: bounds.width as i32, + height: bounds.height as i32, + } + }, + |r| Rectangle { + x: r.x as i32, + y: r.y as i32, + width: r.width as i32, + height: r.height as i32, + }, + ), match (state.horizontal_direction, state.vertical_direction) { + (Direction::Positive, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, + (Direction::Positive, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopRight, + (Direction::Negative, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomLeft, + (Direction::Negative, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopLeft, + }) }); + + let menu_node = Widget::layout( + &menu, + &mut Tree::empty(), + renderer, + &Limits::NONE.min_width(1.).min_height(1.), + ); + + let popup_size = menu_node.size(); + let positioner = SctkPositioner { + size: Some(( + popup_size.width.ceil() as u32 + 2, + popup_size.height.ceil() as u32 + 2, + )), + anchor_rect, + anchor: + cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::TopRight, + gravity, + reactive: true, + ..Default::default() + }; + let parent = self.window_id; + shell.publish((self.on_surface_action.as_ref().unwrap())( + crate::surface::action::simple_popup( + move || SctkPopupSettings { + parent, + id: popup_id, + positioner: positioner.clone(), + parent_size: None, + grab: true, + close_with_children: false, + input_zone: None, + }, + Some(move || { + crate::Element::from( + crate::widget::container(menu.clone()).center(Length::Fill), + ) + .map(crate::action::app) + }), + ), + )); + + return status; + } + status + } +} + +impl<'a, Message> From> + for iced::Element<'a, Message, crate::Theme, crate::Renderer> +where + Message: std::clone::Clone + 'static, +{ + fn from(value: Menu<'a, Message>) -> Self { + Self::new(value) } } @@ -749,9 +1082,93 @@ fn pad_rectangle(rect: Rectangle, padding: Padding) -> Rectangle { } } -pub(super) fn init_root_menu( - menu: &mut Menu<'_, '_, Message, Renderer>, - renderer: &Renderer, +#[allow(clippy::too_many_arguments)] +pub(super) fn init_root_menu( + menu: &mut Menu<'_, Message>, + renderer: &crate::Renderer, + shell: &mut Shell<'_, Message>, + overlay_cursor: Point, + viewport_size: Size, + overlay_offset: Vector, + bar_bounds: Rectangle, + main_offset: f32, +) { + menu.tree.inner.with_data_mut(|state| { + if !(state.menu_states.get(menu.depth).is_none() + && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) + || menu.depth > 0 + || !state.open + { + return; + } + + let mut set = false; + for (i, (&root_bounds, mt)) in menu + .root_bounds_list + .iter() + .zip(menu.menu_roots.iter()) + .enumerate() + { + if mt.children.is_empty() { + continue; + } + + if root_bounds.contains(overlay_cursor) { + let view_center = viewport_size.width * 0.5; + let rb_center = root_bounds.center_x(); + + state.horizontal_direction = if rb_center > view_center { + Direction::Negative + } else { + Direction::Positive + }; + + let aod = Aod { + horizontal: true, + vertical: true, + horizontal_overlap: true, + vertical_overlap: false, + horizontal_direction: state.horizontal_direction, + vertical_direction: state.vertical_direction, + horizontal_offset: 0.0, + vertical_offset: main_offset, + }; + let menu_bounds = MenuBounds::new( + mt, + renderer, + menu.item_width, + menu.item_height, + viewport_size, + overlay_offset, + &aod, + menu.bounds_expand, + root_bounds, + &mut state.tree.children[0].children, + menu.is_overlay, + ); + set = true; + state.active_root.push(i); + let ms = MenuState { + index: None, + scroll_offset: 0.0, + menu_bounds, + }; + state.menu_states.push(ms); + + // Hack to ensure menu opens properly + shell.invalidate_layout(); + + break; + } + } + debug_assert!(set, "Root not set"); + }); +} + +#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] +pub(super) fn init_root_popup_menu( + menu: &mut Menu<'_, Message>, + renderer: &crate::Renderer, shell: &mut Shell<'_, Message>, overlay_cursor: Point, viewport_size: Size, @@ -759,143 +1176,153 @@ pub(super) fn init_root_menu( bar_bounds: Rectangle, main_offset: f32, ) where - Renderer: renderer::Renderer, + Message: std::clone::Clone, { - let state = menu.tree.state.downcast_mut::(); - if !(state.menu_states.is_empty() && bar_bounds.contains(overlay_cursor)) { - return; - } - - for (i, (&root_bounds, mt)) in menu - .root_bounds_list - .iter() - .zip(menu.menu_roots.iter()) - .enumerate() - { - if mt.children.is_empty() { - continue; + menu.tree.inner.with_data_mut(|state| { + if !(state.menu_states.get(menu.depth).is_none() + && (!menu.is_overlay || bar_bounds.contains(overlay_cursor))) + { + return; } - if root_bounds.contains(overlay_cursor) { - let view_center = viewport_size.width * 0.5; - let rb_center = root_bounds.center_x(); + let active_roots = &state.active_root[..=menu.depth]; - state.horizontal_direction = if rb_center > view_center { - Direction::Negative - } else { - Direction::Positive - }; - - let aod = Aod { - horizontal: true, - vertical: true, - horizontal_overlap: true, - vertical_overlap: false, - horizontal_direction: state.horizontal_direction, - vertical_direction: state.vertical_direction, - horizontal_offset: 0.0, - vertical_offset: main_offset, - }; - - let menu_bounds = MenuBounds::new( - mt, - renderer, - menu.item_width, - menu.item_height, - viewport_size, - overlay_offset, - &aod, - menu.bounds_expand, - root_bounds, - &mut menu.tree.children[i].children, - ); - - state.active_root = Some(i); - state.menu_states.push(MenuState { - index: None, - scroll_offset: 0.0, - menu_bounds, + let mut set = false; + let mt = active_roots + .iter() + .skip(1) + .fold(&menu.menu_roots[active_roots[0]], |mt, next_active_root| { + &mt.children[*next_active_root] }); + let i = active_roots.last().unwrap(); + let root_bounds = menu.root_bounds_list[*i]; - // Hack to ensure menu opens properly - shell.invalidate_layout(); + assert!(!mt.children.is_empty(), "skipping menu with no children"); + let aod = Aod { + horizontal: true, + vertical: true, + horizontal_overlap: true, + vertical_overlap: false, + horizontal_direction: state.horizontal_direction, + vertical_direction: state.vertical_direction, + horizontal_offset: 0.0, + vertical_offset: main_offset, + }; + let menu_bounds = MenuBounds::new( + mt, + renderer, + menu.item_width, + menu.item_height, + viewport_size, + overlay_offset, + &aod, + menu.bounds_expand, + root_bounds, + // TODO how to select the tree for the popup + &mut state.tree.children[0].children, + menu.is_overlay, + ); - break; - } - } + let view_center = viewport_size.width * 0.5; + let rb_center = root_bounds.center_x(); + + state.horizontal_direction = if rb_center > view_center { + Direction::Negative + } else { + Direction::Positive + }; + set = true; + + let ms = MenuState { + index: None, + scroll_offset: 0.0, + menu_bounds, + }; + state.menu_states.push(ms); + + // Hack to ensure menu opens properly + shell.invalidate_layout(); + // non tree buttons arent active? + debug_assert!(set, "Root popup menu state was not set."); + }); } #[allow(clippy::too_many_arguments)] -fn process_menu_events<'b, Message, Renderer>( - tree: &'b mut Tree, - menu_roots: &'b mut [MenuTree<'_, Message, Renderer>], +fn process_menu_events( + menu: &mut Menu, event: event::Event, view_cursor: Cursor, - renderer: &Renderer, + renderer: &crate::Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, overlay_offset: Vector, -) -> event::Status -where - Renderer: renderer::Renderer, -{ +) -> event::Status { use event::Status; - let state = tree.state.downcast_mut::(); - let Some(active_root) = state.active_root else { - return Status::Ignored; + let my_state = &mut menu.tree; + let menu_roots = match &mut menu.menu_roots { + Cow::Borrowed(_) => panic!(), + Cow::Owned(o) => o.as_mut_slice(), }; + my_state.inner.with_data_mut(|state| { + if state.active_root.len() <= menu.depth { + return event::Status::Ignored; + } - let indices = state.get_trimmed_indices().collect::>(); + let Some(hover) = state.menu_states.last_mut() else { + return Status::Ignored; + }; - if indices.is_empty() { - return Status::Ignored; - } + let Some(hover_index) = hover.index else { + return Status::Ignored; + }; - // get active item - let mt = indices - .iter() - .fold(&mut menu_roots[active_root], |mt, &i| &mut mt.children[i]); + let mt = state.active_root.iter().skip(1).fold( + // then use menu states for each open menu + &mut menu_roots[state.active_root[0]], + |mt, next_active_root| &mut mt.children[*next_active_root], + ); - // widget tree - let tree = &mut tree.children[active_root].children[mt.index]; + let mt = &mut mt.children[hover_index]; + let tree = &mut state.tree.children[state.active_root[0]].children[mt.index]; - // get layout - let last_ms = &state.menu_states[indices.len() - 1]; - let child_node = last_ms.layout_single( - overlay_offset, - last_ms.index.expect("missing index within menu state."), - renderer, - mt, - tree, - ); - let child_layout = Layout::new(&child_node); + // get layout + let child_node = hover.layout_single( + overlay_offset, + hover.index.expect("missing index within menu state."), + renderer, + mt, + tree, + ); + let child_layout = Layout::new(&child_node); - // process only the last widget - mt.item.as_widget_mut().on_event( - tree, - event, - child_layout, - view_cursor, - renderer, - clipboard, - shell, - &Rectangle::default(), - ) + // process only the last widget + mt.item.on_event( + tree, + event, + child_layout, + view_cursor, + renderer, + clipboard, + shell, + &Rectangle::default(), + ) + }) } -#[allow(unused_results)] -fn process_overlay_events( - menu: &mut Menu<'_, '_, Message, Renderer>, - renderer: &Renderer, +#[allow(unused_results, clippy::too_many_lines, clippy::too_many_arguments)] +fn process_overlay_events( + menu: &mut Menu, + renderer: &crate::Renderer, viewport_size: Size, overlay_offset: Vector, view_cursor: Cursor, overlay_cursor: Point, cross_offset: f32, -) -> event::Status + _shell: &mut Shell<'_, Message>, +) -> (Option<(usize, MenuState)>, event::Status) where - Renderer: renderer::Renderer, + Message: std::clone::Clone, { use event::Status::{Captured, Ignored}; /* @@ -907,263 +1334,295 @@ where if active item is a menu: add menu // viewport space */ + let mut new_menu_root = None; - let state = menu.tree.state.downcast_mut::(); + menu.tree.inner.with_data_mut(|state| { - let Some(active_root) = state.active_root else { - if !menu.bar_bounds.contains(overlay_cursor) { - state.reset(); - } - return Ignored; - }; + /* When overlay is running, cursor_position in any widget method will go negative + but I still want Widget::draw() to react to cursor movement */ + state.view_cursor = view_cursor; - if state.pressed { - return Ignored; - } + // * remove invalid menus - /* When overlay is running, cursor_position in any widget method will go negative - but I still want Widget::draw() to react to cursor movement */ - state.view_cursor = view_cursor; - - // * remove invalid menus - let mut prev_bounds = std::iter::once(menu.bar_bounds) - .chain( - state.menu_states[..state.menu_states.len().saturating_sub(1)] - .iter() - .map(|ms| ms.menu_bounds.children_bounds), - ) - .collect::>(); - - if menu.close_condition.leave { - for i in (0..state.menu_states.len()).rev() { - let mb = &state.menu_states[i].menu_bounds; - - if mb.parent_bounds.contains(overlay_cursor) - || mb.children_bounds.contains(overlay_cursor) - || mb.offset_bounds.contains(overlay_cursor) - || (mb.check_bounds.contains(overlay_cursor) - && prev_bounds.iter().all(|pvb| !pvb.contains(overlay_cursor))) - { - break; - } - prev_bounds.pop(); - state.menu_states.pop(); - } - } else { - for i in (0..state.menu_states.len()).rev() { - let mb = &state.menu_states[i].menu_bounds; - - if mb.parent_bounds.contains(overlay_cursor) - || mb.children_bounds.contains(overlay_cursor) - || prev_bounds.iter().all(|pvb| !pvb.contains(overlay_cursor)) - { - break; - } - prev_bounds.pop(); - state.menu_states.pop(); - } - } - - // get indices - let indices = state - .menu_states - .iter() - .map(|ms| ms.index) - .collect::>(); - - // * update active item - let Some(last_menu_state) = state.menu_states.last_mut() else { - // no menus left - state.active_root = None; - - // keep state.open when the cursor is still inside the menu bar - // this allows the overlay to keep drawing when the cursor is - // moving aroung the menu bar - if !menu.bar_bounds.contains(overlay_cursor) { - state.open = false; - } - return Captured; - }; - - let last_menu_bounds = &last_menu_state.menu_bounds; - let last_parent_bounds = last_menu_bounds.parent_bounds; - let last_children_bounds = last_menu_bounds.children_bounds; - - if (!menu.menu_overlays_parent && last_parent_bounds.contains(overlay_cursor)) - // cursor is in the parent part - || !last_children_bounds.contains(overlay_cursor) - // cursor is outside - { - last_menu_state.index = None; - return Captured; - } - // cursor is in the children part - - // calc new index - let height_diff = (overlay_cursor.y - (last_children_bounds.y + last_menu_state.scroll_offset)) - .clamp(0.0, last_children_bounds.height - 0.001); - - let active_menu_root = &menu.menu_roots[active_root]; - - let active_menu = indices[0..indices.len().saturating_sub(1)] - .iter() - .fold(active_menu_root, |mt, i| { - &mt.children[i.expect("missing active child index in menu")] - }); - - let new_index = match menu.item_height { - ItemHeight::Uniform(u) => (height_diff / f32::from(u)).floor() as usize, - ItemHeight::Static(_) | ItemHeight::Dynamic(_) => { - let max_index = active_menu.children.len() - 1; - search_bound( - 0, - 0, - max_index, - height_diff, - &last_menu_bounds.child_positions, - &last_menu_bounds.child_sizes, + let mut prev_bounds = std::iter::once(menu.bar_bounds) + .chain( + if menu.is_overlay { + state.menu_states[..state.menu_states.len().saturating_sub(1)].iter() + } else { + state.menu_states[..menu.depth].iter() + } + .map(|s| s.menu_bounds.children_bounds), ) + .collect::>(); + + if menu.is_overlay && menu.close_condition.leave { + for i in (0..state.menu_states.len()).rev() { + let mb = &state.menu_states[i].menu_bounds; + + if mb.parent_bounds.contains(overlay_cursor) + || menu.is_overlay && mb.children_bounds.contains(overlay_cursor) + || mb.offset_bounds.contains(overlay_cursor) + || (mb.check_bounds.contains(overlay_cursor) + && prev_bounds.iter().all(|pvb| !pvb.contains(overlay_cursor))) + { + break; + } + prev_bounds.pop(); + state.active_root.pop(); + state.menu_states.pop(); + } + } else if menu.is_overlay { + for i in (0..state.menu_states.len()).rev() { + let mb = &state.menu_states[i].menu_bounds; + + if mb.parent_bounds.contains(overlay_cursor) + || mb.children_bounds.contains(overlay_cursor) + || prev_bounds.iter().all(|pvb| !pvb.contains(overlay_cursor)) + { + break; + } + prev_bounds.pop(); + state.active_root.pop(); + state.menu_states.pop(); + } } - }; - // set new index - last_menu_state.index = Some(new_index); + // * update active item + let menu_states_len = state.menu_states.len(); - // get new active item - let item = &active_menu.children[new_index]; + let Some(last_menu_state) = state.menu_states.get_mut(if menu.is_overlay { + menu_states_len.saturating_sub(1) + } else { + menu.depth + }) else { + if menu.is_overlay { + // no menus left + // TODO do we want to avoid this for popups? + // state.active_root.remove(menu.depth); - // * add new menu if the new item is a menu - if !item.children.is_empty() { - let item_position = Point::new( - 0.0, - last_menu_bounds.child_positions[new_index] + last_menu_state.scroll_offset, - ); - let item_size = last_menu_bounds.child_sizes[new_index]; + // keep state.open when the cursor is still inside the menu bar + // this allows the overlay to keep drawing when the cursor is + // moving aroung the menu bar + if !menu.bar_bounds.contains(overlay_cursor) { + state.open = false; + } + } - // overlay space item bounds - let item_bounds = Rectangle::new(item_position, item_size) - + (last_menu_bounds.children_bounds.position() - Point::ORIGIN); - - let aod = Aod { - horizontal: true, - vertical: true, - horizontal_overlap: false, - vertical_overlap: true, - horizontal_direction: state.horizontal_direction, - vertical_direction: state.vertical_direction, - horizontal_offset: cross_offset, - vertical_offset: 0.0, + return (new_menu_root, Captured); }; - state.menu_states.push(MenuState { - index: None, - scroll_offset: 0.0, - menu_bounds: MenuBounds::new( - item, - renderer, - menu.item_width, - menu.item_height, - viewport_size, - overlay_offset, - &aod, - menu.bounds_expand, - item_bounds, - &mut menu.tree.children[active_root].children, - ), - }); - } + let last_menu_bounds = &last_menu_state.menu_bounds; + let last_parent_bounds = last_menu_bounds.parent_bounds; + let last_children_bounds = last_menu_bounds.children_bounds; - Captured + if (menu.is_overlay && !menu.menu_overlays_parent && last_parent_bounds.contains(overlay_cursor)) + // cursor is in the parent part + || menu.is_overlay && !last_children_bounds.contains(overlay_cursor) + // cursor is outside + { + + last_menu_state.index = None; + return (new_menu_root, Captured); + } + + // calc new index + let height_diff = (overlay_cursor.y + - (last_children_bounds.y + last_menu_state.scroll_offset)) + .clamp(0.0, last_children_bounds.height - 0.001); + + let active_root = if menu.is_overlay { + &state.active_root + } else { + &state.active_root[..=menu.depth] + }; + + if state.pressed { + return (new_menu_root, Ignored); + } + let roots = active_root.iter().skip(1).fold( + &menu.menu_roots[active_root[0]].children, + |mt, next_active_root| &mt[*next_active_root].children, + ); + let tree = &mut state.tree.children[active_root[0]].children; + + let active_menu: &Vec> = roots; + let new_index = match menu.item_height { + ItemHeight::Uniform(u) => (height_diff / f32::from(u)).floor() as usize, + ItemHeight::Static(_) | ItemHeight::Dynamic(_) => { + let max_index = active_menu.len() - 1; + search_bound( + 0, + 0, + max_index, + height_diff, + &last_menu_bounds.child_positions, + &last_menu_bounds.child_sizes, + ) + } + }; + + let remove = last_menu_state + .index + .as_ref() + .is_some_and(|i| *i != new_index && !active_menu[*i].children.is_empty()); + + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + { + if 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())({ + crate::surface::action::destroy_popup(id) + })); + } + } + } + let item = &active_menu[new_index]; + // set new index + let old_index = last_menu_state.index.replace(new_index); + + // get new active item + // * add new menu if the new item is a menu + if !item.children.is_empty() && old_index.is_none_or(|i| i != new_index) { + let item_position = Point::new( + 0.0, + last_menu_bounds.child_positions[new_index] + last_menu_state.scroll_offset, + ); + let item_size = last_menu_bounds.child_sizes[new_index]; + + // overlay space item bounds + let item_bounds = Rectangle::new(item_position, item_size) + + (last_menu_bounds.children_bounds.position() - Point::ORIGIN); + + let aod = Aod { + horizontal: true, + vertical: true, + horizontal_overlap: false, + vertical_overlap: true, + horizontal_direction: state.horizontal_direction, + vertical_direction: state.vertical_direction, + horizontal_offset: cross_offset, + vertical_offset: 0.0, + }; + let ms = MenuState { + index: None, + scroll_offset: 0.0, + menu_bounds: MenuBounds::new( + item, + renderer, + menu.item_width, + menu.item_height, + viewport_size, + overlay_offset, + &aod, + menu.bounds_expand, + item_bounds, + tree, + menu.is_overlay, + ), + }; + + new_menu_root = Some((new_index, ms.clone())); + if menu.is_overlay { + state.active_root.push(new_index); + } else { + state.menu_states.truncate(menu.depth + 1); + } + state.menu_states.push(ms); + } else if !menu.is_overlay && remove { + state.menu_states.truncate(menu.depth + 1); + } + + (new_menu_root, Captured) + }) } -fn process_scroll_events( - menu: &mut Menu<'_, '_, Message, Renderer>, +fn process_scroll_events( + menu: &mut Menu<'_, Message>, delta: mouse::ScrollDelta, overlay_cursor: Point, viewport_size: Size, overlay_offset: Vector, ) -> event::Status where - Renderer: renderer::Renderer, + Message: Clone, { use event::Status::{Captured, Ignored}; use mouse::ScrollDelta; - let state = menu.tree.state.downcast_mut::(); + menu.tree.inner.with_data_mut(|state| { + let delta_y = match delta { + ScrollDelta::Lines { y, .. } => y * 60.0, + ScrollDelta::Pixels { y, .. } => y, + }; - let delta_y = match delta { - ScrollDelta::Lines { y, .. } => y * 60.0, - ScrollDelta::Pixels { y, .. } => y, - }; + let calc_offset_bounds = |menu_state: &MenuState, viewport_size: Size| -> (f32, f32) { + // viewport space children bounds + let children_bounds = menu_state.menu_bounds.children_bounds + overlay_offset; - let calc_offset_bounds = |menu_state: &MenuState, viewport_size: Size| -> (f32, f32) { - // viewport space children bounds - let children_bounds = menu_state.menu_bounds.children_bounds + overlay_offset; + let max_offset = (0.0 - children_bounds.y).max(0.0); + let min_offset = + (viewport_size.height - (children_bounds.y + children_bounds.height)).min(0.0); + (max_offset, min_offset) + }; - let max_offset = (0.0 - children_bounds.y).max(0.0); - let min_offset = - (viewport_size.height - (children_bounds.y + children_bounds.height)).min(0.0); - (max_offset, min_offset) - }; + // update + if state.menu_states.is_empty() { + return Ignored; + } else if state.menu_states.len() == 1 { + let last_ms = &mut state.menu_states[0]; - // update - if state.menu_states.is_empty() { - return Ignored; - } else if state.menu_states.len() == 1 { - let last_ms = &mut state.menu_states[0]; - - if last_ms.index.is_none() { - return Captured; - } - - let (max_offset, min_offset) = calc_offset_bounds(last_ms, viewport_size); - last_ms.scroll_offset = (last_ms.scroll_offset + delta_y).clamp(min_offset, max_offset); - } else { - // >= 2 - let max_index = state.menu_states.len() - 1; - let last_two = &mut state.menu_states[max_index - 1..=max_index]; - - if last_two[1].index.is_some() { - // scroll the last one - let (max_offset, min_offset) = calc_offset_bounds(&last_two[1], viewport_size); - last_two[1].scroll_offset = - (last_two[1].scroll_offset + delta_y).clamp(min_offset, max_offset); - } else { - if !last_two[0] - .menu_bounds - .children_bounds - .contains(overlay_cursor) - { + if last_ms.index.is_none() { return Captured; } - // scroll the second last one - let (max_offset, min_offset) = calc_offset_bounds(&last_two[0], viewport_size); - let scroll_offset = (last_two[0].scroll_offset + delta_y).clamp(min_offset, max_offset); - let clamped_delta_y = scroll_offset - last_two[0].scroll_offset; - last_two[0].scroll_offset = scroll_offset; + let (max_offset, min_offset) = calc_offset_bounds(last_ms, viewport_size); + last_ms.scroll_offset = (last_ms.scroll_offset + delta_y).clamp(min_offset, max_offset); + } else { + // >= 2 + let max_index = state.menu_states.len() - 1; + let last_two = &mut state.menu_states[max_index - 1..=max_index]; - // update the bounds of the last one - last_two[1].menu_bounds.parent_bounds.y += clamped_delta_y; - last_two[1].menu_bounds.children_bounds.y += clamped_delta_y; - last_two[1].menu_bounds.check_bounds.y += clamped_delta_y; + if last_two[1].index.is_some() { + // scroll the last one + let (max_offset, min_offset) = calc_offset_bounds(&last_two[1], viewport_size); + last_two[1].scroll_offset = + (last_two[1].scroll_offset + delta_y).clamp(min_offset, max_offset); + } else { + if !last_two[0] + .menu_bounds + .children_bounds + .contains(overlay_cursor) + { + return Captured; + } + + // scroll the second last one + let (max_offset, min_offset) = calc_offset_bounds(&last_two[0], viewport_size); + let scroll_offset = + (last_two[0].scroll_offset + delta_y).clamp(min_offset, max_offset); + let clamped_delta_y = scroll_offset - last_two[0].scroll_offset; + last_two[0].scroll_offset = scroll_offset; + + // update the bounds of the last one + last_two[1].menu_bounds.parent_bounds.y += clamped_delta_y; + last_two[1].menu_bounds.children_bounds.y += clamped_delta_y; + last_two[1].menu_bounds.check_bounds.y += clamped_delta_y; + } } - } - Captured + Captured + }) } #[allow(clippy::pedantic)] /// Returns (children_size, child_positions, child_sizes) -fn get_children_layout( - menu_tree: &MenuTree<'_, Message, Renderer>, - renderer: &Renderer, +fn get_children_layout( + menu_tree: &MenuTree, + renderer: &crate::Renderer, item_width: ItemWidth, item_height: ItemHeight, tree: &mut [Tree], -) -> (Size, Vec, Vec) -where - Renderer: renderer::Renderer, -{ +) -> (Size, Vec, Vec) { let width = match item_width { ItemWidth::Uniform(u) => f32::from(u), ItemWidth::Static(s) => f32::from(menu_tree.width.unwrap_or(s)), @@ -1183,37 +1642,39 @@ where .children .iter() .map(|mt| { - let w = mt.item.as_widget(); - match w.size().height { - Length::Fixed(f) => Size::new(width, f), - Length::Shrink => { - let l_height = w - .layout( - &mut tree[mt.index], - renderer, - &Limits::new(Size::ZERO, Size::new(width, f32::MAX)), - ) - .size() - .height; + mt.item + .element + .with_data(|w| match w.as_widget().size().height { + Length::Fixed(f) => Size::new(width, f), + Length::Shrink => { + let l_height = w + .as_widget() + .layout( + &mut tree[mt.index], + renderer, + &Limits::new(Size::ZERO, Size::new(width, f32::MAX)), + ) + .size() + .height; - let height = if (f32::MAX - l_height) < 0.001 { - f32::from(d) - } else { - l_height - }; + let height = if (f32::MAX - l_height) < 0.001 { + f32::from(d) + } else { + l_height + }; - Size::new(width, height) - } - _ => mt.height.map_or_else( - || Size::new(width, f32::from(d)), - |h| Size::new(width, f32::from(h)), - ), - } + Size::new(width, height) + } + _ => mt.height.map_or_else( + || Size::new(width, f32::from(d)), + |h| Size::new(width, f32::from(h)), + ), + }) }) .collect(), }; - let max_index = menu_tree.children.len() - 1; + let max_index = menu_tree.children.len().saturating_sub(1); let child_positions: Vec = std::iter::once(0.0) .chain(child_sizes[0..max_index].iter().scan(0.0, |acc, x| { *acc += x.height; diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 1f3fd4ab..d02b2b27 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -11,7 +11,7 @@ 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, icon}; +use crate::widget::{Button, RcElementWrapper, icon}; use crate::{theme, widget}; /// Nested menu is essentially a tree of items, a menu is a collection of items @@ -23,27 +23,25 @@ use crate::{theme, widget}; /// but there's no need to explicitly distinguish them here, if a menu tree /// has children, it's a menu, otherwise it's an item #[allow(missing_debug_implementations)] -pub struct MenuTree<'a, Message, Renderer = crate::Renderer> { +#[derive(Clone)] +pub struct MenuTree { /// The menu tree will be flatten into a vector to build a linear widget tree, /// the `index` field is the index of the item in that vector pub(crate) index: usize, /// The item of the menu tree - pub(crate) item: Element<'a, Message, crate::Theme, Renderer>, + pub(crate) item: RcElementWrapper, /// The children of the menu tree - pub(crate) children: Vec>, + pub(crate) children: Vec>, /// The width of the menu tree pub(crate) width: Option, /// The height of the menu tree pub(crate) height: Option, } -impl<'a, Message, Renderer> MenuTree<'a, Message, Renderer> -where - Renderer: renderer::Renderer, -{ +impl MenuTree { /// Create a new menu tree from a widget - pub fn new(item: impl Into>) -> Self { + pub fn new(item: impl Into>) -> Self { Self { index: 0, item: item.into(), @@ -55,8 +53,8 @@ where /// Create a menu tree from a widget and a vector of sub trees pub fn with_children( - item: impl Into>, - children: Vec>>, + item: impl Into>, + children: Vec>>, ) -> Self { Self { index: 0, @@ -92,7 +90,7 @@ where /// Set the index of each item pub(crate) fn set_index(&mut self) { /// inner counting function. - fn rec(mt: &mut MenuTree<'_, Message, Renderer>, count: &mut usize) { + fn rec(mt: &mut MenuTree, count: &mut usize) { // keep items under the same menu line up mt.children.iter_mut().for_each(|c| { c.index = *count; @@ -109,18 +107,18 @@ where } /// Flatten the menu tree - pub(crate) fn flattern(&'a self) -> Vec<&Self> { + pub(crate) fn flattern(&self) -> Vec<&Self> { /// Inner flattening function - fn rec<'a, Message, Renderer>( - mt: &'a MenuTree<'a, Message, Renderer>, - flat: &mut Vec<&MenuTree<'a, Message, Renderer>>, + fn rec<'a, Message: Clone + 'static>( + mt: &'a MenuTree, + flat: &mut Vec<&'a MenuTree>, ) { mt.children.iter().for_each(|c| { flat.push(c); }); mt.children.iter().for_each(|c| { - rec(c, flat); + rec(&c, flat); }); } @@ -132,13 +130,9 @@ where } } -impl<'a, Message, Renderer> From> - for MenuTree<'a, Message, Renderer> -where - Renderer: renderer::Renderer, -{ - fn from(value: Element<'a, Message, crate::Theme, Renderer>) -> Self { - Self::new(value) +impl From> for MenuTree { + fn from(value: crate::Element<'static, Message>) -> Self { + Self::new(RcElementWrapper::new(value)) } } @@ -160,6 +154,7 @@ where .class(theme::Button::MenuItem) } +#[derive(Clone)] /// Represents a menu item that performs an action when selected or a separator between menu items. /// /// - `Action` - Represents a menu item that performs an action when selected. @@ -215,20 +210,15 @@ where /// /// # Returns /// - A vector of `MenuTree`. +#[must_use] pub fn menu_items< - 'a, A: MenuAction, L: Into> + 'static, - Message, - Renderer: renderer::Renderer + 'a, + Message: 'static + std::clone::Clone, >( key_binds: &HashMap, children: Vec>, -) -> Vec> -where - Element<'a, Message, crate::Theme, Renderer>: From>, - Message: 'a + Clone, -{ +) -> Vec> { fn find_key(action: &A, key_binds: &HashMap) -> String { for (key_bind, key_action) in key_binds { if action == key_action { @@ -249,9 +239,10 @@ where match item { MenuItem::Button(label, icon, action) => { + let l: Cow<'static, str> = label.into(); let key = find_key(&action, key_binds); let mut items = vec![ - widget::text(label).into(), + widget::text(l.clone()).into(), widget::horizontal_space().into(), widget::text(key).into(), ]; @@ -261,15 +252,18 @@ where items.insert(1, widget::Space::with_width(spacing.space_xxs).into()); } + // dbg!("button with action...", action.message()); let menu_button = menu_button(items).on_press(action.message()); - trees.push(MenuTree::::new(menu_button)); + trees.push(MenuTree::::from(Element::from(menu_button))); } MenuItem::ButtonDisabled(label, icon, action) => { + let l: Cow<'static, str> = label.into(); + let key = find_key(&action, key_binds); let mut items = vec![ - widget::text(label).into(), + widget::text(l.clone()).into(), widget::horizontal_space().into(), widget::text(key).into(), ]; @@ -281,7 +275,7 @@ where let menu_button = menu_button(items); - trees.push(MenuTree::::new(menu_button)); + trees.push(MenuTree::::from(Element::from(menu_button))); } MenuItem::CheckBox(label, icon, value, action) => { let key = find_key(&action, key_binds); @@ -311,36 +305,42 @@ where items.insert(2, widget::icon::icon(icon).size(14).into()); } - trees.push(MenuTree::new(menu_button(items).on_press(action.message()))); + trees.push(MenuTree::from(Element::from( + menu_button(items).on_press(action.message()), + ))); } MenuItem::Folder(label, children) => { - trees.push(MenuTree::::with_children( - menu_button(vec![ - widget::text(label).into(), - widget::horizontal_space().into(), - widget::icon::from_name("pan-end-symbolic") - .size(16) - .icon() - .into(), - ]) - .class( - // Menu folders have no on_press so they take on the disabled style by default - if children.is_empty() { - // This will make the folder use the disabled style if it has no children - theme::Button::MenuItem - } else { - // This will make the folder use the enabled style if it has children - theme::Button::MenuFolder - }, - ), + let l: Cow<'static, str> = label.into(); + + trees.push(MenuTree::::with_children( + RcElementWrapper::new(crate::Element::from( + menu_button::<'static, _>(vec![ + widget::text(l.clone()).into(), + widget::horizontal_space().into(), + widget::icon::from_name("pan-end-symbolic") + .size(16) + .icon() + .into(), + ]) + .class( + // Menu folders have no on_press so they take on the disabled style by default + if children.is_empty() { + // This will make the folder use the disabled style if it has no children + theme::Button::MenuItem + } else { + // This will make the folder use the enabled style if it has children + theme::Button::MenuFolder + }, + ), + )), menu_items(key_binds, children), )); } MenuItem::Divider => { if i != size - 1 { - trees.push(MenuTree::::new( + trees.push(MenuTree::::from(Element::from( widget::divider::horizontal::light(), - )); + ))); } } } diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index fef3cbe4..6923472a 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -69,7 +69,7 @@ impl<'a, Message: Clone + 'static> NavBar<'a, Message> { } #[inline] - pub fn context_menu(mut self, context_menu: Option>>) -> Self { + pub fn context_menu(mut self, context_menu: Option>>) -> Self { self.segmented_button = self.segmented_button.context_menu(context_menu); self } diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index 65c5d3eb..3d9557d0 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -9,6 +9,7 @@ use crate::{ use super::menu::{self, ItemHeight, ItemWidth}; +#[must_use] pub fn responsive_menu_bar() -> ResponsiveMenuBar { ResponsiveMenuBar::default() } @@ -33,18 +34,21 @@ impl Default for ResponsiveMenuBar { impl ResponsiveMenuBar { /// Set the item width + #[must_use] pub fn item_width(mut self, item_width: ItemWidth) -> Self { self.item_width = item_width; self } /// Set the item height + #[must_use] pub fn item_height(mut self, item_height: ItemHeight) -> Self { self.item_height = item_height; self } /// Set the spacing + #[must_use] pub fn spacing(mut self, spacing: f32) -> Self { self.spacing = spacing; self @@ -56,14 +60,14 @@ impl ResponsiveMenuBar { pub fn into_element< 'a, Message: Clone + 'static, - A: menu::Action, + A: menu::Action + Clone, S: Into> + 'static, >( self, core: &Core, key_binds: &HashMap, id: crate::widget::Id, - action_message: impl Fn(crate::surface::Action) -> Message + 'static, + action_message: impl Fn(crate::surface::Action) -> Message + Send + Sync + Clone + 'static, trees: Vec<(S, Vec>)>, ) -> Element<'a, Message> { use crate::widget::id_container; @@ -80,17 +84,21 @@ impl ResponsiveMenuBar { menu::bar( trees .into_iter() - .map(|mt| { + .map(|mt: (S, Vec>)| { menu::Tree::<_>::with_children( - menu::root(mt.0), - menu::items(key_binds, mt.1.into()), + crate::widget::RcElementWrapper::new(Element::from( + menu::root(mt.0), + )), + menu::items(key_binds, mt.1), ) }) .collect(), ) .item_width(self.item_width) .item_height(self.item_height) - .spacing(self.spacing), + .spacing(self.spacing) + .on_surface_action(action_message.clone()) + .window_id_maybe(core.main_window_id()), crate::widget::Id::new(format!("menu_bar_expanded_{id}")), ), id, @@ -123,7 +131,9 @@ impl ResponsiveMenuBar { )]) .item_height(self.item_height) .item_width(self.collapsed_item_width) - .spacing(self.spacing), + .spacing(self.spacing) + .on_surface_action(action_message.clone()) + .window_id_maybe(core.main_window_id()), crate::widget::Id::new(format!("menu_bar_collapsed_{id}")), ), id, diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 3cb64e2d..313b686d 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -17,7 +17,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, + event, keyboard, mouse, touch, window, }; use iced_core::mouse::ScrollDelta; use iced_core::text::{LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; @@ -127,7 +127,7 @@ where pub(super) style: Style, /// The context menu to display when a context is activated #[setters(skip)] - pub(super) context_menu: Option>>, + pub(super) context_menu: Option>>, /// Emits the ID of the item that was activated. #[setters(skip)] pub(super) on_activate: Option Message + 'static>>, @@ -198,13 +198,13 @@ where } } - pub fn context_menu(mut self, context_menu: Option>>) -> Self + pub fn context_menu(mut self, context_menu: Option>>) -> Self where - Message: 'static, + Message: Clone + 'static, { self.context_menu = context_menu.map(|menus| { vec![menu::Tree::with_children( - crate::widget::row::<'static, Message>(), + crate::Element::from(crate::widget::row::<'static, Message>()), menus, )] }); @@ -577,6 +577,7 @@ where fn state(&self) -> tree::State { #[allow(clippy::default_trait_access)] tree::State::new(LocalState { + menu_state: Default::default(), paragraphs: SecondaryMap::new(), text_hashes: SecondaryMap::new(), buttons_visible: Default::default(), @@ -955,8 +956,10 @@ where let menu_state = tree.children[0].state.downcast_mut::(); - menu_state.open = true; - menu_state.view_cursor = cursor_position; + menu_state.inner.with_data_mut(|data| { + data.open = true; + data.view_cursor = cursor_position; + }); shell.publish(on_context(key)); return event::Status::Captured; @@ -1346,7 +1349,11 @@ where let center_y = bounds.center_y(); let menu_open = !tree.children.is_empty() - && tree.children[0].state.downcast_ref::().open; + && tree.children[0] + .state + .downcast_ref::() + .inner + .with_data(|data| data.open); let key_is_active = self.model.is_active(key); let key_is_hovered = self.button_is_hovered(state, key); @@ -1556,6 +1563,7 @@ where translation: Vector, ) -> Option> { let state = tree.state.downcast_ref::(); + let menu_state = state.menu_state.clone(); let Some(entity) = state.show_context else { return None; @@ -1575,7 +1583,12 @@ where return None; }; - if !tree.children[0].state.downcast_ref::().open { + if !tree.children[0] + .state + .downcast_ref::() + .inner + .with_data(|data| data.open) + { return None; } @@ -1584,8 +1597,8 @@ where Some( crate::widget::menu::Menu { - tree: &mut tree.children[0], - menu_roots: context_menu, + tree: menu_state, + menu_roots: std::borrow::Cow::Borrowed(context_menu), bounds_expand: 16, menu_overlays_parent: true, close_condition: CloseCondition { @@ -1600,8 +1613,12 @@ where cross_offset: 0, root_bounds_list: vec![bounds], path_highlight: Some(PathHighlight::MenuActive), - style: &crate::theme::menu_bar::MenuBarStyle::Default, + style: std::borrow::Cow::Borrowed(&crate::theme::menu_bar::MenuBarStyle::Default), position: Point::new(translation.x, translation.y), + is_overlay: true, + window_id: window::Id::NONE, + depth: 0, + on_surface_action: None, } .overlay(), ) @@ -1653,6 +1670,8 @@ where /// State that is maintained by each individual widget. pub struct LocalState { + /// Menu state + pub(crate) menu_state: MenuBarState, /// Defines how many buttons to show at a time. pub(super) buttons_visible: usize, /// Button visibility offset, when collapsed. diff --git a/src/widget/table/mod.rs b/src/widget/table/mod.rs index 7063dc8e..c546383c 100644 --- a/src/widget/table/mod.rs +++ b/src/widget/table/mod.rs @@ -20,9 +20,9 @@ pub type MultiSelectTableView<'a, Item, Category, Message> = TableView<'a, MultiSelect, Item, Category, Message>; pub type MultiSelectModel = Model; -pub fn table<'a, SelectionMode, Item, Category, Message>( - model: &'a Model, -) -> TableView<'a, SelectionMode, Item, Category, Message> +pub fn table( + model: &Model, +) -> TableView<'_, SelectionMode, Item, Category, Message> where Message: Clone, SelectionMode: Default, @@ -33,9 +33,9 @@ where TableView::new(model) } -pub fn compact_table<'a, SelectionMode, Item, Category, Message>( - model: &'a Model, -) -> CompactTableView<'a, SelectionMode, Item, Category, Message> +pub fn compact_table( + model: &Model, +) -> CompactTableView<'_, SelectionMode, Item, Category, Message> where Message: Clone, SelectionMode: Default, diff --git a/src/widget/table/widget/compact.rs b/src/widget/table/widget/compact.rs index 43a32de2..47864f6d 100644 --- a/src/widget/table/widget/compact.rs +++ b/src/widget/table/widget/compact.rs @@ -44,7 +44,7 @@ where #[setters(skip)] pub(super) on_item_mb_right: Option Message + 'static>>, #[setters(skip)] - pub(super) item_context_builder: Box Option>>>, + pub(super) item_context_builder: Box Option>>>, } impl<'a, SelectionMode, Item, Category, Message> @@ -97,7 +97,7 @@ where ] }) .flatten() - .collect::>>(); + .collect::>>(); elements.pop(); elements .apply(widget::row::with_children) @@ -247,7 +247,7 @@ where pub fn item_context(mut self, context_menu_builder: F) -> Self where - F: Fn(&Item) -> Option>> + 'static, + F: Fn(&Item) -> Option>> + 'static, Message: 'static, { self.item_context_builder = Box::new(context_menu_builder); diff --git a/src/widget/table/widget/standard.rs b/src/widget/table/widget/standard.rs index 01d0ea56..eb9ba7a4 100644 --- a/src/widget/table/widget/standard.rs +++ b/src/widget/table/widget/standard.rs @@ -51,7 +51,7 @@ where #[setters(skip)] pub(super) on_item_mb_right: Option Message + 'static>>, #[setters(skip)] - pub(super) item_context_builder: Box Option>>>, + pub(super) item_context_builder: Box Option>>>, // Item DND // === Category Interaction === @@ -64,8 +64,7 @@ where #[setters(skip)] pub(super) on_category_mb_right: Option Message + 'static>>, #[setters(skip)] - pub(super) category_context_builder: - Box Option>>>, + pub(super) category_context_builder: Box Option>>>, } impl<'a, SelectionMode, Item, Category, Message> @@ -83,7 +82,7 @@ where .model .categories .iter() - .cloned() + .copied() .map(|category| { let cat_context_tree = (val.category_context_builder)(category); @@ -167,7 +166,7 @@ where .align_y(Alignment::Center) .apply(Element::from) }) - .collect::>>() + .collect::>>() .apply(widget::row::with_children) .apply(container) .padding(val.item_padding) @@ -328,7 +327,7 @@ where pub fn item_context(mut self, context_menu_builder: F) -> Self where - F: Fn(&Item) -> Option>> + 'static, + F: Fn(&Item) -> Option>> + 'static, Message: 'static, { self.item_context_builder = Box::new(context_menu_builder); @@ -367,7 +366,7 @@ where pub fn category_context(mut self, context_menu_builder: F) -> Self where - F: Fn(Category) -> Option>> + 'static, + F: Fn(Category) -> Option>> + 'static, Message: 'static, { self.category_context_builder = Box::new(context_menu_builder); diff --git a/src/widget/wrapper.rs b/src/widget/wrapper.rs index 0579c4b2..92f26fd4 100644 --- a/src/widget/wrapper.rs +++ b/src/widget/wrapper.rs @@ -1,4 +1,5 @@ use std::{ + borrow::Borrow, cell::RefCell, rc::Rc, thread::{self, ThreadId}, @@ -14,6 +15,12 @@ pub struct RcWrapper { pub(crate) thread_id: ThreadId, } +impl Default for RcWrapper { + fn default() -> Self { + Self::new(T::default()) + } +} + impl Clone for RcWrapper { fn clone(&self) -> Self { Self { @@ -75,6 +82,12 @@ impl RcElementWrapper { } } +impl Borrow> for RcElementWrapper { + fn borrow(&self) -> &(dyn Widget + 'static) { + self + } +} + impl Widget for RcElementWrapper { fn size(&self) -> Size { self.element.with_data(|e| e.as_widget().size()) From 7f2d34ead4a3934fd7f295815ffe170c22cbade4 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 10 Jun 2025 16:55:30 -0400 Subject: [PATCH 004/352] fix(menu-bar): exit early from popup creation if no root is hovered --- iced | 2 +- src/widget/menu/menu_bar.rs | 9 +++++---- 2 files changed, 6 insertions(+), 5 deletions(-) diff --git a/iced b/iced index 717bc5db..fc9d49eb 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 717bc5dbfbc8f78e367e08e76a9572ee0ebc1f32 +Subproject commit fc9d49eb2f830fe394252ff6799d59ad828243bc diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index eddc4f3a..2c355bf4 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -375,13 +375,14 @@ where let hovered_root = layout .children() .position(|lo| view_cursor.is_over(lo.bounds())); - - if old_active_root - .zip(hovered_root) - .is_some_and(|r| r.0 == r.1) + if hovered_root.is_none() + || old_active_root + .zip(hovered_root) + .is_some_and(|r| r.0 == r.1) { return; } + let (id, root_list) = my_state.inner.with_data_mut(|state| { if let Some(id) = state.popup_id.get(&self.window_id).copied() { // close existing popups From 96416c2a3fc217627308ea877979ffd25661b68d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 10 Jun 2025 16:56:02 -0400 Subject: [PATCH 005/352] fix(button): return from draw if there is no content layout --- src/widget/button/widget.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index aa8f0c32..da8612f7 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -437,7 +437,11 @@ impl<'a, Message: 'a + Clone> Widget if !viewport.intersects(&bounds) { return; } - let content_layout = layout.children().next().unwrap(); + + // FIXME: Why is there no content layout + let Some(content_layout) = layout.children().next() else { + return; + }; let mut headerbar_alpha = None; From f835afa59c3890ee0e16933bbf7d03263447c285 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 11 Jun 2025 09:24:02 +0200 Subject: [PATCH 006/352] fix(segmented_button): unfocus when clicking out of bounds --- src/widget/segmented_button/widget.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 313b686d..a94763d6 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -935,6 +935,7 @@ where } if can_activate { + eprintln!("can activate focus"); shell.publish(on_activate(key)); state.focused = true; state.focused_item = Item::Tab(key); @@ -1036,6 +1037,16 @@ where } } } + } else if state.focused { + // Unfocus on clicks outside of the boundaries of the segmented button. + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) = event + { + state.focused = true; + state.focused_item = Item::None; + state.pressed_item = None; + return event::Status::Ignored; + } } if state.focused { @@ -1724,11 +1735,13 @@ impl operation::Focusable for LocalState { } fn focus(&mut self) { + eprintln!("focus"); self.focused = true; self.focused_item = Item::Set; } fn unfocus(&mut self) { + eprintln!("unfocus"); self.focused = false; self.focused_item = Item::None; self.show_context = None; From 8edbbec1e8983c7a30f8867688f432798ec99af7 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 11 Jun 2025 09:25:21 +0200 Subject: [PATCH 007/352] fix!(desktop): support launching terminal-based desktop entries --- Cargo.toml | 2 ++ src/desktop.rs | 30 +++++++++++++++++++++++++++--- 2 files changed, 29 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index e9934e45..a90b0e73 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,7 @@ rfd = ["dep:rfd"] # Enables desktop files helpers desktop = [ "process", + "dep:cosmic-settings-config", "dep:freedesktop-desktop-entry", "dep:mime", "dep:shlex", @@ -105,6 +106,7 @@ auto_enums = "0.8.7" cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "178eb0b", optional = true } chrono = "0.4.40" cosmic-config = { path = "cosmic-config" } +cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } css-color = "0.2.8" derive_setters = "0.1.6" diff --git a/src/desktop.rs b/src/desktop.rs index c9c4c491..34673f91 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -47,6 +47,7 @@ pub struct DesktopEntryData { pub desktop_actions: Vec, pub mime_types: Vec, pub prefers_dgpu: bool, + pub terminal: bool, } #[cfg(not(windows))] @@ -196,6 +197,7 @@ impl DesktopEntryData { }) .unwrap_or_default(), prefers_dgpu: de.prefers_non_default_gpu(), + terminal: de.terminal(), path: Some(de.path), } } @@ -203,14 +205,36 @@ impl DesktopEntryData { #[cfg(not(windows))] #[cold] -pub async fn spawn_desktop_exec(exec: S, env_vars: I, app_id: Option<&str>) -where +pub async fn spawn_desktop_exec( + exec: S, + env_vars: I, + app_id: Option<&str>, + terminal: bool, +) where S: AsRef, I: IntoIterator, K: AsRef, V: AsRef, { - let mut exec = shlex::Shlex::new(exec.as_ref()); + let term_exec; + + let exec_str = if terminal { + let term = cosmic_settings_config::shortcuts::context() + .ok() + .and_then(|config| { + cosmic_settings_config::shortcuts::system_actions(&config) + .get(&cosmic_settings_config::shortcuts::action::System::Terminal) + .cloned() + }) + .unwrap_or_else(|| String::from("cosmic-term")); + + term_exec = format!("{term} -- {}", exec.as_ref()); + &term_exec + } else { + exec.as_ref() + }; + + let mut exec = shlex::Shlex::new(exec_str); let executable = match exec.next() { Some(executable) if !executable.contains('=') => executable, From 3f4a50ee2c3c04732b28497572e86101b4d9a055 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 11 Jun 2025 11:49:32 +0200 Subject: [PATCH 008/352] chore: remove eprintln logs --- src/widget/segmented_button/widget.rs | 3 --- src/widget/text_input/input.rs | 1 - 2 files changed, 4 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index a94763d6..07e6cbc1 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -935,7 +935,6 @@ where } if can_activate { - eprintln!("can activate focus"); shell.publish(on_activate(key)); state.focused = true; state.focused_item = Item::Tab(key); @@ -1735,13 +1734,11 @@ impl operation::Focusable for LocalState { } fn focus(&mut self) { - eprintln!("focus"); self.focused = true; self.focused_item = Item::Set; } fn unfocus(&mut self) { - eprintln!("unfocus"); self.focused = false; self.focused_item = Item::None; self.show_context = None; diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index eed1bed1..06a193b9 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -882,7 +882,6 @@ where if let Some(on_unfocus) = self.on_unfocus.as_ref() { if state.emit_unfocus { state.emit_unfocus = false; - eprintln!("unfocus"); shell.publish(on_unfocus.clone()); } } From 4c6061d40a17c0cbcc2ec66edc15ca3e476b14ff Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 11 Jun 2025 11:03:14 -0400 Subject: [PATCH 009/352] fix(menu inner): avoid unnecessary panic in debug builds. --- src/widget/menu/menu_inner.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 8ebca090..c41cded2 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -1161,7 +1161,6 @@ pub(super) fn init_root_menu( break; } } - debug_assert!(set, "Root not set"); }); } @@ -1242,8 +1241,6 @@ pub(super) fn init_root_popup_menu( // Hack to ensure menu opens properly shell.invalidate_layout(); - // non tree buttons arent active? - debug_assert!(set, "Root popup menu state was not set."); }); } From ba72aed6fbb574131d62e917cc22581af0d2a39e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 11 Jun 2025 14:50:25 -0400 Subject: [PATCH 010/352] feat: context menu popups --- examples/context-menu/src/main.rs | 20 ++- src/widget/context_menu.rs | 264 +++++++++++++++++++++++++++++- src/widget/menu.rs | 2 +- src/widget/menu/menu_bar.rs | 2 +- src/widget/menu/menu_inner.rs | 10 +- 5 files changed, 283 insertions(+), 15 deletions(-) diff --git a/examples/context-menu/src/main.rs b/examples/context-menu/src/main.rs index 4a307840..c744f963 100644 --- a/examples/context-menu/src/main.rs +++ b/examples/context-menu/src/main.rs @@ -27,9 +27,8 @@ fn main() -> Result<(), Box> { #[derive(Clone, Debug)] pub enum Message { Clicked, - ShowContext, WindowClose, - ShowWindowMenu, + Surface(cosmic::surface::Action), ToggleHideContent, WindowNew, } @@ -85,7 +84,19 @@ impl cosmic::Application for App { /// Handle application events here. fn update(&mut self, message: Self::Message) -> Task { - self.button_label = format!("Clicked {message:?}"); + match message { + Message::Clicked => { + self.button_label = format!("Clicked {message:?}"); + } + Message::Surface(action) => { + return cosmic::task::message(cosmic::Action::Cosmic( + cosmic::app::Action::Surface(action), + )); + } + Message::WindowClose => {} + Message::ToggleHideContent => {} + Message::WindowNew => {} + } Task::none() } @@ -95,7 +106,8 @@ impl cosmic::Application for App { let widget = cosmic::widget::context_menu( cosmic::widget::button::text(self.button_label.to_string()).on_press(Message::Clicked), self.context_menu(), - ); + ) + .on_surface_action(Message::Surface); let centered = cosmic::widget::container(widget) .width(iced::Length::Fill) diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 6769dff2..a00ae751 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -4,7 +4,8 @@ //! A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation. use crate::widget::menu::{ - self, CloseCondition, ItemHeight, ItemWidth, MenuBarState, PathHighlight, menu_roots_diff, + self, CloseCondition, Direction, ItemHeight, ItemWidth, MenuBarState, PathHighlight, + init_root_menu, menu_roots_diff, }; use derive_setters::Setters; use iced::touch::Finger; @@ -12,6 +13,7 @@ use iced::{Event, Vector, window}; use iced_core::widget::{Tree, Widget, tree}; use iced_core::{Length, Point, Size, event, mouse, touch}; use std::collections::HashSet; +use std::sync::Arc; /// A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation. pub fn context_menu( @@ -27,6 +29,8 @@ pub fn context_menu( menus, )] }), + window_id: window::Id::RESERVED, + on_surface_action: None, }; if let Some(ref mut context_menu) = this.context_menu { @@ -44,6 +48,156 @@ pub struct ContextMenu<'a, Message> { content: crate::Element<'a, Message>, #[setters(skip)] context_menu: Option>>, + pub window_id: window::Id, + #[setters(skip)] + pub(crate) on_surface_action: + Option Message + Send + Sync + 'static>>, +} + +impl ContextMenu<'_, Message> { + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[allow(clippy::too_many_lines)] + fn create_popup( + &mut self, + layout: iced_core::Layout<'_>, + view_cursor: iced_core::mouse::Cursor, + renderer: &crate::Renderer, + shell: &mut iced_core::Shell<'_, Message>, + viewport: &iced::Rectangle, + my_state: &mut LocalState, + ) { + if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { + use crate::{surface::action::destroy_popup, widget::menu::Menu}; + use iced_runtime::platform_specific::wayland::popup::{ + SctkPopupSettings, SctkPositioner, + }; + + let mut bounds = layout.bounds(); + bounds.x = my_state.context_cursor.x; + bounds.y = my_state.context_cursor.y; + + let (id, root_list) = my_state.menu_bar_state.inner.with_data_mut(|state| { + if let Some(id) = state.popup_id.get(&self.window_id).copied() { + // close existing popups + state.menu_states.clear(); + state.active_root.clear(); + shell.publish(self.on_surface_action.as_ref().unwrap()(destroy_popup(id))); + state.view_cursor = view_cursor; + ( + id, + layout.children().map(|lo| lo.bounds()).collect::>(), + ) + } else { + ( + window::Id::unique(), + layout.children().map(|lo| lo.bounds()).collect(), + ) + } + }); + let Some(context_menu) = self.context_menu.as_mut() else { + return; + }; + + let mut popup_menu: Menu<'static, _> = Menu { + tree: my_state.menu_bar_state.clone(), + menu_roots: std::borrow::Cow::Owned(context_menu.clone()), + bounds_expand: 16, + menu_overlays_parent: true, + close_condition: CloseCondition { + leave: false, + click_outside: true, + click_inside: true, + }, + item_width: ItemWidth::Uniform(240), + item_height: ItemHeight::Dynamic(40), + bar_bounds: bounds, + main_offset: -(bounds.height as i32), + cross_offset: 0, + root_bounds_list: vec![bounds], + path_highlight: Some(PathHighlight::MenuActive), + style: std::borrow::Cow::Owned(crate::theme::menu_bar::MenuBarStyle::Default), + position: Point::new(0., 0.), + is_overlay: false, + window_id: id, + depth: 0, + on_surface_action: self.on_surface_action.clone(), + }; + + init_root_menu( + &mut popup_menu, + renderer, + shell, + view_cursor.position().unwrap(), + viewport.size(), + Vector::new(0., 0.), + layout.bounds(), + -bounds.height, + ); + let (anchor_rect, gravity) = my_state.menu_bar_state.inner.with_data_mut(|state| { + use iced::Rectangle; + + state.popup_id.insert(self.window_id, id); + ({ + let pos = view_cursor.position().unwrap_or_default(); + Rectangle { + x: pos.x as i32, + y: pos.y as i32, + width: 1, + height: 1, + } + }, + match (state.horizontal_direction, state.vertical_direction) { + (Direction::Positive, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, + (Direction::Positive, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopRight, + (Direction::Negative, Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomLeft, + (Direction::Negative, Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopLeft, + }) + }); + + let menu_node = + popup_menu.layout(renderer, iced::Limits::NONE.min_width(1.).min_height(1.)); + let popup_size = menu_node.size(); + let positioner = SctkPositioner { + size: Some(( + popup_size.width.ceil() as u32 + 2, + popup_size.height.ceil() as u32 + 2, + )), + anchor_rect, + anchor: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::None, + gravity: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, + reactive: true, + ..Default::default() + }; + let parent = self.window_id; + shell.publish((self.on_surface_action.as_ref().unwrap())( + crate::surface::action::simple_popup( + move || SctkPopupSettings { + parent, + id, + positioner: positioner.clone(), + parent_size: None, + grab: true, + close_with_children: false, + input_zone: None, + }, + Some(move || { + crate::Element::from( + crate::widget::container(popup_menu.clone()).center(Length::Fill), + ) + .map(crate::action::app) + }), + ), + )); + } + } + + pub fn on_surface_action( + mut self, + handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static, + ) -> Self { + self.on_surface_action = Some(Arc::new(handler)); + self + } } impl Widget @@ -155,6 +309,7 @@ impl Widget .operate(&mut tree.children[0], layout, renderer, operation); } + #[allow(clippy::too_many_lines)] fn on_event( &mut self, tree: &mut Tree, @@ -169,6 +324,25 @@ impl Widget let state = tree.state.downcast_mut::(); let bounds = layout.bounds(); + // XXX this should reset the state if there are no other copies of the state, which implies no dropdown menus open. + let reset = self.window_id != window::Id::NONE + && state + .menu_bar_state + .inner + .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(); + } + } + } + state.open + }); + if cursor.is_over(bounds) { let fingers_pressed = state.fingers_pressed.len(); @@ -181,6 +355,29 @@ impl Widget state.fingers_pressed.remove(&id); } + Event::Window(window::Event::Focused) => { + #[cfg(all( + feature = "wayland", + feature = "winit", + feature = "surface-message" + ))] + state.menu_bar_state.inner.with_data_mut(|state| { + if let Some(id) = state.popup_id.remove(&self.window_id) { + state.menu_states.clear(); + state.active_root.clear(); + state.open = false; + + { + let surface_action = self.on_surface_action.as_ref().unwrap(); + shell.publish(surface_action( + crate::surface::action::destroy_popup(id), + )); + } + state.view_cursor = cursor; + } + }); + } + _ => (), } @@ -190,13 +387,64 @@ impl Widget { state.context_cursor = cursor.position().unwrap_or_default(); let state = tree.state.downcast_mut::(); - state.menu_bar_state.inner.with_data_mut(|state| { state.open = true; state.view_cursor = cursor; }); + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + self.create_popup(layout, cursor, renderer, shell, viewport, state); return event::Status::Captured; + } else if right_button_released(&event) + || (touch_lifted(&event)) + || left_button_released(&event) + { + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + state.menu_bar_state.inner.with_data_mut(|state| { + if let Some(id) = state.popup_id.remove(&self.window_id) { + state.menu_states.clear(); + state.active_root.clear(); + state.open = false; + + { + let surface_action = self.on_surface_action.as_ref().unwrap(); + + shell + .publish(surface_action(crate::surface::action::destroy_popup(id))); + } + state.view_cursor = cursor; + } + }); + } + } else if open { + match event { + Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Right | mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerLifted { .. }) => { + #[cfg(all( + feature = "wayland", + feature = "winit", + feature = "surface-message" + ))] + state.menu_bar_state.inner.with_data_mut(|state| { + if let Some(id) = state.popup_id.remove(&self.window_id) { + state.menu_states.clear(); + state.active_root.clear(); + state.open = false; + + { + let surface_action = self.on_surface_action.as_ref().unwrap(); + + shell.publish(surface_action( + crate::surface::action::destroy_popup(id), + )); + } + state.view_cursor = cursor; + } + }); + } + _ => (), } } @@ -219,6 +467,11 @@ impl Widget _renderer: &crate::Renderer, translation: Vector, ) -> Option> { + #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { + return None; + } + let state = tree.state.downcast_ref::(); let context_menu = self.context_menu.as_mut()?; @@ -287,6 +540,13 @@ fn right_button_released(event: &Event) -> bool { ) } +fn left_button_released(event: &Event) -> bool { + matches!( + event, + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left,)) + ) +} + fn touch_lifted(event: &Event) -> bool { matches!(event, Event::Touch(touch::Event::FingerLifted { .. })) } diff --git a/src/widget/menu.rs b/src/widget/menu.rs index 2b54bf6e..9d4ce4b1 100644 --- a/src/widget/menu.rs +++ b/src/widget/menu.rs @@ -74,5 +74,5 @@ pub use menu_tree::{ pub use crate::style::menu_bar::{Appearance, StyleSheet}; pub(crate) use menu_bar::{menu_roots_children, menu_roots_diff}; -pub(crate) use menu_inner::Menu; pub use menu_inner::{CloseCondition, ItemHeight, ItemWidth, PathHighlight}; +pub(crate) use menu_inner::{Direction, Menu, init_root_menu}; diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 2c355bf4..66a4b9b9 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -67,7 +67,7 @@ impl MenuBarStateInner { .map(|ms| ms.index.expect("No indices were found in the menu state.")) } - pub(super) fn reset(&mut self) { + pub(crate) fn reset(&mut self) { self.open = false; self.active_root = Vec::new(); self.menu_states.clear(); diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index c41cded2..595632ad 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -297,7 +297,7 @@ impl MenuBounds { #[derive(Clone)] pub(crate) struct MenuState { /// The index of the active menu item - pub(super) index: Option, + pub(crate) index: Option, scroll_offset: f32, pub menu_bounds: MenuBounds, } @@ -1083,7 +1083,7 @@ fn pad_rectangle(rect: Rectangle, padding: Padding) -> Rectangle { } #[allow(clippy::too_many_arguments)] -pub(super) fn init_root_menu( +pub(crate) fn init_root_menu( menu: &mut Menu<'_, Message>, renderer: &crate::Renderer, shell: &mut Shell<'_, Message>, @@ -1102,7 +1102,6 @@ pub(super) fn init_root_menu( return; } - let mut set = false; for (i, (&root_bounds, mt)) in menu .root_bounds_list .iter() @@ -1117,7 +1116,7 @@ pub(super) fn init_root_menu( let view_center = viewport_size.width * 0.5; let rb_center = root_bounds.center_x(); - state.horizontal_direction = if rb_center > view_center { + state.horizontal_direction = if menu.is_overlay && rb_center > view_center { Direction::Negative } else { Direction::Positive @@ -1146,7 +1145,6 @@ pub(super) fn init_root_menu( &mut state.tree.children[0].children, menu.is_overlay, ); - set = true; state.active_root.push(i); let ms = MenuState { index: None, @@ -1186,7 +1184,6 @@ pub(super) fn init_root_popup_menu( let active_roots = &state.active_root[..=menu.depth]; - let mut set = false; let mt = active_roots .iter() .skip(1) @@ -1230,7 +1227,6 @@ pub(super) fn init_root_popup_menu( } else { Direction::Positive }; - set = true; let ms = MenuState { index: None, From 00ba16fe01e6d22170d69572505eb282ff0bdec3 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 12 Jun 2025 11:40:50 -0400 Subject: [PATCH 011/352] refactor(menu): fallback behavior for non wayland windowing system --- Cargo.toml | 1 + iced | 2 +- src/app/action.rs | 4 + src/app/cosmic.rs | 75 ++++++++++- src/widget/context_menu.rs | 98 ++++++++------- src/widget/menu/menu_bar.rs | 17 ++- src/widget/menu/menu_inner.rs | 231 +++++++++++++++++----------------- 7 files changed, 268 insertions(+), 160 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a90b0e73..132aac8c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,6 +120,7 @@ libc = { version = "0.2.171", optional = true } license = { version = "3.6.0", optional = true } mime = { version = "0.3.17", optional = true } palette = "0.7.6" +raw-window-handle = "0.6" rfd = { version = "0.15.3", default-features = false, features = [ "xdg-portal", ], optional = true } diff --git a/iced b/iced index fc9d49eb..2d1511d0 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit fc9d49eb2f830fe394252ff6799d59ad828243bc +Subproject commit 2d1511d0cf0296e6f0cfcfcd13f2a1aa334c6915 diff --git a/src/app/action.rs b/src/app/action.rs index 44655ffa..0f05a6a6 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -36,6 +36,8 @@ pub enum Action { NavBar(nav_bar::Id), /// Activates a context menu for an item from the nav bar. NavBarContext(nav_bar::Id), + /// A new window was opened. + Opened(iced::window::Id), /// Set scaling factor ScaleFactor(f32), /// Show the window menu @@ -60,6 +62,8 @@ pub enum Action { ToolkitConfig(CosmicTk), /// Window focus lost Unfocus(iced::window::Id), + /// Windowing system initialized + WindowingSystemInitialized, /// Updates the window maximized state WindowMaximized(iced::window::Id, bool), /// Updates the tracked window geometry. diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 055ff947..f2de0f5e 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -19,6 +19,65 @@ use iced::{Task, window}; use iced_futures::event::listen_with; use palette::color_difference::EuclideanDistance; +#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] +#[non_exhaustive] +pub enum WindowingSystem { + UiKit, + AppKit, + Orbital, + OhosNdk, + Xlib, + Xcb, + Wayland, + Drm, + Gbm, + Win32, + WinRt, + Web, + WebCanvas, + WebOffscreenCanvas, + AndroidNdk, + Haiku, +} + +pub(crate) static WINDOWING_SYSTEM: std::sync::OnceLock = + std::sync::OnceLock::new(); + +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(); + let system = match raw { + window::raw_window_handle::RawWindowHandle::UiKit(_) => WindowingSystem::UiKit, + window::raw_window_handle::RawWindowHandle::AppKit(_) => WindowingSystem::AppKit, + window::raw_window_handle::RawWindowHandle::Orbital(_) => WindowingSystem::Orbital, + window::raw_window_handle::RawWindowHandle::OhosNdk(_) => WindowingSystem::OhosNdk, + window::raw_window_handle::RawWindowHandle::Xlib(_) => WindowingSystem::Xlib, + window::raw_window_handle::RawWindowHandle::Xcb(_) => WindowingSystem::Xcb, + window::raw_window_handle::RawWindowHandle::Wayland(_) => WindowingSystem::Wayland, + window::raw_window_handle::RawWindowHandle::Web(_) => WindowingSystem::Web, + window::raw_window_handle::RawWindowHandle::WebCanvas(_) => WindowingSystem::WebCanvas, + window::raw_window_handle::RawWindowHandle::WebOffscreenCanvas(_) => { + WindowingSystem::WebOffscreenCanvas + } + window::raw_window_handle::RawWindowHandle::AndroidNdk(_) => WindowingSystem::AndroidNdk, + window::raw_window_handle::RawWindowHandle::Haiku(_) => WindowingSystem::Haiku, + window::raw_window_handle::RawWindowHandle::Drm(_) => WindowingSystem::Drm, + window::raw_window_handle::RawWindowHandle::Gbm(_) => WindowingSystem::Gbm, + window::raw_window_handle::RawWindowHandle::Win32(_) => WindowingSystem::Win32, + window::raw_window_handle::RawWindowHandle::WinRt(_) => WindowingSystem::WinRt, + _ => { + tracing::warn!("Unknown windowing system: {raw:?}"); + return crate::Action::Cosmic(Action::WindowingSystemInitialized); + } + }; + + _ = WINDOWING_SYSTEM.set(system); + crate::Action::Cosmic(Action::WindowingSystemInitialized) +} + #[derive(Default)] pub struct Cosmic { pub app: App, @@ -41,10 +100,17 @@ where use iced_futures::futures::executor::block_on; core.settings_daemon = block_on(cosmic_config::dbus::settings_daemon_proxy()).ok(); } + let id = core.main_window_id().unwrap_or(window::Id::RESERVED); let (model, command) = T::init(core, flags); - (Self::new(model), command) + ( + Self::new(model), + Task::batch(vec![ + command, + iced_runtime::window::run_with_handle(id, init_windowing_system), + ]), + ) } #[cfg(not(feature = "multi-window"))] @@ -57,6 +123,7 @@ where self.app.title(id).to_string() } + #[allow(clippy::too_many_lines)] pub fn surface_update( &mut self, _surface_message: crate::surface::Action, @@ -255,6 +322,9 @@ where iced::Event::Window(window::Event::Resized(iced::Size { width, height })) => { return Some(Action::WindowResize(id, width, height)); } + iced::Event::Window(window::Event::Opened { .. }) => { + return Some(Action::Opened(id)); + } iced::Event::Window(window::Event::Closed) => { return Some(Action::SurfaceClosed(id)); } @@ -786,6 +856,9 @@ impl Cosmic { let core = self.app.core_mut(); core.applet.suggested_bounds = b; } + Action::Opened(id) => { + return iced_runtime::window::run_with_handle(id, init_windowing_system); + } _ => {} } diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index a00ae751..b1014cf4 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -3,6 +3,8 @@ //! 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"))] +use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::widget::menu::{ self, CloseCondition, Direction, ItemHeight, ItemWidth, MenuBarState, PathHighlight, init_root_menu, menu_roots_diff, @@ -361,21 +363,23 @@ impl Widget feature = "winit", feature = "surface-message" ))] - state.menu_bar_state.inner.with_data_mut(|state| { - if let Some(id) = state.popup_id.remove(&self.window_id) { - state.menu_states.clear(); - state.active_root.clear(); - state.open = false; + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { + state.menu_bar_state.inner.with_data_mut(|state| { + if let Some(id) = state.popup_id.remove(&self.window_id) { + state.menu_states.clear(); + state.active_root.clear(); + state.open = false; - { - let surface_action = self.on_surface_action.as_ref().unwrap(); - shell.publish(surface_action( - crate::surface::action::destroy_popup(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; } - state.view_cursor = cursor; - } - }); + }); + } } _ => (), @@ -392,7 +396,9 @@ impl Widget state.view_cursor = cursor; }); #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] - self.create_popup(layout, cursor, renderer, shell, viewport, state); + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { + self.create_popup(layout, cursor, renderer, shell, viewport, state); + } return event::Status::Captured; } else if right_button_released(&event) @@ -400,33 +406,7 @@ impl Widget || left_button_released(&event) { #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] - state.menu_bar_state.inner.with_data_mut(|state| { - if let Some(id) = state.popup_id.remove(&self.window_id) { - state.menu_states.clear(); - state.active_root.clear(); - state.open = false; - - { - let surface_action = self.on_surface_action.as_ref().unwrap(); - - shell - .publish(surface_action(crate::surface::action::destroy_popup(id))); - } - state.view_cursor = cursor; - } - }); - } - } else if open { - match event { - Event::Mouse(mouse::Event::ButtonReleased( - mouse::Button::Right | mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerLifted { .. }) => { - #[cfg(all( - feature = "wayland", - feature = "winit", - feature = "surface-message" - ))] + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { state.menu_bar_state.inner.with_data_mut(|state| { if let Some(id) = state.popup_id.remove(&self.window_id) { state.menu_states.clear(); @@ -444,6 +424,37 @@ impl Widget } }); } + } + } else if open { + match event { + Event::Mouse(mouse::Event::ButtonReleased( + mouse::Button::Right | mouse::Button::Left, + )) + | Event::Touch(touch::Event::FingerLifted { .. }) => { + #[cfg(all( + feature = "wayland", + feature = "winit", + feature = "surface-message" + ))] + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { + state.menu_bar_state.inner.with_data_mut(|state| { + if let Some(id) = state.popup_id.remove(&self.window_id) { + state.menu_states.clear(); + state.active_root.clear(); + state.open = false; + + { + let surface_action = self.on_surface_action.as_ref().unwrap(); + + shell.publish(surface_action( + crate::surface::action::destroy_popup(id), + )); + } + state.view_cursor = cursor; + } + }); + } + } _ => (), } } @@ -468,7 +479,10 @@ impl Widget translation: Vector, ) -> Option> { #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] - if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) + && self.window_id != window::Id::NONE + && self.on_surface_action.is_some() + { return None; } diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 66a4b9b9..707aebdc 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -9,6 +9,8 @@ use super::{ }, menu_tree::MenuTree, }; +#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] +use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::{ Renderer, style::menu_bar::StyleSheet, @@ -629,13 +631,17 @@ where return event::Status::Ignored; } #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] - self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { + self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); + } } Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) if open && view_cursor.is_over(layout.bounds()) => { #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] - self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { + self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); + } } _ => (), } @@ -710,7 +716,12 @@ where translation: Vector, ) -> Option> { #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] - return None; + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) + && self.on_surface_action.is_some() + && self.window_id != window::Id::NONE + { + return None; + } let state = tree.state.downcast_ref::(); if state.inner.with_data(|state| !state.open) { diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 595632ad..ecf5f8a7 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -4,6 +4,8 @@ use std::{borrow::Cow, sync::Arc}; use super::{menu_bar::MenuBarState, menu_tree::MenuTree}; +#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] +use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::style::menu_bar::StyleSheet; use iced::window; @@ -665,21 +667,24 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { feature = "winit", feature = "surface-message" ))] - 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; + 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); } - root = *parent.0; - depth = depth.saturating_sub(1); + shell.publish((handler)(crate::surface::Action::DestroyPopup( + root, + ))); } - shell - .publish((handler)(crate::surface::Action::DestroyPopup(root))); } state.reset(); @@ -922,72 +927,73 @@ impl Widget Widget Widget Date: Thu, 12 Jun 2025 13:25:29 -0400 Subject: [PATCH 012/352] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 2d1511d0..9312d3c2 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 2d1511d0cf0296e6f0cfcfcd13f2a1aa334c6915 +Subproject commit 9312d3c29b6308d714725a20342375a6145359d9 From 7d7274b8010da57c7e2d81e90614e8df473ff19e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 17 Jun 2025 23:52:28 -0400 Subject: [PATCH 013/352] fix(header-bar): allocate space that accounts for window controls --- src/widget/header_bar.rs | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index fdc9d962..6f7c35fd 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -307,6 +307,9 @@ 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()); @@ -327,8 +330,9 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { } } }; - let portion = ((start.len().max(end.len()) as f32 / center.len().max(1) as f32).round() - as u16) + let portion = ((start.len().max(end.len() + window_control_cnt) as f32 + / center.len().max(1) as f32) + .round() as u16) .max(1); // Creates the headerbar widget. let mut widget = widget::row::with_capacity(3) From 90ed634b0680f546bab7e8d04f0989d718d721fa Mon Sep 17 00:00:00 2001 From: Adrian Geipert Date: Wed, 18 Jun 2025 09:11:22 +0200 Subject: [PATCH 014/352] chore: update syn to v2 --- cosmic-config-derive/Cargo.toml | 4 ++-- cosmic-config-derive/src/lib.rs | 14 +++++++++----- 2 files changed, 11 insertions(+), 7 deletions(-) diff --git a/cosmic-config-derive/Cargo.toml b/cosmic-config-derive/Cargo.toml index 46d79658..55eeb871 100644 --- a/cosmic-config-derive/Cargo.toml +++ b/cosmic-config-derive/Cargo.toml @@ -8,5 +8,5 @@ edition = "2021" proc-macro = true [dependencies] -syn = "1.0" -quote = "1.0" \ No newline at end of file +syn = "2.0" +quote = "1.0" diff --git a/cosmic-config-derive/src/lib.rs b/cosmic-config-derive/src/lib.rs index e1ea70fe..668154cd 100644 --- a/cosmic-config-derive/src/lib.rs +++ b/cosmic-config-derive/src/lib.rs @@ -17,12 +17,16 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let version = attributes .iter() .find_map(|attr| { - if attr.path.is_ident("version") { - match attr.parse_meta() { - Ok(syn::Meta::NameValue(syn::MetaNameValue { - lit: syn::Lit::Int(lit_int), + if attr.path().is_ident("version") { + match attr.meta { + syn::Meta::NameValue(syn::MetaNameValue { + value: + syn::Expr::Lit(syn::ExprLit { + lit: syn::Lit::Int(ref lit_int), + .. + }), .. - })) => Some(lit_int.base10_parse::().unwrap()), + }) => Some(lit_int.base10_parse::().unwrap()), _ => None, } } else { From bf9fc4c29f903e41928d1118a87cdc3636617ef2 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 18 Jun 2025 15:27:04 -0400 Subject: [PATCH 015/352] fix(menu): disable slide_x for repositioned nested popups --- src/widget/menu/menu_inner.rs | 26 ++++++++++++++------------ 1 file changed, 14 insertions(+), 12 deletions(-) diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index ecf5f8a7..6a3eb093 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -1030,18 +1030,20 @@ impl Widget Date: Wed, 18 Jun 2025 15:50:31 -0400 Subject: [PATCH 016/352] improv: use full root menu width when using wayland popups --- src/widget/responsive_menu_bar.rs | 16 +++++++++++++++- 1 file changed, 15 insertions(+), 1 deletion(-) diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index 3d9557d0..3c9151e7 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -24,7 +24,21 @@ pub struct ResponsiveMenuBar { impl Default for ResponsiveMenuBar { fn default() -> ResponsiveMenuBar { ResponsiveMenuBar { - collapsed_item_width: ItemWidth::Static(84), + collapsed_item_width: { + #[cfg(all(feature = "winit", feature = "wayland"))] + if matches!( + crate::app::cosmic::WINDOWING_SYSTEM.get(), + Some(crate::app::cosmic::WindowingSystem::Wayland) + ) { + ItemWidth::Static(150) + } else { + ItemWidth::Static(84) + } + #[cfg(not(all(feature = "winit", feature = "wayland")))] + { + ItemWidth::Static(84) + } + }, item_width: ItemWidth::Uniform(150), item_height: ItemHeight::Uniform(30), spacing: 0., From 6be54038528ab2fc3f7d92b634d329a6424c7428 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 18 Jun 2025 17:06:36 -0400 Subject: [PATCH 017/352] improv(header): remove title if condensed and better handle large fixed size elements --- src/app/mod.rs | 3 ++- src/widget/header_bar.rs | 40 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 38 insertions(+), 5 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index eec9741c..890a3429 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -703,7 +703,8 @@ 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)); + .on_double_click(crate::Action::Cosmic(Action::Maximize)) + .is_condensed(is_condensed); 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 6f7c35fd..3603cc68 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -26,6 +26,7 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { maximized: false, is_ssd: false, on_double_click: None, + is_condensed: false, } } @@ -84,6 +85,9 @@ pub struct HeaderBar<'a, Message> { /// HeaderBar used for server-side decorations is_ssd: bool, + + /// Whether the headerbar should be compact + is_condensed: bool, } impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { @@ -294,6 +298,7 @@ impl Widget } 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 { @@ -330,10 +335,37 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { } } }; - let portion = ((start.len().max(end.len() + window_control_cnt) as f32 + + 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. { + (1, 2) + } else if left_to_right_ratio > 2. { + (2, 1) + } else { + (left_len as u16, (right_len + window_control_cnt) as u16) + } + } else { + (portion, portion) + }; // Creates the headerbar widget. let mut widget = widget::row::with_capacity(3) // If elements exist in the start region, append them here. @@ -343,7 +375,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .align_y(iced::Alignment::Center) .apply(widget::container) .align_x(iced::Alignment::Start) - .width(Length::FillPortion(portion)), + .width(Length::FillPortion(left_portion)), ) // If elements exist in the center region, use them here. // This will otherwise use the title as a widget if a title was defined. @@ -356,7 +388,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .center_x(Length::Fill) .into(), ) - } else if !self.title.is_empty() { + } else if !self.title.is_empty() && !self.is_condensed { Some(self.title_widget()) } else { None @@ -367,7 +399,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .align_y(iced::Alignment::Center) .apply(widget::container) .align_x(iced::Alignment::End) - .width(Length::FillPortion(portion)), + .width(Length::FillPortion(right_portion)), ) .align_y(iced::Alignment::Center) .height(Length::Fixed(32.0 + padding[0] as f32 + padding[2] as f32)) From 5be9611c8a80825b478023743dde16912a21948b Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 19 Jun 2025 09:36:07 -0400 Subject: [PATCH 018/352] fix(segmented-button): context menu state management --- src/widget/segmented_button/widget.rs | 30 +++++++-------------------- 1 file changed, 8 insertions(+), 22 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 07e6cbc1..1c92d0b2 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -647,16 +647,9 @@ where // Diff the context menu if let Some(context_menu) = &mut self.context_menu { - if tree.children.is_empty() { - let mut child_tree = Tree::empty(); - child_tree.state = tree::State::new(MenuBarState::default()); - tree.children.push(child_tree); - } else { - tree.children.truncate(1); - } - menu_roots_diff(context_menu, &mut tree.children[0]); - } else { - tree.children.clear(); + state.menu_state.inner.with_data_mut(|inner| { + menu_roots_diff(context_menu, &mut inner.tree); + }); } } @@ -954,9 +947,7 @@ where state.context_cursor = cursor_position.position().unwrap_or_default(); - let menu_state = - tree.children[0].state.downcast_mut::(); - menu_state.inner.with_data_mut(|data| { + state.menu_state.inner.with_data_mut(|data| { data.open = true; data.view_cursor = cursor_position; }); @@ -1593,22 +1584,17 @@ where return None; }; - if !tree.children[0] - .state - .downcast_ref::() - .inner - .with_data(|data| data.open) - { + if !menu_state.inner.with_data(|data| data.open) { + // If the menu is not open, we don't need to show it. return None; } - bounds.x = state.context_cursor.x; bounds.y = state.context_cursor.y; Some( crate::widget::menu::Menu { tree: menu_state, - menu_roots: std::borrow::Cow::Borrowed(context_menu), + menu_roots: std::borrow::Cow::Owned(context_menu.clone()), bounds_expand: 16, menu_overlays_parent: true, close_condition: CloseCondition { @@ -1619,7 +1605,7 @@ where item_width: ItemWidth::Uniform(240), item_height: ItemHeight::Dynamic(40), bar_bounds: bounds, - main_offset: -(bounds.height as i32), + main_offset: -bounds.height as i32, cross_offset: 0, root_bounds_list: vec![bounds], path_highlight: Some(PathHighlight::MenuActive), From 90ad3e9e1b763aae2790ffc4503dfa28973b86bb Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 23 Jun 2025 17:13:58 +0200 Subject: [PATCH 019/352] improv(cosmic-config): use notifier debouncer on inotify watchers --- cosmic-config/Cargo.toml | 1 + cosmic-config/src/lib.rs | 20 +++++++++++--------- cosmic-config/src/subscription.rs | 3 ++- 3 files changed, 14 insertions(+), 10 deletions(-) diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index a79237c8..424ea262 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -26,6 +26,7 @@ dirs.workspace = true tokio = { version = "1.44", optional = true, features = ["time"] } async-std = { version = "1.13", optional = true } tracing = "0.1" +notify-debouncer-full = "0.5.0" [target.'cfg(unix)'.dependencies] xdg = "2.5" diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index beab95fb..1f0c8846 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -1,15 +1,15 @@ //! Integrations for cosmic-config — the cosmic configuration system. use notify::{ - event::{EventKind, ModifyKind}, - Watcher, + event::{EventKind, ModifyKind}, RecommendedWatcher, Watcher }; +use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache}; use serde::{de::DeserializeOwned, Serialize}; use std::{ fmt, fs, io::Write, path::{Path, PathBuf}, - sync::Mutex, + sync::Mutex, time::Duration, }; #[cfg(feature = "subscription")] @@ -244,7 +244,7 @@ impl Config { // This may end up being an mpsc channel instead of a function // See EventHandler in the notify crate: https://docs.rs/notify/latest/notify/trait.EventHandler.html // Having a callback allows for any application abstraction to be used - pub fn watch(&self, f: F) -> Result + pub fn watch(&self, f: F) -> Result, Error> // Argument is an array of all keys that changed in that specific transaction //TODO: simplify F requirements where @@ -256,10 +256,11 @@ impl Config { }; let user_path_clone = user_path.clone(); let mut watcher = - notify::recommended_watcher(move |event_res: Result| { - match &event_res { - Ok(event) => { - match &event.kind { + notify_debouncer_full::new_debouncer(Duration::from_secs(1), None, move |event_res: Result, Vec>| { + match event_res { + Ok(events) => { + for event in events { + match &event.event.kind { EventKind::Access(_) | EventKind::Modify(ModifyKind::Metadata(_)) => { // Data not mutated return; @@ -287,8 +288,9 @@ impl Config { if !keys.is_empty() { f(&watch_config, &keys); } + } } - Err(_err) => { + Err(_errs) => { //TODO: handle errors } } diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs index 64255954..591b85ea 100644 --- a/cosmic-config/src/subscription.rs +++ b/cosmic-config/src/subscription.rs @@ -1,13 +1,14 @@ use iced_futures::futures::{SinkExt, Stream}; use iced_futures::{futures::channel::mpsc, stream}; use notify::RecommendedWatcher; +use notify_debouncer_full::{Debouncer, RecommendedCache}; use std::{borrow::Cow, hash::Hash}; use crate::{Config, CosmicConfigEntry}; pub enum ConfigState { Init(Cow<'static, str>, u64, bool), - Waiting(T, RecommendedWatcher, mpsc::Receiver>, Config), + Waiting(T, Debouncer, mpsc::Receiver>, Config), Failed, } From 1af2f4ffe57b402000c5982e3008395191e5cadd Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 23 Jun 2025 17:50:28 +0200 Subject: [PATCH 020/352] chore: format --- cosmic-config/src/lib.rs | 58 +++++++++++++++++-------------- cosmic-config/src/subscription.rs | 7 +++- src/theme/mod.rs | 52 +++++++++++++-------------- 3 files changed, 64 insertions(+), 53 deletions(-) diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 1f0c8846..fecfdb75 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -1,7 +1,8 @@ //! Integrations for cosmic-config — the cosmic configuration system. use notify::{ - event::{EventKind, ModifyKind}, RecommendedWatcher, Watcher + event::{EventKind, ModifyKind}, + RecommendedWatcher, Watcher, }; use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache}; use serde::{de::DeserializeOwned, Serialize}; @@ -9,7 +10,8 @@ use std::{ fmt, fs, io::Write, path::{Path, PathBuf}, - sync::Mutex, time::Duration, + sync::Mutex, + time::Duration, }; #[cfg(feature = "subscription")] @@ -255,46 +257,50 @@ impl Config { return Err(Error::NoConfigDirectory); }; let user_path_clone = user_path.clone(); - let mut watcher = - notify_debouncer_full::new_debouncer(Duration::from_secs(1), None, move |event_res: Result, Vec>| { + let mut watcher = notify_debouncer_full::new_debouncer( + Duration::from_secs(1), + None, + move |event_res: Result, Vec>| { match event_res { Ok(events) => { for event in events { match &event.event.kind { - EventKind::Access(_) | EventKind::Modify(ModifyKind::Metadata(_)) => { - // Data not mutated - return; + EventKind::Access(_) + | EventKind::Modify(ModifyKind::Metadata(_)) => { + // Data not mutated + return; + } + _ => {} } - _ => {} - } - let mut keys = Vec::new(); - for path in &event.paths { - match path.strip_prefix(&user_path_clone) { - Ok(key_path) => { - if let Some(key) = key_path.to_str() { - // Skip any .atomicwrite temporary files - if key.starts_with(".atomicwrite") { - continue; + let mut keys = Vec::new(); + for path in &event.paths { + match path.strip_prefix(&user_path_clone) { + Ok(key_path) => { + if let Some(key) = key_path.to_str() { + // Skip any .atomicwrite temporary files + if key.starts_with(".atomicwrite") { + continue; + } + keys.push(key.to_string()); } - keys.push(key.to_string()); + } + Err(_err) => { + //TODO: handle errors } } - Err(_err) => { - //TODO: handle errors - } } - } - if !keys.is_empty() { - f(&watch_config, &keys); - } + if !keys.is_empty() { + f(&watch_config, &keys); + } } } Err(_errs) => { //TODO: handle errors } } - })?; + }, + )?; watcher.watch(user_path, notify::RecursiveMode::NonRecursive)?; Ok(watcher) } diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs index 591b85ea..2d2c5117 100644 --- a/cosmic-config/src/subscription.rs +++ b/cosmic-config/src/subscription.rs @@ -8,7 +8,12 @@ use crate::{Config, CosmicConfigEntry}; pub enum ConfigState { Init(Cow<'static, str>, u64, bool), - Waiting(T, Debouncer, mpsc::Receiver>, Config), + Waiting( + T, + Debouncer, + mpsc::Receiver>, + Config, + ), Failed, } diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 7fbb5bad..5e335f59 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -85,33 +85,33 @@ pub fn is_high_contrast() -> bool { active_type().is_high_contrast() } -/// Watches for changes to the system's theme preference. -#[cold] -pub fn subscription(is_dark: bool) -> Subscription { - config_subscription::<_, crate::cosmic_theme::Theme>( - ( - std::any::TypeId::of::(), - is_dark, - ), - if is_dark { - cosmic_theme::DARK_THEME_ID - } else { - cosmic_theme::LIGHT_THEME_ID - } - .into(), - crate::cosmic_theme::Theme::VERSION, - ) - .map(|res| { - for error in res.errors.into_iter().filter(cosmic_config::Error::is_err) { - tracing::error!( - ?error, - "error while watching system theme preference changes" - ); - } +// /// Watches for changes to the system's theme preference. +// #[cold] +// pub fn subscription(is_dark: bool) -> Subscription { +// config_subscription::<_, crate::cosmic_theme::Theme>( +// ( +// std::any::TypeId::of::(), +// is_dark, +// ), +// if is_dark { +// cosmic_theme::DARK_THEME_ID +// } else { +// cosmic_theme::LIGHT_THEME_ID +// } +// .into(), +// crate::cosmic_theme::Theme::VERSION, +// ) +// .map(|res| { +// for error in res.errors.into_iter().filter(cosmic_config::Error::is_err) { +// tracing::error!( +// ?error, +// "error while watching system theme preference changes" +// ); +// } - Theme::system(Arc::new(res.config)) - }) -} +// Theme::system(Arc::new(res.config)) +// }) +// } pub fn system_dark() -> Theme { let Ok(helper) = crate::cosmic_theme::Theme::dark_config() else { From a85b3693994ef2b8275eca6a9eccc86a2d7e9f86 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Mon, 23 Jun 2025 11:20:48 -0600 Subject: [PATCH 021/352] Fix config watching --- cosmic-config/Cargo.toml | 1 - cosmic-config/src/lib.rs | 61 ++++++++++++++----------------- cosmic-config/src/subscription.rs | 3 +- 3 files changed, 29 insertions(+), 36 deletions(-) diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 424ea262..a79237c8 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -26,7 +26,6 @@ dirs.workspace = true tokio = { version = "1.44", optional = true, features = ["time"] } async-std = { version = "1.13", optional = true } tracing = "0.1" -notify-debouncer-full = "0.5.0" [target.'cfg(unix)'.dependencies] xdg = "2.5" diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index fecfdb75..0f846562 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -4,7 +4,6 @@ use notify::{ event::{EventKind, ModifyKind}, RecommendedWatcher, Watcher, }; -use notify_debouncer_full::{DebouncedEvent, Debouncer, RecommendedCache}; use serde::{de::DeserializeOwned, Serialize}; use std::{ fmt, fs, @@ -246,7 +245,7 @@ impl Config { // This may end up being an mpsc channel instead of a function // See EventHandler in the notify crate: https://docs.rs/notify/latest/notify/trait.EventHandler.html // Having a callback allows for any application abstraction to be used - pub fn watch(&self, f: F) -> Result, Error> + pub fn watch(&self, f: F) -> Result // Argument is an array of all keys that changed in that specific transaction //TODO: simplify F requirements where @@ -257,51 +256,47 @@ impl Config { return Err(Error::NoConfigDirectory); }; let user_path_clone = user_path.clone(); - let mut watcher = notify_debouncer_full::new_debouncer( - Duration::from_secs(1), - None, - move |event_res: Result, Vec>| { + let mut watcher = notify::recommended_watcher( + move |event_res: Result| { match event_res { - Ok(events) => { - for event in events { - match &event.event.kind { - EventKind::Access(_) - | EventKind::Modify(ModifyKind::Metadata(_)) => { - // Data not mutated - return; - } - _ => {} + Ok(event) => { + match &event.kind { + EventKind::Access(_) + | EventKind::Modify(ModifyKind::Metadata(_)) => { + // Data not mutated + return; } + _ => {} + } - let mut keys = Vec::new(); - for path in &event.paths { - match path.strip_prefix(&user_path_clone) { - Ok(key_path) => { - if let Some(key) = key_path.to_str() { - // Skip any .atomicwrite temporary files - if key.starts_with(".atomicwrite") { - continue; - } - keys.push(key.to_string()); + let mut keys = Vec::new(); + for path in &event.paths { + match path.strip_prefix(&user_path_clone) { + Ok(key_path) => { + if let Some(key) = key_path.to_str() { + // Skip any .atomicwrite temporary files + if key.starts_with(".atomicwrite") { + continue; } - } - Err(_err) => { - //TODO: handle errors + keys.push(key.to_string()); } } - } - if !keys.is_empty() { - f(&watch_config, &keys); + Err(_err) => { + //TODO: handle errors + } } } + if !keys.is_empty() { + f(&watch_config, &keys); + } } - Err(_errs) => { + Err(_err) => { //TODO: handle errors } } }, )?; - watcher.watch(user_path, notify::RecursiveMode::NonRecursive)?; + watcher.watch(user_path, notify::RecursiveMode::Recursive)?; Ok(watcher) } diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs index 2d2c5117..3e54c1c3 100644 --- a/cosmic-config/src/subscription.rs +++ b/cosmic-config/src/subscription.rs @@ -1,7 +1,6 @@ use iced_futures::futures::{SinkExt, Stream}; use iced_futures::{futures::channel::mpsc, stream}; use notify::RecommendedWatcher; -use notify_debouncer_full::{Debouncer, RecommendedCache}; use std::{borrow::Cow, hash::Hash}; use crate::{Config, CosmicConfigEntry}; @@ -10,7 +9,7 @@ pub enum ConfigState { Init(Cow<'static, str>, u64, bool), Waiting( T, - Debouncer, + RecommendedWatcher, mpsc::Receiver>, Config, ), From 7fb514bfac0ebf6ae6a1f045e8bdb976544d69b5 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 25 Jun 2025 17:51:08 -0400 Subject: [PATCH 022/352] fix(menu): only draw within intersection of viewport bounds --- src/widget/menu/menu_inner.rs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 6a3eb093..18b4433f 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -781,7 +781,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { // println!("color: {:?}\n", styling.background); let menu_quad = renderer::Quad { bounds: pad_rectangle( - children_bounds, + children_bounds.intersection(&viewport).unwrap_or_default(), styling.background_expand.into(), ), border: Border { @@ -800,7 +800,10 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { .nth(active.saturating_sub(start_index)) { let path_quad = renderer::Quad { - bounds: active_layout.bounds(), + bounds: active_layout + .bounds() + .intersection(&viewport) + .unwrap_or_default(), border: Border { radius: styling.menu_border_radius.into(), ..Default::default() @@ -824,7 +827,10 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { style, clo, view_cursor, - &children_layout.bounds(), + &children_layout + .bounds() + .intersection(&viewport) + .unwrap_or_default(), ); }); } From 7555d9dfd122c4865d09e4aad03e5c4efbb50c64 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 25 Jun 2025 17:52:03 -0400 Subject: [PATCH 023/352] cargo fmt --- cosmic-config/src/lib.rs | 10 ++++------ cosmic-config/src/subscription.rs | 7 +------ 2 files changed, 5 insertions(+), 12 deletions(-) diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 0f846562..7a392fde 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -256,13 +256,12 @@ impl Config { return Err(Error::NoConfigDirectory); }; let user_path_clone = user_path.clone(); - let mut watcher = notify::recommended_watcher( - move |event_res: Result| { + let mut watcher = + notify::recommended_watcher(move |event_res: Result| { match event_res { Ok(event) => { match &event.kind { - EventKind::Access(_) - | EventKind::Modify(ModifyKind::Metadata(_)) => { + EventKind::Access(_) | EventKind::Modify(ModifyKind::Metadata(_)) => { // Data not mutated return; } @@ -294,8 +293,7 @@ impl Config { //TODO: handle errors } } - }, - )?; + })?; watcher.watch(user_path, notify::RecursiveMode::Recursive)?; Ok(watcher) } diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs index 3e54c1c3..64255954 100644 --- a/cosmic-config/src/subscription.rs +++ b/cosmic-config/src/subscription.rs @@ -7,12 +7,7 @@ use crate::{Config, CosmicConfigEntry}; pub enum ConfigState { Init(Cow<'static, str>, u64, bool), - Waiting( - T, - RecommendedWatcher, - mpsc::Receiver>, - Config, - ), + Waiting(T, RecommendedWatcher, mpsc::Receiver>, Config), Failed, } From 46cbce033b3863b4633032f7bc90f92116ad4c31 Mon Sep 17 00:00:00 2001 From: Joshua Megnauth <48846352+joshuamegnauth54@users.noreply.github.com> Date: Thu, 26 Jun 2025 00:36:52 -0400 Subject: [PATCH 024/352] fix(header_bar): Windows build fix --- src/desktop.rs | 1 + src/widget/header_bar.rs | 12 ++++-------- 2 files changed, 5 insertions(+), 8 deletions(-) diff --git a/src/desktop.rs b/src/desktop.rs index 34673f91..d41f29a2 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -10,6 +10,7 @@ pub trait IconSourceExt { fn as_cosmic_icon(&self) -> crate::widget::icon::Icon; } +#[cfg(not(windows))] impl IconSourceExt for fde::IconSource { fn as_cosmic_icon(&self) -> crate::widget::icon::Icon { match self { diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 3603cc68..53c14e0b 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -454,14 +454,10 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { #[cfg(not(target_os = "linux"))] let icon = { - widget::icon::from_svg_bytes(include_bytes!(concat!( - "../../res/icons/", - $name, - ".svg" - ))) - .symbolic(true) - .apply(widget::button::icon) - .padding(8) + widget::icon::from_path(concat!("../../res/icons/", $name, ".svg").into()) + .symbolic(true) + .apply(widget::button::icon) + .padding(8) }; icon.class(crate::theme::Button::HeaderBar) From dfdca0ef81985d260d584f1e4f7a6c1473678356 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 27 Jun 2025 09:53:20 -0600 Subject: [PATCH 025/352] fix(menu): make shortcut text 75 percent opacity --- src/widget/menu/menu_tree.rs | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index d02b2b27..8cf890f2 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -6,6 +6,7 @@ use std::borrow::Cow; use std::collections::HashMap; use std::rc::Rc; +use iced::advanced::widget::text::Style as TextStyle; use iced_widget::core::{Element, renderer}; use crate::iced_core::{Alignment, Length}; @@ -228,6 +229,15 @@ pub fn menu_items< String::new() } + fn key_style(theme: &crate::Theme) -> TextStyle { + let mut color = theme.cosmic().background.component.on; + color.alpha *= 0.75; + TextStyle { + color: Some(color.into()), + } + } + let key_class = theme::Text::Custom(key_style); + let size = children.len(); children @@ -244,7 +254,7 @@ pub fn menu_items< let mut items = vec![ widget::text(l.clone()).into(), widget::horizontal_space().into(), - widget::text(key).into(), + widget::text(key).class(key_class).into(), ]; if let Some(icon) = icon { @@ -265,7 +275,7 @@ pub fn menu_items< let mut items = vec![ widget::text(l.clone()).into(), widget::horizontal_space().into(), - widget::text(key).into(), + widget::text(key).class(key_class).into(), ]; if let Some(icon) = icon { @@ -297,7 +307,7 @@ pub fn menu_items< widget::Space::with_width(spacing.space_xxs).into(), widget::text(label).align_x(iced::Alignment::Start).into(), widget::horizontal_space().into(), - widget::text(key).into(), + widget::text(key).class(key_class).into(), ]; if let Some(icon) = icon { From 52b802a11a0fafcb2dd84f3e06840d943aa8d933 Mon Sep 17 00:00:00 2001 From: 8roken <211849604+8roken@users.noreply.github.com> Date: Fri, 27 Jun 2025 14:44:20 -0400 Subject: [PATCH 026/352] fix(cosmic-config): Avoid dual notifications in transaction commits When a transaction gets committed, the files gets written to a file in the .atomicwrite[0-9a-Z] folder and then gets moved to their final location. The watcher will emit two events: - Modify(Name(To)) - Modify(Name(Both) The last one will include both the source and the destination and is essentially a duplicate of the first event. By discarding this event, behavior seems to be the same, and all consumers of those events get only notified once instead of twice when a configuration changes. --- cosmic-config/src/lib.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 7a392fde..1a3ec66a 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -1,7 +1,7 @@ //! Integrations for cosmic-config — the cosmic configuration system. use notify::{ - event::{EventKind, ModifyKind}, + event::{EventKind, ModifyKind, RenameMode}, RecommendedWatcher, Watcher, }; use serde::{de::DeserializeOwned, Serialize}; @@ -261,7 +261,9 @@ impl Config { match event_res { Ok(event) => { match &event.kind { - EventKind::Access(_) | EventKind::Modify(ModifyKind::Metadata(_)) => { + EventKind::Access(_) + | EventKind::Modify(ModifyKind::Metadata(_)) + | EventKind::Modify(ModifyKind::Name(RenameMode::Both)) => { // Data not mutated return; } From aaa4b83577a70c15af8b91d1fb161e2a2931596b Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 1 Jul 2025 09:30:27 -0600 Subject: [PATCH 027/352] Fix bundling of header bar icons --- src/widget/header_bar.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 53c14e0b..3603cc68 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -454,10 +454,14 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { #[cfg(not(target_os = "linux"))] let icon = { - widget::icon::from_path(concat!("../../res/icons/", $name, ".svg").into()) - .symbolic(true) - .apply(widget::button::icon) - .padding(8) + widget::icon::from_svg_bytes(include_bytes!(concat!( + "../../res/icons/", + $name, + ".svg" + ))) + .symbolic(true) + .apply(widget::button::icon) + .padding(8) }; icon.class(crate::theme::Button::HeaderBar) From 50367b96e3bb9a2a2a62a6c51cd71fc2858e61ed Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 8 Jul 2025 17:00:07 -0400 Subject: [PATCH 028/352] fix(headerbar): handle zero length segments --- src/app/cosmic.rs | 4 ++-- src/widget/header_bar.rs | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index f2de0f5e..3a75871d 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -452,11 +452,11 @@ where if let Some(v) = self.surface_views.get(&id) { return v(&self.app); } - if !self + if self .app .core() .main_window_id() - .is_some_and(|main_id| main_id == id) + .is_none_or(|main_id| main_id != id) { return self.app.view_window(id).map(crate::Action::App); } diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 3603cc68..8eec9ef4 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -356,9 +356,9 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { 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. { + if right_to_left_ratio > 2. || left_len < 1 { (1, 2) - } else if left_to_right_ratio > 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) From 0943f131c2590b097fe800c403274ccfed63607c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 16 Jul 2025 13:48:08 -0400 Subject: [PATCH 029/352] refactor: track focus chain --- cosmic-config/src/lib.rs | 1 - src/app/cosmic.rs | 66 +++++++++++++++++++++++++++++++++++----- src/app/mod.rs | 5 +-- src/core.rs | 15 ++++++--- 4 files changed, 73 insertions(+), 14 deletions(-) diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 1a3ec66a..e408eac5 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -10,7 +10,6 @@ use std::{ io::Write, path::{Path, PathBuf}, sync::Mutex, - time::Duration, }; #[cfg(feature = "subscription")] diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 3a75871d..a2cdeb87 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -17,6 +17,8 @@ use iced::Application as IcedApplication; use iced::event::wayland; use iced::{Task, window}; use iced_futures::event::listen_with; +#[cfg(feature = "wayland")] +use iced_winit::SurfaceIdWrapper; use palette::color_difference::EuclideanDistance; #[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)] @@ -84,7 +86,11 @@ pub struct Cosmic { #[cfg(feature = "wayland")] pub surface_views: HashMap< window::Id, - Box Fn(&'a App) -> Element<'a, crate::Action>>, + ( + Option, + SurfaceIdWrapper, + Box Fn(&'a App) -> Element<'a, crate::Action>>, + ), >, } @@ -449,7 +455,7 @@ where #[cfg(feature = "multi-window")] pub fn view(&self, id: window::Id) -> Element> { #[cfg(feature = "wayland")] - if let Some(v) = self.surface_views.get(&id) { + if let Some((_, _, v)) = self.surface_views.get(&id) { return v(&self.app); } if self @@ -841,13 +847,45 @@ impl Cosmic { } Action::Focus(f) => { - self.app.core_mut().focused_window = Some(f); + #[cfg(all( + feature = "wayland", + feature = "multi-window", + feature = "surface-message" + ))] + if let Some(( + parent, + SurfaceIdWrapper::Subsurface(_) | SurfaceIdWrapper::Popup(_), + _, + )) = self.surface_views.get(&f) + { + // If the parent is already focused, push the new focus + // to the end of the focus chain. + if parent.is_some_and(|p| self.app.core().focused_window.last() == Some(&p)) { + self.app.core_mut().focused_window.push(f); + return iced::Task::none(); + } else { + // set the whole parent chain to the focus chain + let mut parent_chain = vec![f]; + let mut cur = *parent; + while let Some(p) = cur { + parent_chain.push(p); + cur = self + .surface_views + .get(&p) + .and_then(|(parent, _, _)| *parent); + } + parent_chain.reverse(); + self.app.core_mut().focused_window = parent_chain; + return iced::Task::none(); + } + } + self.app.core_mut().focused_window = vec![f]; } Action::Unfocus(id) => { let core = self.app.core_mut(); - if core.focused_window.as_ref().is_some_and(|cur| *cur == id) { - core.focused_window = None; + if core.focused_window().as_ref().is_some_and(|cur| *cur == id) { + core.focused_window.pop(); } } #[cfg(feature = "applet")] @@ -886,7 +924,14 @@ impl Cosmic { ) -> Task> { use iced_winit::commands::subsurface::get_subsurface; - self.surface_views.insert(settings.id, view); + self.surface_views.insert( + settings.id, + ( + Some(settings.parent), + SurfaceIdWrapper::Subsurface(settings.id), + view, + ), + ); get_subsurface(settings) } @@ -901,7 +946,14 @@ impl Cosmic { ) -> Task> { use iced_winit::commands::popup::get_popup; - self.surface_views.insert(settings.id, view); + self.surface_views.insert( + settings.id, + ( + Some(settings.parent), + SurfaceIdWrapper::Popup(settings.id), + view, + ), + ); get_popup(settings) } } diff --git a/src/app/mod.rs b/src/app/mod.rs index 890a3429..35b55f02 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -548,8 +548,9 @@ impl ApplicationExt for App { let show_context = core.window.show_context; let nav_bar_active = core.nav_bar_active(); let focused = core - .focused_window() - .is_some_and(|i| Some(i) == self.core().main_window_id()); + .focus_chain() + .iter() + .any(|i| Some(*i) == self.core().main_window_id()); let border_padding = if sharp_corners { 8 } else { 7 }; diff --git a/src/core.rs b/src/core.rs index 744b33b7..4b9811ec 100644 --- a/src/core.rs +++ b/src/core.rs @@ -64,7 +64,7 @@ pub struct Core { scale_factor: f32, /// Window focus state - pub(super) focused_window: Option, + pub(super) focused_window: Vec, pub(super) theme_sub_counter: u64, /// Last known system theme @@ -141,7 +141,7 @@ impl Default for Core { height: 0., width: 0., }, - focused_window: None, + focused_window: Vec::new(), #[cfg(feature = "applet")] applet: crate::applet::Context::default(), #[cfg(feature = "single-instance")] @@ -384,8 +384,15 @@ impl Core { /// Get the current focused window if it exists #[must_use] #[inline] - pub const fn focused_window(&self) -> Option { - self.focused_window + pub fn focused_window(&self) -> Option { + self.focused_window.last().copied() + } + + /// Get the current focus chain of windows + #[must_use] + #[inline] + pub fn focus_chain(&self) -> &[window::Id] { + &self.focused_window } /// Whether the application should use a dark theme, according to the system From 364af2bcdfd7599813ec8510a33d171955d7468c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 16 Jul 2025 20:20:08 -0400 Subject: [PATCH 030/352] refactor: introduce new palette colors for control tint neutral colors will not be tinted anymore --- cosmic-theme/src/model/cosmic_palette.rs | 23 +++++ cosmic-theme/src/model/dark.ron | 2 +- cosmic-theme/src/model/light.ron | 2 +- cosmic-theme/src/model/theme.rs | 106 +++++++++++------------ 4 files changed, 78 insertions(+), 55 deletions(-) diff --git a/cosmic-theme/src/model/cosmic_palette.rs b/cosmic-theme/src/model/cosmic_palette.rs index 6a189089..2c4ddfbd 100644 --- a/cosmic-theme/src/model/cosmic_palette.rs +++ b/cosmic-theme/src/model/cosmic_palette.rs @@ -132,6 +132,29 @@ pub struct CosmicPaletteInner { /// A wider spread of dark colors for more general use. pub neutral_10: Srgba, + /// Tinted control colors. + pub control_0: Srgba, + /// Tinted control colors. + pub control_1: Srgba, + /// Tinted control colors. + pub control_2: Srgba, + /// Tinted control colors. + pub control_3: Srgba, + /// Tinted control colors. + pub control_4: Srgba, + /// Tinted control colors. + pub control_5: Srgba, + /// Tinted control colors. + pub control_6: Srgba, + /// Tinted control colors. + pub control_7: Srgba, + /// Tinted control colors. + pub control_8: Srgba, + /// Tinted control colors. + pub control_9: Srgba, + /// Tinted control colors. + pub control_10: Srgba, + /// Potential Accent Color Combos pub accent_blue: Srgba, /// Potential Accent Color Combos diff --git a/cosmic-theme/src/model/dark.ron b/cosmic-theme/src/model/dark.ron index 4453b8bf..f4531efd 100644 --- a/cosmic-theme/src/model/dark.ron +++ b/cosmic-theme/src/model/dark.ron @@ -1 +1 @@ -Dark((name:"cosmic-dark",bright_red:(red:1.0,green:0.62745098,blue:0.60392157,alpha:1.0),bright_green:(red:0.36862745,green:0.85882352,blue:0.54901960,alpha:1.0),bright_orange:(red:1.0,green:0.63921569,blue:0.49019608,alpha:1.0),gray_1:(red:0.10588235,green:0.10588235,blue:0.10588235,alpha:1.0),gray_2:(red:0.14901961,green:0.14901961,blue:0.14901961,alpha:1.0),neutral_0:(red:0.0,green:0.0,blue:0.0,alpha:1.0),neutral_1:(red:0.01176471,green:0.01176471,blue:0.01176471,alpha:1.0),neutral_2:(red:0.08627451,green:0.08627451,blue:0.08627451,alpha:1.0),neutral_3:(red:0.18039216,green:0.18039216,blue:0.18039216,alpha:1.0),neutral_4:(red:0.28235294,green:0.28235294,blue:0.28235294,alpha:1.0),neutral_5:(red:0.38823529,green:0.38823529,blue:0.38823529,alpha:1.0),neutral_6:(red:0.50196078,green:0.50196078,blue:0.50196078,alpha:1.0),neutral_7:(red:0.61960784,green:0.61960784,blue:0.61960784,alpha:1.0),neutral_8:(red:0.74509804,green:0.74509804,blue:0.74509804,alpha:1.0),neutral_9:(red:0.87058824,green:0.87058824,blue:0.87058824,alpha:1.0),neutral_10:(red:1.0,green:1.0,blue:1.0,alpha:1.0),accent_blue:(red:0.3882353,green:0.81568627,blue:0.87450981,alpha:1.0),accent_indigo:(red:0.63137255,green:0.75294118,blue:0.92156863,alpha:1.0),accent_purple:(red:0.90588235,green:0.61176471,blue:0.99607843,alpha:1.0),accent_pink:(red:1.0,green:0.61176471,blue:0.69411765,alpha:1.0),accent_red:(red:0.99215686,green:0.63137255,blue:0.62745098,alpha:1.0),accent_orange:(red:1.0,green:0.67843137,blue:0.0,alpha:1.0),accent_yellow:(red:0.96862745,green:0.87843137,blue:0.38431373,alpha:1.0),accent_green:(red:0.57254902,green:0.81176471,blue:0.61176471,alpha:1.0),accent_warm_grey:(red:0.79215686,green:0.72941176,blue:0.70588235,alpha:1.0),ext_warm_grey:(red:0.60784314,green:0.55686275,blue:0.54117647,alpha:1.0),ext_orange:(red:1.0,green:0.67843137,blue:0.0,alpha:1.0),ext_yellow:(red:0.99607843,green:0.85882353,blue:0.25098039,alpha:1.0),ext_blue:(red:0.28235294,green:0.72549020,blue:0.78039216,alpha:1.0),ext_purple:(red:0.81176471,green:0.49019608,blue:1.0,alpha:1.0),ext_pink:(red:0.97647059,green:0.22745098,blue:0.51372549,alpha:1.0),ext_indigo:(red:0.24313725,green:0.53333333,blue:1.0,alpha:1.0))) +Dark((name:"cosmic-dark",bright_red:(red:1.0,green:0.62745098,blue:0.60392157,alpha:1.0),bright_green:(red:0.36862745,green:0.85882352,blue:0.54901960,alpha:1.0),bright_orange:(red:1.0,green:0.63921569,blue:0.49019608,alpha:1.0),gray_1:(red:0.10588235,green:0.10588235,blue:0.10588235,alpha:1.0),gray_2:(red:0.14901961,green:0.14901961,blue:0.14901961,alpha:1.0),control_0:(red:0.0,green:0.0,blue:0.0,alpha:1.0),control_1:(red:0.01176471,green:0.01176471,blue:0.01176471,alpha:1.0),control_2:(red:0.08627451,green:0.08627451,blue:0.08627451,alpha:1.0),control_3:(red:0.18039216,green:0.18039216,blue:0.18039216,alpha:1.0),control_4:(red:0.28235294,green:0.28235294,blue:0.28235294,alpha:1.0),control_5:(red:0.38823529,green:0.38823529,blue:0.38823529,alpha:1.0),control_6:(red:0.50196078,green:0.50196078,blue:0.50196078,alpha:1.0),control_7:(red:0.61960784,green:0.61960784,blue:0.61960784,alpha:1.0),control_8:(red:0.74509804,green:0.74509804,blue:0.74509804,alpha:1.0),control_9:(red:0.87058824,green:0.87058824,blue:0.87058824,alpha:1.0),control_10:(red:1.0,green:1.0,blue:1.0,alpha:1.0),neutral_0:(red:0.0,green:0.0,blue:0.0,alpha:1.0),neutral_1:(red:0.01176471,green:0.01176471,blue:0.01176471,alpha:1.0),neutral_2:(red:0.08627451,green:0.08627451,blue:0.08627451,alpha:1.0),neutral_3:(red:0.18039216,green:0.18039216,blue:0.18039216,alpha:1.0),neutral_4:(red:0.28235294,green:0.28235294,blue:0.28235294,alpha:1.0),neutral_5:(red:0.38823529,green:0.38823529,blue:0.38823529,alpha:1.0),neutral_6:(red:0.50196078,green:0.50196078,blue:0.50196078,alpha:1.0),neutral_7:(red:0.61960784,green:0.61960784,blue:0.61960784,alpha:1.0),neutral_8:(red:0.74509804,green:0.74509804,blue:0.74509804,alpha:1.0),neutral_9:(red:0.87058824,green:0.87058824,blue:0.87058824,alpha:1.0),neutral_10:(red:1.0,green:1.0,blue:1.0,alpha:1.0),accent_blue:(red:0.3882353,green:0.81568627,blue:0.87450981,alpha:1.0),accent_indigo:(red:0.63137255,green:0.75294118,blue:0.92156863,alpha:1.0),accent_purple:(red:0.90588235,green:0.61176471,blue:0.99607843,alpha:1.0),accent_pink:(red:1.0,green:0.61176471,blue:0.69411765,alpha:1.0),accent_red:(red:0.99215686,green:0.63137255,blue:0.62745098,alpha:1.0),accent_orange:(red:1.0,green:0.67843137,blue:0.0,alpha:1.0),accent_yellow:(red:0.96862745,green:0.87843137,blue:0.38431373,alpha:1.0),accent_green:(red:0.57254902,green:0.81176471,blue:0.61176471,alpha:1.0),accent_warm_grey:(red:0.79215686,green:0.72941176,blue:0.70588235,alpha:1.0),ext_warm_grey:(red:0.60784314,green:0.55686275,blue:0.54117647,alpha:1.0),ext_orange:(red:1.0,green:0.67843137,blue:0.0,alpha:1.0),ext_yellow:(red:0.99607843,green:0.85882353,blue:0.25098039,alpha:1.0),ext_blue:(red:0.28235294,green:0.72549020,blue:0.78039216,alpha:1.0),ext_purple:(red:0.81176471,green:0.49019608,blue:1.0,alpha:1.0),ext_pink:(red:0.97647059,green:0.22745098,blue:0.51372549,alpha:1.0),ext_indigo:(red:0.24313725,green:0.53333333,blue:1.0,alpha:1.0))) diff --git a/cosmic-theme/src/model/light.ron b/cosmic-theme/src/model/light.ron index 29b3ad65..06be1a49 100644 --- a/cosmic-theme/src/model/light.ron +++ b/cosmic-theme/src/model/light.ron @@ -1 +1 @@ -Light((name:"cosmic-light",bright_red:(red:0.53725490,green:0.01568627,blue:0.09411765,alpha:1.0),bright_green:(red:0.0,green:0.34117647,blue:0.17254901,alpha:1.0),bright_orange:(red:0.47450980,green:0.17254902,blue:0.0,alpha:1.0),gray_1:(red:0.84313725,green:0.84313725,blue:0.84313725,alpha:1.0),gray_2:(red:0.89411765,green:0.89411765,blue:0.89411765,alpha:1.0),neutral_0:(red:1.0,green:1.0,blue:1.0,alpha:1.0),neutral_1:(red:0.87058824,green:0.87058824,blue:0.87058824,alpha:1.0),neutral_2:(red:0.74509804,green:0.74509804,blue:0.74509804,alpha:1.0),neutral_3:(red:0.61960784,green:0.61960784,blue:0.61960784,alpha:1.0),neutral_4:(red:0.50196078,green:0.50196078,blue:0.50196078,alpha:1.0),neutral_5:(red:0.38823529,green:0.38823529,blue:0.38823529,alpha:1.0),neutral_6:(red:0.28235294,green:0.28235294,blue:0.28235294,alpha:1.0),neutral_7:(red:0.18039216,green:0.18039216,blue:0.18039216,alpha:1.0),neutral_8:(red:0.08627451,green:0.08627451,blue:0.08627451,alpha:1.0),neutral_9:(red:0.01176471,green:0.01176471,blue:0.01176471,alpha:1.0),neutral_10:(red:0.0,green:0.0,blue:0.0,alpha:1.0),accent_blue:(red:0.0,green:0.32156863,blue:0.35294118,alpha:1.0),accent_indigo:(red:0.18039216,green:0.28627451,blue:0.42745098,alpha:1.0),accent_purple:(red:0.40784314,green:0.12941176,blue:0.48627451,alpha:1.0),accent_pink:(red:0.52549020,green:0.01568627,blue:0.22745098,alpha:1.0),accent_red:(red:0.47058824,green:0.16078431,blue:0.18039216,alpha:1.0),accent_orange:(red:0.38431373,green:0.25098039,blue:0.0,alpha:1.0),accent_yellow:(red:0.32549020,green:0.28235294,blue:0.0,alpha:1.0),accent_green:(red:0.09411765,green:0.33333333,blue:0.16078431,alpha:1.0),accent_warm_grey:(red:0.33333333,green:0.27843137,blue:0.25882353,alpha:1.0),ext_warm_grey:(red:0.60784314,green:0.55686275,blue:0.54117647,alpha:1.0),ext_orange:(red:0.98431373,green:0.72156863,blue:0.42352941,alpha:1.0),ext_yellow:(red:0.96862745,green:0.87843137,blue:0.38431373,alpha:1.0),ext_blue:(red:0.41568627,green:0.79215686,blue:0.84705882,alpha:1.0),ext_purple:(red:0.83529412,green:0.54901961,blue:1.0,alpha:1.0),ext_pink:(red:1.0,green:0.61176471,blue:0.86666667,alpha:1.0),ext_indigo:(red:0.58431373,green:0.76862745,blue:0.98823529,alpha:1.0))) +Light((name:"cosmic-light",bright_red:(red:0.53725490,green:0.01568627,blue:0.09411765,alpha:1.0),bright_green:(red:0.0,green:0.34117647,blue:0.17254901,alpha:1.0),bright_orange:(red:0.47450980,green:0.17254902,blue:0.0,alpha:1.0),gray_1:(red:0.84313725,green:0.84313725,blue:0.84313725,alpha:1.0),gray_2:(red:0.89411765,green:0.89411765,blue:0.89411765,alpha:1.0),control_0:(red:1.0,green:1.0,blue:1.0,alpha:1.0),control_1:(red:0.87058824,green:0.87058824,blue:0.87058824,alpha:1.0),control_2:(red:0.74509804,green:0.74509804,blue:0.74509804,alpha:1.0),control_3:(red:0.61960784,green:0.61960784,blue:0.61960784,alpha:1.0),control_4:(red:0.50196078,green:0.50196078,blue:0.50196078,alpha:1.0),control_5:(red:0.38823529,green:0.38823529,blue:0.38823529,alpha:1.0),control_6:(red:0.28235294,green:0.28235294,blue:0.28235294,alpha:1.0),control_7:(red:0.18039216,green:0.18039216,blue:0.18039216,alpha:1.0),control_8:(red:0.08627451,green:0.08627451,blue:0.08627451,alpha:1.0),control_9:(red:0.01176471,green:0.01176471,blue:0.01176471,alpha:1.0),control_10:(red:0.0,green:0.0,blue:0.0,alpha:1.0),neutral_0:(red:1.0,green:1.0,blue:1.0,alpha:1.0),neutral_1:(red:0.87058824,green:0.87058824,blue:0.87058824,alpha:1.0),neutral_2:(red:0.74509804,green:0.74509804,blue:0.74509804,alpha:1.0),neutral_3:(red:0.61960784,green:0.61960784,blue:0.61960784,alpha:1.0),neutral_4:(red:0.50196078,green:0.50196078,blue:0.50196078,alpha:1.0),neutral_5:(red:0.38823529,green:0.38823529,blue:0.38823529,alpha:1.0),neutral_6:(red:0.28235294,green:0.28235294,blue:0.28235294,alpha:1.0),neutral_7:(red:0.18039216,green:0.18039216,blue:0.18039216,alpha:1.0),neutral_8:(red:0.08627451,green:0.08627451,blue:0.08627451,alpha:1.0),neutral_9:(red:0.01176471,green:0.01176471,blue:0.01176471,alpha:1.0),neutral_10:(red:0.0,green:0.0,blue:0.0,alpha:1.0),accent_blue:(red:0.0,green:0.32156863,blue:0.35294118,alpha:1.0),accent_indigo:(red:0.18039216,green:0.28627451,blue:0.42745098,alpha:1.0),accent_purple:(red:0.40784314,green:0.12941176,blue:0.48627451,alpha:1.0),accent_pink:(red:0.52549020,green:0.01568627,blue:0.22745098,alpha:1.0),accent_red:(red:0.47058824,green:0.16078431,blue:0.18039216,alpha:1.0),accent_orange:(red:0.38431373,green:0.25098039,blue:0.0,alpha:1.0),accent_yellow:(red:0.32549020,green:0.28235294,blue:0.0,alpha:1.0),accent_green:(red:0.09411765,green:0.33333333,blue:0.16078431,alpha:1.0),accent_warm_grey:(red:0.33333333,green:0.27843137,blue:0.25882353,alpha:1.0),ext_warm_grey:(red:0.60784314,green:0.55686275,blue:0.54117647,alpha:1.0),ext_orange:(red:0.98431373,green:0.72156863,blue:0.42352941,alpha:1.0),ext_yellow:(red:0.96862745,green:0.87843137,blue:0.38431373,alpha:1.0),ext_blue:(red:0.41568627,green:0.79215686,blue:0.84705882,alpha:1.0),ext_purple:(red:0.83529412,green:0.54901961,blue:1.0,alpha:1.0),ext_pink:(red:1.0,green:0.61176471,blue:0.86666667,alpha:1.0),ext_indigo:(red:0.58431373,green:0.76862745,blue:0.98823529,alpha:1.0))) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index d159b405..3e30a1ad 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -904,17 +904,17 @@ impl ThemeBuilder { } let p = palette.as_mut(); - p.neutral_0 = neutral_steps_arr[0]; - p.neutral_1 = neutral_steps_arr[1]; - p.neutral_2 = neutral_steps_arr[2]; - p.neutral_3 = neutral_steps_arr[3]; - p.neutral_4 = neutral_steps_arr[4]; - p.neutral_5 = neutral_steps_arr[5]; - p.neutral_6 = neutral_steps_arr[6]; - p.neutral_7 = neutral_steps_arr[7]; - p.neutral_8 = neutral_steps_arr[8]; - p.neutral_9 = neutral_steps_arr[9]; - p.neutral_10 = neutral_steps_arr[10]; + p.control_0 = neutral_steps_arr[0]; + p.control_1 = neutral_steps_arr[1]; + p.control_2 = neutral_steps_arr[2]; + p.control_3 = neutral_steps_arr[3]; + p.control_4 = neutral_steps_arr[4]; + p.control_5 = neutral_steps_arr[5]; + p.control_6 = neutral_steps_arr[6]; + p.control_7 = neutral_steps_arr[7]; + p.control_8 = neutral_steps_arr[8]; + p.control_9 = neutral_steps_arr[9]; + p.control_10 = neutral_steps_arr[10]; } let p_ref = palette.as_ref(); @@ -934,24 +934,24 @@ impl ThemeBuilder { let bg_index = color_index(bg, step_array.len()); let mut component_hovered_overlay = if bg_index < 91 { - p_ref.neutral_10 + p_ref.control_10 } else { - p_ref.neutral_0 + p_ref.control_0 }; component_hovered_overlay.alpha = 0.1; let mut component_pressed_overlay = component_hovered_overlay; component_pressed_overlay.alpha = 0.2; - // Standard button background is neutral 7 with 25% opacity + // Standard button background is control 7 with 25% opacity let button_bg = { - let mut color = p_ref.neutral_7; + let mut color = p_ref.control_7; color.alpha = 0.25; color }; let (mut button_hovered_overlay, mut button_pressed_overlay) = - (p_ref.neutral_5, p_ref.neutral_2); + (p_ref.control_5, p_ref.control_2); button_hovered_overlay.alpha = 0.2; button_pressed_overlay.alpha = 0.5; @@ -959,7 +959,7 @@ impl ThemeBuilder { let on_bg_component = get_text( color_index(bg_component, step_array.len()), &step_array, - &p_ref.neutral_8, + &p_ref.control_8, text_steps_array.as_deref(), ); @@ -967,17 +967,17 @@ impl ThemeBuilder { let container_bg = if let Some(primary_container_bg_color) = primary_container_bg { primary_container_bg_color } else { - get_surface_color(bg_index, 5, &step_array, is_dark, &p_ref.neutral_1) + get_surface_color(bg_index, 5, &step_array, is_dark, &p_ref.control_1) }; let base_index: usize = color_index(container_bg, step_array.len()); let component_base = - get_surface_color(base_index, 6, &step_array, is_dark, &p_ref.neutral_3); + get_surface_color(base_index, 6, &step_array, is_dark, &p_ref.control_3); component_hovered_overlay = if base_index < 91 { - p_ref.neutral_10 + p_ref.control_10 } else { - p_ref.neutral_0 + p_ref.control_0 }; component_hovered_overlay.alpha = 0.1; @@ -991,22 +991,22 @@ impl ThemeBuilder { get_text( color_index(component_base, step_array.len()), &step_array, - &p_ref.neutral_8, + &p_ref.control_8, text_steps_array.as_deref(), ), component_hovered_overlay, component_pressed_overlay, is_high_contrast, - p_ref.neutral_8, + p_ref.control_8, ), container_bg, get_text( base_index, &step_array, - &p_ref.neutral_8, + &p_ref.control_8, text_steps_array.as_deref(), ), - get_small_widget_color(base_index, 5, &neutral_steps, &p_ref.neutral_6), + get_small_widget_color(base_index, 5, &neutral_steps, &p_ref.control_6), is_high_contrast, ); @@ -1072,16 +1072,16 @@ impl ThemeBuilder { component_hovered_overlay, component_pressed_overlay, is_high_contrast, - p_ref.neutral_8, + p_ref.control_8, ), bg, get_text( bg_index, &step_array, - &p_ref.neutral_8, + &p_ref.control_8, text_steps_array.as_deref(), ), - get_small_widget_color(bg_index, 5, &neutral_steps, &p_ref.neutral_6), + get_small_widget_color(bg_index, 5, &neutral_steps, &p_ref.control_6), is_high_contrast, ), primary, @@ -1089,17 +1089,17 @@ impl ThemeBuilder { let container_bg = if let Some(secondary_container_bg) = secondary_container_bg { secondary_container_bg } else { - get_surface_color(bg_index, 10, &step_array, is_dark, &p_ref.neutral_2) + get_surface_color(bg_index, 10, &step_array, is_dark, &p_ref.control_2) }; let base_index = color_index(container_bg, step_array.len()); let secondary_component = - get_surface_color(base_index, 3, &step_array, is_dark, &p_ref.neutral_4); + get_surface_color(base_index, 3, &step_array, is_dark, &p_ref.control_4); component_hovered_overlay = if base_index < 91 { - p_ref.neutral_10 + p_ref.control_10 } else { - p_ref.neutral_0 + p_ref.control_0 }; component_hovered_overlay.alpha = 0.1; @@ -1113,36 +1113,36 @@ impl ThemeBuilder { get_text( color_index(secondary_component, step_array.len()), &step_array, - &p_ref.neutral_8, + &p_ref.control_8, text_steps_array.as_deref(), ), component_hovered_overlay, component_pressed_overlay, is_high_contrast, - p_ref.neutral_8, + p_ref.control_8, ), container_bg, get_text( base_index, &step_array, - &p_ref.neutral_8, + &p_ref.control_8, text_steps_array.as_deref(), ), - get_small_widget_color(base_index, 5, &neutral_steps, &p_ref.neutral_6), + get_small_widget_color(base_index, 5, &neutral_steps, &p_ref.control_6), is_high_contrast, ) }, accent: Component::colored_component( accent, - p_ref.neutral_0, + p_ref.control_0, accent, button_hovered_overlay, button_pressed_overlay, ), accent_button: Component::colored_button( accent, - p_ref.neutral_1, - p_ref.neutral_0, + p_ref.control_1, + p_ref.control_0, accent, button_hovered_overlay, button_pressed_overlay, @@ -1154,19 +1154,19 @@ impl ThemeBuilder { button_hovered_overlay, button_pressed_overlay, is_high_contrast, - p_ref.neutral_8, + p_ref.control_8, ), destructive: Component::colored_component( destructive, - p_ref.neutral_0, + p_ref.control_0, accent, button_hovered_overlay, button_pressed_overlay, ), destructive_button: Component::colored_button( destructive, - p_ref.neutral_1, - p_ref.neutral_0, + p_ref.control_1, + p_ref.control_0, accent, button_hovered_overlay, button_pressed_overlay, @@ -1174,11 +1174,11 @@ impl ThemeBuilder { icon_button: Component::component( Srgba::new(0.0, 0.0, 0.0, 0.0), accent, - p_ref.neutral_8, + p_ref.control_8, button_hovered_overlay, button_pressed_overlay, is_high_contrast, - p_ref.neutral_8, + p_ref.control_8, ), link_button: { let mut component = Component::component( @@ -1188,7 +1188,7 @@ impl ThemeBuilder { Srgba::new(0.0, 0.0, 0.0, 0.0), Srgba::new(0.0, 0.0, 0.0, 0.0), is_high_contrast, - p_ref.neutral_8, + p_ref.control_8, ); let mut on_50 = component.on; @@ -1199,15 +1199,15 @@ impl ThemeBuilder { }, success: Component::colored_component( success, - p_ref.neutral_0, + p_ref.control_0, accent, button_hovered_overlay, button_pressed_overlay, ), success_button: Component::colored_button( success, - p_ref.neutral_1, - p_ref.neutral_0, + p_ref.control_1, + p_ref.control_0, accent, button_hovered_overlay, button_pressed_overlay, @@ -1219,19 +1219,19 @@ impl ThemeBuilder { button_hovered_overlay, button_pressed_overlay, is_high_contrast, - p_ref.neutral_8, + p_ref.control_8, ), warning: Component::colored_component( warning, - p_ref.neutral_0, + p_ref.control_0, accent, button_hovered_overlay, button_pressed_overlay, ), warning_button: Component::colored_button( warning, - p_ref.neutral_10, - p_ref.neutral_0, + p_ref.control_10, + p_ref.control_0, accent, button_hovered_overlay, button_pressed_overlay, From 0041fc2d12cf7b4318fbc051d9301f4d37be085b Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 17 Jul 2025 14:46:16 -0400 Subject: [PATCH 031/352] Revert "refactor: introduce new palette colors for control tint" This reverts commit b8f9dc6cb0af2115ff0a0ec2ff9d35076ace16b8. --- cosmic-theme/src/model/cosmic_palette.rs | 23 ----- cosmic-theme/src/model/dark.ron | 2 +- cosmic-theme/src/model/light.ron | 2 +- cosmic-theme/src/model/theme.rs | 106 +++++++++++------------ 4 files changed, 55 insertions(+), 78 deletions(-) diff --git a/cosmic-theme/src/model/cosmic_palette.rs b/cosmic-theme/src/model/cosmic_palette.rs index 2c4ddfbd..6a189089 100644 --- a/cosmic-theme/src/model/cosmic_palette.rs +++ b/cosmic-theme/src/model/cosmic_palette.rs @@ -132,29 +132,6 @@ pub struct CosmicPaletteInner { /// A wider spread of dark colors for more general use. pub neutral_10: Srgba, - /// Tinted control colors. - pub control_0: Srgba, - /// Tinted control colors. - pub control_1: Srgba, - /// Tinted control colors. - pub control_2: Srgba, - /// Tinted control colors. - pub control_3: Srgba, - /// Tinted control colors. - pub control_4: Srgba, - /// Tinted control colors. - pub control_5: Srgba, - /// Tinted control colors. - pub control_6: Srgba, - /// Tinted control colors. - pub control_7: Srgba, - /// Tinted control colors. - pub control_8: Srgba, - /// Tinted control colors. - pub control_9: Srgba, - /// Tinted control colors. - pub control_10: Srgba, - /// Potential Accent Color Combos pub accent_blue: Srgba, /// Potential Accent Color Combos diff --git a/cosmic-theme/src/model/dark.ron b/cosmic-theme/src/model/dark.ron index f4531efd..4453b8bf 100644 --- a/cosmic-theme/src/model/dark.ron +++ b/cosmic-theme/src/model/dark.ron @@ -1 +1 @@ -Dark((name:"cosmic-dark",bright_red:(red:1.0,green:0.62745098,blue:0.60392157,alpha:1.0),bright_green:(red:0.36862745,green:0.85882352,blue:0.54901960,alpha:1.0),bright_orange:(red:1.0,green:0.63921569,blue:0.49019608,alpha:1.0),gray_1:(red:0.10588235,green:0.10588235,blue:0.10588235,alpha:1.0),gray_2:(red:0.14901961,green:0.14901961,blue:0.14901961,alpha:1.0),control_0:(red:0.0,green:0.0,blue:0.0,alpha:1.0),control_1:(red:0.01176471,green:0.01176471,blue:0.01176471,alpha:1.0),control_2:(red:0.08627451,green:0.08627451,blue:0.08627451,alpha:1.0),control_3:(red:0.18039216,green:0.18039216,blue:0.18039216,alpha:1.0),control_4:(red:0.28235294,green:0.28235294,blue:0.28235294,alpha:1.0),control_5:(red:0.38823529,green:0.38823529,blue:0.38823529,alpha:1.0),control_6:(red:0.50196078,green:0.50196078,blue:0.50196078,alpha:1.0),control_7:(red:0.61960784,green:0.61960784,blue:0.61960784,alpha:1.0),control_8:(red:0.74509804,green:0.74509804,blue:0.74509804,alpha:1.0),control_9:(red:0.87058824,green:0.87058824,blue:0.87058824,alpha:1.0),control_10:(red:1.0,green:1.0,blue:1.0,alpha:1.0),neutral_0:(red:0.0,green:0.0,blue:0.0,alpha:1.0),neutral_1:(red:0.01176471,green:0.01176471,blue:0.01176471,alpha:1.0),neutral_2:(red:0.08627451,green:0.08627451,blue:0.08627451,alpha:1.0),neutral_3:(red:0.18039216,green:0.18039216,blue:0.18039216,alpha:1.0),neutral_4:(red:0.28235294,green:0.28235294,blue:0.28235294,alpha:1.0),neutral_5:(red:0.38823529,green:0.38823529,blue:0.38823529,alpha:1.0),neutral_6:(red:0.50196078,green:0.50196078,blue:0.50196078,alpha:1.0),neutral_7:(red:0.61960784,green:0.61960784,blue:0.61960784,alpha:1.0),neutral_8:(red:0.74509804,green:0.74509804,blue:0.74509804,alpha:1.0),neutral_9:(red:0.87058824,green:0.87058824,blue:0.87058824,alpha:1.0),neutral_10:(red:1.0,green:1.0,blue:1.0,alpha:1.0),accent_blue:(red:0.3882353,green:0.81568627,blue:0.87450981,alpha:1.0),accent_indigo:(red:0.63137255,green:0.75294118,blue:0.92156863,alpha:1.0),accent_purple:(red:0.90588235,green:0.61176471,blue:0.99607843,alpha:1.0),accent_pink:(red:1.0,green:0.61176471,blue:0.69411765,alpha:1.0),accent_red:(red:0.99215686,green:0.63137255,blue:0.62745098,alpha:1.0),accent_orange:(red:1.0,green:0.67843137,blue:0.0,alpha:1.0),accent_yellow:(red:0.96862745,green:0.87843137,blue:0.38431373,alpha:1.0),accent_green:(red:0.57254902,green:0.81176471,blue:0.61176471,alpha:1.0),accent_warm_grey:(red:0.79215686,green:0.72941176,blue:0.70588235,alpha:1.0),ext_warm_grey:(red:0.60784314,green:0.55686275,blue:0.54117647,alpha:1.0),ext_orange:(red:1.0,green:0.67843137,blue:0.0,alpha:1.0),ext_yellow:(red:0.99607843,green:0.85882353,blue:0.25098039,alpha:1.0),ext_blue:(red:0.28235294,green:0.72549020,blue:0.78039216,alpha:1.0),ext_purple:(red:0.81176471,green:0.49019608,blue:1.0,alpha:1.0),ext_pink:(red:0.97647059,green:0.22745098,blue:0.51372549,alpha:1.0),ext_indigo:(red:0.24313725,green:0.53333333,blue:1.0,alpha:1.0))) +Dark((name:"cosmic-dark",bright_red:(red:1.0,green:0.62745098,blue:0.60392157,alpha:1.0),bright_green:(red:0.36862745,green:0.85882352,blue:0.54901960,alpha:1.0),bright_orange:(red:1.0,green:0.63921569,blue:0.49019608,alpha:1.0),gray_1:(red:0.10588235,green:0.10588235,blue:0.10588235,alpha:1.0),gray_2:(red:0.14901961,green:0.14901961,blue:0.14901961,alpha:1.0),neutral_0:(red:0.0,green:0.0,blue:0.0,alpha:1.0),neutral_1:(red:0.01176471,green:0.01176471,blue:0.01176471,alpha:1.0),neutral_2:(red:0.08627451,green:0.08627451,blue:0.08627451,alpha:1.0),neutral_3:(red:0.18039216,green:0.18039216,blue:0.18039216,alpha:1.0),neutral_4:(red:0.28235294,green:0.28235294,blue:0.28235294,alpha:1.0),neutral_5:(red:0.38823529,green:0.38823529,blue:0.38823529,alpha:1.0),neutral_6:(red:0.50196078,green:0.50196078,blue:0.50196078,alpha:1.0),neutral_7:(red:0.61960784,green:0.61960784,blue:0.61960784,alpha:1.0),neutral_8:(red:0.74509804,green:0.74509804,blue:0.74509804,alpha:1.0),neutral_9:(red:0.87058824,green:0.87058824,blue:0.87058824,alpha:1.0),neutral_10:(red:1.0,green:1.0,blue:1.0,alpha:1.0),accent_blue:(red:0.3882353,green:0.81568627,blue:0.87450981,alpha:1.0),accent_indigo:(red:0.63137255,green:0.75294118,blue:0.92156863,alpha:1.0),accent_purple:(red:0.90588235,green:0.61176471,blue:0.99607843,alpha:1.0),accent_pink:(red:1.0,green:0.61176471,blue:0.69411765,alpha:1.0),accent_red:(red:0.99215686,green:0.63137255,blue:0.62745098,alpha:1.0),accent_orange:(red:1.0,green:0.67843137,blue:0.0,alpha:1.0),accent_yellow:(red:0.96862745,green:0.87843137,blue:0.38431373,alpha:1.0),accent_green:(red:0.57254902,green:0.81176471,blue:0.61176471,alpha:1.0),accent_warm_grey:(red:0.79215686,green:0.72941176,blue:0.70588235,alpha:1.0),ext_warm_grey:(red:0.60784314,green:0.55686275,blue:0.54117647,alpha:1.0),ext_orange:(red:1.0,green:0.67843137,blue:0.0,alpha:1.0),ext_yellow:(red:0.99607843,green:0.85882353,blue:0.25098039,alpha:1.0),ext_blue:(red:0.28235294,green:0.72549020,blue:0.78039216,alpha:1.0),ext_purple:(red:0.81176471,green:0.49019608,blue:1.0,alpha:1.0),ext_pink:(red:0.97647059,green:0.22745098,blue:0.51372549,alpha:1.0),ext_indigo:(red:0.24313725,green:0.53333333,blue:1.0,alpha:1.0))) diff --git a/cosmic-theme/src/model/light.ron b/cosmic-theme/src/model/light.ron index 06be1a49..29b3ad65 100644 --- a/cosmic-theme/src/model/light.ron +++ b/cosmic-theme/src/model/light.ron @@ -1 +1 @@ -Light((name:"cosmic-light",bright_red:(red:0.53725490,green:0.01568627,blue:0.09411765,alpha:1.0),bright_green:(red:0.0,green:0.34117647,blue:0.17254901,alpha:1.0),bright_orange:(red:0.47450980,green:0.17254902,blue:0.0,alpha:1.0),gray_1:(red:0.84313725,green:0.84313725,blue:0.84313725,alpha:1.0),gray_2:(red:0.89411765,green:0.89411765,blue:0.89411765,alpha:1.0),control_0:(red:1.0,green:1.0,blue:1.0,alpha:1.0),control_1:(red:0.87058824,green:0.87058824,blue:0.87058824,alpha:1.0),control_2:(red:0.74509804,green:0.74509804,blue:0.74509804,alpha:1.0),control_3:(red:0.61960784,green:0.61960784,blue:0.61960784,alpha:1.0),control_4:(red:0.50196078,green:0.50196078,blue:0.50196078,alpha:1.0),control_5:(red:0.38823529,green:0.38823529,blue:0.38823529,alpha:1.0),control_6:(red:0.28235294,green:0.28235294,blue:0.28235294,alpha:1.0),control_7:(red:0.18039216,green:0.18039216,blue:0.18039216,alpha:1.0),control_8:(red:0.08627451,green:0.08627451,blue:0.08627451,alpha:1.0),control_9:(red:0.01176471,green:0.01176471,blue:0.01176471,alpha:1.0),control_10:(red:0.0,green:0.0,blue:0.0,alpha:1.0),neutral_0:(red:1.0,green:1.0,blue:1.0,alpha:1.0),neutral_1:(red:0.87058824,green:0.87058824,blue:0.87058824,alpha:1.0),neutral_2:(red:0.74509804,green:0.74509804,blue:0.74509804,alpha:1.0),neutral_3:(red:0.61960784,green:0.61960784,blue:0.61960784,alpha:1.0),neutral_4:(red:0.50196078,green:0.50196078,blue:0.50196078,alpha:1.0),neutral_5:(red:0.38823529,green:0.38823529,blue:0.38823529,alpha:1.0),neutral_6:(red:0.28235294,green:0.28235294,blue:0.28235294,alpha:1.0),neutral_7:(red:0.18039216,green:0.18039216,blue:0.18039216,alpha:1.0),neutral_8:(red:0.08627451,green:0.08627451,blue:0.08627451,alpha:1.0),neutral_9:(red:0.01176471,green:0.01176471,blue:0.01176471,alpha:1.0),neutral_10:(red:0.0,green:0.0,blue:0.0,alpha:1.0),accent_blue:(red:0.0,green:0.32156863,blue:0.35294118,alpha:1.0),accent_indigo:(red:0.18039216,green:0.28627451,blue:0.42745098,alpha:1.0),accent_purple:(red:0.40784314,green:0.12941176,blue:0.48627451,alpha:1.0),accent_pink:(red:0.52549020,green:0.01568627,blue:0.22745098,alpha:1.0),accent_red:(red:0.47058824,green:0.16078431,blue:0.18039216,alpha:1.0),accent_orange:(red:0.38431373,green:0.25098039,blue:0.0,alpha:1.0),accent_yellow:(red:0.32549020,green:0.28235294,blue:0.0,alpha:1.0),accent_green:(red:0.09411765,green:0.33333333,blue:0.16078431,alpha:1.0),accent_warm_grey:(red:0.33333333,green:0.27843137,blue:0.25882353,alpha:1.0),ext_warm_grey:(red:0.60784314,green:0.55686275,blue:0.54117647,alpha:1.0),ext_orange:(red:0.98431373,green:0.72156863,blue:0.42352941,alpha:1.0),ext_yellow:(red:0.96862745,green:0.87843137,blue:0.38431373,alpha:1.0),ext_blue:(red:0.41568627,green:0.79215686,blue:0.84705882,alpha:1.0),ext_purple:(red:0.83529412,green:0.54901961,blue:1.0,alpha:1.0),ext_pink:(red:1.0,green:0.61176471,blue:0.86666667,alpha:1.0),ext_indigo:(red:0.58431373,green:0.76862745,blue:0.98823529,alpha:1.0))) +Light((name:"cosmic-light",bright_red:(red:0.53725490,green:0.01568627,blue:0.09411765,alpha:1.0),bright_green:(red:0.0,green:0.34117647,blue:0.17254901,alpha:1.0),bright_orange:(red:0.47450980,green:0.17254902,blue:0.0,alpha:1.0),gray_1:(red:0.84313725,green:0.84313725,blue:0.84313725,alpha:1.0),gray_2:(red:0.89411765,green:0.89411765,blue:0.89411765,alpha:1.0),neutral_0:(red:1.0,green:1.0,blue:1.0,alpha:1.0),neutral_1:(red:0.87058824,green:0.87058824,blue:0.87058824,alpha:1.0),neutral_2:(red:0.74509804,green:0.74509804,blue:0.74509804,alpha:1.0),neutral_3:(red:0.61960784,green:0.61960784,blue:0.61960784,alpha:1.0),neutral_4:(red:0.50196078,green:0.50196078,blue:0.50196078,alpha:1.0),neutral_5:(red:0.38823529,green:0.38823529,blue:0.38823529,alpha:1.0),neutral_6:(red:0.28235294,green:0.28235294,blue:0.28235294,alpha:1.0),neutral_7:(red:0.18039216,green:0.18039216,blue:0.18039216,alpha:1.0),neutral_8:(red:0.08627451,green:0.08627451,blue:0.08627451,alpha:1.0),neutral_9:(red:0.01176471,green:0.01176471,blue:0.01176471,alpha:1.0),neutral_10:(red:0.0,green:0.0,blue:0.0,alpha:1.0),accent_blue:(red:0.0,green:0.32156863,blue:0.35294118,alpha:1.0),accent_indigo:(red:0.18039216,green:0.28627451,blue:0.42745098,alpha:1.0),accent_purple:(red:0.40784314,green:0.12941176,blue:0.48627451,alpha:1.0),accent_pink:(red:0.52549020,green:0.01568627,blue:0.22745098,alpha:1.0),accent_red:(red:0.47058824,green:0.16078431,blue:0.18039216,alpha:1.0),accent_orange:(red:0.38431373,green:0.25098039,blue:0.0,alpha:1.0),accent_yellow:(red:0.32549020,green:0.28235294,blue:0.0,alpha:1.0),accent_green:(red:0.09411765,green:0.33333333,blue:0.16078431,alpha:1.0),accent_warm_grey:(red:0.33333333,green:0.27843137,blue:0.25882353,alpha:1.0),ext_warm_grey:(red:0.60784314,green:0.55686275,blue:0.54117647,alpha:1.0),ext_orange:(red:0.98431373,green:0.72156863,blue:0.42352941,alpha:1.0),ext_yellow:(red:0.96862745,green:0.87843137,blue:0.38431373,alpha:1.0),ext_blue:(red:0.41568627,green:0.79215686,blue:0.84705882,alpha:1.0),ext_purple:(red:0.83529412,green:0.54901961,blue:1.0,alpha:1.0),ext_pink:(red:1.0,green:0.61176471,blue:0.86666667,alpha:1.0),ext_indigo:(red:0.58431373,green:0.76862745,blue:0.98823529,alpha:1.0))) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 3e30a1ad..d159b405 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -904,17 +904,17 @@ impl ThemeBuilder { } let p = palette.as_mut(); - p.control_0 = neutral_steps_arr[0]; - p.control_1 = neutral_steps_arr[1]; - p.control_2 = neutral_steps_arr[2]; - p.control_3 = neutral_steps_arr[3]; - p.control_4 = neutral_steps_arr[4]; - p.control_5 = neutral_steps_arr[5]; - p.control_6 = neutral_steps_arr[6]; - p.control_7 = neutral_steps_arr[7]; - p.control_8 = neutral_steps_arr[8]; - p.control_9 = neutral_steps_arr[9]; - p.control_10 = neutral_steps_arr[10]; + p.neutral_0 = neutral_steps_arr[0]; + p.neutral_1 = neutral_steps_arr[1]; + p.neutral_2 = neutral_steps_arr[2]; + p.neutral_3 = neutral_steps_arr[3]; + p.neutral_4 = neutral_steps_arr[4]; + p.neutral_5 = neutral_steps_arr[5]; + p.neutral_6 = neutral_steps_arr[6]; + p.neutral_7 = neutral_steps_arr[7]; + p.neutral_8 = neutral_steps_arr[8]; + p.neutral_9 = neutral_steps_arr[9]; + p.neutral_10 = neutral_steps_arr[10]; } let p_ref = palette.as_ref(); @@ -934,24 +934,24 @@ impl ThemeBuilder { let bg_index = color_index(bg, step_array.len()); let mut component_hovered_overlay = if bg_index < 91 { - p_ref.control_10 + p_ref.neutral_10 } else { - p_ref.control_0 + p_ref.neutral_0 }; component_hovered_overlay.alpha = 0.1; let mut component_pressed_overlay = component_hovered_overlay; component_pressed_overlay.alpha = 0.2; - // Standard button background is control 7 with 25% opacity + // Standard button background is neutral 7 with 25% opacity let button_bg = { - let mut color = p_ref.control_7; + let mut color = p_ref.neutral_7; color.alpha = 0.25; color }; let (mut button_hovered_overlay, mut button_pressed_overlay) = - (p_ref.control_5, p_ref.control_2); + (p_ref.neutral_5, p_ref.neutral_2); button_hovered_overlay.alpha = 0.2; button_pressed_overlay.alpha = 0.5; @@ -959,7 +959,7 @@ impl ThemeBuilder { let on_bg_component = get_text( color_index(bg_component, step_array.len()), &step_array, - &p_ref.control_8, + &p_ref.neutral_8, text_steps_array.as_deref(), ); @@ -967,17 +967,17 @@ impl ThemeBuilder { let container_bg = if let Some(primary_container_bg_color) = primary_container_bg { primary_container_bg_color } else { - get_surface_color(bg_index, 5, &step_array, is_dark, &p_ref.control_1) + get_surface_color(bg_index, 5, &step_array, is_dark, &p_ref.neutral_1) }; let base_index: usize = color_index(container_bg, step_array.len()); let component_base = - get_surface_color(base_index, 6, &step_array, is_dark, &p_ref.control_3); + get_surface_color(base_index, 6, &step_array, is_dark, &p_ref.neutral_3); component_hovered_overlay = if base_index < 91 { - p_ref.control_10 + p_ref.neutral_10 } else { - p_ref.control_0 + p_ref.neutral_0 }; component_hovered_overlay.alpha = 0.1; @@ -991,22 +991,22 @@ impl ThemeBuilder { get_text( color_index(component_base, step_array.len()), &step_array, - &p_ref.control_8, + &p_ref.neutral_8, text_steps_array.as_deref(), ), component_hovered_overlay, component_pressed_overlay, is_high_contrast, - p_ref.control_8, + p_ref.neutral_8, ), container_bg, get_text( base_index, &step_array, - &p_ref.control_8, + &p_ref.neutral_8, text_steps_array.as_deref(), ), - get_small_widget_color(base_index, 5, &neutral_steps, &p_ref.control_6), + get_small_widget_color(base_index, 5, &neutral_steps, &p_ref.neutral_6), is_high_contrast, ); @@ -1072,16 +1072,16 @@ impl ThemeBuilder { component_hovered_overlay, component_pressed_overlay, is_high_contrast, - p_ref.control_8, + p_ref.neutral_8, ), bg, get_text( bg_index, &step_array, - &p_ref.control_8, + &p_ref.neutral_8, text_steps_array.as_deref(), ), - get_small_widget_color(bg_index, 5, &neutral_steps, &p_ref.control_6), + get_small_widget_color(bg_index, 5, &neutral_steps, &p_ref.neutral_6), is_high_contrast, ), primary, @@ -1089,17 +1089,17 @@ impl ThemeBuilder { let container_bg = if let Some(secondary_container_bg) = secondary_container_bg { secondary_container_bg } else { - get_surface_color(bg_index, 10, &step_array, is_dark, &p_ref.control_2) + get_surface_color(bg_index, 10, &step_array, is_dark, &p_ref.neutral_2) }; let base_index = color_index(container_bg, step_array.len()); let secondary_component = - get_surface_color(base_index, 3, &step_array, is_dark, &p_ref.control_4); + get_surface_color(base_index, 3, &step_array, is_dark, &p_ref.neutral_4); component_hovered_overlay = if base_index < 91 { - p_ref.control_10 + p_ref.neutral_10 } else { - p_ref.control_0 + p_ref.neutral_0 }; component_hovered_overlay.alpha = 0.1; @@ -1113,36 +1113,36 @@ impl ThemeBuilder { get_text( color_index(secondary_component, step_array.len()), &step_array, - &p_ref.control_8, + &p_ref.neutral_8, text_steps_array.as_deref(), ), component_hovered_overlay, component_pressed_overlay, is_high_contrast, - p_ref.control_8, + p_ref.neutral_8, ), container_bg, get_text( base_index, &step_array, - &p_ref.control_8, + &p_ref.neutral_8, text_steps_array.as_deref(), ), - get_small_widget_color(base_index, 5, &neutral_steps, &p_ref.control_6), + get_small_widget_color(base_index, 5, &neutral_steps, &p_ref.neutral_6), is_high_contrast, ) }, accent: Component::colored_component( accent, - p_ref.control_0, + p_ref.neutral_0, accent, button_hovered_overlay, button_pressed_overlay, ), accent_button: Component::colored_button( accent, - p_ref.control_1, - p_ref.control_0, + p_ref.neutral_1, + p_ref.neutral_0, accent, button_hovered_overlay, button_pressed_overlay, @@ -1154,19 +1154,19 @@ impl ThemeBuilder { button_hovered_overlay, button_pressed_overlay, is_high_contrast, - p_ref.control_8, + p_ref.neutral_8, ), destructive: Component::colored_component( destructive, - p_ref.control_0, + p_ref.neutral_0, accent, button_hovered_overlay, button_pressed_overlay, ), destructive_button: Component::colored_button( destructive, - p_ref.control_1, - p_ref.control_0, + p_ref.neutral_1, + p_ref.neutral_0, accent, button_hovered_overlay, button_pressed_overlay, @@ -1174,11 +1174,11 @@ impl ThemeBuilder { icon_button: Component::component( Srgba::new(0.0, 0.0, 0.0, 0.0), accent, - p_ref.control_8, + p_ref.neutral_8, button_hovered_overlay, button_pressed_overlay, is_high_contrast, - p_ref.control_8, + p_ref.neutral_8, ), link_button: { let mut component = Component::component( @@ -1188,7 +1188,7 @@ impl ThemeBuilder { Srgba::new(0.0, 0.0, 0.0, 0.0), Srgba::new(0.0, 0.0, 0.0, 0.0), is_high_contrast, - p_ref.control_8, + p_ref.neutral_8, ); let mut on_50 = component.on; @@ -1199,15 +1199,15 @@ impl ThemeBuilder { }, success: Component::colored_component( success, - p_ref.control_0, + p_ref.neutral_0, accent, button_hovered_overlay, button_pressed_overlay, ), success_button: Component::colored_button( success, - p_ref.control_1, - p_ref.control_0, + p_ref.neutral_1, + p_ref.neutral_0, accent, button_hovered_overlay, button_pressed_overlay, @@ -1219,19 +1219,19 @@ impl ThemeBuilder { button_hovered_overlay, button_pressed_overlay, is_high_contrast, - p_ref.control_8, + p_ref.neutral_8, ), warning: Component::colored_component( warning, - p_ref.control_0, + p_ref.neutral_0, accent, button_hovered_overlay, button_pressed_overlay, ), warning_button: Component::colored_button( warning, - p_ref.control_10, - p_ref.control_0, + p_ref.neutral_10, + p_ref.neutral_0, accent, button_hovered_overlay, button_pressed_overlay, From 7748e59ae6576d93fc8b9594b1e159682e8ab844 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 17 Jul 2025 14:48:54 -0400 Subject: [PATCH 032/352] refactor: better method of implementing tinted control colors --- cosmic-theme/src/model/theme.rs | 211 +++++++++++++++++++++++--------- 1 file changed, 155 insertions(+), 56 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index d159b405..4d42ad3f 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -100,6 +100,10 @@ pub struct Theme { /// accent text colors /// If None, accent base color is the accent text color. pub accent_text: Option, + /// control tint color + pub control_tint: Option, + /// text tint color + pub text_tint: Option, } impl Default for Theme { @@ -164,6 +168,109 @@ impl Theme { todo!(); } + #[must_use] + #[allow(clippy::doc_markdown)] + #[inline] + /// get control_0 color + pub fn control_0(&self) -> Srgba { + self.tint_neutral(self.palette.neutral_0) + } + + #[must_use] + #[allow(clippy::doc_markdown)] + #[inline] + /// get control_1 color + pub fn control_1(&self) -> Srgba { + self.tint_neutral(self.palette.neutral_1) + } + + #[must_use] + #[allow(clippy::doc_markdown)] + #[inline] + /// get control_2 color + pub fn control_2(&self) -> Srgba { + self.tint_neutral(self.palette.neutral_2) + } + + #[must_use] + #[allow(clippy::doc_markdown)] + #[inline] + /// get control_3 color + pub fn control_3(&self) -> Srgba { + self.tint_neutral(self.palette.neutral_3) + } + + #[must_use] + #[allow(clippy::doc_markdown)] + #[inline] + /// get control_3 color + pub fn control_4(&self) -> Srgba { + self.tint_neutral(self.palette.neutral_4) + } + + #[must_use] + #[allow(clippy::doc_markdown)] + #[inline] + /// get control_3 color + pub fn control_5(&self) -> Srgba { + self.tint_neutral(self.palette.neutral_5) + } + + #[must_use] + #[allow(clippy::doc_markdown)] + #[inline] + /// get control_3 color + pub fn control_6(&self) -> Srgba { + self.tint_neutral(self.palette.neutral_6) + } + + #[must_use] + #[allow(clippy::doc_markdown)] + #[inline] + /// get control_3 color + pub fn control_7(&self) -> Srgba { + self.tint_neutral(self.palette.neutral_7) + } + + #[must_use] + #[allow(clippy::doc_markdown)] + #[inline] + /// get control_3 color + pub fn control_8(&self) -> Srgba { + self.tint_neutral(self.palette.neutral_8) + } + + #[must_use] + #[allow(clippy::doc_markdown)] + #[inline] + /// get control_3 color + pub fn control_9(&self) -> Srgba { + self.tint_neutral(self.palette.neutral_9) + } + + #[must_use] + #[allow(clippy::doc_markdown)] + #[inline] + /// get control_3 color + pub fn control_10(&self) -> Srgba { + self.tint_neutral(self.palette.neutral_10) + } + + #[must_use] + #[allow(clippy::doc_markdown)] + #[inline] + /// get @accent_color + fn tint_neutral(&self, neutral: Srgba) -> Srgba { + let Some(tint) = self.control_tint else { + return neutral; + }; + let mut oklch_neutral: Oklcha = neutral.into_color(); + let oklch_tint: Oklcha = tint.into_color(); + oklch_neutral.hue = oklch_tint.hue; + oklch_neutral.chroma = oklch_tint.chroma; + oklch_neutral.into_color() + } + // TODO convenient getter functions for each named color variable #[must_use] #[allow(clippy::doc_markdown)] @@ -897,25 +1004,15 @@ impl ThemeBuilder { let text_steps_array = text_tint.map(|c| steps(c, NonZeroUsize::new(100).unwrap())); - if let Some(neutral_tint) = neutral_tint { + let control_steps_array = if let Some(neutral_tint) = neutral_tint { let mut neutral_steps_arr = steps(neutral_tint, NonZeroUsize::new(11).unwrap()); if !is_dark { neutral_steps_arr.reverse(); } - - let p = palette.as_mut(); - p.neutral_0 = neutral_steps_arr[0]; - p.neutral_1 = neutral_steps_arr[1]; - p.neutral_2 = neutral_steps_arr[2]; - p.neutral_3 = neutral_steps_arr[3]; - p.neutral_4 = neutral_steps_arr[4]; - p.neutral_5 = neutral_steps_arr[5]; - p.neutral_6 = neutral_steps_arr[6]; - p.neutral_7 = neutral_steps_arr[7]; - p.neutral_8 = neutral_steps_arr[8]; - p.neutral_9 = neutral_steps_arr[9]; - p.neutral_10 = neutral_steps_arr[10]; - } + neutral_steps_arr + } else { + steps(palette.as_ref().neutral_2, NonZeroUsize::new(11).unwrap()) + }; let p_ref = palette.as_ref(); @@ -934,9 +1031,9 @@ impl ThemeBuilder { let bg_index = color_index(bg, step_array.len()); let mut component_hovered_overlay = if bg_index < 91 { - p_ref.neutral_10 + control_steps_array[10] } else { - p_ref.neutral_0 + control_steps_array[0] }; component_hovered_overlay.alpha = 0.1; @@ -945,13 +1042,13 @@ impl ThemeBuilder { // Standard button background is neutral 7 with 25% opacity let button_bg = { - let mut color = p_ref.neutral_7; + let mut color = control_steps_array[7]; color.alpha = 0.25; color }; let (mut button_hovered_overlay, mut button_pressed_overlay) = - (p_ref.neutral_5, p_ref.neutral_2); + (control_steps_array[5], control_steps_array[2]); button_hovered_overlay.alpha = 0.2; button_pressed_overlay.alpha = 0.5; @@ -959,7 +1056,7 @@ impl ThemeBuilder { let on_bg_component = get_text( color_index(bg_component, step_array.len()), &step_array, - &p_ref.neutral_8, + &control_steps_array[8], text_steps_array.as_deref(), ); @@ -967,17 +1064,17 @@ impl ThemeBuilder { let container_bg = if let Some(primary_container_bg_color) = primary_container_bg { primary_container_bg_color } else { - get_surface_color(bg_index, 5, &step_array, is_dark, &p_ref.neutral_1) + get_surface_color(bg_index, 5, &step_array, is_dark, &control_steps_array[1]) }; let base_index: usize = color_index(container_bg, step_array.len()); let component_base = - get_surface_color(base_index, 6, &step_array, is_dark, &p_ref.neutral_3); + get_surface_color(base_index, 6, &step_array, is_dark, &control_steps_array[3]); component_hovered_overlay = if base_index < 91 { - p_ref.neutral_10 + control_steps_array[10] } else { - p_ref.neutral_0 + control_steps_array[0] }; component_hovered_overlay.alpha = 0.1; @@ -991,22 +1088,22 @@ impl ThemeBuilder { get_text( color_index(component_base, step_array.len()), &step_array, - &p_ref.neutral_8, + &control_steps_array[8], text_steps_array.as_deref(), ), component_hovered_overlay, component_pressed_overlay, is_high_contrast, - p_ref.neutral_8, + control_steps_array[8], ), container_bg, get_text( base_index, &step_array, - &p_ref.neutral_8, + &control_steps_array[8], text_steps_array.as_deref(), ), - get_small_widget_color(base_index, 5, &neutral_steps, &p_ref.neutral_6), + get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), is_high_contrast, ); @@ -1072,16 +1169,16 @@ impl ThemeBuilder { component_hovered_overlay, component_pressed_overlay, is_high_contrast, - p_ref.neutral_8, + control_steps_array[8], ), bg, get_text( bg_index, &step_array, - &p_ref.neutral_8, + &control_steps_array[8], text_steps_array.as_deref(), ), - get_small_widget_color(bg_index, 5, &neutral_steps, &p_ref.neutral_6), + get_small_widget_color(bg_index, 5, &neutral_steps, &control_steps_array[6]), is_high_contrast, ), primary, @@ -1089,17 +1186,17 @@ impl ThemeBuilder { let container_bg = if let Some(secondary_container_bg) = secondary_container_bg { secondary_container_bg } else { - get_surface_color(bg_index, 10, &step_array, is_dark, &p_ref.neutral_2) + get_surface_color(bg_index, 10, &step_array, is_dark, &control_steps_array[2]) }; let base_index = color_index(container_bg, step_array.len()); let secondary_component = - get_surface_color(base_index, 3, &step_array, is_dark, &p_ref.neutral_4); + get_surface_color(base_index, 3, &step_array, is_dark, &control_steps_array[4]); component_hovered_overlay = if base_index < 91 { - p_ref.neutral_10 + control_steps_array[10] } else { - p_ref.neutral_0 + control_steps_array[0] }; component_hovered_overlay.alpha = 0.1; @@ -1113,36 +1210,36 @@ impl ThemeBuilder { get_text( color_index(secondary_component, step_array.len()), &step_array, - &p_ref.neutral_8, + &control_steps_array[8], text_steps_array.as_deref(), ), component_hovered_overlay, component_pressed_overlay, is_high_contrast, - p_ref.neutral_8, + control_steps_array[8], ), container_bg, get_text( base_index, &step_array, - &p_ref.neutral_8, + &control_steps_array[8], text_steps_array.as_deref(), ), - get_small_widget_color(base_index, 5, &neutral_steps, &p_ref.neutral_6), + get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), is_high_contrast, ) }, accent: Component::colored_component( accent, - p_ref.neutral_0, + control_steps_array[0], accent, button_hovered_overlay, button_pressed_overlay, ), accent_button: Component::colored_button( accent, - p_ref.neutral_1, - p_ref.neutral_0, + control_steps_array[1], + control_steps_array[0], accent, button_hovered_overlay, button_pressed_overlay, @@ -1154,19 +1251,19 @@ impl ThemeBuilder { button_hovered_overlay, button_pressed_overlay, is_high_contrast, - p_ref.neutral_8, + control_steps_array[8], ), destructive: Component::colored_component( destructive, - p_ref.neutral_0, + control_steps_array[0], accent, button_hovered_overlay, button_pressed_overlay, ), destructive_button: Component::colored_button( destructive, - p_ref.neutral_1, - p_ref.neutral_0, + control_steps_array[1], + control_steps_array[0], accent, button_hovered_overlay, button_pressed_overlay, @@ -1174,11 +1271,11 @@ impl ThemeBuilder { icon_button: Component::component( Srgba::new(0.0, 0.0, 0.0, 0.0), accent, - p_ref.neutral_8, + control_steps_array[8], button_hovered_overlay, button_pressed_overlay, is_high_contrast, - p_ref.neutral_8, + control_steps_array[8], ), link_button: { let mut component = Component::component( @@ -1188,7 +1285,7 @@ impl ThemeBuilder { Srgba::new(0.0, 0.0, 0.0, 0.0), Srgba::new(0.0, 0.0, 0.0, 0.0), is_high_contrast, - p_ref.neutral_8, + control_steps_array[8], ); let mut on_50 = component.on; @@ -1199,15 +1296,15 @@ impl ThemeBuilder { }, success: Component::colored_component( success, - p_ref.neutral_0, + control_steps_array[0], accent, button_hovered_overlay, button_pressed_overlay, ), success_button: Component::colored_button( success, - p_ref.neutral_1, - p_ref.neutral_0, + control_steps_array[1], + control_steps_array[0], accent, button_hovered_overlay, button_pressed_overlay, @@ -1219,19 +1316,19 @@ impl ThemeBuilder { button_hovered_overlay, button_pressed_overlay, is_high_contrast, - p_ref.neutral_8, + control_steps_array[8], ), warning: Component::colored_component( warning, - p_ref.neutral_0, + control_steps_array[0], accent, button_hovered_overlay, button_pressed_overlay, ), warning_button: Component::colored_button( warning, - p_ref.neutral_10, - p_ref.neutral_0, + control_steps_array[10], + control_steps_array[0], accent, button_hovered_overlay, button_pressed_overlay, @@ -1246,6 +1343,8 @@ impl ThemeBuilder { window_hint, is_frosted, accent_text, + control_tint: neutral_tint, + text_tint, }; theme.spacing = spacing; theme.corner_radii = corner_radii; From ec7a531539ab0fbb3c488b184a27dd273e61cde7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= Date: Fri, 20 Jun 2025 01:56:04 +0200 Subject: [PATCH 033/352] chore: use `with_alpha()` where applicable --- cosmic-theme/src/model/derivation.rs | 20 +++------ cosmic-theme/src/model/theme.rs | 29 +++++-------- cosmic-theme/src/output/gtk4_output.rs | 5 +-- src/theme/style/iced.rs | 57 +++++++++----------------- src/theme/style/segmented_button.rs | 35 ++++++++-------- src/widget/spin_button.rs | 2 - 6 files changed, 57 insertions(+), 91 deletions(-) diff --git a/cosmic-theme/src/model/derivation.rs b/cosmic-theme/src/model/derivation.rs index f4147c2c..bcc4990f 100644 --- a/cosmic-theme/src/model/derivation.rs +++ b/cosmic-theme/src/model/derivation.rs @@ -1,4 +1,4 @@ -use palette::Srgba; +use palette::{Srgba, WithAlpha}; use serde::{Deserialize, Serialize}; use crate::composite::over; @@ -27,9 +27,7 @@ impl Container { mut small_widget: Srgba, is_high_contrast: bool, ) -> Self { - let mut divider_c = on; - divider_c.alpha = if is_high_contrast { 0.5 } else { 0.2 }; - + let divider_c = on.with_alpha(if is_high_contrast { 0.5 } else { 0.2 }); small_widget.alpha = 0.25; Self { @@ -115,13 +113,11 @@ impl Component { hovered: Srgba, pressed: Srgba, ) -> Self { - let base: Srgba = base; let mut base_50 = base; base_50.alpha *= 0.5; let on_20 = neutral; - let mut on_50: Srgba = on_20; - on_50.alpha = 0.5; + let on_50 = on_20.with_alpha(0.5); Component { base, @@ -151,8 +147,7 @@ impl Component { let mut component = Component::colored_component(base, overlay, accent, hovered, pressed); component.on = on_button; - let mut on_disabled = on_button; - on_disabled.alpha = 0.5; + let on_disabled = on_button.with_alpha(0.5); component.on_disabled = on_disabled; component @@ -172,11 +167,8 @@ impl Component { let mut base_50 = base; base_50.alpha *= 0.5; - let mut on_20 = on_component; - let mut on_50 = on_20; - - on_20.alpha = 0.2; - on_50.alpha = 0.5; + let on_20 = on_component.with_alpha(0.2); + let on_50 = on_20.with_alpha(0.5); let mut disabled_border = border; disabled_border.alpha *= 0.5; diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 4d42ad3f..6b9d1783 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1,11 +1,13 @@ use crate::{ composite::over, - steps::{color_index, get_index, get_small_widget_color, get_surface_color, get_text, steps}, + steps::{color_index, get_small_widget_color, get_surface_color, get_text, steps}, Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, Spacing, ThemeMode, DARK_PALETTE, LIGHT_PALETTE, NAME, }; use cosmic_config::{Config, CosmicConfigEntry}; -use palette::{color_difference::Wcag21RelativeContrast, rgb::Rgb, IntoColor, Oklcha, Srgb, Srgba}; +use palette::{ + color_difference::Wcag21RelativeContrast, rgb::Rgb, IntoColor, Oklcha, Srgb, Srgba, WithAlpha, +}; use serde::{Deserialize, Serialize}; use std::num::NonZeroUsize; @@ -309,9 +311,7 @@ impl Theme { #[inline] /// get @small_widget_divider pub fn small_widget_divider(&self) -> Srgba { - let mut neutral_9 = self.palette.neutral_9; - neutral_9.alpha = 0.2; - neutral_9 + self.palette.neutral_9.with_alpha(0.2) } // Containers @@ -1041,16 +1041,12 @@ impl ThemeBuilder { component_pressed_overlay.alpha = 0.2; // Standard button background is neutral 7 with 25% opacity - let button_bg = { - let mut color = control_steps_array[7]; - color.alpha = 0.25; - color - }; + let button_bg = control_steps_array[7].with_alpha(0.25); - let (mut button_hovered_overlay, mut button_pressed_overlay) = - (control_steps_array[5], control_steps_array[2]); - button_hovered_overlay.alpha = 0.2; - button_pressed_overlay.alpha = 0.5; + let (button_hovered_overlay, button_pressed_overlay) = ( + control_steps_array[5].with_alpha(0.2), + control_steps_array[2].with_alpha(0.5), + ); let bg_component = get_surface_color(bg_index, 8, &step_array, is_dark, &p_ref.neutral_2); let on_bg_component = get_text( @@ -1288,10 +1284,7 @@ impl ThemeBuilder { control_steps_array[8], ); - let mut on_50 = component.on; - on_50.alpha = 0.5; - - component.on_disabled = over(on_50, component.base); + component.on_disabled = over(component.on.with_alpha(0.5), component.base); component }, success: Component::colored_component( diff --git a/cosmic-theme/src/output/gtk4_output.rs b/cosmic-theme/src/output/gtk4_output.rs index c172e4ec..9d7210f0 100644 --- a/cosmic-theme/src/output/gtk4_output.rs +++ b/cosmic-theme/src/output/gtk4_output.rs @@ -1,5 +1,5 @@ use crate::{composite::over, steps::steps, Component, Theme}; -use palette::{rgb::Rgba, Darken, IntoColor, Lighten, Srgba}; +use palette::{rgb::Rgba, Darken, IntoColor, Lighten, Srgba, WithAlpha}; use std::{ fs::{self, File}, io::{self, Write}, @@ -75,8 +75,7 @@ impl Theme { Rgba::new(0.0, 0.0, 0.0, 0.08) }); - let mut inverted_bg_divider = background.base; - inverted_bg_divider.alpha = 0.5; + let inverted_bg_divider = background.base.with_alpha(0.5); let scrollbar_outline = to_rgba(inverted_bg_divider); let mut css = format! {r#"/* GENERATED BY COSMIC */ diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index b284e4e3..f62e5825 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -16,6 +16,7 @@ use iced::{ }; use iced_core::{Background, Border, Color, Shadow, Vector}; use iced_widget::{pane_grid::Highlight, text_editor, text_input}; +use palette::WithAlpha; use std::rc::Rc; pub mod application { @@ -720,9 +721,8 @@ impl slider::Catalog for Theme { border_radius: cosmic.corner_radii.radius_m.into(), }; appearance.handle.border_width = 3.0; - let mut border_color = self.cosmic().palette.neutral_10; - border_color.alpha = 0.1; - appearance.handle.border_color = border_color.into(); + appearance.handle.border_color = + self.cosmic().palette.neutral_10.with_alpha(0.1).into(); appearance } Slider::Custom { hovered, .. } => hovered(self), @@ -736,15 +736,12 @@ impl slider::Catalog for Theme { border_radius: cosmic.corner_radii.radius_m.into(), }; appearance.handle.border_width = 3.0; - let mut border_color = self.cosmic().palette.neutral_10; - border_color.alpha = 0.1; - appearance.handle.border_color = border_color.into(); + appearance.handle.border_color = + self.cosmic().palette.neutral_10.with_alpha(0.1).into(); appearance }; - let mut border_color = self.cosmic().palette.neutral_10; - border_color.alpha = 0.2; - style.handle.border_color = border_color.into(); - + style.handle.border_color = + self.cosmic().palette.neutral_10.with_alpha(0.2).into(); style } Slider::Custom { dragging, .. } => dragging(self), @@ -824,8 +821,6 @@ impl radio::Catalog for Theme { fn style(&self, class: &Self::Class<'_>, status: radio::Status) -> radio::Style { let theme = self.cosmic(); - let mut neutral_10 = theme.palette.neutral_10; - neutral_10.alpha = 0.1; match status { radio::Status::Active { is_selected } => radio::Style { @@ -850,7 +845,7 @@ impl radio::Catalog for Theme { Color::from(theme.accent.base).into() } else { // TODO: this seems to be defined weirdly in FIGMA - Color::from(neutral_10).into() + Color::from(theme.palette.neutral_10.with_alpha(0.1)).into() }, dot_color: theme.accent.on.into(), border_width: 1.0, @@ -877,8 +872,7 @@ impl toggler::Catalog for Theme { fn style(&self, class: &Self::Class<'_>, status: toggler::Status) -> toggler::Style { let cosmic = self.cosmic(); const HANDLE_MARGIN: f32 = 2.0; - let mut neutral_10 = cosmic.palette.neutral_10; - neutral_10.alpha = 0.1; + let neutral_10 = cosmic.palette.neutral_10.with_alpha(0.1); let mut active = toggler::Style { background: if matches!(status, toggler::Status::Active { is_toggled: true }) { @@ -1098,8 +1092,7 @@ impl scrollable::Catalog for Theme { match status { scrollable::Status::Active => { let cosmic = self.cosmic(); - let mut neutral_5 = cosmic.palette.neutral_5; - neutral_5.alpha = 0.7; + let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); let mut a = scrollable::Style { container: iced_container::transparent(self), vertical_rail: scrollable::Rail { @@ -1134,8 +1127,7 @@ impl scrollable::Catalog for Theme { }; if matches!(class, Scrollable::Permanent) { - let mut neutral_3 = cosmic.palette.neutral_3; - neutral_3.alpha = 0.7; + let neutral_3 = cosmic.palette.neutral_3.with_alpha(0.7); a.horizontal_rail.background = Some(Background::Color(neutral_3.into())); a.vertical_rail.background = Some(Background::Color(neutral_3.into())); } @@ -1145,14 +1137,12 @@ impl scrollable::Catalog for Theme { // TODO handle vertical / horizontal scrollable::Status::Hovered { .. } | scrollable::Status::Dragged { .. } => { let cosmic = self.cosmic(); - let mut neutral_5 = cosmic.palette.neutral_5; - neutral_5.alpha = 0.7; + let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); // TODO hover // if is_mouse_over_scrollbar { - // let mut hover_overlay = cosmic.palette.neutral_0; - // hover_overlay.alpha = 0.2; + // let hover_overlay = cosmic.palette.neutral_0.with_alpha(0.2); // neutral_5 = over(hover_overlay, neutral_5); // } let mut a: scrollable::Style = scrollable::Style { @@ -1189,8 +1179,7 @@ impl scrollable::Catalog for Theme { }; if matches!(class, Scrollable::Permanent) { - let mut neutral_3 = cosmic.palette.neutral_3; - neutral_3.alpha = 0.7; + let neutral_3 = cosmic.palette.neutral_3.with_alpha(0.7); a.horizontal_rail.background = Some(Background::Color(neutral_3.into())); a.vertical_rail.background = Some(Background::Color(neutral_3.into())); } @@ -1289,13 +1278,11 @@ impl text_input::Catalog for Theme { fn style(&self, class: &Self::Class<'_>, status: text_input::Status) -> text_input::Style { let palette = self.cosmic(); - let mut bg = palette.palette.neutral_7; - bg.alpha = 0.25; + let bg = palette.palette.neutral_7.with_alpha(0.25); - let mut neutral_9 = palette.palette.neutral_9; + let neutral_9 = palette.palette.neutral_9; let value = neutral_9.into(); - neutral_9.alpha = 0.7; - let placeholder = neutral_9.into(); + let placeholder = neutral_9.with_alpha(0.7).into(); let selection = palette.accent.base.into(); let mut appearance = match class { @@ -1327,8 +1314,7 @@ impl text_input::Catalog for Theme { match status { text_input::Status::Active => appearance, text_input::Status::Hovered => { - let mut bg = palette.palette.neutral_7; - bg.alpha = 0.25; + let bg = palette.palette.neutral_7.with_alpha(0.25); match class { TextInput::Default => text_input::Style { @@ -1357,8 +1343,7 @@ impl text_input::Catalog for Theme { } } text_input::Status::Focused => { - let mut bg = palette.palette.neutral_7; - bg.alpha = 0.25; + let bg = palette.palette.neutral_7.with_alpha(0.25); match class { TextInput::Default => text_input::Style { @@ -1433,9 +1418,7 @@ impl iced_widget::text_editor::Catalog for Theme { let selection = cosmic.accent.base.into(); let value = cosmic.palette.neutral_9.into(); - let mut placeholder = cosmic.palette.neutral_9; - placeholder.alpha = 0.7; - let placeholder = placeholder.into(); + let placeholder = cosmic.palette.neutral_9.with_alpha(0.7).into(); let icon = cosmic.background.on.into(); match status { diff --git a/src/theme/style/segmented_button.rs b/src/theme/style/segmented_button.rs index 70e2c937..c4d81151 100644 --- a/src/theme/style/segmented_button.rs +++ b/src/theme/style/segmented_button.rs @@ -7,6 +7,7 @@ use crate::widget::segmented_button::{Appearance, ItemAppearance, StyleSheet}; use crate::{theme::Theme, widget::segmented_button::ItemStatusAppearance}; use cosmic_theme::{Component, Container}; use iced_core::{Background, border::Radius}; +use palette::WithAlpha; #[derive(Default)] pub enum SegmentedButton { @@ -166,19 +167,19 @@ mod horizontal { use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; use cosmic_theme::Component; use iced_core::{Background, border::Radius}; + use palette::WithAlpha; pub fn selection_active( cosmic: &cosmic_theme::Theme, component: &Component, ) -> ItemStatusAppearance { - let mut color = cosmic.palette.neutral_5; - color.alpha = 0.2; - let rad_m = cosmic.corner_radii.radius_m; let rad_0 = cosmic.corner_radii.radius_0; ItemStatusAppearance { - background: Some(Background::Color(color.into())), + background: Some(Background::Color( + cosmic.palette.neutral_5.with_alpha(0.2).into(), + )), first: ItemAppearance { border_radius: Radius::from([rad_m[0], rad_0[1], rad_0[2], rad_m[3]]), ..Default::default() @@ -196,12 +197,12 @@ mod horizontal { } pub fn tab_bar_active(cosmic: &cosmic_theme::Theme) -> ItemStatusAppearance { - let mut neutral_5 = cosmic.palette.neutral_5; - neutral_5.alpha = 0.2; let rad_s = cosmic.corner_radii.radius_s; let rad_0 = cosmic.corner_radii.radius_0; ItemStatusAppearance { - background: Some(Background::Color(neutral_5.into())), + background: Some(Background::Color( + cosmic.palette.neutral_5.with_alpha(0.2).into(), + )), first: ItemAppearance { border_radius: Radius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), border_bottom: Some((4.0, cosmic.accent.base.into())), @@ -240,10 +241,10 @@ pub fn hover( component: &Component, default: &ItemStatusAppearance, ) -> ItemStatusAppearance { - let mut color = cosmic.palette.neutral_8; - color.alpha = 0.2; ItemStatusAppearance { - background: Some(Background::Color(color.into())), + background: Some(Background::Color( + cosmic.palette.neutral_8.with_alpha(0.2).into(), + )), text_color: cosmic.accent.base.into(), ..*default } @@ -253,19 +254,19 @@ mod vertical { use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; use cosmic_theme::Component; use iced_core::{Background, border::Radius}; + use palette::WithAlpha; pub fn selection_active( cosmic: &cosmic_theme::Theme, component: &Component, ) -> ItemStatusAppearance { - let mut color = component.selected_state_color(); - color.alpha = 0.3; - let rad_0 = cosmic.corner_radii.radius_0; let rad_m = cosmic.corner_radii.radius_m; ItemStatusAppearance { - background: Some(Background::Color(color.into())), + background: Some(Background::Color( + component.selected_state_color().with_alpha(0.3).into(), + )), first: ItemAppearance { border_radius: Radius::from([rad_m[0], rad_m[1], rad_0[2], rad_0[3]]), ..Default::default() @@ -283,10 +284,10 @@ mod vertical { } pub fn tab_bar_active(cosmic: &cosmic_theme::Theme) -> ItemStatusAppearance { - let mut neutral_5 = cosmic.palette.neutral_5; - neutral_5.alpha = 0.2; ItemStatusAppearance { - background: Some(Background::Color(neutral_5.into())), + background: Some(Background::Color( + cosmic.palette.neutral_5.with_alpha(0.2).into(), + )), first: ItemAppearance { border_radius: cosmic.corner_radii.radius_m.into(), ..Default::default() diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index 0dba4109..8186a689 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -219,8 +219,6 @@ where #[allow(clippy::trivially_copy_pass_by_ref)] fn container_style(theme: &crate::Theme) -> iced_widget::container::Style { let cosmic_theme = &theme.cosmic(); - let mut neutral_10 = cosmic_theme.palette.neutral_10; - neutral_10.alpha = 0.1; let accent = &cosmic_theme.accent; let corners = &cosmic_theme.corner_radii; let border = if theme.theme_type.is_high_contrast() { From 38dde24f96658ae90f0d654d28b965044196dc40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Vuka=C5=A1in=20Vojinovi=C4=87?= Date: Fri, 20 Jun 2025 01:54:56 +0200 Subject: [PATCH 034/352] chore(applet): add spacing field --- src/applet/mod.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/applet/mod.rs b/src/applet/mod.rs index e0ab993c..ded92cf6 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -21,7 +21,7 @@ use crate::{ }; pub use cosmic_panel_config; use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize}; -use iced_core::{Layout, Padding, Shadow}; +use iced_core::{Padding, Shadow}; use iced_widget::runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity}; use std::{borrow::Cow, num::NonZeroU32, rc::Rc, sync::LazyLock, time::Duration}; @@ -39,6 +39,7 @@ static TOOLTIP_WINDOW_ID: LazyLock = LazyLock::new(window::Id::uniqu pub struct Context { pub size: Size, pub anchor: PanelAnchor, + pub spacing: u32, pub background: CosmicPanelBackground, pub output_name: String, pub panel_type: PanelType, @@ -93,6 +94,10 @@ impl Default for Context { .ok() .and_then(|size| ron::from_str(size.as_str()).ok()) .unwrap_or(PanelAnchor::Top), + spacing: std::env::var("COSMIC_PANEL_SPACING") + .ok() + .and_then(|size| ron::from_str(size.as_str()).ok()) + .unwrap_or(4), background: std::env::var("COSMIC_PANEL_BACKGROUND") .ok() .and_then(|size| ron::from_str(size.as_str()).ok()) From b8eaad2a7e0f853efefef5e4d3f8f88ec9d0297f Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Mon, 21 Jul 2025 13:59:09 -0700 Subject: [PATCH 035/352] feat: add `dbus_connection()` method to `app::Application` trait --- src/app/action.rs | 2 ++ src/app/cosmic.rs | 5 +++++ src/app/mod.rs | 8 ++++++++ src/dbus_activation.rs | 6 ++++++ 4 files changed, 21 insertions(+) diff --git a/src/app/action.rs b/src/app/action.rs index 0f05a6a6..cbdd1a55 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -22,6 +22,8 @@ pub enum Action { Close, /// Closes or shows the context drawer. ContextDrawer(bool), + #[cfg(feature = "single-instance")] + DbusConnection(zbus::Connection), /// Requests to drag the window. Drag, /// Window focus changed diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index a2cdeb87..00ce3ddc 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -761,6 +761,11 @@ impl Cosmic { } } + #[cfg(feature = "single-instance")] + Action::DbusConnection(conn) => { + return self.app.dbus_connection(conn); + } + #[cfg(feature = "xdg-portal")] Action::DesktopSettings(crate::theme::portal::Desktop::ColorScheme(s)) => { use ashpd::desktop::settings::ColorScheme; diff --git a/src/app/mod.rs b/src/app/mod.rs index 35b55f02..1b10b68d 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -437,6 +437,14 @@ where fn dbus_activation(&mut self, msg: crate::dbus_activation::Message) -> Task { Task::none() } + + /// Invoked on connect to dbus session socket used for dbus activation + /// + /// Can be used to expose custom interfaces on the same owned name. + #[cfg(feature = "single-instance")] + fn dbus_connection(&mut self, conn: zbus::Connection) -> Task { + Task::none() + } } /// Methods automatically derived for all types implementing [`Application`]. diff --git a/src/dbus_activation.rs b/src/dbus_activation.rs index 9880a7d0..c8931dd4 100644 --- a/src/dbus_activation.rs +++ b/src/dbus_activation.rs @@ -44,6 +44,12 @@ pub fn subscription() -> Subscription Date: Mon, 21 Jul 2025 10:49:47 -0400 Subject: [PATCH 036/352] chore: theme color updates --- cosmic-theme/src/model/theme.rs | 4 +- src/theme/style/button.rs | 12 ++-- src/theme/style/dropdown.rs | 2 +- src/theme/style/iced.rs | 91 +++++++++++++++++++---------- src/theme/style/segmented_button.rs | 14 ++--- src/widget/color_picker/mod.rs | 8 +-- src/widget/menu/menu_tree.rs | 3 +- src/widget/spin_button.rs | 6 +- 8 files changed, 84 insertions(+), 56 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 6b9d1783..e84417e1 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1277,7 +1277,7 @@ impl ThemeBuilder { let mut component = Component::component( Srgba::new(0.0, 0.0, 0.0, 0.0), accent, - accent, + accent_text.unwrap_or(accent), Srgba::new(0.0, 0.0, 0.0, 0.0), Srgba::new(0.0, 0.0, 0.0, 0.0), is_high_contrast, @@ -1305,7 +1305,7 @@ impl ThemeBuilder { text_button: Component::component( Srgba::new(0.0, 0.0, 0.0, 0.0), accent, - accent, + accent_text.unwrap_or(accent), button_hovered_overlay, button_pressed_overlay, is_high_contrast, diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index 8459908d..1073fe85 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -99,7 +99,7 @@ pub fn appearance( Button::Image => { appearance.background = None; - appearance.text_color = Some(cosmic.accent.base.into()); + appearance.text_color = Some(cosmic.accent_text_color().into()); appearance.icon_color = Some(cosmic.accent.base.into()); corner_radii = &cosmic.corner_radii.radius_s; @@ -119,7 +119,7 @@ pub fn appearance( Button::Link => { appearance.background = None; appearance.icon_color = Some(cosmic.accent.base.into()); - appearance.text_color = Some(cosmic.accent.base.into()); + appearance.text_color = Some(cosmic.accent_text_color().into()); corner_radii = &cosmic.corner_radii.radius_0; } @@ -156,7 +156,7 @@ pub fn appearance( appearance.background = Some(Background::Color(cosmic.primary.component.hover.into())); appearance.icon_color = Some(cosmic.accent.base.into()); - appearance.text_color = Some(cosmic.accent.base.into()); + appearance.text_color = Some(cosmic.accent_text_color().into()); } else { appearance.background = Some(Background::Color(background)); appearance.icon_color = icon; @@ -203,7 +203,7 @@ impl Catalog for crate::Theme { Button::Icon | Button::IconVertical | Button::HeaderBar ) && selected { - Some(self.cosmic().accent_color().into()) + Some(self.cosmic().accent_text_color().into()) } else { Some(component.on.into()) }; @@ -249,7 +249,7 @@ impl Catalog for crate::Theme { Button::Icon | Button::IconVertical | Button::HeaderBar ) && selected { - Some(self.cosmic().accent_color().into()) + Some(self.cosmic().accent_text_color().into()) } else { Some(component.on.into()) }; @@ -270,7 +270,7 @@ impl Catalog for crate::Theme { Button::Icon | Button::IconVertical | Button::HeaderBar ) && selected { - Some(self.cosmic().accent_color().into()) + Some(self.cosmic().accent_text_color().into()) } else { Some(component.on.into()) }; diff --git a/src/theme/style/dropdown.rs b/src/theme/style/dropdown.rs index 02c69c2a..cc89a399 100644 --- a/src/theme/style/dropdown.rs +++ b/src/theme/style/dropdown.rs @@ -21,7 +21,7 @@ impl dropdown::menu::StyleSheet for Theme { hovered_text_color: cosmic.on_bg_color().into(), hovered_background: Background::Color(cosmic.primary.component.hover.into()), - selected_text_color: cosmic.accent.base.into(), + selected_text_color: cosmic.accent_text_color().into(), selected_background: Background::Color(cosmic.primary.component.hover.into()), description_color: cosmic.primary.component.on_disabled.into(), diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index f62e5825..e3da3f98 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -156,8 +156,8 @@ impl Button { Self::Positive => &cosmic.success_button, Self::Destructive => &cosmic.destructive_button, Self::Text => &cosmic.text_button, - Self::Link => &cosmic.accent_button, - Self::LinkActive => &cosmic.accent_button, + Self::Link => &cosmic.link_button, + Self::LinkActive => &cosmic.link_button, Self::Transparent => &TRANSPARENT_COMPONENT, Self::Deactivated => &theme.current_container().component, Self::Card => &theme.current_container().component, @@ -190,6 +190,7 @@ impl iced_checkbox::Catalog for Theme { Checkbox::default() } + #[allow(clippy::too_many_lines)] fn style( &self, class: &Self::Class<'_>, @@ -208,7 +209,7 @@ impl iced_checkbox::Catalog for Theme { background: Background::Color(if is_checked { cosmic.accent.base.into() } else { - cosmic.button.base.into() + cosmic.background.small_widget.into() }), icon_color: cosmic.accent.on.into(), border: Border { @@ -217,7 +218,7 @@ impl iced_checkbox::Catalog for Theme { color: if is_checked { cosmic.accent.base } else { - cosmic.button.border + cosmic.palette.neutral_8 } .into(), }, @@ -251,7 +252,7 @@ impl iced_checkbox::Catalog for Theme { color: if is_checked { cosmic.success.base } else { - cosmic.button.border + cosmic.palette.neutral_8 } .into(), }, @@ -504,7 +505,7 @@ impl iced_container::Catalog for Theme { Container::HeaderBar { focused } => { let (icon_color, text_color) = if *focused { ( - Color::from(cosmic.accent.base), + Color::from(cosmic.accent_text_color()), Color::from(cosmic.background.on), ) } else { @@ -667,9 +668,9 @@ impl slider::Catalog for Theme { ( cosmic.accent_text_color(), if is_dark { - cosmic.palette.neutral_6 + cosmic.palette.neutral_5 } else { - cosmic.palette.neutral_4 + cosmic.palette.neutral_3 }, ) } else { @@ -828,15 +829,14 @@ impl radio::Catalog for Theme { Color::from(theme.accent.base).into() } else { // TODO: this seems to be defined weirdly in FIGMA - Color::from(theme.background.base).into() + Color::from(theme.background.small_widget).into() }, dot_color: theme.accent.on.into(), border_width: 1.0, border_color: if is_selected { Color::from(theme.accent.base) } else { - // TODO: this seems to be defined weirdly in FIGMA - Color::from(theme.palette.neutral_7) + Color::from(theme.palette.neutral_8) }, text_color: None, }, @@ -844,8 +844,7 @@ impl radio::Catalog for Theme { background: if is_selected { Color::from(theme.accent.base).into() } else { - // TODO: this seems to be defined weirdly in FIGMA - Color::from(theme.palette.neutral_10.with_alpha(0.1)).into() + Color::from(theme.background.small_widget.with_alpha(0.2)).into() }, dot_color: theme.accent.on.into(), border_width: 1.0, @@ -878,7 +877,11 @@ impl toggler::Catalog for Theme { background: if matches!(status, toggler::Status::Active { is_toggled: true }) { cosmic.accent.base.into() } else { - cosmic.palette.neutral_5.into() + if cosmic.is_dark { + cosmic.palette.neutral_6.into() + } else { + cosmic.palette.neutral_5.into() + } }, foreground: cosmic.palette.neutral_2.into(), border_radius: cosmic.radius_xl().into(), @@ -900,7 +903,14 @@ impl toggler::Catalog for Theme { background: if is_active { over(neutral_10, cosmic.accent_color()) } else { - over(neutral_10, cosmic.palette.neutral_5) + over( + neutral_10, + if cosmic.is_dark { + cosmic.palette.neutral_6 + } else { + cosmic.palette.neutral_5 + }, + ) } .into(), ..active @@ -1092,7 +1102,8 @@ impl scrollable::Catalog for Theme { match status { scrollable::Status::Active => { let cosmic = self.cosmic(); - let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); + let mut neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); + let mut neutral_6 = cosmic.palette.neutral_6.with_alpha(0.7); let mut a = scrollable::Style { container: iced_container::transparent(self), vertical_rail: scrollable::Rail { @@ -1102,7 +1113,11 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - color: neutral_5.into(), + color: if cosmic.is_dark { + neutral_6.into() + } else { + neutral_5.into() + }, border: Border { radius: cosmic.corner_radii.radius_s.into(), ..Default::default() @@ -1116,7 +1131,11 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - color: neutral_5.into(), + color: if cosmic.is_dark { + neutral_6.into() + } else { + neutral_5.into() + }, border: Border { radius: cosmic.corner_radii.radius_s.into(), ..Default::default() @@ -1137,9 +1156,9 @@ impl scrollable::Catalog for Theme { // TODO handle vertical / horizontal scrollable::Status::Hovered { .. } | scrollable::Status::Dragged { .. } => { let cosmic = self.cosmic(); - let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); - - // TODO hover + let mut neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); + let mut neutral_6 = cosmic.palette.neutral_6.with_alpha(0.7); + let mut neutral_3 = cosmic.palette.neutral_3; // if is_mouse_over_scrollbar { // let hover_overlay = cosmic.palette.neutral_0.with_alpha(0.2); @@ -1154,7 +1173,11 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - color: neutral_5.into(), + color: if cosmic.is_dark { + neutral_6.into() + } else { + neutral_5.into() + }, border: Border { radius: cosmic.corner_radii.radius_s.into(), ..Default::default() @@ -1168,7 +1191,11 @@ impl scrollable::Catalog for Theme { }, background: None, scroller: scrollable::Scroller { - color: neutral_5.into(), + color: if cosmic.is_dark { + neutral_6.into() + } else { + neutral_5.into() + }, border: Border { radius: cosmic.corner_radii.radius_s.into(), ..Default::default() @@ -1179,9 +1206,13 @@ impl scrollable::Catalog for Theme { }; if matches!(class, Scrollable::Permanent) { - let neutral_3 = cosmic.palette.neutral_3.with_alpha(0.7); - a.horizontal_rail.background = Some(Background::Color(neutral_3.into())); - a.vertical_rail.background = Some(Background::Color(neutral_3.into())); + let small_widget_container = + cosmic.background.small_widget.clone().with_alpha(0.7); + + a.horizontal_rail.background = + Some(Background::Color(small_widget_container.into())); + a.vertical_rail.background = + Some(Background::Color(small_widget_container.into())); } a @@ -1250,7 +1281,7 @@ impl iced_widget::text::Catalog for Theme { fn style(&self, class: &Self::Class<'_>) -> iced_widget::text::Style { match class { Text::Accent => iced_widget::text::Style { - color: Some(self.cosmic().accent.base.into()), + color: Some(self.cosmic().accent_text_color().into()), }, Text::Default => iced_widget::text::Style { color: None }, Text::Color(c) => iced_widget::text::Style { color: Some(*c) }, @@ -1278,7 +1309,7 @@ impl text_input::Catalog for Theme { fn style(&self, class: &Self::Class<'_>, status: text_input::Status) -> text_input::Style { let palette = self.cosmic(); - let bg = palette.palette.neutral_7.with_alpha(0.25); + let mut bg = palette.background.small_widget.with_alpha(0.25); let neutral_9 = palette.palette.neutral_9; let value = neutral_9.into(); @@ -1314,7 +1345,7 @@ impl text_input::Catalog for Theme { match status { text_input::Status::Active => appearance, text_input::Status::Hovered => { - let bg = palette.palette.neutral_7.with_alpha(0.25); + let mut bg = palette.background.small_widget.with_alpha(0.25); match class { TextInput::Default => text_input::Style { @@ -1343,7 +1374,7 @@ impl text_input::Catalog for Theme { } } text_input::Status::Focused => { - let bg = palette.palette.neutral_7.with_alpha(0.25); + let mut bg = palette.background.small_widget.with_alpha(0.25); match class { TextInput::Default => text_input::Style { diff --git a/src/theme/style/segmented_button.rs b/src/theme/style/segmented_button.rs index c4d81151..ff759715 100644 --- a/src/theme/style/segmented_button.rs +++ b/src/theme/style/segmented_button.rs @@ -192,7 +192,7 @@ mod horizontal { border_radius: Radius::from([rad_0[0], rad_m[1], rad_m[2], rad_0[3]]), ..Default::default() }, - text_color: cosmic.accent.base.into(), + text_color: cosmic.accent_text_color().into(), } } @@ -218,7 +218,7 @@ mod horizontal { border_bottom: Some((4.0, cosmic.accent.base.into())), ..Default::default() }, - text_color: cosmic.accent.base.into(), + text_color: cosmic.accent_text_color().into(), } } } @@ -231,7 +231,7 @@ pub fn focus( let color = container.small_widget; ItemStatusAppearance { background: Some(Background::Color(color.into())), - text_color: cosmic.accent.base.into(), + text_color: cosmic.accent_text_color().into(), ..*default } } @@ -242,9 +242,7 @@ pub fn hover( default: &ItemStatusAppearance, ) -> ItemStatusAppearance { ItemStatusAppearance { - background: Some(Background::Color( - cosmic.palette.neutral_8.with_alpha(0.2).into(), - )), + background: Some(Background::Color(component.hover.with_alpha(0.2).into())), text_color: cosmic.accent.base.into(), ..*default } @@ -279,7 +277,7 @@ mod vertical { border_radius: Radius::from([rad_0[0], rad_0[1], rad_m[2], rad_m[3]]), ..Default::default() }, - text_color: cosmic.accent.base.into(), + text_color: cosmic.accent_text_color().into(), } } @@ -300,7 +298,7 @@ mod vertical { border_radius: cosmic.corner_radii.radius_m.into(), ..Default::default() }, - text_color: cosmic.accent.base.into(), + text_color: cosmic.accent_text_color().into(), } } } diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 7d088515..789969ac 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -940,7 +940,7 @@ pub fn color_button<'a, Message: Clone + 'static>( background: color.map(Background::from).or(standard.background), border_radius: cosmic.radius_xs().into(), border_width: 1.0, - border_color: cosmic.on_bg_color().into(), + border_color: cosmic.palette.neutral_8.into(), outline_width, outline_color, icon_color: None, @@ -957,7 +957,7 @@ pub fn color_button<'a, Message: Clone + 'static>( background: color.map(Background::from).or(standard.background), border_radius: cosmic.radius_xs().into(), border_width: 1.0, - border_color: cosmic.on_bg_color().into(), + border_color: cosmic.palette.neutral_8.into(), outline_width: 0.0, outline_color: Color::TRANSPARENT, icon_color: None, @@ -980,7 +980,7 @@ pub fn color_button<'a, Message: Clone + 'static>( background: color.map(Background::from).or(standard.background), border_radius: cosmic.radius_xs().into(), border_width: 1.0, - border_color: cosmic.on_bg_color().into(), + border_color: cosmic.palette.neutral_8.into(), outline_width, outline_color, icon_color: None, @@ -1003,7 +1003,7 @@ pub fn color_button<'a, Message: Clone + 'static>( background: color.map(Background::from).or(standard.background), border_radius: cosmic.radius_xs().into(), border_width: 1.0, - border_color: cosmic.on_bg_color().into(), + border_color: cosmic.palette.neutral_8.into(), outline_width, outline_color, icon_color: None, diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 8cf890f2..67f999f7 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -262,7 +262,6 @@ pub fn menu_items< items.insert(1, widget::Space::with_width(spacing.space_xxs).into()); } - // dbg!("button with action...", action.message()); let menu_button = menu_button(items).on_press(action.message()); trees.push(MenuTree::::from(Element::from(menu_button))); @@ -296,7 +295,7 @@ pub fn menu_items< .icon() .class(theme::Svg::Custom(Rc::new(|theme| { iced_widget::svg::Style { - color: Some(theme.cosmic().accent_color().into()), + color: Some(theme.cosmic().accent_text_color().into()), } }))) .width(Length::Fixed(16.0)) diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index 8186a689..eba4641a 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -221,8 +221,8 @@ fn container_style(theme: &crate::Theme) -> iced_widget::container::Style { let cosmic_theme = &theme.cosmic(); let accent = &cosmic_theme.accent; let corners = &cosmic_theme.corner_radii; + let current_container = theme.current_container(); let border = if theme.theme_type.is_high_contrast() { - let current_container = theme.current_container(); Border { radius: corners.radius_s.into(), width: 1., @@ -237,8 +237,8 @@ fn container_style(theme: &crate::Theme) -> iced_widget::container::Style { }; iced_widget::container::Style { - icon_color: Some(accent.base.into()), - text_color: Some(cosmic_theme.palette.neutral_10.into()), + icon_color: Some(current_container.on.into()), + text_color: Some(current_container.on.into()), background: None, border, shadow: Shadow::default(), From 8ebd06bed0f6d162ae9297fd0bbf088653e6872d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 23 Jul 2025 17:42:15 -0400 Subject: [PATCH 037/352] chore: more style updates --- src/theme/style/button.rs | 2 +- src/theme/style/iced.rs | 2 +- src/theme/style/segmented_button.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index 1073fe85..0575ce67 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -118,7 +118,7 @@ pub fn appearance( Button::Link => { appearance.background = None; - appearance.icon_color = Some(cosmic.accent.base.into()); + appearance.icon_color = Some(cosmic.accent_text_color().into()); appearance.text_color = Some(cosmic.accent_text_color().into()); corner_radii = &cosmic.corner_radii.radius_0; } diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index e3da3f98..02818d81 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -766,7 +766,7 @@ impl menu::Catalog for Theme { radius: cosmic.corner_radii.radius_m.into(), ..Default::default() }, - selected_text_color: cosmic.accent.base.into(), + selected_text_color: cosmic.accent_text_color().into(), selected_background: Background::Color(cosmic.background.component.hover.into()), } } diff --git a/src/theme/style/segmented_button.rs b/src/theme/style/segmented_button.rs index ff759715..398f6fb2 100644 --- a/src/theme/style/segmented_button.rs +++ b/src/theme/style/segmented_button.rs @@ -243,7 +243,7 @@ pub fn hover( ) -> ItemStatusAppearance { ItemStatusAppearance { background: Some(Background::Color(component.hover.with_alpha(0.2).into())), - text_color: cosmic.accent.base.into(), + text_color: cosmic.accent_text_color().into(), ..*default } } From 5aa025af7dc9af4194f84ac08c7ac484b0d9d556 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Tue, 22 Jul 2025 15:45:22 -0600 Subject: [PATCH 038/352] context-menu: allow borrowed content --- src/widget/context_menu.rs | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index b1014cf4..d0e1125a 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -18,11 +18,11 @@ use std::collections::HashSet; use std::sync::Arc; /// A context menu is a menu in a graphical user interface that appears upon user interaction, such as a right-click mouse operation. -pub fn context_menu( - content: impl Into> + 'static, +pub fn context_menu<'a, Message: 'static + Clone>( + content: impl Into>, // on_context: Message, context_menu: Option>>, -) -> ContextMenu<'static, Message> { +) -> ContextMenu<'a, Message> { let mut this = ContextMenu { content: content.into(), context_menu: context_menu.map(|menus| { @@ -539,10 +539,8 @@ impl Widget } } -impl<'a, Message: Clone + 'static> From> - for crate::Element<'static, Message> -{ - fn from(widget: ContextMenu<'static, Message>) -> Self { +impl<'a, Message: Clone + 'static> From> for crate::Element<'a, Message> { + fn from(widget: ContextMenu<'a, Message>) -> Self { Self::new(widget) } } From 3c13669865aadedab4ae18eb40c684e0355eed10 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 22 Jul 2025 19:33:07 -0400 Subject: [PATCH 039/352] fix: close context menu on escape press --- src/widget/context_menu.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index d0e1125a..59f78128 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -11,7 +11,7 @@ use crate::widget::menu::{ }; use derive_setters::Setters; use iced::touch::Finger; -use iced::{Event, Vector, window}; +use iced::{Event, Vector, keyboard, window}; use iced_core::widget::{Tree, Widget, tree}; use iced_core::{Length, Point, Size, event, mouse, touch}; use std::collections::HashSet; @@ -31,6 +31,7 @@ pub fn context_menu<'a, Message: 'static + Clone>( menus, )] }), + close_on_escape: true, window_id: window::Id::RESERVED, on_surface_action: None, }; @@ -51,6 +52,7 @@ pub struct ContextMenu<'a, Message> { #[setters(skip)] context_menu: Option>>, pub window_id: window::Id, + pub close_on_escape: bool, #[setters(skip)] pub(crate) on_surface_action: Option Message + Send + Sync + 'static>>, @@ -344,7 +346,31 @@ impl Widget } state.open }); + if matches!( + event, + Event::Keyboard(keyboard::Event::KeyPressed { + key: keyboard::Key::Named(keyboard::key::Named::Escape), + .. + }) + ) { + state.menu_bar_state.inner.with_data_mut(|state| { + state.menu_states.clear(); + state.active_root.clear(); + 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 cursor.is_over(bounds) { let fingers_pressed = state.fingers_pressed.len(); From c40eb8761179ec680a5b0772eb73b6b199e62ddf Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 23 Jul 2025 10:22:31 -0400 Subject: [PATCH 040/352] fix(context-menu): close menu if pressed out of bounds and open --- src/widget/context_menu.rs | 103 +++++++++++-------------------------- 1 file changed, 29 insertions(+), 74 deletions(-) diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 59f78128..d9dc529a 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -346,14 +346,21 @@ impl Widget } state.open }); - if matches!( - event, + let mut was_open = false; + if matches!(event, Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(keyboard::key::Named::Escape), .. }) - ) { + | Event::Mouse(mouse::Event::ButtonPressed( + 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| { + was_open = true; state.menu_states.clear(); state.active_root.clear(); state.open = false; @@ -371,7 +378,8 @@ impl Widget } }); } - if cursor.is_over(bounds) { + + if !was_open && cursor.is_over(bounds) { let fingers_pressed = state.fingers_pressed.len(); match event { @@ -383,36 +391,12 @@ impl Widget state.fingers_pressed.remove(&id); } - Event::Window(window::Event::Focused) => { - #[cfg(all( - feature = "wayland", - feature = "winit", - feature = "surface-message" - ))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { - state.menu_bar_state.inner.with_data_mut(|state| { - if let Some(id) = state.popup_id.remove(&self.window_id) { - state.menu_states.clear(); - state.active_root.clear(); - state.open = false; - - { - let surface_action = self.on_surface_action.as_ref().unwrap(); - shell.publish(surface_action( - crate::surface::action::destroy_popup(id), - )); - } - state.view_cursor = cursor; - } - }); - } - } - _ => (), } // Present a context menu on a right click event. - if self.context_menu.is_some() + if !was_open + && self.context_menu.is_some() && (right_button_released(&event) || (touch_lifted(&event) && fingers_pressed == 2)) { state.context_cursor = cursor.position().unwrap_or_default(); @@ -427,64 +411,35 @@ impl Widget } return event::Status::Captured; - } else if right_button_released(&event) + } else if !was_open && right_button_released(&event) || (touch_lifted(&event)) || left_button_released(&event) { - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { - state.menu_bar_state.inner.with_data_mut(|state| { - if let Some(id) = state.popup_id.remove(&self.window_id) { - state.menu_states.clear(); - state.active_root.clear(); - state.open = false; + state.menu_bar_state.inner.with_data_mut(|state| { + was_open = true; + state.menu_states.clear(); + state.active_root.clear(); + state.open = false; - { - let surface_action = self.on_surface_action.as_ref().unwrap(); - - shell.publish(surface_action( - crate::surface::action::destroy_popup(id), - )); - } - state.view_cursor = cursor; - } - }); - } - } - } else if open { - match event { - Event::Mouse(mouse::Event::ButtonReleased( - mouse::Button::Right | mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerLifted { .. }) => { #[cfg(all( feature = "wayland", feature = "winit", feature = "surface-message" ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { - state.menu_bar_state.inner.with_data_mut(|state| { - if let Some(id) = state.popup_id.remove(&self.window_id) { - state.menu_states.clear(); - state.active_root.clear(); - state.open = false; - - { - 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 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; + } } - } - _ => (), + }); } } - self.content.as_widget_mut().on_event( &mut tree.children[0], event, From 1b988ed1e9d2b4346b32c451bac7880d28257dc2 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 24 Jul 2025 14:31:33 -0400 Subject: [PATCH 041/352] fix(theme change): make sure that all theme variables are in sync after a change --- src/app/cosmic.rs | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 00ce3ddc..c8a8dfeb 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -690,10 +690,11 @@ impl Cosmic { let mut cmds = vec![self.app.system_theme_mode_update(&keys, &mode)]; let core = self.app.core_mut(); - let prev_is_dark = core.system_is_dark(); core.system_theme_mode = mode; let is_dark = core.system_is_dark(); - let changed = prev_is_dark != is_dark; + let changed = core.system_theme_mode.is_dark != is_dark + || core.portal_is_dark != Some(is_dark) + || core.system_theme.cosmic().is_dark != is_dark; if changed { core.theme_sub_counter += 1; let mut new_theme = if is_dark { @@ -784,10 +785,13 @@ impl Cosmic { ColorScheme::PreferLight => Some(false), }; let core = self.app.core_mut(); - let prev_is_dark = core.system_is_dark(); + core.portal_is_dark = is_dark; let is_dark = core.system_is_dark(); - let changed = prev_is_dark != is_dark; + let changed = core.system_theme_mode.is_dark != is_dark + || core.portal_is_dark != Some(is_dark) + || core.system_theme.cosmic().is_dark != is_dark; + if changed { core.theme_sub_counter += 1; let new_theme = if is_dark { From 2099dc45cb44102f96f0dd4eefb97a0c74d18125 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 24 Jul 2025 15:34:56 -0600 Subject: [PATCH 042/352] fix(header_bar): increase title portion based on maximum left or right portion --- src/widget/header_bar.rs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 8eec9ef4..3763ae32 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -7,7 +7,7 @@ use apply::Apply; use derive_setters::Setters; use iced::Length; use iced_core::{Vector, Widget, widget::tree}; -use std::borrow::Cow; +use std::{borrow::Cow, cmp}; #[must_use] pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { @@ -366,6 +366,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { } 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. @@ -389,7 +390,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .into(), ) } else if !self.title.is_empty() && !self.is_condensed { - Some(self.title_widget()) + Some(self.title_widget(title_portion)) } else { None }) @@ -431,13 +432,13 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { widget.into() } - fn title_widget(&mut self) -> Element<'a, Message> { + 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) .apply(widget::container) - .center(Length::Fill) + .center(Length::FillPortion(title_portion)) .into() } From 5e136f94994140548b4feee759c811284988f076 Mon Sep 17 00:00:00 2001 From: wiiznokes <78230769+wiiznokes@users.noreply.github.com> Date: Mon, 28 Jul 2025 16:33:22 +0200 Subject: [PATCH 043/352] fix!(windows): remove `desktop` dependency for the `about` feature BREAKING CHANGE: Icon must be provided as a handle instead of a string. --- Cargo.toml | 2 +- src/widget/about.rs | 8 ++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 132aac8c..017d84d2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ default = ["multi-window", "a11y"] # Accessibility support a11y = ["iced/a11y", "iced_accessibility"] # Enable about widget -about = ["desktop", "dep:license"] +about = ["dep:license"] # Builds support for animated images animated-image = ["dep:async-fs", "image/gif", "tokio?/io-util", "tokio?/fs"] # XXX autosize should not be used on winit windows unless dialogs diff --git a/src/widget/about.rs b/src/widget/about.rs index 47a9baa1..6590bb9d 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -1,7 +1,6 @@ use { crate::{ Element, - desktop::{IconSourceExt, fde}, iced::{Alignment, Length}, widget::{self, horizontal_space}, }, @@ -15,7 +14,7 @@ pub struct About { /// The application's name. name: Option, /// The application's icon name. - icon: Option, + icon: Option, /// The application's version. version: Option, /// Name of the application's author. @@ -138,10 +137,7 @@ pub fn about<'a, Message: Clone + 'static>( }; let application_name = about.name.as_ref().map(widget::text::title3); - let application_icon = about - .icon - .as_ref() - .map(|icon| fde::IconSource::Name(icon.clone()).as_cosmic_icon()); + let application_icon = about.icon.as_ref().map(|i| i.clone().icon()); let author = about.author.as_ref().map(widget::text::body); let version = about.version.as_ref().map(widget::button::standard); let links_section = section(&about.links, "Links"); From 05874e8ea252be0e6115c268aef18a19019842f4 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 29 Jul 2025 15:45:52 -0400 Subject: [PATCH 044/352] fix: theme updates --- cosmic-theme/src/model/theme.rs | 4 +- src/theme/style/iced.rs | 228 +++++++++++++++------------- src/theme/style/segmented_button.rs | 4 +- src/theme/style/text_input.rs | 10 +- 4 files changed, 132 insertions(+), 114 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index e84417e1..aad71228 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -957,7 +957,7 @@ impl ThemeBuilder { /// build the theme pub fn build(self) -> Theme { let Self { - mut palette, + palette, spacing, corner_radii, neutral_tint, @@ -1063,6 +1063,7 @@ impl ThemeBuilder { get_surface_color(bg_index, 5, &step_array, is_dark, &control_steps_array[1]) }; + let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); let base_index: usize = color_index(container_bg, step_array.len()); let component_base = get_surface_color(base_index, 6, &step_array, is_dark, &control_steps_array[3]); @@ -1185,6 +1186,7 @@ impl ThemeBuilder { get_surface_color(bg_index, 10, &step_array, is_dark, &control_steps_array[2]) }; + let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); let base_index = color_index(container_bg, step_array.len()); let secondary_component = get_surface_color(base_index, 3, &step_array, is_dark, &control_steps_array[4]); diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 02818d81..764c1654 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -209,7 +209,7 @@ impl iced_checkbox::Catalog for Theme { background: Background::Color(if is_checked { cosmic.accent.base.into() } else { - cosmic.background.small_widget.into() + self.current_container().small_widget.into() }), icon_color: cosmic.accent.on.into(), border: Border { @@ -229,13 +229,13 @@ impl iced_checkbox::Catalog for Theme { background: Background::Color(if is_checked { cosmic.background.component.base.into() } else { - cosmic.background.base.into() + self.current_container().small_widget.into() }), icon_color: cosmic.background.on.into(), border: Border { radius: corners.radius_xs.into(), width: if is_checked { 0.0 } else { 1.0 }, - color: cosmic.button.border.into(), + color: cosmic.palette.neutral_8.into(), }, text_color: None, }, @@ -243,7 +243,7 @@ impl iced_checkbox::Catalog for Theme { background: Background::Color(if is_checked { cosmic.success.base.into() } else { - cosmic.button.base.into() + self.current_container().small_widget.into() }), icon_color: cosmic.success.on.into(), border: Border { @@ -262,7 +262,7 @@ impl iced_checkbox::Catalog for Theme { background: Background::Color(if is_checked { cosmic.destructive.base.into() } else { - cosmic.button.base.into() + self.current_container().small_widget.into() }), icon_color: cosmic.destructive.on.into(), border: Border { @@ -271,7 +271,7 @@ impl iced_checkbox::Catalog for Theme { color: if is_checked { cosmic.destructive.base } else { - cosmic.button.border + cosmic.palette.neutral_8 } .into(), }, @@ -294,84 +294,89 @@ impl iced_checkbox::Catalog for Theme { } active } - iced_checkbox::Status::Hovered { is_checked } => match class { - Checkbox::Primary => iced_checkbox::Style { - background: Background::Color(if is_checked { - cosmic.accent.base.into() - } else { - cosmic.button.base.into() - }), - icon_color: cosmic.accent.on.into(), - border: Border { - radius: corners.radius_xs.into(), - width: if is_checked { 0.0 } else { 1.0 }, - color: if is_checked { - cosmic.accent.base + iced_checkbox::Status::Hovered { is_checked } => { + let cur_container = self.current_container().small_widget; + // TODO: this should probably be done with a custom widget instead, or the theme needs more small widget variables. + let hovered_bg = over(cosmic.palette.neutral_0.with_alpha(0.1), cur_container); + match class { + Checkbox::Primary => iced_checkbox::Style { + background: Background::Color(if is_checked { + cosmic.accent.hover_state_color().into() } else { - cosmic.button.border - } - .into(), + hovered_bg.into() + }), + icon_color: cosmic.accent.on.into(), + border: Border { + radius: corners.radius_xs.into(), + width: if is_checked { 0.0 } else { 1.0 }, + color: if is_checked { + cosmic.accent.base + } else { + cosmic.palette.neutral_8 + } + .into(), + }, + text_color: None, }, - text_color: None, - }, - Checkbox::Secondary => iced_checkbox::Style { - background: Background::Color(if is_checked { - self.current_container().base.into() - } else { - cosmic.button.base.into() - }), - icon_color: self.current_container().on.into(), - border: Border { - radius: corners.radius_xs.into(), - width: if is_checked { 0.0 } else { 1.0 }, - color: if is_checked { - self.current_container().base + Checkbox::Secondary => iced_checkbox::Style { + background: Background::Color(if is_checked { + self.current_container().component.hover.into() } else { - cosmic.button.border - } - .into(), + hovered_bg.into() + }), + icon_color: self.current_container().on.into(), + border: Border { + radius: corners.radius_xs.into(), + width: if is_checked { 0.0 } else { 1.0 }, + color: if is_checked { + self.current_container().base + } else { + cosmic.palette.neutral_8 + } + .into(), + }, + text_color: None, }, - text_color: None, - }, - Checkbox::Success => iced_checkbox::Style { - background: Background::Color(if is_checked { - cosmic.success.base.into() - } else { - cosmic.button.base.into() - }), - icon_color: cosmic.success.on.into(), - border: Border { - radius: corners.radius_xs.into(), - width: if is_checked { 0.0 } else { 1.0 }, - color: if is_checked { - cosmic.success.base + Checkbox::Success => iced_checkbox::Style { + background: Background::Color(if is_checked { + cosmic.success.hover.into() } else { - cosmic.button.border - } - .into(), + hovered_bg.into() + }), + icon_color: cosmic.success.on.into(), + border: Border { + radius: corners.radius_xs.into(), + width: if is_checked { 0.0 } else { 1.0 }, + color: if is_checked { + cosmic.success.base + } else { + cosmic.palette.neutral_8 + } + .into(), + }, + text_color: None, }, - text_color: None, - }, - Checkbox::Danger => iced_checkbox::Style { - background: Background::Color(if is_checked { - cosmic.destructive.base.into() - } else { - cosmic.button.base.into() - }), - icon_color: cosmic.destructive.on.into(), - border: Border { - radius: corners.radius_xs.into(), - width: if is_checked { 0.0 } else { 1.0 }, - color: if is_checked { - cosmic.destructive.base + Checkbox::Danger => iced_checkbox::Style { + background: Background::Color(if is_checked { + cosmic.destructive.hover.into() } else { - cosmic.button.border - } - .into(), + hovered_bg.into() + }), + icon_color: cosmic.destructive.on.into(), + border: Border { + radius: corners.radius_xs.into(), + width: if is_checked { 0.0 } else { 1.0 }, + color: if is_checked { + cosmic.destructive.base + } else { + cosmic.palette.neutral_8 + } + .into(), + }, + text_color: None, }, - text_color: None, - }, - }, + } + } } } } @@ -821,6 +826,7 @@ impl radio::Catalog for Theme { fn default<'a>() -> Self::Class<'a> {} fn style(&self, class: &Self::Class<'_>, status: radio::Status) -> radio::Style { + let cur_container = self.current_container(); let theme = self.cosmic(); match status { @@ -829,7 +835,7 @@ impl radio::Catalog for Theme { Color::from(theme.accent.base).into() } else { // TODO: this seems to be defined weirdly in FIGMA - Color::from(theme.background.small_widget).into() + Color::from(cur_container.small_widget).into() }, dot_color: theme.accent.on.into(), border_width: 1.0, @@ -840,22 +846,26 @@ impl radio::Catalog for Theme { }, text_color: None, }, - radio::Status::Hovered { is_selected } => radio::Style { - background: if is_selected { - Color::from(theme.accent.base).into() + radio::Status::Hovered { is_selected } => { + let bg = if is_selected { + theme.accent.base } else { - Color::from(theme.background.small_widget.with_alpha(0.2)).into() - }, - dot_color: theme.accent.on.into(), - border_width: 1.0, - border_color: if is_selected { - Color::from(theme.accent.base) - } else { - // TODO: this seems to be defined weirdly in FIGMA - Color::from(theme.palette.neutral_7) - }, - text_color: None, - }, + self.current_container().small_widget + }; + // TODO: this should probably be done with a custom widget instead, or the theme needs more small widget variables. + let hovered_bg = Color::from(over(theme.palette.neutral_0.with_alpha(0.1), bg)); + radio::Style { + background: hovered_bg.into(), + dot_color: theme.accent.on.into(), + border_width: 1.0, + border_color: if is_selected { + Color::from(theme.accent.base) + } else { + Color::from(theme.palette.neutral_8) + }, + text_color: None, + } + } } } } @@ -1102,8 +1112,8 @@ impl scrollable::Catalog for Theme { match status { scrollable::Status::Active => { let cosmic = self.cosmic(); - let mut neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); - let mut neutral_6 = cosmic.palette.neutral_6.with_alpha(0.7); + let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); + let neutral_6 = cosmic.palette.neutral_6.with_alpha(0.7); let mut a = scrollable::Style { container: iced_container::transparent(self), vertical_rail: scrollable::Rail { @@ -1144,11 +1154,17 @@ impl scrollable::Catalog for Theme { }, gap: None, }; + let small_widget_container = self + .current_container() + .small_widget + .clone() + .with_alpha(0.7); if matches!(class, Scrollable::Permanent) { - let neutral_3 = cosmic.palette.neutral_3.with_alpha(0.7); - a.horizontal_rail.background = Some(Background::Color(neutral_3.into())); - a.vertical_rail.background = Some(Background::Color(neutral_3.into())); + a.horizontal_rail.background = + Some(Background::Color(small_widget_container.into())); + a.vertical_rail.background = + Some(Background::Color(small_widget_container.into())); } a @@ -1156,9 +1172,8 @@ impl scrollable::Catalog for Theme { // TODO handle vertical / horizontal scrollable::Status::Hovered { .. } | scrollable::Status::Dragged { .. } => { let cosmic = self.cosmic(); - let mut neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); - let mut neutral_6 = cosmic.palette.neutral_6.with_alpha(0.7); - let mut neutral_3 = cosmic.palette.neutral_3; + let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); + let neutral_6 = cosmic.palette.neutral_6.with_alpha(0.7); // if is_mouse_over_scrollbar { // let hover_overlay = cosmic.palette.neutral_0.with_alpha(0.2); @@ -1206,8 +1221,11 @@ impl scrollable::Catalog for Theme { }; if matches!(class, Scrollable::Permanent) { - let small_widget_container = - cosmic.background.small_widget.clone().with_alpha(0.7); + let small_widget_container = self + .current_container() + .small_widget + .clone() + .with_alpha(0.7); a.horizontal_rail.background = Some(Background::Color(small_widget_container.into())); @@ -1309,7 +1327,7 @@ impl text_input::Catalog for Theme { fn style(&self, class: &Self::Class<'_>, status: text_input::Status) -> text_input::Style { let palette = self.cosmic(); - let mut bg = palette.background.small_widget.with_alpha(0.25); + let bg = self.current_container().small_widget.with_alpha(0.25); let neutral_9 = palette.palette.neutral_9; let value = neutral_9.into(); @@ -1345,7 +1363,7 @@ impl text_input::Catalog for Theme { match status { text_input::Status::Active => appearance, text_input::Status::Hovered => { - let mut bg = palette.background.small_widget.with_alpha(0.25); + let bg = self.current_container().small_widget.with_alpha(0.25); match class { TextInput::Default => text_input::Style { @@ -1374,7 +1392,7 @@ impl text_input::Catalog for Theme { } } text_input::Status::Focused => { - let mut bg = palette.background.small_widget.with_alpha(0.25); + let bg = self.current_container().small_widget.with_alpha(0.25); match class { TextInput::Default => text_input::Style { diff --git a/src/theme/style/segmented_button.rs b/src/theme/style/segmented_button.rs index 398f6fb2..4f7b4a4f 100644 --- a/src/theme/style/segmented_button.rs +++ b/src/theme/style/segmented_button.rs @@ -177,9 +177,7 @@ mod horizontal { let rad_0 = cosmic.corner_radii.radius_0; ItemStatusAppearance { - background: Some(Background::Color( - cosmic.palette.neutral_5.with_alpha(0.2).into(), - )), + background: Some(Background::Color(component.selected_state_color().into())), first: ItemAppearance { border_radius: Radius::from([rad_m[0], rad_0[1], rad_0[2], rad_m[3]]), ..Default::default() diff --git a/src/theme/style/text_input.rs b/src/theme/style/text_input.rs index c809961a..8085a48d 100644 --- a/src/theme/style/text_input.rs +++ b/src/theme/style/text_input.rs @@ -6,6 +6,7 @@ use crate::ext::ColorExt; use crate::widget::text_input::{Appearance, StyleSheet}; use iced_core::Color; +use palette::WithAlpha; #[derive(Default)] pub enum TextInput { @@ -31,8 +32,7 @@ impl StyleSheet for crate::Theme { let palette = self.cosmic(); let container = self.current_container(); - let mut background: Color = container.component.base.into(); - background.a = 0.25; + let background: Color = container.small_widget.with_alpha(0.25).into(); let corner = palette.corner_radii; let label_color = palette.palette.neutral_9; @@ -125,7 +125,7 @@ impl StyleSheet for crate::Theme { let palette = self.cosmic(); let container = self.current_container(); - let mut background: Color = container.component.base.into(); + let mut background: Color = container.small_widget.into(); background.a = 0.25; let corner = palette.corner_radii; @@ -188,7 +188,7 @@ impl StyleSheet for crate::Theme { let palette = self.cosmic(); let container = self.current_container(); - let mut background: Color = container.component.base.into(); + let mut background: Color = container.small_widget.into(); background.a = 0.25; let corner = palette.corner_radii; @@ -283,7 +283,7 @@ impl StyleSheet for crate::Theme { let palette = self.cosmic(); let container = self.current_container(); - let mut background: Color = container.component.base.into(); + let mut background: Color = container.small_widget.into(); background.a = 0.25; let corner = palette.corner_radii; From b58d864e85b3b842132e9be1b6445ad21c1f0258 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 31 Jul 2025 14:23:06 -0400 Subject: [PATCH 045/352] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 9312d3c2..13134181 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 9312d3c29b6308d714725a20342375a6145359d9 +Subproject commit 13134181f8d5cfeaee4fb52172e12985b06af1cf From 562e88587207368969e7bcd43bce3ccf81aee8d9 Mon Sep 17 00:00:00 2001 From: Friedrich <50049702+FriedrichGaming@users.noreply.github.com> Date: Wed, 6 Aug 2025 01:07:34 +0200 Subject: [PATCH 046/352] Make ashpd optional for async-std feature --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 017d84d2..87ad0867 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -91,7 +91,7 @@ markdown = ["iced/markdown"] highlighter = ["iced/highlighter"] async-std = [ "dep:async-std", - "ashpd/async-std", + "ashpd?/async-std", "rfd?/async-std", "zbus?/async-io", "iced/async-std", From 8badf733833d0a14de5ab7553da5077152931321 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 12 Aug 2025 17:15:18 +0200 Subject: [PATCH 047/352] improv(segmented_button): nav bar, tab, and segmented control theme improvements --- src/theme/style/segmented_button.rs | 342 ++++++++++++---------- src/widget/nav_bar.rs | 2 +- src/widget/segmented_button/horizontal.rs | 2 + src/widget/segmented_button/style.rs | 17 +- src/widget/segmented_button/vertical.rs | 2 + src/widget/segmented_button/widget.rs | 166 +++++------ src/widget/segmented_control.rs | 2 - src/widget/tab_bar.rs | 2 - 8 files changed, 279 insertions(+), 256 deletions(-) diff --git a/src/theme/style/segmented_button.rs b/src/theme/style/segmented_button.rs index 4f7b4a4f..3f5d92db 100644 --- a/src/theme/style/segmented_button.rs +++ b/src/theme/style/segmented_button.rs @@ -5,7 +5,7 @@ use crate::widget::segmented_button::{Appearance, ItemAppearance, StyleSheet}; use crate::{theme::Theme, widget::segmented_button::ItemStatusAppearance}; -use cosmic_theme::{Component, Container}; +use iced::Border; use iced_core::{Background, border::Radius}; use palette::WithAlpha; @@ -16,6 +16,8 @@ pub enum SegmentedButton { TabBar, /// A widget for multiple choice selection. Control, + /// Navigation bar style + NavBar, /// Or implement any custom theme of your liking. Custom(Box Appearance>), } @@ -25,85 +27,54 @@ impl StyleSheet for Theme { #[allow(clippy::too_many_lines)] fn horizontal(&self, style: &Self::Style) -> Appearance { - let container = &self.current_container(); - + let cosmic = self.cosmic(); + let container = self.current_container(); match style { - SegmentedButton::TabBar => { - let cosmic = self.cosmic(); - let active = horizontal::tab_bar_active(cosmic); - let hc = cosmic.is_high_contrast; - let (border_end, border_start, border_top) = if hc { - ( - Some((1., container.component.border.into())), - Some((1., container.component.border.into())), - Some((1., container.component.border.into())), - ) - } else { - (None, None, None) - }; - Appearance { - border_radius: cosmic.corner_radii.radius_0.into(), - inactive: ItemStatusAppearance { - background: None, - first: ItemAppearance { - border_radius: cosmic.corner_radii.radius_0.into(), - border_bottom: Some((1.0, cosmic.accent.base.into())), - border_end, - border_start, - border_top, - }, - middle: ItemAppearance { - border_radius: cosmic.corner_radii.radius_0.into(), - border_bottom: Some((1.0, cosmic.accent.base.into())), - border_end, - border_start, - border_top, - }, - last: ItemAppearance { - border_radius: cosmic.corner_radii.radius_0.into(), - border_bottom: Some((1.0, cosmic.accent.base.into())), - border_end, - border_start, - border_top, - }, - text_color: container.component.on.into(), - }, - hover: hover(cosmic, &container.component, &active), - focus: focus(cosmic, container, &active), - active, - ..Default::default() - } - } SegmentedButton::Control => { - let cosmic = self.cosmic(); - let active = horizontal::selection_active(cosmic, &container.component); - let rad_m = cosmic.corner_radii.radius_m; + let rad_xl = cosmic.corner_radii.radius_xl; let rad_0 = cosmic.corner_radii.radius_0; + let active = horizontal::selection_active(cosmic, &container.component); Appearance { - background: Some(Background::Color(container.small_widget.into())), - border_radius: rad_m.into(), + background: Some(Background::Color(container.component.base.into())), + border: Border { + radius: rad_xl.into(), + ..Default::default() + }, inactive: ItemStatusAppearance { background: None, first: ItemAppearance { - border_radius: Radius::from([rad_m[0], rad_0[1], rad_0[2], rad_m[3]]), - ..Default::default() + border: Border { + radius: Radius::from([rad_xl[0], rad_0[1], rad_0[2], rad_xl[3]]), + ..Default::default() + }, }, middle: ItemAppearance { - border_radius: cosmic.corner_radii.radius_0.into(), - ..Default::default() + border: Border { + radius: cosmic.corner_radii.radius_0.into(), + ..Default::default() + }, }, last: ItemAppearance { - border_radius: Radius::from([rad_0[0], rad_m[1], rad_m[2], rad_0[3]]), - ..Default::default() + border: Border { + radius: Radius::from([rad_0[0], rad_xl[1], rad_xl[2], rad_0[3]]), + ..Default::default() + }, }, text_color: container.component.on.into(), }, - hover: hover(cosmic, &container.component, &active), - focus: focus(cosmic, container, &active), + hover: hover(cosmic, &active, 0.2), active, ..Default::default() } } + + SegmentedButton::NavBar => Appearance { + active_width: 0.0, + ..horizontal::tab_bar(cosmic, container) + }, + + SegmentedButton::TabBar => horizontal::tab_bar(cosmic, container), + SegmentedButton::Custom(func) => func(self), } } @@ -111,84 +82,126 @@ impl StyleSheet for Theme { #[allow(clippy::too_many_lines)] fn vertical(&self, style: &Self::Style) -> Appearance { let cosmic = self.cosmic(); - let rad_m = cosmic.corner_radii.radius_m; - let rad_0 = cosmic.corner_radii.radius_0; + let container = self.current_container(); match style { - SegmentedButton::TabBar => { - let container = &self.cosmic().primary; - let active = vertical::tab_bar_active(cosmic); - Appearance { - border_radius: cosmic.corner_radii.radius_0.into(), - inactive: ItemStatusAppearance { - background: None, - text_color: container.component.on.into(), - ..active - }, - hover: hover(cosmic, &container.component, &active), - focus: focus(cosmic, container, &active), - active, - ..Default::default() - } - } SegmentedButton::Control => { - let container = self.current_container(); + let rad_xl = cosmic.corner_radii.radius_xl; + let rad_0 = cosmic.corner_radii.radius_0; let active = vertical::selection_active(cosmic, &container.component); Appearance { - background: Some(Background::Color(container.small_widget.into())), - border_radius: rad_m.into(), + background: Some(Background::Color(container.component.base.into())), + border: Border { + radius: rad_xl.into(), + ..Default::default() + }, inactive: ItemStatusAppearance { background: None, first: ItemAppearance { - border_radius: Radius::from([rad_m[0], rad_m[1], rad_0[0], rad_0[0]]), - ..Default::default() + border: Border { + radius: Radius::from([rad_xl[0], rad_xl[1], rad_0[0], rad_0[0]]), + ..Default::default() + }, }, middle: ItemAppearance { - border_radius: cosmic.corner_radii.radius_0.into(), - ..Default::default() + border: Border { + radius: cosmic.corner_radii.radius_0.into(), + ..Default::default() + }, }, last: ItemAppearance { - border_radius: Radius::from([rad_0[0], rad_0[1], rad_m[2], rad_m[3]]), - ..Default::default() + border: Border { + radius: Radius::from([rad_0[0], rad_0[1], rad_xl[2], rad_xl[3]]), + ..Default::default() + }, }, text_color: container.component.on.into(), }, - hover: hover(cosmic, &container.component, &active), - focus: focus(cosmic, container, &active), + hover: hover(cosmic, &active, 0.2), active, ..Default::default() } } + + SegmentedButton::NavBar => Appearance { + active_width: 0.0, + ..vertical::tab_bar(cosmic, container) + }, + + SegmentedButton::TabBar => vertical::tab_bar(cosmic, container), + SegmentedButton::Custom(func) => func(self), } } } mod horizontal { + use super::Appearance; use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; - use cosmic_theme::Component; + use cosmic_theme::{Component, Container}; + use iced::Border; use iced_core::{Background, border::Radius}; use palette::WithAlpha; + pub fn tab_bar(cosmic: &cosmic_theme::Theme, container: &Container) -> Appearance { + let active = tab_bar_active(cosmic); + let hc = cosmic.is_high_contrast; + let border = if hc { + Border { + color: container.component.border.into(), + radius: cosmic.corner_radii.radius_0.into(), + width: 1.0, + } + } else { + Border::default() + }; + + Appearance { + active_width: 4.0, + border: Border { + radius: cosmic.corner_radii.radius_0.into(), + ..Default::default() + }, + inactive: ItemStatusAppearance { + background: None, + first: ItemAppearance { border }, + middle: ItemAppearance { border }, + last: ItemAppearance { border }, + text_color: container.component.on.into(), + }, + hover: super::hover(cosmic, &active, 0.3), + active, + ..Default::default() + } + } + pub fn selection_active( cosmic: &cosmic_theme::Theme, component: &Component, ) -> ItemStatusAppearance { - let rad_m = cosmic.corner_radii.radius_m; + let rad_xl = cosmic.corner_radii.radius_xl; let rad_0 = cosmic.corner_radii.radius_0; ItemStatusAppearance { - background: Some(Background::Color(component.selected_state_color().into())), + background: Some(Background::Color( + cosmic.palette.neutral_5.with_alpha(0.1).into(), + )), first: ItemAppearance { - border_radius: Radius::from([rad_m[0], rad_0[1], rad_0[2], rad_m[3]]), - ..Default::default() + border: Border { + radius: Radius::from([rad_xl[0], rad_0[1], rad_0[2], rad_xl[3]]), + ..Default::default() + }, }, middle: ItemAppearance { - border_radius: cosmic.corner_radii.radius_0.into(), - ..Default::default() + border: Border { + radius: cosmic.corner_radii.radius_0.into(), + ..Default::default() + }, }, last: ItemAppearance { - border_radius: Radius::from([rad_0[0], rad_m[1], rad_m[2], rad_0[3]]), - ..Default::default() + border: Border { + radius: Radius::from([rad_0[0], rad_xl[1], rad_xl[2], rad_0[3]]), + ..Default::default() + }, }, text_color: cosmic.accent_text_color().into(), } @@ -202,78 +215,86 @@ mod horizontal { cosmic.palette.neutral_5.with_alpha(0.2).into(), )), first: ItemAppearance { - border_radius: Radius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), - border_bottom: Some((4.0, cosmic.accent.base.into())), - ..Default::default() + border: Border { + color: cosmic.accent.base.into(), + radius: Radius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), + width: 0.0, + }, }, middle: ItemAppearance { - border_radius: Radius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), - border_bottom: Some((4.0, cosmic.accent.base.into())), - ..Default::default() + border: Border { + color: cosmic.accent.base.into(), + radius: Radius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), + width: 0.0, + }, }, last: ItemAppearance { - border_radius: Radius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), - border_bottom: Some((4.0, cosmic.accent.base.into())), - ..Default::default() + border: Border { + color: cosmic.accent.base.into(), + radius: Radius::from([rad_s[0], rad_s[1], rad_0[2], rad_0[3]]), + width: 0.0, + }, }, text_color: cosmic.accent_text_color().into(), } } } -pub fn focus( - cosmic: &cosmic_theme::Theme, - container: &Container, - default: &ItemStatusAppearance, -) -> ItemStatusAppearance { - let color = container.small_widget; - ItemStatusAppearance { - background: Some(Background::Color(color.into())), - text_color: cosmic.accent_text_color().into(), - ..*default - } -} - -pub fn hover( - cosmic: &cosmic_theme::Theme, - component: &Component, - default: &ItemStatusAppearance, -) -> ItemStatusAppearance { - ItemStatusAppearance { - background: Some(Background::Color(component.hover.with_alpha(0.2).into())), - text_color: cosmic.accent_text_color().into(), - ..*default - } -} - mod vertical { + use super::Appearance; use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; - use cosmic_theme::Component; + use cosmic_theme::{Component, Container}; + use iced::Border; use iced_core::{Background, border::Radius}; use palette::WithAlpha; + pub fn tab_bar(cosmic: &cosmic_theme::Theme, container: &Container) -> Appearance { + let active = tab_bar_active(cosmic); + Appearance { + active_width: 4.0, + border: Border { + radius: cosmic.corner_radii.radius_0.into(), + ..Default::default() + }, + inactive: ItemStatusAppearance { + background: None, + text_color: container.component.on.into(), + ..active + }, + hover: super::hover(cosmic, &active, 0.3), + active, + ..Default::default() + } + } + pub fn selection_active( cosmic: &cosmic_theme::Theme, component: &Component, ) -> ItemStatusAppearance { let rad_0 = cosmic.corner_radii.radius_0; - let rad_m = cosmic.corner_radii.radius_m; + let rad_xl = cosmic.corner_radii.radius_xl; ItemStatusAppearance { background: Some(Background::Color( - component.selected_state_color().with_alpha(0.3).into(), + cosmic.palette.neutral_5.with_alpha(0.1).into(), )), first: ItemAppearance { - border_radius: Radius::from([rad_m[0], rad_m[1], rad_0[2], rad_0[3]]), - ..Default::default() + border: Border { + radius: Radius::from([rad_xl[0], rad_xl[1], rad_0[2], rad_0[3]]), + ..Default::default() + }, }, middle: ItemAppearance { - border_radius: cosmic.corner_radii.radius_0.into(), - ..Default::default() + border: Border { + radius: cosmic.corner_radii.radius_0.into(), + ..Default::default() + }, }, last: ItemAppearance { - border_radius: Radius::from([rad_0[0], rad_0[1], rad_m[2], rad_m[3]]), - ..Default::default() + border: Border { + radius: Radius::from([rad_0[0], rad_0[1], rad_xl[2], rad_xl[3]]), + ..Default::default() + }, }, text_color: cosmic.accent_text_color().into(), } @@ -285,18 +306,41 @@ mod vertical { cosmic.palette.neutral_5.with_alpha(0.2).into(), )), first: ItemAppearance { - border_radius: cosmic.corner_radii.radius_m.into(), - ..Default::default() + border: Border { + radius: cosmic.corner_radii.radius_m.into(), + width: 0.0, + ..Default::default() + }, }, middle: ItemAppearance { - border_radius: cosmic.corner_radii.radius_m.into(), - ..Default::default() + border: Border { + radius: cosmic.corner_radii.radius_m.into(), + width: 0.0, + ..Default::default() + }, }, last: ItemAppearance { - border_radius: cosmic.corner_radii.radius_m.into(), - ..Default::default() + border: Border { + radius: cosmic.corner_radii.radius_m.into(), + width: 0.0, + ..Default::default() + }, }, text_color: cosmic.accent_text_color().into(), } } } + +pub fn hover( + cosmic: &cosmic_theme::Theme, + default: &ItemStatusAppearance, + alpha: f32, +) -> ItemStatusAppearance { + ItemStatusAppearance { + background: Some(Background::Color( + cosmic.palette.neutral_5.with_alpha(alpha).into(), + )), + text_color: cosmic.accent_text_color().into(), + ..*default + } +} diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index 6923472a..1ae4005d 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -149,7 +149,7 @@ impl<'a, Message: Clone + 'static> From> .button_padding([space_s, space_xxs, space_s, space_xxs]) .button_spacing(space_xxs) .spacing(space_xxs) - .style(crate::theme::SegmentedButton::TabBar) + .style(crate::theme::SegmentedButton::NavBar) .apply(container) .padding(space_xxs) .apply(scrollable) diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index ccf4c8ae..724ded96 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -36,6 +36,8 @@ where Model: Selectable, SelectionMode: Default, { + const VERTICAL: bool = false; + fn variant_appearance( theme: &crate::Theme, style: &crate::theme::SegmentedButton, diff --git a/src/widget/segmented_button/style.rs b/src/widget/segmented_button/style.rs index 102b3686..f09a74a2 100644 --- a/src/widget/segmented_button/style.rs +++ b/src/widget/segmented_button/style.rs @@ -1,31 +1,24 @@ // Copyright 2022 System76 // SPDX-License-Identifier: MPL-2.0 -use iced_core::{Background, Color, border::Radius}; +use iced::Border; +use iced_core::{Background, Color}; /// Appearance of the segmented button. #[derive(Default, Clone, Copy)] pub struct Appearance { pub background: Option, - pub border_radius: Radius, - pub border_bottom: Option<(f32, Color)>, - pub border_end: Option<(f32, Color)>, - pub border_start: Option<(f32, Color)>, - pub border_top: Option<(f32, Color)>, + pub border: Border, + pub active_width: f32, pub active: ItemStatusAppearance, pub inactive: ItemStatusAppearance, pub hover: ItemStatusAppearance, - pub focus: ItemStatusAppearance, } /// Appearance of an item in the segmented button. #[derive(Default, Clone, Copy)] pub struct ItemAppearance { - pub border_radius: Radius, - pub border_bottom: Option<(f32, Color)>, - pub border_end: Option<(f32, Color)>, - pub border_start: Option<(f32, Color)>, - pub border_top: Option<(f32, Color)>, + pub border: Border, } /// Appearance of an item based on its status. diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 3f5d5645..ce9f50fe 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -36,6 +36,8 @@ where Model: Selectable, SelectionMode: Default, { + const VERTICAL: bool = true; + fn variant_appearance( theme: &crate::Theme, style: &crate::theme::SegmentedButton, diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 1c92d0b2..620c8439 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -22,11 +22,12 @@ use iced::{ use iced_core::mouse::ScrollDelta; use iced_core::text::{LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; use iced_core::widget::{self, operation, tree}; -use iced_core::{Border, Gradient, Point, Renderer as IcedRenderer, Shadow, Text}; +use iced_core::{Border, Point, Renderer as IcedRenderer, Shadow, Text}; use iced_core::{Clipboard, Layout, Shell, Widget, layout, renderer, widget::Tree}; use iced_runtime::{Action, task}; use slotmap::{Key, SecondaryMap}; use std::borrow::Cow; +use std::cell::LazyCell; use std::collections::HashSet; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; @@ -46,6 +47,8 @@ pub enum ItemBounds { /// Isolates variant-specific behaviors from [`SegmentedButton`]. pub trait SegmentedVariant { + const VERTICAL: bool; + /// Get the appearance for this variant of the widget. fn variant_appearance( theme: &crate::Theme, @@ -107,11 +110,11 @@ where /// Spacing for each indent. pub(super) indent_spacing: u16, /// Desired font for active tabs. - pub(super) font_active: Option, + pub(super) font_active: crate::font::Font, /// Desired font for hovered tabs. - pub(super) font_hovered: Option, + pub(super) font_hovered: crate::font::Font, /// Desired font for inactive tabs. - pub(super) font_inactive: Option, + pub(super) font_inactive: crate::font::Font, /// Size of the font. pub(super) font_size: f32, /// Desired width of the widget. @@ -175,9 +178,9 @@ where minimum_button_width: u16::MIN, maximum_button_width: u16::MAX, indent_spacing: 16, - font_active: None, - font_hovered: None, - font_inactive: None, + font_active: crate::font::semibold(), + font_hovered: crate::font::semibold(), + font_inactive: crate::font::default(), font_size: 14.0, height: Length::Shrink, width: Length::Fill, @@ -603,16 +606,16 @@ where for key in self.model.order.iter().copied() { if let Some(text) = self.model.text.get(key) { - let (font, button_state) = - if self.model.is_active(key) || self.button_is_focused(state, key) { - (self.font_active, 0) - } else if self.button_is_hovered(state, key) { - (self.font_hovered, 1) - } else { - (self.font_inactive, 2) - }; + let (font, button_state) = if self.button_is_focused(state, key) { + (self.font_active, 0) + } else if state.show_context.is_some() || self.button_is_hovered(state, key) { + (self.font_hovered, 1) + } else if self.model.is_active(key) { + (self.font_active, 2) + } else { + (self.font_inactive, 3) + }; - let font = font.unwrap_or_else(crate::font::default); let mut hasher = DefaultHasher::new(); text.hash(&mut hasher); font.hash(&mut hasher); @@ -1171,47 +1174,15 @@ where let bounds: Rectangle = layout.bounds(); let button_amount = self.model.items.len(); - // Modifies alpha color when `on_activate` is unset. - let apply_alpha = |mut c: Color| { - if self.on_activate.is_none() { - c.a /= 2.0; - } - - c - }; - - // Maps `apply_alpha` to background color. - let bg_with_alpha = |mut b| { - match &mut b { - Background::Color(c) => { - *c = apply_alpha(*c); - } - - Background::Gradient(g) => { - let Gradient::Linear(l) = g; - for c in &mut l.stops { - let Some(stop) = c else { - continue; - }; - stop.color = apply_alpha(stop.color); - } - } - } - b - }; - // Draw the background, if a background was defined. if let Some(background) = appearance.background { renderer.fill_quad( renderer::Quad { bounds, - border: Border { - radius: appearance.border_radius, - ..Border::default() - }, + border: appearance.border, shadow: Shadow::default(), }, - bg_with_alpha(background), + background, ); } @@ -1222,7 +1193,7 @@ where // Previous tab button let mut background_appearance = if self.on_activate.is_some() && Item::PrevButton == state.focused_item { - Some(appearance.focus) + Some(appearance.active) } else if self.on_activate.is_some() && Item::PrevButton == state.hovered { Some(appearance.hover) } else { @@ -1241,7 +1212,7 @@ where }, background_appearance .background - .map_or(Background::Color(Color::TRANSPARENT), bg_with_alpha), + .unwrap_or(Background::Color(Color::TRANSPARENT)), ); } @@ -1251,13 +1222,11 @@ where style, cursor, viewport, - apply_alpha(if state.buttons_offset == 0 { + if state.buttons_offset == 0 { appearance.inactive.text_color - } else if let Item::PrevButton = state.focused_item { - appearance.focus.text_color } else { appearance.active.text_color - }), + }, Rectangle { x: tab_bounds.x + 8.0, y: tab_bounds.y + f32::from(self.button_height) / 4.0, @@ -1272,7 +1241,7 @@ where // Next tab button background_appearance = if self.on_activate.is_some() && Item::NextButton == state.focused_item { - Some(appearance.focus) + Some(appearance.active) } else if self.on_activate.is_some() && Item::NextButton == state.hovered { Some(appearance.hover) } else { @@ -1301,13 +1270,13 @@ where style, cursor, viewport, - apply_alpha(if self.next_tab_sensitive(state) { + if self.next_tab_sensitive(state) { appearance.active.text_color } else if let Item::NextButton = state.focused_item { - appearance.focus.text_color + appearance.active.text_color } else { appearance.inactive.text_color - }), + }, Rectangle { x: tab_bounds.x + 8.0, y: tab_bounds.y + f32::from(self.button_height) / 4.0, @@ -1349,22 +1318,23 @@ where let center_y = bounds.center_y(); - let menu_open = !tree.children.is_empty() - && tree.children[0] - .state - .downcast_ref::() - .inner - .with_data(|data| data.open); + let menu_open = || { + state.show_context == Some(key) + && !tree.children.is_empty() + && tree.children[0] + .state + .downcast_ref::() + .inner + .with_data(|data| data.open) + }; let key_is_active = self.model.is_active(key); - let key_is_hovered = self.button_is_hovered(state, key); - let key_has_context_menu_open = menu_open && state.show_context == Some(key); - let status_appearance = if self.button_is_focused(state, key) { - appearance.focus + let key_is_focused = self.button_is_focused(state, key); + let key_is_hovered = LazyCell::new(|| self.button_is_hovered(state, key)); + let status_appearance = if *key_is_hovered || menu_open() { + appearance.hover } else if key_is_active { appearance.active - } else if key_is_hovered || key_has_context_menu_open { - appearance.hover } else { appearance.inactive }; @@ -1378,29 +1348,45 @@ where }; // Render the background of the button. - if status_appearance.background.is_some() { + if key_is_focused || status_appearance.background.is_some() { renderer.fill_quad( renderer::Quad { bounds, - border: Border { - radius: button_appearance.border_radius, - ..Default::default() + border: if key_is_focused { + Border { + width: 1.0, + color: appearance.active.text_color, + radius: button_appearance.border.radius, + } + } else { + button_appearance.border }, shadow: Shadow::default(), }, status_appearance .background - .map_or(Background::Color(Color::TRANSPARENT), bg_with_alpha), + .unwrap_or(Background::Color(Color::TRANSPARENT)), ); } - // Draw the bottom border defined for this button. - if let Some((width, background)) = button_appearance.border_bottom { - let mut bounds = bounds; - bounds.y = bounds.y + bounds.height - width; - bounds.height = width; - + // Draw the active hint on tabs + if appearance.active_width > 0.0 { let rad_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0; + let active_width = if key_is_active { + appearance.active_width + } else { + 1.0 + }; + let mut bounds = bounds; + + if Self::VERTICAL { + bounds.x += bounds.height - active_width; + bounds.width = active_width; + } else { + bounds.y += bounds.height - active_width; + bounds.height = active_width; + } + renderer.fill_quad( renderer::Quad { bounds, @@ -1410,7 +1396,7 @@ where }, shadow: Shadow::default(), }, - bg_with_alpha(background.into()), + appearance.active.text_color, ); } @@ -1455,7 +1441,7 @@ where style, cursor, viewport, - apply_alpha(status_appearance.text_color), + status_appearance.text_color, Rectangle { width, height: width, @@ -1470,7 +1456,7 @@ where if key_is_active { if let crate::theme::SegmentedButton::Control = self.style { let mut image_bounds = bounds; - image_bounds.y = center_y - 16.0 / 2.0; + image_bounds.y = center_y - 8.0; draw_icon::( renderer, @@ -1478,7 +1464,7 @@ where style, cursor, viewport, - apply_alpha(status_appearance.text_color), + status_appearance.text_color, Rectangle { width: 16.0, height: 16.0, @@ -1505,7 +1491,7 @@ where // Whether to show the close button on this tab. let show_close_button = - (key_is_active || !self.show_close_icon_on_hover || key_is_hovered) + (key_is_active || !self.show_close_icon_on_hover || *key_is_hovered) && self.model.is_closable(key); // Width of the icon used by the close button, which we will subtract from the text bounds. @@ -1527,7 +1513,7 @@ where renderer.fill_paragraph( state.paragraphs[key].raw(), bounds.position(), - apply_alpha(status_appearance.text_color), + status_appearance.text_color, Rectangle { x: bounds.x, width: bounds.width, @@ -1546,7 +1532,7 @@ where style, cursor, viewport, - apply_alpha(status_appearance.text_color), + status_appearance.text_color, close_button_bounds, self.close_icon.clone(), ); diff --git a/src/widget/segmented_control.rs b/src/widget/segmented_control.rs index 9dbcfc51..0c213b2c 100644 --- a/src/widget/segmented_control.rs +++ b/src/widget/segmented_control.rs @@ -30,7 +30,6 @@ where .button_padding([space_s, 0, space_s, 0]) .button_spacing(space_xxs) .style(crate::theme::SegmentedButton::Control) - .font_active(Some(crate::font::semibold())) } /// A selection of multiple choices appearing as a conjoined button. @@ -55,5 +54,4 @@ where .button_padding([space_s, 0, space_s, 0]) .button_spacing(space_xxs) .style(crate::theme::SegmentedButton::Control) - .font_active(Some(crate::font::semibold())) } diff --git a/src/widget/tab_bar.rs b/src/widget/tab_bar.rs index b3def5ca..4f4c6149 100644 --- a/src/widget/tab_bar.rs +++ b/src/widget/tab_bar.rs @@ -29,7 +29,6 @@ where .button_height(44) .button_padding([space_s, space_xs, space_s, space_xs]) .style(crate::theme::SegmentedButton::TabBar) - .font_active(Some(crate::font::semibold())) } /// A collection of tabs for developing a tabbed interface. @@ -52,5 +51,4 @@ where .button_height(44) .button_padding([space_s, space_xs, space_s, space_xs]) .style(crate::theme::SegmentedButton::TabBar) - .font_active(Some(crate::font::semibold())) } From 989fcad99eea56967b65aab137175d8a29e8a046 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 11 Aug 2025 22:40:10 -0400 Subject: [PATCH 048/352] fix(input): reset cursor and last click state on unfocus --- src/widget/text_input/input.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 06a193b9..b8c035d4 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -2654,6 +2654,8 @@ impl State { /// Unfocuses the [`TextInput`]. #[cold] pub(super) fn unfocus(&mut self) { + self.move_cursor_to_front(); + self.last_click = None; self.is_focused = None; self.dragging_state = None; self.is_pasting = None; From 6a5076ecb7dc51fc3d255e8cc865acfc9a7b9343 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 12 Aug 2025 22:20:28 +0200 Subject: [PATCH 049/352] fix(context_drawer): adjust header to avoid text wrapping --- src/widget/context_drawer/widget.rs | 27 ++++++++++++++++++--------- 1 file changed, 18 insertions(+), 9 deletions(-) diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index 7f493dac..23afae3b 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -57,23 +57,32 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { let horizontal_padding = if max_width < 392.0 { space_s } else { space_l }; + let title = + title.map(|title| text::heading(title).width(Length::FillPortion(3)).center()); + + let close_width = if title.is_some() { + Length::FillPortion(1) + } else { + Length::Shrink + }; + let header_row = row::with_capacity(3) .width(Length::Fixed(480.0)) .align_y(Alignment::Center) - .push( - row::with_children(header_actions) - .spacing(space_xxs) - .width(Length::FillPortion(1)), - ) - .push_maybe( - title.map(|title| text::heading(title).width(Length::FillPortion(1)).center()), - ) + .push(row::with_children(header_actions).spacing(space_xxs).width( + if title.is_some() { + Length::FillPortion(1) + } else { + Length::Fill + }, + )) + .push_maybe(title) .push( button::text("Close") .trailing_icon(icon::from_name("go-next-symbolic")) .on_press(on_close) .apply(container) - .width(Length::FillPortion(1)) + .width(close_width) .align_x(Alignment::End), ); let header = column::with_capacity(2) From 4f423349a2be727d5dae21aacc86d062e702f93c Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 13 Aug 2025 11:18:58 +0200 Subject: [PATCH 050/352] fix(segmented_button): duplicate focus fix --- src/widget/segmented_button/widget.rs | 63 ++++++++++++++++++++------- 1 file changed, 48 insertions(+), 15 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 620c8439..126c78e5 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -21,13 +21,14 @@ use iced::{ }; use iced_core::mouse::ScrollDelta; use iced_core::text::{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}; use iced_core::{Clipboard, Layout, Shell, Widget, layout, renderer, widget::Tree}; use iced_runtime::{Action, task}; use slotmap::{Key, SecondaryMap}; use std::borrow::Cow; -use std::cell::LazyCell; +use std::cell::{Cell, LazyCell}; use std::collections::HashSet; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; @@ -35,6 +36,11 @@ use std::marker::PhantomData; use std::mem; use std::time::{Duration, Instant}; +thread_local! { + // Prevents two segmented buttons from being focused at the same time. + static LAST_FOCUS_UPDATE: LazyCell> = LazyCell::new(|| Cell::new(Instant::now())); +} + /// A command that focuses a segmented item stored in a widget. pub fn focus(id: Id) -> Task { task::effect(Action::Widget(Box::new(operation::focusable::focus(id.0)))) @@ -521,7 +527,9 @@ where } fn button_is_focused(&self, state: &LocalState, key: Entity) -> bool { - self.on_activate.is_some() && Item::Tab(key) == state.focused_item + state.focused.is_some() + && self.on_activate.is_some() + && Item::Tab(key) == state.focused_item } fn button_is_hovered(&self, state: &LocalState, key: Entity) -> bool { @@ -636,7 +644,7 @@ where horizontal_alignment: alignment::Horizontal::Left, vertical_alignment: alignment::Vertical::Center, shaping: Shaping::Advanced, - wrapping: Wrapping::default(), + wrapping: Wrapping::None, line_height: self.line_height, }; @@ -654,6 +662,13 @@ where menu_roots_diff(context_menu, &mut inner.tree); }); } + + // 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(); + } + } } fn size(&self) -> Size { @@ -911,8 +926,7 @@ where if let Event::Mouse(mouse::Event::ButtonReleased(_)) | Event::Touch(touch::Event::FingerLifted { .. }) = event { - state.focused = false; - state.focused_item = Item::None; + state.unfocus(); } if let Some(on_activate) = self.on_activate.as_ref() { @@ -932,7 +946,7 @@ where if can_activate { shell.publish(on_activate(key)); - state.focused = true; + state.set_focused(); state.focused_item = Item::Tab(key); state.pressed_item = None; return event::Status::Captured; @@ -1020,7 +1034,7 @@ where if let Some(key) = activate_key { shell.publish(on_activate(key)); - state.focused = true; + state.set_focused(); state.focused_item = Item::Tab(key); return event::Status::Captured; } @@ -1030,19 +1044,18 @@ where } } } - } else if state.focused { + } else if state.is_focused() { // Unfocus on clicks outside of the boundaries of the segmented button. if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) = event { - state.focused = true; - state.focused_item = Item::None; + state.unfocus(); state.pressed_item = None; return event::Status::Ignored; } } - if state.focused { + if state.is_focused() { if let Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(keyboard::key::Named::Tab), modifiers, @@ -1650,6 +1663,12 @@ where } } +#[derive(Debug, Clone, Copy)] +struct Focus { + updated_at: Instant, + now: Instant, +} + /// State that is maintained by each individual widget. pub struct LocalState { /// Menu state @@ -1661,7 +1680,7 @@ pub struct LocalState { /// Whether buttons need to be collapsed to preserve minimum width pub(super) collapsed: bool, /// If the widget is focused or not. - focused: bool, + focused: Option, /// The key inside the widget that is currently focused. focused_item: Item, /// The ID of the button that is being hovered. Defaults to null. @@ -1700,18 +1719,32 @@ enum Item { Tab(Entity), } +impl LocalState { + fn set_focused(&mut self) { + let now = Instant::now(); + LAST_FOCUS_UPDATE.with(|x| x.set(now)); + + self.focused = Some(Focus { + updated_at: now, + now, + }); + } +} + impl operation::Focusable for LocalState { fn is_focused(&self) -> bool { - self.focused + self.focused.map_or(false, |f| { + f.updated_at == LAST_FOCUS_UPDATE.with(|f| f.get()) + }) } fn focus(&mut self) { - self.focused = true; + self.set_focused(); self.focused_item = Item::Set; } fn unfocus(&mut self) { - self.focused = false; + self.focused = None; self.focused_item = Item::None; self.show_context = None; } From 5434dc95d5df6d8da0fb3e7fb7c91b6c6aee3637 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 13 Aug 2025 12:13:05 +0200 Subject: [PATCH 051/352] feat(segmented_button): pressed state style --- src/theme/style/segmented_button.rs | 4 ++ src/widget/segmented_button/style.rs | 1 + src/widget/segmented_button/widget.rs | 55 ++++++++++++++++----------- 3 files changed, 37 insertions(+), 23 deletions(-) diff --git a/src/theme/style/segmented_button.rs b/src/theme/style/segmented_button.rs index 3f5d92db..5306b3bf 100644 --- a/src/theme/style/segmented_button.rs +++ b/src/theme/style/segmented_button.rs @@ -63,6 +63,7 @@ impl StyleSheet for Theme { text_color: container.component.on.into(), }, hover: hover(cosmic, &active, 0.2), + pressed: hover(cosmic, &active, 0.15), active, ..Default::default() } @@ -117,6 +118,7 @@ impl StyleSheet for Theme { text_color: container.component.on.into(), }, hover: hover(cosmic, &active, 0.2), + pressed: hover(cosmic, &active, 0.15), active, ..Default::default() } @@ -169,6 +171,7 @@ mod horizontal { text_color: container.component.on.into(), }, hover: super::hover(cosmic, &active, 0.3), + pressed: super::hover(cosmic, &active, 0.25), active, ..Default::default() } @@ -262,6 +265,7 @@ mod vertical { ..active }, hover: super::hover(cosmic, &active, 0.3), + pressed: super::hover(cosmic, &active, 0.25), active, ..Default::default() } diff --git a/src/widget/segmented_button/style.rs b/src/widget/segmented_button/style.rs index f09a74a2..4aa856ef 100644 --- a/src/widget/segmented_button/style.rs +++ b/src/widget/segmented_button/style.rs @@ -13,6 +13,7 @@ pub struct Appearance { pub active: ItemStatusAppearance, pub inactive: ItemStatusAppearance, pub hover: ItemStatusAppearance, + pub pressed: ItemStatusAppearance, } /// Appearance of an item in the segmented button. diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 126c78e5..6701bddb 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -541,6 +541,10 @@ where .is_some_and(|id| id.data.is_some_and(|d| d == key)) } + fn button_is_pressed(&self, state: &LocalState, key: Entity) -> bool { + state.pressed_item == Some(Item::Tab(key)) + } + /// Returns the drag id of the destination. /// /// # Panics @@ -923,28 +927,15 @@ where } } - if let Event::Mouse(mouse::Event::ButtonReleased(_)) - | Event::Touch(touch::Event::FingerLifted { .. }) = event - { + if is_lifted(&event) { state.unfocus(); } if let Some(on_activate) = self.on_activate.as_ref() { - if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) = event - { + if is_pressed(&event) { state.pressed_item = Some(Item::Tab(key)); - } else if let Event::Mouse(mouse::Event::ButtonReleased( - mouse::Button::Left, - )) - | Event::Touch(touch::Event::FingerLifted { .. }) = event - { - let mut can_activate = true; - if state.pressed_item != Some(Item::Tab(key)) { - can_activate = false; - } - - if can_activate { + } 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); @@ -1046,13 +1037,13 @@ where } } else if state.is_focused() { // Unfocus on clicks outside of the boundaries of the segmented button. - if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) - | Event::Touch(touch::Event::FingerPressed { .. }) = event - { + if is_pressed(&event) { state.unfocus(); state.pressed_item = None; return event::Status::Ignored; } + } else if is_lifted(&event) { + state.pressed_item = None; } if state.is_focused() { @@ -1343,8 +1334,10 @@ where let key_is_active = self.model.is_active(key); let key_is_focused = self.button_is_focused(state, key); - let key_is_hovered = LazyCell::new(|| self.button_is_hovered(state, key)); - let status_appearance = if *key_is_hovered || menu_open() { + let key_is_hovered = self.button_is_hovered(state, key); + let status_appearance = if self.button_is_pressed(state, key) && key_is_hovered { + appearance.pressed + } else if key_is_hovered || menu_open() { appearance.hover } else if key_is_active { appearance.active @@ -1504,7 +1497,7 @@ where // Whether to show the close button on this tab. let show_close_button = - (key_is_active || !self.show_close_icon_on_hover || *key_is_hovered) + (key_is_active || !self.show_close_icon_on_hover || key_is_hovered) && self.model.is_closable(key); // Width of the icon used by the close button, which we will subtract from the text bounds. @@ -1856,6 +1849,22 @@ fn right_button_released(event: &Event) -> bool { ) } +fn is_pressed(event: &Event) -> bool { + matches!( + event, + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + | Event::Touch(touch::Event::FingerPressed { .. }) + ) +} + +fn is_lifted(event: &Event) -> bool { + matches!( + event, + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left,)) + | Event::Touch(touch::Event::FingerLifted { .. }) + ) +} + fn touch_lifted(event: &Event) -> bool { matches!(event, Event::Touch(touch::Event::FingerLifted { .. })) } From 95ebabf149aa287dbdbf93c97df8d8bc03c57b47 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 13 Aug 2025 12:20:22 +0200 Subject: [PATCH 052/352] improv(segmented_button): hide focus state until tabbed --- src/widget/segmented_button/widget.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 6701bddb..d21f409b 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -600,6 +600,7 @@ where collapsed: Default::default(), focused: Default::default(), focused_item: Default::default(), + focused_visible: false, hovered: Default::default(), known_length: Default::default(), middle_clicked: Default::default(), @@ -1053,6 +1054,7 @@ where .. }) = event { + state.focused_visible = true; return if modifiers.shift() { self.focus_previous(state) } else { @@ -1333,7 +1335,7 @@ where }; let key_is_active = self.model.is_active(key); - let key_is_focused = self.button_is_focused(state, key); + let key_is_focused = state.focused_visible && self.button_is_focused(state, key); let key_is_hovered = self.button_is_hovered(state, key); let status_appearance = if self.button_is_pressed(state, key) && key_is_hovered { appearance.pressed @@ -1672,6 +1674,8 @@ pub struct LocalState { pub(super) buttons_offset: usize, /// Whether buttons need to be collapsed to preserve minimum width pub(super) collapsed: bool, + /// Visibility of focus state + focused_visible: bool, /// If the widget is focused or not. focused: Option, /// The key inside the widget that is currently focused. @@ -1733,12 +1737,14 @@ impl operation::Focusable for LocalState { fn focus(&mut self) { self.set_focused(); + self.focused_visible = true; self.focused_item = Item::Set; } fn unfocus(&mut self) { self.focused = None; self.focused_item = Item::None; + self.focused_visible = false; self.show_context = None; } } From 7712ec0021efdd13a56863a25cf5571fbb2a89ec Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 13 Aug 2025 20:06:06 +0200 Subject: [PATCH 053/352] fix(context_drawer): adjust fill portion when max_width < 392 --- src/widget/context_drawer/widget.rs | 38 +++++++++++++++++------------ 1 file changed, 23 insertions(+), 15 deletions(-) diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index 23afae3b..e618fbcf 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -55,27 +55,35 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { .. } = crate::theme::spacing(); - let horizontal_padding = if max_width < 392.0 { space_s } else { space_l }; - - let title = - title.map(|title| text::heading(title).width(Length::FillPortion(3)).center()); - - let close_width = if title.is_some() { - Length::FillPortion(1) + let (horizontal_padding, title_portion, side_portion) = if max_width < 392.0 { + (space_s, 1, 1) } else { - Length::Shrink + (space_l, 2, 1) + }; + + let title = title.map(|title| { + text::heading(title) + .width(Length::FillPortion(title_portion)) + .center() + }); + + let (actions_width, close_width) = if title.is_some() { + ( + Length::FillPortion(side_portion), + Length::FillPortion(side_portion), + ) + } else { + (Length::Fill, Length::Shrink) }; let header_row = row::with_capacity(3) .width(Length::Fixed(480.0)) .align_y(Alignment::Center) - .push(row::with_children(header_actions).spacing(space_xxs).width( - if title.is_some() { - Length::FillPortion(1) - } else { - Length::Fill - }, - )) + .push( + row::with_children(header_actions) + .spacing(space_xxs) + .width(actions_width), + ) .push_maybe(title) .push( button::text("Close") From 8412dd593913b85618ec30e8b92a58aaa0ad6bb8 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 13 Aug 2025 21:39:29 +0200 Subject: [PATCH 054/352] fix(image_button): improve rendering of selected image buttons --- src/widget/button/widget.rs | 42 +++---------------------------------- 1 file changed, 3 insertions(+), 39 deletions(-) diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index da8612f7..3f5a1fdf 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -521,27 +521,6 @@ impl<'a, Message: 'a + Clone> Widget let c_rad = THEME.lock().unwrap().cosmic().corner_radii; - // NOTE: Workaround to round the border of the unselected, unhovered image. - if !self.selected && !is_mouse_over { - let mut bounds = bounds; - bounds.x -= 2.0; - bounds.y -= 2.0; - bounds.width += 4.0; - bounds.height += 4.0; - renderer.fill_quad( - renderer::Quad { - bounds, - border: Border { - width: 2.0, - color: crate::theme::active().current_container().base.into(), - radius: 9.0.into(), - }, - shadow: Shadow::default(), - }, - Color::TRANSPARENT, - ); - } - if self.selected { renderer.fill_quad( Quad { @@ -961,25 +940,10 @@ pub fn draw( let mut clipped_bounds = viewport_bounds.intersection(&bounds).unwrap_or_default(); clipped_bounds.height += styling.border_width; + clipped_bounds.width += 1.0; + // Finish by drawing the border above the contents. renderer.with_layer(clipped_bounds, |renderer| { - // NOTE: Workaround to round the border of the hovered/selected image. - if is_image { - renderer.fill_quad( - renderer::Quad { - bounds, - border: Border { - width: styling.border_width, - color: crate::theme::active().current_container().base.into(), - radius: 0.0.into(), - }, - shadow: Shadow::default(), - }, - Color::TRANSPARENT, - ); - } - - // Finish by drawing the border above the contents. renderer.fill_quad( renderer::Quad { bounds, @@ -992,7 +956,7 @@ pub fn draw( }, Color::TRANSPARENT, ); - }); + }) } else { draw_contents(renderer, styling); } From c10695600b070d86d12d1e775bfb7d2a6dca93c6 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 19 Aug 2025 11:13:28 +0200 Subject: [PATCH 055/352] feat(segmented_button): add FileNav style with related widget improvements --- src/theme/style/segmented_button.rs | 6 +- src/widget/segmented_button/horizontal.rs | 8 +- src/widget/segmented_button/widget.rs | 143 ++++++++++++++-------- 3 files changed, 100 insertions(+), 57 deletions(-) diff --git a/src/theme/style/segmented_button.rs b/src/theme/style/segmented_button.rs index 5306b3bf..b9863c88 100644 --- a/src/theme/style/segmented_button.rs +++ b/src/theme/style/segmented_button.rs @@ -18,6 +18,8 @@ pub enum SegmentedButton { Control, /// Navigation bar style NavBar, + /// File browser + FileNav, /// Or implement any custom theme of your liking. Custom(Box Appearance>), } @@ -69,7 +71,7 @@ impl StyleSheet for Theme { } } - SegmentedButton::NavBar => Appearance { + SegmentedButton::NavBar | SegmentedButton::FileNav => Appearance { active_width: 0.0, ..horizontal::tab_bar(cosmic, container) }, @@ -124,7 +126,7 @@ impl StyleSheet for Theme { } } - SegmentedButton::NavBar => Appearance { + SegmentedButton::NavBar | SegmentedButton::FileNav => Appearance { active_width: 0.0, ..vertical::tab_bar(cosmic, container) }, diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 724ded96..966f3a7c 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -67,7 +67,7 @@ where / num as f32; } - let segmetned_control = matches!(self.style, crate::theme::SegmentedButton::Control); + let is_control = matches!(self.style, crate::theme::SegmentedButton::Control); Box::new( self.model @@ -93,7 +93,7 @@ where let button_bounds = ItemBounds::Button(key, layout_bounds); let mut divider = None; - if self.dividers && segmetned_control && nth + 1 < num { + if self.dividers && is_control && nth + 1 < num { divider = Some(ItemBounds::Divider( Rectangle { width: 1.0, @@ -143,7 +143,7 @@ where let max_size = limits.height(Length::Fixed(max_height)).resolve( Length::Fill, max_height, - Size::new(f32::MAX, max_height), + Size::new(limits.max().width, max_height), ); let mut visible_width = 0.0; @@ -152,7 +152,7 @@ where for (button_size, _actual_size) in &state.internal_layout { visible_width += button_size.width; - if max_size.width >= visible_width { + if max_size.width - spacing >= visible_width { state.buttons_visible += 1; } else { visible_width = max_size.width - max_height; diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index d21f409b..685d3b0e 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -619,20 +619,19 @@ where for key in self.model.order.iter().copied() { if let Some(text) = self.model.text.get(key) { - let (font, button_state) = if self.button_is_focused(state, key) { - (self.font_active, 0) + let font = if self.button_is_focused(state, key) { + self.font_active } else if state.show_context.is_some() || self.button_is_hovered(state, key) { - (self.font_hovered, 1) + self.font_hovered } else if self.model.is_active(key) { - (self.font_active, 2) + self.font_active } else { - (self.font_inactive, 3) + self.font_inactive }; let mut hasher = DefaultHasher::new(); text.hash(&mut hasher); font.hash(&mut hasher); - button_state.hash(&mut hasher); let text_hash = hasher.finish(); if let Some(prev_hash) = state.text_hashes.insert(key, text_hash) { @@ -1293,6 +1292,15 @@ where ); } + let rad_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0; + + let divider_background = Background::Color( + crate::theme::active() + .cosmic() + .primary_component_divider() + .into(), + ); + // Draw each of the items in the widget. let mut nth = 0; self.variant_bounds(state, bounds).for_each(move |item| { @@ -1337,7 +1345,7 @@ where let key_is_active = self.model.is_active(key); let key_is_focused = state.focused_visible && self.button_is_focused(state, key); let key_is_hovered = self.button_is_hovered(state, key); - let status_appearance = if self.button_is_pressed(state, key) && key_is_hovered { + let status_appearance = if self.button_is_pressed(state, key) { appearance.pressed } else if key_is_hovered || menu_open() { appearance.hover @@ -1355,11 +1363,87 @@ where status_appearance.middle }; + // Draw the active hint on tabs + if appearance.active_width > 0.0 { + let active_width = if key_is_active { + appearance.active_width + } else { + 1.0 + }; + + renderer.fill_quad( + renderer::Quad { + bounds: if Self::VERTICAL { + Rectangle { + x: bounds.x + bounds.width - active_width, + width: active_width, + ..bounds + } + } else { + Rectangle { + y: bounds.y + bounds.height - active_width, + height: active_width, + ..bounds + } + }, + border: Border { + radius: rad_0.into(), + ..Default::default() + }, + shadow: Shadow::default(), + }, + appearance.active.text_color, + ); + } + + let original_bounds = bounds; + bounds.x += f32::from(self.button_padding[0]); + bounds.width -= f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]); + 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; + + // Draw indent line + if let crate::theme::SegmentedButton::FileNav = self.style { + if indent > 1 { + indent_padding = 7.0; + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x - self.indent_spacing as f32 + indent_padding, + width: 1.0, + ..bounds + }, + border: Border { + radius: rad_0.into(), + ..Default::default() + }, + shadow: Shadow::default(), + }, + divider_background, + ); + indent_padding += 4.0; + } + } + } + } + // Render the background of the button. if key_is_focused || status_appearance.background.is_some() { renderer.fill_quad( renderer::Quad { - bounds, + bounds: Rectangle { + x: bounds.x - f32::from(self.button_padding[0]) + indent_padding, + width: bounds.width + f32::from(self.button_padding[0]) + - f32::from(self.button_padding[2]) + - indent_padding, + ..bounds + }, border: if key_is_focused { Border { width: 1.0, @@ -1377,49 +1461,6 @@ where ); } - // Draw the active hint on tabs - if appearance.active_width > 0.0 { - let rad_0 = THEME.lock().unwrap().cosmic().corner_radii.radius_0; - let active_width = if key_is_active { - appearance.active_width - } else { - 1.0 - }; - let mut bounds = bounds; - - if Self::VERTICAL { - bounds.x += bounds.height - active_width; - bounds.width = active_width; - } else { - bounds.y += bounds.height - active_width; - bounds.height = active_width; - } - - renderer.fill_quad( - renderer::Quad { - bounds, - border: Border { - radius: rad_0.into(), - ..Default::default() - }, - shadow: Shadow::default(), - }, - appearance.active.text_color, - ); - } - - let original_bounds = bounds; - - bounds.x += f32::from(self.button_padding[0]); - bounds.width -= f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]); - - // Adjust bounds by indent - if let Some(indent) = self.model.indent(key) { - let adjustment = f32::from(indent) * f32::from(self.indent_spacing); - bounds.x += adjustment; - bounds.width -= adjustment; - } - // Align contents of the button to the requested `button_alignment`. { let actual_width = state.internal_layout[nth].1.width; From 6e7a6343981df7d86f7ab01fe102d0b69d8e3bed Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 19 Aug 2025 16:31:19 +0200 Subject: [PATCH 056/352] fix(segmented_button): draw all indent levels --- src/widget/segmented_button/widget.rs | 34 ++++++++++++++++----------- 1 file changed, 20 insertions(+), 14 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 685d3b0e..0fd8dcd6 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -1412,21 +1412,27 @@ where if let crate::theme::SegmentedButton::FileNav = self.style { if indent > 1 { indent_padding = 7.0; - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x - self.indent_spacing as f32 + indent_padding, - width: 1.0, - ..bounds + + for level in 1..indent { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + - (level as f32 * self.indent_spacing as f32) + + indent_padding, + width: 1.0, + ..bounds + }, + border: Border { + radius: rad_0.into(), + ..Default::default() + }, + shadow: Shadow::default(), }, - border: Border { - radius: rad_0.into(), - ..Default::default() - }, - shadow: Shadow::default(), - }, - divider_background, - ); + divider_background, + ); + } + indent_padding += 4.0; } } From e7b7c3a1261c6a3b4d38b1c11c3fd119810236ac Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 20 Aug 2025 16:09:17 +0200 Subject: [PATCH 057/352] improv: enable dbus-config by default, but only for Linux targets --- Cargo.toml | 13 +++++++++---- src/app/cosmic.rs | 2 +- src/core.rs | 8 ++++---- src/theme/mod.rs | 3 --- 4 files changed, 14 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 87ad0867..c55d2c44 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,7 +8,7 @@ rust-version = "1.85" name = "cosmic" [features] -default = ["multi-window", "a11y"] +default = ["dbus-config", "multi-window", "a11y"] # Accessibility support a11y = ["iced/a11y", "iced_accessibility"] # Enable about widget @@ -27,8 +27,8 @@ applet = [ "multi-window", ] applet-token = ["applet"] -# Use the cosmic-settings-daemon for config handling -dbus-config = ["cosmic-config/dbus", "dep:zbus", "cosmic-settings-daemon"] +# Use the cosmic-settings-daemon for config handling on Linux targets +dbus-config = [] # Debug features debug = ["iced/debug"] # Enables pipewire support in ashpd, if ashpd is enabled @@ -107,7 +107,6 @@ cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-c chrono = "0.4.40" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } -cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } css-color = "0.2.8" derive_setters = "0.1.6" futures = "0.3" @@ -135,6 +134,12 @@ unicode-segmentation = "1.12" url = "2.5.4" zbus = { version = "5.7.1", default-features = false, optional = true } +# 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.7.1", default-features = false } + [target.'cfg(unix)'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } freedesktop-desktop-entry = { version = "0.7.11", optional = true } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index c8a8dfeb..9814cf70 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -101,7 +101,7 @@ where pub fn init( (mut core, flags): (Core, T::Flags), ) -> (Self, iced::Task>) { - #[cfg(feature = "dbus-config")] + #[cfg(all(feature = "dbus-config", target_os = "linux"))] { use iced_futures::futures::executor::block_on; core.settings_daemon = block_on(cosmic_config::dbus::settings_daemon_proxy()).ok(); diff --git a/src/core.rs b/src/core.rs index 4b9811ec..c82aa839 100644 --- a/src/core.rs +++ b/src/core.rs @@ -89,7 +89,7 @@ pub struct Core { #[cfg(feature = "single-instance")] pub(crate) single_instance: bool, - #[cfg(feature = "dbus-config")] + #[cfg(all(feature = "dbus-config", target_os = "linux"))] pub(crate) settings_daemon: Option>, pub(crate) main_window: Option, @@ -146,7 +146,7 @@ impl Default for Core { applet: crate::applet::Context::default(), #[cfg(feature = "single-instance")] single_instance: false, - #[cfg(feature = "dbus-config")] + #[cfg(all(feature = "dbus-config", target_os = "linux"))] settings_daemon: None, portal_is_dark: None, portal_accent: None, @@ -353,7 +353,7 @@ impl Core { &self, config_id: &'static str, ) -> iced::Subscription> { - #[cfg(feature = "dbus-config")] + #[cfg(all(feature = "dbus-config", target_os = "linux"))] if let Some(settings_daemon) = self.settings_daemon.clone() { return cosmic_config::dbus::watcher_subscription(settings_daemon, config_id, false); } @@ -370,7 +370,7 @@ impl Core { &self, state_id: &'static str, ) -> iced::Subscription> { - #[cfg(feature = "dbus-config")] + #[cfg(all(feature = "dbus-config", target_os = "linux"))] if let Some(settings_daemon) = self.settings_daemon.clone() { return cosmic_config::dbus::watcher_subscription(settings_daemon, state_id, true); } diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 5e335f59..9c4e7d53 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -18,9 +18,6 @@ use iced_runtime::{Appearance, DefaultStyle}; use std::sync::{Arc, Mutex}; pub use style::*; -#[cfg(feature = "dbus-config")] -use cosmic_config::dbus; - pub type CosmicColor = ::palette::rgb::Srgba; pub type CosmicComponent = cosmic_theme::Component; pub type CosmicTheme = cosmic_theme::Theme; From ba2f4b193a26663e58359a55d394446dc00501e6 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 19 Aug 2025 23:13:43 -0400 Subject: [PATCH 058/352] fix(theme): control tint colors need to be reversed for light theme --- cosmic-theme/src/model/theme.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index aad71228..7bfd41c5 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1004,15 +1004,14 @@ impl ThemeBuilder { let text_steps_array = text_tint.map(|c| steps(c, NonZeroUsize::new(100).unwrap())); - let control_steps_array = if let Some(neutral_tint) = neutral_tint { - let mut neutral_steps_arr = steps(neutral_tint, NonZeroUsize::new(11).unwrap()); - if !is_dark { - neutral_steps_arr.reverse(); - } - neutral_steps_arr + let mut control_steps_array = if let Some(neutral_tint) = neutral_tint { + steps(neutral_tint, NonZeroUsize::new(11).unwrap()) } else { steps(palette.as_ref().neutral_2, NonZeroUsize::new(11).unwrap()) }; + if !is_dark { + control_steps_array.reverse(); + } let p_ref = palette.as_ref(); From 29f38f83a38b550ae0de2b130fde9f2c36341fab Mon Sep 17 00:00:00 2001 From: Soso <51865119+sgued@users.noreply.github.com> Date: Wed, 20 Aug 2025 17:33:55 +0200 Subject: [PATCH 059/352] fix(about): wrong icon size in about widget --- src/widget/about.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/widget/about.rs b/src/widget/about.rs index 6590bb9d..13fcea23 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -137,7 +137,13 @@ pub fn about<'a, Message: Clone + 'static>( }; let application_name = about.name.as_ref().map(widget::text::title3); - let application_icon = about.icon.as_ref().map(|i| i.clone().icon()); + let application_icon = about.icon.as_ref().map(|i| { + i.clone() + .icon() + .content_fit(iced::ContentFit::Contain) + .width(Length::Fixed(128.)) + .height(Length::Fixed(128.)) + }); let author = about.author.as_ref().map(widget::text::body); let version = about.version.as_ref().map(widget::button::standard); let links_section = section(&about.links, "Links"); From 8415d77b0aa4035a7c21189d350b23f5203559b2 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 21 Aug 2025 10:51:36 -0600 Subject: [PATCH 060/352] feat(settings/section): support custom header widgets --- src/widget/settings/section.rs | 22 ++++++++++------------ 1 file changed, 10 insertions(+), 12 deletions(-) diff --git a/src/widget/settings/section.rs b/src/widget/settings/section.rs index bc885005..899826dc 100644 --- a/src/widget/settings/section.rs +++ b/src/widget/settings/section.rs @@ -8,10 +8,7 @@ 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.into(), - children: ListColumn::default(), - } + section().title(title) } /// A section within a settings view column. @@ -22,21 +19,26 @@ pub fn section<'a, Message: 'static>() -> Section<'a, Message> { /// A section with a pre-defined list column. pub fn with_column(children: ListColumn<'_, Message>) -> Section<'_, Message> { Section { - title: Cow::Borrowed(""), + header: None, children, } } #[must_use] pub struct Section<'a, Message> { - title: Cow<'a, str>, + header: Option>, children: ListColumn<'a, Message>, } impl<'a, Message: 'static> Section<'a, Message> { /// Define an optional title for the section. pub fn title(mut self, title: impl Into>) -> Self { - self.title = title.into(); + self.header(text::heading(title.into())) + } + + /// Define an optional custom header for the section. + pub fn header(mut self, header: impl Into>) -> Self { + self.header = Some(header.into()); self } @@ -69,11 +71,7 @@ impl<'a, Message: 'static> From> for Element<'a, Message> { fn from(data: Section<'a, Message>) -> Self { column::with_capacity(2) .spacing(8) - .push_maybe(if data.title.is_empty() { - None - } else { - Some(text::heading(data.title)) - }) + .push_maybe(data.header) .push(data.children) .into() } From 2d62503fdf042a215ebb9647e8a69f2d6dbde218 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 22 Aug 2025 13:41:12 -0700 Subject: [PATCH 061/352] fix: don't error when default config for toolkit settings is not present --- src/app/cosmic.rs | 6 ++++++ src/config/mod.rs | 6 ++++++ 2 files changed, 12 insertions(+) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 9814cf70..17331832 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -369,6 +369,12 @@ where .into_iter() .filter(cosmic_config::Error::is_err) { + if let cosmic_config::Error::GetKey(_, err) = &why { + if err.kind() == std::io::ErrorKind::NotFound { + // No system default config installed; don't error + continue; + } + } tracing::error!(?why, "cosmic toolkit config update error"); } diff --git a/src/config/mod.rs b/src/config/mod.rs index dedadbc2..1253ce8d 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -25,6 +25,12 @@ pub static COSMIC_TK: LazyLock> = LazyLock::new(|| { .map(|c| { CosmicTk::get_entry(&c).unwrap_or_else(|(errors, mode)| { for why in errors.into_iter().filter(cosmic_config::Error::is_err) { + if let cosmic_config::Error::GetKey(_, err) = &why { + if err.kind() == std::io::ErrorKind::NotFound { + // No system default config installed; don't error + continue; + } + } tracing::error!(?why, "CosmicTk config entry error"); } mode From 66a2632e2ee72a1e3a9888cd5638821f6af2f861 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 22 Aug 2025 14:40:03 -0700 Subject: [PATCH 062/352] fix(cosmic-config): Fixes for error printing * Use `tracing::error!` in places where `eprintln!` was used * Loop over errors and print seperately * Print errors with `Display` rather than `Debug` * Don't print errors that should be ignored - Matches https://github.com/pop-os/libcosmic/pull/949, for same reasons. With this, and the previous change, cosmic-panel no longer spams a bunch of config errors from different applets on start. --- cosmic-config/src/dbus.rs | 16 ++++++++++++---- 1 file changed, 12 insertions(+), 4 deletions(-) diff --git a/cosmic-config/src/dbus.rs b/cosmic-config/src/dbus.rs index e66d8556..f8256bc3 100644 --- a/cosmic-config/src/dbus.rs +++ b/cosmic-config/src/dbus.rs @@ -156,8 +156,16 @@ fn watcher_stream config, Err((errors, default)) => { - if !errors.is_empty() { - eprintln!("Error getting config: {config_id} {errors:?}"); + 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 } @@ -171,7 +179,7 @@ fn watcher_stream Date: Mon, 25 Aug 2025 11:30:40 -0400 Subject: [PATCH 063/352] chore: update libcosmic --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 13134181..ebbfd6ca 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 13134181f8d5cfeaee4fb52172e12985b06af1cf +Subproject commit ebbfd6ca6be1e2b2e73f390ef2beefd31a4d868a From 94ee4e1915d05807c041c8770d27c6e1ec41ec05 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 26 Aug 2025 15:13:15 -0400 Subject: [PATCH 064/352] theme: fix disabled button --- cosmic-theme/src/composite.rs | 14 ++++---------- cosmic-theme/src/model/derivation.rs | 4 ++-- 2 files changed, 6 insertions(+), 12 deletions(-) diff --git a/cosmic-theme/src/composite.rs b/cosmic-theme/src/composite.rs index c30469b2..66d7ac92 100644 --- a/cosmic-theme/src/composite.rs +++ b/cosmic-theme/src/composite.rs @@ -4,16 +4,10 @@ use palette::Srgba; pub fn over, B: Into>(a: A, b: B) -> Srgba { let a = a.into(); let b = b.into(); - let o_a = (alpha_over(a.alpha, b.alpha)).max(0.0).min(1.0); - let o_r = (c_over(a.red, b.red, a.alpha, b.alpha, o_a)) - .max(0.0) - .min(1.0); - let o_g = (c_over(a.green, b.green, a.alpha, b.alpha, o_a)) - .max(0.0) - .min(1.0); - let o_b = (c_over(a.blue, b.blue, a.alpha, b.alpha, o_a)) - .max(0.0) - .min(1.0); + let o_a = (alpha_over(a.alpha, b.alpha)).clamp(0.0, 1.0); + let o_r = (c_over(a.red, b.red, a.alpha, b.alpha, o_a)).clamp(0.0, 1.0); + let o_g = (c_over(a.green, b.green, a.alpha, b.alpha, o_a)).clamp(0.0, 1.0); + let o_b = (c_over(a.blue, b.blue, a.alpha, b.alpha, o_a)).clamp(0.0, 1.0); Srgba::new(o_r, o_g, o_b, o_a) } diff --git a/cosmic-theme/src/model/derivation.rs b/cosmic-theme/src/model/derivation.rs index bcc4990f..2944af40 100644 --- a/cosmic-theme/src/model/derivation.rs +++ b/cosmic-theme/src/model/derivation.rs @@ -194,8 +194,8 @@ impl Component { focus: accent, divider: if is_high_contrast { on_50 } else { on_20 }, on: on_component, - disabled: over(base_50, base), - on_disabled: over(on_50, base), + disabled: base_50, + on_disabled: on_50, border, disabled_border, } From 364c0b938183af799739abd3e870be15601ca727 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 26 Aug 2025 16:00:26 -0400 Subject: [PATCH 065/352] refactor(theme): .65 opacity for disabled button text --- cosmic-theme/src/model/derivation.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cosmic-theme/src/model/derivation.rs b/cosmic-theme/src/model/derivation.rs index 2944af40..dce653e5 100644 --- a/cosmic-theme/src/model/derivation.rs +++ b/cosmic-theme/src/model/derivation.rs @@ -168,7 +168,7 @@ impl Component { base_50.alpha *= 0.5; let on_20 = on_component.with_alpha(0.2); - let on_50 = on_20.with_alpha(0.5); + let on_65 = on_20.with_alpha(0.65); let mut disabled_border = border; disabled_border.alpha *= 0.5; @@ -192,10 +192,10 @@ impl Component { }, selected_text: accent, focus: accent, - divider: if is_high_contrast { on_50 } else { on_20 }, + divider: if is_high_contrast { on_65 } else { on_20 }, on: on_component, disabled: base_50, - on_disabled: on_50, + on_disabled: on_65, border, disabled_border, } From 4d06524f2c0a2c9fc8911f8aefae0bfb2b3e13d7 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 2 Sep 2025 12:01:05 -0400 Subject: [PATCH 066/352] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index ebbfd6ca..567b4a09 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit ebbfd6ca6be1e2b2e73f390ef2beefd31a4d868a +Subproject commit 567b4a0973d7a1797e7581f896c1aee236142f32 From 2dd6dce0533118104052594cce250314975453bc Mon Sep 17 00:00:00 2001 From: Tony4dev <78384793+Tony4dev@users.noreply.github.com> Date: Wed, 3 Sep 2025 12:49:35 +0000 Subject: [PATCH 067/352] improv(about): support custom license URLs --- examples/about/src/main.rs | 1 + src/widget/about.rs | 14 +++++++++----- 2 files changed, 10 insertions(+), 5 deletions(-) diff --git a/examples/about/src/main.rs b/examples/about/src/main.rs index 5450b47e..957433f0 100644 --- a/examples/about/src/main.rs +++ b/examples/about/src/main.rs @@ -71,6 +71,7 @@ impl cosmic::Application for App { .version("0.1.0") .author("System 76") .license("GPL-3.0-only") + //.license_url("https://www.some-custom-license-url.com") .developers([("Michael Murphy", "mmstick@system76.com")]) .links([ ("Website", "https://system76.com/cosmic"), diff --git a/src/widget/about.rs b/src/widget/about.rs index 13fcea23..aea92991 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -25,6 +25,8 @@ pub struct About { copyright: Option, /// The license name. license: Option, + /// The license url. If None spdx.org url is used. + license_url: Option, /// Artists who contributed to the application. #[setters(skip)] artists: Vec<(String, String)>, @@ -95,10 +97,12 @@ impl<'a> About { self } - fn license_url(&self) -> Option { - self.license.as_ref().and_then(|license_str| { - let license: &dyn License = license_str.parse().ok()?; - Some(format!("https://spdx.org/licenses/{}.html", license.id())) + fn get_license_url(&self) -> Option { + self.license_url.clone().or_else(|| { + self.license.as_ref().and_then(|license_str| { + let license: &dyn License = license_str.parse().ok()?; + Some(format!("https://spdx.org/licenses/{}.html", license.id())) + }) }) } } @@ -153,7 +157,7 @@ pub fn about<'a, Message: Clone + 'static>( let translators_section = section(&about.translators, "Translators"); let documenters_section = section(&about.documenters, "Documenters"); let license = about.license.as_ref().map(|license| { - let url = about.license_url(); + let url = about.get_license_url(); widget::settings::section().title("License").add( widget::button::custom( widget::row() From f5f7c14f0375453abae8f9e9dab126fc2e637757 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 3 Sep 2025 13:31:36 -0400 Subject: [PATCH 068/352] chore: update cctk --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index c55d2c44..4e9bf983 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,7 @@ ashpd = { version = "0.11.0", default-features = false, optional = true } async-fs = { version = "2.1", 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 = "178eb0b", optional = true } +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "6254f50", optional = true } chrono = "0.4.40" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } From c5df9dcf889fcb425bb96c15d4fd46e08690e58b Mon Sep 17 00:00:00 2001 From: UchiWerfer <87275644+UchiWerfer@users.noreply.github.com> Date: Wed, 3 Sep 2025 17:35:37 +0000 Subject: [PATCH 069/352] fix(calendar): show button icons on non-Linux targets --- res/icons/go-next-symbolic.svg | 3 +++ res/icons/go-previous-symbolic.svg | 3 +++ src/widget/calendar.rs | 34 +++++++++++++++++++++--------- 3 files changed, 30 insertions(+), 10 deletions(-) create mode 100644 res/icons/go-next-symbolic.svg create mode 100644 res/icons/go-previous-symbolic.svg diff --git a/res/icons/go-next-symbolic.svg b/res/icons/go-next-symbolic.svg new file mode 100644 index 00000000..3aed3717 --- /dev/null +++ b/res/icons/go-next-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/res/icons/go-previous-symbolic.svg b/res/icons/go-previous-symbolic.svg new file mode 100644 index 00000000..4957cffd --- /dev/null +++ b/res/icons/go-previous-symbolic.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index 83b1dcfd..02b98cfb 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -7,6 +7,7 @@ use std::cmp; use crate::iced_core::{Alignment, Length, Padding}; use crate::widget::{Grid, button, column, grid, icon, row, text}; +use apply::Apply; use chrono::{Datelike, Days, Local, Months, NaiveDate, Weekday}; /// A widget that displays an interactive calendar. @@ -115,19 +116,32 @@ where Message: Clone + 'static, { fn from(this: Calendar<'a, Message>) -> Self { + macro_rules! icon { + ($name:expr, $on_press:expr) => {{ + #[cfg(target_os = "linux")] + let icon = { + icon::from_name($name) + .apply(button::icon) + }; + #[cfg(not(target_os = "linux"))] + let icon = { + icon::from_svg_bytes(include_bytes!(concat!( + "../../res/icons/", + $name, + ".svg" + ))) + .symbolic(true) + .apply(button::icon) + }; + icon.padding([0, 12]) + .on_press($on_press) + }}; + } let date = text(this.model.visible.format("%B %Y").to_string()).size(18); let month_controls = row::with_capacity(2) - .push( - button::icon(icon::from_name("go-previous-symbolic")) - .padding([0, 12]) - .on_press((this.on_prev)()), - ) - .push( - button::icon(icon::from_name("go-next-symbolic")) - .padding([0, 12]) - .on_press((this.on_next)()), - ); + .push(icon!("go-previous-symbolic", (this.on_prev)())) + .push(icon!("go-next-symbolic", (this.on_next)())); // Calender let mut calendar_grid: Grid<'_, Message> = From b72b15d71961e06bcdaed43d1f2c66113eb565b1 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, 3 Sep 2025 20:08:49 +0200 Subject: [PATCH 070/352] chore: update dependencies --- Cargo.toml | 28 ++++++++++++++-------------- cosmic-config/Cargo.toml | 16 ++++++++-------- cosmic-config/src/lib.rs | 8 ++------ cosmic-theme/Cargo.toml | 8 ++++---- 4 files changed, 28 insertions(+), 32 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 4e9bf983..a8fcdae7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,50 +99,50 @@ async-std = [ [dependencies] apply = "0.3.0" -ashpd = { version = "0.11.0", default-features = false, optional = true } +ashpd = { version = "0.12.0", default-features = false, optional = true } async-fs = { version = "2.1", 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 = "6254f50", optional = true } -chrono = "0.4.40" +chrono = "0.4.41" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } css-color = "0.2.8" -derive_setters = "0.1.6" +derive_setters = "0.1.8" futures = "0.3" -image = { version = "0.25.5", default-features = false, features = [ +image = { version = "0.25.8", default-features = false, features = [ "jpeg", "png", ] } lazy_static = "1.5.0" -libc = { version = "0.2.171", optional = true } -license = { version = "3.6.0", optional = true } +libc = { version = "0.2.175", optional = true } +license = { version = "3.7.0", optional = true } mime = { version = "0.3.17", optional = true } palette = "0.7.6" raw-window-handle = "0.6" -rfd = { version = "0.15.3", default-features = false, features = [ +rfd = { version = "0.15.4", default-features = false, features = [ "xdg-portal", ], optional = true } rustix = { version = "1.0", features = ["pipe", "process"], optional = true } serde = { version = "1.0.219", features = ["derive"] } slotmap = "1.0.7" smol = { version = "2.0.2", optional = true } -thiserror = "2.0.12" -tokio = { version = "1.44.1", optional = true } +thiserror = "2.0.16" +tokio = { version = "1.47.1", optional = true } tracing = "0.1.41" unicode-segmentation = "1.12" -url = "2.5.4" -zbus = { version = "5.7.1", default-features = false, optional = true } +url = "2.5.7" +zbus = { version = "5.10.0", default-features = false, optional = true } # 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.7.1", default-features = false } +zbus = { version = "5.10.0", default-features = false } [target.'cfg(unix)'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } -freedesktop-desktop-entry = { version = "0.7.11", optional = true } +freedesktop-desktop-entry = { version = "0.7.14", optional = true } shlex = { version = "1.3.0", optional = true } [dependencies.cosmic-theme] @@ -197,7 +197,7 @@ git = "https://github.com/pop-os/cosmic-panel" optional = true [dependencies.ron] -version = "0.9" +version = "0.11" optional = true [dependencies.taffy] diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index a79237c8..4d7b99e1 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -11,24 +11,24 @@ subscription = ["iced_futures"] [dependencies] cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } -zbus = { version = "5.7.1", default-features = false, optional = true } +zbus = { version = "5.10.0", default-features = false, optional = true } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } -calloop = { version = "0.14.2", optional = true } -notify = "8.0.0" -ron = "0.9.0" +calloop = { version = "0.14.3", optional = true } +notify = "8.2.0" +ron = "0.11.0" serde = "1.0.219" cosmic-config-derive = { path = "../cosmic-config-derive/", optional = true } iced = { path = "../iced/", default-features = false, optional = true } iced_futures = { path = "../iced/futures/", default-features = false, optional = true } -once_cell = "1.21.1" +once_cell = "1.21.3" futures-util = { version = "0.3", optional = true } dirs.workspace = true -tokio = { version = "1.44", optional = true, features = ["time"] } +tokio = { version = "1.47", optional = true, features = ["time"] } async-std = { version = "1.13", optional = true } tracing = "0.1" [target.'cfg(unix)'.dependencies] -xdg = "2.5" +xdg = "3.0" [target.'cfg(windows)'.dependencies] -known-folders = "1.2.0" +known-folders = "1.3.1" diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index e408eac5..8759a527 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -140,9 +140,7 @@ impl Config { pub fn system(name: &str, version: u64) -> Result { let path = sanitize_name(name)?.join(format!("v{version}")); #[cfg(unix)] - let system_path = xdg::BaseDirectories::with_prefix("cosmic") - .map_err(std::io::Error::from)? - .find_data_file(path); + let system_path = xdg::BaseDirectories::with_prefix("cosmic").find_data_file(path); #[cfg(windows)] let system_path = @@ -164,9 +162,7 @@ impl Config { // Search data file, which provides default (e.g. /usr/share) #[cfg(unix)] - let system_path = xdg::BaseDirectories::with_prefix("cosmic") - .map_err(std::io::Error::from)? - .find_data_file(&path); + let system_path = xdg::BaseDirectories::with_prefix("cosmic").find_data_file(&path); #[cfg(windows)] let system_path = diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 483014f6..bb735342 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -18,15 +18,15 @@ no-default = [] palette = { version = "0.7.6", features = ["serializing"] } almost = "0.2" serde = { version = "1.0.219", features = ["derive"] } -serde_json = { version = "1.0.140", optional = true, features = [ +serde_json = { version = "1.0.143", optional = true, features = [ "preserve_order", ] } -ron = "0.9.0" +ron = "0.11.0" lazy_static = "1.5.0" -csscolorparser = { version = "0.7.0", features = ["serde"] } +csscolorparser = { version = "0.7.2", features = ["serde"] } cosmic-config = { path = "../cosmic-config/", default-features = false, features = [ "subscription", "macro", ] } dirs.workspace = true -thiserror = "2.0.12" +thiserror = "2.0.16" From ea349aca82eecd1edabee376203608f94851d8dc 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, 3 Sep 2025 20:22:06 +0200 Subject: [PATCH 071/352] chore: use `std::syncLazyLock` Also migrates workspace members to Rust 2024. --- Cargo.toml | 1 - cosmic-config/Cargo.toml | 3 +- cosmic-config/src/dbus.rs | 5 +-- cosmic-config/src/lib.rs | 4 +-- cosmic-config/src/subscription.rs | 2 +- cosmic-theme/Cargo.toml | 3 +- cosmic-theme/src/model/cosmic_palette.rs | 17 +++++---- cosmic-theme/src/model/theme.rs | 6 ++-- cosmic-theme/src/output/gtk4_output.rs | 6 ++-- cosmic-theme/src/output/mod.rs | 2 +- cosmic-theme/src/output/vs_code.rs | 2 +- cosmic-theme/src/steps.rs | 2 +- src/theme/mod.rs | 46 +++++++++++++----------- src/widget/calendar.rs | 18 +++------- src/widget/color_picker/mod.rs | 18 +++++----- 15 files changed, 64 insertions(+), 71 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index a8fcdae7..33bf7c49 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -114,7 +114,6 @@ image = { version = "0.25.8", default-features = false, features = [ "jpeg", "png", ] } -lazy_static = "1.5.0" libc = { version = "0.2.175", optional = true } license = { version = "3.7.0", optional = true } mime = { version = "0.3.17", optional = true } diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 4d7b99e1..e838f9b5 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cosmic-config" version = "0.1.0" -edition = "2021" +edition = "2024" [features] default = ["macro", "subscription"] @@ -20,7 +20,6 @@ serde = "1.0.219" cosmic-config-derive = { path = "../cosmic-config-derive/", optional = true } iced = { path = "../iced/", default-features = false, optional = true } iced_futures = { path = "../iced/futures/", default-features = false, optional = true } -once_cell = "1.21.3" futures-util = { version = "0.3", optional = true } dirs.workspace = true tokio = { version = "1.47", optional = true, features = ["time"] } diff --git a/cosmic-config/src/dbus.rs b/cosmic-config/src/dbus.rs index f8256bc3..e9e3395c 100644 --- a/cosmic-config/src/dbus.rs +++ b/cosmic-config/src/dbus.rs @@ -4,8 +4,9 @@ use crate::{CosmicConfigEntry, Update}; use cosmic_settings_daemon::{Changed, ConfigProxy, CosmicSettingsDaemonProxy}; use futures_util::SinkExt; use iced_futures::{ - futures::{self, future::pending, Stream, StreamExt}, - stream, Subscription, + Subscription, + futures::{self, Stream, StreamExt, future::pending}, + stream, }; pub async fn settings_daemon_proxy() -> zbus::Result> { diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 8759a527..72b02371 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -1,10 +1,10 @@ //! Integrations for cosmic-config — the cosmic configuration system. use notify::{ - event::{EventKind, ModifyKind, RenameMode}, RecommendedWatcher, Watcher, + event::{EventKind, ModifyKind, RenameMode}, }; -use serde::{de::DeserializeOwned, Serialize}; +use serde::{Serialize, de::DeserializeOwned}; use std::{ fmt, fs, io::Write, diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs index 64255954..88f8bfa2 100644 --- a/cosmic-config/src/subscription.rs +++ b/cosmic-config/src/subscription.rs @@ -62,7 +62,7 @@ async fn start_listening, output: &mut mpsc::Sender>, ) -> ConfigState { - use iced_futures::futures::{future::pending, StreamExt}; + use iced_futures::futures::{StreamExt, future::pending}; match state { ConfigState::Init(config_id, version, is_state) => { diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index bb735342..44b0df5a 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cosmic-theme" version = "0.1.0" -edition = "2021" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -22,7 +22,6 @@ serde_json = { version = "1.0.143", optional = true, features = [ "preserve_order", ] } ron = "0.11.0" -lazy_static = "1.5.0" csscolorparser = { version = "0.7.2", features = ["serde"] } cosmic-config = { path = "../cosmic-config/", default-features = false, features = [ "subscription", diff --git a/cosmic-theme/src/model/cosmic_palette.rs b/cosmic-theme/src/model/cosmic_palette.rs index 6a189089..3852742b 100644 --- a/cosmic-theme/src/model/cosmic_palette.rs +++ b/cosmic-theme/src/model/cosmic_palette.rs @@ -1,15 +1,14 @@ -use lazy_static::lazy_static; use palette::Srgba; use serde::{Deserialize, Serialize}; +use std::sync::LazyLock; -lazy_static! { - /// built in light palette - pub static ref LIGHT_PALETTE: CosmicPalette = - ron::from_str(include_str!("light.ron")).unwrap(); - /// built in dark palette - pub static ref DARK_PALETTE: CosmicPalette = - ron::from_str(include_str!("dark.ron")).unwrap(); -} +/// built-in light palette +pub static LIGHT_PALETTE: LazyLock = + LazyLock::new(|| ron::from_str(include_str!("light.ron")).unwrap()); + +/// built-in dark palette +pub static DARK_PALETTE: LazyLock = + LazyLock::new(|| ron::from_str(include_str!("dark.ron")).unwrap()); /// Palette type #[derive(Clone, Debug, Deserialize, Serialize, PartialEq)] diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 7bfd41c5..d1d3ae0a 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1,12 +1,12 @@ use crate::{ + Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, DARK_PALETTE, + LIGHT_PALETTE, NAME, Spacing, ThemeMode, composite::over, steps::{color_index, get_small_widget_color, get_surface_color, get_text, steps}, - Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, Spacing, ThemeMode, - DARK_PALETTE, LIGHT_PALETTE, NAME, }; use cosmic_config::{Config, CosmicConfigEntry}; use palette::{ - color_difference::Wcag21RelativeContrast, rgb::Rgb, IntoColor, Oklcha, Srgb, Srgba, WithAlpha, + IntoColor, Oklcha, Srgb, Srgba, WithAlpha, color_difference::Wcag21RelativeContrast, rgb::Rgb, }; use serde::{Deserialize, Serialize}; use std::num::NonZeroUsize; diff --git a/cosmic-theme/src/output/gtk4_output.rs b/cosmic-theme/src/output/gtk4_output.rs index 9d7210f0..df6aca6a 100644 --- a/cosmic-theme/src/output/gtk4_output.rs +++ b/cosmic-theme/src/output/gtk4_output.rs @@ -1,5 +1,5 @@ -use crate::{composite::over, steps::steps, Component, Theme}; -use palette::{rgb::Rgba, Darken, IntoColor, Lighten, Srgba, WithAlpha}; +use crate::{Component, Theme, composite::over, steps::steps}; +use palette::{Darken, IntoColor, Lighten, Srgba, WithAlpha, rgb::Rgba}; use std::{ fs::{self, File}, io::{self, Write}, @@ -7,7 +7,7 @@ use std::{ path::Path, }; -use super::{to_rgba, OutputError}; +use super::{OutputError, to_rgba}; impl Theme { #[must_use] diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index f2eb6b4b..832771d4 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -1,4 +1,4 @@ -use palette::{rgb::Rgba, Srgba}; +use palette::{Srgba, rgb::Rgba}; use thiserror::Error; use crate::Theme; diff --git a/cosmic-theme/src/output/vs_code.rs b/cosmic-theme/src/output/vs_code.rs index 5c770cd6..b07c82e1 100644 --- a/cosmic-theme/src/output/vs_code.rs +++ b/cosmic-theme/src/output/vs_code.rs @@ -2,7 +2,7 @@ use serde::{Deserialize, Serialize}; use crate::Theme; -use super::{to_hex, OutputError}; +use super::{OutputError, to_hex}; /// Represents the workbench.colorCustomizations section of a VS Code settings.json file #[derive(Debug, Clone, Serialize, Deserialize)] diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs index 506b6fa8..6c0779c2 100644 --- a/cosmic-theme/src/steps.rs +++ b/cosmic-theme/src/steps.rs @@ -1,7 +1,7 @@ use std::num::NonZeroUsize; use almost::equal; -use palette::{convert::FromColorUnclamped, ClampAssign, FromColor, Lch, Oklcha, Srgb, Srgba}; +use palette::{ClampAssign, FromColor, Lch, Oklcha, Srgb, Srgba, convert::FromColorUnclamped}; /// Get an array of 100 colors with a specific hue and chroma /// over the full range of lightness. diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 9c4e7d53..f01180c1 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -15,33 +15,37 @@ use cosmic_theme::Spacing; use cosmic_theme::ThemeMode; use iced_futures::Subscription; use iced_runtime::{Appearance, DefaultStyle}; -use std::sync::{Arc, Mutex}; +use std::sync::{Arc, LazyLock, Mutex}; pub use style::*; pub type CosmicColor = ::palette::rgb::Srgba; pub type CosmicComponent = cosmic_theme::Component; pub type CosmicTheme = cosmic_theme::Theme; -lazy_static::lazy_static! { - pub static ref COSMIC_DARK: CosmicTheme = CosmicTheme::dark_default(); - pub static ref COSMIC_HC_DARK: CosmicTheme = CosmicTheme::high_contrast_dark_default(); - pub static ref COSMIC_LIGHT: CosmicTheme = CosmicTheme::light_default(); - pub static ref COSMIC_HC_LIGHT: CosmicTheme = CosmicTheme::high_contrast_light_default(); - pub static ref TRANSPARENT_COMPONENT: Component = Component { - base: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - hover: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - pressed: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - selected: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - selected_text: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - focus: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - disabled: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - on: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - on_disabled: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - divider: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - border: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - disabled_border: CosmicColor::new(0.0, 0.0, 0.0, 0.0), - }; -} +pub static COSMIC_DARK: LazyLock = LazyLock::new(|| CosmicTheme::dark_default()); + +pub static COSMIC_HC_DARK: LazyLock = + LazyLock::new(|| CosmicTheme::high_contrast_dark_default()); + +pub static COSMIC_LIGHT: LazyLock = LazyLock::new(|| CosmicTheme::light_default()); + +pub static COSMIC_HC_LIGHT: LazyLock = + LazyLock::new(|| CosmicTheme::high_contrast_light_default()); + +pub static TRANSPARENT_COMPONENT: LazyLock = LazyLock::new(|| Component { + base: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + hover: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + pressed: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + selected: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + selected_text: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + focus: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + disabled: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + on: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + on_disabled: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + divider: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + border: CosmicColor::new(0.0, 0.0, 0.0, 0.0), + disabled_border: CosmicColor::new(0.0, 0.0, 0.0, 0.0), +}); pub(crate) static THEME: Mutex = Mutex::new(Theme { theme_type: ThemeType::Dark, diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index 02b98cfb..303a1ed9 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -119,22 +119,14 @@ where macro_rules! icon { ($name:expr, $on_press:expr) => {{ #[cfg(target_os = "linux")] - let icon = { - icon::from_name($name) - .apply(button::icon) - }; + let icon = { icon::from_name($name).apply(button::icon) }; #[cfg(not(target_os = "linux"))] let icon = { - icon::from_svg_bytes(include_bytes!(concat!( - "../../res/icons/", - $name, - ".svg" - ))) - .symbolic(true) - .apply(button::icon) + icon::from_svg_bytes(include_bytes!(concat!("../../res/icons/", $name, ".svg"))) + .symbolic(true) + .apply(button::icon) }; - icon.padding([0, 12]) - .on_press($on_press) + icon.padding([0, 12]).on_press($on_press) }}; } let date = text(this.model.visible.format("%B %Y").to_string()).size(18); diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 789969ac..a17625dc 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -6,6 +6,7 @@ use std::borrow::Cow; use std::iter; use std::rc::Rc; +use std::sync::LazyLock; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; @@ -26,7 +27,6 @@ use iced_core::{ use iced_widget::slider::HandleShape; use iced_widget::{Row, canvas, column, horizontal_space, row, scrollable, vertical_space}; -use lazy_static::lazy_static; use palette::{FromColor, RgbHue}; use super::divider::horizontal; @@ -38,17 +38,17 @@ use super::{Icon, button, segmented_control, text, text_input, tooltip}; pub use ColorPickerModel as Model; // TODO is this going to look correct enough? -lazy_static! { - pub static ref HSV_RAINBOW: Vec = (0u16..8) - .map( - |h| iced::Color::from(palette::Srgba::from_color(palette::Hsv::new_srgb_const( +pub static HSV_RAINBOW: LazyLock> = LazyLock::new(|| { + (0u16..8) + .map(|h| { + Color::from(palette::Srgba::from_color(palette::Hsv::new_srgb_const( RgbHue::new(f32::from(h) * 360.0 / 7.0), 1.0, - 1.0 + 1.0, ))) - ) - .collect(); -} + }) + .collect() +}); const MAX_RECENT: usize = 20; From 066999586bef4a30f45de0edb872ef0dddd7adf9 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, 5 Sep 2025 18:50:25 +0200 Subject: [PATCH 072/352] feat: add i18n support for libcosmic widgets --- Cargo.toml | 7 ++++ i18n.toml | 4 +++ i18n/en/libcosmic.ftl | 11 +++++++ i18n/sr-Cyrl/libcosmic.ftl | 11 +++++++ i18n/sr-Latn/libcosmic.ftl | 11 +++++++ src/lib.rs | 2 ++ src/localize.rs | 51 +++++++++++++++++++++++++++++ src/widget/about.rs | 18 +++++----- src/widget/context_drawer/widget.rs | 10 +++--- 9 files changed, 110 insertions(+), 15 deletions(-) create mode 100644 i18n.toml create mode 100644 i18n/en/libcosmic.ftl create mode 100644 i18n/sr-Cyrl/libcosmic.ftl create mode 100644 i18n/sr-Latn/libcosmic.ftl create mode 100644 src/localize.rs diff --git a/Cargo.toml b/Cargo.toml index 33bf7c49..076fc00f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,6 +107,13 @@ cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-c chrono = "0.4.41" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } +# Internationalization +i18n-embed = { version = "0.16.0", features = [ + "fluent-system", + "desktop-requester", +] } +i18n-embed-fl = "0.10" +rust-embed = "8.7.2" css-color = "0.2.8" derive_setters = "0.1.8" futures = "0.3" diff --git a/i18n.toml b/i18n.toml new file mode 100644 index 00000000..76f7c310 --- /dev/null +++ b/i18n.toml @@ -0,0 +1,4 @@ +fallback_language = "en" + +[fluent] +assets_dir = "i18n" diff --git a/i18n/en/libcosmic.ftl b/i18n/en/libcosmic.ftl new file mode 100644 index 00000000..45266a9b --- /dev/null +++ b/i18n/en/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Close + +# About +license = License +links = Links +developers = Developers +designers = Designers +artists = Artists +translators = Translators +documenters = Documenters diff --git a/i18n/sr-Cyrl/libcosmic.ftl b/i18n/sr-Cyrl/libcosmic.ftl new file mode 100644 index 00000000..579392f4 --- /dev/null +++ b/i18n/sr-Cyrl/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Затвори + +# About +license = Лиценца +links = Линкови +Developers = Програмери +Designers = Дизајнери +Artists = Уметници +Translators = Преводиоци +Documenters = Документатори diff --git a/i18n/sr-Latn/libcosmic.ftl b/i18n/sr-Latn/libcosmic.ftl new file mode 100644 index 00000000..9fbe9a21 --- /dev/null +++ b/i18n/sr-Latn/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Zatvori + +# About +license = Licenca +links = Linkovi +developers = Programeri +designers = Dizajneri +artists = Umetnici +translators = Prevodioci +documenters = Dokumentatori diff --git a/src/lib.rs b/src/lib.rs index e8aeeedd..a180c224 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -90,6 +90,8 @@ pub use iced_wgpu; pub mod icon_theme; pub mod keyboard_nav; +mod localize; + #[cfg(all(target_env = "gnu", not(target_os = "windows")))] pub(crate) mod malloc; diff --git a/src/localize.rs b/src/localize.rs new file mode 100644 index 00000000..95a31655 --- /dev/null +++ b/src/localize.rs @@ -0,0 +1,51 @@ +// SPDX-License-Identifier: GPL-3.0-only + +use i18n_embed::{ + DefaultLocalizer, LanguageLoader, Localizer, + fluent::{FluentLanguageLoader, fluent_language_loader}, +}; +use rust_embed::RustEmbed; +use std::sync::{LazyLock, OnceLock}; + +#[derive(RustEmbed)] +#[folder = "i18n/"] +struct Localizations; + +pub static LANGUAGE_LOADER: LazyLock = LazyLock::new(|| { + let loader: FluentLanguageLoader = fluent_language_loader!(); + + loader + .load_fallback_language(&Localizations) + .expect("Error while loading fallback language"); + + loader +}); + +static LOCALIZATION_INITIALIZED: OnceLock<()> = OnceLock::new(); + +#[macro_export] +macro_rules! fl { + ($message_id:literal) => {{ + $crate::localize::localize(); + i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id) + }}; + ($message_id:literal, $($args:expr),*) => {{ + $crate::localize::localize(); + i18n_embed_fl::fl!($crate::localize::LANGUAGE_LOADER, $message_id, $($args), *) + }}; +} + +// Get the `Localizer` to be used for localizing this library. +pub fn localizer() -> Box { + Box::from(DefaultLocalizer::new(&*LANGUAGE_LOADER, &Localizations)) +} + +pub fn localize() { + LOCALIZATION_INITIALIZED.get_or_init(|| { + let localizer = localizer(); + let requested_languages = i18n_embed::DesktopLanguageRequester::requested_languages(); + if let Err(error) = localizer.select(&requested_languages) { + eprintln!("Error while loading language for libcosmic {}", error); + } + }); +} diff --git a/src/widget/about.rs b/src/widget/about.rs index aea92991..f1f84106 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -1,6 +1,6 @@ use { crate::{ - Element, + Element, fl, iced::{Alignment, Length}, widget::{self, horizontal_space}, }, @@ -116,7 +116,7 @@ pub fn about<'a, Message: Clone + 'static>( space_xxs, space_m, .. } = crate::theme::spacing(); - let section = |list: &'a Vec<(String, String)>, title: &'a str| { + let section = |list: &'a Vec<(String, String)>, title: String| { (!list.is_empty()).then_some({ let items: Vec> = list.iter() @@ -150,15 +150,15 @@ pub fn about<'a, Message: Clone + 'static>( }); let author = about.author.as_ref().map(widget::text::body); let version = about.version.as_ref().map(widget::button::standard); - let links_section = section(&about.links, "Links"); - let developers_section = section(&about.developers, "Developers"); - let designers_section = section(&about.designers, "Designers"); - let artists_section = section(&about.artists, "Artists"); - let translators_section = section(&about.translators, "Translators"); - let documenters_section = section(&about.documenters, "Documenters"); + let links_section = section(&about.links, fl!("links")); + let developers_section = section(&about.developers, fl!("developers")); + let designers_section = section(&about.designers, fl!("designers")); + let artists_section = section(&about.artists, fl!("artists")); + let translators_section = section(&about.translators, fl!("translators")); + let documenters_section = section(&about.documenters, fl!("documenters")); let license = about.license.as_ref().map(|license| { let url = about.get_license_url(); - widget::settings::section().title("License").add( + widget::settings::section().title(fl!("license")).add( widget::button::custom( widget::row() .push(widget::text(license)) diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index e618fbcf..b46f6017 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -1,12 +1,10 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -use std::borrow::Cow; - -use crate::widget::{LayerContainer, button, column, container, icon, row, scrollable, text}; -use crate::{Apply, Element, Renderer, Theme}; - use super::overlay::Overlay; +use crate::widget::{LayerContainer, button, column, container, icon, row, scrollable, text}; +use crate::{Apply, Element, Renderer, Theme, fl}; +use std::borrow::Cow; use iced_core::Alignment; use iced_core::event::{self, Event}; @@ -86,7 +84,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { ) .push_maybe(title) .push( - button::text("Close") + button::text(fl!("close")) .trailing_icon(icon::from_name("go-next-symbolic")) .on_press(on_close) .apply(container) From ac18f009b4fd2a20d18e2f656cd668c320ba58fd Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Sun, 7 Sep 2025 19:17:59 -0600 Subject: [PATCH 073/352] Update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 567b4a09..f89222f4 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 567b4a0973d7a1797e7581f896c1aee236142f32 +Subproject commit f89222f4fa7c11d24a97ee12a80877189427ddbe From 39a5607400452fbf27fe2c1d14c1d2dea8d51447 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, 9 Sep 2025 15:30:12 +0200 Subject: [PATCH 074/352] improv(icon): use correct size variant for `Named` Update`Icon::size` method to correctly handle `Named` icons by using the provided size retroactively. --- src/widget/icon/mod.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/widget/icon/mod.rs b/src/widget/icon/mod.rs index 5a90d35b..8b21b6dd 100644 --- a/src/widget/icon/mod.rs +++ b/src/widget/icon/mod.rs @@ -43,6 +43,7 @@ pub struct Icon { #[setters(skip)] handle: Handle, class: crate::theme::Svg, + #[setters(skip)] pub(super) size: u16, content_fit: ContentFit, #[setters(strip_option)] @@ -72,6 +73,22 @@ impl Icon { None } + #[must_use] + pub fn size(mut self, size: u16) -> Self { + match &self.handle.data { + // ensures correct icon size variant selection + Data::Name(named) => { + let mut new_named = named.clone(); + new_named.size = Some(size); + self.handle = new_named.handle(); + } + _ => { + self.size = size; + } + } + self + } + #[must_use] fn view<'a, Message: 'a>(self) -> Element<'a, Message> { let from_image = |handle| { From e83e43bf1e38476e79383b299668afa525bad3e9 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, 9 Sep 2025 16:50:38 +0200 Subject: [PATCH 075/352] fix(icon): always set size Fixes an oversight in my previous commit 39a5607400452fbf27fe2c1d14c1d2dea8d51447. --- src/widget/icon/mod.rs | 16 ++++++---------- 1 file changed, 6 insertions(+), 10 deletions(-) diff --git a/src/widget/icon/mod.rs b/src/widget/icon/mod.rs index 8b21b6dd..20e8bf25 100644 --- a/src/widget/icon/mod.rs +++ b/src/widget/icon/mod.rs @@ -75,16 +75,12 @@ impl Icon { #[must_use] pub fn size(mut self, size: u16) -> Self { - match &self.handle.data { - // ensures correct icon size variant selection - Data::Name(named) => { - let mut new_named = named.clone(); - new_named.size = Some(size); - self.handle = new_named.handle(); - } - _ => { - self.size = size; - } + self.size = size; + // ensures correct icon size variant selection + if let Data::Name(named) = &self.handle.data { + let mut new_named = named.clone(); + new_named.size = Some(size); + self.handle = new_named.handle(); } self } From 31aa0bd3dfea708722ce214d44ad24265cf2112d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 10 Sep 2025 11:32:10 -0400 Subject: [PATCH 076/352] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index f89222f4..efcad825 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit f89222f4fa7c11d24a97ee12a80877189427ddbe +Subproject commit efcad82516588897420adc646b33ed71f7f836c3 From e568122083ebc42d11e82b95ecc2e233c00554f7 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, 11 Sep 2025 16:37:42 +0200 Subject: [PATCH 077/352] fix(context_drawer): title alignment Something caused text alignment to break, so this gets around it by wrapping the text in a container. --- Cargo.toml | 8 ++++---- cosmic-config/Cargo.toml | 2 +- src/widget/context_drawer/widget.rs | 4 ++-- 3 files changed, 7 insertions(+), 7 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 076fc00f..5d22afc8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,7 +104,7 @@ async-fs = { version = "2.1", 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 = "6254f50", optional = true } -chrono = "0.4.41" +chrono = "0.4.42" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } # Internationalization @@ -129,7 +129,7 @@ raw-window-handle = "0.6" rfd = { version = "0.15.4", default-features = false, features = [ "xdg-portal", ], optional = true } -rustix = { version = "1.0", features = ["pipe", "process"], optional = true } +rustix = { version = "1.1", features = ["pipe", "process"], optional = true } serde = { version = "1.0.219", features = ["derive"] } slotmap = "1.0.7" smol = { version = "2.0.2", optional = true } @@ -138,13 +138,13 @@ tokio = { version = "1.47.1", optional = true } tracing = "0.1.41" unicode-segmentation = "1.12" url = "2.5.7" -zbus = { version = "5.10.0", default-features = false, optional = true } +zbus = { version = "5.11.0", default-features = false, optional = true } # 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.10.0", default-features = false } +zbus = { version = "5.11.0", default-features = false } [target.'cfg(unix)'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index e838f9b5..9b5aca07 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -11,7 +11,7 @@ subscription = ["iced_futures"] [dependencies] cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } -zbus = { version = "5.10.0", default-features = false, optional = true } +zbus = { version = "5.11.0", default-features = false, optional = true } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } calloop = { version = "0.14.3", optional = true } notify = "8.2.0" diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index b46f6017..c65fe082 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -61,8 +61,8 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { let title = title.map(|title| { text::heading(title) - .width(Length::FillPortion(title_portion)) - .center() + .apply(container) + .center_x(Length::FillPortion(title_portion)) }); let (actions_width, close_width) = if title.is_some() { From b9a00c6e799b80154190f11943bb65c1fc4dc58b 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, 12 Sep 2025 20:31:01 +0200 Subject: [PATCH 078/352] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index efcad825..d0508750 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit efcad82516588897420adc646b33ed71f7f836c3 +Subproject commit d05087507a7a0e37e26f174cfc97629c960b4383 From 978bde5720ee00b26fe4ab221a914926851bd62b Mon Sep 17 00:00:00 2001 From: Matei Pralea Date: Sun, 14 Sep 2025 10:40:06 +0300 Subject: [PATCH 079/352] i18n(ro): Add Romanian translation --- i18n/ro/libcosmic.ftl | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 i18n/ro/libcosmic.ftl diff --git a/i18n/ro/libcosmic.ftl b/i18n/ro/libcosmic.ftl new file mode 100644 index 00000000..da9f80a5 --- /dev/null +++ b/i18n/ro/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Închide + +# About +license = Licență +links = Linkuri +developers = Dezvoltatori +designers = Designeri +artists = Artiști +translators = Traducători +documenters = Documentatori From 0e797b244043ee86610113d547950204258dea83 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 11 Sep 2025 01:46:03 -0400 Subject: [PATCH 080/352] improv(input): better initial handling of focus state --- src/widget/text_input/input.rs | 53 ++++++++++++++++++++++------------ 1 file changed, 35 insertions(+), 18 deletions(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index b8c035d4..3a2af337 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -591,7 +591,10 @@ where // Unfocus text input if it becomes disabled if self.on_input.is_none() && !self.manage_value { state.last_click = None; - state.is_focused = None; + state.is_focused = state.is_focused.map(|mut f| { + f.focused = false; + f + }); state.is_pasting = None; state.dragging_state = None; } @@ -628,18 +631,19 @@ where state.dirty = true; } - if self.always_active && state.is_focused.is_none() { + if self.always_active && !state.is_focused() { let now = Instant::now(); LAST_FOCUS_UPDATE.with(|x| x.set(now)); state.is_focused = Some(Focus { updated_at: now, now, + focused: true, }); } // 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.is_some() { + 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()); @@ -647,7 +651,7 @@ where }; } - if let Some(f) = state.is_focused.as_ref() { + if let Some(f) = state.is_focused.as_ref().filter(|f| f.focused) { if f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get()) { state.unfocus(); state.emit_unfocus = true; @@ -838,9 +842,12 @@ where if self.is_editable_variant { if let Some(ref on_edit) = self.on_toggle_edit { let state = tree.state.downcast_mut::(); - if !state.is_read_only && state.is_focused.is_none() { + 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)); } } } @@ -1392,6 +1399,7 @@ pub fn update<'a, Message: Clone + 'static>( state.is_focused = Some(Focus { updated_at: now, now, + focused: true, }); } @@ -1520,7 +1528,7 @@ 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.is_none() + if !state.is_focused() && matches!(state.dragging_state, None | Some(DraggingState::Selection)) { if let Some(on_focus) = on_focus { @@ -1541,6 +1549,7 @@ pub fn update<'a, Message: Clone + 'static>( state.is_focused = Some(Focus { updated_at: now, now, + focused: true, }); } @@ -1592,8 +1601,7 @@ pub fn update<'a, Message: Clone + 'static>( .. }) => { let state = state(); - - if let Some(focus) = &mut state.is_focused { + 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; }; @@ -1873,7 +1881,7 @@ pub fn update<'a, Message: Clone + 'static>( Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { let state = state(); - if state.is_focused.is_some() { + if state.is_focused() { match key { keyboard::Key::Character(c) if "v" == c => { state.is_pasting = None; @@ -1897,7 +1905,7 @@ pub fn update<'a, Message: Clone + 'static>( Event::Window(window::Event::RedrawRequested(now)) => { let state = state(); - if let Some(focus) = &mut state.is_focused { + if let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) { focus.now = now; let millis_until_redraw = CURSOR_BLINK_INTERVAL_MILLIS @@ -2258,12 +2266,15 @@ pub fn draw<'a, Message>( let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None); #[cfg(not(feature = "wayland"))] let handling_dnd_offer = false; - let (cursor, offset) = if let Some(focus) = &state.is_focused.or_else(|| { - handling_dnd_offer.then(|| Focus { - updated_at: Instant::now(), - now: Instant::now(), - }) - }) { + let (cursor, offset) = if let Some(focus) = + state.is_focused.filter(|f| f.focused).or_else(|| { + let now = Instant::now(); + handling_dnd_offer.then(|| Focus { + updated_at: now, + now, + focused: true, + }) + }) { match state.cursor.state(value) { cursor::State::Index(position) => { let (text_value_width, offset) = @@ -2547,6 +2558,7 @@ pub struct State { struct Focus { updated_at: Instant, now: Instant, + focused: bool, } impl State { @@ -2565,6 +2577,7 @@ impl State { Focus { updated_at: now, now, + focused: true, } }), select_on_focus, @@ -2623,7 +2636,7 @@ impl State { #[inline] #[must_use] pub fn is_focused(&self) -> bool { - self.is_focused.is_some() + self.is_focused.is_some_and(|f| f.focused) } /// Returns the [`Cursor`] of the [`TextInput`]. @@ -2642,6 +2655,7 @@ impl State { self.is_focused = Some(Focus { updated_at: now, now, + focused: true, }); if self.select_on_focus { @@ -2656,7 +2670,10 @@ impl State { pub(super) fn unfocus(&mut self) { self.move_cursor_to_front(); self.last_click = None; - self.is_focused = None; + self.is_focused = self.is_focused.map(|mut f| { + f.focused = false; + f + }); self.dragging_state = None; self.is_pasting = None; self.keyboard_modifiers = keyboard::Modifiers::default(); From c01254dd18c95f05742690f04964feef7d931192 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 16 Sep 2025 22:54:27 -0400 Subject: [PATCH 081/352] fix(menu): overlays should be used when multi-window is not active --- src/widget/menu/menu_bar.rs | 41 ++++++++++++++++++++++++++++------- src/widget/menu/menu_inner.rs | 24 ++++++++++++++++---- 2 files changed, 53 insertions(+), 12 deletions(-) diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 707aebdc..30c802c1 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -9,7 +9,12 @@ use super::{ }, menu_tree::MenuTree, }; -#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] +#[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + feature = "surface-message" +))] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::{ Renderer, @@ -190,7 +195,7 @@ pub struct MenuBar { menu_roots: Vec>, style: ::Style, window_id: window::Id, - #[cfg(all(feature = "wayland", feature = "winit"))] + #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, pub(crate) on_surface_action: Option Message + Send + Sync + 'static>>, @@ -225,7 +230,7 @@ where menu_roots, style: ::Style::default(), window_id: window::Id::NONE, - #[cfg(all(feature = "wayland", feature = "winit"))] + #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), on_surface_action: None, } @@ -319,7 +324,7 @@ where self } - #[cfg(all(feature = "wayland", feature = "winit"))] + #[cfg(all(feature = "multi-window", feature = "wayland", feature = "winit"))] pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -351,7 +356,12 @@ where self } - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + feature = "surface-message" + ))] #[allow(clippy::too_many_lines)] fn create_popup( &mut self, @@ -630,7 +640,12 @@ where if !create_popup { return event::Status::Ignored; } - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + feature = "surface-message" + ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); } @@ -638,7 +653,12 @@ where Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) if open && view_cursor.is_over(layout.bounds()) => { - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + feature = "surface-message" + ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); } @@ -715,7 +735,12 @@ where _renderer: &Renderer, translation: Vector, ) -> Option> { - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + feature = "surface-message" + ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && self.on_surface_action.is_some() && self.window_id != window::Id::NONE diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 18b4433f..6c694de7 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -4,7 +4,12 @@ use std::{borrow::Cow, sync::Arc}; use super::{menu_bar::MenuBarState, menu_tree::MenuTree}; -#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] +#[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + feature = "surface-message" +))] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::style::menu_bar::StyleSheet; @@ -663,6 +668,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { if needs_reset { #[cfg(all( + feature = "multi-window", feature = "wayland", feature = "winit", feature = "surface-message" @@ -932,7 +938,12 @@ impl Widget event::Status { let (new_root, status) = self.on_event(event, layout, cursor, renderer, clipboard, shell); - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + feature = "surface-message" + ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { if let Some((new_root, new_ms)) = new_root { use iced_runtime::platform_specific::wayland::popup::{ @@ -1177,7 +1188,12 @@ pub(crate) fn init_root_menu( }); } -#[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] +#[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + feature = "surface-message" +))] pub(super) fn init_root_popup_menu( menu: &mut Menu<'_, Message>, renderer: &crate::Renderer, @@ -1474,7 +1490,7 @@ where .as_ref() .is_some_and(|i| *i != new_index && !active_menu[*i].children.is_empty()); - #[cfg(all(feature = "wayland", feature = "winit", feature = "surface-message"))] + #[cfg(all(feature = "multi-window", feature = "wayland", 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); From 9ff208e9d7b538bc780bd2c4b13d342337780469 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 17 Sep 2025 14:31:41 -0400 Subject: [PATCH 082/352] fix: if editable input is focused by operation, emit a message --- src/widget/text_input/input.rs | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 3a2af337..12e8e7ce 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -638,6 +638,7 @@ where updated_at: now, now, focused: true, + needs_update: false, }); } @@ -838,7 +839,7 @@ where let size = self.size.unwrap_or_else(|| renderer.default_size().0); let line_height = self.line_height; - // Disables editing of the editable variant when clicking outside of it. + // Disables editing of the editable variant when clicking outside of, or for tab focus changes. if self.is_editable_variant { if let Some(ref on_edit) = self.on_toggle_edit { let state = tree.state.downcast_mut::(); @@ -848,6 +849,11 @@ where } 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; + state.is_read_only = true; + shell.publish((on_edit)(f.focused)); } } } @@ -1400,6 +1406,7 @@ pub fn update<'a, Message: Clone + 'static>( updated_at: now, now, focused: true, + needs_update: false, }); } @@ -1550,6 +1557,7 @@ pub fn update<'a, Message: Clone + 'static>( updated_at: now, now, focused: true, + needs_update: false, }); } @@ -2270,6 +2278,7 @@ pub fn draw<'a, Message>( state.is_focused.filter(|f| f.focused).or_else(|| { let now = Instant::now(); handling_dnd_offer.then(|| Focus { + needs_update: false, updated_at: now, now, focused: true, @@ -2559,6 +2568,7 @@ struct Focus { updated_at: Instant, now: Instant, focused: bool, + needs_update: bool, } impl State { @@ -2578,6 +2588,7 @@ impl State { updated_at: now, now, focused: true, + needs_update: false, } }), select_on_focus, @@ -2656,6 +2667,7 @@ impl State { updated_at: now, now, focused: true, + needs_update: false, }); if self.select_on_focus { @@ -2672,6 +2684,7 @@ impl State { self.last_click = None; self.is_focused = self.is_focused.map(|mut f| { f.focused = false; + f.needs_update = false; f }); self.dragging_state = None; @@ -2724,11 +2737,17 @@ impl operation::Focusable for State { #[inline] fn focus(&mut self) { Self::focus(self); + if let Some(focus) = self.is_focused.as_mut() { + focus.needs_update = true; + } } #[inline] fn unfocus(&mut self) { Self::unfocus(self); + if let Some(focus) = self.is_focused.as_mut() { + focus.needs_update = true; + } } } From 19d273ed2e2058e52a947387c948a8bce7bb39a0 Mon Sep 17 00:00:00 2001 From: jermanuts <109705802+jermanuts@users.noreply.github.com> Date: Sat, 6 Sep 2025 05:54:13 +0200 Subject: [PATCH 083/352] i18n(ar): add Arabic translation --- i18n/ar/libcosmic.ftl | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 i18n/ar/libcosmic.ftl diff --git a/i18n/ar/libcosmic.ftl b/i18n/ar/libcosmic.ftl new file mode 100644 index 00000000..4fc8582b --- /dev/null +++ b/i18n/ar/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = أغلق + +# About +license = الترخيص +links = الروابط +developers = المطوّرون +designers = المصمّمون +artists = الفنانون +translators = المترجمون +documenters = الموثّقون From 66df10ad89a3a37183c124bfbef254af108af5ac Mon Sep 17 00:00:00 2001 From: FurkanAdmin <47474630+FurkanAdmin@users.noreply.github.com> Date: Fri, 19 Sep 2025 01:20:39 +0300 Subject: [PATCH 084/352] i18n(tr): add translation --- i18n/tr/libcosmic.ftl | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 i18n/tr/libcosmic.ftl diff --git a/i18n/tr/libcosmic.ftl b/i18n/tr/libcosmic.ftl new file mode 100644 index 00000000..fd0f5475 --- /dev/null +++ b/i18n/tr/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Kapat + +# About +license = Lisans +links = Bağlantılar +developers = Geliştiriciler +designers = Tasarımcılar +artists = Sanatçılar +translators = Çevirmenler +documenters = Belgelendiriciler From 17fa2cd29a69eb2098eb4f3bb912631ee6cf1df1 Mon Sep 17 00:00:00 2001 From: David Carvalho Date: Sun, 7 Sep 2025 22:47:59 -0300 Subject: [PATCH 085/352] i18n(pt-BR): add translations --- i18n/pt-BR/libcosmic.ftl | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 i18n/pt-BR/libcosmic.ftl diff --git a/i18n/pt-BR/libcosmic.ftl b/i18n/pt-BR/libcosmic.ftl new file mode 100644 index 00000000..febf5b2e --- /dev/null +++ b/i18n/pt-BR/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Fechar + +# About +license = Licença +links = Links +developers = Desenvolvedores +designers = Designers +artists = Artistas +translators = Tradutores +documenters = Documentadores From 31fa09a92a98ccbe47c81c83b41cd20a91107aae Mon Sep 17 00:00:00 2001 From: therealmate <61843503+therealmate@users.noreply.github.com> Date: Fri, 19 Sep 2025 00:21:35 +0200 Subject: [PATCH 086/352] i18n(hu): add translation --- i18n/hu/libcosmic.ftl | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 i18n/hu/libcosmic.ftl diff --git a/i18n/hu/libcosmic.ftl b/i18n/hu/libcosmic.ftl new file mode 100644 index 00000000..ddc43e6c --- /dev/null +++ b/i18n/hu/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Bezárás + +# About +license = Licenc +links = Linkek +developers = Fejlesztők +designers = Tervezők +artists = Művészek +translators = Fordítók +documenters = Dokumentálók From 4a29788199532e050a11f9c3e7fb839de9b900ad Mon Sep 17 00:00:00 2001 From: VandaLHJ Date: Mon, 15 Sep 2025 08:24:02 +0200 Subject: [PATCH 087/352] Create libcosmic.ftl PL initial translation Initial translation, i hope it's good enough. --- i18n/libcosmic.ftl | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 i18n/libcosmic.ftl diff --git a/i18n/libcosmic.ftl b/i18n/libcosmic.ftl new file mode 100644 index 00000000..f4a65aa6 --- /dev/null +++ b/i18n/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Zamknij + +# About +license = Licencja +links = Linki +developers = Programiści +designers = Projektanci +artists = Artyści +translators = Tłumacze +documenters = Dokumentaliści From f1998afff91a1466a1b246956f0bdc26d10fc16f Mon Sep 17 00:00:00 2001 From: VandaLHJ Date: Fri, 19 Sep 2025 07:12:37 +0200 Subject: [PATCH 088/352] Create libcosmic.ftl PL initial translation This time in correct location --- i18n/pl/libcosmic.ftl | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 i18n/pl/libcosmic.ftl diff --git a/i18n/pl/libcosmic.ftl b/i18n/pl/libcosmic.ftl new file mode 100644 index 00000000..f4a65aa6 --- /dev/null +++ b/i18n/pl/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Zamknij + +# About +license = Licencja +links = Linki +developers = Programiści +designers = Projektanci +artists = Artyści +translators = Tłumacze +documenters = Dokumentaliści From 47daaab610f0fae2d3cee72746f66e0a6b732af7 Mon Sep 17 00:00:00 2001 From: VandaLHJ Date: Fri, 19 Sep 2025 07:14:32 +0200 Subject: [PATCH 089/352] Delete i18n/libcosmic.ftl PL wrong location --- i18n/libcosmic.ftl | 11 ----------- 1 file changed, 11 deletions(-) delete mode 100644 i18n/libcosmic.ftl diff --git a/i18n/libcosmic.ftl b/i18n/libcosmic.ftl deleted file mode 100644 index f4a65aa6..00000000 --- a/i18n/libcosmic.ftl +++ /dev/null @@ -1,11 +0,0 @@ -# Context Drawer -close = Zamknij - -# About -license = Licencja -links = Linki -developers = Programiści -designers = Projektanci -artists = Artyści -translators = Tłumacze -documenters = Dokumentaliści From 9ccade723a3f5d4438b16d5ad5ace927b903e794 Mon Sep 17 00:00:00 2001 From: twlvnn kraftwerk Date: Sun, 21 Sep 2025 16:00:29 +0200 Subject: [PATCH 090/352] i18n(bg): added bulgarian translation --- i18n/bg/libcosmic.ftl | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 i18n/bg/libcosmic.ftl diff --git a/i18n/bg/libcosmic.ftl b/i18n/bg/libcosmic.ftl new file mode 100644 index 00000000..2ac4d072 --- /dev/null +++ b/i18n/bg/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Затваряне + +# About +license = Лиценз +links = Връзки +developers = Разработчици +designers = Дизайнери +artists = Художници +translators = Преводачи +documenters = Документатори From ad70236a5860562e8f77ef376cdb42af033dfdbf Mon Sep 17 00:00:00 2001 From: lorduskordus Date: Sat, 27 Sep 2025 17:59:56 +0200 Subject: [PATCH 091/352] i18n(cs): Add Czech translation --- i18n/cs/libcosmic.ftl | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 i18n/cs/libcosmic.ftl diff --git a/i18n/cs/libcosmic.ftl b/i18n/cs/libcosmic.ftl new file mode 100644 index 00000000..561ca9ac --- /dev/null +++ b/i18n/cs/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Zavřít + +# About +license = Licence +links = Odkazy +developers = Vývojáři +designers = Designéři +artists = Grafici +translators = Překladatelé +documenters = Tvůrci dokumentace From 12014b683a97807c61809a8d31b6bc89ed6344e6 Mon Sep 17 00:00:00 2001 From: UchiWerfer Date: Sun, 28 Sep 2025 20:58:42 +0200 Subject: [PATCH 092/352] i18n(de): add German translations --- i18n/de/libcosmic.ftl | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 i18n/de/libcosmic.ftl diff --git a/i18n/de/libcosmic.ftl b/i18n/de/libcosmic.ftl new file mode 100644 index 00000000..3806cc59 --- /dev/null +++ b/i18n/de/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Schließen + +# About +license = Lizenz +links = Links +developers = Entwickler*innen +designers = Designer*innen +artists = Künstler*innen +translators = Übersetzer*innen +documenters = Dokumentierer*innen From 43314e3e6af112681b97f05e2228177f790e3f76 Mon Sep 17 00:00:00 2001 From: rdsq Date: Mon, 29 Sep 2025 17:29:25 +0300 Subject: [PATCH 093/352] add eo and uk --- i18n/eo/libcosmic.ftl | 11 +++++++++++ i18n/uk/libcosmic.ftl | 11 +++++++++++ 2 files changed, 22 insertions(+) create mode 100644 i18n/eo/libcosmic.ftl create mode 100644 i18n/uk/libcosmic.ftl diff --git a/i18n/eo/libcosmic.ftl b/i18n/eo/libcosmic.ftl new file mode 100644 index 00000000..69764d88 --- /dev/null +++ b/i18n/eo/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Fermi + +# About +license = Permesilo +links = Ligiloj +developers = Programistoj +designers = Grafikistoj +artists = Artistoj +translators = Tradukantoj +documenters = Dokumentantoj diff --git a/i18n/uk/libcosmic.ftl b/i18n/uk/libcosmic.ftl new file mode 100644 index 00000000..07dbaf9c --- /dev/null +++ b/i18n/uk/libcosmic.ftl @@ -0,0 +1,11 @@ +# Context Drawer +close = Закрити + +# About +license = Ліцензія +links = Лінки +developers = Розробники +designers = Дизайнери +artists = Митці +translators = Перекладачі +documenters = Документатори From 9815d4d98125b04ecb1afc28ef4b407510b45ac9 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 24 Sep 2025 15:55:34 -0400 Subject: [PATCH 094/352] feat(wayland): corner-radius protocol support --- Cargo.toml | 8 +- examples/multi-window/Cargo.toml | 2 +- iced | 2 +- src/app/cosmic.rs | 245 ++++++++++++++++++++++++++++--- src/core.rs | 16 ++ src/surface/action.rs | 85 +++++++++++ src/surface/mod.rs | 25 ++++ 7 files changed, 355 insertions(+), 28 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 5d22afc8..2edab9f0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,7 @@ ashpd = { version = "0.12.0", default-features = false, optional = true } async-fs = { version = "2.1", 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 = "6254f50", optional = true } +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "633beb0", optional = true } chrono = "0.4.42" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } @@ -229,6 +229,6 @@ libcosmic = { path = "./" } # FIXME update winit deps where necessary to use this # [patch.crates-io] -# [patch."https://github.com/pop-os/winit.git"] -# winit = { git = "https://github.com/rust-windowing/winit.git", rev = "241b7a80bba96c91fa3901729cd5dec66abb9be4" } -# winit = { path = "../../winit" } +[patch."https://github.com/pop-os/winit.git"] +winit = { git = "https://github.com/pop-os/winit.git//", branch = "xdg-toplevel" } +# winit = { path = "../winit" } diff --git a/examples/multi-window/Cargo.toml b/examples/multi-window/Cargo.toml index 7a8e3051..168bd4ec 100644 --- a/examples/multi-window/Cargo.toml +++ b/examples/multi-window/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "multi-window", "dbus-config", "wgpu"] } +libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "multi-window", "dbus-config", "wgpu", "wayland"] } diff --git a/iced b/iced index d0508750..788be2f7 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit d05087507a7a0e37e26f174cfc97629c960b4383 +Subproject commit 788be2f7825b648ec3ce33697c6e675a7b7265ec diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 17331832..9c5a39f8 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 use std::borrow::Borrow; -use std::collections::HashMap; +use std::collections::{HashMap, HashSet}; use std::sync::Arc; use super::{Action, Application, ApplicationExt, Subscription}; @@ -92,6 +92,7 @@ pub struct Cosmic { Box Fn(&'a App) -> Element<'a, crate::Action>>, ), >, + pub tracked_windows: HashSet, } impl Cosmic @@ -139,11 +140,11 @@ where #[cfg(feature = "wayland")] crate::surface::Action::AppSubsurface(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) - .ok() - .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync>>().ok()) else { - tracing::error!("Invalid settings for subsurface"); - return Task::none(); - }; + .ok() + .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync>>().ok()) else { + tracing::error!("Invalid settings for subsurface"); + return Task::none(); + }; if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: { let Some(settings) = std::sync::Arc::try_unwrap(settings) - .ok() - .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync>>().ok()) else { - tracing::error!("Invalid settings for subsurface"); - return Task::none(); - }; + .ok() + .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync>>().ok()) else { + tracing::error!("Invalid settings for subsurface"); + return Task::none(); + }; if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: { let Some(settings) = std::sync::Arc::try_unwrap(settings) - .ok() - .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync>>().ok()) else { - tracing::error!("Invalid settings for popup"); - return Task::none(); - }; + .ok() + .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync>>().ok()) else { + tracing::error!("Invalid settings for popup"); + return Task::none(); + }; if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: { iced_winit::commands::subsurface::destroy_subsurface(id) } + #[cfg(feature = "wayland")] + crate::surface::Action::DestroyWindow(id) => iced::window::close(id), crate::surface::Action::ResponsiveMenuBar { menu_bar, limits, @@ -241,11 +244,11 @@ where #[cfg(feature = "wayland")] crate::surface::Action::Popup(settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) - .ok() - .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync>>().ok()) else { - tracing::error!("Invalid settings for popup"); - return Task::none(); - }; + .ok() + .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync>>().ok()) else { + tracing::error!("Invalid settings for popup"); + return Task::none(); + }; if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: { + let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { + s.downcast:: iced::window::Settings + Send + Sync>>() + .ok() + }) else { + tracing::error!("Invalid settings for AppWindow"); + return Task::none(); + }; + + if let Some(view) = view.and_then(|view| { + match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Fn(&'a T) -> Element<'a, crate::Action> + + Send + + Sync, + >>() { + Ok(v) => Some(v), + Err(err) => { + tracing::error!("Invalid view for AppWindow: {err:?}"); + None + } + } + }) { + let settings = settings(&mut self.app); + self.get_window(id, settings, *view) + } else { + let settings = settings(&mut self.app); + + self.tracked_windows.insert(id); + iced_runtime::task::oneshot(|channel| { + iced_runtime::Action::Window(iced_runtime::window::Action::Open( + id, settings, channel, + )) + }) + .discard() + } + } + #[cfg(feature = "wayland")] + 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>>() + .ok() + }) else { + tracing::error!("Invalid settings for Window"); + return Task::none(); + }; + + if let Some(view) = view.and_then(|view| { + match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Element<'static, crate::Action> + Send + Sync, + >>() { + Ok(v) => Some(v), + Err(err) => { + tracing::error!("Invalid view for Window: {err:?}"); + None + } + } + }) { + let settings = settings(); + self.get_window(id, settings, Box::new(move |_| view())) + } else { + let settings = settings(); + + self.tracked_windows.insert(id); + + iced_runtime::task::oneshot(|channel| { + iced_runtime::Action::Window(iced_runtime::window::Action::Open( + id, settings, channel, + )) + }) + .discard() + } + } + crate::surface::Action::Ignore => iced::Task::none(), crate::surface::Action::Task(f) => { f().map(|sm| crate::Action::Cosmic(Action::Surface(sm))) @@ -667,6 +744,42 @@ impl Cosmic { new_theme.theme_type.prefer_dark(prefer_dark); cosmic_theme.set_theme(new_theme.theme_type); + #[cfg(feature = "wayland")] + 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; + + let t = cosmic_theme.cosmic(); + + let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); + let cur_rad = CornerRadius { + top_left: radii[0].round() as u32, + top_right: radii[1].round() as u32, + bottom_left: radii[2].round() as u32, + bottom_right: radii[3].round() as u32, + }; + + // Update radius for the main window + let main_window_id = self + .app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED); + let mut cmds = + vec![corner_radius(main_window_id, Some(cur_rad)).discard()]; + // Update radius for each tracked view with the window surface type + for (id, (_, surface_type, _)) in self.surface_views.iter() { + if let SurfaceIdWrapper::Window(_) = surface_type { + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + } + } + // Update radius for all tracked windows + for id in self.tracked_windows.iter() { + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + } + + return Task::batch(cmds); + } } } @@ -725,9 +838,46 @@ impl Cosmic { core.system_theme = new_theme.clone(); { let mut cosmic_theme = THEME.lock().unwrap(); + // Only apply update if the theme is set to load a system theme - if let ThemeType::System { theme: _, .. } = cosmic_theme.theme_type { + if let ThemeType::System { .. } = cosmic_theme.theme_type { cosmic_theme.set_theme(new_theme.theme_type); + #[cfg(feature = "wayland")] + 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; + + let t = cosmic_theme.cosmic(); + + let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); + let cur_rad = CornerRadius { + top_left: radii[0].round() as u32, + top_right: radii[1].round() as u32, + bottom_left: radii[2].round() as u32, + bottom_right: radii[3].round() as u32, + }; + + // Update radius for the main window + let main_window_id = self + .app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED); + let mut cmds = + vec![corner_radius(main_window_id, Some(cur_rad)).discard()]; + // Update radius for each tracked view with the window surface type + for (id, (_, surface_type, _)) in self.surface_views.iter() { + if let SurfaceIdWrapper::Window(_) = surface_type { + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + } + } + // Update radius for all tracked windows + for id in self.tracked_windows.iter() { + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + } + + return Task::batch(cmds); + } } } } @@ -748,6 +898,10 @@ impl Cosmic { Action::Surface(action) => return self.surface_update(action), Action::SurfaceClosed(id) => { + #[cfg(feature = "wayland")] + self.surface_views.remove(&id); + self.tracked_windows.remove(&id); + let mut ret = if let Some(msg) = self.app.on_close_requested(id) { self.app.update(msg) } else { @@ -910,6 +1064,26 @@ impl Cosmic { core.applet.suggested_bounds = b; } Action::Opened(id) => { + self.tracked_windows.insert(id); + #[cfg(feature = "wayland")] + 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; + + let theme = THEME.lock().unwrap(); + let t = theme.cosmic(); + let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); + let cur_rad = CornerRadius { + top_left: radii[0].round() as u32, + top_right: radii[1].round() as u32, + bottom_left: radii[2].round() as u32, + bottom_right: radii[3].round() as u32, + }; + return Task::batch(vec![ + corner_radius(id, Some(cur_rad)).discard(), + iced_runtime::window::run_with_handle(id, init_windowing_system), + ]); + } return iced_runtime::window::run_with_handle(id, init_windowing_system); } _ => {} @@ -925,6 +1099,7 @@ impl Cosmic { app, #[cfg(feature = "wayland")] surface_views: HashMap::new(), + tracked_windows: HashSet::new(), } } @@ -971,4 +1146,30 @@ impl Cosmic { ); get_popup(settings) } + + #[cfg(feature = "wayland")] + /// Create a window surface + pub fn get_window( + &mut self, + id: iced::window::Id, + settings: iced::window::Settings, + view: Box< + dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync, + >, + ) -> Task> { + use iced_winit::SurfaceIdWrapper; + + self.surface_views.insert( + id.clone(), + ( + None, // TODO parent for window, platform specific option maybe? + SurfaceIdWrapper::Window(id), + view, + ), + ); + iced_runtime::task::oneshot(|channel| { + iced_runtime::Action::Window(iced_runtime::window::Action::Open(id, settings, channel)) + }) + .discard() + } } diff --git a/src/core.rs b/src/core.rs index c82aa839..6069a83d 100644 --- a/src/core.rs +++ b/src/core.rs @@ -97,6 +97,9 @@ pub struct Core { pub(crate) exit_on_main_window_closed: bool, pub(crate) menu_bars: HashMap, + + #[cfg(feature = "wayland")] + pub(crate) sync_window_border_radii_to_theme: bool, } impl Default for Core { @@ -154,6 +157,8 @@ impl Default for Core { main_window: None, exit_on_main_window_closed: true, menu_bars: HashMap::new(), + #[cfg(feature = "wayland")] + sync_window_border_radii_to_theme: true } } } @@ -476,4 +481,15 @@ impl Core { crate::command::toggle_maximize(id) } + + // TODO should we emit tasks setting the corner radius or unsetting it if this is changed? + #[cfg(feature = "wayland")] + pub fn set_sync_window_border_radii_to_theme(&mut self, sync: bool) { + self.sync_window_border_radii_to_theme = sync; + } + + #[cfg(feature = "wayland")] + pub fn sync_window_border_radii_to_theme(&self) -> bool { + self.sync_window_border_radii_to_theme + } } diff --git a/src/surface/action.rs b/src/surface/action.rs index fdf2680e..25c45ce0 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -5,6 +5,7 @@ use super::Action; #[cfg(feature = "winit")] use crate::Application; +use iced::window; use std::{any::Any, sync::Arc}; /// Used to produce a destroy popup message from within a widget. @@ -20,6 +21,90 @@ pub fn destroy_subsurface(id: iced_core::window::Id) -> Action { Action::DestroySubsurface(id) } +#[cfg(feature = "wayland")] +#[must_use] +pub fn destroy_window(id: iced_core::window::Id) -> Action { + Action::DestroyWindow(id) +} + +#[cfg(all(feature = "wayland", feature = "winit"))] +#[must_use] +pub fn app_window( + settings: impl Fn(&mut App) -> window::Settings + + Send + + Sync + + 'static, + view: Option< + Box< + dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action> + + Send + + Sync + + 'static, + >, + >, +) -> (window::Id, Action) { + let id = window::Id::unique(); + + let boxed: Box< + dyn Fn(&mut App) -> window::Settings + + Send + + Sync + + 'static, + > = Box::new(settings); + let boxed: Box = Box::new(boxed); + + ( + id, + Action::AppWindow( + id, + Arc::new(boxed), + view.map(|view| { + let boxed: Box = Box::new(view); + Arc::new(boxed) + }), + ) + ) +} + +/// Used to create a window message from within a widget. +#[cfg(all(feature = "wayland", feature = "winit"))] +#[must_use] +pub fn simple_window( + settings: impl Fn() -> window::Settings + + Send + + Sync + + 'static, + view: Option< + impl Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, + >, +) -> (window::Id, Action) { + let id = window::Id::unique(); + + let boxed: Box< + dyn Fn() -> window::Settings + + Send + + Sync + + 'static, + > = Box::new(settings); + let boxed: Box = Box::new(boxed); + + ( + id, + Action::Window( + id, + Arc::new(boxed), + view.map(|view| { + let boxed: Box< + dyn Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, + > = Box::new(view); + let boxed: Box = Box::new(boxed); + Arc::new(boxed) + }), + ) + ) +} + + #[cfg(all(feature = "wayland", feature = "winit"))] #[must_use] pub fn app_popup( diff --git a/src/surface/mod.rs b/src/surface/mod.rs index 3041fa54..b4ef63b6 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -36,6 +36,22 @@ pub enum Action { ), /// Destroy a subsurface with a view function DestroyPopup(iced::window::Id), + + /// Create a window with a view function accepting the App as a parameter + AppWindow( + iced::window::Id, + std::sync::Arc>, + Option>>, + ), + /// Create a window with a view function + Window( + iced::window::Id, + std::sync::Arc>, + Option>>, + ), + /// Destroy a window + DestroyWindow(iced::window::Id), + /// Responsive menu bar update ResponsiveMenuBar { /// Id of the menu bar @@ -80,6 +96,15 @@ impl std::fmt::Debug for Action { .field("size", size) .finish(), Self::Ignore => write!(f, "Ignore"), + Self::AppWindow(id, arg0, arg1) => { + f.debug_tuple("AppWindow").field(id).field(arg0).field(arg1).finish() + } + Self::Window(id, arg0, arg1) => { + f.debug_tuple("Window").field(id).field(arg0).field(arg1).finish() + } + Self::DestroyWindow(arg0) => { + f.debug_tuple("DestroyWindow").field(arg0).finish() + } Self::Task(_) => f.debug_tuple("Future").finish(), } } From ab41b83cd8715268ce5703ce99f64fe2e9c5dff8 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 24 Sep 2025 17:52:03 -0400 Subject: [PATCH 095/352] cargo fmt --- src/core.rs | 2 +- src/surface/action.rs | 35 +++++++++++------------------------ src/surface/mod.rs | 22 +++++++++++++--------- 3 files changed, 25 insertions(+), 34 deletions(-) diff --git a/src/core.rs b/src/core.rs index 6069a83d..2e4e0497 100644 --- a/src/core.rs +++ b/src/core.rs @@ -158,7 +158,7 @@ impl Default for Core { exit_on_main_window_closed: true, menu_bars: HashMap::new(), #[cfg(feature = "wayland")] - sync_window_border_radii_to_theme: true + sync_window_border_radii_to_theme: true, } } } diff --git a/src/surface/action.rs b/src/surface/action.rs index 25c45ce0..3a078ca3 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -30,10 +30,7 @@ pub fn destroy_window(id: iced_core::window::Id) -> Action { #[cfg(all(feature = "wayland", feature = "winit"))] #[must_use] pub fn app_window( - settings: impl Fn(&mut App) -> window::Settings - + Send - + Sync - + 'static, + settings: impl Fn(&mut App) -> window::Settings + Send + Sync + 'static, view: Option< Box< dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action> @@ -45,12 +42,8 @@ pub fn app_window( ) -> (window::Id, Action) { let id = window::Id::unique(); - let boxed: Box< - dyn Fn(&mut App) -> window::Settings - + Send - + Sync - + 'static, - > = Box::new(settings); + let boxed: Box window::Settings + Send + Sync + 'static> = + Box::new(settings); let boxed: Box = Box::new(boxed); ( @@ -62,7 +55,7 @@ pub fn app_window( let boxed: Box = Box::new(view); Arc::new(boxed) }), - ) + ), ) } @@ -70,22 +63,14 @@ pub fn app_window( #[cfg(all(feature = "wayland", feature = "winit"))] #[must_use] pub fn simple_window( - settings: impl Fn() -> window::Settings - + Send - + Sync - + 'static, + settings: impl Fn() -> window::Settings + Send + Sync + 'static, view: Option< impl Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, >, ) -> (window::Id, Action) { let id = window::Id::unique(); - let boxed: Box< - dyn Fn() -> window::Settings - + Send - + Sync - + 'static, - > = Box::new(settings); + let boxed: Box window::Settings + Send + Sync + 'static> = Box::new(settings); let boxed: Box = Box::new(boxed); ( @@ -95,16 +80,18 @@ pub fn simple_window( Arc::new(boxed), view.map(|view| { let boxed: Box< - dyn Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, + dyn Fn() -> crate::Element<'static, crate::Action> + + Send + + Sync + + 'static, > = Box::new(view); let boxed: Box = Box::new(boxed); Arc::new(boxed) }), - ) + ), ) } - #[cfg(all(feature = "wayland", feature = "winit"))] #[must_use] pub fn app_popup( diff --git a/src/surface/mod.rs b/src/surface/mod.rs index b4ef63b6..4598ac7c 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -96,15 +96,19 @@ impl std::fmt::Debug for Action { .field("size", size) .finish(), Self::Ignore => write!(f, "Ignore"), - Self::AppWindow(id, arg0, arg1) => { - f.debug_tuple("AppWindow").field(id).field(arg0).field(arg1).finish() - } - Self::Window(id, arg0, arg1) => { - f.debug_tuple("Window").field(id).field(arg0).field(arg1).finish() - } - Self::DestroyWindow(arg0) => { - f.debug_tuple("DestroyWindow").field(arg0).finish() - } + Self::AppWindow(id, arg0, arg1) => f + .debug_tuple("AppWindow") + .field(id) + .field(arg0) + .field(arg1) + .finish(), + Self::Window(id, arg0, arg1) => f + .debug_tuple("Window") + .field(id) + .field(arg0) + .field(arg1) + .finish(), + Self::DestroyWindow(arg0) => f.debug_tuple("DestroyWindow").field(arg0).finish(), Self::Task(_) => f.debug_tuple("Future").finish(), } } From 27f591e5aa85e998a2f0dc77fd08dc4e90fcea1d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 25 Sep 2025 12:52:16 -0400 Subject: [PATCH 096/352] fix(corner-radius): fix radius from array to match iced and better respect sharp corners --- src/app/cosmic.rs | 45 ++++++++++++++++++++++++++++++--------------- 1 file changed, 30 insertions(+), 15 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 9c5a39f8..1d52ee0f 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -755,27 +755,31 @@ impl Cosmic { let cur_rad = CornerRadius { top_left: radii[0].round() as u32, top_right: radii[1].round() as u32, - bottom_left: radii[2].round() as u32, - bottom_right: radii[3].round() as u32, + bottom_right: radii[2].round() as u32, + bottom_left: radii[3].round() as u32, }; + let rounded = !self.app.core().window.sharp_corners; // Update radius for the main window let main_window_id = self .app .core() .main_window_id() .unwrap_or(window::Id::RESERVED); - let mut cmds = - vec![corner_radius(main_window_id, Some(cur_rad)).discard()]; + let mut cmds = vec![ + corner_radius(main_window_id, rounded.then_some(cur_rad)).discard(), + ]; // Update radius for each tracked view with the window surface type for (id, (_, surface_type, _)) in self.surface_views.iter() { if let SurfaceIdWrapper::Window(_) = surface_type { - cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + cmds.push( + corner_radius(*id, rounded.then_some(cur_rad)).discard(), + ); } } // Update radius for all tracked windows for id in self.tracked_windows.iter() { - cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + cmds.push(corner_radius(*id, rounded.then_some(cur_rad)).discard()); } return Task::batch(cmds); @@ -853,9 +857,10 @@ impl Cosmic { let cur_rad = CornerRadius { top_left: radii[0].round() as u32, top_right: radii[1].round() as u32, - bottom_left: radii[2].round() as u32, - bottom_right: radii[3].round() as u32, + bottom_right: radii[2].round() as u32, + bottom_left: radii[3].round() as u32, }; + let rounded = !self.app.core().window.sharp_corners; // Update radius for the main window let main_window_id = self @@ -863,17 +868,24 @@ impl Cosmic { .core() .main_window_id() .unwrap_or(window::Id::RESERVED); - let mut cmds = - vec![corner_radius(main_window_id, Some(cur_rad)).discard()]; + let mut cmds = vec![ + corner_radius(main_window_id, rounded.then_some(cur_rad)) + .discard(), + ]; // Update radius for each tracked view with the window surface type for (id, (_, surface_type, _)) in self.surface_views.iter() { if let SurfaceIdWrapper::Window(_) = surface_type { - cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + cmds.push( + corner_radius(*id, rounded.then_some(cur_rad)) + .discard(), + ); } } // Update radius for all tracked windows for id in self.tracked_windows.iter() { - cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + cmds.push( + corner_radius(*id, rounded.then_some(cur_rad)).discard(), + ); } return Task::batch(cmds); @@ -1076,11 +1088,14 @@ impl Cosmic { let cur_rad = CornerRadius { top_left: radii[0].round() as u32, top_right: radii[1].round() as u32, - bottom_left: radii[2].round() as u32, - bottom_right: radii[3].round() as u32, + bottom_right: radii[2].round() as u32, + bottom_left: radii[3].round() as u32, }; + // TODO do we need per window sharp corners? + let rounded = !self.app.core().window.sharp_corners; + return Task::batch(vec![ - corner_radius(id, Some(cur_rad)).discard(), + corner_radius(id, rounded.then_some(cur_rad)).discard(), iced_runtime::window::run_with_handle(id, init_windowing_system), ]); } From 03f07d2f1e6accd8942b35793184d6c25f953d5e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 25 Sep 2025 15:41:16 -0400 Subject: [PATCH 097/352] fix: sharp corners & window state handling --- iced | 2 +- src/app/cosmic.rs | 25 +++++++++++++++++++++++++ src/app/mod.rs | 19 ++++++++++++++----- src/theme/style/iced.rs | 18 +++++++++++++++--- src/widget/header_bar.rs | 1 + 5 files changed, 56 insertions(+), 9 deletions(-) diff --git a/iced b/iced index 788be2f7..f581f19f 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 788be2f7825b648ec3ce33697c6e675a7b7265ec +Subproject commit f581f19f897e1ccc4393c8867d4ae3ed532742b4 diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 1d52ee0f..58b73b81 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -426,6 +426,12 @@ where ) => { return Some(Action::SuggestedBounds(b)); } + #[cfg(feature = "wayland")] + wayland::Event::Window(iced::event::wayland::WindowEvent::WindowState( + s, + )) => { + return Some(Action::WindowState(id, s)); + } _ => (), } } @@ -588,6 +594,7 @@ impl Cosmic { fn cosmic_update(&mut self, message: Action) -> iced::Task> { match message { Action::WindowMaximized(id, maximized) => { + #[cfg(not(feature = "wayland"))] if self .app .core() @@ -635,6 +642,24 @@ impl Cosmic { | WindowState::TILED_BOTTOM, ); } + 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; + + let theme = THEME.lock().unwrap(); + let t = theme.cosmic(); + let radii = t.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); + let cur_rad = CornerRadius { + top_left: radii[0].round() as u32, + top_right: radii[1].round() as u32, + bottom_right: radii[2].round() as u32, + bottom_left: radii[3].round() as u32, + }; + let rounded = !self.app.core().window.sharp_corners; + return Task::batch(vec![ + corner_radius(id, rounded.then_some(cur_rad)).discard(), + ]); + } } #[cfg(feature = "wayland")] diff --git a/src/app/mod.rs b/src/app/mod.rs index 1b10b68d..11053142 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -759,16 +759,25 @@ impl ApplicationExt for App { header .apply(container) .class(crate::theme::Container::custom(move |theme| { + let cosmic = theme.cosmic(); container::Style { background: Some(iced::Background::Color( - theme.cosmic().background.base.into(), + cosmic.background.base.into(), )), border: iced::Border { radius: [ - window_corner_radius[0] - 1.0, - window_corner_radius[1] - 1.0, - theme.cosmic().radius_0()[2], - theme.cosmic().radius_0()[3], + if sharp_corners { + cosmic.radius_0()[0] + } else { + window_corner_radius[0] - 1.0 + }, + if sharp_corners { + cosmic.radius_0()[1] + } else { + window_corner_radius[1] - 1.0 + }, + cosmic.radius_0()[2], + cosmic.radius_0()[3], ] .into(), ..Default::default() diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 764c1654..1f212d13 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -395,6 +395,7 @@ pub enum Container<'a> { Dropdown, HeaderBar { focused: bool, + sharp_corners: bool, }, List, Primary, @@ -507,7 +508,10 @@ impl iced_container::Catalog for Theme { } } - Container::HeaderBar { focused } => { + Container::HeaderBar { + focused, + sharp_corners, + } => { let (icon_color, text_color) = if *focused { ( Color::from(cosmic.accent_text_color()), @@ -526,8 +530,16 @@ impl iced_container::Catalog for Theme { background: Some(iced::Background::Color(cosmic.background.base.into())), border: Border { radius: [ - window_corner_radius[0], - window_corner_radius[1], + if *sharp_corners { + cosmic.corner_radii.radius_0[0] + } else { + window_corner_radius[0] + }, + if *sharp_corners { + cosmic.corner_radii.radius_0[1] + } else { + window_corner_radius[1] + }, cosmic.corner_radii.radius_0[2], cosmic.corner_radii.radius_0[3], ] diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 3763ae32..bed7d363 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -409,6 +409,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .apply(widget::container) .class(crate::theme::Container::HeaderBar { focused: self.focused, + sharp_corners: self.maximized, }) .center_y(Length::Shrink) .apply(widget::mouse_area); From 7015b8ace423cea32c881dca670271535498c8f9 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 30 Sep 2025 08:45:57 -0400 Subject: [PATCH 098/352] chore: update iced --- Cargo.toml | 4 ++-- iced | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 2edab9f0..868d69b6 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -229,6 +229,6 @@ libcosmic = { path = "./" } # FIXME update winit deps where necessary to use this # [patch.crates-io] -[patch."https://github.com/pop-os/winit.git"] -winit = { git = "https://github.com/pop-os/winit.git//", branch = "xdg-toplevel" } +# [patch."https://github.com/pop-os/winit.git"] +# winit = { git = "https://github.com/pop-os/winit.git//", branch = "xdg-toplevel" } # winit = { path = "../winit" } diff --git a/iced b/iced index f581f19f..12810507 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit f581f19f897e1ccc4393c8867d4ae3ed532742b4 +Subproject commit 12810507e181b52bd13f222d8810d877c6b2d0f4 From 4a71189d346e766c46f5bfaeb494a921fe0fccbd Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 30 Sep 2025 11:34:39 -0400 Subject: [PATCH 099/352] chore: update cosmic-protocols --- Cargo.toml | 2 +- iced | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 868d69b6..6ccf57d1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -103,7 +103,7 @@ ashpd = { version = "0.12.0", default-features = false, optional = true } async-fs = { version = "2.1", 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 = "633beb0", optional = true } +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "d0e95be", optional = true } chrono = "0.4.42" 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 12810507..521a04d7 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 12810507e181b52bd13f222d8810d877c6b2d0f4 +Subproject commit 521a04d7e7589fdd61b314c92155277bf350d944 From 00b4a8a9f51aff381e0083da6e7c4abf13c002be Mon Sep 17 00:00:00 2001 From: oddib Date: Tue, 30 Sep 2025 00:02:39 +0200 Subject: [PATCH 100/352] =?UTF-8?q?Added=20translation=20using=20Weblate?= =?UTF-8?q?=20(Norwegian=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- i18n/nb-NO/libcosmic.ftl | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 i18n/nb-NO/libcosmic.ftl diff --git a/i18n/nb-NO/libcosmic.ftl b/i18n/nb-NO/libcosmic.ftl new file mode 100644 index 00000000..e69de29b From 0ca7a99c9f0b130aae6cbd1ebf74fb89cd7c9ad1 Mon Sep 17 00:00:00 2001 From: oddib Date: Tue, 30 Sep 2025 00:04:42 +0200 Subject: [PATCH 101/352] =?UTF-8?q?Translated=20using=20Weblate=20(Norwegi?= =?UTF-8?q?an=20Bokm=C3=A5l)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Currently translated at 100.0% (8 of 8 strings) Translation: Pop OS/libcosmic Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/nb_NO/ --- i18n/nb-NO/libcosmic.ftl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/i18n/nb-NO/libcosmic.ftl b/i18n/nb-NO/libcosmic.ftl index e69de29b..64d4e5d1 100644 --- a/i18n/nb-NO/libcosmic.ftl +++ b/i18n/nb-NO/libcosmic.ftl @@ -0,0 +1,8 @@ +close = Lukk +license = Lisens +links = Linker +developers = Utviklere +designers = Designere +artists = Artister +translators = Oversettere +documenters = Dokumentører From f097b643b327a391fabb17efd8358ec3f4df1c1c Mon Sep 17 00:00:00 2001 From: Mattias Eriksson Date: Tue, 30 Sep 2025 07:35:30 +0200 Subject: [PATCH 102/352] Added translation using Weblate (Swedish) --- i18n/sv/libcosmic.ftl | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 i18n/sv/libcosmic.ftl diff --git a/i18n/sv/libcosmic.ftl b/i18n/sv/libcosmic.ftl new file mode 100644 index 00000000..e69de29b From 59e480a4c64fd895d8c16b84241feea4f81ce5ed Mon Sep 17 00:00:00 2001 From: Mattias Eriksson Date: Tue, 30 Sep 2025 07:43:13 +0200 Subject: [PATCH 103/352] Translated using Weblate (Swedish) Currently translated at 100.0% (8 of 8 strings) Translation: Pop OS/libcosmic Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/sv/ --- i18n/sv/libcosmic.ftl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/i18n/sv/libcosmic.ftl b/i18n/sv/libcosmic.ftl index e69de29b..75cb7fb4 100644 --- a/i18n/sv/libcosmic.ftl +++ b/i18n/sv/libcosmic.ftl @@ -0,0 +1,8 @@ +license = Licens +links = Länkar +developers = Utvecklare +designers = Designers +artists = Konstnärer +translators = Översättare +documenters = Skribenter +close = Stäng From ee84ad958f4fd28486580b0a9b5928d74d0030bf Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 30 Sep 2025 19:56:00 +0200 Subject: [PATCH 104/352] Added translation using Weblate (Estonian) --- i18n/et/libcosmic.ftl | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 i18n/et/libcosmic.ftl diff --git a/i18n/et/libcosmic.ftl b/i18n/et/libcosmic.ftl new file mode 100644 index 00000000..e69de29b From df9df4096345513c6bebd3ae943e943d76006f17 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 30 Sep 2025 22:04:32 +0200 Subject: [PATCH 105/352] chore(about): drop license dependency Not needed since the application can already give URLs to their license --- Cargo.toml | 3 +-- src/app/context_drawer.rs | 8 ++++---- src/widget/about.rs | 26 +++++++------------------- 3 files changed, 12 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 6ccf57d1..74195a21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -12,7 +12,7 @@ default = ["dbus-config", "multi-window", "a11y"] # Accessibility support a11y = ["iced/a11y", "iced_accessibility"] # Enable about widget -about = ["dep:license"] +about = [] # Builds support for animated images animated-image = ["dep:async-fs", "image/gif", "tokio?/io-util", "tokio?/fs"] # XXX autosize should not be used on winit windows unless dialogs @@ -122,7 +122,6 @@ image = { version = "0.25.8", default-features = false, features = [ "png", ] } libc = { version = "0.2.175", optional = true } -license = { version = "3.7.0", optional = true } mime = { version = "0.3.17", optional = true } palette = "0.7.6" raw-window-handle = "0.6" diff --git a/src/app/context_drawer.rs b/src/app/context_drawer.rs index 5d15d7b4..b33d2ba6 100644 --- a/src/app/context_drawer.rs +++ b/src/app/context_drawer.rs @@ -15,11 +15,11 @@ pub struct ContextDrawer<'a, Message: Clone + 'static> { } #[cfg(feature = "about")] -pub fn about( - about: &crate::widget::about::About, - on_url_press: impl Fn(String) -> Message, +pub fn about<'a, Message: Clone + 'static>( + about: &'a crate::widget::about::About, + on_url_press: impl Fn(&'a str) -> Message + 'a, on_close: Message, -) -> ContextDrawer<'_, Message> { +) -> ContextDrawer<'a, Message> { context_drawer(crate::widget::about(about, on_url_press), on_close) } diff --git a/src/widget/about.rs b/src/widget/about.rs index f1f84106..f1538d8f 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -1,10 +1,7 @@ -use { - crate::{ - Element, fl, - iced::{Alignment, Length}, - widget::{self, horizontal_space}, - }, - license::License, +use crate::{ + Element, fl, + iced::{Alignment, Length}, + widget::{self, horizontal_space}, }; #[derive(Debug, Default, Clone, derive_setters::Setters)] @@ -96,21 +93,12 @@ impl<'a> About { .collect(); self } - - fn get_license_url(&self) -> Option { - self.license_url.clone().or_else(|| { - self.license.as_ref().and_then(|license_str| { - let license: &dyn License = license_str.parse().ok()?; - Some(format!("https://spdx.org/licenses/{}.html", license.id())) - }) - }) - } } /// Constructs the widget for the about section. pub fn about<'a, Message: Clone + 'static>( about: &'a About, - on_url_press: impl Fn(String) -> Message, + on_url_press: impl Fn(&'a str) -> Message + 'a, ) -> Element<'a, Message> { let cosmic_theme::Spacing { space_xxs, space_m, .. @@ -131,7 +119,7 @@ pub fn about<'a, Message: Clone + 'static>( .align_y(Alignment::Center), ) .class(crate::theme::Button::Link) - .on_press(on_url_press(url.clone())) + .on_press(on_url_press(url)) .width(Length::Fill) .into() }) @@ -157,7 +145,7 @@ pub fn about<'a, Message: Clone + 'static>( let translators_section = section(&about.translators, fl!("translators")); let documenters_section = section(&about.documenters, fl!("documenters")); let license = about.license.as_ref().map(|license| { - let url = about.get_license_url(); + let url = about.license_url.as_deref(); widget::settings::section().title(fl!("license")).add( widget::button::custom( widget::row() From 6a0c06a3689ea8a868a629c86f5b4782a4b22aa0 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 30 Sep 2025 22:18:02 +0200 Subject: [PATCH 106/352] chore: update taffy crate to crates.io release --- Cargo.toml | 6 +----- src/widget/flex_row/layout.rs | 10 +++++----- src/widget/grid/layout.rs | 4 ++-- 3 files changed, 8 insertions(+), 12 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 74195a21..16af9575 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -133,6 +133,7 @@ serde = { version = "1.0.219", features = ["derive"] } slotmap = "1.0.7" smol = { version = "2.0.2", optional = true } thiserror = "2.0.16" +taffy = { version = "0.9.1", features = ["grid"] } tokio = { version = "1.47.1", optional = true } tracing = "0.1.41" unicode-segmentation = "1.12" @@ -205,11 +206,6 @@ optional = true version = "0.11" optional = true -[dependencies.taffy] -git = "https://github.com/DioxusLabs/taffy" -rev = "7781c70" -features = ["grid"] - [workspace] members = [ "cosmic-config", diff --git a/src/widget/flex_row/layout.rs b/src/widget/flex_row/layout.rs index d781e4f9..720e4561 100644 --- a/src/widget/flex_row/layout.rs +++ b/src/widget/flex_row/layout.rs @@ -44,7 +44,7 @@ pub fn resolve( min_size: taffy::geometry::Size { width: length(max_size.width), - height: Dimension::Auto, + height: Dimension::auto(), }, align_items, @@ -71,7 +71,7 @@ pub fn resolve( let c_size = child_widget.size(); let (width, flex_grow, justify_self) = match c_size.width { Length::Fill | Length::FillPortion(_) => { - (Dimension::Auto, 1.0, Some(AlignItems::Stretch)) + (Dimension::auto(), 1.0, Some(AlignItems::Stretch)) } _ => (length(size.width), 0.0, None), }; @@ -82,15 +82,15 @@ pub fn resolve( min_size: taffy::geometry::Size { width: match min_item_width { Some(width) => length(size.width.min(width)), - None => Dimension::Auto, + None => Dimension::auto(), }, - height: Dimension::Auto, + height: Dimension::auto(), }, size: taffy::geometry::Size { width, height: match c_size.height { - Length::Fill | Length::FillPortion(_) => Dimension::Auto, + Length::Fill | Length::FillPortion(_) => Dimension::auto(), _ => length(size.height), }, }, diff --git a/src/widget/grid/layout.rs b/src/widget/grid/layout.rs index 6423c377..d1da68af 100644 --- a/src/widget/grid/layout.rs +++ b/src/widget/grid/layout.rs @@ -48,7 +48,7 @@ pub fn resolve( let c_size = child_widget.size(); let (width, flex_grow, justify_self) = match c_size.width { Length::Fill | Length::FillPortion(_) => { - (Dimension::Auto, 1.0, Some(AlignItems::Stretch)) + (Dimension::auto(), 1.0, Some(AlignItems::Stretch)) } _ => (length(size.width), 0.0, None), }; @@ -72,7 +72,7 @@ pub fn resolve( size: taffy::geometry::Size { width, height: match c_size.height { - Length::Fill | Length::FillPortion(_) => Dimension::Auto, + Length::Fill | Length::FillPortion(_) => Dimension::auto(), _ => length(size.height), }, }, From 432e43d258f11f24226e97abe08c110e30f9ceb3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Priit=20J=C3=B5er=C3=BC=C3=BCt?= Date: Tue, 30 Sep 2025 20:21:56 +0200 Subject: [PATCH 107/352] i18n(et): update translations from Weblate Currently translated at 87.5% (7 of 8 strings) Translation: Pop OS/libcosmic Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/et/ --- i18n/et/libcosmic.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/i18n/et/libcosmic.ftl b/i18n/et/libcosmic.ftl index e69de29b..1449e0af 100644 --- a/i18n/et/libcosmic.ftl +++ b/i18n/et/libcosmic.ftl @@ -0,0 +1,7 @@ +close = Sulge +license = Litsents +links = Lingid +developers = Arendajad +artists = Kunstnikud +translators = Tõlkijad +documenters = Dokumenteerijad From 511fe02624a74007de728142368fa680217d51c3 Mon Sep 17 00:00:00 2001 From: Walter William Beckerleg Bruckman Date: Wed, 1 Oct 2025 14:45:20 +0200 Subject: [PATCH 108/352] i18n: adding translation for Spanish (Latin America) --- i18n/es-419/libcosmic.ftl | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 i18n/es-419/libcosmic.ftl diff --git a/i18n/es-419/libcosmic.ftl b/i18n/es-419/libcosmic.ftl new file mode 100644 index 00000000..e69de29b From 5092d1986148781eb7c7329de586957d61202d7a Mon Sep 17 00:00:00 2001 From: Walter William Beckerleg Bruckman Date: Wed, 1 Oct 2025 14:54:29 +0200 Subject: [PATCH 109/352] i18n(es-419): update translations from Weblate Currently translated at 100.0% (8 of 8 strings) Translation: Pop OS/libcosmic Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/es_419/ --- i18n/es-419/libcosmic.ftl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/i18n/es-419/libcosmic.ftl b/i18n/es-419/libcosmic.ftl index e69de29b..8ef988e9 100644 --- a/i18n/es-419/libcosmic.ftl +++ b/i18n/es-419/libcosmic.ftl @@ -0,0 +1,8 @@ +close = Cerrar +license = Licencia +links = Enlaces +developers = Desarrolladores +designers = Diseñadores +artists = Artistas +translators = Traductores +documenters = Documentalistas From aeafe447e3fbde3d18d4872c0b6e51a76675d597 Mon Sep 17 00:00:00 2001 From: Walter William Beckerleg Bruckman Date: Wed, 1 Oct 2025 16:28:15 +0200 Subject: [PATCH 110/352] i18n: adding translation for Chinese (Simplified Han script) --- i18n/zh-Hans/libcosmic.ftl | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 i18n/zh-Hans/libcosmic.ftl diff --git a/i18n/zh-Hans/libcosmic.ftl b/i18n/zh-Hans/libcosmic.ftl new file mode 100644 index 00000000..e69de29b From 1c83be9d1c4e62123a8b6b24425433eb0880230e Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 1 Oct 2025 21:58:53 +0200 Subject: [PATCH 111/352] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Hosted Weblate Co-authored-by: Hugo Carvalho Co-authored-by: Languages add-on Co-authored-by: Priit Jõerüüt Co-authored-by: Walter William Beckerleg Bruckman Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/es_419/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/et/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/pt/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/zh_Hans/ Translation: Pop OS/libcosmic --- i18n/af/libcosmic.ftl | 0 i18n/be/libcosmic.ftl | 0 i18n/ca/libcosmic.ftl | 0 i18n/da/libcosmic.ftl | 0 i18n/el/libcosmic.ftl | 0 i18n/en-GB/libcosmic.ftl | 0 i18n/es-MX/libcosmic.ftl | 0 i18n/es/libcosmic.ftl | 0 i18n/fa/libcosmic.ftl | 0 i18n/fi/libcosmic.ftl | 0 i18n/fr/libcosmic.ftl | 0 i18n/fy/libcosmic.ftl | 0 i18n/ga/libcosmic.ftl | 0 i18n/gd/libcosmic.ftl | 0 i18n/he/libcosmic.ftl | 0 i18n/hi/libcosmic.ftl | 0 i18n/hr/libcosmic.ftl | 0 i18n/id/libcosmic.ftl | 0 i18n/ie/libcosmic.ftl | 0 i18n/it/libcosmic.ftl | 0 i18n/ja/libcosmic.ftl | 0 i18n/jv/libcosmic.ftl | 0 i18n/kn/libcosmic.ftl | 0 i18n/ko/libcosmic.ftl | 0 i18n/li/libcosmic.ftl | 0 i18n/lt/libcosmic.ftl | 0 i18n/nl/libcosmic.ftl | 0 i18n/pt/libcosmic.ftl | 8 ++++++++ i18n/ru/libcosmic.ftl | 0 i18n/sk/libcosmic.ftl | 0 i18n/sr/libcosmic.ftl | 0 i18n/ta/libcosmic.ftl | 0 i18n/th/libcosmic.ftl | 0 i18n/vi/libcosmic.ftl | 0 i18n/zh-Hans/libcosmic.ftl | 6 ++++++ i18n/zh-Hant/libcosmic.ftl | 0 36 files changed, 14 insertions(+) create mode 100644 i18n/af/libcosmic.ftl create mode 100644 i18n/be/libcosmic.ftl create mode 100644 i18n/ca/libcosmic.ftl create mode 100644 i18n/da/libcosmic.ftl create mode 100644 i18n/el/libcosmic.ftl create mode 100644 i18n/en-GB/libcosmic.ftl create mode 100644 i18n/es-MX/libcosmic.ftl create mode 100644 i18n/es/libcosmic.ftl create mode 100644 i18n/fa/libcosmic.ftl create mode 100644 i18n/fi/libcosmic.ftl create mode 100644 i18n/fr/libcosmic.ftl create mode 100644 i18n/fy/libcosmic.ftl create mode 100644 i18n/ga/libcosmic.ftl create mode 100644 i18n/gd/libcosmic.ftl create mode 100644 i18n/he/libcosmic.ftl create mode 100644 i18n/hi/libcosmic.ftl create mode 100644 i18n/hr/libcosmic.ftl create mode 100644 i18n/id/libcosmic.ftl create mode 100644 i18n/ie/libcosmic.ftl create mode 100644 i18n/it/libcosmic.ftl create mode 100644 i18n/ja/libcosmic.ftl create mode 100644 i18n/jv/libcosmic.ftl create mode 100644 i18n/kn/libcosmic.ftl create mode 100644 i18n/ko/libcosmic.ftl create mode 100644 i18n/li/libcosmic.ftl create mode 100644 i18n/lt/libcosmic.ftl create mode 100644 i18n/nl/libcosmic.ftl create mode 100644 i18n/pt/libcosmic.ftl create mode 100644 i18n/ru/libcosmic.ftl create mode 100644 i18n/sk/libcosmic.ftl create mode 100644 i18n/sr/libcosmic.ftl create mode 100644 i18n/ta/libcosmic.ftl create mode 100644 i18n/th/libcosmic.ftl create mode 100644 i18n/vi/libcosmic.ftl create mode 100644 i18n/zh-Hant/libcosmic.ftl diff --git a/i18n/af/libcosmic.ftl b/i18n/af/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/be/libcosmic.ftl b/i18n/be/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/ca/libcosmic.ftl b/i18n/ca/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/da/libcosmic.ftl b/i18n/da/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/el/libcosmic.ftl b/i18n/el/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/en-GB/libcosmic.ftl b/i18n/en-GB/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/es-MX/libcosmic.ftl b/i18n/es-MX/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/es/libcosmic.ftl b/i18n/es/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/fa/libcosmic.ftl b/i18n/fa/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/fi/libcosmic.ftl b/i18n/fi/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/fr/libcosmic.ftl b/i18n/fr/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/fy/libcosmic.ftl b/i18n/fy/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/ga/libcosmic.ftl b/i18n/ga/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/gd/libcosmic.ftl b/i18n/gd/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/he/libcosmic.ftl b/i18n/he/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/hi/libcosmic.ftl b/i18n/hi/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/hr/libcosmic.ftl b/i18n/hr/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/id/libcosmic.ftl b/i18n/id/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/ie/libcosmic.ftl b/i18n/ie/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/it/libcosmic.ftl b/i18n/it/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/ja/libcosmic.ftl b/i18n/ja/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/jv/libcosmic.ftl b/i18n/jv/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/kn/libcosmic.ftl b/i18n/kn/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/ko/libcosmic.ftl b/i18n/ko/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/li/libcosmic.ftl b/i18n/li/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/lt/libcosmic.ftl b/i18n/lt/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/nl/libcosmic.ftl b/i18n/nl/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/pt/libcosmic.ftl b/i18n/pt/libcosmic.ftl new file mode 100644 index 00000000..e1786efb --- /dev/null +++ b/i18n/pt/libcosmic.ftl @@ -0,0 +1,8 @@ +close = Fechar +license = Licença +links = Ligações +developers = Programadores +designers = Designers +artists = Artistas +translators = Tradutores +documenters = Documentadores diff --git a/i18n/ru/libcosmic.ftl b/i18n/ru/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/sk/libcosmic.ftl b/i18n/sk/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/sr/libcosmic.ftl b/i18n/sr/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/ta/libcosmic.ftl b/i18n/ta/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/th/libcosmic.ftl b/i18n/th/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/vi/libcosmic.ftl b/i18n/vi/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/zh-Hans/libcosmic.ftl b/i18n/zh-Hans/libcosmic.ftl index e69de29b..e7c83e5c 100644 --- a/i18n/zh-Hans/libcosmic.ftl +++ b/i18n/zh-Hans/libcosmic.ftl @@ -0,0 +1,6 @@ +close = 关闭 +license = 许可证 +links = 链接 +developers = 开发者 +designers = 设计师 +translators = 译者 diff --git a/i18n/zh-Hant/libcosmic.ftl b/i18n/zh-Hant/libcosmic.ftl new file mode 100644 index 00000000..e69de29b From cc8e5ebdeaeb15b58f109398b15b0c0dd682788d Mon Sep 17 00:00:00 2001 From: grant-wilson Date: Wed, 1 Oct 2025 21:30:55 -0400 Subject: [PATCH 112/352] Fix typo in README dependencies section --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 595e0d3b..ac8e60aa 100644 --- a/README.md +++ b/README.md @@ -13,7 +13,7 @@ A platform toolkit based on iced for creating applets and applications for the C ## Dependencies -While libcosmic is written entirely in Rust, some of its dependencies may require shared system library headers to be installed. On Pop!_OS, the following dependencies are all that's necessary compile a typical COSMIC project: +While libcosmic is written entirely in Rust, some of its dependencies may require shared system library headers to be installed. On Pop!_OS, the following dependencies are all that's necessary to compile a typical COSMIC project: ```sh sudo apt install cargo cmake just libexpat1-dev libfontconfig-dev libfreetype-dev libxkbcommon-dev pkgconf From 6c5b799b343f877c310d9f9bf23ea3f282bdf1a2 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 2 Oct 2025 17:05:31 +0200 Subject: [PATCH 113/352] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aindriú Mac Giolla Eoin Co-authored-by: Hosted Weblate Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ga/ Translation: Pop OS/libcosmic --- i18n/ga/libcosmic.ftl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/i18n/ga/libcosmic.ftl b/i18n/ga/libcosmic.ftl index e69de29b..61557ccd 100644 --- a/i18n/ga/libcosmic.ftl +++ b/i18n/ga/libcosmic.ftl @@ -0,0 +1,8 @@ +close = Dún +license = Ceadúnas +links = Naisc +developers = Forbróirí +designers = Dearthóirí +artists = Ealaíontóirí +translators = Aistritheoirí +documenters = Doiciméadóirí From 0059fe182bfc4f6f5e1dd0e611cc700a4fa45835 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 3 Oct 2025 12:09:55 -0400 Subject: [PATCH 114/352] refactor: set sharp corner window radius to 0 instead of unsetting --- src/app/cosmic.rs | 132 ++++++++++++++++++++++++++++++++++++++++++---- 1 file changed, 122 insertions(+), 10 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 58b73b81..fc602d20 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -657,7 +657,21 @@ impl Cosmic { }; let rounded = !self.app.core().window.sharp_corners; return Task::batch(vec![ - corner_radius(id, rounded.then_some(cur_rad)).discard(), + corner_radius( + id, + if rounded { + Some(cur_rad) + } else { + let rad_0 = t.radius_0(); + Some(CornerRadius { + top_left: rad_0[0].round() as u32, + top_right: rad_0[1].round() as u32, + bottom_right: rad_0[2].round() as u32, + bottom_left: rad_0[3].round() as u32, + }) + }, + ) + .discard(), ]); } } @@ -792,19 +806,63 @@ impl Cosmic { .main_window_id() .unwrap_or(window::Id::RESERVED); let mut cmds = vec![ - corner_radius(main_window_id, rounded.then_some(cur_rad)).discard(), + corner_radius( + main_window_id, + if rounded { + Some(cur_rad) + } else { + let rad_0 = t.radius_0(); + Some(CornerRadius { + top_left: rad_0[0].round() as u32, + top_right: rad_0[1].round() as u32, + bottom_right: rad_0[2].round() as u32, + bottom_left: rad_0[3].round() as u32, + }) + }, + ) + .discard(), ]; // Update radius for each tracked view with the window surface type for (id, (_, surface_type, _)) in self.surface_views.iter() { if let SurfaceIdWrapper::Window(_) = surface_type { cmds.push( - corner_radius(*id, rounded.then_some(cur_rad)).discard(), + corner_radius( + *id, + if rounded { + Some(cur_rad) + } else { + let rad_0 = t.radius_0(); + Some(CornerRadius { + top_left: rad_0[0].round() as u32, + top_right: rad_0[1].round() as u32, + bottom_right: rad_0[2].round() as u32, + bottom_left: rad_0[3].round() as u32, + }) + }, + ) + .discard(), ); } } // Update radius for all tracked windows for id in self.tracked_windows.iter() { - cmds.push(corner_radius(*id, rounded.then_some(cur_rad)).discard()); + cmds.push( + corner_radius( + *id, + if rounded { + Some(cur_rad) + } else { + let rad_0 = t.radius_0(); + Some(CornerRadius { + top_left: rad_0[0].round() as u32, + top_right: rad_0[1].round() as u32, + bottom_right: rad_0[2].round() as u32, + bottom_left: rad_0[3].round() as u32, + }) + }, + ) + .discard(), + ); } return Task::batch(cmds); @@ -894,22 +952,62 @@ impl Cosmic { .main_window_id() .unwrap_or(window::Id::RESERVED); let mut cmds = vec![ - corner_radius(main_window_id, rounded.then_some(cur_rad)) - .discard(), + corner_radius( + main_window_id, + if rounded { + Some(cur_rad) + } else { + let rad_0 = t.radius_0(); + Some(CornerRadius { + top_left: rad_0[0].round() as u32, + top_right: rad_0[1].round() as u32, + bottom_right: rad_0[2].round() as u32, + bottom_left: rad_0[3].round() as u32, + }) + }, + ) + .discard(), ]; // Update radius for each tracked view with the window surface type for (id, (_, surface_type, _)) in self.surface_views.iter() { if let SurfaceIdWrapper::Window(_) = surface_type { cmds.push( - corner_radius(*id, rounded.then_some(cur_rad)) - .discard(), + corner_radius( + *id, + if rounded { + Some(cur_rad) + } else { + let rad_0 = t.radius_0(); + Some(CornerRadius { + top_left: rad_0[0].round() as u32, + top_right: rad_0[1].round() as u32, + bottom_right: rad_0[2].round() as u32, + bottom_left: rad_0[3].round() as u32, + }) + }, + ) + .discard(), ); } } // Update radius for all tracked windows for id in self.tracked_windows.iter() { cmds.push( - corner_radius(*id, rounded.then_some(cur_rad)).discard(), + corner_radius( + *id, + if rounded { + Some(cur_rad) + } else { + let rad_0 = t.radius_0(); + Some(CornerRadius { + top_left: rad_0[0].round() as u32, + top_right: rad_0[1].round() as u32, + bottom_right: rad_0[2].round() as u32, + bottom_left: rad_0[3].round() as u32, + }) + }, + ) + .discard(), ); } @@ -1120,7 +1218,21 @@ impl Cosmic { let rounded = !self.app.core().window.sharp_corners; return Task::batch(vec![ - corner_radius(id, rounded.then_some(cur_rad)).discard(), + corner_radius( + id, + if rounded { + Some(cur_rad) + } else { + let rad_0 = t.radius_0(); + Some(CornerRadius { + top_left: rad_0[0].round() as u32, + top_right: rad_0[1].round() as u32, + bottom_right: rad_0[2].round() as u32, + bottom_left: rad_0[3].round() as u32, + }) + }, + ) + .discard(), iced_runtime::window::run_with_handle(id, init_windowing_system), ]); } From 5cd774241308e807dedefd34d353492b3db51848 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 Oct 2025 16:18:50 +0200 Subject: [PATCH 115/352] chore(about): styling fixes Also reduces code duplication a bit. --- src/widget/about.rs | 162 +++++++++++++++++++++----------------------- 1 file changed, 76 insertions(+), 86 deletions(-) diff --git a/src/widget/about.rs b/src/widget/about.rs index f1538d8f..9f2276c8 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -1,5 +1,5 @@ use crate::{ - Element, fl, + Apply, Element, fl, iced::{Alignment, Length}, widget::{self, horizontal_space}, }; @@ -22,7 +22,7 @@ pub struct About { copyright: Option, /// The license name. license: Option, - /// The license url. If None spdx.org url is used. + /// The license url. license_url: Option, /// Artists who contributed to the application. #[setters(skip)] @@ -51,36 +51,28 @@ fn add_contributors(contributors: Vec<(&str, &str)>) -> Vec<(String, String)> { .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 { - /// Artists who contributed to the application. - pub fn artists(mut self, artists: impl Into>) -> Self { - self.artists = add_contributors(artists.into()); - self - } - - /// Designers who contributed to the application. - pub fn designers(mut self, designers: impl Into>) -> Self { - self.designers = add_contributors(designers.into()); - self - } - - /// Developers who contributed to the application. - pub fn developers(mut self, developers: impl Into>) -> Self { - self.developers = add_contributors(developers.into()); - self - } - - /// Documenters who contributed to the application. - pub fn documenters(mut self, documenters: impl Into>) -> Self { - self.documenters = add_contributors(documenters.into()); - self - } - - /// Translators who contributed to the application. - pub fn translators(mut self, translators: impl Into>) -> Self { - self.translators = add_contributors(translators.into()); - self - } + 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." + ); /// Links associated with the application. pub fn links, V: Into>( @@ -104,88 +96,86 @@ 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() + .push(widget::text(name)) + .push(horizontal_space()) + .push_maybe( + (!url.is_empty()).then_some(crate::widget::icon::from_name("link-symbolic").icon()), + ) + .align_y(Alignment::Center) + .apply(widget::button::custom) + .class(crate::theme::Button::Link) + .on_press(on_url_press(url)) + .width(Length::Fill) + .into() + }; + let section = |list: &'a Vec<(String, String)>, title: String| { (!list.is_empty()).then_some({ - let items: Vec> = - list.iter() - .map(|(name, url)| { - widget::button::custom( - widget::row() - .push(widget::text(name)) - .push(horizontal_space()) - .push_maybe((!url.is_empty()).then_some( - crate::widget::icon::from_name("link-symbolic").icon(), - )) - .align_y(Alignment::Center), - ) - .class(crate::theme::Button::Link) - .on_press(on_url_press(url)) - .width(Length::Fill) - .into() - }) - .collect(); + let items: Vec> = list + .iter() + .map(|(name, url)| section_button(name, url)) + .collect(); widget::settings::section().title(title).extend(items) }) }; - let application_name = about.name.as_ref().map(widget::text::title3); - let application_icon = about.icon.as_ref().map(|i| { - i.clone() - .icon() - .content_fit(iced::ContentFit::Contain) - .width(Length::Fixed(128.)) - .height(Length::Fixed(128.)) - }); - let author = about.author.as_ref().map(widget::text::body); - let version = about.version.as_ref().map(widget::button::standard); + let header_children: Vec> = [ + about.icon.as_ref().map(|i| { + i.clone() + .icon() + .size(256) + .width(Length::Fixed(128.)) + .height(Length::Fixed(128.)) + .content_fit(iced::ContentFit::Contain) + .into() + }), + about.name.as_ref().map(|n| widget::text::title3(n).into()), + about.author.as_ref().map(|a| widget::text::body(a).into()), + about.version.as_ref().map(|v| { + widget::button::standard(v) + .apply(widget::container) + .padding([space_xxs, 0, 0, 0]) + .into() + }), + ] + .into_iter() + .flatten() + .collect(); + let header = (!header_children.is_empty()) + .then_some(widget::column::with_children(header_children).align_x(Alignment::Center)); + let links_section = section(&about.links, fl!("links")); let developers_section = section(&about.developers, fl!("developers")); let designers_section = section(&about.designers, fl!("designers")); let artists_section = section(&about.artists, fl!("artists")); let translators_section = section(&about.translators, fl!("translators")); let documenters_section = section(&about.documenters, fl!("documenters")); - let license = about.license.as_ref().map(|license| { - let url = about.license_url.as_deref(); - widget::settings::section().title(fl!("license")).add( - widget::button::custom( - widget::row() - .push(widget::text(license)) - .push(horizontal_space()) - .push_maybe( - url.is_some() - .then_some(crate::widget::icon::from_name("link-symbolic").icon()), - ) - .align_y(Alignment::Center), - ) - .class(crate::theme::Button::Link) - .on_press(on_url_press(url.unwrap_or_default())) - .width(Length::Fill), + let license_section = about.license.as_ref().and_then(|license| { + let url = about.license_url.as_deref().unwrap_or_default(); + Some( + widget::settings::section() + .title(fl!("license")) + .add(section_button(license, url)), ) }); let copyright = about.copyright.as_ref().map(widget::text::body); let comments = about.comments.as_ref().map(widget::text::body); widget::column() - .push( - widget::column() - .push_maybe(application_icon) - .push_maybe(application_name) - .push_maybe(author) - .push_maybe(version) - .align_x(Alignment::Center) - .spacing(space_xxs), - ) - .push_maybe(license) + .push_maybe(header) .push_maybe(links_section) .push_maybe(developers_section) .push_maybe(designers_section) .push_maybe(artists_section) .push_maybe(translators_section) .push_maybe(documenters_section) + .push_maybe(license_section) .push_maybe(comments) .push_maybe(copyright) - .align_x(Alignment::Center) .spacing(space_m) .width(Length::Fill) + .align_x(Alignment::Center) .into() } From ad1672b8815389f7c13643615c54968597ffc07e 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 Oct 2025 18:19:19 +0200 Subject: [PATCH 116/352] fix: window corner handling --- src/app/cosmic.rs | 4 ++- src/app/mod.rs | 54 +++++++++++++++++++--------------------- src/core.rs | 2 ++ src/theme/style/iced.rs | 2 +- src/widget/header_bar.rs | 6 ++++- 5 files changed, 36 insertions(+), 32 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index fc602d20..42ae122b 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -542,7 +542,7 @@ where } #[cfg(feature = "multi-window")] - pub fn view(&self, id: window::Id) -> Element> { + pub fn view(&self, id: window::Id) -> Element<'_, crate::Action> { #[cfg(feature = "wayland")] if let Some((_, _, v)) = self.surface_views.get(&id) { return v(&self.app); @@ -641,6 +641,8 @@ impl Cosmic { | WindowState::TILED_TOP | WindowState::TILED_BOTTOM, ); + self.app.core_mut().window.is_maximized = + state.intersects(WindowState::MAXIMIZED | WindowState::FULLSCREEN); } if self.app.core().sync_window_border_radii_to_theme() { use iced_runtime::platform_specific::wayland::CornerRadius; diff --git a/src/app/mod.rs b/src/app/mod.rs index 11053142..a2f8d6ad 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -287,37 +287,37 @@ where /// Displays a context drawer on the side of the application window when `Some`. /// Use the [`ApplicationExt::set_show_context`] function for this to take effect. - fn context_drawer(&self) -> Option> { + fn context_drawer(&self) -> Option> { None } /// Displays a dialog in the center of the application window when `Some`. - fn dialog(&self) -> Option> { + fn dialog(&self) -> Option> { None } /// Displays a footer at the bottom of the application window when `Some`. - fn footer(&self) -> Option> { + fn footer(&self) -> Option> { None } /// Attaches elements to the start section of the header. - fn header_start(&self) -> Vec> { + fn header_start(&self) -> Vec> { Vec::new() } /// Attaches elements to the center of the header. - fn header_center(&self) -> Vec> { + fn header_center(&self) -> Vec> { Vec::new() } /// Attaches elements to the end section of the header. - fn header_end(&self) -> Vec> { + fn header_end(&self) -> Vec> { Vec::new() } /// Allows overriding the default nav bar widget. - fn nav_bar(&self) -> Option>> { + fn nav_bar(&self) -> Option>> { if !self.core().nav_bar_active() { return None; } @@ -485,7 +485,7 @@ pub trait ApplicationExt: Application { fn set_window_title(&mut self, title: String, id: window::Id) -> Task; /// View template for the main window. - fn view_main(&self) -> Element>; + fn view_main(&self) -> Element<'_, crate::Action>; fn watch_config( &self, @@ -546,12 +546,11 @@ impl ApplicationExt for App { #[allow(clippy::too_many_lines)] /// Creates the view for the main window. - fn view_main(&self) -> Element> { + fn view_main(&self) -> Element<'_, crate::Action> { let core = self.core(); let is_condensed = core.is_condensed(); - // TODO: More granularity might be needed for different window border - // handling of maximized and tiled windows let sharp_corners = core.window.sharp_corners; + let maximized = core.window.is_maximized; let content_container = core.window.content_container; let show_context = core.window.show_context; let nav_bar_active = core.nav_bar_active(); @@ -560,7 +559,7 @@ impl ApplicationExt for App { .iter() .any(|i| Some(*i) == self.core().main_window_id()); - let border_padding = if sharp_corners { 8 } else { 7 }; + let border_padding = if maximized { 8 } else { 7 }; let main_content_padding = if !content_container { [0, 0, 0, 0] @@ -698,17 +697,22 @@ impl ApplicationExt for App { }; // Ensures visually aligned radii for content and window corners - let window_corner_radius = crate::theme::active() - .cosmic() - .radius_s() - .map(|x| if x < 4.0 { x } else { x + 4.0 }); + let window_corner_radius = if sharp_corners { + crate::theme::active().cosmic().radius_0() + } else { + crate::theme::active() + .cosmic() + .radius_s() + .map(|x| if x < 4.0 { x } else { x + 4.0 }) + }; let view_column = crate::widget::column::with_capacity(2) .push_maybe(if core.window.show_headerbar { Some({ let mut header = crate::widget::header_bar() .focused(focused) - .maximized(sharp_corners) + .maximized(maximized) + .sharp_corners(sharp_corners) .title(&core.window.header_title) .on_drag(crate::Action::Cosmic(Action::Drag)) .on_right_click(crate::Action::Cosmic(Action::ShowWindowMenu)) @@ -766,16 +770,8 @@ impl ApplicationExt for App { )), border: iced::Border { radius: [ - if sharp_corners { - cosmic.radius_0()[0] - } else { - window_corner_radius[0] - 1.0 - }, - if sharp_corners { - cosmic.radius_0()[1] - } else { - window_corner_radius[1] - 1.0 - }, + (window_corner_radius[0] - 1.0).max(0.0), + (window_corner_radius[1] - 1.0).max(0.0), cosmic.radius_0()[2], cosmic.radius_0()[3], ] @@ -794,7 +790,7 @@ impl ApplicationExt for App { // The content element contains every element beneath the header. .push(content) .apply(container) - .padding(if sharp_corners { 0 } else { 1 }) + .padding(if maximized { 0 } else { 1 }) .class(crate::theme::Container::custom(move |theme| { container::Style { background: if content_container { @@ -806,7 +802,7 @@ impl ApplicationExt for App { }, border: iced::Border { color: theme.cosmic().bg_divider().into(), - width: if sharp_corners { 0.0 } else { 1.0 }, + width: if maximized { 0.0 } else { 1.0 }, radius: window_corner_radius.into(), }, ..Default::default() diff --git a/src/core.rs b/src/core.rs index 2e4e0497..338e0e85 100644 --- a/src/core.rs +++ b/src/core.rs @@ -38,6 +38,7 @@ pub struct Window { pub show_close: bool, pub show_maximize: bool, pub show_minimize: bool, + pub is_maximized: bool, height: f32, width: f32, } @@ -141,6 +142,7 @@ impl Default for Core { show_maximize: true, show_minimize: true, show_window_menu: false, + is_maximized: false, height: 0., width: 0., }, diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 1f212d13..c8dacbb9 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -148,7 +148,7 @@ impl iced_button::Catalog for Theme { impl Button { #[allow(clippy::trivially_copy_pass_by_ref)] #[allow(clippy::match_same_arms)] - fn cosmic<'a>(&'a self, theme: &'a Theme) -> &CosmicComponent { + fn cosmic<'a>(&'a self, theme: &'a Theme) -> &'a CosmicComponent { let cosmic = theme.cosmic(); match self { Self::Primary => &cosmic.accent_button, diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index bed7d363..3f4b5d88 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -24,6 +24,7 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { density: None, focused: false, maximized: false, + sharp_corners: false, is_ssd: false, on_double_click: None, is_condensed: false, @@ -83,6 +84,9 @@ pub struct HeaderBar<'a, Message> { /// Maximized state of the window maximized: bool, + /// Whether the corners of the window should be sharp + sharp_corners: bool, + /// HeaderBar used for server-side decorations is_ssd: bool, @@ -409,7 +413,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .apply(widget::container) .class(crate::theme::Container::HeaderBar { focused: self.focused, - sharp_corners: self.maximized, + sharp_corners: self.sharp_corners, }) .center_y(Length::Shrink) .apply(widget::mouse_area); From 34f55d6720b8623050b7ac6153d07cabae253bf8 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 3 Oct 2025 17:52:31 -0400 Subject: [PATCH 117/352] fix: surface cleanup --- src/app/cosmic.rs | 26 ++++++++++++++++++++------ 1 file changed, 20 insertions(+), 6 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 42ae122b..c53bb6a6 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -93,6 +93,7 @@ pub struct Cosmic { ), >, pub tracked_windows: HashSet, + pub opened_surfaces: HashMap, } impl Cosmic @@ -161,6 +162,7 @@ where } }) { let settings = settings(&mut self.app); + self.get_subsurface(settings, *view) } else { iced_winit::commands::subsurface::get_subsurface(settings(&mut self.app)) @@ -188,6 +190,7 @@ where } }) { let settings = settings(); + self.get_subsurface(settings, Box::new(move |_| view())) } else { iced_winit::commands::subsurface::get_subsurface(settings()) @@ -292,6 +295,8 @@ where } }) { let settings = settings(&mut self.app); + self.tracked_windows.insert(id); + self.get_window(id, settings, *view) } else { let settings = settings(&mut self.app); @@ -327,6 +332,8 @@ where } }) { let settings = settings(); + self.tracked_windows.insert(id); + self.get_window(id, settings, Box::new(move |_| view())) } else { let settings = settings(); @@ -1035,9 +1042,15 @@ impl Cosmic { Action::Surface(action) => return self.surface_update(action), Action::SurfaceClosed(id) => { - #[cfg(feature = "wayland")] - self.surface_views.remove(&id); - self.tracked_windows.remove(&id); + if self.opened_surfaces.get_mut(&id).is_some_and(|v| { + *v = v.saturating_sub(1); + *v == 0 + }) { + self.opened_surfaces.remove(&id); + #[cfg(feature = "wayland")] + self.surface_views.remove(&id); + self.tracked_windows.remove(&id); + } let mut ret = if let Some(msg) = self.app.on_close_requested(id) { self.app.update(msg) @@ -1201,7 +1214,6 @@ impl Cosmic { core.applet.suggested_bounds = b; } Action::Opened(id) => { - self.tracked_windows.insert(id); #[cfg(feature = "wayland")] if self.app.core().sync_window_border_radii_to_theme() { use iced_runtime::platform_specific::wayland::CornerRadius; @@ -1254,6 +1266,7 @@ impl Cosmic { #[cfg(feature = "wayland")] surface_views: HashMap::new(), tracked_windows: HashSet::new(), + opened_surfaces: HashMap::new(), } } @@ -1268,6 +1281,7 @@ impl Cosmic { ) -> Task> { use iced_winit::commands::subsurface::get_subsurface; + *self.opened_surfaces.entry(settings.id).or_insert_with(|| 0) += 1; self.surface_views.insert( settings.id, ( @@ -1289,7 +1303,7 @@ impl Cosmic { >, ) -> Task> { use iced_winit::commands::popup::get_popup; - + *self.opened_surfaces.entry(settings.id).or_insert_with(|| 0) += 1; self.surface_views.insert( settings.id, ( @@ -1312,7 +1326,7 @@ impl Cosmic { >, ) -> Task> { use iced_winit::SurfaceIdWrapper; - + *self.opened_surfaces.entry(id.clone()).or_insert_with(|| 0) += 1; self.surface_views.insert( id.clone(), ( From a27bb5e05ddb89651f86b1576a2567e36f570352 Mon Sep 17 00:00:00 2001 From: Cheong Lau <234708519+Cheong-Lau@users.noreply.github.com> Date: Sun, 5 Oct 2025 12:27:32 +1000 Subject: [PATCH 118/352] chore: apply clippy suggestions --- cosmic-config/src/lib.rs | 2 +- cosmic-config/src/subscription.rs | 2 +- cosmic-theme/src/model/theme.rs | 8 ++--- cosmic-theme/src/steps.rs | 2 +- src/app/cosmic.rs | 42 +++++++++++------------ src/app/mod.rs | 4 +-- src/desktop.rs | 7 ++-- src/dialog/file_chooser/save.rs | 6 ++++ src/theme/mod.rs | 8 ++--- src/theme/style/iced.rs | 21 ++++-------- src/widget/about.rs | 10 +++--- src/widget/button/widget.rs | 10 +++--- src/widget/calendar.rs | 2 +- src/widget/color_picker/mod.rs | 2 +- src/widget/dropdown/multi/menu.rs | 10 +++--- src/widget/dropdown/multi/model.rs | 4 +-- src/widget/header_bar.rs | 6 ++-- src/widget/menu/menu_bar.rs | 6 ++-- src/widget/menu/menu_inner.rs | 4 +-- src/widget/menu/menu_tree.rs | 2 +- src/widget/nav_bar.rs | 4 +-- src/widget/responsive_menu_bar.rs | 2 +- src/widget/segmented_button/horizontal.rs | 2 +- src/widget/segmented_button/model/mod.rs | 8 +++-- src/widget/segmented_button/vertical.rs | 2 +- src/widget/segmented_button/widget.rs | 32 +++++++---------- src/widget/segmented_control.rs | 4 +-- src/widget/settings/item.rs | 2 +- src/widget/tab_bar.rs | 4 +-- src/widget/table/model/mod.rs | 8 ++--- src/widget/table/widget/compact.rs | 9 +++-- src/widget/table/widget/standard.rs | 16 ++++----- src/widget/text_input/input.rs | 7 ++-- src/widget/text_input/value.rs | 4 +-- 34 files changed, 116 insertions(+), 146 deletions(-) diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 72b02371..261b4412 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -229,7 +229,7 @@ impl Config { // Start a transaction (to set multiple configs at the same time) #[inline] - pub fn transaction(&self) -> ConfigTransaction { + pub fn transaction(&self) -> ConfigTransaction<'_> { ConfigTransaction { config: self, updates: Mutex::new(Vec::new()), diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs index 88f8bfa2..32f48849 100644 --- a/cosmic-config/src/subscription.rs +++ b/cosmic-config/src/subscription.rs @@ -93,7 +93,7 @@ async fn start_listening { let update = crate::Update { - errors: errors, + errors, keys: Vec::new(), config: t.clone(), }; diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index d1d3ae0a..1f94f5a2 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -814,7 +814,7 @@ pub struct ThemeBuilder { impl Default for ThemeBuilder { fn default() -> Self { Self { - palette: DARK_PALETTE.to_owned().into(), + palette: DARK_PALETTE.to_owned(), spacing: Spacing::default(), corner_radii: CornerRadii::default(), neutral_tint: Default::default(), @@ -1077,7 +1077,7 @@ impl ThemeBuilder { component_pressed_overlay = component_hovered_overlay; component_pressed_overlay.alpha = 0.2; - let container = Container::new( + Container::new( Component::component( component_base, accent, @@ -1101,9 +1101,7 @@ impl ThemeBuilder { ), get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), is_high_contrast, - ); - - container + ) }; let accent_text = if is_dark { diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs index 6c0779c2..143cf532 100644 --- a/cosmic-theme/src/steps.rs +++ b/cosmic-theme/src/steps.rs @@ -93,7 +93,7 @@ pub fn get_text( let index = get_index(base_index, 70, step_array.len(), is_dark) .or_else(|| get_index(base_index, 50, step_array.len(), is_dark)) - .unwrap_or_else(|| if is_dark { 99 } else { 0 }); + .unwrap_or(if is_dark { 99 } else { 0 }); *step_array.get(index).unwrap_or(fallback) } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index c53bb6a6..2e4b3cb9 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -114,7 +114,7 @@ where ( Self::new(model), - Task::batch(vec![ + Task::batch([ command, iced_runtime::window::run_with_handle(id, init_windowing_system), ]), @@ -665,23 +665,21 @@ impl Cosmic { bottom_left: radii[3].round() as u32, }; let rounded = !self.app.core().window.sharp_corners; - return Task::batch(vec![ - corner_radius( - id, - if rounded { - Some(cur_rad) - } else { - let rad_0 = t.radius_0(); - Some(CornerRadius { - top_left: rad_0[0].round() as u32, - top_right: rad_0[1].round() as u32, - bottom_right: rad_0[2].round() as u32, - bottom_left: rad_0[3].round() as u32, - }) - }, - ) - .discard(), - ]); + return Task::batch([corner_radius( + id, + if rounded { + Some(cur_rad) + } else { + let rad_0 = t.radius_0(); + Some(CornerRadius { + top_left: rad_0[0].round() as u32, + top_right: rad_0[1].round() as u32, + bottom_right: rad_0[2].round() as u32, + bottom_left: rad_0[3].round() as u32, + }) + }, + ) + .discard()]); } } @@ -1061,7 +1059,7 @@ impl Cosmic { if core.exit_on_main_window_closed && core.main_window_id().is_some_and(|m_id| id == m_id) { - ret = Task::batch(vec![iced::exit::>()]); + ret = Task::batch([iced::exit::>()]); } return ret; } @@ -1231,7 +1229,7 @@ impl Cosmic { // TODO do we need per window sharp corners? let rounded = !self.app.core().window.sharp_corners; - return Task::batch(vec![ + return Task::batch([ corner_radius( id, if rounded { @@ -1326,9 +1324,9 @@ impl Cosmic { >, ) -> Task> { use iced_winit::SurfaceIdWrapper; - *self.opened_surfaces.entry(id.clone()).or_insert_with(|| 0) += 1; + *self.opened_surfaces.entry(id).or_insert(0) += 1; self.surface_views.insert( - id.clone(), + id, ( None, // TODO parent for window, platform specific option maybe? SurfaceIdWrapper::Window(id), diff --git a/src/app/mod.rs b/src/app/mod.rs index a2f8d6ad..eaf0bae6 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -420,10 +420,10 @@ where } /// Constructs the view for the main window. - fn view(&self) -> Element; + fn view(&self) -> Element<'_, Self::Message>; /// Constructs views for other windows. - fn view_window(&self, id: window::Id) -> Element { + fn view_window(&self, id: window::Id) -> Element<'_, Self::Message> { panic!("no view for window {id:?}"); } diff --git a/src/desktop.rs b/src/desktop.rs index d41f29a2..687fa6c4 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -136,7 +136,7 @@ pub fn load_applications_for_app_ids<'a>( } #[cfg(not(windows))] -pub fn load_desktop_file<'a>(locales: &'a [String], path: PathBuf) -> Option { +pub fn load_desktop_file(locales: &[String], path: PathBuf) -> Option { fde::DesktopEntry::from_path(path, Some(locales)) .ok() .map(|de| DesktopEntryData::from_desktop_entry(locales, de)) @@ -144,10 +144,7 @@ pub fn load_desktop_file<'a>(locales: &'a [String], path: PathBuf) -> Option( - locales: &'a [String], - de: fde::DesktopEntry, - ) -> DesktopEntryData { + pub fn from_desktop_entry(locales: &[String], de: fde::DesktopEntry) -> DesktopEntryData { let name = de .name(locales) .unwrap_or(Cow::Borrowed(&de.appid)) diff --git a/src/dialog/file_chooser/save.rs b/src/dialog/file_chooser/save.rs index cfb1382b..d7a2a34e 100644 --- a/src/dialog/file_chooser/save.rs +++ b/src/dialog/file_chooser/save.rs @@ -120,6 +120,12 @@ impl Dialog { } } +impl Default for Dialog { + fn default() -> Self { + Self::new() + } +} + #[cfg(feature = "xdg-portal")] mod portal { use super::Dialog; diff --git a/src/theme/mod.rs b/src/theme/mod.rs index f01180c1..b7e85237 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -22,15 +22,15 @@ pub type CosmicColor = ::palette::rgb::Srgba; pub type CosmicComponent = cosmic_theme::Component; pub type CosmicTheme = cosmic_theme::Theme; -pub static COSMIC_DARK: LazyLock = LazyLock::new(|| CosmicTheme::dark_default()); +pub static COSMIC_DARK: LazyLock = LazyLock::new(CosmicTheme::dark_default); pub static COSMIC_HC_DARK: LazyLock = - LazyLock::new(|| CosmicTheme::high_contrast_dark_default()); + LazyLock::new(CosmicTheme::high_contrast_dark_default); -pub static COSMIC_LIGHT: LazyLock = LazyLock::new(|| CosmicTheme::light_default()); +pub static COSMIC_LIGHT: LazyLock = LazyLock::new(CosmicTheme::light_default); pub static COSMIC_HC_LIGHT: LazyLock = - LazyLock::new(|| CosmicTheme::high_contrast_light_default()); + LazyLock::new(CosmicTheme::high_contrast_light_default); pub static TRANSPARENT_COMPONENT: LazyLock = LazyLock::new(|| Component { base: CosmicColor::new(0.0, 0.0, 0.0, 0.0), diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index c8dacbb9..32309860 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -898,12 +898,10 @@ impl toggler::Catalog for Theme { let mut active = toggler::Style { background: if matches!(status, toggler::Status::Active { is_toggled: true }) { cosmic.accent.base.into() + } else if cosmic.is_dark { + cosmic.palette.neutral_6.into() } else { - if cosmic.is_dark { - cosmic.palette.neutral_6.into() - } else { - cosmic.palette.neutral_5.into() - } + cosmic.palette.neutral_5.into() }, foreground: cosmic.palette.neutral_2.into(), border_radius: cosmic.radius_xl().into(), @@ -1166,11 +1164,7 @@ impl scrollable::Catalog for Theme { }, gap: None, }; - let small_widget_container = self - .current_container() - .small_widget - .clone() - .with_alpha(0.7); + let small_widget_container = self.current_container().small_widget.with_alpha(0.7); if matches!(class, Scrollable::Permanent) { a.horizontal_rail.background = @@ -1233,11 +1227,8 @@ impl scrollable::Catalog for Theme { }; if matches!(class, Scrollable::Permanent) { - let small_widget_container = self - .current_container() - .small_widget - .clone() - .with_alpha(0.7); + let small_widget_container = + self.current_container().small_widget.with_alpha(0.7); a.horizontal_rail.background = Some(Background::Color(small_widget_container.into())); diff --git a/src/widget/about.rs b/src/widget/about.rs index 9f2276c8..628f53c6 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -152,13 +152,11 @@ pub fn about<'a, Message: Clone + 'static>( let artists_section = section(&about.artists, fl!("artists")); let translators_section = section(&about.translators, fl!("translators")); let documenters_section = section(&about.documenters, fl!("documenters")); - let license_section = about.license.as_ref().and_then(|license| { + let license_section = about.license.as_ref().map(|license| { let url = about.license_url.as_deref().unwrap_or_default(); - Some( - widget::settings::section() - .title(fl!("license")) - .add(section_button(license, url)), - ) + widget::settings::section() + .title(fl!("license")) + .add(section_button(license, url)) }); let copyright = about.copyright.as_ref().map(widget::text::body); let comments = about.comments.as_ref().map(widget::text::body); diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 3f5a1fdf..87233330 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -793,7 +793,7 @@ pub fn update<'a, Message: Clone>( } Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) => { - if let Some(on_press) = on_press.clone() { + if let Some(on_press) = on_press { let state = state(); if state.is_pressed { @@ -816,9 +816,9 @@ pub fn update<'a, Message: Clone>( #[cfg(feature = "a11y")] Event::A11y(event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => { let state = state(); - if let Some(Some(on_press)) = (event_id == event_id - && matches!(action, iced_accessibility::accesskit::Action::Default)) - .then(|| on_press.clone()) + if let Some(on_press) = matches!(action, iced_accessibility::accesskit::Action::Default) + .then_some(on_press) + .flatten() { state.is_pressed = false; let msg = (on_press)(layout.virtual_offset(), layout.bounds()); @@ -828,7 +828,7 @@ pub fn update<'a, Message: Clone>( return event::Status::Captured; } Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { - if let Some(on_press) = on_press.clone() { + if let Some(on_press) = on_press { let state = state(); if state.is_focused && key == keyboard::Key::Named(keyboard::key::Named::Enter) { state.is_pressed = true; diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index 303a1ed9..a1aace33 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -17,7 +17,7 @@ pub fn calendar( on_prev: impl Fn() -> M + 'static, on_next: impl Fn() -> M + 'static, first_day_of_week: Weekday, -) -> Calendar { +) -> Calendar<'_, M> { Calendar { model, on_select: Box::new(on_select), diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index a17625dc..536531a4 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -233,7 +233,7 @@ impl ColorPickerModel { pub fn builder( &self, on_update: fn(ColorPickerUpdate) -> Message, - ) -> ColorPickerBuilder { + ) -> ColorPickerBuilder<'_, Message> { ColorPickerBuilder { model: &self.segmented_model, active_color: self.active_color, diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index da103f8a..10b0d8d4 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -673,8 +673,8 @@ pub(super) enum OptionElement<'a, S, Item> { } impl Model { - pub(super) fn elements(&self) -> impl Iterator> + '_ { - let iterator = self.lists.iter().flat_map(|list| { + pub(super) fn elements(&self) -> impl Iterator> + '_ { + self.lists.iter().flat_map(|list| { let description = list .description .as_ref() @@ -686,9 +686,7 @@ impl Model { description .chain(options) .chain(std::iter::once(OptionElement::Separator)) - }); - - iterator + }) } fn element_heights( @@ -709,7 +707,7 @@ impl Model { text_line_height: f32, offset: f32, height: f32, - ) -> impl Iterator, f32)> + '_ { + ) -> impl Iterator, f32)> + '_ { let heights = self.element_heights(padding_vertical, text_line_height); let mut current = 0.0; diff --git a/src/widget/dropdown/multi/model.rs b/src/widget/dropdown/multi/model.rs index 12bf4269..f67f8edd 100644 --- a/src/widget/dropdown/multi/model.rs +++ b/src/widget/dropdown/multi/model.rs @@ -66,9 +66,7 @@ impl Model { } pub(super) fn next(&self) -> Option<&(S, Item)> { - let Some(item) = self.selected.as_ref() else { - return None; - }; + let item = self.selected.as_ref()?; let mut next = false; for list in &self.lists { diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 3f4b5d88..01a8d559 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -293,11 +293,9 @@ impl Widget ) -> iced_accessibility::A11yTree { let c_layout = layout.children().next().unwrap(); let c_state = &state.children[0]; - let ret = self - .header_bar_inner + self.header_bar_inner .as_widget() - .a11y_nodes(c_layout, c_state, p); - ret + .a11y_nodes(c_layout, c_state, p) } } diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 30c802c1..bbbb4a2b 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -97,7 +97,7 @@ impl Default for MenuBarStateInner { } } -pub(crate) fn menu_roots_children(menu_roots: &Vec>) -> Vec +pub(crate) fn menu_roots_children(menu_roots: &[MenuTree]) -> Vec where Message: Clone + 'static, { @@ -126,7 +126,7 @@ where } #[allow(invalid_reference_casting)] -pub(crate) fn menu_roots_diff(menu_roots: &mut Vec>, tree: &mut Tree) +pub(crate) fn menu_roots_diff(menu_roots: &mut [MenuTree], tree: &mut Tree) where Message: Clone + 'static, { @@ -381,7 +381,7 @@ where let surface_action = self.on_surface_action.as_ref().unwrap(); let old_active_root = my_state .inner - .with_data(|state| state.active_root.get(0).copied()); + .with_data(|state| state.active_root.first().copied()); // if position is not on menu bar button skip. let hovered_root = layout diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 6c694de7..18f9940d 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -435,7 +435,7 @@ impl MenuState { pub(crate) struct Menu<'b, Message: std::clone::Clone> { pub(crate) tree: MenuBarState, // Flattened menu tree - pub(crate) menu_roots: Cow<'b, Vec>>, + pub(crate) menu_roots: Cow<'b, [MenuTree]>, pub(crate) bounds_expand: u16, /// Allows menu overlay items to overlap the parent pub(crate) menu_overlays_parent: bool, @@ -740,7 +740,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { let styling = theme.appearance(&self.style); let roots = active_root.iter().skip(1).fold( &self.menu_roots[active_root[0]].children, - |mt, next_active_root| (&mt[*next_active_root].children), + |mt, next_active_root| &mt[*next_active_root].children, ); let indices = state.get_trimmed_indices(self.depth).collect::>(); state.menu_states[if self.is_overlay { 0 } else { self.depth }..=if self.is_overlay { diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 67f999f7..e63e523b 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -119,7 +119,7 @@ impl MenuTree { }); mt.children.iter().for_each(|c| { - rec(&c, flat); + rec(c, flat); }); } diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index 1ae4005d..140385bc 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -26,7 +26,7 @@ pub type Model = segmented_button::SingleSelectModel; pub fn nav_bar( model: &segmented_button::SingleSelectModel, on_activate: fn(segmented_button::Entity) -> Message, -) -> NavBar { +) -> NavBar<'_, Message> { NavBar { segmented_button: segmented_button::vertical(model).on_activate(on_activate), } @@ -41,7 +41,7 @@ pub fn nav_bar_dnd( on_dnd_leave: impl Fn(segmented_button::Entity) -> Message + 'static, on_dnd_drop: impl Fn(segmented_button::Entity, Option, DndAction) -> Message + 'static, id: DragId, -) -> NavBar +) -> NavBar<'_, Message> where Message: Clone + 'static, { diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index 3c9151e7..5f855260 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -132,7 +132,7 @@ impl ResponsiveMenuBar { key_binds, trees .into_iter() - .map(|mt| menu::Item::Folder(mt.0, mt.1.into())) + .map(|mt| menu::Item::Folder(mt.0, mt.1)) .collect(), ) .into_iter() diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 966f3a7c..3e46dd5e 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -23,7 +23,7 @@ pub struct Horizontal; /// For details on the model, see the [`segmented_button`](super) module for more details. pub fn horizontal( model: &Model, -) -> SegmentedButton +) -> SegmentedButton<'_, Horizontal, SelectionMode, Message> where Model: Selectable, { diff --git a/src/widget/segmented_button/model/mod.rs b/src/widget/segmented_button/model/mod.rs index 83a1702d..6b5a8a64 100644 --- a/src/widget/segmented_button/model/mod.rs +++ b/src/widget/segmented_button/model/mod.rs @@ -292,7 +292,7 @@ where /// ``` #[must_use] #[inline] - pub fn insert(&mut self) -> EntityMut { + pub fn insert(&mut self) -> EntityMut<'_, SelectionMode> { let id = self.items.insert(Settings::default()); self.order.push_back(id); EntityMut { model: self, id } @@ -447,7 +447,11 @@ where /// println!("{:?} had text {}", id, old_text) /// } /// ``` - pub fn text_set(&mut self, id: Entity, text: impl Into>) -> Option> { + pub fn text_set( + &mut self, + id: Entity, + text: impl Into>, + ) -> Option> { if !self.contains_item(id) { return None; } diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index ce9f50fe..7963e9c8 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -22,7 +22,7 @@ pub type VerticalSegmentedButton<'a, SelectionMode, Message> = /// For details on the model, see the [`segmented_button`](super) module for more details. pub fn vertical( model: &Model, -) -> SegmentedButton +) -> SegmentedButton<'_, Vertical, SelectionMode, Message> where Model: Selectable, SelectionMode: Default, diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 0fd8dcd6..3cbe12f9 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -263,7 +263,7 @@ where /// Check if an item is enabled. fn is_enabled(&self, key: Entity) -> bool { - self.model.items.get(key).map_or(false, |item| item.enabled) + self.model.items.get(key).is_some_and(|item| item.enabled) } /// Handle the dnd drop event. @@ -987,7 +987,7 @@ where let current = Instant::now(); // Permit successive scroll wheel events only after a given delay. - if state.wheel_timestamp.map_or(true, |previous| { + if state.wheel_timestamp.is_none_or(|previous| { current.duration_since(previous) > Duration::from_millis(250) }) { state.wheel_timestamp = Some(current); @@ -1607,23 +1607,16 @@ where let state = tree.state.downcast_ref::(); let menu_state = state.menu_state.clone(); - let Some(entity) = state.show_context else { - return None; - }; + let entity = state.show_context?; - let bounds = self - .variant_bounds(state, layout.bounds()) - .find_map(|item| match item { - ItemBounds::Button(e, bounds) if e == entity => Some(bounds), - _ => None, - }); - let Some(mut bounds) = bounds else { - return None; - }; + let mut bounds = + self.variant_bounds(state, layout.bounds()) + .find_map(|item| match item { + ItemBounds::Button(e, bounds) if e == entity => Some(bounds), + _ => None, + })?; - let Some(context_menu) = self.context_menu.as_mut() else { - return None; - }; + let context_menu = self.context_menu.as_mut()?; if !menu_state.inner.with_data(|data| data.open) { // If the menu is not open, we don't need to show it. @@ -1777,9 +1770,8 @@ impl LocalState { impl operation::Focusable for LocalState { fn is_focused(&self) -> bool { - self.focused.map_or(false, |f| { - f.updated_at == LAST_FOCUS_UPDATE.with(|f| f.get()) - }) + self.focused + .is_some_and(|f| f.updated_at == LAST_FOCUS_UPDATE.with(|f| f.get())) } fn focus(&mut self) { diff --git a/src/widget/segmented_control.rs b/src/widget/segmented_control.rs index 0c213b2c..046956c7 100644 --- a/src/widget/segmented_control.rs +++ b/src/widget/segmented_control.rs @@ -16,7 +16,7 @@ use super::segmented_button::{ /// For details on the model, see the [`segmented_button`] module for more details. pub fn horizontal( model: &Model, -) -> HorizontalSegmentedButton +) -> HorizontalSegmentedButton<'_, SelectionMode, Message> where Model: Selectable, { @@ -39,7 +39,7 @@ where /// For details on the model, see the [`segmented_button`] module for more details. pub fn vertical( model: &Model, -) -> VerticalSegmentedButton +) -> VerticalSegmentedButton<'_, SelectionMode, Message> where Model: Selectable, SelectionMode: Default, diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index a8c38a0d..d62bbc99 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -131,7 +131,7 @@ impl<'a, Message: 'static> Item<'a, Message> { contents.push(text(self.title).width(Length::Fill).into()); } - contents.push(widget.into()); + contents.push(widget); contents } diff --git a/src/widget/tab_bar.rs b/src/widget/tab_bar.rs index 4f4c6149..a08128b4 100644 --- a/src/widget/tab_bar.rs +++ b/src/widget/tab_bar.rs @@ -16,7 +16,7 @@ use super::segmented_button::{ /// For details on the model, see the [`segmented_button`] module for more details. pub fn horizontal( model: &Model, -) -> HorizontalSegmentedButton +) -> HorizontalSegmentedButton<'_, SelectionMode, Message> where Model: Selectable, { @@ -37,7 +37,7 @@ where /// For details on the model, see the [`segmented_button`] module for more details. pub fn vertical( model: &Model, -) -> VerticalSegmentedButton +) -> VerticalSegmentedButton<'_, SelectionMode, Message> where Model: Selectable, SelectionMode: Default, diff --git a/src/widget/table/model/mod.rs b/src/widget/table/model/mod.rs index f664e438..d6250eaf 100644 --- a/src/widget/table/model/mod.rs +++ b/src/widget/table/model/mod.rs @@ -221,7 +221,7 @@ where /// let id = model.insert().text("Item A").icon("custom-icon").id(); /// ``` #[must_use] - pub fn insert(&mut self, item: Item) -> EntityMut { + pub fn insert(&mut self, item: Item) -> EntityMut<'_, SelectionMode, Item, Category> { let id = self.items.insert(item); self.order.push_back(id); EntityMut { model: self, id } @@ -244,7 +244,7 @@ where /// ``` #[must_use] pub fn is_enabled(&self, id: Entity) -> bool { - self.active.get(id).map_or(false, |e| *e) + self.active.get(id).is_some_and(|e| *e) } /// Iterates across items in the model in the order that they are displayed. @@ -288,9 +288,7 @@ where /// } /// ``` pub fn position_set(&mut self, id: Entity, position: u16) -> Option { - let Some(index) = self.position(id) else { - return None; - }; + let index = self.position(id)?; self.order.remove(index as usize); diff --git a/src/widget/table/widget/compact.rs b/src/widget/table/widget/compact.rs index 47864f6d..7cda2dfb 100644 --- a/src/widget/table/widget/compact.rs +++ b/src/widget/table/widget/compact.rs @@ -63,7 +63,7 @@ where .map(|entity| { let item = val.model.item(entity).unwrap(); let selected = val.model.is_active(entity); - let context_menu = (val.item_context_builder)(&item); + let context_menu = (val.item_context_builder)(item); widget::column() .spacing(val.item_spacing) @@ -89,14 +89,13 @@ where .categories .iter() .skip_while(|cat| **cat != Category::default()) - .map(|category| { - vec![ + .flat_map(|category| { + [ widget::text::caption(item.get_text(*category)) .apply(Element::from), widget::text::caption("-").apply(Element::from), ] }) - .flatten() .collect::>>(); elements.pop(); elements @@ -201,7 +200,7 @@ where divider_padding: Padding::from(0).left(space_xxxs).right(space_xxxs), - item_padding: Padding::from(space_xxs).into(), + item_padding: Padding::from(space_xxs), item_spacing: 0, icon_size: 48, diff --git a/src/widget/table/widget/standard.rs b/src/widget/table/widget/standard.rs index eb9ba7a4..3ee1ac4a 100644 --- a/src/widget/table/widget/standard.rs +++ b/src/widget/table/widget/standard.rs @@ -139,13 +139,13 @@ where } else { val.model .iter() - .map(move |entity| { + .flat_map(move |entity| { let item = val.model.item(entity).unwrap(); let categories = &val.model.categories; let selected = val.model.is_active(entity); - let item_context = (val.item_context_builder)(&item); + let item_context = (val.item_context_builder)(item); - vec![ + [ divider::horizontal::default() .apply(container) .padding(val.divider_padding) @@ -233,13 +233,11 @@ where .apply(Element::from), ] }) - .flatten() .collect::>>() }; - vec![vec![header_row], items_full] - .into_iter() - .flatten() - .collect::>>() + let mut elements = items_full; + elements.insert(0, header_row); + elements .apply(widget::column::with_children) .width(val.width) .height(val.height) @@ -272,7 +270,7 @@ where width: Length::Fill, height: Length::Shrink, - item_padding: Padding::from(space_xxs).into(), + item_padding: Padding::from(space_xxs), item_spacing: 0, icon_spacing: space_xxxs, icon_size: 24, diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 12e8e7ce..ab38a718 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -546,7 +546,6 @@ where } /// Get the layout node of the actual text input - fn text_layout<'b>(&'a self, layout: Layout<'b>) -> Layout<'b> { if self.dnd_icon { layout @@ -1389,8 +1388,8 @@ pub fn update<'a, Message: Clone + 'static>( if let Some(cursor_position) = click_position { // Check if the edit button was clicked. - if state.dragging_state == None - && edit_button_layout.map_or(false, |l| cursor.is_over(l.bounds())) + if state.dragging_state.is_none() + && edit_button_layout.is_some_and(|l| cursor.is_over(l.bounds())) { if is_editable_variant { state.is_read_only = !state.is_read_only; @@ -2277,7 +2276,7 @@ pub fn draw<'a, Message>( let (cursor, offset) = if let Some(focus) = state.is_focused.filter(|f| f.focused).or_else(|| { let now = Instant::now(); - handling_dnd_offer.then(|| Focus { + handling_dnd_offer.then_some(Focus { needs_update: false, updated_at: now, now, diff --git a/src/widget/text_input/value.rs b/src/widget/text_input/value.rs index 60647db3..900aac0f 100644 --- a/src/widget/text_input/value.rs +++ b/src/widget/text_input/value.rs @@ -129,9 +129,7 @@ impl Value { #[must_use] pub fn secure(&self) -> Self { Self { - graphemes: std::iter::repeat(String::from("•")) - .take(self.graphemes.len()) - .collect(), + graphemes: std::iter::repeat_n(String::from("•"), self.graphemes.len()).collect(), } } } From 4c4eddb50c79ace202c76b0f6972596930537e1b Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 6 Oct 2025 14:52:39 -0400 Subject: [PATCH 119/352] fix: use is_maximized --- src/app/cosmic.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 2e4b3cb9..ae554846 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -391,7 +391,7 @@ where pub fn style(&self, theme: &Theme) -> iced_runtime::Appearance { if let Some(style) = self.app.style() { style - } else if self.app.core().window.sharp_corners { + } else if self.app.core().window.is_maximized { let theme = THEME.lock().unwrap(); crate::style::iced::application::appearance(theme.borrow()) } else { From 4d4f754318998ea3318ffab15fb96d04b3d33e81 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 6 Oct 2025 11:02:11 +0200 Subject: [PATCH 120/352] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aindriú Mac Giolla Eoin Co-authored-by: Aliaksandr Truš Co-authored-by: Fedorov Alexei Co-authored-by: Hosted Weblate Co-authored-by: Priit Jõerüüt Co-authored-by: Yago Raña Gayoso Co-authored-by: mikenu Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/be/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/es/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/et/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ga/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ja/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ru/ Translation: Pop OS/libcosmic --- i18n/be/libcosmic.ftl | 8 ++++++++ i18n/es/libcosmic.ftl | 7 +++++++ i18n/et/libcosmic.ftl | 1 + i18n/ja/libcosmic.ftl | 8 ++++++++ i18n/ru/libcosmic.ftl | 8 ++++++++ i18n/sr-Cyrl/libcosmic.ftl | 6 ------ 6 files changed, 32 insertions(+), 6 deletions(-) diff --git a/i18n/be/libcosmic.ftl b/i18n/be/libcosmic.ftl index e69de29b..eb3abf33 100644 --- a/i18n/be/libcosmic.ftl +++ b/i18n/be/libcosmic.ftl @@ -0,0 +1,8 @@ +close = Закрыць +license = Ліцэнзія +links = Спасылкі +developers = Распрацоўшчыкі +designers = Дызайнеры +artists = Мастакі +translators = Перакладчыкі +documenters = Дакументалісты diff --git a/i18n/es/libcosmic.ftl b/i18n/es/libcosmic.ftl index e69de29b..6d30b5ad 100644 --- a/i18n/es/libcosmic.ftl +++ b/i18n/es/libcosmic.ftl @@ -0,0 +1,7 @@ +license = Licencia +links = Enlaces +developers = Desarrolladores +designers = Diseñadores +artists = Artistas +translators = Traductores +documenters = Documentadores diff --git a/i18n/et/libcosmic.ftl b/i18n/et/libcosmic.ftl index 1449e0af..38b16698 100644 --- a/i18n/et/libcosmic.ftl +++ b/i18n/et/libcosmic.ftl @@ -5,3 +5,4 @@ developers = Arendajad artists = Kunstnikud translators = Tõlkijad documenters = Dokumenteerijad +designers = Kujundajad diff --git a/i18n/ja/libcosmic.ftl b/i18n/ja/libcosmic.ftl index e69de29b..c6b9ed1a 100644 --- a/i18n/ja/libcosmic.ftl +++ b/i18n/ja/libcosmic.ftl @@ -0,0 +1,8 @@ +close = 閉じる +license = ライセンス +links = リンク +developers = 開発者 +designers = デザイナー +artists = アーティスト +translators = 翻訳者 +documenters = ドキュメント作成者 diff --git a/i18n/ru/libcosmic.ftl b/i18n/ru/libcosmic.ftl index e69de29b..0ef03fb1 100644 --- a/i18n/ru/libcosmic.ftl +++ b/i18n/ru/libcosmic.ftl @@ -0,0 +1,8 @@ +close = Закрыть +license = Лицензия +links = Ссылки +developers = Разработчики +designers = Дизайнеры +artists = Художники +translators = Переводчики +documenters = Авторы документации diff --git a/i18n/sr-Cyrl/libcosmic.ftl b/i18n/sr-Cyrl/libcosmic.ftl index 579392f4..30ed82d3 100644 --- a/i18n/sr-Cyrl/libcosmic.ftl +++ b/i18n/sr-Cyrl/libcosmic.ftl @@ -1,11 +1,5 @@ # Context Drawer close = Затвори - # About license = Лиценца links = Линкови -Developers = Програмери -Designers = Дизајнери -Artists = Уметници -Translators = Преводиоци -Documenters = Документатори From dc4e0edd7311152963c1574ee51540ae5b20e683 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 7 Oct 2025 13:28:42 -0400 Subject: [PATCH 121/352] fix(input): drag threshold --- src/widget/text_input/input.rs | 176 +++++++++++++++++++-------------- 1 file changed, 102 insertions(+), 74 deletions(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index ab38a718..fb889138 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -211,6 +211,7 @@ pub struct TextInput<'a, Message> { always_active: bool, /// The text input tracks and manages the input value in its state. manage_value: bool, + drag_threshold: f32, } impl<'a, Message> TextInput<'a, Message> @@ -259,6 +260,7 @@ where helper_text: None, always_active: false, manage_value: false, + drag_threshold: 20.0, } } @@ -557,6 +559,12 @@ where layout.children().next().unwrap() } } + + /// Set the drag threshold. + pub fn drag_threshold(mut self, drag_threshold: f32) -> Self { + self.drag_threshold = drag_threshold; + self + } } impl Widget for TextInput<'_, Message> @@ -926,6 +934,7 @@ where line_height, layout, self.manage_value, + self.drag_threshold, ) } @@ -1346,6 +1355,7 @@ pub fn update<'a, Message: Clone + 'static>( line_height: text::LineHeight, layout: Layout<'_>, manage_value: bool, + drag_threshold: f32, ) -> event::Status { let update_cache = |state, value| { replace_paragraph( @@ -1424,84 +1434,39 @@ pub fn update<'a, Message: Clone + 'static>( ) { #[cfg(feature = "wayland")] (None, click::Kind::Single, cursor::State::Selection { start, end }) => { - // if something is already selected, we can start a drag and drop for a - // single click that is on top of the selected text - // is the click on selected text? + let left = start.min(end); + let right = end.max(start); - if on_input.is_some() || manage_value { - let left = start.min(end); - let right = end.max(start); + let (left_position, _left_offset) = measure_cursor_and_scroll_offset( + state.value.raw(), + text_layout.bounds(), + left, + ); - let (left_position, _left_offset) = measure_cursor_and_scroll_offset( - state.value.raw(), - text_layout.bounds(), - left, - ); + let (right_position, _right_offset) = measure_cursor_and_scroll_offset( + state.value.raw(), + text_layout.bounds(), + right, + ); - let (right_position, _right_offset) = measure_cursor_and_scroll_offset( - state.value.raw(), - text_layout.bounds(), - right, - ); + let width = right_position - left_position; + let selection_bounds = Rectangle { + x: text_layout.bounds().x + left_position, + y: text_layout.bounds().y, + width, + height: text_layout.bounds().height, + }; - let width = right_position - left_position; - let selection_bounds = Rectangle { - x: text_layout.bounds().x + left_position, - y: text_layout.bounds().y, - width, - height: text_layout.bounds().height, - }; - - if cursor.is_over(selection_bounds) { - // XXX never start a dnd if the input is secure - if is_secure { - return event::Status::Ignored; - } - let input_text = - state.selected_text(&value.to_string()).unwrap_or_default(); - state.dragging_state = Some(DraggingState::Dnd( - DndAction::empty(), - input_text.clone(), - )); - let mut editor = Editor::new(unsecured_value, &mut state.cursor); - editor.delete(); - - let contents = editor.contents(); - let unsecured_value = Value::new(&contents); - state.tracked_value = unsecured_value.clone(); - if let Some(on_input) = on_input { - let message = (on_input)(contents); - shell.publish(message); - } - if let Some(on_start_dnd) = on_start_dnd_source { - shell.publish(on_start_dnd(state.clone())); - } - let state_clone = state.clone(); - - iced_core::clipboard::start_dnd( - clipboard, - false, - id.map(iced_core::clipboard::DndSource::Widget), - Some(iced_core::clipboard::IconSurface::new( - Element::from( - TextInput::<'static, ()>::new("", input_text.clone()) - .dnd_icon(true), - ), - iced_core::widget::tree::State::new(state_clone), - Vector::ZERO, - )), - Box::new(TextInputString(input_text)), - DndAction::Move, - ); - - update_cache(state, &unsecured_value); - } else { - update_cache(state, value); - state.setting_selection(value, text_layout.bounds(), target); - } - } else { - state.setting_selection(value, text_layout.bounds(), target); + if cursor.is_over(selection_bounds) && (on_input.is_some() || manage_value) + { + state.dragging_state = Some(DraggingState::PrepareDnd(cursor_position)); + return event::Status::Captured; } + // 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; } (None, click::Kind::Single, _) => { state.setting_selection(value, text_layout.bounds(), target); @@ -1575,6 +1540,15 @@ pub fn update<'a, Message: Clone + 'static>( | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => { cold(); let state = state(); + #[cfg(feature = "wayland")] + if matches!(state.dragging_state, Some(DraggingState::PrepareDnd(_))) { + // 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; + state.setting_selection(value, text_layout.bounds(), target); + } + } state.dragging_state = None; return if cursor.is_over(layout.bounds()) { @@ -1598,6 +1572,58 @@ pub fn update<'a, Message: Clone + 'static>( .cursor .select_range(state.cursor.start(value), position); + return event::Status::Captured; + } + #[cfg(feature = "wayland")] + 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)) + .sqrt(); + + if distance >= drag_threshold { + if is_secure { + return event::Status::Ignored; + } + + let input_text = state.selected_text(&value.to_string()).unwrap_or_default(); + state.dragging_state = + Some(DraggingState::Dnd(DndAction::empty(), input_text.clone())); + let mut editor = Editor::new(unsecured_value, &mut state.cursor); + editor.delete(); + + let contents = editor.contents(); + let unsecured_value = Value::new(&contents); + state.tracked_value = unsecured_value.clone(); + if let Some(on_input) = on_input { + let message = (on_input)(contents); + shell.publish(message); + } + if let Some(on_start_dnd) = on_start_dnd_source { + shell.publish(on_start_dnd(state.clone())); + } + let state_clone = state.clone(); + + iced_core::clipboard::start_dnd( + clipboard, + false, + id.map(iced_core::clipboard::DndSource::Widget), + Some(iced_core::clipboard::IconSurface::new( + Element::from( + TextInput::<'static, ()>::new("", input_text.clone()) + .dnd_icon(true), + ), + iced_core::widget::tree::State::new(state_clone), + Vector::ZERO, + )), + Box::new(TextInputString(input_text)), + DndAction::Move, + ); + + update_cache(state, &unsecured_value); + } else { + state.dragging_state = Some(DraggingState::PrepareDnd(start_position)); + } + return event::Status::Captured; } } @@ -2519,10 +2545,12 @@ impl AsMimeTypes for TextInputString { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq)] pub(crate) enum DraggingState { Selection, #[cfg(feature = "wayland")] + PrepareDnd(Point), + #[cfg(feature = "wayland")] Dnd(DndAction, String), } From d40e9fa4e49397b0c5846b1c243b0b297df5fa36 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 7 Oct 2025 15:55:31 -0400 Subject: [PATCH 122/352] fix: support NotShowIn --- src/desktop.rs | 14 ++++++++++++-- 1 file changed, 12 insertions(+), 2 deletions(-) diff --git a/src/desktop.rs b/src/desktop.rs index 687fa6c4..c9b50704 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -61,9 +61,14 @@ pub fn load_applications<'a>( .filter_map(move |p| fde::DesktopEntry::from_path(p, Some(locales)).ok()) .filter(move |de| { (include_no_display || !de.no_display()) - && !only_show_in.zip(de.only_show_in()).is_some_and( + && only_show_in.zip(de.only_show_in()).is_none_or( |(xdg_current_desktop, only_show_in)| { - !only_show_in.contains(&xdg_current_desktop) + only_show_in.contains(&xdg_current_desktop) + }, + ) + && only_show_in.zip(de.not_show_in()).is_none_or( + |(xdg_current_desktop, not_show_in)| { + !not_show_in.contains(&xdg_current_desktop) }, ) }) @@ -94,6 +99,11 @@ pub fn load_applications_for_app_ids<'a>( ) { return false; } + if only_show_in.zip(de.not_show_in()).is_some_and( + |(xdg_current_desktop, not_show_in)| not_show_in.contains(&xdg_current_desktop), + ) { + return false; + } // Search by ID first app_ids From 2dda96c07f5c020bc69711d8bda7be3084c3d4e2 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Tue, 7 Oct 2025 23:30:29 +0200 Subject: [PATCH 123/352] i18n: translation update from Hosted Weblate (#1008) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * i18n: translation updates from weblate Co-authored-by: Aindriú Mac Giolla Eoin Co-authored-by: Aliaksandr Truš Co-authored-by: Fedorov Alexei Co-authored-by: Guðmundur Erlingsson Co-authored-by: Hosted Weblate Co-authored-by: Ignacio Viggiani Co-authored-by: Priit Jõerüüt Co-authored-by: Yago Raña Gayoso Co-authored-by: mikenu Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/be/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/es/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/et/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ga/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ja/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ru/ Translation: Pop OS/libcosmic * i18n: adding translation for Norwegian Nynorsk --------- Co-authored-by: Aindriú Mac Giolla Eoin Co-authored-by: Aliaksandr Truš Co-authored-by: Fedorov Alexei Co-authored-by: Guðmundur Erlingsson Co-authored-by: Ignacio Viggiani Co-authored-by: Priit Jõerüüt Co-authored-by: Yago Raña Gayoso Co-authored-by: mikenu Co-authored-by: oddib Co-authored-by: Jeremy Soller --- i18n/es/libcosmic.ftl | 1 + i18n/is/libcosmic.ftl | 0 i18n/nn/libcosmic.ftl | 0 3 files changed, 1 insertion(+) create mode 100644 i18n/is/libcosmic.ftl create mode 100644 i18n/nn/libcosmic.ftl diff --git a/i18n/es/libcosmic.ftl b/i18n/es/libcosmic.ftl index 6d30b5ad..3e6e337d 100644 --- a/i18n/es/libcosmic.ftl +++ b/i18n/es/libcosmic.ftl @@ -5,3 +5,4 @@ designers = Diseñadores artists = Artistas translators = Traductores documenters = Documentadores +close = Cerrar diff --git a/i18n/is/libcosmic.ftl b/i18n/is/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/nn/libcosmic.ftl b/i18n/nn/libcosmic.ftl new file mode 100644 index 00000000..e69de29b From f17cd2928a37a09bfce70f4ef1d775eebe138cf0 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 8 Oct 2025 16:45:59 -0400 Subject: [PATCH 124/352] fix: forward events to trailing element regardless of cursor position --- src/widget/text_input/input.rs | 26 ++++++++++++-------------- 1 file changed, 12 insertions(+), 14 deletions(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index fb889138..949f2040 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -876,21 +876,19 @@ 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 { - if cursor_position.is_over(trailing_layout.bounds()) { - let res = trailing_icon.as_widget_mut().on_event( - tree, - event.clone(), - trailing_layout, - cursor_position, - renderer, - clipboard, - shell, - viewport, - ); + let res = trailing_icon.as_widget_mut().on_event( + tree, + event.clone(), + trailing_layout, + cursor_position, + renderer, + clipboard, + shell, + viewport, + ); - if res == event::Status::Captured { - return res; - } + if res == event::Status::Captured { + return res; } } } From a929829521b229aa6426e14ecc1ba4c047f809e1 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 8 Oct 2025 19:04:52 -0400 Subject: [PATCH 125/352] fix(color picker): avoid 0 in color picker slider value --- src/widget/color_picker/mod.rs | 377 +++++++++++++-------------------- 1 file changed, 152 insertions(+), 225 deletions(-) diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 536531a4..87e7a4d3 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -289,237 +289,164 @@ where copy_to_clipboard_label: T, copied_to_clipboard_label: T, ) -> ColorPicker<'a, Message> { + fn rail_backgrounds(hue: f32) -> (Background, Background) { + let pivot = hue * 7.0 / 360.; + + let low_end = pivot.floor() as usize; + let high_start = pivot.ceil() as usize; + let pivot_color = palette::Hsv::new_srgb(RgbHue::new(hue), 1.0, 1.0); + let low_range = HSV_RAINBOW[0..=low_end] + .iter() + .enumerate() + .map(|(i, color)| ColorStop { + color: *color, + offset: i as f32 / pivot.max(0.0001), + }) + .chain(iter::once(ColorStop { + color: iced::Color::from(palette::Srgba::from_color(pivot_color)), + offset: 1., + })) + .collect::>(); + let high_range = iter::once(ColorStop { + color: iced::Color::from(palette::Srgba::from_color(pivot_color)), + offset: 0., + }) + .chain( + HSV_RAINBOW[high_start..] + .iter() + .enumerate() + .map(|(i, color)| ColorStop { + color: *color, + offset: (i as f32 + (1. - pivot.fract())) / (7. - pivot).max(0.0001), + }), + ) + .collect::>(); + ( + Background::Gradient(iced::Gradient::Linear( + Linear::new(Radians(90.0)).add_stops(low_range), + )), + Background::Gradient(iced::Gradient::Linear( + Linear::new(Radians(90.0)).add_stops(high_range), + )), + ) + } + let on_update = self.on_update; let spacing = THEME.lock().unwrap().cosmic().spacing; - let mut inner = - column![ - // segmented buttons - segmented_control::horizontal(self.model) - .on_activate(Box::new(move |e| on_update( - ColorPickerUpdate::ActivateSegmented(e) - ))) - .minimum_button_width(0) - .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)) - .width(self.width) - .height(self.height), - slider( - 0.0..=359.99, - self.active_color.hue.into_positive_degrees(), - move |v| { - let mut new = self.active_color; - new.hue = v.into(); - on_update(ColorPickerUpdate::ActiveColor(new)) - } + + let mut inner = column![ + // segmented buttons + segmented_control::horizontal(self.model) + .on_activate(Box::new(move |e| on_update( + ColorPickerUpdate::ActivateSegmented(e) + ))) + .minimum_button_width(0) + .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)) + .width(self.width) + .height(self.height), + slider( + 0.001..=359.99, + self.active_color.hue.into_positive_degrees(), + move |v| { + let mut new = self.active_color; + new.hue = v.into(); + on_update(ColorPickerUpdate::ActiveColor(new)) + } + ) + .on_release(on_update(ColorPickerUpdate::ActionFinished)) + .class(Slider::Custom { + active: Rc::new(move |t| { + let cosmic = t.cosmic(); + let mut a = + slider::Catalog::style(t, &Slider::default(), slider::Status::Active); + let hue = self.active_color.hue.into_positive_degrees(); + a.rail.backgrounds = rail_backgrounds(hue); + a.rail.width = 8.0; + a.handle.background = Color::TRANSPARENT.into(); + a.handle.shape = HandleShape::Circle { radius: 8.0 }; + a.handle.border_color = cosmic.palette.neutral_10.into(); + a.handle.border_width = 4.0; + a + }), + hovered: Rc::new(move |t| { + let cosmic = t.cosmic(); + let mut a = + slider::Catalog::style(t, &Slider::default(), slider::Status::Active); + let hue = self.active_color.hue.into_positive_degrees(); + a.rail.backgrounds = rail_backgrounds(hue); + a.rail.width = 8.0; + a.handle.background = Color::TRANSPARENT.into(); + a.handle.shape = HandleShape::Circle { radius: 8.0 }; + a.handle.border_color = cosmic.palette.neutral_10.into(); + a.handle.border_width = 4.0; + a + }), + dragging: Rc::new(move |t| { + let cosmic = t.cosmic(); + let mut a = + slider::Catalog::style(t, &Slider::default(), slider::Status::Active); + let hue = self.active_color.hue.into_positive_degrees(); + a.rail.backgrounds = rail_backgrounds(hue); + a.rail.width = 8.0; + a.handle.background = Color::TRANSPARENT.into(); + a.handle.shape = HandleShape::Circle { radius: 8.0 }; + a.handle.border_color = cosmic.palette.neutral_10.into(); + a.handle.border_width = 4.0; + a + }), + }) + .width(self.width), + 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)) + .leading_icon( + color_button( + None, + Some(Color::from(palette::Srgb::from_color(self.active_color))), + Length::FillPortion(12) + ) + .into() ) - .on_release(on_update(ColorPickerUpdate::ActionFinished)) - .class(Slider::Custom { - active: Rc::new(move |t| { - let cosmic = t.cosmic(); - let mut a = - slider::Catalog::style(t, &Slider::default(), slider::Status::Active); + // TODO copy paste input contents + .trailing_icon({ + let button = button::custom(crate::widget::icon( + from_name("edit-copy-symbolic").size(spacing.space_s).into(), + )) + .on_press(on_update(ColorPickerUpdate::Copied(Instant::now()))) + .class(Button::Text); - let hue = self.active_color.hue.into_positive_degrees(); - let pivot = hue * 7.0 / 360.; - - let low_end = pivot.floor() as usize; - let high_start = pivot.ceil() as usize; - let pivot_color = palette::Hsv::new_srgb(RgbHue::new(hue), 1.0, 1.0); - let low_range = HSV_RAINBOW[0..=low_end] - .iter() - .enumerate() - .map(|(i, color)| ColorStop { - color: *color, - offset: i as f32 / pivot.max(0.0001), - }) - .chain(iter::once(ColorStop { - color: iced::Color::from(palette::Srgba::from_color(pivot_color)), - offset: 1., - })) - .collect::>(); - let high_range = - iter::once(ColorStop { - color: iced::Color::from(palette::Srgba::from_color(pivot_color)), - offset: 0., - }) - .chain(HSV_RAINBOW[high_start..].iter().enumerate().map( - |(i, color)| ColorStop { - color: *color, - offset: (i as f32 + (1. - pivot.fract())) - / (7. - pivot).max(0.0001), - }, - )) - .collect::>(); - a.rail.backgrounds = ( - Background::Gradient(iced::Gradient::Linear( - Linear::new(Radians(90.0)).add_stops(low_range), - )), - Background::Gradient(iced::Gradient::Linear( - Linear::new(Radians(90.0)).add_stops(high_range), - )), - ); - - a.rail.width = 8.0; - a.handle.background = Color::TRANSPARENT.into(); - a.handle.shape = HandleShape::Circle { radius: 8.0 }; - a.handle.border_color = cosmic.palette.neutral_10.into(); - a.handle.border_width = 4.0; - a - }), - hovered: Rc::new(move |t| { - let cosmic = t.cosmic(); - let mut a = - slider::Catalog::style(t, &Slider::default(), slider::Status::Active); - let hue = self.active_color.hue.into_positive_degrees(); - let pivot = hue * 7.0 / 360.; - - let low_end = pivot.floor() as usize; - let high_start = pivot.ceil() as usize; - let pivot_color = palette::Hsv::new_srgb(RgbHue::new(hue), 1.0, 1.0); - let low_range = HSV_RAINBOW[0..=low_end] - .iter() - .enumerate() - .map(|(i, color)| ColorStop { - color: *color, - offset: i as f32 / pivot.max(0.0001), - }) - .chain(iter::once(ColorStop { - color: iced::Color::from(palette::Srgba::from_color(pivot_color)), - offset: 1., - })) - .collect::>(); - let high_range = - iter::once(ColorStop { - color: iced::Color::from(palette::Srgba::from_color(pivot_color)), - offset: 0., - }) - .chain(HSV_RAINBOW[high_start..].iter().enumerate().map( - |(i, color)| ColorStop { - color: *color, - offset: (i as f32 + (1. - pivot.fract())) - / (7. - pivot).max(0.0001), - }, - )) - .collect::>(); - a.rail.backgrounds = ( - Background::Gradient(iced::Gradient::Linear( - Linear::new(Radians(90.0)).add_stops(low_range), - )), - Background::Gradient(iced::Gradient::Linear( - Linear::new(Radians(90.0)).add_stops(high_range), - )), - ); - a.rail.width = 8.0; - a.handle.background = Color::TRANSPARENT.into(); - a.handle.shape = HandleShape::Circle { radius: 8.0 }; - a.handle.border_color = cosmic.palette.neutral_10.into(); - a.handle.border_width = 4.0; - a - }), - dragging: Rc::new(move |t| { - let cosmic = t.cosmic(); - let mut a = - slider::Catalog::style(t, &Slider::default(), slider::Status::Active); - let hue = self.active_color.hue.into_positive_degrees(); - let pivot = hue * 7.0 / 360.; - - let low_end = pivot.floor() as usize; - let high_start = pivot.ceil() as usize; - let pivot_color = palette::Hsv::new_srgb(RgbHue::new(hue), 1.0, 1.0); - let low_range = HSV_RAINBOW[0..=low_end] - .iter() - .enumerate() - .map(|(i, color)| ColorStop { - color: *color, - offset: i as f32 / pivot.max(0.0001), - }) - .chain(iter::once(ColorStop { - color: iced::Color::from(palette::Srgba::from_color(pivot_color)), - offset: 1., - })) - .collect::>(); - let high_range = - iter::once(ColorStop { - color: iced::Color::from(palette::Srgba::from_color(pivot_color)), - offset: 0., - }) - .chain(HSV_RAINBOW[high_start..].iter().enumerate().map( - |(i, color)| ColorStop { - color: *color, - offset: (i as f32 + (1. - pivot.fract())) - / (7. - pivot).max(0.0001), - }, - )) - .collect::>(); - a.rail.backgrounds = ( - Background::Gradient(iced::Gradient::Linear( - Linear::new(Radians(90.0)).add_stops(low_range), - )), - Background::Gradient(iced::Gradient::Linear( - Linear::new(Radians(90.0)).add_stops(high_range), - )), - ); - a.rail.width = 8.0; - a.handle.background = Color::TRANSPARENT.into(); - a.handle.shape = HandleShape::Circle { radius: 8.0 }; - a.handle.border_color = cosmic.palette.neutral_10.into(); - a.handle.border_width = 4.0; - a - }), + match self.copied_at.take() { + Some(t) if Instant::now().duration_since(t) > Duration::from_secs(2) => { + button.into() + } + Some(_) => tooltip( + button, + text(copied_to_clipboard_label), + iced_widget::tooltip::Position::Bottom, + ) + .into(), + None => tooltip( + button, + text(copy_to_clipboard_label), + iced_widget::tooltip::Position::Bottom, + ) + .into(), + } }) .width(self.width), - 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)) - .leading_icon( - color_button( - None, - Some(Color::from(palette::Srgb::from_color(self.active_color))), - Length::FillPortion(12) - ) - .into() - ) - // TODO copy paste input contents - .trailing_icon({ - let button = button::custom(crate::widget::icon( - from_name("edit-copy-symbolic").size(spacing.space_s).into(), - )) - .on_press(on_update(ColorPickerUpdate::Copied(Instant::now()))) - .class(Button::Text); - - match self.copied_at.take() { - Some(t) - if Instant::now().duration_since(t) > Duration::from_secs(2) => - { - button.into() - } - Some(_) => tooltip( - button, - text(copied_to_clipboard_label), - iced_widget::tooltip::Position::Bottom, - ) - .into(), - None => tooltip( - button, - text(copy_to_clipboard_label), - iced_widget::tooltip::Position::Bottom, - ) - .into(), - } - }) - .width(self.width), - ] - // Should we ensure the side padding is at least half the width of the handle? - .padding([ - spacing.space_none, - spacing.space_s, - spacing.space_s, - spacing.space_s, - ]) - .spacing(spacing.space_s); + ] + // Should we ensure the side padding is at least half the width of the handle? + .padding([ + spacing.space_none, + spacing.space_s, + spacing.space_s, + spacing.space_s, + ]) + .spacing(spacing.space_s); if !self.recent_colors.is_empty() { inner = inner.push(horizontal::light().width(self.width)); From 804250af64e941aa273687d6fc75bd91ef18e9bf Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 9 Oct 2025 00:07:32 +0200 Subject: [PATCH 126/352] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Feike Donia Co-authored-by: Guðmundur Erlingsson Co-authored-by: Hosted Weblate Co-authored-by: Stepan Denysenko Co-authored-by: oddib Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/is/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/nl/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/nn/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/uk/ Translation: Pop OS/libcosmic --- i18n/is/libcosmic.ftl | 8 ++++++++ i18n/nl/libcosmic.ftl | 1 + i18n/nn/libcosmic.ftl | 2 ++ i18n/uk/libcosmic.ftl | 3 +-- 4 files changed, 12 insertions(+), 2 deletions(-) diff --git a/i18n/is/libcosmic.ftl b/i18n/is/libcosmic.ftl index e69de29b..391eaf08 100644 --- a/i18n/is/libcosmic.ftl +++ b/i18n/is/libcosmic.ftl @@ -0,0 +1,8 @@ +close = Loka +license = Notandaleyfi +links = Tenglar +developers = Forritarar +designers = Hönnuðir +artists = Listafólk +translators = Þýðendur +documenters = Skjölunarhöfundar diff --git a/i18n/nl/libcosmic.ftl b/i18n/nl/libcosmic.ftl index e69de29b..b0aafeba 100644 --- a/i18n/nl/libcosmic.ftl +++ b/i18n/nl/libcosmic.ftl @@ -0,0 +1 @@ +close = Sluiten diff --git a/i18n/nn/libcosmic.ftl b/i18n/nn/libcosmic.ftl index e69de29b..ffa3faf5 100644 --- a/i18n/nn/libcosmic.ftl +++ b/i18n/nn/libcosmic.ftl @@ -0,0 +1,2 @@ +close = Lukk +license = Lisens diff --git a/i18n/uk/libcosmic.ftl b/i18n/uk/libcosmic.ftl index 07dbaf9c..396dab36 100644 --- a/i18n/uk/libcosmic.ftl +++ b/i18n/uk/libcosmic.ftl @@ -1,9 +1,8 @@ # Context Drawer close = Закрити - # About license = Ліцензія -links = Лінки +links = Посилання developers = Розробники designers = Дизайнери artists = Митці From d82e6a167c2e79f6c615ef596d5ec3ddbb71d6b1 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Thu, 9 Oct 2025 12:08:57 -0700 Subject: [PATCH 127/352] Update `iced` Update iced with https://github.com/pop-os/iced/pull/244. --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 521a04d7..8cbf2b70 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 521a04d7e7589fdd61b314c92155277bf350d944 +Subproject commit 8cbf2b70ad229a4bc5b7055e3d0a9eef265bd10d From 483fb2cdd103e44d3d6cfc7522455f683789b87e Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Fri, 10 Oct 2025 01:07:35 +0200 Subject: [PATCH 128/352] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Feike Donia Co-authored-by: Guðmundur Erlingsson Co-authored-by: Hosted Weblate Co-authored-by: Stepan Denysenko Co-authored-by: Ziad El-sayed Co-authored-by: oddib Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ar/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/is/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/nl/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/nn/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/uk/ Translation: Pop OS/libcosmic --- i18n/ar/libcosmic.ftl | 7 +++---- i18n/uk/libcosmic.ftl | 2 +- 2 files changed, 4 insertions(+), 5 deletions(-) diff --git a/i18n/ar/libcosmic.ftl b/i18n/ar/libcosmic.ftl index 4fc8582b..ad86e64e 100644 --- a/i18n/ar/libcosmic.ftl +++ b/i18n/ar/libcosmic.ftl @@ -1,11 +1,10 @@ # Context Drawer close = أغلق - # About license = الترخيص links = الروابط -developers = المطوّرون -designers = المصمّمون +developers = المطورون +designers = المصممون artists = الفنانون translators = المترجمون -documenters = الموثّقون +documenters = الموثقون diff --git a/i18n/uk/libcosmic.ftl b/i18n/uk/libcosmic.ftl index 396dab36..cfdc14b8 100644 --- a/i18n/uk/libcosmic.ftl +++ b/i18n/uk/libcosmic.ftl @@ -5,6 +5,6 @@ license = Ліцензія links = Посилання developers = Розробники designers = Дизайнери -artists = Митці +artists = Художники translators = Перекладачі documenters = Документатори From cd3e9c1493644b5dffc34beb4c6b202fa3db7bc4 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 13 Oct 2025 22:07:31 +0200 Subject: [PATCH 129/352] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aleksandar Anžel <44969003+AAnzel@users.noreply.github.com> Co-authored-by: Hosted Weblate Co-authored-by: sicKat Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/it/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/sr_Cyrl/ Translation: Pop OS/libcosmic --- i18n/it/libcosmic.ftl | 8 ++++++++ i18n/sr-Cyrl/libcosmic.ftl | 5 +++++ 2 files changed, 13 insertions(+) diff --git a/i18n/it/libcosmic.ftl b/i18n/it/libcosmic.ftl index e69de29b..a551a716 100644 --- a/i18n/it/libcosmic.ftl +++ b/i18n/it/libcosmic.ftl @@ -0,0 +1,8 @@ +close = Chiudi +license = Licenza +links = Link +developers = Sviluppatori +designers = Designer +artists = Artisti +translators = Traduttori +documenters = Documentatori diff --git a/i18n/sr-Cyrl/libcosmic.ftl b/i18n/sr-Cyrl/libcosmic.ftl index 30ed82d3..ce6afb28 100644 --- a/i18n/sr-Cyrl/libcosmic.ftl +++ b/i18n/sr-Cyrl/libcosmic.ftl @@ -3,3 +3,8 @@ close = Затвори # About license = Лиценца links = Линкови +developers = Програмер +designers = Дизајнери +artists = Уметници +translators = Преводиоци +documenters = Произвођачи документације From f44d82a7e83af15270a9ca3beb832f4799699337 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 14 Oct 2025 16:28:43 +0200 Subject: [PATCH 130/352] fix(spin_buttton): change text style to body --- src/widget/spin_button.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index eba4641a..a93f2ee4 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -178,7 +178,7 @@ where let decrement_button = make_button!(spin_button, "list-remove-symbolic", decrement); let increment_button = make_button!(spin_button, "list-add-symbolic", increment); - let label = text::title4(spin_button.label) + let label = text::body(spin_button.label) .apply(container) .center_x(Length::Fixed(48.0)) .align_y(Alignment::Center); @@ -201,7 +201,7 @@ where let decrement_button = make_button!(spin_button, "list-remove-symbolic", decrement); let increment_button = make_button!(spin_button, "list-add-symbolic", increment); - let label = text::title4(spin_button.label) + let label = text::body(spin_button.label) .apply(container) .center_x(Length::Fixed(48.0)) .align_y(Alignment::Center); From 76c1897d4d9a637c8aa4016483bf05fec5f10ebd Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Fri, 17 Oct 2025 08:59:39 -0700 Subject: [PATCH 131/352] Update `iced` for `input_zone` change https://github.com/pop-os/iced/pull/241 --- iced | 2 +- src/applet/mod.rs | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/iced b/iced index 8cbf2b70..7bb364e0 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 8cbf2b70ad229a4bc5b7055e3d0a9eef265bd10d +Subproject commit 7bb364e01d6cd6c07703416828006ab497a082e6 diff --git a/src/applet/mod.rs b/src/applet/mod.rs index ded92cf6..6dfaeef6 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -252,10 +252,10 @@ impl Context { parent: parent_id.unwrap_or(window::Id::RESERVED), id: window_id, grab: false, - input_zone: Some(Rectangle::new( + input_zone: Some(vec![Rectangle::new( iced::Point::new(-1000., -1000.), iced::Size::default(), - )), + )]), positioner: SctkPositioner { size: None, size_limits: Limits::NONE.min_width(1.).min_height(1.), From 529eeebaebb34890ce9ca54b5ee8bc4f5bbc0bd5 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 20 Oct 2025 11:47:37 -0400 Subject: [PATCH 132/352] fix: avoid focus effects if already focused --- src/widget/text_input/input.rs | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 949f2040..4c9fa0f9 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -656,7 +656,7 @@ where if index == old_value.len() { state.cursor.move_to(self.value.len()); } - }; + } } if let Some(f) = state.is_focused.as_ref().filter(|f| f.focused) { @@ -2687,6 +2687,7 @@ impl State { pub fn focus(&mut self) { let now = Instant::now(); LAST_FOCUS_UPDATE.with(|x| x.set(now)); + let was_focused = self.is_focused.is_some_and(|f| f.focused); self.is_read_only = false; self.is_focused = Some(Focus { updated_at: now, @@ -2695,6 +2696,9 @@ impl State { needs_update: false, }); + if was_focused { + return; + } if self.select_on_focus { self.select_all() } else { From f2e965c76cddf3bac183e35f2c7b91874b5f2628 Mon Sep 17 00:00:00 2001 From: Eduardo Flores Date: Mon, 20 Oct 2025 12:00:32 -0700 Subject: [PATCH 133/352] fix: dialog body overflows --- src/widget/dialog.rs | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/widget/dialog.rs b/src/widget/dialog.rs index 50bf4f1e..ecc6ef05 100644 --- a/src/widget/dialog.rs +++ b/src/widget/dialog.rs @@ -125,7 +125,9 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes content_col = content_col .push(widget::vertical_space().height(Length::Fixed(space_xxs.into()))); } - content_col = content_col.push(widget::text::body(body)); + content_col = content_col.push( + widget::container(widget::scrollable(widget::text::body(body))).max_height(300.), + ); should_space = true; } for control in dialog.controls { From 2e87bd7c41a4067d6464d085705c6efa48456c83 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 21 Oct 2025 13:03:38 -0400 Subject: [PATCH 134/352] fix(segmented_button): ensure modifier state exact match for tab --- src/widget/segmented_button/widget.rs | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 3cbe12f9..0e725132 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -1054,10 +1054,12 @@ where }) = event { state.focused_visible = true; - return if modifiers.shift() { + return if modifiers == keyboard::Modifiers::SHIFT { self.focus_previous(state) - } else { + } else if modifiers.is_empty() { self.focus_next(state) + } else { + event::Status::Ignored }; } From 840ef21e4de2a3678c25621cf2ae07e643b63c60 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 21 Oct 2025 21:28:21 -0400 Subject: [PATCH 135/352] fix(dnd_destination): Don't capture leave events --- src/widget/dnd_destination.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 121648ab..ccc0fb18 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -359,7 +359,7 @@ impl Widget } return event::Status::Captured; } - Event::Dnd(DndEvent::Offer(id, OfferEvent::Leave)) => { + Event::Dnd(DndEvent::Offer(_, OfferEvent::Leave)) => { if let Some(msg) = state.on_leave(self.on_leave.as_ref().map(std::convert::AsRef::as_ref)) { @@ -380,7 +380,7 @@ impl Widget viewport, ); } - return event::Status::Captured; + return event::Status::Ignored; } Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if id == Some(my_id) => { if let Some(msg) = state.on_motion( @@ -412,13 +412,13 @@ impl Widget } return event::Status::Captured; } - Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) => { + Event::Dnd(DndEvent::Offer(_, OfferEvent::LeaveDestination)) => { if let Some(msg) = state.on_leave(self.on_leave.as_ref().map(std::convert::AsRef::as_ref)) { shell.publish(msg); } - return event::Status::Captured; + return event::Status::Ignored; } Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if id == Some(my_id) => { if let Some(msg) = From bd438a8581f6ab781144e3fbf87ea0aaeb192d33 Mon Sep 17 00:00:00 2001 From: Cheong Lau <234708519+Cheong-Lau@users.noreply.github.com> Date: Sat, 11 Oct 2025 13:36:35 +1000 Subject: [PATCH 136/352] perf: reduce memory allocations This also changes `widget::column::with_children` and `widget::row::with_children` to take an `impl IntoIterator` instead of a `Vec`, like the `iced` variants of these functions do. This shouldn't be a breaking change since passing in a `Vec` will still compile and function exactly as before. (Using `iced::widget::Column::from_vec` or `iced::widget::Row::from_vec` isn't possible, since the elements of the `Vec` aren't checked, so the size of the resulting `Column` or `Row` won't adapt to the size of its children. Perhaps a new function could be added to mirror `iced`'s?) --- cosmic-config/src/lib.rs | 21 +++++++++------------ cosmic-theme/src/output/gtk4_output.rs | 19 ++++++++----------- cosmic-theme/src/output/vs_code.rs | 11 ++++++----- src/applet/mod.rs | 6 +++--- src/applet/token/wayland_handler.rs | 2 +- src/core.rs | 16 ++++++++++++---- src/widget/about.rs | 5 +---- src/widget/calendar.rs | 4 ++-- src/widget/color_picker/mod.rs | 25 ++++++++++--------------- src/widget/dialog.rs | 3 +-- src/widget/dropdown/menu/mod.rs | 8 ++++---- src/widget/dropdown/multi/widget.rs | 4 ++-- src/widget/menu/menu_inner.rs | 2 +- src/widget/menu/menu_tree.rs | 6 +++--- src/widget/mod.rs | 16 ++++++++++------ src/widget/popover.rs | 4 ++-- src/widget/segmented_button/widget.rs | 4 ++-- src/widget/table/widget/compact.rs | 1 - src/widget/table/widget/standard.rs | 2 -- src/widget/text_input/input.rs | 12 ++++++------ 20 files changed, 83 insertions(+), 88 deletions(-) diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 261b4412..5f424cc3 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -170,11 +170,10 @@ impl Config { .map(|x| x.join("COSMIC").join(&path)); // Get libcosmic user configuration directory - let cosmic_user_path = dirs::config_dir() - .ok_or(Error::NoConfigDirectory)? - .join("cosmic"); + let mut user_path = dirs::config_dir().ok_or(Error::NoConfigDirectory)?; + user_path.push("cosmic"); + user_path.push(path); - let user_path = cosmic_user_path.join(path); // Create new configuration directory if not found. fs::create_dir_all(&user_path)?; @@ -190,9 +189,9 @@ impl Config { // Look for [name]/v[version] let path = sanitize_name(name)?.join(format!("v{version}")); - let cosmic_user_path = custom_path.join("cosmic"); - - let user_path = cosmic_user_path.join(path); + let mut user_path = custom_path; + user_path.push("cosmic"); + user_path.push(path); // Create new configuration directory if not found. fs::create_dir_all(&user_path)?; @@ -213,11 +212,9 @@ impl Config { let path = sanitize_name(name)?.join(format!("v{}", version)); // Get libcosmic user state directory - let cosmic_user_path = dirs::state_dir() - .ok_or(Error::NoConfigDirectory)? - .join("cosmic"); - - let user_path = cosmic_user_path.join(path); + let mut user_path = dirs::state_dir().ok_or(Error::NoConfigDirectory)?; + user_path.push("cosmic"); + user_path.push(path); // Create new state directory if not found. fs::create_dir_all(&user_path)?; diff --git a/cosmic-theme/src/output/gtk4_output.rs b/cosmic-theme/src/output/gtk4_output.rs index df6aca6a..6fdf26d5 100644 --- a/cosmic-theme/src/output/gtk4_output.rs +++ b/cosmic-theme/src/output/gtk4_output.rs @@ -148,7 +148,7 @@ impl Theme { #[cold] pub fn write_gtk4(&self) -> Result<(), OutputError> { let css_str = self.as_gtk4(); - let Some(config_dir) = dirs::config_dir() else { + let Some(mut config_dir) = dirs::config_dir() else { return Err(OutputError::MissingConfigDir); }; @@ -158,7 +158,7 @@ impl Theme { "light.css" }; - let config_dir = config_dir.join("gtk-4.0").join("cosmic"); + config_dir.extend(["gtk-4.0", "cosmic"]); if !config_dir.exists() { std::fs::create_dir_all(&config_dir).map_err(OutputError::Io)?; } @@ -181,23 +181,20 @@ impl Theme { return Err(OutputError::MissingConfigDir); }; - let gtk4 = config_dir.join("gtk-4.0"); - let gtk3 = config_dir.join("gtk-3.0"); + let mut gtk4 = config_dir.join("gtk-4.0"); + let mut gtk3 = config_dir.join("gtk-3.0"); fs::create_dir_all(>k4).map_err(OutputError::Io)?; fs::create_dir_all(>k3).map_err(OutputError::Io)?; let cosmic_css_dir = gtk4.join("cosmic"); - let cosmic_css = - cosmic_css_dir - .clone() - .join(if is_dark { "dark.css" } else { "light.css" }); + let cosmic_css = cosmic_css_dir.join(if is_dark { "dark.css" } else { "light.css" }); - let gtk4_dest = gtk4.join("gtk.css"); - let gtk3_dest = gtk3.join("gtk.css"); + gtk4.push("gtk.css"); + gtk3.push("gtk.css"); #[cfg(target_family = "unix")] - for gtk_dest in [>k4_dest, >k3_dest] { + for gtk_dest in [>k4, >k3] { use std::os::unix::fs::symlink; Self::backup_non_cosmic_css(gtk_dest, &cosmic_css_dir).map_err(OutputError::Io)?; diff --git a/cosmic-theme/src/output/vs_code.rs b/cosmic-theme/src/output/vs_code.rs index b07c82e1..43c36bb6 100644 --- a/cosmic-theme/src/output/vs_code.rs +++ b/cosmic-theme/src/output/vs_code.rs @@ -269,8 +269,9 @@ impl Theme { #[cold] pub fn apply_vs_code(self) -> Result<(), OutputError> { let vs_theme = VsTheme::from(self); - let config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?; - let vs_code_dir = config_dir.join("Code").join("User"); + let mut config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?; + config_dir.extend(["Code", "User"]); + let vs_code_dir = config_dir; if !vs_code_dir.exists() { std::fs::create_dir_all(&vs_code_dir).map_err(OutputError::Io)?; } @@ -292,9 +293,9 @@ impl Theme { #[cold] pub fn reset_vs_code() -> Result<(), OutputError> { - let config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?; - let vs_code_dir = config_dir.join("Code").join("User"); - let settings_file = vs_code_dir.join("settings.json"); + let mut config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?; + config_dir.extend(["Code", "User", "settings.json"]); + let settings_file = config_dir; // just remove the json entry for workbench.colorCustomizations let settings = std::fs::read_to_string(&settings_file).unwrap_or_default(); let mut settings: serde_json::Value = serde_json::from_str(&settings).unwrap_or_default(); diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 6dfaeef6..659b7e92 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -76,7 +76,7 @@ impl From for PanelType { match value.as_str() { "Panel" => PanelType::Panel, "Dock" => PanelType::Dock, - other => PanelType::Other(other.to_string()), + _ => PanelType::Other(value), } } } @@ -470,8 +470,8 @@ pub fn run(flags: App::Flags) -> iced::Result { crate::malloc::limit_mmap_threshold(threshold); } - if let Some(icon_theme) = settings.default_icon_theme.clone() { - crate::icon_theme::set_default(icon_theme); + if let Some(icon_theme) = settings.default_icon_theme.as_ref() { + crate::icon_theme::set_default(icon_theme.clone()); } THEME diff --git a/src/applet/token/wayland_handler.rs b/src/applet/token/wayland_handler.rs index ee8f9b4e..3db84fc4 100644 --- a/src/applet/token/wayland_handler.rs +++ b/src/applet/token/wayland_handler.rs @@ -162,8 +162,8 @@ pub(crate) fn wayland_handler( exit: false, tx, seat_state: SeatState::new(&globals, &qh), - queue_handle: qh.clone(), activation_state: ActivationState::bind::(&globals, &qh).ok(), + queue_handle: qh, registry_state, }; diff --git a/src/core.rs b/src/core.rs index 338e0e85..4d50e764 100644 --- a/src/core.rs +++ b/src/core.rs @@ -361,8 +361,12 @@ impl Core { config_id: &'static str, ) -> iced::Subscription> { #[cfg(all(feature = "dbus-config", target_os = "linux"))] - if let Some(settings_daemon) = self.settings_daemon.clone() { - return cosmic_config::dbus::watcher_subscription(settings_daemon, config_id, false); + if let Some(settings_daemon) = self.settings_daemon.as_ref() { + return cosmic_config::dbus::watcher_subscription( + settings_daemon.clone(), + config_id, + false, + ); } cosmic_config::config_subscription( std::any::TypeId::of::(), @@ -378,8 +382,12 @@ impl Core { state_id: &'static str, ) -> iced::Subscription> { #[cfg(all(feature = "dbus-config", target_os = "linux"))] - if let Some(settings_daemon) = self.settings_daemon.clone() { - return cosmic_config::dbus::watcher_subscription(settings_daemon, state_id, true); + if let Some(settings_daemon) = self.settings_daemon.as_ref() { + return cosmic_config::dbus::watcher_subscription( + settings_daemon.clone(), + state_id, + true, + ); } cosmic_config::config_subscription( std::any::TypeId::of::(), diff --git a/src/widget/about.rs b/src/widget/about.rs index 628f53c6..384aee4a 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -113,10 +113,7 @@ pub fn about<'a, Message: Clone + 'static>( let section = |list: &'a Vec<(String, String)>, title: String| { (!list.is_empty()).then_some({ - let items: Vec> = list - .iter() - .map(|(name, url)| section_button(name, url)) - .collect(); + let items = list.iter().map(|(name, url)| section_button(name, url)); widget::settings::section().title(title).extend(items) }) }; diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index a1aace33..8531ab3d 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -179,8 +179,8 @@ where )); } - let content_list = column::with_children(vec![ - row::with_children(vec![ + let content_list = column::with_children([ + row::with_children([ date.into(), crate::widget::Space::with_width(Length::Fill).into(), month_controls.into(), diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 87e7a4d3..8dba2e1a 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -455,21 +455,16 @@ where // TODO get global colors from some cache? // TODO how to handle overflow? should this use a grid widget for the list or a horizontal scroll and a limit for the max? crate::widget::scrollable( - Row::with_children( - self.recent_colors - .iter() - .map(|c| { - let initial_srgb = palette::Srgb::from(*c); - let hsv = palette::Hsv::from_color(initial_srgb); - color_button( - Some(on_update(ColorPickerUpdate::ActiveColor(hsv))), - Some(*c), - Length::FillPortion(12), - ) - .into() - }) - .collect::>(), - ) + Row::with_children(self.recent_colors.iter().map(|c| { + let initial_srgb = palette::Srgb::from(*c); + let hsv = palette::Hsv::from_color(initial_srgb); + color_button( + Some(on_update(ColorPickerUpdate::ActiveColor(hsv))), + Some(*c), + Length::FillPortion(12), + ) + .into() + })) .padding([0.0, 0.0, f32::from(spacing.space_m), 0.0]) .spacing(spacing.space_xxs), ) diff --git a/src/widget/dialog.rs b/src/widget/dialog.rs index ecc6ef05..ba5b55e2 100644 --- a/src/widget/dialog.rs +++ b/src/widget/dialog.rs @@ -158,8 +158,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes } let mut container = widget::container( - widget::column::with_children(vec![content_row.into(), button_row.into()]) - .spacing(space_l), + widget::column::with_children([content_row.into(), button_row.into()]).spacing(space_l), ) .class(style::Container::Dialog) .padding(space_m) diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index 0026283c..021fcc60 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -477,8 +477,8 @@ where if cursor.is_over(layout.bounds()) { if let Some(index) = *hovered_guard { shell.publish((self.on_selected)(index)); - if let Some(close_on_selected) = self.close_on_selected.clone() { - shell.publish(close_on_selected); + if let Some(close_on_selected) = self.close_on_selected.as_ref() { + shell.publish(close_on_selected.clone()); } return event::Status::Captured; } @@ -521,8 +521,8 @@ where if let Some(index) = *hovered_guard { shell.publish((self.on_selected)(index)); - if let Some(close_on_selected) = self.close_on_selected.clone() { - shell.publish(close_on_selected); + if let Some(close_on_selected) = self.close_on_selected.as_ref() { + shell.publish(close_on_selected.clone()); } return event::Status::Captured; } diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index 1b0637bb..9c183292 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -521,8 +521,8 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( style.background, ); - if let Some(handle) = state.icon.clone() { - let svg_handle = iced_core::Svg::new(handle).color(style.text_color); + 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, diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 18f9940d..c88a7570 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -1653,7 +1653,7 @@ fn get_children_layout( let child_sizes: Vec = match item_height { ItemHeight::Uniform(u) => { let count = menu_tree.children.len(); - (0..count).map(|_| Size::new(width, f32::from(u))).collect() + vec![Size::new(width, f32::from(u)); count] } ItemHeight::Static(s) => menu_tree .children diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index e63e523b..15dd5810 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -144,7 +144,7 @@ where Message: std::clone::Clone + 'a, { widget::button::custom( - widget::Row::with_children(children) + widget::Row::from_vec(children) .align_y(Alignment::Center) .height(Length::Fill) .width(Length::Fill), @@ -252,7 +252,7 @@ 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.clone()).into(), + widget::text(l).into(), widget::horizontal_space().into(), widget::text(key).class(key_class).into(), ]; @@ -272,7 +272,7 @@ pub fn menu_items< let key = find_key(&action, key_binds); let mut items = vec![ - widget::text(l.clone()).into(), + widget::text(l).into(), widget::horizontal_space().into(), widget::text(key).class(key_class).into(), ]; diff --git a/src/widget/mod.rs b/src/widget/mod.rs index f212906a..202173ef 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -147,12 +147,14 @@ pub mod column { #[must_use] /// A pre-allocated [`column`]. pub fn with_capacity<'a, Message>(capacity: usize) -> Column<'a, Message> { - Column::with_children(Vec::with_capacity(capacity)) + Column::with_capacity(capacity) } #[must_use] - /// A [`column`] that will be assigned a [`Vec`] of children. - pub fn with_children(children: Vec>) -> Column { + /// 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) } } @@ -298,12 +300,14 @@ pub mod row { #[must_use] /// A pre-allocated [`row`]. pub fn with_capacity<'a, Message>(capacity: usize) -> Row<'a, Message> { - Row::with_children(Vec::with_capacity(capacity)) + Row::with_capacity(capacity) } #[must_use] - /// A [`row`] that will be assigned a [`Vec`] of children. - pub fn with_children(children: Vec>) -> Row { + /// 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) } } diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 6c6f6652..ddc31455 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -141,14 +141,14 @@ where if matches!(event, Event::Mouse(_) | Event::Touch(_)) { return event::Status::Captured; } - } else if let Some(on_close) = self.on_close.clone() { + } else if let Some(on_close) = self.on_close.as_ref() { if matches!( event, Event::Mouse(mouse::Event::ButtonPressed(_)) | Event::Touch(touch::Event::FingerPressed { .. }) ) && !cursor_position.is_over(layout.bounds()) { - shell.publish(on_close); + shell.publish(on_close.clone()); } } } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 0e725132..5dd8e7c7 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -274,7 +274,7 @@ where self.on_dnd_drop = Some(Box::new(move |entity, data, mime, action| { dnd_drop_handler(entity, D::try_from((data, mime)).ok(), action) })); - self.mimes = D::allowed().iter().cloned().collect(); + self.mimes = D::allowed().into_owned(); self } @@ -1867,7 +1867,7 @@ fn draw_icon( }); Widget::::draw( - Element::::from(icon.clone()).as_widget(), + Element::::from(icon).as_widget(), &Tree::empty(), renderer, theme, diff --git a/src/widget/table/widget/compact.rs b/src/widget/table/widget/compact.rs index 7cda2dfb..0ad92166 100644 --- a/src/widget/table/widget/compact.rs +++ b/src/widget/table/widget/compact.rs @@ -170,7 +170,6 @@ where ) .apply(Element::from) }) - .collect::>>() .apply(widget::column::with_children) .spacing(val.item_spacing) .padding(val.element_padding) diff --git a/src/widget/table/widget/standard.rs b/src/widget/table/widget/standard.rs index 3ee1ac4a..c0207f06 100644 --- a/src/widget/table/widget/standard.rs +++ b/src/widget/table/widget/standard.rs @@ -125,7 +125,6 @@ where .apply(|mouse_area| widget::context_menu(mouse_area, cat_context_tree)) .apply(Element::from) }) - .collect::>>() .apply(widget::row::with_children) .apply(Element::from); // Build the items @@ -166,7 +165,6 @@ where .align_y(Alignment::Center) .apply(Element::from) }) - .collect::>>() .apply(widget::row::with_children) .apply(container) .padding(val.item_padding) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 4c9fa0f9..958673ef 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -760,14 +760,14 @@ where if state.dirty { state.dirty = false; let value = if self.is_secure { - self.value.secure() + &self.value.secure() } else { - self.value.clone() + &self.value }; replace_paragraph( state, Layout::new(&res), - &value, + value, font, iced::Pixels(size), line_height, @@ -2022,7 +2022,7 @@ pub fn update<'a, Message: Clone + 'static>( if let DndOfferState::HandlingOffer(mime_types, _action) = state.dnd_offer.clone() { let Some(mime_type) = SUPPORTED_TEXT_MIME_TYPES .iter() - .find(|m| mime_types.contains(&(**m).to_string())) + .find(|&&m| mime_types.iter().any(|t| t == m)) else { state.dnd_offer = DndOfferState::None; return event::Status::Captured; @@ -2057,7 +2057,7 @@ pub fn update<'a, Message: Clone + 'static>( { cold(); let state = state(); - if let DndOfferState::Dropped = state.dnd_offer.clone() { + 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; @@ -2536,7 +2536,7 @@ impl AsMimeTypes for TextInputString { fn as_bytes(&self, mime_type: &str) -> Option> { if SUPPORTED_TEXT_MIME_TYPES.contains(&mime_type) { - Some(Cow::Owned(self.0.clone().as_bytes().to_vec())) + Some(Cow::Owned(self.0.clone().into_bytes())) } else { None } From 1d6a43486eaa5ad15bc9627d0a31f0620181fe9d Mon Sep 17 00:00:00 2001 From: Cheong Lau <234708519+Cheong-Lau@users.noreply.github.com> Date: Sat, 11 Oct 2025 16:24:38 +1000 Subject: [PATCH 137/352] remove redundant `clone`s, use `mul_add` on `f32`s --- src/widget/context_drawer/overlay.rs | 2 +- src/widget/dropdown/menu/mod.rs | 7 +-- src/widget/dropdown/multi/menu.rs | 15 +++--- src/widget/dropdown/multi/widget.rs | 66 ++++++++++++++------------- src/widget/flex_row/layout.rs | 2 +- src/widget/grid/layout.rs | 2 +- src/widget/id_container.rs | 2 +- src/widget/menu/flex.rs | 28 ++++++------ src/widget/menu/menu_inner.rs | 2 +- src/widget/responsive_container.rs | 2 +- src/widget/segmented_button/widget.rs | 4 +- src/widget/toaster/widget.rs | 6 +-- 12 files changed, 69 insertions(+), 69 deletions(-) diff --git a/src/widget/context_drawer/overlay.rs b/src/widget/context_drawer/overlay.rs index d9cc88ab..4f72e113 100644 --- a/src/widget/context_drawer/overlay.rs +++ b/src/widget/context_drawer/overlay.rs @@ -33,7 +33,7 @@ where .layout(self.tree, renderer, &limits); let node_size = node.size(); - node.clone().move_to(Point { + node.move_to(Point { x: if bounds.width > node_size.width - 8.0 { bounds.width - node_size.width - 8.0 } else { diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index 021fcc60..1d42d01f 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -204,7 +204,7 @@ impl<'a, Message: Clone + 'a> Overlay<'a, Message> { .with_data_mut(|tree| tree.diff(&mut container as &mut dyn Widget<_, _, _>)); Self { - state: state.tree.clone(), + state: state.tree, container, width, target_height, @@ -234,10 +234,11 @@ impl<'a, Message: Clone + 'a> Overlay<'a, Message> { .state .with_data_mut(|tree| self.container.layout(tree, renderer, &limits)); - node.clone().move_to(if space_below > space_above { + let node_size = node.size(); + node.move_to(if space_below > space_above { self.position + Vector::new(0.0, self.target_height) } else { - self.position - Vector::new(0.0, node.size().height) + self.position - Vector::new(0.0, node_size.height) }) } diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index 10b0d8d4..0035829f 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -199,15 +199,14 @@ impl iced_core::Overlay for Ove ) .width(self.width); - let mut node = self.container.layout(self.state, renderer, &limits); + let node = self.container.layout(self.state, renderer, &limits); - node = node.clone().move_to(if space_below > space_above { + let node_size = node.size(); + node.move_to(if space_below > space_above { position + Vector::new(0.0, self.target_height) } else { - position - Vector::new(0.0, node.size().height) - }); - - node + position - Vector::new(0.0, node_size.height) + }) } fn on_event( @@ -513,7 +512,7 @@ where OptionElement::Option((option, item)) => { let (color, font) = if self.selected_option.as_ref() == Some(&item) { let item_x = bounds.x + appearance.border_width; - let item_width = bounds.width - appearance.border_width * 2.0; + let item_width = appearance.border_width.mul_add(-2.0, bounds.width); bounds = Rectangle { x: item_x, @@ -551,7 +550,7 @@ where (appearance.selected_text_color, crate::font::semibold()) } else if self.hovered_option.as_ref() == Some(item) { let item_x = bounds.x + appearance.border_width; - let item_width = bounds.width - appearance.border_width * 2.0; + let item_width = appearance.border_width.mul_add(-2.0, bounds.width); bounds = Rectangle { x: item_x, diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index 9c183292..79b1a6b7 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -432,44 +432,46 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static }; let mut desc_count = 0; - selections - .elements() - .map(|element| match element { - super::menu::OptionElement::Description(desc) => { - let paragraph = if state.descriptions.len() > desc_count { - &mut state.descriptions[desc_count] - } else { - state.descriptions.push(crate::Plain::default()); - state.descriptions.last_mut().unwrap() - }; - desc_count += 1; - measure(desc.as_ref(), paragraph, description_line_height) - } + padding.horizontal().mul_add( + 2.0, + selections + .elements() + .map(|element| match element { + super::menu::OptionElement::Description(desc) => { + let paragraph = if state.descriptions.len() > desc_count { + &mut state.descriptions[desc_count] + } else { + state.descriptions.push(crate::Plain::default()); + state.descriptions.last_mut().unwrap() + }; + desc_count += 1; + measure(desc.as_ref(), paragraph, description_line_height) + } - super::menu::OptionElement::Option((option, item)) => { - let selection_index = state.selections.iter().position(|(i, _)| i == item); + super::menu::OptionElement::Option((option, item)) => { + let selection_index = + state.selections.iter().position(|(i, _)| i == item); - let selection_index = match selection_index { - Some(index) => index, - None => { - state - .selections - .push((item.clone(), crate::Plain::default())); - state.selections.len() - 1 - } - }; + let selection_index = match selection_index { + Some(index) => index, + None => { + state + .selections + .push((item.clone(), crate::Plain::default())); + state.selections.len() - 1 + } + }; - let paragraph = &mut state.selections[selection_index].1; + let paragraph = &mut state.selections[selection_index].1; - measure(option.as_ref(), paragraph, text_line_height) - } + measure(option.as_ref(), paragraph, text_line_height) + } - super::menu::OptionElement::Separator => 1.0, - }) - .fold(0.0, |next, current| current.max(next)) - + gap + super::menu::OptionElement::Separator => 1.0, + }) + .fold(0.0, |next, current| current.max(next)), + ) + gap + 16.0 - + (padding.horizontal() * 2.0) }) .padding(padding) .text_size(text_size); diff --git a/src/widget/flex_row/layout.rs b/src/widget/flex_row/layout.rs index 720e4561..744b607d 100644 --- a/src/widget/flex_row/layout.rs +++ b/src/widget/flex_row/layout.rs @@ -156,7 +156,7 @@ pub fn resolve( _ => (), } - *node = node.clone().move_to(Point { + node.move_to_mut(Point { x: leaf_layout.location.x, y: leaf_layout.location.y, }); diff --git a/src/widget/grid/layout.rs b/src/widget/grid/layout.rs index d1da68af..a7e42759 100644 --- a/src/widget/grid/layout.rs +++ b/src/widget/grid/layout.rs @@ -187,7 +187,7 @@ pub fn resolve( _ => (), } - *node = node.clone().move_to(Point { + node.move_to_mut(Point { x: leaf_layout.location.x, y: leaf_layout.location.y, }) diff --git a/src/widget/id_container.rs b/src/widget/id_container.rs index 7f6bc97e..3d468b20 100644 --- a/src/widget/id_container.rs +++ b/src/widget/id_container.rs @@ -112,7 +112,7 @@ where ) -> event::Status { self.content.as_widget_mut().on_event( &mut tree.children[0], - event.clone(), + event, layout .children() .next() diff --git a/src/widget/menu/flex.rs b/src/widget/menu/flex.rs index 5eaf3d94..8eb08d4e 100644 --- a/src/widget/menu/flex.rs +++ b/src/widget/menu/flex.rs @@ -200,16 +200,16 @@ where let (x, y) = axis.pack(main, pad.1); - let node_ = node.clone().move_to(Point::new(x, y)); + node.move_to_mut(Point::new(x, y)); - let node_ = match axis { - Axis::Horizontal => node_.align(Alignment::Start, align_items, Size::new(0.0, cross)), - Axis::Vertical => node_.align(align_items, Alignment::Start, Size::new(cross, 0.0)), + match axis { + Axis::Horizontal => { + node.align_mut(Alignment::Start, align_items, Size::new(0.0, cross)) + } + Axis::Vertical => node.align_mut(align_items, Alignment::Start, Size::new(cross, 0.0)), }; - let size = node_.bounds().size(); - - *node = node_; + let size = node.bounds().size(); main += axis.main(size); } @@ -367,16 +367,16 @@ pub fn resolve_wrapper<'a, Message>( let (x, y) = axis.pack(main, pad.1); - let node_ = node.clone().move_to(Point::new(x, y)); + node.move_to_mut(Point::new(x, y)); - let node_ = match axis { - Axis::Horizontal => node_.align(Alignment::Start, align_items, Size::new(0.0, cross)), - Axis::Vertical => node_.align(align_items, Alignment::Start, Size::new(cross, 0.0)), + match axis { + Axis::Horizontal => { + node.align_mut(Alignment::Start, align_items, Size::new(0.0, cross)) + } + Axis::Vertical => node.align_mut(align_items, Alignment::Start, Size::new(cross, 0.0)), }; - let size = node_.bounds().size(); - - *node = node_; + let size = node.bounds().size(); main += axis.main(size); } diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index c88a7570..c455cd13 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -370,7 +370,7 @@ impl MenuState { let limits = Limits::new(Size::ZERO, self.menu_bounds.child_sizes[index]); let parent_offset = children_bounds.position() - Point::ORIGIN; let node = menu_tree.item.layout(tree, renderer, &limits); - node.clone().move_to(Point::new( + node.move_to(Point::new( parent_offset.x, parent_offset.y + position + self.scroll_offset, )) diff --git a/src/widget/responsive_container.rs b/src/widget/responsive_container.rs index 92bedef1..fbc2df9e 100644 --- a/src/widget/responsive_container.rs +++ b/src/widget/responsive_container.rs @@ -168,7 +168,7 @@ where self.content.as_widget_mut().on_event( &mut tree.children[0], - event.clone(), + event, layout .children() .next() diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 5dd8e7c7..bb05aa9d 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -1419,8 +1419,8 @@ where renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: bounds.x - - (level as f32 * self.indent_spacing as f32) + x: (level as f32) + .mul_add(-(self.indent_spacing as f32), bounds.x) + indent_padding, width: 1.0, ..bounds diff --git a/src/widget/toaster/widget.rs b/src/widget/toaster/widget.rs index f6324e15..52604592 100644 --- a/src/widget/toaster/widget.rs +++ b/src/widget/toaster/widget.rs @@ -199,7 +199,7 @@ where fn layout(&mut self, renderer: &Renderer, bounds: Size) -> Node { let limits = Limits::new(Size::ZERO, bounds); - let mut node = self + let node = self .element .as_widget() .layout(self.state, renderer, &limits); @@ -211,9 +211,7 @@ where bounds.height - (node.size().height + offset), ); - node.move_to_mut(position); - - node + node.move_to(position) } fn draw( From e49a30104bb169e08f44913b39a13496374390be Mon Sep 17 00:00:00 2001 From: UchiWerfer Date: Tue, 7 Oct 2025 23:29:29 +0200 Subject: [PATCH 138/352] added localization for month and weekday to calendar-widget --- i18n/en/libcosmic.ftl | 21 +++++++++++++++++++++ src/widget/calendar.rs | 42 +++++++++++++++++++++++++++++++++++++++--- 2 files changed, 60 insertions(+), 3 deletions(-) diff --git a/i18n/en/libcosmic.ftl b/i18n/en/libcosmic.ftl index 45266a9b..119ac38e 100644 --- a/i18n/en/libcosmic.ftl +++ b/i18n/en/libcosmic.ftl @@ -9,3 +9,24 @@ designers = Designers artists = Artists translators = Translators documenters = Documenters + +# Calendar +january = January { $year } +february = February { $year } +march = March { $year } +april = April { $year } +may = May { $year } +june = June { $year } +july = July { $year } +august = August { $year } +september = September { $year } +october = October { $year } +november = November { $year } +december = December { $year } +monday = Mon +tuesday = Tue +wednesday = Wed +thursday = Thu +friday = Fri +saturday = Sat +sunday = Sun diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index 8531ab3d..134e84f2 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -8,7 +8,8 @@ use std::cmp; use crate::iced_core::{Alignment, Length, Padding}; use crate::widget::{Grid, button, column, grid, icon, row, text}; use apply::Apply; -use chrono::{Datelike, Days, Local, Months, NaiveDate, Weekday}; +use chrono::{Datelike, Days, Local, Month, Months, NaiveDate, Weekday}; +use crate::fl; /// A widget that displays an interactive calendar. pub fn calendar( @@ -129,7 +130,42 @@ where icon.padding([0, 12]).on_press($on_press) }}; } - let date = text(this.model.visible.format("%B %Y").to_string()).size(18); + macro_rules! translate_month { + ($month:expr, $year:expr) => {{ + match $month { + chrono::Month::January => fl!("january", year=$year), + chrono::Month::February => fl!("february", year=$year), + chrono::Month::March => fl!("march", year=$year), + chrono::Month::April => fl!("april", year=$year), + chrono::Month::May => fl!("may", year=$year), + chrono::Month::June => fl!("june", year=$year), + chrono::Month::July => fl!("july", year=$year), + chrono::Month::August => fl!("august", year=$year), + chrono::Month::September => fl!("september", year=$year), + chrono::Month::October => fl!("october", year=$year), + chrono::Month::November => fl!("november", year=$year), + chrono::Month::December => fl!("december", year=$year) + } + }} + } + macro_rules! translate_weekday { + ($weekday:expr) => {{ + match $weekday { + Weekday::Mon => fl!("monday"), + Weekday::Tue => fl!("tuesday"), + Weekday::Wed => fl!("wednesday"), + Weekday::Thu => fl!("thursday"), + Weekday::Fri => fl!("friday"), + Weekday::Sat => fl!("saturday"), + Weekday::Sun => fl!("sunday") + } + }} + } + + let date = text(translate_month!( + Month::try_from(this.model.visible.month() as u8) + .expect("Previously valid month is suddenly invalid"), + this.model.visible.year())).size(18); let month_controls = row::with_capacity(2) .push(icon!("go-previous-symbolic", (this.on_prev)())) @@ -142,7 +178,7 @@ where let mut first_day_of_week = this.first_day_of_week; for _ in 0..7 { calendar_grid = calendar_grid.push( - text(first_day_of_week.to_string()) + text(translate_weekday!(first_day_of_week)) .size(12) .width(Length::Fixed(36.0)) .align_x(Alignment::Center), From 380042396bf9b19df5a1ae25613b71609e725868 Mon Sep 17 00:00:00 2001 From: UchiWerfer Date: Tue, 7 Oct 2025 23:30:29 +0200 Subject: [PATCH 139/352] added German translations to the localization of the calendar-widget --- i18n/de/libcosmic.ftl | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/i18n/de/libcosmic.ftl b/i18n/de/libcosmic.ftl index 3806cc59..7d8dfe93 100644 --- a/i18n/de/libcosmic.ftl +++ b/i18n/de/libcosmic.ftl @@ -9,3 +9,24 @@ designers = Designer*innen artists = Künstler*innen translators = Übersetzer*innen documenters = Dokumentierer*innen + +# Calendar +january = Januar { $year } +february = Februar { $year } +march = März { $year } +april = April { $year } +may = Mai { $year } +june = Juni { $year } +july = Juli { $year } +august = August { $year } +september = September { $year } +october = Oktober { $year } +november = November { $year } +december = Dezember { $year } +monday = Mo +tuesday = Di +wednesday = Mi +thursday = Do +friday = Fr +saturday = Sa +sunday = So From 6204784f202c19b675c5a0c8a56865e3f1d9bd69 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 23 Oct 2025 17:12:06 -0400 Subject: [PATCH 140/352] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 7bb364e0..cfe5f4b1 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 7bb364e01d6cd6c07703416828006ab497a082e6 +Subproject commit cfe5f4b1a4413b9c94c10832093b2a9e0de5eece From 0c6c85429e313c538d7fe3cd25a49ef818293a6e Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Mon, 27 Oct 2025 11:24:02 -0400 Subject: [PATCH 141/352] chore: update iced (#1029) --- iced | 2 +- src/widget/calendar.rs | 36 +++++++++++++++++++----------------- 2 files changed, 20 insertions(+), 18 deletions(-) diff --git a/iced b/iced index cfe5f4b1..783d764c 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit cfe5f4b1a4413b9c94c10832093b2a9e0de5eece +Subproject commit 783d764cabd6eee020eeae3b50a0d4727a721056 diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index 134e84f2..7f9ac0ad 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -5,11 +5,11 @@ use std::cmp; +use crate::fl; use crate::iced_core::{Alignment, Length, Padding}; use crate::widget::{Grid, button, column, grid, icon, row, text}; use apply::Apply; use chrono::{Datelike, Days, Local, Month, Months, NaiveDate, Weekday}; -use crate::fl; /// A widget that displays an interactive calendar. pub fn calendar( @@ -133,20 +133,20 @@ where macro_rules! translate_month { ($month:expr, $year:expr) => {{ match $month { - chrono::Month::January => fl!("january", year=$year), - chrono::Month::February => fl!("february", year=$year), - chrono::Month::March => fl!("march", year=$year), - chrono::Month::April => fl!("april", year=$year), - chrono::Month::May => fl!("may", year=$year), - chrono::Month::June => fl!("june", year=$year), - chrono::Month::July => fl!("july", year=$year), - chrono::Month::August => fl!("august", year=$year), - chrono::Month::September => fl!("september", year=$year), - chrono::Month::October => fl!("october", year=$year), - chrono::Month::November => fl!("november", year=$year), - chrono::Month::December => fl!("december", year=$year) + chrono::Month::January => fl!("january", year = $year), + chrono::Month::February => fl!("february", year = $year), + chrono::Month::March => fl!("march", year = $year), + chrono::Month::April => fl!("april", year = $year), + chrono::Month::May => fl!("may", year = $year), + chrono::Month::June => fl!("june", year = $year), + chrono::Month::July => fl!("july", year = $year), + chrono::Month::August => fl!("august", year = $year), + chrono::Month::September => fl!("september", year = $year), + chrono::Month::October => fl!("october", year = $year), + chrono::Month::November => fl!("november", year = $year), + chrono::Month::December => fl!("december", year = $year), } - }} + }}; } macro_rules! translate_weekday { ($weekday:expr) => {{ @@ -157,15 +157,17 @@ where Weekday::Thu => fl!("thursday"), Weekday::Fri => fl!("friday"), Weekday::Sat => fl!("saturday"), - Weekday::Sun => fl!("sunday") + Weekday::Sun => fl!("sunday"), } - }} + }}; } let date = text(translate_month!( Month::try_from(this.model.visible.month() as u8) .expect("Previously valid month is suddenly invalid"), - this.model.visible.year())).size(18); + this.model.visible.year() + )) + .size(18); let month_controls = row::with_capacity(2) .push(icon!("go-previous-symbolic", (this.on_prev)())) From a1b64dde3e445f67d5b3c7845c4b5b2b80b4fd4e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 27 Oct 2025 12:41:18 -0400 Subject: [PATCH 142/352] fix(input): handle ctrl shortcuts with caps lock --- src/widget/text_input/input.rs | 17 +++++++++++------ 1 file changed, 11 insertions(+), 6 deletions(-) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 958673ef..7dd92e12 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -1629,22 +1629,27 @@ pub fn update<'a, Message: Clone + 'static>( key, text, physical_key, + modifiers, .. }) => { let state = state(); + 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; }; - let modifiers = state.keyboard_modifiers; 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.command() { + 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") | keyboard::Key::Character("C") => { if !is_secure { if let Some((start, end)) = state.cursor.selection(value) { clipboard.write( @@ -1656,7 +1661,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") | keyboard::Key::Character("X") => { if !is_secure { if let Some((start, end)) = state.cursor.selection(value) { clipboard.write( @@ -1675,7 +1680,7 @@ pub fn update<'a, Message: Clone + 'static>( } } } - keyboard::Key::Character("v") => { + keyboard::Key::Character("v") | keyboard::Key::Character("V") => { let content = if let Some(content) = state.is_pasting.take() { content } else { @@ -1719,7 +1724,7 @@ pub fn update<'a, Message: Clone + 'static>( return event::Status::Captured; } - keyboard::Key::Character("a") => { + keyboard::Key::Character("a") | keyboard::Key::Character("A") => { state.cursor.select_all(value); return event::Status::Captured; } From 8e1d06e7da79de027f37793bd7e15f23a5c4838b Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 29 Oct 2025 12:04:48 +0100 Subject: [PATCH 143/352] i18n: translation updates from weblate Co-authored-by: Hosted Weblate Co-authored-by: Mattias Eriksson Co-authored-by: Sachin Chaudhary Co-authored-by: VandaL Co-authored-by: lorduskordus Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/cs/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/pl/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/sv/ Translation: Pop OS/libcosmic --- i18n/cs/libcosmic.ftl | 20 +++++++++++++++++++- i18n/gu/libcosmic.ftl | 0 i18n/pl/libcosmic.ftl | 20 +++++++++++++++++++- i18n/sv/libcosmic.ftl | 19 +++++++++++++++++++ 4 files changed, 57 insertions(+), 2 deletions(-) create mode 100644 i18n/gu/libcosmic.ftl diff --git a/i18n/cs/libcosmic.ftl b/i18n/cs/libcosmic.ftl index 561ca9ac..8f2ef348 100644 --- a/i18n/cs/libcosmic.ftl +++ b/i18n/cs/libcosmic.ftl @@ -1,6 +1,5 @@ # Context Drawer close = Zavřít - # About license = Licence links = Odkazy @@ -9,3 +8,22 @@ designers = Designéři artists = Grafici translators = Překladatelé documenters = Tvůrci dokumentace +sunday = Ne +january = Leden { $year } +february = Únor { $year } +march = Březen { $year } +april = Duben { $year } +may = Květen { $year } +june = Červen { $year } +july = Červenec { $year } +august = Srpen { $year } +september = Září { $year } +october = Říjen { $year } +november = Listopad { $year } +december = Prosinec { $year } +monday = Po +tuesday = Út +wednesday = St +thursday = Čt +friday = Pá +saturday = So diff --git a/i18n/gu/libcosmic.ftl b/i18n/gu/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/pl/libcosmic.ftl b/i18n/pl/libcosmic.ftl index f4a65aa6..4bbfd67f 100644 --- a/i18n/pl/libcosmic.ftl +++ b/i18n/pl/libcosmic.ftl @@ -1,6 +1,5 @@ # Context Drawer close = Zamknij - # About license = Licencja links = Linki @@ -9,3 +8,22 @@ designers = Projektanci artists = Artyści translators = Tłumacze documenters = Dokumentaliści +january = Styczeń { $year } +february = Luty { $year } +march = Marzec { $year } +april = Kwiecień { $year } +may = Maj { $year } +june = Czerwiec { $year } +july = Lipiec { $year } +august = Sierpień { $year } +september = Wrzesień { $year } +october = Październik { $year } +november = Listopad { $year } +december = Grudzień { $year } +monday = Pon +tuesday = Wto +wednesday = Śro +thursday = Czw +friday = Pią +saturday = Sob +sunday = Nie diff --git a/i18n/sv/libcosmic.ftl b/i18n/sv/libcosmic.ftl index 75cb7fb4..f0c647a1 100644 --- a/i18n/sv/libcosmic.ftl +++ b/i18n/sv/libcosmic.ftl @@ -6,3 +6,22 @@ artists = Konstnärer translators = Översättare documenters = Skribenter close = Stäng +january = Januari { $year } +february = Februari { $year } +march = Mars { $year } +april = April { $year } +may = Maj { $year } +june = Juni { $year } +july = Juli { $year } +august = Augusti { $year } +september = September { $year } +october = Oktober { $year } +november = November { $year } +december = December { $year } +monday = Mån +tuesday = Tis +wednesday = Ons +thursday = Tor +friday = Fre +saturday = Lör +sunday = Sön From b110b9ca3f7da5871224237ff5479a89cfc5e0cb Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 30 Oct 2025 15:21:26 +0100 Subject: [PATCH 144/352] i18n: translation updates from weblate (#1033) Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/hi/ Translation: Pop OS/libcosmic Co-authored-by: Kartik Nayak --- i18n/hi/libcosmic.ftl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/i18n/hi/libcosmic.ftl b/i18n/hi/libcosmic.ftl index e69de29b..ef2b0efa 100644 --- a/i18n/hi/libcosmic.ftl +++ b/i18n/hi/libcosmic.ftl @@ -0,0 +1,5 @@ +close = बंद करें +license = लाइसेंस +links = लिंक +developers = डेवलपर्स +designers = डिज़ाइनर From 2299b46862f61a8fdbdd6eeacac8005ad1a86fd3 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Thu, 30 Oct 2025 17:35:27 +0100 Subject: [PATCH 145/352] i18n: translation updates from weblate (#1034) Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/hi/ Translation: Pop OS/libcosmic Co-authored-by: Kartik Nayak --- i18n/hi/libcosmic.ftl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/i18n/hi/libcosmic.ftl b/i18n/hi/libcosmic.ftl index ef2b0efa..8603e773 100644 --- a/i18n/hi/libcosmic.ftl +++ b/i18n/hi/libcosmic.ftl @@ -3,3 +3,10 @@ license = लाइसेंस links = लिंक developers = डेवलपर्स designers = डिज़ाइनर +february = फ़रवरी { $year } +documenters = दस्तावेज़ बनाने वाले +april = अप्रैल { $year } +translators = अनुवादक +artists = कलाकार +march = मार्च { $year } +january = जनवरी { $year } From b6c6d1cb7b364f8859a8140da916e0d66e605fbb 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: Mon, 3 Nov 2025 19:16:20 +0100 Subject: [PATCH 146/352] improv(context_drawer): move title out of header row This moves the title below the header row containing actions and the close button, allowing more room for the title and actions. Also makes actions an `Element` instead of a `Vec`, providing more flexibility for developers. --- src/app/context_drawer.rs | 31 ++++----- src/app/mod.rs | 4 +- src/widget/context_drawer/mod.rs | 15 ++--- src/widget/context_drawer/widget.rs | 101 +++++++++++----------------- 4 files changed, 60 insertions(+), 91 deletions(-) diff --git a/src/app/context_drawer.rs b/src/app/context_drawer.rs index b33d2ba6..ac9d5673 100644 --- a/src/app/context_drawer.rs +++ b/src/app/context_drawer.rs @@ -7,7 +7,7 @@ use crate::Element; pub struct ContextDrawer<'a, Message: Clone + 'static> { pub title: Option>, - pub header_actions: Vec>, + pub actions: Option>, pub header: Option>, pub content: Element<'a, Message>, pub footer: Option>, @@ -29,29 +29,28 @@ pub fn context_drawer<'a, Message: Clone + 'static>( ) -> ContextDrawer<'a, Message> { ContextDrawer { title: None, + actions: None, + header: None, content: content.into(), - header_actions: vec![], footer: None, on_close, - header: None, } } impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { - /// Set a context drawer header title + /// Set a context drawer title pub fn title(mut self, title: impl Into>) -> Self { self.title = Some(title.into()); self } - /// App-specific actions at the start of the context drawer header - pub fn header_actions( - mut self, - header_actions: impl IntoIterator>, - ) -> Self { - self.header_actions = header_actions.into_iter().collect(); + + /// App-specific actions at the top-left corner of the context drawer + pub fn actions(mut self, actions: impl Into>) -> Self { + self.actions = Some(actions.into()); self } - /// Non-scrolling elements placed below the context drawer title row + + /// Elements placed above the context drawer scrollable pub fn header(mut self, header: impl Into>) -> Self { self.header = Some(header.into()); self @@ -64,20 +63,16 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { } pub fn map( - mut self, + self, on_message: fn(Message) -> Out, ) -> ContextDrawer<'a, Out> { ContextDrawer { title: self.title, - content: self.content.map(on_message), + actions: self.actions.map(|el| el.map(on_message)), header: self.header.map(|el| el.map(on_message)), + content: self.content.map(on_message), footer: self.footer.map(|el| el.map(on_message)), on_close: on_message(self.on_close), - header_actions: self - .header_actions - .into_iter() - .map(|el| el.map(on_message)) - .collect(), } } } diff --git a/src/app/mod.rs b/src/app/mod.rs index eaf0bae6..090698df 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -603,7 +603,7 @@ impl ApplicationExt for App { widgets.push( crate::widget::context_drawer( context.title, - context.header_actions, + context.actions, context.header, context.footer, context.on_close, @@ -640,7 +640,7 @@ impl ApplicationExt for App { widgets.push( crate::widget::ContextDrawer::new_inner( context.title, - context.header_actions, + context.actions, context.header, context.footer, context.content, diff --git a/src/widget/context_drawer/mod.rs b/src/widget/context_drawer/mod.rs index f1621220..107c1ff5 100644 --- a/src/widget/context_drawer/mod.rs +++ b/src/widget/context_drawer/mod.rs @@ -15,9 +15,9 @@ use crate::Element; /// An overlayed widget that attaches a toggleable context drawer to the view. pub fn context_drawer<'a, Message: Clone + 'static, Content, Drawer>( title: Option>, - header_actions: Vec>, - header_opt: Option>, - footer_opt: Option>, + actions: Option>, + header: Option>, + footer: Option>, on_close: Message, content: Content, drawer: Drawer, @@ -28,13 +28,6 @@ where Drawer: Into>, { ContextDrawer::new( - title, - header_actions, - header_opt, - footer_opt, - content, - drawer, - on_close, - max_width, + title, actions, header, footer, content, drawer, on_close, max_width, ) } diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index c65fe082..cb4b7f94 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 use super::overlay::Overlay; -use crate::widget::{LayerContainer, button, column, container, icon, row, scrollable, text}; +use crate::widget::{self, LayerContainer, button, column, container, icon, row, scrollable, text}; use crate::{Apply, Element, Renderer, Theme, fl}; use std::borrow::Cow; @@ -25,9 +25,9 @@ pub struct ContextDrawer<'a, Message> { impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { pub fn new_inner( title: Option>, - header_actions: Vec>, - header_opt: Option>, - footer_opt: Option>, + actions: Option>, + header: Option>, + footer: Option>, drawer: Drawer, on_close: Message, max_width: f32, @@ -38,7 +38,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { #[inline(never)] fn inner<'a, Message: Clone + 'static>( title: Option>, - header_actions: Vec>, + actions_opt: Option>, header_opt: Option>, footer_opt: Option>, drawer: Element<'a, Message>, @@ -53,68 +53,57 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { .. } = crate::theme::spacing(); - let (horizontal_padding, title_portion, side_portion) = if max_width < 392.0 { - (space_s, 1, 1) - } else { - (space_l, 2, 1) - }; + let horizontal_padding = if max_width < 392.0 { space_s } else { space_l }; let title = title.map(|title| { - text::heading(title) + text::title4(title) .apply(container) - .center_x(Length::FillPortion(title_portion)) + .padding([if actions_opt.is_some() { space_m } else { 0 }, 0, 0, 0]) + .width(Length::Fill) }); - - let (actions_width, close_width) = if title.is_some() { - ( - Length::FillPortion(side_portion), - Length::FillPortion(side_portion), - ) + let actions = if let Some(actions) = actions_opt { + actions + .apply(container) + .width(Length::Fill) + .apply(Element::from) } else { - (Length::Fill, Length::Shrink) + widget::horizontal_space().apply(Element::from) }; - let header_row = row::with_capacity(3) - .width(Length::Fixed(480.0)) + let header_row = row::with_capacity(2) .align_y(Alignment::Center) - .push( - row::with_children(header_actions) - .spacing(space_xxs) - .width(actions_width), - ) - .push_maybe(title) + .push(actions) .push( button::text(fl!("close")) .trailing_icon(icon::from_name("go-next-symbolic")) - .on_press(on_close) - .apply(container) - .width(close_width) - .align_x(Alignment::End), + .on_press(on_close), ); - let header = column::with_capacity(2) - .width(Length::Fixed(480.0)) + let header_element = + header_opt.map(|el| el.apply(container).padding([space_m, 0, 0, 0])); + + let header = column::with_capacity(3) .align_x(Alignment::Center) - .spacing(space_m) .padding([space_m, horizontal_padding]) .push(header_row) - .push_maybe(header_opt); + .push_maybe(title) + .push_maybe(header_element); let footer = footer_opt.map(|element| { container(element) - .width(Length::Fixed(480.0)) .align_y(Alignment::Center) .padding([space_xxs, horizontal_padding]) }); let pane = column::with_capacity(3) .push(header) .push( - scrollable(container(drawer).padding([ - 0, - horizontal_padding, - if footer.is_some() { 0 } else { space_l }, - horizontal_padding, - ])) - .height(Length::Fill) - .width(Length::Shrink), + container(drawer) + .padding([ + 0, + horizontal_padding, + if footer.is_some() { 0 } else { space_l }, + horizontal_padding, + ]) + .apply(scrollable) + .height(Length::Fill), ) .push_maybe(footer); @@ -136,9 +125,9 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { inner( title, - header_actions, - header_opt, - footer_opt, + actions, + header, + footer, drawer.into(), on_close, max_width, @@ -148,9 +137,9 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { /// Creates an empty [`ContextDrawer`]. pub fn new( title: Option>, - header_actions: Vec>, - header_opt: Option>, - footer_opt: Option>, + actions: Option>, + header: Option>, + footer: Option>, content: Content, drawer: Drawer, on_close: Message, @@ -160,15 +149,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { Content: Into>, Drawer: Into>, { - let drawer = Self::new_inner( - title, - header_actions, - header_opt, - footer_opt, - drawer, - on_close, - max_width, - ); + let drawer = Self::new_inner(title, actions, header, footer, drawer, on_close, max_width); ContextDrawer { id: None, @@ -188,7 +169,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { /// Map the message type of the context drawer to another #[inline] pub fn map( - mut self, + self, on_message: fn(Message) -> Out, ) -> ContextDrawer<'a, Out> { ContextDrawer { From d2f7fdea6d24e70b54e017e89973b8a5a44b4e54 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 3 Nov 2025 15:51:20 +0100 Subject: [PATCH 147/352] i18n: translation updates from weblate Co-authored-by: Anonymous Co-authored-by: Guilherme Aiolfi Co-authored-by: Hosted Weblate Co-authored-by: Kartik Nayak Co-authored-by: Torsten Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/de/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/hi/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/pt_BR/ Translation: Pop OS/libcosmic --- i18n/de/libcosmic.ftl | 2 -- i18n/pt-BR/libcosmic.ftl | 20 +++++++++++++++++++- 2 files changed, 19 insertions(+), 3 deletions(-) diff --git a/i18n/de/libcosmic.ftl b/i18n/de/libcosmic.ftl index 7d8dfe93..2ef7b765 100644 --- a/i18n/de/libcosmic.ftl +++ b/i18n/de/libcosmic.ftl @@ -1,6 +1,5 @@ # Context Drawer close = Schließen - # About license = Lizenz links = Links @@ -9,7 +8,6 @@ designers = Designer*innen artists = Künstler*innen translators = Übersetzer*innen documenters = Dokumentierer*innen - # Calendar january = Januar { $year } february = Februar { $year } diff --git a/i18n/pt-BR/libcosmic.ftl b/i18n/pt-BR/libcosmic.ftl index febf5b2e..f02828bf 100644 --- a/i18n/pt-BR/libcosmic.ftl +++ b/i18n/pt-BR/libcosmic.ftl @@ -1,6 +1,5 @@ # Context Drawer close = Fechar - # About license = Licença links = Links @@ -9,3 +8,22 @@ designers = Designers artists = Artistas translators = Tradutores documenters = Documentadores +january = Janeiro { $year } +february = Fevereiro { $year } +march = Março { $year } +april = Abril { $year } +may = Maio { $year } +june = Junho { $year } +july = Julho { $year } +august = Agosto { $year } +september = Setembro { $year } +october = Outubro { $year } +november = Novembro { $year } +december = Dezembro { $year } +monday = Seg +tuesday = Ter +wednesday = Qua +thursday = Qui +friday = Sex +saturday = Sáb +sunday = Dom From 37ae722320a9750ff67808928d794538393c8556 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, 6 Nov 2025 01:12:52 +0100 Subject: [PATCH 148/352] fix(context_drawer): match to designs --- src/widget/context_drawer/widget.rs | 40 ++++++++++++----------------- 1 file changed, 17 insertions(+), 23 deletions(-) diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index cb4b7f94..5366832f 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -55,38 +55,32 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { let horizontal_padding = if max_width < 392.0 { space_s } else { space_l }; - let title = title.map(|title| { - text::title4(title) - .apply(container) - .padding([if actions_opt.is_some() { space_m } else { 0 }, 0, 0, 0]) - .width(Length::Fill) - }); - let actions = if let Some(actions) = actions_opt { - actions + let (actions_slot, column_title) = if let Some(actions) = actions_opt { + let actions = actions .apply(container) .width(Length::Fill) - .apply(Element::from) + .apply(Element::from); + let title = title.map(|title| text::title4(title).width(Length::Fill)); + (actions, title) } else { - widget::horizontal_space().apply(Element::from) + let title = title + .map(|title| text::title4(title).width(Length::Fill).apply(Element::from)) + .unwrap_or_else(|| widget::horizontal_space().apply(Element::from)); + (title, None) }; - let header_row = row::with_capacity(2) - .align_y(Alignment::Center) - .push(actions) - .push( - button::text(fl!("close")) - .trailing_icon(icon::from_name("go-next-symbolic")) - .on_press(on_close), - ); - let header_element = - header_opt.map(|el| el.apply(container).padding([space_m, 0, 0, 0])); - + let header_row = row::with_capacity(2).push(actions_slot).push( + button::text(fl!("close")) + .trailing_icon(icon::from_name("go-next-symbolic")) + .on_press(on_close), + ); let header = column::with_capacity(3) .align_x(Alignment::Center) .padding([space_m, horizontal_padding]) + .spacing(space_m) .push(header_row) - .push_maybe(title) - .push_maybe(header_element); + .push_maybe(column_title) + .push_maybe(header_opt); let footer = footer_opt.map(|element| { container(element) .align_y(Alignment::Center) From 6439507aa2d8d7e6a89c0fc016895dc0ab9252d4 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 6 Nov 2025 07:57:03 +0100 Subject: [PATCH 149/352] fix(icon): default to prefer_svg if symbolic --- src/widget/icon/named.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/widget/icon/named.rs b/src/widget/icon/named.rs index da5c4677..e1c53500 100644 --- a/src/widget/icon/named.rs +++ b/src/widget/icon/named.rs @@ -41,13 +41,14 @@ pub struct Named { impl Named { pub fn new(name: impl Into>) -> Self { let name = name.into(); + let symbolic = name.ends_with("-symbolic"); Self { - symbolic: name.ends_with("-symbolic"), + symbolic, name, fallback: Some(IconFallback::Default), size: None, scale: None, - prefer_svg: false, + prefer_svg: symbolic, } } From bb6f6e9ac8685051eb443cadcaf49e5d4a1f6410 Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Mon, 10 Nov 2025 10:28:39 -0800 Subject: [PATCH 150/352] improv(cosmic-config): Remove unneeded trait bounds for subscriptions It looks like these functions where previously implemented in a different way that required these traits, but now it uses `Subscription::run_with_id`, the `id` only needs to be `Hash + 'static`. --- cosmic-config/src/subscription.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs index 32f48849..45e021fe 100644 --- a/cosmic-config/src/subscription.rs +++ b/cosmic-config/src/subscription.rs @@ -18,7 +18,7 @@ pub enum ConfigUpdate { #[cold] pub fn config_subscription< - I: 'static + Copy + Send + Sync + Hash, + I: 'static + Hash, T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, >( id: I, @@ -30,7 +30,7 @@ pub fn config_subscription< #[cold] pub fn config_state_subscription< - I: 'static + Copy + Send + Sync + Hash, + I: 'static + Hash, T: 'static + Send + Sync + PartialEq + Clone + CosmicConfigEntry, >( id: I, From bc744bd4e3287776d95e250db135a854ac49f056 Mon Sep 17 00:00:00 2001 From: Cheong Lau <234708519+Cheong-Lau@users.noreply.github.com> Date: Tue, 11 Nov 2025 16:18:38 +0000 Subject: [PATCH 151/352] fix(segmented_button): use less restrictive `FnOnce` for builder method over `Fn` --- src/widget/segmented_button/model/builder.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/segmented_button/model/builder.rs b/src/widget/segmented_button/model/builder.rs index d8070aa4..7e17f706 100644 --- a/src/widget/segmented_button/model/builder.rs +++ b/src/widget/segmented_button/model/builder.rs @@ -25,7 +25,7 @@ where #[must_use] pub fn insert( mut self, - builder: impl Fn(BuilderEntity) -> BuilderEntity, + builder: impl FnOnce(BuilderEntity) -> BuilderEntity, ) -> Self { let id = self.0.insert().id(); builder(BuilderEntity { model: self, id }).model From 2c93a4094fe331726ad0f7b80ae00a28f856543b Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 11 Nov 2025 16:51:41 +0100 Subject: [PATCH 152/352] i18n: translation updates from weblate Co-authored-by: Anonymous Co-authored-by: Feike Donia Co-authored-by: Hosted Weblate Co-authored-by: Yelysei Co-authored-by: twlvnn kraftwerk Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/bg/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/uk/ Translation: Pop OS/libcosmic --- i18n/bg/libcosmic.ftl | 20 +++++++++++++++++++- i18n/frk/libcosmic.ftl | 0 i18n/uk/libcosmic.ftl | 19 +++++++++++++++++++ 3 files changed, 38 insertions(+), 1 deletion(-) create mode 100644 i18n/frk/libcosmic.ftl diff --git a/i18n/bg/libcosmic.ftl b/i18n/bg/libcosmic.ftl index 2ac4d072..ab5ffb56 100644 --- a/i18n/bg/libcosmic.ftl +++ b/i18n/bg/libcosmic.ftl @@ -1,6 +1,5 @@ # Context Drawer close = Затваряне - # About license = Лиценз links = Връзки @@ -9,3 +8,22 @@ 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 = Пн +tuesday = Вт +wednesday = Ср +thursday = Чт +friday = Пт +saturday = Сб +sunday = Нд diff --git a/i18n/frk/libcosmic.ftl b/i18n/frk/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/uk/libcosmic.ftl b/i18n/uk/libcosmic.ftl index cfdc14b8..73278ae4 100644 --- a/i18n/uk/libcosmic.ftl +++ b/i18n/uk/libcosmic.ftl @@ -8,3 +8,22 @@ designers = Дизайнери artists = Художники translators = Перекладачі documenters = Документатори +february = Лютий { $year } +november = Листопад { $year } +friday = Пт +tuesday = Вт +may = Травень { $year } +wednesday = Ср +april = Квітень { $year } +monday = Пн +december = Грудень { $year } +sunday = Нд +march = Березень { $year } +june = Червень { $year } +saturday = Сб +august = Серпень { $year } +july = Липень { $year } +thursday = Чт +september = Вересень { $year } +october = Жовтень { $year } +january = Січень { $year } From 2296e8e94dee2b8b2c2d658c72b96eccf37d566e Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Tue, 11 Nov 2025 15:04:09 -0500 Subject: [PATCH 153/352] feat(applets): configurable applet overlap and padding increases --- Cargo.toml | 1 + src/applet/column.rs | 508 +++++++++++++++++++++++++++++++++ src/applet/mod.rs | 139 +++++++-- src/applet/row.rs | 498 ++++++++++++++++++++++++++++++++ src/widget/color_picker/mod.rs | 50 ++-- 5 files changed, 1135 insertions(+), 61 deletions(-) create mode 100644 src/applet/column.rs create mode 100644 src/applet/row.rs diff --git a/Cargo.toml b/Cargo.toml index 16af9575..1f6dfe3d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -200,6 +200,7 @@ optional = true [dependencies.cosmic-panel-config] git = "https://github.com/pop-os/cosmic-panel" +# path = "../cosmic-panel/cosmic-panel-config" optional = true [dependencies.ron] diff --git a/src/applet/column.rs b/src/applet/column.rs new file mode 100644 index 00000000..8fa2fa9f --- /dev/null +++ b/src/applet/column.rs @@ -0,0 +1,508 @@ +//! Distribute content vertically. +use crate::iced; +use iced::core::alignment::{self, Alignment}; +use iced::core::event::{self, Event}; +use iced::core::layout; +use iced::core::mouse; +use iced::core::overlay; +use iced::core::renderer; +use iced::core::widget::{Operation, Tree}; +use iced::core::{ + Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, + widget, +}; + +/// A container that distributes its contents vertically. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{button, column}; +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// column![ +/// "I am on top!", +/// button("I am in the center!"), +/// "I am below.", +/// ].into() +/// } +/// ``` +#[allow(missing_debug_implementations)] +#[must_use] +pub struct Column<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> { + spacing: f32, + padding: Padding, + width: Length, + height: Length, + max_width: f32, + align: Alignment, + clip: bool, + children: Vec>, +} + +impl<'a, Message, Theme, Renderer> Column<'a, Message, Theme, Renderer> +where + Renderer: iced::core::Renderer, +{ + /// Creates an empty [`Column`]. + pub fn new() -> Self { + Self::from_vec(Vec::new()) + } + + /// Creates a [`Column`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vec(Vec::with_capacity(capacity)) + } + + /// Creates a [`Column`] with the given elements. + pub fn with_children( + children: impl IntoIterator>, + ) -> Self { + let iterator = children.into_iter(); + + Self::with_capacity(iterator.size_hint().0).extend(iterator) + } + + /// Creates a [`Column`] from an already allocated [`Vec`]. + /// + /// Keep in mind that the [`Column`] will not inspect the [`Vec`], which means + /// it won't automatically adapt to the sizing strategy of its contents. + /// + /// If any of the children have a [`Length::Fill`] strategy, you will need to + /// call [`Column::width`] or [`Column::height`] accordingly. + pub fn from_vec(children: Vec>) -> Self { + Self { + spacing: 0.0, + padding: Padding::ZERO, + width: Length::Shrink, + height: Length::Shrink, + max_width: f32::INFINITY, + align: Alignment::Start, + clip: false, + children, + } + } + + /// Sets the vertical spacing _between_ elements. + /// + /// Custom margins per element do not exist in iced. You should use this + /// method instead! While less flexible, it helps you keep spacing between + /// elements consistent. + pub fn spacing(mut self, amount: impl Into) -> Self { + self.spacing = amount.into().0; + self + } + + /// Sets the [`Padding`] of the [`Column`]. + pub fn padding>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the width of the [`Column`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Column`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the maximum width of the [`Column`]. + pub fn max_width(mut self, max_width: impl Into) -> Self { + self.max_width = max_width.into().0; + self + } + + /// Sets the horizontal alignment of the contents of the [`Column`] . + pub fn align_x(mut self, align: impl Into) -> Self { + self.align = Alignment::from(align.into()); + self + } + + /// Sets whether the contents of the [`Column`] should be clipped on + /// overflow. + pub fn clip(mut self, clip: bool) -> Self { + self.clip = clip; + self + } + + /// Adds an element to the [`Column`]. + pub fn push(mut self, child: impl Into>) -> Self { + let child = child.into(); + let child_size = child.as_widget().size_hint(); + + self.width = self.width.enclose(child_size.width); + self.height = self.height.enclose(child_size.height); + + self.children.push(child); + self + } + + /// Adds an element to the [`Column`], if `Some`. + #[must_use] + pub fn push_maybe( + self, + child: Option>>, + ) -> Self { + if let Some(child) = child { + self.push(child) + } else { + self + } + } + + /// Extends the [`Column`] with the given children. + pub fn extend( + self, + children: impl IntoIterator>, + ) -> Self { + children.into_iter().fold(self, Self::push) + } +} + +impl Default for Column<'_, Message, Renderer> +where + Renderer: iced::core::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'a, Message, Theme, Renderer: iced::core::Renderer> + FromIterator> for Column<'a, Message, Theme, Renderer> +{ + fn from_iter>>(iter: T) -> Self { + Self::with_children(iter) + } +} + +impl Widget + for Column<'_, Message, Theme, Renderer> +where + Renderer: iced::core::Renderer, +{ + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() + } + + fn state(&self) -> widget::tree::State { + widget::tree::State::new(State::default()) + } + + fn tag(&self) -> widget::tree::Tag { + widget::tree::Tag::of::() + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(self.children.as_mut_slice()); + } + + fn size(&self) -> Size { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits.max_width(self.max_width); + + layout::flex::resolve( + layout::flex::Axis::Vertical, + renderer, + &limits, + self.width, + self.height, + self.padding, + self.spacing, + self.align, + &self.children, + &mut tree.children, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), c_layout)| { + child.as_widget().operate( + state, + c_layout.with_virtual_offset(layout.virtual_offset()), + renderer, + operation, + ); + }); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + 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 { + let child_layout = layout.children().nth(hovered); + if let Some(child_layout) = child_layout + && cursor.is_over(child_layout.bounds()) + { + // if mouse event, we can skip checking other children + if let Event::Mouse(e) = &event { + if !matches!( + e, + mouse::Event::CursorLeft | mouse::Event::ButtonReleased { .. } + ) { + return self.children[hovered].as_widget_mut().on_event( + &mut tree.children[hovered], + event, + child_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + } else if let Event::Touch(t) = &event { + if !matches!( + t, + iced::core::touch::Event::FingerLifted { .. } + | iced::core::touch::Event::FingerLost { .. } + ) { + return self.children[hovered].as_widget_mut().on_event( + &mut tree.children[hovered], + event, + child_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + } + } else { + my_state.hovered = None; + } + } + + 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().on_event( + state, + event.clone(), + 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().on_event( + state, + event.clone(), + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor_virtual, + renderer, + clipboard, + shell, + viewport, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|((child, state), c_layout)| { + child.as_widget().mouse_interaction( + state, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { + let my_state = tree.state.downcast_ref::(); + + let viewport = if self.clip { + &clipped_viewport + } else { + viewport + }; + + for (i, ((child, state), c_layout)) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .filter(|(_, layout)| layout.bounds().intersects(viewport)) + .enumerate() + { + child.as_widget().draw( + state, + renderer, + theme, + style, + c_layout.with_virtual_offset(layout.virtual_offset()), + if my_state.hovered.is_some_and(|h| i == h) { + cursor + } else { + mouse::Cursor::Unavailable + }, + viewport, + ); + } + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + overlay::from_children(&mut self.children, tree, layout, renderer, translation) + } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::A11yTree; + A11yTree::join( + self.children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + .map(|((c, c_layout), state)| { + c.as_widget().a11y_nodes( + c_layout.with_virtual_offset(layout.virtual_offset()), + state, + cursor, + ) + }), + ) + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut iced::core::clipboard::DndDestinationRectangles, + ) { + for ((e, c_layout), state) in self + .children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + { + e.as_widget().drag_destinations( + state, + c_layout.with_virtual_offset(layout.virtual_offset()), + renderer, + dnd_rectangles, + ); + } + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: iced::core::Renderer + 'a, +{ + fn from(column: Column<'a, Message, Theme, Renderer>) -> Self { + Self::new(column) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct State { + hovered: Option, +} diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 659b7e92..0ab18817 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -1,13 +1,14 @@ #[cfg(feature = "applet-token")] pub mod token; +use crate::app::cosmic; use crate::{ Application, Element, Renderer, app::iced_settings, cctk::sctk, iced::{ self, Color, Length, Limits, Rectangle, - alignment::{Horizontal, Vertical}, + alignment::{Alignment, Horizontal, Vertical}, widget::Container, window, }, @@ -16,18 +17,24 @@ use crate::{ widget::{ self, autosize::{self, Autosize, autosize}, - layer_container, + column::Column, + horizontal_space, layer_container, + row::Row, + vertical_space, }, }; pub use cosmic_panel_config; use cosmic_panel_config::{CosmicPanelBackground, PanelAnchor, PanelSize}; use iced_core::{Padding, Shadow}; +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::{borrow::Cow, num::NonZeroU32, rc::Rc, sync::LazyLock, time::Duration}; use tracing::info; -use crate::app::cosmic; +pub mod column; +pub mod row; + static AUTOSIZE_ID: LazyLock = LazyLock::new(|| iced::id::Id::new("cosmic-applet-autosize")); static AUTOSIZE_MAIN_ID: LazyLock = @@ -46,6 +53,8 @@ pub struct Context { /// Includes the configured size of the window. /// This can be used by apples to handle overflow themselves. pub suggested_bounds: Option, + /// Ratio of overlap for applet padding. + pub padding_overlap: f32, } #[derive(Clone, Debug, PartialEq, Eq)] @@ -104,6 +113,10 @@ impl Default for Context { .unwrap_or(CosmicPanelBackground::ThemeDefault), output_name: std::env::var("COSMIC_PANEL_OUTPUT").unwrap_or_default(), panel_type: PanelType::from(std::env::var("COSMIC_PANEL_NAME").unwrap_or_default()), + padding_overlap: str::parse( + &std::env::var("COSMIC_PANEL_PADDING_OVERLAP").unwrap_or_default(), + ) + .unwrap_or(0.0), suggested_bounds: None, } } @@ -124,13 +137,19 @@ impl Context { #[must_use] pub fn suggested_window_size(&self) -> (NonZeroU32, NonZeroU32) { let suggested = self.suggested_size(true); - let applet_padding = self.suggested_padding(true); + let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true); + let (horizontal_padding, vertical_padding) = if self.is_horizontal() { + (applet_padding_major_axis, applet_padding_minor_axis) + } else { + (applet_padding_minor_axis, applet_padding_major_axis) + }; + let configured_width = self .suggested_bounds .as_ref() .and_then(|c| NonZeroU32::new(c.width as u32)) // TODO: should this be physical size instead of logical? .unwrap_or_else(|| { - NonZeroU32::new(suggested.0 as u32 + applet_padding as u32 * 2).unwrap() + NonZeroU32::new(suggested.0 as u32 + horizontal_padding as u32 * 2).unwrap() }); let configured_height = self @@ -138,17 +157,20 @@ impl Context { .as_ref() .and_then(|c| NonZeroU32::new(c.height as u32)) .unwrap_or_else(|| { - NonZeroU32::new(suggested.1 as u32 + applet_padding as u32 * 2).unwrap() + NonZeroU32::new(suggested.1 as u32 + vertical_padding as u32 * 2).unwrap() }); info!("{configured_height:?}"); (configured_width, configured_height) } #[must_use] - pub fn suggested_padding(&self, is_symbolic: bool) -> u16 { + pub fn suggested_padding(&self, is_symbolic: bool) -> (u16, u16) { match &self.size { - Size::PanelSize(size) => size.get_applet_padding(is_symbolic), - Size::Hardcoded(_) => 8, + Size::PanelSize(size) => ( + size.get_applet_shrinkable_padding(is_symbolic), + size.get_applet_padding(is_symbolic), + ), + Size::Hardcoded(_) => (12, 8), } } @@ -160,9 +182,15 @@ impl Context { #[allow(clippy::cast_precision_loss)] pub fn window_settings(&self) -> crate::app::Settings { let (width, height) = self.suggested_size(true); - let applet_padding = self.suggested_padding(true); - let width = f32::from(width) + applet_padding as f32 * 2.; - let height = f32::from(height) + applet_padding as f32 * 2.; + let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true); + let (horizontal_padding, vertical_padding) = if self.is_horizontal() { + (applet_padding_major_axis, applet_padding_minor_axis) + } else { + (applet_padding_minor_axis, applet_padding_major_axis) + }; + + let width = f32::from(width) + horizontal_padding as f32 * 2.; + let height = f32::from(height) + vertical_padding as f32 * 2.; let mut settings = crate::app::Settings::default() .size(iced_core::Size::new(width, height)) .size_limits(Limits::NONE.min_height(height).min_width(width)) @@ -187,28 +215,70 @@ impl Context { icon: widget::icon::Handle, ) -> crate::widget::Button<'a, Message> { let suggested = self.suggested_size(icon.symbolic); - let applet_padding = self.suggested_padding(icon.symbolic); - + let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true); + let (horizontal_padding, vertical_padding) = if self.is_horizontal() { + (applet_padding_major_axis, applet_padding_minor_axis) + } else { + (applet_padding_minor_axis, applet_padding_major_axis) + }; let symbolic = icon.symbolic; + let icon = widget::icon(icon) + .class(if symbolic { + theme::Svg::Custom(Rc::new(|theme| crate::iced_widget::svg::Style { + color: Some(theme.cosmic().background.on.into()), + })) + } else { + theme::Svg::default() + }) + .width(Length::Fixed(suggested.0 as f32)) + .height(Length::Fixed(suggested.1 as f32)); + self.button_from_element(icon, symbolic) + } + pub fn button_from_element<'a, Message: Clone + 'static>( + &self, + content: impl Into>, + use_symbolic_size: bool, + ) -> crate::widget::Button<'a, Message> { + let suggested = self.suggested_size(use_symbolic_size); + let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true); + let (horizontal_padding, vertical_padding) = if self.is_horizontal() { + (applet_padding_major_axis, applet_padding_minor_axis) + } else { + (applet_padding_minor_axis, applet_padding_major_axis) + }; + + 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)) + .class(Button::AppletIcon) + } + + pub fn text_button<'a, Message: Clone + 'static>( + &self, + text: impl Into>, + message: Message, + ) -> crate::widget::Button<'a, Message> { + let text = text.into(); + let suggested = self.suggested_size(true); + + let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true); + let (horizontal_padding, vertical_padding) = if self.is_horizontal() { + (applet_padding_major_axis, applet_padding_minor_axis) + } else { + (applet_padding_minor_axis, applet_padding_major_axis) + }; crate::widget::button::custom( layer_container( - widget::icon(icon) - .class(if symbolic { - theme::Svg::Custom(Rc::new(|theme| crate::iced_widget::svg::Style { - color: Some(theme.cosmic().background.on.into()), - })) - } else { - theme::Svg::default() - }) - .width(Length::Fixed(suggested.0 as f32)) - .height(Length::Fixed(suggested.1 as f32)), + Text::from(text) + .height(Length::Fill) + .align_y(Alignment::Center), ) - .center(Length::Fill), + .center_y(Length::Fixed(f32::from(suggested.1 + 2 * vertical_padding))), ) - .width(Length::Fixed((suggested.0 + 2 * applet_padding) as f32)) - .height(Length::Fixed((suggested.1 + 2 * applet_padding) as f32)) - .class(Button::AppletIcon) + .on_press_down(message) + .padding([0, horizontal_padding]) + .class(crate::theme::Button::AppletIcon) } pub fn icon_button<'a, Message: Clone + 'static>( @@ -345,7 +415,12 @@ impl Context { height_padding: Option, ) -> SctkPopupSettings { let (width, height) = self.suggested_size(true); - let applet_padding = self.suggested_padding(true); + let (applet_padding_major_axis, applet_padding_minor_axis) = self.suggested_padding(true); + let (horizontal_padding, vertical_padding) = if self.is_horizontal() { + (applet_padding_major_axis, applet_padding_minor_axis) + } else { + (applet_padding_minor_axis, applet_padding_major_axis) + }; let pixel_offset = 4; let (offset, anchor, gravity) = match self.anchor { PanelAnchor::Left => ((pixel_offset, 0), Anchor::Right, Gravity::Right), @@ -364,8 +439,10 @@ impl Context { anchor_rect: Rectangle { x: 0, y: 0, - width: width_padding.unwrap_or(applet_padding as i32) * 2 + i32::from(width), - height: height_padding.unwrap_or(applet_padding as i32) * 2 + i32::from(height), + width: width_padding.unwrap_or(horizontal_padding as i32) * 2 + + i32::from(width), + height: height_padding.unwrap_or(vertical_padding as i32) * 2 + + i32::from(height), }, reactive: true, constraint_adjustment: 15, // slide_y, slide_x, flip_x, flip_y diff --git a/src/applet/row.rs b/src/applet/row.rs new file mode 100644 index 00000000..b5cf851f --- /dev/null +++ b/src/applet/row.rs @@ -0,0 +1,498 @@ +//! Distribute content horizontally. +use crate::iced; +use iced::core::alignment::{self, Alignment}; +use iced::core::event::{self, Event}; +use iced::core::layout::{self, Layout}; +use iced::core::mouse; +use iced::core::overlay; +use iced::core::renderer; +use iced::core::widget::{Operation, Tree}; +use iced::core::{ + Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, widget, +}; +use iced::touch; + +/// A container that distributes its contents horizontally. +/// +/// # Example +/// ```no_run +/// # mod iced { pub mod widget { pub use iced_widget::*; } } +/// # pub type State = (); +/// # pub type Element<'a, Message> = iced_widget::core::Element<'a, Message, iced_widget::Theme, iced_widget::Renderer>; +/// use iced::widget::{button, row}; +/// +/// #[derive(Debug, Clone)] +/// enum Message { +/// // ... +/// } +/// +/// fn view(state: &State) -> Element<'_, Message> { +/// row![ +/// "I am to the left!", +/// button("I am in the middle!"), +/// "I am to the right!", +/// ].into() +/// } +/// ``` +#[allow(missing_debug_implementations)] +#[must_use] +pub struct Row<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer> { + spacing: f32, + padding: Padding, + width: Length, + height: Length, + align: Alignment, + clip: bool, + children: Vec>, +} + +impl<'a, Message, Theme, Renderer> Row<'a, Message, Theme, Renderer> +where + Renderer: iced::core::Renderer, +{ + /// Creates an empty [`Row`]. + pub fn new() -> Self { + Self::from_vec(Vec::new()) + } + + /// Creates a [`Row`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vec(Vec::with_capacity(capacity)) + } + + /// Creates a [`Row`] with the given elements. + pub fn with_children( + children: impl IntoIterator>, + ) -> Self { + let iterator = children.into_iter(); + + Self::with_capacity(iterator.size_hint().0).extend(iterator) + } + + /// Creates a [`Row`] from an already allocated [`Vec`]. + /// + /// Keep in mind that the [`Row`] will not inspect the [`Vec`], which means + /// it won't automatically adapt to the sizing strategy of its contents. + /// + /// If any of the children have a [`Length::Fill`] strategy, you will need to + /// call [`Row::width`] or [`Row::height`] accordingly. + pub fn from_vec(children: Vec>) -> Self { + Self { + spacing: 0.0, + padding: Padding::ZERO, + width: Length::Shrink, + height: Length::Shrink, + align: Alignment::Start, + clip: false, + children, + } + } + + /// Sets the horizontal spacing _between_ elements. + /// + /// Custom margins per element do not exist in iced. You should use this + /// method instead! While less flexible, it helps you keep spacing between + /// elements consistent. + pub fn spacing(mut self, amount: impl Into) -> Self { + self.spacing = amount.into().0; + self + } + + /// Sets the [`Padding`] of the [`Row`]. + pub fn padding>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the width of the [`Row`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`Row`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the vertical alignment of the contents of the [`Row`] . + pub fn align_y(mut self, align: impl Into) -> Self { + self.align = Alignment::from(align.into()); + self + } + + /// Sets whether the contents of the [`Row`] should be clipped on + /// overflow. + pub fn clip(mut self, clip: bool) -> Self { + self.clip = clip; + self + } + + /// Adds an [`Element`] to the [`Row`]. + pub fn push(mut self, child: impl Into>) -> Self { + let child = child.into(); + let child_size = child.as_widget().size_hint(); + + self.width = self.width.enclose(child_size.width); + self.height = self.height.enclose(child_size.height); + + self.children.push(child); + self + } + + /// Adds an element to the [`Row`], if `Some`. + pub fn push_maybe( + self, + child: Option>>, + ) -> Self { + if let Some(child) = child { + self.push(child) + } else { + self + } + } + + /// Extends the [`Row`] with the given children. + pub fn extend( + self, + children: impl IntoIterator>, + ) -> Self { + children.into_iter().fold(self, Self::push) + } +} + +impl<'a, Message, Renderer> Default for Row<'a, Message, Renderer> +where + Renderer: iced::core::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'a, Message, Theme, Renderer: iced::core::Renderer> + FromIterator> for Row<'a, Message, Theme, Renderer> +{ + fn from_iter>>(iter: T) -> Self { + Self::with_children(iter) + } +} + +impl Widget + for Row<'_, Message, Theme, Renderer> +where + Renderer: iced::core::Renderer, +{ + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() + } + + fn state(&self) -> widget::tree::State { + widget::tree::State::new(State::default()) + } + + fn tag(&self) -> widget::tree::Tag { + widget::tree::Tag::of::() + } + + fn diff(&mut self, tree: &mut Tree) { + tree.diff_children(&mut self.children); + } + + fn size(&self) -> Size { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + layout::flex::resolve( + layout::flex::Axis::Horizontal, + renderer, + limits, + self.width, + self.height, + self.padding, + self.spacing, + self.align, + &self.children, + &mut tree.children, + ) + } + + fn operate( + &self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + operation.container(None, layout.bounds(), &mut |operation| { + self.children + .iter() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), c_layout)| { + child.as_widget().operate( + state, + c_layout.with_virtual_offset(layout.virtual_offset()), + renderer, + operation, + ); + }); + }); + } + + fn on_event( + &mut self, + tree: &mut Tree, + 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 { + let child_layout = layout.children().nth(hovered); + if let Some(child_layout) = child_layout + && cursor.is_over(child_layout.bounds()) + { + // if mouse event, we can skip checking other children + if let Event::Mouse(e) = &event { + if !matches!( + e, + mouse::Event::CursorLeft | mouse::Event::ButtonReleased { .. } + ) { + return self.children[hovered].as_widget_mut().on_event( + &mut tree.children[hovered], + event, + child_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + } else if let Event::Touch(t) = &event { + if !matches!( + t, + iced::core::touch::Event::FingerLifted { .. } + | iced::core::touch::Event::FingerLost { .. } + ) { + return self.children[hovered].as_widget_mut().on_event( + &mut tree.children[hovered], + event, + child_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + } + } else { + my_state.hovered = None; + } + } + + 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().on_event( + state, + event.clone(), + 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().on_event( + state, + event.clone(), + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor_virtual, + renderer, + clipboard, + shell, + viewport, + ) + }) + .fold(event::Status::Ignored, event::Status::merge) + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + self.children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|((child, state), c_layout)| { + child.as_widget().mouse_interaction( + state, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { + let my_state = tree.state.downcast_ref::(); + + let viewport = if self.clip { + &clipped_viewport + } else { + viewport + }; + + for (i, ((child, state), c_layout)) in self + .children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .filter(|(_, layout)| layout.bounds().intersects(viewport)) + .enumerate() + { + child.as_widget().draw( + state, + renderer, + theme, + style, + c_layout.with_virtual_offset(layout.virtual_offset()), + if my_state.hovered.is_some_and(|h| i == h) { + cursor + } else { + mouse::Cursor::Unavailable + }, + viewport, + ); + } + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + translation: Vector, + ) -> Option> { + overlay::from_children(&mut self.children, tree, layout, renderer, translation) + } + + #[cfg(feature = "a11y")] + /// get the a11y nodes for the widget + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + use iced_accessibility::A11yTree; + A11yTree::join( + self.children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + .map(|((c, c_layout), state)| { + c.as_widget().a11y_nodes( + c_layout.with_virtual_offset(layout.virtual_offset()), + state, + cursor, + ) + }), + ) + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut iced::core::clipboard::DndDestinationRectangles, + ) { + for ((e, c_layout), state) in self + .children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + { + e.as_widget().drag_destinations( + state, + c_layout.with_virtual_offset(layout.virtual_offset()), + renderer, + dnd_rectangles, + ); + } + } +} + +impl<'a, Message, Theme, Renderer> From> + for Element<'a, Message, Theme, Renderer> +where + Message: 'a, + Theme: 'a, + Renderer: iced::core::Renderer + 'a, +{ + fn from(row: Row<'a, Message, Theme, Renderer>) -> Self { + Self::new(row) + } +} + +#[derive(Debug, Clone, Copy, PartialEq, Default)] +pub struct State { + hovered: Option, +} diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 8dba2e1a..40a4a940 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -50,6 +50,24 @@ pub static HSV_RAINBOW: LazyLock> = LazyLock::new(|| { .collect() }); +fn hsv_rainbow(low_hue: f32, high_hue: f32) -> Vec { + let mut colors = Vec::new(); + let steps: u8 = 7; + let step_size = (high_hue - low_hue) / f32::from(steps); + for i in 0..=steps { + let hue = low_hue + step_size * f32::from(i); + colors.push(ColorStop { + color: Color::from(palette::Srgba::from_color(palette::Hsv::new_srgb_const( + RgbHue::new(hue), + 1.0, + 1.0, + ))), + offset: f32::from(i) / f32::from(steps), + }); + } + colors +} + const MAX_RECENT: usize = 20; #[derive(Debug, Clone)] @@ -290,37 +308,9 @@ where copied_to_clipboard_label: T, ) -> ColorPicker<'a, Message> { fn rail_backgrounds(hue: f32) -> (Background, Background) { - let pivot = hue * 7.0 / 360.; + let low_range = hsv_rainbow(0., hue); + let high_range = hsv_rainbow(hue, 360.); - let low_end = pivot.floor() as usize; - let high_start = pivot.ceil() as usize; - let pivot_color = palette::Hsv::new_srgb(RgbHue::new(hue), 1.0, 1.0); - let low_range = HSV_RAINBOW[0..=low_end] - .iter() - .enumerate() - .map(|(i, color)| ColorStop { - color: *color, - offset: i as f32 / pivot.max(0.0001), - }) - .chain(iter::once(ColorStop { - color: iced::Color::from(palette::Srgba::from_color(pivot_color)), - offset: 1., - })) - .collect::>(); - let high_range = iter::once(ColorStop { - color: iced::Color::from(palette::Srgba::from_color(pivot_color)), - offset: 0., - }) - .chain( - HSV_RAINBOW[high_start..] - .iter() - .enumerate() - .map(|(i, color)| ColorStop { - color: *color, - offset: (i as f32 + (1. - pivot.fract())) / (7. - pivot).max(0.0001), - }), - ) - .collect::>(); ( Background::Gradient(iced::Gradient::Linear( Linear::new(Radians(90.0)).add_stops(low_range), From 690f1d331d7fbe732837f615688f1e1ce1df81b4 Mon Sep 17 00:00:00 2001 From: Stephan Buys Date: Thu, 13 Nov 2025 17:02:12 +0200 Subject: [PATCH 154/352] feat(desktop): add DesktopEntryCache and unit tests for known problematic entries --- Cargo.toml | 9 +- src/desktop.rs | 816 ++++++++++++++++++++++++++++++++++++++++++++++++- 2 files changed, 816 insertions(+), 9 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1f6dfe3d..94b53a64 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -219,12 +219,7 @@ exclude = ["iced"] [workspace.dependencies] dirs = "6.0.0" +[dev-dependencies] +tempfile = "3.13.0" -[patch."https://github.com/pop-os/libcosmic"] -libcosmic = { path = "./" } -# FIXME update winit deps where necessary to use this -# [patch.crates-io] -# [patch."https://github.com/pop-os/winit.git"] -# winit = { git = "https://github.com/pop-os/winit.git//", branch = "xdg-toplevel" } -# winit = { path = "../winit" } diff --git a/src/desktop.rs b/src/desktop.rs index c9b50704..82242460 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -2,9 +2,9 @@ pub use freedesktop_desktop_entry as fde; #[cfg(not(windows))] pub use mime::Mime; -use std::path::PathBuf; +use std::path::{Path, PathBuf}; #[cfg(not(windows))] -use std::{borrow::Cow, ffi::OsStr}; +use std::{borrow::Cow, collections::HashSet, ffi::OsStr}; pub trait IconSourceExt { fn as_cosmic_icon(&self) -> crate::widget::icon::Icon; @@ -51,6 +51,557 @@ pub struct DesktopEntryData { pub terminal: bool, } +#[cfg(not(windows))] +#[derive(Debug, Clone)] +pub struct DesktopEntryCache { + locales: Vec, + entries: Vec, +} + +#[cfg(not(windows))] +impl DesktopEntryCache { + pub fn new(locales: Vec) -> Self { + Self { + locales, + entries: Vec::new(), + } + } + + pub fn from_entries(locales: Vec, entries: Vec) -> Self { + Self { locales, entries } + } + + pub fn ensure_loaded(&mut self) { + if self.entries.is_empty() { + self.refresh(); + } + } + + pub fn refresh(&mut self) { + self.entries = fde::Iter::new(fde::default_paths()) + .filter_map(|p| fde::DesktopEntry::from_path(p, Some(&self.locales)).ok()) + .collect(); + } + + pub fn insert(&mut self, entry: fde::DesktopEntry) { + if self + .entries + .iter() + .any(|existing| existing.id() == entry.id()) + { + return; + } + + self.entries.push(entry); + } + + pub fn locales(&self) -> &[String] { + &self.locales + } + + pub fn entries(&self) -> &[fde::DesktopEntry] { + &self.entries + } + + pub fn entries_mut(&mut self) -> &mut [fde::DesktopEntry] { + &mut self.entries + } +} + +#[cfg(not(windows))] +impl Default for DesktopEntryCache { + fn default() -> Self { + Self::new(Vec::new()) + } +} + +#[cfg(not(windows))] +#[derive(Debug, Clone)] +pub struct DesktopLookupContext<'a> { + pub app_id: Cow<'a, str>, + pub identifier: Option>, + pub title: Option>, +} + +#[cfg(not(windows))] +impl<'a> DesktopLookupContext<'a> { + pub fn new(app_id: impl Into>) -> Self { + Self { + app_id: app_id.into(), + identifier: None, + title: None, + } + } + + pub fn with_identifier(mut self, identifier: impl Into>) -> Self { + self.identifier = Some(identifier.into()); + self + } + + pub fn with_title(mut self, title: impl Into>) -> Self { + self.title = Some(title.into()); + self + } +} + +#[cfg(not(windows))] +#[derive(Debug, Clone)] +pub struct DesktopResolveOptions { + pub include_no_display: bool, + pub xdg_current_desktop: Option, +} + +#[cfg(not(windows))] +impl Default for DesktopResolveOptions { + fn default() -> Self { + Self { + include_no_display: false, + xdg_current_desktop: std::env::var("XDG_CURRENT_DESKTOP").ok(), + } + } +} + +#[cfg(not(windows))] +/// Resolve a DesktopEntry for a running toplevel, applying heuristics over +/// app_id, identifier, and title. Includes Proton/Wine handling: Proton can +/// open games as `steam_app_X` (often `steam_app_default`), and Wine windows +/// may use an `.exe` app_id. In those cases we match the localized name +/// against the toplevel title and, for Proton default, restrict matches to +/// entries with `Game` in Categories. +pub fn resolve_desktop_entry( + cache: &mut DesktopEntryCache, + context: &DesktopLookupContext<'_>, + options: &DesktopResolveOptions, +) -> fde::DesktopEntry { + let app_id = fde::unicase::Ascii::new(context.app_id.as_ref()); + + if let Some(entry) = fde::find_app_by_id(cache.entries(), app_id) { + return entry.clone(); + } + + cache.refresh(); + if let Some(entry) = fde::find_app_by_id(cache.entries(), app_id) { + return entry.clone(); + } + + let candidate_ids = candidate_desktop_ids(context); + + if let Some(entry) = try_match_cached(cache.entries(), &candidate_ids) { + return entry; + } + + if let Some(entry) = load_entry_via_app_ids( + cache, + &candidate_ids, + options.include_no_display, + options.xdg_current_desktop.as_deref(), + ) { + cache.insert(entry.clone()); + return entry; + } + + if let Some(entry) = match_startup_wm_class(cache.entries(), context) { + return entry; + } + + // Chromium/CRX heuristic: scan exec/wmclass/icon for a CRX id match. + if let Some(entry) = match_crx_id(cache.entries(), context) { + return entry; + } + + if let Some(entry) = match_exec_basename(cache.entries(), &candidate_ids) { + return entry; + } + + if let Some(entry) = proton_or_wine_fallback(cache, context) { + cache.insert(entry.clone()); + entry + } else { + let fallback = fallback_entry(context); + cache.insert(fallback.clone()); + fallback + } +} + +#[cfg(not(windows))] +fn try_match_cached( + entries: &[fde::DesktopEntry], + candidate_ids: &[String], +) -> Option { + candidate_ids.iter().find_map(|candidate| { + fde::find_app_by_id(entries, fde::unicase::Ascii::new(candidate.as_str())).cloned() + }) +} + +#[cfg(not(windows))] +fn load_entry_via_app_ids( + cache: &DesktopEntryCache, + candidate_ids: &[String], + include_no_display: bool, + xdg_current_desktop: Option<&str>, +) -> Option { + if candidate_ids.is_empty() { + return None; + } + + let candidate_refs: Vec<&str> = candidate_ids.iter().map(String::as_str).collect(); + let locales = cache.locales().to_vec(); + let iter_locales = locales.clone(); + + let desktop_iter = fde::Iter::new(fde::default_paths()) + .filter_map(move |path| fde::DesktopEntry::from_path(path, Some(&iter_locales)).ok()); + + let app_iter = load_applications_for_app_ids( + desktop_iter, + &locales, + candidate_refs, + false, + include_no_display, + xdg_current_desktop, + ); + + let locales_for_load = cache.locales().to_vec(); + for app in app_iter { + if let Some(path) = app.path { + if let Ok(entry) = fde::DesktopEntry::from_path(path, Some(&locales_for_load)) { + return Some(entry); + } + } + } + + None +} + +#[cfg(not(windows))] +fn match_startup_wm_class( + entries: &[fde::DesktopEntry], + context: &DesktopLookupContext<'_>, +) -> Option { + let mut candidates = Vec::new(); + candidates.push(context.app_id.as_ref()); + if let Some(identifier) = context.identifier.as_deref() { + candidates.push(identifier); + } + if let Some(title) = context.title.as_deref() { + candidates.push(title); + } + + for entry in entries { + let Some(wm_class) = entry.startup_wm_class() else { + continue; + }; + + if candidates + .iter() + .any(|candidate| candidate.eq_ignore_ascii_case(wm_class)) + { + return Some(entry.clone()); + } + } + + None +} + +#[cfg(not(windows))] +fn is_crx_id(candidate: &str) -> bool { + is_crx_bytes(candidate.as_bytes()) +} + +#[cfg(not(windows))] +fn is_crx_bytes(bytes: &[u8]) -> bool { + bytes.len() == 32 && bytes.iter().all(|b| matches!(b, b'a'..=b'p')) +} + +#[cfg(not(windows))] +pub fn extract_crx_id(value: &str) -> Option { + if let Some(rest) = value.strip_prefix("chrome-") { + if let Some(first) = rest.split(&['-', '_'][..]).next() { + if is_crx_id(first) { + return Some(first.to_string()); + } + } + } + if let Some(rest) = value.strip_prefix("crx_") { + let token = rest + .split(|c: char| !c.is_ascii_lowercase()) + .next() + .unwrap_or(rest); + if is_crx_id(token) { + return Some(token.to_string()); + } + } + if is_crx_id(value) { + return Some(value.to_string()); + } + + for window in value.as_bytes().windows(32) { + if is_crx_bytes(window) { + // SAFETY: `is_crx_bytes` guarantees the window is ASCII. + let slice = std::str::from_utf8(window).expect("ASCII window"); + return Some(slice.to_string()); + } + } + + None +} + +#[cfg(not(windows))] +fn match_crx_id( + entries: &[fde::DesktopEntry], + context: &DesktopLookupContext<'_>, +) -> Option { + let crx = extract_crx_id(context.app_id.as_ref()) + .or_else(|| context.identifier.as_deref().and_then(extract_crx_id))?; + + for entry in entries { + if let Some(exec) = entry.exec() { + if exec.contains(&format!("--app-id={}", crx)) { + return Some(entry.clone()); + } + } + if let Some(wm) = entry.startup_wm_class() { + if wm.eq_ignore_ascii_case(&format!("crx_{}", crx)) { + return Some(entry.clone()); + } + } + if let Some(icon) = entry.icon() { + if icon.contains(&crx) { + return Some(entry.clone()); + } + } + } + + None +} + +#[cfg(not(windows))] +fn match_exec_basename( + entries: &[fde::DesktopEntry], + candidate_ids: &[String], +) -> Option { + fn normalize_candidate(candidate: &str) -> String { + candidate + .trim_matches(|c: char| c == '"' || c == '\'') + .to_ascii_lowercase() + } + + let mut normalized: Vec = candidate_ids + .iter() + .map(|c| normalize_candidate(c)) + .collect(); + normalized.retain(|c| !c.is_empty()); + + for entry in entries { + let Some(exec) = entry.exec() else { + continue; + }; + + let command = exec + .split_whitespace() + .next() + .map(|token| token.trim_matches(|c: char| c == '"' || c == '\'')) + .filter(|token| !token.is_empty()); + + let Some(command) = command else { + continue; + }; + + let command = Path::new(command); + let basename = command + .file_stem() + .or_else(|| command.file_name()) + .and_then(|s| s.to_str()); + + let Some(basename) = basename else { + continue; + }; + + let basename_lower = basename.to_ascii_lowercase(); + + if normalized + .iter() + .any(|candidate| candidate == &basename_lower) + { + return Some(entry.clone()); + } + } + + None +} + +#[cfg(not(windows))] +fn fallback_entry(context: &DesktopLookupContext<'_>) -> fde::DesktopEntry { + let mut entry = fde::DesktopEntry { + appid: context.app_id.to_string(), + groups: Default::default(), + path: Default::default(), + ubuntu_gettext_domain: None, + }; + + let name = context + .title + .as_ref() + .map(|title| title.to_string()) + .unwrap_or_else(|| context.app_id.to_string()); + entry.add_desktop_entry("Name".to_string(), name); + entry +} + +#[cfg(not(windows))] +// proton opens games as steam_app_X, where X is either the steam appid or +// "default". Games with a steam appid can have a desktop entry generated +// elsewhere; this specifically handles non-steam games opened under Proton. +// In addition, try to match WINE entries whose app_id is the full name of +// the executable (including `.exe`). +fn proton_or_wine_fallback( + cache: &DesktopEntryCache, + context: &DesktopLookupContext<'_>, +) -> 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"); + + if !is_proton_game && !is_wine_entry { + return None; + } + + let title = context.title.as_deref()?; + + for entry in cache.entries() { + let localized_name_matches = entry + .name(cache.locales()) + .is_some_and(|name| name == title); + + if !localized_name_matches { + continue; + } + + if is_proton_game && !entry.categories().unwrap_or_default().contains(&"Game") { + continue; + } + + return Some(entry.clone()); + } + + None +} + +#[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() { + return; + } + + let key = trimmed.to_ascii_lowercase(); + if seen.insert(key) { + ordered.push(trimmed.to_string()); + } + } + + fn add_variants( + seen: &mut HashSet, + ordered: &mut Vec, + value: Option<&str>, + suffixes: &[&str], + ) { + let Some(value) = value else { + return; + }; + + let stripped_quotes = value.trim_matches(|c: char| c == '"' || c == '\''); + let trimmed = stripped_quotes.trim(); + if trimmed.is_empty() { + return; + } + + push_candidate(seen, ordered, trimmed); + if stripped_quotes != trimmed { + push_candidate(seen, ordered, stripped_quotes.trim()); + } + + for suffix in suffixes { + if trimmed.ends_with(suffix) { + let cut = &trimmed[..trimmed.len() - suffix.len()]; + push_candidate(seen, ordered, cut); + } + } + + if trimmed.contains('.') { + if let Some(last) = trimmed.rsplit('.').next() { + if last.len() >= 2 { + push_candidate(seen, ordered, last); + } + } + } + + if trimmed.contains('-') { + push_candidate(seen, ordered, &trimmed.replace('-', "_")); + } + if trimmed.contains('_') { + push_candidate(seen, ordered, &trimmed.replace('_', "-")); + } + + for token in trimmed.split(|c: char| matches!(c, '.' | '-' | '_' | '@' | ' ')) { + if token.len() >= 2 && token != trimmed { + push_candidate(seen, ordered, token); + } + } + } + + add_variants( + &mut seen, + &mut ordered, + Some(context.app_id.as_ref()), + SUFFIXES, + ); + add_variants( + &mut seen, + &mut ordered, + context.identifier.as_deref(), + SUFFIXES, + ); + add_variants(&mut seen, &mut ordered, context.title.as_deref(), &[]); + + // Chromium/Chrome PWA heuristics: favorites may store a short id like + // "chrome--Default" while the actual desktop id is + // "org.chromium.Chromium.flextop.chrome--Default" (Flatpak Chromium) + // or sometimes "org.chromium.Chromium.chrome--Default". Expand those + // candidates so we can match cached entries. + if let Some(app_id) = Some(context.app_id.as_ref()) { + if let Some(rest) = app_id.strip_prefix("chrome-") { + if rest.ends_with("-Default") { + let crx = rest.trim_end_matches("-Default"); + let variants = [ + format!("org.chromium.Chromium.flextop.chrome-{}-Default", crx), + format!("org.chromium.Chromium.chrome-{}-Default", crx), + ]; + for v in variants { + push_candidate(&mut seen, &mut ordered, &v); + } + } + } + if let Some(rest) = app_id.strip_prefix("crx_") { + // Older identifiers may be crx_; expand similarly + let crx = rest; + let variants = [ + format!("org.chromium.Chromium.flextop.chrome-{}-Default", crx), + format!("org.chromium.Chromium.chrome-{}-Default", crx), + ]; + for v in variants { + push_candidate(&mut seen, &mut ordered, &v); + } + } + } + + ordered +} + #[cfg(not(windows))] pub fn load_applications<'a>( locales: &'a [String], @@ -315,3 +866,264 @@ trait SystemdManger { aux: &[(String, Vec<(String, zbus::zvariant::OwnedValue)>)], ) -> zbus::Result; } + +#[cfg(all(test, not(windows)))] +mod tests { + use super::*; + use std::{env, fs, path::Path, path::PathBuf}; + use tempfile::tempdir; + + struct EnvVarGuard { + key: &'static str, + original: Option, + } + + impl EnvVarGuard { + fn set(key: &'static str, value: &Path) -> Self { + let original = env::var(key).ok(); + std::env::set_var(key, value); + Self { key, original } + } + } + + impl Drop for EnvVarGuard { + fn drop(&mut self) { + if let Some(ref original) = self.original { + std::env::set_var(self.key, original); + } else { + std::env::remove_var(self.key); + } + } + } + + fn load_entry(file_name: &str, contents: &str, locales: &[String]) -> fde::DesktopEntry { + let temp = tempdir().expect("tempdir"); + let path = temp.path().join(file_name); + fs::write(&path, contents).expect("write desktop file"); + let entry = fde::DesktopEntry::from_path(path, Some(locales)).expect("load desktop file"); + // Ensure directory stays alive until after parsing + temp.close().expect("close tempdir"); + entry + } + + #[test] + fn candidate_generation_covers_common_variants() { + let ctx = DesktopLookupContext::new("com.example.App.desktop") + .with_identifier("com-example-App") + .with_title("Example App"); + 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())); + } + + #[test] + fn startup_wm_class_matching_detects_flatpak_chrome_apps() { + let temp = tempdir().expect("tempdir"); + let apps_dir = temp.path().join("applications"); + fs::create_dir_all(&apps_dir).expect("create applications dir"); + + let desktop_contents = "\ +[Desktop Entry] +Version=1.0 +Type=Application +Name=Proton Mail +Exec=chromium --app-id=jnpecgipniidlgicjocehkhajgdnjekh +Icon=chrome-jnpecgipniidlgicjocehkhajgdnjekh-Default +StartupWMClass=crx_jnpecgipniidlgicjocehkhajgdnjekh +"; + let desktop_path = apps_dir.join( + "org.chromium.Chromium.flextop.chrome-jnpecgipniidlgicjocehkhajgdnjekh-Default.desktop", + ); + fs::write(desktop_path, desktop_contents).expect("write desktop file"); + + let _guard = EnvVarGuard::set("XDG_DATA_HOME", temp.path()); + + let locales = vec!["en_US.UTF-8".to_string()]; + let mut cache = DesktopEntryCache::new(locales.clone()); + cache.refresh(); + + let ctx = DesktopLookupContext::new("crx_jnpecgipniidlgicjocehkhajgdnjekh"); + let resolved = resolve_desktop_entry(&mut cache, &ctx, &DesktopResolveOptions::default()); + + assert_eq!( + resolved.id(), + "org.chromium.Chromium.flextop.chrome-jnpecgipniidlgicjocehkhajgdnjekh-Default" + ); + } + + #[test] + fn exec_basename_matching_handles_vmware() { + let temp = tempdir().expect("tempdir"); + let apps_dir = temp.path().join("applications"); + fs::create_dir_all(&apps_dir).expect("create applications dir"); + + let desktop_contents = "\ +[Desktop Entry]\n\ +Version=1.0\n\ +Type=Application\n\ +Name=VMware Workstation\n\ +Exec=/usr/bin/vmware %U\n\ +Icon=vmware-workstation\n\ +"; + let desktop_path = apps_dir.join("vmware-workstation.desktop"); + fs::write(desktop_path, desktop_contents).expect("write desktop file"); + + let _guard = EnvVarGuard::set("XDG_DATA_HOME", temp.path()); + + let locales = vec!["en_US.UTF-8".to_string()]; + let mut cache = DesktopEntryCache::new(locales.clone()); + cache.refresh(); + + let ctx = DesktopLookupContext::new("vmware").with_title("Library — VMware Workstation"); + + let resolved = resolve_desktop_entry(&mut cache, &ctx, &DesktopResolveOptions::default()); + + assert_eq!(resolved.id(), "vmware-workstation.desktop"); + } + + #[test] + fn proton_fallback_prefers_game_entries() { + let locales = vec!["en_US.UTF-8".to_string()]; + let entry = load_entry( + "proton.desktop", + "[Desktop Entry]\nType=Application\nName=Proton Game\nCategories=Game;Utility;\nExec=proton-game\n", + &locales, + ); + let cache = DesktopEntryCache::from_entries(locales.clone(), vec![entry]); + let ctx = DesktopLookupContext::new("steam_app_default").with_title("Proton Game"); + + let resolved = proton_or_wine_fallback(&cache, &ctx).expect("expected proton match"); + let name = resolved + .name(&locales) + .expect("name available") + .into_owned(); + + assert_eq!(name, "Proton Game"); + } + + #[test] + fn proton_fallback_skips_non_games() { + let locales = vec!["en_US.UTF-8".to_string()]; + let entry = load_entry( + "tool.desktop", + "[Desktop Entry]\nType=Application\nName=Proton Tool\nCategories=Utility;\nExec=proton-tool\n", + &locales, + ); + let cache = DesktopEntryCache::from_entries(locales, vec![entry]); + let ctx = DesktopLookupContext::new("steam_app_default").with_title("Proton Tool"); + + assert!(proton_or_wine_fallback(&cache, &ctx).is_none()); + } + + #[test] + fn wine_fallback_matches_executable_titles() { + let locales = vec!["en_US.UTF-8".to_string()]; + let entry = load_entry( + "wine.desktop", + "[Desktop Entry]\nType=Application\nName=Wine Game\nExec=wine-game\n", + &locales, + ); + let cache = DesktopEntryCache::from_entries(locales.clone(), vec![entry]); + let ctx = DesktopLookupContext::new("WINEGAME.EXE").with_title("Wine Game"); + + let resolved = proton_or_wine_fallback(&cache, &ctx).expect("expected wine match"); + let name = resolved + .name(&locales) + .expect("name available") + .into_owned(); + assert_eq!(name, "Wine Game"); + } + + #[test] + fn fallback_entry_uses_title_when_available() { + let ctx = DesktopLookupContext::new("unknown-app").with_title("Unknown App"); + let entry = fallback_entry(&ctx); + + assert_eq!(entry.id(), "unknown-app"); + assert_eq!( + entry.name(&["en_US".to_string()]), + Some(Cow::Owned("Unknown App".to_string())) + ); + } + + #[test] + fn desktop_entry_data_prefers_localized_name() { + let locales = vec!["fr".to_string(), "en_US".to_string()]; + let entry = load_entry( + "localized.desktop", + "[Desktop Entry]\nType=Application\nName=Default\nName[fr]=Localisé\nExec=localized\n", + &locales, + ); + let data = DesktopEntryData::from_desktop_entry(&locales, entry); + + assert_eq!(data.name, "Localisé"); + } + + #[test] + fn crx_id_extraction_variants() { + let id = "cadlkienfkclaiaibeoongdcgmdikeeg"; // 32 chars a..p + assert_eq!( + super::extract_crx_id(&format!("chrome-{}-Default", id)), + Some(id.to_string()) + ); + assert_eq!( + super::extract_crx_id(&format!("crx_{}", id)), + Some(id.to_string()) + ); + assert_eq!(super::extract_crx_id(id), Some(id.to_string())); + // Embedded + let embedded = format!("org.chromium.Chromium.flextop.chrome-{}-Default", id); + assert_eq!(super::extract_crx_id(&embedded), Some(id.to_string())); + } + + #[test] + fn crx_matcher_by_exec_and_wmclass() { + use std::fs; + let id = "cadlkienfkclaiaibeoongdcgmdikeeg"; + let temp = tempdir().expect("tempdir"); + let apps_dir = temp.path().join("applications"); + fs::create_dir_all(&apps_dir).expect("create applications dir"); + let desktop_contents = format!( + "[Desktop Entry]\nType=Application\nName=ChatGPT\nExec=chromium --app-id={} --profile-directory=Default\nStartupWMClass=crx_{}\nIcon=chrome-{}-Default\n", + id, id, id + ); + let desktop_path = apps_dir.join( + "org.chromium.Chromium.flextop.chrome-cadlkienfkclaiaibeoongdcgmdikeeg-Default.desktop", + ); + fs::write(&desktop_path, desktop_contents).expect("write desktop file"); + + let _guard = EnvVarGuard::set("XDG_DATA_HOME", temp.path()); + let locales = vec!["en_US.UTF-8".to_string()]; + let mut cache = DesktopEntryCache::new(locales.clone()); + cache.refresh(); + + let short_id = format!("chrome-{}-Default", id); + let ctx = DesktopLookupContext::new(short_id); + let resolved = resolve_desktop_entry(&mut cache, &ctx, &DesktopResolveOptions::default()); + assert!(resolved.icon().is_some()); + assert!(resolved.exec().is_some()); + assert_eq!(resolved.startup_wm_class(), Some(&format!("crx_{}", id))); + } + + #[test] + fn crx_extraction_handles_utf8_prefixes() { + let id = "cadlkienfkclaiaibeoongdcgmdikeeg"; + let prefixed = format!("å{}", id); + assert_eq!(super::extract_crx_id(&prefixed), Some(id.to_string())); + } + + #[test] + fn crx_extraction_ignores_non_ascii_sequences() { + let id = "cadlkienfkclaiaibeoongdcgmdikeeg"; + let embedded = format!("{id}æøå"); + + assert_eq!(super::extract_crx_id(&embedded), Some(id.to_string())); + assert_eq!(super::extract_crx_id("æøå"), None); + } +} From d6b3720e1f161f064a586f4317422e78cbb60214 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 13 Nov 2025 17:51:26 +0100 Subject: [PATCH 155/352] i18n: translation updates from weblate Co-authored-by: Anonymous Co-authored-by: Hosted Weblate Co-authored-by: therealmate Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/hu/ Translation: Pop OS/libcosmic --- i18n/hu/libcosmic.ftl | 20 +++++++++++++++++++- 1 file changed, 19 insertions(+), 1 deletion(-) diff --git a/i18n/hu/libcosmic.ftl b/i18n/hu/libcosmic.ftl index ddc43e6c..583fbe5c 100644 --- a/i18n/hu/libcosmic.ftl +++ b/i18n/hu/libcosmic.ftl @@ -1,6 +1,5 @@ # Context Drawer close = Bezárás - # About license = Licenc links = Linkek @@ -9,3 +8,22 @@ designers = Tervezők artists = Művészek translators = Fordítók documenters = Dokumentálók +january = { $year } január +february = { $year } február +march = { $year } március +april = { $year } április +may = { $year } május +june = { $year } június +july = { $year } július +august = { $year } augusztus +september = { $year } szeptember +october = { $year } október +november = { $year } november +december = { $year } december +monday = H +tuesday = K +wednesday = Sze +thursday = Cs +friday = P +saturday = Szo +sunday = V From 96a51be3e4e0ae2de4af223f3dda75f321973a3e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 14 Nov 2025 11:46:21 -0500 Subject: [PATCH 156/352] chore: update iced image improvements --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 783d764c..16b1f1f3 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 783d764cabd6eee020eeae3b50a0d4727a721056 +Subproject commit 16b1f1f3a2c1ed09e5830724e84bbe5ad6909216 From 16d095b2cdf3696718b1da87a83d8679fbee01a0 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 14 Nov 2025 15:00:01 -0500 Subject: [PATCH 157/352] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 16b1f1f3..b788625a 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 16b1f1f3a2c1ed09e5830724e84bbe5ad6909216 +Subproject commit b788625a353593daea8ef64e9fec58f199ae08d8 From 85284773554d6fcf4f5e9676eb04deb0da472328 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 17 Nov 2025 21:51:23 +0100 Subject: [PATCH 158/352] i18n: translation updates from weblate Co-authored-by: Feike Donia Co-authored-by: GerardWassink Co-authored-by: Hosted Weblate Co-authored-by: Julien Brouillard Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/fr/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/nl/ Translation: Pop OS/libcosmic --- i18n/fr/libcosmic.ftl | 27 +++++++++++++++++++++++++++ i18n/nl/libcosmic.ftl | 20 ++++++++++++++++++++ 2 files changed, 47 insertions(+) diff --git a/i18n/fr/libcosmic.ftl b/i18n/fr/libcosmic.ftl index e69de29b..43e2d6f7 100644 --- a/i18n/fr/libcosmic.ftl +++ b/i18n/fr/libcosmic.ftl @@ -0,0 +1,27 @@ +close = Fermer +documenters = Rédacteurs +translators = Traducteurs +artists = Artistes +license = Licence +links = Liens +developers = Développeurs +january = Janvier { $year } +february = Février { $year } +april = Avril { $year } +march = Mars { $year } +november = Novembre { $year } +friday = Ven +tuesday = Mar +may = Mai { $year } +wednesday = Mer +monday = Lun +december = Décembre { $year } +sunday = Dim +june = Juin { $year } +saturday = Sam +august = Août { $year } +july = Juillet { $year } +thursday = Jeu +september = Septembre { $year } +october = Octobre { $year } +designers = Designers diff --git a/i18n/nl/libcosmic.ftl b/i18n/nl/libcosmic.ftl index b0aafeba..75fc8cdf 100644 --- a/i18n/nl/libcosmic.ftl +++ b/i18n/nl/libcosmic.ftl @@ -1 +1,21 @@ close = Sluiten +license = Licentie +january = Januari { $year } +february = Februari { $year } +march = Maart { $year } +april = April { $year } +may = Mei { $year } +june = Juni { $year } +july = Juli { $year } +august = Augustus { $year } +september = September { $year } +october = Oktober { $year } +november = November { $year } +december = December { $year } +monday = Ma +tuesday = Di +wednesday = Woe +thursday = Do +friday = Vrij +saturday = Za +sunday = Zon From 47cc6dbdbf35b214bf8bf1deaf6b523ea0fe7f93 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 18 Nov 2025 10:36:11 -0500 Subject: [PATCH 159/352] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index b788625a..d2949f2d 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit b788625a353593daea8ef64e9fec58f199ae08d8 +Subproject commit d2949f2dbbb60c65a48e941e89c36563a5cde3a6 From 7eecbe30d78b1d4f959429ea233b294900af4eed Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 18 Nov 2025 18:35:27 +0100 Subject: [PATCH 160/352] feat(dropdown): add `Id` support with custom `close`, `open` operations --- src/widget/dropdown/mod.rs | 14 +- src/widget/dropdown/operation.rs | 72 ++++++++++ src/widget/dropdown/widget.rs | 238 ++++++++++++++++++++----------- 3 files changed, 236 insertions(+), 88 deletions(-) create mode 100644 src/widget/dropdown/operation.rs diff --git a/src/widget/dropdown/mod.rs b/src/widget/dropdown/mod.rs index bcb37af8..fa4184c4 100644 --- a/src/widget/dropdown/mod.rs +++ b/src/widget/dropdown/mod.rs @@ -7,15 +7,17 @@ use std::borrow::Cow; pub mod menu; -use iced_core::window; pub use menu::Menu; pub mod multi; +pub mod operation; mod widget; pub use widget::*; use crate::surface; +pub use iced_core::widget::Id; +use iced_core::window; /// Displays a list of options in a popover menu on select. pub fn dropdown< @@ -53,3 +55,13 @@ 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 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/operation.rs b/src/widget/dropdown/operation.rs new file mode 100644 index 00000000..8cea4566 --- /dev/null +++ b/src/widget/dropdown/operation.rs @@ -0,0 +1,72 @@ +// Copyright 2025 System76 +// SPDX-License-Identifier: MPL-2.0 AND MIT +//! Operate on dropdown widgets. + +use super::State; +use iced::Rectangle; +use iced_core::widget::{Id, Operation}; + +pub trait Dropdown { + fn close(&mut self); + fn open(&mut self); +} + +/// 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; + } + + let Some(state) = state.downcast_mut::() else { + return; + }; + + state.close(); + } + + fn container( + &mut self, + _id: Option<&Id>, + _bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + Close(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; + } + + let Some(state) = state.downcast_mut::() else { + return; + }; + + state.open(); + } + + fn container( + &mut self, + _id: Option<&Id>, + _bounds: Rectangle, + operate_on_children: &mut dyn FnMut(&mut dyn Operation), + ) { + operate_on_children(self) + } + } + + Open(id) +} diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index d196215d..47df9b89 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -2,6 +2,7 @@ // Copyright 2019 Héctor Ramón, Iced contributors // SPDX-License-Identifier: MPL-2.0 AND MIT +use super::Id; use super::menu::{self, Menu}; use crate::widget::icon::{self, Handle}; use crate::{Element, surface}; @@ -18,19 +19,21 @@ use iced_widget::pick_list::{self, Catalog}; use std::borrow::Cow; use std::ffi::OsStr; use std::hash::{DefaultHasher, Hash, Hasher}; -use std::marker::PhantomData; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, LazyLock, Mutex}; pub type DropdownView = Arc Element<'static, Message> + Send + Sync>; static AUTOSIZE_ID: LazyLock = LazyLock::new(|| crate::widget::Id::new("cosmic-applet-autosize")); + /// A widget for selecting a single value from a list of selections. #[derive(Setters)] pub struct Dropdown<'a, S: AsRef + Send + Sync + Clone + 'static, Message, AppMessage> where [S]: std::borrow::ToOwned, { + #[setters(skip)] + id: Option, #[setters(skip)] on_selected: Arc Message + Send + Sync>, #[setters(skip)] @@ -78,6 +81,7 @@ where on_selected: impl Fn(usize) -> Message + 'static + Send + Sync, ) -> Self { Self { + id: None, on_selected: Arc::new(on_selected), selections, icons: Cow::Borrowed(&[]), @@ -100,12 +104,13 @@ where /// Handle dropdown requests for popup creation. /// Intended to be used with [`crate::app::message::get_popup`] pub fn with_popup( - mut self, + self, parent_id: window::Id, on_surface_action: impl Fn(surface::Action) -> Message + Send + Sync + 'static, action_map: impl Fn(Message) -> NewAppMessage + Send + Sync + 'static, ) -> Dropdown<'a, S, Message, NewAppMessage> { let Self { + id, on_selected, selections, icons, @@ -121,6 +126,7 @@ where } = self; Dropdown::<'a, S, Message, NewAppMessage> { + id, on_selected, selections, icons, @@ -138,6 +144,11 @@ where } } + pub fn id(mut self, id: Id) -> Self { + self.id = Some(id); + self + } + #[cfg(all(feature = "winit", feature = "wayland"))] pub fn with_positioner( mut self, @@ -299,6 +310,17 @@ where ); } + fn operate( + &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()); + } + fn overlay<'b>( &'b mut self, tree: &'b mut Tree, @@ -364,6 +386,8 @@ pub struct State { menu: menu::State, keyboard_modifiers: keyboard::Modifiers, is_open: Arc, + close_operation: bool, + open_operation: bool, hovered_option: Arc>>, hashes: Vec, selections: Vec, @@ -389,6 +413,8 @@ impl State { selections: Vec::new(), hashes: Vec::new(), popup_id: window::Id::unique(), + close_operation: false, + open_operation: false, } } } @@ -399,6 +425,16 @@ impl Default for State { } } +impl super::operation::Dropdown for State { + fn close(&mut self) { + self.close_operation = true; + } + + fn open(&mut self) { + self.open_operation = true; + } +} + /// Computes the layout of a [`Dropdown`]. #[allow(clippy::too_many_arguments)] pub fn layout( @@ -484,10 +520,121 @@ pub fn update< font: Option, selected_option: Option, ) -> event::Status { + let state = state(); + + let open = |shell: &mut Shell<'_, Message>, + state: &mut State, + on_selected: Arc Message + Send + Sync + 'static>| { + state.is_open.store(true, Ordering::Relaxed); + let mut hovered_guard = state.hovered_option.lock().unwrap(); + *hovered_guard = selected; + let id = window::Id::unique(); + state.popup_id = id; + #[cfg(all(feature = "winit", feature = "wayland"))] + if let Some(((on_surface_action, parent), action_map)) = on_surface_action + .as_ref() + .zip(_window_id) + .zip(action_map.clone()) + { + use iced_runtime::platform_specific::wayland::popup::{ + SctkPopupSettings, SctkPositioner, + }; + let bounds = layout.bounds(); + let anchor_rect = Rectangle { + x: bounds.x as i32, + y: bounds.y as i32, + width: bounds.width as i32, + height: bounds.height as i32, + }; + let icon_width = if icons.is_empty() { 0.0 } else { 24.0 }; + 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 selections_width = selections + .iter() + .zip(state.selections.iter_mut()) + .map(|(label, selection)| measure(label.as_ref(), selection.raw())) + .fold(0.0, |next, current| current.max(next)); + + let icons: Cow<'static, [Handle]> = Cow::Owned(icons.to_vec()); + let selections: Cow<'static, [S]> = Cow::Owned(selections.to_vec()); + let state = state.clone(); + let on_close = surface::action::destroy_popup(id); + let on_surface_action_clone = on_surface_action.clone(); + let translation = layout.virtual_offset(); + let get_popup_action = surface::action::simple_popup::( + move || { + SctkPopupSettings { + parent, + id, + input_zone: None, + positioner: SctkPositioner { + size: Some((selections_width as u32 + gap as u32 + pad_width as u32 + icon_width as u32, 10)), + anchor_rect, + // TODO: left or right alignment based on direction? + anchor: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft, + gravity: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, + reactive: true, + offset: ((-padding.left - translation.x) as i32, -translation.y as i32), + constraint_adjustment: 9, + ..Default::default() + }, + parent_size: None, + grab: true, + close_with_children: true, + } + }, + Some(Box::new(move || { + let action_map = action_map.clone(); + let on_selected = on_selected.clone(); + let e: Element<'static, crate::Action> = + Element::from(menu_widget( + bounds, + &state, + gap, + padding, + text_size.unwrap_or(14.0), + selections.clone(), + icons.clone(), + selected_option, + Arc::new(move |i| on_selected.clone()(i)), + Some(on_surface_action_clone(on_close.clone())), + )) + .map(move |m| crate::Action::App(action_map.clone()(m))); + e + })), + ); + shell.publish(on_surface_action(get_popup_action)); + } + }; + + let is_open = state.is_open.load(Ordering::Relaxed); + let refresh = state.close_operation && state.open_operation; + + if state.close_operation { + state.close_operation = false; + state.is_open.store(false, Ordering::SeqCst); + if is_open { + #[cfg(all(feature = "winit", feature = "wayland"))] + if let Some(ref on_close) = on_surface_action { + shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); + } + } + } + + if state.open_operation { + state.open_operation = false; + state.is_open.store(true, Ordering::SeqCst); + if (refresh && is_open) || (!refresh && !is_open) { + open(shell, state, on_selected.clone()); + } + } + match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - let state = state(); let is_open = state.is_open.load(Ordering::Relaxed); if is_open { // Event wasn't processed by overlay, so cursor was clicked either outside it's @@ -499,87 +646,7 @@ pub fn update< } event::Status::Captured } else if cursor.is_over(layout.bounds()) { - state.is_open.store(true, Ordering::Relaxed); - let mut hovered_guard = state.hovered_option.lock().unwrap(); - *hovered_guard = selected; - let id = window::Id::unique(); - state.popup_id = id; - #[cfg(all(feature = "winit", feature = "wayland"))] - if let Some(((on_surface_action, parent), action_map)) = - on_surface_action.zip(_window_id).zip(action_map) - { - use iced_runtime::platform_specific::wayland::popup::{ - SctkPopupSettings, SctkPositioner, - }; - let bounds = layout.bounds(); - let anchor_rect = Rectangle { - x: bounds.x as i32, - y: bounds.y as i32, - width: bounds.width as i32, - height: bounds.height as i32, - }; - let icon_width = if icons.is_empty() { 0.0 } else { 24.0 }; - 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 selections_width = selections - .iter() - .zip(state.selections.iter_mut()) - .map(|(label, selection)| measure(label.as_ref(), selection.raw())) - .fold(0.0, |next, current| current.max(next)); - - let icons: Cow<'static, [Handle]> = Cow::Owned(icons.to_vec()); - let selections: Cow<'static, [S]> = Cow::Owned(selections.to_vec()); - let state = state.clone(); - let on_close = surface::action::destroy_popup(id); - let on_surface_action_clone = on_surface_action.clone(); - let translation = layout.virtual_offset(); - let get_popup_action = surface::action::simple_popup::( - move || { - SctkPopupSettings { - parent, - id, - input_zone: None, - positioner: SctkPositioner { - size: Some((selections_width as u32 + gap as u32 + pad_width as u32 + icon_width as u32, 10)), - anchor_rect, - // TODO: left or right alignment based on direction? - anchor: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Anchor::BottomLeft, - gravity: cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, - reactive: true, - offset: ((-padding.left - translation.x) as i32, -translation.y as i32), - constraint_adjustment: 9, - ..Default::default() - }, - parent_size: None, - grab: true, - close_with_children: true, - } - }, - Some(Box::new(move || { - let action_map = action_map.clone(); - let on_selected = on_selected.clone(); - let e: Element<'static, crate::Action> = - Element::from(menu_widget( - bounds, - &state, - gap, - padding, - text_size.unwrap_or(14.0), - selections.clone(), - icons.clone(), - selected_option, - Arc::new(move |i| on_selected.clone()(i)), - Some(on_surface_action_clone(on_close.clone())), - )) - .map(move |m| crate::Action::App(action_map.clone()(m))); - e - })), - ); - shell.publish(on_surface_action(get_popup_action)); - } + open(shell, state, on_selected); event::Status::Captured } else { event::Status::Ignored @@ -588,7 +655,6 @@ pub fn update< Event::Mouse(mouse::Event::WheelScrolled { delta: mouse::ScrollDelta::Lines { .. }, }) => { - let state = state(); let is_open = state.is_open.load(Ordering::Relaxed); if state.keyboard_modifiers.command() && cursor.is_over(layout.bounds()) && !is_open { @@ -604,8 +670,6 @@ pub fn update< } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - let state = state(); - state.keyboard_modifiers = *modifiers; event::Status::Ignored From fc85fcac3e1d1b7a05982e7396a8bc23ff4d0143 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 18 Nov 2025 18:47:24 +0100 Subject: [PATCH 161/352] fix(dropdown): refresh popup when selections change --- src/widget/dropdown/widget.rs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 47df9b89..a5612ccd 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -178,6 +178,8 @@ where fn diff(&mut self, tree: &mut Tree) { let state = tree.state.downcast_mut::(); + let mut selections_changed = state.selections.len() != self.selections.len(); + state .selections .resize_with(self.selections.len(), crate::Plain::default); @@ -192,6 +194,7 @@ where continue; } + selections_changed = true; state.hashes[i] = text_hash; state.selections[i].update(Text { content: selection.as_ref(), @@ -206,6 +209,11 @@ where wrapping: text::Wrapping::default(), }); } + + if state.is_open.load(Ordering::SeqCst) && selections_changed { + state.close_operation = true; + state.open_operation = true; + } } fn size(&self) -> Size { From 709044891ee04c6ca62ff3d1087ab0e4ebb59bb4 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 19 Nov 2025 10:39:23 -0500 Subject: [PATCH 162/352] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index d2949f2d..c9cd78e0 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit d2949f2dbbb60c65a48e941e89c36563a5cde3a6 +Subproject commit c9cd78e030d5b228f190af28d908e1fbcf8737ce From 7f321cb0a3b5ec53f6a6d33320e4f5d0f737959c Mon Sep 17 00:00:00 2001 From: Stephan Buys Date: Wed, 19 Nov 2025 15:44:31 +0200 Subject: [PATCH 163/352] segmented button: support tab drag + drop --- Cargo.toml | 2 +- src/widget/dnd_destination.rs | 144 ++++- src/widget/segmented_button/mod.rs | 13 + src/widget/segmented_button/model/mod.rs | 71 +++ src/widget/segmented_button/widget.rs | 753 ++++++++++++++++++++++- 5 files changed, 950 insertions(+), 33 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 94b53a64..430af23d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -122,6 +122,7 @@ image = { version = "0.25.8", default-features = false, features = [ "png", ] } libc = { version = "0.2.175", optional = true } +log = "0.4" mime = { version = "0.3.17", optional = true } palette = "0.7.6" raw-window-handle = "0.6" @@ -222,4 +223,3 @@ dirs = "6.0.0" [dev-dependencies] tempfile = "3.13.0" - diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index ccc0fb18..c943d2c7 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -39,6 +39,7 @@ pub fn dnd_destination_for_data<'a, T: AllowedMimeTypes, Message: 'static>( } static DRAG_ID_COUNTER: AtomicU64 = AtomicU64::new(0); +const DND_DEST_LOG_TARGET: &str = "libcosmic::widget::dnd_destination"; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct DragId(pub u128); @@ -75,6 +76,12 @@ pub struct DndDestination<'a, Message> { } impl<'a, Message: 'static> DndDestination<'a, Message> { + fn mime_matches(&self, offered: &[String]) -> bool { + self.mime_types.is_empty() + || offered + .iter() + .any(|mime| self.mime_types.iter().any(|allowed| allowed == mime)) + } pub fn new(child: impl Into>, mimes: Vec>) -> Self { Self { id: Id::unique(), @@ -324,6 +331,12 @@ impl Widget let my_id = self.get_drag_id(); + log::trace!( + target: DND_DEST_LOG_TARGET, + "dnd_destination id={:?}: event {:?}", + self.drag_id.unwrap_or_default(), + event + ); match event { Event::Dnd(DndEvent::Offer( id, @@ -331,6 +344,18 @@ impl Widget x, y, mime_types, .. }, )) 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; + } + 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, @@ -360,6 +385,11 @@ impl Widget return event::Status::Captured; } Event::Dnd(DndEvent::Offer(_, OfferEvent::Leave)) => { + log::trace!( + target: DND_DEST_LOG_TARGET, + "offer leave id={:?}", + my_id + ); if let Some(msg) = state.on_leave(self.on_leave.as_ref().map(std::convert::AsRef::as_ref)) { @@ -383,6 +413,10 @@ impl Widget return event::Status::Ignored; } 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, @@ -413,6 +447,11 @@ impl Widget return event::Status::Captured; } Event::Dnd(DndEvent::Offer(_, OfferEvent::LeaveDestination)) => { + log::trace!( + target: DND_DEST_LOG_TARGET, + "offer leave-destination id={:?}", + my_id + ); if let Some(msg) = state.on_leave(self.on_leave.as_ref().map(std::convert::AsRef::as_ref)) { @@ -421,6 +460,10 @@ impl Widget return event::Status::Ignored; } Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if id == Some(my_id) => { + log::trace!( + target: DND_DEST_LOG_TARGET, + "offer drop id={my_id:?}" + ); if let Some(msg) = state.on_drop(self.on_drop.as_ref().map(std::convert::AsRef::as_ref)) { @@ -431,6 +474,10 @@ impl Widget Event::Dnd(DndEvent::Offer(id, OfferEvent::SelectedAction(action))) 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, self.on_action_selected @@ -444,6 +491,11 @@ impl Widget Event::Dnd(DndEvent::Offer(id, OfferEvent::Data { data, mime_type })) if id == Some(my_id) => { + log::trace!( + target: DND_DEST_LOG_TARGET, + "offer data id={my_id:?} mime={mime_type:?} bytes={}", + data.len() + ); if let (Some(msg), ret) = state.on_data_received( mime_type, data, @@ -521,6 +573,16 @@ impl Widget ) { let bounds = layout.bounds(); let my_id = self.get_drag_id(); + log::trace!( + target: DND_DEST_LOG_TARGET, + "register destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}", + my_id, + bounds.x, + bounds.y, + bounds.width, + bounds.height, + self.mime_types + ); let my_dest = DndDestinationRectangle { id: my_id, rectangle: dnd::Rectangle { @@ -535,12 +597,14 @@ impl Widget }; dnd_rectangles.push(my_dest); - self.container.as_widget().drag_destinations( - &state.children[0], - layout, - renderer, - dnd_rectangles, - ); + if let Some(child_layout) = layout.children().next() { + self.container.as_widget().drag_destinations( + &state.children[0], + child_layout.with_virtual_offset(layout.virtual_offset()), + renderer, + dnd_rectangles, + ); + } } fn id(&self) -> Option { @@ -696,3 +760,71 @@ impl<'a, Message: 'static> From> for Element<'a, Mes Element::new(wrapper) } } + +#[cfg(test)] +mod tests { + use super::*; + + #[derive(Clone, Copy, Debug, PartialEq)] + enum TestMsg { + Data, + Finished, + } + + #[test] + fn data_before_drop_invokes_data_handler_only() { + let mut state: State<()> = State::new(); + assert!(state.drag_offer.is_none()); + state.on_enter::( + 4.0, + 2.0, + vec!["text/plain".into()], + Option:: TestMsg>::None, + (), + ); + let (message, status) = state.on_data_received( + "text/plain".into(), + vec![1], + Some(|mime, data| { + assert_eq!(mime, "text/plain"); + assert_eq!(data, vec![1]); + TestMsg::Data + }), + Option:: TestMsg>::None, + ); + assert!(matches!(message, Some(TestMsg::Data))); + assert_eq!(status, event::Status::Captured); + assert!(state.drag_offer.is_some()); + } + + #[test] + fn finish_only_emits_after_drop() { + let mut state: State<()> = State::new(); + state.on_enter::( + 5.0, + -1.0, + vec![], + Option:: TestMsg>::None, + (), + ); + state.on_action_selected::(DndAction::Move, Option:: TestMsg>::None); + state.on_drop::(Option:: TestMsg>::None); + + let (message, status) = state.on_data_received( + "application/x-test".into(), + vec![7], + Option:: TestMsg>::None, + Some(|mime, data, action, x, y| { + assert_eq!(mime, "application/x-test"); + assert_eq!(data, vec![7]); + assert_eq!(action, DndAction::Move); + assert_eq!(x, 5.0); + assert_eq!(y, -1.0); + TestMsg::Finished + }), + ); + assert!(matches!(message, Some(TestMsg::Finished))); + assert_eq!(status, event::Status::Captured); + assert!(state.drag_offer.is_none()); + } +} diff --git a/src/widget/segmented_button/mod.rs b/src/widget/segmented_button/mod.rs index e609d70b..81c71be8 100644 --- a/src/widget/segmented_button/mod.rs +++ b/src/widget/segmented_button/mod.rs @@ -88,6 +88,19 @@ pub use self::style::{Appearance, ItemAppearance, ItemStatusAppearance, StyleShe pub use self::vertical::{VerticalSegmentedButton, vertical}; pub use self::widget::{Id, SegmentedButton, SegmentedVariant, focus}; +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum InsertPosition { + Before, + After, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub struct ReorderEvent { + pub dragged: Entity, + pub target: Entity, + pub position: InsertPosition, +} + /// Associates extra data with an external secondary map. /// /// The secondary map internally uses a `Vec`, so should only be used for data that diff --git a/src/widget/segmented_button/model/mod.rs b/src/widget/segmented_button/model/mod.rs index 6b5a8a64..e0dd8c54 100644 --- a/src/widget/segmented_button/model/mod.rs +++ b/src/widget/segmented_button/model/mod.rs @@ -11,6 +11,7 @@ mod selection; pub use self::selection::{MultiSelect, Selectable, SingleSelect}; use crate::widget::Icon; +use crate::widget::segmented_button::InsertPosition; use slotmap::{SecondaryMap, SlotMap}; use std::any::{Any, TypeId}; use std::borrow::Cow; @@ -410,6 +411,36 @@ where true } + /// Reorder `dragged` relative to `target` based on the provided position. + /// + /// Returns `true` if the model changed, or `false` if the move was invalid. + pub fn reorder(&mut self, dragged: Entity, target: Entity, position: InsertPosition) -> bool { + if !self.contains_item(dragged) || !self.contains_item(target) || dragged == target { + return false; + } + + let len = self.iter().count(); + let target_pos = self.position(target).map(|pos| pos as usize).unwrap_or(len); + let from_pos = self + .position(dragged) + .map(|pos| pos as usize) + .unwrap_or(target_pos); + let mut insert_pos = match position { + InsertPosition::Before => target_pos, + InsertPosition::After => target_pos.saturating_add(1), + }; + if from_pos < insert_pos { + insert_pos = insert_pos.saturating_sub(1); + } + if len > 0 { + insert_pos = insert_pos.min(len.saturating_sub(1)); + } + + self.position_set(dragged, insert_pos as u16); + self.activate(dragged); + true + } + /// Removes an item from the model. /// /// The generation of the slot for the ID will be incremented, so this ID will no @@ -469,3 +500,43 @@ where self.text.remove(id) } } + +#[cfg(test)] +mod tests { + use super::*; + + fn sample_model() -> (Model, Vec) { + let mut ids = Vec::new(); + let model = Model::builder() + .insert(|b| b.text("Tab1").with_id(|id| ids.push(id))) + .insert(|b| b.text("Tab2").with_id(|id| ids.push(id))) + .insert(|b| b.text("Tab3").with_id(|id| ids.push(id))) + .insert(|b| b.text("Tab4").with_id(|id| ids.push(id))) + .build(); + (model, ids) + } + + fn order_of(model: &Model) -> Vec { + model.iter().collect() + } + + #[test] + fn reorder_inserts_before_target() { + let (mut model, ids) = sample_model(); + assert!(model.reorder(ids[3], ids[1], InsertPosition::Before)); + assert_eq!(order_of(&model), vec![ids[0], ids[3], ids[1], ids[2]]); + } + + #[test] + fn reorder_inserts_after_target() { + let (mut model, ids) = sample_model(); + assert!(model.reorder(ids[0], ids[2], InsertPosition::After)); + assert_eq!(order_of(&model), vec![ids[1], ids[2], ids[0], ids[3]]); + } + + #[test] + fn reorder_rejects_invalid_entities() { + let (mut model, ids) = sample_model(); + assert!(!model.reorder(ids[0], ids[0], InsertPosition::After)); + } +} diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index bb05aa9d..e852a2eb 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -2,6 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 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; @@ -12,7 +13,9 @@ use crate::widget::menu::{ use crate::widget::{Icon, icon}; use crate::{Element, Renderer}; use derive_setters::Setters; -use iced::clipboard::dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent}; +use iced::clipboard::dnd::{ + self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent, SourceEvent, +}; use iced::clipboard::mime::AllowedMimeTypes; use iced::touch::Finger; use iced::{ @@ -41,6 +44,8 @@ thread_local! { static LAST_FOCUS_UPDATE: LazyCell> = LazyCell::new(|| Cell::new(Instant::now())); } +const TAB_REORDER_LOG_TARGET: &str = "libcosmic::widget::tab_reorder"; + /// A command that focuses a segmented item stored in a widget. pub fn focus(id: Id) -> Task { task::effect(Action::Widget(Box::new(operation::focusable::focus(id.0)))) @@ -51,6 +56,27 @@ pub enum ItemBounds { Divider(Rectangle, bool), } +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +enum DropSide { + Before, + After, +} + +impl From for InsertPosition { + fn from(side: DropSide) -> Self { + match side { + DropSide::Before => InsertPosition::Before, + DropSide::After => InsertPosition::After, + } + } +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +struct DropHint { + entity: Entity, + side: DropSide, +} + /// Isolates variant-specific behaviors from [`SegmentedButton`]. pub trait SegmentedVariant { const VERTICAL: bool; @@ -157,6 +183,12 @@ where #[setters(strip_option)] pub(super) drag_id: Option, #[setters(skip)] + pub(super) tab_drag: Option>, + #[setters(skip)] + pub(super) on_drop_hint: Option) -> Message + 'static>>, + #[setters(skip)] + pub(super) on_reorder: Option Message + 'static>>, + #[setters(skip)] /// Defines the implementation of this struct variant: PhantomData, } @@ -204,6 +236,9 @@ where mimes: Vec::new(), variant: PhantomData, drag_id: None, + tab_drag: None, + on_drop_hint: None, + on_reorder: None, } } @@ -261,6 +296,77 @@ where self } + /// Enable drag-and-drop support for tabs using the provided payload builder. + pub fn enable_tab_drag( + mut self, + payload: impl Fn(Entity) -> Option<(String, Vec)> + 'static, + ) -> Self { + self.tab_drag = Some(TabDragSource::new(payload)); + self + } + + /// Receive drop hint updates during drag-and-drop. + pub fn on_drop_hint( + mut self, + callback: impl Fn(Option<(Entity, bool)>) -> Message + 'static, + ) -> Self { + self.on_drop_hint = Some(Box::new(callback)); + self + } + + /// Emit a message when a tab drag is dropped inside this widget. + pub fn on_reorder(mut self, callback: impl Fn(ReorderEvent) -> Message + 'static) -> Self { + self.on_reorder = Some(Box::new(callback)); + self + } + + /// Set the pointer distance threshold before a drag is started. + pub fn tab_drag_threshold(mut self, threshold: f32) -> Self { + if let Some(tab_drag) = self.tab_drag.as_mut() { + tab_drag.threshold = threshold.max(1.0); + } + self + } + + fn reorder_event_for_drop(&self, state: &LocalState, target: Entity) -> Option { + let dragged = state.dragging_tab?; + if dragged == target + || !self.model.contains_item(dragged) + || !self.model.contains_item(target) + { + return None; + } + let position = state + .drop_hint + .filter(|hint| hint.entity == target) + .map(|hint| InsertPosition::from(hint.side)) + .unwrap_or_else(|| self.default_insert_position(dragged, target)); + Some(ReorderEvent { + dragged, + target, + position, + }) + } + + fn default_insert_position(&self, dragged: Entity, target: Entity) -> InsertPosition { + let len = self.model.len(); + let target_pos = self + .model + .position(target) + .map(|pos| pos as usize) + .unwrap_or(len); + let from_pos = self + .model + .position(dragged) + .map(|pos| pos as usize) + .unwrap_or(target_pos); + if from_pos < target_pos { + InsertPosition::After + } else { + InsertPosition::Before + } + } + /// Check if an item is enabled. fn is_enabled(&self, key: Entity) -> bool { self.model.items.get(key).is_some_and(|item| item.enabled) @@ -545,6 +651,101 @@ where state.pressed_item == Some(Item::Tab(key)) } + fn emit_drop_hint(&self, shell: &mut Shell<'_, Message>, hint: Option) { + if let Some(on_hint) = self.on_drop_hint.as_ref() { + let mapped = hint.map(|hint| (hint.entity, matches!(hint.side, DropSide::After))); + shell.publish(on_hint(mapped)); + } + } + + fn drop_hint_for_position( + &self, + state: &LocalState, + bounds: Rectangle, + cursor: Point, + ) -> Option { + let dragging = state.dragging_tab?; + + self.variant_bounds(state, bounds) + .filter_map(|item| match item { + ItemBounds::Button(entity, rect) if rect.contains(cursor) => Some((entity, rect)), + _ => None, + }) + .find_map(|(entity, rect)| { + let before = if Self::VERTICAL { + cursor.y < rect.center_y() + } else { + cursor.x < rect.center_x() + }; + Some(DropHint { + entity, + side: if before { + DropSide::Before + } else { + DropSide::After + }, + }) + }) + } + + fn start_tab_drag( + &self, + state: &mut LocalState, + entity: Entity, + bounds: Rectangle, + cursor: Point, + clipboard: &mut dyn Clipboard, + ) -> bool { + let Some(tab_drag) = self.tab_drag.as_ref() else { + return false; + }; + + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "start_tab_drag requested entity={:?} cursor=({:.2},{:.2}) bounds=({:.2},{:.2},{:.2},{:.2}) threshold={}", + entity, + cursor.x, + cursor.y, + bounds.x, + bounds.y, + bounds.width, + bounds.height, + tab_drag.threshold + ); + + let Some((mime, data)) = (tab_drag.payload)(entity) else { + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "start_tab_drag aborted entity={:?}: payload builder returned None", + entity + ); + return false; + }; + + let data_len = data.len(); + let mime_label = mime.clone(); + + iced_core::clipboard::start_dnd::( + clipboard, + false, + Some(iced_core::clipboard::DndSource::Widget(self.id.0.clone())), + None, + Box::new(SimpleDragData::new(mime, data)), + DndAction::Move, + ); + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "tab drag started entity={:?} mime={} bytes={}", + entity, + mime_label, + data_len + ); + state.dragging_tab = Some(entity); + state.tab_drag_candidate = None; + state.pressed_item = None; + true + } + /// Returns the drag id of the destination. /// /// # Panics @@ -611,6 +812,9 @@ where dnd_state: Default::default(), fingers_pressed: Default::default(), pressed_item: None, + tab_drag_candidate: None, + dragging_tab: None, + drop_hint: None, }) } @@ -701,7 +905,7 @@ where layout: Layout<'_>, cursor_position: mouse::Cursor, _renderer: &Renderer, - _clipboard: &mut dyn Clipboard, + clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, _viewport: &iced::Rectangle, ) -> event::Status { @@ -717,7 +921,26 @@ where .drag_offer .as_ref() .map(|dnd_state| dnd_state.data); + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "segmented button {:?} received DnD event: {:?} entity={entity:?}", + my_id, + e + ); match e { + DndEvent::Source(SourceEvent::Cancelled | SourceEvent::Finished) => { + if state.dragging_tab.take().is_some() { + state.tab_drag_candidate = None; + state.drop_hint = None; + self.emit_drop_hint(shell, state.drop_hint); + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "tab drag source finished id={:?}", + my_id + ); + return event::Status::Captured; + } + } DndEvent::Offer( id, OfferEvent::Enter { @@ -732,6 +955,16 @@ where }) .find(|(_key, bounds)| bounds.contains(Point::new(*x as f32, *y as f32))) .map(|(key, _)| key); + state.drop_hint = self.drop_hint_for_position( + state, + bounds, + Point::new(*x as f32, *y as f32), + ); + self.emit_drop_hint(shell, state.drop_hint); + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "offer enter id={my_id:?} entity={entity:?} @ ({x},{y}) mimes={mime_types:?}" + ); let on_dnd_enter = self.on_dnd_enter @@ -750,15 +983,28 @@ where ); } DndEvent::Offer(id, OfferEvent::LeaveDestination) if Some(my_id) != *id => {} - DndEvent::Offer(id, OfferEvent::Leave | OfferEvent::LeaveDestination) => { + DndEvent::Offer(id, OfferEvent::Leave | OfferEvent::LeaveDestination) + if Some(my_id) == *id => + { + state.drop_hint = None; + self.emit_drop_hint(shell, state.drop_hint); if let Some(Some(entity)) = entity { if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() { shell.publish(on_dnd_leave(entity)); } } + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "offer leave id={my_id:?} entity={entity:?}" + ); _ = state.dnd_state.on_leave::(None); } + DndEvent::Offer(_, OfferEvent::Leave | OfferEvent::LeaveDestination) => {} DndEvent::Offer(id, OfferEvent::Motion { x, y }) if Some(my_id) == *id => { + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "offer motion id={my_id:?} cursor=({x},{y}) current_entity={entity:?}" + ); let new = self .variant_bounds(state, bounds) .filter_map(|item| match item { @@ -775,6 +1021,12 @@ where None:: Message>, Some(new_entity), ); + state.drop_hint = self.drop_hint_for_position( + state, + bounds, + Point::new(*x as f32, *y as f32), + ); + self.emit_drop_hint(shell, state.drop_hint); if Some(Some(new_entity)) != entity { let prev_action = state .dnd_state @@ -792,6 +1044,12 @@ where } } } else if entity.is_some() { + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "offer motion leaving id={my_id:?}" + ); + state.drop_hint = None; + self.emit_drop_hint(shell, state.drop_hint); state.dnd_state.on_motion::( *x, *y, @@ -807,32 +1065,81 @@ where } } DndEvent::Offer(id, OfferEvent::Drop) if Some(my_id) == *id => { + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "offer drop id={my_id:?} entity={entity:?}" + ); _ = state .dnd_state .on_drop::(None:: Message>); } DndEvent::Offer(id, OfferEvent::SelectedAction(action)) if Some(my_id) == *id => { if state.dnd_state.drag_offer.is_some() { + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "offer selected action id={my_id:?} action={action:?} entity={entity:?}" + ); _ = state .dnd_state .on_action_selected::(*action, None:: Message>); } } DndEvent::Offer(id, OfferEvent::Data { data, mime_type }) if Some(my_id) == *id => { - if let Some(Some(entity)) = entity { + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "offer data id={my_id:?} entity={entity:?} mime={mime_type:?}" + ); + let drop_entity = entity + .flatten() + .or_else(|| state.drop_hint.map(|hint| hint.entity)); + let allow_reorder = state + .dnd_state + .drag_offer + .as_ref() + .is_some_and(|offer| offer.selected_action.contains(DndAction::Move)); + let pending_reorder = if allow_reorder && self.on_reorder.is_some() { + drop_entity.and_then(|target| self.reorder_event_for_drop(state, target)) + } else { + None + }; + if let Some(entity) = drop_entity { let on_drop = self.on_dnd_drop.as_ref(); let on_drop = on_drop.map(|on_drop| { |mime, data, action, _, _| on_drop(entity, data, mime, action) }); - if let (Some(msg), ret) = state.dnd_state.on_data_received( + let (maybe_msg, ret) = state.dnd_state.on_data_received( mem::take(mime_type), mem::take(data), None:: Message>, on_drop, - ) { + ); + if let Some(msg) = maybe_msg { + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "publishing drop message entity={entity:?}" + ); shell.publish(msg); - return ret; + } + state.drop_hint = None; + self.emit_drop_hint(shell, state.drop_hint); + if let Some(event) = pending_reorder { + if let Some(on_reorder) = self.on_reorder.as_ref() { + shell.publish(on_reorder(event)); + } + } + return ret; + } else { + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "data received without entity id={my_id:?}" + ); + state.drop_hint = None; + self.emit_drop_hint(shell, state.drop_hint); + if let Some(event) = pending_reorder { + if let Some(on_reorder) = self.on_reorder.as_ref() { + shell.publish(on_reorder(event)); + } } } } @@ -897,12 +1204,16 @@ where // Record that the mouse is hovering over this button. state.hovered = Item::Tab(key); + let close_button_bounds = + close_bounds(bounds, f32::from(self.close_icon.size)); + let over_close_button = self.model.items[key].closable + && cursor_position.is_over(close_button_bounds); + // If marked as closable, show a close icon. if self.model.items[key].closable { // Emit close message if the close button is pressed. if let Some(on_close) = self.on_close.as_ref() { - if cursor_position - .is_over(close_bounds(bounds, f32::from(self.close_icon.size))) + if over_close_button && (left_button_released(&event) || (touch_lifted(&event) && fingers_pressed == 1)) { @@ -927,6 +1238,36 @@ where } } + if self.tab_drag.is_some() + && matches!( + event, + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) + ) + && !over_close_button + { + 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 + ); + } + } + } + if is_lifted(&event) { state.unfocus(); } @@ -1046,6 +1387,42 @@ where state.pressed_item = None; } + if let (Some(tab_drag), Some(candidate)) = + (self.tab_drag.as_ref(), state.tab_drag_candidate) + { + 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, + ) { + return event::Status::Captured; + } + } + } + } + } + } + + if matches!( + event, + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) + ) { + state.tab_drag_candidate = None; + } + if state.is_focused() { if let Event::Keyboard(keyboard::Event::KeyPressed { key: keyboard::Key::Named(keyboard::key::Named::Tab), @@ -1120,6 +1497,7 @@ where ) { let state = tree.state.downcast_mut::(); operation.focusable(state, Some(&self.id.0)); + operation.custom(state, Some(&self.id.0)); if let Item::Set = state.focused_item { if self.prev_tab_sensitive(state) { @@ -1180,6 +1558,12 @@ where let appearance = Self::variant_appearance(theme, &self.style); let bounds: Rectangle = layout.bounds(); let button_amount = self.model.items.len(); + let show_drop_hint = state.dragging_tab.is_some(); + let drop_hint = if show_drop_hint { + state.drop_hint + } else { + None + }; // Draw the background, if a background was defined. if let Some(background) = appearance.background { @@ -1305,6 +1689,8 @@ where // Draw each of the items in the widget. let mut nth = 0; + let drop_hint_marker = drop_hint; + let show_drop_hint_marker = show_drop_hint; self.variant_bounds(state, bounds).for_each(move |item| { let (key, mut bounds) = match item { // Draw a button @@ -1332,8 +1718,27 @@ where } }; + let original_bounds = bounds; let center_y = bounds.center_y(); + if show_drop_hint_marker { + if 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, + ); + } + } + let menu_open = || { state.show_context == Some(key) && !tree.children.is_empty() @@ -1398,7 +1803,6 @@ where ); } - let original_bounds = bounds; bounds.x += f32::from(self.button_padding[0]); bounds.width -= f32::from(self.button_padding[0]) - f32::from(self.button_padding[2]); let mut indent_padding = 0.0; @@ -1595,6 +1999,24 @@ where ); } + if show_drop_hint_marker { + if matches!( + drop_hint_marker, + Some(DropHint { + entity, + side: DropSide::After + }) if entity == key + ) { + draw_drop_indicator( + renderer, + original_bounds, + DropSide::After, + Self::VERTICAL, + appearance.active.text_color, + ); + } + } + nth += 1; }); } @@ -1658,27 +2080,68 @@ where fn drag_destinations( &self, - _state: &Tree, + tree: &Tree, layout: Layout<'_>, _renderer: &Renderer, dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, ) { - let bounds = layout.bounds(); - + let local_state = tree.state.downcast_ref::(); let my_id = self.get_drag_id(); - let dnd_rect = DndDestinationRectangle { - id: my_id, - rectangle: dnd::Rectangle { - x: f64::from(bounds.x), - y: f64::from(bounds.y), - width: f64::from(bounds.width), - height: f64::from(bounds.height), - }, - mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(), - actions: DndAction::Copy | DndAction::Move, - preferred: DndAction::Move, - }; - dnd_rectangles.push(dnd_rect); + let mut pushed = false; + + for item in self.variant_bounds(local_state, layout.bounds()) { + if let ItemBounds::Button(_entity, rect) = item { + pushed = true; + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}", + my_id, + rect.x, + rect.y, + rect.width, + rect.height, + self.mimes + ); + dnd_rectangles.push(DndDestinationRectangle { + id: my_id, + rectangle: dnd::Rectangle { + x: f64::from(rect.x), + y: f64::from(rect.y), + width: f64::from(rect.width), + height: f64::from(rect.height), + }, + mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(), + actions: DndAction::Copy | DndAction::Move, + preferred: DndAction::Move, + }); + } + } + + if !pushed { + let bounds = layout.bounds(); + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}", + my_id, + bounds.x, + bounds.y, + bounds.width, + bounds.height, + self.mimes + ); + dnd_rectangles.push(DndDestinationRectangle { + id: my_id, + rectangle: dnd::Rectangle { + x: f64::from(bounds.x), + y: f64::from(bounds.y), + width: f64::from(bounds.width), + height: f64::from(bounds.height), + }, + mime_types: self.mimes.clone().into_iter().map(Cow::Owned).collect(), + actions: DndAction::Copy | DndAction::Move, + preferred: DndAction::Move, + }); + } } } @@ -1700,6 +2163,54 @@ where } } +struct TabDragSource { + payload: Box Option<(String, Vec)>>, + threshold: f32, + _marker: PhantomData, +} + +impl TabDragSource { + fn new(payload: impl Fn(Entity) -> Option<(String, Vec)> + 'static) -> Self { + Self { + payload: Box::new(payload), + threshold: 8.0, + _marker: PhantomData, + } + } +} + +struct SimpleDragData { + mime: String, + bytes: Vec, +} + +impl SimpleDragData { + fn new(mime: String, bytes: Vec) -> Self { + Self { mime, bytes } + } +} + +impl iced::clipboard::mime::AsMimeTypes for SimpleDragData { + fn available(&self) -> Cow<'static, [String]> { + Cow::Owned(vec![self.mime.clone()]) + } + + fn as_bytes(&self, mime_type: &str) -> Option> { + if mime_type == self.mime { + Some(Cow::Owned(self.bytes.clone())) + } else { + None + } + } +} + +#[derive(Clone, Copy)] +struct TabDragCandidate { + entity: Entity, + bounds: Rectangle, + origin: Point, +} + #[derive(Debug, Clone, Copy)] struct Focus { updated_at: Instant, @@ -1746,6 +2257,12 @@ pub struct LocalState { fingers_pressed: HashSet, /// The currently pressed item pressed_item: Option, + /// Pending tab drag candidate data + tab_drag_candidate: Option, + /// Currently dragging tab entity + dragging_tab: Option, + /// Current drop hint for drag-and-drop indicator + drop_hint: Option, } #[derive(Debug, Default, PartialEq)] @@ -1770,6 +2287,143 @@ impl LocalState { } } +#[cfg(test)] +mod tests { + use super::*; + use crate::widget::segmented_button::{self, Appearance as SegAppearance}; + use iced::Size; + use slotmap::SecondaryMap; + use std::collections::HashSet; + + #[derive(Clone, Debug)] + enum TestMessage {} + + struct TestVariant; + + impl SegmentedVariant + for SegmentedButton<'_, TestVariant, SelectionMode, Message> + where + Model: Selectable, + SelectionMode: Default, + { + const VERTICAL: bool = false; + + fn variant_appearance( + _theme: &crate::Theme, + _style: &crate::theme::SegmentedButton, + ) -> SegAppearance { + SegAppearance::default() + } + + fn variant_bounds<'b>( + &'b self, + _state: &'b LocalState, + bounds: Rectangle, + ) -> Box + 'b> { + let len = self.model.order.len(); + if len == 0 { + return Box::new(std::iter::empty()); + } + let width = bounds.width / len as f32; + Box::new( + self.model + .order + .iter() + .copied() + .enumerate() + .map(move |(idx, entity)| { + let rect = Rectangle { + x: bounds.x + (idx as f32) * width, + y: bounds.y, + width, + height: bounds.height, + }; + ItemBounds::Button(entity, rect) + }), + ) + } + + fn variant_layout( + &self, + _state: &mut LocalState, + _renderer: &crate::Renderer, + _limits: &layout::Limits, + ) -> Size { + Size::ZERO + } + } + + fn sample_model() -> ( + segmented_button::SingleSelectModel, + Vec, + ) { + let mut entities = Vec::new(); + let model = segmented_button::Model::builder() + .insert(|b| b.text("One").with_id(|id| entities.push(id))) + .insert(|b| b.text("Two").with_id(|id| entities.push(id))) + .insert(|b| b.text("Three").with_id(|id| entities.push(id))) + .build(); + (model, entities) + } + + fn test_state(dragging: segmented_button::Entity, len: usize) -> LocalState { + let mut state = LocalState { + menu_state: MenuBarState::default(), + paragraphs: SecondaryMap::new(), + text_hashes: SecondaryMap::new(), + buttons_visible: 0, + buttons_offset: 0, + collapsed: false, + focused: None, + focused_item: Item::default(), + focused_visible: false, + hovered: Item::default(), + known_length: 0, + middle_clicked: None, + internal_layout: Vec::new(), + context_cursor: Point::ORIGIN, + show_context: None, + wheel_timestamp: None, + dnd_state: crate::widget::dnd_destination::State::>::new(), + fingers_pressed: HashSet::new(), + pressed_item: None, + tab_drag_candidate: None, + dragging_tab: Some(dragging), + drop_hint: None, + }; + state.buttons_visible = len; + state.known_length = len; + state + } + + #[test] + fn drop_hint_reports_before_and_after() { + let (model, ids) = sample_model(); + let button = + SegmentedButton::::new( + &model, + ); + let state = test_state(ids[0], model.order.len()); + let bounds = Rectangle { + x: 0.0, + y: 0.0, + width: 300.0, + height: 30.0, + }; + let before = button + .drop_hint_for_position(&state, bounds, Point::new(10.0, 15.0)) + .expect("hint"); + assert_eq!(before.entity, ids[0]); + assert!(matches!(before.side, DropSide::Before)); + + let after = button + .drop_hint_for_position(&state, bounds, Point::new(290.0, 15.0)) + .expect("hint"); + assert_eq!(after.entity, ids[2]); + assert!(matches!(after.side, DropSide::After)); + } +} + impl operation::Focusable for LocalState { fn is_focused(&self) -> bool { self.focused @@ -1882,6 +2536,53 @@ fn draw_icon( ); } +fn draw_drop_indicator( + renderer: &mut Renderer, + bounds: Rectangle, + side: DropSide, + vertical: bool, + color: Color, +) { + let thickness = 4.0; + let quad_bounds = if vertical { + let y = match side { + DropSide::Before => bounds.y - thickness / 2.0, + DropSide::After => bounds.y + bounds.height - thickness / 2.0, + }; + + Rectangle { + x: bounds.x, + y, + width: bounds.width, + height: thickness, + } + } else { + let x = match side { + DropSide::Before => bounds.x - thickness / 2.0, + DropSide::After => bounds.x + bounds.width - thickness / 2.0, + }; + + Rectangle { + x, + y: bounds.y, + width: thickness, + height: bounds.height, + } + }; + + renderer.fill_quad( + renderer::Quad { + bounds: quad_bounds, + border: Border { + radius: 2.0.into(), + ..Default::default() + }, + shadow: Shadow::default(), + }, + Background::Color(color), + ); +} + fn left_button_released(event: &Event) -> bool { matches!( event, From ce0868582b3cc09d52de917fef91f50d439fb2a9 Mon Sep 17 00:00:00 2001 From: Stephan Buys Date: Thu, 20 Nov 2025 11:23:49 +0200 Subject: [PATCH 164/352] tests: fix env guard and pipe read for tab dnd --- src/desktop.rs | 11 +++++++---- src/process.rs | 30 ++++++++++++++++++++---------- 2 files changed, 27 insertions(+), 14 deletions(-) diff --git a/src/desktop.rs b/src/desktop.rs index 82242460..01698af5 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -881,7 +881,9 @@ mod tests { impl EnvVarGuard { fn set(key: &'static str, value: &Path) -> Self { let original = env::var(key).ok(); - std::env::set_var(key, value); + // std::env::{set_var, remove_var} are unsafe on newer toolchains; + // we limit scope here to the test helper that toggles a single key. + unsafe { std::env::set_var(key, value) }; Self { key, original } } } @@ -889,9 +891,9 @@ mod tests { impl Drop for EnvVarGuard { fn drop(&mut self) { if let Some(ref original) = self.original { - std::env::set_var(self.key, original); + unsafe { std::env::set_var(self.key, original) }; } else { - std::env::remove_var(self.key); + unsafe { std::env::remove_var(self.key) }; } } } @@ -1108,7 +1110,8 @@ Icon=vmware-workstation\n\ let resolved = resolve_desktop_entry(&mut cache, &ctx, &DesktopResolveOptions::default()); assert!(resolved.icon().is_some()); assert!(resolved.exec().is_some()); - assert_eq!(resolved.startup_wm_class(), Some(&format!("crx_{}", id))); + let expected = format!("crx_{}", id); + assert_eq!(resolved.startup_wm_class(), Some(expected.as_str())); } #[test] diff --git a/src/process.rs b/src/process.rs index 1ad048dc..2b6c4e0e 100644 --- a/src/process.rs +++ b/src/process.rs @@ -9,18 +9,28 @@ use std::process::{Command, Stdio, exit}; #[cfg(feature = "tokio")] use tokio::io::AsyncReadExt; -#[cfg(feature = "tokio")] async fn read_from_pipe(read: OwnedFd) -> Option { - let mut read = tokio::net::unix::pipe::Receiver::from_owned_fd(read).unwrap(); - read.read_u32().await.ok() -} + #[cfg(feature = "tokio")] + { + let mut read = tokio::net::unix::pipe::Receiver::from_owned_fd(read).unwrap(); + return read.read_u32().await.ok(); + } -#[cfg(all(feature = "smol", not(feature = "tokio")))] -async fn read_from_pipe(read: OwnedFd) -> Option { - let mut read = smol::Async::new(std::fs::File::from(read)).unwrap(); - let mut bytes = [0; 4]; - read.read_exact(&mut bytes).await.ok()?; - Some(u32::from_be_bytes(bytes)) + #[cfg(all(feature = "smol", not(feature = "tokio")))] + { + let mut read = smol::Async::new(std::fs::File::from(read)).unwrap(); + let mut bytes = [0; 4]; + read.read_exact(&mut bytes).await.ok()?; + return Some(u32::from_be_bytes(bytes)); + } + + #[cfg(not(any(feature = "tokio", feature = "smol")))] + { + use rustix::fd::AsFd; + let mut bytes = [0u8; 4]; + rustix::io::read(&read, &mut bytes).ok()?; + return Some(u32::from_be_bytes(bytes)); + } } /// Performs a double fork with setsid to spawn and detach a command. From 639326fcc31a95a0c7a9a5bb56f1ed4d53530f26 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 11 Nov 2025 23:02:57 +0100 Subject: [PATCH 165/352] feat(icon): optimize & bundle icons with crabtime for non-unix platforms --- .gitmodules | 3 ++ Cargo.toml | 7 ++- cosmic-icons | 1 + res/icons/close-menu-symbolic.svg | 4 -- res/icons/go-next-symbolic.svg | 3 -- res/icons/go-previous-symbolic.svg | 3 -- res/icons/list-add-symbolic.svg | 3 -- res/icons/list-remove-symbolic.svg | 3 -- res/icons/navbar-closed-symbolic.svg | 10 ----- res/icons/navbar-open-symbolic.svg | 8 ---- res/icons/open-menu-symbolic.svg | 3 -- res/icons/window-close-symbolic.svg | 3 -- res/icons/window-maximize-symbolic.svg | 4 -- res/icons/window-minimize-symbolic.svg | 3 -- res/icons/window-restore-symbolic.svg | 4 -- src/widget/button/icon.rs | 4 -- src/widget/button/text.rs | 22 ++++----- src/widget/dropdown/multi/widget.rs | 4 -- src/widget/dropdown/widget.rs | 4 -- src/widget/header_bar.rs | 13 ------ src/widget/icon/bundle.rs | 62 ++++++++++++++++++++++++++ src/widget/icon/handle.rs | 6 +-- src/widget/icon/mod.rs | 55 ++--------------------- src/widget/icon/named.rs | 16 ++++++- src/widget/nav_bar_toggle.rs | 12 ++--- src/widget/spin_button.rs | 48 ++++++++++---------- src/widget/warning.rs | 9 ---- 27 files changed, 128 insertions(+), 189 deletions(-) create mode 160000 cosmic-icons delete mode 100644 res/icons/close-menu-symbolic.svg delete mode 100644 res/icons/go-next-symbolic.svg delete mode 100644 res/icons/go-previous-symbolic.svg delete mode 100644 res/icons/list-add-symbolic.svg delete mode 100644 res/icons/list-remove-symbolic.svg delete mode 100644 res/icons/navbar-closed-symbolic.svg delete mode 100644 res/icons/navbar-open-symbolic.svg delete mode 100644 res/icons/open-menu-symbolic.svg delete mode 100644 res/icons/window-close-symbolic.svg delete mode 100644 res/icons/window-maximize-symbolic.svg delete mode 100644 res/icons/window-minimize-symbolic.svg delete mode 100644 res/icons/window-restore-symbolic.svg create mode 100644 src/widget/icon/bundle.rs diff --git a/.gitmodules b/.gitmodules index 367f7f22..fdaf8abe 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,3 +2,6 @@ path = iced url = https://github.com/pop-os/iced.git branch = master +[submodule "icon-theme"] + path = cosmic-icons + url = https://github.com/pop-os/cosmic-icons diff --git a/Cargo.toml b/Cargo.toml index 430af23d..4d742126 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,6 +107,8 @@ cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-c chrono = "0.4.42" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } +# Compile-time generation of code +crabtime = "1.1.4" # Internationalization i18n-embed = { version = "0.16.0", features = [ "fluent-system", @@ -152,6 +154,10 @@ freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://githu freedesktop-desktop-entry = { version = "0.7.14", optional = true } shlex = { version = "1.3.0", optional = true } +[target.'cfg(not(unix))'.dependencies] +# Used to embed bundled icons for non-unix platforms. +phf = { version = "0.13.1", features = ["macros"] } + [dependencies.cosmic-theme] path = "cosmic-theme" @@ -222,4 +228,3 @@ dirs = "6.0.0" [dev-dependencies] tempfile = "3.13.0" - diff --git a/cosmic-icons b/cosmic-icons new file mode 160000 index 00000000..70b07582 --- /dev/null +++ b/cosmic-icons @@ -0,0 +1 @@ +Subproject commit 70b07582e24ec2114672256b9657ca80670bca8a diff --git a/res/icons/close-menu-symbolic.svg b/res/icons/close-menu-symbolic.svg deleted file mode 100644 index caf00d31..00000000 --- a/res/icons/close-menu-symbolic.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/icons/go-next-symbolic.svg b/res/icons/go-next-symbolic.svg deleted file mode 100644 index 3aed3717..00000000 --- a/res/icons/go-next-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/icons/go-previous-symbolic.svg b/res/icons/go-previous-symbolic.svg deleted file mode 100644 index 4957cffd..00000000 --- a/res/icons/go-previous-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/icons/list-add-symbolic.svg b/res/icons/list-add-symbolic.svg deleted file mode 100644 index 59b2fb03..00000000 --- a/res/icons/list-add-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/icons/list-remove-symbolic.svg b/res/icons/list-remove-symbolic.svg deleted file mode 100644 index 5b9ded7c..00000000 --- a/res/icons/list-remove-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/icons/navbar-closed-symbolic.svg b/res/icons/navbar-closed-symbolic.svg deleted file mode 100644 index 46f35e16..00000000 --- a/res/icons/navbar-closed-symbolic.svg +++ /dev/null @@ -1,10 +0,0 @@ - - - - - - - - - - diff --git a/res/icons/navbar-open-symbolic.svg b/res/icons/navbar-open-symbolic.svg deleted file mode 100644 index c1f32161..00000000 --- a/res/icons/navbar-open-symbolic.svg +++ /dev/null @@ -1,8 +0,0 @@ - - - - - - - - diff --git a/res/icons/open-menu-symbolic.svg b/res/icons/open-menu-symbolic.svg deleted file mode 100644 index efae2a2f..00000000 --- a/res/icons/open-menu-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/icons/window-close-symbolic.svg b/res/icons/window-close-symbolic.svg deleted file mode 100644 index 25336395..00000000 --- a/res/icons/window-close-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/icons/window-maximize-symbolic.svg b/res/icons/window-maximize-symbolic.svg deleted file mode 100644 index ef66334e..00000000 --- a/res/icons/window-maximize-symbolic.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/res/icons/window-minimize-symbolic.svg b/res/icons/window-minimize-symbolic.svg deleted file mode 100644 index fdcf99b4..00000000 --- a/res/icons/window-minimize-symbolic.svg +++ /dev/null @@ -1,3 +0,0 @@ - - - diff --git a/res/icons/window-restore-symbolic.svg b/res/icons/window-restore-symbolic.svg deleted file mode 100644 index bcb506f5..00000000 --- a/res/icons/window-restore-symbolic.svg +++ /dev/null @@ -1,4 +0,0 @@ - - - - diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs index 0bb3c84d..754bc433 100644 --- a/src/widget/button/icon.rs +++ b/src/widget/button/icon.rs @@ -132,10 +132,6 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes fn from(mut builder: Button<'a, Message>) -> Element<'a, Message> { let mut content = Vec::with_capacity(2); - if let icon::Data::Name(ref mut named) = builder.variant.handle.data { - named.size = Some(builder.icon_size); - } - content.push( crate::widget::icon(builder.variant.handle.clone()) .size(builder.icon_size) diff --git a/src/widget/button/text.rs b/src/widget/button/text.rs index e5dea9f3..3f58c932 100644 --- a/src/widget/button/text.rs +++ b/src/widget/button/text.rs @@ -91,21 +91,15 @@ impl Button<'_, Message> { impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { fn from(mut builder: Button<'a, Message>) -> Element<'a, Message> { - let trailing_icon = builder.variant.trailing_icon.map(|mut i| { - if let icon::Data::Name(ref mut named) = i.data { - named.size = Some(builder.icon_size); - } + let trailing_icon = builder + .variant + .trailing_icon + .map(crate::widget::icon::Handle::icon); - i.icon() - }); - - let leading_icon = builder.variant.leading_icon.map(|mut i| { - if let icon::Data::Name(ref mut named) = i.data { - named.size = Some(builder.icon_size); - } - - i.icon() - }); + let leading_icon = builder + .variant + .leading_icon + .map(crate::widget::icon::Handle::icon); let label: Option> = (!builder.label.is_empty()).then(|| { let font = crate::font::Font { diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index 79b1a6b7..458cf5e6 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -230,10 +230,6 @@ impl State { pub fn new() -> Self { Self { icon: match icon::from_name("pan-down-symbolic").size(16).handle().data { - icon::Data::Name(named) => named - .path() - .filter(|path| path.extension().is_some_and(|ext| ext == OsStr::new("svg"))) - .map(iced_core::svg::Handle::from_path), icon::Data::Svg(handle) => Some(handle), icon::Data::Image(_) => None, }, diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index a5612ccd..d4a9bc87 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -407,10 +407,6 @@ impl State { pub fn new() -> Self { Self { icon: match icon::from_name("pan-down-symbolic").size(16).handle().data { - icon::Data::Name(named) => named - .path() - .filter(|path| path.extension().is_some_and(|ext| ext == OsStr::new("svg"))) - .map(iced_core::svg::Handle::from_path), icon::Data::Svg(handle) => Some(handle), icon::Data::Image(_) => None, }, diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 01a8d559..d500bde3 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -449,25 +449,12 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { fn window_controls(&mut self) -> Element<'a, Message> { macro_rules! icon { ($name:expr, $size:expr, $on_press:expr) => {{ - #[cfg(target_os = "linux")] let icon = { widget::icon::from_name($name) .apply(widget::button::icon) .padding(8) }; - #[cfg(not(target_os = "linux"))] - let icon = { - widget::icon::from_svg_bytes(include_bytes!(concat!( - "../../res/icons/", - $name, - ".svg" - ))) - .symbolic(true) - .apply(widget::button::icon) - .padding(8) - }; - icon.class(crate::theme::Button::HeaderBar) .selected(self.focused) .icon_size($size) diff --git a/src/widget/icon/bundle.rs b/src/widget/icon/bundle.rs new file mode 100644 index 00000000..0e1fdc16 --- /dev/null +++ b/src/widget/icon/bundle.rs @@ -0,0 +1,62 @@ +// Copyright 2025 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Embedded icons for platforms which do not support icon themes yet. + +/// Icon bundling is not enabled on unix platforms. +pub fn get(icon_name: &str) -> Option { + None +} + +#[cfg(not(unix))] +/// Get a bundled icon on non-unix platforms. +pub fn get(icon_name: &str) -> Option { + ICONS + .get(icon_name) + .map(|bytes| super::Data::Svg(crate::iced::widget::svg::Handle::from_memory(*bytes))) +} + +#[cfg(not(unix))] +#[crabtime::expression] +fn comptime_icon_bundler() -> String { + let manifest_dir = std::path::Path::new(crabtime::WORKSPACE_PATH); + let icon_paths = [ + "cosmic-icons/freedesktop/scalable", + "cosmic-icons/extra/scalable", + ]; + + let key_value_assignments = icon_paths + .into_iter() + .map(|path| manifest_dir.join(path)) + .inspect(|icon_path| assert!(icon_path.exists(), "path = {icon_path:?}")) + .map(|icon_path| std::fs::read_dir(icon_path).unwrap()) + .flat_map(|dir| { + dir.flat_map(|entry| entry.unwrap().path().read_dir().unwrap()) + .map(|entry| { + let entry = entry.unwrap(); + let path = entry.path().canonicalize().unwrap(); + let file_name = path.file_stem().unwrap().to_str().unwrap().to_owned(); + let path = path.into_os_string().into_string().unwrap(); + (file_name, path) + }) + }) + .fold( + std::collections::BTreeMap::new(), + |mut set, (name, path)| { + set.insert(name, path); + set + }, + ) + .into_iter() + .fold(String::new(), |mut output, (name, path)| { + output.push_str(&format!(" \"{name}\" => include_bytes!(\"{path}\"),\n")); + output + }); + + ["phf::phf_map!(\n", &key_value_assignments, ")"].concat() +} + +#[cfg(not(unix))] +static ICONS: phf::Map<&'static str, &'static [u8]> = { + comptime_icon_bundler! {} +}; diff --git a/src/widget/icon/handle.rs b/src/widget/icon/handle.rs index 1fa2d85f..a4ddd364 100644 --- a/src/widget/icon/handle.rs +++ b/src/widget/icon/handle.rs @@ -1,7 +1,7 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -use super::{Icon, Named}; +use super::Icon; use crate::widget::{image, svg}; use std::borrow::Cow; use std::ffi::OsStr; @@ -26,7 +26,7 @@ impl Handle { #[must_use] #[derive(Clone, Debug, Hash)] pub enum Data { - Name(Named), + // Name(Named), Image(image::Handle), Svg(svg::Handle), } @@ -94,7 +94,7 @@ pub fn from_raster_pixels( /// Create a SVG handle from memory. pub fn from_svg_bytes(bytes: impl Into>) -> Handle { Handle { - symbolic: false, + symbolic: true, data: Data::Svg(svg::Handle::from_memory(bytes)), } } diff --git a/src/widget/icon/mod.rs b/src/widget/icon/mod.rs index 20e8bf25..6c6a9f08 100644 --- a/src/widget/icon/mod.rs +++ b/src/widget/icon/mod.rs @@ -3,8 +3,8 @@ //! Lazily-generated SVG icon widget for Iced. +mod bundle; mod named; -use std::ffi::OsStr; use std::sync::Arc; pub use named::{IconFallback, Named}; @@ -58,14 +58,6 @@ impl Icon { #[must_use] pub fn into_svg_handle(self) -> Option { match self.handle.data { - Data::Name(named) => { - if let Some(path) = named.path() { - if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) { - return Some(iced_core::svg::Handle::from_path(path)); - } - } - } - Data::Image(_) => (), Data::Svg(handle) => return Some(handle), } @@ -76,12 +68,6 @@ impl Icon { #[must_use] pub fn size(mut self, size: u16) -> Self { self.size = size; - // ensures correct icon size variant selection - if let Data::Name(named) = &self.handle.data { - let mut new_named = named.clone(); - new_named.size = Some(size); - self.handle = new_named.handle(); - } self } @@ -120,19 +106,6 @@ impl Icon { }; match self.handle.data { - Data::Name(named) => { - if let Some(path) = named.path() { - if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) { - from_svg(iced_core::svg::Handle::from_path(path)) - } else { - from_image(iced_core::image::Handle::from_path(path)) - } - } else { - let bytes: &'static [u8] = &[]; - from_svg(iced_core::svg::Handle::from_memory(bytes)) - } - } - Data::Image(handle) => from_image(handle), Data::Svg(handle) => from_svg(handle), } @@ -147,32 +120,14 @@ impl<'a, Message: 'a> From for Element<'a, Message> { /// Draw an icon in the given bounds via the runtime's renderer. pub fn draw(renderer: &mut crate::Renderer, handle: &Handle, icon_bounds: Rectangle) { - enum IcedHandle { - Svg(iced_core::svg::Handle), - Image(iced_core::image::Handle), - } - - let iced_handle = match handle.clone().data { - Data::Name(named) => named.path().map(|path| { - if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) { - IcedHandle::Svg(iced_core::svg::Handle::from_path(path)) - } else { - IcedHandle::Image(iced_core::image::Handle::from_path(path)) - } - }), - - Data::Image(handle) => Some(IcedHandle::Image(handle)), - Data::Svg(handle) => Some(IcedHandle::Svg(handle)), - }; - - match iced_handle { - Some(IcedHandle::Svg(handle)) => iced_core::svg::Renderer::draw_svg( + match handle.clone().data { + Data::Svg(handle) => iced_core::svg::Renderer::draw_svg( renderer, iced_core::svg::Svg::new(handle), icon_bounds, ), - Some(IcedHandle::Image(handle)) => { + Data::Image(handle) => { iced_core::image::Renderer::draw_image( renderer, handle, @@ -183,7 +138,5 @@ pub fn draw(renderer: &mut crate::Renderer, handle: &Handle, icon_bounds: Rectan [0.0; 4], ); } - - None => {} } } diff --git a/src/widget/icon/named.rs b/src/widget/icon/named.rs index e1c53500..8405e080 100644 --- a/src/widget/icon/named.rs +++ b/src/widget/icon/named.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 use super::{Handle, Icon}; -use std::{borrow::Cow, path::PathBuf, sync::Arc}; +use std::{borrow::Cow, ffi::OsStr, path::PathBuf, sync::Arc}; #[derive(Debug, Clone, Default, Hash)] /// Fallback icon to use if the icon was not found. @@ -116,9 +116,21 @@ impl Named { #[inline] pub fn handle(self) -> Handle { + let name = self.name.clone(); Handle { symbolic: self.symbolic, - data: super::Data::Name(self), + data: if let Some(path) = self.path() { + if path.extension().is_some_and(|ext| ext == OsStr::new("svg")) { + super::Data::Svg(iced_core::svg::Handle::from_path(path)) + } else { + super::Data::Image(iced_core::image::Handle::from_path(path)) + } + } else { + super::bundle::get(&name).unwrap_or_else(|| { + let bytes: &'static [u8] = &[]; + super::Data::Svg(iced_core::svg::Handle::from_memory(bytes)) + }) + }, } } diff --git a/src/widget/nav_bar_toggle.rs b/src/widget/nav_bar_toggle.rs index 23495e3b..b0849dd2 100644 --- a/src/widget/nav_bar_toggle.rs +++ b/src/widget/nav_bar_toggle.rs @@ -28,18 +28,12 @@ pub const fn nav_bar_toggle() -> NavBarToggle { impl From> for Element<'_, Message> { fn from(nav_bar_toggle: NavBarToggle) -> Self { let icon = if nav_bar_toggle.active { - widget::icon::from_svg_bytes( - &include_bytes!("../../res/icons/navbar-open-symbolic.svg")[..], - ) - .symbolic(true) + "navbar-open-symbolic" } else { - widget::icon::from_svg_bytes( - &include_bytes!("../../res/icons/navbar-closed-symbolic.svg")[..], - ) - .symbolic(true) + "navbar-closed-symbolic" }; - widget::button::icon(icon) + widget::button::icon(widget::icon::from_name(icon)) .padding([8, 16]) .on_press_maybe(nav_bar_toggle.on_toggle) .selected(nav_bar_toggle.selected) diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index a93f2ee4..6f4a4de2 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -115,7 +115,7 @@ where } } -fn increment(value: T, step: T, min: T, max: T) -> T +fn increment(value: T, step: T, _min: T, max: T) -> T where T: Copy + Sub + Add + PartialOrd, { @@ -126,7 +126,7 @@ where } } -fn decrement(value: T, step: T, min: T, max: T) -> T +fn decrement(value: T, step: T, min: T, _max: T) -> T where T: Copy + Sub + Add + PartialOrd, { @@ -149,25 +149,25 @@ where } } } -macro_rules! make_button { - ($spin_button:expr, $icon:expr, $operation:expr) => {{ - #[cfg(target_os = "linux")] - let button = icon::from_name($icon); - #[cfg(not(target_os = "linux"))] - let button = - icon::from_svg_bytes(include_bytes!(concat!["../../res/icons/", $icon, ".svg"])) - .symbolic(true); - - button - .apply(button::icon) - .on_press(($spin_button.on_press)($operation( - $spin_button.value, - $spin_button.step, - $spin_button.min, - $spin_button.max, - ))) - }}; +fn make_button<'a, T, Message>( + spin_button: &SpinButton<'a, T, Message>, + icon: &'static str, + operation: fn(T, T, T, T) -> T, +) -> Element<'a, Message> +where + Message: Clone + 'static, + T: Copy + Sub + Add + PartialOrd, +{ + icon::from_name(icon) + .apply(button::icon) + .on_press((spin_button.on_press)(operation( + spin_button.value, + spin_button.step, + spin_button.min, + spin_button.max, + ))) + .into() } fn horizontal_variant(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message> @@ -175,8 +175,8 @@ where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, { - let decrement_button = make_button!(spin_button, "list-remove-symbolic", decrement); - let increment_button = make_button!(spin_button, "list-add-symbolic", increment); + let decrement_button = make_button(&spin_button, "list-remove-symbolic", decrement); + let increment_button = make_button(&spin_button, "list-add-symbolic", increment); let label = text::body(spin_button.label) .apply(container) @@ -198,8 +198,8 @@ where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, { - let decrement_button = make_button!(spin_button, "list-remove-symbolic", decrement); - let increment_button = make_button!(spin_button, "list-add-symbolic", increment); + let decrement_button = make_button(&spin_button, "list-remove-symbolic", decrement); + let increment_button = make_button(&spin_button, "list-add-symbolic", increment); let label = text::body(spin_button.label) .apply(container) diff --git a/src/widget/warning.rs b/src/widget/warning.rs index 3e3a1ad4..942ffb8b 100644 --- a/src/widget/warning.rs +++ b/src/widget/warning.rs @@ -33,20 +33,11 @@ impl<'a, Message: 'static + Clone> Warning<'a, Message> { pub fn into_widget(self) -> widget::Container<'a, Message, crate::Theme, Renderer> { let label = widget::container(crate::widget::text(self.message)).width(Length::Fill); - #[cfg(target_os = "linux")] let close_button = icon::from_name("window-close-symbolic") .size(16) .apply(widget::button::icon) .on_press_maybe(self.on_close); - #[cfg(not(target_os = "linux"))] - let close_button = - icon::from_svg_bytes(include_bytes!("../../res/icons/window-close-symbolic.svg")) - .symbolic(true) - .apply(widget::button::icon) - .icon_size(16) - .on_press_maybe(self.on_close); - widget::row::with_capacity(2) .push(label) .push(close_button) From 62f661e077a11090f35ff8ace026e4387ab8c85e Mon Sep 17 00:00:00 2001 From: Kyle Scheuing Date: Wed, 26 Nov 2025 01:25:27 -0500 Subject: [PATCH 166/352] fix: compile errors on windows calendar.rs had some left over icon! macro_rules macros referencing now deleted files. bundle::get was defined twice on non-unix platforms. A known remaining issue is that projects using libcosmic need to have cosmic-icons in their project root, since the crabtime macro uses crabtime::WORKSPACE_PATH rather than the path to wherever cargo puts libcosmic's git submodule. See: 639326fcc31a95a0c7a9a5bb56f1ed4d53530f26 --- src/widget/calendar.rs | 27 ++++++++++++--------------- src/widget/icon/bundle.rs | 1 + 2 files changed, 13 insertions(+), 15 deletions(-) diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index 7f9ac0ad..2e21ebfc 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -117,19 +117,6 @@ where Message: Clone + 'static, { fn from(this: Calendar<'a, Message>) -> Self { - macro_rules! icon { - ($name:expr, $on_press:expr) => {{ - #[cfg(target_os = "linux")] - let icon = { icon::from_name($name).apply(button::icon) }; - #[cfg(not(target_os = "linux"))] - let icon = { - icon::from_svg_bytes(include_bytes!(concat!("../../res/icons/", $name, ".svg"))) - .symbolic(true) - .apply(button::icon) - }; - icon.padding([0, 12]).on_press($on_press) - }}; - } macro_rules! translate_month { ($month:expr, $year:expr) => {{ match $month { @@ -170,8 +157,18 @@ where .size(18); let month_controls = row::with_capacity(2) - .push(icon!("go-previous-symbolic", (this.on_prev)())) - .push(icon!("go-next-symbolic", (this.on_next)())); + .push( + icon::from_name("go-previous-symbolic") + .apply(button::icon) + .padding([0, 12]) + .on_press((this.on_prev)()), + ) + .push( + icon::from_name("go-next-symbolic") + .apply(button::icon) + .padding([0, 12]) + .on_press((this.on_next)()), + ); // Calender let mut calendar_grid: Grid<'_, Message> = diff --git a/src/widget/icon/bundle.rs b/src/widget/icon/bundle.rs index 0e1fdc16..bdb74c04 100644 --- a/src/widget/icon/bundle.rs +++ b/src/widget/icon/bundle.rs @@ -4,6 +4,7 @@ //! Embedded icons for platforms which do not support icon themes yet. /// Icon bundling is not enabled on unix platforms. +#[cfg(unix)] pub fn get(icon_name: &str) -> Option { None } From 882481e518f786a92a77cd24e7f8669e63441108 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, 18 Nov 2025 13:29:56 +0100 Subject: [PATCH 167/352] fix(popover): match popup styling to designs --- src/widget/popover.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/src/widget/popover.rs b/src/widget/popover.rs index ddc31455..26120b75 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -14,18 +14,20 @@ use iced_core::{ Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, }; -pub use iced_widget::container::{Catalog, Style}; - pub fn popover<'a, Message, Renderer>( content: impl Into>, -) -> Popover<'a, Message, Renderer> { +) -> Popover<'a, Message, Renderer> +where + Renderer: iced_core::Renderer + 'a, + Message: 'a, +{ Popover::new(content) } #[derive(Clone, Copy, Debug, Default)] pub enum Position { - #[default] Center, + #[default] Bottom, Point(Point), } @@ -40,7 +42,11 @@ pub struct Popover<'a, Message, Renderer> { on_close: Option, } -impl<'a, Message, Renderer> Popover<'a, Message, Renderer> { +impl<'a, Message, Renderer> Popover<'a, Message, Renderer> +where + Renderer: iced_core::Renderer + 'a, + Message: 'a, +{ pub fn new(content: impl Into>) -> Self { Self { content: content.into(), @@ -67,7 +73,12 @@ impl<'a, Message, Renderer> Popover<'a, Message, Renderer> { #[inline] pub fn popup(mut self, popup: impl Into>) -> Self { - self.popup = Some(popup.into()); + self.popup = Some( + iced_widget::container(popup) + .padding(crate::theme::spacing().space_xxs) + .class(crate::style::Container::Dropdown) + .into(), + ); self } From 14cbebbadc015d4fd3d7d702e1f77d4b879ffc5e 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, 2 Dec 2025 17:32:08 +0100 Subject: [PATCH 168/352] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index c9cd78e0..10db38f9 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit c9cd78e030d5b228f190af28d908e1fbcf8737ce +Subproject commit 10db38f982001a714bd94e99a082368762b378ee From 18182e5f97c989b93e50a6f425073232f217692f 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, 2 Dec 2025 18:00:56 +0100 Subject: [PATCH 169/352] fix(popover): set default position to `Bottom` I didn't see this part in my previous PR (sorry!). --- src/widget/popover.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 26120b75..4cea3ebf 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -52,7 +52,7 @@ where content: content.into(), modal: false, popup: None, - position: Position::Center, + position: Position::Bottom, on_close: None, } } From 80875d596219c5ca354340b861d0274e8fa68e64 Mon Sep 17 00:00:00 2001 From: Kyle Scheuing Date: Thu, 4 Dec 2025 11:31:47 -0500 Subject: [PATCH 170/352] fix: compiling on windows requires cosmic-icons in project root * fix: compiling on windows requires cosmic-icons in project root crabtime provides crabtime::WORKSPACE_PATH to refer to the CARGO_MANIFEST_DIR of the top level crate being built, which means when building libcosmic directly, crabtime::WORKSPACE_PATH will work, but when building it as a dependency of another crate, crabtime::WORKSPACE_PATH will no longer refer to the path to libcosmic. I don't think there's a good workaround, since when in the context of crabtime, CARGO_MANIFEST_DIR refers to the path to the crate generated by crabtime rather than to libcosmic. This replaces crabtime with a simple build.rs script that generates a file in OUT_DIR. * fix: do not generate icon bundle for unix targets --------- Co-authored-by: Michael Aaron Murphy --- Cargo.toml | 2 -- build.rs | 56 +++++++++++++++++++++++++++++++++++++++ src/widget/icon/bundle.rs | 44 +----------------------------- 3 files changed, 57 insertions(+), 45 deletions(-) create mode 100644 build.rs diff --git a/Cargo.toml b/Cargo.toml index 4d742126..927444e8 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -107,8 +107,6 @@ cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-c chrono = "0.4.42" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } -# Compile-time generation of code -crabtime = "1.1.4" # Internationalization i18n-embed = { version = "0.16.0", features = [ "fluent-system", diff --git a/build.rs b/build.rs new file mode 100644 index 00000000..7ba035d4 --- /dev/null +++ b/build.rs @@ -0,0 +1,56 @@ +fn main() { + println!("cargo::rerun-if-changed=build.rs"); + + #[cfg(not(unix))] + generate_bundled_icons(); +} + +#[cfg(not(unix))] +fn generate_bundled_icons() { + println!("cargo::rerun-if-changed=cosmic-icons"); + + let manifest_dir = std::path::Path::new(std::env!("CARGO_MANIFEST_DIR")); + let icon_paths = [ + "cosmic-icons/freedesktop/scalable", + "cosmic-icons/extra/scalable", + ]; + + let key_value_assignments = icon_paths + .into_iter() + .map(|path| manifest_dir.join(path)) + .inspect(|icon_path| assert!(icon_path.exists(), "path = {icon_path:?}")) + .map(|icon_path| std::fs::read_dir(icon_path).unwrap()) + .flat_map(|dir| { + dir.flat_map(|entry| entry.unwrap().path().read_dir().unwrap()) + .map(|entry| { + let entry = entry.unwrap(); + let path = entry.path().canonicalize().unwrap(); + let file_name = path.file_stem().unwrap().to_str().unwrap().to_owned(); + let path = path.into_os_string().into_string().unwrap(); + (file_name, path) + }) + }) + .fold( + std::collections::BTreeMap::new(), + |mut set, (name, path)| { + set.insert(name, path); + set + }, + ) + .into_iter() + .fold(String::new(), |mut output, (name, path)| { + output.push_str(&format!(" \"{name}\" => include_bytes!(\"{path}\"),\n")); + output + }); + + let code = [ + "static ICONS: phf::Map<&'static str, &'static [u8]> = phf::phf_map!(\n", + &key_value_assignments, + ");", + ] + .concat(); + + let out_dir = std::env::var_os("OUT_DIR").unwrap(); + let out_file = std::path::Path::new(&out_dir).join("bundled_icons.rs"); + std::fs::write(&out_file, &code).unwrap(); +} diff --git a/src/widget/icon/bundle.rs b/src/widget/icon/bundle.rs index bdb74c04..9d0877d0 100644 --- a/src/widget/icon/bundle.rs +++ b/src/widget/icon/bundle.rs @@ -18,46 +18,4 @@ pub fn get(icon_name: &str) -> Option { } #[cfg(not(unix))] -#[crabtime::expression] -fn comptime_icon_bundler() -> String { - let manifest_dir = std::path::Path::new(crabtime::WORKSPACE_PATH); - let icon_paths = [ - "cosmic-icons/freedesktop/scalable", - "cosmic-icons/extra/scalable", - ]; - - let key_value_assignments = icon_paths - .into_iter() - .map(|path| manifest_dir.join(path)) - .inspect(|icon_path| assert!(icon_path.exists(), "path = {icon_path:?}")) - .map(|icon_path| std::fs::read_dir(icon_path).unwrap()) - .flat_map(|dir| { - dir.flat_map(|entry| entry.unwrap().path().read_dir().unwrap()) - .map(|entry| { - let entry = entry.unwrap(); - let path = entry.path().canonicalize().unwrap(); - let file_name = path.file_stem().unwrap().to_str().unwrap().to_owned(); - let path = path.into_os_string().into_string().unwrap(); - (file_name, path) - }) - }) - .fold( - std::collections::BTreeMap::new(), - |mut set, (name, path)| { - set.insert(name, path); - set - }, - ) - .into_iter() - .fold(String::new(), |mut output, (name, path)| { - output.push_str(&format!(" \"{name}\" => include_bytes!(\"{path}\"),\n")); - output - }); - - ["phf::phf_map!(\n", &key_value_assignments, ")"].concat() -} - -#[cfg(not(unix))] -static ICONS: phf::Map<&'static str, &'static [u8]> = { - comptime_icon_bundler! {} -}; +include!(concat!(env!("OUT_DIR"), "/bundled_icons.rs")); From 54934a961fc39d43bd22e7246f18a75b5935f37e Mon Sep 17 00:00:00 2001 From: Kyle Scheuing Date: Thu, 4 Dec 2025 13:21:34 -0500 Subject: [PATCH 171/352] fix: cross compiling for windows from linux #[cfg(not(unix))] applies to the host machine (since that's where the build script is running) rather than the compilation target. Instead, environment variables are available to provide the information relevant to the build target at the build script's runtime. --- build.rs | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/build.rs b/build.rs index 7ba035d4..a8f1a4cc 100644 --- a/build.rs +++ b/build.rs @@ -1,11 +1,13 @@ +use std::env; + fn main() { println!("cargo::rerun-if-changed=build.rs"); - #[cfg(not(unix))] - generate_bundled_icons(); + if env::var_os("CARGO_CFG_UNIX").is_none() { + generate_bundled_icons(); + } } -#[cfg(not(unix))] fn generate_bundled_icons() { println!("cargo::rerun-if-changed=cosmic-icons"); From c2b7d7847a34079213dcc9af421ffb16dc7f77e3 Mon Sep 17 00:00:00 2001 From: Frederic Laing Date: Tue, 2 Dec 2025 17:21:28 +0100 Subject: [PATCH 172/352] feat: add Flatpak sandbox support for config paths Implement get_config_dir() and get_state_dir() helper functions that detect Flatpak sandboxing via FLATPAK_ID and use HOST_XDG_CONFIG_HOME/HOST_XDG_STATE_HOME environment variables or fallback to HOME-based paths. This allows libcosmic apps running in Flatpak sandboxes to properly read system-wide COSMIC configuration (themes, corner radii, etc.) from the host. --- cosmic-config/src/lib.rs | 48 +++++++++++++++++++++++++++++++++++++--- 1 file changed, 45 insertions(+), 3 deletions(-) diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 5f424cc3..c8eda064 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -6,12 +6,54 @@ use notify::{ }; use serde::{Serialize, de::DeserializeOwned}; use std::{ - fmt, fs, + env, fmt, fs, io::Write, path::{Path, PathBuf}, sync::Mutex, }; +/// Get the config directory, with Flatpak sandbox support. +/// In Flatpak, HOST_XDG_CONFIG_HOME points to the real user config directory, +/// allowing sandboxed apps to read host config files. +fn get_config_dir() -> Option { + // Check if we're running in Flatpak + if let Some(flatpak_id) = env::var_os("FLATPAK_ID") { + tracing::debug!("Running in Flatpak: {:?}", flatpak_id); + // Try HOST_XDG_CONFIG_HOME first (requires --filesystem=xdg-config permission) + if let Some(host_config) = env::var_os("HOST_XDG_CONFIG_HOME") { + tracing::debug!("Using HOST_XDG_CONFIG_HOME: {:?}", host_config); + return Some(PathBuf::from(host_config)); + } + // Fallback: try to construct from HOME (which points to real home in Flatpak) + if let Some(home) = env::var_os("HOME") { + let config_path = PathBuf::from(&home).join(".config"); + tracing::debug!("Using HOME fallback for config: {:?}", config_path); + return Some(config_path); + } + tracing::warn!("Flatpak detected but no config directory found"); + } + // Not in Flatpak or no host config available, use standard dirs + let config_dir = dirs::config_dir(); + tracing::debug!("Using standard config dir: {:?}", config_dir); + config_dir +} + +/// Get the state directory, with Flatpak sandbox support. +fn get_state_dir() -> Option { + // Check if we're running in Flatpak + if env::var_os("FLATPAK_ID").is_some() { + // Try HOST_XDG_STATE_HOME first + if let Some(host_state) = env::var_os("HOST_XDG_STATE_HOME") { + return Some(PathBuf::from(host_state)); + } + // Fallback: try to construct from HOME + if let Some(home) = env::var_os("HOME") { + return Some(PathBuf::from(home).join(".local").join("state")); + } + } + dirs::state_dir() +} + #[cfg(feature = "subscription")] mod subscription; #[cfg(feature = "subscription")] @@ -170,7 +212,7 @@ impl Config { .map(|x| x.join("COSMIC").join(&path)); // Get libcosmic user configuration directory - let mut user_path = dirs::config_dir().ok_or(Error::NoConfigDirectory)?; + let mut user_path = get_config_dir().ok_or(Error::NoConfigDirectory)?; user_path.push("cosmic"); user_path.push(path); @@ -212,7 +254,7 @@ impl Config { let path = sanitize_name(name)?.join(format!("v{}", version)); // Get libcosmic user state directory - let mut user_path = dirs::state_dir().ok_or(Error::NoConfigDirectory)?; + let mut user_path = get_state_dir().ok_or(Error::NoConfigDirectory)?; user_path.push("cosmic"); user_path.push(path); // Create new state directory if not found. From 45fd683bc949e82d40005d8d80dcd8a422821342 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 5 Dec 2025 16:42:29 +0100 Subject: [PATCH 173/352] examples(about): update and fix compile --- examples/about/Cargo.toml | 3 -- examples/about/src/main.rs | 80 ++++++++++++++++---------------------- 2 files changed, 33 insertions(+), 50 deletions(-) diff --git a/examples/about/Cargo.toml b/examples/about/Cargo.toml index cf067095..0f598535 100644 --- a/examples/about/Cargo.toml +++ b/examples/about/Cargo.toml @@ -4,9 +4,6 @@ version = "0.1.0" edition = "2021" [dependencies] -tracing = "0.1.41" -tracing-subscriber = "0.3.19" -tracing-log = "0.2.0" open = "5.3.2" [dependencies.libcosmic] diff --git a/examples/about/src/main.rs b/examples/about/src/main.rs index 957433f0..50f25da4 100644 --- a/examples/about/src/main.rs +++ b/examples/about/src/main.rs @@ -5,17 +5,14 @@ use cosmic::app::context_drawer::{self, ContextDrawer}; use cosmic::app::{Core, Settings, Task}; -use cosmic::iced::widget::column; -use cosmic::iced_core::Size; +use cosmic::executor; +use cosmic::iced::{alignment, Length, Size}; +use cosmic::prelude::*; use cosmic::widget::{self, about::About, nav_bar}; -use cosmic::{executor, iced, ApplicationExt, Element}; /// Runs application with these settings #[rustfmt::skip] fn main() -> Result<(), Box> { - tracing_subscriber::fmt::init(); - let _ = tracing_log::LogTracer::init(); - let settings = Settings::default() .size(Size::new(1024., 768.)); @@ -67,12 +64,12 @@ impl cosmic::Application for App { let about = About::default() .name("About Demo") - .icon(Self::APP_ID) + .icon(widget::icon::from_name(Self::APP_ID)) .version("0.1.0") - .author("System 76") + .author("System76") .license("GPL-3.0-only") - //.license_url("https://www.some-custom-license-url.com") - .developers([("Michael Murphy", "mmstick@system76.com")]) + .license_url("https://choosealicense.com/licenses/gpl-3.0/") + .developers([("Michael Murphy", "info@system76.com")]) .links([ ("Website", "https://system76.com/cosmic"), ("Repository", "https://github.com/pop-os/libcosmic"), @@ -86,7 +83,11 @@ impl cosmic::Application for App { show_about: false, }; - let command = app.update_title(); + app.set_header_title("COSMIC About Example".into()); + let command = app.set_window_title( + "COSMIC About Example".into(), + app.core.main_window_id().unwrap(), + ); (app, command) } @@ -99,12 +100,17 @@ impl cosmic::Application for App { /// Called when a navigation item is selected. fn on_nav_select(&mut self, id: nav_bar::Id) -> Task { self.nav_model.activate(id); - self.update_title() + Task::none() } - fn context_drawer(&self) -> Option> { - self.show_about - .then(|| context_drawer::about(&self.about, Message::Open, Message::ToggleAbout)) + fn context_drawer(&self) -> Option> { + self.show_about.then(|| { + context_drawer::about( + &self.about, + |url| Message::Open(url.to_owned()), + Message::ToggleAbout, + ) + }) } /// Handle application events here. @@ -116,47 +122,27 @@ impl cosmic::Application for App { } Message::Open(url) => match open::that_detached(url) { Ok(_) => (), - Err(err) => tracing::error!("Failed to open URL: {err}"), + Err(err) => eprintln!("Failed to open URL: {err}"), }, } Task::none() } /// Creates a view after each update. - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Self::Message> { + let show_about_button = widget::button::text("Show about").on_press(Message::ToggleAbout); let centered = cosmic::widget::container( - column![widget::button::text("Show about").on_press(Message::ToggleAbout)] - .width(iced::Length::Fill) - .height(iced::Length::Shrink) - .align_x(iced::alignment::Horizontal::Center), + widget::column() + .push(show_about_button) + .width(Length::Fill) + .height(Length::Shrink) + .align_x(alignment::Horizontal::Center), ) - .width(iced::Length::Fill) - .height(iced::Length::Shrink) - .align_x(iced::alignment::Horizontal::Center) - .align_y(iced::alignment::Vertical::Center); + .width(Length::Fill) + .height(Length::Shrink) + .align_x(alignment::Horizontal::Center) + .align_y(alignment::Vertical::Center); Element::from(centered) } } - -impl App -where - Self: cosmic::Application, -{ - fn active_page_title(&mut self) -> &str { - self.nav_model - .text(self.nav_model.active()) - .unwrap_or("Unknown Page") - } - - fn update_title(&mut self) -> Task { - let header_title = self.active_page_title().to_owned(); - let window_title = format!("{header_title} — COSMIC AppDemo"); - self.set_header_title(header_title); - if let Some(id) = self.core.main_window_id() { - self.set_window_title(window_title, id) - } else { - Task::none() - } - } -} From 866da0f94b9111174f0460ba3c395d1fe055a216 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 5 Dec 2025 16:44:39 +0100 Subject: [PATCH 174/352] revert: "fix(popover): set default position to `Bottom`" Causes popups to be misplaced in applications that required the previous behavior. This reverts commit 18182e5f97c989b93e50a6f425073232f217692f. --- src/widget/popover.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 4cea3ebf..26120b75 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -52,7 +52,7 @@ where content: content.into(), modal: false, popup: None, - position: Position::Bottom, + position: Position::Center, on_close: None, } } From e13ab241510ddc2e9ddfc112786426328b3124de Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 5 Dec 2025 16:46:00 +0100 Subject: [PATCH 175/352] revert: "fix(popover): match popup styling to designs" Some application popovers required the previous behavior This reverts commit 882481e518f786a92a77cd24e7f8669e63441108. --- src/widget/popover.rs | 23 ++++++----------------- 1 file changed, 6 insertions(+), 17 deletions(-) diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 26120b75..ddc31455 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -14,20 +14,18 @@ use iced_core::{ Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, }; +pub use iced_widget::container::{Catalog, Style}; + pub fn popover<'a, Message, Renderer>( content: impl Into>, -) -> Popover<'a, Message, Renderer> -where - Renderer: iced_core::Renderer + 'a, - Message: 'a, -{ +) -> Popover<'a, Message, Renderer> { Popover::new(content) } #[derive(Clone, Copy, Debug, Default)] pub enum Position { - Center, #[default] + Center, Bottom, Point(Point), } @@ -42,11 +40,7 @@ pub struct Popover<'a, Message, Renderer> { on_close: Option, } -impl<'a, Message, Renderer> Popover<'a, Message, Renderer> -where - Renderer: iced_core::Renderer + 'a, - Message: 'a, -{ +impl<'a, Message, Renderer> Popover<'a, Message, Renderer> { pub fn new(content: impl Into>) -> Self { Self { content: content.into(), @@ -73,12 +67,7 @@ where #[inline] pub fn popup(mut self, popup: impl Into>) -> Self { - self.popup = Some( - iced_widget::container(popup) - .padding(crate::theme::spacing().space_xxs) - .class(crate::style::Container::Dropdown) - .into(), - ); + self.popup = Some(popup.into()); self } From 8a9cd0da326e638a78149439a71753ea16c31540 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sun, 30 Nov 2025 02:53:48 +0100 Subject: [PATCH 176/352] i18n: translation updates from weblate Co-authored-by: CYAXXX Co-authored-by: Hosted Weblate --- i18n/kmr/libcosmic.ftl | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 i18n/kmr/libcosmic.ftl diff --git a/i18n/kmr/libcosmic.ftl b/i18n/kmr/libcosmic.ftl new file mode 100644 index 00000000..e69de29b From 2ffd1f32f404ac7f9fcb8fafeb309ce43d3fd8c3 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 5 Dec 2025 17:05:20 +0100 Subject: [PATCH 177/352] examples(application): update and fix compile --- examples/application/src/main.rs | 92 +++++++++++++++----------------- 1 file changed, 42 insertions(+), 50 deletions(-) diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index c70a9d30..45805579 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -3,25 +3,14 @@ //! Application API example +use cosmic::app::Settings; +use cosmic::iced::{Alignment, Length, Size}; +use cosmic::widget::menu::{self, KeyBind}; +use cosmic::widget::nav_bar; +use cosmic::{executor, iced, prelude::*, widget, Core}; use std::collections::HashMap; use std::sync::LazyLock; -use cosmic::app::{Core, Settings, Task}; -use cosmic::iced::alignment::{Horizontal, Vertical}; -use cosmic::iced::widget::column; -use cosmic::iced::Length; -use cosmic::iced_core::Size; -use cosmic::widget::icon::{from_name, Handle}; -use cosmic::widget::menu::KeyBind; -use cosmic::widget::{button, text}; -use cosmic::widget::{ - container, - menu::menu_button, - menu::{self, action::MenuAction}, - nav_bar, responsive, -}; -use cosmic::{executor, iced, ApplicationExt, Element}; - static MENU_ID: LazyLock = LazyLock::new(|| iced::id::Id::new("menu_id")); #[derive(Clone, Copy)] @@ -50,7 +39,7 @@ pub enum Action { Hi3, } -impl MenuAction for Action { +impl widget::menu::Action for Action { type Message = Message; fn message(&self) -> Message { @@ -129,7 +118,7 @@ impl cosmic::Application for App { } /// Creates the application, and optionally emits task on initialize. - fn init(core: Core, input: Self::Flags) -> (Self, Task) { + fn init(core: Core, input: Self::Flags) -> (Self, cosmic::app::Task) { let mut nav_model = nav_bar::Model::default(); for (title, content) in input { @@ -158,13 +147,13 @@ impl cosmic::Application for App { } /// Called when a navigation item is selected. - fn on_nav_select(&mut self, id: nav_bar::Id) -> Task { + fn on_nav_select(&mut self, id: nav_bar::Id) -> cosmic::app::Task { self.nav_model.activate(id); self.update_title() } /// Handle application events here. - fn update(&mut self, message: Self::Message) -> Task { + fn update(&mut self, message: Self::Message) -> cosmic::app::Task { match message { Message::Input1(v) => { self.input_1 = v; @@ -195,46 +184,49 @@ impl cosmic::Application for App { } /// Creates a view after each update. - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Self::Message> { let page_content = self .nav_model .active_data::() .map_or("No page selected", String::as_str); - let text = cosmic::widget::text(page_content); - - let centered = cosmic::widget::container( - column![ - text, - cosmic::widget::text_input::text_input("", &self.input_1) - .on_input(Message::Input1) - .on_clear(Message::Ignore), - cosmic::widget::text_input::secure_input( - "", - &self.input_1, - Some(Message::ToggleHide), - self.hidden + let centered = widget::container( + widget::column() + .push(widget::text::body(page_content)) + .push( + widget::text_input::text_input("", &self.input_1) + .on_input(Message::Input1) + .on_clear(Message::Ignore), ) - .on_input(Message::Input1), - cosmic::widget::text_input::text_input("", &self.input_1).on_input(Message::Input1), - cosmic::widget::text_input::search_input("", &self.input_2) - .on_input(Message::Input2) - .on_clear(Message::Ignore), - ] - .spacing(cosmic::theme::spacing().space_s) - .width(iced::Length::Fill) - .height(iced::Length::Shrink) - .align_x(iced::Alignment::Center), + .push( + widget::text_input::secure_input( + "", + &self.input_1, + Some(Message::ToggleHide), + self.hidden, + ) + .on_input(Message::Input1), + ) + .push(widget::text_input::text_input("", &self.input_2).on_input(Message::Input2)) + .push( + widget::text_input::search_input("", &self.input_2) + .on_input(Message::Input2) + .on_clear(Message::Ignore), + ) + .spacing(cosmic::theme::spacing().space_s) + .width(Length::Fill) + .height(Length::Shrink) + .align_x(Alignment::Center), ) - .width(iced::Length::Fill) - .height(iced::Length::Shrink) - .align_x(iced::Alignment::Center) - .align_y(iced::Alignment::Center); + .width(Length::Fill) + .height(Length::Shrink) + .align_x(Alignment::Center) + .align_y(Alignment::Center); Element::from(centered) } - fn header_start(&self) -> Vec> { + fn header_start(&self) -> Vec> { vec![cosmic::widget::responsive_menu_bar().into_element( self.core(), &self.keybinds, @@ -322,7 +314,7 @@ where .unwrap_or("Unknown Page") } - fn update_title(&mut self) -> Task { + fn update_title(&mut self) -> cosmic::app::Task { let header_title = self.active_page_title().to_owned(); let window_title = format!("{header_title} — COSMIC AppDemo"); self.set_header_title(header_title); From 6793950bbc6039eb59cfd0ee92a95919fb636e81 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 5 Dec 2025 17:16:35 +0100 Subject: [PATCH 178/352] fix(icon): from_svg_bytes should not default to symbolic --- src/widget/icon/handle.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/widget/icon/handle.rs b/src/widget/icon/handle.rs index a4ddd364..7e0bab02 100644 --- a/src/widget/icon/handle.rs +++ b/src/widget/icon/handle.rs @@ -94,7 +94,7 @@ pub fn from_raster_pixels( /// Create a SVG handle from memory. pub fn from_svg_bytes(bytes: impl Into>) -> Handle { Handle { - symbolic: true, + symbolic: false, data: Data::Svg(svg::Handle::from_memory(bytes)), } } From cdf4eafc9ecb53693000f8011362238ef9c3f769 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 5 Dec 2025 17:18:26 +0100 Subject: [PATCH 179/352] fix(segmented_button): set icon to symbolic --- src/widget/segmented_button/widget.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index e852a2eb..72bc7580 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -1935,6 +1935,7 @@ where 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()) From f39ad728c9f84394d9a8ac8d4543b9c1c2aec8a2 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 5 Dec 2025 17:29:11 +0100 Subject: [PATCH 180/352] examples(calendar): update and fix compile --- examples/calendar/src/main.rs | 4 ++-- src/widget/calendar.rs | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) diff --git a/examples/calendar/src/main.rs b/examples/calendar/src/main.rs index 47549a70..fec7b543 100644 --- a/examples/calendar/src/main.rs +++ b/examples/calendar/src/main.rs @@ -84,7 +84,7 @@ impl cosmic::Application for App { } /// Creates a view after each update. - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Self::Message> { let mut content = cosmic::widget::column().spacing(12); let calendar = cosmic::widget::calendar( @@ -111,7 +111,7 @@ impl App where Self: cosmic::Application, { - fn update_title(&mut self) -> Task { + fn update_title(&mut self) -> cosmic::app::Task { self.set_header_title(String::from("Calendar Demo")); self.set_window_title(String::from("Calendar Demo")) } diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index 2e21ebfc..7ee06204 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -10,6 +10,7 @@ use crate::iced_core::{Alignment, Length, Padding}; use crate::widget::{Grid, button, column, grid, icon, row, text}; use apply::Apply; use chrono::{Datelike, Days, Local, Month, Months, NaiveDate, Weekday}; +use iced::alignment::Vertical; /// A widget that displays an interactive calendar. pub fn calendar( @@ -156,17 +157,17 @@ where )) .size(18); + let day = text::body(translate_weekday!(this.model.visible.weekday())); + let month_controls = row::with_capacity(2) .push( icon::from_name("go-previous-symbolic") .apply(button::icon) - .padding([0, 12]) .on_press((this.on_prev)()), ) .push( icon::from_name("go-next-symbolic") .apply(button::icon) - .padding([0, 12]) .on_press((this.on_next)()), ); @@ -216,10 +217,11 @@ where let content_list = column::with_children([ row::with_children([ - date.into(), + column().push(date).push(day).into(), crate::widget::Space::with_width(Length::Fill).into(), month_controls.into(), ]) + .align_y(Vertical::Center) .padding([12, 20]) .into(), calendar_grid.into(), From 05c66088422b7a03e54ac1f96ff50339cc7776cf Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 5 Dec 2025 17:59:42 +0100 Subject: [PATCH 181/352] examples: fix libcosmic features, warnings, etc. --- examples/about/Cargo.toml | 3 --- examples/application/Cargo.toml | 3 --- examples/calendar/Cargo.toml | 1 - examples/calendar/src/main.rs | 5 ++++- examples/config/src/main.rs | 2 +- examples/context-menu/Cargo.toml | 3 +-- examples/context-menu/src/main.rs | 4 +--- examples/image-button/Cargo.toml | 3 +-- examples/image-button/src/main.rs | 7 +++++-- examples/menu/Cargo.toml | 3 +-- examples/menu/src/main.rs | 4 ++-- examples/multi-window/Cargo.toml | 2 +- examples/multi-window/src/window.rs | 12 ++++++------ examples/nav-context/Cargo.toml | 3 +-- examples/nav-context/src/main.rs | 2 +- examples/open-dialog/Cargo.toml | 3 +-- examples/open-dialog/src/main.rs | 10 +++++++--- examples/spin-button/Cargo.toml | 2 +- examples/spin-button/src/main.rs | 2 +- examples/table-view/Cargo.toml | 3 +-- examples/table-view/src/main.rs | 2 +- examples/text-input/Cargo.toml | 3 +-- examples/text-input/src/main.rs | 4 ++-- 23 files changed, 40 insertions(+), 46 deletions(-) diff --git a/examples/about/Cargo.toml b/examples/about/Cargo.toml index 0f598535..d2642cd6 100644 --- a/examples/about/Cargo.toml +++ b/examples/about/Cargo.toml @@ -8,18 +8,15 @@ open = "5.3.2" [dependencies.libcosmic] path = "../../" -default-features = false features = [ "debug", "winit", "tokio", "xdg-portal", - "dbus-config", "desktop", "a11y", "wayland", "wgpu", "single-instance", - "multi-window", "about", ] diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index e5ae2f30..28c13117 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -14,16 +14,13 @@ tracing-log = "0.2.0" [dependencies.libcosmic] path = "../../" -default-features = false features = [ "debug", "winit", "tokio", "xdg-portal", - "dbus-config", "a11y", "wgpu", "single-instance", - "multi-window", "surface-message", ] diff --git a/examples/calendar/Cargo.toml b/examples/calendar/Cargo.toml index 18bc6b49..9ffb838c 100644 --- a/examples/calendar/Cargo.toml +++ b/examples/calendar/Cargo.toml @@ -10,5 +10,4 @@ chrono = "0.4.40" [dependencies.libcosmic] path = "../../" -default-features = false features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"] diff --git a/examples/calendar/src/main.rs b/examples/calendar/src/main.rs index fec7b543..589bc1ff 100644 --- a/examples/calendar/src/main.rs +++ b/examples/calendar/src/main.rs @@ -113,6 +113,9 @@ where { fn update_title(&mut self) -> cosmic::app::Task { self.set_header_title(String::from("Calendar Demo")); - self.set_window_title(String::from("Calendar Demo")) + self.set_window_title( + String::from("Calendar Demo"), + self.core.main_window_id().unwrap(), + ) } } diff --git a/examples/config/src/main.rs b/examples/config/src/main.rs index f606e15c..f6fb5c0d 100644 --- a/examples/config/src/main.rs +++ b/examples/config/src/main.rs @@ -4,7 +4,7 @@ use cosmic_config::{Config, ConfigGet, ConfigSet}; fn test_config(config: Config) { - let watcher = config + let _watcher = config .watch(|config, keys| { println!("Changed: {:?}", keys); for key in keys.iter() { diff --git a/examples/context-menu/Cargo.toml b/examples/context-menu/Cargo.toml index 9a24a1c8..45cbf78a 100644 --- a/examples/context-menu/Cargo.toml +++ b/examples/context-menu/Cargo.toml @@ -10,13 +10,12 @@ tracing-log = "0.2.0" [dependencies.libcosmic] path = "../../" -default-features = false features = [ "debug", "winit", + "wgpu", "tokio", "xdg-portal", - "multi-window", "surface-message", "wayland", ] diff --git a/examples/context-menu/src/main.rs b/examples/context-menu/src/main.rs index c744f963..db66ba1b 100644 --- a/examples/context-menu/src/main.rs +++ b/examples/context-menu/src/main.rs @@ -37,7 +37,6 @@ pub enum Message { pub struct App { core: Core, button_label: String, - show_context: bool, hide_content: bool, } @@ -69,7 +68,6 @@ impl cosmic::Application for App { core, button_label: String::from("Right click me"), hide_content: false, - show_context: false, }; app.set_header_title("COSMIC Context Menu Demo".into()); @@ -102,7 +100,7 @@ impl cosmic::Application for App { } /// Creates a view after each update. - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Self::Message> { let widget = cosmic::widget::context_menu( cosmic::widget::button::text(self.button_label.to_string()).on_press(Message::Clicked), self.context_menu(), diff --git a/examples/image-button/Cargo.toml b/examples/image-button/Cargo.toml index 110be619..cf61955a 100644 --- a/examples/image-button/Cargo.toml +++ b/examples/image-button/Cargo.toml @@ -9,5 +9,4 @@ tracing-subscriber = "0.3.19" [dependencies.libcosmic] path = "../../" -default-features = false -features = ["debug", "winit", "tokio"] +features = ["debug", "winit", "wgpu", "tokio"] diff --git a/examples/image-button/src/main.rs b/examples/image-button/src/main.rs index 34d907e7..0ac906ca 100644 --- a/examples/image-button/src/main.rs +++ b/examples/image-button/src/main.rs @@ -79,7 +79,7 @@ impl cosmic::Application for App { } /// Creates a view after each update. - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Self::Message> { let mut content = cosmic::widget::column().spacing(12); for (id, image) in self.images.iter().enumerate() { @@ -108,6 +108,9 @@ where { fn update_title(&mut self) -> Task { self.set_header_title(String::from("Image Button Demo")); - self.set_window_title(String::from("Image Button Demo")) + self.set_window_title( + String::from("Image Button Demo"), + self.core.main_window_id().unwrap(), + ) } } diff --git a/examples/menu/Cargo.toml b/examples/menu/Cargo.toml index c83a216d..dcab1ef5 100644 --- a/examples/menu/Cargo.toml +++ b/examples/menu/Cargo.toml @@ -10,5 +10,4 @@ tracing-log = "0.2.0" [dependencies.libcosmic] path = "../../" -default-features = false -features = ["debug", "winit", "tokio", "xdg-portal", "multi-window"] +features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"] diff --git a/examples/menu/src/main.rs b/examples/menu/src/main.rs index 7037a62c..8b5a1cb7 100644 --- a/examples/menu/src/main.rs +++ b/examples/menu/src/main.rs @@ -110,7 +110,7 @@ impl cosmic::Application for App { (app, Task::none()) } - fn header_start(&self) -> Vec> { + fn header_start(&self) -> Vec> { vec![menu_bar(&self.config, &self.key_binds)] } @@ -137,7 +137,7 @@ impl cosmic::Application for App { } /// Creates a view after each update. - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Self::Message> { let text = if self.config.hide_content { cosmic::widget::text("") } else { diff --git a/examples/multi-window/Cargo.toml b/examples/multi-window/Cargo.toml index 168bd4ec..0b5440f8 100644 --- a/examples/multi-window/Cargo.toml +++ b/examples/multi-window/Cargo.toml @@ -6,4 +6,4 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "multi-window", "dbus-config", "wgpu", "wayland"] } +libcosmic = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "wgpu", "wayland"] } diff --git a/examples/multi-window/src/window.rs b/examples/multi-window/src/window.rs index 96d166d4..74ab5386 100644 --- a/examples/multi-window/src/window.rs +++ b/examples/multi-window/src/window.rs @@ -2,11 +2,11 @@ use std::collections::HashMap; use cosmic::{ app::Core, - iced::{self, event, window}, + iced::{self, event, window, Subscription}, iced_core::{id, Alignment, Length, Point}, iced_widget::{column, container, scrollable, text}, + prelude::*, widget::{button, header_bar}, - ApplicationExt, Task, }; #[derive(Debug, Clone, PartialEq)] @@ -57,7 +57,7 @@ impl cosmic::Application for MultiWindow { (windows, cosmic::app::Task::none()) } - fn subscription(&self) -> cosmic::iced_futures::Subscription { + fn subscription(&self) -> Subscription { event::listen_with(|event, _, id| { if let iced::Event::Window(window_event) = event { match window_event { @@ -74,7 +74,7 @@ impl cosmic::Application for MultiWindow { }) } - fn update(&mut self, message: Self::Message) -> iced::Task> { + fn update(&mut self, message: Self::Message) -> Task> { match message { Message::CloseWindow(id) => window::close(id), Message::WindowClosed(id) => { @@ -119,7 +119,7 @@ impl cosmic::Application for MultiWindow { } } - fn view_window(&self, id: window::Id) -> cosmic::prelude::Element { + fn view_window(&self, id: window::Id) -> Element<'_, Self::Message> { let w = self.windows.get(&id).unwrap(); let input_id = w.input_id.clone(); @@ -152,7 +152,7 @@ impl cosmic::Application for MultiWindow { } } - fn view(&self) -> cosmic::prelude::Element { + fn view(&self) -> Element<'_, Self::Message> { self.view_window(self.core.main_window_id().unwrap()) } } diff --git a/examples/nav-context/Cargo.toml b/examples/nav-context/Cargo.toml index a1b95413..93dbe3e9 100644 --- a/examples/nav-context/Cargo.toml +++ b/examples/nav-context/Cargo.toml @@ -10,5 +10,4 @@ tracing-log = "0.2.0" [dependencies.libcosmic] path = "../../" -default-features = false -features = ["debug", "winit", "tokio", "xdg-portal", "multi-window"] +features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"] diff --git a/examples/nav-context/src/main.rs b/examples/nav-context/src/main.rs index be458171..fdfb90f9 100644 --- a/examples/nav-context/src/main.rs +++ b/examples/nav-context/src/main.rs @@ -172,7 +172,7 @@ impl cosmic::Application for App { } /// Creates a view after each update. - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Self::Message> { let page_content = self .nav_model .active_data::() diff --git a/examples/open-dialog/Cargo.toml b/examples/open-dialog/Cargo.toml index 3fa07d42..2a734da0 100644 --- a/examples/open-dialog/Cargo.toml +++ b/examples/open-dialog/Cargo.toml @@ -16,6 +16,5 @@ tracing-subscriber = "0.3.19" url = "2.5.4" [dependencies.libcosmic] -features = ["debug", "winit", "multi-window", "wayland", "tokio"] +features = ["debug", "winit", "wgpu", "wayland", "tokio"] path = "../../" -default-features = false diff --git a/examples/open-dialog/src/main.rs b/examples/open-dialog/src/main.rs index 0edac466..10e46315 100644 --- a/examples/open-dialog/src/main.rs +++ b/examples/open-dialog/src/main.rs @@ -82,7 +82,7 @@ impl cosmic::Application for App { (app, cmd) } - fn header_end(&self) -> Vec> { + fn header_end(&self) -> Vec> { // Places a button the header to create open dialogs. vec![button::suggested("Open").on_press(Message::OpenFile).into()] } @@ -186,13 +186,17 @@ impl cosmic::Application for App { Message::CloseError => { self.error_status = None; } - Message::Surface(surface) => {} + Message::Surface(action) => { + return cosmic::task::message(cosmic::Action::Cosmic( + cosmic::app::Action::Surface(action), + )); + } } Task::none() } - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Self::Message> { let mut content = Vec::new(); if let Some(error) = self.error_status.as_deref() { diff --git a/examples/spin-button/Cargo.toml b/examples/spin-button/Cargo.toml index 3088a313..a522050b 100644 --- a/examples/spin-button/Cargo.toml +++ b/examples/spin-button/Cargo.toml @@ -7,6 +7,6 @@ edition = "2021" fraction = "0.15.3" [dependencies.libcosmic] -features = ["debug", "multi-window", "wayland", "winit", "desktop", "tokio"] +features = ["debug", "wgpu", "winit", "desktop", "tokio"] path = "../.." default-features = false diff --git a/examples/spin-button/src/main.rs b/examples/spin-button/src/main.rs index 310c5107..47db4dce 100644 --- a/examples/spin-button/src/main.rs +++ b/examples/spin-button/src/main.rs @@ -130,7 +130,7 @@ impl Application for SpinButtonExamplApp { Task::none() } - fn view(&self) -> Element { + fn view(&'_ self) -> Element<'_, Self::Message> { let space_xs = cosmic::theme::spacing().space_xs; let vert_spinner_row = iced::widget::row![ diff --git a/examples/table-view/Cargo.toml b/examples/table-view/Cargo.toml index ba3bd88e..41669cb8 100644 --- a/examples/table-view/Cargo.toml +++ b/examples/table-view/Cargo.toml @@ -10,6 +10,5 @@ tracing-log = "0.2.0" chrono = "*" [dependencies.libcosmic] -features = ["debug", "multi-window", "wayland", "winit", "desktop", "tokio"] +features = ["debug", "wgpu", "winit", "desktop", "tokio"] path = "../.." -default-features = false diff --git a/examples/table-view/src/main.rs b/examples/table-view/src/main.rs index 6bd773bc..bbd9cf5b 100644 --- a/examples/table-view/src/main.rs +++ b/examples/table-view/src/main.rs @@ -204,7 +204,7 @@ impl cosmic::Application for App { } /// Creates a view after each update. - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Self::Message> { cosmic::widget::responsive(|size| { if size.width < 600.0 { widget::compact_table(&self.table_model) diff --git a/examples/text-input/Cargo.toml b/examples/text-input/Cargo.toml index 1cc35d1d..fb1bdf28 100644 --- a/examples/text-input/Cargo.toml +++ b/examples/text-input/Cargo.toml @@ -10,5 +10,4 @@ tracing-log = "0.2.0" [dependencies.libcosmic] path = "../../" -default-features = false -features = ["debug", "winit", "tokio", "xdg-portal"] +features = ["debug", "winit", "wgpu", "tokio", "xdg-portal"] diff --git a/examples/text-input/src/main.rs b/examples/text-input/src/main.rs index 573b9dc1..ea99666c 100644 --- a/examples/text-input/src/main.rs +++ b/examples/text-input/src/main.rs @@ -87,7 +87,7 @@ impl cosmic::Application for App { } /// Creates a view after each update. - fn view(&self) -> Element { + fn view(&self) -> Element<'_, Self::Message> { let editable = cosmic::widget::editable_input( "Input text here", &self.input, @@ -118,6 +118,6 @@ where fn update_title(&mut self) -> Task { let window_title = format!("COSMIC TextInputs Demo"); self.set_header_title(window_title.clone()); - self.set_window_title(window_title) + self.set_window_title(window_title, self.core.main_window_id().unwrap()) } } From 2f0b3334914e4ab1b0f3df821eeadd7ad700566f Mon Sep 17 00:00:00 2001 From: Ian Douglas Scott Date: Mon, 13 Oct 2025 13:59:45 -0700 Subject: [PATCH 182/352] Add helper for accumulating scroll into discrete delta This converts `ScrollDelta::Pixels` and `ScrollDelta::Lines` into integer values, accumulating partial scrolls until a full integer is reached. It also has a configurable rate-limit, so discrete integer events can occur at a certain maximum frequency. This may need tuning for different use cases, though I haven't tried using it for things other than changing workspaces so far. --- src/lib.rs | 2 + src/scroll.rs | 112 ++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 114 insertions(+) create mode 100644 src/scroll.rs diff --git a/src/lib.rs b/src/lib.rs index a180c224..7e61730b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -108,6 +108,8 @@ pub mod task; pub mod theme; +pub mod scroll; + #[doc(inline)] pub use theme::{Theme, style}; diff --git a/src/scroll.rs b/src/scroll.rs new file mode 100644 index 00000000..b6d42378 --- /dev/null +++ b/src/scroll.rs @@ -0,0 +1,112 @@ +use iced::Task; +use iced::mouse::ScrollDelta; +use std::time::{Duration, Instant}; + +// Number of scroll pixels before changing workspace +const SCROLL_PIXELS: f32 = 24.0; + +// Timeout for scroll accumulation; older partial scroll is dropped +const SCROLL_TIMEOUT: Duration = Duration::from_millis(100); + +/// A scroll delta with discrete integer deltas +#[derive(Debug, Default, Clone, Copy)] +pub struct DiscreteScrollDelta { + pub x: isize, + pub y: isize, +} + +/// Helper for accumulating and converting pixel/line scrolls into and integer +/// delta between discrete options. +#[derive(Debug, Default)] +pub struct DiscreteScrollState { + x: Scroll, + y: Scroll, + rate_limit: Option, +} + +impl DiscreteScrollState { + /// Set a rate limit. If set, a call to `update()` will only not produce + /// values other than 1, -1, or 0 and a non-zero return value will not + /// occur more frequently than this duration. + pub fn rate_limit(mut self, rate_limit: Option) -> Self { + self.rate_limit = rate_limit; + self + } + + /// Reset, clearing any acculuated scroll events that haven't been + /// converted to discrete events yet. + pub fn reset(&mut self) { + self.x.reset(); + self.y.reset(); + } + + /// Accumulate delta with a timer + pub fn update(&mut self, delta: ScrollDelta) -> DiscreteScrollDelta { + let (x, y) = match delta { + ScrollDelta::Pixels { x, y } => (x / SCROLL_PIXELS, y / SCROLL_PIXELS), + ScrollDelta::Lines { x, y } => (x, y), + }; + + DiscreteScrollDelta { + x: self.x.update(x, self.rate_limit), + y: self.y.update(y, self.rate_limit), + } + } +} + +/// Scroll over a single axis +#[derive(Debug, Default)] +struct Scroll { + scroll: Option<(f32, Instant)>, + last_discrete: Option, +} + +impl Scroll { + fn reset(&mut self) { + *self = Default::default(); + } + + fn update(&mut self, delta: f32, rate_limit: Option) -> isize { + if delta == 0. { + // If delta is 0, scroll is on other axis; clear accumulated scroll + self.reset(); + 0 + } else { + let previous_scroll = if let Some((scroll, last_scroll_time)) = self.scroll { + if last_scroll_time.elapsed() > SCROLL_TIMEOUT { + 0. + } else { + scroll + } + } else { + 0. + }; + + let scroll = previous_scroll + delta; + + if self + .last_discrete + .is_some_and(|time| time.elapsed() < rate_limit.unwrap_or(Duration::ZERO)) + { + // If rate limit is hit, continute accumulating, but don't return + // a discrete event yet. + self.scroll = Some((scroll, Instant::now())); + 0 + } else { + // Return integer part of scroll, and keep remainder + self.scroll = Some((scroll.fract(), Instant::now())); + let mut discrete = scroll.trunc() as isize; + if discrete != 0 { + self.last_discrete = Some(Instant::now()); + } + if rate_limit.is_some() { + // If we are rate limiting, don't return multiple discrete events + // at once; drop extras. + discrete.signum() + } else { + discrete + } + } + } + } +} From 3b8ad45950f5d23c8550e18e628f6e70b7089d89 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Tue, 9 Dec 2025 15:00:24 +0100 Subject: [PATCH 183/352] i18n: translation updates from weblate Co-authored-by: Hosted Weblate Co-authored-by: jonnysemon Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ar/ Translation: Pop OS/libcosmic --- i18n/ar/libcosmic.ftl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/i18n/ar/libcosmic.ftl b/i18n/ar/libcosmic.ftl index ad86e64e..428bd892 100644 --- a/i18n/ar/libcosmic.ftl +++ b/i18n/ar/libcosmic.ftl @@ -1,5 +1,5 @@ # Context Drawer -close = أغلق +close = أغلِق # About license = الترخيص links = الروابط From aabc8dcda530a6ac70617dd578cea55910af53c8 Mon Sep 17 00:00:00 2001 From: Bryan Hyland Date: Tue, 9 Dec 2025 11:01:57 -0800 Subject: [PATCH 184/352] build(windows): change icon path separator for native windows builds --- build.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.rs b/build.rs index a8f1a4cc..c69feaf5 100644 --- a/build.rs +++ b/build.rs @@ -41,6 +41,9 @@ fn generate_bundled_icons() { ) .into_iter() .fold(String::new(), |mut output, (name, path)| { + // This changes the escape character to the one used by Windows. + #[cfg(windows)] + let path = path.replace("\\", "/"); output.push_str(&format!(" \"{name}\" => include_bytes!(\"{path}\"),\n")); output }); From e4978693b9004cd0cbae8e2ffef978c1b4a49484 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 15 Dec 2025 21:48:29 +0100 Subject: [PATCH 185/352] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aindriú Mac Giolla Eoin Co-authored-by: Ekramul Reza Co-authored-by: Hosted Weblate Co-authored-by: Temuri Doghonadze Co-authored-by: Vilius Paliokas Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ga/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/lt/ Translation: Pop OS/libcosmic --- i18n/bn/libcosmic.ftl | 0 i18n/ga/libcosmic.ftl | 19 +++++++++++++++++++ i18n/ka/libcosmic.ftl | 0 i18n/lt/libcosmic.ftl | 27 +++++++++++++++++++++++++++ 4 files changed, 46 insertions(+) create mode 100644 i18n/bn/libcosmic.ftl create mode 100644 i18n/ka/libcosmic.ftl diff --git a/i18n/bn/libcosmic.ftl b/i18n/bn/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/ga/libcosmic.ftl b/i18n/ga/libcosmic.ftl index 61557ccd..024841bf 100644 --- a/i18n/ga/libcosmic.ftl +++ b/i18n/ga/libcosmic.ftl @@ -6,3 +6,22 @@ designers = Dearthóirí artists = Ealaíontóirí translators = Aistritheoirí documenters = Doiciméadóirí +january = Eanáir { $year } +february = Feabhra { $year } +march = Márta { $year } +april = Aibreán { $year } +may = Bealtaine { $year } +june = Meitheamh { $year } +july = Iúil { $year } +august = Lúnasa { $year } +september = Meán Fómhair { $year } +october = Deireadh Fómhair { $year } +november = Samhain { $year } +december = Nollaig { $year } +monday = Lua +tuesday = Mái +wednesday = Céa +thursday = Déa +friday = Aoi +saturday = Sat +sunday = Dom diff --git a/i18n/ka/libcosmic.ftl b/i18n/ka/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/lt/libcosmic.ftl b/i18n/lt/libcosmic.ftl index e69de29b..6472cbd3 100644 --- a/i18n/lt/libcosmic.ftl +++ b/i18n/lt/libcosmic.ftl @@ -0,0 +1,27 @@ +february = Vasaris { $year } +close = Uždaryti +documenters = Dokumentuotojai +november = Lapkritis { $year } +friday = Penk +tuesday = Antr +may = Gegužė { $year } +wednesday = Treč +april = Balandis { $year } +monday = Pirm +translators = Vertėjai +artists = Menininkai +license = Licencija +december = Gruodis { $year } +sunday = Sekm +links = Nuorodos +march = Kovas { $year } +june = Birželis { $year } +saturday = Šešt +august = Rugpjūtis { $year } +developers = Kūrėjai +july = Liepa { $year } +thursday = Ketv +september = Rugsėjis { $year } +designers = Dizaineriai +october = Spalis { $year } +january = Sausis { $year } From fa26e0e2413cf5cd4c88201be8eb6ddd14917042 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 17 Dec 2025 03:25:00 +0100 Subject: [PATCH 186/352] docs: add link to cosmic-applet-template --- README.md | 1 + 1 file changed, 1 insertion(+) diff --git a/README.md b/README.md index ac8e60aa..23da97bc 100644 --- a/README.md +++ b/README.md @@ -10,6 +10,7 @@ A platform toolkit based on iced for creating applets and applications for the C ## Templates - https://github.com/pop-os/cosmic-app-template: Application project template +- https://github.com/pop-os/cosmic-applet-template: Panel applet project template ## Dependencies From dd3610b8ae4f1bcf2e2299e82f908913d1a4a57d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 19 Dec 2025 15:35:21 -0500 Subject: [PATCH 187/352] fix(dnd_destination): layout for dnd rectangle children --- src/widget/dnd_destination.rs | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index c943d2c7..947d2fe3 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -597,14 +597,12 @@ impl Widget }; dnd_rectangles.push(my_dest); - if let Some(child_layout) = layout.children().next() { - self.container.as_widget().drag_destinations( - &state.children[0], - child_layout.with_virtual_offset(layout.virtual_offset()), - renderer, - dnd_rectangles, - ); - } + self.container.as_widget().drag_destinations( + &state.children[0], + layout, + renderer, + dnd_rectangles, + ); } fn id(&self) -> Option { From 6f92465fcbed24beca85dec3d7e89db6e63a46de Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 25 Dec 2025 12:08:02 +0100 Subject: [PATCH 188/352] 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: Hosted Weblate Co-authored-by: Walter William Beckerleg Bruckman Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/pt_BR/ Translation: Pop OS/libcosmic --- i18n/kab/libcosmic.ftl | 0 i18n/pt-BR/libcosmic.ftl | 24 ++++++++++++------------ 2 files changed, 12 insertions(+), 12 deletions(-) create mode 100644 i18n/kab/libcosmic.ftl diff --git a/i18n/kab/libcosmic.ftl b/i18n/kab/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/pt-BR/libcosmic.ftl b/i18n/pt-BR/libcosmic.ftl index f02828bf..51b5f6c3 100644 --- a/i18n/pt-BR/libcosmic.ftl +++ b/i18n/pt-BR/libcosmic.ftl @@ -8,18 +8,18 @@ designers = Designers artists = Artistas translators = Tradutores documenters = Documentadores -january = Janeiro { $year } -february = Fevereiro { $year } -march = Março { $year } -april = Abril { $year } -may = Maio { $year } -june = Junho { $year } -july = Julho { $year } -august = Agosto { $year } -september = Setembro { $year } -october = Outubro { $year } -november = Novembro { $year } -december = Dezembro { $year } +january = Janeiro de { $year } +february = Fevereiro de { $year } +march = Março de { $year } +april = Abril de { $year } +may = Maio de { $year } +june = Junho de { $year } +july = Julho de { $year } +august = Agosto de { $year } +september = Setembro de { $year } +october = Outubro de { $year } +november = Novembro de { $year } +december = Dezembro de { $year } monday = Seg tuesday = Ter wednesday = Qua From a9f64c33ce9159485be5dad1ce07ccf7c12399d5 Mon Sep 17 00:00:00 2001 From: Michael Murphy Date: Tue, 30 Dec 2025 11:53:22 +0100 Subject: [PATCH 189/352] i18n: removing translation for Frankish --- i18n/frk/libcosmic.ftl | 0 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 i18n/frk/libcosmic.ftl diff --git a/i18n/frk/libcosmic.ftl b/i18n/frk/libcosmic.ftl deleted file mode 100644 index e69de29b..00000000 From e9bb5ed97d9120872e1e28d16f7bfcc3a4b81e2c Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 6 Jan 2026 02:25:11 +0100 Subject: [PATCH 190/352] chore: update freedesktop-desktop-entry --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 927444e8..decdac93 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -149,7 +149,7 @@ zbus = { version = "5.11.0", default-features = false } [target.'cfg(unix)'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } -freedesktop-desktop-entry = { version = "0.7.14", optional = true } +freedesktop-desktop-entry = { version = "0.8.1", optional = true } shlex = { version = "1.3.0", optional = true } [target.'cfg(not(unix))'.dependencies] From 421552dea1c06e876d5999333794b9ee918340a1 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 6 Jan 2026 02:25:46 +0100 Subject: [PATCH 191/352] fix!(desktop): IconSourceExt::as_cosmic_icon should return Handle with SVG preference --- src/desktop.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/src/desktop.rs b/src/desktop.rs index 01698af5..0d3dbb52 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -7,23 +7,22 @@ use std::path::{Path, PathBuf}; use std::{borrow::Cow, collections::HashSet, ffi::OsStr}; pub trait IconSourceExt { - fn as_cosmic_icon(&self) -> crate::widget::icon::Icon; + fn as_cosmic_icon(&self) -> crate::widget::icon::Handle; } #[cfg(not(windows))] impl IconSourceExt for fde::IconSource { - fn as_cosmic_icon(&self) -> crate::widget::icon::Icon { + fn as_cosmic_icon(&self) -> crate::widget::icon::Handle { match self { fde::IconSource::Name(name) => crate::widget::icon::from_name(name.as_str()) + .prefer_svg(true) .size(128) .fallback(Some(crate::widget::icon::IconFallback::Names(vec![ "application-default".into(), "application-x-executable".into(), ]))) - .into(), - fde::IconSource::Path(path) => { - crate::widget::icon(crate::widget::icon::from_path(path.clone())) - } + .handle(), + fde::IconSource::Path(path) => crate::widget::icon::from_path(path.clone()), } } } From f6039597b72d3eefe2ee1d6528a04077982db238 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Fri, 2 Jan 2026 23:01:52 +0100 Subject: [PATCH 192/352] i18n: translation updates from weblate Co-authored-by: Hosted Weblate Co-authored-by: Walter William Beckerleg Bruckman Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/zh_Hans/ Translation: Pop OS/libcosmic --- i18n/zh-Hans/libcosmic.ftl | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/i18n/zh-Hans/libcosmic.ftl b/i18n/zh-Hans/libcosmic.ftl index e7c83e5c..5d9fbd66 100644 --- a/i18n/zh-Hans/libcosmic.ftl +++ b/i18n/zh-Hans/libcosmic.ftl @@ -4,3 +4,23 @@ links = 链接 developers = 开发者 designers = 设计师 translators = 译者 +january = { $year }年1月 +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月 +monday = 周一 +tuesday = 周二 +wednesday = 周三 +thursday = 周四 +friday = 周五 +saturday = 周六 +sunday = 周日 +artists = 艺术家 From b9c24d24212a865977db4871efc13ff890055648 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 9 Jan 2026 23:03:09 +0100 Subject: [PATCH 193/352] feat(a11y): screen reader name and description support for button widgets --- src/widget/button/icon.rs | 11 +++++- src/widget/button/image.rs | 16 ++++++-- src/widget/button/link.rs | 15 ++++++- src/widget/button/mod.rs | 10 +++++ src/widget/button/text.rs | 8 +++- src/widget/spin_button.rs | 81 ++++++++++++++++++++++++++++++++------ 6 files changed, 122 insertions(+), 19 deletions(-) diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs index 754bc433..edb54272 100644 --- a/src/widget/button/icon.rs +++ b/src/widget/button/icon.rs @@ -38,6 +38,10 @@ impl Button<'_, Message> { Self { id: Id::unique(), label: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + name: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + description: Cow::Borrowed(""), tooltip: Cow::Borrowed(""), on_press: None, width: Length::Shrink, @@ -151,7 +155,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes ); } - let button = if builder.variant.vertical { + let mut button = if builder.variant.vertical { crate::widget::column::with_children(content) .padding(builder.padding) .spacing(builder.spacing) @@ -167,6 +171,11 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .apply(super::custom) }; + #[cfg(feature = "a11y")] + { + button = button.name(builder.name).description(builder.description); + } + let button = button .padding(0) .id(builder.id) diff --git a/src/widget/button/image.rs b/src/widget/button/image.rs index 6a5c47b1..ab51e667 100644 --- a/src/widget/button/image.rs +++ b/src/widget/button/image.rs @@ -33,6 +33,10 @@ impl<'a, Message> Button<'a, Message> { Self { id: Id::unique(), label: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + name: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + description: Cow::Borrowed(""), tooltip: Cow::Borrowed(""), on_press: None, width: Length::Shrink, @@ -79,12 +83,18 @@ where .width(builder.width) .height(builder.height); - super::custom_image_button(content, builder.variant.on_remove) + let mut button = super::custom_image_button(content, builder.variant.on_remove) .padding(0) .selected(builder.variant.selected) .id(builder.id) .on_press_maybe(builder.on_press) - .class(builder.class) - .into() + .class(builder.class); + + #[cfg(feature = "a11y")] + { + button = button.name(builder.name).description(builder.description); + } + + button.into() } } diff --git a/src/widget/button/link.rs b/src/widget/button/link.rs index b86ef1a3..9ce81268 100644 --- a/src/widget/button/link.rs +++ b/src/widget/button/link.rs @@ -34,6 +34,10 @@ impl<'a, Message> Button<'a, Message> { Self { id: Id::unique(), label: label.into(), + #[cfg(feature = "a11y")] + name: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + description: Cow::Borrowed(""), tooltip: Cow::Borrowed(""), on_press: None, width: Length::Shrink, @@ -62,7 +66,7 @@ pub fn icon() -> Handle { impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { fn from(mut builder: Button<'a, Message>) -> Element<'a, Message> { - let button: super::Button<'a, Message> = row::with_capacity(2) + let mut button: super::Button<'a, Message> = row::with_capacity(2) .push({ // TODO: Avoid allocation crate::widget::text(builder.label.to_string()) @@ -89,6 +93,15 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .on_press_maybe(builder.on_press.take()) .class(builder.class); + #[cfg(feature = "a11y")] + { + if !builder.label.is_empty() { + button = button.name(builder.label); + } + + button = button.description(builder.description); + } + if builder.tooltip.is_empty() { button.into() } else { diff --git a/src/widget/button/mod.rs b/src/widget/button/mod.rs index d9a4df94..f5975d39 100644 --- a/src/widget/button/mod.rs +++ b/src/widget/button/mod.rs @@ -69,6 +69,16 @@ pub struct Builder<'a, Message, Variant> { #[setters(into)] label: Cow<'a, str>, + /// A name for screen reader support + #[cfg(feature = "a11y")] + #[setters(into)] + name: Cow<'a, str>, + + /// A description for screen reader support + #[cfg(feature = "a11y")] + #[setters(into)] + description: Cow<'a, str>, + // Adds a tooltip to the button. #[setters(into)] tooltip: Cow<'a, str>, diff --git a/src/widget/button/text.rs b/src/widget/button/text.rs index 3f58c932..bcdd02ba 100644 --- a/src/widget/button/text.rs +++ b/src/widget/button/text.rs @@ -63,6 +63,10 @@ impl Button<'_, Message> { Self { id: Id::unique(), label: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + name: Cow::Borrowed(""), + #[cfg(feature = "a11y")] + description: Cow::Borrowed(""), tooltip: Cow::Borrowed(""), on_press: None, width: Length::Shrink, @@ -136,8 +140,10 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes #[cfg(feature = "a11y")] { if !builder.label.is_empty() { - button = button.name(builder.label); + button = button.name(builder.label) } + + button = button.description(builder.description); } if builder.tooltip.is_empty() { diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index 6f4a4de2..db90a000 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -16,6 +16,7 @@ use std::ops::{Add, Sub}; /// Horizontal spin button widget. pub fn spin_button<'a, T, M>( label: impl Into>, + #[cfg(feature = "a11y")] name: impl Into>, value: T, step: T, min: T, @@ -25,7 +26,7 @@ pub fn spin_button<'a, T, M>( where T: Copy + Sub + Add + PartialOrd, { - SpinButton::new( + let mut button = SpinButton::new( label, value, step, @@ -33,12 +34,20 @@ where max, Orientation::Horizontal, on_press, - ) + ); + + #[cfg(feature = "a11y")] + { + button = button.name(name.into()); + } + + button } /// Vertical spin button widget. pub fn vertical<'a, T, M>( label: impl Into>, + #[cfg(feature = "a11y")] name: impl Into>, value: T, step: T, min: T, @@ -48,15 +57,22 @@ pub fn vertical<'a, T, M>( where T: Copy + Sub + Add + PartialOrd, { - SpinButton::new( + let mut button = SpinButton::new( label, value, step, min, max, - Orientation::Vertical, + Orientation::Horizontal, on_press, - ) + ); + + #[cfg(feature = "a11y")] + { + button = button.name(name.into()); + } + + button } #[derive(Clone, Copy)] @@ -71,6 +87,9 @@ where { /// The formatted value of the spin button. label: Cow<'a, str>, + /// A name for screen reader support. + #[cfg(feature = "a11y")] + name: Cow<'a, str>, /// The current value of the spin button. value: T, /// The amount to increment or decrement the value. @@ -99,6 +118,8 @@ where ) -> Self { Self { label: label.into(), + #[cfg(feature = "a11y")] + name: Cow::Borrowed(""), step, value: if value < min { min @@ -113,6 +134,12 @@ where on_press: Box::from(on_press), } } + + #[cfg(feature = "a11y")] + pub(self) fn name(mut self, name: Cow<'a, str>) -> Self { + self.name = name; + self + } } fn increment(value: T, step: T, _min: T, max: T) -> T @@ -153,21 +180,28 @@ where fn make_button<'a, T, Message>( spin_button: &SpinButton<'a, T, Message>, icon: &'static str, + #[cfg(feature = "a11y")] name: String, operation: fn(T, T, T, T) -> T, ) -> Element<'a, Message> where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, { - icon::from_name(icon) + let mut button = icon::from_name(icon) .apply(button::icon) .on_press((spin_button.on_press)(operation( spin_button.value, spin_button.step, spin_button.min, spin_button.max, - ))) - .into() + ))); + + #[cfg(feature = "a11y")] + { + button = button.name(name.clone()); + } + + button.into() } fn horizontal_variant(spin_button: SpinButton<'_, T, Message>) -> Element<'_, Message> @@ -175,9 +209,20 @@ where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, { - let decrement_button = make_button(&spin_button, "list-remove-symbolic", decrement); - let increment_button = make_button(&spin_button, "list-add-symbolic", increment); - + let decrement_button = make_button( + &spin_button, + "list-remove-symbolic", + #[cfg(feature = "a11y")] + [&spin_button.name, " decrease"].concat(), + decrement, + ); + let increment_button = make_button( + &spin_button, + "list-add-symbolic", + #[cfg(feature = "a11y")] + [&spin_button.name, " increase"].concat(), + increment, + ); let label = text::body(spin_button.label) .apply(container) .center_x(Length::Fixed(48.0)) @@ -198,8 +243,18 @@ where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, { - let decrement_button = make_button(&spin_button, "list-remove-symbolic", decrement); - let increment_button = make_button(&spin_button, "list-add-symbolic", increment); + let decrement_button = make_button( + &spin_button, + "list-remove-symbolic", + [&spin_button.label, " decrease"].concat(), + decrement, + ); + let increment_button = make_button( + &spin_button, + "list-add-symbolic", + [&spin_button.label, " increase"].concat(), + increment, + ); let label = text::body(spin_button.label) .apply(container) From f453db2425fa80d3be65840f490a6f13cf66af98 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Mateusz=20Miku=C5=82a?= Date: Mon, 12 Jan 2026 21:15:14 +0100 Subject: [PATCH 194/352] chore: update iced submodule This pulls in the fix made in https://github.com/pop-os/iced/pull/253. --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 10db38f9..176589f6 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 10db38f982001a714bd94e99a082368762b378ee +Subproject commit 176589f64cc9adc3cb65da373d2e56c998326fc2 From f00043369074fc5c9528f16ebf32f5aa06896936 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 13 Jan 2026 17:01:27 +0100 Subject: [PATCH 195/352] fix(spin_button): compiler error on build without a11y --- src/widget/spin_button.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index db90a000..13cc881f 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -246,12 +246,14 @@ where let decrement_button = make_button( &spin_button, "list-remove-symbolic", + #[cfg(feature = "a11y")] [&spin_button.label, " decrease"].concat(), decrement, ); let increment_button = make_button( &spin_button, "list-add-symbolic", + #[cfg(feature = "a11y")] [&spin_button.label, " increase"].concat(), increment, ); From b0cbb54bf2b3528c895f7636c7ad1fd520fd2a9e Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 13 Jan 2026 17:01:57 +0100 Subject: [PATCH 196/352] chore(widget): remove unused RcWrapper method --- src/widget/wrapper.rs | 8 -------- 1 file changed, 8 deletions(-) diff --git a/src/widget/wrapper.rs b/src/widget/wrapper.rs index 92f26fd4..59c0a376 100644 --- a/src/widget/wrapper.rs +++ b/src/widget/wrapper.rs @@ -58,14 +58,6 @@ impl RcWrapper { let my_refmut: &mut T = &mut RefCell::borrow_mut(self.data.as_ref()); f(my_refmut) } - - /// # Panics - /// - /// Will panic if used outside of original thread. - pub(crate) unsafe fn as_ptr(&self) -> *mut T { - assert_eq!(self.thread_id, thread::current().id()); - RefCell::as_ptr(self.data.as_ref()) - } } #[derive(Clone)] From 03c440b97a401177ee353ec4b100e56ca80518ba Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 14 Jan 2026 18:46:53 +0100 Subject: [PATCH 197/352] chore(cargo): update all crate dependencies --- Cargo.toml | 30 +++++------ cosmic-config/Cargo.toml | 8 +-- cosmic-theme/Cargo.toml | 6 +-- examples/about/Cargo.toml | 2 +- examples/applet/Cargo.toml | 4 +- examples/application/Cargo.toml | 4 +- examples/calendar/Cargo.toml | 2 +- examples/context-menu/Cargo.toml | 4 +- examples/cosmic/Cargo.toml | 4 +- examples/image-button/Cargo.toml | 4 +- examples/menu/Cargo.toml | 4 +- examples/nav-context/Cargo.toml | 4 +- examples/open-dialog/Cargo.toml | 8 +-- examples/subscriptions/Cargo.toml | 10 ++++ examples/subscriptions/src/main.rs | 80 ++++++++++++++++++++++++++++++ examples/table-view/Cargo.toml | 4 +- examples/text-input/Cargo.toml | 4 +- 17 files changed, 136 insertions(+), 46 deletions(-) create mode 100644 examples/subscriptions/Cargo.toml create mode 100644 examples/subscriptions/src/main.rs diff --git a/Cargo.toml b/Cargo.toml index decdac93..46091bcc 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -99,8 +99,8 @@ async-std = [ [dependencies] apply = "0.3.0" -ashpd = { version = "0.12.0", default-features = false, optional = true } -async-fs = { version = "2.1", optional = true } +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 } @@ -113,15 +113,15 @@ i18n-embed = { version = "0.16.0", features = [ "desktop-requester", ] } i18n-embed-fl = "0.10" -rust-embed = "8.7.2" +rust-embed = "8.11.0" css-color = "0.2.8" derive_setters = "0.1.8" futures = "0.3" -image = { version = "0.25.8", default-features = false, features = [ +image = { version = "0.25.9", default-features = false, features = [ "jpeg", "png", ] } -libc = { version = "0.2.175", optional = true } +libc = { version = "0.2.180", optional = true } log = "0.4" mime = { version = "0.3.17", optional = true } palette = "0.7.6" @@ -130,22 +130,22 @@ rfd = { version = "0.15.4", default-features = false, features = [ "xdg-portal", ], optional = true } rustix = { version = "1.1", features = ["pipe", "process"], optional = true } -serde = { version = "1.0.219", features = ["derive"] } -slotmap = "1.0.7" +serde = { version = "1.0.228", features = ["derive"] } +slotmap = "1.1.1" smol = { version = "2.0.2", optional = true } -thiserror = "2.0.16" -taffy = { version = "0.9.1", features = ["grid"] } -tokio = { version = "1.47.1", optional = true } -tracing = "0.1.41" +thiserror = "2.0.17" +taffy = { version = "0.9.2", features = ["grid"] } +tokio = { version = "1.49.0", optional = true } +tracing = "0.1.44" unicode-segmentation = "1.12" -url = "2.5.7" -zbus = { version = "5.11.0", default-features = false, optional = true } +url = "2.5.8" +zbus = { version = "5.13.1", default-features = false, optional = true } # 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.11.0", default-features = false } +zbus = { version = "5.13.1", default-features = false } [target.'cfg(unix)'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } @@ -225,4 +225,4 @@ exclude = ["iced"] dirs = "6.0.0" [dev-dependencies] -tempfile = "3.13.0" +tempfile = "3.24.0" diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 9b5aca07..78d671ca 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -11,18 +11,18 @@ subscription = ["iced_futures"] [dependencies] cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } -zbus = { version = "5.11.0", default-features = false, optional = true } +zbus = { version = "5.13.1", default-features = false, optional = true } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } calloop = { version = "0.14.3", optional = true } notify = "8.2.0" ron = "0.11.0" -serde = "1.0.219" +serde = "1.0.228" cosmic-config-derive = { path = "../cosmic-config-derive/", optional = true } 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.47", optional = true, features = ["time"] } +tokio = { version = "1.49", 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.3.1" +known-folders = "1.4.0" diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 44b0df5a..10b548b4 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -17,8 +17,8 @@ no-default = [] [dependencies] palette = { version = "0.7.6", features = ["serializing"] } almost = "0.2" -serde = { version = "1.0.219", features = ["derive"] } -serde_json = { version = "1.0.143", optional = true, features = [ +serde = { version = "1.0.228", features = ["derive"] } +serde_json = { version = "1.0.149", optional = true, features = [ "preserve_order", ] } ron = "0.11.0" @@ -28,4 +28,4 @@ cosmic-config = { path = "../cosmic-config/", default-features = false, features "macro", ] } dirs.workspace = true -thiserror = "2.0.16" +thiserror = "2.0.17" diff --git a/examples/about/Cargo.toml b/examples/about/Cargo.toml index d2642cd6..f980811c 100644 --- a/examples/about/Cargo.toml +++ b/examples/about/Cargo.toml @@ -4,7 +4,7 @@ version = "0.1.0" edition = "2021" [dependencies] -open = "5.3.2" +open = "5.3.3" [dependencies.libcosmic] path = "../../" diff --git a/examples/applet/Cargo.toml b/examples/applet/Cargo.toml index c39ca288..f97bff44 100644 --- a/examples/applet/Cargo.toml +++ b/examples/applet/Cargo.toml @@ -7,10 +7,10 @@ edition = "2021" [dependencies] once_cell = "1" -rust-embed = "8.6.0" +rust-embed = "8.11.0" tracing = "0.1" env_logger = "0.10.2" -log = "0.4.26" +log = "0.4.29" [dependencies.libcosmic] git = "https://github.com/pop-os/libcosmic" diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index 28c13117..f05c0418 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -8,8 +8,8 @@ default = ["wayland"] wayland = ["libcosmic/wayland"] [dependencies] -tracing = "0.1.41" -tracing-subscriber = "0.3.19" +tracing = "0.1.44" +tracing-subscriber = "0.3.22" tracing-log = "0.2.0" [dependencies.libcosmic] diff --git a/examples/calendar/Cargo.toml b/examples/calendar/Cargo.toml index 9ffb838c..59b23c0c 100644 --- a/examples/calendar/Cargo.toml +++ b/examples/calendar/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -chrono = "0.4.40" +chrono = "0.4.42" [dependencies.libcosmic] path = "../../" diff --git a/examples/context-menu/Cargo.toml b/examples/context-menu/Cargo.toml index 45cbf78a..39c550f4 100644 --- a/examples/context-menu/Cargo.toml +++ b/examples/context-menu/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -tracing = "0.1.41" -tracing-subscriber = "0.3.19" +tracing = "0.1.44" +tracing-subscriber = "0.3.22" tracing-log = "0.2.0" [dependencies.libcosmic] diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index 695f0c37..8c2a3126 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -19,9 +19,9 @@ libcosmic = { path = "../..", features = [ "xdg-portal", ] } once_cell = "1.21" -slotmap = "1.0.7" +slotmap = "1.1.1" env_logger = "0.10" -log = "0.4.26" +log = "0.4.29" [dependencies.cosmic-time] git = "https://github.com/pop-os/cosmic-time" diff --git a/examples/image-button/Cargo.toml b/examples/image-button/Cargo.toml index cf61955a..c219a53b 100644 --- a/examples/image-button/Cargo.toml +++ b/examples/image-button/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -tracing = "0.1.41" -tracing-subscriber = "0.3.19" +tracing = "0.1.44" +tracing-subscriber = "0.3.22" [dependencies.libcosmic] path = "../../" diff --git a/examples/menu/Cargo.toml b/examples/menu/Cargo.toml index dcab1ef5..430b26ea 100644 --- a/examples/menu/Cargo.toml +++ b/examples/menu/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -tracing = "0.1.41" -tracing-subscriber = "0.3.19" +tracing = "0.1.44" +tracing-subscriber = "0.3.22" tracing-log = "0.2.0" [dependencies.libcosmic] diff --git a/examples/nav-context/Cargo.toml b/examples/nav-context/Cargo.toml index 93dbe3e9..d829df0f 100644 --- a/examples/nav-context/Cargo.toml +++ b/examples/nav-context/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -tracing = "0.1.41" -tracing-subscriber = "0.3.19" +tracing = "0.1.44" +tracing-subscriber = "0.3.22" tracing-log = "0.2.0" [dependencies.libcosmic] diff --git a/examples/open-dialog/Cargo.toml b/examples/open-dialog/Cargo.toml index 2a734da0..94049270 100644 --- a/examples/open-dialog/Cargo.toml +++ b/examples/open-dialog/Cargo.toml @@ -10,10 +10,10 @@ xdg-portal = ["libcosmic/xdg-portal"] [dependencies] apply = "0.3.0" -tokio = { version = "1.44", features = ["full"] } -tracing = "0.1.41" -tracing-subscriber = "0.3.19" -url = "2.5.4" +tokio = { version = "1.49", features = ["full"] } +tracing = "0.1.44" +tracing-subscriber = "0.3.22" +url = "2.5.8" [dependencies.libcosmic] features = ["debug", "winit", "wgpu", "wayland", "tokio"] diff --git a/examples/subscriptions/Cargo.toml b/examples/subscriptions/Cargo.toml new file mode 100644 index 00000000..8eb69ff3 --- /dev/null +++ b/examples/subscriptions/Cargo.toml @@ -0,0 +1,10 @@ +[package] +name = "subscriptions" +version = "0.1.0" +edition = "2024" + +[dependencies] + +[dependencies.libcosmic] +path = "../../" +features = ["debug", "winit", "wgpu", "tokio", "xdg-portal"] diff --git a/examples/subscriptions/src/main.rs b/examples/subscriptions/src/main.rs new file mode 100644 index 00000000..47bd3772 --- /dev/null +++ b/examples/subscriptions/src/main.rs @@ -0,0 +1,80 @@ +// Copyright 2025 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Application API example + +use cosmic::app::{Core, Settings, Task}; +use cosmic::iced::Subscription; +use cosmic::{executor, prelude::*, widget}; + +/// Runs application with these settings +fn main() -> Result<(), Box> { + cosmic::app::run::(Settings::default(), ())?; + Ok(()) +} + +/// Messages that are used specifically by our [`App`]. +#[derive(Clone, Debug)] +pub enum Message {} + +/// The [`App`] stores application-specific state. +pub struct App { + core: Core, +} + +/// Implement [`cosmic::Application`] to integrate with COSMIC. +impl cosmic::Application for App { + /// Default async executor to use with the app. + type Executor = executor::Default; + + /// Argument received [`cosmic::Application::new`]. + type Flags = (); + + /// Message type specific to our [`App`]. + type Message = Message; + + /// The unique application ID to supply to the window manager. + const APP_ID: &'static str = "org.cosmic.TextInputsDemo"; + + fn core(&self) -> &Core { + &self.core + } + + fn core_mut(&mut self) -> &mut Core { + &mut self.core + } + + /// Creates the application, and optionally emits task on initialize. + fn init(core: Core, _input: Self::Flags) -> (Self, Task) { + let mut app = App { core }; + + let commands = Task::batch(vec![app.update_title()]); + + (app, commands) + } + + fn subscription(&self) -> Subscription { + Subscription::none() + } + + /// Handle application events here. + fn update(&mut self, message: Self::Message) -> Task { + Task::none() + } + + /// Creates a view after each update. + fn view(&self) -> Element<'_, Self::Message> { + widget::row().into() + } +} + +impl App +where + Self: cosmic::Application, +{ + fn update_title(&mut self) -> Task { + let window_title = format!("COSMIC Subscriptions Demo"); + self.set_header_title(window_title.clone()); + self.set_window_title(window_title, self.core.main_window_id().unwrap()) + } +} diff --git a/examples/table-view/Cargo.toml b/examples/table-view/Cargo.toml index 41669cb8..8ed45928 100644 --- a/examples/table-view/Cargo.toml +++ b/examples/table-view/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -tracing = "0.1.37" -tracing-subscriber = "0.3.17" +tracing = "0.1.44" +tracing-subscriber = "0.3.22" tracing-log = "0.2.0" chrono = "*" diff --git a/examples/text-input/Cargo.toml b/examples/text-input/Cargo.toml index fb1bdf28..fe6105c2 100644 --- a/examples/text-input/Cargo.toml +++ b/examples/text-input/Cargo.toml @@ -4,8 +4,8 @@ version = "0.1.0" edition = "2021" [dependencies] -tracing = "0.1.41" -tracing-subscriber = "0.3.19" +tracing = "0.1.44" +tracing-subscriber = "0.3.22" tracing-log = "0.2.0" [dependencies.libcosmic] From 85709b5c2943648df1293baeef852dc0d7907d2e Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 15 Jan 2026 15:23:51 +0100 Subject: [PATCH 198/352] fix(iced): fix for crash in cosmic-launcher --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 176589f6..2db5545f 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 176589f64cc9adc3cb65da373d2e56c998326fc2 +Subproject commit 2db5545fbee505c2c643c628a8984d1666c4d451 From 3e6c9a6addca2dfe8cadc1dbd03add72cb6d0673 Mon Sep 17 00:00:00 2001 From: Jonatan Pettersson Date: Fri, 16 Jan 2026 14:19:06 +0100 Subject: [PATCH 199/352] feat: add optional placeholder text to dropdown --- src/widget/dropdown/widget.rs | 30 +++++++++++++++++++++++++----- 1 file changed, 25 insertions(+), 5 deletions(-) diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index d4a9bc87..03be4eb3 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -47,6 +47,8 @@ where gap: f32, #[setters(into)] padding: Padding, + #[setters(strip_option, into)] + placeholder: Option>, #[setters(strip_option)] text_size: Option, text_line_height: text::LineHeight, @@ -86,6 +88,7 @@ where selections, icons: Cow::Borrowed(&[]), selected, + placeholder: None, width: Length::Shrink, gap: Self::DEFAULT_GAP, padding: Self::DEFAULT_PADDING, @@ -115,6 +118,7 @@ where selections, icons, selected, + placeholder, width, gap, padding, @@ -131,6 +135,7 @@ where selections, icons, selected, + placeholder, width, gap, padding, @@ -241,6 +246,7 @@ where .map(AsRef::as_ref) .zip(tree.state.downcast_mut::().selections.get_mut(id)) }), + self.placeholder.as_deref(), !self.icons.is_empty(), ) } @@ -313,6 +319,7 @@ where font, self.selected.and_then(|id| self.selections.get(id)), self.selected.and_then(|id| self.icons.get(id)), + self.placeholder.as_deref(), tree.state.downcast_ref::(), viewport, ); @@ -451,6 +458,7 @@ pub fn layout( text_line_height: text::LineHeight, font: Option, selection: Option<(&str, &mut crate::Plain)>, + placeholder: Option<&str>, has_icons: bool, ) -> layout::Node { use std::f32; @@ -459,8 +467,8 @@ pub fn layout( let max_width = match width { Length::Shrink => { - let measure = move |(label, paragraph): (_, &mut crate::Plain)| -> f32 { - paragraph.update(Text { + 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), @@ -470,11 +478,22 @@ pub fn layout( vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), - }); + }; + let paragraph = match paragraph { + Some(p) => { + p.update(text); + p + } + None => &mut crate::Plain::new(text), + }; paragraph.min_width().round() }; - selection.map(measure).unwrap_or_default() + selection + .map(|(l, p)| (l, Some(p))) + .or_else(|| placeholder.map(|l| (l, None))) + .map(measure) + .unwrap_or_default() } _ => 0.0, }; @@ -841,6 +860,7 @@ pub fn draw<'a, S>( font: crate::font::Font, selected: Option<&'a S>, icon: Option<&'a icon::Handle>, + placeholder: Option<&'a str>, state: &'a State, viewport: &Rectangle, ) where @@ -880,7 +900,7 @@ pub fn draw<'a, S>( ); } - if let Some(content) = selected.map(AsRef::as_ref) { + if let Some(content) = selected.map(AsRef::as_ref).or(placeholder) { let text_size = text_size.unwrap_or_else(|| text::Renderer::default_size(renderer).0); let mut bounds = Rectangle { From 097c76f0e56919f4c168e8a53aa5e67e207ac8b1 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Fri, 16 Jan 2026 17:23:40 +0100 Subject: [PATCH 200/352] i18n: translation updates from weblate Co-authored-by: Baurzhan Muftakhidinov Co-authored-by: Hosted Weblate --- i18n/kk/libcosmic.ftl | 0 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 i18n/kk/libcosmic.ftl diff --git a/i18n/kk/libcosmic.ftl b/i18n/kk/libcosmic.ftl new file mode 100644 index 00000000..e69de29b From 689f25be539bb7163fe01dd3daaa253dc212f131 Mon Sep 17 00:00:00 2001 From: vacenty <193441458+vacenty@users.noreply.github.com> Date: Wed, 21 Jan 2026 14:08:25 +0100 Subject: [PATCH 201/352] feat(spin_button): when value is min/maxed, disable decrease/increase button --- src/widget/spin_button.rs | 32 +++++++++++++++++++++++--------- 1 file changed, 23 insertions(+), 9 deletions(-) diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index 13cc881f..9ad81b4d 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -181,20 +181,22 @@ fn make_button<'a, T, Message>( spin_button: &SpinButton<'a, T, Message>, icon: &'static str, #[cfg(feature = "a11y")] name: String, - operation: fn(T, T, T, T) -> T, + operation: Option T>, ) -> Element<'a, Message> where Message: Clone + 'static, T: Copy + Sub + Add + PartialOrd, { - let mut button = icon::from_name(icon) - .apply(button::icon) - .on_press((spin_button.on_press)(operation( + let mut button = icon::from_name(icon).apply(button::icon); + + if let Some(f) = operation { + button = button.on_press((spin_button.on_press)(f( spin_button.value, spin_button.step, spin_button.min, spin_button.max, - ))); + ))) + }; #[cfg(feature = "a11y")] { @@ -214,14 +216,20 @@ where "list-remove-symbolic", #[cfg(feature = "a11y")] [&spin_button.name, " decrease"].concat(), - decrement, + match spin_button.value == spin_button.min { + true => None, + false => Some(decrement), + }, ); let increment_button = make_button( &spin_button, "list-add-symbolic", #[cfg(feature = "a11y")] [&spin_button.name, " increase"].concat(), - increment, + match spin_button.value == spin_button.max { + true => None, + false => Some(increment), + }, ); let label = text::body(spin_button.label) .apply(container) @@ -248,14 +256,20 @@ where "list-remove-symbolic", #[cfg(feature = "a11y")] [&spin_button.label, " decrease"].concat(), - decrement, + match spin_button.value == spin_button.min { + true => None, + false => Some(decrement), + }, ); let increment_button = make_button( &spin_button, "list-add-symbolic", #[cfg(feature = "a11y")] [&spin_button.label, " increase"].concat(), - increment, + match spin_button.value == spin_button.max { + true => None, + false => Some(increment), + }, ); let label = text::body(spin_button.label) From d71c42102d9899d8a6a924c4b064175d2e4a2230 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 21 Jan 2026 19:21:46 -0500 Subject: [PATCH 202/352] fix(segmented button): tab dnd --- src/widget/segmented_button/widget.rs | 117 +++++++++++++++++--------- 1 file changed, 75 insertions(+), 42 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 72bc7580..7a01749e 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -297,11 +297,8 @@ where } /// Enable drag-and-drop support for tabs using the provided payload builder. - pub fn enable_tab_drag( - mut self, - payload: impl Fn(Entity) -> Option<(String, Vec)> + 'static, - ) -> Self { - self.tab_drag = Some(TabDragSource::new(payload)); + pub fn enable_tab_drag(mut self, mime: String) -> Self { + self.tab_drag = Some(TabDragSource::new(mime)); self } @@ -664,28 +661,29 @@ where bounds: Rectangle, cursor: Point, ) -> Option { - let dragging = state.dragging_tab?; + let _ = state.dragging_tab?; self.variant_bounds(state, bounds) .filter_map(|item| match item { ItemBounds::Button(entity, rect) if rect.contains(cursor) => Some((entity, rect)), _ => None, }) - .find_map(|(entity, rect)| { + .map(|(entity, rect)| { let before = if Self::VERTICAL { cursor.y < rect.center_y() } else { cursor.x < rect.center_x() }; - Some(DropHint { + DropHint { entity, side: if before { DropSide::Before } else { DropSide::After }, - }) + } }) + .next() } fn start_tab_drag( @@ -713,33 +711,24 @@ where tab_drag.threshold ); - let Some((mime, data)) = (tab_drag.payload)(entity) else { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "start_tab_drag aborted entity={:?}: payload builder returned None", - entity - ); - return false; - }; - - let data_len = data.len(); - let mime_label = mime.clone(); + let data_len = 0; iced_core::clipboard::start_dnd::( clipboard, false, Some(iced_core::clipboard::DndSource::Widget(self.id.0.clone())), None, - Box::new(SimpleDragData::new(mime, data)), + Box::new(SimpleDragData::new(tab_drag.mime.clone(), vec![1])), DndAction::Move, ); log::trace!( target: TAB_REORDER_LOG_TARGET, "tab drag started entity={:?} mime={} bytes={}", entity, - mime_label, + tab_drag.mime, data_len ); + state.dragging_tab = Some(entity); state.tab_drag_candidate = None; state.pressed_item = None; @@ -815,6 +804,7 @@ where tab_drag_candidate: None, dragging_tab: None, drop_hint: None, + offer_mimes: Vec::new(), }) } @@ -966,26 +956,29 @@ where "offer enter id={my_id:?} entity={entity:?} @ ({x},{y}) mimes={mime_types:?}" ); - let on_dnd_enter = - self.on_dnd_enter - .as_ref() - .zip(entity) - .map(|(on_enter, entity)| { - move |_, _, mime_types| on_enter(entity, mime_types) - }); + let on_dnd_enter = self + .on_dnd_enter + .as_ref() + .zip(entity) + .map(|(on_enter, entity)| move |_, _, mimes| on_enter(entity, mimes)); + let mimes = if let Some(mime) = self.tab_drag.as_ref().map(|d| &d.mime) + && mime_types.is_empty() + { + vec![mime.clone()] + } else { + mime_types.clone() + }; + state.offer_mimes = mimes.clone(); - _ = state.dnd_state.on_enter::( - *x, - *y, - mime_types.clone(), - on_dnd_enter, - entity, - ); + _ = state + .dnd_state + .on_enter::(*x, *y, mimes, on_dnd_enter, entity); } DndEvent::Offer(id, OfferEvent::LeaveDestination) if Some(my_id) != *id => {} DndEvent::Offer(id, OfferEvent::Leave | OfferEvent::LeaveDestination) if Some(my_id) == *id => { + state.dragging_tab = None; state.drop_hint = None; self.emit_drop_hint(shell, state.drop_hint); if let Some(Some(entity)) = entity { @@ -999,7 +992,6 @@ where ); _ = state.dnd_state.on_leave::(None); } - DndEvent::Offer(_, OfferEvent::Leave | OfferEvent::LeaveDestination) => {} DndEvent::Offer(id, OfferEvent::Motion { x, y }) if Some(my_id) == *id => { log::trace!( target: TAB_REORDER_LOG_TARGET, @@ -1034,7 +1026,7 @@ where .as_ref() .map(|dnd| dnd.selected_action); if let Some(on_dnd_enter) = self.on_dnd_enter.as_ref() { - shell.publish(on_dnd_enter(new_entity, Vec::new())); + shell.publish(on_dnd_enter(new_entity, state.offer_mimes.clone())); } if let Some(dnd) = state.dnd_state.drag_offer.as_mut() { dnd.data = Some(new_entity); @@ -1097,7 +1089,11 @@ where .drag_offer .as_ref() .is_some_and(|offer| offer.selected_action.contains(DndAction::Move)); - let pending_reorder = if allow_reorder && self.on_reorder.is_some() { + let pending_reorder = if allow_reorder + && self.on_reorder.is_some() + && self.tab_drag.as_ref().is_some_and(|d| d.mime == *mime_type) + && state.dragging_tab.is_some() + { drop_entity.and_then(|target| self.reorder_event_for_drop(state, target)) } else { None @@ -1122,6 +1118,8 @@ where shell.publish(msg); } state.drop_hint = None; + state.dragging_tab = None; + self.emit_drop_hint(shell, state.drop_hint); if let Some(event) = pending_reorder { if let Some(on_reorder) = self.on_reorder.as_ref() { @@ -1135,6 +1133,8 @@ where "data received without entity id={my_id:?}" ); state.drop_hint = None; + state.dragging_tab = None; + self.emit_drop_hint(shell, state.drop_hint); if let Some(event) = pending_reorder { if let Some(on_reorder) = self.on_reorder.as_ref() { @@ -2118,6 +2118,36 @@ where } } + if let Some(mime) = self.tab_drag.as_ref().map(|d| &d.mime) { + for item in self.variant_bounds(local_state, layout.bounds()) { + if let ItemBounds::Button(_entity, rect) = item { + pushed = true; + log::trace!( + target: TAB_REORDER_LOG_TARGET, + "register drag destination id={:?} bounds=({:.2},{:.2},{:.2},{:.2}) mimes={:?}", + my_id, + rect.x, + rect.y, + rect.width, + rect.height, + mime + ); + dnd_rectangles.push(DndDestinationRectangle { + id: my_id, + rectangle: dnd::Rectangle { + x: f64::from(rect.x), + y: f64::from(rect.y), + width: f64::from(rect.width), + height: f64::from(rect.height), + }, + mime_types: vec![Cow::Owned(mime.clone())], + actions: DndAction::Copy | DndAction::Move, + preferred: DndAction::Move, + }); + } + } + } + if !pushed { let bounds = layout.bounds(); log::trace!( @@ -2165,15 +2195,15 @@ where } struct TabDragSource { - payload: Box Option<(String, Vec)>>, + mime: String, threshold: f32, _marker: PhantomData, } impl TabDragSource { - fn new(payload: impl Fn(Entity) -> Option<(String, Vec)> + 'static) -> Self { + fn new(mime: String) -> Self { Self { - payload: Box::new(payload), + mime, threshold: 8.0, _marker: PhantomData, } @@ -2254,6 +2284,8 @@ pub struct LocalState { wheel_timestamp: Option, /// Dnd state pub dnd_state: crate::widget::dnd_destination::State>, + /// Dnd state + pub offer_mimes: Vec, /// Tracks multi-touch events fingers_pressed: HashSet, /// The currently pressed item @@ -2391,6 +2423,7 @@ mod tests { tab_drag_candidate: None, dragging_tab: Some(dragging), drop_hint: None, + offer_mimes: Vec::new(), }; state.buttons_visible = len; state.known_length = len; From beddbf17703728182395a13267954d839226331d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 22 Jan 2026 10:21:05 -0500 Subject: [PATCH 203/352] improv(segmented_button): dnd state handling --- src/widget/segmented_button/widget.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 7a01749e..4206e727 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -968,17 +968,20 @@ where } else { mime_types.clone() }; - state.offer_mimes = mimes.clone(); + state.offer_mimes.clone_from(&mimes); _ = state .dnd_state .on_enter::(*x, *y, mimes, on_dnd_enter, entity); } DndEvent::Offer(id, OfferEvent::LeaveDestination) if Some(my_id) != *id => {} - DndEvent::Offer(id, OfferEvent::Leave | OfferEvent::LeaveDestination) - if Some(my_id) == *id => + DndEvent::Offer(id, leave) + if matches!(leave, OfferEvent::Leave | OfferEvent::LeaveDestination) + && Some(my_id) == *id => { - state.dragging_tab = None; + if matches!(leave, OfferEvent::Leave) { + state.dragging_tab = None; + } state.drop_hint = None; self.emit_drop_hint(shell, state.drop_hint); if let Some(Some(entity)) = entity { From 927035809f1564674434c27cbecdc67e199db28e Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 22 Jan 2026 15:49:47 -0500 Subject: [PATCH 204/352] refactor(segmented button): only clear tab drag after source event cancel or finish --- src/widget/segmented_button/widget.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 4206e727..9d276be8 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -1121,7 +1121,6 @@ where shell.publish(msg); } state.drop_hint = None; - state.dragging_tab = None; self.emit_drop_hint(shell, state.drop_hint); if let Some(event) = pending_reorder { @@ -1136,7 +1135,6 @@ where "data received without entity id={my_id:?}" ); state.drop_hint = None; - state.dragging_tab = None; self.emit_drop_hint(shell, state.drop_hint); if let Some(event) = pending_reorder { From f1c43f79abd4d5c0c610241def1d51f5ba0fbe3a Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sat, 24 Jan 2026 17:02:07 +0100 Subject: [PATCH 205/352] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aman Alam Co-authored-by: Baurzhan Muftakhidinov Co-authored-by: Hosted Weblate Co-authored-by: Jun Hwi Ku Co-authored-by: Walter William Beckerleg Bruckman Co-authored-by: gift983 <983649@my.leicestercollege.ac.uk> Co-authored-by: summoner001 Co-authored-by: 김유빈 Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/hu/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/kk/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ko/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/zh_Hans/ Translation: Pop OS/libcosmic --- i18n/hu/libcosmic.ftl | 2 +- i18n/kk/libcosmic.ftl | 27 +++++++++++++++++++++++++++ i18n/ko/libcosmic.ftl | 27 +++++++++++++++++++++++++++ i18n/pa/libcosmic.ftl | 0 i18n/ti/libcosmic.ftl | 0 i18n/zh-Hans/libcosmic.ftl | 1 + 6 files changed, 56 insertions(+), 1 deletion(-) create mode 100644 i18n/pa/libcosmic.ftl create mode 100644 i18n/ti/libcosmic.ftl diff --git a/i18n/hu/libcosmic.ftl b/i18n/hu/libcosmic.ftl index 583fbe5c..02069244 100644 --- a/i18n/hu/libcosmic.ftl +++ b/i18n/hu/libcosmic.ftl @@ -2,7 +2,7 @@ close = Bezárás # About license = Licenc -links = Linkek +links = Hivatkozások developers = Fejlesztők designers = Tervezők artists = Művészek diff --git a/i18n/kk/libcosmic.ftl b/i18n/kk/libcosmic.ftl index e69de29b..bb06e98f 100644 --- a/i18n/kk/libcosmic.ftl +++ b/i18n/kk/libcosmic.ftl @@ -0,0 +1,27 @@ +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 = Дс +tuesday = Сс +wednesday = Ср +thursday = Бс +friday = Жм +saturday = Сб +sunday = Жс diff --git a/i18n/ko/libcosmic.ftl b/i18n/ko/libcosmic.ftl index e69de29b..8d499756 100644 --- a/i18n/ko/libcosmic.ftl +++ b/i18n/ko/libcosmic.ftl @@ -0,0 +1,27 @@ +february = { $year }년 2월 +close = 닫기 +documenters = 문서 작성자 +november = { $year }년 11월 +friday = 금 +tuesday = 화 +may = { $year }년 5월 +wednesday = 수 +april = { $year }년 4월 +monday = 월 +translators = 번역가 +artists = 아티스트 +license = 라이선스 +december = { $year }년 12월 +sunday = 일 +links = 링크 +march = { $year }년 3월 +june = { $year }년 6월 +saturday = 토 +august = { $year }년 8월 +developers = 개발자 +july = { $year }년 7월 +thursday = 목 +september = { $year }년 9월 +designers = 디자이너 +october = { $year }년 10월 +january = { $year }년 1월 diff --git a/i18n/pa/libcosmic.ftl b/i18n/pa/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/ti/libcosmic.ftl b/i18n/ti/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/zh-Hans/libcosmic.ftl b/i18n/zh-Hans/libcosmic.ftl index 5d9fbd66..9dfd6139 100644 --- a/i18n/zh-Hans/libcosmic.ftl +++ b/i18n/zh-Hans/libcosmic.ftl @@ -24,3 +24,4 @@ friday = 周五 saturday = 周六 sunday = 周日 artists = 艺术家 +documenters = 文档作者 From 9fcd449611d30891d5fe5272520672da5ef6a723 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 27 Jan 2026 13:38:58 -0500 Subject: [PATCH 206/352] fix(segmented_button): hover state handling when hover state changes, paragraphs also need to be updated. I'll make a not to check this again after the rebase though. --- src/widget/segmented_button/widget.rs | 182 +++++++++++++++----------- 1 file changed, 106 insertions(+), 76 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 9d276be8..4f68b3de 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -242,6 +242,49 @@ 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) { + self.font_active + } else if state.show_context.is_some() || self.button_is_hovered(state, key) { + self.font_hovered + } else if self.model.is_active(key) { + self.font_active + } else { + self.font_inactive + }; + + let mut hasher = DefaultHasher::new(); + text.hash(&mut hasher); + 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; + } + } + + 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, + line_height: self.line_height, + }; + + if let Some(paragraph) = state.paragraphs.get_mut(key) { + paragraph.update(text); + } else { + state.paragraphs.insert(key, crate::Plain::new(text)); + } + } + } + pub fn context_menu(mut self, context_menu: Option>>) -> Self where Message: Clone + 'static, @@ -761,6 +804,14 @@ where SelectionMode: Default, Message: 'static + Clone, { + fn id(&self) -> Option { + Some(self.id.0.clone()) + } + + fn set_id(&mut self, id: widget::Id) { + self.id = Id(id); + } + fn children(&self) -> Vec { let mut children = Vec::new(); @@ -812,46 +863,7 @@ where let state = tree.state.downcast_mut::(); for key in self.model.order.iter().copied() { - if let Some(text) = self.model.text.get(key) { - let font = if self.button_is_focused(state, key) { - self.font_active - } else if state.show_context.is_some() || self.button_is_hovered(state, key) { - self.font_hovered - } else if self.model.is_active(key) { - self.font_active - } else { - self.font_inactive - }; - - let mut hasher = DefaultHasher::new(); - text.hash(&mut hasher); - 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 { - continue; - } - } - - 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, - line_height: self.line_height, - }; - - if let Some(paragraph) = state.paragraphs.get_mut(key) { - paragraph.update(text); - } else { - state.paragraphs.insert(key, crate::Plain::new(text)); - } - } + self.update_entity_paragraph(state, key); } // Diff the context menu @@ -899,9 +911,8 @@ where shell: &mut Shell<'_, Message>, _viewport: &iced::Rectangle, ) -> event::Status { - let bounds = layout.bounds(); + let my_bounds = layout.bounds(); let state = tree.state.downcast_mut::(); - state.hovered = Item::None; let my_id = self.get_drag_id(); @@ -938,7 +949,7 @@ where }, ) if Some(my_id) == *id => { let entity = self - .variant_bounds(state, bounds) + .variant_bounds(state, my_bounds) .filter_map(|item| match item { ItemBounds::Button(entity, bounds) => Some((entity, bounds)), _ => None, @@ -947,7 +958,7 @@ where .map(|(key, _)| key); state.drop_hint = self.drop_hint_for_position( state, - bounds, + my_bounds, Point::new(*x as f32, *y as f32), ); self.emit_drop_hint(shell, state.drop_hint); @@ -979,9 +990,6 @@ where if matches!(leave, OfferEvent::Leave | OfferEvent::LeaveDestination) && Some(my_id) == *id => { - if matches!(leave, OfferEvent::Leave) { - state.dragging_tab = None; - } state.drop_hint = None; self.emit_drop_hint(shell, state.drop_hint); if let Some(Some(entity)) = entity { @@ -1001,7 +1009,7 @@ where "offer motion id={my_id:?} cursor=({x},{y}) current_entity={entity:?}" ); let new = self - .variant_bounds(state, bounds) + .variant_bounds(state, my_bounds) .filter_map(|item| match item { ItemBounds::Button(entity, bounds) => Some((entity, bounds)), _ => None, @@ -1018,11 +1026,15 @@ where ); state.drop_hint = self.drop_hint_for_position( state, - bounds, + my_bounds, Point::new(*x as f32, *y as f32), ); self.emit_drop_hint(shell, state.drop_hint); if Some(Some(new_entity)) != entity { + state.hovered = Item::Tab(new_entity); + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } let prev_action = state .dnd_state .drag_offer @@ -1039,6 +1051,10 @@ where } } } else if entity.is_some() { + state.hovered = Item::None; + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } log::trace!( target: TAB_REORDER_LOG_TARGET, "offer motion leaving id={my_id:?}" @@ -1124,31 +1140,24 @@ where self.emit_drop_hint(shell, state.drop_hint); if let Some(event) = pending_reorder { + state.focused_item = Item::Tab(event.dragged); + state.hovered = Item::None; + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } if let Some(on_reorder) = self.on_reorder.as_ref() { shell.publish(on_reorder(event)); + return event::Status::Captured; } } return ret; - } else { - log::trace!( - target: TAB_REORDER_LOG_TARGET, - "data received without entity id={my_id:?}" - ); - state.drop_hint = None; - - self.emit_drop_hint(shell, state.drop_hint); - if let Some(event) = pending_reorder { - if let Some(on_reorder) = self.on_reorder.as_ref() { - shell.publish(on_reorder(event)); - } - } } } _ => {} } } - if cursor_position.is_over(bounds) { + if cursor_position.is_over(my_bounds) { let fingers_pressed = state.fingers_pressed.len(); match event { @@ -1166,10 +1175,14 @@ where // Check for clicks on the previous and next tab buttons, when tabs are collapsed. if state.collapsed { // Check if the prev tab button was clicked. - if cursor_position.is_over(prev_tab_bounds(&bounds, f32::from(self.button_height))) + if cursor_position + .is_over(prev_tab_bounds(&my_bounds, f32::from(self.button_height))) && self.prev_tab_sensitive(state) { state.hovered = Item::PrevButton; + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) = event { @@ -1178,11 +1191,13 @@ where } else { // Check if the next tab button was clicked. if cursor_position - .is_over(next_tab_bounds(&bounds, f32::from(self.button_height))) + .is_over(next_tab_bounds(&my_bounds, f32::from(self.button_height))) && self.next_tab_sensitive(state) { state.hovered = Item::NextButton; - + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } if let Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. }) = event { @@ -1193,7 +1208,7 @@ where } for (key, bounds) in self - .variant_bounds(state, bounds) + .variant_bounds(state, my_bounds) .filter_map(|item| match item { ItemBounds::Button(entity, bounds) => Some((entity, bounds)), _ => None, @@ -1203,7 +1218,12 @@ where if cursor_position.is_over(bounds) { if self.model.items[key].enabled { // Record that the mouse is hovering over this button. - state.hovered = Item::Tab(key); + if state.hovered != Item::Tab(key) { + state.hovered = Item::Tab(key); + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } + } let close_button_bounds = close_bounds(bounds, f32::from(self.close_icon.size)); @@ -1320,6 +1340,9 @@ where } break; + } else if state.hovered == Item::Tab(key) { + state.hovered = Item::None; + self.update_entity_paragraph(state, key); } } @@ -1377,15 +1400,22 @@ where } } } - } else if state.is_focused() { - // Unfocus on clicks outside of the boundaries of the segmented button. - if is_pressed(&event) { - state.unfocus(); - state.pressed_item = None; - return event::Status::Ignored; + } else { + 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); + } + } + if state.is_focused() { + // Unfocus on clicks outside of the boundaries of the segmented button. + if is_pressed(&event) { + state.unfocus(); + state.pressed_item = None; + return event::Status::Ignored; + } + } else if is_lifted(&event) { + state.pressed_item = None; } - } else if is_lifted(&event) { - state.pressed_item = None; } if let (Some(tab_drag), Some(candidate)) = From b71a7c9edffa6b278836da90106976ded9e90159 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, 28 Jan 2026 00:38:23 +0100 Subject: [PATCH 207/352] improv: remove double coloring of `content_container` windows This sets the main content and the header bar to transparent when `content_container` is true, so that things aren't colored twice and overlayed on top of each other. This ensures that modifying color alpha behaves as expected, especially for frosted glass. --- src/app/mod.rs | 2 +- src/theme/style/iced.rs | 8 +++++++- src/widget/header_bar.rs | 5 +++++ 3 files changed, 13 insertions(+), 2 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 090698df..67636dac 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -689,7 +689,6 @@ impl ApplicationExt for App { .apply(container) .width(iced::Length::Fill) .height(iced::Length::Fill) - .class(crate::theme::Container::WindowBackground) .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_content_container"))) .into() } else { @@ -713,6 +712,7 @@ impl ApplicationExt for App { .focused(focused) .maximized(maximized) .sharp_corners(sharp_corners) + .transparent(content_container) .title(&core.window.header_title) .on_drag(crate::Action::Cosmic(Action::Drag)) .on_right_click(crate::Action::Cosmic(Action::ShowWindowMenu)) diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 32309860..937ee388 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -396,6 +396,7 @@ pub enum Container<'a> { HeaderBar { focused: bool, sharp_corners: bool, + transparent: bool, }, List, Primary, @@ -511,6 +512,7 @@ impl iced_container::Catalog for Theme { Container::HeaderBar { focused, sharp_corners, + transparent, } => { let (icon_color, text_color) = if *focused { ( @@ -527,7 +529,11 @@ impl iced_container::Catalog for Theme { iced_container::Style { icon_color: Some(icon_color), text_color: Some(text_color), - background: Some(iced::Background::Color(cosmic.background.base.into())), + background: if *transparent { + None + } else { + Some(iced::Background::Color(cosmic.background.base.into())) + }, border: Border { radius: [ if *sharp_corners { diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index d500bde3..c5bde28f 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -28,6 +28,7 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { is_ssd: false, on_double_click: None, is_condensed: false, + transparent: false, } } @@ -92,6 +93,9 @@ pub struct HeaderBar<'a, Message> { /// Whether the headerbar should be compact is_condensed: bool, + + /// Whether the headerbar should be transparent + transparent: bool, } impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { @@ -412,6 +416,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .class(crate::theme::Container::HeaderBar { focused: self.focused, sharp_corners: self.sharp_corners, + transparent: self.transparent, }) .center_y(Length::Shrink) .apply(widget::mouse_area); From cf19ac665f353bbca0bad945403976ccdf6c8191 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, 28 Jan 2026 00:49:48 +0100 Subject: [PATCH 208/352] chore: update dependencies --- Cargo.toml | 16 ++++++++-------- cosmic-config-derive/Cargo.toml | 4 ++-- cosmic-config-derive/src/lib.rs | 4 ++-- cosmic-config/Cargo.toml | 6 +++--- cosmic-theme/Cargo.toml | 8 ++++---- 5 files changed, 19 insertions(+), 19 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 46091bcc..feaa8c74 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,8 +1,8 @@ [package] name = "libcosmic" -version = "0.1.0" +version = "1.0.0" edition = "2024" -rust-version = "1.85" +rust-version = "1.90" [lib] name = "cosmic" @@ -104,7 +104,7 @@ 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 } -chrono = "0.4.42" +chrono = "0.4.43" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } # Internationalization @@ -126,26 +126,26 @@ log = "0.4" mime = { version = "0.3.17", optional = true } palette = "0.7.6" raw-window-handle = "0.6" -rfd = { version = "0.15.4", default-features = false, features = [ +rfd = { version = "0.16.0", default-features = false, features = [ "xdg-portal", ], optional = true } rustix = { version = "1.1", features = ["pipe", "process"], optional = true } serde = { version = "1.0.228", features = ["derive"] } slotmap = "1.1.1" smol = { version = "2.0.2", optional = true } -thiserror = "2.0.17" +thiserror = "2.0.18" taffy = { version = "0.9.2", features = ["grid"] } tokio = { version = "1.49.0", optional = true } tracing = "0.1.44" unicode-segmentation = "1.12" url = "2.5.8" -zbus = { version = "5.13.1", default-features = false, optional = true } +zbus = { version = "5.13.2", default-features = false, optional = true } # 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.1", default-features = false } +zbus = { version = "5.13.2", default-features = false } [target.'cfg(unix)'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } @@ -209,7 +209,7 @@ git = "https://github.com/pop-os/cosmic-panel" optional = true [dependencies.ron] -version = "0.11" +version = "0.12" optional = true [workspace] diff --git a/cosmic-config-derive/Cargo.toml b/cosmic-config-derive/Cargo.toml index 55eeb871..9d5f4b88 100644 --- a/cosmic-config-derive/Cargo.toml +++ b/cosmic-config-derive/Cargo.toml @@ -1,7 +1,7 @@ [package] name = "cosmic-config-derive" -version = "0.1.0" -edition = "2021" +version = "1.0.0" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [lib] diff --git a/cosmic-config-derive/src/lib.rs b/cosmic-config-derive/src/lib.rs index 668154cd..cc19a91e 100644 --- a/cosmic-config-derive/src/lib.rs +++ b/cosmic-config-derive/src/lib.rs @@ -106,7 +106,7 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { }) }); - let gen = quote! { + let generate = quote! { impl CosmicConfigEntry for #name { const VERSION: u64 = #version; @@ -147,5 +147,5 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { } }; - gen.into() + generate.into() } diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 78d671ca..6103c15e 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cosmic-config" -version = "0.1.0" +version = "1.0.0" edition = "2024" [features] @@ -11,11 +11,11 @@ subscription = ["iced_futures"] [dependencies] cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } -zbus = { version = "5.13.1", default-features = false, optional = true } +zbus = { version = "5.13.2", default-features = false, optional = true } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } calloop = { version = "0.14.3", optional = true } notify = "8.2.0" -ron = "0.11.0" +ron = "0.12.0" serde = "1.0.228" cosmic-config-derive = { path = "../cosmic-config-derive/", optional = true } iced = { path = "../iced/", default-features = false, optional = true } diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 10b548b4..cf6afe74 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "cosmic-theme" -version = "0.1.0" +version = "1.0.0" edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html @@ -21,11 +21,11 @@ serde = { version = "1.0.228", features = ["derive"] } serde_json = { version = "1.0.149", optional = true, features = [ "preserve_order", ] } -ron = "0.11.0" -csscolorparser = { version = "0.7.2", features = ["serde"] } +ron = "0.12.0" +csscolorparser = { version = "0.8.1", features = ["serde"] } cosmic-config = { path = "../cosmic-config/", default-features = false, features = [ "subscription", "macro", ] } dirs.workspace = true -thiserror = "2.0.17" +thiserror = "2.0.18" From fdcba7d8ececc35c09a7871b018930f752ac784b Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 28 Jan 2026 18:04:55 -0500 Subject: [PATCH 209/352] fix(segmented_button): dnd hover --- src/widget/segmented_button/widget.rs | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 4f68b3de..e4f416bf 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -966,6 +966,13 @@ where target: TAB_REORDER_LOG_TARGET, "offer enter id={my_id:?} entity={entity:?} @ ({x},{y}) mimes={mime_types:?}" ); + // force hovered state update + if let Some(entity) = entity { + state.hovered = Item::Tab(entity); + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } + } let on_dnd_enter = self .on_dnd_enter @@ -1001,6 +1008,10 @@ where target: TAB_REORDER_LOG_TARGET, "offer leave id={my_id:?} entity={entity:?}" ); + state.hovered = Item::None; + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } _ = state.dnd_state.on_leave::(None); } DndEvent::Offer(id, OfferEvent::Motion { x, y }) if Some(my_id) == *id => { From 3e78eb238159d90956e85e95e868164671b649f6 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Fri, 30 Jan 2026 21:07:58 +0100 Subject: [PATCH 210/352] i18n: translation updates from weblate Co-authored-by: Hafidz Nasruddin Co-authored-by: Hosted Weblate Co-authored-by: Languages add-on Co-authored-by: Zahid Rizky Fakhri Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/id/ Translation: Pop OS/libcosmic --- i18n/id/libcosmic.ftl | 27 +++++++++++++++++++++++++++ i18n/ms/libcosmic.ftl | 0 i18n/uz/libcosmic.ftl | 0 3 files changed, 27 insertions(+) create mode 100644 i18n/ms/libcosmic.ftl create mode 100644 i18n/uz/libcosmic.ftl diff --git a/i18n/id/libcosmic.ftl b/i18n/id/libcosmic.ftl index e69de29b..2ce82dab 100644 --- a/i18n/id/libcosmic.ftl +++ b/i18n/id/libcosmic.ftl @@ -0,0 +1,27 @@ +close = Tutup +license = Lisensi +links = Tautan +developers = Pengembang +designers = Perancang +artists = Artis +translators = Penerjemah +documenters = Dokumenter +january = Januari { $year } +february = Februari { $year } +march = Maret { $year } +april = April { $year } +may = Mei { $year } +june = Juni { $year } +july = Juli { $year } +august = Agustus { $year } +september = September { $year } +october = Oktober { $year } +november = November { $year } +december = Desember { $year } +monday = Sen +tuesday = Sel +wednesday = Rab +sunday = Min +saturday = Sab +friday = Jum +thursday = Kam diff --git a/i18n/ms/libcosmic.ftl b/i18n/ms/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/uz/libcosmic.ftl b/i18n/uz/libcosmic.ftl new file mode 100644 index 00000000..e69de29b From 30a02ec0bb3cccabb664572d98a77740ab56c2fe Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sat, 7 Feb 2026 22:08:52 +0100 Subject: [PATCH 211/352] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aliaksandr Truš Co-authored-by: Drugi Sapog Co-authored-by: Hosted Weblate Co-authored-by: Quentin PAGÈS Co-authored-by: jickson john Co-authored-by: jonnysemon Co-authored-by: Димко Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ar/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/be/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/uk/ Translation: Pop OS/libcosmic --- i18n/ar/libcosmic.ftl | 21 ++++++++++++++++++++- i18n/be/libcosmic.ftl | 19 +++++++++++++++++++ i18n/ml/libcosmic.ftl | 0 i18n/oc/libcosmic.ftl | 0 i18n/uk/libcosmic.ftl | 2 +- 5 files changed, 40 insertions(+), 2 deletions(-) create mode 100644 i18n/ml/libcosmic.ftl create mode 100644 i18n/oc/libcosmic.ftl diff --git a/i18n/ar/libcosmic.ftl b/i18n/ar/libcosmic.ftl index 428bd892..ce3eb1e8 100644 --- a/i18n/ar/libcosmic.ftl +++ b/i18n/ar/libcosmic.ftl @@ -4,7 +4,26 @@ close = أغلِق license = الترخيص links = الروابط developers = المطورون -designers = المصممون +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 = الاثنين +tuesday = الثلاثاء +wednesday = الأربعاء +thursday = الخميس +friday = الجمعة +saturday = السبت +sunday = الأحد diff --git a/i18n/be/libcosmic.ftl b/i18n/be/libcosmic.ftl index eb3abf33..1682a174 100644 --- a/i18n/be/libcosmic.ftl +++ b/i18n/be/libcosmic.ftl @@ -6,3 +6,22 @@ designers = Дызайнеры artists = Мастакі translators = Перакладчыкі documenters = Дакументалісты +february = Люты { $year } +november = Лістапад { $year } +friday = Пт +tuesday = Аў +may = Май { $year } +wednesday = Ср +april = Красавік { $year } +monday = Пн +december = Снежань { $year } +sunday = Нд +march = Сакавік { $year } +june = Чэрвень { $year } +saturday = Сб +august = Жнівень { $year } +july = Ліпень { $year } +thursday = Чц +september = Верасень { $year } +october = Кастрычнік { $year } +january = Студзень { $year } diff --git a/i18n/ml/libcosmic.ftl b/i18n/ml/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/oc/libcosmic.ftl b/i18n/oc/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/uk/libcosmic.ftl b/i18n/uk/libcosmic.ftl index 73278ae4..d82c2a6e 100644 --- a/i18n/uk/libcosmic.ftl +++ b/i18n/uk/libcosmic.ftl @@ -2,7 +2,7 @@ close = Закрити # About license = Ліцензія -links = Посилання +links = Ланки developers = Розробники designers = Дизайнери artists = Художники From a3cf875793aa56bda4963bd5eaa8877a4d3aefb0 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 9 Feb 2026 22:04:13 +0100 Subject: [PATCH 212/352] fix(single-instance): unminimize main window on dbus activate --- src/app/cosmic.rs | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index ae554846..803a56bd 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -369,7 +369,16 @@ where crate::Action::Cosmic(message) => self.cosmic_update(message), crate::Action::None => iced::Task::none(), #[cfg(feature = "single-instance")] - crate::Action::DbusActivation(message) => self.app.dbus_activation(message), + crate::Action::DbusActivation(message) => { + let mut task = self.app.dbus_activation(message); + + if let Some(id) = self.app.core().main_window_id() { + let unminimize = iced_runtime::window::minimize::<()>(id, false); + task = task.chain(unminimize.discard()); + } + + task + } }; #[cfg(all(target_env = "gnu", not(target_os = "windows")))] From ae830ca21dd9f2da3c1f4a1617daeec126d3867e Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 12 Feb 2026 15:52:40 +0100 Subject: [PATCH 213/352] perf(font): use RwLock when getting fonts instead of Mutex --- src/config/mod.rs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 1253ce8d..5a96a5e1 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -8,7 +8,7 @@ use cosmic_config::cosmic_config_derive::CosmicConfigEntry; use cosmic_config::{Config, CosmicConfigEntry}; use serde::{Deserialize, Serialize}; use std::collections::BTreeSet; -use std::sync::{LazyLock, Mutex, RwLock}; +use std::sync::{LazyLock, RwLock}; /// ID for the `CosmicTk` config. pub const ID: &str = "com.system76.CosmicTk"; @@ -17,7 +17,7 @@ const MONO_FAMILY_DEFAULT: &str = "Noto Sans Mono"; const SANS_FAMILY_DEFAULT: &str = "Open Sans"; /// Stores static strings of the family names for `iced::Font` compatibility. -pub static FAMILY_MAP: LazyLock>> = LazyLock::new(Mutex::default); +pub static FAMILY_MAP: LazyLock>> = LazyLock::new(RwLock::default); pub static COSMIC_TK: LazyLock> = LazyLock::new(|| { RwLock::new( @@ -156,14 +156,14 @@ pub struct FontConfig { impl From for iced::Font { fn from(font: FontConfig) -> Self { - let mut family_map = FAMILY_MAP.lock().unwrap(); - - let name: &'static str = family_map + let name = FAMILY_MAP + .read() + .unwrap() .get(font.family.as_str()) .copied() .unwrap_or_else(|| { - let value = font.family.clone().leak(); - family_map.insert(value); + let value: &'static str = font.family.clone().leak(); + FAMILY_MAP.write().unwrap().insert(value); value }); From 031818c6b08d706459f41a793e99338dd922bdbe Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 13 Feb 2026 18:30:14 +0100 Subject: [PATCH 214/352] fix(font): explicitly drop read guard in on font family lookup --- src/config/mod.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 5a96a5e1..9807961c 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -16,9 +16,6 @@ pub const ID: &str = "com.system76.CosmicTk"; const MONO_FAMILY_DEFAULT: &str = "Noto Sans Mono"; const SANS_FAMILY_DEFAULT: &str = "Open Sans"; -/// Stores static strings of the family names for `iced::Font` compatibility. -pub static FAMILY_MAP: LazyLock>> = LazyLock::new(RwLock::default); - pub static COSMIC_TK: LazyLock> = LazyLock::new(|| { RwLock::new( CosmicTk::config() @@ -156,16 +153,19 @@ pub struct FontConfig { impl From for iced::Font { fn from(font: FontConfig) -> Self { - let name = FAMILY_MAP - .read() - .unwrap() - .get(font.family.as_str()) - .copied() - .unwrap_or_else(|| { - let value: &'static str = font.family.clone().leak(); - FAMILY_MAP.write().unwrap().insert(value); - value - }); + /// Stores static strings of the family names for `iced::Font` compatibility. + static FAMILY_MAP: LazyLock>> = + LazyLock::new(RwLock::default); + + let read_guard = FAMILY_MAP.read().unwrap(); + let name: Option<&'static str> = read_guard.get(font.family.as_str()).copied(); + drop(read_guard); + + let name = name.unwrap_or_else(|| { + let value: &'static str = font.family.clone().leak(); + FAMILY_MAP.write().unwrap().insert(value); + value + }); Self { family: iced::font::Family::Name(name), From ae1f15f37ee6d1fde579c6a6557e44b1a208f95e Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 13 Feb 2026 12:36:03 -0700 Subject: [PATCH 215/352] Add pull request template --- .github/PULL_REQUEST_TEMPLATE.md | 8 ++++++++ 1 file changed, 8 insertions(+) create mode 100644 .github/PULL_REQUEST_TEMPLATE.md diff --git a/.github/PULL_REQUEST_TEMPLATE.md b/.github/PULL_REQUEST_TEMPLATE.md new file mode 100644 index 00000000..e6ca28bc --- /dev/null +++ b/.github/PULL_REQUEST_TEMPLATE.md @@ -0,0 +1,8 @@ +- [ ] I have disclosed use of any AI generated code in my commit messages. + - If you are using an LLM, and do not fully understand the changes it is making to the code base, do not create a PR. + - In our experience, AI generated code often results in overly complex code that lacks enough context for a proper fix or feature inclusion. This results in considerably longer code reviews. Due to this, AI authored or partially authored PRs may be closed without comment. +- [ ] I understand these changes in full and will be able to respond to review comments. +- [ ] My change is accurately described in the commit message. +- [ ] My contribution is tested and working as described. +- [ ] I have read the [Developer Certificate of Origin](https://developercertificate.org/) and certify my contribution under its conditions. + From 21c5a4f34a33795d7836ff673a360ef1472f7567 Mon Sep 17 00:00:00 2001 From: Frieder Hannenheim Date: Mon, 16 Feb 2026 15:41:35 +0000 Subject: [PATCH 216/352] feat(dnd_destination): xdg file transfer portal support Requires the `xdg-portal` feature to be enabled to use these features. - Adds `DndDestination::on_file_transfer` method to handle `application/vnd.portal.filetransfer` drop requests - Adds `command::file_transfer_receive` function to handle the file transfer request messages - Adds `command::file_transfer_send` to initiate a file transfer from the application --- src/command.rs | 24 ++++++++++++++++++++++++ src/widget/dnd_destination.rs | 31 +++++++++++++++++++++++++++++++ 2 files changed, 55 insertions(+) diff --git a/src/command.rs b/src/command.rs index 73c900c1..14d326b4 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,6 +1,9 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 +#[cfg(feature = "xdg-portal")] +use std::os::fd::AsFd; + use iced::window; /// Initiates a window drag. @@ -43,3 +46,24 @@ pub fn set_windowed(id: window::Id) -> iced::Task> { pub fn toggle_maximize(id: window::Id) -> iced::Task> { iced_runtime::window::toggle_maximize(id) } + +#[cfg(feature = "xdg-portal")] +pub fn file_transfer_send(writeable: bool, auto_stop: bool, files: Vec) -> iced::Task> { + iced::Task::future(async move { + let file_transfer = ashpd::documents::FileTransfer::new().await?; + let key = file_transfer.start_transfer(writeable, auto_stop).await?; + file_transfer.add_files(&key, &files).await?; + Ok(key) + }) +} + +/// Receive the files offered over the xdg share portal using the `key`. +/// Returns a list of file paths. +#[cfg(feature = "xdg-portal")] +pub fn file_transfer_receive(key: String) -> iced::Task>> { + dbg!(&key); + iced::Task::future(async move { + let file_transfer = ashpd::documents::FileTransfer::new().await?; + file_transfer.retrieve_files(&key).await + }) +} diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 947d2fe3..a32a9fba 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -40,6 +40,8 @@ pub fn dnd_destination_for_data<'a, T: AllowedMimeTypes, Message: 'static>( static DRAG_ID_COUNTER: AtomicU64 = AtomicU64::new(0); const DND_DEST_LOG_TARGET: &str = "libcosmic::widget::dnd_destination"; +#[cfg(feature = "xdg-portal")] +pub const FILE_TRANSFER_MIME: &str = "application/vnd.portal.filetransfer"; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct DragId(pub u128); @@ -73,6 +75,8 @@ pub struct DndDestination<'a, Message> { on_action_selected: Option Message>>, on_data_received: Option) -> Message>>, on_finish: Option, DndAction, f64, f64) -> Message>>, + #[cfg(feature = "xdg-portal")] + on_file_transfer: Option Message>>, } impl<'a, Message: 'static> DndDestination<'a, Message> { @@ -99,6 +103,8 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { on_action_selected: None, on_data_received: None, on_finish: None, + #[cfg(feature = "xdg-portal")] + on_file_transfer: None, } } @@ -124,6 +130,8 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { on_finish: Some(Box::new(move |mime, data, action, _, _| { on_finish(T::try_from((data, mime)).ok(), action) })), + #[cfg(feature = "xdg-portal")] + on_file_transfer: None, } } @@ -159,6 +167,8 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { on_action_selected: None, on_data_received: None, on_finish: None, + #[cfg(feature = "xdg-portal")] + on_file_transfer: None, } } @@ -237,6 +247,20 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { self } + /// Add a message that will be emitted instead of [`on_data_received`](Self::on_data_received) if the dropped files + /// are offered through the xdg share portal. You can then use [`crate::command::file_transfer_receive`] + /// with the key to receive the files. + #[cfg(feature = "xdg-portal")] + #[must_use] + pub fn on_file_transfer(mut self, f: impl Fn(String) -> Message + 'static) -> Self { + match self.mime_types.iter().position(|v| v == "text/uri-list") { + Some(i) => self.mime_types.insert(i, Cow::Borrowed(FILE_TRANSFER_MIME)), + None => self.mime_types.push(Cow::Borrowed(FILE_TRANSFER_MIME)), + } + self.on_file_transfer = Some(Box::new(f)); + self + } + /// Returns the drag id of the destination. /// /// # Panics @@ -496,6 +520,13 @@ impl Widget "offer data id={my_id:?} mime={mime_type:?} bytes={}", data.len() ); + + #[cfg(feature = "xdg-portal")] + if mime_type == FILE_TRANSFER_MIME && let Some(f) = self.on_file_transfer.as_ref() && let Ok(s) = String::from_utf8(data[..data.len() - 1].to_vec()) { + shell.publish(f(s)); + return event::Status::Captured; + } + if let (Some(msg), ret) = state.on_data_received( mime_type, data, From 6328c40ef763e165f365d9af680912348414d17b Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 16 Feb 2026 16:51:02 +0100 Subject: [PATCH 217/352] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 2db5545f..e2a24417 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 2db5545fbee505c2c643c628a8984d1666c4d451 +Subproject commit e2a2441789a7e302f099c0e8e9493ef81b58e265 From a2e903ad94c6c2728c22454f098a81cb10f212bc Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Tue, 17 Feb 2026 16:39:37 +0000 Subject: [PATCH 218/352] feat(cosmic-theme): add color schemes for qt apps --- cosmic-theme/Cargo.toml | 1 + cosmic-theme/src/model/theme.rs | 3 + cosmic-theme/src/output/gtk4_output.rs | 16 +- cosmic-theme/src/output/mod.rs | 30 ++ cosmic-theme/src/output/qt56ct_output.rs | 113 +++++ cosmic-theme/src/output/qt_output.rs | 517 +++++++++++++++++++++++ 6 files changed, 677 insertions(+), 3 deletions(-) create mode 100644 cosmic-theme/src/output/qt56ct_output.rs create mode 100644 cosmic-theme/src/output/qt_output.rs diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index cf6afe74..80f4805d 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -27,5 +27,6 @@ cosmic-config = { path = "../cosmic-config/", default-features = false, features "subscription", "macro", ] } +configparser = "3.1.0" dirs.workspace = true thiserror = "2.0.18" diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 1f94f5a2..cef479ae 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -690,6 +690,9 @@ impl Theme { let config = Config::new(Self::id(), Self::VERSION).map_err(|e| (vec![e], Self::default()))?; let is_dark = ThemeMode::is_dark(&config).map_err(|e| (vec![e], Self::default()))?; + Self::get_active_with_brightness(is_dark) + } + pub fn get_active_with_brightness(is_dark: bool) -> Result, Self)> { let config = if is_dark { Self::dark_config() } else { diff --git a/cosmic-theme/src/output/gtk4_output.rs b/cosmic-theme/src/output/gtk4_output.rs index 6fdf26d5..40eba5b4 100644 --- a/cosmic-theme/src/output/gtk4_output.rs +++ b/cosmic-theme/src/output/gtk4_output.rs @@ -163,9 +163,19 @@ impl Theme { std::fs::create_dir_all(&config_dir).map_err(OutputError::Io)?; } - let mut file = File::create(config_dir.join(name)).map_err(OutputError::Io)?; - file.write_all(css_str.as_bytes()) - .map_err(OutputError::Io)?; + let file_path = config_dir.join(name); + let tmp_file_path = config_dir.join(name.to_owned() + "~"); + + // 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(css_str.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(()) } diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index 832771d4..61f0e49d 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -6,6 +6,11 @@ use crate::Theme; /// Module for outputting the Cosmic gtk4 theme type as CSS pub mod gtk4_output; +/// Module for configuring qt5ct and qt6ct to use our qt theme +pub mod qt56ct_output; +/// Module for outputting the Cosmic qt theme type as kdeglobals +pub mod qt_output; + pub mod vs_code; #[derive(Error, Debug)] @@ -14,32 +19,57 @@ pub enum OutputError { Io(std::io::Error), #[error("Missing config directory")] MissingConfigDir, + #[error("Missing data directory")] + MissingDataDir, #[error("Serde Error: {0}")] Serde(#[from] serde_json::Error), + #[error("Ini Error: {0}")] + Ini(String), } impl Theme { #[inline] pub fn apply_exports(&self) -> Result<(), OutputError> { let gtk_res = Theme::apply_gtk(self.is_dark); + let qt_res = Theme::apply_qt(self.is_dark); + let qt56ct_res = Theme::apply_qt56ct(self.is_dark); let vs_res = self.clone().apply_vs_code(); gtk_res?; + qt_res?; + qt56ct_res?; vs_res?; Ok(()) } + #[inline] + /// To avoid rewriting too much code, I replaced calls to `Theme::apply_gtk` with this. + /// Note that vscode isn't touched by this function. + pub fn apply_exports_static(is_dark: bool) -> Result<(), OutputError> { + let gtk_res = Theme::apply_gtk(is_dark); + let qt_res = Theme::apply_qt(is_dark); + let qt56ct_res = Theme::apply_qt56ct(is_dark); + gtk_res?; + qt_res?; + qt56ct_res?; + Ok(()) + } + #[inline] pub fn write_exports(&self) -> Result<(), OutputError> { let gtk_res = self.write_gtk4(); + let qt_res = self.write_qt(); gtk_res?; + qt_res?; Ok(()) } #[inline] pub fn reset_exports() -> Result<(), OutputError> { let gtk_res = Theme::reset_gtk(); + let qt_res = Theme::reset_qt(); let vs_res = Theme::reset_vs_code(); gtk_res?; + qt_res?; vs_res?; Ok(()) } diff --git a/cosmic-theme/src/output/qt56ct_output.rs b/cosmic-theme/src/output/qt56ct_output.rs new file mode 100644 index 00000000..d4736597 --- /dev/null +++ b/cosmic-theme/src/output/qt56ct_output.rs @@ -0,0 +1,113 @@ +use crate::Theme; +use configparser::ini::Ini; +use std::{ + fs::{self, File}, + path::PathBuf, +}; + +use super::OutputError; + +impl Theme { + /// The "version" of this theme. + /// + /// To avoid repeatedly overwriting the user's config, we use a version system. + /// + /// 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; + + /// Edits qt{5,6}ct.conf to use COSMIC styles if needed. + #[cold] + pub fn apply_qt56ct(is_dark: bool) -> Result<(), OutputError> { + let qt5ct_res = Self::apply_ct("qt5ct", is_dark); + let qt6ct_res = Self::apply_ct("qt6ct", is_dark); + qt5ct_res?; + qt6ct_res?; + Ok(()) + } + #[must_use] + #[cold] + fn apply_ct(ct: &str, is_dark: bool) -> 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(); + + let color_scheme_path = Self::get_qt_colors_path(is_dark)?; + let icon_theme = if is_dark { "breeze-dark" } else { "breeze" }; + + ini.set( + "Appearance", + "cosmic_qt_version", + Some(Theme::COSMIC_QT_VERSION.to_string()), + ); + + if old_version < Theme::COSMIC_QT_VERSION { + // Config is outdated, update it unconditionally! + + ini.setstr( + "Appearance", + "color_scheme_path", + color_scheme_path.to_str(), + ); + // Enable the above color scheme, instead of using the default color scheme of e.g. Breeze + ini.setstr("Appearance", "custom_palette", Some("true")); + // COSMIC icons are stuck in light mode, so use breeze icons instead + ini.setstr("Appearance", "icon_theme", Some(icon_theme)); + // Use COSMIC dialogs instead of KDE's + ini.setstr("Appearance", "standard_dialogs", Some("xdgdesktopportal")); + + // TODO: Add fonts section to match COSMIC + } else { + // Config is not outdated, check before updating light/dark mode only! + + let old_color_scheme_path = ini + .get("Appearance", "color_scheme_path") + .unwrap_or_else(|| "CosmicPlease".to_owned()); + if old_color_scheme_path.contains("Cosmic") { + ini.setstr( + "Appearance", + "color_scheme_path", + color_scheme_path.to_str(), + ); + } + + let old_icon_theme = ini + .get("Appearance", "icon_theme") + .unwrap_or_else(|| "breeze".to_owned()); + if old_icon_theme.contains("breeze") { + ini.setstr("Appearance", "icon_theme", Some(icon_theme)); + } + } + + ini.write(path).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. + fn get_conf_path(ct: &str) -> Result { + let Some(mut config_dir) = dirs::config_dir() else { + return Err(OutputError::MissingConfigDir); + }; + config_dir.push(&ct); + if !config_dir.exists() { + fs::create_dir_all(&config_dir).map_err(OutputError::Io)?; + } + + let file_path = config_dir.join(ct.to_owned() + ".conf"); + if !file_path.exists() { + File::create_new(&file_path).map_err(OutputError::Io)?; + } + + Ok(file_path) + } +} diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs new file mode 100644 index 00000000..0d9a4258 --- /dev/null +++ b/cosmic-theme/src/output/qt_output.rs @@ -0,0 +1,517 @@ +use crate::Theme; +use configparser::ini::Ini; +use palette::{Mix, Srgba, blend::Compose}; +use std::{ + fs::{self, File}, + io::{self, Write}, + path::{Path, PathBuf}, +}; + +use super::OutputError; + +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/ + #[must_use] + #[cold] + pub fn as_qt(&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, + color_amount: 0.0, + color_effect: ColorEffect::Desaturate, + contrast_amount: 0.65, + contrast_effect: ColorEffect::Fade, + intensity_amount: 0.1, + intensity_effect: IntensityEffect::Lighten, + }; + // Usually, inactive elements will have reduced contrast (text fades slightly into the background) and may have slightly reduced intensity + let inactive_color_effects = IniColorEffects { + color: self.palette.gray_1, + color_amount: 0.025, + color_effect: ColorEffect::Tint, + contrast_amount: 0.1, + contrast_effect: ColorEffect::Tint, + intensity_amount: 0.0, + intensity_effect: IntensityEffect::Shade, + }; + + let bg = self.background.base; + // the background container + let view_colors = IniColors { + background_alternate: bg.mix(self.accent.base, 0.05), + background_normal: bg, + decoration_focus: self.accent_text_color(), + decoration_hover: self.accent_text_color(), + foreground_active: self.accent_text_color(), + foreground_inactive: self.background.on.mix(bg, 0.1), + foreground_link: self.link_button.base, + foreground_negative: self.destructive_text_color(), + foreground_neutral: self.warning_text_color(), + foreground_normal: self.background.on, + foreground_positive: self.success_text_color(), + foreground_visited: self.accent_text_color(), + }; + // components inside the background container + let window_colors = IniColors { + background_alternate: self.background.component.base.mix(self.accent.base, 0.05), + background_normal: self.background.component.base, + ..view_colors + }; + + // selected text and items + let selection_colors = { + let selected = self.background.component.selected; + let selected_text = self.background.component.selected_text; + IniColors { + background_alternate: selected.mix(bg, 0.5), + background_normal: selected, + decoration_focus: selected, + decoration_hover: selected, + foreground_active: selected_text, + foreground_inactive: selected_text.mix(selected, 0.5), + foreground_link: self.link_button.on, + foreground_negative: self.destructive_color(), + foreground_neutral: self.warning_color(), + foreground_normal: selected_text, + foreground_positive: self.success_color(), + foreground_visited: self.accent_color(), + } + }; + + let button_colors = IniColors { + background_alternate: self.accent_button.base, + background_normal: self.button.base, + ..view_colors + }; + + // Complementary: Areas of applications with an alternative color scheme; usually with a dark background for light color schemes. + let complementary_colors = { + let dark = if self.is_dark { + self.clone() + } else { + Self::get_active_with_brightness(false).unwrap_or_else(|_| self.clone()) + }; + IniColors { + background_alternate: dark.accent.base, + background_normal: dark.background.base, + decoration_focus: dark.accent_text_color(), + decoration_hover: dark.accent_text_color(), + foreground_active: dark.accent_text_color(), + foreground_inactive: dark.background.on.mix(dark.background.base, 0.1), + foreground_link: dark.link_button.base, + foreground_negative: dark.destructive_text_color(), + foreground_neutral: dark.warning_text_color(), + foreground_normal: dark.background.on, + foreground_positive: dark.success_text_color(), + foreground_visited: dark.accent_text_color(), + } + }; + + // headers in cosmic don't have a background + let header_colors = &view_colors; + let header_colors_inactive = &view_colors; + // tool tips, "What's This" tips, and similar elements + let tooltip_colors = &window_colors; + + let general_color_scheme = if self.is_dark { + "CosmicDark" + } else { + "CosmicLight" + }; + let general_name = if self.is_dark { + "COSMIC Dark" + } else { + "COSMIC Light" + }; + // COSMIC icons are stuck in light mode, so use breeze icons instead + let icons_theme = if self.is_dark { + "breeze-dark" + } else { + "breeze" + }; + + format!( + r#"# GENERATED BY COSMIC + +[ColorEffects:Disabled] +{} + +[ColorEffects:Inactive] +ChangeSelectionColor=false +Enable=false +{} + +[Colors:Button] +{} + +[Colors:Complementary] +{} + +[Colors:Header] +{} + +[Colors:Header][Inactive] +{} + +[Colors:Selection] +{} + +[Colors:Tooltip] +{} + +[Colors:View] +{} + +[Colors:Window] +{} + +[General] +ColorScheme={general_color_scheme} +Name={general_name} +shadeSortColumn=true + +[Icons] +Theme={icons_theme} + +[KDE] +contrast=4 +widgetStyle=qt6ct-style + +[WM] +{} +"#, + format_ini_color_effects(&disabled_color_effects, bg), + format_ini_color_effects(&inactive_color_effects, bg), + format_ini_colors(&button_colors, bg), + format_ini_colors(&complementary_colors, bg), + format_ini_colors(&header_colors, bg), + format_ini_colors(&header_colors_inactive, bg), + format_ini_colors(&selection_colors, bg), + 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), + ) + } + + /// Write the color scheme to the appropriate directory. + /// Should be written in `~/.local/share/color-schemes/`. + /// + /// See the docs: https://develop.kde.org/docs/plasma/#color-scheme + /// + /// # Errors + /// + /// 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 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()) + .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(()) + } + + /// Apply the color scheme by copying its values to `~/.config/kdeglobals`. + /// + /// See the docs: https://develop.kde.org/docs/plasma/#color-scheme + /// + /// # Errors + /// + /// Returns an `OutputError` if there is an error applying the color scheme. + #[cold] + pub fn apply_qt(is_dark: bool) -> Result<(), OutputError> { + let Some(config_dir) = dirs::config_dir() else { + return Err(OutputError::MissingConfigDir); + }; + 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_ini = Self::read_ini(&src_file)?; + + Self::backup_non_cosmic_kdeglobals(&kdeglobals_ini, &kdeglobals_file) + .map_err(OutputError::Io)?; + + for (section, key_value) in src_ini.get_map_ref() { + for (key, value) in key_value { + kdeglobals_ini.set(section, key, value.clone()); + } + } + + kdeglobals_ini + .write(kdeglobals_file) + .map_err(OutputError::Io)?; + Ok(()) + } + + /// Reset the applied qt colors by removing color scheme values from the + /// `~/.config/kdeglobals` file. + /// + /// This does not restore the backed up kdeglobals file. + /// + /// # Errors + /// + /// Returns an `OutputError` if there is an error resetting the CSS file. + #[cold] + pub fn reset_qt() -> Result<(), OutputError> { + let Some(config_dir) = dirs::config_dir() else { + return Err(OutputError::MissingConfigDir); + }; + let kdeglobals_file = config_dir.join("kdeglobals"); + let mut kdeglobals_ini = Self::read_ini(&kdeglobals_file)?; + + if !Self::is_cosmic_kdeglobals(&kdeglobals_ini) + .map_err(OutputError::Io)? + .unwrap_or_default() + { + // Not a cosmic kdeglobals file, do nothing + return Ok(()); + } + + 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_ini = Self::read_ini(&src_file)?; + + for (section, key_value) in src_ini.get_map_ref() { + for (key, _) in key_value { + kdeglobals_ini.remove_key(section, key); + } + } + + kdeglobals_ini + .write(kdeglobals_file) + .map_err(OutputError::Io)?; + Ok(()) + } + + /// Gets a path like `~/.config/color-schemes/CosmicDark.colors` + pub fn get_qt_colors_path(is_dark: bool) -> Result { + let Some(mut data_dir) = dirs::data_dir() else { + return Err(OutputError::MissingDataDir); + }; + + let file_name = if is_dark { + "CosmicDark.colors" + } else { + "CosmicLight.colors" + }; + + data_dir.push("color-schemes"); + if !data_dir.exists() { + std::fs::create_dir_all(&data_dir).map_err(OutputError::Io)?; + } + + Ok(data_dir.join(file_name)) + } + + #[cold] + fn read_ini(path: &PathBuf) -> Result { + let mut ini = Ini::new_cs(); + if !path.exists() { + return Ok(ini); + } + let file_content = fs::read_to_string(path).map_err(OutputError::Io)?; + ini.read(file_content).map_err(OutputError::Ini)?; + Ok(ini) + } + + #[cold] + fn backup_non_cosmic_kdeglobals(ini: &Ini, path: &Path) -> io::Result<()> { + if !Self::is_cosmic_kdeglobals(&ini)?.unwrap_or(true) { + let backup_path = path.with_extension("bak"); + fs::rename(path, &backup_path)?; + } + Ok(()) + } + + #[cold] + fn is_cosmic_kdeglobals(ini: &Ini) -> io::Result> { + let color_scheme = ini.get("General", "ColorScheme"); + if let Some(color_scheme) = color_scheme { + Ok(Some( + color_scheme == "CosmicDark" || color_scheme == "CosmicLight", + )) + } else { + Ok(None) + } + } +} + +/// Formats a color in the form `r,g,b` e.g. `255,255,255`. +/// If the color has transparency, it is mixed with bg first. +fn to_rgb(c: Srgba, bg: Srgba) -> String { + let c_u8: Srgba = c.over(bg).into_format(); + format!("{},{},{}", c_u8.red, c_u8.green, c_u8.blue) +} + +fn format_ini_color_effects(color_effects: &IniColorEffects, bg: Srgba) -> String { + format!( + r#"Color={} +ColorAmount={} +ColorEffect={} +ContrastAmount={} +ContrastEffect={} +IntensityAmount={} +IntensityEffect={}"#, + to_rgb(color_effects.color, bg), + color_effects.color_amount, + color_effects.color_effect.as_u8(), + color_effects.contrast_amount, + color_effects.contrast_effect.as_u8(), + color_effects.intensity_amount, + color_effects.intensity_effect.as_u8(), + ) +} + +fn format_ini_colors(colors: &IniColors, bg: Srgba) -> String { + format!( + r#"BackgroundAlternate={} +BackgroundNormal={} +DecorationFocus={} +DecorationHover={} +ForegroundActive={} +ForegroundInactive={} +ForegroundLink={} +ForegroundNegative={} +ForegroundNeutral={} +ForegroundNormal={} +ForegroundPositive={} +ForegroundVisited={}"#, + to_rgb(colors.background_alternate, bg), + to_rgb(colors.background_normal, bg), + to_rgb(colors.decoration_focus, bg), + to_rgb(colors.decoration_hover, bg), + to_rgb(colors.foreground_active, bg), + to_rgb(colors.foreground_inactive, bg), + to_rgb(colors.foreground_link, bg), + to_rgb(colors.foreground_negative, bg), + to_rgb(colors.foreground_neutral, bg), + to_rgb(colors.foreground_normal, bg), + to_rgb(colors.foreground_positive, bg), + to_rgb(colors.foreground_visited, bg), + ) +} + +/// Sets the colors for the titlebars of active and inactive windows. +fn format_ini_wm_colors(view_colors: &IniColors, is_dark: bool) -> String { + let bg = view_colors.background_normal; + let fg = view_colors.foreground_active; + let blend = if is_dark { fg } else { bg }; + + format!( + r#"activeBackground={} +activeBlend={} +activeForeground={} +inactiveBackground={} +inactiveBlend={} +inactiveForeground={}"#, + to_rgb(bg, bg), + to_rgb(blend, bg), + to_rgb(fg, bg), + to_rgb(bg, bg), + to_rgb(blend, bg), + to_rgb(fg, bg), + ) +} + +struct IniColorEffects { + color: Srgba, + color_amount: f32, + color_effect: ColorEffect, + contrast_amount: f32, + /// Applied to the text, using the background as the reference color. + contrast_effect: ColorEffect, + intensity_amount: f32, + intensity_effect: IntensityEffect, +} +/// Each color set is made up of a number of roles which are available in all other sets. +/// In addition, except for Inactive Text, there is a corresponding background role for each of the text roles. Currently (except for Normal and Alternate Background), these colors are not chosen here but are automatically determined based on Normal Background and the corresponding Text color. +struct IniColors { + /// used when there is a need to subtly change the background to aid in item association. This might be used e.g. as the background of a heading, but is mostly used for alternating rows in lists, especially multi-column lists, to aid in visually tracking rows. + background_alternate: Srgba, + /// Normal background + background_normal: Srgba, + /// Used for drawing lines or shading UI elements to indicate the item which has active input focus. + /// Typically the same as foreground_active. + decoration_focus: Srgba, + /// Used for drawing lines or shading UI elements for mouse-over effects, e.g. the "illumination" effects for buttons. + /// Typically the same as foreground_active. + decoration_hover: Srgba, + /// used to indicate an active element or attract attention, e.g. alerts, notifications; also for hovered hyperlinks + foreground_active: Srgba, + /// used for text which should be unobtrusive, e.g. comments, "subtitles", unimportant information, etc. + foreground_inactive: Srgba, + /// used for hyperlinks or to otherwise indicate "something which may be visited", or to show relationships + foreground_link: Srgba, + /// used for errors, failure notices, notifications that an action may be dangerous (e.g. unsafe web page or security context), etc. + foreground_negative: Srgba, + /// used to draw attention when another role is not appropriate; e.g. warnings, to indicate secure/encrypted content, etc. + foreground_neutral: Srgba, + /// Normal foreground + foreground_normal: Srgba, + /// used for success notices, to indicate trusted content, etc. + foreground_positive: Srgba, + /// used for "something (e.g. a hyperlink) that has been visited", or to indicate something that is "old". + foreground_visited: Srgba, +} + +/// Intensity allows the overall color to be lightened or darkened. +#[allow(dead_code)] +enum IntensityEffect { + /// Makes everything lighter or darker in a controlled manner. + /// + /// intensity_amount increases or decreases the overall intensity (i.e. perceived brightness) by an absolute amount. + Shade, + /// Changes the intensity to a percentage of the initial value. + Darken, + /// Conceptually the opposite of darken; lighten can be thought of as working with "distance from white", where darken works with "distance from black". + Lighten, +} + +impl IntensityEffect { + pub fn as_u8(&self) -> u8 { + match self { + Self::Shade => 0, + Self::Darken => 1, + Self::Lighten => 2, + } + } +} + +/// This also changes the overall color like [IntensityEffect], +/// but is not limited to intensity. +#[allow(dead_code)] +enum ColorEffect { + /// changes the relative chroma + /// + /// This is available for "ColorEffect" but not "ContrastEffect". + Desaturate, + /// smoothly blends the original color into a reference color + Fade, + /// similar to Fade, except that the color (hue and chroma) changes more quickly while the intensity changes more slowly as the amount is increased + Tint, +} + +impl ColorEffect { + pub fn as_u8(&self) -> u8 { + match self { + Self::Desaturate => 0, + Self::Fade => 1, + Self::Tint => 2, + } + } +} From b05f040e5f0dc390af1847caf5f50b6813215795 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 16 Feb 2026 08:03:35 +0100 Subject: [PATCH 219/352] i18n: translation updates from weblate Co-authored-by: Benmak Kizuna Co-authored-by: Fedorov Alexei Co-authored-by: Hosted Weblate Co-authored-by: jonnysemon Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ar/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ru/ Translation: Pop OS/libcosmic --- i18n/ar/libcosmic.ftl | 2 +- i18n/ru/libcosmic.ftl | 19 +++++++++++++++++++ i18n/yue-Hant/libcosmic.ftl | 0 3 files changed, 20 insertions(+), 1 deletion(-) create mode 100644 i18n/yue-Hant/libcosmic.ftl diff --git a/i18n/ar/libcosmic.ftl b/i18n/ar/libcosmic.ftl index ce3eb1e8..92f016f0 100644 --- a/i18n/ar/libcosmic.ftl +++ b/i18n/ar/libcosmic.ftl @@ -3,7 +3,7 @@ close = أغلِق # About license = الترخيص links = الروابط -developers = المطورون +developers = المطوِّرون designers = المصمّمون artists = الفنانون translators = المترجمون diff --git a/i18n/ru/libcosmic.ftl b/i18n/ru/libcosmic.ftl index 0ef03fb1..7fe9b3dc 100644 --- a/i18n/ru/libcosmic.ftl +++ b/i18n/ru/libcosmic.ftl @@ -6,3 +6,22 @@ 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 = Пн +tuesday = Вт +wednesday = Ср +thursday = Чт +friday = Пт +saturday = Сб +sunday = Вс diff --git a/i18n/yue-Hant/libcosmic.ftl b/i18n/yue-Hant/libcosmic.ftl new file mode 100644 index 00000000..e69de29b From 990e2e291b33379f985bae0f81e46292b3d0e9cd 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 Feb 2026 20:32:21 +0100 Subject: [PATCH 220/352] refactor(calendar): use `jiff` instead of `chrono` This refactors the calendar widget to use `jiff` instead of `chrono`. Also mostly matches the design of the widget to the time applet. --- Cargo.toml | 2 +- examples/calendar/Cargo.toml | 6 +- examples/calendar/src/main.rs | 8 +- i18n/en/libcosmic.ftl | 21 ++-- src/widget/calendar.rs | 197 +++++++++++++++------------------- 5 files changed, 111 insertions(+), 123 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index feaa8c74..4aaf9d0a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -104,7 +104,7 @@ 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 } -chrono = "0.4.43" +jiff = "0.2" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } # Internationalization diff --git a/examples/calendar/Cargo.toml b/examples/calendar/Cargo.toml index 59b23c0c..b7286825 100644 --- a/examples/calendar/Cargo.toml +++ b/examples/calendar/Cargo.toml @@ -1,12 +1,12 @@ [package] name = "calendar" -version = "0.1.0" -edition = "2021" +version = "1.0.0" +edition = "2024" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [dependencies] -chrono = "0.4.42" +jiff = "0.2" [dependencies.libcosmic] path = "../../" diff --git a/examples/calendar/src/main.rs b/examples/calendar/src/main.rs index 589bc1ff..240684c6 100644 --- a/examples/calendar/src/main.rs +++ b/examples/calendar/src/main.rs @@ -3,10 +3,10 @@ //! Calendar widget example -use chrono::NaiveDate; use cosmic::app::{Core, Settings, Task}; use cosmic::widget::calendar::CalendarModel; -use cosmic::{executor, iced, ApplicationExt, Element}; +use cosmic::{ApplicationExt, Element, executor, iced}; +use jiff::civil::{Date, Weekday}; /// Runs application with these settings #[rustfmt::skip] @@ -19,7 +19,7 @@ fn main() -> Result<(), Box> { /// Messages that are used specifically by our [`App`]. #[derive(Clone, Debug)] pub enum Message { - DateSelected(NaiveDate), + DateSelected(Date), PrevMonth, NextMonth, } @@ -92,7 +92,7 @@ impl cosmic::Application for App { |date| Message::DateSelected(date), || Message::PrevMonth, || Message::NextMonth, - chrono::Weekday::Sun, + Weekday::Sunday, ); content = content.push(calendar); diff --git a/i18n/en/libcosmic.ftl b/i18n/en/libcosmic.ftl index 119ac38e..257fc44f 100644 --- a/i18n/en/libcosmic.ftl +++ b/i18n/en/libcosmic.ftl @@ -23,10 +23,17 @@ september = September { $year } october = October { $year } november = November { $year } december = December { $year } -monday = Mon -tuesday = Tue -wednesday = Wed -thursday = Thu -friday = Fri -saturday = Sat -sunday = Sun +monday = Monday +mon = Mon +tuesday = Tuesday +tue = Tue +wednesday = Wednesday +wed = Wed +thursday = Thursday +thu = Thu +friday = Friday +fri = Fri +saturday = Saturday +sat = Sat +sunday = Sunday +sun = Sun diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index 7ee06204..ea10fddb 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -3,19 +3,20 @@ //! A widget that displays an interactive calendar. -use std::cmp; - use crate::fl; -use crate::iced_core::{Alignment, Length, Padding}; -use crate::widget::{Grid, button, column, grid, icon, row, text}; +use crate::iced_core::{Alignment, Length}; +use crate::widget::{button, column, grid, icon, row, text}; use apply::Apply; -use chrono::{Datelike, Days, Local, Month, Months, NaiveDate, Weekday}; use iced::alignment::Vertical; +use jiff::{ + ToSpan, + civil::{Date, Weekday}, +}; /// A widget that displays an interactive calendar. pub fn calendar( model: &CalendarModel, - on_select: impl Fn(NaiveDate) -> M + 'static, + on_select: impl Fn(Date) -> M + 'static, on_prev: impl Fn() -> M + 'static, on_next: impl Fn() -> M + 'static, first_day_of_week: Weekday, @@ -29,61 +30,40 @@ pub fn calendar( } } -pub fn set_day(date_selected: NaiveDate, day: u32) -> NaiveDate { - let current = date_selected.day(); - - let new_date = match current.cmp(&day) { - cmp::Ordering::Less => date_selected.checked_add_days(Days::new((day - current) as u64)), - - cmp::Ordering::Greater => date_selected.checked_sub_days(Days::new((current - day) as u64)), - - _ => None, - }; - - if let Some(new) = new_date { - new - } else { - date_selected - } +pub fn set_day(date_selected: Date, day: i8) -> Date { + date_selected + .with() + .day(day) + .build() + .unwrap_or(date_selected) } #[derive(Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug, Hash)] pub struct CalendarModel { - pub selected: NaiveDate, - pub visible: NaiveDate, + pub selected: Date, + pub visible: Date, } impl CalendarModel { pub fn now() -> Self { - let now = Local::now(); - let naive_now = NaiveDate::from(now.naive_local()); + let now = jiff::Zoned::now().date(); CalendarModel { - selected: naive_now, - visible: naive_now, + selected: now, + visible: now, } } #[inline] - pub fn new(selected: NaiveDate, visible: NaiveDate) -> Self { + pub fn new(selected: Date, visible: Date) -> Self { CalendarModel { selected, visible } } pub fn show_prev_month(&mut self) { - let prev_month_date = self - .visible - .checked_sub_months(Months::new(1)) - .expect("valid naivedate"); - - self.visible = prev_month_date; + self.visible = self.visible.checked_sub(1.month()).expect("valid date"); } pub fn show_next_month(&mut self) { - let next_month_date = self - .visible - .checked_add_months(Months::new(1)) - .expect("valid naivedate"); - - self.visible = next_month_date; + self.visible = self.visible.checked_add(1.month()).expect("valid date"); } #[inline] @@ -99,7 +79,7 @@ impl CalendarModel { } #[inline] - pub fn set_selected_visible(&mut self, selected: NaiveDate) { + pub fn set_selected_visible(&mut self, selected: Date) { self.selected = selected; self.visible = self.selected; } @@ -107,7 +87,7 @@ impl CalendarModel { pub struct Calendar<'a, M> { model: &'a CalendarModel, - on_select: Box M>, + on_select: Box M>, on_prev: Box M>, on_next: Box M>, first_day_of_week: Weekday, @@ -121,45 +101,57 @@ where macro_rules! translate_month { ($month:expr, $year:expr) => {{ match $month { - chrono::Month::January => fl!("january", year = $year), - chrono::Month::February => fl!("february", year = $year), - chrono::Month::March => fl!("march", year = $year), - chrono::Month::April => fl!("april", year = $year), - chrono::Month::May => fl!("may", year = $year), - chrono::Month::June => fl!("june", year = $year), - chrono::Month::July => fl!("july", year = $year), - chrono::Month::August => fl!("august", year = $year), - chrono::Month::September => fl!("september", year = $year), - chrono::Month::October => fl!("october", year = $year), - chrono::Month::November => fl!("november", year = $year), - chrono::Month::December => fl!("december", year = $year), + 1 => fl!("january", year = $year), + 2 => fl!("february", year = $year), + 3 => fl!("march", year = $year), + 4 => fl!("april", year = $year), + 5 => fl!("may", year = $year), + 6 => fl!("june", year = $year), + 7 => fl!("july", year = $year), + 8 => fl!("august", year = $year), + 9 => fl!("september", year = $year), + 10 => fl!("october", year = $year), + 11 => fl!("november", year = $year), + 12 => fl!("december", year = $year), + _ => unreachable!(), } }}; } macro_rules! translate_weekday { - ($weekday:expr) => {{ + ($weekday:expr, short) => {{ match $weekday { - Weekday::Mon => fl!("monday"), - Weekday::Tue => fl!("tuesday"), - Weekday::Wed => fl!("wednesday"), - Weekday::Thu => fl!("thursday"), - Weekday::Fri => fl!("friday"), - Weekday::Sat => fl!("saturday"), - Weekday::Sun => fl!("sunday"), + Weekday::Monday => fl!("mon"), + Weekday::Tuesday => fl!("tue"), + Weekday::Wednesday => fl!("wed"), + Weekday::Thursday => fl!("thu"), + Weekday::Friday => fl!("fri"), + Weekday::Saturday => fl!("sat"), + Weekday::Sunday => fl!("sun"), + } + }}; + ($weekday:expr, long) => {{ + match $weekday { + Weekday::Monday => fl!("monday"), + Weekday::Tuesday => fl!("tuesday"), + Weekday::Wednesday => fl!("wednesday"), + Weekday::Thursday => fl!("thursday"), + Weekday::Friday => fl!("friday"), + Weekday::Saturday => fl!("saturday"), + Weekday::Sunday => fl!("sunday"), } }}; } let date = text(translate_month!( - Month::try_from(this.model.visible.month() as u8) - .expect("Previously valid month is suddenly invalid"), + this.model.visible.month(), this.model.visible.year() )) .size(18); - let day = text::body(translate_weekday!(this.model.visible.weekday())); + let day = text::body(translate_weekday!(this.model.visible.weekday(), long)); let month_controls = row::with_capacity(2) + .spacing(8) .push( icon::from_name("go-previous-symbolic") .apply(button::icon) @@ -171,46 +163,49 @@ where .on_press((this.on_next)()), ); - // Calender - let mut calendar_grid: Grid<'_, Message> = - grid().padding([0, 12].into()).width(Length::Fill); + // Calendar + let mut calendar_grid = grid().padding([0, 12].into()).width(Length::Fill); let mut first_day_of_week = this.first_day_of_week; for _ in 0..7 { calendar_grid = calendar_grid.push( - text(translate_weekday!(first_day_of_week)) - .size(12) - .width(Length::Fixed(36.0)) + text::caption(translate_weekday!(first_day_of_week, short)) + .width(Length::Fixed(44.0)) .align_x(Alignment::Center), ); - first_day_of_week = first_day_of_week.succ(); + first_day_of_week = first_day_of_week.next(); } calendar_grid = calendar_grid.insert_row(); - let monday = get_calender_first( + let first = get_calendar_first( this.model.visible.year(), this.model.visible.month(), - first_day_of_week, + this.first_day_of_week, ); - let mut day_iter = monday.iter_days(); + + let today = jiff::Zoned::now().date(); for i in 0..42 { if i > 0 && i % 7 == 0 { calendar_grid = calendar_grid.insert_row(); } - let date = day_iter.next().unwrap(); - let is_currently_viewed_month = date.month() == this.model.visible.month() - && date.year_ce() == this.model.visible.year_ce(); - let is_currently_selected_month = date.month() == this.model.selected.month() - && date.year_ce() == this.model.selected.year_ce(); + let date = first + .checked_add(i.days()) + .expect("valid date in calendar range"); + let is_currently_viewed_month = + date.first_of_month() == this.model.visible.first_of_month(); + let is_currently_selected_month = + date.first_of_month() == this.model.selected.first_of_month(); let is_currently_selected_day = date.day() == this.model.selected.day() && is_currently_selected_month; + let is_today = date == today; calendar_grid = calendar_grid.push(date_button( date, is_currently_viewed_month, is_currently_selected_day, + is_today, &this.on_select, )); } @@ -225,9 +220,8 @@ where .padding([12, 20]) .into(), calendar_grid.into(), - padded_control(crate::widget::divider::horizontal::default()).into(), ]) - .width(315) + .width(360) .padding([8, 0]); Self::new(content_list) @@ -235,21 +229,24 @@ where } fn date_button( - date: NaiveDate, + date: Date, is_currently_viewed_month: bool, is_currently_selected_day: bool, - on_select: &dyn Fn(NaiveDate) -> Message, + is_today: bool, + on_select: &dyn Fn(Date) -> Message, ) -> crate::widget::Button<'static, Message> { let style = if is_currently_selected_day { button::ButtonClass::Suggested + } else if is_today { + button::ButtonClass::Standard } else { button::ButtonClass::Text }; let button = button::custom(text(format!("{}", date.day())).center()) .class(style) - .height(Length::Fixed(36.0)) - .width(Length::Fixed(36.0)); + .height(Length::Fixed(44.0)) + .width(Length::Fixed(44.0)); if is_currently_viewed_month { button.on_press((on_select)(set_day(date, date.day()))) @@ -258,26 +255,10 @@ fn date_button( } } -/// Gets the first date that will be visible on the calender +/// Gets the first date that will be visible on the calendar #[must_use] -pub fn get_calender_first(year: i32, month: u32, from_weekday: Weekday) -> NaiveDate { - let date = NaiveDate::from_ymd_opt(year, month, 1).unwrap(); - let num_days = (date.weekday() as u32 + 7 - from_weekday as u32) % 7; // chrono::Weekday.num_days_from - date.checked_sub_days(Days::new(num_days as u64)).unwrap() -} - -// TODO: Refactor to use same function from applet module. -fn padded_control<'a, Message>( - content: impl Into>, -) -> crate::widget::container::Container<'a, Message, crate::Theme, crate::Renderer> { - crate::widget::container(content) - .padding(menu_control_padding()) - .width(Length::Fill) -} - -#[inline] -fn menu_control_padding() -> Padding { - let guard = crate::theme::THEME.lock().unwrap(); - let cosmic = guard.cosmic(); - [cosmic.space_xxs(), cosmic.space_m()].into() +pub fn get_calendar_first(year: i16, month: i8, from_weekday: Weekday) -> Date { + let date = Date::new(year, month, 1).expect("valid date"); + let num_days = date.weekday().since(from_weekday); + date.checked_sub(num_days.days()).expect("valid date") } From cb288070af610e0858e0c5e1818dc4dee3dcc00b 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 Feb 2026 20:37:56 +0100 Subject: [PATCH 221/352] chore: cargo fmt --- cosmic-theme/src/model/theme.rs | 4 +++- src/command.rs | 6 +++++- src/widget/dnd_destination.rs | 5 ++++- 3 files changed, 12 insertions(+), 3 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index cef479ae..89d87b6b 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -692,7 +692,9 @@ impl Theme { let is_dark = ThemeMode::is_dark(&config).map_err(|e| (vec![e], Self::default()))?; Self::get_active_with_brightness(is_dark) } - pub fn get_active_with_brightness(is_dark: bool) -> Result, Self)> { + pub fn get_active_with_brightness( + is_dark: bool, + ) -> Result, Self)> { let config = if is_dark { Self::dark_config() } else { diff --git a/src/command.rs b/src/command.rs index 14d326b4..00684e55 100644 --- a/src/command.rs +++ b/src/command.rs @@ -48,7 +48,11 @@ pub fn toggle_maximize(id: window::Id) -> iced::Task> { } #[cfg(feature = "xdg-portal")] -pub fn file_transfer_send(writeable: bool, auto_stop: bool, files: Vec) -> iced::Task> { +pub fn file_transfer_send( + writeable: bool, + auto_stop: bool, + files: Vec, +) -> iced::Task> { iced::Task::future(async move { let file_transfer = ashpd::documents::FileTransfer::new().await?; let key = file_transfer.start_transfer(writeable, auto_stop).await?; diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index a32a9fba..7225e917 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -522,7 +522,10 @@ impl Widget ); #[cfg(feature = "xdg-portal")] - if mime_type == FILE_TRANSFER_MIME && let Some(f) = self.on_file_transfer.as_ref() && let Ok(s) = String::from_utf8(data[..data.len() - 1].to_vec()) { + if mime_type == FILE_TRANSFER_MIME + && let Some(f) = self.on_file_transfer.as_ref() + && let Ok(s) = String::from_utf8(data[..data.len() - 1].to_vec()) + { shell.publish(f(s)); return event::Status::Captured; } From be98b7dd6f618c28778cb7c0fb9982a96c8a36aa Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 18 Feb 2026 14:18:27 +0100 Subject: [PATCH 222/352] refactor(cosmic-theme): remove recently-added `Theme::get_active_with_brightness` The added method was not necessary. Also improves the code in the get_active method. --- cosmic-theme/src/model/theme.rs | 26 ++++++++++---------------- cosmic-theme/src/output/qt_output.rs | 7 ++++++- 2 files changed, 16 insertions(+), 17 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 89d87b6b..8e1cd9f7 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -685,23 +685,17 @@ impl Theme { self.shade } - /// get the active theme + /// Get the active theme based on the current theme mode. pub fn get_active() -> Result, Self)> { - let config = - Config::new(Self::id(), Self::VERSION).map_err(|e| (vec![e], Self::default()))?; - let is_dark = ThemeMode::is_dark(&config).map_err(|e| (vec![e], Self::default()))?; - Self::get_active_with_brightness(is_dark) - } - pub fn get_active_with_brightness( - is_dark: bool, - ) -> Result, Self)> { - let config = if is_dark { - Self::dark_config() - } else { - Self::light_config() - } - .map_err(|e| (vec![e], Self::default()))?; - Self::get_entry(&config) + (|| { + (if ThemeMode::is_dark(&Config::new(Self::id(), Self::VERSION)?)? { + Self::dark_config + } else { + Self::light_config + })() + })() + .map_err(|error| (vec![error], Self::default())) + .and_then(|theme_config| Self::get_entry(&theme_config)) } #[must_use] diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index 0d9a4258..78bdec61 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -1,5 +1,6 @@ use crate::Theme; use configparser::ini::Ini; +use cosmic_config::CosmicConfigEntry; use palette::{Mix, Srgba, blend::Compose}; use std::{ fs::{self, File}, @@ -92,7 +93,11 @@ impl Theme { let dark = if self.is_dark { self.clone() } else { - Self::get_active_with_brightness(false).unwrap_or_else(|_| self.clone()) + Theme::light_config() + .ok() + .as_ref() + .and_then(|conf| Theme::get_entry(conf).ok()) + .unwrap_or_else(|| self.clone()) }; IniColors { background_alternate: dark.accent.base, From 7c49a736ec628150fd656e50a24ad2541b28f3ef Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 18 Feb 2026 14:23:09 +0100 Subject: [PATCH 223/352] refactor(cosmic-theme): remove `Theme::apply_exports_static` Recently-added method is redundant with `apply_exports`, and the dark mode preference is already defined in the theme being applied. --- cosmic-theme/src/output/mod.rs | 13 ------------- 1 file changed, 13 deletions(-) diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index 61f0e49d..37271dbb 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -41,19 +41,6 @@ impl Theme { Ok(()) } - #[inline] - /// To avoid rewriting too much code, I replaced calls to `Theme::apply_gtk` with this. - /// Note that vscode isn't touched by this function. - pub fn apply_exports_static(is_dark: bool) -> Result<(), OutputError> { - let gtk_res = Theme::apply_gtk(is_dark); - let qt_res = Theme::apply_qt(is_dark); - let qt56ct_res = Theme::apply_qt56ct(is_dark); - gtk_res?; - qt_res?; - qt56ct_res?; - Ok(()) - } - #[inline] pub fn write_exports(&self) -> Result<(), OutputError> { let gtk_res = self.write_gtk4(); From e1dad541b281bbc54f3f798380ccd4141a22d67c Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 18 Feb 2026 14:59:14 +0100 Subject: [PATCH 224/352] chore(cosmic-theme): `Theme::apply_exports` should not apply VS Code theme currently --- cosmic-theme/src/output/mod.rs | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index 37271dbb..0baefb86 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -29,19 +29,19 @@ pub enum OutputError { impl Theme { #[inline] + /// Apply COSMIC theme exports for GTK and Qt applications. pub fn apply_exports(&self) -> Result<(), OutputError> { let gtk_res = Theme::apply_gtk(self.is_dark); let qt_res = Theme::apply_qt(self.is_dark); let qt56ct_res = Theme::apply_qt56ct(self.is_dark); - let vs_res = self.clone().apply_vs_code(); gtk_res?; qt_res?; qt56ct_res?; - vs_res?; Ok(()) } #[inline] + /// Write COSMIC theme exports for GTK and Qt applications. pub fn write_exports(&self) -> Result<(), OutputError> { let gtk_res = self.write_gtk4(); let qt_res = self.write_qt(); @@ -51,6 +51,7 @@ impl Theme { } #[inline] + /// Un-export GTK and Qt theme configurations applied by us. pub fn reset_exports() -> Result<(), OutputError> { let gtk_res = Theme::reset_gtk(); let qt_res = Theme::reset_qt(); From dc3c194f09734256498b23b218b2c69f0eb21bf4 Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 18 Feb 2026 20:02:58 +0000 Subject: [PATCH 225/352] fix(cosmic-theme): inverted Qt link_button colors --- cosmic-theme/src/output/qt_output.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index 78bdec61..2e926a2e 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -48,7 +48,7 @@ impl Theme { decoration_hover: self.accent_text_color(), foreground_active: self.accent_text_color(), foreground_inactive: self.background.on.mix(bg, 0.1), - foreground_link: self.link_button.base, + foreground_link: self.link_button.on, foreground_negative: self.destructive_text_color(), foreground_neutral: self.warning_text_color(), foreground_normal: self.background.on, @@ -73,7 +73,7 @@ impl Theme { decoration_hover: selected, foreground_active: selected_text, foreground_inactive: selected_text.mix(selected, 0.5), - foreground_link: self.link_button.on, + foreground_link: self.link_button.base, foreground_negative: self.destructive_color(), foreground_neutral: self.warning_color(), foreground_normal: selected_text, @@ -106,7 +106,7 @@ impl Theme { decoration_hover: dark.accent_text_color(), foreground_active: dark.accent_text_color(), foreground_inactive: dark.background.on.mix(dark.background.base, 0.1), - foreground_link: dark.link_button.base, + foreground_link: dark.link_button.on, foreground_negative: dark.destructive_text_color(), foreground_neutral: dark.warning_text_color(), foreground_normal: dark.background.on, From 3ed5c173fd78e97b24fab0a059e5fe23b2f63b8f Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 18 Feb 2026 20:10:42 +0000 Subject: [PATCH 226/352] fix(cosmic-theme): copy for backup, not rename We're now merging the colors with kdeglobals, not replacing it with a symlink. So renaming the file gives us a missing file Io error: [2026-02-18T20:03:08Z ERROR cosmic_settings_daemon::theme] Failed to apply COSMIC theme exports. Io(Os { code: 2, kind: NotFound, message: "No such file or directory" }) --- cosmic-theme/src/output/qt_output.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index 2e926a2e..67ffbd69 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -338,7 +338,7 @@ widgetStyle=qt6ct-style fn backup_non_cosmic_kdeglobals(ini: &Ini, path: &Path) -> io::Result<()> { if !Self::is_cosmic_kdeglobals(&ini)?.unwrap_or(true) { let backup_path = path.with_extension("bak"); - fs::rename(path, &backup_path)?; + fs::copy(path, &backup_path)?; } Ok(()) } From 754b064bffbb399b955662d6c1fcca3468accb0c Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Wed, 18 Feb 2026 20:42:34 +0000 Subject: [PATCH 227/352] tweak(cosmic-theme): pretty write ini --- cosmic-theme/src/output/mod.rs | 7 +++++++ cosmic-theme/src/output/qt56ct_output.rs | 5 +++-- cosmic-theme/src/output/qt_output.rs | 4 ++-- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index 0baefb86..f331e8d2 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -1,3 +1,4 @@ +use configparser::ini::WriteOptions; use palette::{Srgba, rgb::Rgba}; use thiserror::Error; @@ -78,3 +79,9 @@ pub fn to_rgba(c: Srgba) -> String { c_u8.red, c_u8.green, c_u8.blue, c.alpha ) } + +pub fn qt_settings_ini_style() -> WriteOptions { + let mut write_options = WriteOptions::default(); + write_options.blank_lines_between_sections = 1; + write_options +} diff --git a/cosmic-theme/src/output/qt56ct_output.rs b/cosmic-theme/src/output/qt56ct_output.rs index d4736597..552e7fec 100644 --- a/cosmic-theme/src/output/qt56ct_output.rs +++ b/cosmic-theme/src/output/qt56ct_output.rs @@ -5,7 +5,7 @@ use std::{ path::PathBuf, }; -use super::OutputError; +use super::{OutputError, qt_settings_ini_style}; impl Theme { /// The "version" of this theme. @@ -86,7 +86,8 @@ impl Theme { } } - ini.write(path).map_err(OutputError::Io)?; + ini.pretty_write(path, &qt_settings_ini_style()) + .map_err(OutputError::Io)?; Ok(()) } diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index 67ffbd69..9bca3d18 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -8,7 +8,7 @@ use std::{ path::{Path, PathBuf}, }; -use super::OutputError; +use super::{OutputError, qt_settings_ini_style}; impl Theme { /// Produces a color scheme ini file for Qt. @@ -258,7 +258,7 @@ widgetStyle=qt6ct-style } kdeglobals_ini - .write(kdeglobals_file) + .pretty_write(kdeglobals_file, &qt_settings_ini_style()) .map_err(OutputError::Io)?; Ok(()) } From c1c09624bd5f46cdcfc042561070bab6faabaffc Mon Sep 17 00:00:00 2001 From: mariinkys Date: Thu, 19 Feb 2026 16:32:30 +0100 Subject: [PATCH 228/352] fix: right-clicking any sidebar item makes all sidebar items bold --- 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 e4f416bf..5201c908 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -246,7 +246,7 @@ where if let Some(text) = self.model.text.get(key) { let font = if self.button_is_focused(state, key) { self.font_active - } else if state.show_context.is_some() || self.button_is_hovered(state, key) { + } else if state.show_context == Some(key) || self.button_is_hovered(state, key) { self.font_hovered } else if self.model.is_active(key) { self.font_active From 1f6086e5ead97063bfc654f2ca9ad31eaf6944d3 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 19 Feb 2026 09:18:35 -0700 Subject: [PATCH 229/352] Update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index e2a24417..ecc29a83 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit e2a2441789a7e302f099c0e8e9493ef81b58e265 +Subproject commit ecc29a83982839f628e2ed1c01605c694a1fd3ac From b9bd773940950dc07b2cfa7c62c2588b0a653017 Mon Sep 17 00:00:00 2001 From: Hojjat Abdollahi Date: Thu, 19 Feb 2026 10:06:45 -0700 Subject: [PATCH 230/352] feat: ellipsize text (#1132) --- iced | 2 +- src/widget/dropdown/menu/mod.rs | 1 + src/widget/dropdown/multi/menu.rs | 2 ++ src/widget/dropdown/multi/widget.rs | 3 +++ src/widget/dropdown/widget.rs | 3 +++ src/widget/segmented_button/widget.rs | 4 +++- src/widget/text_input/input.rs | 7 +++++++ 7 files changed, 20 insertions(+), 2 deletions(-) diff --git a/iced b/iced index ecc29a83..d36e4df4 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit ecc29a83982839f628e2ed1c01605c694a1fd3ac +Subproject commit d36e4df47f2e277fafcd3505229d53438c7f128d diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index 1d42d01f..3fd099b3 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -682,6 +682,7 @@ where vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), }, bounds.position(), color, diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index 0035829f..39e89ee2 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -594,6 +594,7 @@ where vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), }, bounds.position(), color, @@ -643,6 +644,7 @@ where vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), }, bounds.position(), appearance.description_color, diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index 458cf5e6..43a0836f 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -279,6 +279,7 @@ pub fn layout( vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), }); paragraph.min_width().round() }; @@ -423,6 +424,7 @@ pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), }); paragraph.min_width().round() }; @@ -555,6 +557,7 @@ pub fn draw<'a, S, Item: Clone + PartialEq + 'static>( vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), }, bounds.position(), style.text_color, diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 03be4eb3..67101d26 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -212,6 +212,7 @@ where vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), }); } @@ -478,6 +479,7 @@ pub fn layout( vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), }; let paragraph = match paragraph { Some(p) => { @@ -934,6 +936,7 @@ pub fn draw<'a, S>( vertical_alignment: alignment::Vertical::Center, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::default(), + ellipsize: text::Ellipsize::default(), }, bounds.position(), style.text_color, diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 5201c908..1f009cc6 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::{LineHeight, Renderer as TextRenderer, Shaping, Wrapping}; +use iced_core::text::{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}; @@ -274,6 +274,7 @@ where vertical_alignment: alignment::Vertical::Center, shaping: Shaping::Advanced, wrapping: Wrapping::None, + ellipsize: Ellipsize::None, line_height: self.line_height, }; @@ -602,6 +603,7 @@ where vertical_alignment: alignment::Vertical::Center, shaping: Shaping::Advanced, wrapping: Wrapping::default(), + ellipsize: Ellipsize::default(), line_height: self.line_height, }) }); diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 7dd92e12..e98d4cfa 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -728,6 +728,7 @@ where line_height: text::LineHeight::default(), shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, + ellipsize: text::Ellipsize::None, }); let Size { width, height } = @@ -1160,6 +1161,7 @@ pub fn layout( line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, + ellipsize: text::Ellipsize::None, }); let label_size = label_paragraph.min_bounds(); @@ -1297,6 +1299,7 @@ pub fn layout( line_height: helper_text_line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, + ellipsize: text::Ellipsize::None, }); let helper_text_size = helper_text_paragraph.min_bounds(); let helper_text_node = layout::Node::new(helper_text_size).translate(helper_pos); @@ -2260,6 +2263,7 @@ pub fn draw<'a, Message>( line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, + ellipsize: text::Ellipsize::None, }, label_layout.bounds().position(), appearance.label_color, @@ -2449,6 +2453,7 @@ pub fn draw<'a, Message>( line_height: text::LineHeight::default(), shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, + ellipsize: text::Ellipsize::None, }, bounds.position(), color, @@ -2497,6 +2502,7 @@ pub fn draw<'a, Message>( line_height: helper_line_height, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, + ellipsize: text::Ellipsize::None, }, helper_text_layout.bounds().position(), text_color, @@ -2877,6 +2883,7 @@ fn replace_paragraph( vertical_alignment: alignment::Vertical::Top, shaping: text::Shaping::Advanced, wrapping: text::Wrapping::None, + ellipsize: text::Ellipsize::None, }); } From 384e8f6e219bb458720eafa5bb971b832c057f23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Alex=20Mar=C3=ADn?= <62134857+mariinkys@users.noreply.github.com> Date: Fri, 20 Feb 2026 12:06:45 +0100 Subject: [PATCH 231/352] fix(segmented_button): clear bold button text on context menu close --- src/widget/segmented_button/widget.rs | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 1f009cc6..0e1af1d0 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -2073,7 +2073,7 @@ where _renderer: &Renderer, translation: Vector, ) -> Option> { - let state = tree.state.downcast_ref::(); + let state = tree.state.downcast_mut::(); let menu_state = state.menu_state.clone(); let entity = state.show_context?; @@ -2089,6 +2089,12 @@ where if !menu_state.inner.with_data(|data| data.open) { // If the menu is not open, we don't need to show it. + // We also clear the context entity and update the text + // cache so that the item is not bold when the context menu is closed + state.show_context = None; + for key in self.model.order.iter().copied() { + self.update_entity_paragraph(state, key); + } return None; } bounds.x = state.context_cursor.x; From a37be90e811f365273bf632bf7090b63a368f092 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 9 Feb 2026 22:04:13 +0100 Subject: [PATCH 232/352] fix(single-instance): unminimize main window on dbus activate --- src/app/cosmic.rs | 29 +++++++++++++++++++++-------- 1 file changed, 21 insertions(+), 8 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 803a56bd..bfda4a1d 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -1034,15 +1034,28 @@ impl Cosmic { } return Task::batch(cmds); } - Action::Activate(_token) => - { - #[cfg(feature = "wayland")] + Action::Activate(_token) => { if let Some(id) = self.app.core().main_window_id() { - return iced_winit::platform_specific::commands::activation::activate( - id, - #[allow(clippy::used_underscore_binding)] - _token, - ); + // Unminimize window before requesting to activate it. + let mut task = iced_runtime::window::minimize(id, false); + + #[cfg(feature = "wayland")] + { + task = task.chain( + iced_winit::platform_specific::commands::activation::activate( + id, + #[allow(clippy::used_underscore_binding)] + _token, + ), + ) + } + + #[cfg(not(feature = "wayland"))] + { + task = task.chain(iced_runtime::window::gain_focus(id)); + } + + return task; } } From f2caa66f0ff8e8fc1172d12742d08356fd55a78c Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 2 Mar 2026 17:10:06 +0100 Subject: [PATCH 233/352] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Aindriú Mac Giolla Eoin Co-authored-by: Anonymous Co-authored-by: Arve Eriksson <031299870@telia.com> Co-authored-by: Baurzhan Muftakhidinov Co-authored-by: Benmak Kizuna Co-authored-by: David Carvalho Co-authored-by: Ettore Atalan Co-authored-by: Fedorov Alexei Co-authored-by: Feike Donia Co-authored-by: Geeson Wan Co-authored-by: Hosted Weblate Co-authored-by: Jiri Grönroos Co-authored-by: Julien Brouillard Co-authored-by: Marko X Co-authored-by: Tommi Nieminen Co-authored-by: VandaL Co-authored-by: Zahid Rizky Fakhri Co-authored-by: jonnysemon Co-authored-by: lorduskordus Co-authored-by: therealmate Co-authored-by: yakup Co-authored-by: Димко Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ar/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/cs/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/de/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/fi/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/fr/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ga/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/hu/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/id/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/kk/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/nl/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/pl/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/pt_BR/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ru/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/sv/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/tr/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/uk/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/zh_Hans/ Translation: Pop OS/libcosmic --- i18n/ar/libcosmic.ftl | 7 +++++++ i18n/cs/libcosmic.ftl | 21 ++++++++++++++------- i18n/de/libcosmic.ftl | 23 ++++++++++++++--------- i18n/fi/libcosmic.ftl | 34 ++++++++++++++++++++++++++++++++++ i18n/fr/libcosmic.ftl | 21 ++++++++++++++------- i18n/ga/libcosmic.ftl | 21 ++++++++++++++------- i18n/hu/libcosmic.ftl | 21 ++++++++++++++------- i18n/id/libcosmic.ftl | 21 ++++++++++++++------- i18n/kk/libcosmic.ftl | 21 ++++++++++++++------- i18n/nl/libcosmic.ftl | 8 +++++++- i18n/pl/libcosmic.ftl | 21 ++++++++++++++------- i18n/pt-BR/libcosmic.ftl | 21 ++++++++++++++------- i18n/ru/libcosmic.ftl | 21 ++++++++++++++------- i18n/sl/libcosmic.ftl | 0 i18n/sv/libcosmic.ftl | 21 ++++++++++++++------- i18n/tr/libcosmic.ftl | 27 ++++++++++++++++++++++++++- i18n/uk/libcosmic.ftl | 21 ++++++++++++++------- i18n/zh-Hans/libcosmic.ftl | 21 ++++++++++++++------- 18 files changed, 256 insertions(+), 95 deletions(-) create mode 100644 i18n/sl/libcosmic.ftl diff --git a/i18n/ar/libcosmic.ftl b/i18n/ar/libcosmic.ftl index 92f016f0..35e6050f 100644 --- a/i18n/ar/libcosmic.ftl +++ b/i18n/ar/libcosmic.ftl @@ -27,3 +27,10 @@ thursday = الخميس friday = الجمعة saturday = السبت sunday = الأحد +mon = ن +tue = ث +wed = ر +thu = خ +fri = ج +sat = س +sun = ح diff --git a/i18n/cs/libcosmic.ftl b/i18n/cs/libcosmic.ftl index 8f2ef348..850870d9 100644 --- a/i18n/cs/libcosmic.ftl +++ b/i18n/cs/libcosmic.ftl @@ -8,7 +8,7 @@ designers = Designéři artists = Grafici translators = Překladatelé documenters = Tvůrci dokumentace -sunday = Ne +sunday = Neděle january = Leden { $year } february = Únor { $year } march = Březen { $year } @@ -21,9 +21,16 @@ september = Září { $year } october = Říjen { $year } november = Listopad { $year } december = Prosinec { $year } -monday = Po -tuesday = Út -wednesday = St -thursday = Čt -friday = Pá -saturday = So +monday = Pondělí +tuesday = Úterý +wednesday = Středa +thursday = Čtvrtek +friday = Pátek +saturday = Sobota +mon = Po +tue = Út +wed = St +thu = Čt +fri = Pá +sat = So +sun = Ne diff --git a/i18n/de/libcosmic.ftl b/i18n/de/libcosmic.ftl index 2ef7b765..1f17c924 100644 --- a/i18n/de/libcosmic.ftl +++ b/i18n/de/libcosmic.ftl @@ -3,11 +3,11 @@ close = Schließen # About license = Lizenz links = Links -developers = Entwickler*innen -designers = Designer*innen -artists = Künstler*innen +developers = Entwickler(innen) +designers = Designer(innen) +artists = Künstler(innen) translators = Übersetzer*innen -documenters = Dokumentierer*innen +documenters = Dokumentierer(innen) # Calendar january = Januar { $year } february = Februar { $year } @@ -23,8 +23,13 @@ november = November { $year } december = Dezember { $year } monday = Mo tuesday = Di -wednesday = Mi -thursday = Do -friday = Fr -saturday = Sa -sunday = So +wednesday = Mittwoch +thursday = Donnerstag +friday = Freitag +saturday = Samstag +sunday = Sonntag +wed = Mi +thu = Do +fri = Fr +sat = Sa +sun = So diff --git a/i18n/fi/libcosmic.ftl b/i18n/fi/libcosmic.ftl index e69de29b..877f225d 100644 --- a/i18n/fi/libcosmic.ftl +++ b/i18n/fi/libcosmic.ftl @@ -0,0 +1,34 @@ +monday = Maanantai +mon = ma +tuesday = Tiistai +tue = ti +wednesday = Keskiviikko +wed = ke +thursday = Torstai +thu = to +friday = Perjantai +fri = pe +saturday = Lauantai +sat = la +sunday = Sunnuntai +sun = su +close = Sulje +license = Lisenssi +links = Linkit +developers = Kehittäjät +designers = Suunnittelijat +artists = Artistit +translators = Kääntäjät +documenters = Dokumentoijat +january = Tammikuu { $year } +february = Helmikuu { $year } +march = Maaliskuu { $year } +april = Huhtikuu { $year } +may = Toukokuu { $year } +june = Kesäkuu { $year } +july = Heinäkuu { $year } +august = Elokuu { $year } +september = Syyskuu { $year } +october = Lokakuu { $year } +november = Marraskuu { $year } +december = Joulukuu { $year } diff --git a/i18n/fr/libcosmic.ftl b/i18n/fr/libcosmic.ftl index 43e2d6f7..1ec6c0cf 100644 --- a/i18n/fr/libcosmic.ftl +++ b/i18n/fr/libcosmic.ftl @@ -10,18 +10,25 @@ february = Février { $year } april = Avril { $year } march = Mars { $year } november = Novembre { $year } -friday = Ven -tuesday = Mar +friday = Vendredi +tuesday = Mardi may = Mai { $year } -wednesday = Mer -monday = Lun +wednesday = Mercredi +monday = Lundi december = Décembre { $year } -sunday = Dim +sunday = Dimanche june = Juin { $year } -saturday = Sam +saturday = Samedi august = Août { $year } july = Juillet { $year } -thursday = Jeu +thursday = Jeudi september = Septembre { $year } october = Octobre { $year } designers = Designers +mon = Lun +tue = Mar +wed = Mer +thu = Jeu +fri = Ven +sat = Sam +sun = Dim diff --git a/i18n/ga/libcosmic.ftl b/i18n/ga/libcosmic.ftl index 024841bf..bdf38d20 100644 --- a/i18n/ga/libcosmic.ftl +++ b/i18n/ga/libcosmic.ftl @@ -18,10 +18,17 @@ september = Meán Fómhair { $year } october = Deireadh Fómhair { $year } november = Samhain { $year } december = Nollaig { $year } -monday = Lua -tuesday = Mái -wednesday = Céa -thursday = Déa -friday = Aoi -saturday = Sat -sunday = Dom +monday = Dé Luain +tuesday = Dé Máirt +wednesday = Dé Céadaoin +thursday = Déardaoin +friday = Dé hAoine +saturday = Dé Sathairn +sunday = Dé Domhnaigh +mon = Lua +tue = Mái +wed = Céa +thu = Déa +fri = Aoi +sat = Sat +sun = Dom diff --git a/i18n/hu/libcosmic.ftl b/i18n/hu/libcosmic.ftl index 02069244..7ff046b3 100644 --- a/i18n/hu/libcosmic.ftl +++ b/i18n/hu/libcosmic.ftl @@ -20,10 +20,17 @@ september = { $year } szeptember october = { $year } október november = { $year } november december = { $year } december -monday = H -tuesday = K -wednesday = Sze -thursday = Cs -friday = P -saturday = Szo -sunday = V +monday = Hétfő +tuesday = Kedd +wednesday = Szerda +thursday = Csütörtök +friday = Péntek +saturday = Szombat +sunday = Vasárnap +mon = H +tue = K +wed = Sze +thu = Cs +fri = P +sat = Szo +sun = V diff --git a/i18n/id/libcosmic.ftl b/i18n/id/libcosmic.ftl index 2ce82dab..53e7736b 100644 --- a/i18n/id/libcosmic.ftl +++ b/i18n/id/libcosmic.ftl @@ -18,10 +18,17 @@ september = September { $year } october = Oktober { $year } november = November { $year } december = Desember { $year } -monday = Sen -tuesday = Sel -wednesday = Rab -sunday = Min -saturday = Sab -friday = Jum -thursday = Kam +monday = Senin +tuesday = Selasa +wednesday = Rabu +sunday = Minggu +saturday = Sabtu +friday = Jum'at +thursday = Kamis +mon = Sen +tue = Sel +wed = Rab +thu = Kam +fri = Jum +sat = Sab +sun = Min diff --git a/i18n/kk/libcosmic.ftl b/i18n/kk/libcosmic.ftl index bb06e98f..9d257114 100644 --- a/i18n/kk/libcosmic.ftl +++ b/i18n/kk/libcosmic.ftl @@ -18,10 +18,17 @@ september = Қыркүйек { $year } october = Қазан { $year } november = Қараша { $year } december = Желтоқсан { $year } -monday = Дс -tuesday = Сс -wednesday = Ср -thursday = Бс -friday = Жм -saturday = Сб -sunday = Жс +monday = Дүйсенбі +tuesday = Сейсенбі +wednesday = Сәрсенбі +thursday = Бейсенбі +friday = Жұма +saturday = Сенбі +sunday = Жексенбі +mon = Дс +tue = Сс +wed = Ср +thu = Бс +fri = Жм +sat = Сн +sun = Жк diff --git a/i18n/nl/libcosmic.ftl b/i18n/nl/libcosmic.ftl index 75fc8cdf..7676b811 100644 --- a/i18n/nl/libcosmic.ftl +++ b/i18n/nl/libcosmic.ftl @@ -18,4 +18,10 @@ wednesday = Woe thursday = Do friday = Vrij saturday = Za -sunday = Zon +sunday = Zo +links = Links +developers = Ontwikkeling +designers = Ontwerp +translators = Vertaling +documenters = Documentatie +artists = Vormgeving diff --git a/i18n/pl/libcosmic.ftl b/i18n/pl/libcosmic.ftl index 4bbfd67f..0d1649d4 100644 --- a/i18n/pl/libcosmic.ftl +++ b/i18n/pl/libcosmic.ftl @@ -20,10 +20,17 @@ september = Wrzesień { $year } october = Październik { $year } november = Listopad { $year } december = Grudzień { $year } -monday = Pon -tuesday = Wto -wednesday = Śro -thursday = Czw -friday = Pią -saturday = Sob -sunday = Nie +monday = Poniedziałek +tuesday = Wtorek +wednesday = Środa +thursday = Czwartek +friday = Piątek +saturday = Sobota +sunday = Niedziela +mon = Pon +tue = Wto +wed = Śro +thu = Czw +fri = Pia +sat = Sob +sun = Nie diff --git a/i18n/pt-BR/libcosmic.ftl b/i18n/pt-BR/libcosmic.ftl index 51b5f6c3..1a51c799 100644 --- a/i18n/pt-BR/libcosmic.ftl +++ b/i18n/pt-BR/libcosmic.ftl @@ -20,10 +20,17 @@ september = Setembro de { $year } october = Outubro de { $year } november = Novembro de { $year } december = Dezembro de { $year } -monday = Seg -tuesday = Ter -wednesday = Qua -thursday = Qui -friday = Sex -saturday = Sáb -sunday = Dom +monday = Segunda-feira +tuesday = Terça-feira +wednesday = Quarta-feira +thursday = Quinta-feira +friday = Sexta-feira +saturday = Sábado +sunday = Domingo +mon = Seg +tue = Ter +wed = Qua +thu = Qui +fri = Sex +sat = Sáb +sun = Dom diff --git a/i18n/ru/libcosmic.ftl b/i18n/ru/libcosmic.ftl index 7fe9b3dc..1ff78655 100644 --- a/i18n/ru/libcosmic.ftl +++ b/i18n/ru/libcosmic.ftl @@ -18,10 +18,17 @@ september = Сентябрь { $year } october = Октябрь { $year } november = Ноябрь { $year } december = Декабрь { $year } -monday = Пн -tuesday = Вт -wednesday = Ср -thursday = Чт -friday = Пт -saturday = Сб -sunday = Вс +monday = Понедельник +tuesday = Вторник +wednesday = Среда +thursday = Четверг +friday = Пятница +saturday = Суббота +sunday = Воскресенье +mon = Пн +tue = Вт +wed = Ср +thu = Чт +fri = Пт +sat = Сб +sun = Вс diff --git a/i18n/sl/libcosmic.ftl b/i18n/sl/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/sv/libcosmic.ftl b/i18n/sv/libcosmic.ftl index f0c647a1..27cdb393 100644 --- a/i18n/sv/libcosmic.ftl +++ b/i18n/sv/libcosmic.ftl @@ -18,10 +18,17 @@ september = September { $year } october = Oktober { $year } november = November { $year } december = December { $year } -monday = Mån -tuesday = Tis -wednesday = Ons -thursday = Tor -friday = Fre -saturday = Lör -sunday = Sön +monday = Måndag +tuesday = Tisdag +wednesday = Onsdag +thursday = Torsdag +friday = Fredag +saturday = Lördag +sunday = Söndag +sun = Sön +mon = Mån +tue = Tis +wed = Ons +thu = Tor +fri = Fre +sat = Lör diff --git a/i18n/tr/libcosmic.ftl b/i18n/tr/libcosmic.ftl index fd0f5475..39690200 100644 --- a/i18n/tr/libcosmic.ftl +++ b/i18n/tr/libcosmic.ftl @@ -1,6 +1,5 @@ # Context Drawer close = Kapat - # About license = Lisans links = Bağlantılar @@ -9,3 +8,29 @@ designers = Tasarımcılar artists = Sanatçılar translators = Çevirmenler documenters = Belgelendiriciler +january = Ocak { $year } +february = Şubat { $year } +march = Mart { $year } +april = Nisan { $year } +may = Mayıs { $year } +june = Haziran { $year } +july = Temmuz { $year } +august = Ağustos { $year } +september = Eylül { $year } +october = Ekim { $year } +november = Kasım { $year } +december = Aralık { $year } +monday = Pazartesi +mon = Pzt +tuesday = Salı +tue = Sal +wednesday = Çarşamba +wed = Çar +thursday = Perşembe +thu = Per +friday = Cuma +fri = Cum +saturday = Cumartesi +sat = Cmt +sunday = Pazar +sun = Paz diff --git a/i18n/uk/libcosmic.ftl b/i18n/uk/libcosmic.ftl index d82c2a6e..cbe1cfaf 100644 --- a/i18n/uk/libcosmic.ftl +++ b/i18n/uk/libcosmic.ftl @@ -10,20 +10,27 @@ translators = Перекладачі documenters = Документатори february = Лютий { $year } november = Листопад { $year } -friday = Пт -tuesday = Вт +friday = П'ятниця +tuesday = Вівторок may = Травень { $year } -wednesday = Ср +wednesday = Середа april = Квітень { $year } -monday = Пн +monday = Понеділок december = Грудень { $year } -sunday = Нд +sunday = Неділя march = Березень { $year } june = Червень { $year } -saturday = Сб +saturday = Субота august = Серпень { $year } july = Липень { $year } -thursday = Чт +thursday = Четвер september = Вересень { $year } october = Жовтень { $year } january = Січень { $year } +mon = Пн +tue = Вт +wed = Ср +thu = Чт +fri = Пт +sat = Cб +sun = Нд diff --git a/i18n/zh-Hans/libcosmic.ftl b/i18n/zh-Hans/libcosmic.ftl index 9dfd6139..42330dcb 100644 --- a/i18n/zh-Hans/libcosmic.ftl +++ b/i18n/zh-Hans/libcosmic.ftl @@ -16,12 +16,19 @@ september = { $year }年9月 october = { $year }年10月 november = { $year }年11月 december = { $year }年12月 -monday = 周一 -tuesday = 周二 -wednesday = 周三 -thursday = 周四 -friday = 周五 -saturday = 周六 -sunday = 周日 +monday = 星期一 +tuesday = 星期二 +wednesday = 星期三 +thursday = 星期四 +friday = 星期五 +saturday = 星期六 +sunday = 星期日 artists = 艺术家 documenters = 文档作者 +mon = 周一 +tue = 周二 +wed = 周三 +thu = 周四 +fri = 周五 +sat = 周六 +sun = 周日 From bd1d3d5a73a858443c920ed8f83607846650a3e6 Mon Sep 17 00:00:00 2001 From: Hojjat Abdollahi Date: Mon, 2 Mar 2026 12:01:19 -0700 Subject: [PATCH 234/352] fix: ellipsize headerbar title instead of wrapping (#1140) --- src/widget/header_bar.rs | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index c5bde28f..b0957d68 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -445,6 +445,10 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { 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() From 85c27a99604f343bfa0fb2d78e6726f511f6a57a Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 3 Mar 2026 21:18:45 +0100 Subject: [PATCH 235/352] fix(cosmic-theme): on reset of theme exports, do not remove VS code configs Closes #1139 --- cosmic-theme/src/output/mod.rs | 2 -- 1 file changed, 2 deletions(-) diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index f331e8d2..b2474dc1 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -56,10 +56,8 @@ impl Theme { pub fn reset_exports() -> Result<(), OutputError> { let gtk_res = Theme::reset_gtk(); let qt_res = Theme::reset_qt(); - let vs_res = Theme::reset_vs_code(); gtk_res?; qt_res?; - vs_res?; Ok(()) } } From 86dcf8af6cfcb3ab65e41649e4793f47c5595433 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 3 Mar 2026 23:32:00 +0100 Subject: [PATCH 236/352] feat(cosmic-icons): new icons for cosmic image viewer app --- cosmic-icons | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cosmic-icons b/cosmic-icons index 70b07582..52520957 160000 --- a/cosmic-icons +++ b/cosmic-icons @@ -1 +1 @@ -Subproject commit 70b07582e24ec2114672256b9657ca80670bca8a +Subproject commit 5252095787cc96e2aed64604158f94e450703455 From e10459fb375d6a84e4ad296df98b1af610fcc531 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 10 Feb 2026 15:37:41 -0500 Subject: [PATCH 237/352] 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 238/352] 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 239/352] 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 240/352] 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 241/352] 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 242/352] 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 243/352] 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 244/352] 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 245/352] 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 246/352] 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 247/352] 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 248/352] 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 249/352] 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 250/352] 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 251/352] 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 252/352] 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 253/352] 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 254/352] 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 255/352] 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 256/352] 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 257/352] 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 258/352] 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 259/352] 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 260/352] 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 261/352] 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 262/352] 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 263/352] 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 264/352] 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 265/352] 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 266/352] 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 267/352] 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 268/352] 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 269/352] 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 270/352] 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 271/352] 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 272/352] 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 273/352] 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 274/352] 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 275/352] 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 276/352] 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 277/352] 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 278/352] 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 279/352] 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 280/352] 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 281/352] 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 282/352] 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 283/352] 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 284/352] 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 285/352] 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 286/352] 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 287/352] 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 288/352] 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 289/352] 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 290/352] 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 291/352] 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 292/352] 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 293/352] 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 294/352] 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 295/352] 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 296/352] 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 297/352] 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 298/352] 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 299/352] 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 300/352] 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 301/352] 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 302/352] 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 303/352] 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 304/352] 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 305/352] 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 306/352] 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 307/352] 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 308/352] 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 309/352] 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 310/352] 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 311/352] 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 312/352] 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 313/352] 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 314/352] 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 315/352] 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 316/352] 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 317/352] 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 318/352] 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 319/352] 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 320/352] 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 321/352] 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 322/352] 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 323/352] 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 324/352] 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 325/352] 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 326/352] 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 327/352] 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 328/352] 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 329/352] 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 330/352] 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 331/352] 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 332/352] 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 333/352] 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 334/352] 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 335/352] 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 336/352] 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 337/352] 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 338/352] 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 339/352] 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 340/352] 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 341/352] 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 342/352] 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 343/352] 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 344/352] 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 345/352] 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 346/352] 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 347/352] 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 348/352] 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 349/352] 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 350/352] 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 351/352] 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 352/352] 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();