cosmic/widget/progress_bar/
circular.rs1use super::animation::{Animation, Progress};
3use super::style::{self, Catalog};
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: Catalog,
19{
20 size: f32,
21 bar_height: Option<f32>,
22 class: Theme::Class,
23 cycle_duration: Duration,
24 period: Duration,
25 progress: Option<f32>,
26}
27
28impl<Theme> Circular<Theme>
29where
30 Theme: Catalog<Class = style::Class>,
31{
32 pub fn new() -> Self {
34 Circular {
35 size: 48.0,
36 bar_height: None,
37 class: Theme::Class::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 class(mut self, class: Theme::Class) -> Self {
59 self.class = class;
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 pub fn track_color(mut self, color: impl Into<iced::Color>) -> Self {
84 self.class = self.class.track_color(color);
85 self
86 }
87
88 pub fn bar_color(mut self, color: impl Into<iced::Color>) -> Self {
90 self.class = self.class.bar_color(color);
91 self
92 }
93
94 pub fn border_color(mut self, color: impl Into<iced::Color>) -> Self {
96 self.class = self.class.border_color(color);
97 self
98 }
99}
100
101impl<Theme> Default for Circular<Theme>
102where
103 Theme: Catalog<Class = style::Class>,
104{
105 fn default() -> Self {
106 Self::new()
107 }
108}
109
110#[derive(Default)]
111struct State {
112 animation: Animation,
113 cache: canvas::Cache,
114 progress: Progress,
115}
116
117impl<Message, Theme> Widget<Message, Theme, Renderer> for Circular<Theme>
118where
119 Message: Clone,
120 Theme: Catalog,
121{
122 fn tag(&self) -> tree::Tag {
123 tree::Tag::of::<State>()
124 }
125
126 fn state(&self) -> tree::State {
127 tree::State::new(State::default())
128 }
129
130 fn size(&self) -> Size<Length> {
131 Size {
132 width: Length::Fixed(self.size),
133 height: Length::Fixed(self.size),
134 }
135 }
136
137 fn layout(
138 &mut self,
139 _tree: &mut Tree,
140 _renderer: &Renderer,
141 limits: &layout::Limits,
142 ) -> layout::Node {
143 layout::atomic(limits, self.size, self.size)
144 }
145
146 fn update(
147 &mut self,
148 tree: &mut Tree,
149 event: &Event,
150 _layout: Layout<'_>,
151 _cursor: mouse::Cursor,
152 _renderer: &Renderer,
153 _clipboard: &mut dyn Clipboard,
154 shell: &mut Shell<'_, Message>,
155 _viewport: &Rectangle,
156 ) {
157 let state = tree.state.downcast_mut::<State>();
158 if let Event::Window(window::Event::RedrawRequested(now)) = event {
159 if let Some(target) = self.progress {
160 if state.progress.update(target, *now) {
161 state.cache.clear();
162 shell.request_redraw();
163 }
164 } else {
165 state.animation = state.animation.timed_transition(
166 self.cycle_duration,
167 self.period,
168 MAX_WRAP,
169 *now,
170 );
171 state.cache.clear();
172 shell.request_redraw();
173 }
174 }
175 }
176
177 fn draw(
178 &self,
179 tree: &Tree,
180 renderer: &mut Renderer,
181 theme: &Theme,
182 _style: &renderer::Style,
183 layout: Layout<'_>,
184 _cursor: mouse::Cursor,
185 _viewport: &Rectangle,
186 ) {
187 use advanced::Renderer as _;
188
189 let state = tree.state.downcast_ref::<State>();
190 let bounds = layout.bounds();
191 let custom_style = theme.style(&self.class, self.progress.is_some(), true);
192
193 let geometry = state.cache.draw(renderer, bounds.size(), |frame| {
194 let bar_height = self.bar_height.unwrap_or((frame.width() / 12.0).max(2.0));
195 let track_radius = (frame.width() - bar_height) / 2.0;
196 if track_radius <= 0.0 {
197 return;
198 }
199
200 let track_path = canvas::Path::circle(frame.center(), track_radius);
201 frame.stroke(
202 &track_path,
203 canvas::Stroke::default()
204 .with_color(custom_style.track_color)
205 .with_width(bar_height),
206 );
207
208 let draw_bar = |frame: &mut canvas::Frame, start: f32, end: f32| {
209 let mut builder = canvas::path::Builder::new();
210 builder.arc(canvas::path::Arc {
211 center: frame.center(),
212 radius: track_radius,
213 start_angle: Radians(2.0 * PI * start - PI / 2.0),
214 end_angle: Radians(2.0 * PI * end - PI / 2.0),
215 });
216 frame.stroke(
217 &builder.build(),
218 canvas::Stroke::default()
219 .with_color(custom_style.bar_color)
220 .with_width(bar_height)
221 .with_line_cap(canvas::LineCap::Round),
222 );
223 };
224
225 if self.progress.is_some() {
226 if let Some(border_color) = custom_style.border_color {
227 for radius_offset in [bar_height / 2.0 - 0.5, -(bar_height / 2.0 - 0.5)] {
229 let border_path =
230 canvas::Path::circle(frame.center(), track_radius + radius_offset);
231 frame.stroke(
232 &border_path,
233 canvas::Stroke::default()
234 .with_color(border_color)
235 .with_width(1.0),
236 );
237 }
238 }
239 draw_bar(frame, 0.0, state.progress.current);
240 } else {
241 let (start, end) =
243 state
244 .animation
245 .bar_positions(self.cycle_duration, f32::EPSILON, MAX_WRAP);
246 draw_bar(frame, start, end);
247 }
248 });
249
250 renderer.with_translation(Vector::new(bounds.x, bounds.y), |renderer| {
251 use iced::advanced::graphics::geometry::Renderer as _;
252
253 renderer.draw_geometry(geometry);
254 });
255 }
256}
257
258impl<'a, Message, Theme> From<Circular<Theme>> for Element<'a, Message, Theme, Renderer>
259where
260 Message: Clone + 'a,
261 Theme: Catalog + 'a,
262{
263 fn from(circular: Circular<Theme>) -> Self {
264 Self::new(circular)
265 }
266}