use denise::{Rect, Size};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct Anchors {
pub left: bool,
pub top: bool,
pub right: bool,
pub bottom: bool,
}
impl Anchors {
pub const TOP_LEFT: Self = Self {
left: true,
top: true,
right: false,
bottom: false,
};
pub const STRETCH: Self = Self {
left: true,
top: true,
right: true,
bottom: true,
};
pub const CENTER: Self = Self {
left: false,
top: false,
right: false,
bottom: false,
};
pub const fn new(left: bool, top: bool, right: bool, bottom: bool) -> Self {
Self {
left,
top,
right,
bottom,
}
}
pub const fn is_fixed(self) -> bool {
self.left && self.top && !self.right && !self.bottom
}
}
impl Default for Anchors {
fn default() -> Self {
Self::TOP_LEFT
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Dock {
Top,
Bottom,
Left,
Right,
Fill,
}
const fn anchored_axis(
position: i32,
extent: i32,
growth: i32,
low: bool,
high: bool,
) -> (i32, i32) {
match (low, high) {
(true, true) => {
let stretched = extent + growth;
(position, if stretched < 0 { 0 } else { stretched })
}
(true, false) => (position, extent),
(false, true) => (position + growth, extent),
(false, false) => (position + growth / 2, extent),
}
}
pub fn anchored(layout: Rect, base: Size, current: Size, anchors: Anchors) -> Rect {
let dw = current.width as i32 - base.width as i32;
let dh = current.height as i32 - base.height as i32;
let (x, width) = anchored_axis(layout.x, layout.width, dw, anchors.left, anchors.right);
let (y, height) = anchored_axis(layout.y, layout.height, dh, anchors.top, anchors.bottom);
Rect::new(x, y, width, height)
}
pub fn docked(layout: Rect, remaining: Rect, dock: Dock) -> (Rect, Rect) {
let want_h = layout.height.clamp(0, remaining.height.max(0));
let want_w = layout.width.clamp(0, remaining.width.max(0));
match dock {
Dock::Top => (
Rect::new(remaining.x, remaining.y, remaining.width, want_h),
Rect::new(
remaining.x,
remaining.y + want_h,
remaining.width,
remaining.height - want_h,
),
),
Dock::Bottom => (
Rect::new(
remaining.x,
remaining.y + remaining.height - want_h,
remaining.width,
want_h,
),
Rect::new(
remaining.x,
remaining.y,
remaining.width,
remaining.height - want_h,
),
),
Dock::Left => (
Rect::new(remaining.x, remaining.y, want_w, remaining.height),
Rect::new(
remaining.x + want_w,
remaining.y,
remaining.width - want_w,
remaining.height,
),
),
Dock::Right => (
Rect::new(
remaining.x + remaining.width - want_w,
remaining.y,
want_w,
remaining.height,
),
Rect::new(
remaining.x,
remaining.y,
remaining.width - want_w,
remaining.height,
),
),
Dock::Fill => (
remaining,
Rect::new(remaining.x, remaining.y, 0, remaining.height),
),
}
}
#[cfg(test)]
mod tests {
use super::*;
const BASE: Size = Size::new(100, 100);
fn grown(layout: Rect, anchors: Anchors, to: Size) -> Rect {
anchored(layout, BASE, to, anchors)
}
#[test]
fn the_default_is_what_the_tree_did_before_anchoring_existed() {
let layout = Rect::new(10, 20, 30, 40);
for size in [Size::new(100, 100), Size::new(200, 300), Size::new(40, 40)] {
assert_eq!(
grown(layout, Anchors::TOP_LEFT, size),
layout,
"a top-left node must not move or resize, whatever the parent does"
);
}
assert!(Anchors::TOP_LEFT.is_fixed());
assert_eq!(Anchors::default(), Anchors::TOP_LEFT);
}
#[test]
fn a_right_anchored_node_follows_the_right_edge_and_keeps_its_size() {
let layout = Rect::new(60, 10, 30, 20);
let anchors = Anchors::new(false, true, true, false);
let grown = grown(layout, anchors, Size::new(140, 100));
assert_eq!(grown, Rect::new(100, 10, 30, 20));
assert_eq!(
140 - (grown.x + grown.width),
100 - (layout.x + layout.width)
);
}
#[test]
fn a_node_anchored_to_both_edges_stretches() {
let layout = Rect::new(10, 10, 80, 20);
let anchors = Anchors::new(true, true, true, false);
let grown = grown(layout, anchors, Size::new(150, 100));
assert_eq!(grown, Rect::new(10, 10, 130, 20));
assert_eq!(grown.x, layout.x);
assert_eq!(
150 - (grown.x + grown.width),
100 - (layout.x + layout.width)
);
}
#[test]
fn a_stretched_node_stops_at_nothing_rather_than_inverting() {
let layout = Rect::new(10, 10, 80, 20);
let squeezed = grown(layout, Anchors::STRETCH, Size::new(20, 20));
assert_eq!(squeezed.width, 0, "a negative width is not a rectangle");
assert_eq!(squeezed.height, 0);
}
#[test]
fn an_unanchored_axis_keeps_a_centred_node_centred() {
let layout = Rect::new(40, 40, 20, 20);
let grown = grown(layout, Anchors::CENTER, Size::new(200, 200));
assert_eq!(grown, Rect::new(90, 90, 20, 20));
assert_eq!(grown.x, (200 - grown.width) / 2);
}
#[test]
fn docking_takes_an_edge_and_leaves_the_rest() {
let all = Rect::new(0, 0, 200, 100);
let (top, rest) = docked(Rect::new(0, 0, 999, 20), all, Dock::Top);
assert_eq!(top, Rect::new(0, 0, 200, 20), "full width, its own height");
assert_eq!(rest, Rect::new(0, 20, 200, 80));
let (left, rest) = docked(Rect::new(0, 0, 50, 999), rest, Dock::Left);
assert_eq!(left, Rect::new(0, 20, 50, 80), "full remaining height");
assert_eq!(rest, Rect::new(50, 20, 150, 80));
let (bottom, rest) = docked(Rect::new(0, 0, 0, 30), rest, Dock::Bottom);
assert_eq!(bottom, Rect::new(50, 70, 150, 30));
assert_eq!(rest, Rect::new(50, 20, 150, 50));
let (right, rest) = docked(Rect::new(0, 0, 25, 0), rest, Dock::Right);
assert_eq!(right, Rect::new(175, 20, 25, 50));
assert_eq!(rest, Rect::new(50, 20, 125, 50));
let (fill, rest) = docked(Rect::new(9, 9, 9, 9), rest, Dock::Fill);
assert_eq!(
fill,
Rect::new(50, 20, 125, 50),
"fill ignores its own size"
);
assert_eq!(rest.width, 0, "nothing is left after a fill");
}
#[test]
fn two_bars_docked_to_the_same_edge_stack_in_order() {
let all = Rect::new(0, 0, 100, 100);
let (first, rest) = docked(Rect::new(0, 0, 0, 10), all, Dock::Top);
let (second, rest) = docked(Rect::new(0, 0, 0, 10), rest, Dock::Top);
assert_eq!(first, Rect::new(0, 0, 100, 10));
assert_eq!(second, Rect::new(0, 10, 100, 10));
assert_eq!(rest, Rect::new(0, 20, 100, 80));
}
#[test]
fn a_dock_cannot_take_more_room_than_is_left() {
let small = Rect::new(0, 0, 40, 30);
let (taken, rest) = docked(Rect::new(0, 0, 0, 500), small, Dock::Top);
assert_eq!(taken, small);
assert_eq!(rest.height, 0);
}
}