tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
1//! `llimphi-widget-status-bar` — barra de estado inferior.
2//!
3//! Patrón clásico de IDEs/editores: barra delgada en el borde inferior
4//! de la ventana con tres regiones (left/center/right). Cada región
5//! tiene N segmentos, cada uno puede llevar icono + texto + handler de
6//! click opcional.
7//!
8//! Útil para mostrar: rama git activa, posición del cursor, tipo de
9//! archivo, modo (insert/normal), notificaciones pendientes, etc.
10
11#![forbid(unsafe_code)]
12
13
14// Sin `std`: este widget no usa nada del sistema —sólo `alloc` para
15// `String`/`Vec`—, y declararlo es lo que le permite cruzar a una jaula
16// WASM o al kernel. Antes enlazaba `std` por omisión (W4.6).
17#![no_std]
18
19extern crate alloc;
20
21use alloc::string::String;
22use alloc::vec::Vec;
23use alloc::vec;
24use llimphi_compositor::llimphi_layout::taffy::{
25 prelude::{length, percent, FlexDirection, Size, Style},
26 AlignItems, JustifyContent, Rect,
27};
28use llimphi_compositor::peniko::Color;
29use llimphi_compositor::llimphi_text::Alignment;
30use llimphi_compositor::View;
31use llimphi_icons::{icon_view, Icon};
32use llimphi_theme::Theme;
33use llimphi_widget_panel::{panel_signature_painter, PanelStyle};
34
35/// Paleta de la barra de estado.
36#[derive(Debug, Clone, Copy)]
37pub struct StatusBarPalette {
38 pub bg: Color,
39 pub fg: Color,
40 pub fg_muted: Color,
41 pub bg_hover: Color,
42 pub border: Color,
43 /// Firma visual de la barra: gradient sutil + hairline accent en su
44 /// top edge — el hairline funciona como "techo" que separa la barra
45 /// de la zona de contenido. `None` cae al fill plano + border top
46 /// del modo previo (back-compat).
47 pub signature: Option<PanelStyle>,
48}
49
50impl StatusBarPalette {
51 pub fn from_theme(t: &Theme) -> Self {
52 Self {
53 bg: t.bg_panel_alt,
54 fg: t.fg_text,
55 fg_muted: t.fg_muted,
56 bg_hover: t.bg_row_hover,
57 border: t.border,
58 signature: Some(PanelStyle {
59 radius: 0.0,
60 bg_base: t.bg_panel_alt,
61 ..PanelStyle::from_theme(t)
62 }),
63 }
64 }
65}
66
67/// Un segmento de la barra. `icon` y `on_click` son opcionales.
68#[derive(Clone)]
69pub struct StatusSegment<Msg> {
70 pub text: String,
71 pub icon: Option<Icon>,
72 pub on_click: Option<Msg>,
73 /// Si `true`, usa `fg` en vez de `fg_muted` — útil para destacar
74 /// estados importantes (ej. "modificado").
75 pub emphasized: bool,
76}
77
78impl<Msg> StatusSegment<Msg> {
79 pub fn text(text: impl Into<String>) -> Self {
80 Self {
81 text: text.into(),
82 icon: None,
83 on_click: None,
84 emphasized: false,
85 }
86 }
87 pub fn with_icon(mut self, icon: Icon) -> Self {
88 self.icon = Some(icon);
89 self
90 }
91 pub fn clickable(mut self, msg: Msg) -> Self {
92 self.on_click = Some(msg);
93 self
94 }
95 pub fn emphasized(mut self) -> Self {
96 self.emphasized = true;
97 self
98 }
99}
100
101const BAR_H: f32 = 22.0;
102const SEG_GAP: f32 = 14.0;
103const FONT_SIZE: f32 = 11.0;
104const ICON_SIZE: f32 = 12.0;
105
106pub fn status_bar_view<Msg: Clone + 'static>(
107 left: Vec<StatusSegment<Msg>>,
108 center: Vec<StatusSegment<Msg>>,
109 right: Vec<StatusSegment<Msg>>,
110 palette: &StatusBarPalette,
111) -> View<Msg> {
112 let make_region = |segs: Vec<StatusSegment<Msg>>, justify: JustifyContent| -> View<Msg> {
113 let children: Vec<View<Msg>> = segs
114 .into_iter()
115 .map(|s| segment_view(s, palette))
116 .collect();
117 View::new(Style {
118 flex_direction: FlexDirection::Row,
119 size: Size {
120 width: percent(1.0_f32),
121 height: percent(1.0_f32),
122 },
123 flex_grow: 1.0,
124 align_items: Some(AlignItems::CENTER),
125 justify_content: Some(justify),
126 gap: Size {
127 width: length(SEG_GAP),
128 height: length(0.0_f32),
129 },
130 ..Default::default()
131 })
132 .children(children)
133 };
134
135 let left_region = make_region(left, JustifyContent::FLEX_START);
136 let center_region = make_region(center, JustifyContent::CENTER);
137 let right_region = make_region(right, JustifyContent::FLEX_END);
138
139 // Modo con firma: la barra trae su propio hairline accent en el top
140 // edge — reemplaza el border plano del modo previo.
141 let bar_style = Style {
142 flex_direction: FlexDirection::Row,
143 size: Size {
144 width: percent(1.0_f32),
145 height: length(BAR_H),
146 },
147 padding: Rect {
148 left: length(10.0_f32),
149 right: length(10.0_f32),
150 top: length(0.0_f32),
151 bottom: length(0.0_f32),
152 },
153 align_items: Some(AlignItems::CENTER),
154 flex_shrink: 0.0,
155 ..Default::default()
156 };
157
158 if let Some(style) = palette.signature {
159 return View::new(bar_style)
160 .paint_with(panel_signature_painter(style))
161 .children(vec![left_region, center_region, right_region]);
162 }
163
164 // Back-compat: fill plano + border top 1px en el wrapper column.
165 let border = View::new(Style {
166 size: Size {
167 width: percent(1.0_f32),
168 height: length(1.0_f32),
169 },
170 flex_shrink: 0.0,
171 ..Default::default()
172 })
173 .fill(palette.border);
174
175 let bar = View::new(bar_style)
176 .fill(palette.bg)
177 .children(vec![left_region, center_region, right_region]);
178
179 View::new(Style {
180 flex_direction: FlexDirection::Column,
181 size: Size {
182 width: percent(1.0_f32),
183 height: length(BAR_H + 1.0),
184 },
185 flex_shrink: 0.0,
186 ..Default::default()
187 })
188 // Status: región informativa. A diferencia de `Alert`, el lector la
189 // anuncia sin interrumpir lo que esté leyendo.
190 .role(llimphi_compositor::Role::Status)
191 .children(vec![border, bar])
192}
193
194fn segment_view<Msg: Clone + 'static>(
195 seg: StatusSegment<Msg>,
196 palette: &StatusBarPalette,
197) -> View<Msg> {
198 let fg = if seg.emphasized { palette.fg } else { palette.fg_muted };
199 let approx_w = seg.text.chars().count() as f32 * 6.0
200 + if seg.icon.is_some() { ICON_SIZE + 4.0 } else { 0.0 }
201 + 12.0;
202
203 let mut children: Vec<View<Msg>> = Vec::with_capacity(2);
204 if let Some(icon) = seg.icon {
205 children.push(
206 View::new(Style {
207 size: Size {
208 width: length(ICON_SIZE),
209 height: length(ICON_SIZE),
210 },
211 flex_shrink: 0.0,
212 ..Default::default()
213 })
214 .children(vec![icon_view(icon, fg, 1.4)]),
215 );
216 }
217 children.push(
218 View::new(Style {
219 size: Size {
220 width: percent(1.0_f32),
221 height: percent(1.0_f32),
222 },
223 flex_grow: 1.0,
224 align_items: Some(AlignItems::CENTER),
225 ..Default::default()
226 })
227 .text_aligned(seg.text.clone(), FONT_SIZE, fg, Alignment::Start),
228 );
229
230 let mut node = View::new(Style {
231 flex_direction: FlexDirection::Row,
232 size: Size {
233 width: length(approx_w),
234 height: percent(1.0_f32),
235 },
236 align_items: Some(AlignItems::CENTER),
237 padding: Rect {
238 left: length(6.0_f32),
239 right: length(6.0_f32),
240 top: length(0.0_f32),
241 bottom: length(0.0_f32),
242 },
243 gap: Size {
244 width: length(4.0_f32),
245 height: length(0.0_f32),
246 },
247 flex_shrink: 0.0,
248 ..Default::default()
249 })
250 .children(children);
251
252 if let Some(msg) = seg.on_click {
253 node = node.hover_fill(palette.bg_hover).on_click(msg);
254 }
255 node
256}