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