Another project
1
fork

Configure Feed

Select the types of activity you want to include in your feed.

feat(ui): focus manager, scopes, roving

Lewis: May this revision serve well! <lu5a@proton.me>

+595
+595
crates/bone-ui/src/focus.rs
··· 1 + use core::num::NonZeroU32; 2 + use std::collections::BTreeSet; 3 + 4 + use serde::{Deserialize, Serialize}; 5 + 6 + use crate::input::InputSnapshot; 7 + use crate::widget_id::WidgetId; 8 + 9 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)] 10 + #[serde(transparent)] 11 + pub struct FocusScopeId(NonZeroU32); 12 + 13 + impl FocusScopeId { 14 + pub const ROOT: Self = match NonZeroU32::new(1) { 15 + Some(n) => Self(n), 16 + None => panic!("ROOT scope id is 1"), 17 + }; 18 + 19 + #[must_use] 20 + pub const fn get(self) -> NonZeroU32 { 21 + self.0 22 + } 23 + } 24 + 25 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 26 + pub enum FocusScopeKind { 27 + Window, 28 + Modal, 29 + Roving, 30 + } 31 + 32 + #[derive(Copy, Clone, Debug, PartialEq, Eq)] 33 + struct FocusScope { 34 + id: FocusScopeId, 35 + kind: FocusScopeKind, 36 + } 37 + 38 + const FIRST_CHILD_SCOPE: NonZeroU32 = match NonZeroU32::new(2) { 39 + Some(n) => n, 40 + None => panic!("first child scope id is 2"), 41 + }; 42 + 43 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 44 + pub enum InputModality { 45 + Keyboard, 46 + Pointer, 47 + } 48 + 49 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 50 + pub enum RovingDirection { 51 + Next, 52 + Prev, 53 + First, 54 + Last, 55 + } 56 + 57 + #[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)] 58 + pub enum FocusRequest { 59 + Set(WidgetId), 60 + Clear, 61 + Advance, 62 + Retreat, 63 + Rove(RovingDirection), 64 + } 65 + 66 + #[derive(Clone, Debug, PartialEq)] 67 + pub struct FocusManager { 68 + focused: Option<WidgetId>, 69 + modality: InputModality, 70 + scopes: Vec<FocusScope>, 71 + next_scope: NonZeroU32, 72 + tab_stops: Vec<(WidgetId, FocusScopeId)>, 73 + tab_stop_ids: BTreeSet<WidgetId>, 74 + focusable: BTreeSet<WidgetId>, 75 + request: Option<FocusRequest>, 76 + } 77 + 78 + impl Default for FocusManager { 79 + fn default() -> Self { 80 + Self::new() 81 + } 82 + } 83 + 84 + impl FocusManager { 85 + #[must_use] 86 + pub fn new() -> Self { 87 + let root = FocusScope { 88 + id: FocusScopeId::ROOT, 89 + kind: FocusScopeKind::Window, 90 + }; 91 + Self { 92 + focused: None, 93 + modality: InputModality::Pointer, 94 + scopes: vec![root], 95 + next_scope: FIRST_CHILD_SCOPE, 96 + tab_stops: Vec::new(), 97 + tab_stop_ids: BTreeSet::new(), 98 + focusable: BTreeSet::new(), 99 + request: None, 100 + } 101 + } 102 + 103 + #[must_use] 104 + pub fn focused(&self) -> Option<WidgetId> { 105 + self.focused 106 + } 107 + 108 + #[must_use] 109 + pub fn focus_visible(&self) -> bool { 110 + matches!(self.modality, InputModality::Keyboard) 111 + } 112 + 113 + pub fn observe_input(&mut self, input: &InputSnapshot) { 114 + if !input.keys_pressed.is_empty() { 115 + self.modality = InputModality::Keyboard; 116 + } else if !input.buttons_pressed.is_empty() || !input.buttons_released.is_empty() { 117 + self.modality = InputModality::Pointer; 118 + } 119 + } 120 + 121 + #[must_use] 122 + pub fn current_scope(&self) -> FocusScopeId { 123 + self.scopes.last().map_or(FocusScopeId::ROOT, |s| s.id) 124 + } 125 + 126 + pub fn push_scope(&mut self, kind: FocusScopeKind) -> FocusScopeId { 127 + let id = FocusScopeId(self.next_scope); 128 + self.next_scope = match self.next_scope.checked_add(1) { 129 + Some(n) => n, 130 + None => panic!("focus scope id space exhausted"), 131 + }; 132 + self.scopes.push(FocusScope { id, kind }); 133 + id 134 + } 135 + 136 + pub fn pop_scope(&mut self) -> Option<FocusScopeId> { 137 + if self.scopes.len() <= 1 { 138 + return None; 139 + } 140 + self.scopes.pop().map(|s| s.id) 141 + } 142 + 143 + pub fn register_tab_stop(&mut self, id: WidgetId) { 144 + if !self.tab_stop_ids.insert(id) { 145 + return; 146 + } 147 + self.focusable.insert(id); 148 + let scope = self.current_scope(); 149 + self.tab_stops.push((id, scope)); 150 + } 151 + 152 + pub fn register_focusable(&mut self, id: WidgetId) { 153 + self.focusable.insert(id); 154 + } 155 + 156 + #[must_use] 157 + pub fn tab_stops(&self) -> &[(WidgetId, FocusScopeId)] { 158 + &self.tab_stops 159 + } 160 + 161 + pub fn request(&mut self, request: FocusRequest) { 162 + self.request = Some(request); 163 + } 164 + 165 + pub fn request_focus(&mut self, id: WidgetId) { 166 + self.request(FocusRequest::Set(id)); 167 + } 168 + 169 + pub fn begin_frame(&mut self) { 170 + self.tab_stops.clear(); 171 + self.tab_stop_ids.clear(); 172 + self.focusable.clear(); 173 + self.scopes.truncate(1); 174 + self.next_scope = FIRST_CHILD_SCOPE; 175 + } 176 + 177 + pub fn end_frame(&mut self) { 178 + let request = self.request.take(); 179 + let next = match request { 180 + None => self.focused, 181 + Some(FocusRequest::Set(id)) => Some(id), 182 + Some(FocusRequest::Clear) => None, 183 + Some(FocusRequest::Advance) => self.advance_within_scope(false), 184 + Some(FocusRequest::Retreat) => self.advance_within_scope(true), 185 + Some(FocusRequest::Rove(direction)) => self.rove_within_roving_scope(direction), 186 + }; 187 + self.focused = self.validate_focus(next); 188 + } 189 + 190 + fn validate_focus(&self, candidate: Option<WidgetId>) -> Option<WidgetId> { 191 + candidate.filter(|id| self.focusable.contains(id)) 192 + } 193 + 194 + fn nearest_scope(&self, kind: FocusScopeKind) -> Option<FocusScopeId> { 195 + self.scopes 196 + .iter() 197 + .rev() 198 + .find(|s| s.kind == kind) 199 + .map(|s| s.id) 200 + } 201 + 202 + fn stops_for_scope(&self, scope: FocusScopeId) -> Vec<WidgetId> { 203 + self.tab_stops 204 + .iter() 205 + .filter(|(_, s)| *s == scope) 206 + .map(|(id, _)| *id) 207 + .collect() 208 + } 209 + 210 + fn advance_within_scope(&self, reverse: bool) -> Option<WidgetId> { 211 + let trap = self 212 + .nearest_scope(FocusScopeKind::Modal) 213 + .unwrap_or(FocusScopeId::ROOT); 214 + let stops = self.stops_for_scope(trap); 215 + cycle(&stops, self.focused, reverse) 216 + } 217 + 218 + fn rove_within_roving_scope(&self, direction: RovingDirection) -> Option<WidgetId> { 219 + let scope = self.nearest_scope(FocusScopeKind::Roving)?; 220 + let stops = self.stops_for_scope(scope); 221 + match direction { 222 + RovingDirection::Next => cycle(&stops, self.focused, false), 223 + RovingDirection::Prev => cycle(&stops, self.focused, true), 224 + RovingDirection::First => stops.first().copied(), 225 + RovingDirection::Last => stops.last().copied(), 226 + } 227 + } 228 + } 229 + 230 + fn cycle(stops: &[WidgetId], current: Option<WidgetId>, reverse: bool) -> Option<WidgetId> { 231 + if stops.is_empty() { 232 + return None; 233 + } 234 + let len = stops.len(); 235 + let next_index = match current.and_then(|id| stops.iter().position(|s| *s == id)) { 236 + None => { 237 + if reverse { 238 + len - 1 239 + } else { 240 + 0 241 + } 242 + } 243 + Some(idx) => { 244 + if reverse { 245 + (idx + len - 1) % len 246 + } else { 247 + (idx + 1) % len 248 + } 249 + } 250 + }; 251 + stops.get(next_index).copied() 252 + } 253 + 254 + #[cfg(test)] 255 + mod tests { 256 + use super::{FocusManager, FocusRequest, FocusScopeKind, RovingDirection}; 257 + use crate::input::{ 258 + FrameInstant, InputSnapshot, KeyChar, KeyCode, KeyEvent, ModifierMask, PointerButton, 259 + PointerButtonMask, 260 + }; 261 + use crate::widget_id::{WidgetId, WidgetKey}; 262 + 263 + fn id(key: &'static str) -> WidgetId { 264 + WidgetId::ROOT.child(WidgetKey::new(key)) 265 + } 266 + 267 + fn run_frame<F: FnOnce(&mut FocusManager)>(focus: &mut FocusManager, build: F) { 268 + focus.begin_frame(); 269 + build(focus); 270 + focus.end_frame(); 271 + } 272 + 273 + fn key_input() -> InputSnapshot { 274 + let mut input = InputSnapshot::idle(FrameInstant::ZERO); 275 + input.keys_pressed.push(KeyEvent::new( 276 + KeyCode::Char(KeyChar::from_char('a')), 277 + ModifierMask::NONE, 278 + )); 279 + input 280 + } 281 + 282 + fn pointer_button_input() -> InputSnapshot { 283 + let mut input = InputSnapshot::idle(FrameInstant::ZERO); 284 + input.buttons_pressed = PointerButtonMask::just(PointerButton::Primary); 285 + input 286 + } 287 + 288 + #[test] 289 + fn observe_input_flips_to_keyboard_on_keypress() { 290 + let mut focus = FocusManager::new(); 291 + focus.observe_input(&pointer_button_input()); 292 + assert!(!focus.focus_visible()); 293 + focus.observe_input(&key_input()); 294 + assert!(focus.focus_visible()); 295 + } 296 + 297 + #[test] 298 + fn observe_input_flips_back_to_pointer_on_button() { 299 + let mut focus = FocusManager::new(); 300 + focus.observe_input(&key_input()); 301 + assert!(focus.focus_visible()); 302 + focus.observe_input(&pointer_button_input()); 303 + assert!(!focus.focus_visible()); 304 + } 305 + 306 + #[test] 307 + fn observe_input_idle_keeps_modality() { 308 + let mut focus = FocusManager::new(); 309 + focus.observe_input(&key_input()); 310 + assert!(focus.focus_visible()); 311 + focus.observe_input(&InputSnapshot::idle(FrameInstant::ZERO)); 312 + assert!(focus.focus_visible(), "idle frames must not flip modality"); 313 + } 314 + 315 + #[test] 316 + fn advance_walks_tab_stops_in_order() { 317 + let mut focus = FocusManager::new(); 318 + let a = id("a"); 319 + let b = id("b"); 320 + let c = id("c"); 321 + run_frame(&mut focus, |f| { 322 + f.register_tab_stop(a); 323 + f.register_tab_stop(b); 324 + f.register_tab_stop(c); 325 + f.request(FocusRequest::Advance); 326 + }); 327 + assert_eq!(focus.focused(), Some(a)); 328 + run_frame(&mut focus, |f| { 329 + f.register_tab_stop(a); 330 + f.register_tab_stop(b); 331 + f.register_tab_stop(c); 332 + f.request(FocusRequest::Advance); 333 + }); 334 + assert_eq!(focus.focused(), Some(b)); 335 + run_frame(&mut focus, |f| { 336 + f.register_tab_stop(a); 337 + f.register_tab_stop(b); 338 + f.register_tab_stop(c); 339 + f.request(FocusRequest::Advance); 340 + }); 341 + assert_eq!(focus.focused(), Some(c)); 342 + run_frame(&mut focus, |f| { 343 + f.register_tab_stop(a); 344 + f.register_tab_stop(b); 345 + f.register_tab_stop(c); 346 + f.request(FocusRequest::Advance); 347 + }); 348 + assert_eq!(focus.focused(), Some(a)); 349 + } 350 + 351 + #[test] 352 + fn retreat_walks_in_reverse() { 353 + let mut focus = FocusManager::new(); 354 + let a = id("a"); 355 + let b = id("b"); 356 + run_frame(&mut focus, |f| { 357 + f.register_tab_stop(a); 358 + f.register_tab_stop(b); 359 + f.request_focus(a); 360 + }); 361 + run_frame(&mut focus, |f| { 362 + f.register_tab_stop(a); 363 + f.register_tab_stop(b); 364 + f.request(FocusRequest::Retreat); 365 + }); 366 + assert_eq!(focus.focused(), Some(b)); 367 + } 368 + 369 + #[test] 370 + fn modal_trap_restricts_tab_traversal() { 371 + let mut focus = FocusManager::new(); 372 + let outer = id("outer"); 373 + let modal_a = id("modal_a"); 374 + let modal_b = id("modal_b"); 375 + run_frame(&mut focus, |f| { 376 + f.register_tab_stop(outer); 377 + f.push_scope(FocusScopeKind::Modal); 378 + f.register_tab_stop(modal_a); 379 + f.register_tab_stop(modal_b); 380 + f.request_focus(modal_a); 381 + }); 382 + assert_eq!(focus.focused(), Some(modal_a)); 383 + run_frame(&mut focus, |f| { 384 + f.register_tab_stop(outer); 385 + f.push_scope(FocusScopeKind::Modal); 386 + f.register_tab_stop(modal_a); 387 + f.register_tab_stop(modal_b); 388 + f.request(FocusRequest::Advance); 389 + }); 390 + assert_eq!(focus.focused(), Some(modal_b)); 391 + run_frame(&mut focus, |f| { 392 + f.register_tab_stop(outer); 393 + f.push_scope(FocusScopeKind::Modal); 394 + f.register_tab_stop(modal_a); 395 + f.register_tab_stop(modal_b); 396 + f.request(FocusRequest::Advance); 397 + }); 398 + assert_eq!(focus.focused(), Some(modal_a)); 399 + } 400 + 401 + #[test] 402 + fn roving_within_composite_widget() { 403 + let mut focus = FocusManager::new(); 404 + let toolbar_a = id("tool_a"); 405 + let toolbar_b = id("tool_b"); 406 + let toolbar_c = id("tool_c"); 407 + run_frame(&mut focus, |f| { 408 + f.push_scope(FocusScopeKind::Roving); 409 + f.register_tab_stop(toolbar_a); 410 + f.register_tab_stop(toolbar_b); 411 + f.register_tab_stop(toolbar_c); 412 + f.request_focus(toolbar_b); 413 + }); 414 + run_frame(&mut focus, |f| { 415 + f.push_scope(FocusScopeKind::Roving); 416 + f.register_tab_stop(toolbar_a); 417 + f.register_tab_stop(toolbar_b); 418 + f.register_tab_stop(toolbar_c); 419 + f.request(FocusRequest::Rove(RovingDirection::Next)); 420 + }); 421 + assert_eq!(focus.focused(), Some(toolbar_c)); 422 + run_frame(&mut focus, |f| { 423 + f.push_scope(FocusScopeKind::Roving); 424 + f.register_tab_stop(toolbar_a); 425 + f.register_tab_stop(toolbar_b); 426 + f.register_tab_stop(toolbar_c); 427 + f.request(FocusRequest::Rove(RovingDirection::First)); 428 + }); 429 + assert_eq!(focus.focused(), Some(toolbar_a)); 430 + } 431 + 432 + #[test] 433 + fn focus_drops_when_widget_disappears() { 434 + let mut focus = FocusManager::new(); 435 + let a = id("a"); 436 + let b = id("b"); 437 + run_frame(&mut focus, |f| { 438 + f.register_tab_stop(a); 439 + f.register_tab_stop(b); 440 + f.request_focus(b); 441 + }); 442 + assert_eq!(focus.focused(), Some(b)); 443 + run_frame(&mut focus, |f| { 444 + f.register_tab_stop(a); 445 + }); 446 + assert_eq!(focus.focused(), None); 447 + } 448 + 449 + #[test] 450 + fn pop_scope_does_not_unseat_root() { 451 + let mut focus = FocusManager::new(); 452 + assert!(focus.pop_scope().is_none()); 453 + let inner = focus.push_scope(FocusScopeKind::Modal); 454 + assert_eq!(focus.pop_scope(), Some(inner)); 455 + assert!(focus.pop_scope().is_none()); 456 + } 457 + 458 + #[test] 459 + fn tab_skips_roving_stops_without_modal() { 460 + let mut focus = FocusManager::new(); 461 + let host = id("toolbar"); 462 + let rove_a = id("rove_a"); 463 + let rove_b = id("rove_b"); 464 + let trailing = id("after"); 465 + run_frame(&mut focus, |f| { 466 + f.register_tab_stop(host); 467 + f.push_scope(FocusScopeKind::Roving); 468 + f.register_tab_stop(rove_a); 469 + f.register_tab_stop(rove_b); 470 + f.pop_scope(); 471 + f.register_tab_stop(trailing); 472 + f.request(FocusRequest::Advance); 473 + }); 474 + assert_eq!(focus.focused(), Some(host)); 475 + run_frame(&mut focus, |f| { 476 + f.register_tab_stop(host); 477 + f.push_scope(FocusScopeKind::Roving); 478 + f.register_tab_stop(rove_a); 479 + f.register_tab_stop(rove_b); 480 + f.pop_scope(); 481 + f.register_tab_stop(trailing); 482 + f.request(FocusRequest::Advance); 483 + }); 484 + assert_eq!(focus.focused(), Some(trailing)); 485 + } 486 + 487 + #[test] 488 + fn rove_returns_none_outside_roving_scope() { 489 + let mut focus = FocusManager::new(); 490 + let a = id("a"); 491 + let b = id("b"); 492 + run_frame(&mut focus, |f| { 493 + f.register_tab_stop(a); 494 + f.register_tab_stop(b); 495 + f.request_focus(a); 496 + }); 497 + assert_eq!(focus.focused(), Some(a)); 498 + run_frame(&mut focus, |f| { 499 + f.register_tab_stop(a); 500 + f.register_tab_stop(b); 501 + f.request(FocusRequest::Rove(RovingDirection::Next)); 502 + }); 503 + assert_eq!(focus.focused(), None); 504 + } 505 + 506 + #[test] 507 + fn begin_frame_resets_scope_ids() { 508 + let mut focus = FocusManager::new(); 509 + focus.begin_frame(); 510 + let first = focus.push_scope(FocusScopeKind::Modal); 511 + focus.end_frame(); 512 + focus.begin_frame(); 513 + let second = focus.push_scope(FocusScopeKind::Modal); 514 + focus.end_frame(); 515 + assert_eq!(first, second, "scope ids reset each frame"); 516 + } 517 + 518 + #[test] 519 + fn begin_frame_truncates_leaked_scopes() { 520 + let mut focus = FocusManager::new(); 521 + focus.push_scope(FocusScopeKind::Modal); 522 + focus.push_scope(FocusScopeKind::Roving); 523 + focus.begin_frame(); 524 + assert_eq!(focus.current_scope(), super::FocusScopeId::ROOT); 525 + } 526 + 527 + #[test] 528 + fn register_tab_stop_dedupes_per_frame() { 529 + let mut focus = FocusManager::new(); 530 + let a = id("a"); 531 + let b = id("b"); 532 + run_frame(&mut focus, |f| { 533 + f.register_tab_stop(a); 534 + f.register_tab_stop(a); 535 + f.register_tab_stop(b); 536 + f.request_focus(a); 537 + }); 538 + assert_eq!(focus.tab_stops().len(), 2); 539 + run_frame(&mut focus, |f| { 540 + f.register_tab_stop(a); 541 + f.register_tab_stop(a); 542 + f.register_tab_stop(b); 543 + f.request(FocusRequest::Advance); 544 + }); 545 + assert_eq!(focus.focused(), Some(b)); 546 + } 547 + 548 + #[test] 549 + fn programmatic_focus_holds_for_focusable_non_tab_stop() { 550 + let mut focus = FocusManager::new(); 551 + let click_only = id("canvas"); 552 + run_frame(&mut focus, |f| { 553 + f.register_focusable(click_only); 554 + f.request_focus(click_only); 555 + }); 556 + assert_eq!(focus.focused(), Some(click_only)); 557 + } 558 + 559 + #[test] 560 + fn programmatic_focus_drops_for_unknown_widget() { 561 + let mut focus = FocusManager::new(); 562 + let ghost = id("ghost"); 563 + run_frame(&mut focus, |f| { 564 + f.request_focus(ghost); 565 + }); 566 + assert_eq!(focus.focused(), None); 567 + } 568 + 569 + #[test] 570 + fn modal_traps_tab_with_nested_roving_excluded() { 571 + let mut focus = FocusManager::new(); 572 + let modal_a = id("modal_a"); 573 + let rove_x = id("rove_x"); 574 + let modal_b = id("modal_b"); 575 + run_frame(&mut focus, |f| { 576 + f.push_scope(FocusScopeKind::Modal); 577 + f.register_tab_stop(modal_a); 578 + f.push_scope(FocusScopeKind::Roving); 579 + f.register_tab_stop(rove_x); 580 + f.pop_scope(); 581 + f.register_tab_stop(modal_b); 582 + f.request_focus(modal_a); 583 + }); 584 + run_frame(&mut focus, |f| { 585 + f.push_scope(FocusScopeKind::Modal); 586 + f.register_tab_stop(modal_a); 587 + f.push_scope(FocusScopeKind::Roving); 588 + f.register_tab_stop(rove_x); 589 + f.pop_scope(); 590 + f.register_tab_stop(modal_b); 591 + f.request(FocusRequest::Advance); 592 + }); 593 + assert_eq!(focus.focused(), Some(modal_b)); 594 + } 595 + }