tawasuyu

tawasuyu/llimphi

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

Rust 229TOML 101Markdown 82Shell 24052 Definiciones

⎇ main widgets/status-bar/src/lib.rs Rust · 256 líneas · 7.6 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

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}