⎇ ced5853154b41aefd8e16f57e31f0d3ddfc3a56c
crates/modules/fana/fana-editor-gpui/src/lib.rs
Historia
Quién tocó qué
crudo
feat(fana): backend GPUI + app — editor de escritura DAG
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}