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:
@@ -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
@@ -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])
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user