//! `llimphi-widget-fab` — Floating Action Button. //! //! Botón circular elevado pensado para la **acción primaria** de una //! pantalla (componer, nuevo, +, capturar). Heredamos el patrón //! Material/Flutter: rest sobre sombra E3, círculo del color `accent`, //! glyph blanco centrado, y **feedback de hover** (el fill se aviva al //! pasar el cursor, vía `hover_fill` del compositor — sin estado en el host). //! //! La **entrada** usa `animated_pop_in` para el "pop" canónico de Material //! (scale 0.6 → 1.0 + fade-in) en vez de aparecer de golpe. #![forbid(unsafe_code)] // Sin `std`: este widget no usa nada del sistema —sólo `alloc` para // `String`/`Vec`—, y declararlo es lo que le permite cruzar a una jaula // WASM o al kernel. Antes enlazaba `std` por omisión (W4.6). #![no_std] extern crate alloc; use alloc::string::String; use alloc::vec::Vec; use llimphi_compositor::Shadow; use llimphi_compositor::llimphi_layout::taffy::{ prelude::{length, Size, Style}, AlignItems, JustifyContent, }; use llimphi_compositor::peniko::Color; use llimphi_compositor::llimphi_text::Alignment; use llimphi_compositor::View; use llimphi_theme::{elevation, motion, Theme}; /// Tamaño del FAB. El estándar Material es 56 px; el "mini" 40 px; /// el "extended" (texto + glyph, píldora ancha) es [`fab_extended`] / /// [`fab_extended_icon`], no un `FabSize`. #[derive(Debug, Clone, Copy)] pub enum FabSize { Regular, Mini, } /// Aviva un color hacia el blanco en `t` (0..1) — feedback de hover. fn brighten(c: Color, t: f32) -> Color { let [r, g, b, a] = c.components; Color { components: [ r + (1.0 - r) * t, g + (1.0 - g) * t, b + (1.0 - b) * t, a, ], ..c } } impl FabSize { pub fn px(self) -> f32 { match self { FabSize::Regular => 56.0, FabSize::Mini => 40.0, } } } /// Paleta del FAB. #[derive(Debug, Clone, Copy)] pub struct FabPalette { /// Fill del círculo (idle). pub bg: Color, /// Color del glyph. pub fg: Color, } impl FabPalette { pub fn from_theme(t: &Theme) -> Self { Self { bg: t.accent, // Texto sobre accent: blanco (los accents del repo son todos // suficientemente saturados para hacer contraste). fg: Color::from_rgba8(255, 255, 255, 255), } } } /// Compone el FAB. `key` debe ser estable para que la anim de hover /// quede vinculada al mismo nodo entre frames. pub fn fab_view( glyph: impl Into, size: FabSize, key: u64, palette: &FabPalette, on_click: Msg, ) -> View { let s = size.px(); let (a, blur, dy) = elevation::E3; let shadow = Shadow { color: Color::from_rgba8(0, 0, 0, a), blur, dx: 0.0, dy, spread: 0.0, }; let glyph: String = glyph.into(); let aria = glyph.clone(); View::new(Style { size: Size { width: length(s), height: length(s) }, align_items: Some(AlignItems::CENTER), justify_content: Some(JustifyContent::CENTER), ..Default::default() }) .fill(palette.bg) .hover_fill(brighten(palette.bg, 0.12)) .radius((s as f64) * 0.5) .shadow(shadow) .animated_pop_in(key, motion::FAST) .text_aligned( glyph, (s * 0.42).round(), palette.fg, Alignment::Center, ) // El glyph (+, ✎, etc.) no siempre es un buen nombre para el lector — el // caller suele querer overridear con `.aria_label("Crear nota")`. Lo dejamos // como fallback igual: mejor decir "más" que nada. .role(llimphi_compositor::Role::Button) .aria_label(aria) .on_click(on_click) .cursor(llimphi_compositor::Cursor::Pointer) } /// Extended FAB (Material): píldora ancha con **texto**. Para texto + glyph /// líder, usar [`fab_extended_icon`]. pub fn fab_extended( label: impl Into, key: u64, palette: &FabPalette, on_click: Msg, ) -> View { fab_extended_icon(None::, label, key, palette, on_click) } /// Extended FAB con **glyph líder + texto** (el "texto + ícono" que promete /// el patrón Material). `glyph` `None` = sólo texto (equivale a /// [`fab_extended`]). pub fn fab_extended_icon( glyph: Option>, label: impl Into, key: u64, palette: &FabPalette, on_click: Msg, ) -> View { let label: String = label.into(); let aria = label.clone(); let h = 48.0_f32; let (a, blur, dy) = elevation::E3; let shadow = Shadow { color: Color::from_rgba8(0, 0, 0, a), blur, dx: 0.0, dy, spread: 0.0, }; let mut children: Vec> = Vec::with_capacity(2); if let Some(g) = glyph { children.push( View::new(Style { size: Size { width: length(20.0_f32), height: length(h) }, align_items: Some(AlignItems::CENTER), justify_content: Some(JustifyContent::CENTER), margin: llimphi_compositor::llimphi_layout::taffy::Rect { left: length(0.0_f32), right: length(8.0_f32), top: length(0.0_f32), bottom: length(0.0_f32), }, ..Default::default() }) .text_aligned(g.into(), 18.0, palette.fg, Alignment::Center), ); } children.push( View::new(Style { size: Size { width: llimphi_compositor::llimphi_layout::taffy::prelude::auto(), height: length(h), }, align_items: Some(AlignItems::CENTER), justify_content: Some(JustifyContent::CENTER), ..Default::default() }) .text_aligned(label, 14.0, palette.fg, Alignment::Center), ); View::new(Style { size: Size { width: llimphi_compositor::llimphi_layout::taffy::prelude::auto(), height: length(h), }, flex_direction: llimphi_compositor::llimphi_layout::taffy::prelude::FlexDirection::Row, align_items: Some(AlignItems::CENTER), justify_content: Some(JustifyContent::CENTER), padding: llimphi_compositor::llimphi_layout::taffy::Rect { left: length(20.0_f32), right: length(20.0_f32), top: length(0.0_f32), bottom: length(0.0_f32), }, ..Default::default() }) .fill(palette.bg) .hover_fill(brighten(palette.bg, 0.12)) .radius((h as f64) * 0.5) .shadow(shadow) .animated_pop_in(key, motion::FAST) .role(llimphi_compositor::Role::Button) .aria_label(aria) .on_click(on_click) .cursor(llimphi_compositor::Cursor::Pointer) .children(children) }