use crate::common::Rect;
use crate::layout::types::{
ActualEntry, ActualLayout, Column, MonitorInfo, Padding, VirtualLayout,
};
#[must_use]
pub fn project(
virtual_layout: &VirtualLayout,
monitor: &MonitorInfo,
padding: &Padding,
) -> ActualLayout {
let monitor_rect = monitor.work_area;
let viewport_left = virtual_layout.viewport_offset;
let viewport_right = viewport_left + monitor_rect.width;
let slot_gap = padding.window_gap;
let mut entries = Vec::new();
let mut canvas_x: i32 = slot_gap;
for column in &virtual_layout.columns {
let col_width = column.width_px;
let canvas_col_left = canvas_x;
let canvas_col_right = canvas_x + col_width;
let visible = canvas_col_right > viewport_left && canvas_col_left < viewport_right;
let x = if visible {
monitor_rect.x + (canvas_col_left - viewport_left)
} else if canvas_col_right <= viewport_left {
monitor_rect.x - col_width
} else {
monitor_rect.x + monitor_rect.width
};
stack_column_rows(column, x, monitor_rect, col_width, padding, &mut entries);
canvas_x += col_width + slot_gap;
}
ActualLayout { entries }
}
fn stack_column_rows(
column: &Column,
x: i32,
monitor_rect: Rect,
col_width: i32,
padding: &Padding,
entries: &mut Vec<ActualEntry>,
) {
if column.rows.is_empty() {
return;
}
let mut y = monitor_rect.y + padding.up + padding.window_gap;
for row in &column.rows {
entries.push(ActualEntry {
window_id: row.window_id,
rect: Rect {
x,
y,
width: col_width,
height: row.height,
},
});
y += row.height + padding.window_gap;
}
}
#[must_use]
pub fn canvas_width(layout: &VirtualLayout, window_gap: i32) -> i32 {
if layout.columns.is_empty() {
return 0;
}
let total_slots: i32 = layout.columns.iter().map(|c| c.width_px + window_gap).sum();
window_gap + total_slots
}
#[must_use]
pub fn column_step_width(column: &Column, window_gap: i32) -> i32 {
column.width_px + window_gap
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::WindowId;
use crate::layout::types::Row;
fn test_monitor() -> MonitorInfo {
MonitorInfo {
work_area: Rect {
x: 0,
y: 0,
width: 1920,
height: 1080,
},
}
}
fn test_padding() -> Padding {
Padding {
window_gap: 4,
up: 0,
down: 0,
}
}
#[test]
fn project_single_column_fills_monitor() {
let layout = VirtualLayout::with_columns(
vec![Column::with_row(1920, Row::new(WindowId(1), 1072))],
0,
);
let actual = project(&layout, &test_monitor(), &test_padding());
assert_eq!(actual.entries.len(), 1);
let entry = &actual.entries[0];
assert_eq!(entry.window_id, WindowId(1));
assert_eq!(entry.rect.x, 4);
assert_eq!(entry.rect.y, 4);
assert_eq!(entry.rect.width, 1920);
assert_eq!(entry.rect.height, 1072);
}
#[test]
fn project_two_equal_columns() {
let layout = VirtualLayout::with_columns(
vec![
Column::with_row(960, Row::new(WindowId(1), 0)),
Column::with_row(960, Row::new(WindowId(2), 0)),
],
0,
);
let actual = project(&layout, &test_monitor(), &test_padding());
assert_eq!(actual.entries.len(), 2);
let col_width = 960; assert_eq!(actual.entries[0].rect.x, 4);
assert_eq!(actual.entries[0].rect.width, col_width);
assert_eq!(actual.entries[1].rect.x, col_width + 2 * 4);
assert_eq!(actual.entries[1].rect.width, col_width);
}
#[test]
fn project_column_with_two_rows() {
let layout = VirtualLayout::with_columns(
vec![Column::with_rows(
1920,
vec![Row::new(WindowId(1), 534), Row::new(WindowId(2), 534)],
)],
0,
);
let actual = project(&layout, &test_monitor(), &test_padding());
assert_eq!(actual.entries.len(), 2);
assert_eq!(actual.entries[0].rect.x, actual.entries[1].rect.x);
assert!(actual.entries[1].rect.y > actual.entries[0].rect.y);
}
#[test]
fn off_screen_left_parked_one_column_beyond() {
let monitor = test_monitor();
let padding = test_padding();
let layout = VirtualLayout::with_columns(
vec![
Column::with_row(960, Row::new(WindowId(1), 0)),
Column::with_row(960, Row::new(WindowId(2), 0)),
],
968, );
let actual = project(&layout, &monitor, &padding);
assert_eq!(actual.entries[0].rect.x, -960);
assert!(actual.entries[1].rect.x >= 0);
}
#[test]
fn off_screen_right_parked_one_column_beyond() {
let monitor = test_monitor();
let padding = test_padding();
let layout = VirtualLayout::with_columns(
vec![
Column::with_row(960, Row::new(WindowId(1), 0)),
Column::with_row(960, Row::new(WindowId(2), 0)),
Column::with_row(960, Row::new(WindowId(3), 0)), ],
0,
);
let actual = project(&layout, &monitor, &padding);
assert_eq!(actual.entries[2].rect.x, 1920);
}
#[test]
fn canvas_width_empty() {
let layout = VirtualLayout::new();
assert_eq!(canvas_width(&layout, 4), 0);
}
#[test]
fn canvas_width_two_columns() {
let layout = VirtualLayout::with_columns(
vec![
Column::with_row(960, Row::new(WindowId(1), 0)),
Column::with_row(960, Row::new(WindowId(2), 0)),
],
0,
);
assert_eq!(canvas_width(&layout, 4), 1932);
}
#[test]
fn project_three_varying_width_columns_exact_pixels() {
let layout = VirtualLayout::with_columns(
vec![
Column::with_row(240, Row::new(WindowId(1), 0)),
Column::with_row(720, Row::new(WindowId(2), 0)),
Column::with_row(960, Row::new(WindowId(3), 0)),
],
0,
);
let actual = project(&layout, &test_monitor(), &test_padding());
assert_eq!(actual.entries.len(), 3);
assert_eq!(actual.entries[0].rect.x, 4);
assert_eq!(actual.entries[0].rect.width, 240);
assert_eq!(actual.entries[1].rect.x, 248);
assert_eq!(actual.entries[1].rect.width, 720);
assert_eq!(actual.entries[2].rect.x, 972);
assert_eq!(actual.entries[2].rect.width, 960);
}
#[test]
fn project_visible_tiles_cover_full_monitor_width() {
let zero_padding = Padding {
window_gap: 0,
up: 0,
down: 0,
};
let layout = VirtualLayout::with_columns(
vec![
Column::with_row(960, Row::new(WindowId(1), 0)),
Column::with_row(960, Row::new(WindowId(2), 0)),
],
0,
);
let actual = project(&layout, &test_monitor(), &zero_padding);
let total_width: i32 = actual.entries.iter().map(|e| e.rect.width).sum();
assert_eq!(
total_width, 1920,
"visible tiles must cover full monitor width with zero gap"
);
}
#[test]
fn project_parked_tiles_left_and_right_simultaneously() {
let layout = VirtualLayout::with_columns(
vec![
Column::with_row(960, Row::new(WindowId(1), 0)),
Column::with_row(960, Row::new(WindowId(2), 0)),
Column::with_row(960, Row::new(WindowId(3), 0)),
Column::with_row(960, Row::new(WindowId(4), 0)),
Column::with_row(960, Row::new(WindowId(5), 0)),
],
1932, );
let actual = project(&layout, &test_monitor(), &test_padding());
assert_eq!(actual.entries[0].rect.x, -960);
assert_eq!(actual.entries[1].rect.x, -960);
assert!(actual.entries[2].rect.x >= 0, "col 2 should be visible");
assert!(actual.entries[3].rect.x >= 0, "col 3 should be visible");
assert_eq!(actual.entries[4].rect.x, 1920);
}
#[test]
fn project_parked_tiles_no_overlap_with_visible() {
let layout = VirtualLayout::with_columns(
vec![
Column::with_row(960, Row::new(WindowId(1), 0)),
Column::with_row(960, Row::new(WindowId(2), 0)),
Column::with_row(960, Row::new(WindowId(3), 0)),
],
968, );
let actual = project(&layout, &test_monitor(), &test_padding());
let parked_entry = &actual.entries[0];
let parked_right = parked_entry.rect.x + parked_entry.rect.width;
assert!(
parked_right <= 0,
"parked left tile overlaps visible: right edge = {parked_right}"
);
}
#[test]
fn project_nonzero_viewport_offset() {
let layout = VirtualLayout::with_columns(
vec![
Column::with_row(960, Row::new(WindowId(1), 0)),
Column::with_row(960, Row::new(WindowId(2), 0)),
Column::with_row(960, Row::new(WindowId(3), 0)),
],
500, );
let actual = project(&layout, &test_monitor(), &test_padding());
assert_eq!(actual.entries[0].rect.x, -496);
assert_eq!(actual.entries[1].rect.x, 468);
assert_eq!(actual.entries[2].rect.x, 1432);
}
#[test]
fn project_two_rows_equal_height() {
let layout = VirtualLayout::with_columns(
vec![Column::with_rows(
1920,
vec![Row::new(WindowId(1), 534), Row::new(WindowId(2), 534)],
)],
0,
);
let actual = project(&layout, &test_monitor(), &test_padding());
assert_eq!(actual.entries.len(), 2);
assert_eq!(actual.entries[0].rect.height, 534);
assert_eq!(actual.entries[1].rect.height, 534);
assert_eq!(actual.entries[0].rect.y, 4);
assert_eq!(actual.entries[1].rect.y, 542);
}
#[test]
fn project_single_column_narrow_width() {
let layout =
VirtualLayout::with_columns(vec![Column::with_row(240, Row::new(WindowId(1), 0))], 0);
let actual = project(&layout, &test_monitor(), &test_padding());
assert_eq!(actual.entries.len(), 1);
assert_eq!(actual.entries[0].rect.width, 240);
}
#[test]
fn project_empty_layout_yields_empty_actual() {
let layout = VirtualLayout::new();
let actual = project(&layout, &test_monitor(), &test_padding());
assert!(actual.entries.is_empty());
}
#[test]
fn canvas_width_single_column() {
let layout =
VirtualLayout::with_columns(vec![Column::with_row(1920, Row::new(WindowId(1), 0))], 0);
assert_eq!(canvas_width(&layout, 4), 1928);
}
#[test]
fn canvas_width_five_columns() {
let layout = VirtualLayout::with_columns(
vec![
Column::with_row(960, Row::new(WindowId(1), 0)),
Column::with_row(960, Row::new(WindowId(2), 0)),
Column::with_row(960, Row::new(WindowId(3), 0)),
Column::with_row(960, Row::new(WindowId(4), 0)),
Column::with_row(960, Row::new(WindowId(5), 0)),
],
0,
);
assert_eq!(canvas_width(&layout, 4), 4824);
}
#[test]
fn project_with_up_and_down_padding() {
let padding = Padding {
window_gap: 4,
up: 10,
down: 40,
};
let layout = VirtualLayout::with_columns(
vec![Column::with_row(1920, Row::new(WindowId(1), 1022))],
0,
);
let actual = project(&layout, &test_monitor(), &padding);
assert_eq!(actual.entries.len(), 1);
let entry = &actual.entries[0];
assert_eq!(entry.rect.y, 14);
assert_eq!(entry.rect.height, 1022);
}
#[test]
fn project_consumes_row_height_verbatim_uneven() {
let layout = VirtualLayout::with_columns(
vec![Column::with_rows(
960,
vec![Row::new(WindowId(1), 200), Row::new(WindowId(2), 800)],
)],
0,
);
let actual = project(&layout, &test_monitor(), &test_padding());
assert_eq!(actual.entries.len(), 2);
assert_eq!(actual.entries[0].rect.y, 4);
assert_eq!(actual.entries[0].rect.height, 200);
assert_eq!(actual.entries[1].rect.y, 208);
assert_eq!(actual.entries[1].rect.height, 800);
}
}