Skip to main content

cranpose_ui/
focus_manager.rs

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/// Moves focus between the focus targets the tree holds, the way
12/// `LocalFocusManager` does in Jetpack Compose.
13///
14/// ```ignore
15/// let focus = cranpose_ui::local_focus_manager().current();
16/// focus.move_focus(FocusDirection::Next);
17/// focus.clear_focus();
18/// ```
19///
20/// Tab and Shift+Tab reach this through the app shell, so an app that puts
21/// `Modifier::focusable()` on its controls gets keyboard traversal with no
22/// further code.
23#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
24pub struct FocusManager;
25
26impl FocusManager {
27    /// Moves focus one step in `direction`. Answers whether focus moved.
28    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    /// Drops focus from whatever holds it, and answers whether one held it.
36    /// Compose takes a `force` flag here for targets that refuse to give focus
37    /// up; no target in Cranpose refuses, so this always clears.
38    pub fn clear_focus(&self) -> bool {
39        focus_dispatch::clear_active_focus()
40    }
41}
42
43/// CompositionLocal carrying the [`FocusManager`], as `LocalFocusManager` does
44/// in Compose. The same instance comes back on every call.
45pub 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
58/// Moves focus onto `node_id` when it holds a focus target. A platform's
59/// accessibility layer calls this when a screen reader lands on a control, so
60/// the app's focus follows the reader's.
61pub 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}