tawasuyu

tawasuyu/llimphi

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

Rust 660Markdown 302TOML 157RON 312382 Sutichaykuna

⎇ main widgets/text-input/examples/campo_demo.rs Rust · 187 siq'ikuna · 6.5 KB Ñawpa willay Pi imata llamkarqan ch'uya

chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros d251eb32b · Sergio ·

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}