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