use crate::{Point, Rect, Scene};
#[derive(Debug, Clone, PartialEq)]
pub enum Selection {
Window(Rect),
Region(Rect),
}
#[derive(Debug, Clone, PartialEq)]
pub enum SelectionResult {
Selected(Selection),
Cancelled,
}
#[derive(Debug, Clone, PartialEq)]
enum State {
Waiting,
Hover {
pointer: Point,
target: Option<usize>,
},
Pressed {
origin: Point,
target: Option<usize>,
},
Dragging {
origin: Point,
pointer: Point,
},
}
pub struct Selector {
scene: Scene,
state: State,
drag_threshold_squared: f64,
}
impl Selector {
#[must_use]
pub fn new(scene: Scene, drag_threshold: f64) -> Self {
Self {
scene,
state: State::Waiting,
drag_threshold_squared: drag_threshold.powi(2),
}
}
#[must_use]
pub const fn scene(&self) -> &Scene {
&self.scene
}
#[must_use]
pub fn target_geometry(&self) -> Option<Rect> {
let target = match self.state {
State::Hover { target, .. } | State::Pressed { target, .. } => target,
State::Waiting | State::Dragging { .. } => None,
}?;
Some(self.scene.windows[target].geometry)
}
#[must_use]
pub fn region(&self) -> Option<Rect> {
let State::Dragging { origin, pointer } = self.state else {
return None;
};
Some(Rect::from_points(origin, pointer))
}
#[must_use]
pub fn select_fullscreen(&self) -> Option<SelectionResult> {
let pointer = match self.state {
State::Hover { pointer, .. } | State::Dragging { pointer, .. } => pointer,
State::Pressed { origin, .. } => origin,
State::Waiting => return None,
};
let geometry = self
.scene
.outputs
.iter()
.find(|output| output.logical_geometry.contains(pointer))?
.logical_geometry;
Some(SelectionResult::Selected(Selection::Region(geometry)))
}
pub fn pointer_moved(&mut self, pointer: Point) {
self.state = match self.state {
State::Waiting | State::Hover { .. } => State::Hover {
pointer,
target: self.scene.window_index_at(pointer),
},
State::Pressed { origin, .. }
if origin.distance_squared(pointer) >= self.drag_threshold_squared =>
{
State::Dragging { origin, pointer }
}
State::Pressed { origin, target } => State::Pressed { origin, target },
State::Dragging { origin, .. } => State::Dragging { origin, pointer },
};
}
pub const fn press(&mut self) {
if let State::Hover { pointer, target } = self.state {
self.state = State::Pressed {
origin: pointer,
target,
};
}
}
pub fn release(&mut self) -> Option<SelectionResult> {
let result = match self.state {
State::Pressed {
target: Some(target),
..
} => Selection::Window(self.scene.windows[target].geometry),
State::Dragging { origin, pointer } => {
let region = Rect::from_points(origin, pointer);
if region.width() == 0.0 || region.height() == 0.0 {
self.state = State::Hover {
pointer,
target: self.scene.window_index_at(pointer),
};
return None;
}
Selection::Region(region)
}
State::Pressed {
origin,
target: None,
} => {
self.state = State::Hover {
pointer: origin,
target: None,
};
return None;
}
State::Waiting | State::Hover { .. } => return None,
};
Some(SelectionResult::Selected(result))
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::test_support::{output, window};
fn scene() -> Scene {
Scene {
outputs: vec![output("DP-1", 1.0)],
windows: vec![window(Rect::new(100.0, 100.0, 800.0, 600.0))],
}
}
fn selector() -> Selector {
Selector::new(scene(), 4.0)
}
#[test]
fn click_selects_the_snapped_window() {
let mut selector = selector();
selector.pointer_moved(Point::new(200.0, 200.0));
selector.press();
assert_eq!(
selector.release(),
Some(SelectionResult::Selected(Selection::Window(Rect::new(
100.0, 100.0, 800.0, 600.0
))))
);
}
#[test]
fn dragging_past_the_threshold_selects_a_region() {
let mut selector = selector();
selector.pointer_moved(Point::new(200.0, 200.0));
selector.press();
selector.pointer_moved(Point::new(202.0, 203.0));
assert_eq!(selector.region(), None);
selector.pointer_moved(Point::new(150.0, 120.0));
assert_eq!(
selector.release(),
Some(SelectionResult::Selected(Selection::Region(Rect::new(
150.0, 120.0, 50.0, 80.0
))))
);
}
#[test]
fn fullscreen_selects_the_output_under_the_pointer() {
let mut left = output("left", 1.0);
left.logical_geometry = Rect::new(-1280.0, 100.0, 1280.0, 720.0);
let mut right = output("right", 1.0);
right.logical_geometry = Rect::new(0.0, 0.0, 1920.0, 1080.0);
let mut selector = Selector::new(
Scene {
outputs: vec![left, right],
windows: Vec::new(),
},
4.0,
);
selector.pointer_moved(Point::new(100.0, 100.0));
assert_eq!(
selector.select_fullscreen(),
Some(SelectionResult::Selected(Selection::Region(Rect::new(
0.0, 0.0, 1920.0, 1080.0
))))
);
}
#[test]
fn releasing_an_empty_click_does_not_start_a_late_drag() {
let mut selector = selector();
selector.pointer_moved(Point::new(1_000.0, 1_000.0));
selector.press();
assert_eq!(selector.release(), None);
selector.pointer_moved(Point::new(1_100.0, 1_000.0));
assert_eq!(selector.region(), None);
}
#[test]
fn a_line_is_not_a_region() {
let mut selector = selector();
selector.pointer_moved(Point::new(1_000.0, 1_000.0));
selector.press();
selector.pointer_moved(Point::new(1_100.0, 1_000.0));
assert_eq!(selector.release(), None);
assert_eq!(selector.region(), None);
}
#[test]
fn releasing_a_line_restores_the_window_target() {
let mut selector = selector();
selector.pointer_moved(Point::new(200.0, 200.0));
selector.press();
selector.pointer_moved(Point::new(300.0, 200.0));
assert_eq!(selector.target_geometry(), None);
assert_eq!(selector.release(), None);
assert_eq!(
selector.target_geometry(),
Some(Rect::new(100.0, 100.0, 800.0, 600.0))
);
}
}