cosmic/widget/progress_bar/
circular.rs1use super::animation::{Animation, Progress};
3use super::style::StyleSheet;
4use iced::advanced::widget::tree::{self, Tree};
5use iced::advanced::{self, Clipboard, Layout, Shell, Widget, layout, renderer};
6use iced::widget::canvas;
7use iced::{Element, Event, Length, Radians, Rectangle, Renderer, Size, Vector, mouse, window};
8
9use std::f32::consts::PI;
10use std::time::Duration;
11
12const MIN_GAP_ANGLE: Radians = Radians(PI / 4.0);
13const MAX_WRAP: f32 = 1.0 - MIN_GAP_ANGLE.0 / (2.0 * PI);
14
15#[must_use]
16pub struct Circular<Theme>
17where
18 Theme: StyleSheet,
19{
20 size: f32,
21 bar_height: Option<f32>,
22 style: Theme::Style,
23 cycle_duration: Duration,
24 period: Duration,
25 progress: Option<f32>,
26}
27
28impl<Theme> Circular<Theme>
29where
30 Theme: StyleSheet,
31{
32 pub fn new() -> Self {
34 Circular {
35 size: 48.0,
36 bar_height: None,
37 style: Theme::Style::default(),
38 cycle_duration: Duration::from_millis(1500),
39 period: Duration::from_secs(2),
40 progress: None,
41 }
42 }
43
44 pub fn size(mut self, size: f32) -> Self {
46 self.size = size;
47 self
48 }
49
50 pub fn bar_height(mut self, bar_height: f32) -> Self {
53 self.bar_height = Some(bar_height);
54 self
55 }
56
57 pub fn style(mut self, style: Theme::Style) -> Self {
59 self.style = style;
60 self
61 }
62
63 pub fn cycle_duration(mut self, duration: Duration) -> Self {
65 self.cycle_duration = duration / 2;
66 self
67 }
68
69 pub fn period(mut self, duration: Duration) -> Self {
72 self.period = duration;
73 self
74 }
75
76 pub fn progress(mut self, progress: f32) -> Self {
78 self.progress = Some(progress.clamp(0.0, 1.0));
79 self
80 }
81}
82
83impl<Theme> Default for Circular<Theme>
84where
85 Theme: StyleSheet,
86{
87 fn default() -> Self {
88 Self::new()
89 }
90}
91
92#[derive(Default)]
93struct State {
94 animation: Animation,
95 cache: canvas::Cache,
96 progress: Progress,
97}
98
99impl<Message, Theme> Widget<Message, Theme, Renderer> for Circular<Theme>
100where
101 Message: Clone,
102 Theme: StyleSheet,
103{
104 fn tag(&self) -> tree::Tag {
105 tree::Tag::of::<State>()
106 }
107
108 fn state(&self) -> tree::State {
109 tree::State::new(State::default())
110 }
111
112 fn size(&self) -> Size<Length> {
113 Size {
114 width: Length::Fixed(self.size),
115 height: Length::Fixed(self.size),
116 }
117 }
118
119 fn layout(
120 &mut self,
121 _tree: &mut Tree,
122 _renderer: &Renderer,
123 limits: &layout::Limits,
124 ) -> layout::Node {
125 layout::atomic(limits, self.size, self.size)
126 }
127
128 fn update(
129 &mut self,
130 tree: &mut Tree,
131 event: &Event,
132 _layout: Layout<'_>,
133 _cursor: mouse::Cursor,
134 _renderer: &Renderer,
135 _clipboard: &mut dyn Clipboard,
136 shell: &mut Shell<'_, Message>,
137 _viewport: &Rectangle,
138 ) {
139 let state = tree.state.downcast_mut::<State>();
140 if let Event::Window(window::Event::RedrawRequested(now)) = event {
141 if let Some(target) = self.progress {
142 if state.progress.update(target, *now) {
143 state.cache.clear();
144 shell.request_redraw();
145 }
146 } else {
147 state.animation = state.animation.timed_transition(
148 self.cycle_duration,
149 self.period,
150 MAX_WRAP,
151 *now,
152 );
153 state.cache.clear();
154 shell.request_redraw();
155 }
156 }
157 }
158
159 fn draw(
160 &self,
161 tree: &Tree,
162 renderer: &mut Renderer,
163 theme: &Theme,
164 _style: &renderer::Style,
165 layout: Layout<'_>,
166 _cursor: mouse::Cursor,
167 _viewport: &Rectangle,
168 ) {
169 use advanced::Renderer as _;
170
171 let state = tree.state.downcast_ref::<State>();
172 let bounds = layout.bounds();
173 let custom_style = Theme::appearance(theme, &self.style, self.progress.is_some(), true);
174
175 let geometry = state.cache.draw(renderer, bounds.size(), |frame| {
176 let bar_height = self.bar_height.unwrap_or((frame.width() / 12.0).max(2.0));
177 let track_radius = (frame.width() - bar_height) / 2.0;
178 if track_radius <= 0.0 {
179 return;
180 }
181
182 let track_path = canvas::Path::circle(frame.center(), track_radius);
183 frame.stroke(
184 &track_path,
185 canvas::Stroke::default()
186 .with_color(custom_style.track_color)
187 .with_width(bar_height),
188 );
189
190 let draw_bar = |frame: &mut canvas::Frame, start: f32, end: f32| {
191 let mut builder = canvas::path::Builder::new();
192 builder.arc(canvas::path::Arc {
193 center: frame.center(),
194 radius: track_radius,
195 start_angle: Radians(2.0 * PI * start - PI / 2.0),
196 end_angle: Radians(2.0 * PI * end - PI / 2.0),
197 });
198 frame.stroke(
199 &builder.build(),
200 canvas::Stroke::default()
201 .with_color(custom_style.bar_color)
202 .with_width(bar_height)
203 .with_line_cap(canvas::LineCap::Round),
204 );
205 };
206
207 if self.progress.is_some() {
208 if let Some(border_color) = custom_style.border_color {
209 for radius_offset in [bar_height / 2.0 - 0.5, -(bar_height / 2.0 - 0.5)] {
211 let border_path =
212 canvas::Path::circle(frame.center(), track_radius + radius_offset);
213 frame.stroke(
214 &border_path,
215 canvas::Stroke::default()
216 .with_color(border_color)
217 .with_width(1.0),
218 );
219 }
220 }
221 draw_bar(frame, 0.0, state.progress.current);
222 } else {
223 let (start, end) =
225 state
226 .animation
227 .bar_positions(self.cycle_duration, f32::EPSILON, MAX_WRAP);
228 draw_bar(frame, start, end);
229 }
230 });
231
232 renderer.with_translation(Vector::new(bounds.x, bounds.y), |renderer| {
233 use iced::advanced::graphics::geometry::Renderer as _;
234
235 renderer.draw_geometry(geometry);
236 });
237 }
238}
239
240impl<'a, Message, Theme> From<Circular<Theme>> for Element<'a, Message, Theme, Renderer>
241where
242 Message: Clone + 'a,
243 Theme: StyleSheet + 'a,
244{
245 fn from(circular: Circular<Theme>) -> Self {
246 Self::new(circular)
247 }
248}