tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ c1b11b2c01d7623a4f346519b9d1ec435be30b95
widgets/status-bar/src/lib.rs
Historia
Quién tocó qué
crudo
feat: llimphi standalone — framework UI soberano extraído del monorepo
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
13use llimphi_ui::llimphi_layout::taffy::{
14 prelude::{length, percent, FlexDirection, Size, Style},
15 AlignItems, JustifyContent, Rect,
16};
17use llimphi_ui::llimphi_raster::peniko::Color;
18use llimphi_ui::llimphi_text::Alignment;
19use llimphi_ui::View;
20use llimphi_icons::{icon_view, Icon};
21use llimphi_theme::Theme;
22use llimphi_widget_panel::{panel_signature_painter, PanelStyle};
23
24/// Paleta de la barra de estado.
25#[derive(Debug, Clone, Copy)]
26pub struct StatusBarPalette {
27 pub bg: Color,
28 pub fg: Color,
29 pub fg_muted: Color,
30 pub bg_hover: Color,
31 pub border: Color,
32 /// Firma visual de la barra: gradient sutil + hairline accent en su
33 /// top edge — el hairline funciona como "techo" que separa la barra
34 /// de la zona de contenido. `None` cae al fill plano + border top
35 /// del modo previo (back-compat).
36 pub signature: Option<PanelStyle>,
37}
38
39impl StatusBarPalette {
40 pub fn from_theme(t: &Theme) -> Self {
41 Self {
42 bg: t.bg_panel_alt,
43 fg: t.fg_text,
44 fg_muted: t.fg_muted,
45 bg_hover: t.bg_row_hover,
46 border: t.border,
47 signature: Some(PanelStyle {
48 radius: 0.0,
49 bg_base: t.bg_panel_alt,
50 ..PanelStyle::from_theme(t)
51 }),
52 }
53 }
54}
55
56/// Un segmento de la barra. `icon` y `on_click` son opcionales.
57#[derive(Clone)]
58pub struct StatusSegment<Msg> {
59 pub text: String,
60 pub icon: Option<Icon>,
61 pub on_click: Option<Msg>,
62 /// Si `true`, usa `fg` en vez de `fg_muted` — útil para destacar
63 /// estados importantes (ej. "modificado").
64 pub emphasized: bool,
65}
66
67impl<Msg> StatusSegment<Msg> {
68 pub fn text(text: impl Into<String>) -> Self {
69 Self {
70 text: text.into(),
71 icon: None,
72 on_click: None,
73 emphasized: false,
74 }
75 }
76 pub fn with_icon(mut self, icon: Icon) -> Self {
77 self.icon = Some(icon);
78 self
79 }
80 pub fn clickable(mut self, msg: Msg) -> Self {
81 self.on_click = Some(msg);
82 self
83 }
84 pub fn emphasized(mut self) -> Self {
85 self.emphasized = true;
86 self
87 }
88}
89
90const BAR_H: f32 = 22.0;
91const SEG_GAP: f32 = 14.0;
92const FONT_SIZE: f32 = 11.0;
93const ICON_SIZE: f32 = 12.0;
94
95pub fn status_bar_view<Msg: Clone + 'static>(
96 left: Vec<StatusSegment<Msg>>,
97 center: Vec<StatusSegment<Msg>>,
98 right: Vec<StatusSegment<Msg>>,
99 palette: &StatusBarPalette,
100) -> View<Msg> {
101 let make_region = |segs: Vec<StatusSegment<Msg>>, justify: JustifyContent| -> View<Msg> {
102 let children: Vec<View<Msg>> = segs
103 .into_iter()
104 .map(|s| segment_view(s, palette))
105 .collect();
106 View::new(Style {
107 flex_direction: FlexDirection::Row,
108 size: Size {
109 width: percent(1.0_f32),
110 height: percent(1.0_f32),
111 },
112 flex_grow: 1.0,
113 align_items: Some(AlignItems::Center),
114 justify_content: Some(justify),
115 gap: Size {
116 width: length(SEG_GAP),
117 height: length(0.0_f32),
118 },
119 ..Default::default()
120 })
121 .children(children)
122 };
123
124 let left_region = make_region(left, JustifyContent::FlexStart);
125 let center_region = make_region(center, JustifyContent::Center);
126 let right_region = make_region(right, JustifyContent::FlexEnd);
127
128 // Modo con firma: la barra trae su propio hairline accent en el top
129 // edge — reemplaza el border plano del modo previo.
130 let bar_style = Style {
131 flex_direction: FlexDirection::Row,
132 size: Size {
133 width: percent(1.0_f32),
134 height: length(BAR_H),
135 },
136 padding: Rect {
137 left: length(10.0_f32),
138 right: length(10.0_f32),
139 top: length(0.0_f32),
140 bottom: length(0.0_f32),
141 },
142 align_items: Some(AlignItems::Center),
143 flex_shrink: 0.0,
144 ..Default::default()
145 };
146
147 if let Some(style) = palette.signature {
148 return View::new(bar_style)
149 .paint_with(panel_signature_painter(style))
150 .children(vec![left_region, center_region, right_region]);
151 }
152
153 // Back-compat: fill plano + border top 1px en el wrapper column.
154 let border = View::new(Style {
155 size: Size {
156 width: percent(1.0_f32),
157 height: length(1.0_f32),
158 },
159 flex_shrink: 0.0,
160 ..Default::default()
161 })
162 .fill(palette.border);
163
164 let bar = View::new(bar_style)
165 .fill(palette.bg)
166 .children(vec![left_region, center_region, right_region]);
167
168 View::new(Style {
169 flex_direction: FlexDirection::Column,
170 size: Size {
171 width: percent(1.0_f32),
172 height: length(BAR_H + 1.0),
173 },
174 flex_shrink: 0.0,
175 ..Default::default()
176 })
177 .children(vec![border, bar])
178}
179
180fn segment_view<Msg: Clone + 'static>(
181 seg: StatusSegment<Msg>,
182 palette: &StatusBarPalette,
183) -> View<Msg> {
184 let fg = if seg.emphasized { palette.fg } else { palette.fg_muted };
185 let approx_w = seg.text.chars().count() as f32 * 6.0
186 + if seg.icon.is_some() { ICON_SIZE + 4.0 } else { 0.0 }
187 + 12.0;
188
189 let mut children: Vec<View<Msg>> = Vec::with_capacity(2);
190 if let Some(icon) = seg.icon {
191 children.push(
192 View::new(Style {
193 size: Size {
194 width: length(ICON_SIZE),
195 height: length(ICON_SIZE),
196 },
197 flex_shrink: 0.0,
198 ..Default::default()
199 })
200 .children(vec![icon_view(icon, fg, 1.4)]),
201 );
202 }
203 children.push(
204 View::new(Style {
205 size: Size {
206 width: percent(1.0_f32),
207 height: percent(1.0_f32),
208 },
209 flex_grow: 1.0,
210 align_items: Some(AlignItems::Center),
211 ..Default::default()
212 })
213 .text_aligned(seg.text.clone(), FONT_SIZE, fg, Alignment::Start),
214 );
215
216 let mut node = View::new(Style {
217 flex_direction: FlexDirection::Row,
218 size: Size {
219 width: length(approx_w),
220 height: percent(1.0_f32),
221 },
222 align_items: Some(AlignItems::Center),
223 padding: Rect {
224 left: length(6.0_f32),
225 right: length(6.0_f32),
226 top: length(0.0_f32),
227 bottom: length(0.0_f32),
228 },
229 gap: Size {
230 width: length(4.0_f32),
231 height: length(0.0_f32),
232 },
233 flex_shrink: 0.0,
234 ..Default::default()
235 })
236 .children(children);
237
238 if let Some(msg) = seg.on_click {
239 node = node.hover_fill(palette.bg_hover).on_click(msg);
240 }
241 node
242}