gioser

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definitions

⎇ c5a232cf24788ab75a850ffe81eca5a7af50e2f8 widgets/breadcrumb/src/lib.rs Rust · 129 lines · 3.7 KB History Who touched what raw

chore: refresco desde el monorepo — 27 crates nuevos (anim, lottie, svg, mesh, image, layer, test, wire-view, widgets rag-sidebar/router/lazy-list/rive-button) y sync de los 95 existentes 93253cc22 · 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
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}