tawasuyu

tawasuyu/brahman

Rust 326TOML 123JSON 17Markdown 144606 Definiciones

⎇ 72758e75ce515f355f27615671c95ecaf8c2a8bc crates/modules/ui_engine/widgets/lapaloma-render/src/gpui_backend.rs Rust · 191 líneas · 6.5 KB Historia Quién tocó qué crudo

feat(lapaloma-cartesian): ejes con ticks y labels decimadas fd2536971 · sergio ·

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}