gioser

tawasuyu/brahman

Rust 296TOML 105JSON 14Markdown 73751 Definiciones

⎇ ced5853154b41aefd8e16f57e31f0d3ddfc3a56c crates/modules/fana/fana-editor-gpui/src/lib.rs Rust · 226 líneas · 6.9 KB Historia Quién tocó qué crudo

feat(fana): backend GPUI + app — editor de escritura DAG ced585315 · sergio ·

1//! `fana-editor-gpui` — el backend GPUI del editor DAG.
2//!
3//! Consume un [`RenderPlan`] de `fana-render-plan` y lo vuelca a la
4//! pantalla: los bloques de átomo y las marcas del osciloscopio son
5//! `div`s posicionados en absoluto (texto y estilo nativos); los
6//! conectores de dependencia van por un [`EdgesElement`] que pinta
7//! paths debajo de todo.
8//!
9//! Es el único crate de fana visual que toca `gpui` — el resto de la
10//! cadena (`core`, `graph`, `render-plan`) es agnóstico.
11
12#![forbid(unsafe_code)]
13
14use std::panic;
15
16use fana_render_plan::{CoherenceTone, Edge, RenderPlan};
17use gpui::{
18    div, point, prelude::*, px, App, Bounds, Element, ElementId, GlobalElementId, Hsla,
19    InspectorElementId, IntoElement, LayoutId, PathBuilder, Pixels, SharedString, Style, Window,
20};
21use nahual_theme::Theme;
22
23/// Color semántico de un estado de coherencia. Fijo, no temático: el
24/// rojo de "conflicto" y el ámbar de "pendiente" son señales, no estilo.
25pub fn tone_color(tone: CoherenceTone) -> Hsla {
26    match tone {
27        CoherenceTone::Valid => gpui::hsla(145.0 / 360.0, 0.42, 0.55, 1.0),
28        CoherenceTone::Pending => gpui::hsla(42.0 / 360.0, 0.82, 0.58, 1.0),
29        CoherenceTone::Conflict => gpui::hsla(2.0 / 360.0, 0.70, 0.58, 1.0),
30    }
31}
32
33/// Etiqueta corta de un tono — para leyendas.
34pub fn tone_label(tone: CoherenceTone) -> &'static str {
35    match tone {
36        CoherenceTone::Valid => "coherente",
37        CoherenceTone::Pending => "por evaluar",
38        CoherenceTone::Conflict => "en conflicto",
39    }
40}
41
42/// `Element` que pinta los conectores de dependencia como líneas.
43///
44/// Llena su contenedor: posiciona cada arista relativa al origen de sus
45/// bounds, igual que los `div`s absolutos de los bloques.
46pub struct EdgesElement {
47    edges: Vec<Edge>,
48    color: Hsla,
49    width: f32,
50}
51
52impl EdgesElement {
53    pub fn new(edges: Vec<Edge>, color: Hsla, width: f32) -> Self {
54        Self { edges, color, width }
55    }
56}
57
58impl IntoElement for EdgesElement {
59    type Element = Self;
60    fn into_element(self) -> Self::Element {
61        self
62    }
63}
64
65impl Element for EdgesElement {
66    type RequestLayoutState = ();
67    type PrepaintState = ();
68
69    fn id(&self) -> Option<ElementId> {
70        None
71    }
72
73    fn source_location(&self) -> Option<&'static panic::Location<'static>> {
74        None
75    }
76
77    fn request_layout(
78        &mut self,
79        _id: Option<&GlobalElementId>,
80        _inspector_id: Option<&InspectorElementId>,
81        window: &mut Window,
82        cx: &mut App,
83    ) -> (LayoutId, Self::RequestLayoutState) {
84        let mut style = Style::default();
85        style.size.width = gpui::Length::Definite(gpui::DefiniteLength::Fraction(1.0));
86        style.size.height = gpui::Length::Definite(gpui::DefiniteLength::Fraction(1.0));
87        (window.request_layout(style, [], cx), ())
88    }
89
90    fn prepaint(
91        &mut self,
92        _id: Option<&GlobalElementId>,
93        _inspector_id: Option<&InspectorElementId>,
94        _bounds: Bounds<Pixels>,
95        _layout: &mut Self::RequestLayoutState,
96        _window: &mut Window,
97        _cx: &mut App,
98    ) -> Self::PrepaintState {
99    }
100
101    fn paint(
102        &mut self,
103        _id: Option<&GlobalElementId>,
104        _inspector_id: Option<&InspectorElementId>,
105        bounds: Bounds<Pixels>,
106        _layout: &mut Self::RequestLayoutState,
107        _prepaint: &mut Self::PrepaintState,
108        window: &mut Window,
109        _cx: &mut App,
110    ) {
111        let ox = bounds.origin.x;
112        let oy = bounds.origin.y;
113        for e in &self.edges {
114            let mut pb = PathBuilder::stroke(px(self.width));
115            pb.move_to(point(ox + px(e.x1), oy + px(e.y1)));
116            // Codo en S: baja recto, cruza, baja recto — legible aunque
117            // las columnas estén separadas.
118            let mid_y = oy + px((e.y1 + e.y2) * 0.5);
119            pb.line_to(point(ox + px(e.x1), mid_y));
120            pb.line_to(point(ox + px(e.x2), mid_y));
121            pb.line_to(point(ox + px(e.x2), oy + px(e.y2)));
122            if let Ok(path) = pb.build() {
123                window.paint_path(path, self.color);
124            }
125        }
126    }
127}
128
129/// Bloque de un átomo: caja posicionada en absoluto con su preview.
130fn block_div(b: &fana_render_plan::AtomBlock, theme: &Theme) -> impl IntoElement {
131    div()
132        .absolute()
133        .left(px(b.x))
134        .top(px(b.y))
135        .w(px(b.w))
136        .h(px(b.h))
137        .flex()
138        .flex_col()
139        .gap(px(3.))
140        .p(px(8.))
141        .bg(theme.bg_panel)
142        .border_2()
143        .border_color(tone_color(b.tone))
144        .rounded(px(5.))
145        .child(
146            div()
147                .text_size(px(10.))
148                .text_color(theme.fg_muted)
149                .child(SharedString::from(format!(
150                    "{}  ·  profundidad {}  ·  {}",
151                    b.branch,
152                    b.depth,
153                    tone_label(b.tone)
154                ))),
155        )
156        .child(
157            div()
158                .text_size(px(13.))
159                .text_color(theme.fg_text)
160                .child(SharedString::from(b.preview.clone())),
161        )
162}
163
164/// Marca del osciloscopio de coherencia en el sidepane.
165fn mark_div(m: &fana_render_plan::SidepaneMark, cfg: &fana_render_plan::LayoutConfig) -> impl IntoElement {
166    let usable = (cfg.sidepane_width - 8.0).max(4.0);
167    let w = (m.intensity * usable).max(3.0);
168    div()
169        .absolute()
170        .left(px(cfg.margin))
171        .top(px(m.y))
172        .w(px(w))
173        .h(px(m.h))
174        .bg(tone_color(m.tone))
175        .rounded(px(3.))
176}
177
178/// Compone el plan completo en un árbol GPUI: capa de conectores al
179/// fondo, bloques y marcas encima. El resultado mide exactamente el
180/// contenido — envolverlo en un contenedor con scroll para documentos
181/// largos.
182pub fn editor_view(plan: &RenderPlan, theme: &Theme) -> impl IntoElement {
183    let cfg = plan.config;
184    let content_w = plan
185        .blocks
186        .iter()
187        .map(|b| b.x + b.w)
188        .fold(0.0f32, f32::max)
189        + cfg.margin;
190
191    let blocks: Vec<_> = plan.blocks.iter().map(|b| block_div(b, theme)).collect();
192    let marks: Vec<_> = plan.sidepane.iter().map(|m| mark_div(m, &cfg)).collect();
193
194    div()
195        .relative()
196        .w(px(content_w.max(cfg.margin * 2.0)))
197        .h(px(plan.content_height.max(cfg.margin * 2.0)))
198        .child(
199            div()
200                .absolute()
201                .left(px(0.))
202                .top(px(0.))
203                .size_full()
204                .child(EdgesElement::new(plan.edges.clone(), theme.border_strong, 1.6)),
205        )
206        .children(blocks)
207        .children(marks)
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn tones_have_distinct_colors() {
216        let v = tone_color(CoherenceTone::Valid);
217        let p = tone_color(CoherenceTone::Pending);
218        let c = tone_color(CoherenceTone::Conflict);
219        assert!(v != p && p != c && v != c);
220    }
221
222    #[test]
223    fn tone_labels_are_set() {
224        assert_eq!(tone_label(CoherenceTone::Conflict), "en conflicto");
225    }
226}