rae 0.1.10

Renderer-neutral widget, layout, sanitization, and GLSL shader primitives for Rust desktop tools.
Documentation
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);
    }
}