Skip to main content

qframe/widgets/
click.rs

1//! How many clicks open a row, and telling a double click from two clicks.
2//!
3//! A terminal reports presses and releases, never clicks, so a widget counts them itself: a
4//! second press on the same row soon after the first is a double click. A press that turns into
5//! a drag, or one with Ctrl or Shift held, starts nothing, so a drag followed by a click is never
6//! read as a double click.
7
8use std::time::Duration;
9
10use crate::runtime::MULTI_PRESS;
11
12/// How many clicks open a row of a [`Tree`](super::Tree), a [`Table`](super::Table) or a
13/// [`CardGrid`](super::CardGrid).
14///
15/// A list of choices opens what is clicked at once; a file explorer selects with one click and
16/// opens with two, so a click can start a drag or a selection without opening anything.
17#[derive(Debug, Clone, Copy, PartialEq, Eq)]
18pub enum Click {
19    /// One click selects a row and opens it.
20    Single,
21    /// One click only selects a row; a second press on the same row within
22    /// [`Click::INTERVAL`] opens it. Enter still opens the selected row.
23    Double,
24}
25
26impl Click {
27    /// Two presses on the same row closer together than this are a double click: 400 ms, the
28    /// interval desktops start with.
29    pub const INTERVAL: Duration = MULTI_PRESS;
30}
31
32/// Whether a press at `now` follows one at `last` closely enough to make a double click.
33pub(crate) fn is_double(last: Duration, now: Duration) -> bool {
34    now.saturating_sub(last) < Click::INTERVAL
35}
36
37/// The last press on a row, kept in a widget's memory to tell a double click.
38#[derive(Debug)]
39pub(crate) struct LastPress<K> {
40    last: Option<(K, Duration)>,
41}
42
43impl<K> Default for LastPress<K> {
44    fn default() -> Self {
45        Self { last: None }
46    }
47}
48
49impl<K: PartialEq> LastPress<K> {
50    /// Counts a press on `row` at `now`. True when it makes a double click with the press before
51    /// it, which is then used up, so a third press starts over rather than opening again.
52    pub(crate) fn press(&mut self, row: K, now: Duration) -> bool {
53        let double = self.last.as_ref().is_some_and(|(last, at)| *last == row && is_double(*at, now));
54        self.last = if double { None } else { Some((row, now)) };
55        double
56    }
57
58    /// Forgets the last press: it became a drag or a modified click, which start no double click.
59    pub(crate) fn forget(&mut self) {
60        self.last = None;
61    }
62}
63
64#[cfg(test)]
65mod tests {
66    use super::*;
67
68    fn ms(millis: u64) -> Duration {
69        Duration::from_millis(millis)
70    }
71
72    #[test]
73    fn two_presses_on_one_row_within_the_interval_are_a_double_click_and_a_third_starts_over() {
74        let mut presses = LastPress::default();
75        assert!(!presses.press(3, ms(1_000)));
76        assert!(presses.press(3, ms(1_399)), "399 ms later is a double click");
77        assert!(!presses.press(3, ms(1_450)), "a third press starts a new count");
78        assert!(presses.press(3, ms(1_500)));
79    }
80
81    #[test]
82    fn a_slow_second_press_another_row_or_a_drag_between_are_not_a_double_click() {
83        let mut presses = LastPress::default();
84        assert!(!presses.press(3, ms(0)));
85        assert!(!presses.press(3, ms(400)), "the interval itself is too slow");
86        assert!(!presses.press(4, ms(500)), "another row");
87        presses.forget();
88        assert!(!presses.press(4, ms(600)), "the press before became a drag");
89    }
90}