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:
@@ -0,0 +1,12 @@
|
||||
[package]
|
||||
name = "llimphi-widget-button"
|
||||
version.workspace = true
|
||||
edition.workspace = true
|
||||
license.workspace = true
|
||||
authors.workspace = true
|
||||
publish.workspace = true
|
||||
description = "llimphi-widget-button — botón clicable con estado hover. Reusable entre apps Llimphi; cambia el bg cuando el cursor pasa por encima. Compuesto de `View::fill().hover_fill().on_click()` con una paleta tematizable."
|
||||
|
||||
[dependencies]
|
||||
llimphi-ui = { workspace = true }
|
||||
llimphi-theme = { workspace = true }
|
||||
@@ -0,0 +1,5 @@
|
||||
# llimphi-widget-button
|
||||
|
||||
> Botón con variantes para [llimphi](../../README.md).
|
||||
|
||||
Variantes: `primary`, `secondary`, `ghost`, `danger`. Estado hover/active/disabled. Soporta icono + label, o sólo icono.
|
||||
@@ -0,0 +1,5 @@
|
||||
# llimphi-widget-button
|
||||
|
||||
> Button with variants for [llimphi](../../README.md).
|
||||
|
||||
Variants: `primary`, `secondary`, `ghost`, `danger`. Hover/active/disabled states. Supports icon + label, or icon-only.
|
||||
@@ -0,0 +1,136 @@
|
||||
//! Showcase de `llimphi-widget-button`: tres botones con hover.
|
||||
//!
|
||||
//! Corré con: `cargo run -p llimphi-widget-button --example showcase --release`.
|
||||
|
||||
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::{App, Handle, View};
|
||||
use llimphi_widget_button::{button_styled, button_view, ButtonPalette};
|
||||
|
||||
#[derive(Clone, Debug)]
|
||||
enum Msg {
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
}
|
||||
|
||||
struct Model {
|
||||
last: Option<Msg>,
|
||||
counter: u32,
|
||||
}
|
||||
|
||||
struct Showcase;
|
||||
|
||||
impl App for Showcase {
|
||||
type Model = Model;
|
||||
type Msg = Msg;
|
||||
|
||||
fn title() -> &'static str {
|
||||
"llimphi · button showcase"
|
||||
}
|
||||
|
||||
fn init(_: &Handle<Msg>) -> Model {
|
||||
Model {
|
||||
last: None,
|
||||
counter: 0,
|
||||
}
|
||||
}
|
||||
|
||||
fn update(model: Model, msg: Msg, _: &Handle<Msg>) -> Model {
|
||||
let mut m = model;
|
||||
m.counter += 1;
|
||||
m.last = Some(msg);
|
||||
m
|
||||
}
|
||||
|
||||
fn view(model: &Model) -> View<Msg> {
|
||||
let palette = ButtonPalette::default();
|
||||
let warning = ButtonPalette {
|
||||
bg: Color::from_rgba8(140, 70, 30, 255),
|
||||
bg_hover: Color::from_rgba8(200, 100, 40, 255),
|
||||
..palette
|
||||
};
|
||||
let danger = ButtonPalette {
|
||||
bg: Color::from_rgba8(150, 40, 40, 255),
|
||||
bg_hover: Color::from_rgba8(220, 70, 70, 255),
|
||||
..palette
|
||||
};
|
||||
|
||||
let a = button_view("acción A", &palette, Msg::A);
|
||||
let b = button_view("acción B (warning)", &warning, Msg::B);
|
||||
let c = button_styled(
|
||||
"borrar (left-aligned, fixed width)",
|
||||
Style {
|
||||
size: Size {
|
||||
width: length(320.0_f32),
|
||||
height: length(34.0_f32),
|
||||
},
|
||||
padding: Rect {
|
||||
left: length(12.0_f32),
|
||||
right: length(12.0_f32),
|
||||
top: length(0.0_f32),
|
||||
bottom: length(0.0_f32),
|
||||
},
|
||||
align_items: Some(AlignItems::Center),
|
||||
..Default::default()
|
||||
},
|
||||
Alignment::Start,
|
||||
&danger,
|
||||
Msg::C,
|
||||
);
|
||||
|
||||
let status = View::new(Style {
|
||||
size: Size {
|
||||
width: percent(1.0_f32),
|
||||
height: length(40.0_f32),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.text_aligned(
|
||||
format!(
|
||||
"clicks: {} · último: {}",
|
||||
model.counter,
|
||||
match model.last {
|
||||
Some(Msg::A) => "A",
|
||||
Some(Msg::B) => "B",
|
||||
Some(Msg::C) => "C",
|
||||
None => "—",
|
||||
}
|
||||
),
|
||||
14.0,
|
||||
Color::from_rgba8(180, 190, 205, 255),
|
||||
Alignment::Start,
|
||||
);
|
||||
|
||||
View::new(Style {
|
||||
flex_direction: FlexDirection::Column,
|
||||
size: Size {
|
||||
width: percent(1.0_f32),
|
||||
height: percent(1.0_f32),
|
||||
},
|
||||
gap: Size {
|
||||
width: length(0.0_f32),
|
||||
height: length(14.0_f32),
|
||||
},
|
||||
padding: Rect {
|
||||
left: length(32.0_f32),
|
||||
right: length(32.0_f32),
|
||||
top: length(32.0_f32),
|
||||
bottom: length(32.0_f32),
|
||||
},
|
||||
align_items: Some(AlignItems::Start),
|
||||
justify_content: Some(JustifyContent::Start),
|
||||
..Default::default()
|
||||
})
|
||||
.fill(Color::from_rgba8(20, 24, 32, 255))
|
||||
.children(vec![a, b, c, status])
|
||||
}
|
||||
}
|
||||
|
||||
fn main() {
|
||||
llimphi_ui::run::<Showcase>();
|
||||
}
|
||||
@@ -0,0 +1,124 @@
|
||||
//! `llimphi-widget-button` — botón clicable con estado hover.
|
||||
//!
|
||||
//! Reusable entre apps Llimphi: `button_view(label, palette, on_click)`
|
||||
//! devuelve una vista que cambia de color cuando el cursor pasa por
|
||||
//! encima y emite `on_click` al ser apretada. El caller controla las
|
||||
//! dimensiones envolviendo el `View` retornado en un contenedor flex
|
||||
//! con el tamaño que necesite (botón ancho completo, chip 80×30, etc).
|
||||
//!
|
||||
//! No expone estado interno — todo el estado vive en el `Model` del App
|
||||
//! (el hover lo trackea llimphi-ui automáticamente vía `hover_fill`).
|
||||
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use llimphi_ui::llimphi_layout::taffy::{
|
||||
prelude::{length, percent, Size, Style},
|
||||
AlignItems, JustifyContent, Rect,
|
||||
};
|
||||
use llimphi_ui::llimphi_raster::peniko::Color;
|
||||
use llimphi_ui::llimphi_text::Alignment;
|
||||
use llimphi_ui::View;
|
||||
|
||||
/// Paleta del botón. Por default un chip dark con highlight tenue al
|
||||
/// hover — similar al patrón `bg_panel_alt` + `bg_row_hover` de
|
||||
/// `nahual-theme`.
|
||||
#[derive(Debug, Clone, Copy)]
|
||||
pub struct ButtonPalette {
|
||||
pub bg: Color,
|
||||
pub bg_hover: Color,
|
||||
pub fg: Color,
|
||||
pub radius: f64,
|
||||
}
|
||||
|
||||
impl Default for ButtonPalette {
|
||||
fn default() -> Self {
|
||||
Self::from_theme(&llimphi_theme::Theme::dark())
|
||||
}
|
||||
}
|
||||
|
||||
impl ButtonPalette {
|
||||
/// Construye la paleta desde un `Theme` semántico.
|
||||
pub fn from_theme(t: &llimphi_theme::Theme) -> Self {
|
||||
Self {
|
||||
bg: t.bg_button,
|
||||
bg_hover: t.bg_button_hover,
|
||||
fg: t.fg_text,
|
||||
radius: 5.0,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Compone un botón rectangular: bg + texto + on_click + hover. Por
|
||||
/// default ocupa ancho 100% del padre y alto 30 px; sobre-escribir
|
||||
/// pasando un `Style` propio vía [`button_styled`].
|
||||
pub fn button_view<Msg: Clone + 'static>(
|
||||
label: impl Into<String>,
|
||||
palette: &ButtonPalette,
|
||||
on_click: Msg,
|
||||
) -> View<Msg> {
|
||||
button_styled(
|
||||
label,
|
||||
Style {
|
||||
size: Size {
|
||||
width: percent(1.0_f32),
|
||||
height: length(30.0_f32),
|
||||
},
|
||||
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),
|
||||
justify_content: Some(JustifyContent::Center),
|
||||
..Default::default()
|
||||
},
|
||||
Alignment::Center,
|
||||
palette,
|
||||
on_click,
|
||||
)
|
||||
}
|
||||
|
||||
/// Variante con `Style` y alineación de texto explícitos — útil cuando
|
||||
/// la app necesita un botón con dimensiones particulares o el texto a
|
||||
/// la izquierda.
|
||||
pub fn button_styled<Msg: Clone + 'static>(
|
||||
label: impl Into<String>,
|
||||
style: Style,
|
||||
text_alignment: Alignment,
|
||||
palette: &ButtonPalette,
|
||||
on_click: Msg,
|
||||
) -> View<Msg> {
|
||||
// Gloss superior: gradient blanco alpha 28 → 0 sobre la mitad de
|
||||
// arriba. `paint_with` corre entre el fill (que respeta hover_fill)
|
||||
// y el texto, así que la luz se suma al color de base sin sustituirlo
|
||||
// — el hover sigue funcionando idéntico. El RoundedRect cubre el
|
||||
// botón completo y `Extend::Pad` (default de peniko) deja la mitad
|
||||
// inferior en alpha 0. Match: chrome/splash — superficie con luz
|
||||
// descendente desde el edge superior.
|
||||
let radius = palette.radius;
|
||||
View::new(style)
|
||||
.fill(palette.bg)
|
||||
.hover_fill(palette.bg_hover)
|
||||
.radius(radius)
|
||||
.paint_with(move |scene, _ts, rect| {
|
||||
use llimphi_ui::llimphi_raster::kurbo::{Affine, Point, RoundedRect};
|
||||
use llimphi_ui::llimphi_raster::peniko::{Fill, Gradient};
|
||||
if rect.w <= 0.0 || rect.h <= 0.0 {
|
||||
return;
|
||||
}
|
||||
let x0 = rect.x as f64;
|
||||
let y0 = rect.y as f64;
|
||||
let x1 = (rect.x + rect.w) as f64;
|
||||
let y1 = (rect.y + rect.h) as f64;
|
||||
let y_mid = y0 + (y1 - y0) * 0.5;
|
||||
let rr = RoundedRect::new(x0, y0, x1, y1, radius);
|
||||
let top = Color::from_rgba8(255, 255, 255, 28);
|
||||
let bot = Color::from_rgba8(255, 255, 255, 0);
|
||||
let gradient = Gradient::new_linear(Point::new(x0, y0), Point::new(x0, y_mid))
|
||||
.with_stops([top, bot].as_slice());
|
||||
scene.fill(Fill::NonZero, Affine::IDENTITY, &gradient, None, &rr);
|
||||
})
|
||||
.text_aligned(label.into(), 13.0, palette.fg, text_alignment)
|
||||
.on_click(on_click)
|
||||
}
|
||||
Reference in New Issue
Block a user