refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel

Re-sincroniza las fuentes desde el monorepo (estaba en vello 0.5/wgpu 24 y con la
estructura vieja de eventloop) y suma el 3D:

- bump del workspace a vello 0.7 / wgpu 27 / parley 0.6, + accesskit 0.24 /
  accesskit_winit 0.33 / vello_hybrid 0.0.9.
- nuevos crates: llimphi-3d (voxels ray-march + mallas en un depth compartido,
  montable dentro de un View 2D vía set_viewport+scissor) y llimphi-voxel
  (world-gen, personajes, director de escenas) + shared/foreign-vox (puente .vox).
- README: sección "Not just 2D — a 3D voxel engine" + GIF (docs/llimphi_voxel.gif).
- excluido modules/allichay (arrastra deps fuera del alcance del front-door).
- cargo check --workspace: verde.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
Sergio
2026-06-18 14:40:00 +00:00
parent e74800d9da
commit ccab39f140
202 changed files with 44034 additions and 1811 deletions
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "llimphi-widget-detail-table"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "llimphi-widget-detail-table — grilla read-only con columnas de ancho flex/fijo y encabezados clicables que ordenan (▲/▼). La vista 'detalle' de un file manager: una fila por nodo, selección resaltada, click de fila y click de encabezado emiten Msg. Stateless; el caller pasa filas ya ordenadas/visibles."
[dependencies]
llimphi-ui = { workspace = true }
llimphi-theme = { workspace = true }
+313
View File
@@ -0,0 +1,313 @@
//! `llimphi-widget-detail-table` — la vista **detalle** de un file manager.
//!
//! Una grilla read-only de columnas (nombre · tamaño · fecha · tipo…) con
//! **encabezados clicables que ordenan**: click en una columna emite
//! `on_sort(col)`; la columna activa muestra una flecha `▲`/`▼`. Cada fila
//! es clicable (selección) y opcionalmente lleva un tinte de acento (para
//! labels/colores, Fase 4.5).
//!
//! Como el resto de los widgets Llimphi es **render-only y stateless**: el
//! orden, el filtro y la selección viven en el `Model` del caller (típicamente
//! un `nahual_source_core::Navigator`); el widget recibe las filas **ya
//! ordenadas y ya filtradas** (igual que `widget-list` recibe sólo la ventana
//! visible) y sólo pinta + avisa.
//!
//! Las columnas declaran su ancho como [`ColWidth::Flex`] (reparte el sobrante
//! proporcionalmente — para la columna nombre) o [`ColWidth::Fixed`] (px
//! constantes — para tamaño/fecha/tipo). Encabezado y filas usan el MISMO
//! reparto, así que las columnas quedan alineadas.
//!
//! ```ignore
//! detail_table_view(
//! DetailSpec {
//! columns: &[Column::flex("Nombre", 1.0), Column::fixed("Tamaño", 90.0).right(),
//! Column::fixed("Modificado", 150.0), Column::fixed("Tipo", 80.0)],
//! rows, // ya ordenadas/filtradas por el caller
//! sort: Some((1, SortDir::Desc)),
//! row_height: 22.0,
//! caption: Some("42 entradas".into()),
//! palette: DetailPalette::from_theme(&theme),
//! },
//! Msg::SortBy, // Fn(usize) -> Msg
//! )
//! ```
#![forbid(unsafe_code)]
use llimphi_ui::llimphi_layout::taffy::{
prelude::{length, percent, Dimension, FlexDirection, Size, Style},
AlignItems, Rect,
};
use llimphi_ui::llimphi_raster::peniko::Color;
use llimphi_ui::llimphi_text::Alignment;
use llimphi_ui::View;
/// Ancho de una columna.
#[derive(Debug, Clone, Copy)]
pub enum ColWidth {
/// Reparte el sobrante proporcionalmente al peso (la columna "nombre").
Flex(f32),
/// Ancho fijo en px (tamaño/fecha/tipo).
Fixed(f32),
}
/// Dirección de orden — sólo para pintar la flecha del encabezado.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SortDir {
Asc,
Desc,
}
impl SortDir {
/// La flecha del encabezado activo.
fn arrow(self) -> &'static str {
match self {
SortDir::Asc => "",
SortDir::Desc => "",
}
}
}
/// Una columna de la grilla: rótulo + ancho + alineación del texto.
#[derive(Debug, Clone)]
pub struct Column {
pub title: String,
pub width: ColWidth,
pub align: Alignment,
}
impl Column {
/// Columna flexible (reparte sobrante). Alineada a la izquierda.
pub fn flex(title: impl Into<String>, weight: f32) -> Self {
Self { title: title.into(), width: ColWidth::Flex(weight), align: Alignment::Start }
}
/// Columna de ancho fijo. Alineada a la izquierda.
pub fn fixed(title: impl Into<String>, px: f32) -> Self {
Self { title: title.into(), width: ColWidth::Fixed(px), align: Alignment::Start }
}
/// Variante alineada a la derecha (números: tamaño).
pub fn right(mut self) -> Self {
self.align = Alignment::End;
self
}
}
/// Una fila de datos. `cells` se aparea posicionalmente con las columnas;
/// celdas de más se ignoran, de menos se pintan vacías.
pub struct DetailRow<Msg> {
pub cells: Vec<String>,
pub selected: bool,
/// Tinte de fila opcional (labels/colores, Fase 4.5). `None` = sin tinte.
pub accent: Option<Color>,
pub on_click: Msg,
}
/// Paleta de la grilla detalle.
#[derive(Debug, Clone, Copy)]
pub struct DetailPalette {
pub bg_panel: Color,
pub bg_header: Color,
pub bg_selected: Color,
pub bg_hover: Color,
pub fg_text: Color,
pub fg_muted: Color,
pub fg_header: Color,
pub accent: Color,
pub border: Color,
}
impl Default for DetailPalette {
fn default() -> Self {
Self::from_theme(&llimphi_theme::Theme::dark())
}
}
impl DetailPalette {
pub fn from_theme(t: &llimphi_theme::Theme) -> Self {
Self {
bg_panel: t.bg_panel,
bg_header: t.bg_panel_alt,
bg_selected: t.bg_selected,
bg_hover: t.bg_row_hover,
fg_text: t.fg_text,
fg_muted: t.fg_muted,
fg_header: t.fg_placeholder,
accent: t.accent,
border: t.border,
}
}
}
/// Especificación de la grilla. Las `rows` vienen YA ordenadas y filtradas
/// por el caller; `sort` es sólo para la flecha del encabezado.
pub struct DetailSpec<'a, Msg> {
pub columns: &'a [Column],
pub rows: Vec<DetailRow<Msg>>,
/// Columna activa de orden + su dirección (para la flecha). `None` = sin
/// indicador.
pub sort: Option<(usize, SortDir)>,
pub row_height: f32,
pub caption: Option<String>,
pub palette: DetailPalette,
}
/// Compone la grilla detalle. `on_sort(col)` se emite al clickear un
/// encabezado.
pub fn detail_table_view<Msg, FSort>(spec: DetailSpec<Msg>, on_sort: FSort) -> View<Msg>
where
Msg: Clone + 'static,
FSort: Fn(usize) -> Msg + Clone + 'static,
{
let DetailSpec { columns, rows, sort, row_height, caption, palette } = spec;
let mut children: Vec<View<Msg>> = Vec::with_capacity(rows.len() + 2);
// Caption opcional (conteo).
if let Some(text) = caption {
children.push(
View::new(Style {
size: Size { width: percent(1.0_f32), height: length(18.0_f32) },
padding: pad_lr(10.0),
align_items: Some(AlignItems::Center),
..Default::default()
})
.text_aligned(text, 10.0, palette.fg_muted, Alignment::Start),
);
}
// Encabezado: una celda clicable por columna.
let header_cells: Vec<View<Msg>> = columns
.iter()
.enumerate()
.map(|(i, col)| {
let activa = sort.map(|(c, _)| c == i).unwrap_or(false);
let flecha = match sort {
Some((c, dir)) if c == i => dir.arrow(),
_ => "",
};
let label = format!("{}{flecha}", col.title);
let fg = if activa { palette.fg_header } else { palette.fg_header };
col_cell(
col.width,
View::new(full())
.text_aligned(label, 10.5, fg, col.align)
.ellipsis(1),
)
.hover_fill(palette.bg_hover)
.on_click(on_sort(i))
})
.collect();
children.push(
row_box(header_height(row_height))
.fill(palette.bg_header)
.children(header_cells),
);
// Filas de datos.
for row in rows {
let DetailRow { cells, selected, accent, on_click } = row;
let bg = if selected { palette.bg_selected } else { palette.bg_panel };
let cell_views: Vec<View<Msg>> = columns
.iter()
.enumerate()
.map(|(i, col)| {
let text = cells.get(i).cloned().unwrap_or_default();
// La primera columna (nombre) lleva el acento si hay; el resto
// va en fg_muted salvo el nombre que va en fg_text.
let fg = if i == 0 {
accent.unwrap_or(palette.fg_text)
} else {
palette.fg_muted
};
col_cell(
col.width,
View::new(full())
.text_aligned(text, 11.5, fg, col.align)
.ellipsis(1),
)
})
.collect();
children.push(
row_box(row_height)
.fill(bg)
.hover_fill(palette.bg_hover)
.on_click(on_click)
.children(cell_views),
);
}
View::new(Style {
flex_direction: FlexDirection::Column,
size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
padding: Rect {
left: length(0.0_f32),
right: length(0.0_f32),
top: length(0.0_f32),
bottom: length(6.0_f32),
},
..Default::default()
})
.fill(palette.bg_panel)
.clip(true)
.children(children)
}
/// Alto del encabezado: como una fila pero un toque más bajo, con piso.
fn header_height(row_height: f32) -> f32 {
(row_height - 2.0).max(18.0)
}
/// Una fila horizontal de alto fijo (encabezado o registro). El caller le
/// agrega `.fill`/`.on_click`/`.children`.
fn row_box<Msg: Clone + 'static>(height: f32) -> View<Msg> {
View::new(Style {
flex_direction: FlexDirection::Row,
size: Size { width: percent(1.0_f32), height: length(height) },
padding: pad_lr(8.0),
align_items: Some(AlignItems::Center),
gap: Size { width: length(8.0_f32), height: length(0.0_f32) },
..Default::default()
})
}
/// Envuelve el contenido de una celda con el ancho de la columna (flex o
/// fijo). Encabezado y registro usan esto idéntico → columnas alineadas.
fn col_cell<Msg: Clone + 'static>(width: ColWidth, child: View<Msg>) -> View<Msg> {
let style = match width {
ColWidth::Flex(w) => Style {
flex_grow: w,
flex_basis: length(0.0_f32),
min_size: Size { width: length(0.0_f32), height: Dimension::auto() },
size: Size { width: Dimension::auto(), height: percent(1.0_f32) },
align_items: Some(AlignItems::Center),
..Default::default()
},
ColWidth::Fixed(px) => Style {
flex_shrink: 0.0,
size: Size { width: length(px), height: percent(1.0_f32) },
align_items: Some(AlignItems::Center),
..Default::default()
},
};
View::new(style).children(vec![child])
}
/// Estilo de un hijo que ocupa todo el ancho de su celda.
fn full() -> Style {
Style {
size: Size { width: percent(1.0_f32), height: Dimension::auto() },
..Default::default()
}
}
/// Padding horizontal `px` (top/bottom en cero).
fn pad_lr(px: f32) -> Rect<llimphi_ui::llimphi_layout::taffy::LengthPercentage> {
Rect {
left: length(px),
right: length(px),
top: length(0.0_f32),
bottom: length(0.0_f32),
}
}