use teksilo_canvas::{EdgeInsets, Rect, Size};
use crate::environment::LayoutDirection;
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct OverlayViewport {
pub size: Size,
pub safe_area: EdgeInsets,
pub occluded: Option<Rect>,
}
impl OverlayViewport {
pub fn new(size: Size) -> Self {
Self {
size,
safe_area: EdgeInsets::ZERO,
occluded: None,
}
}
pub fn with_safe_area(mut self, safe_area: EdgeInsets) -> Self {
self.safe_area = safe_area;
self
}
pub fn with_occluded(mut self, occluded: Option<Rect>) -> Self {
self.occluded = occluded;
self
}
pub fn full(&self) -> Rect {
Rect::from_origin_size(teksilo_canvas::Point::ZERO, self.size)
}
pub fn usable(&self, direction: LayoutDirection) -> Rect {
let rtl = matches!(direction, LayoutDirection::RightToLeft);
let (left, right) = if rtl {
(self.safe_area.trailing, self.safe_area.leading)
} else {
(self.safe_area.leading, self.safe_area.trailing)
};
let safe = Rect::new(
left,
self.safe_area.top,
(self.size.width - left - right).max(0.0),
(self.size.height - self.safe_area.top - self.safe_area.bottom).max(0.0),
);
match self.occluded {
Some(occluded) => largest_free_slab(safe, occluded),
None => safe,
}
}
}
impl Default for OverlayViewport {
fn default() -> Self {
Self::new(Size::ZERO)
}
}
impl From<(f32, f32)> for OverlayViewport {
fn from((width, height): (f32, f32)) -> Self {
Self::new(Size::new(width, height))
}
}
impl From<Size> for OverlayViewport {
fn from(size: Size) -> Self {
Self::new(size)
}
}
fn largest_free_slab(area: Rect, blocked: Rect) -> Rect {
let overlap_w = (area.right().min(blocked.right()) - area.x.max(blocked.x)).max(0.0);
let overlap_h = (area.bottom().min(blocked.bottom()) - area.y.max(blocked.y)).max(0.0);
if overlap_w <= 0.0 || overlap_h <= 0.0 {
return area;
}
let above = Rect::new(area.x, area.y, area.width, (blocked.y - area.y).max(0.0));
let below = Rect::new(
area.x,
blocked.bottom().max(area.y),
area.width,
(area.bottom() - blocked.bottom()).max(0.0),
);
let leading = Rect::new(area.x, area.y, (blocked.x - area.x).max(0.0), area.height);
let trailing = Rect::new(
blocked.right().max(area.x),
area.y,
(area.right() - blocked.right()).max(0.0),
area.height,
);
let best = [above, below, leading, trailing]
.into_iter()
.max_by(|a, b| {
(a.width * a.height)
.partial_cmp(&(b.width * b.height))
.unwrap_or(std::cmp::Ordering::Equal)
})
.unwrap_or(area);
if best.width <= 0.0 || best.height <= 0.0 {
area
} else {
best
}
}
#[cfg(test)]
mod tests {
use super::*;
const LTR: LayoutDirection = LayoutDirection::LeftToRight;
const RTL: LayoutDirection = LayoutDirection::RightToLeft;
#[test]
fn a_bare_size_is_the_whole_window() {
let viewport = OverlayViewport::from((800.0, 600.0));
for direction in [LTR, RTL] {
let usable = viewport.usable(direction);
assert_eq!((usable.x, usable.y), (0.0, 0.0));
assert_eq!((usable.width, usable.height), (800.0, 600.0));
}
assert_eq!(viewport.full(), Rect::new(0.0, 0.0, 800.0, 600.0));
}
#[test]
fn safe_area_insets_mirror_with_the_layout_direction() {
let viewport = OverlayViewport::from((800.0, 600.0))
.with_safe_area(EdgeInsets::new(44.0, 0.0, 34.0, 20.0));
let ltr = viewport.usable(LTR);
assert_eq!((ltr.x, ltr.y), (20.0, 44.0));
assert_eq!((ltr.width, ltr.height), (780.0, 522.0));
let rtl = viewport.usable(RTL);
assert_eq!((rtl.x, rtl.y), (0.0, 44.0));
assert_eq!((rtl.width, rtl.height), (780.0, 522.0));
assert_eq!(rtl.right(), 780.0);
}
#[test]
fn a_bottom_occlusion_leaves_the_band_above_it() {
let viewport = OverlayViewport::from((800.0, 600.0))
.with_occluded(Some(Rect::new(0.0, 380.0, 800.0, 220.0)));
let usable = viewport.usable(LTR);
assert_eq!((usable.x, usable.y), (0.0, 0.0));
assert_eq!((usable.width, usable.height), (800.0, 380.0));
}
#[test]
fn a_side_occlusion_leaves_the_band_beside_it() {
let viewport = OverlayViewport::from((800.0, 600.0))
.with_occluded(Some(Rect::new(500.0, 0.0, 300.0, 600.0)));
let usable = viewport.usable(LTR);
assert_eq!((usable.x, usable.width), (0.0, 500.0));
}
#[test]
fn an_occlusion_that_misses_the_window_changes_nothing() {
let viewport = OverlayViewport::from((800.0, 600.0))
.with_occluded(Some(Rect::new(900.0, 0.0, 100.0, 600.0)));
assert_eq!(viewport.usable(LTR), Rect::new(0.0, 0.0, 800.0, 600.0));
}
#[test]
fn a_total_occlusion_falls_back_to_the_safe_rect() {
let viewport = OverlayViewport::from((800.0, 600.0))
.with_occluded(Some(Rect::new(-10.0, -10.0, 900.0, 700.0)));
assert_eq!(viewport.usable(LTR), Rect::new(0.0, 0.0, 800.0, 600.0));
}
#[test]
fn a_top_occlusion_leaves_the_band_below_it() {
let viewport = OverlayViewport::from((800.0, 600.0))
.with_occluded(Some(Rect::new(0.0, 0.0, 800.0, 200.0)));
assert_eq!(
viewport.usable(LTR),
Rect::new(0.0, 200.0, 800.0, 400.0),
"the band below the bar, not the whole window"
);
}
#[test]
fn a_leading_side_occlusion_leaves_the_band_after_it() {
let viewport = OverlayViewport::from((800.0, 600.0))
.with_occluded(Some(Rect::new(0.0, 0.0, 300.0, 600.0)));
let usable = viewport.usable(LTR);
assert_eq!(
(usable.x, usable.width),
(300.0, 500.0),
"the band after the panel, not the whole window"
);
}
}