tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
1//! `llimphi-widget-progress` — progreso determinado, lineal o radial.
2//!
3//! Determinado = la app conoce el porcentaje (`0.0..=1.0`). Para
4//! progreso indeterminado (la op está corriendo, no sé cuánto falta),
5//! usar `llimphi-widget-spinner`.
6//!
7//! Dos formas:
8//! - [`linear_progress_view`] — barra horizontal con relleno proporcional.
9//! - [`radial_progress_view`] — anillo cuya porción llena indica el avance.
10
11#![forbid(unsafe_code)]
12
13
14// Sin `std`: este widget no usa nada del sistema —sólo `alloc` para
15// `String`/`Vec`/`Arc`—, y declararlo es lo que le permite cruzar a una
16// jaula WASM o al kernel. Antes enlazaba `std` por omisión (W4.6).
17#![no_std]
18
19extern crate alloc;
20
21use alloc::vec;
22use alloc::format;
23use llimphi_compositor::llimphi_layout::taffy::{
24 prelude::{length, percent, FlexDirection, Position, Size, Style},
25 Rect,
26};
27use llimphi_compositor::peniko::kurbo::{Affine, Arc, Cap, Stroke};
28use llimphi_compositor::peniko::Color;
29use llimphi_compositor::View;
30use llimphi_theme::radius;
31
32/// Barra horizontal: una pista (`track`) con un fill proporcional al
33/// `progress` (0.0..=1.0) pintado encima.
34pub fn linear_progress_view<Msg: Clone + 'static>(
35 progress: f32,
36 track_color: Color,
37 fill_color: Color,
38 height_px: f32,
39) -> View<Msg> {
40 let p = progress.clamp(0.0, 1.0);
41 let fill_radius = radius::XS;
42 let fill = View::new(Style {
43 position: Position::Absolute,
44 inset: Rect {
45 left: length(0.0_f32),
46 top: length(0.0_f32),
47 right: llimphi_compositor::llimphi_layout::taffy::prelude::auto(),
48 bottom: length(0.0_f32),
49 },
50 size: Size {
51 width: percent(p),
52 height: percent(1.0_f32),
53 },
54 ..Default::default()
55 })
56 .fill(fill_color)
57 .radius(fill_radius)
58 .paint_with(move |scene, _ts, rect| {
59 // Gloss superior sobre la porción rellena — la barra deja de
60 // leerse como un rect plano y se siente como una luz que avanza.
61 // Mismo patrón que button/badge (P6).
62 use llimphi_compositor::peniko::kurbo::{Affine, Point, RoundedRect};
63 use llimphi_compositor::peniko::{Fill, Gradient};
64 if rect.w <= 0.0 || rect.h <= 0.0 {
65 return;
66 }
67 let x0 = rect.x as f64;
68 let y0 = rect.y as f64;
69 let x1 = (rect.x + rect.w) as f64;
70 let y1 = (rect.y + rect.h) as f64;
71 let y_mid = y0 + (y1 - y0) * 0.5;
72 let rr = RoundedRect::new(x0, y0, x1, y1, fill_radius);
73 let top = Color::from_rgba8(255, 255, 255, 50);
74 let bot = Color::from_rgba8(255, 255, 255, 0);
75 let g = Gradient::new_linear(Point::new(x0, y0), Point::new(x0, y_mid))
76 .with_stops([top, bot].as_slice());
77 scene.fill(Fill::NonZero, Affine::IDENTITY, &g, None, &rr);
78 });
79
80 View::new(Style {
81 flex_direction: FlexDirection::Row,
82 size: Size {
83 width: percent(1.0_f32),
84 height: length(height_px),
85 },
86 ..Default::default()
87 })
88 .fill(track_color)
89 .radius(radius::XS)
90 // Sin esto la barra es una caja de color: el lector no tiene forma de
91 // enunciar el avance. El `value` es el porcentaje ya redondeado.
92 .role(llimphi_compositor::Role::ProgressBar)
93 .aria_value(format!("{}%", (p * 100.0 + 0.5) as i32))
94 .children(vec![fill])
95}
96
97/// Anillo cuya porción angular llena indica el avance. Empieza desde
98/// arriba (12 en punto) y gira en sentido horario, igual que la
99/// convención de relojes y muchos progress radiales.
100pub fn radial_progress_view<Msg: Clone + 'static>(
101 progress: f32,
102 track_color: Color,
103 fill_color: Color,
104 stroke_width_ratio: f32,
105) -> View<Msg> {
106 let p = progress.clamp(0.0, 1.0);
107 let sw = stroke_width_ratio;
108 View::new(Style {
109 position: Position::Absolute,
110 size: Size {
111 width: percent(1.0_f32),
112 height: percent(1.0_f32),
113 },
114 ..Default::default()
115 })
116 .paint_with(move |scene, _ts, rect| {
117 let side = rect.w.min(rect.h) as f64;
118 if side <= 0.0 {
119 return;
120 }
121 let cx = rect.x as f64 + rect.w as f64 * 0.5;
122 let cy = rect.y as f64 + rect.h as f64 * 0.5;
123 let stroke_w = (side * sw as f64).max(1.0);
124 let radius = (side - stroke_w) * 0.5;
125 let stroke = Stroke::new(stroke_w).with_caps(Cap::Round);
126
127 // Track completo (anillo gris).
128 let track = Arc::new((cx, cy), (radius, radius), 0.0, core::f64::consts::TAU, 0.0);
129 scene.stroke(&stroke, Affine::IDENTITY, track_color, None, &track);
130
131 // Arco lleno — arranca en -π/2 (12 en punto) y barre `p * 2π`
132 // en sentido horario (positivo en el sistema y-down de vello).
133 if p > 0.0 {
134 let theta0 = -core::f64::consts::FRAC_PI_2;
135 let sweep = core::f64::consts::TAU * p as f64;
136 let fill_arc = Arc::new((cx, cy), (radius, radius), theta0, sweep, 0.0);
137 scene.stroke(&stroke, Affine::IDENTITY, fill_color, None, &fill_arc);
138 }
139 })
140 // El anillo se pinta entero en un `paint_with`: sin semántica declarada
141 // sería un nodo vacío para el lector.
142 .role(llimphi_compositor::Role::ProgressBar)
143 .aria_value(format!("{}%", (p * 100.0 + 0.5) as i32))
144}