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: 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: 40.0,
36 bar_height: 4.0,
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 {
52 self.bar_height = bar_height;
53 self
54 }
55
56 pub fn style(mut self, style: Theme::Style) -> Self {
58 self.style = style;
59 self
60 }
61
62 pub fn cycle_duration(mut self, duration: Duration) -> Self {
64 self.cycle_duration = duration / 2;
65 self
66 }
67
68 pub fn period(mut self, duration: Duration) -> Self {
71 self.period = duration;
72 self
73 }
74
75 pub fn progress(mut self, progress: f32) -> Self {
77 self.progress = Some(progress.clamp(0.0, 1.0));
78 self
79 }
80}
81
82impl<Theme> Default for Circular<Theme>
83where
84 Theme: StyleSheet,
85{
86 fn default() -> Self {
87 Self::new()
88 }
89}
90
91#[derive(Default)]
92struct State {
93 animation: Animation,
94 cache: canvas::Cache,
95 progress: Progress,
96}
97
98impl<Message, Theme> Widget<Message, Theme, Renderer> for Circular<Theme>
99where
100 Message: Clone,
101 Theme: StyleSheet,
102{
103 fn tag(&self) -> tree::Tag {
104 tree::Tag::of::<State>()
105 }
106
107 fn state(&self) -> tree::State {
108 tree::State::new(State::default())
109 }
110
111 fn size(&self) -> Size<Length> {
112 Size {
113 width: Length::Fixed(self.size),
114 height: Length::Fixed(self.size),
115 }
116 }
117
118 fn layout(
119 &mut self,
120 _tree: &mut Tree,
121 _renderer: &Renderer,
122 limits: &layout::Limits,
123 ) -> layout::Node {
124 layout::atomic(limits, self.size, self.size)
125 }
126
127 fn update(
128 &mut self,
129 tree: &mut Tree,
130 event: &Event,
131 _layout: Layout<'_>,
132 _cursor: mouse::Cursor,
133 _renderer: &Renderer,
134 _clipboard: &mut dyn Clipboard,
135 shell: &mut Shell<'_, Message>,
136 _viewport: &Rectangle,
137 ) {
138 let state = tree.state.downcast_mut::<State>();
139 if let Event::Window(window::Event::RedrawRequested(now)) = event {
140 if let Some(target) = self.progress {
141 if state.progress.update(target, *now) {
142 state.cache.clear();
143 shell.request_redraw();
144 }
145 } else {
146 state.animation = state.animation.timed_transition(
147 self.cycle_duration,
148 self.period,
149 MAX_WRAP,
150 *now,
151 );
152 state.cache.clear();
153 shell.request_redraw();
154 }
155 }
156 }
157
158 fn draw(
159 &self,
160 tree: &Tree,
161 renderer: &mut Renderer,
162 theme: &Theme,
163 _style: &renderer::Style,
164 layout: Layout<'_>,
165 _cursor: mouse::Cursor,
166 _viewport: &Rectangle,
167 ) {
168 use advanced::Renderer as _;
169
170 let state = tree.state.downcast_ref::<State>();
171 let bounds = layout.bounds();
172 let custom_style = Theme::appearance(theme, &self.style, self.progress.is_some(), true);
173
174 let geometry = state.cache.draw(renderer, bounds.size(), |frame| {
175 let track_radius = frame.width() / 2.0 - self.bar_height;
176 if track_radius <= 0.0 {
177 return;
178 }
179 let track_path = canvas::Path::circle(frame.center(), track_radius);
180
181 frame.stroke(
182 &track_path,
183 canvas::Stroke::default()
184 .with_color(custom_style.track_color)
185 .with_width(self.bar_height),
186 );
187
188 let to_angle = |t: f32| t * 2.0 * PI - PI / 2.0;
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(to_angle(start)),
196 end_angle: Radians(to_angle(end)),
197 });
198 frame.stroke(
199 &builder.build(),
200 canvas::Stroke::default()
201 .with_color(custom_style.bar_color)
202 .with_width(self.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 [self.bar_height / 2.0, -(self.bar_height / 2.0)] {
210 let border_path =
211 canvas::Path::circle(frame.center(), track_radius + radius_offset);
212 frame.stroke(
213 &border_path,
214 canvas::Stroke::default()
215 .with_color(border_color)
216 .with_width(1.0),
217 );
218 }
219 }
220 draw_bar(frame, 0.0, state.progress.current);
221 } else {
222 let (start, end) =
224 state
225 .animation
226 .bar_positions(self.cycle_duration, f32::EPSILON, MAX_WRAP);
227 draw_bar(frame, start, end);
228 }
229 });
230
231 renderer.with_translation(Vector::new(bounds.x, bounds.y), |renderer| {
232 use iced::advanced::graphics::geometry::Renderer as _;
233
234 renderer.draw_geometry(geometry);
235 });
236 }
237}
238
239impl<'a, Message, Theme> From<Circular<Theme>> for Element<'a, Message, Theme, Renderer>
240where
241 Message: Clone + 'a,
242 Theme: StyleSheet + 'a,
243{
244 fn from(circular: Circular<Theme>) -> Self {
245 Self::new(circular)
246 }
247}