use crate::utils::time::now_ms;
use std::{
collections::VecDeque,
sync::{Mutex, MutexGuard, OnceLock},
};
pub const FRAME_KEEP: usize = 32;
pub const DRAG_ANCHOR_TTL_MS: u64 = 60_000;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct Rect {
pub x: i32,
pub y: i32,
pub width: u32,
pub height: u32,
}
impl Rect {
pub fn right(&self) -> i32 {
self.x.saturating_add(self.width as i32)
}
pub fn bottom(&self) -> i32 {
self.y.saturating_add(self.height as i32)
}
pub fn contains(&self, x: i32, y: i32) -> bool {
x >= self.x && y >= self.y && x < self.right() && y < self.bottom()
}
}
#[derive(Debug, Clone)]
pub struct Frame {
pub id: String,
pub origin: (i32, i32),
pub screen: (u32, u32),
pub image: (u32, u32),
pub scale: f64,
pub window: Option<u64>,
pub window_rect: Option<Rect>,
pub created_ms: u64,
}
impl Frame {
pub fn project(&self, x: f64, y: f64) -> Result<(i32, i32), String> {
if !(x >= 0.0 && y >= 0.0 && x <= self.image.0 as f64 && y <= self.image.1 as f64) {
return Err(format!(
"the point ({x}, {y}) is outside frame {} ({}x{} px); coordinates passed with from_frame are pixels *in that image*",
self.id, self.image.0, self.image.1
));
}
let scale = if self.scale > 0.0 { self.scale } else { 1.0 };
let sx = self.origin.0 as f64 + x / scale;
let sy = self.origin.1 as f64 + y / scale;
Ok((sx.round() as i32, sy.round() as i32))
}
}
#[derive(Debug, Clone)]
pub struct DragAnchor {
pub handle: String,
pub end: (i32, i32),
pub at_ms: u64,
}
#[derive(Debug, Default)]
pub struct State {
pub frames: VecDeque<Frame>,
pub frame_seq: u64,
pub drag_anchor: Option<DragAnchor>,
pub drag_seq: u64,
pub held_keys: Vec<String>,
pub held_buttons: Vec<String>,
}
fn state() -> &'static Mutex<State> {
static STATE: OnceLock<Mutex<State>> = OnceLock::new();
STATE.get_or_init(|| Mutex::new(State::default()))
}
pub fn lock() -> MutexGuard<'static, State> {
state().lock().unwrap_or_else(|e| e.into_inner())
}
pub fn reset() {
let mut st = lock();
st.frames.clear();
st.drag_anchor = None;
st.held_keys.clear();
st.held_buttons.clear();
}
#[must_use]
pub fn register_frame(
origin: (i32, i32),
screen: (u32, u32),
image: (u32, u32),
window: Option<(u64, Rect)>,
) -> String {
let mut st = lock();
st.frame_seq += 1;
let id = format!("f{}", st.frame_seq);
let (window_id, window_rect) = match window {
Some((id, rect)) => (Some(id), Some(rect)),
None => (None, None),
};
let scale = if screen.0 == 0 || screen.1 == 0 {
1.0
} else {
image.0 as f64 / screen.0 as f64
};
st.frames.push_back(Frame {
id: id.clone(),
origin,
screen,
image,
scale,
window: window_id,
window_rect,
created_ms: now_ms(),
});
while st.frames.len() > FRAME_KEEP {
st.frames.pop_front();
}
id
}
pub fn frame(id: &str) -> Option<Frame> {
lock().frames.iter().find(|f| f.id == id).cloned()
}
#[must_use]
pub fn remember_drag(end: (i32, i32)) -> String {
let mut st = lock();
st.drag_seq += 1;
let handle = format!("d{}", st.drag_seq);
st.drag_anchor = Some(DragAnchor {
handle: handle.clone(),
end,
at_ms: now_ms(),
});
handle
}
pub fn drag_anchor(handle: Option<&str>) -> Result<(i32, i32), String> {
let st = lock();
let Some(anchor) = st.drag_anchor.as_ref() else {
return Err(
"there is no previous drag in this session; pass from_x/from_y (or path)".to_string(),
);
};
if let Some(want) = handle
&& want != anchor.handle
{
return Err(format!(
"drag handle {want} is not the most recent one ({}); the anchor only survives until the next drag",
anchor.handle
));
}
if now_ms().saturating_sub(anchor.at_ms) > DRAG_ANCHOR_TTL_MS {
return Err(format!(
"drag handle {} has expired; pass from_x/from_y again",
anchor.handle
));
}
Ok(anchor.end)
}
#[cfg(test)]
mod tests {
use super::*;
fn make_frame(origin: (i32, i32), screen: (u32, u32), image: (u32, u32)) -> Frame {
Frame {
id: "f1".to_string(),
origin,
screen,
image,
scale: image.0 as f64 / screen.0 as f64,
window: None,
window_rect: None,
created_ms: now_ms(),
}
}
#[test]
fn project_is_identity_at_scale_one() {
let f = make_frame((100, 50), (800, 600), (800, 600));
assert_eq!(f.project(0.0, 0.0).unwrap(), (100, 50));
assert_eq!(f.project(799.0, 599.0).unwrap(), (899, 649));
}
#[test]
fn project_divides_by_the_delivered_scale() {
let f = make_frame((0, 0), (800, 600), (400, 300));
assert_eq!(f.project(200.0, 150.0).unwrap(), (400, 300));
}
#[test]
fn project_rejects_points_outside_the_image() {
let f = make_frame((0, 0), (800, 600), (800, 600));
assert!(f.project(801.0, 10.0).is_err());
assert!(f.project(-1.0, 10.0).is_err());
}
#[test]
fn frames_are_kept_and_evicted() {
for _ in 0..(FRAME_KEEP + 3) {
let _ = register_frame((0, 0), (10, 10), (10, 10), None);
}
let st = lock();
assert_eq!(st.frames.len(), FRAME_KEEP);
let first = st.frames.front().unwrap().id.clone();
drop(st);
assert!(frame(&first).is_some());
}
}