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-gauge` — medidor radial.
2//!
3//! Arco de 270° (de 7:30 a 4:30, manecillas de reloj) con un track
4//! gris fino y un arco activo del color `accent` que crece con el
5//! valor. En el centro, una etiqueta opcional con el valor formateado.
6//! Pensado para dashboards (CPU, RAM, throughput) y métricas con
7//! contexto (lleno / vacío / objetivo).
8
9#![forbid(unsafe_code)]
10
11
12// Sin `std`: este widget no usa nada del sistema —sólo `alloc` para
13// `String`/`Vec`—, y declararlo es lo que le permite cruzar a una jaula
14// WASM o al kernel. Antes enlazaba `std` por omisión (W4.6).
15#![no_std]
16
17extern crate alloc;
18
19use alloc::string::String;
20use alloc::format;
21use llimphi_compositor::llimphi_layout::taffy::{
22 prelude::{length, Size, Style},
23 AlignItems, JustifyContent,
24};
25use llimphi_compositor::peniko::Color;
26use llimphi_compositor::llimphi_text::Alignment;
27use llimphi_compositor::View;
28use llimphi_theme::Theme;
29
30#[derive(Debug, Clone, Copy)]
31pub struct GaugePalette {
32 pub track: Color,
33 pub active: Color,
34 pub fg: Color,
35}
36
37impl GaugePalette {
38 pub fn from_theme(t: &Theme) -> Self {
39 Self {
40 track: t.border,
41 active: t.accent,
42 fg: t.fg_text,
43 }
44 }
45}
46
47/// Render del gauge. `value` es la fracción 0..=1 que representa la
48/// progresión del arco. `label` es texto centrado opcional.
49pub fn gauge_view<Msg: Clone + 'static>(
50 value: f32,
51 size_px: f32,
52 label: Option<String>,
53 palette: &GaugePalette,
54) -> View<Msg> {
55 let v = value.clamp(0.0, 1.0);
56 let track = palette.track;
57 let active = palette.active;
58 let mut node = View::new(Style {
59 size: Size { width: length(size_px), height: length(size_px) },
60 align_items: Some(AlignItems::CENTER),
61 justify_content: Some(JustifyContent::CENTER),
62 ..Default::default()
63 })
64 .paint_with(move |scene, _ts, rect| {
65 paint_gauge(scene, rect, v, track, active);
66 })
67 // El arco lo pinta un `paint_with`: sin esto es un nodo vacío para el
68 // lector. El label visible (si lo hay) suele ser el valor formateado por
69 // el caller, pero el porcentaje crudo va igual en `value`.
70 .role(llimphi_compositor::Role::ProgressBar)
71 .aria_value(format!("{}%", (v * 100.0).round() as i32));
72 if let Some(lbl) = label {
73 node = node
74 .aria_label(lbl.clone())
75 .text_aligned(lbl, (size_px * 0.22).max(11.0), palette.fg, Alignment::Center);
76 }
77 node
78}
79
80fn paint_gauge(
81 scene: &mut llimphi_compositor::Lienzo,
82 rect: llimphi_compositor::PaintRect,
83 value: f32,
84 track: Color,
85 active: Color,
86) {
87 use llimphi_compositor::peniko::kurbo::{Affine, Arc, Point, Stroke};
88 if rect.w <= 0.0 || rect.h <= 0.0 {
89 return;
90 }
91 let cx = (rect.x + rect.w * 0.5) as f64;
92 let cy = (rect.y + rect.h * 0.5) as f64;
93 let s = rect.w.min(rect.h) as f64;
94 let stroke_w = (s * 0.10).max(2.0);
95 let r = (s * 0.5) - stroke_w * 0.6;
96
97 // Arco completo (270° = 1.5π), empieza en 135° (7:30) y avanza
98 // sentido horario. En kurbo, `start_angle` está en radianes; CCW
99 // positivo. Tomamos start = 135° (back of bottom-left), sweep
100 // negativo para ir CW.
101 let start_deg = 135.0_f64;
102 let total_sweep_deg = -270.0_f64; // CW
103 let active_sweep_deg = total_sweep_deg * (value as f64);
104
105 let center = Point::new(cx, cy);
106 let radii = (r, r);
107
108 let track_arc = Arc {
109 center,
110 radii: radii.into(),
111 start_angle: start_deg.to_radians(),
112 sweep_angle: total_sweep_deg.to_radians(),
113 x_rotation: 0.0,
114 };
115 let stroke = Stroke::new(stroke_w).with_caps(llimphi_compositor::peniko::kurbo::Cap::Round);
116 scene.stroke(&stroke, Affine::IDENTITY, track, None, &track_arc);
117
118 if value > 0.001 {
119 let active_arc = Arc {
120 center,
121 radii: radii.into(),
122 start_angle: start_deg.to_radians(),
123 sweep_angle: active_sweep_deg.to_radians(),
124 x_rotation: 0.0,
125 };
126 scene.stroke(&stroke, Affine::IDENTITY, active, None, &active_arc);
127 }
128}