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