tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
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}