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,
}
}
}
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::*;
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
);
}
}