Skip to main content

llimphi_widget_fab/
lib.rs

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}