1#![allow(non_snake_case)]
2
3use web_time::Duration;
4
5use repose_core::animation::{AnimationSpec, CubicBezier, Easing, KeyframesSpec, RepeatableSpec};
6use repose_core::*;
7use repose_ui::{
8 Box,
9};
10
11use super::*;
12
13#[derive(Clone, Debug)]
15pub struct CircularProgressIndicatorConfig {
16 pub modifier: Modifier,
17 pub color: Color,
18 pub track_color: Color,
19 pub stroke_width: f32,
20 pub stroke_cap: StrokeCap,
21 pub gap_size: f32,
22}
23
24impl Default for CircularProgressIndicatorConfig {
25 fn default() -> Self {
26 Self {
27 modifier: Modifier::new(),
28 color: ProgressIndicatorDefaults::circular_color(),
29 track_color: ProgressIndicatorDefaults::circular_track_color(),
30 stroke_width: ProgressIndicatorDefaults::CIRCULAR_STROKE_WIDTH,
31 stroke_cap: StrokeCap::Round,
32 gap_size: 0.0,
33 }
34 }
35}
36
37pub fn CircularProgressIndicator(
42 value: Option<f32>,
43 config: CircularProgressIndicatorConfig,
44) -> View {
45 let sz = dp_to_px(ProgressIndicatorDefaults::CIRCULAR_INDICATOR_SIZE);
46 let stroke_px = dp_to_px(config.stroke_width);
47 let val = value.map(|v| v.clamp(0.0, 1.0));
48
49 let (global_rotation, additional_rotation, sweep_val) = if value.is_none() {
54 let shared = remember_state_with_key("circ_ind_shared", || {
55 let mut a = AnimatedValue::new(
56 0.0f32,
57 AnimationSpec::tween(Duration::from_millis(6000), Easing::Linear)
58 .repeated(RepeatableSpec::infinite()),
59 );
60 a.set_target(1.0);
61 a
62 });
63 let mut s = shared.borrow_mut();
64 s.update();
65 let t = *s.get();
66 drop(s);
67
68 let gv = t * 1080.0;
69
70 let emph = Easing::Custom(CubicBezier::new(0.05, 0.7, 0.1, 1.0));
71 let add_kf = remember_state_with_key("circ_ind_add_kf", || KeyframesSpec {
72 keyframes: vec![
73 (0.0, 0.0, None),
74 (0.05, 90.0, Some(emph)),
75 (0.25, 90.0, None),
76 (0.30, 180.0, None),
77 (0.50, 180.0, None),
78 (0.55, 270.0, None),
79 (0.75, 270.0, None),
80 (0.80, 360.0, None),
81 (1.0, 360.0, None),
82 ],
83 });
84 let av = add_kf.borrow().evaluate(t);
85
86 let std_dec = Easing::Custom(CubicBezier::new(0.2, 0.0, 0.0, 1.0));
87 let sweep_kf = remember_state_with_key("circ_ind_sweep_kf", || KeyframesSpec {
88 keyframes: vec![
89 (0.0, 0.1, None),
90 (0.5, 0.87, Some(std_dec)),
91 (1.0, 0.1, None),
92 ],
93 });
94 let sv = sweep_kf.borrow().evaluate(t);
95
96 (gv, av, sv)
97 } else {
98 (0.0, 0.0, 0.0)
99 };
100
101 let indicator_size_dp = ProgressIndicatorDefaults::CIRCULAR_INDICATOR_SIZE;
103 let adjusted_gap_dp = if config.stroke_cap == StrokeCap::Butt {
104 config.gap_size
105 } else {
106 config.gap_size + config.stroke_width
107 };
108 let circle_dia_dp = indicator_size_dp - config.stroke_width;
109 let gap_sweep_rad = 2.0 * adjusted_gap_dp / circle_dia_dp;
110
111 Box(Modifier::new().size(sz, sz).then(config.modifier).painter(
112 move |scene: &mut Scene, rect: Rect, alpha: f32| {
113 let mul_c = |c: Color| {
114 Color(
115 c.0,
116 c.1,
117 c.2,
118 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
119 )
120 };
121 let cx = rect.x + rect.w * 0.5;
122 let cy = rect.y + rect.h * 0.5;
123 let r = (rect.w.min(rect.h)) * 0.5 - stroke_px * 0.5;
124 let circle = Rect {
125 x: cx - r,
126 y: cy - r,
127 w: r * 2.0,
128 h: r * 2.0,
129 };
130
131 match val {
132 Some(p) => {
133 let sweep_rad = p * std::f32::consts::TAU;
134 let start_angle = -std::f32::consts::FRAC_PI_2;
135 let effective_gap = gap_sweep_rad.min(sweep_rad);
136
137 if p > 0.0 {
139 scene.nodes.push(SceneNode::Arc {
140 rect: circle,
141 start_angle,
142 sweep_angle: sweep_rad,
143 stroke_width: stroke_px,
144 color: mul_c(config.color),
145 cap: config.stroke_cap,
146 });
147 }
148
149 let track_start = start_angle + sweep_rad + effective_gap;
151 let track_sweep = std::f32::consts::TAU - sweep_rad - 2.0 * effective_gap;
152 if track_sweep > 0.0 {
153 scene.nodes.push(SceneNode::Arc {
154 rect: circle,
155 start_angle: track_start,
156 sweep_angle: track_sweep,
157 stroke_width: stroke_px,
158 color: mul_c(config.track_color),
159 cap: config.stroke_cap,
160 });
161 }
162 }
163 None => {
164 let radians =
165 (global_rotation + additional_rotation) * std::f32::consts::PI / 180.0;
166 let start_angle = -std::f32::consts::FRAC_PI_2 + radians;
167 let sweep_rad = sweep_val * std::f32::consts::TAU;
168 let effective_gap = gap_sweep_rad.min(sweep_rad);
169
170 scene.nodes.push(SceneNode::Arc {
172 rect: circle,
173 start_angle,
174 sweep_angle: sweep_rad,
175 stroke_width: stroke_px,
176 color: mul_c(config.color),
177 cap: config.stroke_cap,
178 });
179
180 let track_start = start_angle + sweep_rad + effective_gap;
182 let track_sweep = std::f32::consts::TAU - sweep_rad - 2.0 * effective_gap;
183 if track_sweep > 0.0 {
184 scene.nodes.push(SceneNode::Arc {
185 rect: circle,
186 start_angle: track_start,
187 sweep_angle: track_sweep,
188 stroke_width: stroke_px,
189 color: mul_c(config.track_color),
190 cap: config.stroke_cap,
191 });
192 }
193 }
194 }
195 },
196 ))
197 .semantics(Semantics {
198 role: Role::ProgressBar,
199 label: None,
200 focused: false,
201 enabled: true,
202 selectable_group: false,
203 })
204}
205
206#[derive(Clone, Debug)]
208pub struct LinearProgressIndicatorConfig {
209 pub modifier: Modifier,
210 pub color: Color,
211 pub track_color: Color,
212 pub stroke_cap: StrokeCap,
214 pub gap_size: f32,
216 pub stop_size: f32,
218}
219
220impl Default for LinearProgressIndicatorConfig {
221 fn default() -> Self {
222 Self {
223 modifier: Modifier::new(),
224 color: ProgressIndicatorDefaults::linear_color(),
225 track_color: ProgressIndicatorDefaults::linear_track_color(),
226 stroke_cap: StrokeCap::Round,
227 gap_size: ProgressIndicatorDefaults::LINEAR_INDICATOR_GAP_SIZE,
228 stop_size: ProgressIndicatorDefaults::LINEAR_TRACK_STOP_SIZE,
229 }
230 }
231}
232
233pub fn LinearProgressIndicator(value: Option<f32>, config: LinearProgressIndicatorConfig) -> View {
238 Box(Modifier::new()
239 .fill_max_width()
240 .height(ProgressIndicatorDefaults::LINEAR_INDICATOR_HEIGHT)
241 .then(config.modifier)
242 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
243 let mul_c = |c: Color| {
244 Color(
245 c.0,
246 c.1,
247 c.2,
248 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
249 )
250 };
251 let track_h = rect.h;
252 let corner = track_h * 0.5;
253 let dot_r = dp_to_px(config.stop_size) * 0.5;
254 let cy = rect.y + rect.h * 0.5;
255 let t = value.unwrap_or(0.0).clamp(0.0, 1.0);
256
257 let cap_radius = if config.stroke_cap == StrokeCap::Butt {
258 0.0
259 } else {
260 corner
261 };
262
263 let gap = dp_to_px(config.gap_size)
264 - if config.stroke_cap == StrokeCap::Butt {
265 0.0
266 } else {
267 cap_radius
268 };
269
270 let cap_ofs = cap_radius;
271 let ind_end = (t * rect.w).clamp(cap_ofs, rect.w - cap_ofs);
272 let ind_w = (ind_end - cap_ofs).max(0.0);
273
274 if t > 0.0 && ind_w > 0.0 {
276 scene.nodes.push(SceneNode::Rect {
277 rect: Rect {
278 x: rect.x + cap_ofs,
279 y: cy - corner,
280 w: ind_w,
281 h: track_h,
282 },
283 brush: Brush::Solid(mul_c(config.color)),
284 radius: [cap_radius; 4],
285 });
286 }
287
288 let track_start = (rect.x + ind_end + gap).min(rect.x + rect.w);
290 let track_w = (rect.x + rect.w - track_start).max(0.0);
291 if t < 1.0 && track_w > 0.0 {
292 let track_left = track_start + cap_ofs;
293 let track_right = rect.x + rect.w;
294 if track_right > track_left {
295 scene.nodes.push(SceneNode::Rect {
296 rect: Rect {
297 x: track_left,
298 y: cy - corner,
299 w: track_right - track_left,
300 h: track_h,
301 },
302 brush: Brush::Solid(mul_c(config.track_color)),
303 radius: [cap_radius; 4],
304 });
305 }
306 }
307
308 {
310 let sx = rect.x + rect.w - dot_r;
311 scene.nodes.push(SceneNode::Ellipse {
312 rect: Rect {
313 x: sx - dot_r,
314 y: cy - dot_r,
315 w: dot_r * 2.0,
316 h: dot_r * 2.0,
317 },
318 brush: Brush::Solid(mul_c(config.color)),
319 });
320 }
321 }))
322 .semantics(Semantics {
323 role: Role::ProgressBar,
324 label: None,
325 focused: false,
326 enabled: true,
327 selectable_group: false,
328 })
329}