feat: llimphi standalone — framework UI soberano extraído del monorepo

Motor gráfico Llimphi como workspace independiente: bucle Elm
(input→update→view→layout→raster→present) sobre wgpu+vello+taffy+parley.
Núcleo (hal/raster/layout/text/ui/theme/surface/motion/icons) + ~40 widgets
+ módulos, sin dependencias al resto del monorepo. cargo check --workspace
pasa (64 crates). Puerta de entrada: cargo run -p llimphi-ui --example counter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 04:23:42 +00:00
commit e65e9cc623
286 changed files with 46136 additions and 0 deletions
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "llimphi-widget-dock-rail"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "llimphi-widget-dock-rail — rail vertical de dientes (pestañas con barra de acento + icono) para sidebars acoplables; clic activa, arrastre mueve entre rails."
[dependencies]
llimphi-ui = { workspace = true }
llimphi-theme = { workspace = true }
+184
View File
@@ -0,0 +1,184 @@
//! `llimphi-widget-dock-rail` — rail vertical de **dientes** para
//! sidebars acoplables.
//!
//! Cada diente es una pestaña vertical: una **barra de acento** de 3px
//! pegada al borde interno (encendida cuando el item está activo) + un
//! **icono** centrado. Los dientes apilan en una columna redondeada que
//! se pinta como una franja flotante al borde del centro — el patrón del
//! dock de cosmos, ahora reutilizable.
//!
//! Render-only y agnóstico del `Msg`: el item se identifica por un `u64`
//! opaco. El clic (en el *press*, para no pelear con el arrastre) activa
//! vía `on_activate(id)`; el diente es **arrastrable** con su `id` como
//! payload, y el rail entero es **drop target** (`on_drop(payload)`) —
//! así una app puede mover un diente de un sidebar a otro soltándolo
//! sobre el rail opuesto. El icono lo dibuja el caller (`make_icon`), que
//! recibe el color ya resuelto según el estado activo/inactivo.
//!
//! ```ignore
//! let items = [DockRailItem { id: 0, active: true }, DockRailItem { id: 1, active: false }];
//! dock_rail_view(
//! &items,
//! 44.0,
//! &DockRailPalette::from_theme(&theme),
//! |id, size, color| my_icon_view(id, size, color),
//! |id| Msg::DockActivate(side, id),
//! move |payload| Some(Msg::DockDrop(side, payload)),
//! )
//! ```
#![forbid(unsafe_code)]
use llimphi_ui::llimphi_layout::taffy::{
prelude::{auto, length, percent, FlexDirection, Size, Style},
AlignItems, JustifyContent,
};
use llimphi_ui::llimphi_raster::peniko::Color;
use llimphi_ui::{DragPhase, View};
use llimphi_theme::Theme;
/// Alto de cada diente (px).
const TOOTH_H: f32 = 42.0;
/// Alto de la barra de acento (px) — un poco menor que el diente para
/// dejar aire arriba y abajo.
const BAR_H: f32 = 40.0;
/// Tamaño del icono que se le pide al caller (px).
const ICON_PX: f32 = 20.0;
/// Paleta del rail.
#[derive(Debug, Clone, Copy)]
pub struct DockRailPalette {
/// Fondo de la franja del rail.
pub bg_rail: Color,
/// Fondo del diente activo.
pub bg_active: Color,
/// Fondo al hover (diente) y al sobrevolar un drop válido (rail).
pub bg_hover: Color,
/// Color del acento: barra del diente activo + su icono.
pub accent: Color,
/// Color del icono de un diente inactivo.
pub icon_inactive: Color,
}
impl DockRailPalette {
pub fn from_theme(t: &Theme) -> Self {
Self {
bg_rail: t.bg_panel_alt,
bg_active: t.bg_selected,
bg_hover: t.bg_row_hover,
accent: t.accent,
icon_inactive: t.fg_muted,
}
}
}
/// Un diente del rail: su id opaco y si está activo.
#[derive(Debug, Clone, Copy)]
pub struct DockRailItem {
pub id: u64,
pub active: bool,
}
/// Construye el rail de dientes.
///
/// - `items`: en el orden en que se quieren mostrar (el widget no
/// reordena).
/// - `width`: ancho de la franja del rail (px).
/// - `make_icon(id, size, color)`: dibuja el icono del item con el color
/// ya resuelto (acento si activo, atenuado si no).
/// - `on_activate(id)`: `Msg` al clickear (en el press).
/// - `on_drop(payload)`: `Msg` opcional cuando se suelta un diente
/// (cualquier `id`) sobre este rail.
pub fn dock_rail_view<Msg, FIcon, FAct, FDrop>(
items: &[DockRailItem],
width: f32,
palette: &DockRailPalette,
make_icon: FIcon,
on_activate: FAct,
on_drop: FDrop,
) -> View<Msg>
where
Msg: Clone + Send + Sync + 'static,
FIcon: Fn(u64, f32, Color) -> View<Msg>,
FAct: Fn(u64) -> Msg,
FDrop: Fn(u64) -> Option<Msg> + Send + Sync + 'static,
{
let mut teeth: Vec<View<Msg>> = Vec::with_capacity(items.len());
for item in items {
let fg = if item.active {
palette.accent
} else {
palette.icon_inactive
};
// Barra de acento, pegada al borde interno.
let accent_bar = {
let b = View::new(Style {
size: Size {
width: length(3.0_f32),
height: length(BAR_H),
},
flex_shrink: 0.0,
..Default::default()
});
if item.active {
b.fill(palette.accent).radius(2.0)
} else {
b
}
};
let icon_box = View::new(Style {
flex_grow: 1.0,
size: Size {
width: percent(0.0_f32),
height: length(TOOTH_H),
},
align_items: Some(AlignItems::Center),
justify_content: Some(JustifyContent::Center),
..Default::default()
})
.children(vec![make_icon(item.id, ICON_PX, fg)]);
let id = item.id;
let msg = on_activate(id);
let mut tooth = View::new(Style {
flex_direction: FlexDirection::Row,
size: Size {
width: percent(1.0_f32),
height: length(TOOTH_H),
},
flex_shrink: 0.0,
align_items: Some(AlignItems::Center),
..Default::default()
})
.hover_fill(palette.bg_hover)
// Click en el press activa; arrastrar mueve de rail (payload=id).
.on_click_at(move |_, _, _, _| Some(msg.clone()))
.draggable_at(|phase, _, _, _, _| match phase {
DragPhase::Move | DragPhase::End => None,
})
.drag_payload(id)
.children(vec![accent_bar, icon_box]);
if item.active {
tooth = tooth.fill(palette.bg_active);
}
teeth.push(tooth);
}
// La franja: sólo del alto de los dientes (el hueco de abajo lo deja
// libre para que el área central lo aproveche si el rail flota como
// overlay). Es además el drop target del lado.
View::new(Style {
flex_direction: FlexDirection::Column,
size: Size {
width: length(width),
height: auto(),
},
flex_shrink: 0.0,
..Default::default()
})
.fill(palette.bg_rail)
.radius(5.0)
.on_drop(on_drop)
.drop_hover_fill(palette.bg_hover)
.children(teeth)
}