1use 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)]
124mod tests {
125 use std::{cell::RefCell, rc::Rc};
126
127 use cranpose_foundation::FocusState;
128 use cranpose_ui_graphics::Rect;
129
130 use super::*;
131 use crate::{
132 focus_dispatch::{FocusTargetHandle, register_focus_target},
133 focus_order::set_focus_order,
134 };
135
136 struct Target {
137 states: RefCell<Vec<FocusState>>,
138 }
139
140 impl Target {
141 fn new() -> Rc<Self> {
142 Rc::new(Self {
143 states: RefCell::new(Vec::new()),
144 })
145 }
146 }
147
148 impl FocusTargetHandle for Target {
149 fn set_focus_state(&self, state: FocusState) {
150 self.states.borrow_mut().push(state);
151 }
152 }
153
154 fn row(node_id: NodeId, x: f32, y: f32) -> FocusEntry {
155 FocusEntry {
156 node_id,
157 rect: Rect {
158 x,
159 y,
160 width: 100.0,
161 height: 40.0,
162 },
163 }
164 }
165
166 fn targets(ids: &[NodeId]) -> Vec<Rc<Target>> {
167 ids.iter()
168 .map(|node_id| {
169 let target = Target::new();
170 register_focus_target(*node_id, Rc::clone(&target) as Rc<dyn FocusTargetHandle>);
171 target
172 })
173 .collect()
174 }
175
176 #[test]
177 fn next_takes_the_first_target_when_nothing_holds_focus() {
178 let _app_context = crate::render_state::app_context_test_scope();
179 let _targets = targets(&[1, 2, 3]);
180 set_focus_order(vec![
181 row(1, 0.0, 0.0),
182 row(2, 0.0, 50.0),
183 row(3, 0.0, 100.0),
184 ]);
185
186 assert!(FocusManager.move_focus(FocusDirection::Next));
187 assert_eq!(focus_dispatch::active_focus_target(), Some(1));
188 }
189
190 #[test]
191 fn next_and_previous_walk_the_order_and_wrap() {
192 let _app_context = crate::render_state::app_context_test_scope();
193 let _targets = targets(&[1, 2, 3]);
194 set_focus_order(vec![
195 row(1, 0.0, 0.0),
196 row(2, 0.0, 50.0),
197 row(3, 0.0, 100.0),
198 ]);
199
200 FocusManager.move_focus(FocusDirection::Next);
201 FocusManager.move_focus(FocusDirection::Next);
202 assert_eq!(focus_dispatch::active_focus_target(), Some(2));
203
204 FocusManager.move_focus(FocusDirection::Next);
205 assert_eq!(focus_dispatch::active_focus_target(), Some(3));
206
207 FocusManager.move_focus(FocusDirection::Next);
208 assert_eq!(
209 focus_dispatch::active_focus_target(),
210 Some(1),
211 "Next past the last target comes back to the first"
212 );
213
214 FocusManager.move_focus(FocusDirection::Previous);
215 assert_eq!(
216 focus_dispatch::active_focus_target(),
217 Some(3),
218 "Previous from the first target goes to the last"
219 );
220 }
221
222 #[test]
223 fn a_direction_takes_the_nearest_target_that_way() {
224 let _app_context = crate::render_state::app_context_test_scope();
225 let _targets = targets(&[1, 2, 3, 4]);
226 set_focus_order(vec![
227 row(1, 0.0, 0.0),
228 row(2, 200.0, 0.0),
229 row(3, 0.0, 200.0),
230 row(4, 0.0, 600.0),
231 ]);
232
233 FocusManager.move_focus(FocusDirection::Next);
234 assert_eq!(focus_dispatch::active_focus_target(), Some(1));
235
236 assert!(FocusManager.move_focus(FocusDirection::Right));
237 assert_eq!(focus_dispatch::active_focus_target(), Some(2));
238
239 assert!(FocusManager.move_focus(FocusDirection::Left));
240 assert_eq!(focus_dispatch::active_focus_target(), Some(1));
241
242 assert!(FocusManager.move_focus(FocusDirection::Down));
243 assert_eq!(
244 focus_dispatch::active_focus_target(),
245 Some(3),
246 "Down takes the nearer of the two targets below, not the far one"
247 );
248
249 assert!(FocusManager.move_focus(FocusDirection::Up));
250 assert_eq!(
251 focus_dispatch::active_focus_target(),
252 Some(1),
253 "Up from the lower row takes the target straight above, not the one off to the side"
254 );
255 }
256
257 #[test]
258 fn a_direction_with_nothing_that_way_leaves_focus_alone() {
259 let _app_context = crate::render_state::app_context_test_scope();
260 let _targets = targets(&[1, 2]);
261 set_focus_order(vec![row(1, 0.0, 0.0), row(2, 0.0, 50.0)]);
262
263 FocusManager.move_focus(FocusDirection::Next);
264 assert_eq!(focus_dispatch::active_focus_target(), Some(1));
265
266 assert!(!FocusManager.move_focus(FocusDirection::Up));
267 assert_eq!(focus_dispatch::active_focus_target(), Some(1));
268 }
269
270 #[test]
271 fn clear_focus_drops_the_active_target() {
272 let _app_context = crate::render_state::app_context_test_scope();
273 let targets = targets(&[1, 2]);
274 set_focus_order(vec![row(1, 0.0, 0.0), row(2, 0.0, 50.0)]);
275
276 FocusManager.move_focus(FocusDirection::Next);
277 assert!(FocusManager.clear_focus());
278 assert_eq!(focus_dispatch::active_focus_target(), None);
279 assert_eq!(
280 targets[0].states.borrow().last(),
281 Some(&FocusState::Inactive)
282 );
283 assert!(!FocusManager.clear_focus());
284 }
285
286 #[test]
287 fn a_move_without_any_target_answers_no() {
288 let _app_context = crate::render_state::app_context_test_scope();
289 set_focus_order(Vec::new());
290
291 assert!(!FocusManager.move_focus(FocusDirection::Next));
292 assert_eq!(focus_dispatch::active_focus_target(), None);
293 }
294}