pub(crate) const SCREEN_MARGIN: f32 = 12.0;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct Placement {
x: f32,
y: f32,
}
impl Default for Placement {
fn default() -> Self {
Self::new(1.0, 1.0)
}
}
impl Placement {
pub fn new(x: f32, y: f32) -> Self {
let fraction = |v: f32| if v.is_nan() { 0.5 } else { v.clamp(0.0, 1.0) };
Self {
x: fraction(x),
y: fraction(y),
}
}
pub fn x(self) -> f32 {
self.x
}
pub fn y(self) -> f32 {
self.y
}
pub(crate) fn place(self, area: Area, width: i32, height: i32, margin: i32) -> (i32, i32) {
let (x, y) = self.place_exact(area, width as f32, height as f32, margin as f32);
(x.round() as i32, y.round() as i32)
}
pub(crate) fn place_exact(
self,
area: Area,
width: f32,
height: f32,
margin: f32,
) -> (f32, f32) {
(
along(area.left, area.right, width, margin, self.x),
along(area.top, area.bottom, height, margin, self.y),
)
}
pub(crate) fn at_exact(
area: Area,
width: f32,
height: f32,
margin: f32,
(x, y): (f32, f32),
) -> Self {
Self::new(
fraction_at(area.left, area.right, width, margin, x),
fraction_at(area.top, area.bottom, height, margin, y),
)
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub(crate) struct Area {
pub left: f32,
pub top: f32,
pub right: f32,
pub bottom: f32,
}
impl From<(i32, i32, i32, i32)> for Area {
fn from((left, top, right, bottom): (i32, i32, i32, i32)) -> Self {
Self {
left: left as f32,
top: top as f32,
right: right as f32,
bottom: bottom as f32,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) struct Rect {
pub x: i32,
pub y: i32,
pub width: i32,
pub height: i32,
}
impl Rect {
fn overlaps(&self, other: &Rect) -> bool {
self.x < other.x + other.width
&& other.x < self.x + self.width
&& self.y < other.y + other.height
&& other.y < self.y + self.height
}
}
pub(crate) fn stack(
rect: Rect,
others: &[Rect],
up: bool,
gap: i32,
top: i32,
bottom: i32,
) -> Rect {
let mut moved = rect;
for _ in 0..others.len() {
let Some(hit) = others.iter().find(|other| moved.overlaps(other)) else {
break;
};
moved.y = if up {
hit.y - gap - moved.height
} else {
hit.y + hit.height + gap
};
}
let fits = moved.y >= top && moved.y + moved.height <= bottom;
if fits && !others.iter().any(|other| moved.overlaps(other)) {
moved
} else {
rect
}
}
fn along(start: f32, end: f32, size: f32, margin: f32, fraction: f32) -> f32 {
let free = end - start - 2.0 * margin - size;
(start + margin + fraction * free).max(start)
}
fn fraction_at(start: f32, end: f32, size: f32, margin: f32, position: f32) -> f32 {
let free = end - start - 2.0 * margin - size;
if free <= 0.0 {
return 0.5;
}
(position - start - margin) / free
}
#[cfg(test)]
mod tests {
use super::*;
fn rect(y: i32) -> Rect {
Rect {
x: 600,
y,
width: 360,
height: 200,
}
}
#[test]
fn a_free_place_stays() {
assert_eq!(stack(rect(500), &[rect(100)], true, 8, 0, 800), rect(500));
}
#[test]
fn a_popup_goes_above_the_ones_that_show() {
let others = [rect(588), rect(380)];
assert_eq!(stack(rect(588), &others, true, 8, 0, 800), rect(172));
}
#[test]
fn a_popup_goes_below_from_the_top() {
assert_eq!(stack(rect(12), &[rect(12)], false, 8, 0, 800), rect(220));
}
#[test]
fn a_full_stack_keeps_the_first_place() {
let others = [rect(588), rect(380), rect(172)];
assert_eq!(stack(rect(588), &others, true, 8, 0, 800), rect(588));
}
const AREA: Area = Area {
left: 0.0,
top: 0.0,
right: 1000.0,
bottom: 800.0,
};
#[test]
fn places_corners_and_center() {
assert_eq!(Placement::new(0.0, 0.0).place(AREA, 100, 50, 10), (10, 10));
assert_eq!(
Placement::new(1.0, 1.0).place(AREA, 100, 50, 10),
(890, 740)
);
assert_eq!(
Placement::new(0.5, 0.5).place(AREA, 100, 50, 10),
(450, 375)
);
assert_eq!(
Placement::new(0.5, 1.0).place(AREA, 100, 50, 10),
(450, 740)
);
}
#[test]
fn places_between_the_edges() {
assert_eq!(
Placement::new(0.25, 0.75).place(AREA, 100, 50, 10),
(230, 558)
);
}
#[test]
fn window_taller_than_area_keeps_its_top_visible() {
assert_eq!(Placement::new(1.0, 1.0).place(AREA, 100, 900, 10), (890, 0));
assert_eq!(Placement::new(0.5, 0.5).place(AREA, 100, 900, 10), (450, 0));
assert_eq!(Placement::new(0.0, 0.0).place(AREA, 100, 900, 10), (10, 10));
}
#[test]
fn window_wider_than_area_keeps_its_left_visible() {
assert_eq!(Placement::new(1.0, 1.0).place(AREA, 1200, 50, 10), (0, 740));
}
#[test]
fn values_are_clamped() {
assert_eq!(Placement::new(-1.0, 2.0), Placement::new(0.0, 1.0));
assert_eq!(Placement::new(f32::NAN, 0.3).x(), 0.5);
}
#[test]
fn at_is_the_inverse_of_place() {
let p = Placement::new(0.25, 0.75);
let (x, y) = p.place(AREA, 100, 50, 10);
let back = Placement::at_exact(AREA, 100.0, 50.0, 10.0, (x as f32, y as f32));
assert!((back.x() - 0.25).abs() < 0.01 && (back.y() - 0.75).abs() < 0.01);
}
#[test]
fn at_clamps_outside_the_free_space() {
assert_eq!(
Placement::at_exact(AREA, 100.0, 50.0, 10.0, (-50.0, 2000.0)),
Placement::new(0.0, 1.0)
);
assert_eq!(
Placement::at_exact(AREA, 2000.0, 50.0, 10.0, (0.0, 0.0)).x(),
0.5
);
}
}