use crate::components::NavDirection;
use crate::gfx::setting_action;
const PERP_WEIGHT: f32 = 2.0;
#[derive(Debug, Clone)]
pub(crate) struct Candidate {
pub(crate) index: usize,
pub(crate) rect: [f32; 4],
pub(crate) action: String,
}
#[derive(Debug, Clone)]
pub(crate) struct Target {
pub(crate) index: usize,
pub(crate) rect: [f32; 4],
pub(crate) setting: Option<String>,
}
#[derive(Debug, Clone)]
pub(crate) struct FocusRef {
pub(crate) index: usize,
pub(crate) rect: [f32; 4],
}
pub(crate) fn targets(candidates: &[Candidate]) -> Vec<Target> {
candidates
.iter()
.filter_map(|c| {
if setting_action::key_with_verb(&c.action, "prev").is_some() {
return None;
}
if let Some(key) = setting_action::key_with_verb(&c.action, "next") {
return Some(Target {
index: c.index,
rect: c.rect,
setting: Some(key.to_string()),
});
}
if let Some(key) = setting_action::key_with_verb(&c.action, "open") {
return Some(Target {
index: c.index,
rect: c.rect,
setting: Some(key.to_string()),
});
}
if let Some(key) = setting_action::key_with_verb(&c.action, "drag") {
return Some(Target {
index: c.index,
rect: c.rect,
setting: Some(key.to_string()),
});
}
(!c.action.is_empty()).then_some(Target {
index: c.index,
rect: c.rect,
setting: None,
})
})
.collect()
}
fn center(rect: [f32; 4]) -> (f32, f32) {
(rect[0] + rect[2] * 0.5, rect[1] + rect[3] * 0.5)
}
fn distances(from: (f32, f32), to: (f32, f32), dir: NavDirection) -> (f32, f32) {
let (dx, dy) = (to.0 - from.0, to.1 - from.1);
match dir {
NavDirection::Up => (-dy, dx.abs()),
NavDirection::Down => (dy, dx.abs()),
NavDirection::Left => (-dx, dy.abs()),
NavDirection::Right => (dx, dy.abs()),
}
}
fn nearest_in_direction(
targets: &[Target],
from: (f32, f32),
dir: NavDirection,
skip: Option<usize>,
) -> Option<usize> {
targets
.iter()
.filter(|t| Some(t.index) != skip)
.filter_map(|t| {
let (ahead, perp) = distances(from, center(t.rect), dir);
(ahead > 0.5).then_some((t.index, ahead + perp * PERP_WEIGHT))
})
.min_by(|a, b| a.1.total_cmp(&b.1))
.map(|(index, _)| index)
}
pub(crate) fn initial(targets: &[Target]) -> Option<usize> {
targets
.iter()
.min_by(|a, b| {
let (ax, ay) = center(a.rect);
let (bx, by) = center(b.rect);
(ay, ax)
.partial_cmp(&(by, bx))
.unwrap_or(std::cmp::Ordering::Equal)
})
.map(|t| t.index)
}
pub(crate) fn navigate(
targets: &[Target],
current: Option<&FocusRef>,
dir: NavDirection,
) -> Option<usize> {
if targets.is_empty() {
return None;
}
let Some(current) = current else {
return initial(targets);
};
let from = center(current.rect);
let skip = targets
.iter()
.any(|t| t.index == current.index)
.then_some(current.index);
if let Some(next) = nearest_in_direction(targets, from, dir, skip) {
return Some(next);
}
match dir {
NavDirection::Up | NavDirection::Down => {
let flipped = match dir {
NavDirection::Up => NavDirection::Down,
_ => NavDirection::Up,
};
targets
.iter()
.filter(|t| Some(t.index) != skip)
.filter_map(|t| {
let (ahead, perp) = distances(from, center(t.rect), flipped);
(ahead > 0.5).then_some((t.index, ahead - perp * PERP_WEIGHT))
})
.max_by(|a, b| a.1.total_cmp(&b.1))
.map(|(index, _)| index)
.or(Some(current.index))
}
NavDirection::Left | NavDirection::Right => Some(current.index),
}
}
#[cfg(test)]
mod tests {
use super::*;
fn cand(index: usize, x: f32, y: f32, action: &str) -> Candidate {
Candidate {
index,
rect: [x, y, 200.0, 30.0],
action: action.to_string(),
}
}
fn focus(targets: &[Target], index: usize) -> FocusRef {
let t = targets.iter().find(|t| t.index == index).unwrap();
FocusRef {
index,
rect: t.rect,
}
}
#[test]
fn steppers_group_to_one_target_and_prev_twins_drop() {
let t = targets(&[
cand(0, 0.0, 0.0, "setting:vsync:prev"),
cand(1, 60.0, 0.0, "setting:vsync:next"),
cand(2, 0.0, 40.0, "setting:exposure:drag"),
cand(3, 0.0, 80.0, "setting:window_mode:open"),
cand(4, 0.0, 120.0, "setting:key_forward:rebind"),
cand(5, 0.0, 160.0, "group:toggle:0"),
cand(6, 0.0, 200.0, "screen:hide"),
cand(7, 0.0, 240.0, ""),
]);
let by_index: Vec<usize> = t.iter().map(|t| t.index).collect();
assert_eq!(by_index, vec![1, 2, 3, 4, 5, 6], "prev + empty drop");
assert_eq!(t[0].setting.as_deref(), Some("vsync"));
assert_eq!(t[1].setting.as_deref(), Some("exposure"));
assert_eq!(t[2].setting.as_deref(), Some("window_mode"));
assert!(t[3].setting.is_none(), "a rebind row is not a value row");
}
#[test]
fn first_pulse_lands_topmost_leftmost() {
let t = targets(&[
cand(0, 100.0, 200.0, "screen:hide"),
cand(1, 0.0, 50.0, "quit"),
cand(2, 300.0, 50.0, "scene:1"),
]);
assert_eq!(navigate(&t, None, NavDirection::Down), Some(1));
assert_eq!(initial(&t), Some(1));
}
#[test]
fn vertical_list_walks_and_wraps() {
let t = targets(&[
cand(0, 0.0, 0.0, "a:1"),
cand(1, 0.0, 50.0, "a:2"),
cand(2, 0.0, 100.0, "a:3"),
]);
let f0 = focus(&t, 0);
assert_eq!(navigate(&t, Some(&f0), NavDirection::Down), Some(1));
let f2 = focus(&t, 2);
assert_eq!(
navigate(&t, Some(&f2), NavDirection::Down),
Some(0),
"wraps to top"
);
assert_eq!(
navigate(&t, Some(&f0), NavDirection::Up),
Some(2),
"wraps to bottom"
);
}
#[test]
fn horizontal_neighbors_reachable_and_clamped_at_the_ends() {
let t = targets(&[
cand(0, 0.0, 0.0, "screen:show:1"),
cand(1, 250.0, 0.0, "screen:show:2"),
cand(2, 500.0, 0.0, "screen:show:3"),
]);
let f0 = focus(&t, 0);
assert_eq!(navigate(&t, Some(&f0), NavDirection::Right), Some(1));
let f2 = focus(&t, 2);
assert_eq!(
navigate(&t, Some(&f2), NavDirection::Right),
Some(2),
"clamps"
);
assert_eq!(navigate(&t, Some(&f2), NavDirection::Left), Some(1));
}
#[test]
fn straight_ahead_beats_nearer_but_far_off_axis() {
let t = targets(&[
cand(0, 200.0, 0.0, "screen:show:1"),
cand(1, 700.0, 40.0, "side:1"),
cand(2, 200.0, 90.0, "row:1"),
]);
let f0 = focus(&t, 0);
assert_eq!(navigate(&t, Some(&f0), NavDirection::Down), Some(2));
}
#[test]
fn vanished_focus_reanchors_from_its_last_rect() {
let t = targets(&[cand(0, 0.0, 0.0, "a:1"), cand(2, 0.0, 100.0, "a:3")]);
let gone = FocusRef {
index: 9,
rect: [0.0, 50.0, 200.0, 30.0],
};
assert_eq!(navigate(&t, Some(&gone), NavDirection::Down), Some(2));
assert_eq!(navigate(&t, Some(&gone), NavDirection::Up), Some(0));
}
#[test]
fn empty_targets_focus_nothing() {
assert_eq!(navigate(&[], None, NavDirection::Down), None);
let gone = FocusRef {
index: 0,
rect: [0.0, 0.0, 10.0, 10.0],
};
assert_eq!(navigate(&[], Some(&gone), NavDirection::Down), None);
}
}