ccab39f140
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>
191 lines
6.4 KiB
Rust
191 lines
6.4 KiB
Rust
//! `llimphi-widget-dock-rail` — rail vertical de **dientes** para
|
|
//! sidebars acoplables.
|
|
//!
|
|
//! Cada diente es una pestaña vertical: una **barra de acento** de 3px
|
|
//! pegada al borde interno (encendida cuando el item está activo) + un
|
|
//! **icono** centrado. Los dientes apilan en una columna redondeada que
|
|
//! se pinta como una franja flotante al borde del centro — el patrón del
|
|
//! dock de cosmos, ahora reutilizable.
|
|
//!
|
|
//! Render-only y agnóstico del `Msg`: el item se identifica por un `u64`
|
|
//! opaco. El clic (en el *press*, para no pelear con el arrastre) activa
|
|
//! vía `on_activate(id)`; el diente es **arrastrable** con su `id` como
|
|
//! payload, y el rail entero es **drop target** (`on_drop(payload)`) —
|
|
//! así una app puede mover un diente de un sidebar a otro soltándolo
|
|
//! sobre el rail opuesto. El icono lo dibuja el caller (`make_icon`), que
|
|
//! recibe el color ya resuelto según el estado activo/inactivo.
|
|
//!
|
|
//! ```ignore
|
|
//! let items = [DockRailItem { id: 0, active: true }, DockRailItem { id: 1, active: false }];
|
|
//! dock_rail_view(
|
|
//! &items,
|
|
//! 44.0,
|
|
//! &DockRailPalette::from_theme(&theme),
|
|
//! |id, size, color| my_icon_view(id, size, color),
|
|
//! |id| Msg::DockActivate(side, id),
|
|
//! move |payload| Some(Msg::DockDrop(side, payload)),
|
|
//! )
|
|
//! ```
|
|
|
|
#![forbid(unsafe_code)]
|
|
|
|
use llimphi_ui::llimphi_layout::taffy::{
|
|
prelude::{auto, length, percent, FlexDirection, Size, Style},
|
|
AlignItems, JustifyContent,
|
|
};
|
|
use llimphi_ui::llimphi_raster::peniko::Color;
|
|
use llimphi_ui::{DragPhase, View};
|
|
use llimphi_theme::Theme;
|
|
|
|
/// Alto de cada diente (px).
|
|
const TOOTH_H: f32 = 42.0;
|
|
/// Alto de la barra de acento (px) — un poco menor que el diente para
|
|
/// dejar aire arriba y abajo.
|
|
const BAR_H: f32 = 40.0;
|
|
/// Tamaño del icono que se le pide al caller (px).
|
|
const ICON_PX: f32 = 20.0;
|
|
|
|
/// Paleta del rail.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct DockRailPalette {
|
|
/// Fondo de la franja del rail.
|
|
pub bg_rail: Color,
|
|
/// Fondo del diente activo.
|
|
pub bg_active: Color,
|
|
/// Fondo al hover (diente) y al sobrevolar un drop válido (rail).
|
|
pub bg_hover: Color,
|
|
/// Color del acento: barra del diente activo + su icono.
|
|
pub accent: Color,
|
|
/// Color del icono de un diente inactivo.
|
|
pub icon_inactive: Color,
|
|
}
|
|
|
|
impl DockRailPalette {
|
|
pub fn from_theme(t: &Theme) -> Self {
|
|
Self {
|
|
bg_rail: t.bg_panel_alt,
|
|
bg_active: t.bg_selected,
|
|
bg_hover: t.bg_row_hover,
|
|
accent: t.accent,
|
|
icon_inactive: t.fg_muted,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Un diente del rail: su id opaco y si está activo.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct DockRailItem {
|
|
pub id: u64,
|
|
pub active: bool,
|
|
}
|
|
|
|
/// Construye el rail de dientes.
|
|
///
|
|
/// - `items`: en el orden en que se quieren mostrar (el widget no
|
|
/// reordena).
|
|
/// - `width`: ancho de la franja del rail (px).
|
|
/// - `make_icon(id, size, color)`: dibuja el icono del item con el color
|
|
/// ya resuelto (acento si activo, atenuado si no).
|
|
/// - `on_activate(id)`: `Msg` al clickear (en el press).
|
|
/// - `on_drop(payload)`: `Msg` opcional cuando se suelta un diente
|
|
/// (cualquier `id`) sobre este rail.
|
|
pub fn dock_rail_view<Msg, FIcon, FAct, FDrop>(
|
|
items: &[DockRailItem],
|
|
width: f32,
|
|
palette: &DockRailPalette,
|
|
make_icon: FIcon,
|
|
on_activate: FAct,
|
|
on_drop: FDrop,
|
|
) -> View<Msg>
|
|
where
|
|
Msg: Clone + Send + Sync + 'static,
|
|
FIcon: Fn(u64, f32, Color) -> View<Msg>,
|
|
FAct: Fn(u64) -> Msg,
|
|
FDrop: Fn(u64) -> Option<Msg> + Send + Sync + 'static,
|
|
{
|
|
let mut teeth: Vec<View<Msg>> = Vec::with_capacity(items.len());
|
|
for item in items {
|
|
let fg = if item.active {
|
|
palette.accent
|
|
} else {
|
|
palette.icon_inactive
|
|
};
|
|
// Barra de acento, pegada al borde interno.
|
|
let accent_bar = {
|
|
let b = View::new(Style {
|
|
size: Size {
|
|
width: length(3.0_f32),
|
|
height: length(BAR_H),
|
|
},
|
|
flex_shrink: 0.0,
|
|
..Default::default()
|
|
});
|
|
if item.active {
|
|
b.fill(palette.accent).radius(2.0)
|
|
} else {
|
|
b
|
|
}
|
|
};
|
|
let icon_box = View::new(Style {
|
|
flex_grow: 1.0,
|
|
size: Size {
|
|
width: percent(0.0_f32),
|
|
height: length(TOOTH_H),
|
|
},
|
|
align_items: Some(AlignItems::Center),
|
|
justify_content: Some(JustifyContent::Center),
|
|
..Default::default()
|
|
})
|
|
.children(vec![make_icon(item.id, ICON_PX, fg)]);
|
|
|
|
let id = item.id;
|
|
let msg = on_activate(id);
|
|
let mut tooth = View::new(Style {
|
|
flex_direction: FlexDirection::Row,
|
|
size: Size {
|
|
width: percent(1.0_f32),
|
|
height: length(TOOTH_H),
|
|
},
|
|
flex_shrink: 0.0,
|
|
align_items: Some(AlignItems::Center),
|
|
..Default::default()
|
|
})
|
|
.hover_fill(palette.bg_hover)
|
|
// Click en el press activa; arrastrar mueve de rail (payload=id).
|
|
.on_click_at(move |_, _, _, _| Some(msg.clone()))
|
|
.draggable_at(|phase, _, _, _, _| match phase {
|
|
DragPhase::Move | DragPhase::End => None,
|
|
})
|
|
.drag_payload(id)
|
|
.children(vec![accent_bar, icon_box]);
|
|
if item.active {
|
|
// Pestaña activa: además del fill, redondea el lado que sobresale
|
|
// hacia el contenido (la barra de acento marca el borde interno a
|
|
// la izquierda, así que el diente abre a la derecha). Le da el look
|
|
// de pestaña que sale del rail en vez de un rectángulo plano.
|
|
tooth = tooth
|
|
.fill(palette.bg_active)
|
|
.radius_corners(0.0, 8.0, 8.0, 0.0);
|
|
}
|
|
teeth.push(tooth);
|
|
}
|
|
|
|
// La franja: sólo del alto de los dientes (el hueco de abajo lo deja
|
|
// libre para que el área central lo aproveche si el rail flota como
|
|
// overlay). Es además el drop target del lado.
|
|
View::new(Style {
|
|
flex_direction: FlexDirection::Column,
|
|
size: Size {
|
|
width: length(width),
|
|
height: auto(),
|
|
},
|
|
flex_shrink: 0.0,
|
|
..Default::default()
|
|
})
|
|
.fill(palette.bg_rail)
|
|
.radius(5.0)
|
|
.on_drop(on_drop)
|
|
.drop_hover_fill(palette.bg_hover)
|
|
.children(teeth)
|
|
}
|