//! `llimphi-widget-breadcrumb` — ruta navegable con separadores chevron. //! //! Patrón clásico: `home › docs › 2026 › nota.md`. Cada segmento es //! clicable y emite un Msg con su índice. El último segmento (la //! "página actual") se renderiza con énfasis y sin click handler. #![forbid(unsafe_code)] // Sin `std`: este widget no usa nada del sistema —sólo `alloc` para // `String`/`Vec`—, y declararlo es lo que le permite cruzar a una jaula // WASM o al kernel. Antes enlazaba `std` por omisión (W4.6). #![no_std] extern crate alloc; use alloc::vec::Vec; use alloc::vec; use alloc::string::ToString; use llimphi_compositor::llimphi_layout::taffy::{ prelude::{length, percent, FlexDirection, Size, Style}, AlignItems, Rect, }; use llimphi_compositor::peniko::Color; use llimphi_compositor::llimphi_text::Alignment; use llimphi_compositor::View; use llimphi_icons::{icon_view, Icon}; use llimphi_theme::Theme; /// Paleta del breadcrumb. #[derive(Debug, Clone, Copy)] pub struct BreadcrumbPalette { pub fg_link: Color, pub fg_current: Color, pub fg_separator: Color, pub bg_hover: Color, } impl BreadcrumbPalette { pub fn from_theme(t: &Theme) -> Self { Self { fg_link: t.fg_muted, fg_current: t.fg_text, fg_separator: t.fg_placeholder, bg_hover: t.bg_row_hover, } } } const SEG_H: f32 = 22.0; const SEG_PAD: f32 = 6.0; const FONT: f32 = 11.5; const SEP_BOX: f32 = 12.0; /// Construye el breadcrumb. `segments` son los labels visibles, en /// orden de raíz a hoja. `make_msg(i)` se llama al click en el /// segmento `i` (no se llama para el último — la "página actual"). pub fn breadcrumb_view( segments: &[&str], make_msg: F, palette: &BreadcrumbPalette, ) -> View where Msg: Clone + 'static, F: Fn(usize) -> Msg, { let last = segments.len().saturating_sub(1); let mut children: Vec> = Vec::with_capacity(segments.len() * 2); for (i, &label) in segments.iter().enumerate() { let is_current = i == last; children.push(segment_view( label, is_current, if is_current { None } else { Some(make_msg(i)) }, palette, )); if !is_current { children.push(separator_view(palette)); } } View::new(Style { flex_direction: FlexDirection::Row, size: Size { width: percent(1.0_f32), height: length(SEG_H), }, align_items: Some(AlignItems::CENTER), gap: Size { width: length(2.0_f32), height: length(0.0_f32), }, ..Default::default() }) .children(children) } fn segment_view( label: &str, is_current: bool, msg: Option, palette: &BreadcrumbPalette, ) -> View { let fg = if is_current { palette.fg_current } else { palette.fg_link }; // Ancho por glifos reales (fuente proporcional), no `chars*const`. let seg_w = llimphi_compositor::llimphi_text::text_width(label, FONT) + SEG_PAD * 2.0; let mut node = View::new(Style { size: Size { width: length(seg_w), height: length(SEG_H), }, align_items: Some(AlignItems::CENTER), padding: Rect { left: length(SEG_PAD), right: length(SEG_PAD), top: length(0.0_f32), bottom: length(0.0_f32), }, flex_shrink: 0.0, ..Default::default() }) .text_aligned(label.to_string(), FONT, fg, Alignment::Center) .radius(llimphi_theme::radius::XS); if let Some(m) = msg { // Los segmentos previos navegan: son enlaces. El último es dónde // estamos parados — texto, no acción. node = node .role(llimphi_compositor::Role::Link) .hover_fill(palette.bg_hover) .on_click(m); } else { node = node.role(llimphi_compositor::Role::Label); } node } fn separator_view(palette: &BreadcrumbPalette) -> View { View::new(Style { size: Size { width: length(SEP_BOX), height: length(SEP_BOX), }, flex_shrink: 0.0, ..Default::default() }) .children(vec![icon_view(Icon::ChevronRight, palette.fg_separator, 1.6)]) }