1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
//! Focus registry and input ownership.
//!
//! Focusable regions register a stable string id each frame. The registry
//! tracks which id currently holds focus and cycles through them with
//! Tab/BackTab. Overlays claim *input ownership*: while an overlay is open, the
//! registry reports its id as focused and the host routes events there,
//! regardless of the base tree's focus ring — the missing "input ownership
//! across overlay and non-overlay components" piece from Pi.
use super::event::{Event, EventFlow, KeyCode};
/// Tracks the focus ring and the currently focused region.
#[derive(Clone, Debug, Default)]
pub struct FocusRegistry {
order: Vec<String>,
focused: Option<String>,
/// When set, this id owns all input (an open overlay).
owner: Option<String>,
}
impl FocusRegistry {
/// Create an empty registry with no registered or focused regions.
pub fn new() -> Self {
Self::default()
}
/// Begin a frame: clear the per-frame focus ring. Registrations follow.
pub fn begin_frame(&mut self) {
self.order.clear();
}
/// Register a focusable region for this frame, in tab order.
pub fn register(&mut self, id: impl Into<String>) {
let id = id.into();
if self.focused.is_none() {
self.focused = Some(id.clone());
}
self.order.push(id);
}
/// Give exclusive input ownership to `id` (e.g. an overlay) until cleared.
pub fn set_owner(&mut self, id: impl Into<String>) {
self.owner = Some(id.into());
}
/// Release input ownership.
pub fn clear_owner(&mut self) {
self.owner = None;
}
/// The id that should receive input: the owner if any, else the focused id.
pub fn active(&self) -> Option<&str> {
self.owner.as_deref().or(self.focused.as_deref())
}
/// Whether `id` is the active input target.
pub fn is_active(&self, id: &str) -> bool {
self.active() == Some(id)
}
/// Whether `id` holds focus in the base ring (ignores overlay ownership).
pub fn is_focused(&self, id: &str) -> bool {
self.focused.as_deref() == Some(id)
}
fn advance(&mut self, delta: isize) {
if self.order.is_empty() {
return;
}
let len = self.order.len() as isize;
let current = self
.focused
.as_ref()
.and_then(|f| self.order.iter().position(|id| id == f))
.map(|p| p as isize)
.unwrap_or(0);
let next = ((current + delta) % len + len) % len;
self.focused = Some(self.order[next as usize].clone());
}
/// Route Tab/BackTab to move focus. Ignored while an overlay owns input.
pub fn handle(&mut self, event: &Event) -> EventFlow {
if self.owner.is_some() {
return EventFlow::Ignored;
}
let Event::Key(k) = event else {
return EventFlow::Ignored;
};
match k.code {
KeyCode::Tab if k.plain() => {
self.advance(1);
EventFlow::Consumed
}
KeyCode::BackTab => {
self.advance(-1);
EventFlow::Consumed
}
_ => EventFlow::Ignored,
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::{Event, EventFlow, Key, KeyCode};
#[test]
fn focus_tab_cycles_registered_regions() {
let mut f = FocusRegistry::new();
f.begin_frame();
f.register("a");
f.register("b");
f.register("c");
assert!(f.is_focused("a"));
let tab = Event::Key(Key::new(KeyCode::Tab));
assert_eq!(f.handle(&tab), EventFlow::Consumed);
assert!(f.is_focused("b"));
let back = Event::Key(Key::new(KeyCode::BackTab));
f.handle(&back);
assert!(f.is_focused("a"));
// Wrap backwards.
f.handle(&back);
assert!(f.is_focused("c"));
}
#[test]
fn overlay_owner_takes_input_and_blocks_tab() {
let mut f = FocusRegistry::new();
f.begin_frame();
f.register("composer");
f.set_owner("dialog");
assert!(f.is_active("dialog"));
assert!(!f.is_active("composer"));
// Tab is swallowed while an overlay owns input.
let tab = Event::Key(Key::new(KeyCode::Tab));
assert_eq!(f.handle(&tab), EventFlow::Ignored);
f.clear_owner();
assert!(f.is_active("composer"));
}
}