Compare commits

..

14 commits

Author SHA1 Message Date
f7bdd84ae4 chore: use local graphics dependencies 2026-05-25 19:36:38 +02:00
4a00191ac9 chore: point iced at local glyphon 2026-05-25 18:28:10 +02:00
d4d421511a chore: use local COSMIC support crates 2026-05-25 18:09:50 +02:00
f3db54342c fix: drop iced_winit/single-instance feature ref
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
2026-05-25 13:13:30 +02:00
6722f1e581 yoda: use local dbus settings bindings 2026-05-25 13:10:12 +02:00
5f50d74477 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
2026-05-25 13:08:39 +02:00
7a191cf086 yoda: warning cleanup sweep (dead code + clippy --fix) (squashed)
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
2026-05-25 13:07:48 +02:00
38acba82b4 yoda: bump iced submodule + cargo auto-fix sweeps (squashed)
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
2026-05-25 13:07:13 +02:00
ef8f69134f feat(cosmic-theme): add apply_gtk_decoration_layout helper
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
2026-05-25 13:05:50 +02:00
597aba3e9f 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
2026-05-25 13:05:22 +02:00
e3dcdf1fce yoda: fork pivot — Wayland-only + ungate winit + soft-fork libcosmic-yoda (squashed)
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
2026-05-25 13:05:05 +02:00
10422b8f4a header_bar: add WindowControlsPosition (macOS-style left controls)
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<WindowControlsPosition>)` 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) <noreply@anthropic.com>
2026-05-25 13:04:22 +02:00
87510782ae 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
2026-05-25 13:03:13 +02:00
ea17ada931 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
2026-05-25 13:03:13 +02:00
7 changed files with 129 additions and 332 deletions

View file

@ -123,6 +123,8 @@ 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(

2
iced

@ -1 +1 @@
Subproject commit d9fe129f7316cea3ef6d2576e9421a305759b8bf
Subproject commit de1247123a631087d2f78831c99f5f1b66d5a582

View file

@ -23,12 +23,6 @@ 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

View file

@ -5,89 +5,6 @@
#![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 {
pub use crate::ApplicationExt;

View file

@ -51,10 +51,9 @@ impl KeyBind {
physical_key: Option<&Physical>,
) -> bool {
let key_eq = self.key_eq(key)
|| (!is_latin_shortcut_key(key)
&& physical_key
|| physical_key
.and_then(physical_key_to_latin)
.is_some_and(|latin| self.key_eq(&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)
@ -71,19 +70,6 @@ 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
@ -152,92 +138,3 @@ 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)),
));
}
}

View file

@ -9,8 +9,7 @@ use iced::{Element, Event, Length, Radians, Rectangle, Renderer, Size, Vector, m
use std::f32::consts::PI;
use std::time::Duration;
const MIN_GAP_ANGLE: Radians = Radians(PI / 4.0);
const MAX_WRAP: f32 = 1.0 - MIN_GAP_ANGLE.0 / (2.0 * PI);
const MIN_ANGLE: Radians = Radians(PI / 8.0);
#[must_use]
pub struct Circular<Theme>
@ -77,6 +76,12 @@ 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<Theme> Default for Circular<Theme>
@ -143,12 +148,11 @@ where
shell.request_redraw();
}
} else {
state.animation = state.animation.timed_transition(
self.cycle_duration,
self.period,
MAX_WRAP,
*now,
);
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();
}
@ -187,6 +191,25 @@ 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 {
@ -199,9 +222,10 @@ where
&builder.build(),
canvas::Stroke::default()
.with_color(custom_style.bar_color)
.with_width(self.bar_height)
.with_line_cap(canvas::LineCap::Round),
.with_width(self.bar_height),
);
draw_cap(frame, end, false);
draw_cap(frame, start, true);
};
if self.progress.is_some() {
@ -219,11 +243,10 @@ where
}
draw_bar(frame, 0.0, state.progress.current);
} else {
// f32::EPSILON prevents flicker when wrap angle is 0.0
let (start, end) =
state
let (min, wrap) = self.min_wrap(track_radius);
let (start, end) = state
.animation
.bar_positions(self.cycle_duration, f32::EPSILON, MAX_WRAP);
.bar_positions(self.cycle_duration, min, wrap);
draw_bar(frame, start, end);
}
});

View file

@ -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::{Background, 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: 1.0,
segment_spacing: 0.0,
}
}
@ -81,11 +81,11 @@ where
}
/// Sets the markers of a determinate progress bar, which divide the bar into segments.
/// Each marker is a value between `0.0` and `1.0` that defines the position of a visual gap.
/// 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<Vec<f32>>) -> Self {
let mut markers = markers.into();
for marker in &mut markers {
*marker = marker.clamp(0.0, 1.0);
for bp in &mut markers {
*bp = bp.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<Pixels>) -> Self {
self.segment_spacing = spacing.into().0.max(1.0);
self.segment_spacing = spacing.into().0;
self
}
}
@ -197,144 +197,110 @@ 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;
}
}
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 {
x: bounds.x + x,
y: bounds.y + (bounds.height - height) / 2.0,
width,
height,
x: bounds.x + x * bounds.width,
y: bounds.y,
width: width * bounds.width,
height: bounds.height,
},
border,
snap: true,
..renderer::Quad::default()
},
color,
Background::Color(color),
);
}
};
if self.progress.is_some() {
let current_p = state.progress.current;
let len = self.markers.len();
let spacing = self.segment_spacing;
let spacing = self.segment_spacing.max(1.0);
let radius_inner = radius.min(spacing);
let gap = if len != 0 {
spacing / bounds.width
} else {
let gap = if self.markers.is_empty() {
0.0
} else {
spacing / bounds.width
};
let drawable = 1.0 - gap * len as f32;
let drawable = 1.0 - gap * self.markers.len() as f32;
let num_segments = self.markers.len() + 1;
let mut absolute_width = 0.0;
for i in 0..=len {
let (seg_lo, r_left) = if i == 0 {
(0.0, radius)
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 - 1], radius_inner)
self.markers[i]
};
let (seg_hi, r_right) = if i == len {
(1.0, radius)
(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 {
(self.markers[i], radius_inner)
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 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
draw_quad(
x_start * bounds.width,
x_width * bounds.width,
x_start,
x_width,
custom_style.track_color,
iced::Border {
width: border_width,
color: border_color,
radius: segment_radius,
radius: get_radius(i),
},
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,
);
}
// 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,
bounds.width,
1.0,
custom_style.track_color,
iced::Border {
width: border_width,
color: border_color,
radius: radius.into(),
},
true,
bounds.width,
);
// draw bar
@ -346,26 +312,24 @@ 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 * bounds.width,
right_width * bounds.width,
start,
right_width,
custom_style.bar_color,
border,
false,
(right_width + start) * bounds.width,
iced::Border {
radius: radius.into(),
..iced::Border::default()
},
);
draw_quad(
0.0,
left_width * bounds.width,
left_width,
custom_style.bar_color,
border,
false,
left_width * bounds.width,
iced::Border {
radius: radius.into(),
..iced::Border::default()
},
);
}
}