use std::time::{Duration, Instant};
const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(500);
pub(crate) type ContentCell = (u16, usize);
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub(crate) struct ContentRange {
pub start: ContentCell,
pub end: ContentCell,
}
impl ContentRange {
fn between(a: ContentCell, b: ContentCell) -> Self {
let (start, end) = if (a.1, a.0) <= (b.1, b.0) {
(a, b)
} else {
(b, a)
};
ContentRange { start, end }
}
}
#[derive(Clone, Copy, Debug, Default)]
pub(crate) struct TranscriptSelection {
anchor: ContentCell,
cursor: ContentCell,
pressed: bool,
selecting: bool,
last_click: Option<(ContentCell, Instant)>,
pending_word: Option<ContentCell>,
}
impl TranscriptSelection {
pub fn new() -> Self {
Self::default()
}
pub fn press(&mut self, cell: ContentCell) -> bool {
self.anchor = cell;
self.cursor = cell;
self.pressed = true;
let had = self.selecting;
self.selecting = false;
had
}
pub fn drag(&mut self, cell: ContentCell) -> bool {
if !self.pressed {
return false;
}
self.cursor = cell;
self.selecting = self.cursor != self.anchor;
self.last_click = None;
self.pending_word = None;
true
}
pub fn release(&mut self, cell: ContentCell) -> bool {
if !self.pressed {
return false;
}
self.cursor = cell;
self.pressed = false;
self.selecting = self.cursor != self.anchor;
if self.selecting {
self.last_click = None;
} else {
let now = Instant::now();
let is_double = self.last_click.is_some_and(|(previous, at)| {
previous == cell && now.duration_since(at) <= DOUBLE_CLICK_INTERVAL
});
self.last_click = Some((cell, now));
self.pending_word = is_double.then_some(cell);
}
true
}
pub fn take_pending_word(&mut self) -> Option<ContentCell> {
self.pending_word.take()
}
pub fn select_word(&mut self, range: ContentRange) {
self.anchor = range.start;
self.cursor = range.end;
self.selecting = true;
}
pub fn clear(&mut self) {
*self = Self::default();
}
pub fn is_pressed(&self) -> bool {
self.pressed
}
pub fn is_active(&self) -> bool {
self.selecting
}
pub fn range(&self) -> Option<ContentRange> {
self.selecting
.then(|| ContentRange::between(self.anchor, self.cursor))
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn drag_across_rows_selects_in_reading_order() {
let mut sel = TranscriptSelection::new();
assert!(!sel.press((5, 40))); assert!(sel.drag((2, 3))); assert!(sel.release((2, 3)));
let range = sel.range().expect("a drag selects");
assert_eq!(range.start, (2, 3));
assert_eq!(range.end, (5, 40));
assert!(sel.is_active());
}
#[test]
fn selection_survives_a_conceptual_scroll() {
let mut sel = TranscriptSelection::new();
sel.press((0, 100));
sel.drag((10, 250));
let range = sel.range().expect("selection");
assert_eq!(range.start, (0, 100));
assert_eq!(range.end, (10, 250));
}
#[test]
fn plain_click_leaves_no_selection() {
let mut sel = TranscriptSelection::new();
sel.press((3, 7));
sel.release((3, 7));
assert!(sel.range().is_none());
assert!(!sel.is_active());
}
#[test]
fn a_new_press_clears_the_previous_selection() {
let mut sel = TranscriptSelection::new();
sel.press((0, 0));
sel.drag((3, 2));
sel.release((3, 2));
assert!(sel.range().is_some());
assert!(sel.press((1, 5)));
assert!(sel.range().is_none());
}
#[test]
fn double_click_queues_a_pending_word() {
let mut sel = TranscriptSelection::new();
sel.press((9, 4));
sel.release((9, 4));
sel.press((9, 4));
assert!(sel.release((9, 4)));
assert_eq!(sel.take_pending_word(), Some((9, 4)));
sel.select_word(ContentRange {
start: (7, 4),
end: (17, 4),
});
let range = sel.range().expect("word selection");
assert_eq!(range.start, (7, 4));
assert_eq!(range.end, (17, 4));
}
}