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:
Sergio
2026-05-08 04:45:44 +00:00
commit 53dbdf0f1d
176 changed files with 34845 additions and 0 deletions
@@ -0,0 +1,362 @@
//! `yahweh_widget_splitter` — `SplitContainer` genérico.
//!
//! Aloja `n` hijos `AnyView` con flex weights individuales y un divisor
//! arrastrable entre cada par adyacente. Dirección horizontal o vertical
//! intercambiable. Emite [`SplitEvent::FlexChanged`] cuando un drag termina,
//! para que el host (LayoutHost / DemoApp) persista los flex.
//!
//! El SplitContainer NO conoce a sus hijos: los recibe vía
//! `set_children(Vec<ChildSlot>)`. Eso permite que el LayoutHost reuse las
//! mismas instancias cuando el JSON cambia el `kind` del contenedor (Split
//! → Tabs → Tiled) — los AnyView siguen vivos, solo cambia su contenedor.
//!
//! Drag: usamos el patrón canónico de gpui (ver `data_table.rs` ejemplo) —
//! cada divider tiene un `canvas(prepaint, paint)` que en su paint registra
//! handlers de `MouseDown / MouseMove / MouseUp` a nivel de window vía
//! `window.on_mouse_event`. Esto garantiza que el drag continúa aunque el
//! cursor salga del divider.
use std::cell::RefCell;
use std::rc::Rc;
use gpui::{
App, Bounds, Context, EventEmitter, IntoElement, Length, MouseButton, MouseDownEvent,
MouseMoveEvent, MouseUpEvent, Pixels, Point, Render, Window, canvas, div, prelude::*, px,
};
use yahweh_core::{LayoutDirection, NodeId};
use yahweh_theme::Theme;
pub use yahweh_widget_container_core::ChildSlot;
#[derive(Clone, Debug)]
pub enum SplitEvent {
/// Un drag actualizó los flex weights. Se emite UNA vez por movimiento
/// (cada frame durante un drag), con los IDs y flex finales de los dos
/// hijos adyacentes al divisor.
FlexChanged {
left_id: NodeId,
right_id: NodeId,
left_flex: f32,
right_flex: f32,
},
/// El drag terminó (mouseup). Útil para persistir batched.
DragEnd,
}
// =====================================================================
// Widget
// =====================================================================
/// Estado interno del drag activo. `divider_index` apunta al espacio entre
/// `children[i]` y `children[i+1]`. Los snapshots `flex_*_initial` y
/// `start_pos_main` se capturan en MouseDown — durante MouseMove se
/// recalcula el flex desde el delta.
struct DragState {
divider_index: usize,
start_pos_main: Pixels,
flex_left_initial: f32,
flex_right_initial: f32,
/// Longitud total del SplitContainer en el eje principal al iniciar el
/// drag (capturada de `bounds`). Usada para convertir delta_px ↔
/// delta_flex preservando el sum total.
total_main_size: Pixels,
total_flex_initial: f32,
}
pub struct SplitContainer {
children: Vec<ChildSlot>,
direction: LayoutDirection,
drag: Option<DragState>,
/// Bounds del frame anterior. Capturados vía canvas absolute en cada
/// paint. Lo usamos al iniciar drag para resolver `total_main_size`.
bounds: Rc<RefCell<Option<Bounds<Pixels>>>>,
}
impl EventEmitter<SplitEvent> for SplitContainer {}
impl SplitContainer {
pub fn new(direction: LayoutDirection, cx: &mut Context<Self>) -> Self {
cx.observe_global::<Theme>(|_, cx| cx.notify()).detach();
Self {
children: Vec::new(),
direction,
drag: None,
bounds: Rc::new(RefCell::new(None)),
}
}
pub fn set_children(&mut self, children: Vec<ChildSlot>, cx: &mut Context<Self>) {
self.children = children;
cx.notify();
}
pub fn set_direction(&mut self, direction: LayoutDirection, cx: &mut Context<Self>) {
if self.direction != direction {
self.direction = direction;
cx.notify();
}
}
pub fn direction(&self) -> LayoutDirection {
self.direction
}
pub fn children(&self) -> &[ChildSlot] {
&self.children
}
// -------- Drag handlers --------
fn start_drag(&mut self, divider_index: usize, position: Point<Pixels>) {
if divider_index >= self.children.len().saturating_sub(1) {
return;
}
let bounds = match *self.bounds.borrow() {
Some(b) => b,
None => return,
};
let raw_main = main_axis(self.direction, bounds.size.width, bounds.size.height);
// Restamos el espacio que ocupan los divisores — son fixed-size en el
// eje principal, no participan del flex. El "espacio disponible
// para flex" es lo que importa para convertir delta_px → delta_flex.
let dividers_total = px(DIVIDER_THICKNESS) * (self.children.len().saturating_sub(1) as f32);
let total_main = raw_main - dividers_total;
if total_main <= px(0.0) {
return;
}
let total_flex: f32 = self.children.iter().map(|c| c.flex.max(0.0)).sum();
let total_flex = total_flex.max(0.001);
let start_main = main_axis_pt(self.direction, position);
self.drag = Some(DragState {
divider_index,
start_pos_main: start_main,
flex_left_initial: self.children[divider_index].flex,
flex_right_initial: self.children[divider_index + 1].flex,
total_main_size: total_main,
total_flex_initial: total_flex,
});
}
fn continue_drag(&mut self, position: Point<Pixels>, cx: &mut Context<Self>) {
let Some(drag) = &self.drag else { return };
let drag_idx = drag.divider_index;
if drag_idx + 1 >= self.children.len() {
return;
}
let cur_main = main_axis_pt(self.direction, position);
let delta_px = cur_main - drag.start_pos_main;
// delta_flex = delta_px / total_main_size * total_flex_initial.
let total_main_f = f32::from(drag.total_main_size).max(1.0);
let delta_flex = (f32::from(delta_px) / total_main_f) * drag.total_flex_initial;
const MIN_FLEX: f32 = 0.05;
let new_left = (drag.flex_left_initial + delta_flex).max(MIN_FLEX);
let new_right = (drag.flex_right_initial - delta_flex).max(MIN_FLEX);
// Solo aplicamos si NINGUNO se aplastó al mínimo y se "comió" el
// delta — eso significa que el drag llegó al borde de un hijo.
let fits = (drag.flex_left_initial + delta_flex) >= MIN_FLEX
&& (drag.flex_right_initial - delta_flex) >= MIN_FLEX;
if !fits {
// Recortamos: aplicamos los mínimos pero no propagamos delta más
// allá del límite. Resultado: el divisor "frena" en el borde.
}
self.children[drag_idx].flex = new_left;
self.children[drag_idx + 1].flex = new_right;
let left_id = self.children[drag_idx].id.clone();
let right_id = self.children[drag_idx + 1].id.clone();
cx.emit(SplitEvent::FlexChanged {
left_id,
right_id,
left_flex: new_left,
right_flex: new_right,
});
cx.notify();
}
fn end_drag(&mut self, cx: &mut Context<Self>) {
if self.drag.take().is_some() {
cx.emit(SplitEvent::DragEnd);
cx.notify();
}
}
}
// =====================================================================
// Helpers de eje
// =====================================================================
fn main_axis(dir: LayoutDirection, w: Pixels, h: Pixels) -> Pixels {
match dir {
LayoutDirection::Horizontal => w,
_ => h,
}
}
fn main_axis_pt(dir: LayoutDirection, p: Point<Pixels>) -> Pixels {
match dir {
LayoutDirection::Horizontal => p.x,
_ => p.y,
}
}
// =====================================================================
// Render
// =====================================================================
const DIVIDER_THICKNESS: f32 = 4.0;
impl Render for SplitContainer {
fn render(&mut self, _w: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let theme = Theme::global(cx).clone();
let direction = self.direction;
let entity = cx.entity();
let bounds_holder = self.bounds.clone();
let total_flex: f32 = self
.children
.iter()
.map(|c| c.flex.max(0.0))
.sum::<f32>()
.max(0.001);
// Root flex container.
let mut root = div().size_full().relative();
root = match direction {
LayoutDirection::Horizontal => root.flex().flex_row(),
_ => root.flex().flex_col(),
};
// Canvas absolute para capturar bounds del SplitContainer en cada
// frame. No participa del flex (absolute), no captura clicks
// (canvas sin id es no-interactivo).
root = root.child({
let bounds_holder = bounds_holder.clone();
canvas(
move |bounds, _w, _cx| {
*bounds_holder.borrow_mut() = Some(bounds);
},
|_, _, _, _| {},
)
.absolute()
.size_full()
});
// Children + dividers entre cada par.
let n = self.children.len();
for (i, child) in self.children.iter().enumerate() {
let weight = (child.flex.max(0.0) / total_flex).max(0.001);
let mut item = div().relative();
// flex_grow fraccional — el helper `flex_grow()` solo setea 1.0,
// así que vamos directo al campo subyacente para repartir
// proporcionalmente según el `flex` de cada slot.
item.style().flex_grow = Some(weight);
item.style().flex_shrink = Some(1.0);
// CRUCIAL: el default de flexbox es `min-width: auto` (= min
// content size). Si no lo aplastamos a 0, taffy clamp-ea al
// tamaño mínimo del contenido (un TreeView con label largo, un
// uniform_list, etc.) y el divisor no puede pasar de ese punto
// — el cursor avanza pero el divisor se queda. Forzando min=0
// y overflow:hidden en el wrapper, el child puede shrink-arse a
// donde sea y el contenido se recorta.
item.style().min_size.width = Some(Length::Definite(px(0.0).into()));
item.style().min_size.height = Some(Length::Definite(px(0.0).into()));
// Eje cruzado: full. Eje principal: lo decide flex.
let item = match direction {
LayoutDirection::Horizontal => item.h_full(),
_ => item.w_full(),
}
.overflow_hidden()
.child(child.view.clone());
root = root.child(item);
// Divisor entre i e i+1 (no después del último).
if i + 1 < n {
let divider_idx = i;
let entity_for_canvas = entity.clone();
let mut divider = div();
let divider_bg = if self.drag.as_ref().map(|d| d.divider_index) == Some(divider_idx)
{
theme.accent_strong
} else {
theme.border_strong
};
divider = divider.bg(divider_bg).hover(|s| s.bg(theme.accent));
divider = match direction {
LayoutDirection::Horizontal => divider
.w(px(DIVIDER_THICKNESS))
.h_full()
.cursor_ew_resize(),
_ => divider
.w_full()
.h(px(DIVIDER_THICKNESS))
.cursor_ns_resize(),
};
// Canvas con handlers de drag a nivel de window.
let divider = divider.child(
canvas(
|_, _, _| (),
move |canvas_bounds: Bounds<Pixels>, _, window, _| {
// MouseDown sobre el divisor → start_drag.
window.on_mouse_event({
let entity = entity_for_canvas.clone();
move |ev: &MouseDownEvent, _, _w: &mut Window, cx: &mut App| {
if ev.button != MouseButton::Left {
return;
}
if !canvas_bounds.contains(&ev.position) {
return;
}
entity.update(cx, |this, _| {
this.start_drag(divider_idx, ev.position);
});
}
});
// MouseMove anywhere → continue_drag si hay drag.
window.on_mouse_event({
let entity = entity_for_canvas.clone();
move |ev: &MouseMoveEvent, _, _w: &mut Window, cx: &mut App| {
if !ev.dragging() {
return;
}
entity.update(cx, |this, cx| {
if this.drag.is_some() {
this.continue_drag(ev.position, cx);
}
});
}
});
// MouseUp anywhere → end_drag.
window.on_mouse_event({
let entity = entity_for_canvas.clone();
move |_: &MouseUpEvent, _, _w: &mut Window, cx: &mut App| {
entity.update(cx, |this, cx| this.end_drag(cx));
}
});
},
)
.size_full(),
);
root = root.child(divider);
}
}
root
}
}