550c98f275
Reorganización física de crates/: - core/ (mezclaba 6 propósitos) se divide en protocol/, init/, runtime/, compat/ - shared/ (3 crates) se redistribuye en protocol/ e init/ - lapaloma (sub-módulo de ui_engine) se promueve a modules/pineal/ Renames de proyectos: - shipote → shuma (runtime de sandboxes) - nouser → akasha (explorador de Mónadas) - yahweh → nahual (motor GPUI, antes ui_engine/) - lapaloma → pineal (data-viz agnóstica) Fraccionamiento UI → core agnóstico: - vista-core (DeckState + snap, 175 LOC, 5 tests verdes) - barra-core (Task + render_html + sanitize, 90 LOC, 5 tests verdes) - vista-web y barra-web ahora son thin DOM bindings Documentación nueva: - 16 SDDs por subdirectorio (≤80 LOC c/u): protocol/init/runtime/compat + 10 módulos + apps/ - docs/STATUS.md con cifras reales por proyecto - docs/ROADMAP.md con plan a finalización (6 hitos, ~6-8 semanas) - CHANGELOG.md particionado en docs/changelog/<proyecto>.md (7 buckets) Automatización: - scripts/reorg.py — script idempotente que: git mv directorios, renombra package names, recomputa path = refs, reescribe imports rust, actualiza workspace Cargo.toml. Soporta --dry-run. - scripts/split-changelog.py — particiona CHANGELOG por componente. Validación: - cargo check --workspace pasa (124 crates + 2 nuevos cores). - 10 tests adicionales (5 en vista-core + 5 en barra-core) verdes. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
577 lines
22 KiB
Rust
577 lines
22 KiB
Rust
//! `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 nahual_bus::{AppBus, AppEvent};
|
|
use nahual_core::{LayerConfig, LayoutDirection, NodeId};
|
|
use nahual_database_explorer::{DatabaseExplorer, DatabaseExplorerEvent};
|
|
use nahual_file_explorer::{FileExplorer, FileExplorerEvent};
|
|
use nahual_image_viewer::ImageViewer;
|
|
use nahual_text_viewer::TextViewer;
|
|
use nahual_widget_container_core::ChildSlot;
|
|
use nahual_widget_splitter::{SplitContainer, SplitEvent};
|
|
use nahual_widget_tabs::TabContainer;
|
|
use nahual_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: "nahual.db" en cwd.
|
|
let path = cfg
|
|
.get_param("path")
|
|
.cloned()
|
|
.unwrap_or_else(|| "nahual.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::<nahual_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 = nahual_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)"),
|
|
)
|
|
}
|
|
}
|