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-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}