tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ 5ae7c737031521b9a873f3d7cff36261fbf7ae06
widgets/toolbar/src/lib.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
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}