use crate::{Insets, Rect, Vec2};
#[cfg(feature = "serde")]
use serde::{Deserialize, Serialize};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum CursorKind {
#[default]
Default,
Pointer,
Text,
Grab,
ResizeHorizontal,
ResizeVertical,
ResizeDiagonalDown,
ResizeDiagonalUp,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
#[cfg_attr(feature = "serde", serde(rename_all = "snake_case"))]
pub enum ResizeHandle {
Left,
Right,
Top,
Bottom,
TopLeft,
TopRight,
BottomLeft,
BottomRight,
}
impl ResizeHandle {
pub fn cursor(self) -> CursorKind {
match self {
Self::Left | Self::Right => CursorKind::ResizeHorizontal,
Self::Top | Self::Bottom => CursorKind::ResizeVertical,
Self::TopLeft | Self::BottomRight => CursorKind::ResizeDiagonalDown,
Self::TopRight | Self::BottomLeft => CursorKind::ResizeDiagonalUp,
}
}
}
#[derive(Clone, Debug, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct HitRegion {
pub id: String,
pub rect: Rect,
pub z_index: i16,
pub disabled: bool,
pub cursor: CursorKind,
}
impl HitRegion {
pub fn new(id: impl Into<String>, rect: Rect) -> Self {
Self {
id: id.into(),
rect,
z_index: 0,
disabled: false,
cursor: CursorKind::Default,
}
}
pub fn with_z_index(mut self, z_index: i16) -> Self {
self.z_index = z_index;
self
}
pub fn with_cursor(mut self, cursor: CursorKind) -> Self {
self.cursor = cursor;
self
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
pub fn contains(&self, point: Vec2) -> bool {
!self.disabled && self.rect.contains(point.x, point.y)
}
}
#[derive(Clone, Debug, Default, PartialEq)]
#[cfg_attr(feature = "serde", derive(Serialize, Deserialize))]
pub struct HitMap {
pub regions: Vec<HitRegion>,
}
impl HitMap {
pub fn new() -> Self {
Self::default()
}
pub fn push(&mut self, region: HitRegion) {
self.regions.push(region);
}
pub fn hit_test(&self, point: Vec2) -> Option<&HitRegion> {
self.regions
.iter()
.filter(|region| region.contains(point))
.max_by_key(|region| region.z_index)
}
pub fn all_at(&self, point: Vec2) -> Vec<&HitRegion> {
let mut hits = self
.regions
.iter()
.filter(|region| region.contains(point))
.collect::<Vec<_>>();
hits.sort_by_key(|region| region.z_index);
hits.reverse();
hits
}
}
pub fn local_point(rect: Rect, point: Vec2) -> Option<Vec2> {
rect.contains(point.x, point.y)
.then(|| Vec2::new(point.x - rect.x, point.y - rect.y))
}
pub fn hit_resize_handle(rect: Rect, point: Vec2, threshold: f32) -> Option<ResizeHandle> {
let outer = rect.inset(Insets {
top: -threshold,
right: -threshold,
bottom: -threshold,
left: -threshold,
});
if !outer.contains(point.x, point.y) {
return None;
}
let near_left = (point.x - rect.x).abs() <= threshold;
let near_right = (point.x - rect.right()).abs() <= threshold;
let near_top = (point.y - rect.y).abs() <= threshold;
let near_bottom = (point.y - rect.bottom()).abs() <= threshold;
match (near_left, near_right, near_top, near_bottom) {
(true, _, true, _) => Some(ResizeHandle::TopLeft),
(_, true, true, _) => Some(ResizeHandle::TopRight),
(true, _, _, true) => Some(ResizeHandle::BottomLeft),
(_, true, _, true) => Some(ResizeHandle::BottomRight),
(true, _, _, _) => Some(ResizeHandle::Left),
(_, true, _, _) => Some(ResizeHandle::Right),
(_, _, true, _) => Some(ResizeHandle::Top),
(_, _, _, true) => Some(ResizeHandle::Bottom),
_ => None,
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn hit_map_returns_topmost_enabled_region() {
let mut map = HitMap::new();
map.push(HitRegion::new("base", Rect::new(0.0, 0.0, 100.0, 100.0)));
map.push(HitRegion::new("top", Rect::new(20.0, 20.0, 40.0, 40.0)).with_z_index(10));
assert_eq!(map.hit_test(Vec2::new(24.0, 24.0)).unwrap().id, "top");
}
#[test]
fn resize_handle_detects_corners_and_edges() {
let rect = Rect::new(10.0, 20.0, 100.0, 80.0);
assert_eq!(
hit_resize_handle(rect, Vec2::new(11.0, 21.0), 4.0),
Some(ResizeHandle::TopLeft)
);
assert_eq!(
hit_resize_handle(rect, Vec2::new(110.0, 55.0), 4.0),
Some(ResizeHandle::Right)
);
assert_eq!(hit_resize_handle(rect, Vec2::new(60.0, 60.0), 4.0), None);
}
}