use denise::{Rect, Size};
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum Side {
Above,
Below,
Before,
After,
}
impl Side {
#[inline]
pub const fn opposite(self) -> Side {
match self {
Side::Above => Side::Below,
Side::Below => Side::Above,
Side::Before => Side::After,
Side::After => Side::Before,
}
}
}
const fn beside(anchor: Rect, size: Size, side: Side, gap: i32) -> Rect {
let (w, h) = (size.width as i32, size.height as i32);
match side {
Side::Above => Rect::new(anchor.x, anchor.y - gap - h, w, h),
Side::Below => Rect::new(anchor.x, anchor.bottom() + gap, w, h),
Side::Before => Rect::new(anchor.x - gap - w, anchor.y, w, h),
Side::After => Rect::new(anchor.right() + gap, anchor.y, w, h),
}
}
const fn fits(surface: Size, rect: Rect) -> bool {
rect.x >= 0
&& rect.y >= 0
&& rect.right() <= surface.width as i32
&& rect.bottom() <= surface.height as i32
}
pub fn anchored(surface: Size, anchor: Rect, size: Size, side: Side, gap: i32) -> Rect {
let preferred = beside(anchor, size, side, gap);
let flipped = beside(anchor, size, side.opposite(), gap);
let mut rect = if fits(surface, preferred) || !fits(surface, flipped) {
preferred
} else {
flipped
};
rect.x = rect.x.min(surface.width as i32 - rect.width).max(0);
rect.y = rect.y.min(surface.height as i32 - rect.height).max(0);
rect
}
#[cfg(test)]
mod tests {
use super::*;
const SURFACE: Size = Size::new(800, 480);
const ANCHOR: Rect = Rect::new(300, 200, 120, 40);
#[test]
fn the_preferred_side_is_used_when_it_fits() {
let below = anchored(SURFACE, ANCHOR, Size::new(160, 100), Side::Below, 4);
assert_eq!(below.y, ANCHOR.bottom() + 4);
assert_eq!(below.x, ANCHOR.x, "aligned to the anchor's leading edge");
let above = anchored(SURFACE, ANCHOR, Size::new(160, 100), Side::Above, 4);
assert_eq!(above.bottom(), ANCHOR.y - 4);
let after = anchored(SURFACE, ANCHOR, Size::new(160, 100), Side::After, 4);
assert_eq!(after.x, ANCHOR.right() + 4);
assert_eq!(after.y, ANCHOR.y);
let before = anchored(SURFACE, ANCHOR, Size::new(160, 100), Side::Before, 4);
assert_eq!(before.right(), ANCHOR.x - 4);
}
#[test]
fn a_popup_with_no_room_on_its_side_flips_to_the_other() {
let anchor = Rect::new(300, 420, 120, 40);
let rect = anchored(SURFACE, anchor, Size::new(160, 100), Side::Below, 4);
assert_eq!(rect.bottom(), anchor.y - 4, "flipped above");
let anchor = Rect::new(300, 10, 120, 40);
let rect = anchored(SURFACE, anchor, Size::new(160, 100), Side::Above, 4);
assert_eq!(rect.y, anchor.bottom() + 4, "flipped below");
let anchor = Rect::new(700, 200, 90, 40);
let rect = anchored(SURFACE, anchor, Size::new(160, 100), Side::After, 4);
assert_eq!(rect.right(), anchor.x - 4, "flipped before");
}
#[test]
fn flipping_never_changes_the_axis() {
let anchor = Rect::new(300, 420, 120, 40);
let rect = anchored(SURFACE, anchor, Size::new(160, 100), Side::Below, 4);
assert_eq!(rect.x, anchor.x, "still vertically attached");
assert!(rect.bottom() <= anchor.y, "above, not beside");
}
#[test]
fn a_popup_too_tall_for_either_side_is_clamped_inside() {
let rect = anchored(SURFACE, ANCHOR, Size::new(160, 460), Side::Below, 4);
assert!(rect.y >= 0, "top inside");
assert!(rect.bottom() <= 480, "bottom inside");
}
#[test]
fn the_cross_axis_is_slid_inside_the_surface() {
let anchor = Rect::new(700, 200, 90, 40);
let rect = anchored(SURFACE, anchor, Size::new(240, 100), Side::Below, 4);
assert_eq!(rect.right(), 800, "slid left to fit");
assert_eq!(rect.y, anchor.bottom() + 4, "the main axis is untouched");
}
#[test]
fn a_popup_bigger_than_the_surface_pins_to_the_origin() {
let rect = anchored(SURFACE, ANCHOR, Size::new(2000, 2000), Side::Below, 4);
assert_eq!((rect.x, rect.y), (0, 0));
}
#[test]
fn an_absurd_anchor_still_lands_inside_the_surface() {
for anchor in [
Rect::new(-500, -500, 50, 20),
Rect::new(5000, 5000, 50, 20),
Rect::new(0, 0, 0, 0),
] {
for side in [Side::Above, Side::Below, Side::Before, Side::After] {
let rect = anchored(SURFACE, anchor, Size::new(100, 60), side, 4);
assert!(fits(SURFACE, rect), "{anchor:?} {side:?} gave {rect:?}");
}
}
}
#[test]
fn opposites_are_symmetric() {
for side in [Side::Above, Side::Below, Side::Before, Side::After] {
assert_eq!(side.opposite().opposite(), side);
}
}
}