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
@@ -0,0 +1,133 @@
//! Showcase de [`llimphi_widget_list::reorderable_list_view`] (Bloque
//! 14 de PARIDAD-FLUTTER, primera variante de Tier 5 backlog). Una
//! lista de tareas con drag-handle al borde izquierdo (`⋮⋮`); arrastrá
//! una fila y soltala sobre otra para intercambiarlas. El destino se
//! ilumina con `bg_drop_hover` mientras está bajo el cursor.
//!
//! Cliquear una fila la marca como "completada" (cambia a tachado en el
//! label de su descripción de abajo) — sirve para ver que `on_click`
//! coexiste con el drag sin pelearse.
//!
//! `cargo run -p llimphi-widget-list --example reorderable_list_demo --release`
use std::sync::Arc;
use llimphi_theme::Theme;
use llimphi_ui::llimphi_layout::taffy::{
prelude::{length, percent, FlexDirection, Size, Style},
AlignItems, JustifyContent, Rect,
};
use llimphi_ui::{App, Handle, View};
use llimphi_widget_list::{
reorderable_list_view, ListPalette, ReorderableListRow, ReorderableListSpec,
};
#[derive(Clone)]
enum Msg {
Reorder { from: usize, to: usize },
Toggle(usize),
}
struct Model {
items: Vec<(String, bool)>,
}
struct Showcase;
impl App for Showcase {
type Model = Model;
type Msg = Msg;
fn title() -> &'static str {
"llimphi · reorderable list (drag las filas para reordenar)"
}
fn initial_size() -> (u32, u32) {
(560, 520)
}
fn init(_: &Handle<Msg>) -> Model {
Model {
items: vec![
("Cerrar Tier 1 del roadmap (backdrop blur)".into(), true),
("Closeout Tier 2: RichText spans".into(), true),
("ImageFit Contain/Cover/Fill/None".into(), true),
("Reorderable list widget".into(), false),
("Texto seleccionable fuera del editor".into(), false),
("RepaintBoundary (Tier 8)".into(), false),
("Cross-fade real entre identidades".into(), false),
],
}
}
fn update(model: Model, msg: Msg, _: &Handle<Msg>) -> Model {
let mut m = model;
match msg {
Msg::Reorder { from, to } => {
if from != to && from < m.items.len() && to < m.items.len() {
let item = m.items.remove(from);
let dest = if to > from { to - 1 } else { to };
m.items.insert(dest.min(m.items.len()), item);
}
}
Msg::Toggle(i) => {
if let Some(it) = m.items.get_mut(i) {
it.1 = !it.1;
}
}
}
m
}
fn view(model: &Model) -> View<Msg> {
let theme = Theme::dark();
let palette = ListPalette::from_theme(&theme);
let rows: Vec<ReorderableListRow<Msg>> = model
.items
.iter()
.enumerate()
.map(|(i, (label, done))| {
let prefix = if *done { "" } else { "" };
ReorderableListRow {
label: format!("{prefix}{label}"),
selected: *done,
on_click: Some(Msg::Toggle(i)),
}
})
.collect();
let panel = reorderable_list_view(ReorderableListSpec {
rows,
caption: Some(format!("{} tareas — drag para reordenar, click para marcar", model.items.len())),
row_height: 36.0,
palette,
on_reorder: Arc::new(|from, to| Some(Msg::Reorder { from, to })),
});
// Marco exterior con padding para que el panel no toque los
// bordes de la ventana.
View::new(Style {
flex_direction: FlexDirection::Column,
size: Size {
width: percent(1.0_f32),
height: percent(1.0_f32),
},
align_items: Some(AlignItems::Stretch),
justify_content: Some(JustifyContent::Center),
padding: Rect {
left: length(20.0_f32),
right: length(20.0_f32),
top: length(20.0_f32),
bottom: length(20.0_f32),
},
..Default::default()
})
.fill(theme.bg_app)
.children(vec![panel])
}
}
fn main() {
llimphi_ui::run::<Showcase>();
}
+196 -1
View File
@@ -37,13 +37,15 @@
#![forbid(unsafe_code)]
use std::sync::Arc;
use llimphi_ui::llimphi_layout::taffy::{
prelude::{length, percent, FlexDirection, Size, Style},
AlignItems, Rect,
};
use llimphi_ui::llimphi_raster::peniko::Color;
use llimphi_ui::llimphi_text::Alignment;
use llimphi_ui::View;
use llimphi_ui::{DragPhase, View};
/// Paleta de la lista. Los defaults son una variante dark con selección
/// azulada — equivalente conceptual a `nahual_theme` en su tema oscuro.
@@ -53,6 +55,11 @@ pub struct ListPalette {
pub bg_selected: Color,
pub fg_text: Color,
pub fg_muted: Color,
/// Resalte de la fila destino mientras un drag de reorder pasa por
/// encima. Sólo se usa en [`reorderable_list_view`]. Default = accent
/// translúcido (40 %) sobre `bg_selected` — distinguible del hover y
/// la selección estable.
pub bg_drop_hover: Color,
}
impl Default for ListPalette {
@@ -64,11 +71,17 @@ impl Default for ListPalette {
impl ListPalette {
/// Construye la paleta desde un `Theme` semántico.
pub fn from_theme(t: &llimphi_theme::Theme) -> Self {
// Resalte de drop = accent del theme con 40 % de opacidad
// multiplicada — gana sobre `bg_selected` por luminancia para que
// un drag sobre una fila ya seleccionada se note.
let mut drop = t.accent;
drop.components[3] *= 0.40;
Self {
bg_panel: t.bg_panel,
bg_selected: t.bg_selected,
fg_text: t.fg_text,
fg_muted: t.fg_muted,
bg_drop_hover: drop,
}
}
}
@@ -197,5 +210,187 @@ fn row_view<Msg: Clone + 'static>(row: ListRow<Msg>, height: f32, palette: &List
})
.fill(bg)
.text_aligned(row.label, 12.0, palette.fg_text, Alignment::Start)
// Labels largos terminan en `…` (single-line) en vez de cortarse seco.
.ellipsis(1)
.on_click(row.on_click)
}
/// Función que el caller usa para reaccionar a un reorder. Recibe `(from,
/// to)` — índices en `rows` — y devuelve el `Msg` a despachar (o `None`
/// para ignorar el drop, p. ej. si `from == to`).
pub type ReorderFn<Msg> = Arc<dyn Fn(usize, usize) -> Option<Msg> + Send + Sync>;
/// Una fila para [`reorderable_list_view`]. Pesa más que [`ListRow`]
/// porque cada fila acepta `on_click` opcional y siempre lleva drag
/// handle al borde izquierdo (gripper `⋮⋮` en `fg_muted`) — convención
/// kanban/Trello/Flutter `ReorderableListView`.
pub struct ReorderableListRow<Msg> {
pub label: String,
pub selected: bool,
pub on_click: Option<Msg>,
}
/// Especificación de una lista reordenable por drag&drop (Bloque 14 de
/// PARIDAD-FLUTTER, sigue Tier 5). Cada fila lleva un gripper a la
/// izquierda; arrastrar una fila y soltarla sobre otra emite
/// `on_reorder(from, to)`. La fila destino se ilumina con
/// `palette.bg_drop_hover` mientras el cursor está sobre ella durante el
/// drag.
pub struct ReorderableListSpec<Msg> {
pub rows: Vec<ReorderableListRow<Msg>>,
pub caption: Option<String>,
pub row_height: f32,
pub palette: ListPalette,
pub on_reorder: ReorderFn<Msg>,
}
/// Compone una lista reordenable (Bloque 14). Patrón: cada fila exhibe
/// un gripper al borde izquierdo y es `draggable` con `payload = idx`;
/// la **fila entera** (no sólo el gripper) recibe drops con `on_drop` y
/// `drop_hover_fill`. El handler `on_reorder(from, to)` cae al caller
/// que decide qué `Msg` despachar — el widget no muta nada por sí solo.
///
/// Composición pura sobre los primitives `drag_payload` / `on_drop` /
/// `drop_hover_fill` / `draggable` de `llimphi-ui` (ver `tiled` que
/// reordena paneles bajo el mismo idiom).
pub fn reorderable_list_view<Msg>(spec: ReorderableListSpec<Msg>) -> View<Msg>
where
Msg: Clone + Send + Sync + 'static,
{
let ReorderableListSpec {
rows,
caption,
row_height,
palette,
on_reorder,
} = spec;
let mut children: Vec<View<Msg>> = Vec::with_capacity(rows.len() + 1);
if let Some(text) = caption {
children.push(
View::new(Style {
size: Size {
width: percent(1.0_f32),
height: length(20.0_f32),
},
padding: Rect {
left: length(10.0_f32),
right: length(10.0_f32),
top: length(0.0_f32),
bottom: length(0.0_f32),
},
align_items: Some(AlignItems::Center),
..Default::default()
})
.text_aligned(text, 10.0, palette.fg_muted, Alignment::Start),
);
}
for (idx, row) in rows.into_iter().enumerate() {
children.push(reorderable_row_view(idx, row, row_height, &palette, on_reorder.clone()));
}
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(6.0_f32),
bottom: length(6.0_f32),
},
..Default::default()
})
.fill(palette.bg_panel)
.clip(true)
.children(children)
}
fn reorderable_row_view<Msg>(
idx: usize,
row: ReorderableListRow<Msg>,
height: f32,
palette: &ListPalette,
on_reorder: ReorderFn<Msg>,
) -> View<Msg>
where
Msg: Clone + Send + Sync + 'static,
{
let bg = if row.selected {
palette.bg_selected
} else {
palette.bg_panel
};
// Gripper `⋮⋮` al borde izquierdo, en `fg_muted` y arrastrable. El
// drag entrega `payload = idx`. Devolvemos `None` por evento de drag
// (no usamos dx/dy aquí — el destino se decide en el `on_drop` del
// otro nodo).
let gripper = View::new(Style {
size: Size {
width: length(20.0_f32),
height: percent(1.0_f32),
},
align_items: Some(AlignItems::Center),
..Default::default()
})
.text_aligned("⋮⋮", 14.0, palette.fg_muted, Alignment::Center)
.draggable(|_phase: DragPhase, _dx: f32, _dy: f32| None)
.drag_payload(idx as u64)
.cursor(llimphi_ui::Cursor::Grab);
// Etiqueta: ocupa el resto de la fila, con ellipsis y click opcional.
let mut label = View::new(Style {
size: Size {
width: percent(1.0_f32),
height: percent(1.0_f32),
},
align_items: Some(AlignItems::Center),
..Default::default()
})
.text_aligned(row.label, 12.0, palette.fg_text, Alignment::Start)
.ellipsis(1);
if let Some(msg) = row.on_click {
label = label.on_click(msg);
}
// El **row entero** es target de drop. Cuando el cursor pasa por
// encima durante un drag, `drop_hover_fill` lo ilumina. Al soltar,
// emitimos el reorder si from != to.
let to_idx = idx;
let reorder = on_reorder.clone();
View::new(Style {
flex_direction: FlexDirection::Row,
size: Size {
width: percent(1.0_f32),
height: length(height),
},
padding: Rect {
left: length(4.0_f32),
right: length(10.0_f32),
top: length(0.0_f32),
bottom: length(0.0_f32),
},
align_items: Some(AlignItems::Center),
gap: Size {
width: length(6.0_f32),
height: length(0.0_f32),
},
..Default::default()
})
.fill(bg)
.on_drop(move |from: u64| {
let from = from as usize;
if from == to_idx {
None
} else {
(reorder)(from, to_idx)
}
})
.drop_hover_fill(palette.bg_drop_hover)
.children(vec![gripper, label])
}