From 634a3e7f3b9f4f7d8108fd5cb86889ad6bebac0f Mon Sep 17 00:00:00 2001 From: Csaba Date: Tue, 17 Feb 2026 23:58:53 +0100 Subject: [PATCH] fix(wm): correct bsp/grid container positioning when flipped The recursive_fibonacci function used formulas for main_x and main_y that mixed unresized area dimensions with resized dimensions when calculating flipped positions. This caused a gap between containers proportional to resize_delta * (2 * ratio - 1), meaning any column_ratios or row_ratios value other than the default 0.5 would produce visible gaps or overlaps between containers when the layout was both flipped and resized. The fix replaces the flipped position formulas with ones that derive main_x and main_y directly from the alt area width and height expressions used by the recursive child call, ensuring the main container is always positioned immediately adjacent to the alt area regardless of ratio or resize delta. The horizontal flip for grid layouts swapped column left positions directly from the unflipped layout. With non-default column ratios this caused narrow columns to receive wide column positions, producing overlapping containers. Precompute flipped column left positions by laying out the original column widths in reverse order from the area left edge, ensuring containers tile without overlap regardless of column ratio. Separated all unit test for arrangements into a new file. Added 4 BSP-specific regression tests (horizontal flip, vertical flip, both axes, sweep across multiple ratios/deltas). Added 7 adjacency tests covering ALL other layouts (Columns, Rows, VerticalStack, RightMainVerticalStack, HorizontalStack, UltrawideVerticalStack, Scrolling) confirming they don't have the gap issue. Added 2 Grid tests that verify containers don't overlap and tile the full area when column ratios are non-default. The first test checks a horizontal flip specifically, confirming 2 columns form with no gaps edge-to-edge. The second test runs all three flip axes and asserts no overlaps and full area coverage for each. --- komorebi-layouts/src/arrangement.rs | 706 +----------- komorebi-layouts/src/arrangement_tests.rs | 1274 +++++++++++++++++++++ 2 files changed, 1301 insertions(+), 679 deletions(-) create mode 100644 komorebi-layouts/src/arrangement_tests.rs diff --git a/komorebi-layouts/src/arrangement.rs b/komorebi-layouts/src/arrangement.rs index 5058fb40..95565cec 100644 --- a/komorebi-layouts/src/arrangement.rs +++ b/komorebi-layouts/src/arrangement.rs @@ -660,6 +660,24 @@ impl Arrangement for DefaultLayout { current_left += width; } + // Pre-calculate flipped column positions: same widths laid out + // in reverse order so that the last column sits at area.left + let flipped_col_lefts = if matches!( + layout_flip, + Some(Axis::Horizontal | Axis::HorizontalAndVertical) + ) { + let n = num_cols as usize; + let mut flipped = vec![0i32; n]; + let mut fl = area.left; + for i in (0..n).rev() { + flipped[i] = fl; + fl += col_widths[i]; + } + flipped + } else { + vec![] + }; + let mut iter = layouts.iter_mut().enumerate().peekable(); for col in 0..num_cols { @@ -687,20 +705,16 @@ impl Arrangement for DefaultLayout { match layout_flip { Some(Axis::Horizontal) => { - // Calculate flipped left position - let flipped_col = (num_cols - 1 - col) as usize; - left = col_lefts[flipped_col]; + left = flipped_col_lefts[col_idx]; } Some(Axis::Vertical) => { - // Calculate flipped top position top = area.bottom - win_height * (row + 1) + area.top; } Some(Axis::HorizontalAndVertical) => { - let flipped_col = (num_cols - 1 - col) as usize; - left = col_lefts[flipped_col]; + left = flipped_col_lefts[col_idx]; top = area.bottom - win_height * (row + 1) + area.top; } - None => {} // No flip + None => {} } win.bottom = win_height; @@ -1129,47 +1143,33 @@ fn recursive_fibonacci( *area }; - #[allow(clippy::cast_possible_truncation)] - let primary_width = (area.right as f32 * column_split_ratio) as i32; - #[allow(clippy::cast_possible_truncation)] - let primary_height = (area.bottom as f32 * row_split_ratio) as i32; #[allow(clippy::cast_possible_truncation)] let primary_resized_width = (resized.right as f32 * column_split_ratio) as i32; #[allow(clippy::cast_possible_truncation)] let primary_resized_height = (resized.bottom as f32 * row_split_ratio) as i32; - let secondary_width = area.right - primary_width; - let secondary_resized_width = resized.right - primary_resized_width; - let secondary_resized_height = resized.bottom - primary_resized_height; - let (main_x, alt_x, alt_y, main_y); if let Some(flip) = layout_flip { match flip { Axis::Horizontal => { - main_x = - resized.left + secondary_width + (secondary_width - secondary_resized_width); + main_x = resized.left + (area.right - primary_resized_width); alt_x = resized.left; alt_y = resized.top + primary_resized_height; main_y = resized.top; } Axis::Vertical => { - main_y = resized.top - + (area.bottom - primary_height) - + ((area.bottom - primary_height) - secondary_resized_height); + main_y = resized.top + (area.bottom - primary_resized_height); alt_y = resized.top; main_x = resized.left; alt_x = resized.left + primary_resized_width; } Axis::HorizontalAndVertical => { - main_x = - resized.left + secondary_width + (secondary_width - secondary_resized_width); + main_x = resized.left + (area.right - primary_resized_width); alt_x = resized.left; - main_y = resized.top - + (area.bottom - primary_height) - + ((area.bottom - primary_height) - secondary_resized_height); + main_y = resized.top + (area.bottom - primary_resized_height); alt_y = resized.top; } } @@ -1541,657 +1541,5 @@ fn resize_bottom(rect: &mut Rect, resize: i32) { } #[cfg(test)] -mod tests { - 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), - } - } - - 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 i in 1..5 { - assert_eq!(layouts[i].right, 200); - } - } - } - - 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); - } - } - - 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); - } - } - - 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); - } - } - - 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); - } - } - - 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 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); - } - } -} +#[path = "arrangement_tests.rs"] +mod tests; diff --git a/komorebi-layouts/src/arrangement_tests.rs b/komorebi-layouts/src/arrangement_tests.rs new file mode 100644 index 00000000..36c2b4d8 --- /dev/null +++ b/komorebi-layouts/src/arrangement_tests.rs @@ -0,0 +1,1274 @@ +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 i in 1..5 { + assert_eq!(layouts[i].right, 200); + } + } +} + +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); + } +} + +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); + } +} + +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" + ); + } + } +} + +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 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, + ); + } + } +}