use crate::geometry::{Rect, Size};
const MARGIN: f64 = 4.0;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum Align {
Min,
Center,
Max,
}
impl Align {
pub fn frac(self) -> f64 {
match self {
Align::Min => 0.0,
Align::Center => 0.5,
Align::Max => 1.0,
}
}
pub fn sign(self) -> f64 {
match self {
Align::Min => -1.0,
Align::Center => 0.0,
Align::Max => 1.0,
}
}
pub fn flip(self) -> Self {
match self {
Align::Min => Align::Max,
Align::Center => Align::Center,
Align::Max => Align::Min,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Align2 {
pub x: Align,
pub y: Align,
}
impl Align2 {
pub const LEFT_TOP: Self = Self {
x: Align::Min,
y: Align::Max,
};
pub const CENTER_TOP: Self = Self {
x: Align::Center,
y: Align::Max,
};
pub const RIGHT_TOP: Self = Self {
x: Align::Max,
y: Align::Max,
};
pub const LEFT_CENTER: Self = Self {
x: Align::Min,
y: Align::Center,
};
pub const CENTER: Self = Self {
x: Align::Center,
y: Align::Center,
};
pub const RIGHT_CENTER: Self = Self {
x: Align::Max,
y: Align::Center,
};
pub const LEFT_BOTTOM: Self = Self {
x: Align::Min,
y: Align::Min,
};
pub const CENTER_BOTTOM: Self = Self {
x: Align::Center,
y: Align::Min,
};
pub const RIGHT_BOTTOM: Self = Self {
x: Align::Max,
y: Align::Min,
};
pub const ALL: [(Self, &'static str); 9] = [
(Self::LEFT_TOP, "LEFT_TOP"),
(Self::LEFT_CENTER, "LEFT_CENTER"),
(Self::LEFT_BOTTOM, "LEFT_BOTTOM"),
(Self::CENTER_TOP, "CENTER_TOP"),
(Self::CENTER, "CENTER_CENTER"),
(Self::CENTER_BOTTOM, "CENTER_BOTTOM"),
(Self::RIGHT_TOP, "RIGHT_TOP"),
(Self::RIGHT_CENTER, "RIGHT_CENTER"),
(Self::RIGHT_BOTTOM, "RIGHT_BOTTOM"),
];
pub fn all_index(self) -> usize {
Self::ALL
.iter()
.position(|(a, _)| *a == self)
.unwrap_or(0)
}
pub fn point_in(self, rect: Rect) -> (f64, f64) {
(
rect.x + rect.width * self.x.frac(),
rect.y + rect.height * self.y.frac(),
)
}
pub fn flip_x(self) -> Self {
Self {
x: self.x.flip(),
y: self.y,
}
}
pub fn flip_y(self) -> Self {
Self {
x: self.x,
y: self.y.flip(),
}
}
pub fn flip(self) -> Self {
Self {
x: self.x.flip(),
y: self.y.flip(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct RectAlign {
pub parent: Align2,
pub child: Align2,
}
impl RectAlign {
pub const BOTTOM_START: Self = Self {
parent: Align2::LEFT_BOTTOM,
child: Align2::LEFT_TOP,
};
pub const BOTTOM: Self = Self {
parent: Align2::CENTER_BOTTOM,
child: Align2::CENTER_TOP,
};
pub const BOTTOM_END: Self = Self {
parent: Align2::RIGHT_BOTTOM,
child: Align2::RIGHT_TOP,
};
pub const TOP_START: Self = Self {
parent: Align2::LEFT_TOP,
child: Align2::LEFT_BOTTOM,
};
pub const TOP: Self = Self {
parent: Align2::CENTER_TOP,
child: Align2::CENTER_BOTTOM,
};
pub const TOP_END: Self = Self {
parent: Align2::RIGHT_TOP,
child: Align2::RIGHT_BOTTOM,
};
pub const RIGHT_START: Self = Self {
parent: Align2::RIGHT_TOP,
child: Align2::LEFT_TOP,
};
pub const RIGHT: Self = Self {
parent: Align2::RIGHT_CENTER,
child: Align2::LEFT_CENTER,
};
pub const RIGHT_END: Self = Self {
parent: Align2::RIGHT_BOTTOM,
child: Align2::LEFT_BOTTOM,
};
pub const LEFT_START: Self = Self {
parent: Align2::LEFT_TOP,
child: Align2::RIGHT_TOP,
};
pub const LEFT: Self = Self {
parent: Align2::LEFT_CENTER,
child: Align2::RIGHT_CENTER,
};
pub const LEFT_END: Self = Self {
parent: Align2::LEFT_BOTTOM,
child: Align2::RIGHT_BOTTOM,
};
pub const PRESETS: [(Self, &'static str); 12] = [
(Self::TOP_START, "TOP_START"),
(Self::TOP, "TOP"),
(Self::TOP_END, "TOP_END"),
(Self::RIGHT_START, "RIGHT_START"),
(Self::RIGHT, "RIGHT"),
(Self::RIGHT_END, "RIGHT_END"),
(Self::BOTTOM_START, "BOTTOM_START"),
(Self::BOTTOM, "BOTTOM"),
(Self::BOTTOM_END, "BOTTOM_END"),
(Self::LEFT_START, "LEFT_START"),
(Self::LEFT, "LEFT"),
(Self::LEFT_END, "LEFT_END"),
];
pub const MENU_ALIGNS: [Self; 12] = [
Self::BOTTOM_START,
Self::BOTTOM_END,
Self::TOP_START,
Self::TOP_END,
Self::RIGHT_END,
Self::RIGHT_START,
Self::LEFT_END,
Self::LEFT_START,
Self::TOP,
Self::RIGHT,
Self::BOTTOM,
Self::LEFT,
];
pub fn preset_label(self) -> Option<&'static str> {
Self::PRESETS
.iter()
.find(|(a, _)| *a == self)
.map(|(_, label)| *label)
}
pub fn flip_x(self) -> Self {
Self {
parent: self.parent.flip_x(),
child: self.child.flip_x(),
}
}
pub fn flip_y(self) -> Self {
Self {
parent: self.parent.flip_y(),
child: self.child.flip_y(),
}
}
pub fn flip(self) -> Self {
Self {
parent: self.parent.flip(),
child: self.child.flip(),
}
}
pub fn symmetries(self) -> [Self; 3] {
[self.flip_x(), self.flip_y(), self.flip()]
}
pub fn find_best_align(
values_to_try: impl Iterator<Item = Self>,
content_rect: Rect,
parent_rect: Rect,
gap: f64,
expected_size: Size,
) -> Option<Self> {
let mut first_choice = None;
for align in values_to_try {
first_choice = first_choice.or(Some(align));
let suggested = align.place_child(parent_rect, expected_size, gap);
if contains_rect(content_rect, suggested) {
return Some(align);
}
}
first_choice
}
pub fn place_child(self, parent: Rect, size: Size, gap: f64) -> Rect {
let (px, py) = self.parent.point_in(parent);
let gx = if self.parent.x != self.child.x {
self.parent.x.sign()
} else {
0.0
};
let gy = if self.parent.y != self.child.y {
self.parent.y.sign()
} else {
0.0
};
let ax = px + gap * gx;
let ay = py + gap * gy;
let cx = ax - size.width * self.child.x.frac();
let cy = ay - size.height * self.child.y.frac();
Rect::new(cx, cy, size.width, size.height)
}
pub fn place_child_clamped(self, parent: Rect, size: Size, gap: f64, viewport: Size) -> Rect {
clamp_rect(self.place_child(parent, size, gap), viewport)
}
}
fn contains_rect(outer: Rect, inner: Rect) -> bool {
inner.x >= outer.x
&& inner.y >= outer.y
&& inner.x + inner.width <= outer.x + outer.width
&& inner.y + inner.height <= outer.y + outer.height
}
pub fn clamp_rect(rect: Rect, viewport: Size) -> Rect {
let max_x = (viewport.width - rect.width - MARGIN).max(MARGIN);
let max_y = (viewport.height - rect.height - MARGIN).max(MARGIN);
Rect::new(
rect.x.clamp(MARGIN, max_x),
rect.y.clamp(MARGIN, max_y),
rect.width,
rect.height,
)
}
#[cfg(test)]
mod tests {
use super::*;
const PARENT: Rect = Rect {
x: 100.0,
y: 100.0,
width: 40.0,
height: 20.0,
};
const SIZE: Size = Size {
width: 60.0,
height: 30.0,
};
const GAP: f64 = 4.0;
#[test]
fn bottom_start_hangs_below_left_aligned() {
let r = RectAlign::BOTTOM_START.place_child(PARENT, SIZE, GAP);
assert_eq!(r, Rect::new(100.0, 66.0, 60.0, 30.0));
assert_eq!(r.x, PARENT.x);
assert_eq!(r.y + r.height, PARENT.y - GAP);
}
#[test]
fn top_is_centered_above() {
let r = RectAlign::TOP.place_child(PARENT, SIZE, GAP);
assert_eq!(r.y, PARENT.y + PARENT.height + GAP);
let popup_center_x = r.x + r.width * 0.5;
let parent_center_x = PARENT.x + PARENT.width * 0.5;
assert!((popup_center_x - parent_center_x).abs() < 1e-9);
}
#[test]
fn right_is_centered_to_the_right() {
let r = RectAlign::RIGHT.place_child(PARENT, SIZE, GAP);
assert_eq!(r.x, PARENT.x + PARENT.width + GAP);
let popup_center_y = r.y + r.height * 0.5;
let parent_center_y = PARENT.y + PARENT.height * 0.5;
assert!((popup_center_y - parent_center_y).abs() < 1e-9);
}
#[test]
fn left_is_centered_to_the_left() {
let r = RectAlign::LEFT.place_child(PARENT, SIZE, GAP);
assert_eq!(r.x + r.width, PARENT.x - GAP);
let popup_center_y = r.y + r.height * 0.5;
let parent_center_y = PARENT.y + PARENT.height * 0.5;
assert!((popup_center_y - parent_center_y).abs() < 1e-9);
}
#[test]
fn zero_gap_puts_child_anchor_on_parent_anchor() {
let align = RectAlign {
parent: Align2::RIGHT_TOP,
child: Align2::LEFT_BOTTOM,
};
let r = align.place_child(PARENT, SIZE, 0.0);
let (px, py) = Align2::RIGHT_TOP.point_in(PARENT);
let (cx, cy) = Align2::LEFT_BOTTOM.point_in(r);
assert!((px - cx).abs() < 1e-9);
assert!((py - cy).abs() < 1e-9);
}
#[test]
fn clamp_keeps_popup_on_screen() {
let parent = Rect::new(2.0, 6.0, 40.0, 20.0);
let viewport = Size::new(400.0, 300.0);
let r = RectAlign::BOTTOM_START.place_child_clamped(parent, SIZE, GAP, viewport);
assert!(r.x >= 4.0);
assert!(r.y >= 4.0);
assert!(r.x + r.width <= viewport.width);
assert!(r.y + r.height <= viewport.height);
}
#[test]
fn presets_and_anchor_tables_round_trip() {
assert_eq!(RectAlign::BOTTOM.preset_label(), Some("BOTTOM"));
for (i, (a, _)) in Align2::ALL.iter().enumerate() {
assert_eq!(a.all_index(), i);
}
}
#[test]
fn flips_mirror_the_named_presets() {
assert_eq!(RectAlign::BOTTOM_START.flip_y(), RectAlign::TOP_START);
assert_eq!(RectAlign::TOP_END.flip_y(), RectAlign::BOTTOM_END);
assert_eq!(RectAlign::LEFT.flip_x(), RectAlign::RIGHT);
assert_eq!(RectAlign::RIGHT_START.flip_x(), RectAlign::LEFT_START);
assert_eq!(RectAlign::BOTTOM_START.flip(), RectAlign::TOP_END);
assert_eq!(
RectAlign::BOTTOM_START.symmetries(),
[
RectAlign::BOTTOM_END,
RectAlign::TOP_START,
RectAlign::TOP_END
]
);
}
fn candidates(align: RectAlign) -> impl Iterator<Item = RectAlign> {
std::iter::once(align)
.chain(align.symmetries())
.chain(RectAlign::MENU_ALIGNS)
}
#[test]
fn find_best_align_flips_at_each_screen_edge() {
let screen = Rect::new(0.0, 0.0, 400.0, 300.0);
let size = Size::new(100.0, 80.0);
let parent = Rect::new(150.0, 10.0, 40.0, 20.0);
let best = RectAlign::find_best_align(
candidates(RectAlign::BOTTOM_START),
screen,
parent,
GAP,
size,
);
assert_eq!(best, Some(RectAlign::TOP_START));
let parent = Rect::new(150.0, 270.0, 40.0, 20.0);
let best = RectAlign::find_best_align(
candidates(RectAlign::TOP_START),
screen,
parent,
GAP,
size,
);
assert_eq!(best, Some(RectAlign::BOTTOM_START));
let parent = Rect::new(340.0, 100.0, 40.0, 20.0);
let best = RectAlign::find_best_align(
candidates(RectAlign::RIGHT_START),
screen,
parent,
GAP,
size,
);
assert_eq!(best, Some(RectAlign::LEFT_START));
let parent = Rect::new(10.0, 100.0, 40.0, 20.0);
let best = RectAlign::find_best_align(
candidates(RectAlign::LEFT_START),
screen,
parent,
GAP,
size,
);
assert_eq!(best, Some(RectAlign::RIGHT_START));
}
#[test]
fn find_best_align_keeps_preferred_align_when_it_fits() {
let screen = Rect::new(0.0, 0.0, 400.0, 300.0);
let parent = Rect::new(150.0, 150.0, 40.0, 20.0);
let best = RectAlign::find_best_align(
candidates(RectAlign::BOTTOM_START),
screen,
parent,
GAP,
Size::new(100.0, 80.0),
);
assert_eq!(best, Some(RectAlign::BOTTOM_START));
}
#[test]
fn find_best_align_falls_back_to_first_candidate_when_nothing_fits() {
let screen = Rect::new(0.0, 0.0, 100.0, 100.0);
let parent = Rect::new(40.0, 40.0, 20.0, 10.0);
let big = Size::new(500.0, 500.0);
let best = RectAlign::find_best_align(
candidates(RectAlign::BOTTOM_START),
screen,
parent,
GAP,
big,
);
assert_eq!(best, Some(RectAlign::BOTTOM_START));
assert_eq!(
RectAlign::find_best_align(std::iter::empty(), screen, parent, GAP, big),
None
);
}
}