tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8
widgets/progress/src/lib.rs
Historia
Quién tocó qué
crudo
feat: llimphi standalone — framework UI soberano extraído del monorepo
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
13use llimphi_ui::llimphi_layout::taffy::{
14 prelude::{length, percent, FlexDirection, Position, Size, Style},
15 Rect,
16};
17use llimphi_ui::llimphi_raster::kurbo::{Affine, Arc, Cap, Stroke};
18use llimphi_ui::llimphi_raster::peniko::Color;
19use llimphi_ui::View;
20use llimphi_theme::radius;
21
22/// Barra horizontal: una pista (`track`) con un fill proporcional al
23/// `progress` (0.0..=1.0) pintado encima.
24pub fn linear_progress_view<Msg: Clone + 'static>(
25 progress: f32,
26 track_color: Color,
27 fill_color: Color,
28 height_px: f32,
29) -> View<Msg> {
30 let p = progress.clamp(0.0, 1.0);
31 let fill_radius = radius::XS;
32 let fill = View::new(Style {
33 position: Position::Absolute,
34 inset: Rect {
35 left: length(0.0_f32),
36 top: length(0.0_f32),
37 right: llimphi_ui::llimphi_layout::taffy::prelude::auto(),
38 bottom: length(0.0_f32),
39 },
40 size: Size {
41 width: percent(p),
42 height: percent(1.0_f32),
43 },
44 ..Default::default()
45 })
46 .fill(fill_color)
47 .radius(fill_radius)
48 .paint_with(move |scene, _ts, rect| {
49 // Gloss superior sobre la porción rellena — la barra deja de
50 // leerse como un rect plano y se siente como una luz que avanza.
51 // Mismo patrón que button/badge (P6).
52 use llimphi_ui::llimphi_raster::kurbo::{Affine, Point, RoundedRect};
53 use llimphi_ui::llimphi_raster::peniko::{Fill, Gradient};
54 if rect.w <= 0.0 || rect.h <= 0.0 {
55 return;
56 }
57 let x0 = rect.x as f64;
58 let y0 = rect.y as f64;
59 let x1 = (rect.x + rect.w) as f64;
60 let y1 = (rect.y + rect.h) as f64;
61 let y_mid = y0 + (y1 - y0) * 0.5;
62 let rr = RoundedRect::new(x0, y0, x1, y1, fill_radius);
63 let top = Color::from_rgba8(255, 255, 255, 50);
64 let bot = Color::from_rgba8(255, 255, 255, 0);
65 let g = Gradient::new_linear(Point::new(x0, y0), Point::new(x0, y_mid))
66 .with_stops([top, bot].as_slice());
67 scene.fill(Fill::NonZero, Affine::IDENTITY, &g, None, &rr);
68 });
69
70 View::new(Style {
71 flex_direction: FlexDirection::Row,
72 size: Size {
73 width: percent(1.0_f32),
74 height: length(height_px),
75 },
76 ..Default::default()
77 })
78 .fill(track_color)
79 .radius(radius::XS)
80 .children(vec![fill])
81}
82
83/// Anillo cuya porción angular llena indica el avance. Empieza desde
84/// arriba (12 en punto) y gira en sentido horario, igual que la
85/// convención de relojes y muchos progress radiales.
86pub fn radial_progress_view<Msg: Clone + 'static>(
87 progress: f32,
88 track_color: Color,
89 fill_color: Color,
90 stroke_width_ratio: f32,
91) -> View<Msg> {
92 let p = progress.clamp(0.0, 1.0);
93 let sw = stroke_width_ratio;
94 View::new(Style {
95 position: Position::Absolute,
96 size: Size {
97 width: percent(1.0_f32),
98 height: percent(1.0_f32),
99 },
100 ..Default::default()
101 })
102 .paint_with(move |scene, _ts, rect| {
103 let side = rect.w.min(rect.h) as f64;
104 if side <= 0.0 {
105 return;
106 }
107 let cx = rect.x as f64 + rect.w as f64 * 0.5;
108 let cy = rect.y as f64 + rect.h as f64 * 0.5;
109 let stroke_w = (side * sw as f64).max(1.0);
110 let radius = (side - stroke_w) * 0.5;
111 let stroke = Stroke::new(stroke_w).with_caps(Cap::Round);
112
113 // Track completo (anillo gris).
114 let track = Arc::new((cx, cy), (radius, radius), 0.0, std::f64::consts::TAU, 0.0);
115 scene.stroke(&stroke, Affine::IDENTITY, track_color, None, &track);
116
117 // Arco lleno — arranca en -π/2 (12 en punto) y barre `p * 2π`
118 // en sentido horario (positivo en el sistema y-down de vello).
119 if p > 0.0 {
120 let theta0 = -std::f64::consts::FRAC_PI_2;
121 let sweep = std::f64::consts::TAU * p as f64;
122 let fill_arc = Arc::new((cx, cy), (radius, radius), theta0, sweep, 0.0);
123 scene.stroke(&stroke, Affine::IDENTITY, fill_color, None, &fill_arc);
124 }
125 })
126}