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
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
//! 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: reconcile focus against the preceding frame, then clear
/// the per-frame focus ring. Registrations for the new frame follow.
pub fn begin_frame(&mut self) {
if self.order.is_empty() {
self.focused = None;
} else if !self
.focused
.as_ref()
.is_some_and(|focused| self.order.contains(focused))
{
self.focused = self.order.first().cloned();
}
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;
}
fn ring_focus(&self) -> Option<&str> {
self.focused
.as_deref()
.filter(|focused| self.order.iter().any(|id| id == focused))
.or_else(|| self.order.first().map(String::as_str))
}
/// The id that should receive input: the owner if any, else the focused id.
/// If a focused region disappeared this frame, the first current
/// registration is used immediately.
pub fn active(&self) -> Option<&str> {
self.owner.as_deref().or_else(|| self.ring_focus())
}
/// 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.ring_focus() == Some(id)
}
fn advance(&mut self, delta: isize) {
if self.order.is_empty() {
return;
}
let len = self.order.len() as isize;
let current = self
.ring_focus()
.and_then(|focused| self.order.iter().position(|id| id == focused))
.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"));
}
#[test]
fn removed_focus_target_falls_back_to_the_current_ring() {
let mut f = FocusRegistry::new();
f.begin_frame();
f.register("old");
assert_eq!(f.active(), Some("old"));
f.begin_frame();
f.register("new");
assert_eq!(f.active(), Some("new"));
assert!(f.is_focused("new"));
// Starting another frame commits the fallback, so reintroducing the old
// target cannot steal focus back.
f.begin_frame();
f.register("old");
f.register("new");
assert_eq!(f.active(), Some("new"));
}
}