use crate::manager::SplitArea;
use crate::types::AreaId;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Left,
Right,
Up,
Down,
}
pub fn navigate(areas: &[SplitArea], current_id: AreaId, direction: Direction) -> Option<AreaId> {
let current = areas.iter().find(|a| a.id == current_id)?;
let current_rect = current.rect;
let mut best: Option<(AreaId, u16, i32)> = None;
for candidate in areas {
if candidate.id == current_id {
continue;
}
let r = candidate.rect;
let (overlap, distance) = match direction {
Direction::Left => {
let distance = i32::from(current_rect.x) - (i32::from(r.x) + i32::from(r.width));
let overlap = overlap_range(current_rect.y, current_rect.height, r.y, r.height);
(overlap, distance)
}
Direction::Right => {
let distance =
i32::from(r.x) - (i32::from(current_rect.x) + i32::from(current_rect.width));
let overlap = overlap_range(current_rect.y, current_rect.height, r.y, r.height);
(overlap, distance)
}
Direction::Up => {
let distance = i32::from(current_rect.y) - (i32::from(r.y) + i32::from(r.height));
let overlap = overlap_range(current_rect.x, current_rect.width, r.x, r.width);
(overlap, distance)
}
Direction::Down => {
let distance =
i32::from(r.y) - (i32::from(current_rect.y) + i32::from(current_rect.height));
let overlap = overlap_range(current_rect.x, current_rect.width, r.x, r.width);
(overlap, distance)
}
};
if overlap == 0 || distance < 0 {
continue;
}
#[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)]
let is_better = match &best {
None => true,
Some((_, best_overlap, best_distance)) => {
let closeness_score = u32::from(u16::MAX - (distance as u16)) * u32::from(overlap);
let best_closeness_score =
u32::from(u16::MAX - (*best_distance as u16)) * u32::from(*best_overlap);
closeness_score > best_closeness_score
}
};
if is_better {
best = Some((candidate.id, overlap, distance));
}
}
best.map(|(id, _, _)| id)
}
fn overlap_range(a_start: u16, a_len: u16, b_start: u16, b_len: u16) -> u16 {
let a_end = a_start.saturating_add(a_len);
let b_end = b_start.saturating_add(b_len);
a_end.min(b_end).saturating_sub(a_start.max(b_start))
}
#[cfg(test)]
mod tests {
use super::*;
use ratatui::layout::Rect;
fn area(id: u64, x: u16, y: u16, w: u16, h: u16) -> SplitArea {
SplitArea {
id: AreaId(id),
rect: Rect::new(x, y, w, h),
}
}
#[test]
fn navigate_returns_right_neighbor() {
let areas = &[area(1, 0, 0, 50, 100), area(2, 50, 0, 50, 100)];
let result = navigate(areas, AreaId(1), Direction::Right);
assert_eq!(result, Some(AreaId(2)));
}
#[test]
fn navigate_returns_left_neighbor() {
let areas = &[area(1, 0, 0, 50, 100), area(2, 50, 0, 50, 100)];
let result = navigate(areas, AreaId(2), Direction::Left);
assert_eq!(result, Some(AreaId(1)));
}
#[test]
fn navigate_returns_down_neighbor() {
let areas = &[area(1, 0, 0, 100, 50), area(2, 0, 50, 100, 50)];
let result = navigate(areas, AreaId(1), Direction::Down);
assert_eq!(result, Some(AreaId(2)));
}
#[test]
fn navigate_returns_up_neighbor() {
let areas = &[area(1, 0, 0, 100, 50), area(2, 0, 50, 100, 50)];
let result = navigate(areas, AreaId(2), Direction::Up);
assert_eq!(result, Some(AreaId(1)));
}
#[test]
fn navigate_returns_none_when_no_neighbor() {
let areas = &[area(1, 0, 0, 100, 100)];
let result = navigate(areas, AreaId(1), Direction::Right);
assert_eq!(result, None);
}
#[test]
fn navigate_prefers_closest_neighbor_from_leftmost() {
let areas = &[
area(1, 0, 0, 30, 100),
area(2, 30, 0, 30, 100),
area(3, 60, 0, 40, 100),
];
let result = navigate(areas, AreaId(1), Direction::Right);
assert_eq!(result, Some(AreaId(2)));
}
#[test]
fn navigate_prefers_closest_neighbor_from_middle() {
let areas = &[
area(1, 0, 0, 30, 100),
area(2, 30, 0, 30, 100),
area(3, 60, 0, 40, 100),
];
let result = navigate(areas, AreaId(2), Direction::Right);
assert_eq!(result, Some(AreaId(3)));
}
#[test]
fn navigate_finds_neighbor_with_partial_overlap() {
let areas = &[area(1, 0, 0, 50, 100), area(2, 50, 20, 50, 100)];
let result = navigate(areas, AreaId(1), Direction::Right);
assert_eq!(result, Some(AreaId(2)));
}
#[test]
fn navigate_returns_none_when_no_overlap() {
let areas = &[area(1, 0, 0, 50, 20), area(2, 50, 30, 50, 20)];
let result = navigate(areas, AreaId(1), Direction::Right);
assert_eq!(result, None);
}
#[test]
fn overlap_range_computes_intersection() {
let result = overlap_range(0, 10, 5, 10);
assert_eq!(result, 5);
let result = overlap_range(0, 10, 0, 10);
assert_eq!(result, 10);
let result = overlap_range(0, 5, 10, 5);
assert_eq!(result, 0);
let result = overlap_range(0, 10, 3, 4);
assert_eq!(result, 4);
}
}