Files
komorebi/komorebi-layouts/src/arrangement_tests.rs
2026-03-22 16:11:43 -07:00

1846 lines
57 KiB
Rust

use super::*;
use std::num::NonZeroUsize;
// Helper to create a test area
fn test_area() -> Rect {
Rect {
left: 0,
top: 0,
right: 1000,
bottom: 800,
}
}
// Helper to create LayoutOptions with column ratios
fn layout_options_with_column_ratios(ratios: &[f32]) -> LayoutOptions {
let mut arr = [None; MAX_RATIOS];
for (i, &r) in ratios.iter().take(MAX_RATIOS).enumerate() {
arr[i] = Some(r);
}
LayoutOptions {
scrolling: None,
grid: None,
column_ratios: Some(arr),
row_ratios: None,
}
}
// Helper to create LayoutOptions with row ratios
fn layout_options_with_row_ratios(ratios: &[f32]) -> LayoutOptions {
let mut arr = [None; MAX_RATIOS];
for (i, &r) in ratios.iter().take(MAX_RATIOS).enumerate() {
arr[i] = Some(r);
}
LayoutOptions {
scrolling: None,
grid: None,
column_ratios: None,
row_ratios: Some(arr),
}
}
// Helper to create LayoutOptions with both column and row ratios
fn layout_options_with_ratios(column_ratios: &[f32], row_ratios: &[f32]) -> LayoutOptions {
let mut col_arr = [None; MAX_RATIOS];
for (i, &r) in column_ratios.iter().take(MAX_RATIOS).enumerate() {
col_arr[i] = Some(r);
}
let mut row_arr = [None; MAX_RATIOS];
for (i, &r) in row_ratios.iter().take(MAX_RATIOS).enumerate() {
row_arr[i] = Some(r);
}
LayoutOptions {
scrolling: None,
grid: None,
column_ratios: Some(col_arr),
row_ratios: Some(row_arr),
}
}
fn assert_containers_adjacent_horizontally(layouts: &[Rect], area: &Rect) {
let mut sorted: Vec<&Rect> = layouts.iter().collect();
sorted.sort_by_key(|r| r.left);
for pair in sorted.windows(2) {
assert_eq!(
pair[0].left + pair[0].right,
pair[1].left,
"gap between containers at left={} (width {}) and left={}",
pair[0].left,
pair[0].right,
pair[1].left,
);
}
let rightmost = sorted.last().unwrap();
assert_eq!(
rightmost.left + rightmost.right,
area.left + area.right,
"rightmost container does not reach the area edge",
);
}
fn assert_containers_adjacent_vertically(layouts: &[Rect], area: &Rect) {
let mut sorted: Vec<&Rect> = layouts.iter().collect();
sorted.sort_by_key(|r| r.top);
for pair in sorted.windows(2) {
assert_eq!(
pair[0].top + pair[0].bottom,
pair[1].top,
"gap between containers at top={} (height {}) and top={}",
pair[0].top,
pair[0].bottom,
pair[1].top,
);
}
let bottommost = sorted.last().unwrap();
assert_eq!(
bottommost.top + bottommost.bottom,
area.top + area.bottom,
"bottommost container does not reach the area edge",
);
}
mod columns_with_ratios_tests {
use super::*;
#[test]
fn test_columns_equal_width_no_ratios() {
let area = test_area();
let layouts = columns_with_ratios(&area, 4, None);
assert_eq!(layouts.len(), 4);
// Each column should be 250 pixels wide (1000 / 4)
for layout in &layouts {
assert_eq!(layout.right, 250);
assert_eq!(layout.bottom, 800);
}
}
#[test]
fn test_columns_with_single_ratio() {
let area = test_area();
let opts = layout_options_with_column_ratios(&[0.3]);
let layouts = columns_with_ratios(&area, 3, opts.column_ratios);
assert_eq!(layouts.len(), 3);
// First column: 30% of 1000 = 300
assert_eq!(layouts[0].right, 300);
// Remaining 700 split between 2 columns = 350 each
assert_eq!(layouts[1].right, 350);
assert_eq!(layouts[2].right, 350);
}
#[test]
fn test_columns_with_multiple_ratios() {
let area = test_area();
let opts = layout_options_with_column_ratios(&[0.2, 0.3, 0.5]);
let layouts = columns_with_ratios(&area, 4, opts.column_ratios);
assert_eq!(layouts.len(), 4);
// First column: 20% of 1000 = 200
assert_eq!(layouts[0].right, 200);
// Second column: 30% of 1000 = 300
assert_eq!(layouts[1].right, 300);
// Third column: 50% of 1000 = 500
// But wait - cumulative is 1.0, so third might be truncated
// Let's check what actually happens
// Actually, the sum 0.2 + 0.3 = 0.5 < 1.0, and 0.5 + 0.5 = 1.0
// So 0.5 won't be included because cumulative would reach 1.0
}
#[test]
fn test_columns_positions_are_correct() {
let area = test_area();
let opts = layout_options_with_column_ratios(&[0.3, 0.4]);
let layouts = columns_with_ratios(&area, 3, opts.column_ratios);
// First column starts at 0
assert_eq!(layouts[0].left, 0);
// Second column starts where first ends
assert_eq!(layouts[1].left, layouts[0].right);
// Third column starts where second ends
assert_eq!(layouts[2].left, layouts[1].left + layouts[1].right);
}
#[test]
fn test_columns_last_column_gets_remaining_space() {
let area = test_area();
let opts = layout_options_with_column_ratios(&[0.3]);
let layouts = columns_with_ratios(&area, 2, opts.column_ratios);
assert_eq!(layouts.len(), 2);
// First column: 30% = 300
assert_eq!(layouts[0].right, 300);
// Last column gets remaining space: 700
assert_eq!(layouts[1].right, 700);
}
#[test]
fn test_columns_single_column() {
let area = test_area();
let opts = layout_options_with_column_ratios(&[0.5]);
let layouts = columns_with_ratios(&area, 1, opts.column_ratios);
assert_eq!(layouts.len(), 1);
// Single column takes full width regardless of ratio
assert_eq!(layouts[0].right, 1000);
}
#[test]
fn test_columns_more_columns_than_ratios() {
let area = test_area();
let opts = layout_options_with_column_ratios(&[0.2]);
let layouts = columns_with_ratios(&area, 5, opts.column_ratios);
assert_eq!(layouts.len(), 5);
// First column: 20% = 200
assert_eq!(layouts[0].right, 200);
// Remaining 800 split among 4 columns = 200 each
for item in layouts.iter().take(5).skip(1) {
assert_eq!(item.right, 200);
}
}
#[test]
fn test_columns_cover_full_width_no_ratios() {
// 1000 / 3 = 333, 333*3 = 999 => 1px remainder
let area = test_area();
let layouts = columns_with_ratios(&area, 3, None);
let total_width: i32 = layouts.iter().map(|r| r.right).sum();
assert_eq!(
total_width, area.right,
"columns should cover full width, got {total_width} expected {}",
area.right,
);
let last = layouts.last().unwrap();
let right_edge = last.left + last.right;
assert_eq!(right_edge, area.left + area.right);
}
#[test]
fn test_columns_cover_full_width_with_ratios() {
// ratio=0.3 with 4 columns: col0=300, remaining 700/3=233, 233*3=699 => 1px remainder
let area = test_area();
let opts = layout_options_with_column_ratios(&[0.3]);
let layouts = columns_with_ratios(&area, 4, opts.column_ratios);
let total_width: i32 = layouts.iter().map(|r| r.right).sum();
assert_eq!(
total_width, area.right,
"columns should cover full width, got {total_width} expected {}",
area.right,
);
let last = layouts.last().unwrap();
let right_edge = last.left + last.right;
assert_eq!(right_edge, area.left + area.right);
}
}
mod rows_with_ratios_tests {
use super::*;
#[test]
fn test_rows_equal_height_no_ratios() {
let area = test_area();
let layouts = rows_with_ratios(&area, 4, None);
assert_eq!(layouts.len(), 4);
// Each row should be 200 pixels tall (800 / 4)
for layout in &layouts {
assert_eq!(layout.bottom, 200);
assert_eq!(layout.right, 1000);
}
}
#[test]
fn test_rows_with_single_ratio() {
let area = test_area();
let opts = layout_options_with_row_ratios(&[0.5]);
let layouts = rows_with_ratios(&area, 3, opts.row_ratios);
assert_eq!(layouts.len(), 3);
// First row: 50% of 800 = 400
assert_eq!(layouts[0].bottom, 400);
// Remaining 400 split between 2 rows = 200 each
assert_eq!(layouts[1].bottom, 200);
assert_eq!(layouts[2].bottom, 200);
}
#[test]
fn test_rows_positions_are_correct() {
let area = test_area();
let opts = layout_options_with_row_ratios(&[0.25, 0.25]);
let layouts = rows_with_ratios(&area, 3, opts.row_ratios);
// First row starts at top
assert_eq!(layouts[0].top, 0);
// Second row starts where first ends
assert_eq!(layouts[1].top, layouts[0].bottom);
// Third row starts where second ends
assert_eq!(layouts[2].top, layouts[1].top + layouts[1].bottom);
}
#[test]
fn test_rows_last_row_gets_remaining_space() {
let area = test_area();
let opts = layout_options_with_row_ratios(&[0.25]);
let layouts = rows_with_ratios(&area, 2, opts.row_ratios);
assert_eq!(layouts.len(), 2);
// First row: 25% of 800 = 200
assert_eq!(layouts[0].bottom, 200);
// Last row gets remaining: 600
assert_eq!(layouts[1].bottom, 600);
}
#[test]
fn test_rows_cover_full_height_no_ratios() {
// 800 / 3 = 266, 266*3 = 798 => 2px remainder
let area = test_area();
let layouts = rows_with_ratios(&area, 3, None);
let total_height: i32 = layouts.iter().map(|r| r.bottom).sum();
assert_eq!(
total_height, area.bottom,
"rows should cover full height, got {total_height} expected {}",
area.bottom,
);
let last = layouts.last().unwrap();
let bottom_edge = last.top + last.bottom;
assert_eq!(bottom_edge, area.top + area.bottom);
}
#[test]
fn test_rows_cover_full_height_with_ratios() {
// ratio=0.3 with 4 rows: row0=240, remaining 560/3=186, 186*3=558 => 2px remainder
let area = test_area();
let opts = layout_options_with_row_ratios(&[0.3]);
let layouts = rows_with_ratios(&area, 4, opts.row_ratios);
let total_height: i32 = layouts.iter().map(|r| r.bottom).sum();
assert_eq!(
total_height, area.bottom,
"rows should cover full height, got {total_height} expected {}",
area.bottom,
);
let last = layouts.last().unwrap();
let bottom_edge = last.top + last.bottom;
assert_eq!(bottom_edge, area.top + area.bottom);
}
}
mod vertical_stack_layout_tests {
use super::*;
#[test]
fn test_vertical_stack_default_ratio() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let layouts =
DefaultLayout::VerticalStack.calculate(&area, len, None, None, &[], 0, None, &[]);
assert_eq!(layouts.len(), 3);
// Primary column should be 50% (default ratio)
assert_eq!(layouts[0].right, 500);
}
#[test]
fn test_vertical_stack_custom_ratio() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let opts = layout_options_with_column_ratios(&[0.7]);
let layouts =
DefaultLayout::VerticalStack.calculate(&area, len, None, None, &[], 0, Some(opts), &[]);
assert_eq!(layouts.len(), 3);
// Primary column should be 70%
assert_eq!(layouts[0].right, 700);
// Stack columns should share remaining 30%
assert_eq!(layouts[1].right, 300);
assert_eq!(layouts[2].right, 300);
}
#[test]
fn test_vertical_stack_with_row_ratios() {
let area = test_area();
let len = NonZeroUsize::new(4).unwrap();
let opts = layout_options_with_ratios(&[0.6], &[0.5, 0.3]);
let layouts =
DefaultLayout::VerticalStack.calculate(&area, len, None, None, &[], 0, Some(opts), &[]);
assert_eq!(layouts.len(), 4);
// Primary column: 60%
assert_eq!(layouts[0].right, 600);
// Stack rows should use row_ratios
// First stack row: 50% of 800 = 400
assert_eq!(layouts[1].bottom, 400);
// Second stack row: 30% of 800 = 240
assert_eq!(layouts[2].bottom, 240);
}
#[test]
fn test_vertical_stack_single_window() {
let area = test_area();
let len = NonZeroUsize::new(1).unwrap();
let opts = layout_options_with_column_ratios(&[0.6]);
let layouts =
DefaultLayout::VerticalStack.calculate(&area, len, None, None, &[], 0, Some(opts), &[]);
assert_eq!(layouts.len(), 1);
// Single window should take full width
assert_eq!(layouts[0].right, 1000);
}
}
mod horizontal_stack_layout_tests {
use super::*;
#[test]
fn test_horizontal_stack_default_ratio() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let layouts =
DefaultLayout::HorizontalStack.calculate(&area, len, None, None, &[], 0, None, &[]);
assert_eq!(layouts.len(), 3);
// Primary row should be 50% height (default ratio)
assert_eq!(layouts[0].bottom, 400);
}
#[test]
fn test_horizontal_stack_custom_ratio() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let opts = layout_options_with_row_ratios(&[0.7]);
let layouts = DefaultLayout::HorizontalStack.calculate(
&area,
len,
None,
None,
&[],
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 3);
// Primary row should be 70% height
assert_eq!(layouts[0].bottom, 560);
}
#[test]
fn test_horizontal_stack_columns_cover_full_width() {
// 4 windows: primary row + 3 stack columns
// stack width = 1000, 1000/3 = 333, 333*3 = 999 => 1px gap
let area = test_area();
let len = NonZeroUsize::new(4).unwrap();
let layouts =
DefaultLayout::HorizontalStack.calculate(&area, len, None, None, &[], 0, None, &[]);
// Stack windows (indices 1..4) share the bottom row
let stack = &layouts[1..];
let last = stack.last().unwrap();
let right_edge = last.left + last.right;
assert_eq!(
right_edge,
area.left + area.right,
"stack columns should cover full width, right edge is {right_edge} expected {}",
area.left + area.right,
);
}
}
mod vertical_stack_rows_cover_full_height_tests {
use super::*;
#[test]
fn test_vertical_stack_rows_cover_full_height() {
// 4 windows: primary column + 3 stack rows
// stack height = 800, 800/3 = 266, 266*3 = 798 => 2px gap
let area = test_area();
let len = NonZeroUsize::new(4).unwrap();
let layouts =
DefaultLayout::VerticalStack.calculate(&area, len, None, None, &[], 0, None, &[]);
// Stack windows (indices 1..4) share the right column
let stack = &layouts[1..];
let last = stack.last().unwrap();
let bottom_edge = last.top + last.bottom;
assert_eq!(
bottom_edge,
area.top + area.bottom,
"stack rows should cover full height, bottom edge is {bottom_edge} expected {}",
area.top + area.bottom,
);
}
}
mod scrolling_layout_tests {
use super::*;
#[test]
fn test_scrolling_visible_columns_cover_full_width() {
// 1921 / 3 = 640, 640*3 = 1920 => 1px gap
let area = Rect {
left: 0,
top: 0,
right: 1921,
bottom: 800,
};
let len = NonZeroUsize::new(5).unwrap();
let opts = LayoutOptions {
scrolling: Some(crate::ScrollingLayoutOptions {
columns: 3,
center_focused_column: None,
}),
grid: None,
column_ratios: None,
row_ratios: None,
};
let layouts =
DefaultLayout::Scrolling.calculate(&area, len, None, None, &[], 0, Some(opts), &[]);
// First 3 windows should be visible (focused_idx=0)
let visible = &layouts[0..3];
let last_visible = visible.last().unwrap();
let right_edge = last_visible.left + last_visible.right;
assert_eq!(
right_edge,
area.left + area.right,
"visible columns should cover full width, right edge is {right_edge} expected {}",
area.left + area.right,
);
}
}
mod ultrawide_layout_tests {
use super::*;
#[test]
fn test_ultrawide_default_ratios() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let layouts = DefaultLayout::UltrawideVerticalStack.calculate(
&area,
len,
None,
None,
&[],
0,
None,
&[],
);
assert_eq!(layouts.len(), 3);
// Primary (center): 50% = 500
assert_eq!(layouts[0].right, 500);
// Secondary (left): 25% = 250
assert_eq!(layouts[1].right, 250);
// Tertiary gets remaining: 250
assert_eq!(layouts[2].right, 250);
}
#[test]
fn test_ultrawide_custom_ratios() {
let area = test_area();
let len = NonZeroUsize::new(4).unwrap();
let opts = layout_options_with_column_ratios(&[0.5, 0.2]);
let layouts = DefaultLayout::UltrawideVerticalStack.calculate(
&area,
len,
None,
None,
&[],
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 4);
// Primary (center): 50% = 500
assert_eq!(layouts[0].right, 500);
// Secondary (left): 20% = 200
assert_eq!(layouts[1].right, 200);
// Tertiary column gets remaining: 300
assert_eq!(layouts[2].right, 300);
assert_eq!(layouts[3].right, 300);
}
#[test]
fn test_ultrawide_two_windows() {
let area = test_area();
let len = NonZeroUsize::new(2).unwrap();
let opts = layout_options_with_column_ratios(&[0.6]);
let layouts = DefaultLayout::UltrawideVerticalStack.calculate(
&area,
len,
None,
None,
&[],
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 2);
// Primary: 60% = 600
assert_eq!(layouts[0].right, 600);
// Secondary gets remaining: 400
assert_eq!(layouts[1].right, 400);
}
}
mod bsp_layout_tests {
use super::*;
#[test]
fn test_bsp_default_ratio() {
let area = test_area();
let len = NonZeroUsize::new(2).unwrap();
let layouts = DefaultLayout::BSP.calculate(&area, len, None, None, &[], 0, None, &[]);
assert_eq!(layouts.len(), 2);
// First window should be 50% width
assert_eq!(layouts[0].right, 500);
}
#[test]
fn test_bsp_custom_column_ratio() {
let area = test_area();
let len = NonZeroUsize::new(2).unwrap();
let opts = layout_options_with_column_ratios(&[0.7]);
let layouts = DefaultLayout::BSP.calculate(&area, len, None, None, &[], 0, Some(opts), &[]);
assert_eq!(layouts.len(), 2);
// First window should be 70% width
assert_eq!(layouts[0].right, 700);
}
#[test]
fn test_bsp_custom_row_ratio() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let opts = layout_options_with_ratios(&[0.5], &[0.7]);
let layouts = DefaultLayout::BSP.calculate(&area, len, None, None, &[], 0, Some(opts), &[]);
assert_eq!(layouts.len(), 3);
// Second window should be 70% of remaining height
assert_eq!(layouts[1].bottom, 560);
}
#[test]
fn test_bsp_horizontal_flip_no_gap_with_resize() {
let area = test_area();
let len = NonZeroUsize::new(2).unwrap();
let opts = layout_options_with_column_ratios(&[0.7]);
// Container 0 resized right by 50
let resize = [
Some(Rect {
left: 0,
top: 0,
right: 50,
bottom: 0,
}),
None,
];
let layouts = DefaultLayout::BSP.calculate(
&area,
len,
None,
Some(Axis::Horizontal),
&resize,
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 2);
assert_containers_adjacent_horizontally(&layouts, &area);
}
#[test]
fn test_bsp_vertical_flip_no_gap_with_resize() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let opts = layout_options_with_ratios(&[0.5], &[0.7]);
// Container 1 resized bottom by 50
let resize = [
None,
Some(Rect {
left: 0,
top: 0,
right: 0,
bottom: 50,
}),
None,
];
let layouts = DefaultLayout::BSP.calculate(
&area,
len,
None,
Some(Axis::Vertical),
&resize,
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 3);
// Containers 1 and 2 share the right column vertically
assert_containers_adjacent_vertically(
&layouts[1..],
&Rect {
left: layouts[1].left,
top: area.top,
right: layouts[1].right,
bottom: area.bottom,
},
);
}
#[test]
fn test_bsp_horizontal_and_vertical_flip_no_gap_with_resize() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let opts = layout_options_with_ratios(&[0.7], &[0.7]);
// Both containers resized
let resize = [
Some(Rect {
left: 0,
top: 0,
right: 50,
bottom: 0,
}),
Some(Rect {
left: 0,
top: 0,
right: 0,
bottom: 40,
}),
None,
];
let layouts = DefaultLayout::BSP.calculate(
&area,
len,
None,
Some(Axis::HorizontalAndVertical),
&resize,
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 3);
assert_containers_adjacent_horizontally(&[layouts[0], layouts[1]], &area);
assert_containers_adjacent_vertically(
&layouts[1..],
&Rect {
left: layouts[1].left,
top: area.top,
right: layouts[1].right,
bottom: area.bottom,
},
);
}
#[test]
fn test_bsp_flip_no_gap_across_multiple_ratios() {
let area = test_area();
for &ratio in &[0.3, 0.4, 0.6, 0.7, 0.8] {
let opts = layout_options_with_column_ratios(&[ratio]);
let len = NonZeroUsize::new(2).unwrap();
for &delta in &[25, 50, 100] {
let resize = [
Some(Rect {
left: 0,
top: 0,
right: delta,
bottom: 0,
}),
None,
];
let layouts = DefaultLayout::BSP.calculate(
&area,
len,
None,
Some(Axis::Horizontal),
&resize,
0,
Some(opts),
&[],
);
assert_containers_adjacent_horizontally(&layouts, &area);
}
}
}
}
mod right_main_vertical_stack_tests {
use super::*;
#[test]
fn test_right_main_default_ratio() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let layouts = DefaultLayout::RightMainVerticalStack.calculate(
&area,
len,
None,
None,
&[],
0,
None,
&[],
);
assert_eq!(layouts.len(), 3);
// Primary should be on the right, 50% width
assert_eq!(layouts[0].right, 500);
assert_eq!(layouts[0].left, 500); // Right side
}
#[test]
fn test_right_main_custom_ratio() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let opts = layout_options_with_column_ratios(&[0.6]);
let layouts = DefaultLayout::RightMainVerticalStack.calculate(
&area,
len,
None,
None,
&[],
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 3);
// Primary: 60% = 600
assert_eq!(layouts[0].right, 600);
// Should be positioned on the right
assert_eq!(layouts[0].left, 400);
}
}
mod columns_layout_tests {
use super::*;
#[test]
fn test_columns_layout_with_ratios() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let opts = layout_options_with_column_ratios(&[0.2, 0.5]);
let layouts =
DefaultLayout::Columns.calculate(&area, len, None, None, &[], 0, Some(opts), &[]);
assert_eq!(layouts.len(), 3);
assert_eq!(layouts[0].right, 200); // 20%
assert_eq!(layouts[1].right, 500); // 50%
assert_eq!(layouts[2].right, 300); // remaining
}
}
mod rows_layout_tests {
use super::*;
#[test]
fn test_rows_layout_with_ratios() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let opts = layout_options_with_row_ratios(&[0.25, 0.5]);
let layouts =
DefaultLayout::Rows.calculate(&area, len, None, None, &[], 0, Some(opts), &[]);
assert_eq!(layouts.len(), 3);
assert_eq!(layouts[0].bottom, 200); // 25%
assert_eq!(layouts[1].bottom, 400); // 50%
assert_eq!(layouts[2].bottom, 200); // remaining
}
}
mod grid_layout_tests {
use super::*;
#[test]
fn test_grid_with_column_ratios() {
let area = test_area();
let len = NonZeroUsize::new(4).unwrap();
let opts = layout_options_with_column_ratios(&[0.3]);
let layouts =
DefaultLayout::Grid.calculate(&area, len, None, None, &[], 0, Some(opts), &[]);
assert_eq!(layouts.len(), 4);
// Grid with 4 windows should be 2x2
// First column: 30% = 300
assert_eq!(layouts[0].right, 300);
assert_eq!(layouts[1].right, 300);
}
#[test]
fn test_grid_without_ratios() {
let area = test_area();
let len = NonZeroUsize::new(4).unwrap();
let layouts = DefaultLayout::Grid.calculate(&area, len, None, None, &[], 0, None, &[]);
assert_eq!(layouts.len(), 4);
// 2x2 grid, equal columns = 500 each
assert_eq!(layouts[0].right, 500);
assert_eq!(layouts[2].right, 500);
}
#[test]
fn test_grid_flip_horizontal_with_ratios_no_overlap() {
// 4 windows => 2x2 grid, column_ratios=[0.3]
// col 0: width=300 (30%), col 1: width=700 (remaining)
// With horizontal flip: col 1 (width 700) at left=0, col 0 (width 300) at left=700
let area = test_area(); // 1000x800
let opts = layout_options_with_column_ratios(&[0.3]);
let layouts = DefaultLayout::Grid.calculate(
&area,
NonZeroUsize::new(4).unwrap(),
None,
Some(Axis::Horizontal),
&[],
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 4);
// Group by left position
let mut columns: std::collections::BTreeMap<i32, Vec<&Rect>> =
std::collections::BTreeMap::new();
for layout in &layouts {
columns.entry(layout.left).or_default().push(layout);
}
assert_eq!(
columns.len(),
2,
"expected 2 columns, got {:?}",
columns.keys().collect::<Vec<_>>()
);
// No container should overlap with any other
for (i, a) in layouts.iter().enumerate() {
for (j, b) in layouts.iter().enumerate() {
if i >= j {
continue;
}
let h_overlap = a.left < b.left + b.right && b.left < a.left + a.right;
let v_overlap = a.top < b.top + b.bottom && b.top < a.top + a.bottom;
assert!(
!(h_overlap && v_overlap),
"containers {i} and {j} overlap: {a:?} vs {b:?}"
);
}
}
// Columns should tile the full width with no gaps
let col_entries: Vec<_> = columns.iter().collect();
let first_left = *col_entries[0].0;
let last = col_entries.last().unwrap();
let last_right_edge = last.0 + last.1[0].right;
assert_eq!(
first_left, area.left,
"first column should start at area.left"
);
assert_eq!(
last_right_edge,
area.left + area.right,
"last column should reach area right edge"
);
}
#[test]
fn test_grid_flip_all_axes_with_ratios_no_overlap() {
let area = test_area();
let opts = layout_options_with_column_ratios(&[0.3]);
for flip in [
Axis::Horizontal,
Axis::Vertical,
Axis::HorizontalAndVertical,
] {
let layouts = DefaultLayout::Grid.calculate(
&area,
NonZeroUsize::new(4).unwrap(),
None,
Some(flip),
&[],
0,
Some(opts),
&[],
);
for (i, a) in layouts.iter().enumerate() {
for (j, b) in layouts.iter().enumerate() {
if i >= j {
continue;
}
let h_overlap = a.left < b.left + b.right && b.left < a.left + a.right;
let v_overlap = a.top < b.top + b.bottom && b.top < a.top + a.bottom;
assert!(
!(h_overlap && v_overlap),
"{flip:?}: containers {i} and {j} overlap: {a:?} vs {b:?}"
);
}
}
// All containers should cover the full area
let total_area: i64 = layouts
.iter()
.map(|r| r.right as i64 * r.bottom as i64)
.sum();
assert_eq!(
total_area,
area.right as i64 * area.bottom as i64,
"{flip:?}: total container area doesn't match grid area"
);
}
}
#[test]
fn test_grid_uneven_rows_cover_full_height() {
// 7 windows => ceil(sqrt(7)) = 3 columns
// Distribution: col0=2 rows, col1=2 rows, col2=3 rows
// With area.bottom=800:
// 2-row columns: 800/2=400 each, total=800 (ok)
// 3-row column: 800/3=266 each, total=798 (2px gap!)
let area = Rect {
left: 0,
top: 0,
right: 1200,
bottom: 800,
};
let layouts = DefaultLayout::Grid.calculate(
&area,
NonZeroUsize::new(7).unwrap(),
None,
None,
&[],
0,
None,
&[],
);
assert_eq!(layouts.len(), 7);
// Group windows by column (by their left position)
let mut columns: std::collections::BTreeMap<i32, Vec<&Rect>> =
std::collections::BTreeMap::new();
for layout in &layouts {
columns.entry(layout.left).or_default().push(layout);
}
// Every column's windows should cover the full area height
for (&col_left, windows) in &columns {
// Sort by top position
let mut sorted: Vec<&&Rect> = windows.iter().collect();
sorted.sort_by_key(|w| w.top);
// First window should start at area.top
assert_eq!(
sorted[0].top, area.top,
"column at left={col_left}: first window should start at area.top"
);
// Last window's bottom edge should reach area.bottom
let last = sorted.last().unwrap();
let bottom_edge = last.top + last.bottom;
assert_eq!(
bottom_edge,
area.bottom,
"column at left={col_left} ({} rows): bottom edge is {bottom_edge}, \
expected {}. Gap of {} pixels",
windows.len(),
area.bottom,
area.bottom - bottom_edge,
);
}
}
#[test]
fn test_grid_uneven_rows_cover_full_height_with_vertical_flip() {
let area = Rect {
left: 0,
top: 0,
right: 1200,
bottom: 800,
};
for flip in [Axis::Vertical, Axis::HorizontalAndVertical] {
let layouts = DefaultLayout::Grid.calculate(
&area,
NonZeroUsize::new(7).unwrap(),
None,
Some(flip),
&[],
0,
None,
&[],
);
let mut columns: std::collections::BTreeMap<i32, Vec<&Rect>> =
std::collections::BTreeMap::new();
for layout in &layouts {
columns.entry(layout.left).or_default().push(layout);
}
for (&col_left, windows) in &columns {
let mut sorted: Vec<&&Rect> = windows.iter().collect();
sorted.sort_by_key(|w| w.top);
assert_eq!(
sorted[0].top, area.top,
"{flip:?}: column at left={col_left}: first window should start at area.top"
);
let last = sorted.last().unwrap();
let bottom_edge = last.top + last.bottom;
assert_eq!(
bottom_edge,
area.bottom,
"{flip:?}: column at left={col_left} ({} rows): bottom edge is {bottom_edge}, \
expected {}. Gap of {} pixels",
windows.len(),
area.bottom,
area.bottom - bottom_edge,
);
// Adjacent windows within the column should have no gaps
for pair in sorted.windows(2) {
let edge = pair[0].top + pair[0].bottom;
assert_eq!(
edge, pair[1].top,
"{flip:?}: column at left={col_left}: gap between rows at y={edge} and y={}",
pair[1].top,
);
}
}
}
}
#[test]
fn test_grid_uneven_columns_cover_full_width() {
// 5 windows => ceil(sqrt(5)) = 3 columns
// With area.right=1000: 1000/3=333 each, total=999 (1px gap!)
let area = Rect {
left: 0,
top: 0,
right: 1000,
bottom: 800,
};
let layouts = DefaultLayout::Grid.calculate(
&area,
NonZeroUsize::new(5).unwrap(),
None,
None,
&[],
0,
None,
&[],
);
assert_eq!(layouts.len(), 5);
// Group windows by column (by their left position)
let mut columns: std::collections::BTreeMap<i32, Vec<&Rect>> =
std::collections::BTreeMap::new();
for layout in &layouts {
columns.entry(layout.left).or_default().push(layout);
}
// First column should start at area.left
let first_left = *columns.keys().next().unwrap();
assert_eq!(
first_left, area.left,
"first column should start at area.left"
);
// Last column's right edge should reach area.right
let (&last_left, last_windows) = columns.iter().last().unwrap();
let last_right_edge = last_left + last_windows[0].right;
assert_eq!(
last_right_edge,
area.left + area.right,
"last column right edge is {last_right_edge}, expected {}. Gap of {} pixels",
area.left + area.right,
area.left + area.right - last_right_edge,
);
// Adjacent columns should have no gaps
let col_entries: Vec<_> = columns.iter().collect();
for pair in col_entries.windows(2) {
let (&left_a, windows_a) = pair[0];
let (&left_b, _) = pair[1];
let right_edge_a = left_a + windows_a[0].right;
assert_eq!(
right_edge_a, left_b,
"gap between columns at x={right_edge_a} and x={left_b}",
);
}
}
#[test]
fn test_grid_uneven_columns_cover_full_width_with_horizontal_flip() {
let area = Rect {
left: 0,
top: 0,
right: 1000,
bottom: 800,
};
for flip in [Axis::Horizontal, Axis::HorizontalAndVertical] {
let layouts = DefaultLayout::Grid.calculate(
&area,
NonZeroUsize::new(5).unwrap(),
None,
Some(flip),
&[],
0,
None,
&[],
);
let mut columns: std::collections::BTreeMap<i32, Vec<&Rect>> =
std::collections::BTreeMap::new();
for layout in &layouts {
columns.entry(layout.left).or_default().push(layout);
}
let first_left = *columns.keys().next().unwrap();
assert_eq!(
first_left, area.left,
"{flip:?}: first column should start at area.left"
);
let (&last_left, last_windows) = columns.iter().last().unwrap();
let last_right_edge = last_left + last_windows[0].right;
assert_eq!(
last_right_edge,
area.left + area.right,
"{flip:?}: last column right edge is {last_right_edge}, expected {}. Gap of {} pixels",
area.left + area.right,
area.left + area.right - last_right_edge,
);
// Adjacent columns should have no gaps
let col_entries: Vec<_> = columns.iter().collect();
for pair in col_entries.windows(2) {
let (&left_a, windows_a) = pair[0];
let (&left_b, _) = pair[1];
let right_edge_a = left_a + windows_a[0].right;
assert_eq!(
right_edge_a, left_b,
"{flip:?}: gap between columns at x={right_edge_a} and x={left_b}",
);
}
}
}
}
mod layout_flip_tests {
use super::*;
#[test]
fn test_columns_flip_horizontal() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let opts = layout_options_with_column_ratios(&[0.2, 0.3]);
let layouts = DefaultLayout::Columns.calculate(
&area,
len,
None,
Some(Axis::Horizontal),
&[],
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 3);
// Columns should be reversed
// Last column (originally 50%) should now be first
assert_eq!(layouts[2].left, 0);
}
#[test]
fn test_rows_flip_vertical() {
let area = test_area();
let len = NonZeroUsize::new(3).unwrap();
let opts = layout_options_with_row_ratios(&[0.25, 0.5]);
let layouts = DefaultLayout::Rows.calculate(
&area,
len,
None,
Some(Axis::Vertical),
&[],
0,
Some(opts),
&[],
);
assert_eq!(layouts.len(), 3);
// Rows should be reversed
// Last row should now be at top
assert_eq!(layouts[2].top, 0);
}
}
mod flip_remainder_coverage_tests {
use super::*;
/// Verify that layouts tile the full area with no gaps after flipping.
/// Checks that the leftmost edge == area.left, rightmost edge == area.left + area.right,
/// topmost edge == area.top, bottommost edge == area.top + area.bottom,
/// and no two windows overlap.
fn assert_full_coverage(layouts: &[Rect], area: &Rect, label: &str) {
assert!(!layouts.is_empty(), "{label}: no layouts produced");
let left_edge = layouts.iter().map(|r| r.left).min().unwrap();
let top_edge = layouts.iter().map(|r| r.top).min().unwrap();
let right_edge = layouts.iter().map(|r| r.left + r.right).max().unwrap();
let bottom_edge = layouts.iter().map(|r| r.top + r.bottom).max().unwrap();
assert_eq!(left_edge, area.left, "{label}: left edge gap");
assert_eq!(top_edge, area.top, "{label}: top edge gap");
assert_eq!(
right_edge,
area.left + area.right,
"{label}: right edge gap of {} pixels",
area.left + area.right - right_edge,
);
assert_eq!(
bottom_edge,
area.top + area.bottom,
"{label}: bottom edge gap of {} pixels",
area.top + area.bottom - bottom_edge,
);
// No overlaps
for (i, a) in layouts.iter().enumerate() {
for (j, b) in layouts.iter().enumerate() {
if i >= j {
continue;
}
let h = a.left < b.left + b.right && b.left < a.left + a.right;
let v = a.top < b.top + b.bottom && b.top < a.top + a.bottom;
assert!(
!(h && v),
"{label}: windows {i} and {j} overlap: {a:?} vs {b:?}"
);
}
}
}
// Area whose dimensions are not evenly divisible by 3
fn uneven_area() -> Rect {
Rect {
left: 0,
top: 0,
right: 1000, // 1000/3 = 333 rem 1
bottom: 800, // 800/3 = 266 rem 2
}
}
#[test]
fn test_columns_flipped_cover_full_area() {
let area = uneven_area();
let len = NonZeroUsize::new(3).unwrap();
for flip in [Axis::Horizontal, Axis::HorizontalAndVertical] {
let layouts =
DefaultLayout::Columns.calculate(&area, len, None, Some(flip), &[], 0, None, &[]);
assert_full_coverage(&layouts, &area, &format!("Columns {flip:?}"));
}
}
#[test]
fn test_rows_flipped_cover_full_area() {
let area = uneven_area();
let len = NonZeroUsize::new(3).unwrap();
for flip in [Axis::Vertical, Axis::HorizontalAndVertical] {
let layouts =
DefaultLayout::Rows.calculate(&area, len, None, Some(flip), &[], 0, None, &[]);
assert_full_coverage(&layouts, &area, &format!("Rows {flip:?}"));
}
}
#[test]
fn test_vertical_stack_flipped_cover_full_area() {
let area = uneven_area();
// 4 windows: 1 primary + 3 stack rows (triggers remainder in rows_with_ratios)
let len = NonZeroUsize::new(4).unwrap();
for flip in [
Axis::Horizontal,
Axis::Vertical,
Axis::HorizontalAndVertical,
] {
let layouts = DefaultLayout::VerticalStack.calculate(
&area,
len,
None,
Some(flip),
&[],
0,
None,
&[],
);
assert_full_coverage(&layouts, &area, &format!("VerticalStack {flip:?}"));
}
}
#[test]
fn test_horizontal_stack_flipped_cover_full_area() {
let area = uneven_area();
// 4 windows: 1 primary + 3 stack columns (triggers remainder in columns_with_ratios)
let len = NonZeroUsize::new(4).unwrap();
for flip in [
Axis::Horizontal,
Axis::Vertical,
Axis::HorizontalAndVertical,
] {
let layouts = DefaultLayout::HorizontalStack.calculate(
&area,
len,
None,
Some(flip),
&[],
0,
None,
&[],
);
assert_full_coverage(&layouts, &area, &format!("HorizontalStack {flip:?}"));
}
}
#[test]
fn test_right_main_vertical_stack_flipped_cover_full_area() {
let area = uneven_area();
let len = NonZeroUsize::new(4).unwrap();
for flip in [
Axis::Horizontal,
Axis::Vertical,
Axis::HorizontalAndVertical,
] {
let layouts = DefaultLayout::RightMainVerticalStack.calculate(
&area,
len,
None,
Some(flip),
&[],
0,
None,
&[],
);
assert_full_coverage(&layouts, &area, &format!("RightMainVerticalStack {flip:?}"));
}
}
#[test]
fn test_ultrawide_vertical_stack_flipped_cover_full_area() {
let area = uneven_area();
// 5 windows: primary + secondary + 3 tertiary rows (triggers remainder)
let len = NonZeroUsize::new(5).unwrap();
for flip in [
Axis::Horizontal,
Axis::Vertical,
Axis::HorizontalAndVertical,
] {
let layouts = DefaultLayout::UltrawideVerticalStack.calculate(
&area,
len,
None,
Some(flip),
&[],
0,
None,
&[],
);
assert_full_coverage(&layouts, &area, &format!("UltrawideVerticalStack {flip:?}"));
}
}
}
mod container_padding_tests {
use super::*;
#[test]
fn test_padding_applied_to_all_layouts() {
let area = test_area();
let len = NonZeroUsize::new(2).unwrap();
let padding = 10;
let layouts =
DefaultLayout::Columns.calculate(&area, len, Some(padding), None, &[], 0, None, &[]);
assert_eq!(layouts.len(), 2);
// Each layout should have padding applied
// left increases, right decreases, top increases, bottom decreases
assert_eq!(layouts[0].left, padding);
assert_eq!(layouts[0].top, padding);
assert_eq!(layouts[0].right, 500 - padding * 2);
assert_eq!(layouts[0].bottom, 800 - padding * 2);
}
}
mod flip_resize_adjacency_tests {
use super::*;
fn resize_3() -> Vec<Option<Rect>> {
vec![
Some(Rect {
left: 0,
top: 0,
right: 50,
bottom: 0,
}),
Some(Rect {
left: 0,
top: 0,
right: 0,
bottom: 40,
}),
None,
]
}
fn resize_4() -> Vec<Option<Rect>> {
vec![
Some(Rect {
left: 0,
top: 0,
right: 50,
bottom: 0,
}),
Some(Rect {
left: 0,
top: 0,
right: 0,
bottom: 40,
}),
None,
None,
]
}
#[test]
fn test_columns_flip_resize_no_gap() {
let area = test_area();
let opts = layout_options_with_column_ratios(&[0.3, 0.5]);
for flip in [Axis::Horizontal, Axis::HorizontalAndVertical] {
let layouts = DefaultLayout::Columns.calculate(
&area,
NonZeroUsize::new(3).unwrap(),
None,
Some(flip),
&resize_3(),
0,
Some(opts),
&[],
);
assert_containers_adjacent_horizontally(&layouts, &area);
}
}
#[test]
fn test_rows_flip_resize_no_gap() {
let area = test_area();
let opts = layout_options_with_row_ratios(&[0.3, 0.5]);
for flip in [Axis::Vertical, Axis::HorizontalAndVertical] {
let layouts = DefaultLayout::Rows.calculate(
&area,
NonZeroUsize::new(3).unwrap(),
None,
Some(flip),
&resize_3(),
0,
Some(opts),
&[],
);
assert_containers_adjacent_vertically(&layouts, &area);
}
}
#[test]
fn test_vertical_stack_flip_resize_no_gap() {
let area = test_area();
let opts = layout_options_with_ratios(&[0.7], &[0.4]);
for flip in [
Axis::Horizontal,
Axis::Vertical,
Axis::HorizontalAndVertical,
] {
let layouts = DefaultLayout::VerticalStack.calculate(
&area,
NonZeroUsize::new(4).unwrap(),
None,
Some(flip),
&resize_4(),
0,
Some(opts),
&[],
);
// Primary and stack share the horizontal axis
let primary = &layouts[0];
let stack = &layouts[1..];
// All stack elements should be in the same column
let stack_left = stack[0].left;
let stack_width = stack[0].right;
for s in stack {
assert_eq!(s.left, stack_left);
assert_eq!(s.right, stack_width);
}
// Primary and stack column should be adjacent and fill the area
if primary.left < stack_left {
assert_eq!(primary.left + primary.right, stack_left);
assert_eq!(stack_left + stack_width, area.left + area.right);
} else {
assert_eq!(stack_left + stack_width, primary.left);
assert_eq!(primary.left + primary.right, area.left + area.right);
}
// Stack elements should tile vertically
assert_containers_adjacent_vertically(
stack,
&Rect {
left: stack_left,
top: area.top,
right: stack_width,
bottom: area.bottom,
},
);
}
}
#[test]
fn test_right_main_vertical_stack_flip_resize_no_gap() {
let area = test_area();
let opts = layout_options_with_ratios(&[0.7], &[0.4]);
for flip in [
Axis::Horizontal,
Axis::Vertical,
Axis::HorizontalAndVertical,
] {
let layouts = DefaultLayout::RightMainVerticalStack.calculate(
&area,
NonZeroUsize::new(4).unwrap(),
None,
Some(flip),
&resize_4(),
0,
Some(opts),
&[],
);
let primary = &layouts[0];
let stack = &layouts[1..];
let stack_left = stack[0].left;
let stack_width = stack[0].right;
for s in stack {
assert_eq!(s.left, stack_left);
assert_eq!(s.right, stack_width);
}
if primary.left < stack_left {
assert_eq!(primary.left + primary.right, stack_left);
assert_eq!(stack_left + stack_width, area.left + area.right);
} else {
assert_eq!(stack_left + stack_width, primary.left);
assert_eq!(primary.left + primary.right, area.left + area.right);
}
assert_containers_adjacent_vertically(
stack,
&Rect {
left: stack_left,
top: area.top,
right: stack_width,
bottom: area.bottom,
},
);
}
}
#[test]
fn test_horizontal_stack_flip_resize_no_gap() {
let area = test_area();
let opts = layout_options_with_ratios(&[0.3, 0.5], &[0.7]);
for flip in [
Axis::Horizontal,
Axis::Vertical,
Axis::HorizontalAndVertical,
] {
let layouts = DefaultLayout::HorizontalStack.calculate(
&area,
NonZeroUsize::new(4).unwrap(),
None,
Some(flip),
&resize_4(),
0,
Some(opts),
&[],
);
let primary = &layouts[0];
let stack = &layouts[1..];
// All stack elements should be in the same row
let stack_top = stack[0].top;
let stack_height = stack[0].bottom;
for s in stack {
assert_eq!(s.top, stack_top);
assert_eq!(s.bottom, stack_height);
}
// Primary and stack row should be adjacent and fill the area
if primary.top < stack_top {
assert_eq!(primary.top + primary.bottom, stack_top);
assert_eq!(stack_top + stack_height, area.top + area.bottom);
} else {
assert_eq!(stack_top + stack_height, primary.top);
assert_eq!(primary.top + primary.bottom, area.top + area.bottom);
}
// Stack elements should tile horizontally
assert_containers_adjacent_horizontally(
stack,
&Rect {
left: area.left,
top: stack_top,
right: area.right,
bottom: stack_height,
},
);
}
}
#[test]
fn test_ultrawide_vertical_stack_flip_resize_no_gap() {
let area = test_area();
let opts = layout_options_with_ratios(&[0.5, 0.2], &[0.6]);
for flip in [
Axis::Horizontal,
Axis::Vertical,
Axis::HorizontalAndVertical,
] {
let layouts = DefaultLayout::UltrawideVerticalStack.calculate(
&area,
NonZeroUsize::new(4).unwrap(),
None,
Some(flip),
&resize_4(),
0,
Some(opts),
&[],
);
let primary = &layouts[0];
let secondary = &layouts[1];
let tertiary = &layouts[2..];
// All tertiary elements share the same column
let tert_left = tertiary[0].left;
let tert_width = tertiary[0].right;
for t in tertiary {
assert_eq!(t.left, tert_left);
assert_eq!(t.right, tert_width);
}
// The three columns (primary, secondary, tertiary) should tile horizontally
let columns = [
Rect {
left: primary.left,
top: 0,
right: primary.right,
bottom: 0,
},
Rect {
left: secondary.left,
top: 0,
right: secondary.right,
bottom: 0,
},
Rect {
left: tert_left,
top: 0,
right: tert_width,
bottom: 0,
},
];
assert_containers_adjacent_horizontally(&columns, &area);
// Tertiary elements should tile vertically
if tertiary.len() > 1 {
assert_containers_adjacent_vertically(
tertiary,
&Rect {
left: tert_left,
top: area.top,
right: tert_width,
bottom: area.bottom,
},
);
}
}
}
#[test]
fn test_scrolling_resize_no_gap() {
let area = test_area();
// Scrolling doesn't support flip, but verify resize adjacency
let layouts = DefaultLayout::Scrolling.calculate(
&area,
NonZeroUsize::new(3).unwrap(),
None,
None,
&resize_3(),
0,
None,
&[],
);
// Adjacent visible columns should not have gaps
for pair in layouts.windows(2) {
assert_eq!(
pair[0].left + pair[0].right,
pair[1].left,
"scrolling gap at left={} (width {}) and left={}",
pair[0].left,
pair[0].right,
pair[1].left,
);
}
}
}