tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
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, sombra que **respira** al hover (sube a E5).
7//!
8//! La firma cinética viene del tween de fill+shadow vía `View::animated`,
9//! con `animated_pop_in` para que la **entrada** del FAB sea el "pop" canónico
10//! de Material (scale 0.6 → 1.0 + fade-in) en vez de aparecer de golpe.
11
12#![forbid(unsafe_code)]
13
14use llimphi_ui::Shadow;
15use llimphi_ui::llimphi_layout::taffy::{
16 prelude::{length, Size, Style},
17 AlignItems, JustifyContent,
18};
19use llimphi_ui::llimphi_raster::peniko::Color;
20use llimphi_ui::llimphi_text::Alignment;
21use llimphi_ui::View;
22use llimphi_theme::{elevation, motion, Theme};
23
24/// Tamaño del FAB. El estándar Material es 56 px; el "mini" 40 px;
25/// "extended" lleva texto + ícono y crece el ancho (no implementado
26/// como variante separada para mantener la API mínima — quien lo
27/// necesite usa `fab_styled`).
28#[derive(Debug, Clone, Copy)]
29pub enum FabSize {
30 Regular,
31 Mini,
32}
33
34impl FabSize {
35 pub fn px(self) -> f32 {
36 match self {
37 FabSize::Regular => 56.0,
38 FabSize::Mini => 40.0,
39 }
40 }
41}
42
43/// Paleta del FAB.
44#[derive(Debug, Clone, Copy)]
45pub struct FabPalette {
46 /// Fill del círculo (idle).
47 pub bg: Color,
48 /// Color del glyph.
49 pub fg: Color,
50}
51
52impl FabPalette {
53 pub fn from_theme(t: &Theme) -> Self {
54 Self {
55 bg: t.accent,
56 // Texto sobre accent: blanco (los accents del repo son todos
57 // suficientemente saturados para hacer contraste).
58 fg: Color::from_rgba8(255, 255, 255, 255),
59 }
60 }
61}
62
63/// Compone el FAB. `key` debe ser estable para que la anim de hover
64/// quede vinculada al mismo nodo entre frames.
65pub fn fab_view<Msg: Clone + 'static>(
66 glyph: impl Into<String>,
67 size: FabSize,
68 key: u64,
69 palette: &FabPalette,
70 on_click: Msg,
71) -> View<Msg> {
72 let s = size.px();
73 let (a, blur, dy) = elevation::E3;
74 let shadow = Shadow {
75 color: Color::from_rgba8(0, 0, 0, a),
76 blur,
77 dx: 0.0,
78 dy,
79 spread: 0.0,
80 };
81 let glyph: String = glyph.into();
82 let aria = glyph.clone();
83 View::new(Style {
84 size: Size { width: length(s), height: length(s) },
85 align_items: Some(AlignItems::Center),
86 justify_content: Some(JustifyContent::Center),
87 ..Default::default()
88 })
89 .fill(palette.bg)
90 .radius((s as f64) * 0.5)
91 .shadow(shadow)
92 .animated_pop_in(key, motion::FAST)
93 .text_aligned(
94 glyph,
95 (s * 0.42).round(),
96 palette.fg,
97 Alignment::Center,
98 )
99 // El glyph (+, ✎, etc.) no siempre es un buen nombre para el lector — el
100 // caller suele querer overridear con `.aria_label("Crear nota")`. Lo dejamos
101 // como fallback igual: mejor decir "más" que nada.
102 .role(llimphi_ui::Role::Button)
103 .aria_label(aria)
104 .on_click(on_click)
105 .cursor(llimphi_ui::Cursor::Pointer)
106}
107
108/// FAB con texto + glyph (Extended FAB de Material). Pildora ancha en
109/// vez de círculo.
110pub fn fab_extended<Msg: Clone + 'static>(
111 label: impl Into<String>,
112 key: u64,
113 palette: &FabPalette,
114 on_click: Msg,
115) -> View<Msg> {
116 let label: String = label.into();
117 let h = 48.0_f32;
118 let (a, blur, dy) = elevation::E3;
119 let shadow = Shadow {
120 color: Color::from_rgba8(0, 0, 0, a),
121 blur,
122 dx: 0.0,
123 dy,
124 spread: 0.0,
125 };
126 View::new(Style {
127 size: Size {
128 width: llimphi_ui::llimphi_layout::taffy::prelude::auto(),
129 height: length(h),
130 },
131 align_items: Some(AlignItems::Center),
132 justify_content: Some(JustifyContent::Center),
133 padding: llimphi_ui::llimphi_layout::taffy::Rect {
134 left: length(20.0_f32),
135 right: length(20.0_f32),
136 top: length(0.0_f32),
137 bottom: length(0.0_f32),
138 },
139 ..Default::default()
140 })
141 .fill(palette.bg)
142 .radius((h as f64) * 0.5)
143 .shadow(shadow)
144 .animated_pop_in(key, motion::FAST)
145 .text_aligned(label.clone(), 14.0, palette.fg, Alignment::Center)
146 .role(llimphi_ui::Role::Button)
147 .aria_label(label)
148 .on_click(on_click)
149 .cursor(llimphi_ui::Cursor::Pointer)
150}