tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
feat: llimphi standalone — framework UI soberano extraído del monorepo
1//! `llimphi-widget-button` — botón clicable con estado hover.
2//!
3//! Reusable entre apps Llimphi: `button_view(label, palette, on_click)`
4//! devuelve una vista que cambia de color cuando el cursor pasa por
5//! encima y emite `on_click` al ser apretada. El caller controla las
6//! dimensiones envolviendo el `View` retornado en un contenedor flex
7//! con el tamaño que necesite (botón ancho completo, chip 80×30, etc).
8//!
9//! No expone estado interno — todo el estado vive en el `Model` del App
10//! (el hover lo trackea llimphi-ui automáticamente vía `hover_fill`).
11
12#![forbid(unsafe_code)]
13
14use llimphi_ui::llimphi_layout::taffy::{
15 prelude::{length, percent, Size, Style},
16 AlignItems, JustifyContent, Rect,
17};
18use llimphi_ui::llimphi_raster::peniko::Color;
19use llimphi_ui::llimphi_text::Alignment;
20use llimphi_ui::View;
21
22/// Paleta del botón. Por default un chip dark con highlight tenue al
23/// hover — similar al patrón `bg_panel_alt` + `bg_row_hover` de
24/// `nahual-theme`.
25#[derive(Debug, Clone, Copy)]
26pub struct ButtonPalette {
27 pub bg: Color,
28 pub bg_hover: Color,
29 pub fg: Color,
30 pub radius: f64,
31}
32
33impl Default for ButtonPalette {
34 fn default() -> Self {
35 Self::from_theme(&llimphi_theme::Theme::dark())
36 }
37}
38
39impl ButtonPalette {
40 /// Construye la paleta desde un `Theme` semántico.
41 pub fn from_theme(t: &llimphi_theme::Theme) -> Self {
42 Self {
43 bg: t.bg_button,
44 bg_hover: t.bg_button_hover,
45 fg: t.fg_text,
46 radius: 5.0,
47 }
48 }
49}
50
51/// Compone un botón rectangular: bg + texto + on_click + hover. Por
52/// default ocupa ancho 100% del padre y alto 30 px; sobre-escribir
53/// pasando un `Style` propio vía [`button_styled`].
54pub fn button_view<Msg: Clone + 'static>(
55 label: impl Into<String>,
56 palette: &ButtonPalette,
57 on_click: Msg,
58) -> View<Msg> {
59 button_styled(
60 label,
61 Style {
62 size: Size {
63 width: percent(1.0_f32),
64 height: length(30.0_f32),
65 },
66 padding: Rect {
67 left: length(10.0_f32),
68 right: length(10.0_f32),
69 top: length(0.0_f32),
70 bottom: length(0.0_f32),
71 },
72 align_items: Some(AlignItems::Center),
73 justify_content: Some(JustifyContent::Center),
74 ..Default::default()
75 },
76 Alignment::Center,
77 palette,
78 on_click,
79 )
80}
81
82/// Variante con `Style` y alineación de texto explícitos — útil cuando
83/// la app necesita un botón con dimensiones particulares o el texto a
84/// la izquierda.
85pub fn button_styled<Msg: Clone + 'static>(
86 label: impl Into<String>,
87 style: Style,
88 text_alignment: Alignment,
89 palette: &ButtonPalette,
90 on_click: Msg,
91) -> View<Msg> {
92 // Gloss superior: gradient blanco alpha 28 → 0 sobre la mitad de
93 // arriba. `paint_with` corre entre el fill (que respeta hover_fill)
94 // y el texto, así que la luz se suma al color de base sin sustituirlo
95 // — el hover sigue funcionando idéntico. El RoundedRect cubre el
96 // botón completo y `Extend::Pad` (default de peniko) deja la mitad
97 // inferior en alpha 0. Match: chrome/splash — superficie con luz
98 // descendente desde el edge superior.
99 let radius = palette.radius;
100 View::new(style)
101 .fill(palette.bg)
102 .hover_fill(palette.bg_hover)
103 .radius(radius)
104 .paint_with(move |scene, _ts, rect| {
105 use llimphi_ui::llimphi_raster::kurbo::{Affine, Point, RoundedRect};
106 use llimphi_ui::llimphi_raster::peniko::{Fill, Gradient};
107 if rect.w <= 0.0 || rect.h <= 0.0 {
108 return;
109 }
110 let x0 = rect.x as f64;
111 let y0 = rect.y as f64;
112 let x1 = (rect.x + rect.w) as f64;
113 let y1 = (rect.y + rect.h) as f64;
114 let y_mid = y0 + (y1 - y0) * 0.5;
115 let rr = RoundedRect::new(x0, y0, x1, y1, radius);
116 let top = Color::from_rgba8(255, 255, 255, 28);
117 let bot = Color::from_rgba8(255, 255, 255, 0);
118 let gradient = Gradient::new_linear(Point::new(x0, y0), Point::new(x0, y_mid))
119 .with_stops([top, bot].as_slice());
120 scene.fill(Fill::NonZero, Affine::IDENTITY, &gradient, None, &rr);
121 })
122 .text_aligned(label.into(), 13.0, palette.fg, text_alignment)
123 .on_click(on_click)
124}