gioser

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198630 Definitions

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>

ced5853154b41aefd8e16f57e31f0d3ddfc3a56c · by sergio · (5 months ago) · parent 494fb7c0b · browse the tree at this commit

Semantic delta · 31

crates/apps/fana/src/main.rs

crates/modules/fana/fana-editor-gpui/src/lib.rs

crates/modules/fana/fana-render-plan/src/lib.rs

7 files · +558 −0

diff --git a/Cargo.lock b/Cargo.lock
index c9e1546..57eab1b 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -3999,6 +3999,20 @@ version = "0.1.9"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 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]]
 name = "fana-core"
 version = "0.1.0"
@@ -4008,6 +4022,15 @@ dependencies = [
  "uuid",
 ]
 
+[[package]]
+name = "fana-editor-gpui"
+version = "0.1.0"
+dependencies = [
+ "fana-render-plan",
+ "gpui",
+ "nahual-theme",
+]
+
 [[package]]
 name = "fana-graph"
 version = "0.1.0"
diff --git a/Cargo.toml b/Cargo.toml
index 7a97a82..5038407 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -167,6 +167,7 @@ members = [
     "crates/modules/fana/fana-core",
     "crates/modules/fana/fana-graph",
     "crates/modules/fana/fana-render-plan",
+    "crates/modules/fana/fana-editor-gpui",
     "crates/modules/fana/fana-store",
     "crates/modules/fana/fana-semantic",
     "crates/modules/fana/fana-md",
@@ -227,6 +228,7 @@ members = [
     "crates/apps/cosmobiologia-cli",
     "crates/apps/cosmobiologia-server",
     "crates/apps/dominium",
+    "crates/apps/fana",
 ]
 
 [workspace.package]
diff --git a/crates/apps/fana/Cargo.toml b/crates/apps/fana/Cargo.toml
new file mode 100644
index 0000000..481d1c3
--- /dev/null
+++ b/crates/apps/fana/Cargo.toml
@@ -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 }
diff --git a/crates/apps/fana/src/main.rs b/crates/apps/fana/src/main.rs
new file mode 100644
index 0000000..6a1e476
--- /dev/null
+++ b/crates/apps/fana/src/main.rs
@@ -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);
+}
diff --git a/crates/modules/fana/fana-editor-gpui/Cargo.toml b/crates/modules/fana/fana-editor-gpui/Cargo.toml
new file mode 100644
index 0000000..ed045fc
--- /dev/null
+++ b/crates/modules/fana/fana-editor-gpui/Cargo.toml
@@ -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 }
diff --git a/crates/modules/fana/fana-editor-gpui/src/lib.rs b/crates/modules/fana/fana-editor-gpui/src/lib.rs
new file mode 100644
index 0000000..1f26f07
--- /dev/null
+++ b/crates/modules/fana/fana-editor-gpui/src/lib.rs
@@ -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");
+    }
+}
diff --git a/crates/modules/fana/fana-render-plan/src/lib.rs b/crates/modules/fana/fana-render-plan/src/lib.rs
index 21d4c23..57282b0 100644
--- a/crates/modules/fana/fana-render-plan/src/lib.rs
+++ b/crates/modules/fana/fana-render-plan/src/lib.rs
@@ -124,6 +124,9 @@ pub struct RenderPlan {
     pub sidepane: Vec<SidepaneMark>,
     /// Alto total del contenido — el backend lo usa para el scroll.
     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`
@@ -254,6 +257,7 @@ pub fn build_plan(graph: &NarrativeGraph, cfg: &LayoutConfig) -> RenderPlan {
         edges,
         sidepane,
         content_height: cfg.margin * 2.0 + order.len() as f32 * row_stride,
+        config: *cfg,
     }
 }