#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub struct Room {
pub width: u16,
pub lead: u16,
pub padding: u16,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct OnScreen {
pub first: usize,
pub last: usize,
pub cursor: usize,
pub total: usize,
}
impl OnScreen {
pub fn widest_label(&self, compact: bool) -> String {
Self {
cursor: self.total,
..*self
}
.label(compact)
}
pub fn label(&self, compact: bool) -> String {
if compact {
format!("col {}/{}", self.cursor, self.total)
} else {
format!("col {} of {}", self.cursor, self.total)
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ColumnMove {
StepLeft,
StepRight,
First,
PageLeft,
PageRight,
Last,
Reveal(usize),
Keep(usize),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CursorMove {
Left,
Right,
First,
Last,
PageLeft,
PageRight,
}
pub fn whole_from(
start: usize,
count: usize,
room: Room,
width: &mut impl FnMut(usize) -> Option<u16>,
) -> Option<usize> {
let mut used = room.lead;
let mut whole = 0;
for i in start..count {
let w = width(i)?;
if used.saturating_add(w) > room.width {
break;
}
used = used.saturating_add(w).saturating_add(room.padding);
whole += 1;
}
Some(whole)
}
pub fn start_ending_at(
end: usize,
room: Room,
width: &mut impl FnMut(usize) -> Option<u16>,
) -> Option<usize> {
let mut used = room.lead.saturating_add(width(end)?);
let mut start = end;
while start > 0 {
let next = used
.saturating_add(room.padding)
.saturating_add(width(start - 1)?);
if next > room.width {
break;
}
used = next;
start -= 1;
}
Some(start)
}
pub fn plan(
mv: ColumnMove,
current: usize,
count: usize,
room: Room,
mut width: impl FnMut(usize) -> Option<u16>,
) -> Option<usize> {
let Some(last) = count.checked_sub(1) else {
return Some(0);
};
let current = current.min(last);
match mv {
ColumnMove::StepLeft => Some(current.saturating_sub(1)),
ColumnMove::StepRight => Some((current + 1).min(last)),
ColumnMove::First => Some(0),
ColumnMove::PageLeft if current == 0 => Some(0),
ColumnMove::PageLeft => start_ending_at(current - 1, room, &mut width),
ColumnMove::PageRight if current == last => Some(current),
ColumnMove::PageRight => {
let whole = whole_from(current, count, room, &mut width)?;
if current + whole >= count {
return Some(current);
}
let next = current + whole.max(1);
let last_page = start_ending_at(last, room, &mut width)?;
Some(next.min(last_page).max(current + 1))
}
ColumnMove::Last => start_ending_at(last, room, &mut width),
ColumnMove::Reveal(column) => {
let column = column.min(last);
if column >= current && column - current < whole_from(current, count, room, &mut width)?
{
return Some(current);
}
Some(column.min(start_ending_at(last, room, &mut width)?))
}
ColumnMove::Keep(column) => {
let column = column.min(last);
if column <= current {
return Some(column);
}
if column - current < whole_from(current, column + 1, room, &mut width)? {
return Some(current);
}
start_ending_at(column, room, &mut width)
}
}
}
#[cfg(test)]
mod tests {
use super::*;
const ROOM: Room = Room {
width: 30,
lead: 0,
padding: 2,
};
fn widths(ws: &[u16]) -> impl FnMut(usize) -> Option<u16> + '_ {
|i| ws.get(i).copied()
}
#[test]
fn on_screen_labels() {
let on = OnScreen {
first: 41,
last: 47,
cursor: 43,
total: 300,
};
assert_eq!(on.label(false), "col 43 of 300");
assert_eq!(on.label(true), "col 43/300");
assert_eq!(on.widest_label(true), "col 300/300");
}
#[test]
fn whole_columns_count_padding_and_lead() {
let ws = [8, 8, 8, 8];
assert_eq!(whole_from(0, 4, ROOM, &mut widths(&ws)), Some(3));
let lead = Room { lead: 3, ..ROOM };
assert_eq!(whole_from(0, 4, lead, &mut widths(&ws)), Some(2));
assert_eq!(whole_from(0, 1, ROOM, &mut widths(&[30])), Some(1));
assert_eq!(whole_from(0, 1, ROOM, &mut widths(&[31])), Some(0));
}
#[test]
fn page_right_starts_at_the_first_column_not_whole() {
let ws = [8; 12];
assert_eq!(
plan(ColumnMove::PageRight, 0, 12, ROOM, widths(&ws)),
Some(3)
);
assert_eq!(
plan(ColumnMove::PageRight, 3, 12, ROOM, widths(&ws)),
Some(6)
);
let ws = [10, 10, 15, 4, 4];
assert_eq!(
plan(ColumnMove::PageRight, 0, 5, ROOM, widths(&ws)),
Some(2)
);
}
#[test]
fn page_right_never_leaves_a_short_last_page() {
let ws = [8; 10];
assert_eq!(
plan(ColumnMove::PageRight, 6, 10, ROOM, widths(&ws)),
Some(7)
);
assert_eq!(
plan(ColumnMove::PageRight, 7, 10, ROOM, widths(&ws)),
Some(7)
);
}
#[test]
fn a_column_wider_than_the_room_still_moves_one_at_a_time() {
let ws = [50, 50, 50];
assert_eq!(
plan(ColumnMove::PageRight, 0, 3, ROOM, widths(&ws)),
Some(1)
);
assert_eq!(
plan(ColumnMove::PageRight, 1, 3, ROOM, widths(&ws)),
Some(2)
);
assert_eq!(plan(ColumnMove::PageLeft, 2, 3, ROOM, widths(&ws)), Some(1));
assert_eq!(plan(ColumnMove::Last, 0, 3, ROOM, widths(&ws)), Some(2));
}
#[test]
fn page_left_ends_at_the_column_before_the_first() {
let ws = [8; 12];
assert_eq!(
plan(ColumnMove::PageLeft, 6, 12, ROOM, widths(&ws)),
Some(3)
);
assert_eq!(
plan(ColumnMove::PageLeft, 2, 12, ROOM, widths(&ws)),
Some(0)
);
assert_eq!(
plan(ColumnMove::PageLeft, 0, 12, ROOM, widths(&ws)),
Some(0)
);
let ws = [2, 2, 2, 9, 2, 4, 20, 1];
assert_eq!(plan(ColumnMove::PageLeft, 7, 8, ROOM, widths(&ws)), Some(4));
}
#[test]
fn last_fills_the_room_ending_at_the_last_column() {
let ws = [8; 10];
assert_eq!(plan(ColumnMove::Last, 0, 10, ROOM, widths(&ws)), Some(7));
assert_eq!(plan(ColumnMove::Last, 9, 10, ROOM, widths(&ws)), Some(7));
assert_eq!(plan(ColumnMove::Last, 0, 3, ROOM, widths(&ws)), Some(0));
}
#[test]
fn reveal_keeps_a_column_already_whole_on_screen() {
let ws = [8; 10];
assert_eq!(
plan(ColumnMove::Reveal(2), 0, 10, ROOM, widths(&ws)),
Some(0)
);
assert_eq!(
plan(ColumnMove::Reveal(3), 0, 10, ROOM, widths(&ws)),
Some(3)
);
assert_eq!(
plan(ColumnMove::Reveal(1), 4, 10, ROOM, widths(&ws)),
Some(1)
);
assert_eq!(
plan(ColumnMove::Reveal(9), 0, 10, ROOM, widths(&ws)),
Some(7)
);
}
#[test]
fn keep_scrolls_only_as_far_as_the_column() {
let ws = [8; 10];
for column in 0..3 {
assert_eq!(
plan(ColumnMove::Keep(column), 0, 10, ROOM, widths(&ws)),
Some(0)
);
}
assert_eq!(plan(ColumnMove::Keep(3), 0, 10, ROOM, widths(&ws)), Some(1));
assert_eq!(plan(ColumnMove::Keep(9), 0, 10, ROOM, widths(&ws)), Some(7));
assert_eq!(plan(ColumnMove::Keep(2), 5, 10, ROOM, widths(&ws)), Some(2));
let wide = [8, 8, 50, 8];
assert_eq!(
plan(ColumnMove::Keep(2), 0, 4, ROOM, widths(&wide)),
Some(2)
);
let mut seen = Vec::new();
plan(ColumnMove::Keep(4), 2, 10, ROOM, |i| {
seen.push(i);
Some(8)
});
assert!(seen.iter().all(|&i| i <= 4), "{seen:?}");
}
#[test]
fn no_columns_and_one_column() {
let ws: [u16; 0] = [];
for mv in [
ColumnMove::PageLeft,
ColumnMove::PageRight,
ColumnMove::Last,
] {
assert_eq!(plan(mv, 0, 0, ROOM, widths(&ws)), Some(0));
}
let ws = [80];
for mv in [
ColumnMove::PageLeft,
ColumnMove::PageRight,
ColumnMove::Last,
] {
assert_eq!(plan(mv, 0, 1, ROOM, widths(&ws)), Some(0));
}
}
#[test]
fn an_unknown_width_defers_the_plan() {
let known = |i: usize| (i < 4).then_some(8);
assert_eq!(plan(ColumnMove::PageRight, 0, 10, ROOM, known), None);
assert_eq!(plan(ColumnMove::Last, 0, 10, ROOM, known), None);
assert_eq!(plan(ColumnMove::PageLeft, 4, 10, ROOM, known), Some(1));
}
}