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-app-header` — tira superior estándar de las apps.
2//!
3//! Reproduce el contrato del `nahual-widget-app-header` GPUI: label
4//! dinámico a la izquierda con `flex_grow`, slot a la derecha para
5//! acciones (theme switcher, botones de toolbar, etc.). bg = `bg_panel`,
6//! line-bottom como `border` del theme.
7//!
8//! Uso típico:
9//!
10//! ```ignore
11//! app_header(format!("Log: {} · {} entries", path, n), vec![], &palette)
12//! ```
13
14#![forbid(unsafe_code)]
15
16
17// Sin `std`: este widget no usa nada del sistema —sólo `alloc` para
18// `String`/`Vec`—, y declararlo es lo que le permite cruzar a una jaula
19// WASM o al kernel. Antes enlazaba `std` por omisión (W4.6).
20#![no_std]
21
22extern crate alloc;
23
24use alloc::string::String;
25use alloc::vec::Vec;
26use alloc::vec;
27use llimphi_compositor::llimphi_layout::taffy::{
28 prelude::{length, percent, FlexDirection, Size, Style},
29 AlignItems, Rect,
30};
31use llimphi_compositor::peniko::Color;
32use llimphi_compositor::llimphi_text::Alignment;
33use llimphi_compositor::View;
34use llimphi_widget_panel::{panel_signature_painter, PanelStyle};
35
36/// Paleta del header. Defaults desde el theme global.
37#[derive(Debug, Clone, Copy)]
38pub struct AppHeaderPalette {
39 pub bg: Color,
40 pub border_bottom: Color,
41 pub fg_text: Color,
42 pub height: f32,
43 /// Firma visual: gradient sutil + hairline accent en el top edge. Se
44 /// activa por defecto al construir desde theme. `None` cae al fill
45 /// plano de `bg` (modo back-compat para sitios que arman la palette
46 /// a mano sin theme).
47 pub signature: Option<PanelStyle>,
48}
49
50impl Default for AppHeaderPalette {
51 fn default() -> Self {
52 Self::from_theme(&llimphi_theme::Theme::dark())
53 }
54}
55
56impl AppHeaderPalette {
57 pub fn from_theme(t: &llimphi_theme::Theme) -> Self {
58 Self {
59 bg: t.bg_panel,
60 border_bottom: t.border,
61 fg_text: t.fg_text,
62 height: 40.0,
63 signature: Some(PanelStyle {
64 radius: 0.0,
65 ..PanelStyle::from_theme(t)
66 }),
67 }
68 }
69}
70
71/// Re-export del catálogo de íconos de marca para [`app_header_iconed`].
72pub use llimphi_icons::app_icons::AppIcon;
73
74/// Header con `label` a la izquierda y `actions` a la derecha. `actions`
75/// es vacío para apps sin toolbar; viene como Vec para que la app meta
76/// botones / switcher / status pill / lo que necesite.
77pub fn app_header<Msg: Clone + 'static>(
78 label: impl Into<String>,
79 actions: Vec<View<Msg>>,
80 palette: &AppHeaderPalette,
81) -> View<Msg> {
82 header_impl(None, label, actions, palette)
83}
84
85/// Como [`app_header`] pero con el **ícono de marca** de la app a la izquierda
86/// del título (titlebar con identidad). El ícono se pinta vectorial, en su color
87/// de marca — determinista en toda máquina.
88pub fn app_header_iconed<Msg: Clone + 'static>(
89 icon: AppIcon,
90 label: impl Into<String>,
91 actions: Vec<View<Msg>>,
92 palette: &AppHeaderPalette,
93) -> View<Msg> {
94 header_impl(Some(icon), label, actions, palette)
95}
96
97fn header_impl<Msg: Clone + 'static>(
98 icon: Option<AppIcon>,
99 label: impl Into<String>,
100 actions: Vec<View<Msg>>,
101 palette: &AppHeaderPalette,
102) -> View<Msg> {
103 // Caja del ícono de marca (si hay), cuadrada, centrada vertical.
104 let icon_box = icon.map(|ic| {
105 let side = (palette.height * 0.5).clamp(16.0, 26.0);
106 // Spacer izquierdo (14px) + caja cuadrada del ícono — sin `margin` para
107 // no pelearnos con el tipo LengthPercentageAuto de taffy.
108 View::new(Style {
109 flex_direction: FlexDirection::Row,
110 size: Size { width: length(side + 14.0), height: length(palette.height) },
111 flex_shrink: 0.0,
112 padding: Rect {
113 left: length(14.0_f32),
114 right: length(0.0_f32),
115 top: length(0.0_f32),
116 bottom: length(0.0_f32),
117 },
118 align_items: Some(AlignItems::CENTER),
119 ..Default::default()
120 })
121 .children(vec![View::new(Style {
122 size: Size { width: length(side), height: length(side) },
123 flex_shrink: 0.0,
124 ..Default::default()
125 })
126 .children(vec![llimphi_icons::app_icons::app_icon_view(ic, 1.8)])])
127 });
128 // Con ícono, el título arranca más cerca (el ícono ya da el aire izquierdo).
129 let label_left: f32 = if icon.is_some() { 10.0 } else { 16.0 };
130 let label_view = View::new(Style {
131 size: Size {
132 width: percent(1.0_f32),
133 height: length(palette.height),
134 },
135 flex_grow: 1.0,
136 padding: Rect {
137 left: length(label_left),
138 right: length(16.0_f32),
139 top: length(0.0_f32),
140 bottom: length(0.0_f32),
141 },
142 align_items: Some(AlignItems::CENTER),
143 ..Default::default()
144 })
145 .text_aligned(label.into(), 14.0, palette.fg_text, Alignment::Start)
146 // Semántica: el label del header es el **título** de la app/ventana —
147 // rol Heading para que el lector lo enuncie como tal y los usuarios
148 // puedan saltar entre headings con sus atajos.
149 .role(llimphi_compositor::Role::Heading);
150
151 let actions_view = View::new(Style {
152 flex_direction: FlexDirection::Row,
153 size: Size {
154 width: llimphi_compositor::llimphi_layout::taffy::prelude::Dimension::auto(),
155 height: length(palette.height),
156 },
157 align_items: Some(AlignItems::CENTER),
158 padding: Rect {
159 left: length(8.0_f32),
160 right: length(8.0_f32),
161 top: length(0.0_f32),
162 bottom: length(0.0_f32),
163 },
164 gap: Size {
165 width: length(6.0_f32),
166 height: length(0.0_f32),
167 },
168 ..Default::default()
169 })
170 .children(actions);
171
172 // Bottom border: el header rellena `bg` (o aplica la firma si está
173 // habilitada), y debajo va una línea 1px de `border_bottom`. Lo
174 // metemos como un wrapper column.
175 let bar_style = Style {
176 flex_direction: FlexDirection::Row,
177 size: Size {
178 width: percent(1.0_f32),
179 height: length(palette.height),
180 },
181 align_items: Some(AlignItems::CENTER),
182 ..Default::default()
183 };
184 let mut bar_children: Vec<View<Msg>> = Vec::with_capacity(3);
185 if let Some(b) = icon_box {
186 bar_children.push(b);
187 }
188 bar_children.push(label_view);
189 bar_children.push(actions_view);
190 let bar = match palette.signature {
191 Some(style) => View::new(bar_style)
192 .paint_with(panel_signature_painter(style))
193 .children(bar_children),
194 None => View::new(bar_style).fill(palette.bg).children(bar_children),
195 };
196
197 let underline = View::new(Style {
198 size: Size {
199 width: percent(1.0_f32),
200 height: length(1.0_f32),
201 },
202 ..Default::default()
203 })
204 .fill(palette.border_bottom);
205
206 View::new(Style {
207 flex_direction: FlexDirection::Column,
208 size: Size {
209 width: percent(1.0_f32),
210 height: length(palette.height + 1.0),
211 },
212 ..Default::default()
213 })
214 .children(vec![bar, underline])
215}