feat(fana): backend GPUI + app — editor de escritura DAG
fana-editor-gpui: EdgesElement pinta los conectores de dependencia como paths; editor_view compone bloques de átomo (divs absolutos coloreados por coherencia) + osciloscopio del sidepane. RenderPlan ahora lleva su LayoutConfig para que el backend sea autosuficiente. app fana: ventana con un relato de ejemplo (rama principal + alterna), botón «Mutar raíz» que dispara la onda de choque lógica (propagate_mutation), «Re-validar todo», leyenda y estadísticas. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
Generated
+23
@@ -3999,6 +3999,20 @@ version = "0.1.9"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fana"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"fana-core",
|
||||||
|
"fana-editor-gpui",
|
||||||
|
"fana-graph",
|
||||||
|
"fana-render-plan",
|
||||||
|
"gpui",
|
||||||
|
"nahual-launcher",
|
||||||
|
"nahual-theme",
|
||||||
|
"uuid",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fana-core"
|
name = "fana-core"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
@@ -4008,6 +4022,15 @@ dependencies = [
|
|||||||
"uuid",
|
"uuid",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "fana-editor-gpui"
|
||||||
|
version = "0.1.0"
|
||||||
|
dependencies = [
|
||||||
|
"fana-render-plan",
|
||||||
|
"gpui",
|
||||||
|
"nahual-theme",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "fana-graph"
|
name = "fana-graph"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
|||||||
@@ -167,6 +167,7 @@ members = [
|
|||||||
"crates/modules/fana/fana-core",
|
"crates/modules/fana/fana-core",
|
||||||
"crates/modules/fana/fana-graph",
|
"crates/modules/fana/fana-graph",
|
||||||
"crates/modules/fana/fana-render-plan",
|
"crates/modules/fana/fana-render-plan",
|
||||||
|
"crates/modules/fana/fana-editor-gpui",
|
||||||
"crates/modules/fana/fana-store",
|
"crates/modules/fana/fana-store",
|
||||||
"crates/modules/fana/fana-semantic",
|
"crates/modules/fana/fana-semantic",
|
||||||
"crates/modules/fana/fana-md",
|
"crates/modules/fana/fana-md",
|
||||||
@@ -227,6 +228,7 @@ members = [
|
|||||||
"crates/apps/cosmobiologia-cli",
|
"crates/apps/cosmobiologia-cli",
|
||||||
"crates/apps/cosmobiologia-server",
|
"crates/apps/cosmobiologia-server",
|
||||||
"crates/apps/dominium",
|
"crates/apps/dominium",
|
||||||
|
"crates/apps/fana",
|
||||||
]
|
]
|
||||||
|
|
||||||
[workspace.package]
|
[workspace.package]
|
||||||
|
|||||||
@@ -0,0 +1,23 @@
|
|||||||
|
[package]
|
||||||
|
name = "fana"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
rust-version.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "fana — editor de escritura DAG: ventana GPUI con el documento como grafo de átomos narrativos, conectores de dependencia y osciloscopio de coherencia."
|
||||||
|
|
||||||
|
[[bin]]
|
||||||
|
name = "fana"
|
||||||
|
path = "src/main.rs"
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
fana-core = { path = "../../modules/fana/fana-core" }
|
||||||
|
fana-graph = { path = "../../modules/fana/fana-graph" }
|
||||||
|
fana-render-plan = { path = "../../modules/fana/fana-render-plan" }
|
||||||
|
fana-editor-gpui = { path = "../../modules/fana/fana-editor-gpui" }
|
||||||
|
nahual-theme = { path = "../../modules/nahual/libs/theme" }
|
||||||
|
nahual-launcher = { path = "../../modules/nahual/libs/launcher" }
|
||||||
|
gpui = { workspace = true }
|
||||||
|
uuid = { workspace = true }
|
||||||
@@ -0,0 +1,267 @@
|
|||||||
|
//! `fana` — el editor de escritura DAG, ventana GPUI.
|
||||||
|
//!
|
||||||
|
//! Compone la cadena de fana:
|
||||||
|
//!
|
||||||
|
//! ```text
|
||||||
|
//! fana-core ─► fana-graph ─► fana-render-plan ─►
|
||||||
|
//! fana-editor-gpui ─► [esta ventana]
|
||||||
|
//! ```
|
||||||
|
//!
|
||||||
|
//! El documento no es un texto plano sino un grafo de átomos
|
||||||
|
//! narrativos. La ventana lo muestra en columnas por rama, con los
|
||||||
|
//! conectores de dependencia y el osciloscopio de coherencia. El botón
|
||||||
|
//! «Mutar raíz» reescribe el átomo origen y dispara la onda de choque
|
||||||
|
//! lógica: todo descendiente cae a «por evaluar».
|
||||||
|
|
||||||
|
use fana_core::{CoherenceState, NarrativeAtom};
|
||||||
|
use fana_editor_gpui::{editor_view, tone_color};
|
||||||
|
use fana_graph::NarrativeGraph;
|
||||||
|
use fana_render_plan::{build_plan, CoherenceTone, LayoutConfig};
|
||||||
|
use gpui::{div, prelude::*, px, Context, IntoElement, Render, SharedString, Window};
|
||||||
|
use nahual_launcher::launch_app;
|
||||||
|
use nahual_theme::Theme;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
/// Estado del editor.
|
||||||
|
struct Fana {
|
||||||
|
graph: NarrativeGraph,
|
||||||
|
/// Átomo raíz — el que muta el botón de demostración.
|
||||||
|
root: Uuid,
|
||||||
|
/// Cuántas veces se mutó la raíz (para variar el texto nuevo).
|
||||||
|
mutations: u32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Fana {
|
||||||
|
fn new(_cx: &mut Context<Self>) -> Self {
|
||||||
|
let (graph, root) = seed_document();
|
||||||
|
Self { graph, root, mutations: 0 }
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Reescribe la raíz y propaga la onda de choque a sus descendientes.
|
||||||
|
fn mutate_root(&mut self) {
|
||||||
|
self.mutations += 1;
|
||||||
|
let nuevo = format!(
|
||||||
|
"Capítulo 1 — versión {}: el viajero nunca llegó al puerto.",
|
||||||
|
self.mutations
|
||||||
|
);
|
||||||
|
if let Some(atom) = self.graph.get_mut(self.root) {
|
||||||
|
atom.set_content(nuevo); // marca la raíz como PendingEvaluation
|
||||||
|
}
|
||||||
|
// Marca en cascada todo descendiente transitivo.
|
||||||
|
self.graph.propagate_mutation(self.root);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Devuelve todos los átomos a estado coherente.
|
||||||
|
fn revalidate(&mut self) {
|
||||||
|
let ids: Vec<Uuid> = self.graph.atoms().map(|a| a.id).collect();
|
||||||
|
for id in ids {
|
||||||
|
if let Some(atom) = self.graph.get_mut(id) {
|
||||||
|
atom.coherence = CoherenceState::Valid;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Cuenta átomos en cada estado de coherencia: `(pendientes, conflictos)`.
|
||||||
|
fn coherence_counts(&self) -> (usize, usize) {
|
||||||
|
let mut pending = 0;
|
||||||
|
let mut conflict = 0;
|
||||||
|
for a in self.graph.atoms() {
|
||||||
|
match a.coherence {
|
||||||
|
CoherenceState::PendingEvaluation => pending += 1,
|
||||||
|
CoherenceState::InConflict { .. } => conflict += 1,
|
||||||
|
CoherenceState::Valid => {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(pending, conflict)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Construye el documento de ejemplo: un relato corto con una rama
|
||||||
|
/// alterna. Devuelve el grafo y el id de la raíz.
|
||||||
|
fn seed_document() -> (NarrativeGraph, Uuid) {
|
||||||
|
let mut root = NarrativeAtom::new(
|
||||||
|
"Capítulo 1 — el viajero llega al puerto al amanecer.",
|
||||||
|
"principal",
|
||||||
|
);
|
||||||
|
root.semantic_vectors.insert("calma".into(), 0.6);
|
||||||
|
let root_id = root.id;
|
||||||
|
|
||||||
|
let mut posada = NarrativeAtom::new(
|
||||||
|
"El posadero le ofrece cuarto y un vaso de vino tibio.",
|
||||||
|
"principal",
|
||||||
|
)
|
||||||
|
.depends_on(root_id);
|
||||||
|
posada.semantic_vectors.insert("calma".into(), 0.4);
|
||||||
|
posada.semantic_vectors.insert("misterio".into(), 0.3);
|
||||||
|
let posada_id = posada.id;
|
||||||
|
|
||||||
|
let mut pasos = NarrativeAtom::new(
|
||||||
|
"Por la noche escucha pasos lentos en el pasillo.",
|
||||||
|
"principal",
|
||||||
|
)
|
||||||
|
.depends_on(posada_id);
|
||||||
|
pasos.semantic_vectors.insert("misterio".into(), 0.9);
|
||||||
|
pasos.semantic_vectors.insert("miedo".into(), 0.7);
|
||||||
|
let pasos_id = pasos.id;
|
||||||
|
|
||||||
|
let mut puerta = NarrativeAtom::new(
|
||||||
|
"Al amanecer, la puerta de su cuarto está entreabierta.",
|
||||||
|
"principal",
|
||||||
|
)
|
||||||
|
.depends_on(pasos_id);
|
||||||
|
puerta.semantic_vectors.insert("miedo".into(), 1.0);
|
||||||
|
puerta.coherence = CoherenceState::InConflict {
|
||||||
|
origin: pasos_id,
|
||||||
|
reason: "el amanecer ya se narró en el capítulo siguiente".into(),
|
||||||
|
};
|
||||||
|
|
||||||
|
// Rama alterna: el viajero rechaza la posada.
|
||||||
|
let mut muelle = NarrativeAtom::new(
|
||||||
|
"Pero el viajero rechaza el cuarto y duerme sobre el muelle.",
|
||||||
|
"alterna",
|
||||||
|
)
|
||||||
|
.depends_on(posada_id);
|
||||||
|
muelle.semantic_vectors.insert("soledad".into(), 0.8);
|
||||||
|
|
||||||
|
let graph = NarrativeGraph::from_atoms([root, posada, pasos, puerta, muelle]);
|
||||||
|
(graph, root_id)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fila de leyenda: muestra el color de un tono y su etiqueta.
|
||||||
|
fn legend_row(tone: CoherenceTone, label: &str, theme: &Theme) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.items_center()
|
||||||
|
.gap(px(8.))
|
||||||
|
.child(div().w(px(12.)).h(px(12.)).rounded(px(3.)).bg(tone_color(tone)))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(12.))
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child(SharedString::from(label.to_string())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Fila etiqueta/valor del panel.
|
||||||
|
fn stat_row(label: &str, value: String, theme: &Theme) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.justify_between()
|
||||||
|
.child(div().text_color(theme.fg_muted).child(SharedString::from(label.to_string())))
|
||||||
|
.child(div().text_color(theme.fg_text).child(SharedString::from(value)))
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Render for Fana {
|
||||||
|
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||||
|
let theme = Theme::global(cx).clone();
|
||||||
|
let panel = gpui::hsla(220.0 / 360.0, 0.18, 0.10, 1.0);
|
||||||
|
let chip = gpui::hsla(220.0 / 360.0, 0.16, 0.16, 1.0);
|
||||||
|
let (pending, conflict) = self.coherence_counts();
|
||||||
|
|
||||||
|
let plan = build_plan(&self.graph, &LayoutConfig::default());
|
||||||
|
|
||||||
|
// --- Barra de estado ---
|
||||||
|
let status = div()
|
||||||
|
.h(px(34.))
|
||||||
|
.flex()
|
||||||
|
.flex_row()
|
||||||
|
.items_center()
|
||||||
|
.justify_between()
|
||||||
|
.px(px(14.))
|
||||||
|
.bg(panel)
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.child("fana · editor de escritura DAG")
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child(SharedString::from(format!("{} átomos", self.graph.len()))),
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Lienzo del editor (con scroll) ---
|
||||||
|
let canvas = div()
|
||||||
|
.id("editor-scroll")
|
||||||
|
.flex_1()
|
||||||
|
.overflow_x_scroll()
|
||||||
|
.overflow_y_scroll()
|
||||||
|
.bg(theme.bg_app)
|
||||||
|
.child(editor_view(&plan, &theme));
|
||||||
|
|
||||||
|
// --- Botones (los listeners se cablean abajo con cx.listener) ---
|
||||||
|
let btn_mutar = div()
|
||||||
|
.id("mutar")
|
||||||
|
.px(px(10.))
|
||||||
|
.py(px(7.))
|
||||||
|
.bg(chip)
|
||||||
|
.rounded(px(5.))
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|s| s.bg(theme.bg_row_hover))
|
||||||
|
.child("⚡ Mutar raíz")
|
||||||
|
.on_click(cx.listener(|fana, _ev, _w, cx| {
|
||||||
|
fana.mutate_root();
|
||||||
|
cx.notify();
|
||||||
|
}));
|
||||||
|
let btn_revalidar = div()
|
||||||
|
.id("revalidar")
|
||||||
|
.px(px(10.))
|
||||||
|
.py(px(7.))
|
||||||
|
.bg(chip)
|
||||||
|
.rounded(px(5.))
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.cursor_pointer()
|
||||||
|
.hover(|s| s.bg(theme.bg_row_hover))
|
||||||
|
.child("✓ Re-validar todo")
|
||||||
|
.on_click(cx.listener(|fana, _ev, _w, cx| {
|
||||||
|
fana.revalidate();
|
||||||
|
cx.notify();
|
||||||
|
}));
|
||||||
|
|
||||||
|
// --- Panel lateral ---
|
||||||
|
let side = div()
|
||||||
|
.w(px(240.))
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap(px(10.))
|
||||||
|
.p(px(12.))
|
||||||
|
.bg(panel)
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.child(div().text_color(theme.fg_muted).child("[DOCUMENTO]"))
|
||||||
|
.child(btn_mutar)
|
||||||
|
.child(btn_revalidar)
|
||||||
|
.child(div().h(px(1.)).bg(theme.border))
|
||||||
|
.child(stat_row("Átomos", format!("{}", self.graph.len()), &theme))
|
||||||
|
.child(stat_row("Por evaluar", format!("{pending}"), &theme))
|
||||||
|
.child(stat_row("En conflicto", format!("{conflict}"), &theme))
|
||||||
|
.child(div().h(px(1.)).bg(theme.border))
|
||||||
|
.child(div().text_color(theme.fg_muted).child("coherencia"))
|
||||||
|
.child(legend_row(CoherenceTone::Valid, "coherente", &theme))
|
||||||
|
.child(legend_row(CoherenceTone::Pending, "por evaluar", &theme))
|
||||||
|
.child(legend_row(CoherenceTone::Conflict, "en conflicto", &theme))
|
||||||
|
.child(div().h(px(1.)).bg(theme.border))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(11.))
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child(
|
||||||
|
"«Mutar raíz» reescribe el átomo origen: la onda \
|
||||||
|
de choque marca cada descendiente como «por \
|
||||||
|
evaluar».",
|
||||||
|
),
|
||||||
|
);
|
||||||
|
|
||||||
|
// --- Composición ---
|
||||||
|
div()
|
||||||
|
.size_full()
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.bg(theme.bg_app)
|
||||||
|
.child(status)
|
||||||
|
.child(div().flex().flex_row().flex_1().child(canvas).child(side))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
fn main() {
|
||||||
|
launch_app("brahman · fana", (1180., 760.), Fana::new);
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
[package]
|
||||||
|
name = "fana-editor-gpui"
|
||||||
|
version.workspace = true
|
||||||
|
edition.workspace = true
|
||||||
|
license.workspace = true
|
||||||
|
authors.workspace = true
|
||||||
|
publish.workspace = true
|
||||||
|
description = "fana — backend GPUI del editor DAG: pinta un RenderPlan como bloques de átomo, conectores de dependencia y osciloscopio de coherencia."
|
||||||
|
|
||||||
|
[dependencies]
|
||||||
|
fana-render-plan = { path = "../fana-render-plan" }
|
||||||
|
nahual-theme = { path = "../../nahual/libs/theme" }
|
||||||
|
gpui = { workspace = true }
|
||||||
@@ -0,0 +1,226 @@
|
|||||||
|
//! `fana-editor-gpui` — el backend GPUI del editor DAG.
|
||||||
|
//!
|
||||||
|
//! Consume un [`RenderPlan`] de `fana-render-plan` y lo vuelca a la
|
||||||
|
//! pantalla: los bloques de átomo y las marcas del osciloscopio son
|
||||||
|
//! `div`s posicionados en absoluto (texto y estilo nativos); los
|
||||||
|
//! conectores de dependencia van por un [`EdgesElement`] que pinta
|
||||||
|
//! paths debajo de todo.
|
||||||
|
//!
|
||||||
|
//! Es el único crate de fana visual que toca `gpui` — el resto de la
|
||||||
|
//! cadena (`core`, `graph`, `render-plan`) es agnóstico.
|
||||||
|
|
||||||
|
#![forbid(unsafe_code)]
|
||||||
|
|
||||||
|
use std::panic;
|
||||||
|
|
||||||
|
use fana_render_plan::{CoherenceTone, Edge, RenderPlan};
|
||||||
|
use gpui::{
|
||||||
|
div, point, prelude::*, px, App, Bounds, Element, ElementId, GlobalElementId, Hsla,
|
||||||
|
InspectorElementId, IntoElement, LayoutId, PathBuilder, Pixels, SharedString, Style, Window,
|
||||||
|
};
|
||||||
|
use nahual_theme::Theme;
|
||||||
|
|
||||||
|
/// Color semántico de un estado de coherencia. Fijo, no temático: el
|
||||||
|
/// rojo de "conflicto" y el ámbar de "pendiente" son señales, no estilo.
|
||||||
|
pub fn tone_color(tone: CoherenceTone) -> Hsla {
|
||||||
|
match tone {
|
||||||
|
CoherenceTone::Valid => gpui::hsla(145.0 / 360.0, 0.42, 0.55, 1.0),
|
||||||
|
CoherenceTone::Pending => gpui::hsla(42.0 / 360.0, 0.82, 0.58, 1.0),
|
||||||
|
CoherenceTone::Conflict => gpui::hsla(2.0 / 360.0, 0.70, 0.58, 1.0),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Etiqueta corta de un tono — para leyendas.
|
||||||
|
pub fn tone_label(tone: CoherenceTone) -> &'static str {
|
||||||
|
match tone {
|
||||||
|
CoherenceTone::Valid => "coherente",
|
||||||
|
CoherenceTone::Pending => "por evaluar",
|
||||||
|
CoherenceTone::Conflict => "en conflicto",
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// `Element` que pinta los conectores de dependencia como líneas.
|
||||||
|
///
|
||||||
|
/// Llena su contenedor: posiciona cada arista relativa al origen de sus
|
||||||
|
/// bounds, igual que los `div`s absolutos de los bloques.
|
||||||
|
pub struct EdgesElement {
|
||||||
|
edges: Vec<Edge>,
|
||||||
|
color: Hsla,
|
||||||
|
width: f32,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl EdgesElement {
|
||||||
|
pub fn new(edges: Vec<Edge>, color: Hsla, width: f32) -> Self {
|
||||||
|
Self { edges, color, width }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl IntoElement for EdgesElement {
|
||||||
|
type Element = Self;
|
||||||
|
fn into_element(self) -> Self::Element {
|
||||||
|
self
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Element for EdgesElement {
|
||||||
|
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) {
|
||||||
|
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));
|
||||||
|
(window.request_layout(style, [], cx), ())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn prepaint(
|
||||||
|
&mut self,
|
||||||
|
_id: Option<&GlobalElementId>,
|
||||||
|
_inspector_id: Option<&InspectorElementId>,
|
||||||
|
_bounds: Bounds<Pixels>,
|
||||||
|
_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>,
|
||||||
|
_layout: &mut Self::RequestLayoutState,
|
||||||
|
_prepaint: &mut Self::PrepaintState,
|
||||||
|
window: &mut Window,
|
||||||
|
_cx: &mut App,
|
||||||
|
) {
|
||||||
|
let ox = bounds.origin.x;
|
||||||
|
let oy = bounds.origin.y;
|
||||||
|
for e in &self.edges {
|
||||||
|
let mut pb = PathBuilder::stroke(px(self.width));
|
||||||
|
pb.move_to(point(ox + px(e.x1), oy + px(e.y1)));
|
||||||
|
// Codo en S: baja recto, cruza, baja recto — legible aunque
|
||||||
|
// las columnas estén separadas.
|
||||||
|
let mid_y = oy + px((e.y1 + e.y2) * 0.5);
|
||||||
|
pb.line_to(point(ox + px(e.x1), mid_y));
|
||||||
|
pb.line_to(point(ox + px(e.x2), mid_y));
|
||||||
|
pb.line_to(point(ox + px(e.x2), oy + px(e.y2)));
|
||||||
|
if let Ok(path) = pb.build() {
|
||||||
|
window.paint_path(path, self.color);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Bloque de un átomo: caja posicionada en absoluto con su preview.
|
||||||
|
fn block_div(b: &fana_render_plan::AtomBlock, theme: &Theme) -> impl IntoElement {
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.left(px(b.x))
|
||||||
|
.top(px(b.y))
|
||||||
|
.w(px(b.w))
|
||||||
|
.h(px(b.h))
|
||||||
|
.flex()
|
||||||
|
.flex_col()
|
||||||
|
.gap(px(3.))
|
||||||
|
.p(px(8.))
|
||||||
|
.bg(theme.bg_panel)
|
||||||
|
.border_2()
|
||||||
|
.border_color(tone_color(b.tone))
|
||||||
|
.rounded(px(5.))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(10.))
|
||||||
|
.text_color(theme.fg_muted)
|
||||||
|
.child(SharedString::from(format!(
|
||||||
|
"{} · profundidad {} · {}",
|
||||||
|
b.branch,
|
||||||
|
b.depth,
|
||||||
|
tone_label(b.tone)
|
||||||
|
))),
|
||||||
|
)
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.text_size(px(13.))
|
||||||
|
.text_color(theme.fg_text)
|
||||||
|
.child(SharedString::from(b.preview.clone())),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Marca del osciloscopio de coherencia en el sidepane.
|
||||||
|
fn mark_div(m: &fana_render_plan::SidepaneMark, cfg: &fana_render_plan::LayoutConfig) -> impl IntoElement {
|
||||||
|
let usable = (cfg.sidepane_width - 8.0).max(4.0);
|
||||||
|
let w = (m.intensity * usable).max(3.0);
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.left(px(cfg.margin))
|
||||||
|
.top(px(m.y))
|
||||||
|
.w(px(w))
|
||||||
|
.h(px(m.h))
|
||||||
|
.bg(tone_color(m.tone))
|
||||||
|
.rounded(px(3.))
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Compone el plan completo en un árbol GPUI: capa de conectores al
|
||||||
|
/// fondo, bloques y marcas encima. El resultado mide exactamente el
|
||||||
|
/// contenido — envolverlo en un contenedor con scroll para documentos
|
||||||
|
/// largos.
|
||||||
|
pub fn editor_view(plan: &RenderPlan, theme: &Theme) -> impl IntoElement {
|
||||||
|
let cfg = plan.config;
|
||||||
|
let content_w = plan
|
||||||
|
.blocks
|
||||||
|
.iter()
|
||||||
|
.map(|b| b.x + b.w)
|
||||||
|
.fold(0.0f32, f32::max)
|
||||||
|
+ cfg.margin;
|
||||||
|
|
||||||
|
let blocks: Vec<_> = plan.blocks.iter().map(|b| block_div(b, theme)).collect();
|
||||||
|
let marks: Vec<_> = plan.sidepane.iter().map(|m| mark_div(m, &cfg)).collect();
|
||||||
|
|
||||||
|
div()
|
||||||
|
.relative()
|
||||||
|
.w(px(content_w.max(cfg.margin * 2.0)))
|
||||||
|
.h(px(plan.content_height.max(cfg.margin * 2.0)))
|
||||||
|
.child(
|
||||||
|
div()
|
||||||
|
.absolute()
|
||||||
|
.left(px(0.))
|
||||||
|
.top(px(0.))
|
||||||
|
.size_full()
|
||||||
|
.child(EdgesElement::new(plan.edges.clone(), theme.border_strong, 1.6)),
|
||||||
|
)
|
||||||
|
.children(blocks)
|
||||||
|
.children(marks)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod tests {
|
||||||
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tones_have_distinct_colors() {
|
||||||
|
let v = tone_color(CoherenceTone::Valid);
|
||||||
|
let p = tone_color(CoherenceTone::Pending);
|
||||||
|
let c = tone_color(CoherenceTone::Conflict);
|
||||||
|
assert!(v != p && p != c && v != c);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn tone_labels_are_set() {
|
||||||
|
assert_eq!(tone_label(CoherenceTone::Conflict), "en conflicto");
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -124,6 +124,9 @@ pub struct RenderPlan {
|
|||||||
pub sidepane: Vec<SidepaneMark>,
|
pub sidepane: Vec<SidepaneMark>,
|
||||||
/// Alto total del contenido — el backend lo usa para el scroll.
|
/// Alto total del contenido — el backend lo usa para el scroll.
|
||||||
pub content_height: f32,
|
pub content_height: f32,
|
||||||
|
/// Config con la que se construyó — el backend lee de aquí los
|
||||||
|
/// márgenes y el ancho del sidepane sin recibirlos por separado.
|
||||||
|
pub config: LayoutConfig,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Profundidad topológica de cada átomo: 0 para las raíces, `1 + máx`
|
/// Profundidad topológica de cada átomo: 0 para las raíces, `1 + máx`
|
||||||
@@ -254,6 +257,7 @@ pub fn build_plan(graph: &NarrativeGraph, cfg: &LayoutConfig) -> RenderPlan {
|
|||||||
edges,
|
edges,
|
||||||
sidepane,
|
sidepane,
|
||||||
content_height: cfg.margin * 2.0 + order.len() as f32 * row_stride,
|
content_height: cfg.margin * 2.0 + order.len() as f32 * row_stride,
|
||||||
|
config: *cfg,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user