use super::*; /// Hit-test parametrizado por elegibilidad. Devuelve el índice del nodo /// más al frente (último en pre-orden) cuyo rect contiene `(x, y)` y para /// el cual `pred` devuelve `true`, respetando `clip`: si el punto cae /// afuera de un nodo con clip, el subárbol entero es invisible. /// /// **Respeta `transform`**: igual que [`paint`], compone el afín acumulado /// de los ancestros (cada `transform` alrededor del centro del rect del /// nodo, convención CSS `transform-origin: 50% 50%`). El punto de pantalla /// `(x, y)` se lleva al espacio local del nodo invirtiendo ese afín, y se /// testea contra el rect sin transformar. Así un nodo rotado/escalado/ /// trasladado recibe los clicks donde realmente se ve pintado (recorrido /// tipo Prezi, lienzos de tullpu, `@keyframes` de puriy). Un subárbol con /// afín singular (escala 0) es inalcanzable, igual que es invisible. pub fn hit_test_pred( mounted: &Mounted, computed: &ComputedLayout, x: f32, y: f32, pred: F, ) -> Option where F: Fn(&MountedNode) -> bool, { let mut hit: Option = None; // Segundo candidato, para los subárboles [`View::over`]: `paint` los SALTA // y `paint_over_layers` los pinta DESPUÉS, así que quedan encima de todo lo // demás aunque su lugar en el árbol sea anterior. Sin esto, un nodo base // posterior en orden de árbol le roba el click a un botón que el usuario ve // arriba (caso real: la barra de control de media es `.over(true)`). // El z efectivo es: [nodos normales, orden de árbol] < [capas over, orden de // árbol], así que basta un candidato aparte y preferirlo al final. Las capas // over anidadas las pinta la externa (`paint_over_layers` salta a // `subtree_end`), o sea que no agregan un nivel de z: alcanza con saber si // estamos DENTRO de alguna. let mut hit_over: Option = None; let mut over_end: usize = 0; let mut clip_stack: Vec = Vec::new(); // Espejo del stack de transformaciones de `paint`: `cur_xf` es el // producto acumulado de los `transform` de los ancestros activos // (local → pantalla). Vacío ⇒ identidad ⇒ camino directo sin invertir // (cero costo para la abrumadora mayoría de árboles sin transform). let mut xf_stack: Vec<(usize, Affine)> = Vec::new(); let mut cur_xf = Affine::IDENTITY; let mut idx = 0; while idx < mounted.nodes.len() { while let Some(&end) = clip_stack.last() { if idx >= end { clip_stack.pop(); } else { break; } } while let Some(&(end, prev)) = xf_stack.last() { if idx >= end { cur_xf = prev; xf_stack.pop(); } else { break; } } let node = &mounted.nodes[idx]; if node.over_layer && idx >= over_end { over_end = node.subtree_end; } let Some(r) = computed.get(node.id) else { idx += 1; continue; }; // Componé el transform de este nodo igual que `paint`, ANTES de // resolver el punto local (su propio rect ya cae en el espacio // transformado). if let Some(centered) = resolve_node_transform(node.transform, node.transform_rel, node.transform_origin, r) { xf_stack.push((node.subtree_end, cur_xf)); cur_xf *= centered; } // Punto en el espacio local del nodo. Sin transform activo, es el // punto de pantalla tal cual. Con transform, se invierte el afín; // si es singular (no invertible) el subárbol es inalcanzable. let (lx, ly) = if xf_stack.is_empty() { (x as f64, y as f64) } else if cur_xf.determinant().abs() < 1e-9 { idx = node.subtree_end; continue; } else { let p = cur_xf.inverse() * Point::new(x as f64, y as f64); (p.x, p.y) }; let inside = lx >= r.x as f64 && lx < (r.x + r.w) as f64 && ly >= r.y as f64 && ly < (r.y + r.h) as f64; if node.clip { if !inside { idx = node.subtree_end; continue; } clip_stack.push(node.subtree_end); } if inside && pred(node) { if idx < over_end { hit_over = Some(idx); } else { hit = Some(idx); } } idx += 1; } hit_over.or(hit) } /// Hit-test específico para clicks (incluye nodos draggables). pub fn hit_test_click( mounted: &Mounted, computed: &ComputedLayout, x: f32, y: f32, ) -> Option { hit_test_pred(mounted, computed, x, y, |n| { n.on_click.is_some() || n.on_click_at.is_some() || n.on_click_screen.is_some() || n.drag.is_some() || n.drag_at.is_some() || n.drag_velocity.is_some() }) } /// Hit-test específico para right-click. Sólo considera nodos que /// declararon `on_right_click` o `on_right_click_at` — un right-click /// sobre un nodo sin handler no hace nada (no se "filtra" al click /// izquierdo). pub fn hit_test_right_click( mounted: &Mounted, computed: &ComputedLayout, x: f32, y: f32, ) -> Option { hit_test_pred(mounted, computed, x, y, |n| { n.on_right_click.is_some() || n.on_right_click_at.is_some() || n.on_right_click_screen.is_some() }) } /// Hit-test específico para middle-click. Mismo modelo que right-click: /// sólo nodos que declararon `on_middle_click` reaccionan. pub fn hit_test_middle_click( mounted: &Mounted, computed: &ComputedLayout, x: f32, y: f32, ) -> Option { hit_test_pred(mounted, computed, x, y, |n| n.on_middle_click.is_some()) } /// Hit-test específico para hover (nodos con `hover_fill`). pub fn hit_test_hover( mounted: &Mounted, computed: &ComputedLayout, x: f32, y: f32, ) -> Option { hit_test_pred(mounted, computed, x, y, |n| n.hover_fill.is_some()) } /// Hit-test específico para **tooltips** (nodos con `tooltip`). Separado del /// hover: un nodo puede declarar tooltip sin `hover_fill` (p. ej. los iconos /// fugaces de pata, que pintan por `paint_with` y no quieren tinte de hover) — /// con el predicado de hover su tooltip jamás se encontraba. pub fn hit_test_tooltip( mounted: &Mounted, computed: &ComputedLayout, x: f32, y: f32, ) -> Option { hit_test_pred(mounted, computed, x, y, |n| n.tooltip.is_some()) } /// Hit-test para movimiento posicional del cursor (nodos con /// `on_pointer_move_at`). El runtime lo invoca en cada `CursorMoved` para /// reportar la posición local al nodo más al frente que lo declare. pub fn hit_test_pointer_move( mounted: &Mounted, computed: &ComputedLayout, x: f32, y: f32, ) -> Option { hit_test_pred(mounted, computed, x, y, |n| n.on_pointer_move_at.is_some()) } /// Hit-test específico para la **forma del cursor**: devuelve el [`Cursor`] /// del nodo más al frente bajo el punto que declare uno. Como un hijo sin /// cursor no matchea el predicado, el cursor "cae" al ancestro más cercano que /// lo declare — herencia estilo CSS sin recorrer el árbol a mano. `None` = /// ningún nodo bajo el punto declara cursor (el runtime usa el default de la /// ventana). Lo invoca `llimphi-ui` en la transición de hover. pub fn hit_test_cursor( mounted: &Mounted, computed: &ComputedLayout, x: f32, y: f32, ) -> Option { hit_test_pred(mounted, computed, x, y, |n| n.cursor.is_some()) .and_then(|i| mounted.nodes[i].cursor) } /// Hit-test específico para drop targets (nodos con `on_drop`). Usado /// durante un drag activo para resaltar el destino y para invocar el /// handler al soltar. pub fn hit_test_drop( mounted: &Mounted, computed: &ComputedLayout, x: f32, y: f32, ) -> Option { hit_test_pred(mounted, computed, x, y, |n| n.on_drop.is_some()) } /// Hit-test específico para áreas de scroll (nodos con `on_scroll`). El /// runtime lo usa al recibir la rueda: el nodo más al frente bajo el /// cursor con handler de scroll consume el evento antes del `on_wheel` /// global. pub fn hit_test_scroll( mounted: &Mounted, computed: &ComputedLayout, x: f32, y: f32, ) -> Option { hit_test_pred(mounted, computed, x, y, |n| n.on_scroll.is_some()) } /// Cadena de **scroll anidado**: devuelve todos los nodos con `on_scroll` /// que contienen el punto, ordenados **front→back** (el primero es el más /// al frente, igual que [`hit_test_scroll`]; los siguientes son sus /// ancestros scrollables). El runtime itera la cadena al recibir la rueda /// y se queda con el primer handler que devuelva `Some`: si un scroll /// interno está en el extremo del eje y devuelve `None`, el evento "pasa" /// al ancestro scrollable más cercano (lista dentro de panel, etc.). /// Recorrido idéntico al de [`hit_test_pred`] pero acumulando todos los /// hits en vez de pisar. pub fn hit_test_scroll_chain( mounted: &Mounted, computed: &ComputedLayout, x: f32, y: f32, ) -> Vec { let mut chain: Vec = Vec::new(); let mut clip_stack: Vec = Vec::new(); let mut xf_stack: Vec<(usize, Affine)> = Vec::new(); let mut cur_xf = Affine::IDENTITY; let mut idx = 0; while idx < mounted.nodes.len() { while let Some(&end) = clip_stack.last() { if idx >= end { clip_stack.pop(); } else { break; } } while let Some(&(end, prev)) = xf_stack.last() { if idx >= end { cur_xf = prev; xf_stack.pop(); } else { break; } } let node = &mounted.nodes[idx]; let Some(r) = computed.get(node.id) else { idx += 1; continue; }; if let Some(centered) = resolve_node_transform(node.transform, node.transform_rel, node.transform_origin, r) { xf_stack.push((node.subtree_end, cur_xf)); cur_xf *= centered; } let (lx, ly) = if xf_stack.is_empty() { (x as f64, y as f64) } else if cur_xf.determinant().abs() < 1e-9 { idx = node.subtree_end; continue; } else { let p = cur_xf.inverse() * Point::new(x as f64, y as f64); (p.x, p.y) }; let inside = lx >= r.x as f64 && lx < (r.x + r.w) as f64 && ly >= r.y as f64 && ly < (r.y + r.h) as f64; if node.clip { if !inside { idx = node.subtree_end; continue; } clip_stack.push(node.subtree_end); } if inside && (node.on_scroll.is_some() || node.on_scroll_take.is_some()) { chain.push(idx); } idx += 1; } // El recorrido es pre-orden, así que los ancestros aparecen primero y // los hijos después. Para front→back necesitamos el orden inverso. chain.reverse(); chain } /// Reparte un evento de rueda a lo largo de la cadena de scroll (front→back, /// de [`hit_test_scroll_chain`]). Para cada nodo: /// /// - con [`on_scroll_take`](crate::View::on_scroll_take): consume lo que /// puede y el **sobrante** sigue al próximo de la cadena — un mismo evento /// puede mover al hijo hasta su tope Y al ancestro con el resto; /// - con `on_scroll` clásico: semántica histórica — `Some` consume TODO lo /// restante y corta; `None` deja pasar el delta completo. /// /// Devuelve los `Msg` en orden (el runtime los aplica en secuencia sobre el /// `update`). Vacío = nadie consumió (el evento cae al `on_wheel` global). /// Pura respecto del árbol: no muta nada, testeable headless. pub fn consume_scroll_chain( mounted: &Mounted, chain: &[usize], dx: f32, dy: f32, ) -> Vec { const EPS: f32 = 1e-4; let mut msgs = Vec::new(); let (mut rx, mut ry) = (dx, dy); for &idx in chain { if rx.abs() <= EPS && ry.abs() <= EPS { break; } let node = &mounted.nodes[idx]; if let Some(take) = &node.on_scroll_take { let (msg, nx, ny) = take(rx, ry); if let Some(m) = msg { msgs.push(m); } // Defensa: un handler no puede "crear" delta ni invertir su // signo — el sobrante se acota al delta que recibió. rx = clamp_rest(nx, rx); ry = clamp_rest(ny, ry); } else if let Some(h) = &node.on_scroll { if let Some(m) = h(rx, ry) { msgs.push(m); break; } } } msgs } /// Acota el sobrante declarado por un handler al rango `[0, delta]` (con el /// signo del delta): ni negativo del otro lado, ni mayor que lo recibido. pub(crate) fn clamp_rest(rest: f32, delta: f32) -> f32 { if delta >= 0.0 { rest.clamp(0.0, delta) } else { rest.clamp(delta, 0.0) } } /// Hit-test específico para gestos de **escala** (pinch-to-zoom): el nodo más /// al frente bajo el punto que declaró un `on_scale`. Como un hijo sin handler /// no matchea el predicado, el gesto "cae" al ancestro más cercano que lo /// declare (un canvas grande zoomeable con widgets encima que no zoomean). El /// runtime lo invoca al recibir Ctrl+rueda o un pinch de trackpad. `None` = /// ningún nodo zoomeable bajo el cursor (el evento cae al scroll/`on_wheel`). pub fn hit_test_scale( mounted: &Mounted, computed: &ComputedLayout, x: f32, y: f32, ) -> Option { hit_test_pred(mounted, computed, x, y, |n| n.on_scale.is_some()) } /// Hit-test específico para gestos de **rotación** (trackpad): el nodo más al /// frente bajo el punto que declaró un `on_rotate`. Análogo a /// [`hit_test_scale`]; el runtime lo invoca al recibir un `RotationGesture`. /// `None` = ningún nodo rotable bajo el cursor. pub fn hit_test_rotate( mounted: &Mounted, computed: &ComputedLayout, x: f32, y: f32, ) -> Option { hit_test_pred(mounted, computed, x, y, |n| n.on_rotate.is_some()) } /// Hit-test para **doble-tap**: el nodo más al frente bajo el punto que /// declaró `on_double_tap`/`on_double_tap_at`. El runtime lo usa al detectar /// dos presses rápidos y cercanos. pub fn hit_test_double_tap( mounted: &Mounted, computed: &ComputedLayout, x: f32, y: f32, ) -> Option { hit_test_pred(mounted, computed, x, y, |n| { n.on_double_tap.is_some() || n.on_double_tap_at.is_some() }) } /// Hit-test para **long-press**: el nodo más al frente bajo el punto que /// declaró `on_long_press`/`on_long_press_at`. El runtime lo usa al armar el /// gesto en el press (que vence por tiempo si no hay movimiento ni release). pub fn hit_test_long_press( mounted: &Mounted, computed: &ComputedLayout, x: f32, y: f32, ) -> Option { hit_test_pred(mounted, computed, x, y, |n| { n.on_long_press.is_some() || n.on_long_press_at.is_some() }) } /// Hit-test para **ripple**: el nodo más al frente bajo el punto que declaró /// un [`Ripple`] (vía [`View::ripple`]). El runtime lo usa en el press para /// disparar la salpicadura. Aditivo — no compite con click/drag. pub fn hit_test_ripple( mounted: &Mounted, computed: &ComputedLayout, x: f32, y: f32, ) -> Option { hit_test_pred(mounted, computed, x, y, |n| n.ripple.is_some()) } /// Hit-test para foco: el id `focusable` del nodo más al frente bajo el /// cursor (click-to-focus). `None` si no se clickeó nada enfocable. pub fn hit_test_focusable( mounted: &Mounted, computed: &ComputedLayout, x: f32, y: f32, ) -> Option { hit_test_pred(mounted, computed, x, y, |n| n.focusable.is_some()) .and_then(|i| mounted.nodes[i].focusable) } /// Hit-test para **selección de texto**: el índice del nodo de texto /// seleccionable (`text_select_key`) más al frente bajo el cursor. El runtime /// lo usa para arrancar/extender una selección; devuelve el índice (no la key) /// para que el caller acceda al `text` + rect del nodo. `None` si no hay texto /// seleccionable bajo el punto. pub fn hit_test_selectable( mounted: &Mounted, computed: &ComputedLayout, x: f32, y: f32, ) -> Option { hit_test_pred(mounted, computed, x, y, |n| n.text_select_key.is_some()) } /// Ids enfocables en orden de Tab (pre-orden del árbol = orden de /// inserción de `Mounted::nodes`). Sólo nodos con rect computado /// (presentes en el layout). Es el orden DOM-like de tabulación. pub fn focus_order(mounted: &Mounted, computed: &ComputedLayout) -> Vec { mounted .nodes .iter() .filter_map(|n| { n.focusable .filter(|_| computed.get(n.id).is_some()) }) .collect() } /// Próximo id de foco al pulsar Tab (o Shift+Tab si `reverse`), dado el /// `order` (de [`focus_order`]) y el `current`. Envuelve en los extremos. /// Si no hay enfocables devuelve `None`; si `current` ya no existe en el /// orden, arranca por el primero (Tab) o el último (Shift+Tab). pub fn next_focus(order: &[u64], current: Option, reverse: bool) -> Option { if order.is_empty() { return None; } let n = order.len(); let pos = current.and_then(|c| order.iter().position(|&id| id == c)); let next_idx = match pos { Some(i) => { if reverse { (i + n - 1) % n } else { (i + 1) % n } } None => { if reverse { n - 1 } else { 0 } } }; Some(order[next_idx]) }