tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8
widgets/breadcrumb/src/lib.rs
History
Who touched what
raw
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
9use llimphi_ui::llimphi_layout::taffy::{
10 prelude::{length, percent, FlexDirection, Size, Style},
11 AlignItems, Rect,
12};
13use llimphi_ui::llimphi_raster::peniko::Color;
14use llimphi_ui::llimphi_text::Alignment;
15use llimphi_ui::View;
16use llimphi_icons::{icon_view, Icon};
17use llimphi_theme::Theme;
18
19/// Paleta del breadcrumb.
20#[derive(Debug, Clone, Copy)]
21pub struct BreadcrumbPalette {
22 pub fg_link: Color,
23 pub fg_current: Color,
24 pub fg_separator: Color,
25 pub bg_hover: Color,
26}
27
28impl BreadcrumbPalette {
29 pub fn from_theme(t: &Theme) -> Self {
30 Self {
31 fg_link: t.fg_muted,
32 fg_current: t.fg_text,
33 fg_separator: t.fg_placeholder,
34 bg_hover: t.bg_row_hover,
35 }
36 }
37}
38
39const SEG_H: f32 = 22.0;
40const SEG_PAD: f32 = 6.0;
41const FONT: f32 = 11.5;
42const SEP_BOX: f32 = 12.0;
43
44/// Construye el breadcrumb. `segments` son los labels visibles, en
45/// orden de raíz a hoja. `make_msg(i)` se llama al click en el
46/// segmento `i` (no se llama para el último — la "página actual").
47pub fn breadcrumb_view<Msg, F>(
48 segments: &[&str],
49 make_msg: F,
50 palette: &BreadcrumbPalette,
51) -> View<Msg>
52where
53 Msg: Clone + 'static,
54 F: Fn(usize) -> Msg,
55{
56 let last = segments.len().saturating_sub(1);
57 let mut children: Vec<View<Msg>> = Vec::with_capacity(segments.len() * 2);
58 for (i, &label) in segments.iter().enumerate() {
59 let is_current = i == last;
60 children.push(segment_view(
61 label,
62 is_current,
63 if is_current { None } else { Some(make_msg(i)) },
64 palette,
65 ));
66 if !is_current {
67 children.push(separator_view(palette));
68 }
69 }
70
71 View::new(Style {
72 flex_direction: FlexDirection::Row,
73 size: Size {
74 width: percent(1.0_f32),
75 height: length(SEG_H),
76 },
77 align_items: Some(AlignItems::Center),
78 gap: Size {
79 width: length(2.0_f32),
80 height: length(0.0_f32),
81 },
82 ..Default::default()
83 })
84 .children(children)
85}
86
87fn segment_view<Msg: Clone + 'static>(
88 label: &str,
89 is_current: bool,
90 msg: Option<Msg>,
91 palette: &BreadcrumbPalette,
92) -> View<Msg> {
93 let fg = if is_current { palette.fg_current } else { palette.fg_link };
94 // Ancho por glifos reales (fuente proporcional), no `chars*const`.
95 let seg_w = llimphi_ui::llimphi_text::text_width(label, FONT) + SEG_PAD * 2.0;
96 let mut node = View::new(Style {
97 size: Size {
98 width: length(seg_w),
99 height: length(SEG_H),
100 },
101 align_items: Some(AlignItems::Center),
102 padding: Rect {
103 left: length(SEG_PAD),
104 right: length(SEG_PAD),
105 top: length(0.0_f32),
106 bottom: length(0.0_f32),
107 },
108 flex_shrink: 0.0,
109 ..Default::default()
110 })
111 .text_aligned(label.to_string(), FONT, fg, Alignment::Center)
112 .radius(llimphi_theme::radius::XS);
113 if let Some(m) = msg {
114 node = node.hover_fill(palette.bg_hover).on_click(m);
115 }
116 node
117}
118
119fn separator_view<Msg: Clone + 'static>(palette: &BreadcrumbPalette) -> View<Msg> {
120 View::new(Style {
121 size: Size {
122 width: length(SEP_BOX),
123 height: length(SEP_BOX),
124 },
125 flex_shrink: 0.0,
126 ..Default::default()
127 })
128 .children(vec![icon_view(Icon::ChevronRight, palette.fg_separator, 1.6)])
129}