cranpose_ui/
focus_manager.rs1use std::cell::RefCell;
2
3use cranpose_core::{CompositionLocal, NodeId};
4
5use crate::{
6 focus_dispatch,
7 focus_order::{FocusEntry, with_focus_order},
8 modifier::FocusDirection,
9};
10
11#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
24pub struct FocusManager;
25
26impl FocusManager {
27 pub fn move_focus(&self, direction: FocusDirection) -> bool {
29 let Some(target) = next_target(direction) else {
30 return false;
31 };
32 focus_dispatch::request_focus_in_context(target)
33 }
34
35 pub fn clear_focus(&self) -> bool {
39 focus_dispatch::clear_active_focus()
40 }
41}
42
43pub fn local_focus_manager() -> CompositionLocal<FocusManager> {
46 thread_local! {
47 static LOCAL_FOCUS_MANAGER: RefCell<Option<CompositionLocal<FocusManager>>> =
48 const { RefCell::new(None) };
49 }
50
51 LOCAL_FOCUS_MANAGER.with(|cell| {
52 cell.borrow_mut()
53 .get_or_insert_with(|| cranpose_core::compositionLocalOf(FocusManager::default))
54 .clone()
55 })
56}
57
58pub fn request_focus_from_platform(node_id: NodeId) -> bool {
62 focus_dispatch::request_focus_in_context(node_id)
63}
64
65fn next_target(direction: FocusDirection) -> Option<NodeId> {
66 with_focus_order(|order| {
67 if order.is_empty() {
68 return None;
69 }
70 let active = focus_dispatch::active_focus_target();
71 let current = active.and_then(|node_id| order.iter().position(|e| e.node_id == node_id));
72
73 match direction {
74 FocusDirection::Next | FocusDirection::Enter => Some(step(order, current, 1)),
75 FocusDirection::Previous => Some(step(order, current, -1)),
76 FocusDirection::Exit => None,
77 FocusDirection::Up
78 | FocusDirection::Down
79 | FocusDirection::Left
80 | FocusDirection::Right => current.and_then(|index| nearest(order, index, direction)),
81 }
82 })
83}
84
85fn step(order: &[FocusEntry], current: Option<usize>, delta: isize) -> NodeId {
86 let count = order.len() as isize;
87 let index = match current {
88 Some(index) => (index as isize + delta).rem_euclid(count),
89 None if delta > 0 => 0,
90 None => count - 1,
91 };
92 order[index as usize].node_id
93}
94
95fn nearest(order: &[FocusEntry], from: usize, direction: FocusDirection) -> Option<NodeId> {
96 let (from_x, from_y) = order[from].center();
97 let mut best: Option<(f32, NodeId)> = None;
98
99 for (index, entry) in order.iter().enumerate() {
100 if index == from {
101 continue;
102 }
103 let (x, y) = entry.center();
104 let (along, across) = match direction {
105 FocusDirection::Up => (from_y - y, (x - from_x).abs()),
106 FocusDirection::Down => (y - from_y, (x - from_x).abs()),
107 FocusDirection::Left => (from_x - x, (y - from_y).abs()),
108 FocusDirection::Right => (x - from_x, (y - from_y).abs()),
109 _ => continue,
110 };
111 if along <= 0.0 {
112 continue;
113 }
114 let cost = along + across * 2.0;
115 if best.is_none_or(|(best_cost, _)| cost < best_cost) {
116 best = Some((cost, entry.node_id));
117 }
118 }
119
120 best.map(|(_, node_id)| node_id)
121}
122
123#[cfg(test)]
124#[path = "tests/focus_manager_tests.rs"]
125mod tests;