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] 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 9f89061..05841f5 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 7672579..0ebe402 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() + }, + ); } } }