From 6dba6a2561c84e5a1bf527c56856e7d78fb3c094 Mon Sep 17 00:00:00 2001 From: KENZ Date: Mon, 20 Apr 2026 00:02:26 +0900 Subject: [PATCH 001/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] =?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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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/135] 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 9e9ae942c4fca245b17d0279c96d3625f3fcbf90 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Thu, 2 Jul 2026 19:00:00 +0200 Subject: [PATCH 131/135] 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 132/135] 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 3e4ab4576d66d27d842f0589e055fc10cb0fbb25 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Sat, 4 Jul 2026 19:41:28 +0200 Subject: [PATCH 133/135] 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 28f7ab4651cbafbd9c6edc1bb6ce8763b638f52c Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Sat, 25 Jul 2026 22:20:01 +0200 Subject: [PATCH 134/135] 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 5392d9d5aea773320f541ea1fbf9742340c657e5 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Wed, 19 Aug 2026 21:18:31 +0200 Subject: [PATCH 135/135] 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,