tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
1//! `llimphi-widget-fab` — Floating Action Button.
2//!
3//! Botón circular elevado pensado para la **acción primaria** de una
4//! pantalla (componer, nuevo, +, capturar). Heredamos el patrón
5//! Material/Flutter: rest sobre sombra E3, círculo del color `accent`,
6//! glyph blanco centrado, y **feedback de hover** (el fill se aviva al
7//! pasar el cursor, vía `hover_fill` del compositor — sin estado en el host).
8//!
9//! La **entrada** usa `animated_pop_in` para el "pop" canónico de Material
10//! (scale 0.6 → 1.0 + fade-in) en vez de aparecer de golpe.
11
12#![forbid(unsafe_code)]
13
14
15// Sin `std`: este widget no usa nada del sistema —sólo `alloc` para
16// `String`/`Vec`—, y declararlo es lo que le permite cruzar a una jaula
17// WASM o al kernel. Antes enlazaba `std` por omisión (W4.6).
18#![no_std]
19
20extern crate alloc;
21
22use alloc::string::String;
23use alloc::vec::Vec;
24use llimphi_compositor::Shadow;
25use llimphi_compositor::llimphi_layout::taffy::{
26 prelude::{length, Size, Style},
27 AlignItems, JustifyContent,
28};
29use llimphi_compositor::peniko::Color;
30use llimphi_compositor::llimphi_text::Alignment;
31use llimphi_compositor::View;
32use llimphi_theme::{elevation, motion, Theme};
33
34/// Tamaño del FAB. El estándar Material es 56 px; el "mini" 40 px;
35/// el "extended" (texto + glyph, píldora ancha) es [`fab_extended`] /
36/// [`fab_extended_icon`], no un `FabSize`.
37#[derive(Debug, Clone, Copy)]
38pub enum FabSize {
39 Regular,
40 Mini,
41}
42
43/// Aviva un color hacia el blanco en `t` (0..1) — feedback de hover.
44fn brighten(c: Color, t: f32) -> Color {
45 let [r, g, b, a] = c.components;
46 Color {
47 components: [
48 r + (1.0 - r) * t,
49 g + (1.0 - g) * t,
50 b + (1.0 - b) * t,
51 a,
52 ],
53 ..c
54 }
55}
56
57impl FabSize {
58 pub fn px(self) -> f32 {
59 match self {
60 FabSize::Regular => 56.0,
61 FabSize::Mini => 40.0,
62 }
63 }
64}
65
66/// Paleta del FAB.
67#[derive(Debug, Clone, Copy)]
68pub struct FabPalette {
69 /// Fill del círculo (idle).
70 pub bg: Color,
71 /// Color del glyph.
72 pub fg: Color,
73}
74
75impl FabPalette {
76 pub fn from_theme(t: &Theme) -> Self {
77 Self {
78 bg: t.accent,
79 // Texto sobre accent: blanco (los accents del repo son todos
80 // suficientemente saturados para hacer contraste).
81 fg: Color::from_rgba8(255, 255, 255, 255),
82 }
83 }
84}
85
86/// Compone el FAB. `key` debe ser estable para que la anim de hover
87/// quede vinculada al mismo nodo entre frames.
88pub fn fab_view<Msg: Clone + 'static>(
89 glyph: impl Into<String>,
90 size: FabSize,
91 key: u64,
92 palette: &FabPalette,
93 on_click: Msg,
94) -> View<Msg> {
95 let s = size.px();
96 let (a, blur, dy) = elevation::E3;
97 let shadow = Shadow {
98 color: Color::from_rgba8(0, 0, 0, a),
99 blur,
100 dx: 0.0,
101 dy,
102 spread: 0.0,
103 };
104 let glyph: String = glyph.into();
105 let aria = glyph.clone();
106 View::new(Style {
107 size: Size { width: length(s), height: length(s) },
108 align_items: Some(AlignItems::CENTER),
109 justify_content: Some(JustifyContent::CENTER),
110 ..Default::default()
111 })
112 .fill(palette.bg)
113 .hover_fill(brighten(palette.bg, 0.12))
114 .radius((s as f64) * 0.5)
115 .shadow(shadow)
116 .animated_pop_in(key, motion::FAST)
117 .text_aligned(
118 glyph,
119 (s * 0.42).round(),
120 palette.fg,
121 Alignment::Center,
122 )
123 // El glyph (+, ✎, etc.) no siempre es un buen nombre para el lector — el
124 // caller suele querer overridear con `.aria_label("Crear nota")`. Lo dejamos
125 // como fallback igual: mejor decir "más" que nada.
126 .role(llimphi_compositor::Role::Button)
127 .aria_label(aria)
128 .on_click(on_click)
129 .cursor(llimphi_compositor::Cursor::Pointer)
130}
131
132/// Extended FAB (Material): píldora ancha con **texto**. Para texto + glyph
133/// líder, usar [`fab_extended_icon`].
134pub fn fab_extended<Msg: Clone + 'static>(
135 label: impl Into<String>,
136 key: u64,
137 palette: &FabPalette,
138 on_click: Msg,
139) -> View<Msg> {
140 fab_extended_icon(None::<String>, label, key, palette, on_click)
141}
142
143/// Extended FAB con **glyph líder + texto** (el "texto + ícono" que promete
144/// el patrón Material). `glyph` `None` = sólo texto (equivale a
145/// [`fab_extended`]).
146pub fn fab_extended_icon<Msg: Clone + 'static>(
147 glyph: Option<impl Into<String>>,
148 label: impl Into<String>,
149 key: u64,
150 palette: &FabPalette,
151 on_click: Msg,
152) -> View<Msg> {
153 let label: String = label.into();
154 let aria = label.clone();
155 let h = 48.0_f32;
156 let (a, blur, dy) = elevation::E3;
157 let shadow = Shadow {
158 color: Color::from_rgba8(0, 0, 0, a),
159 blur,
160 dx: 0.0,
161 dy,
162 spread: 0.0,
163 };
164
165 let mut children: Vec<View<Msg>> = Vec::with_capacity(2);
166 if let Some(g) = glyph {
167 children.push(
168 View::new(Style {
169 size: Size { width: length(20.0_f32), height: length(h) },
170 align_items: Some(AlignItems::CENTER),
171 justify_content: Some(JustifyContent::CENTER),
172 margin: llimphi_compositor::llimphi_layout::taffy::Rect {
173 left: length(0.0_f32),
174 right: length(8.0_f32),
175 top: length(0.0_f32),
176 bottom: length(0.0_f32),
177 },
178 ..Default::default()
179 })
180 .text_aligned(g.into(), 18.0, palette.fg, Alignment::Center),
181 );
182 }
183 children.push(
184 View::new(Style {
185 size: Size {
186 width: llimphi_compositor::llimphi_layout::taffy::prelude::auto(),
187 height: length(h),
188 },
189 align_items: Some(AlignItems::CENTER),
190 justify_content: Some(JustifyContent::CENTER),
191 ..Default::default()
192 })
193 .text_aligned(label, 14.0, palette.fg, Alignment::Center),
194 );
195
196 View::new(Style {
197 size: Size {
198 width: llimphi_compositor::llimphi_layout::taffy::prelude::auto(),
199 height: length(h),
200 },
201 flex_direction: llimphi_compositor::llimphi_layout::taffy::prelude::FlexDirection::Row,
202 align_items: Some(AlignItems::CENTER),
203 justify_content: Some(JustifyContent::CENTER),
204 padding: llimphi_compositor::llimphi_layout::taffy::Rect {
205 left: length(20.0_f32),
206 right: length(20.0_f32),
207 top: length(0.0_f32),
208 bottom: length(0.0_f32),
209 },
210 ..Default::default()
211 })
212 .fill(palette.bg)
213 .hover_fill(brighten(palette.bg, 0.12))
214 .radius((h as f64) * 0.5)
215 .shadow(shadow)
216 .animated_pop_in(key, motion::FAST)
217 .role(llimphi_compositor::Role::Button)
218 .aria_label(aria)
219 .on_click(on_click)
220 .cursor(llimphi_compositor::Cursor::Pointer)
221 .children(children)
222}