#![allow(clippy::manual_midpoint, clippy::float_cmp)]
use crate::session::AnnotId;
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Rect {
pub(crate) left: f32,
pub(crate) bottom: f32,
pub(crate) right: f32,
pub(crate) top: f32,
}
impl Rect {
pub(crate) fn new(left: f32, bottom: f32, right: f32, top: f32) -> Rect {
Rect {
left,
bottom,
right,
top,
}
}
pub(crate) fn center_y(self) -> f32 {
(self.top + self.bottom) / 2.0
}
pub(crate) fn center_x(self) -> f32 {
(self.left + self.right) / 2.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum TabOrder {
#[default]
Structure,
Row,
Column,
}
impl TabOrder {
#[must_use]
pub fn from_tabs(tabs: Option<&[u8]>) -> TabOrder {
match tabs {
Some(b"R") => TabOrder::Row,
Some(b"C") => TabOrder::Column,
_ => TabOrder::Structure,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Focusable {
pub(crate) id: AnnotId,
pub(crate) rect: Rect,
}
#[derive(Debug, Clone, PartialEq)]
pub struct FocusRing {
pub order: Vec<AnnotId>,
pub degenerate: bool,
}
impl FocusRing {
pub(crate) fn build(annots: &[Focusable], order: TabOrder) -> FocusRing {
match order {
TabOrder::Structure => FocusRing {
order: annots.iter().map(|a| a.id).collect(),
degenerate: false,
},
TabOrder::Row => band(annots, Axis::Row),
TabOrder::Column => band(annots, Axis::Column),
}
}
#[must_use]
pub fn next(&self, current: AnnotId) -> Option<AnnotId> {
let at = self.order.iter().position(|a| *a == current)?;
self.order.get(at + 1).copied()
}
#[must_use]
pub fn prev(&self, current: AnnotId) -> Option<AnnotId> {
let at = self.order.iter().position(|a| *a == current)?;
self.order.get(at.checked_sub(1)?).copied()
}
#[must_use]
pub fn first(&self) -> Option<AnnotId> {
self.order.first().copied()
}
#[must_use]
pub fn last(&self) -> Option<AnnotId> {
self.order.last().copied()
}
#[must_use]
pub fn len(&self) -> usize {
self.order.len()
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.order.is_empty()
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum Axis {
Row,
Column,
}
fn band(annots: &[Focusable], axis: Axis) -> FocusRing {
let mut remaining: Vec<Focusable> = annots.to_vec();
match axis {
Axis::Row => remaining.sort_by(|a, b| {
a.rect
.left
.partial_cmp(&b.rect.left)
.unwrap_or(std::cmp::Ordering::Equal)
}),
Axis::Column => remaining.sort_by(|a, b| {
b.rect
.top
.partial_cmp(&a.rect.top)
.unwrap_or(std::cmp::Ordering::Equal)
}),
}
let mut order = Vec::with_capacity(remaining.len());
let mut degenerate = false;
while !remaining.is_empty() {
let Some(seed) = seed_index(&remaining, axis) else {
degenerate = true;
order.extend(remaining.iter().map(|a| a.id));
break;
};
let Some(head) = remaining.get(seed).copied() else {
degenerate = true;
order.extend(remaining.iter().map(|a| a.id));
break;
};
remaining.remove(seed);
order.push(head.id);
let mut kept = Vec::with_capacity(remaining.len());
for annot in remaining.drain(..) {
let joins = match axis {
Axis::Row => {
annot.rect.center_y() > head.rect.bottom
&& annot.rect.center_y() < head.rect.top
}
Axis::Column => {
annot.rect.center_x() > head.rect.left
&& annot.rect.center_x() < head.rect.right
}
};
if joins {
order.push(annot.id);
} else {
kept.push(annot);
}
}
remaining = kept;
}
FocusRing { order, degenerate }
}
fn seed_index(remaining: &[Focusable], axis: Axis) -> Option<usize> {
match axis {
Axis::Row => {
let mut best: Option<usize> = None;
let mut top = 0.0f32;
for (i, annot) in remaining.iter().enumerate().rev() {
if annot.rect.top > top {
best = Some(i);
top = annot.rect.top;
}
}
best
}
Axis::Column => {
let mut best: Option<usize> = None;
let mut left = -1.0f32;
for (i, annot) in remaining.iter().enumerate().rev() {
if left < 0.0 {
best = Some(0);
left = annot.rect.left;
} else if annot.rect.left < left {
best = Some(i);
left = annot.rect.left;
}
}
best
}
}
}
#[cfg(test)]
mod tests {
use super::*;
fn annot(index: u32, left: f32, bottom: f32, right: f32, top: f32) -> Focusable {
Focusable {
id: AnnotId::new(0, index),
rect: Rect::new(left, bottom, right, top),
}
}
fn ids(ring: &FocusRing) -> Vec<u32> {
ring.order.iter().map(|a| a.index).collect()
}
#[test]
fn the_tabs_entry_recognizes_exactly_two_spellings() {
assert_eq!(TabOrder::from_tabs(Some(b"R")), TabOrder::Row);
assert_eq!(TabOrder::from_tabs(Some(b"C")), TabOrder::Column);
assert_eq!(TabOrder::from_tabs(Some(b"S")), TabOrder::Structure);
assert_eq!(TabOrder::from_tabs(Some(b"r")), TabOrder::Structure);
assert_eq!(TabOrder::from_tabs(Some(b"")), TabOrder::Structure);
assert_eq!(TabOrder::from_tabs(None), TabOrder::Structure);
}
#[test]
fn structure_order_sorts_nothing() {
let annots = [
annot(0, 500.0, 500.0, 600.0, 600.0),
annot(1, 0.0, 0.0, 100.0, 100.0),
annot(2, 200.0, 700.0, 300.0, 800.0),
];
let ring = FocusRing::build(&annots, TabOrder::Structure);
assert_eq!(ids(&ring), vec![0, 1, 2]);
assert!(!ring.degenerate);
}
#[test]
fn the_ring_has_two_ends_and_no_wrap() {
let annots = [
annot(0, 0.0, 0.0, 10.0, 10.0),
annot(1, 0.0, 20.0, 10.0, 30.0),
annot(2, 0.0, 40.0, 10.0, 50.0),
];
let ring = FocusRing::build(&annots, TabOrder::Structure);
assert_eq!(ring.next(AnnotId::new(0, 0)), Some(AnnotId::new(0, 1)));
assert_eq!(ring.next(AnnotId::new(0, 2)), None);
assert_eq!(ring.prev(AnnotId::new(0, 2)), Some(AnnotId::new(0, 1)));
assert_eq!(ring.prev(AnnotId::new(0, 0)), None);
}
#[test]
fn the_two_ends_are_different_annotations() {
let annots = [
annot(0, 0.0, 0.0, 10.0, 10.0),
annot(1, 0.0, 20.0, 10.0, 30.0),
];
let ring = FocusRing::build(&annots, TabOrder::Structure);
assert_eq!(ring.first(), Some(AnnotId::new(0, 0)));
assert_eq!(ring.last(), Some(AnnotId::new(0, 1)));
assert_ne!(ring.first(), ring.last());
}
#[test]
fn row_order_seeds_each_band_with_the_rightmost_of_the_topmost() {
let annots = [
annot(0, 300.0, 100.0, 400.0, 150.0), annot(1, 100.0, 500.0, 200.0, 550.0), annot(2, 100.0, 100.0, 200.0, 150.0), annot(3, 300.0, 500.0, 400.0, 550.0), ];
let ring = FocusRing::build(&annots, TabOrder::Row);
assert_eq!(ids(&ring), vec![3, 1, 0, 2]);
assert!(!ring.degenerate);
}
#[test]
fn an_untied_row_band_reads_left_to_right() {
let annots = [
annot(0, 300.0, 500.0, 400.0, 540.0), annot(1, 100.0, 500.0, 200.0, 550.0), annot(2, 100.0, 100.0, 200.0, 150.0), ];
let ring = FocusRing::build(&annots, TabOrder::Row);
assert_eq!(ids(&ring), vec![1, 0, 2]);
}
#[test]
fn column_order_reads_down_bands() {
let annots = [
annot(0, 300.0, 100.0, 400.0, 150.0), annot(1, 100.0, 500.0, 200.0, 550.0), annot(2, 100.0, 100.0, 200.0, 150.0), annot(3, 300.0, 500.0, 400.0, 550.0), ];
let ring = FocusRing::build(&annots, TabOrder::Column);
assert_eq!(ids(&ring), vec![1, 2, 3, 0]);
assert!(!ring.degenerate);
}
#[test]
fn a_centre_exactly_on_a_band_edge_does_not_join_it() {
let annots = [
annot(0, 0.0, 100.0, 50.0, 200.0),
annot(1, 100.0, 50.0, 150.0, 150.0),
];
let ring = FocusRing::build(&annots, TabOrder::Row);
assert_eq!(annots[1].rect.center_y(), annots[0].rect.bottom);
assert_eq!(ids(&ring), vec![0, 1]);
}
#[test]
fn banding_terminates_where_the_oracle_would_hang() {
let annots = [
annot(0, 10.0, -200.0, 20.0, -100.0),
annot(1, 30.0, -400.0, 40.0, -300.0),
];
let ring = FocusRing::build(&annots, TabOrder::Row);
assert!(ring.degenerate, "the recovery should be reported");
assert_eq!(ring.len(), 2, "no annotation may be dropped");
assert_eq!(ids(&ring), vec![0, 1]);
}
#[test]
fn a_zero_top_is_on_the_hanging_side_of_the_comparison() {
let annots = [annot(0, 0.0, -10.0, 10.0, 0.0)];
let ring = FocusRing::build(&annots, TabOrder::Row);
assert!(ring.degenerate);
assert_eq!(ids(&ring), vec![0]);
}
#[test]
fn banding_always_terminates_and_preserves_every_annotation() {
for seed in 0..200u32 {
let mut bits = seed.wrapping_mul(2_654_435_761);
let mut annots = Vec::new();
for i in 0..6u32 {
let mut next = || {
bits = bits.wrapping_mul(1_103_515_245).wrapping_add(12_345);
f32::from(u16::try_from((bits >> 16) % 1000).unwrap_or(0)) - 500.0
};
let (x, y) = (next(), next());
annots.push(annot(i, x, y, x + 20.0, y + 20.0));
}
for order in [TabOrder::Row, TabOrder::Column, TabOrder::Structure] {
let ring = FocusRing::build(&annots, order);
assert_eq!(ring.len(), 6, "annotation lost at seed {seed}");
let mut seen: Vec<u32> = ring.order.iter().map(|a| a.index).collect();
seen.sort_unstable();
assert_eq!(seen, vec![0, 1, 2, 3, 4, 5], "duplicate at seed {seed}");
}
}
}
#[test]
fn the_ring_keeps_the_gaps_that_unfocusable_annotations_leave() {
let annots = [
annot(1, 100.0, 400.0, 200.0, 450.0),
annot(3, 100.0, 200.0, 200.0, 250.0),
];
let ring = FocusRing::build(&annots, TabOrder::Structure);
assert_eq!(ids(&ring), vec![1, 3], "the ring is not renumbered");
assert_eq!(ring.first(), Some(AnnotId::new(0, 1)));
assert_eq!(ring.next(AnnotId::new(0, 1)), Some(AnnotId::new(0, 3)));
assert_eq!(ring.next(AnnotId::new(0, 3)), None);
}
#[test]
fn an_empty_page_has_an_empty_ring() {
let ring = FocusRing::build(&[], TabOrder::Row);
assert!(ring.is_empty());
assert_eq!(ring.first(), None);
assert_eq!(ring.last(), None);
assert!(!ring.degenerate);
}
#[test]
fn an_unknown_annotation_has_no_neighbours() {
let annots = [annot(0, 0.0, 0.0, 10.0, 10.0)];
let ring = FocusRing::build(&annots, TabOrder::Structure);
assert_eq!(ring.next(AnnotId::new(0, 99)), None);
assert_eq!(ring.prev(AnnotId::new(0, 99)), None);
}
mod annotiter {
use super::*;
fn annotiter_widgets() -> Vec<Focusable> {
[
(0, 200.0, 200.0, 220.0, 220.0), (1, 401.0, 401.0, 421.0, 421.0), (2, 201.0, 400.0, 221.0, 420.0), (3, 400.0, 201.0, 420.0, 221.0), ]
.into_iter()
.map(|(index, left, bottom, right, top)| Focusable {
id: AnnotId::new(0, index),
rect: Rect::new(left, bottom, right, top),
})
.collect()
}
fn row_ring() -> FocusRing {
FocusRing::build(&annotiter_widgets(), TabOrder::Row)
}
fn indices(ring: &FocusRing) -> Vec<u32> {
ring.order.iter().map(|a| a.index).collect()
}
#[test]
fn first_tab_lands_on_annot_one() {
assert_eq!(row_ring().first(), Some(AnnotId::new(0, 1)));
}
#[test]
fn first_shift_tab_lands_on_annot_zero() {
assert_eq!(row_ring().last(), Some(AnnotId::new(0, 0)));
}
#[test]
fn continuous_tab_visits_one_two_three_zero_then_stops() {
let ring = row_ring();
let mut at = ring.first().expect("the ring is not empty");
assert_eq!(at, AnnotId::new(0, 1));
let mut visited = vec![at.index];
for _ in 0..3 {
at = ring.next(at).expect("a next annotation");
visited.push(at.index);
}
assert_eq!(visited, vec![1, 2, 3, 0]);
assert_eq!(ring.next(at), None, "the fifth tab is not handled");
}
#[test]
fn continuous_shift_tab_visits_zero_three_two_one_then_stops() {
let ring = row_ring();
let mut at = ring.last().expect("the ring is not empty");
assert_eq!(at, AnnotId::new(0, 0));
let mut visited = vec![at.index];
for _ in 0..3 {
at = ring.prev(at).expect("a previous annotation");
visited.push(at.index);
}
assert_eq!(visited, vec![0, 3, 2, 1]);
assert_eq!(ring.prev(at), None, "the fifth shift-tab is not handled");
}
#[test]
fn the_backward_walk_reverses_the_forward_one() {
let ring = row_ring();
let forward = indices(&ring);
let mut backward = forward.clone();
backward.reverse();
let mut walked = vec![ring.last().expect("a last").index];
let mut at = ring.last().expect("a last");
while let Some(prev) = ring.prev(at) {
walked.push(prev.index);
at = prev;
}
assert_eq!(walked, backward);
}
#[test]
fn the_three_orders_disagree_over_this_fixture() {
let widgets = annotiter_widgets();
let row = indices(&FocusRing::build(&widgets, TabOrder::Row));
let column = indices(&FocusRing::build(&widgets, TabOrder::Column));
let structure = indices(&FocusRing::build(&widgets, TabOrder::Structure));
assert_eq!(structure, vec![0, 1, 2, 3], "structure order sorts nothing");
assert_eq!(row, vec![1, 2, 3, 0]);
assert_ne!(row, column);
assert_ne!(row, structure);
assert_ne!(column, structure);
}
#[test]
fn every_order_is_a_permutation() {
let widgets = annotiter_widgets();
for order in [TabOrder::Row, TabOrder::Column, TabOrder::Structure] {
let ring = FocusRing::build(&widgets, order);
let mut seen = indices(&ring);
seen.sort_unstable();
assert_eq!(seen, vec![0, 1, 2, 3], "{order:?} is not a permutation");
assert!(!ring.degenerate, "{order:?} should not degenerate");
}
}
}
mod never_panics {
use crate::event::Point;
use crate::geom::{Plate, Rotation};
use crate::hit::{Candidate, LayoutBand, Permissions, WidgetHit, widget_at_point};
use crate::session::AnnotId;
use crate::tab::{FocusRing, Focusable, Rect, TabOrder};
struct Gen(u32);
impl Gen {
fn next(&mut self) -> u32 {
self.0 = self.0.wrapping_mul(1_103_515_245).wrapping_add(12_345);
self.0
}
fn coord(&mut self) -> f32 {
let raw = i16::try_from(self.next() % 2000).unwrap_or(0) - 1000;
f32::from(raw) / 2.0
}
fn below(&mut self, n: u32) -> u32 {
if n == 0 { 0 } else { self.next() % n }
}
}
fn awkward_rects() -> Vec<Rect> {
vec![
Rect::new(0.0, 0.0, 0.0, 0.0),
Rect::new(10.0, 10.0, 10.0, 10.0),
Rect::new(1.0, 1.0, 2.0, 2.0),
Rect::new(100.0, 100.0, 200.0, -130.0),
Rect::new(200.0, 200.0, 100.0, 100.0),
Rect::new(-500.0, -500.0, -400.0, -400.0),
Rect::new(0.0, 0.0, 1e6, 1e6),
]
}
#[test]
fn the_plate_transform_never_panics_or_produces_nonsense() {
let mut rng = Gen(1);
for rect in awkward_rects() {
for rotation in [
Rotation::None,
Rotation::Quarter,
Rotation::Half,
Rotation::ThreeQuarter,
] {
let plate = Plate::new(rect, rotation);
for _ in 0..20 {
let at = Point::new(rng.coord(), rng.coord());
let there = plate.to_plate(at);
let back = plate.to_page(there);
assert!(there.x.is_finite() && there.y.is_finite());
assert!(back.x.is_finite() && back.y.is_finite());
}
assert!(plate.width().is_finite());
assert!(plate.height().is_finite());
assert!(
plate.width() >= 0.0,
"a normalized box has no negative width"
);
assert!(plate.height() >= 0.0);
}
}
}
#[test]
fn hit_testing_never_panics_and_never_invents_an_annotation() {
let mut rng = Gen(7);
for _ in 0..200 {
let count = rng.below(6);
let candidates: Vec<Candidate> = (0..count)
.map(|i| {
let (x, y) = (rng.coord(), rng.coord());
Candidate {
id: AnnotId::new(rng.below(3), i),
rect: Rect::new(x, y, x + rng.coord(), y + rng.coord()),
band: match rng.below(3) {
0 => LayoutBand::Popup,
1 => LayoutBand::Widget,
_ => LayoutBand::Other,
},
widget: (rng.below(2) == 0).then(|| WidgetHit {
signature: rng.below(2) == 0,
hidden: rng.below(2) == 0,
read_only: rng.below(2) == 0,
push_button: rng.below(2) == 0,
}),
}
})
.collect();
let focused = candidates.first().map(|c| c.id);
for permissions in [Permissions::ALL, Permissions::NONE] {
let hit = widget_at_point(
&candidates,
focused,
permissions,
rng.coord(),
rng.coord(),
);
if let Some(hit) = hit {
assert!(
candidates.iter().any(|c| c.id == hit),
"hit test named an annotation that is not in the list"
);
}
}
}
}
#[test]
fn the_focus_ring_always_terminates_and_is_always_a_permutation() {
let mut rng = Gen(13);
for _ in 0..300 {
let count = rng.below(8);
let annots: Vec<Focusable> = (0..count)
.map(|i| {
let (x, y) = (rng.coord(), rng.coord());
Focusable {
id: AnnotId::new(0, i),
rect: Rect::new(x, y, x + rng.coord(), y + rng.coord()),
}
})
.collect();
for order in [TabOrder::Row, TabOrder::Column, TabOrder::Structure] {
let built = FocusRing::build(&annots, order);
assert_eq!(built.len(), annots.len(), "{order:?} lost an annotation");
let mut seen: Vec<u32> = built.order.iter().map(|a| a.index).collect();
seen.sort_unstable();
let expected: Vec<u32> = (0..count).collect();
assert_eq!(seen, expected, "{order:?} is not a permutation");
if let Some(mut at) = built.first() {
let mut steps = 0;
while let Some(next) = built.next(at) {
at = next;
steps += 1;
assert!(
steps <= count as usize,
"the forward walk did not terminate"
);
}
}
}
}
}
}
}