tawasuyu

tawasuyu/llimphi

Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.

Rust 614Markdown 274TOML 149RON 310606 Definiciones

⎇ main widgets/chip/src/lib.rs Rust · 247 líneas · 8.0 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

1//! `llimphi-widget-chip` — chip compacto con cuatro sabores: **filter**
2//! (toggle binario), **choice** (radio dentro de un grupo), **input**
3//! (etiqueta removible con × al final) y **assist** (chip-acción con
4//! ícono opcional).
5//!
6//! Forma: rectángulo redondeado bien pegado (radius pill), padding
7//! horizontal 10/4, alto 24 px. Tema-consciente: cuando `selected`,
8//! pinta `accent` apenas tinted (alpha bajo) — sobrio, no chillón.
9//!
10//! Como toda la elegancia de Llimphi, hereda de `llimphi-theme::motion`
11//! para la transición de fill al togglear (animación implícita via
12//! `View::animated(key, motion::MICRO)`).
13
14#![forbid(unsafe_code)]
15
16
17// Sin `std`: este widget no usa nada del sistema —sólo `alloc` para
18// `String`/`Vec`—, y declararlo es lo que le permite cruzar a una jaula
19// WASM o al kernel. Antes enlazaba `std` por omisión (W4.6).
20#![no_std]
21
22extern crate alloc;
23
24use alloc::string::String;
25use alloc::vec::Vec;
26use alloc::string::ToString;
27use llimphi_compositor::llimphi_layout::taffy::{
28    prelude::{auto, length, FlexDirection, Size, Style},
29    AlignItems, JustifyContent, Rect,
30};
31use llimphi_compositor::peniko::Color;
32use llimphi_compositor::llimphi_text::Alignment;
33use llimphi_compositor::View;
34use llimphi_theme::{motion, radius, Theme};
35
36/// Paleta del chip — tres slots: idle, selected, fg.
37#[derive(Debug, Clone, Copy)]
38pub struct ChipPalette {
39    pub bg_idle: Color,
40    pub bg_selected: Color,
41    pub fg_idle: Color,
42    pub fg_selected: Color,
43    pub border: Color,
44}
45
46impl ChipPalette {
47    pub fn from_theme(t: &Theme) -> Self {
48        // El selected es el accent atenuado hacia el fondo —"tinte", no
49        // bloque sólido—. Calculado en sRGB-ish: `accent · 0.32 + bg · 0.68`.
50        let a = t.accent.components;
51        let b = t.bg_panel.components;
52        let mix = Color {
53            components: [
54                a[0] * 0.32 + b[0] * 0.68,
55                a[1] * 0.32 + b[1] * 0.68,
56                a[2] * 0.32 + b[2] * 0.68,
57                1.0,
58            ],
59            ..t.accent
60        };
61        Self {
62            bg_idle: t.bg_button,
63            bg_selected: mix,
64            fg_idle: t.fg_text,
65            fg_selected: t.fg_text,
66            border: t.border,
67        }
68    }
69}
70
71/// Sabor del chip — decide cómo se interpreta el `selected` y si lleva
72/// botón × removible.
73#[derive(Debug, Clone, Copy, PartialEq, Eq)]
74pub enum ChipKind {
75    /// Toggle binario (selected ↔ no). Usado en `FilterChip` de Material.
76    Filter,
77    /// Radio dentro de un grupo (selected = exclusive). Visualmente igual
78    /// a Filter; la diferencia está en el caller que mantiene 1 seleccionado.
79    Choice,
80    /// Etiqueta con × final que dispara `on_remove`. Para tags, multi-select
81    /// presentado como contenedor de chips.
82    Input,
83    /// Acción rápida (no togglea); selected siempre falso.
84    Assist,
85}
86
87/// Chip base — devuelve el view con anim implícita de fill por estado.
88/// `key` debe ser estable entre frames del mismo chip (idx + grupo).
89pub fn chip_view<Msg: Clone + 'static>(
90    label: impl Into<String>,
91    kind: ChipKind,
92    selected: bool,
93    key: u64,
94    palette: &ChipPalette,
95    on_click: Msg,
96    on_remove: Option<Msg>,
97) -> View<Msg> {
98    chip_view_icon(None::<String>, label, kind, selected, key, palette, on_click, on_remove)
99}
100
101/// Como [`chip_view`] pero con **glyph líder** opcional (el "ícono opcional"
102/// que el assist-chip promete). `icon` `None` = sin ícono (idéntico a
103/// [`chip_view`]).
104#[allow(clippy::too_many_arguments)]
105pub fn chip_view_icon<Msg: Clone + 'static>(
106    icon: Option<impl Into<String>>,
107    label: impl Into<String>,
108    kind: ChipKind,
109    selected: bool,
110    key: u64,
111    palette: &ChipPalette,
112    on_click: Msg,
113    on_remove: Option<Msg>,
114) -> View<Msg> {
115    let bg = if selected { palette.bg_selected } else { palette.bg_idle };
116    let fg = if selected { palette.fg_selected } else { palette.fg_idle };
117
118    let label: String = label.into();
119    let aria = label.clone();
120    let mut children: Vec<View<Msg>> = Vec::with_capacity(3);
121    if let Some(g) = icon {
122        children.push(
123            View::new(Style {
124                size: Size { width: auto(), height: auto() },
125                margin: Rect {
126                    left: length(0.0_f32),
127                    right: length(5.0_f32),
128                    top: length(0.0_f32),
129                    bottom: length(0.0_f32),
130                },
131                ..Default::default()
132            })
133            .text_aligned(g.into(), 12.0, fg, Alignment::Center),
134        );
135    }
136    children.push(
137        View::new(Style {
138            size: Size { width: auto(), height: auto() },
139            ..Default::default()
140        })
141        .text_aligned(label, 12.0, fg, Alignment::Center),
142    );
143
144    if let (ChipKind::Input, Some(rm)) = (kind, on_remove) {
145        // ×: glifo pequeño a la derecha con on_click propio. No es
146        // botón nested (View no soporta nested on_click), así que es
147        // un nodo hermano con on_click separado dentro del mismo chip
148        // — el árbitro de hit-test elige el más interno.
149        children.push(
150            View::new(Style {
151                size: Size { width: length(14.0_f32), height: length(14.0_f32) },
152                margin: Rect {
153                    left: length(6.0_f32),
154                    right: length(0.0_f32),
155                    top: length(0.0_f32),
156                    bottom: length(0.0_f32),
157                },
158                align_items: Some(AlignItems::CENTER),
159                justify_content: Some(JustifyContent::CENTER),
160                ..Default::default()
161            })
162            .radius(7.0)
163            .hover_fill(palette.border)
164            .text_aligned("×".to_string(), 12.0, fg, Alignment::Center)
165            .on_click(rm)
166            .cursor(llimphi_compositor::Cursor::Pointer),
167        );
168    }
169
170    View::new(Style {
171        flex_direction: FlexDirection::Row,
172        align_items: Some(AlignItems::CENTER),
173        padding: Rect {
174            left: length(10.0_f32),
175            right: length(10.0_f32),
176            top: length(0.0_f32),
177            bottom: length(0.0_f32),
178        },
179        size: Size { width: auto(), height: length(24.0_f32) },
180        ..Default::default()
181    })
182    .fill(bg)
183    .radius(radius::XL)
184    .border(1.0, palette.border)
185    .animated(key, motion::MICRO)
186    .children(children)
187    // Filter chips se reportan como botones de toggle (pressed = selected);
188    // los input/assist chips son botones plain. AccessKit anuncia el estado
189    // de toggle al lector, así "está seleccionado / no seleccionado" sale solo.
190    .role(llimphi_compositor::Role::Button)
191    .aria_label(aria)
192    .aria_pressed(selected)
193    .on_click(on_click)
194    .cursor(llimphi_compositor::Cursor::Pointer)
195}
196
197/// Atajo: chip filter (toggle).
198pub fn filter_chip<Msg: Clone + 'static>(
199    label: impl Into<String>,
200    selected: bool,
201    key: u64,
202    palette: &ChipPalette,
203    on_toggle: Msg,
204) -> View<Msg> {
205    chip_view(label, ChipKind::Filter, selected, key, palette, on_toggle, None)
206}
207
208/// Atajo: chip input (removible).
209pub fn input_chip<Msg: Clone + 'static>(
210    label: impl Into<String>,
211    key: u64,
212    palette: &ChipPalette,
213    on_click: Msg,
214    on_remove: Msg,
215) -> View<Msg> {
216    chip_view(
217        label,
218        ChipKind::Input,
219        false,
220        key,
221        palette,
222        on_click,
223        Some(on_remove),
224    )
225}
226
227/// Atajo: chip assist (acción).
228pub fn assist_chip<Msg: Clone + 'static>(
229    label: impl Into<String>,
230    key: u64,
231    palette: &ChipPalette,
232    on_click: Msg,
233) -> View<Msg> {
234    chip_view(label, ChipKind::Assist, false, key, palette, on_click, None)
235}
236
237/// Atajo: chip assist con **glyph líder** (el "chip-acción con ícono" del
238/// docstring del módulo).
239pub fn assist_chip_icon<Msg: Clone + 'static>(
240    icon: impl Into<String>,
241    label: impl Into<String>,
242    key: u64,
243    palette: &ChipPalette,
244    on_click: Msg,
245) -> View<Msg> {
246    chip_view_icon(Some(icon), label, ChipKind::Assist, false, key, palette, on_click, None)
247}