use std::time::{Duration, Instant};
use super::zones;
use crate::input::{Bounds, CaptureControl, DeadMan};
pub(super) const TAKE_WINDOW: Duration = Duration::from_secs(1);
#[derive(Debug, Clone, Copy, PartialEq)]
pub(super) struct ReleaseOrder {
pub(super) activation_id: u32,
pub(super) cursor: Option<(f64, f64)>,
}
#[derive(Debug)]
struct Activation {
id: u32,
origin: (i32, i32),
position: (i32, i32),
since: Instant,
reach: i32,
crossed: bool,
left_zones: bool,
}
#[derive(Debug)]
pub(super) struct CaptureFlow {
zones: Vec<Bounds>,
deadman: DeadMan,
grab_wanted: bool,
active: Option<Activation>,
}
impl CaptureFlow {
pub(super) fn new(zones: Vec<Bounds>, now: Instant) -> Self {
Self {
zones,
deadman: DeadMan::new(now),
grab_wanted: false,
active: None,
}
}
pub(super) fn set_zones(&mut self, zones: Vec<Bounds>) {
self.zones = zones;
}
pub(super) fn is_active(&self) -> bool {
self.active.is_some()
}
pub(super) fn activate(&mut self, id: u32, cursor: (f64, f64), now: Instant) {
let position = (round_to_i32(cursor.0), round_to_i32(cursor.1));
let origin = zones::nearest_inside(&self.zones, position.0, position.1).unwrap_or(position);
self.active = Some(Activation {
id,
origin,
position,
since: now,
reach: distance(origin, position),
crossed: self.grab_wanted,
left_zones: !zones::contains(&self.zones, position.0, position.1),
});
}
pub(super) fn deactivate(&mut self, id: u32) {
if self.active.as_ref().is_some_and(|active| active.id == id) {
self.active = None;
}
}
pub(super) fn motion(&mut self, dx: i32, dy: i32, now: Instant) -> Option<(i32, i32)> {
let active = self.active.as_mut()?;
active.position = (
active.position.0.saturating_add(dx),
active.position.1.saturating_add(dy),
);
let reach = distance(active.origin, active.position);
if reach > active.reach {
active.reach = reach;
active.since = now;
}
let inside = zones::contains(&self.zones, active.position.0, active.position.1);
active.left_zones |= !inside;
Some(active.position)
}
pub(super) fn control(
&mut self,
command: CaptureControl,
now: Instant,
) -> Option<ReleaseOrder> {
match command {
CaptureControl::Keepalive => self.deadman.keepalive(now),
CaptureControl::Grab(true) => {
self.deadman.keepalive(now);
self.grab_wanted = true;
if let Some(active) = &mut self.active {
active.crossed = true;
}
}
CaptureControl::Grab(false) => {
self.grab_wanted = false;
return self.release();
}
}
None
}
pub(super) fn poll(&mut self, now: Instant) -> Option<ReleaseOrder> {
let active = self.active.as_ref()?;
let done = if active.crossed {
self.deadman.expired(now)
} else {
let waited = now.saturating_duration_since(active.since) >= TAKE_WINDOW;
let moved_back = active.left_zones
&& zones::contains(&self.zones, active.position.0, active.position.1);
waited || moved_back
};
if done { self.release() } else { None }
}
pub(super) fn release(&mut self) -> Option<ReleaseOrder> {
let active = self.active.take()?;
let cursor =
(!active.crossed).then(|| (f64::from(active.origin.0), f64::from(active.origin.1)));
Some(ReleaseOrder {
activation_id: active.id,
cursor,
})
}
}
fn distance(from: (i32, i32), to: (i32, i32)) -> i32 {
from.0
.abs_diff(to.0)
.max(from.1.abs_diff(to.1))
.try_into()
.unwrap_or(i32::MAX)
}
#[allow(clippy::cast_possible_truncation, reason = "clamped to the i32 range")]
fn round_to_i32(value: f64) -> i32 {
if value.is_nan() {
return 0;
}
value
.round()
.clamp(f64::from(i32::MIN), f64::from(i32::MAX)) as i32
}
#[cfg(test)]
mod tests {
use super::*;
use crate::input::{DEADMAN_TIMEOUT, KEEPALIVE_INTERVAL};
const ACTIVATION: u32 = 7;
fn zones() -> Vec<Bounds> {
vec![Bounds {
x: 0,
y: 0,
width: 1920,
height: 1080,
}]
}
fn pushed_flow(now: Instant) -> CaptureFlow {
let mut flow = CaptureFlow::new(zones(), now);
flow.activate(ACTIVATION, (1925.0, 500.0), now);
flow
}
#[test]
fn control_grab_off_before_take_releases_at_activation_point() {
let now = Instant::now();
let mut flow = pushed_flow(now);
let order = flow.control(CaptureControl::Grab(false), now);
assert_eq!(
order,
Some(ReleaseOrder {
activation_id: ACTIVATION,
cursor: Some((1919.0, 500.0))
})
);
assert!(!flow.is_active());
}
#[test]
fn control_grab_off_after_take_releases_without_cursor() {
let now = Instant::now();
let mut flow = pushed_flow(now);
flow.control(CaptureControl::Grab(true), now);
let order = flow.control(CaptureControl::Grab(false), now);
assert_eq!(
order,
Some(ReleaseOrder {
activation_id: ACTIVATION,
cursor: None
})
);
}
#[test]
fn poll_no_take_within_window_releases_at_activation_point() {
let now = Instant::now();
let mut flow = pushed_flow(now);
let early = flow.poll(now + TAKE_WINDOW.saturating_sub(Duration::from_millis(1)));
let late = flow.poll(now + TAKE_WINDOW);
assert_eq!(early, None);
assert_eq!(
late,
Some(ReleaseOrder {
activation_id: ACTIVATION,
cursor: Some((1919.0, 500.0))
})
);
}
#[test]
fn poll_push_still_getting_further_keeps_capturing_past_the_window() {
let now = Instant::now();
let mut flow = pushed_flow(now);
let later = now + TAKE_WINDOW;
flow.motion(40, 0, later);
let order = flow.poll(later + TAKE_WINDOW.saturating_sub(Duration::from_millis(1)));
assert_eq!(order, None);
}
#[test]
fn poll_pointer_moved_back_inside_before_take_releases() {
let now = Instant::now();
let mut flow = pushed_flow(now);
flow.motion(10, 0, now);
let still_out = flow.poll(now);
flow.motion(-40, 0, now);
let back = flow.poll(now);
assert_eq!(still_out, None);
assert_eq!(
back,
Some(ReleaseOrder {
activation_id: ACTIVATION,
cursor: Some((1919.0, 500.0))
})
);
}
#[test]
fn poll_activation_on_last_pixel_does_not_count_as_moved_back() {
let now = Instant::now();
let mut flow = CaptureFlow::new(zones(), now);
flow.activate(ACTIVATION, (1919.0, 500.0), now);
flow.motion(0, 3, now);
let order = flow.poll(now);
assert_eq!(order, None);
}
#[test]
fn poll_silent_daemon_during_take_releases_at_deadman_timeout() {
let now = Instant::now();
let mut flow = pushed_flow(now);
flow.control(CaptureControl::Grab(true), now);
flow.control(CaptureControl::Keepalive, now + KEEPALIVE_INTERVAL);
let alive = flow.poll(
now + KEEPALIVE_INTERVAL + DEADMAN_TIMEOUT.saturating_sub(Duration::from_millis(1)),
);
let dead = flow.poll(now + KEEPALIVE_INTERVAL + DEADMAN_TIMEOUT);
assert_eq!(alive, None);
assert_eq!(
dead,
Some(ReleaseOrder {
activation_id: ACTIVATION,
cursor: None
})
);
}
#[test]
fn poll_take_within_window_keeps_capturing_past_the_window() {
let now = Instant::now();
let mut flow = pushed_flow(now);
let later = now + TAKE_WINDOW * 3;
flow.control(CaptureControl::Grab(true), now + Duration::from_millis(300));
flow.control(CaptureControl::Keepalive, later);
flow.motion(-100, 0, now);
let order = flow.poll(later);
assert_eq!(order, None);
assert!(flow.is_active());
}
#[test]
fn activate_grab_already_wanted_is_a_crossing_from_the_start() {
let now = Instant::now();
let mut flow = CaptureFlow::new(zones(), now);
flow.control(CaptureControl::Grab(true), now);
flow.activate(ACTIVATION, (1925.0, 500.0), now);
let after_window = flow.poll(now + TAKE_WINDOW);
assert_eq!(after_window, None);
}
#[test]
fn motion_without_activation_is_ignored() {
let now = Instant::now();
let mut flow = CaptureFlow::new(zones(), now);
let position = flow.motion(5, 5, now);
assert_eq!(position, None);
}
}