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>
This commit is contained in:
@@ -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"]
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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};
|
||||
|
||||
Reference in New Issue
Block a user