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:
Generated
+13
@@ -5393,6 +5393,18 @@ dependencies = [
|
|||||||
name = "lapaloma-core"
|
name = "lapaloma-core"
|
||||||
version = "0.1.0"
|
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]]
|
[[package]]
|
||||||
name = "lapaloma-export"
|
name = "lapaloma-export"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -5461,6 +5473,7 @@ dependencies = [
|
|||||||
name = "lapaloma-render"
|
name = "lapaloma-render"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
|
"gpui",
|
||||||
"lapaloma-core",
|
"lapaloma-core",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -136,6 +136,7 @@ members = [
|
|||||||
"crates/apps/shipote-gateway",
|
"crates/apps/shipote-gateway",
|
||||||
"crates/apps/shipote-shell",
|
"crates/apps/shipote-shell",
|
||||||
"crates/apps/gioser-web",
|
"crates/apps/gioser-web",
|
||||||
|
"crates/apps/lapaloma-demo",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|||||||
@@ -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" }
|
||||||
@@ -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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -10,4 +10,8 @@ description = "Lapaloma — gráficos cartesianos: LineSeries / BarSeries / Area
|
|||||||
[dependencies]
|
[dependencies]
|
||||||
lapaloma-core = { path = "../../libs/lapaloma-core" }
|
lapaloma-core = { path = "../../libs/lapaloma-core" }
|
||||||
lapaloma-render = { path = "../lapaloma-render" }
|
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 coord_system;
|
||||||
pub mod series;
|
pub mod series;
|
||||||
|
|
||||||
|
#[cfg(feature = "gpui")]
|
||||||
|
pub mod element;
|
||||||
|
|
||||||
// Pendientes — siguen como placeholders hasta su fase.
|
// Pendientes — siguen como placeholders hasta su fase.
|
||||||
pub mod axis {}
|
pub mod axis {}
|
||||||
pub mod picture_cache {}
|
pub mod picture_cache {}
|
||||||
pub mod element {}
|
|
||||||
|
|
||||||
pub use viewport::ChartViewport;
|
pub use viewport::ChartViewport;
|
||||||
pub use coord_system::CoordinateSystem;
|
pub use coord_system::CoordinateSystem;
|
||||||
pub use series::{LineSeries, PaintCtx, RenderMode, Series};
|
pub use series::{LineSeries, PaintCtx, RenderMode, Series};
|
||||||
|
|
||||||
|
#[cfg(feature = "gpui")]
|
||||||
|
pub use element::{lapaloma_chart, LapalomaChartElement};
|
||||||
|
|||||||
@@ -9,3 +9,8 @@ description = "Lapaloma — abstracción de painter: trait Canvas + RenderPlan +
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
lapaloma-core = { path = "../../libs/lapaloma-core" }
|
lapaloma-core = { path = "../../libs/lapaloma-core" }
|
||||||
|
gpui = { workspace = true, optional = true }
|
||||||
|
|
||||||
|
[features]
|
||||||
|
default = []
|
||||||
|
gpui = ["dep:gpui"]
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,13 @@ pub mod geom;
|
|||||||
pub mod canvas;
|
pub mod canvas;
|
||||||
pub mod plan;
|
pub mod plan;
|
||||||
|
|
||||||
|
#[cfg(feature = "gpui")]
|
||||||
|
pub mod gpui_backend;
|
||||||
|
|
||||||
pub use color::Color;
|
pub use color::Color;
|
||||||
pub use geom::{Point, Rect};
|
pub use geom::{Point, Rect};
|
||||||
pub use canvas::{Canvas, StrokeStyle};
|
pub use canvas::{Canvas, StrokeStyle};
|
||||||
pub use plan::{RenderCmd, RenderPlan};
|
pub use plan::{RenderCmd, RenderPlan};
|
||||||
|
|
||||||
|
#[cfg(feature = "gpui")]
|
||||||
|
pub use gpui_backend::WindowCanvas;
|
||||||
|
|||||||
Reference in New Issue
Block a user