tawasuyu

tawasuyu/brahman

Rust 519TOML 202Markdown 57JSON 197424 Definitions

⎇ 53dbdf0f1deea10d6fc567ea2b43e1e75d6d60cd crates/apps/yahweh-shell/src/layout_host.rs Rust · 576 lines · 21.9 KB History Who touched what raw

chore: monorepo inicial con arje + minga + yahweh absorbidos 53dbdf0f1 · Sergio ·

1//! `LayoutHost` — orquestador del layout dinámico.
2//!
3//! Lee un `LayerConfig` (raíz del JSON) y construye un árbol de entidades
4//! GPUI dispatch-eando por `kind`:
5//!
6//! | kind        | factory                                      |
7//! |-------------|----------------------------------------------|
8//! | "Split"     | `SplitContainer` con dirección + flex        |
9//! | "Tree"      | `ManagedTree` con dataset stub               |
10//! | "Status"    | `StatusPanel`                                |
11//! | (otro)      | placeholder textual con el kind y params     |
12//!
13//! Cada entidad se memoiza por `NodeId` (string opcional del JSON o el path
14//! sintético `root/child/0`). Mientras el `id` no cambie entre rebuilds, la
15//! misma instancia se reusa — esto es lo que permite swappear el `kind` de
16//! un container (Split → Tabs → Tiled) preservando los hijos sin reset.
17//!
18//! En Fase 3 el `LayoutHost` solo reconstruye al inicio. La observación del
19//! `LayoutModel` (hot-reload del JSON, mutaciones desde la UI) entra en
20//! Fase 7. Pero el diseño ya soporta `rebuild()` idempotente — se invoca
21//! cuando el modelo cambia y la memoización mantiene los hijos vivos.
22
23use std::collections::HashMap;
24
25use gpui::{
26    AnyView, Context, Entity, IntoElement, Render, SharedString, Window, div, prelude::*,
27};
28
29use yahweh_bus::{AppBus, AppEvent};
30use yahweh_core::{LayerConfig, LayoutDirection, NodeId};
31use yahweh_database_explorer::{DatabaseExplorer, DatabaseExplorerEvent};
32use yahweh_file_explorer::{FileExplorer, FileExplorerEvent};
33use yahweh_image_viewer::ImageViewer;
34use yahweh_text_viewer::TextViewer;
35use yahweh_widget_container_core::ChildSlot;
36use yahweh_widget_splitter::{SplitContainer, SplitEvent};
37use yahweh_widget_tabs::TabContainer;
38use yahweh_widget_tiled::{TiledContainer, TiledEvent};
39
40use crate::layout_model::{LayoutModel, LayoutModelEvent};
41use crate::managed_tree::ManagedTree;
42use crate::persister::Persister;
43use crate::status_panel::StatusPanel;
44
45// =====================================================================
46// LayoutHost
47// =====================================================================
48
49pub struct LayoutHost {
50    /// Modelo observable. Cualquier mutación (set_kind, replace_tree)
51    /// dispara un rebuild idempotente que preserva memoización.
52    model: Entity<LayoutModel>,
53
54    /// Bus app-level. Lo distribuimos a viewers (que se subscriben para
55    /// reaccionar a EntitySelected/Opened) y forwardeamos los eventos
56    /// tipados de explorers hacia él.
57    bus: Entity<AppBus>,
58
59    /// Persister adoptado — el LayoutHost mantiene su Entity vivo. Sin
60    /// strong handle el entity se dropea y la subscripción al model
61    /// queda inactiva.
62    #[allow(dead_code)]
63    persister: Entity<Persister>,
64
65    /// Memoización de instancias por NodeId. Cada slot guarda la entidad
66    /// tipada para poder llamarle métodos específicos (set_children en
67    /// containers, etc.). Se preserva entre rebuilds — eso es lo que
68    /// permite swappear kind del padre sin perder los hijos.
69    nodes: HashMap<NodeId, NodeSlot>,
70
71    /// La AnyView raíz, computada por el último `rebuild`. El render solo
72    /// pinta este handle.
73    root_view: Option<AnyView>,
74}
75
76/// Una entidad concreta instanciada en el árbol. Distinguimos por variante
77/// porque cada tipo tiene una API distinta para reaccionar a actualizaciones
78/// (set_children para Split, etc.). Las factories devuelven `AnyView`
79/// directamente (`AnyView::from(entity.clone())`); esta enum solo guarda
80/// la handle tipada para llamadas futuras.
81enum NodeSlot {
82    Split(Entity<SplitContainer>),
83    Tabs(Entity<TabContainer>),
84    Tiled(Entity<TiledContainer>),
85    Tree(Entity<ManagedTree>),
86    FileExplorer(Entity<FileExplorer>),
87    DatabaseExplorer(Entity<DatabaseExplorer>),
88    TextViewer(Entity<TextViewer>),
89    ImageViewer(Entity<ImageViewer>),
90    Status(Entity<StatusPanel>),
91    Placeholder(Entity<PlaceholderView>),
92}
93
94impl LayoutHost {
95    pub fn new(
96        model: Entity<LayoutModel>,
97        bus: Entity<AppBus>,
98        persister: Entity<Persister>,
99        cx: &mut Context<Self>,
100    ) -> Self {
101        // Subscripción event-filtered: solo rebuildeamos en cambios
102        // estructurales (set_kind, replace_tree). FlexChanged proviene de
103        // drags de divisor — el splitter ya tiene el flex aplicado en su
104        // Vec, rebuildear lo resetearía y rompería el drag.
105        cx.subscribe(&model, |this, _, ev: &LayoutModelEvent, cx| match ev {
106            LayoutModelEvent::StructureChanged => this.rebuild(cx),
107            LayoutModelEvent::FlexChanged => {}
108        })
109        .detach();
110
111        let mut me = Self {
112            model,
113            bus,
114            persister,
115            nodes: HashMap::new(),
116            root_view: None,
117        };
118        me.rebuild(cx);
119        me
120    }
121
122    /// Rebuild idempotente: walk del árbol del model, instancia (o reusa)
123    /// cada nodo, propaga children a los containers.
124    pub fn rebuild(&mut self, cx: &mut Context<Self>) {
125        // Snapshot del config para no chocar con el borrow al iterar +
126        // mutar self.nodes.
127        let cfg = self.model.read(cx).tree().clone();
128        let used_ids = std::cell::RefCell::new(Vec::new());
129        let view = self.build_node(&cfg, "root", &used_ids, cx);
130
131        // GC: tirar nodos cuyo id ya no aparece en el árbol nuevo.
132        let used: std::collections::HashSet<NodeId> =
133            used_ids.into_inner().into_iter().collect();
134        self.nodes.retain(|id, _| used.contains(id));
135
136        self.root_view = Some(view);
137        cx.notify();
138    }
139
140    /// DFS recursivo. `path` se acumula para los nodos que no traen `id`
141    /// propio en el JSON (`root/0/1` etc) — la sintetización vive en
142    /// `NodeId::from_layer`.
143    fn build_node(
144        &mut self,
145        cfg: &LayerConfig,
146        path: &str,
147        used_ids: &std::cell::RefCell<Vec<NodeId>>,
148        cx: &mut Context<Self>,
149    ) -> AnyView {
150        let id = NodeId::from_layer(cfg, path);
151        used_ids.borrow_mut().push(id.clone());
152
153        match cfg.kind.as_str() {
154            "Split" => self.build_split(id, cfg, path, used_ids, cx),
155            "Tabs" => self.build_tabs(id, cfg, path, used_ids, cx),
156            "Tiled" => self.build_tiled(id, cfg, path, used_ids, cx),
157            "Tree" => self.build_tree(id, cfg, cx),
158            "FileExplorer" => self.build_file_explorer(id, cfg, cx),
159            "DatabaseExplorer" => self.build_database_explorer(id, cfg, cx),
160            "TextViewer" => self.build_text_viewer(id, cx),
161            "ImageViewer" => self.build_image_viewer(id, cx),
162            "Status" => self.build_status(id, cx),
163            _ => self.build_placeholder(id, cfg, cx),
164        }
165    }
166
167    /// Helper común — construye los `ChildSlot`s de un contenedor haciendo
168    /// recursión sobre los hijos del JSON. Usado por Split / Tabs / Tiled.
169    fn build_child_slots(
170        &mut self,
171        cfg: &LayerConfig,
172        path: &str,
173        used_ids: &std::cell::RefCell<Vec<NodeId>>,
174        cx: &mut Context<Self>,
175    ) -> Vec<ChildSlot> {
176        let mut slots = Vec::with_capacity(cfg.children.len());
177        for (i, child) in cfg.children.iter().enumerate() {
178            let child_path = format!("{}/{}", path, i);
179            let child_view = self.build_node(child, &child_path, used_ids, cx);
180            slots.push(ChildSlot {
181                id: NodeId::from_layer(child, &child_path),
182                flex: child.flex_weight() as f32,
183                label: child.get_param("label").cloned(),
184                view: child_view,
185            });
186        }
187        slots
188    }
189
190    // ------- factories por kind -------
191
192    fn build_split(
193        &mut self,
194        id: NodeId,
195        cfg: &LayerConfig,
196        path: &str,
197        used_ids: &std::cell::RefCell<Vec<NodeId>>,
198        cx: &mut Context<Self>,
199    ) -> AnyView {
200        let direction = match cfg.layout_direction() {
201            LayoutDirection::Vertical => LayoutDirection::Vertical,
202            LayoutDirection::Horizontal => LayoutDirection::Horizontal,
203            LayoutDirection::Overlay => LayoutDirection::Vertical, // fallback
204        };
205
206        // Get-or-create — si ya existe del rebuild anterior y es Split, lo
207        // reusamos. Si era de otro tipo, lo descartamos.
208        let entity = match self.nodes.get(&id) {
209            Some(NodeSlot::Split(e)) => e.clone(),
210            _ => {
211                let e = cx.new(|cx| SplitContainer::new(direction, cx));
212                // Suscripción a DragEnd para persistir flex al model.
213                // Usamos el id del Split como ancla para resolver children
214                // por id en el LayerConfig.
215                let model = self.model.clone();
216                let split_node_id = id.clone();
217                cx.subscribe(&e, move |_, split_entity, ev: &SplitEvent, cx| {
218                    if !matches!(ev, SplitEvent::DragEnd) {
219                        return;
220                    }
221                    // Snapshot de los flex actuales del splitter.
222                    let snapshots: Vec<(NodeId, f32)> = split_entity
223                        .read(cx)
224                        .children()
225                        .iter()
226                        .map(|c| (c.id.clone(), c.flex))
227                        .collect();
228                    let _ = split_node_id; // (queda disponible si en
229                                            // futuro queremos targetear
230                                            // el padre directamente).
231                    model.update(cx, |m, cx| {
232                        for (child_id, flex) in snapshots {
233                            m.set_flex(&child_id, flex, cx);
234                        }
235                    });
236                })
237                .detach();
238                self.nodes.insert(id.clone(), NodeSlot::Split(e.clone()));
239                e
240            }
241        };
242
243        // Sincronizamos la dirección por si el JSON cambió.
244        entity.update(cx, |s, cx| s.set_direction(direction, cx));
245
246        let slots = self.build_child_slots(cfg, path, used_ids, cx);
247        entity.update(cx, |s, cx| s.set_children(slots, cx));
248
249        AnyView::from(entity)
250    }
251
252    fn build_tabs(
253        &mut self,
254        id: NodeId,
255        cfg: &LayerConfig,
256        path: &str,
257        used_ids: &std::cell::RefCell<Vec<NodeId>>,
258        cx: &mut Context<Self>,
259    ) -> AnyView {
260        let entity = match self.nodes.get(&id) {
261            Some(NodeSlot::Tabs(e)) => e.clone(),
262            _ => {
263                let e = cx.new(|cx| TabContainer::new(cx));
264                self.nodes.insert(id.clone(), NodeSlot::Tabs(e.clone()));
265                e
266            }
267        };
268
269        let slots = self.build_child_slots(cfg, path, used_ids, cx);
270        entity.update(cx, |s, cx| s.set_children(slots, cx));
271
272        AnyView::from(entity)
273    }
274
275    fn build_tiled(
276        &mut self,
277        id: NodeId,
278        cfg: &LayerConfig,
279        path: &str,
280        used_ids: &std::cell::RefCell<Vec<NodeId>>,
281        cx: &mut Context<Self>,
282    ) -> AnyView {
283        let entity = match self.nodes.get(&id) {
284            Some(NodeSlot::Tiled(e)) => e.clone(),
285            _ => {
286                let e = cx.new(|cx| TiledContainer::new(cx));
287                // Drag-to-swap: el TiledContainer emite Reordered cuando
288                // un drag termina sobre otro tile. Lo commiteamos al
289                // model swappeando children del padre — el rebuild
290                // posterior aplicará el nuevo orden preservando los
291                // entities por NodeId.
292                let model = self.model.clone();
293                let parent_id = id.clone();
294                cx.subscribe(&e, move |_, _, ev: &TiledEvent, cx| match ev {
295                    TiledEvent::Reordered {
296                        from_index,
297                        to_index,
298                        ..
299                    } => {
300                        let from = *from_index;
301                        let to = *to_index;
302                        let parent = parent_id.clone();
303                        model.update(cx, |m, cx| m.swap_children(&parent, from, to, cx));
304                    }
305                })
306                .detach();
307                self.nodes.insert(id.clone(), NodeSlot::Tiled(e.clone()));
308                e
309            }
310        };
311
312        let slots = self.build_child_slots(cfg, path, used_ids, cx);
313        entity.update(cx, |s, cx| s.set_children(slots, cx));
314
315        AnyView::from(entity)
316    }
317
318    fn build_tree(&mut self, id: NodeId, cfg: &LayerConfig, cx: &mut Context<Self>) -> AnyView {
319        // Param `dataset` selecciona el stub. Default: "sources".
320        let dataset = cfg
321            .get_param("dataset")
322            .cloned()
323            .unwrap_or_else(|| "sources".to_string());
324
325        let entity = match self.nodes.get(&id) {
326            Some(NodeSlot::Tree(e)) => e.clone(),
327            _ => {
328                let list_id = SharedString::from(format!("tree-{}", id));
329                let dataset_key = SharedString::from(dataset);
330                let e = cx.new(|cx| ManagedTree::new(list_id, dataset_key, cx));
331                self.nodes.insert(id.clone(), NodeSlot::Tree(e.clone()));
332                e
333            }
334        };
335
336        AnyView::from(entity)
337    }
338
339    fn build_file_explorer(
340        &mut self,
341        id: NodeId,
342        cfg: &LayerConfig,
343        cx: &mut Context<Self>,
344    ) -> AnyView {
345        // Param `root` define el path inicial. Default: "." (cwd).
346        let root = cfg
347            .get_param("root")
348            .cloned()
349            .unwrap_or_else(|| ".".to_string());
350
351        let entity = match self.nodes.get(&id) {
352            Some(NodeSlot::FileExplorer(e)) => e.clone(),
353            _ => {
354                let e = cx.new(|cx| FileExplorer::new(root, cx));
355                // Forwarder: cuando el explorer emite eventos tipados, los
356                // traducimos al formato agnóstico del AppBus.
357                let bus = self.bus.clone();
358                cx.subscribe(&e, move |_, _, ev: &FileExplorerEvent, cx| {
359                    let app_ev = match ev {
360                        FileExplorerEvent::FileSelected { path } => {
361                            Some(AppEvent::EntitySelected {
362                                provider: "local_fs".to_string(),
363                                provider_path: None,
364                                id: path.clone(),
365                            })
366                        }
367                        FileExplorerEvent::FileOpened { path } => {
368                            Some(AppEvent::EntityOpened {
369                                provider: "local_fs".to_string(),
370                                provider_path: None,
371                                id: path.clone(),
372                            })
373                        }
374                        FileExplorerEvent::RootChanged { .. } => None,
375                    };
376                    if let Some(ev) = app_ev {
377                        bus.update(cx, |_, cx| cx.emit(ev));
378                    }
379                })
380                .detach();
381                self.nodes
382                    .insert(id.clone(), NodeSlot::FileExplorer(e.clone()));
383                e
384            }
385        };
386        AnyView::from(entity)
387    }
388
389    fn build_database_explorer(
390        &mut self,
391        id: NodeId,
392        cfg: &LayerConfig,
393        cx: &mut Context<Self>,
394    ) -> AnyView {
395        // Param `path` define el .sqlite. Default: "yahweh.db" en cwd.
396        let path = cfg
397            .get_param("path")
398            .cloned()
399            .unwrap_or_else(|| "yahweh.db".to_string());
400
401        let entity = match self.nodes.get(&id) {
402            Some(NodeSlot::DatabaseExplorer(e)) => e.clone(),
403            _ => {
404                let e = cx.new(|cx| DatabaseExplorer::new(path.clone(), cx));
405                // Forwarder al bus; el `provider_path` lleva el path del
406                // .sqlite para que el TextViewer pueda construir su propio
407                // SqliteDataProvider de la misma DB.
408                let bus = self.bus.clone();
409                let db_path = path.clone();
410                cx.subscribe(&e, move |_, _, ev: &DatabaseExplorerEvent, cx| {
411                    let app_ev = match ev {
412                        DatabaseExplorerEvent::EntitySelected { id } => {
413                            Some(AppEvent::EntitySelected {
414                                provider: "sqlite_db".to_string(),
415                                provider_path: Some(db_path.clone()),
416                                id: id.clone(),
417                            })
418                        }
419                        DatabaseExplorerEvent::EntityOpened { id } => {
420                            Some(AppEvent::EntityOpened {
421                                provider: "sqlite_db".to_string(),
422                                provider_path: Some(db_path.clone()),
423                                id: id.clone(),
424                            })
425                        }
426                    };
427                    if let Some(ev) = app_ev {
428                        bus.update(cx, |_, cx| cx.emit(ev));
429                    }
430                })
431                .detach();
432                self.nodes
433                    .insert(id.clone(), NodeSlot::DatabaseExplorer(e.clone()));
434                e
435            }
436        };
437        AnyView::from(entity)
438    }
439
440    fn build_text_viewer(&mut self, id: NodeId, cx: &mut Context<Self>) -> AnyView {
441        let entity = match self.nodes.get(&id) {
442            Some(NodeSlot::TextViewer(e)) => e.clone(),
443            _ => {
444                let bus = self.bus.clone();
445                let e = cx.new(|cx| TextViewer::new(bus, cx));
446                self.nodes
447                    .insert(id.clone(), NodeSlot::TextViewer(e.clone()));
448                e
449            }
450        };
451        AnyView::from(entity)
452    }
453
454    fn build_image_viewer(&mut self, id: NodeId, cx: &mut Context<Self>) -> AnyView {
455        let entity = match self.nodes.get(&id) {
456            Some(NodeSlot::ImageViewer(e)) => e.clone(),
457            _ => {
458                let bus = self.bus.clone();
459                let e = cx.new(|cx| ImageViewer::new(bus, cx));
460                self.nodes
461                    .insert(id.clone(), NodeSlot::ImageViewer(e.clone()));
462                e
463            }
464        };
465        AnyView::from(entity)
466    }
467
468    fn build_status(&mut self, id: NodeId, cx: &mut Context<Self>) -> AnyView {
469        let entity = match self.nodes.get(&id) {
470            Some(NodeSlot::Status(e)) => e.clone(),
471            _ => {
472                let model = self.model.clone();
473                let e = cx.new(|cx| StatusPanel::new(model, cx));
474                self.nodes.insert(id.clone(), NodeSlot::Status(e.clone()));
475                e
476            }
477        };
478        AnyView::from(entity)
479    }
480
481    fn build_placeholder(
482        &mut self,
483        id: NodeId,
484        cfg: &LayerConfig,
485        cx: &mut Context<Self>,
486    ) -> AnyView {
487        // Si ya hay placeholder con esta id pero el kind cambió, lo
488        // recreamos para reflejar el nuevo `kind` en su mensaje.
489        let want_kind = cfg.kind.clone();
490        let create_new = match self.nodes.get(&id) {
491            Some(NodeSlot::Placeholder(e)) => {
492                let same_kind = e.read(cx).kind == want_kind;
493                !same_kind
494            }
495            _ => true,
496        };
497
498        if create_new {
499            let kind_clone = cfg.kind.clone();
500            let e = cx.new(|cx| PlaceholderView::new(kind_clone, cx));
501            self.nodes
502                .insert(id.clone(), NodeSlot::Placeholder(e.clone()));
503            return AnyView::from(e);
504        }
505
506        // Reuso.
507        if let Some(NodeSlot::Placeholder(e)) = self.nodes.get(&id) {
508            return AnyView::from(e.clone());
509        }
510        // Imposible llegar acá si la lógica de arriba está bien, pero
511        // mantenemos el fallback para no panicar en debug builds.
512        let kind_clone = cfg.kind.clone();
513        let e = cx.new(|cx| PlaceholderView::new(kind_clone, cx));
514        self.nodes.insert(id, NodeSlot::Placeholder(e.clone()));
515        AnyView::from(e)
516    }
517}
518
519impl Render for LayoutHost {
520    fn render(&mut self, _w: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
521        // En Fase 3 el árbol se construyó en `new` y queda fijo. Si
522        // root_view es None (ej. config vacío + GC barrió todo), pintamos
523        // un placeholder neutro.
524        match self.root_view.clone() {
525            Some(v) => div().size_full().child(v),
526            None => div()
527                .size_full()
528                .child("(layout vacío — revisar layout.json)"),
529        }
530    }
531}
532
533// =====================================================================
534// PlaceholderView — kind no reconocido
535// =====================================================================
536
537/// View neutra que se instancia para cualquier `kind` que el LayoutHost no
538/// sepa construir. Renderea el `kind` y los params para que sea evidente
539/// qué falta implementar — útil mientras se desarrollan kinds nuevos
540/// (FileExplorer, Tabs, Tiled, etc.).
541pub struct PlaceholderView {
542    kind: String,
543}
544
545impl PlaceholderView {
546    pub fn new(kind: String, cx: &mut Context<Self>) -> Self {
547        cx.observe_global::<yahweh_theme::Theme>(|_, cx| cx.notify())
548            .detach();
549        Self { kind }
550    }
551}
552
553impl Render for PlaceholderView {
554    fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
555        let theme = yahweh_theme::Theme::global(cx).clone();
556        div()
557            .size_full()
558            .bg(theme.bg_panel.clone())
559            .p(gpui::px(16.0))
560            .flex()
561            .flex_col()
562            .gap(gpui::px(6.0))
563            .child(
564                div()
565                    .text_color(theme.accent)
566                    .text_size(gpui::px(14.0))
567                    .child(SharedString::from(format!("⟨ kind: {} ⟩", self.kind))),
568            )
569            .child(
570                div()
571                    .text_color(theme.fg_muted)
572                    .text_size(gpui::px(11.0))
573                    .child("(placeholder — kind no implementado todavía)"),
574            )
575    }
576}