From 6dba6a2561c84e5a1bf527c56856e7d78fb3c094 Mon Sep 17 00:00:00 2001 From: KENZ Date: Mon, 20 Apr 2026 00:02:26 +0900 Subject: [PATCH 001/201] fix(iced): make IME event forwarding to sctk optional pulls in the change to make IME events forwarding to sctk optionally --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 78caabba..aa2a870c 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 78caabba7ef91cd1030da6f70b41d266704ffece +Subproject commit aa2a870c357284630045b05186fa7a6313c27693 From dc84488cd87376b66d9d71275362366355bb228d Mon Sep 17 00:00:00 2001 From: KENZ Date: Mon, 20 Apr 2026 00:03:20 +0900 Subject: [PATCH 002/201] fix(iced): double IME commit on GNOME desktop --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index d73da2dc..2a9684fd 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -69,7 +69,7 @@ desktop-systemd-scope = ["desktop", "dep:zbus"] # Enables keycode serialization serde-keycode = ["iced_core/serde"] # Prevents multiple separate process instances. -single-instance = ["zbus/blocking-api", "ron"] +single-instance = ["iced_winit/single-instance", "zbus/blocking-api", "ron"] # smol async runtime smol = ["dep:smol", "iced/smol", "zbus?/async-io", "rfd?/async-std"] tokio = [ From 4d39cf3d7b340cc75a508df42a3fa037e652dcde 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, 19 Apr 2026 00:30:20 +0200 Subject: [PATCH 003/201] improv(progress_bar): unify indeterminate animations --- src/widget/progress_bar/animation.rs | 73 +++++++ src/widget/progress_bar/circular.rs | 303 +++++---------------------- src/widget/progress_bar/linear.rs | 169 +++++---------- src/widget/progress_bar/mod.rs | 1 + 4 files changed, 178 insertions(+), 368 deletions(-) create mode 100644 src/widget/progress_bar/animation.rs diff --git a/src/widget/progress_bar/animation.rs b/src/widget/progress_bar/animation.rs new file mode 100644 index 00000000..52a3432a --- /dev/null +++ b/src/widget/progress_bar/animation.rs @@ -0,0 +1,73 @@ +use crate::anim::smootherstep; +use iced::time::Instant; +use std::time::Duration; + +#[derive(Clone, Copy)] +pub struct Animation { + expanding: bool, + start: Instant, + last: Instant, + offset: u32, +} + +impl Default for Animation { + fn default() -> Self { + let now = Instant::now(); + Self { + expanding: true, + start: now, + last: now, + offset: 0, + } + } +} + +impl Animation { + pub fn timed_transition( + &self, + cycle_duration: Duration, + rotation_duration: Duration, + wrap: f32, + now: Instant, + ) -> Self { + let additional = ((now - self.last).as_secs_f32() / rotation_duration.as_secs_f32() + * u32::MAX as f32) as u32; + let new_offset = self.offset.wrapping_add(additional); + + if !cycle_duration.is_zero() && now.duration_since(self.start) > cycle_duration { + let offset = if self.expanding { + new_offset + } else { + new_offset.wrapping_add((wrap * u32::MAX as f32) as u32) + }; + Self { + expanding: !self.expanding, + start: now, + last: now, + offset, + } + } else { + Self { + last: now, + offset: new_offset, + ..*self + } + } + } + + pub fn bar_positions(&self, cycle_duration: Duration, min: f32, wrap: f32) -> (f32, f32) { + let offset = self.offset as f32 / u32::MAX as f32; + let progress = if !cycle_duration.is_zero() { + smootherstep( + self.last.duration_since(self.start).as_secs_f32() / cycle_duration.as_secs_f32(), + ) + } else { + 1.0 + }; + if self.expanding { + (offset, offset + min + wrap * progress) + } else { + (offset + wrap * progress, offset + min + wrap) + } + } +} diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs index fa8c38fe..684467f1 100644 --- a/src/widget/progress_bar/circular.rs +++ b/src/widget/progress_bar/circular.rs @@ -1,12 +1,11 @@ //! Show a circular progress indicator. +use super::animation::Animation; 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}; @@ -23,7 +22,7 @@ where { size: f32, bar_height: f32, - style: ::Style, + style: Theme::Style, cycle_duration: Duration, rotation_duration: Duration, progress: Option, @@ -38,7 +37,7 @@ where Circular { size: 40.0, bar_height: 4.0, - style: ::Style::default(), + style: Theme::Style::default(), cycle_duration: Duration::from_millis(1500), rotation_duration: Duration::from_secs(2), progress: None, @@ -58,7 +57,7 @@ where } /// Sets the style variant of this [`Circular`]. - pub fn style(mut self, style: ::Style) -> Self { + pub fn style(mut self, style: Theme::Style) -> Self { self.style = style; self } @@ -70,7 +69,7 @@ where } /// 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) + /// rotation would take if the cycle duration were set to 0.0 (no expanding or contracting) pub fn rotation_duration(mut self, duration: Duration) -> Self { self.rotation_duration = duration; self @@ -82,10 +81,10 @@ where self } - fn min_wrap_angle(&self, track_radius: f32) -> (f32, f32) { + fn min_wrap(&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) + ((gap - cap_angle) / (2.0 * PI), 1.0 - gap / PI) } } @@ -98,120 +97,6 @@ where } } -#[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, wrap_angle: f32, 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( - (f64::from((wrap_angle) / (2.0 * 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, - wrap_angle: f32, - 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, wrap_angle, 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, @@ -261,25 +146,20 @@ where ) { 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() - ) { + if state.progress != self.progress { state.progress = self.progress; state.cache.clear(); } return; } if let Event::Window(window::Event::RedrawRequested(now)) = event { - let (_, wrap_angle) = self.min_wrap_angle(self.size / 2.0 - self.bar_height); + let (_, wrap) = self.min_wrap(self.size / 2.0 - self.bar_height); state.animation = state.animation.timed_transition( self.cycle_duration, self.rotation_duration, - wrap_angle, + wrap, *now, ); - state.cache.clear(); shell.request_redraw(); } @@ -299,8 +179,7 @@ where let state = tree.state.downcast_ref::(); let bounds = layout.bounds(); - let custom_style = - ::appearance(theme, &self.style, self.progress.is_some(), true); + let custom_style = Theme::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; @@ -313,133 +192,65 @@ where .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); + // Converts a track fraction to an angle in radians, with 0 being top of circle + let to_angle = |t: f32| t * 2.0 * PI - PI / 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 = 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), - ), - Animation::Contracting { progress, .. } => ( - start + wrap_angle * smootherstep(progress), - start + min_angle + wrap_angle, - ), + let draw_cap = |frame: &mut canvas::Frame, t: f32, flip: bool| { + let angle = to_angle(t); + let center = frame.center() + Vector::new(angle.cos(), angle.sin()) * track_radius; + let (start_angle, end_angle) = if flip { + (angle - PI, angle) + } else { + (angle, angle + PI) }; + let mut builder = canvas::path::Builder::new(); builder.arc(canvas::path::Arc { - center: frame.center(), - radius: track_radius, + center, + radius: self.bar_height / 2.0, start_angle: Radians(start_angle), end_angle: Radians(end_angle), }); + frame.fill(&builder.build(), custom_style.bar_color); + }; - let bar_path = builder.build(); - + let draw_bar = |frame: &mut canvas::Frame, start: f32, end: f32| { + let mut builder = canvas::path::Builder::new(); + builder.arc(canvas::path::Arc { + center: frame.center(), + radius: track_radius, + start_angle: Radians(to_angle(start)), + end_angle: Radians(to_angle(end)), + }); frame.stroke( - &bar_path, + &builder.build(), canvas::Stroke::default() .with_color(custom_style.bar_color) .with_width(self.bar_height), ); + draw_cap(frame, end, false); + draw_cap(frame, start, true); + }; - 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.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_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); + if let Some(progress) = self.progress { + if let Some(border_color) = custom_style.border_color { + for radius_offset in [self.bar_height / 2.0, -(self.bar_height / 2.0)] { + let border_path = + canvas::Path::circle(frame.center(), track_radius + radius_offset); + frame.stroke( + &border_path, + canvas::Stroke::default() + .with_color(border_color) + .with_width(1.0), + ); + } + } + draw_bar(frame, 0.0, progress); + } else { + let (min, wrap) = self.min_wrap(track_radius); + let (start, end) = state + .animation + .bar_positions(self.cycle_duration, min, wrap); + draw_bar(frame, start, end); } }); diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index 226b2b5f..881d59c3 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -1,19 +1,19 @@ //! Show a linear progress indicator. +use super::animation::Animation; +use super::style::StyleSheet; use iced::advanced::layout; -use iced::advanced::renderer::{self, Quad}; +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::window; use iced::{Background, Element, Event, Length, Rectangle, Size}; -use crate::anim::smootherstep; - -use super::style::StyleSheet; - use std::time::Duration; +const MIN_LENGTH: f32 = 0.15; +const WRAP_LENGTH: f32 = 0.618; // avoids animation repetition + #[must_use] pub struct Linear where @@ -23,6 +23,7 @@ where girth: Length, style: Theme::Style, cycle_duration: Duration, + traversal_duration: Duration, progress: Option, } @@ -37,6 +38,7 @@ where girth: Length::Fixed(4.0), style: Theme::Style::default(), cycle_duration: Duration::from_millis(1500), + traversal_duration: Duration::from_secs(2), progress: None, } } @@ -65,6 +67,13 @@ where self } + /// Sets the base traversal duration of this [`Linear`]. This is the duration that a full + /// traversal would take if the cycle duration were set to 0.0 (no expanding or contracting) + pub fn traversal_duration(mut self, duration: Duration) -> Self { + self.traversal_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)); @@ -81,65 +90,6 @@ where } } -#[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, @@ -147,11 +97,11 @@ where Renderer: advanced::Renderer, { fn tag(&self) -> tree::Tag { - tree::Tag::of::() + tree::Tag::of::() } fn state(&self) -> tree::State { - tree::State::new(State::default()) + tree::State::new(Animation::default()) } fn size(&self) -> Size { @@ -185,11 +135,15 @@ where return; } - let state = tree.state.downcast_mut::(); + let animation = tree.state.downcast_mut::(); if let Event::Window(window::Event::RedrawRequested(now)) = event { - *state = state.timed_transition(self.cycle_duration, *now); - + *animation = animation.timed_transition( + self.cycle_duration, + self.traversal_duration, + WRAP_LENGTH, + *now, + ); shell.request_redraw(); } } @@ -206,16 +160,11 @@ where ) { let bounds = layout.bounds(); let custom_style = theme.appearance(&self.style, self.progress.is_some(), false); - let state = tree.state.downcast_ref::(); + let animation = tree.state.downcast_ref::(); renderer.fill_quad( renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: bounds.y, - width: bounds.width, - height: bounds.height, - }, + bounds, border: iced::Border { width: if custom_style.border_color.is_some() { 1.0 @@ -231,37 +180,18 @@ where 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( + let mut draw_segment = |x: f32, width: f32| { + if width > 0.001 { + renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: bounds.x, + x: bounds.x + x * bounds.width, y: bounds.y, - width: smootherstep(*progress) * bounds.width, + width: width * bounds.width, height: bounds.height, }, border: iced::Border { - width: 0., + width: 0.0, color: iced::Color::TRANSPARENT, radius: custom_style.border_radius.into(), }, @@ -269,27 +199,22 @@ where ..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), - ), + ); } + }; + + if let Some(progress) = self.progress { + draw_segment(0.0, progress); + } else { + let (bar_start, bar_end) = + animation.bar_positions(self.cycle_duration, MIN_LENGTH, WRAP_LENGTH); + let length = bar_end - bar_start; + let start = bar_start % 1.0; + let right_width = (1.0 - start).min(length); + let left_width = length - right_width; + + draw_segment(start, right_width); + draw_segment(0.0, left_width); } } } diff --git a/src/widget/progress_bar/mod.rs b/src/widget/progress_bar/mod.rs index 4e277b0a..cb24adac 100644 --- a/src/widget/progress_bar/mod.rs +++ b/src/widget/progress_bar/mod.rs @@ -1,3 +1,4 @@ +mod animation; pub mod circular; pub mod linear; pub mod style; From 8b4c8adec830d74802931b551cd34f5c046ea6a4 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, 20 Apr 2026 14:00:09 +0200 Subject: [PATCH 004/201] feat(progress_bar): animate determinate progress --- src/widget/progress_bar/animation.rs | 39 ++++++++++++++++-- src/widget/progress_bar/circular.rs | 50 +++++++++++------------ src/widget/progress_bar/linear.rs | 61 ++++++++++++++++------------ 3 files changed, 95 insertions(+), 55 deletions(-) diff --git a/src/widget/progress_bar/animation.rs b/src/widget/progress_bar/animation.rs index 52a3432a..6baf5071 100644 --- a/src/widget/progress_bar/animation.rs +++ b/src/widget/progress_bar/animation.rs @@ -2,6 +2,39 @@ use crate::anim::smootherstep; use iced::time::Instant; use std::time::Duration; +const LAG: f32 = 0.1; + +pub struct Progress { + pub current: f32, + last: Instant, +} + +impl Default for Progress { + fn default() -> Self { + Self { + current: 0.0, + last: Instant::now(), + } + } +} + +impl Progress { + /// Smoothly chases `target` using exponential decay. + /// Returns `true` if still animating and a redraw should be requested. + pub fn update(&mut self, target: f32, now: Instant) -> bool { + let dt = (now - self.last).as_secs_f32(); + self.last = now; + let next = self.current + (target - self.current) * (1.0 - (-dt / LAG).exp()); + if (next - target).abs() > 0.001 { + self.current = next; + true + } else { + self.current = target; + false + } + } +} + #[derive(Clone, Copy)] pub struct Animation { expanding: bool, @@ -26,12 +59,12 @@ impl Animation { pub fn timed_transition( &self, cycle_duration: Duration, - rotation_duration: Duration, + period: Duration, wrap: f32, now: Instant, ) -> Self { - let additional = ((now - self.last).as_secs_f32() / rotation_duration.as_secs_f32() - * u32::MAX as f32) as u32; + let additional = + ((now - self.last).as_secs_f32() / period.as_secs_f32() * u32::MAX as f32) as u32; let new_offset = self.offset.wrapping_add(additional); if !cycle_duration.is_zero() && now.duration_since(self.start) > cycle_duration { diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs index 684467f1..42c8e642 100644 --- a/src/widget/progress_bar/circular.rs +++ b/src/widget/progress_bar/circular.rs @@ -1,5 +1,5 @@ //! Show a circular progress indicator. -use super::animation::Animation; +use super::animation::{Animation, Progress}; use super::style::StyleSheet; use iced::advanced::layout; use iced::advanced::renderer; @@ -24,7 +24,7 @@ where bar_height: f32, style: Theme::Style, cycle_duration: Duration, - rotation_duration: Duration, + period: Duration, progress: Option, } @@ -39,7 +39,7 @@ where bar_height: 4.0, style: Theme::Style::default(), cycle_duration: Duration::from_millis(1500), - rotation_duration: Duration::from_secs(2), + period: Duration::from_secs(2), progress: None, } } @@ -68,10 +68,10 @@ where self } - /// Sets the base rotation duration of this [`Circular`]. This is the duration that a full - /// rotation would take if the cycle duration were set to 0.0 (no expanding or contracting) - pub fn rotation_duration(mut self, duration: Duration) -> Self { - self.rotation_duration = duration; + /// Sets the base period of this [`Circular`]. This is the duration that a full rotation + /// would take if the cycle duration were set to 0.0 (no expanding or contracting) + pub fn period(mut self, duration: Duration) -> Self { + self.period = duration; self } @@ -101,7 +101,7 @@ where struct State { animation: Animation, cache: canvas::Cache, - progress: Option, + progress: Progress, } impl Widget for Circular @@ -145,23 +145,21 @@ where _viewport: &Rectangle, ) { let state = tree.state.downcast_mut::(); - if self.progress.is_some() { - if state.progress != self.progress { - state.progress = self.progress; - state.cache.clear(); - } - return; - } if let Event::Window(window::Event::RedrawRequested(now)) = event { - let (_, wrap) = self.min_wrap(self.size / 2.0 - self.bar_height); - state.animation = state.animation.timed_transition( - self.cycle_duration, - self.rotation_duration, - wrap, - *now, - ); - state.cache.clear(); - shell.request_redraw(); + if let Some(target) = self.progress { + if state.progress.update(target, *now) { + state.cache.clear(); + shell.request_redraw(); + } + } else { + let (_, wrap) = self.min_wrap(self.size / 2.0 - self.bar_height); + state.animation = + state + .animation + .timed_transition(self.cycle_duration, self.period, wrap, *now); + state.cache.clear(); + shell.request_redraw(); + } } } @@ -231,7 +229,7 @@ where draw_cap(frame, start, true); }; - if let Some(progress) = self.progress { + if self.progress.is_some() { if let Some(border_color) = custom_style.border_color { for radius_offset in [self.bar_height / 2.0, -(self.bar_height / 2.0)] { let border_path = @@ -244,7 +242,7 @@ where ); } } - draw_bar(frame, 0.0, progress); + draw_bar(frame, 0.0, state.progress.current); } else { let (min, wrap) = self.min_wrap(track_radius); let (start, end) = state diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index 881d59c3..7bcbb4f7 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -1,5 +1,5 @@ //! Show a linear progress indicator. -use super::animation::Animation; +use super::animation::{Animation, Progress}; use super::style::StyleSheet; use iced::advanced::layout; use iced::advanced::renderer; @@ -23,7 +23,7 @@ where girth: Length, style: Theme::Style, cycle_duration: Duration, - traversal_duration: Duration, + period: Duration, progress: Option, } @@ -38,7 +38,7 @@ where girth: Length::Fixed(4.0), style: Theme::Style::default(), cycle_duration: Duration::from_millis(1500), - traversal_duration: Duration::from_secs(2), + period: Duration::from_secs(2), progress: None, } } @@ -67,10 +67,10 @@ where self } - /// Sets the base traversal duration of this [`Linear`]. This is the duration that a full - /// traversal would take if the cycle duration were set to 0.0 (no expanding or contracting) - pub fn traversal_duration(mut self, duration: Duration) -> Self { - self.traversal_duration = duration; + /// Sets the base period of this [`Linear`]. This is the duration that a full traversal + /// would take if the cycle duration were set to 0.0 (no expanding or contracting) + pub fn period(mut self, duration: Duration) -> Self { + self.period = duration; self } @@ -90,6 +90,12 @@ where } } +#[derive(Default)] +struct State { + animation: Animation, + progress: Progress, +} + impl Widget for Linear where Message: Clone, @@ -97,11 +103,11 @@ where Renderer: advanced::Renderer, { fn tag(&self) -> tree::Tag { - tree::Tag::of::() + tree::Tag::of::() } fn state(&self) -> tree::State { - tree::State::new(Animation::default()) + tree::State::new(State::default()) } fn size(&self) -> Size { @@ -131,20 +137,21 @@ where shell: &mut Shell<'_, Message>, _viewport: &Rectangle, ) { - if self.progress.is_some() { - return; - } - - let animation = tree.state.downcast_mut::(); - + let state = tree.state.downcast_mut::(); if let Event::Window(window::Event::RedrawRequested(now)) = event { - *animation = animation.timed_transition( - self.cycle_duration, - self.traversal_duration, - WRAP_LENGTH, - *now, - ); - shell.request_redraw(); + if let Some(target) = self.progress { + if state.progress.update(target, *now) { + shell.request_redraw(); + } + } else { + state.animation = state.animation.timed_transition( + self.cycle_duration, + self.period, + WRAP_LENGTH, + *now, + ); + shell.request_redraw(); + } } } @@ -160,7 +167,7 @@ where ) { let bounds = layout.bounds(); let custom_style = theme.appearance(&self.style, self.progress.is_some(), false); - let animation = tree.state.downcast_ref::(); + let state = tree.state.downcast_ref::(); renderer.fill_quad( renderer::Quad { @@ -203,11 +210,13 @@ where } }; - if let Some(progress) = self.progress { - draw_segment(0.0, progress); + if self.progress.is_some() { + draw_segment(0.0, state.progress.current); } else { let (bar_start, bar_end) = - animation.bar_positions(self.cycle_duration, MIN_LENGTH, WRAP_LENGTH); + state + .animation + .bar_positions(self.cycle_duration, MIN_LENGTH, WRAP_LENGTH); let length = bar_end - bar_start; let start = bar_start % 1.0; let right_width = (1.0 - start).min(length); From dad5f1e2731dbdccb3044f136a81f18dfead9de4 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, 22 Apr 2026 12:26:29 +0200 Subject: [PATCH 005/201] fix(progress_bar): sync determinate animation clock on target change This prevents the time delta from becoming very large when the widget hasn't animated for a bit, leading to snapping instead of a smooth animation. Also prevents animating on widget creation, since it doesn't feel good in `cosmic-osd`. --- src/widget/progress_bar/animation.rs | 41 +++++++++++++++++----------- 1 file changed, 25 insertions(+), 16 deletions(-) diff --git a/src/widget/progress_bar/animation.rs b/src/widget/progress_bar/animation.rs index 6baf5071..a9d52831 100644 --- a/src/widget/progress_bar/animation.rs +++ b/src/widget/progress_bar/animation.rs @@ -4,29 +4,38 @@ use std::time::Duration; const LAG: f32 = 0.1; +#[derive(Default)] pub struct Progress { pub current: f32, - last: Instant, -} - -impl Default for Progress { - fn default() -> Self { - Self { - current: 0.0, - last: Instant::now(), - } - } + target: Option, + last: Option, } impl Progress { /// Smoothly chases `target` using exponential decay. - /// Returns `true` if still animating and a redraw should be requested. + /// Returns `true` if a redraw should be requested. pub fn update(&mut self, target: f32, now: Instant) -> bool { - let dt = (now - self.last).as_secs_f32(); - self.last = now; - let next = self.current + (target - self.current) * (1.0 - (-dt / LAG).exp()); - if (next - target).abs() > 0.001 { - self.current = next; + // Don't animate on start + let Some(last) = self.last else { + self.current = target; + self.target = Some(target); + self.last = Some(now); + return false; + }; + + // Sync animation clock when target changes + if self.target != Some(target) { + self.target = Some(target); + self.last = Some(now); + return true; + } + + let dt = (now - last).as_secs_f32(); + self.last = Some(now); + let diff = target - self.current; + + if diff.abs() > 0.001 { + self.current += diff * (1.0 - (-dt / LAG).exp()); true } else { self.current = target; From 17291536a10124a053b40c49bb459d7b5085331b 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, 22 Apr 2026 22:46:50 +0200 Subject: [PATCH 006/201] fix(header_bar): prevent double coloring --- src/app/mod.rs | 1 - src/widget/header_bar.rs | 9 +++------ 2 files changed, 3 insertions(+), 7 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index f78beac7..ad1ceefe 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -773,7 +773,6 @@ 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/widget/header_bar.rs b/src/widget/header_bar.rs index a772f7d2..0a10b0aa 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -26,7 +26,6 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { sharp_corners: false, is_ssd: false, on_double_click: None, - transparent: false, } } @@ -88,9 +87,6 @@ pub struct HeaderBar<'a, Message> { /// HeaderBar used for server-side decorations is_ssd: bool, - - /// Whether the headerbar should be transparent - transparent: bool, } impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { @@ -370,6 +366,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { space_xxs, .. } = theme::spacing(); + let is_ssd = self.is_ssd; // Take ownership of the regions to be packed. let start = std::mem::take(&mut self.start); @@ -379,7 +376,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { // Also packs the window controls at the very end. end.push(self.window_controls(space_xxs)); - let padding = if self.is_ssd { + let padding = if is_ssd { [2, 8, 2, 8] } else { match ( @@ -424,7 +421,7 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { .class(theme::Container::HeaderBar { focused: self.focused, sharp_corners: self.sharp_corners, - transparent: self.transparent, + transparent: if is_ssd { false } else { true }, }) .height(Length::Fixed(32.0 + padding[0] as f32 + padding[2] as f32)) .padding(padding) From 58b7532849fea259a22a7bce4a5cd6657b01e8bf Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 27 Apr 2026 14:37:22 +0200 Subject: [PATCH 007/201] chore: update MSRV to Rust 1.93 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 2a9684fd..3927c18b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -2,7 +2,7 @@ name = "libcosmic" version = "1.0.0" edition = "2024" -rust-version = "1.90" +rust-version = "1.93" [lib] name = "cosmic" From 21977ef617295cd4f872b86bd113fd0e2fd11cf0 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 27 Apr 2026 14:39:52 +0200 Subject: [PATCH 008/201] feat(iced): update to wgpu 28 & cosmic-text 0.19 --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index aa2a870c..90d2c418 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit aa2a870c357284630045b05186fa7a6313c27693 +Subproject commit 90d2c418a73af0669302e58fb6a0978bb73a7436 From f3fc763b5ff872a5dcf1136b5038cf82de239ac8 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 27 Apr 2026 14:43:39 +0200 Subject: [PATCH 009/201] ci(pages): update Rust toolchain to latest nightly --- .github/workflows/pages.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index 3e3a042e..af5b059b 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -17,13 +17,13 @@ jobs: - name: Install Rust nightly uses: dtolnay/rust-toolchain@master with: - toolchain: nightly-2025-07-31 + toolchain: nightly-2026-04-27 - 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 \ + cargo +nightly-2026-04-27 doc --no-deps \ -p cosmic-client-toolkit \ -p cosmic-protocols \ -p libcosmic \ From 5825afb0dafe00353674ee47087de7183fc8a7c8 Mon Sep 17 00:00:00 2001 From: Will Sheehan Date: Sat, 25 Apr 2026 14:01:34 -0700 Subject: [PATCH 010/201] fix(segmented_button): reset menu state when reopening context menu When a context menu is open and the user right-clicks a different item, the overlay's close handler resets the menu state but no longer captures the event (since e10459f). The right-click propagates to the widget, which reopens the menu with stale `MenuBounds` from the previous context menu. If the new menu has a different number of items, this causes an assertion failure in `MenuState::layout`. Call `data.reset()` before setting `data.open = true` in the right-click handler to ensure stale `MenuBounds` are cleared before the next layout pass rebuilds them. --- src/widget/segmented_button/widget.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 44ca8574..fcb1c596 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -1398,6 +1398,8 @@ where state.context_cursor = cursor_position.position().unwrap_or_default(); state.menu_state.inner.with_data_mut(|data| { + // Clear stale MenuBounds from any previous context menu before opening a new one. + data.reset(); data.open = true; data.view_cursor = cursor_position; }); From 51be7dc42d9e9746b42cf57d89d9924180b463fe Mon Sep 17 00:00:00 2001 From: Rakshat Date: Mon, 27 Apr 2026 18:30:52 +0530 Subject: [PATCH 011/201] fix(menu_bar): use corner radius from theme for menu border --- src/theme/style/menu_bar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theme/style/menu_bar.rs b/src/theme/style/menu_bar.rs index ed0e657a..75525014 100644 --- a/src/theme/style/menu_bar.rs +++ b/src/theme/style/menu_bar.rs @@ -71,7 +71,7 @@ impl StyleSheet for Theme { background: component.base.into(), border_width: 1.0, bar_border_radius: cosmic.corner_radii.radius_xl, - menu_border_radius: cosmic.corner_radii.radius_s.map(|x| x + 2.0), + menu_border_radius: cosmic.corner_radii.radius_m, border_color: component.divider.into(), background_expand: [1; 4], path: component.hover.into(), From 49846d9f284ced0777ae6afce0a1f72eea5ec2da Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 27 Apr 2026 15:04:47 +0200 Subject: [PATCH 012/201] fix: wgpu validation error on float16 shader with older GPUs --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 90d2c418..3928e0d3 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 90d2c418a73af0669302e58fb6a0978bb73a7436 +Subproject commit 3928e0d38572d63ff1ef1d768b17770e9a91d7c0 From 8768bce2e2a4921ced57d95cff74bc7c92ef08b8 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, 27 Apr 2026 15:23:41 +0200 Subject: [PATCH 013/201] fix(menu_bar): menu border radius --- src/theme/style/menu_bar.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/theme/style/menu_bar.rs b/src/theme/style/menu_bar.rs index 75525014..db6e0ba1 100644 --- a/src/theme/style/menu_bar.rs +++ b/src/theme/style/menu_bar.rs @@ -71,7 +71,7 @@ impl StyleSheet for Theme { background: component.base.into(), border_width: 1.0, bar_border_radius: cosmic.corner_radii.radius_xl, - menu_border_radius: cosmic.corner_radii.radius_m, + menu_border_radius: cosmic.corner_radii.radius_s, border_color: component.divider.into(), background_expand: [1; 4], path: component.hover.into(), From cae05f3ffbb48a7db4e512aaa41589c07e89d7f3 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Mon, 27 Apr 2026 16:10:03 +0200 Subject: [PATCH 014/201] i18n: translation updates from weblate Co-authored-by: Hosted Weblate Co-authored-by: Jim Spentzos Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/el/ Translation: Pop OS/libcosmic --- i18n/el/libcosmic.ftl | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/i18n/el/libcosmic.ftl b/i18n/el/libcosmic.ftl index e69de29b..f4fe0b31 100644 --- a/i18n/el/libcosmic.ftl +++ b/i18n/el/libcosmic.ftl @@ -0,0 +1,34 @@ +close = Κλείσιμο +february = Φεβρουάριος { $year } +documenters = Τεκμηριωτές +november = Νοέμβριος { $year } +may = Μάιος { $year } +april = Απρίλιος { $year } +translators = Μεταφραστές +artists = Καλλιτέχνες +license = Άδεια χρήσης +december = Δεκέμβριος { $year } +links = Σύνδεσμοι +march = Μάρτιος { $year } +june = Ιούνιος { $year } +august = Αύγουστος { $year } +developers = Προγραμματιστές +july = Ιούλιος { $year } +september = Σεπτέμβριος { $year } +designers = Σχεδιαστές +october = Οκτώβριος { $year } +january = Ιανουάριος { $year } +monday = Δευτέρα +mon = Δευ +tuesday = Τρίτη +tue = Τρί +wednesday = Τετάρτη +wed = Τετ +thursday = Πέμπτη +thu = Πέμ +friday = Παρασκευή +fri = Παρ +saturday = Σάββατο +sat = Σάβ +sunday = Κυριακή +sun = Κυρ From 564ef834cec33a948dc10c9b401cf29db5d18373 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, 28 Apr 2026 12:52:16 +0200 Subject: [PATCH 015/201] improv(theme): list_button style This adds a new `list_button` field to theme, which makes ListItem buttons stay transparent when pressed. It's essentially a mix of `text_button` and `background.component`, without hover highlights (as per designs). --- cosmic-theme/src/model/theme.rs | 11 +++++++++++ src/theme/style/button.rs | 26 +++++--------------------- 2 files changed, 16 insertions(+), 21 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 5db0f32c..1bc8050f 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -75,6 +75,8 @@ pub struct Theme { pub icon_button: Component, /// link button element colors pub link_button: Component, + /// list button element colors + pub list_button: Component, /// text button element colors pub text_button: Component, /// button component styling @@ -1285,6 +1287,15 @@ impl ThemeBuilder { component.on_disabled = over(component.on.with_alpha(0.5), component.base); component }, + list_button: Component::component( + Srgba::new(0.0, 0.0, 0.0, 0.0), + accent, + on_bg_component, + Srgba::new(0.0, 0.0, 0.0, 0.0), + button_pressed_overlay, + is_high_contrast, + control_steps_array[8], + ), success: Component::colored_component( success, control_steps_array[0], diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index bb52d9a6..e83e48e8 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -150,7 +150,7 @@ pub fn appearance( } Button::ListItem(radii) => { corner_radii = radii; - let (background, text, icon) = color(&cosmic.background.component); + let (background, text, icon) = color(&cosmic.list_button); if selected { appearance.background = @@ -197,7 +197,7 @@ impl Catalog for crate::Theme { return active(focused, self); } - let mut s = appearance(self, focused, selected, false, style, move |component| { + appearance(self, focused, selected, false, style, move |component| { let text_color = if matches!( style, Button::Icon | Button::IconVertical | Button::HeaderBar @@ -209,15 +209,7 @@ 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 { @@ -245,7 +237,7 @@ impl Catalog for crate::Theme { return hovered(focused, self); } - let mut s = appearance( + appearance( self, focused || matches!(style, Button::Image), selected, @@ -264,15 +256,7 @@ 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 { From 15a8a535dd3f822dfdb6a3b4c1cbc05b6e062534 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Sun, 3 May 2026 01:52:48 +0200 Subject: [PATCH 016/201] i18n: translation updates from weblate MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: BoneNI Co-authored-by: Hosted Weblate Co-authored-by: Марко М. Костић Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/sr/ Translation: Pop OS/libcosmic --- i18n/lo/libcosmic.ftl | 0 i18n/sr/libcosmic.ftl | 34 ++++++++++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) create mode 100644 i18n/lo/libcosmic.ftl diff --git a/i18n/lo/libcosmic.ftl b/i18n/lo/libcosmic.ftl new file mode 100644 index 00000000..e69de29b diff --git a/i18n/sr/libcosmic.ftl b/i18n/sr/libcosmic.ftl index e69de29b..e8b88303 100644 --- a/i18n/sr/libcosmic.ftl +++ b/i18n/sr/libcosmic.ftl @@ -0,0 +1,34 @@ +close = Затвори +february = Фебруар { $year } +documenters = Документатори +november = Новембар { $year } +may = Мај { $year } +april = Април { $year } +translators = Преводиоци +artists = Уметници +license = Дозвола +december = Децембар { $year } +links = Везе +march = Март { $year } +june = Јун { $year } +august = Август { $year } +developers = Програмери +july = Јул { $year } +september = Септембар { $year } +designers = Дизајнери +october = Октобар { $year } +january = Јануар { $year } +monday = Понедељак +mon = Пон +tuesday = Уторак +tue = Уто +wednesday = Среда +wed = Сре +thursday = Четвртак +thu = Чет +friday = Петак +fri = Пет +saturday = Субота +sat = Суб +sunday = Недеља +sun = Нед From 34326b3925b65d7859bd0458d1a96dee0b99f51e 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, 30 Apr 2026 15:15:32 +0200 Subject: [PATCH 017/201] chore(cosmic-theme): move Roundness to libcosmic --- cosmic-theme/src/model/corner.rs | 48 +++++++++++++++++++++ cosmic-theme/src/model/density.rs | 69 ------------------------------- cosmic-theme/src/model/mod.rs | 2 - cosmic-theme/src/model/spacing.rs | 56 +++++++++++++++++++++++++ 4 files changed, 104 insertions(+), 71 deletions(-) delete mode 100644 cosmic-theme/src/model/density.rs diff --git a/cosmic-theme/src/model/corner.rs b/cosmic-theme/src/model/corner.rs index ecd18c0b..f2fa95ec 100644 --- a/cosmic-theme/src/model/corner.rs +++ b/cosmic-theme/src/model/corner.rs @@ -29,3 +29,51 @@ impl Default for CornerRadii { } } } + +/// Roundness options for the Cosmic theme +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub enum Roundness { + /// Round style + #[default] + Round, + /// Slightly round style + SlightlyRound, + /// Square style + Square, +} + +impl From for CornerRadii { + fn from(value: Roundness) -> Self { + match value { + Roundness::Round => CornerRadii::default(), + Roundness::SlightlyRound => CornerRadii { + radius_0: [0.0; 4], + radius_xs: [2.0; 4], + radius_s: [8.0; 4], + radius_m: [8.0; 4], + radius_l: [8.0; 4], + radius_xl: [8.0; 4], + }, + Roundness::Square => CornerRadii { + radius_0: [0.0; 4], + radius_xs: [2.0; 4], + radius_s: [2.0; 4], + radius_m: [2.0; 4], + radius_l: [2.0; 4], + radius_xl: [2.0; 4], + }, + } + } +} + +impl From for Roundness { + fn from(value: CornerRadii) -> Self { + if (value.radius_m[0] - 16.0).abs() < 0.01 { + Self::Round + } else if (value.radius_m[0] - 8.0).abs() < 0.01 { + Self::SlightlyRound + } else { + Self::Square + } + } +} diff --git a/cosmic-theme/src/model/density.rs b/cosmic-theme/src/model/density.rs deleted file mode 100644 index 7655361c..00000000 --- a/cosmic-theme/src/model/density.rs +++ /dev/null @@ -1,69 +0,0 @@ -use crate::Spacing; -use serde::{Deserialize, Serialize}; - -/// Density options for the Cosmic theme -#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] -pub enum Density { - /// Lower padding/spacing of elements - Compact, - /// Higher padding/spacing of elements - Spacious, - /// Standard padding/spacing of elements - #[default] - Standard, -} - -impl From for Spacing { - fn from(value: Density) -> Self { - match value { - Density::Compact => Spacing { - space_none: 0, - space_xxxs: 4, - space_xxs: 4, - space_xs: 8, - space_s: 8, - space_m: 16, - space_l: 24, - space_xl: 32, - space_xxl: 48, - space_xxxl: 64, - }, - Density::Spacious => Spacing { - space_none: 4, - space_xxxs: 8, - space_xxs: 12, - space_xs: 16, - space_s: 24, - space_m: 32, - space_l: 48, - space_xl: 64, - space_xxl: 128, - space_xxxl: 160, - }, - Density::Standard => Spacing { - space_none: 0, - space_xxxs: 4, - space_xxs: 8, - space_xs: 12, - space_s: 16, - space_m: 24, - space_l: 32, - space_xl: 48, - space_xxl: 64, - space_xxxl: 128, - }, - } - } -} - -impl From for Density { - fn from(value: Spacing) -> Self { - if value.space_m.saturating_sub(16) == 0 { - Self::Compact - } else if value.space_m.saturating_sub(24) == 0 { - Self::Standard - } else { - Self::Spacious - } - } -} diff --git a/cosmic-theme/src/model/mod.rs b/cosmic-theme/src/model/mod.rs index f48d1a8d..19370dee 100644 --- a/cosmic-theme/src/model/mod.rs +++ b/cosmic-theme/src/model/mod.rs @@ -1,6 +1,5 @@ pub use corner::*; pub use cosmic_palette::*; -pub use density::*; pub use derivation::*; pub use mode::*; pub use spacing::*; @@ -8,7 +7,6 @@ pub use theme::*; mod corner; mod cosmic_palette; -mod density; mod derivation; mod mode; mod spacing; diff --git a/cosmic-theme/src/model/spacing.rs b/cosmic-theme/src/model/spacing.rs index 93b1bf43..f02cf510 100644 --- a/cosmic-theme/src/model/spacing.rs +++ b/cosmic-theme/src/model/spacing.rs @@ -41,3 +41,59 @@ impl Default for Spacing { } } } + +/// Density options for the Cosmic theme +#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Serialize, Deserialize)] +pub enum Density { + /// Lower padding/spacing of elements + Compact, + /// Standard padding/spacing of elements + #[default] + Standard, + /// Higher padding/spacing of elements + Spacious, +} + +impl From for Spacing { + fn from(value: Density) -> Self { + match value { + Density::Compact => Spacing { + space_none: 0, + space_xxxs: 4, + space_xxs: 4, + space_xs: 8, + space_s: 8, + space_m: 16, + space_l: 24, + space_xl: 32, + space_xxl: 48, + space_xxxl: 64, + }, + Density::Standard => Spacing::default(), + Density::Spacious => Spacing { + space_none: 4, + space_xxxs: 8, + space_xxs: 12, + space_xs: 16, + space_s: 24, + space_m: 32, + space_l: 48, + space_xl: 64, + space_xxl: 128, + space_xxxl: 160, + }, + } + } +} + +impl From for Density { + fn from(value: Spacing) -> Self { + if value.space_m.saturating_sub(16) == 0 { + Self::Compact + } else if value.space_m.saturating_sub(24) == 0 { + Self::Standard + } else { + Self::Spacious + } + } +} From 9abc4d483baba96cf9e0f769277bc56bfd3c9d28 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, 30 Apr 2026 15:56:13 +0200 Subject: [PATCH 018/201] chore: update dependencies --- Cargo.toml | 35 ++++++++++++++++++++--------------- cosmic-config/Cargo.toml | 12 ++++++------ cosmic-theme/Cargo.toml | 8 ++++---- 3 files changed, 30 insertions(+), 25 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3927c18b..de87a395 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -125,7 +125,7 @@ x11 = ["iced/x11", "iced_winit/x11"] apply = "0.3.0" ashpd = { version = "0.12.3", default-features = false, optional = true } async-fs = { version = "2.2", optional = true } -async-std = { version = "1.13", optional = true } +async-std = { workspace = true, optional = true } 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" @@ -150,31 +150,32 @@ image-extras = { version = "0.1.0", default-features = false, features = [ "xpm", "xbm", ], optional = true } -libc = { version = "0.2.183", optional = true } +libc = { version = "0.2.186", optional = true } log = "0.4" mime = { version = "0.3.17", optional = true } -palette = "0.7.6" +palette.workspace = true 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"] } +serde = { workspace = true, features = ["derive"] } slotmap = "1.1.1" smol = { version = "2.0.2", optional = true } -thiserror = "2.0.18" +thiserror.workspace = true taffy = { version = "0.9.2", features = ["grid"] } -tokio = { version = "1.50.0", optional = true } -tracing = "0.1.44" -unicode-segmentation = "1.12" +tokio = { workspace = true, optional = true } +tracing.workspace = true +unicode-segmentation = "1.13" url = "2.5.8" -zbus = { version = "5.14.0", default-features = false, optional = true } +zbus = { workspace = true, optional = true } float-cmp = "0.10.0" +ron = { workspace = true, 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.14.0", default-features = false } +zbus.workspace = true [target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } @@ -237,10 +238,6 @@ git = "https://github.com/pop-os/cosmic-panel" # path = "../cosmic-panel/cosmic-panel-config" optional = true -[dependencies.ron] -version = "0.12" -optional = true - [workspace] members = [ "cosmic-config", @@ -251,7 +248,15 @@ members = [ exclude = ["iced"] [workspace.dependencies] -dirs = "6.0.0" +async-std = "1.13" +dirs = "6.0" +palette = "0.7" +ron = "0.12" +serde = "1.0" +thiserror = "2.0" +tracing = "0.1" +tokio = "1.52" +zbus = {version = "5.15", default-features = false} [dev-dependencies] tempfile = "3.27.0" diff --git a/cosmic-config/Cargo.toml b/cosmic-config/Cargo.toml index 0a7653e0..0316c707 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -11,20 +11,20 @@ subscription = ["iced_futures"] [dependencies] cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } -zbus = { version = "5.14.0", default-features = false, optional = true } +zbus = { workspace = true, default-features = false, optional = true } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } calloop = { version = "0.14.4", optional = true } notify = "8.2.0" -ron = "0.12.0" -serde = "1.0.228" +ron.workspace = true +serde.workspace = true 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.50", optional = true, features = ["time"] } -async-std = { version = "1.13", optional = true } -tracing = "0.1" +tokio = { workspace = true, optional = true, features = ["time"] } +async-std = { workspace = true, optional = true } +tracing.workspace = true [target.'cfg(unix)'.dependencies] xdg = "3.0" diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 7e408d8d..15aee334 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -15,13 +15,13 @@ export = ["serde_json"] no-default = [] [dependencies] -palette = { version = "0.7.6", features = ["serializing"] } +palette = { workspace = true, features = ["serializing"] } almost = "0.2" -serde = { version = "1.0.228", features = ["derive"] } +serde = { workspace = true, features = ["derive"] } serde_json = { version = "1.0.149", optional = true, features = [ "preserve_order", ] } -ron = "0.12.0" +ron.workspace = true csscolorparser = { version = "0.8.3", features = ["serde"] } cosmic-config = { path = "../cosmic-config/", default-features = false, features = [ "subscription", @@ -29,7 +29,7 @@ cosmic-config = { path = "../cosmic-config/", default-features = false, features ] } configparser = "3.1.0" dirs.workspace = true -thiserror = "2.0.18" +thiserror.workspace = true [dev-dependencies] insta = "1.47.2" From 9c2a86a8f4d6faff2fe28627820c29c5430d95d5 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, 30 Apr 2026 16:07:25 +0200 Subject: [PATCH 019/201] chore: add rustfmt config --- .zed/settings.json | 15 +++++++ cosmic-config/src/dbus.rs | 11 +++-- cosmic-config/src/lib.rs | 19 ++++----- cosmic-config/src/subscription.rs | 9 ++-- cosmic-theme/src/model/theme.rs | 10 ++--- cosmic-theme/src/output/gtk4_output.rs | 17 ++++---- cosmic-theme/src/output/mod.rs | 3 +- cosmic-theme/src/output/qt56ct_output.rs | 14 +++---- cosmic-theme/src/output/qt_output.rs | 11 +++-- cosmic-theme/src/steps.rs | 3 +- examples/about/src/main.rs | 3 +- examples/applet/src/window.rs | 3 +- examples/application/src/main.rs | 3 +- examples/cosmic/src/window.rs | 14 +++---- examples/cosmic/src/window/bluetooth.rs | 8 ++-- examples/cosmic/src/window/demo.rs | 24 +++++------ examples/cosmic/src/window/desktop.rs | 11 ++--- .../cosmic/src/window/system_and_accounts.rs | 10 ++--- examples/menu/src/main.rs | 6 +-- examples/multi-window/src/window.rs | 14 +++---- examples/spin-button/src/main.rs | 15 ++----- examples/subscriptions/src/main.rs | 3 +- examples/table-view/src/main.rs | 3 +- rustfmt.toml | 1 + src/app/action.rs | 4 +- src/app/mod.rs | 6 +-- src/applet/column.rs | 6 +-- src/applet/mod.rs | 40 ++++++++---------- src/applet/row.rs | 6 +-- src/applet/token/subscription.rs | 13 +++--- src/applet/token/wayland_handler.rs | 35 +++++++--------- src/dbus_activation.rs | 20 ++++----- src/desktop.rs | 3 +- src/localize.rs | 6 +-- src/surface/action.rs | 3 +- src/surface/mod.rs | 4 +- src/theme/mod.rs | 8 +--- src/theme/style/button.rs | 6 +-- src/theme/style/iced.rs | 19 ++++----- src/theme/style/segmented_button.rs | 15 ++++--- src/widget/about.rs | 8 ++-- src/widget/aspect_ratio.rs | 5 +-- src/widget/autosize.rs | 9 ++-- src/widget/button/icon.rs | 8 +++- src/widget/button/image.rs | 11 ++--- src/widget/button/link.rs | 7 ++-- src/widget/button/style.rs | 3 +- src/widget/button/text.rs | 5 ++- src/widget/button/widget.rs | 13 ++---- src/widget/calendar.rs | 6 +-- src/widget/cards.rs | 25 +++++------ src/widget/color_picker/mod.rs | 10 ++--- src/widget/context_drawer/overlay.rs | 3 +- src/widget/context_drawer/widget.rs | 3 +- src/widget/context_menu.rs | 3 +- src/widget/dialog.rs | 7 +--- src/widget/dnd_destination.rs | 32 +++++--------- src/widget/dnd_source.rs | 22 ++++------ src/widget/dropdown/menu/appearance.rs | 3 +- src/widget/dropdown/multi/widget.rs | 4 +- src/widget/dropdown/widget.rs | 4 +- src/widget/frames.rs | 6 +-- src/widget/header_bar.rs | 3 +- src/widget/icon/named.rs | 5 ++- src/widget/id_container.rs | 9 ++-- src/widget/layer_container.rs | 5 +-- src/widget/list/list_column.rs | 3 +- src/widget/menu/flex.rs | 10 ++--- src/widget/menu/menu_bar.rs | 42 +++++++++---------- src/widget/menu/menu_inner.rs | 28 ++++++------- src/widget/mod.rs | 2 +- src/widget/nav_bar.rs | 7 ++-- src/widget/popover.rs | 8 +--- src/widget/progress_bar/circular.rs | 8 +--- src/widget/progress_bar/linear.rs | 8 +--- src/widget/radio.rs | 6 +-- src/widget/rectangle_tracker/mod.rs | 12 +++--- src/widget/rectangle_tracker/subscription.rs | 15 +++---- src/widget/responsive_container.rs | 9 ++-- src/widget/responsive_menu_bar.rs | 6 +-- src/widget/segmented_button/widget.rs | 8 ++-- src/widget/settings/item.rs | 9 ++-- src/widget/spin_button.rs | 9 ++-- src/widget/table/mod.rs | 9 ++-- src/widget/table/model/entity.rs | 6 +-- src/widget/table/model/mod.rs | 6 +-- src/widget/table/model/selection.rs | 6 +-- src/widget/table/widget/compact.rs | 14 +++---- src/widget/table/widget/standard.rs | 14 +++---- src/widget/text_input/editor.rs | 3 +- src/widget/text_input/input.rs | 10 ++--- src/widget/text_input/style.rs | 3 +- src/widget/toaster/mod.rs | 9 ++-- src/widget/toaster/widget.rs | 10 ++--- src/widget/toggler.rs | 11 +++-- src/widget/wayland/tooltip/mod.rs | 3 +- src/widget/wayland/tooltip/widget.rs | 8 +--- src/widget/wrapper.rs | 14 +++---- 98 files changed, 419 insertions(+), 540 deletions(-) create mode 100644 .zed/settings.json create mode 100644 rustfmt.toml diff --git a/.zed/settings.json b/.zed/settings.json new file mode 100644 index 00000000..2cc7b98d --- /dev/null +++ b/.zed/settings.json @@ -0,0 +1,15 @@ +{ + "format_on_save": "on", + "lsp": { + "rust-analyzer": { + "initialization_options": { + "check": { + "command": "clippy", + }, + "rustfmt": { + "extraArgs": ["+nightly"], + }, + }, + }, + }, +} diff --git a/cosmic-config/src/dbus.rs b/cosmic-config/src/dbus.rs index da7bcb68..9b6e8697 100644 --- a/cosmic-config/src/dbus.rs +++ b/cosmic-config/src/dbus.rs @@ -1,13 +1,12 @@ -use std::{any::TypeId, ops::Deref}; +use std::any::TypeId; +use std::ops::Deref; use crate::{CosmicConfigEntry, Update}; use cosmic_settings_daemon::{Changed, ConfigProxy, CosmicSettingsDaemonProxy}; use futures_util::SinkExt; -use iced_futures::{ - Subscription, - futures::{self, StreamExt, future::pending}, - stream, -}; +use iced_futures::futures::future::pending; +use iced_futures::futures::{self, StreamExt}; +use iced_futures::{Subscription, stream}; pub async fn settings_daemon_proxy() -> zbus::Result> { let conn = zbus::Connection::session().await?; diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index c8eda064..7ca9b907 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -1,16 +1,13 @@ //! Integrations for cosmic-config — the cosmic configuration system. -use notify::{ - RecommendedWatcher, Watcher, - event::{EventKind, ModifyKind, RenameMode}, -}; -use serde::{Serialize, de::DeserializeOwned}; -use std::{ - env, fmt, fs, - io::Write, - path::{Path, PathBuf}, - sync::Mutex, -}; +use notify::event::{EventKind, ModifyKind, RenameMode}; +use notify::{RecommendedWatcher, Watcher}; +use serde::Serialize; +use serde::de::DeserializeOwned; +use std::io::Write; +use std::path::{Path, PathBuf}; +use std::sync::Mutex; +use std::{env, fmt, fs}; /// Get the config directory, with Flatpak sandbox support. /// In Flatpak, HOST_XDG_CONFIG_HOME points to the real user config directory, diff --git a/cosmic-config/src/subscription.rs b/cosmic-config/src/subscription.rs index d16b9b65..f0387877 100644 --- a/cosmic-config/src/subscription.rs +++ b/cosmic-config/src/subscription.rs @@ -1,7 +1,9 @@ +use iced_futures::futures::channel::mpsc; use iced_futures::futures::{SinkExt, Stream}; -use iced_futures::{futures::channel::mpsc, stream}; +use iced_futures::stream; use notify::RecommendedWatcher; -use std::{borrow::Cow, hash::Hash}; +use std::borrow::Cow; +use std::hash::Hash; use crate::{Config, CosmicConfigEntry}; @@ -77,7 +79,8 @@ async fn start_listening, output: &mut mpsc::Sender>, ) -> ConfigState { - use iced_futures::futures::{StreamExt, future::pending}; + use iced_futures::futures::StreamExt; + use iced_futures::futures::future::pending; match state { ConfigState::Init(config_id, version, is_state) => { diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 1bc8050f..a403403c 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1,13 +1,13 @@ +use crate::composite::over; +use crate::steps::{color_index, get_small_widget_color, get_surface_color, get_text, steps}; 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}, }; use cosmic_config::{Config, CosmicConfigEntry}; -use palette::{ - IntoColor, Oklcha, Srgb, Srgba, WithAlpha, color_difference::Wcag21RelativeContrast, rgb::Rgb, -}; +use palette::color_difference::Wcag21RelativeContrast; +use palette::rgb::Rgb; +use palette::{IntoColor, Oklcha, Srgb, Srgba, WithAlpha}; 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 40eba5b4..16a3c36e 100644 --- a/cosmic-theme/src/output/gtk4_output.rs +++ b/cosmic-theme/src/output/gtk4_output.rs @@ -1,11 +1,12 @@ -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}, - num::NonZeroUsize, - path::Path, -}; +use crate::composite::over; +use crate::steps::steps; +use crate::{Component, Theme}; +use palette::rgb::Rgba; +use palette::{Darken, IntoColor, Lighten, Srgba, WithAlpha}; +use std::fs::{self, File}; +use std::io::{self, Write}; +use std::num::NonZeroUsize; +use std::path::Path; use super::{OutputError, to_rgba}; diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index 19f7bc5b..04dce39f 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -1,5 +1,6 @@ use configparser::ini::WriteOptions; -use palette::{Srgba, rgb::Rgba}; +use palette::Srgba; +use palette::rgb::Rgba; use thiserror::Error; use crate::Theme; diff --git a/cosmic-theme/src/output/qt56ct_output.rs b/cosmic-theme/src/output/qt56ct_output.rs index 43a45470..16df211f 100644 --- a/cosmic-theme/src/output/qt56ct_output.rs +++ b/cosmic-theme/src/output/qt56ct_output.rs @@ -1,12 +1,12 @@ 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 palette::blend::Compose; +use palette::rgb::Rgba; +use palette::{Mix, Srgba, WithAlpha}; +use std::fs::{self, File}; +use std::io::Write; +use std::path::PathBuf; +use std::vec; use super::{OutputError, qt_settings_ini_style}; diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index d42d553b..84614e7b 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -1,12 +1,11 @@ use crate::Theme; use configparser::ini::Ini; use cosmic_config::CosmicConfigEntry; -use palette::{Mix, Srgba, blend::Compose}; -use std::{ - fs::{self, File}, - io::{self, Write}, - path::{Path, PathBuf}, -}; +use palette::blend::Compose; +use palette::{Mix, Srgba}; +use std::fs::{self, File}; +use std::io::{self, Write}; +use std::path::{Path, PathBuf}; use super::{OutputError, qt_settings_ini_style}; diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs index 6ebf1015..d156722c 100644 --- a/cosmic-theme/src/steps.rs +++ b/cosmic-theme/src/steps.rs @@ -1,7 +1,8 @@ use std::num::NonZeroUsize; use almost::equal; -use palette::{ClampAssign, FromColor, Lch, Oklcha, Srgb, Srgba, convert::FromColorUnclamped}; +use palette::convert::FromColorUnclamped; +use palette::{ClampAssign, FromColor, Lch, Oklcha, Srgb, Srgba}; /// Get an array of 100 colors with a specific hue and chroma /// over the full range of lightness. diff --git a/examples/about/src/main.rs b/examples/about/src/main.rs index c25a9b9a..d1ea475e 100644 --- a/examples/about/src/main.rs +++ b/examples/about/src/main.rs @@ -8,7 +8,8 @@ use cosmic::app::{Core, Settings, Task}; use cosmic::executor; use cosmic::iced::{alignment, Length, Size}; use cosmic::prelude::*; -use cosmic::widget::{self, about::About, nav_bar}; +use cosmic::widget::about::About; +use cosmic::widget::{self, nav_bar}; /// Runs application with these settings #[rustfmt::skip] diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 22903eac..57546ab1 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -4,7 +4,8 @@ use cosmic::iced::core::window; use cosmic::iced::window::Id; use cosmic::iced::{Length, Rectangle}; use cosmic::surface::action::{app_popup, destroy_popup}; -use cosmic::widget::{dropdown::popup_dropdown, list_column, settings, toggler}; +use cosmic::widget::dropdown::popup_dropdown; +use cosmic::widget::{list_column, settings, toggler}; use cosmic::Element; const ID: &str = "com.system76.CosmicAppletExample"; diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index f6e571e0..9f89061b 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -5,9 +5,10 @@ use cosmic::app::Settings; use cosmic::iced::{Alignment, Length, Size}; +use cosmic::prelude::*; use cosmic::widget::menu::{self, KeyBind}; use cosmic::widget::nav_bar; -use cosmic::{executor, iced, prelude::*, widget, Core}; +use cosmic::{executor, iced, widget, Core}; use std::collections::HashMap; use std::sync::LazyLock; diff --git a/examples/cosmic/src/window.rs b/examples/cosmic/src/window.rs index 9fce8767..4168718d 100644 --- a/examples/cosmic/src/window.rs +++ b/examples/cosmic/src/window.rs @@ -23,15 +23,11 @@ use cosmic::{ Element, }; use cosmic_time::{Instant, Timeline}; -use std::{ - cell::RefCell, - rc::Rc, - sync::{ - atomic::{AtomicU32, Ordering}, - Arc, - }, - vec, -}; +use std::cell::RefCell; +use std::rc::Rc; +use std::sync::atomic::{AtomicU32, Ordering}; +use std::sync::Arc; +use std::vec; // XXX The use of button is removed because it assigns the same ID to multiple buttons, causing a crash when a11y is enabled... // static BTN: Lazy = Lazy::new(|| id::Id::new("BTN")); diff --git a/examples/cosmic/src/window/bluetooth.rs b/examples/cosmic/src/window/bluetooth.rs index 1b5892f6..2878b46a 100644 --- a/examples/cosmic/src/window/bluetooth.rs +++ b/examples/cosmic/src/window/bluetooth.rs @@ -1,9 +1,7 @@ use super::{Page, Window}; -use cosmic::{ - iced::widget::{column, text}, - widget::{list_column, settings, toggler}, - Element, -}; +use cosmic::iced::widget::{column, text}; +use cosmic::widget::{list_column, settings, toggler}; +use cosmic::Element; #[derive(Clone, Copy, Debug)] pub enum Message { diff --git a/examples/cosmic/src/window/demo.rs b/examples/cosmic/src/window/demo.rs index 0d31fa93..20a730b5 100644 --- a/examples/cosmic/src/window/demo.rs +++ b/examples/cosmic/src/window/demo.rs @@ -1,19 +1,17 @@ -use std::{cell::RefCell, rc::Rc}; +use std::cell::RefCell; +use std::rc::Rc; use apply::Apply; -use cosmic::{ - cosmic_theme, - iced::widget::{checkbox, column, progress_bar, radio, slider, text}, - iced::{Alignment, Length}, - iced_core::id, - theme::ThemeType, - widget::{ - button, color_picker::ColorPickerUpdate, dropdown, icon, layer_container as container, - segmented_button, segmented_control, settings, spin_button, tab_bar, toggler, - ColorPickerModel, - }, - Element, +use cosmic::iced::widget::{checkbox, column, progress_bar, radio, slider, text}; +use cosmic::iced::{Alignment, Length}; +use cosmic::iced_core::id; +use cosmic::theme::ThemeType; +use cosmic::widget::color_picker::ColorPickerUpdate; +use cosmic::widget::{ + button, dropdown, icon, layer_container as container, segmented_button, segmented_control, + settings, spin_button, tab_bar, toggler, ColorPickerModel, }; +use cosmic::{cosmic_theme, Element}; use cosmic_time::{anim, chain, Timeline}; use fraction::{Decimal, ToPrimitive}; use once_cell::sync::Lazy; diff --git a/examples/cosmic/src/window/desktop.rs b/examples/cosmic/src/window/desktop.rs index 46a4e5b8..a087de9e 100644 --- a/examples/cosmic/src/window/desktop.rs +++ b/examples/cosmic/src/window/desktop.rs @@ -1,10 +1,7 @@ -use cosmic::{ - iced::widget::{column, container, horizontal_space, image, row, svg, text}, - iced::Length, - theme, - widget::{list_column, settings, toggler}, - Element, -}; +use cosmic::iced::widget::{column, container, horizontal_space, image, row, svg, text}; +use cosmic::iced::Length; +use cosmic::widget::{list_column, settings, toggler}; +use cosmic::{theme, Element}; use super::{Page, SubPage, Window}; diff --git a/examples/cosmic/src/window/system_and_accounts.rs b/examples/cosmic/src/window/system_and_accounts.rs index ed1bd004..5f498857 100644 --- a/examples/cosmic/src/window/system_and_accounts.rs +++ b/examples/cosmic/src/window/system_and_accounts.rs @@ -1,9 +1,7 @@ -use cosmic::{ - iced::widget::{horizontal_space, row, text}, - iced::Length, - widget::{icon, list_column, settings}, - Element, -}; +use cosmic::iced::widget::{horizontal_space, row, text}; +use cosmic::iced::Length; +use cosmic::widget::{icon, list_column, settings}; +use cosmic::Element; use super::{Message, Page, SubPage, Window}; diff --git a/examples/menu/src/main.rs b/examples/menu/src/main.rs index da0c3231..900d0322 100644 --- a/examples/menu/src/main.rs +++ b/examples/menu/src/main.rs @@ -9,11 +9,9 @@ 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::{Length, Size}; +use cosmic::iced::{window, Length, Size}; use cosmic::widget::menu::action::MenuAction; -use cosmic::widget::menu::key_bind::KeyBind; -use cosmic::widget::menu::key_bind::Modifier; +use cosmic::widget::menu::key_bind::{KeyBind, Modifier}; use cosmic::widget::menu::{self, ItemHeight, ItemWidth}; use cosmic::widget::RcElementWrapper; use cosmic::{executor, Element}; diff --git a/examples/multi-window/src/window.rs b/examples/multi-window/src/window.rs index 754a0d86..a6d40d36 100644 --- a/examples/multi-window/src/window.rs +++ b/examples/multi-window/src/window.rs @@ -1,13 +1,11 @@ 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}, - prelude::*, - widget::{button, header_bar}, -}; +use cosmic::app::Core; +use cosmic::iced::core::{id, Alignment, Length, Point}; +use cosmic::iced::widget::{column, container, scrollable, text}; +use cosmic::iced::{self, event, window, Subscription}; +use cosmic::prelude::*; +use cosmic::widget::{button, header_bar}; #[derive(Debug, Clone, PartialEq)] pub enum Message { diff --git a/examples/spin-button/src/main.rs b/examples/spin-button/src/main.rs index 47db4dce..0228291c 100644 --- a/examples/spin-button/src/main.rs +++ b/examples/spin-button/src/main.rs @@ -1,15 +1,8 @@ -use cosmic::iced::Length; +use cosmic::app::{Core, Task}; +use cosmic::iced::alignment::{Horizontal, Vertical}; +use cosmic::iced::{self, Alignment, Length, Size}; use cosmic::widget::{column, container, spin_button}; -use cosmic::Apply; -use cosmic::{ - app::{Core, Task}, - iced::{ - self, - alignment::{Horizontal, Vertical}, - Alignment, Size, - }, - Application, Element, -}; +use cosmic::{Application, Apply, Element}; use fraction::Decimal; pub struct SpinButtonExamplApp { diff --git a/examples/subscriptions/src/main.rs b/examples/subscriptions/src/main.rs index 17e630aa..325af162 100644 --- a/examples/subscriptions/src/main.rs +++ b/examples/subscriptions/src/main.rs @@ -5,7 +5,8 @@ use cosmic::app::{Core, Settings, Task}; use cosmic::iced::Subscription; -use cosmic::{executor, prelude::*, widget}; +use cosmic::prelude::*; +use cosmic::{executor, widget}; /// Runs application with these settings fn main() -> Result<(), Box> { diff --git a/examples/table-view/src/main.rs b/examples/table-view/src/main.rs index d2478429..e518b3cc 100644 --- a/examples/table-view/src/main.rs +++ b/examples/table-view/src/main.rs @@ -9,8 +9,7 @@ use chrono::Datelike; use cosmic::app::{Core, Settings, Task}; use cosmic::iced::Size; use cosmic::prelude::*; -use cosmic::widget::table; -use cosmic::widget::{self, nav_bar}; +use cosmic::widget::{self, nav_bar, table}; use cosmic::{executor, iced}; #[derive(Debug, Default, PartialEq, Eq, Clone, Copy, Hash)] diff --git a/rustfmt.toml b/rustfmt.toml new file mode 100644 index 00000000..c1578aaf --- /dev/null +++ b/rustfmt.toml @@ -0,0 +1 @@ +imports_granularity = "Module" diff --git a/src/app/action.rs b/src/app/action.rs index fb982acb..166df16b 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -1,10 +1,10 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -use crate::surface; +use crate::config::CosmicTk; use crate::theme::Theme; use crate::widget::nav_bar; -use crate::{config::CosmicTk, keyboard_nav}; +use crate::{keyboard_nav, surface}; #[cfg(all(feature = "wayland", target_os = "linux"))] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; diff --git a/src/app/mod.rs b/src/app/mod.rs index ad1ceefe..264f1d16 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -22,11 +22,11 @@ use crate::prelude::*; use crate::theme::THEME; use crate::widget::{container, id_container, menu, nav_bar, popover, space}; use apply::Apply; -use iced::{Length, Subscription}; -use iced::{theme, window}; +use iced::{Length, Subscription, theme, window}; pub use settings::Settings; use std::borrow::Cow; -use std::{cell::RefCell, rc::Rc}; +use std::cell::RefCell; +use std::rc::Rc; #[cold] pub(crate) fn iced_settings( diff --git a/src/applet/column.rs b/src/applet/column.rs index 9657b566..1ec9c731 100644 --- a/src/applet/column.rs +++ b/src/applet/column.rs @@ -2,14 +2,10 @@ 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, + layout, mouse, overlay, renderer, widget, }; /// A container that distributes its contents vertically. diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 48721e1c..e16b0303 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -1,37 +1,31 @@ #[cfg(feature = "applet-token")] pub mod token; -use crate::app::{BootData, BootDataInner, cosmic}; -use crate::{ - Application, Element, Renderer, - app::iced_settings, - cctk::sctk, - theme::{self, Button, THEME, system_dark, system_light}, - widget::{ - self, - autosize::{self, Autosize, autosize}, - column::Column, - layer_container, - row::Row, - space::horizontal, - space::vertical, - }, -}; +use crate::app::{BootData, BootDataInner, cosmic, iced_settings}; +use crate::cctk::sctk; +use crate::theme::{self, Button, THEME, system_dark, system_light}; +use crate::widget::autosize::{self, Autosize, autosize}; +use crate::widget::column::Column; +use crate::widget::row::Row; +use crate::widget::space::{horizontal, vertical}; +use crate::widget::{self, layer_container}; +use crate::{Application, Element, Renderer}; 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::alignment::{Alignment, Horizontal, Vertical}; +use iced::widget::Container; +use iced::{self, Color, Length, Limits, Rectangle, window}; use iced_core::{Padding, Shadow}; use iced_runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; use iced_widget::Text; use sctk::reexports::protocols::xdg::shell::client::xdg_positioner::{Anchor, Gravity}; +use std::borrow::Cow; use std::cell::RefCell; -use std::{borrow::Cow, num::NonZeroU32, rc::Rc, sync::LazyLock, time::Duration}; +use std::num::NonZeroU32; +use std::rc::Rc; +use std::sync::LazyLock; +use std::time::Duration; use tracing::info; pub mod column; diff --git a/src/applet/row.rs b/src/applet/row.rs index a6745d1c..718f366c 100644 --- a/src/applet/row.rs +++ b/src/applet/row.rs @@ -3,12 +3,10 @@ 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, + Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, mouse, + overlay, renderer, widget, }; use iced::touch; diff --git a/src/applet/token/subscription.rs b/src/applet/token/subscription.rs index 07c528ea..64be0ce2 100644 --- a/src/applet/token/subscription.rs +++ b/src/applet/token/subscription.rs @@ -1,13 +1,12 @@ use crate::iced; use cctk::sctk::reexports::calloop; -use futures::{ - SinkExt, StreamExt, - channel::mpsc::{UnboundedReceiver, unbounded}, -}; +use futures::channel::mpsc::{UnboundedReceiver, unbounded}; +use futures::{SinkExt, StreamExt}; use iced::Subscription; -use iced_futures::futures; -use iced_futures::stream; -use std::{fmt::Debug, hash::Hash, thread::JoinHandle}; +use iced_futures::{futures, stream}; +use std::fmt::Debug; +use std::hash::Hash; +use std::thread::JoinHandle; use super::wayland_handler::wayland_handler; diff --git a/src/applet/token/wayland_handler.rs b/src/applet/token/wayland_handler.rs index 3db84fc4..4ab73235 100644 --- a/src/applet/token/wayland_handler.rs +++ b/src/applet/token/wayland_handler.rs @@ -1,27 +1,20 @@ -use std::os::{ - fd::{FromRawFd, RawFd}, - unix::net::UnixStream, -}; +use std::os::fd::{FromRawFd, RawFd}; +use std::os::unix::net::UnixStream; use super::subscription::{TokenRequest, TokenUpdate}; -use cctk::{ - sctk::{ - self, - activation::{RequestData, RequestDataExt}, - reexports::{calloop, calloop_wayland_source::WaylandSource}, - seat::{SeatHandler, SeatState}, - }, - wayland_client::{ - self, - protocol::{wl_seat::WlSeat, wl_surface::WlSurface}, - }, -}; +use cctk::sctk::activation::{RequestData, RequestDataExt}; +use cctk::sctk::reexports::calloop; +use cctk::sctk::reexports::calloop_wayland_source::WaylandSource; +use cctk::sctk::seat::{SeatHandler, SeatState}; +use cctk::sctk::{self}; +use cctk::wayland_client::protocol::wl_seat::WlSeat; +use cctk::wayland_client::protocol::wl_surface::WlSurface; +use cctk::wayland_client::{self}; use iced_futures::futures::channel::mpsc::UnboundedSender; -use sctk::{ - activation::{ActivationHandler, ActivationState}, - registry::{ProvidesRegistryState, RegistryState}, -}; -use wayland_client::{Connection, QueueHandle, globals::registry_queue_init}; +use sctk::activation::{ActivationHandler, ActivationState}; +use sctk::registry::{ProvidesRegistryState, RegistryState}; +use wayland_client::globals::registry_queue_init; +use wayland_client::{Connection, QueueHandle}; struct AppData { exit: bool, diff --git a/src/dbus_activation.rs b/src/dbus_activation.rs index 99e2f9f0..e7bc6900 100644 --- a/src/dbus_activation.rs +++ b/src/dbus_activation.rs @@ -1,17 +1,15 @@ // Copyright 2024 System76 // SPDX-License-Identifier: MPL-2.0 -use { - crate::ApplicationExt, - iced::Subscription, - iced_futures::futures::{ - SinkExt, - channel::mpsc::{Receiver, Sender}, - }, - std::{any::TypeId, collections::HashMap}, - url::Url, - zbus::{interface, proxy, zvariant::Value}, -}; +use crate::ApplicationExt; +use iced::Subscription; +use iced_futures::futures::SinkExt; +use iced_futures::futures::channel::mpsc::{Receiver, Sender}; +use std::any::TypeId; +use std::collections::HashMap; +use url::Url; +use zbus::zvariant::Value; +use zbus::{interface, proxy}; #[cold] pub fn subscription() -> Subscription> { diff --git a/src/desktop.rs b/src/desktop.rs index 98ce7d4b..cfa7a668 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -872,7 +872,8 @@ trait SystemdManger { #[cfg(all(test, not(windows)))] mod tests { use super::*; - use std::{env, fs, path::Path, path::PathBuf}; + use std::path::{Path, PathBuf}; + use std::{env, fs}; use tempfile::tempdir; struct EnvVarGuard { diff --git a/src/localize.rs b/src/localize.rs index 95a31655..d63d9706 100644 --- a/src/localize.rs +++ b/src/localize.rs @@ -1,9 +1,7 @@ // SPDX-License-Identifier: GPL-3.0-only -use i18n_embed::{ - DefaultLocalizer, LanguageLoader, Localizer, - fluent::{FluentLanguageLoader, fluent_language_loader}, -}; +use i18n_embed::fluent::{FluentLanguageLoader, fluent_language_loader}; +use i18n_embed::{DefaultLocalizer, LanguageLoader, Localizer}; use rust_embed::RustEmbed; use std::sync::{LazyLock, OnceLock}; diff --git a/src/surface/action.rs b/src/surface/action.rs index 50e2b4a9..5bd5a294 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -6,7 +6,8 @@ use super::Action; use crate::Application; use iced::window; -use std::{any::Any, sync::Arc}; +use std::any::Any; +use std::sync::Arc; /// Used to produce a destroy popup message from within a widget. #[cfg(all(feature = "wayland", target_os = "linux"))] diff --git a/src/surface/mod.rs b/src/surface/mod.rs index 0dad6459..307b5275 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -3,9 +3,7 @@ pub mod action; -use iced::Limits; -use iced::Size; -use iced::Task; +use iced::{Limits, Size, Task}; use std::future::Future; use std::sync::Arc; diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 093bac05..a5ae5d4a 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -7,12 +7,8 @@ pub mod portal; pub mod style; -use cosmic_config::CosmicConfigEntry; -use cosmic_config::config_subscription; -use cosmic_theme::Component; -use cosmic_theme::LayeredTheme; -use cosmic_theme::Spacing; -use cosmic_theme::ThemeMode; +use cosmic_config::{CosmicConfigEntry, config_subscription}; +use cosmic_theme::{Component, LayeredTheme, Spacing, ThemeMode}; use iced_futures::Subscription; use iced_runtime::{Appearance, DefaultStyle}; use std::sync::{Arc, LazyLock, Mutex}; diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index e83e48e8..f84c9e97 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -6,10 +6,8 @@ use cosmic_theme::Component; use iced_core::{Background, Color}; -use crate::{ - theme::TRANSPARENT_COMPONENT, - widget::button::{Catalog, Style}, -}; +use crate::theme::TRANSPARENT_COMPONENT; +use crate::widget::button::{Catalog, Style}; #[derive(Default)] pub enum Button { diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index aa6f4b33..1761f6a5 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -5,18 +5,17 @@ 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, - slider::{self, Rail}, - svg, toggler, - }, +use iced::overlay::menu; +use iced::theme::Base; +use iced::widget::slider::{self, Rail}; +use iced::widget::{ + button as iced_button, checkbox as iced_checkbox, combo_box, container as iced_container, + pane_grid, pick_list, progress_bar, radio, rule, scrollable, svg, toggler, }; use iced_core::{Background, Border, Color, Shadow, Vector}; -use iced_widget::{pane_grid::Highlight, scrollable::AutoScroll, text_editor, text_input}; +use iced_widget::pane_grid::Highlight; +use iced_widget::scrollable::AutoScroll; +use iced_widget::{text_editor, text_input}; use palette::WithAlpha; use std::rc::Rc; diff --git a/src/theme/style/segmented_button.rs b/src/theme/style/segmented_button.rs index b9863c88..381c4a05 100644 --- a/src/theme/style/segmented_button.rs +++ b/src/theme/style/segmented_button.rs @@ -3,10 +3,13 @@ //! Contains stylesheet implementation for [`crate::widget::segmented_button`]. -use crate::widget::segmented_button::{Appearance, ItemAppearance, StyleSheet}; -use crate::{theme::Theme, widget::segmented_button::ItemStatusAppearance}; +use crate::theme::Theme; +use crate::widget::segmented_button::{ + Appearance, ItemAppearance, ItemStatusAppearance, StyleSheet, +}; use iced::Border; -use iced_core::{Background, border::Radius}; +use iced_core::Background; +use iced_core::border::Radius; use palette::WithAlpha; #[derive(Default)] @@ -143,7 +146,8 @@ mod horizontal { use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; use cosmic_theme::{Component, Container}; use iced::Border; - use iced_core::{Background, border::Radius}; + use iced_core::Background; + use iced_core::border::Radius; use palette::WithAlpha; pub fn tab_bar(cosmic: &cosmic_theme::Theme, container: &Container) -> Appearance { @@ -250,7 +254,8 @@ mod vertical { use crate::widget::segmented_button::{ItemAppearance, ItemStatusAppearance}; use cosmic_theme::{Component, Container}; use iced::Border; - use iced_core::{Background, border::Radius}; + use iced_core::Background; + use iced_core::border::Radius; use palette::WithAlpha; pub fn tab_bar(cosmic: &cosmic_theme::Theme, container: &Container) -> Appearance { diff --git a/src/widget/about.rs b/src/widget/about.rs index 9b21e93a..490b0378 100644 --- a/src/widget/about.rs +++ b/src/widget/about.rs @@ -1,8 +1,6 @@ -use crate::{ - Apply, Element, fl, - iced::{Alignment, Length}, - widget::{self, list}, -}; +use crate::iced::{Alignment, Length}; +use crate::widget::{self, list}; +use crate::{Apply, Element, fl}; use std::rc::Rc; #[derive(Debug, Default, Clone, derive_setters::Setters)] diff --git a/src/widget/aspect_ratio.rs b/src/widget/aspect_ratio.rs index 577bea95..5cd8b9c0 100644 --- a/src/widget/aspect_ratio.rs +++ b/src/widget/aspect_ratio.rs @@ -3,13 +3,10 @@ use iced::Size; use iced::widget::Container; use iced_core::event::Event; -use iced_core::layout; -use iced_core::mouse; -use iced_core::overlay; -use iced_core::renderer; use iced_core::widget::Tree; use iced_core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Vector, Widget, + layout, mouse, overlay, renderer, }; pub use iced_widget::container::{Catalog, Style}; diff --git a/src/widget/autosize.rs b/src/widget/autosize.rs index 69fd9c83..c52367db 100644 --- a/src/widget/autosize.rs +++ b/src/widget/autosize.rs @@ -1,12 +1,11 @@ //! Autosize Container, which will resize the window to its contents. use iced_core::event::{self, Event}; -use iced_core::layout; -use iced_core::mouse; -use iced_core::overlay; -use iced_core::renderer; use iced_core::widget::{Id, Operation, Tree}; -use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; +use iced_core::{ + Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay, + renderer, +}; pub use iced_widget::container::{Catalog, Style}; pub fn autosize<'a, Message: 'static, Theme, E>( diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs index 04d2bdd5..6be13008 100644 --- a/src/widget/button/icon.rs +++ b/src/widget/button/icon.rs @@ -3,9 +3,13 @@ use super::{Builder, ButtonClass}; use crate::Element; -use crate::widget::{icon::Handle, tooltip}; +use crate::widget::icon::Handle; +use crate::widget::tooltip; use apply::Apply; -use iced_core::{Alignment, Length, Padding, font::Weight, text::LineHeight, widget::Id}; +use iced_core::font::Weight; +use iced_core::text::LineHeight; +use iced_core::widget::Id; +use iced_core::{Alignment, Length, Padding}; use std::borrow::Cow; pub type Button<'a, Message> = Builder<'a, Message, Icon>; diff --git a/src/widget/button/image.rs b/src/widget/button/image.rs index ab51e667..3b382035 100644 --- a/src/widget/button/image.rs +++ b/src/widget/button/image.rs @@ -2,11 +2,12 @@ // SPDX-License-Identifier: MPL-2.0 use super::Builder; -use crate::{ - Element, - widget::{self, image::Handle}, -}; -use iced_core::{Length, Padding, font::Weight, widget::Id}; +use crate::Element; +use crate::widget::image::Handle; +use crate::widget::{self}; +use iced_core::font::Weight; +use iced_core::widget::Id; +use iced_core::{Length, Padding}; use std::borrow::Cow; pub type Button<'a, Message> = Builder<'a, Message, Image<'a, Handle, Message>>; diff --git a/src/widget/button/link.rs b/src/widget/button/link.rs index 9ce81268..c0afd327 100644 --- a/src/widget/button/link.rs +++ b/src/widget/button/link.rs @@ -3,14 +3,15 @@ //! Hyperlink button widget -use super::Builder; -use super::ButtonClass; +use super::{Builder, ButtonClass}; use crate::Element; use crate::prelude::*; use crate::widget::icon::{self, Handle}; use crate::widget::{button, row, tooltip}; +use iced_core::font::Weight; use iced_core::text::LineHeight; -use iced_core::{Alignment, Length, Padding, font::Weight, widget::Id}; +use iced_core::widget::Id; +use iced_core::{Alignment, Length, Padding}; use std::borrow::Cow; pub type Button<'a, Message> = Builder<'a, Message, Hyperlink>; diff --git a/src/widget/button/style.rs b/src/widget/button/style.rs index 21afa08b..f8df8c62 100644 --- a/src/widget/button/style.rs +++ b/src/widget/button/style.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: MPL-2.0 //! Change the apperance of a button. -use iced_core::{Background, Color, Vector, border::Radius}; +use iced_core::border::Radius; +use iced_core::{Background, Color, Vector}; use crate::theme::THEME; diff --git a/src/widget/button/text.rs b/src/widget/button/text.rs index bcdd02ba..52ab6145 100644 --- a/src/widget/button/text.rs +++ b/src/widget/button/text.rs @@ -4,7 +4,10 @@ use super::{Builder, ButtonClass}; use crate::widget::{icon, row, tooltip}; use crate::{Apply, Element}; -use iced_core::{Alignment, Length, Padding, font::Weight, text::LineHeight, widget::Id}; +use iced_core::font::Weight; +use iced_core::text::LineHeight; +use iced_core::widget::Id; +use iced_core::{Alignment, Length, Padding}; use std::borrow::Cow; pub type Button<'a, Message> = Builder<'a, Message, Text>; diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 4acf3f2d..cca54fd2 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -11,15 +11,12 @@ use iced_runtime::{Action, Task, keyboard, task}; use iced_core::event::{self, Event}; use iced_core::renderer::{self, Quad, Renderer}; -use iced_core::touch; use iced_core::widget::Operation; use iced_core::widget::tree::{self, Tree}; use iced_core::{ - Background, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget, + Background, Border, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shadow, Shell, + Vector, Widget, layout, mouse, overlay, svg, touch, }; -use iced_core::{Border, mouse}; -use iced_core::{Shadow, overlay}; -use iced_core::{layout, svg}; use iced_renderer::core::widget::operation; use crate::theme::THEME; @@ -647,10 +644,8 @@ impl<'a, Message: 'a + Clone> Widget state: &Tree, p: mouse::Cursor, ) -> iced_accessibility::A11yTree { - use iced_accessibility::{ - A11yNode, A11yTree, - accesskit::{Action, Node, NodeId, Rect, Role}, - }; + use iced_accessibility::accesskit::{Action, Node, NodeId, Rect, Role}; + use iced_accessibility::{A11yNode, A11yTree}; // TODO why is state None sometimes? if matches!(state.state, iced_core::widget::tree::State::None) { tracing::info!("Button state is missing."); diff --git a/src/widget/calendar.rs b/src/widget/calendar.rs index 91c601d3..35632c3a 100644 --- a/src/widget/calendar.rs +++ b/src/widget/calendar.rs @@ -8,10 +8,8 @@ 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}, -}; +use jiff::ToSpan; +use jiff::civil::{Date, Weekday}; /// A widget that displays an interactive calendar. pub fn calendar( diff --git a/src/widget/cards.rs b/src/widget/cards.rs index 66267a73..00014740 100644 --- a/src/widget/cards.rs +++ b/src/widget/cards.rs @@ -1,23 +1,18 @@ //! An expandable stack of cards use std::time::Duration; -use crate::{ - anim, - widget::{ - button, - card::style::Style, - column, - icon::{self, Handle}, - row, text, - }, -}; +use crate::anim; +use crate::widget::card::style::Style; +use crate::widget::icon::{self, Handle}; +use crate::widget::{button, column, row, text}; 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}; +use iced_core::border::Radius; +use iced_core::id::Id; +use iced_core::layout::Node; +use iced_core::renderer::Quad; +use iced_core::widget::{Tree, tree}; +use iced_core::{Border, Element, Event, Length, Shadow, Size, Vector, Widget, window}; const ICON_SIZE: u16 = 16; const TOP_SPACING: u16 = 4; diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 318e943b..66ffd847 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -12,7 +12,9 @@ use std::time::{Duration, Instant}; use crate::Element; use crate::theme::iced::Slider; use crate::theme::{Button, THEME}; -use crate::widget::{button::Catalog, container, segmented_button::Entity, slider}; +use crate::widget::button::Catalog; +use crate::widget::segmented_button::Entity; +use crate::widget::{container, slider}; use derive_setters::Setters; use iced::Task; use iced_core::event::{self, Event}; @@ -25,10 +27,8 @@ use iced_core::{ }; use iced_widget::slider::HandleShape; -use iced_widget::{ - Row, canvas, column, row, scrollable, - space::{horizontal, vertical}, -}; +use iced_widget::space::{horizontal, vertical}; +use iced_widget::{Row, canvas, column, row, scrollable}; use palette::{FromColor, RgbHue}; use super::divider::horizontal; diff --git a/src/widget/context_drawer/overlay.rs b/src/widget/context_drawer/overlay.rs index 39b34217..01f5cfcf 100644 --- a/src/widget/context_drawer/overlay.rs +++ b/src/widget/context_drawer/overlay.rs @@ -5,8 +5,7 @@ use crate::Element; use iced::advanced::layout::{self, Layout}; use iced::advanced::widget::{self, Operation}; -use iced::advanced::{Clipboard, Shell}; -use iced::advanced::{overlay, renderer}; +use iced::advanced::{Clipboard, Shell, overlay, renderer}; use iced::{Event, Point, Size, mouse}; use iced_core::{Renderer, touch}; diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index 7420738c..9a7448eb 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -6,11 +6,10 @@ use crate::widget::{self, LayerContainer, button, column, container, icon, row, use crate::{Apply, Element, Renderer, Theme, fl}; use std::borrow::Cow; -use iced_core::Alignment; use iced_core::event::Event; use iced_core::widget::{Operation, Tree}; use iced_core::{ - Clipboard, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, + Alignment, Clipboard, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay as iced_overlay, renderer, }; diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 3f35f04a..bd950bff 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -81,7 +81,8 @@ impl ContextMenu<'_, Message> { 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 crate::surface::action::destroy_popup; + use crate::widget::menu::Menu; use iced_runtime::platform_specific::wayland::popup::{ SctkPopupSettings, SctkPositioner, }; diff --git a/src/widget/dialog.rs b/src/widget/dialog.rs index 7d084626..72ab11f4 100644 --- a/src/widget/dialog.rs +++ b/src/widget/dialog.rs @@ -1,8 +1,5 @@ -use crate::{ - Element, - iced::{Length, Pixels}, - style, theme, widget, -}; +use crate::iced::{Length, Pixels}; +use crate::{Element, style, theme, widget}; use std::borrow::Cow; pub fn dialog<'a, Message>() -> Dialog<'a, Message> { diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 10bf7a8b..0ebff3dc 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -1,29 +1,17 @@ -use std::{ - borrow::Cow, - sync::atomic::{AtomicU64, Ordering}, -}; +use std::borrow::Cow; +use std::sync::atomic::{AtomicU64, Ordering}; use iced::Vector; -use crate::{ - Element, - widget::{Id, Widget}, -}; +use crate::Element; +use crate::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}, -}; +use iced::clipboard::dnd::{self, DndAction, DndDestinationRectangle, DndEvent, OfferEvent}; +use iced::clipboard::mime::AllowedMimeTypes; +use iced::id::Internal; +use iced::{Event, Length, Rectangle, event, mouse, overlay}; +use iced_core::widget::{Tree, tree}; +use iced_core::{self, Clipboard, Shell, layout}; pub fn dnd_destination<'a, Message: 'static>( child: impl Into>, diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index 980723e3..3cc9fa71 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -1,20 +1,14 @@ use std::any::Any; -use iced_core::{widget::Operation, window}; +use iced_core::widget::Operation; +use iced_core::window; -use crate::{ - Element, - 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}, -}; +use crate::Element; +use crate::widget::{Id, Widget, container}; +use iced::clipboard::dnd::{DndAction, DndEvent, SourceEvent}; +use iced::{Event, Length, Point, Rectangle, Vector, event, mouse, overlay}; +use iced_core::widget::{Tree, tree}; +use iced_core::{self, Clipboard, Shell, layout, renderer}; pub fn dnd_source< 'a, diff --git a/src/widget/dropdown/menu/appearance.rs b/src/widget/dropdown/menu/appearance.rs index d1bed21c..2c32c011 100644 --- a/src/widget/dropdown/menu/appearance.rs +++ b/src/widget/dropdown/menu/appearance.rs @@ -3,7 +3,8 @@ // SPDX-License-Identifier: MPL-2.0 AND MIT //! Change the appearance of menus. -use iced_core::{Background, Color, border::Radius}; +use iced_core::border::Radius; +use iced_core::{Background, Color}; /// The appearance of a menu. #[derive(Debug, Clone, Copy)] diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index 779c6d00..3c01f5f8 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -9,9 +9,9 @@ use iced_core::event::{self, Event}; use iced_core::text::{self, Paragraph, Text}; use iced_core::widget::tree::{self, Tree}; use iced_core::{ - Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, + Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shadow, Shell, Size, Vector, Widget, + alignment, keyboard, layout, mouse, overlay, renderer, svg, touch, }; -use iced_core::{Shadow, alignment, keyboard, layout, mouse, overlay, renderer, svg, touch}; use iced_widget::pick_list; use std::ffi::OsStr; diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 2ff9c92f..046cf84d 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -12,9 +12,9 @@ use iced_core::event::{self, Event}; use iced_core::text::{self, Paragraph, Text}; use iced_core::widget::tree::{self, Tree}; use iced_core::{ - Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, + Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shadow, Shell, Size, Vector, Widget, + alignment, keyboard, layout, mouse, overlay, renderer, svg, touch, }; -use iced_core::{Shadow, alignment, keyboard, layout, mouse, overlay, renderer, svg, touch}; use iced_widget::pick_list::{self, Catalog}; use std::borrow::Cow; use std::ffi::OsStr; diff --git a/src/widget/frames.rs b/src/widget/frames.rs index a542cec6..bf985ce2 100644 --- a/src/widget/frames.rs +++ b/src/widget/frames.rs @@ -2,14 +2,12 @@ //! Based on use std::ffi::OsStr; -use std::fmt; -use std::io; use std::path::Path; use std::time::{Duration, Instant}; +use std::{fmt, io}; use ::image as image_rs; -use iced::Task; -use iced::mouse; +use iced::{Task, mouse}; use iced_core::image::Renderer as ImageRenderer; use iced_core::mouse::Cursor; use iced_core::widget::{Tree, tree}; diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 0a10b0aa..556466fd 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -5,7 +5,8 @@ use crate::cosmic_theme::{Density, Spacing}; use crate::{Element, theme, widget}; use apply::Apply; use derive_setters::Setters; -use iced_core::{Length, Size, Vector, Widget, layout, text, widget::tree}; +use iced_core::widget::tree; +use iced_core::{Length, Size, Vector, Widget, layout, text}; use std::borrow::Cow; #[must_use] diff --git a/src/widget/icon/named.rs b/src/widget/icon/named.rs index dfd66cf5..c6bec753 100644 --- a/src/widget/icon/named.rs +++ b/src/widget/icon/named.rs @@ -2,7 +2,10 @@ // SPDX-License-Identifier: MPL-2.0 use super::{Handle, Icon}; -use std::{borrow::Cow, ffi::OsStr, path::PathBuf, sync::Arc}; +use std::borrow::Cow; +use std::ffi::OsStr; +use std::path::PathBuf; +use std::sync::Arc; #[derive(Debug, Clone, Default, Hash)] /// Fallback icon to use if the icon was not found. diff --git a/src/widget/id_container.rs b/src/widget/id_container.rs index 716ee138..6b0d13ce 100644 --- a/src/widget/id_container.rs +++ b/src/widget/id_container.rs @@ -1,10 +1,9 @@ use iced_core::event::{self, Event}; -use iced_core::layout; -use iced_core::mouse; -use iced_core::overlay; -use iced_core::renderer; use iced_core::widget::{Id, Operation, Tree}; -use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; +use iced_core::{ + Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay, + renderer, +}; pub use iced_widget::container::{Catalog, Style}; pub fn id_container<'a, Message: 'static, Theme, E>( diff --git a/src/widget/layer_container.rs b/src/widget/layer_container.rs index 110af518..3958b03c 100644 --- a/src/widget/layer_container.rs +++ b/src/widget/layer_container.rs @@ -2,13 +2,10 @@ use crate::Theme; use cosmic_theme::LayeredTheme; use iced::widget::Container; 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::Tree; use iced_core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Vector, Widget, + layout, mouse, overlay, renderer, }; pub use iced_widget::container::{Catalog, Style}; diff --git a/src/widget/list/list_column.rs b/src/widget/list/list_column.rs index 4ef3fc01..9e4204cc 100644 --- a/src/widget/list/list_column.rs +++ b/src/widget/list/list_column.rs @@ -2,7 +2,8 @@ // SPDX-License-Identifier: MPL-2.0 use crate::widget::container::Catalog; -use crate::widget::{button, column, container, divider, row, space::vertical}; +use crate::widget::space::vertical; +use crate::widget::{button, column, container, divider, row}; use crate::{Apply, Element, theme}; use iced::{Length, Padding}; diff --git a/src/widget/menu/flex.rs b/src/widget/menu/flex.rs index 4a58f13a..213ee694 100644 --- a/src/widget/menu/flex.rs +++ b/src/widget/menu/flex.rs @@ -1,11 +1,9 @@ // From iced_aw, license MIT -use iced_core::{Widget, widget::Tree}; -use iced_widget::core::{ - Alignment, Element, Padding, Point, Size, - layout::{Limits, Node}, - renderer, -}; +use iced_core::Widget; +use iced_core::widget::Tree; +use iced_widget::core::layout::{Limits, Node}; +use iced_widget::core::{Alignment, Element, Padding, Point, Size, renderer}; use crate::widget::RcElementWrapper; diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 981446e8..b5ffca42 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -1,14 +1,14 @@ // From iced_aw, license MIT //! A widget that handles menu trees -use std::{collections::HashMap, sync::Arc}; +use std::collections::HashMap; +use std::sync::Arc; -use super::{ - menu_inner::{ - CloseCondition, Direction, ItemHeight, ItemWidth, Menu, MenuState, PathHighlight, - }, - menu_tree::MenuTree, +use super::menu_inner::{ + CloseCondition, Direction, ItemHeight, ItemWidth, Menu, MenuState, PathHighlight, }; +use super::menu_tree::MenuTree; +use crate::Renderer; #[cfg(all( feature = "multi-window", feature = "wayland", @@ -17,26 +17,21 @@ use super::{ feature = "surface-message" ))] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; -use crate::{ - Renderer, - style::menu_bar::StyleSheet, - widget::{ - RcWrapper, - dropdown::menu::{self, State}, - menu::menu_inner::init_root_menu, - }, -}; +use crate::style::menu_bar::StyleSheet; +use crate::widget::RcWrapper; +use crate::widget::dropdown::menu::{self, State}; +use crate::widget::menu::menu_inner::init_root_menu; -use iced::{Point, Shadow, Vector, event::Status, window}; +use iced::event::Status; +use iced::{Point, Shadow, Vector, window}; use iced_core::Border; +use iced_widget::core::layout::{Limits, Node}; +use iced_widget::core::mouse::{self, Cursor}; +use iced_widget::core::renderer::{self, Renderer as IcedRenderer}; +use iced_widget::core::widget::{Tree, tree}; use iced_widget::core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, event, - layout::{Limits, Node}, - mouse::{self, Cursor}, - overlay, - renderer::{self, Renderer as IcedRenderer}, - touch, - widget::{Tree, tree}, + overlay, touch, }; /// A `MenuBar` collects `MenuTree`s and handles all the layout, event processing, and drawing. @@ -590,7 +585,8 @@ where viewport: &Rectangle, ) { use event::Event::{Mouse, Touch}; - use mouse::{Button::Left, Event::ButtonReleased}; + use mouse::Button::Left; + use mouse::Event::ButtonReleased; use touch::Event::{FingerLifted, FingerLost}; process_root_events( diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 74afe60f..6211e201 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -1,9 +1,11 @@ // From iced_aw, license MIT //! Menu tree overlay -use std::{borrow::Cow, sync::Arc}; +use std::borrow::Cow; +use std::sync::Arc; -use super::{menu_bar::MenuBarState, menu_tree::MenuTree}; +use super::menu_bar::MenuBarState; +use super::menu_tree::MenuTree; #[cfg(all( feature = "multi-window", feature = "wayland", @@ -16,12 +18,12 @@ use crate::style::menu_bar::StyleSheet; use iced::window; use iced_core::{Border, Renderer as IcedRenderer, Shadow, Widget}; +use iced_widget::core::layout::{Limits, Node}; +use iced_widget::core::mouse::{self, Cursor}; +use iced_widget::core::widget::Tree; use iced_widget::core::{ - Clipboard, Layout, Length, Padding, Point, Rectangle, Shell, Size, Vector, event, - layout::{Limits, Node}, - mouse::{self, Cursor}, - overlay, renderer, touch, - widget::Tree, + Clipboard, Layout, Length, Padding, Point, Rectangle, Shell, Size, Vector, event, overlay, + renderer, touch, }; /// The condition of when to close a menu @@ -559,14 +561,10 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) -> Option<(usize, MenuState)> { - use event::{ - Event::{Mouse, Touch}, - Status::{Captured, Ignored}, - }; - use mouse::{ - Button::Left, - Event::{ButtonPressed, ButtonReleased, CursorMoved, WheelScrolled}, - }; + use event::Event::{Mouse, Touch}; + use event::Status::{Captured, Ignored}; + use mouse::Button::Left; + use mouse::Event::{ButtonPressed, ButtonReleased, CursorMoved, WheelScrolled}; use touch::Event::{FingerLifted, FingerMoved, FingerPressed}; if !self diff --git a/src/widget/mod.rs b/src/widget/mod.rs index f442b0da..d0dee7c0 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -154,7 +154,7 @@ pub use dialog::{Dialog, dialog}; pub mod divider { /// Horizontal variant of a divider. pub mod horizontal { - use iced::{widget::Rule, widget::rule}; + use iced::widget::{Rule, rule}; /// Horizontal divider with default thickness #[must_use] diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index ad6f9206..b7391abb 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -6,10 +6,9 @@ //! For details on the model, see the [`segmented_button`] module for more details. use apply::Apply; -use iced::{ - Background, Length, - clipboard::{dnd::DndAction, mime::AllowedMimeTypes}, -}; +use iced::clipboard::dnd::DndAction; +use iced::clipboard::mime::AllowedMimeTypes; +use iced::{Background, Length}; use iced_core::{Border, Color, Shadow}; use crate::widget::{Container, Icon, container, menu, scrollable, segmented_button}; diff --git a/src/widget/popover.rs b/src/widget/popover.rs index af5370a8..57e4568a 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -5,14 +5,10 @@ use iced::widget; 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::touch; use iced_core::widget::{Operation, Tree}; use iced_core::{ - Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, + Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, layout, + mouse, overlay, renderer, touch, }; pub use iced_widget::container::{Catalog, Style}; diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs index 42c8e642..2123d2e6 100644 --- a/src/widget/progress_bar/circular.rs +++ b/src/widget/progress_bar/circular.rs @@ -1,14 +1,10 @@ //! Show a circular progress indicator. use super::animation::{Animation, Progress}; use super::style::StyleSheet; -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::advanced::{self, Clipboard, Layout, Shell, Widget, layout, renderer}; use iced::widget::canvas; -use iced::window; -use iced::{Element, Event, Length, Radians, Rectangle, Renderer, Size, Vector}; +use iced::{Element, Event, Length, Radians, Rectangle, Renderer, Size, Vector, mouse, window}; use std::f32::consts::PI; use std::time::Duration; diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index 7bcbb4f7..7672579e 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -1,13 +1,9 @@ //! Show a linear progress indicator. use super::animation::{Animation, Progress}; use super::style::StyleSheet; -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::window; -use iced::{Background, Element, Event, Length, Rectangle, Size}; +use iced::advanced::{self, Clipboard, Layout, Shell, Widget, layout, renderer}; +use iced::{Background, Element, Event, Length, Rectangle, Size, mouse, window}; use std::time::Duration; diff --git a/src/widget/radio.rs b/src/widget/radio.rs index c3f115c0..7e6ef424 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -2,14 +2,10 @@ use crate::{Theme, theme}; use iced::border; 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::touch; use iced_core::widget::tree::Tree; use iced_core::{ Border, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Vector, Widget, + layout, mouse, overlay, renderer, touch, }; use iced_widget::radio as iced_radio; diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index b3066ecb..839fa9a7 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -6,13 +6,13 @@ use iced::widget::Container; pub use subscription::*; 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::Tree; -use iced_core::{Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget}; -use std::{fmt::Debug, hash::Hash}; +use iced_core::{ + Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, layout, + mouse, overlay, renderer, +}; +use std::fmt::Debug; +use std::hash::Hash; pub use iced_widget::container::{Catalog, Style}; diff --git a/src/widget/rectangle_tracker/subscription.rs b/src/widget/rectangle_tracker/subscription.rs index 02fa4329..22b42441 100644 --- a/src/widget/rectangle_tracker/subscription.rs +++ b/src/widget/rectangle_tracker/subscription.rs @@ -1,13 +1,10 @@ -use iced::{ - Rectangle, - futures::{ - StreamExt, - channel::mpsc::{UnboundedReceiver, unbounded}, - stream, - }, -}; +use iced::Rectangle; +use iced::futures::channel::mpsc::{UnboundedReceiver, unbounded}; +use iced::futures::{StreamExt, stream}; use iced_futures::Subscription; -use std::{collections::HashMap, fmt::Debug, hash::Hash}; +use std::collections::HashMap; +use std::fmt::Debug; +use std::hash::Hash; use super::RectangleTracker; diff --git a/src/widget/responsive_container.rs b/src/widget/responsive_container.rs index b9b6a289..c8925cc2 100644 --- a/src/widget/responsive_container.rs +++ b/src/widget/responsive_container.rs @@ -2,12 +2,11 @@ use iced::{Limits, Size}; use iced_core::event::{self, Event}; -use iced_core::layout; -use iced_core::mouse; -use iced_core::overlay; -use iced_core::renderer; use iced_core::widget::{Id, Operation, Tree, tree}; -use iced_core::{Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget}; +use iced_core::{ + Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay, + renderer, +}; pub(crate) fn responsive_container<'a, Message: 'static, Theme, E>( content: E, diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index b5dd556d..2cae68d8 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -2,10 +2,8 @@ use std::collections::HashMap; use apply::Apply; -use crate::{ - Core, Element, - widget::{button, icon, responsive_container}, -}; +use crate::widget::{button, icon, responsive_container}; +use crate::{Core, Element}; use super::menu::{self, ItemHeight, ItemWidth}; diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index fcb1c596..edd58eb7 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -25,9 +25,11 @@ 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; -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_core::widget::{self, Tree, operation, tree}; +use iced_core::{ + Border, Clipboard, Layout, Point, Renderer as IcedRenderer, Shadow, Shell, Text, Widget, + layout, renderer, +}; use iced_runtime::{Action, task}; use slotmap::{Key, SecondaryMap}; use std::borrow::Cow; diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index 5abb464c..4eb5bb17 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -3,12 +3,11 @@ use std::borrow::Cow; -use crate::{ - Element, Theme, theme, - widget::{FlexRow, Row, column, container, flex_row, list, row, text}, -}; +use crate::widget::{FlexRow, Row, column, container, flex_row, list, row, text}; +use crate::{Element, Theme, theme}; use derive_setters::Setters; -use iced_core::{Length, text::Wrapping}; +use iced_core::Length; +use iced_core::text::Wrapping; use iced_widget::space; use taffy::AlignContent; diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index 833e90b8..71933124 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -3,13 +3,10 @@ //! A control for incremental adjustments of a value. -use crate::{ - Element, theme, - widget::{button, column, container, icon, row, text}, -}; +use crate::widget::{button, column, container, icon, row, text}; +use crate::{Element, theme}; use apply::Apply; -use iced::{Alignment, Length}; -use iced::{Border, Shadow}; +use iced::{Alignment, Border, Length, Shadow}; use std::borrow::Cow; use std::ops::{Add, Sub}; diff --git a/src/widget/table/mod.rs b/src/widget/table/mod.rs index c546383c..dfde3810 100644 --- a/src/widget/table/mod.rs +++ b/src/widget/table/mod.rs @@ -2,12 +2,9 @@ //! pub mod model; -pub use model::{ - Entity, Model, - category::ItemCategory, - category::ItemInterface, - selection::{MultiSelect, SingleSelect}, -}; +pub use model::category::{ItemCategory, ItemInterface}; +pub use model::selection::{MultiSelect, SingleSelect}; +pub use model::{Entity, Model}; pub mod widget; pub use widget::compact::CompactTableView; pub use widget::standard::TableView; diff --git a/src/widget/table/model/entity.rs b/src/widget/table/model/entity.rs index 51c60609..910ccd2f 100644 --- a/src/widget/table/model/entity.rs +++ b/src/widget/table/model/entity.rs @@ -3,10 +3,8 @@ use slotmap::{SecondaryMap, SparseSecondaryMap}; -use super::{ - Entity, Model, Selectable, - category::{ItemCategory, ItemInterface}, -}; +use super::category::{ItemCategory, ItemInterface}; +use super::{Entity, Model, Selectable}; /// A newly-inserted item which may have additional actions applied to it. pub struct EntityMut< diff --git a/src/widget/table/model/mod.rs b/src/widget/table/model/mod.rs index d6250eaf..93998f9f 100644 --- a/src/widget/table/model/mod.rs +++ b/src/widget/table/model/mod.rs @@ -2,10 +2,8 @@ pub mod category; pub mod entity; pub mod selection; -use std::{ - any::{Any, TypeId}, - collections::{HashMap, VecDeque}, -}; +use std::any::{Any, TypeId}; +use std::collections::{HashMap, VecDeque}; use category::{ItemCategory, ItemInterface}; use entity::EntityMut; diff --git a/src/widget/table/model/selection.rs b/src/widget/table/model/selection.rs index 20a07248..3e3d46b5 100644 --- a/src/widget/table/model/selection.rs +++ b/src/widget/table/model/selection.rs @@ -3,10 +3,8 @@ //! Describes logic specific to the single-select and multi-select modes of a model. -use super::{ - Entity, Model, - category::{ItemCategory, ItemInterface}, -}; +use super::category::{ItemCategory, ItemInterface}; +use super::{Entity, Model}; use std::collections::HashSet; /// Describes a type that has selectable items. diff --git a/src/widget/table/widget/compact.rs b/src/widget/table/widget/compact.rs index 65ac9058..6d027caf 100644 --- a/src/widget/table/widget/compact.rs +++ b/src/widget/table/widget/compact.rs @@ -1,14 +1,10 @@ use derive_setters::Setters; -use crate::widget::table::model::{ - Entity, Model, - category::{ItemCategory, ItemInterface}, - selection::Selectable, -}; -use crate::{ - Apply, Element, theme, - widget::{self, container, menu}, -}; +use crate::widget::table::model::category::{ItemCategory, ItemInterface}; +use crate::widget::table::model::selection::Selectable; +use crate::widget::table::model::{Entity, Model}; +use crate::widget::{self, container, menu}; +use crate::{Apply, Element, theme}; use iced::{Alignment, Border, Padding}; #[derive(Setters)] diff --git a/src/widget/table/widget/standard.rs b/src/widget/table/widget/standard.rs index 9ab76c9d..0e206238 100644 --- a/src/widget/table/widget/standard.rs +++ b/src/widget/table/widget/standard.rs @@ -1,14 +1,10 @@ use derive_setters::Setters; -use crate::widget::table::model::{ - Entity, Model, - category::{ItemCategory, ItemInterface}, - selection::Selectable, -}; -use crate::{ - Apply, Element, theme, - widget::{self, container, divider, menu}, -}; +use crate::widget::table::model::category::{ItemCategory, ItemInterface}; +use crate::widget::table::model::selection::Selectable; +use crate::widget::table::model::{Entity, Model}; +use crate::widget::{self, container, divider, menu}; +use crate::{Apply, Element, theme}; use iced::{Alignment, Border, Length, Padding}; // THIS IS A PLACEHOLDER UNTIL A MORE SOPHISTICATED WIDGET CAN BE DEVELOPED diff --git a/src/widget/text_input/editor.rs b/src/widget/text_input/editor.rs index b8144761..c9c2ca7a 100644 --- a/src/widget/text_input/editor.rs +++ b/src/widget/text_input/editor.rs @@ -2,7 +2,8 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MIT -use super::{cursor::Cursor, value::Value}; +use super::cursor::Cursor; +use super::value::Value; pub struct Editor<'a> { value: &'a mut Value, diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 4336c757..ea1122ea 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -28,18 +28,14 @@ use iced_core::overlay::Group; use iced_core::renderer::{self, Renderer as CoreRenderer}; use iced_core::text::{self, Affinity, Paragraph, Renderer, Text}; use iced_core::time::{Duration, Instant}; -use iced_core::touch; use iced_core::widget::Id; use iced_core::widget::operation::{self, Operation}; use iced_core::widget::tree::{self, Tree}; -use iced_core::window; -use iced_core::{Background, alignment}; -use iced_core::{Border, Shadow, keyboard}; use iced_core::{ - Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, Rectangle, Shell, Size, - Vector, Widget, + Background, Border, Clipboard, Color, Element, Layout, Length, Padding, Pixels, Point, + Rectangle, Shadow, Shell, Size, Vector, Widget, alignment, keyboard, layout, overlay, touch, + window, }; -use iced_core::{layout, overlay}; use iced_runtime::{Action, Task, task}; thread_local! { diff --git a/src/widget/text_input/style.rs b/src/widget/text_input/style.rs index 8af5e63e..52b98784 100644 --- a/src/widget/text_input/style.rs +++ b/src/widget/text_input/style.rs @@ -4,7 +4,8 @@ //! Change the appearance of a text input. -use iced_core::{Background, Color, border::Radius}; +use iced_core::border::Radius; +use iced_core::{Background, Color}; /// The appearance of a text input. #[derive(Debug, Clone, Copy)] diff --git a/src/widget/toaster/mod.rs b/src/widget/toaster/mod.rs index bafaa9f9..43acc5c1 100644 --- a/src/widget/toaster/mod.rs +++ b/src/widget/toaster/mod.rs @@ -6,16 +6,13 @@ use std::collections::VecDeque; use std::rc::Rc; -use crate::widget::Column; -use crate::widget::container; +use crate::widget::{Column, container}; use iced::Task; use iced_core::Element; -use slotmap::SlotMap; -use slotmap::new_key_type; +use slotmap::{SlotMap, new_key_type}; use widget::Toaster; -use super::column; -use super::{button, icon, row, text}; +use super::{button, column, icon, row, text}; mod widget; diff --git a/src/widget/toaster/widget.rs b/src/widget/toaster/widget.rs index de47a9bd..922c1433 100644 --- a/src/widget/toaster/widget.rs +++ b/src/widget/toaster/widget.rs @@ -4,16 +4,14 @@ use iced::{Limits, Size}; use iced_core::layout::Node; -use iced_core::Element; -use iced_core::Overlay; use iced_core::event::{self, Event}; -use iced_core::layout; -use iced_core::mouse; -use iced_core::overlay; use iced_core::renderer::{self}; use iced_core::widget::Operation; use iced_core::widget::tree::Tree; -use iced_core::{Clipboard, Layout, Length, Point, Rectangle, Shell, Vector, Widget}; +use iced_core::{ + Clipboard, Element, Layout, Length, Overlay, Point, Rectangle, Shell, Vector, Widget, layout, + mouse, overlay, +}; pub struct Toaster<'a, Message, Theme, Renderer> { toasts: Element<'a, Message, Theme, Renderer>, diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index b95b596e..2dad2ed9 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -3,15 +3,14 @@ use std::time::{Duration, Instant}; use crate::{Element, anim}; +use iced_core::renderer::{self, Renderer}; +use iced_core::widget::{self, Tree, tree}; use iced_core::{ Border, Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, - event, layout, mouse, - renderer::{self, Renderer}, - text, touch, - widget::{self, Tree, tree}, - window, + event, layout, mouse, text, touch, window, }; -use iced_widget::{Id, toggler::Status}; +use iced_widget::Id; +use iced_widget::toggler::Status; pub use iced_widget::toggler::{Catalog, Style}; diff --git a/src/widget/wayland/tooltip/mod.rs b/src/widget/wayland/tooltip/mod.rs index 947d1e83..aa80fad6 100644 --- a/src/widget/wayland/tooltip/mod.rs +++ b/src/widget/wayland/tooltip/mod.rs @@ -5,7 +5,8 @@ pub mod widget; // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -use iced_core::{Background, Color, Vector, border::Radius}; +use iced_core::border::Radius; +use iced_core::{Background, Color, Vector}; use crate::theme::THEME; diff --git a/src/widget/wayland/tooltip/widget.rs b/src/widget/wayland/tooltip/widget.rs index 7bf0991a..203a2010 100644 --- a/src/widget/wayland/tooltip/widget.rs +++ b/src/widget/wayland/tooltip/widget.rs @@ -14,16 +14,12 @@ use iced::Task; use iced_runtime::core::widget::Id; use iced_core::event::{self, Event}; -use iced_core::renderer; -use iced_core::touch; use iced_core::widget::Operation; use iced_core::widget::tree::{self, Tree}; use iced_core::{ - Background, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shell, Vector, Widget, + Background, Border, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shadow, Shell, + Vector, Widget, layout, mouse, overlay, renderer, svg, touch, }; -use iced_core::{Border, mouse}; -use iced_core::{Shadow, overlay}; -use iced_core::{layout, svg}; pub use super::{Catalog, Style}; diff --git a/src/widget/wrapper.rs b/src/widget/wrapper.rs index 133f9b87..aee06e5a 100644 --- a/src/widget/wrapper.rs +++ b/src/widget/wrapper.rs @@ -1,13 +1,13 @@ -use std::{ - borrow::Borrow, - cell::RefCell, - rc::Rc, - thread::{self, ThreadId}, -}; +use std::borrow::Borrow; +use std::cell::RefCell; +use std::rc::Rc; +use std::thread::{self, ThreadId}; use crate::Element; use iced::{Length, Rectangle, Size, event}; -use iced_core::{Widget, id::Id, widget, widget::tree}; +use iced_core::id::Id; +use iced_core::widget::tree; +use iced_core::{Widget, widget}; #[derive(Debug)] pub struct RcWrapper { From 0e0960f3c79138b445ae84f7ac6f91d4db36e8b4 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Fri, 1 May 2026 13:21:07 -0600 Subject: [PATCH 020/201] feat: reorderable flex row --- src/widget/mod.rs | 4 + src/widget/reorderable_flex_row/mod.rs | 8 + src/widget/reorderable_flex_row/widget.rs | 1296 +++++++++++++++++++++ 3 files changed, 1308 insertions(+) create mode 100644 src/widget/reorderable_flex_row/mod.rs create mode 100644 src/widget/reorderable_flex_row/widget.rs diff --git a/src/widget/mod.rs b/src/widget/mod.rs index d0dee7c0..5089b997 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -215,6 +215,10 @@ pub mod flex_row; #[doc(inline)] pub use flex_row::{FlexRow, flex_row}; +pub mod reorderable_flex_row; +#[doc(inline)] +pub use reorderable_flex_row::{ReorderableFlexRow, reorderable_flex_row}; + pub mod grid; #[doc(inline)] pub use grid::{Grid, grid}; diff --git a/src/widget/reorderable_flex_row/mod.rs b/src/widget/reorderable_flex_row/mod.rs new file mode 100644 index 00000000..d5da9f4a --- /dev/null +++ b/src/widget/reorderable_flex_row/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2026 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! A keyed wrapping flex row whose items can be dragged to reorder. + +mod widget; + +pub use widget::{ReorderableFlexRow, reorderable_flex_row}; diff --git a/src/widget/reorderable_flex_row/widget.rs b/src/widget/reorderable_flex_row/widget.rs new file mode 100644 index 00000000..ed95a34f --- /dev/null +++ b/src/widget/reorderable_flex_row/widget.rs @@ -0,0 +1,1296 @@ +// Copyright 2026 System76 +// SPDX-License-Identifier: MPL-2.0 + +use crate::{Element, Renderer}; +use iced::{Alignment, Pixels, alignment}; +use iced_core::event::Event; +use iced_core::layout::{self, Layout}; +use iced_core::mouse::{self, Cursor}; +use iced_core::widget::Operation; +use iced_core::widget::tree::{self, Tree}; +#[cfg(feature = "wgpu")] +use iced_core::{Background, Border, Shadow}; +use iced_core::{ + Clipboard, Length, Padding, Point, Rectangle, Renderer as _, Shell, Size, Vector, Widget, + overlay, renderer, window, +}; +use std::collections::{HashMap, HashSet}; +use std::hash::Hash; +use std::time::{Duration, Instant}; + +const DEFAULT_ANIMATION_DURATION: Duration = Duration::from_millis(180); +const DEFAULT_DRAG_LIFT: f32 = 10.0; +const DEFAULT_DRAG_THRESHOLD: f32 = 6.0; +const SHADOW_BLUR_RADIUS: f32 = 20.0; +const POSITION_EPSILON: f32 = 0.5; + +#[derive(Debug, Clone)] +struct SlotAnimation { + from: Point, + to: Point, + started_at: Option, +} + +impl SlotAnimation { + fn new(position: Point) -> Self { + Self { + from: position, + to: position, + started_at: None, + } + } + + fn current_position(&self, duration: Duration) -> Point { + let Some(started_at) = self.started_at else { + return self.to; + }; + + let duration_secs = duration.as_secs_f32(); + if duration_secs <= f32::EPSILON { + return self.to; + } + + let progress = (started_at.elapsed().as_secs_f32() / duration_secs).clamp(0.0, 1.0); + let eased = 1.0 - (1.0 - progress).powi(3); + + Point::new( + self.from.x + (self.to.x - self.from.x) * eased, + self.from.y + (self.to.y - self.from.y) * eased, + ) + } + + fn retarget(&mut self, new_target: Point, duration: Duration) { + if approx_eq_point(self.to, new_target) { + if !self.is_animating(duration) { + self.from = new_target; + self.to = new_target; + self.started_at = None; + } + return; + } + + self.from = self.current_position(duration); + self.to = new_target; + self.started_at = Some(Instant::now()); + } + + fn is_animating(&self, duration: Duration) -> bool { + self.started_at + .is_some_and(|started_at| started_at.elapsed() < duration) + } + + fn finish_if_done(&mut self, duration: Duration) { + if self + .started_at + .is_some_and(|started_at| started_at.elapsed() >= duration) + { + self.from = self.to; + self.started_at = None; + } + } +} + +#[derive(Debug, Clone)] +struct PendingDragState +where + Key: Clone + Eq + Hash + 'static, +{ + key: Key, + item_index: usize, + original_index: usize, + press_local: Point, + pointer_offset: Vector, +} + +#[derive(Debug, Clone)] +struct DragState +where + Key: Clone + Eq + Hash + 'static, +{ + key: Key, + item_index: usize, + original_index: usize, + current_index: usize, + cursor_local: Point, + pointer_offset: Vector, +} + +#[derive(Debug, Clone)] +struct State +where + Key: Clone + Eq + Hash + 'static, +{ + keys: Vec, + slot_positions: HashMap, + pending_drag: Option>, + drag: Option>, + wrap_width: f32, + initialized: bool, +} + +impl Default for State +where + Key: Clone + Eq + Hash + 'static, +{ + fn default() -> Self { + Self { + keys: Vec::new(), + slot_positions: HashMap::new(), + pending_drag: None, + drag: None, + wrap_width: f32::INFINITY, + initialized: false, + } + } +} + +impl State +where + Key: Clone + Eq + Hash + 'static, +{ + fn retain_keys(&mut self, keys: &[Key]) { + let keep: HashSet<_> = keys.iter().cloned().collect(); + self.slot_positions.retain(|key, _| keep.contains(key)); + + if self + .pending_drag + .as_ref() + .is_some_and(|drag| !keep.contains(&drag.key)) + { + self.pending_drag = None; + } + + if self + .drag + .as_ref() + .is_some_and(|drag| !keep.contains(&drag.key)) + { + self.drag = None; + } + } + + fn finish_animations(&mut self, duration: Duration) { + self.slot_positions + .values_mut() + .for_each(|slot| slot.finish_if_done(duration)); + } + + fn is_animating(&self, duration: Duration) -> bool { + self.slot_positions + .values() + .any(|slot| slot.is_animating(duration)) + } + + fn current_slot_position(&self, key: &Key, fallback: Point, duration: Duration) -> Point { + self.slot_positions + .get(key) + .map(|slot| slot.current_position(duration)) + .unwrap_or(fallback) + } + + fn retarget_slot(&mut self, key: &Key, target: Point, duration: Duration) { + self.slot_positions + .entry(key.clone()) + .or_insert_with(|| SlotAnimation::new(target)) + .retarget(target, duration); + } + + fn snap_slot(&mut self, key: &Key, target: Point) { + self.slot_positions + .insert(key.clone(), SlotAnimation::new(target)); + } + + fn apply_layout_position(&mut self, key: &Key, target: Point, duration: Duration) { + if self.initialized { + self.retarget_slot(key, target, duration); + } else { + self.snap_slot(key, target); + } + } +} + +#[derive(Debug, Clone)] +struct LocalSlot +where + Key: Clone + Eq + Hash + 'static, +{ + key: Key, + locked: bool, + bounds: Rectangle, +} + +/// A horizontal flex row with drag-to-reorder behavior. +#[must_use] +pub struct ReorderableFlexRow<'a, Key, Message> +where + Key: Clone + Eq + Hash + 'static, +{ + spacing: f32, + padding: Padding, + width: Length, + height: Length, + align: Alignment, + clip: bool, + drag_lift: f32, + animation_duration: Duration, + on_reorder: Box) -> Message + 'a>, + keys: Vec, + locked: Vec, + children: Vec>, +} + +impl<'a, Key, Message> ReorderableFlexRow<'a, Key, Message> +where + Key: Clone + Eq + Hash + 'static, +{ + pub fn new(on_reorder: impl Fn(Vec) -> Message + 'a) -> Self { + Self { + spacing: 0.0, + padding: Padding::ZERO, + width: Length::Shrink, + height: Length::Shrink, + align: Alignment::Start, + clip: false, + drag_lift: DEFAULT_DRAG_LIFT, + animation_duration: DEFAULT_ANIMATION_DURATION, + on_reorder: Box::new(on_reorder), + keys: Vec::new(), + locked: Vec::new(), + children: Vec::new(), + } + } + + pub fn with_capacity(capacity: usize, on_reorder: impl Fn(Vec) -> Message + 'a) -> Self { + let mut row = Self::new(on_reorder); + row.keys = Vec::with_capacity(capacity); + row.locked = Vec::with_capacity(capacity); + row.children = Vec::with_capacity(capacity); + row + } + + pub fn spacing(mut self, amount: impl Into) -> Self { + self.spacing = amount.into().0; + self + } + + pub fn padding>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + pub fn align_y(mut self, align: impl Into) -> Self { + self.align = Alignment::from(align.into()); + self + } + + /// Leave disabled for dragged item to visibly lift above the row. + pub fn clip(mut self, clip: bool) -> Self { + self.clip = clip; + self + } + + pub fn drag_lift(mut self, lift: f32) -> Self { + self.drag_lift = lift.max(0.0); + self + } + + pub fn animation_duration(mut self, duration: Duration) -> Self { + self.animation_duration = duration; + self + } + + pub fn push(self, key: Key, child: impl Into>) -> Self { + self.push_with_lock(key, false, child) + } + + /// Item stays fixed, never participates in reordering. + pub fn push_locked(self, key: Key, child: impl Into>) -> Self { + self.push_with_lock(key, true, child) + } + + fn push_with_lock( + mut self, + key: Key, + locked: bool, + child: impl Into>, + ) -> Self { + let child = child.into(); + let child_size = child.as_widget().size_hint(); + + if !child_size.is_void() { + self.width = self.width.enclose(child_size.width); + self.height = self.height.enclose(child_size.height); + self.keys.push(key); + self.locked.push(locked); + self.children.push(child); + } + + self + } + + pub fn extend(self, items: impl IntoIterator)>) -> Self { + items + .into_iter() + .fold(self, |row, (key, child)| row.push(key, child)) + } + + pub fn extend_locked( + self, + items: impl IntoIterator)>, + ) -> Self { + items + .into_iter() + .fold(self, |row, (key, child)| row.push_locked(key, child)) + } + + fn item_local_slots_from_layout( + &self, + bounds: Rectangle, + child_layouts: &[Layout<'_>], + ) -> Vec> { + self.keys + .iter() + .zip(self.locked.iter()) + .zip(child_layouts.iter()) + .map(|((key, locked), child_layout)| { + let child_bounds = child_layout.bounds(); + LocalSlot { + key: key.clone(), + locked: *locked, + bounds: Rectangle { + x: child_bounds.x - bounds.x, + y: child_bounds.y - bounds.y, + width: child_bounds.width, + height: child_bounds.height, + }, + } + }) + .collect() + } + + fn sync_slot_positions(&self, state: &mut State, slots: &[LocalSlot]) { + let ordered_keys: Vec = slots.iter().map(|slot| slot.key.clone()).collect(); + state.retain_keys(&ordered_keys); + + let size_by_key: HashMap = slots + .iter() + .map(|slot| { + ( + slot.key.clone(), + Size::new(slot.bounds.width, slot.bounds.height), + ) + }) + .collect(); + let locked_by_key: HashMap = slots + .iter() + .map(|slot| (slot.key.clone(), slot.locked)) + .collect(); + + let Some(drag_snapshot) = state.drag.as_ref().map(|drag| { + ( + drag.key.clone(), + drag.cursor_local, + drag.pointer_offset, + drag.item_index, + ) + }) else { + for slot in slots { + state.apply_layout_position( + &slot.key, + Point::new(slot.bounds.x, slot.bounds.y), + self.animation_duration, + ); + } + return; + }; + + let (drag_key, cursor_local, pointer_offset, drag_item_index) = drag_snapshot; + + let Some(dragged_slot) = slots.iter().find(|slot| slot.key == drag_key) else { + state.drag = None; + for slot in slots { + state.apply_layout_position( + &slot.key, + Point::new(slot.bounds.x, slot.bounds.y), + self.animation_duration, + ); + } + return; + }; + + if dragged_slot.locked { + state.drag = None; + return; + } + + let dragged_center = Point::new( + cursor_local.x - pointer_offset.x + dragged_slot.bounds.width / 2.0, + cursor_local.y - pointer_offset.y + dragged_slot.bounds.height / 2.0, + ); + let target_index = target_index_for_drag(slots, &drag_key, dragged_center); + let prior_index = state.drag.as_ref().map(|drag| drag.current_index); + + if let Some(drag) = state.drag.as_mut() { + drag.current_index = target_index; + drag.item_index = drag_item_index; + } + + if prior_index == Some(target_index) { + return; + } + + let reordered_keys = + reordered_keys_for_drag(&ordered_keys, &locked_by_key, &drag_key, target_index); + let (target_slots, _) = compute_wrapped_slots( + &reordered_keys, + &locked_by_key, + &size_by_key, + state.wrap_width, + self.padding, + self.spacing, + self.align, + ); + let target_positions: HashMap = target_slots + .iter() + .map(|slot| (slot.key.clone(), Point::new(slot.bounds.x, slot.bounds.y))) + .collect(); + + for slot in slots { + let target = target_positions + .get(&slot.key) + .copied() + .unwrap_or(Point::new(slot.bounds.x, slot.bounds.y)); + state.retarget_slot(&slot.key, target, self.animation_duration); + } + } +} + +impl<'a, Key, Message> Widget + for ReorderableFlexRow<'a, Key, Message> +where + Key: Clone + Eq + Hash + 'static, + Message: 'a, +{ + fn tag(&self) -> tree::Tag { + tree::Tag::of::>() + } + + fn state(&self) -> tree::State { + tree::State::new(State { + keys: self.keys.clone(), + ..State::default() + }) + } + + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() + } + + fn diff(&mut self, tree: &mut Tree) { + let Tree { + state, children, .. + } = tree; + let state = state.downcast_mut::>(); + let previous_keys = state.keys.clone(); + let previous_children = std::mem::take(children); + let mut previous_by_key = HashMap::with_capacity(previous_keys.len()); + + for (key, child_tree) in previous_keys.into_iter().zip(previous_children) { + previous_by_key.insert(key, child_tree); + } + + children.reserve(self.children.len()); + + for (key, child) in self.keys.iter().cloned().zip(self.children.iter_mut()) { + let mut child_tree = previous_by_key + .remove(&key) + .unwrap_or_else(|| Tree::new(child.as_widget())); + child.as_widget_mut().diff(&mut child_tree); + children.push(child_tree); + } + + if state.keys != self.keys { + state.keys.clone_from(&self.keys); + } + state.retain_keys(&self.keys); + } + + fn size(&self) -> Size { + Size { + width: self.width, + height: self.height, + } + } + + fn layout( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + limits: &layout::Limits, + ) -> layout::Node { + let limits = limits + .width(self.width) + .height(self.height) + .shrink(self.padding); + let child_limits = limits.loose(); + let wrap_width = limits.max().width; + + let mut nodes = Vec::with_capacity(self.children.len()); + let mut size_by_key = HashMap::with_capacity(self.children.len()); + let locked_by_key: HashMap = self + .keys + .iter() + .cloned() + .zip(self.locked.iter().copied()) + .collect(); + + for ((key, child), child_tree) in self + .keys + .iter() + .zip(self.children.iter_mut()) + .zip(tree.children.iter_mut()) + { + let node = child + .as_widget_mut() + .layout(child_tree, renderer, &child_limits); + size_by_key.insert(key.clone(), node.size()); + nodes.push(node); + } + + let (slots, intrinsic_size) = compute_wrapped_slots( + &self.keys, + &locked_by_key, + &size_by_key, + wrap_width, + self.padding, + self.spacing, + self.align, + ); + + for (node, slot) in nodes.iter_mut().zip(&slots) { + node.move_to_mut(Point::new(slot.bounds.x, slot.bounds.y)); + } + + let size = limits.resolve(self.width, self.height, intrinsic_size); + let node = layout::Node::with_children(size.expand(self.padding), nodes); + let state = tree.state.downcast_mut::>(); + state.wrap_width = wrap_width; + self.sync_slot_positions(state, &slots); + + node + } + + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { + self.children + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), child_layout)| { + child.as_widget_mut().operate( + state, + child_layout.with_virtual_offset(layout.virtual_offset()), + renderer, + operation, + ); + }); + }); + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor: Cursor, + renderer: &Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + let bounds = layout.bounds(); + let state = tree.state.downcast_mut::>(); + let child_layouts: Vec<_> = layout.children().collect(); + + if let Event::Window(window::Event::RedrawRequested(_)) = event { + state.initialized = true; + state.finish_animations(self.animation_duration); + if state.drag.is_some() || state.is_animating(self.animation_duration) { + shell.request_redraw(); + } + } + + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { + if state.drag.is_none() + && state.pending_drag.is_none() + && let Some(cursor_pos) = cursor.position() + && let Some((index, child_layout)) = child_layouts + .iter() + .enumerate() + .find(|(_, child_layout)| child_layout.bounds().contains(cursor_pos)) + && !self.locked.get(index).copied().unwrap_or(false) + && let Some(reorder_index) = reorderable_index_for_child(&self.locked, index) + { + let child_bounds = child_layout.bounds(); + state.pending_drag = Some(PendingDragState { + key: self.keys[index].clone(), + item_index: index, + original_index: reorder_index, + press_local: Point::new(cursor_pos.x - bounds.x, cursor_pos.y - bounds.y), + pointer_offset: Vector::new( + cursor_pos.x - child_bounds.x, + cursor_pos.y - child_bounds.y, + ), + }); + } + } + Event::Mouse(mouse::Event::CursorMoved { .. }) => { + if let Some(pending) = state.pending_drag.clone() + && let Some(cursor_pos) = cursor.position() + { + let cursor_local = Point::new(cursor_pos.x - bounds.x, cursor_pos.y - bounds.y); + let delta = Vector::new( + cursor_local.x - pending.press_local.x, + cursor_local.y - pending.press_local.y, + ); + let distance = (delta.x.powi(2) + delta.y.powi(2)).sqrt(); + + if distance >= DEFAULT_DRAG_THRESHOLD { + if let (Some(child), Some(child_tree), Some(child_layout)) = ( + self.children.get_mut(pending.item_index), + tree.children.get_mut(pending.item_index), + child_layouts.get(pending.item_index), + ) { + let cursor_left = Event::Mouse(mouse::Event::CursorLeft); + child.as_widget_mut().update( + child_tree, + &cursor_left, + child_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + + state.pending_drag = None; + state.drag = Some(DragState { + key: pending.key, + item_index: pending.item_index, + original_index: pending.original_index, + current_index: pending.original_index, + cursor_local, + pointer_offset: pending.pointer_offset, + }); + let slots = self.item_local_slots_from_layout(bounds, &child_layouts); + self.sync_slot_positions(state, &slots); + shell.capture_event(); + shell.request_redraw(); + return; + } + } + + if let Some(drag) = state.drag.as_mut() + && let Some(cursor_pos) = cursor.position() + { + drag.cursor_local = + Point::new(cursor_pos.x - bounds.x, cursor_pos.y - bounds.y); + let slots = self.item_local_slots_from_layout(bounds, &child_layouts); + self.sync_slot_positions(state, &slots); + shell.capture_event(); + shell.request_redraw(); + return; + } + } + Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => { + state.pending_drag = None; + + if state.drag.is_some() { + let slots = self.item_local_slots_from_layout(bounds, &child_layouts); + self.sync_slot_positions(state, &slots); + } + if let Some(drag) = state.drag.take() { + if drag.current_index != drag.original_index { + let locked_by_key: HashMap = self + .keys + .iter() + .cloned() + .zip(self.locked.iter().copied()) + .collect(); + let new_order = reordered_keys_for_drag( + &self.keys, + &locked_by_key, + &drag.key, + drag.current_index, + ); + shell.publish((self.on_reorder)(new_order)); + } + shell.capture_event(); + shell.request_redraw(); + return; + } + } + _ => {} + } + + if state.drag.is_some() { + return; + } + + for ((item, tree), child_layout) in self + .children + .iter_mut() + .zip(&mut tree.children) + .zip(child_layouts.into_iter()) + { + item.as_widget_mut().update( + tree, + event, + child_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let state = tree.state.downcast_ref::>(); + + if state.drag.is_some() { + return mouse::Interaction::Grabbing; + } + + if let Some(cursor_pos) = cursor.position() + && self + .locked + .iter() + .zip(layout.children()) + .any(|(locked, child_layout)| { + !*locked && child_layout.bounds().contains(cursor_pos) + }) + { + return mouse::Interaction::Grab; + } + + self.children + .iter() + .zip(&tree.children) + .zip(layout.children()) + .map(|((child, tree), child_layout)| { + child.as_widget().mouse_interaction( + tree, + child_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + viewport, + renderer, + ) + }) + .max() + .unwrap_or_default() + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: Cursor, + viewport: &Rectangle, + ) { + let state = tree.state.downcast_ref::>(); + let bounds = layout.bounds(); + + let Some(clipped_viewport) = bounds.intersection(viewport) else { + return; + }; + + let viewport = if self.clip { + &clipped_viewport + } else { + viewport + }; + let drag_key = state.drag.as_ref().map(|drag| &drag.key); + let drag_item = state.drag.as_ref().and_then(|drag| { + self.keys + .iter() + .zip(&self.children) + .zip(&tree.children) + .zip(layout.children()) + .find_map(|(((key, child), state), child_layout)| { + (key == &drag.key).then_some((key, child, state, child_layout, drag)) + }) + }); + + for (((key, child), child_tree), child_layout) in self + .keys + .iter() + .zip(&self.children) + .zip(&tree.children) + .zip(layout.children()) + { + if drag_key.is_some_and(|drag_key| drag_key == key) { + continue; + } + + let child_layout = child_layout.with_virtual_offset(layout.virtual_offset()); + let child_bounds = child_layout.bounds(); + let base_local = Point::new(child_bounds.x - bounds.x, child_bounds.y - bounds.y); + let target_local = + state.current_slot_position(key, base_local, self.animation_duration); + let translation = + Vector::new(target_local.x - base_local.x, target_local.y - base_local.y); + let translated_bounds = translate_rect(child_bounds, translation); + + if translated_bounds.intersects(viewport) { + renderer.with_translation(translation, |renderer| { + child.as_widget().draw( + child_tree, + renderer, + theme, + style, + child_layout, + cursor, + viewport, + ); + }); + } + } + + if let Some((_key, child, child_tree, child_layout, drag)) = drag_item { + let child_layout = child_layout.with_virtual_offset(layout.virtual_offset()); + let child_bounds = child_layout.bounds(); + let base_local = Point::new(child_bounds.x - bounds.x, child_bounds.y - bounds.y); + let drag_local = Point::new( + drag.cursor_local.x - drag.pointer_offset.x, + drag.cursor_local.y - drag.pointer_offset.y - self.drag_lift, + ); + let translation = Vector::new(drag_local.x - base_local.x, drag_local.y - base_local.y); + + #[cfg(feature = "wgpu")] + { + let translated_bounds = translate_rect(child_bounds, translation); + draw_drag_backdrop(renderer, theme, translated_bounds); + } + + renderer.with_translation(translation, |renderer| { + child.as_widget().draw( + child_tree, + renderer, + theme, + style, + child_layout, + cursor, + viewport, + ); + }); + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'b>, + renderer: &Renderer, + viewport: &Rectangle, + translation: Vector, + ) -> Option> { + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + viewport, + translation, + ) + } + + fn drag_destinations( + &self, + state: &Tree, + layout: Layout<'_>, + renderer: &Renderer, + dnd_rectangles: &mut iced_core::clipboard::DndDestinationRectangles, + ) { + for ((item, child_layout), child_state) in self + .children + .iter() + .zip(layout.children()) + .zip(state.children.iter()) + { + item.as_widget().drag_destinations( + child_state, + child_layout.with_virtual_offset(layout.virtual_offset()), + renderer, + dnd_rectangles, + ); + } + } +} + +impl<'a, Key, Message> From> for Element<'a, Message> +where + Key: Clone + Eq + Hash + 'static, + Message: 'a, +{ + fn from(row: ReorderableFlexRow<'a, Key, Message>) -> Self { + Element::new(row) + } +} + +/// Create a horizontal flex row with drag-to-reorder behavior. +pub fn reorderable_flex_row<'a, Key, Message>( + on_reorder: impl Fn(Vec) -> Message + 'a, +) -> ReorderableFlexRow<'a, Key, Message> +where + Key: Clone + Eq + Hash + 'static, +{ + ReorderableFlexRow::new(on_reorder) +} + +fn compute_wrapped_slots( + ordered_keys: &[Key], + locked_by_key: &HashMap, + size_by_key: &HashMap, + wrap_width: f32, + padding: Padding, + spacing: f32, + align: Alignment, +) -> (Vec>, Size) +where + Key: Clone + Eq + Hash + 'static, +{ + let wrap_width = if wrap_width.is_finite() { + wrap_width.max(0.0) + } else { + f32::INFINITY + }; + + let mut slots = Vec::with_capacity(ordered_keys.len()); + let mut intrinsic_size = Size::ZERO; + let mut row_start = 0; + let mut row_height = 0.0; + let mut x = 0.0; + let mut y = 0.0; + + let align_factor = match align { + Alignment::Start => 0.0, + Alignment::Center => 2.0, + Alignment::End => 1.0, + }; + + let align_row = + |range: std::ops::Range, row_height: f32, slots: &mut [LocalSlot]| { + if align_factor == 0.0 { + return; + } + + for slot in &mut slots[range] { + slot.bounds.y += (row_height - slot.bounds.height) / align_factor; + } + }; + + for (index, key) in ordered_keys.iter().enumerate() { + let size = size_by_key.get(key).copied().unwrap_or(Size::ZERO); + + if x != 0.0 && x + size.width > wrap_width { + intrinsic_size.width = intrinsic_size.width.max(x - spacing); + align_row(row_start..index, row_height, &mut slots); + y += row_height + spacing; + x = 0.0; + row_start = index; + row_height = 0.0; + } + + row_height = row_height.max(size.height); + + slots.push(LocalSlot { + key: key.clone(), + locked: locked_by_key.get(key).copied().unwrap_or(false), + bounds: Rectangle { + x: x + padding.left, + y: y + padding.top, + width: size.width, + height: size.height, + }, + }); + + x += size.width + spacing; + } + + if x != 0.0 { + intrinsic_size.width = intrinsic_size.width.max(x - spacing); + } + + intrinsic_size.height = y + row_height; + align_row(row_start..slots.len(), row_height, &mut slots); + + (slots, intrinsic_size) +} + +fn reordered_keys_for_drag( + ordered_keys: &[Key], + locked_by_key: &HashMap, + dragged_key: &Key, + target_index: usize, +) -> Vec +where + Key: Clone + Eq + Hash + 'static, +{ + let movable_keys: Vec = ordered_keys + .iter() + .filter(|key| !locked_by_key.get(*key).copied().unwrap_or(false)) + .cloned() + .collect(); + let mut remaining: Vec = movable_keys + .iter() + .filter(|key| *key != dragged_key) + .cloned() + .collect(); + + remaining.insert(target_index.min(remaining.len()), dragged_key.clone()); + merge_locked_and_reordered_keys(ordered_keys, locked_by_key, &remaining) +} + +/// Picks insertion index among movable items using row-aware midpoint rule. +/// +/// Walks laid-out slots in reading order, counting how many non-dragged movable +/// items the cursor has moved past. Skips locked slots. O(n) single pass, no +/// allocations. +fn target_index_for_drag( + slots: &[LocalSlot], + dragged_key: &Key, + dragged_center: Point, +) -> usize +where + Key: Clone + Eq + Hash + 'static, +{ + let mut target = 0; + let mut passed_movable: usize = 0; + let mut found_target = false; + + let mut i = 0; + while i < slots.len() { + let slot = &slots[i]; + + if slot.locked || &slot.key == dragged_key { + i += 1; + continue; + } + + let row_top = slot.bounds.y; + let row_bottom = row_top + slot.bounds.height; + + if !found_target && dragged_center.y < row_top { + target = passed_movable; + found_target = true; + break; + } + + if dragged_center.y > row_bottom { + passed_movable += 1; + i += 1; + continue; + } + + let center_x = slot.bounds.x + slot.bounds.width / 2.0; + if dragged_center.x < center_x { + target = passed_movable; + found_target = true; + break; + } + + passed_movable += 1; + i += 1; + } + + if !found_target { + target = passed_movable; + } + + target +} + +fn reorderable_index_for_child(locked: &[bool], item_index: usize) -> Option { + (!locked.get(item_index).copied().unwrap_or(false)).then(|| { + locked[..item_index] + .iter() + .filter(|is_locked| !**is_locked) + .count() + }) +} + +fn merge_locked_and_reordered_keys( + ordered_keys: &[Key], + locked_by_key: &HashMap, + reordered_movable_keys: &[Key], +) -> Vec +where + Key: Clone + Eq + Hash + 'static, +{ + let mut movable = reordered_movable_keys.iter(); + + ordered_keys + .iter() + .map(|key| { + if locked_by_key.get(key).copied().unwrap_or(false) { + key.clone() + } else { + movable.next().cloned().unwrap_or_else(|| key.clone()) + } + }) + .collect() +} + +fn approx_eq_point(a: Point, b: Point) -> bool { + (a.x - b.x).abs() <= POSITION_EPSILON && (a.y - b.y).abs() <= POSITION_EPSILON +} + +fn translate_rect(bounds: Rectangle, translation: Vector) -> Rectangle { + Rectangle { + x: bounds.x + translation.x, + y: bounds.y + translation.y, + width: bounds.width, + height: bounds.height, + } +} + +#[cfg(feature = "wgpu")] +fn draw_drag_backdrop(renderer: &mut Renderer, theme: &crate::Theme, bounds: Rectangle) { + let cosmic = theme.cosmic(); + let backdrop = iced::Color { + a: 0.08, + ..iced::Color::from(cosmic.bg_component_color()) + }; + + renderer.fill_quad( + renderer::Quad { + bounds, + border: Border { + radius: cosmic.corner_radii.radius_m.into(), + ..Border::default() + }, + shadow: Shadow { + color: cosmic.shade.into(), + offset: Vector::new(0.0, 8.0), + blur_radius: SHADOW_BLUR_RADIUS, + }, + snap: true, + }, + Background::Color(backdrop), + ); +} + +#[cfg(test)] +mod tests { + use super::{compute_wrapped_slots, reordered_keys_for_drag, target_index_for_drag}; + use iced::{Alignment, Padding, Point, Size}; + use std::collections::HashMap; + + fn size_map(keys: &[usize], width: f32, height: f32) -> HashMap { + keys.iter() + .copied() + .map(|key| (key, Size::new(width, height))) + .collect() + } + + fn locked_map(keys: &[usize], locked_keys: &[usize]) -> HashMap { + keys.iter() + .copied() + .map(|key| (key, locked_keys.contains(&key))) + .collect() + } + + #[test] + fn compute_wrapped_slots_creates_new_rows() { + let ordered_keys = vec![0, 1, 2]; + let locked_by_key = locked_map(&ordered_keys, &[]); + let size_by_key = size_map(&ordered_keys, 100.0, 40.0); + let (slots, intrinsic_size) = compute_wrapped_slots( + &ordered_keys, + &locked_by_key, + &size_by_key, + 220.0, + Padding::ZERO, + 10.0, + Alignment::Start, + ); + + assert_eq!(slots[0].bounds.x, 0.0); + assert_eq!(slots[0].bounds.y, 0.0); + assert_eq!(slots[1].bounds.x, 110.0); + assert_eq!(slots[1].bounds.y, 0.0); + assert_eq!(slots[2].bounds.x, 0.0); + assert_eq!(slots[2].bounds.y, 50.0); + assert_eq!(intrinsic_size.width, 210.0); + assert_eq!(intrinsic_size.height, 90.0); + } + + #[test] + fn reordered_keys_for_drag_inserts_key_at_target_index() { + let keys = [0, 1, 2, 3]; + let locked_by_key = locked_map(&keys, &[]); + let reordered = reordered_keys_for_drag(&keys, &locked_by_key, &0, 3); + assert_eq!(reordered, vec![1, 2, 3, 0]); + } + + #[test] + fn target_index_tracks_wrapped_drop_positions() { + let ordered_keys = vec![0, 1, 2, 3]; + let locked_by_key = locked_map(&ordered_keys, &[]); + let size_by_key = size_map(&ordered_keys, 100.0, 40.0); + + let (slots, _) = compute_wrapped_slots( + &ordered_keys, + &locked_by_key, + &size_by_key, + 220.0, + Padding::ZERO, + 10.0, + Alignment::Start, + ); + + let target_index = target_index_for_drag(&slots, &0, Point::new(160.0, 70.0)); + + assert_eq!(target_index, 3); + } + + #[test] + fn reordered_keys_for_drag_preserves_locked_positions() { + let keys = [10, 11, 12, 13]; + let locked_by_key = locked_map(&keys, &[10, 13]); + let reordered = reordered_keys_for_drag(&keys, &locked_by_key, &11, 1); + + assert_eq!(reordered, vec![10, 12, 11, 13]); + } +} From 528513725d3856e5094cfbff5f0dc1a79d6c4018 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 7 May 2026 12:40:51 -0400 Subject: [PATCH 021/201] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 3928e0d3..d7595fab 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 3928e0d38572d63ff1ef1d768b17770e9a91d7c0 +Subproject commit d7595fab95579f640d43d90e16c174709c4b5c01 From b105e9a108484f2199cbc55b1c4e68bfe0a7a010 Mon Sep 17 00:00:00 2001 From: KENZ Date: Sat, 9 May 2026 00:25:49 +0900 Subject: [PATCH 022/201] feat(iced): IME support improvements --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index d7595fab..1444d5c4 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit d7595fab95579f640d43d90e16c174709c4b5c01 +Subproject commit 1444d5c43e8b103ca8077555606443ab77e03ce5 From 2aa9133f9d3334e8f77bd596c67727a2698e8bed Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 13 May 2026 16:14:34 +0200 Subject: [PATCH 023/201] docs(readme): add Made for COSMIC Flatpak section --- README.md | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/README.md b/README.md index 23da97bc..698316db 100644 --- a/README.md +++ b/README.md @@ -20,6 +20,24 @@ While libcosmic is written entirely in Rust, some of its dependencies may requir sudo apt install cargo cmake just libexpat1-dev libfontconfig-dev libfreetype-dev libxkbcommon-dev pkgconf ``` +## Made-for-COSMIC Flatpak IDs + +To identify a project as a COSMIC Application, add `com.system76.CosmicApplication` to the provides section of the project's metainfo. + +```xml + + com.system76.CosmicApplication + +``` + +For COSMIC Applets, use `com.system76.CosmicApplet`. + +```xml + + com.system76.CosmicApplet + +``` + ## Examples Some examples are included in the [examples](./examples) directory to to kickstart your From cab9d5aff2d8561fc540fd0bbc3b615ba56d641a Mon Sep 17 00:00:00 2001 From: Ilia Malanin Date: Thu, 14 May 2026 16:10:59 +0200 Subject: [PATCH 024/201] feat!(menu): support physical key matches for non-Latin keyboard layouts --- src/widget/menu/key_bind.rs | 86 +++++++++++++++++++++++++++++++++---- 1 file changed, 78 insertions(+), 8 deletions(-) diff --git a/src/widget/menu/key_bind.rs b/src/widget/menu/key_bind.rs index 8b4ed227..d6e40e07 100644 --- a/src/widget/menu/key_bind.rs +++ b/src/widget/menu/key_bind.rs @@ -1,3 +1,4 @@ +use iced_core::keyboard::key::{Code, Physical}; use iced_core::keyboard::{Key, Modifiers}; use std::fmt; @@ -27,28 +28,97 @@ pub struct KeyBind { } impl KeyBind { - /// Checks if the given key and modifiers match the `KeyBind`. + /// Checks if the given key and modifiers match the `KeyBind`, with an + /// optional fallback to the physical key position for non-Latin keyboard + /// layouts. /// /// # Arguments /// /// * `modifiers` - A `Modifiers` instance representing the current active modifiers. /// * `key` - A reference to the `Key` that is being checked. + /// * `physical_key` - An optional reference to the physical key position, + /// used as a fallback when the logical `key` does not match (e.g. on + /// Cyrillic or other non-Latin layouts). Can be `None` for keys where + /// the physical position is not relevant (e.g. `Key::Named`). /// /// # Returns /// /// * `bool` - `true` if the key and modifiers match the `KeyBind`, `false` otherwise. - pub fn matches(&self, modifiers: Modifiers, key: &Key) -> bool { - let key_eq = match (key, &self.key) { - // CapsLock and Shift change the case of Key::Character, so we compare these in a case insensitive way - (Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(b), - (a, b) => a.eq(b), - }; + pub fn matches(&self, modifiers: Modifiers, key: &Key, physical_key: Option<&Physical>) -> bool { + let key_eq = self.key_eq(key) + || physical_key + .and_then(physical_key_to_latin) + .is_some_and(|latin| self.key_eq(&latin)); key_eq && modifiers.logo() == self.modifiers.contains(&Modifier::Super) && modifiers.control() == self.modifiers.contains(&Modifier::Ctrl) && modifiers.alt() == self.modifiers.contains(&Modifier::Alt) && modifiers.shift() == self.modifiers.contains(&Modifier::Shift) } + + fn key_eq(&self, key: &Key) -> bool { + match (key, &self.key) { + // CapsLock and Shift change the case of Key::Character, so we compare these in a case insensitive way + (Key::Character(a), Key::Character(b)) => a.eq_ignore_ascii_case(b), + (a, b) => a.eq(b), + } + } +} + +/// Converts a physical key code to the corresponding US-layout Latin `Key`. +/// +/// This mapping is intentionally limited to keys that may produce different +/// characters on non-Latin keyboard layouts (letters and punctuation). Keys +/// like digits are not included because they remain the same across layouts. +/// +/// Only used as a fallback when the primary key comparison in +/// [`KeyBind::matches`] does not match. +fn physical_key_to_latin(physical_key: &Physical) -> Option { + let code = match physical_key { + Physical::Code(code) => code, + Physical::Unidentified(_) => return None, + }; + let ch = match code { + Code::KeyA => "a", + Code::KeyB => "b", + Code::KeyC => "c", + Code::KeyD => "d", + Code::KeyE => "e", + Code::KeyF => "f", + Code::KeyG => "g", + Code::KeyH => "h", + Code::KeyI => "i", + Code::KeyJ => "j", + Code::KeyK => "k", + Code::KeyL => "l", + Code::KeyM => "m", + Code::KeyN => "n", + Code::KeyO => "o", + Code::KeyP => "p", + Code::KeyQ => "q", + Code::KeyR => "r", + Code::KeyS => "s", + Code::KeyT => "t", + Code::KeyU => "u", + Code::KeyV => "v", + Code::KeyW => "w", + Code::KeyX => "x", + Code::KeyY => "y", + Code::KeyZ => "z", + Code::Minus => "-", + Code::Equal => "=", + Code::BracketLeft => "[", + Code::BracketRight => "]", + Code::Backslash => "\\", + Code::Semicolon => ";", + Code::Quote => "'", + Code::Backquote => "`", + Code::Comma => ",", + Code::Period => ".", + Code::Slash => "/", + _ => return None, + }; + Some(Key::Character(ch.into())) } impl fmt::Display for KeyBind { @@ -62,4 +132,4 @@ impl fmt::Display for KeyBind { other => write!(f, "{:?}", other), } } -} +} \ No newline at end of file From 631c81b170b792fa57898161b73a5a673ad79b48 Mon Sep 17 00:00:00 2001 From: DanielKaleby <74084485+DanielKaleby@users.noreply.github.com> Date: Thu, 14 May 2026 11:19:43 -0300 Subject: [PATCH 025/201] feat(cosmic-config): add `new_data` method. --- cosmic-config/src/lib.rs | 34 ++++++++++++++++++++++++++++++++++ examples/config/src/main.rs | 3 +++ 2 files changed, 37 insertions(+) diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 7ca9b907..2af498ff 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -51,6 +51,22 @@ fn get_state_dir() -> Option { dirs::state_dir() } +/// 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")] @@ -263,6 +279,24 @@ impl Config { }) } + /// Get data for the given application name and config version. + pub fn new_data(name: &str, version: u64) -> 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), + }) + } + // 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 f6fb5c0d..dfcc5b99 100644 --- a/examples/config/src/main.rs +++ b/examples/config/src/main.rs @@ -88,4 +88,7 @@ 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 5782cb927ede98978f8f76fec2cf0867ff19166b Mon Sep 17 00:00:00 2001 From: Andrei Ivanou Date: Sun, 17 May 2026 11:08:40 +0200 Subject: [PATCH 026/201] fix(process): safely exit forked child after spawn Co-Authored-By: Michael Murphy --- src/process.rs | 23 ++++++++++++++++------- 1 file changed, 16 insertions(+), 7 deletions(-) diff --git a/src/process.rs b/src/process.rs index 2b6c4e0e..e2cb0f70 100644 --- a/src/process.rs +++ b/src/process.rs @@ -5,7 +5,7 @@ use smol::io::AsyncReadExt; use std::io; use std::os::fd::OwnedFd; -use std::process::{Command, Stdio, exit}; +use std::process::{Command, Stdio}; #[cfg(feature = "tokio")] use tokio::io::AsyncReadExt; @@ -56,24 +56,33 @@ pub async fn spawn(mut command: Command) -> Option { match unsafe { libc::fork() } { // Parent process - 1.. => { + fork_pid @ 1..=i32::MAX => { // Drop copy of write end, then read PID from pipe drop(write); let pid = read_from_pipe(read).await; - // wait to prevent zombie - _ = rustix::process::wait(rustix::process::WaitOptions::empty()); + _ = rustix::process::waitpid( + rustix::process::Pid::from_raw(fork_pid), + rustix::process::WaitOptions::empty(), + ); pid } // Child process 0 => { let _res = rustix::process::setsid(); - if let Ok(child) = command.spawn() { + let exit_status = if let Ok(child) = command.spawn() { // Write PID to pipe let _ = rustix::io::write(write, &child.id().to_be_bytes()); - } + 0 + } else { + 1 + }; - exit(0) + // # Safety + // Required for child fork to exit without affecting the parent. + unsafe { + libc::_exit(exit_status); + } } ..=-1 => { From 6f863f19052f1ac576d1a9bef2897d14b29b3f7d Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 18 May 2026 15:05:11 +0200 Subject: [PATCH 027/201] chore: cargo +nightly fmt --- src/widget/menu/key_bind.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/widget/menu/key_bind.rs b/src/widget/menu/key_bind.rs index d6e40e07..ef87b344 100644 --- a/src/widget/menu/key_bind.rs +++ b/src/widget/menu/key_bind.rs @@ -44,7 +44,12 @@ impl KeyBind { /// # Returns /// /// * `bool` - `true` if the key and modifiers match the `KeyBind`, `false` otherwise. - pub fn matches(&self, modifiers: Modifiers, key: &Key, physical_key: Option<&Physical>) -> bool { + pub fn matches( + &self, + modifiers: Modifiers, + key: &Key, + physical_key: Option<&Physical>, + ) -> bool { let key_eq = self.key_eq(key) || physical_key .and_then(physical_key_to_latin) @@ -132,4 +137,4 @@ impl fmt::Display for KeyBind { other => write!(f, "{:?}", other), } } -} \ No newline at end of file +} From 466e25d9f8c08e76215b9435fc5badc4e0e98e9a 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, 19 May 2026 15:01:44 +0200 Subject: [PATCH 028/201] feat(progress_bar): linear progress markers --- examples/application/src/main.rs | 4 +- src/widget/progress_bar/linear.rs | 169 +++++++++++++++++++++++++----- 2 files changed, 143 insertions(+), 30 deletions(-) diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 9f89061b..05841f5b 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -240,7 +240,9 @@ impl cosmic::Application for App { widget::progress_bar::linear::Linear::new() .girth(10.0) .progress(self.progress) - .width(Length::Fill), + .width(Length::Fill) + .markers([0.25, 0.5, 0.75]) + .segment_spacing(2), ) .push( widget::progress_bar::circular::Circular::new() diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index 7672579e..0ebe402f 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -3,7 +3,7 @@ use super::animation::{Animation, Progress}; use super::style::StyleSheet; use iced::advanced::widget::tree::{self, Tree}; use iced::advanced::{self, Clipboard, Layout, Shell, Widget, layout, renderer}; -use iced::{Background, Element, Event, Length, Rectangle, Size, mouse, window}; +use iced::{Background, Element, Event, Length, Pixels, Rectangle, Size, mouse, window}; use std::time::Duration; @@ -21,6 +21,8 @@ where cycle_duration: Duration, period: Duration, progress: Option, + markers: Vec, + segment_spacing: f32, } impl Linear @@ -36,6 +38,8 @@ where cycle_duration: Duration::from_millis(1500), period: Duration::from_secs(2), progress: None, + markers: Vec::new(), + segment_spacing: 0.0, } } @@ -75,6 +79,26 @@ where self.progress = Some(progress.clamp(0.0, 1.0)); self } + + /// Sets the markers of a determinate progress bar, which divide the bar into segments. + /// Each value is a progress fraction between `0.0` and `1.0 at which a visual gap is inserted. + pub fn markers(mut self, markers: impl Into>) -> Self { + let mut markers = markers.into(); + for bp in &mut markers { + *bp = bp.clamp(0.0, 1.0); + } + markers.sort_by(f32::total_cmp); + markers.dedup(); + + self.markers = markers; + self + } + + /// Sets the spacing between segments at each marker. + pub fn segment_spacing(mut self, spacing: impl Into) -> Self { + self.segment_spacing = spacing.into().0; + self + } } impl Default for Linear @@ -165,26 +189,17 @@ where let custom_style = theme.appearance(&self.style, self.progress.is_some(), false); let state = tree.state.downcast_ref::(); - renderer.fill_quad( - renderer::Quad { - bounds, - 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), - ); + let border_width = if custom_style.border_color.is_some() { + 1.0 + } else { + 0.0 + }; + let border_color = custom_style.border_color.unwrap_or(custom_style.bar_color); + let radius = custom_style.border_radius; - let mut draw_segment = |x: f32, width: f32| { - if width > 0.001 { + let mut draw_quad = |x: f32, width: f32, color: iced::Color, border: iced::Border| { + // don't draw if width is less than 0.1 pixels + if width * bounds.width > 0.1 { renderer.fill_quad( renderer::Quad { bounds: Rectangle { @@ -193,22 +208,102 @@ where width: width * bounds.width, height: bounds.height, }, - border: iced::Border { - width: 0.0, - color: iced::Color::TRANSPARENT, - radius: custom_style.border_radius.into(), - }, + border, snap: true, ..renderer::Quad::default() }, - Background::Color(custom_style.bar_color), + Background::Color(color), ); } }; if self.progress.is_some() { - draw_segment(0.0, state.progress.current); + let spacing = self.segment_spacing.max(1.0); + let radius_inner = radius.min(spacing); + + let gap = if self.markers.is_empty() { + 0.0 + } else { + spacing / bounds.width + }; + let drawable = 1.0 - gap * self.markers.len() as f32; + let num_segments = self.markers.len() + 1; + + let segment_bounds = |i: usize| { + let seg_lo = if i == 0 { 0.0 } else { self.markers[i - 1] }; + let seg_hi = if i == num_segments - 1 { + 1.0 + } else { + self.markers[i] + }; + (seg_lo, seg_hi) + }; + let get_radius = |i: usize| { + let r_left = if i == 0 { radius } else { radius_inner }; + let r_right = if i == num_segments - 1 { + radius + } else { + radius_inner + }; + [r_left, r_right, r_right, r_left].into() + }; + + // draw track segments + for i in 0..num_segments { + let (seg_lo, seg_hi) = segment_bounds(i); + let x_start = seg_lo * drawable + i as f32 * gap; + let x_width = (seg_hi - seg_lo) * drawable; + + draw_quad( + x_start, + x_width, + custom_style.track_color, + iced::Border { + width: border_width, + color: border_color, + radius: get_radius(i), + }, + ); + } + + // draw bar segments + let current_p = state.progress.current; + for i in 0..num_segments { + let (seg_lo, seg_hi) = segment_bounds(i); + + // don't iterate over non-filled segments + if current_p < seg_lo { + break; + } + + let x_start = seg_lo * drawable + i as f32 * gap; + let x_width = (seg_hi - seg_lo) * drawable; + let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).clamp(0.0, 1.0); + + draw_quad( + x_start, + x_width * fill, + custom_style.bar_color, + iced::Border { + radius: get_radius(i), + ..iced::Border::default() + }, + ); + } } else { + // draw track + draw_quad( + 0.0, + 1.0, + custom_style.track_color, + iced::Border { + width: border_width, + color: border_color, + radius: radius.into(), + }, + ); + + // draw bar let (bar_start, bar_end) = state .animation @@ -218,8 +313,24 @@ where let right_width = (1.0 - start).min(length); let left_width = length - right_width; - draw_segment(start, right_width); - draw_segment(0.0, left_width); + draw_quad( + start, + right_width, + custom_style.bar_color, + iced::Border { + radius: radius.into(), + ..iced::Border::default() + }, + ); + draw_quad( + 0.0, + left_width, + custom_style.bar_color, + iced::Border { + radius: radius.into(), + ..iced::Border::default() + }, + ); } } } From 147306d581722456ec240bf9962a6fc4a5ab566f Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 20 May 2026 00:30:52 -0600 Subject: [PATCH 029/201] chore: update iced --- iced | 2 +- src/theme/style/iced.rs | 11 +++++++++-- src/widget/menu/menu_tree.rs | 1 + 3 files changed, 11 insertions(+), 3 deletions(-) diff --git a/iced b/iced index 1444d5c4..251254ad 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 1444d5c43e8b103ca8077555606443ab77e03ce5 +Subproject commit 251254ad5179f05b849e420425b72d4c40091db1 diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 1761f6a5..131538e4 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -1341,9 +1341,16 @@ impl iced_widget::text::Catalog for Theme { match class { Text::Accent => iced_widget::text::Style { color: Some(self.cosmic().accent_text_color().into()), + ..Default::default() + }, + Text::Default => iced_widget::text::Style { + color: None, + ..Default::default() + }, + Text::Color(c) => iced_widget::text::Style { + color: Some(*c), + ..Default::default() }, - Text::Default => iced_widget::text::Style { color: None }, - Text::Color(c) => iced_widget::text::Style { color: Some(*c) }, Text::Custom(f) => f(self), } } diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index 41cf1dff..efcf9800 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -234,6 +234,7 @@ pub fn menu_items< color.alpha *= 0.75; TextStyle { color: Some(color.into()), + ..Default::default() } } let key_class = theme::Text::Custom(key_style); From b279d82240fc74710187043b8ee6b9cfa2469d2f Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 20 May 2026 00:32:14 -0600 Subject: [PATCH 030/201] fix: circular progressbar crashes on very small windows --- src/widget/progress_bar/circular.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs index 2123d2e6..3a3fedc4 100644 --- a/src/widget/progress_bar/circular.rs +++ b/src/widget/progress_bar/circular.rs @@ -177,6 +177,9 @@ where let geometry = state.cache.draw(renderer, bounds.size(), |frame| { let track_radius = frame.width() / 2.0 - self.bar_height; + if track_radius <= 0.0 { + return; + } let track_path = canvas::Path::circle(frame.center(), track_radius); frame.stroke( From e2b172e0bc8c38c1a499d761d9abaaa9d921bca0 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 20 May 2026 16:06:07 +0200 Subject: [PATCH 031/201] feat(iced): X11 resize fix, f16 shader compat, & selectable text feature --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 251254ad..b007caba 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 251254ad5179f05b849e420425b72d4c40091db1 +Subproject commit b007caba44b0be1d282edbe1cd6fcd440ef06eab From 2f022280f2f974a567d33d5921f96bbf5fa0f9b4 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Wed, 20 May 2026 17:04:07 +0200 Subject: [PATCH 032/201] docs: fix cargo doc generation --- Cargo.toml | 2 +- iced | 2 +- src/lib.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index de87a395..3d42ed05 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,7 +127,7 @@ ashpd = { version = "0.12.3", default-features = false, optional = true } async-fs = { version = "2.2", optional = true } async-std = { workspace = true, optional = true } auto_enums = "0.8.8" -cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "160b086", optional = true } +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "c253ec1", 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 b007caba..98fb6a0f 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit b007caba44b0be1d282edbe1cd6fcd440ef06eab +Subproject commit 98fb6a0fba98cfdb6c04f33ccf0be6abad7fcfff diff --git a/src/lib.rs b/src/lib.rs index 02623799..875d9148 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -3,7 +3,7 @@ #![allow(clippy::module_name_repetitions)] #![cfg_attr(target_os = "redox", feature(lazy_cell))] -#![cfg_attr(docsrs, feature(doc_auto_cfg))] +#![cfg_attr(docsrs, feature(doc_cfg))] /// Recommended default imports. pub mod prelude { From 113e0ae1f9f4faf7c7bd38a8b1e55dae1c282078 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 22 May 2026 01:21:03 +0200 Subject: [PATCH 033/201] chore(applet): gracefully exit with log when panel exits This was causing confusion for users that thought this symptom may be the cause of a problem. --- src/applet/token/wayland_handler.rs | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/applet/token/wayland_handler.rs b/src/applet/token/wayland_handler.rs index 4ab73235..3bbaf573 100644 --- a/src/applet/token/wayland_handler.rs +++ b/src/applet/token/wayland_handler.rs @@ -164,7 +164,18 @@ pub(crate) fn wayland_handler( if app_data.exit { break; } - event_loop.dispatch(None, &mut app_data).unwrap(); + + if let Err(why) = event_loop.dispatch(None, &mut app_data) { + if let calloop::Error::IoError(ref why) = why + && why.kind() == std::io::ErrorKind::BrokenPipe + { + tracing::info!("Connection to panel has ended. The applet will now exit with it."); + break; + } + + tracing::error!(?why, "dispatch error on Wayland connection to panel"); + break; + } } } From 8fa6a01d049905ce99eb9adf8415b2fa73611818 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 18 May 2026 13:49:14 -0600 Subject: [PATCH 034/201] feat: add ability to provide extra paths to search for icons --- src/widget/icon/named.rs | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/src/widget/icon/named.rs b/src/widget/icon/named.rs index c6bec753..89050301 100644 --- a/src/widget/icon/named.rs +++ b/src/widget/icon/named.rs @@ -39,6 +39,10 @@ pub struct Named { /// Prioritizes SVG over PNG pub prefer_svg: bool, + + /// Extra directories to search as flat paths before the icon theme chain. + #[setters(skip)] + pub extra_paths: Vec, } impl Named { @@ -52,19 +56,30 @@ impl Named { size: None, scale: None, prefer_svg: symbolic, + extra_paths: Vec::new(), } } + pub fn with_extra_paths(mut self, paths: Vec) -> Self { + self.extra_paths = paths; + self + } + #[cfg(all(unix, not(target_os = "macos")))] #[must_use] pub fn path(self) -> Option { let name = &*self.name; let fallback = &self.fallback; + let extra_paths = &self.extra_paths; let locate = |theme: &str, name| { let mut lookup = freedesktop_icons::lookup(name) .with_theme(theme.as_ref()) .with_cache(); + if !extra_paths.is_empty() { + lookup = lookup.with_extra_paths(extra_paths); + } + if let Some(scale) = self.scale { lookup = lookup.with_scale(scale); } From 8379ae7b0b6541310a7ae7212e59714a0c31e49a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 22 May 2026 15:43:29 -0400 Subject: [PATCH 035/201] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 98fb6a0f..6105d8bd 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 98fb6a0fba98cfdb6c04f33ccf0be6abad7fcfff +Subproject commit 6105d8bdef8bcd39fbca1ef168d82c986dd18d20 From 59bebbdffeb8ed4abd3f2577c099745b9a2f2c37 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Wed, 27 May 2026 18:23:26 -0400 Subject: [PATCH 036/201] fix(iced): context menu items are not highlighted on hover in Wayland popups --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 6105d8bd..9c748f16 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 6105d8bdef8bcd39fbca1ef168d82c986dd18d20 +Subproject commit 9c748f1656bc15d159651eea0091c13c024ebc47 From 04a93d304cc7eb9d4c5989c96a230bfb5db80d95 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, 28 May 2026 23:09:23 +0200 Subject: [PATCH 037/201] chore(linear): cleanup --- src/widget/progress_bar/linear.rs | 115 +++++++++++------------------- 1 file changed, 42 insertions(+), 73 deletions(-) diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index 0ebe402f..3ca63d5d 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -3,7 +3,7 @@ use super::animation::{Animation, Progress}; use super::style::StyleSheet; use iced::advanced::widget::tree::{self, Tree}; use iced::advanced::{self, Clipboard, Layout, Shell, Widget, layout, renderer}; -use iced::{Background, Element, Event, Length, Pixels, Rectangle, Size, mouse, window}; +use iced::{Element, Event, Length, Pixels, Rectangle, Size, mouse, window}; use std::time::Duration; @@ -39,7 +39,7 @@ where period: Duration::from_secs(2), progress: None, markers: Vec::new(), - segment_spacing: 0.0, + segment_spacing: 1.0, } } @@ -81,11 +81,11 @@ where } /// Sets the markers of a determinate progress bar, which divide the bar into segments. - /// Each value is a progress fraction between `0.0` and `1.0 at which a visual gap is inserted. + /// Each marker is a value between `0.0` and `1.0` that defines the position of a visual gap. pub fn markers(mut self, markers: impl Into>) -> Self { let mut markers = markers.into(); - for bp in &mut markers { - *bp = bp.clamp(0.0, 1.0); + for marker in &mut markers { + *marker = marker.clamp(0.0, 1.0); } markers.sort_by(f32::total_cmp); markers.dedup(); @@ -96,7 +96,7 @@ where /// Sets the spacing between segments at each marker. pub fn segment_spacing(mut self, spacing: impl Into) -> Self { - self.segment_spacing = spacing.into().0; + self.segment_spacing = spacing.into().0.max(1.0); self } } @@ -212,48 +212,40 @@ where snap: true, ..renderer::Quad::default() }, - Background::Color(color), + color, ); } }; if self.progress.is_some() { - let spacing = self.segment_spacing.max(1.0); + let current_p = state.progress.current; + let len = self.markers.len(); + let spacing = self.segment_spacing; let radius_inner = radius.min(spacing); - let gap = if self.markers.is_empty() { - 0.0 - } else { + let gap = if len != 0 { spacing / bounds.width + } else { + 0.0 }; - let drawable = 1.0 - gap * self.markers.len() as f32; - let num_segments = self.markers.len() + 1; + let drawable = 1.0 - gap * len as f32; - let segment_bounds = |i: usize| { - let seg_lo = if i == 0 { 0.0 } else { self.markers[i - 1] }; - let seg_hi = if i == num_segments - 1 { - 1.0 + for i in 0..=len { + let (seg_lo, r_left) = if i == 0 { + (0.0, radius) } else { - self.markers[i] + (self.markers[i - 1], radius_inner) }; - (seg_lo, seg_hi) - }; - let get_radius = |i: usize| { - let r_left = if i == 0 { radius } else { radius_inner }; - let r_right = if i == num_segments - 1 { - radius + let (seg_hi, r_right) = if i == len { + (1.0, radius) } else { - radius_inner + (self.markers[i], radius_inner) }; - [r_left, r_right, r_right, r_left].into() - }; - - // draw track segments - for i in 0..num_segments { - let (seg_lo, seg_hi) = segment_bounds(i); let x_start = seg_lo * drawable + i as f32 * gap; let x_width = (seg_hi - seg_lo) * drawable; + let segment_radius = [r_left, r_right, r_right, r_left].into(); + // draw track segment draw_quad( x_start, x_width, @@ -261,34 +253,23 @@ where iced::Border { width: border_width, color: border_color, - radius: get_radius(i), + radius: segment_radius, }, ); - } - // draw bar segments - let current_p = state.progress.current; - for i in 0..num_segments { - let (seg_lo, seg_hi) = segment_bounds(i); - - // don't iterate over non-filled segments - if current_p < seg_lo { - break; + // draw bar segment + if current_p > seg_lo { + let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).clamp(0.0, 1.0); + draw_quad( + x_start, + x_width * fill, + custom_style.bar_color, + iced::Border { + radius: segment_radius, + ..iced::Border::default() + }, + ); } - - let x_start = seg_lo * drawable + i as f32 * gap; - let x_width = (seg_hi - seg_lo) * drawable; - let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).clamp(0.0, 1.0); - - draw_quad( - x_start, - x_width * fill, - custom_style.bar_color, - iced::Border { - radius: get_radius(i), - ..iced::Border::default() - }, - ); } } else { // draw track @@ -312,25 +293,13 @@ where let start = bar_start % 1.0; let right_width = (1.0 - start).min(length); let left_width = length - right_width; + let border = iced::Border { + radius: radius.into(), + ..iced::Border::default() + }; - draw_quad( - start, - right_width, - custom_style.bar_color, - iced::Border { - radius: radius.into(), - ..iced::Border::default() - }, - ); - draw_quad( - 0.0, - left_width, - custom_style.bar_color, - iced::Border { - radius: radius.into(), - ..iced::Border::default() - }, - ); + draw_quad(start, right_width, custom_style.bar_color, border); + draw_quad(0.0, left_width, custom_style.bar_color, border); } } } From c9255fe871a751a2f38ea8735e4301dfb9f6bbe5 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, 29 May 2026 17:43:55 +0200 Subject: [PATCH 038/201] feat(progress_bar): draw circular line caps directly & fix gaps between the bar and caps --- src/widget/progress_bar/circular.rs | 53 ++++++++--------------------- src/widget/progress_bar/linear.rs | 2 +- 2 files changed, 16 insertions(+), 39 deletions(-) diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs index 3a3fedc4..3318d7a3 100644 --- a/src/widget/progress_bar/circular.rs +++ b/src/widget/progress_bar/circular.rs @@ -9,7 +9,8 @@ use iced::{Element, Event, Length, Radians, Rectangle, Renderer, Size, Vector, m use std::f32::consts::PI; use std::time::Duration; -const MIN_ANGLE: Radians = Radians(PI / 8.0); +const MIN_GAP_ANGLE: Radians = Radians(PI / 4.0); +const MAX_WRAP: f32 = 1.0 - MIN_GAP_ANGLE.0 / (2.0 * PI); #[must_use] pub struct Circular @@ -76,12 +77,6 @@ where self.progress = Some(progress.clamp(0.0, 1.0)); self } - - fn min_wrap(&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), 1.0 - gap / PI) - } } impl Default for Circular @@ -148,11 +143,12 @@ where shell.request_redraw(); } } else { - let (_, wrap) = self.min_wrap(self.size / 2.0 - self.bar_height); - state.animation = - state - .animation - .timed_transition(self.cycle_duration, self.period, wrap, *now); + state.animation = state.animation.timed_transition( + self.cycle_duration, + self.period, + MAX_WRAP, + *now, + ); state.cache.clear(); shell.request_redraw(); } @@ -191,25 +187,6 @@ where // Converts a track fraction to an angle in radians, with 0 being top of circle let to_angle = |t: f32| t * 2.0 * PI - PI / 2.0; - - let draw_cap = |frame: &mut canvas::Frame, t: f32, flip: bool| { - let angle = to_angle(t); - let center = frame.center() + Vector::new(angle.cos(), angle.sin()) * track_radius; - let (start_angle, end_angle) = if flip { - (angle - PI, angle) - } else { - (angle, angle + PI) - }; - let mut builder = canvas::path::Builder::new(); - builder.arc(canvas::path::Arc { - center, - radius: self.bar_height / 2.0, - start_angle: Radians(start_angle), - end_angle: Radians(end_angle), - }); - frame.fill(&builder.build(), custom_style.bar_color); - }; - let draw_bar = |frame: &mut canvas::Frame, start: f32, end: f32| { let mut builder = canvas::path::Builder::new(); builder.arc(canvas::path::Arc { @@ -222,10 +199,9 @@ where &builder.build(), canvas::Stroke::default() .with_color(custom_style.bar_color) - .with_width(self.bar_height), + .with_width(self.bar_height) + .with_line_cap(canvas::LineCap::Round), ); - draw_cap(frame, end, false); - draw_cap(frame, start, true); }; if self.progress.is_some() { @@ -243,10 +219,11 @@ where } draw_bar(frame, 0.0, state.progress.current); } else { - let (min, wrap) = self.min_wrap(track_radius); - let (start, end) = state - .animation - .bar_positions(self.cycle_duration, min, wrap); + // f32::EPSILON prevents flicker when wrap angle is 0.0 + let (start, end) = + state + .animation + .bar_positions(self.cycle_duration, f32::EPSILON, MAX_WRAP); draw_bar(frame, start, end); } }); diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index 3ca63d5d..231e30d3 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -259,7 +259,7 @@ where // draw bar segment if current_p > seg_lo { - let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).clamp(0.0, 1.0); + let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).min(1.0); draw_quad( x_start, x_width * fill, From fe7fd5b26a6fa7d35998cab0f8db8e55ef305ef8 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Fri, 29 May 2026 20:00:45 +0200 Subject: [PATCH 039/201] perf(iced): reuse swash cache across draws --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 9c748f16..590e0930 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 9c748f1656bc15d159651eea0091c13c024ebc47 +Subproject commit 590e093086247dfc0b41580f752a4d59d5817a78 From 4178b149e69ef1ae871bd06dfb0259e763b9fad3 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Fri, 29 May 2026 16:48:59 -0400 Subject: [PATCH 040/201] fix(iced): revert swash cache change --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 590e0930..ebb3b125 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 590e093086247dfc0b41580f752a4d59d5817a78 +Subproject commit ebb3b125bfc829617411f831afada122b6b4d02c From 9f71679cef9176e38837475ebbf0366013b12f3b Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Sat, 30 May 2026 03:01:43 +0200 Subject: [PATCH 041/201] perf(iced): reuse cached cosmic-text fonts for svg renderer --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index ebb3b125..5013f639 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit ebb3b125bfc829617411f831afada122b6b4d02c +Subproject commit 5013f63956d9797571cfcebba6778adf9c8c3d87 From 758c13723fc35b1d82d7fa4f35859c9f7f3c1b0c Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Sat, 30 May 2026 19:00:54 +0200 Subject: [PATCH 042/201] doc: add crate level documentation --- justfile | 6 ++++ src/lib.rs | 83 ++++++++++++++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 89 insertions(+) diff --git a/justfile b/justfile index 4653434e..7d9aecee 100644 --- a/justfile +++ b/justfile @@ -23,6 +23,12 @@ check-json: (check '--message-format=json') clean: cargo clean +# Generate documentation +doc: + env RUSTDOCFLAGS="--cfg docsrs" cargo +nightly-2026-04-27 doc --no-deps --verbose \ + -p cosmic-client-toolkit -p cosmic-protocols -p libcosmic \ + --features tokio,winit,wayland,desktop,single-instance,applet,xdg-portal,multi-window + # Also remove .cargo and vendored dependencies clean-dist: clean rm -rf .cargo vendor vendor.tar target diff --git a/src/lib.rs b/src/lib.rs index 875d9148..c1646e23 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -5,6 +5,89 @@ #![cfg_attr(target_os = "redox", feature(lazy_cell))] #![cfg_attr(docsrs, feature(doc_cfg))] +//! # The COSMIC Toolkit +//! +//! Quickly code your project with modular elements created with customization in mind. +//! Panels, applets, theming, tiling, launcher, app library, keyboard shortcuts and +//! dynamic or pinned workspaces are all made flexible to bend to your users needs. +//! +//! [COSMIC](https://system76.com/cosmic) empowers frictionless development by being +//! approachable, easy to maintain, and modular. Use the same language and toolkit to +//! build apps and applets, with helpful templates provided. Even shell components and +//! the compositor use the same toolkit. Learn once and use your knowledge anywhere in +//! the desktop. +//! +//! ## Architecture +//! +//! Based on [iced](https://iced.rs/), COSMIC apps and applets are modeled with the +//! MVU (Model-View-Update) pattern from [The Elm Architecture](https://guide.elm-lang.org/architecture/). +//! For more details, see the [architecture page of the iced book](https://book.iced.rs/architecture.html). +//! +//! An application will consist of: +//! +//! * An application **model** for holding persistent application state which implements +//! the [Application] trait. +//! * A [view](Application::view) function which borrows data from the model to construct +//! a view with stateless widgets +//! * An [update](Application::update) function which receives application messages from +//! widgets, tasks, and subscriptions. +//! +//! Messages handled by the update function are used to update the +//! application model and spawn background tasks that can emit messages back to the +//! app's update function. +//! +//! ### Tasks +//! +//! Tasks returned by the update function are scheduled for concurrent execution on a +//! background thread managed by the application's async executor, which is tokio by default. +//! They can be constructed from futures and may also stream events back to the application +//! asynchronously. +//! +//! ### Subscriptions +//! +//! Applications may also use a [subscription](Application::subscription) function to subscribe to external +//! asynchronous event streams. These can run perpetually from application start; or +//! optionally started, stopped, and restarted based on changes to the application model +//! between updates. Such as: +//! +//! * Conditionally starting and stopping a subscription based on the state of a boolean +//! value or enum +//! * Restarting a subscription when the hash of its borrowed data changes +//! * Dynamically spawning a subscription for each item in a list. +//! +//! ## Templates +//! +//! Get started using [cargo-generate](https://github.com/cargo-generate/cargo-generate) +//! with one of the following templates. The app template is for developing desktop +//! applications and the applet template is for developing COSMIC applets. +//! +//! - [App Template](https://github.com/pop-os/cosmic-app-template/) +//! - [Applet Template](https://github.com/pop-os/cosmic-applet-template/) +//! +//! ## Widgets +//! +//! Reference the [`widget`] module for available widgets for use in the view function. +//! Widgets are composable and can be configured through chainable builder methods. +//! Compose widgets together to create complex interfaces and higher level widgets. +//! +//! Composed widgets may be managed by their own custom type with its own view and +//! update functions. It is a common pattern to use these functions within the +//! application's own view and update functions. If using a custom type, implement +//! [From] for the [Element] type to have API treat a composed widget the same as a +//! native custom widget. +//! +//! If a widget does not exist for a specific use case, use the [Widget](iced::advanced::Widget) +//! trait to create an advanced custom widget. This can then be used to composed higher +//! level widgets by chaining composable widgets together. +//! +//! ## Core +//! +//! Every application model requires a [cosmic::Core](app::Core). This contains +//! application state which is managed by libcosmic's runtime for its generated +//! interfaces. Such as the context drawner, nav bar, and the headerbar. This can be +//! used by the app to subscribe to configuration changes and to emit events to the +//! libcosmic-managed portion of the application's state and view. + /// Recommended default imports. pub mod prelude { #[cfg(feature = "winit")] From f0f68933f1552857e2165fc0fa953228107bddef Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 1 Jun 2026 14:28:16 -0400 Subject: [PATCH 043/201] improv(key_bind): allow fallback if not ascci graphic or space check if the logical key is within the valid latin key range and use that if so --- src/widget/menu/key_bind.rs | 109 +++++++++++++++++++++++++++++++++++- 1 file changed, 106 insertions(+), 3 deletions(-) diff --git a/src/widget/menu/key_bind.rs b/src/widget/menu/key_bind.rs index ef87b344..b764fde7 100644 --- a/src/widget/menu/key_bind.rs +++ b/src/widget/menu/key_bind.rs @@ -51,9 +51,10 @@ impl KeyBind { physical_key: Option<&Physical>, ) -> bool { let key_eq = self.key_eq(key) - || physical_key - .and_then(physical_key_to_latin) - .is_some_and(|latin| self.key_eq(&latin)); + || (!is_latin_shortcut_key(key) + && physical_key + .and_then(physical_key_to_latin) + .is_some_and(|latin| self.key_eq(&latin))); key_eq && modifiers.logo() == self.modifiers.contains(&Modifier::Super) && modifiers.control() == self.modifiers.contains(&Modifier::Ctrl) @@ -70,6 +71,19 @@ impl KeyBind { } } +fn is_latin_shortcut_key(key: &Key) -> bool { + let Key::Character(s) = key else { + return false; + }; + + let mut chars = s.chars(); + let Some(ch) = chars.next() else { + return false; + }; + + chars.next().is_none() && (ch.is_ascii_graphic() || ch == ' ') +} + /// Converts a physical key code to the corresponding US-layout Latin `Key`. /// /// This mapping is intentionally limited to keys that may produce different @@ -138,3 +152,92 @@ impl fmt::Display for KeyBind { } } } + +#[cfg(test)] +mod test { + use super::*; + + fn bind_ctrl_w() -> KeyBind { + KeyBind { + modifiers: vec![Modifier::Ctrl], + key: Key::Character("w".into()), + } + } + + #[test] + fn ctrl_w() { + assert!(bind_ctrl_w().matches( + Modifiers::CTRL, + &Key::Character("w".into()), + Some(&Physical::Code(Code::KeyW)), + )); + } + + #[test] + fn ctrl_w_no_fallback_to_dvorak_comma() { + assert!(!bind_ctrl_w().matches( + Modifiers::CTRL, + &Key::Character(",".into()), + Some(&Physical::Code(Code::KeyW)), + )); + } + + #[test] + fn non_latin_layout_fallback() { + assert!(bind_ctrl_w().matches( + Modifiers::CTRL, + &Key::Character("ц".into()), + Some(&Physical::Code(Code::KeyW)), + )); + + let bind = KeyBind { + modifiers: vec![Modifier::Ctrl], + key: Key::Character("s".into()), + }; + + assert!(bind.matches( + Modifiers::CTRL, + &Key::Character("ы".into()), + Some(&Physical::Code(Code::KeyS)), + )); + + assert!(!bind.matches( + Modifiers::CTRL, + &Key::Character("ц".into()), + Some(&Physical::Code(Code::KeyQ)), + )); + } + + #[test] + fn ctrl_space() { + let bind = KeyBind { + modifiers: vec![Modifier::Ctrl], + key: Key::Character(" ".into()), + }; + + assert!(bind.matches(Modifiers::CTRL, &Key::Character(" ".into()), None,)); + } + + #[test] + fn ctrl_space_no_fallback() { + assert!(!bind_ctrl_w().matches( + Modifiers::CTRL, + &Key::Character(" ".into()), + Some(&Physical::Code(Code::KeyW)), + )); + } + + #[test] + fn ctrl_a_no_fallback_to_french_azerty_q() { + let bind = KeyBind { + modifiers: vec![Modifier::Ctrl], + key: Key::Character("a".into()), + }; + + assert!(!bind.matches( + Modifiers::CTRL, + &Key::Character("q".into()), + Some(&Physical::Code(Code::KeyA)), + )); + } +} From bb10afd849f1ad172be6b78eeec347d548aa4fd7 Mon Sep 17 00:00:00 2001 From: Ashley Wulber <48420062+wash2@users.noreply.github.com> Date: Mon, 8 Jun 2026 12:26:35 -0400 Subject: [PATCH 044/201] chore(iced): ignore duplicate surfaces and log a warning --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 5013f639..dcf4cf01 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 5013f63956d9797571cfcebba6778adf9c8c3d87 +Subproject commit dcf4cf01943b5a1baf7b7360ab7d604369a5c470 From 87901c2d489341b473f70a79fdcfd945eec7ff55 Mon Sep 17 00:00:00 2001 From: Aadil127 <211808495+Aadil127@users.noreply.github.com> Date: Tue, 9 Jun 2026 14:17:36 +0530 Subject: [PATCH 045/201] Fix progress bar showing incorrectly with small width. --- src/widget/progress_bar/linear.rs | 28 +++++++++++++++++++--------- 1 file changed, 19 insertions(+), 9 deletions(-) diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index 231e30d3..16ed5fb0 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -197,16 +197,24 @@ where let border_color = custom_style.border_color.unwrap_or(custom_style.bar_color); let radius = custom_style.border_radius; - let mut draw_quad = |x: f32, width: f32, color: iced::Color, border: iced::Border| { - // don't draw if width is less than 0.1 pixels - if width * bounds.width > 0.1 { + let mut draw_quad = + |x: f32, width: f32, color: iced::Color, border: iced::Border, marker_segment: bool| { + let height = if width * bounds.width + < border.radius.top_left.max(border.radius.bottom_left) + && !marker_segment + { + (width * bounds.width * 2.0).min(bounds.height) + } else { + bounds.height + }; + renderer.fill_quad( renderer::Quad { bounds: Rectangle { x: bounds.x + x * bounds.width, - y: bounds.y, + y: bounds.y + (bounds.height - height) / 2.0, width: width * bounds.width, - height: bounds.height, + height, }, border, snap: true, @@ -214,8 +222,7 @@ where }, color, ); - } - }; + }; if self.progress.is_some() { let current_p = state.progress.current; @@ -255,6 +262,7 @@ where color: border_color, radius: segment_radius, }, + false, ); // draw bar segment @@ -268,6 +276,7 @@ where radius: segment_radius, ..iced::Border::default() }, + i != 0, ); } } @@ -282,6 +291,7 @@ where color: border_color, radius: radius.into(), }, + false, ); // draw bar @@ -298,8 +308,8 @@ where ..iced::Border::default() }; - draw_quad(start, right_width, custom_style.bar_color, border); - draw_quad(0.0, left_width, custom_style.bar_color, border); + draw_quad(start, right_width, custom_style.bar_color, border, false); + draw_quad(0.0, left_width, custom_style.bar_color, border, false); } } } From dc4e89596a69482c79228aff0bd36cd19c319a8e Mon Sep 17 00:00:00 2001 From: Aadil127 <211808495+Aadil127@users.noreply.github.com> Date: Tue, 9 Jun 2026 16:10:46 +0530 Subject: [PATCH 046/201] Fix progress says consistant for different girth values. --- src/widget/progress_bar/linear.rs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index 16ed5fb0..4c51a713 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -203,7 +203,8 @@ where < border.radius.top_left.max(border.radius.bottom_left) && !marker_segment { - (width * bounds.width * 2.0).min(bounds.height) + (bounds.height - (2.0 * radius).min(bounds.height) + width * bounds.width * 2.0) + .min(bounds.height) } else { bounds.height }; From 0d028c2dd2fe065e2f19915b3a0c7311e6cc7c68 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 9 Jun 2026 10:39:58 -0400 Subject: [PATCH 047/201] chore: update iced --- examples/applet/src/window.rs | 2 -- iced | 2 +- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 57546ab1..8722a2a4 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -123,8 +123,6 @@ impl cosmic::Application for Window { }, Some(Box::new(move |state: &Window| { let content_list = list_column() - .padding(5) - .spacing(0) .add(settings::item( "Example row", cosmic::widget::container( diff --git a/iced b/iced index dcf4cf01..c3c8fcf5 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit dcf4cf01943b5a1baf7b7360ab7d604369a5c470 +Subproject commit c3c8fcf52cbe46c864459e74357dfe7985072a70 From 9b8c48a328ac006951e34f8a697f6c49553c563e Mon Sep 17 00:00:00 2001 From: Aadil127 <211808495+Aadil127@users.noreply.github.com> Date: Wed, 10 Jun 2026 19:38:46 +0530 Subject: [PATCH 048/201] Fix progress bar now has flat egde. --- src/widget/progress_bar/linear.rs | 114 +++++++++++++++++++++--------- 1 file changed, 80 insertions(+), 34 deletions(-) diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index 4c51a713..e978e038 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -197,33 +197,60 @@ where let border_color = custom_style.border_color.unwrap_or(custom_style.bar_color); let radius = custom_style.border_radius; - let mut draw_quad = - |x: f32, width: f32, color: iced::Color, border: iced::Border, marker_segment: bool| { - let height = if width * bounds.width - < border.radius.top_left.max(border.radius.bottom_left) - && !marker_segment - { - (bounds.height - (2.0 * radius).min(bounds.height) + width * bounds.width * 2.0) - .min(bounds.height) + let mut draw_quad = |x: f32, + width: f32, + color: iced::Color, + mut border: iced::Border, + is_track: bool, + total_progress_width: f32| { + let mut height = bounds.height; + if !is_track { + // For progress that is at the end of completion + if total_progress_width > bounds.width - radius { + let border_radius = + radius.min(bounds.height / 2.0) - (bounds.width - total_progress_width); + border.radius.top_right = border_radius; + border.radius.bottom_right = border_radius; } else { - bounds.height - }; + border.radius.top_right = 0.0; + border.radius.bottom_right = 0.0; + } - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + x * bounds.width, - y: bounds.y + (bounds.height - height) / 2.0, - width: width * bounds.width, - height, - }, - border, - snap: true, - ..renderer::Quad::default() + // For indeterminate mode or when progress has just started + if x < radius.min(bounds.height / 2.0) { + let border_radius = radius.min(bounds.height / 2.0) - x; + border.radius.top_left = border_radius; + border.radius.bottom_left = border_radius; + + if total_progress_width < radius.min(bounds.height / 2.0) { + height = + bounds.height - 2.0 * radius.min(bounds.height / 2.0) + total_progress_width * 2.0; + } + } else { + border.radius.top_left = 0.0; + border.radius.bottom_left = 0.0; + } + + if x > bounds.width - radius.min(bounds.height / 2.0) { + height = bounds.height - 2.0 * radius.min(bounds.height / 2.0) + width * 2.0; + } + } + + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + x, + y: bounds.y + (bounds.height - height) / 2.0, + width, + height, }, - color, - ); - }; + border, + snap: true, + ..renderer::Quad::default() + }, + color, + ); + }; if self.progress.is_some() { let current_p = state.progress.current; @@ -238,6 +265,7 @@ where }; let drawable = 1.0 - gap * len as f32; + let mut absolute_width = 0.0; for i in 0..=len { let (seg_lo, r_left) = if i == 0 { (0.0, radius) @@ -255,29 +283,32 @@ where // draw track segment draw_quad( - x_start, - x_width, + x_start * bounds.width, + x_width * bounds.width, custom_style.track_color, iced::Border { width: border_width, color: border_color, radius: segment_radius, }, - false, + true, + bounds.width, ); // draw bar segment if current_p > seg_lo { let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).min(1.0); + absolute_width += x_width * fill; draw_quad( - x_start, - x_width * fill, + x_start * bounds.width, + x_width * fill * bounds.width, custom_style.bar_color, iced::Border { radius: segment_radius, ..iced::Border::default() }, - i != 0, + false, + absolute_width * bounds.width, ); } } @@ -285,14 +316,15 @@ where // draw track draw_quad( 0.0, - 1.0, + bounds.width, custom_style.track_color, iced::Border { width: border_width, color: border_color, radius: radius.into(), }, - false, + true, + bounds.width, ); // draw bar @@ -309,8 +341,22 @@ where ..iced::Border::default() }; - draw_quad(start, right_width, custom_style.bar_color, border, false); - draw_quad(0.0, left_width, custom_style.bar_color, border, false); + draw_quad( + start * bounds.width, + right_width * bounds.width, + custom_style.bar_color, + border, + false, + (right_width + start) * bounds.width, + ); + draw_quad( + 0.0, + left_width * bounds.width, + custom_style.bar_color, + border, + false, + left_width * bounds.width, + ); } } } From 904b7b95b1a521fafd6159b3cbeda1413d41b78e Mon Sep 17 00:00:00 2001 From: Aadil127 <211808495+Aadil127@users.noreply.github.com> Date: Wed, 10 Jun 2026 22:56:56 +0530 Subject: [PATCH 049/201] Fix progress bar containing some width now does not overflow. --- src/widget/progress_bar/linear.rs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index e978e038..0a0a907c 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -223,8 +223,8 @@ where border.radius.bottom_left = border_radius; if total_progress_width < radius.min(bounds.height / 2.0) { - height = - bounds.height - 2.0 * radius.min(bounds.height / 2.0) + total_progress_width * 2.0; + height = bounds.height - 2.0 * radius.min(bounds.height / 2.0) + + total_progress_width * 2.0; } } else { border.radius.top_left = 0.0; @@ -279,7 +279,13 @@ where }; let x_start = seg_lo * drawable + i as f32 * gap; let x_width = (seg_hi - seg_lo) * drawable; - let segment_radius = [r_left, r_right, r_right, r_left].into(); + let mut segment_radius = if i == 0 { + [r_left, 0.0, 0.0, r_left].into() + } else if i == len { + [0.0, r_right, r_right, 0.0].into() + } else { + [0.0, 0.0, 0.0, 0.0].into() + }; // draw track segment draw_quad( @@ -298,7 +304,8 @@ where // draw bar segment if current_p > seg_lo { let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).min(1.0); - absolute_width += x_width * fill; + absolute_width += x_width * fill + gap; + segment_radius = [r_left, r_right, r_right, r_left].into(); draw_quad( x_start * bounds.width, x_width * fill * bounds.width, From bce86dc7c9177027471ce4020381a4804547d65c Mon Sep 17 00:00:00 2001 From: Jeremy Soller Date: Wed, 10 Jun 2026 12:05:15 -0600 Subject: [PATCH 050/201] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index c3c8fcf5..62b90179 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit c3c8fcf52cbe46c864459e74357dfe7985072a70 +Subproject commit 62b90179a298b06ed3e88362e08ef47f8557da27 From 80b370d23062bdc3d55cac06b6b475f35e3c1a26 Mon Sep 17 00:00:00 2001 From: Aadil127 <211808495+Aadil127@users.noreply.github.com> Date: Wed, 10 Jun 2026 23:35:29 +0530 Subject: [PATCH 051/201] Fix bug in progress bar. --- src/widget/progress_bar/linear.rs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index 0a0a907c..b8e92453 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -279,7 +279,10 @@ where }; let x_start = seg_lo * drawable + i as f32 * gap; let x_width = (seg_hi - seg_lo) * drawable; - let mut segment_radius = if i == 0 { + + let mut segment_radius = if i == 0 && len == 0 { + [r_left, r_right, r_right, r_left].into() + } else if i == 0 { [r_left, 0.0, 0.0, r_left].into() } else if i == len { [0.0, r_right, r_right, 0.0].into() @@ -304,7 +307,7 @@ where // draw bar segment if current_p > seg_lo { let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).min(1.0); - absolute_width += x_width * fill + gap; + absolute_width += x_width * fill + if i == 0 { 0.0 } else { gap }; segment_radius = [r_left, r_right, r_right, r_left].into(); draw_quad( x_start * bounds.width, From 4d36a2ee5ff225868e2e4ec0638f3b049791058a Mon Sep 17 00:00:00 2001 From: Aadil127 <211808495+Aadil127@users.noreply.github.com> Date: Mon, 15 Jun 2026 13:16:07 +0530 Subject: [PATCH 052/201] Fix: progress bar overflow on low progress phase. --- src/widget/progress_bar/linear.rs | 228 ++++++++++++++---------------- 1 file changed, 110 insertions(+), 118 deletions(-) diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index b8e92453..941666a2 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -175,6 +175,7 @@ where } } + #[allow(clippy::too_many_lines)] fn draw( &self, tree: &Tree, @@ -197,62 +198,50 @@ where let border_color = custom_style.border_color.unwrap_or(custom_style.bar_color); let radius = custom_style.border_radius; - let mut draw_quad = |x: f32, - width: f32, - color: iced::Color, - mut border: iced::Border, - is_track: bool, - total_progress_width: f32| { - let mut height = bounds.height; - if !is_track { - // For progress that is at the end of completion - if total_progress_width > bounds.width - radius { - let border_radius = - radius.min(bounds.height / 2.0) - (bounds.width - total_progress_width); - border.radius.top_right = border_radius; - border.radius.bottom_right = border_radius; - } else { - border.radius.top_right = 0.0; - border.radius.bottom_right = 0.0; - } - - // For indeterminate mode or when progress has just started - if x < radius.min(bounds.height / 2.0) { - let border_radius = radius.min(bounds.height / 2.0) - x; - border.radius.top_left = border_radius; - border.radius.bottom_left = border_radius; - - if total_progress_width < radius.min(bounds.height / 2.0) { - height = bounds.height - 2.0 * radius.min(bounds.height / 2.0) - + total_progress_width * 2.0; - } - } else { - border.radius.top_left = 0.0; - border.radius.bottom_left = 0.0; - } - - if x > bounds.width - radius.min(bounds.height / 2.0) { - height = bounds.height - 2.0 * radius.min(bounds.height / 2.0) + width * 2.0; - } - } - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + x, - y: bounds.y + (bounds.height - height) / 2.0, - width, - height, - }, - border, - snap: true, - ..renderer::Quad::default() - }, - color, - ); - }; - if self.progress.is_some() { + let mut draw_quad = + |start: f32, segment_width: f32, progress_width: f32, border: iced::Border| { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + start, + y: bounds.y, + width: segment_width, + height: bounds.height, + }, + border, + snap: true, + ..renderer::Quad::default() + }, + custom_style.track_color, + ); + + renderer.with_layer( + Rectangle { + x: bounds.x + start, + y: bounds.y, + width: progress_width, + height: bounds.height, + }, + |renderer| { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + start, + y: bounds.y, + width: segment_width, + height: bounds.height, + }, + border, + snap: true, + ..renderer::Quad::default() + }, + custom_style.bar_color, + ); + }, + ); + }; + let current_p = state.progress.current; let len = self.markers.len(); let spacing = self.segment_spacing; @@ -265,7 +254,6 @@ where }; let drawable = 1.0 - gap * len as f32; - let mut absolute_width = 0.0; for i in 0..=len { let (seg_lo, r_left) = if i == 0 { (0.0, radius) @@ -279,65 +267,21 @@ where }; let x_start = seg_lo * drawable + i as f32 * gap; let x_width = (seg_hi - seg_lo) * drawable; + let segment_radius = [r_left, r_right, r_right, r_left].into(); - let mut segment_radius = if i == 0 && len == 0 { - [r_left, r_right, r_right, r_left].into() - } else if i == 0 { - [r_left, 0.0, 0.0, r_left].into() - } else if i == len { - [0.0, r_right, r_right, 0.0].into() - } else { - [0.0, 0.0, 0.0, 0.0].into() - }; - - // draw track segment + let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).min(1.0); draw_quad( x_start * bounds.width, x_width * bounds.width, - custom_style.track_color, + x_width * fill * bounds.width, iced::Border { width: border_width, color: border_color, radius: segment_radius, }, - true, - bounds.width, ); - - // draw bar segment - if current_p > seg_lo { - let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).min(1.0); - absolute_width += x_width * fill + if i == 0 { 0.0 } else { gap }; - segment_radius = [r_left, r_right, r_right, r_left].into(); - draw_quad( - x_start * bounds.width, - x_width * fill * bounds.width, - custom_style.bar_color, - iced::Border { - radius: segment_radius, - ..iced::Border::default() - }, - false, - absolute_width * bounds.width, - ); - } } } else { - // draw track - draw_quad( - 0.0, - bounds.width, - custom_style.track_color, - iced::Border { - width: border_width, - color: border_color, - radius: radius.into(), - }, - true, - bounds.width, - ); - - // draw bar let (bar_start, bar_end) = state .animation @@ -347,25 +291,73 @@ where let right_width = (1.0 - start).min(length); let left_width = length - right_width; let border = iced::Border { + width: border_width, + color: border_color, radius: radius.into(), - ..iced::Border::default() }; - draw_quad( - start * bounds.width, - right_width * bounds.width, - custom_style.bar_color, - border, - false, - (right_width + start) * bounds.width, + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }, + border, + ..renderer::Quad::default() + }, + custom_style.track_color, ); - draw_quad( - 0.0, - left_width * bounds.width, - custom_style.bar_color, - border, - false, - left_width * bounds.width, + + renderer.with_layer( + Rectangle { + x: bounds.x, + y: bounds.y, + width: left_width * bounds.width, + height: bounds.height, + }, + |renderer| { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }, + border, + snap: true, + ..renderer::Quad::default() + }, + custom_style.bar_color, + ); + }, + ); + + renderer.with_layer( + Rectangle { + x: bounds.x + start * bounds.width, + y: bounds.y, + width: right_width * bounds.width, + height: bounds.height, + }, + |renderer| { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x, + y: bounds.y, + width: bounds.width, + height: bounds.height, + }, + border, + snap: true, + ..renderer::Quad::default() + }, + custom_style.bar_color, + ); + }, ); } } From a991d7bb433f67016c1473e8e4a03691ededd75a Mon Sep 17 00:00:00 2001 From: Aadil127 <211808495+Aadil127@users.noreply.github.com> Date: Mon, 15 Jun 2026 18:02:23 +0530 Subject: [PATCH 053/201] Fix: Removed unnecessary quad draw calls. --- src/widget/progress_bar/linear.rs | 179 ++++++++++++++---------------- 1 file changed, 86 insertions(+), 93 deletions(-) diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index 941666a2..b94540ee 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -198,50 +198,29 @@ where let border_color = custom_style.border_color.unwrap_or(custom_style.bar_color); let radius = custom_style.border_radius; + let draw_quad = |renderer: &mut Renderer, + x: f32, + width: f32, + color: iced::Color, + border: iced::Border| { + renderer.fill_quad( + renderer::Quad { + bounds: Rectangle { + x: bounds.x + x, + y: bounds.y, + width, + height: bounds.height, + }, + border, + snap: true, + ..renderer::Quad::default() + }, + color, + ); + }; + + // determinate progress bar if self.progress.is_some() { - let mut draw_quad = - |start: f32, segment_width: f32, progress_width: f32, border: iced::Border| { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + start, - y: bounds.y, - width: segment_width, - height: bounds.height, - }, - border, - snap: true, - ..renderer::Quad::default() - }, - custom_style.track_color, - ); - - renderer.with_layer( - Rectangle { - x: bounds.x + start, - y: bounds.y, - width: progress_width, - height: bounds.height, - }, - |renderer| { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + start, - y: bounds.y, - width: segment_width, - height: bounds.height, - }, - border, - snap: true, - ..renderer::Quad::default() - }, - custom_style.bar_color, - ); - }, - ); - }; - let current_p = state.progress.current; let len = self.markers.len(); let spacing = self.segment_spacing; @@ -270,18 +249,64 @@ where let segment_radius = [r_left, r_right, r_right, r_left].into(); let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).min(1.0); - draw_quad( - x_start * bounds.width, - x_width * bounds.width, - x_width * fill * bounds.width, - iced::Border { - width: border_width, - color: border_color, - radius: segment_radius, - }, - ); + let border = iced::Border { + width: border_width, + color: border_color, + radius: segment_radius, + }; + + // empty segment + if current_p < seg_lo { + draw_quad( + renderer, + x_start * bounds.width, + x_width * bounds.width, + custom_style.track_color, + border, + ); + } + // filled segment + else if current_p > seg_hi { + draw_quad( + renderer, + x_start * bounds.width, + x_width * bounds.width, + custom_style.bar_color, + border, + ); + } + // partially filled segment + else { + draw_quad( + renderer, + x_start * bounds.width, + x_width * bounds.width, + custom_style.track_color, + border, + ); + + renderer.with_layer( + Rectangle { + x: bounds.x + x_start * bounds.width, + y: bounds.y, + width: x_width * bounds.width * fill, + height: bounds.height, + }, + |renderer| { + draw_quad( + renderer, + x_start * bounds.width, + x_width * bounds.width, + custom_style.bar_color, + border, + ); + }, + ); + } } - } else { + } + // indeterminate progress bar + else { let (bar_start, bar_end) = state .animation @@ -296,18 +321,12 @@ where radius: radius.into(), }; - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: bounds.y, - width: bounds.width, - height: bounds.height, - }, - border, - ..renderer::Quad::default() - }, + draw_quad( + renderer, + 0.0, + bounds.width, custom_style.track_color, + border, ); renderer.with_layer( @@ -318,20 +337,7 @@ where height: bounds.height, }, |renderer| { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: bounds.y, - width: bounds.width, - height: bounds.height, - }, - border, - snap: true, - ..renderer::Quad::default() - }, - custom_style.bar_color, - ); + draw_quad(renderer, 0.0, bounds.width, custom_style.bar_color, border); }, ); @@ -343,20 +349,7 @@ where height: bounds.height, }, |renderer| { - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x, - y: bounds.y, - width: bounds.width, - height: bounds.height, - }, - border, - snap: true, - ..renderer::Quad::default() - }, - custom_style.bar_color, - ); + draw_quad(renderer, 0.0, bounds.width, custom_style.bar_color, border); }, ); } From f79ccb8cba517106d657e962b8ff48297bbeedd9 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, 14 Jun 2026 17:18:36 +0200 Subject: [PATCH 054/201] chore(ci): switch to `actions-rust-lang` --- .github/workflows/ci.yml | 75 ++++++++++++------------------------- .github/workflows/pages.yml | 7 ++-- 2 files changed, 28 insertions(+), 54 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 7897eb01..1e660871 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -6,24 +6,17 @@ on: - master pull_request: - jobs: format: - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v6 - name: Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 with: components: rustfmt - - name: Cargo cache - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry - ~/.cargo/git - key: ${{ runner.os }}-cargo-rust_stable-${{ hashFiles('**/Cargo.toml') }} + rustflags: "" - name: Format run: cargo fmt -- --check @@ -34,38 +27,28 @@ jobs: fail-fast: false matrix: 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 + - --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-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: submodules: recursive - - name: Get date for registry cache - id: date - run: echo "::set-output name=date::$(date +'%Y-%m-%d')" - - name: Cargo registry cache - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry/index - ~/.cargo/registry/cache - ~/.cargo/git - key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }} - restore-keys: ${{ runner.os }}-cargo-registry- - name: System dependencies run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" - name: Test features run: cargo test ${{ matrix.test_args }} -- --test-threads=1 env: @@ -82,28 +65,18 @@ jobs: - "open-dialog" - "context-menu" - "nav-context" - runs-on: ubuntu-22.04 + runs-on: ubuntu-latest steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: submodules: recursive - - name: Get date for registry cache - id: date - run: echo "::set-output name=date::$(date +'%Y-%m-%d')" - - name: Cargo registry cache - uses: actions/cache@v3 - with: - path: | - ~/.cargo/registry/index - ~/.cargo/registry/cache - ~/.cargo/git - key: ${{ runner.os }}-cargo-registry-${{ steps.date.outputs.date }} - restore-keys: ${{ runner.os }}-cargo-registry- - name: System dependencies run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Rust toolchain - uses: dtolnay/rust-toolchain@stable + uses: actions-rust-lang/setup-rust-toolchain@v1 + with: + rustflags: "" - name: Check example run: cargo check -p "${{ matrix.examples }}" env: diff --git a/.github/workflows/pages.yml b/.github/workflows/pages.yml index af5b059b..e51c552f 100644 --- a/.github/workflows/pages.yml +++ b/.github/workflows/pages.yml @@ -11,13 +11,14 @@ jobs: steps: - name: Checkout sources - uses: actions/checkout@v3 + uses: actions/checkout@v6 with: submodules: recursive - name: Install Rust nightly - uses: dtolnay/rust-toolchain@master + uses: actions-rust-lang/setup-rust-toolchain@v1 with: toolchain: nightly-2026-04-27 + rustflags: "" - name: System dependencies run: sudo apt-get update; sudo apt-get install -y libxkbcommon-dev libwayland-dev - name: Build documentation @@ -29,7 +30,7 @@ jobs: -p libcosmic \ --verbose --features tokio,winit,wayland,desktop,single-instance,applet,xdg-portal,multi-window - name: Deploy documentation - uses: peaceiris/actions-gh-pages@v3 + uses: peaceiris/actions-gh-pages@v4 with: github_token: ${{ secrets.GITHUB_TOKEN }} publish_dir: ./target/doc From 8df1858e5d25b49e9d4e79f631ba7322c3fc6bd6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E9=BB=91=E5=AB=96233?= Date: Tue, 16 Jun 2026 23:35:59 +0800 Subject: [PATCH 055/201] feat(list_column): add drag and drop support to widget API --- src/widget/list/list_column.rs | 60 ++++++++++++++++++++++++++-------- 1 file changed, 46 insertions(+), 14 deletions(-) diff --git a/src/widget/list/list_column.rs b/src/widget/list/list_column.rs index 9e4204cc..b4640d34 100644 --- a/src/widget/list/list_column.rs +++ b/src/widget/list/list_column.rs @@ -3,7 +3,7 @@ use crate::widget::container::Catalog; use crate::widget::space::vertical; -use crate::widget::{button, column, container, divider, row}; +use crate::widget::{DndDestination, DndSource, button, column, container, divider, row}; use crate::{Apply, Element, theme}; use iced::{Length, Padding}; @@ -11,14 +11,23 @@ use iced::{Length, Padding}; pub struct ListButton<'a, Message> { content: Element<'a, Message>, on_press: Option, + dnd_source_builder: Option>>, + dnd_destination_builder: Option>>, selected: bool, } +/// Builds a DndSource, wrapping an element +pub type DndSourceBuilder<'a, Message> = dyn FnOnce(Element<'a, Message>) -> DndSource<'a, Message, Box>; +/// Builds a DndDestination, wrapping an element +pub type DndDestinationBuilder<'a, Message> = dyn FnOnce(Element<'a, Message>) -> DndDestination<'a, Message>; + /// 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, + dnd_source_builder: None, + dnd_destination_builder: None, selected: false, } } @@ -38,6 +47,16 @@ impl<'a, Message: 'static> ListButton<'a, Message> { self.selected = selected; self } + + pub fn with_dnd_source(mut self, builder: Box>) -> Self { + self.dnd_source_builder = Some(builder); + self + } + + pub fn with_dnd_destination(mut self, builder: Box>) -> Self { + self.dnd_destination_builder = Some(builder); + self + } } pub enum ListItem<'a, Message> { @@ -175,19 +194,32 @@ impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { 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, - ))), - ), + dnd_source_builder, + dnd_destination_builder, + }) => { + let mut button: Element<'a, Message> = + 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, + ))) + .into(); + if let Some(builder) = dnd_source_builder { + button = builder(button).into(); + } + + if let Some(builder) = dnd_destination_builder { + button = builder(button).into(); + } + + col.push(button) + }, }; } From e7f278d17f0cf8275173ab127a9b47fdd7440d9a Mon Sep 17 00:00:00 2001 From: Hojjat Abdollahi Date: Mon, 22 Jun 2026 13:33:23 -0600 Subject: [PATCH 056/201] fix(text_input): add alt_clipboard bindings --- iced | 2 +- src/widget/list/list_column.rs | 40 +++++++++++++++++++--------------- src/widget/text_input/input.rs | 19 +++++++++++++--- 3 files changed, 40 insertions(+), 21 deletions(-) diff --git a/iced b/iced index 62b90179..7afd9ee0 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 62b90179a298b06ed3e88362e08ef47f8557da27 +Subproject commit 7afd9ee0f83cfd1a4e9f9b56cf0f365e5ae60347 diff --git a/src/widget/list/list_column.rs b/src/widget/list/list_column.rs index b4640d34..41892f69 100644 --- a/src/widget/list/list_column.rs +++ b/src/widget/list/list_column.rs @@ -17,9 +17,13 @@ pub struct ListButton<'a, Message> { } /// Builds a DndSource, wrapping an element -pub type DndSourceBuilder<'a, Message> = dyn FnOnce(Element<'a, Message>) -> DndSource<'a, Message, Box>; +pub type DndSourceBuilder<'a, Message> = + dyn FnOnce( + Element<'a, Message>, + ) -> DndSource<'a, Message, Box>; /// Builds a DndDestination, wrapping an element -pub type DndDestinationBuilder<'a, Message> = dyn FnOnce(Element<'a, Message>) -> DndDestination<'a, Message>; +pub type DndDestinationBuilder<'a, Message> = + dyn FnOnce(Element<'a, Message>) -> DndDestination<'a, Message>; /// Creates a [`ListButton`] with the given content. pub fn button<'a, Message>(content: impl Into>) -> ListButton<'a, Message> { @@ -53,7 +57,10 @@ impl<'a, Message: 'static> ListButton<'a, Message> { self } - pub fn with_dnd_destination(mut self, builder: Box>) -> Self { + pub fn with_dnd_destination( + mut self, + builder: Box>, + ) -> Self { self.dnd_destination_builder = Some(builder); self } @@ -197,19 +204,18 @@ impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { dnd_source_builder, dnd_destination_builder, }) => { - let mut button: Element<'a, Message> = - 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, - ))) - .into(); + let mut button: Element<'a, Message> = 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, + ))) + .into(); if let Some(builder) = dnd_source_builder { button = builder(button).into(); } @@ -219,7 +225,7 @@ impl<'a, Message: Clone + 'static> ListColumn<'a, Message> { } col.push(button) - }, + } }; } diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index ea1122ea..65b84882 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -1821,9 +1821,22 @@ 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/Command+A/C/V/X was pressed. - if state.keyboard_modifiers.command() { - match key.to_latin(*physical_key) { + // Ctrl/Command+A/C/V/X, plus the traditional alternate clipboard + let clip_key = match key.as_ref() { + keyboard::Key::Named(keyboard::key::Named::Insert) if modifiers.shift() => { + Some('v') + } + keyboard::Key::Named(keyboard::key::Named::Insert) if modifiers.command() => { + Some('c') + } + keyboard::Key::Named(keyboard::key::Named::Delete) if modifiers.shift() => { + Some('x') + } + _ if modifiers.command() => key.to_latin(*physical_key), + _ => None, + }; + { + match clip_key { Some('c') => { if !is_secure { if let Some((start, end)) = state.cursor.selection(value) { From 6e116097c00fa085114572d8d2671e7df854ea44 Mon Sep 17 00:00:00 2001 From: "Weblate (bot)" Date: Tue, 23 Jun 2026 17:09:47 +0200 Subject: [PATCH 057/201] i18n: translation updates from weblate (#1318) Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/kmr/ Translation: Pop OS/libcosmic Co-authored-by: CYAXXX <85353920+CYAXXX@users.noreply.github.com> --- i18n/kmr/libcosmic.ftl | 34 ++++++++++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/i18n/kmr/libcosmic.ftl b/i18n/kmr/libcosmic.ftl index e69de29b..a771f86d 100644 --- a/i18n/kmr/libcosmic.ftl +++ b/i18n/kmr/libcosmic.ftl @@ -0,0 +1,34 @@ +february = Reşemî { $year } +close = Bigire +documenters = Pêlbendvan +november = Mijdar { $year } +may = Gulan { $year } +april = Avrêl { $year } +translators = Wergêr +artists = Hunermend +license = Lîsans +december = Berfanbar { $year } +links = Girêdan +march = Adar { $year } +june = Pûşber { $year } +august = Tebax { $year } +developers = Pêşvebir +july = Tîrmeh { $year } +september = Îlon { $year } +designers = Nigarvan +october = Cotmeh { $year } +january = Çile { $year } +monday = Duşem +mon = Duş +tuesday = Sêşem +tue = Sêş +wednesday = Çarşem +wed = Çar +thursday = Pênçşem +thu = Pên +friday = Înî +fri = Înî +saturday = Şemî +sat = Şem +sunday = Yekşem +sun = Yek From 76bc13ff40eec798a0937d87fce20b699688c69a 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, 23 Jun 2026 18:30:28 +0200 Subject: [PATCH 058/201] feat(progress_bar): improve determinate animations Switches to using a critically damped spring animation, which prevents issues with rapid progress updates, and is generally smoother. --- examples/application/src/main.rs | 18 +++++++- src/widget/progress_bar/animation.rs | 33 +++++++------- src/widget/progress_bar/linear.rs | 68 ++++++++++------------------ 3 files changed, 57 insertions(+), 62 deletions(-) diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 05841f5b..2b4999b1 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -84,6 +84,7 @@ pub enum Message { Hi2, Hi3, Tick, + ValueChanged(f32), } /// The [`App`] stores application-specific state. @@ -95,6 +96,7 @@ pub struct App { hidden: bool, keybinds: HashMap, progress: f32, + progress_slider: f32, } /// Implement [`cosmic::Application`] to integrate with COSMIC. @@ -137,6 +139,7 @@ impl cosmic::Application for App { hidden: true, keybinds: HashMap::new(), progress: 0.0, + progress_slider: 0.0, }; let command = app.update_title(); @@ -185,6 +188,9 @@ impl cosmic::Application for App { Message::Tick => { self.progress = (self.progress + 0.01) % 1.0; } + Message::ValueChanged(value) => { + self.progress_slider = value; + } } Task::none() } @@ -201,7 +207,7 @@ impl cosmic::Application for App { .map_or("No page selected", String::as_str); let centered = widget::container( - widget::column::with_capacity(14) + widget::column::with_capacity(16) .push(widget::text::body(page_content)) .push( widget::text_input::text_input("", &self.input_1) @@ -240,6 +246,16 @@ impl cosmic::Application for App { widget::progress_bar::linear::Linear::new() .girth(10.0) .progress(self.progress) + .width(Length::Fill), + ) + .push( + widget::slider(0.0..=1.0, self.progress_slider, Message::ValueChanged) + .step(0.001), + ) + .push( + widget::progress_bar::linear::Linear::new() + .girth(10.0) + .progress(self.progress_slider) .width(Length::Fill) .markers([0.25, 0.5, 0.75]) .segment_spacing(2), diff --git a/src/widget/progress_bar/animation.rs b/src/widget/progress_bar/animation.rs index a9d52831..0be5c0d3 100644 --- a/src/widget/progress_bar/animation.rs +++ b/src/widget/progress_bar/animation.rs @@ -2,45 +2,44 @@ use crate::anim::smootherstep; use iced::time::Instant; use std::time::Duration; -const LAG: f32 = 0.1; +/// Angular frequency of the critically damped spring (snappiness) +const OMEGA: f32 = 15.0; #[derive(Default)] pub struct Progress { pub current: f32, - target: Option, + velocity: f32, last: Option, } impl Progress { - /// Smoothly chases `target` using exponential decay. + /// Chases `target` using a critically damped spring. /// Returns `true` if a redraw should be requested. pub fn update(&mut self, target: f32, now: Instant) -> bool { // Don't animate on start let Some(last) = self.last else { self.current = target; - self.target = Some(target); + self.velocity = 0.0; self.last = Some(now); return false; }; - // Sync animation clock when target changes - if self.target != Some(target) { - self.target = Some(target); - self.last = Some(now); - return true; - } - let dt = (now - last).as_secs_f32(); self.last = Some(now); - let diff = target - self.current; + let displacement = self.current - target; - if diff.abs() > 0.001 { - self.current += diff * (1.0 - (-dt / LAG).exp()); - true - } else { + if displacement.abs() < 0.001 && self.velocity.abs() < 0.001 { self.current = target; - false + self.velocity = 0.0; + return false; } + + let exp = (-OMEGA * dt).exp(); + let coeff = self.velocity + OMEGA * displacement; + self.current = target + (displacement + coeff * dt) * exp; + self.velocity = (self.velocity - OMEGA * coeff * dt) * exp; + + true } } diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index b94540ee..9bd9fa02 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -206,9 +206,9 @@ where renderer.fill_quad( renderer::Quad { bounds: Rectangle { - x: bounds.x + x, + x: bounds.x + x * bounds.width, y: bounds.y, - width, + width: width * bounds.width, height: bounds.height, }, border, @@ -248,7 +248,6 @@ where let x_width = (seg_hi - seg_lo) * drawable; let segment_radius = [r_left, r_right, r_right, r_left].into(); - let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).min(1.0); let border = iced::Border { width: border_width, color: border_color, @@ -257,34 +256,16 @@ where // empty segment if current_p < seg_lo { - draw_quad( - renderer, - x_start * bounds.width, - x_width * bounds.width, - custom_style.track_color, - border, - ); + draw_quad(renderer, x_start, x_width, custom_style.track_color, border); } // filled segment else if current_p > seg_hi { - draw_quad( - renderer, - x_start * bounds.width, - x_width * bounds.width, - custom_style.bar_color, - border, - ); + draw_quad(renderer, x_start, x_width, custom_style.bar_color, border); } // partially filled segment else { - draw_quad( - renderer, - x_start * bounds.width, - x_width * bounds.width, - custom_style.track_color, - border, - ); - + let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).min(1.0); + draw_quad(renderer, x_start, x_width, custom_style.track_color, border); renderer.with_layer( Rectangle { x: bounds.x + x_start * bounds.width, @@ -293,13 +274,7 @@ where height: bounds.height, }, |renderer| { - draw_quad( - renderer, - x_start * bounds.width, - x_width * bounds.width, - custom_style.bar_color, - border, - ); + draw_quad(renderer, x_start, x_width, custom_style.bar_color, border); }, ); } @@ -307,6 +282,20 @@ where } // indeterminate progress bar else { + // draw track + draw_quad( + renderer, + 0.0, + 1.0, + custom_style.track_color, + iced::Border { + width: border_width, + color: border_color, + radius: radius.into(), + }, + ); + + // draw bar let (bar_start, bar_end) = state .animation @@ -316,19 +305,10 @@ where let right_width = (1.0 - start).min(length); let left_width = length - right_width; let border = iced::Border { - width: border_width, - color: border_color, radius: radius.into(), + ..iced::Border::default() }; - draw_quad( - renderer, - 0.0, - bounds.width, - custom_style.track_color, - border, - ); - renderer.with_layer( Rectangle { x: bounds.x, @@ -337,7 +317,7 @@ where height: bounds.height, }, |renderer| { - draw_quad(renderer, 0.0, bounds.width, custom_style.bar_color, border); + draw_quad(renderer, 0.0, 1.0, custom_style.bar_color, border); }, ); @@ -349,7 +329,7 @@ where height: bounds.height, }, |renderer| { - draw_quad(renderer, 0.0, bounds.width, custom_style.bar_color, border); + draw_quad(renderer, 0.0, 1.0, custom_style.bar_color, border); }, ); } From 417923fbafc0f38a63846b307036e4f20fc4960d Mon Sep 17 00:00:00 2001 From: Aadil <211808495+Aadil127@users.noreply.github.com> Date: Wed, 24 Jun 2026 01:43:35 +0530 Subject: [PATCH 059/201] feat(color_picker): apply gradient shifting to hue slider This also removes vertical line gap in the center of slider handle by adding current hue color to handle. --- src/widget/color_picker/mod.rs | 155 ++++++++++++++------------------- 1 file changed, 66 insertions(+), 89 deletions(-) diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 66ffd847..e5aeedfa 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -5,13 +5,11 @@ use std::borrow::Cow; use std::rc::Rc; -use std::sync::LazyLock; use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; use crate::Element; -use crate::theme::iced::Slider; -use crate::theme::{Button, THEME}; +use crate::theme::{Button, THEME, Theme, iced::Slider}; use crate::widget::button::Catalog; use crate::widget::segmented_button::Entity; use crate::widget::{container, slider}; @@ -39,37 +37,6 @@ use super::{Icon, button, segmented_control, text, text_input, tooltip}; #[doc(inline)] pub use ColorPickerModel as Model; -// TODO is this going to look correct enough? -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, - ))) - }) - .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)] @@ -301,23 +268,67 @@ where copy_to_clipboard_label: T, copied_to_clipboard_label: T, ) -> ColorPicker<'a, Message> { - fn rail_backgrounds(hue: f32) -> (Background, Background) { - let low_range = hsv_rainbow(0., hue); - let high_range = hsv_rainbow(hue, 360.); - - ( - 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 color_slider_style = Rc::new(move |t: &Theme| { + let cosmic = t.cosmic(); + let mut a = slider::Catalog::style(t, &Slider::default(), slider::Status::Active); + a.rail.backgrounds = ( + // active track + Background::Gradient(iced::Gradient::Linear( + Linear::new(Radians(90.0)).add_stops((0..8_u8).map(|index| { + let offset; + let hue = self.active_color.hue.into_positive_degrees(); + let new_hue: f32; + if hue <= f32::from(index) * 360.0 / 7.0 { + offset = 1.0; + new_hue = hue; + } else { + offset = (f32::from(index) / 7.0) * (360.0 / hue); + new_hue = f32::from(index) * 360.0 / 7.0; + } + ColorStop { + color: Color::from(palette::Srgba::from_color( + palette::Hsv::new_srgb_const(RgbHue::new(new_hue), 1.0, 1.0), + )), + offset, + } + })), + )), + // inactive track + Background::Gradient(iced::Gradient::Linear( + Linear::new(Radians(90.0)).add_stops((0..8_u8).map(|index| { + let offset; + let hue = self.active_color.hue.into_positive_degrees(); + let new_hue: f32; + if hue >= f32::from(index) * 360.0 / 7.0 { + offset = 0.0; + new_hue = hue; + } else { + offset = + ((f32::from(index) / 7.0) - (hue / 360.0)) / (1.0 - (hue / 360.0)); + new_hue = f32::from(index) * 360.0 / 7.0; + } + ColorStop { + color: Color::from(palette::Srgba::from_color( + palette::Hsv::new_srgb_const(RgbHue::new(new_hue), 1.0, 1.0), + )), + offset, + } + })), + )), + ); + a.rail.width = 8.0; + a.handle.background = Background::Color(Color::from(palette::Srgba::from_color( + palette::Hsv::new_srgb_const(self.active_color.hue, 1.0, 1.0), + ))); + a.handle.shape = HandleShape::Circle { radius: 8.0 }; + a.handle.border_color = cosmic.palette.neutral_10.into(); + a.handle.border_width = 4.0; + a + }); + let mut inner = column![ // segmented buttons segmented_control::horizontal(self.model) @@ -332,56 +343,22 @@ where .width(self.width) .height(self.height), slider( - 0.001..=359.99, + 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)) - } + }, ) .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 - }), + active: color_slider_style.clone(), + hovered: color_slider_style.clone(), + dragging: color_slider_style, }) + .step(4.0 / 17.0) + .shift_step(64.0 / 17.0) .width(self.width), text_input("", self.input_color) .on_input(move |s| on_update(ColorPickerUpdate::Input(s))) From 13fb59851737f5a8906f23f6059fb896bc519abc 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, 26 Jun 2026 17:25:28 +0200 Subject: [PATCH 060/201] chore(progress_bar): simplify linear code --- .github/workflows/ci.yml | 3 +- src/widget/color_picker/mod.rs | 3 +- src/widget/progress_bar/linear.rs | 98 +++++++++++-------------------- 3 files changed, 37 insertions(+), 67 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 1e660871..5a5258f3 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,10 +15,11 @@ jobs: - name: Rust toolchain uses: actions-rust-lang/setup-rust-toolchain@v1 with: + toolchain: nightly components: rustfmt rustflags: "" - name: Format - run: cargo fmt -- --check + run: cargo +nightly fmt -- --check tests: needs: diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index e5aeedfa..4f9249dd 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -9,7 +9,8 @@ use std::sync::atomic::{AtomicBool, Ordering}; use std::time::{Duration, Instant}; use crate::Element; -use crate::theme::{Button, THEME, Theme, iced::Slider}; +use crate::theme::iced::Slider; +use crate::theme::{Button, THEME, Theme}; use crate::widget::button::Catalog; use crate::widget::segmented_button::Entity; use crate::widget::{container, slider}; diff --git a/src/widget/progress_bar/linear.rs b/src/widget/progress_bar/linear.rs index 9bd9fa02..6c06e2f7 100644 --- a/src/widget/progress_bar/linear.rs +++ b/src/widget/progress_bar/linear.rs @@ -3,7 +3,7 @@ use super::animation::{Animation, Progress}; use super::style::StyleSheet; use iced::advanced::widget::tree::{self, Tree}; use iced::advanced::{self, Clipboard, Layout, Shell, Widget, layout, renderer}; -use iced::{Element, Event, Length, Pixels, Rectangle, Size, mouse, window}; +use iced::{Border, Color, Element, Event, Length, Pixels, Rectangle, Size, mouse, window}; use std::time::Duration; @@ -175,7 +175,6 @@ where } } - #[allow(clippy::too_many_lines)] fn draw( &self, tree: &Tree, @@ -197,20 +196,13 @@ where }; let border_color = custom_style.border_color.unwrap_or(custom_style.bar_color); let radius = custom_style.border_radius; + let track_color = custom_style.track_color; + let bar_color = custom_style.bar_color; - let draw_quad = |renderer: &mut Renderer, - x: f32, - width: f32, - color: iced::Color, - border: iced::Border| { + let draw_quad = |renderer: &mut Renderer, rect: Rectangle, border: Border, color: Color| { renderer.fill_quad( renderer::Quad { - bounds: Rectangle { - x: bounds.x + x * bounds.width, - y: bounds.y, - width: width * bounds.width, - height: bounds.height, - }, + bounds: rect, border, snap: true, ..renderer::Quad::default() @@ -218,20 +210,21 @@ where color, ); }; + let to_rect = |x: f32, width: f32| Rectangle { + x: bounds.x + x * bounds.width, + y: bounds.y, + width: width * bounds.width, + height: bounds.height, + }; // determinate progress bar if self.progress.is_some() { let current_p = state.progress.current; let len = self.markers.len(); let spacing = self.segment_spacing; - let radius_inner = radius.min(spacing); - - let gap = if len != 0 { - spacing / bounds.width - } else { - 0.0 - }; + let gap = spacing / bounds.width; let drawable = 1.0 - gap * len as f32; + let radius_inner = radius.min(spacing); for i in 0..=len { let (seg_lo, r_left) = if i == 0 { @@ -246,9 +239,10 @@ where }; let x_start = seg_lo * drawable + i as f32 * gap; let x_width = (seg_hi - seg_lo) * drawable; + let rect = to_rect(x_start, x_width); let segment_radius = [r_left, r_right, r_right, r_left].into(); - let border = iced::Border { + let border = Border { width: border_width, color: border_color, radius: segment_radius, @@ -256,27 +250,19 @@ where // empty segment if current_p < seg_lo { - draw_quad(renderer, x_start, x_width, custom_style.track_color, border); + draw_quad(renderer, rect, border, track_color); } // filled segment else if current_p > seg_hi { - draw_quad(renderer, x_start, x_width, custom_style.bar_color, border); + draw_quad(renderer, rect, border, bar_color); } // partially filled segment else { - let fill = ((current_p - seg_lo) / (seg_hi - seg_lo)).min(1.0); - draw_quad(renderer, x_start, x_width, custom_style.track_color, border); - renderer.with_layer( - Rectangle { - x: bounds.x + x_start * bounds.width, - y: bounds.y, - width: x_width * bounds.width * fill, - height: bounds.height, - }, - |renderer| { - draw_quad(renderer, x_start, x_width, custom_style.bar_color, border); - }, - ); + let fill = (current_p - seg_lo) / (seg_hi - seg_lo); + draw_quad(renderer, rect, border, track_color); + renderer.with_layer(to_rect(x_start, x_width * fill), |renderer| { + draw_quad(renderer, rect, border, bar_color); + }); } } } @@ -285,14 +271,13 @@ where // draw track draw_quad( renderer, - 0.0, - 1.0, - custom_style.track_color, - iced::Border { + bounds, + Border { width: border_width, color: border_color, radius: radius.into(), }, + track_color, ); // draw bar @@ -304,34 +289,17 @@ where let start = bar_start % 1.0; let right_width = (1.0 - start).min(length); let left_width = length - right_width; - let border = iced::Border { + let border = Border { radius: radius.into(), - ..iced::Border::default() + ..Border::default() }; - renderer.with_layer( - Rectangle { - x: bounds.x, - y: bounds.y, - width: left_width * bounds.width, - height: bounds.height, - }, - |renderer| { - draw_quad(renderer, 0.0, 1.0, custom_style.bar_color, border); - }, - ); - - renderer.with_layer( - Rectangle { - x: bounds.x + start * bounds.width, - y: bounds.y, - width: right_width * bounds.width, - height: bounds.height, - }, - |renderer| { - draw_quad(renderer, 0.0, 1.0, custom_style.bar_color, border); - }, - ); + renderer.with_layer(to_rect(start, right_width), |renderer| { + draw_quad(renderer, bounds, border, bar_color); + }); + renderer.with_layer(to_rect(0.0, left_width), |renderer| { + draw_quad(renderer, bounds, border, bar_color); + }); } } } From e7f37f3e817a13b81f63266df98f2528ef608eaf Mon Sep 17 00:00:00 2001 From: Frederic Laing Date: Fri, 26 Jun 2026 16:11:25 +0200 Subject: [PATCH 061/201] fix(popover): forward in-flight pointer events to modal popups so a selection drag started inside still receives its release --- src/widget/popover.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 57e4568a..c822899d 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -402,6 +402,26 @@ where && matches!(event, Event::Mouse(_) | Event::Touch(_)) && !cursor_position.is_over(layout.bounds()) { + // Swallow new presses outside the popup, but still forward other + // events so an interaction started inside it (such as a selection + // drag) receives its release once the cursor leaves the bounds. + let is_press = matches!( + event, + Event::Mouse(mouse::Event::ButtonPressed(_)) + | Event::Touch(touch::Event::FingerPressed { .. }) + ); + if !is_press { + self.content.as_widget_mut().update( + self.tree, + event, + layout, + cursor_position, + renderer, + clipboard, + shell, + &layout.bounds(), + ); + } shell.capture_event(); return; } From 18a7a75e3316b1f4855777af677d989467f2da74 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 26 Jun 2026 14:17:11 -0400 Subject: [PATCH 062/201] fix(key bind): space --- src/widget/menu/key_bind.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/widget/menu/key_bind.rs b/src/widget/menu/key_bind.rs index b764fde7..2202125f 100644 --- a/src/widget/menu/key_bind.rs +++ b/src/widget/menu/key_bind.rs @@ -146,6 +146,7 @@ impl fmt::Display for KeyBind { write!(f, "{:?} + ", modifier)?; } match &self.key { + Key::Character(c) if c.as_str() == " " => write!(f, "Space"), Key::Character(c) => write!(f, "{}", c.to_uppercase()), Key::Named(named) => write!(f, "{:?}", named), other => write!(f, "{:?}", other), @@ -216,6 +217,7 @@ mod test { }; assert!(bind.matches(Modifiers::CTRL, &Key::Character(" ".into()), None,)); + assert!(format!("{}", bind) == String::from("Ctrl + Space")) } #[test] From 18d76659f2aa5509a04da37d2a63a7d6b3116156 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 Jul 2026 11:59:24 +0200 Subject: [PATCH 063/201] fix(progress_bar): prevent large animation time delta --- src/widget/progress_bar/animation.rs | 26 +++++++++----------------- 1 file changed, 9 insertions(+), 17 deletions(-) diff --git a/src/widget/progress_bar/animation.rs b/src/widget/progress_bar/animation.rs index 0be5c0d3..500ba4ac 100644 --- a/src/widget/progress_bar/animation.rs +++ b/src/widget/progress_bar/animation.rs @@ -2,44 +2,36 @@ use crate::anim::smootherstep; use iced::time::Instant; use std::time::Duration; -/// Angular frequency of the critically damped spring (snappiness) -const OMEGA: f32 = 15.0; +const LAG: f32 = 0.1; #[derive(Default)] pub struct Progress { pub current: f32, - velocity: f32, last: Option, } impl Progress { - /// Chases `target` using a critically damped spring. + /// Chases `target` using exponential decay. /// Returns `true` if a redraw should be requested. pub fn update(&mut self, target: f32, now: Instant) -> bool { // Don't animate on start let Some(last) = self.last else { self.current = target; - self.velocity = 0.0; self.last = Some(now); return false; }; - let dt = (now - last).as_secs_f32(); + let dt = (now - last).as_secs_f32().min(1.0 / 60.0); self.last = Some(now); - let displacement = self.current - target; + let diff = target - self.current; - if displacement.abs() < 0.001 && self.velocity.abs() < 0.001 { + if diff.abs() > 0.001 { + self.current += diff * (1.0 - (-dt / LAG).exp()); + true + } else { self.current = target; - self.velocity = 0.0; - return false; + false } - - let exp = (-OMEGA * dt).exp(); - let coeff = self.velocity + OMEGA * displacement; - self.current = target + (displacement + coeff * dt) * exp; - self.velocity = (self.velocity - OMEGA * coeff * dt) * exp; - - true } } From aa337c2a1618c31b72c6249edec8192ba4799ec0 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 26 Mar 2026 15:51:34 -0400 Subject: [PATCH 064/201] feat(config): support for fallback to previous config version --- cosmic-config/src/lib.rs | 60 ++++++++++++++++++++++++++++++++++++++-- 1 file changed, 57 insertions(+), 3 deletions(-) diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 2af498ff..92eef09c 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -175,6 +175,7 @@ pub trait ConfigSet { pub struct Config { system_path: Option, user_path: Option, + previous: Option>, } /// Check that the name is relative and doesn't contain . or .. @@ -193,9 +194,13 @@ fn sanitize_name(name: &str) -> Result<&Path, Error> { impl Config { /// Get a system config for the given name and config version pub fn system(name: &str, version: u64) -> Result { + Self::system_inner(name, version, true) + } + + fn system_inner(name: &str, version: u64, look_for_previous: bool) -> Result { let path = sanitize_name(name)?.join(format!("v{version}")); #[cfg(unix)] - let system_path = xdg::BaseDirectories::with_prefix("cosmic").find_data_file(path); + let system_path = xdg::BaseDirectories::with_prefix("cosmic").find_data_file(&path); #[cfg(windows)] let system_path = @@ -205,6 +210,13 @@ impl Config { Ok(Self { system_path, user_path: None, + previous: if version > 1 && look_for_previous { + Self::system_inner(name, version - 1, false) + .ok() + .map(Box::new) + } else { + None + }, }) } @@ -212,6 +224,10 @@ impl Config { // Use folder at XDG config/name for config storage, return Config if successful //TODO: fallbacks for flatpak (HOST_XDG_CONFIG_HOME, xdg-desktop settings proxy) pub fn new(name: &str, version: u64) -> Result { + Self::new_inner(name, version, true) + } + + fn new_inner(name: &str, version: u64, look_for_previous: bool) -> Result { // Look for [name]/v[version] let path = sanitize_name(name)?.join(format!("v{}", version)); @@ -236,15 +252,29 @@ impl Config { Ok(Self { system_path, user_path: Some(user_path), + previous: if version > 1 && look_for_previous { + Self::new_inner(name, version - 1, false).ok().map(Box::new) + } else { + None + }, }) } /// Get config for the given application name and config version and custom path. pub fn with_custom_path(name: &str, version: u64, custom_path: PathBuf) -> Result { + Self::with_custom_path_inner(name, version, custom_path, true) + } + + fn with_custom_path_inner( + name: &str, + version: u64, + custom_path: PathBuf, + look_for_previous: bool, + ) -> Result { // Look for [name]/v[version] let path = sanitize_name(name)?.join(format!("v{version}")); - let mut user_path = custom_path; + let mut user_path = custom_path.clone(); user_path.push("cosmic"); user_path.push(path); // Create new configuration directory if not found. @@ -254,6 +284,13 @@ impl Config { Ok(Self { system_path: None, user_path: Some(user_path), + previous: if version > 1 && look_for_previous { + Self::with_custom_path_inner(name, version - 1, custom_path.clone(), false) + .ok() + .map(Box::new) + } else { + None + }, }) } @@ -263,6 +300,10 @@ impl Config { // Use folder at XDG config/name for config storage, return Config if successful //TODO: fallbacks for flatpak (HOST_XDG_CONFIG_HOME, xdg-desktop settings proxy) pub fn new_state(name: &str, version: u64) -> Result { + Self::new_state_inner(name, version, true) + } + + fn new_state_inner(name: &str, version: u64, look_for_previous: bool) -> Result { // Look for [name]/v[version] let path = sanitize_name(name)?.join(format!("v{}", version)); @@ -276,6 +317,13 @@ impl Config { Ok(Self { system_path: None, user_path: Some(user_path), + previous: if version > 1 && look_for_previous { + Self::new_state_inner(name, version - 1, false) + .ok() + .map(Box::new) + } else { + None + }, }) } @@ -404,7 +452,13 @@ impl ConfigGet for Config { Ok(ron::from_str(&data)?) } - _ => Err(Error::NotFound), + _ => { + if let Some(previous) = self.previous.as_ref() { + previous.get_local(key) + } else { + Err(Error::NotFound) + } + } } } From 7ecc6a32d6fe575cbbfbb0eba61606a5410ea515 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 26 Mar 2026 15:51:41 -0400 Subject: [PATCH 065/201] feat(config): support for intermediate serialization/deserialization type via field attribute --- cosmic-config-derive/src/lib.rs | 116 ++++++++++++++++++++++++++------ 1 file changed, 96 insertions(+), 20 deletions(-) diff --git a/cosmic-config-derive/src/lib.rs b/cosmic-config-derive/src/lib.rs index cc19a91e..861398e4 100644 --- a/cosmic-config-derive/src/lib.rs +++ b/cosmic-config-derive/src/lib.rs @@ -1,8 +1,8 @@ use proc_macro::TokenStream; use quote::quote; -use syn::{self}; +use syn; -#[proc_macro_derive(CosmicConfigEntry, attributes(version, id))] +#[proc_macro_derive(CosmicConfigEntry, attributes(version, id, cosmic_config_entry))] pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream { // Construct a representation of Rust code as a syntax tree // that we can manipulate @@ -12,6 +12,25 @@ pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream { impl_cosmic_config_entry_macro(&ast) } +fn get_cosmic_config_attrs(field: &syn::Field) -> Result, syn::Error> { + let mut with = None; + + for attr in &field.attrs { + if !attr.path().is_ident("cosmic_config_entry") { + continue; + } + attr.parse_nested_meta(|meta| { + if meta.path.is_ident("with") { + let value = meta.value()?; + with = Some(value.parse()?); + } + Ok(()) + })?; + } + + Ok(with) +} + fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let attributes = &ast.attrs; let version = attributes @@ -48,19 +67,54 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let write_each_config_field = fields.iter().map(|field| { let field_name = &field.ident; - quote! { - cosmic_config::ConfigSet::set(&tx, stringify!(#field_name), &self.#field_name)?; + let with = match get_cosmic_config_attrs(field) { + Ok(attrs) => attrs, + Err(e) => { + return e.to_compile_error(); + } + }; + + if let Some(with) = with { + quote! { + { + let conv = self.#field_name.clone().into(); + cosmic_config::ConfigSet::set::<#with>(&tx, stringify!(#field_name), conv)?; + } + } + } else { + quote! { + cosmic_config::ConfigSet::set(&tx, stringify!(#field_name), &self.#field_name)?; + } } }); let get_each_config_field = fields.iter().map(|field| { let field_name = &field.ident; let field_type = &field.ty; - quote! { - match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) { - Ok(#field_name) => default.#field_name = #field_name, - Err(why) if matches!(why, cosmic_config::Error::NoConfigDirectory) => (), - Err(e) => errors.push(e), + let with = match get_cosmic_config_attrs(field) { + Ok(attrs) => attrs, + Err(e) => { + return e.to_compile_error(); + } + }; + + if let Some(with) = with { + quote! { + match cosmic_config::ConfigGet::get::<#with>(config, stringify!(#field_name)) { + Ok(value) => { + default.#field_name = value.into(); + } + Err(why) if matches!(why, cosmic_config::Error::NoConfigDirectory) => (), + Err(e) => errors.push(e), + } + } + } else { + quote! { + match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) { + Ok(#field_name) => default.#field_name = #field_name, + Err(why) if matches!(why, cosmic_config::Error::NoConfigDirectory) => (), + Err(e) => errors.push(e), + } } } }); @@ -68,17 +122,39 @@ fn impl_cosmic_config_entry_macro(ast: &syn::DeriveInput) -> TokenStream { let update_each_config_field = fields.iter().map(|field| { let field_name = &field.ident; let field_type = &field.ty; - quote! { - stringify!(#field_name) => { - match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) { - Ok(value) => { - if self.#field_name != value { - keys.push(stringify!(#field_name)); - } - self.#field_name = value; - }, - Err(e) => { - errors.push(e); + let with = match get_cosmic_config_attrs(field) { + Ok(attrs) => attrs, + Err(e) => { + return e.to_compile_error(); + } + }; + + if let Some(with) = with { + quote! { + stringify!(#field_name) => { + match cosmic_config::ConfigGet::get::<#with>(config, stringify!(#field_name)) { + Ok(value) => { + let value = value.into(); + if self.#field_name != value { + keys.push(stringify!(#field_name)); + } + self.#field_name = value; + }, + Err(e) => errors.push(e), + } + } + } + } else { + quote! { + stringify!(#field_name) => { + match cosmic_config::ConfigGet::get::<#field_type>(config, stringify!(#field_name)) { + Ok(value) => { + if self.#field_name != value { + keys.push(stringify!(#field_name)); + } + self.#field_name = value; + }, + Err(e) => errors.push(e), } } } From 72ddeaa5fb1ec4435b11899420b981733b4ad839 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 26 Mar 2026 16:17:36 -0400 Subject: [PATCH 066/201] feat: hex_color serialization for the theme can also can deserialize the previous version of the theme, so existing themes should not be affected --- cosmic-config-derive/src/lib.rs | 4 - cosmic-theme/Cargo.toml | 1 + cosmic-theme/src/model/color.rs | 173 +++++++++++++++++++++++ cosmic-theme/src/model/cosmic_palette.rs | 33 +++++ cosmic-theme/src/model/derivation.rs | 17 +++ cosmic-theme/src/model/mod.rs | 1 + cosmic-theme/src/model/theme.rs | 43 +++++- iced | 2 +- src/command.rs | 1 - 9 files changed, 262 insertions(+), 13 deletions(-) create mode 100644 cosmic-theme/src/model/color.rs diff --git a/cosmic-config-derive/src/lib.rs b/cosmic-config-derive/src/lib.rs index 861398e4..b49cf447 100644 --- a/cosmic-config-derive/src/lib.rs +++ b/cosmic-config-derive/src/lib.rs @@ -4,11 +4,7 @@ use syn; #[proc_macro_derive(CosmicConfigEntry, attributes(version, id, cosmic_config_entry))] pub fn cosmic_config_entry_derive(input: TokenStream) -> TokenStream { - // Construct a representation of Rust code as a syntax tree - // that we can manipulate let ast = syn::parse(input).unwrap(); - - // Build the trait implementation impl_cosmic_config_entry_macro(&ast) } diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 15aee334..82ff9fd3 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -16,6 +16,7 @@ no-default = [] [dependencies] palette = { workspace = true, features = ["serializing"] } +hex_color = { version = "3", features = ["serde"] } almost = "0.2" serde = { workspace = true, features = ["derive"] } serde_json = { version = "1.0.149", optional = true, features = [ diff --git a/cosmic-theme/src/model/color.rs b/cosmic-theme/src/model/color.rs new file mode 100644 index 00000000..cd64ec7f --- /dev/null +++ b/cosmic-theme/src/model/color.rs @@ -0,0 +1,173 @@ +//! Color representation and serde helpers for the Cosmic theme + +use hex_color::HexColor; +use palette::{Srgb, Srgba}; +use serde::{Deserialize, Serialize}; + +/// A color in the Cosmic theme for serialization and deserialization +#[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)] +#[serde(untagged)] +pub enum ColorRepr { + /// A color represented as a hex string + #[serde(with = "hex_color::rgba")] + Hex(HexColor), + /// A color represented as an RGBA value + Rgba(Srgba), + /// A color represented as an RGB value + Rgb(Srgb), +} + +/// An optional color in the Cosmic theme for serialization and deserialization +#[repr(transparent)] +#[derive(Debug, Copy, Clone, PartialEq, Deserialize, Serialize)] +#[serde(transparent)] +pub struct ColorReprOption(Option); + +impl From for ColorRepr { + fn from(color: Srgb) -> Self { + let rgb_u8: Srgb = color.into_format(); + ColorRepr::Hex(HexColor { + r: rgb_u8.red, + g: rgb_u8.green, + b: rgb_u8.blue, + a: 255, + }) + } +} + +impl From for ColorRepr { + fn from(color: Srgba) -> Self { + let rgba_u8: Srgba = color.into_format(); + ColorRepr::Hex(HexColor { + r: rgba_u8.red, + g: rgba_u8.green, + b: rgba_u8.blue, + a: rgba_u8.alpha, + }) + } +} + +impl From for Srgb { + fn from(value: ColorRepr) -> Self { + match value { + ColorRepr::Hex(hex) => Srgb::::new(hex.r, hex.g, hex.b).into_format(), + ColorRepr::Rgb(rgb) => rgb, + ColorRepr::Rgba(rgba) => Srgb::new(rgba.red, rgba.green, rgba.blue), + } + } +} + +impl From for Srgba { + fn from(value: ColorRepr) -> Self { + match value { + ColorRepr::Hex(hex) => Srgba::::new(hex.r, hex.g, hex.b, hex.a).into_format(), + ColorRepr::Rgb(rgb) => Srgba::new(rgb.red, rgb.green, rgb.blue, 1.0), + ColorRepr::Rgba(rgba) => rgba, + } + } +} + +impl From for Option { + fn from(value: ColorReprOption) -> Self { + value.0.map(std::convert::Into::into) + } +} + +impl From for Option { + fn from(value: ColorReprOption) -> Self { + value.0.map(std::convert::Into::into) + } +} + +impl From> for ColorReprOption { + fn from(value: Option) -> Self { + ColorReprOption(value.map(std::convert::Into::into)) + } +} + +impl From> for ColorReprOption { + fn from(value: Option) -> Self { + ColorReprOption(value.map(std::convert::Into::into)) + } +} + +/// A trait for converting between a color type and its representation for serialization and deserialization +pub trait ConvColorRepr: Sized { + /// Convert from a color representation to the color type + fn from_repr(repr: ColorRepr) -> Self; + /// Convert from the color type to its representation for serialization + fn to_repr(&self) -> ColorRepr; +} + +impl ConvColorRepr for Srgba { + fn from_repr(repr: ColorRepr) -> Self { + repr.into() + } + + fn to_repr(&self) -> ColorRepr { + (*self).into() + } +} + +impl ConvColorRepr for Srgb { + fn from_repr(repr: ColorRepr) -> Self { + repr.into() + } + + fn to_repr(&self) -> ColorRepr { + (*self).into() + } +} + +/// Serde helpers for serializing and deserializing colors in the Cosmic theme +pub mod color_serde { + use super::*; + use serde::{Deserialize, Deserializer, Serializer}; + + /// Serialize a color to a hex string + pub fn serialize(color: &T, serializer: S) -> Result + where + T: ConvColorRepr, + S: Serializer, + { + let repr = color.to_repr(); + repr.serialize(serializer) + } + + /// Deserialize a color from a hex string or RGB/RGBA + pub fn deserialize<'de, T, D>(deserializer: D) -> Result + where + T: ConvColorRepr, + D: Deserializer<'de>, + { + let repr = ColorRepr::deserialize(deserializer)?; + Ok(T::from_repr(repr)) + } + + /// Serde helpers for serializing and deserializing optional colors in the Cosmic theme + pub mod option { + use super::*; + + /// Serialize an optional color + pub fn serialize(value: &Option, serializer: S) -> Result + where + T: ConvColorRepr, + S: Serializer, + { + match value { + Some(v) => super::serialize(v, serializer), + None => serializer.serialize_none(), + } + } + + /// Deserialize an optional color + pub fn deserialize<'de, T, D>(deserializer: D) -> Result, D::Error> + where + T: ConvColorRepr, + D: Deserializer<'de>, + { + let opt = Option::::deserialize(deserializer)?; + Ok(opt.map(T::from_repr)) + } + } +} diff --git a/cosmic-theme/src/model/cosmic_palette.rs b/cosmic-theme/src/model/cosmic_palette.rs index 3852742b..4360f8f4 100644 --- a/cosmic-theme/src/model/cosmic_palette.rs +++ b/cosmic-theme/src/model/cosmic_palette.rs @@ -1,3 +1,4 @@ +use crate::color::color_serde; use palette::Srgba; use serde::{Deserialize, Serialize}; use std::sync::LazyLock; @@ -95,75 +96,107 @@ pub struct CosmicPaletteInner { /// Utility Colors /// Colors used for various points of emphasis in the UI. + #[serde(with = "color_serde")] pub bright_red: Srgba, /// Colors used for various points of emphasis in the UI. + #[serde(with = "color_serde")] pub bright_green: Srgba, /// Colors used for various points of emphasis in the UI. + #[serde(with = "color_serde")] pub bright_orange: Srgba, /// Surface Grays /// Colors used for three levels of surfaces in the UI. + #[serde(with = "color_serde")] pub gray_1: Srgba, /// Colors used for three levels of surfaces in the UI. + #[serde(with = "color_serde")] pub gray_2: Srgba, /// System Neutrals /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_0: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_1: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_2: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_3: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_4: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_5: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_6: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_7: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_8: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_9: Srgba, /// A wider spread of dark colors for more general use. + #[serde(with = "color_serde")] pub neutral_10: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_blue: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_indigo: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_purple: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_pink: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_red: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_orange: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_yellow: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_green: Srgba, /// Potential Accent Color Combos + #[serde(with = "color_serde")] pub accent_warm_grey: Srgba, /// Extended Color Palette /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_warm_grey: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_orange: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_yellow: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_blue: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_purple: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_pink: Srgba, /// Colors used for themes, app icons, illustrations, and other brand purposes. + #[serde(with = "color_serde")] pub ext_indigo: Srgba, } diff --git a/cosmic-theme/src/model/derivation.rs b/cosmic-theme/src/model/derivation.rs index dce653e5..796ddab3 100644 --- a/cosmic-theme/src/model/derivation.rs +++ b/cosmic-theme/src/model/derivation.rs @@ -1,3 +1,4 @@ +use crate::color::color_serde; use palette::{Srgba, WithAlpha}; use serde::{Deserialize, Serialize}; @@ -8,14 +9,18 @@ use crate::composite::over; #[must_use] pub struct Container { /// the color of the container + #[serde(with = "color_serde")] pub base: Srgba, /// the color of components in the container pub component: Component, /// the color of dividers in the container + #[serde(with = "color_serde")] pub divider: Srgba, /// the color of text in the container + #[serde(with = "color_serde")] pub on: Srgba, /// the color of @small_widget_container + #[serde(with = "color_serde")] pub small_widget: Srgba, } @@ -45,30 +50,42 @@ impl Container { #[must_use] pub struct Component { /// The base color of the widget + #[serde(with = "color_serde")] pub base: Srgba, /// The color of the widget when it is hovered + #[serde(with = "color_serde")] pub hover: Srgba, /// the color of the widget when it is pressed + #[serde(with = "color_serde")] pub pressed: Srgba, /// the color of the widget when it is selected + #[serde(with = "color_serde")] pub selected: Srgba, /// the color of the widget when it is selected + #[serde(with = "color_serde")] pub selected_text: Srgba, /// the color of the widget when it is focused + #[serde(with = "color_serde")] pub focus: Srgba, /// the color of dividers for this widget + #[serde(with = "color_serde")] pub divider: Srgba, /// the color of text for this widget + #[serde(with = "color_serde")] pub on: Srgba, // the color of text with opacity 80 for this widget // pub text_opacity_80: Srgba, /// the color of the widget when it is disabled + #[serde(with = "color_serde")] pub disabled: Srgba, /// the color of text in the widget when it is disabled + #[serde(with = "color_serde")] pub on_disabled: Srgba, /// the color of the border for the widget + #[serde(with = "color_serde")] pub border: Srgba, /// the color of the border for the widget when it is disabled + #[serde(with = "color_serde")] pub disabled_border: Srgba, } diff --git a/cosmic-theme/src/model/mod.rs b/cosmic-theme/src/model/mod.rs index 19370dee..a7ac613f 100644 --- a/cosmic-theme/src/model/mod.rs +++ b/cosmic-theme/src/model/mod.rs @@ -5,6 +5,7 @@ pub use mode::*; pub use spacing::*; pub use theme::*; +pub mod color; mod corner; mod cosmic_palette; mod derivation; diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index a403403c..8b7aa66e 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -3,6 +3,7 @@ use crate::steps::{color_index, get_small_widget_color, get_surface_color, get_t use crate::{ Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, DARK_PALETTE, LIGHT_PALETTE, NAME, Spacing, ThemeMode, + color::{ColorRepr, ColorReprOption, color_serde, color_serde::option as color_serde_option}, }; use cosmic_config::{Config, CosmicConfigEntry}; use palette::color_difference::Wcag21RelativeContrast; @@ -45,7 +46,7 @@ pub enum Layer { PartialEq, cosmic_config::cosmic_config_derive::CosmicConfigEntry, )] -#[version = 1] +#[version = 2] pub struct Theme { /// name of the theme pub name: String, @@ -100,13 +101,21 @@ pub struct Theme { /// enables blurred transparency pub is_frosted: bool, /// shade color for dialogs + #[serde(with = "color_serde")] + #[cosmic_config_entry(with = ColorRepr)] pub shade: Srgba, /// accent text colors /// If None, accent base color is the accent text color. + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub accent_text: Option, /// control tint color + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub control_tint: Option, /// text tint color + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub text_tint: Option, } @@ -741,7 +750,7 @@ impl Theme { if color_scheme.trim().contains("default") || color_scheme.trim().contains("light") { return Self::light_default(); } - }; + } Self::dark_default() } @@ -750,10 +759,10 @@ impl Theme { pub fn preferred_theme() -> Self { let current_desktop = std::env::var("XDG_CURRENT_DESKTOP"); - if let Ok(desktop) = current_desktop { - if desktop.trim().to_lowercase().contains("gnome") { - return Self::gtk_prefer_colorscheme(); - } + if let Ok(desktop) = current_desktop + && desktop.trim().to_lowercase().contains("gnome") + { + return Self::gtk_prefer_colorscheme(); } Self::dark_default() @@ -776,7 +785,7 @@ impl From for Theme { cosmic_config::cosmic_config_derive::CosmicConfigEntry, PartialEq, )] -#[version = 1] +#[version = 2] pub struct ThemeBuilder { /// override the palette for the builder pub palette: CosmicPalette, @@ -785,22 +794,40 @@ pub struct ThemeBuilder { /// override corner radii for the builder pub corner_radii: CornerRadii, /// override neutral_tint for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub neutral_tint: Option, /// override bg_color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub bg_color: Option, /// override the primary container bg color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub primary_container_bg: Option, /// override the secontary container bg color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub secondary_container_bg: Option, /// override the text tint for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub text_tint: Option, /// override the accent color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub accent: Option, /// override the success color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub success: Option, /// override the warning color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub warning: Option, /// override the destructive color for the builder + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub destructive: Option, /// enabled blurred transparency pub is_frosted: bool, // TODO handle @@ -809,6 +836,8 @@ pub struct ThemeBuilder { /// cosmic-comp active hint window outline width pub active_hint: u32, /// cosmic-comp custom window hint color + #[serde(with = "color_serde_option")] + #[cosmic_config_entry(with = ColorReprOption)] pub window_hint: Option, } diff --git a/iced b/iced index 7afd9ee0..cff835bc 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 7afd9ee0f83cfd1a4e9f9b56cf0f365e5ae60347 +Subproject commit cff835bc64e887b3db5c1fafa1f4c5cdf8db1b42 diff --git a/src/command.rs b/src/command.rs index 1d6f635c..6bb16e8d 100644 --- a/src/command.rs +++ b/src/command.rs @@ -65,7 +65,6 @@ pub fn file_transfer_send( /// 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 From 7ac8f7482556e3ea7efb15f823e44ab0abcb4632 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 27 Mar 2026 13:22:43 -0400 Subject: [PATCH 067/201] chore: update ron files --- cosmic-theme/src/model/dark.ron | 2 +- cosmic-theme/src/model/light.ron | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/cosmic-theme/src/model/dark.ron b/cosmic-theme/src/model/dark.ron index 4453b8bf..ffec0818 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: "#FFA09AFF",bright_green: "#5EDB8CFF",bright_orange: "#FFA37DFF",gray_1: "#1B1B1BFF",gray_2: "#262626FF",neutral_0: "#000000FF",neutral_1: "#030303FF",neutral_2: "#161616FF",neutral_3: "#2E2E2EFF",neutral_4: "#484848FF",neutral_5: "#636363FF",neutral_6: "#808080FF",neutral_7: "#9E9E9EFF",neutral_8: "#BEBEBEFF",neutral_9: "#DEDEDEFF",neutral_10: "#FFFFFFFF",accent_blue: "#63D0DFFF",accent_indigo: "#A1C0EBFF",accent_purple: "#E79CFEFF",accent_pink: "#FF9CB1FF",accent_red: "#FDA1A0FF",accent_orange: "#FFAD00FF",accent_yellow: "#F7E062FF",accent_green: "#92CF9CFF",accent_warm_grey: "#CABAB4FF",ext_warm_grey: "#9B8E8AFF",ext_orange: "#FFAD00FF",ext_yellow: "#FEDB40FF",ext_blue: "#48B9C7FF",ext_purple: "#CF7DFFFF",ext_pink: "#F93A83FF",ext_indigo: "#3E88FFFF",)) diff --git a/cosmic-theme/src/model/light.ron b/cosmic-theme/src/model/light.ron index 29b3ad65..c9fcc6ce 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: "#890418FF",bright_green: "#00572CFF",bright_orange: "#792C00FF",gray_1: "#D7D7D7FF",gray_2: "#E4E4E4FF",neutral_0: "#FFFFFFFF",neutral_1: "#DEDEDEFF",neutral_2: "#BEBEBEFF",neutral_3: "#9E9E9EFF",neutral_4: "#808080FF",neutral_5: "#636363FF",neutral_6: "#484848FF",neutral_7: "#2E2E2EFF",neutral_8: "#161616FF",neutral_9: "#030303FF",neutral_10: "#000000FF",accent_blue: "#00525AFF",accent_indigo: "#2E496DFF",accent_purple: "#68217CFF",accent_pink: "#86043AFF",accent_red: "#78292EFF",accent_orange: "#624000FF",accent_yellow: "#534800FF",accent_green: "#185529FF",accent_warm_grey: "#554742FF",ext_warm_grey: "#9B8E8AFF",ext_orange: "#FBB86CFF",ext_yellow: "#F7E062FF",ext_blue: "#6ACAD8FF",ext_purple: "#D58CFFFF",ext_pink: "#FF9CDDFF",ext_indigo: "#95C4FCFF",)) From 9395a23a0b832e165a0d2cdd1533673fe90e655d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 2 Apr 2026 11:31:51 -0400 Subject: [PATCH 068/201] chore: update qt light default kcolorscheme this was likely off by one because of a rounding change from v1 --- ...e__output__qt_output__tests__light_default_kcolorscheme.snap | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) 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 ae2bcb66..40363f95 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 @@ -81,7 +81,7 @@ ForegroundPositive=0,87,44 ForegroundVisited=0,82,90 [Colors:Selection] -BackgroundAlternate=108,149,152 +BackgroundAlternate=108,149,153 BackgroundNormal=0,82,90 DecorationFocus=0,82,90 DecorationHover=0,82,90 From 439109db0406e3b425ec3edb99f15deff923bfe4 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 13 Apr 2026 11:04:55 -0400 Subject: [PATCH 069/201] wip: blurred transparency --- cosmic-theme/src/model/theme.rs | 90 ++++++++++++++++++++++++++++++--- src/app/cosmic.rs | 88 ++++++++++++++++++++++++++++++-- src/core.rs | 7 +++ src/theme/style/iced.rs | 3 ++ 4 files changed, 176 insertions(+), 12 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 8b7aa66e..49042449 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -99,7 +99,8 @@ pub struct Theme { /// cosmic-comp custom window hint color pub window_hint: Option, /// enables blurred transparency - pub is_frosted: bool, + /// If None, frosted effect is disabled. + pub frosted: Option, /// shade color for dialogs #[serde(with = "color_serde")] #[cosmic_config_entry(with = ColorRepr)] @@ -830,7 +831,7 @@ pub struct ThemeBuilder { #[cosmic_config_entry(with = ColorReprOption)] pub destructive: Option, /// enabled blurred transparency - pub is_frosted: bool, // TODO handle + pub frosted: Option, /// cosmic-comp window gaps size (outer, inner) pub gaps: (u32, u32), /// cosmic-comp active hint window outline width @@ -856,7 +857,7 @@ impl Default for ThemeBuilder { success: Default::default(), warning: Default::default(), destructive: Default::default(), - is_frosted: false, + frosted: None, // cosmic-comp theme settings gaps: (0, 8), active_hint: 3, @@ -1002,9 +1003,11 @@ impl ThemeBuilder { gaps, active_hint, window_hint, - is_frosted, + frosted, } = self; + let container_alpha = frosted.map_or(1.0, |f| f.alpha()); + let is_dark = palette.is_dark(); let is_high_contrast = palette.is_high_contrast(); @@ -1050,12 +1053,14 @@ impl ThemeBuilder { NonZeroUsize::new(100).unwrap(), ); - let bg = if let Some(bg_color) = bg_color { + let mut bg = if let Some(bg_color) = bg_color { bg_color } else { p_ref.gray_1 }; + bg.alpha = container_alpha; + let step_array = steps(bg, NonZeroUsize::new(100).unwrap()); let bg_index = color_index(bg, step_array.len()); @@ -1086,11 +1091,12 @@ impl ThemeBuilder { ); let primary = { - let container_bg = if let Some(primary_container_bg_color) = primary_container_bg { + let mut 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, &control_steps_array[1]) }; + container_bg.alpha = container_alpha; let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); let base_index: usize = color_index(container_bg, step_array.len()); @@ -1207,11 +1213,13 @@ impl ThemeBuilder { ), primary, secondary: { - let container_bg = if let Some(secondary_container_bg) = secondary_container_bg { + let mut 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, &control_steps_array[2]) }; + container_bg.alpha = container_alpha; let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); let base_index = color_index(container_bg, step_array.len()); @@ -1372,7 +1380,7 @@ impl ThemeBuilder { gaps, active_hint, window_hint, - is_frosted, + frosted, accent_text, control_tint: neutral_tint, text_tint, @@ -1394,3 +1402,69 @@ impl ThemeBuilder { Config::new(LIGHT_THEME_BUILDER_ID, Self::VERSION) } } + +#[repr(u8)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub enum BlurStrength { + ExtremelyLow = 1, + ExtremelyLow2, + VeryLow, + VeryLow2, + Low, + Low2, + Medium, + Medium2, + High, + High2, + VeryHigh, + VeryHigh2, + ExtremelyHigh, + ExtremelyHigh2, +} + +impl BlurStrength { + /// Get the alpha value corresponding to the blur strength + /// Lower alpha values correspond to stronger blur effects, and higher alpha values correspond to weaker blur effects. The mapping is as follows: + pub fn alpha(&self) -> f32 { + match self { + Self::ExtremelyLow => 0.95, + Self::ExtremelyLow2 => 0.85, + Self::VeryLow => 0.8, + Self::VeryLow2 => 0.75, + Self::Low => 0.7, + Self::Low2 => 0.65, + Self::Medium => 0.6, + Self::Medium2 => 0.55, + Self::High => 0.5, + Self::High2 => 0.45, + Self::VeryHigh => 0.4, + Self::VeryHigh2 => 0.35, + Self::ExtremelyHigh => 0.2, + Self::ExtremelyHigh2 => 0.05, + } + } +} + +impl TryFrom for BlurStrength { + type Error = (); + + fn try_from(value: u8) -> Result { + match value { + 1 => Ok(BlurStrength::ExtremelyLow), + 2 => Ok(BlurStrength::ExtremelyLow2), + 3 => Ok(BlurStrength::VeryLow), + 4 => Ok(BlurStrength::VeryLow2), + 5 => Ok(BlurStrength::Low), + 6 => Ok(BlurStrength::Low2), + 7 => Ok(BlurStrength::Medium), + 8 => Ok(BlurStrength::Medium2), + 9 => Ok(BlurStrength::High), + 10 => Ok(BlurStrength::High2), + 11 => Ok(BlurStrength::VeryHigh), + 12 => Ok(BlurStrength::VeryHigh2), + 13 => Ok(BlurStrength::ExtremelyHigh), + 14 => Ok(BlurStrength::ExtremelyHigh2), + _ => Err(()), + } + } +} diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 030ed041..6b7b04ba 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -773,10 +773,31 @@ impl Cosmic { if a.distance_squared(*t_inner.accent_color()) > 0.00001 { theme = Theme::system(Arc::new(t_inner.with_accent(a))); } - }; + } } + let new_blur = theme.cosmic().frosted.is_some(); THEME.lock().unwrap().set_theme(theme.theme_type); + + let core = self.app.core(); + if core.auto_blur { + let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); + let blur = if new_blur { + iced::window::enable_blur + } else { + iced::window::disable_blur + }; + cmds.push(blur( + self.app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED), + )); + for id in &self.tracked_windows { + cmds.push(blur(*id)); + } + return Task::batch(cmds); + } } Action::SystemThemeChange(keys, theme) => { @@ -809,6 +830,8 @@ impl Cosmic { theme }; new_theme.theme_type.prefer_dark(prefer_dark); + // TODO adjust theme container alphas to remove transparency? + // if auto-blur is disabled & theme is frosted, should we make container colors in theme opaque? cosmic_theme.set_theme(new_theme.theme_type); #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -873,7 +896,7 @@ impl Cosmic { } } // Update radius for all tracked windows - for id in self.tracked_windows.iter() { + for id in &self.tracked_windows { cmds.push( corner_radius( *id, @@ -956,6 +979,9 @@ impl Cosmic { // Only apply update if the theme is set to load a system theme if let ThemeType::System { .. } = cosmic_theme.theme_type { + // TODO adjust theme container alphas to remove transparency? + // if auto-blur is disabled & theme is frosted, should we make container colors in theme opaque? + let new_blur = new_theme.cosmic().frosted.is_some(); cosmic_theme.set_theme(new_theme.theme_type); #[cfg(all(feature = "wayland", target_os = "linux"))] if self.app.core().sync_window_border_radii_to_theme() { @@ -1019,7 +1045,7 @@ impl Cosmic { } } // Update radius for all tracked windows - for id in self.tracked_windows.iter() { + for id in &self.tracked_windows { cmds.push( corner_radius( *id, @@ -1039,6 +1065,25 @@ impl Cosmic { ); } + let core = self.app.core(); + if core.auto_blur { + let blur = if new_blur { + iced::window::enable_blur + } else { + iced::window::disable_blur + }; + + cmds.push(blur( + self.app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED), + )); + for id in &self.tracked_windows { + cmds.push(blur(*id)); + } + } + return Task::batch(cmds); } } @@ -1147,7 +1192,26 @@ impl Cosmic { // Only apply update if the theme is set to load a system theme if let ThemeType::System { theme: _, .. } = cosmic_theme.theme_type { + let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); + + if core.auto_blur { + let blur = if new_theme.cosmic().frosted.is_some() { + iced::window::enable_blur + } else { + iced::window::disable_blur + }; + cmds.push(blur( + self.app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED), + )); + for id in &self.tracked_windows { + cmds.push(blur(*id)); + } + } cosmic_theme.set_theme(new_theme.theme_type); + return Task::batch(cmds); } } } @@ -1263,8 +1327,24 @@ impl Cosmic { }; // TODO do we need per window sharp corners? let rounded = !self.app.core().window.sharp_corners; - + let core = self.app.core(); + let blur_cmd = if core.auto_blur { + let blur = if t.frosted.is_some() { + iced::window::enable_blur + } else { + iced::window::disable_blur + }; + let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); + cmds.push(blur(id)); + for id in &self.tracked_windows { + cmds.push(blur(*id)); + } + Task::batch(cmds) + } else { + Task::none() + }; return Task::batch([ + blur_cmd, corner_radius( id, if rounded { diff --git a/src/core.rs b/src/core.rs index 970a5351..2ea0890b 100644 --- a/src/core.rs +++ b/src/core.rs @@ -101,6 +101,8 @@ pub struct Core { #[cfg(all(feature = "wayland", target_os = "linux"))] pub(crate) sync_window_border_radii_to_theme: bool, + + pub(crate) auto_blur: bool, } impl Default for Core { @@ -161,6 +163,7 @@ impl Default for Core { menu_bars: HashMap::new(), #[cfg(all(feature = "wayland", target_os = "linux"))] sync_window_border_radii_to_theme: true, + auto_blur: true, } } } @@ -502,4 +505,8 @@ impl Core { pub fn sync_window_border_radii_to_theme(&self) -> bool { self.sync_window_border_radii_to_theme } + + pub fn set_auto_blur(&mut self, auto_blur: bool) { + self.auto_blur = auto_blur; + } } diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 131538e4..84d9b8c0 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -564,6 +564,9 @@ impl iced_container::Catalog for Theme { Container::ContextDrawer => { let mut a = Container::primary(cosmic); + if let Some(Background::Color(ref mut color)) = a.background { + color.a = 1.; + } if cosmic.is_high_contrast { a.border.width = 1.; From d0b164d216379a22d79e61fbf382403241c41914 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 14 Apr 2026 23:44:01 -0400 Subject: [PATCH 070/201] feat(blur): better align with designs and remove transparency from theme when not on wayland --- cosmic-theme/src/model/theme.rs | 84 +++++++++++++++++--------- src/app/cosmic.rs | 104 +++++++++++++++++++++++++++++--- src/applet/mod.rs | 5 +- src/core.rs | 25 ++++++++ src/theme/mod.rs | 23 +++++++ src/theme/style/iced.rs | 18 +++++- src/theme/style/menu_bar.rs | 4 +- 7 files changed, 222 insertions(+), 41 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 49042449..87b7269c 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -10,7 +10,7 @@ use palette::color_difference::Wcag21RelativeContrast; use palette::rgb::Rgb; use palette::{IntoColor, Oklcha, Srgb, Srgba, WithAlpha}; use serde::{Deserialize, Serialize}; -use std::num::NonZeroUsize; +use std::{default, num::NonZeroUsize}; /// ID for the current dark `ThemeBuilder` config pub const DARK_THEME_BUILDER_ID: &str = "com.system76.CosmicTheme.Dark.Builder"; @@ -99,8 +99,15 @@ pub struct Theme { /// cosmic-comp custom window hint color pub window_hint: Option, /// enables blurred transparency - /// If None, frosted effect is disabled. - pub frosted: Option, + pub frosted: BlurStrength, + /// frosted windows + pub frosted_windows: bool, + /// frosted system interface + pub frosted_system_interface: bool, + /// frosted panel + pub frosted_panel: bool, + /// frosted applet popups + pub frosted_applets: bool, /// shade color for dialogs #[serde(with = "color_serde")] #[cosmic_config_entry(with = ColorRepr)] @@ -831,7 +838,7 @@ pub struct ThemeBuilder { #[cosmic_config_entry(with = ColorReprOption)] pub destructive: Option, /// enabled blurred transparency - pub frosted: Option, + pub frosted: BlurStrength, /// cosmic-comp window gaps size (outer, inner) pub gaps: (u32, u32), /// cosmic-comp active hint window outline width @@ -840,6 +847,14 @@ pub struct ThemeBuilder { #[serde(with = "color_serde_option")] #[cosmic_config_entry(with = ColorReprOption)] pub window_hint: Option, + /// frosted windows + pub frosted_windows: bool, + /// frosted system interface + pub frosted_system_interface: bool, + /// frosted panel + pub frosted_panel: bool, + /// frosted applet popups + pub frosted_applets: bool, } impl Default for ThemeBuilder { @@ -857,11 +872,15 @@ impl Default for ThemeBuilder { success: Default::default(), warning: Default::default(), destructive: Default::default(), - frosted: None, + frosted: BlurStrength::default(), // cosmic-comp theme settings gaps: (0, 8), active_hint: 3, window_hint: None, + frosted_windows: false, + frosted_system_interface: false, + frosted_panel: false, + frosted_applets: false, } } } @@ -1004,9 +1023,13 @@ impl ThemeBuilder { active_hint, window_hint, frosted, + frosted_windows, + frosted_system_interface, + frosted_panel, + frosted_applets, } = self; - let container_alpha = frosted.map_or(1.0, |f| f.alpha()); + let container_alpha = frosted.alpha(); let is_dark = palette.is_dark(); let is_high_contrast = palette.is_high_contrast(); @@ -1096,7 +1119,7 @@ impl ThemeBuilder { } else { get_surface_color(bg_index, 5, &step_array, is_dark, &control_steps_array[1]) }; - container_bg.alpha = container_alpha; + container_bg.alpha = (container_alpha + if is_dark { 0.3 } else { 0.25 }).min(1.0); let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); let base_index: usize = color_index(container_bg, step_array.len()); @@ -1219,7 +1242,7 @@ impl ThemeBuilder { } else { get_surface_color(bg_index, 10, &step_array, is_dark, &control_steps_array[2]) }; - container_bg.alpha = container_alpha; + container_bg.alpha = (container_alpha + if is_dark { 0.6 } else { 0.5 }).min(1.0); let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); let base_index = color_index(container_bg, step_array.len()); @@ -1384,6 +1407,10 @@ impl ThemeBuilder { accent_text, control_tint: neutral_tint, text_tint, + frosted_windows, + frosted_system_interface, + frosted_panel, + frosted_applets, }; theme.spacing = spacing; theme.corner_radii = corner_radii; @@ -1403,15 +1430,18 @@ impl ThemeBuilder { } } +/// Actual blur radius is decided by cosmic-comp, +/// but this represents the strength of the blur effect. #[repr(u8)] -#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum BlurStrength { - ExtremelyLow = 1, + ExtremelyLow, ExtremelyLow2, VeryLow, VeryLow2, Low, Low2, + #[default] Medium, Medium2, High, @@ -1427,7 +1457,7 @@ impl BlurStrength { /// Lower alpha values correspond to stronger blur effects, and higher alpha values correspond to weaker blur effects. The mapping is as follows: pub fn alpha(&self) -> f32 { match self { - Self::ExtremelyLow => 0.95, + Self::ExtremelyLow => 0.90, Self::ExtremelyLow2 => 0.85, Self::VeryLow => 0.8, Self::VeryLow2 => 0.75, @@ -1439,8 +1469,8 @@ impl BlurStrength { Self::High2 => 0.45, Self::VeryHigh => 0.4, Self::VeryHigh2 => 0.35, - Self::ExtremelyHigh => 0.2, - Self::ExtremelyHigh2 => 0.05, + Self::ExtremelyHigh => 0.25, + Self::ExtremelyHigh2 => 0.2, } } } @@ -1450,20 +1480,20 @@ impl TryFrom for BlurStrength { fn try_from(value: u8) -> Result { match value { - 1 => Ok(BlurStrength::ExtremelyLow), - 2 => Ok(BlurStrength::ExtremelyLow2), - 3 => Ok(BlurStrength::VeryLow), - 4 => Ok(BlurStrength::VeryLow2), - 5 => Ok(BlurStrength::Low), - 6 => Ok(BlurStrength::Low2), - 7 => Ok(BlurStrength::Medium), - 8 => Ok(BlurStrength::Medium2), - 9 => Ok(BlurStrength::High), - 10 => Ok(BlurStrength::High2), - 11 => Ok(BlurStrength::VeryHigh), - 12 => Ok(BlurStrength::VeryHigh2), - 13 => Ok(BlurStrength::ExtremelyHigh), - 14 => Ok(BlurStrength::ExtremelyHigh2), + 0 => Ok(BlurStrength::ExtremelyLow), + 1 => Ok(BlurStrength::ExtremelyLow2), + 2 => Ok(BlurStrength::VeryLow), + 3 => Ok(BlurStrength::VeryLow2), + 4 => Ok(BlurStrength::Low), + 5 => Ok(BlurStrength::Low2), + 6 => Ok(BlurStrength::Medium), + 7 => Ok(BlurStrength::Medium2), + 8 => Ok(BlurStrength::High), + 9 => Ok(BlurStrength::High2), + 10 => Ok(BlurStrength::VeryHigh), + 11 => Ok(BlurStrength::VeryHigh2), + 12 => Ok(BlurStrength::ExtremelyHigh), + 13 => Ok(BlurStrength::ExtremelyHigh2), _ => Err(()), } } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 6b7b04ba..88ff025a 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -115,8 +115,8 @@ where ( Self::new(model), Task::batch([ - command, iced_runtime::window::run_with_handle(id, init_windowing_system), + command, ]), ) } @@ -776,7 +776,17 @@ impl Cosmic { } } - let new_blur = theme.cosmic().frosted.is_some(); + let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { + let t = theme.cosmic(); + match self.app.core().app_type() { + crate::core::AppType::Window => t.frosted_windows, + crate::core::AppType::System => t.frosted_system_interface, + crate::core::AppType::Applet => t.frosted_applets, + } + }; + if !new_blur { + theme = theme.into_opaque(); + } THEME.lock().unwrap().set_theme(theme.theme_type); let core = self.app.core(); @@ -979,9 +989,19 @@ impl Cosmic { // Only apply update if the theme is set to load a system theme if let ThemeType::System { .. } = cosmic_theme.theme_type { - // TODO adjust theme container alphas to remove transparency? - // if auto-blur is disabled & theme is frosted, should we make container colors in theme opaque? - let new_blur = new_theme.cosmic().frosted.is_some(); + let new_blur = + WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { + let t = new_theme.cosmic(); + match self.app.core().app_type() { + crate::core::AppType::Window => t.frosted_windows, + crate::core::AppType::System => t.frosted_system_interface, + crate::core::AppType::Applet => t.frosted_applets, + } + }; + if !new_blur { + new_theme = new_theme.into_opaque(); + } + cosmic_theme.set_theme(new_theme.theme_type); #[cfg(all(feature = "wayland", target_os = "linux"))] if self.app.core().sync_window_border_radii_to_theme() { @@ -1181,7 +1201,7 @@ impl Cosmic { if changed { core.theme_sub_counter += 1; - let new_theme = if is_dark { + let mut new_theme = if is_dark { crate::theme::system_dark() } else { crate::theme::system_light() @@ -1195,7 +1215,21 @@ impl Cosmic { let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); if core.auto_blur { - let blur = if new_theme.cosmic().frosted.is_some() { + let new_blur = + WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { + let t = new_theme.cosmic(); + match self.app.core().app_type() { + crate::core::AppType::Window => t.frosted_windows, + crate::core::AppType::System => { + t.frosted_system_interface + } + crate::core::AppType::Applet => t.frosted_applets, + } + }; + if !new_blur { + new_theme = new_theme.into_opaque(); + } + let blur = if new_blur { iced::window::enable_blur } else { iced::window::disable_blur @@ -1316,7 +1350,7 @@ impl Cosmic { use iced_runtime::platform_specific::wayland::CornerRadius; use iced_winit::platform_specific::commands::corner_radius::corner_radius; - let theme = THEME.lock().unwrap(); + let mut 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 { @@ -1328,8 +1362,19 @@ impl Cosmic { // TODO do we need per window sharp corners? let rounded = !self.app.core().window.sharp_corners; let core = self.app.core(); + let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { + let t = theme.cosmic(); + match self.app.core().app_type() { + crate::core::AppType::Window => t.frosted_windows, + crate::core::AppType::System => t.frosted_system_interface, + crate::core::AppType::Applet => t.frosted_applets, + } + }; + if !new_blur { + *theme = theme.into_opaque(); + } let blur_cmd = if core.auto_blur { - let blur = if t.frosted.is_some() { + let blur = if new_blur { iced::window::enable_blur } else { iced::window::disable_blur @@ -1343,6 +1388,7 @@ impl Cosmic { } else { Task::none() }; + let t = theme.cosmic(); return Task::batch([ blur_cmd, corner_radius( @@ -1365,7 +1411,45 @@ impl Cosmic { } return iced_runtime::window::run_with_handle(id, init_windowing_system); } - _ => {} + Action::WindowingSystemInitialized => { + let core = self.app.core(); + let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { + let t = core.system_theme.cosmic(); + match self.app.core().app_type() { + crate::core::AppType::Window => t.frosted_windows, + crate::core::AppType::System => t.frosted_system_interface, + crate::core::AppType::Applet => t.frosted_applets, + } + }; + let mut t = THEME.lock().unwrap(); + + if let ThemeType::System { prefer_dark, theme } = &t.theme_type + && new_blur + { + let mut reloaded = if theme.is_dark { + crate::theme::system_dark() + } else { + crate::theme::system_light() + }; + reloaded.theme_type.prefer_dark(*prefer_dark); + *t = reloaded; + } + if core.auto_blur { + let blur = if new_blur { + iced::window::enable_blur + } else { + iced::window::disable_blur + }; + let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); + if let Some(main_id) = core.main_window_id() { + cmds.push(blur(main_id)); + } + for id in &self.tracked_windows { + cmds.push(blur(*id)); + } + return Task::batch(cmds); + } + } } iced::Task::none() diff --git a/src/applet/mod.rs b/src/applet/mod.rs index e16b0303..56cef05e 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -372,9 +372,11 @@ impl Context { Container::::new(content).style(|theme| { let cosmic = theme.cosmic(); let corners = cosmic.corner_radii; + let mut bg = cosmic.background.base; + bg.alpha = (bg.alpha + if cosmic.is_dark { 0.6 } else { 0.5 }).min(1.); iced_widget::container::Style { text_color: Some(cosmic.background.on.into()), - background: Some(Color::from(cosmic.background.base).into()), + background: Some(Color::from(bg).into()), border: iced::Border { radius: corners.radius_m.into(), width: 1.0, @@ -559,6 +561,7 @@ pub fn run(flags: App::Flags) -> iced::Result { core.window.show_maximize = false; core.window.show_minimize = false; core.window.use_template = false; + core.app_type = crate::core::AppType::Applet; window_settings.decorations = false; window_settings.exit_on_close_request = true; diff --git a/src/core.rs b/src/core.rs index 2ea0890b..f1c3946d 100644 --- a/src/core.rs +++ b/src/core.rs @@ -103,6 +103,18 @@ pub struct Core { pub(crate) sync_window_border_radii_to_theme: bool, pub(crate) auto_blur: bool, + + pub(crate) app_type: AppType, +} + +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum AppType { + /// A regular application + Window, + /// A system application + System, + /// An applet + Applet, } impl Default for Core { @@ -164,6 +176,7 @@ impl Default for Core { #[cfg(all(feature = "wayland", target_os = "linux"))] sync_window_border_radii_to_theme: true, auto_blur: true, + app_type: AppType::Window, } } } @@ -509,4 +522,16 @@ impl Core { pub fn set_auto_blur(&mut self, auto_blur: bool) { self.auto_blur = auto_blur; } + + pub fn auto_blur(&self) -> bool { + self.auto_blur + } + + pub fn set_app_type(&mut self, app_type: AppType) { + self.app_type = app_type; + } + + pub fn app_type(&self) -> AppType { + self.app_type + } } diff --git a/src/theme/mod.rs b/src/theme/mod.rs index a5ae5d4a..9af597e2 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -290,6 +290,29 @@ impl Theme { pub fn set_theme(&mut self, theme: ThemeType) { self.theme_type = theme; } + + pub fn into_opaque(&self) -> Self { + let mut new_theme = Theme { + theme_type: match &self.theme_type { + ThemeType::System { theme, prefer_dark } => { + let mut new_t = (**theme).clone(); + new_t.background.base.alpha = 1.0; + new_t.primary.base.alpha = 1.0; + new_t.secondary.base.alpha = 1.0; + ThemeType::System { + theme: Arc::new(new_t), + prefer_dark: *prefer_dark, + } + } + other => other.clone(), + }, + layer: self.layer, + }; + let cosmic = new_theme.cosmic(); + // copy theme but make all container colors opaque + + new_theme + } } impl LayeredTheme for Theme { diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 84d9b8c0..662facb8 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -477,7 +477,21 @@ impl iced_container::Catalog for Theme { let window_corner_radius = cosmic.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); match class { - Container::Transparent => iced_container::Style::default(), + Container::Transparent => { + let component = &self.current_container().component; + + iced_container::Style { + icon_color: Some(component.on.into()), + text_color: Some(component.on.into()), + background: None, + border: Border { + radius: 0.into(), + ..Default::default() + }, + shadow: Shadow::default(), + snap: true, + } + } Container::Custom(f) => f(self), @@ -565,7 +579,7 @@ impl iced_container::Catalog for Theme { Container::ContextDrawer => { let mut a = Container::primary(cosmic); if let Some(Background::Color(ref mut color)) = a.background { - color.a = 1.; + color.a = (color.a + if cosmic.is_dark { 0.60 } else { 0.5 }).min(1.); } if cosmic.is_high_contrast { diff --git a/src/theme/style/menu_bar.rs b/src/theme/style/menu_bar.rs index db6e0ba1..5447d9e2 100644 --- a/src/theme/style/menu_bar.rs +++ b/src/theme/style/menu_bar.rs @@ -65,10 +65,12 @@ impl StyleSheet for Theme { fn appearance(&self, style: &Self::Style) -> Appearance { let cosmic = self.cosmic(); let component = &cosmic.background.component; + let mut bg = component.base; + bg.alpha = (bg.alpha + if cosmic.is_dark { 0.6 } else { 0.5 }).min(1.); match style { MenuBarStyle::Default => Appearance { - background: component.base.into(), + background: bg.into(), border_width: 1.0, bar_border_radius: cosmic.corner_radii.radius_xl, menu_border_radius: cosmic.corner_radii.radius_s, From 19142fd38e3df81bbe8f3f37b6ad6d79a0131ddd Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 15 Apr 2026 15:06:07 -0400 Subject: [PATCH 071/201] refactor opaque fallback --- cosmic-theme/src/model/theme.rs | 177 +++++++++++++++++++++++++++++--- src/app/cosmic.rs | 99 +++++++++--------- src/app/mod.rs | 4 +- src/applet/mod.rs | 10 +- src/theme/mod.rs | 31 +----- src/theme/style/button.rs | 19 ++-- src/theme/style/dropdown.rs | 18 +++- src/theme/style/iced.rs | 118 +++++++++++++-------- src/theme/style/menu_bar.rs | 2 +- src/widget/card/style.rs | 22 ++-- src/widget/menu/menu_tree.rs | 2 +- src/widget/nav_bar.rs | 4 +- 12 files changed, 345 insertions(+), 161 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 87b7269c..730c00f8 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -51,11 +51,19 @@ pub struct Theme { /// name of the theme pub name: String, /// background element colors - pub background: Container, + pub(crate) background: Container, /// primary element colors - pub primary: Container, + pub(crate) primary: Container, /// secondary element colors - pub secondary: Container, + pub(crate) secondary: Container, + /// background element colors + pub(crate) transparent_background: Container, + /// primary element colors + pub(crate) transparent_primary: Container, + /// secondary element colors + pub(crate) transparent_secondary: Container, + /// button component styling + pub button: Component, /// accent element colors pub accent: Component, /// suggested element colors @@ -80,8 +88,6 @@ pub struct Theme { pub list_button: Component, /// text button element colors pub text_button: Component, - /// button component styling - pub button: Component, /// palette pub palette: CosmicPaletteInner, /// spacing @@ -189,6 +195,39 @@ impl Theme { todo!(); } + #[allow(clippy::doc_markdown)] + #[inline] + /// get opaque or transparent background based on whether blur is active + pub fn background(&self, transparent: bool) -> &Container { + if transparent { + &self.transparent_background + } else { + &self.background + } + } + + #[allow(clippy::doc_markdown)] + #[inline] + /// get opaque or transparent primary based on whether blur is active + pub fn primary(&self, transparent: bool) -> &Container { + if transparent { + &self.transparent_primary + } else { + &self.primary + } + } + + #[allow(clippy::doc_markdown)] + #[inline] + /// get opaque or transparent secondary based on whether blur is active + pub fn secondary(&self, transparent: bool) -> &Container { + if transparent { + &self.transparent_secondary + } else { + &self.secondary + } + } + #[must_use] #[allow(clippy::doc_markdown)] #[inline] @@ -1076,17 +1115,19 @@ impl ThemeBuilder { NonZeroUsize::new(100).unwrap(), ); - let mut bg = if let Some(bg_color) = bg_color { + let bg = if let Some(bg_color) = bg_color { bg_color } else { p_ref.gray_1 }; - bg.alpha = container_alpha; - let step_array = steps(bg, NonZeroUsize::new(100).unwrap()); let bg_index = color_index(bg, step_array.len()); + let mut transparent_bg = bg; + transparent_bg.alpha = container_alpha; + let transparent_bg_steps_array = steps(transparent_bg, NonZeroUsize::new(100).unwrap()); + let mut component_hovered_overlay = if bg_index < 91 { control_steps_array[10] } else { @@ -1113,13 +1154,26 @@ impl ThemeBuilder { text_steps_array.as_deref(), ); + let transparent_bg_component = get_surface_color( + bg_index, + 8, + &transparent_bg_steps_array, + is_dark, + &p_ref.neutral_2, + ); + let on_transparent_bg_component = get_text( + color_index(transparent_bg_component, transparent_bg_steps_array.len()), + &transparent_bg_steps_array, + &control_steps_array[8], + text_steps_array.as_deref(), + ); + let primary = { let mut 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, &control_steps_array[1]) }; - container_bg.alpha = (container_alpha + if is_dark { 0.3 } else { 0.25 }).min(1.0); let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); let base_index: usize = color_index(container_bg, step_array.len()); @@ -1162,6 +1216,45 @@ impl ThemeBuilder { is_high_contrast, ) }; + let transparent_primary = { + let mut 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, &control_steps_array[1]) + }; + container_bg.alpha = (container_alpha + if is_dark { 0.3 } else { 0.25 }).min(1.0); + + 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]); + + Container::new( + Component::component( + component_base, + accent, + get_text( + color_index(component_base, step_array.len()), + &step_array, + &control_steps_array[8], + text_steps_array.as_deref(), + ), + Srgba::new(0., 0., 0., 0.0), + Srgba::new(0., 0., 0., 0.0), + is_high_contrast, + control_steps_array[8], + ), + container_bg, + get_text( + base_index, + &step_array, + &control_steps_array[8], + text_steps_array.as_deref(), + ), + get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), + is_high_contrast, + ) + }; let accent_text = if is_dark { (primary.base.relative_contrast(accent.color) < 4.).then(|| { @@ -1236,13 +1329,11 @@ impl ThemeBuilder { ), primary, secondary: { - let mut container_bg = if let Some(secondary_container_bg) = secondary_container_bg - { + 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, &control_steps_array[2]) }; - container_bg.alpha = (container_alpha + if is_dark { 0.6 } else { 0.5 }).min(1.0); let step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); let base_index = color_index(container_bg, step_array.len()); @@ -1411,6 +1502,67 @@ impl ThemeBuilder { frosted_system_interface, frosted_panel, frosted_applets, + transparent_background: Container::new( + Component::component( + transparent_bg_component, + accent, + on_transparent_bg_component, + Srgba::new(0., 0., 0., 0.0), + Srgba::new(0., 0., 0., 0.0), + is_high_contrast, + control_steps_array[8], + ), + transparent_bg, + get_text( + bg_index, + &transparent_bg_steps_array, + &control_steps_array[8], + text_steps_array.as_deref(), + ), + get_small_widget_color(bg_index, 5, &neutral_steps, &control_steps_array[6]), + is_high_contrast, + ), + transparent_primary, + transparent_secondary: { + let mut 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, &control_steps_array[2]) + }; + container_bg.alpha = (container_alpha + if is_dark { 0.6 } else { 0.5 }).min(1.0); + + 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]); + + Container::new( + Component::component( + secondary_component, + accent, + get_text( + color_index(secondary_component, step_array.len()), + &step_array, + &control_steps_array[8], + text_steps_array.as_deref(), + ), + Srgba::new(0., 0., 0., 0.0), + Srgba::new(0., 0., 0., 0.0), + is_high_contrast, + control_steps_array[8], + ), + container_bg, + get_text( + base_index, + &step_array, + &control_steps_array[8], + text_steps_array.as_deref(), + ), + get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), + is_high_contrast, + ) + }, }; theme.spacing = spacing; theme.corner_radii = corner_radii; @@ -1432,6 +1584,7 @@ impl ThemeBuilder { /// Actual blur radius is decided by cosmic-comp, /// but this represents the strength of the blur effect. +#[allow(missing_docs)] #[repr(u8)] #[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum BlurStrength { diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 88ff025a..e09ee958 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -784,10 +784,11 @@ impl Cosmic { crate::core::AppType::Applet => t.frosted_applets, } }; - if !new_blur { - theme = theme.into_opaque(); - } - THEME.lock().unwrap().set_theme(theme.theme_type); + theme.transparent = new_blur; + let mut guard = THEME.lock().unwrap(); + guard.set_theme(theme.theme_type); + guard.transparent = new_blur; + drop(guard); let core = self.app.core(); if core.auto_blur { @@ -810,12 +811,23 @@ impl Cosmic { } } - Action::SystemThemeChange(keys, theme) => { + Action::SystemThemeChange(keys, mut theme) => { let cur_is_dark = THEME.lock().unwrap().theme_type.is_dark(); // Ignore updates if the current theme mode does not match. if cur_is_dark != theme.cosmic().is_dark { return iced::Task::none(); } + // update transparent + let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { + let t = theme.cosmic(); + match self.app.core().app_type() { + crate::core::AppType::Window => t.frosted_windows, + crate::core::AppType::System => t.frosted_system_interface, + crate::core::AppType::Applet => t.frosted_applets, + } + }; + theme.transparent = new_blur; + let cmd = self.app.system_theme_update(&keys, theme.cosmic()); // Record the last-known system theme in event that the current theme is custom. self.app.core_mut().system_theme = theme.clone(); @@ -839,11 +851,12 @@ impl Cosmic { } else { theme }; + new_theme.transparent = new_blur; new_theme.theme_type.prefer_dark(prefer_dark); - // TODO adjust theme container alphas to remove transparency? - // if auto-blur is disabled & theme is frosted, should we make container colors in theme opaque? cosmic_theme.set_theme(new_theme.theme_type); + cosmic_theme.transparent = new_blur; + #[cfg(all(feature = "wayland", target_os = "linux"))] if self.app.core().sync_window_border_radii_to_theme() { use iced_runtime::platform_specific::wayland::CornerRadius; @@ -954,6 +967,7 @@ impl Cosmic { } { return iced::Task::none(); } + let mut cmds = vec![self.app.system_theme_mode_update(&keys, &mode)]; let core = self.app.core_mut(); @@ -982,6 +996,15 @@ impl Cosmic { } else { new_theme }; + let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { + let t = new_theme.cosmic(); + match core.app_type() { + crate::core::AppType::Window => t.frosted_windows, + crate::core::AppType::System => t.frosted_system_interface, + crate::core::AppType::Applet => t.frosted_applets, + } + }; + new_theme.transparent = new_blur; core.system_theme = new_theme.clone(); { @@ -989,20 +1012,8 @@ impl Cosmic { // Only apply update if the theme is set to load a system theme if let ThemeType::System { .. } = cosmic_theme.theme_type { - let new_blur = - WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { - let t = new_theme.cosmic(); - match self.app.core().app_type() { - crate::core::AppType::Window => t.frosted_windows, - crate::core::AppType::System => t.frosted_system_interface, - crate::core::AppType::Applet => t.frosted_applets, - } - }; - if !new_blur { - new_theme = new_theme.into_opaque(); - } - cosmic_theme.set_theme(new_theme.theme_type); + cosmic_theme.transparent = new_blur; #[cfg(all(feature = "wayland", target_os = "linux"))] if self.app.core().sync_window_border_radii_to_theme() { use iced_runtime::platform_specific::wayland::CornerRadius; @@ -1087,7 +1098,7 @@ impl Cosmic { let core = self.app.core(); if core.auto_blur { - let blur = if new_blur { + let blur = if cosmic_theme.transparent { iced::window::enable_blur } else { iced::window::disable_blur @@ -1206,6 +1217,18 @@ impl Cosmic { } else { crate::theme::system_light() }; + if let ThemeType::System { .. } = new_theme.theme_type { + let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) + && { + let t = new_theme.cosmic(); + match core.app_type() { + crate::core::AppType::Window => t.frosted_windows, + crate::core::AppType::System => t.frosted_system_interface, + crate::core::AppType::Applet => t.frosted_applets, + } + }; + new_theme.transparent = new_blur; + } core.system_theme = new_theme.clone(); { let mut cosmic_theme = THEME.lock().unwrap(); @@ -1215,21 +1238,7 @@ impl Cosmic { let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); if core.auto_blur { - let new_blur = - WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { - let t = new_theme.cosmic(); - match self.app.core().app_type() { - crate::core::AppType::Window => t.frosted_windows, - crate::core::AppType::System => { - t.frosted_system_interface - } - crate::core::AppType::Applet => t.frosted_applets, - } - }; - if !new_blur { - new_theme = new_theme.into_opaque(); - } - let blur = if new_blur { + let blur = if cosmic_theme.transparent { iced::window::enable_blur } else { iced::window::disable_blur @@ -1370,9 +1379,7 @@ impl Cosmic { crate::core::AppType::Applet => t.frosted_applets, } }; - if !new_blur { - *theme = theme.into_opaque(); - } + theme.transparent = new_blur; let blur_cmd = if core.auto_blur { let blur = if new_blur { iced::window::enable_blur @@ -1412,6 +1419,7 @@ impl Cosmic { return iced_runtime::window::run_with_handle(id, init_windowing_system); } Action::WindowingSystemInitialized => { + // TODO do this after blur event confirms support instead of for all wayland windows let core = self.app.core(); let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { let t = core.system_theme.cosmic(); @@ -1423,17 +1431,8 @@ impl Cosmic { }; let mut t = THEME.lock().unwrap(); - if let ThemeType::System { prefer_dark, theme } = &t.theme_type - && new_blur - { - let mut reloaded = if theme.is_dark { - crate::theme::system_dark() - } else { - crate::theme::system_light() - }; - reloaded.theme_type.prefer_dark(*prefer_dark); - *t = reloaded; - } + t.transparent = matches!(&t.theme_type, ThemeType::System { .. }) && new_blur; + if core.auto_blur { let blur = if new_blur { iced::window::enable_blur diff --git a/src/app/mod.rs b/src/app/mod.rs index 264f1d16..e2d6ae1c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -825,7 +825,7 @@ impl ApplicationExt for App { let cosmic = theme.cosmic(); container::Style { background: Some(iced::Background::Color( - cosmic.background.base.into(), + cosmic.background(theme.transparent).base.into(), )), border: iced::Border { radius: [ @@ -854,7 +854,7 @@ impl ApplicationExt for App { container::Style { background: if content_container { Some(iced::Background::Color( - theme.cosmic().background.base.into(), + theme.cosmic().background(theme.transparent).base.into(), )) } else { None diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 56cef05e..897ea7ca 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -221,7 +221,7 @@ impl Context { let icon = widget::icon(icon) .class(if symbolic { theme::Svg::Custom(Rc::new(|theme| iced_widget::svg::Style { - color: Some(theme.cosmic().background.on.into()), + color: Some(theme.cosmic().background(theme.transparent).on.into()), })) } else { theme::Svg::default() @@ -372,18 +372,18 @@ impl Context { Container::::new(content).style(|theme| { let cosmic = theme.cosmic(); let corners = cosmic.corner_radii; - let mut bg = cosmic.background.base; + let mut bg = cosmic.background(theme.transparent).base; bg.alpha = (bg.alpha + if cosmic.is_dark { 0.6 } else { 0.5 }).min(1.); iced_widget::container::Style { - text_color: Some(cosmic.background.on.into()), + text_color: Some(cosmic.background(theme.transparent).on.into()), background: Some(Color::from(bg).into()), border: iced::Border { radius: corners.radius_m.into(), width: 1.0, - color: cosmic.background.divider.into(), + color: cosmic.background(theme.transparent).divider.into(), }, shadow: Shadow::default(), - icon_color: Some(cosmic.background.on.into()), + icon_color: Some(cosmic.background(theme.transparent).on.into()), snap: true, } }), diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 9af597e2..07c5f874 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -46,6 +46,7 @@ pub static TRANSPARENT_COMPONENT: LazyLock = LazyLock::new(|| Compone pub(crate) static THEME: Mutex = Mutex::new(Theme { theme_type: ThemeType::Dark, layer: cosmic_theme::Layer::Background, + transparent: false, }); /// Currently-defined theme. @@ -209,6 +210,7 @@ impl ThemeType { pub struct Theme { pub theme_type: ThemeType, pub layer: cosmic_theme::Layer, + pub transparent: bool, } impl Theme { @@ -279,9 +281,9 @@ impl Theme { /// can be used in a component that is intended to be a child of a `CosmicContainer` pub fn current_container(&self) -> &cosmic_theme::Container { match self.layer { - cosmic_theme::Layer::Background => &self.cosmic().background, - cosmic_theme::Layer::Primary => &self.cosmic().primary, - cosmic_theme::Layer::Secondary => &self.cosmic().secondary, + cosmic_theme::Layer::Background => &self.cosmic().background(self.transparent), + cosmic_theme::Layer::Primary => &self.cosmic().primary(self.transparent), + cosmic_theme::Layer::Secondary => &self.cosmic().secondary(self.transparent), } } @@ -290,29 +292,6 @@ impl Theme { pub fn set_theme(&mut self, theme: ThemeType) { self.theme_type = theme; } - - pub fn into_opaque(&self) -> Self { - let mut new_theme = Theme { - theme_type: match &self.theme_type { - ThemeType::System { theme, prefer_dark } => { - let mut new_t = (**theme).clone(); - new_t.background.base.alpha = 1.0; - new_t.primary.base.alpha = 1.0; - new_t.secondary.base.alpha = 1.0; - ThemeType::System { - theme: Arc::new(new_t), - prefer_dark: *prefer_dark, - } - } - other => other.clone(), - }, - layer: self.layer, - }; - let cosmic = new_theme.cosmic(); - // copy theme but make all container colors opaque - - new_theme - } } impl LayeredTheme for Theme { diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index f84c9e97..723d1b6b 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -126,20 +126,20 @@ pub fn appearance( let (background, _, _) = color(&cosmic.text_button); appearance.background = Some(Background::Color(background)); - appearance.icon_color = Some(cosmic.background.on.into()); - appearance.text_color = Some(cosmic.background.on.into()); + appearance.icon_color = Some(cosmic.background(theme.transparent).on.into()); + appearance.text_color = Some(cosmic.background(theme.transparent).on.into()); corner_radii = &cosmic.corner_radii.radius_0; } Button::AppletIcon => { let (background, _, _) = color(&cosmic.text_button); appearance.background = Some(Background::Color(background)); - appearance.icon_color = Some(cosmic.background.on.into()); - appearance.text_color = Some(cosmic.background.on.into()); + appearance.icon_color = Some(cosmic.background(theme.transparent).on.into()); + appearance.text_color = Some(cosmic.background(theme.transparent).on.into()); } Button::MenuFolder => { // Menu folders cannot be disabled, ignore customized icon and text color - let component = &cosmic.background.component; + let component = &cosmic.background(theme.transparent).component; let (background, _, _) = color(component); appearance.background = Some(Background::Color(background)); appearance.icon_color = Some(component.on.into()); @@ -151,8 +151,9 @@ pub fn appearance( let (background, text, icon) = color(&cosmic.list_button); if selected { - appearance.background = - Some(Background::Color(cosmic.primary.component.hover.into())); + appearance.background = Some(Background::Color( + cosmic.primary(theme.transparent).component.hover.into(), + )); appearance.icon_color = Some(cosmic.accent.base.into()); appearance.text_color = Some(cosmic.accent_text_color().into()); } else { @@ -162,7 +163,7 @@ pub fn appearance( } } Button::MenuItem => { - let (background, text, icon) = color(&cosmic.background.component); + let (background, text, icon) = color(&cosmic.background(theme.transparent).component); appearance.background = Some(Background::Color(background)); appearance.icon_color = icon; appearance.text_color = text; @@ -278,6 +279,6 @@ impl Catalog for crate::Theme { } fn selection_background(&self) -> Background { - Background::Color(self.cosmic().primary.base.into()) + Background::Color(self.cosmic().primary(self.transparent).base.into()) } } diff --git a/src/theme/style/dropdown.rs b/src/theme/style/dropdown.rs index cc89a399..56f5536e 100644 --- a/src/theme/style/dropdown.rs +++ b/src/theme/style/dropdown.rs @@ -13,18 +13,28 @@ impl dropdown::menu::StyleSheet for Theme { dropdown::menu::Appearance { text_color: cosmic.on_bg_color().into(), - background: Background::Color(cosmic.background.component.base.into()), + background: Background::Color( + cosmic.background(self.transparent).component.base.into(), + ), border_width: 0.0, border_radius: cosmic.corner_radii.radius_m.into(), border_color: Color::TRANSPARENT, hovered_text_color: cosmic.on_bg_color().into(), - hovered_background: Background::Color(cosmic.primary.component.hover.into()), + hovered_background: Background::Color( + cosmic.primary(self.transparent).component.hover.into(), + ), selected_text_color: cosmic.accent_text_color().into(), - selected_background: Background::Color(cosmic.primary.component.hover.into()), + selected_background: Background::Color( + cosmic.primary(self.transparent).component.hover.into(), + ), - description_color: cosmic.primary.component.on_disabled.into(), + description_color: cosmic + .primary(self.transparent) + .component + .on_disabled + .into(), } } } diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 662facb8..a18068a4 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -227,11 +227,11 @@ impl iced_checkbox::Catalog for Theme { }, Checkbox::Secondary => iced_checkbox::Style { background: Background::Color(if is_checked { - cosmic.background.component.base.into() + cosmic.background(self.transparent).component.base.into() } else { self.current_container().small_widget.into() }), - icon_color: cosmic.background.on.into(), + icon_color: cosmic.background(self.transparent).on.into(), border: Border { radius: corners.radius_xs.into(), width: if is_checked { 0.0 } else { 1.0 }, @@ -412,11 +412,13 @@ impl<'a> Container<'a> { } #[must_use] - pub fn background(theme: &cosmic_theme::Theme) -> iced_container::Style { + pub fn background(theme: &cosmic_theme::Theme, transparent: bool) -> iced_container::Style { iced_container::Style { - icon_color: Some(Color::from(theme.background.on)), - text_color: Some(Color::from(theme.background.on)), - background: Some(iced::Background::Color(theme.background.base.into())), + icon_color: Some(Color::from(theme.background(transparent).on)), + text_color: Some(Color::from(theme.background(transparent).on)), + background: Some(iced::Background::Color( + theme.background(transparent).base.into(), + )), border: Border { radius: theme.corner_radii.radius_s.into(), ..Default::default() @@ -427,11 +429,13 @@ impl<'a> Container<'a> { } #[must_use] - pub fn primary(theme: &cosmic_theme::Theme) -> iced_container::Style { + pub fn primary(theme: &cosmic_theme::Theme, transparent: bool) -> iced_container::Style { iced_container::Style { - icon_color: Some(Color::from(theme.primary.on)), - text_color: Some(Color::from(theme.primary.on)), - background: Some(iced::Background::Color(theme.primary.base.into())), + icon_color: Some(Color::from(theme.primary(transparent).on)), + text_color: Some(Color::from(theme.primary(transparent).on)), + background: Some(iced::Background::Color( + theme.primary(transparent).base.into(), + )), border: Border { radius: theme.corner_radii.radius_s.into(), ..Default::default() @@ -442,11 +446,13 @@ impl<'a> Container<'a> { } #[must_use] - pub fn secondary(theme: &cosmic_theme::Theme) -> iced_container::Style { + pub fn secondary(theme: &cosmic_theme::Theme, transparent: bool) -> iced_container::Style { iced_container::Style { - icon_color: Some(Color::from(theme.secondary.on)), - text_color: Some(Color::from(theme.secondary.on)), - background: Some(iced::Background::Color(theme.secondary.base.into())), + icon_color: Some(Color::from(theme.secondary(transparent).on)), + text_color: Some(Color::from(theme.secondary(transparent).on)), + background: Some(iced::Background::Color( + theme.secondary(transparent).base.into(), + )), border: Border { radius: theme.corner_radii.radius_s.into(), ..Default::default() @@ -496,9 +502,11 @@ impl iced_container::Catalog for Theme { Container::Custom(f) => f(self), Container::WindowBackground => iced_container::Style { - icon_color: Some(Color::from(cosmic.background.on)), - text_color: Some(Color::from(cosmic.background.on)), - background: Some(iced::Background::Color(cosmic.background.base.into())), + icon_color: Some(Color::from(cosmic.background(self.transparent).on)), + text_color: Some(Color::from(cosmic.background(self.transparent).on)), + background: Some(iced::Background::Color( + cosmic.background(self.transparent).base.into(), + )), border: Border { radius: [ cosmic.corner_radii.radius_0[0], @@ -536,12 +544,13 @@ impl iced_container::Catalog for Theme { let (icon_color, text_color) = if *focused { ( Color::from(cosmic.accent_text_color()), - Color::from(cosmic.background.on), + Color::from(cosmic.background(self.transparent).on), ) } else { use crate::ext::ColorExt; - let unfocused_color = Color::from(cosmic.background.component.on) - .blend_alpha(cosmic.background.base.into(), 0.5); + let unfocused_color = + Color::from(cosmic.background(self.transparent).component.on) + .blend_alpha(cosmic.background(self.transparent).base.into(), 0.5); (unfocused_color, unfocused_color) }; @@ -551,7 +560,9 @@ impl iced_container::Catalog for Theme { background: if *transparent { None } else { - Some(iced::Background::Color(cosmic.background.base.into())) + Some(iced::Background::Color( + cosmic.background(self.transparent).base.into(), + )) }, border: Border { radius: [ @@ -577,23 +588,23 @@ impl iced_container::Catalog for Theme { } Container::ContextDrawer => { - let mut a = Container::primary(cosmic); + let mut a = Container::primary(cosmic, self.transparent); if let Some(Background::Color(ref mut color)) = a.background { color.a = (color.a + if cosmic.is_dark { 0.60 } else { 0.5 }).min(1.); } if cosmic.is_high_contrast { a.border.width = 1.; - a.border.color = cosmic.primary.divider.into(); + a.border.color = cosmic.primary(self.transparent).divider.into(); } a } - Container::Background => Container::background(cosmic), + Container::Background => Container::background(cosmic, self.transparent), - Container::Primary => Container::primary(cosmic), + Container::Primary => Container::primary(cosmic, self.transparent), - Container::Secondary => Container::secondary(cosmic), + Container::Secondary => Container::secondary(cosmic, self.transparent), Container::Dropdown => iced_container::Style { icon_color: None, @@ -625,10 +636,14 @@ impl iced_container::Catalog for Theme { match self.layer { cosmic_theme::Layer::Background => iced_container::Style { - icon_color: Some(Color::from(cosmic.background.component.on)), - text_color: Some(Color::from(cosmic.background.component.on)), + icon_color: Some(Color::from( + cosmic.background(self.transparent).component.on, + )), + text_color: Some(Color::from( + cosmic.background(self.transparent).component.on, + )), background: Some(iced::Background::Color( - cosmic.background.component.base.into(), + cosmic.background(self.transparent).component.base.into(), )), border: Border { radius: cosmic.corner_radii.radius_s.into(), @@ -638,10 +653,14 @@ impl iced_container::Catalog for Theme { snap: true, }, cosmic_theme::Layer::Primary => iced_container::Style { - icon_color: Some(Color::from(cosmic.primary.component.on)), - text_color: Some(Color::from(cosmic.primary.component.on)), + icon_color: Some(Color::from( + cosmic.primary(self.transparent).component.on, + )), + text_color: Some(Color::from( + cosmic.primary(self.transparent).component.on, + )), background: Some(iced::Background::Color( - cosmic.primary.component.base.into(), + cosmic.primary(self.transparent).component.base.into(), )), border: Border { radius: cosmic.corner_radii.radius_s.into(), @@ -651,10 +670,14 @@ impl iced_container::Catalog for Theme { snap: true, }, cosmic_theme::Layer::Secondary => iced_container::Style { - icon_color: Some(Color::from(cosmic.secondary.component.on)), - text_color: Some(Color::from(cosmic.secondary.component.on)), + icon_color: Some(Color::from( + cosmic.secondary(self.transparent).component.on, + )), + text_color: Some(Color::from( + cosmic.secondary(self.transparent).component.on, + )), background: Some(iced::Background::Color( - cosmic.secondary.component.base.into(), + cosmic.secondary(self.transparent).component.base.into(), )), border: Border { radius: cosmic.corner_radii.radius_s.into(), @@ -667,11 +690,13 @@ impl iced_container::Catalog for Theme { } Container::Dialog => iced_container::Style { - icon_color: Some(Color::from(cosmic.primary.on)), - text_color: Some(Color::from(cosmic.primary.on)), - background: Some(iced::Background::Color(cosmic.primary.base.into())), + icon_color: Some(Color::from(cosmic.primary(self.transparent).on)), + text_color: Some(Color::from(cosmic.primary(self.transparent).on)), + background: Some(iced::Background::Color( + cosmic.primary(self.transparent).base.into(), + )), border: Border { - color: cosmic.primary.divider.into(), + color: cosmic.primary(self.transparent).divider.into(), width: 1.0, radius: cosmic.corner_radii.radius_m.into(), }, @@ -813,13 +838,15 @@ impl menu::Catalog for Theme { menu::Style { text_color: cosmic.on_bg_color().into(), - background: Background::Color(cosmic.background.base.into()), + background: Background::Color(cosmic.background(self.transparent).base.into()), border: Border { radius: cosmic.corner_radii.radius_m.into(), ..Default::default() }, selected_text_color: cosmic.accent_text_color().into(), - selected_background: Background::Color(cosmic.background.component.hover.into()), + selected_background: Background::Color( + cosmic.background(self.transparent).component.hover.into(), + ), shadow: Default::default(), } } @@ -857,7 +884,7 @@ impl pick_list::Catalog for Theme { match status { pick_list::Status::Active => appearance, pick_list::Status::Hovered => pick_list::Style { - background: Background::Color(cosmic.background.base.into()), + background: Background::Color(cosmic.background(self.transparent).base.into()), ..appearance }, pick_list::Status::Opened { is_hovered: _ } => appearance, @@ -1053,7 +1080,10 @@ impl progress_bar::Catalog for Theme { }, ) } else { - (theme.accent.base, theme.background.divider) + ( + theme.accent.base, + theme.background(self.transparent).divider, + ) }; let border = Border { radius: theme.corner_radii.radius_xl.into(), @@ -1533,7 +1563,7 @@ 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: Color = cosmic.background.on.into(); + let icon: Color = cosmic.background(self.transparent).on.into(); // TODO do we need to add icon color back? match status { diff --git a/src/theme/style/menu_bar.rs b/src/theme/style/menu_bar.rs index 5447d9e2..2a618f6b 100644 --- a/src/theme/style/menu_bar.rs +++ b/src/theme/style/menu_bar.rs @@ -64,7 +64,7 @@ impl StyleSheet for Theme { fn appearance(&self, style: &Self::Style) -> Appearance { let cosmic = self.cosmic(); - let component = &cosmic.background.component; + let component = &cosmic.background(self.transparent).component; let mut bg = component.base; bg.alpha = (bg.alpha + if cosmic.is_dark { 0.6 } else { 0.5 }).min(1.); diff --git a/src/widget/card/style.rs b/src/widget/card/style.rs index 0e63e846..d8e95277 100644 --- a/src/widget/card/style.rs +++ b/src/widget/card/style.rs @@ -31,16 +31,26 @@ impl crate::widget::card::style::Catalog for crate::Theme { match self.layer { cosmic_theme::Layer::Background => crate::widget::card::style::Style { - card_1: Background::Color(cosmic.background.component.hover.into()), - card_2: Background::Color(cosmic.background.component.pressed.into()), + card_1: Background::Color( + cosmic.background(self.transparent).component.hover.into(), + ), + card_2: Background::Color( + cosmic.background(self.transparent).component.pressed.into(), + ), }, cosmic_theme::Layer::Primary => crate::widget::card::style::Style { - card_1: Background::Color(cosmic.primary.component.hover.into()), - card_2: Background::Color(cosmic.primary.component.pressed.into()), + card_1: Background::Color(cosmic.primary(self.transparent).component.hover.into()), + card_2: Background::Color( + cosmic.primary(self.transparent).component.pressed.into(), + ), }, cosmic_theme::Layer::Secondary => crate::widget::card::style::Style { - card_1: Background::Color(cosmic.secondary.component.hover.into()), - card_2: Background::Color(cosmic.secondary.component.pressed.into()), + card_1: Background::Color( + cosmic.secondary(self.transparent).component.hover.into(), + ), + card_2: Background::Color( + cosmic.secondary(self.transparent).component.pressed.into(), + ), }, } } diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index efcf9800..c09151aa 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -230,7 +230,7 @@ pub fn menu_items< } fn key_style(theme: &crate::Theme) -> TextStyle { - let mut color = theme.cosmic().background.component.on; + let mut color = theme.cosmic().background(theme.transparent).component.on; color.alpha *= 0.75; TextStyle { color: Some(color.into()), diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index b7391abb..75809398 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -172,7 +172,9 @@ pub fn nav_bar_style(theme: &Theme) -> iced_widget::container::Style { iced_widget::container::Style { icon_color: Some(cosmic.on_bg_color().into()), text_color: Some(cosmic.on_bg_color().into()), - background: Some(Background::Color(cosmic.primary.base.into())), + background: Some(Background::Color( + cosmic.primary(theme.transparent).base.into(), + )), border: Border { width: 0.0, color: Color::TRANSPARENT, From cfd5220c1ebc36e73ba965ddea496819d9e019ed Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 16 Apr 2026 23:02:39 -0400 Subject: [PATCH 072/201] wip: corner radius v2 --- Cargo.toml | 4 +- src/app/cosmic.rs | 238 ++++++++++++++-------------------------------- src/core.rs | 39 ++++++++ 3 files changed, 115 insertions(+), 166 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3d42ed05..8e21f986 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,7 +127,7 @@ ashpd = { version = "0.12.3", default-features = false, optional = true } async-fs = { version = "2.2", optional = true } async-std = { workspace = true, optional = true } auto_enums = "0.8.8" -cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "c253ec1", optional = true } +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "a7d2d7a", optional = true } jiff = "0.2" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } @@ -256,7 +256,7 @@ serde = "1.0" thiserror = "2.0" tracing = "0.1" tokio = "1.52" -zbus = {version = "5.15", default-features = false} +zbus = { version = "5.15", default-features = false } [dev-dependencies] tempfile = "3.27.0" diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index e09ee958..e3731f77 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -6,6 +6,7 @@ use std::collections::{HashMap, HashSet}; use std::sync::Arc; use super::{Action, Application, ApplicationExt, Subscription}; +use crate::core::AppType; use crate::theme::{THEME, Theme, ThemeType}; use crate::{Core, Element, keyboard_nav}; #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -673,34 +674,13 @@ impl Cosmic { state.intersects(WindowState::MAXIMIZED | WindowState::FULLSCREEN); } 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([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()]); + + let cur_rad = self.app.core().app_type.corners(&theme, rounded); + return Task::batch([corner_radius(id, Some(cur_rad)).discard()]); } } @@ -864,79 +844,26 @@ impl Cosmic { 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_right: radii[2].round() as u32, - bottom_left: radii[3].round() as u32, - }; - let rounded = !self.app.core().window.sharp_corners; + + let cur_rad = self.app.core().app_type.corners(&cosmic_theme, rounded); + // 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, - 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(), - ]; + 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, - 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(), - ); - } + let cur_rad = corners(*surface_type, rounded, &cosmic_theme); + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); } // Update radius for all tracked windows for id in &self.tracked_windows { - 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(), - ); + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); } return Task::batch(cmds); @@ -1016,19 +943,13 @@ impl Cosmic { cosmic_theme.transparent = new_blur; #[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; 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_right: radii[2].round() as u32, - bottom_left: radii[3].round() as u32, - }; let rounded = !self.app.core().window.sharp_corners; + let cur_rad = + self.app.core().app_type.corners(&cosmic_theme, rounded); // Update radius for the main window let main_window_id = self @@ -1036,64 +957,16 @@ impl Cosmic { .core() .main_window_id() .unwrap_or(window::Id::RESERVED); - let mut cmds = vec![ - 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(), - ]; + 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, - 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(), - ); - } + let cur_rad = corners(*surface_type, rounded, &cosmic_theme); + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); } // Update radius for all tracked windows for id in &self.tracked_windows { - 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(), - ); + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); } let core = self.app.core(); @@ -1387,32 +1260,33 @@ impl Cosmic { iced::window::disable_blur }; let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); - cmds.push(blur(id)); - for id in &self.tracked_windows { - cmds.push(blur(*id)); + if !self.tracked_windows.contains(&id) { + cmds.push(blur(id)); } + Task::batch(cmds) } else { Task::none() }; + let corner_task = if let Some(s) = self.surface_views.get(&id) { + corner_radius(id, Some(corners(s.1, rounded, &theme))).discard() + } else if id + == self + .app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED) + || self.tracked_windows.contains(&id) + { + corner_radius(id, Some(self.app.core().app_type.corners(&theme, rounded))) + .discard() + } else { + Task::none() + }; let t = theme.cosmic(); return Task::batch([ blur_cmd, - 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(), + corner_task, iced_runtime::window::run_with_handle(id, init_windowing_system), ]); } @@ -1537,3 +1411,39 @@ impl Cosmic { .discard() } } + +#[cfg(all(feature = "wayland", target_os = "linux"))] +fn corners( + surface_type: SurfaceIdWrapper, + rounded: bool, + theme: &Theme, +) -> iced_runtime::platform_specific::wayland::CornerRadius { + let theme = theme.cosmic(); + if let SurfaceIdWrapper::Popup(_) = surface_type { + let radius_m = theme.radius_m(); + iced_runtime::platform_specific::wayland::CornerRadius { + top_left: radius_m[0].round() as u32, + top_right: radius_m[1].round() as u32, + bottom_right: radius_m[2].round() as u32, + bottom_left: radius_m[3].round() as u32, + } + } else if let SurfaceIdWrapper::Window(_) = surface_type + && rounded + { + let radius_0 = theme.radius_0(); + iced_runtime::platform_specific::wayland::CornerRadius { + top_left: radius_0[0].round() as u32, + top_right: radius_0[1].round() as u32, + bottom_right: radius_0[2].round() as u32, + bottom_left: radius_0[3].round() as u32, + } + } else { + let radius_s = theme.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); + iced_runtime::platform_specific::wayland::CornerRadius { + top_left: radius_s[0].round() as u32, + top_right: radius_s[1].round() as u32, + bottom_right: radius_s[2].round() as u32, + bottom_left: radius_s[3].round() as u32, + } + } +} diff --git a/src/core.rs b/src/core.rs index f1c3946d..e725547f 100644 --- a/src/core.rs +++ b/src/core.rs @@ -117,6 +117,45 @@ pub enum AppType { Applet, } +impl AppType { + /// Calculate suggested corners for each app type main window + #[cfg(all(feature = "wayland", target_os = "linux"))] + pub fn corners( + &self, + theme: &Theme, + rounded: bool, + ) -> iced_runtime::platform_specific::wayland::CornerRadius { + let theme = theme.cosmic(); + if let Self::Applet = self { + let radius_l = theme.radius_l(); + iced_runtime::platform_specific::wayland::CornerRadius { + top_left: radius_l[0].round() as u32, + top_right: radius_l[1].round() as u32, + bottom_right: radius_l[2].round() as u32, + bottom_left: radius_l[3].round() as u32, + } + } else if let Self::Window = self + && rounded + { + let radius_0 = theme.radius_0(); + iced_runtime::platform_specific::wayland::CornerRadius { + top_left: radius_0[0].round() as u32, + top_right: radius_0[1].round() as u32, + bottom_right: radius_0[2].round() as u32, + bottom_left: radius_0[3].round() as u32, + } + } else { + let radius_s = theme.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); + iced_runtime::platform_specific::wayland::CornerRadius { + top_left: radius_s[0].round() as u32, + top_right: radius_s[1].round() as u32, + bottom_right: radius_s[2].round() as u32, + bottom_left: radius_s[3].round() as u32, + } + } + } +} + impl Default for Core { fn default() -> Self { Self { From 9e7721074045021b7db526fc27ee1b827939d3ac Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 17 Apr 2026 13:42:51 -0400 Subject: [PATCH 073/201] fix: blur only after blur event --- src/app/action.rs | 2 ++ src/app/cosmic.rs | 56 ++++++++++++++++++++--------------------------- 2 files changed, 26 insertions(+), 32 deletions(-) diff --git a/src/app/action.rs b/src/app/action.rs index 166df16b..a3b9dd6a 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -64,6 +64,8 @@ pub enum Action { Unfocus(iced::window::Id), /// Windowing system initialized WindowingSystemInitialized, + /// Blur support enabled + BlurEnabled, /// 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 e3731f77..c22b8dd6 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -95,6 +95,7 @@ pub struct Cosmic { >, pub tracked_windows: HashSet, pub opened_surfaces: HashMap, + blur_enabled: bool, } impl Cosmic @@ -461,6 +462,9 @@ where )) => { return Some(Action::WindowState(id, s)); } + wayland::Event::BlurEnabled => { + return Some(Action::BlurEnabled); + } _ => (), } } @@ -756,7 +760,7 @@ impl Cosmic { } } - let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { + let new_blur = self.blur_enabled && { let t = theme.cosmic(); match self.app.core().app_type() { crate::core::AppType::Window => t.frosted_windows, @@ -798,7 +802,7 @@ impl Cosmic { return iced::Task::none(); } // update transparent - let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { + let new_blur = self.blur_enabled && { let t = theme.cosmic(); match self.app.core().app_type() { crate::core::AppType::Window => t.frosted_windows, @@ -839,7 +843,6 @@ impl Cosmic { #[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; let t = cosmic_theme.cosmic(); @@ -923,7 +926,7 @@ impl Cosmic { } else { new_theme }; - let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { + let new_blur = self.blur_enabled && { let t = new_theme.cosmic(); match core.app_type() { crate::core::AppType::Window => t.frosted_windows, @@ -945,8 +948,6 @@ impl Cosmic { if self.app.core().sync_window_border_radii_to_theme() { use iced_winit::platform_specific::commands::corner_radius::corner_radius; - let t = cosmic_theme.cosmic(); - let rounded = !self.app.core().window.sharp_corners; let cur_rad = self.app.core().app_type.corners(&cosmic_theme, rounded); @@ -1008,7 +1009,7 @@ impl Cosmic { #[allow(clippy::used_underscore_binding)] _token, ), - ) + ); } #[cfg(not(all(feature = "wayland", target_os = "linux")))] @@ -1091,15 +1092,14 @@ impl Cosmic { crate::theme::system_light() }; if let ThemeType::System { .. } = new_theme.theme_type { - let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) - && { - let t = new_theme.cosmic(); - match core.app_type() { - crate::core::AppType::Window => t.frosted_windows, - crate::core::AppType::System => t.frosted_system_interface, - crate::core::AppType::Applet => t.frosted_applets, - } - }; + let new_blur = self.blur_enabled && { + let t = new_theme.cosmic(); + match core.app_type() { + crate::core::AppType::Window => t.frosted_windows, + crate::core::AppType::System => t.frosted_system_interface, + crate::core::AppType::Applet => t.frosted_applets, + } + }; new_theme.transparent = new_blur; } core.system_theme = new_theme.clone(); @@ -1229,22 +1229,14 @@ impl Cosmic { Action::Opened(id) => { #[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; let mut 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, - }; + // TODO do we need per window sharp corners? let rounded = !self.app.core().window.sharp_corners; let core = self.app.core(); - let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { + let new_blur = self.blur_enabled && { let t = theme.cosmic(); match self.app.core().app_type() { crate::core::AppType::Window => t.frosted_windows, @@ -1260,9 +1252,7 @@ impl Cosmic { iced::window::disable_blur }; let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); - if !self.tracked_windows.contains(&id) { - cmds.push(blur(id)); - } + cmds.push(blur(id)); Task::batch(cmds) } else { @@ -1283,7 +1273,6 @@ impl Cosmic { } else { Task::none() }; - let t = theme.cosmic(); return Task::batch([ blur_cmd, corner_task, @@ -1292,10 +1281,11 @@ impl Cosmic { } return iced_runtime::window::run_with_handle(id, init_windowing_system); } - Action::WindowingSystemInitialized => { + Action::BlurEnabled => { // TODO do this after blur event confirms support instead of for all wayland windows let core = self.app.core(); - let new_blur = WINDOWING_SYSTEM.get() == Some(&WindowingSystem::Wayland) && { + self.blur_enabled = true; + let new_blur = self.blur_enabled && { let t = core.system_theme.cosmic(); match self.app.core().app_type() { crate::core::AppType::Window => t.frosted_windows, @@ -1323,6 +1313,7 @@ impl Cosmic { return Task::batch(cmds); } } + _ => (), } iced::Task::none() @@ -1337,6 +1328,7 @@ impl Cosmic { surface_views: HashMap::new(), tracked_windows: HashSet::new(), opened_surfaces: HashMap::new(), + blur_enabled: false, } } From cadb7733777330e595417fcf2b5023e40b779d97 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 20 Apr 2026 15:33:44 -0400 Subject: [PATCH 074/201] wip refactor blur/corners --- src/app/cosmic.rs | 65 ++++++++++++++++++++++++++--------------------- 1 file changed, 36 insertions(+), 29 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index c22b8dd6..bdd283c7 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -2,11 +2,10 @@ // SPDX-License-Identifier: MPL-2.0 use std::borrow::Borrow; -use std::collections::{HashMap, HashSet}; +use std::collections::HashMap; use std::sync::Arc; use super::{Action, Application, ApplicationExt, Subscription}; -use crate::core::AppType; use crate::theme::{THEME, Theme, ThemeType}; use crate::{Core, Element, keyboard_nav}; #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -93,7 +92,6 @@ pub struct Cosmic { Box Fn(&'a App) -> Element<'a, crate::Action>>, ), >, - pub tracked_windows: HashSet, pub opened_surfaces: HashMap, blur_enabled: bool, } @@ -308,13 +306,11 @@ 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); - self.tracked_windows.insert(id); iced_runtime::task::oneshot(|channel| { iced_runtime::Action::Window(iced_runtime::window::Action::Open( id, settings, channel, @@ -345,14 +341,11 @@ where } }) { let settings = settings(); - self.tracked_windows.insert(id); 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, @@ -681,7 +674,19 @@ impl Cosmic { use iced_winit::platform_specific::commands::corner_radius::corner_radius; let theme = THEME.lock().unwrap(); - let rounded = !self.app.core().window.sharp_corners; + let rounded = !self.app.core().window.sharp_corners + || self + .surface_views + .get(&id) + .is_some_and(|(_, surface_type, _)| { + if let SurfaceIdWrapper::Popup(_) + | SurfaceIdWrapper::LayerSurface(_) = surface_type + { + true + } else { + false + } + }); let cur_rad = self.app.core().app_type.corners(&theme, rounded); return Task::batch([corner_radius(id, Some(cur_rad)).discard()]); @@ -775,8 +780,9 @@ impl Cosmic { drop(guard); let core = self.app.core(); + #[cfg(all(feature = "wayland", target_os = "linux"))] if core.auto_blur { - let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); + let mut cmds = Vec::with_capacity(1 + self.surface_views.len()); let blur = if new_blur { iced::window::enable_blur } else { @@ -788,7 +794,7 @@ impl Cosmic { .main_window_id() .unwrap_or(window::Id::RESERVED), )); - for id in &self.tracked_windows { + for (id, ..) in &self.surface_views { cmds.push(blur(*id)); } return Task::batch(cmds); @@ -864,10 +870,6 @@ impl Cosmic { let cur_rad = corners(*surface_type, rounded, &cosmic_theme); cmds.push(corner_radius(*id, Some(cur_rad)).discard()); } - // Update radius for all tracked windows - for id in &self.tracked_windows { - cmds.push(corner_radius(*id, Some(cur_rad)).discard()); - } return Task::batch(cmds); } @@ -965,10 +967,6 @@ impl Cosmic { let cur_rad = corners(*surface_type, rounded, &cosmic_theme); cmds.push(corner_radius(*id, Some(cur_rad)).discard()); } - // Update radius for all tracked windows - for id in &self.tracked_windows { - cmds.push(corner_radius(*id, Some(cur_rad)).discard()); - } let core = self.app.core(); if core.auto_blur { @@ -984,7 +982,7 @@ impl Cosmic { .main_window_id() .unwrap_or(window::Id::RESERVED), )); - for id in &self.tracked_windows { + for (id, ..) in &self.surface_views { cmds.push(blur(*id)); } } @@ -1031,7 +1029,6 @@ impl Cosmic { self.opened_surfaces.remove(&id); #[cfg(all(feature = "wayland", target_os = "linux"))] self.surface_views.remove(&id); - self.tracked_windows.remove(&id); } let mut ret = if let Some(msg) = self.app.on_close_requested(id) { @@ -1108,7 +1105,7 @@ impl Cosmic { // Only apply update if the theme is set to load a system theme if let ThemeType::System { theme: _, .. } = cosmic_theme.theme_type { - let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); + let mut cmds = Vec::with_capacity(1); if core.auto_blur { let blur = if cosmic_theme.transparent { @@ -1122,7 +1119,8 @@ impl Cosmic { .main_window_id() .unwrap_or(window::Id::RESERVED), )); - for id in &self.tracked_windows { + #[cfg(all(feature = "wayland", target_os = "linux"))] + for (id, ..) in &self.surface_views { cmds.push(blur(*id)); } } @@ -1234,7 +1232,16 @@ impl Cosmic { let mut theme = THEME.lock().unwrap(); // TODO do we need per window sharp corners? - let rounded = !self.app.core().window.sharp_corners; + let rounded = !self.app.core().window.sharp_corners + || self + .surface_views + .get(&id) + .is_some_and(|(_, surface_type, _)| { + matches!( + surface_type, + SurfaceIdWrapper::Popup(_) | SurfaceIdWrapper::LayerSurface(_) + ) + }); let core = self.app.core(); let new_blur = self.blur_enabled && { let t = theme.cosmic(); @@ -1251,13 +1258,14 @@ impl Cosmic { } else { iced::window::disable_blur }; - let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); + let mut cmds = Vec::with_capacity(1 + self.surface_views.len()); cmds.push(blur(id)); Task::batch(cmds) } else { Task::none() }; + let corner_task = if let Some(s) = self.surface_views.get(&id) { corner_radius(id, Some(corners(s.1, rounded, &theme))).discard() } else if id @@ -1266,7 +1274,6 @@ impl Cosmic { .core() .main_window_id() .unwrap_or(window::Id::RESERVED) - || self.tracked_windows.contains(&id) { corner_radius(id, Some(self.app.core().app_type.corners(&theme, rounded))) .discard() @@ -1281,6 +1288,7 @@ impl Cosmic { } return iced_runtime::window::run_with_handle(id, init_windowing_system); } + #[cfg(all(feature = "wayland", target_os = "linux"))] Action::BlurEnabled => { // TODO do this after blur event confirms support instead of for all wayland windows let core = self.app.core(); @@ -1303,11 +1311,11 @@ impl Cosmic { } else { iced::window::disable_blur }; - let mut cmds = Vec::with_capacity(1 + self.tracked_windows.len()); + let mut cmds = Vec::with_capacity(1 + self.surface_views.len()); if let Some(main_id) = core.main_window_id() { cmds.push(blur(main_id)); } - for id in &self.tracked_windows { + for id in self.surface_views.keys() { cmds.push(blur(*id)); } return Task::batch(cmds); @@ -1326,7 +1334,6 @@ impl Cosmic { app, #[cfg(all(feature = "wayland", target_os = "linux"))] surface_views: HashMap::new(), - tracked_windows: HashSet::new(), opened_surfaces: HashMap::new(), blur_enabled: false, } From 732785a50c5de8fc40034710d13a87612e6a173b Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 20 Apr 2026 23:50:27 -0400 Subject: [PATCH 075/201] track surfaces that use a normal view method --- Cargo.toml | 1 + src/app/cosmic.rs | 251 ++++++++++++++++++++++++++++++++++-------- src/core.rs | 50 ++++++++- src/surface/action.rs | 79 +++++++++++++ src/surface/mod.rs | 27 +++++ 5 files changed, 361 insertions(+), 47 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8e21f986..40230807 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -170,6 +170,7 @@ url = "2.5.8" zbus = { workspace = true, optional = true } float-cmp = "0.10.0" ron = { workspace = true, optional = true } +enumflags2 = "0.7.12" # Enable DBus feature on Linux targets [target.'cfg(target_os = "linux")'.dependencies] diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index bdd283c7..56711dbf 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -92,6 +92,7 @@ pub struct Cosmic { Box Fn(&'a App) -> Element<'a, crate::Action>>, ), >, + pub tracked_surfaces: HashMap, pub opened_surfaces: HashMap, blur_enabled: bool, } @@ -165,7 +166,10 @@ where self.get_subsurface(settings, *view) } else { - iced_winit::commands::subsurface::get_subsurface(settings(&mut self.app)) + let settings = settings(&mut self.app); + self.tracked_surfaces + .insert(settings.id, SurfaceIdWrapper::Subsurface(settings.id)); + iced_winit::commands::subsurface::get_subsurface(settings) } } #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -193,7 +197,10 @@ where self.get_subsurface(settings, Box::new(move |_| view())) } else { - iced_winit::commands::subsurface::get_subsurface(settings()) + let settings = settings(); + self.tracked_surfaces + .insert(settings.id, SurfaceIdWrapper::Subsurface(settings.id)); + iced_winit::commands::subsurface::get_subsurface(settings) } } #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -222,7 +229,10 @@ where self.get_popup(settings, *view) } else { - iced_winit::commands::popup::get_popup(settings(&mut self.app)) + let settings = settings(&mut self.app); + self.tracked_surfaces + .insert(settings.id, SurfaceIdWrapper::Popup(settings.id)); + iced_winit::commands::popup::get_popup(settings) } } #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -279,7 +289,10 @@ where self.get_popup(settings, Box::new(move |_| view())) } else { - iced_winit::commands::popup::get_popup(settings()) + let settings = settings(); + self.tracked_surfaces + .insert(settings.id, SurfaceIdWrapper::Popup(settings.id)); + iced_winit::commands::popup::get_popup(settings) } } #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -310,7 +323,8 @@ where self.get_window(id, settings, *view) } else { let settings = settings(&mut self.app); - + self.tracked_surfaces + .insert(id, SurfaceIdWrapper::Window(id)); iced_runtime::task::oneshot(|channel| { iced_runtime::Action::Window(iced_runtime::window::Action::Open( id, settings, channel, @@ -345,6 +359,8 @@ where self.get_window(id, settings, Box::new(move |_| view())) } else { let settings = settings(); + self.tracked_surfaces + .insert(id, SurfaceIdWrapper::Window(id)); iced_runtime::task::oneshot(|channel| { iced_runtime::Action::Window(iced_runtime::window::Action::Open( @@ -359,6 +375,74 @@ where crate::surface::Action::Task(f) => { f().map(|sm| crate::Action::Cosmic(Action::Surface(sm))) } + #[cfg(all(feature = "wayland", target_os = "linux"))] + crate::surface::Action::AppLayerShell(settings, view) => { + let Some(settings) = std::sync::Arc::try_unwrap(settings) + .ok() + .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + Send + Sync>>().ok()) else { + tracing::error!("Invalid settings for layer surface"); + 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 layer surface: {err:?}"); + None + } + } + }) { + let settings = settings(&mut self.app); + + self.get_layer_shell(settings, *view) + } else { + let settings = settings(&mut self.app); + self.tracked_surfaces + .insert(settings.id, SurfaceIdWrapper::LayerSurface(settings.id)); + iced_winit::commands::layer_surface::get_layer_surface(settings) + } + } + #[cfg(all(feature = "wayland", target_os = "linux"))] + crate::surface::Action::LayerShell(settings, view) => { + let Some(settings) = std::sync::Arc::try_unwrap(settings) + .ok() + .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + Send + Sync>>().ok()) else { + tracing::error!("Invalid settings for layer surface"); + 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 layer surface: {err:?}"); + None + } + } + }) { + let settings = settings(); + + self.get_layer_shell(settings, Box::new(move |_| view())) + } else { + let settings = settings(); + self.tracked_surfaces.insert( + settings.id, + iced_winit::SurfaceIdWrapper::LayerSurface(settings.id), + ); + iced_winit::commands::layer_surface::get_layer_surface(settings) + } + } + #[cfg(all(feature = "wayland", target_os = "linux"))] + crate::surface::Action::DestroyLayerShell(id) => { + iced_winit::commands::layer_surface::destroy_layer_surface(id) + } _ => iced::Task::none(), } @@ -775,27 +859,38 @@ impl Cosmic { }; theme.transparent = new_blur; let mut guard = THEME.lock().unwrap(); - guard.set_theme(theme.theme_type); + guard.set_theme(theme.theme_type.clone()); guard.transparent = new_blur; drop(guard); let core = self.app.core(); #[cfg(all(feature = "wayland", target_os = "linux"))] - if core.auto_blur { + { + use crate::widget::wrapper; + let mut cmds = Vec::with_capacity(1 + self.surface_views.len()); let blur = if new_blur { iced::window::enable_blur } else { iced::window::disable_blur }; - cmds.push(blur( - self.app - .core() - .main_window_id() - .unwrap_or(window::Id::RESERVED), - )); - for (id, ..) in &self.surface_views { - cmds.push(blur(*id)); + if core.blur(&theme, None) { + cmds.push(blur( + self.app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED), + )); + } + for (id, wrapper, ..) in &self.surface_views { + if core.blur(&theme, Some(wrapper.1)) { + cmds.push(blur(*id)); + } + } + for (id, wrapper) in &self.tracked_surfaces { + if core.blur(&theme, Some(*wrapper)) { + cmds.push(blur(*id)); + } } return Task::batch(cmds); } @@ -866,10 +961,14 @@ impl Cosmic { 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() { + for (id, (_, surface_type, _)) in &self.surface_views { let cur_rad = corners(*surface_type, rounded, &cosmic_theme); cmds.push(corner_radius(*id, Some(cur_rad)).discard()); } + for (id, wrapper) in &self.tracked_surfaces { + let cur_rad = corners(*wrapper, rounded, &cosmic_theme); + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + } return Task::batch(cmds); } @@ -963,26 +1062,37 @@ impl Cosmic { 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() { + for (id, (_, surface_type, _)) in &self.surface_views { let cur_rad = corners(*surface_type, rounded, &cosmic_theme); cmds.push(corner_radius(*id, Some(cur_rad)).discard()); } + for (id, wrapper) in &self.tracked_surfaces { + let cur_rad = corners(*wrapper, rounded, &cosmic_theme); + cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + } let core = self.app.core(); - if core.auto_blur { - let blur = if cosmic_theme.transparent { - iced::window::enable_blur - } else { - iced::window::disable_blur - }; + let blur = if cosmic_theme.transparent { + iced::window::enable_blur + } else { + iced::window::disable_blur + }; + if core.blur(&cosmic_theme, None) { cmds.push(blur( self.app .core() .main_window_id() .unwrap_or(window::Id::RESERVED), )); - for (id, ..) in &self.surface_views { + } + for (id, wrapper, ..) in &self.surface_views { + if core.blur(&cosmic_theme, Some(wrapper.1)) { + cmds.push(blur(*id)); + } + } + for (id, wrapper) in &self.tracked_surfaces { + if core.blur(&cosmic_theme, Some(*wrapper)) { cmds.push(blur(*id)); } } @@ -1030,6 +1140,9 @@ impl Cosmic { #[cfg(all(feature = "wayland", target_os = "linux"))] self.surface_views.remove(&id); } + self.tracked_surfaces.remove(&id); + self.tracked_surfaces + .shrink_to(self.tracked_surfaces.len() * 2); let mut ret = if let Some(msg) = self.app.on_close_requested(id) { self.app.update(msg) @@ -1106,22 +1219,28 @@ impl Cosmic { // Only apply update if the theme is set to load a system theme if let ThemeType::System { theme: _, .. } = cosmic_theme.theme_type { let mut cmds = Vec::with_capacity(1); - - if core.auto_blur { + #[cfg(all(feature = "wayland", target_os = "linux"))] + { let blur = if cosmic_theme.transparent { iced::window::enable_blur } else { iced::window::disable_blur }; - cmds.push(blur( - self.app - .core() - .main_window_id() - .unwrap_or(window::Id::RESERVED), - )); - #[cfg(all(feature = "wayland", target_os = "linux"))] - for (id, ..) in &self.surface_views { - cmds.push(blur(*id)); + + if core.blur(&cosmic_theme, None) { + cmds.push(blur( + core.main_window_id().unwrap_or(window::Id::RESERVED), + )); + } + for (id, wrapper, ..) in &self.surface_views { + if core.blur(&cosmic_theme, Some(wrapper.1)) { + cmds.push(blur(*id)); + } + } + for (id, wrapper) in &self.tracked_surfaces { + if core.blur(&cosmic_theme, Some(*wrapper)) { + cmds.push(blur(*id)); + } } } cosmic_theme.set_theme(new_theme.theme_type); @@ -1252,7 +1371,13 @@ impl Cosmic { } }; theme.transparent = new_blur; - let blur_cmd = if core.auto_blur { + let wrapper = self + .surface_views + .get(&id) + .map(|s| s.1) + .or(self.tracked_surfaces.get(&id).copied()); + // this will blur untracked windows as if they were the main window + let blur_cmd = if core.blur(&theme, wrapper) { let blur = if new_blur { iced::window::enable_blur } else { @@ -1266,8 +1391,13 @@ impl Cosmic { Task::none() }; - let corner_task = if let Some(s) = self.surface_views.get(&id) { - corner_radius(id, Some(corners(s.1, rounded, &theme))).discard() + let corner_task = if let Some(s) = self + .surface_views + .get(&id) + .map(|s| s.1) + .or(self.tracked_surfaces.get(&id).copied()) + { + corner_radius(id, Some(corners(s, rounded, &theme))).discard() } else if id == self .app @@ -1305,18 +1435,30 @@ impl Cosmic { t.transparent = matches!(&t.theme_type, ThemeType::System { .. }) && new_blur; - if core.auto_blur { + { let blur = if new_blur { iced::window::enable_blur } else { iced::window::disable_blur }; let mut cmds = Vec::with_capacity(1 + self.surface_views.len()); - if let Some(main_id) = core.main_window_id() { - cmds.push(blur(main_id)); + if core.blur(&t, None) { + cmds.push(blur( + self.app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED), + )); } - for id in self.surface_views.keys() { - cmds.push(blur(*id)); + for (id, wrapper, ..) in &self.surface_views { + if core.blur(&t, Some(wrapper.1)) { + cmds.push(blur(*id)); + } + } + for (id, wrapper) in &self.tracked_surfaces { + if core.blur(&t, Some(*wrapper)) { + cmds.push(blur(*id)); + } } return Task::batch(cmds); } @@ -1335,6 +1477,7 @@ impl Cosmic { #[cfg(all(feature = "wayland", target_os = "linux"))] surface_views: HashMap::new(), opened_surfaces: HashMap::new(), + tracked_surfaces: HashMap::new(), blur_enabled: false, } } @@ -1409,6 +1552,28 @@ impl Cosmic { }) .discard() } + + #[cfg(all(feature = "wayland", target_os = "linux"))] + pub fn get_layer_shell( + &mut self, + settings: iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings, + view: Box< + dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync, + >, + ) -> Task> { + use iced_winit::SurfaceIdWrapper; + use iced_winit::platform_specific::commands::layer_surface::get_layer_surface; + *self.opened_surfaces.entry(settings.id).or_insert(0) += 1; + self.surface_views.insert( + settings.id, + ( + None, // TODO parent for layer shell, platform specific option maybe? + SurfaceIdWrapper::LayerSurface(settings.id), + view, + ), + ); + get_layer_surface(settings) + } } #[cfg(all(feature = "wayland", target_os = "linux"))] diff --git a/src/core.rs b/src/core.rs index e725547f..53e53c94 100644 --- a/src/core.rs +++ b/src/core.rs @@ -6,8 +6,10 @@ use std::collections::HashMap; use crate::widget::nav_bar; use cosmic_config::CosmicConfigEntry; use cosmic_theme::ThemeMode; +use enumflags2::{self, BitFlags, bitflags}; use iced::{Limits, Size, window}; use iced_core::window::Id; +use iced_winit::SurfaceIdWrapper; use palette::Srgba; use slotmap::Key; @@ -43,6 +45,18 @@ pub struct Window { width: f32, } +#[bitflags] +#[repr(u8)] +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum Auto { + /// Automatically apply effect to regular windows + Window, + /// Automatically apply effect to popups + Popup, + /// Automatically apply effect to system interface elements (layer shell surfaces) + System, +} + /// COSMIC-specific application settings #[derive(Clone)] pub struct Core { @@ -102,7 +116,7 @@ pub struct Core { #[cfg(all(feature = "wayland", target_os = "linux"))] pub(crate) sync_window_border_radii_to_theme: bool, - pub(crate) auto_blur: bool, + pub(crate) auto_blur: BitFlags, pub(crate) app_type: AppType, } @@ -214,7 +228,7 @@ impl Default for Core { menu_bars: HashMap::new(), #[cfg(all(feature = "wayland", target_os = "linux"))] sync_window_border_radii_to_theme: true, - auto_blur: true, + auto_blur: Auto::System | Auto::Popup | Auto::Window, app_type: AppType::Window, } } @@ -558,11 +572,11 @@ impl Core { self.sync_window_border_radii_to_theme } - pub fn set_auto_blur(&mut self, auto_blur: bool) { + pub fn set_auto_blur(&mut self, auto_blur: BitFlags) { self.auto_blur = auto_blur; } - pub fn auto_blur(&self) -> bool { + pub fn auto_blur(&self) -> BitFlags { self.auto_blur } @@ -573,4 +587,32 @@ impl Core { pub fn app_type(&self) -> AppType { self.app_type } + + pub fn blur(&self, theme: &Theme, surface_id_wrapper: Option) -> bool { + let theme = theme.cosmic(); + match surface_id_wrapper { + Some(SurfaceIdWrapper::LayerSurface(_)) => { + theme.frosted_system_interface && self.auto_blur.contains(Auto::System) + } + Some(SurfaceIdWrapper::Window(_)) => { + theme.frosted_windows && self.auto_blur.contains(Auto::Window) + } + Some(SurfaceIdWrapper::Popup(_)) + if matches!(self.app_type, AppType::Window | AppType::System) => + { + theme.frosted_windows && self.auto_blur.contains(Auto::Popup) + } + Some(SurfaceIdWrapper::Popup(_)) if matches!(self.app_type, AppType::Applet) => { + theme.frosted_applets && self.auto_blur.contains(Auto::Popup) + } + None => match self.app_type { + AppType::Window => theme.frosted_windows && self.auto_blur.contains(Auto::Window), + AppType::System => { + theme.frosted_system_interface && self.auto_blur.contains(Auto::System) + } + AppType::Applet => false, + }, + _ => false, + } + } } diff --git a/src/surface/action.rs b/src/surface/action.rs index 5bd5a294..300d844b 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -28,6 +28,12 @@ pub fn destroy_window(id: iced_core::window::Id) -> Action { Action::DestroyWindow(id) } +#[cfg(all(feature = "wayland", target_os = "linux"))] +#[must_use] +pub fn destroy_layer_shell(id: iced_core::window::Id) -> Action { + Action::DestroyLayerShell(id) +} + #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn app_window( @@ -226,3 +232,76 @@ pub fn subsurface( }), ) } + +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[must_use] +pub fn simple_layer_shell( + settings: impl Fn() + -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + + Send + + Sync + + 'static, + view: Option< + impl Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, + >, +) -> Action { + let boxed: Box< + dyn Fn() + -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + + Send + + Sync + + 'static, + > = Box::new(settings); + let boxed: Box = Box::new(boxed); + + Action::LayerShell( + 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", target_os = "linux", feature = "winit"))] +#[must_use] +pub fn layer_shell( + settings: impl Fn( + &mut App, + ) + -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + + Send + + Sync + + 'static, + // XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways. + view: Option< + Box< + dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action> + + Send + + Sync + + 'static, + >, + >, +) -> Action { + let boxed: Box< + dyn Fn( + &mut App, + ) + -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + + Send + + Sync + + 'static, + > = Box::new(settings); + let boxed: Box = Box::new(boxed); + + Action::AppLayerShell( + Arc::new(boxed), + view.map(|view| { + let boxed: Box = Box::new(view); + Arc::new(boxed) + }), + ) +} diff --git a/src/surface/mod.rs b/src/surface/mod.rs index 307b5275..29a9eada 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -52,6 +52,21 @@ pub enum Action { /// Destroy a window DestroyWindow(iced::window::Id), + /// Create a layer shell surface with a view function accepting the App as a parameter + AppLayerShell( + std::sync::Arc>, + Option>>, + ), + + /// Create a layer shell surface with a view function + LayerShell( + std::sync::Arc>, + Option>>, + ), + + /// Destroy a layer shell surface + DestroyLayerShell(iced::window::Id), + /// Responsive menu bar update ResponsiveMenuBar { /// Id of the menu bar @@ -111,6 +126,18 @@ impl std::fmt::Debug for Action { .finish(), Self::DestroyWindow(arg0) => f.debug_tuple("DestroyWindow").field(arg0).finish(), Self::Task(_) => f.debug_tuple("Future").finish(), + Self::AppLayerShell(any, any1) => f + .debug_tuple("AppLayerShell") + .field(any) + .field(any1) + .finish(), + Self::LayerShell(any, any1) => { + f.debug_tuple("LayerShell").field(any).field(any1).finish() + } + Self::DestroyLayerShell(arg0) => { + f.debug_tuple("DestroyLayerShell").field(arg0).finish() + } + Self::Task(_) => f.debug_tuple("Task").finish(), } } } From 2d7f66d5280bc209fb3ceae950431c9cedf56a75 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 21 Apr 2026 11:14:23 -0400 Subject: [PATCH 076/201] refactor: remove extra opacity where it was added --- src/applet/mod.rs | 1 - src/theme/style/iced.rs | 11 +++++------ src/theme/style/menu_bar.rs | 1 - src/widget/context_drawer/widget.rs | 28 ++++++++++++++++++++++++++-- 4 files changed, 31 insertions(+), 10 deletions(-) diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 897ea7ca..37635bf4 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -373,7 +373,6 @@ impl Context { let cosmic = theme.cosmic(); let corners = cosmic.corner_radii; let mut bg = cosmic.background(theme.transparent).base; - bg.alpha = (bg.alpha + if cosmic.is_dark { 0.6 } else { 0.5 }).min(1.); iced_widget::container::Style { text_color: Some(cosmic.background(theme.transparent).on.into()), background: Some(Color::from(bg).into()), diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index a18068a4..bac6e275 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -389,7 +389,9 @@ pub enum Container<'a> { WindowBackground, Background, Card, - ContextDrawer, + ContextDrawer { + transparent: bool, + }, Custom(Box iced_container::Style + 'a>), Dialog, Dropdown, @@ -587,11 +589,8 @@ impl iced_container::Catalog for Theme { } } - Container::ContextDrawer => { - let mut a = Container::primary(cosmic, self.transparent); - if let Some(Background::Color(ref mut color)) = a.background { - color.a = (color.a + if cosmic.is_dark { 0.60 } else { 0.5 }).min(1.); - } + Container::ContextDrawer { transparent } => { + let mut a = Container::primary(cosmic, self.transparent && *transparent); if cosmic.is_high_contrast { a.border.width = 1.; diff --git a/src/theme/style/menu_bar.rs b/src/theme/style/menu_bar.rs index 2a618f6b..c6f40c52 100644 --- a/src/theme/style/menu_bar.rs +++ b/src/theme/style/menu_bar.rs @@ -66,7 +66,6 @@ impl StyleSheet for Theme { let cosmic = self.cosmic(); let component = &cosmic.background(self.transparent).component; let mut bg = component.base; - bg.alpha = (bg.alpha + if cosmic.is_dark { 0.6 } else { 0.5 }).min(1.); match style { MenuBarStyle::Default => Appearance { diff --git a/src/widget/context_drawer/widget.rs b/src/widget/context_drawer/widget.rs index 9a7448eb..b743c4eb 100644 --- a/src/widget/context_drawer/widget.rs +++ b/src/widget/context_drawer/widget.rs @@ -31,6 +31,24 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { on_close: Message, max_width: f32, ) -> Element<'a, Message> + where + Drawer: Into>, + { + Self::new_inner_overlay( + title, actions, header, footer, drawer, on_close, max_width, false, + ) + } + + pub fn new_inner_overlay( + title: Option>, + actions: Option>, + header: Option>, + footer: Option>, + drawer: Drawer, + on_close: Message, + max_width: f32, + overlay: bool, + ) -> Element<'a, Message> where Drawer: Into>, { @@ -43,6 +61,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { drawer: Element<'a, Message>, on_close: Message, max_width: f32, + overlay: bool, ) -> Element<'a, Message> { let cosmic_theme::Spacing { space_xxs, @@ -105,7 +124,9 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { container( LayerContainer::new(pane) .layer(cosmic_theme::Layer::Primary) - .class(crate::style::Container::ContextDrawer) + .class(crate::style::Container::ContextDrawer { + transparent: !overlay, + }) .width(Length::Fill) .height(Length::Fill) .max_width(max_width), @@ -124,6 +145,7 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { drawer.into(), on_close, max_width, + overlay, ) } @@ -142,7 +164,9 @@ impl<'a, Message: Clone + 'static> ContextDrawer<'a, Message> { Content: Into>, Drawer: Into>, { - let drawer = Self::new_inner(title, actions, header, footer, drawer, on_close, max_width); + let drawer = Self::new_inner_overlay( + title, actions, header, footer, drawer, on_close, max_width, true, + ); ContextDrawer { id: None, From bf5ffcc2af5bbd8b4277a4b2cf99f51b070bcf45 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 21 Apr 2026 17:58:24 -0400 Subject: [PATCH 077/201] fix: apply blur enabled to system theme --- src/app/cosmic.rs | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 56711dbf..6fd97110 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -857,6 +857,7 @@ impl Cosmic { crate::core::AppType::Applet => t.frosted_applets, } }; + theme.transparent = new_blur; let mut guard = THEME.lock().unwrap(); guard.set_theme(theme.theme_type.clone()); @@ -866,8 +867,6 @@ impl Cosmic { let core = self.app.core(); #[cfg(all(feature = "wayland", target_os = "linux"))] { - use crate::widget::wrapper; - let mut cmds = Vec::with_capacity(1 + self.surface_views.len()); let blur = if new_blur { iced::window::enable_blur @@ -1361,7 +1360,6 @@ impl Cosmic { SurfaceIdWrapper::Popup(_) | SurfaceIdWrapper::LayerSurface(_) ) }); - let core = self.app.core(); let new_blur = self.blur_enabled && { let t = theme.cosmic(); match self.app.core().app_type() { @@ -1370,14 +1368,14 @@ impl Cosmic { crate::core::AppType::Applet => t.frosted_applets, } }; - theme.transparent = new_blur; + let wrapper = self .surface_views .get(&id) .map(|s| s.1) .or(self.tracked_surfaces.get(&id).copied()); // this will blur untracked windows as if they were the main window - let blur_cmd = if core.blur(&theme, wrapper) { + let blur_cmd = if self.app.core().blur(&theme, wrapper) { let blur = if new_blur { iced::window::enable_blur } else { @@ -1421,20 +1419,21 @@ impl Cosmic { #[cfg(all(feature = "wayland", target_os = "linux"))] Action::BlurEnabled => { // TODO do this after blur event confirms support instead of for all wayland windows - let core = self.app.core(); self.blur_enabled = true; + let mut t = THEME.lock().unwrap(); + let new_blur = self.blur_enabled && { - let t = core.system_theme.cosmic(); + let t = t.cosmic(); match self.app.core().app_type() { crate::core::AppType::Window => t.frosted_windows, crate::core::AppType::System => t.frosted_system_interface, crate::core::AppType::Applet => t.frosted_applets, } }; - let mut t = THEME.lock().unwrap(); - t.transparent = matches!(&t.theme_type, ThemeType::System { .. }) && new_blur; + t.transparent = new_blur; + self.app.core_mut().system_theme.transparent = new_blur; { let blur = if new_blur { iced::window::enable_blur @@ -1442,7 +1441,7 @@ impl Cosmic { iced::window::disable_blur }; let mut cmds = Vec::with_capacity(1 + self.surface_views.len()); - if core.blur(&t, None) { + if self.app.core().blur(&t, None) { cmds.push(blur( self.app .core() @@ -1451,12 +1450,12 @@ impl Cosmic { )); } for (id, wrapper, ..) in &self.surface_views { - if core.blur(&t, Some(wrapper.1)) { + if self.app.core().blur(&t, Some(wrapper.1)) { cmds.push(blur(*id)); } } for (id, wrapper) in &self.tracked_surfaces { - if core.blur(&t, Some(*wrapper)) { + if self.app.core().blur(&t, Some(*wrapper)) { cmds.push(blur(*id)); } } From 1fafd57578be27053de83b7313cf1e4598821118 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 21 Apr 2026 19:03:35 -0400 Subject: [PATCH 078/201] remove header container i'm not sure what gaps it is referring to --- src/app/mod.rs | 28 +--------------------------- 1 file changed, 1 insertion(+), 27 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index e2d6ae1c..b007757c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -815,33 +815,7 @@ impl ApplicationExt for App { header = header.end(element.map(crate::Action::App)); } - if content_container { - header.apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_header"))) - } else { - // Needed to avoid header bar corner gaps for apps without a content container - header - .apply(container) - .class(crate::theme::Container::custom(move |theme| { - let cosmic = theme.cosmic(); - container::Style { - background: Some(iced::Background::Color( - cosmic.background(theme.transparent).base.into(), - )), - border: iced::Border { - radius: [ - (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], - ] - .into(), - ..Default::default() - }, - ..Default::default() - } - })) - .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_header"))) - } + header.apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_header"))) }) } else { None From fb4628e4609f4ee8cff23364d0b3cdb038134002 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 21 Apr 2026 19:10:54 -0400 Subject: [PATCH 079/201] fix: winit feature gate --- src/core.rs | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/core.rs b/src/core.rs index 53e53c94..b454f301 100644 --- a/src/core.rs +++ b/src/core.rs @@ -9,7 +9,6 @@ use cosmic_theme::ThemeMode; use enumflags2::{self, BitFlags, bitflags}; use iced::{Limits, Size, window}; use iced_core::window::Id; -use iced_winit::SurfaceIdWrapper; use palette::Srgba; use slotmap::Key; @@ -588,7 +587,13 @@ impl Core { self.app_type } - pub fn blur(&self, theme: &Theme, surface_id_wrapper: Option) -> bool { + #[cfg(feature = "winit")] + pub fn blur( + &self, + theme: &Theme, + surface_id_wrapper: Option, + ) -> bool { + use iced_winit::SurfaceIdWrapper; let theme = theme.cosmic(); match surface_id_wrapper { Some(SurfaceIdWrapper::LayerSurface(_)) => { From 0ee51316708a25fdf692111f70f0f7db1f2a49bb Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 22 Apr 2026 16:42:31 -0400 Subject: [PATCH 080/201] fix: increase text contrast constraints for transparent surfaces --- cosmic-theme/src/model/theme.rs | 44 +++++++++++++++++++++++++++------ cosmic-theme/src/steps.rs | 24 +++++++++++++++--- 2 files changed, 57 insertions(+), 11 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 730c00f8..2fc613cc 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1069,6 +1069,12 @@ impl ThemeBuilder { } = self; let container_alpha = frosted.alpha(); + let actual_alpha = + if (frosted_windows || frosted_system_interface || frosted_panel || frosted_applets) { + container_alpha + } else { + 1.0 + }; let is_dark = palette.is_dark(); let is_high_contrast = palette.is_high_contrast(); @@ -1152,6 +1158,7 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), + actual_alpha, ); let transparent_bg_component = get_surface_color( @@ -1166,6 +1173,7 @@ impl ThemeBuilder { &transparent_bg_steps_array, &control_steps_array[8], text_steps_array.as_deref(), + actual_alpha, ); let primary = { @@ -1199,6 +1207,7 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), + container_alpha, ), component_hovered_overlay, component_pressed_overlay, @@ -1211,6 +1220,7 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), + container_alpha, ), get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), is_high_contrast, @@ -1238,6 +1248,7 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), + actual_alpha, ), Srgba::new(0., 0., 0., 0.0), Srgba::new(0., 0., 0., 0.0), @@ -1250,6 +1261,7 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), + actual_alpha, ), get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), is_high_contrast, @@ -1323,6 +1335,7 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), + actual_alpha, ), get_small_widget_color(bg_index, 5, &neutral_steps, &control_steps_array[6]), is_high_contrast, @@ -1359,6 +1372,7 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), + actual_alpha, ), component_hovered_overlay, component_pressed_overlay, @@ -1371,6 +1385,7 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), + actual_alpha, ), get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), is_high_contrast, @@ -1383,14 +1398,24 @@ impl ThemeBuilder { button_hovered_overlay, button_pressed_overlay, ), - accent_button: Component::colored_button( - accent, - control_steps_array[1], - control_steps_array[0], - accent, - button_hovered_overlay, - button_pressed_overlay, - ), + accent_button: { + let step_array = steps(control_steps_array[5], NonZeroUsize::new(100).unwrap()); + + Component::colored_button( + accent, + control_steps_array[1], + get_text( + color_index(accent, step_array.len()), + &step_array, + &step_array[0], + text_steps_array.as_deref(), + 1., // accent is opaque anyway + ), + accent, + button_hovered_overlay, + button_pressed_overlay, + ) + }, button: Component::component( button_bg, accent, @@ -1518,6 +1543,7 @@ impl ThemeBuilder { &transparent_bg_steps_array, &control_steps_array[8], text_steps_array.as_deref(), + actual_alpha, ), get_small_widget_color(bg_index, 5, &neutral_steps, &control_steps_array[6]), is_high_contrast, @@ -1546,6 +1572,7 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), + actual_alpha, ), Srgba::new(0., 0., 0., 0.0), Srgba::new(0., 0., 0., 0.0), @@ -1558,6 +1585,7 @@ impl ThemeBuilder { &step_array, &control_steps_array[8], text_steps_array.as_deref(), + actual_alpha, ), get_small_widget_color(base_index, 5, &neutral_steps, &control_steps_array[6]), is_high_contrast, diff --git a/cosmic-theme/src/steps.rs b/cosmic-theme/src/steps.rs index d156722c..e595d8f8 100644 --- a/cosmic-theme/src/steps.rs +++ b/cosmic-theme/src/steps.rs @@ -81,6 +81,7 @@ pub fn get_text( step_array: &[Srgba], fallback: &Srgba, tint_array: Option<&[Srgba]>, + alpha: f32, ) -> Srgba { assert!(step_array.len() == 100); let step_array = if let Some(tint_array) = tint_array { @@ -90,11 +91,28 @@ pub fn get_text( step_array }; + let alpha_extra_steps = if alpha < 1.0 { + ((1. - alpha) * 100.0).round() as usize + } else { + 0 + }; let is_dark = base_index < 60; - 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(if is_dark { 99 } else { 0 }); + let index = get_index( + base_index, + 70 + alpha_extra_steps, + step_array.len(), + is_dark, + ) + .or_else(|| { + get_index( + base_index, + 50 + alpha_extra_steps, + step_array.len(), + is_dark, + ) + }) + .unwrap_or(if is_dark { 99 } else { 0 }); *step_array.get(index).unwrap_or(fallback) } From 5938776c26b37603202127ee0522b2663b4f6a5f Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 28 Apr 2026 23:08:58 -0400 Subject: [PATCH 081/201] chore: surface task helper --- src/core.rs | 1 + src/surface/mod.rs | 4 ++++ 2 files changed, 5 insertions(+) diff --git a/src/core.rs b/src/core.rs index b454f301..d52f2fe0 100644 --- a/src/core.rs +++ b/src/core.rs @@ -587,6 +587,7 @@ impl Core { self.app_type } + #[must_use] #[cfg(feature = "winit")] pub fn blur( &self, diff --git a/src/surface/mod.rs b/src/surface/mod.rs index 29a9eada..e6355b8f 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -80,6 +80,10 @@ pub enum Action { Task(Arc Task + Send + Sync>), } +pub fn surface_task(action: Action) -> Task> { + crate::task::message(crate::Action::Cosmic(crate::app::Action::Surface(action))) +} + impl std::fmt::Debug for Action { #[cold] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { From 5fca59df705e69474ce4e976c991a8c2a0719c9a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 29 Apr 2026 11:47:05 -0400 Subject: [PATCH 082/201] update blur alpha --- cosmic-theme/src/model/theme.rs | 26 +++++++++++++------------- 1 file changed, 13 insertions(+), 13 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 2fc613cc..b2daabe7 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1639,19 +1639,19 @@ impl BlurStrength { pub fn alpha(&self) -> f32 { match self { Self::ExtremelyLow => 0.90, - Self::ExtremelyLow2 => 0.85, - Self::VeryLow => 0.8, - Self::VeryLow2 => 0.75, - Self::Low => 0.7, - Self::Low2 => 0.65, - Self::Medium => 0.6, - Self::Medium2 => 0.55, - Self::High => 0.5, - Self::High2 => 0.45, - Self::VeryHigh => 0.4, - Self::VeryHigh2 => 0.35, - Self::ExtremelyHigh => 0.25, - Self::ExtremelyHigh2 => 0.2, + Self::ExtremelyLow2 => 0.87692, + Self::VeryLow => 0.85385, + Self::VeryLow2 => 0.83076, + Self::Low => 0.80769, + Self::Low2 => 0.78461, + Self::Medium => 0.76154, + Self::Medium2 => 0.73846, + Self::High => 0.71538, + Self::High2 => 0.69231, + Self::VeryHigh => 0.66023, + Self::VeryHigh2 => 0.64615, + Self::ExtremelyHigh => 0.62308, + Self::ExtremelyHigh2 => 0.6, } } } From 408d37169af14ff481f32158fe80b175994438f1 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 29 Apr 2026 11:51:49 -0400 Subject: [PATCH 083/201] fix: feature gate --- examples/application/src/main.rs | 1 + src/surface/mod.rs | 1 + 2 files changed, 2 insertions(+) diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index 2b4999b1..e60b4f7e 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -282,6 +282,7 @@ impl cosmic::Application for App { .progress(1.0) .width(Length::Fill), ) + .push(widget::button::suggested("asdf").on_press(Message::Ignore)) .spacing(cosmic::theme::spacing().space_s) .width(Length::Fill) .height(Length::Shrink) diff --git a/src/surface/mod.rs b/src/surface/mod.rs index e6355b8f..f4b9417a 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -80,6 +80,7 @@ pub enum Action { Task(Arc Task + Send + Sync>), } +#[cfg(feature = "winit")] pub fn surface_task(action: Action) -> Task> { crate::task::message(crate::Action::Cosmic(crate::app::Action::Surface(action))) } From 7d62b193f487918b66ffad9f6b7cba74c8fc7ab2 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 5 May 2026 10:36:13 -0400 Subject: [PATCH 084/201] feat: improve handling of list items using data in theme --- src/theme/mod.rs | 46 ++-- src/widget/button/widget.rs | 18 +- src/widget/menu.rs | 2 + src/widget/menu/menu_column.rs | 411 +++++++++++++++++++++++++++++++++ src/widget/menu/menu_inner.rs | 19 +- 5 files changed, 461 insertions(+), 35 deletions(-) create mode 100644 src/widget/menu/menu_column.rs diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 07c5f874..830746a1 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -7,6 +7,7 @@ pub mod portal; pub mod style; +use ::iced::Alignment; use cosmic_config::{CosmicConfigEntry, config_subscription}; use cosmic_theme::{Component, LayeredTheme, Spacing, ThemeMode}; use iced_futures::Subscription; @@ -47,6 +48,7 @@ pub(crate) static THEME: Mutex = Mutex::new(Theme { theme_type: ThemeType::Dark, layer: cosmic_theme::Layer::Background, transparent: false, + list_item_position: None, }); /// Currently-defined theme. @@ -83,34 +85,6 @@ 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" -// ); -// } - -// Theme::system(Arc::new(res.config)) -// }) -// } - pub fn system_dark() -> Theme { let Ok(helper) = crate::cosmic_theme::Theme::dark_config() else { return Theme::dark(); @@ -211,6 +185,8 @@ pub struct Theme { pub theme_type: ThemeType, pub layer: cosmic_theme::Layer, pub transparent: bool, + /// Only meaningful for widgets that must be in a list. Otherwise it should be ignored. + pub list_item_position: Option<(Alignment, usize)>, } impl Theme { @@ -281,9 +257,9 @@ impl Theme { /// can be used in a component that is intended to be a child of a `CosmicContainer` pub fn current_container(&self) -> &cosmic_theme::Container { match self.layer { - cosmic_theme::Layer::Background => &self.cosmic().background(self.transparent), - cosmic_theme::Layer::Primary => &self.cosmic().primary(self.transparent), - cosmic_theme::Layer::Secondary => &self.cosmic().secondary(self.transparent), + cosmic_theme::Layer::Background => self.cosmic().background(self.transparent), + cosmic_theme::Layer::Primary => self.cosmic().primary(self.transparent), + cosmic_theme::Layer::Secondary => self.cosmic().secondary(self.transparent), } } @@ -292,6 +268,14 @@ impl Theme { pub fn set_theme(&mut self, theme: ThemeType) { self.theme_type = theme; } + + #[inline] + /// Clone theme with a list position + pub fn with_list_item_position(&self, position: Option<(Alignment, usize)>) -> Self { + let mut new = self.clone(); + new.list_item_position = position; + new + } } impl LayeredTheme for Theme { diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index cca54fd2..aa2c8a78 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -6,6 +6,7 @@ //! //! A [`Button`] has some local [`State`]. +use iced::Alignment; use iced_runtime::core::widget::Id; use iced_runtime::{Action, Task, keyboard, task}; @@ -449,7 +450,7 @@ impl<'a, Message: 'a + Clone> Widget let state = tree.state.downcast_ref::(); - let styling = if !is_enabled { + let mut styling = if !is_enabled { theme.disabled(&self.style) } else if is_mouse_over { if state.is_pressed { @@ -471,6 +472,21 @@ impl<'a, Message: 'a + Clone> Widget theme.active(state.is_focused, self.selected, &self.style) }; + if matches!(self.style, crate::theme::Button::MenuItem) { + match theme.list_item_position { + Some((Alignment::Start, _)) => { + styling.border_radius = + styling.border_radius.bottom(theme.cosmic().radius_0()[3]); + } + Some((Alignment::End, _)) => { + styling.border_radius = styling.border_radius.top(theme.cosmic().radius_0()[0]); + } + Some((Alignment::Center, _)) => {} + None => { + styling.border_radius = theme.cosmic().radius_0().into(); + } + }; + } let mut icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color); diff --git a/src/widget/menu.rs b/src/widget/menu.rs index 9d4ce4b1..a452e2b8 100644 --- a/src/widget/menu.rs +++ b/src/widget/menu.rs @@ -66,6 +66,8 @@ mod menu_bar; pub(crate) use menu_bar::MenuBarState; pub use menu_bar::{MenuBar, menu_bar as bar}; +pub mod menu_column; + mod menu_inner; mod menu_tree; pub use menu_tree::{ diff --git a/src/widget/menu/menu_column.rs b/src/widget/menu/menu_column.rs new file mode 100644 index 00000000..ecb4a129 --- /dev/null +++ b/src/widget/menu/menu_column.rs @@ -0,0 +1,411 @@ +//! Distribute content vertically. +use crate::iced; +use iced::core::alignment::{self, Alignment}; +use iced::core::event::{self, Event}; +use iced::core::widget::{Operation, Tree}; +use iced::core::{ + Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, + layout, mouse, overlay, renderer, widget, +}; + +#[allow(missing_debug_implementations)] +#[must_use] +pub struct MenuColumn<'a, Message, Renderer = iced::Renderer> { + spacing: f32, + padding: Padding, + width: Length, + height: Length, + max_width: f32, + align: Alignment, + clip: bool, + children: Vec>, +} + +impl<'a, Message, Renderer> MenuColumn<'a, Message, Renderer> +where + Renderer: iced::core::Renderer, +{ + /// Creates an empty [`MenuColumn`]. + pub fn new() -> Self { + Self::from_vec(Vec::new()) + } + + /// Creates a [`MenuColumn`] with the given capacity. + pub fn with_capacity(capacity: usize) -> Self { + Self::from_vec(Vec::with_capacity(capacity)) + } + + /// Creates a [`MenuColumn`] 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 [`MenuColumn`] from an already allocated [`Vec`]. + /// + /// Keep in mind that the [`MenuColumn`] 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 [`MenuColumn::width`] or [`MenuColumn::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 [`MenuColumn`]. + pub fn padding>(mut self, padding: P) -> Self { + self.padding = padding.into(); + self + } + + /// Sets the width of the [`MenuColumn`]. + pub fn width(mut self, width: impl Into) -> Self { + self.width = width.into(); + self + } + + /// Sets the height of the [`MenuColumn`]. + pub fn height(mut self, height: impl Into) -> Self { + self.height = height.into(); + self + } + + /// Sets the maximum width of the [`MenuColumn`]. + 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 [`MenuColumn`] . + pub fn align_x(mut self, align: impl Into) -> Self { + self.align = Alignment::from(align.into()); + self + } + + /// Sets whether the contents of the [`MenuColumn`] should be clipped on + /// overflow. + pub fn clip(mut self, clip: bool) -> Self { + self.clip = clip; + self + } + + /// Adds an element to the [`MenuColumn`]. + 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 [`MenuColumn`], if `Some`. + #[must_use] + pub fn push_maybe( + self, + child: Option>>, + ) -> Self { + if let Some(child) = child { + self.push(child) + } else { + self + } + } + + /// Extends the [`MenuColumn`] with the given children. + pub fn extend( + self, + children: impl IntoIterator>, + ) -> Self { + children.into_iter().fold(self, Self::push) + } +} + +impl Default for MenuColumn<'_, Message, Renderer> +where + Renderer: iced::core::Renderer, +{ + fn default() -> Self { + Self::new() + } +} + +impl<'a, Message, Renderer: iced::core::Renderer> + FromIterator> + for MenuColumn<'a, Message, Renderer> +{ + fn from_iter>>( + iter: T, + ) -> Self { + Self::with_children(iter) + } +} + +impl Widget + for MenuColumn<'_, Message, Renderer> +where + Renderer: iced::core::Renderer, +{ + fn children(&self) -> Vec { + self.children.iter().map(Tree::new).collect() + } + + 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( + &mut 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, + &mut self.children, + &mut tree.children, + ) + } + + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn Operation, + ) { + operation.container(None, layout.bounds()); + operation.traverse(&mut |operation| { + self.children + .iter_mut() + .zip(&mut tree.children) + .zip(layout.children()) + .for_each(|((child, state), c_layout)| { + child.as_widget_mut().operate( + state, + c_layout.with_virtual_offset(layout.virtual_offset()), + renderer, + operation, + ); + }); + }); + } + + 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, + ) { + for (((i, child), state), c_layout) in self + .children + .iter_mut() + .enumerate() + .zip(&mut tree.children) + .zip(layout.children()) + { + child.as_widget_mut().update( + state, + &event, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + renderer, + clipboard, + shell, + viewport, + ); + } + } + + 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: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + if let Some(clipped_viewport) = layout.bounds().intersection(viewport) { + 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() + { + let t = theme.with_list_item_position(if self.children.len() == 1 { + Some((Alignment::Center, i)) + } else if 0 == i { + Some((Alignment::Start, i)) + } else if i == self.children.len() - 1 { + Some((Alignment::End, i)) + } else { + None + }); + child.as_widget().draw( + state, + renderer, + &t, + style, + c_layout.with_virtual_offset(layout.virtual_offset()), + cursor, + viewport, + ); + } + } + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'b>, + renderer: &Renderer, + viewport: &Rectangle, + translation: Vector, + ) -> Option> { + overlay::from_children( + &mut self.children, + tree, + layout, + renderer, + viewport, + 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, Renderer> From> + for Element<'a, Message, crate::Theme, Renderer> +where + Message: 'a, + Renderer: iced::core::Renderer + 'a, +{ + fn from(column: MenuColumn<'a, Message, Renderer>) -> Self { + Self::new(column) + } +} diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 6211e201..bb33be5e 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -16,7 +16,7 @@ use super::menu_tree::MenuTree; use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::style::menu_bar::StyleSheet; -use iced::window; +use iced::{Alignment, window}; use iced_core::{Border, Renderer as IcedRenderer, Shadow, Widget}; use iced_widget::core::layout::{Limits, Node}; use iced_widget::core::mouse::{self, Cursor}; @@ -836,12 +836,25 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { // draw item menu_roots[start_index..=end_index] .iter() + .enumerate() .zip(children_layout.children()) - .for_each(|(mt, clo)| { + .for_each(|((i, mt), clo)| { + let t = theme.with_list_item_position( + if start_index == end_index { + Some((Alignment::Center, i)) + } else if 0 == i { + Some((Alignment::Start, i)) + } else if i == end_index - start_index { + Some((Alignment::End, i)) + } else { + None + }, + ); + mt.item.draw( &state.tree.children[active_root[0]].children[mt.index], r, - theme, + &t, style, clo, view_cursor, From 40792f21de4efb81dcdcab3fe8ca72c78a51be02 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 5 May 2026 13:11:35 -0400 Subject: [PATCH 085/201] Revert "remove header container" This reverts commit 8035ef9535fa87deb05d88594a6b6a412324c039. --- src/app/mod.rs | 28 +++++++++++++++++++++++++++- 1 file changed, 27 insertions(+), 1 deletion(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index b007757c..e2d6ae1c 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -815,7 +815,33 @@ impl ApplicationExt for App { header = header.end(element.map(crate::Action::App)); } - header.apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_header"))) + if content_container { + header.apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_header"))) + } else { + // Needed to avoid header bar corner gaps for apps without a content container + header + .apply(container) + .class(crate::theme::Container::custom(move |theme| { + let cosmic = theme.cosmic(); + container::Style { + background: Some(iced::Background::Color( + cosmic.background(theme.transparent).base.into(), + )), + border: iced::Border { + radius: [ + (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], + ] + .into(), + ..Default::default() + }, + ..Default::default() + } + })) + .apply(|w| id_container(w, iced_core::id::Id::new("COSMIC_header"))) + } }) } else { None From 90ac265ee05fd42ac5a662fbd93dbc1c860c1117 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 6 May 2026 11:59:36 -0400 Subject: [PATCH 086/201] chore: remove unnecessary bound --- src/surface/mod.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/surface/mod.rs b/src/surface/mod.rs index f4b9417a..5377b421 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -81,7 +81,7 @@ pub enum Action { } #[cfg(feature = "winit")] -pub fn surface_task(action: Action) -> Task> { +pub fn surface_task(action: Action) -> Task> { crate::task::message(crate::Action::Cosmic(crate::app::Action::Surface(action))) } From bd56f82ca01946fe0abd7e19d45423dfbf49c397 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 6 May 2026 16:58:56 -0400 Subject: [PATCH 087/201] fix(theme): color serialization --- cosmic-theme/src/model/color.rs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/cosmic-theme/src/model/color.rs b/cosmic-theme/src/model/color.rs index cd64ec7f..fb4c2202 100644 --- a/cosmic-theme/src/model/color.rs +++ b/cosmic-theme/src/model/color.rs @@ -155,7 +155,7 @@ pub mod color_serde { S: Serializer, { match value { - Some(v) => super::serialize(v, serializer), + Some(v) => Some(v.to_repr()).serialize(serializer), None => serializer.serialize_none(), } } From 58980231b9423c5795055bb935cea18b7d5184ce Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Sun, 10 May 2026 22:52:00 -0400 Subject: [PATCH 088/201] refactor: allow opting out of auto corner radii tracking --- src/app/cosmic.rs | 121 +++++++++++++++++++++++++++++++------------- src/core.rs | 109 ++++++++++++++++++++------------------- src/widget/cards.rs | 23 ++++----- 3 files changed, 153 insertions(+), 100 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 6fd97110..30fadcdc 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -6,11 +6,15 @@ use std::collections::HashMap; use std::sync::Arc; use super::{Action, Application, ApplicationExt, Subscription}; +#[cfg(all(feature = "wayland", target_os = "linux"))] +use crate::core::Auto; use crate::theme::{THEME, Theme, ThemeType}; use crate::{Core, Element, keyboard_nav}; #[cfg(all(feature = "wayland", target_os = "linux"))] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; +#[cfg(all(feature = "wayland", target_os = "linux"))] +use enumflags2::BitFlags; #[cfg(not(any(feature = "multi-window", feature = "wayland", target_os = "linux")))] use iced::Application as IcedApplication; #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -754,11 +758,12 @@ impl Cosmic { 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_winit::platform_specific::commands::corner_radius::corner_radius; let theme = THEME.lock().unwrap(); - let rounded = !self.app.core().window.sharp_corners + let rounded = (!self.app.core().window.sharp_corners + && self.app.core().sync_window_border_radii_to_theme()) || self .surface_views .get(&id) @@ -772,8 +777,8 @@ impl Cosmic { } }); - let cur_rad = self.app.core().app_type.corners(&theme, rounded); - return Task::batch([corner_radius(id, Some(cur_rad)).discard()]); + let cur_rad = self.app.core().corners(&theme, rounded); + return Task::batch([corner_radius(id, cur_rad).discard()]); } } @@ -864,9 +869,9 @@ impl Cosmic { guard.transparent = new_blur; drop(guard); - let core = self.app.core(); #[cfg(all(feature = "wayland", target_os = "linux"))] { + let core = self.app.core(); let mut cmds = Vec::with_capacity(1 + self.surface_views.len()); let blur = if new_blur { iced::window::enable_blur @@ -942,14 +947,13 @@ impl Cosmic { cosmic_theme.transparent = new_blur; #[cfg(all(feature = "wayland", target_os = "linux"))] - if self.app.core().sync_window_border_radii_to_theme() { + { use iced_winit::platform_specific::commands::corner_radius::corner_radius; - let t = cosmic_theme.cosmic(); + let rounded = self.app.core().sync_window_border_radii_to_theme() + && !self.app.core().window.sharp_corners; - let rounded = !self.app.core().window.sharp_corners; - - let cur_rad = self.app.core().app_type.corners(&cosmic_theme, rounded); + let cur_rad = self.app.core().corners(&cosmic_theme, rounded); // Update radius for the main window let main_window_id = self @@ -957,16 +961,31 @@ 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, cur_rad).discard()]; // Update radius for each tracked view with the window surface type for (id, (_, surface_type, _)) in &self.surface_views { - let cur_rad = corners(*surface_type, rounded, &cosmic_theme); - cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + let cur_rad = corners( + *surface_type, + rounded, + &cosmic_theme, + self.app.core().auto_corner_radius, + ); + if cur_rad.is_none() { + continue; + } + cmds.push(corner_radius(*id, cur_rad).discard()); } for (id, wrapper) in &self.tracked_surfaces { - let cur_rad = corners(*wrapper, rounded, &cosmic_theme); - cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + let cur_rad = corners( + *wrapper, + rounded, + &cosmic_theme, + self.app.core().auto_corner_radius, + ); + if cur_rad.is_none() { + continue; + } + cmds.push(corner_radius(*id, cur_rad).discard()); } return Task::batch(cmds); @@ -1045,12 +1064,12 @@ impl Cosmic { cosmic_theme.set_theme(new_theme.theme_type); cosmic_theme.transparent = new_blur; #[cfg(all(feature = "wayland", target_os = "linux"))] - if self.app.core().sync_window_border_radii_to_theme() { + { use iced_winit::platform_specific::commands::corner_radius::corner_radius; - let rounded = !self.app.core().window.sharp_corners; - let cur_rad = - self.app.core().app_type.corners(&cosmic_theme, rounded); + let rounded = self.app.core().sync_window_border_radii_to_theme() + && !self.app.core().window.sharp_corners; + let cur_rad = self.app.core().corners(&cosmic_theme, rounded); // Update radius for the main window let main_window_id = self @@ -1059,15 +1078,31 @@ impl Cosmic { .main_window_id() .unwrap_or(window::Id::RESERVED); let mut cmds = - vec![corner_radius(main_window_id, Some(cur_rad)).discard()]; + vec![corner_radius(main_window_id, cur_rad).discard()]; // Update radius for each tracked view with the window surface type for (id, (_, surface_type, _)) in &self.surface_views { - let cur_rad = corners(*surface_type, rounded, &cosmic_theme); - cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + let cur_rad = corners( + *surface_type, + rounded, + &cosmic_theme, + self.app.core().auto_corner_radius, + ); + if cur_rad.is_none() { + continue; + } + cmds.push(corner_radius(*id, cur_rad).discard()); } for (id, wrapper) in &self.tracked_surfaces { - let cur_rad = corners(*wrapper, rounded, &cosmic_theme); - cmds.push(corner_radius(*id, Some(cur_rad)).discard()); + let cur_rad = corners( + *wrapper, + rounded, + &cosmic_theme, + self.app.core().auto_corner_radius, + ); + if cur_rad.is_none() { + continue; + } + cmds.push(corner_radius(*id, cur_rad).discard()); } let core = self.app.core(); @@ -1344,13 +1379,14 @@ impl Cosmic { } Action::Opened(id) => { #[cfg(all(feature = "wayland", target_os = "linux"))] - if self.app.core().sync_window_border_radii_to_theme() { + { use iced_winit::platform_specific::commands::corner_radius::corner_radius; let mut theme = THEME.lock().unwrap(); // TODO do we need per window sharp corners? - let rounded = !self.app.core().window.sharp_corners + let rounded = (!self.app.core().window.sharp_corners + && self.app.core().sync_window_border_radii_to_theme()) || self .surface_views .get(&id) @@ -1374,6 +1410,7 @@ impl Cosmic { .get(&id) .map(|s| s.1) .or(self.tracked_surfaces.get(&id).copied()); + // this will blur untracked windows as if they were the main window let blur_cmd = if self.app.core().blur(&theme, wrapper) { let blur = if new_blur { @@ -1389,13 +1426,16 @@ impl Cosmic { Task::none() }; - let corner_task = if let Some(s) = self + let corner_task = if let Some((_, cur_rad)) = self .surface_views .get(&id) .map(|s| s.1) .or(self.tracked_surfaces.get(&id).copied()) - { - corner_radius(id, Some(corners(s, rounded, &theme))).discard() + .and_then(|s| { + corners(s, rounded, &theme, self.app.core().auto_corner_radius) + .map(|c| (s, c)) + }) { + corner_radius(id, Some(cur_rad)).discard() } else if id == self .app @@ -1403,11 +1443,11 @@ impl Cosmic { .main_window_id() .unwrap_or(window::Id::RESERVED) { - corner_radius(id, Some(self.app.core().app_type.corners(&theme, rounded))) - .discard() + corner_radius(id, self.app.core().corners(&theme, rounded)).discard() } else { Task::none() }; + return Task::batch([ blur_cmd, corner_task, @@ -1580,9 +1620,18 @@ fn corners( surface_type: SurfaceIdWrapper, rounded: bool, theme: &Theme, -) -> iced_runtime::platform_specific::wayland::CornerRadius { + auto_corner_radius: BitFlags, +) -> Option { + if !match surface_type { + SurfaceIdWrapper::Window(_) => auto_corner_radius.contains(Auto::Window), + SurfaceIdWrapper::LayerSurface(_) => auto_corner_radius.contains(Auto::System), + SurfaceIdWrapper::Popup(_) => auto_corner_radius.contains(Auto::Popup), + _ => false, + } { + return None; + } let theme = theme.cosmic(); - if let SurfaceIdWrapper::Popup(_) = surface_type { + Some(if let SurfaceIdWrapper::Popup(_) = surface_type { let radius_m = theme.radius_m(); iced_runtime::platform_specific::wayland::CornerRadius { top_left: radius_m[0].round() as u32, @@ -1591,7 +1640,7 @@ fn corners( bottom_left: radius_m[3].round() as u32, } } else if let SurfaceIdWrapper::Window(_) = surface_type - && rounded + && !rounded { let radius_0 = theme.radius_0(); iced_runtime::platform_specific::wayland::CornerRadius { @@ -1608,5 +1657,5 @@ fn corners( bottom_right: radius_s[2].round() as u32, bottom_left: radius_s[3].round() as u32, } - } + }) } diff --git a/src/core.rs b/src/core.rs index d52f2fe0..a8691d11 100644 --- a/src/core.rs +++ b/src/core.rs @@ -112,11 +112,10 @@ pub struct Core { pub(crate) menu_bars: HashMap, - #[cfg(all(feature = "wayland", target_os = "linux"))] - pub(crate) sync_window_border_radii_to_theme: bool, - pub(crate) auto_blur: BitFlags, + pub(crate) auto_corner_radius: BitFlags, + pub(crate) app_type: AppType, } @@ -130,45 +129,6 @@ pub enum AppType { Applet, } -impl AppType { - /// Calculate suggested corners for each app type main window - #[cfg(all(feature = "wayland", target_os = "linux"))] - pub fn corners( - &self, - theme: &Theme, - rounded: bool, - ) -> iced_runtime::platform_specific::wayland::CornerRadius { - let theme = theme.cosmic(); - if let Self::Applet = self { - let radius_l = theme.radius_l(); - iced_runtime::platform_specific::wayland::CornerRadius { - top_left: radius_l[0].round() as u32, - top_right: radius_l[1].round() as u32, - bottom_right: radius_l[2].round() as u32, - bottom_left: radius_l[3].round() as u32, - } - } else if let Self::Window = self - && rounded - { - let radius_0 = theme.radius_0(); - iced_runtime::platform_specific::wayland::CornerRadius { - top_left: radius_0[0].round() as u32, - top_right: radius_0[1].round() as u32, - bottom_right: radius_0[2].round() as u32, - bottom_left: radius_0[3].round() as u32, - } - } else { - let radius_s = theme.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); - iced_runtime::platform_specific::wayland::CornerRadius { - top_left: radius_s[0].round() as u32, - top_right: radius_s[1].round() as u32, - bottom_right: radius_s[2].round() as u32, - bottom_left: radius_s[3].round() as u32, - } - } - } -} - impl Default for Core { fn default() -> Self { Self { @@ -225,9 +185,8 @@ impl Default for Core { main_window: None, exit_on_main_window_closed: true, menu_bars: HashMap::new(), - #[cfg(all(feature = "wayland", target_os = "linux"))] - sync_window_border_radii_to_theme: true, auto_blur: Auto::System | Auto::Popup | Auto::Window, + auto_corner_radius: Auto::System | Auto::Popup | Auto::Window, app_type: AppType::Window, } } @@ -560,15 +519,13 @@ impl Core { crate::command::toggle_maximize(id) } - // TODO should we emit tasks setting the corner radius or unsetting it if this is changed? - #[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(all(feature = "wayland", target_os = "linux"))] pub fn sync_window_border_radii_to_theme(&self) -> bool { - self.sync_window_border_radii_to_theme + match self.app_type { + AppType::Window => self.auto_corner_radius.contains(Auto::Window), + AppType::System => self.auto_corner_radius.contains(Auto::System), + AppType::Applet => false, + } } pub fn set_auto_blur(&mut self, auto_blur: BitFlags) { @@ -579,6 +536,14 @@ impl Core { self.auto_blur } + pub fn set_auto_corner_radius(&mut self, auto_corner_radius: BitFlags) { + self.auto_corner_radius = auto_corner_radius; + } + + pub fn auto_corner_radius(&self) -> BitFlags { + self.auto_corner_radius + } + pub fn set_app_type(&mut self, app_type: AppType) { self.app_type = app_type; } @@ -621,4 +586,46 @@ impl Core { _ => false, } } + + /// Calculate suggested corners for each app type main window + #[must_use] + #[cfg(all(feature = "wayland", target_os = "linux"))] + pub fn corners( + &self, + theme: &Theme, + rounded: bool, + ) -> Option { + if !self.sync_window_border_radii_to_theme() { + return None; + } + let theme = theme.cosmic(); + let ret = if let AppType::Applet = self.app_type { + let radius_l = theme.radius_l(); + iced_runtime::platform_specific::wayland::CornerRadius { + top_left: radius_l[0].round() as u32, + top_right: radius_l[1].round() as u32, + bottom_right: radius_l[2].round() as u32, + bottom_left: radius_l[3].round() as u32, + } + } else if let AppType::Window = self.app_type + && !rounded + { + let radius_0 = theme.radius_0(); + iced_runtime::platform_specific::wayland::CornerRadius { + top_left: radius_0[0].round() as u32, + top_right: radius_0[1].round() as u32, + bottom_right: radius_0[2].round() as u32, + bottom_left: radius_0[3].round() as u32, + } + } else { + let radius_s = theme.radius_s().map(|x| if x < 4.0 { x } else { x + 4.0 }); + iced_runtime::platform_specific::wayland::CornerRadius { + top_left: radius_s[0].round() as u32, + top_right: radius_s[1].round() as u32, + bottom_right: radius_s[2].round() as u32, + bottom_left: radius_s[3].round() as u32, + } + }; + Some(ret) + } } diff --git a/src/widget/cards.rs b/src/widget/cards.rs index 00014740..ce1831d0 100644 --- a/src/widget/cards.rs +++ b/src/widget/cards.rs @@ -2,6 +2,7 @@ use std::time::Duration; use crate::anim; +use crate::theme::THEME; use crate::widget::card::style::Style; use crate::widget::icon::{self, Handle}; use crate::widget::{button, column, row, text}; @@ -19,7 +20,6 @@ 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 @@ -337,14 +337,15 @@ where for i in 1..self.elements.len().min(3) { // height must be 16px for 8px padding // but we only want 4px visible + let guard = THEME.lock().unwrap(); + let radius_xs = guard.cosmic().radius_xs(); 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, - )); + let node = Node::new(Size::new(width - 2.0 * margin, radius_xs[0] * 2.0)) + .translate(Vector::new( + margin, + size.height - radius_xs[0] * 2.0 + BG_CARD_VISIBLE_HEIGHT, + )); size.height += BG_CARD_VISIBLE_HEIGHT; children.push(node); } @@ -424,6 +425,7 @@ where _ = tree_children.next(); } + let radius_xs = theme.cosmic().radius_xs(); // Draw first to appear behind if fully_unexpanded { let card_layout = layout.next().unwrap(); @@ -434,12 +436,7 @@ where Quad { bounds: layout.bounds(), border: Border { - radius: Radius::from([ - 0.0, - 0.0, - BG_CARD_BORDER_RADIUS, - BG_CARD_BORDER_RADIUS, - ]), + radius: Radius::from([0.0, 0.0, radius_xs[2], radius_xs[3]]), ..Default::default() }, shadow: Shadow::default(), From f268bb86cf3574fddc70e8b78aae29e2dd1049e4 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 13 May 2026 15:05:21 -0400 Subject: [PATCH 089/201] fix: allow loading v1 themes fallback to default value for missing attributes --- cosmic-theme/src/model/theme.rs | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index b2daabe7..e68d9a09 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -104,14 +104,19 @@ pub struct Theme { pub active_hint: u32, /// cosmic-comp custom window hint color pub window_hint: Option, + #[serde(default)] /// enables blurred transparency pub frosted: BlurStrength, + #[serde(default)] /// frosted windows pub frosted_windows: bool, + #[serde(default)] /// frosted system interface pub frosted_system_interface: bool, + #[serde(default)] /// frosted panel pub frosted_panel: bool, + #[serde(default)] /// frosted applet popups pub frosted_applets: bool, /// shade color for dialogs @@ -876,6 +881,7 @@ pub struct ThemeBuilder { #[serde(with = "color_serde_option")] #[cosmic_config_entry(with = ColorReprOption)] pub destructive: Option, + #[serde(default)] /// enabled blurred transparency pub frosted: BlurStrength, /// cosmic-comp window gaps size (outer, inner) @@ -886,12 +892,16 @@ pub struct ThemeBuilder { #[serde(with = "color_serde_option")] #[cosmic_config_entry(with = ColorReprOption)] pub window_hint: Option, + #[serde(default)] /// frosted windows pub frosted_windows: bool, + #[serde(default)] /// frosted system interface pub frosted_system_interface: bool, + #[serde(default)] /// frosted panel pub frosted_panel: bool, + #[serde(default)] /// frosted applet popups pub frosted_applets: bool, } From ac4a1ab2593b889274e2e640a42ca82433c779da Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 15 May 2026 14:27:17 -0400 Subject: [PATCH 090/201] improv: allow overrides for corner radius or blur that apply on creation as well --- examples/applet/src/window.rs | 1 + src/app/cosmic.rs | 484 ++++++++++++++++----------- src/surface/action.rs | 95 ++++-- src/surface/mod.rs | 94 +++--- src/widget/context_menu.rs | 3 +- src/widget/dropdown/widget.rs | 3 + src/widget/menu/menu_bar.rs | 3 +- src/widget/menu/menu_inner.rs | 3 + src/widget/wayland/tooltip/widget.rs | 14 + 9 files changed, 415 insertions(+), 285 deletions(-) diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 8722a2a4..f77bf512 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -101,6 +101,7 @@ impl cosmic::Application for Window { Message::Surface(destroy_popup(id)) } else { Message::Surface(app_popup::( + |_| Default::default(), move |state: &mut Window| { let new_id = Id::unique(); state.popup = Some(new_id); diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 30fadcdc..edca03e6 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -8,6 +8,8 @@ use std::sync::Arc; use super::{Action, Application, ApplicationExt, Subscription}; #[cfg(all(feature = "wayland", target_os = "linux"))] use crate::core::Auto; +#[cfg(all(feature = "wayland", target_os = "linux"))] +use crate::surface::action::LiveSettings; use crate::theme::{THEME, Theme, ThemeType}; use crate::{Core, Element, keyboard_nav}; #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -21,7 +23,7 @@ use iced::Application as IcedApplication; use iced::event::wayland; use iced::{Task, theme, window}; use iced_futures::event::listen_with; -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(feature = "winit")] use iced_winit::SurfaceIdWrapper; use palette::color_difference::EuclideanDistance; @@ -87,16 +89,22 @@ fn init_windowing_system(handle: window::raw_window_handle::WindowHandle) -> #[derive(Default)] pub struct Cosmic { pub app: App, - #[cfg(all(feature = "wayland", target_os = "linux"))] pub surface_views: HashMap< window::Id, ( Option, SurfaceIdWrapper, - Box Fn(&'a App) -> Element<'a, crate::Action>>, + Box Fn(&'a App) -> crate::surface::action::LiveSettings>, + Option< + Box< + dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + + Send + + Sync + + 'static, + >, + >, ), >, - pub tracked_surfaces: HashMap, pub opened_surfaces: HashMap, blur_enabled: bool, } @@ -144,7 +152,7 @@ where #[cfg(feature = "surface-message")] match _surface_message { #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::AppSubsurface(settings, view) => { + crate::surface::Action::AppSubsurface(settings, live_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 { @@ -152,7 +160,9 @@ where return Task::none(); }; - if let Some(view) = view.and_then(|view| { + let settings = settings(&mut self.app); + + let view = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Fn(&'a T) -> Element<'a, crate::Action> + Send @@ -165,25 +175,18 @@ where None } } - }) { - let settings = settings(&mut self.app); - - self.get_subsurface(settings, *view) - } else { - let settings = settings(&mut self.app); - self.tracked_surfaces - .insert(settings.id, SurfaceIdWrapper::Subsurface(settings.id)); - iced_winit::commands::subsurface::get_subsurface(settings) - } + }); + self.get_subsurface(settings, view.map(|v| *v)) } #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::Subsurface(settings, view) => { + crate::surface::Action::Subsurface(settings, live_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(); }; + let settings = settings(); if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: { + crate::surface::Action::AppPopup(settings, live_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(); }; + let Some(live_settings) = + std::sync::Arc::try_unwrap(live_settings) + .ok() + .and_then(|s| { + s.downcast:: LiveSettings + Send + Sync>>() + .ok() + }) + else { + tracing::error!("Invalid live settings for popup"); + return Task::none(); + }; - if let Some(view) = view.and_then(|view| { + let view = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Fn(&'a T) -> Element<'a, crate::Action> + Send @@ -228,16 +237,10 @@ where None } } - }) { - let settings = settings(&mut self.app); + }); + let settings = settings(&mut self.app); - self.get_popup(settings, *view) - } else { - let settings = settings(&mut self.app); - self.tracked_surfaces - .insert(settings.id, SurfaceIdWrapper::Popup(settings.id)); - iced_winit::commands::popup::get_popup(settings) - } + self.get_popup(settings, *live_settings, view.map(|v| *v)) } #[cfg(all(feature = "wayland", target_os = "linux"))] crate::surface::Action::DestroyPopup(id) => { @@ -270,7 +273,7 @@ where iced::Task::none() } #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::Popup(settings, view) => { + crate::surface::Action::Popup(settings, live_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 { @@ -278,6 +281,20 @@ where return Task::none(); }; + let Some(live_settings) = + std::sync::Arc::try_unwrap(live_settings) + .ok() + .and_then(|s| { + s.downcast:: LiveSettings + Send + Sync>>() + .ok() + }) + else { + tracing::error!("Invalid live settings for popup"); + return Task::none(); + }; + let settings = settings(); + let live_settings = Box::new(move |app: &T| live_settings()); + if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Element<'static, crate::Action> + Send + Sync, @@ -289,18 +306,13 @@ where } } }) { - let settings = settings(); - - self.get_popup(settings, Box::new(move |_| view())) + self.get_popup(settings, live_settings, Some(Box::new(move |_| view()))) } else { - let settings = settings(); - self.tracked_surfaces - .insert(settings.id, SurfaceIdWrapper::Popup(settings.id)); - iced_winit::commands::popup::get_popup(settings) + self.get_popup(settings, live_settings, None) } } #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::AppWindow(id, settings, view) => { + crate::surface::Action::AppWindow(id, settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { s.downcast:: iced::window::Settings + Send + Sync>>() .ok() @@ -308,8 +320,19 @@ where tracing::error!("Invalid settings for AppWindow"); return Task::none(); }; + let Some(live_settings) = + std::sync::Arc::try_unwrap(live_settings) + .ok() + .and_then(|s| { + s.downcast:: LiveSettings + Send + Sync>>() + .ok() + }) + else { + tracing::error!("Invalid live settings for popup"); + return Task::none(); + }; - if let Some(view) = view.and_then(|view| { + let view = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Fn(&'a T) -> Element<'a, crate::Action> + Send @@ -321,24 +344,13 @@ where None } } - }) { - let settings = settings(&mut self.app); + }); + let settings = settings(&mut self.app); - self.get_window(id, settings, *view) - } else { - let settings = settings(&mut self.app); - self.tracked_surfaces - .insert(id, SurfaceIdWrapper::Window(id)); - iced_runtime::task::oneshot(|channel| { - iced_runtime::Action::Window(iced_runtime::window::Action::Open( - id, settings, channel, - )) - }) - .discard() - } + self.get_window(id, settings, *live_settings, view.map(|v| *v)) } #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::Window(id, settings, view) => { + crate::surface::Action::Window(id, settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { s.downcast:: iced::window::Settings + Send + Sync>>() .ok() @@ -347,6 +359,18 @@ where return Task::none(); }; + let Some(live_settings) = + std::sync::Arc::try_unwrap(live_settings) + .ok() + .and_then(|s| { + s.downcast:: LiveSettings + Send + Sync>>() + .ok() + }) + else { + tracing::error!("Invalid live settings for popup"); + 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, @@ -360,11 +384,14 @@ where }) { let settings = settings(); - self.get_window(id, settings, Box::new(move |_| view())) + self.get_window( + id, + settings, + Box::new(move |_| live_settings()), + Some(Box::new(move |_| view())), + ) } else { let settings = settings(); - self.tracked_surfaces - .insert(id, SurfaceIdWrapper::Window(id)); iced_runtime::task::oneshot(|channel| { iced_runtime::Action::Window(iced_runtime::window::Action::Open( @@ -380,15 +407,26 @@ where f().map(|sm| crate::Action::Cosmic(Action::Surface(sm))) } #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::AppLayerShell(settings, view) => { + crate::surface::Action::AppLayerShell(settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + Send + Sync>>().ok()) else { tracing::error!("Invalid settings for layer surface"); return Task::none(); }; + let Some(live_settings) = + std::sync::Arc::try_unwrap(live_settings) + .ok() + .and_then(|s| { + s.downcast:: LiveSettings + Send + Sync>>() + .ok() + }) + else { + tracing::error!("Invalid live settings for popup"); + return Task::none(); + }; - if let Some(view) = view.and_then(|view| { + let view = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Fn(&'a T) -> Element<'a, crate::Action> + Send @@ -400,19 +438,14 @@ where None } } - }) { - let settings = settings(&mut self.app); + }); - self.get_layer_shell(settings, *view) - } else { - let settings = settings(&mut self.app); - self.tracked_surfaces - .insert(settings.id, SurfaceIdWrapper::LayerSurface(settings.id)); - iced_winit::commands::layer_surface::get_layer_surface(settings) - } + let settings = settings(&mut self.app); + + self.get_layer_shell(settings, *live_settings, view.map(|v| *v)) } #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::LayerShell(settings, view) => { + crate::surface::Action::LayerShell(settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() .and_then(|s| s.downcast:: iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + Send + Sync>>().ok()) else { @@ -420,6 +453,21 @@ where return Task::none(); }; + let Some(live_settings) = + std::sync::Arc::try_unwrap(live_settings) + .ok() + .and_then(|s| { + s.downcast:: LiveSettings + Send + Sync>>() + .ok() + }) + else { + tracing::error!("Invalid live settings for popup"); + return Task::none(); + }; + let settings = settings(); + let live_settings = live_settings(); + let live_settings = Box::new(move |_app: &T| live_settings); + if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Element<'static, crate::Action> + Send + Sync, @@ -431,16 +479,9 @@ where } } }) { - let settings = settings(); - - self.get_layer_shell(settings, Box::new(move |_| view())) + self.get_layer_shell(settings, live_settings, Some(Box::new(move |_| view()))) } else { - let settings = settings(); - self.tracked_surfaces.insert( - settings.id, - iced_winit::SurfaceIdWrapper::LayerSurface(settings.id), - ); - iced_winit::commands::layer_surface::get_layer_surface(settings) + self.get_layer_shell(settings, live_settings, None) } } #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -657,8 +698,7 @@ where #[cfg(feature = "multi-window")] pub fn view(&self, id: window::Id) -> Element<'_, crate::Action> { - #[cfg(all(feature = "wayland", target_os = "linux"))] - if let Some((_, _, v)) = self.surface_views.get(&id) { + if let Some((_, _, _, Some(v))) = self.surface_views.get(&id) { return v(&self.app); } if self @@ -762,20 +802,8 @@ impl Cosmic { use iced_winit::platform_specific::commands::corner_radius::corner_radius; let theme = THEME.lock().unwrap(); - let rounded = (!self.app.core().window.sharp_corners - && self.app.core().sync_window_border_radii_to_theme()) - || self - .surface_views - .get(&id) - .is_some_and(|(_, surface_type, _)| { - if let SurfaceIdWrapper::Popup(_) - | SurfaceIdWrapper::LayerSurface(_) = surface_type - { - true - } else { - false - } - }); + let rounded = !self.app.core().window.sharp_corners + && self.app.core().sync_window_border_radii_to_theme(); let cur_rad = self.app.core().corners(&theme, rounded); return Task::batch([corner_radius(id, cur_rad).discard()]); @@ -887,13 +915,12 @@ impl Cosmic { )); } for (id, wrapper, ..) in &self.surface_views { - if core.blur(&theme, Some(wrapper.1)) { - cmds.push(blur(*id)); - } - } - for (id, wrapper) in &self.tracked_surfaces { - if core.blur(&theme, Some(*wrapper)) { + let overriden = wrapper.2(&self.app); + if core.blur(&theme, Some(wrapper.1)) || overriden.blur.unwrap_or_default() + { cmds.push(blur(*id)); + } else if overriden.blur.is_some_and(|b| !b) { + cmds.push(iced::window::disable_blur(*id)); } } return Task::batch(cmds); @@ -963,25 +990,18 @@ impl Cosmic { .unwrap_or(window::Id::RESERVED); let mut cmds = vec![corner_radius(main_window_id, cur_rad).discard()]; // Update radius for each tracked view with the window surface type - for (id, (_, surface_type, _)) in &self.surface_views { - let cur_rad = corners( - *surface_type, - rounded, - &cosmic_theme, - self.app.core().auto_corner_radius, - ); - if cur_rad.is_none() { - continue; - } - cmds.push(corner_radius(*id, cur_rad).discard()); - } - for (id, wrapper) in &self.tracked_surfaces { - let cur_rad = corners( - *wrapper, - rounded, - &cosmic_theme, - self.app.core().auto_corner_radius, - ); + for (id, (_, surface_type, live_settings, _)) in &self.surface_views { + let overriden = live_settings(&self.app); + let cur_rad = if let Some(c) = overriden.corners { + Some(c) + } else { + corners( + *surface_type, + rounded, + &cosmic_theme, + self.app.core().auto_corner_radius, + ) + }; if cur_rad.is_none() { continue; } @@ -1080,25 +1100,19 @@ impl Cosmic { let mut cmds = vec![corner_radius(main_window_id, cur_rad).discard()]; // Update radius for each tracked view with the window surface type - for (id, (_, surface_type, _)) in &self.surface_views { - let cur_rad = corners( - *surface_type, - rounded, - &cosmic_theme, - self.app.core().auto_corner_radius, - ); - if cur_rad.is_none() { - continue; - } - cmds.push(corner_radius(*id, cur_rad).discard()); - } - for (id, wrapper) in &self.tracked_surfaces { - let cur_rad = corners( - *wrapper, - rounded, - &cosmic_theme, - self.app.core().auto_corner_radius, - ); + for (id, (_, surface_type, live_settings, _)) in &self.surface_views + { + let overriden = live_settings(&self.app); + let cur_rad = if let Some(c) = overriden.corners { + Some(c) + } else { + corners( + *surface_type, + rounded, + &cosmic_theme, + self.app.core().auto_corner_radius, + ) + }; if cur_rad.is_none() { continue; } @@ -1121,13 +1135,13 @@ impl Cosmic { )); } for (id, wrapper, ..) in &self.surface_views { - if core.blur(&cosmic_theme, Some(wrapper.1)) { - cmds.push(blur(*id)); - } - } - for (id, wrapper) in &self.tracked_surfaces { - if core.blur(&cosmic_theme, Some(*wrapper)) { + let overriden = wrapper.2(&self.app); + if core.blur(&cosmic_theme, Some(wrapper.1)) + || overriden.blur.unwrap_or_default() + { cmds.push(blur(*id)); + } else if overriden.blur.is_some_and(|b| !b) { + cmds.push(iced::window::disable_blur(*id)); } } @@ -1171,12 +1185,9 @@ impl Cosmic { *v == 0 }) { self.opened_surfaces.remove(&id); - #[cfg(all(feature = "wayland", target_os = "linux"))] self.surface_views.remove(&id); } - self.tracked_surfaces.remove(&id); - self.tracked_surfaces - .shrink_to(self.tracked_surfaces.len() * 2); + self.surface_views.shrink_to(self.surface_views.len() * 2); let mut ret = if let Some(msg) = self.app.on_close_requested(id) { self.app.update(msg) @@ -1247,6 +1258,7 @@ impl Cosmic { new_theme.transparent = new_blur; } core.system_theme = new_theme.clone(); + let core = self.app.core(); { let mut cosmic_theme = THEME.lock().unwrap(); @@ -1266,14 +1278,15 @@ impl Cosmic { core.main_window_id().unwrap_or(window::Id::RESERVED), )); } + for (id, wrapper, ..) in &self.surface_views { - if core.blur(&cosmic_theme, Some(wrapper.1)) { - cmds.push(blur(*id)); - } - } - for (id, wrapper) in &self.tracked_surfaces { - if core.blur(&cosmic_theme, Some(*wrapper)) { + let overriden = wrapper.2(&self.app); + if core.blur(&cosmic_theme, Some(wrapper.1)) + || overriden.blur.unwrap_or_default() + { cmds.push(blur(*id)); + } else if overriden.blur.is_some_and(|b| !b) { + cmds.push(iced::window::disable_blur(*id)); } } } @@ -1339,6 +1352,7 @@ impl Cosmic { parent, SurfaceIdWrapper::Subsurface(_) | SurfaceIdWrapper::Popup(_), _, + _, )) = self.surface_views.get(&f) { // If the parent is already focused, push the new focus @@ -1355,7 +1369,7 @@ impl Cosmic { cur = self .surface_views .get(&p) - .and_then(|(parent, _, _)| *parent); + .and_then(|(parent, _, _, _)| *parent); } parent_chain.reverse(); self.app.core_mut().focused_window = parent_chain; @@ -1387,15 +1401,14 @@ impl Cosmic { // TODO do we need per window sharp corners? let rounded = (!self.app.core().window.sharp_corners && self.app.core().sync_window_border_radii_to_theme()) - || self - .surface_views - .get(&id) - .is_some_and(|(_, surface_type, _)| { + || self.surface_views.get(&id).is_some_and( + |(_, surface_type, live_settings, _)| { matches!( surface_type, SurfaceIdWrapper::Popup(_) | SurfaceIdWrapper::LayerSurface(_) ) - }); + }, + ); let new_blur = self.blur_enabled && { let t = theme.cosmic(); match self.app.core().app_type() { @@ -1405,11 +1418,7 @@ impl Cosmic { } }; - let wrapper = self - .surface_views - .get(&id) - .map(|s| s.1) - .or(self.tracked_surfaces.get(&id).copied()); + let wrapper = self.surface_views.get(&id).map(|s| s.1); // this will blur untracked windows as if they were the main window let blur_cmd = if self.app.core().blur(&theme, wrapper) { @@ -1426,15 +1435,18 @@ impl Cosmic { Task::none() }; - let corner_task = if let Some((_, cur_rad)) = self - .surface_views - .get(&id) - .map(|s| s.1) - .or(self.tracked_surfaces.get(&id).copied()) - .and_then(|s| { - corners(s, rounded, &theme, self.app.core().auto_corner_radius) - .map(|c| (s, c)) - }) { + let corner_task = if let Some((_, cur_rad)) = + self.surface_views.get(&id).map(|s| (s.1, &s.2)).and_then( + |(s, overriden)| { + let overriden = overriden(&self.app); + if let Some(c) = overriden.corners { + Some((s, c)) + } else { + corners(s, rounded, &theme, self.app.core().auto_corner_radius) + .map(|c| (s, c)) + } + }, + ) { corner_radius(id, Some(cur_rad)).discard() } else if id == self @@ -1490,13 +1502,13 @@ impl Cosmic { )); } for (id, wrapper, ..) in &self.surface_views { - if self.app.core().blur(&t, Some(wrapper.1)) { - cmds.push(blur(*id)); - } - } - for (id, wrapper) in &self.tracked_surfaces { - if self.app.core().blur(&t, Some(*wrapper)) { + let overriden = wrapper.2(&self.app); + if self.app.core().blur(&t, Some(wrapper.1)) + || overriden.blur.unwrap_or_default() + { cmds.push(blur(*id)); + } else if overriden.blur.is_some_and(|b| !b) { + cmds.push(iced::window::disable_blur(*id)); } } return Task::batch(cmds); @@ -1513,35 +1525,79 @@ impl Cosmic { pub fn new(app: App) -> Self { Self { app, - #[cfg(all(feature = "wayland", target_os = "linux"))] surface_views: HashMap::new(), opened_surfaces: HashMap::new(), - tracked_surfaces: HashMap::new(), blur_enabled: false, } } + #[cfg(all(feature = "wayland", feature = "winit", target_os = "linux"))] + /// Apply live setting overrides for a surface + pub fn apply_live_settings( + &mut self, + id_wrapper: SurfaceIdWrapper, + live_settings: &LiveSettings, + ) -> Task> { + use iced_winit::commands::corner_radius; + + let id = id_wrapper.inner(); + + let mut cmds = Vec::with_capacity(2); + let t = THEME.try_lock().unwrap(); + + if let Some(blur) = live_settings.blur { + let blur_cmd = if blur { + iced::window::enable_blur + } else { + iced::window::disable_blur + }; + cmds.push(blur_cmd(id)); + } else if self.app.core().blur(&*t, Some(id_wrapper)) { + cmds.push(iced::window::enable_blur(id)); + } + if let Some(corners) = live_settings.corners { + cmds.push(corner_radius::corner_radius(id, Some(corners)).discard()); + } else { + let rounded = !self.app.core().window.sharp_corners + && self.app.core().sync_window_border_radii_to_theme(); + if let Some(cur_rad) = + corners(id_wrapper, rounded, &*t, self.app.core().auto_corner_radius) + { + cmds.push(corner_radius::corner_radius(id, Some(cur_rad)).discard()) + } + } + Task::batch(cmds) + } + #[cfg(all(feature = "wayland", target_os = "linux"))] /// Create a subsurface pub fn get_subsurface( &mut self, settings: iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings, - view: Box< - dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync, + view: Option< + Box Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync>, >, ) -> Task> { use iced_winit::commands::subsurface::get_subsurface; *self.opened_surfaces.entry(settings.id).or_insert_with(|| 0) += 1; + let live_settings_task = self.apply_live_settings( + SurfaceIdWrapper::Subsurface(settings.id), + &LiveSettings { + blur: Some(self.blur_enabled), + corners: None, + }, + ); self.surface_views.insert( settings.id, ( Some(settings.parent), SurfaceIdWrapper::Subsurface(settings.id), + Box::new(|_| LiveSettings::default()), view, ), ); - get_subsurface(settings) + Task::batch([get_subsurface(settings), live_settings_task]) } #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -1549,21 +1605,27 @@ impl Cosmic { pub fn get_popup( &mut self, settings: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings, - view: Box< - dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync, + live_settings: Box Fn(&'a App) -> LiveSettings + Send + Sync>, + view: Option< + Box Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync>, >, ) -> Task> { use iced_winit::commands::popup::get_popup; *self.opened_surfaces.entry(settings.id).or_insert_with(|| 0) += 1; + let live_settings_task = self.apply_live_settings( + SurfaceIdWrapper::Popup(settings.id), + &live_settings(&self.app), + ); self.surface_views.insert( settings.id, ( Some(settings.parent), SurfaceIdWrapper::Popup(settings.id), + live_settings, view, ), ); - get_popup(settings) + Task::batch([get_popup(settings), live_settings_task]) } #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -1572,46 +1634,62 @@ impl Cosmic { &mut self, id: iced::window::Id, settings: iced::window::Settings, - view: Box< - dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync, + live_settings: Box Fn(&'a App) -> LiveSettings + Send + Sync>, + + view: Option< + Box Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync>, >, ) -> Task> { use iced_winit::SurfaceIdWrapper; *self.opened_surfaces.entry(id).or_insert(0) += 1; + let live_settings_task = + self.apply_live_settings(SurfaceIdWrapper::Window(id), &live_settings(&self.app)); self.surface_views.insert( id, ( None, // TODO parent for window, platform specific option maybe? SurfaceIdWrapper::Window(id), + live_settings, view, ), ); - iced_runtime::task::oneshot(|channel| { - iced_runtime::Action::Window(iced_runtime::window::Action::Open(id, settings, channel)) - }) - .discard() + Task::batch([ + iced_runtime::task::oneshot(|channel| { + iced_runtime::Action::Window(iced_runtime::window::Action::Open( + id, settings, channel, + )) + }) + .discard(), + live_settings_task, + ]) } #[cfg(all(feature = "wayland", target_os = "linux"))] pub fn get_layer_shell( &mut self, settings: iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings, - view: Box< - dyn for<'a> Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync, + live_settings: Box Fn(&'a App) -> LiveSettings + Send + Sync>, + view: Option< + Box Fn(&'a App) -> Element<'a, crate::Action> + Send + Sync>, >, ) -> Task> { use iced_winit::SurfaceIdWrapper; use iced_winit::platform_specific::commands::layer_surface::get_layer_surface; *self.opened_surfaces.entry(settings.id).or_insert(0) += 1; + let live_settings_task = self.apply_live_settings( + SurfaceIdWrapper::LayerSurface(settings.id), + &live_settings(&self.app), + ); self.surface_views.insert( settings.id, ( None, // TODO parent for layer shell, platform specific option maybe? SurfaceIdWrapper::LayerSurface(settings.id), + live_settings, view, ), ); - get_layer_surface(settings) + Task::batch([get_layer_surface(settings), live_settings_task]) } } diff --git a/src/surface/action.rs b/src/surface/action.rs index 300d844b..23a72e68 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -5,7 +5,9 @@ use super::Action; #[cfg(feature = "winit")] use crate::Application; -use iced::window; +use iced::{Rectangle, window}; +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +use iced_runtime::platform_specific::wayland::CornerRadius; use std::any::Any; use std::sync::Arc; @@ -34,18 +36,30 @@ pub fn destroy_layer_shell(id: iced_core::window::Id) -> Action { Action::DestroyLayerShell(id) } +#[derive(Debug, Default, Copy, Clone)] +pub struct LiveSettings { + #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] + /// Override the default corner radius value for the surface type. + pub corners: Option, + /// Override the default blur setting for the surface type. + pub blur: Option, +} + +type BoxedView = Option< + Box< + dyn Fn(&App) -> crate::Element<'_, crate::Action<::Message>> + + Send + + Sync + + 'static, + >, +>; + #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn app_window( + live_settings: impl Fn(&App) -> LiveSettings + 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> - + Send - + Sync - + 'static, - >, - >, + view: BoxedView, ) -> (window::Id, Action) { let id = window::Id::unique(); @@ -53,11 +67,16 @@ pub fn app_window( Box::new(settings); let boxed: Box = Box::new(boxed); + let boxed_live: Box LiveSettings + Send + Sync + 'static> = + Box::new(live_settings); + let boxed_live: Box = Box::new(boxed_live); + ( id, Action::AppWindow( id, Arc::new(boxed), + Arc::new(boxed_live), view.map(|view| { let boxed: Box = Box::new(view); Arc::new(boxed) @@ -70,6 +89,7 @@ pub fn app_window( #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn simple_window( + live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, settings: impl Fn() -> window::Settings + Send + Sync + 'static, view: Option< impl Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, @@ -80,11 +100,15 @@ pub fn simple_window( let boxed: Box window::Settings + Send + Sync + 'static> = Box::new(settings); let boxed: Box = Box::new(boxed); + let boxed_live: Box LiveSettings + Send + Sync + 'static> = Box::new(live_settings); + let boxed_live: Box = Box::new(boxed_live); + ( id, Action::Window( id, Arc::new(boxed), + Arc::new(boxed_live), view.map(|view| { let boxed: Box< dyn Fn() -> crate::Element<'static, crate::Action> @@ -102,18 +126,12 @@ pub fn simple_window( #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn app_popup( + live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static, settings: impl Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync + 'static, - view: Option< - Box< - dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action> - + Send - + Sync - + 'static, - >, - >, + view: BoxedView, ) -> Action { let boxed: Box< dyn Fn(&mut App) -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings @@ -123,8 +141,13 @@ pub fn app_popup( > = Box::new(settings); let boxed: Box = Box::new(boxed); + let boxed_live: Box LiveSettings + Send + Sync + 'static> = + Box::new(live_settings); + let boxed_live: Box = Box::new(boxed_live); + Action::AppPopup( Arc::new(boxed), + Arc::new(boxed_live), view.map(|view| { let boxed: Box = Box::new(view); Arc::new(boxed) @@ -154,6 +177,7 @@ pub fn simple_subsurface( Action::Subsurface( Arc::new(boxed), + Arc::new(Box::new(LiveSettings::default)), view.map(|view| { let boxed: Box = Box::new(view); Arc::new(boxed) @@ -165,6 +189,7 @@ pub fn simple_subsurface( #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn simple_popup( + live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, settings: impl Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + Send + Sync @@ -181,8 +206,12 @@ pub fn simple_popup( > = Box::new(settings); let boxed: Box = Box::new(boxed); + let boxed_live: Box LiveSettings + Send + Sync + 'static> = Box::new(live_settings); + let boxed_live: Box = Box::new(boxed_live); + Action::Popup( Arc::new(boxed), + Arc::new(boxed_live), view.map(|view| { let boxed: Box< dyn Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, @@ -204,14 +233,7 @@ pub fn subsurface( + Sync + 'static, // XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways. - view: Option< - Box< - dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action> - + Send - + Sync - + 'static, - >, - >, + view: BoxedView, ) -> Action { let boxed: Box< dyn Fn( @@ -226,6 +248,7 @@ pub fn subsurface( Action::AppSubsurface( Arc::new(boxed), + Arc::new(Box::new(|_: &App| LiveSettings::default())), view.map(|view| { let boxed: Box = Box::new(view); Arc::new(boxed) @@ -236,6 +259,7 @@ pub fn subsurface( #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] pub fn simple_layer_shell( + live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, settings: impl Fn() -> iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings + Send @@ -253,9 +277,11 @@ pub fn simple_layer_shell( + 'static, > = Box::new(settings); let boxed: Box = Box::new(boxed); - + let boxed_live: Box LiveSettings + Send + Sync + 'static> = Box::new(live_settings); + let boxed_live: Box = Box::new(boxed_live); Action::LayerShell( Arc::new(boxed), + Arc::new(boxed_live), view.map(|view| { let boxed: Box< dyn Fn() -> crate::Element<'static, crate::Action> + Send + Sync + 'static, @@ -268,7 +294,8 @@ pub fn simple_layer_shell( #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] -pub fn layer_shell( +pub fn app_layer_shell( + live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static, settings: impl Fn( &mut App, ) @@ -277,14 +304,7 @@ pub fn layer_shell( + Sync + 'static, // XXX Boxed trait object is required for less cumbersome type inference, but we box it anyways. - view: Option< - Box< - dyn for<'a> Fn(&'a App) -> crate::Element<'a, crate::Action> - + Send - + Sync - + 'static, - >, - >, + view: BoxedView, ) -> Action { let boxed: Box< dyn Fn( @@ -297,8 +317,13 @@ pub fn layer_shell( > = Box::new(settings); let boxed: Box = Box::new(boxed); + let boxed_live: Box LiveSettings + Send + Sync + 'static> = + Box::new(live_settings); + let boxed_live: Box = Box::new(boxed_live); + Action::AppLayerShell( Arc::new(boxed), + Arc::new(boxed_live), view.map(|view| { let boxed: Box = Box::new(view); Arc::new(boxed) diff --git a/src/surface/mod.rs b/src/surface/mod.rs index 5377b421..092ef07b 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -4,34 +4,24 @@ pub mod action; use iced::{Limits, Size, Task}; -use std::future::Future; +use std::any::Any; use std::sync::Arc; +type BoxedSetting = Arc>; + /// Ignore this message in your application. It will be intercepted. #[derive(Clone)] pub enum Action { /// Create a subsurface with a view function accepting the App as a parameter - AppSubsurface( - std::sync::Arc>, - Option>>, - ), + AppSubsurface(BoxedSetting, BoxedSetting, Option), /// Create a subsurface with a view function - Subsurface( - std::sync::Arc>, - Option>>, - ), + Subsurface(BoxedSetting, BoxedSetting, Option), /// Destroy a subsurface with a view function DestroySubsurface(iced::window::Id), /// Create a popup with a view function accepting the App as a parameter - AppPopup( - std::sync::Arc>, - Option>>, - ), + AppPopup(BoxedSetting, BoxedSetting, Option), /// Create a popup - Popup( - std::sync::Arc>, - Option>>, - ), + Popup(BoxedSetting, BoxedSetting, Option), /// Destroy a subsurface with a view function DestroyPopup(iced::window::Id), /// Destroys the global tooltip popup subsurface @@ -40,29 +30,25 @@ pub enum Action { /// Create a window with a view function accepting the App as a parameter AppWindow( iced::window::Id, - std::sync::Arc>, - Option>>, + BoxedSetting, + BoxedSetting, + Option, ), /// Create a window with a view function Window( iced::window::Id, - std::sync::Arc>, - Option>>, + BoxedSetting, + BoxedSetting, + Option, ), /// Destroy a window DestroyWindow(iced::window::Id), /// Create a layer shell surface with a view function accepting the App as a parameter - AppLayerShell( - std::sync::Arc>, - Option>>, - ), + AppLayerShell(BoxedSetting, BoxedSetting, Option), /// Create a layer shell surface with a view function - LayerShell( - std::sync::Arc>, - Option>>, - ), + LayerShell(BoxedSetting, BoxedSetting, Option), /// Destroy a layer shell surface DestroyLayerShell(iced::window::Id), @@ -89,21 +75,33 @@ impl std::fmt::Debug for Action { #[cold] fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { match self { - Self::AppSubsurface(arg0, arg1) => f + Self::AppSubsurface(arg0, arg1, arg2) => f .debug_tuple("AppSubsurface") .field(arg0) .field(arg1) + .field(arg2) + .finish(), + Self::Subsurface(arg0, arg1, arg2) => f + .debug_tuple("Subsurface") + .field(arg0) + .field(arg1) + .field(arg2) .finish(), - Self::Subsurface(arg0, arg1) => { - f.debug_tuple("Subsurface").field(arg0).field(arg1).finish() - } Self::DestroySubsurface(arg0) => { f.debug_tuple("DestroySubsurface").field(arg0).finish() } - Self::AppPopup(arg0, arg1) => { - f.debug_tuple("AppPopup").field(arg0).field(arg1).finish() - } - Self::Popup(arg0, arg1) => f.debug_tuple("Popup").field(arg0).field(arg1).finish(), + Self::AppPopup(arg0, arg1, arg2) => f + .debug_tuple("AppPopup") + .field(arg0) + .field(arg1) + .field(arg2) + .finish(), + Self::Popup(arg0, arg1, arg2) => f + .debug_tuple("Popup") + .field(arg0) + .field(arg1) + .field(arg2) + .finish(), Self::DestroyPopup(arg0) => f.debug_tuple("DestroyPopup").field(arg0).finish(), Self::DestroyTooltipPopup => f.debug_tuple("DestroyTooltipPopup").finish(), Self::ResponsiveMenuBar { @@ -117,28 +115,34 @@ impl std::fmt::Debug for Action { .field("size", size) .finish(), Self::Ignore => write!(f, "Ignore"), - Self::AppWindow(id, arg0, arg1) => f + Self::AppWindow(id, arg0, arg1, arg2) => f .debug_tuple("AppWindow") .field(id) .field(arg0) .field(arg1) + .field(arg2) .finish(), - Self::Window(id, arg0, arg1) => f + Self::Window(id, arg0, arg1, arg2) => f .debug_tuple("Window") .field(id) .field(arg0) .field(arg1) + .field(arg2) .finish(), Self::DestroyWindow(arg0) => f.debug_tuple("DestroyWindow").field(arg0).finish(), Self::Task(_) => f.debug_tuple("Future").finish(), - Self::AppLayerShell(any, any1) => f + Self::AppLayerShell(arg, arg1, arg2) => f .debug_tuple("AppLayerShell") - .field(any) - .field(any1) + .field(arg) + .field(arg1) + .field(arg2) + .finish(), + Self::LayerShell(arg, arg1, arg2) => f + .debug_tuple("LayerShell") + .field(arg) + .field(arg1) + .field(arg2) .finish(), - Self::LayerShell(any, any1) => { - f.debug_tuple("LayerShell").field(any).field(any1).finish() - } Self::DestroyLayerShell(arg0) => { f.debug_tuple("DestroyLayerShell").field(arg0).finish() } diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index bd950bff..b66f95b1 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -81,7 +81,7 @@ impl ContextMenu<'_, Message> { my_state: &mut LocalState, ) { if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { - use crate::surface::action::destroy_popup; + use crate::surface::action::{LiveSettings, destroy_popup}; use crate::widget::menu::Menu; use iced_runtime::platform_specific::wayland::popup::{ SctkPopupSettings, SctkPositioner, @@ -187,6 +187,7 @@ impl ContextMenu<'_, Message> { let parent = self.window_id; shell.publish((self.on_surface_action.as_ref().unwrap())( crate::surface::action::simple_popup( + || LiveSettings::default(), move || SctkPopupSettings { parent, id, diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 046cf84d..79f854fb 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -580,6 +580,8 @@ pub fn update< use iced_runtime::platform_specific::wayland::popup::{ SctkPopupSettings, SctkPositioner, }; + + use crate::surface::action::LiveSettings; let bounds = layout.bounds(); let anchor_rect = Rectangle { x: bounds.x as i32, @@ -606,6 +608,7 @@ pub fn update< let on_surface_action_clone = on_surface_action.clone(); let translation = layout.virtual_offset(); let get_popup_action = surface::action::simple_popup::( + || LiveSettings::default(), move || { SctkPopupSettings { parent, diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index b5ffca42..facdfb85 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -385,7 +385,7 @@ where my_state: &mut MenuBarState, ) { if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { - use crate::surface::action::destroy_popup; + use crate::surface::action::{LiveSettings, destroy_popup}; use iced_runtime::platform_specific::wayland::popup::{ SctkPopupSettings, SctkPositioner, }; @@ -501,6 +501,7 @@ where }; let parent = self.window_id; shell.publish((surface_action)(crate::surface::action::simple_popup( + || LiveSettings::default(), move || SctkPopupSettings { parent, id, diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index bb33be5e..b2e7fd9d 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -988,6 +988,8 @@ impl Widget Widget( > = Box::new(move || s(bounds)); let boxed: Box = Box::new(boxed); + + let boxed_live: Box< + dyn Fn() -> LiveSettings + Send + Sync + 'static, + > = Box::new(move || Default::default()); + let boxed_live: Box = + Box::new(boxed_live); crate::surface::Action::Popup( Arc::new(boxed), + Arc::new(boxed_live), Some({ let boxed: Box< dyn Fn() -> crate::Element< @@ -553,9 +562,14 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( + 'static, > = Box::new(move || s(bounds)); let boxed: Box = Box::new(boxed); + let boxed_live: Box LiveSettings + Send + Sync + 'static> = + Box::new(move || Default::default()); + let boxed_live: Box = + Box::new(boxed_live); let sm = crate::surface::Action::Popup( Arc::new(boxed), + Arc::new(boxed_live), Some({ let boxed: Box< dyn Fn() -> crate::Element< From 8d2dbb3f23e9afe828c548e82a2925f6431ec2da Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 18 May 2026 16:54:23 -0400 Subject: [PATCH 091/201] fix feature gate --- src/surface/action.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/surface/action.rs b/src/surface/action.rs index 23a72e68..1a647293 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -45,6 +45,7 @@ pub struct LiveSettings { pub blur: Option, } +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] type BoxedView = Option< Box< dyn Fn(&App) -> crate::Element<'_, crate::Action<::Message>> From 991a8f2c786e84512defbb0f3a96f7f31aac703a Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 18 May 2026 16:56:23 -0400 Subject: [PATCH 092/201] feat: configurable blur alpha --- cosmic-theme/src/model/theme.rs | 97 +++++++++++++++++++++++++-------- 1 file changed, 74 insertions(+), 23 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index e68d9a09..3b9c426f 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -119,6 +119,9 @@ pub struct Theme { #[serde(default)] /// frosted applet popups pub frosted_applets: bool, + #[serde(default)] + /// alpha map + pub alpha_map: AlphaMap, /// shade color for dialogs #[serde(with = "color_serde")] #[cosmic_config_entry(with = ColorRepr)] @@ -904,6 +907,9 @@ pub struct ThemeBuilder { #[serde(default)] /// frosted applet popups pub frosted_applets: bool, + #[serde(default)] + /// alpha map + pub alpha_map: AlphaMap, } impl Default for ThemeBuilder { @@ -930,6 +936,7 @@ impl Default for ThemeBuilder { frosted_system_interface: false, frosted_panel: false, frosted_applets: false, + alpha_map: AlphaMap::default(), } } } @@ -1076,9 +1083,10 @@ impl ThemeBuilder { frosted_system_interface, frosted_panel, frosted_applets, + alpha_map, } = self; - let container_alpha = frosted.alpha(); + let container_alpha = alpha_map.blurred_alpha(frosted); let actual_alpha = if (frosted_windows || frosted_system_interface || frosted_panel || frosted_applets) { container_alpha @@ -1537,6 +1545,7 @@ impl ThemeBuilder { frosted_system_interface, frosted_panel, frosted_applets, + alpha_map, transparent_background: Container::new( Component::component( transparent_bg_component, @@ -1624,7 +1633,7 @@ impl ThemeBuilder { /// but this represents the strength of the blur effect. #[allow(missing_docs)] #[repr(u8)] -#[derive(Default, Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] pub enum BlurStrength { ExtremelyLow, ExtremelyLow2, @@ -1632,7 +1641,6 @@ pub enum BlurStrength { VeryLow2, Low, Low2, - #[default] Medium, Medium2, High, @@ -1643,26 +1651,9 @@ pub enum BlurStrength { ExtremelyHigh2, } -impl BlurStrength { - /// Get the alpha value corresponding to the blur strength - /// Lower alpha values correspond to stronger blur effects, and higher alpha values correspond to weaker blur effects. The mapping is as follows: - pub fn alpha(&self) -> f32 { - match self { - Self::ExtremelyLow => 0.90, - Self::ExtremelyLow2 => 0.87692, - Self::VeryLow => 0.85385, - Self::VeryLow2 => 0.83076, - Self::Low => 0.80769, - Self::Low2 => 0.78461, - Self::Medium => 0.76154, - Self::Medium2 => 0.73846, - Self::High => 0.71538, - Self::High2 => 0.69231, - Self::VeryHigh => 0.66023, - Self::VeryHigh2 => 0.64615, - Self::ExtremelyHigh => 0.62308, - Self::ExtremelyHigh2 => 0.6, - } +impl Default for BlurStrength { + fn default() -> Self { + Self::Medium } } @@ -1689,3 +1680,63 @@ impl TryFrom for BlurStrength { } } } + +#[derive(Copy, Clone, Debug, Serialize, Deserialize, PartialEq)] +pub struct AlphaMap { + pub extremely_low: f32, + pub extremely_low_2: f32, + pub very_low: f32, + pub very_low_2: f32, + pub low: f32, + pub low_2: f32, + pub medium: f32, + pub medium_2: f32, + pub high: f32, + pub high_2: f32, + pub very_high: f32, + pub very_high_2: f32, + pub extremely_high: f32, + pub extremely_high_2: f32, +} + +impl AlphaMap { + pub fn blurred_alpha(&self, blur: BlurStrength) -> f32 { + match blur { + BlurStrength::ExtremelyLow => self.extremely_low, + BlurStrength::ExtremelyLow2 => self.extremely_low_2, + BlurStrength::VeryLow => self.very_low, + BlurStrength::VeryLow2 => self.very_low_2, + BlurStrength::Low => self.low, + BlurStrength::Low2 => self.low_2, + BlurStrength::Medium => self.medium, + BlurStrength::Medium2 => self.medium_2, + BlurStrength::High => self.high, + BlurStrength::High2 => self.high_2, + BlurStrength::VeryHigh => self.very_high, + BlurStrength::VeryHigh2 => self.very_high_2, + BlurStrength::ExtremelyHigh => self.extremely_high, + BlurStrength::ExtremelyHigh2 => self.extremely_high_2, + } + } +} + +impl Default for AlphaMap { + fn default() -> Self { + Self { + extremely_low: 0.90, + extremely_low_2: 0.87692, + very_low: 0.85385, + very_low_2: 0.83076, + low: 0.80769, + low_2: 0.78461, + medium: 0.76154, + medium_2: 0.73846, + high: 0.71538, + high_2: 0.69231, + very_high: 0.66023, + very_high_2: 0.64615, + extremely_high: 0.62308, + extremely_high_2: 0.6, + } + } +} From 4a7ed015f6d04f22997056dc23aafc4ebc952c22 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 18 May 2026 21:44:52 -0400 Subject: [PATCH 093/201] feat: padding setting on surface creation --- src/app/cosmic.rs | 9 ++++++++- src/surface/action.rs | 5 +++++ 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index edca03e6..0f5ef322 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -1539,6 +1539,7 @@ impl Cosmic { live_settings: &LiveSettings, ) -> Task> { use iced_winit::commands::corner_radius; + use iced_winit::commands::layer_surface::set_padding; let id = id_wrapper.inner(); @@ -1563,9 +1564,14 @@ impl Cosmic { if let Some(cur_rad) = corners(id_wrapper, rounded, &*t, self.app.core().auto_corner_radius) { - cmds.push(corner_radius::corner_radius(id, Some(cur_rad)).discard()) + cmds.push(corner_radius::corner_radius(id, Some(cur_rad)).discard()); } } + if let (SurfaceIdWrapper::LayerSurface(id), Some(padding)) = + (id_wrapper, live_settings.padding) + { + cmds.push(set_padding(id, padding)); + } Task::batch(cmds) } @@ -1586,6 +1592,7 @@ impl Cosmic { &LiveSettings { blur: Some(self.blur_enabled), corners: None, + padding: None, }, ); self.surface_views.insert( diff --git a/src/surface/action.rs b/src/surface/action.rs index 1a647293..fc38b31d 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -8,6 +8,8 @@ use crate::Application; use iced::{Rectangle, window}; #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] use iced_runtime::platform_specific::wayland::CornerRadius; +#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +use iced_runtime::platform_specific::wayland::layer_surface::IcedMargin; use std::any::Any; use std::sync::Arc; @@ -38,6 +40,9 @@ pub fn destroy_layer_shell(id: iced_core::window::Id) -> Action { #[derive(Debug, Default, Copy, Clone)] pub struct LiveSettings { + #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] + /// Override the surface padding value for the surface type. + pub padding: Option, #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] /// Override the default corner radius value for the surface type. pub corners: Option, From 76954a6524a710efcdf035a3e43f161442899776 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 20 May 2026 14:49:19 -0400 Subject: [PATCH 094/201] fix: tooltip corner radius --- src/app/cosmic.rs | 8 ++++-- src/widget/wayland/tooltip/widget.rs | 41 +++++++++++++++++++--------- 2 files changed, 33 insertions(+), 16 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 0f5ef322..3ed4f30a 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -293,7 +293,7 @@ where return Task::none(); }; let settings = settings(); - let live_settings = Box::new(move |app: &T| live_settings()); + let live_settings = Box::new(move |_: &T| live_settings()); if let Some(view) = view.and_then(|view| { match std::sync::Arc::try_unwrap(view).ok()?.downcast:: Cosmic { ) -> Task> { use iced_winit::commands::corner_radius; use iced_winit::commands::layer_surface::set_padding; - + dbg!(id_wrapper, live_settings); let id = id_wrapper.inner(); let mut cmds = Vec::with_capacity(2); @@ -1553,12 +1553,14 @@ impl Cosmic { iced::window::disable_blur }; cmds.push(blur_cmd(id)); - } else if self.app.core().blur(&*t, Some(id_wrapper)) { + } else if self.app.core().blur(&t, Some(id_wrapper)) { cmds.push(iced::window::enable_blur(id)); } if let Some(corners) = live_settings.corners { + dbg!(corners); cmds.push(corner_radius::corner_radius(id, Some(corners)).discard()); } else { + dbg!("app corners"); let rounded = !self.app.core().window.sharp_corners && self.app.core().sync_window_border_radii_to_theme(); if let Some(cur_rad) = diff --git a/src/widget/wayland/tooltip/widget.rs b/src/widget/wayland/tooltip/widget.rs index b9de0a7c..a5e4520a 100644 --- a/src/widget/wayland/tooltip/widget.rs +++ b/src/widget/wayland/tooltip/widget.rs @@ -13,27 +13,20 @@ use std::time::Duration; use iced::Task; use iced_runtime::core::widget::Id; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::Operation; use iced_core::widget::tree::{self, Tree}; use iced_core::{ Background, Border, Clipboard, Color, Layout, Length, Padding, Point, Rectangle, Shadow, Shell, - Vector, Widget, layout, mouse, overlay, renderer, svg, touch, + Vector, Widget, layout, mouse, overlay, renderer, touch, }; +use iced_runtime::platform_specific::wayland::CornerRadius; use crate::surface::action::LiveSettings; +use crate::theme::THEME; pub use super::{Catalog, Style}; -/// Internally defines different button widget variants. -enum Variant { - Normal, - Image { - close_icon: svg::Handle, - on_remove: Option, - }, -} - /// A generic button which emits a message when pressed. #[allow(missing_debug_implementations)] #[must_use] @@ -524,9 +517,20 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( let boxed: Box = Box::new(boxed); + let theme = THEME.lock().unwrap(); + + let corners = theme.cosmic().corner_radii.radius_s; let boxed_live: Box< dyn Fn() -> LiveSettings + Send + Sync + 'static, - > = Box::new(move || Default::default()); + > = Box::new(move || LiveSettings { + corners: Some(CornerRadius { + top_left: corners[0] as u32, + top_right: corners[1] as u32, + bottom_left: corners[3] as u32, + bottom_right: corners[2] as u32, + }), + ..Default::default() + }); let boxed_live: Box = Box::new(boxed_live); crate::surface::Action::Popup( @@ -562,8 +566,19 @@ pub fn update<'a, Message: Clone + 'static, TopLevelMessage: Clone + 'static>( + 'static, > = Box::new(move || s(bounds)); let boxed: Box = Box::new(boxed); + let theme = THEME.lock().unwrap(); + + let corners = theme.cosmic().corner_radii.radius_s; let boxed_live: Box LiveSettings + Send + Sync + 'static> = - Box::new(move || Default::default()); + Box::new(move || LiveSettings { + corners: Some(CornerRadius { + top_left: corners[0] as u32, + top_right: corners[1] as u32, + bottom_left: corners[3] as u32, + bottom_right: corners[2] as u32, + }), + ..Default::default() + }); let boxed_live: Box = Box::new(boxed_live); From 033dfe6c577a15a95231364f62c5ebb547ebf2d8 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 20 May 2026 15:40:01 -0400 Subject: [PATCH 095/201] cleanup --- src/app/cosmic.rs | 3 --- 1 file changed, 3 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 3ed4f30a..13f5438b 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -1540,7 +1540,6 @@ impl Cosmic { ) -> Task> { use iced_winit::commands::corner_radius; use iced_winit::commands::layer_surface::set_padding; - dbg!(id_wrapper, live_settings); let id = id_wrapper.inner(); let mut cmds = Vec::with_capacity(2); @@ -1557,10 +1556,8 @@ impl Cosmic { cmds.push(iced::window::enable_blur(id)); } if let Some(corners) = live_settings.corners { - dbg!(corners); cmds.push(corner_radius::corner_radius(id, Some(corners)).discard()); } else { - dbg!("app corners"); let rounded = !self.app.core().window.sharp_corners && self.app.core().sync_window_border_radii_to_theme(); if let Some(cur_rad) = From 43de9e3e77b09a84f3e217851e54a85a8f4946f1 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 21 May 2026 14:12:08 -0400 Subject: [PATCH 096/201] cleanup and update iced --- examples/application/Cargo.toml | 1 + src/app/cosmic.rs | 2 +- src/surface/action.rs | 2 +- 3 files changed, 3 insertions(+), 2 deletions(-) diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index 7a6083e0..1fb2949b 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -22,4 +22,5 @@ features = [ "surface-message", "multi-window", "wgpu", + "wayland", ] diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 13f5438b..66a3e7a6 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -1561,7 +1561,7 @@ impl Cosmic { let rounded = !self.app.core().window.sharp_corners && self.app.core().sync_window_border_radii_to_theme(); if let Some(cur_rad) = - corners(id_wrapper, rounded, &*t, self.app.core().auto_corner_radius) + corners(id_wrapper, rounded, &t, self.app.core().auto_corner_radius) { cmds.push(corner_radius::corner_radius(id, Some(cur_rad)).discard()); } diff --git a/src/surface/action.rs b/src/surface/action.rs index fc38b31d..493c9ad0 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -164,7 +164,7 @@ pub fn app_popup( /// Used to create a subsurface message from within a widget. #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] #[must_use] -pub fn simple_subsurface( +pub fn simple_subsurface( settings: impl Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings + Send + Sync From 19f4a289e500a554cb3faabb8fbc427904e56e1c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 26 May 2026 16:11:00 -0400 Subject: [PATCH 097/201] fix: config --- cosmic-config/src/lib.rs | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/cosmic-config/src/lib.rs b/cosmic-config/src/lib.rs index 92eef09c..1396b5cb 100644 --- a/cosmic-config/src/lib.rs +++ b/cosmic-config/src/lib.rs @@ -328,7 +328,16 @@ 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)); @@ -342,6 +351,13 @@ impl Config { 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 + }, }) } From 00067dc5b85e9477cc52eec8727ab5650ba5e68f Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 28 May 2026 19:25:53 -0400 Subject: [PATCH 098/201] fix: set corners and blur before surface creation these are then saved and applied on creation --- src/app/cosmic.rs | 37 ++++++++++++++++++++++++++++++------- 1 file changed, 30 insertions(+), 7 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 66a3e7a6..214a9345 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -1538,8 +1538,11 @@ impl Cosmic { id_wrapper: SurfaceIdWrapper, live_settings: &LiveSettings, ) -> Task> { + use iced::{Point, Rectangle, Size}; + use iced_winit::commands::blur::blur as blur_rect; use iced_winit::commands::corner_radius; use iced_winit::commands::layer_surface::set_padding; + let id = id_wrapper.inner(); let mut cmds = Vec::with_capacity(2); @@ -1547,13 +1550,32 @@ impl Cosmic { if let Some(blur) = live_settings.blur { let blur_cmd = if blur { - iced::window::enable_blur + use iced::Rectangle; + if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { + window::enable_blur(id) + } else { + blur_rect( + id, + Some(vec![Rectangle::new(Point::ORIGIN, Size::INFINITE)]), + ) + .discard() + } + } else if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { + window::disable_blur(id) } else { - iced::window::disable_blur + blur_rect(id, None).discard() }; - cmds.push(blur_cmd(id)); + cmds.push(blur_cmd); } else if self.app.core().blur(&t, Some(id_wrapper)) { - cmds.push(iced::window::enable_blur(id)); + cmds.push(if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { + window::enable_blur(id) + } else { + blur_rect( + id, + Some(vec![Rectangle::new(Point::ORIGIN, Size::INFINITE)]), + ) + .discard() + }); } if let Some(corners) = live_settings.corners { cmds.push(corner_radius::corner_radius(id, Some(corners)).discard()); @@ -1603,7 +1625,7 @@ impl Cosmic { view, ), ); - Task::batch([get_subsurface(settings), live_settings_task]) + Task::batch([live_settings_task, get_subsurface(settings)]) } #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -1631,7 +1653,7 @@ impl Cosmic { view, ), ); - Task::batch([get_popup(settings), live_settings_task]) + live_settings_task.chain(get_popup(settings)).discard() } #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -1666,6 +1688,7 @@ impl Cosmic { )) }) .discard(), + // We don't control window creation in the same way live_settings_task, ]) } @@ -1695,7 +1718,7 @@ impl Cosmic { view, ), ); - Task::batch([get_layer_surface(settings), live_settings_task]) + Task::batch([live_settings_task, get_layer_surface(settings)]) } } From 0c336d77e47460660915454d2dd627f9d79db3c2 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 29 May 2026 13:40:23 -0400 Subject: [PATCH 099/201] chore(theme): helpers for alpha map --- cosmic-theme/src/model/theme.rs | 66 +++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 3b9c426f..76588ccc 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1718,6 +1718,72 @@ impl AlphaMap { BlurStrength::ExtremelyHigh2 => self.extremely_high_2, } } + + pub fn offset(self, offset: f32) -> Self { + let Self { + mut extremely_low, + mut extremely_low_2, + mut very_low, + mut very_low_2, + mut low, + mut low_2, + mut medium, + mut medium_2, + mut high, + mut high_2, + mut very_high, + mut very_high_2, + mut extremely_high, + mut extremely_high_2, + } = self; + extremely_low += offset; + extremely_low_2 += offset; + very_low += offset; + very_low_2 += offset; + low += offset; + low_2 += offset; + medium += offset; + medium_2 += offset; + high += offset; + high_2 += offset; + very_high += offset; + very_high_2 += offset; + extremely_high += offset; + extremely_high_2 += offset; + Self { + extremely_low, + extremely_low_2, + very_low, + very_low_2, + low, + low_2, + medium, + medium_2, + high, + high_2, + very_high, + very_high_2, + extremely_high, + extremely_high_2, + } + } + + pub fn guess_offset(&self) -> f32 { + let Self { + extremely_low, + extremely_high_2, + .. + } = self; + let default_low = Self::default().extremely_low; + let default_high = Self::default().extremely_high_2; + if *extremely_low > default_low { + (*extremely_low - default_low) / (1.0 - default_low) + } else if *extremely_high_2 < default_high { + (default_high - *extremely_high_2) / default_high + } else { + 0.5 + } + } } impl Default for AlphaMap { From 217b4abe997dfc1a02b328b52ad8c46de139a358 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 29 May 2026 16:13:29 -0400 Subject: [PATCH 100/201] fix: guess offset --- cosmic-theme/src/model/theme.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 76588ccc..1f0bd748 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1777,9 +1777,9 @@ impl AlphaMap { let default_low = Self::default().extremely_low; let default_high = Self::default().extremely_high_2; if *extremely_low > default_low { - (*extremely_low - default_low) / (1.0 - default_low) + 0.5 * (*extremely_low - default_low) / (1.0 - default_low) + 0.5 } else if *extremely_high_2 < default_high { - (default_high - *extremely_high_2) / default_high + 0.5 - (0.5 * (default_high - *extremely_high_2) / default_high) } else { 0.5 } From e939d334f6ad7764a8ad842db1d647413e7cbf84 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 2 Jun 2026 22:28:43 -0400 Subject: [PATCH 101/201] fix: on init, set blur value if theme is already marked transparent useful for things like the files dialog --- src/app/cosmic.rs | 21 ++++++++++++++------- 1 file changed, 14 insertions(+), 7 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 214a9345..9fe3a3fd 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -124,14 +124,21 @@ where let id = core.main_window_id().unwrap_or(window::Id::RESERVED); let (model, command) = T::init(core, flags); + let existing_theme = THEME.lock().unwrap(); + let blur = existing_theme.transparent; + drop(existing_theme); - ( - Self::new(model), - Task::batch([ - iced_runtime::window::run_with_handle(id, init_windowing_system), - command, - ]), - ) + let mut cmds = vec![ + iced_runtime::window::run_with_handle(id, init_windowing_system), + command, + ]; + + if blur { + cmds.push(crate::task::message(crate::action::cosmic( + Action::BlurEnabled, + ))); + } + (Self::new(model), Task::batch(cmds)) } #[cfg(not(feature = "multi-window"))] From f04437cee54f73a9709c23a621dbbe4a0dee0909 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 5 Jun 2026 12:29:38 -0400 Subject: [PATCH 102/201] chore: surface action for LiveSettings update --- src/app/cosmic.rs | 80 +++++++++++++++++++++++++++++++++------------- src/surface/mod.rs | 3 +- 2 files changed, 60 insertions(+), 23 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 9fe3a3fd..6b740e57 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -495,6 +495,13 @@ where crate::surface::Action::DestroyLayerShell(id) => { iced_winit::commands::layer_surface::destroy_layer_surface(id) } + crate::surface::Action::SyncLiveSettings(id) => { + if let Some((_, id, live_settings, _)) = self.surface_views.get(&id) { + let live_settings = live_settings(&self.app); + return self.apply_live_settings(*id, &live_settings); + } + Task::none() + } _ => iced::Task::none(), } @@ -1538,18 +1545,13 @@ impl Cosmic { } } - #[cfg(all(feature = "wayland", feature = "winit", target_os = "linux"))] + #[cfg(feature = "surface-message")] /// Apply live setting overrides for a surface pub fn apply_live_settings( &mut self, id_wrapper: SurfaceIdWrapper, - live_settings: &LiveSettings, + live_settings: &crate::surface::action::LiveSettings, ) -> Task> { - use iced::{Point, Rectangle, Size}; - use iced_winit::commands::blur::blur as blur_rect; - use iced_winit::commands::corner_radius; - use iced_winit::commands::layer_surface::set_padding; - let id = id_wrapper.inner(); let mut cmds = Vec::with_capacity(2); @@ -1557,48 +1559,82 @@ impl Cosmic { if let Some(blur) = live_settings.blur { let blur_cmd = if blur { - use iced::Rectangle; if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { window::enable_blur(id) } else { - blur_rect( - id, - Some(vec![Rectangle::new(Point::ORIGIN, Size::INFINITE)]), - ) - .discard() + #[cfg(all(feature = "wayland", feature = "winit", target_os = "linux"))] + { + iced_winit::commands::blur::blur( + id, + Some(vec![iced::Rectangle::new( + iced::Point::ORIGIN, + iced::Size::INFINITE, + )]), + ) + .discard() + } + #[cfg(not(all(feature = "wayland", feature = "winit", target_os = "linux")))] + { + iced::window::enable_blur(id) + } } } else if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { window::disable_blur(id) } else { - blur_rect(id, None).discard() + #[cfg(all(feature = "wayland", feature = "winit", target_os = "linux"))] + { + iced_winit::commands::blur::blur(id, None).discard() + } + #[cfg(not(all(feature = "wayland", feature = "winit", target_os = "linux")))] + { + iced::window::disable_blur(id) + } }; cmds.push(blur_cmd); } else if self.app.core().blur(&t, Some(id_wrapper)) { cmds.push(if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { window::enable_blur(id) } else { - blur_rect( - id, - Some(vec![Rectangle::new(Point::ORIGIN, Size::INFINITE)]), - ) - .discard() + #[cfg(all(feature = "wayland", feature = "winit", target_os = "linux"))] + { + iced_winit::commands::blur::blur( + id, + Some(vec![iced::Rectangle::new( + iced::Point::ORIGIN, + iced::Size::INFINITE, + )]), + ) + .discard() + } + #[cfg(not(all(feature = "wayland", feature = "winit", target_os = "linux")))] + { + iced::window::enable_blur(id) + } }); } + #[cfg(all(feature = "wayland", feature = "winit", target_os = "linux"))] if let Some(corners) = live_settings.corners { - cmds.push(corner_radius::corner_radius(id, Some(corners)).discard()); + cmds.push( + iced_winit::commands::corner_radius::corner_radius(id, Some(corners)).discard(), + ); } else { let rounded = !self.app.core().window.sharp_corners && self.app.core().sync_window_border_radii_to_theme(); if let Some(cur_rad) = corners(id_wrapper, rounded, &t, self.app.core().auto_corner_radius) { - cmds.push(corner_radius::corner_radius(id, Some(cur_rad)).discard()); + cmds.push( + iced_winit::commands::corner_radius::corner_radius(id, Some(cur_rad)).discard(), + ); } } + #[cfg(all(feature = "wayland", feature = "winit", target_os = "linux"))] if let (SurfaceIdWrapper::LayerSurface(id), Some(padding)) = (id_wrapper, live_settings.padding) { - cmds.push(set_padding(id, padding)); + cmds.push(iced_winit::commands::layer_surface::set_padding( + id, padding, + )); } Task::batch(cmds) } diff --git a/src/surface/mod.rs b/src/surface/mod.rs index 092ef07b..eec81e3f 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -63,6 +63,7 @@ pub enum Action { size: Size, }, Ignore, + SyncLiveSettings(iced::window::Id), Task(Arc Task + Send + Sync>), } @@ -146,7 +147,7 @@ impl std::fmt::Debug for Action { Self::DestroyLayerShell(arg0) => { f.debug_tuple("DestroyLayerShell").field(arg0).finish() } - Self::Task(_) => f.debug_tuple("Task").finish(), + Self::SyncLiveSettings(arg0) => f.debug_tuple("SyncLiveSettings").field(arg0).finish(), } } } From f5fc392ba4064be5287c0f86d6d74db70e698b72 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 8 Jun 2026 16:49:09 -0400 Subject: [PATCH 103/201] fix: opaque menu for overlay --- src/theme/style/menu_bar.rs | 10 +++++----- src/widget/menu/menu_bar.rs | 16 +++++++++++++++- src/widget/menu/menu_inner.rs | 2 +- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/src/theme/style/menu_bar.rs b/src/theme/style/menu_bar.rs index c6f40c52..50263b14 100644 --- a/src/theme/style/menu_bar.rs +++ b/src/theme/style/menu_bar.rs @@ -31,7 +31,7 @@ pub trait StyleSheet { type Style: Default; /// Produces the [`Appearance`] of a menu bar and its menus. - fn appearance(&self, style: &Self::Style) -> Appearance; + fn appearance(&self, style: &Self::Style, is_overlay: bool) -> Appearance; } /// The style of a menu bar and its menus @@ -54,7 +54,7 @@ impl From Appearance> for MenuBarStyle { impl StyleSheet for fn(&Theme) -> Appearance { type Style = Theme; - fn appearance(&self, style: &Self::Style) -> Appearance { + fn appearance(&self, style: &Self::Style, _is_overlay: bool) -> Appearance { (self)(style) } } @@ -62,9 +62,9 @@ impl StyleSheet for fn(&Theme) -> Appearance { impl StyleSheet for Theme { type Style = MenuBarStyle; - fn appearance(&self, style: &Self::Style) -> Appearance { + fn appearance(&self, style: &Self::Style, is_overlay: bool) -> Appearance { let cosmic = self.cosmic(); - let component = &cosmic.background(self.transparent).component; + let component = &cosmic.background(!is_overlay && self.transparent).component; let mut bg = component.base; match style { @@ -77,7 +77,7 @@ impl StyleSheet for Theme { background_expand: [1; 4], path: component.hover.into(), }, - MenuBarStyle::Custom(c) => c.appearance(self), + MenuBarStyle::Custom(c) => c.appearance(self, is_overlay), } } } diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index facdfb85..e9d25dd8 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -715,7 +715,21 @@ where // draw path highlight if self.path_highlight.is_some() { - let styling = theme.appearance(&self.style); + let mut is_overlay = true; + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + 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 + { + is_overlay = true; + }; + let styling = theme.appearance(&self.style, is_overlay); if let Some(active) = state.active_root.first() { let active_bounds = layout .children() diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index b2e7fd9d..b7595ba5 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -742,7 +742,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { Rectangle::new(Point::ORIGIN, Size::INFINITE) }; - let styling = theme.appearance(&self.style); + let styling = theme.appearance(&self.style, self.is_overlay); let roots = active_root.iter().skip(1).fold( &self.menu_roots[active_root[0]].children, |mt, next_active_root| &mt[*next_active_root].children, From cde1989e78ed95d552aecc1ba9428fe3fc710801 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 12 Jun 2026 14:20:37 -0400 Subject: [PATCH 104/201] refactor: popups for nav bar / segmented buttons --- examples/application/src/main.rs | 87 ++++- examples/nav-context/src/main.rs | 26 +- src/app/mod.rs | 32 +- src/widget/menu.rs | 1 + src/widget/menu/menu_bar.rs | 2 +- src/widget/menu/menu_inner.rs | 1 + src/widget/menu/menu_tree.rs | 16 + src/widget/nav_bar.rs | 40 ++- src/widget/segmented_button/horizontal.rs | 4 +- src/widget/segmented_button/vertical.rs | 4 +- src/widget/segmented_button/widget.rs | 380 ++++++++++++++++++++-- src/widget/segmented_control.rs | 4 +- src/widget/tab_bar.rs | 4 +- 13 files changed, 544 insertions(+), 57 deletions(-) diff --git a/examples/application/src/main.rs b/examples/application/src/main.rs index e60b4f7e..af4200c9 100644 --- a/examples/application/src/main.rs +++ b/examples/application/src/main.rs @@ -7,7 +7,7 @@ use cosmic::app::Settings; use cosmic::iced::{Alignment, Length, Size}; use cosmic::prelude::*; use cosmic::widget::menu::{self, KeyBind}; -use cosmic::widget::nav_bar; +use cosmic::widget::{nav_bar, RcElementWrapper}; use cosmic::{executor, iced, widget, Core}; use std::collections::HashMap; use std::sync::LazyLock; @@ -43,7 +43,7 @@ pub enum Action { impl widget::menu::Action for Action { type Message = Message; - fn message(&self) -> Message { + fn message(&self) -> Self::Message { match self { Action::Hi => Message::Hi, Action::Hi2 => Message::Hi2, @@ -52,6 +52,25 @@ impl widget::menu::Action for Action { } } +#[derive(Clone, Copy, Debug, Eq, PartialEq)] +pub enum Action2 { + Hi, + Hi2, + Hi3, +} + +impl widget::menu::Action for Action2 { + type Message = cosmic::Action; + + fn message(&self) -> Self::Message { + cosmic::Action::App(match self { + Action2::Hi => Message::Hi, + Action2::Hi2 => Message::Hi2, + Action2::Hi3 => Message::Hi3, + }) + } +} + /// Runs application with these settings #[rustfmt::skip] fn main() -> Result<(), Box> { @@ -158,6 +177,70 @@ impl cosmic::Application for App { self.update_title() } + fn nav_context_menu(&self) -> Option>>> { + Some(menu::nav_context( + &HashMap::new(), + vec![ + vec![cosmic::widget::menu::Item::Button( + "Hi", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + )], + vec![ + cosmic::widget::menu::Item::Button( + "Hi 2", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + ), + cosmic::widget::menu::Item::Button( + "Hi 22", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + ), + ], + vec![ + cosmic::widget::menu::Item::Button( + "Hi 3", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + ), + cosmic::widget::menu::Item::Button( + "Hi 33", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + ), + cosmic::widget::menu::Item::Button( + "Hi 333", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + ), + ], + vec![ + cosmic::widget::menu::Item::Button( + "Hi 44", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + ), + cosmic::widget::menu::Item::Button( + "Hi 4444", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + ), + cosmic::widget::menu::Item::Button( + "Hi 444444", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + ), + cosmic::widget::menu::Item::Button( + "Hi 444444444444", + Some(cosmic::widget::icon::from_name("screenshot-window-symbolic").into()), + Action2::Hi, + ), + ], + ], + )) + } + /// Handle application events here. fn update(&mut self, message: Self::Message) -> cosmic::app::Task { match message { diff --git a/examples/nav-context/src/main.rs b/examples/nav-context/src/main.rs index 1992066f..875b83ff 100644 --- a/examples/nav-context/src/main.rs +++ b/examples/nav-context/src/main.rs @@ -7,7 +7,7 @@ use std::collections::HashMap; use cosmic::app::{Core, Settings, Task}; use cosmic::iced::Size; -use cosmic::widget::{menu, nav_bar}; +use cosmic::widget::{menu, nav_bar, space}; use cosmic::{executor, iced, ApplicationExt, Element}; #[derive(Clone, Copy)] @@ -127,18 +127,20 @@ impl cosmic::Application for App { Some(&self.nav_model) } - /// The context menu to display for the given nav bar item ID. - fn nav_context_menu( - &self, - id: nav_bar::Id, - ) -> Option>>> { - Some(menu::items( + /// The context menu list. + fn nav_context_menu(&self) -> Option>>> { + Some(menu::nav_context( &HashMap::new(), - vec![ - menu::Item::Button("Move Up", None, NavMenuAction::MoveUp(id)), - menu::Item::Button("Move Down", None, NavMenuAction::MoveDown(id)), - menu::Item::Button("Delete", None, NavMenuAction::Delete(id)), - ], + self.nav_model + .iter() + .map(|e| { + vec![ + menu::Item::Button("Move Up", None, NavMenuAction::MoveUp(e)), + menu::Item::Button("Move Down", None, NavMenuAction::MoveDown(e)), + menu::Item::Button("Delete", None, NavMenuAction::Delete(e)), + ] + }) + .collect(), )) } diff --git a/src/app/mod.rs b/src/app/mod.rs index e2d6ae1c..99ea6e53 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -389,10 +389,23 @@ where let mut nav = crate::widget::nav_bar(nav_model, |id| crate::Action::Cosmic(Action::NavBar(id))) .on_context(|id| crate::Action::Cosmic(Action::NavBarContext(id))) - .context_menu(self.nav_context_menu(self.core().nav_bar_context())) - .into_container() - .width(iced::Length::Shrink) - .height(iced::Length::Fill); + .context_menu(self.nav_context_menu()); + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] + { + nav = nav + .window_id_maybe(self.core().main_window_id()) + .on_surface_action(|m| crate::Action::Cosmic(crate::app::Action::Surface(m))) + } + let mut nav = nav + .into_container() + .width(iced::Length::Shrink) + .height(iced::Length::Fill); if !self.core().is_condensed() { nav = nav.max_width(280); @@ -402,10 +415,7 @@ where } /// Shows a context menu for the active nav bar item. - fn nav_context_menu( - &self, - id: nav_bar::Id, - ) -> Option>>> { + fn nav_context_menu(&self) -> Option>>> { None } @@ -635,10 +645,8 @@ impl ApplicationExt for App { let mut widgets = Vec::with_capacity(3); // Insert nav bar onto the left side of the window. - let has_nav = if let Some(nav) = self - .nav_bar() - .map(|nav| id_container(nav, iced_core::id::Id::new("COSMIC_nav_bar"))) - { + let has_nav = if let Some(nav) = self.nav_bar() { + let nav = id_container(nav, iced_core::id::Id::new("COSMIC_nav_bar")); widgets.push( container(nav) .padding([ diff --git a/src/widget/menu.rs b/src/widget/menu.rs index a452e2b8..17d47cdd 100644 --- a/src/widget/menu.rs +++ b/src/widget/menu.rs @@ -72,6 +72,7 @@ mod menu_inner; mod menu_tree; pub use menu_tree::{ MenuItem as Item, MenuTree as Tree, menu_button, menu_items as items, menu_root as root, + nav_context, }; pub use crate::style::menu_bar::{Appearance, StyleSheet}; diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index e9d25dd8..560b8d2a 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -230,7 +230,7 @@ where path_highlight: Some(PathHighlight::MenuActive), menu_roots, style: ::Style::default(), - window_id: window::Id::NONE, + window_id: window::Id::RESERVED, #[cfg(all( feature = "multi-window", feature = "wayland", diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index b7595ba5..47e8e30d 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -525,6 +525,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { ); 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); diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index c09151aa..bd4f1e6c 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -401,3 +401,19 @@ pub fn menu_items< }) .collect() } + +/// Create a menu tree from a widget and a vector of sub trees +pub fn nav_context< + A: MenuAction, + L: Into> + From<&'static str> + 'static, + Message: 'static + std::clone::Clone, +>( + key_binds: &HashMap, + children: Vec>>, +) -> Vec> { + let menus = children + .into_iter() + .map(|m| MenuItem::::Folder(L::from(""), m)); + let root = vec![MenuItem::::Folder(L::from(""), menus.collect())]; + menu_items(key_binds, root) +} diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index 75809398..d6595359 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -8,7 +8,7 @@ use apply::Apply; use iced::clipboard::dnd::DndAction; use iced::clipboard::mime::AllowedMimeTypes; -use iced::{Background, Length}; +use iced::{Background, Length, window}; use iced_core::{Border, Color, Shadow}; use crate::widget::{Container, Icon, container, menu, scrollable, segmented_button}; @@ -55,7 +55,7 @@ where } #[must_use] -pub struct NavBar<'a, Message> { +pub struct NavBar<'a, Message: Clone + 'static> { segmented_button: segmented_button::VerticalSegmentedButton<'a, segmented_button::SingleSelect, Message>, } @@ -133,6 +133,42 @@ impl<'a, Message: Clone + 'static> NavBar<'a, Message> { self.segmented_button = self.segmented_button.on_dnd_leave(handler); self } + + #[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, + ) -> Self { + self.segmented_button = self.segmented_button.with_positioner(positioner); + self + } + + #[must_use] + pub fn window_id(mut self, id: window::Id) -> Self { + self.segmented_button = self.segmented_button.window_id(id); + self + } + + #[must_use] + pub fn window_id_maybe(mut self, id: Option) -> Self { + self.segmented_button = self.segmented_button.window_id_maybe(id); + + self + } + + #[must_use] + pub fn on_surface_action( + mut self, + handler: impl Fn(crate::surface::Action) -> Message + Send + Sync + 'static, + ) -> Self { + self.segmented_button = self.segmented_button.on_surface_action(handler); + self + } } impl<'a, Message: Clone + 'static> From> diff --git a/src/widget/segmented_button/horizontal.rs b/src/widget/segmented_button/horizontal.rs index 5fd67649..62205264 100644 --- a/src/widget/segmented_button/horizontal.rs +++ b/src/widget/segmented_button/horizontal.rs @@ -21,7 +21,7 @@ pub struct Horizontal; /// Horizontal implementation of the [`SegmentedButton`]. /// /// For details on the model, see the [`segmented_button`](super) module for more details. -pub fn horizontal( +pub fn horizontal( model: &Model, ) -> SegmentedButton<'_, Horizontal, SelectionMode, Message> where @@ -30,7 +30,7 @@ where SegmentedButton::new(model) } -impl SegmentedVariant +impl SegmentedVariant for SegmentedButton<'_, Horizontal, SelectionMode, Message> where Model: Selectable, diff --git a/src/widget/segmented_button/vertical.rs b/src/widget/segmented_button/vertical.rs index 5458cd0a..26cc2bea 100644 --- a/src/widget/segmented_button/vertical.rs +++ b/src/widget/segmented_button/vertical.rs @@ -20,7 +20,7 @@ pub type VerticalSegmentedButton<'a, SelectionMode, Message> = /// Vertical implementation of the [`SegmentedButton`]. /// /// For details on the model, see the [`segmented_button`](super) module for more details. -pub fn vertical( +pub fn vertical( model: &Model, ) -> SegmentedButton<'_, Vertical, SelectionMode, Message> where @@ -30,7 +30,7 @@ where SegmentedButton::new(model) } -impl SegmentedVariant +impl SegmentedVariant for SegmentedButton<'_, Vertical, SelectionMode, Message> where Model: Selectable, diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index edd58eb7..773e20a1 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -9,7 +9,7 @@ use crate::widget::menu::{ self, CloseCondition, ItemHeight, ItemWidth, MenuBarState, PathHighlight, menu_roots_children, menu_roots_diff, }; -use crate::widget::{Icon, icon}; +use crate::widget::{Icon, context_menu, icon}; use crate::{Element, Renderer}; use derive_setters::Setters; use iced::clipboard::dnd::{ @@ -38,6 +38,7 @@ use std::collections::HashSet; use std::collections::hash_map::DefaultHasher; use std::hash::{Hash, Hasher}; use std::marker::PhantomData; +use std::sync::Arc; use std::time::{Duration, Instant}; thread_local! { @@ -107,7 +108,7 @@ pub trait SegmentedVariant { /// A conjoined group of items that function together as a button. #[derive(Setters)] #[must_use] -pub struct SegmentedButton<'a, Variant, SelectionMode, Message> +pub struct SegmentedButton<'a, Variant, SelectionMode, Message: Clone + 'static> where Model: Selectable, SelectionMode: Default, @@ -189,14 +190,28 @@ where 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)] + window_id: window::Id, + #[cfg(all( + feature = "multi-window", + feature = "wayland", + feature = "winit", + target_os = "linux" + ))] + positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, + #[setters(skip)] + pub(crate) on_surface_action: + Option Message + Send + Sync + 'static>>, + /// Defines the implementation of this struct variant: PhantomData, } -impl<'a, Variant, SelectionMode, Message> SegmentedButton<'a, Variant, SelectionMode, Message> +impl<'a, Variant, SelectionMode, Message: Clone + 'static> + SegmentedButton<'a, Variant, SelectionMode, Message> where Self: SegmentedVariant, Model: Selectable, @@ -243,6 +258,15 @@ where tab_drag: None, on_drop_hint: None, on_reorder: None, + window_id: window::Id::RESERVED, + #[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, } } @@ -306,12 +330,7 @@ where where Message: Clone + 'static, { - self.context_menu = context_menu.map(|menus| { - vec![menu::Tree::with_children( - crate::Element::from(crate::widget::Row::new()), - menus, - )] - }); + self.context_menu = context_menu; if let Some(ref mut context_menu) = self.context_menu { context_menu.iter_mut().for_each(menu::Tree::set_index); @@ -863,6 +882,253 @@ where |id| id.0, ) } + + #[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, + ) -> 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 = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] + #[allow(clippy::too_many_lines)] + fn create_popup<'b>( + &mut self, + layout: Layout<'b>, + view_cursor: mouse::Cursor, + renderer: &'b Renderer, + shell: &'b mut Shell<'_, Message>, + viewport: &'b Rectangle, + tree: &'b mut Tree, + ) { + let state = tree.state.downcast_mut::(); + let my_state = state.menu_state.clone(); + + if self.window_id != window::Id::NONE { + use crate::surface::action::{LiveSettings, destroy_popup}; + use iced_runtime::platform_specific::wayland::popup::{ + SctkPopupSettings, SctkPositioner, + }; + + let Some(surface_action) = self.on_surface_action.as_ref() else { + return; + }; + + let id = 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 + } else { + window::Id::unique() + } + }); + let Some(entity) = state.show_context else { + return; + }; + + let Some((mut bounds, i)) = self + .variant_bounds(state, layout.bounds()) + .enumerate() + .find_map(|(i, item)| match item { + ItemBounds::Button(e, bounds) if e == entity => Some((bounds, i)), + _ => None, + }) + else { + return; + }; + + assert!( + self.context_menu + .as_ref() + .is_none_or(|m| m[0].children.len() == self.model.len()), + "model length must match the number of context menus" + ); + let menu = self + .context_menu + .as_mut() + .map(|m| m[0].children[i].clone()) + .unwrap(); + + bounds.x = state.context_cursor.x; + bounds.y = state.context_cursor.y; + + let mut popup_menu: menu::Menu<'static, _> = menu::Menu { + tree: my_state.clone(), + menu_roots: std::borrow::Cow::Owned(vec![menu]), + bounds_expand: 0, + menu_overlays_parent: false, + 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: 0, + cross_offset: 0, + root_bounds_list: vec![bounds], + path_highlight: Some(PathHighlight::MenuActive), + style: std::borrow::Cow::Borrowed(&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(), + }; + + menu::init_root_menu( + &mut popup_menu, + renderer, + shell, + view_cursor.position().unwrap(), + viewport.size(), + Vector::new(0., 0.), + bounds, + 0., // TODO offset? + ); + 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: 1, + height: 1, + } + }, + |r| Rectangle { + x: r.x as i32, + y: r.y as i32, + width: 1, + height: 1, + }, + ), match (state.horizontal_direction, state.vertical_direction) { + (menu::Direction::Positive, menu::Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomRight, + (menu::Direction::Positive, menu::Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopRight, + (menu::Direction::Negative, menu::Direction::Positive) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::BottomLeft, + (menu::Direction::Negative, menu::Direction::Negative) => cctk::wayland_protocols::xdg::shell::client::xdg_positioner::Gravity::TopLeft, + }) + }); + + let menu_node = + popup_menu.layout(renderer, layout::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; + /// Used to create a popup message from within a widget. + #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] + #[must_use] + pub fn simple_popup( + live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, + settings: impl Fn() + -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + + Send + + Sync + + 'static, + view: Option crate::Element<'static, Message> + Send + Sync + 'static>, + ) -> crate::surface::Action { + use std::any::Any; + + let boxed: Box< + dyn Fn() -> iced_runtime::platform_specific::wayland::popup::SctkPopupSettings + + Send + + Sync + + 'static, + > = Box::new(settings); + let boxed: Box = Box::new(boxed); + + let boxed_live: Box LiveSettings + Send + Sync + 'static> = + Box::new(live_settings); + let boxed_live: Box = Box::new(boxed_live); + + crate::surface::Action::Popup( + Arc::new(boxed), + Arc::new(boxed_live), + view.map(|view| { + let boxed: Box< + dyn Fn() -> crate::Element<'static, Message> + Send + Sync + 'static, + > = Box::new(view); + let boxed: Box = Box::new(boxed); + Arc::new(boxed) + }), + ) + } + shell.publish((surface_action)(simple_popup( + LiveSettings::default, + 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)) + }), + ))); + } + } } impl Widget @@ -974,10 +1240,10 @@ where mut event: &Event, layout: Layout<'_>, cursor_position: mouse::Cursor, - _renderer: &Renderer, + renderer: &Renderer, clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, - _viewport: &iced::Rectangle, + viewport: &iced::Rectangle, ) { let my_bounds = layout.bounds(); let state = tree.state.downcast_mut::(); @@ -1396,6 +1662,7 @@ where && (right_button_released(&event) || (touch_lifted(&event) && fingers_pressed == 2)) { + // TODO create popup state.show_context = Some(key); state.context_cursor = cursor_position.position().unwrap_or_default(); @@ -1408,9 +1675,29 @@ where shell.publish(on_context(key)); shell.capture_event(); + + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] + if matches!( + crate::app::cosmic::WINDOWING_SYSTEM.get(), + Some(crate::app::cosmic::WindowingSystem::Wayland) + ) { + self.create_popup( + layout, + cursor_position, + renderer, + shell, + viewport, + tree, + ); + } return; } - if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Middle)) = event { @@ -1528,6 +1815,34 @@ where } } + if (matches!(event, Event::Mouse(mouse::Event::ButtonReleased(_))) || (touch_lifted(event))) + && let Some(_id) = state + .menu_state + .inner + .with_data_mut(|ms| ms.popup_id.remove(&self.window_id)) + { + #[cfg(all( + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] + { + let surface_action = self.on_surface_action.as_ref().unwrap(); + shell.capture_event(); + + shell.publish(surface_action(crate::surface::action::destroy_popup(_id))); + } + state.show_context = None; + + state.menu_state.inner.with_data_mut(|data| { + // Clear stale MenuBounds from any previous context menu before opening a new one. + data.reset(); + data.open = false; + data.view_cursor = cursor_position; + }); + } + if matches!( event, Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) @@ -2146,19 +2461,44 @@ where _viewport: &iced_core::Rectangle, translation: Vector, ) -> Option> { + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] + if matches!( + crate::app::cosmic::WINDOWING_SYSTEM.get(), + Some(crate::app::cosmic::WindowingSystem::Wayland) + ) && self.on_surface_action.is_some() + && self.window_id != window::Id::NONE + { + return None; + } + let state = tree.state.downcast_mut::(); let menu_state = state.menu_state.clone(); let entity = state.show_context?; - 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 (mut bounds, i) = self + .variant_bounds(state, layout.bounds()) + .enumerate() + .find_map(|(i, item)| match item { + ItemBounds::Button(e, bounds) if e == entity => Some((bounds, i)), + _ => None, + })?; - let context_menu = self.context_menu.as_mut()?; + assert!( + self.context_menu + .as_ref() + .is_none_or(|m| m[0].children.len() == self.model.len()) + ); + let menu = self + .context_menu + .as_mut() + .map(|m| m[0].children[i].clone())?; if !menu_state.inner.with_data(|data| data.open) { // If the menu is not open, we don't need to show it. @@ -2176,7 +2516,7 @@ where Some( crate::widget::menu::Menu { tree: menu_state, - menu_roots: std::borrow::Cow::Owned(context_menu.clone()), + menu_roots: std::borrow::Cow::Owned(vec![menu]), bounds_expand: 16, menu_overlays_parent: true, close_condition: CloseCondition { diff --git a/src/widget/segmented_control.rs b/src/widget/segmented_control.rs index 046956c7..cecc3eb1 100644 --- a/src/widget/segmented_control.rs +++ b/src/widget/segmented_control.rs @@ -14,7 +14,7 @@ use super::segmented_button::{ /// The data for the widget comes from a model that is maintained the application. /// /// For details on the model, see the [`segmented_button`] module for more details. -pub fn horizontal( +pub fn horizontal( model: &Model, ) -> HorizontalSegmentedButton<'_, SelectionMode, Message> where @@ -37,7 +37,7 @@ where /// The data for the widget comes from a model that is maintained the application. /// /// For details on the model, see the [`segmented_button`] module for more details. -pub fn vertical( +pub fn vertical( model: &Model, ) -> VerticalSegmentedButton<'_, SelectionMode, Message> where diff --git a/src/widget/tab_bar.rs b/src/widget/tab_bar.rs index a08128b4..358f1d4b 100644 --- a/src/widget/tab_bar.rs +++ b/src/widget/tab_bar.rs @@ -14,7 +14,7 @@ use super::segmented_button::{ /// The data for the widget comes from a model supplied by the application. /// /// For details on the model, see the [`segmented_button`] module for more details. -pub fn horizontal( +pub fn horizontal( model: &Model, ) -> HorizontalSegmentedButton<'_, SelectionMode, Message> where @@ -35,7 +35,7 @@ where /// /// The data for the widget comes from a model that is maintained the application. /// For details on the model, see the [`segmented_button`] module for more details. -pub fn vertical( +pub fn vertical( model: &Model, ) -> VerticalSegmentedButton<'_, SelectionMode, Message> where From 94d2c18a233b15ef98994d7f4336b744050b4142 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 12 Jun 2026 15:10:29 -0400 Subject: [PATCH 105/201] chore: dialog default to overlay --- src/theme/style/iced.rs | 11 +++++++---- src/widget/dialog.rs | 9 ++++++++- 2 files changed, 15 insertions(+), 5 deletions(-) diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index bac6e275..97246369 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -393,7 +393,7 @@ pub enum Container<'a> { transparent: bool, }, Custom(Box iced_container::Style + 'a>), - Dialog, + Dialog(bool), Dropdown, HeaderBar { focused: bool, @@ -688,14 +688,17 @@ impl iced_container::Catalog for Theme { } } - Container::Dialog => iced_container::Style { + Container::Dialog(is_overlay) => iced_container::Style { icon_color: Some(Color::from(cosmic.primary(self.transparent).on)), text_color: Some(Color::from(cosmic.primary(self.transparent).on)), background: Some(iced::Background::Color( - cosmic.primary(self.transparent).base.into(), + cosmic.primary(self.transparent && !is_overlay).base.into(), )), border: Border { - color: cosmic.primary(self.transparent).divider.into(), + color: cosmic + .primary(self.transparent && !is_overlay) + .divider + .into(), width: 1.0, radius: cosmic.corner_radii.radius_m.into(), }, diff --git a/src/widget/dialog.rs b/src/widget/dialog.rs index 72ab11f4..4ee027bd 100644 --- a/src/widget/dialog.rs +++ b/src/widget/dialog.rs @@ -18,6 +18,7 @@ pub struct Dialog<'a, Message> { height: Option, max_width: Option, max_height: Option, + is_overlay: bool, } impl Default for Dialog<'_, Message> { @@ -40,6 +41,7 @@ impl<'a, Message> Dialog<'a, Message> { height: None, max_width: None, max_height: None, + is_overlay: true, } } @@ -97,6 +99,11 @@ impl<'a, Message> Dialog<'a, Message> { self.max_width = Some(max_width.into()); self } + + pub fn is_overlay(mut self, is_overlay: bool) -> Self { + self.is_overlay = is_overlay; + self + } } impl<'a, Message: Clone + 'static> From> for Element<'a, Message> { @@ -157,7 +164,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes let mut container = widget::container( widget::column::with_children([content_row.into(), button_row.into()]).spacing(space_l), ) - .class(style::Container::Dialog) + .class(style::Container::Dialog(dialog.is_overlay)) .padding(space_m) .width(dialog.width.unwrap_or(Length::Fixed(570.0))); From 46c717fd57223ec35414c5272b7d7408b9d5f60b Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 15 Jun 2026 11:31:44 -0400 Subject: [PATCH 106/201] fix: corner radius setting match style --- src/widget/context_menu.rs | 19 +++++++++++++++++-- src/widget/menu/menu_bar.rs | 26 ++++++++++++++++++++++---- src/widget/menu/menu_inner.rs | 23 +++++++++++++++++++---- src/widget/segmented_button/widget.rs | 26 ++++++++++++++++++++++---- 4 files changed, 80 insertions(+), 14 deletions(-) diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index b66f95b1..4b31fc01 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -82,7 +82,9 @@ impl ContextMenu<'_, Message> { ) { if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { use crate::surface::action::{LiveSettings, destroy_popup}; - use crate::widget::menu::Menu; + use crate::theme::THEME; + use crate::widget::menu::{Menu, StyleSheet as _}; + use iced_runtime::platform_specific::wayland::CornerRadius; use iced_runtime::platform_specific::wayland::popup::{ SctkPopupSettings, SctkPositioner, }; @@ -185,9 +187,22 @@ impl ContextMenu<'_, Message> { ..Default::default() }; let parent = self.window_id; + let t = THEME.lock().unwrap(); + let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false); + drop(t); + let rad = styling.menu_border_radius; + shell.publish((self.on_surface_action.as_ref().unwrap())( crate::surface::action::simple_popup( - || LiveSettings::default(), + move || LiveSettings { + corners: Some(CornerRadius { + top_left: rad[0] as u32, + top_right: rad[1] as u32, + bottom_left: rad[2] as u32, + bottom_right: rad[3] as u32, + }), + ..Default::default() + }, move || SctkPopupSettings { parent, id, diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 560b8d2a..0e9fe1c3 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -385,9 +385,13 @@ where my_state: &mut MenuBarState, ) { if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { - use crate::surface::action::{LiveSettings, destroy_popup}; - use iced_runtime::platform_specific::wayland::popup::{ - SctkPopupSettings, SctkPositioner, + use crate::{ + surface::action::{LiveSettings, destroy_popup}, + theme::THEME, + }; + use iced_runtime::platform_specific::wayland::{ + CornerRadius, + popup::{SctkPopupSettings, SctkPositioner}, }; let surface_action = self.on_surface_action.as_ref().unwrap(); @@ -500,8 +504,22 @@ where ..Default::default() }; let parent = self.window_id; + + let t = THEME.lock().unwrap(); + let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false); + drop(t); + let rad = styling.menu_border_radius; + shell.publish((surface_action)(crate::surface::action::simple_popup( - || LiveSettings::default(), + move || LiveSettings { + corners: Some(CornerRadius { + top_left: rad[0] as u32, + top_right: rad[1] as u32, + bottom_left: rad[2] as u32, + bottom_right: rad[3] as u32, + }), + ..Default::default() + }, move || SctkPopupSettings { parent, id, diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 47e8e30d..ead2d5ac 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -986,11 +986,12 @@ impl Widget Widget Date: Mon, 15 Jun 2026 12:28:23 -0400 Subject: [PATCH 107/201] cleanup(segmented button): popup destruction on press --- src/widget/segmented_button/widget.rs | 38 ++++++++++++++++++++++++++- 1 file changed, 37 insertions(+), 1 deletion(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index cc3638e9..083d4efb 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -1661,6 +1661,43 @@ where state.unfocus(); } + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] + if is_pressed(event) + && let Some(on_context) = self.on_context.as_ref() + { + let (was_open, id) = state.menu_state.inner.with_data_mut(|data| { + let was_open = data.open; + data.reset(); + data.open = false; + data.view_cursor = cursor_position; + let root = data.popup_id.remove(&self.window_id); + data.popup_id.clear(); + (was_open, root) + }); + if let Some(w) = id + && let Some(surface_action) = self.on_surface_action.as_ref() + && was_open + { + use crate::surface::action::destroy_popup; + + shell.publish((surface_action)(destroy_popup(w))); + return; + } + } + + #[cfg(all( + feature = "multi-window", + feature = "wayland", + target_os = "linux", + feature = "winit", + feature = "surface-message" + ))] if let Some(on_activate) = self.on_activate.as_ref() { if is_pressed(event) { state.pressed_item = Some(Item::Tab(key)); @@ -1680,7 +1717,6 @@ where && (right_button_released(&event) || (touch_lifted(&event) && fingers_pressed == 2)) { - // TODO create popup state.show_context = Some(key); state.context_cursor = cursor_position.position().unwrap_or_default(); From 5ea8a8695972b0444481662ac7d1100637758ecb Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 15 Jun 2026 15:56:59 -0400 Subject: [PATCH 108/201] fix: card style --- src/widget/cards.rs | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/widget/cards.rs b/src/widget/cards.rs index ce1831d0..43325002 100644 --- a/src/widget/cards.rs +++ b/src/widget/cards.rs @@ -112,7 +112,7 @@ where Self { can_show_more, - id: Id::unique(), + id, show_less_button: { let mut show_less_children = Vec::with_capacity(3); if let Some(source) = show_less_icon { @@ -428,10 +428,11 @@ where let radius_xs = theme.cosmic().radius_xs(); // Draw first to appear behind if fully_unexpanded { + use crate::widget::card::style::Catalog as _; let card_layout = layout.next().unwrap(); - let appearance = Style::default(); + let appearance = theme.default(); let bg_layout = layout.collect::>(); - for (i, layout) in (0..2).zip(bg_layout.into_iter()).rev() { + for (i, layout) in (0..2).zip(bg_layout).rev() { renderer.fill_quad( Quad { bounds: layout.bounds(), From 9855d8b6fdd3cbe44dde24209f20d1de7537aa3d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 18 Jun 2026 16:18:33 -0400 Subject: [PATCH 109/201] fix(theme-gen): typo, use overlay colors --- cosmic-theme/src/model/theme.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 1f0bd748..390ca2f1 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1551,8 +1551,8 @@ impl ThemeBuilder { transparent_bg_component, accent, on_transparent_bg_component, - Srgba::new(0., 0., 0., 0.0), - Srgba::new(0., 0., 0., 0.0), + component_hovered_overlay, + component_pressed_overlay, is_high_contrast, control_steps_array[8], ), @@ -1593,8 +1593,8 @@ impl ThemeBuilder { text_steps_array.as_deref(), actual_alpha, ), - Srgba::new(0., 0., 0., 0.0), - Srgba::new(0., 0., 0., 0.0), + component_hovered_overlay, + component_pressed_overlay, is_high_contrast, control_steps_array[8], ), From 46b25d42182c3d08586f946354a2ef8b275289c2 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Fri, 19 Jun 2026 00:48:09 -0400 Subject: [PATCH 110/201] refactor: allow for transparent header --- src/app/mod.rs | 9 +++++++-- src/core.rs | 2 ++ 2 files changed, 9 insertions(+), 2 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index 99ea6e53..f757d5be 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -22,7 +22,7 @@ use crate::prelude::*; use crate::theme::THEME; use crate::widget::{container, id_container, menu, nav_bar, popover, space}; use apply::Apply; -use iced::{Length, Subscription, theme, window}; +use iced::{Color, Length, Subscription, theme, window}; pub use settings::Settings; use std::borrow::Cow; use std::cell::RefCell; @@ -625,6 +625,7 @@ impl ApplicationExt for App { let content_container = core.window.content_container; let show_context = core.window.show_context; let nav_bar_active = core.nav_bar_active(); + let transparent_header = core.window.transparent_header; let focused = core .focus_chain() .iter() @@ -833,7 +834,11 @@ impl ApplicationExt for App { let cosmic = theme.cosmic(); container::Style { background: Some(iced::Background::Color( - cosmic.background(theme.transparent).base.into(), + if transparent_header { + Color::TRANSPARENT + } else { + cosmic.background(theme.transparent).base.into() + }, )), border: iced::Border { radius: [ diff --git a/src/core.rs b/src/core.rs index a8691d11..0ca555e3 100644 --- a/src/core.rs +++ b/src/core.rs @@ -39,6 +39,7 @@ pub struct Window { pub show_close: bool, pub show_maximize: bool, pub show_minimize: bool, + pub transparent_header: bool, pub is_maximized: bool, height: f32, width: f32, @@ -169,6 +170,7 @@ impl Default for Core { show_minimize: true, show_window_menu: false, is_maximized: false, + transparent_header: false, height: 0., width: 0., }, From 822d3b2a0f5ee7110b624ca0dbfc8796aa23b0a9 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 25 Jun 2026 13:45:25 -0400 Subject: [PATCH 111/201] chore: update cosmic-protocols --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 40230807..b02b6ca7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -127,7 +127,7 @@ ashpd = { version = "0.12.3", default-features = false, optional = true } async-fs = { version = "2.2", optional = true } async-std = { workspace = true, optional = true } auto_enums = "0.8.8" -cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "a7d2d7a", optional = true } +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "32283d7", optional = true } jiff = "0.2" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } From cad4be9401b654ca3cb96908877a07fe379bfc5f Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 29 Jun 2026 13:40:58 -0400 Subject: [PATCH 112/201] fix: live settings override auto blur --- src/app/cosmic.rs | 26 ++++++++++++++++---------- 1 file changed, 16 insertions(+), 10 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 6b740e57..2d5b10d3 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -930,8 +930,7 @@ impl Cosmic { } for (id, wrapper, ..) in &self.surface_views { let overriden = wrapper.2(&self.app); - if core.blur(&theme, Some(wrapper.1)) || overriden.blur.unwrap_or_default() - { + if core.blur(&theme, Some(wrapper.1)) && overriden.blur.unwrap_or(true) { cmds.push(blur(*id)); } else if overriden.blur.is_some_and(|b| !b) { cmds.push(iced::window::disable_blur(*id)); @@ -1151,7 +1150,7 @@ impl Cosmic { for (id, wrapper, ..) in &self.surface_views { let overriden = wrapper.2(&self.app); if core.blur(&cosmic_theme, Some(wrapper.1)) - || overriden.blur.unwrap_or_default() + && overriden.blur.unwrap_or(true) { cmds.push(blur(*id)); } else if overriden.blur.is_some_and(|b| !b) { @@ -1296,7 +1295,7 @@ impl Cosmic { for (id, wrapper, ..) in &self.surface_views { let overriden = wrapper.2(&self.app); if core.blur(&cosmic_theme, Some(wrapper.1)) - || overriden.blur.unwrap_or_default() + && overriden.blur.unwrap_or(true) { cmds.push(blur(*id)); } else if overriden.blur.is_some_and(|b| !b) { @@ -1415,14 +1414,15 @@ impl Cosmic { // TODO do we need per window sharp corners? let rounded = (!self.app.core().window.sharp_corners && self.app.core().sync_window_border_radii_to_theme()) - || self.surface_views.get(&id).is_some_and( - |(_, surface_type, live_settings, _)| { + || self + .surface_views + .get(&id) + .is_some_and(|(_, surface_type, _, _)| { matches!( surface_type, SurfaceIdWrapper::Popup(_) | SurfaceIdWrapper::LayerSurface(_) ) - }, - ); + }); let new_blur = self.blur_enabled && { let t = theme.cosmic(); match self.app.core().app_type() { @@ -1435,7 +1435,13 @@ impl Cosmic { let wrapper = self.surface_views.get(&id).map(|s| s.1); // this will blur untracked windows as if they were the main window - let blur_cmd = if self.app.core().blur(&theme, wrapper) { + let blur_cmd = if self.app.core().blur(&theme, wrapper) + && self + .surface_views + .get(&id) + .and_then(|s| s.2(&self.app).blur) + .unwrap_or(true) + { let blur = if new_blur { iced::window::enable_blur } else { @@ -1518,7 +1524,7 @@ impl Cosmic { for (id, wrapper, ..) in &self.surface_views { let overriden = wrapper.2(&self.app); if self.app.core().blur(&t, Some(wrapper.1)) - || overriden.blur.unwrap_or_default() + && overriden.blur.unwrap_or(true) { cmds.push(blur(*id)); } else if overriden.blur.is_some_and(|b| !b) { From a365d4509155e9d4c92dfbe6f8b8b14fa48fca5c Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 29 Jun 2026 15:47:13 -0400 Subject: [PATCH 113/201] fix: menu folder corners --- src/widget/button/widget.rs | 5 ++++- src/widget/menu/menu_inner.rs | 15 ++++++++++++++- 2 files changed, 18 insertions(+), 2 deletions(-) diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index aa2c8a78..5ebfc5b2 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -472,7 +472,10 @@ impl<'a, Message: 'a + Clone> Widget theme.active(state.is_focused, self.selected, &self.style) }; - if matches!(self.style, crate::theme::Button::MenuItem) { + if matches!( + self.style, + crate::theme::Button::MenuItem | crate::theme::Button::MenuFolder + ) { match theme.list_item_position { Some((Alignment::Start, _)) => { styling.border_radius = diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index ead2d5ac..59c1c4d6 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -818,13 +818,26 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { .children() .nth(active.saturating_sub(start_index)) { + let i = active.saturating_sub(start_index); + let mut rad = styling.menu_border_radius; + let rad_0 = theme.cosmic().radius_0(); + if start_index == end_index { + } else if 0 == i { + rad[0] = rad_0[0]; + rad[1] = rad_0[1]; + } else if i == end_index - start_index { + rad[2] = rad_0[2]; + rad[3] = rad_0[3]; + } else { + rad = rad_0; + } let path_quad = renderer::Quad { bounds: active_layout .bounds() .intersection(&viewport) .unwrap_or_default(), border: Border { - radius: styling.menu_border_radius.into(), + radius: rad.into(), ..Default::default() }, shadow: Shadow::default(), From bb0145c566724340e65b0ec1617add643a82dd98 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Tue, 30 Jun 2026 11:06:41 -0400 Subject: [PATCH 114/201] chore: add the rounded rectangle strip helper --- src/surface/corner_radius.rs | 86 ++++++++++++++++++++++++++++++++++++ src/surface/mod.rs | 2 + src/widget/button/widget.rs | 2 +- 3 files changed, 89 insertions(+), 1 deletion(-) create mode 100644 src/surface/corner_radius.rs diff --git a/src/surface/corner_radius.rs b/src/surface/corner_radius.rs new file mode 100644 index 00000000..1eda578d --- /dev/null +++ b/src/surface/corner_radius.rs @@ -0,0 +1,86 @@ +use iced::Rectangle; +use iced_runtime::platform_specific::wayland::CornerRadius; + +#[must_use] +pub fn rounded_rect_strips(rect: Rectangle, radius: CornerRadius) -> Vec> { + let mut out = Vec::new(); + + let w = rect.width.max(0.0); + let h = rect.height.max(0.0); + + if w <= 0.0 || h <= 0.0 { + return out; + } + + let tl = radius.top_left as i32; + let tr = radius.top_right as i32; + let bl = radius.bottom_left as i32; + let br = radius.bottom_right as i32; + + let max_top = tl.max(tr); + let max_bottom = bl.max(br); + + let center_y = rect.y + max_top as f32; + let center_h = (h as i32 - max_top - max_bottom).max(0) as f32; + + if center_h > 0.0 { + out.push(Rectangle { + x: rect.x, + y: center_y, + width: w, + height: center_h, + }); + } + + for y in 0..max_top { + let left = if y < tl { circle_inset(tl, y) } else { 0 }; + + let right = if y < tr { circle_inset(tr, y) } else { 0 }; + + let strip_x = rect.x + left as f32; + let strip_w = (w as i32 - left - right).max(0) as f32; + + if strip_w > 0.0 { + out.push(Rectangle { + x: strip_x, + y: rect.y + y as f32, + width: strip_w, + height: 1.0, + }); + } + } + + for y in 0..max_bottom { + let cy = max_bottom - 1 - y; + let left = if cy < bl { circle_inset(bl, cy) } else { 0 }; + + let right = if cy < br { circle_inset(br, cy) } else { 0 }; + + let strip_x = rect.x + left as f32; + let strip_w = (w as i32 - left - right).max(0) as f32; + + if strip_w > 0.0 { + out.push(Rectangle { + x: strip_x, + y: rect.y + h - max_bottom as f32 + y as f32, + width: strip_w, + height: 1.0, + }); + } + } + + out +} + +fn circle_inset(radius: i32, y: i32) -> i32 { + if radius <= 0 { + return 0; + } + + let fy = y as f32 + 0.5; + let r = radius as f32; + + let x = r - (r * r - (r - fy) * (r - fy)).sqrt(); + + x.floor() as i32 +} diff --git a/src/surface/mod.rs b/src/surface/mod.rs index eec81e3f..b3858b97 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -2,6 +2,8 @@ // SPDX-License-Identifier: MPL-2.0 pub mod action; +#[cfg(all(feature = "wayland", target_os = "linux"))] +pub mod corner_radius; use iced::{Limits, Size, Task}; use std::any::Any; diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index 5ebfc5b2..a9d44b2b 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -488,7 +488,7 @@ impl<'a, Message: 'a + Clone> Widget None => { styling.border_radius = theme.cosmic().radius_0().into(); } - }; + } } let mut icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color); From be499d01afeffb3cee7ef58023e3d056317bca64 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 1 Jul 2026 10:56:48 -0400 Subject: [PATCH 115/201] fix: test --- cosmic-theme/src/model/theme.rs | 6 ++++-- iced | 2 +- src/widget/menu/menu_bar.rs | 12 +++++------- src/widget/menu/menu_inner.rs | 9 +++++---- src/widget/segmented_button/widget.rs | 13 ++++++------- 5 files changed, 21 insertions(+), 21 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 390ca2f1..9a80d31a 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1,16 +1,18 @@ +use crate::color::color_serde::option as color_serde_option; +use crate::color::{ColorRepr, ColorReprOption, color_serde}; use crate::composite::over; use crate::steps::{color_index, get_small_widget_color, get_surface_color, get_text, steps}; use crate::{ Component, Container, CornerRadii, CosmicPalette, CosmicPaletteInner, DARK_PALETTE, LIGHT_PALETTE, NAME, Spacing, ThemeMode, - color::{ColorRepr, ColorReprOption, color_serde, color_serde::option as color_serde_option}, }; use cosmic_config::{Config, CosmicConfigEntry}; use palette::color_difference::Wcag21RelativeContrast; use palette::rgb::Rgb; use palette::{IntoColor, Oklcha, Srgb, Srgba, WithAlpha}; use serde::{Deserialize, Serialize}; -use std::{default, num::NonZeroUsize}; +use std::default; +use std::num::NonZeroUsize; /// ID for the current dark `ThemeBuilder` config pub const DARK_THEME_BUILDER_ID: &str = "com.system76.CosmicTheme.Dark.Builder"; diff --git a/iced b/iced index cff835bc..c781ff61 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit cff835bc64e887b3db5c1fafa1f4c5cdf8db1b42 +Subproject commit c781ff6199715f5650411c7ac625d4e1929bfdfb diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 0e9fe1c3..fce272e1 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -385,13 +385,11 @@ where my_state: &mut MenuBarState, ) { if self.window_id != window::Id::NONE && self.on_surface_action.is_some() { - use crate::{ - surface::action::{LiveSettings, destroy_popup}, - theme::THEME, - }; - use iced_runtime::platform_specific::wayland::{ - CornerRadius, - popup::{SctkPopupSettings, SctkPositioner}, + use crate::surface::action::{LiveSettings, destroy_popup}; + use crate::theme::THEME; + use iced_runtime::platform_specific::wayland::CornerRadius; + use iced_runtime::platform_specific::wayland::popup::{ + SctkPopupSettings, SctkPositioner, }; let surface_action = self.on_surface_action.as_ref().unwrap(); diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 59c1c4d6..220a6a7f 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -999,12 +999,13 @@ impl Widget: Selectable, SelectionMode: Default, + Message: Clone, { const VERTICAL: bool = false; From 20dfd7c236208930634881ec52d22e7a353206dd Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 1 Jul 2026 12:32:33 -0400 Subject: [PATCH 116/201] cleanup: corner radius menu path logic --- src/widget/menu/menu_inner.rs | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 220a6a7f..53c87415 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -821,15 +821,16 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { let i = active.saturating_sub(start_index); let mut rad = styling.menu_border_radius; let rad_0 = theme.cosmic().radius_0(); - if start_index == end_index { - } else if 0 == i { - rad[0] = rad_0[0]; - rad[1] = rad_0[1]; - } else if i == end_index - start_index { - rad[2] = rad_0[2]; - rad[3] = rad_0[3]; - } else { - rad = rad_0; + if start_index != end_index { + if 0 == i { + rad[0] = rad_0[0]; + rad[1] = rad_0[1]; + } else if i == end_index - start_index { + rad[2] = rad_0[2]; + rad[3] = rad_0[3]; + } else { + rad = rad_0; + } } let path_quad = renderer::Quad { bounds: active_layout From 6219084eb6d7781d2ea532059e26831b4717a47c Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Mon, 25 May 2026 13:01:53 +0200 Subject: [PATCH 117/201] perf: throttle malloc_trim + avoid VecDeque clones (squashed) Squash of 2 yoda commits: - 77262dd0 perf(malloc): throttle malloc_trim to 1 Hz in hot paths - 1d98eee6 perf(widget): avoid VecDeque clone in segmented_button/table Model::clear --- src/malloc.rs | 34 +++++++++++++++++++++--- src/widget/segmented_button/model/mod.rs | 5 +++- src/widget/table/model/mod.rs | 5 +++- 3 files changed, 39 insertions(+), 5 deletions(-) diff --git a/src/malloc.rs b/src/malloc.rs index b99a66f4..bc5e8353 100644 --- a/src/malloc.rs +++ b/src/malloc.rs @@ -1,21 +1,49 @@ // Copyright 2025 System76 // SPDX-License-Identifier: MPL-2.0 +use std::cell::Cell; use std::os::raw::c_int; +use std::time::{Duration, Instant}; const M_MMAP_THRESHOLD: c_int = -3; +/// Minimum interval between two actual `malloc_trim` calls. +/// +/// `trim` is called at the end of every `update()` and `view()`, which can +/// reach 60-200 Hz during typical scrolling, resize, or animation. Each +/// `malloc_trim` walks the glibc heap (10 µs to several ms depending on +/// fragmentation), so calling it at render frequency can consume a +/// substantial fraction of the frame budget. Throttling to 1 Hz keeps RSS +/// bounded while removing the per-frame syscall from the hot path. +const TRIM_MIN_INTERVAL: Duration = Duration::from_millis(1000); + +thread_local! { + static LAST_TRIM: Cell> = const { Cell::new(None) }; +} + unsafe extern "C" { fn malloc_trim(pad: usize); fn mallopt(param: c_int, value: c_int) -> c_int; } +/// Throttled wrapper over `malloc_trim`. Safe to call at render frequency: +/// consecutive calls within `TRIM_MIN_INTERVAL` (per-thread) skip the syscall. #[inline] pub fn trim(pad: usize) { - unsafe { - malloc_trim(pad); - } + LAST_TRIM.with(|last| { + let now = Instant::now(); + let should_trim = match last.get() { + None => true, + Some(prev) => now.duration_since(prev) >= TRIM_MIN_INTERVAL, + }; + if should_trim { + last.set(Some(now)); + unsafe { + malloc_trim(pad); + } + } + }); } /// Prevents glibc from hoarding memory via memory fragmentation. diff --git a/src/widget/segmented_button/model/mod.rs b/src/widget/segmented_button/model/mod.rs index e0dd8c54..fff1cf6b 100644 --- a/src/widget/segmented_button/model/mod.rs +++ b/src/widget/segmented_button/model/mod.rs @@ -132,7 +132,10 @@ where /// ``` #[inline] pub fn clear(&mut self) { - for entity in self.order.clone() { + // `remove()` mutates `self.order`, so transfer ownership first: + // the inner `self.order.remove(index)` then no-ops because + // `position()` can't find the entity in the empty VecDeque. + for entity in std::mem::take(&mut self.order) { self.remove(entity); } } diff --git a/src/widget/table/model/mod.rs b/src/widget/table/model/mod.rs index 93998f9f..f0e5e61c 100644 --- a/src/widget/table/model/mod.rs +++ b/src/widget/table/model/mod.rs @@ -98,7 +98,10 @@ where /// model.clear(); /// ``` pub fn clear(&mut self) { - for entity in self.order.clone() { + // `remove()` mutates `self.order`, so transfer ownership first: + // the inner `self.order.remove(index)` then no-ops because + // `position()` can't find the entity in the empty VecDeque. + for entity in std::mem::take(&mut self.order) { self.remove(entity); } } From b7c81907baa948211985c39857010275efd2ba61 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Mon, 25 May 2026 13:01:53 +0200 Subject: [PATCH 118/201] feat(segmented_button): on_double_click + internal tab reorder (squashed) Squash of 2 yoda commits: - 108441ef segmented_button: add on_double_click callback - a322516f segmented_button: fix internal tab reorder end-to-end --- src/widget/segmented_button/widget.rs | 67 ++++++++++++++++++++++++--- 1 file changed, 61 insertions(+), 6 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index f5ca21a8..7a996db1 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -176,6 +176,10 @@ where pub(super) on_context: Option Message + 'static>>, #[setters(skip)] pub(super) on_middle_press: Option Message + 'static>>, + /// Emits the ID of the item that was double-clicked with the left button. + /// Fires in addition to `on_activate` (which keeps firing on each click). + #[setters(skip)] + pub(super) on_double_click: Option Message + 'static>>, #[setters(skip)] pub(super) on_dnd_drop: Option, String, DndAction) -> Message + 'static>>, @@ -249,6 +253,7 @@ where on_close: None, on_context: None, on_middle_press: None, + on_double_click: None, on_dnd_drop: None, on_dnd_enter: None, on_dnd_leave: None, @@ -375,6 +380,16 @@ where self } + /// Emitted when a tab is double-clicked with the left mouse button. + /// Fires in addition to `on_activate`, which keeps firing on each click. + pub fn on_double_click(mut self, on_double_click: T) -> Self + where + T: Fn(Entity) -> Message + 'static, + { + self.on_double_click = Some(Box::new(on_double_click)); + self + } + /// Enable drag-and-drop support for tabs using the provided payload builder. pub fn enable_tab_drag(mut self, mime: String) -> Self { self.tab_drag = Some(TabDragSource::new(mime)); @@ -412,11 +427,12 @@ where { 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)); + // Always use positional swap (Konsole/Firefox/Chrome semantics): + // dropping onto any part of a different tab swaps it with the dragged + // tab. drop_hint.side-based Before/After is counter-intuitive: dropping + // A (pos 0) on the left half of B (pos 1) resolves to "Before B" which, + // after removing A, lands at pos 0 — so the tab appears not to move. + let position = self.default_insert_position(dragged, target); Some(ReorderEvent { dragged, target, @@ -1196,6 +1212,7 @@ where hovered: Default::default(), known_length: Default::default(), middle_clicked: Default::default(), + last_click: None, internal_layout: Default::default(), context_cursor: Point::default(), show_context: Default::default(), @@ -1469,7 +1486,14 @@ where .dnd_state .drag_offer .as_ref() - .is_some_and(|offer| offer.selected_action.contains(DndAction::Move)); + .is_some_and(|offer| offer.selected_action.contains(DndAction::Move)) + // Self-drop fallback: some compositors (cosmic-comp + // observed) do not emit OfferEvent::SelectedAction for + // internal drags, leaving selected_action empty. + // dragging_tab is only set by start_tab_drag on this + // same widget, so this covers the self-drop case + // safely; mime and on_reorder are checked below. + || state.dragging_tab.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) @@ -1704,6 +1728,33 @@ where state.set_focused(); state.focused_item = Item::Tab(key); state.pressed_item = None; + + // Double-click detection on the same entity + // within 400 ms — fires after on_activate so + // the tab is already focused when the handler + // runs. + if let Some(on_double_click) = + self.on_double_click.as_ref() + { + let now = Instant::now(); + let is_double = match state.last_click { + Some((prev, t)) => { + prev == key + && now.duration_since(t) + < Duration::from_millis(400) + } + None => false, + }; + state.last_click = if is_double { + None + } else { + Some((key, now)) + }; + if is_double { + shell.publish(on_double_click(key)); + } + } + shell.capture_event(); return; } @@ -2783,6 +2834,9 @@ pub struct LocalState { hovered: Item, /// The ID of the button that was middle-clicked, but not yet released. middle_clicked: Option, + /// Entity and timestamp of the most recent left-click activation, used + /// to detect double-clicks on the same tab. + last_click: Option<(Entity, Instant)>, /// Last known length of the model. pub(super) known_length: usize, /// Dimensions of internal buttons when shrinking @@ -2929,6 +2983,7 @@ mod tests { hovered: Item::default(), known_length: 0, middle_clicked: None, + last_click: None, internal_layout: Vec::new(), context_cursor: Point::ORIGIN, show_context: None, From 261a211647a55ad0917c618aa44f89149eab2312 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Wed, 22 Apr 2026 15:08:12 +0200 Subject: [PATCH 119/201] header_bar: add WindowControlsPosition (macOS-style left controls) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adds a new public enum `WindowControlsPosition { Start, End }` and a matching field on `HeaderBar`, allowing window controls (close / minimize / maximize) to be packed on the start side of the headerbar (macOS style, icon order close → minimize → maximize) instead of the default end side (Linux / GNOME style, minimize → maximize → close). Wiring: - `crate::widget::WindowControlsPosition` re-exported alongside `HeaderBar`. - `HeaderBar::controls_position(Option)` setter; when left unset, falls back to `crate::config::window_controls_position()` (reads `CosmicTk.window_controls_position`), mirroring how `density` falls back to `header_size()`. - New `CosmicTk.window_controls_position` field with default `End` for backwards compatibility; serde-friendly enum so existing configs keep working via `#[serde(default)]` semantics. Tested with cosmic-yoterm, cosmic-settings, cosmic-edit, cosmic-files rebuilt against this libcosmic via a local `[patch]` override. Config changes picked up live through the existing cosmic-config subscription. Co-Authored-By: Claude Opus 4.7 (1M context) --- src/config/mod.rs | 12 +++++ src/widget/header_bar.rs | 104 ++++++++++++++++++++++++++++++--------- src/widget/mod.rs | 2 +- 3 files changed, 94 insertions(+), 24 deletions(-) diff --git a/src/config/mod.rs b/src/config/mod.rs index 9807961c..1691caa8 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -4,6 +4,7 @@ //! Configurations available to libcosmic applications. use crate::cosmic_theme::Density; +use crate::widget::WindowControlsPosition; use cosmic_config::cosmic_config_derive::CosmicConfigEntry; use cosmic_config::{Config, CosmicConfigEntry}; use serde::{Deserialize, Serialize}; @@ -67,6 +68,12 @@ pub fn header_size() -> Density { COSMIC_TK.read().unwrap().header_size } +/// Position of the window control buttons (close / minimize / maximize). +#[allow(clippy::missing_panics_doc)] +pub fn window_controls_position() -> WindowControlsPosition { + COSMIC_TK.read().unwrap().window_controls_position +} + /// Interface density. #[allow(clippy::missing_panics_doc)] pub fn interface_density() -> Density { @@ -109,6 +116,10 @@ pub struct CosmicTk { /// Mono font family pub monospace_font: FontConfig, + + /// Side on which window control buttons (close / minimize / maximize) + /// are placed. `End` = right (Linux / GNOME), `Start` = left (macOS). + pub window_controls_position: WindowControlsPosition, } impl Default for CosmicTk { @@ -132,6 +143,7 @@ impl Default for CosmicTk { stretch: iced::font::Stretch::Normal, style: iced::font::Style::Normal, }, + window_controls_position: WindowControlsPosition::default(), } } } diff --git a/src/widget/header_bar.rs b/src/widget/header_bar.rs index 556466fd..82a7e8e9 100644 --- a/src/widget/header_bar.rs +++ b/src/widget/header_bar.rs @@ -27,9 +27,32 @@ pub fn header_bar<'a, Message>() -> HeaderBar<'a, Message> { sharp_corners: false, is_ssd: false, on_double_click: None, + transparent: false, + controls_position: None, } } +/// Position of the window control buttons (close/min/max) within the headerbar. +#[derive( + Clone, + Copy, + Debug, + Default, + PartialEq, + Eq, + serde::Serialize, + serde::Deserialize, +)] +pub enum WindowControlsPosition { + /// Controls packed at the start (left on LTR) — macOS style. + /// Internal icon order becomes close → minimize → maximize. + Start, + /// Controls packed at the end (right on LTR) — Linux / GNOME style. + /// Internal icon order is minimize → maximize → close. + #[default] + End, +} + #[derive(Setters)] pub struct HeaderBar<'a, Message> { /// Defines the title of the window @@ -88,6 +111,17 @@ pub struct HeaderBar<'a, Message> { /// HeaderBar used for server-side decorations is_ssd: bool, + + /// Whether the headerbar should be transparent + transparent: bool, + + /// Side on which the window control buttons (close / minimize / maximize) + /// are rendered. `None` falls back to the user's CosmicTk config + /// (`crate::config::window_controls_position()`). `Some` overrides it. + /// `End` matches Linux / GNOME conventions; `Start` provides macOS-style + /// left-side controls. + #[setters(strip_option)] + controls_position: Option, } impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { @@ -370,12 +404,20 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { let is_ssd = self.is_ssd; // Take ownership of the regions to be packed. - let start = std::mem::take(&mut self.start); + let mut start = std::mem::take(&mut self.start); let center = std::mem::take(&mut self.center); let mut end = std::mem::take(&mut self.end); - // Also packs the window controls at the very end. - end.push(self.window_controls(space_xxs)); + // Pack window controls on the configured side (reads CosmicTk + // config when the builder did not set an explicit override). + let controls_position = self + .controls_position + .unwrap_or_else(crate::config::window_controls_position); + let controls = self.window_controls(space_xxs, controls_position); + match controls_position { + WindowControlsPosition::End => end.push(controls), + WindowControlsPosition::Start => start.insert(0, controls), + } let padding = if is_ssd { [2, 8, 2, 8] @@ -445,7 +487,11 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { } /// Creates the widget for window controls. - fn window_controls(&mut self, spacing: u16) -> Element<'a, Message> { + fn window_controls( + &mut self, + spacing: u16, + controls_position: WindowControlsPosition, + ) -> Element<'a, Message> { macro_rules! icon { ($name:expr, $size:expr, $on_press:expr) => {{ widget::icon::from_name($name) @@ -458,25 +504,37 @@ impl<'a, Message: Clone + 'static> HeaderBar<'a, Message> { }}; } - widget::row::with_capacity(3) - .push_maybe( - self.on_minimize - .take() - .map(|m| icon!("window-minimize-symbolic", 16, m)), - ) - .push_maybe(self.on_maximize.take().map(|m| { - if self.maximized { - icon!("window-restore-symbolic", 16, m) - } else { - icon!("window-maximize-symbolic", 16, m) - } - })) - .push_maybe( - self.on_close - .take() - .map(|m| icon!("window-close-symbolic", 16, m)), - ) - .spacing(spacing) + let minimize = self + .on_minimize + .take() + .map(|m| icon!("window-minimize-symbolic", 16, m)); + let maximize = self.on_maximize.take().map(|m| { + if self.maximized { + icon!("window-restore-symbolic", 16, m) + } else { + icon!("window-maximize-symbolic", 16, m) + } + }); + let close = self + .on_close + .take() + .map(|m| icon!("window-close-symbolic", 16, m)); + + // Icon order follows the OS convention for the chosen side: + // End → minimize, maximize, close (Linux / GNOME) + // Start → close, minimize, maximize (macOS) + let row = widget::row::with_capacity(3); + let row = match controls_position { + WindowControlsPosition::End => row + .push_maybe(minimize) + .push_maybe(maximize) + .push_maybe(close), + WindowControlsPosition::Start => row + .push_maybe(close) + .push_maybe(minimize) + .push_maybe(maximize), + }; + row.spacing(spacing) .align_y(iced::Alignment::Center) .into() } diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 5089b997..331d6c21 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -225,7 +225,7 @@ pub use grid::{Grid, grid}; mod header_bar; #[doc(inline)] -pub use header_bar::{HeaderBar, header_bar}; +pub use header_bar::{HeaderBar, WindowControlsPosition, header_bar}; pub mod icon; #[doc(inline)] From 45eaaf1605bc160ab0e0bf90fe3236d5beed1f1f Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Mon, 25 May 2026 13:02:07 +0200 Subject: [PATCH 120/201] =?UTF-8?q?yoda:=20fork=20pivot=20=E2=80=94=20Wayl?= =?UTF-8?q?and-only=20+=20ungate=20winit=20+=20soft-fork=20libcosmic-yoda?= =?UTF-8?q?=20(squashed)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squash of 7 yoda commits forming the fork pivot: - 255cf7cc rename: libcosmic -> libcosmic-yoda (fork 0.1.0-yoda) - 8701aa31 feat(yoda): Wayland-only cut — drop winit and x11 features - 6736a596 yoda: soft-fork pivot — keep Cargo name 'libcosmic' for dep unification - 3e23d087 yoda: re-apply hard rename — libcosmic -> libcosmic-yoda (0.1.0-yoda) - aec3eb61 yoda: ungate remaining winit+wayland combined cfgs - 8ab7b158 yoda-v2: color_picker Theme ref + context_menu/menu ungate winit - 8d1d8739 yoda: drop x11 defaults on iced_winit + iced_tiny_skia --- Cargo.toml | 51 +++++++++++-------- examples/about/Cargo.toml | 2 +- examples/applet/Cargo.toml | 2 +- examples/application/Cargo.toml | 4 +- examples/calendar/Cargo.toml | 2 +- examples/context-menu/Cargo.toml | 2 +- examples/cosmic/Cargo.toml | 2 +- examples/image-button/Cargo.toml | 2 +- examples/menu/Cargo.toml | 2 +- examples/multi-window/Cargo.toml | 2 +- examples/nav-context/Cargo.toml | 2 +- examples/open-dialog/Cargo.toml | 6 +-- examples/spin-button/Cargo.toml | 2 +- examples/subscriptions/Cargo.toml | 2 +- examples/table-view/Cargo.toml | 2 +- examples/text-input/Cargo.toml | 2 +- i18n/af/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/ar/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/be/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/bg/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/bn/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/ca/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/cs/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/da/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/de/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/el/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 .../{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/en/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/eo/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 .../{libcosmic.ftl => libcosmic_yoda.ftl} | 0 .../{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/es/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/et/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/eu/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/fa/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/fi/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/fr/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/fy/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/ga/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/gd/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/gu/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/he/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/hi/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/hr/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/hu/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/id/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/ie/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/is/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/it/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/ja/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/jv/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/ka/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 .../kab/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/kk/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 .../kmr/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/kn/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/ko/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/li/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/lt/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/ml/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/ms/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 .../{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/nl/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/nn/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/oc/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/pa/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/pl/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 .../{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/pt/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/ro/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/ru/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/sk/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/sl/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 .../{libcosmic.ftl => libcosmic_yoda.ftl} | 0 .../{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/sr/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/sv/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/ta/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/th/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/ti/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/tr/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/uk/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/uz/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 i18n/vi/{libcosmic.ftl => libcosmic_yoda.ftl} | 0 .../{libcosmic.ftl => libcosmic_yoda.ftl} | 0 .../{libcosmic.ftl => libcosmic_yoda.ftl} | 0 .../{libcosmic.ftl => libcosmic_yoda.ftl} | 0 src/action.rs | 3 -- src/command.rs | 2 - src/core.rs | 6 --- src/lib.rs | 3 -- src/surface/action.rs | 23 ++++----- src/theme/style/mod.rs | 4 +- src/widget/color_picker/mod.rs | 6 ++- src/widget/context_menu.rs | 6 --- src/widget/dropdown/mod.rs | 2 +- src/widget/dropdown/widget.rs | 22 ++++---- src/widget/menu/menu_bar.rs | 9 ---- src/widget/menu/menu_inner.rs | 6 +-- src/widget/mod.rs | 2 +- src/widget/responsive_menu_bar.rs | 4 +- 101 files changed, 82 insertions(+), 103 deletions(-) rename i18n/af/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/ar/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/be/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/bg/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/bn/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/ca/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/cs/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/da/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/de/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/el/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/en-GB/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/en/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/eo/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/es-419/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/es-MX/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/es/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/et/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/eu/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/fa/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/fi/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/fr/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/fy/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/ga/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/gd/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/gu/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/he/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/hi/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/hr/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/hu/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/id/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/ie/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/is/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/it/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/ja/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/jv/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/ka/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/kab/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/kk/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/kmr/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/kn/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/ko/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/li/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/lt/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/ml/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/ms/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/nb-NO/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/nl/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/nn/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/oc/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/pa/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/pl/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/pt-BR/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/pt/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/ro/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/ru/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/sk/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/sl/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/sr-Cyrl/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/sr-Latn/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/sr/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/sv/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/ta/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/th/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/ti/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/tr/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/uk/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/uz/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/vi/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/yue-Hant/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/zh-Hans/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) rename i18n/zh-Hant/{libcosmic.ftl => libcosmic_yoda.ftl} (100%) diff --git a/Cargo.toml b/Cargo.toml index b02b6ca7..8aa2c74c 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,11 @@ [package] -name = "libcosmic" -version = "1.0.0" +# Yoda fork: hard-renamed. Every consumer (leyoda/cosmic-files fork + each +# leyoda/cosmic-* app) depends directly on `libcosmic-yoda` by path, bypassing +# pop-os/libcosmic entirely. No [patch] shenanigans needed — transitive deps +# that used to ask for `libcosmic` are replaced by deps on our forks that ask +# for `libcosmic-yoda`. +name = "libcosmic-yoda" +version = "0.1.0-yoda.2" edition = "2024" rust-version = "1.93" @@ -9,12 +14,10 @@ name = "cosmic" [features] default = [ - "winit", "tokio", "a11y", "dbus-config", - "x11", - "iced-wayland", + "wayland", "multi-window", ] advanced-shaping = ["iced/advanced-shaping"] @@ -35,7 +38,6 @@ animated-image = [ autosize = [] applet = [ "autosize", - "winit", "wayland", "tokio", "cosmic-panel-config", @@ -81,32 +83,34 @@ tokio = [ "cosmic-config/tokio", ] # Tokio async runtime -# Wayland window support -iced-wayland = [ +# Wayland window support (yoda fork is Wayland-only; always active in default). +# We still need iced/winit because pop-os/iced hosts the runtime dispatcher +# (`iced_winit as shell`) there — the name is a misnomer, it's the same crate +# that provides both the winit path AND the sctk/cctk wayland path. +wayland = [ "ashpd?/wayland", "autosize", + "iced/winit", "iced/wayland", "iced_winit/wayland", - "surface-message", -] -wayland = [ - "iced-wayland", "iced_runtime/cctk", "iced_winit/cctk", "iced_wgpu/cctk", "iced/cctk", + "dep:iced_winit", "dep:cctk", + "surface-message", ] surface-message = [] # multi-window support multi-window = [] # Render with wgpu wgpu = ["iced/wgpu", "iced_wgpu"] -# X11 window support via winit -winit = ["iced/winit", "iced_winit"] -winit_debug = ["winit", "debug"] -winit_tokio = ["winit", "tokio"] -winit_wgpu = ["winit", "wgpu"] +# Compat stubs — kept empty so upstream deps (cosmic-files, cosmic-text, …) +# that still ask for `winit` / `x11` features resolve cleanly against the +# yoda fork. Activating them has no effect: no code is gated on these. +winit = [] +x11 = [] # Enables XDG portal integrations xdg-portal = ["ashpd"] qr_code = ["iced/qr_code"] @@ -119,7 +123,6 @@ async-std = [ "zbus?/async-io", "iced/async-std", ] -x11 = ["iced/x11", "iced_winit/x11"] [dependencies] apply = "0.3.0" @@ -225,10 +228,17 @@ optional = true [dependencies.iced_tiny_skia] path = "./iced/tiny_skia" +# Yoda: drop the x11 default → softbuffer no longer pulls tiny-xlib/x11-dl/etc. +default-features = false +features = ["wayland"] [dependencies.iced_winit] path = "./iced/winit" optional = true +# Yoda: drop the x11 default → winit won't pull winit-x11/tiny-xlib/x11-dl. +# Keep wayland + wayland-dlopen (default behaviour minus x11). +default-features = false +features = ["wayland", "wayland-dlopen"] [dependencies.iced_wgpu] path = "./iced/wgpu" @@ -244,9 +254,10 @@ members = [ "cosmic-config", "cosmic-config-derive", "cosmic-theme", - "examples/*", ] -exclude = ["iced"] +# examples/* excluded — many depend on the removed winit/x11 features. +# They will be revisited and adapted in a later phase. +exclude = ["iced", "examples"] [workspace.dependencies] async-std = "1.13" diff --git a/examples/about/Cargo.toml b/examples/about/Cargo.toml index f980811c..b27b5134 100644 --- a/examples/about/Cargo.toml +++ b/examples/about/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] open = "5.3.3" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" features = [ "debug", diff --git a/examples/applet/Cargo.toml b/examples/applet/Cargo.toml index 13eff684..265fbe7f 100644 --- a/examples/applet/Cargo.toml +++ b/examples/applet/Cargo.toml @@ -12,7 +12,7 @@ tracing = "0.1" env_logger = "0.10.2" log = "0.4.29" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" default-features = false features = ["applet-token"] diff --git a/examples/application/Cargo.toml b/examples/application/Cargo.toml index 1fb2949b..b5968461 100644 --- a/examples/application/Cargo.toml +++ b/examples/application/Cargo.toml @@ -5,12 +5,12 @@ edition = "2021" [features] default = ["wayland"] -wayland = ["libcosmic/wayland"] +wayland = ["libcosmic-yoda/wayland"] [dependencies] env_logger = "0.11" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" features = [ "debug", diff --git a/examples/calendar/Cargo.toml b/examples/calendar/Cargo.toml index b7286825..203f7c17 100644 --- a/examples/calendar/Cargo.toml +++ b/examples/calendar/Cargo.toml @@ -8,6 +8,6 @@ edition = "2024" [dependencies] jiff = "0.2" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"] diff --git a/examples/context-menu/Cargo.toml b/examples/context-menu/Cargo.toml index 39c550f4..4c1eed60 100644 --- a/examples/context-menu/Cargo.toml +++ b/examples/context-menu/Cargo.toml @@ -8,7 +8,7 @@ tracing = "0.1.44" tracing-subscriber = "0.3.22" tracing-log = "0.2.0" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" features = [ "debug", diff --git a/examples/cosmic/Cargo.toml b/examples/cosmic/Cargo.toml index 8c2a3126..eebf6c31 100644 --- a/examples/cosmic/Cargo.toml +++ b/examples/cosmic/Cargo.toml @@ -8,7 +8,7 @@ publish = false [dependencies] apply = "0.3.0" fraction = "0.15.3" -libcosmic = { path = "../..", features = [ +libcosmic-yoda = { path = "../..", features = [ "debug", "winit", "tokio", diff --git a/examples/image-button/Cargo.toml b/examples/image-button/Cargo.toml index c219a53b..8bc521f7 100644 --- a/examples/image-button/Cargo.toml +++ b/examples/image-button/Cargo.toml @@ -7,6 +7,6 @@ edition = "2021" tracing = "0.1.44" tracing-subscriber = "0.3.22" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" features = ["debug", "winit", "wgpu", "tokio"] diff --git a/examples/menu/Cargo.toml b/examples/menu/Cargo.toml index 430b26ea..047055e5 100644 --- a/examples/menu/Cargo.toml +++ b/examples/menu/Cargo.toml @@ -8,6 +8,6 @@ tracing = "0.1.44" tracing-subscriber = "0.3.22" tracing-log = "0.2.0" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"] diff --git a/examples/multi-window/Cargo.toml b/examples/multi-window/Cargo.toml index 0b5440f8..c38595fa 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", "wgpu", "wayland"] } +libcosmic-yoda = { path = "../..", features = ["debug", "winit", "tokio", "single-instance", "wgpu", "wayland"] } diff --git a/examples/nav-context/Cargo.toml b/examples/nav-context/Cargo.toml index d829df0f..ea2bc2b8 100644 --- a/examples/nav-context/Cargo.toml +++ b/examples/nav-context/Cargo.toml @@ -8,6 +8,6 @@ tracing = "0.1.44" tracing-subscriber = "0.3.22" tracing-log = "0.2.0" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" features = ["debug", "winit", "tokio", "xdg-portal", "wgpu"] diff --git a/examples/open-dialog/Cargo.toml b/examples/open-dialog/Cargo.toml index 94049270..b09b98ca 100644 --- a/examples/open-dialog/Cargo.toml +++ b/examples/open-dialog/Cargo.toml @@ -5,8 +5,8 @@ edition = "2021" [features] default = ["xdg-portal"] -rfd = ["libcosmic/rfd"] -xdg-portal = ["libcosmic/xdg-portal"] +rfd = ["libcosmic-yoda/rfd"] +xdg-portal = ["libcosmic-yoda/xdg-portal"] [dependencies] apply = "0.3.0" @@ -15,6 +15,6 @@ tracing = "0.1.44" tracing-subscriber = "0.3.22" url = "2.5.8" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] features = ["debug", "winit", "wgpu", "wayland", "tokio"] path = "../../" diff --git a/examples/spin-button/Cargo.toml b/examples/spin-button/Cargo.toml index a522050b..082c0fd5 100644 --- a/examples/spin-button/Cargo.toml +++ b/examples/spin-button/Cargo.toml @@ -6,7 +6,7 @@ edition = "2021" [dependencies] fraction = "0.15.3" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] features = ["debug", "wgpu", "winit", "desktop", "tokio"] path = "../.." default-features = false diff --git a/examples/subscriptions/Cargo.toml b/examples/subscriptions/Cargo.toml index 8eb69ff3..ae31a395 100644 --- a/examples/subscriptions/Cargo.toml +++ b/examples/subscriptions/Cargo.toml @@ -5,6 +5,6 @@ edition = "2024" [dependencies] -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" features = ["debug", "winit", "wgpu", "tokio", "xdg-portal"] diff --git a/examples/table-view/Cargo.toml b/examples/table-view/Cargo.toml index 8ed45928..8f71e5b4 100644 --- a/examples/table-view/Cargo.toml +++ b/examples/table-view/Cargo.toml @@ -9,6 +9,6 @@ tracing-subscriber = "0.3.22" tracing-log = "0.2.0" chrono = "*" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] features = ["debug", "wgpu", "winit", "desktop", "tokio"] path = "../.." diff --git a/examples/text-input/Cargo.toml b/examples/text-input/Cargo.toml index fe6105c2..69bd2a19 100644 --- a/examples/text-input/Cargo.toml +++ b/examples/text-input/Cargo.toml @@ -8,6 +8,6 @@ tracing = "0.1.44" tracing-subscriber = "0.3.22" tracing-log = "0.2.0" -[dependencies.libcosmic] +[dependencies.libcosmic-yoda] path = "../../" features = ["debug", "winit", "wgpu", "tokio", "xdg-portal"] diff --git a/i18n/af/libcosmic.ftl b/i18n/af/libcosmic_yoda.ftl similarity index 100% rename from i18n/af/libcosmic.ftl rename to i18n/af/libcosmic_yoda.ftl diff --git a/i18n/ar/libcosmic.ftl b/i18n/ar/libcosmic_yoda.ftl similarity index 100% rename from i18n/ar/libcosmic.ftl rename to i18n/ar/libcosmic_yoda.ftl diff --git a/i18n/be/libcosmic.ftl b/i18n/be/libcosmic_yoda.ftl similarity index 100% rename from i18n/be/libcosmic.ftl rename to i18n/be/libcosmic_yoda.ftl diff --git a/i18n/bg/libcosmic.ftl b/i18n/bg/libcosmic_yoda.ftl similarity index 100% rename from i18n/bg/libcosmic.ftl rename to i18n/bg/libcosmic_yoda.ftl diff --git a/i18n/bn/libcosmic.ftl b/i18n/bn/libcosmic_yoda.ftl similarity index 100% rename from i18n/bn/libcosmic.ftl rename to i18n/bn/libcosmic_yoda.ftl diff --git a/i18n/ca/libcosmic.ftl b/i18n/ca/libcosmic_yoda.ftl similarity index 100% rename from i18n/ca/libcosmic.ftl rename to i18n/ca/libcosmic_yoda.ftl diff --git a/i18n/cs/libcosmic.ftl b/i18n/cs/libcosmic_yoda.ftl similarity index 100% rename from i18n/cs/libcosmic.ftl rename to i18n/cs/libcosmic_yoda.ftl diff --git a/i18n/da/libcosmic.ftl b/i18n/da/libcosmic_yoda.ftl similarity index 100% rename from i18n/da/libcosmic.ftl rename to i18n/da/libcosmic_yoda.ftl diff --git a/i18n/de/libcosmic.ftl b/i18n/de/libcosmic_yoda.ftl similarity index 100% rename from i18n/de/libcosmic.ftl rename to i18n/de/libcosmic_yoda.ftl diff --git a/i18n/el/libcosmic.ftl b/i18n/el/libcosmic_yoda.ftl similarity index 100% rename from i18n/el/libcosmic.ftl rename to i18n/el/libcosmic_yoda.ftl diff --git a/i18n/en-GB/libcosmic.ftl b/i18n/en-GB/libcosmic_yoda.ftl similarity index 100% rename from i18n/en-GB/libcosmic.ftl rename to i18n/en-GB/libcosmic_yoda.ftl diff --git a/i18n/en/libcosmic.ftl b/i18n/en/libcosmic_yoda.ftl similarity index 100% rename from i18n/en/libcosmic.ftl rename to i18n/en/libcosmic_yoda.ftl diff --git a/i18n/eo/libcosmic.ftl b/i18n/eo/libcosmic_yoda.ftl similarity index 100% rename from i18n/eo/libcosmic.ftl rename to i18n/eo/libcosmic_yoda.ftl diff --git a/i18n/es-419/libcosmic.ftl b/i18n/es-419/libcosmic_yoda.ftl similarity index 100% rename from i18n/es-419/libcosmic.ftl rename to i18n/es-419/libcosmic_yoda.ftl diff --git a/i18n/es-MX/libcosmic.ftl b/i18n/es-MX/libcosmic_yoda.ftl similarity index 100% rename from i18n/es-MX/libcosmic.ftl rename to i18n/es-MX/libcosmic_yoda.ftl diff --git a/i18n/es/libcosmic.ftl b/i18n/es/libcosmic_yoda.ftl similarity index 100% rename from i18n/es/libcosmic.ftl rename to i18n/es/libcosmic_yoda.ftl diff --git a/i18n/et/libcosmic.ftl b/i18n/et/libcosmic_yoda.ftl similarity index 100% rename from i18n/et/libcosmic.ftl rename to i18n/et/libcosmic_yoda.ftl diff --git a/i18n/eu/libcosmic.ftl b/i18n/eu/libcosmic_yoda.ftl similarity index 100% rename from i18n/eu/libcosmic.ftl rename to i18n/eu/libcosmic_yoda.ftl diff --git a/i18n/fa/libcosmic.ftl b/i18n/fa/libcosmic_yoda.ftl similarity index 100% rename from i18n/fa/libcosmic.ftl rename to i18n/fa/libcosmic_yoda.ftl diff --git a/i18n/fi/libcosmic.ftl b/i18n/fi/libcosmic_yoda.ftl similarity index 100% rename from i18n/fi/libcosmic.ftl rename to i18n/fi/libcosmic_yoda.ftl diff --git a/i18n/fr/libcosmic.ftl b/i18n/fr/libcosmic_yoda.ftl similarity index 100% rename from i18n/fr/libcosmic.ftl rename to i18n/fr/libcosmic_yoda.ftl diff --git a/i18n/fy/libcosmic.ftl b/i18n/fy/libcosmic_yoda.ftl similarity index 100% rename from i18n/fy/libcosmic.ftl rename to i18n/fy/libcosmic_yoda.ftl diff --git a/i18n/ga/libcosmic.ftl b/i18n/ga/libcosmic_yoda.ftl similarity index 100% rename from i18n/ga/libcosmic.ftl rename to i18n/ga/libcosmic_yoda.ftl diff --git a/i18n/gd/libcosmic.ftl b/i18n/gd/libcosmic_yoda.ftl similarity index 100% rename from i18n/gd/libcosmic.ftl rename to i18n/gd/libcosmic_yoda.ftl diff --git a/i18n/gu/libcosmic.ftl b/i18n/gu/libcosmic_yoda.ftl similarity index 100% rename from i18n/gu/libcosmic.ftl rename to i18n/gu/libcosmic_yoda.ftl diff --git a/i18n/he/libcosmic.ftl b/i18n/he/libcosmic_yoda.ftl similarity index 100% rename from i18n/he/libcosmic.ftl rename to i18n/he/libcosmic_yoda.ftl diff --git a/i18n/hi/libcosmic.ftl b/i18n/hi/libcosmic_yoda.ftl similarity index 100% rename from i18n/hi/libcosmic.ftl rename to i18n/hi/libcosmic_yoda.ftl diff --git a/i18n/hr/libcosmic.ftl b/i18n/hr/libcosmic_yoda.ftl similarity index 100% rename from i18n/hr/libcosmic.ftl rename to i18n/hr/libcosmic_yoda.ftl diff --git a/i18n/hu/libcosmic.ftl b/i18n/hu/libcosmic_yoda.ftl similarity index 100% rename from i18n/hu/libcosmic.ftl rename to i18n/hu/libcosmic_yoda.ftl diff --git a/i18n/id/libcosmic.ftl b/i18n/id/libcosmic_yoda.ftl similarity index 100% rename from i18n/id/libcosmic.ftl rename to i18n/id/libcosmic_yoda.ftl diff --git a/i18n/ie/libcosmic.ftl b/i18n/ie/libcosmic_yoda.ftl similarity index 100% rename from i18n/ie/libcosmic.ftl rename to i18n/ie/libcosmic_yoda.ftl diff --git a/i18n/is/libcosmic.ftl b/i18n/is/libcosmic_yoda.ftl similarity index 100% rename from i18n/is/libcosmic.ftl rename to i18n/is/libcosmic_yoda.ftl diff --git a/i18n/it/libcosmic.ftl b/i18n/it/libcosmic_yoda.ftl similarity index 100% rename from i18n/it/libcosmic.ftl rename to i18n/it/libcosmic_yoda.ftl diff --git a/i18n/ja/libcosmic.ftl b/i18n/ja/libcosmic_yoda.ftl similarity index 100% rename from i18n/ja/libcosmic.ftl rename to i18n/ja/libcosmic_yoda.ftl diff --git a/i18n/jv/libcosmic.ftl b/i18n/jv/libcosmic_yoda.ftl similarity index 100% rename from i18n/jv/libcosmic.ftl rename to i18n/jv/libcosmic_yoda.ftl diff --git a/i18n/ka/libcosmic.ftl b/i18n/ka/libcosmic_yoda.ftl similarity index 100% rename from i18n/ka/libcosmic.ftl rename to i18n/ka/libcosmic_yoda.ftl diff --git a/i18n/kab/libcosmic.ftl b/i18n/kab/libcosmic_yoda.ftl similarity index 100% rename from i18n/kab/libcosmic.ftl rename to i18n/kab/libcosmic_yoda.ftl diff --git a/i18n/kk/libcosmic.ftl b/i18n/kk/libcosmic_yoda.ftl similarity index 100% rename from i18n/kk/libcosmic.ftl rename to i18n/kk/libcosmic_yoda.ftl diff --git a/i18n/kmr/libcosmic.ftl b/i18n/kmr/libcosmic_yoda.ftl similarity index 100% rename from i18n/kmr/libcosmic.ftl rename to i18n/kmr/libcosmic_yoda.ftl diff --git a/i18n/kn/libcosmic.ftl b/i18n/kn/libcosmic_yoda.ftl similarity index 100% rename from i18n/kn/libcosmic.ftl rename to i18n/kn/libcosmic_yoda.ftl diff --git a/i18n/ko/libcosmic.ftl b/i18n/ko/libcosmic_yoda.ftl similarity index 100% rename from i18n/ko/libcosmic.ftl rename to i18n/ko/libcosmic_yoda.ftl diff --git a/i18n/li/libcosmic.ftl b/i18n/li/libcosmic_yoda.ftl similarity index 100% rename from i18n/li/libcosmic.ftl rename to i18n/li/libcosmic_yoda.ftl diff --git a/i18n/lt/libcosmic.ftl b/i18n/lt/libcosmic_yoda.ftl similarity index 100% rename from i18n/lt/libcosmic.ftl rename to i18n/lt/libcosmic_yoda.ftl diff --git a/i18n/ml/libcosmic.ftl b/i18n/ml/libcosmic_yoda.ftl similarity index 100% rename from i18n/ml/libcosmic.ftl rename to i18n/ml/libcosmic_yoda.ftl diff --git a/i18n/ms/libcosmic.ftl b/i18n/ms/libcosmic_yoda.ftl similarity index 100% rename from i18n/ms/libcosmic.ftl rename to i18n/ms/libcosmic_yoda.ftl diff --git a/i18n/nb-NO/libcosmic.ftl b/i18n/nb-NO/libcosmic_yoda.ftl similarity index 100% rename from i18n/nb-NO/libcosmic.ftl rename to i18n/nb-NO/libcosmic_yoda.ftl diff --git a/i18n/nl/libcosmic.ftl b/i18n/nl/libcosmic_yoda.ftl similarity index 100% rename from i18n/nl/libcosmic.ftl rename to i18n/nl/libcosmic_yoda.ftl diff --git a/i18n/nn/libcosmic.ftl b/i18n/nn/libcosmic_yoda.ftl similarity index 100% rename from i18n/nn/libcosmic.ftl rename to i18n/nn/libcosmic_yoda.ftl diff --git a/i18n/oc/libcosmic.ftl b/i18n/oc/libcosmic_yoda.ftl similarity index 100% rename from i18n/oc/libcosmic.ftl rename to i18n/oc/libcosmic_yoda.ftl diff --git a/i18n/pa/libcosmic.ftl b/i18n/pa/libcosmic_yoda.ftl similarity index 100% rename from i18n/pa/libcosmic.ftl rename to i18n/pa/libcosmic_yoda.ftl diff --git a/i18n/pl/libcosmic.ftl b/i18n/pl/libcosmic_yoda.ftl similarity index 100% rename from i18n/pl/libcosmic.ftl rename to i18n/pl/libcosmic_yoda.ftl diff --git a/i18n/pt-BR/libcosmic.ftl b/i18n/pt-BR/libcosmic_yoda.ftl similarity index 100% rename from i18n/pt-BR/libcosmic.ftl rename to i18n/pt-BR/libcosmic_yoda.ftl diff --git a/i18n/pt/libcosmic.ftl b/i18n/pt/libcosmic_yoda.ftl similarity index 100% rename from i18n/pt/libcosmic.ftl rename to i18n/pt/libcosmic_yoda.ftl diff --git a/i18n/ro/libcosmic.ftl b/i18n/ro/libcosmic_yoda.ftl similarity index 100% rename from i18n/ro/libcosmic.ftl rename to i18n/ro/libcosmic_yoda.ftl diff --git a/i18n/ru/libcosmic.ftl b/i18n/ru/libcosmic_yoda.ftl similarity index 100% rename from i18n/ru/libcosmic.ftl rename to i18n/ru/libcosmic_yoda.ftl diff --git a/i18n/sk/libcosmic.ftl b/i18n/sk/libcosmic_yoda.ftl similarity index 100% rename from i18n/sk/libcosmic.ftl rename to i18n/sk/libcosmic_yoda.ftl diff --git a/i18n/sl/libcosmic.ftl b/i18n/sl/libcosmic_yoda.ftl similarity index 100% rename from i18n/sl/libcosmic.ftl rename to i18n/sl/libcosmic_yoda.ftl diff --git a/i18n/sr-Cyrl/libcosmic.ftl b/i18n/sr-Cyrl/libcosmic_yoda.ftl similarity index 100% rename from i18n/sr-Cyrl/libcosmic.ftl rename to i18n/sr-Cyrl/libcosmic_yoda.ftl diff --git a/i18n/sr-Latn/libcosmic.ftl b/i18n/sr-Latn/libcosmic_yoda.ftl similarity index 100% rename from i18n/sr-Latn/libcosmic.ftl rename to i18n/sr-Latn/libcosmic_yoda.ftl diff --git a/i18n/sr/libcosmic.ftl b/i18n/sr/libcosmic_yoda.ftl similarity index 100% rename from i18n/sr/libcosmic.ftl rename to i18n/sr/libcosmic_yoda.ftl diff --git a/i18n/sv/libcosmic.ftl b/i18n/sv/libcosmic_yoda.ftl similarity index 100% rename from i18n/sv/libcosmic.ftl rename to i18n/sv/libcosmic_yoda.ftl diff --git a/i18n/ta/libcosmic.ftl b/i18n/ta/libcosmic_yoda.ftl similarity index 100% rename from i18n/ta/libcosmic.ftl rename to i18n/ta/libcosmic_yoda.ftl diff --git a/i18n/th/libcosmic.ftl b/i18n/th/libcosmic_yoda.ftl similarity index 100% rename from i18n/th/libcosmic.ftl rename to i18n/th/libcosmic_yoda.ftl diff --git a/i18n/ti/libcosmic.ftl b/i18n/ti/libcosmic_yoda.ftl similarity index 100% rename from i18n/ti/libcosmic.ftl rename to i18n/ti/libcosmic_yoda.ftl diff --git a/i18n/tr/libcosmic.ftl b/i18n/tr/libcosmic_yoda.ftl similarity index 100% rename from i18n/tr/libcosmic.ftl rename to i18n/tr/libcosmic_yoda.ftl diff --git a/i18n/uk/libcosmic.ftl b/i18n/uk/libcosmic_yoda.ftl similarity index 100% rename from i18n/uk/libcosmic.ftl rename to i18n/uk/libcosmic_yoda.ftl diff --git a/i18n/uz/libcosmic.ftl b/i18n/uz/libcosmic_yoda.ftl similarity index 100% rename from i18n/uz/libcosmic.ftl rename to i18n/uz/libcosmic_yoda.ftl diff --git a/i18n/vi/libcosmic.ftl b/i18n/vi/libcosmic_yoda.ftl similarity index 100% rename from i18n/vi/libcosmic.ftl rename to i18n/vi/libcosmic_yoda.ftl diff --git a/i18n/yue-Hant/libcosmic.ftl b/i18n/yue-Hant/libcosmic_yoda.ftl similarity index 100% rename from i18n/yue-Hant/libcosmic.ftl rename to i18n/yue-Hant/libcosmic_yoda.ftl diff --git a/i18n/zh-Hans/libcosmic.ftl b/i18n/zh-Hans/libcosmic_yoda.ftl similarity index 100% rename from i18n/zh-Hans/libcosmic.ftl rename to i18n/zh-Hans/libcosmic_yoda.ftl diff --git a/i18n/zh-Hant/libcosmic.ftl b/i18n/zh-Hant/libcosmic_yoda.ftl similarity index 100% rename from i18n/zh-Hant/libcosmic.ftl rename to i18n/zh-Hant/libcosmic_yoda.ftl diff --git a/src/action.rs b/src/action.rs index b7162896..19e228b8 100644 --- a/src/action.rs +++ b/src/action.rs @@ -1,7 +1,6 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -#[cfg(feature = "winit")] use crate::app; #[cfg(feature = "single-instance")] use crate::dbus_activation; @@ -9,7 +8,6 @@ use crate::dbus_activation; pub const fn app(message: M) -> Action { Action::App(message) } -#[cfg(feature = "winit")] pub const fn cosmic(message: app::Action) -> Action { Action::Cosmic(message) } @@ -23,7 +21,6 @@ pub const fn none() -> Action { pub enum Action { /// Messages from the application, for the application. App(M), - #[cfg(feature = "winit")] /// Internal messages to be handled by libcosmic. Cosmic(app::Action), #[cfg(feature = "single-instance")] diff --git a/src/command.rs b/src/command.rs index 6bb16e8d..fee60ec5 100644 --- a/src/command.rs +++ b/src/command.rs @@ -27,12 +27,10 @@ pub fn set_title(id: window::Id, title: String) -> iced::Task(factor: f32) -> iced::Task> { iced::Task::done(crate::app::Action::ScaleFactor(factor)).map(crate::Action::Cosmic) } -#[cfg(feature = "winit")] pub fn set_theme(theme: crate::Theme) -> iced::Task> { iced::Task::done(crate::app::Action::AppThemeChange(theme)).map(crate::Action::Cosmic) } diff --git a/src/core.rs b/src/core.rs index 0ca555e3..fbb98fbe 100644 --- a/src/core.rs +++ b/src/core.rs @@ -461,7 +461,6 @@ impl Core { id } - #[cfg(feature = "winit")] pub fn drag(&self, id: Option) -> crate::app::Task { let Some(id) = id.or(self.main_window) else { return iced::Task::none(); @@ -469,7 +468,6 @@ impl Core { crate::command::drag(id) } - #[cfg(feature = "winit")] pub fn maximize( &self, id: Option, @@ -481,7 +479,6 @@ impl Core { crate::command::maximize(id, maximized) } - #[cfg(feature = "winit")] pub fn minimize(&self, id: Option) -> crate::app::Task { let Some(id) = id.or(self.main_window) else { return iced::Task::none(); @@ -489,7 +486,6 @@ impl Core { crate::command::minimize(id) } - #[cfg(feature = "winit")] pub fn set_title( &self, id: Option, @@ -501,7 +497,6 @@ impl Core { crate::command::set_title(id, title) } - #[cfg(feature = "winit")] pub fn set_windowed(&self, id: Option) -> crate::app::Task { let Some(id) = id.or(self.main_window) else { return iced::Task::none(); @@ -509,7 +504,6 @@ impl Core { crate::command::set_windowed(id) } - #[cfg(feature = "winit")] pub fn toggle_maximize( &self, id: Option, diff --git a/src/lib.rs b/src/lib.rs index c1646e23..2dcbe67e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -90,7 +90,6 @@ /// Recommended default imports. pub mod prelude { - #[cfg(feature = "winit")] pub use crate::ApplicationExt; pub use crate::ext::*; pub use crate::{Also, Apply, Element, Renderer, Task, Theme}; @@ -104,9 +103,7 @@ pub use action::Action; pub mod anim; -#[cfg(feature = "winit")] pub mod app; -#[cfg(feature = "winit")] #[doc(inline)] pub use app::{Application, ApplicationExt}; diff --git a/src/surface/action.rs b/src/surface/action.rs index 493c9ad0..6de5af71 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -2,13 +2,12 @@ // SPDX-License-Identifier: MPL-2.0 use super::Action; -#[cfg(feature = "winit")] use crate::Application; use iced::{Rectangle, window}; -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] use iced_runtime::platform_specific::wayland::CornerRadius; -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] use iced_runtime::platform_specific::wayland::layer_surface::IcedMargin; use std::any::Any; use std::sync::Arc; @@ -40,17 +39,17 @@ pub fn destroy_layer_shell(id: iced_core::window::Id) -> Action { #[derive(Debug, Default, Copy, Clone)] pub struct LiveSettings { - #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] /// Override the surface padding value for the surface type. pub padding: Option, - #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] /// Override the default corner radius value for the surface type. pub corners: Option, /// Override the default blur setting for the surface type. pub blur: Option, } -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] type BoxedView = Option< Box< dyn Fn(&App) -> crate::Element<'_, crate::Action<::Message>> @@ -60,7 +59,7 @@ type BoxedView = Option< >, >; -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn app_window( live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static, @@ -92,7 +91,7 @@ pub fn app_window( } /// Used to create a window message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn simple_window( live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, @@ -129,7 +128,7 @@ pub fn simple_window( ) } -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn app_popup( live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static, @@ -162,7 +161,7 @@ pub fn app_popup( } /// Used to create a subsurface message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn simple_subsurface( settings: impl Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings @@ -192,7 +191,7 @@ pub fn simple_subsurface( } /// Used to create a popup message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn simple_popup( live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, @@ -228,7 +227,7 @@ pub fn simple_popup( ) } -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn subsurface( settings: impl Fn( diff --git a/src/theme/style/mod.rs b/src/theme/style/mod.rs index bc648a73..cc489312 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", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] pub mod tooltip; -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] pub use tooltip::Tooltip; diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 4f9249dd..3bebee35 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -597,8 +597,10 @@ where let bounds = canvas_layout.bounds(); // Draw the handle on the saturation value canvas - let t = THEME.lock().unwrap().clone(); - let t = t.cosmic(); + // Yoda: use the Theme passed into draw() instead of locking the global + // THEME Mutex and cloning the whole Theme. Fires on every color-picker + // redraw, so saving the Mutex lock + full Theme clone adds up. + let t = theme.cosmic(); let handle_radius = f32::from(t.space_xs()) / 2.0; let (x, y) = ( self.active_color diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 4b31fc01..7bc18250 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -6,7 +6,6 @@ #[cfg(all( feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; @@ -67,7 +66,6 @@ impl ContextMenu<'_, Message> { #[cfg(all( feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] #[allow(clippy::too_many_lines)] @@ -394,7 +392,6 @@ impl Widget #[cfg(all( feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) @@ -438,7 +435,6 @@ impl Widget #[cfg(all( feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { @@ -460,7 +456,6 @@ impl Widget #[cfg(all( feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) @@ -499,7 +494,6 @@ impl Widget #[cfg(all( feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) diff --git a/src/widget/dropdown/mod.rs b/src/widget/dropdown/mod.rs index b5fd4c06..0ea3a212 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", target_os = "linux"))] + #[cfg(all(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 79f854fb..3aa4660b 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", target_os = "linux"))] + #[cfg(all(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", target_os = "linux"))] + #[cfg(all(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", target_os = "linux"))] + #[cfg(all(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", target_os = "linux"))] + #[cfg(all(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", target_os = "linux"))] + #[cfg(all(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", target_os = "linux"))] + #[cfg(all(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", target_os = "linux"))] + #[cfg(all(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", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] if let Some(((on_surface_action, parent), action_map)) = on_surface_action .as_ref() .zip(_window_id) @@ -661,7 +661,7 @@ pub fn update< state.close_operation = false; state.is_open.store(false, Ordering::SeqCst); if is_open { - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(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))); } @@ -684,7 +684,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", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] if let Some(on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } @@ -729,7 +729,7 @@ pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::In } } -#[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] +#[cfg(all(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 fce272e1..1715ae13 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -13,7 +13,6 @@ use crate::Renderer; feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; @@ -194,7 +193,6 @@ pub struct MenuBar { #[cfg(all( feature = "multi-window", feature = "wayland", - feature = "winit", target_os = "linux" ))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -234,7 +232,6 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", - feature = "winit", target_os = "linux" ))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), @@ -333,7 +330,6 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", - feature = "winit", target_os = "linux" ))] pub fn with_positioner( @@ -371,7 +367,6 @@ where feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] #[allow(clippy::too_many_lines)] @@ -660,7 +655,6 @@ where #[cfg(all( feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] { @@ -684,7 +678,6 @@ where feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { @@ -699,7 +692,6 @@ where feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { @@ -796,7 +788,6 @@ where feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 53c87415..6e17df52 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -10,7 +10,6 @@ use super::menu_tree::MenuTree; feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; @@ -681,7 +680,6 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) @@ -993,7 +991,6 @@ impl Widget( feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] pub(super) fn init_root_popup_menu( @@ -1572,7 +1568,7 @@ where .as_ref() .is_some_and(|i| *i != new_index && !active_menu[*i].children.is_empty()); - #[cfg(all(feature = "multi-window", feature = "wayland",target_os = "linux", feature = "winit", feature = "surface-message"))] + #[cfg(all(feature = "multi-window", feature = "wayland", target_os = "linux", 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 331d6c21..e68e7477 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -312,7 +312,7 @@ pub use toggler::{Toggler, toggler}; #[doc(inline)] pub use tooltip::{Tooltip, tooltip}; -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] pub mod wayland; pub mod tooltip { diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index 2cae68d8..de323c13 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -23,7 +23,7 @@ impl Default for ResponsiveMenuBar { fn default() -> ResponsiveMenuBar { ResponsiveMenuBar { collapsed_item_width: { - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] if matches!( crate::app::cosmic::WINDOWING_SYSTEM.get(), Some(crate::app::cosmic::WindowingSystem::Wayland) @@ -32,7 +32,7 @@ impl Default for ResponsiveMenuBar { } else { ItemWidth::Static(84) } - #[cfg(not(all(feature = "winit", feature = "wayland", target_os = "linux")))] + #[cfg(not(all(feature = "wayland", target_os = "linux")))] { ItemWidth::Static(84) } From 695c602ba13914260028fe158df2377f5b0f8321 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Mon, 25 May 2026 13:02:25 +0200 Subject: [PATCH 121/201] yoda: bump iced submodule (Wayland-only + softbuffer cuts) (squashed) Squash of 3 yoda commits: - c118f5a2 yoda: bump iced submodule ref -> yoda-wayland-only HEAD 8f6be798 - 388e0655 yoda: bump iced submodule -> softbuffer + window_clipboard cuts - 9b2a3643 Update iced warning fixes --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index c781ff61..d9fe129f 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit c781ff6199715f5650411c7ac625d4e1929bfdfb +Subproject commit d9fe129f7316cea3ef6d2576e9421a305759b8bf From 7c0b3ff1b707f45f56d7c36e6bd823f9091ad201 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Tue, 5 May 2026 09:29:47 +0200 Subject: [PATCH 122/201] feat(cosmic-theme): add apply_gtk_decoration_layout helper MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Writes the gtk-decoration-layout key to ~/.config/gtk-{3,4}.0/settings.ini and best-effort updates GNOME's button-layout GSettings key for apps that still consult it. Factors out write_gtk_settings_key for reuse. Leyoda 2026 – GPLv3 --- cosmic-theme/src/output/gtk4_output.rs | 77 +++++++++++++++++++++++--- 1 file changed, 68 insertions(+), 9 deletions(-) diff --git a/cosmic-theme/src/output/gtk4_output.rs b/cosmic-theme/src/output/gtk4_output.rs index 16a3c36e..bbb4f246 100644 --- a/cosmic-theme/src/output/gtk4_output.rs +++ b/cosmic-theme/src/output/gtk4_output.rs @@ -1,12 +1,13 @@ -use crate::composite::over; -use crate::steps::steps; -use crate::{Component, Theme}; -use palette::rgb::Rgba; -use palette::{Darken, IntoColor, Lighten, Srgba, WithAlpha}; -use std::fs::{self, File}; -use std::io::{self, Write}; -use std::num::NonZeroUsize; -use std::path::Path; +use crate::{Component, Theme, composite::over, steps::steps}; +use configparser::ini::Ini; +use palette::{Darken, IntoColor, Lighten, Srgba, WithAlpha, rgb::Rgba}; +use std::{ + fs::{self, File}, + io::{self, Write}, + num::NonZeroUsize, + path::Path, + process::Command, +}; use super::{OutputError, to_rgba}; @@ -218,6 +219,50 @@ impl Theme { Ok(()) } + /// Apply the preferred GTK client-side decoration button layout. + /// + /// This writes the GTK 3/4 `settings.ini` value used by GTK header bars and + /// also best-effort updates GNOME's `button-layout` GSettings key for apps + /// that still consult it. + /// + /// # Errors + /// + /// Returns an `OutputError` if the GTK settings files cannot be written. + #[cold] + pub fn apply_gtk_decoration_layout(buttons_at_start: bool) -> Result<(), OutputError> { + let Some(config_dir) = dirs::config_dir() else { + return Err(OutputError::MissingConfigDir); + }; + + let layout = if buttons_at_start { + "close,minimize,maximize:" + } else { + ":minimize,maximize,close" + }; + + for gtk_version in ["gtk-3.0", "gtk-4.0"] { + let gtk_dir = config_dir.join(gtk_version); + fs::create_dir_all(>k_dir).map_err(OutputError::Io)?; + Self::write_gtk_settings_key( + >k_dir.join("settings.ini"), + "gtk-decoration-layout", + layout, + )?; + } + + // best-effort: gsettings is absent on non-GNOME systems + let _ = Command::new("gsettings") + .args([ + "set", + "org.gnome.desktop.wm.preferences", + "button-layout", + layout, + ]) + .status(); + + Ok(()) + } + /// Reset the applied gtk css /// /// # Errors @@ -257,6 +302,20 @@ impl Theme { Ok(()) } + #[cold] + fn write_gtk_settings_key(path: &Path, key: &str, value: &str) -> Result<(), OutputError> { + let mut ini = Ini::new_cs(); + + if path.exists() { + let file_content = fs::read_to_string(path).map_err(OutputError::Io)?; + ini.read(file_content).map_err(OutputError::Ini)?; + } + + ini.setstr("Settings", key, Some(value)); + ini.pretty_write(path, &super::qt_settings_ini_style()) + .map_err(OutputError::Io) + } + fn is_cosmic_css(path: &Path, cosmic_css: &Path) -> io::Result> { if !path.exists() { return Ok(None); From c85efc6180fa6b450fd444230469230e0bd2a26a Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Mon, 25 May 2026 13:02:25 +0200 Subject: [PATCH 123/201] yoda: bump iced submodule + cargo auto-fix sweeps (squashed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squash of 6 yoda commits: - 282813c8 yoda: bump iced -> window_clipboard via public Forgejo fork - cdf34938 yoda: cargo fix --lib (libcosmic-yoda) — drop 99 trivial warnings - 38a988cb yoda: cargo fix on cosmic-config + bump iced auto-fix commit - 301bbf6e yoda: bump iced submodule -> iced_winit warning cleanup (0 left) - b94c03d9 yoda: bump iced submodule -> iced_widget cleanup (0 left) - a9492d76 yoda: bump iced submodule -> all iced crates at 0 warnings --- src/app/cosmic.rs | 4 ++-- src/app/mod.rs | 2 +- src/ext.rs | 1 - src/scroll.rs | 3 +-- src/theme/style/segmented_button.rs | 4 ++-- src/widget/button/widget.rs | 8 ++++---- src/widget/cards.rs | 2 +- src/widget/color_picker/mod.rs | 2 +- src/widget/context_menu.rs | 4 ++-- src/widget/dropdown/menu/mod.rs | 11 +++++------ src/widget/dropdown/multi/menu.rs | 2 +- src/widget/dropdown/multi/widget.rs | 7 +++---- src/widget/dropdown/operation.rs | 3 --- src/widget/dropdown/widget.rs | 15 +++++++-------- src/widget/flex_row/widget.rs | 2 +- src/widget/grid/widget.rs | 2 +- src/widget/icon/bundle.rs | 2 +- src/widget/mod.rs | 2 -- src/widget/text_input/input.rs | 18 +++++++++--------- 19 files changed, 42 insertions(+), 52 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 2d5b10d3..6de7f35b 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -546,7 +546,7 @@ where f64::from(self.app.core().scale_factor()) } - pub fn style(&self, theme: &Theme) -> theme::Style { + pub fn style(&self, _theme: &Theme) -> theme::Style { if let Some(style) = self.app.style() { style } else if self.app.core().window.is_maximized { @@ -761,7 +761,7 @@ impl Cosmic { #[allow(clippy::too_many_lines)] fn cosmic_update(&mut self, message: Action) -> iced::Task> { match message { - Action::WindowMaximized(id, maximized) => { + Action::WindowMaximized(_id, _maximized) => { #[cfg(not(all(feature = "wayland", target_os = "linux")))] if self .app diff --git a/src/app/mod.rs b/src/app/mod.rs index f757d5be..1ed17855 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -136,7 +136,7 @@ pub fn run(settings: Settings, flags: App::Flags) -> iced::Res crate::malloc::limit_mmap_threshold(threshold); } - let default_font = settings.default_font; + let _default_font = settings.default_font; let (settings, (mut core, flags), window_settings) = iced_settings::(settings, flags); #[cfg(not(feature = "multi-window"))] { diff --git a/src/ext.rs b/src/ext.rs index 8eb749e5..65ca0145 100644 --- a/src/ext.rs +++ b/src/ext.rs @@ -2,7 +2,6 @@ // SPDX-License-Identifier: MPL-2.0 use iced::Color; -use iced_core::Widget; pub trait ElementExt { #[must_use] diff --git a/src/scroll.rs b/src/scroll.rs index b6d42378..67947399 100644 --- a/src/scroll.rs +++ b/src/scroll.rs @@ -1,4 +1,3 @@ -use iced::Task; use iced::mouse::ScrollDelta; use std::time::{Duration, Instant}; @@ -95,7 +94,7 @@ impl Scroll { } else { // Return integer part of scroll, and keep remainder self.scroll = Some((scroll.fract(), Instant::now())); - let mut discrete = scroll.trunc() as isize; + let discrete = scroll.trunc() as isize; if discrete != 0 { self.last_discrete = Some(Instant::now()); } diff --git a/src/theme/style/segmented_button.rs b/src/theme/style/segmented_button.rs index 381c4a05..45fcc0b8 100644 --- a/src/theme/style/segmented_button.rs +++ b/src/theme/style/segmented_button.rs @@ -185,7 +185,7 @@ mod horizontal { pub fn selection_active( cosmic: &cosmic_theme::Theme, - component: &Component, + _component: &Component, ) -> ItemStatusAppearance { let rad_xl = cosmic.corner_radii.radius_xl; let rad_0 = cosmic.corner_radii.radius_0; @@ -280,7 +280,7 @@ mod vertical { pub fn selection_active( cosmic: &cosmic_theme::Theme, - component: &Component, + _component: &Component, ) -> ItemStatusAppearance { let rad_0 = cosmic.corner_radii.radius_0; let rad_xl = cosmic.corner_radii.radius_xl; diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index a9d44b2b..ebdf2d66 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -10,7 +10,7 @@ use iced::Alignment; use iced_runtime::core::widget::Id; use iced_runtime::{Action, Task, keyboard, task}; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::renderer::{self, Quad, Renderer}; use iced_core::widget::Operation; use iced_core::widget::tree::{self, Tree}; @@ -681,7 +681,7 @@ impl<'a, Message: 'a + Clone> Widget height, } = layout.bounds(); 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 _is_hovered = state.state.downcast_ref::().is_hovered; let mut node = Node::new(Role::Button); node.add_action(Action::Focus); @@ -841,7 +841,7 @@ pub fn update<'a, Message: Clone>( } } #[cfg(feature = "a11y")] - Event::A11y(event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => { + Event::A11y(_event_id, iced_accessibility::accesskit::ActionRequest { action, .. }) => { let state = state(); if let Some(on_press) = matches!(action, iced_accessibility::accesskit::Action::Click) .then_some(on_press) @@ -884,7 +884,7 @@ pub fn draw( viewport_bounds: Rectangle, styling: &super::style::Style, draw_contents: impl FnOnce(&mut Renderer, &Style), - is_image: bool, + _is_image: bool, ) where Theme: super::style::Catalog, { diff --git a/src/widget/cards.rs b/src/widget/cards.rs index 43325002..afdd1fe6 100644 --- a/src/widget/cards.rs +++ b/src/widget/cards.rs @@ -93,7 +93,7 @@ where /// Get an expandable stack of cards #[allow(clippy::too_many_arguments)] pub fn new( - id: widget::Id, + _id: widget::Id, card_inner_elements: Vec>, on_clear_all: Message, on_show_more: Option, diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 3bebee35..ce733655 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -16,7 +16,7 @@ use crate::widget::segmented_button::Entity; use crate::widget::{container, slider}; use derive_setters::Setters; use iced::Task; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::gradient::{ColorStop, Linear}; use iced_core::renderer::Quad; use iced_core::widget::{Tree, tree}; diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 7bc18250..0d3ba1fb 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -91,7 +91,7 @@ impl ContextMenu<'_, Message> { 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| { + 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(); @@ -149,7 +149,7 @@ impl ContextMenu<'_, Message> { layout.bounds(), -bounds.height, ); - let (anchor_rect, gravity) = my_state.menu_bar_state.inner.with_data_mut(|state| { + 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); diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index 0c96c1c6..bb3cead1 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -8,9 +8,8 @@ use std::sync::{Arc, Mutex}; pub use appearance::{Appearance, StyleSheet}; -use crate::surface; use crate::widget::{Container, RcWrapper, icon}; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::layout::{self, Layout}; use iced_core::text::{self, Text}; use iced_core::widget::Tree; @@ -391,7 +390,7 @@ impl<'a, Message: Clone + 'a> crate::widget::Widget, - cursor: mouse::Cursor, + _cursor: mouse::Cursor, viewport: &Rectangle, ) { let appearance = theme.appearance(&()); diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index 0a761097..a1da9da7 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -2,7 +2,7 @@ use super::Model; pub use crate::widget::dropdown::menu::{Appearance, StyleSheet}; use crate::widget::Container; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::layout::{self, Layout}; use iced_core::text::{self, Text}; use iced_core::widget::Tree; diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index 3c01f5f8..90c6ab71 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -5,7 +5,7 @@ use super::menu::{self, Menu}; use crate::widget::icon; use derive_setters::Setters; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::text::{self, Paragraph, Text}; use iced_core::widget::tree::{self, Tree}; use iced_core::{ @@ -13,7 +13,6 @@ use iced_core::{ alignment, keyboard, layout, mouse, overlay, renderer, svg, touch, }; use iced_widget::pick_list; -use std::ffi::OsStr; pub use iced_widget::pick_list::{Catalog, Style}; @@ -253,7 +252,7 @@ impl Default for State { /// Computes the layout of a [`Dropdown`]. #[allow(clippy::too_many_arguments)] pub fn layout( - renderer: &crate::Renderer, + _renderer: &crate::Renderer, limits: &layout::Limits, width: Length, gap: f32, @@ -376,7 +375,7 @@ pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::In #[allow(clippy::too_many_arguments)] pub fn overlay<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static>( layout: Layout<'_>, - renderer: &crate::Renderer, + _renderer: &crate::Renderer, state: &'a mut State, gap: f32, padding: Padding, diff --git a/src/widget/dropdown/operation.rs b/src/widget/dropdown/operation.rs index 1a4e1a9f..4cd266d4 100644 --- a/src/widget/dropdown/operation.rs +++ b/src/widget/dropdown/operation.rs @@ -2,9 +2,6 @@ // 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); diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 3aa4660b..20a0e70e 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -8,7 +8,7 @@ use crate::widget::icon::{self, Handle}; use crate::{Element, surface}; use derive_setters::Setters; use iced::window; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::text::{self, Paragraph, Text}; use iced_core::widget::tree::{self, Tree}; use iced_core::{ @@ -17,7 +17,6 @@ use iced_core::{ }; use iced_widget::pick_list::{self, Catalog}; use std::borrow::Cow; -use std::ffi::OsStr; use std::hash::{DefaultHasher, Hash, Hasher}; use std::sync::atomic::{AtomicBool, Ordering}; use std::sync::{Arc, LazyLock, Mutex}; @@ -328,10 +327,10 @@ where fn operate( &mut self, - tree: &mut Tree, + _tree: &mut Tree, _layout: Layout<'_>, _renderer: &crate::Renderer, - operation: &mut dyn iced_core::widget::Operation, + _operation: &mut dyn iced_core::widget::Operation, ) { // TODO: double check operation handling // let state = tree.state.downcast_mut::(); @@ -343,7 +342,7 @@ where tree: &'b mut Tree, layout: Layout<'b>, renderer: &crate::Renderer, - viewport: &Rectangle, + _viewport: &Rectangle, translation: Vector, ) -> Option> { #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -452,7 +451,7 @@ impl super::operation::Dropdown for State { /// Computes the layout of a [`Dropdown`]. #[allow(clippy::too_many_arguments)] pub fn layout( - renderer: &crate::Renderer, + _renderer: &crate::Renderer, limits: &layout::Limits, width: Length, gap: f32, @@ -546,7 +545,7 @@ pub fn update< cursor: mouse::Cursor, shell: &mut Shell<'_, Message>, #[cfg(all(feature = "wayland", target_os = "linux"))] - positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, + _positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, on_selected: Arc Message + Send + Sync + 'static>, selected: Option, selections: &[S], @@ -558,7 +557,7 @@ pub fn update< gap: f32, padding: Padding, text_size: Option, - font: Option, + _font: Option, selected_option: Option, ) { let state = state(); diff --git a/src/widget/flex_row/widget.rs b/src/widget/flex_row/widget.rs index 0b2e6e13..e5c0b615 100644 --- a/src/widget/flex_row/widget.rs +++ b/src/widget/flex_row/widget.rs @@ -3,7 +3,7 @@ use crate::{Element, Renderer}; use derive_setters::Setters; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::{Operation, Tree}; use iced_core::{ Clipboard, Layout, Length, Padding, Rectangle, Shell, Vector, Widget, layout, mouse, overlay, diff --git a/src/widget/grid/widget.rs b/src/widget/grid/widget.rs index e59ba90d..55ce3c91 100644 --- a/src/widget/grid/widget.rs +++ b/src/widget/grid/widget.rs @@ -3,7 +3,7 @@ use crate::{Element, Renderer}; use derive_setters::Setters; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::{Operation, Tree}; use iced_core::{ Alignment, Clipboard, Layout, Length, Padding, Rectangle, Shell, Vector, Widget, layout, mouse, diff --git a/src/widget/icon/bundle.rs b/src/widget/icon/bundle.rs index bb6ce244..30a9938a 100644 --- a/src/widget/icon/bundle.rs +++ b/src/widget/icon/bundle.rs @@ -5,7 +5,7 @@ /// Icon bundling is not enabled on unix platforms. #[cfg(all(unix, not(target_os = "macos")))] -pub fn get(icon_name: &str) -> Option { +pub fn get(_icon_name: &str) -> Option { None } diff --git a/src/widget/mod.rs b/src/widget/mod.rs index e68e7477..5764ab44 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -119,8 +119,6 @@ pub mod calendar; pub use calendar::{Calendar, calendar}; pub mod card; -#[doc(inline)] -pub use card::*; pub mod color_picker; #[doc(inline)] diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 65b84882..a8b668c0 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -21,12 +21,12 @@ use apply::Apply; 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::event::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, Affinity, Paragraph, Renderer, Text}; +use iced_core::text::{self, Paragraph, Renderer, Text}; use iced_core::time::{Duration, Instant}; use iced_core::widget::Id; use iced_core::widget::operation::{self, Operation}; @@ -825,7 +825,7 @@ where &mut self, tree: &mut Tree, layout: Layout<'_>, - renderer: &crate::Renderer, + _renderer: &crate::Renderer, operation: &mut dyn Operation, ) { operation.container(Some(&self.id), layout.bounds()); @@ -917,7 +917,7 @@ 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().update( + let _res = trailing_icon.as_widget_mut().update( tree, event, trailing_layout, @@ -2229,7 +2229,7 @@ pub fn update<'a, Message: Clone + 'static>( )) if *rectangle == Some(dnd_id) => { cold(); let state = state(); - let is_clicked = text_layout.bounds().contains(Point { + let _is_clicked = text_layout.bounds().contains(Point { x: *x as f32, y: *y as f32, }); @@ -2237,7 +2237,7 @@ pub fn update<'a, Message: Clone + 'static>( let mut accepted = false; for m in mime_types { if SUPPORTED_TEXT_MIME_TYPES.contains(&m.as_str()) { - let clone = m.clone(); + let _clone = m.clone(); accepted = true; } } @@ -2300,7 +2300,7 @@ pub fn update<'a, Message: Clone + 'static>( cold(); let state = state(); if let DndOfferState::HandlingOffer(mime_types, _action) = state.dnd_offer.clone() { - let Some(mime_type) = SUPPORTED_TEXT_MIME_TYPES + let Some(_mime_type) = SUPPORTED_TEXT_MIME_TYPES .iter() .find(|&&m| mime_types.iter().any(|t| t == m)) else { @@ -2317,7 +2317,7 @@ pub fn update<'a, Message: Clone + 'static>( Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != *id => {} #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer( - rectangle, + _rectangle, OfferEvent::Leave | OfferEvent::LeaveDestination, )) => { cold(); @@ -2620,7 +2620,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 (cursors, offset, is_selecting) = 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 { From 4e1080108c96a3e9e9fa0fb194b3147f5b446161 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Mon, 25 May 2026 13:02:38 +0200 Subject: [PATCH 124/201] yoda: warning cleanup sweep (dead code + clippy --fix) (squashed) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Squash of 4 yoda commits: - 84437e21 yoda: libcosmic-yoda dead-code purge (14->0 warnings) - 999db0a4 yoda: cosmic-theme cleanup (4->0 warnings) — workspace at 0 warnings total - 4743bb8e yoda: cargo clippy --fix on libcosmic-yoda (115->33 warnings) - 675f3b59 chore: reduce local stack warnings --- Cargo.toml | 8 ++++ cosmic-theme/Cargo.toml | 3 -- cosmic-theme/src/model/theme.rs | 6 +++ cosmic-theme/src/output/vs_code.rs | 15 +++++++ src/app/mod.rs | 2 +- src/applet/row.rs | 3 +- src/config/mod.rs | 5 +-- src/core.rs | 3 -- src/dbus_activation.rs | 2 +- src/desktop.rs | 12 +++-- src/lib.rs | 11 +++++ src/theme/style/iced.rs | 11 ++--- src/widget/button/icon.rs | 8 ++-- src/widget/button/image.rs | 8 ++-- src/widget/button/link.rs | 8 ++-- src/widget/button/text.rs | 8 ++-- src/widget/button/widget.rs | 23 +++------- src/widget/cards.rs | 4 +- src/widget/color_picker/mod.rs | 29 ++++++------ src/widget/dnd_destination.rs | 9 +--- src/widget/dropdown/menu/mod.rs | 12 ++--- src/widget/dropdown/multi/menu.rs | 6 +-- src/widget/dropdown/multi/widget.rs | 4 +- src/widget/dropdown/widget.rs | 2 +- src/widget/menu/flex.rs | 7 +-- src/widget/menu/menu_bar.rs | 18 ++------ src/widget/menu/menu_inner.rs | 5 +-- src/widget/popover.rs | 11 +---- src/widget/radio.rs | 1 - src/widget/segmented_button/widget.rs | 40 +++++++---------- src/widget/spin_button.rs | 18 +++++--- src/widget/table/widget/standard.rs | 7 ++- src/widget/text_input/input.rs | 64 +++++++++------------------ src/widget/text_input/value.rs | 4 +- src/widget/toggler.rs | 6 +-- 35 files changed, 173 insertions(+), 210 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 8aa2c74c..fb751df4 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -270,5 +270,13 @@ tracing = "0.1" tokio = "1.52" zbus = { version = "5.15", default-features = false } +# Speed up snapshot diffing in cosmic-theme tests. Cargo silently ignores +# [profile.*] blocks in non-root manifests, so this lives at the +# workspace root. +[profile.dev.package.insta] +opt-level = 3 +[profile.dev.package.similar] +opt-level = 3 + [dev-dependencies] tempfile = "3.27.0" diff --git a/cosmic-theme/Cargo.toml b/cosmic-theme/Cargo.toml index 82ff9fd3..2907441f 100644 --- a/cosmic-theme/Cargo.toml +++ b/cosmic-theme/Cargo.toml @@ -35,6 +35,3 @@ thiserror.workspace = true [dev-dependencies] insta = "1.47.2" -[profile.dev.package] -insta.opt-level = 3 -similar.opt-level = 3 diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 9a80d31a..64f81f8a 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -1062,6 +1062,12 @@ impl ThemeBuilder { } #[allow(clippy::too_many_lines)] + // The component_hovered/pressed_overlay vars are seeded once near the + // top of this fn and then reassigned inside each container block + // (primary, secondary, …) before being read again. The initial seed + // is therefore overwritten before any read, which is what the + // unused_assignments lint flags below. + #[allow(unused_assignments)] /// build the theme pub fn build(self) -> Theme { let Self { diff --git a/cosmic-theme/src/output/vs_code.rs b/cosmic-theme/src/output/vs_code.rs index 43c36bb6..f49c888f 100644 --- a/cosmic-theme/src/output/vs_code.rs +++ b/cosmic-theme/src/output/vs_code.rs @@ -266,6 +266,14 @@ impl From for VsTheme { } impl Theme { + /// Write this theme to VS Code's `settings.json` as a + /// `workbench.colorCustomizations` entry, and enable + /// `window.autoDetectColorScheme` so VS Code follows the system theme. + /// + /// # Errors + /// + /// Returns an `OutputError` if the user config dir is missing, the + /// settings file cannot be read/written, or its JSON is invalid. #[cold] pub fn apply_vs_code(self) -> Result<(), OutputError> { let vs_theme = VsTheme::from(self); @@ -291,6 +299,13 @@ impl Theme { Ok(()) } + /// Remove the `workbench.colorCustomizations` entry previously written + /// by [`Theme::apply_vs_code`] from VS Code's `settings.json`. + /// + /// # Errors + /// + /// Returns an `OutputError` if the user config dir is missing, the + /// settings file cannot be read/written, or its JSON is invalid. #[cold] pub fn reset_vs_code() -> Result<(), OutputError> { let mut config_dir = dirs::config_dir().ok_or(OutputError::MissingConfigDir)?; diff --git a/src/app/mod.rs b/src/app/mod.rs index 1ed17855..8ddc82d1 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -284,7 +284,7 @@ where // 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, diff --git a/src/applet/row.rs b/src/applet/row.rs index 718f366c..888c68ef 100644 --- a/src/applet/row.rs +++ b/src/applet/row.rs @@ -1,14 +1,13 @@ //! Distribute content horizontally. use crate::iced; use iced::core::alignment::{self, Alignment}; -use iced::core::event::{self, Event}; +use iced::core::event::Event; use iced::core::layout::{self, Layout}; use iced::core::widget::{Operation, Tree}; use iced::core::{ Clipboard, Element, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, mouse, overlay, renderer, widget, }; -use iced::touch; /// A container that distributes its contents horizontally. /// diff --git a/src/config/mod.rs b/src/config/mod.rs index 1691caa8..08a4c0ca 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -23,12 +23,11 @@ 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 { + if let cosmic_config::Error::GetKey(_, err) = &why + && err.kind() == std::io::ErrorKind::NotFound { // No system default config installed; don't error continue; } - } tracing::error!(?why, "CosmicTk config entry error"); } mode diff --git a/src/core.rs b/src/core.rs index fbb98fbe..e76bcd8d 100644 --- a/src/core.rs +++ b/src/core.rs @@ -92,8 +92,6 @@ pub struct Core { pub(super) portal_accent: Option, - pub(super) portal_is_high_contrast: Option, - pub(super) title: HashMap, pub window: Window, @@ -183,7 +181,6 @@ impl Default for Core { settings_daemon: None, portal_is_dark: None, portal_accent: None, - portal_is_high_contrast: None, main_window: None, exit_on_main_window_closed: true, menu_bars: HashMap::new(), diff --git a/src/dbus_activation.rs b/src/dbus_activation.rs index e7bc6900..880db634 100644 --- a/src/dbus_activation.rs +++ b/src/dbus_activation.rs @@ -43,7 +43,7 @@ pub fn subscription() -> Subscription( exec: S, env_vars: I, - app_id: Option<&str>, + _app_id: Option<&str>, terminal: bool, ) where S: AsRef, @@ -816,13 +816,17 @@ pub async fn spawn_desktop_exec( // https://systemd.io/DESKTOP_ENVIRONMENTS // // Similar to what Gnome sets, for now. - if let Some(pid) = crate::process::spawn(cmd).await { + if let Some(_pid) = crate::process::spawn(cmd).await { #[cfg(feature = "desktop-systemd-scope")] if let Ok(session) = zbus::Connection::session().await { if let Ok(systemd_manager) = SystemdMangerProxy::new(&session).await { let _ = systemd_manager .start_transient_unit( - &format!("app-cosmic-{}-{}.scope", app_id.unwrap_or(&executable), pid), + &format!( + "app-cosmic-{}-{}.scope", + _app_id.unwrap_or(&executable), + _pid + ), "fail", &[ ( @@ -833,7 +837,7 @@ pub async fn spawn_desktop_exec( ), ( "PIDs".to_string(), - zbus::zvariant::Value::from(vec![pid]) + zbus::zvariant::Value::from(vec![_pid]) .try_to_owned() .unwrap(), ), diff --git a/src/lib.rs b/src/lib.rs index 2dcbe67e..e22a3a1f 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -146,6 +146,17 @@ 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_runtime; +#[doc(inline)] +pub use iced_widget; +#[doc(inline)] +#[cfg(feature = "wayland")] +pub use iced_winit; pub mod icon_theme; pub mod keyboard_nav; diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 97246369..e4552226 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -170,18 +170,15 @@ impl Button { * TODO: Checkbox */ #[derive(Debug, Clone, Copy, PartialEq, Eq)] +#[derive(Default)] pub enum Checkbox { + #[default] Primary, Secondary, Success, Danger, } -impl Default for Checkbox { - fn default() -> Self { - Self::Primary - } -} impl iced_checkbox::Catalog for Theme { type Class<'a> = Checkbox; @@ -1241,7 +1238,7 @@ impl scrollable::Catalog for Theme { background: Color::TRANSPARENT.into(), border: Border::default(), shadow: Shadow::default(), - icon: Color::TRANSPARENT.into(), + icon: Color::TRANSPARENT, }, }; let small_widget_container = self.current_container().small_widget.with_alpha(0.7); @@ -1309,7 +1306,7 @@ impl scrollable::Catalog for Theme { background: Color::TRANSPARENT.into(), border: Border::default(), shadow: Shadow::default(), - icon: Color::TRANSPARENT.into(), + icon: Color::TRANSPARENT, }, }; diff --git a/src/widget/button/icon.rs b/src/widget/button/icon.rs index 6be13008..47713536 100644 --- a/src/widget/button/icon.rs +++ b/src/widget/button/icon.rs @@ -156,7 +156,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes ); } - let mut button = if builder.variant.vertical { + let button = if builder.variant.vertical { crate::widget::column::with_children(content) .padding(builder.padding) .spacing(builder.spacing) @@ -173,9 +173,11 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes }; #[cfg(feature = "a11y")] - { + let button = { + let mut button = button; button = button.name(builder.name).description(builder.description); - } + button + }; let button = button .padding(0) diff --git a/src/widget/button/image.rs b/src/widget/button/image.rs index 3b382035..915cc532 100644 --- a/src/widget/button/image.rs +++ b/src/widget/button/image.rs @@ -84,7 +84,7 @@ where .width(builder.width) .height(builder.height); - let mut button = super::custom_image_button(content, builder.variant.on_remove) + let button = super::custom_image_button(content, builder.variant.on_remove) .padding(0) .selected(builder.variant.selected) .id(builder.id) @@ -92,9 +92,11 @@ where .class(builder.class); #[cfg(feature = "a11y")] - { + let button = { + let mut button = button; button = button.name(builder.name).description(builder.description); - } + button + }; button.into() } diff --git a/src/widget/button/link.rs b/src/widget/button/link.rs index c0afd327..e6f8ac7e 100644 --- a/src/widget/button/link.rs +++ b/src/widget/button/link.rs @@ -67,7 +67,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 mut button: super::Button<'a, Message> = row::with_capacity(2) + let button: super::Button<'a, Message> = row::with_capacity(2) .push({ // TODO: Avoid allocation crate::widget::text(builder.label.to_string()) @@ -95,13 +95,15 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .class(builder.class); #[cfg(feature = "a11y")] - { + let button = { + let mut button = button; if !builder.label.is_empty() { button = button.name(builder.label); } button = button.description(builder.description); - } + button + }; if builder.tooltip.is_empty() { button.into() diff --git a/src/widget/button/text.rs b/src/widget/button/text.rs index 52ab6145..55623ff8 100644 --- a/src/widget/button/text.rs +++ b/src/widget/button/text.rs @@ -122,7 +122,7 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .into() }); - let mut button: super::Button<'a, Message> = row::with_capacity(3) + let button: super::Button<'a, Message> = row::with_capacity(3) // Optional icon to place before label. .push_maybe(leading_icon) // Optional label between icons. @@ -141,13 +141,15 @@ impl<'a, Message: Clone + 'static> From> for Element<'a, Mes .class(builder.class); #[cfg(feature = "a11y")] - { + let button = { + let mut button = button; if !builder.label.is_empty() { button = button.name(builder.label) } button = button.description(builder.description); - } + button + }; if builder.tooltip.is_empty() { button.into() diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index ebdf2d66..72113b87 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -379,13 +379,12 @@ impl<'a, Message: 'a + Clone> Widget match event { Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { - if let Some(position) = cursor.position() { - if removal_bounds(layout.bounds(), 4.0).contains(position) { + if let Some(position) = cursor.position() + && removal_bounds(layout.bounds(), 4.0).contains(position) { shell.publish(on_remove.clone()); shell.capture_event(); return; } - } } _ => (), @@ -576,9 +575,9 @@ impl<'a, Message: 'a + Clone> Widget } } - if on_remove.is_some() { - if let Some(position) = cursor.position() { - if bounds.contains(position) { + if on_remove.is_some() + && let Some(position) = cursor.position() + && bounds.contains(position) { let bounds = removal_bounds(layout.bounds(), 4.0); renderer.fill_quad( renderer::Quad { @@ -610,8 +609,6 @@ impl<'a, Message: 'a + Clone> Widget }, ); } - } - } }); } } @@ -762,12 +759,6 @@ impl State { self.is_focused } - /// Returns whether the [`Button`] is currently hovered or not. - #[inline] - pub fn is_hovered(self) -> bool { - self.is_hovered - } - /// Focuses the [`Button`]. #[inline] pub fn focus(&mut self) { @@ -813,7 +804,6 @@ pub fn update<'a, Message: Clone>( } shell.capture_event(); - return; } } } @@ -833,7 +823,6 @@ pub fn update<'a, Message: Clone>( } shell.capture_event(); - return; } } else if on_press_down.is_some() { let state = state(); @@ -853,7 +842,6 @@ pub fn update<'a, Message: Clone>( shell.publish(msg); } shell.capture_event(); - return; } Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) => { if let Some(on_press) = on_press { @@ -864,7 +852,6 @@ pub fn update<'a, Message: Clone>( shell.publish(msg); shell.capture_event(); - return; } } } diff --git a/src/widget/cards.rs b/src/widget/cards.rs index afdd1fe6..006d6a31 100644 --- a/src/widget/cards.rs +++ b/src/widget/cards.rs @@ -525,7 +525,7 @@ where 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, + state, event, c_layout, cursor, renderer, clipboard, shell, viewport, ); } @@ -535,7 +535,7 @@ where 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, + c_state, event, layout, cursor, renderer, clipboard, shell, viewport, ); if shell.is_event_captured() || fully_unexpanded { break; diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index ce733655..3ec2ff35 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -723,7 +723,7 @@ where let column_tree = &mut tree.children[0]; self.inner.as_widget_mut().update( column_tree, - &event, + event, column_layout, cursor, renderer, @@ -736,22 +736,19 @@ where return; } - match event { - Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - let bounds = column_layout.children().nth(1).unwrap().bounds(); - if let Some(point) = cursor.position_over(bounds) { - let relative_pos = point - bounds.position(); - let (s, v) = ( - relative_pos.x / bounds.width, - 1.0 - relative_pos.y / bounds.height, - ); - state.dragging = true; - let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v); - shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv))); - shell.capture_event(); - } + if let Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) = event { + let bounds = column_layout.children().nth(1).unwrap().bounds(); + if let Some(point) = cursor.position_over(bounds) { + let relative_pos = point - bounds.position(); + let (s, v) = ( + relative_pos.x / bounds.width, + 1.0 - relative_pos.y / bounds.height, + ); + state.dragging = true; + let hsv: palette::Hsv = palette::Hsv::new(self.active_color.hue, s, v); + shell.publish((self.on_update)(ColorPickerUpdate::ActiveColor(hsv))); + shell.capture_event(); } - _ => {} } } diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 0ebff3dc..afe135bc 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -357,7 +357,7 @@ impl Widget x, y, mime_types, .. }, )) if *id == Some(my_id) => { - if !self.mime_matches(&mime_types) { + if !self.mime_matches(mime_types) { log::trace!( target: DND_DEST_LOG_TARGET, "offer enter id={my_id:?} ignored (mimes={mime_types:?} not in {:?})", @@ -396,7 +396,6 @@ impl Widget ); } shell.capture_event(); - return; } Event::Dnd(DndEvent::Offer(_, OfferEvent::Leave)) => { log::trace!( @@ -424,7 +423,6 @@ impl Widget viewport, ); } - return; } Event::Dnd(DndEvent::Offer(id, OfferEvent::Motion { x, y })) if *id == Some(my_id) => { log::trace!( @@ -459,7 +457,6 @@ impl Widget ); } shell.capture_event(); - return; } Event::Dnd(DndEvent::Offer(_, OfferEvent::LeaveDestination)) => { log::trace!( @@ -472,7 +469,6 @@ impl Widget { shell.publish(msg); } - return; } Event::Dnd(DndEvent::Offer(id, OfferEvent::Drop)) if *id == Some(my_id) => { log::trace!( @@ -485,7 +481,6 @@ impl Widget shell.publish(msg); } shell.capture_event(); - return; } Event::Dnd(DndEvent::Offer(id, OfferEvent::SelectedAction(action))) if *id == Some(my_id) => @@ -503,7 +498,6 @@ impl Widget shell.publish(msg); } shell.capture_event(); - return; } Event::Dnd(DndEvent::Offer(id, OfferEvent::Data { data, mime_type })) if *id == Some(my_id) => @@ -543,7 +537,6 @@ impl Widget return; } shell.capture_event(); - return; } _ => {} } diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index bb3cead1..99bee955 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -474,16 +474,14 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { let hovered_guard = self.hovered_option.lock().unwrap(); - if cursor.is_over(layout.bounds()) { - if let Some(index) = *hovered_guard { + if cursor.is_over(layout.bounds()) + && let Some(index) = *hovered_guard { shell.publish((self.on_selected)(index)); if let Some(close_on_selected) = self.close_on_selected.as_ref() { shell.publish(close_on_selected.clone()); } shell.capture_event(); - return; } - } } Event::Mouse(mouse::Event::CursorMoved { .. }) => { if let Some(cursor_position) = cursor.position_in(layout.bounds()) { @@ -498,11 +496,10 @@ where let new_hovered_option = (cursor_position.y / option_height) as usize; let mut hovered_guard = self.hovered_option.lock().unwrap(); - if let Some(on_option_hovered) = self.on_option_hovered { - if *hovered_guard != Some(new_hovered_option) { + if let Some(on_option_hovered) = self.on_option_hovered + && *hovered_guard != Some(new_hovered_option) { shell.publish(on_option_hovered(new_hovered_option)); } - } *hovered_guard = Some(new_hovered_option); } @@ -526,7 +523,6 @@ where shell.publish(close_on_selected.clone()); } shell.capture_event(); - return; } } } diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index a1da9da7..883b8ada 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -343,13 +343,11 @@ where match event { Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => { - if cursor.is_over(bounds) { - if let Some(item) = self.hovered_option.as_ref() { + if cursor.is_over(bounds) + && let Some(item) = self.hovered_option.as_ref() { shell.publish((self.on_selected)(item.clone())); shell.capture_event(); - return; } - } } Event::Mouse(mouse::Event::CursorMoved { .. }) => { if let Some(cursor_position) = cursor.position_in(bounds) { diff --git a/src/widget/dropdown/multi/widget.rs b/src/widget/dropdown/multi/widget.rs index 90c6ab71..c7a46592 100644 --- a/src/widget/dropdown/multi/widget.rs +++ b/src/widget/dropdown/multi/widget.rs @@ -6,7 +6,7 @@ use super::menu::{self, Menu}; use crate::widget::icon; use derive_setters::Setters; use iced_core::event::Event; -use iced_core::text::{self, Paragraph, Text}; +use iced_core::text::{self, Text}; use iced_core::widget::tree::{self, Tree}; use iced_core::{ Clipboard, Layout, Length, Padding, Pixels, Rectangle, Shadow, Shell, Size, Vector, Widget, @@ -127,7 +127,7 @@ impl<'a, S: AsRef, Message: 'a, Item: Clone + PartialEq + 'static> _viewport: &Rectangle, ) { update( - &event, + event, layout, cursor, shell, diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 20a0e70e..2fa742e9 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -263,7 +263,7 @@ where _viewport: &Rectangle, ) { update::( - &event, + event, layout, cursor, shell, diff --git a/src/widget/menu/flex.rs b/src/widget/menu/flex.rs index 213ee694..e4a32870 100644 --- a/src/widget/menu/flex.rs +++ b/src/widget/menu/flex.rs @@ -48,6 +48,7 @@ impl Axis { /// padding and alignment to the items as needed. /// /// It returns a new layout [`Node`]. +#[allow(dead_code)] // kept as public helper; not currently called by libcosmic pub fn resolve<'a, E, Message, Renderer>( axis: &Axis, renderer: &Renderer, @@ -246,7 +247,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.into_iter().zip(tree.iter_mut()) { + for (child, tree) in items.iter_mut().zip(tree.iter_mut()) { let c_size = child.size(); let cross_fill_factor = match axis { Axis::Horizontal => c_size.height, @@ -269,7 +270,7 @@ pub fn resolve_wrapper<'a, Message>( cross = fill_cross; } - for (i, (child, tree)) in items.into_iter().zip(tree.iter_mut()).enumerate() { + for (i, (child, tree)) in items.iter_mut().zip(tree.iter_mut()).enumerate() { let c_size = child.size(); let fill_factor = match axis { Axis::Horizontal => c_size.width, @@ -312,7 +313,7 @@ pub fn resolve_wrapper<'a, Message>( let remaining = available.max(0.0); - for (i, (child, tree)) in items.into_iter().zip(tree.iter_mut()).enumerate() { + for (i, (child, tree)) in items.iter_mut().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 1715ae13..acb7ab06 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -50,7 +50,6 @@ 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: Vec, @@ -87,7 +86,6 @@ impl Default for MenuBarStateInner { vertical_direction: Direction::Positive, menu_states: Vec::new(), popup_id: HashMap::new(), - bar_pressed: false, } } } @@ -164,14 +162,6 @@ where } } -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)] @@ -622,14 +612,12 @@ where .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() { + 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 }); diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 6e17df52..04c418c4 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -1569,14 +1569,13 @@ where .is_some_and(|i| *i != new_index && !active_menu[*i].children.is_empty()); #[cfg(all(feature = "multi-window", feature = "wayland", target_os = "linux", feature = "surface-message"))] - if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && remove { - if let Some(id) = state.popup_id.remove(&menu.window_id) { + if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && remove + && 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); diff --git a/src/widget/popover.rs b/src/widget/popover.rs index c822899d..1f218cea 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -160,8 +160,8 @@ where shell.capture_event(); return; } - } else if let Some(on_close) = self.on_close.as_ref() { - if matches!( + } else if let Some(on_close) = self.on_close.as_ref() + && matches!( event, Event::Mouse(mouse::Event::ButtonPressed(_)) | Event::Touch(touch::Event::FingerPressed { .. }) @@ -169,7 +169,6 @@ where { shell.publish(on_close.clone()); } - } } // Hide cursor from background content when modal popup is active @@ -492,12 +491,6 @@ where } } -/// The local state of a [`Popover`]. -#[derive(Debug, Default)] -struct State { - is_open: bool, -} - /// The first child in [`Popover::children`] is always the wrapped content. fn content_tree(tree: &Tree) -> &Tree { &tree.children[0] diff --git a/src/widget/radio.rs b/src/widget/radio.rs index 7e6ef424..f8e174e1 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -262,7 +262,6 @@ where if cursor.is_over(layout.bounds()) { shell.publish(self.on_click.clone()); shell.capture_event(); - return; } } _ => {} diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 7a996db1..11ee364d 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -713,11 +713,10 @@ where 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 { + } else if self.model.is_active(key) + && 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 += @@ -1367,11 +1366,10 @@ where { 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() { + if let Some(Some(entity)) = entity + && 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:?}" @@ -1447,11 +1445,10 @@ where None:: Message>, None, ); - if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() { - if let Some(Some(entity)) = entity { + if let Some(on_dnd_leave) = self.on_dnd_leave.as_ref() + && let Some(Some(entity)) = entity { shell.publish(on_dnd_leave(entity)); } - } } } DndEvent::Offer(id, OfferEvent::Drop) if Some(my_id) == *id => { @@ -1624,8 +1621,8 @@ where // Emit close message if the close button is pressed. if let Some(on_close) = self.on_close.as_ref() { if over_close_button - && (left_button_released(&event) - || (touch_lifted(&event) && fingers_pressed == 1)) + && (left_button_released(event) + || (touch_lifted(event) && fingers_pressed == 1)) { shell.publish(on_close(key)); shell.capture_event(); @@ -1679,7 +1676,7 @@ where } } - if is_lifted(&event) { + if is_lifted(event) { state.unfocus(); } @@ -1723,7 +1720,7 @@ where if let Some(on_activate) = self.on_activate.as_ref() { if is_pressed(event) { state.pressed_item = Some(Item::Tab(key)); - } else if is_lifted(&event) && self.button_is_pressed(state, key) { + } 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); @@ -1763,8 +1760,8 @@ where // Present a context menu on a right click event. 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)) + && (right_button_released(event) + || (touch_lifted(event) && fingers_pressed == 2)) { state.show_context = Some(key); state.context_cursor = cursor_position.position().unwrap_or_default(); @@ -1882,12 +1879,12 @@ where } if state.is_focused() { // Unfocus on clicks outside of the boundaries of the segmented button. - if is_pressed(&event) { + if is_pressed(event) { state.unfocus(); state.pressed_item = None; return; } - } else if is_lifted(&event) { + } else if is_lifted(event) { state.pressed_item = None; } } @@ -2534,8 +2531,8 @@ where ); } - if show_drop_hint_marker { - if matches!( + if show_drop_hint_marker + && matches!( drop_hint_marker, Some(DropHint { entity, @@ -2550,7 +2547,6 @@ where appearance.active.text_color, ); } - } nth += 1; }); @@ -2760,7 +2756,7 @@ where } } -struct TabDragSource { +pub(super) struct TabDragSource { mime: String, threshold: f32, _marker: PhantomData, @@ -2811,7 +2807,6 @@ struct TabDragCandidate { #[derive(Debug, Clone, Copy)] struct Focus { updated_at: Instant, - now: Instant, } /// State that is maintained by each individual widget. @@ -2884,7 +2879,6 @@ impl LocalState { self.focused = Some(Focus { updated_at: now, - now, }); } } diff --git a/src/widget/spin_button.rs b/src/widget/spin_button.rs index 71933124..b2528861 100644 --- a/src/widget/spin_button.rs +++ b/src/widget/spin_button.rs @@ -23,7 +23,7 @@ pub fn spin_button<'a, T, M>( where T: Copy + Sub + Add + PartialOrd, { - let mut button = SpinButton::new( + let button = SpinButton::new( label, value, step, @@ -34,9 +34,11 @@ where ); #[cfg(feature = "a11y")] - { + let button = { + let mut button = button; button = button.name(name.into()); - } + button + }; button } @@ -54,20 +56,22 @@ pub fn vertical<'a, T, M>( where T: Copy + Sub + Add + PartialOrd, { - let mut button = SpinButton::new( + let button = SpinButton::new( label, value, step, min, max, - Orientation::Horizontal, + Orientation::Vertical, on_press, ); #[cfg(feature = "a11y")] - { + let button = { + let mut button = button; button = button.name(name.into()); - } + button + }; button } diff --git a/src/widget/table/widget/standard.rs b/src/widget/table/widget/standard.rs index 0e206238..e21cd22f 100644 --- a/src/widget/table/widget/standard.rs +++ b/src/widget/table/widget/standard.rs @@ -84,15 +84,14 @@ where let mut sort_state = 0; - if let Some(sort) = val.model.sort { - if sort.0 == category { + if let Some(sort) = val.model.sort + && sort.0 == category { if sort.1 { sort_state = 1; } else { sort_state = 2; } - } - }; + }; // Build the category header widget::row::with_capacity(2) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index a8b668c0..cecd7ae7 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -666,18 +666,15 @@ where let old_value = Value::new(&old_value); if state.is_focused() && let cursor::State::Index(index) = state.cursor.state(&old_value) - { - if index == old_value.len() { + && index == old_value.len() { state.cursor.move_to(self.value.len()); } - } - if let Some(f) = state.is_focused.as_ref().filter(|f| f.focused) { - if f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get()) { + if let Some(f) = state.is_focused.as_ref().filter(|f| f.focused) + && f.updated_at != LAST_FOCUS_UPDATE.with(|f| f.get()) { state.unfocus(); state.emit_unfocus = true; } - } if self.is_editable_variant { if !state.is_focused() { @@ -891,8 +888,8 @@ where let line_height = self.line_height; // 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 { + if self.is_editable_variant + && let Some(ref on_edit) = self.on_toggle_edit { let state = tree.state.downcast_mut::(); if !state.is_read_only && state.is_focused.is_some_and(|f| !f.focused) { state.is_read_only = true; @@ -904,7 +901,6 @@ where shell.publish((on_edit)(f.focused)); } } - } // Calculates the layout of the trailing icon button element. if !tree.children.is_empty() { @@ -915,9 +911,9 @@ where trailing_icon_layout = Some(text_layout.children().last().unwrap()); // 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().update( + if !self.is_editable_variant + && let Some(trailing_layout) = trailing_icon_layout { + trailing_icon.as_widget_mut().update( tree, event, trailing_layout, @@ -932,18 +928,16 @@ where return; } } - } } } let state = tree.state.downcast_mut::(); - if let Some(on_unfocus) = self.on_unfocus.as_ref() { - if state.emit_unfocus { + if let Some(on_unfocus) = self.on_unfocus.as_ref() + && state.emit_unfocus { state.emit_unfocus = false; shell.publish(on_unfocus.clone()); } - } let dnd_id = self.dnd_id(); let id = Widget::id(self); @@ -1652,11 +1646,10 @@ pub fn update<'a, Message: Clone + 'static>( if matches!(state.dragging_state, None | Some(DraggingState::Selection)) && (!state.is_focused() || (is_editable_variant && state.is_read_only)) { - if !state.is_focused() { - if let Some(on_focus) = on_focus { + if !state.is_focused() + && let Some(on_focus) = on_focus { shell.publish(on_focus.clone()); } - } if state.is_read_only { state.is_read_only = false; @@ -1681,7 +1674,6 @@ pub fn update<'a, Message: Clone + 'static>( state.last_click = Some(click); shell.capture_event(); - return; } else { state.unfocus(); @@ -1717,7 +1709,6 @@ pub fn update<'a, Message: Clone + 'static>( if cursor.is_over(layout.bounds()) { shell.capture_event(); } - return; } Event::Mouse(mouse::Event::CursorMoved { position }) | Event::Touch(touch::Event::FingerMoved { position, .. }) => { @@ -1800,7 +1791,6 @@ pub fn update<'a, Message: Clone + 'static>( } shell.capture_event(); - return; } } Event::Keyboard(keyboard::Event::KeyPressed { @@ -1838,14 +1828,13 @@ pub fn update<'a, Message: Clone + 'static>( { match clip_key { Some('c') => { - if !is_secure { - if let Some((start, end)) = state.cursor.selection(value) { + if !is_secure + && let Some((start, end)) = state.cursor.selection(value) { clipboard.write( iced_core::clipboard::Kind::Standard, value.select(start, end).to_string(), ); } - } } // XXX if we want to allow cutting of secure text, we need to // update the cache and decide which value to cut @@ -2100,7 +2089,6 @@ pub fn update<'a, Message: Clone + 'static>( } shell.capture_event(); - return; } } Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { @@ -2120,7 +2108,6 @@ pub fn update<'a, Message: Clone + 'static>( } shell.capture_event(); - return; } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { @@ -2136,7 +2123,6 @@ pub fn update<'a, Message: Clone + 'static>( 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() { @@ -2146,7 +2132,6 @@ pub fn update<'a, Message: Clone + 'static>( text_size: Some(size.into()), }); shell.capture_event(); - return; } } input_method::Event::Commit(text) => { @@ -2164,7 +2149,7 @@ pub fn update<'a, Message: Clone + 'static>( 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)); + editor.paste(Value::new(text)); let contents = editor.contents(); let unsecured_value = Value::new(&contents); @@ -2184,7 +2169,6 @@ pub fn update<'a, Message: Clone + 'static>( update_cache(state, &value); shell.capture_event(); - return; } } } @@ -2214,7 +2198,6 @@ pub fn update<'a, Message: Clone + 'static>( // TODO: restore value in text input state.dragging_state = None; shell.capture_event(); - return; } } #[cfg(all(feature = "wayland", target_os = "linux"))] @@ -2224,7 +2207,7 @@ pub fn update<'a, Message: Clone + 'static>( x, y, mime_types, - surface, + surface: _surface, }, )) if *rectangle == Some(dnd_id) => { cold(); @@ -2264,11 +2247,10 @@ pub fn update<'a, Message: Clone + 'static>( state.cursor.set_affinity(affinity); state.cursor.move_to(position); shell.capture_event(); - return; } } #[cfg(all(feature = "wayland", target_os = "linux"))] - Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y })) + Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y: _ })) if *rectangle == Some(dnd_id) => { let state = state(); @@ -2293,7 +2275,6 @@ pub fn update<'a, Message: Clone + 'static>( state.cursor.set_affinity(affinity); state.cursor.move_to(position); shell.capture_event(); - return; } #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if *rectangle == Some(dnd_id) => { @@ -2310,8 +2291,6 @@ pub fn update<'a, Message: Clone + 'static>( }; state.dnd_offer = DndOfferState::Dropped; } - - return; } #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != *id => {} @@ -2331,7 +2310,6 @@ pub fn update<'a, Message: Clone + 'static>( } }; shell.capture_event(); - return; } #[cfg(all(feature = "wayland", target_os = "linux"))] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type })) @@ -2368,9 +2346,7 @@ pub fn update<'a, Message: Clone + 'static>( }; update_cache(state, &value); shell.capture_event(); - return; } - return; } _ => {} } @@ -2803,11 +2779,11 @@ pub fn draw<'a, Message>( // draw the end icon in the text input if let (Some(icon), Some(tree)) = (trailing_icon, trailing_icon_tree) { let mut children = text_layout.children(); - let mut icon_layout = children.next().unwrap(); + children.next().unwrap(); // skip text layout if has_start_icon { - icon_layout = children.next().unwrap(); + children.next().unwrap(); // skip start-icon layout } - icon_layout = children.next().unwrap(); + let icon_layout = children.next().unwrap(); // trailing-icon layout icon.as_widget().draw( tree, diff --git a/src/widget/text_input/value.rs b/src/widget/text_input/value.rs index 3f7b8d73..59e77752 100644 --- a/src/widget/text_input/value.rs +++ b/src/widget/text_input/value.rs @@ -45,9 +45,7 @@ impl Value { pub fn previous_start_of_word(&self, index: usize) -> usize { let previous_string = &self.graphemes[..index.min(self.graphemes.len())].concat(); - UnicodeSegmentation::split_word_bound_indices(previous_string as &str) - .filter(|(_, word)| !word.trim_start().is_empty()) - .next_back() + UnicodeSegmentation::split_word_bound_indices(previous_string as &str).rfind(|(_, word)| !word.trim_start().is_empty()) .map_or(0, |(i, previous_word)| { index - UnicodeSegmentation::graphemes(previous_word, true).count() diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 2dad2ed9..2254759e 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -182,7 +182,8 @@ impl<'a, Message> Widget for Toggler<'a, ) -> layout::Node { let limits = limits.width(self.width); - let res = next_to_each_other( + + next_to_each_other( &limits, self.spacing, |limits| { @@ -221,8 +222,7 @@ impl<'a, Message> Widget for Toggler<'a, } }, |_| layout::Node::new(Size::new(48., 24.)), - ); - res + ) } fn update( From 9512f77a589f4f995389008351fd93437d6f0b9d Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Mon, 25 May 2026 13:02:38 +0200 Subject: [PATCH 125/201] feat(flex_row): reorderable rows with shadow constant gated (squashed) Squash of 2 yoda commits: - 7dd0ee83 feat: reorderable flex row - b1b8203a fix: gate reorderable flex row shadow constant --- src/widget/reorderable_flex_row/widget.rs | 1 + 1 file changed, 1 insertion(+) diff --git a/src/widget/reorderable_flex_row/widget.rs b/src/widget/reorderable_flex_row/widget.rs index ed95a34f..03789877 100644 --- a/src/widget/reorderable_flex_row/widget.rs +++ b/src/widget/reorderable_flex_row/widget.rs @@ -21,6 +21,7 @@ use std::time::{Duration, Instant}; const DEFAULT_ANIMATION_DURATION: Duration = Duration::from_millis(180); const DEFAULT_DRAG_LIFT: f32 = 10.0; const DEFAULT_DRAG_THRESHOLD: f32 = 6.0; +#[cfg(feature = "wgpu")] const SHADOW_BLUR_RADIUS: f32 = 20.0; const POSITION_EPSILON: f32 = 0.5; From 2c91d818c95138a40070ff38f8e9cf14540c9def Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Mon, 25 May 2026 09:55:11 +0200 Subject: [PATCH 126/201] yoda: use local dbus settings bindings --- Cargo.toml | 4 ++-- cosmic-config/Cargo.toml | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index fb751df4..ae56d709 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -178,8 +178,8 @@ enumflags2 = "0.7.12" # 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.workspace = true +cosmic-settings-daemon = { path = "../dbus-settings-bindings/cosmic-settings-daemon" } +zbus = { version = "5.14.0", default-features = false } [target.'cfg(all(unix, not(target_os = "macos")))'.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 0316c707..a81d13fd 100644 --- a/cosmic-config/Cargo.toml +++ b/cosmic-config/Cargo.toml @@ -10,8 +10,8 @@ macro = ["cosmic-config-derive"] subscription = ["iced_futures"] [dependencies] -cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings", optional = true } -zbus = { workspace = true, default-features = false, optional = true } +cosmic-settings-daemon = { path = "../../dbus-settings-bindings/cosmic-settings-daemon", optional = true } +zbus = { version = "5.14.0", default-features = false, optional = true } atomicwrites = { git = "https://github.com/jackpot51/rust-atomicwrites" } calloop = { version = "0.14.4", optional = true } notify = "8.2.0" From ce5e3b6eca6bf80ff45407bbbe313ceecc80e979 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Mon, 25 May 2026 13:13:30 +0200 Subject: [PATCH 127/201] fix: drop iced_winit/single-instance feature ref MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream dc84488c "fix(iced): double IME commit on GNOME desktop" wires the libcosmic single-instance feature through iced_winit/single-instance, but our pinned iced fork (yoda-wayland-only @ 81639935) does not carry that feature yet. Drop the ref so libcosmic-yoda compiles after rebase; the GNOME IME fix needs to be ported separately into the iced submodule before this can be reinstated. Leyoda 2026 – GPLv3 --- Cargo.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index ae56d709..0063bf0e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -71,7 +71,7 @@ desktop-systemd-scope = ["desktop", "dep:zbus"] # Enables keycode serialization serde-keycode = ["iced_core/serde"] # Prevents multiple separate process instances. -single-instance = ["iced_winit/single-instance", "zbus/blocking-api", "ron"] +single-instance = ["zbus/blocking-api", "ron"] # smol async runtime smol = ["dep:smol", "iced/smol", "zbus?/async-io", "rfd?/async-std"] tokio = [ From ca66c7c1b0e52d3242d4a0083e144e27366185c4 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Mon, 25 May 2026 18:09:50 +0200 Subject: [PATCH 128/201] chore: use local COSMIC support crates --- Cargo.toml | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 0063bf0e..46065556 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -130,10 +130,10 @@ ashpd = { version = "0.12.3", default-features = false, optional = true } async-fs = { version = "2.2", optional = true } async-std = { workspace = true, optional = true } auto_enums = "0.8.8" -cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "32283d7", optional = true } +cctk = { path = "../cosmic-protocols/client-toolkit", package = "cosmic-client-toolkit", optional = true } jiff = "0.2" cosmic-config = { path = "cosmic-config" } -cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } +cosmic-settings-config = { path = "../cosmic-settings-daemon/config", optional = true } # Internationalization i18n-embed = { version = "0.16.0", features = [ "fluent-system", @@ -245,8 +245,7 @@ path = "./iced/wgpu" optional = true [dependencies.cosmic-panel-config] -git = "https://github.com/pop-os/cosmic-panel" -# path = "../cosmic-panel/cosmic-panel-config" +path = "../cosmic-panel/cosmic-panel-config" optional = true [workspace] From 37829fd89980efef69a64a7c33e8780f53081432 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Mon, 25 May 2026 19:36:38 +0200 Subject: [PATCH 129/201] chore: use local graphics dependencies --- Cargo.toml | 25 ++++++++++++++++++++++++- 1 file changed, 24 insertions(+), 1 deletion(-) diff --git a/Cargo.toml b/Cargo.toml index 46065556..9948711a 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -182,7 +182,7 @@ cosmic-settings-daemon = { path = "../dbus-settings-bindings/cosmic-settings-dae zbus = { version = "5.14.0", default-features = false } [target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] -freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } +freedesktop-icons = { package = "cosmic-freedesktop-icons", path = "../cosmic-freedesktop-icons" } freedesktop-desktop-entry = { version = "0.8.1", optional = true } shlex = { version = "1.3.0", optional = true } @@ -248,6 +248,29 @@ optional = true path = "../cosmic-panel/cosmic-panel-config" optional = true +[patch.'https://github.com/pop-os/freedesktop-icons'] +cosmic-freedesktop-icons = { path = "../cosmic-freedesktop-icons" } + +[patch.'https://github.com/pop-os/softbuffer'] +softbuffer = { path = "../softbuffer" } + +[patch.'https://github.com/pop-os/smithay-clipboard'] +smithay-clipboard = { path = "../smithay-clipboard" } + +[patch.'https://github.com/pop-os/winit.git'] +dpi = { path = "../winit/dpi" } +winit = { path = "../winit/winit" } +winit-android = { path = "../winit/winit-android" } +winit-appkit = { path = "../winit/winit-appkit" } +winit-common = { path = "../winit/winit-common" } +winit-core = { path = "../winit/winit-core" } +winit-orbital = { path = "../winit/winit-orbital" } +winit-uikit = { path = "../winit/winit-uikit" } +winit-wayland = { path = "../winit/winit-wayland" } +winit-web = { path = "../winit/winit-web" } +winit-win32 = { path = "../winit/winit-win32" } +winit-x11 = { path = "../winit/winit-x11" } + [workspace] members = [ "cosmic-config", From 18c2326f34d29423c2b26537de8d5ac440c45998 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Thu, 2 Jul 2026 18:46:55 +0200 Subject: [PATCH 130/201] yoda: rebase iced fork onto upstream c781ff61 (iced 0.14 bump) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Leyoda 2026 – GPLv3 --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index d9fe129f..362b4abb 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit d9fe129f7316cea3ef6d2576e9421a305759b8bf +Subproject commit 362b4abb1558374686080db674b8f7c5868ae6ac From 6cdb4c62ced3e13b0ef9f6894d004e6cbf1a3c27 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 2 Jul 2026 12:38:03 -0400 Subject: [PATCH 131/201] refactor(rectange tracker): relax Copy to Clone --- src/widget/rectangle_tracker/mod.rs | 8 +++---- src/widget/rectangle_tracker/subscription.rs | 24 +++++++++++--------- 2 files changed, 17 insertions(+), 15 deletions(-) diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index 839fa9a7..1fb89ac3 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -22,15 +22,15 @@ pub fn rectangle_tracking_container<'a, Message, I, T>( tx: UnboundedSender<(I, Rectangle)>, ) -> RectangleTrackingContainer<'a, Message, crate::Renderer, I> where - I: Hash + Copy + Send + Sync + Debug + 'a, + I: Hash + Clone + Send + Sync + Debug + 'a, T: Into>, { RectangleTrackingContainer::new(content, id, tx) } pub fn subscription< - I: 'static + Hash + Copy + Send + Sync + Debug, - R: 'static + Hash + Copy + Send + Sync + Debug + Eq, + I: 'static + Hash + Clone + Send + Sync + Debug, + R: 'static + Hash + Clone + Send + Sync + Debug + Eq, >( id: I, ) -> iced::Subscription<(I, RectangleUpdate)> { @@ -76,7 +76,7 @@ where impl<'a, Message, Renderer, I> RectangleTrackingContainer<'a, Message, Renderer, I> where Renderer: iced_core::Renderer, - I: 'a + Hash + Copy + Send + Sync + Debug, + I: 'a + Hash + Clone + Send + Sync + Debug, { /// Creates an empty [`Container`]. pub(crate) fn new(content: T, id: I, tx: UnboundedSender<(I, Rectangle)>) -> Self diff --git a/src/widget/rectangle_tracker/subscription.rs b/src/widget/rectangle_tracker/subscription.rs index 22b42441..98082547 100644 --- a/src/widget/rectangle_tracker/subscription.rs +++ b/src/widget/rectangle_tracker/subscription.rs @@ -10,14 +10,16 @@ use super::RectangleTracker; #[cold] pub fn rectangle_tracker_subscription< - I: 'static + Hash + Copy + Send + Sync + Debug, - R: 'static + Hash + Copy + Send + Sync + Debug + Eq, + I: 'static + Hash + Clone + Send + Sync + Debug, + R: 'static + Hash + Clone + Send + Sync + Debug + Eq, >( id: I, ) -> Subscription<(I, RectangleUpdate)> { Subscription::run_with(id, |id| { - let id = *id; - stream::unfold(State::Ready, move |state| start_listening(id, state)) + let id = id.clone(); + stream::unfold(State::Ready, move |state| { + start_listening(id.clone(), state) + }) }) } @@ -27,7 +29,7 @@ pub enum State { Finished, } -async fn start_listening( +async fn start_listening( id: I, mut state: State, ) -> Option<((I, RectangleUpdate), State)> { @@ -37,7 +39,7 @@ async fn start_listening 0.1 || (prev.y - new.y).abs() > 0.1 { - map.insert(u.0, new); + map.insert(u.0.clone(), new); ( - Some((id, RectangleUpdate::Rectangle(u))), + Some((id.clone(), RectangleUpdate::Rectangle(u))), State::Waiting(rx, map), ) } else { (None, State::Waiting(rx, map)) } } else { - map.insert(u.0, u.1); + map.insert(u.0.clone(), u.1); ( - Some((id, RectangleUpdate::Rectangle(u))), + Some((id.clone(), RectangleUpdate::Rectangle(u))), State::Waiting(rx, map), ) } @@ -80,7 +82,7 @@ async fn start_listening where - I: 'static + Hash + Copy + Send + Sync + Debug, + I: 'static + Hash + Clone + Send + Sync + Debug, { Rectangle((I, Rectangle)), Init(RectangleTracker), From 9e9ae942c4fca245b17d0279c96d3625f3fcbf90 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Thu, 2 Jul 2026 19:00:00 +0200 Subject: [PATCH 132/201] yoda: absorb upstream theme-v2 integration debt post-rebase MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - degate 'feature = "winit"' cfgs added by upstream (fork policy: winit toujours actif, le stub Cargo reste vide) - cards.rs: restore 'id' param consumed by upstream's new struct init - iced submodule rebased onto upstream c781ff61 (wgpu 28, cryoglyph) - Cargo.lock regenerated against local path deps Leyoda 2026 – GPLv3 --- src/app/cosmic.rs | 17 ++++++++--------- src/app/mod.rs | 1 - src/core.rs | 1 - src/surface/action.rs | 4 ++-- src/surface/mod.rs | 1 - src/widget/cards.rs | 2 +- src/widget/menu/menu_bar.rs | 1 - src/widget/nav_bar.rs | 1 - src/widget/segmented_button/widget.rs | 11 +---------- 9 files changed, 12 insertions(+), 27 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 6de7f35b..49013312 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -23,7 +23,6 @@ use iced::Application as IcedApplication; use iced::event::wayland; use iced::{Task, theme, window}; use iced_futures::event::listen_with; -#[cfg(feature = "winit")] use iced_winit::SurfaceIdWrapper; use palette::color_difference::EuclideanDistance; @@ -1568,7 +1567,7 @@ impl Cosmic { if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { window::enable_blur(id) } else { - #[cfg(all(feature = "wayland", feature = "winit", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] { iced_winit::commands::blur::blur( id, @@ -1579,7 +1578,7 @@ impl Cosmic { ) .discard() } - #[cfg(not(all(feature = "wayland", feature = "winit", target_os = "linux")))] + #[cfg(not(all(feature = "wayland", target_os = "linux")))] { iced::window::enable_blur(id) } @@ -1587,11 +1586,11 @@ impl Cosmic { } else if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { window::disable_blur(id) } else { - #[cfg(all(feature = "wayland", feature = "winit", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] { iced_winit::commands::blur::blur(id, None).discard() } - #[cfg(not(all(feature = "wayland", feature = "winit", target_os = "linux")))] + #[cfg(not(all(feature = "wayland", target_os = "linux")))] { iced::window::disable_blur(id) } @@ -1601,7 +1600,7 @@ impl Cosmic { cmds.push(if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { window::enable_blur(id) } else { - #[cfg(all(feature = "wayland", feature = "winit", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] { iced_winit::commands::blur::blur( id, @@ -1612,13 +1611,13 @@ impl Cosmic { ) .discard() } - #[cfg(not(all(feature = "wayland", feature = "winit", target_os = "linux")))] + #[cfg(not(all(feature = "wayland", target_os = "linux")))] { iced::window::enable_blur(id) } }); } - #[cfg(all(feature = "wayland", feature = "winit", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] if let Some(corners) = live_settings.corners { cmds.push( iced_winit::commands::corner_radius::corner_radius(id, Some(corners)).discard(), @@ -1634,7 +1633,7 @@ impl Cosmic { ); } } - #[cfg(all(feature = "wayland", feature = "winit", target_os = "linux"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] if let (SurfaceIdWrapper::LayerSurface(id), Some(padding)) = (id_wrapper, live_settings.padding) { diff --git a/src/app/mod.rs b/src/app/mod.rs index 8ddc82d1..bf5da95e 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -394,7 +394,6 @@ where feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] { diff --git a/src/core.rs b/src/core.rs index e76bcd8d..8845d2af 100644 --- a/src/core.rs +++ b/src/core.rs @@ -546,7 +546,6 @@ impl Core { } #[must_use] - #[cfg(feature = "winit")] pub fn blur( &self, theme: &Theme, diff --git a/src/surface/action.rs b/src/surface/action.rs index 6de5af71..0314d1d5 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -261,7 +261,7 @@ pub fn subsurface( ) } -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn simple_layer_shell( live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, @@ -297,7 +297,7 @@ pub fn simple_layer_shell( ) } -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn app_layer_shell( live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static, diff --git a/src/surface/mod.rs b/src/surface/mod.rs index b3858b97..d34d684f 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -69,7 +69,6 @@ pub enum Action { Task(Arc Task + Send + Sync>), } -#[cfg(feature = "winit")] pub fn surface_task(action: Action) -> Task> { crate::task::message(crate::Action::Cosmic(crate::app::Action::Surface(action))) } diff --git a/src/widget/cards.rs b/src/widget/cards.rs index 006d6a31..e66a3c01 100644 --- a/src/widget/cards.rs +++ b/src/widget/cards.rs @@ -93,7 +93,7 @@ where /// Get an expandable stack of cards #[allow(clippy::too_many_arguments)] pub fn new( - _id: widget::Id, + id: widget::Id, card_inner_elements: Vec>, on_clear_all: Message, on_show_more: Option, diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index acb7ab06..7f72e9c4 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -716,7 +716,6 @@ where feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index d6595359..6f9d0210 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -137,7 +137,6 @@ impl<'a, Message: Clone + 'static> NavBar<'a, Message> { #[cfg(all( feature = "multi-window", feature = "wayland", - feature = "winit", target_os = "linux" ))] pub fn with_positioner( diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 11ee364d..fa4ce5b0 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -202,7 +202,6 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", - feature = "winit", target_os = "linux" ))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -267,7 +266,6 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", - feature = "winit", target_os = "linux" ))] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), @@ -901,7 +899,6 @@ where #[cfg(all( feature = "multi-window", feature = "wayland", - feature = "winit", target_os = "linux" ))] pub fn with_positioner( @@ -939,7 +936,6 @@ where feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] #[allow(clippy::too_many_lines)] @@ -1098,7 +1094,7 @@ where let rad = styling.menu_border_radius; /// Used to create a popup message from within a widget. - #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] + #[cfg(all(feature = "wayland", target_os = "linux"))] #[must_use] pub fn simple_popup( live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, @@ -1684,7 +1680,6 @@ where feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if is_pressed(event) @@ -1714,7 +1709,6 @@ where feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if let Some(on_activate) = self.on_activate.as_ref() { @@ -1780,7 +1774,6 @@ where feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!( @@ -1924,7 +1917,6 @@ where #[cfg(all( feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] { @@ -2564,7 +2556,6 @@ where feature = "multi-window", feature = "wayland", target_os = "linux", - feature = "winit", feature = "surface-message" ))] if matches!( From 2fe2d3528658d9e51395e761aaddd74e2c9d7bde Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Thu, 2 Jul 2026 19:11:38 +0200 Subject: [PATCH 133/201] yoda: cargo fix sweep post-rebase + iced submodule sweep bump MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Leyoda 2026 – GPLv3 --- iced | 2 +- src/app/cosmic.rs | 6 ++--- src/surface/action.rs | 2 +- src/theme/mod.rs | 3 +-- src/theme/style/iced.rs | 32 +++++++++++++-------------- src/theme/style/menu_bar.rs | 2 +- src/widget/cards.rs | 1 - src/widget/dnd_source.rs | 2 +- src/widget/id_container.rs | 2 +- src/widget/layer_container.rs | 2 +- src/widget/menu/menu_bar.rs | 4 +--- src/widget/menu/menu_column.rs | 6 ++--- src/widget/menu/menu_inner.rs | 16 +++++++------- src/widget/popover.rs | 2 +- src/widget/radio.rs | 2 +- src/widget/rectangle_tracker/mod.rs | 2 +- src/widget/responsive_container.rs | 2 +- src/widget/segmented_button/widget.rs | 4 ++-- src/widget/toaster/widget.rs | 4 ++-- src/widget/toggler.rs | 11 +++++---- src/widget/wayland/tooltip/widget.rs | 2 +- src/widget/wrapper.rs | 2 +- 22 files changed, 53 insertions(+), 58 deletions(-) diff --git a/iced b/iced index 362b4abb..1a57634a 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 362b4abb1558374686080db674b8f7c5868ae6ac +Subproject commit 1a57634add6e820c299f1c59664cd98098a9c348 diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 49013312..e29b9a99 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -158,7 +158,7 @@ where #[cfg(feature = "surface-message")] match _surface_message { #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::AppSubsurface(settings, live_settings, view) => { + crate::surface::Action::AppSubsurface(settings, _live_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 { @@ -185,7 +185,7 @@ where self.get_subsurface(settings, view.map(|v| *v)) } #[cfg(all(feature = "wayland", target_os = "linux"))] - crate::surface::Action::Subsurface(settings, live_settings, view) => { + crate::surface::Action::Subsurface(settings, _live_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 { @@ -1408,7 +1408,7 @@ impl Cosmic { { use iced_winit::platform_specific::commands::corner_radius::corner_radius; - let mut theme = THEME.lock().unwrap(); + let theme = THEME.lock().unwrap(); // TODO do we need per window sharp corners? let rounded = (!self.app.core().window.sharp_corners diff --git a/src/surface/action.rs b/src/surface/action.rs index 0314d1d5..2d86b007 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -4,7 +4,7 @@ use super::Action; use crate::Application; -use iced::{Rectangle, window}; +use iced::window; #[cfg(all(feature = "wayland", target_os = "linux"))] use iced_runtime::platform_specific::wayland::CornerRadius; #[cfg(all(feature = "wayland", target_os = "linux"))] diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 830746a1..1fd19cf3 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -8,9 +8,8 @@ pub mod portal; pub mod style; use ::iced::Alignment; -use cosmic_config::{CosmicConfigEntry, config_subscription}; +use cosmic_config::CosmicConfigEntry; use cosmic_theme::{Component, LayeredTheme, Spacing, ThemeMode}; -use iced_futures::Subscription; use iced_runtime::{Appearance, DefaultStyle}; use std::sync::{Arc, LazyLock, Mutex}; pub use style::*; diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index e4552226..38030e4b 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -832,7 +832,7 @@ impl menu::Catalog for Theme { fn default<'a>() -> ::Class<'a> {} - fn style(&self, class: &::Class<'_>) -> menu::Style { + fn style(&self, _class: &::Class<'_>) -> menu::Style { let cosmic = self.cosmic(); menu::Style { @@ -858,7 +858,7 @@ impl pick_list::Catalog for Theme { fn style( &self, - class: &::Class<'_>, + _class: &::Class<'_>, status: pick_list::Status, ) -> pick_list::Style { let cosmic = &self.cosmic(); @@ -899,7 +899,7 @@ impl radio::Catalog for Theme { fn default<'a>() -> Self::Class<'a> {} - fn style(&self, class: &Self::Class<'_>, status: radio::Status) -> radio::Style { + fn style(&self, _class: &Self::Class<'_>, status: radio::Status) -> radio::Style { let cur_container = self.current_container(); let theme = self.cosmic(); @@ -952,7 +952,7 @@ impl toggler::Catalog for Theme { fn default<'a>() -> Self::Class<'a> {} - fn style(&self, class: &Self::Class<'_>, status: toggler::Status) -> toggler::Style { + fn style(&self, _class: &Self::Class<'_>, status: toggler::Status) -> toggler::Style { let cosmic = self.cosmic(); const HANDLE_MARGIN: f32 = 2.0; let neutral_10 = cosmic.palette.neutral_10.with_alpha(0.1); @@ -980,8 +980,8 @@ impl toggler::Catalog for Theme { padding_ratio: 0.0, }; match status { - toggler::Status::Active { is_toggled } => active, - toggler::Status::Hovered { is_toggled } => { + toggler::Status::Active { is_toggled: _ } => active, + toggler::Status::Hovered { is_toggled: _ } => { let is_active = matches!(status, toggler::Status::Hovered { is_toggled: true }); toggler::Style { background: if is_active { @@ -1000,7 +1000,7 @@ impl toggler::Catalog for Theme { ..active } } - toggler::Status::Disabled { is_toggled } => { + toggler::Status::Disabled { is_toggled: _ } => { active.background = active.background.scale_alpha(0.5); active.foreground = active.foreground.scale_alpha(0.5); active @@ -1017,7 +1017,7 @@ impl pane_grid::Catalog for Theme { fn default<'a>() -> ::Class<'a> {} - fn style(&self, class: &::Class<'_>) -> pane_grid::Style { + fn style(&self, _class: &::Class<'_>) -> pane_grid::Style { let theme = self.cosmic(); pane_grid::Style { @@ -1188,8 +1188,8 @@ impl scrollable::Catalog for Theme { fn style(&self, class: &Self::Class<'_>, status: scrollable::Status) -> scrollable::Style { match status { scrollable::Status::Active { - is_horizontal_scrollbar_disabled, - is_vertical_scrollbar_disabled, + is_horizontal_scrollbar_disabled: _, + is_vertical_scrollbar_disabled: _, } => { let cosmic = self.cosmic(); let neutral_5 = cosmic.palette.neutral_5.with_alpha(0.7); @@ -1348,7 +1348,7 @@ impl svg::Catalog for Theme { Svg::default() } - fn style(&self, class: &Self::Class<'_>, status: svg::Status) -> svg::Style { + fn style(&self, class: &Self::Class<'_>, _status: svg::Status) -> svg::Style { #[allow(clippy::match_same_arms)] match class { Svg::Default => svg::Style::default(), @@ -1485,7 +1485,7 @@ impl text_input::Catalog for Theme { }, } } - text_input::Status::Focused { is_hovered } => { + text_input::Status::Focused { is_hovered: _ } => { let bg = self.current_container().small_widget.with_alpha(0.25); match class { @@ -1562,7 +1562,7 @@ 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: Color = cosmic.background(self.transparent).on.into(); + let _icon: Color = cosmic.background(self.transparent).on.into(); // TODO do we need to add icon color back? match status { @@ -1579,7 +1579,7 @@ impl iced_widget::text_editor::Catalog for Theme { value, selection, }, - iced_widget::text_editor::Status::Focused { is_hovered } => { + iced_widget::text_editor::Status::Focused { is_hovered: _ } => { iced_widget::text_editor::Style { background: iced::Color::from(cosmic.bg_color()).into(), border: Border { @@ -1682,8 +1682,8 @@ impl Base for 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, + crate::theme::ThemeType::Custom(_theme) => "Custom Cosmic Theme", + crate::theme::ThemeType::System { prefer_dark: _, theme } => &theme.name, } } } diff --git a/src/theme/style/menu_bar.rs b/src/theme/style/menu_bar.rs index 50263b14..37e14486 100644 --- a/src/theme/style/menu_bar.rs +++ b/src/theme/style/menu_bar.rs @@ -65,7 +65,7 @@ impl StyleSheet for Theme { fn appearance(&self, style: &Self::Style, is_overlay: bool) -> Appearance { let cosmic = self.cosmic(); let component = &cosmic.background(!is_overlay && self.transparent).component; - let mut bg = component.base; + let bg = component.base; match style { MenuBarStyle::Default => Appearance { diff --git a/src/widget/cards.rs b/src/widget/cards.rs index e66a3c01..5c8852a8 100644 --- a/src/widget/cards.rs +++ b/src/widget/cards.rs @@ -3,7 +3,6 @@ use std::time::Duration; use crate::anim; use crate::theme::THEME; -use crate::widget::card::style::Style; use crate::widget::icon::{self, Handle}; use crate::widget::{button, column, row, text}; use float_cmp::approx_eq; diff --git a/src/widget/dnd_source.rs b/src/widget/dnd_source.rs index 3cc9fa71..7b706478 100644 --- a/src/widget/dnd_source.rs +++ b/src/widget/dnd_source.rs @@ -6,7 +6,7 @@ use iced_core::window; use crate::Element; use crate::widget::{Id, Widget, container}; use iced::clipboard::dnd::{DndAction, DndEvent, SourceEvent}; -use iced::{Event, Length, Point, Rectangle, Vector, event, mouse, overlay}; +use iced::{Event, Length, Point, Rectangle, Vector, mouse, overlay}; use iced_core::widget::{Tree, tree}; use iced_core::{self, Clipboard, Shell, layout, renderer}; diff --git a/src/widget/id_container.rs b/src/widget/id_container.rs index 6b0d13ce..7c149203 100644 --- a/src/widget/id_container.rs +++ b/src/widget/id_container.rs @@ -1,4 +1,4 @@ -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::{Id, Operation, Tree}; use iced_core::{ Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay, diff --git a/src/widget/layer_container.rs b/src/widget/layer_container.rs index 3958b03c..91a6729c 100644 --- a/src/widget/layer_container.rs +++ b/src/widget/layer_container.rs @@ -1,7 +1,7 @@ use crate::Theme; use cosmic_theme::LayeredTheme; use iced::widget::Container; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::Tree; use iced_core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Vector, Widget, diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 7f72e9c4..01b91b59 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -18,10 +18,8 @@ use crate::Renderer; use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::style::menu_bar::StyleSheet; use crate::widget::RcWrapper; -use crate::widget::dropdown::menu::{self, State}; use crate::widget::menu::menu_inner::init_root_menu; -use iced::event::Status; use iced::{Point, Shadow, Vector, window}; use iced_core::Border; use iced_widget::core::layout::{Limits, Node}; @@ -768,7 +766,7 @@ where tree: &'b mut Tree, layout: Layout<'b>, _renderer: &Renderer, - viewport: &Rectangle, + _viewport: &Rectangle, translation: Vector, ) -> Option> { #[cfg(all( diff --git a/src/widget/menu/menu_column.rs b/src/widget/menu/menu_column.rs index ecb4a129..3e7455d6 100644 --- a/src/widget/menu/menu_column.rs +++ b/src/widget/menu/menu_column.rs @@ -1,11 +1,11 @@ //! Distribute content vertically. use crate::iced; use iced::core::alignment::{self, Alignment}; -use iced::core::event::{self, Event}; +use iced::core::event::Event; use iced::core::widget::{Operation, Tree}; use iced::core::{ Clipboard, Element, Layout, Length, Padding, Pixels, Rectangle, Shell, Size, Vector, Widget, - layout, mouse, overlay, renderer, widget, + layout, mouse, overlay, renderer, }; #[allow(missing_debug_implementations)] @@ -242,7 +242,7 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - for (((i, child), state), c_layout) in self + for (((_i, child), state), c_layout) in self .children .iter_mut() .enumerate() diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 04c418c4..d5e50b92 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -562,7 +562,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { shell: &mut Shell<'_, Message>, ) -> Option<(usize, MenuState)> { use event::Event::{Mouse, Touch}; - use event::Status::{Captured, Ignored}; + use mouse::Button::Left; use mouse::Event::{ButtonPressed, ButtonReleased, CursorMoved, WheelScrolled}; use touch::Event::{FingerLifted, FingerMoved, FingerPressed}; @@ -580,7 +580,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { 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 { + let _menu_roots = match &mut self.menu_roots { Cow::Borrowed(_) => panic!(), Cow::Owned(o) => o.as_mut_slice(), }; @@ -977,7 +977,7 @@ impl Widget, cursor: mouse::Cursor, @@ -995,7 +995,7 @@ impl Widget Widget Widget( ) where Message: Clone, { - use event::Status::{Captured, Ignored}; + use mouse::ScrollDelta; menu.tree.inner.with_data_mut(|state| { diff --git a/src/widget/popover.rs b/src/widget/popover.rs index 1f218cea..fb904248 100644 --- a/src/widget/popover.rs +++ b/src/widget/popover.rs @@ -4,7 +4,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::event::Event; use iced_core::widget::{Operation, Tree}; use iced_core::{ Clipboard, Element, Layout, Length, Point, Rectangle, Shell, Size, Vector, Widget, layout, diff --git a/src/widget/radio.rs b/src/widget/radio.rs index f8e174e1..edf5a3c7 100644 --- a/src/widget/radio.rs +++ b/src/widget/radio.rs @@ -1,7 +1,7 @@ //! Create choices using radio buttons. use crate::{Theme, theme}; use iced::border; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::tree::Tree; use iced_core::{ Border, Clipboard, Element, Layout, Length, Pixels, Rectangle, Shell, Size, Vector, Widget, diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index 839fa9a7..8863309b 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -5,7 +5,7 @@ use iced::futures::channel::mpsc::UnboundedSender; use iced::widget::Container; pub use subscription::*; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::Tree; use iced_core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, layout, diff --git a/src/widget/responsive_container.rs b/src/widget/responsive_container.rs index c8925cc2..b30bb848 100644 --- a/src/widget/responsive_container.rs +++ b/src/widget/responsive_container.rs @@ -1,7 +1,7 @@ //! Responsive Container, which will notify of size changes. use iced::{Limits, Size}; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::{Id, Operation, Tree, tree}; use iced_core::{ Clipboard, Element, Layout, Length, Rectangle, Shell, Vector, Widget, layout, mouse, overlay, diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index fa4ce5b0..49778367 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -9,7 +9,7 @@ use crate::widget::menu::{ self, CloseCondition, ItemHeight, ItemWidth, MenuBarState, PathHighlight, menu_roots_children, menu_roots_diff, }; -use crate::widget::{Icon, context_menu, icon}; +use crate::widget::{Icon, icon}; use crate::{Element, Renderer}; use derive_setters::Setters; use iced::clipboard::dnd::{ @@ -1683,7 +1683,7 @@ where feature = "surface-message" ))] if is_pressed(event) - && let Some(on_context) = self.on_context.as_ref() + && let Some(_on_context) = self.on_context.as_ref() { let (was_open, id) = state.menu_state.inner.with_data_mut(|data| { let was_open = data.open; diff --git a/src/widget/toaster/widget.rs b/src/widget/toaster/widget.rs index 922c1433..03bdf1bd 100644 --- a/src/widget/toaster/widget.rs +++ b/src/widget/toaster/widget.rs @@ -4,7 +4,7 @@ use iced::{Limits, Size}; use iced_core::layout::Node; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::renderer::{self}; use iced_core::widget::Operation; use iced_core::widget::tree::Tree; @@ -152,7 +152,7 @@ where translation, ) } else { - let bounds = layout.bounds(); + let _bounds = layout.bounds(); Some(overlay::Element::new(Box::new(ToasterOverlay::new( &mut state.children[1], diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 2254759e..6d17503c 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -1,18 +1,17 @@ //! Show toggle controls using togglers. -use std::time::{Duration, Instant}; +use std::time::Duration; use crate::{Element, anim}; use iced_core::renderer::{self, Renderer}; use iced_core::widget::{self, Tree, tree}; use iced_core::{ - Border, Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, - event, layout, mouse, text, touch, window, + Border, Clipboard, Event, Layout, Length, Pixels, Rectangle, Shell, Size, Widget, alignment, layout, mouse, text, touch, window, }; use iced_widget::Id; use iced_widget::toggler::Status; -pub use iced_widget::toggler::{Catalog, Style}; +pub use iced_widget::toggler::Catalog; pub fn toggler<'a, Message>(is_checked: bool) -> Toggler<'a, Message> { Toggler::new(is_checked) @@ -260,7 +259,7 @@ impl<'a, Message> Widget for Toggler<'a, shell.capture_event(); } } - Event::Window(window::Event::RedrawRequested(now)) => { + Event::Window(window::Event::RedrawRequested(_now)) => { state.anim.anim_done(self.duration); if state.anim.last_change.is_some() { shell.request_redraw(); @@ -370,7 +369,7 @@ impl<'a, Message> Widget for Toggler<'a, }, style.background, ); - let mut t = state.anim.t(self.duration, self.is_toggled); + let t = state.anim.t(self.duration, self.is_toggled); let toggler_foreground_bounds = Rectangle { x: bounds.x diff --git a/src/widget/wayland/tooltip/widget.rs b/src/widget/wayland/tooltip/widget.rs index a5e4520a..310491cb 100644 --- a/src/widget/wayland/tooltip/widget.rs +++ b/src/widget/wayland/tooltip/widget.rs @@ -301,7 +301,7 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> } let content_layout = layout.children().next().unwrap(); - let state = tree.state.downcast_ref::(); + let _state = tree.state.downcast_ref::(); let styling = theme.style(&self.style); diff --git a/src/widget/wrapper.rs b/src/widget/wrapper.rs index aee06e5a..27f75a3d 100644 --- a/src/widget/wrapper.rs +++ b/src/widget/wrapper.rs @@ -4,7 +4,7 @@ use std::rc::Rc; use std::thread::{self, ThreadId}; use crate::Element; -use iced::{Length, Rectangle, Size, event}; +use iced::{Length, Rectangle, Size}; use iced_core::id::Id; use iced_core::widget::tree; use iced_core::{Widget, widget}; From 9d01c86f25c813411a7631bb07d5c80a1738e05d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 2 Jul 2026 13:25:37 -0400 Subject: [PATCH 134/201] refactor(rectange tracker): relax Copy to Clone --- src/widget/rectangle_tracker/mod.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/widget/rectangle_tracker/mod.rs b/src/widget/rectangle_tracker/mod.rs index 1fb89ac3..71e44c86 100644 --- a/src/widget/rectangle_tracker/mod.rs +++ b/src/widget/rectangle_tracker/mod.rs @@ -5,7 +5,7 @@ use iced::futures::channel::mpsc::UnboundedSender; use iced::widget::Container; pub use subscription::*; -use iced_core::event::{self, Event}; +use iced_core::event::Event; use iced_core::widget::Tree; use iced_core::{ Alignment, Clipboard, Element, Layout, Length, Padding, Rectangle, Shell, Widget, layout, @@ -44,7 +44,7 @@ pub struct RectangleTracker { impl RectangleTracker where - I: Hash + Copy + Send + Sync + Debug, + I: Hash + Clone + Send + Sync + Debug, { pub fn container<'a, Message: 'static, T>( &self, @@ -185,7 +185,7 @@ impl<'a, Message, Renderer, I> Widget for RectangleTrackingContainer<'a, Message, Renderer, I> where Renderer: iced_core::Renderer, - I: 'a + Hash + Copy + Send + Sync + Debug, + I: 'a + Hash + Clone + Send + Sync + Debug, { fn children(&self) -> Vec { self.container.children() @@ -275,7 +275,7 @@ where cursor_position: mouse::Cursor, viewport: &Rectangle, ) { - let _ = self.tx.unbounded_send((self.id, layout.bounds())); + let _ = self.tx.unbounded_send((self.id.clone(), layout.bounds())); self.container.draw( tree, renderer, @@ -327,7 +327,7 @@ impl<'a, Message, Renderer, I> From, From ee5d965983957271cf759fd76e3e4eed4f188d35 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 2 Jul 2026 14:10:00 -0400 Subject: [PATCH 135/201] fix: disabled menu buttons don't need to be transparent --- src/theme/style/button.rs | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/theme/style/button.rs b/src/theme/style/button.rs index 723d1b6b..76798104 100644 --- a/src/theme/style/button.rs +++ b/src/theme/style/button.rs @@ -218,7 +218,12 @@ impl Catalog for crate::Theme { appearance(self, false, false, true, style, |component| { let mut background = Color::from(component.base); - background.a *= 0.5; + if !matches!( + style, + Button::MenuFolder | Button::MenuItem | Button::MenuRoot + ) { + background.a *= 0.5; + } ( background, Some(component.on_disabled.into()), From 3e4ab4576d66d27d842f0589e055fc10cb0fbb25 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Sat, 4 Jul 2026 19:41:28 +0200 Subject: [PATCH 136/201] fix(segmented_button): publish on_context without widget context_menu MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Upstream's popup refactor gated the on_context callback behind context_menu.is_some(), silencing consumers that provide their own dialog (cosmic-term tab rename). Publish the message unconditionally; keep menu state + popup creation gated on an actual context_menu. Leyoda 2026 – GPLv3 --- src/widget/segmented_button/widget.rs | 35 ++++++++++++++++----------- 1 file changed, 21 insertions(+), 14 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 49778367..8c3a4ae2 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -1752,20 +1752,25 @@ where } // Present a context menu on a right click event. - if self.context_menu.is_some() - && let Some(on_context) = self.on_context.as_ref() + // Yoda: on_context must fire even without a widget-side + // context_menu — consumers (cosmic-term tab rename) rely + // on the message alone and provide their own dialog. + if 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(); + if self.context_menu.is_some() { + state.show_context = Some(key); + state.context_cursor = + cursor_position.position().unwrap_or_default(); - state.menu_state.inner.with_data_mut(|data| { - // Clear stale MenuBounds from any previous context menu before opening a new one. - data.reset(); - data.open = true; - data.view_cursor = cursor_position; - }); + state.menu_state.inner.with_data_mut(|data| { + // Clear stale MenuBounds from any previous context menu before opening a new one. + data.reset(); + data.open = true; + data.view_cursor = cursor_position; + }); + } shell.publish(on_context(key)); shell.capture_event(); @@ -1776,10 +1781,12 @@ where target_os = "linux", feature = "surface-message" ))] - if matches!( - crate::app::cosmic::WINDOWING_SYSTEM.get(), - Some(crate::app::cosmic::WindowingSystem::Wayland) - ) { + if self.context_menu.is_some() + && matches!( + crate::app::cosmic::WINDOWING_SYSTEM.get(), + Some(crate::app::cosmic::WindowingSystem::Wayland) + ) + { self.create_popup( layout, cursor_position, From 9691870f7266543a2bf7308f11aeb325a44f12f2 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 6 Jul 2026 14:38:59 -0400 Subject: [PATCH 137/201] refactor: option for customizing the border padding, instead of disabling the content container --- src/app/mod.rs | 20 +++++++++----------- src/core.rs | 4 ++-- 2 files changed, 11 insertions(+), 13 deletions(-) diff --git a/src/app/mod.rs b/src/app/mod.rs index f757d5be..5ead69fe 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -625,21 +625,23 @@ impl ApplicationExt for App { let content_container = core.window.content_container; let show_context = core.window.show_context; let nav_bar_active = core.nav_bar_active(); - let transparent_header = core.window.transparent_header; let focused = core .focus_chain() .iter() - .any(|i| Some(*i) == self.core().main_window_id()); + .any(|i| Some(*i) == core.main_window_id()); - let border_padding = if maximized { 8 } else { 7 }; + let border_padding = core + .window + .border_padding + .unwrap_or(if maximized { 8 } else { 7 }); - let main_content_padding = if !content_container { - [0, 0, 0, 0] - } else { + let main_content_padding = if content_container { let right_padding = if show_context { 0 } else { border_padding }; let left_padding = if nav_bar_active { 0 } else { border_padding }; [0, right_padding, 0, left_padding] + } else { + [0, 0, 0, 0] }; let content_row = crate::widget::row::with_children({ @@ -834,11 +836,7 @@ impl ApplicationExt for App { let cosmic = theme.cosmic(); container::Style { background: Some(iced::Background::Color( - if transparent_header { - Color::TRANSPARENT - } else { - cosmic.background(theme.transparent).base.into() - }, + cosmic.background(theme.transparent).base.into(), )), border: iced::Border { radius: [ diff --git a/src/core.rs b/src/core.rs index 0ca555e3..b2bf8693 100644 --- a/src/core.rs +++ b/src/core.rs @@ -39,8 +39,8 @@ pub struct Window { pub show_close: bool, pub show_maximize: bool, pub show_minimize: bool, - pub transparent_header: bool, pub is_maximized: bool, + pub border_padding: Option, height: f32, width: f32, } @@ -170,9 +170,9 @@ impl Default for Core { show_minimize: true, show_window_menu: false, is_maximized: false, - transparent_header: false, height: 0., width: 0., + border_padding: None, }, focused_window: Vec::new(), #[cfg(feature = "applet")] From 2a325fa69a1d6e2c2fd344055454fbd70050eb63 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 6 Jul 2026 14:51:29 -0400 Subject: [PATCH 138/201] fix: update blur status after theme change --- src/app/cosmic.rs | 23 +++++++++++++++++++++++ 1 file changed, 23 insertions(+) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 2d5b10d3..c0e61843 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -1021,6 +1021,29 @@ impl Cosmic { cmds.push(corner_radius(*id, cur_rad).discard()); } + let blur = if new_blur { + iced::window::enable_blur + } else { + iced::window::disable_blur + }; + if self.app.core().blur(&cosmic_theme, None) { + cmds.push(blur( + self.app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED), + )); + } + for (id, wrapper, ..) in &self.surface_views { + let overriden = wrapper.2(&self.app); + if self.app.core().blur(&cosmic_theme, Some(wrapper.1)) + && overriden.blur.unwrap_or(true) + { + cmds.push(blur(*id)); + } else if overriden.blur.is_some_and(|b| !b) { + cmds.push(iced::window::disable_blur(*id)); + } + } return Task::batch(cmds); } } From d9b2b38bf4dc8f2af3d7dc7be9fa296ab75c3170 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 6 Jul 2026 18:49:36 -0400 Subject: [PATCH 139/201] fix: update iced popup blur & corner cleanup --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index c781ff61..b5b2b2fc 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit c781ff6199715f5650411c7ac625d4e1929bfdfb +Subproject commit b5b2b2fcc3e58be793624ba3a7dd45485813935a From 5056fc6f9bf4218a940ad64c03229983317ae578 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 8 Jul 2026 11:59:04 -0400 Subject: [PATCH 140/201] improv: overlap corner radius strip regions fixes handling of unfortunate scale values --- src/surface/corner_radius.rs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/surface/corner_radius.rs b/src/surface/corner_radius.rs index 1eda578d..595b4cf0 100644 --- a/src/surface/corner_radius.rs +++ b/src/surface/corner_radius.rs @@ -45,7 +45,7 @@ pub fn rounded_rect_strips(rect: Rectangle, radius: CornerRadius) -> Vec 1. { 2. } else { 1. }, }); } } @@ -62,9 +62,9 @@ pub fn rounded_rect_strips(rect: Rectangle, radius: CornerRadius) -> Vec 0.0 { out.push(Rectangle { x: strip_x, - y: rect.y + h - max_bottom as f32 + y as f32, + y: rect.y + h - max_bottom as f32 + y as f32 - if h > 1. { 1. } else { 0. }, width: strip_w, - height: 1.0, + height: if h > 1. { 2. } else { 1. }, }); } } From 74b2407342de7de1d9093a7dce34e4f58e6b53cd Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 8 Jul 2026 15:09:57 -0400 Subject: [PATCH 141/201] fix: avoid blurring subsurfaces on creation --- 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 c0e61843..daad0927 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -1683,7 +1683,7 @@ impl Cosmic { let live_settings_task = self.apply_live_settings( SurfaceIdWrapper::Subsurface(settings.id), &LiveSettings { - blur: Some(self.blur_enabled), + blur: None, corners: None, padding: None, }, From dc11d6a11f601692fb8fd67fdc99775f0a9eb899 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 8 Jul 2026 19:34:10 -0400 Subject: [PATCH 142/201] fix: no text should use the steps array for transparent color --- cosmic-theme/src/model/theme.rs | 35 +++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 13 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 9a80d31a..77f94c8f 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -11,7 +11,6 @@ use palette::color_difference::Wcag21RelativeContrast; use palette::rgb::Rgb; use palette::{IntoColor, Oklcha, Srgb, Srgba, WithAlpha}; use serde::{Deserialize, Serialize}; -use std::default; use std::num::NonZeroUsize; /// ID for the current dark `ThemeBuilder` config @@ -1190,7 +1189,7 @@ impl ThemeBuilder { ); let on_transparent_bg_component = get_text( color_index(transparent_bg_component, transparent_bg_steps_array.len()), - &transparent_bg_steps_array, + &step_array, &control_steps_array[8], text_steps_array.as_deref(), actual_alpha, @@ -1254,17 +1253,22 @@ impl ThemeBuilder { }; container_bg.alpha = (container_alpha + if is_dark { 0.3 } else { 0.25 }).min(1.0); - 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]); + let t_step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); + let base_index: usize = color_index(container_bg, t_step_array.len()); + let component_base = get_surface_color( + base_index, + 6, + &t_step_array, + is_dark, + &control_steps_array[3], + ); Container::new( Component::component( component_base, accent, get_text( - color_index(component_base, step_array.len()), + color_index(component_base, t_step_array.len()), &step_array, &control_steps_array[8], text_steps_array.as_deref(), @@ -1368,10 +1372,15 @@ 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]); + let t_step_array = steps(container_bg, NonZeroUsize::new(100).unwrap()); + let base_index = color_index(container_bg, t_step_array.len()); + let secondary_component = get_surface_color( + base_index, + 3, + &t_step_array, + is_dark, + &control_steps_array[4], + ); component_hovered_overlay = if base_index < 91 { control_steps_array[10] @@ -1388,7 +1397,7 @@ impl ThemeBuilder { secondary_component, accent, get_text( - color_index(secondary_component, step_array.len()), + color_index(secondary_component, t_step_array.len()), &step_array, &control_steps_array[8], text_steps_array.as_deref(), @@ -1561,7 +1570,7 @@ impl ThemeBuilder { transparent_bg, get_text( bg_index, - &transparent_bg_steps_array, + &step_array, &control_steps_array[8], text_steps_array.as_deref(), actual_alpha, From 8cb10bf6e24b86a7d2a9cf8c662d3e0154db1b21 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Thu, 9 Jul 2026 17:39:23 -0400 Subject: [PATCH 143/201] fix: config watcher hash & blur --- cosmic-config/src/dbus.rs | 2 ++ src/app/cosmic.rs | 18 +++++++++--------- 2 files changed, 11 insertions(+), 9 deletions(-) diff --git a/cosmic-config/src/dbus.rs b/cosmic-config/src/dbus.rs index 9b6e8697..76c03ad5 100644 --- a/cosmic-config/src/dbus.rs +++ b/cosmic-config/src/dbus.rs @@ -67,6 +67,8 @@ struct Wrapper( impl std::hash::Hash for Wrapper { fn hash(&self, state: &mut H) { self.0.hash(state); + self.2.hash(state); + self.3.hash(state); } } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index daad0927..ed81d416 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -941,7 +941,7 @@ impl Cosmic { } Action::SystemThemeChange(keys, mut theme) => { - let cur_is_dark = THEME.lock().unwrap().theme_type.is_dark(); + let cur_is_dark = self.app.core().system_theme_mode.is_dark; // Ignore updates if the current theme mode does not match. if cur_is_dark != theme.cosmic().is_dark { return iced::Task::none(); @@ -1026,14 +1026,14 @@ impl Cosmic { } else { iced::window::disable_blur }; - if self.app.core().blur(&cosmic_theme, None) { - cmds.push(blur( - self.app - .core() - .main_window_id() - .unwrap_or(window::Id::RESERVED), - )); - } + + cmds.push(blur( + self.app + .core() + .main_window_id() + .unwrap_or(window::Id::RESERVED), + )); + for (id, wrapper, ..) in &self.surface_views { let overriden = wrapper.2(&self.app); if self.app.core().blur(&cosmic_theme, Some(wrapper.1)) From a1003f24ec721fc1592b813a12e0f2d12d7ac414 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Thu, 9 Jul 2026 17:36:31 -0600 Subject: [PATCH 144/201] chore: update iced to fix corner radius --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index b5b2b2fc..5da11885 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit b5b2b2fcc3e58be793624ba3a7dd45485813935a +Subproject commit 5da1188557b58cdbe1951c72aad4360ebe05f5f4 From 5187dd6d1db7a1a97ffee962dbc1a87f72479f05 Mon Sep 17 00:00:00 2001 From: Eduardo Flores Date: Sat, 11 Jul 2026 21:06:25 -0700 Subject: [PATCH 145/201] fix(segmented_button): remove feature gate --- src/widget/segmented_button/widget.rs | 7 ------- 1 file changed, 7 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index f5ca21a8..ce5a6f21 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -1689,13 +1689,6 @@ where } } - #[cfg(all( - feature = "multi-window", - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] if let Some(on_activate) = self.on_activate.as_ref() { if is_pressed(event) { state.pressed_item = Some(Item::Tab(key)); From 18731e839265eeb534924b4a83e69a0e5043f32b Mon Sep 17 00:00:00 2001 From: name-is-hai <90458368+name-is-hai@users.noreply.github.com> Date: Mon, 13 Jul 2026 00:12:46 +0700 Subject: [PATCH 146/201] fix(desktop): XDG desktop entry precedence Resolve duplicate desktop IDs before applying desktop entry visibility rules This allows user desktop entries to override matching system entries and ensures Hidden, NoDisplay, and NotShowIn overrides do not reveal lower-priority entries Signed-off-by: name-is-hai <90458368+name-is-hai@users.noreply.github.com> --- src/desktop.rs | 266 ++++++++++++++++++++++++++++++++++++++++++------- 1 file changed, 228 insertions(+), 38 deletions(-) diff --git a/src/desktop.rs b/src/desktop.rs index cfa7a668..67002ca5 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -77,9 +77,12 @@ impl DesktopEntryCache { } 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(); + self.entries = desktop_entries_with_precedence( + fde::Iter::new(fde::default_paths()) + .filter_map(|p| fde::DesktopEntry::from_path(p, Some(&self.locales)).ok()), + ) + .filter(|entry| !entry.hidden()) + .collect(); } pub fn insert(&mut self, entry: fde::DesktopEntry) { @@ -604,28 +607,36 @@ fn candidate_desktop_ids(context: &DesktopLookupContext<'_>) -> Vec { ordered } +#[cfg(not(windows))] +fn desktop_entries_with_precedence<'a>( + entries: impl Iterator + 'a, +) -> impl Iterator + 'a { + let mut seen = HashSet::new(); + + entries.filter(move |entry| seen.insert(entry.id().to_owned())) +} + #[cfg(not(windows))] pub fn load_applications<'a>( locales: &'a [String], include_no_display: bool, only_show_in: Option<&'a str>, ) -> impl Iterator + 'a { - fde::Iter::new(fde::default_paths()) - .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_none_or( - |(xdg_current_desktop, only_show_in)| { - 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) - }, - ) - }) - .map(move |de| DesktopEntryData::from_desktop_entry(locales, de)) + desktop_entries_with_precedence( + fde::Iter::new(fde::default_paths()) + .filter_map(move |p| fde::DesktopEntry::from_path(p, Some(locales)).ok()), + ) + .filter(move |de| { + !de.hidden() + && (include_no_display || !de.no_display()) + && 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.zip(de.not_show_in()).is_none_or( + |(xdg_current_desktop, not_show_in)| !not_show_in.contains(&xdg_current_desktop), + ) + }) + .map(move |de| DesktopEntryData::from_desktop_entry(locales, de)) } // Create an iterator which filters desktop entries by app IDs. @@ -642,40 +653,51 @@ pub fn load_applications_for_app_ids<'a>( let app_ids = std::rc::Rc::new(std::cell::RefCell::new(app_ids)); let app_ids_ = app_ids.clone(); - let applications = iter + let applications = desktop_entries_with_precedence(iter) .filter(move |de| { + // Match and consume the requested ID before applying visibility + // filters. This prevents a Hidden or NoDisplay override from being + // recreated by fill_missing_ones. + let position = { + let requested = app_ids.borrow(); + + requested + .iter() + .position(|id| de.matches_id(fde::unicase::Ascii::new(*id))) + .or_else(|| { + requested + .iter() + .position(|id| de.matches_name(fde::unicase::Ascii::new(*id))) + }) + }; + + let Some(position) = position else { + return false; + }; + + app_ids.borrow_mut().remove(position); + + if de.hidden() { + return false; + } + if !include_no_display && de.no_display() { return false; } + if only_show_in.zip(de.only_show_in()).is_some_and( |(xdg_current_desktop, only_show_in)| !only_show_in.contains(&xdg_current_desktop), ) { 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 - .borrow() - .iter() - .position(|id| de.matches_id(fde::unicase::Ascii::new(*id))) - // Then fall back to search by name - .or_else(|| { - app_ids - .borrow() - .iter() - .position(|id| de.matches_name(fde::unicase::Ascii::new(*id))) - }) - // Remove the app ID if found - .map(|i| { - app_ids.borrow_mut().remove(i); - true - }) - .unwrap_or_default() + true }) .map(move |de| DesktopEntryData::from_desktop_entry(locales, de)); @@ -911,6 +933,174 @@ mod tests { entry } + #[test] + fn desktop_entry_precedence_prefers_first_entry() { + let locales = vec!["en_US.UTF-8".to_string()]; + + let user_entry = load_entry( + "com.example.App.desktop", + "[Desktop Entry]\n\ + Type=Application\n\ + Name=User Application\n\ + Exec=user-application\n", + &locales, + ); + + let system_entry = load_entry( + "com.example.App.desktop", + "[Desktop Entry]\n\ + Type=Application\n\ + Name=System Application\n\ + Exec=system-application\n", + &locales, + ); + + let entries = desktop_entries_with_precedence(vec![user_entry, system_entry].into_iter()) + .collect::>(); + + assert_eq!(entries.len(), 1); + assert_eq!(entries[0].exec(), Some("user-application")); + } + + #[test] + fn hidden_override_masks_system_entry_without_fallback() { + let locales = vec!["en_US.UTF-8".to_string()]; + + let user_entry = load_entry( + "com.example.App.desktop", + "[Desktop Entry]\n\ + Type=Application\n\ + Name=Example Application\n\ + Hidden=true\n", + &locales, + ); + + let system_entry = load_entry( + "com.example.App.desktop", + "[Desktop Entry]\n\ + Type=Application\n\ + Name=Example Application\n\ + Exec=system-application\n", + &locales, + ); + + let applications = load_applications_for_app_ids( + vec![user_entry, system_entry].into_iter(), + &locales, + vec!["com.example.App"], + true, + false, + Some("COSMIC"), + ) + .collect::>(); + + assert!(applications.is_empty()); + } + + #[test] + fn no_display_override_masks_system_entry() { + let locales = vec!["en_US.UTF-8".to_string()]; + + let user_entry = load_entry( + "com.example.App.desktop", + "[Desktop Entry]\n\ + Type=Application\n\ + Name=User Application\n\ + Exec=user-application\n\ + NoDisplay=true\n", + &locales, + ); + + let system_entry = load_entry( + "com.example.App.desktop", + "[Desktop Entry]\n\ + Type=Application\n\ + Name=System Application\n\ + Exec=system-application\n", + &locales, + ); + + let hidden_from_menu = load_applications_for_app_ids( + vec![user_entry.clone(), system_entry.clone()].into_iter(), + &locales, + vec!["com.example.App"], + true, + false, + Some("COSMIC"), + ) + .collect::>(); + + assert!(hidden_from_menu.is_empty()); + + let included_when_requested = load_applications_for_app_ids( + vec![user_entry, system_entry].into_iter(), + &locales, + vec!["com.example.App"], + true, + true, + Some("COSMIC"), + ) + .collect::>(); + + assert_eq!(included_when_requested.len(), 1); + assert_eq!( + included_when_requested[0].exec.as_deref(), + Some("user-application") + ); + } + + #[test] + fn not_show_in_override_masks_system_entry_for_cosmic() { + let locales = vec!["en_US.UTF-8".to_string()]; + + let user_entry = load_entry( + "com.example.App.desktop", + "[Desktop Entry]\n\ + Type=Application\n\ + Name=User Application\n\ + Exec=user-application\n\ + NotShowIn=COSMIC;\n", + &locales, + ); + + let system_entry = load_entry( + "com.example.App.desktop", + "[Desktop Entry]\n\ + Type=Application\n\ + Name=System Application\n\ + Exec=system-application\n", + &locales, + ); + + let hidden_in_cosmic = load_applications_for_app_ids( + vec![user_entry.clone(), system_entry.clone()].into_iter(), + &locales, + vec!["com.example.App"], + true, + false, + Some("COSMIC"), + ) + .collect::>(); + + assert!(hidden_in_cosmic.is_empty()); + + let visible_in_gnome = load_applications_for_app_ids( + vec![user_entry, system_entry].into_iter(), + &locales, + vec!["com.example.App"], + true, + false, + Some("GNOME"), + ) + .collect::>(); + + assert_eq!(visible_in_gnome.len(), 1); + assert_eq!( + visible_in_gnome[0].exec.as_deref(), + Some("user-application") + ); + } + #[test] fn candidate_generation_covers_common_variants() { let ctx = DesktopLookupContext::new("com.example.App.desktop") From fc5debccef41cd6bd6731cbc768cb719847d1a2f Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 13 Jul 2026 16:00:44 +0200 Subject: [PATCH 147/201] fix: matching desktop entries by CRX ID The existing test for this was broken, and the desktop entry cache was being refreshed before a match is fully checked. --- src/desktop.rs | 131 +++++++++++++++++++++++++------------------------ 1 file changed, 67 insertions(+), 64 deletions(-) diff --git a/src/desktop.rs b/src/desktop.rs index 67002ca5..2baa4d97 100644 --- a/src/desktop.rs +++ b/src/desktop.rs @@ -176,53 +176,58 @@ pub fn resolve_desktop_entry( options: &DesktopResolveOptions, ) -> fde::DesktopEntry { let app_id = fde::unicase::Ascii::new(context.app_id.as_ref()); + let resolve = |cache: &mut DesktopEntryCache| { + if let Some(entry) = fde::find_app_by_id(cache.entries(), app_id) { + return Some(entry.clone()); + } - if let Some(entry) = fde::find_app_by_id(cache.entries(), app_id) { + let candidate_ids = candidate_desktop_ids(context); + + if let Some(entry) = try_match_cached(cache.entries(), &candidate_ids) { + return Some(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 Some(entry); + } + + if let Some(entry) = match_startup_wm_class(cache.entries(), context) { + return Some(entry); + } + + // Chromium/CRX heuristic: scan exec/wmclass/icon for a CRX id match. + if let Some(entry) = match_crx_id(cache.entries(), context) { + return Some(entry); + } + + if let Some(entry) = match_exec_basename(cache.entries(), &candidate_ids) { + return Some(entry); + } + + if let Some(entry) = proton_or_wine_fallback(cache, context) { + cache.insert(entry.clone()); + return Some(entry); + } + + None + }; + + if let Some(entry) = resolve(cache) { 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 { + resolve(cache).unwrap_or_else(|| { let fallback = fallback_entry(context); cache.insert(fallback.clone()); fallback - } + }) } #[cfg(not(windows))] @@ -1166,14 +1171,13 @@ StartupWMClass=crx_jnpecgipniidlgicjocehkhajgdnjekh 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_contents = r#"[Desktop Entry] +Version=1.0 +Type=Application +Name=VMware Workstation +Exec=/usr/bin/vmware %U +Icon=vmware-workstation +"#; let desktop_path = apps_dir.join("vmware-workstation.desktop"); fs::write(desktop_path, desktop_contents).expect("write desktop file"); @@ -1287,30 +1291,29 @@ Icon=vmware-workstation\n\ #[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 id = "cadlkienfkclaiaibeoongdcgmdikeeg"; let mut cache = DesktopEntryCache::new(locales.clone()); - cache.refresh(); + cache.insert(fde::DesktopEntry::from_str( + "org.chromium.Chromium.flextop.chrome-cadlkienfkclaiaibeoongdcgmdikeeg-Default.desktop", + &format!( + r#"[Desktop Entry] +Type=Application +Name=Example +Exec=chromium --app-id={id} --profile-directory=Default +StartupWMClass=crx_{id} +Icon=chrome-{id}-Default +"# + ), + Some(&locales), + ) + .unwrap()); 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!(resolved.icon().is_some()); let expected = format!("crx_{}", id); assert_eq!(resolved.startup_wm_class(), Some(expected.as_str())); } From c53b43d41bfb487c9deba4cb2afe45f1eb9b0ae6 Mon Sep 17 00:00:00 2001 From: Aadil <211808495+Aadil127@users.noreply.github.com> Date: Mon, 13 Jul 2026 23:45:46 +0530 Subject: [PATCH 148/201] feat(color_picker): add mouse_interaction function (#1326) --- src/widget/color_picker/mod.rs | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index 4f9249dd..ab0f73fc 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -756,6 +756,23 @@ where fn size(&self) -> Size { Size::new(self.width, Length::Shrink) } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &crate::Renderer, + ) -> mouse::Interaction { + self.inner.as_widget().mouse_interaction( + &tree.children[0], + layout, + cursor, + viewport, + renderer, + ) + } } #[derive(Debug, Default)] From 96a82045d75b0f4eefcf1f668b305d8397c4f351 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 13 Jul 2026 20:06:45 +0200 Subject: [PATCH 149/201] fix(segmented_button): crash when enumerating context menu buttons --- src/widget/segmented_button/widget.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index ce5a6f21..fbefce34 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -970,11 +970,12 @@ where let Some((mut bounds, i)) = self .variant_bounds(state, layout.bounds()) - .enumerate() - .find_map(|(i, item)| match item { - ItemBounds::Button(e, bounds) if e == entity => Some((bounds, i)), + .filter_map(|item| match item { + ItemBounds::Button(entity, bounds) => Some((bounds, entity)), _ => None, }) + .enumerate() + .find_map(|(i, (bounds, e))| if e == entity { Some((bounds, i)) } else { None }) else { return; }; @@ -2529,11 +2530,12 @@ where let (mut bounds, i) = self .variant_bounds(state, layout.bounds()) - .enumerate() - .find_map(|(i, item)| match item { - ItemBounds::Button(e, bounds) if e == entity => Some((bounds, i)), + .filter_map(|item| match item { + ItemBounds::Button(entity, bounds) => Some((bounds, entity)), _ => None, - })?; + }) + .enumerate() + .find_map(|(i, (bounds, e))| if e == entity { Some((bounds, i)) } else { None })?; assert!( self.context_menu From 511384f6206527cf87369da67a9164831afbabef Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 13 Jul 2026 16:20:29 -0600 Subject: [PATCH 150/201] chore: update iced to pull in fixes for curved corners --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 5da11885..f6daee14 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 5da1188557b58cdbe1951c72aad4360ebe05f5f4 +Subproject commit f6daee14ac8f7cf090a5413ee1576aa59649a956 From 8e6aea650ac8e9c8b70405a6a4ac493a49bd72e3 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 15 Jul 2026 12:36:30 -0400 Subject: [PATCH 151/201] fix: feature gates --- Cargo.toml | 33 ++++----- build.rs | 1 + iced | 2 +- src/app/action.rs | 6 +- src/app/cosmic.rs | 97 +++++++++++++-------------- src/app/mod.rs | 8 +-- src/app/settings.rs | 4 +- src/core.rs | 4 +- src/lib.rs | 2 +- src/surface/action.rs | 34 +++++----- src/surface/mod.rs | 2 +- src/theme/style/mod.rs | 4 +- src/widget/autosize.rs | 2 +- src/widget/context_menu.rs | 42 ++---------- src/widget/dropdown/mod.rs | 2 +- src/widget/dropdown/widget.rs | 22 +++--- src/widget/menu/menu_bar.rs | 76 +++------------------ src/widget/menu/menu_inner.rs | 34 ++-------- src/widget/mod.rs | 2 +- src/widget/nav_bar.rs | 7 +- src/widget/responsive_menu_bar.rs | 4 +- src/widget/segmented_button/widget.rs | 62 +++-------------- src/widget/text_input/input.rs | 38 +++++------ 23 files changed, 161 insertions(+), 327 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index b02b6ca7..3b3871c7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -14,7 +14,7 @@ default = [ "a11y", "dbus-config", "x11", - "iced-wayland", + "wayland", "multi-window", ] advanced-shaping = ["iced/advanced-shaping"] @@ -72,6 +72,7 @@ serde-keycode = ["iced_core/serde"] single-instance = ["iced_winit/single-instance", "zbus/blocking-api", "ron"] # smol async runtime smol = ["dep:smol", "iced/smol", "zbus?/async-io", "rfd?/async-std"] +# Tokio async runtime tokio = [ "dep:tokio", "ashpd?/tokio", @@ -80,22 +81,18 @@ tokio = [ "zbus?/tokio", "cosmic-config/tokio", ] -# Tokio async runtime # Wayland window support -iced-wayland = [ - "ashpd?/wayland", +wayland = [ "autosize", + "surface-message", + "cctk", + "multi-window", + "winit", + "ashpd?/wayland", "iced/wayland", "iced_winit/wayland", - "surface-message", -] -wayland = [ - "iced-wayland", - "iced_runtime/cctk", - "iced_winit/cctk", - "iced_wgpu/cctk", - "iced/cctk", - "dep:cctk", + "iced_runtime/wayland", + "iced_wgpu/wayland", ] surface-message = [] # multi-window support @@ -127,7 +124,6 @@ ashpd = { version = "0.12.3", default-features = false, optional = true } async-fs = { version = "2.2", optional = true } async-std = { workspace = true, optional = true } auto_enums = "0.8.8" -cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "32283d7", optional = true } jiff = "0.2" cosmic-config = { path = "cosmic-config" } cosmic-settings-config = { git = "https://github.com/pop-os/cosmic-settings-daemon", optional = true } @@ -173,7 +169,8 @@ ron = { workspace = true, optional = true } enumflags2 = "0.7.12" # Enable DBus feature on Linux targets -[target.'cfg(target_os = "linux")'.dependencies] +[target.'cfg(all(unix, not(any(target_os = "redox", target_family = "wasm", target_os = "android", target_vendor = "apple"))))'.dependencies] +cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-client-toolkit", rev = "32283d7", optional = true } cosmic-config = { path = "cosmic-config", features = ["dbus"] } cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" } zbus.workspace = true @@ -261,3 +258,9 @@ zbus = { version = "5.15", default-features = false } [dev-dependencies] tempfile = "3.27.0" + +[workspace.package] +edition = "2024" + +[build-dependencies] +build_helpers = { path = "./iced/build_helpers" } diff --git a/build.rs b/build.rs index 4ce0aa9e..722d2c57 100644 --- a/build.rs +++ b/build.rs @@ -8,6 +8,7 @@ fn main() { { generate_bundled_icons(); } + build_helpers::cfg_aliases_setup(); } fn generate_bundled_icons() { diff --git a/iced b/iced index f6daee14..7177c92b 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit f6daee14ac8f7cf090a5413ee1576aa59649a956 +Subproject commit 7177c92bb0b10382acafdec1af7441312138c69e diff --git a/src/app/action.rs b/src/app/action.rs index a3b9dd6a..1308335e 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -5,7 +5,7 @@ use crate::config::CosmicTk; use crate::theme::Theme; use crate::widget::nav_bar; use crate::{keyboard_nav, surface}; -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(wayland_platform)] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; @@ -71,10 +71,10 @@ pub enum Action { /// Updates the tracked window geometry. WindowResize(iced::window::Id, f32, f32), /// Tracks updates to window state. - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] WindowState(iced::window::Id, WindowState), /// Capabilities the window manager supports - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] 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 ed81d416..f3f51e05 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -6,20 +6,20 @@ use std::collections::HashMap; use std::sync::Arc; use super::{Action, Application, ApplicationExt, Subscription}; -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(wayland_platform)] use crate::core::Auto; -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(wayland_platform)] use crate::surface::action::LiveSettings; use crate::theme::{THEME, Theme, ThemeType}; use crate::{Core, Element, keyboard_nav}; -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(wayland_platform)] use cctk::sctk::reexports::csd_frame::{WindowManagerCapabilities, WindowState}; use cosmic_theme::ThemeMode; -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(wayland_platform)] use enumflags2::BitFlags; -#[cfg(not(any(feature = "multi-window", feature = "wayland", target_os = "linux")))] +#[cfg(not(any(feature = "multi-window", wayland_platform)))] use iced::Application as IcedApplication; -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(wayland_platform)] use iced::event::wayland; use iced::{Task, theme, window}; use iced_futures::event::listen_with; @@ -158,7 +158,7 @@ where ) -> iced::Task> { #[cfg(feature = "surface-message")] match _surface_message { - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] crate::surface::Action::AppSubsurface(settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -185,7 +185,7 @@ where }); self.get_subsurface(settings, view.map(|v| *v)) } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] crate::surface::Action::Subsurface(settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -212,7 +212,7 @@ where self.get_subsurface(settings, None) } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] crate::surface::Action::AppPopup(settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -249,11 +249,11 @@ where self.get_popup(settings, *live_settings, view.map(|v| *v)) } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] crate::surface::Action::DestroyPopup(id) => { iced_winit::commands::popup::destroy_popup(id) } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] crate::surface::Action::DestroyTooltipPopup => { #[cfg(feature = "applet")] { @@ -264,11 +264,11 @@ where Task::none() } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] crate::surface::Action::DestroySubsurface(id) => { iced_winit::commands::subsurface::destroy_subsurface(id) } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] crate::surface::Action::DestroyWindow(id) => iced::window::close(id), crate::surface::Action::ResponsiveMenuBar { menu_bar, @@ -279,7 +279,7 @@ where core.menu_bars.insert(menu_bar, (limits, size)); iced::Task::none() } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] crate::surface::Action::Popup(settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -318,7 +318,7 @@ where self.get_popup(settings, live_settings, None) } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] crate::surface::Action::AppWindow(id, settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { s.downcast:: iced::window::Settings + Send + Sync>>() @@ -356,7 +356,7 @@ where self.get_window(id, settings, *live_settings, view.map(|v| *v)) } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] crate::surface::Action::Window(id, settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings).ok().and_then(|s| { s.downcast:: iced::window::Settings + Send + Sync>>() @@ -413,7 +413,7 @@ where crate::surface::Action::Task(f) => { f().map(|sm| crate::Action::Cosmic(Action::Surface(sm))) } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] crate::surface::Action::AppLayerShell(settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -451,7 +451,7 @@ where self.get_layer_shell(settings, *live_settings, view.map(|v| *v)) } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] crate::surface::Action::LayerShell(settings, live_settings, view) => { let Some(settings) = std::sync::Arc::try_unwrap(settings) .ok() @@ -491,7 +491,7 @@ where self.get_layer_shell(settings, live_settings, None) } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] crate::surface::Action::DestroyLayerShell(id) => { iced_winit::commands::layer_surface::destroy_layer_surface(id) } @@ -579,7 +579,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(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] iced::Event::PlatformSpecific(iced::event::PlatformSpecific::Wayland(event)) => { match event { wayland::Event::Popup(wayland::PopupEvent::Done, _, id) @@ -592,7 +592,7 @@ where ) => { return Some(Action::SuggestedBounds(b)); } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] wayland::Event::Window(iced::event::wayland::WindowEvent::WindowState( s, )) => { @@ -762,7 +762,7 @@ impl Cosmic { fn cosmic_update(&mut self, message: Action) -> iced::Task> { match message { Action::WindowMaximized(id, maximized) => { - #[cfg(not(all(feature = "wayland", target_os = "linux")))] + #[cfg(not(wayland_platform))] if self .app .core() @@ -792,7 +792,7 @@ impl Cosmic { }); } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] Action::WindowState(id, state) => { if self .app @@ -824,7 +824,7 @@ impl Cosmic { } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] Action::WmCapabilities(id, capabilities) => { if self .app @@ -911,7 +911,7 @@ impl Cosmic { guard.transparent = new_blur; drop(guard); - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] { let core = self.app.core(); let mut cmds = Vec::with_capacity(1 + self.surface_views.len()); @@ -986,7 +986,7 @@ impl Cosmic { cosmic_theme.set_theme(new_theme.theme_type); cosmic_theme.transparent = new_blur; - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] { use iced_winit::platform_specific::commands::corner_radius::corner_radius; @@ -1119,7 +1119,7 @@ impl Cosmic { if let ThemeType::System { .. } = cosmic_theme.theme_type { cosmic_theme.set_theme(new_theme.theme_type); cosmic_theme.transparent = new_blur; - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] { use iced_winit::platform_specific::commands::corner_radius::corner_radius; @@ -1193,7 +1193,7 @@ impl Cosmic { // Unminimize window before requesting to activate it. let mut task = iced_runtime::window::minimize(id, false); - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] { task = task.chain( iced_winit::platform_specific::commands::activation::activate( @@ -1204,7 +1204,7 @@ impl Cosmic { ); } - #[cfg(not(all(feature = "wayland", target_os = "linux")))] + #[cfg(not(wayland_platform))] { task = task.chain(iced_runtime::window::gain_focus(id)); } @@ -1301,7 +1301,7 @@ impl Cosmic { // Only apply update if the theme is set to load a system theme if let ThemeType::System { theme: _, .. } = cosmic_theme.theme_type { let mut cmds = Vec::with_capacity(1); - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] { let blur = if cosmic_theme.transparent { iced::window::enable_blur @@ -1378,12 +1378,7 @@ impl Cosmic { } Action::Focus(f) => { - #[cfg(all( - feature = "wayland", - feature = "multi-window", - feature = "surface-message", - target_os = "linux" - ))] + #[cfg(wayland_platform)] if let Some(( parent, SurfaceIdWrapper::Subsurface(_) | SurfaceIdWrapper::Popup(_), @@ -1428,7 +1423,7 @@ impl Cosmic { core.applet.suggested_bounds = b; } Action::Opened(id) => { - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] { use iced_winit::platform_specific::commands::corner_radius::corner_radius; @@ -1511,7 +1506,7 @@ impl Cosmic { } return iced_runtime::window::run_with_handle(id, init_windowing_system); } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] Action::BlurEnabled => { // TODO do this after blur event confirms support instead of for all wayland windows self.blur_enabled = true; @@ -1591,7 +1586,7 @@ impl Cosmic { if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { window::enable_blur(id) } else { - #[cfg(all(feature = "wayland", feature = "winit", target_os = "linux"))] + #[cfg(wayland_platform)] { iced_winit::commands::blur::blur( id, @@ -1602,7 +1597,7 @@ impl Cosmic { ) .discard() } - #[cfg(not(all(feature = "wayland", feature = "winit", target_os = "linux")))] + #[cfg(not(wayland_platform))] { iced::window::enable_blur(id) } @@ -1610,11 +1605,11 @@ impl Cosmic { } else if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { window::disable_blur(id) } else { - #[cfg(all(feature = "wayland", feature = "winit", target_os = "linux"))] + #[cfg(wayland_platform)] { iced_winit::commands::blur::blur(id, None).discard() } - #[cfg(not(all(feature = "wayland", feature = "winit", target_os = "linux")))] + #[cfg(not(wayland_platform))] { iced::window::disable_blur(id) } @@ -1624,7 +1619,7 @@ impl Cosmic { cmds.push(if matches!(id_wrapper, SurfaceIdWrapper::Window(_)) { window::enable_blur(id) } else { - #[cfg(all(feature = "wayland", feature = "winit", target_os = "linux"))] + #[cfg(wayland_platform)] { iced_winit::commands::blur::blur( id, @@ -1635,13 +1630,13 @@ impl Cosmic { ) .discard() } - #[cfg(not(all(feature = "wayland", feature = "winit", target_os = "linux")))] + #[cfg(not(wayland_platform))] { iced::window::enable_blur(id) } }); } - #[cfg(all(feature = "wayland", feature = "winit", target_os = "linux"))] + #[cfg(wayland_platform)] if let Some(corners) = live_settings.corners { cmds.push( iced_winit::commands::corner_radius::corner_radius(id, Some(corners)).discard(), @@ -1657,7 +1652,7 @@ impl Cosmic { ); } } - #[cfg(all(feature = "wayland", feature = "winit", target_os = "linux"))] + #[cfg(wayland_platform)] if let (SurfaceIdWrapper::LayerSurface(id), Some(padding)) = (id_wrapper, live_settings.padding) { @@ -1668,7 +1663,7 @@ impl Cosmic { Task::batch(cmds) } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] /// Create a subsurface pub fn get_subsurface( &mut self, @@ -1700,7 +1695,7 @@ impl Cosmic { Task::batch([live_settings_task, get_subsurface(settings)]) } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] /// Create a subsurface pub fn get_popup( &mut self, @@ -1728,7 +1723,7 @@ impl Cosmic { live_settings_task.chain(get_popup(settings)).discard() } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] /// Create a window surface pub fn get_window( &mut self, @@ -1765,7 +1760,7 @@ impl Cosmic { ]) } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] pub fn get_layer_shell( &mut self, settings: iced_runtime::platform_specific::wayland::layer_surface::SctkLayerSurfaceSettings, @@ -1794,7 +1789,7 @@ impl Cosmic { } } -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(wayland_platform)] fn corners( surface_type: SurfaceIdWrapper, rounded: bool, diff --git a/src/app/mod.rs b/src/app/mod.rs index 5ead69fe..d963b7dd 100644 --- a/src/app/mod.rs +++ b/src/app/mod.rs @@ -390,13 +390,7 @@ where crate::widget::nav_bar(nav_model, |id| crate::Action::Cosmic(Action::NavBar(id))) .on_context(|id| crate::Action::Cosmic(Action::NavBarContext(id))) .context_menu(self.nav_context_menu()); - #[cfg(all( - feature = "multi-window", - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(wayland_platform)] { nav = nav .window_id_maybe(self.core().main_window_id()) diff --git a/src/app/settings.rs b/src/app/settings.rs index 5c903f09..0c101741 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(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] 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(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] autosize: false, no_main_window: false, client_decorations: true, diff --git a/src/core.rs b/src/core.rs index b2bf8693..9b301fb5 100644 --- a/src/core.rs +++ b/src/core.rs @@ -521,7 +521,7 @@ impl Core { crate::command::toggle_maximize(id) } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] pub fn sync_window_border_radii_to_theme(&self) -> bool { match self.app_type { AppType::Window => self.auto_corner_radius.contains(Auto::Window), @@ -591,7 +591,7 @@ impl Core { /// Calculate suggested corners for each app type main window #[must_use] - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] pub fn corners( &self, theme: &Theme, diff --git a/src/lib.rs b/src/lib.rs index c1646e23..38230db5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -162,7 +162,7 @@ pub(crate) mod malloc; pub mod process; #[doc(inline)] -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(wayland_platform)] pub use cctk; pub mod surface; diff --git a/src/surface/action.rs b/src/surface/action.rs index 493c9ad0..6620c34f 100644 --- a/src/surface/action.rs +++ b/src/surface/action.rs @@ -6,33 +6,33 @@ use super::Action; use crate::Application; use iced::{Rectangle, window}; -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(all(wayland_platform, feature = "winit"))] use iced_runtime::platform_specific::wayland::CornerRadius; -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(wayland_platform)] use iced_runtime::platform_specific::wayland::layer_surface::IcedMargin; use std::any::Any; use std::sync::Arc; /// Used to produce a destroy popup message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(wayland_platform)] #[must_use] pub fn destroy_popup(id: iced_core::window::Id) -> Action { Action::DestroyPopup(id) } -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(wayland_platform)] #[must_use] pub fn destroy_subsurface(id: iced_core::window::Id) -> Action { Action::DestroySubsurface(id) } -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(wayland_platform)] #[must_use] pub fn destroy_window(id: iced_core::window::Id) -> Action { Action::DestroyWindow(id) } -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(wayland_platform)] #[must_use] pub fn destroy_layer_shell(id: iced_core::window::Id) -> Action { Action::DestroyLayerShell(id) @@ -40,17 +40,17 @@ pub fn destroy_layer_shell(id: iced_core::window::Id) -> Action { #[derive(Debug, Default, Copy, Clone)] pub struct LiveSettings { - #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] + #[cfg(wayland_platform)] /// Override the surface padding value for the surface type. pub padding: Option, - #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] + #[cfg(wayland_platform)] /// Override the default corner radius value for the surface type. pub corners: Option, /// Override the default blur setting for the surface type. pub blur: Option, } -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(wayland_platform)] type BoxedView = Option< Box< dyn Fn(&App) -> crate::Element<'_, crate::Action<::Message>> @@ -60,7 +60,7 @@ type BoxedView = Option< >, >; -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(wayland_platform)] #[must_use] pub fn app_window( live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static, @@ -92,7 +92,7 @@ pub fn app_window( } /// Used to create a window message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(wayland_platform)] #[must_use] pub fn simple_window( live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, @@ -129,7 +129,7 @@ pub fn simple_window( ) } -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(wayland_platform)] #[must_use] pub fn app_popup( live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static, @@ -162,7 +162,7 @@ pub fn app_popup( } /// Used to create a subsurface message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(wayland_platform)] #[must_use] pub fn simple_subsurface( settings: impl Fn() -> iced_runtime::platform_specific::wayland::subsurface::SctkSubsurfaceSettings @@ -192,7 +192,7 @@ pub fn simple_subsurface( } /// Used to create a popup message from within a widget. -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(wayland_platform)] #[must_use] pub fn simple_popup( live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, @@ -228,7 +228,7 @@ pub fn simple_popup( ) } -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(wayland_platform)] #[must_use] pub fn subsurface( settings: impl Fn( @@ -262,7 +262,7 @@ pub fn subsurface( ) } -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(wayland_platform)] #[must_use] pub fn simple_layer_shell( live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, @@ -298,7 +298,7 @@ pub fn simple_layer_shell( ) } -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(wayland_platform)] #[must_use] pub fn app_layer_shell( live_settings: impl Fn(&App) -> LiveSettings + Send + Sync + 'static, diff --git a/src/surface/mod.rs b/src/surface/mod.rs index b3858b97..b0a4a819 100644 --- a/src/surface/mod.rs +++ b/src/surface/mod.rs @@ -2,7 +2,7 @@ // SPDX-License-Identifier: MPL-2.0 pub mod action; -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(wayland_platform)] pub mod corner_radius; use iced::{Limits, Size, Task}; diff --git a/src/theme/style/mod.rs b/src/theme/style/mod.rs index bc648a73..b2a4d62e 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", target_os = "linux", feature = "winit"))] +#[cfg(wayland_platform)] pub mod tooltip; -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(wayland_platform)] pub use tooltip::Tooltip; diff --git a/src/widget/autosize.rs b/src/widget/autosize.rs index c52367db..83c4d97e 100644 --- a/src/widget/autosize.rs +++ b/src/widget/autosize.rs @@ -169,7 +169,7 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] if matches!( event, Event::PlatformSpecific(event::PlatformSpecific::Wayland( diff --git a/src/widget/context_menu.rs b/src/widget/context_menu.rs index 4b31fc01..f121bbe2 100644 --- a/src/widget/context_menu.rs +++ b/src/widget/context_menu.rs @@ -3,12 +3,7 @@ //! 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", - target_os = "linux", - feature = "winit", - feature = "surface-message" -))] +#[cfg(wayland_platform)] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::widget::menu::{ self, CloseCondition, Direction, ItemHeight, ItemWidth, MenuBarState, PathHighlight, @@ -64,12 +59,7 @@ pub struct ContextMenu<'a, Message> { } impl ContextMenu<'_, Message> { - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(wayland_platform)] #[allow(clippy::too_many_lines)] fn create_popup( &mut self, @@ -391,12 +381,7 @@ impl Widget state.active_root.clear(); state.open = false; - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(wayland_platform)] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && let Some(id) = state.popup_id.remove(&self.window_id) { @@ -435,12 +420,7 @@ impl Widget state.open = true; state.view_cursor = cursor; }); - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(wayland_platform)] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { self.create_popup(layout, cursor, renderer, shell, viewport, state); } @@ -457,12 +437,7 @@ impl Widget state.active_root.clear(); state.open = false; - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(wayland_platform)] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && let Some(id) = state.popup_id.remove(&self.window_id) { @@ -496,12 +471,7 @@ impl Widget _viewport: &iced::Rectangle, translation: Vector, ) -> Option> { - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(wayland_platform)] 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 b5fd4c06..d0ec175a 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", target_os = "linux"))] + #[cfg(wayland_platform)] 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 79f854fb..696e6c0d 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", target_os = "linux"))] + #[cfg(wayland_platform)] 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", target_os = "linux"))] + #[cfg(wayland_platform)] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), on_surface_action: None, action_map: None, } } - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] /// 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", target_os = "linux"))] + #[cfg(wayland_platform)] 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", target_os = "linux"))] + #[cfg(wayland_platform)] 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", target_os = "linux"))] + #[cfg(wayland_platform)] 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", target_os = "linux"))] + #[cfg(wayland_platform)] 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", target_os = "linux"))] + #[cfg(wayland_platform)] if let Some(((on_surface_action, parent), action_map)) = on_surface_action .as_ref() .zip(_window_id) @@ -661,7 +661,7 @@ pub fn update< state.close_operation = false; state.is_open.store(false, Ordering::SeqCst); if is_open { - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] if let Some(ref on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } @@ -684,7 +684,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", target_os = "linux"))] + #[cfg(wayland_platform)] if let Some(on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); } @@ -729,7 +729,7 @@ pub fn mouse_interaction(layout: Layout<'_>, cursor: mouse::Cursor) -> mouse::In } } -#[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] +#[cfg(wayland_platform)] /// 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 fce272e1..84927b67 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -9,13 +9,7 @@ use super::menu_inner::{ }; use super::menu_tree::MenuTree; use crate::Renderer; -#[cfg(all( - feature = "multi-window", - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" -))] +#[cfg(wayland_platform)] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::style::menu_bar::StyleSheet; use crate::widget::RcWrapper; @@ -191,12 +185,7 @@ pub struct MenuBar { menu_roots: Vec>, style: ::Style, window_id: window::Id, - #[cfg(all( - feature = "multi-window", - feature = "wayland", - feature = "winit", - target_os = "linux" - ))] + #[cfg(wayland_platform)] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, pub(crate) on_surface_action: Option Message + Send + Sync + 'static>>, @@ -231,12 +220,7 @@ where menu_roots, style: ::Style::default(), window_id: window::Id::RESERVED, - #[cfg(all( - feature = "multi-window", - feature = "wayland", - feature = "winit", - target_os = "linux" - ))] + #[cfg(wayland_platform)] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), on_surface_action: None, } @@ -330,12 +314,7 @@ where self } - #[cfg(all( - feature = "multi-window", - feature = "wayland", - feature = "winit", - target_os = "linux" - ))] + #[cfg(wayland_platform)] pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -367,13 +346,7 @@ where self } - #[cfg(all( - feature = "multi-window", - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(wayland_platform)] #[allow(clippy::too_many_lines)] fn create_popup( &mut self, @@ -657,12 +630,7 @@ where state.menu_states.clear(); state.active_root.clear(); state.open = false; - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(wayland_platform)] { let surface_action = self.on_surface_action.as_ref().unwrap(); shell.capture_event(); @@ -680,13 +648,7 @@ where return; } shell.capture_event(); - #[cfg(all( - feature = "multi-window", - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(wayland_platform)] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); } @@ -695,13 +657,7 @@ where if open && view_cursor.is_over(layout.bounds()) => { shell.capture_event(); - #[cfg(all( - feature = "multi-window", - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(wayland_platform)] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { self.create_popup(layout, view_cursor, renderer, shell, viewport, my_state); } @@ -732,13 +688,7 @@ where // draw path highlight if self.path_highlight.is_some() { let mut is_overlay = true; - #[cfg(all( - feature = "multi-window", - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(wayland_platform)] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && self.on_surface_action.is_some() && self.window_id != window::Id::NONE @@ -792,13 +742,7 @@ where viewport: &Rectangle, translation: Vector, ) -> Option> { - #[cfg(all( - feature = "multi-window", - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(wayland_platform)] 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 53c87415..c0d7f10a 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -6,13 +6,7 @@ use std::sync::Arc; use super::menu_bar::MenuBarState; use super::menu_tree::MenuTree; -#[cfg(all( - feature = "multi-window", - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" -))] +#[cfg(wayland_platform)] use crate::app::cosmic::{WINDOWING_SYSTEM, WindowingSystem}; use crate::style::menu_bar::StyleSheet; @@ -677,13 +671,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { needs_reset |= self.close_condition.click_outside && !is_inside; if needs_reset { - #[cfg(all( - feature = "multi-window", - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(wayland_platform)] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) && let Some(handler) = self.on_surface_action.as_ref() { @@ -990,13 +978,7 @@ impl Widget( }); } -#[cfg(all( - feature = "multi-window", - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" -))] +#[cfg(wayland_platform)] pub(super) fn init_root_popup_menu( menu: &mut Menu<'_, Message>, renderer: &crate::Renderer, @@ -1572,7 +1548,7 @@ where .as_ref() .is_some_and(|i| *i != new_index && !active_menu[*i].children.is_empty()); - #[cfg(all(feature = "multi-window", feature = "wayland",target_os = "linux", feature = "winit", feature = "surface-message"))] + #[cfg(wayland_platform)] 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 5089b997..ab1eef31 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -312,7 +312,7 @@ pub use toggler::{Toggler, toggler}; #[doc(inline)] pub use tooltip::{Tooltip, tooltip}; -#[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] +#[cfg(wayland_platform)] pub mod wayland; pub mod tooltip { diff --git a/src/widget/nav_bar.rs b/src/widget/nav_bar.rs index d6595359..14790866 100644 --- a/src/widget/nav_bar.rs +++ b/src/widget/nav_bar.rs @@ -134,12 +134,7 @@ impl<'a, Message: Clone + 'static> NavBar<'a, Message> { self } - #[cfg(all( - feature = "multi-window", - feature = "wayland", - feature = "winit", - target_os = "linux" - ))] + #[cfg(wayland_platform)] pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, diff --git a/src/widget/responsive_menu_bar.rs b/src/widget/responsive_menu_bar.rs index 2cae68d8..b7b416ef 100644 --- a/src/widget/responsive_menu_bar.rs +++ b/src/widget/responsive_menu_bar.rs @@ -23,7 +23,7 @@ impl Default for ResponsiveMenuBar { fn default() -> ResponsiveMenuBar { ResponsiveMenuBar { collapsed_item_width: { - #[cfg(all(feature = "winit", feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] if matches!( crate::app::cosmic::WINDOWING_SYSTEM.get(), Some(crate::app::cosmic::WindowingSystem::Wayland) @@ -32,7 +32,7 @@ impl Default for ResponsiveMenuBar { } else { ItemWidth::Static(84) } - #[cfg(not(all(feature = "winit", feature = "wayland", target_os = "linux")))] + #[cfg(not(wayland_platform))] { ItemWidth::Static(84) } diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index fbefce34..ca877348 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -195,12 +195,7 @@ where pub(super) on_reorder: Option Message + 'static>>, #[setters(skip)] window_id: window::Id, - #[cfg(all( - feature = "multi-window", - feature = "wayland", - feature = "winit", - target_os = "linux" - ))] + #[cfg(wayland_platform)] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, #[setters(skip)] pub(crate) on_surface_action: @@ -259,12 +254,7 @@ where on_drop_hint: None, on_reorder: None, window_id: window::Id::RESERVED, - #[cfg(all( - feature = "multi-window", - feature = "wayland", - feature = "winit", - target_os = "linux" - ))] + #[cfg(wayland_platform)] positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner::default(), on_surface_action: None, } @@ -883,12 +873,7 @@ where ) } - #[cfg(all( - feature = "multi-window", - feature = "wayland", - feature = "winit", - target_os = "linux" - ))] + #[cfg(wayland_platform)] pub fn with_positioner( mut self, positioner: iced_runtime::platform_specific::wayland::popup::SctkPositioner, @@ -920,13 +905,7 @@ where self } - #[cfg(all( - feature = "multi-window", - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(wayland_platform)] #[allow(clippy::too_many_lines)] fn create_popup<'b>( &mut self, @@ -1084,7 +1063,7 @@ where let rad = styling.menu_border_radius; /// Used to create a popup message from within a widget. - #[cfg(all(feature = "wayland", target_os = "linux", feature = "winit"))] + #[cfg(wayland_platform)] #[must_use] pub fn simple_popup( live_settings: impl Fn() -> LiveSettings + Send + Sync + 'static, @@ -1660,13 +1639,7 @@ where state.unfocus(); } - #[cfg(all( - feature = "multi-window", - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(wayland_platform)] if is_pressed(event) && let Some(on_context) = self.on_context.as_ref() { @@ -1722,13 +1695,7 @@ where shell.publish(on_context(key)); shell.capture_event(); - #[cfg(all( - feature = "multi-window", - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(wayland_platform)] if matches!( crate::app::cosmic::WINDOWING_SYSTEM.get(), Some(crate::app::cosmic::WindowingSystem::Wayland) @@ -1867,12 +1834,7 @@ where .inner .with_data_mut(|ms| ms.popup_id.remove(&self.window_id)) { - #[cfg(all( - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(wayland_platform)] { let surface_action = self.on_surface_action.as_ref().unwrap(); shell.capture_event(); @@ -2507,13 +2469,7 @@ where _viewport: &iced_core::Rectangle, translation: Vector, ) -> Option> { - #[cfg(all( - feature = "multi-window", - feature = "wayland", - target_os = "linux", - feature = "winit", - feature = "surface-message" - ))] + #[cfg(wayland_platform)] if matches!( crate::app::cosmic::WINDOWING_SYSTEM.get(), Some(crate::app::cosmic::WindowingSystem::Wayland) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index 65b84882..ece26192 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -525,7 +525,7 @@ where } /// Sets the start dnd handler of the [`TextInput`]. - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] 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 @@ -1553,7 +1553,7 @@ pub fn update<'a, Message: Clone + 'static>( click.kind(), state.cursor().state(value), ) { - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] (None, click::Kind::Single, cursor::State::Selection { start, end }) => { let left = start.min(end); let right = end.max(start); @@ -1694,7 +1694,7 @@ pub fn update<'a, Message: Clone + 'static>( | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => { cold(); let state = state(); - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] if matches!(state.dragging_state, Some(DraggingState::PrepareDnd(_))) { // clear selection and place cursor at click position update_cache(state, value); @@ -1749,7 +1749,7 @@ pub fn update<'a, Message: Clone + 'static>( shell.capture_event(); return; } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] 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)) @@ -2206,7 +2206,7 @@ pub fn update<'a, Message: Clone + 'static>( shell.request_redraw(); } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] Event::Dnd(DndEvent::Source(SourceEvent::Finished | SourceEvent::Cancelled)) => { cold(); let state = state(); @@ -2217,7 +2217,7 @@ pub fn update<'a, Message: Clone + 'static>( return; } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] Event::Dnd(DndEvent::Offer( rectangle, OfferEvent::Enter { @@ -2267,7 +2267,7 @@ pub fn update<'a, Message: Clone + 'static>( return; } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y })) if *rectangle == Some(dnd_id) => { @@ -2295,7 +2295,7 @@ pub fn update<'a, Message: Clone + 'static>( shell.capture_event(); return; } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Drop)) if *rectangle == Some(dnd_id) => { cold(); let state = state(); @@ -2313,9 +2313,9 @@ pub fn update<'a, Message: Clone + 'static>( return; } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] Event::Dnd(DndEvent::Offer(id, OfferEvent::LeaveDestination)) if Some(dnd_id) != *id => {} - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] Event::Dnd(DndEvent::Offer( rectangle, OfferEvent::Leave | OfferEvent::LeaveDestination, @@ -2333,7 +2333,7 @@ pub fn update<'a, Message: Clone + 'static>( shell.capture_event(); return; } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Data { data, mime_type })) if *rectangle == Some(dnd_id) => { @@ -2616,9 +2616,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(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] let handling_dnd_offer = !matches!(state.dnd_offer, DndOfferState::None); - #[cfg(not(all(feature = "wayland", target_os = "linux")))] + #[cfg(not(wayland_platform))] let handling_dnd_offer = false; let (cursors, offset, is_selecting) = if let Some(focus) = state.is_focused.filter(|f| f.focused).or_else(|| { @@ -2868,7 +2868,7 @@ pub fn mouse_interaction( #[derive(Debug, Clone)] pub struct TextInputString(pub String); -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(wayland_platform)] impl AsMimeTypes for TextInputString { fn available(&self) -> Cow<'static, [String]> { Cow::Owned( @@ -2892,13 +2892,13 @@ impl AsMimeTypes for TextInputString { #[derive(Debug, Clone, PartialEq)] pub(crate) enum DraggingState { Selection, - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] PrepareDnd(Point), - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] Dnd(DndAction, String), } -#[cfg(all(feature = "wayland", target_os = "linux"))] +#[cfg(wayland_platform)] #[derive(Debug, Default, Clone)] pub(crate) enum DndOfferState { #[default] @@ -2907,7 +2907,7 @@ pub(crate) enum DndOfferState { Dropped, } #[derive(Debug, Default, Clone)] -#[cfg(not(all(feature = "wayland", target_os = "linux")))] +#[cfg(not(wayland_platform))] pub(crate) struct DndOfferState; /// The state of a [`TextInput`]. @@ -2983,7 +2983,7 @@ impl State { } } - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(wayland_platform)] /// Returns the current value of the dragged text in the [`TextInput`]. #[must_use] pub fn dragged_text(&self) -> Option { From 63568a8d9c5885ca08eff54bd6d3e810a6d6c030 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 15 Jul 2026 15:24:46 -0400 Subject: [PATCH 152/201] improv: feature gate for xdg_portal --- Cargo.toml | 2 +- iced | 2 +- src/app/action.rs | 2 +- src/app/cosmic.rs | 8 ++++---- src/command.rs | 6 +++--- src/dialog/file_chooser/mod.rs | 4 ++-- src/dialog/file_chooser/open.rs | 30 +++++++++++++++--------------- src/dialog/file_chooser/save.rs | 32 ++++++++++++++++---------------- src/dialog/mod.rs | 2 +- src/lib.rs | 2 +- src/theme/mod.rs | 2 +- src/widget/dnd_destination.rs | 14 +++++++------- 12 files changed, 53 insertions(+), 53 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 3b3871c7..5268098d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -120,7 +120,6 @@ x11 = ["iced/x11", "iced_winit/x11"] [dependencies] apply = "0.3.0" -ashpd = { version = "0.12.3", default-features = false, optional = true } async-fs = { version = "2.2", optional = true } async-std = { workspace = true, optional = true } auto_enums = "0.8.8" @@ -174,6 +173,7 @@ cctk = { git = "https://github.com/pop-os/cosmic-protocols", package = "cosmic-c cosmic-config = { path = "cosmic-config", features = ["dbus"] } cosmic-settings-daemon = { git = "https://github.com/pop-os/dbus-settings-bindings" } zbus.workspace = true +ashpd = { version = "0.12.3", default-features = false, optional = true } [target.'cfg(all(unix, not(target_os = "macos")))'.dependencies] freedesktop-icons = { package = "cosmic-freedesktop-icons", git = "https://github.com/pop-os/freedesktop-icons" } diff --git a/iced b/iced index 7177c92b..7b5d58ec 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 7177c92bb0b10382acafdec1af7441312138c69e +Subproject commit 7b5d58ecbde79b105baf938e8065ab4ba4e5a35d diff --git a/src/app/action.rs b/src/app/action.rs index 1308335e..873622da 100644 --- a/src/app/action.rs +++ b/src/app/action.rs @@ -76,6 +76,6 @@ pub enum Action { /// Capabilities the window manager supports #[cfg(wayland_platform)] WmCapabilities(iced::window::Id, WindowManagerCapabilities), - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] DesktopSettings(crate::theme::portal::Desktop), } diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index f3f51e05..13e3ee83 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -678,7 +678,7 @@ where }) .map(crate::Action::Cosmic), window_events.map(crate::Action::Cosmic), - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] crate::theme::portal::desktop_settings() .map(Action::DesktopSettings) .map(crate::Action::Cosmic), @@ -1250,7 +1250,7 @@ impl Cosmic { return self.app.dbus_connection(conn); } - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] Action::DesktopSettings(crate::theme::portal::Desktop::ColorScheme(s)) => { use ashpd::desktop::settings::ColorScheme; if match THEME.lock().unwrap().theme_type { @@ -1332,7 +1332,7 @@ impl Cosmic { } } } - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] Action::DesktopSettings(crate::theme::portal::Desktop::Accent(c)) => { use palette::Srgba; let c = Srgba::new(c.red() as f32, c.green() as f32, c.blue() as f32, 1.0); @@ -1361,7 +1361,7 @@ impl Cosmic { } } } - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] Action::DesktopSettings(crate::theme::portal::Desktop::Contrast(_)) => { // TODO when high contrast is integrated in settings and all custom themes } diff --git a/src/command.rs b/src/command.rs index 6bb16e8d..64184f46 100644 --- a/src/command.rs +++ b/src/command.rs @@ -1,7 +1,7 @@ // Copyright 2023 System76 // SPDX-License-Identifier: MPL-2.0 -#[cfg(feature = "xdg-portal")] +#[cfg(xdg_portal)] use std::os::fd::AsFd; use iced::window; @@ -47,7 +47,7 @@ pub fn toggle_maximize(id: window::Id) -> iced::Task> { iced_runtime::window::toggle_maximize(id) } -#[cfg(feature = "xdg-portal")] +#[cfg(xdg_portal)] pub fn file_transfer_send( writeable: bool, auto_stop: bool, @@ -63,7 +63,7 @@ pub fn file_transfer_send( /// Receive the files offered over the xdg share portal using the `key`. /// Returns a list of file paths. -#[cfg(feature = "xdg-portal")] +#[cfg(xdg_portal)] pub fn file_transfer_receive(key: String) -> iced::Task>> { iced::Task::future(async move { let file_transfer = ashpd::documents::FileTransfer::new().await?; diff --git a/src/dialog/file_chooser/mod.rs b/src/dialog/file_chooser/mod.rs index 186f7625..d1b385a0 100644 --- a/src/dialog/file_chooser/mod.rs +++ b/src/dialog/file_chooser/mod.rs @@ -90,7 +90,7 @@ pub mod open; /// Save file dialog. pub mod save; -#[cfg(feature = "xdg-portal")] +#[cfg(xdg_portal)] pub use ashpd::desktop::file_chooser::{Choice, FileFilter}; use thiserror::Error; @@ -139,7 +139,7 @@ pub enum Error { UrlAbsolute, } -#[cfg(feature = "xdg-portal")] +#[cfg(xdg_portal)] pub type DialogError = ashpd::Error; #[cfg(feature = "rfd")] diff --git a/src/dialog/file_chooser/open.rs b/src/dialog/file_chooser/open.rs index f24afda9..c15a5b91 100644 --- a/src/dialog/file_chooser/open.rs +++ b/src/dialog/file_chooser/open.rs @@ -6,7 +6,7 @@ //! Check out the [open-dialog](https://github.com/pop-os/libcosmic/tree/master/examples/open-dialog) //! example in our repository. -#[cfg(feature = "xdg-portal")] +#[cfg(xdg_portal)] pub use portal::{FileResponse, MultiFileResponse, file, files, folder, folders}; #[cfg(feature = "rfd")] @@ -24,7 +24,7 @@ pub struct Dialog { title: String, /// The label for the accept button. Mnemonic underlines are allowed. - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] #[setters(skip)] accept_label: Option, @@ -39,17 +39,17 @@ pub struct Dialog { file_name: Option, /// Modal dialogs require user input before continuing the program. - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] #[setters(skip)] modal: bool, /// Adds a list of choices. - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] #[setters(skip)] choices: Vec, /// Specifies the default file filter. - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] #[setters(skip)] current_filter: Option, @@ -62,36 +62,36 @@ impl Dialog { pub const fn new() -> Self { Self { title: String::new(), - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] accept_label: None, directory: None, file_name: None, - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] modal: true, - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] current_filter: None, - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] choices: Vec::new(), filters: Vec::new(), } } /// The label for the accept button. Mnemonic underlines are allowed. - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] pub fn accept_label(mut self, label: impl Into) -> Self { self.accept_label = Some(label.into()); self } /// Adds a choice. - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] pub fn choice(mut self, choice: impl Into) -> Self { self.choices.push(choice.into()); self } /// Specifies the default file filter. - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] pub fn current_filter(mut self, filter: impl Into) -> Self { self.current_filter = Some(filter.into()); self @@ -104,7 +104,7 @@ impl Dialog { } /// Modal dialogs require user input before continuing the program. - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] pub fn modal(mut self, modal: bool) -> Self { self.modal = modal; self @@ -131,7 +131,7 @@ impl Dialog { } } -#[cfg(feature = "xdg-portal")] +#[cfg(xdg_portal)] mod portal { use super::Dialog; use crate::dialog::file_chooser::Error; @@ -147,7 +147,7 @@ mod portal { } /// Creates a new file dialog, and begins to await its responses. - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] pub async fn create( dialog: super::Dialog, folders: bool, diff --git a/src/dialog/file_chooser/save.rs b/src/dialog/file_chooser/save.rs index d7a2a34e..aa8c1398 100644 --- a/src/dialog/file_chooser/save.rs +++ b/src/dialog/file_chooser/save.rs @@ -6,7 +6,7 @@ //! Check out the [open-dialog](https://github.com/pop-os/libcosmic/tree/master/examples/open-dialog) //! example in our repository. -#[cfg(feature = "xdg-portal")] +#[cfg(xdg_portal)] pub use portal::{Response, file}; #[cfg(feature = "rfd")] @@ -23,12 +23,12 @@ pub struct Dialog { title: String, /// The label for the accept button. Mnemonic underlines are allowed. - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] #[setters(skip)] accept_label: Option, /// Modal dialogs require user input before continuing the program. - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] #[setters(skip)] modal: bool, @@ -41,17 +41,17 @@ pub struct Dialog { directory: Option, /// Sets the absolute path of the file - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] #[setters(skip)] current_file: Option, /// Adds a list of choices. - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] #[setters(skip)] choices: Vec, /// Specifies the default file filter. - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] #[setters(skip)] current_filter: Option, @@ -64,38 +64,38 @@ impl Dialog { pub const fn new() -> Self { Self { title: String::new(), - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] accept_label: None, - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] modal: true, file_name: None, directory: None, - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] current_file: None, - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] current_filter: None, - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] choices: Vec::new(), filters: Vec::new(), } } /// The label for the accept button. Mnemonic underlines are allowed. - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] pub fn accept_label(mut self, label: impl Into) -> Self { self.accept_label = Some(label.into()); self } /// Adds a choice. - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] pub fn choice(mut self, choice: impl Into) -> Self { self.choices.push(choice.into()); self } /// Set the current file filter. - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] pub fn current_filter(mut self, filter: impl Into) -> Self { self.current_filter = Some(filter.into()); self @@ -108,7 +108,7 @@ impl Dialog { } /// Modal dialogs require user input before continuing the program. - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] pub fn modal(mut self, modal: bool) -> Self { self.modal = modal; self @@ -126,7 +126,7 @@ impl Default for Dialog { } } -#[cfg(feature = "xdg-portal")] +#[cfg(xdg_portal)] mod portal { use super::Dialog; use crate::dialog::file_chooser::Error; diff --git a/src/dialog/mod.rs b/src/dialog/mod.rs index 66b3cec7..d67b7f3b 100644 --- a/src/dialog/mod.rs +++ b/src/dialog/mod.rs @@ -3,7 +3,7 @@ //! Create dialogs for retrieving user input. -#[cfg(feature = "xdg-portal")] +#[cfg(xdg_portal)] pub use ashpd; pub mod file_chooser; diff --git a/src/lib.rs b/src/lib.rs index 38230db5..c44700e5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -136,7 +136,7 @@ pub use dbus_activation::DbusActivation; #[cfg(feature = "desktop")] pub mod desktop; -#[cfg(any(feature = "xdg-portal", feature = "rfd"))] +#[cfg(any(xdg_portal, feature = "rfd"))] pub mod dialog; pub mod executor; diff --git a/src/theme/mod.rs b/src/theme/mod.rs index 830746a1..65ea9314 100644 --- a/src/theme/mod.rs +++ b/src/theme/mod.rs @@ -3,7 +3,7 @@ //! Contains the [`Theme`] type and its widget stylesheet implementations. -#[cfg(feature = "xdg-portal")] +#[cfg(xdg_portal)] pub mod portal; pub mod style; diff --git a/src/widget/dnd_destination.rs b/src/widget/dnd_destination.rs index 0ebff3dc..54d77808 100644 --- a/src/widget/dnd_destination.rs +++ b/src/widget/dnd_destination.rs @@ -29,7 +29,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"; -#[cfg(feature = "xdg-portal")] +#[cfg(xdg_portal)] pub const FILE_TRANSFER_MIME: &str = "application/vnd.portal.filetransfer"; #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] @@ -64,7 +64,7 @@ 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")] + #[cfg(xdg_portal)] on_file_transfer: Option Message>>, } @@ -92,7 +92,7 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { on_action_selected: None, on_data_received: None, on_finish: None, - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] on_file_transfer: None, } } @@ -119,7 +119,7 @@ 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")] + #[cfg(xdg_portal)] on_file_transfer: None, } } @@ -156,7 +156,7 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { on_action_selected: None, on_data_received: None, on_finish: None, - #[cfg(feature = "xdg-portal")] + #[cfg(xdg_portal)] on_file_transfer: None, } } @@ -239,7 +239,7 @@ impl<'a, Message: 'static> DndDestination<'a, Message> { /// 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")] + #[cfg(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") { @@ -514,7 +514,7 @@ impl Widget data.len() ); - #[cfg(feature = "xdg-portal")] + #[cfg(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()) From 6359a9465b8fe4c3b0ada0cd2b87cddf644ca732 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 15 Jul 2026 16:47:16 -0400 Subject: [PATCH 153/201] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 7b5d58ec..a7c71f8d 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 7b5d58ecbde79b105baf938e8065ab4ba4e5a35d +Subproject commit a7c71f8dd3914e9f83363bf7552c99cbbb62ab2c From 16d148807c663071ac13f70b4b0cfa7816465046 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 13 Jul 2026 17:56:29 -0600 Subject: [PATCH 154/201] fix: clear paste cache after shift+insert --- src/widget/text_input/input.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index ece26192..c86c5a3e 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -2111,6 +2111,9 @@ pub fn update<'a, Message: Clone + 'static>( keyboard::Key::Character(c) if "v" == c => { state.is_pasting = None; } + keyboard::Key::Named(keyboard::key::Named::Insert) => { + state.is_pasting = None; + } keyboard::Key::Named(keyboard::key::Named::Tab) | keyboard::Key::Named(keyboard::key::Named::ArrowUp) | keyboard::Key::Named(keyboard::key::Named::ArrowDown) => { From ac74ed51d9fa4492d354e7a83261cb39297699cb 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 Jul 2026 19:25:08 +0200 Subject: [PATCH 155/201] fix(circular): calculate bar height based on the size of the circular This calculates the `bar_height` based on the size of the Circular by default, so that ones with a small size don't have visual issues caused by a fixed height. Also fixes the `track_radius` calculation, which caused the widget to appear smaller than it should be. --- src/widget/progress_bar/circular.rs | 27 ++++++++++++++------------- 1 file changed, 14 insertions(+), 13 deletions(-) diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs index 3318d7a3..8da4be14 100644 --- a/src/widget/progress_bar/circular.rs +++ b/src/widget/progress_bar/circular.rs @@ -18,7 +18,7 @@ where Theme: StyleSheet, { size: f32, - bar_height: f32, + bar_height: Option, style: Theme::Style, cycle_duration: Duration, period: Duration, @@ -32,8 +32,8 @@ where /// Creates a new [`Circular`] with the given content. pub fn new() -> Self { Circular { - size: 40.0, - bar_height: 4.0, + size: 48.0, + bar_height: None, style: Theme::Style::default(), cycle_duration: Duration::from_millis(1500), period: Duration::from_secs(2), @@ -48,8 +48,9 @@ where } /// Sets the bar height of the [`Circular`]. + /// By default, the height is based on the size of the [`Circular`]. pub fn bar_height(mut self, bar_height: f32) -> Self { - self.bar_height = bar_height; + self.bar_height = Some(bar_height); self } @@ -172,41 +173,41 @@ where let custom_style = Theme::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 bar_height = self.bar_height.unwrap_or((frame.width() / 12.0).max(2.0)); + let track_radius = (frame.width() - bar_height) / 2.0; if track_radius <= 0.0 { return; } - let track_path = canvas::Path::circle(frame.center(), track_radius); + 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), + .with_width(bar_height), ); - // Converts a track fraction to an angle in radians, with 0 being top of circle - let to_angle = |t: f32| t * 2.0 * PI - PI / 2.0; let draw_bar = |frame: &mut canvas::Frame, start: f32, end: f32| { let mut builder = canvas::path::Builder::new(); builder.arc(canvas::path::Arc { center: frame.center(), radius: track_radius, - start_angle: Radians(to_angle(start)), - end_angle: Radians(to_angle(end)), + start_angle: Radians(2.0 * PI * start - PI / 2.0), + end_angle: Radians(2.0 * PI * end - PI / 2.0), }); frame.stroke( &builder.build(), canvas::Stroke::default() .with_color(custom_style.bar_color) - .with_width(self.bar_height) + .with_width(bar_height) .with_line_cap(canvas::LineCap::Round), ); }; if self.progress.is_some() { if let Some(border_color) = custom_style.border_color { - for radius_offset in [self.bar_height / 2.0, -(self.bar_height / 2.0)] { + // - 0.5 ensures the border is inside the track + for radius_offset in [bar_height / 2.0 - 0.5, -(bar_height / 2.0 - 0.5)] { let border_path = canvas::Path::circle(frame.center(), track_radius + radius_offset); frame.stroke( From 66263a76d36ec446c03d19c57ac0030fd4785c2b 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 Jul 2026 13:14:18 +0200 Subject: [PATCH 156/201] fix(toggler): toggle on release --- src/widget/toggler.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index 2dad2ed9..a610747c 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -249,8 +249,8 @@ impl<'a, Message> Widget for Toggler<'a, } 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 { .. }) => { let mouse_over = cursor_position.is_over(layout.bounds()); if mouse_over { From 0eef591171c6c449b99fe0966571cd3ea3507919 Mon Sep 17 00:00:00 2001 From: Adil Hanney Date: Fri, 17 Jul 2026 15:58:48 +0100 Subject: [PATCH 157/201] fix(cosmic-theme): remove kdeglobals theme generation support --- cosmic-theme/src/output/mod.rs | 4 - cosmic-theme/src/output/qt_output.rs | 531 ++---------------- ...put__tests__dark_default_kcolorscheme.snap | 157 ------ ...ut__tests__light_default_kcolorscheme.snap | 157 ------ 4 files changed, 52 insertions(+), 797 deletions(-) delete mode 100644 cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap delete mode 100644 cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap diff --git a/cosmic-theme/src/output/mod.rs b/cosmic-theme/src/output/mod.rs index 04dce39f..93824111 100644 --- a/cosmic-theme/src/output/mod.rs +++ b/cosmic-theme/src/output/mod.rs @@ -34,10 +34,8 @@ impl Theme { /// 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); gtk_res?; - qt_res?; qt56ct_res?; Ok(()) } @@ -46,10 +44,8 @@ impl Theme { /// 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(); let qt56ct_res = self.write_qt56ct(); gtk_res?; - qt_res?; qt56ct_res?; Ok(()) } diff --git a/cosmic-theme/src/output/qt_output.rs b/cosmic-theme/src/output/qt_output.rs index 84614e7b..92dfad9f 100644 --- a/cosmic-theme/src/output/qt_output.rs +++ b/cosmic-theme/src/output/qt_output.rs @@ -1,280 +1,21 @@ use crate::Theme; use configparser::ini::Ini; -use cosmic_config::CosmicConfigEntry; -use palette::blend::Compose; -use palette::{Mix, Srgba}; -use std::fs::{self, File}; -use std::io::{self, Write}; -use std::path::{Path, PathBuf}; +use std::path::PathBuf; +use std::{fs, io}; -use super::{OutputError, qt_settings_ini_style}; +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://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_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, - 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 window_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.on, - 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 view_colors = IniColors { - background_alternate: self.background.component.base.mix(self.accent.base, 0.05), - background_normal: self.background.component.base, - ..window_colors - }; - - // selected text and items - let selection_colors = { - // 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, - decoration_focus: selected, - decoration_hover: selected, - foreground_active: selected_text, - foreground_inactive: selected_text.mix(selected, 0.5), - foreground_link: self.link_button.base, - 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 if cfg!(test) { - // For reproducible results in tests, use the default dark theme - Theme::dark_default() - } else { - Theme::dark_config() - .ok() - .as_ref() - .and_then(|conf| Theme::get_entry(conf).ok()) - .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.on, - 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 = &window_colors; - let header_colors_inactive = &window_colors; - // tool tips, "What's This" tips, and similar elements - let tooltip_colors = &view_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(&window_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 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(kcolorscheme.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_kcolorscheme_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 - .pretty_write(kdeglobals_file, &qt_settings_ini_style()) - .map_err(OutputError::Io)?; - Ok(()) - } - - /// Reset the applied qt colors by removing color scheme values from the - /// `~/.config/kdeglobals` file. + /// Reset the applied qt colors: + /// - Remove relevant keys from the `~/.config/kdeglobals` file + /// - Delete `~/.local/share/color-schemes/CosmicDark.colors` and `CosmicLight.colors` /// /// This does not restore the backed up kdeglobals file. /// /// # Errors /// - /// Returns an `OutputError` if there is an error resetting the CSS file. + /// Returns an `OutputError` if there is an error resetting the kdeglobals + /// file or deleting the color schemes. #[cold] pub fn reset_qt() -> Result<(), OutputError> { let Some(config_dir) = dirs::config_dir() else { @@ -291,19 +32,29 @@ widgetStyle=qt6ct-style return Ok(()); } - let is_dark = false; // doesn't matter since we're only reading keys - let src_file = Self::get_kcolorscheme_path(is_dark)?; - let src_ini = Self::read_ini(&src_file)?; + let light_scheme = Self::get_kcolorscheme_path(false)?; + let dark_scheme = Self::get_kcolorscheme_path(true)?; + if light_scheme.exists() { + let src_ini = Self::read_ini(&light_scheme)?; - for (section, key_value) in src_ini.get_map_ref() { - for (key, _) in key_value { - kdeglobals_ini.remove_key(section, key); + // Remove color scheme keys from kdeglobals + for (section, key_value) in src_ini.get_map_ref() { + for key in key_value.keys() { + kdeglobals_ini.remove_key(section, key); + } } + + kdeglobals_ini + .write(kdeglobals_file) + .map_err(OutputError::Io)?; } - kdeglobals_ini - .write(kdeglobals_file) - .map_err(OutputError::Io)?; + // Delete now-unused kcolorscheme files + let delete_light_res = fs::remove_file(&light_scheme).map_err(OutputError::Io); + let delete_dark_res = fs::remove_file(&dark_scheme).map_err(OutputError::Io); + delete_light_res?; + delete_dark_res?; + Ok(()) } @@ -312,6 +63,7 @@ widgetStyle=qt6ct-style let Some(mut data_dir) = dirs::data_dir() else { return Err(OutputError::MissingDataDir); }; + data_dir.push("color-schemes"); let file_name = if is_dark { "CosmicDark.colors" @@ -319,11 +71,6 @@ widgetStyle=qt6ct-style "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)) } @@ -338,15 +85,6 @@ widgetStyle=qt6ct-style 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::copy(path, &backup_path)?; - } - Ok(()) - } - #[cold] fn is_cosmic_kdeglobals(ini: &Ini) -> io::Result> { let color_scheme = ini.get("General", "ColorScheme"); @@ -360,208 +98,43 @@ widgetStyle=qt6ct-style } } -/// 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, - } - } -} - #[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"); + fn test_is_cosmic_kdeglobals_dark() -> io::Result<()> { + let mut ini = Ini::new_cs(); + ini.setstr("General", "ColorScheme", Some("CosmicDark")); + let is_cosmic = Theme::is_cosmic_kdeglobals(&ini)?; + assert_eq!(is_cosmic, Some(true)); + Ok(()) } #[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"); + fn test_is_cosmic_kdeglobals_light() -> io::Result<()> { + let mut ini = Ini::new_cs(); + ini.setstr("General", "ColorScheme", Some("CosmicLight")); + let is_cosmic = Theme::is_cosmic_kdeglobals(&ini)?; + assert_eq!(is_cosmic, Some(true)); + Ok(()) } #[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"); + fn test_is_cosmic_kdeglobals_breeze() -> io::Result<()> { + let mut ini = Ini::new_cs(); + ini.setstr("General", "ColorScheme", Some("BreezeDark")); + let is_cosmic = Theme::is_cosmic_kdeglobals(&ini)?; + assert_eq!(is_cosmic, Some(false)); + Ok(()) } #[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); + fn test_is_cosmic_kdeglobals_blank() -> io::Result<()> { + let mut ini = Ini::new_cs(); + ini.setstr("General", "ColorScheme", None); + let is_cosmic = Theme::is_cosmic_kdeglobals(&ini)?; + assert_eq!(is_cosmic, None); + Ok(()) } } 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 deleted file mode 100644 index c50f95dc..00000000 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__dark_default_kcolorscheme.snap +++ /dev/null @@ -1,157 +0,0 @@ ---- -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 deleted file mode 100644 index 40363f95..00000000 --- a/cosmic-theme/src/output/snapshots/cosmic_theme__output__qt_output__tests__light_default_kcolorscheme.snap +++ /dev/null @@ -1,157 +0,0 @@ ---- -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=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 -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,153 -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 ef162b8e16ba4493e05c169cd56c7b9f77f0fda5 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Sun, 19 Jul 2026 04:48:42 +0200 Subject: [PATCH 158/201] feat(icon): add opacity method --- src/widget/icon/mod.rs | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/widget/icon/mod.rs b/src/widget/icon/mod.rs index 031b4b0c..5685e448 100644 --- a/src/widget/icon/mod.rs +++ b/src/widget/icon/mod.rs @@ -25,6 +25,7 @@ pub fn icon(handle: Handle) -> Icon { handle, height: None, size: 16, + opacity: 1.0, class: crate::theme::Svg::default(), rotation: None, width: None, @@ -45,6 +46,7 @@ pub struct Icon { class: crate::theme::Svg, #[setters(skip)] pub(super) size: u16, + opacity: f32, content_fit: ContentFit, #[setters(strip_option)] width: Option, @@ -65,7 +67,6 @@ impl Icon { None } - #[must_use] pub fn size(mut self, size: u16) -> Self { self.size = size; self @@ -83,6 +84,7 @@ impl Icon { self.height .unwrap_or_else(|| Length::Fixed(f32::from(self.size))), ) + .opacity(self.opacity) .rotation(self.rotation.unwrap_or_default()) .content_fit(self.content_fit) .into() @@ -99,6 +101,7 @@ impl Icon { self.height .unwrap_or_else(|| Length::Fixed(f32::from(self.size))), ) + .opacity(self.opacity) .rotation(self.rotation.unwrap_or_default()) .content_fit(self.content_fit) .symbolic(self.handle.symbolic) From c994f94ea9e0c2046931c30e6d082bf6116f3f21 Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Thu, 23 Jul 2026 18:51:12 +0200 Subject: [PATCH 159/201] 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: Isaac Subirana Co-authored-by: Nara Díaz Viñolas Co-authored-by: Prasanta-Hembram Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/ca/ Translation: Pop OS/libcosmic --- i18n/ca/libcosmic.ftl | 3 +++ i18n/sat/libcosmic.ftl | 0 2 files changed, 3 insertions(+) create mode 100644 i18n/sat/libcosmic.ftl diff --git a/i18n/ca/libcosmic.ftl b/i18n/ca/libcosmic.ftl index e69de29b..0fbc5ca5 100644 --- a/i18n/ca/libcosmic.ftl +++ b/i18n/ca/libcosmic.ftl @@ -0,0 +1,3 @@ +license = Llicència +close = Tancar +links = Enllaços diff --git a/i18n/sat/libcosmic.ftl b/i18n/sat/libcosmic.ftl new file mode 100644 index 00000000..e69de29b From 7e777077b81e3a1b9c7b723f35399e80f1b2cba5 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 15 Jul 2026 19:40:09 -0600 Subject: [PATCH 160/201] fix: gate frosted transparency on auto_blur --- src/app/cosmic.rs | 54 ++++++----------------------------------------- src/core.rs | 28 ++++++++++++++++++------ 2 files changed, 27 insertions(+), 55 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 13e3ee83..eddf34bd 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -896,14 +896,7 @@ impl Cosmic { } } - let new_blur = self.blur_enabled && { - let t = theme.cosmic(); - match self.app.core().app_type() { - crate::core::AppType::Window => t.frosted_windows, - crate::core::AppType::System => t.frosted_system_interface, - crate::core::AppType::Applet => t.frosted_applets, - } - }; + let new_blur = self.blur_enabled && self.app.core().frosted(theme.cosmic()); theme.transparent = new_blur; let mut guard = THEME.lock().unwrap(); @@ -947,14 +940,7 @@ impl Cosmic { return iced::Task::none(); } // update transparent - let new_blur = self.blur_enabled && { - let t = theme.cosmic(); - match self.app.core().app_type() { - crate::core::AppType::Window => t.frosted_windows, - crate::core::AppType::System => t.frosted_system_interface, - crate::core::AppType::Applet => t.frosted_applets, - } - }; + let new_blur = self.blur_enabled && self.app.core().frosted(theme.cosmic()); theme.transparent = new_blur; let cmd = self.app.system_theme_update(&keys, theme.cosmic()); @@ -1101,14 +1087,7 @@ impl Cosmic { } else { new_theme }; - let new_blur = self.blur_enabled && { - let t = new_theme.cosmic(); - match core.app_type() { - crate::core::AppType::Window => t.frosted_windows, - crate::core::AppType::System => t.frosted_system_interface, - crate::core::AppType::Applet => t.frosted_applets, - } - }; + let new_blur = self.blur_enabled && core.frosted(new_theme.cosmic()); new_theme.transparent = new_blur; core.system_theme = new_theme.clone(); @@ -1283,14 +1262,7 @@ impl Cosmic { crate::theme::system_light() }; if let ThemeType::System { .. } = new_theme.theme_type { - let new_blur = self.blur_enabled && { - let t = new_theme.cosmic(); - match core.app_type() { - crate::core::AppType::Window => t.frosted_windows, - crate::core::AppType::System => t.frosted_system_interface, - crate::core::AppType::Applet => t.frosted_applets, - } - }; + let new_blur = self.blur_enabled && core.frosted(new_theme.cosmic()); new_theme.transparent = new_blur; } core.system_theme = new_theme.clone(); @@ -1441,14 +1413,7 @@ impl Cosmic { SurfaceIdWrapper::Popup(_) | SurfaceIdWrapper::LayerSurface(_) ) }); - let new_blur = self.blur_enabled && { - let t = theme.cosmic(); - match self.app.core().app_type() { - crate::core::AppType::Window => t.frosted_windows, - crate::core::AppType::System => t.frosted_system_interface, - crate::core::AppType::Applet => t.frosted_applets, - } - }; + let new_blur = self.blur_enabled && self.app.core().frosted(theme.cosmic()); let wrapper = self.surface_views.get(&id).map(|s| s.1); @@ -1512,14 +1477,7 @@ impl Cosmic { self.blur_enabled = true; let mut t = THEME.lock().unwrap(); - let new_blur = self.blur_enabled && { - let t = t.cosmic(); - match self.app.core().app_type() { - crate::core::AppType::Window => t.frosted_windows, - crate::core::AppType::System => t.frosted_system_interface, - crate::core::AppType::Applet => t.frosted_applets, - } - }; + let new_blur = self.blur_enabled && self.app.core().frosted(t.cosmic()); t.transparent = new_blur; diff --git a/src/core.rs b/src/core.rs index 9b301fb5..a3483b63 100644 --- a/src/core.rs +++ b/src/core.rs @@ -538,6 +538,20 @@ impl Core { self.auto_blur } + /// Whether the active theme's frosted (blur) styling should apply to this + /// app's surfaces. + #[must_use] + pub fn frosted(&self, theme: &cosmic_theme::Theme) -> bool { + if self.auto_blur.is_empty() { + return false; + } + match self.app_type { + AppType::Window => theme.frosted_windows, + AppType::System => theme.frosted_system_interface, + AppType::Applet => theme.frosted_applets, + } + } + pub fn set_auto_corner_radius(&mut self, auto_corner_radius: BitFlags) { self.auto_corner_radius = auto_corner_radius; } @@ -570,13 +584,13 @@ impl Core { Some(SurfaceIdWrapper::Window(_)) => { theme.frosted_windows && self.auto_blur.contains(Auto::Window) } - Some(SurfaceIdWrapper::Popup(_)) - if matches!(self.app_type, AppType::Window | AppType::System) => - { - theme.frosted_windows && self.auto_blur.contains(Auto::Popup) - } - Some(SurfaceIdWrapper::Popup(_)) if matches!(self.app_type, AppType::Applet) => { - theme.frosted_applets && self.auto_blur.contains(Auto::Popup) + Some(SurfaceIdWrapper::Popup(_)) => { + self.auto_blur.contains(Auto::Popup) + && match self.app_type { + AppType::Window => theme.frosted_windows, + AppType::System => theme.frosted_system_interface, + AppType::Applet => theme.frosted_applets, + } } None => match self.app_type { AppType::Window => theme.frosted_windows && self.auto_blur.contains(Auto::Window), From 28f7ab4651cbafbd9c6edc1bb6ce8763b638f52c Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Sat, 25 Jul 2026 22:20:01 +0200 Subject: [PATCH 161/201] fix(segmented_button): crash when enumerating context menu buttons Cherry-pick of upstream fix (pop-os/libcosmic#1354). The .enumerate().find_map() pattern counted ALL items (buttons + dividers) but the resulting index was used to access a children array containing only button entries. When a divider preceded the target button (e.g. trash in nav bar), the index was off-by-N, causing an out-of-bounds panic and crashing cosmic-files. Fix: filter_map first to keep only buttons, then enumerate. --- src/widget/segmented_button/widget.rs | 16 +++++++++------- 1 file changed, 9 insertions(+), 7 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 8c3a4ae2..61808fbe 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -981,11 +981,12 @@ where let Some((mut bounds, i)) = self .variant_bounds(state, layout.bounds()) - .enumerate() - .find_map(|(i, item)| match item { - ItemBounds::Button(e, bounds) if e == entity => Some((bounds, i)), + .filter_map(|item| match item { + ItemBounds::Button(entity, bounds) => Some((bounds, entity)), _ => None, }) + .enumerate() + .find_map(|(i, (bounds, e))| if e == entity { Some((bounds, i)) } else { None }) else { return; }; @@ -2581,11 +2582,12 @@ where let (mut bounds, i) = self .variant_bounds(state, layout.bounds()) - .enumerate() - .find_map(|(i, item)| match item { - ItemBounds::Button(e, bounds) if e == entity => Some((bounds, i)), + .filter_map(|item| match item { + ItemBounds::Button(entity, bounds) => Some((bounds, entity)), _ => None, - })?; + }) + .enumerate() + .find_map(|(i, (bounds, e))| if e == entity { Some((bounds, i)) } else { None })?; assert!( self.context_menu From bdb273389d4134db9564d3aa0363f5efd468deb3 Mon Sep 17 00:00:00 2001 From: Michael Aaron Murphy Date: Mon, 27 Jul 2026 14:11:20 +0200 Subject: [PATCH 162/201] chore: update iced with slider handle improvements --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index a7c71f8d..65b5dbc6 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit a7c71f8dd3914e9f83363bf7552c99cbbb62ab2c +Subproject commit 65b5dbc67172208b0f9af1cf8a5a72f4017734cc From 1f6dc991eaa5115a09be2505cc8a323e5b2b0bff Mon Sep 17 00:00:00 2001 From: Aadil <211808495+Aadil127@users.noreply.github.com> Date: Mon, 27 Jul 2026 17:46:25 +0530 Subject: [PATCH 163/201] fix(slider): adjust handle bounds and size for iced widget slider update --- src/theme/style/iced.rs | 27 +++++---------------------- src/widget/color_picker/mod.rs | 4 +++- 2 files changed, 8 insertions(+), 23 deletions(-) diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 97246369..0a09a179 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -775,12 +775,12 @@ impl slider::Catalog for Theme { handle: slider::Handle { shape: slider::HandleShape::Rectangle { - height: 20, - width: 20, + height: 26, + width: 26, border_radius: cosmic.corner_radii.radius_m.into(), }, border_color: Color::TRANSPARENT, - border_width: 0.0, + border_width: 3.0, background: Background::Color(cosmic.accent.base.into()), }, @@ -795,12 +795,6 @@ impl slider::Catalog for Theme { slider::Status::Active => appearance, slider::Status::Hovered => match class { Slider::Standard => { - appearance.handle.shape = slider::HandleShape::Rectangle { - height: 26, - width: 26, - border_radius: cosmic.corner_radii.radius_m.into(), - }; - appearance.handle.border_width = 3.0; appearance.handle.border_color = self.cosmic().palette.neutral_10.with_alpha(0.1).into(); appearance @@ -809,20 +803,9 @@ impl slider::Catalog for Theme { }, slider::Status::Dragged => match class { Slider::Standard => { - let mut style = { - appearance.handle.shape = slider::HandleShape::Rectangle { - height: 26, - width: 26, - border_radius: cosmic.corner_radii.radius_m.into(), - }; - appearance.handle.border_width = 3.0; - appearance.handle.border_color = - self.cosmic().palette.neutral_10.with_alpha(0.1).into(); - appearance - }; - style.handle.border_color = + appearance.handle.border_color = self.cosmic().palette.neutral_10.with_alpha(0.2).into(); - style + appearance } Slider::Custom { dragging, .. } => dragging(self), }, diff --git a/src/widget/color_picker/mod.rs b/src/widget/color_picker/mod.rs index ab0f73fc..4041c934 100644 --- a/src/widget/color_picker/mod.rs +++ b/src/widget/color_picker/mod.rs @@ -360,7 +360,9 @@ where }) .step(4.0 / 17.0) .shift_step(64.0 / 17.0) - .width(self.width), + .width(self.width) + .handle_height(16) + .handle_width(16), text_input("", self.input_color) .on_input(move |s| on_update(ColorPickerUpdate::Input(s))) .on_paste(move |s| on_update(ColorPickerUpdate::Input(s))) From 0ca1b784c931c946517379795ce87328c6a902b8 Mon Sep 17 00:00:00 2001 From: vacenty <193441458+vacenty@users.noreply.github.com> Date: Thu, 16 Jul 2026 00:52:57 +0200 Subject: [PATCH 164/201] feat: Add option for frozen glass in maximized windows --- cosmic-theme/src/model/theme.rs | 9 +++++++++ src/app/cosmic.rs | 16 ++++++++-------- 2 files changed, 17 insertions(+), 8 deletions(-) diff --git a/cosmic-theme/src/model/theme.rs b/cosmic-theme/src/model/theme.rs index 77f94c8f..5974523b 100644 --- a/cosmic-theme/src/model/theme.rs +++ b/cosmic-theme/src/model/theme.rs @@ -121,6 +121,9 @@ pub struct Theme { /// frosted applet popups pub frosted_applets: bool, #[serde(default)] + /// frosted apps when maximized + pub frosted_maximized_apps: bool, + #[serde(default)] /// alpha map pub alpha_map: AlphaMap, /// shade color for dialogs @@ -909,6 +912,9 @@ pub struct ThemeBuilder { /// frosted applet popups pub frosted_applets: bool, #[serde(default)] + /// frosted apps when maximized + pub frosted_maximized_apps: bool, + #[serde(default)] /// alpha map pub alpha_map: AlphaMap, } @@ -937,6 +943,7 @@ impl Default for ThemeBuilder { frosted_system_interface: false, frosted_panel: false, frosted_applets: false, + frosted_maximized_apps: false, alpha_map: AlphaMap::default(), } } @@ -1084,6 +1091,7 @@ impl ThemeBuilder { frosted_system_interface, frosted_panel, frosted_applets, + frosted_maximized_apps, alpha_map, } = self; @@ -1556,6 +1564,7 @@ impl ThemeBuilder { frosted_system_interface, frosted_panel, frosted_applets, + frosted_maximized_apps, alpha_map, transparent_background: Container::new( Component::component( diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index eddf34bd..d31b6088 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -549,16 +549,16 @@ where 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::style(theme.borrow()) } else { let theme = THEME.lock().unwrap(); - - theme::Style { - background_color: iced_core::Color::TRANSPARENT, - icon_color: theme.cosmic().on_bg_color().into(), - text_color: theme.cosmic().on_bg_color().into(), + if self.app.core().window.is_maximized && !theme.cosmic().frosted_maximized_apps { + crate::style::iced::application::style(theme.borrow()) + } else { + theme::Style { + background_color: iced_core::Color::TRANSPARENT, + icon_color: theme.cosmic().on_bg_color().into(), + text_color: theme.cosmic().on_bg_color().into(), + } } } } From abaa06f10f4c03f6122d984e1bf02496937bb20d Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 29 Jul 2026 12:09:27 -0400 Subject: [PATCH 165/201] fix(menu): path highlight corners flipped --- src/widget/menu/menu_inner.rs | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index c0d7f10a..25bbfa7b 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -703,7 +703,7 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { } _ => {} - }; + } None } @@ -811,11 +811,11 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { let rad_0 = theme.cosmic().radius_0(); if start_index != end_index { if 0 == i { - rad[0] = rad_0[0]; - rad[1] = rad_0[1]; + rad[2] = rad_0[0]; + rad[3] = rad_0[1]; } else if i == end_index - start_index { - rad[2] = rad_0[2]; - rad[3] = rad_0[3]; + rad[0] = rad_0[2]; + rad[1] = rad_0[3]; } else { rad = rad_0; } From dc1cf9f00cbe2902a52166492654bb9fee8a73d1 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 29 Jul 2026 14:23:16 -0400 Subject: [PATCH 166/201] chore: update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 65b5dbc6..7346cffd 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 65b5dbc67172208b0f9af1cf8a5a72f4017734cc +Subproject commit 7346cffd2e51e45fbe4dd31bdd42211b8ca0078e From 05be5041740e1a943623e5489bdf831e22d5316c Mon Sep 17 00:00:00 2001 From: Hosted Weblate Date: Wed, 5 Aug 2026 17:02:13 +0200 Subject: [PATCH 167/201] 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: Martin Meizoso Co-authored-by: Quentin PAGÈS Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/gl/ Translate-URL: https://hosted.weblate.org/projects/pop-os/libcosmic/oc/ Translation: Pop OS/libcosmic --- i18n/gl/libcosmic.ftl | 34 ++++++++++++++++++++++++++++++++++ i18n/oc/libcosmic.ftl | 14 ++++++++++++++ 2 files changed, 48 insertions(+) create mode 100644 i18n/gl/libcosmic.ftl diff --git a/i18n/gl/libcosmic.ftl b/i18n/gl/libcosmic.ftl new file mode 100644 index 00000000..94bc4909 --- /dev/null +++ b/i18n/gl/libcosmic.ftl @@ -0,0 +1,34 @@ +close = Pechar +license = Licenza +links = Enderezos +developers = Desenvolvedores +designers = Deseñadores +artists = Artistas +translators = Traductores +documenters = Documentadores +january = Xaneiro { $year } +february = Febreiro { $year } +march = Marzo { $year } +april = Abril { $year } +may = Maio { $year } +june = Xuño { $year } +july = Xullo { $year } +august = Agosto { $year } +september = Setembro { $year } +october = Outubro { $year } +november = Novembro { $year } +december = Decembro { $year } +monday = Luns +mon = Lun +tuesday = Martes +tue = Mar +wednesday = Mércores +wed = Mér +thursday = Xoves +thu = Xov +friday = Venres +fri = Ven +saturday = Sábado +sat = Sáb +sunday = Domingo +sun = Dom diff --git a/i18n/oc/libcosmic.ftl b/i18n/oc/libcosmic.ftl index e69de29b..e1d98649 100644 --- a/i18n/oc/libcosmic.ftl +++ b/i18n/oc/libcosmic.ftl @@ -0,0 +1,14 @@ +monday = Diluns +mon = Dil +tuesday = Dimars +tue = Dim +wednesday = Dimècres +wed = Dma +thursday = Dijòus +thu = Dij +friday = Divendres +fri = Div +saturday = Dissabte +sat = Dis +sunday = Dimenge +sun = Dim From 1ac21a9d994cc68c8e4cce6101a6bade6a9da89d Mon Sep 17 00:00:00 2001 From: Hojjat Date: Tue, 4 Aug 2026 11:50:12 -0600 Subject: [PATCH 168/201] fix: request redraw when hover state changes --- src/widget/button/widget.rs | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/widget/button/widget.rs b/src/widget/button/widget.rs index a9d44b2b..f5501134 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -868,8 +868,21 @@ pub fn update<'a, Message: Clone>( } } } + Event::Mouse(mouse::Event::CursorMoved { .. }) + | Event::Touch(touch::Event::FingerMoved { .. }) => { + let state = state(); + let is_hovered = cursor.is_over(layout.bounds()); + if state.is_hovered != is_hovered { + state.is_hovered = is_hovered; + shell.request_redraw(); + } + } Event::Touch(touch::Event::FingerLost { .. }) | Event::Mouse(mouse::Event::CursorLeft) => { let state = state(); + + if state.is_hovered || state.is_pressed { + shell.request_redraw(); + } state.is_hovered = false; state.is_pressed = false; } From 5fd1ca4220629b961b5f088d4d4aaf857573a3a8 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Tue, 4 Aug 2026 13:58:23 -0600 Subject: [PATCH 169/201] fix: request redraw in menus --- src/widget/dropdown/menu/mod.rs | 6 ++++-- src/widget/dropdown/multi/menu.rs | 2 ++ src/widget/menu/menu_bar.rs | 1 + src/widget/menu/menu_inner.rs | 8 +++++++- 4 files changed, 14 insertions(+), 3 deletions(-) diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index 0c96c1c6..f18016d8 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -499,8 +499,10 @@ where let new_hovered_option = (cursor_position.y / option_height) as usize; let mut hovered_guard = self.hovered_option.lock().unwrap(); - if let Some(on_option_hovered) = self.on_option_hovered { - if *hovered_guard != Some(new_hovered_option) { + if *hovered_guard != Some(new_hovered_option) { + shell.request_redraw(); + + if let Some(on_option_hovered) = self.on_option_hovered { shell.publish(on_option_hovered(new_hovered_option)); } } diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index 0a761097..689fb0de 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -382,6 +382,8 @@ where if previous_hover_option.as_ref() == Some(item) { previous_hover_option } else { + shell.request_redraw(); + if let Some(on_option_hovered) = self.on_option_hovered { shell.publish(on_option_hovered(item.clone())); } diff --git a/src/widget/menu/menu_bar.rs b/src/widget/menu/menu_bar.rs index 84927b67..71a39c42 100644 --- a/src/widget/menu/menu_bar.rs +++ b/src/widget/menu/menu_bar.rs @@ -656,6 +656,7 @@ where Mouse(mouse::Event::CursorMoved { .. } | mouse::Event::CursorEntered) if open && view_cursor.is_over(layout.bounds()) => { + shell.request_redraw(); shell.capture_event(); #[cfg(wayland_platform)] if matches!(WINDOWING_SYSTEM.get(), Some(WindowingSystem::Wayland)) { diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 25bbfa7b..58721124 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -1502,7 +1502,9 @@ where // cursor is outside { - last_menu_state.index = None; + if last_menu_state.index.take().is_some() { + shell.request_redraw(); + } shell.capture_event(); return new_menu_root; } @@ -1561,6 +1563,10 @@ where // set new index let old_index = last_menu_state.index.replace(new_index); + if old_index != Some(new_index) { + shell.request_redraw(); + } + // 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) { From 31ec448bf0fb286c7a794701185becf5426deca9 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Tue, 4 Aug 2026 13:58:47 -0600 Subject: [PATCH 170/201] fix: request redraw in text input when needed --- src/widget/text_input/input.rs | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index c86c5a3e..d382a5a8 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -1680,6 +1680,7 @@ pub fn update<'a, Message: Clone + 'static>( state.last_click = Some(click); + shell.request_redraw(); shell.capture_event(); return; } else { @@ -1746,6 +1747,7 @@ pub fn update<'a, Message: Clone + 'static>( .cursor .select_range(state.cursor.start(value), position); + shell.request_redraw(); shell.capture_event(); return; } @@ -2099,6 +2101,7 @@ pub fn update<'a, Message: Clone + 'static>( _ => {} } + shell.request_redraw(); shell.capture_event(); return; } From 142133783111a341c019b4413f99f765b2d6ec69 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 5 Aug 2026 12:14:55 -0600 Subject: [PATCH 171/201] fix: do not publish ResponsiveMenuBar messages in a loop --- src/widget/responsive_container.rs | 25 ++++++++++++++++++------- 1 file changed, 18 insertions(+), 7 deletions(-) diff --git a/src/widget/responsive_container.rs b/src/widget/responsive_container.rs index c8925cc2..e75b7a6b 100644 --- a/src/widget/responsive_container.rs +++ b/src/widget/responsive_container.rs @@ -180,13 +180,21 @@ where let state = tree.state.downcast_mut::(); if state.needs_update { - shell.publish((self.on_action)( - crate::surface::Action::ResponsiveMenuBar { - menu_bar: self.id.clone(), - limits: state.limits, - size: state.size, - }, - )); + // `needs_update` stays set while the content cannot fit, don't publish messages in a + // busy loop + let announcement = (state.limits, state.size); + + if state.announced != Some(announcement) { + state.announced = Some(announcement); + shell.publish((self.on_action)( + crate::surface::Action::ResponsiveMenuBar { + menu_bar: self.id.clone(), + limits: announcement.0, + size: announcement.1, + }, + )); + } + state.needs_update = false; } @@ -328,6 +336,8 @@ struct State { limits: Limits, size: Size, needs_update: bool, + /// The `(limits, size)` last announced, so an unsatisfiable layout does not announce forever + announced: Option<(Limits, Size)>, } impl State { @@ -336,6 +346,7 @@ impl State { limits: Limits::NONE, size: Size::new(0., 0.), needs_update: false, + announced: None, } } } From 43a50cb6f5af18fa6cbea535fb46460a3c5cd573 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 5 Aug 2026 13:37:02 -0600 Subject: [PATCH 172/201] chore: Update iced --- iced | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/iced b/iced index 7346cffd..f5aa5a6f 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit 7346cffd2e51e45fbe4dd31bdd42211b8ca0078e +Subproject commit f5aa5a6f8cc2db6e2adbbc140c310a5a4af2f8d2 From 834cd366eeaf744d1600a9b383ef5cde78a768f1 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Tue, 19 May 2026 19:36:44 -0600 Subject: [PATCH 173/201] feat: add context menu support to selectable Text widget --- src/theme/style/iced.rs | 13 +- src/widget/text.rs | 525 ++++++++++++++++++++++++++++++++++++---- 2 files changed, 494 insertions(+), 44 deletions(-) diff --git a/src/theme/style/iced.rs b/src/theme/style/iced.rs index 0a09a179..3b0d9794 100644 --- a/src/theme/style/iced.rs +++ b/src/theme/style/iced.rs @@ -1370,18 +1370,19 @@ impl iced_widget::text::Catalog for Theme { } fn style(&self, class: &Self::Class<'_>) -> iced_widget::text::Style { + let selected_fill = self.cosmic().accent.base.into(); match class { Text::Accent => iced_widget::text::Style { color: Some(self.cosmic().accent_text_color().into()), - ..Default::default() + selected_fill, }, Text::Default => iced_widget::text::Style { color: None, - ..Default::default() + selected_fill, }, Text::Color(c) => iced_widget::text::Style { color: Some(*c), - ..Default::default() + selected_fill, }, Text::Custom(f) => f(self), } @@ -1527,6 +1528,12 @@ pub enum TextEditor<'a> { Custom(text_editor::StyleFn<'a, Theme>), } +impl<'a> From> for TextEditor<'a> { + fn from(style: text_editor::StyleFn<'a, Theme>) -> Self { + Self::Custom(style) + } +} + impl iced_widget::text_editor::Catalog for Theme { type Class<'a> = TextEditor<'a>; diff --git a/src/widget/text.rs b/src/widget/text.rs index 37e85b80..9fe051ac 100644 --- a/src/widget/text.rs +++ b/src/widget/text.rs @@ -1,13 +1,460 @@ use crate::Renderer; -pub use iced::widget::Text; +use crate::widget::menu::MenuBarState; + +use iced_core::event::Event; use iced_core::text::LineHeight; +use iced_core::widget::Widget; +use iced_core::widget::text::{Alignment, Catalog, Ellipsize, Shaping, Style, Wrapping}; +use iced_core::widget::tree::{self, Tree}; +use iced_core::{ + Clipboard, Color, Layout, Length, Pixels, Rectangle, Shell, Size, Vector, alignment, mouse, + overlay, renderer, +}; use std::borrow::Cow; -/// Creates a new [`Text`] widget with the provided content. +type InnerText<'a> = iced_core::widget::text::Text<'a, crate::Theme, Renderer>; + +/// A text widget with optional selection and context menu support. /// -/// [`Text`]: widget::Text -pub fn text<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { - Text::new(text.into()).font(crate::font::default()) +/// By default, text is non-selectable. Call [`.selectable()`](Text::selectable) +/// to enable text selection and a right-click context menu with Copy and +/// Select All. +pub struct Text<'a> { + inner: InnerText<'a>, + context_menu: bool, +} + +impl<'a> Text<'a> { + /// Creates a new [`Text`] widget with the given content. + pub fn new(content: impl iced_core::text::IntoFragment<'a>) -> Self { + Self { + inner: InnerText::new(content), + context_menu: false, + } + } + + /// Enables text selection and a right-click context menu. + pub fn selectable(mut self) -> Self { + self.inner = self.inner.selectable(); + self.context_menu = true; + self + } + + /// Controls whether the right-click context menu is shown. + /// + /// Only meaningful after calling [`.selectable()`](Self::selectable). + /// Pass `false` to keep text selection but disable the context menu. + pub fn context_menu(mut self, enabled: bool) -> Self { + self.context_menu = enabled; + self + } + + fn uses_popup_context_menu(&self) -> bool { + #[cfg(feature = "wayland")] + if matches!( + crate::app::cosmic::WINDOWING_SYSTEM.get(), + Some(crate::app::cosmic::WindowingSystem::Wayland) + ) { + return true; + } + false + } + + pub fn size(mut self, size: impl Into) -> Self { + self.inner = self.inner.size(size); + self + } + + pub fn line_height(mut self, line_height: impl Into) -> Self { + self.inner = self.inner.line_height(line_height); + self + } + + pub fn font(mut self, font: impl Into<::Font>) -> Self { + self.inner = self.inner.font(font); + self + } + + pub fn font_maybe( + mut self, + font: Option::Font>>, + ) -> Self { + self.inner = self.inner.font_maybe(font); + self + } + + pub fn width(mut self, width: impl Into) -> Self { + self.inner = self.inner.width(width); + self + } + + pub fn height(mut self, height: impl Into) -> Self { + self.inner = self.inner.height(height); + self + } + + pub fn center(mut self) -> Self { + self.inner = self.inner.center(); + self + } + + pub fn align_x(mut self, alignment: impl Into) -> Self { + self.inner = self.inner.align_x(alignment); + self + } + + pub fn align_y(mut self, alignment: impl Into) -> Self { + self.inner = self.inner.align_y(alignment); + self + } + + pub fn shaping(mut self, shaping: Shaping) -> Self { + self.inner = self.inner.shaping(shaping); + self + } + + pub fn wrapping(mut self, wrapping: Wrapping) -> Self { + self.inner = self.inner.wrapping(wrapping); + self + } + + pub fn ellipsize(mut self, ellipsize: Ellipsize) -> Self { + self.inner = self.inner.ellipsize(ellipsize); + self + } + + #[must_use] + pub fn style(mut self, style: impl Fn(&crate::Theme) -> Style + 'a) -> Self + where + ::Class<'a>: + From>, + { + self.inner = self.inner.style(style); + self + } + + pub fn color(mut self, color: impl Into) -> Self + where + ::Class<'a>: + From>, + { + self.inner = self.inner.color(color); + self + } + + pub fn color_maybe(mut self, color: Option>) -> Self + where + ::Class<'a>: + From>, + { + self.inner = self.inner.color_maybe(color); + self + } + + #[must_use] + pub fn class(mut self, class: impl Into<::Class<'a>>) -> Self { + self.inner = self.inner.class(class); + self + } +} + +impl<'a> From> for Text<'a> { + fn from(inner: InnerText<'a>) -> Self { + Self { + inner, + context_menu: false, + } + } +} + +impl<'a> From<&'a str> for Text<'a> { + fn from(content: &'a str) -> Self { + Self::new(content) + } +} + +struct TextWrapperState { + menu_bar_state: MenuBarState, + pending_action: crate::widget::text_context_menu::PendingAction, +} + +fn w<'x, Message>(inner: &'x InnerText<'_>) -> &'x dyn Widget { + inner +} + +fn w_mut<'x, Message>( + inner: &'x mut InnerText<'_>, +) -> &'x mut dyn Widget { + inner +} + +impl<'a, Message: Clone + 'static> Widget for Text<'a> { + fn tag(&self) -> tree::Tag { + if self.context_menu { + tree::Tag::of::() + } else { + w::(&self.inner).tag() + } + } + + fn state(&self) -> tree::State { + if self.context_menu { + tree::State::new(TextWrapperState { + menu_bar_state: MenuBarState::default(), + pending_action: crate::widget::text_context_menu::pending_action(), + }) + } else { + w::(&self.inner).state() + } + } + + fn children(&self) -> Vec { + if self.context_menu { + vec![Tree::new(w::(&self.inner))] + } else { + Vec::new() + } + } + + fn diff(&mut self, tree: &mut Tree) { + if self.context_menu { + if let Some(child) = tree.children.first_mut() { + child.diff(&mut self.inner as &mut dyn Widget); + } + } + } + + fn size(&self) -> Size { + w::(&self.inner).size() + } + + fn layout( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + limits: &iced_core::layout::Limits, + ) -> iced_core::layout::Node { + let inner_tree = if self.context_menu { + &mut tree.children[0] + } else { + tree + }; + w_mut::(&mut self.inner).layout(inner_tree, renderer, limits) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &crate::Theme, + defaults: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let inner_tree = if self.context_menu { + &tree.children[0] + } else { + tree + }; + w::(&self.inner).draw( + inner_tree, renderer, theme, defaults, layout, cursor, viewport, + ); + + // draw a border when the text is focused + use iced_core::renderer::Renderer as RendererExt; + use iced_core::widget::text::State as TextState; + + let state = inner_tree + .state + .downcast_ref::::Paragraph>>(); + if state.is_keyboard_focused() { + let cosmic = theme.cosmic(); + let accent: Color = cosmic.accent.base.into(); + renderer.fill_quad( + renderer::Quad { + bounds: layout.bounds(), + border: iced_core::Border { + color: accent, + width: 1.0, + radius: cosmic.corner_radii.radius_s.into(), + }, + ..renderer::Quad::default() + }, + Color::TRANSPARENT, + ); + } + } + + 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.context_menu { + w_mut::(&mut self.inner).update( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + + #[cfg(feature = "wayland")] + if self.uses_popup_context_menu() { + use iced_core::widget::text::HasSelectableText; + + if self + .inner + .context_menu_position(&tree.children[0]) + .is_some() + { + let selected_text = self.inner.selected_text(&tree.children[0]); + let has_selection = selected_text.is_some(); + let click_position = + self.inner.context_menu_position(&tree.children[0]).unwrap(); + let wrapper_state = tree.state.downcast_ref::(); + let menu_bar_state = wrapper_state.menu_bar_state.clone(); + let pending_action = wrapper_state.pending_action.clone(); + + crate::widget::text_context_menu::create_text_context_popup( + click_position, + selected_text, + false, + has_selection, + &menu_bar_state, + &pending_action, + renderer, + viewport, + cursor, + ); + + self.inner + .set_context_menu_position(&mut tree.children[0], None); + } + + { + let wrapper_state = tree.state.downcast_ref::(); + let pending_action = wrapper_state.pending_action.clone(); + if let Some(action) = + crate::widget::text_context_menu::take_pending_action(&pending_action) + { + match action { + crate::widget::text_context_menu::TextCtxAction::Copy => {} + crate::widget::text_context_menu::TextCtxAction::SelectAll => { + self.inner.select_all(&mut tree.children[0]); + } + _ => {} + } + } + } + + let wrapper_state = tree.state.downcast_ref::(); + let menu_bar_state = wrapper_state.menu_bar_state.clone(); + crate::widget::text_context_menu::cleanup_text_popup(&menu_bar_state); + } + } else { + w_mut::(&mut self.inner).update( + tree, event, layout, cursor, renderer, clipboard, shell, viewport, + ); + } + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let inner_tree = if self.context_menu { + &tree.children[0] + } else { + tree + }; + w::(&self.inner).mouse_interaction(inner_tree, layout, cursor, viewport, renderer) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + _layout: Layout<'b>, + _renderer: &Renderer, + _viewport: &Rectangle, + translation: Vector, + ) -> Option> { + if self.context_menu && !self.uses_popup_context_menu() { + let menu_bar_state = tree + .state + .downcast_ref::() + .menu_bar_state + .clone(); + let inner_tree = &mut tree.children[0]; + crate::widget::text_context_menu::context_menu_overlay( + &self.inner, + inner_tree, + None, + translation, + menu_bar_state, + ) + } else { + None + } + } + + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn iced_core::widget::Operation, + ) { + let inner_tree = if self.context_menu { + &mut tree.children[0] + } else { + tree + }; + w_mut::(&mut self.inner).operate(inner_tree, layout, renderer, operation); + } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + let inner_state = if self.context_menu { + &state.children[0] + } else { + state + }; + w::(&self.inner).a11y_nodes(layout, inner_state, cursor) + } + + fn id(&self) -> Option { + w::(&self.inner).id() + } + + fn set_id(&mut self, id: iced_core::widget::Id) { + w_mut::(&mut self.inner).set_id(id); + } +} + +impl<'a, Message: Clone + 'static> From> for crate::Element<'a, Message> { + fn from(text: Text<'a>) -> Self { + Self::new(text) + } } /// Available presets for text typography @@ -24,119 +471,115 @@ pub enum Typography { Title4, } +/// Creates a new [`Text`] widget with the provided content. +pub fn text<'a>(content: impl Into> + 'a) -> Text<'a> { + Text::new(content.into()).font(crate::font::default()) +} + /// [`Text`] widget with the Title 1 typography preset. -pub fn title1<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { +pub fn title1<'a>(content: impl Into> + 'a) -> Text<'a> { #[inline(never)] - fn inner(text: Cow) -> Text { + fn inner(text: Cow) -> Text { Text::new(text) .size(35.0) .line_height(LineHeight::Absolute(52.0.into())) .font(crate::font::semibold()) } - - inner(text.into()) + inner(content.into()) } /// [`Text`] widget with the Title 2 typography preset. -pub fn title2<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { +pub fn title2<'a>(content: impl Into> + 'a) -> Text<'a> { #[inline(never)] - fn inner(text: Cow) -> Text { + fn inner(text: Cow) -> Text { Text::new(text) .size(29.0) .line_height(LineHeight::Absolute(43.0.into())) .font(crate::font::semibold()) } - - inner(text.into()) + inner(content.into()) } /// [`Text`] widget with the Title 3 typography preset. -pub fn title3<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { +pub fn title3<'a>(content: impl Into> + 'a) -> Text<'a> { #[inline(never)] - fn inner(text: Cow) -> Text { + fn inner(text: Cow) -> Text { Text::new(text) .size(24.0) .line_height(LineHeight::Absolute(36.0.into())) .font(crate::font::bold()) } - - inner(text.into()) + inner(content.into()) } /// [`Text`] widget with the Title 4 typography preset. -pub fn title4<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { +pub fn title4<'a>(content: impl Into> + 'a) -> Text<'a> { #[inline(never)] - fn inner(text: Cow) -> Text { + fn inner(text: Cow) -> Text { Text::new(text) .size(20.0) .line_height(LineHeight::Absolute(30.0.into())) .font(crate::font::bold()) } - - inner(text.into()) + inner(content.into()) } /// [`Text`] widget with the Heading typography preset. -pub fn heading<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { +pub fn heading<'a>(content: impl Into> + 'a) -> Text<'a> { #[inline(never)] - fn inner(text: Cow) -> Text { + fn inner(text: Cow) -> Text { Text::new(text) .size(14.0) .line_height(LineHeight::Absolute(iced::Pixels(21.0))) .font(crate::font::bold()) } - - inner(text.into()) + inner(content.into()) } /// [`Text`] widget with the Caption Heading typography preset. -pub fn caption_heading<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { +pub fn caption_heading<'a>(content: impl Into> + 'a) -> Text<'a> { #[inline(never)] - fn inner(text: Cow) -> Text { + fn inner(text: Cow) -> Text { Text::new(text) .size(12.0) .line_height(LineHeight::Absolute(iced::Pixels(17.0))) .font(crate::font::semibold()) } - - inner(text.into()) + inner(content.into()) } /// [`Text`] widget with the Body typography preset. -pub fn body<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { +pub fn body<'a>(content: impl Into> + 'a) -> Text<'a> { #[inline(never)] - fn inner(text: Cow) -> Text { + fn inner(text: Cow) -> Text { Text::new(text) .size(14.0) .line_height(LineHeight::Absolute(21.0.into())) .font(crate::font::default()) } - - inner(text.into()) + inner(content.into()) } /// [`Text`] widget with the Caption typography preset. -pub fn caption<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { +pub fn caption<'a>(content: impl Into> + 'a) -> Text<'a> { #[inline(never)] - fn inner(text: Cow) -> Text { + fn inner(text: Cow) -> Text { Text::new(text) .size(12.0) .line_height(LineHeight::Absolute(17.0.into())) .font(crate::font::default()) } - - inner(text.into()) + inner(content.into()) } /// [`Text`] widget with the Monotext typography preset. -pub fn monotext<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { +pub fn monotext<'a>(content: impl Into> + 'a) -> Text<'a> { #[inline(never)] - fn inner(text: Cow) -> Text { + fn inner(text: Cow) -> Text { Text::new(text) .size(14.0) .line_height(LineHeight::Absolute(20.0.into())) .font(crate::font::mono()) } - - inner(text.into()) + inner(content.into()) } From 2cd43a8b89a7bd456286ea3729aef778daf33904 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Tue, 19 May 2026 19:36:47 -0600 Subject: [PATCH 174/201] feat: add context menu wrapper for TextEditor --- src/widget/text_editor.rs | 452 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 452 insertions(+) create mode 100644 src/widget/text_editor.rs diff --git a/src/widget/text_editor.rs b/src/widget/text_editor.rs new file mode 100644 index 00000000..9bbe2718 --- /dev/null +++ b/src/widget/text_editor.rs @@ -0,0 +1,452 @@ +// Copyright 2025 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! A multi-line text editor + +pub use iced_widget::text_editor::{ + Action, Binding, Catalog, Content, Cursor, Edit, Id, KeyPress, Line, LineEnding, Motion, + Position, Selection, State, Status, Style, StyleFn, +}; + +use crate::widget::menu::MenuBarState; + +use iced_core::event::Event; +use iced_core::text::highlighter; +use iced_core::widget::Widget; +use iced_core::widget::tree::{self, Tree}; +use iced_core::{ + Clipboard, Layout, Length, Rectangle, Shell, Size, Vector, mouse, overlay, renderer, +}; + +type InnerEditor<'a, Message> = + iced_widget::TextEditor<'a, highlighter::PlainText, Message, crate::Theme, crate::Renderer>; + +pub struct TextEditor<'a, Message> { + inner: InnerEditor<'a, Message>, + has_context_menu: bool, +} + +struct EditorWrapperState { + menu_bar_state: MenuBarState, + pending_action: crate::widget::text_context_menu::PendingAction, +} + +impl<'a, Message: Clone + 'static> TextEditor<'a, Message> { + /// Creates a new [`TextEditor`] from the given [`Content`]. + pub fn new(content: &'a Content) -> Self { + Self { + inner: iced_widget::text_editor(content), + has_context_menu: true, + } + } + + /// Controls whether the right-click context menu is shown. + /// + /// The context menu is enabled by default. Pass `false` to disable it. + pub fn context_menu(mut self, enabled: bool) -> Self { + self.has_context_menu = enabled; + self + } + + pub fn id(mut self, id: impl Into) -> Self { + self.inner = self.inner.id(id); + self + } + + pub fn placeholder(mut self, placeholder: impl iced_core::text::IntoFragment<'a>) -> Self { + self.inner = self.inner.placeholder(placeholder); + self + } + + pub fn width(mut self, width: impl Into) -> Self { + self.inner = self.inner.width(width); + self + } + + pub fn height(mut self, height: impl Into) -> Self { + self.inner = self.inner.height(height); + self + } + + pub fn min_height(mut self, min_height: impl Into) -> Self { + self.inner = self.inner.min_height(min_height); + self + } + + pub fn max_height(mut self, max_height: impl Into) -> Self { + self.inner = self.inner.max_height(max_height); + self + } + + pub fn on_action(mut self, on_edit: impl Fn(Action) -> Message + 'a) -> Self { + self.inner = self.inner.on_action(on_edit); + self + } + + pub fn font( + mut self, + font: impl Into<::Font>, + ) -> Self { + self.inner = self.inner.font(font); + self + } + + pub fn size(mut self, size: impl Into) -> Self { + self.inner = self.inner.size(size); + self + } + + pub fn line_height(mut self, line_height: impl Into) -> Self { + self.inner = self.inner.line_height(line_height); + self + } + + pub fn padding(mut self, padding: impl Into) -> Self { + self.inner = self.inner.padding(padding); + self + } + + pub fn wrapping(mut self, wrapping: iced_core::text::Wrapping) -> Self { + self.inner = self.inner.wrapping(wrapping); + self + } + + pub fn key_binding( + mut self, + key_binding: impl Fn(KeyPress) -> Option> + 'a, + ) -> Self { + self.inner = self.inner.key_binding(key_binding); + self + } + + #[must_use] + pub fn style(mut self, style: impl Fn(&crate::Theme, Status) -> Style + 'a) -> Self + where + ::Class<'a>: + From>, + { + self.inner = self.inner.style(style); + self + } + + fn uses_popup_context_menu(&self) -> bool { + #[cfg(feature = "wayland")] + if matches!( + crate::app::cosmic::WINDOWING_SYSTEM.get(), + Some(crate::app::cosmic::WindowingSystem::Wayland) + ) { + return true; + } + false + } +} + +/// Creates a new [`TextEditor`] from the given [`Content`]. +pub fn text_editor<'a, Message: Clone + 'static>( + content: &'a Content, +) -> TextEditor<'a, Message> { + TextEditor::new(content) +} + +fn ew<'x, Message>( + inner: &'x InnerEditor<'_, Message>, +) -> &'x dyn Widget { + inner +} + +fn ew_mut<'x, Message>( + inner: &'x mut InnerEditor<'_, Message>, +) -> &'x mut dyn Widget { + inner +} + +impl<'a, Message: Clone + 'static> Widget + for TextEditor<'a, Message> +{ + fn tag(&self) -> tree::Tag { + if self.has_context_menu { + tree::Tag::of::() + } else { + ew::(&self.inner).tag() + } + } + + fn state(&self) -> tree::State { + if self.has_context_menu { + tree::State::new(EditorWrapperState { + menu_bar_state: MenuBarState::default(), + pending_action: crate::widget::text_context_menu::pending_action(), + }) + } else { + ew::(&self.inner).state() + } + } + + fn children(&self) -> Vec { + if self.has_context_menu { + vec![Tree::new(ew::(&self.inner))] + } else { + ew::(&self.inner).children() + } + } + + fn diff(&mut self, tree: &mut Tree) { + if self.has_context_menu { + if let Some(child) = tree.children.first_mut() { + child.diff( + &mut self.inner as &mut dyn Widget, + ); + } + } else { + ew_mut::(&mut self.inner).diff(tree); + } + } + + fn size(&self) -> Size { + ew::(&self.inner).size() + } + + fn layout( + &mut self, + tree: &mut Tree, + renderer: &crate::Renderer, + limits: &iced_core::layout::Limits, + ) -> iced_core::layout::Node { + let inner_tree = if self.has_context_menu { + &mut tree.children[0] + } else { + tree + }; + ew_mut::(&mut self.inner).layout(inner_tree, renderer, limits) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + defaults: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let inner_tree = if self.has_context_menu { + &tree.children[0] + } else { + tree + }; + ew::(&self.inner).draw( + inner_tree, renderer, theme, defaults, layout, cursor, viewport, + ); + } + + fn update( + &mut self, + tree: &mut Tree, + event: &Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &crate::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + if self.has_context_menu { + ew_mut::(&mut self.inner).update( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + + use iced_core::widget::text::HasSelectableText; + if self.inner.is_focused(&tree.children[0]) + && matches!( + event, + Event::Mouse(mouse::Event::ButtonPressed(_)) + | Event::Touch(iced_core::touch::Event::FingerPressed { .. }) + ) + && cursor.is_over(layout.bounds()) + { + crate::widget::text_input::notify_focus_change(); + } + + #[cfg(feature = "wayland")] + if self.uses_popup_context_menu() { + if self + .inner + .context_menu_position(&tree.children[0]) + .is_some() + { + let selected_text = self.inner.selected_text(&tree.children[0]); + let has_selection = selected_text.is_some(); + let click_position = + self.inner.context_menu_position(&tree.children[0]).unwrap(); + let wrapper_state = tree.state.downcast_ref::(); + let menu_bar_state = wrapper_state.menu_bar_state.clone(); + let pending_action = wrapper_state.pending_action.clone(); + + crate::widget::text_context_menu::create_text_context_popup( + click_position, + selected_text, + self.inner.is_editable(), + has_selection, + &menu_bar_state, + &pending_action, + renderer, + viewport, + cursor, + ); + + self.inner + .set_context_menu_position(&mut tree.children[0], None); + } + + // Process deferred actions from the popup. + { + let wrapper_state = tree.state.downcast_ref::(); + let pending_action = wrapper_state.pending_action.clone(); + if let Some(action) = + crate::widget::text_context_menu::take_pending_action(&pending_action) + { + match action { + crate::widget::text_context_menu::TextCtxAction::Copy => {} + crate::widget::text_context_menu::TextCtxAction::Cut => { + self.inner.delete_selection(&mut tree.children[0]); + } + crate::widget::text_context_menu::TextCtxAction::Paste => { + let content: String = clipboard + .read(iced_core::clipboard::Kind::Standard) + .unwrap_or_default(); + self.inner.paste_text(&mut tree.children[0], &content); + } + crate::widget::text_context_menu::TextCtxAction::SelectAll => { + self.inner.select_all(&mut tree.children[0]); + } + } + } + } + + // Cleanup popup if menu closed. + let wrapper_state = tree.state.downcast_ref::(); + let menu_bar_state = wrapper_state.menu_bar_state.clone(); + crate::widget::text_context_menu::cleanup_text_popup(&menu_bar_state); + } + } else { + ew_mut::(&mut self.inner).update( + tree, event, layout, cursor, renderer, clipboard, shell, viewport, + ); + } + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &crate::Renderer, + ) -> mouse::Interaction { + let inner_tree = if self.has_context_menu { + &tree.children[0] + } else { + tree + }; + ew::(&self.inner).mouse_interaction(inner_tree, layout, cursor, viewport, renderer) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + layout: Layout<'b>, + renderer: &crate::Renderer, + viewport: &Rectangle, + translation: Vector, + ) -> Option> { + if self.has_context_menu { + if !self.uses_popup_context_menu() { + use iced_core::widget::text::HasSelectableText; + let inner_tree = &mut tree.children[0]; + if self.inner.context_menu_position(inner_tree).is_some() { + let menu_bar_state = tree + .state + .downcast_ref::() + .menu_bar_state + .clone(); + let inner_tree = &mut tree.children[0]; + return crate::widget::text_context_menu::context_menu_overlay( + &self.inner, + inner_tree, + None, + translation, + menu_bar_state, + ); + } + } + + ew_mut::(&mut self.inner).overlay( + &mut tree.children[0], + layout, + renderer, + viewport, + translation, + ) + } else { + ew_mut::(&mut self.inner).overlay( + tree, + layout, + renderer, + viewport, + translation, + ) + } + } + + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &crate::Renderer, + operation: &mut dyn iced_core::widget::Operation, + ) { + let inner_tree = if self.has_context_menu { + &mut tree.children[0] + } else { + tree + }; + ew_mut::(&mut self.inner).operate(inner_tree, layout, renderer, operation); + } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + let inner_state = if self.has_context_menu { + &state.children[0] + } else { + state + }; + ew::(&self.inner).a11y_nodes(layout, inner_state, cursor) + } + + fn id(&self) -> Option { + ew::(&self.inner).id() + } + + fn set_id(&mut self, id: iced_core::widget::Id) { + ew_mut::(&mut self.inner).set_id(id); + } +} + +impl<'a, Message: Clone + 'static> From> for crate::Element<'a, Message> { + fn from(editor: TextEditor<'a, Message>) -> Self { + Self::new(editor) + } +} From c6cf3db29daad1aad74bbae260726301e56ccbc4 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Tue, 19 May 2026 19:36:37 -0600 Subject: [PATCH 175/201] feat: add text context menu (overlay and wayland popup) --- src/widget/menu/menu_inner.rs | 2 +- src/widget/menu/menu_tree.rs | 2 +- src/widget/mod.rs | 7 +- src/widget/settings/item.rs | 8 +- src/widget/text_context_menu.rs | 713 ++++++++++++++++++++++++++++++++ src/widget/text_input/cursor.rs | 6 + 6 files changed, 731 insertions(+), 7 deletions(-) create mode 100644 src/widget/text_context_menu.rs diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 58721124..4ea71bc4 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -237,7 +237,7 @@ pub(super) struct MenuSlice { #[derive(Debug, Clone)] /// Menu bounds in overlay space pub struct MenuBounds { - child_positions: Vec, + pub(crate) child_positions: Vec, child_sizes: Vec, children_bounds: Rectangle, pub parent_bounds: Rectangle, diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index bd4f1e6c..cf99335c 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -194,7 +194,7 @@ pub fn menu_root<'a, Message, Renderer: renderer::Renderer>( ) -> Button<'a, Message> where Element<'a, Message, crate::Theme, Renderer>: From>, - Message: std::clone::Clone + 'a, + Message: std::clone::Clone + 'static, { widget::button::custom(widget::text(label)) .padding([4, 12]) diff --git a/src/widget/mod.rs b/src/widget/mod.rs index ab1eef31..4c33f7e3 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -89,8 +89,9 @@ pub use iced::widget::{Slider, VerticalSlider, slider, vertical_slider}; #[doc(inline)] pub use iced::widget::{Svg, svg}; +pub mod text_editor; #[doc(inline)] -pub use iced::widget::{TextEditor, text_editor}; +pub use text_editor::TextEditor; #[doc(inline)] pub use iced_core::widget::{Id, Operation, Widget}; @@ -295,6 +296,10 @@ pub mod text; #[doc(inline)] pub use text::{Text, text}; +pub mod text_context_menu; +#[doc(inline)] +pub use text_context_menu::HasSelectableText; + pub mod text_input; #[doc(inline)] pub use text_input::{ diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index 4eb5bb17..af4b17ab 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -14,12 +14,12 @@ use taffy::AlignContent; /// A settings item aligned in a row #[must_use] #[allow(clippy::module_name_repetitions)] -pub fn item<'a, Message: 'static>( +pub fn item<'a, Message: Clone + 'static>( title: impl Into> + 'a, widget: impl Into> + 'a, ) -> Row<'a, Message, Theme> { #[inline(never)] - fn inner<'a, Message: 'static>( + fn inner<'a, Message: Clone + 'static>( title: Cow<'a, str>, widget: Element<'a, Message>, ) -> Row<'a, Message, Theme> { @@ -45,12 +45,12 @@ pub fn item_row(children: Vec>) -> Row /// A settings item aligned in a flex row #[allow(clippy::module_name_repetitions)] -pub fn flex_item<'a, Message: 'static>( +pub fn flex_item<'a, Message: Clone + 'static>( title: impl Into> + 'a, widget: impl Into> + 'a, ) -> FlexRow<'a, Message> { #[inline(never)] - fn inner<'a, Message: 'static>( + fn inner<'a, Message: Clone + 'static>( title: Cow<'a, str>, widget: Element<'a, Message>, ) -> FlexRow<'a, Message> { diff --git a/src/widget/text_context_menu.rs b/src/widget/text_context_menu.rs new file mode 100644 index 00000000..6bf530c9 --- /dev/null +++ b/src/widget/text_context_menu.rs @@ -0,0 +1,713 @@ +// Copyright 2025 System76 +// SPDX-License-Identifier: MPL-2.0 + +//! Right-click context menu for widgets with selectable text. +//! +//! Use [`context_menu_overlay`] from your widget's `overlay()` method +//! with any widget that implements +//! [`HasSelectableText`](iced_core::widget::text::HasSelectableText) +//! to get a context menu with Copy, Select All, and optionally Cut/Paste. +//! +//! Internally uses libcosmic's [`Menu`](crate::widget::menu) system for +//! proper rendering, hover effects, and positioning. +//! +//! On Wayland, [`create_text_context_popup`] can be used instead to show +//! the context menu as a native popup surface. + +pub use iced_core::widget::text::HasSelectableText; + +use crate::widget::RcElementWrapper; +use crate::widget::menu::{ + self, CloseCondition, ItemHeight, ItemWidth, Menu, MenuBarState, PathHighlight, menu_roots_diff, +}; +use crate::{theme, widget}; + +use iced_core::layout::Limits; +use iced_core::widget::Tree; +use iced_core::{ + Clipboard, Layout, Point, Rectangle, Shell, Size, Vector, clipboard, mouse, overlay, renderer, +}; +use iced_widget::core::event; +use std::borrow::Cow; +use std::mem; +use std::sync::{Arc, Mutex}; + +/// Shared state for communicating deferred context menu actions +/// from a Wayland popup back to the owning text widget. +pub(crate) type PendingAction = Arc>>; + +/// Creates a new [`PendingAction`] for use with popup-based context menus. +pub(crate) fn pending_action() -> PendingAction { + Arc::new(Mutex::new(None)) +} + +/// Takes a pending action if one was set by a popup menu, and returns it. +pub(crate) fn take_pending_action(pending: &PendingAction) -> Option { + pending.lock().ok().and_then(|mut guard| guard.take()) +} + +use std::cell::Cell; +use std::collections::HashMap; +use std::sync::LazyLock; + +thread_local! { + static CURRENT_WINDOW_ID: Cell = const { Cell::new(iced_core::window::Id::NONE) }; +} + +/// Data needed to construct a popup view. Stored in a global registry +/// so `Cosmic::view()` can build the `Element` with the correct message type. +#[cfg(feature = "wayland")] +struct PopupViewData { + menu: Menu<'static, TextCtxAction>, + selected_text: Option, + pending_action: PendingAction, +} + +#[cfg(feature = "wayland")] +static POPUP_VIEW_REGISTRY: LazyLock>> = + LazyLock::new(|| Mutex::new(HashMap::new())); + +pub(crate) fn set_current_window_id(id: iced_core::window::Id) { + CURRENT_WINDOW_ID.set(id); +} + +pub(crate) fn current_window_id() -> iced_core::window::Id { + CURRENT_WINDOW_ID.get() +} + +/// Returns the popup view `Element` for a given popup ID, if one exists. +/// Called by `Cosmic::view()` where `Message` is known. +#[cfg(feature = "wayland")] +pub(crate) fn popup_view_for_id( + id: iced_core::window::Id, +) -> Option>> { + let registry = POPUP_VIEW_REGISTRY.lock().ok()?; + let data = registry.get(&id)?; + let popup_widget: TextContextMenuPopup = TextContextMenuPopup { + menu: data.menu.clone(), + selected_text: data.selected_text.clone(), + pending_action: data.pending_action.clone(), + _phantom: std::marker::PhantomData, + }; + drop(registry); + Some( + crate::Element::from( + crate::widget::container(popup_widget).center(iced_core::Length::Fill), + ) + .map(crate::action::app), + ) +} + +/// Sends a wayland popup action directly to the SCTK event loop, +/// bypassing the iced Task system. +#[cfg(feature = "wayland")] +fn send_popup_direct(settings: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings) { + iced_winit::send_wayland_action_direct( + iced_runtime::platform_specific::wayland::Action::Popup( + iced_runtime::platform_specific::wayland::popup::Action::Popup { popup: settings }, + ), + ); +} + +/// Sends a destroy popup action directly to the SCTK event loop. +#[cfg(feature = "wayland")] +fn send_destroy_popup_direct(id: iced_core::window::Id) { + iced_winit::send_wayland_action_direct( + iced_runtime::platform_specific::wayland::Action::Popup( + iced_runtime::platform_specific::wayland::popup::Action::Destroy { id }, + ), + ); +} + +/// Creates a context menu overlay for any widget implementing +/// [`HasSelectableText`]. +/// +/// Call this from your widget's `overlay()` method. Pass `on_input` for +/// editable widgets so Cut and Paste can publish text-change messages. +/// +/// The `menu_bar_state` parameter must be a persistent [`MenuBarState`] +/// stored in the widget's tree state. +pub(crate) fn context_menu_overlay<'a, W, Message>( + widget: &'a W, + tree: &'a mut Tree, + on_input: Option<&'a dyn Fn(String) -> Message>, + translation: Vector, + menu_bar_state: MenuBarState, +) -> Option> +where + W: HasSelectableText + 'a, + Message: Clone + 'static, +{ + let click_position = widget.context_menu_position(tree)?; + let selected_text = widget.selected_text(tree); + let is_editable = widget.is_editable(); + + let mut menu_roots = build_menu_roots(is_editable, selected_text.is_some()); + menu_roots.iter_mut().for_each(menu::Tree::set_index); + + let item_count = menu_roots[0].children.len(); + menu_bar_state.inner.with_data_mut(|state| { + let stale = state + .menu_states + .first() + .is_some_and(|ms| ms.menu_bounds.child_positions.len() != item_count); + if !state.open || stale { + state.menu_states.clear(); + state.active_root.clear(); + state.open = true; + } + menu_roots_diff(&mut menu_roots, &mut state.tree); + }); + + let offscreen = Rectangle::new(Point::new(-10000.0, -10000.0), Size::ZERO); + + let menu = Menu { + tree: menu_bar_state.clone(), + menu_roots: Cow::Owned(menu_roots), + 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: offscreen, + main_offset: -240, + cross_offset: 0, + root_bounds_list: vec![offscreen], + path_highlight: Some(PathHighlight::MenuActive), + style: Cow::Owned(theme::menu_bar::MenuBarStyle::Default), + position: Point::new(translation.x, translation.y), + is_overlay: true, + window_id: iced::window::Id::NONE, + depth: 0, + on_surface_action: None, + }; + + Some(overlay::Element::new(Box::new(TextMenuOverlay { + menu, + widget, + tree, + on_input, + click_position, + }))) +} + +#[derive(Clone, Copy, PartialEq, Eq)] +pub(crate) enum TextCtxAction { + Copy, + Cut, + Paste, + SelectAll, +} + +fn build_menu_roots(is_editable: bool, has_selection: bool) -> Vec> { + let mut items = Vec::with_capacity(4); + + if is_editable && has_selection { + items.push(menu::Tree::from(crate::Element::from( + menu::menu_button(vec![widget::text("Cut").into()]).on_press(TextCtxAction::Cut), + ))); + } + if has_selection { + items.push(menu::Tree::from(crate::Element::from( + menu::menu_button(vec![widget::text("Copy").into()]).on_press(TextCtxAction::Copy), + ))); + } + if is_editable { + items.push(menu::Tree::from(crate::Element::from( + menu::menu_button(vec![widget::text("Paste").into()]).on_press(TextCtxAction::Paste), + ))); + } + items.push(menu::Tree::from(crate::Element::from( + menu::menu_button(vec![widget::text("Select All").into()]) + .on_press(TextCtxAction::SelectAll), + ))); + + vec![menu::Tree::with_children( + RcElementWrapper::new(crate::Element::from(widget::Row::new())), + items, + )] +} + +struct TextMenuOverlay<'a, W, Message: Clone + 'static> { + menu: Menu<'a, TextCtxAction>, + widget: &'a W, + tree: &'a mut Tree, + on_input: Option<&'a dyn Fn(String) -> Message>, + click_position: Point, +} + +impl overlay::Overlay + for TextMenuOverlay<'_, W, Message> +where + W: HasSelectableText, + Message: Clone + 'static, +{ + fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> iced_core::layout::Node { + self.menu.layout( + renderer, + Limits::NONE + .min_width(bounds.width) + .max_width(bounds.width) + .min_height(bounds.height) + .max_height(bounds.height), + ) + } + + fn draw( + &self, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + ) { + self.menu.draw(renderer, theme, style, layout, cursor); + } + + fn update( + &mut self, + event: &event::Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &crate::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + ) { + // Pre-initialize the menu if it hasn't been yet. + let needs_init = self + .menu + .tree + .inner + .with_data(|state| state.open && state.menu_states.is_empty()); + + if needs_init { + let viewport = layout.bounds(); + let viewport_size = viewport.size(); + let overlay_offset = Point::ORIGIN - viewport.position(); + let overlay_cursor = cursor.position().unwrap_or_default() - overlay_offset; + + let init_bounds = Rectangle { + x: self.click_position.x, + y: self.click_position.y, + width: 240.0, + height: 240.0, + }; + + // Temporarily set real bounds so init_root_menu can find + // root_bounds_list entries that contain the cursor. + let main_offset = self.menu.main_offset as f32; + let saved_bounds = mem::replace(&mut self.menu.bar_bounds, init_bounds); + let saved_roots = mem::replace(&mut self.menu.root_bounds_list, vec![init_bounds]); + + let mut init_messages: Vec = Vec::new(); + let mut init_shell = Shell::new(&mut init_messages); + menu::init_root_menu( + &mut self.menu, + renderer, + &mut init_shell, + overlay_cursor, + viewport_size, + overlay_offset, + init_bounds, + main_offset, + ); + + // Restore offscreen bounds + self.menu.bar_bounds = saved_bounds; + self.menu.root_bounds_list = saved_roots; + + if init_shell.is_layout_invalid() { + shell.invalidate_layout(); + } + } + + // Right-button releases are meaningless for menu interaction + if matches!( + event, + event::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) + ) { + return; + } + + let mut local_messages = Vec::new(); + let mut local_shell = Shell::new(&mut local_messages); + + self.menu + .update(event, layout, cursor, renderer, clipboard, &mut local_shell); + + if local_shell.is_event_captured() { + shell.capture_event(); + } + shell.request_redraw_at(local_shell.redraw_request()); + if local_shell.is_layout_invalid() { + shell.invalidate_layout(); + } + + for action in local_messages { + match action { + TextCtxAction::Copy => { + self.widget.copy_to_clipboard(self.tree, clipboard); + } + TextCtxAction::Cut => { + self.widget.copy_to_clipboard(self.tree, clipboard); + if let Some(contents) = self.widget.delete_selection(self.tree) { + if let Some(on_input) = self.on_input { + shell.publish((on_input)(contents)); + } + } + } + TextCtxAction::Paste => { + let content: String = clipboard + .read(clipboard::Kind::Standard) + .unwrap_or_default(); + if let Some(contents) = self.widget.paste_text(self.tree, &content) { + if let Some(on_input) = self.on_input { + shell.publish((on_input)(contents)); + } + } + } + TextCtxAction::SelectAll => { + self.widget.select_all(self.tree); + } + } + self.widget.set_context_menu_position(self.tree, None); + } + + let is_open = self.menu.tree.inner.with_data(|state| state.open); + if !is_open { + self.widget.set_context_menu_position(self.tree, None); + } + } + + fn mouse_interaction( + &self, + layout: Layout<'_>, + cursor: mouse::Cursor, + _renderer: &crate::Renderer, + ) -> mouse::Interaction { + if cursor.is_over(layout.bounds()) { + mouse::Interaction::Idle + } else { + mouse::Interaction::None + } + } +} + +/// Creates a Wayland popup surface containing the text context menu. +/// +/// Sends the popup action directly to the SCTK event loop and registers +/// the view data so `Cosmic::view()` can render it. No app-level wiring +/// is needed +#[cfg(feature = "wayland")] +pub(crate) fn create_text_context_popup( + click_position: Point, + selected_text: Option, + is_editable: bool, + has_selection: bool, + menu_bar_state: &MenuBarState, + pending_action: &PendingAction, + renderer: &crate::Renderer, + viewport: &Rectangle, + cursor: mouse::Cursor, +) { + use iced_runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; + + let window_id = current_window_id(); + if window_id == iced_core::window::Id::NONE { + return; + } + + let mut menu_roots = build_menu_roots(is_editable, has_selection); + menu_roots.iter_mut().for_each(menu::Tree::set_index); + + let id = menu_bar_state.inner.with_data_mut(|state| { + state.menu_states.clear(); + state.active_root.clear(); + menu_roots_diff(&mut menu_roots, &mut state.tree); + if let Some(id) = state.popup_id.get(&window_id).copied() { + send_destroy_popup_direct(id); + state.view_cursor = cursor; + id + } else { + state.open = true; + state.view_cursor = cursor; + iced::window::Id::unique() + } + }); + + let bounds = Rectangle { + x: click_position.x, + y: click_position.y, + width: 240.0, + height: 240.0, + }; + + let mut popup_menu: Menu<'static, TextCtxAction> = Menu { + tree: menu_bar_state.clone(), + menu_roots: Cow::Owned(menu_roots), + 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: Cow::Owned(theme::menu_bar::MenuBarStyle::Default), + position: Point::new(0., 0.), + is_overlay: false, + window_id: id, + depth: 0, + on_surface_action: None, + }; + + { + let mut init_messages: Vec = Vec::new(); + let mut init_shell = Shell::new(&mut init_messages); + menu::init_root_menu( + &mut popup_menu, + renderer, + &mut init_shell, + cursor.position().unwrap_or_default(), + viewport.size(), + Vector::new(0., 0.), + bounds, + -(bounds.height), + ); + } + + let anchor_rect = menu_bar_state.inner.with_data_mut(|state| { + state.popup_id.insert(window_id, id); + let pos = cursor.position().unwrap_or_default(); + iced::Rectangle { + x: pos.x as i32, + y: pos.y as i32, + width: 1, + height: 1, + } + }); + + 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() + }; + + // Register view data so Cosmic::view() can render this popup. + if let Ok(mut registry) = POPUP_VIEW_REGISTRY.lock() { + registry.insert( + id, + PopupViewData { + menu: popup_menu, + selected_text, + pending_action: pending_action.clone(), + }, + ); + } + + // Send the popup creation directly to the SCTK event loop. + send_popup_direct(SctkPopupSettings { + parent: window_id, + id, + positioner, + parent_size: None, + grab: true, + close_with_children: false, + input_zone: None, + }); +} + +/// Checks whether a popup menu has closed and destroys it if needed. +/// Call this from the text widget's `update()` on each frame. +#[cfg(feature = "wayland")] +pub(crate) fn cleanup_text_popup(menu_bar_state: &MenuBarState) { + let window_id = current_window_id(); + let should_destroy = menu_bar_state + .inner + .with_data(|state| !state.open && state.popup_id.contains_key(&window_id)); + if should_destroy { + menu_bar_state.inner.with_data_mut(|state| { + if let Some(popup_id) = state.popup_id.remove(&window_id) { + send_destroy_popup_direct(popup_id); + } + state.reset(); + }); + } +} + +/// Widget that wraps [`Menu`] inside a Wayland popup and intercepts +/// [`TextCtxAction`] messages for clipboard operations. +#[derive(Clone)] +struct TextContextMenuPopup { + menu: Menu<'static, TextCtxAction>, + selected_text: Option, + pending_action: PendingAction, + _phantom: std::marker::PhantomData, +} + +impl iced_core::widget::Widget + for TextContextMenuPopup +{ + fn size(&self) -> Size { + use iced_core::widget::Widget; + Widget::::size(&self.menu) + } + + fn tag(&self) -> iced_core::widget::tree::Tag { + use iced_core::widget::Widget; + Widget::::tag(&self.menu) + } + + fn state(&self) -> iced_core::widget::tree::State { + use iced_core::widget::Widget; + Widget::::state(&self.menu) + } + + fn children(&self) -> Vec { + use iced_core::widget::Widget; + Widget::::children(&self.menu) + } + + fn diff(&mut self, tree: &mut iced_core::widget::Tree) { + use iced_core::widget::Widget; + Widget::::diff(&mut self.menu, tree); + } + + fn layout( + &mut self, + tree: &mut iced_core::widget::Tree, + renderer: &crate::Renderer, + limits: &iced_core::layout::Limits, + ) -> iced_core::layout::Node { + use iced_core::widget::Widget; + Widget::::layout( + &mut self.menu, + tree, + renderer, + limits, + ) + } + + fn draw( + &self, + tree: &iced_core::widget::Tree, + renderer: &mut crate::Renderer, + theme: &crate::Theme, + style: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + use iced_core::widget::Widget; + Widget::::draw( + &self.menu, tree, renderer, theme, style, layout, cursor, viewport, + ); + } + + fn update( + &mut self, + tree: &mut iced_core::widget::Tree, + event: &event::Event, + layout: Layout<'_>, + cursor: mouse::Cursor, + renderer: &crate::Renderer, + clipboard: &mut dyn Clipboard, + shell: &mut Shell<'_, Message>, + viewport: &Rectangle, + ) { + let mut local_messages: Vec = Vec::new(); + let mut local_shell = Shell::new(&mut local_messages); + + { + use iced_core::widget::Widget; + Widget::::update( + &mut self.menu, + tree, + event, + layout, + cursor, + renderer, + clipboard, + &mut local_shell, + viewport, + ); + } + + if local_shell.is_event_captured() { + shell.capture_event(); + } + shell.request_redraw_at(local_shell.redraw_request()); + if local_shell.is_layout_invalid() { + shell.invalidate_layout(); + } + + for action in local_messages { + match action { + TextCtxAction::Copy => { + if let Some(ref text) = self.selected_text { + clipboard.write(clipboard::Kind::Standard, text.clone()); + } + } + TextCtxAction::Cut => { + if let Some(ref text) = self.selected_text { + clipboard.write(clipboard::Kind::Standard, text.clone()); + } + if let Ok(mut guard) = self.pending_action.lock() { + *guard = Some(TextCtxAction::Cut); + } + } + TextCtxAction::Paste => { + if let Ok(mut guard) = self.pending_action.lock() { + *guard = Some(TextCtxAction::Paste); + } + } + TextCtxAction::SelectAll => { + if let Ok(mut guard) = self.pending_action.lock() { + *guard = Some(TextCtxAction::SelectAll); + } + } + } + } + } + + fn mouse_interaction( + &self, + tree: &iced_core::widget::Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &crate::Renderer, + ) -> mouse::Interaction { + use iced_core::widget::Widget; + Widget::::mouse_interaction( + &self.menu, tree, layout, cursor, viewport, renderer, + ) + } +} + +impl From> + for crate::Element<'static, Message> +{ + fn from(popup: TextContextMenuPopup) -> Self { + Self::new(popup) + } +} diff --git a/src/widget/text_input/cursor.rs b/src/widget/text_input/cursor.rs index 3ffb535c..d83d99b1 100644 --- a/src/widget/text_input/cursor.rs +++ b/src/widget/text_input/cursor.rs @@ -76,6 +76,12 @@ impl Cursor { self.state = State::Index(position); } + pub(crate) fn clear_selection(&mut self) { + if let State::Selection { end, .. } = self.state { + self.state = State::Index(end); + } + } + #[inline] pub(crate) fn move_right(&mut self, value: &Value) { self.move_right_by_amount(value, 1); From d751855c74e81b84646b2dedf0bb09393c71b7b2 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Tue, 19 May 2026 19:36:50 -0600 Subject: [PATCH 176/201] feat: add Wayland popup context menu to TextInput --- src/widget/text_context_menu.rs | 10 - src/widget/text_input/input.rs | 380 +++++++++++++++++++++------ src/widget/wayland/tooltip/widget.rs | 2 - 3 files changed, 301 insertions(+), 91 deletions(-) diff --git a/src/widget/text_context_menu.rs b/src/widget/text_context_menu.rs index 6bf530c9..c8505c1a 100644 --- a/src/widget/text_context_menu.rs +++ b/src/widget/text_context_menu.rs @@ -109,16 +109,6 @@ fn send_popup_direct(settings: iced_runtime::platform_specific::wayland::popup:: ); } -/// Sends a destroy popup action directly to the SCTK event loop. -#[cfg(feature = "wayland")] -fn send_destroy_popup_direct(id: iced_core::window::Id) { - iced_winit::send_wayland_action_direct( - iced_runtime::platform_specific::wayland::Action::Popup( - iced_runtime::platform_specific::wayland::popup::Action::Destroy { id }, - ), - ); -} - /// Creates a context menu overlay for any widget implementing /// [`HasSelectableText`]. /// diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index d382a5a8..c46f895c 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -43,6 +43,12 @@ thread_local! { static LAST_FOCUS_UPDATE: LazyCell> = LazyCell::new(|| Cell::new(Instant::now())); } +/// Notify all text inputs that a different widget has taken focus. +/// This causes any focused text input to unfocus on its next layout. +pub fn notify_focus_change() { + LAST_FOCUS_UPDATE.with(|x| x.set(Instant::now())); +} + /// Creates a new [`TextInput`]. /// /// [`TextInput`]: widget::TextInput @@ -577,6 +583,17 @@ where self.drag_threshold = drag_threshold; self } + + fn uses_popup_context_menu(&self) -> bool { + #[cfg(feature = "wayland")] + if matches!( + crate::app::cosmic::WINDOWING_SYSTEM.get(), + Some(crate::app::cosmic::WindowingSystem::Wayland) + ) { + return true; + } + false + } } impl Widget for TextInput<'_, Message> @@ -843,6 +860,24 @@ where viewport: &Rectangle, translation: Vector, ) -> Option> { + if !self.uses_popup_context_menu() { + let has_context_menu = tree + .state + .downcast_ref::() + .context_menu_position + .is_some(); + if has_context_menu { + let menu_bar_state = tree.state.downcast_ref::().menu_bar_state.clone(); + return crate::widget::text_context_menu::context_menu_overlay( + self, + tree, + self.on_input.as_deref(), + translation, + menu_bar_state, + ); + } + } + let mut layout_ = Vec::with_capacity(2); if self.leading_icon.is_some() { let mut children = self.text_layout(layout).children(); @@ -857,7 +892,7 @@ where } layout_.push(children.next().unwrap()); }; - let children = self + let children: Vec> = self .leading_icon .iter_mut() .chain(self.trailing_icon.iter_mut()) @@ -868,7 +903,7 @@ where .as_widget_mut() .overlay(state, layout, renderer, viewport, translation) }) - .collect::>(); + .collect(); (!children.is_empty()).then(|| Group::with_children(children).overlay()) } @@ -936,6 +971,23 @@ where } } + // Unfocus on any click outside widget bounds. + if matches!( + event, + Event::Mouse(mouse::Event::ButtonPressed(_)) + | Event::Touch(touch::Event::FingerPressed { .. }) + ) && cursor_position.position_over(layout.bounds()).is_none() + { + let state = tree.state.downcast_mut::(); + state.is_focused = None; + state.context_menu_position = None; + state.dragging_state = None; + if let Some(on_unfocus) = self.on_unfocus.as_ref() { + shell.publish(on_unfocus.clone()); + } + return; + } + let state = tree.state.downcast_mut::(); if let Some(on_unfocus) = self.on_unfocus.as_ref() { @@ -977,6 +1029,77 @@ where self.always_active, ); + // On Wayland: if right-click just set context_menu_position, create a popup instead. + #[cfg(feature = "wayland")] + if matches!( + crate::app::cosmic::WINDOWING_SYSTEM.get(), + Some(crate::app::cosmic::WindowingSystem::Wayland) + ) { + let state = tree.state.downcast_ref::(); + if state.context_menu_position.is_some() { + let selected_text = state + .cursor() + .selection(&state.tracked_value) + .map(|(start, end)| state.tracked_value.select(start, end).to_string()); + let has_selection = selected_text.is_some(); + let click_position = state.context_menu_position.unwrap(); + let menu_bar_state = state.menu_bar_state.clone(); + let pending_action = state.pending_action.clone(); + + crate::widget::text_context_menu::create_text_context_popup( + click_position, + selected_text, + true, + has_selection, + &menu_bar_state, + &pending_action, + renderer, + viewport, + cursor_position, + ); + + let state = tree.state.downcast_mut::(); + state.context_menu_position = None; + } + + // Process deferred actions from the popup. + let state = tree.state.downcast_ref::(); + let pending_action = state.pending_action.clone(); + if let Some(action) = + crate::widget::text_context_menu::take_pending_action(&pending_action) + { + let state = tree.state.downcast_mut::(); + match action { + crate::widget::text_context_menu::TextCtxAction::Copy => {} + crate::widget::text_context_menu::TextCtxAction::Cut => { + let contents = state.delete_selection(); + if let Some(on_input) = self.on_input.as_deref() { + shell.publish((on_input)(contents)); + } + } + crate::widget::text_context_menu::TextCtxAction::Paste => { + let content: String = clipboard + .read(iced_core::clipboard::Kind::Standard) + .unwrap_or_default(); + let filtered: String = + content.chars().filter(|c| !c.is_control()).collect(); + let contents = state.paste_text(&filtered); + if let Some(on_input) = self.on_input.as_deref() { + shell.publish((on_input)(contents)); + } + } + crate::widget::text_context_menu::TextCtxAction::SelectAll => { + state.select_all(); + } + } + } + + // Cleanup popup if menu closed. + let state = tree.state.downcast_ref::(); + let menu_bar_state = state.menu_bar_state.clone(); + crate::widget::text_context_menu::cleanup_text_popup(&menu_bar_state); + } + let state = tree.state.downcast_mut::(); let value = if self.is_secure { self.value.secure() @@ -1461,11 +1584,44 @@ pub fn update<'a, Message: Clone + 'static>( #[cold] fn cold() {} + let state = state(); + + // Any click outside clears focus and selection. + if matches!( + event, + Event::Mouse(mouse::Event::ButtonPressed(_)) + | Event::Touch(touch::Event::FingerPressed { .. }) + ) && cursor.position_over(layout.bounds()).is_none() + { + state.is_focused = None; + state.context_menu_position = None; + state.dragging_state = None; + if let Some(on_unfocus) = on_unfocus { + shell.publish(on_unfocus.clone()); + } + return; + } + match event { + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) => { + if let Some(pos) = cursor.position_over(layout.bounds()) { + if !state.is_focused() { + state.focus(); + } + state.context_menu_position = Some(pos); + shell.capture_event(); + return; + } + } + Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) | Event::Touch(touch::Event::FingerPressed { .. }) => { cold(); - let state = state(); + + if state.context_menu_position.take().is_some() { + shell.capture_event(); + return; + } let click_position = if on_input.is_some() || manage_value { cursor.position_over(layout.bounds()) @@ -1694,7 +1850,6 @@ pub fn update<'a, Message: Clone + 'static>( Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => { cold(); - let state = state(); #[cfg(wayland_platform)] if matches!(state.dragging_state, Some(DraggingState::PrepareDnd(_))) { // clear selection and place cursor at click position @@ -1722,8 +1877,6 @@ pub fn update<'a, Message: Clone + 'static>( } Event::Mouse(mouse::Event::CursorMoved { position }) | Event::Touch(touch::Event::FingerMoved { position, .. }) => { - let state = state(); - if matches!(state.dragging_state, Some(DraggingState::Selection)) { let target = { let text_bounds = text_layout.bounds(); @@ -1812,7 +1965,6 @@ pub fn update<'a, Message: Clone + 'static>( modifiers, .. }) => { - let state = state(); state.keyboard_modifiers = *modifiers; if let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) { @@ -2107,8 +2259,6 @@ pub fn update<'a, Message: Clone + 'static>( } } Event::Keyboard(keyboard::Event::KeyReleased { key, .. }) => { - let state = state(); - if state.is_focused() { match key { keyboard::Key::Character(c) if "v" == c => { @@ -2130,73 +2280,65 @@ pub fn update<'a, Message: Clone + 'static>( } } Event::Keyboard(keyboard::Event::ModifiersChanged(modifiers)) => { - let state = state(); - 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() { - 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) = state.is_focused.as_mut().filter(|f| f.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); + Event::InputMethod(event) => 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() { + state.preedit = Some(input_method::Preedit { + content: content.to_owned(), + selection: selection.clone(), + text_size: Some(size.into()), + }); shell.capture_event(); return; } } - } - Event::Window(window::Event::RedrawRequested(now)) => { - let state = state(); + input_method::Event::Commit(text) => { + let Some(focus) = state.is_focused.as_mut().filter(|f| f.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)) => { if let Some(focus) = state.is_focused.as_mut().filter(|f| f.focused) { focus.now = *now; @@ -2215,7 +2357,6 @@ pub fn update<'a, Message: Clone + 'static>( #[cfg(wayland_platform)] Event::Dnd(DndEvent::Source(SourceEvent::Finished | SourceEvent::Cancelled)) => { cold(); - let state = state(); if matches!(state.dragging_state, Some(DraggingState::Dnd(..))) { // TODO: restore value in text input state.dragging_state = None; @@ -2234,7 +2375,6 @@ pub fn update<'a, Message: Clone + 'static>( }, )) 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, @@ -2277,8 +2417,6 @@ pub fn update<'a, Message: Clone + 'static>( Event::Dnd(DndEvent::Offer(rectangle, OfferEvent::Motion { x, y })) if *rectangle == Some(dnd_id) => { - let state = state(); - let target = { let text_bounds = text_layout.bounds(); @@ -2304,7 +2442,6 @@ pub fn update<'a, Message: Clone + 'static>( #[cfg(wayland_platform)] 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() { let Some(mime_type) = SUPPORTED_TEXT_MIME_TYPES .iter() @@ -2327,7 +2464,6 @@ pub fn update<'a, Message: Clone + 'static>( OfferEvent::Leave | OfferEvent::LeaveDestination, )) => { cold(); - let state = state(); // ASHLEY TODO we should be able to reset but for now we don't if we are handling a // drop match state.dnd_offer { @@ -2344,7 +2480,6 @@ pub fn update<'a, Message: Clone + 'static>( 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() { @@ -2917,7 +3052,7 @@ pub(crate) enum DndOfferState { pub(crate) struct DndOfferState; /// The state of a [`TextInput`]. -#[derive(Debug, Default, Clone)] +#[derive(Default, Clone)] #[must_use] pub struct State { pub tracked_value: Value, @@ -2940,6 +3075,19 @@ pub struct State { preedit: Option, keyboard_modifiers: keyboard::Modifiers, scroll_offset: f32, + context_menu_position: Option, + pub(crate) menu_bar_state: crate::widget::menu::MenuBarState, + pub(crate) pending_action: crate::widget::text_context_menu::PendingAction, +} + +impl std::fmt::Debug for State { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("State") + .field("is_secure", &self.is_secure) + .field("is_read_only", &self.is_read_only) + .field("dirty", &self.dirty) + .finish_non_exhaustive() + } } #[derive(Debug, Clone, Copy)] @@ -2984,7 +3132,7 @@ impl State { cursor::State::Selection { start, end } => { let left = start.min(end); let right = end.max(start); - Some(text[left..right].to_string()) + Some(value.select(left, right).to_string()) } } } @@ -3022,6 +3170,9 @@ impl State { keyboard_modifiers: keyboard::Modifiers::default(), scroll_offset: 0.0, dirty: false, + context_menu_position: None, + menu_bar_state: crate::widget::menu::MenuBarState::default(), + pending_action: crate::widget::text_context_menu::pending_action(), } } @@ -3066,7 +3217,7 @@ impl State { /// Unfocuses the [`TextInput`]. #[cold] pub(super) fn unfocus(&mut self) { - self.move_cursor_to_front(); + self.cursor.clear_selection(); self.last_click = None; self.is_focused = self.is_focused.map(|mut f| { f.focused = false; @@ -3108,6 +3259,33 @@ impl State { self.cursor.select_range(start, end); } + /// Returns the context menu position, if a context menu should be shown. + pub fn context_menu_position(&self) -> Option { + self.context_menu_position + } + + /// Sets or clears the context menu position. + pub fn set_context_menu_position(&mut self, pos: Option) { + self.context_menu_position = pos; + } + + /// Deletes the current selection and returns the new text content. + pub fn delete_selection(&mut self) -> String { + let mut editor = super::editor::Editor::new(&mut self.tracked_value, &mut self.cursor); + editor.delete(); + editor.contents() + } + + /// Pastes text at the current cursor position and returns the new text content. + pub fn paste_text(&mut self, text: &str) -> String { + let paste_value = super::value::Value::new(text); + let mut editor = super::editor::Editor::new(&mut self.tracked_value, &mut self.cursor); + editor.paste(paste_value); + let contents = editor.contents(); + self.tracked_value = super::value::Value::new(&contents); + contents + } + pub(super) fn setting_selection(&mut self, value: &Value, bounds: Rectangle, target: f32) { let (position, affinity) = find_cursor_position(bounds, value, self, target) .unwrap_or((0, text::Affinity::Before)); @@ -3335,3 +3513,47 @@ fn effective_alignment(paragraph: &impl text::Paragraph) -> alignment::Horizonta alignment::Horizontal::Left } } + +use iced_core::widget::tree::Tree as WidgetTree; + +impl iced_core::widget::text::HasSelectableText + for TextInput<'_, Message> +{ + fn selected_text(&self, tree: &WidgetTree) -> Option { + let state = tree.state.downcast_ref::(); + let (start, end) = state.cursor().selection(&state.tracked_value)?; + Some(state.tracked_value.select(start, end).to_string()) + } + + fn select_all(&self, tree: &mut WidgetTree) { + let state = tree.state.downcast_mut::(); + state.select_all(); + } + + fn is_editable(&self) -> bool { + true + } + + fn is_focused(&self, tree: &WidgetTree) -> bool { + tree.state.downcast_ref::().is_focused() + } + + fn context_menu_position(&self, tree: &WidgetTree) -> Option { + tree.state.downcast_ref::().context_menu_position + } + + fn set_context_menu_position(&self, tree: &mut WidgetTree, pos: Option) { + tree.state.downcast_mut::().context_menu_position = pos; + } + + fn delete_selection(&self, tree: &mut WidgetTree) -> Option { + let state = tree.state.downcast_mut::(); + Some(state.delete_selection()) + } + + fn paste_text(&self, tree: &mut WidgetTree, text: &str) -> Option { + let filtered: String = text.chars().filter(|c| !c.is_control()).collect(); + let state = tree.state.downcast_mut::(); + Some(state.paste_text(&filtered)) + } +} diff --git a/src/widget/wayland/tooltip/widget.rs b/src/widget/wayland/tooltip/widget.rs index a5e4520a..b0287eed 100644 --- a/src/widget/wayland/tooltip/widget.rs +++ b/src/widget/wayland/tooltip/widget.rs @@ -301,8 +301,6 @@ impl<'a, Message: 'static + Clone, TopLevelMessage: 'static + Clone> } let content_layout = layout.children().next().unwrap(); - let state = tree.state.downcast_ref::(); - let styling = theme.style(&self.style); let icon_color = styling.icon_color.unwrap_or(renderer_style.icon_color); From a70feab2bff23d9a6066fa6b4977a028cc5b4b02 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Tue, 19 May 2026 19:36:54 -0600 Subject: [PATCH 177/201] feat: integrate text context menu popups into Cosmic framework view system --- src/app/cosmic.rs | 15 +++++++++++++-- 1 file changed, 13 insertions(+), 2 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index d31b6088..cc689223 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -513,7 +513,7 @@ where &mut self, message: crate::Action, ) -> iced::Task> { - let message = match message { + let mut task = match message { crate::Action::App(message) => self.app.update(message), crate::Action::Cosmic(message) => self.cosmic_update(message), crate::Action::None => iced::Task::none(), @@ -533,7 +533,7 @@ where #[cfg(all(target_env = "gnu", not(target_os = "windows")))] crate::malloc::trim(0); - message + task } #[cfg(not(feature = "multi-window"))] @@ -712,9 +712,15 @@ where #[cfg(feature = "multi-window")] pub fn view(&self, id: window::Id) -> Element<'_, crate::Action> { + #[cfg(all(feature = "wayland", target_os = "linux"))] if let Some((_, _, _, Some(v))) = self.surface_views.get(&id) { return v(&self.app); } + #[cfg(feature = "wayland")] + if let Some(element) = crate::widget::text_context_menu::popup_view_for_id::(id) + { + return element; + } if self .app .core() @@ -724,6 +730,8 @@ where return self.app.view_window(id).map(crate::Action::App); } + crate::widget::text_context_menu::set_current_window_id(id); + let view = if self.app.core().window.use_template { self.app.view_main() } else { @@ -738,6 +746,9 @@ where #[cfg(not(feature = "multi-window"))] pub fn view(&self) -> Element> { + if let Some(id) = self.app.core().main_window_id() { + crate::widget::text_context_menu::set_current_window_id(id); + } let view = self.app.view_main(); #[cfg(all(target_env = "gnu", not(target_os = "windows")))] From 8b31e83d5827891df54172be7a2d58d84c6bdc8d Mon Sep 17 00:00:00 2001 From: Hojjat Date: Wed, 20 May 2026 10:24:28 -0600 Subject: [PATCH 178/201] fix(test): `--features applet` only fix(test): `--feature wayland` only --- src/applet/mod.rs | 4 ++-- src/widget/text.rs | 2 +- src/widget/text_editor.rs | 2 +- src/widget/text_input/input.rs | 4 ++-- 4 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/applet/mod.rs b/src/applet/mod.rs index 37635bf4..b9805c9b 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -290,7 +290,7 @@ impl Context { ) } - pub fn applet_tooltip<'a, Message: 'static>( + pub fn applet_tooltip<'a, Message: Clone + 'static>( &self, content: impl Into>, tooltip: impl Into>, @@ -502,7 +502,7 @@ impl Context { } } - pub fn text<'a>(&self, msg: impl Into>) -> crate::widget::Text<'a, crate::Theme> { + pub fn text<'a>(&self, msg: impl Into>) -> crate::widget::Text<'a> { let msg = msg.into(); let t = match self.size { Size::Hardcoded(_) => crate::widget::text, diff --git a/src/widget/text.rs b/src/widget/text.rs index 9fe051ac..192adda3 100644 --- a/src/widget/text.rs +++ b/src/widget/text.rs @@ -50,7 +50,7 @@ impl<'a> Text<'a> { } fn uses_popup_context_menu(&self) -> bool { - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", feature = "winit"))] if matches!( crate::app::cosmic::WINDOWING_SYSTEM.get(), Some(crate::app::cosmic::WindowingSystem::Wayland) diff --git a/src/widget/text_editor.rs b/src/widget/text_editor.rs index 9bbe2718..368beba2 100644 --- a/src/widget/text_editor.rs +++ b/src/widget/text_editor.rs @@ -130,7 +130,7 @@ impl<'a, Message: Clone + 'static> TextEditor<'a, Message> { } fn uses_popup_context_menu(&self) -> bool { - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", feature = "winit"))] if matches!( crate::app::cosmic::WINDOWING_SYSTEM.get(), Some(crate::app::cosmic::WindowingSystem::Wayland) diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index c46f895c..fe5855e5 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -585,7 +585,7 @@ where } fn uses_popup_context_menu(&self) -> bool { - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", feature = "winit"))] if matches!( crate::app::cosmic::WINDOWING_SYSTEM.get(), Some(crate::app::cosmic::WindowingSystem::Wayland) @@ -1030,7 +1030,7 @@ where ); // On Wayland: if right-click just set context_menu_position, create a popup instead. - #[cfg(feature = "wayland")] + #[cfg(all(feature = "wayland", feature = "winit"))] if matches!( crate::app::cosmic::WINDOWING_SYSTEM.get(), Some(crate::app::cosmic::WindowingSystem::Wayland) From 1de6ce9460b419344f6f5f2c564c7e55350dcd7f Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 29 Jun 2026 16:09:28 -0600 Subject: [PATCH 179/201] fix: dismiss the pop up on click outside or esc --- src/app/cosmic.rs | 36 ++- src/widget/text.rs | 2 +- src/widget/text_context_menu.rs | 397 ++++++++++++++++++++++---------- src/widget/text_editor.rs | 4 +- src/widget/text_input/input.rs | 11 +- 5 files changed, 312 insertions(+), 138 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index cc689223..5a83f66e 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -530,6 +530,29 @@ where } }; + // Drain any text context-menu popup teardown requests queued by + // widgets during `app.update()` and destroy them through the normal + // popup pipeline. Drained before the creation queue so a + // destroy-then-recreate (a second right-click reusing the same id) + // keeps its order. + #[cfg(all(feature = "wayland", target_os = "linux"))] + for id in crate::widget::text_context_menu::take_popup_destroys() { + task = task.chain(iced_winit::commands::popup::destroy_popup(id)); + } + + // Drain any text context-menu popup requests queued by widgets during + // `app.update()` and create them through the normal popup pipeline. + #[cfg(all(feature = "wayland", target_os = "linux"))] + for req in crate::widget::text_context_menu::take_popup_requests() { + let (settings, view) = + crate::widget::text_context_menu::into_popup_view::(req); + task = task.chain(self.get_popup( + settings, + Box::new(|_| LiveSettings::default()), + Some(Box::new(move |_| view())), + )); + } + #[cfg(all(target_env = "gnu", not(target_os = "windows")))] crate::malloc::trim(0); @@ -697,6 +720,14 @@ where subscriptions.push(crate::dbus_activation::subscription::()); } + // Drives the text context-menu popup queues: a right-click queues a + // popup but publishes no message, so this re-emits `Action::None` to + // make `update()` run and drain the queue. + #[cfg(all(feature = "wayland", target_os = "linux"))] + subscriptions.push(crate::widget::text_context_menu::wake_subscription::< + T::Message, + >()); + Subscription::batch(subscriptions) } @@ -716,11 +747,6 @@ where if let Some((_, _, _, Some(v))) = self.surface_views.get(&id) { return v(&self.app); } - #[cfg(feature = "wayland")] - if let Some(element) = crate::widget::text_context_menu::popup_view_for_id::(id) - { - return element; - } if self .app .core() diff --git a/src/widget/text.rs b/src/widget/text.rs index 192adda3..5c490d4d 100644 --- a/src/widget/text.rs +++ b/src/widget/text.rs @@ -360,7 +360,7 @@ impl<'a, Message: Clone + 'static> Widget for T let wrapper_state = tree.state.downcast_ref::(); let menu_bar_state = wrapper_state.menu_bar_state.clone(); - crate::widget::text_context_menu::cleanup_text_popup(&menu_bar_state); + crate::widget::text_context_menu::dismiss_popup_on_event(&menu_bar_state, event); } } else { w_mut::(&mut self.inner).update( diff --git a/src/widget/text_context_menu.rs b/src/widget/text_context_menu.rs index c8505c1a..7912e37c 100644 --- a/src/widget/text_context_menu.rs +++ b/src/widget/text_context_menu.rs @@ -29,7 +29,6 @@ use iced_core::{ }; use iced_widget::core::event; use std::borrow::Cow; -use std::mem; use std::sync::{Arc, Mutex}; /// Shared state for communicating deferred context menu actions @@ -47,25 +46,30 @@ pub(crate) fn take_pending_action(pending: &PendingAction) -> Option = const { Cell::new(iced_core::window::Id::NONE) }; } -/// Data needed to construct a popup view. Stored in a global registry -/// so `Cosmic::view()` can build the `Element` with the correct message type. #[cfg(feature = "wayland")] -struct PopupViewData { +use iced_runtime::platform_specific::wayland::popup::SctkPopupSettings; + +/// A request to create a text context-menu popup surface, queued by a widget +/// during `update()` and drained by `Cosmic::update()` so the popup goes +/// through the normal `get_popup()` Task + `surface_views` pipeline. +#[cfg(feature = "wayland")] +pub(crate) struct PopupRequest { + settings: SctkPopupSettings, menu: Menu<'static, TextCtxAction>, selected_text: Option, pending_action: PendingAction, } #[cfg(feature = "wayland")] -static POPUP_VIEW_REGISTRY: LazyLock>> = - LazyLock::new(|| Mutex::new(HashMap::new())); +thread_local! { + static PENDING_POPUP_REQUESTS: std::cell::RefCell> = + const { std::cell::RefCell::new(Vec::new()) }; +} pub(crate) fn set_current_window_id(id: iced_core::window::Id) { CURRENT_WINDOW_ID.set(id); @@ -75,38 +79,108 @@ pub(crate) fn current_window_id() -> iced_core::window::Id { CURRENT_WINDOW_ID.get() } -/// Returns the popup view `Element` for a given popup ID, if one exists. -/// Called by `Cosmic::view()` where `Message` is known. +/// Drains all popup requests queued by widgets this frame. #[cfg(feature = "wayland")] -pub(crate) fn popup_view_for_id( - id: iced_core::window::Id, -) -> Option>> { - let registry = POPUP_VIEW_REGISTRY.lock().ok()?; - let data = registry.get(&id)?; - let popup_widget: TextContextMenuPopup = TextContextMenuPopup { - menu: data.menu.clone(), - selected_text: data.selected_text.clone(), - pending_action: data.pending_action.clone(), - _phantom: std::marker::PhantomData, - }; - drop(registry); - Some( - crate::Element::from( - crate::widget::container(popup_widget).center(iced_core::Length::Fill), - ) - .map(crate::action::app), - ) +pub(crate) fn take_popup_requests() -> Vec { + PENDING_POPUP_REQUESTS.with(|q| std::mem::take(&mut *q.borrow_mut())) } -/// Sends a wayland popup action directly to the SCTK event loop, -/// bypassing the iced Task system. +/// Consumes a [`PopupRequest`], returning the popup settings plus a view +/// builder. The builder rebuilds the menu element each frame from the +/// captured content, independent of app state. #[cfg(feature = "wayland")] -fn send_popup_direct(settings: iced_runtime::platform_specific::wayland::popup::SctkPopupSettings) { - iced_winit::send_wayland_action_direct( - iced_runtime::platform_specific::wayland::Action::Popup( - iced_runtime::platform_specific::wayland::popup::Action::Popup { popup: settings }, - ), - ); +#[allow(clippy::type_complexity)] +pub(crate) fn into_popup_view( + req: PopupRequest, +) -> ( + SctkPopupSettings, + Box crate::Element<'static, crate::Action> + Send + Sync>, +) { + let PopupRequest { + settings, + menu, + selected_text, + pending_action, + } = req; + + let view = Box::new(move || { + let popup_widget: TextContextMenuPopup = TextContextMenuPopup { + menu: menu.clone(), + selected_text: selected_text.clone(), + pending_action: pending_action.clone(), + _phantom: std::marker::PhantomData, + }; + crate::Element::from(crate::widget::container(popup_widget).center(iced_core::Length::Fill)) + .map(crate::action::app) + }); + + (settings, view) +} + +#[cfg(feature = "wayland")] +thread_local! { + static PENDING_POPUP_DESTROYS: std::cell::RefCell> = + const { std::cell::RefCell::new(Vec::new()) }; +} + +/// Queues a context-menu popup for teardown. +/// +/// Mirrors [`create_text_context_popup`]'s request queue: widgets running +/// inside `update()` can't reach `Cosmic` to issue a Task, so they push the +/// id here and `Cosmic::update()` drains it into a `destroy_popup` Task that +/// flows through the normal surface pipeline. +#[cfg(feature = "wayland")] +fn queue_destroy_popup(id: iced_core::window::Id) { + PENDING_POPUP_DESTROYS.with(|q| q.borrow_mut().push(id)); + wake_runtime(); +} + +#[cfg(feature = "wayland")] +static WAKE_TX: std::sync::OnceLock> = + std::sync::OnceLock::new(); + +/// Stable identity for [`wake_subscription`]. +#[cfg(feature = "wayland")] +struct PopupWake; + +/// Nudges the runtime so `Cosmic::update()` runs and drains the popup queues. +#[cfg(feature = "wayland")] +fn wake_runtime() { + if let Some(tx) = WAKE_TX.get() { + let _ = tx.clone().try_send(()); + } +} + +/// Subscription that backs [`wake_runtime`]: it owns the receiving end of the +/// wake channel and re-emits each ping as [`crate::Action::None`]. Add it to +/// the app's subscriptions (done by `Cosmic::subscription`) so popup creation +/// and teardown queued from widget `update()` get drained promptly. +#[cfg(feature = "wayland")] +pub(crate) fn wake_subscription() +-> iced_futures::Subscription> { + use iced_futures::futures::{SinkExt, StreamExt}; + iced_futures::Subscription::run_with(std::any::TypeId::of::(), |_| { + iced::stream::channel( + 16, + |mut output: iced_futures::futures::channel::mpsc::Sender>| async move { + let (tx, mut rx) = iced_futures::futures::channel::mpsc::channel(16); + let _ = WAKE_TX.set(tx); + while rx.next().await.is_some() { + let _ = output.send(crate::Action::None).await; + } + }, + ) + }) +} + +/// Drains all popup teardown requests queued by widgets this frame. +/// +/// Called by `Cosmic::update()`, which turns each id into a `destroy_popup()` +/// Task. Drained before the creation queue so a destroy-then-recreate (a +/// second right-click reusing the same id) keeps its order. +#[cfg(wayland_platform)] +pub(crate) fn take_popup_destroys() -> Vec { + PENDING_POPUP_DESTROYS.with(|q| std::mem::take(&mut *q.borrow_mut())) } /// Creates a context menu overlay for any widget implementing @@ -135,12 +209,20 @@ where let mut menu_roots = build_menu_roots(is_editable, selected_text.is_some()); menu_roots.iter_mut().for_each(menu::Tree::set_index); + let bounds = Rectangle { + x: click_position.x, + y: click_position.y, + width: 240.0, + height: 240.0, + }; + let item_count = menu_roots[0].children.len(); menu_bar_state.inner.with_data_mut(|state| { - let stale = state - .menu_states - .first() - .is_some_and(|ms| ms.menu_bounds.child_positions.len() != item_count); + let stale = state.menu_states.first().is_some_and(|ms| { + ms.menu_bounds.child_positions.len() != item_count + || (ms.menu_bounds.parent_bounds.x - bounds.x).abs() > 0.5 + || (ms.menu_bounds.parent_bounds.y - bounds.y).abs() > 0.5 + }); if !state.open || stale { state.menu_states.clear(); state.active_root.clear(); @@ -149,8 +231,6 @@ where menu_roots_diff(&mut menu_roots, &mut state.tree); }); - let offscreen = Rectangle::new(Point::new(-10000.0, -10000.0), Size::ZERO); - let menu = Menu { tree: menu_bar_state.clone(), menu_roots: Cow::Owned(menu_roots), @@ -163,10 +243,10 @@ where }, item_width: ItemWidth::Uniform(240), item_height: ItemHeight::Dynamic(40), - bar_bounds: offscreen, - main_offset: -240, + bar_bounds: bounds, + main_offset: -(bounds.height as i32), cross_offset: 0, - root_bounds_list: vec![offscreen], + root_bounds_list: vec![bounds], path_highlight: Some(PathHighlight::MenuActive), style: Cow::Owned(theme::menu_bar::MenuBarStyle::Default), position: Point::new(translation.x, translation.y), @@ -181,7 +261,6 @@ where widget, tree, on_input, - click_position, }))) } @@ -227,7 +306,6 @@ struct TextMenuOverlay<'a, W, Message: Clone + 'static> { widget: &'a W, tree: &'a mut Tree, on_input: Option<&'a dyn Fn(String) -> Message>, - click_position: Point, } impl overlay::Overlay @@ -237,6 +315,34 @@ where Message: Clone + 'static, { fn layout(&mut self, renderer: &crate::Renderer, bounds: Size) -> iced_core::layout::Node { + // Initialise the menu before the first draw so it appears at the click + // position immediately + let needs_init = self + .menu + .tree + .inner + .with_data(|state| state.open && state.menu_states.is_empty()); + + if needs_init { + let overlay_offset = Point::ORIGIN - self.menu.position; + let bar_bounds = self.menu.bar_bounds; + let main_offset = self.menu.main_offset as f32; + let overlay_cursor = bar_bounds.center(); + + let mut init_messages: Vec = Vec::new(); + let mut init_shell = Shell::new(&mut init_messages); + menu::init_root_menu( + &mut self.menu, + renderer, + &mut init_shell, + overlay_cursor, + bounds, + overlay_offset, + bar_bounds, + main_offset, + ); + } + self.menu.layout( renderer, Limits::NONE @@ -267,59 +373,27 @@ where clipboard: &mut dyn Clipboard, shell: &mut Shell<'_, Message>, ) { - // Pre-initialize the menu if it hasn't been yet. - let needs_init = self - .menu - .tree - .inner - .with_data(|state| state.open && state.menu_states.is_empty()); - - if needs_init { - let viewport = layout.bounds(); - let viewport_size = viewport.size(); - let overlay_offset = Point::ORIGIN - viewport.position(); - let overlay_cursor = cursor.position().unwrap_or_default() - overlay_offset; - - let init_bounds = Rectangle { - x: self.click_position.x, - y: self.click_position.y, - width: 240.0, - height: 240.0, - }; - - // Temporarily set real bounds so init_root_menu can find - // root_bounds_list entries that contain the cursor. - let main_offset = self.menu.main_offset as f32; - let saved_bounds = mem::replace(&mut self.menu.bar_bounds, init_bounds); - let saved_roots = mem::replace(&mut self.menu.root_bounds_list, vec![init_bounds]); - - let mut init_messages: Vec = Vec::new(); - let mut init_shell = Shell::new(&mut init_messages); - menu::init_root_menu( - &mut self.menu, - renderer, - &mut init_shell, - overlay_cursor, - viewport_size, - overlay_offset, - init_bounds, - main_offset, - ); - - // Restore offscreen bounds - self.menu.bar_bounds = saved_bounds; - self.menu.root_bounds_list = saved_roots; - - if init_shell.is_layout_invalid() { - shell.invalidate_layout(); - } - } - - // Right-button releases are meaningless for menu interaction + // Right-clicks are not menu interactions. A right-click *on* the menu + // (notably the press/release that opened it) is swallowed so it does + // not close the menu. A right-click *outside* closes it — clearing the + // menu position lets the widget reopen it at the new point on the same + // event. Initialization happens in `layout()`. if matches!( event, - event::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) + event::Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Right)) + | event::Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Right)) ) { + let over_menu = cursor + .position() + .is_some_and(|p| layout.bounds().contains(p)); + if !over_menu { + self.menu.tree.inner.with_data_mut(|state| { + state.menu_states.clear(); + state.active_root.clear(); + state.open = false; + }); + self.widget.set_context_menu_position(self.tree, None); + } return; } @@ -387,11 +461,11 @@ where } } -/// Creates a Wayland popup surface containing the text context menu. +/// Queues a Wayland popup surface containing the text context menu. /// -/// Sends the popup action directly to the SCTK event loop and registers -/// the view data so `Cosmic::view()` can render it. No app-level wiring -/// is needed +/// Pushes a [`PopupRequest`] onto the request queue; `Cosmic::update()` +/// drains it and creates the popup through `get_popup()`, so it flows +/// through the normal Task + `surface_views` pipeline. #[cfg(feature = "wayland")] pub(crate) fn create_text_context_popup( click_position: Point, @@ -419,7 +493,7 @@ pub(crate) fn create_text_context_popup( state.active_root.clear(); menu_roots_diff(&mut menu_roots, &mut state.tree); if let Some(id) = state.popup_id.get(&window_id).copied() { - send_destroy_popup_direct(id); + queue_destroy_popup(id); state.view_cursor = cursor; id } else { @@ -502,20 +576,10 @@ pub(crate) fn create_text_context_popup( ..Default::default() }; - // Register view data so Cosmic::view() can render this popup. - if let Ok(mut registry) = POPUP_VIEW_REGISTRY.lock() { - registry.insert( - id, - PopupViewData { - menu: popup_menu, - selected_text, - pending_action: pending_action.clone(), - }, - ); - } - - // Send the popup creation directly to the SCTK event loop. - send_popup_direct(SctkPopupSettings { + // Queue the request. `Cosmic::update()` drains it and creates the popup + // through `get_popup()`, so it flows through the normal Task + + // `surface_views` pipeline (rendering and teardown included). + let settings = SctkPopupSettings { parent: window_id, id, positioner, @@ -523,24 +587,46 @@ pub(crate) fn create_text_context_popup( grab: true, close_with_children: false, input_zone: None, + }; + + PENDING_POPUP_REQUESTS.with(|q| { + q.borrow_mut().push(PopupRequest { + settings, + menu: popup_menu, + selected_text, + pending_action: pending_action.clone(), + }); }); + wake_runtime(); } -/// Checks whether a popup menu has closed and destroys it if needed. -/// Call this from the text widget's `update()` on each frame. +/// Dismisses this widget's open context-menu popup on an outside click, +/// touch, or Escape. #[cfg(feature = "wayland")] -pub(crate) fn cleanup_text_popup(menu_bar_state: &MenuBarState) { +pub(crate) fn dismiss_popup_on_event(menu_bar_state: &MenuBarState, event: &event::Event) { + let is_dismiss = matches!( + event, + event::Event::Mouse(mouse::Event::ButtonPressed( + mouse::Button::Left | mouse::Button::Middle + )) | event::Event::Keyboard(iced_core::keyboard::Event::KeyPressed { + key: iced_core::keyboard::Key::Named(iced_core::keyboard::key::Named::Escape), + .. + }) | event::Event::Touch(iced_core::touch::Event::FingerPressed { .. }) + ); + if !is_dismiss { + return; + } + let window_id = current_window_id(); - let should_destroy = menu_bar_state + let popup_id = menu_bar_state .inner - .with_data(|state| !state.open && state.popup_id.contains_key(&window_id)); - if should_destroy { + .with_data(|state| state.popup_id.get(&window_id).copied()); + if let Some(popup_id) = popup_id { menu_bar_state.inner.with_data_mut(|state| { - if let Some(popup_id) = state.popup_id.remove(&window_id) { - send_destroy_popup_direct(popup_id); - } + state.popup_id.retain(|_, v| *v != popup_id); state.reset(); }); + queue_destroy_popup(popup_id); } } @@ -624,6 +710,50 @@ impl iced_core::widget::Widget, viewport: &Rectangle, ) { + #[cfg(feature = "wayland")] + { + use iced_core::event::wayland::PopupEvent; + let popup_event = match event { + event::Event::PlatformSpecific(iced_core::event::PlatformSpecific::Wayland( + iced_core::event::wayland::Event::Popup(e, _, _), + )) => Some(e), + _ => None, + }; + if matches!(popup_event, Some(PopupEvent::Done | PopupEvent::Unfocused)) { + let popup_id = self.menu.window_id; + self.menu.tree.inner.with_data_mut(|state| { + state.popup_id.retain(|_, v| *v != popup_id); + state.reset(); + }); + // `Done` means the surface was already destroyed by the + // compositor; only `Unfocused` needs an explicit destroy. + if matches!(popup_event, Some(PopupEvent::Unfocused)) { + queue_destroy_popup(popup_id); + } + return; + } + } + + // Escape dismisses the popup. Under the grab, keyboard input is + // delivered to the popup surface, so handle it here. + #[cfg(feature = "wayland")] + if matches!( + event, + event::Event::Keyboard(iced_core::keyboard::Event::KeyPressed { + key: iced_core::keyboard::Key::Named(iced_core::keyboard::key::Named::Escape), + .. + }) + ) { + let popup_id = self.menu.window_id; + self.menu.tree.inner.with_data_mut(|state| { + state.popup_id.retain(|_, v| *v != popup_id); + state.reset(); + }); + queue_destroy_popup(popup_id); + shell.capture_event(); + return; + } + let mut local_messages: Vec = Vec::new(); let mut local_shell = Shell::new(&mut local_messages); @@ -677,6 +807,23 @@ impl iced_core::widget::Widget Widget(); let menu_bar_state = wrapper_state.menu_bar_state.clone(); - crate::widget::text_context_menu::cleanup_text_popup(&menu_bar_state); + crate::widget::text_context_menu::dismiss_popup_on_event(&menu_bar_state, event); } } else { ew_mut::(&mut self.inner).update( diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index fe5855e5..95c97d15 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -919,6 +919,12 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { + #[cfg(all(feature = "wayland", feature = "winit"))] + if self.uses_popup_context_menu() { + let menu_bar_state = tree.state.downcast_ref::().menu_bar_state.clone(); + crate::widget::text_context_menu::dismiss_popup_on_event(&menu_bar_state, event); + } + let text_layout = self.text_layout(layout); let mut trailing_icon_layout = None; let font = self.font.unwrap_or_else(|| renderer.default_font()); @@ -1093,11 +1099,6 @@ where } } } - - // Cleanup popup if menu closed. - let state = tree.state.downcast_ref::(); - let menu_bar_state = state.menu_bar_state.clone(); - crate::widget::text_context_menu::cleanup_text_popup(&menu_bar_state); } let state = tree.state.downcast_mut::(); From 8a03f063c015bee0a0031f24c1082a086791e4a9 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 29 Jun 2026 16:49:35 -0600 Subject: [PATCH 180/201] refactor: rename to `SelectableText` instead of `Text` This avoids API breakage --- src/applet/mod.rs | 4 +- src/widget/menu/menu_tree.rs | 2 +- src/widget/mod.rs | 4 + src/widget/selectable_text.rs | 505 ++++++++++++++++++++++++++++++++ src/widget/settings/item.rs | 8 +- src/widget/text.rs | 525 +++------------------------------- 6 files changed, 557 insertions(+), 491 deletions(-) create mode 100644 src/widget/selectable_text.rs diff --git a/src/applet/mod.rs b/src/applet/mod.rs index b9805c9b..37635bf4 100644 --- a/src/applet/mod.rs +++ b/src/applet/mod.rs @@ -290,7 +290,7 @@ impl Context { ) } - pub fn applet_tooltip<'a, Message: Clone + 'static>( + pub fn applet_tooltip<'a, Message: 'static>( &self, content: impl Into>, tooltip: impl Into>, @@ -502,7 +502,7 @@ impl Context { } } - pub fn text<'a>(&self, msg: impl Into>) -> crate::widget::Text<'a> { + pub fn text<'a>(&self, msg: impl Into>) -> crate::widget::Text<'a, crate::Theme> { let msg = msg.into(); let t = match self.size { Size::Hardcoded(_) => crate::widget::text, diff --git a/src/widget/menu/menu_tree.rs b/src/widget/menu/menu_tree.rs index cf99335c..bd4f1e6c 100644 --- a/src/widget/menu/menu_tree.rs +++ b/src/widget/menu/menu_tree.rs @@ -194,7 +194,7 @@ pub fn menu_root<'a, Message, Renderer: renderer::Renderer>( ) -> Button<'a, Message> where Element<'a, Message, crate::Theme, Renderer>: From>, - Message: std::clone::Clone + 'static, + Message: std::clone::Clone + 'a, { widget::button::custom(widget::text(label)) .padding([4, 12]) diff --git a/src/widget/mod.rs b/src/widget/mod.rs index 4c33f7e3..ed6f55d7 100644 --- a/src/widget/mod.rs +++ b/src/widget/mod.rs @@ -296,6 +296,10 @@ pub mod text; #[doc(inline)] pub use text::{Text, text}; +pub mod selectable_text; +#[doc(inline)] +pub use selectable_text::{SelectableText, selectable_text}; + pub mod text_context_menu; #[doc(inline)] pub use text_context_menu::HasSelectableText; diff --git a/src/widget/selectable_text.rs b/src/widget/selectable_text.rs new file mode 100644 index 00000000..824077c7 --- /dev/null +++ b/src/widget/selectable_text.rs @@ -0,0 +1,505 @@ +use crate::Renderer; +use crate::widget::menu::MenuBarState; + +use iced_core::event::Event; +use iced_core::text::LineHeight; +use iced_core::widget::Widget; +use iced_core::widget::text::{Alignment, Catalog, Ellipsize, Shaping, Style, Wrapping}; +use iced_core::widget::tree::{self, Tree}; +use iced_core::{ + Clipboard, Color, Layout, Length, Pixels, Rectangle, Shell, Size, Vector, alignment, mouse, + overlay, renderer, +}; +use std::borrow::Cow; + +type InnerText<'a> = iced_core::widget::text::Text<'a, crate::Theme, Renderer>; + +/// A selectable text widget with right-click context menu support. +/// +/// Wraps the plain [`Text`](crate::widget::Text) widget to add text selection +/// and a context menu (Copy / Select All), both **on by default**. Created +/// with [`selectable_text`]; the plain [`text`](crate::widget::text) +/// constructor and the typography presets return the unwrapped +/// [`Text`](crate::widget::Text), so non-selectable text keeps iced's looser +/// trait bounds. +/// +/// Use [`.selectable(false)`](Self::selectable) for plain, non-interactive +/// text, or [`.context_menu(false)`](Self::context_menu) to keep selection +/// without the menu. +pub struct SelectableText<'a> { + inner: InnerText<'a>, + selectable: bool, + context_menu: bool, +} + +impl<'a> SelectableText<'a> { + /// Creates a new [`SelectableText`] widget with the given content. + /// + /// Selection and the context menu are enabled by default. + pub fn new(content: impl iced_core::text::IntoFragment<'a>) -> Self { + Self { + inner: InnerText::new(content), + selectable: true, + context_menu: true, + } + } + + /// Controls whether the text is selectable. On by default. + /// + /// Pass `false` to render plain, non-interactive text (this also + /// suppresses the context menu, which has nothing to act on). + pub fn selectable(mut self, enabled: bool) -> Self { + self.selectable = enabled; + self + } + + /// Controls whether the right-click context menu is shown. On by default. + /// + /// Pass `false` to keep text selection but disable the context menu. + pub fn context_menu(mut self, enabled: bool) -> Self { + self.context_menu = enabled; + self + } + + /// Whether the wrapper should install its context-menu machinery: only + /// when the menu is enabled *and* the text is selectable (a menu over + /// non-selectable text would have nothing to act on). + fn shows_context_menu(&self) -> bool { + self.selectable && self.context_menu + } + + fn uses_popup_context_menu(&self) -> bool { + #[cfg(all(feature = "wayland", feature = "winit"))] + if matches!( + crate::app::cosmic::WINDOWING_SYSTEM.get(), + Some(crate::app::cosmic::WindowingSystem::Wayland) + ) { + return true; + } + false + } + + pub fn size(mut self, size: impl Into) -> Self { + self.inner = self.inner.size(size); + self + } + + pub fn line_height(mut self, line_height: impl Into) -> Self { + self.inner = self.inner.line_height(line_height); + self + } + + pub fn font(mut self, font: impl Into<::Font>) -> Self { + self.inner = self.inner.font(font); + self + } + + pub fn font_maybe( + mut self, + font: Option::Font>>, + ) -> Self { + self.inner = self.inner.font_maybe(font); + self + } + + pub fn width(mut self, width: impl Into) -> Self { + self.inner = self.inner.width(width); + self + } + + pub fn height(mut self, height: impl Into) -> Self { + self.inner = self.inner.height(height); + self + } + + pub fn center(mut self) -> Self { + self.inner = self.inner.center(); + self + } + + pub fn align_x(mut self, alignment: impl Into) -> Self { + self.inner = self.inner.align_x(alignment); + self + } + + pub fn align_y(mut self, alignment: impl Into) -> Self { + self.inner = self.inner.align_y(alignment); + self + } + + pub fn shaping(mut self, shaping: Shaping) -> Self { + self.inner = self.inner.shaping(shaping); + self + } + + pub fn wrapping(mut self, wrapping: Wrapping) -> Self { + self.inner = self.inner.wrapping(wrapping); + self + } + + pub fn ellipsize(mut self, ellipsize: Ellipsize) -> Self { + self.inner = self.inner.ellipsize(ellipsize); + self + } + + #[must_use] + pub fn style(mut self, style: impl Fn(&crate::Theme) -> Style + 'a) -> Self + where + ::Class<'a>: + From>, + { + self.inner = self.inner.style(style); + self + } + + pub fn color(mut self, color: impl Into) -> Self + where + ::Class<'a>: + From>, + { + self.inner = self.inner.color(color); + self + } + + pub fn color_maybe(mut self, color: Option>) -> Self + where + ::Class<'a>: + From>, + { + self.inner = self.inner.color_maybe(color); + self + } + + #[must_use] + pub fn class(mut self, class: impl Into<::Class<'a>>) -> Self { + self.inner = self.inner.class(class); + self + } +} + +impl<'a> From> for SelectableText<'a> { + fn from(inner: InnerText<'a>) -> Self { + Self { + inner, + selectable: true, + context_menu: true, + } + } +} + +impl<'a> From<&'a str> for SelectableText<'a> { + fn from(content: &'a str) -> Self { + Self::new(content) + } +} + +struct TextWrapperState { + menu_bar_state: MenuBarState, + pending_action: crate::widget::text_context_menu::PendingAction, +} + +fn w<'x, Message>(inner: &'x InnerText<'_>) -> &'x dyn Widget { + inner +} + +fn w_mut<'x, Message>( + inner: &'x mut InnerText<'_>, +) -> &'x mut dyn Widget { + inner +} + +impl<'a, Message: Clone + 'static> Widget for SelectableText<'a> { + fn tag(&self) -> tree::Tag { + if self.shows_context_menu() { + tree::Tag::of::() + } else { + w::(&self.inner).tag() + } + } + + fn state(&self) -> tree::State { + if self.shows_context_menu() { + tree::State::new(TextWrapperState { + menu_bar_state: MenuBarState::default(), + pending_action: crate::widget::text_context_menu::pending_action(), + }) + } else { + w::(&self.inner).state() + } + } + + fn children(&self) -> Vec { + if self.shows_context_menu() { + vec![Tree::new(w::(&self.inner))] + } else { + Vec::new() + } + } + + fn diff(&mut self, tree: &mut Tree) { + if self.shows_context_menu() { + if let Some(child) = tree.children.first_mut() { + child.diff(&mut self.inner as &mut dyn Widget); + } + } + } + + fn size(&self) -> Size { + w::(&self.inner).size() + } + + fn layout( + &mut self, + tree: &mut Tree, + renderer: &Renderer, + limits: &iced_core::layout::Limits, + ) -> iced_core::layout::Node { + let inner_tree = if self.shows_context_menu() { + &mut tree.children[0] + } else { + tree + }; + w_mut::(&mut self.inner).layout(inner_tree, renderer, limits) + } + + fn draw( + &self, + tree: &Tree, + renderer: &mut Renderer, + theme: &crate::Theme, + defaults: &renderer::Style, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + ) { + let inner_tree = if self.shows_context_menu() { + &tree.children[0] + } else { + tree + }; + w::(&self.inner).draw( + inner_tree, renderer, theme, defaults, layout, cursor, viewport, + ); + + // draw a border when the text is focused + use iced_core::renderer::Renderer as RendererExt; + use iced_core::widget::text::State as TextState; + + let state = inner_tree + .state + .downcast_ref::::Paragraph>>(); + if state.is_keyboard_focused() { + let cosmic = theme.cosmic(); + let accent: Color = cosmic.accent.base.into(); + renderer.fill_quad( + renderer::Quad { + bounds: layout.bounds(), + border: iced_core::Border { + color: accent, + width: 1.0, + radius: cosmic.corner_radii.radius_s.into(), + }, + ..renderer::Quad::default() + }, + Color::TRANSPARENT, + ); + } + } + + 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.shows_context_menu() { + w_mut::(&mut self.inner).update( + &mut tree.children[0], + event, + layout, + cursor, + renderer, + clipboard, + shell, + viewport, + ); + + #[cfg(feature = "wayland")] + if self.uses_popup_context_menu() { + use iced_core::widget::text::HasSelectableText; + + if self + .inner + .context_menu_position(&tree.children[0]) + .is_some() + { + let selected_text = self.inner.selected_text(&tree.children[0]); + let has_selection = selected_text.is_some(); + let click_position = + self.inner.context_menu_position(&tree.children[0]).unwrap(); + let wrapper_state = tree.state.downcast_ref::(); + let menu_bar_state = wrapper_state.menu_bar_state.clone(); + let pending_action = wrapper_state.pending_action.clone(); + + crate::widget::text_context_menu::create_text_context_popup( + click_position, + selected_text, + false, + has_selection, + &menu_bar_state, + &pending_action, + renderer, + viewport, + cursor, + ); + + self.inner + .set_context_menu_position(&mut tree.children[0], None); + } + + { + let wrapper_state = tree.state.downcast_ref::(); + let pending_action = wrapper_state.pending_action.clone(); + if let Some(action) = + crate::widget::text_context_menu::take_pending_action(&pending_action) + { + match action { + crate::widget::text_context_menu::TextCtxAction::Copy => {} + crate::widget::text_context_menu::TextCtxAction::SelectAll => { + self.inner.select_all(&mut tree.children[0]); + } + _ => {} + } + } + } + + let wrapper_state = tree.state.downcast_ref::(); + let menu_bar_state = wrapper_state.menu_bar_state.clone(); + crate::widget::text_context_menu::dismiss_popup_on_event(&menu_bar_state, event); + } + } else { + w_mut::(&mut self.inner).update( + tree, event, layout, cursor, renderer, clipboard, shell, viewport, + ); + } + } + + fn mouse_interaction( + &self, + tree: &Tree, + layout: Layout<'_>, + cursor: mouse::Cursor, + viewport: &Rectangle, + renderer: &Renderer, + ) -> mouse::Interaction { + let inner_tree = if self.shows_context_menu() { + &tree.children[0] + } else { + tree + }; + w::(&self.inner).mouse_interaction(inner_tree, layout, cursor, viewport, renderer) + } + + fn overlay<'b>( + &'b mut self, + tree: &'b mut Tree, + _layout: Layout<'b>, + _renderer: &Renderer, + _viewport: &Rectangle, + translation: Vector, + ) -> Option> { + if self.shows_context_menu() && !self.uses_popup_context_menu() { + let menu_bar_state = tree + .state + .downcast_ref::() + .menu_bar_state + .clone(); + let inner_tree = &mut tree.children[0]; + crate::widget::text_context_menu::context_menu_overlay( + &self.inner, + inner_tree, + None, + translation, + menu_bar_state, + ) + } else { + None + } + } + + fn operate( + &mut self, + tree: &mut Tree, + layout: Layout<'_>, + renderer: &Renderer, + operation: &mut dyn iced_core::widget::Operation, + ) { + let inner_tree = if self.shows_context_menu() { + &mut tree.children[0] + } else { + tree + }; + w_mut::(&mut self.inner).operate(inner_tree, layout, renderer, operation); + } + + #[cfg(feature = "a11y")] + fn a11y_nodes( + &self, + layout: Layout<'_>, + state: &Tree, + cursor: mouse::Cursor, + ) -> iced_accessibility::A11yTree { + let inner_state = if self.shows_context_menu() { + &state.children[0] + } else { + state + }; + w::(&self.inner).a11y_nodes(layout, inner_state, cursor) + } + + fn id(&self) -> Option { + w::(&self.inner).id() + } + + fn set_id(&mut self, id: iced_core::widget::Id) { + w_mut::(&mut self.inner).set_id(id); + } +} + +impl<'a, Message: Clone + 'static> From> for crate::Element<'a, Message> { + fn from(text: SelectableText<'a>) -> Self { + let SelectableText { + inner, + selectable, + context_menu, + } = text; + // Apply selection to the inner widget here, at the last moment before + // it becomes part of the tree. iced's `Text::selectable()` is one-way + // (it can only set the flag), so deferring until conversion lets + // `selectable(false)` work without ever having set it. + let inner = if selectable { + inner.selectable() + } else { + inner + }; + Self::new(SelectableText { + inner, + selectable, + context_menu, + }) + } +} + +/// Creates a [`SelectableText`] widget: text the user can select, with a +/// right-click context menu (Copy / Select All). +/// +/// Use [`.context_menu(false)`](SelectableText::context_menu) to keep +/// selection but suppress the menu. +pub fn selectable_text<'a>(content: impl Into> + 'a) -> SelectableText<'a> { + SelectableText::new(content.into()).font(crate::font::default()) +} diff --git a/src/widget/settings/item.rs b/src/widget/settings/item.rs index af4b17ab..4eb5bb17 100644 --- a/src/widget/settings/item.rs +++ b/src/widget/settings/item.rs @@ -14,12 +14,12 @@ use taffy::AlignContent; /// A settings item aligned in a row #[must_use] #[allow(clippy::module_name_repetitions)] -pub fn item<'a, Message: Clone + 'static>( +pub fn item<'a, Message: 'static>( title: impl Into> + 'a, widget: impl Into> + 'a, ) -> Row<'a, Message, Theme> { #[inline(never)] - fn inner<'a, Message: Clone + 'static>( + fn inner<'a, Message: 'static>( title: Cow<'a, str>, widget: Element<'a, Message>, ) -> Row<'a, Message, Theme> { @@ -45,12 +45,12 @@ pub fn item_row(children: Vec>) -> Row /// A settings item aligned in a flex row #[allow(clippy::module_name_repetitions)] -pub fn flex_item<'a, Message: Clone + 'static>( +pub fn flex_item<'a, Message: 'static>( title: impl Into> + 'a, widget: impl Into> + 'a, ) -> FlexRow<'a, Message> { #[inline(never)] - fn inner<'a, Message: Clone + 'static>( + fn inner<'a, Message: 'static>( title: Cow<'a, str>, widget: Element<'a, Message>, ) -> FlexRow<'a, Message> { diff --git a/src/widget/text.rs b/src/widget/text.rs index 5c490d4d..37e85b80 100644 --- a/src/widget/text.rs +++ b/src/widget/text.rs @@ -1,460 +1,13 @@ use crate::Renderer; -use crate::widget::menu::MenuBarState; - -use iced_core::event::Event; +pub use iced::widget::Text; use iced_core::text::LineHeight; -use iced_core::widget::Widget; -use iced_core::widget::text::{Alignment, Catalog, Ellipsize, Shaping, Style, Wrapping}; -use iced_core::widget::tree::{self, Tree}; -use iced_core::{ - Clipboard, Color, Layout, Length, Pixels, Rectangle, Shell, Size, Vector, alignment, mouse, - overlay, renderer, -}; use std::borrow::Cow; -type InnerText<'a> = iced_core::widget::text::Text<'a, crate::Theme, Renderer>; - -/// A text widget with optional selection and context menu support. +/// Creates a new [`Text`] widget with the provided content. /// -/// By default, text is non-selectable. Call [`.selectable()`](Text::selectable) -/// to enable text selection and a right-click context menu with Copy and -/// Select All. -pub struct Text<'a> { - inner: InnerText<'a>, - context_menu: bool, -} - -impl<'a> Text<'a> { - /// Creates a new [`Text`] widget with the given content. - pub fn new(content: impl iced_core::text::IntoFragment<'a>) -> Self { - Self { - inner: InnerText::new(content), - context_menu: false, - } - } - - /// Enables text selection and a right-click context menu. - pub fn selectable(mut self) -> Self { - self.inner = self.inner.selectable(); - self.context_menu = true; - self - } - - /// Controls whether the right-click context menu is shown. - /// - /// Only meaningful after calling [`.selectable()`](Self::selectable). - /// Pass `false` to keep text selection but disable the context menu. - pub fn context_menu(mut self, enabled: bool) -> Self { - self.context_menu = enabled; - self - } - - fn uses_popup_context_menu(&self) -> bool { - #[cfg(all(feature = "wayland", feature = "winit"))] - if matches!( - crate::app::cosmic::WINDOWING_SYSTEM.get(), - Some(crate::app::cosmic::WindowingSystem::Wayland) - ) { - return true; - } - false - } - - pub fn size(mut self, size: impl Into) -> Self { - self.inner = self.inner.size(size); - self - } - - pub fn line_height(mut self, line_height: impl Into) -> Self { - self.inner = self.inner.line_height(line_height); - self - } - - pub fn font(mut self, font: impl Into<::Font>) -> Self { - self.inner = self.inner.font(font); - self - } - - pub fn font_maybe( - mut self, - font: Option::Font>>, - ) -> Self { - self.inner = self.inner.font_maybe(font); - self - } - - pub fn width(mut self, width: impl Into) -> Self { - self.inner = self.inner.width(width); - self - } - - pub fn height(mut self, height: impl Into) -> Self { - self.inner = self.inner.height(height); - self - } - - pub fn center(mut self) -> Self { - self.inner = self.inner.center(); - self - } - - pub fn align_x(mut self, alignment: impl Into) -> Self { - self.inner = self.inner.align_x(alignment); - self - } - - pub fn align_y(mut self, alignment: impl Into) -> Self { - self.inner = self.inner.align_y(alignment); - self - } - - pub fn shaping(mut self, shaping: Shaping) -> Self { - self.inner = self.inner.shaping(shaping); - self - } - - pub fn wrapping(mut self, wrapping: Wrapping) -> Self { - self.inner = self.inner.wrapping(wrapping); - self - } - - pub fn ellipsize(mut self, ellipsize: Ellipsize) -> Self { - self.inner = self.inner.ellipsize(ellipsize); - self - } - - #[must_use] - pub fn style(mut self, style: impl Fn(&crate::Theme) -> Style + 'a) -> Self - where - ::Class<'a>: - From>, - { - self.inner = self.inner.style(style); - self - } - - pub fn color(mut self, color: impl Into) -> Self - where - ::Class<'a>: - From>, - { - self.inner = self.inner.color(color); - self - } - - pub fn color_maybe(mut self, color: Option>) -> Self - where - ::Class<'a>: - From>, - { - self.inner = self.inner.color_maybe(color); - self - } - - #[must_use] - pub fn class(mut self, class: impl Into<::Class<'a>>) -> Self { - self.inner = self.inner.class(class); - self - } -} - -impl<'a> From> for Text<'a> { - fn from(inner: InnerText<'a>) -> Self { - Self { - inner, - context_menu: false, - } - } -} - -impl<'a> From<&'a str> for Text<'a> { - fn from(content: &'a str) -> Self { - Self::new(content) - } -} - -struct TextWrapperState { - menu_bar_state: MenuBarState, - pending_action: crate::widget::text_context_menu::PendingAction, -} - -fn w<'x, Message>(inner: &'x InnerText<'_>) -> &'x dyn Widget { - inner -} - -fn w_mut<'x, Message>( - inner: &'x mut InnerText<'_>, -) -> &'x mut dyn Widget { - inner -} - -impl<'a, Message: Clone + 'static> Widget for Text<'a> { - fn tag(&self) -> tree::Tag { - if self.context_menu { - tree::Tag::of::() - } else { - w::(&self.inner).tag() - } - } - - fn state(&self) -> tree::State { - if self.context_menu { - tree::State::new(TextWrapperState { - menu_bar_state: MenuBarState::default(), - pending_action: crate::widget::text_context_menu::pending_action(), - }) - } else { - w::(&self.inner).state() - } - } - - fn children(&self) -> Vec { - if self.context_menu { - vec![Tree::new(w::(&self.inner))] - } else { - Vec::new() - } - } - - fn diff(&mut self, tree: &mut Tree) { - if self.context_menu { - if let Some(child) = tree.children.first_mut() { - child.diff(&mut self.inner as &mut dyn Widget); - } - } - } - - fn size(&self) -> Size { - w::(&self.inner).size() - } - - fn layout( - &mut self, - tree: &mut Tree, - renderer: &Renderer, - limits: &iced_core::layout::Limits, - ) -> iced_core::layout::Node { - let inner_tree = if self.context_menu { - &mut tree.children[0] - } else { - tree - }; - w_mut::(&mut self.inner).layout(inner_tree, renderer, limits) - } - - fn draw( - &self, - tree: &Tree, - renderer: &mut Renderer, - theme: &crate::Theme, - defaults: &renderer::Style, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - ) { - let inner_tree = if self.context_menu { - &tree.children[0] - } else { - tree - }; - w::(&self.inner).draw( - inner_tree, renderer, theme, defaults, layout, cursor, viewport, - ); - - // draw a border when the text is focused - use iced_core::renderer::Renderer as RendererExt; - use iced_core::widget::text::State as TextState; - - let state = inner_tree - .state - .downcast_ref::::Paragraph>>(); - if state.is_keyboard_focused() { - let cosmic = theme.cosmic(); - let accent: Color = cosmic.accent.base.into(); - renderer.fill_quad( - renderer::Quad { - bounds: layout.bounds(), - border: iced_core::Border { - color: accent, - width: 1.0, - radius: cosmic.corner_radii.radius_s.into(), - }, - ..renderer::Quad::default() - }, - Color::TRANSPARENT, - ); - } - } - - 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.context_menu { - w_mut::(&mut self.inner).update( - &mut tree.children[0], - event, - layout, - cursor, - renderer, - clipboard, - shell, - viewport, - ); - - #[cfg(feature = "wayland")] - if self.uses_popup_context_menu() { - use iced_core::widget::text::HasSelectableText; - - if self - .inner - .context_menu_position(&tree.children[0]) - .is_some() - { - let selected_text = self.inner.selected_text(&tree.children[0]); - let has_selection = selected_text.is_some(); - let click_position = - self.inner.context_menu_position(&tree.children[0]).unwrap(); - let wrapper_state = tree.state.downcast_ref::(); - let menu_bar_state = wrapper_state.menu_bar_state.clone(); - let pending_action = wrapper_state.pending_action.clone(); - - crate::widget::text_context_menu::create_text_context_popup( - click_position, - selected_text, - false, - has_selection, - &menu_bar_state, - &pending_action, - renderer, - viewport, - cursor, - ); - - self.inner - .set_context_menu_position(&mut tree.children[0], None); - } - - { - let wrapper_state = tree.state.downcast_ref::(); - let pending_action = wrapper_state.pending_action.clone(); - if let Some(action) = - crate::widget::text_context_menu::take_pending_action(&pending_action) - { - match action { - crate::widget::text_context_menu::TextCtxAction::Copy => {} - crate::widget::text_context_menu::TextCtxAction::SelectAll => { - self.inner.select_all(&mut tree.children[0]); - } - _ => {} - } - } - } - - let wrapper_state = tree.state.downcast_ref::(); - let menu_bar_state = wrapper_state.menu_bar_state.clone(); - crate::widget::text_context_menu::dismiss_popup_on_event(&menu_bar_state, event); - } - } else { - w_mut::(&mut self.inner).update( - tree, event, layout, cursor, renderer, clipboard, shell, viewport, - ); - } - } - - fn mouse_interaction( - &self, - tree: &Tree, - layout: Layout<'_>, - cursor: mouse::Cursor, - viewport: &Rectangle, - renderer: &Renderer, - ) -> mouse::Interaction { - let inner_tree = if self.context_menu { - &tree.children[0] - } else { - tree - }; - w::(&self.inner).mouse_interaction(inner_tree, layout, cursor, viewport, renderer) - } - - fn overlay<'b>( - &'b mut self, - tree: &'b mut Tree, - _layout: Layout<'b>, - _renderer: &Renderer, - _viewport: &Rectangle, - translation: Vector, - ) -> Option> { - if self.context_menu && !self.uses_popup_context_menu() { - let menu_bar_state = tree - .state - .downcast_ref::() - .menu_bar_state - .clone(); - let inner_tree = &mut tree.children[0]; - crate::widget::text_context_menu::context_menu_overlay( - &self.inner, - inner_tree, - None, - translation, - menu_bar_state, - ) - } else { - None - } - } - - fn operate( - &mut self, - tree: &mut Tree, - layout: Layout<'_>, - renderer: &Renderer, - operation: &mut dyn iced_core::widget::Operation, - ) { - let inner_tree = if self.context_menu { - &mut tree.children[0] - } else { - tree - }; - w_mut::(&mut self.inner).operate(inner_tree, layout, renderer, operation); - } - - #[cfg(feature = "a11y")] - fn a11y_nodes( - &self, - layout: Layout<'_>, - state: &Tree, - cursor: mouse::Cursor, - ) -> iced_accessibility::A11yTree { - let inner_state = if self.context_menu { - &state.children[0] - } else { - state - }; - w::(&self.inner).a11y_nodes(layout, inner_state, cursor) - } - - fn id(&self) -> Option { - w::(&self.inner).id() - } - - fn set_id(&mut self, id: iced_core::widget::Id) { - w_mut::(&mut self.inner).set_id(id); - } -} - -impl<'a, Message: Clone + 'static> From> for crate::Element<'a, Message> { - fn from(text: Text<'a>) -> Self { - Self::new(text) - } +/// [`Text`]: widget::Text +pub fn text<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { + Text::new(text.into()).font(crate::font::default()) } /// Available presets for text typography @@ -471,115 +24,119 @@ pub enum Typography { Title4, } -/// Creates a new [`Text`] widget with the provided content. -pub fn text<'a>(content: impl Into> + 'a) -> Text<'a> { - Text::new(content.into()).font(crate::font::default()) -} - /// [`Text`] widget with the Title 1 typography preset. -pub fn title1<'a>(content: impl Into> + 'a) -> Text<'a> { +pub fn title1<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { #[inline(never)] - fn inner(text: Cow) -> Text { + fn inner(text: Cow) -> Text { Text::new(text) .size(35.0) .line_height(LineHeight::Absolute(52.0.into())) .font(crate::font::semibold()) } - inner(content.into()) + + inner(text.into()) } /// [`Text`] widget with the Title 2 typography preset. -pub fn title2<'a>(content: impl Into> + 'a) -> Text<'a> { +pub fn title2<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { #[inline(never)] - fn inner(text: Cow) -> Text { + fn inner(text: Cow) -> Text { Text::new(text) .size(29.0) .line_height(LineHeight::Absolute(43.0.into())) .font(crate::font::semibold()) } - inner(content.into()) + + inner(text.into()) } /// [`Text`] widget with the Title 3 typography preset. -pub fn title3<'a>(content: impl Into> + 'a) -> Text<'a> { +pub fn title3<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { #[inline(never)] - fn inner(text: Cow) -> Text { + fn inner(text: Cow) -> Text { Text::new(text) .size(24.0) .line_height(LineHeight::Absolute(36.0.into())) .font(crate::font::bold()) } - inner(content.into()) + + inner(text.into()) } /// [`Text`] widget with the Title 4 typography preset. -pub fn title4<'a>(content: impl Into> + 'a) -> Text<'a> { +pub fn title4<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { #[inline(never)] - fn inner(text: Cow) -> Text { + fn inner(text: Cow) -> Text { Text::new(text) .size(20.0) .line_height(LineHeight::Absolute(30.0.into())) .font(crate::font::bold()) } - inner(content.into()) + + inner(text.into()) } /// [`Text`] widget with the Heading typography preset. -pub fn heading<'a>(content: impl Into> + 'a) -> Text<'a> { +pub fn heading<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { #[inline(never)] - fn inner(text: Cow) -> Text { + fn inner(text: Cow) -> Text { Text::new(text) .size(14.0) .line_height(LineHeight::Absolute(iced::Pixels(21.0))) .font(crate::font::bold()) } - inner(content.into()) + + inner(text.into()) } /// [`Text`] widget with the Caption Heading typography preset. -pub fn caption_heading<'a>(content: impl Into> + 'a) -> Text<'a> { +pub fn caption_heading<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { #[inline(never)] - fn inner(text: Cow) -> Text { + fn inner(text: Cow) -> Text { Text::new(text) .size(12.0) .line_height(LineHeight::Absolute(iced::Pixels(17.0))) .font(crate::font::semibold()) } - inner(content.into()) + + inner(text.into()) } /// [`Text`] widget with the Body typography preset. -pub fn body<'a>(content: impl Into> + 'a) -> Text<'a> { +pub fn body<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { #[inline(never)] - fn inner(text: Cow) -> Text { + fn inner(text: Cow) -> Text { Text::new(text) .size(14.0) .line_height(LineHeight::Absolute(21.0.into())) .font(crate::font::default()) } - inner(content.into()) + + inner(text.into()) } /// [`Text`] widget with the Caption typography preset. -pub fn caption<'a>(content: impl Into> + 'a) -> Text<'a> { +pub fn caption<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { #[inline(never)] - fn inner(text: Cow) -> Text { + fn inner(text: Cow) -> Text { Text::new(text) .size(12.0) .line_height(LineHeight::Absolute(17.0.into())) .font(crate::font::default()) } - inner(content.into()) + + inner(text.into()) } /// [`Text`] widget with the Monotext typography preset. -pub fn monotext<'a>(content: impl Into> + 'a) -> Text<'a> { +pub fn monotext<'a>(text: impl Into> + 'a) -> Text<'a, crate::Theme, Renderer> { #[inline(never)] - fn inner(text: Cow) -> Text { + fn inner(text: Cow) -> Text { Text::new(text) .size(14.0) .line_height(LineHeight::Absolute(20.0.into())) .font(crate::font::mono()) } - inner(content.into()) + + inner(text.into()) } From 654ae0bf5d7165be7919d8cfd502184bdf0af0a6 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 29 Jun 2026 18:12:16 -0600 Subject: [PATCH 181/201] chore: add typography presets for selectable_text --- src/widget/selectable_text.rs | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/src/widget/selectable_text.rs b/src/widget/selectable_text.rs index 824077c7..5bd85ed4 100644 --- a/src/widget/selectable_text.rs +++ b/src/widget/selectable_text.rs @@ -503,3 +503,23 @@ impl<'a, Message: Clone + 'static> From> for crate::Element<' pub fn selectable_text<'a>(content: impl Into> + 'a) -> SelectableText<'a> { SelectableText::new(content.into()).font(crate::font::default()) } + +// Typography presets, mirroring [`crate::widget::text`] +macro_rules! selectable_preset { + ($(#[$meta:meta])* $name:ident) => { + $(#[$meta])* + pub fn $name<'a>(content: impl Into> + 'a) -> SelectableText<'a> { + SelectableText::from(crate::widget::text::$name(content)) + } + }; +} + +selectable_preset!(title1); +selectable_preset!(title2); +selectable_preset!(title3); +selectable_preset!(title4); +selectable_preset!(heading); +selectable_preset!(caption_heading); +selectable_preset!(body); +selectable_preset!(caption); +selectable_preset!(monotext); From 03a66751ea5356909ee1fad4ded5fd368eb98be4 Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 20 Jul 2026 11:25:44 -0400 Subject: [PATCH 182/201] chore: feature gates --- src/app/cosmic.rs | 8 ++++---- src/widget/selectable_text.rs | 4 ++-- src/widget/text_context_menu.rs | 32 ++++++++++++++++---------------- src/widget/text_editor.rs | 4 ++-- src/widget/text_input/input.rs | 6 +++--- 5 files changed, 27 insertions(+), 27 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index 5a83f66e..b559ab4a 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -535,14 +535,14 @@ where // popup pipeline. Drained before the creation queue so a // destroy-then-recreate (a second right-click reusing the same id) // keeps its order. - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(all(wayland_platform, target_os = "linux"))] for id in crate::widget::text_context_menu::take_popup_destroys() { task = task.chain(iced_winit::commands::popup::destroy_popup(id)); } // Drain any text context-menu popup requests queued by widgets during // `app.update()` and create them through the normal popup pipeline. - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(all(wayland_platform, target_os = "linux"))] for req in crate::widget::text_context_menu::take_popup_requests() { let (settings, view) = crate::widget::text_context_menu::into_popup_view::(req); @@ -723,7 +723,7 @@ where // Drives the text context-menu popup queues: a right-click queues a // popup but publishes no message, so this re-emits `Action::None` to // make `update()` run and drain the queue. - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(all(wayland_platform, target_os = "linux"))] subscriptions.push(crate::widget::text_context_menu::wake_subscription::< T::Message, >()); @@ -743,7 +743,7 @@ where #[cfg(feature = "multi-window")] pub fn view(&self, id: window::Id) -> Element<'_, crate::Action> { - #[cfg(all(feature = "wayland", target_os = "linux"))] + #[cfg(all(wayland_platform, target_os = "linux"))] if let Some((_, _, _, Some(v))) = self.surface_views.get(&id) { return v(&self.app); } diff --git a/src/widget/selectable_text.rs b/src/widget/selectable_text.rs index 5bd85ed4..4b47cfd4 100644 --- a/src/widget/selectable_text.rs +++ b/src/widget/selectable_text.rs @@ -69,7 +69,7 @@ impl<'a> SelectableText<'a> { } fn uses_popup_context_menu(&self) -> bool { - #[cfg(all(feature = "wayland", feature = "winit"))] + #[cfg(all(wayland_platform, feature = "winit"))] if matches!( crate::app::cosmic::WINDOWING_SYSTEM.get(), Some(crate::app::cosmic::WindowingSystem::Wayland) @@ -329,7 +329,7 @@ impl<'a, Message: Clone + 'static> Widget for S viewport, ); - #[cfg(feature = "wayland")] + #[cfg(wayland_platform)] if self.uses_popup_context_menu() { use iced_core::widget::text::HasSelectableText; diff --git a/src/widget/text_context_menu.rs b/src/widget/text_context_menu.rs index 7912e37c..6fdea222 100644 --- a/src/widget/text_context_menu.rs +++ b/src/widget/text_context_menu.rs @@ -51,13 +51,13 @@ thread_local! { static CURRENT_WINDOW_ID: Cell = const { Cell::new(iced_core::window::Id::NONE) }; } -#[cfg(feature = "wayland")] +#[cfg(wayland_platform)] use iced_runtime::platform_specific::wayland::popup::SctkPopupSettings; /// A request to create a text context-menu popup surface, queued by a widget /// during `update()` and drained by `Cosmic::update()` so the popup goes /// through the normal `get_popup()` Task + `surface_views` pipeline. -#[cfg(feature = "wayland")] +#[cfg(wayland_platform)] pub(crate) struct PopupRequest { settings: SctkPopupSettings, menu: Menu<'static, TextCtxAction>, @@ -65,7 +65,7 @@ pub(crate) struct PopupRequest { pending_action: PendingAction, } -#[cfg(feature = "wayland")] +#[cfg(wayland_platform)] thread_local! { static PENDING_POPUP_REQUESTS: std::cell::RefCell> = const { std::cell::RefCell::new(Vec::new()) }; @@ -80,7 +80,7 @@ pub(crate) fn current_window_id() -> iced_core::window::Id { } /// Drains all popup requests queued by widgets this frame. -#[cfg(feature = "wayland")] +#[cfg(wayland_platform)] pub(crate) fn take_popup_requests() -> Vec { PENDING_POPUP_REQUESTS.with(|q| std::mem::take(&mut *q.borrow_mut())) } @@ -88,7 +88,7 @@ pub(crate) fn take_popup_requests() -> Vec { /// Consumes a [`PopupRequest`], returning the popup settings plus a view /// builder. The builder rebuilds the menu element each frame from the /// captured content, independent of app state. -#[cfg(feature = "wayland")] +#[cfg(wayland_platform)] #[allow(clippy::type_complexity)] pub(crate) fn into_popup_view( req: PopupRequest, @@ -117,7 +117,7 @@ pub(crate) fn into_popup_view( (settings, view) } -#[cfg(feature = "wayland")] +#[cfg(wayland_platform)] thread_local! { static PENDING_POPUP_DESTROYS: std::cell::RefCell> = const { std::cell::RefCell::new(Vec::new()) }; @@ -129,22 +129,22 @@ thread_local! { /// inside `update()` can't reach `Cosmic` to issue a Task, so they push the /// id here and `Cosmic::update()` drains it into a `destroy_popup` Task that /// flows through the normal surface pipeline. -#[cfg(feature = "wayland")] +#[cfg(wayland_platform)] fn queue_destroy_popup(id: iced_core::window::Id) { PENDING_POPUP_DESTROYS.with(|q| q.borrow_mut().push(id)); wake_runtime(); } -#[cfg(feature = "wayland")] +#[cfg(wayland_platform)] static WAKE_TX: std::sync::OnceLock> = std::sync::OnceLock::new(); /// Stable identity for [`wake_subscription`]. -#[cfg(feature = "wayland")] +#[cfg(wayland_platform)] struct PopupWake; /// Nudges the runtime so `Cosmic::update()` runs and drains the popup queues. -#[cfg(feature = "wayland")] +#[cfg(wayland_platform)] fn wake_runtime() { if let Some(tx) = WAKE_TX.get() { let _ = tx.clone().try_send(()); @@ -155,7 +155,7 @@ fn wake_runtime() { /// wake channel and re-emits each ping as [`crate::Action::None`]. Add it to /// the app's subscriptions (done by `Cosmic::subscription`) so popup creation /// and teardown queued from widget `update()` get drained promptly. -#[cfg(feature = "wayland")] +#[cfg(wayland_platform)] pub(crate) fn wake_subscription() -> iced_futures::Subscription> { use iced_futures::futures::{SinkExt, StreamExt}; @@ -466,7 +466,7 @@ where /// Pushes a [`PopupRequest`] onto the request queue; `Cosmic::update()` /// drains it and creates the popup through `get_popup()`, so it flows /// through the normal Task + `surface_views` pipeline. -#[cfg(feature = "wayland")] +#[cfg(wayland_platform)] pub(crate) fn create_text_context_popup( click_position: Point, selected_text: Option, @@ -602,7 +602,7 @@ pub(crate) fn create_text_context_popup( /// Dismisses this widget's open context-menu popup on an outside click, /// touch, or Escape. -#[cfg(feature = "wayland")] +#[cfg(wayland_platform)] pub(crate) fn dismiss_popup_on_event(menu_bar_state: &MenuBarState, event: &event::Event) { let is_dismiss = matches!( event, @@ -710,7 +710,7 @@ impl iced_core::widget::Widget, viewport: &Rectangle, ) { - #[cfg(feature = "wayland")] + #[cfg(wayland_platform)] { use iced_core::event::wayland::PopupEvent; let popup_event = match event { @@ -736,7 +736,7 @@ impl iced_core::widget::Widget iced_core::widget::Widget TextEditor<'a, Message> { } fn uses_popup_context_menu(&self) -> bool { - #[cfg(all(feature = "wayland", feature = "winit"))] + #[cfg(all(wayland_platform, feature = "winit"))] if matches!( crate::app::cosmic::WINDOWING_SYSTEM.get(), Some(crate::app::cosmic::WindowingSystem::Wayland) @@ -275,7 +275,7 @@ impl<'a, Message: Clone + 'static> Widget bool { - #[cfg(all(feature = "wayland", feature = "winit"))] + #[cfg(all(wayland_platform, feature = "winit"))] if matches!( crate::app::cosmic::WINDOWING_SYSTEM.get(), Some(crate::app::cosmic::WindowingSystem::Wayland) @@ -919,7 +919,7 @@ where shell: &mut Shell<'_, Message>, viewport: &Rectangle, ) { - #[cfg(all(feature = "wayland", feature = "winit"))] + #[cfg(all(wayland_platform, feature = "winit"))] if self.uses_popup_context_menu() { let menu_bar_state = tree.state.downcast_ref::().menu_bar_state.clone(); crate::widget::text_context_menu::dismiss_popup_on_event(&menu_bar_state, event); @@ -1036,7 +1036,7 @@ where ); // On Wayland: if right-click just set context_menu_position, create a popup instead. - #[cfg(all(feature = "wayland", feature = "winit"))] + #[cfg(all(wayland_platform, feature = "winit"))] if matches!( crate::app::cosmic::WINDOWING_SYSTEM.get(), Some(crate::app::cosmic::WindowingSystem::Wayland) From b0b9bf16f4681de3f1a8617d35af2311ee7181dc Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 20 Jul 2026 11:26:46 -0400 Subject: [PATCH 183/201] fix: multi-window example headerbar the headerbar is transparent, but a wrapping container applies the right color --- examples/multi-window/src/window.rs | 23 +++++++++++++++++------ 1 file changed, 17 insertions(+), 6 deletions(-) diff --git a/examples/multi-window/src/window.rs b/examples/multi-window/src/window.rs index a6d40d36..c6e69d7e 100644 --- a/examples/multi-window/src/window.rs +++ b/examples/multi-window/src/window.rs @@ -5,7 +5,7 @@ use cosmic::iced::core::{id, Alignment, Length, Point}; use cosmic::iced::widget::{column, container, scrollable, text}; use cosmic::iced::{self, event, window, Subscription}; use cosmic::prelude::*; -use cosmic::widget::{button, header_bar}; +use cosmic::widget::{button, header_bar, selectable_text}; #[derive(Debug, Clone, PartialEq)] pub enum Message { @@ -132,10 +132,14 @@ impl cosmic::Application for MultiWindow { let new_window_button = button::custom(text("New Window")).on_press(Message::NewWindow); let content = scrollable( - column![input, new_window_button] - .spacing(50) - .width(Length::Fill) - .align_x(Alignment::Center), + column![ + input, + new_window_button, + selectable_text::title1("I'm selectable...") + ] + .spacing(50) + .width(Length::Fill) + .align_x(Alignment::Center), ); let window_content = container(container(content).center_x(Length::Fixed(200.))) @@ -146,7 +150,14 @@ impl cosmic::Application for MultiWindow { if id == self.core.main_window_id().unwrap() { window_content.into() } else { - column![header_bar().focused(focused), window_content].into() + container(column![ + header_bar() + .on_close(Message::CloseWindow(id)) + .focused(focused), + window_content + ]) + .class(cosmic::theme::Container::WindowBackground) + .into() } } From 324ef6e9056b540346c0de141fd2c6ffcd2d80dc Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Mon, 20 Jul 2026 14:46:35 -0400 Subject: [PATCH 184/201] chore: update selectable text menu corner radius --- src/app/cosmic.rs | 4 ++-- src/widget/text_context_menu.rs | 24 +++++++++++++++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index b559ab4a..d1fffa33 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -544,11 +544,11 @@ where // `app.update()` and create them through the normal popup pipeline. #[cfg(all(wayland_platform, target_os = "linux"))] for req in crate::widget::text_context_menu::take_popup_requests() { - let (settings, view) = + let (live_settings, settings, view) = crate::widget::text_context_menu::into_popup_view::(req); task = task.chain(self.get_popup( settings, - Box::new(|_| LiveSettings::default()), + Box::new(move |_| live_settings), Some(Box::new(move |_| view())), )); } diff --git a/src/widget/text_context_menu.rs b/src/widget/text_context_menu.rs index 6fdea222..11df26d7 100644 --- a/src/widget/text_context_menu.rs +++ b/src/widget/text_context_menu.rs @@ -15,8 +15,14 @@ //! the context menu as a native popup surface. pub use iced_core::widget::text::HasSelectableText; +#[cfg(wayland_platform)] +use iced_runtime::platform_specific::wayland::CornerRadius; +#[cfg(wayland_platform)] +use crate::surface::action::LiveSettings; use crate::widget::RcElementWrapper; +#[cfg(wayland_platform)] +use crate::widget::menu::StyleSheet; use crate::widget::menu::{ self, CloseCondition, ItemHeight, ItemWidth, Menu, MenuBarState, PathHighlight, menu_roots_diff, }; @@ -59,6 +65,7 @@ use iced_runtime::platform_specific::wayland::popup::SctkPopupSettings; /// through the normal `get_popup()` Task + `surface_views` pipeline. #[cfg(wayland_platform)] pub(crate) struct PopupRequest { + live_settings: LiveSettings, settings: SctkPopupSettings, menu: Menu<'static, TextCtxAction>, selected_text: Option, @@ -93,6 +100,7 @@ pub(crate) fn take_popup_requests() -> Vec { pub(crate) fn into_popup_view( req: PopupRequest, ) -> ( + LiveSettings, SctkPopupSettings, Box crate::Element<'static, crate::Action> + Send + Sync>, ) { @@ -101,6 +109,7 @@ pub(crate) fn into_popup_view( menu, selected_text, pending_action, + live_settings, } = req; let view = Box::new(move || { @@ -114,7 +123,7 @@ pub(crate) fn into_popup_view( .map(crate::action::app) }); - (settings, view) + (live_settings, settings, view) } #[cfg(wayland_platform)] @@ -589,12 +598,25 @@ pub(crate) fn create_text_context_popup( input_zone: None, }; + let t = crate::theme::THEME.lock().unwrap(); + let styling = t.appearance(&crate::theme::menu_bar::MenuBarStyle::Default, false); + drop(t); + let rad = styling.menu_border_radius; PENDING_POPUP_REQUESTS.with(|q| { q.borrow_mut().push(PopupRequest { settings, menu: popup_menu, selected_text, pending_action: pending_action.clone(), + live_settings: LiveSettings { + corners: Some(CornerRadius { + top_left: rad[0] as u32, + top_right: rad[1] as u32, + bottom_left: rad[2] as u32, + bottom_right: rad[3] as u32, + }), + ..Default::default() + }, }); }); wake_runtime(); From 8a017a15ee7753241c0a631b4294a68edf79b13b Mon Sep 17 00:00:00 2001 From: Ashley Wulber Date: Wed, 5 Aug 2026 21:20:51 +0200 Subject: [PATCH 185/201] fix: window id handling for multi-window --- src/app/cosmic.rs | 3 +-- src/widget/selectable_text.rs | 13 ++++++++++++- src/widget/text_context_menu.rs | 11 ++++++++--- src/widget/text_editor.rs | 11 +++++++++-- src/widget/text_input/input.rs | 9 ++++++++- 5 files changed, 38 insertions(+), 9 deletions(-) diff --git a/src/app/cosmic.rs b/src/app/cosmic.rs index d1fffa33..ce8a4154 100644 --- a/src/app/cosmic.rs +++ b/src/app/cosmic.rs @@ -743,6 +743,7 @@ where #[cfg(feature = "multi-window")] pub fn view(&self, id: window::Id) -> Element<'_, crate::Action> { + crate::widget::text_context_menu::set_current_window_id(id); #[cfg(all(wayland_platform, target_os = "linux"))] if let Some((_, _, _, Some(v))) = self.surface_views.get(&id) { return v(&self.app); @@ -756,8 +757,6 @@ where return self.app.view_window(id).map(crate::Action::App); } - crate::widget::text_context_menu::set_current_window_id(id); - let view = if self.app.core().window.use_template { self.app.view_main() } else { diff --git a/src/widget/selectable_text.rs b/src/widget/selectable_text.rs index 4b47cfd4..1b65d774 100644 --- a/src/widget/selectable_text.rs +++ b/src/widget/selectable_text.rs @@ -1,6 +1,7 @@ use crate::Renderer; use crate::widget::menu::MenuBarState; +use iced::window; use iced_core::event::Event; use iced_core::text::LineHeight; use iced_core::widget::Widget; @@ -30,6 +31,7 @@ pub struct SelectableText<'a> { inner: InnerText<'a>, selectable: bool, context_menu: bool, + window_id: window::Id, } impl<'a> SelectableText<'a> { @@ -41,6 +43,7 @@ impl<'a> SelectableText<'a> { inner: InnerText::new(content), selectable: true, context_menu: true, + window_id: crate::widget::text_context_menu::current_window_id(), } } @@ -183,6 +186,7 @@ impl<'a> From> for SelectableText<'a> { inner, selectable: true, context_menu: true, + window_id: crate::widget::text_context_menu::current_window_id(), } } } @@ -356,6 +360,7 @@ impl<'a, Message: Clone + 'static> Widget for S renderer, viewport, cursor, + self.window_id, ); self.inner @@ -380,7 +385,11 @@ impl<'a, Message: Clone + 'static> Widget for S let wrapper_state = tree.state.downcast_ref::(); let menu_bar_state = wrapper_state.menu_bar_state.clone(); - crate::widget::text_context_menu::dismiss_popup_on_event(&menu_bar_state, event); + crate::widget::text_context_menu::dismiss_popup_on_event( + &menu_bar_state, + event, + self.window_id, + ); } } else { w_mut::(&mut self.inner).update( @@ -477,6 +486,7 @@ impl<'a, Message: Clone + 'static> From> for crate::Element<' inner, selectable, context_menu, + window_id, } = text; // Apply selection to the inner widget here, at the last moment before // it becomes part of the tree. iced's `Text::selectable()` is one-way @@ -491,6 +501,7 @@ impl<'a, Message: Clone + 'static> From> for crate::Element<' inner, selectable, context_menu, + window_id, }) } } diff --git a/src/widget/text_context_menu.rs b/src/widget/text_context_menu.rs index 11df26d7..c156d869 100644 --- a/src/widget/text_context_menu.rs +++ b/src/widget/text_context_menu.rs @@ -16,6 +16,8 @@ pub use iced_core::widget::text::HasSelectableText; #[cfg(wayland_platform)] +use iced_core::window; +#[cfg(wayland_platform)] use iced_runtime::platform_specific::wayland::CornerRadius; #[cfg(wayland_platform)] @@ -486,10 +488,10 @@ pub(crate) fn create_text_context_popup( renderer: &crate::Renderer, viewport: &Rectangle, cursor: mouse::Cursor, + window_id: window::Id, ) { use iced_runtime::platform_specific::wayland::popup::{SctkPopupSettings, SctkPositioner}; - let window_id = current_window_id(); if window_id == iced_core::window::Id::NONE { return; } @@ -625,7 +627,11 @@ pub(crate) fn create_text_context_popup( /// Dismisses this widget's open context-menu popup on an outside click, /// touch, or Escape. #[cfg(wayland_platform)] -pub(crate) fn dismiss_popup_on_event(menu_bar_state: &MenuBarState, event: &event::Event) { +pub(crate) fn dismiss_popup_on_event( + menu_bar_state: &MenuBarState, + event: &event::Event, + window_id: window::Id, +) { let is_dismiss = matches!( event, event::Event::Mouse(mouse::Event::ButtonPressed( @@ -639,7 +645,6 @@ pub(crate) fn dismiss_popup_on_event(menu_bar_state: &MenuBarState, event: &even return; } - let window_id = current_window_id(); let popup_id = menu_bar_state .inner .with_data(|state| state.popup_id.get(&window_id).copied()); diff --git a/src/widget/text_editor.rs b/src/widget/text_editor.rs index 9c617363..f25cb277 100644 --- a/src/widget/text_editor.rs +++ b/src/widget/text_editor.rs @@ -15,7 +15,7 @@ use iced_core::text::highlighter; use iced_core::widget::Widget; use iced_core::widget::tree::{self, Tree}; use iced_core::{ - Clipboard, Layout, Length, Rectangle, Shell, Size, Vector, mouse, overlay, renderer, + Clipboard, Layout, Length, Rectangle, Shell, Size, Vector, mouse, overlay, renderer, window, }; type InnerEditor<'a, Message> = @@ -24,6 +24,7 @@ type InnerEditor<'a, Message> = pub struct TextEditor<'a, Message> { inner: InnerEditor<'a, Message>, has_context_menu: bool, + window_id: window::Id, } struct EditorWrapperState { @@ -37,6 +38,7 @@ impl<'a, Message: Clone + 'static> TextEditor<'a, Message> { Self { inner: iced_widget::text_editor(content), has_context_menu: true, + window_id: crate::widget::text_context_menu::current_window_id(), } } @@ -300,6 +302,7 @@ impl<'a, Message: Clone + 'static> Widget Widget(); let menu_bar_state = wrapper_state.menu_bar_state.clone(); - crate::widget::text_context_menu::dismiss_popup_on_event(&menu_bar_state, event); + crate::widget::text_context_menu::dismiss_popup_on_event( + &menu_bar_state, + event, + self.window_id, + ); } } else { ew_mut::(&mut self.inner).update( diff --git a/src/widget/text_input/input.rs b/src/widget/text_input/input.rs index d970e435..37db1756 100644 --- a/src/widget/text_input/input.rs +++ b/src/widget/text_input/input.rs @@ -218,6 +218,7 @@ pub struct TextInput<'a, Message> { /// The text input tracks and manages the input value in its state. manage_value: bool, drag_threshold: f32, + window_id: window::Id, } impl<'a, Message> TextInput<'a, Message> @@ -268,6 +269,7 @@ where always_active: false, manage_value: false, drag_threshold: 20.0, + window_id: crate::widget::text_context_menu::current_window_id(), } } @@ -922,7 +924,11 @@ where #[cfg(all(wayland_platform, feature = "winit"))] if self.uses_popup_context_menu() { let menu_bar_state = tree.state.downcast_ref::().menu_bar_state.clone(); - crate::widget::text_context_menu::dismiss_popup_on_event(&menu_bar_state, event); + crate::widget::text_context_menu::dismiss_popup_on_event( + &menu_bar_state, + event, + self.window_id, + ); } let text_layout = self.text_layout(layout); @@ -1062,6 +1068,7 @@ where renderer, viewport, cursor_position, + self.window_id, ); let state = tree.state.downcast_mut::(); From 030a7dbe02dea922a782aa7606d5057b8d1b5be3 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Fri, 7 Aug 2026 13:45:34 -0600 Subject: [PATCH 186/201] fix: request redraw in selectable text and context menu --- iced | 2 +- src/widget/selectable_text.rs | 1 + src/widget/text_context_menu.rs | 3 +++ 3 files changed, 5 insertions(+), 1 deletion(-) diff --git a/iced b/iced index f5aa5a6f..7918b282 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit f5aa5a6f8cc2db6e2adbbc140c310a5a4af2f8d2 +Subproject commit 7918b2826ec8ad4f9e0032f6a21286bac431e482 diff --git a/src/widget/selectable_text.rs b/src/widget/selectable_text.rs index 1b65d774..18328c40 100644 --- a/src/widget/selectable_text.rs +++ b/src/widget/selectable_text.rs @@ -377,6 +377,7 @@ impl<'a, Message: Clone + 'static> Widget for S crate::widget::text_context_menu::TextCtxAction::Copy => {} crate::widget::text_context_menu::TextCtxAction::SelectAll => { self.inner.select_all(&mut tree.children[0]); + shell.request_redraw(); } _ => {} } diff --git a/src/widget/text_context_menu.rs b/src/widget/text_context_menu.rs index c156d869..2e908331 100644 --- a/src/widget/text_context_menu.rs +++ b/src/widget/text_context_menu.rs @@ -404,6 +404,7 @@ where state.open = false; }); self.widget.set_context_menu_position(self.tree, None); + shell.request_redraw(); } return; } @@ -450,6 +451,8 @@ where } } self.widget.set_context_menu_position(self.tree, None); + // The menu closes and the selection may have changed. + shell.request_redraw(); } let is_open = self.menu.tree.inner.with_data(|state| state.open); From 633e6ab83fa019a75f7c7c94c55d04dc5650bd7b Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 10 Aug 2026 10:13:19 -0600 Subject: [PATCH 187/201] fix: request redraw if hovered state changes in segmented_button --- 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 ca877348..29c26362 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -1244,6 +1244,8 @@ where let my_bounds = layout.bounds(); let state = tree.state.downcast_mut::(); + let hovered_before = state.hovered; + let my_id = self.get_drag_id(); if let Event::Dnd(e) = &mut event { @@ -1916,6 +1918,10 @@ where shell.capture_event(); } } + + if hovered_before != state.hovered { + shell.request_redraw(); + } } fn operate( @@ -2764,7 +2770,7 @@ pub struct LocalState { drop_hint: Option, } -#[derive(Debug, Default, PartialEq)] +#[derive(Clone, Copy, Debug, Default, PartialEq)] enum Item { NextButton, #[default] From 5da2e943379fd460561002cadf62243886ea972d Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 10 Aug 2026 12:12:18 -0600 Subject: [PATCH 188/201] fix: request redraw when menu item hover state changes --- src/widget/dropdown/multi/menu.rs | 23 ++++++++++------------- src/widget/menu/menu_inner.rs | 5 +++++ 2 files changed, 15 insertions(+), 13 deletions(-) diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index 689fb0de..e699890b 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -366,7 +366,8 @@ where let mut current_offset = 0.0; - let previous_hover_option = self.hovered_option.take(); + let previous_hover_option = self.hovered_option.clone(); + *self.hovered_option = None; for (element, elem_height) in self.options.elements().zip(heights) { let bounds = Rectangle { @@ -377,27 +378,23 @@ where }; if bounds.contains(cursor_position) { - *self.hovered_option = if let OptionElement::Option((_, item)) = element - { - if previous_hover_option.as_ref() == Some(item) { - previous_hover_option - } else { - shell.request_redraw(); - + if let OptionElement::Option((_, item)) = element { + *self.hovered_option = Some(item.clone()); + if previous_hover_option.as_ref() != Some(item) { if let Some(on_option_hovered) = self.on_option_hovered { shell.publish(on_option_hovered(item.clone())); } - - Some(item.clone()) } - } else { - None - }; + } break; } current_offset += elem_height; } + + if *self.hovered_option != previous_hover_option { + shell.request_redraw(); + } } } Event::Touch(touch::Event::FingerPressed { .. }) => { diff --git a/src/widget/menu/menu_inner.rs b/src/widget/menu/menu_inner.rs index 4ea71bc4..291d7db0 100644 --- a/src/widget/menu/menu_inner.rs +++ b/src/widget/menu/menu_inner.rs @@ -634,6 +634,10 @@ impl<'b, Message: Clone + 'static> Menu<'b, Message> { shell, ); + if view_cursor.is_over(viewport) { + shell.request_redraw(); + } + if self.is_overlay && view_cursor.is_over(viewport) { shell.capture_event(); } @@ -1699,6 +1703,7 @@ fn process_scroll_events( } } shell.capture_event(); + shell.request_redraw(); }); } From 06337516337d3bb6e7e47bdf325013f366b1bd6d Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 10 Aug 2026 12:22:38 -0600 Subject: [PATCH 189/201] fix: request redraw if pressed state changes in button widget --- 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 f5501134..91e88624 100644 --- a/src/widget/button/widget.rs +++ b/src/widget/button/widget.rs @@ -806,6 +806,7 @@ pub fn update<'a, Message: Clone>( if cursor.is_over(bounds) { state.is_pressed = true; + shell.request_redraw(); if let Some(on_press_down) = on_press_down { let msg = (on_press_down)(layout.virtual_offset(), layout.bounds()); @@ -837,7 +838,10 @@ pub fn update<'a, Message: Clone>( } } else if on_press_down.is_some() { let state = state(); - state.is_pressed = false; + if state.is_pressed { + state.is_pressed = false; + shell.request_redraw(); + } } } #[cfg(feature = "a11y")] From 1f8d8786940cc6db59d412153c5a03f69a20375c Mon Sep 17 00:00:00 2001 From: Hojjat Date: Mon, 10 Aug 2026 12:25:40 -0600 Subject: [PATCH 190/201] fix: request redraw when hover state changes in toggle widget --- src/widget/toggler.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/widget/toggler.rs b/src/widget/toggler.rs index a610747c..5c36a5b6 100644 --- a/src/widget/toggler.rs +++ b/src/widget/toggler.rs @@ -266,6 +266,17 @@ impl<'a, Message> Widget for Toggler<'a, shell.request_redraw(); } } + Event::Mouse(mouse::Event::CursorMoved { .. }) => { + let toggler_bounds = layout + .children() + .nth(1) + .map_or_else(|| layout.bounds(), |l| l.bounds()); + let hovered = cursor_position.is_over(toggler_bounds); + if state.hovered != hovered { + state.hovered = hovered; + shell.request_redraw(); + } + } _ => {} } } @@ -442,4 +453,5 @@ pub struct State { text: widget::text::State<::Paragraph>, anim: anim::State, prev_toggled: bool, + hovered: bool, } From 75e9ee7e65a00f168ac8b1123d69d2d648dc388c Mon Sep 17 00:00:00 2001 From: Paul <33877645+iampaulmata@users.noreply.github.com> Date: Mon, 17 Aug 2026 18:51:29 -0400 Subject: [PATCH 191/201] fix: request redraw on dropdown open/close in the in-window overlay path MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `Dropdown::new` (the plain constructor used by any non-applet consumer) leaves `window_id`/`on_surface_action` unset, which routes the widget's `overlay()` impl to the in-window overlay path rather than the surface-action-based popup path. On that path, `update()`'s `open` closure and its close counterparts only ever requested a redraw inside the `#[cfg(wayland_platform)] if let Some(...)` block gated on those fields being set — so for the common case, opening or closing a dropdown flipped `is_open` with no redraw requested at all. The stale frame stuck around until some unrelated later event (e.g. cursor motion landing on the now-open/closed menu's bounds) forced a repaint, making the menu appear to not open until the mouse moved, and not close until the same thing happened again on the way out. Add an unconditional `shell.request_redraw()` at each of the three `is_open` transitions that don't already publish a Message of their own (an actual option selection already triggers a normal Message-driven redraw through the app's own update cycle, so that path needed no change): - `open`: right after `is_open` is set true, ahead of the wayland-only surface-action block. - the `close_operation` branch of the open/close operation state machine (`Id`-based programmatic close). - the click-outside-closes-it branch of the mouse/touch press handler. Fixes #1395 --- src/widget/dropdown/widget.rs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 696e6c0d..8d80fb4a 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -661,6 +661,7 @@ pub fn update< state.close_operation = false; state.is_open.store(false, Ordering::SeqCst); if is_open { + shell.request_redraw(); #[cfg(wayland_platform)] if let Some(ref on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); @@ -684,6 +685,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); + shell.request_redraw(); #[cfg(wayland_platform)] if let Some(on_close) = on_surface_action { shell.publish(on_close(surface::action::destroy_popup(state.popup_id))); From a5267e623f356d1792cba7f480dd671cc8b52406 Mon Sep 17 00:00:00 2001 From: Hojjat Date: Tue, 18 Aug 2026 18:39:52 -0600 Subject: [PATCH 192/201] fix: request redraw on hover state change in dropdown widget This also add a missing request when dropdown opens --- src/widget/dropdown/menu/mod.rs | 15 +++++++++++++++ src/widget/dropdown/multi/menu.rs | 9 +++++++++ src/widget/dropdown/widget.rs | 1 + 3 files changed, 25 insertions(+) diff --git a/src/widget/dropdown/menu/mod.rs b/src/widget/dropdown/menu/mod.rs index f18016d8..f6fbb218 100644 --- a/src/widget/dropdown/menu/mod.rs +++ b/src/widget/dropdown/menu/mod.rs @@ -508,6 +508,21 @@ where } *hovered_guard = Some(new_hovered_option); + } else { + let mut hovered_guard = self.hovered_option.lock().unwrap(); + + if hovered_guard.is_some() { + *hovered_guard = None; + shell.request_redraw(); + } + } + } + Event::Mouse(mouse::Event::CursorLeft) => { + let mut hovered_guard = self.hovered_option.lock().unwrap(); + + if hovered_guard.is_some() { + *hovered_guard = None; + shell.request_redraw(); } } Event::Touch(touch::Event::FingerPressed { .. }) => { diff --git a/src/widget/dropdown/multi/menu.rs b/src/widget/dropdown/multi/menu.rs index e699890b..fe1369c3 100644 --- a/src/widget/dropdown/multi/menu.rs +++ b/src/widget/dropdown/multi/menu.rs @@ -395,6 +395,15 @@ where if *self.hovered_option != previous_hover_option { shell.request_redraw(); } + } else if self.hovered_option.is_some() { + *self.hovered_option = None; + shell.request_redraw(); + } + } + Event::Mouse(mouse::Event::CursorLeft) => { + if self.hovered_option.is_some() { + *self.hovered_option = None; + shell.request_redraw(); } } Event::Touch(touch::Event::FingerPressed { .. }) => { diff --git a/src/widget/dropdown/widget.rs b/src/widget/dropdown/widget.rs index 8d80fb4a..e38be851 100644 --- a/src/widget/dropdown/widget.rs +++ b/src/widget/dropdown/widget.rs @@ -567,6 +567,7 @@ pub fn update< state: &mut State, on_selected: Arc Message + Send + Sync + 'static>| { state.is_open.store(true, Ordering::Relaxed); + shell.request_redraw(); let mut hovered_guard = state.hovered_option.lock().unwrap(); *hovered_guard = selected; let id = window::Id::unique(); From 5392d9d5aea773320f541ea1fbf9742340c657e5 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Wed, 19 Aug 2026 21:18:31 +0200 Subject: [PATCH 193/201] segmented_button: honour WindowControlsPosition for the tab close button The close button of closable tabs was pinned to the trailing edge, so enabling macOS-style window controls left the window buttons on the left and the tab crosses on the right. Read crate::config::window_controls_position() and, when Start, place the close button before the tab contents and shift them accordingly. The hit-test, the text width and the drawn icon all derive from the same position so they stay in sync. Co-Authored-By: Claude Opus 5 (1M context) --- src/widget/segmented_button/widget.rs | 56 ++++++++++++++++++++------- 1 file changed, 41 insertions(+), 15 deletions(-) diff --git a/src/widget/segmented_button/widget.rs b/src/widget/segmented_button/widget.rs index 61808fbe..6b2633fb 100644 --- a/src/widget/segmented_button/widget.rs +++ b/src/widget/segmented_button/widget.rs @@ -9,7 +9,7 @@ use crate::widget::menu::{ self, CloseCondition, ItemHeight, ItemWidth, MenuBarState, PathHighlight, menu_roots_children, menu_roots_diff, }; -use crate::widget::{Icon, icon}; +use crate::widget::{Icon, WindowControlsPosition, icon}; use crate::{Element, Renderer}; use derive_setters::Setters; use iced::clipboard::dnd::{ @@ -1608,8 +1608,11 @@ where } } - let close_button_bounds = - close_bounds(bounds, f32::from(self.close_icon.size)); + let close_button_bounds = close_bounds( + bounds, + f32::from(self.close_icon.size), + crate::config::window_controls_position(), + ); let over_close_button = self.model.items[key].closable && cursor_position.is_over(close_button_bounds); @@ -2420,6 +2423,25 @@ 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) + && self.model.is_closable(key); + + // Width of the icon used by the close button, which we will subtract from the text bounds. + let close_icon_width = if show_close_button { + f32::from(self.close_icon.size) + } else { + 0.0 + }; + + // With macOS-style controls the close button precedes the contents, so + // shift them right to make room for it. + let close_position = crate::config::window_controls_position(); + if show_close_button && close_position == WindowControlsPosition::Start { + bounds.x += close_icon_width + f32::from(self.button_spacing); + } + // Draw the image beside the text. if let Some(icon) = self.model.icon(key) { let mut image_bounds = bounds; @@ -2477,21 +2499,17 @@ 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) - && self.model.is_closable(key); - - // Width of the icon used by the close button, which we will subtract from the text bounds. - let close_icon_width = if show_close_button { - f32::from(self.close_icon.size) + // Room reserved after the contents: only needed when the close button + // trails them, since a leading one is already counted in `bounds.x`. + let trailing_close_width = if close_position == WindowControlsPosition::End { + close_icon_width } else { 0.0 }; bounds.width = original_bounds.width - (bounds.x - original_bounds.x) - - close_icon_width + - trailing_close_width - f32::from(self.button_padding[2]); bounds.y = center_y; @@ -2517,7 +2535,8 @@ where // Draw a close button if set. if show_close_button { - let close_button_bounds = close_bounds(original_bounds, close_icon_width); + let close_button_bounds = + close_bounds(original_bounds, close_icon_width, close_position); draw_icon::( renderer, @@ -3070,9 +3089,16 @@ impl From for widget::Id { } /// Calculates the bounds of the close button within the area of an item. -fn close_bounds(area: Rectangle, icon_size: f32) -> Rectangle { +fn close_bounds( + area: Rectangle, + icon_size: f32, + position: WindowControlsPosition, +) -> Rectangle { Rectangle { - x: area.x + area.width - icon_size - 8.0, + x: match position { + WindowControlsPosition::Start => area.x + 8.0, + WindowControlsPosition::End => area.x + area.width - icon_size - 8.0, + }, y: area.center_y() - (icon_size / 2.0), width: icon_size, height: icon_size, From 3a8ebf91977bd21b0ab148b3a8326eb2ce52c246 Mon Sep 17 00:00:00 2001 From: Alex Klepov Date: Thu, 20 Aug 2026 01:18:04 +0300 Subject: [PATCH 194/201] 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 195/201] 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 196/201] 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 197/201] 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 198/201] 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 199/201] 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 200/201] 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 201/201] 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 {