tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ dba855e446ca2bd4e7d146bdc168fb232cad15a4
widgets/field/src/lib.rs
Ñawpa willay
Pi imata llamkarqan
ch'uya
feat: llimphi standalone — framework UI soberano extraído del monorepo
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}