tawasuyu

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Definitions

⎇ main widgets/card/src/lib.rs Rust · 180 lines · 5.3 KB History Who touched what raw

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

1//! `llimphi-widget-card` — container card-shape para entries de
2//! timeline, info cards, dashboards, etc.
3//!
4//! Aporta la **forma**: padding consistente (12/8), `radius` 4, gap
5//! pequeño entre children, y opcionalmente un accent vertical
6//! (4 px) pegado a la izquierda para entries semánticas (verde =
7//! OK, rojo = error, ámbar = warning, etc).
8//!
9//! Análogo Llimphi al `nahual-widget-card` GPUI.
10
11#![forbid(unsafe_code)]
12
13
14// Sin `std`: este widget no usa nada del sistema —sólo `alloc` para
15// `String`/`Vec`/`Arc`—, y declararlo es lo que le permite cruzar a una
16// jaula WASM o al kernel. Antes enlazaba `std` por omisión (W4.6).
17#![no_std]
18
19extern crate alloc;
20
21use alloc::vec::Vec;
22use alloc::vec;
23use llimphi_compositor::llimphi_layout::taffy::{
24    prelude::{length, percent, FlexDirection, Size, Style},
25    Rect,
26};
27use llimphi_compositor::peniko::Color;
28use llimphi_compositor::{Shadow, View};
29use llimphi_theme::elevation;
30use llimphi_widget_panel::{panel_signature_painter, PanelStyle};
31
32#[derive(Debug, Clone, Copy)]
33pub struct CardPalette {
34    pub bg: Color,
35}
36
37impl Default for CardPalette {
38    fn default() -> Self {
39        Self::from_theme(&llimphi_theme::Theme::dark())
40    }
41}
42
43impl CardPalette {
44    pub fn from_theme(t: &llimphi_theme::Theme) -> Self {
45        Self { bg: t.bg_panel }
46    }
47}
48
49/// Opciones del card.
50#[derive(Debug, Clone, Copy)]
51pub struct CardOptions {
52    /// Accent vertical a la izquierda (4 px). `None` = sin accent.
53    pub accent: Option<Color>,
54    pub padding: f32,
55    pub gap: f32,
56    pub radius: f64,
57    /// Firma visual del panel (gradient sutil + hairline accent en el
58    /// top). `Some(style)` reemplaza el fill plano del body por el
59    /// painter de la firma — usar para cards prominentes (dashboards,
60    /// timeline entries grandes) donde se nota el "tallado". `None`
61    /// mantiene el fill sólido del `CardPalette` (default).
62    pub signature: Option<PanelStyle>,
63}
64
65impl Default for CardOptions {
66    fn default() -> Self {
67        Self {
68            accent: None,
69            padding: 12.0,
70            gap: 4.0,
71            radius: 4.0,
72            signature: None,
73        }
74    }
75}
76
77impl CardOptions {
78    /// Variante con firma visual derivada del theme. El `radius` del
79    /// card se alinea al del `PanelStyle` para que la silueta del
80    /// gradiente coincida con las esquinas del nodo.
81    pub fn with_signature(t: &llimphi_theme::Theme) -> Self {
82        let style = PanelStyle::from_theme(t);
83        Self {
84            accent: None,
85            padding: 12.0,
86            gap: 4.0,
87            radius: style.radius,
88            signature: Some(style),
89        }
90    }
91}
92
93/// Compone un card: bg + radius + padding + flex-column con gap entre
94/// children. Si `opts.accent` está presente, hay una franja vertical
95/// de 4 px del color del accent pegada al borde izquierdo.
96pub fn card_view<Msg: Clone + 'static>(
97    children: Vec<View<Msg>>,
98    opts: CardOptions,
99    palette: &CardPalette,
100) -> View<Msg> {
101    let pad = opts.padding;
102    let body_style = Style {
103        flex_direction: FlexDirection::Column,
104        size: Size {
105            width: percent(1.0_f32),
106            height: llimphi_compositor::llimphi_layout::taffy::prelude::Dimension::auto(),
107        },
108        flex_grow: 1.0,
109        padding: Rect {
110            left: length(pad),
111            right: length(pad),
112            top: length(pad * 0.66),
113            bottom: length(pad * 0.66),
114        },
115        gap: Size {
116            width: length(0.0_f32),
117            height: length(opts.gap),
118        },
119        ..Default::default()
120    };
121    let body = if let Some(style) = opts.signature {
122        View::new(body_style)
123            .paint_with(panel_signature_painter(style))
124            .radius(opts.radius)
125            .clip(true)
126            .children(children)
127    } else {
128        View::new(body_style)
129            .fill(palette.bg)
130            .radius(opts.radius)
131            .children(children)
132    };
133
134    let Some(accent) = opts.accent else {
135        return body;
136    };
137
138    let accent_strip = View::new(Style {
139        size: Size {
140            width: length(4.0_f32),
141            height: percent(1.0_f32),
142        },
143        ..Default::default()
144    })
145    .fill(accent)
146    .radius(opts.radius);
147
148    View::new(Style {
149        flex_direction: FlexDirection::Row,
150        size: Size {
151            width: percent(1.0_f32),
152            height: llimphi_compositor::llimphi_layout::taffy::prelude::Dimension::auto(),
153        },
154        ..Default::default()
155    })
156    .children(vec![accent_strip, body])
157}
158
159/// Variante elevada — la misma forma de `card_view` pero con sombra
160/// del nivel `elev` (token de [`llimphi_theme::elevation`]). Default
161/// pensado: `elevation::E2` (card flotante sobre el panel). El accent
162/// strip — si está presente — queda detrás de la sombra; el visual
163/// dominante es el body. Útil para dashboards y entries destacadas
164/// que necesitan separación clara del fondo del panel.
165pub fn card_elevated_view<Msg: Clone + 'static>(
166    children: Vec<View<Msg>>,
167    opts: CardOptions,
168    palette: &CardPalette,
169    elev: elevation::Elev,
170) -> View<Msg> {
171    let (a, blur, dy) = elev;
172    let shadow = Shadow {
173        color: Color::from_rgba8(0, 0, 0, a),
174        blur,
175        dx: 0.0,
176        dy,
177        spread: 0.0,
178    };
179    card_view(children, opts, palette).shadow(shadow)
180}