Files
brahman/crates/modules/pineal/render/src/gpui_backend.rs
T
sergio 550c98f275 refactor(monorepo): reorganización lógica + renames + SDDs + split CHANGELOG
Reorganización física de crates/:
- core/ (mezclaba 6 propósitos) se divide en protocol/, init/, runtime/, compat/
- shared/ (3 crates) se redistribuye en protocol/ e init/
- lapaloma (sub-módulo de ui_engine) se promueve a modules/pineal/

Renames de proyectos:
- shipote → shuma (runtime de sandboxes)
- nouser → akasha (explorador de Mónadas)
- yahweh → nahual (motor GPUI, antes ui_engine/)
- lapaloma → pineal (data-viz agnóstica)

Fraccionamiento UI → core agnóstico:
- vista-core (DeckState + snap, 175 LOC, 5 tests verdes)
- barra-core (Task + render_html + sanitize, 90 LOC, 5 tests verdes)
- vista-web y barra-web ahora son thin DOM bindings

Documentación nueva:
- 16 SDDs por subdirectorio (≤80 LOC c/u): protocol/init/runtime/compat
  + 10 módulos + apps/
- docs/STATUS.md con cifras reales por proyecto
- docs/ROADMAP.md con plan a finalización (6 hitos, ~6-8 semanas)
- CHANGELOG.md particionado en docs/changelog/<proyecto>.md (7 buckets)

Automatización:
- scripts/reorg.py — script idempotente que: git mv directorios, renombra
  package names, recomputa path = refs, reescribe imports rust, actualiza
  workspace Cargo.toml. Soporta --dry-run.
- scripts/split-changelog.py — particiona CHANGELOG por componente.

Validación:
- cargo check --workspace pasa (124 crates + 2 nuevos cores).
- 10 tests adicionales (5 en vista-core + 5 en barra-core) verdes.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-19 14:48:34 +00:00

192 lines
6.5 KiB
Rust

//! 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, font, hsla, point as gpui_point, px, size as gpui_size, Bounds, Hsla, PathBuilder,
SharedString, TextRun, 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 nahual.
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 pineal 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) {
if text.is_empty() {
return;
}
let hsla = color_to_hsla(color);
let font_size = px(size_px);
let text_str: SharedString = text.to_string().into();
let runs = [TextRun {
len: text.len(),
font: font("Monospace"),
color: hsla,
background_color: None,
underline: None,
strikethrough: None,
}];
let shaped = self
.window
.text_system()
.shape_line(text_str, font_size, &runs, None);
// Iteramos glyphs vía `paint_glyph` para evitar la
// dependencia con `&mut App` que pide `ShapedLine::paint`.
// Eso encaja con el contrato actual del Canvas trait que
// sólo expone `&mut Window`.
let origin_x = px(p.x);
let origin_y = px(p.y);
for run in shaped.runs.iter() {
for glyph in run.glyphs.iter() {
let gx = origin_x + glyph.position.x;
let gy = origin_y + glyph.position.y;
let _ = self
.window
.paint_glyph(gpui_point(gx, gy), run.font_id, glyph.id, font_size, hsla);
}
}
}
}
#[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);
}
}