tawasuyu

tawasuyu/brahman

Rust 226TOML 67JSON 14HTML 12860 Definitions

⎇ main crates/apps/fana/src/main.rs Rust · 267 lines · 9.1 KB History Who touched what raw

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

1//! `fana` — el editor de escritura DAG, ventana GPUI.
2//!
3//! Compone la cadena de fana:
4//!
5//! ```text
6//!   fana-core ─► fana-graph ─► fana-render-plan ─►
7//!   fana-editor-gpui ─► [esta ventana]
8//! ```
9//!
10//! El documento no es un texto plano sino un grafo de átomos
11//! narrativos. La ventana lo muestra en columnas por rama, con los
12//! conectores de dependencia y el osciloscopio de coherencia. El botón
13//! «Mutar raíz» reescribe el átomo origen y dispara la onda de choque
14//! lógica: todo descendiente cae a «por evaluar».
15
16use fana_core::{CoherenceState, NarrativeAtom};
17use fana_editor_gpui::{editor_view, tone_color};
18use fana_graph::NarrativeGraph;
19use fana_render_plan::{build_plan, CoherenceTone, LayoutConfig};
20use gpui::{div, prelude::*, px, Context, IntoElement, Render, SharedString, Window};
21use nahual_launcher::launch_app;
22use nahual_theme::Theme;
23use uuid::Uuid;
24
25/// Estado del editor.
26struct Fana {
27    graph: NarrativeGraph,
28    /// Átomo raíz — el que muta el botón de demostración.
29    root: Uuid,
30    /// Cuántas veces se mutó la raíz (para variar el texto nuevo).
31    mutations: u32,
32}
33
34impl Fana {
35    fn new(_cx: &mut Context<Self>) -> Self {
36        let (graph, root) = seed_document();
37        Self { graph, root, mutations: 0 }
38    }
39
40    /// Reescribe la raíz y propaga la onda de choque a sus descendientes.
41    fn mutate_root(&mut self) {
42        self.mutations += 1;
43        let nuevo = format!(
44            "Capítulo 1 — versión {}: el viajero nunca llegó al puerto.",
45            self.mutations
46        );
47        if let Some(atom) = self.graph.get_mut(self.root) {
48            atom.set_content(nuevo); // marca la raíz como PendingEvaluation
49        }
50        // Marca en cascada todo descendiente transitivo.
51        self.graph.propagate_mutation(self.root);
52    }
53
54    /// Devuelve todos los átomos a estado coherente.
55    fn revalidate(&mut self) {
56        let ids: Vec<Uuid> = self.graph.atoms().map(|a| a.id).collect();
57        for id in ids {
58            if let Some(atom) = self.graph.get_mut(id) {
59                atom.coherence = CoherenceState::Valid;
60            }
61        }
62    }
63
64    /// Cuenta átomos en cada estado de coherencia: `(pendientes, conflictos)`.
65    fn coherence_counts(&self) -> (usize, usize) {
66        let mut pending = 0;
67        let mut conflict = 0;
68        for a in self.graph.atoms() {
69            match a.coherence {
70                CoherenceState::PendingEvaluation => pending += 1,
71                CoherenceState::InConflict { .. } => conflict += 1,
72                CoherenceState::Valid => {}
73            }
74        }
75        (pending, conflict)
76    }
77}
78
79/// Construye el documento de ejemplo: un relato corto con una rama
80/// alterna. Devuelve el grafo y el id de la raíz.
81fn seed_document() -> (NarrativeGraph, Uuid) {
82    let mut root = NarrativeAtom::new(
83        "Capítulo 1 — el viajero llega al puerto al amanecer.",
84        "principal",
85    );
86    root.semantic_vectors.insert("calma".into(), 0.6);
87    let root_id = root.id;
88
89    let mut posada = NarrativeAtom::new(
90        "El posadero le ofrece cuarto y un vaso de vino tibio.",
91        "principal",
92    )
93    .depends_on(root_id);
94    posada.semantic_vectors.insert("calma".into(), 0.4);
95    posada.semantic_vectors.insert("misterio".into(), 0.3);
96    let posada_id = posada.id;
97
98    let mut pasos = NarrativeAtom::new(
99        "Por la noche escucha pasos lentos en el pasillo.",
100        "principal",
101    )
102    .depends_on(posada_id);
103    pasos.semantic_vectors.insert("misterio".into(), 0.9);
104    pasos.semantic_vectors.insert("miedo".into(), 0.7);
105    let pasos_id = pasos.id;
106
107    let mut puerta = NarrativeAtom::new(
108        "Al amanecer, la puerta de su cuarto está entreabierta.",
109        "principal",
110    )
111    .depends_on(pasos_id);
112    puerta.semantic_vectors.insert("miedo".into(), 1.0);
113    puerta.coherence = CoherenceState::InConflict {
114        origin: pasos_id,
115        reason: "el amanecer ya se narró en el capítulo siguiente".into(),
116    };
117
118    // Rama alterna: el viajero rechaza la posada.
119    let mut muelle = NarrativeAtom::new(
120        "Pero el viajero rechaza el cuarto y duerme sobre el muelle.",
121        "alterna",
122    )
123    .depends_on(posada_id);
124    muelle.semantic_vectors.insert("soledad".into(), 0.8);
125
126    let graph = NarrativeGraph::from_atoms([root, posada, pasos, puerta, muelle]);
127    (graph, root_id)
128}
129
130/// Fila de leyenda: muestra el color de un tono y su etiqueta.
131fn legend_row(tone: CoherenceTone, label: &str, theme: &Theme) -> impl IntoElement {
132    div()
133        .flex()
134        .flex_row()
135        .items_center()
136        .gap(px(8.))
137        .child(div().w(px(12.)).h(px(12.)).rounded(px(3.)).bg(tone_color(tone)))
138        .child(
139            div()
140                .text_size(px(12.))
141                .text_color(theme.fg_muted)
142                .child(SharedString::from(label.to_string())),
143        )
144}
145
146/// Fila etiqueta/valor del panel.
147fn stat_row(label: &str, value: String, theme: &Theme) -> impl IntoElement {
148    div()
149        .flex()
150        .flex_row()
151        .justify_between()
152        .child(div().text_color(theme.fg_muted).child(SharedString::from(label.to_string())))
153        .child(div().text_color(theme.fg_text).child(SharedString::from(value)))
154}
155
156impl Render for Fana {
157    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
158        let theme = Theme::global(cx).clone();
159        let panel = gpui::hsla(220.0 / 360.0, 0.18, 0.10, 1.0);
160        let chip = gpui::hsla(220.0 / 360.0, 0.16, 0.16, 1.0);
161        let (pending, conflict) = self.coherence_counts();
162
163        let plan = build_plan(&self.graph, &LayoutConfig::default());
164
165        // --- Barra de estado ---
166        let status = div()
167            .h(px(34.))
168            .flex()
169            .flex_row()
170            .items_center()
171            .justify_between()
172            .px(px(14.))
173            .bg(panel)
174            .text_color(theme.fg_text)
175            .child("fana · editor de escritura DAG")
176            .child(
177                div()
178                    .text_color(theme.fg_muted)
179                    .child(SharedString::from(format!("{} átomos", self.graph.len()))),
180            );
181
182        // --- Lienzo del editor (con scroll) ---
183        let canvas = div()
184            .id("editor-scroll")
185            .flex_1()
186            .overflow_x_scroll()
187            .overflow_y_scroll()
188            .bg(theme.bg_app)
189            .child(editor_view(&plan, &theme));
190
191        // --- Botones (los listeners se cablean abajo con cx.listener) ---
192        let btn_mutar = div()
193            .id("mutar")
194            .px(px(10.))
195            .py(px(7.))
196            .bg(chip)
197            .rounded(px(5.))
198            .text_color(theme.fg_text)
199            .cursor_pointer()
200            .hover(|s| s.bg(theme.bg_row_hover))
201            .child("⚡  Mutar raíz")
202            .on_click(cx.listener(|fana, _ev, _w, cx| {
203                fana.mutate_root();
204                cx.notify();
205            }));
206        let btn_revalidar = div()
207            .id("revalidar")
208            .px(px(10.))
209            .py(px(7.))
210            .bg(chip)
211            .rounded(px(5.))
212            .text_color(theme.fg_text)
213            .cursor_pointer()
214            .hover(|s| s.bg(theme.bg_row_hover))
215            .child("✓  Re-validar todo")
216            .on_click(cx.listener(|fana, _ev, _w, cx| {
217                fana.revalidate();
218                cx.notify();
219            }));
220
221        // --- Panel lateral ---
222        let side = div()
223            .w(px(240.))
224            .flex()
225            .flex_col()
226            .gap(px(10.))
227            .p(px(12.))
228            .bg(panel)
229            .text_color(theme.fg_text)
230            .child(div().text_color(theme.fg_muted).child("[DOCUMENTO]"))
231            .child(btn_mutar)
232            .child(btn_revalidar)
233            .child(div().h(px(1.)).bg(theme.border))
234            .child(stat_row("Átomos", format!("{}", self.graph.len()), &theme))
235            .child(stat_row("Por evaluar", format!("{pending}"), &theme))
236            .child(stat_row("En conflicto", format!("{conflict}"), &theme))
237            .child(div().h(px(1.)).bg(theme.border))
238            .child(div().text_color(theme.fg_muted).child("coherencia"))
239            .child(legend_row(CoherenceTone::Valid, "coherente", &theme))
240            .child(legend_row(CoherenceTone::Pending, "por evaluar", &theme))
241            .child(legend_row(CoherenceTone::Conflict, "en conflicto", &theme))
242            .child(div().h(px(1.)).bg(theme.border))
243            .child(
244                div()
245                    .text_size(px(11.))
246                    .text_color(theme.fg_muted)
247                    .child(
248                        "«Mutar raíz» reescribe el átomo origen: la onda \
249                         de choque marca cada descendiente como «por \
250                         evaluar».",
251                    ),
252            );
253
254        // --- Composición ---
255        div()
256            .size_full()
257            .flex()
258            .flex_col()
259            .bg(theme.bg_app)
260            .child(status)
261            .child(div().flex().flex_row().flex_1().child(canvas).child(side))
262    }
263}
264
265fn main() {
266    launch_app("brahman · fana", (1180., 760.), Fana::new);
267}