feat: llimphi standalone — framework UI soberano extraído del monorepo

Motor gráfico Llimphi como workspace independiente: bucle Elm
(input→update→view→layout→raster→present) sobre wgpu+vello+taffy+parley.
Núcleo (hal/raster/layout/text/ui/theme/surface/motion/icons) + ~40 widgets
+ módulos, sin dependencias al resto del monorepo. cargo check --workspace
pasa (64 crates). Puerta de entrada: cargo run -p llimphi-ui --example counter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 04:23:42 +00:00
commit e65e9cc623
286 changed files with 46136 additions and 0 deletions
+14
View File
@@ -0,0 +1,14 @@
[package]
name = "llimphi-widget-status-bar"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "llimphi-widget-status-bar — barra inferior con segmentos left/center/right configurables. Cada segmento puede llevar icono opcional y handler de click."
[dependencies]
llimphi-ui = { workspace = true }
llimphi-theme = { workspace = true }
llimphi-icons = { workspace = true }
llimphi-widget-panel = { workspace = true }
+242
View File
@@ -0,0 +1,242 @@
//! `llimphi-widget-status-bar` — barra de estado inferior.
//!
//! Patrón clásico de IDEs/editores: barra delgada en el borde inferior
//! de la ventana con tres regiones (left/center/right). Cada región
//! tiene N segmentos, cada uno puede llevar icono + texto + handler de
//! click opcional.
//!
//! Útil para mostrar: rama git activa, posición del cursor, tipo de
//! archivo, modo (insert/normal), notificaciones pendientes, etc.
#![forbid(unsafe_code)]
use llimphi_ui::llimphi_layout::taffy::{
prelude::{length, percent, FlexDirection, Size, Style},
AlignItems, JustifyContent, Rect,
};
use llimphi_ui::llimphi_raster::peniko::Color;
use llimphi_ui::llimphi_text::Alignment;
use llimphi_ui::View;
use llimphi_icons::{icon_view, Icon};
use llimphi_theme::Theme;
use llimphi_widget_panel::{panel_signature_painter, PanelStyle};
/// Paleta de la barra de estado.
#[derive(Debug, Clone, Copy)]
pub struct StatusBarPalette {
pub bg: Color,
pub fg: Color,
pub fg_muted: Color,
pub bg_hover: Color,
pub border: Color,
/// Firma visual de la barra: gradient sutil + hairline accent en su
/// top edge — el hairline funciona como "techo" que separa la barra
/// de la zona de contenido. `None` cae al fill plano + border top
/// del modo previo (back-compat).
pub signature: Option<PanelStyle>,
}
impl StatusBarPalette {
pub fn from_theme(t: &Theme) -> Self {
Self {
bg: t.bg_panel_alt,
fg: t.fg_text,
fg_muted: t.fg_muted,
bg_hover: t.bg_row_hover,
border: t.border,
signature: Some(PanelStyle {
radius: 0.0,
bg_base: t.bg_panel_alt,
..PanelStyle::from_theme(t)
}),
}
}
}
/// Un segmento de la barra. `icon` y `on_click` son opcionales.
#[derive(Clone)]
pub struct StatusSegment<Msg> {
pub text: String,
pub icon: Option<Icon>,
pub on_click: Option<Msg>,
/// Si `true`, usa `fg` en vez de `fg_muted` — útil para destacar
/// estados importantes (ej. "modificado").
pub emphasized: bool,
}
impl<Msg> StatusSegment<Msg> {
pub fn text(text: impl Into<String>) -> Self {
Self {
text: text.into(),
icon: None,
on_click: None,
emphasized: false,
}
}
pub fn with_icon(mut self, icon: Icon) -> Self {
self.icon = Some(icon);
self
}
pub fn clickable(mut self, msg: Msg) -> Self {
self.on_click = Some(msg);
self
}
pub fn emphasized(mut self) -> Self {
self.emphasized = true;
self
}
}
const BAR_H: f32 = 22.0;
const SEG_GAP: f32 = 14.0;
const FONT_SIZE: f32 = 11.0;
const ICON_SIZE: f32 = 12.0;
pub fn status_bar_view<Msg: Clone + 'static>(
left: Vec<StatusSegment<Msg>>,
center: Vec<StatusSegment<Msg>>,
right: Vec<StatusSegment<Msg>>,
palette: &StatusBarPalette,
) -> View<Msg> {
let make_region = |segs: Vec<StatusSegment<Msg>>, justify: JustifyContent| -> View<Msg> {
let children: Vec<View<Msg>> = segs
.into_iter()
.map(|s| segment_view(s, palette))
.collect();
View::new(Style {
flex_direction: FlexDirection::Row,
size: Size {
width: percent(1.0_f32),
height: percent(1.0_f32),
},
flex_grow: 1.0,
align_items: Some(AlignItems::Center),
justify_content: Some(justify),
gap: Size {
width: length(SEG_GAP),
height: length(0.0_f32),
},
..Default::default()
})
.children(children)
};
let left_region = make_region(left, JustifyContent::FlexStart);
let center_region = make_region(center, JustifyContent::Center);
let right_region = make_region(right, JustifyContent::FlexEnd);
// Modo con firma: la barra trae su propio hairline accent en el top
// edge — reemplaza el border plano del modo previo.
let bar_style = Style {
flex_direction: FlexDirection::Row,
size: Size {
width: percent(1.0_f32),
height: length(BAR_H),
},
padding: Rect {
left: length(10.0_f32),
right: length(10.0_f32),
top: length(0.0_f32),
bottom: length(0.0_f32),
},
align_items: Some(AlignItems::Center),
flex_shrink: 0.0,
..Default::default()
};
if let Some(style) = palette.signature {
return View::new(bar_style)
.paint_with(panel_signature_painter(style))
.children(vec![left_region, center_region, right_region]);
}
// Back-compat: fill plano + border top 1px en el wrapper column.
let border = View::new(Style {
size: Size {
width: percent(1.0_f32),
height: length(1.0_f32),
},
flex_shrink: 0.0,
..Default::default()
})
.fill(palette.border);
let bar = View::new(bar_style)
.fill(palette.bg)
.children(vec![left_region, center_region, right_region]);
View::new(Style {
flex_direction: FlexDirection::Column,
size: Size {
width: percent(1.0_f32),
height: length(BAR_H + 1.0),
},
flex_shrink: 0.0,
..Default::default()
})
.children(vec![border, bar])
}
fn segment_view<Msg: Clone + 'static>(
seg: StatusSegment<Msg>,
palette: &StatusBarPalette,
) -> View<Msg> {
let fg = if seg.emphasized { palette.fg } else { palette.fg_muted };
let approx_w = seg.text.chars().count() as f32 * 6.0
+ if seg.icon.is_some() { ICON_SIZE + 4.0 } else { 0.0 }
+ 12.0;
let mut children: Vec<View<Msg>> = Vec::with_capacity(2);
if let Some(icon) = seg.icon {
children.push(
View::new(Style {
size: Size {
width: length(ICON_SIZE),
height: length(ICON_SIZE),
},
flex_shrink: 0.0,
..Default::default()
})
.children(vec![icon_view(icon, fg, 1.4)]),
);
}
children.push(
View::new(Style {
size: Size {
width: percent(1.0_f32),
height: percent(1.0_f32),
},
flex_grow: 1.0,
align_items: Some(AlignItems::Center),
..Default::default()
})
.text_aligned(seg.text.clone(), FONT_SIZE, fg, Alignment::Start),
);
let mut node = View::new(Style {
flex_direction: FlexDirection::Row,
size: Size {
width: length(approx_w),
height: percent(1.0_f32),
},
align_items: Some(AlignItems::Center),
padding: Rect {
left: length(6.0_f32),
right: length(6.0_f32),
top: length(0.0_f32),
bottom: length(0.0_f32),
},
gap: Size {
width: length(4.0_f32),
height: length(0.0_f32),
},
flex_shrink: 0.0,
..Default::default()
})
.children(children);
if let Some(msg) = seg.on_click {
node = node.hover_fill(palette.bg_hover).on_click(msg);
}
node
}