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.
This commit is contained in:
Csaba
2026-02-17 23:58:53 +01:00
committed by LGUG2Z
parent 5b6fab0044
commit 634a3e7f3b
2 changed files with 1301 additions and 679 deletions

View File

@@ -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;

File diff suppressed because it is too large Load Diff