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> = 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::>() ); // 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> = 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> = 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> = 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> = 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> { 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> { 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, ); } } }