tawasuyu

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definiciones

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

feat: llimphi standalone — framework UI soberano extraído del monorepo e65e9cc62 · 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
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}