⎇ 53dbdf0f1deea10d6fc567ea2b43e1e75d6d60cd
crates/apps/yahweh-shell/src/layout_host.rs
History
Who touched what
raw
chore: monorepo inicial con arje + minga + yahweh absorbidos
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}