tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
⎇ 5ae7c737031521b9a873f3d7cff36261fbf7ae06
llimphi-ui/src/eventloop/input.rs
History
Who touched what
raw
refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
1// input.rs — Manejo de todos los eventos de entrada de la ventana primaria.
2// Cubre: teclado, mouse (click/drag/rueda/gestos), IME, drop de archivos,
3// resize, DPI. Cada variante de `WindowEvent` queda en su sección.
4
5use super::super::*;
6use super::helpers::*;
7
8/// Resultado de un hit-test de click izquierdo. Tupla interna para extraer
9/// handlers del cache antes de mutar el modelo (suelta el borrow).
10pub(super) type HitInfo<M> = (
11 Option<DragFn<M>>,
12 Option<DragAtFn<M>>,
13 Option<DragVelocityFn<M>>,
14 Option<u64>,
15 Option<M>,
16 Option<ClickAtFn<M>>,
17 Option<(f32, f32, f32, f32)>,
18);
19
20/// Extrae el `HitInfo` de click izquierdo para un árbol montado dado.
21pub(super) fn lookup_click_hit<Msg: Clone>(
22 m: &Mounted<Msg>,
23 c: &ComputedLayout,
24 cx: f32,
25 cy: f32,
26) -> Option<HitInfo<Msg>> {
27 hit_test_click(m, c, cx, cy).map(|i| {
28 let node = &m.nodes[i];
29 let rect = c.get(node.id).map(|r| (r.x, r.y, r.w, r.h));
30 (
31 node.drag.clone(),
32 node.drag_at.clone(),
33 node.drag_velocity.clone(),
34 node.drag_payload,
35 node.on_click.clone(),
36 node.on_click_at.clone(),
37 rect,
38 )
39 })
40}
41
42impl<A: App> Runtime<A> {
43 /// Maneja un `WindowEvent` de la ventana primaria. La delegación a
44 /// secciones internas sigue el orden de frecuencia (más común primero).
45 pub(super) fn handle_primary_window_event(
46 &mut self,
47 event_loop: &llimphi_hal::winit::event_loop::ActiveEventLoop,
48 event: llimphi_hal::winit::event::WindowEvent,
49 ) {
50 use llimphi_hal::winit::event::WindowEvent;
51 let Some(state) = self.state.as_mut() else {
52 return;
53 };
54 // Cada window_event debe pasar primero por el adapter AccessKit para
55 // que las tecnologías asistivas se enteren del estado real de la
56 // ventana (focus_change del SO, cursor moves, etc.). El adapter
57 // no consume el evento — lo despacha aparte vía el EventLoopProxy.
58 state.a11y_adapter.process_event(&state.window, &event);
59 match event {
60 WindowEvent::CloseRequested => event_loop.exit(),
61 WindowEvent::Resized(size) => {
62 state.surface.resize(size.width, size.height);
63 // La app puede reaccionar al nuevo viewport (emitir un
64 // evento `resize`, recalcular layout, etc.). El update se
65 // corre tras reconfigurar la surface; el cache se invalida
66 // para repintar con el tamaño nuevo.
67 if let Some(msg) =
68 A::on_resize(state.model.as_ref().expect("model"), size.width, size.height)
69 {
70 let model = state.model.take().expect("model");
71 state.model = Some(A::update(model, msg, &self.handle));
72 state.last_render = None;
73 }
74 state.window.request_redraw();
75 }
76 WindowEvent::ScaleFactorChanged { scale_factor, .. } => {
77 // El DPI de la ventana cambió (movida a otro monitor, escalado
78 // del sistema). winit envía un Resized aparte para el nuevo
79 // tamaño físico; aquí sólo propagamos el factor.
80 if let Some(msg) =
81 A::on_scale_factor(state.model.as_ref().expect("model"), scale_factor)
82 {
83 let model = state.model.take().expect("model");
84 state.model = Some(A::update(model, msg, &self.handle));
85 state.last_render = None;
86 }
87 state.window.request_redraw();
88 }
89 WindowEvent::CursorMoved { position, .. } => {
90 self.handle_cursor_moved(position);
91 }
92 WindowEvent::ModifiersChanged(mods) => {
93 let Some(state) = self.state.as_mut() else { return };
94 state.modifiers = mods.state().into();
95 }
96 WindowEvent::Ime(ime) if A::ime_allowed() => {
97 self.handle_ime(ime);
98 }
99 WindowEvent::KeyboardInput { event, .. } => {
100 self.handle_keyboard_input(event);
101 }
102 WindowEvent::DroppedFile(path) => {
103 // Un evento por archivo (winit los entrega serializados); si
104 // el usuario suelta varios, el bucle re-entra y aplicamos
105 // updates en orden.
106 let Some(state) = self.state.as_mut() else { return };
107 if let Some(msg) = A::on_file_drop(state.model.as_ref().expect("model"), path) {
108 let model = state.model.take().expect("model");
109 state.model = Some(A::update(model, msg, &self.handle));
110 state.last_render = None;
111 state.window.request_redraw();
112 }
113 }
114 WindowEvent::MouseWheel { delta, .. } => {
115 self.handle_mouse_wheel(delta);
116 }
117 WindowEvent::PinchGesture { delta, phase, .. } => {
118 self.handle_pinch_gesture(delta, phase);
119 }
120 WindowEvent::RotationGesture { delta, phase, .. } => {
121 self.handle_rotation_gesture(delta, phase);
122 }
123 WindowEvent::MouseInput {
124 state: ElementState::Pressed,
125 button: MouseButton::Left,
126 ..
127 } => {
128 self.handle_left_press();
129 }
130 WindowEvent::MouseInput {
131 state: ElementState::Pressed,
132 button: MouseButton::Middle,
133 ..
134 } => {
135 self.handle_middle_press();
136 }
137 WindowEvent::MouseInput {
138 state: ElementState::Pressed,
139 button: MouseButton::Right,
140 ..
141 } => {
142 self.handle_right_press();
143 }
144 WindowEvent::MouseInput {
145 state: ElementState::Released,
146 button: MouseButton::Left,
147 ..
148 } => {
149 self.handle_left_release();
150 }
151 WindowEvent::RedrawRequested => {
152 super::redraw::handle_redraw::<A>(
153 self.state.as_mut().expect("state en redraw"),
154 &self.handle,
155 );
156 }
157 _ => {}
158 }
159 }
160
161 // ── Cursor moved ─────────────────────────────────────────────────────────
162
163 fn handle_cursor_moved(
164 &mut self,
165 position: llimphi_hal::winit::dpi::PhysicalPosition<f64>,
166 ) {
167 let Some(state) = self.state.as_mut() else { return };
168 let prev_cursor = state.cursor;
169 state.cursor = position;
170 // Selección de texto en curso: extender el foco al punto actual.
171 if let Some(tsel) = state.selection.filter(|s| s.dragging) {
172 let info = state
173 .last_render
174 .as_ref()
175 .and_then(|c| selectable_by_key(c, tsel.key));
176 if let Some((spec, (rx, ry, rw, _rh))) = info {
177 let layout = build_selectable_layout(&mut state.typesetter, &spec, rw);
178 let lx = position.x as f32 - rx;
179 let ly = position.y as f32 - ry;
180 let new_sel = tsel.sel.extend_to_point(&layout, lx, ly);
181 state.selection = Some(TextSelection { sel: new_sel, ..tsel });
182 state.last_render = None;
183 state.window.request_redraw();
184 }
185 }
186 // Long-press armado: si el cursor se alejó del origen del press
187 // más que el umbral, el gesto pasó a drag/scroll → cancelar.
188 if let Some(p) = state.pending_long_press.as_ref() {
189 let dx = position.x - p.origin.x;
190 let dy = position.y - p.origin.y;
191 if (dx * dx + dy * dy).sqrt() > LONG_PRESS_MOVE_CANCEL {
192 state.pending_long_press = None;
193 }
194 }
195 // Drag activo: dispatchear delta al handler + actualizar
196 // tracking del drop target hovereado (solo si hay payload).
197 if let Some(drag) = state.drag.as_mut() {
198 let dx = (position.x - drag.last_cursor.x) as f32;
199 let dy = (position.y - drag.last_cursor.y) as f32;
200 drag.last_cursor = position;
201 let payload_active = drag.payload.is_some();
202 let mut need_redraw = false;
203 if dx != 0.0 || dy != 0.0 {
204 let msg_opt = match &drag.handler {
205 DragHandlerKind::Delta(h) => h(DragPhase::Move, dx, dy),
206 DragHandlerKind::DeltaAt(h, lx0, ly0) => {
207 h(DragPhase::Move, dx, dy, *lx0, *ly0)
208 }
209 DragHandlerKind::Velocity(h) => {
210 // Durante Move, vx=vy=0 — la velocidad sólo
211 // tiene sentido al End. Acá registramos el
212 // sample para esa medición.
213 let now = std::time::Instant::now();
214 drag.samples.push_back((now, dx as f64, dy as f64));
215 while drag.samples.len() > VELOCITY_MAX_SAMPLES {
216 drag.samples.pop_front();
217 }
218 h(DragPhase::Move, dx, dy, 0.0, 0.0)
219 }
220 };
221 if let Some(msg) = msg_opt {
222 let model = state.model.take().expect("model");
223 state.model = Some(A::update(model, msg, &self.handle));
224 // Durante drag NO invalidamos el cache —
225 // queda válido para el próximo Move.
226 need_redraw = true;
227 }
228 }
229 if payload_active {
230 if let Some(cache) = state.last_render.as_mut() {
231 let new_drop = hit_test_drop(
232 &cache.mounted,
233 &cache.computed,
234 position.x as f32,
235 position.y as f32,
236 );
237 if new_drop != cache.drop_hover_idx {
238 cache.drop_hover_idx = new_drop;
239 need_redraw = true;
240 }
241 }
242 }
243 if need_redraw {
244 state.window.request_redraw();
245 }
246 } else {
247 // Sin drag: chequear hover. Si hay overlay, el
248 // hover-test va contra él; el árbol principal queda
249 // congelado mientras el overlay esté arriba.
250 //
251 // Además del repintado (para el `hover_fill`), en la
252 // transición de hover dispatcheamos dos Msgs: el
253 // `on_pointer_leave` del nodo que abandonamos y el
254 // `on_pointer_enter` del recién hovereado. Es lo que
255 // permite, p.ej., cambiar de menú con el mouse, abrir un
256 // submenú al pasar por encima, o cerrar un tooltip/drawer
257 // al salir. Extraemos los Msg en un scope para soltar el
258 // borrow del cache antes de mutar el modelo.
259 let mut enter_msg: Option<A::Msg> = None;
260 let mut leave_msg: Option<A::Msg> = None;
261 let mut hovered_changed = false;
262 let mut new_hovered: Option<usize> = state.hovered;
263 // Forma del cursor del nodo recién hovereado (sólo se
264 // resuelve en la transición; ver `to_winit_cursor`).
265 let mut new_cursor: Option<Cursor> = None;
266 if let Some(cache) = state.last_render.as_ref() {
267 let (mounted, computed) = match cache.overlay.as_ref() {
268 Some(ov) => (&ov.mounted, &ov.computed),
269 None => (&cache.mounted, &cache.computed),
270 };
271 let new_hover = hit_test_hover(
272 mounted,
273 computed,
274 position.x as f32,
275 position.y as f32,
276 );
277 // Comparamos contra el hover PERSISTENTE (state.hovered),
278 // no contra el del cache: el render recomputa el del cache
279 // al cursor actual cada cuadro, así que en una app que
280 // re-renderiza sin parar la transición de hover se perdería
281 // (y el hover-switch de menús no andaría). Ver `hovered`.
282 if new_hover != state.hovered {
283 hovered_changed = true;
284 // El nodo que dejamos (índice persistente previo)
285 // dispara su leave; el nuevo, su enter. Ambos se
286 // resuelven contra el árbol montado actual, igual
287 // que el resto del hit-test de hover.
288 leave_msg = state
289 .hovered
290 .and_then(|i| mounted.nodes.get(i))
291 .and_then(|n| n.on_pointer_leave.clone());
292 enter_msg = new_hover
293 .and_then(|i| mounted.nodes.get(i))
294 .and_then(|n| n.on_pointer_enter.clone());
295 new_cursor = hit_test_cursor(
296 mounted,
297 computed,
298 position.x as f32,
299 position.y as f32,
300 );
301 }
302 new_hovered = new_hover;
303 }
304 state.hovered = new_hovered;
305 if hovered_changed {
306 state.window.set_cursor(to_winit_cursor(new_cursor));
307 // Invalidar el cache de paint retenido: el `hover_fill`
308 // del nodo viejo está pintado en `state.scene` y el del
309 // nuevo no — un cache hit re-presentaría el frame stale.
310 state.last_render = None;
311 state.window.request_redraw();
312 }
313 // Despachamos leave antes que enter: el orden natural de
314 // la transición es "salgo de A, luego entro en B".
315 for msg in [leave_msg, enter_msg].into_iter().flatten() {
316 let model = state.model.take().expect("model");
317 state.model = Some(A::update(model, msg, &self.handle));
318 // El estado cambió → invalidamos el cache para
319 // re-render (p.ej. el submenú que se abre/cierra).
320 state.last_render = None;
321 }
322 // Movimiento posicional: dispatch `on_pointer_move_at` en CADA move
323 // mientras el cursor está sobre el nodo (no sólo al entrar, como
324 // `on_pointer_enter`). Base del thumbnail/drawer que sigue al
325 // cursor. Extraemos el handler (Arc) + rect en un scope para soltar
326 // el borrow del cache antes de mutar el modelo.
327 let move_call: Option<(ClickAtFn<A::Msg>, f32, f32, f32, f32)> =
328 state.last_render.as_ref().and_then(|cache| {
329 let (mounted, computed) = match cache.overlay.as_ref() {
330 Some(ov) => (&ov.mounted, &ov.computed),
331 None => (&cache.mounted, &cache.computed),
332 };
333 let px = position.x as f32;
334 let py = position.y as f32;
335 let i = hit_test_pointer_move(mounted, computed, px, py)?;
336 let node = &mounted.nodes[i];
337 let h = node.on_pointer_move_at.clone()?;
338 let r = computed.get(node.id)?;
339 Some((h, px - r.x, py - r.y, r.w, r.h))
340 });
341 if let Some((h, lx, ly, w, hh)) = move_call {
342 if let Some(msg) = h(lx, ly, w, hh) {
343 let model = state.model.take().expect("model");
344 state.model = Some(A::update(model, msg, &self.handle));
345 state.last_render = None;
346 state.window.request_redraw();
347 }
348 }
349 let _ = prev_cursor;
350 }
351 }
352
353 // ── IME ──────────────────────────────────────────────────────────────────
354
355 fn handle_ime(&mut self, ime: llimphi_hal::winit::event::Ime) {
356 use llimphi_hal::winit::event::Ime;
357 let Some(state) = self.state.as_mut() else { return };
358 let ev = match ime {
359 Ime::Enabled => ImeEvent::Enabled,
360 Ime::Preedit(text, cursor) => ImeEvent::Preedit { text, cursor },
361 Ime::Commit(text) => ImeEvent::Commit(text),
362 Ime::Disabled => ImeEvent::Disabled,
363 };
364 if let Some(msg) = A::on_ime(state.model.as_ref().expect("model"), &ev) {
365 let model = state.model.take().expect("model");
366 state.model = Some(A::update(model, msg, &self.handle));
367 state.last_render = None;
368 state.window.request_redraw();
369 }
370 }
371
372 // ── Teclado ───────────────────────────────────────────────────────────────
373
374 fn handle_keyboard_input(
375 &mut self,
376 event: llimphi_hal::winit::event::KeyEvent,
377 ) {
378 let Some(state) = self.state.as_mut() else { return };
379 // Tab / Shift+Tab mueven el foco entre nodos `focusable`,
380 // que administra el runtime. Sólo intercepta si hay
381 // enfocables y en Pressed; si no, cae al `on_key` normal
382 // (apps que usan Tab para otra cosa lo siguen recibiendo).
383 let is_tab = event.state == ElementState::Pressed
384 && matches!(event.logical_key, Key::Named(NamedKey::Tab));
385 if is_tab {
386 let order = state
387 .last_render
388 .as_ref()
389 .map(|c| focus_order(&c.mounted, &c.computed))
390 .unwrap_or_default();
391 if !order.is_empty() {
392 let next = next_focus(&order, state.focused, state.modifiers.shift);
393 state.focused = next;
394 if let Some(msg) =
395 A::on_focus(state.model.as_ref().expect("model"), next)
396 {
397 let model = state.model.take().expect("model");
398 state.model = Some(A::update(model, msg, &self.handle));
399 }
400 state.last_render = None;
401 state.window.request_redraw();
402 return;
403 }
404 }
405 // Ctrl/Cmd+C copia la selección de texto activa (fuera del
406 // editor). Consume el evento sólo si había algo seleccionado;
407 // si no, cae al `on_key` normal (apps que usan Ctrl+C para lo
408 // suyo lo siguen recibiendo).
409 if event.state == ElementState::Pressed
410 && (state.modifiers.ctrl || state.modifiers.meta)
411 && key_is_char(&event.logical_key, 'c')
412 {
413 if let Some(tsel) = state.selection {
414 let text = state
415 .last_render
416 .as_ref()
417 .and_then(|c| selectable_by_key(c, tsel.key))
418 .and_then(|(spec, _)| {
419 spec.content.get(tsel.sel.text_range()).map(str::to_string)
420 });
421 if let Some(t) = text.filter(|t| !t.is_empty()) {
422 copy_to_clipboard(&t);
423 return;
424 }
425 }
426 }
427 let ev = KeyEvent {
428 key: event.logical_key.clone(),
429 state: match event.state {
430 ElementState::Pressed => KeyState::Pressed,
431 ElementState::Released => KeyState::Released,
432 },
433 text: event.text.as_ref().map(|t| t.to_string()),
434 modifiers: state.modifiers,
435 repeat: event.repeat,
436 };
437 if let Some(msg) = A::on_key(state.model.as_ref().expect("model"), &ev) {
438 let model = state.model.take().expect("model");
439 state.model = Some(A::update(model, msg, &self.handle));
440 state.last_render = None;
441 state.window.request_redraw();
442 }
443 }
444
445 // ── Rueda del mouse ───────────────────────────────────────────────────────
446
447 fn handle_mouse_wheel(
448 &mut self,
449 delta: MouseScrollDelta,
450 ) {
451 let Some(state) = self.state.as_mut() else { return };
452 // Convención winit: LineDelta es líneas; PixelDelta es
453 // píxeles físicos (touchpads). En CSS y aquí, positivo
454 // (rueda hacia adelante / dos dedos arriba) = scroll
455 // hacia arriba, así que invertimos `y` para que el
456 // contenido "siga al dedo" en y positivo. `x` queda
457 // como llega.
458 let wd = match delta {
459 MouseScrollDelta::LineDelta(x, y) => WheelDelta { x, y: -y },
460 MouseScrollDelta::PixelDelta(p) => WheelDelta {
461 x: (p.x as f32) / 20.0,
462 y: -(p.y as f32) / 20.0,
463 },
464 };
465 let cursor = (state.cursor.x as f32, state.cursor.y as f32);
466 // Ctrl+rueda = **pinch-to-zoom sintético** (camino universal de
467 // desktop: Wayland/Windows no emiten el gesto de pinch del
468 // trackpad). Si hay un nodo `on_scale` bajo el cursor, el zoom
469 // lo consume ANTES que el scroll/`on_wheel`. Factor
470 // multiplicativo incremental `1.1^(-dy)`: rueda hacia arriba
471 // (`dy<0`) agranda, hacia abajo achica. El punto focal es el
472 // cursor (en coords locales al nodo), para zoomear "hacia el
473 // cursor". Sin nodo zoomeable, Ctrl+rueda cae al `on_wheel`
474 // global como siempre (browsers, etc. lo siguen recibiendo).
475 if state.modifiers.ctrl {
476 let scale_hit = state
477 .last_render
478 .as_ref()
479 .and_then(|cache| scale_hit_from_cache(cache, cursor.0, cursor.1));
480 if let Some((h, fx, fy)) = scale_hit {
481 let factor = 1.1_f32.powf(-wd.y);
482 if let Some(msg) = h(GesturePhase::Update, factor, fx, fy) {
483 let model = state.model.take().expect("model");
484 state.model = Some(A::update(model, msg, &self.handle));
485 state.last_render = None;
486 state.window.request_redraw();
487 }
488 return;
489 }
490 }
491 // Primero: ¿hay nodos con `on_scroll` bajo el cursor? Se
492 // arma la **cadena** (front→back) y se invoca cada handler
493 // en orden; el primero que devuelva `Some` consume el
494 // evento. Un scroll interno que llegó al extremo de su eje
495 // puede devolver `None` para dejar el sobrante al ancestro
496 // scrollable más cercano (scroll anidado). Si todos
497 // devuelven `None`, cae al `on_wheel` global.
498 // El overlay tiene prioridad, igual que con clicks. Se
499 // extraen los handlers en un scope para soltar el borrow
500 // del cache antes de mutar el modelo.
501 let scroll_chain: Vec<ScrollFn<A::Msg>> =
502 if let Some(cache) = state.last_render.as_ref() {
503 let (m, c) = match cache.overlay.as_ref() {
504 Some(ov) => (&ov.mounted, &ov.computed),
505 None => (&cache.mounted, &cache.computed),
506 };
507 hit_test_scroll_chain(m, c, cursor.0, cursor.1)
508 .into_iter()
509 .filter_map(|i| m.nodes[i].on_scroll.clone())
510 .collect()
511 } else {
512 Vec::new()
513 };
514 let mut msg: Option<A::Msg> = None;
515 for h in &scroll_chain {
516 if let Some(m) = h(wd.x, wd.y) {
517 msg = Some(m);
518 break;
519 }
520 }
521 if msg.is_none() {
522 msg = A::on_wheel(
523 state.model.as_ref().expect("model"),
524 wd,
525 cursor,
526 state.modifiers,
527 );
528 }
529 if let Some(msg) = msg {
530 let model = state.model.take().expect("model");
531 state.model = Some(A::update(model, msg, &self.handle));
532 state.last_render = None;
533 state.window.request_redraw();
534 }
535 }
536
537 // ── Gestos trackpad ───────────────────────────────────────────────────────
538
539 fn handle_pinch_gesture(
540 &mut self,
541 delta: f64,
542 phase: llimphi_hal::winit::event::TouchPhase,
543 ) {
544 use llimphi_hal::winit::event::TouchPhase;
545 let Some(state) = self.state.as_mut() else { return };
546 // Pinch del trackpad (winit lo emite **sólo en macOS/iOS**; en
547 // Wayland/Windows el zoom va por Ctrl+rueda, arriba). `delta` es
548 // el cambio de escala incremental (p. ej. 0.01 = +1%); lo
549 // mapeamos al mismo `on_scale` que Ctrl+rueda, con factor
550 // multiplicativo `1.0 + delta`. La fase de winit se traduce a
551 // la de gesto (Begin/Update/End) para que el handler pueda, p.
552 // ej., abrir/cerrar un estado de zoom en vivo.
553 let cursor = (state.cursor.x as f32, state.cursor.y as f32);
554 let gphase = match phase {
555 TouchPhase::Started => GesturePhase::Begin,
556 TouchPhase::Moved => GesturePhase::Update,
557 TouchPhase::Ended | TouchPhase::Cancelled => GesturePhase::End,
558 };
559 // `delta` puede venir NaN según la doc de winit; en ese caso
560 // (o fuera de Update) el factor neutro es 1.0.
561 let factor = if gphase == GesturePhase::Update && delta.is_finite() {
562 (1.0 + delta) as f32
563 } else {
564 1.0
565 };
566 let scale_hit = state
567 .last_render
568 .as_ref()
569 .and_then(|cache| scale_hit_from_cache(cache, cursor.0, cursor.1));
570 if let Some((h, fx, fy)) = scale_hit {
571 if let Some(msg) = h(gphase, factor, fx, fy) {
572 let model = state.model.take().expect("model");
573 state.model = Some(A::update(model, msg, &self.handle));
574 state.last_render = None;
575 state.window.request_redraw();
576 }
577 }
578 }
579
580 fn handle_rotation_gesture(
581 &mut self,
582 delta: f32,
583 phase: llimphi_hal::winit::event::TouchPhase,
584 ) {
585 use llimphi_hal::winit::event::TouchPhase;
586 let Some(state) = self.state.as_mut() else { return };
587 // Rotación de dos dedos en el trackpad (winit la emite **sólo
588 // en macOS**). `delta` viene en **grados**; lo convertimos a
589 // radianes para el handler (positivo = horario). La fase de
590 // winit se traduce a la de gesto (Begin/Update/End). No hay
591 // camino universal por teclado/rueda como sí lo tiene el zoom.
592 let cursor = (state.cursor.x as f32, state.cursor.y as f32);
593 let gphase = match phase {
594 TouchPhase::Started => GesturePhase::Begin,
595 TouchPhase::Moved => GesturePhase::Update,
596 TouchPhase::Ended | TouchPhase::Cancelled => GesturePhase::End,
597 };
598 let delta_rad = if gphase == GesturePhase::Update && delta.is_finite() {
599 delta.to_radians()
600 } else {
601 0.0
602 };
603 let rotate_hit = state
604 .last_render
605 .as_ref()
606 .and_then(|cache| rotate_hit_from_cache(cache, cursor.0, cursor.1));
607 if let Some((h, fx, fy)) = rotate_hit {
608 if let Some(msg) = h(gphase, delta_rad, fx, fy) {
609 let model = state.model.take().expect("model");
610 state.model = Some(A::update(model, msg, &self.handle));
611 state.last_render = None;
612 state.window.request_redraw();
613 }
614 }
615 }
616
617 // ── Click izquierdo ───────────────────────────────────────────────────────
618
619 fn handle_left_press(&mut self) {
620 let Some(state) = self.state.as_mut() else { return };
621 let cursor = state.cursor;
622 // Click-to-focus: si el click cae sobre un nodo enfocable,
623 // el runtime le da el foco ANTES de procesar la acción de
624 // click. Extraemos el id en un scope (suelta el borrow del
625 // cache) y recién después mutamos el foco/modelo.
626 let focus_hit = state
627 .last_render
628 .as_ref()
629 .and_then(|cache| {
630 let (m, c) = match cache.overlay.as_ref() {
631 Some(ov) => (&ov.mounted, &ov.computed),
632 None => (&cache.mounted, &cache.computed),
633 };
634 hit_test_focusable(m, c, cursor.x as f32, cursor.y as f32)
635 });
636 if focus_hit.is_some() && focus_hit != state.focused {
637 state.focused = focus_hit;
638 if let Some(msg) =
639 A::on_focus(state.model.as_ref().expect("model"), focus_hit)
640 {
641 let model = state.model.take().expect("model");
642 state.model = Some(A::update(model, msg, &self.handle));
643 }
644 state.last_render = None;
645 }
646 // ── Selección de texto fuera del editor (aditiva) ──────────
647 // Si el press cae sobre un nodo `View::selectable`, arranca una
648 // selección (anchor = punto). Un press en cualquier otro lado
649 // limpia la selección activa. Aditivo: un label seleccionable
650 // sin `on_click`/drag no choca con el camino de abajo.
651 let sel_hit = state.last_render.as_ref().and_then(|c| {
652 selectable_hit_from_cache(c, cursor.x as f32, cursor.y as f32)
653 });
654 if let Some((key, spec, (rx, ry, rw, _rh))) = sel_hit {
655 let layout = build_selectable_layout(&mut state.typesetter, &spec, rw);
656 let lx = cursor.x as f32 - rx;
657 let ly = cursor.y as f32 - ry;
658 let sel = llimphi_text::parley::Selection::from_point(&layout, lx, ly);
659 state.selection = Some(TextSelection { key, sel, dragging: true });
660 state.last_render = None;
661 state.window.request_redraw();
662 } else if state.selection.is_some() {
663 state.selection = None;
664 state.last_render = None;
665 state.window.request_redraw();
666 }
667 // ── Arena de gestos (aditiva): doble-tap + long-press ──────
668 // Se resuelven con su propio hit-test y NO tocan el camino de
669 // click/drag de abajo (intacto). El árbitro real es el tiempo:
670 // doble-tap = dos presses cercanos dentro de una ventana;
671 // long-press = un press que sobrevive ~500 ms quieto (lo vence
672 // `about_to_wait`, lo cancela el movimiento/release).
673 let now = std::time::Instant::now();
674 // Doble-tap: si este press cae sobre un nodo con doble-tap y
675 // hubo un tap previo cercano y a tiempo, dispará. Si no, este
676 // press queda registrado como "primer tap".
677 if let Some(resolved) =
678 state.last_render.as_ref().and_then(|c| {
679 double_tap_hit_from_cache(c, cursor.x as f32, cursor.y as f32)
680 })
681 {
682 let qualifies = double_tap_qualifies(state.last_tap, now, cursor);
683 if qualifies {
684 state.last_tap = None; // consumido; un 3er tap no re-dispara
685 if let Some(msg) = resolved.invoke() {
686 let model = state.model.take().expect("model");
687 state.model = Some(A::update(model, msg, &self.handle));
688 state.last_render = None;
689 state.window.request_redraw();
690 }
691 } else {
692 state.last_tap = Some((now, cursor));
693 }
694 }
695 // Long-press: si el press cae sobre un nodo con long-press,
696 // armalo. `about_to_wait` lo dispara al vencer; CursorMoved lo
697 // cancela si el cursor se aleja; el release lo cancela siempre.
698 if let Some(handler) = state.last_render.as_ref().and_then(|c| {
699 long_press_hit_from_cache(c, cursor.x as f32, cursor.y as f32)
700 }) {
701 state.pending_long_press = Some(PendingLongPress {
702 deadline: now + LONG_PRESS_DELAY,
703 origin: cursor,
704 handler,
705 });
706 }
707 // Ripple/InkWell: si el press cae sobre un nodo con ripple,
708 // dispará la salpicadura desde el punto. Aditivo — no toca el
709 // camino click/drag de abajo; un botón con `on_click` +
710 // `.ripple(...)` recibe ambos.
711 if let Some((rp, lx, ly)) = state
712 .last_render
713 .as_ref()
714 .and_then(|c| ripple_hit_from_cache(c, cursor.x as f32, cursor.y as f32))
715 {
716 state
717 .ripple_registry
718 .trigger(rp.key, lx, ly, rp.color, rp.duration, now);
719 // El ripple se pinta sobre la scene; invalidamos el cache
720 // de paint para que el próximo redraw lo dibuje (sin esto,
721 // un cache hit re-presentaría el frame sin la salpicadura).
722 state.last_render = None;
723 state.window.request_redraw();
724 }
725 // Con overlay activo, los clicks van EXCLUSIVAMENTE a él.
726 // Si el cursor cae sobre un nodo del overlay sin handler,
727 // el click se descarta — la convención de "scrim que
728 // dismissa" pide que la app meta su propio fondo
729 // clicable con `on_click = DismissOverlay`.
730 let idx_and_action: Option<HitInfo<A::Msg>> = if let Some(cache) =
731 state.last_render.as_ref()
732 {
733 if let Some(ov) = cache.overlay.as_ref() {
734 lookup_click_hit(&ov.mounted, &ov.computed, cursor.x as f32, cursor.y as f32)
735 } else {
736 lookup_click_hit(&cache.mounted, &cache.computed, cursor.x as f32, cursor.y as f32)
737 }
738 } else {
739 let (w, h) = state.surface.size();
740 // Mismo resolve de LayoutBuilder que el redraw, para que el
741 // hit-test del fallback vea los hijos producidos por builders.
742 let view = resolve_layout_builders::<A>(
743 state.model.as_ref().expect("model"),
744 (w as f32, h as f32),
745 &mut state.typesetter,
746 );
747 let model_ref = state.model.as_ref().expect("model");
748 let overlay_view = A::view_overlay(model_ref);
749 let mut layout = LayoutTree::new();
750 let mounted: Mounted<A::Msg> = mount(&mut layout, view);
751 let ts = &mut state.typesetter;
752 let computed = {
753 let tmap = &mounted.text_measures;
754 layout
755 .compute_with_measure(mounted.root, (w as f32, h as f32), |nid, known, avail| {
756 match tmap.get(&nid) {
757 Some(tm) => measure_text_node(ts, tm, known, avail),
758 None => llimphi_layout::taffy::Size::ZERO,
759 }
760 })
761 .expect("layout")
762 };
763 if let Some(ov) = overlay_view {
764 let mut olay = LayoutTree::new();
765 let omounted: Mounted<A::Msg> = mount(&mut olay, ov);
766 let ocomp = {
767 let tmap = &omounted.text_measures;
768 olay
769 .compute_with_measure(omounted.root, (w as f32, h as f32), |nid, known, avail| {
770 match tmap.get(&nid) {
771 Some(tm) => measure_text_node(ts, tm, known, avail),
772 None => llimphi_layout::taffy::Size::ZERO,
773 }
774 })
775 .expect("layout overlay")
776 };
777 lookup_click_hit(&omounted, &ocomp, cursor.x as f32, cursor.y as f32)
778 } else {
779 lookup_click_hit(&mounted, &computed, cursor.x as f32, cursor.y as f32)
780 }
781 };
782 // Prioridad: drag_velocity > drag_at + on_click_at > drag >
783 // on_click_at > on_click. `drag_velocity` gana exclusivo
784 // (no convive con on_click_at) — el caller que lo elige
785 // suele querer un fling físico puro (lienzos pan-and-fling).
786 if let Some((_, _, Some(handler_v), payload, _, _, _)) = &idx_and_action {
787 state.drag = Some(DragState {
788 handler: DragHandlerKind::Velocity(handler_v.clone()),
789 last_cursor: cursor,
790 payload: *payload,
791 samples: std::collections::VecDeque::with_capacity(VELOCITY_MAX_SAMPLES),
792 });
793 state.window.request_redraw();
794 } else if let Some((_, Some(handler_at), _, payload, _, click_at, Some((ox, oy, rw, rh)))) =
795 &idx_and_action
796 {
797 // drag_at + on_click_at COEXISTEN: el press dispara
798 // on_click_at (si está) y arranca un drag rastreado con
799 // la posición inicial. Diseño pensado para canvas
800 // elements que necesitan select-on-press + move-on-drag.
801 let lx0 = cursor.x as f32 - ox;
802 let ly0 = cursor.y as f32 - oy;
803 // Disparar on_click_at en el press (si también está).
804 if let Some(click_at_h) = click_at {
805 if let Some(msg) = click_at_h(lx0, ly0, *rw, *rh) {
806 let model = state.model.take().expect("model");
807 state.model = Some(A::update(model, msg, &self.handle));
808 state.last_render = None;
809 }
810 }
811 state.drag = Some(DragState {
812 handler: DragHandlerKind::DeltaAt(handler_at.clone(), lx0, ly0),
813 last_cursor: cursor,
814 payload: *payload,
815 samples: std::collections::VecDeque::new(),
816 });
817 state.window.request_redraw();
818 } else if let Some((Some(handler), _, _, payload, _, _, _)) = &idx_and_action {
819 // `drag` simple (sin _at, sin velocity) mantiene la
820 // semántica antigua: gana exclusivo sobre on_click.
821 state.drag = Some(DragState {
822 handler: DragHandlerKind::Delta(handler.clone()),
823 last_cursor: cursor,
824 payload: *payload,
825 samples: std::collections::VecDeque::new(),
826 });
827 // Si hay payload, repintar para que el drop target
828 // bajo cursor (si lo hay) se ilumine de entrada.
829 if payload.is_some() {
830 if let Some(cache) = state.last_render.as_mut() {
831 let new_drop = hit_test_drop(
832 &cache.mounted,
833 &cache.computed,
834 cursor.x as f32,
835 cursor.y as f32,
836 );
837 if new_drop != cache.drop_hover_idx {
838 cache.drop_hover_idx = new_drop;
839 state.window.request_redraw();
840 }
841 }
842 }
843 } else if let Some((_, _, _, _, _, Some(handler), Some((ox, oy, rw, rh)))) =
844 &idx_and_action
845 {
846 // on_click_at gana sobre on_click si ambos existen.
847 let lx = cursor.x as f32 - ox;
848 let ly = cursor.y as f32 - oy;
849 if let Some(msg) = handler(lx, ly, *rw, *rh) {
850 let model = state.model.take().expect("model");
851 state.model = Some(A::update(model, msg, &self.handle));
852 state.last_render = None;
853 state.window.request_redraw();
854 }
855 } else if let Some((_, _, _, _, Some(msg), _, _)) = idx_and_action {
856 let model = state.model.take().expect("model");
857 state.model = Some(A::update(model, msg, &self.handle));
858 state.last_render = None;
859 state.window.request_redraw();
860 }
861 }
862
863 // ── Click medio ───────────────────────────────────────────────────────────
864
865 fn handle_middle_press(&mut self) {
866 let Some(state) = self.state.as_mut() else { return };
867 // Middle-click: dispatcha `on_middle_click` del nodo
868 // bajo cursor si lo declaró. La capa overlay tiene
869 // prioridad (mismo razonamiento que el left/right click).
870 let cursor = state.cursor;
871 let lookup =
872 |m: &Mounted<A::Msg>, c: &ComputedLayout| -> Option<A::Msg> {
873 hit_test_middle_click(m, c, cursor.x as f32, cursor.y as f32)
874 .and_then(|i| m.nodes[i].on_middle_click.clone())
875 };
876 let msg = if let Some(cache) = state.last_render.as_ref() {
877 if let Some(ov) = cache.overlay.as_ref() {
878 lookup(&ov.mounted, &ov.computed)
879 } else {
880 lookup(&cache.mounted, &cache.computed)
881 }
882 } else {
883 None
884 };
885 if let Some(msg) = msg {
886 let model = state.model.take().expect("model");
887 state.model = Some(A::update(model, msg, &self.handle));
888 state.last_render = None;
889 state.window.request_redraw();
890 }
891 }
892
893 // ── Click derecho ─────────────────────────────────────────────────────────
894
895 fn handle_right_press(&mut self) {
896 let Some(state) = self.state.as_mut() else { return };
897 // Right-click: dispatcheamos `on_right_click` o
898 // `on_right_click_at` del nodo bajo cursor. La capa
899 // overlay tiene prioridad (mismo razonamiento que el
900 // left-click). Nodos sin handler de right-click no
901 // reaccionan — no "filtramos" al left.
902 let cursor = state.cursor;
903 let lookup =
904 |m: &Mounted<A::Msg>, c: &ComputedLayout| -> Option<(Option<A::Msg>, Option<ClickAtFn<A::Msg>>, (f32, f32, f32, f32))> {
905 hit_test_right_click(m, c, cursor.x as f32, cursor.y as f32).map(|i| {
906 let node = &m.nodes[i];
907 let rect = c
908 .get(node.id)
909 .map(|r| (r.x, r.y, r.w, r.h))
910 .unwrap_or((0.0, 0.0, 0.0, 0.0));
911 (
912 node.on_right_click.clone(),
913 node.on_right_click_at.clone(),
914 rect,
915 )
916 })
917 };
918 let hit = if let Some(cache) = state.last_render.as_ref() {
919 if let Some(ov) = cache.overlay.as_ref() {
920 lookup(&ov.mounted, &ov.computed)
921 } else {
922 lookup(&cache.mounted, &cache.computed)
923 }
924 } else {
925 None
926 };
927 if let Some((msg_opt, at_opt, (ox, oy, rw, rh))) = hit {
928 let msg = if let Some(handler) = at_opt {
929 handler(
930 cursor.x as f32 - ox,
931 cursor.y as f32 - oy,
932 rw,
933 rh,
934 )
935 } else {
936 msg_opt
937 };
938 if let Some(msg) = msg {
939 let model = state.model.take().expect("model");
940 state.model = Some(A::update(model, msg, &self.handle));
941 state.last_render = None;
942 state.window.request_redraw();
943 }
944 }
945 }
946
947 // ── Release izquierdo ─────────────────────────────────────────────────────
948
949 fn handle_left_release(&mut self) {
950 let Some(state) = self.state.as_mut() else { return };
951 // El botón se soltó antes de vencer el long-press → no era un
952 // long-press (fue un click/drag); cancelá el gesto armado.
953 state.pending_long_press = None;
954 // Fin del arrastre de selección: la selección queda viva (para
955 // Ctrl/Cmd+C) pero deja de extenderse con el cursor.
956 if let Some(tsel) = state.selection.as_mut() {
957 tsel.dragging = false;
958 }
959 if let Some(drag) = state.drag.take() {
960 let cursor = state.cursor;
961 // 1. Drop: si hay payload + drop target bajo cursor,
962 // invocamos su handler. El Msg resultante se aplica
963 // ANTES del End del drag — la convención es "drop
964 // primero, cleanup del drag después".
965 if let Some(payload) = drag.payload {
966 if let Some(cache) = state.last_render.as_ref() {
967 if let Some(idx) = hit_test_drop(
968 &cache.mounted,
969 &cache.computed,
970 cursor.x as f32,
971 cursor.y as f32,
972 ) {
973 if let Some(drop_h) =
974 cache.mounted.nodes[idx].on_drop.clone()
975 {
976 if let Some(msg) = (drop_h)(payload) {
977 let model = state.model.take().expect("model");
978 state.model = Some(A::update(model, msg, &self.handle));
979 }
980 }
981 }
982 }
983 }
984 // 2. Cierre del drag.
985 let end_msg = match &drag.handler {
986 DragHandlerKind::Delta(h) => h(DragPhase::End, 0.0, 0.0),
987 DragHandlerKind::DeltaAt(h, lx0, ly0) => {
988 h(DragPhase::End, 0.0, 0.0, *lx0, *ly0)
989 }
990 DragHandlerKind::Velocity(h) => {
991 let (vx, vy) =
992 compute_drag_velocity(&drag.samples, std::time::Instant::now());
993 h(DragPhase::End, 0.0, 0.0, vx, vy)
994 }
995 };
996 if let Some(msg) = end_msg {
997 let model = state.model.take().expect("model");
998 state.model = Some(A::update(model, msg, &self.handle));
999 }
1000 // Cache invalidado siempre — hover/drop pueden cambiar
1001 // y el modelo posiblemente mutó.
1002 state.last_render = None;
1003 state.window.request_redraw();
1004 }
1005 }
1006}