tawasuyu

tawasuyu/llimphi

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

Rust 124Markdown 82TOML 78Shell 12149 Sutichaykuna

⎇ dba855e446ca2bd4e7d146bdc168fb232cad15a4 widgets/field/src/lib.rs Rust · 127 siq'ikuna · 3.8 KB Ñawpa willay Pi imata llamkarqan ch'uya

feat: llimphi standalone — framework UI soberano extraído del monorepo e65e9cc62 · Sergio ·

1//! `llimphi-widget-field` — wrapper de formulario para inputs.
2//!
3//! Patrón estándar de campo:
4//! ```text
5//! Nombre del campo            (label — bold, fg_text)
6//! [ input control aquí ]      (slot — viene como View<Msg>)
7//! Descripción o error abajo   (helper — fg_muted o fg_destructive)
8//! ```
9//!
10//! El widget no implementa el input — lo recibe como `View<Msg>` y lo
11//! envuelve. Esto permite usarlo con `text-input`, `text-area`, `switch`,
12//! `segmented` o cualquier otro control.
13
14#![forbid(unsafe_code)]
15
16use llimphi_ui::llimphi_layout::taffy::{
17    prelude::{length, percent, FlexDirection, Size, Style},
18};
19use llimphi_ui::llimphi_raster::peniko::Color;
20use llimphi_ui::llimphi_text::Alignment;
21use llimphi_ui::View;
22use llimphi_theme::Theme;
23
24#[derive(Debug, Clone, Copy)]
25pub struct FieldPalette {
26    pub fg_label: Color,
27    pub fg_helper: Color,
28    pub fg_error: Color,
29    pub fg_required: Color,
30}
31
32impl FieldPalette {
33    pub fn from_theme(t: &Theme) -> Self {
34        Self {
35            fg_label: t.fg_text,
36            fg_helper: t.fg_muted,
37            fg_error: t.fg_destructive,
38            fg_required: t.fg_destructive,
39        }
40    }
41}
42
43/// Spec del field. `helper` y `error` son mutuamente excluyentes —
44/// si hay error, se renderiza el error (rojo); si no, el helper.
45pub struct FieldSpec<Msg: Clone + 'static> {
46    pub label: String,
47    /// El input/control concreto (text-input, switch, segmented, etc).
48    pub control: View<Msg>,
49    /// Marca el field como requerido — agrega un asterisco al label.
50    pub required: bool,
51    /// Texto explicativo debajo del control. `None` para omitirlo.
52    pub helper: Option<String>,
53    /// Mensaje de error — gana sobre `helper` cuando está presente.
54    pub error: Option<String>,
55    pub palette: FieldPalette,
56}
57
58const LABEL_H: f32 = 16.0;
59const HELPER_H: f32 = 16.0;
60const GAP_LABEL: f32 = 4.0;
61const GAP_HELPER: f32 = 4.0;
62const LABEL_FONT: f32 = 11.5;
63const HELPER_FONT: f32 = 10.5;
64
65pub fn field_view<Msg: Clone + 'static>(spec: FieldSpec<Msg>) -> View<Msg> {
66    let FieldSpec {
67        label,
68        control,
69        required,
70        helper,
71        error,
72        palette,
73    } = spec;
74
75    // Label: nombre + (asterisco si required).
76    let label_text = if required { format!("{label} *") } else { label };
77    let label_view = View::new(Style {
78        size: Size {
79            width: percent(1.0_f32),
80            height: length(LABEL_H),
81        },
82        flex_shrink: 0.0,
83        ..Default::default()
84    })
85    .text_aligned(label_text, LABEL_FONT, palette.fg_label, Alignment::Start);
86
87    // Helper / error — error gana si presente.
88    let helper_text = error.clone().or(helper.clone());
89    let helper_color = if error.is_some() { palette.fg_error } else { palette.fg_helper };
90
91    let mut children: Vec<View<Msg>> = vec![label_view, spacer(GAP_LABEL), control];
92    if let Some(t) = helper_text {
93        children.push(spacer(GAP_HELPER));
94        children.push(
95            View::new(Style {
96                size: Size {
97                    width: percent(1.0_f32),
98                    height: length(HELPER_H),
99                },
100                flex_shrink: 0.0,
101                ..Default::default()
102            })
103            .text_aligned(t, HELPER_FONT, helper_color, Alignment::Start),
104        );
105    }
106
107    View::new(Style {
108        flex_direction: FlexDirection::Column,
109        size: Size {
110            width: percent(1.0_f32),
111            height: llimphi_ui::llimphi_layout::taffy::prelude::auto(),
112        },
113        ..Default::default()
114    })
115    .children(children)
116}
117
118fn spacer<Msg: Clone + 'static>(h: f32) -> View<Msg> {
119    View::new(Style {
120        size: Size {
121            width: percent(1.0_f32),
122            height: length(h),
123        },
124        flex_shrink: 0.0,
125        ..Default::default()
126    })
127}