1use iced::advanced::layout;
2use iced::advanced::renderer;
3use iced::advanced::widget::Tree;
4use iced::advanced::{Clipboard, Layout, Shell, Widget};
5use iced::border::Border;
6use iced::window;
7use iced::{Background, Color, Element, Event, Length, Rectangle, Shadow, Size};
8
9use crate::button::ButtonRadius;
10use crate::theme::Theme;
11use crate::tokens::{AccentColor, accent_color, accent_high, accent_low, is_dark};
12
13#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
14pub enum ProgressSize {
15 Size1,
16 #[default]
17 Size2,
18 Size3,
19}
20
21#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
22pub enum ProgressVariant {
23 Classic,
24 #[default]
25 Surface,
26 Soft,
27}
28
29#[derive(Clone, Copy, Debug)]
30pub struct ProgressProps {
31 pub size: ProgressSize,
32 pub variant: ProgressVariant,
33 pub color: AccentColor,
34 pub radius: Option<ButtonRadius>,
35 pub high_contrast: bool,
36 pub duration_ms: u32,
37 pub value: Option<f32>,
38 pub max: f32,
39}
40
41impl Default for ProgressProps {
42 fn default() -> Self {
43 Self {
44 size: ProgressSize::Size2,
45 variant: ProgressVariant::Surface,
46 color: AccentColor::Gray,
47 radius: None,
48 high_contrast: false,
49 duration_ms: 1200,
50 value: None,
51 max: 100.0,
52 }
53 }
54}
55
56impl ProgressProps {
57 pub fn new() -> Self {
58 Self::default()
59 }
60
61 pub fn size(mut self, size: ProgressSize) -> Self {
62 self.size = size;
63 self
64 }
65
66 pub fn variant(mut self, variant: ProgressVariant) -> Self {
67 self.variant = variant;
68 self
69 }
70
71 pub fn color(mut self, color: AccentColor) -> Self {
72 self.color = color;
73 self
74 }
75
76 pub fn radius(mut self, radius: ButtonRadius) -> Self {
77 self.radius = Some(radius);
78 self
79 }
80
81 pub fn high_contrast(mut self, high_contrast: bool) -> Self {
82 self.high_contrast = high_contrast;
83 self
84 }
85
86 pub fn duration_ms(mut self, duration_ms: u32) -> Self {
87 self.duration_ms = duration_ms.max(1);
88 self
89 }
90
91 pub fn value(mut self, value: f32) -> Self {
92 self.value = Some(value);
93 self
94 }
95
96 pub fn indeterminate(mut self) -> Self {
97 self.value = None;
98 self
99 }
100
101 pub fn max(mut self, max: f32) -> Self {
102 self.max = max.max(1.0);
103 self
104 }
105}
106
107fn apply_opacity(color: Color, opacity: f32) -> Color {
108 Color {
109 a: color.a * opacity,
110 ..color
111 }
112}
113
114impl ProgressSize {
115 fn height(self) -> f32 {
116 match self {
117 ProgressSize::Size1 => 4.0,
118 ProgressSize::Size2 => 8.0,
119 ProgressSize::Size3 => 12.0,
120 }
121 }
122}
123
124fn progress_radius(theme: &Theme, props: ProgressProps) -> f32 {
125 let height = props.size.height();
126 match props.radius {
127 Some(ButtonRadius::None) => 0.0,
128 Some(ButtonRadius::Small) => theme.radius.sm,
129 Some(ButtonRadius::Medium) => theme.radius.md,
130 Some(ButtonRadius::Large) => theme.radius.lg,
131 Some(ButtonRadius::Full) => (height / 2.0).max(9999.0),
132 None => (height / 3.0).max(theme.radius.sm),
133 }
134}
135
136#[derive(Debug, Default)]
137struct ProgressState {
138 start_time: Option<std::time::Instant>,
139 phase: f32,
140}
141
142pub fn progress(props: ProgressProps, theme: &Theme) -> ProgressWidget {
143 ProgressWidget::new(props, theme)
144}
145
146pub struct ProgressWidget {
147 props: ProgressProps,
148 theme: Theme,
149}
150
151impl ProgressWidget {
152 fn new(props: ProgressProps, theme: &Theme) -> Self {
153 Self {
154 props,
155 theme: theme.clone(),
156 }
157 }
158}
159
160impl<Message, AppTheme, Renderer> Widget<Message, AppTheme, Renderer> for ProgressWidget
161where
162 Renderer: renderer::Renderer,
163{
164 fn tag(&self) -> iced::advanced::widget::tree::Tag {
165 iced::advanced::widget::tree::Tag::of::<ProgressState>()
166 }
167
168 fn state(&self) -> iced::advanced::widget::tree::State {
169 iced::advanced::widget::tree::State::new(ProgressState::default())
170 }
171
172 fn size(&self) -> Size<Length> {
173 Size::new(Length::Fill, Length::Fixed(self.props.size.height()))
174 }
175
176 fn layout(
177 &mut self,
178 _tree: &mut Tree,
179 _renderer: &Renderer,
180 limits: &layout::Limits,
181 ) -> layout::Node {
182 layout::atomic(
183 limits,
184 Length::Fill,
185 Length::Fixed(self.props.size.height()),
186 )
187 }
188
189 fn update(
190 &mut self,
191 tree: &mut Tree,
192 event: &Event,
193 _layout: Layout<'_>,
194 _cursor: iced::mouse::Cursor,
195 _renderer: &Renderer,
196 _clipboard: &mut dyn Clipboard,
197 shell: &mut Shell<'_, Message>,
198 _viewport: &Rectangle,
199 ) {
200 if self.props.value.is_some() {
201 return;
202 }
203
204 if let Event::Window(window::Event::RedrawRequested(now)) = event {
205 let state = tree.state.downcast_mut::<ProgressState>();
206
207 if state.start_time.is_none() {
208 state.start_time = Some(*now);
209 }
210
211 if let Some(start) = state.start_time {
212 let elapsed = now.saturating_duration_since(start);
213 let duration = std::time::Duration::from_millis(self.props.duration_ms as u64);
214 state.phase = (elapsed.as_secs_f32() / duration.as_secs_f32()) % 1.0;
215 }
216
217 shell.request_redraw();
218 }
219 }
220
221 fn draw(
222 &self,
223 tree: &Tree,
224 renderer: &mut Renderer,
225 _theme: &AppTheme,
226 _style: &renderer::Style,
227 layout: Layout<'_>,
228 _cursor: iced::mouse::Cursor,
229 viewport: &Rectangle,
230 ) {
231 let bounds = layout.bounds();
232 if !bounds.intersects(viewport) {
233 return;
234 }
235
236 let palette = self.theme.palette;
237 let radius = progress_radius(&self.theme, self.props);
238
239 let base_bg = if is_dark(&palette) {
240 apply_opacity(accent_low(&palette, AccentColor::Gray), 0.9)
241 } else {
242 apply_opacity(accent_low(&palette, AccentColor::Gray), 0.7)
243 };
244
245 let (background, border, shadow) = match self.props.variant {
246 ProgressVariant::Surface => (
247 Background::Color(base_bg),
248 Border {
249 color: apply_opacity(palette.border, 0.7),
250 width: 1.0,
251 radius: radius.into(),
252 },
253 Shadow::default(),
254 ),
255 ProgressVariant::Classic => (
256 Background::Color(base_bg),
257 Border {
258 color: Color::TRANSPARENT,
259 width: 0.0,
260 radius: radius.into(),
261 },
262 Shadow {
263 color: apply_opacity(Color::BLACK, 0.10),
264 offset: iced::Vector::new(0.0, 1.0),
265 blur_radius: 8.0,
266 },
267 ),
268 ProgressVariant::Soft => (
269 Background::Color(apply_opacity(accent_low(&palette, AccentColor::Gray), 1.0)),
270 Border {
271 color: Color::TRANSPARENT,
272 width: 0.0,
273 radius: radius.into(),
274 },
275 Shadow::default(),
276 ),
277 };
278
279 renderer.fill_quad(
280 renderer::Quad {
281 bounds,
282 border,
283 shadow,
284 ..renderer::Quad::default()
285 },
286 background,
287 );
288
289 let indicator_color = if self.props.high_contrast {
290 accent_high(&palette, self.props.color)
291 } else {
292 accent_color(&palette, self.props.color)
293 };
294
295 let (indicator_x, indicator_width) = if let Some(value) = self.props.value {
296 let ratio = if self.props.max <= 0.0 {
297 0.0
298 } else {
299 (value / self.props.max).clamp(0.0, 1.0)
300 };
301 (bounds.x, bounds.width * ratio)
302 } else {
303 let state = tree.state.downcast_ref::<ProgressState>();
305 let bar_width = (bounds.width * 0.35).max(12.0);
306 let travel = bounds.width - bar_width;
307 let t = (state.phase * std::f32::consts::PI * 2.0).sin() * 0.5 + 0.5;
308 let x = bounds.x + travel * t;
309 (x, bar_width)
310 };
311
312 if indicator_width <= 0.0 {
313 return;
314 }
315
316 let indicator_bounds = Rectangle {
317 x: indicator_x,
318 y: bounds.y,
319 width: indicator_width,
320 height: bounds.height,
321 };
322
323 renderer.fill_quad(
324 renderer::Quad {
325 bounds: indicator_bounds,
326 border: Border {
327 color: Color::TRANSPARENT,
328 width: 0.0,
329 radius: radius.into(),
330 },
331 ..renderer::Quad::default()
332 },
333 Background::Color(indicator_color),
334 );
335 }
336}
337
338impl<'a, Message, AppTheme, Renderer> From<ProgressWidget>
339 for Element<'a, Message, AppTheme, Renderer>
340where
341 Renderer: renderer::Renderer + 'a,
342 Message: 'a,
343{
344 fn from(widget: ProgressWidget) -> Element<'a, Message, AppTheme, Renderer> {
345 Element::new(widget)
346 }
347}