use kui_core::{Rect, Size, Vec2};
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) struct Surface {
pub origin: (f64, f64),
pub scale: f64,
pub size: Size,
}
impl Surface {
pub fn to_screen(self, p: Vec2) -> (f64, f64) {
(
self.origin.0 + p.x as f64 * self.scale,
self.origin.1 + p.y as f64 * self.scale,
)
}
pub fn to_local(self, s: (f64, f64)) -> Vec2 {
Vec2::new(
((s.0 - self.origin.0) / self.scale) as f32,
((s.1 - self.origin.1) / self.scale) as f32,
)
}
pub fn contains(self, p: Vec2) -> bool {
Rect::from_pos_size(Vec2::ZERO, self.size).contains(p)
}
}
pub(crate) fn retarget(from: &Surface, p: Vec2, to: &Surface) -> Option<Vec2> {
let q = to.to_local(from.to_screen(p));
to.contains(q).then_some(q)
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub(crate) enum Landing {
Popup { index: usize, at: Vec2 },
Anchor,
Outside,
}
pub(crate) fn landing(owner: &Surface, p: Vec2, anchor: Rect, popups: &[Surface]) -> Landing {
let hit = popups.iter().enumerate().rev().find_map(|(index, popup)| {
retarget(owner, p, popup).map(|at| Landing::Popup { index, at })
});
if let Some(hit) = hit {
hit
} else if anchor.contains(p) {
Landing::Anchor
} else {
Landing::Outside
}
}
#[cfg(test)]
mod tests {
use super::*;
const ANCHOR: Rect = Rect {
x: 16.0,
y: 40.0,
w: 200.0,
h: 30.0,
};
const OWNER: Surface = Surface {
origin: (100.0, 100.0),
scale: 1.0,
size: Size { w: 360.0, h: 150.0 },
};
const POPUP: Surface = Surface {
origin: (116.0, 170.0),
scale: 1.0,
size: Size { w: 200.0, h: 300.0 },
};
fn close(a: Vec2, b: Vec2) -> bool {
(a.x - b.x).abs() < 1e-3 && (a.y - b.y).abs() < 1e-3
}
#[test]
fn a_point_below_the_field_is_a_point_in_the_popup() {
let q = retarget(&OWNER, Vec2::new(50.0, 100.0), &POPUP).unwrap();
assert!(close(q, Vec2::new(34.0, 30.0)), "{q:?}");
}
#[test]
fn a_point_above_the_popup_is_not_in_it() {
assert_eq!(retarget(&OWNER, Vec2::new(50.0, 20.0), &POPUP), None);
assert_eq!(retarget(&OWNER, Vec2::new(216.0, 100.0), &POPUP), None);
assert!(retarget(&OWNER, Vec2::new(215.9, 100.0), &POPUP).is_some());
}
#[test]
fn the_measured_walk_off_the_top_lands_on_the_anchor() {
for p in [
Vec2::new(93.0, -1.0),
Vec2::new(95.0, -5.0),
Vec2::new(97.0, -10.0),
] {
assert_eq!(
retarget(&POPUP, p, &OWNER).map(|q| ANCHOR.contains(q)),
Some(true),
"{p:?}"
);
}
}
#[test]
fn mixed_dpi_maps_through_physical_pixels() {
let owner = Surface {
origin: (0.0, 0.0),
scale: 2.0,
size: Size::new(400.0, 400.0),
};
let popup = Surface {
origin: (400.0, 600.0),
scale: 1.0,
size: Size::new(200.0, 300.0),
};
let q = retarget(&owner, Vec2::new(250.0, 350.0), &popup).unwrap();
assert!(close(q, Vec2::new(100.0, 100.0)), "{q:?}");
let r = retarget(&popup, q, &owner).unwrap();
assert!(close(r, Vec2::new(250.0, 350.0)), "{r:?}");
}
#[test]
fn on_macos_the_common_frame_is_points() {
let owner = Surface {
origin: (0.0, 0.0),
scale: 1.0,
size: Size::new(400.0, 400.0),
};
let popup = Surface {
origin: (200.0, 300.0),
scale: 1.0,
size: Size::new(200.0, 300.0),
};
let q = retarget(&owner, Vec2::new(250.0, 350.0), &popup).unwrap();
assert!(close(q, Vec2::new(50.0, 50.0)), "{q:?}");
let r = retarget(&popup, q, &owner).unwrap();
assert!(close(r, Vec2::new(250.0, 350.0)), "{r:?}");
}
#[test]
fn a_release_lands_in_one_of_three_places() {
let popups = [POPUP];
assert_eq!(
landing(&OWNER, Vec2::new(50.0, 100.0), ANCHOR, &popups),
Landing::Popup {
index: 0,
at: Vec2::new(34.0, 30.0)
}
);
assert_eq!(
landing(&OWNER, Vec2::new(100.0, 55.0), ANCHOR, &popups),
Landing::Anchor
);
assert_eq!(
landing(&OWNER, Vec2::new(300.0, 20.0), ANCHOR, &popups),
Landing::Outside
);
assert_eq!(
landing(&OWNER, Vec2::new(50.0, 100.0), ANCHOR, &[]),
Landing::Outside
);
}
#[test]
fn the_last_opened_popup_wins_where_two_overlap() {
let sub = Surface {
origin: (216.0, 200.0),
scale: 1.0,
size: Size::new(200.0, 100.0),
};
let popups = [POPUP, sub];
assert!(matches!(
landing(&OWNER, Vec2::new(150.0, 150.0), ANCHOR, &popups),
Landing::Popup { index: 1, .. }
));
assert!(matches!(
landing(&OWNER, Vec2::new(50.0, 150.0), ANCHOR, &popups),
Landing::Popup { index: 0, .. }
));
}
#[test]
fn a_popup_placed_over_its_field_still_wins_over_the_anchor() {
let over = Surface {
origin: (116.0, 130.0),
scale: 1.0,
size: Size::new(200.0, 300.0),
};
assert!(matches!(
landing(&OWNER, Vec2::new(100.0, 55.0), ANCHOR, &[over]),
Landing::Popup { index: 0, .. }
));
}
}