//! `llimphi-widget-field` — wrapper de formulario para inputs. //! //! Patrón estándar de campo: //! ```text //! Nombre del campo (label — bold, fg_text) //! [ input control aquí ] (slot — viene como View) //! Descripción o error abajo (helper — fg_muted o fg_destructive) //! ``` //! //! El widget no implementa el input — lo recibe como `View` y lo //! envuelve. Esto permite usarlo con `text-input`, `text-area`, `switch`, //! `segmented` o cualquier otro control. #![forbid(unsafe_code)] // Sin `std`: este widget no usa nada del sistema —sólo `alloc` para // `String`/`Vec`—, y declararlo es lo que le permite cruzar a una jaula // WASM o al kernel. Antes enlazaba `std` por omisión (W4.6). #![no_std] extern crate alloc; use alloc::string::String; use alloc::vec::Vec; use alloc::vec; use alloc::format; use llimphi_compositor::llimphi_layout::taffy::{ prelude::{length, percent, FlexDirection, Size, Style}, }; use llimphi_compositor::peniko::Color; use llimphi_compositor::llimphi_text::Alignment; use llimphi_compositor::View; use llimphi_theme::Theme; #[derive(Debug, Clone, Copy)] pub struct FieldPalette { pub fg_label: Color, pub fg_helper: Color, pub fg_error: Color, pub fg_required: Color, } impl FieldPalette { pub fn from_theme(t: &Theme) -> Self { Self { fg_label: t.fg_text, fg_helper: t.fg_muted, fg_error: t.fg_destructive, fg_required: t.fg_destructive, } } } /// Spec del field. `helper` y `error` son mutuamente excluyentes — /// si hay error, se renderiza el error (rojo); si no, el helper. pub struct FieldSpec { pub label: String, /// El input/control concreto (text-input, switch, segmented, etc). pub control: View, /// Marca el field como requerido — agrega un asterisco al label. pub required: bool, /// Texto explicativo debajo del control. `None` para omitirlo. pub helper: Option, /// Mensaje de error — gana sobre `helper` cuando está presente. pub error: Option, pub palette: FieldPalette, } const LABEL_H: f32 = 16.0; const HELPER_H: f32 = 16.0; const GAP_LABEL: f32 = 4.0; const GAP_HELPER: f32 = 4.0; const LABEL_FONT: f32 = 11.5; const HELPER_FONT: f32 = 10.5; pub fn field_view(spec: FieldSpec) -> View { let FieldSpec { label, control, required, helper, error, palette, } = spec; // Label: nombre + (asterisco si required). // El nombre limpio (sin asterisco) es el que enuncia el lector: el `*` es // notación visual, y `required` ya viaja como flag. let aria_nombre = label.clone(); let label_text = if required { format!("{label} *") } else { label }; let label_view = View::new(Style { size: Size { width: percent(1.0_f32), height: length(LABEL_H), }, flex_shrink: 0.0, ..Default::default() }) .text_aligned(label_text, LABEL_FONT, palette.fg_label, Alignment::Start) .role(llimphi_compositor::Role::Label); // Helper / error — error gana si presente. let helper_text = error.clone().or(helper.clone()); let helper_color = if error.is_some() { palette.fg_error } else { palette.fg_helper }; let mut children: Vec> = vec![label_view, spacer(GAP_LABEL), control]; if let Some(t) = helper_text { children.push(spacer(GAP_HELPER)); children.push( View::new(Style { size: Size { width: percent(1.0_f32), height: length(HELPER_H), }, flex_shrink: 0.0, ..Default::default() }) .text_aligned(t, HELPER_FONT, helper_color, Alignment::Start), ); } // El grupo lleva el nombre y el estado del campo. Llimphi no modela el // `aria-labelledby` que asociaría label↔control, así que la aproximación // es agrupar: al entrar, el lector enuncia ", requerido, " // y después el control. let mut raiz = View::new(Style { flex_direction: FlexDirection::Column, size: Size { width: percent(1.0_f32), height: llimphi_compositor::llimphi_layout::taffy::prelude::auto(), }, ..Default::default() }) .role(llimphi_compositor::Role::Group) .aria_label(aria_nombre) .children(children); if required { raiz = raiz.aria_required(true); } // El error se anuncia como descripción; el helper también, si no hay error. if let Some(d) = error.or(helper) { raiz = raiz.aria_description(d); } raiz } fn spacer(h: f32) -> View { View::new(Style { size: Size { width: percent(1.0_f32), height: length(h), }, flex_shrink: 0.0, ..Default::default() }) }