gioser

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 660Markdown 302TOML 157RON 312382 Sutichaykuna

⎇ 5ae7c737031521b9a873f3d7cff36261fbf7ae06 widgets/toolbar/src/lib.rs Rust · 257 siq'ikuna · 7.3 KB Ñawpa willay Pi imata llamkarqan ch'uya

refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel ccab39f14 · Sergio ·

1//! `llimphi-widget-toolbar` — barra de herramientas moderna.
2//!
3//! Grupos de botones-ícono planos: hover con fondo redondeado, estado
4//! **activo** con fondo de selección + ícono en acento, **deshabilitado**
5//! atenuado y sin click. Entre grupos, un separador vertical sutil.
6//!
7//! El widget es render-only y agnóstico del `Msg`:
8//! - los **íconos los dibuja el caller** vía closure `Fn(size, color) ->
9//!   View` (mismo contrato que `dock-rail::make_icon`) — el widget resuelve
10//!   el color según el estado (activo/normal/deshabilitado);
11//! - los **grupos son datos** (`Vec<ToolbarGroup>`): el caller los arma,
12//!   reordena o filtra — la barra es componible/configurable por
13//!   construcción, sin sistema de config propio.
14//!
15//! ```ignore
16//! let barra = toolbar_view(
17//!     vec![
18//!         ToolbarGroup::new(vec![
19//!             ToolbarItem::new(|s, c| icon_view(Icon::ChevronUp, c, 1.7), Msg::Subir)
20//!                 .with_label("subir"),
21//!         ]),
22//!         ToolbarGroup::new(vec![
23//!             ToolbarItem::new(|s, c| icon_view(Icon::Rows, c, 1.7), Msg::Vista(0)).active(lista),
24//!             ToolbarItem::new(|s, c| icon_view(Icon::Grid, c, 1.7), Msg::Vista(2)).active(iconos),
25//!         ]),
26//!     ],
27//!     36.0,
28//!     &ToolbarPalette::from_theme(&theme),
29//! );
30//! ```
31
32#![forbid(unsafe_code)]
33
34use llimphi_ui::llimphi_layout::taffy::{
35    prelude::{auto, length, percent, FlexDirection, Size, Style},
36    AlignItems, JustifyContent, Rect,
37};
38use llimphi_ui::llimphi_raster::peniko::Color;
39use llimphi_ui::View;
40
41/// Tamaño del ícono que se le pide al caller (px).
42const ICON_PX: f32 = 16.0;
43
44/// Paleta de la barra — subset del `Theme` semántico.
45#[derive(Debug, Clone, Copy)]
46pub struct ToolbarPalette {
47    /// Fondo de la franja.
48    pub bg_bar: Color,
49    /// Fondo del botón al hover.
50    pub bg_hover: Color,
51    /// Fondo del botón activo (toggle prendido).
52    pub bg_active: Color,
53    /// Ícono/label normal.
54    pub fg: Color,
55    /// Ícono del botón activo.
56    pub fg_active: Color,
57    /// Ícono/label deshabilitado.
58    pub fg_disabled: Color,
59    /// Separador vertical entre grupos.
60    pub separator: Color,
61}
62
63impl ToolbarPalette {
64    pub fn from_theme(t: &llimphi_theme::Theme) -> Self {
65        Self {
66            bg_bar: t.bg_panel_alt,
67            bg_hover: t.bg_row_hover,
68            bg_active: t.bg_selected,
69            fg: t.fg_muted,
70            fg_active: t.accent,
71            fg_disabled: t.fg_placeholder,
72            separator: t.border,
73        }
74    }
75}
76
77impl Default for ToolbarPalette {
78    fn default() -> Self {
79        Self::from_theme(&llimphi_theme::Theme::dark())
80    }
81}
82
83/// Un botón de la barra. El ícono es una closure `(size_px, color) -> View`
84/// — el widget la invoca con el color ya resuelto por estado.
85pub struct ToolbarItem<Msg> {
86    pub icon: Box<dyn Fn(f32, Color) -> View<Msg>>,
87    /// Texto corto opcional a la derecha del ícono (la barra es icon-first).
88    pub label: Option<String>,
89    /// Toggle prendido (fondo de selección + acento).
90    pub active: bool,
91    /// `false` = atenuado y sin click.
92    pub enabled: bool,
93    pub on_click: Msg,
94}
95
96impl<Msg> ToolbarItem<Msg> {
97    pub fn new(
98        icon: impl Fn(f32, Color) -> View<Msg> + 'static,
99        on_click: Msg,
100    ) -> Self {
101        Self {
102            icon: Box::new(icon),
103            label: None,
104            active: false,
105            enabled: true,
106            on_click,
107        }
108    }
109
110    pub fn with_label(mut self, label: impl Into<String>) -> Self {
111        self.label = Some(label.into());
112        self
113    }
114
115    pub fn active(mut self, active: bool) -> Self {
116        self.active = active;
117        self
118    }
119
120    pub fn enabled(mut self, enabled: bool) -> Self {
121        self.enabled = enabled;
122        self
123    }
124}
125
126/// Un grupo de botones contiguos; entre grupos va un separador.
127pub struct ToolbarGroup<Msg> {
128    pub items: Vec<ToolbarItem<Msg>>,
129}
130
131impl<Msg> ToolbarGroup<Msg> {
132    pub fn new(items: Vec<ToolbarItem<Msg>>) -> Self {
133        Self { items }
134    }
135}
136
137/// Compone la barra: franja horizontal de alto `height` con los grupos
138/// alineados a la izquierda.
139pub fn toolbar_view<Msg: Clone + 'static>(
140    groups: Vec<ToolbarGroup<Msg>>,
141    height: f32,
142    palette: &ToolbarPalette,
143) -> View<Msg> {
144    let mut kids: Vec<View<Msg>> = Vec::new();
145    let n = groups.len();
146    for (gi, group) in groups.into_iter().enumerate() {
147        for item in group.items {
148            kids.push(button(item, height, palette));
149        }
150        if gi + 1 < n {
151            kids.push(separator(height, palette));
152        }
153    }
154    View::new(Style {
155        flex_direction: FlexDirection::Row,
156        size: Size {
157            width: percent(1.0_f32),
158            height: length(height),
159        },
160        flex_shrink: 0.0,
161        align_items: Some(AlignItems::Center),
162        padding: Rect {
163            left: length(6.0_f32),
164            right: length(6.0_f32),
165            top: length(0.0_f32),
166            bottom: length(0.0_f32),
167        },
168        gap: Size {
169            width: length(2.0_f32),
170            height: length(0.0_f32),
171        },
172        ..Default::default()
173    })
174    .fill(palette.bg_bar)
175    .children(kids)
176}
177
178fn button<Msg: Clone + 'static>(
179    item: ToolbarItem<Msg>,
180    bar_h: f32,
181    palette: &ToolbarPalette,
182) -> View<Msg> {
183    let fg = if !item.enabled {
184        palette.fg_disabled
185    } else if item.active {
186        palette.fg_active
187    } else {
188        palette.fg
189    };
190    let mut inner: Vec<View<Msg>> = vec![View::new(Style {
191        size: Size {
192            width: length(ICON_PX),
193            height: length(ICON_PX),
194        },
195        flex_shrink: 0.0,
196        ..Default::default()
197    })
198    .children(vec![(item.icon)(ICON_PX, fg)])];
199    if let Some(label) = item.label {
200        inner.push(
201            View::new(Style {
202                size: Size { width: auto(), height: percent(1.0_f32) },
203                align_items: Some(AlignItems::Center),
204                ..Default::default()
205            })
206            .text(label, 11.5, fg),
207        );
208    }
209    let mut btn = View::new(Style {
210        flex_direction: FlexDirection::Row,
211        size: Size {
212            width: auto(),
213            height: length(bar_h - 8.0),
214        },
215        flex_shrink: 0.0,
216        align_items: Some(AlignItems::Center),
217        justify_content: Some(JustifyContent::Center),
218        padding: Rect {
219            left: length(7.0_f32),
220            right: length(7.0_f32),
221            top: length(0.0_f32),
222            bottom: length(0.0_f32),
223        },
224        gap: Size {
225            width: length(5.0_f32),
226            height: length(0.0_f32),
227        },
228        ..Default::default()
229    })
230    .radius(6.0)
231    .children(inner);
232    if item.active {
233        btn = btn.fill(palette.bg_active);
234    }
235    if item.enabled {
236        btn = btn.hover_fill(palette.bg_hover).on_click(item.on_click);
237    }
238    btn
239}
240
241fn separator<Msg: Clone + 'static>(bar_h: f32, palette: &ToolbarPalette) -> View<Msg> {
242    View::new(Style {
243        size: Size {
244            width: length(1.0_f32),
245            height: length(bar_h - 16.0),
246        },
247        flex_shrink: 0.0,
248        margin: Rect {
249            left: length(5.0_f32),
250            right: length(5.0_f32),
251            top: length(0.0_f32),
252            bottom: length(0.0_f32),
253        },
254        ..Default::default()
255    })
256    .fill(palette.separator)
257}