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,15 @@
|
||||
[package]
|
||||
name = "llimphi-widget-toolbar"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
description = "llimphi-widget-toolbar — barra de herramientas moderna: grupos de botones-ícono planos con hover redondeado, estado activo con acento y separadores sutiles. Los grupos son datos (Vec<ToolbarGroup>) → componibles/configurables por el caller; los íconos los dibuja el caller (closure), igual que dock-rail."
|
||||
|
||||
[dependencies]
|
||||
llimphi-ui = { workspace = true }
|
||||
llimphi-theme = { workspace = true }
|
||||
|
||||
[dev-dependencies]
|
||||
llimphi-icons = { workspace = true }
|
||||
@@ -0,0 +1,117 @@
|
||||
//! Demo interactiva del toolbar: grupo de navegación + toggles de vista +
|
||||
//! acciones (una deshabilitada). El texto de abajo refleja el último click.
|
||||
//!
|
||||
//! `cargo run -p llimphi-widget-toolbar --example toolbar_demo --release`
|
||||
|
||||
use llimphi_icons::{icon_view, Icon};
|
||||
use llimphi_theme::Theme;
|
||||
use llimphi_ui::llimphi_layout::taffy::{
|
||||
prelude::{length, percent, FlexDirection, Size, Style},
|
||||
AlignItems,
|
||||
};
|
||||
use llimphi_ui::{run, App, Handle, View};
|
||||
use llimphi_widget_toolbar::{toolbar_view, ToolbarGroup, ToolbarItem, ToolbarPalette};
|
||||
|
||||
struct Demo;
|
||||
|
||||
struct Model {
|
||||
vista: usize,
|
||||
dual: bool,
|
||||
ultimo: String,
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum Msg {
|
||||
Subir,
|
||||
Vista(usize),
|
||||
Dual,
|
||||
Nueva,
|
||||
}
|
||||
|
||||
impl App for Demo {
|
||||
type Model = Model;
|
||||
type Msg = Msg;
|
||||
|
||||
fn title() -> &'static str {
|
||||
"toolbar · demo"
|
||||
}
|
||||
|
||||
fn init(_handle: &Handle<Self::Msg>) -> Self::Model {
|
||||
Model { vista: 0, dual: false, ultimo: "(sin acciones)".into() }
|
||||
}
|
||||
|
||||
fn update(model: Self::Model, msg: Self::Msg, _h: &Handle<Self::Msg>) -> Self::Model {
|
||||
let mut m = model;
|
||||
match msg {
|
||||
Msg::Subir => m.ultimo = "subir".into(),
|
||||
Msg::Vista(v) => {
|
||||
m.vista = v;
|
||||
m.ultimo = format!("vista {v}");
|
||||
}
|
||||
Msg::Dual => {
|
||||
m.dual = !m.dual;
|
||||
m.ultimo = format!("dual: {}", m.dual);
|
||||
}
|
||||
Msg::Nueva => m.ultimo = "nueva carpeta".into(),
|
||||
}
|
||||
m
|
||||
}
|
||||
|
||||
fn view(model: &Self::Model) -> View<Self::Msg> {
|
||||
let theme = Theme::dark();
|
||||
let pal = ToolbarPalette::from_theme(&theme);
|
||||
let vistas = [Icon::Rows, Icon::Table, Icon::Grid, Icon::Image];
|
||||
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(
|
||||
vistas
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(i, ic)| {
|
||||
let ic = *ic;
|
||||
ToolbarItem::new(move |_s, c| icon_view(ic, c, 1.7), Msg::Vista(i))
|
||||
.active(model.vista == i)
|
||||
})
|
||||
.collect(),
|
||||
),
|
||||
ToolbarGroup::new(vec![
|
||||
ToolbarItem::new(|_s, c| icon_view(Icon::Columns, c, 1.7), Msg::Dual)
|
||||
.active(model.dual),
|
||||
ToolbarItem::new(|_s, c| icon_view(Icon::Plus, c, 1.7), Msg::Nueva)
|
||||
.with_label("carpeta")
|
||||
.enabled(false),
|
||||
]),
|
||||
],
|
||||
36.0,
|
||||
&pal,
|
||||
);
|
||||
let estado = View::new(Style {
|
||||
size: Size { width: percent(1.0_f32), height: length(30.0_f32) },
|
||||
align_items: Some(AlignItems::Center),
|
||||
padding: llimphi_ui::llimphi_layout::taffy::Rect {
|
||||
left: length(12.0_f32),
|
||||
right: length(12.0_f32),
|
||||
top: length(0.0_f32),
|
||||
bottom: length(0.0_f32),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.text(format!("último: {}", model.ultimo), 13.0, theme.fg_text);
|
||||
View::new(Style {
|
||||
flex_direction: FlexDirection::Column,
|
||||
size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
|
||||
..Default::default()
|
||||
})
|
||||
.fill(theme.bg_app)
|
||||
.children(vec![barra, estado])
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
run::<Demo>();
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
//! `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)
|
||||
}
|
||||
Reference in New Issue
Block a user