use crate::common::{Rect, WindowId};
use crate::layout::mutations::{MutationConfig, distribute_heights, remove_window};
use crate::layout::projection;
use crate::layout::types::{AppliedLayout, Column, MonitorInfo, Row, VirtualLayout};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum DropZone {
Row {
col: usize,
row: usize,
},
Column {
col: usize,
},
ScrollLeft,
ScrollRight,
}
#[must_use]
pub fn preview_move(
virtual_layout: &VirtualLayout,
dragged_id: WindowId,
zone: DropZone,
config: &MutationConfig,
monitor: &MonitorInfo,
) -> Option<AppliedLayout> {
if matches!(zone, DropZone::ScrollLeft | DropZone::ScrollRight) {
return None;
}
let (src_col, _src_row) = virtual_layout.find_window(dragged_id)?;
let src_had_single_row = virtual_layout.columns[src_col].rows.len() == 1;
let original_offset = virtual_layout.viewport_offset;
let mut layout = remove_window(virtual_layout, dragged_id, config);
match zone {
DropZone::Column { col } => {
let pos = adjust_index(col, src_col, src_had_single_row);
let pos = pos.min(layout.columns.len());
let new_col = make_single_row_column(dragged_id, config.column_width as i32, config);
layout.columns.insert(pos, new_col);
}
DropZone::Row { col, row } => {
let col_idx = adjust_index(col, src_col, src_had_single_row);
if col_idx >= layout.columns.len() {
return None;
}
insert_row_at(&mut layout, col_idx, row, dragged_id, config);
}
DropZone::ScrollLeft | DropZone::ScrollRight => return None,
}
layout.viewport_offset = original_offset;
if layout == *virtual_layout {
return None;
}
let actual = projection::project(&layout, monitor, &config.padding);
Some(AppliedLayout {
virtual_layout: layout,
actual_layout: actual,
})
}
fn make_single_row_column(window: WindowId, width_px: i32, config: &MutationConfig) -> Column {
let h = distribute_heights(1, config.available_height(), config.padding.window_gap)[0];
Column::with_row(width_px, Row::new(window, h))
}
fn insert_row_at(
layout: &mut VirtualLayout,
col_idx: usize,
row_idx: usize,
window: WindowId,
config: &MutationConfig,
) {
let col = &mut layout.columns[col_idx];
let n = col.rows.len() + 1;
let heights = distribute_heights(n, config.available_height(), config.padding.window_gap);
for (i, row) in col.rows.iter_mut().enumerate() {
row.height = heights[i];
}
let pos = row_idx.min(col.rows.len());
col.rows.insert(pos, Row::new(window, heights[n - 1]));
}
fn adjust_index(idx: usize, src_col: usize, src_col_removed: bool) -> usize {
if src_col_removed && src_col < idx {
idx - 1
} else {
idx
}
}
fn compute_col_edge_band(col_width: i32, ratio: f32, max_px: i32) -> i32 {
((col_width as f32 * ratio) as i32).min(max_px).max(1)
}
fn can_scroll_left(vl: &VirtualLayout) -> bool {
vl.viewport_offset > 0
}
fn can_scroll_right(vl: &VirtualLayout, wa_width: i32, vis_cols: &[(usize, Rect)]) -> bool {
if vis_cols.len() >= 2 {
let col0_w = vl.columns[vis_cols[0].0].width_px;
let gap = (vis_cols[1].1.x - (vis_cols[0].1.x + col0_w)).max(0);
let total_span = projection::canvas_width(vl, gap);
let content_right = total_span - gap;
content_right > vl.viewport_offset + wa_width
} else if let Some(&(idx, _)) = vis_cols.first() {
idx < vl.columns.len() - 1
} else {
false
}
}
fn build_visible_column_rects(applied: &AppliedLayout, wa: &Rect) -> Vec<(usize, Rect)> {
let vl = &applied.virtual_layout;
if vl.columns.is_empty() {
return Vec::new();
}
let mut boxes: Vec<Option<(i32, i32, i32, i32)>> = vec![None; vl.columns.len()];
for entry in &applied.actual_layout.entries {
if let Some((ci, _)) = vl.find_window(entry.window_id) {
let r = entry.rect;
match &mut boxes[ci] {
Some(b) => {
b.1 = b.1.min(r.y);
b.2 = b.2.max(r.y + r.height);
}
None => {
boxes[ci] = Some((r.x, r.y, r.y + r.height, r.width));
}
}
}
}
let mon_left = wa.x;
let mon_right = wa.x + wa.width;
let mut result = Vec::new();
for (ci, b) in boxes.into_iter().enumerate() {
if let Some((x, y_min, y_max, w)) = b {
if x + w <= mon_left || x >= mon_right {
continue;
}
result.push((
ci,
Rect {
x,
y: y_min,
width: w,
height: y_max - y_min,
},
));
}
}
result
}
#[allow(clippy::too_many_arguments)]
#[must_use]
pub fn resolve_drop_zone(
applied: &AppliedLayout,
monitor: &MonitorInfo,
dragged_id: WindowId,
mx: i32,
my: i32,
edge_band: i32,
col_edge_ratio: f32,
col_edge_max_px: i32,
) -> Option<DropZone> {
let _ = dragged_id; let wa = &monitor.work_area;
let vl = &applied.virtual_layout;
let vis_cols = build_visible_column_rects(applied, wa);
if vis_cols.is_empty() {
return None;
}
let wa_right = wa.x + wa.width;
if mx <= wa.x + edge_band && can_scroll_left(vl) {
return Some(DropZone::ScrollLeft);
}
if mx >= wa_right - edge_band && can_scroll_right(vl, wa.width, &vis_cols) {
return Some(DropZone::ScrollRight);
}
let first = &vis_cols[0];
let last = &vis_cols[vis_cols.len() - 1];
if mx < first.1.x {
return Some(DropZone::Column { col: first.0 });
}
let last_right = last.1.x + last.1.width;
if mx >= last_right {
return Some(DropZone::Column { col: last.0 + 1 });
}
for i in 0..vis_cols.len().saturating_sub(1) {
let seam_left = vis_cols[i].1.x + vis_cols[i].1.width;
let seam_right = vis_cols[i + 1].1.x;
if mx >= seam_left && mx < seam_right {
return Some(DropZone::Column {
col: vis_cols[i + 1].0,
});
}
}
for (col_idx, col_rect) in &vis_cols {
let col_right = col_rect.x + col_rect.width;
if mx >= col_rect.x && mx < col_right {
let band = compute_col_edge_band(col_rect.width, col_edge_ratio, col_edge_max_px);
if mx < col_rect.x + band {
return Some(DropZone::Column { col: *col_idx });
}
if mx > col_right - band {
return Some(DropZone::Column { col: col_idx + 1 });
}
let n = vl.columns[*col_idx].rows.len();
let rh = (col_rect.height / (n + 1) as i32).max(1);
let rel_y = (my - col_rect.y).max(0);
let j = (rel_y / rh) as usize;
let j = j.min(n);
return Some(DropZone::Row {
col: *col_idx,
row: j,
});
}
}
Some(DropZone::Column { col: last.0 + 1 })
}
#[cfg(test)]
mod tests {
use super::*;
use crate::common::Rect;
use crate::layout::ActualLayout;
use crate::layout::types::Padding;
fn test_config() -> MutationConfig {
MutationConfig {
monitor_width: 1920,
monitor_height: 1080,
min_window_height_px: 100,
min_row_height_px: 100,
column_width: 960,
min_column_width_px: 200,
max_n: 1,
abs_max_width: 1912,
padding: Padding {
window_gap: 4,
up: 0,
down: 0,
},
columns_per_screen: 2,
}
}
fn test_monitor() -> MonitorInfo {
MonitorInfo {
work_area: Rect {
x: 0,
y: 0,
width: 1920,
height: 1080,
},
}
}
fn two_col_layout() -> VirtualLayout {
let h1 = distribute_heights(1, 1080, 4)[0];
let h2 = distribute_heights(2, 1080, 4);
VirtualLayout::with_columns(
vec![
Column::with_row(960, Row::new(WindowId(1), h1)),
Column::with_rows(
960,
vec![Row::new(WindowId(2), h2[0]), Row::new(WindowId(3), h2[1])],
),
],
0,
)
}
fn three_col_layout() -> VirtualLayout {
let h1 = distribute_heights(1, 1080, 4)[0];
let h2 = distribute_heights(2, 1080, 4);
VirtualLayout::with_columns(
vec![
Column::with_row(960, Row::new(WindowId(1), h1)),
Column::with_row(960, Row::new(WindowId(2), h1)),
Column::with_rows(
960,
vec![Row::new(WindowId(3), h2[0]), Row::new(WindowId(4), h2[1])],
),
],
0,
)
}
#[test]
fn column_zone_inserts_new_column_at_index() {
let layout = two_col_layout();
let result = preview_move(
&layout,
WindowId(2),
DropZone::Column { col: 0 },
&test_config(),
&test_monitor(),
);
let applied = result.expect("should produce layout");
assert_eq!(applied.virtual_layout.columns.len(), 3);
assert_eq!(
applied.virtual_layout.columns[0].rows[0].window_id,
WindowId(2)
); assert_eq!(
applied.virtual_layout.columns[1].rows[0].window_id,
WindowId(1)
); }
#[test]
fn column_zone_at_index_zero() {
let layout = two_col_layout();
let result = preview_move(
&layout,
WindowId(2),
DropZone::Column { col: 0 },
&test_config(),
&test_monitor(),
);
let applied = result.expect("should produce layout");
assert_eq!(applied.virtual_layout.columns.len(), 3);
assert_eq!(
applied.virtual_layout.columns[0].rows[0].window_id,
WindowId(2)
);
}
#[test]
fn column_zone_appends_at_end() {
let layout = three_col_layout();
let result = preview_move(
&layout,
WindowId(2),
DropZone::Column { col: 99 },
&test_config(),
&test_monitor(),
);
let applied = result.expect("should produce layout");
assert_eq!(applied.virtual_layout.columns.len(), 3);
let last = applied.virtual_layout.columns.last().unwrap();
assert_eq!(last.rows[0].window_id, WindowId(2)); }
#[test]
fn column_zone_clamps_beyond_end() {
let layout = two_col_layout();
let result = preview_move(
&layout,
WindowId(1),
DropZone::Column { col: 99 },
&test_config(),
&test_monitor(),
);
let applied = result.expect("should produce layout");
assert_eq!(applied.virtual_layout.columns.len(), 2);
assert_eq!(
applied.virtual_layout.columns[1].rows[0].window_id,
WindowId(1)
);
}
#[test]
fn column_zone_source_removal_shift() {
let layout = three_col_layout();
let result = preview_move(
&layout,
WindowId(1),
DropZone::Column { col: 2 },
&test_config(),
&test_monitor(),
);
let applied = result.expect("should produce layout");
assert_eq!(applied.virtual_layout.columns.len(), 3);
assert_eq!(
applied.virtual_layout.columns[1].rows[0].window_id,
WindowId(1)
);
assert_eq!(
applied.virtual_layout.columns[0].rows[0].window_id,
WindowId(2)
);
}
#[test]
fn row_zone_inserts_at_top() {
let layout = two_col_layout();
let result = preview_move(
&layout,
WindowId(1),
DropZone::Row { col: 1, row: 0 },
&test_config(),
&test_monitor(),
);
let applied = result.expect("should produce layout");
assert_eq!(applied.virtual_layout.columns.len(), 1);
let col = &applied.virtual_layout.columns[0];
assert_eq!(col.rows.len(), 3);
assert_eq!(col.rows[0].window_id, WindowId(1)); assert_eq!(col.rows[1].window_id, WindowId(2));
assert_eq!(col.rows[2].window_id, WindowId(3));
}
#[test]
fn row_zone_inserts_at_bottom() {
let layout = two_col_layout();
let result = preview_move(
&layout,
WindowId(1),
DropZone::Row { col: 1, row: 99 },
&test_config(),
&test_monitor(),
);
let applied = result.expect("should produce layout");
let col = &applied.virtual_layout.columns[0];
assert_eq!(col.rows.last().unwrap().window_id, WindowId(1));
}
#[test]
fn row_zone_inserts_in_middle() {
let layout = two_col_layout();
let result = preview_move(
&layout,
WindowId(1),
DropZone::Row { col: 1, row: 1 },
&test_config(),
&test_monitor(),
);
let applied = result.expect("should produce layout");
let col = &applied.virtual_layout.columns[0];
assert_eq!(col.rows.len(), 3);
assert_eq!(col.rows[0].window_id, WindowId(2)); assert_eq!(col.rows[1].window_id, WindowId(1)); assert_eq!(col.rows[2].window_id, WindowId(3)); }
#[test]
fn row_zone_cross_column() {
let layout = two_col_layout();
let result = preview_move(
&layout,
WindowId(3),
DropZone::Row { col: 0, row: 0 },
&test_config(),
&test_monitor(),
);
let applied = result.expect("should produce layout");
assert_eq!(applied.virtual_layout.columns.len(), 2);
assert_eq!(
applied.virtual_layout.columns[0].rows[0].window_id,
WindowId(3)
); assert_eq!(
applied.virtual_layout.columns[1].rows[0].window_id,
WindowId(2)
); }
#[test]
fn row_zone_within_same_column() {
let layout = two_col_layout();
let result = preview_move(
&layout,
WindowId(3),
DropZone::Row { col: 1, row: 0 },
&test_config(),
&test_monitor(),
);
let applied = result.expect("should produce layout");
let col = &applied.virtual_layout.columns[1];
assert_eq!(col.rows[0].window_id, WindowId(3)); assert_eq!(col.rows[1].window_id, WindowId(2)); }
#[test]
fn scroll_left_returns_none() {
let layout = two_col_layout();
assert!(
preview_move(
&layout,
WindowId(1),
DropZone::ScrollLeft,
&test_config(),
&test_monitor()
)
.is_none()
);
}
#[test]
fn scroll_right_returns_none() {
let layout = two_col_layout();
assert!(
preview_move(
&layout,
WindowId(1),
DropZone::ScrollRight,
&test_config(),
&test_monitor()
)
.is_none()
);
}
#[test]
fn empty_layout_returns_none() {
let empty = VirtualLayout::new();
for zone in [
DropZone::Column { col: 0 },
DropZone::Row { col: 0, row: 0 },
] {
assert!(
preview_move(&empty, WindowId(1), zone, &test_config(), &test_monitor()).is_none(),
"empty layout should return None for zone {zone:?}"
);
}
}
#[test]
fn window_not_found_returns_none() {
let layout = two_col_layout();
assert!(
preview_move(
&layout,
WindowId(99),
DropZone::Column { col: 0 },
&test_config(),
&test_monitor()
)
.is_none()
);
}
#[test]
fn no_op_column_same_spot() {
let h1 = distribute_heights(1, 1080, 4)[0];
let layout = VirtualLayout::with_columns(
vec![
Column::with_row(960, Row::new(WindowId(1), h1)),
Column::with_row(960, Row::new(WindowId(2), h1)),
],
0,
);
assert!(
preview_move(
&layout,
WindowId(1),
DropZone::Column { col: 0 },
&test_config(),
&test_monitor()
)
.is_none(),
"dropping back in same spot should be no-op"
);
}
#[test]
fn row_zone_on_removed_source_column_returns_none() {
let h1 = distribute_heights(1, 1080, 4)[0];
let layout =
VirtualLayout::with_columns(vec![Column::with_row(960, Row::new(WindowId(1), h1))], 0);
assert!(
preview_move(
&layout,
WindowId(1),
DropZone::Row { col: 0, row: 0 },
&test_config(),
&test_monitor()
)
.is_none()
);
}
#[test]
fn row_zone_out_of_bounds_column_returns_none() {
let layout = two_col_layout();
assert!(
preview_move(
&layout,
WindowId(1),
DropZone::Row { col: 5, row: 0 },
&test_config(),
&test_monitor()
)
.is_none()
);
}
#[test]
fn preview_produces_actual_layout() {
let layout = two_col_layout();
let applied = preview_move(
&layout,
WindowId(2),
DropZone::Column { col: 0 },
&test_config(),
&test_monitor(),
)
.expect("should produce layout");
assert_eq!(applied.actual_layout.entries.len(), 3);
let expected_actual = projection::project(
&applied.virtual_layout,
&test_monitor(),
&test_config().padding,
);
assert_eq!(applied.actual_layout, expected_actual);
}
#[test]
fn preview_preserves_viewport_offset() {
let h1 = distribute_heights(1, 1080, 4)[0];
let h2 = distribute_heights(2, 1080, 4);
let layout = VirtualLayout::with_columns(
vec![
Column::with_row(960, Row::new(WindowId(1), h1)),
Column::with_rows(
960,
vec![Row::new(WindowId(2), h2[0]), Row::new(WindowId(3), h2[1])],
),
],
960, );
let applied = preview_move(
&layout,
WindowId(2),
DropZone::Column { col: 0 },
&test_config(),
&test_monitor(),
)
.expect("should produce layout");
assert_eq!(
applied.virtual_layout.viewport_offset, 960,
"viewport offset must be preserved across preview_move"
);
}
#[test]
fn preview_preserves_window_count() {
let layout = three_col_layout();
let applied = preview_move(
&layout,
WindowId(1),
DropZone::Column { col: 2 },
&test_config(),
&test_monitor(),
)
.expect("should produce layout");
assert_eq!(applied.virtual_layout.window_count(), 4);
let ids: std::collections::HashSet<WindowId> = applied
.virtual_layout
.columns
.iter()
.flat_map(|c| c.rows.iter().map(|r| r.window_id))
.collect();
assert_eq!(ids.len(), 4, "window ids must remain unique after move");
}
#[test]
fn row_insert_redistributes_heights_equally() {
let layout = two_col_layout();
let applied = preview_move(
&layout,
WindowId(1),
DropZone::Row { col: 1, row: 0 },
&test_config(),
&test_monitor(),
)
.expect("should produce layout");
assert_eq!(applied.virtual_layout.columns.len(), 1);
let col = &applied.virtual_layout.columns[0];
assert_eq!(col.rows.len(), 3);
let heights: Vec<i32> = col.rows.iter().map(|r| r.height).collect();
let spread = heights.iter().max().unwrap() - heights.iter().min().unwrap();
assert!(
spread <= 1,
"row heights must be equal up to 1px remainder: {heights:?}"
);
}
#[test]
fn no_op_row_same_spot() {
let layout = two_col_layout();
assert!(
preview_move(
&layout,
WindowId(3),
DropZone::Row { col: 1, row: 1 },
&test_config(),
&test_monitor(),
)
.is_none(),
"dropping a window back into its own row slot should be a no-op"
);
}
fn rdz_monitor() -> MonitorInfo {
MonitorInfo {
work_area: Rect {
x: 0,
y: 0,
width: 1000,
height: 1000,
},
}
}
fn rdz_padding(gap: i32) -> Padding {
Padding {
window_gap: gap,
up: 0,
down: 0,
}
}
fn make_rdz_applied(columns: Vec<Column>, viewport_offset: i32, gap: i32) -> AppliedLayout {
let vl = VirtualLayout::with_columns(columns, viewport_offset);
let actual = projection::project(&vl, &rdz_monitor(), &rdz_padding(gap));
AppliedLayout {
virtual_layout: vl,
actual_layout: actual,
}
}
#[test]
fn rdz_single_col_top_region() {
let layout = make_rdz_applied(
vec![Column::with_row(500, Row::new(WindowId(1), 1000))],
0,
0,
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
250,
100,
30,
0.18,
72
),
Some(DropZone::Row { col: 0, row: 0 }),
);
}
#[test]
fn rdz_single_col_bottom_region() {
let layout = make_rdz_applied(
vec![Column::with_row(500, Row::new(WindowId(1), 1000))],
0,
0,
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
250,
800,
30,
0.18,
72
),
Some(DropZone::Row { col: 0, row: 1 }),
);
}
#[test]
fn rdz_single_col_boundary_at_half() {
let layout = make_rdz_applied(
vec![Column::with_row(500, Row::new(WindowId(1), 1000))],
0,
0,
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
250,
499,
30,
0.18,
72
),
Some(DropZone::Row { col: 0, row: 0 }),
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
250,
500,
30,
0.18,
72
),
Some(DropZone::Row { col: 0, row: 1 }),
);
}
#[test]
fn rdz_two_row_col_three_regions() {
let layout = make_rdz_applied(
vec![Column::with_rows(
500,
vec![Row::new(WindowId(1), 500), Row::new(WindowId(2), 500)],
)],
0,
0,
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
250,
100,
30,
0.18,
72
),
Some(DropZone::Row { col: 0, row: 0 }),
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
250,
400,
30,
0.18,
72
),
Some(DropZone::Row { col: 0, row: 1 }),
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
250,
800,
30,
0.18,
72
),
Some(DropZone::Row { col: 0, row: 2 }),
);
}
#[test]
fn rdz_two_row_col_boundaries() {
let layout = make_rdz_applied(
vec![Column::with_rows(
500,
vec![Row::new(WindowId(1), 500), Row::new(WindowId(2), 500)],
)],
0,
0,
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
250,
332,
30,
0.18,
72
),
Some(DropZone::Row { col: 0, row: 0 }),
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
250,
333,
30,
0.18,
72
),
Some(DropZone::Row { col: 0, row: 1 }),
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
250,
666,
30,
0.18,
72
),
Some(DropZone::Row { col: 0, row: 2 }),
);
}
#[test]
fn rdz_col_edge_band_left() {
let layout = make_rdz_applied(
vec![Column::with_row(500, Row::new(WindowId(1), 1000))],
0,
0,
);
assert_eq!(
resolve_drop_zone(&layout, &rdz_monitor(), WindowId(99), 50, 500, 30, 0.18, 72),
Some(DropZone::Column { col: 0 }),
);
}
#[test]
fn rdz_col_edge_band_right() {
let layout = make_rdz_applied(
vec![Column::with_row(500, Row::new(WindowId(1), 1000))],
0,
0,
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
480,
500,
30,
0.18,
72
),
Some(DropZone::Column { col: 1 }),
);
}
#[test]
fn rdz_band_caps_at_max_px() {
let layout = make_rdz_applied(
vec![Column::with_row(1000, Row::new(WindowId(1), 1000))],
0,
0,
);
assert_eq!(
resolve_drop_zone(&layout, &rdz_monitor(), WindowId(99), 71, 500, 30, 0.18, 72),
Some(DropZone::Column { col: 0 }),
);
assert_eq!(
resolve_drop_zone(&layout, &rdz_monitor(), WindowId(99), 73, 500, 30, 0.18, 72),
Some(DropZone::Row { col: 0, row: 1 }),
);
}
#[test]
fn rdz_band_uses_ratio_for_narrow_col() {
let layout = make_rdz_applied(
vec![Column::with_row(200, Row::new(WindowId(1), 1000))],
0,
0,
);
assert_eq!(
resolve_drop_zone(&layout, &rdz_monitor(), WindowId(99), 35, 500, 30, 0.18, 72),
Some(DropZone::Column { col: 0 }),
);
assert_eq!(
resolve_drop_zone(&layout, &rdz_monitor(), WindowId(99), 37, 500, 30, 0.18, 72),
Some(DropZone::Row { col: 0, row: 1 }),
);
}
#[test]
fn rdz_seam_between_columns() {
let layout = make_rdz_applied(
vec![
Column::with_row(490, Row::new(WindowId(1), 1000)),
Column::with_row(490, Row::new(WindowId(2), 1000)),
],
0,
10,
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
505,
500,
30,
0.18,
72
),
Some(DropZone::Column { col: 1 }),
);
}
#[test]
fn rdz_left_of_first_column() {
let layout = make_rdz_applied(
vec![
Column::with_row(490, Row::new(WindowId(1), 1000)),
Column::with_row(490, Row::new(WindowId(2), 1000)),
],
0,
10,
);
assert_eq!(
resolve_drop_zone(&layout, &rdz_monitor(), WindowId(99), 5, 500, 30, 0.18, 72),
Some(DropZone::Column { col: 0 }),
);
}
#[test]
fn rdz_right_of_last_column() {
let layout = make_rdz_applied(
vec![
Column::with_row(490, Row::new(WindowId(1), 1000)),
Column::with_row(490, Row::new(WindowId(2), 1000)),
],
0,
10,
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
1001,
500,
30,
0.18,
72
),
Some(DropZone::Column { col: 2 }),
);
}
#[test]
fn rdz_own_column_allowed() {
let layout = make_rdz_applied(
vec![
Column::with_row(500, Row::new(WindowId(1), 1000)),
Column::with_row(500, Row::new(WindowId(2), 1000)),
],
0,
0,
);
let zone = resolve_drop_zone(&layout, &rdz_monitor(), WindowId(1), 250, 500, 30, 0.18, 72);
assert!(zone.is_some(), "own column should not be excluded");
match zone {
Some(DropZone::Row { col: 0, .. }) | Some(DropZone::Column { col: 0 }) => {}
other => panic!("expected zone in col 0, got {other:?}"),
}
}
#[test]
fn rdz_edge_scroll_left_with_offscreen() {
let layout = make_rdz_applied(
vec![
Column::with_row(500, Row::new(WindowId(1), 1000)),
Column::with_row(500, Row::new(WindowId(2), 1000)),
Column::with_row(500, Row::new(WindowId(3), 1000)),
],
500,
0,
);
assert_eq!(
resolve_drop_zone(&layout, &rdz_monitor(), WindowId(99), 10, 500, 30, 0.18, 72),
Some(DropZone::ScrollLeft),
);
}
#[test]
fn rdz_edge_scroll_left_no_offscreen() {
let layout = make_rdz_applied(
vec![
Column::with_row(500, Row::new(WindowId(1), 1000)),
Column::with_row(500, Row::new(WindowId(2), 1000)),
],
0,
0,
);
let zone = resolve_drop_zone(&layout, &rdz_monitor(), WindowId(99), 10, 500, 30, 0.18, 72);
assert_ne!(zone, Some(DropZone::ScrollLeft));
}
#[test]
fn rdz_edge_scroll_right_with_offscreen() {
let layout = make_rdz_applied(
vec![
Column::with_row(500, Row::new(WindowId(1), 1000)),
Column::with_row(500, Row::new(WindowId(2), 1000)),
Column::with_row(500, Row::new(WindowId(3), 1000)),
],
0,
0,
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
990,
500,
30,
0.18,
72
),
Some(DropZone::ScrollRight),
);
}
#[test]
fn rdz_edge_scroll_right_no_offscreen() {
let layout = make_rdz_applied(
vec![
Column::with_row(500, Row::new(WindowId(1), 1000)),
Column::with_row(500, Row::new(WindowId(2), 1000)),
],
0,
0,
);
let zone = resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
990,
500,
30,
0.18,
72,
);
assert_ne!(zone, Some(DropZone::ScrollRight));
}
#[test]
fn rdz_totality_all_points_some() {
let layout = make_rdz_applied(
vec![
Column::with_row(500, Row::new(WindowId(1), 1000)),
Column::with_row(500, Row::new(WindowId(2), 1000)),
],
0,
0,
);
for &mx in &[0, 200, 499, 500, 750, 999] {
for &my in &[0, 333, 666, 999] {
let zone =
resolve_drop_zone(&layout, &rdz_monitor(), WindowId(99), mx, my, 30, 0.18, 72);
assert!(zone.is_some(), "no zone at ({mx}, {my})");
}
}
}
#[test]
fn rdz_empty_layout_returns_none() {
let empty = AppliedLayout {
virtual_layout: VirtualLayout::new(),
actual_layout: ActualLayout::new(),
};
assert_eq!(
resolve_drop_zone(&empty, &rdz_monitor(), WindowId(99), 500, 500, 30, 0.18, 72),
None,
);
}
#[test]
fn rdz_scrolled_viewport_uses_projected_coords() {
let layout = make_rdz_applied(
vec![
Column::with_row(500, Row::new(WindowId(1), 1000)),
Column::with_row(500, Row::new(WindowId(2), 1000)),
Column::with_row(500, Row::new(WindowId(3), 1000)),
],
500,
0,
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
250,
250,
30,
0.18,
72
),
Some(DropZone::Row { col: 1, row: 0 }),
);
}
#[test]
fn rdz_scroll_right_single_visible_column() {
let layout = make_rdz_applied(
vec![
Column::with_row(1000, Row::new(WindowId(1), 1000)),
Column::with_row(1000, Row::new(WindowId(2), 1000)),
Column::with_row(1000, Row::new(WindowId(3), 1000)),
],
0,
0,
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
990,
500,
30,
0.18,
72
),
Some(DropZone::ScrollRight),
);
}
#[test]
fn rdz_scroll_right_single_column_is_last_no_scroll() {
let layout = make_rdz_applied(
vec![Column::with_row(1000, Row::new(WindowId(1), 1000))],
0,
0,
);
let zone = resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
990,
500,
30,
0.18,
72,
);
assert_ne!(zone, Some(DropZone::ScrollRight));
assert_eq!(zone, Some(DropZone::Column { col: 1 }));
}
#[test]
fn rdz_scroll_right_trailing_gap_content_fits() {
let layout = make_rdz_applied(
vec![
Column::with_row(490, Row::new(WindowId(1), 1000)),
Column::with_row(490, Row::new(WindowId(2), 1000)),
],
0,
10,
);
let zone = resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
985,
500,
30,
0.18,
72,
);
assert_ne!(
zone,
Some(DropZone::ScrollRight),
"trailing gap must not count as scrollable content"
);
assert_eq!(zone, Some(DropZone::Column { col: 2 }));
}
#[test]
fn rdz_scroll_right_with_gap_still_scrolls_offscreen() {
let layout = make_rdz_applied(
vec![
Column::with_row(490, Row::new(WindowId(1), 1000)),
Column::with_row(490, Row::new(WindowId(2), 1000)),
Column::with_row(490, Row::new(WindowId(3), 1000)),
],
0,
10,
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
990,
500,
30,
0.18,
72
),
Some(DropZone::ScrollRight),
);
}
#[test]
fn rdz_two_row_col_clamps_at_very_bottom() {
let layout = make_rdz_applied(
vec![Column::with_rows(
500,
vec![Row::new(WindowId(1), 500), Row::new(WindowId(2), 500)],
)],
0,
0,
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
250,
999,
30,
0.18,
72
),
Some(DropZone::Row { col: 0, row: 2 }),
);
}
#[test]
fn rdz_cursor_above_column_clamps_to_row_zero() {
let layout = make_rdz_applied(
vec![Column::with_row(500, Row::new(WindowId(1), 1000))],
0,
10,
);
assert_eq!(
resolve_drop_zone(&layout, &rdz_monitor(), WindowId(99), 100, 5, 30, 0.18, 72),
Some(DropZone::Row { col: 0, row: 0 }),
);
}
#[test]
fn rdz_col_edge_band_floors_at_one_pixel() {
let layout = make_rdz_applied(vec![Column::with_row(3, Row::new(WindowId(1), 1000))], 0, 0);
assert_eq!(
resolve_drop_zone(&layout, &rdz_monitor(), WindowId(99), 0, 500, 30, 0.18, 72),
Some(DropZone::Column { col: 0 }),
);
assert_eq!(
resolve_drop_zone(&layout, &rdz_monitor(), WindowId(99), 1, 500, 30, 0.18, 72),
Some(DropZone::Row { col: 0, row: 1 }),
);
}
#[test]
fn rdz_seam_boundaries_map_to_right_neighbor() {
let layout = make_rdz_applied(
vec![
Column::with_row(490, Row::new(WindowId(1), 1000)),
Column::with_row(490, Row::new(WindowId(2), 1000)),
],
0,
10,
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
500,
500,
30,
0.18,
72
),
Some(DropZone::Column { col: 1 }),
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
509,
500,
30,
0.18,
72
),
Some(DropZone::Column { col: 1 }),
);
assert_eq!(
resolve_drop_zone(
&layout,
&rdz_monitor(),
WindowId(99),
750,
500,
30,
0.18,
72
),
Some(DropZone::Row { col: 1, row: 0 }),
);
}
}