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)
}
}
}
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)> {
let source = layout.rows.get_mut(row)?;
let panel = source.panels.remove(column);
let height = source.height.max(2);
let taken = height / 2;
source.height = height - taken;
let at = if below { row + 1 } else { row };
layout.rows.insert(
at,
LayoutRow {
height: taken.max(1),
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 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)
);
}
}
}