use std::cell::RefCell;
use std::collections::HashMap;
use std::rc::Rc;
use glam::Vec2;
use repose_core::Rect;
use repose_core::prelude::{FocusDirection, FocusRequester, Scheduler};
use repose_core::runtime::{FocusManager, HitRegion, spatial_focus_next};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum NavDir {
Next,
Previous,
Left,
Right,
Up,
Down,
}
impl NavDir {
pub const ALL: [NavDir; 6] = [
NavDir::Next,
NavDir::Previous,
NavDir::Left,
NavDir::Right,
NavDir::Up,
NavDir::Down,
];
pub fn focus_direction(self) -> FocusDirection {
match self {
NavDir::Next => FocusDirection::Next,
NavDir::Previous => FocusDirection::Previous,
NavDir::Left => FocusDirection::Left,
NavDir::Right => FocusDirection::Right,
NavDir::Up => FocusDirection::Up,
NavDir::Down => FocusDirection::Down,
}
}
pub fn reverse(self) -> Self {
match self {
NavDir::Next => NavDir::Previous,
NavDir::Previous => NavDir::Next,
NavDir::Left => NavDir::Right,
NavDir::Right => NavDir::Left,
NavDir::Up => NavDir::Down,
NavDir::Down => NavDir::Up,
}
}
pub fn is_chain(self) -> bool {
matches!(self, NavDir::Next | NavDir::Previous)
}
fn axis(self) -> Option<(Axis, f32)> {
match self {
NavDir::Left => Some((Axis::X, -1.0)),
NavDir::Right => Some((Axis::X, 1.0)),
NavDir::Up => Some((Axis::Y, -1.0)),
NavDir::Down => Some((Axis::Y, 1.0)),
NavDir::Next | NavDir::Previous => None,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
enum Axis {
X,
Y,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, PartialOrd, Ord)]
pub struct GroupId(u64);
impl GroupId {
pub fn named(tag: &str) -> Self {
let mut hash: u64 = 0xcbf2_9ce4_8422_2325;
for byte in tag.as_bytes() {
hash ^= u64::from(*byte);
hash = hash.wrapping_mul(0x0000_0100_0000_01B3);
}
Self(hash)
}
pub const fn from_raw(raw: u64) -> Self {
Self(raw)
}
pub const fn raw(self) -> u64 {
self.0
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum Wrap {
Stay,
#[default]
Cycle,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum NavStep {
Moved(u64),
Entered(u64),
Already(u64),
Edge,
Held,
Empty,
}
impl NavStep {
pub fn target(self) -> Option<u64> {
match self {
NavStep::Moved(id) | NavStep::Entered(id) | NavStep::Already(id) => Some(id),
NavStep::Edge | NavStep::Held | NavStep::Empty => None,
}
}
}
#[derive(Clone)]
pub struct NavEntry {
id: u64,
rect: Rect,
group: Option<GroupId>,
requester: FocusRequester,
}
impl NavEntry {
pub fn id(&self) -> u64 {
self.id
}
pub fn rect(&self) -> Rect {
self.rect
}
pub fn group(&self) -> Option<GroupId> {
self.group
}
pub fn request_focus(&self) -> bool {
self.requester.try_request_focus()
}
}
struct Entry {
rect: Option<Rect>,
group: Option<GroupId>,
requester: FocusRequester,
frame: u64,
}
struct Pending {
key: usize,
rect: Rect,
}
const PENDING_CAP: usize = 256;
fn requester_key(requester: &FocusRequester) -> usize {
Rc::as_ptr(&requester.target) as usize
}
struct Capture {
id: u64,
requester: FocusRequester,
}
#[derive(Default)]
struct Registry {
frame: u64,
order: Vec<u64>,
entries: HashMap<u64, Entry>,
pending: Vec<Pending>,
capture: Option<Capture>,
}
#[derive(Clone, Default)]
pub struct NavMap {
registry: Rc<RefCell<Registry>>,
}
impl NavMap {
pub fn new() -> Self {
Self::default()
}
pub fn begin_frame(&self) {
let mut registry = self.registry.borrow_mut();
registry.frame += 1;
let frame = registry.frame;
registry.entries.retain(|_, entry| entry.frame + 1 >= frame);
let mut order = std::mem::take(&mut registry.order);
order.retain(|id| registry.entries.contains_key(id));
registry.order = order;
}
pub fn clear(&self) {
let mut registry = self.registry.borrow_mut();
registry.entries.clear();
registry.order.clear();
registry.pending.clear();
}
pub fn register(&self, requester: &FocusRequester, group: Option<GroupId>) -> bool {
let Some(id) = *requester.target.borrow() else {
return false;
};
let mut registry = self.registry.borrow_mut();
let frame = registry.frame;
let key = requester_key(requester);
let rect = match registry.pending.iter().position(|pending| pending.key == key) {
Some(index) => Some(registry.pending.remove(index).rect),
None => None,
};
match registry.entries.get_mut(&id) {
Some(entry) => {
entry.group = group;
entry.requester = requester.clone();
entry.frame = frame;
if let Some(rect) = rect {
entry.rect = Some(rect);
}
}
None => {
registry.order.push(id);
registry.entries.insert(
id,
Entry {
rect,
group,
requester: requester.clone(),
frame,
},
);
}
}
true
}
pub fn mark(&self, id: u64, group: Option<GroupId>, requester: FocusRequester) {
let mut registry = self.registry.borrow_mut();
let frame = registry.frame;
match registry.entries.get_mut(&id) {
Some(entry) => {
entry.group = group;
entry.requester = requester;
entry.frame = frame;
}
None => {
registry.order.push(id);
registry.entries.insert(
id,
Entry {
rect: None,
group,
requester,
frame,
},
);
}
}
}
pub fn place(&self, requester: &FocusRequester, rect: Rect, group: Option<GroupId>) -> bool {
let mut registry = self.registry.borrow_mut();
let frame = registry.frame;
let Some(id) = *requester.target.borrow() else {
let key = requester_key(requester);
match registry.pending.iter_mut().find(|pending| pending.key == key) {
Some(pending) => pending.rect = rect,
None => {
if registry.pending.len() >= PENDING_CAP {
registry.pending.remove(0);
}
registry.pending.push(Pending { key, rect });
}
}
return false;
};
match registry.entries.get_mut(&id) {
Some(entry) => {
entry.rect = Some(rect);
if group.is_some() {
entry.group = group;
}
entry.frame = frame;
}
None => {
registry.order.push(id);
registry.entries.insert(
id,
Entry {
rect: Some(rect),
group,
requester: requester.clone(),
frame,
},
);
}
}
let key = requester_key(requester);
registry.pending.retain(|pending| pending.key != key);
true
}
pub fn active(&self) -> Vec<NavEntry> {
let registry = self.registry.borrow();
registry
.order
.iter()
.filter_map(|id| {
let entry = registry.entries.get(id)?;
Some(NavEntry {
id: *id,
rect: entry.rect?,
group: entry.group,
requester: entry.requester.clone(),
})
})
.collect()
}
pub fn first(&self) -> Option<u64> {
self.active().first().map(NavEntry::id)
}
pub fn len(&self) -> usize {
self.active().len()
}
pub fn is_empty(&self) -> bool {
self.active().is_empty()
}
pub fn contains(&self, id: u64) -> bool {
self.active().iter().any(|entry| entry.id() == id)
}
pub fn request(&self, id: u64) -> bool {
self.entry(id).is_some_and(|entry| entry.request_focus())
}
pub fn capture(&self, id: u64) -> bool {
let Some(entry) = self.entry(id) else {
return false;
};
let requester = entry.requester;
if !requester.capture_focus() {
return false;
}
self.registry.borrow_mut().capture = Some(Capture { id, requester });
true
}
pub fn release(&self, id: u64) -> bool {
let mut registry = self.registry.borrow_mut();
if registry.capture.as_ref().is_some_and(|capture| capture.id == id) {
let capture = registry.capture.take();
if let Some(capture) = capture {
capture.requester.free_focus();
}
return true;
}
match registry.entries.get(&id) {
Some(entry) if entry.rect.is_some() => {
entry.requester.free_focus();
true
}
_ => false,
}
}
fn release_stale_capture(&self) -> bool {
let mut registry = self.registry.borrow_mut();
let Some(capture) = registry.capture.as_ref() else {
return false;
};
let id = capture.id;
if registry
.entries
.get(&id)
.is_some_and(|entry| entry.rect.is_some())
{
return false;
}
let capture = registry.capture.take();
let Some(capture) = capture else {
return false;
};
if repose_core::runtime::focus_capture() == Some(id) {
capture.requester.free_focus();
return true;
}
false
}
fn entry(&self, id: u64) -> Option<NavEntry> {
self.active().into_iter().find(|entry| entry.id == id)
}
pub fn resolve(&self, from: Option<u64>, dir: NavDir, wrap: Wrap) -> NavStep {
let entries = self.active();
if entries.is_empty() {
return NavStep::Empty;
}
let Some(current) = from.filter(|id| entries.iter().any(|entry| entry.id == *id)) else {
return NavStep::Entered(entries[0].id);
};
let (chain, regions) = regions_of(&entries);
let step = |next: u64| {
if next == current {
NavStep::Edge
} else {
NavStep::Moved(next)
}
};
if let Some(next) = spatial_focus_next(&chain, ®ions, Some(current), dir.focus_direction())
{
return step(next);
}
if dir.is_chain() {
return NavStep::Edge;
}
match wrap {
Wrap::Stay => NavStep::Edge,
Wrap::Cycle => self.wrapped(&entries, current, dir).map_or(NavStep::Edge, step),
}
}
fn wrapped(&self, entries: &[NavEntry], current: u64, dir: NavDir) -> Option<u64> {
let (axis, sign) = dir.axis()?;
let span = self.span(entries, axis);
if span <= 0.0 {
return None;
}
let (chain, mut regions) = regions_of(entries);
for (region, entry) in regions.iter_mut().zip(entries) {
if entry.id != current && !is_ahead(entry.rect, entries, current, axis, sign) {
match axis {
Axis::X => region.rect.x += sign * span,
Axis::Y => region.rect.y += sign * span,
}
}
}
spatial_focus_next(&chain, ®ions, Some(current), dir.focus_direction())
}
fn span(&self, entries: &[NavEntry], axis: Axis) -> f32 {
let (min, max) = match axis {
Axis::X => (
entries.iter().map(|entry| entry.rect.x).fold(f32::INFINITY, f32::min),
entries
.iter()
.map(|entry| entry.rect.x + entry.rect.w)
.fold(f32::NEG_INFINITY, f32::max),
),
Axis::Y => (
entries.iter().map(|entry| entry.rect.y).fold(f32::INFINITY, f32::min),
entries
.iter()
.map(|entry| entry.rect.y + entry.rect.h)
.fold(f32::NEG_INFINITY, f32::max),
),
};
max - min
}
}
fn regions_of(entries: &[NavEntry]) -> (Vec<u64>, Vec<HitRegion>) {
let chain = entries.iter().map(NavEntry::id).collect();
let regions = entries
.iter()
.map(|entry| HitRegion {
id: entry.id,
rect: entry.rect,
focusable: true,
focus_group_id: entry.group.map(GroupId::raw),
..HitRegion::default()
})
.collect();
(chain, regions)
}
fn is_ahead(rect: Rect, entries: &[NavEntry], current: u64, axis: Axis, sign: f32) -> bool {
let Some(focused) = entries.iter().find(|entry| entry.id == current) else {
return false;
};
let (ahead, focus) = match axis {
Axis::X => (rect.x + rect.w / 2.0, focused.rect.x + focused.rect.w / 2.0),
Axis::Y => (rect.y + rect.h / 2.0, focused.rect.y + focused.rect.h / 2.0),
};
(ahead - focus) * sign > 0.0
}
#[derive(Clone)]
pub struct NavDriver {
map: NavMap,
wrap: Wrap,
}
impl NavDriver {
pub fn new(map: NavMap) -> Self {
Self {
map,
wrap: Wrap::default(),
}
}
pub fn with_wrap(mut self, wrap: Wrap) -> Self {
self.wrap = wrap;
self
}
pub fn wrap(&self) -> Wrap {
self.wrap
}
pub fn map(&self) -> &NavMap {
&self.map
}
pub fn drive(&self, sched: &Scheduler, dir: NavDir) -> NavStep {
let step = self.map.resolve(sched.focused, dir, self.wrap);
match step.target() {
Some(id) if self.map.request(id) => step,
Some(id) => {
if self.map.release_stale_capture() && self.map.request(id) {
step
} else {
NavStep::Held
}
}
None => step,
}
}
pub fn enter(&self, sched: &Scheduler) -> NavStep {
if let Some(id) = sched.focused.filter(|id| self.map.contains(*id)) {
return NavStep::Already(id);
}
self.drive(sched, NavDir::Next)
}
}
#[derive(Clone, Copy, Debug)]
pub struct StickNav {
threshold: f32,
first_delay: f32,
interval: f32,
held: Option<NavDir>,
timer: f32,
}
impl Default for StickNav {
fn default() -> Self {
Self::new()
}
}
impl StickNav {
pub fn new() -> Self {
Self {
threshold: 0.55,
first_delay: 0.35,
interval: 0.14,
held: None,
timer: 0.0,
}
}
pub fn with_cadence(mut self, threshold: f32, first_delay: f32, interval: f32) -> Self {
self.threshold = if threshold.is_finite() {
threshold.clamp(0.05, 1.0)
} else {
0.55
};
self.first_delay = if first_delay.is_finite() {
first_delay.max(0.0)
} else {
0.35
};
self.interval = if interval.is_finite() {
interval.max(1.0 / 240.0)
} else {
0.14
};
self
}
pub fn held(&self) -> Option<NavDir> {
self.held
}
pub fn release(&mut self) {
self.held = None;
self.timer = 0.0;
}
pub fn step(&mut self, raw: Vec2, dt_secs: f32) -> Option<NavDir> {
let dir = direction_of(repame_input::apply_stick(raw), self.threshold);
match (self.held, dir) {
(Some(held), Some(dir)) if held == dir => {
self.timer -= dt_secs.max(0.0);
if self.timer > 0.0 {
return None;
}
self.timer = self.interval;
Some(dir)
}
(_, None) => {
self.held = None;
self.timer = 0.0;
None
}
(_, Some(dir)) => {
self.held = Some(dir);
self.timer = self.first_delay;
Some(dir)
}
}
}
}
fn direction_of(stick: Vec2, threshold: f32) -> Option<NavDir> {
if stick.length() < threshold {
return None;
}
if stick.x.abs() >= stick.y.abs() {
Some(if stick.x > 0.0 { NavDir::Right } else { NavDir::Left })
} else {
Some(if stick.y > 0.0 { NavDir::Down } else { NavDir::Up })
}
}
pub fn dpad_dirs(pad: &repame_input::GamepadState) -> Vec<NavDir> {
let mut dirs = Vec::new();
if pad.dpad_left_pressed {
dirs.push(NavDir::Left);
}
if pad.dpad_right_pressed {
dirs.push(NavDir::Right);
}
if pad.dpad_up_pressed {
dirs.push(NavDir::Up);
}
if pad.dpad_down_pressed {
dirs.push(NavDir::Down);
}
dirs
}
pub fn pad_confirm(pad: &repame_input::GamepadState) -> bool {
pad.south_pressed
}
pub fn chain_of(map: &NavMap) -> Vec<u64> {
map.active().iter().map(NavEntry::id).collect()
}
pub fn same_group(map: &NavMap, anchor: u64, id: u64) -> bool {
let entries = map.active();
let group_of =
|id: u64| entries.iter().find(|entry| entry.id == id).and_then(NavEntry::group);
match (group_of(anchor), group_of(id)) {
(Some(a), Some(b)) => a == b,
_ => false,
}
}
pub fn resolve_in(
chain: &[u64],
regions: &[HitRegion],
from: Option<u64>,
dir: NavDir,
) -> Option<u64> {
spatial_focus_next(chain, regions, from, dir.focus_direction())
}
pub fn chain_step(chain: &[u64], from: Option<u64>, dir: NavDir) -> Option<u64> {
if chain.is_empty() {
return None;
}
let mut manager = FocusManager::new(chain.to_vec(), from);
manager.move_tab(dir == NavDir::Previous)
}
#[cfg(test)]
mod tests {
use super::*;
fn grid() -> NavMap {
let map = NavMap::new();
map.begin_frame();
for row in 0..2u64 {
for column in 0..3u64 {
let id = row * 10 + column + 1;
let requester = FocusRequester::new();
*requester.target.borrow_mut() = Some(id);
map.mark(id, None, requester.clone());
map.place(
&requester,
Rect {
x: column as f32 * 210.0,
y: row as f32 * 110.0,
w: 200.0,
h: 100.0,
},
None,
);
}
}
map
}
fn ids(map: &NavMap) -> Vec<u64> {
chain_of(map)
}
fn drain_requests() {
while repose_core::runtime::take_focus_request().is_some() {}
}
#[test]
fn controls_are_navigable_in_composition_order() {
let map = grid();
assert_eq!(ids(&map), [1, 2, 3, 11, 12, 13]);
assert_eq!(map.first(), Some(1));
assert_eq!(map.len(), 6);
}
#[test]
fn nothing_focused_enters_at_the_first_control() {
let map = grid();
assert_eq!(map.resolve(None, NavDir::Right, Wrap::Stay), NavStep::Entered(1));
assert_eq!(map.resolve(None, NavDir::Next, Wrap::Stay), NavStep::Entered(1));
assert_eq!(map.resolve(None, NavDir::Up, Wrap::Stay), NavStep::Entered(1));
}
#[test]
fn focus_outside_the_map_enters_instead_of_stalling() {
let map = grid();
assert_eq!(
map.resolve(Some(999), NavDir::Right, Wrap::Stay),
NavStep::Entered(1)
);
}
#[test]
fn spatial_moves_follow_the_layout() {
let map = grid();
assert_eq!(map.resolve(Some(1), NavDir::Right, Wrap::Stay), NavStep::Moved(2));
assert_eq!(map.resolve(Some(2), NavDir::Right, Wrap::Stay), NavStep::Moved(3));
assert_eq!(map.resolve(Some(1), NavDir::Down, Wrap::Stay), NavStep::Moved(11));
assert_eq!(map.resolve(Some(12), NavDir::Up, Wrap::Stay), NavStep::Moved(2));
assert_eq!(map.resolve(Some(3), NavDir::Left, Wrap::Stay), NavStep::Moved(2));
assert_eq!(map.resolve(Some(11), NavDir::Right, Wrap::Stay), NavStep::Moved(12));
}
#[test]
fn the_layout_edge_stays_put_when_wrap_is_stay() {
let map = grid();
assert_eq!(map.resolve(Some(3), NavDir::Right, Wrap::Stay), NavStep::Edge);
assert_eq!(map.resolve(Some(1), NavDir::Left, Wrap::Stay), NavStep::Edge);
assert_eq!(map.resolve(Some(1), NavDir::Up, Wrap::Stay), NavStep::Edge);
assert_eq!(map.resolve(Some(11), NavDir::Down, Wrap::Stay), NavStep::Edge);
}
#[test]
fn horizontal_wrap_stays_in_the_nearest_row() {
let map = grid();
assert_eq!(map.resolve(Some(3), NavDir::Right, Wrap::Cycle), NavStep::Moved(1));
assert_eq!(map.resolve(Some(1), NavDir::Left, Wrap::Cycle), NavStep::Moved(3));
assert_eq!(map.resolve(Some(13), NavDir::Right, Wrap::Cycle), NavStep::Moved(11));
assert_eq!(map.resolve(Some(11), NavDir::Left, Wrap::Cycle), NavStep::Moved(13));
assert_eq!(map.resolve(Some(2), NavDir::Left, Wrap::Cycle), NavStep::Moved(1));
}
#[test]
fn vertical_wrap_stays_in_the_column() {
let map = grid();
assert_eq!(map.resolve(Some(1), NavDir::Up, Wrap::Cycle), NavStep::Moved(11));
assert_eq!(map.resolve(Some(11), NavDir::Down, Wrap::Cycle), NavStep::Moved(1));
assert_eq!(map.resolve(Some(13), NavDir::Down, Wrap::Cycle), NavStep::Moved(3));
assert_eq!(map.resolve(Some(3), NavDir::Up, Wrap::Cycle), NavStep::Moved(13));
}
#[test]
fn a_single_control_has_nowhere_to_go() {
let map = NavMap::new();
map.begin_frame();
let requester = FocusRequester::new();
*requester.target.borrow_mut() = Some(7);
map.mark(7, None, requester.clone());
map.place(&requester, Rect { x: 0.0, y: 0.0, w: 100.0, h: 40.0 }, None);
assert_eq!(map.resolve(Some(7), NavDir::Right, Wrap::Cycle), NavStep::Edge);
assert_eq!(map.resolve(Some(7), NavDir::Down, Wrap::Cycle), NavStep::Edge);
assert_eq!(map.resolve(Some(7), NavDir::Next, Wrap::Cycle), NavStep::Edge);
assert_eq!(map.resolve(Some(7), NavDir::Previous, Wrap::Cycle), NavStep::Edge);
}
#[test]
fn chain_order_wraps_for_tab_and_shift_tab() {
let map = grid();
assert_eq!(map.resolve(Some(1), NavDir::Next, Wrap::Stay), NavStep::Moved(2));
assert_eq!(map.resolve(Some(13), NavDir::Next, Wrap::Stay), NavStep::Moved(1));
assert_eq!(map.resolve(Some(1), NavDir::Previous, Wrap::Stay), NavStep::Moved(13));
assert_eq!(map.resolve(Some(2), NavDir::Previous, Wrap::Stay), NavStep::Moved(1));
}
#[test]
fn a_focus_group_traps_traversal() {
let map = NavMap::new();
map.begin_frame();
let pause = GroupId::named("pause");
for (id, x, group) in [
(1u64, 0.0f32, None),
(2, 300.0, Some(pause)),
(3, 600.0, Some(pause)),
] {
let requester = FocusRequester::new();
*requester.target.borrow_mut() = Some(id);
map.mark(id, group, requester.clone());
map.place(&requester, Rect { x, y: 0.0, w: 200.0, h: 100.0 }, group);
}
assert_eq!(map.resolve(Some(2), NavDir::Right, Wrap::Stay), NavStep::Moved(3));
assert_eq!(
map.resolve(Some(3), NavDir::Right, Wrap::Stay),
NavStep::Edge,
"focus does not escape the modal group"
);
assert_eq!(map.resolve(Some(3), NavDir::Next, Wrap::Stay), NavStep::Moved(2));
assert!(same_group(&map, 2, 3));
assert!(!same_group(&map, 1, 2));
}
#[test]
fn a_closed_panel_is_no_longer_navigable() {
let map = grid();
map.begin_frame();
let requester = FocusRequester::new();
*requester.target.borrow_mut() = Some(50);
map.mark(50, None, requester.clone());
map.place(&requester, Rect { x: 0.0, y: 0.0, w: 40.0, h: 40.0 }, None);
assert!(ids(&map).contains(&1), "the previous layout is still resolvable");
map.begin_frame();
assert_eq!(ids(&map), [50]);
assert_eq!(
map.resolve(Some(1), NavDir::Right, Wrap::Stay),
NavStep::Entered(50)
);
}
#[test]
fn an_empty_map_reports_empty() {
let map = NavMap::new();
map.begin_frame();
assert_eq!(map.resolve(None, NavDir::Next, Wrap::Cycle), NavStep::Empty);
assert_eq!(map.len(), 0);
assert!(map.is_empty());
assert!(!map.request(1));
assert!(!map.capture(1));
assert!(!map.release(1));
}
#[test]
fn an_unbound_control_is_not_navigable() {
let map = NavMap::new();
map.begin_frame();
let requester = FocusRequester::new();
assert!(!map.register(&requester, None), "no view id yet");
assert!(!map.place(&requester, Rect { x: 0.0, y: 0.0, w: 10.0, h: 10.0 }, None));
assert!(map.is_empty(), "an unbound rectangle is not a control yet");
}
#[test]
fn the_first_rectangle_is_bound_when_the_view_id_arrives() {
let map = NavMap::new();
let requester = FocusRequester::new();
let rect = Rect { x: 30.0, y: 40.0, w: 200.0, h: 60.0 };
map.begin_frame();
assert!(!map.place(&requester, rect, None));
assert!(!map.register(&requester, None));
assert!(map.is_empty());
map.begin_frame();
*requester.target.borrow_mut() = Some(4);
assert!(map.place(&requester, rect, None));
assert!(map.register(&requester, None));
assert_eq!(ids(&map), [4]);
assert_eq!(map.active()[0].rect(), rect);
map.begin_frame();
assert!(map.register(&requester, None));
assert_eq!(map.active()[0].rect(), rect);
let moved = Rect { x: 60.0, y: 40.0, w: 200.0, h: 60.0 };
assert!(map.place(&requester, moved, None));
assert_eq!(map.active()[0].rect(), moved);
}
#[test]
fn a_pending_rectangle_survives_the_frames_a_panel_skips() {
let map = NavMap::new();
let requester = FocusRequester::new();
let rect = Rect { x: 10.0, y: 20.0, w: 30.0, h: 40.0 };
map.begin_frame();
assert!(!map.place(&requester, rect, None));
map.begin_frame();
map.begin_frame();
*requester.target.borrow_mut() = Some(9);
assert!(map.register(&requester, None));
assert_eq!(ids(&map), [9]);
assert_eq!(map.active()[0].rect(), rect, "the held rectangle was bound");
}
#[test]
fn a_place_without_a_group_keeps_the_one_the_control_has() {
let map = NavMap::new();
map.begin_frame();
let pause = GroupId::named("pause");
let requester = FocusRequester::new();
*requester.target.borrow_mut() = Some(5);
map.mark(5, Some(pause), requester.clone());
map.place(&requester, Rect { x: 0.0, y: 0.0, w: 200.0, h: 40.0 }, None);
assert_eq!(
map.active()[0].group(),
Some(pause),
"a rectangle-only place must not clear the modal trap"
);
}
#[test]
fn requesting_focus_queues_the_platform_request() {
drain_requests();
let map = grid();
assert!(map.request(2));
assert!(!map.request(99), "an unknown control is not requested");
assert_eq!(
repose_core::runtime::take_focus_request(),
Some(2),
"the request reaches the platform's queue"
);
drain_requests();
}
#[test]
fn a_captured_modal_refuses_other_targets() {
drain_requests();
let map = grid();
assert!(map.capture(2));
assert_eq!(map.resolve(Some(2), NavDir::Left, Wrap::Stay), NavStep::Moved(1));
let driver = NavDriver::new(map.clone());
assert_eq!(
driver.drive(&scheduler_focused_on(2), NavDir::Left),
NavStep::Held,
"focus is pinned to the captured control"
);
assert!(map.release(2));
drain_requests();
}
#[test]
fn a_capture_can_be_released_after_its_control_retires() {
drain_requests();
let map = NavMap::new();
map.begin_frame();
let modal = FocusRequester::new();
*modal.target.borrow_mut() = Some(2);
map.mark(2, None, modal.clone());
map.place(&modal, Rect { x: 0.0, y: 0.0, w: 200.0, h: 40.0 }, None);
assert!(map.capture(2));
assert_eq!(repose_core::runtime::focus_capture(), Some(2));
map.begin_frame();
map.begin_frame();
assert!(map.is_empty(), "the panel retired");
assert_eq!(repose_core::runtime::focus_capture(), Some(2));
assert!(
map.release(2),
"the remembered capture is freed with the entry gone"
);
assert_eq!(repose_core::runtime::focus_capture(), None);
drain_requests();
}
#[test]
fn a_held_move_clears_a_capture_left_by_a_retired_control() {
drain_requests();
let map = NavMap::new();
map.begin_frame();
let modal = FocusRequester::new();
*modal.target.borrow_mut() = Some(2);
map.mark(2, None, modal.clone());
map.place(&modal, Rect { x: 0.0, y: 0.0, w: 200.0, h: 40.0 }, None);
assert!(map.capture(2));
map.begin_frame();
map.begin_frame();
let menu = FocusRequester::new();
*menu.target.borrow_mut() = Some(7);
map.mark(7, None, menu.clone());
map.place(&menu, Rect { x: 300.0, y: 0.0, w: 200.0, h: 40.0 }, None);
let driver = NavDriver::new(map);
assert_eq!(
driver.enter(&Scheduler::default()),
NavStep::Entered(7),
"the stale lock is cleared and the request goes through"
);
assert_eq!(repose_core::runtime::focus_capture(), None);
drain_requests();
}
fn scheduler_focused_on(id: u64) -> Scheduler {
let mut sched = Scheduler::default();
sched.focused = Some(id);
sched
}
#[test]
fn the_driver_enters_and_drives_against_a_live_scheduler() {
drain_requests();
let driver = NavDriver::new(grid()).with_wrap(Wrap::Stay);
let mut sched = Scheduler::default();
assert_eq!(driver.enter(&sched), NavStep::Entered(1));
sched.focused = Some(1);
assert_eq!(driver.enter(&sched), NavStep::Already(1));
assert_eq!(driver.drive(&sched, NavDir::Right), NavStep::Moved(2));
sched.focused = Some(2);
assert_eq!(driver.drive(&sched, NavDir::Down), NavStep::Moved(12));
sched.focused = Some(12);
assert_eq!(driver.drive(&sched, NavDir::Left), NavStep::Moved(11));
sched.focused = Some(11);
assert_eq!(driver.drive(&sched, NavDir::Up), NavStep::Moved(1));
assert_eq!(NavStep::Moved(1).target(), Some(1));
assert_eq!(NavStep::Edge.target(), None);
drain_requests();
}
#[test]
fn stick_nav_ignores_rest_and_small_deflection() {
let mut nav = StickNav::new();
assert_eq!(nav.step(Vec2::ZERO, 0.1), None);
assert_eq!(nav.step(Vec2::new(0.15, 0.0), 0.1), None, "inside the dead zone");
assert_eq!(nav.step(Vec2::new(0.35, 0.0), 0.1), None, "below the press threshold");
assert_eq!(nav.held(), None);
}
#[test]
fn stick_nav_presses_once_then_repeats() {
let mut nav = StickNav::new().with_cadence(0.5, 0.35, 0.14);
assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.016), Some(NavDir::Right));
for _ in 0..20 {
assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.016), None);
}
assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.05), Some(NavDir::Right));
assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.02), None);
assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.14), Some(NavDir::Right));
assert_eq!(nav.step(Vec2::ZERO, 0.5), None);
assert_eq!(nav.held(), None);
assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.016), Some(NavDir::Right));
}
#[test]
fn stick_nav_picks_a_side_and_reverses_on_change() {
let mut nav = StickNav::new();
assert_eq!(nav.step(Vec2::new(0.0, -1.0), 0.016), Some(NavDir::Up));
assert_eq!(nav.step(Vec2::new(0.0, 1.0), 0.016), Some(NavDir::Down));
assert_eq!(nav.step(Vec2::new(-1.0, 0.0), 0.016), Some(NavDir::Left));
assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.016), Some(NavDir::Right));
assert_eq!(nav.step(Vec2::ZERO, 0.016), None);
assert_eq!(nav.step(Vec2::new(1.0, 0.6), 0.016), Some(NavDir::Right));
assert_eq!(nav.step(Vec2::ZERO, 0.016), None);
assert_eq!(nav.step(Vec2::new(0.2, 1.0), 0.016), Some(NavDir::Down));
}
#[test]
fn stick_nav_release_forgets_the_held_direction() {
let mut nav = StickNav::new();
assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.016), Some(NavDir::Right));
nav.release();
assert_eq!(nav.held(), None);
assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.016), Some(NavDir::Right));
}
#[test]
fn a_mis_set_cadence_is_clamped_into_a_sane_band() {
let mut nav = StickNav::new().with_cadence(9.0, -5.0, 0.0);
assert_eq!(nav.step(Vec2::new(1.0, 0.0), 0.0), Some(NavDir::Right));
assert_eq!(
nav.step(Vec2::new(1.0, 0.0), 0.0),
Some(NavDir::Right),
"the repeat interval clamps to one step at 240 Hz, so a held stick repeats"
);
assert_eq!(
nav.step(Vec2::new(0.9, 0.0), 0.0),
None,
"the threshold clamps to full deflection"
);
}
#[test]
fn dpad_edges_map_straight_through() {
let mut pad = repame_input::GamepadState::default();
assert!(dpad_dirs(&pad).is_empty());
assert!(!pad_confirm(&pad));
pad.dpad_left_pressed = true;
pad.dpad_up_pressed = true;
pad.south_pressed = true;
assert_eq!(dpad_dirs(&pad), [NavDir::Left, NavDir::Up]);
assert!(pad_confirm(&pad));
}
#[test]
fn group_ids_are_stable_and_name_derived() {
assert_eq!(GroupId::named("pause"), GroupId::named("pause"));
assert_ne!(GroupId::named("pause"), GroupId::named("death"));
assert_eq!(GroupId::from_raw(7).raw(), 7);
assert_eq!(GroupId::named("").raw(), 0xcbf2_9ce4_8422_2325);
}
#[test]
fn directions_round_trip() {
for dir in NavDir::ALL {
assert_eq!(dir.reverse().reverse(), dir);
}
assert_eq!(NavDir::Next.reverse(), NavDir::Previous);
assert_eq!(NavDir::Previous.reverse(), NavDir::Next);
assert_eq!(NavDir::Left.reverse(), NavDir::Right);
assert_eq!(NavDir::Up.reverse(), NavDir::Down);
assert!(NavDir::Next.is_chain());
assert!(!NavDir::Down.is_chain());
}
#[test]
fn the_raw_repose_helpers_are_usable_without_a_map() {
let chain = [1, 2, 3];
let regions: Vec<HitRegion> = (0..3)
.map(|index| HitRegion {
id: index + 1,
rect: Rect {
x: index as f32 * 100.0,
y: 0.0,
w: 90.0,
h: 40.0,
},
focusable: true,
..HitRegion::default()
})
.collect();
assert_eq!(resolve_in(&chain, ®ions, Some(1), NavDir::Right), Some(2));
assert_eq!(chain_step(&chain, Some(1), NavDir::Next), Some(2));
assert_eq!(chain_step(&chain, Some(3), NavDir::Next), Some(1));
assert_eq!(chain_step(&[], Some(1), NavDir::Next), None);
}
}