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
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "llimphi-widget-avatar"
version.workspace = true
edition.workspace = true
license.workspace = true
authors.workspace = true
publish.workspace = true
description = "llimphi-widget-avatar — círculo de identidad con inicial sobre color generado del hash del nombre. Determinista (mismo nombre → mismo color) y tonal (paleta limitada para que no choque)."
[dependencies]
llimphi-ui = { workspace = true }
llimphi-theme = { workspace = true }
+116
View File
@@ -0,0 +1,116 @@
//! `llimphi-widget-avatar` — círculo de identidad con inicial.
//!
//! Genera un avatar **determinista** de un nombre: el color de fondo
//! viene de un hash del nombre, mapeado a una paleta limitada de 8
//! tonos (para que dos usuarios distintos no acaben con colores que
//! se confundan). La inicial es la primera letra del nombre (uppercase),
//! pintada centrada en blanco-cálido.
//!
//! Útil para chats (ayni), authorship en pluma, presencia en
//! herramientas colaborativas. Una sola función — sin state, sin
//! animación, sin paleta configurable (la consistencia importa más
//! que la personalización).
#![forbid(unsafe_code)]
use llimphi_ui::llimphi_layout::taffy::{
prelude::{length, Size, Style},
AlignItems, JustifyContent,
};
use llimphi_ui::llimphi_raster::peniko::Color;
use llimphi_ui::llimphi_text::Alignment;
use llimphi_ui::View;
/// Construye el avatar de `name` con diámetro `size_px`.
pub fn avatar_view<Msg: Clone + 'static>(name: &str, size_px: f32) -> View<Msg> {
let bg = color_for(name);
let initial = name
.chars()
.next()
.map(|c| c.to_uppercase().next().unwrap_or(c))
.unwrap_or('·');
let fg = Color::from_rgba8(248, 248, 250, 255);
let font = (size_px * 0.42).max(8.0);
View::new(Style {
size: Size {
width: length(size_px),
height: length(size_px),
},
align_items: Some(AlignItems::Center),
justify_content: Some(JustifyContent::Center),
flex_shrink: 0.0,
..Default::default()
})
.fill(bg)
.radius((size_px * 0.5) as f64)
.paint_with(move |scene, _ts, rect| {
// Highlight radial en el cuadrante superior — el avatar se lee
// como esfera. paint_with corre entre el fill y la inicial, así
// que la luz se suma al color del nombre sin tapar el texto.
// Mismo patrón dot-badge / switch-thumb (P6/P7).
use llimphi_ui::llimphi_raster::kurbo::{Affine, Circle};
use llimphi_ui::llimphi_raster::peniko::Fill;
if rect.w <= 0.0 || rect.h <= 0.0 {
return;
}
let cx = (rect.x + rect.w * 0.5) as f64;
let cy = (rect.y + rect.h * 0.30) as f64;
let r = (rect.w as f64 * 0.18).max(1.0);
let highlight = Color::from_rgba8(255, 255, 255, 60);
scene.fill(
Fill::NonZero,
Affine::IDENTITY,
highlight,
None,
&Circle::new((cx, cy), r),
);
})
.text_aligned(initial.to_string(), font, fg, Alignment::Center)
}
/// Paleta tonal limitada — 8 colores HSL-ish elegidos para destacar
/// sobre fondos oscuros sin ser estridentes.
const PALETTE: &[Color] = &[
Color::from_rgba8(96, 130, 220, 255), // azul
Color::from_rgba8(110, 180, 130, 255), // verde aurora
Color::from_rgba8(220, 140, 80, 255), // naranja sunset
Color::from_rgba8(160, 110, 220, 255), // púrpura
Color::from_rgba8(80, 180, 180, 255), // aqua
Color::from_rgba8(220, 120, 160, 255), // rosa
Color::from_rgba8(180, 170, 90, 255), // mostaza
Color::from_rgba8(130, 150, 175, 255), // gris-azul
];
/// Hash FNV-1a simple sobre los bytes del nombre, mod paleta. No
/// requiere crypto — sólo necesitamos que mismo input dé mismo color.
fn color_for(name: &str) -> Color {
let mut h: u32 = 0x811c9dc5;
for b in name.bytes() {
h ^= b as u32;
h = h.wrapping_mul(0x01000193);
}
PALETTE[(h as usize) % PALETTE.len()]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn color_for_is_deterministic() {
assert_eq!(color_for("sergio").components, color_for("sergio").components);
assert_eq!(color_for("calcetin").components, color_for("calcetin").components);
}
#[test]
fn different_names_can_have_different_colors() {
let names = ["a", "b", "c", "d", "e", "f", "g", "h"];
let colors: Vec<_> = names.iter().map(|n| color_for(n)).collect();
// Al menos 2 colores distintos en 8 nombres — el hash es trivial,
// colisiones esperadas, no garantizamos 8 distintos.
let unique: std::collections::HashSet<_> =
colors.iter().map(|c| c.components.map(|x| (x * 255.0) as u8)).collect();
assert!(unique.len() >= 2);
}
}