Files
komorebi/komorebi-scrolling/tests/rules.rs
T
LGUG2Z 6c4eb621d3 feat(scrolling): add a platform-free model of a scrollable strip of columns
Ported from komorebi-for-mac (feature/moar-scrolling). The crate has no
dependencies and no knowledge of windows; it is not used by komorebi yet.
2026-10-30 15:45:37 -07:00

1332 lines
36 KiB
Rust

//! The rules that the crate documentation promises, one test each, written as specifications.
//!
//! Unless a test says otherwise, the viewport is 1200 by 800, there are no gaps, and columns are
//! a quarter of the viewport wide (300), so four of them fit.
mod common;
use common::*;
use komorebi_scrolling::CenterFocusedColumn;
use komorebi_scrolling::ColumnWidth;
use komorebi_scrolling::Options;
use komorebi_scrolling::Rect;
use komorebi_scrolling::ScrollingSpace;
use komorebi_scrolling::Viewport;
use komorebi_scrolling::Visibility;
// Rule 1: a column's width is its own
#[test]
fn opening_columns_never_changes_the_width_of_the_others() {
let mut space = ScrollingSpace::new(options(0.25), VIEW);
for id in 0..9 {
space.add_column(id, None, None, true);
for (_, width) in widths(&space) {
assert_eq!(width, 300, "after opening column {id}");
}
}
}
#[test]
fn closing_columns_never_changes_the_width_of_the_others() {
let mut space = strip(options(0.25), 6);
space.set_width(&2, ColumnWidth::Fixed(500));
let before = widths(&space);
space.remove_column(&0);
space.remove_column(&5);
let expected: Vec<_> = before
.into_iter()
.filter(|(id, _)| *id != 0 && *id != 5)
.collect();
assert_eq!(widths(&space), expected);
}
#[test]
fn moving_focus_never_changes_a_width() {
let mut space = strip(options(0.25), 6);
space.set_width(&1, ColumnWidth::Fixed(555));
let before = widths(&space);
for id in [0, 3, 5, 1, 4] {
space.focus(&id);
assert_eq!(widths(&space), before);
}
}
#[test]
fn a_strip_with_more_columns_than_fit_is_wider_than_the_viewport() {
let space = strip(options(0.5), 6);
let layout = space.layout();
let start = layout.first().unwrap().rect.x;
let end = layout.last().unwrap().rect.right();
assert_eq!(end - start, 6 * 600);
}
#[test]
fn a_lone_column_is_not_stretched_to_fill_the_viewport() {
let space = strip(options(0.5), 1);
assert_eq!(
rect(&space, 0),
Rect {
x: 0,
y: 0,
width: 600,
height: 800
}
);
}
// Rule 2: the focused column does not move by itself
/// A strip of eight columns with column 5 in focus, and what column 5's place on screen is.
fn strip_with_a_focused_column() -> (ScrollingSpace<u32>, i32) {
let mut space = strip(options(0.25), 8);
space.focus(&5);
let x = x_of(&space, 5);
(space, x)
}
#[test]
fn nothing_that_happens_to_other_columns_moves_the_focused_one() {
type Change = Box<dyn Fn(&mut ScrollingSpace<u32>)>;
let changes: Vec<(&str, Change)> = vec![
(
"opening a column at the far left",
Box::new(|s| s.add_column(100, Some(0), None, false)),
),
(
"opening a column directly to the left of it",
Box::new(|s| s.add_column(100, Some(5), None, false)),
),
(
"opening a column directly to the right of it",
Box::new(|s| s.add_column(100, None, None, false)),
),
(
"opening a column at the far right",
Box::new(|s| s.add_column(100, Some(99), None, false)),
),
(
"closing a column to the left",
Box::new(|s| assert!(s.remove_column(&1))),
),
(
"closing the column directly to the left",
Box::new(|s| assert!(s.remove_column(&4))),
),
(
"closing a column to the right",
Box::new(|s| assert!(s.remove_column(&7))),
),
(
"widening a column to the left",
Box::new(|s| s.set_width(&1, ColumnWidth::Fixed(900))),
),
(
"narrowing a column to the left",
Box::new(|s| s.set_width(&4, ColumnWidth::Fixed(100))),
),
(
"widening a column to the right",
Box::new(|s| s.set_width(&6, ColumnWidth::Fixed(900))),
),
(
"cycling the preset of a column to the left",
Box::new(|s| s.cycle_preset_width(&2, true)),
),
(
"making a column to the left full width",
Box::new(|s| s.toggle_full_width(&3)),
),
(
"a column to the left being found to have a minimum width",
Box::new(|s| s.set_width_limits(&4, Some(800), None)),
),
(
"a column to the left going fullscreen",
Box::new(|s| s.set_fullscreen(&2, true)),
),
(
"moving a column from the left to further left",
Box::new(|s| s.move_column(&3, 0)),
),
(
"moving a column from the left to the right of the focused one",
Box::new(|s| s.move_column(&1, 7)),
),
(
"moving a column from the right to the left of the focused one",
Box::new(|s| s.move_column(&7, 1)),
),
];
for (what, change) in changes {
let (mut space, x) = strip_with_a_focused_column();
change(&mut space);
assert_eq!(space.active_id(), Some(&5), "{what} changed what has focus");
assert_eq!(x_of(&space, 5), x, "{what} moved the focused column");
}
}
#[test]
fn widening_the_focused_column_keeps_its_left_edge_until_it_no_longer_fits() {
let mut space = strip(options(0.25), 8);
space.focus(&5);
let x = x_of(&space, 5);
assert_eq!(x, 300);
space.set_width(&5, ColumnWidth::Fixed(600));
assert_eq!(x_of(&space, 5), x, "it fits, so it stays");
space.set_width(&5, ColumnWidth::Fixed(1000));
assert_eq!(
rect(&space, 5).right(),
1200,
"it does not fit, so it scrolls just far enough to"
);
}
// Rule 3: scrolling is as little as it can be
#[test]
fn focusing_a_column_that_is_in_view_does_not_scroll() {
let mut space = strip(options(0.25), 8);
let before = rects(&space);
// columns 4 to 7 are in view
for id in [6, 4, 5, 7, 4] {
space.focus(&id);
assert_eq!(rects(&space), before, "focusing column {id} scrolled");
}
}
#[test]
fn focusing_a_column_off_the_left_edge_brings_it_in_at_the_left_edge() {
let mut space = strip(options(0.25), 8);
space.focus(&3);
assert_eq!(rect(&space, 3).x, 0);
assert_eq!(placement(&space, 3).visibility, Visibility::Visible);
// and the strip moved by exactly that much, no more
assert_eq!(rect(&space, 4).x, 300);
assert_eq!(rect(&space, 6).x, 900);
}
#[test]
fn focusing_a_column_off_the_right_edge_brings_it_in_at_the_right_edge() {
let mut space = strip(options(0.25), 8);
space.focus(&1);
assert_eq!(rect(&space, 1).x, 0);
space.focus(&6);
assert_eq!(rect(&space, 6).right(), 1200);
assert_eq!(rect(&space, 5).x, 600);
}
#[test]
fn a_column_that_is_partly_in_view_counts_as_out_of_view() {
let mut space = strip(options(0.25), 8);
space.focus(&3);
space.set_width(&3, ColumnWidth::Fixed(700));
space.focus(&1);
// column 1 is at the left edge and 2 follows it, so column 3 starts at 600 and ends at 1300,
// and is cut off by the right edge
assert_eq!(rect(&space, 3).x, 600);
assert_eq!(placement(&space, 3).visibility, Visibility::Clipped);
space.focus(&3);
assert_eq!(placement(&space, 3).visibility, Visibility::Visible);
assert_eq!(rect(&space, 3).right(), 1200);
}
#[test]
fn gaps_are_left_between_a_column_that_is_brought_in_and_the_edge() {
let options = Options {
gaps: 16,
..options(0.25)
};
let mut space = strip(options, 8);
// each column is 1184 * 0.25 - 16 = 280 wide
assert_eq!(width_of(&space, 0), 280);
assert_eq!(
rect(&space, 7).right(),
1200 - 16,
"brought in from the right"
);
space.focus(&3);
assert_eq!(rect(&space, 3).x, 16, "brought in from the left");
}
// Rule 4: temporary changes are undone
#[test]
fn closing_a_column_that_was_just_opened_returns_to_the_one_before_it() {
let mut space = strip(options(0.25), 8);
space.focus(&3);
space.focus(&6);
assert_eq!(
rect(&space, 6).x,
900,
"column 6 is at the right edge, with column 7 beyond"
);
let before = rects(&space);
space.add_column(100, None, None, true);
assert_eq!(space.active_id(), Some(&100));
assert_ne!(rects(&space), before);
space.remove_column(&100);
assert_eq!(space.active_id(), Some(&6));
assert_eq!(rects(&space), before, "the view is back where it was");
}
#[test]
fn once_focus_has_moved_closing_a_column_does_not_return() {
let mut space = strip(options(0.25), 8);
space.focus(&3);
space.focus(&6);
space.add_column(100, None, None, true);
space.focus(&5);
space.focus(&100);
space.remove_column(&100);
// there is nothing to return to, so the column that took the closed column's place has focus
assert_eq!(space.active_id(), Some(&7));
}
#[test]
fn closing_the_column_to_return_to_cancels_the_return() {
let mut space = strip(options(0.25), 8);
space.focus(&3);
space.focus(&6);
space.add_column(100, None, None, true);
space.remove_column(&0);
space.remove_column(&100);
assert_eq!(space.active_id(), Some(&7));
}
#[test]
fn opening_a_column_between_the_two_cancels_the_return() {
let mut space = strip(options(0.25), 8);
space.focus(&3);
space.focus(&6);
space.add_column(100, None, None, true);
// a column now sits between 6 and 100, so 6 is no longer the one that 100 was opened next to
let at = space.position(&100).unwrap();
space.add_column(101, Some(at), None, false);
assert_eq!(space.ids()[6..9], [6, 101, 100]);
space.remove_column(&100);
assert_eq!(
space.active_id(),
Some(&7),
"the column after the closed one has focus"
);
}
#[test]
fn a_column_opened_anywhere_else_is_not_returned_from() {
let mut space = strip(options(0.25), 8);
space.focus(&6);
space.add_column(100, Some(0), None, true);
space.remove_column(&100);
// it was on the far left, so it was not next to the old column and nothing is remembered
assert_eq!(space.active_id(), Some(&0));
}
#[test]
fn closing_the_focused_column_otherwise_focuses_the_one_that_takes_its_place() {
let mut space = strip(options(0.25), 8);
space.focus(&4);
let view_pos = space.view_offset();
space.remove_column(&4);
assert_eq!(space.active_id(), Some(&5));
assert_eq!(
space.view_offset(),
view_pos,
"the viewport stays put when it can"
);
}
#[test]
fn closing_the_last_column_focuses_the_one_before_it() {
let mut space = strip(options(0.25), 8);
space.remove_column(&7);
assert_eq!(space.active_id(), Some(&6));
assert_eq!(placement(&space, 6).visibility, Visibility::Visible);
}
#[test]
fn closing_every_column_leaves_an_empty_strip_that_can_be_used_again() {
let mut space = strip(options(0.5), 3);
for id in 0..3 {
assert!(space.remove_column(&id));
}
assert!(space.is_empty());
assert!(space.layout().is_empty());
assert_eq!(space.active_id(), None);
space.add_column(7, None, None, true);
assert_eq!(rect(&space, 7).x, 0);
assert_eq!(space.active_id(), Some(&7));
}
#[test]
fn leaving_fullscreen_puts_the_viewport_back() {
let mut space = strip(options(0.25), 8);
space.focus(&6);
let before = rects(&space);
space.set_fullscreen(&6, true);
assert_eq!(
rect(&space, 6),
Rect {
x: 0,
y: 0,
width: 1200,
height: 800
}
);
space.set_fullscreen(&6, false);
assert_eq!(rects(&space), before);
}
#[test]
fn the_viewport_is_not_put_back_after_focus_has_been_somewhere_else() {
let mut space = strip(options(0.25), 8);
space.focus(&6);
space.set_fullscreen(&6, true);
space.focus(&2);
space.focus(&6);
space.set_fullscreen(&6, false);
// it is just brought into view, like any column
assert_eq!(placement(&space, 6).visibility, Visibility::Visible);
}
// Rule 5: a column wider than the viewport starts at its left edge
#[test]
fn a_column_wider_than_the_viewport_is_aligned_with_the_left_edge() {
let mut space = strip(options(0.25), 4);
space.set_width(&2, ColumnWidth::Fixed(2000));
space.focus(&2);
assert_eq!(rect(&space, 2).x, 0);
assert_eq!(rect(&space, 2).width, 2000);
// and still is after going away and coming back
space.focus(&0);
space.focus(&2);
assert_eq!(rect(&space, 2).x, 0);
}
#[test]
fn a_column_as_wide_as_the_viewport_is_aligned_with_the_left_edge_too() {
let mut space = strip(options(0.25), 3);
space.set_width(&1, ColumnWidth::Fixed(1200));
space.focus(&1);
assert_eq!(rect(&space, 1).x, 0);
}
// Rule 6: proportions include the gaps
#[test]
fn columns_that_add_up_to_the_viewport_tile_it_whatever_the_gaps() {
for gaps in [0, 1, 7, 16, 25] {
for count in 1..=8_u32 {
let options = Options {
gaps,
..options(1.0 / f64::from(count))
};
let space = strip(options, count);
let layout = space.layout();
assert_eq!(
layout.first().unwrap().rect.x,
gaps,
"gaps {gaps}, columns {count}"
);
assert_eq!(
layout.last().unwrap().rect.right(),
1200 - gaps,
"gaps {gaps}, columns {count}"
);
for pair in layout.windows(2) {
let gap = pair[1].rect.x - pair[0].rect.right();
assert_eq!(
gap, gaps,
"gaps {gaps}, columns {count}, after column {}",
pair[0].id
);
}
}
}
}
#[test]
fn a_fixed_width_does_not_depend_on_the_gaps() {
let options = Options {
gaps: 40,
default_width: ColumnWidth::Fixed(500),
..Options::default()
};
let space = strip(options, 3);
for (_, width) in widths(&space) {
assert_eq!(width, 500);
}
}
#[test]
fn columns_leave_a_gap_at_the_top_and_bottom_too() {
let options = Options {
gaps: 16,
..options(0.5)
};
let space = strip(options, 1);
let rect = rect(&space, 0);
assert_eq!((rect.y, rect.bottom()), (16, 800 - 16));
}
// Rule 7: fullscreen is a column like any other
#[test]
fn a_fullscreen_column_can_be_scrolled_away_from_and_back_to() {
let mut space = strip(options(0.5), 3);
space.focus(&1);
space.set_fullscreen(&1, true);
assert_eq!(placement(&space, 0).visibility, Visibility::OffLeft);
assert_eq!(placement(&space, 2).visibility, Visibility::OffRight);
// focus moves on to the next column, which is brought in from the right
space.focus(&2);
assert_eq!(placement(&space, 2).visibility, Visibility::Visible);
assert_eq!(rect(&space, 2).right(), 1200);
// the fullscreen column is still fullscreen, and still where it was in the strip: it is as
// wide as the viewport, so with the 600 of column 2 in view, half of it is still showing
let fullscreen = placement(&space, 1);
assert_eq!(fullscreen.rect.width, 1200);
assert_eq!(fullscreen.rect.height, 800);
assert_eq!(fullscreen.rect.x, -600);
assert_eq!(fullscreen.visibility, Visibility::Clipped);
space.focus(&1);
assert_eq!(
rect(&space, 1),
Rect {
x: 0,
y: 0,
width: 1200,
height: 800
}
);
}
#[test]
fn a_fullscreen_column_ignores_the_gaps() {
let options = Options {
gaps: 16,
..options(0.5)
};
let mut space = strip(options, 2);
space.set_fullscreen(&1, true);
assert_eq!(
rect(&space, 1),
Rect {
x: 0,
y: 0,
width: 1200,
height: 800
}
);
}
// Centring
fn centered(mode: CenterFocusedColumn) -> Options {
Options {
center_focused_column: mode,
..options(0.25)
}
}
#[test]
fn with_always_the_focused_column_is_always_centred() {
let mut space = strip(centered(CenterFocusedColumn::Always), 7);
for id in [3, 0, 6, 2, 4] {
space.focus(&id);
assert_eq!(rect(&space, id).x, 450, "column {id}");
}
}
#[test]
fn a_column_that_is_too_wide_to_centre_is_aligned_with_the_left_edge() {
let mut space = strip(centered(CenterFocusedColumn::Always), 3);
space.set_width(&1, ColumnWidth::Fixed(1500));
space.focus(&1);
assert_eq!(rect(&space, 1).x, 0);
}
#[test]
fn with_never_a_column_in_view_is_not_centred() {
let mut space = strip(centered(CenterFocusedColumn::Never), 3);
assert_eq!(rect(&space, 0).x, 0);
space.focus(&0);
assert_eq!(rect(&space, 0).x, 0);
}
#[test]
fn on_overflow_a_column_that_fits_beside_the_last_one_is_just_brought_in() {
// 480 wide: two of them are 960, which fits
let options = Options {
center_focused_column: CenterFocusedColumn::OnOverflow,
..options(0.4)
};
let mut space = strip(options, 4);
space.focus(&0);
assert_eq!(rect(&space, 0).x, 0);
space.focus(&1);
assert_eq!(rect(&space, 1).x, 480, "in view, so it does not move");
space.focus(&2);
assert_eq!(rect(&space, 2).right(), 1200, "brought in from the right");
}
#[test]
fn on_overflow_a_column_that_does_not_fit_beside_the_last_one_is_centred() {
// 720 wide: two of them are 1440, which does not
let options = Options {
center_focused_column: CenterFocusedColumn::OnOverflow,
..options(0.6)
};
let mut space = strip(options, 4);
space.focus(&0);
space.focus(&1);
assert_eq!(rect(&space, 1).x, 240, "(1200 - 720) / 2");
space.focus(&0);
assert_eq!(rect(&space, 0).x, 240);
}
#[test]
fn a_lone_column_can_be_kept_centred_whatever_the_mode() {
let options = Options {
always_center_single_column: true,
..options(0.5)
};
let mut space = strip(options, 1);
assert_eq!(rect(&space, 0).x, 300);
space.add_column(1, None, None, true);
assert_ne!(
rect(&space, 1).x,
300,
"with two columns it is the usual rule again"
);
space.remove_column(&1);
assert_eq!(rect(&space, 0).x, 300);
}
#[test]
fn centre_column_centres_the_focused_column_once() {
let mut space = strip(options(0.25), 8);
space.focus(&6);
space.center_column();
assert_eq!(rect(&space, 6).x, 450);
// it is not sticky: the next column that is in view does not move the strip
space.focus(&7);
assert_eq!(rect(&space, 6).x, 450);
}
#[test]
fn centre_visible_columns_centres_the_group_that_is_in_view() {
let mut space = strip(options(0.25), 2);
assert_eq!((x_of(&space, 0), x_of(&space, 1)), (0, 300));
space.center_visible_columns();
assert_eq!((x_of(&space, 0), x_of(&space, 1)), (300, 600));
}
#[test]
fn centre_visible_columns_does_nothing_if_the_focused_column_is_not_fully_in_view() {
let mut space = strip(options(0.25), 8);
space.set_width(&7, ColumnWidth::Fixed(2000));
let before = rects(&space);
space.center_visible_columns();
assert_eq!(rects(&space), before);
}
#[test]
fn expand_fills_what_the_columns_in_view_leave_free() {
let mut space = strip(options(0.25), 2);
space.focus(&0);
space.expand_to_available_width();
assert_eq!(width_of(&space, 0), 900);
assert_eq!(x_of(&space, 0), 0);
assert_eq!(x_of(&space, 1), 900);
assert_eq!(rect(&space, 1).right(), 1200);
}
#[test]
fn expand_on_the_only_column_in_view_makes_it_full_width_which_can_be_undone() {
let mut space = strip(options(0.25), 1);
space.expand_to_available_width();
assert_eq!(width_of(&space, 0), 1200);
space.toggle_full_width(&0);
assert_eq!(width_of(&space, 0), 300);
}
#[test]
fn expand_again_does_not_make_the_column_full_width_when_the_columns_add_up_to_a_fraction_over() {
// thirds of 5092 are 1697.33 wide, so what is left after one that was made 200 wider does
// not come to a whole number of pixels
let mut space = ScrollingSpace::new(
options(1.0 / 3.0),
Viewport {
width: 5092,
height: 800,
},
);
space.add_column(0, None, None, true);
space.add_column(1, None, None, true);
space.set_width(&1, ColumnWidth::Fixed(1897));
space.expand_to_available_width();
let once = rects(&space);
assert_eq!(width_of(&space, 1), 3394);
for _ in 0..3 {
space.expand_to_available_width();
assert_eq!(rects(&space), once);
}
}
// Widths
#[test]
fn presets_are_stepped_through_in_order_and_wrap() {
let mut space = strip(options(0.5), 1);
// 1/3, 1/2 and 2/3 are the default presets
let mut seen = vec![];
for _ in 0..4 {
space.cycle_preset_width(&0, true);
seen.push(width_of(&space, 0));
}
// a column at 1/2 is not at a preset yet, so the first step goes to the next one up
assert_eq!(seen, [800, 400, 600, 800]);
}
#[test]
fn presets_can_be_stepped_through_backwards() {
let mut space = strip(options(0.5), 1);
let mut seen = vec![];
for _ in 0..4 {
space.cycle_preset_width(&0, false);
seen.push(width_of(&space, 0));
}
assert_eq!(seen, [400, 800, 600, 400]);
}
#[test]
fn a_column_narrower_than_every_preset_starts_at_the_first_one() {
let mut space = strip(options(0.25), 1);
space.cycle_preset_width(&0, true);
assert_eq!(width_of(&space, 0), 400);
}
#[test]
fn setting_a_width_by_hand_forgets_the_preset() {
let mut space = strip(options(0.5), 1);
space.cycle_preset_width(&0, true);
assert_eq!(width_of(&space, 0), 800);
space.set_width(&0, ColumnWidth::Proportion(0.5));
space.cycle_preset_width(&0, true);
// from 600 the next one up is 800 again, not the one after 800
assert_eq!(width_of(&space, 0), 800);
}
#[test]
fn full_width_takes_the_whole_viewport_less_the_gaps_and_gives_the_width_back() {
// with gaps of 8 a quarter is (1200 - 8) / 4 - 8 = 290, which is a whole number of pixels
let options = Options {
gaps: 8,
..options(0.25)
};
let mut space = strip(options, 2);
assert_eq!(width_of(&space, 1), 290);
space.toggle_full_width(&1);
assert_eq!(width_of(&space, 1), 1200 - 16);
assert_eq!(x_of(&space, 1), 8);
space.toggle_full_width(&1);
assert_eq!(width_of(&space, 1), 290);
}
#[test]
fn a_width_limit_that_is_found_out_is_respected_and_the_columns_after_it_make_room() {
let mut space = strip(options(0.25), 3);
space.focus(&0);
let next = x_of(&space, 1);
space.set_width_limits(&0, Some(700), None);
assert_eq!(width_of(&space, 0), 700);
assert_eq!(x_of(&space, 1), next + 400);
space.set_width_limits(&0, None, None);
assert_eq!(width_of(&space, 0), 300);
assert_eq!(x_of(&space, 1), next);
}
#[test]
fn the_limits_that_a_column_was_given_can_be_read_back() {
let mut space = strip(options(0.25), 2);
assert_eq!(space.width_limits(&0), Some((None, None)));
assert_eq!(space.width_limits(&9), None);
space.set_width_limits(&0, Some(700), None);
space.set_width_limits(&1, None, Some(200));
assert_eq!(space.width_limits(&0), Some((Some(700), None)));
assert_eq!(space.width_limits(&1), Some((None, Some(200))));
// a column that goes takes its limits with it
space.remove_column(&0);
assert_eq!(space.width_limits(&0), None);
}
#[test]
fn a_maximum_width_limits_a_column() {
let mut space = strip(options(0.5), 1);
space.set_width_limits(&0, None, Some(400));
assert_eq!(width_of(&space, 0), 400);
}
#[test]
fn resetting_the_widths_puts_every_column_back_to_the_default() {
let mut space = strip(options(0.25), 5);
space.set_width(&0, ColumnWidth::Fixed(555));
space.cycle_preset_width(&1, true);
space.toggle_full_width(&2);
space.set_width(&3, ColumnWidth::Proportion(0.75));
space.add_column(9, None, Some(ColumnWidth::Proportion(0.4)), false);
space.reset_widths();
assert!(widths(&space).iter().all(|(_, width)| *width == 300));
}
#[test]
fn columns_that_were_reset_follow_the_default_again() {
let mut space = strip(options(0.25), 3);
space.set_width(&1, ColumnWidth::Fixed(500));
space.reset_widths();
space.set_options(options(0.5));
assert!(widths(&space).iter().all(|(_, width)| *width == 600));
}
#[test]
fn resetting_the_widths_forgets_the_preset_cycling_was_at() {
let mut space = strip(options(0.5), 1);
space.cycle_preset_width(&0, true);
space.cycle_preset_width(&0, true);
assert_eq!(width_of(&space, 0), 400);
space.reset_widths();
space.cycle_preset_width(&0, true);
// from 600 the first step up is 800, not the preset after 400
assert_eq!(width_of(&space, 0), 800);
}
#[test]
fn resetting_the_widths_keeps_the_left_edge_of_the_active_column() {
let mut space = strip(options(0.25), 6);
space.focus(&3);
space.set_width(&1, ColumnWidth::Fixed(500));
space.set_width(&2, ColumnWidth::Fixed(200));
let edge = x_of(&space, 3);
space.reset_widths();
// the columns before it are wider or narrower, and it has not moved
assert_eq!(x_of(&space, 3), edge);
assert_eq!(space.active_id(), Some(&3));
}
#[test]
fn resetting_the_widths_scrolls_the_active_column_only_as_far_as_it_has_to() {
let mut space = strip(options(0.25), 3);
space.set_width(&2, ColumnWidth::Fixed(1000));
assert_eq!(x_of(&space, 2), 200);
space.reset_widths();
// the column is a quarter of the viewport again, and its left edge is where it was
assert_eq!(width_of(&space, 2), 300);
assert_eq!(x_of(&space, 2), 200);
}
#[test]
fn resetting_the_widths_brings_the_active_column_back_in_when_it_grows_past_the_edge() {
let mut space = strip(options(0.25), 6);
space.set_width(&5, ColumnWidth::Fixed(100));
assert_eq!(x_of(&space, 5), 900);
// the default is wider than the column was, and it would end past the right edge
space.set_options(options(0.5));
space.reset_widths();
assert_eq!(width_of(&space, 5), 600);
assert_eq!(x_of(&space, 5), 600);
}
#[test]
fn resetting_the_widths_keeps_what_was_found_out_about_a_window() {
let mut space = strip(options(0.25), 3);
space.set_width_limits(&1, Some(700), None);
space.set_width_limits(&2, None, Some(200));
space.set_width(&1, ColumnWidth::Fixed(500));
space.reset_widths();
assert_eq!(widths(&space), [(0, 300), (1, 700), (2, 200)]);
}
#[test]
fn resetting_the_widths_leaves_fullscreen_alone() {
let mut space = strip(options(0.25), 3);
space.focus(&1);
space.set_fullscreen(&1, true);
space.set_width(&2, ColumnWidth::Fixed(500));
space.reset_widths();
assert_eq!(width_of(&space, 1), 1200);
assert_eq!(width_of(&space, 2), 300);
space.set_fullscreen(&1, false);
assert_eq!(width_of(&space, 1), 300);
}
#[test]
fn resetting_the_widths_of_an_empty_strip_does_nothing() {
let mut space = ScrollingSpace::<u32>::new(options(0.25), VIEW);
space.reset_widths();
assert!(space.is_empty());
}
// The viewport and the options
#[test]
fn proportions_follow_the_viewport() {
let mut space = strip(options(0.5), 2);
space.set_viewport(Viewport {
width: 1000,
height: 600,
});
assert_eq!(widths(&space), [(0, 500), (1, 500)]);
assert_eq!(rect(&space, 0).height, 600);
assert_eq!(placement(&space, 1).visibility, Visibility::Visible);
}
#[test]
fn shrinking_the_viewport_keeps_the_focused_column_in_view() {
let mut space = strip(
Options {
default_width: ColumnWidth::Fixed(500),
..Options::default()
},
3,
);
assert_eq!(placement(&space, 2).visibility, Visibility::Visible);
space.set_viewport(Viewport {
width: 700,
height: 800,
});
assert_eq!(placement(&space, 2).visibility, Visibility::Visible);
}
#[test]
fn changing_the_options_applies_them_to_the_columns_there_are() {
let mut space = strip(options(0.25), 3);
space.set_options(Options {
gaps: 10,
center_focused_column: CenterFocusedColumn::Always,
..options(0.25)
});
// each is 1190 * 0.25 - 10 = 287.5 wide, and the focused one is centred
assert_eq!(placement(&space, 2).rect.x, (1200 - 287) / 2);
}
#[test]
fn a_negative_gap_is_no_gap() {
let space = strip(
Options {
gaps: -5,
..options(0.5)
},
2,
);
assert_eq!(space.options().gaps, 0);
assert_eq!(widths(&space), [(0, 600), (1, 600)]);
}
// Visibility
#[test]
fn every_column_says_how_much_of_it_is_in_view() {
let mut space = strip(options(0.25), 8);
space.focus(&3);
space.set_width(&3, ColumnWidth::Fixed(450));
let visibility: Vec<_> = space
.layout()
.into_iter()
.map(|placement| placement.visibility)
.collect();
// column 3 is at the left edge and is 450 wide, so column 4 starts at 450 and column 5 at
// 750, and 6 at 1050, which ends at 1350: past the right edge
assert_eq!(
visibility,
[
Visibility::OffLeft,
Visibility::OffLeft,
Visibility::OffLeft,
Visibility::Visible,
Visibility::Visible,
Visibility::Visible,
Visibility::Clipped,
Visibility::OffRight,
]
);
}
#[test]
fn a_column_that_ends_exactly_at_the_left_edge_is_off_it() {
let mut space = strip(options(0.25), 8);
space.focus(&4);
assert_eq!(rect(&space, 3).right(), 0);
assert_eq!(placement(&space, 3).visibility, Visibility::OffLeft);
}
#[test]
fn exactly_one_column_is_active() {
let mut space = strip(options(0.25), 5);
space.focus(&2);
let active: Vec<_> = space
.layout()
.into_iter()
.filter(|placement| placement.active)
.map(|placement| placement.id)
.collect();
assert_eq!(active, [2]);
assert_eq!(space.active_index(), Some(2));
}
// Reconciling with a list that the host keeps
#[test]
fn reconcile_gives_the_same_strip_as_making_each_change_through_the_model() {
// a window opens (and takes focus), another opens, the first one closes, focus moves, and a
// window moves one place to the left
let steps: [(&[u32], u32); 5] = [
(&[0, 1, 2, 3, 4, 5], 5),
(&[0, 1, 2, 3, 4, 5, 6], 6),
(&[0, 2, 3, 4, 5, 6], 2),
(&[0, 2, 3, 4, 5, 6], 5),
(&[0, 2, 3, 5, 4, 6], 5),
];
let mut reconciled = ScrollingSpace::new(options(0.25), VIEW);
let mut direct = ScrollingSpace::new(options(0.25), VIEW);
for id in 0..6 {
direct.add_column(id, None, None, true);
}
reconciled.reconcile(steps[0].0, Some(&steps[0].1));
assert_eq!(rects(&reconciled), rects(&direct));
direct.add_column(6, None, None, true);
reconciled.reconcile(steps[1].0, Some(&steps[1].1));
assert_eq!(rects(&reconciled), rects(&direct));
direct.remove_column(&1);
direct.focus(&2);
reconciled.reconcile(steps[2].0, Some(&steps[2].1));
assert_eq!(rects(&reconciled), rects(&direct));
direct.focus(&5);
reconciled.reconcile(steps[3].0, Some(&steps[3].1));
assert_eq!(rects(&reconciled), rects(&direct));
direct.move_column(&5, 3);
reconciled.reconcile(steps[4].0, Some(&steps[4].1));
assert_eq!(rects(&reconciled), rects(&direct));
assert_eq!(reconciled.ids(), [0, 2, 3, 5, 4, 6]);
}
#[test]
fn reconcile_keeps_the_width_of_columns_that_were_already_there() {
let mut space = strip(options(0.25), 3);
space.set_width(&1, ColumnWidth::Fixed(777));
space.reconcile(&[0, 1, 2, 3], Some(&3));
assert_eq!(width_of(&space, 1), 777);
assert_eq!(width_of(&space, 3), 300);
}
#[test]
fn reconcile_ignores_a_key_that_is_listed_twice() {
let mut space = ScrollingSpace::new(options(0.25), VIEW);
space.reconcile(&[1, 2, 2, 3, 1], Some(&3));
assert_eq!(space.ids(), [1, 2, 3]);
}
#[test]
fn reconcile_with_nothing_empties_the_strip() {
let mut space = strip(options(0.25), 3);
space.reconcile(&[], None);
assert!(space.is_empty());
}
#[test]
fn reconcile_without_a_focused_key_leaves_focus_where_it_is() {
let mut space = strip(options(0.25), 4);
space.focus(&1);
space.reconcile(&[0, 1, 2, 3], None);
assert_eq!(space.active_id(), Some(&1));
}
#[test]
fn reconcile_when_the_focused_column_is_the_one_that_went_gives_focus_to_what_the_host_says() {
let mut space = strip(options(0.25), 4);
space.focus(&1);
space.reconcile(&[0, 2, 3], Some(&3));
assert_eq!(space.active_id(), Some(&3));
assert_eq!(space.ids(), [0, 2, 3]);
}
#[test]
fn focus_movement_helpers_report_whether_they_moved() {
let mut space = strip(options(0.25), 3);
space.focus(&0);
assert!(!space.focus_left());
assert!(space.focus_right());
assert!(space.focus_right());
assert!(!space.focus_right());
assert!(space.focus_left());
assert!(!space.focus(&99));
assert!(!space.focus_index(99));
assert!(!space.remove_column(&99));
assert_eq!(space.active_id(), Some(&1));
let mut empty: ScrollingSpace<u32> = ScrollingSpace::new(options(0.25), VIEW);
assert!(!empty.focus_left());
assert!(!empty.focus_right());
}
#[test]
fn moving_the_focused_column_takes_focus_and_the_viewport_with_it() {
let mut space = strip(options(0.25), 8);
space.focus(&5);
space.move_column(&5, 1);
assert_eq!(space.active_id(), Some(&5));
assert_eq!(space.active_index(), Some(1));
assert_eq!(placement(&space, 5).visibility, Visibility::Visible);
assert_eq!(rect(&space, 5).x, 0);
}
// Asking questions, and comparing
#[test]
fn a_column_can_be_asked_how_wide_it_is() {
let mut space = strip(options(0.25), 3);
assert_eq!(space.column_width(&1), Some(300.0));
space.set_width(&1, ColumnWidth::Fixed(555));
assert_eq!(space.column_width(&1), Some(555.0));
space.toggle_full_width(&1);
assert_eq!(space.column_width(&1), Some(1200.0));
assert_eq!(space.column_width(&99), None);
}
#[test]
fn the_width_that_is_asked_for_is_not_rounded_to_pixels() {
// with gaps of 10 a quarter is (1200 - 10) / 4 - 10 = 287.5 wide
let options = Options {
gaps: 10,
..options(0.25)
};
let space = strip(options, 2);
assert_eq!(space.column_width(&0), Some(287.5));
}
// The default width
#[test]
fn columns_that_were_never_given_a_width_follow_the_default() {
let mut space = strip(options(0.25), 4);
space.set_width(&1, ColumnWidth::Fixed(500));
space.add_column(9, Some(4), Some(ColumnWidth::Proportion(0.4)), false);
space.set_options(options(0.5));
// 1 was sized by hand and 9 was opened with a width of its own, so neither follows
assert_eq!(
widths(&space),
[(0, 600), (1, 500), (2, 600), (3, 600), (9, 480)]
);
}
#[test]
fn a_column_that_is_sized_stops_following_the_default() {
let mut space = strip(options(0.25), 5);
space.cycle_preset_width(&1, true);
space.set_width(&2, ColumnWidth::Proportion(0.25));
space.focus(&3);
space.expand_to_available_width();
let before = widths(&space);
space.set_options(options(0.5));
for id in [1, 2, 3] {
let id_width = |widths: &[(u32, i32)]| widths.iter().find(|w| w.0 == id).unwrap().1;
assert_eq!(id_width(&widths(&space)), id_width(&before), "column {id}");
}
// and 0 and 4 do
assert_eq!(width_of(&space, 0), 600);
assert_eq!(width_of(&space, 4), 600);
}
#[test]
fn a_column_that_is_toggled_to_full_width_and_back_follows_the_default_again() {
let mut space = strip(options(0.25), 2);
space.toggle_full_width(&1);
space.set_options(options(0.5));
space.toggle_full_width(&1);
assert_eq!(width_of(&space, 1), 600);
}
#[test]
fn opening_a_column_never_resizes_the_others_even_though_they_follow_the_default() {
let mut space = strip(options(0.25), 3);
// the default is not touched by opening a column, so nothing follows anything anywhere
space.add_column(9, None, None, true);
assert!(widths(&space).iter().all(|(_, width)| *width == 300));
}
#[test]
fn a_strip_equals_one_that_was_made_the_same_way() {
let mut one = strip(options(0.25), 4);
let two = strip(options(0.25), 4);
assert_eq!(one, two);
one.focus(&0);
assert_ne!(one, two, "the viewport is part of what is compared");
let mut three = strip(options(0.25), 4);
three.focus(&0);
assert_eq!(one, three);
}