1use crate::env::ScrollStateMap;
2use crate::ui::custom_render::downcast_render_object;
3use fission_diagnostics::prelude as diag;
4use fission_ir::{CoreIR, LayoutOp, Op, PaintOp, WidgetId};
5use fission_layout::{LayoutPoint, LayoutSnapshot};
6use glam::{Mat4, Vec4};
7
8#[derive(Debug, Clone, Copy, PartialEq, Eq)]
9pub enum FocusDirection {
10 Up,
11 Down,
12 Left,
13 Right,
14}
15
16pub fn hit_test(
17 ir: &CoreIR,
18 layout: &LayoutSnapshot,
19 scroll_map: &ScrollStateMap,
20 point: LayoutPoint,
21) -> Option<WidgetId> {
22 hit_test_internal(ir, layout, Some(scroll_map), point)
23}
24
25pub fn hit_test_with_scroll(
26 ir: &CoreIR,
27 layout: &LayoutSnapshot,
28 scroll_map: &ScrollStateMap,
29 point: LayoutPoint,
30) -> Option<WidgetId> {
31 hit_test_internal(ir, layout, Some(scroll_map), point)
32}
33
34fn hit_test_internal(
35 ir: &CoreIR,
36 layout: &LayoutSnapshot,
37 scroll_map: Option<&ScrollStateMap>,
38 point: LayoutPoint,
39) -> Option<WidgetId> {
40 let result = ir
41 .root
42 .and_then(|root| hit_test_recursive(root, ir, layout, scroll_map, point));
43
44 if let Some(id) = result {
45 diag::emit(
46 diag::DiagCategory::Input,
47 diag::DiagLevel::Debug,
48 diag::DiagEventKind::InputEvent {
49 kind: "hit_test_result".into(),
50 target: Some(id.as_u128()),
51 position: Some((point.x, point.y)),
52 },
53 );
54 }
55 result
56}
57
58fn hit_test_recursive(
59 node_id: WidgetId,
60 ir: &CoreIR,
61 layout: &LayoutSnapshot,
62 scroll_map: Option<&ScrollStateMap>,
63 point: LayoutPoint,
64) -> Option<WidgetId> {
65 let node = ir.nodes.get(&node_id)?;
66 let geom = layout.get_node_geometry(node_id)?;
67
68 let is_clip_container = matches!(
69 node.op,
70 Op::Layout(LayoutOp::Clip { .. }) | Op::Layout(LayoutOp::Scroll { .. })
71 );
72
73 if is_clip_container && !geom.rect.contains(point) {
74 return None;
75 }
76
77 let mut child_point = point;
78
79 if let (Some(map), Op::Layout(LayoutOp::Scroll { direction, .. })) = (scroll_map, &node.op) {
80 let offset = map.get_offset(node_id);
81 match direction {
82 fission_ir::FlexDirection::Column => {
83 child_point.y += offset;
84 }
85 fission_ir::FlexDirection::Row => {
86 child_point.x += offset;
87 }
88 }
89 }
90
91 if let Op::Layout(LayoutOp::Transform { transform }) = &node.op {
92 let mat = Mat4::from_cols_array(transform);
93 let inv = mat.inverse();
94 let local_x = point.x - geom.rect.origin.x;
95 let local_y = point.y - geom.rect.origin.y;
96 let p = Vec4::new(local_x, local_y, 0.0, 1.0);
97 let transformed = inv * p;
98 child_point = LayoutPoint::new(
99 transformed.x + geom.rect.origin.x,
100 transformed.y + geom.rect.origin.y,
101 );
102 }
103
104 for child_id in node.children.iter().rev() {
105 if let Some(hit) = hit_test_recursive(*child_id, ir, layout, scroll_map, child_point) {
106 return Some(hit);
107 }
108 }
109
110 if geom.rect.contains(point) {
114 if let Some(any_ro) = ir.custom_render_objects.get(&node_id) {
115 if let Some(render_obj) = downcast_render_object(any_ro) {
116 let local_point =
117 LayoutPoint::new(point.x - geom.rect.origin.x, point.y - geom.rect.origin.y);
118 let result = render_obj.hit_test(local_point, geom.rect);
119 if result.hit {
120 return Some(node_id);
121 }
122 }
123 }
124 }
125
126 if geom.rect.contains(point) && paint_op_blocks_hit_testing(&node.op) {
127 return Some(node_id);
128 }
129
130 let mut current_is_hit = false;
131 if geom.rect.contains(point) {
132 match &node.op {
133 Op::Layout(LayoutOp::Scroll { .. }) | Op::Layout(LayoutOp::Embed { .. }) => {
134 current_is_hit = true;
135 }
136 Op::Semantics(semantics) => {
137 if !semantics.actions.entries.is_empty()
138 || semantics.focusable
139 || semantics.draggable
140 || semantics.scrollable_x
141 || semantics.scrollable_y
142 {
143 current_is_hit = true;
144 }
145 }
146 _ => {}
147 }
148 }
149
150 if current_is_hit {
151 Some(node_id)
152 } else {
153 None
154 }
155}
156
157fn paint_op_blocks_hit_testing(op: &Op) -> bool {
158 match op {
159 Op::Paint(PaintOp::DrawRect {
160 fill,
161 stroke,
162 shadow,
163 ..
164 }) => fill.is_some() || stroke.is_some() || shadow.is_some(),
165 Op::Paint(PaintOp::DrawText { text, .. }) => !text.is_empty(),
166 Op::Paint(PaintOp::DrawRichText { runs, .. }) => {
167 runs.iter().any(|run| !run.text.is_empty())
168 }
169 Op::Paint(PaintOp::DrawImage { .. }) => true,
170 Op::Paint(PaintOp::DrawPath { fill, stroke, .. })
171 | Op::Paint(PaintOp::DrawSvg { fill, stroke, .. }) => fill.is_some() || stroke.is_some(),
172 _ => false,
173 }
174}
175
176pub fn find_next_focus_node(
177 ir: &CoreIR,
178 current: Option<WidgetId>,
179 reverse: bool,
180) -> Option<WidgetId> {
181 let nodes_in_scope = if let Some(barrier_id) = topmost_focus_barrier(ir) {
182 focusable_nodes_in_scope(ir, barrier_id)
183 } else if let Some(scope_id) = current.and_then(|id| find_containing_focus_scope(id, ir)) {
184 if is_focus_barrier(ir, scope_id) {
185 focusable_nodes_in_scope(ir, scope_id)
186 } else {
187 get_all_focusable_nodes(ir)
188 }
189 } else {
190 get_all_focusable_nodes(ir)
191 };
192
193 if nodes_in_scope.is_empty() {
194 return None;
195 }
196
197 let idx = if let Some(curr_id) = current {
198 nodes_in_scope.iter().position(|id| *id == curr_id)
199 } else {
200 None
201 };
202
203 match idx {
204 Some(i) => {
205 if reverse {
206 if i == 0 {
207 Some(nodes_in_scope[nodes_in_scope.len() - 1])
208 } else {
209 Some(nodes_in_scope[i - 1])
210 }
211 } else if i == nodes_in_scope.len() - 1 {
212 Some(nodes_in_scope[0])
213 } else {
214 Some(nodes_in_scope[i + 1])
215 }
216 }
217 None => {
218 if reverse {
219 Some(nodes_in_scope[nodes_in_scope.len() - 1])
220 } else {
221 Some(nodes_in_scope[0])
222 }
223 }
224 }
225}
226
227pub fn get_all_focusable_nodes(ir: &CoreIR) -> Vec<WidgetId> {
228 let mut list = Vec::new();
229 if let Some(root) = ir.root {
230 collect_focusable_nodes(root, ir, &mut list, false, 0);
231 }
232 sort_focusable_nodes(ir, list)
233}
234
235pub fn focus_barriers_in_tree_order(ir: &CoreIR) -> Vec<WidgetId> {
238 let mut barriers = Vec::new();
239 if let Some(root) = ir.root {
240 collect_focus_barriers(root, ir, &mut barriers);
241 }
242 barriers
243}
244
245pub fn topmost_focus_barrier(ir: &CoreIR) -> Option<WidgetId> {
247 focus_barriers_in_tree_order(ir).last().copied()
248}
249
250pub fn focusable_nodes_in_scope(ir: &CoreIR, scope_id: WidgetId) -> Vec<WidgetId> {
252 let mut list = Vec::new();
253 if let Some(scope) = ir.nodes.get(&scope_id) {
254 let mut order = 0;
255 for child in &scope.children {
256 collect_focusable_nodes(*child, ir, &mut list, false, order);
257 order = list.last().map(|(_, index)| *index + 1).unwrap_or(order);
258 }
259 }
260 sort_focusable_nodes(ir, list)
261}
262
263pub fn preferred_focus_node_in_scope(ir: &CoreIR, scope_id: WidgetId) -> Option<WidgetId> {
265 let nodes = focusable_nodes_in_scope(ir, scope_id);
266 nodes
267 .iter()
268 .copied()
269 .find(|id| semantics(ir, *id).is_some_and(|value| value.autofocus))
270 .or_else(|| nodes.first().copied())
271}
272
273pub fn is_enabled_focus_node(ir: &CoreIR, node_id: WidgetId) -> bool {
275 semantics(ir, node_id).is_some_and(|value| value.focusable && !value.disabled)
276}
277
278pub fn is_descendant_or_self(ir: &CoreIR, node_id: WidgetId, ancestor_id: WidgetId) -> bool {
280 let mut current = Some(node_id);
281 while let Some(id) = current {
282 if id == ancestor_id {
283 return true;
284 }
285 current = ir.nodes.get(&id).and_then(|node| node.parent);
286 }
287 false
288}
289
290fn sort_focusable_nodes(ir: &CoreIR, mut list: Vec<(WidgetId, usize)>) -> Vec<WidgetId> {
291 list.sort_by(|(id_a, order_a), (id_b, order_b)| {
292 let idx_a = ir.nodes.get(id_a).and_then(|n| {
293 if let Op::Semantics(s) = &n.op {
294 s.focus_index
295 } else {
296 None
297 }
298 });
299 let idx_b = ir.nodes.get(id_b).and_then(|n| {
300 if let Op::Semantics(s) = &n.op {
301 s.focus_index
302 } else {
303 None
304 }
305 });
306
307 match (idx_a, idx_b) {
308 (Some(a), Some(b)) => a.cmp(&b).then(order_a.cmp(order_b)),
309 (Some(_), None) => std::cmp::Ordering::Less,
310 (None, Some(_)) => std::cmp::Ordering::Greater,
311 (None, None) => order_a.cmp(order_b),
312 }
313 });
314 list.into_iter().map(|(id, _)| id).collect()
315}
316
317fn collect_focusable_nodes(
318 node_id: WidgetId,
319 ir: &CoreIR,
320 list: &mut Vec<(WidgetId, usize)>,
321 stop_at_barriers: bool,
322 mut order: usize,
323) {
324 if let Some(node) = ir.nodes.get(&node_id) {
325 let mut is_barrier = false;
326 if let Op::Semantics(s) = &node.op {
327 if s.focusable && !s.disabled {
328 list.push((node_id, order));
329 order += 1;
330 }
331 is_barrier = s.is_focus_barrier;
332 }
333
334 if stop_at_barriers && is_barrier {
335 return;
336 }
337
338 let mut children = node.children.clone();
339 children.sort_by_key(|cid| {
341 ir.nodes
342 .get(cid)
343 .and_then(|n| {
344 if let Op::Semantics(s) = &n.op {
345 s.focus_index
346 } else {
347 None
348 }
349 })
350 .unwrap_or(i32::MAX)
351 });
352
353 for child in children {
354 collect_focusable_nodes(child, ir, list, stop_at_barriers, order);
355 order = list.last().map(|(_, o)| *o + 1).unwrap_or(order);
356 }
357 }
358}
359
360fn collect_focus_barriers(node_id: WidgetId, ir: &CoreIR, barriers: &mut Vec<WidgetId>) {
361 let Some(node) = ir.nodes.get(&node_id) else {
362 return;
363 };
364 if matches!(&node.op, Op::Semantics(value) if value.is_focus_scope && value.is_focus_barrier) {
365 barriers.push(node_id);
366 }
367 for child in &node.children {
368 collect_focus_barriers(*child, ir, barriers);
369 }
370}
371
372fn find_containing_focus_scope(node_id: WidgetId, ir: &CoreIR) -> Option<WidgetId> {
373 let mut curr = Some(node_id);
374 while let Some(pid) = curr {
375 if let Some(node) = ir.nodes.get(&pid) {
376 if let Op::Semantics(s) = &node.op {
377 if s.is_focus_scope {
378 return Some(pid);
379 }
380 }
381 curr = node.parent;
382 } else {
383 break;
384 }
385 }
386 None
387}
388
389fn is_focus_barrier(ir: &CoreIR, node_id: WidgetId) -> bool {
390 semantics(ir, node_id).is_some_and(|value| value.is_focus_barrier)
391}
392
393fn semantics(ir: &CoreIR, node_id: WidgetId) -> Option<&fission_ir::Semantics> {
394 match &ir.nodes.get(&node_id)?.op {
395 Op::Semantics(value) => Some(value),
396 _ => None,
397 }
398}
399
400pub fn find_neighbor_focus_node(
401 ir: &CoreIR,
402 layout: &LayoutSnapshot,
403 current: WidgetId,
404 direction: FocusDirection,
405) -> Option<WidgetId> {
406 let current_rect = layout.get_node_rect(current)?;
407 let focusable_nodes = get_all_focusable_nodes(ir);
408
409 let mut best_candidate = None;
410 let mut best_dist = f32::INFINITY;
411
412 let (cx, cy) = (
413 current_rect.x() + current_rect.width() / 2.0,
414 current_rect.y() + current_rect.height() / 2.0,
415 );
416
417 for node_id in focusable_nodes {
418 if node_id == current {
419 continue;
420 }
421 let rect = match layout.get_node_rect(node_id) {
422 Some(r) => r,
423 None => continue,
424 };
425
426 let (nx, ny) = (
427 rect.x() + rect.width() / 2.0,
428 rect.y() + rect.height() / 2.0,
429 );
430
431 let is_in_dir = match direction {
432 FocusDirection::Up => ny < cy && (nx - cx).abs() < (ny - cy).abs(),
433 FocusDirection::Down => ny > cy && (nx - cx).abs() < (ny - cy).abs(),
434 FocusDirection::Left => nx < cx && (ny - cy).abs() < (nx - cx).abs(),
435 FocusDirection::Right => nx > cx && (ny - cy).abs() < (nx - cx).abs(),
436 };
437
438 if is_in_dir {
439 let dist = (nx - cx).powi(2) + (ny - cy).powi(2);
440 if dist < best_dist {
441 best_dist = dist;
442 best_candidate = Some(node_id);
443 }
444 }
445 }
446
447 best_candidate
448}