tawasuyu

tawasuyu/llimphi

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

Rust 484Markdown 258TOML 131RON 38118 Definiciones

⎇ main widgets/breadcrumb/src/lib.rs Rust · 147 líneas · 4.3 KB Historia Quién tocó qué crudo

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

1//! `llimphi-widget-breadcrumb` — ruta navegable con separadores chevron.
2//!
3//! Patrón clásico: `home › docs › 2026 › nota.md`. Cada segmento es
4//! clicable y emite un Msg con su índice. El último segmento (la
5//! "página actual") se renderiza con énfasis y sin click handler.
6
7#![forbid(unsafe_code)]
8
9
10// Sin `std`: este widget no usa nada del sistema —sólo `alloc` para
11// `String`/`Vec`—, y declararlo es lo que le permite cruzar a una jaula
12// WASM o al kernel. Antes enlazaba `std` por omisión (W4.6).
13#![no_std]
14
15extern crate alloc;
16
17use alloc::vec::Vec;
18use alloc::vec;
19use alloc::string::ToString;
20use llimphi_compositor::llimphi_layout::taffy::{
21    prelude::{length, percent, FlexDirection, Size, Style},
22    AlignItems, Rect,
23};
24use llimphi_compositor::peniko::Color;
25use llimphi_compositor::llimphi_text::Alignment;
26use llimphi_compositor::View;
27use llimphi_icons::{icon_view, Icon};
28use llimphi_theme::Theme;
29
30/// Paleta del breadcrumb.
31#[derive(Debug, Clone, Copy)]
32pub struct BreadcrumbPalette {
33    pub fg_link: Color,
34    pub fg_current: Color,
35    pub fg_separator: Color,
36    pub bg_hover: Color,
37}
38
39impl BreadcrumbPalette {
40    pub fn from_theme(t: &Theme) -> Self {
41        Self {
42            fg_link: t.fg_muted,
43            fg_current: t.fg_text,
44            fg_separator: t.fg_placeholder,
45            bg_hover: t.bg_row_hover,
46        }
47    }
48}
49
50const SEG_H: f32 = 22.0;
51const SEG_PAD: f32 = 6.0;
52const FONT: f32 = 11.5;
53const SEP_BOX: f32 = 12.0;
54
55/// Construye el breadcrumb. `segments` son los labels visibles, en
56/// orden de raíz a hoja. `make_msg(i)` se llama al click en el
57/// segmento `i` (no se llama para el último — la "página actual").
58pub fn breadcrumb_view<Msg, F>(
59    segments: &[&str],
60    make_msg: F,
61    palette: &BreadcrumbPalette,
62) -> View<Msg>
63where
64    Msg: Clone + 'static,
65    F: Fn(usize) -> Msg,
66{
67    let last = segments.len().saturating_sub(1);
68    let mut children: Vec<View<Msg>> = Vec::with_capacity(segments.len() * 2);
69    for (i, &label) in segments.iter().enumerate() {
70        let is_current = i == last;
71        children.push(segment_view(
72            label,
73            is_current,
74            if is_current { None } else { Some(make_msg(i)) },
75            palette,
76        ));
77        if !is_current {
78            children.push(separator_view(palette));
79        }
80    }
81
82    View::new(Style {
83        flex_direction: FlexDirection::Row,
84        size: Size {
85            width: percent(1.0_f32),
86            height: length(SEG_H),
87        },
88        align_items: Some(AlignItems::CENTER),
89        gap: Size {
90            width: length(2.0_f32),
91            height: length(0.0_f32),
92        },
93        ..Default::default()
94    })
95    .children(children)
96}
97
98fn segment_view<Msg: Clone + 'static>(
99    label: &str,
100    is_current: bool,
101    msg: Option<Msg>,
102    palette: &BreadcrumbPalette,
103) -> View<Msg> {
104    let fg = if is_current { palette.fg_current } else { palette.fg_link };
105    // Ancho por glifos reales (fuente proporcional), no `chars*const`.
106    let seg_w = llimphi_compositor::llimphi_text::text_width(label, FONT) + SEG_PAD * 2.0;
107    let mut node = View::new(Style {
108        size: Size {
109            width: length(seg_w),
110            height: length(SEG_H),
111        },
112        align_items: Some(AlignItems::CENTER),
113        padding: Rect {
114            left: length(SEG_PAD),
115            right: length(SEG_PAD),
116            top: length(0.0_f32),
117            bottom: length(0.0_f32),
118        },
119        flex_shrink: 0.0,
120        ..Default::default()
121    })
122    .text_aligned(label.to_string(), FONT, fg, Alignment::Center)
123    .radius(llimphi_theme::radius::XS);
124    if let Some(m) = msg {
125        // Los segmentos previos navegan: son enlaces. El último es dónde
126        // estamos parados — texto, no acción.
127        node = node
128            .role(llimphi_compositor::Role::Link)
129            .hover_fill(palette.bg_hover)
130            .on_click(m);
131    } else {
132        node = node.role(llimphi_compositor::Role::Label);
133    }
134    node
135}
136
137fn separator_view<Msg: Clone + 'static>(palette: &BreadcrumbPalette) -> View<Msg> {
138    View::new(Style {
139        size: Size {
140            width: length(SEP_BOX),
141            height: length(SEP_BOX),
142        },
143        flex_shrink: 0.0,
144        ..Default::default()
145    })
146    .children(vec![icon_view(Icon::ChevronRight, palette.fg_separator, 1.6)])
147}