From 3a8ebf91977bd21b0ab148b3a8326eb2ce52c246 Mon Sep 17 00:00:00 2001 From: Alex Klepov Date: Thu, 20 Aug 2026 01:18:04 +0300 Subject: [PATCH 1/8] fix(menu): panic when right-clicking a submenu --- examples/context-menu/src/main.rs | 22 +++++++-- src/widget/menu/menu_inner.rs | 77 ++++++++++++++++--------------- 2 files changed, 56 insertions(+), 43 deletions(-) diff --git a/examples/context-menu/src/main.rs b/examples/context-menu/src/main.rs index e5ca5878..a953c8ed 100644 --- a/examples/context-menu/src/main.rs +++ b/examples/context-menu/src/main.rs @@ -30,6 +30,7 @@ pub enum Message { WindowClose, Surface(cosmic::surface::Action), ToggleHideContent, + ToggleSomeAction, WindowNew, } @@ -82,6 +83,7 @@ impl cosmic::Application for App { /// Handle application events here. fn update(&mut self, message: Self::Message) -> Task { + tracing::error!("Clicked {message:?}"); match message { Message::Clicked => { self.button_label = format!("Clicked {message:?}"); @@ -91,9 +93,10 @@ impl cosmic::Application for App { cosmic::app::Action::Surface(action), )); } - Message::WindowClose => {} - Message::ToggleHideContent => {} - Message::WindowNew => {} + Message::WindowClose | + Message::ToggleHideContent | + Message::ToggleSomeAction | + Message::WindowNew=> {} } Task::none() @@ -102,7 +105,7 @@ impl cosmic::Application for App { /// Creates a view after each update. 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), + cosmic::widget::button::text(self.button_label.clone()).on_press(Message::Clicked), self.context_menu(), ) .on_surface_action(Message::Surface); @@ -131,7 +134,14 @@ impl App { None, self.hide_content, ContextMenuAction::ToggleHideContent, - )], + ), + menu::Item::CheckBox( + "Test content", + None, + self.hide_content, + ContextMenuAction::ToggleSomeAction, + ) + ], ), menu::Item::Divider, menu::Item::Button("Quit", None, ContextMenuAction::WindowClose), @@ -144,6 +154,7 @@ impl App { pub enum ContextMenuAction { WindowClose, ToggleHideContent, + ToggleSomeAction, WindowNew, } @@ -153,6 +164,7 @@ impl menu::Action for ContextMenuAction { match self { ContextMenuAction::WindowClose => Message::WindowClose, ContextMenuAction::ToggleHideContent => Message::ToggleHideContent, + ContextMenuAction::ToggleSomeAction => Message::ToggleSomeAction, ContextMenuAction::WindowNew => Message::WindowNew, } } diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 291d7db0..48b0f4ea 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -650,7 +650,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { state.pressed = false; // process close condition - if state + if state.open && state .view_cursor .position() .unwrap_or_default() @@ -1428,47 +1428,48 @@ where state.view_cursor = view_cursor; // * remove invalid menus + if state.open { + 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::>(); - 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() + 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(); } - .map(|s| s.menu_bounds.children_bounds), - ) - .collect::>(); + } else if menu.is_overlay { + for i in (0..state.menu_states.len()).rev() { + let mb = &state.menu_states[i].menu_bounds; - 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; + 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(); } - 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(); } } From 637c454af8764e12497e1a61e5750159c1d5f69b Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Thu, 20 Aug 2026 09:06:44 -0600 Subject: [PATCH 2/8] On platforms without state_dir, fall back to ~/.local/state --- cosmic-config/src/lib.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 1396b5cb..b42a6367 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -48,7 +48,12 @@ fn get_state_dir() -> Option { return Some(PathBuf::from(home).join(".local").join("state")); } } - dirs::state_dir() + if let Some(state_dir) = dirs::state_dir() { + return Some(state_dir); + } + // On platforms without state_dir, fall back to ~/.local/state + let home_dir = dirs::home_dir()?; + Some(home_dir.join(".local").join(".state")) } /// Get the data directory, with Flatpak sandbox support. From 2ab0d4c57079f0baf91d24b08a3821984121af62 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Thu, 20 Aug 2026 21:40:09 +0200 Subject: [PATCH 3/8] fix(iced): allow scrolling when children capture touch presses --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 7918b282..87c03589 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 7918b2826ec8ad4f9e0032f6a21286bac431e482 +Subproject commit 87c0358985cae50fefdcb15eabb0271e19ecf4a2 From fd34b923f77410bc9bc6f3914ce43869500ea1c9 Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Fri, 21 Aug 2026 08:56:00 -0600 Subject: [PATCH 4/8] Revert "feat(cosmic-config): add `new_data` method." It was found that dirs::data_dir conflicts with dirs::config_dir on macOS and Windows. This reverts commit 631c81b170b792fa57898161b73a5a673ad79b48. --- cosmic-config/src/lib.rs | 50 ------------------------------------- examples/config/src/main.rs | 3 --- 2 files changed, 53 deletions(-) diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index b42a6367..e51b4f6a 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -56,22 +56,6 @@ fn get_state_dir() -> Option { Some(home_dir.join(".local").join(".state")) } -/// Get the data directory, with Flatpak sandbox support. -fn get_data_dir() -> Option { - // Check if we're running in Flatpak - if env::var_os("FLATPAK_ID").is_some() { - // Try HOST_XDG_DATA_HOME first - if let Some(host_data) = env::var_os("HOST_XDG_DATA_HOME") { - return Some(PathBuf::from(host_data)); - } - // Fallback: try to construct from HOME - if let Some(home) = env::var_os("HOME") { - return Some(PathBuf::from(home).join(".local").join("share")); - } - } - dirs::data_dir() -} - #[cfg(feature = "subscription")] mod subscription; #[cfg(feature = "subscription")] @@ -332,40 +316,6 @@ impl Config { }) } - /// Get data for the given application name and config version. - /// pub fn new_state(name: &str, version: u64) -> Result { - pub fn new_data(name: &str, version: u64) -> Result { - Self::new_data_inner(name, version, true) - } - - pub fn new_data_inner( - name: &str, - version: u64, - look_for_previous: bool, - ) -> Result { - // Look for [name]/v[version] - let path = sanitize_name(name)?.join(format!("v{}", version)); - - // Get libcosmic user data directory - let mut user_path = get_data_dir().ok_or(Error::NoConfigDirectory)?; - user_path.push("cosmic"); - user_path.push(path); - // Create new data directory if not found. - fs::create_dir_all(&user_path)?; - - Ok(Self { - system_path: None, - user_path: Some(user_path), - previous: if version > 1 && look_for_previous { - Self::new_data_inner(name, version - 1, false) - .ok() - .map(Box::new) - } else { - None - }, - }) - } - // Start a transaction (to set multiple configs at the same time) #[inline] pub fn transaction(&self) -> ConfigTransaction<'_> { diff --git a/examples/config/src/main.rs b/examples/config/src/main.rs index dfcc5b99..f6fb5c0d 100644 --- a/examples/config/src/main.rs +++ b/examples/config/src/main.rs @@ -88,7 +88,4 @@ pub fn main() { println!("Testing state"); test_config(Config::new_state("com.system76.Example", 1).unwrap()); - - println!("Testing data"); - test_config(Config::new_data("com.system76.Example", 1).unwrap()); } From f8f5c7b5a6f639bdf19a742d1786a1395d12f930 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Mon, 24 Aug 2026 20:28:46 -0400 Subject: [PATCH 5/8] feat: popover top position (#1403) --- examples/context-menu/src/main.rs | 33 ++++++++++++++++--------------- src/widget/menu/menu_inner.rs | 13 ++++++------ src/widget/popover.rs | 8 ++++++++ 3 files changed, 32 insertions(+), 22 deletions(-) diff --git a/examples/context-menu/src/main.rs b/examples/context-menu/src/main.rs index a953c8ed..50c76bfb 100644 --- a/examples/context-menu/src/main.rs +++ b/examples/context-menu/src/main.rs @@ -93,10 +93,10 @@ impl cosmic::Application for App { cosmic::app::Action::Surface(action), )); } - Message::WindowClose | - Message::ToggleHideContent | - Message::ToggleSomeAction | - Message::WindowNew=> {} + Message::WindowClose + | Message::ToggleHideContent + | Message::ToggleSomeAction + | Message::WindowNew => {} } Task::none() @@ -129,18 +129,19 @@ impl App { menu::Item::Divider, menu::Item::Folder( "View", - vec![menu::Item::CheckBox( - "Hide content", - None, - self.hide_content, - ContextMenuAction::ToggleHideContent, - ), - menu::Item::CheckBox( - "Test content", - None, - self.hide_content, - ContextMenuAction::ToggleSomeAction, - ) + vec![ + menu::Item::CheckBox( + "Hide content", + None, + self.hide_content, + ContextMenuAction::ToggleHideContent, + ), + menu::Item::CheckBox( + "Test content", + None, + self.hide_content, + ContextMenuAction::ToggleSomeAction, + ), ], ), menu::Item::Divider, diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 48b0f4ea..0ec6110b 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -650,12 +650,13 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { state.pressed = false; // process close condition - if state.open && state - .view_cursor - .position() - .unwrap_or_default() - .distance(view_cursor.position().unwrap_or_default()) - < 2.0 + if state.open + && state + .view_cursor + .position() + .unwrap_or_default() + .distance(view_cursor.position().unwrap_or_default()) + < 2.0 { let is_inside = state.menu_states[..=if self.is_overlay { state.active_root.len().saturating_sub(1) diff --git a/src/widget/popover.rs b/src/widget/popover.rs index c822899d..4368925e 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -24,6 +24,7 @@ pub enum Position { #[default] Center, Bottom, + Top, Point(Point), } @@ -260,6 +261,7 @@ where Position::Point(relative) => { bounds.position() + Vector::new(relative.x, relative.y) } + Position::Top => Point::new(bounds.x + bounds.width / 2.0, bounds.y), }; // Round position to prevent rendering issues @@ -354,6 +356,12 @@ where position.x = (position.x - width / 2.0).clamp(0.0, bounds.width - width); position.y = (position.y - height / 2.0).clamp(0.0, bounds.height - height); } + Position::Top => { + let width = node.size().width; + let height = node.size().height; + position.x = (position.x - width / 2.0).clamp(0.0, bounds.width - width); + position.y = (position.y - height).clamp(0.0, bounds.height - height); + } Position::Bottom => { // Position is set to the center bottom of the widget let width = node.size().width; From 2a73fbc0edfe1525381bf999e241d73def79b222 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Tue, 25 Aug 2026 16:43:06 +0200 Subject: [PATCH 6/8] fix(segmented_button): request redraw on tab focus shift --- 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 29c26362..ea1c1a31 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -1867,6 +1867,7 @@ where .. }) = event { + shell.request_redraw(); state.focused_visible = true; return if *modifiers == keyboard::Modifiers::SHIFT { self.focus_previous(state, shell); From 3b62210d27dc486f93953ed5e7e94c48bce74532 Mon Sep 17 00:00:00 2001 From: Eric Le Bihan Date: Mon, 24 Aug 2026 14:20:45 +0200 Subject: [PATCH 7/8] fix(menu): request redraw when menu bar opens When using a menu bar without Wayland popup support, the menu items are unclickable, as redrawing is never requested. So, request a redraw after opening so the overlay menu is created and drawn immediately. --- src/widget/menu/menu_bar.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 71a39c42..ba2a8991 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -648,6 +648,7 @@ where return; } shell.capture_event(); + shell.request_redraw(); #[cfg(wayland_platform)] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); From caec74c2559924443f12fc6faf97a5bcefe6271d Mon Sep 17 00:00:00 2001 From: Eric Le Bihan Date: Mon, 24 Aug 2026 14:20:51 +0200 Subject: [PATCH 8/8] fix(menu example): fix key binds not being dispatched The menu example advertised key binds (Ctrl+W / Ctrl+Shift+N) that were never dispatched. So add a subscription to take them into account. --- examples/menu/src/main.rs | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/examples/menu/src/main.rs b/examples/menu/src/main.rs index 900d0322..35933b42 100644 --- a/examples/menu/src/main.rs +++ b/examples/menu/src/main.rs @@ -9,7 +9,7 @@ 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, Length, Size}; +use cosmic::iced::{event, keyboard, window, Event, Length, Size, Subscription}; use cosmic::widget::menu::action::MenuAction; use cosmic::widget::menu::key_bind::{KeyBind, Modifier}; use cosmic::widget::menu::{self, ItemHeight, ItemWidth}; @@ -112,6 +112,26 @@ impl cosmic::Application for App { vec![menu_bar(&self.config, &self.key_binds)] } + fn subscription(&self) -> Subscription { + event::listen_with(|event, _status, _window_id| { + let Event::Keyboard(keyboard::Event::KeyPressed { + key, + modifiers, + physical_key, + .. + }) = event + else { + return None; + }; + + let key_binds = key_binds(); + key_binds + .iter() + .find(|(key_bind, _)| key_bind.matches(modifiers, &key, Some(&physical_key))) + .map(|(_, action)| action.message()) + }) + } + /// Handle application events here. fn update(&mut self, message: Self::Message) -> Task { match message {