1use std::cell::RefCell;
54use std::collections::HashMap;
55use std::rc::Rc;
56
57use glam::Vec2;
58use repose_core::Rect;
59use repose_core::prelude::{FocusDirection, FocusRequester, Scheduler};
60use repose_core::runtime::{FocusManager, HitRegion, spatial_focus_next};
61
62#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
64pub enum NavDir {
65 Next,
67 Previous,
69 Left,
70 Right,
71 Up,
72 Down,
73}
74
75impl NavDir {
76 pub const ALL: [NavDir; 6] = [
78 NavDir::Next,
79 NavDir::Previous,
80 NavDir::Left,
81 NavDir::Right,
82 NavDir::Up,
83 NavDir::Down,
84 ];
85
86 pub fn focus_direction(self) -> FocusDirection {
88 match self {
89 NavDir::Next => FocusDirection::Next,
90 NavDir::Previous => FocusDirection::Previous,
91 NavDir::Left => FocusDirection::Left,
92 NavDir::Right => FocusDirection::Right,
93 NavDir::Up => FocusDirection::Up,
94 NavDir::Down => FocusDirection::Down,
95 }
96 }
97
98 pub fn reverse(self) -> Self {
100 match self {
101 NavDir::Next => NavDir::Previous,
102 NavDir::Previous => NavDir::Next,
103 NavDir::Left => NavDir::Right,
104 NavDir::Right => NavDir::Left,
105 NavDir::Up => NavDir::Down,
106 NavDir::Down => NavDir::Up,
107 }
108 }
109
110 pub fn is_chain(self) -> bool {
113 matches!(self, NavDir::Next | NavDir::Previous)
114 }
115
116 fn axis(self) -> Option<(Axis, f32)> {
118 match self {
119 NavDir::Left => Some((Axis::X, -1.0)),
120 NavDir::Right => Some((Axis::X, 1.0)),
121 NavDir::Up => Some((Axis::Y, -1.0)),
122 NavDir::Down => Some((Axis::Y, 1.0)),
123 NavDir::Next | NavDir::Previous => None,
124 }
125 }
126}
127
128#[derive(Clone, Copy, Debug, PartialEq, Eq)]
129enum Axis {
130 X,
131 Y,
132}
133
134#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
142pub struct GroupId(u64);
143
144impl GroupId {
145 pub fn named(tag: &str) -> Self {
147 let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
148 for byte in tag.as_bytes() {
149 hash ^= u64::from(*byte);
150 hash = hash.wrapping_mul(0x0000_0100_0000_01B3);
151 }
152 Self(hash)
153 }
154
155 pub const fn from_raw(raw: u64) -> Self {
157 Self(raw)
158 }
159
160 pub const fn raw(self) -> u64 {
161 self.0
162 }
163}
164
165#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
167pub enum Wrap {
168 Stay,
170 #[default]
173 Cycle,
174}
175
176#[derive(Clone, Copy, Debug, PartialEq, Eq)]
178pub enum NavStep {
179 Moved(u64),
181 Entered(u64),
183 Already(u64),
185 Edge,
188 Held,
190 Empty,
192}
193
194impl NavStep {
195 pub fn target(self) -> Option<u64> {
197 match self {
198 NavStep::Moved(id) | NavStep::Entered(id) | NavStep::Already(id) => Some(id),
199 NavStep::Edge | NavStep::Held | NavStep::Empty => None,
200 }
201 }
202}
203
204#[derive(Clone)]
206pub struct NavEntry {
207 id: u64,
208 rect: Rect,
209 group: Option<GroupId>,
210 requester: FocusRequester,
211}
212
213impl NavEntry {
214 pub fn id(&self) -> u64 {
216 self.id
217 }
218
219 pub fn rect(&self) -> Rect {
221 self.rect
222 }
223
224 pub fn group(&self) -> Option<GroupId> {
225 self.group
226 }
227
228 pub fn request_focus(&self) -> bool {
230 self.requester.try_request_focus()
231 }
232}
233
234struct Entry {
235 rect: Option<Rect>,
236 group: Option<GroupId>,
237 requester: FocusRequester,
238 frame: u64,
240}
241
242struct Pending {
251 key: usize,
252 rect: Rect,
253}
254
255const PENDING_CAP: usize = 256;
259
260fn requester_key(requester: &FocusRequester) -> usize {
262 Rc::as_ptr(&requester.target) as usize
263}
264
265struct Capture {
268 id: u64,
269 requester: FocusRequester,
270}
271
272#[derive(Default)]
273struct Registry {
274 frame: u64,
275 order: Vec<u64>,
276 entries: HashMap<u64, Entry>,
277 pending: Vec<Pending>,
278 capture: Option<Capture>,
279}
280
281#[derive(Clone, Default)]
294pub struct NavMap {
295 registry: Rc<RefCell<Registry>>,
296}
297
298impl NavMap {
299 pub fn new() -> Self {
300 Self::default()
301 }
302
303 pub fn begin_frame(&self) {
306 let mut registry = self.registry.borrow_mut();
307 registry.frame += 1;
308 let frame = registry.frame;
309 registry.entries.retain(|_, entry| entry.frame + 1 >= frame);
310 let mut order = std::mem::take(&mut registry.order);
311 order.retain(|id| registry.entries.contains_key(id));
312 registry.order = order;
313 }
314
315 pub fn clear(&self) {
317 let mut registry = self.registry.borrow_mut();
318 registry.entries.clear();
319 registry.order.clear();
320 registry.pending.clear();
321 }
322
323 pub fn register(&self, requester: &FocusRequester, group: Option<GroupId>) -> bool {
332 let Some(id) = *requester.target.borrow() else {
333 return false;
334 };
335 let mut registry = self.registry.borrow_mut();
336 let frame = registry.frame;
337 let key = requester_key(requester);
338 let rect = match registry.pending.iter().position(|pending| pending.key == key) {
339 Some(index) => Some(registry.pending.remove(index).rect),
340 None => None,
341 };
342 match registry.entries.get_mut(&id) {
343 Some(entry) => {
344 entry.group = group;
345 entry.requester = requester.clone();
346 entry.frame = frame;
347 if let Some(rect) = rect {
348 entry.rect = Some(rect);
349 }
350 }
351 None => {
352 registry.order.push(id);
353 registry.entries.insert(
354 id,
355 Entry {
356 rect,
357 group,
358 requester: requester.clone(),
359 frame,
360 },
361 );
362 }
363 }
364 true
365 }
366
367 pub fn mark(&self, id: u64, group: Option<GroupId>, requester: FocusRequester) {
372 let mut registry = self.registry.borrow_mut();
373 let frame = registry.frame;
374 match registry.entries.get_mut(&id) {
375 Some(entry) => {
376 entry.group = group;
377 entry.requester = requester;
378 entry.frame = frame;
379 }
380 None => {
381 registry.order.push(id);
382 registry.entries.insert(
383 id,
384 Entry {
385 rect: None,
386 group,
387 requester,
388 frame,
389 },
390 );
391 }
392 }
393 }
394
395 pub fn place(&self, requester: &FocusRequester, rect: Rect, group: Option<GroupId>) -> bool {
405 let mut registry = self.registry.borrow_mut();
406 let frame = registry.frame;
407 let Some(id) = *requester.target.borrow() else {
408 let key = requester_key(requester);
409 match registry.pending.iter_mut().find(|pending| pending.key == key) {
410 Some(pending) => pending.rect = rect,
411 None => {
412 if registry.pending.len() >= PENDING_CAP {
413 registry.pending.remove(0);
414 }
415 registry.pending.push(Pending { key, rect });
416 }
417 }
418 return false;
419 };
420 match registry.entries.get_mut(&id) {
421 Some(entry) => {
422 entry.rect = Some(rect);
423 if group.is_some() {
424 entry.group = group;
425 }
426 entry.frame = frame;
427 }
428 None => {
429 registry.order.push(id);
430 registry.entries.insert(
431 id,
432 Entry {
433 rect: Some(rect),
434 group,
435 requester: requester.clone(),
436 frame,
437 },
438 );
439 }
440 }
441 let key = requester_key(requester);
442 registry.pending.retain(|pending| pending.key != key);
443 true
444 }
445
446 pub fn active(&self) -> Vec<NavEntry> {
448 let registry = self.registry.borrow();
449 registry
450 .order
451 .iter()
452 .filter_map(|id| {
453 let entry = registry.entries.get(id)?;
454 Some(NavEntry {
455 id: *id,
456 rect: entry.rect?,
457 group: entry.group,
458 requester: entry.requester.clone(),
459 })
460 })
461 .collect()
462 }
463
464 pub fn first(&self) -> Option<u64> {
466 self.active().first().map(NavEntry::id)
467 }
468
469 pub fn len(&self) -> usize {
470 self.active().len()
471 }
472
473 pub fn is_empty(&self) -> bool {
474 self.active().is_empty()
475 }
476
477 pub fn contains(&self, id: u64) -> bool {
479 self.active().iter().any(|entry| entry.id() == id)
480 }
481
482 pub fn request(&self, id: u64) -> bool {
485 self.entry(id).is_some_and(|entry| entry.request_focus())
486 }
487
488 pub fn capture(&self, id: u64) -> bool {
494 let Some(entry) = self.entry(id) else {
495 return false;
496 };
497 let requester = entry.requester;
498 if !requester.capture_focus() {
499 return false;
500 }
501 self.registry.borrow_mut().capture = Some(Capture { id, requester });
502 true
503 }
504
505 pub fn release(&self, id: u64) -> bool {
512 let mut registry = self.registry.borrow_mut();
513 if registry.capture.as_ref().is_some_and(|capture| capture.id == id) {
514 let capture = registry.capture.take();
515 if let Some(capture) = capture {
516 capture.requester.free_focus();
517 }
518 return true;
519 }
520 match registry.entries.get(&id) {
521 Some(entry) if entry.rect.is_some() => {
522 entry.requester.free_focus();
523 true
524 }
525 _ => false,
526 }
527 }
528
529 fn release_stale_capture(&self) -> bool {
533 let mut registry = self.registry.borrow_mut();
534 let Some(capture) = registry.capture.as_ref() else {
535 return false;
536 };
537 let id = capture.id;
538 if registry
539 .entries
540 .get(&id)
541 .is_some_and(|entry| entry.rect.is_some())
542 {
543 return false;
544 }
545 let capture = registry.capture.take();
546 let Some(capture) = capture else {
547 return false;
548 };
549 if repose_core::runtime::focus_capture() == Some(id) {
550 capture.requester.free_focus();
551 return true;
552 }
553 false
554 }
555
556 fn entry(&self, id: u64) -> Option<NavEntry> {
557 self.active().into_iter().find(|entry| entry.id == id)
558 }
559
560 pub fn resolve(&self, from: Option<u64>, dir: NavDir, wrap: Wrap) -> NavStep {
562 let entries = self.active();
563 if entries.is_empty() {
564 return NavStep::Empty;
565 }
566
567 let Some(current) = from.filter(|id| entries.iter().any(|entry| entry.id == *id)) else {
571 return NavStep::Entered(entries[0].id);
572 };
573
574 let (chain, regions) = regions_of(&entries);
575 let step = |next: u64| {
576 if next == current {
577 NavStep::Edge
578 } else {
579 NavStep::Moved(next)
580 }
581 };
582 if let Some(next) = spatial_focus_next(&chain, ®ions, Some(current), dir.focus_direction())
583 {
584 return step(next);
585 }
586 if dir.is_chain() {
587 return NavStep::Edge;
590 }
591 match wrap {
592 Wrap::Stay => NavStep::Edge,
593 Wrap::Cycle => self.wrapped(&entries, current, dir).map_or(NavStep::Edge, step),
594 }
595 }
596
597 fn wrapped(&self, entries: &[NavEntry], current: u64, dir: NavDir) -> Option<u64> {
610 let (axis, sign) = dir.axis()?;
611 let span = self.span(entries, axis);
612 if span <= 0.0 {
613 return None;
614 }
615 let (chain, mut regions) = regions_of(entries);
616 for (region, entry) in regions.iter_mut().zip(entries) {
617 if entry.id != current && !is_ahead(entry.rect, entries, current, axis, sign) {
618 match axis {
619 Axis::X => region.rect.x += sign * span,
620 Axis::Y => region.rect.y += sign * span,
621 }
622 }
623 }
624 spatial_focus_next(&chain, ®ions, Some(current), dir.focus_direction())
625 }
626
627 fn span(&self, entries: &[NavEntry], axis: Axis) -> f32 {
629 let (min, max) = match axis {
630 Axis::X => (
631 entries.iter().map(|entry| entry.rect.x).fold(f32::INFINITY, f32::min),
632 entries
633 .iter()
634 .map(|entry| entry.rect.x + entry.rect.w)
635 .fold(f32::NEG_INFINITY, f32::max),
636 ),
637 Axis::Y => (
638 entries.iter().map(|entry| entry.rect.y).fold(f32::INFINITY, f32::min),
639 entries
640 .iter()
641 .map(|entry| entry.rect.y + entry.rect.h)
642 .fold(f32::NEG_INFINITY, f32::max),
643 ),
644 };
645 max - min
646 }
647}
648
649fn regions_of(entries: &[NavEntry]) -> (Vec<u64>, Vec<HitRegion>) {
650 let chain = entries.iter().map(NavEntry::id).collect();
651 let regions = entries
652 .iter()
653 .map(|entry| HitRegion {
654 id: entry.id,
655 rect: entry.rect,
656 focusable: true,
657 focus_group_id: entry.group.map(GroupId::raw),
658 ..HitRegion::default()
659 })
660 .collect();
661 (chain, regions)
662}
663
664fn is_ahead(rect: Rect, entries: &[NavEntry], current: u64, axis: Axis, sign: f32) -> bool {
667 let Some(focused) = entries.iter().find(|entry| entry.id == current) else {
668 return false;
669 };
670 let (ahead, focus) = match axis {
671 Axis::X => (rect.x + rect.w / 2.0, focused.rect.x + focused.rect.w / 2.0),
672 Axis::Y => (rect.y + rect.h / 2.0, focused.rect.y + focused.rect.h / 2.0),
673 };
674 (ahead - focus) * sign > 0.0
675}
676
677#[derive(Clone)]
680pub struct NavDriver {
681 map: NavMap,
682 wrap: Wrap,
683}
684
685impl NavDriver {
686 pub fn new(map: NavMap) -> Self {
687 Self {
688 map,
689 wrap: Wrap::default(),
690 }
691 }
692
693 pub fn with_wrap(mut self, wrap: Wrap) -> Self {
694 self.wrap = wrap;
695 self
696 }
697
698 pub fn wrap(&self) -> Wrap {
699 self.wrap
700 }
701
702 pub fn map(&self) -> &NavMap {
703 &self.map
704 }
705
706 pub fn drive(&self, sched: &Scheduler, dir: NavDir) -> NavStep {
715 let step = self.map.resolve(sched.focused, dir, self.wrap);
716 match step.target() {
717 Some(id) if self.map.request(id) => step,
718 Some(id) => {
719 if self.map.release_stale_capture() && self.map.request(id) {
720 step
721 } else {
722 NavStep::Held
723 }
724 }
725 None => step,
726 }
727 }
728
729 pub fn enter(&self, sched: &Scheduler) -> NavStep {
735 if let Some(id) = sched.focused.filter(|id| self.map.contains(*id)) {
736 return NavStep::Already(id);
737 }
738 self.drive(sched, NavDir::Next)
739 }
740}
741
742#[derive(Clone, Copy, Debug)]
750pub struct StickNav {
751 threshold: f32,
753 first_delay: f32,
755 interval: f32,
757 held: Option<NavDir>,
758 timer: f32,
759}
760
761impl Default for StickNav {
762 fn default() -> Self {
763 Self::new()
764 }
765}
766
767impl StickNav {
768 pub fn new() -> Self {
769 Self {
770 threshold: 0.55,
771 first_delay: 0.35,
772 interval: 0.14,
773 held: None,
774 timer: 0.0,
775 }
776 }
777
778 pub fn with_cadence(mut self, threshold: f32, first_delay: f32, interval: f32) -> Self {
784 self.threshold = if threshold.is_finite() {
785 threshold.clamp(0.05, 1.0)
786 } else {
787 0.55
788 };
789 self.first_delay = if first_delay.is_finite() {
790 first_delay.max(0.0)
791 } else {
792 0.35
793 };
794 self.interval = if interval.is_finite() {
795 interval.max(1.0 / 240.0)
796 } else {
797 0.14
798 };
799 self
800 }
801
802 pub fn held(&self) -> Option<NavDir> {
804 self.held
805 }
806
807 pub fn release(&mut self) {
810 self.held = None;
811 self.timer = 0.0;
812 }
813
814 pub fn step(&mut self, raw: Vec2, dt_secs: f32) -> Option<NavDir> {
819 let dir = direction_of(repame_input::apply_stick(raw), self.threshold);
820 match (self.held, dir) {
821 (Some(held), Some(dir)) if held == dir => {
822 self.timer -= dt_secs.max(0.0);
823 if self.timer > 0.0 {
824 return None;
825 }
826 self.timer = self.interval;
829 Some(dir)
830 }
831 (_, None) => {
832 self.held = None;
833 self.timer = 0.0;
834 None
835 }
836 (_, Some(dir)) => {
837 self.held = Some(dir);
838 self.timer = self.first_delay;
839 Some(dir)
840 }
841 }
842 }
843}
844
845fn direction_of(stick: Vec2, threshold: f32) -> Option<NavDir> {
846 if stick.length() < threshold {
847 return None;
848 }
849 if stick.x.abs() >= stick.y.abs() {
850 Some(if stick.x > 0.0 { NavDir::Right } else { NavDir::Left })
851 } else {
852 Some(if stick.y > 0.0 { NavDir::Down } else { NavDir::Up })
853 }
854}
855
856pub fn dpad_dirs(pad: &repame_input::GamepadState) -> Vec<NavDir> {
858 let mut dirs = Vec::new();
859 if pad.dpad_left_pressed {
860 dirs.push(NavDir::Left);
861 }
862 if pad.dpad_right_pressed {
863 dirs.push(NavDir::Right);
864 }
865 if pad.dpad_up_pressed {
866 dirs.push(NavDir::Up);
867 }
868 if pad.dpad_down_pressed {
869 dirs.push(NavDir::Down);
870 }
871 dirs
872}
873
874pub fn pad_confirm(pad: &repame_input::GamepadState) -> bool {
880 pad.south_pressed
881}
882
883pub fn chain_of(map: &NavMap) -> Vec<u64> {
885 map.active().iter().map(NavEntry::id).collect()
886}
887
888pub fn same_group(map: &NavMap, anchor: u64, id: u64) -> bool {
890 let entries = map.active();
891 let group_of =
892 |id: u64| entries.iter().find(|entry| entry.id == id).and_then(NavEntry::group);
893 match (group_of(anchor), group_of(id)) {
894 (Some(a), Some(b)) => a == b,
895 _ => false,
896 }
897}
898
899pub fn resolve_in(
902 chain: &[u64],
903 regions: &[HitRegion],
904 from: Option<u64>,
905 dir: NavDir,
906) -> Option<u64> {
907 spatial_focus_next(chain, regions, from, dir.focus_direction())
908}
909
910pub fn chain_step(chain: &[u64], from: Option<u64>, dir: NavDir) -> Option<u64> {
912 if chain.is_empty() {
913 return None;
914 }
915 let mut manager = FocusManager::new(chain.to_vec(), from);
916 manager.move_tab(dir == NavDir::Previous)
917}
918
919#[cfg(test)]
920mod tests {
921 use super::*;
922
923 fn grid() -> NavMap {
925 let map = NavMap::new();
926 map.begin_frame();
927 for row in 0..2u64 {
928 for column in 0..3u64 {
929 let id = row * 10 + column + 1;
930 let requester = FocusRequester::new();
931 *requester.target.borrow_mut() = Some(id);
932 map.mark(id, None, requester.clone());
933 map.place(
934 &requester,
935 Rect {
936 x: column as f32 * 210.0,
937 y: row as f32 * 110.0,
938 w: 200.0,
939 h: 100.0,
940 },
941 None,
942 );
943 }
944 }
945 map
946 }
947
948 fn ids(map: &NavMap) -> Vec<u64> {
949 chain_of(map)
950 }
951
952 fn drain_requests() {
953 while repose_core::runtime::take_focus_request().is_some() {}
954 }
955
956 #[test]
957 fn controls_are_navigable_in_composition_order() {
958 let map = grid();
959 assert_eq!(ids(&map), [1, 2, 3, 11, 12, 13]);
960 assert_eq!(map.first(), Some(1));
961 assert_eq!(map.len(), 6);
962 }
963
964 #[test]
965 fn nothing_focused_enters_at_the_first_control() {
966 let map = grid();
967 assert_eq!(map.resolve(None, NavDir::Right, Wrap::Stay), NavStep::Entered(1));
968 assert_eq!(map.resolve(None, NavDir::Next, Wrap::Stay), NavStep::Entered(1));
969 assert_eq!(map.resolve(None, NavDir::Up, Wrap::Stay), NavStep::Entered(1));
970 }
971
972 #[test]
973 fn focus_outside_the_map_enters_instead_of_stalling() {
974 let map = grid();
975 assert_eq!(
978 map.resolve(Some(999), NavDir::Right, Wrap::Stay),
979 NavStep::Entered(1)
980 );
981 }
982
983 #[test]
984 fn spatial_moves_follow_the_layout() {
985 let map = grid();
986 assert_eq!(map.resolve(Some(1), NavDir::Right, Wrap::Stay), NavStep::Moved(2));
987 assert_eq!(map.resolve(Some(2), NavDir::Right, Wrap::Stay), NavStep::Moved(3));
988 assert_eq!(map.resolve(Some(1), NavDir::Down, Wrap::Stay), NavStep::Moved(11));
989 assert_eq!(map.resolve(Some(12), NavDir::Up, Wrap::Stay), NavStep::Moved(2));
990 assert_eq!(map.resolve(Some(3), NavDir::Left, Wrap::Stay), NavStep::Moved(2));
991 assert_eq!(map.resolve(Some(11), NavDir::Right, Wrap::Stay), NavStep::Moved(12));
992 }
993
994 #[test]
995 fn the_layout_edge_stays_put_when_wrap_is_stay() {
996 let map = grid();
997 assert_eq!(map.resolve(Some(3), NavDir::Right, Wrap::Stay), NavStep::Edge);
998 assert_eq!(map.resolve(Some(1), NavDir::Left, Wrap::Stay), NavStep::Edge);
999 assert_eq!(map.resolve(Some(1), NavDir::Up, Wrap::Stay), NavStep::Edge);
1000 assert_eq!(map.resolve(Some(11), NavDir::Down, Wrap::Stay), NavStep::Edge);
1001 }
1002
1003 #[test]
1004 fn horizontal_wrap_stays_in_the_nearest_row() {
1005 let map = grid();
1006 assert_eq!(map.resolve(Some(3), NavDir::Right, Wrap::Cycle), NavStep::Moved(1));
1007 assert_eq!(map.resolve(Some(1), NavDir::Left, Wrap::Cycle), NavStep::Moved(3));
1008 assert_eq!(map.resolve(Some(13), NavDir::Right, Wrap::Cycle), NavStep::Moved(11));
1009 assert_eq!(map.resolve(Some(11), NavDir::Left, Wrap::Cycle), NavStep::Moved(13));
1010 assert_eq!(map.resolve(Some(2), NavDir::Left, Wrap::Cycle), NavStep::Moved(1));
1013 }
1014
1015 #[test]
1016 fn vertical_wrap_stays_in_the_column() {
1017 let map = grid();
1018 assert_eq!(map.resolve(Some(1), NavDir::Up, Wrap::Cycle), NavStep::Moved(11));
1019 assert_eq!(map.resolve(Some(11), NavDir::Down, Wrap::Cycle), NavStep::Moved(1));
1020 assert_eq!(map.resolve(Some(13), NavDir::Down, Wrap::Cycle), NavStep::Moved(3));
1021 assert_eq!(map.resolve(Some(3), NavDir::Up, Wrap::Cycle), NavStep::Moved(13));
1022 }
1023
1024 #[test]
1025 fn a_single_control_has_nowhere_to_go() {
1026 let map = NavMap::new();
1027 map.begin_frame();
1028 let requester = FocusRequester::new();
1029 *requester.target.borrow_mut() = Some(7);
1030 map.mark(7, None, requester.clone());
1031 map.place(&requester, Rect { x: 0.0, y: 0.0, w: 100.0, h: 40.0 }, None);
1032 assert_eq!(map.resolve(Some(7), NavDir::Right, Wrap::Cycle), NavStep::Edge);
1033 assert_eq!(map.resolve(Some(7), NavDir::Down, Wrap::Cycle), NavStep::Edge);
1034 assert_eq!(map.resolve(Some(7), NavDir::Next, Wrap::Cycle), NavStep::Edge);
1035 assert_eq!(map.resolve(Some(7), NavDir::Previous, Wrap::Cycle), NavStep::Edge);
1036 }
1037
1038 #[test]
1039 fn chain_order_wraps_for_tab_and_shift_tab() {
1040 let map = grid();
1041 assert_eq!(map.resolve(Some(1), NavDir::Next, Wrap::Stay), NavStep::Moved(2));
1042 assert_eq!(map.resolve(Some(13), NavDir::Next, Wrap::Stay), NavStep::Moved(1));
1043 assert_eq!(map.resolve(Some(1), NavDir::Previous, Wrap::Stay), NavStep::Moved(13));
1044 assert_eq!(map.resolve(Some(2), NavDir::Previous, Wrap::Stay), NavStep::Moved(1));
1045 }
1046
1047 #[test]
1048 fn a_focus_group_traps_traversal() {
1049 let map = NavMap::new();
1050 map.begin_frame();
1051 let pause = GroupId::named("pause");
1052 for (id, x, group) in [
1054 (1u64, 0.0f32, None),
1055 (2, 300.0, Some(pause)),
1056 (3, 600.0, Some(pause)),
1057 ] {
1058 let requester = FocusRequester::new();
1059 *requester.target.borrow_mut() = Some(id);
1060 map.mark(id, group, requester.clone());
1061 map.place(&requester, Rect { x, y: 0.0, w: 200.0, h: 100.0 }, group);
1062 }
1063 assert_eq!(map.resolve(Some(2), NavDir::Right, Wrap::Stay), NavStep::Moved(3));
1064 assert_eq!(
1065 map.resolve(Some(3), NavDir::Right, Wrap::Stay),
1066 NavStep::Edge,
1067 "focus does not escape the modal group"
1068 );
1069 assert_eq!(map.resolve(Some(3), NavDir::Next, Wrap::Stay), NavStep::Moved(2));
1070 assert!(same_group(&map, 2, 3));
1071 assert!(!same_group(&map, 1, 2));
1072 }
1073
1074 #[test]
1075 fn a_closed_panel_is_no_longer_navigable() {
1076 let map = grid();
1077 map.begin_frame();
1080 let requester = FocusRequester::new();
1081 *requester.target.borrow_mut() = Some(50);
1082 map.mark(50, None, requester.clone());
1083 map.place(&requester, Rect { x: 0.0, y: 0.0, w: 40.0, h: 40.0 }, None);
1084 assert!(ids(&map).contains(&1), "the previous layout is still resolvable");
1085
1086 map.begin_frame();
1088 assert_eq!(ids(&map), [50]);
1089 assert_eq!(
1090 map.resolve(Some(1), NavDir::Right, Wrap::Stay),
1091 NavStep::Entered(50)
1092 );
1093 }
1094
1095 #[test]
1096 fn an_empty_map_reports_empty() {
1097 let map = NavMap::new();
1098 map.begin_frame();
1099 assert_eq!(map.resolve(None, NavDir::Next, Wrap::Cycle), NavStep::Empty);
1100 assert_eq!(map.len(), 0);
1101 assert!(map.is_empty());
1102 assert!(!map.request(1));
1103 assert!(!map.capture(1));
1104 assert!(!map.release(1));
1105 }
1106
1107 #[test]
1108 fn an_unbound_control_is_not_navigable() {
1109 let map = NavMap::new();
1110 map.begin_frame();
1111 let requester = FocusRequester::new();
1112 assert!(!map.register(&requester, None), "no view id yet");
1113 assert!(!map.place(&requester, Rect { x: 0.0, y: 0.0, w: 10.0, h: 10.0 }, None));
1114 assert!(map.is_empty(), "an unbound rectangle is not a control yet");
1115 }
1116
1117 #[test]
1118 fn the_first_rectangle_is_bound_when_the_view_id_arrives() {
1119 let map = NavMap::new();
1120 let requester = FocusRequester::new();
1121 let rect = Rect { x: 30.0, y: 40.0, w: 200.0, h: 60.0 };
1122
1123 map.begin_frame();
1125 assert!(!map.place(&requester, rect, None));
1126 assert!(!map.register(&requester, None));
1127 assert!(map.is_empty());
1128
1129 map.begin_frame();
1131 *requester.target.borrow_mut() = Some(4);
1132 assert!(map.place(&requester, rect, None));
1133 assert!(map.register(&requester, None));
1134 assert_eq!(ids(&map), [4]);
1135 assert_eq!(map.active()[0].rect(), rect);
1136
1137 map.begin_frame();
1139 assert!(map.register(&requester, None));
1140 assert_eq!(map.active()[0].rect(), rect);
1141 let moved = Rect { x: 60.0, y: 40.0, w: 200.0, h: 60.0 };
1142 assert!(map.place(&requester, moved, None));
1143 assert_eq!(map.active()[0].rect(), moved);
1144 }
1145
1146 #[test]
1147 fn a_pending_rectangle_survives_the_frames_a_panel_skips() {
1148 let map = NavMap::new();
1149 let requester = FocusRequester::new();
1150 let rect = Rect { x: 10.0, y: 20.0, w: 30.0, h: 40.0 };
1151 map.begin_frame();
1152 assert!(!map.place(&requester, rect, None));
1153 map.begin_frame();
1157 map.begin_frame();
1158 *requester.target.borrow_mut() = Some(9);
1159 assert!(map.register(&requester, None));
1160 assert_eq!(ids(&map), [9]);
1161 assert_eq!(map.active()[0].rect(), rect, "the held rectangle was bound");
1162 }
1163
1164 #[test]
1165 fn a_place_without_a_group_keeps_the_one_the_control_has() {
1166 let map = NavMap::new();
1167 map.begin_frame();
1168 let pause = GroupId::named("pause");
1169 let requester = FocusRequester::new();
1170 *requester.target.borrow_mut() = Some(5);
1171 map.mark(5, Some(pause), requester.clone());
1172 map.place(&requester, Rect { x: 0.0, y: 0.0, w: 200.0, h: 40.0 }, None);
1173 assert_eq!(
1174 map.active()[0].group(),
1175 Some(pause),
1176 "a rectangle-only place must not clear the modal trap"
1177 );
1178 }
1179
1180 #[test]
1181 fn requesting_focus_queues_the_platform_request() {
1182 drain_requests();
1183 let map = grid();
1184 assert!(map.request(2));
1185 assert!(!map.request(99), "an unknown control is not requested");
1186 assert_eq!(
1187 repose_core::runtime::take_focus_request(),
1188 Some(2),
1189 "the request reaches the platform's queue"
1190 );
1191 drain_requests();
1192 }
1193
1194 #[test]
1195 fn a_captured_modal_refuses_other_targets() {
1196 drain_requests();
1197 let map = grid();
1198 assert!(map.capture(2));
1199 assert_eq!(map.resolve(Some(2), NavDir::Left, Wrap::Stay), NavStep::Moved(1));
1200 let driver = NavDriver::new(map.clone());
1201 assert_eq!(
1202 driver.drive(&scheduler_focused_on(2), NavDir::Left),
1203 NavStep::Held,
1204 "focus is pinned to the captured control"
1205 );
1206 assert!(map.release(2));
1207 drain_requests();
1208 }
1209
1210 #[test]
1211 fn a_capture_can_be_released_after_its_control_retires() {
1212 drain_requests();
1213 let map = NavMap::new();
1214 map.begin_frame();
1215 let modal = FocusRequester::new();
1216 *modal.target.borrow_mut() = Some(2);
1217 map.mark(2, None, modal.clone());
1218 map.place(&modal, Rect { x: 0.0, y: 0.0, w: 200.0, h: 40.0 }, None);
1219 assert!(map.capture(2));
1220 assert_eq!(repose_core::runtime::focus_capture(), Some(2));
1221
1222 map.begin_frame();
1225 map.begin_frame();
1226 assert!(map.is_empty(), "the panel retired");
1227 assert_eq!(repose_core::runtime::focus_capture(), Some(2));
1228
1229 assert!(
1230 map.release(2),
1231 "the remembered capture is freed with the entry gone"
1232 );
1233 assert_eq!(repose_core::runtime::focus_capture(), None);
1234 drain_requests();
1235 }
1236
1237 #[test]
1238 fn a_held_move_clears_a_capture_left_by_a_retired_control() {
1239 drain_requests();
1240 let map = NavMap::new();
1241 map.begin_frame();
1242 let modal = FocusRequester::new();
1243 *modal.target.borrow_mut() = Some(2);
1244 map.mark(2, None, modal.clone());
1245 map.place(&modal, Rect { x: 0.0, y: 0.0, w: 200.0, h: 40.0 }, None);
1246 assert!(map.capture(2));
1247
1248 map.begin_frame();
1249 map.begin_frame();
1250 let menu = FocusRequester::new();
1251 *menu.target.borrow_mut() = Some(7);
1252 map.mark(7, None, menu.clone());
1253 map.place(&menu, Rect { x: 300.0, y: 0.0, w: 200.0, h: 40.0 }, None);
1254
1255 let driver = NavDriver::new(map);
1258 assert_eq!(
1259 driver.enter(&Scheduler::default()),
1260 NavStep::Entered(7),
1261 "the stale lock is cleared and the request goes through"
1262 );
1263 assert_eq!(repose_core::runtime::focus_capture(), None);
1264 drain_requests();
1265 }
1266
1267 fn scheduler_focused_on(id: u64) -> Scheduler {
1268 let mut sched = Scheduler::default();
1269 sched.focused = Some(id);
1270 sched
1271 }
1272
1273 #[test]
1274 fn the_driver_enters_and_drives_against_a_live_scheduler() {
1275 drain_requests();
1276 let driver = NavDriver::new(grid()).with_wrap(Wrap::Stay);
1277 let mut sched = Scheduler::default();
1278
1279 assert_eq!(driver.enter(&sched), NavStep::Entered(1));
1280 sched.focused = Some(1);
1282 assert_eq!(driver.enter(&sched), NavStep::Already(1));
1283 assert_eq!(driver.drive(&sched, NavDir::Right), NavStep::Moved(2));
1284
1285 sched.focused = Some(2);
1286 assert_eq!(driver.drive(&sched, NavDir::Down), NavStep::Moved(12));
1287
1288 sched.focused = Some(12);
1289 assert_eq!(driver.drive(&sched, NavDir::Left), NavStep::Moved(11));
1290 sched.focused = Some(11);
1291 assert_eq!(driver.drive(&sched, NavDir::Up), NavStep::Moved(1));
1292 assert_eq!(NavStep::Moved(1).target(), Some(1));
1293 assert_eq!(NavStep::Edge.target(), None);
1294 drain_requests();
1295 }
1296
1297 #[test]
1298 fn stick_nav_ignores_rest_and_small_deflection() {
1299 let mut nav = StickNav::new();
1300 assert_eq!(nav.step(Vec2::ZERO, 0.1), None);
1301 assert_eq!(nav.step(Vec2::new(0.15, 0.0), 0.1), None, "inside the dead zone");
1302 assert_eq!(nav.step(Vec2::new(0.35, 0.0), 0.1), None, "below the press threshold");
1303 assert_eq!(nav.held(), None);
1304 }
1305
1306 #[test]
1307 fn stick_nav_presses_once_then_repeats() {
1308 let mut nav = StickNav::new().with_cadence(0.5, 0.35, 0.14);
1309 assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.016), Some(NavDir::Right));
1310 for _ in 0..20 {
1311 assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.016), None);
1312 }
1313 assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.05), Some(NavDir::Right));
1314 assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.02), None);
1315 assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.14), Some(NavDir::Right));
1316 assert_eq!(nav.step(Vec2::ZERO, 0.5), None);
1317 assert_eq!(nav.held(), None);
1318 assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.016), Some(NavDir::Right));
1319 }
1320
1321 #[test]
1322 fn stick_nav_picks_a_side_and_reverses_on_change() {
1323 let mut nav = StickNav::new();
1324 assert_eq!(nav.step(Vec2::new(0.0, -1.0), 0.016), Some(NavDir::Up));
1325 assert_eq!(nav.step(Vec2::new(0.0, 1.0), 0.016), Some(NavDir::Down));
1326 assert_eq!(nav.step(Vec2::new(-1.0, 0.0), 0.016), Some(NavDir::Left));
1327 assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.016), Some(NavDir::Right));
1328 assert_eq!(nav.step(Vec2::ZERO, 0.016), None);
1330 assert_eq!(nav.step(Vec2::new(1.0, 0.6), 0.016), Some(NavDir::Right));
1331 assert_eq!(nav.step(Vec2::ZERO, 0.016), None);
1332 assert_eq!(nav.step(Vec2::new(0.2, 1.0), 0.016), Some(NavDir::Down));
1333 }
1334
1335 #[test]
1336 fn stick_nav_release_forgets_the_held_direction() {
1337 let mut nav = StickNav::new();
1338 assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.016), Some(NavDir::Right));
1339 nav.release();
1340 assert_eq!(nav.held(), None);
1341 assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.016), Some(NavDir::Right));
1342 }
1343
1344 #[test]
1345 fn a_mis_set_cadence_is_clamped_into_a_sane_band() {
1346 let mut nav = StickNav::new().with_cadence(9.0, -5.0, 0.0);
1347 assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.0), Some(NavDir::Right));
1348 assert_eq!(
1349 nav.step(Vec2::new(1.0, 0.0), 0.0),
1350 Some(NavDir::Right),
1351 "the repeat interval clamps to one step at 240 Hz, so a held stick repeats"
1352 );
1353 assert_eq!(
1354 nav.step(Vec2::new(0.9, 0.0), 0.0),
1355 None,
1356 "the threshold clamps to full deflection"
1357 );
1358 }
1359
1360 #[test]
1361 fn dpad_edges_map_straight_through() {
1362 let mut pad = repame_input::GamepadState::default();
1363 assert!(dpad_dirs(&pad).is_empty());
1364 assert!(!pad_confirm(&pad));
1365 pad.dpad_left_pressed = true;
1366 pad.dpad_up_pressed = true;
1367 pad.south_pressed = true;
1368 assert_eq!(dpad_dirs(&pad), [NavDir::Left, NavDir::Up]);
1369 assert!(pad_confirm(&pad));
1370 }
1371
1372 #[test]
1373 fn group_ids_are_stable_and_name_derived() {
1374 assert_eq!(GroupId::named("pause"), GroupId::named("pause"));
1375 assert_ne!(GroupId::named("pause"), GroupId::named("death"));
1376 assert_eq!(GroupId::from_raw(7).raw(), 7);
1377 assert_eq!(GroupId::named("").raw(), 0xcbf2_9ce4_8422_2325);
1379 }
1380
1381 #[test]
1382 fn directions_round_trip() {
1383 for dir in NavDir::ALL {
1384 assert_eq!(dir.reverse().reverse(), dir);
1385 }
1386 assert_eq!(NavDir::Next.reverse(), NavDir::Previous);
1387 assert_eq!(NavDir::Previous.reverse(), NavDir::Next);
1388 assert_eq!(NavDir::Left.reverse(), NavDir::Right);
1389 assert_eq!(NavDir::Up.reverse(), NavDir::Down);
1390 assert!(NavDir::Next.is_chain());
1391 assert!(!NavDir::Down.is_chain());
1392 }
1393
1394 #[test]
1395 fn the_raw_repose_helpers_are_usable_without_a_map() {
1396 let chain = [1, 2, 3];
1397 let regions: Vec<HitRegion> = (0..3)
1398 .map(|index| HitRegion {
1399 id: index + 1,
1400 rect: Rect {
1401 x: index as f32 * 100.0,
1402 y: 0.0,
1403 w: 90.0,
1404 h: 40.0,
1405 },
1406 focusable: true,
1407 ..HitRegion::default()
1408 })
1409 .collect();
1410 assert_eq!(resolve_in(&chain, ®ions, Some(1), NavDir::Right), Some(2));
1411 assert_eq!(chain_step(&chain, Some(1), NavDir::Next), Some(2));
1412 assert_eq!(chain_step(&chain, Some(3), NavDir::Next), Some(1));
1413 assert_eq!(chain_step(&[], Some(1), NavDir::Next), None);
1414 }
1415}