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>
258 lines
7.3 KiB
Rust
258 lines
7.3 KiB
Rust
//! `llimphi-widget-toolbar` — barra de herramientas moderna.
|
|
//!
|
|
//! Grupos de botones-ícono planos: hover con fondo redondeado, estado
|
|
//! **activo** con fondo de selección + ícono en acento, **deshabilitado**
|
|
//! atenuado y sin click. Entre grupos, un separador vertical sutil.
|
|
//!
|
|
//! El widget es render-only y agnóstico del `Msg`:
|
|
//! - los **íconos los dibuja el caller** vía closure `Fn(size, color) ->
|
|
//! View` (mismo contrato que `dock-rail::make_icon`) — el widget resuelve
|
|
//! el color según el estado (activo/normal/deshabilitado);
|
|
//! - los **grupos son datos** (`Vec<ToolbarGroup>`): el caller los arma,
|
|
//! reordena o filtra — la barra es componible/configurable por
|
|
//! construcción, sin sistema de config propio.
|
|
//!
|
|
//! ```ignore
|
|
//! let barra = toolbar_view(
|
|
//! vec![
|
|
//! ToolbarGroup::new(vec![
|
|
//! ToolbarItem::new(|s, c| icon_view(Icon::ChevronUp, c, 1.7), Msg::Subir)
|
|
//! .with_label("subir"),
|
|
//! ]),
|
|
//! ToolbarGroup::new(vec![
|
|
//! ToolbarItem::new(|s, c| icon_view(Icon::Rows, c, 1.7), Msg::Vista(0)).active(lista),
|
|
//! ToolbarItem::new(|s, c| icon_view(Icon::Grid, c, 1.7), Msg::Vista(2)).active(iconos),
|
|
//! ]),
|
|
//! ],
|
|
//! 36.0,
|
|
//! &ToolbarPalette::from_theme(&theme),
|
|
//! );
|
|
//! ```
|
|
|
|
#![forbid(unsafe_code)]
|
|
|
|
use llimphi_ui::llimphi_layout::taffy::{
|
|
prelude::{auto, length, percent, FlexDirection, Size, Style},
|
|
AlignItems, JustifyContent, Rect,
|
|
};
|
|
use llimphi_ui::llimphi_raster::peniko::Color;
|
|
use llimphi_ui::View;
|
|
|
|
/// Tamaño del ícono que se le pide al caller (px).
|
|
const ICON_PX: f32 = 16.0;
|
|
|
|
/// Paleta de la barra — subset del `Theme` semántico.
|
|
#[derive(Debug, Clone, Copy)]
|
|
pub struct ToolbarPalette {
|
|
/// Fondo de la franja.
|
|
pub bg_bar: Color,
|
|
/// Fondo del botón al hover.
|
|
pub bg_hover: Color,
|
|
/// Fondo del botón activo (toggle prendido).
|
|
pub bg_active: Color,
|
|
/// Ícono/label normal.
|
|
pub fg: Color,
|
|
/// Ícono del botón activo.
|
|
pub fg_active: Color,
|
|
/// Ícono/label deshabilitado.
|
|
pub fg_disabled: Color,
|
|
/// Separador vertical entre grupos.
|
|
pub separator: Color,
|
|
}
|
|
|
|
impl ToolbarPalette {
|
|
pub fn from_theme(t: &llimphi_theme::Theme) -> Self {
|
|
Self {
|
|
bg_bar: t.bg_panel_alt,
|
|
bg_hover: t.bg_row_hover,
|
|
bg_active: t.bg_selected,
|
|
fg: t.fg_muted,
|
|
fg_active: t.accent,
|
|
fg_disabled: t.fg_placeholder,
|
|
separator: t.border,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for ToolbarPalette {
|
|
fn default() -> Self {
|
|
Self::from_theme(&llimphi_theme::Theme::dark())
|
|
}
|
|
}
|
|
|
|
/// Un botón de la barra. El ícono es una closure `(size_px, color) -> View`
|
|
/// — el widget la invoca con el color ya resuelto por estado.
|
|
pub struct ToolbarItem<Msg> {
|
|
pub icon: Box<dyn Fn(f32, Color) -> View<Msg>>,
|
|
/// Texto corto opcional a la derecha del ícono (la barra es icon-first).
|
|
pub label: Option<String>,
|
|
/// Toggle prendido (fondo de selección + acento).
|
|
pub active: bool,
|
|
/// `false` = atenuado y sin click.
|
|
pub enabled: bool,
|
|
pub on_click: Msg,
|
|
}
|
|
|
|
impl<Msg> ToolbarItem<Msg> {
|
|
pub fn new(
|
|
icon: impl Fn(f32, Color) -> View<Msg> + 'static,
|
|
on_click: Msg,
|
|
) -> Self {
|
|
Self {
|
|
icon: Box::new(icon),
|
|
label: None,
|
|
active: false,
|
|
enabled: true,
|
|
on_click,
|
|
}
|
|
}
|
|
|
|
pub fn with_label(mut self, label: impl Into<String>) -> Self {
|
|
self.label = Some(label.into());
|
|
self
|
|
}
|
|
|
|
pub fn active(mut self, active: bool) -> Self {
|
|
self.active = active;
|
|
self
|
|
}
|
|
|
|
pub fn enabled(mut self, enabled: bool) -> Self {
|
|
self.enabled = enabled;
|
|
self
|
|
}
|
|
}
|
|
|
|
/// Un grupo de botones contiguos; entre grupos va un separador.
|
|
pub struct ToolbarGroup<Msg> {
|
|
pub items: Vec<ToolbarItem<Msg>>,
|
|
}
|
|
|
|
impl<Msg> ToolbarGroup<Msg> {
|
|
pub fn new(items: Vec<ToolbarItem<Msg>>) -> Self {
|
|
Self { items }
|
|
}
|
|
}
|
|
|
|
/// Compone la barra: franja horizontal de alto `height` con los grupos
|
|
/// alineados a la izquierda.
|
|
pub fn toolbar_view<Msg: Clone + 'static>(
|
|
groups: Vec<ToolbarGroup<Msg>>,
|
|
height: f32,
|
|
palette: &ToolbarPalette,
|
|
) -> View<Msg> {
|
|
let mut kids: Vec<View<Msg>> = Vec::new();
|
|
let n = groups.len();
|
|
for (gi, group) in groups.into_iter().enumerate() {
|
|
for item in group.items {
|
|
kids.push(button(item, height, palette));
|
|
}
|
|
if gi + 1 < n {
|
|
kids.push(separator(height, palette));
|
|
}
|
|
}
|
|
View::new(Style {
|
|
flex_direction: FlexDirection::Row,
|
|
size: Size {
|
|
width: percent(1.0_f32),
|
|
height: length(height),
|
|
},
|
|
flex_shrink: 0.0,
|
|
align_items: Some(AlignItems::Center),
|
|
padding: Rect {
|
|
left: length(6.0_f32),
|
|
right: length(6.0_f32),
|
|
top: length(0.0_f32),
|
|
bottom: length(0.0_f32),
|
|
},
|
|
gap: Size {
|
|
width: length(2.0_f32),
|
|
height: length(0.0_f32),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.fill(palette.bg_bar)
|
|
.children(kids)
|
|
}
|
|
|
|
fn button<Msg: Clone + 'static>(
|
|
item: ToolbarItem<Msg>,
|
|
bar_h: f32,
|
|
palette: &ToolbarPalette,
|
|
) -> View<Msg> {
|
|
let fg = if !item.enabled {
|
|
palette.fg_disabled
|
|
} else if item.active {
|
|
palette.fg_active
|
|
} else {
|
|
palette.fg
|
|
};
|
|
let mut inner: Vec<View<Msg>> = vec![View::new(Style {
|
|
size: Size {
|
|
width: length(ICON_PX),
|
|
height: length(ICON_PX),
|
|
},
|
|
flex_shrink: 0.0,
|
|
..Default::default()
|
|
})
|
|
.children(vec![(item.icon)(ICON_PX, fg)])];
|
|
if let Some(label) = item.label {
|
|
inner.push(
|
|
View::new(Style {
|
|
size: Size { width: auto(), height: percent(1.0_f32) },
|
|
align_items: Some(AlignItems::Center),
|
|
..Default::default()
|
|
})
|
|
.text(label, 11.5, fg),
|
|
);
|
|
}
|
|
let mut btn = View::new(Style {
|
|
flex_direction: FlexDirection::Row,
|
|
size: Size {
|
|
width: auto(),
|
|
height: length(bar_h - 8.0),
|
|
},
|
|
flex_shrink: 0.0,
|
|
align_items: Some(AlignItems::Center),
|
|
justify_content: Some(JustifyContent::Center),
|
|
padding: Rect {
|
|
left: length(7.0_f32),
|
|
right: length(7.0_f32),
|
|
top: length(0.0_f32),
|
|
bottom: length(0.0_f32),
|
|
},
|
|
gap: Size {
|
|
width: length(5.0_f32),
|
|
height: length(0.0_f32),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.radius(6.0)
|
|
.children(inner);
|
|
if item.active {
|
|
btn = btn.fill(palette.bg_active);
|
|
}
|
|
if item.enabled {
|
|
btn = btn.hover_fill(palette.bg_hover).on_click(item.on_click);
|
|
}
|
|
btn
|
|
}
|
|
|
|
fn separator<Msg: Clone + 'static>(bar_h: f32, palette: &ToolbarPalette) -> View<Msg> {
|
|
View::new(Style {
|
|
size: Size {
|
|
width: length(1.0_f32),
|
|
height: length(bar_h - 16.0),
|
|
},
|
|
flex_shrink: 0.0,
|
|
margin: Rect {
|
|
left: length(5.0_f32),
|
|
right: length(5.0_f32),
|
|
top: length(0.0_f32),
|
|
bottom: length(0.0_f32),
|
|
},
|
|
..Default::default()
|
|
})
|
|
.fill(palette.separator)
|
|
}
|