feat(progress_bar): draw circular line caps directly & fix gaps between the bar and caps

This commit is contained in:
Vukašin Vojinović 2026-05-29 17:43:55 +02:00 committed by GitHub
parent 04a93d304c
commit c9255fe871
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 16 additions and 39 deletions

View file

@ -9,7 +9,8 @@ use iced::{Element, Event, Length, Radians, Rectangle, Renderer, Size, Vector, m
use std::f32::consts::PI; use std::f32::consts::PI;
use std::time::Duration; 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] #[must_use]
pub struct Circular<Theme> pub struct Circular<Theme>
@ -76,12 +77,6 @@ where
self.progress = Some(progress.clamp(0.0, 1.0)); self.progress = Some(progress.clamp(0.0, 1.0));
self 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> impl<Theme> Default for Circular<Theme>
@ -148,11 +143,12 @@ where
shell.request_redraw(); shell.request_redraw();
} }
} else { } else {
let (_, wrap) = self.min_wrap(self.size / 2.0 - self.bar_height); state.animation = state.animation.timed_transition(
state.animation = self.cycle_duration,
state self.period,
.animation MAX_WRAP,
.timed_transition(self.cycle_duration, self.period, wrap, *now); *now,
);
state.cache.clear(); state.cache.clear();
shell.request_redraw(); shell.request_redraw();
} }
@ -191,25 +187,6 @@ where
// Converts a track fraction to an angle in radians, with 0 being top of circle // 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 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 draw_bar = |frame: &mut canvas::Frame, start: f32, end: f32| {
let mut builder = canvas::path::Builder::new(); let mut builder = canvas::path::Builder::new();
builder.arc(canvas::path::Arc { builder.arc(canvas::path::Arc {
@ -222,10 +199,9 @@ where
&builder.build(), &builder.build(),
canvas::Stroke::default() canvas::Stroke::default()
.with_color(custom_style.bar_color) .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() { if self.progress.is_some() {
@ -243,10 +219,11 @@ where
} }
draw_bar(frame, 0.0, state.progress.current); draw_bar(frame, 0.0, state.progress.current);
} else { } else {
let (min, wrap) = self.min_wrap(track_radius); // f32::EPSILON prevents flicker when wrap angle is 0.0
let (start, end) = state let (start, end) =
.animation state
.bar_positions(self.cycle_duration, min, wrap); .animation
.bar_positions(self.cycle_duration, f32::EPSILON, MAX_WRAP);
draw_bar(frame, start, end); draw_bar(frame, start, end);
} }
}); });

View file

@ -259,7 +259,7 @@ where
// draw bar segment // draw bar segment
if current_p > seg_lo { 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( draw_quad(
x_start, x_start,
x_width * fill, x_width * fill,