diff --git a/examples/applet/src/window.rs b/examples/applet/src/window.rs index 8722a2a4..57546ab1 100644 --- a/examples/applet/src/window.rs +++ b/examples/applet/src/window.rs @@ -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( diff --git a/iced b/iced index d9fe129f..de124712 160000 --- a/iced +++ b/iced @@ -1 +1 @@ -Subproject commit d9fe129f7316cea3ef6d2576e9421a305759b8bf +Subproject commit de1247123a631087d2f78831c99f5f1b66d5a582 diff --git a/justfile b/justfile index 7d9aecee..4653434e 100644 --- a/justfile +++ b/justfile @@ -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 diff --git a/src/lib.rs b/src/lib.rs index e22a3a1f..9c31506d 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -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; diff --git a/src/widget/menu/key_bind.rs b/src/widget/menu/key_bind.rs index b764fde7..ef87b344 100644 --- a/src/widget/menu/key_bind.rs +++ b/src/widget/menu/key_bind.rs @@ -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 - .and_then(physical_key_to_latin) - .is_some_and(|latin| self.key_eq(&latin))); + || 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) @@ -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)), - )); - } -} diff --git a/src/widget/progress_bar/circular.rs b/src/widget/progress_bar/circular.rs index 3318d7a3..3a3fedc4 100644 --- a/src/widget/progress_bar/circular.rs +++ b/src/widget/progress_bar/circular.rs @@ -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 @@ -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 Default for Circular @@ -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 - .animation - .bar_positions(self.cycle_duration, f32::EPSILON, MAX_WRAP); + 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 b8e92453..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::{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>) -> 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) -> 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; - } - } - - renderer.fill_quad( - renderer::Quad { - bounds: Rectangle { - x: bounds.x + x, - y: bounds.y + (bounds.height - height) / 2.0, - width, - height, + 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 * bounds.width, + y: bounds.y, + width: width * bounds.width, + height: bounds.height, + }, + border, + snap: true, + ..renderer::Quad::default() }, - 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() + }, ); } }