⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc
crates/modules/ui_engine/widgets/lapaloma-render/src/gpui_backend.rs
Historia
Quién tocó qué
crudo
feat(lapaloma-cartesian): ejes con ticks y labels decimadas
1//! Backend CPU del trait [`crate::Canvas`] sobre `gpui::Window`.
2//!
3//! Bajo el feature `gpui`. Traduce los primitivos de Lapaloma a las
4//! llamadas nativas de GPUI 0.2 (`paint_quad`, `paint_path`). No
5//! introduce dependencia transitiva a gpui en los crates de
6//! visualización — éstos siguen hablando contra el trait abstracto;
7//! sólo el `Element` GPUI de cada widget importa este módulo.
8//!
9//! Limitaciones de la implementación CPU:
10//! - `push_clip` / `pop_clip` quedan como no-op por ahora — GPUI
11//! maneja content mask via builders de alto nivel; el chart se
12//! apoya en el bounds del Element para no pintar fuera.
13//! - `fill_triangle_strip` no implementado (lo necesitan phosphor
14//! y Sankey, que aún no están).
15//! - `draw_text` no implementado (axis labels lo necesitan; va con
16//! `WindowTextSystem` en una fase próxima).
17
18use crate::{Canvas, Color, Point, Rect, StrokeStyle};
19use gpui::{
20 fill, font, hsla, point as gpui_point, px, size as gpui_size, Bounds, Hsla, PathBuilder,
21 SharedString, TextRun, Window,
22};
23
24/// Adapter que pinta sobre un `&mut Window` de GPUI.
25///
26/// Vida útil del borrow del window iguala la de la pintura. Construir
27/// uno nuevo en cada `paint()` del Element.
28pub struct WindowCanvas<'a> {
29 window: &'a mut Window,
30}
31
32impl<'a> WindowCanvas<'a> {
33 pub fn new(window: &'a mut Window) -> Self {
34 Self { window }
35 }
36}
37
38/// Conversión RGB(a) → HSL(a). GPUI consume `Hsla` para casi todo
39/// el path. Linear, sin gamma — coincide con la convención del
40/// resto del codebase yahweh.
41pub(crate) fn color_to_hsla(c: Color) -> Hsla {
42 let (r, g, b, a) = (c.r, c.g, c.b, c.a);
43 let max = r.max(g).max(b);
44 let min = r.min(g).min(b);
45 let l = (max + min) * 0.5;
46 let delta = max - min;
47 if delta.abs() < 1e-6 {
48 return hsla(0.0, 0.0, l, a);
49 }
50 let s = if l < 0.5 { delta / (max + min) } else { delta / (2.0 - max - min) };
51 let h = if max == r {
52 ((g - b) / delta).rem_euclid(6.0)
53 } else if max == g {
54 (b - r) / delta + 2.0
55 } else {
56 (r - g) / delta + 4.0
57 };
58 hsla(h / 6.0, s, l, a)
59}
60
61fn to_bounds(r: Rect) -> Bounds<gpui::Pixels> {
62 Bounds {
63 origin: gpui_point(px(r.x), px(r.y)),
64 size: gpui_size(px(r.w), px(r.h)),
65 }
66}
67
68impl<'a> Canvas for WindowCanvas<'a> {
69 fn push_clip(&mut self, _rect: Rect) {
70 // Sin clip explícito por ahora. El Element pinta dentro
71 // de sus bounds y los painters de lapaloma respetan el
72 // plot_rect en sus proyecciones.
73 }
74 fn pop_clip(&mut self) {}
75
76 fn fill_rect(&mut self, rect: Rect, color: Color) {
77 let hsla = color_to_hsla(color);
78 self.window.paint_quad(fill(to_bounds(rect), hsla));
79 }
80
81 fn stroke_rect(&mut self, rect: Rect, stroke: StrokeStyle) {
82 // 4 line segments con PathBuilder en stroke mode.
83 let mut pb = PathBuilder::stroke(px(stroke.width));
84 pb.move_to(gpui_point(px(rect.x), px(rect.y)));
85 pb.line_to(gpui_point(px(rect.right()), px(rect.y)));
86 pb.line_to(gpui_point(px(rect.right()), px(rect.bottom())));
87 pb.line_to(gpui_point(px(rect.x), px(rect.bottom())));
88 pb.close();
89 if let Ok(path) = pb.build() {
90 self.window.paint_path(path, color_to_hsla(stroke.color));
91 }
92 }
93
94 fn stroke_line(&mut self, a: Point, b: Point, stroke: StrokeStyle) {
95 let mut pb = PathBuilder::stroke(px(stroke.width));
96 pb.move_to(gpui_point(px(a.x), px(a.y)));
97 pb.line_to(gpui_point(px(b.x), px(b.y)));
98 if let Ok(path) = pb.build() {
99 self.window.paint_path(path, color_to_hsla(stroke.color));
100 }
101 }
102
103 fn stroke_polyline(&mut self, coords: &[f32], stroke: StrokeStyle) {
104 if coords.len() < 4 {
105 return; // <2 puntos → no hay segmento
106 }
107 let mut pb = PathBuilder::stroke(px(stroke.width));
108 pb.move_to(gpui_point(px(coords[0]), px(coords[1])));
109 let mut i = 2;
110 while i + 1 < coords.len() {
111 pb.line_to(gpui_point(px(coords[i]), px(coords[i + 1])));
112 i += 2;
113 }
114 if let Ok(path) = pb.build() {
115 self.window.paint_path(path, color_to_hsla(stroke.color));
116 }
117 }
118
119 fn fill_triangle_strip(&mut self, _coords: &[f32], _colors: &[Color]) {
120 // TODO: cuando phosphor / Sankey lo necesiten. GPUI no
121 // tiene API directa para triangle strips con per-vertex
122 // color — habrá que descomponer en quads o subir un
123 // vertex buffer wgpu.
124 }
125
126 fn draw_text(&mut self, p: Point, text: &str, color: Color, size_px: f32) {
127 if text.is_empty() {
128 return;
129 }
130 let hsla = color_to_hsla(color);
131 let font_size = px(size_px);
132 let text_str: SharedString = text.to_string().into();
133 let runs = [TextRun {
134 len: text.len(),
135 font: font("Monospace"),
136 color: hsla,
137 background_color: None,
138 underline: None,
139 strikethrough: None,
140 }];
141
142 let shaped = self
143 .window
144 .text_system()
145 .shape_line(text_str, font_size, &runs, None);
146
147 // Iteramos glyphs vía `paint_glyph` para evitar la
148 // dependencia con `&mut App` que pide `ShapedLine::paint`.
149 // Eso encaja con el contrato actual del Canvas trait que
150 // sólo expone `&mut Window`.
151 let origin_x = px(p.x);
152 let origin_y = px(p.y);
153 for run in shaped.runs.iter() {
154 for glyph in run.glyphs.iter() {
155 let gx = origin_x + glyph.position.x;
156 let gy = origin_y + glyph.position.y;
157 let _ = self
158 .window
159 .paint_glyph(gpui_point(gx, gy), run.font_id, glyph.id, font_size, hsla);
160 }
161 }
162 }
163}
164
165#[cfg(test)]
166mod tests {
167 use super::*;
168
169 #[test]
170 fn rgb_a_hsla_grises() {
171 // (0.5, 0.5, 0.5) → h=0, s=0, l=0.5
172 let h = color_to_hsla(Color::rgb(0.5, 0.5, 0.5));
173 assert!((h.s - 0.0).abs() < 1e-6);
174 assert!((h.l - 0.5).abs() < 1e-6);
175 }
176
177 #[test]
178 fn rgb_a_hsla_rojo_puro() {
179 let h = color_to_hsla(Color::rgb(1.0, 0.0, 0.0));
180 // Rojo: h=0, s=1, l=0.5
181 assert!((h.h - 0.0).abs() < 1e-6);
182 assert!((h.s - 1.0).abs() < 1e-6);
183 assert!((h.l - 0.5).abs() < 1e-6);
184 }
185
186 #[test]
187 fn rgb_a_hsla_alpha_pasa_directo() {
188 let h = color_to_hsla(Color::rgba(0.0, 0.0, 1.0, 0.3));
189 assert!((h.a - 0.3).abs() < 1e-6);
190 }
191}