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//! Demo de `llimphi-widget-text-input`: la paridad TextField/Flutter en vivo.
2//!
3//! Corre con: `cargo run -p llimphi-widget-text-input --example campo_demo --release`.
4//!
5//! Qué mirar:
6//! - **Label flotante ANIMADO**: al focar (o escribir), el rótulo **sube** con
7//! una animación, se achica, cambia al color de foco y **corta el borde**
8//! (notched outline de Material) — el hueco del borde se ajusta a su ancho.
9//! - **Relieve**: sombra + sheen + esquinas redondeadas; el borde engorda al focar.
10//! - **Scroll horizontal**: escribe más de lo que entra — el caret se mantiene a
11//! la vista y el texto se corre (o mira el campo prellenado largo).
12//! - **Selección real**: doble-click = palabra, triple = todo, arrastre = rango;
13//! el click cae en la letra correcta (glifos reales, no ancho fijo).
14//! - **Password**: puntos, sin filtrar el valor.
15
16use llimphi_motion::{animate, motion, Tween};
17use llimphi_ui::llimphi_layout::taffy::{
18 prelude::{length, percent, FlexDirection, Size, Style},
19 AlignItems, Rect,
20};
21use llimphi_ui::llimphi_raster::peniko::Color;
22use llimphi_ui::{App, Handle, KeyEvent, View};
23use llimphi_widget_text_input::{
24 float_target, text_input_field_view, text_input_view_full, MemClipboard, TextInputEvent,
25 TextInputPalette, TextInputState,
26};
27
28/// Cuántos campos usan label flotante (los tres primeros).
29const N_LABEL: usize = 3;
30/// Rótulos de los campos con label flotante.
31const LABELS: &[&str] = &["Nombre", "Correo", "Buscar (escribe para desbordar)"];
32
33#[derive(Clone, Debug)]
34enum Msg {
35 Campo(usize, TextInputEvent),
36 Tecla(KeyEvent),
37 /// Tick de animación (sólo dispara re-render; la view lee el Tween).
38 Tick,
39}
40
41struct Model {
42 campos: Vec<TextInputState>,
43 foco: Option<usize>,
44 clip: MemClipboard,
45 /// Animación del label flotante de cada campo con label (0=dentro, 1=arriba).
46 floats: Vec<Tween<f32>>,
47 /// Destino actual de cada `float` (para saber cuándo relanzar el tween).
48 targets: Vec<f32>,
49}
50
51struct Demo;
52
53impl Demo {
54 /// Tras un cambio, relanza el tween de los labels cuyo destino cambió (focar,
55 /// desfocar, o pasar de vacío↔con-texto) y arranca los ticks de animación.
56 fn reanimar(m: &mut Model, handle: &Handle<Msg>) {
57 for i in 0..N_LABEL {
58 let tgt = float_target(m.foco == Some(i), &m.campos[i]);
59 if (tgt - m.targets[i]).abs() > f32::EPSILON {
60 let actual = m.floats[i].value();
61 m.floats[i] = Tween::new(actual, tgt, motion::NORMAL, motion::ease_out_cubic);
62 m.targets[i] = tgt;
63 animate(handle, motion::NORMAL, || Msg::Tick);
64 }
65 }
66 }
67}
68
69impl App for Demo {
70 type Model = Model;
71 type Msg = Msg;
72
73 fn title() -> &'static str {
74 "llimphi · campo de texto (paridad TextField)"
75 }
76
77 fn init(_: &Handle<Msg>) -> Model {
78 let mut prellenado = TextInputState::new();
79 prellenado
80 .set_text("un texto largo que desborda la caja para ver el scroll horizontal del caret");
81 let campos = vec![
82 TextInputState::new(), // 0: label "Nombre"
83 TextInputState::new(), // 1: label "Correo"
84 prellenado, // 2: label + scroll (arranca con texto → flotado)
85 TextInputState::new(), // 3: plano
86 TextInputState::masked(), // 4: password
87 ];
88 // El campo 2 arranca con texto → su label ya está flotado.
89 let targets: Vec<f32> = (0..N_LABEL).map(|i| float_target(false, &campos[i])).collect();
90 let floats = targets.iter().map(|&t| Tween::idle(t)).collect();
91 Model {
92 campos,
93 foco: None,
94 clip: MemClipboard::new(),
95 floats,
96 targets,
97 }
98 }
99
100 fn on_key(model: &Model, event: &KeyEvent) -> Option<Msg> {
101 model.foco.map(|_| Msg::Tecla(event.clone()))
102 }
103
104 fn ime_allowed() -> bool {
105 true
106 }
107
108 fn update(model: Model, msg: Msg, handle: &Handle<Msg>) -> Model {
109 let mut m = model;
110 match msg {
111 Msg::Tick => {}
112 Msg::Campo(i, ev) => {
113 if matches!(ev, TextInputEvent::Press(_) | TextInputEvent::Context { .. })
114 && m.foco != Some(i)
115 {
116 m.foco = Some(i);
117 }
118 if let Some(st) = m.campos.get_mut(i) {
119 let mut clip = std::mem::take(&mut m.clip);
120 st.handle(ev, &mut clip);
121 m.clip = clip;
122 }
123 Demo::reanimar(&mut m, handle);
124 }
125 Msg::Tecla(e) => {
126 if let Some(i) = m.foco {
127 if let Some(st) = m.campos.get_mut(i) {
128 let mut clip = std::mem::take(&mut m.clip);
129 st.handle(TextInputEvent::Key(e), &mut clip);
130 m.clip = clip;
131 }
132 }
133 Demo::reanimar(&mut m, handle);
134 }
135 }
136 m
137 }
138
139 fn view(model: &Model) -> View<Msg> {
140 let pal = TextInputPalette::default();
141 let mut filas: Vec<View<Msg>> = Vec::new();
142
143 for (i, label) in LABELS.iter().enumerate() {
144 let focado = model.foco == Some(i);
145 let t = model.floats[i].value();
146 let idx = i;
147 filas.push(text_input_field_view(
148 &model.campos[i],
149 label,
150 focado,
151 t,
152 &pal,
153 move |ev| Msg::Campo(idx, ev),
154 ));
155 }
156 for (i, ph) in [(3usize, "campo plano (placeholder)"), (4, "contraseña")] {
157 let focado = model.foco == Some(i);
158 filas.push(text_input_view_full(
159 &model.campos[i],
160 ph,
161 focado,
162 &pal,
163 move |ev| Msg::Campo(i, ev),
164 ));
165 }
166
167 View::new(Style {
168 flex_direction: FlexDirection::Column,
169 size: Size { width: percent(1.0_f32), height: percent(1.0_f32) },
170 gap: Size { width: length(0.0_f32), height: length(20.0_f32) },
171 padding: Rect {
172 left: length(40.0_f32),
173 right: length(40.0_f32),
174 top: length(36.0_f32),
175 bottom: length(36.0_f32),
176 },
177 align_items: Some(AlignItems::STRETCH),
178 ..Default::default()
179 })
180 .fill(Color::from_rgba8(18, 21, 28, 255))
181 .children(filas)
182 }
183}
184
185fn main() {
186 llimphi_ui::run::<Demo>();
187}