tawasuyu

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definitions

⎇ main widgets/app-header/src/lib.rs Rust · 215 lines · 7.1 KB History Who touched what raw

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

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}