Files
llimphi/widgets/scroll/examples/scroll_avanzado.rs
T
Sergio ccab39f140 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>
2026-06-18 14:40:00 +00:00

207 lines
6.9 KiB
Rust

//! 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>();
}