use super::*;
const ANCHOR_GAP: f32 = 4.0;
const AVOID_GAP: f32 = 4.0;
const SELECTION_GAP: f32 = 8.0;
fn leading_aligned_x_in(anchor: Rect, actual_width: f32, x_min: f32, x_max: f32, rtl: bool) -> f32 {
let leading = if rtl {
anchor.x + anchor.width - actual_width
} else {
anchor.x
};
leading.min(x_max - actual_width).max(x_min)
}
#[cfg(test)]
fn leading_aligned_x(anchor: Rect, actual_width: f32, vw: f32, rtl: bool) -> f32 {
leading_aligned_x_in(anchor, actual_width, 0.0, vw, rtl)
}
fn at_least_anchor_width(content_width: f32, anchor: Rect) -> f32 {
content_width.max(anchor.width)
}
fn room_above(anchor: Rect, area: Rect) -> f32 {
(anchor.y - ANCHOR_GAP - area.y).max(0.0)
}
fn room_below(anchor: Rect, area: Rect) -> f32 {
(area.bottom() - anchor.bottom() - ANCHOR_GAP).max(0.0)
}
fn above_anchor(anchor: Rect, height: f32, area: Rect) -> (f32, f32) {
let room = room_above(anchor, area);
if height <= room || room <= 0.0 {
(anchor.y - ANCHOR_GAP - height, height)
} else {
(area.y, room)
}
}
fn below_anchor(anchor: Rect, height: f32, area: Rect) -> (f32, f32) {
let room = room_below(anchor, area);
let height = if room > 0.0 { height.min(room) } else { height };
(anchor.bottom() + ANCHOR_GAP, height)
}
fn avoiding_bounds(avoid: Rect, size: Size, area: Rect, rtl: bool) -> Rect {
let (w, h) = (size.width, size.height);
let clamp_x = |x: f32| x.min(area.right() - w).max(area.x);
let clamp_y = |y: f32| y.min(area.bottom() - h).max(area.y);
let start_x = if rtl {
avoid.right() + AVOID_GAP
} else {
avoid.x - AVOID_GAP - w
};
let end_x = if rtl {
avoid.x - AVOID_GAP - w
} else {
avoid.right() + AVOID_GAP
};
let below_y = avoid.bottom() + AVOID_GAP;
let above_y = avoid.y - AVOID_GAP - h;
let fits_x = |x: f32| x >= area.x && x + w <= area.right();
let fits_y = |y: f32| y >= area.y && y + h <= area.bottom();
for x in [start_x, end_x] {
if fits_x(x) {
let y = if fits_y(below_y) {
below_y
} else if fits_y(above_y) {
above_y
} else {
clamp_y(below_y)
};
return Rect::new(x, y, w, h);
}
}
for y in [below_y, above_y] {
if fits_y(y) {
return Rect::new(clamp_x(start_x), y, w, h);
}
}
Rect::new(clamp_x(start_x), clamp_y(below_y), w, h)
}
fn above_selection_bounds(selection: Rect, size: Size, area: Rect, rtl: bool) -> Rect {
let (w, h) = (size.width, size.height);
let above_y = selection.y - SELECTION_GAP - h;
let below_y = selection.bottom() + SELECTION_GAP;
let y = if above_y >= area.y {
above_y
} else if below_y + h <= area.bottom() {
below_y
} else {
above_y.max(area.y)
};
let centred = selection.center().x - w / 2.0;
let x = if rtl {
centred.max(area.x).min(area.right() - w)
} else {
centred.min(area.right() - w).max(area.x)
};
Rect::new(x, y, w, h)
}
impl OverlayManager {
pub fn position_overlays(
&mut self,
anchor_bounds_fn: impl Fn(WidgetId) -> Option<Rect>,
viewport: impl Into<OverlayViewport>,
layout_direction: LayoutDirection,
) {
let viewport: OverlayViewport = viewport.into();
let area = viewport.usable(layout_direction);
let rtl = matches!(layout_direction, LayoutDirection::RightToLeft);
for overlay in &mut self.stack {
let anchor = match anchor_bounds_fn(overlay.anchor) {
Some(a) => a,
None => {
if matches!(
overlay.placement,
OverlayPlacement::Centered
| OverlayPlacement::FullViewport
| OverlayPlacement::BottomCenter
| OverlayPlacement::ViewportCorner { .. }
| OverlayPlacement::AtPointer(_)
| OverlayPlacement::AtPointerAvoiding { .. }
| OverlayPlacement::AboveSelection { .. }
) {
Rect::ZERO
} else {
continue;
}
}
};
let content_size = overlay.bounds.size();
overlay.bounds = match &overlay.placement {
OverlayPlacement::Below => {
let actual_width = at_least_anchor_width(content_size.width, anchor);
let x = leading_aligned_x_in(anchor, actual_width, area.x, area.right(), rtl);
Rect::new(
x,
anchor.y + anchor.height + ANCHOR_GAP,
actual_width,
content_size.height,
)
}
OverlayPlacement::Above => {
let actual_width = at_least_anchor_width(content_size.width, anchor);
let x = leading_aligned_x_in(anchor, actual_width, area.x, area.right(), rtl);
let (y, height) = above_anchor(anchor, content_size.height, area);
Rect::new(x, y, actual_width, height)
}
OverlayPlacement::TrailingEdge => {
let x = if rtl {
let x_left = anchor.x - content_size.width - 2.0;
if x_left >= area.x {
x_left
} else {
anchor.x + anchor.width + 2.0
}
} else {
let x_right = anchor.x + anchor.width + 2.0;
if x_right + content_size.width <= area.right() {
x_right
} else {
anchor.x - content_size.width - 2.0
}
};
let y = anchor
.y
.min(area.bottom() - content_size.height)
.max(area.y);
Rect::new(x, y, content_size.width, content_size.height)
}
OverlayPlacement::AtPointer(point) => {
let x = point.x.min(area.right() - content_size.width).max(area.x);
let y = if point.y + content_size.height <= area.bottom() {
point.y
} else {
(point.y - content_size.height).max(area.y)
};
Rect::new(x, y, content_size.width, content_size.height)
}
OverlayPlacement::AtPointerAvoiding { avoid, .. } => {
avoiding_bounds(*avoid, content_size, area, rtl)
}
OverlayPlacement::AboveSelection { selection } => {
above_selection_bounds(*selection, content_size, area, rtl)
}
OverlayPlacement::NearAnchor { offset } => {
let below_y = anchor.y + anchor.height + offset.y + ANCHOR_GAP;
let fits_below = below_y + content_size.height <= area.bottom();
let y = if fits_below {
below_y
} else {
let above_y = anchor.y - content_size.height - offset.y - ANCHOR_GAP;
above_y.max(area.y)
};
let unclamped_x = if rtl {
anchor.x + anchor.width - content_size.width - offset.x
} else {
anchor.x + offset.x
};
let x = unclamped_x
.min(area.right() - content_size.width)
.max(area.x);
Rect::new(x, y, content_size.width, content_size.height)
}
OverlayPlacement::Centered => Rect::new(
area.x + ((area.width - content_size.width) / 2.0).max(0.0),
area.y + ((area.height - content_size.height) / 2.0).max(0.0),
content_size.width.min(area.width),
content_size.height.min(area.height),
),
OverlayPlacement::BottomCenter => Rect::new(
area.x + ((area.width - content_size.width) / 2.0).max(0.0),
(area.bottom() - content_size.height - 24.0).max(area.y),
content_size.width.min(area.width),
content_size.height.min(area.height),
),
OverlayPlacement::BelowPreferred => {
let wanted = content_size.height;
let above = room_above(anchor, area);
let below = room_below(anchor, area);
let (y, height) = if wanted <= below || (wanted > above && below > above) {
below_anchor(anchor, wanted, area)
} else {
above_anchor(anchor, wanted, area)
};
let actual_width = at_least_anchor_width(content_size.width, anchor);
let x = leading_aligned_x_in(anchor, actual_width, area.x, area.right(), rtl);
Rect::new(x, y, actual_width, height)
}
OverlayPlacement::ViewportCorner { corner, margin } => {
let (x, y) = corner.resolve(
(content_size.width, content_size.height),
(area.width, area.height),
(margin.x, margin.y),
rtl,
);
Rect::new(
area.x + x,
area.y + y,
content_size.width.min(area.width),
content_size.height.min(area.height),
)
}
OverlayPlacement::FullViewport => viewport.full(),
};
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::overlay::tests::fake_id;
#[test]
fn a_wide_overlay_near_the_trailing_edge_is_clamped_into_the_viewport() {
let vw = 1200.0;
let anchor = Rect::new(1035.0, 760.0, 90.0, 28.0);
let x = leading_aligned_x(anchor, 380.0, vw, false);
assert!(
x + 380.0 <= vw + 0.01,
"overlay must not extend past the viewport: x={x}"
);
assert!(x >= 0.0, "and must not start off the leading edge: x={x}");
let inside = Rect::new(100.0, 760.0, 90.0, 28.0);
assert_eq!(leading_aligned_x(inside, 380.0, vw, false), 100.0);
let x_rtl = leading_aligned_x(inside, 380.0, vw, true);
assert!(x_rtl >= 0.0 && x_rtl + 380.0 <= vw + 0.01);
}
#[test]
fn an_overlay_wider_than_the_viewport_keeps_its_leading_edge_visible() {
let x = leading_aligned_x(Rect::new(40.0, 10.0, 60.0, 20.0), 900.0, 500.0, false);
assert_eq!(x, 0.0);
}
#[test]
fn centered_placement_uses_viewport_center() {
let mut mgr = OverlayManager::new();
let id = mgr.show(OverlayRequest {
content_id: fake_id(10),
anchor: fake_id(1),
placement: OverlayPlacement::Centered,
dismiss: DismissBehavior::Manual,
layer: OverlayLayer::InTree,
parent_overlay: None,
on_dismiss: None,
fade_duration: None,
});
mgr.set_content_bounds(id, Size::new(240.0, 120.0));
mgr.position_overlays(
|_| Some(Rect::new(0.0, 0.0, 10.0, 10.0)),
(800.0, 600.0),
LayoutDirection::LeftToRight,
);
let bounds = mgr
.stack
.iter()
.find(|overlay| overlay.id == id)
.unwrap()
.bounds;
assert!((bounds.x - 280.0).abs() < 0.01);
assert!((bounds.y - 240.0).abs() < 0.01);
}
#[test]
fn bottom_center_placement_uses_viewport_bottom_margin() {
let mut mgr = OverlayManager::new();
let id = mgr.show(OverlayRequest {
content_id: fake_id(10),
anchor: fake_id(1),
placement: OverlayPlacement::BottomCenter,
dismiss: DismissBehavior::Manual,
layer: OverlayLayer::InTree,
parent_overlay: None,
on_dismiss: None,
fade_duration: None,
});
mgr.set_content_bounds(id, Size::new(240.0, 64.0));
mgr.position_overlays(
|_| Some(Rect::new(0.0, 0.0, 10.0, 10.0)),
(800.0, 600.0),
LayoutDirection::LeftToRight,
);
let bounds = mgr
.stack
.iter()
.find(|overlay| overlay.id == id)
.unwrap()
.bounds;
assert!((bounds.x - 280.0).abs() < 0.01);
assert!((bounds.y - 512.0).abs() < 0.01);
}
fn show_corner_overlay(
mgr: &mut OverlayManager,
corner: Corner,
margin: Vec2,
size: Size,
) -> OverlayId {
let id = mgr.show(OverlayRequest {
content_id: fake_id(10),
anchor: fake_id(1),
placement: OverlayPlacement::ViewportCorner { corner, margin },
dismiss: DismissBehavior::Manual,
layer: OverlayLayer::InTree,
parent_overlay: None,
on_dismiss: None,
fade_duration: None,
});
mgr.set_content_bounds(id, size);
id
}
fn overlay_bounds(mgr: &OverlayManager, id: OverlayId) -> Rect {
mgr.stack.iter().find(|o| o.id == id).unwrap().bounds
}
#[test]
fn viewport_corner_top_leading_ltr() {
let mut mgr = OverlayManager::new();
let id = show_corner_overlay(
&mut mgr,
Corner::TopLeading,
Vec2::new(24.0, 24.0),
Size::new(380.0, 100.0),
);
mgr.position_overlays(
|_| Some(Rect::ZERO),
(800.0, 600.0),
LayoutDirection::LeftToRight,
);
let b = overlay_bounds(&mgr, id);
assert!((b.x - 24.0).abs() < 0.01, "x = {}", b.x);
assert!((b.y - 24.0).abs() < 0.01, "y = {}", b.y);
}
#[test]
fn viewport_corner_top_trailing_ltr() {
let mut mgr = OverlayManager::new();
let id = show_corner_overlay(
&mut mgr,
Corner::TopTrailing,
Vec2::new(24.0, 24.0),
Size::new(380.0, 100.0),
);
mgr.position_overlays(
|_| Some(Rect::ZERO),
(800.0, 600.0),
LayoutDirection::LeftToRight,
);
let b = overlay_bounds(&mgr, id);
assert!((b.x - 396.0).abs() < 0.01, "x = {}", b.x);
assert!((b.y - 24.0).abs() < 0.01);
}
#[test]
fn viewport_corner_bottom_leading_ltr() {
let mut mgr = OverlayManager::new();
let id = show_corner_overlay(
&mut mgr,
Corner::BottomLeading,
Vec2::new(24.0, 24.0),
Size::new(380.0, 100.0),
);
mgr.position_overlays(
|_| Some(Rect::ZERO),
(800.0, 600.0),
LayoutDirection::LeftToRight,
);
let b = overlay_bounds(&mgr, id);
assert!((b.x - 24.0).abs() < 0.01);
assert!((b.y - 476.0).abs() < 0.01, "y = {}", b.y);
}
#[test]
fn viewport_corner_bottom_trailing_ltr() {
let mut mgr = OverlayManager::new();
let id = show_corner_overlay(
&mut mgr,
Corner::BottomTrailing,
Vec2::new(24.0, 24.0),
Size::new(380.0, 100.0),
);
mgr.position_overlays(
|_| Some(Rect::ZERO),
(800.0, 600.0),
LayoutDirection::LeftToRight,
);
let b = overlay_bounds(&mgr, id);
assert!((b.x - 396.0).abs() < 0.01);
assert!((b.y - 476.0).abs() < 0.01);
}
#[test]
fn viewport_corner_top_trailing_rtl_flips_to_left() {
let mut mgr = OverlayManager::new();
let id = show_corner_overlay(
&mut mgr,
Corner::TopTrailing,
Vec2::new(24.0, 24.0),
Size::new(380.0, 100.0),
);
mgr.position_overlays(
|_| Some(Rect::ZERO),
(800.0, 600.0),
LayoutDirection::RightToLeft,
);
let b = overlay_bounds(&mgr, id);
assert!((b.x - 24.0).abs() < 0.01, "x = {}", b.x);
assert!((b.y - 24.0).abs() < 0.01);
}
#[test]
fn viewport_corner_bottom_leading_rtl_flips_to_right() {
let mut mgr = OverlayManager::new();
let id = show_corner_overlay(
&mut mgr,
Corner::BottomLeading,
Vec2::new(24.0, 24.0),
Size::new(380.0, 100.0),
);
mgr.position_overlays(
|_| Some(Rect::ZERO),
(800.0, 600.0),
LayoutDirection::RightToLeft,
);
let b = overlay_bounds(&mgr, id);
assert!((b.x - 396.0).abs() < 0.01, "x = {}", b.x);
assert!((b.y - 476.0).abs() < 0.01);
}
#[test]
fn viewport_corner_ignores_anchor_bounds() {
let mut mgr = OverlayManager::new();
let id = show_corner_overlay(
&mut mgr,
Corner::BottomTrailing,
Vec2::new(0.0, 0.0),
Size::new(100.0, 100.0),
);
mgr.position_overlays(
|_| Some(Rect::new(123.0, 456.0, 7.0, 8.0)),
(800.0, 600.0),
LayoutDirection::LeftToRight,
);
let b = overlay_bounds(&mgr, id);
assert_eq!((b.x, b.y), (700.0, 500.0));
}
#[test]
fn near_anchor_horizontal_is_direction_aware() {
let anchor = Rect::new(600.0, 100.0, 100.0, 20.0);
let resolved_x = |dir: LayoutDirection| {
let mut mgr = OverlayManager::new();
let id = mgr.show(OverlayRequest {
content_id: fake_id(10),
anchor: fake_id(1),
placement: OverlayPlacement::NearAnchor {
offset: Vec2::new(0.0, 8.0),
},
dismiss: DismissBehavior::Manual,
layer: OverlayLayer::InTree,
parent_overlay: None,
on_dismiss: None,
fade_duration: None,
});
mgr.set_content_bounds(id, Size::new(200.0, 50.0));
mgr.position_overlays(|_| Some(anchor), (800.0, 600.0), dir);
overlay_bounds(&mgr, id).x
};
assert!(
(resolved_x(LayoutDirection::LeftToRight) - 600.0).abs() < 0.01,
"LTR x = {}",
resolved_x(LayoutDirection::LeftToRight)
);
assert!(
(resolved_x(LayoutDirection::RightToLeft) - 500.0).abs() < 0.01,
"RTL x = {}",
resolved_x(LayoutDirection::RightToLeft)
);
}
fn overlaps(a: Rect, b: Rect) -> bool {
a.x < b.right() && b.x < a.right() && a.y < b.bottom() && b.y < a.bottom()
}
fn placed(
placement: OverlayPlacement,
size: Size,
anchor: Rect,
viewport: impl Into<OverlayViewport>,
direction: LayoutDirection,
) -> Rect {
let mut mgr = OverlayManager::new();
let id = mgr.show(OverlayRequest {
content_id: fake_id(10),
anchor: fake_id(1),
placement,
dismiss: DismissBehavior::Manual,
layer: OverlayLayer::InTree,
parent_overlay: None,
on_dismiss: None,
fade_duration: None,
});
mgr.set_content_bounds(id, size);
mgr.position_overlays(|_| Some(anchor), viewport, direction);
overlay_bounds(&mgr, id)
}
#[test]
fn a_coarse_menu_clears_the_contact_at_every_corner() {
let viewport = (800.0, 600.0);
let menu = Size::new(220.0, 260.0);
for point in [
Point::new(6.0, 6.0),
Point::new(794.0, 6.0),
Point::new(6.0, 594.0),
Point::new(794.0, 594.0),
] {
for direction in [LayoutDirection::LeftToRight, LayoutDirection::RightToLeft] {
let placement = OverlayPlacement::at_pointer_for(
point,
&crate::pointer::PointerInfo::touch(
crate::pointer::PointerId::MOUSE,
crate::pointer::EventTime::ZERO,
),
);
let OverlayPlacement::AtPointerAvoiding { avoid, .. } = placement else {
panic!("a coarse pointer must get the avoiding placement");
};
let bounds = placed(placement.clone(), menu, Rect::ZERO, viewport, direction);
assert!(
!overlaps(bounds, avoid),
"menu at {point:?} ({direction:?}) sits under the contact: \
bounds {bounds:?}, contact {avoid:?}"
);
assert!(
bounds.x >= -0.01
&& bounds.y >= -0.01
&& bounds.right() <= 800.01
&& bounds.bottom() <= 600.01,
"menu at {point:?} ({direction:?}) left the viewport: {bounds:?}"
);
}
}
}
#[test]
fn the_avoiding_quadrant_mirrors_under_rtl() {
let point = Point::new(400.0, 300.0);
let avoid = crate::overlay::rect_centred_on(point, ASSUMED_CONTACT_PATCH);
let menu = Size::new(220.0, 260.0);
let at = |direction| {
placed(
OverlayPlacement::AtPointerAvoiding { point, avoid },
menu,
Rect::ZERO,
(800.0, 600.0),
direction,
)
};
let ltr = at(LayoutDirection::LeftToRight);
let rtl = at(LayoutDirection::RightToLeft);
assert!(
ltr.right() <= avoid.x,
"LTR opens to the left of the contact: {ltr:?} vs {avoid:?}"
);
assert!(
rtl.x >= avoid.right(),
"RTL opens to the right of the contact: {rtl:?} vs {avoid:?}"
);
assert!(
((avoid.x - ltr.right()) - (rtl.x - avoid.right())).abs() < 0.01,
"the two sides must be symmetric: {ltr:?} / {rtl:?}"
);
assert_eq!(ltr.y, rtl.y, "only the inline axis mirrors");
}
fn finger(contact: Option<Size>) -> crate::pointer::PointerInfo {
let mut pointer = crate::pointer::PointerInfo::touch(
crate::pointer::PointerId::MOUSE,
crate::pointer::EventTime::ZERO,
);
pointer.axes.contact = contact;
pointer
}
fn avoided(point: Point, contact: Option<Size>) -> (Rect, Rect) {
let placement = OverlayPlacement::at_pointer_for(point, &finger(contact));
let OverlayPlacement::AtPointerAvoiding { avoid, .. } = placement else {
panic!("a coarse pointer must get the avoiding placement");
};
let bounds = placed(
placement,
Size::new(220.0, 260.0),
Rect::ZERO,
(800.0, 600.0),
LayoutDirection::LeftToRight,
);
(avoid, bounds)
}
#[test]
fn a_contact_patch_larger_than_the_floor_widens_what_the_menu_clears() {
let point = Point::new(400.0, 300.0);
let (avoid, bounds) = avoided(point, Some(Size::new(60.0, 40.0)));
assert_eq!(
avoid,
Rect::new(370.0, 280.0, 60.0, 40.0),
"the reported patch, centred on the contact"
);
assert!(
bounds.right() <= avoid.x + 0.01,
"the menu must clear the thumb: {bounds:?} vs {avoid:?}"
);
let (_, floored) = avoided(point, None);
assert!(
bounds.right() < floored.right() - 0.01,
"a 60 dp patch must push the menu further out than the 24 dp floor \
would ({} vs {})",
bounds.right(),
floored.right()
);
}
#[test]
fn a_contact_patch_smaller_than_the_floor_is_raised_to_it() {
let point = Point::new(400.0, 300.0);
let (avoid, bounds) = avoided(point, Some(Size::new(6.0, 6.0)));
assert_eq!(
avoid,
crate::overlay::rect_centred_on(point, ASSUMED_CONTACT_PATCH),
"the floor, not the 6 dp the device claimed"
);
let claimed = crate::overlay::rect_centred_on(point, Size::new(6.0, 6.0));
assert!(
bounds.right() < claimed.x - 0.01,
"clearing only the reported 6 dp leaves the menu under the finger: \
{bounds:?} vs the 24 dp floor at {avoid:?}"
);
let (_, floored) = avoided(point, None);
assert_eq!(
bounds, floored,
"an under-reported patch must place exactly as no report at all"
);
}
#[test]
fn a_mouse_still_gets_the_plain_at_pointer_placement() {
let point = Point::new(400.0, 300.0);
let mouse = crate::pointer::PointerInfo::mouse(crate::pointer::EventTime::ZERO);
assert!(matches!(
OverlayPlacement::at_pointer_for(point, &mouse),
OverlayPlacement::AtPointer(p) if p == point
));
}
#[test]
fn a_drop_down_never_covers_the_combo_box_that_opened_it() {
let anchor = Rect::new(0.0, 0.0, 300.0, 60.0);
let bounds = placed(
OverlayPlacement::BelowPreferred,
Size::new(300.0, 80.0),
anchor,
(300.0, 60.0),
LayoutDirection::LeftToRight,
);
assert!(
!overlaps(bounds, anchor),
"the list sits on top of its own combo box: {bounds:?} over {anchor:?}"
);
assert!(
bounds.bottom() <= anchor.y,
"the list must stay above the trigger: {bounds:?}"
);
assert_eq!(bounds.height, 80.0, "an empty panel is not the answer");
assert_eq!(bounds.y, -84.0, "hung off the anchor's top edge");
}
#[test]
fn a_drop_down_that_fits_neither_side_takes_the_roomier_one_and_shrinks() {
let anchor = Rect::new(100.0, 10.0, 80.0, 20.0);
let bounds = placed(
OverlayPlacement::BelowPreferred,
Size::new(120.0, 200.0),
anchor,
(800.0, 200.0),
LayoutDirection::LeftToRight,
);
assert_eq!(bounds.y, 34.0, "stays below — that is the roomier side");
assert_eq!(bounds.height, 166.0, "shrunk to the room that is there");
assert!(
bounds.bottom() <= 200.0,
"and inside the window: {bounds:?}"
);
}
#[test]
fn a_drop_down_that_fits_above_still_flips_there_unchanged() {
let anchor = Rect::new(100.0, 400.0, 80.0, 20.0);
let bounds = placed(
OverlayPlacement::BelowPreferred,
Size::new(120.0, 200.0),
anchor,
(800.0, 460.0),
LayoutDirection::LeftToRight,
);
assert_eq!(bounds.y, 196.0, "400 - 200 - 4");
assert_eq!(bounds.height, 200.0, "no shrink: it fits");
}
#[test]
fn an_above_panel_too_tall_for_the_room_shrinks_rather_than_covering() {
let anchor = Rect::new(100.0, 60.0, 80.0, 20.0);
let bounds = placed(
OverlayPlacement::Above,
Size::new(120.0, 200.0),
anchor,
(800.0, 600.0),
LayoutDirection::LeftToRight,
);
assert_eq!(bounds.y, 0.0, "pinned to the top of the usable area");
assert_eq!(
bounds.height, 56.0,
"the 60 dp above the anchor, less the gap"
);
assert!(
bounds.bottom() <= anchor.y,
"and clear of the anchor: {bounds:?}"
);
}
#[test]
fn a_shrunk_panel_starts_below_the_notch() {
let viewport = OverlayViewport::from((800.0, 600.0))
.with_safe_area(teksilo_canvas::EdgeInsets::new(40.0, 0.0, 0.0, 0.0));
let anchor = Rect::new(100.0, 200.0, 80.0, 20.0);
let bounds = placed(
OverlayPlacement::Above,
Size::new(120.0, 220.0),
anchor,
viewport,
LayoutDirection::LeftToRight,
);
assert_eq!(bounds.y, 40.0, "below the notch");
assert_eq!(bounds.height, 156.0, "200 - 40 - 4");
}
#[test]
fn a_selection_toolbar_floats_centred_above_the_selection() {
let selection = Rect::new(300.0, 300.0, 100.0, 20.0);
let bounds = placed(
OverlayPlacement::AboveSelection { selection },
Size::new(200.0, 40.0),
Rect::ZERO,
(800.0, 600.0),
LayoutDirection::LeftToRight,
);
assert_eq!(bounds.x, 250.0, "centred on the selection");
assert_eq!(bounds.y, 252.0, "8 dp above it");
}
#[test]
fn a_selection_toolbar_flips_below_at_the_top_of_the_window() {
let selection = Rect::new(300.0, 10.0, 100.0, 20.0);
let bounds = placed(
OverlayPlacement::AboveSelection { selection },
Size::new(200.0, 40.0),
Rect::ZERO,
(800.0, 600.0),
LayoutDirection::LeftToRight,
);
assert_eq!(bounds.y, 38.0, "8 dp below the selection");
}
#[test]
fn a_selection_toolbar_keeps_its_inline_start_edge_when_it_cannot_fit() {
let selection = Rect::new(10.0, 300.0, 20.0, 20.0);
let toolbar = Size::new(200.0, 40.0);
let at = |direction| {
placed(
OverlayPlacement::AboveSelection { selection },
toolbar,
Rect::ZERO,
(100.0, 600.0),
direction,
)
};
let ltr = at(LayoutDirection::LeftToRight);
let rtl = at(LayoutDirection::RightToLeft);
assert_eq!(ltr.x, 0.0, "LTR keeps the left edge");
assert_eq!(rtl.right(), 100.0, "RTL keeps the right edge");
}
#[test]
fn a_centered_modal_centres_inside_the_safe_area() {
let viewport = OverlayViewport::from((800.0, 600.0))
.with_safe_area(teksilo_canvas::EdgeInsets::new(100.0, 0.0, 0.0, 0.0));
let bounds = placed(
OverlayPlacement::Centered,
Size::new(200.0, 100.0),
Rect::ZERO,
viewport,
LayoutDirection::LeftToRight,
);
assert_eq!(bounds.y, 300.0, "centred in the 500 dp below the notch");
}
#[test]
fn the_scrim_still_covers_the_whole_window() {
let viewport = OverlayViewport::from((800.0, 600.0))
.with_safe_area(teksilo_canvas::EdgeInsets::uniform(40.0))
.with_occluded(Some(Rect::new(0.0, 400.0, 800.0, 200.0)));
let bounds = placed(
OverlayPlacement::FullViewport,
Size::ZERO,
Rect::ZERO,
viewport,
LayoutDirection::LeftToRight,
);
assert_eq!(bounds, Rect::new(0.0, 0.0, 800.0, 600.0));
}
#[test]
fn an_occluded_band_is_not_a_place_to_put_a_menu() {
let viewport = OverlayViewport::from((800.0, 600.0))
.with_occluded(Some(Rect::new(0.0, 340.0, 800.0, 260.0)));
let bounds = placed(
OverlayPlacement::AtPointer(Point::new(100.0, 300.0)),
Size::new(200.0, 120.0),
Rect::ZERO,
viewport,
LayoutDirection::LeftToRight,
);
assert!(
bounds.bottom() <= 340.01,
"the menu must stay above the keyboard: {bounds:?}"
);
}
#[test]
fn viewport_corner_zero_margin_snaps_to_edge() {
let mut mgr = OverlayManager::new();
let id = show_corner_overlay(
&mut mgr,
Corner::TopLeading,
Vec2::ZERO,
Size::new(50.0, 50.0),
);
mgr.position_overlays(
|_| Some(Rect::ZERO),
(800.0, 600.0),
LayoutDirection::LeftToRight,
);
let b = overlay_bounds(&mgr, id);
assert_eq!((b.x, b.y), (0.0, 0.0));
}
fn inset_viewport() -> OverlayViewport {
OverlayViewport::from((800.0, 600.0))
.with_safe_area(teksilo_canvas::EdgeInsets::new(40.0, 40.0, 30.0, 60.0))
}
fn inset_usable(direction: LayoutDirection) -> Rect {
inset_viewport().usable(direction)
}
#[test]
fn a_below_panel_is_clamped_into_the_usable_area_not_the_window() {
let ltr = inset_usable(LayoutDirection::LeftToRight);
let panel = Size::new(300.0, 180.0);
let trailing = placed(
OverlayPlacement::Below,
panel,
Rect::new(690.0, 200.0, 60.0, 24.0),
inset_viewport(),
LayoutDirection::LeftToRight,
);
assert_eq!(trailing.x, 460.0, "clamped to the usable trailing edge");
assert_eq!(trailing.right(), ltr.right());
let leading = placed(
OverlayPlacement::Below,
panel,
Rect::new(20.0, 200.0, 60.0, 24.0),
inset_viewport(),
LayoutDirection::LeftToRight,
);
assert_eq!(leading.x, ltr.x, "clamped to the usable leading edge");
let rtl = inset_usable(LayoutDirection::RightToLeft);
let mirrored = placed(
OverlayPlacement::Below,
panel,
Rect::new(700.0, 200.0, 60.0, 24.0),
inset_viewport(),
LayoutDirection::RightToLeft,
);
assert_eq!(mirrored.x, 440.0, "the mirrored trailing edge");
assert_eq!(mirrored.right(), rtl.right());
let low = placed(
OverlayPlacement::Below,
panel,
Rect::new(100.0, 520.0, 60.0, 24.0),
inset_viewport(),
LayoutDirection::LeftToRight,
);
assert_eq!(low.y, 548.0, "the anchor's bottom edge plus the gap");
assert!(
low.bottom() > ltr.bottom(),
"and it is allowed to run past the usable bottom: {low:?}"
);
}
#[test]
fn an_above_panel_is_clamped_into_the_usable_area_not_the_window() {
let ltr = inset_usable(LayoutDirection::LeftToRight);
let panel = Size::new(300.0, 100.0);
let trailing = placed(
OverlayPlacement::Above,
panel,
Rect::new(690.0, 300.0, 60.0, 24.0),
inset_viewport(),
LayoutDirection::LeftToRight,
);
assert_eq!(trailing.x, 460.0, "clamped to the usable trailing edge");
assert_eq!(trailing.right(), ltr.right());
assert_eq!(trailing.y, 196.0, "and still sits above the anchor");
let leading = placed(
OverlayPlacement::Above,
panel,
Rect::new(20.0, 300.0, 60.0, 24.0),
inset_viewport(),
LayoutDirection::LeftToRight,
);
assert_eq!(leading.x, ltr.x, "clamped to the usable leading edge");
}
#[test]
fn a_below_preferred_panel_is_placed_against_the_usable_area_not_the_window() {
let ltr = inset_usable(LayoutDirection::LeftToRight);
let panel = Size::new(300.0, 100.0);
let trailing = placed(
OverlayPlacement::BelowPreferred,
panel,
Rect::new(690.0, 300.0, 60.0, 24.0),
inset_viewport(),
LayoutDirection::LeftToRight,
);
assert_eq!(trailing.x, 460.0, "clamped to the usable trailing edge");
assert_eq!(trailing.right(), ltr.right());
assert_eq!(trailing.y, 328.0, "and drops below, which still fits");
let leading = placed(
OverlayPlacement::BelowPreferred,
panel,
Rect::new(20.0, 300.0, 60.0, 24.0),
inset_viewport(),
LayoutDirection::LeftToRight,
);
assert_eq!(leading.x, ltr.x, "clamped to the usable leading edge");
let anchor = Rect::new(100.0, 400.0, 60.0, 24.0);
let flipped = placed(
OverlayPlacement::BelowPreferred,
Size::new(300.0, 160.0),
anchor,
inset_viewport(),
LayoutDirection::LeftToRight,
);
assert_eq!(
flipped.y, 236.0,
"flipped above: 160 dp does not fit in 142"
);
assert_eq!(flipped.height, 160.0, "with nothing shrunk away");
assert!(
flipped.bottom() <= anchor.y,
"and clear of the anchor: {flipped:?}"
);
}
#[test]
fn a_near_anchor_tooltip_is_placed_against_the_usable_area_not_the_window() {
let ltr = inset_usable(LayoutDirection::LeftToRight);
let tooltip = Size::new(300.0, 100.0);
let near = |anchor, size| {
placed(
OverlayPlacement::NearAnchor {
offset: Vec2::new(0.0, 8.0),
},
size,
anchor,
inset_viewport(),
LayoutDirection::LeftToRight,
)
};
let trailing = near(Rect::new(690.0, 200.0, 60.0, 24.0), tooltip);
assert_eq!(trailing.x, 460.0, "clamped to the usable trailing edge");
assert_eq!(trailing.right(), ltr.right());
let leading = near(Rect::new(20.0, 200.0, 60.0, 24.0), tooltip);
assert_eq!(leading.x, ltr.x, "clamped to the usable leading edge");
let flipped = near(Rect::new(100.0, 440.0, 60.0, 24.0), tooltip);
assert_eq!(
flipped.y, 328.0,
"no room below the anchor a person can see"
);
let pinned = near(Rect::new(100.0, 500.0, 60.0, 24.0), Size::new(300.0, 480.0));
assert_eq!(pinned.y, ltr.y, "flipped and pinned below the notch");
}
#[test]
fn an_at_pointer_menu_is_placed_against_the_usable_area_not_the_window() {
let ltr = inset_usable(LayoutDirection::LeftToRight);
let at = |point, size| {
placed(
OverlayPlacement::AtPointer(point),
size,
Rect::ZERO,
inset_viewport(),
LayoutDirection::LeftToRight,
)
};
let menu = Size::new(300.0, 100.0);
let trailing = at(Point::new(700.0, 200.0), menu);
assert_eq!(trailing.x, 460.0, "clamped to the usable trailing edge");
assert_eq!(trailing.right(), ltr.right());
let leading = at(Point::new(20.0, 200.0), menu);
assert_eq!(leading.x, ltr.x, "clamped to the usable leading edge");
let flipped = at(Point::new(100.0, 500.0), Size::new(300.0, 80.0));
assert_eq!(flipped.y, 420.0, "opened above the pointer");
let pinned = at(Point::new(100.0, 500.0), Size::new(300.0, 480.0));
assert_eq!(pinned.y, ltr.y, "flipped and pinned below the notch");
}
#[test]
fn a_submenu_flips_and_clamps_against_the_usable_area_not_the_window() {
let ltr = inset_usable(LayoutDirection::LeftToRight);
let submenu = Size::new(200.0, 100.0);
let at = |anchor, direction| {
placed(
OverlayPlacement::TrailingEdge,
submenu,
anchor,
inset_viewport(),
direction,
)
};
let flipped = at(
Rect::new(500.0, 200.0, 60.0, 24.0),
LayoutDirection::LeftToRight,
);
assert_eq!(flipped.x, 298.0, "opened to the leading side instead");
let low = at(
Rect::new(500.0, 520.0, 60.0, 24.0),
LayoutDirection::LeftToRight,
);
assert_eq!(low.y, 470.0, "pulled up to sit above the home indicator");
assert_eq!(low.bottom(), ltr.bottom());
let high = at(
Rect::new(500.0, 10.0, 60.0, 24.0),
LayoutDirection::LeftToRight,
);
assert_eq!(high.y, ltr.y, "and pushed down below the notch");
let mirrored = at(
Rect::new(230.0, 200.0, 60.0, 24.0),
LayoutDirection::RightToLeft,
);
assert_eq!(
mirrored.x, 292.0,
"in RTL the flip is to the right, because 28 is under the rounded \
corner: RTL puts `safe_area.trailing` (40 dp) on the left, and \
the 60 dp sensor housing on the right"
);
}
#[test]
fn a_coarse_menu_avoids_the_contact_without_leaving_the_usable_area() {
let ltr = inset_usable(LayoutDirection::LeftToRight);
let avoid = Rect::new(250.0, 200.0, 24.0, 24.0);
let bounds = placed(
OverlayPlacement::AtPointerAvoiding {
point: avoid.center(),
avoid,
},
Size::new(220.0, 260.0),
Rect::ZERO,
inset_viewport(),
LayoutDirection::LeftToRight,
);
assert_eq!(bounds.x, 278.0, "the inline-end quadrant, past the contact");
assert!(
bounds.x >= ltr.x && bounds.right() <= ltr.right(),
"inside the usable area: {bounds:?}"
);
assert!(
!overlaps(bounds, avoid),
"and still clear of the finger: {bounds:?}"
);
}
#[test]
fn a_selection_toolbar_is_placed_against_the_usable_area_not_the_window() {
let toolbar = Size::new(200.0, 40.0);
let at = |selection, direction| {
placed(
OverlayPlacement::AboveSelection { selection },
toolbar,
Rect::ZERO,
inset_viewport(),
direction,
)
};
let flipped = at(
Rect::new(300.0, 60.0, 100.0, 20.0),
LayoutDirection::LeftToRight,
);
assert_eq!(flipped.y, 88.0, "below the selection, clear of the notch");
let ltr = inset_usable(LayoutDirection::LeftToRight);
let start = at(
Rect::new(70.0, 300.0, 20.0, 20.0),
LayoutDirection::LeftToRight,
);
assert_eq!(start.x, ltr.x, "LTR keeps the usable left edge");
let rtl = inset_usable(LayoutDirection::RightToLeft);
let end = at(
Rect::new(720.0, 300.0, 20.0, 20.0),
LayoutDirection::RightToLeft,
);
assert_eq!(end.right(), rtl.right(), "RTL keeps the usable right edge");
}
#[test]
fn a_centered_modal_is_centred_and_shrunk_inside_the_usable_area() {
let ltr = inset_usable(LayoutDirection::LeftToRight);
let centred = placed(
OverlayPlacement::Centered,
Size::new(200.0, 100.0),
Rect::ZERO,
inset_viewport(),
LayoutDirection::LeftToRight,
);
assert_eq!(centred.x, 310.0, "centred between the side insets");
assert_eq!(centred.y, 255.0, "and between the notch and the indicator");
let oversized = placed(
OverlayPlacement::Centered,
Size::new(900.0, 700.0),
Rect::ZERO,
inset_viewport(),
LayoutDirection::LeftToRight,
);
assert_eq!(oversized.size(), ltr.size(), "shrunk to the usable area");
assert_eq!((oversized.x, oversized.y), (ltr.x, ltr.y));
}
#[test]
fn a_snackbar_sits_above_the_home_indicator() {
let bounds = placed(
OverlayPlacement::BottomCenter,
Size::new(300.0, 60.0),
Rect::ZERO,
inset_viewport(),
LayoutDirection::LeftToRight,
);
assert_eq!(bounds.y, 486.0, "24 dp above the usable bottom");
assert_eq!(bounds.x, 260.0, "centred between the side insets");
}
#[test]
fn a_toast_corner_is_a_corner_of_the_usable_area() {
let ltr = inset_usable(LayoutDirection::LeftToRight);
let corner = |corner| {
placed(
OverlayPlacement::ViewportCorner {
corner,
margin: Vec2::ZERO,
},
Size::new(100.0, 50.0),
Rect::ZERO,
inset_viewport(),
LayoutDirection::LeftToRight,
)
};
let top_leading = corner(Corner::TopLeading);
assert_eq!((top_leading.x, top_leading.y), (ltr.x, ltr.y));
let bottom_trailing = corner(Corner::BottomTrailing);
assert_eq!(
(bottom_trailing.right(), bottom_trailing.bottom()),
(ltr.right(), ltr.bottom())
);
}
#[test]
fn an_oversized_toast_is_shrunk_to_the_usable_area() {
let ltr = inset_usable(LayoutDirection::LeftToRight);
let corner = |corner| {
placed(
OverlayPlacement::ViewportCorner {
corner,
margin: Vec2::ZERO,
},
Size::new(760.0, 560.0),
Rect::ZERO,
inset_viewport(),
LayoutDirection::LeftToRight,
)
};
for c in [
Corner::TopLeading,
Corner::TopTrailing,
Corner::BottomLeading,
Corner::BottomTrailing,
] {
assert_eq!(
corner(c).size(),
ltr.size(),
"{c:?}: shrunk to the usable area, not left at 760 x 560"
);
}
let top_leading = corner(Corner::TopLeading);
assert_eq!((top_leading.x, top_leading.y), (ltr.x, ltr.y));
assert_eq!(
(top_leading.right(), top_leading.bottom()),
(ltr.right(), ltr.bottom()),
"clear of the rounded corner and of the home indicator"
);
}
#[test]
fn a_menu_too_big_to_clear_the_contact_is_still_clamped_on_both_axes() {
let ltr = inset_usable(LayoutDirection::LeftToRight);
let avoid = Rect::new(380.0, 280.0, 40.0, 40.0);
let bounds = placed(
OverlayPlacement::AtPointerAvoiding {
point: avoid.center(),
avoid,
},
Size::new(700.0, 520.0),
Rect::ZERO,
inset_viewport(),
LayoutDirection::LeftToRight,
);
assert_eq!(
bounds.x, ltr.x,
"clamped to the usable leading edge, not left at -324"
);
assert_eq!(
bounds.y, 50.0,
"and lifted off the usable bottom, not left at 324"
);
assert_eq!(
(bounds.right(), bounds.bottom()),
(ltr.right(), ltr.bottom())
);
assert!(
bounds.x >= ltr.x
&& bounds.right() <= ltr.right()
&& bounds.y >= ltr.y
&& bounds.bottom() <= ltr.bottom(),
"wholly inside the usable area: {bounds:?}"
);
assert!(
overlaps(bounds, avoid),
"at this size there is nothing that clears the contact: {bounds:?}"
);
}
#[test]
fn a_below_preferred_panel_shrinks_to_the_usable_room_not_the_window() {
let ltr = inset_usable(LayoutDirection::LeftToRight);
let flipped_up = placed(
OverlayPlacement::BelowPreferred,
Size::new(300.0, 400.0),
Rect::new(100.0, 400.0, 60.0, 24.0),
inset_viewport(),
LayoutDirection::LeftToRight,
);
assert_eq!(flipped_up.y, ltr.y, "pinned below the notch, not to y = 0");
assert_eq!(
flipped_up.height, 356.0,
"and cut to the room between the notch and the anchor"
);
assert_eq!(
flipped_up.bottom(),
396.0,
"which still leaves the anchor visible"
);
let dropped_down = placed(
OverlayPlacement::BelowPreferred,
Size::new(500.0, 500.0),
Rect::new(100.0, 100.0, 60.0, 24.0),
inset_viewport(),
LayoutDirection::LeftToRight,
);
assert_eq!(
dropped_down.y, 128.0,
"below the anchor, which is the roomier side"
);
assert_eq!(
dropped_down.height, 442.0,
"cut at the home indicator, not at the bottom of the window"
);
assert_eq!(dropped_down.bottom(), ltr.bottom());
}
#[test]
fn a_short_list_is_widened_to_the_control_that_opened_it() {
let combo = Rect::new(100.0, 200.0, 240.0, 24.0);
let window = (800.0, 600.0);
let at = |placement| {
placed(
placement,
Size::new(80.0, 120.0),
combo,
window,
LayoutDirection::LeftToRight,
)
};
for placement in [
OverlayPlacement::Below,
OverlayPlacement::Above,
OverlayPlacement::BelowPreferred,
] {
let bounds = at(placement.clone());
assert_eq!(
bounds.width, combo.width,
"{placement:?}: widened to the control, not left at 80"
);
assert_eq!(
bounds.x, combo.x,
"{placement:?}: and still on the control's leading edge"
);
assert_eq!(
bounds.right(),
combo.right(),
"{placement:?}: so it is flush at both"
);
}
let wide = placed(
OverlayPlacement::Below,
Size::new(420.0, 120.0),
combo,
window,
LayoutDirection::LeftToRight,
);
assert_eq!(wide.width, 420.0, "wider than the anchor, so untouched");
}
#[test]
fn an_rtl_panel_hangs_off_the_anchors_right_edge_not_its_left() {
let anchor = Rect::new(100.0, 200.0, 60.0, 24.0);
let panel = Size::new(300.0, 120.0);
let at = |direction| {
placed(
OverlayPlacement::Below,
panel,
anchor,
(800.0, 600.0),
direction,
)
};
assert_eq!(
at(LayoutDirection::RightToLeft).x,
0.0,
"aligned to the anchor's right edge (100 + 60 - 300 = -140), then \
pinned to the leading edge"
);
assert_eq!(
at(LayoutDirection::LeftToRight).x,
anchor.x,
"which is 100 dp from where the LTR arm puts it"
);
}
#[test]
fn an_rtl_submenu_opens_to_the_left_of_its_parent_row() {
let anchor = Rect::new(400.0, 200.0, 60.0, 24.0);
let submenu = Size::new(200.0, 100.0);
let at = |direction| {
placed(
OverlayPlacement::TrailingEdge,
submenu,
anchor,
(800.0, 600.0),
direction,
)
};
assert_eq!(
at(LayoutDirection::RightToLeft).x,
198.0,
"trailing is leftward in Arabic: 400 - 200 - 2"
);
assert_eq!(
at(LayoutDirection::LeftToRight).x,
462.0,
"and rightward in English: 400 + 60 + 2"
);
}
#[test]
fn a_menu_beside_the_contact_prefers_the_room_below_it() {
let avoid = Rect::new(400.0, 300.0, 40.0, 40.0);
let beside = |size| {
placed(
OverlayPlacement::AtPointerAvoiding {
point: avoid.center(),
avoid,
},
size,
Rect::ZERO,
(800.0, 600.0),
LayoutDirection::LeftToRight,
)
};
let fits_either = beside(Size::new(100.0, 100.0));
assert_eq!(
fits_either.x, 296.0,
"inline-start of the contact, which is what puts us in this branch"
);
assert_eq!(
fits_either.y, 344.0,
"below the contact, not at 196 above it"
);
let fits_neither = beside(Size::new(100.0, 560.0));
assert_eq!(fits_neither.x, 296.0, "still inline-start of the contact");
assert_eq!(
fits_neither.y, 40.0,
"lifted until it fits, not left at 344 with its foot at 904"
);
assert_eq!(fits_neither.bottom(), 600.0);
}
#[test]
fn a_menu_too_wide_to_sit_beside_the_contact_drops_below_it_and_stays_on_screen() {
let avoid = Rect::new(400.0, 300.0, 40.0, 40.0);
let bounds = placed(
OverlayPlacement::AtPointerAvoiding {
point: avoid.center(),
avoid,
},
Size::new(700.0, 100.0),
Rect::ZERO,
(800.0, 600.0),
LayoutDirection::LeftToRight,
);
assert_eq!(bounds.y, 344.0, "below the contact, not at 196 above it");
assert_eq!(bounds.x, 0.0, "clamped onto the screen, not left at -304");
assert_eq!(bounds.right(), 700.0);
assert!(
!overlaps(bounds, avoid),
"and the clamp did not cost the clearance: {bounds:?}"
);
}
#[test]
fn a_selection_toolbar_that_fits_on_neither_side_is_pinned_to_the_top() {
let toolbar = Size::new(200.0, 580.0);
let over = |selection, viewport: OverlayViewport| {
placed(
OverlayPlacement::AboveSelection { selection },
toolbar,
Rect::ZERO,
viewport,
LayoutDirection::LeftToRight,
)
};
let window = over(
Rect::new(300.0, 10.0, 100.0, 20.0),
OverlayViewport::from((800.0, 600.0)),
);
assert_eq!(window.y, 0.0, "pinned to the top, not left at -578");
let ltr = inset_usable(LayoutDirection::LeftToRight);
let inset = over(Rect::new(300.0, 60.0, 100.0, 20.0), inset_viewport());
assert_eq!(
inset.y, ltr.y,
"and the top it is pinned to is the usable one, under the notch"
);
}
#[test]
fn an_oversized_snackbar_is_shrunk_to_the_usable_area() {
let ltr = inset_usable(LayoutDirection::LeftToRight);
let bounds = placed(
OverlayPlacement::BottomCenter,
Size::new(760.0, 560.0),
Rect::ZERO,
inset_viewport(),
LayoutDirection::LeftToRight,
);
assert_eq!(
(bounds.width, bounds.height),
(ltr.width, ltr.height),
"shrunk to the usable area, not left at 760 x 560"
);
assert_eq!(
bounds.y, ltr.y,
"and floored at the top of that area, not lifted to -14"
);
assert_eq!(
bounds.x, ltr.x,
"and flush against its leading edge, not pulled back to 30"
);
assert_eq!(
bounds.right(),
ltr.right(),
"which is also what puts its far edge on the rounded corner"
);
assert!(
bounds.y >= ltr.y && bounds.bottom() <= ltr.bottom(),
"wholly inside the usable area: {bounds:?}"
);
}
}