use crate::config::{Layout, LayoutRow};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Direction {
Left,
Right,
Up,
Down,
}
pub fn move_panel(
layout: &mut Layout,
row: usize,
column: usize,
direction: Direction,
) -> Option<(usize, usize)> {
if column >= layout.rows.get(row)?.panels.len() {
return None;
}
match direction {
Direction::Left | Direction::Right => {
let entry = layout.rows.get_mut(row)?;
let target = if direction == Direction::Left {
column.checked_sub(1)?
} else {
(column + 1 < entry.panels.len()).then_some(column + 1)?
};
entry.panels.swap(column, target);
Some((row, target))
}
Direction::Up | Direction::Down => {
vertical(layout, row, column, direction == Direction::Down)
}
}
}
pub fn move_row(layout: &mut Layout, row: usize, down: bool) -> Option<usize> {
if row >= layout.rows.len() {
return None;
}
let target = if down {
(row + 1 < layout.rows.len()).then_some(row + 1)?
} else {
row.checked_sub(1)?
};
layout.rows.swap(row, target);
Some(target)
}
fn vertical(layout: &mut Layout, row: usize, column: usize, down: bool) -> Option<(usize, usize)> {
let alone = layout.rows[row].panels.len() == 1;
let off_the_end = if down {
row + 1 >= layout.rows.len()
} else {
row == 0
};
if off_the_end {
return (!alone).then(|| promote(layout, row, column, down))?;
}
let target = if down { row + 1 } else { row - 1 };
let centre = centre_of(&layout.rows[row], column);
let at = insertion_point(&layout.rows[target], centre);
let panel = layout.rows[row].panels.remove(column);
layout.rows[target].panels.insert(at, panel);
Some((close_if_empty(layout, row, target), at))
}
fn promote(layout: &mut Layout, row: usize, column: usize, below: bool) -> Option<(usize, usize)> {
if layout.rows.get(row).is_some_and(|entry| entry.height == 1) {
for entry in &mut layout.rows {
entry.height = entry.height.saturating_mul(2);
}
}
let source = layout.rows.get_mut(row)?;
let panel = source.panels.remove(column);
let height = source.height;
let taken = height / 2;
source.height = height - taken;
let at = if below { row + 1 } else { row };
layout.rows.insert(
at,
LayoutRow {
height: taken,
panels: vec![panel],
},
);
Some((at, 0))
}
fn close_if_empty(layout: &mut Layout, emptied: usize, target: usize) -> usize {
if !layout.rows[emptied].panels.is_empty() {
return target;
}
let freed = layout.rows[emptied].height;
layout.rows[target].height = layout.rows[target].height.saturating_add(freed);
layout.rows.remove(emptied);
if emptied < target { target - 1 } else { target }
}
fn centre_of(row: &LayoutRow, column: usize) -> f64 {
let weight = |panel: &crate::config::LayoutPanel| f64::from(panel.width.max(1));
let total: f64 = row.panels.iter().map(weight).sum();
if total <= 0.0 {
return 0.0;
}
let before: f64 = row.panels.iter().take(column).map(weight).sum();
let own = row.panels.get(column).map_or(1.0, weight);
(before + own / 2.0) / total
}
fn insertion_point(row: &LayoutRow, centre: f64) -> usize {
let weight = |panel: &crate::config::LayoutPanel| f64::from(panel.width.max(1));
let total: f64 = row.panels.iter().map(weight).sum();
if total <= 0.0 {
return 0;
}
let mut best = 0;
let mut closest = f64::MAX;
let mut running = 0.0;
for index in 0..=row.panels.len() {
let gap = (running / total - centre).abs();
if gap < closest {
closest = gap;
best = index;
}
if let Some(panel) = row.panels.get(index) {
running += weight(panel);
}
}
best
}
#[cfg(test)]
mod tests {
use super::*;
use crate::config::LayoutPanel;
fn layout(rows: &[(u16, &[(&str, u16)])]) -> Layout {
Layout {
rows: rows
.iter()
.map(|(height, panels)| LayoutRow {
height: *height,
panels: panels
.iter()
.map(|(widget, width)| LayoutPanel {
widget: (*widget).into(),
width: *width,
})
.collect(),
})
.collect(),
}
}
fn shape(layout: &Layout) -> Vec<Vec<String>> {
layout
.rows
.iter()
.map(|row| row.panels.iter().map(|p| p.widget.clone()).collect())
.collect()
}
fn heights(layout: &Layout) -> Vec<u16> {
layout.rows.iter().map(|row| row.height).collect()
}
#[test]
fn a_panel_swaps_with_its_neighbour_and_keeps_its_width() {
let mut l = layout(&[(100, &[("clocks", 26), ("calendar", 34), ("weather", 40)])]);
assert_eq!(move_panel(&mut l, 0, 0, Direction::Right), Some((0, 1)));
assert_eq!(shape(&l)[0], ["calendar", "clocks", "weather"]);
let clocks = l.rows[0]
.panels
.iter()
.find(|p| p.widget == "clocks")
.unwrap();
assert_eq!(clocks.width, 26, "clocks kept the width it was given");
}
#[test]
fn the_ends_of_a_row_refuse_rather_than_wrapping() {
let mut l = layout(&[(100, &[("clocks", 50), ("weather", 50)])]);
assert_eq!(move_panel(&mut l, 0, 0, Direction::Left), None);
assert_eq!(move_panel(&mut l, 0, 1, Direction::Right), None);
assert_eq!(shape(&l)[0], ["clocks", "weather"], "nothing moved");
}
#[test]
fn a_panel_lands_under_where_it_already_was() {
let mut l = layout(&[
(50, &[("a", 10), ("b", 10), ("c", 80)]),
(50, &[("w", 25), ("x", 25), ("y", 25), ("z", 25)]),
]);
assert_eq!(move_panel(&mut l, 0, 2, Direction::Down), Some((1, 2)));
assert_eq!(shape(&l)[1], ["w", "x", "c", "y", "z"]);
let mut l = layout(&[
(50, &[("a", 10), ("b", 90)]),
(50, &[("w", 25), ("x", 25), ("y", 25), ("z", 25)]),
]);
assert_eq!(move_panel(&mut l, 0, 0, Direction::Down), Some((1, 0)));
assert_eq!(shape(&l)[1], ["a", "w", "x", "y", "z"]);
}
#[test]
fn the_last_panel_out_of_a_row_closes_it_and_hands_over_the_height() {
let mut l = layout(&[(30, &[("clocks", 100)]), (70, &[("todo", 100)])]);
assert_eq!(move_panel(&mut l, 0, 0, Direction::Down), Some((0, 0)));
assert_eq!(l.rows.len(), 1, "the emptied row closed");
assert_eq!(shape(&l)[0], ["clocks", "todo"]);
assert_eq!(heights(&l), [100], "and its height went to the survivor");
}
#[test]
fn pushing_past_the_edge_opens_a_row_without_inventing_weight() {
let mut l = layout(&[
(40, &[("clocks", 50), ("weather", 50)]),
(60, &[("todo", 100)]),
]);
assert_eq!(move_panel(&mut l, 0, 0, Direction::Up), Some((0, 0)));
assert_eq!(shape(&l), [vec!["clocks"], vec!["weather"], vec!["todo"]]);
assert_eq!(heights(&l), [20, 20, 60]);
assert_eq!(
heights(&l).iter().sum::<u16>(),
100,
"a new row is paid for out of the one it left, not conjured"
);
}
#[test]
fn a_panel_that_already_has_its_own_row_will_not_take_another() {
let mut l = layout(&[(50, &[("clocks", 100)]), (50, &[("todo", 100)])]);
assert_eq!(move_panel(&mut l, 0, 0, Direction::Up), None);
assert_eq!(l.rows.len(), 2, "no row was opened");
assert_eq!(heights(&l), [50, 50], "and no weight was moved");
}
#[test]
fn promoting_out_of_a_thin_row_does_not_rescale_the_rows_around_it() {
for height in [0u16, 1, 2, 3, 7, 100] {
let mut l = layout(&[
(height, &[("clocks", 50), ("weather", 50)]),
(50, &[("todo", 100)]),
]);
let before: u16 = heights(&l).iter().sum();
let untouched_share = f64::from(50) / f64::from(before.max(1));
assert_eq!(
move_panel(&mut l, 0, 0, Direction::Up),
Some((0, 0)),
"height {height}: the move itself must still work"
);
let after: u16 = heights(&l).iter().sum();
let todo = *heights(&l).last().expect("the untouched row");
let share = f64::from(todo) / f64::from(after.max(1));
assert!(
(share - untouched_share).abs() < 1e-9,
"height {height}: the untouched row went from {:.4} of the screen \
to {:.4} — heights {:?}",
untouched_share,
share,
heights(&l)
);
}
}
#[test]
fn no_sequence_of_moves_can_produce_a_layout_the_config_would_reject() {
let start = layout(&[
(1, &[("clocks", 26), ("calendar", 74)]),
(1, &[("todo", 100)]),
(2, &[("cpu", 50), ("network", 50)]),
]);
let mut l = start;
for step in 0..200usize {
let direction = match step % 4 {
0 => Direction::Down,
1 => Direction::Right,
2 => Direction::Up,
_ => Direction::Left,
};
let row = step % l.rows.len().max(1);
let column = step % l.rows[row].panels.len().max(1);
move_panel(&mut l, row, column, direction);
assert!(!l.rows.is_empty(), "step {step}: the layout emptied");
for entry in &l.rows {
assert!(
!entry.panels.is_empty(),
"step {step}: an empty row survived: {:?}",
shape(&l)
);
}
let mut placed: Vec<&str> = l.widgets();
placed.sort_unstable();
assert_eq!(
placed,
["calendar", "clocks", "cpu", "network", "todo"],
"step {step}: a panel was lost or duplicated"
);
}
}
#[test]
fn the_only_panel_on_the_dashboard_refuses_every_direction() {
for direction in [
Direction::Left,
Direction::Right,
Direction::Up,
Direction::Down,
] {
let mut l = layout(&[(100, &[("clocks", 100)])]);
assert_eq!(move_panel(&mut l, 0, 0, direction), None, "{direction:?}");
assert_eq!(shape(&l), [["clocks"]], "{direction:?} moved something");
}
}
#[test]
fn a_move_never_changes_what_the_weights_add_up_to() {
let mut l = layout(&[
(34, &[("clocks", 26), ("calendar", 34), ("weather", 40)]),
(42, &[("todo", 40), ("agenda", 32), ("notes", 28)]),
(24, &[("cpu", 50), ("network", 50)]),
]);
let before: u16 = heights(&l).iter().sum();
for direction in [
Direction::Down,
Direction::Down,
Direction::Up,
Direction::Right,
Direction::Up,
] {
move_panel(&mut l, 0, 0, direction);
assert_eq!(
heights(&l).iter().sum::<u16>(),
before,
"after {direction:?}: {:?}",
heights(&l)
);
}
}
#[test]
fn a_row_can_travel_past_its_neighbour() {
let mut l = layout(&[
(10, &[("clocks", 10)]),
(10, &[("watchlog", 10)]),
(10, &[("notes", 10)]),
(10, &[("cpu", 10)]),
]);
assert_eq!(move_row(&mut l, 1, true), Some(2));
assert_eq!(
shape(&l),
vec![vec!["clocks"], vec!["notes"], vec!["watchlog"], vec!["cpu"]],
"the watch log should have swapped with the notes row"
);
assert_eq!(l.rows.len(), 4, "moving a row must not change the count");
}
#[test]
fn a_row_takes_its_height_with_it() {
let mut l = layout(&[
(30, &[("clocks", 10)]),
(10, &[("watchlog", 10)]),
(60, &[("notes", 10)]),
]);
move_row(&mut l, 0, true);
assert_eq!(
heights(&l),
vec![10, 30, 60],
"each row's height should have travelled with it"
);
}
#[test]
fn a_row_at_the_edge_has_nowhere_to_go() {
let mut l = layout(&[(10, &[("clocks", 10)]), (10, &[("notes", 10)])]);
let before = shape(&l);
assert_eq!(move_row(&mut l, 0, false), None, "the top row cannot rise");
assert_eq!(move_row(&mut l, 1, true), None, "the last cannot sink");
assert_eq!(
move_row(&mut l, 9, true),
None,
"nor can a row that is not there"
);
assert_eq!(shape(&l), before, "a refused move must change nothing");
}
#[test]
fn a_shared_row_travels_whole() {
let mut l = layout(&[
(10, &[("clocks", 5), ("weather", 5)]),
(10, &[("notes", 10)]),
]);
assert_eq!(move_row(&mut l, 0, true), Some(1));
assert_eq!(
shape(&l),
vec![vec!["notes"], vec!["clocks", "weather"]],
"both panels should have moved together"
);
}
}