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 label: None,
198 focused: false,
199 enabled: true,
200 selectable_group: false,
201 })
202}
203
204#[derive(Clone, Debug)]
206pub struct LinearProgressIndicatorConfig {
207 pub modifier: Modifier,
208 pub color: Color,
209 pub track_color: Color,
210 pub stroke_cap: StrokeCap,
212 pub gap_size: f32,
214 pub stop_size: f32,
216}
217
218impl Default for LinearProgressIndicatorConfig {
219 fn default() -> Self {
220 Self {
221 modifier: Modifier::new(),
222 color: ProgressIndicatorDefaults::linear_color(),
223 track_color: ProgressIndicatorDefaults::linear_track_color(),
224 stroke_cap: StrokeCap::Round,
225 gap_size: ProgressIndicatorDefaults::LINEAR_INDICATOR_GAP_SIZE,
226 stop_size: ProgressIndicatorDefaults::LINEAR_TRACK_STOP_SIZE,
227 }
228 }
229}
230
231pub fn LinearProgressIndicator(value: Option<f32>, config: LinearProgressIndicatorConfig) -> View {
236 Box(Modifier::new()
237 .fill_max_width()
238 .height(ProgressIndicatorDefaults::LINEAR_INDICATOR_HEIGHT)
239 .then(config.modifier)
240 .painter(move |scene: &mut Scene, rect: Rect, alpha: f32| {
241 let mul_c = |c: Color| {
242 Color(
243 c.0,
244 c.1,
245 c.2,
246 ((c.3 as f32) * alpha).clamp(0.0, 255.0) as u8,
247 )
248 };
249 let track_h = rect.h;
250 let corner = track_h * 0.5;
251 let dot_r = dp_to_px(config.stop_size) * 0.5;
252 let cy = rect.y + rect.h * 0.5;
253 let t = value.unwrap_or(0.0).clamp(0.0, 1.0);
254
255 let cap_radius = if config.stroke_cap == StrokeCap::Butt {
256 0.0
257 } else {
258 corner
259 };
260
261 let gap = dp_to_px(config.gap_size)
262 - if config.stroke_cap == StrokeCap::Butt {
263 0.0
264 } else {
265 cap_radius
266 };
267
268 let cap_ofs = cap_radius;
269 let ind_end = (t * rect.w).clamp(cap_ofs, rect.w - cap_ofs);
270 let ind_w = (ind_end - cap_ofs).max(0.0);
271
272 if t > 0.0 && ind_w > 0.0 {
274 scene.nodes.push(SceneNode::Rect {
275 rect: Rect {
276 x: rect.x + cap_ofs,
277 y: cy - corner,
278 w: ind_w,
279 h: track_h,
280 },
281 brush: Brush::Solid(mul_c(config.color)),
282 radius: [cap_radius; 4],
283 });
284 }
285
286 let track_start = (rect.x + ind_end + gap).min(rect.x + rect.w);
288 let track_w = (rect.x + rect.w - track_start).max(0.0);
289 if t < 1.0 && track_w > 0.0 {
290 let track_left = track_start + cap_ofs;
291 let track_right = rect.x + rect.w;
292 if track_right > track_left {
293 scene.nodes.push(SceneNode::Rect {
294 rect: Rect {
295 x: track_left,
296 y: cy - corner,
297 w: track_right - track_left,
298 h: track_h,
299 },
300 brush: Brush::Solid(mul_c(config.track_color)),
301 radius: [cap_radius; 4],
302 });
303 }
304 }
305
306 {
308 let sx = rect.x + rect.w - dot_r;
309 scene.nodes.push(SceneNode::Ellipse {
310 rect: Rect {
311 x: sx - dot_r,
312 y: cy - dot_r,
313 w: dot_r * 2.0,
314 h: dot_r * 2.0,
315 },
316 brush: Brush::Solid(mul_c(config.color)),
317 });
318 }
319 }))
320 .semantics(Semantics {
321 role: Role::ProgressBar,
322 label: None,
323 focused: false,
324 enabled: true,
325 selectable_group: false,
326 })
327}