tawasuyu

tawasuyu/brahman

Rust 492TOML 194Markdown 53JSON 176939 Definiciones

feat(lapaloma): backend GPUI + LapalomaChartElement + app demo

Cadena end-to-end DataBuffer → LineSeries → Canvas → gpui::Window
funcionando. cargo run -p lapaloma-demo abre una ventana con sin(x)
sobre 1024 muestras y una sola paint_path por frame.

- lapaloma-render: feature `gpui` opcional. WindowCanvas adapter
  traduce el trait Canvas a paint_quad/paint_path de gpui 0.2.
  Conversión RGB→HSL para integrar con el sistema de colores Hsla
  del resto del codebase yahweh. 3 tests de conversión.
- lapaloma-cartesian: feature `gpui` (default). element::LapalomaChartElement
  con impl Element + IntoElement. Arma WindowCanvas en paint() y
  delega a LineSeries — un solo paint_path por chart.
- crates/apps/lapaloma-demo registrado en workspace.

Limitaciones conocidas v0.1: clip stack, triangle strips y draw_text
no implementados (los necesitan phosphor / Sankey / axes; se
agregan en sus fases).

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

97c09bc96a9c41ade9d40ab3c3613e35909a9f12 · por sergio · (hace 5 meses) · padre 791ca18d8 · ver el árbol en este commit

Delta semántico · 37

crates/apps/lapaloma-demo/src/main.rs

crates/modules/ui_engine/widgets/lapaloma-cartesian/src/element.rs

crates/modules/ui_engine/widgets/lapaloma-render/src/gpui_backend.rs

crates/modules/ui_engine/widgets/lapaloma-render/src/lib.rs

10 archivos · +464 −2

diff --git a/Cargo.lock b/Cargo.lock
index b1adef1..90314b3 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -5393,6 +5393,18 @@ dependencies = [
 name = "lapaloma-core"
 version = "0.1.0"
 
+[[package]]
+name = "lapaloma-demo"
+version = "0.1.0"
+dependencies = [
+ "gpui",
+ "lapaloma-cartesian",
+ "lapaloma-core",
+ "lapaloma-render",
+ "yahweh-launcher",
+ "yahweh-theme",
+]
+
 [[package]]
 name = "lapaloma-export"
 version = "0.1.0"
@@ -5461,6 +5473,7 @@ dependencies = [
 name = "lapaloma-render"
 version = "0.1.0"
 dependencies = [
+ "gpui",
  "lapaloma-core",
 ]
 
diff --git a/Cargo.toml b/Cargo.toml
index 0e444ad..8b31161 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -136,6 +136,7 @@ members = [
     "crates/apps/shipote-gateway",
     "crates/apps/shipote-shell",
     "crates/apps/gioser-web",
+    "crates/apps/lapaloma-demo",
 ]
 
 [workspace.package]
diff --git a/crates/apps/lapaloma-demo/Cargo.toml b/crates/apps/lapaloma-demo/Cargo.toml
new file mode 100644
index 0000000..abf040e
--- /dev/null
+++ b/crates/apps/lapaloma-demo/Cargo.toml
@@ -0,0 +1,16 @@
+[package]
+name = "lapaloma-demo"
+version = { workspace = true }
+edition = { workspace = true }
+license = { workspace = true }
+authors = { workspace = true }
+publish = { workspace = true }
+description = "Lapaloma — demo app: una serie sin(x) sobre ChartViewport rendereada con LapalomaChartElement. Valida la cadena core → render → cartesian → gpui en vivo."
+
+[dependencies]
+gpui = { workspace = true }
+yahweh-launcher = { path = "../../modules/ui_engine/libs/launcher" }
+yahweh-theme = { path = "../../modules/ui_engine/libs/theme" }
+lapaloma-core = { path = "../../modules/ui_engine/libs/lapaloma-core" }
+lapaloma-render = { path = "../../modules/ui_engine/widgets/lapaloma-render", features = ["gpui"] }
+lapaloma-cartesian = { path = "../../modules/ui_engine/widgets/lapaloma-cartesian" }
diff --git a/crates/apps/lapaloma-demo/src/main.rs b/crates/apps/lapaloma-demo/src/main.rs
new file mode 100644
index 0000000..3c6ce14
--- /dev/null
+++ b/crates/apps/lapaloma-demo/src/main.rs
@@ -0,0 +1,93 @@
+//! `lapaloma-demo` — demo visual mínimo de Lapaloma sobre yahweh.
+//!
+//! Levanta una ventana de 900×560 con un único chart cartesiano
+//! pre-llenado con `sin(x · 0.04)` sobre 1024 muestras. Sirve
+//! como smoke test de la cadena completa:
+//!
+//! ```text
+//! DataBuffer        (lapaloma-core)
+//!   ↓ LTTB cuando densidad > 3× ancho del plot
+//! CoordinateSystem  (lapaloma-cartesian)
+//!   ↓ project_buffer dominio → pixel, zero-alloc
+//! LineSeries        (lapaloma-cartesian)
+//!   ↓ canvas.stroke_polyline (una sola draw call)
+//! WindowCanvas      (lapaloma-render::gpui_backend)
+//!   ↓ paint_path
+//! gpui::Window
+//! ```
+//!
+//! Correr con `cargo run -p lapaloma-demo`. Requiere DISPLAY/WAYLAND.
+
+use gpui::{div, prelude::*, px, Context, IntoElement, Render, Window};
+
+use lapaloma_cartesian::{lapaloma_chart, ChartViewport};
+use lapaloma_core::buffer::DataBuffer;
+use lapaloma_render::{Color, StrokeStyle};
+use yahweh_launcher::launch_app;
+use yahweh_theme::Theme;
+
+const N_SAMPLES: usize = 1024;
+const FREQ: f32 = 0.04;
+
+fn main() {
+    launch_app("Lapaloma — sin(x) demo", (900., 560.), Demo::new);
+}
+
+struct Demo {
+    data: DataBuffer,
+}
+
+impl Demo {
+    fn new(_cx: &mut Context<Self>) -> Self {
+        // Buffer canónico interleaved: x va 0..N-1, y = sin(x · FREQ).
+        let mut data = DataBuffer::with_capacity(N_SAMPLES);
+        for i in 0..N_SAMPLES {
+            let x = i as f32;
+            let y = (x * FREQ).sin();
+            data.push(x, y);
+        }
+        Self { data }
+    }
+}
+
+impl Render for Demo {
+    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+        let theme = Theme::global(cx).clone();
+
+        // Viewport: ve toda la X, Y con un poco de margen.
+        let viewport = ChartViewport::new(0.0, (N_SAMPLES - 1) as f64, -1.1, 1.1);
+
+        // Estilo: stroke nórdico azul claro sobre fondo del theme.
+        let stroke = StrokeStyle::new(2.0, Color::from_hex(0x88c0d0));
+        let plot_bg = Color::rgba(0.10, 0.12, 0.16, 1.0);
+
+        // DataBuffer es `Clone`; el Element toma ownership del clone
+        // por frame. Para datasets enormes el siguiente paso es
+        // pasar a `Arc<DataBuffer>`; con 1k samples es trivial.
+        let chart = lapaloma_chart(self.data.clone(), viewport, stroke).background(plot_bg);
+
+        div()
+            .size_full()
+            .bg(theme.bg_app.clone())
+            .p(px(16.))
+            .flex()
+            .flex_col()
+            .gap(px(12.))
+            .child(
+                div()
+                    .text_color(theme.fg_text)
+                    .text_size(px(18.))
+                    .child("Lapaloma — demo cartesian"),
+            )
+            .child(
+                div()
+                    .text_color(theme.fg_muted)
+                    .text_size(px(12.))
+                    .child(format!(
+                        "{} muestras de sin(x · {}). LineSeries · LTTB-on-density · 1 draw call.",
+                        N_SAMPLES, FREQ
+                    )),
+            )
+            .child(div().w_full().flex_grow().child(chart))
+    }
+}
diff --git a/crates/modules/ui_engine/widgets/lapaloma-cartesian/Cargo.toml b/crates/modules/ui_engine/widgets/lapaloma-cartesian/Cargo.toml
index 09892e9..e895f9c 100644
--- a/crates/modules/ui_engine/widgets/lapaloma-cartesian/Cargo.toml
+++ b/crates/modules/ui_engine/widgets/lapaloma-cartesian/Cargo.toml
@@ -10,4 +10,8 @@ description = "Lapaloma — gráficos cartesianos: LineSeries / BarSeries / Area
 [dependencies]
 lapaloma-core = { path = "../../libs/lapaloma-core" }
 lapaloma-render = { path = "../lapaloma-render" }
-gpui = { workspace = true }
+gpui = { workspace = true, optional = true }
+
+[features]
+default = ["gpui"]
+gpui = ["dep:gpui", "lapaloma-render/gpui"]
diff --git a/crates/modules/ui_engine/widgets/lapaloma-cartesian/src/element.rs b/crates/modules/ui_engine/widgets/lapaloma-cartesian/src/element.rs
new file mode 100644
index 0000000..213735e
--- /dev/null
+++ b/crates/modules/ui_engine/widgets/lapaloma-cartesian/src/element.rs
@@ -0,0 +1,165 @@
+//! `LapalomaChartElement` — el `Element` GPUI que envuelve el
+//! pipeline cartesian.
+//!
+//! Owns un `DataBuffer` y un `ChartViewport`. En `paint()` arma el
+//! `WindowCanvas` adapter de `lapaloma-render` y delega a una
+//! [`LineSeries`]. El resultado: una sola `stroke_polyline` =
+//! una sola `paint_path` de GPUI = un solo draw call.
+//!
+//! Sin event handlers todavía — pan/zoom interactivos van en una
+//! fase posterior cuando enganchemos los gesture handlers de GPUI.
+
+use std::panic;
+
+use gpui::{
+    App, Bounds, Element, ElementId, GlobalElementId, InspectorElementId, IntoElement, LayoutId,
+    Pixels, Style, Window,
+};
+
+use lapaloma_core::buffer::DataBuffer;
+use lapaloma_render::{Canvas, Color, Rect, StrokeStyle, WindowCanvas};
+
+use crate::coord_system::CoordinateSystem;
+use crate::series::{LineSeries, PaintCtx, RenderMode, Series};
+use crate::viewport::ChartViewport;
+
+/// Chart cartesiano de una sola serie. Para múltiples series va a
+/// venir un `LapalomaChart` que componga varios `Series` boxed —
+/// por ahora arrancamos con el caso mono-serie.
+pub struct LapalomaChartElement {
+    pub data: DataBuffer,
+    pub viewport: ChartViewport,
+    pub stroke: StrokeStyle,
+    /// Color de fondo del plot. `None` = transparente, hereda
+    /// el container.
+    pub background: Option<Color>,
+    /// Padding interior del plot (deja espacio para futuros ejes).
+    pub padding: f32,
+    /// Scratch buffer reusable entre frames. Sin Arc/Mutex porque
+    /// el Element se mueve al árbol y no se comparte.
+    scratch: Vec<f32>,
+}
+
+impl LapalomaChartElement {
+    pub fn new(data: DataBuffer, viewport: ChartViewport, stroke: StrokeStyle) -> Self {
+        Self {
+            data,
+            viewport,
+            stroke,
+            background: None,
+            padding: 8.0,
+            scratch: Vec::new(),
+        }
+    }
+
+    pub fn background(mut self, color: Color) -> Self {
+        self.background = Some(color);
+        self
+    }
+
+    pub fn padding(mut self, px: f32) -> Self {
+        self.padding = px;
+        self
+    }
+}
+
+impl IntoElement for LapalomaChartElement {
+    type Element = Self;
+    fn into_element(self) -> Self::Element {
+        self
+    }
+}
+
+impl Element for LapalomaChartElement {
+    type RequestLayoutState = ();
+    type PrepaintState = ();
+
+    fn id(&self) -> Option<ElementId> {
+        None
+    }
+
+    fn source_location(&self) -> Option<&'static panic::Location<'static>> {
+        None
+    }
+
+    fn request_layout(
+        &mut self,
+        _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
+        window: &mut Window,
+        cx: &mut App,
+    ) -> (LayoutId, Self::RequestLayoutState) {
+        // Layout default: ocupa lo que su parent le dé via
+        // size_full(). El usuario arma el sizing afuera con
+        // div().w_full().h(px(N)) o equivalente.
+        let mut style = Style::default();
+        style.size.width = gpui::Length::Definite(gpui::DefiniteLength::Fraction(1.0));
+        style.size.height = gpui::Length::Definite(gpui::DefiniteLength::Fraction(1.0));
+        let id = window.request_layout(style, [], cx);
+        (id, ())
+    }
+
+    fn prepaint(
+        &mut self,
+        _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
+        _bounds: Bounds<Pixels>,
+        _request_layout: &mut Self::RequestLayoutState,
+        _window: &mut Window,
+        _cx: &mut App,
+    ) -> Self::PrepaintState {
+    }
+
+    fn paint(
+        &mut self,
+        _id: Option<&GlobalElementId>,
+        _inspector_id: Option<&InspectorElementId>,
+        bounds: Bounds<Pixels>,
+        _request_layout: &mut Self::RequestLayoutState,
+        _prepaint: &mut Self::PrepaintState,
+        window: &mut Window,
+        _cx: &mut App,
+    ) {
+        let ox: f32 = bounds.origin.x.into();
+        let oy: f32 = bounds.origin.y.into();
+        let w: f32 = bounds.size.width.into();
+        let h: f32 = bounds.size.height.into();
+        let plot = Rect::new(
+            ox + self.padding,
+            oy + self.padding,
+            (w - self.padding * 2.0).max(1.0),
+            (h - self.padding * 2.0).max(1.0),
+        );
+
+        let cs = CoordinateSystem::new(self.viewport, plot);
+        let mut canvas = WindowCanvas::new(window);
+
+        if let Some(bg) = self.background {
+            canvas.fill_rect(Rect::new(ox, oy, w, h), bg);
+        }
+
+        let series = LineSeries::new(&self.data, self.stroke);
+        self.scratch.clear();
+        let mut ctx = PaintCtx {
+            cs,
+            mode: RenderMode::UiRich,
+            scratch: &mut self.scratch,
+        };
+        series.paint(&mut ctx, &mut canvas);
+    }
+}
+
+/// Helper builder-style para uso ergonómico desde `Render::render`.
+///
+/// ```ignore
+/// div().w_full().h(px(300.)).child(
+///     lapaloma_chart(buf, viewport, stroke).background(rgb(0xff000000))
+/// )
+/// ```
+pub fn lapaloma_chart(
+    data: DataBuffer,
+    viewport: ChartViewport,
+    stroke: StrokeStyle,
+) -> LapalomaChartElement {
+    LapalomaChartElement::new(data, viewport, stroke)
+}
diff --git a/crates/modules/ui_engine/widgets/lapaloma-cartesian/src/lib.rs b/crates/modules/ui_engine/widgets/lapaloma-cartesian/src/lib.rs
index c8ce08a..d461d12 100644
--- a/crates/modules/ui_engine/widgets/lapaloma-cartesian/src/lib.rs
+++ b/crates/modules/ui_engine/widgets/lapaloma-cartesian/src/lib.rs
@@ -27,11 +27,16 @@ pub mod viewport;
 pub mod coord_system;
 pub mod series;
 
+#[cfg(feature = "gpui")]
+pub mod element;
+
 // Pendientes — siguen como placeholders hasta su fase.
 pub mod axis {}
 pub mod picture_cache {}
-pub mod element {}
 
 pub use viewport::ChartViewport;
 pub use coord_system::CoordinateSystem;
 pub use series::{LineSeries, PaintCtx, RenderMode, Series};
+
+#[cfg(feature = "gpui")]
+pub use element::{lapaloma_chart, LapalomaChartElement};
diff --git a/crates/modules/ui_engine/widgets/lapaloma-render/Cargo.toml b/crates/modules/ui_engine/widgets/lapaloma-render/Cargo.toml
index 4eee5e0..08d4dda 100644
--- a/crates/modules/ui_engine/widgets/lapaloma-render/Cargo.toml
+++ b/crates/modules/ui_engine/widgets/lapaloma-render/Cargo.toml
@@ -9,3 +9,8 @@ description = "Lapaloma — abstracción de painter: trait Canvas + RenderPlan +
 
 [dependencies]
 lapaloma-core = { path = "../../libs/lapaloma-core" }
+gpui = { workspace = true, optional = true }
+
+[features]
+default = []
+gpui = ["dep:gpui"]
diff --git a/crates/modules/ui_engine/widgets/lapaloma-render/src/gpui_backend.rs b/crates/modules/ui_engine/widgets/lapaloma-render/src/gpui_backend.rs
new file mode 100644
index 0000000..7683a4b
--- /dev/null
+++ b/crates/modules/ui_engine/widgets/lapaloma-render/src/gpui_backend.rs
@@ -0,0 +1,154 @@
+//! Backend CPU del trait [`crate::Canvas`] sobre `gpui::Window`.
+//!
+//! Bajo el feature `gpui`. Traduce los primitivos de Lapaloma a las
+//! llamadas nativas de GPUI 0.2 (`paint_quad`, `paint_path`). No
+//! introduce dependencia transitiva a gpui en los crates de
+//! visualización — éstos siguen hablando contra el trait abstracto;
+//! sólo el `Element` GPUI de cada widget importa este módulo.
+//!
+//! Limitaciones de la implementación CPU:
+//! - `push_clip` / `pop_clip` quedan como no-op por ahora — GPUI
+//!   maneja content mask via builders de alto nivel; el chart se
+//!   apoya en el bounds del Element para no pintar fuera.
+//! - `fill_triangle_strip` no implementado (lo necesitan phosphor
+//!   y Sankey, que aún no están).
+//! - `draw_text` no implementado (axis labels lo necesitan; va con
+//!   `WindowTextSystem` en una fase próxima).
+
+use crate::{Canvas, Color, Point, Rect, StrokeStyle};
+use gpui::{fill, hsla, point as gpui_point, px, size as gpui_size, Bounds, Hsla, PathBuilder, Window};
+
+/// Adapter que pinta sobre un `&mut Window` de GPUI.
+///
+/// Vida útil del borrow del window iguala la de la pintura. Construir
+/// uno nuevo en cada `paint()` del Element.
+pub struct WindowCanvas<'a> {
+    window: &'a mut Window,
+}
+
+impl<'a> WindowCanvas<'a> {
+    pub fn new(window: &'a mut Window) -> Self {
+        Self { window }
+    }
+}
+
+/// Conversión RGB(a) → HSL(a). GPUI consume `Hsla` para casi todo
+/// el path. Linear, sin gamma — coincide con la convención del
+/// resto del codebase yahweh.
+pub(crate) fn color_to_hsla(c: Color) -> Hsla {
+    let (r, g, b, a) = (c.r, c.g, c.b, c.a);
+    let max = r.max(g).max(b);
+    let min = r.min(g).min(b);
+    let l = (max + min) * 0.5;
+    let delta = max - min;
+    if delta.abs() < 1e-6 {
+        return hsla(0.0, 0.0, l, a);
+    }
+    let s = if l < 0.5 { delta / (max + min) } else { delta / (2.0 - max - min) };
+    let h = if max == r {
+        ((g - b) / delta).rem_euclid(6.0)
+    } else if max == g {
+        (b - r) / delta + 2.0
+    } else {
+        (r - g) / delta + 4.0
+    };
+    hsla(h / 6.0, s, l, a)
+}
+
+fn to_bounds(r: Rect) -> Bounds<gpui::Pixels> {
+    Bounds {
+        origin: gpui_point(px(r.x), px(r.y)),
+        size: gpui_size(px(r.w), px(r.h)),
+    }
+}
+
+impl<'a> Canvas for WindowCanvas<'a> {
+    fn push_clip(&mut self, _rect: Rect) {
+        // Sin clip explícito por ahora. El Element pinta dentro
+        // de sus bounds y los painters de lapaloma respetan el
+        // plot_rect en sus proyecciones.
+    }
+    fn pop_clip(&mut self) {}
+
+    fn fill_rect(&mut self, rect: Rect, color: Color) {
+        let hsla = color_to_hsla(color);
+        self.window.paint_quad(fill(to_bounds(rect), hsla));
+    }
+
+    fn stroke_rect(&mut self, rect: Rect, stroke: StrokeStyle) {
+        // 4 line segments con PathBuilder en stroke mode.
+        let mut pb = PathBuilder::stroke(px(stroke.width));
+        pb.move_to(gpui_point(px(rect.x), px(rect.y)));
+        pb.line_to(gpui_point(px(rect.right()), px(rect.y)));
+        pb.line_to(gpui_point(px(rect.right()), px(rect.bottom())));
+        pb.line_to(gpui_point(px(rect.x), px(rect.bottom())));
+        pb.close();
+        if let Ok(path) = pb.build() {
+            self.window.paint_path(path, color_to_hsla(stroke.color));
+        }
+    }
+
+    fn stroke_line(&mut self, a: Point, b: Point, stroke: StrokeStyle) {
+        let mut pb = PathBuilder::stroke(px(stroke.width));
+        pb.move_to(gpui_point(px(a.x), px(a.y)));
+        pb.line_to(gpui_point(px(b.x), px(b.y)));
+        if let Ok(path) = pb.build() {
+            self.window.paint_path(path, color_to_hsla(stroke.color));
+        }
+    }
+
+    fn stroke_polyline(&mut self, coords: &[f32], stroke: StrokeStyle) {
+        if coords.len() < 4 {
+            return; // <2 puntos → no hay segmento
+        }
+        let mut pb = PathBuilder::stroke(px(stroke.width));
+        pb.move_to(gpui_point(px(coords[0]), px(coords[1])));
+        let mut i = 2;
+        while i + 1 < coords.len() {
+            pb.line_to(gpui_point(px(coords[i]), px(coords[i + 1])));
+            i += 2;
+        }
+        if let Ok(path) = pb.build() {
+            self.window.paint_path(path, color_to_hsla(stroke.color));
+        }
+    }
+
+    fn fill_triangle_strip(&mut self, _coords: &[f32], _colors: &[Color]) {
+        // TODO: cuando phosphor / Sankey lo necesiten. GPUI no
+        // tiene API directa para triangle strips con per-vertex
+        // color — habrá que descomponer en quads o subir un
+        // vertex buffer wgpu.
+    }
+
+    fn draw_text(&mut self, _p: Point, _text: &str, _color: Color, _size_px: f32) {
+        // TODO: integrar con WindowTextSystem para axis labels.
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use super::*;
+
+    #[test]
+    fn rgb_a_hsla_grises() {
+        // (0.5, 0.5, 0.5) → h=0, s=0, l=0.5
+        let h = color_to_hsla(Color::rgb(0.5, 0.5, 0.5));
+        assert!((h.s - 0.0).abs() < 1e-6);
+        assert!((h.l - 0.5).abs() < 1e-6);
+    }
+
+    #[test]
+    fn rgb_a_hsla_rojo_puro() {
+        let h = color_to_hsla(Color::rgb(1.0, 0.0, 0.0));
+        // Rojo: h=0, s=1, l=0.5
+        assert!((h.h - 0.0).abs() < 1e-6);
+        assert!((h.s - 1.0).abs() < 1e-6);
+        assert!((h.l - 0.5).abs() < 1e-6);
+    }
+
+    #[test]
+    fn rgb_a_hsla_alpha_pasa_directo() {
+        let h = color_to_hsla(Color::rgba(0.0, 0.0, 1.0, 0.3));
+        assert!((h.a - 0.3).abs() < 1e-6);
+    }
+}
diff --git a/crates/modules/ui_engine/widgets/lapaloma-render/src/lib.rs b/crates/modules/ui_engine/widgets/lapaloma-render/src/lib.rs
index 44f05ae..f12f43a 100644
--- a/crates/modules/ui_engine/widgets/lapaloma-render/src/lib.rs
+++ b/crates/modules/ui_engine/widgets/lapaloma-render/src/lib.rs
@@ -24,7 +24,13 @@ pub mod geom;
 pub mod canvas;
 pub mod plan;
 
+#[cfg(feature = "gpui")]
+pub mod gpui_backend;
+
 pub use color::Color;
 pub use geom::{Point, Rect};
 pub use canvas::{Canvas, StrokeStyle};
 pub use plan::{RenderCmd, RenderPlan};
+
+#[cfg(feature = "gpui")]
+pub use gpui_backend::WindowCanvas;