chore: monorepo inicial con arje + minga + yahweh absorbidos
Workspace en 4 ejes (core/modules/apps/shared):
- core/: 24 crates de arje (Init systemd-compatible: ente-card, ente-zero,
ente-kernel, ente-bus, ente-cas, ente-soma, ente-wasm, ente-snapshot,
ente-brain, ente-echo, ente-policy-provider, + 12 crates *-compat)
- modules/semantic_dht/: 5 crates de minga (minga-core con AST/CAS/MST,
minga-p2p con libp2p Kad, minga-store, minga-vfs, minga-cli)
- modules/ui_engine/: 11 crates de yahweh (libs/{core,theme,bus,providers},
widgets/{tree,splitter,tabs,tiled,container_core,text_input})
- apps/: 5 crates de yahweh (file_explorer, database_explorer, text_viewer,
image_viewer, yahweh-shell)
- shared_wit/protocol.wit: handshake/lifecycle inicial
Cargo.toml unificado: thiserror bumped a 2 (transparente para arje), tokio
"full", paths intra-workspace de yahweh redirigidos a su nueva ubicación.
cargo check --workspace: 0 errores, 17 warnings (dead code preexistente).
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,576 @@
|
||||
//! `LayoutHost` — orquestador del layout dinámico.
|
||||
//!
|
||||
//! Lee un `LayerConfig` (raíz del JSON) y construye un árbol de entidades
|
||||
//! GPUI dispatch-eando por `kind`:
|
||||
//!
|
||||
//! | kind | factory |
|
||||
//! |-------------|----------------------------------------------|
|
||||
//! | "Split" | `SplitContainer` con dirección + flex |
|
||||
//! | "Tree" | `ManagedTree` con dataset stub |
|
||||
//! | "Status" | `StatusPanel` |
|
||||
//! | (otro) | placeholder textual con el kind y params |
|
||||
//!
|
||||
//! Cada entidad se memoiza por `NodeId` (string opcional del JSON o el path
|
||||
//! sintético `root/child/0`). Mientras el `id` no cambie entre rebuilds, la
|
||||
//! misma instancia se reusa — esto es lo que permite swappear el `kind` de
|
||||
//! un container (Split → Tabs → Tiled) preservando los hijos sin reset.
|
||||
//!
|
||||
//! En Fase 3 el `LayoutHost` solo reconstruye al inicio. La observación del
|
||||
//! `LayoutModel` (hot-reload del JSON, mutaciones desde la UI) entra en
|
||||
//! Fase 7. Pero el diseño ya soporta `rebuild()` idempotente — se invoca
|
||||
//! cuando el modelo cambia y la memoización mantiene los hijos vivos.
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use gpui::{
|
||||
AnyView, Context, Entity, IntoElement, Render, SharedString, Window, div, prelude::*,
|
||||
};
|
||||
|
||||
use yahweh_bus::{AppBus, AppEvent};
|
||||
use yahweh_core::{LayerConfig, LayoutDirection, NodeId};
|
||||
use yahweh_database_explorer::{DatabaseExplorer, DatabaseExplorerEvent};
|
||||
use yahweh_file_explorer::{FileExplorer, FileExplorerEvent};
|
||||
use yahweh_image_viewer::ImageViewer;
|
||||
use yahweh_text_viewer::TextViewer;
|
||||
use yahweh_widget_container_core::ChildSlot;
|
||||
use yahweh_widget_splitter::{SplitContainer, SplitEvent};
|
||||
use yahweh_widget_tabs::TabContainer;
|
||||
use yahweh_widget_tiled::{TiledContainer, TiledEvent};
|
||||
|
||||
use crate::layout_model::{LayoutModel, LayoutModelEvent};
|
||||
use crate::managed_tree::ManagedTree;
|
||||
use crate::persister::Persister;
|
||||
use crate::status_panel::StatusPanel;
|
||||
|
||||
// =====================================================================
|
||||
// LayoutHost
|
||||
// =====================================================================
|
||||
|
||||
pub struct LayoutHost {
|
||||
/// Modelo observable. Cualquier mutación (set_kind, replace_tree)
|
||||
/// dispara un rebuild idempotente que preserva memoización.
|
||||
model: Entity<LayoutModel>,
|
||||
|
||||
/// Bus app-level. Lo distribuimos a viewers (que se subscriben para
|
||||
/// reaccionar a EntitySelected/Opened) y forwardeamos los eventos
|
||||
/// tipados de explorers hacia él.
|
||||
bus: Entity<AppBus>,
|
||||
|
||||
/// Persister adoptado — el LayoutHost mantiene su Entity vivo. Sin
|
||||
/// strong handle el entity se dropea y la subscripción al model
|
||||
/// queda inactiva.
|
||||
#[allow(dead_code)]
|
||||
persister: Entity<Persister>,
|
||||
|
||||
/// Memoización de instancias por NodeId. Cada slot guarda la entidad
|
||||
/// tipada para poder llamarle métodos específicos (set_children en
|
||||
/// containers, etc.). Se preserva entre rebuilds — eso es lo que
|
||||
/// permite swappear kind del padre sin perder los hijos.
|
||||
nodes: HashMap<NodeId, NodeSlot>,
|
||||
|
||||
/// La AnyView raíz, computada por el último `rebuild`. El render solo
|
||||
/// pinta este handle.
|
||||
root_view: Option<AnyView>,
|
||||
}
|
||||
|
||||
/// Una entidad concreta instanciada en el árbol. Distinguimos por variante
|
||||
/// porque cada tipo tiene una API distinta para reaccionar a actualizaciones
|
||||
/// (set_children para Split, etc.). Las factories devuelven `AnyView`
|
||||
/// directamente (`AnyView::from(entity.clone())`); esta enum solo guarda
|
||||
/// la handle tipada para llamadas futuras.
|
||||
enum NodeSlot {
|
||||
Split(Entity<SplitContainer>),
|
||||
Tabs(Entity<TabContainer>),
|
||||
Tiled(Entity<TiledContainer>),
|
||||
Tree(Entity<ManagedTree>),
|
||||
FileExplorer(Entity<FileExplorer>),
|
||||
DatabaseExplorer(Entity<DatabaseExplorer>),
|
||||
TextViewer(Entity<TextViewer>),
|
||||
ImageViewer(Entity<ImageViewer>),
|
||||
Status(Entity<StatusPanel>),
|
||||
Placeholder(Entity<PlaceholderView>),
|
||||
}
|
||||
|
||||
impl LayoutHost {
|
||||
pub fn new(
|
||||
model: Entity<LayoutModel>,
|
||||
bus: Entity<AppBus>,
|
||||
persister: Entity<Persister>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
// Subscripción event-filtered: solo rebuildeamos en cambios
|
||||
// estructurales (set_kind, replace_tree). FlexChanged proviene de
|
||||
// drags de divisor — el splitter ya tiene el flex aplicado en su
|
||||
// Vec, rebuildear lo resetearía y rompería el drag.
|
||||
cx.subscribe(&model, |this, _, ev: &LayoutModelEvent, cx| match ev {
|
||||
LayoutModelEvent::StructureChanged => this.rebuild(cx),
|
||||
LayoutModelEvent::FlexChanged => {}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut me = Self {
|
||||
model,
|
||||
bus,
|
||||
persister,
|
||||
nodes: HashMap::new(),
|
||||
root_view: None,
|
||||
};
|
||||
me.rebuild(cx);
|
||||
me
|
||||
}
|
||||
|
||||
/// Rebuild idempotente: walk del árbol del model, instancia (o reusa)
|
||||
/// cada nodo, propaga children a los containers.
|
||||
pub fn rebuild(&mut self, cx: &mut Context<Self>) {
|
||||
// Snapshot del config para no chocar con el borrow al iterar +
|
||||
// mutar self.nodes.
|
||||
let cfg = self.model.read(cx).tree().clone();
|
||||
let used_ids = std::cell::RefCell::new(Vec::new());
|
||||
let view = self.build_node(&cfg, "root", &used_ids, cx);
|
||||
|
||||
// GC: tirar nodos cuyo id ya no aparece en el árbol nuevo.
|
||||
let used: std::collections::HashSet<NodeId> =
|
||||
used_ids.into_inner().into_iter().collect();
|
||||
self.nodes.retain(|id, _| used.contains(id));
|
||||
|
||||
self.root_view = Some(view);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
/// DFS recursivo. `path` se acumula para los nodos que no traen `id`
|
||||
/// propio en el JSON (`root/0/1` etc) — la sintetización vive en
|
||||
/// `NodeId::from_layer`.
|
||||
fn build_node(
|
||||
&mut self,
|
||||
cfg: &LayerConfig,
|
||||
path: &str,
|
||||
used_ids: &std::cell::RefCell<Vec<NodeId>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyView {
|
||||
let id = NodeId::from_layer(cfg, path);
|
||||
used_ids.borrow_mut().push(id.clone());
|
||||
|
||||
match cfg.kind.as_str() {
|
||||
"Split" => self.build_split(id, cfg, path, used_ids, cx),
|
||||
"Tabs" => self.build_tabs(id, cfg, path, used_ids, cx),
|
||||
"Tiled" => self.build_tiled(id, cfg, path, used_ids, cx),
|
||||
"Tree" => self.build_tree(id, cfg, cx),
|
||||
"FileExplorer" => self.build_file_explorer(id, cfg, cx),
|
||||
"DatabaseExplorer" => self.build_database_explorer(id, cfg, cx),
|
||||
"TextViewer" => self.build_text_viewer(id, cx),
|
||||
"ImageViewer" => self.build_image_viewer(id, cx),
|
||||
"Status" => self.build_status(id, cx),
|
||||
_ => self.build_placeholder(id, cfg, cx),
|
||||
}
|
||||
}
|
||||
|
||||
/// Helper común — construye los `ChildSlot`s de un contenedor haciendo
|
||||
/// recursión sobre los hijos del JSON. Usado por Split / Tabs / Tiled.
|
||||
fn build_child_slots(
|
||||
&mut self,
|
||||
cfg: &LayerConfig,
|
||||
path: &str,
|
||||
used_ids: &std::cell::RefCell<Vec<NodeId>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<ChildSlot> {
|
||||
let mut slots = Vec::with_capacity(cfg.children.len());
|
||||
for (i, child) in cfg.children.iter().enumerate() {
|
||||
let child_path = format!("{}/{}", path, i);
|
||||
let child_view = self.build_node(child, &child_path, used_ids, cx);
|
||||
slots.push(ChildSlot {
|
||||
id: NodeId::from_layer(child, &child_path),
|
||||
flex: child.flex_weight() as f32,
|
||||
label: child.get_param("label").cloned(),
|
||||
view: child_view,
|
||||
});
|
||||
}
|
||||
slots
|
||||
}
|
||||
|
||||
// ------- factories por kind -------
|
||||
|
||||
fn build_split(
|
||||
&mut self,
|
||||
id: NodeId,
|
||||
cfg: &LayerConfig,
|
||||
path: &str,
|
||||
used_ids: &std::cell::RefCell<Vec<NodeId>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyView {
|
||||
let direction = match cfg.layout_direction() {
|
||||
LayoutDirection::Vertical => LayoutDirection::Vertical,
|
||||
LayoutDirection::Horizontal => LayoutDirection::Horizontal,
|
||||
LayoutDirection::Overlay => LayoutDirection::Vertical, // fallback
|
||||
};
|
||||
|
||||
// Get-or-create — si ya existe del rebuild anterior y es Split, lo
|
||||
// reusamos. Si era de otro tipo, lo descartamos.
|
||||
let entity = match self.nodes.get(&id) {
|
||||
Some(NodeSlot::Split(e)) => e.clone(),
|
||||
_ => {
|
||||
let e = cx.new(|cx| SplitContainer::new(direction, cx));
|
||||
// Suscripción a DragEnd para persistir flex al model.
|
||||
// Usamos el id del Split como ancla para resolver children
|
||||
// por id en el LayerConfig.
|
||||
let model = self.model.clone();
|
||||
let split_node_id = id.clone();
|
||||
cx.subscribe(&e, move |_, split_entity, ev: &SplitEvent, cx| {
|
||||
if !matches!(ev, SplitEvent::DragEnd) {
|
||||
return;
|
||||
}
|
||||
// Snapshot de los flex actuales del splitter.
|
||||
let snapshots: Vec<(NodeId, f32)> = split_entity
|
||||
.read(cx)
|
||||
.children()
|
||||
.iter()
|
||||
.map(|c| (c.id.clone(), c.flex))
|
||||
.collect();
|
||||
let _ = split_node_id; // (queda disponible si en
|
||||
// futuro queremos targetear
|
||||
// el padre directamente).
|
||||
model.update(cx, |m, cx| {
|
||||
for (child_id, flex) in snapshots {
|
||||
m.set_flex(&child_id, flex, cx);
|
||||
}
|
||||
});
|
||||
})
|
||||
.detach();
|
||||
self.nodes.insert(id.clone(), NodeSlot::Split(e.clone()));
|
||||
e
|
||||
}
|
||||
};
|
||||
|
||||
// Sincronizamos la dirección por si el JSON cambió.
|
||||
entity.update(cx, |s, cx| s.set_direction(direction, cx));
|
||||
|
||||
let slots = self.build_child_slots(cfg, path, used_ids, cx);
|
||||
entity.update(cx, |s, cx| s.set_children(slots, cx));
|
||||
|
||||
AnyView::from(entity)
|
||||
}
|
||||
|
||||
fn build_tabs(
|
||||
&mut self,
|
||||
id: NodeId,
|
||||
cfg: &LayerConfig,
|
||||
path: &str,
|
||||
used_ids: &std::cell::RefCell<Vec<NodeId>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyView {
|
||||
let entity = match self.nodes.get(&id) {
|
||||
Some(NodeSlot::Tabs(e)) => e.clone(),
|
||||
_ => {
|
||||
let e = cx.new(|cx| TabContainer::new(cx));
|
||||
self.nodes.insert(id.clone(), NodeSlot::Tabs(e.clone()));
|
||||
e
|
||||
}
|
||||
};
|
||||
|
||||
let slots = self.build_child_slots(cfg, path, used_ids, cx);
|
||||
entity.update(cx, |s, cx| s.set_children(slots, cx));
|
||||
|
||||
AnyView::from(entity)
|
||||
}
|
||||
|
||||
fn build_tiled(
|
||||
&mut self,
|
||||
id: NodeId,
|
||||
cfg: &LayerConfig,
|
||||
path: &str,
|
||||
used_ids: &std::cell::RefCell<Vec<NodeId>>,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyView {
|
||||
let entity = match self.nodes.get(&id) {
|
||||
Some(NodeSlot::Tiled(e)) => e.clone(),
|
||||
_ => {
|
||||
let e = cx.new(|cx| TiledContainer::new(cx));
|
||||
// Drag-to-swap: el TiledContainer emite Reordered cuando
|
||||
// un drag termina sobre otro tile. Lo commiteamos al
|
||||
// model swappeando children del padre — el rebuild
|
||||
// posterior aplicará el nuevo orden preservando los
|
||||
// entities por NodeId.
|
||||
let model = self.model.clone();
|
||||
let parent_id = id.clone();
|
||||
cx.subscribe(&e, move |_, _, ev: &TiledEvent, cx| match ev {
|
||||
TiledEvent::Reordered {
|
||||
from_index,
|
||||
to_index,
|
||||
..
|
||||
} => {
|
||||
let from = *from_index;
|
||||
let to = *to_index;
|
||||
let parent = parent_id.clone();
|
||||
model.update(cx, |m, cx| m.swap_children(&parent, from, to, cx));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
self.nodes.insert(id.clone(), NodeSlot::Tiled(e.clone()));
|
||||
e
|
||||
}
|
||||
};
|
||||
|
||||
let slots = self.build_child_slots(cfg, path, used_ids, cx);
|
||||
entity.update(cx, |s, cx| s.set_children(slots, cx));
|
||||
|
||||
AnyView::from(entity)
|
||||
}
|
||||
|
||||
fn build_tree(&mut self, id: NodeId, cfg: &LayerConfig, cx: &mut Context<Self>) -> AnyView {
|
||||
// Param `dataset` selecciona el stub. Default: "sources".
|
||||
let dataset = cfg
|
||||
.get_param("dataset")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "sources".to_string());
|
||||
|
||||
let entity = match self.nodes.get(&id) {
|
||||
Some(NodeSlot::Tree(e)) => e.clone(),
|
||||
_ => {
|
||||
let list_id = SharedString::from(format!("tree-{}", id));
|
||||
let dataset_key = SharedString::from(dataset);
|
||||
let e = cx.new(|cx| ManagedTree::new(list_id, dataset_key, cx));
|
||||
self.nodes.insert(id.clone(), NodeSlot::Tree(e.clone()));
|
||||
e
|
||||
}
|
||||
};
|
||||
|
||||
AnyView::from(entity)
|
||||
}
|
||||
|
||||
fn build_file_explorer(
|
||||
&mut self,
|
||||
id: NodeId,
|
||||
cfg: &LayerConfig,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyView {
|
||||
// Param `root` define el path inicial. Default: "." (cwd).
|
||||
let root = cfg
|
||||
.get_param("root")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| ".".to_string());
|
||||
|
||||
let entity = match self.nodes.get(&id) {
|
||||
Some(NodeSlot::FileExplorer(e)) => e.clone(),
|
||||
_ => {
|
||||
let e = cx.new(|cx| FileExplorer::new(root, cx));
|
||||
// Forwarder: cuando el explorer emite eventos tipados, los
|
||||
// traducimos al formato agnóstico del AppBus.
|
||||
let bus = self.bus.clone();
|
||||
cx.subscribe(&e, move |_, _, ev: &FileExplorerEvent, cx| {
|
||||
let app_ev = match ev {
|
||||
FileExplorerEvent::FileSelected { path } => {
|
||||
Some(AppEvent::EntitySelected {
|
||||
provider: "local_fs".to_string(),
|
||||
provider_path: None,
|
||||
id: path.clone(),
|
||||
})
|
||||
}
|
||||
FileExplorerEvent::FileOpened { path } => {
|
||||
Some(AppEvent::EntityOpened {
|
||||
provider: "local_fs".to_string(),
|
||||
provider_path: None,
|
||||
id: path.clone(),
|
||||
})
|
||||
}
|
||||
FileExplorerEvent::RootChanged { .. } => None,
|
||||
};
|
||||
if let Some(ev) = app_ev {
|
||||
bus.update(cx, |_, cx| cx.emit(ev));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
self.nodes
|
||||
.insert(id.clone(), NodeSlot::FileExplorer(e.clone()));
|
||||
e
|
||||
}
|
||||
};
|
||||
AnyView::from(entity)
|
||||
}
|
||||
|
||||
fn build_database_explorer(
|
||||
&mut self,
|
||||
id: NodeId,
|
||||
cfg: &LayerConfig,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyView {
|
||||
// Param `path` define el .sqlite. Default: "yahweh.db" en cwd.
|
||||
let path = cfg
|
||||
.get_param("path")
|
||||
.cloned()
|
||||
.unwrap_or_else(|| "yahweh.db".to_string());
|
||||
|
||||
let entity = match self.nodes.get(&id) {
|
||||
Some(NodeSlot::DatabaseExplorer(e)) => e.clone(),
|
||||
_ => {
|
||||
let e = cx.new(|cx| DatabaseExplorer::new(path.clone(), cx));
|
||||
// Forwarder al bus; el `provider_path` lleva el path del
|
||||
// .sqlite para que el TextViewer pueda construir su propio
|
||||
// SqliteDataProvider de la misma DB.
|
||||
let bus = self.bus.clone();
|
||||
let db_path = path.clone();
|
||||
cx.subscribe(&e, move |_, _, ev: &DatabaseExplorerEvent, cx| {
|
||||
let app_ev = match ev {
|
||||
DatabaseExplorerEvent::EntitySelected { id } => {
|
||||
Some(AppEvent::EntitySelected {
|
||||
provider: "sqlite_db".to_string(),
|
||||
provider_path: Some(db_path.clone()),
|
||||
id: id.clone(),
|
||||
})
|
||||
}
|
||||
DatabaseExplorerEvent::EntityOpened { id } => {
|
||||
Some(AppEvent::EntityOpened {
|
||||
provider: "sqlite_db".to_string(),
|
||||
provider_path: Some(db_path.clone()),
|
||||
id: id.clone(),
|
||||
})
|
||||
}
|
||||
};
|
||||
if let Some(ev) = app_ev {
|
||||
bus.update(cx, |_, cx| cx.emit(ev));
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
self.nodes
|
||||
.insert(id.clone(), NodeSlot::DatabaseExplorer(e.clone()));
|
||||
e
|
||||
}
|
||||
};
|
||||
AnyView::from(entity)
|
||||
}
|
||||
|
||||
fn build_text_viewer(&mut self, id: NodeId, cx: &mut Context<Self>) -> AnyView {
|
||||
let entity = match self.nodes.get(&id) {
|
||||
Some(NodeSlot::TextViewer(e)) => e.clone(),
|
||||
_ => {
|
||||
let bus = self.bus.clone();
|
||||
let e = cx.new(|cx| TextViewer::new(bus, cx));
|
||||
self.nodes
|
||||
.insert(id.clone(), NodeSlot::TextViewer(e.clone()));
|
||||
e
|
||||
}
|
||||
};
|
||||
AnyView::from(entity)
|
||||
}
|
||||
|
||||
fn build_image_viewer(&mut self, id: NodeId, cx: &mut Context<Self>) -> AnyView {
|
||||
let entity = match self.nodes.get(&id) {
|
||||
Some(NodeSlot::ImageViewer(e)) => e.clone(),
|
||||
_ => {
|
||||
let bus = self.bus.clone();
|
||||
let e = cx.new(|cx| ImageViewer::new(bus, cx));
|
||||
self.nodes
|
||||
.insert(id.clone(), NodeSlot::ImageViewer(e.clone()));
|
||||
e
|
||||
}
|
||||
};
|
||||
AnyView::from(entity)
|
||||
}
|
||||
|
||||
fn build_status(&mut self, id: NodeId, cx: &mut Context<Self>) -> AnyView {
|
||||
let entity = match self.nodes.get(&id) {
|
||||
Some(NodeSlot::Status(e)) => e.clone(),
|
||||
_ => {
|
||||
let model = self.model.clone();
|
||||
let e = cx.new(|cx| StatusPanel::new(model, cx));
|
||||
self.nodes.insert(id.clone(), NodeSlot::Status(e.clone()));
|
||||
e
|
||||
}
|
||||
};
|
||||
AnyView::from(entity)
|
||||
}
|
||||
|
||||
fn build_placeholder(
|
||||
&mut self,
|
||||
id: NodeId,
|
||||
cfg: &LayerConfig,
|
||||
cx: &mut Context<Self>,
|
||||
) -> AnyView {
|
||||
// Si ya hay placeholder con esta id pero el kind cambió, lo
|
||||
// recreamos para reflejar el nuevo `kind` en su mensaje.
|
||||
let want_kind = cfg.kind.clone();
|
||||
let create_new = match self.nodes.get(&id) {
|
||||
Some(NodeSlot::Placeholder(e)) => {
|
||||
let same_kind = e.read(cx).kind == want_kind;
|
||||
!same_kind
|
||||
}
|
||||
_ => true,
|
||||
};
|
||||
|
||||
if create_new {
|
||||
let kind_clone = cfg.kind.clone();
|
||||
let e = cx.new(|cx| PlaceholderView::new(kind_clone, cx));
|
||||
self.nodes
|
||||
.insert(id.clone(), NodeSlot::Placeholder(e.clone()));
|
||||
return AnyView::from(e);
|
||||
}
|
||||
|
||||
// Reuso.
|
||||
if let Some(NodeSlot::Placeholder(e)) = self.nodes.get(&id) {
|
||||
return AnyView::from(e.clone());
|
||||
}
|
||||
// Imposible llegar acá si la lógica de arriba está bien, pero
|
||||
// mantenemos el fallback para no panicar en debug builds.
|
||||
let kind_clone = cfg.kind.clone();
|
||||
let e = cx.new(|cx| PlaceholderView::new(kind_clone, cx));
|
||||
self.nodes.insert(id, NodeSlot::Placeholder(e.clone()));
|
||||
AnyView::from(e)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for LayoutHost {
|
||||
fn render(&mut self, _w: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
// En Fase 3 el árbol se construyó en `new` y queda fijo. Si
|
||||
// root_view es None (ej. config vacío + GC barrió todo), pintamos
|
||||
// un placeholder neutro.
|
||||
match self.root_view.clone() {
|
||||
Some(v) => div().size_full().child(v),
|
||||
None => div()
|
||||
.size_full()
|
||||
.child("(layout vacío — revisar layout.json)"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// =====================================================================
|
||||
// PlaceholderView — kind no reconocido
|
||||
// =====================================================================
|
||||
|
||||
/// View neutra que se instancia para cualquier `kind` que el LayoutHost no
|
||||
/// sepa construir. Renderea el `kind` y los params para que sea evidente
|
||||
/// qué falta implementar — útil mientras se desarrollan kinds nuevos
|
||||
/// (FileExplorer, Tabs, Tiled, etc.).
|
||||
pub struct PlaceholderView {
|
||||
kind: String,
|
||||
}
|
||||
|
||||
impl PlaceholderView {
|
||||
pub fn new(kind: String, cx: &mut Context<Self>) -> Self {
|
||||
cx.observe_global::<yahweh_theme::Theme>(|_, cx| cx.notify())
|
||||
.detach();
|
||||
Self { kind }
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for PlaceholderView {
|
||||
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let theme = yahweh_theme::Theme::global(cx).clone();
|
||||
div()
|
||||
.size_full()
|
||||
.bg(theme.bg_panel.clone())
|
||||
.p(gpui::px(16.0))
|
||||
.flex()
|
||||
.flex_col()
|
||||
.gap(gpui::px(6.0))
|
||||
.child(
|
||||
div()
|
||||
.text_color(theme.accent)
|
||||
.text_size(gpui::px(14.0))
|
||||
.child(SharedString::from(format!("⟨ kind: {} ⟩", self.kind))),
|
||||
)
|
||||
.child(
|
||||
div()
|
||||
.text_color(theme.fg_muted)
|
||||
.text_size(gpui::px(11.0))
|
||||
.child("(placeholder — kind no implementado todavía)"),
|
||||
)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user