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,206 @@
|
||||
//! Showcase de scroll avanzado (Tier 5): **app-bar colapsable** (sliver) +
|
||||
//! lista scrolleable + **inercia (fling)**. Un único `offset` en el Model
|
||||
//! maneja el colapso del header y el scroll del cuerpo; los botones "Fling"
|
||||
//! sueltan una velocidad que decae con [`fling_step`] vía un ticker periódico.
|
||||
//!
|
||||
//! Corré con:
|
||||
//! ```text
|
||||
//! cargo run -p llimphi-widget-scroll --example scroll_avanzado --release
|
||||
//! ```
|
||||
|
||||
use std::time::Duration;
|
||||
|
||||
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_scroll::{
|
||||
clamp_offset, fling_settled, fling_step, sliver_app_bar, sliver_max_offset, ScrollPalette,
|
||||
FLING_FRICTION,
|
||||
};
|
||||
|
||||
const HEADER_MAX: f32 = 200.0;
|
||||
const HEADER_MIN: f32 = 56.0;
|
||||
const VIEWPORT: f32 = 560.0;
|
||||
const ROW_H: f32 = 46.0;
|
||||
const N_ROWS: usize = 40;
|
||||
const CONTENT_LEN: f32 = N_ROWS as f32 * ROW_H;
|
||||
const DT: f32 = 1.0 / 60.0;
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Msg {
|
||||
/// Delta de scroll en px (rueda / arrastre de barra) a sumar al offset.
|
||||
ScrollBy(f32),
|
||||
/// Soltar una inercia con esta velocidad inicial (px/s).
|
||||
Fling(f32),
|
||||
/// Tick del ticker: avanza la inercia si hay.
|
||||
Tick,
|
||||
}
|
||||
|
||||
struct Model {
|
||||
offset: f32,
|
||||
velocity: f32,
|
||||
theme: Theme,
|
||||
}
|
||||
|
||||
fn max_off() -> f32 {
|
||||
sliver_max_offset(CONTENT_LEN, VIEWPORT, HEADER_MAX, HEADER_MIN)
|
||||
}
|
||||
|
||||
struct Demo;
|
||||
|
||||
impl App for Demo {
|
||||
type Model = Model;
|
||||
type Msg = Msg;
|
||||
|
||||
fn title() -> &'static str {
|
||||
"llimphi · scroll avanzado (sliver + fling)"
|
||||
}
|
||||
|
||||
fn initial_size() -> (u32, u32) {
|
||||
(720, VIEWPORT as u32)
|
||||
}
|
||||
|
||||
fn init(handle: &Handle<Self::Msg>) -> Self::Model {
|
||||
// Ticker de inercia ~60 fps (mismo patrón que `approach`).
|
||||
handle.spawn_periodic(Duration::from_millis(16), || Msg::Tick);
|
||||
Model { offset: 0.0, velocity: 0.0, theme: Theme::dark() }
|
||||
}
|
||||
|
||||
fn update(mut model: Self::Model, msg: Self::Msg, _: &Handle<Self::Msg>) -> Self::Model {
|
||||
match msg {
|
||||
Msg::ScrollBy(d) => {
|
||||
model.velocity = 0.0; // un scroll manual corta la inercia
|
||||
model.offset = clamp_offset(model.offset + d, max_off() + VIEWPORT, VIEWPORT);
|
||||
// (clamp_offset usa content/viewport; acá el "content" efectivo
|
||||
// es max_off + viewport, así max_offset(...) == max_off.)
|
||||
}
|
||||
Msg::Fling(v) => model.velocity = v,
|
||||
Msg::Tick => {
|
||||
if model.velocity != 0.0 {
|
||||
let (v, delta) = fling_step(model.velocity, DT, FLING_FRICTION);
|
||||
model.offset =
|
||||
clamp_offset(model.offset + delta, max_off() + VIEWPORT, VIEWPORT);
|
||||
// Frenar en los topes o al asentarse.
|
||||
if fling_settled(v) || model.offset <= 0.0 || model.offset >= max_off() {
|
||||
model.velocity = 0.0;
|
||||
} else {
|
||||
model.velocity = v;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
model
|
||||
}
|
||||
|
||||
fn view(model: &Self::Model) -> View<Self::Msg> {
|
||||
let t = &model.theme;
|
||||
let pal = ScrollPalette::from_theme(t);
|
||||
|
||||
// Lista (cuerpo del sliver): filas alternadas.
|
||||
let rows: Vec<View<Msg>> = (0..N_ROWS)
|
||||
.map(|i| {
|
||||
let bg = if i % 2 == 0 { t.bg_panel } else { t.bg_panel_alt };
|
||||
View::new(Style {
|
||||
size: Size { width: percent(1.0), height: length(ROW_H) },
|
||||
align_items: Some(AlignItems::Center),
|
||||
padding: Rect { left: length(20.0), right: length(20.0), top: length(0.0), bottom: length(0.0) },
|
||||
..Default::default()
|
||||
})
|
||||
.fill(bg)
|
||||
.text(format!("Fila {:02}", i + 1), 18.0, t.fg_text)
|
||||
})
|
||||
.collect();
|
||||
let list = View::new(Style {
|
||||
flex_direction: FlexDirection::Column,
|
||||
size: Size { width: percent(1.0), height: length(CONTENT_LEN) },
|
||||
..Default::default()
|
||||
})
|
||||
.children(rows);
|
||||
|
||||
let theme = t.clone();
|
||||
let sliver = sliver_app_bar(
|
||||
model.offset,
|
||||
HEADER_MAX,
|
||||
HEADER_MIN,
|
||||
move |frac| header(&theme, frac),
|
||||
list,
|
||||
CONTENT_LEN,
|
||||
VIEWPORT,
|
||||
Msg::ScrollBy,
|
||||
&pal,
|
||||
);
|
||||
|
||||
View::new(Style {
|
||||
size: Size { width: percent(1.0), height: percent(1.0) },
|
||||
..Default::default()
|
||||
})
|
||||
.fill(t.bg_app)
|
||||
.children(vec![sliver])
|
||||
}
|
||||
}
|
||||
|
||||
/// Header colapsable: el título encoge con `frac` y el subtítulo + botones de
|
||||
/// fling se desvanecen al colapsar (el `clip` del header los recorta).
|
||||
fn header(t: &Theme, frac: f32) -> View<Msg> {
|
||||
let title_size = 34.0 - 14.0 * frac; // 34 → 20
|
||||
// Fondo que se aclara al colapsar (de accent a panel).
|
||||
let title_row = View::new(Style {
|
||||
flex_direction: FlexDirection::Row,
|
||||
size: Size { width: percent(1.0), height: length(HEADER_MIN) },
|
||||
align_items: Some(AlignItems::Center),
|
||||
justify_content: Some(JustifyContent::SpaceBetween),
|
||||
padding: Rect { left: length(20.0), right: length(16.0), top: length(0.0), bottom: length(0.0) },
|
||||
..Default::default()
|
||||
})
|
||||
.children(vec![
|
||||
View::new(Style { ..Default::default() })
|
||||
.text("Scroll avanzado", title_size, t.fg_text),
|
||||
fling_buttons(t),
|
||||
]);
|
||||
|
||||
let subtitle = View::new(Style {
|
||||
size: Size { width: percent(1.0), height: length(28.0) },
|
||||
align_items: Some(AlignItems::Center),
|
||||
padding: Rect { left: length(20.0), right: length(20.0), top: length(0.0), bottom: length(0.0) },
|
||||
..Default::default()
|
||||
})
|
||||
.alpha(1.0 - frac) // se desvanece al colapsar
|
||||
.text("Tier 5 · app-bar colapsable + inercia · rueda para scrollear", 15.0, t.fg_muted);
|
||||
|
||||
View::new(Style {
|
||||
flex_direction: FlexDirection::Column,
|
||||
size: Size { width: percent(1.0), height: length(HEADER_MAX) },
|
||||
..Default::default()
|
||||
})
|
||||
.fill(t.bg_panel_alt)
|
||||
.children(vec![title_row, subtitle])
|
||||
}
|
||||
|
||||
fn fling_buttons(t: &Theme) -> View<Msg> {
|
||||
let btn = |label: &str, v: f32| {
|
||||
View::new(Style {
|
||||
size: Size { width: length(96.0), height: length(34.0) },
|
||||
align_items: Some(AlignItems::Center),
|
||||
justify_content: Some(JustifyContent::Center),
|
||||
..Default::default()
|
||||
})
|
||||
.fill(t.bg_button)
|
||||
.hover_fill(t.bg_button_hover)
|
||||
.radius(8.0)
|
||||
.text(label.to_string(), 15.0, t.fg_text)
|
||||
.on_click(Msg::Fling(v))
|
||||
};
|
||||
View::new(Style {
|
||||
flex_direction: FlexDirection::Row,
|
||||
gap: Size { width: length(8.0), height: length(0.0) },
|
||||
..Default::default()
|
||||
})
|
||||
.children(vec![btn("Fling ▲", -2600.0), btn("Fling ▼", 2600.0)])
|
||||
}
|
||||
|
||||
fn main() {
|
||||
llimphi_ui::run::<Demo>();
|
||||
}
|
||||
Reference in New Issue
Block a user