Skip to main content

fission_core/
hit_test.rs

1use crate::env::ScrollStateMap;
2use fission_diagnostics::prelude as diag;
3use fission_ir::{CoreIR, LayoutOp, NodeId, Op, PaintOp};
4use fission_layout::{LayoutPoint, LayoutRect, LayoutSnapshot, LayoutUnit};
5use glam::{Mat4, Vec4};
6
7#[derive(Debug, Clone, Copy, PartialEq, Eq)]
8pub enum FocusDirection {
9    Up,
10    Down,
11    Left,
12    Right,
13}
14
15pub fn hit_test(
16    ir: &CoreIR,
17    layout: &LayoutSnapshot,
18    scroll_map: &ScrollStateMap,
19    point: LayoutPoint,
20) -> Option<NodeId> {
21    hit_test_internal(ir, layout, Some(scroll_map), point)
22}
23
24pub fn hit_test_with_scroll(
25    ir: &CoreIR,
26    layout: &LayoutSnapshot,
27    scroll_map: &ScrollStateMap,
28    point: LayoutPoint,
29) -> Option<NodeId> {
30    hit_test_internal(ir, layout, Some(scroll_map), point)
31}
32
33fn hit_test_internal(
34    ir: &CoreIR,
35    layout: &LayoutSnapshot,
36    scroll_map: Option<&ScrollStateMap>,
37    point: LayoutPoint,
38) -> Option<NodeId> {
39    let result = ir
40        .root
41        .and_then(|root| hit_test_recursive(root, ir, layout, scroll_map, point));
42
43    if let Some(id) = result {
44        diag::emit(
45            diag::DiagCategory::Input,
46            diag::DiagLevel::Debug,
47            diag::DiagEventKind::InputEvent {
48                kind: "hit_test_result".into(),
49                target: Some(id.as_u128()),
50                position: Some((point.x, point.y)),
51            },
52        );
53    }
54    result
55}
56
57fn hit_test_recursive(
58    node_id: NodeId,
59    ir: &CoreIR,
60    layout: &LayoutSnapshot,
61    scroll_map: Option<&ScrollStateMap>,
62    point: LayoutPoint,
63) -> Option<NodeId> {
64    let node = ir.nodes.get(&node_id)?;
65    let geom = layout.get_node_geometry(node_id)?;
66
67    let is_clip_container = matches!(
68        node.op,
69        Op::Layout(LayoutOp::Clip { .. }) | Op::Layout(LayoutOp::Scroll { .. })
70    );
71
72    if is_clip_container && !geom.rect.contains(point) {
73        return None;
74    }
75
76    let mut child_point = point;
77
78    if let (Some(map), Op::Layout(LayoutOp::Scroll { direction, .. })) = (scroll_map, &node.op) {
79        let offset = map.get_offset(node_id);
80        match direction {
81            fission_ir::FlexDirection::Column => {
82                child_point.y += offset;
83            }
84            fission_ir::FlexDirection::Row => {
85                child_point.x += offset;
86            }
87        }
88    }
89
90    if let Op::Layout(LayoutOp::Transform { transform }) = &node.op {
91        let mat = Mat4::from_cols_array(transform);
92        let inv = mat.inverse();
93        let local_x = point.x - geom.rect.origin.x;
94        let local_y = point.y - geom.rect.origin.y;
95        let p = Vec4::new(local_x, local_y, 0.0, 1.0);
96        let transformed = inv * p;
97        child_point = LayoutPoint::new(
98            transformed.x + geom.rect.origin.x,
99            transformed.y + geom.rect.origin.y,
100        );
101    }
102
103    for child_id in node.children.iter().rev() {
104        if let Some(hit) = hit_test_recursive(*child_id, ir, layout, scroll_map, child_point) {
105            return Some(hit);
106        }
107    }
108
109    let mut current_is_hit = false;
110    if geom.rect.contains(point) {
111        match &node.op {
112            Op::Layout(LayoutOp::Scroll { .. }) | Op::Layout(LayoutOp::Embed { .. }) => {
113                current_is_hit = true;
114            }
115            Op::Semantics(semantics) => {
116                if !semantics.actions.entries.is_empty()
117                    || semantics.focusable
118                    || semantics.draggable
119                    || semantics.scrollable_x
120                    || semantics.scrollable_y
121                {
122                    current_is_hit = true;
123                }
124            }
125            _ => {}
126        }
127    }
128
129    if current_is_hit { Some(node_id) } else { None }
130}
131
132fn is_point_in_rounded_rect(p: LayoutPoint, r: LayoutRect, radius: LayoutUnit) -> bool {
133    let local_p_x = p.x - r.x();
134    let local_p_y = p.y - r.y();
135    let (width, height) = (r.width(), r.height());
136
137    if radius <= 0.0 {
138        return true;
139    }
140
141    let clamped_radius = radius.min(width / 2.0).min(height / 2.0);
142
143    if local_p_x < clamped_radius && local_p_y < clamped_radius {
144        return (local_p_x - clamped_radius).powi(2) + (local_p_y - clamped_radius).powi(2)
145            <= clamped_radius.powi(2);
146    }
147    if local_p_x > width - clamped_radius && local_p_y < clamped_radius {
148        return (local_p_x - (width - clamped_radius)).powi(2) + (local_p_y - clamped_radius).powi(2)
149            <= clamped_radius.powi(2);
150    }
151    if local_p_x < clamped_radius && local_p_y > height - clamped_radius {
152        return (local_p_x - clamped_radius).powi(2)
153            + (local_p_y - (height - clamped_radius)).powi(2)
154            <= clamped_radius.powi(2);
155    }
156    if local_p_x > width - clamped_radius && local_p_y > height - clamped_radius {
157        return (local_p_x - (width - clamped_radius)).powi(2)
158            + (local_p_y - (height - clamped_radius)).powi(2)
159            <= clamped_radius.powi(2);
160    }
161
162    true
163}
164
165pub fn find_next_focus_node(ir: &CoreIR, current: Option<NodeId>, reverse: bool) -> Option<NodeId> {
166    // Identify current scope if focused node is provided
167    let (current_scope_id, current_is_barrier) = if let Some(id) = current {
168        let scope = find_parent_scope(id, ir);
169        let mut is_barrier = false;
170        if let Some(sid) = scope {
171            if let Some(node) = ir.nodes.get(&sid) {
172                if let Op::Semantics(s) = &node.op {
173                    is_barrier = s.is_focus_barrier;
174                }
175            }
176        }
177        (scope, is_barrier)
178    } else {
179        (None, false)
180    };
181
182    let nodes_in_scope = if current_is_barrier {
183        let scope_id = current_scope_id.unwrap();
184        let mut list = Vec::new();
185        // Start recursion on CHILDREN of the barrier root to avoid skipping it
186        if let Some(node) = ir.nodes.get(&scope_id) {
187            for child in &node.children {
188                collect_focusable_nodes(*child, ir, &mut list, true, 0);
189            }
190        }
191        sort_focusable_nodes(ir, list)
192    } else {
193        get_all_focusable_nodes(ir)
194    };
195
196    if nodes_in_scope.is_empty() {
197        return None;
198    }
199
200    let idx = if let Some(curr_id) = current {
201        nodes_in_scope.iter().position(|id| *id == curr_id)
202    } else {
203        None
204    };
205
206    match idx {
207        Some(i) => {
208            if reverse {
209                if i == 0 {
210                    Some(nodes_in_scope[nodes_in_scope.len() - 1])
211                } else {
212                    Some(nodes_in_scope[i - 1])
213                }
214            } else if i == nodes_in_scope.len() - 1 {
215                Some(nodes_in_scope[0])
216            } else {
217                Some(nodes_in_scope[i + 1])
218            }
219        }
220        None => {
221            if reverse {
222                Some(nodes_in_scope[nodes_in_scope.len() - 1])
223            } else {
224                Some(nodes_in_scope[0])
225            }
226        }
227    }
228}
229
230pub fn get_all_focusable_nodes(ir: &CoreIR) -> Vec<NodeId> {
231    let mut list = Vec::new();
232    if let Some(root) = ir.root {
233        collect_focusable_nodes(root, ir, &mut list, false, 0);
234    }
235    sort_focusable_nodes(ir, list)
236}
237
238fn sort_focusable_nodes(ir: &CoreIR, mut list: Vec<(NodeId, usize)>) -> Vec<NodeId> {
239    list.sort_by(|(id_a, order_a), (id_b, order_b)| {
240        let idx_a = ir.nodes.get(id_a).and_then(|n| if let Op::Semantics(s) = &n.op { s.focus_index } else { None });
241        let idx_b = ir.nodes.get(id_b).and_then(|n| if let Op::Semantics(s) = &n.op { s.focus_index } else { None });
242
243        match (idx_a, idx_b) {
244            (Some(a), Some(b)) => a.cmp(&b).then(order_a.cmp(order_b)),
245            (Some(_), None) => std::cmp::Ordering::Less,
246            (None, Some(_)) => std::cmp::Ordering::Greater,
247            (None, None) => order_a.cmp(order_b),
248        }
249    });
250    list.into_iter().map(|(id, _)| id).collect()
251}
252
253fn collect_focusable_nodes(node_id: NodeId, ir: &CoreIR, list: &mut Vec<(NodeId, usize)>, stop_at_barriers: bool, mut order: usize) {
254    if let Some(node) = ir.nodes.get(&node_id) {
255        let mut is_barrier = false;
256        if let Op::Semantics(s) = &node.op {
257            if s.focusable && !s.disabled {
258                list.push((node_id, order));
259                order += 1;
260            }
261            is_barrier = s.is_focus_barrier;
262        }
263
264        if stop_at_barriers && is_barrier {
265             return; 
266        }
267
268        let mut children = node.children.clone();
269        // Internal sort within branches still useful for tree-order
270        children.sort_by_key(|cid| {
271            ir.nodes.get(cid).and_then(|n| {
272                if let Op::Semantics(s) = &n.op {
273                    s.focus_index
274                } else {
275                    None
276                }
277            }).unwrap_or(i32::MAX)
278        });
279
280        for child in children {
281            collect_focusable_nodes(child, ir, list, stop_at_barriers, order);
282            order = list.last().map(|(_, o)| *o + 1).unwrap_or(order);
283        }
284    }
285}
286
287fn find_parent_scope(node_id: NodeId, ir: &CoreIR) -> Option<NodeId> {
288    let mut curr = ir.nodes.get(&node_id)?.parent;
289    while let Some(pid) = curr {
290        if let Some(node) = ir.nodes.get(&pid) {
291            if let Op::Semantics(s) = &node.op {
292                if s.is_focus_scope {
293                    return Some(pid);
294                }
295            }
296            curr = node.parent;
297        } else {
298            break;
299        }
300    }
301    None
302}
303
304pub fn find_neighbor_focus_node(
305    ir: &CoreIR,
306    layout: &LayoutSnapshot,
307    current: NodeId,
308    direction: FocusDirection,
309) -> Option<NodeId> {
310    let current_rect = layout.get_node_rect(current)?;
311    let focusable_nodes = get_all_focusable_nodes(ir);
312
313    let mut best_candidate = None;
314    let mut best_dist = f32::INFINITY;
315
316    let (cx, cy) = (
317        current_rect.x() + current_rect.width() / 2.0,
318        current_rect.y() + current_rect.height() / 2.0,
319    );
320
321    for node_id in focusable_nodes {
322        if node_id == current {
323            continue;
324        }
325        let rect = match layout.get_node_rect(node_id) {
326            Some(r) => r,
327            None => continue,
328        };
329
330        let (nx, ny) = (rect.x() + rect.width() / 2.0, rect.y() + rect.height() / 2.0);
331
332        let is_in_dir = match direction {
333            FocusDirection::Up => ny < cy && (nx - cx).abs() < (ny - cy).abs(),
334            FocusDirection::Down => ny > cy && (nx - cx).abs() < (ny - cy).abs(),
335            FocusDirection::Left => nx < cx && (ny - cy).abs() < (nx - cx).abs(),
336            FocusDirection::Right => nx > cx && (ny - cy).abs() < (nx - cx).abs(),
337        };
338
339        if is_in_dir {
340            let dist = (nx - cx).powi(2) + (ny - cy).powi(2);
341            if dist < best_dist {
342                best_dist = dist;
343                best_candidate = Some(node_id);
344            }
345        }
346    }
347
348    best_candidate
349}