cranpose_ui/
focus_manager.rs1use cranpose_core::{CompositionLocal, NodeId};
2use cranpose_ui_graphics::Rect;
3
4use crate::{
5 focus_dispatch,
6 focus_order::{FocusEntry, with_focus_order},
7 modifier::FocusDirection,
8 two_dimensional_focus_search::{Arrow, entry_point, find_best_candidate},
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 let moved = focus_dispatch::request_focus_in_context(target);
33 if moved {
34 focus_dispatch::note_focus_rect(target);
35 }
36 moved
37 }
38
39 pub fn clear_focus(&self) -> bool {
43 focus_dispatch::clear_active_focus()
44 }
45}
46
47pub fn local_focus_manager() -> CompositionLocal<FocusManager> {
50 crate::environment_locals::cached_local(
51 |locals| &locals.focus_manager,
52 || cranpose_core::compositionLocalOf(FocusManager::default),
53 )
54}
55
56pub fn request_focus_from_platform(node_id: NodeId) -> bool {
60 focus_dispatch::request_focus_in_context(node_id)
61}
62
63#[doc(hidden)]
68pub fn request_focus_without_keyboard(node_id: NodeId) -> bool {
69 crate::text_input_session::without_keyboard(|| {
70 focus_dispatch::request_focus_in_context(node_id)
71 })
72}
73
74fn next_target(direction: FocusDirection) -> Option<NodeId> {
75 with_focus_order(|order| {
76 if order.is_empty() {
77 return None;
78 }
79 let active = focus_dispatch::active_focus_target();
80 let current = active.and_then(|node_id| order.iter().position(|e| e.node_id == node_id));
81
82 if let Some(arrow) = Arrow::of(direction) {
83 return arrow_target(order, active, arrow);
84 }
85 match direction {
86 FocusDirection::Next | FocusDirection::Enter => Some(step(order, current, 1)),
87 FocusDirection::Previous => Some(step(order, current, -1)),
88 FocusDirection::Exit
89 | FocusDirection::Up
90 | FocusDirection::Down
91 | FocusDirection::Left
92 | FocusDirection::Right => None,
93 }
94 })
95}
96
97pub(crate) fn arrow_target(
103 order: &[FocusEntry],
104 active: Option<NodeId>,
105 arrow: Arrow,
106) -> Option<NodeId> {
107 if let Some(focused) = order.iter().find(|entry| Some(entry.node_id) == active) {
108 let candidates = order.iter().filter(|entry| Some(entry.node_id) != active);
109 return find_best_candidate(candidates, focused.rect, arrow);
110 }
111 if let Some(last) = focus_dispatch::last_focus_rect() {
112 return order
113 .iter()
114 .find(|entry| covers_center(entry.rect, last))
115 .map(|entry| entry.node_id)
116 .or_else(|| find_best_candidate(order, last, arrow));
117 }
118 find_best_candidate(order, entry_point(order, arrow)?, arrow)
119}
120
121fn covers_center(rect: Rect, of: Rect) -> bool {
122 let (x, y) = (of.x + of.width * 0.5, of.y + of.height * 0.5);
123 x >= rect.x && x < rect.x + rect.width && y >= rect.y && y < rect.y + rect.height
124}
125
126fn step(order: &[FocusEntry], current: Option<usize>, delta: isize) -> NodeId {
127 let count = order.len() as isize;
128 let index = match current {
129 Some(index) => (index as isize + delta).rem_euclid(count),
130 None if delta > 0 => 0,
131 None => count - 1,
132 };
133 order[index as usize].node_id
134}
135
136#[cfg(test)]
137#[path = "tests/focus_manager_tests.rs"]
138mod tests;