tawasuyu

tawasuyu/llimphi

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

Rust 484Markdown 258TOML 131RON 38118 Definiciones

⎇ main llimphi-compositor/src/render/hit.rs Rust · 499 líneas · 17.8 KB Historia Quién tocó qué crudo

chore: refresco desde el monorepo — 128 miembros, los 76 widgets y los 11 módulos b3f12e136 · Sergio ·

1use super::*;
2
3/// Hit-test parametrizado por elegibilidad. Devuelve el índice del nodo
4/// más al frente (último en pre-orden) cuyo rect contiene `(x, y)` y para
5/// el cual `pred` devuelve `true`, respetando `clip`: si el punto cae
6/// afuera de un nodo con clip, el subárbol entero es invisible.
7///
8/// **Respeta `transform`**: igual que [`paint`], compone el afín acumulado
9/// de los ancestros (cada `transform` alrededor del centro del rect del
10/// nodo, convención CSS `transform-origin: 50% 50%`). El punto de pantalla
11/// `(x, y)` se lleva al espacio local del nodo invirtiendo ese afín, y se
12/// testea contra el rect sin transformar. Así un nodo rotado/escalado/
13/// trasladado recibe los clicks donde realmente se ve pintado (recorrido
14/// tipo Prezi, lienzos de tullpu, `@keyframes` de puriy). Un subárbol con
15/// afín singular (escala 0) es inalcanzable, igual que es invisible.
16pub fn hit_test_pred<Msg, F>(
17    mounted: &Mounted<Msg>,
18    computed: &ComputedLayout,
19    x: f32,
20    y: f32,
21    pred: F,
22) -> Option<usize>
23where
24    F: Fn(&MountedNode<Msg>) -> bool,
25{
26    let mut hit: Option<usize> = None;
27    // Segundo candidato, para los subárboles [`View::over`]: `paint` los SALTA
28    // y `paint_over_layers` los pinta DESPUÉS, así que quedan encima de todo lo
29    // demás aunque su lugar en el árbol sea anterior. Sin esto, un nodo base
30    // posterior en orden de árbol le roba el click a un botón que el usuario ve
31    // arriba (caso real: la barra de control de media es `.over(true)`).
32    // El z efectivo es: [nodos normales, orden de árbol] < [capas over, orden de
33    // árbol], así que basta un candidato aparte y preferirlo al final. Las capas
34    // over anidadas las pinta la externa (`paint_over_layers` salta a
35    // `subtree_end`), o sea que no agregan un nivel de z: alcanza con saber si
36    // estamos DENTRO de alguna.
37    let mut hit_over: Option<usize> = None;
38    let mut over_end: usize = 0;
39    let mut clip_stack: Vec<usize> = Vec::new();
40    // Espejo del stack de transformaciones de `paint`: `cur_xf` es el
41    // producto acumulado de los `transform` de los ancestros activos
42    // (local → pantalla). Vacío ⇒ identidad ⇒ camino directo sin invertir
43    // (cero costo para la abrumadora mayoría de árboles sin transform).
44    let mut xf_stack: Vec<(usize, Affine)> = Vec::new();
45    let mut cur_xf = Affine::IDENTITY;
46    let mut idx = 0;
47    while idx < mounted.nodes.len() {
48        while let Some(&end) = clip_stack.last() {
49            if idx >= end {
50                clip_stack.pop();
51            } else {
52                break;
53            }
54        }
55        while let Some(&(end, prev)) = xf_stack.last() {
56            if idx >= end {
57                cur_xf = prev;
58                xf_stack.pop();
59            } else {
60                break;
61            }
62        }
63        let node = &mounted.nodes[idx];
64        if node.over_layer && idx >= over_end {
65            over_end = node.subtree_end;
66        }
67        let Some(r) = computed.get(node.id) else {
68            idx += 1;
69            continue;
70        };
71        // Componé el transform de este nodo igual que `paint`, ANTES de
72        // resolver el punto local (su propio rect ya cae en el espacio
73        // transformado).
74        if let Some(centered) = resolve_node_transform(node.transform, node.transform_rel, node.transform_origin, r) {
75            xf_stack.push((node.subtree_end, cur_xf));
76            cur_xf *= centered;
77        }
78        // Punto en el espacio local del nodo. Sin transform activo, es el
79        // punto de pantalla tal cual. Con transform, se invierte el afín;
80        // si es singular (no invertible) el subárbol es inalcanzable.
81        let (lx, ly) = if xf_stack.is_empty() {
82            (x as f64, y as f64)
83        } else if cur_xf.determinant().abs() < 1e-9 {
84            idx = node.subtree_end;
85            continue;
86        } else {
87            let p = cur_xf.inverse() * Point::new(x as f64, y as f64);
88            (p.x, p.y)
89        };
90        let inside = lx >= r.x as f64
91            && lx < (r.x + r.w) as f64
92            && ly >= r.y as f64
93            && ly < (r.y + r.h) as f64;
94        if node.clip {
95            if !inside {
96                idx = node.subtree_end;
97                continue;
98            }
99            clip_stack.push(node.subtree_end);
100        }
101        if inside && pred(node) {
102            if idx < over_end {
103                hit_over = Some(idx);
104            } else {
105                hit = Some(idx);
106            }
107        }
108        idx += 1;
109    }
110    hit_over.or(hit)
111}
112
113/// Hit-test específico para clicks (incluye nodos draggables).
114pub fn hit_test_click<Msg>(
115    mounted: &Mounted<Msg>,
116    computed: &ComputedLayout,
117    x: f32,
118    y: f32,
119) -> Option<usize> {
120    hit_test_pred(mounted, computed, x, y, |n| {
121        n.on_click.is_some()
122            || n.on_click_at.is_some()
123            || n.on_click_screen.is_some()
124            || n.drag.is_some()
125            || n.drag_at.is_some()
126            || n.drag_velocity.is_some()
127    })
128}
129
130/// Hit-test específico para right-click. Sólo considera nodos que
131/// declararon `on_right_click` o `on_right_click_at` — un right-click
132/// sobre un nodo sin handler no hace nada (no se "filtra" al click
133/// izquierdo).
134pub fn hit_test_right_click<Msg>(
135    mounted: &Mounted<Msg>,
136    computed: &ComputedLayout,
137    x: f32,
138    y: f32,
139) -> Option<usize> {
140    hit_test_pred(mounted, computed, x, y, |n| {
141        n.on_right_click.is_some() || n.on_right_click_at.is_some() || n.on_right_click_screen.is_some()
142    })
143}
144
145/// Hit-test específico para middle-click. Mismo modelo que right-click:
146/// sólo nodos que declararon `on_middle_click` reaccionan.
147pub fn hit_test_middle_click<Msg>(
148    mounted: &Mounted<Msg>,
149    computed: &ComputedLayout,
150    x: f32,
151    y: f32,
152) -> Option<usize> {
153    hit_test_pred(mounted, computed, x, y, |n| n.on_middle_click.is_some())
154}
155
156/// Hit-test específico para hover (nodos con `hover_fill`).
157pub fn hit_test_hover<Msg>(
158    mounted: &Mounted<Msg>,
159    computed: &ComputedLayout,
160    x: f32,
161    y: f32,
162) -> Option<usize> {
163    hit_test_pred(mounted, computed, x, y, |n| n.hover_fill.is_some())
164}
165
166/// Hit-test específico para **tooltips** (nodos con `tooltip`). Separado del
167/// hover: un nodo puede declarar tooltip sin `hover_fill` (p. ej. los iconos
168/// fugaces de pata, que pintan por `paint_with` y no quieren tinte de hover) —
169/// con el predicado de hover su tooltip jamás se encontraba.
170pub fn hit_test_tooltip<Msg>(
171    mounted: &Mounted<Msg>,
172    computed: &ComputedLayout,
173    x: f32,
174    y: f32,
175) -> Option<usize> {
176    hit_test_pred(mounted, computed, x, y, |n| n.tooltip.is_some())
177}
178
179/// Hit-test para movimiento posicional del cursor (nodos con
180/// `on_pointer_move_at`). El runtime lo invoca en cada `CursorMoved` para
181/// reportar la posición local al nodo más al frente que lo declare.
182pub fn hit_test_pointer_move<Msg>(
183    mounted: &Mounted<Msg>,
184    computed: &ComputedLayout,
185    x: f32,
186    y: f32,
187) -> Option<usize> {
188    hit_test_pred(mounted, computed, x, y, |n| n.on_pointer_move_at.is_some())
189}
190
191/// Hit-test específico para la **forma del cursor**: devuelve el [`Cursor`]
192/// del nodo más al frente bajo el punto que declare uno. Como un hijo sin
193/// cursor no matchea el predicado, el cursor "cae" al ancestro más cercano que
194/// lo declare — herencia estilo CSS sin recorrer el árbol a mano. `None` =
195/// ningún nodo bajo el punto declara cursor (el runtime usa el default de la
196/// ventana). Lo invoca `llimphi-ui` en la transición de hover.
197pub fn hit_test_cursor<Msg>(
198    mounted: &Mounted<Msg>,
199    computed: &ComputedLayout,
200    x: f32,
201    y: f32,
202) -> Option<Cursor> {
203    hit_test_pred(mounted, computed, x, y, |n| n.cursor.is_some())
204        .and_then(|i| mounted.nodes[i].cursor)
205}
206
207/// Hit-test específico para drop targets (nodos con `on_drop`). Usado
208/// durante un drag activo para resaltar el destino y para invocar el
209/// handler al soltar.
210pub fn hit_test_drop<Msg>(
211    mounted: &Mounted<Msg>,
212    computed: &ComputedLayout,
213    x: f32,
214    y: f32,
215) -> Option<usize> {
216    hit_test_pred(mounted, computed, x, y, |n| n.on_drop.is_some())
217}
218
219/// Hit-test específico para áreas de scroll (nodos con `on_scroll`). El
220/// runtime lo usa al recibir la rueda: el nodo más al frente bajo el
221/// cursor con handler de scroll consume el evento antes del `on_wheel`
222/// global.
223pub fn hit_test_scroll<Msg>(
224    mounted: &Mounted<Msg>,
225    computed: &ComputedLayout,
226    x: f32,
227    y: f32,
228) -> Option<usize> {
229    hit_test_pred(mounted, computed, x, y, |n| n.on_scroll.is_some())
230}
231
232/// Cadena de **scroll anidado**: devuelve todos los nodos con `on_scroll`
233/// que contienen el punto, ordenados **front→back** (el primero es el más
234/// al frente, igual que [`hit_test_scroll`]; los siguientes son sus
235/// ancestros scrollables). El runtime itera la cadena al recibir la rueda
236/// y se queda con el primer handler que devuelva `Some`: si un scroll
237/// interno está en el extremo del eje y devuelve `None`, el evento "pasa"
238/// al ancestro scrollable más cercano (lista dentro de panel, etc.).
239/// Recorrido idéntico al de [`hit_test_pred`] pero acumulando todos los
240/// hits en vez de pisar.
241pub fn hit_test_scroll_chain<Msg>(
242    mounted: &Mounted<Msg>,
243    computed: &ComputedLayout,
244    x: f32,
245    y: f32,
246) -> Vec<usize> {
247    let mut chain: Vec<usize> = Vec::new();
248    let mut clip_stack: Vec<usize> = Vec::new();
249    let mut xf_stack: Vec<(usize, Affine)> = Vec::new();
250    let mut cur_xf = Affine::IDENTITY;
251    let mut idx = 0;
252    while idx < mounted.nodes.len() {
253        while let Some(&end) = clip_stack.last() {
254            if idx >= end {
255                clip_stack.pop();
256            } else {
257                break;
258            }
259        }
260        while let Some(&(end, prev)) = xf_stack.last() {
261            if idx >= end {
262                cur_xf = prev;
263                xf_stack.pop();
264            } else {
265                break;
266            }
267        }
268        let node = &mounted.nodes[idx];
269        let Some(r) = computed.get(node.id) else {
270            idx += 1;
271            continue;
272        };
273        if let Some(centered) = resolve_node_transform(node.transform, node.transform_rel, node.transform_origin, r) {
274            xf_stack.push((node.subtree_end, cur_xf));
275            cur_xf *= centered;
276        }
277        let (lx, ly) = if xf_stack.is_empty() {
278            (x as f64, y as f64)
279        } else if cur_xf.determinant().abs() < 1e-9 {
280            idx = node.subtree_end;
281            continue;
282        } else {
283            let p = cur_xf.inverse() * Point::new(x as f64, y as f64);
284            (p.x, p.y)
285        };
286        let inside = lx >= r.x as f64
287            && lx < (r.x + r.w) as f64
288            && ly >= r.y as f64
289            && ly < (r.y + r.h) as f64;
290        if node.clip {
291            if !inside {
292                idx = node.subtree_end;
293                continue;
294            }
295            clip_stack.push(node.subtree_end);
296        }
297        if inside && (node.on_scroll.is_some() || node.on_scroll_take.is_some()) {
298            chain.push(idx);
299        }
300        idx += 1;
301    }
302    // El recorrido es pre-orden, así que los ancestros aparecen primero y
303    // los hijos después. Para front→back necesitamos el orden inverso.
304    chain.reverse();
305    chain
306}
307
308/// Reparte un evento de rueda a lo largo de la cadena de scroll (front→back,
309/// de [`hit_test_scroll_chain`]). Para cada nodo:
310///
311/// - con [`on_scroll_take`](crate::View::on_scroll_take): consume lo que
312///   puede y el **sobrante** sigue al próximo de la cadena — un mismo evento
313///   puede mover al hijo hasta su tope Y al ancestro con el resto;
314/// - con `on_scroll` clásico: semántica histórica — `Some` consume TODO lo
315///   restante y corta; `None` deja pasar el delta completo.
316///
317/// Devuelve los `Msg` en orden (el runtime los aplica en secuencia sobre el
318/// `update`). Vacío = nadie consumió (el evento cae al `on_wheel` global).
319/// Pura respecto del árbol: no muta nada, testeable headless.
320pub fn consume_scroll_chain<Msg>(
321    mounted: &Mounted<Msg>,
322    chain: &[usize],
323    dx: f32,
324    dy: f32,
325) -> Vec<Msg> {
326    const EPS: f32 = 1e-4;
327    let mut msgs = Vec::new();
328    let (mut rx, mut ry) = (dx, dy);
329    for &idx in chain {
330        if rx.abs() <= EPS && ry.abs() <= EPS {
331            break;
332        }
333        let node = &mounted.nodes[idx];
334        if let Some(take) = &node.on_scroll_take {
335            let (msg, nx, ny) = take(rx, ry);
336            if let Some(m) = msg {
337                msgs.push(m);
338            }
339            // Defensa: un handler no puede "crear" delta ni invertir su
340            // signo — el sobrante se acota al delta que recibió.
341            rx = clamp_rest(nx, rx);
342            ry = clamp_rest(ny, ry);
343        } else if let Some(h) = &node.on_scroll {
344            if let Some(m) = h(rx, ry) {
345                msgs.push(m);
346                break;
347            }
348        }
349    }
350    msgs
351}
352
353/// Acota el sobrante declarado por un handler al rango `[0, delta]` (con el
354/// signo del delta): ni negativo del otro lado, ni mayor que lo recibido.
355pub(crate) fn clamp_rest(rest: f32, delta: f32) -> f32 {
356    if delta >= 0.0 {
357        rest.clamp(0.0, delta)
358    } else {
359        rest.clamp(delta, 0.0)
360    }
361}
362
363/// Hit-test específico para gestos de **escala** (pinch-to-zoom): el nodo más
364/// al frente bajo el punto que declaró un `on_scale`. Como un hijo sin handler
365/// no matchea el predicado, el gesto "cae" al ancestro más cercano que lo
366/// declare (un canvas grande zoomeable con widgets encima que no zoomean). El
367/// runtime lo invoca al recibir Ctrl+rueda o un pinch de trackpad. `None` =
368/// ningún nodo zoomeable bajo el cursor (el evento cae al scroll/`on_wheel`).
369pub fn hit_test_scale<Msg>(
370    mounted: &Mounted<Msg>,
371    computed: &ComputedLayout,
372    x: f32,
373    y: f32,
374) -> Option<usize> {
375    hit_test_pred(mounted, computed, x, y, |n| n.on_scale.is_some())
376}
377
378/// Hit-test específico para gestos de **rotación** (trackpad): el nodo más al
379/// frente bajo el punto que declaró un `on_rotate`. Análogo a
380/// [`hit_test_scale`]; el runtime lo invoca al recibir un `RotationGesture`.
381/// `None` = ningún nodo rotable bajo el cursor.
382pub fn hit_test_rotate<Msg>(
383    mounted: &Mounted<Msg>,
384    computed: &ComputedLayout,
385    x: f32,
386    y: f32,
387) -> Option<usize> {
388    hit_test_pred(mounted, computed, x, y, |n| n.on_rotate.is_some())
389}
390
391/// Hit-test para **doble-tap**: el nodo más al frente bajo el punto que
392/// declaró `on_double_tap`/`on_double_tap_at`. El runtime lo usa al detectar
393/// dos presses rápidos y cercanos.
394pub fn hit_test_double_tap<Msg>(
395    mounted: &Mounted<Msg>,
396    computed: &ComputedLayout,
397    x: f32,
398    y: f32,
399) -> Option<usize> {
400    hit_test_pred(mounted, computed, x, y, |n| {
401        n.on_double_tap.is_some() || n.on_double_tap_at.is_some()
402    })
403}
404
405/// Hit-test para **long-press**: el nodo más al frente bajo el punto que
406/// declaró `on_long_press`/`on_long_press_at`. El runtime lo usa al armar el
407/// gesto en el press (que vence por tiempo si no hay movimiento ni release).
408pub fn hit_test_long_press<Msg>(
409    mounted: &Mounted<Msg>,
410    computed: &ComputedLayout,
411    x: f32,
412    y: f32,
413) -> Option<usize> {
414    hit_test_pred(mounted, computed, x, y, |n| {
415        n.on_long_press.is_some() || n.on_long_press_at.is_some()
416    })
417}
418
419/// Hit-test para **ripple**: el nodo más al frente bajo el punto que declaró
420/// un [`Ripple`] (vía [`View::ripple`]). El runtime lo usa en el press para
421/// disparar la salpicadura. Aditivo — no compite con click/drag.
422pub fn hit_test_ripple<Msg>(
423    mounted: &Mounted<Msg>,
424    computed: &ComputedLayout,
425    x: f32,
426    y: f32,
427) -> Option<usize> {
428    hit_test_pred(mounted, computed, x, y, |n| n.ripple.is_some())
429}
430
431/// Hit-test para foco: el id `focusable` del nodo más al frente bajo el
432/// cursor (click-to-focus). `None` si no se clickeó nada enfocable.
433pub fn hit_test_focusable<Msg>(
434    mounted: &Mounted<Msg>,
435    computed: &ComputedLayout,
436    x: f32,
437    y: f32,
438) -> Option<u64> {
439    hit_test_pred(mounted, computed, x, y, |n| n.focusable.is_some())
440        .and_then(|i| mounted.nodes[i].focusable)
441}
442
443/// Hit-test para **selección de texto**: el índice del nodo de texto
444/// seleccionable (`text_select_key`) más al frente bajo el cursor. El runtime
445/// lo usa para arrancar/extender una selección; devuelve el índice (no la key)
446/// para que el caller acceda al `text` + rect del nodo. `None` si no hay texto
447/// seleccionable bajo el punto.
448pub fn hit_test_selectable<Msg>(
449    mounted: &Mounted<Msg>,
450    computed: &ComputedLayout,
451    x: f32,
452    y: f32,
453) -> Option<usize> {
454    hit_test_pred(mounted, computed, x, y, |n| n.text_select_key.is_some())
455}
456
457/// Ids enfocables en orden de Tab (pre-orden del árbol = orden de
458/// inserción de `Mounted::nodes`). Sólo nodos con rect computado
459/// (presentes en el layout). Es el orden DOM-like de tabulación.
460pub fn focus_order<Msg>(mounted: &Mounted<Msg>, computed: &ComputedLayout) -> Vec<u64> {
461    mounted
462        .nodes
463        .iter()
464        .filter_map(|n| {
465            n.focusable
466                .filter(|_| computed.get(n.id).is_some())
467        })
468        .collect()
469}
470
471/// Próximo id de foco al pulsar Tab (o Shift+Tab si `reverse`), dado el
472/// `order` (de [`focus_order`]) y el `current`. Envuelve en los extremos.
473/// Si no hay enfocables devuelve `None`; si `current` ya no existe en el
474/// orden, arranca por el primero (Tab) o el último (Shift+Tab).
475pub fn next_focus(order: &[u64], current: Option<u64>, reverse: bool) -> Option<u64> {
476    if order.is_empty() {
477        return None;
478    }
479    let n = order.len();
480    let pos = current.and_then(|c| order.iter().position(|&id| id == c));
481    let next_idx = match pos {
482        Some(i) => {
483            if reverse {
484                (i + n - 1) % n
485            } else {
486                (i + 1) % n
487            }
488        }
489        None => {
490            if reverse {
491                n - 1
492            } else {
493                0
494            }
495        }
496    };
497    Some(order[next_idx])
498}
499