use std::sync::Arc;
use std::time::{Duration, Instant};
use crate::elements::text_edit::ops::reset_blink;
use crate::elements::text_edit::word::word_range_at;
use crate::event::{EventCtx, MouseEvent, MouseHandler};
use super::layout::byte_at_point;
const DOUBLE_CLICK_INTERVAL: Duration = Duration::from_millis(500);
const DOUBLE_CLICK_DISTANCE: f32 = 4.0;
fn next_click_count(
prev_time: Option<Instant>,
prev_pos: (f32, f32),
prev_count: u8,
now: Instant,
pos: (f32, f32),
) -> u8 {
let in_time = prev_time.is_some_and(|t| now.duration_since(t) <= DOUBLE_CLICK_INTERVAL);
let dx = pos.0 - prev_pos.0;
let dy = pos.1 - prev_pos.1;
let in_dist = dx * dx + dy * dy <= DOUBLE_CLICK_DISTANCE * DOUBLE_CLICK_DISTANCE;
if in_time && in_dist {
match prev_count {
1 => 2,
2 => 3,
_ => 1, }
} else {
1
}
}
pub(super) fn build_mouse_down_handler() -> MouseHandler {
Arc::new(move |ev: &MouseEvent, cx: &mut EventCtx| {
let Some(id) = cx.element_id() else { return };
let Some(state_rc) = cx.ime_state(id) else {
return;
};
{
let mut state = state_rc.borrow_mut();
if state.preedit.is_some() {
return;
}
let Some(layout) = state.last_layout.clone() else {
return;
};
let byte = byte_at_point(
&layout,
&state.text,
state.paint_origin_x,
state.paint_origin_y,
ev.position.0,
ev.position.1,
);
debug_assert!(
state.text.is_char_boundary(byte),
"byte_at_point must return a char boundary"
);
let count = next_click_count(
state.last_click_time,
state.last_click_pos,
state.click_count,
ev.timestamp,
ev.position,
);
state.last_click_time = Some(ev.timestamp);
state.last_click_pos = ev.position;
state.click_count = count;
let (anchor, caret) = match count {
2 => {
let r = word_range_at(&state.text, byte);
(r.start, r.end)
}
3 => match layout.lines.get(layout.line_for_byte(byte)) {
Some(l) => (l.byte_start, l.byte_end),
None => (byte, byte),
},
_ => (byte, byte),
};
state.caret = caret;
state.caret_affinity = slate_text::Affinity::Downstream;
state.selection_anchor = Some(anchor);
state.dragging = true;
state.desired_x = None;
reset_blink(&mut state);
state.undo.mark_motion();
}
cx.stop_propagation();
})
}
pub(super) fn build_mouse_move_handler() -> MouseHandler {
Arc::new(move |ev: &MouseEvent, cx: &mut EventCtx| {
let Some(id) = cx.element_id() else { return };
let Some(state_rc) = cx.ime_state(id) else {
return;
};
let mut state = state_rc.borrow_mut();
if !state.dragging || state.preedit.is_some() {
return;
}
let Some(layout) = state.last_layout.clone() else {
return;
};
let byte = byte_at_point(
&layout,
&state.text,
state.paint_origin_x,
state.paint_origin_y,
ev.position.0,
ev.position.1,
);
debug_assert!(
state.text.is_char_boundary(byte),
"byte_at_point must return a char boundary"
);
state.caret = byte;
state.caret_affinity = slate_text::Affinity::Downstream;
reset_blink(&mut state);
drop(state);
cx.stop_propagation();
})
}
pub(super) fn build_mouse_up_handler() -> MouseHandler {
Arc::new(move |_ev: &MouseEvent, cx: &mut EventCtx| {
let Some(id) = cx.element_id() else { return };
let Some(state_rc) = cx.ime_state(id) else {
return;
};
{
let mut state = state_rc.borrow_mut();
state.dragging = false;
if state.selection_anchor == Some(state.caret) {
state.selection_anchor = None;
}
}
cx.stop_propagation();
})
}
#[cfg(test)]
mod tests {
use super::*;
const POS: (f32, f32) = (50.0, 20.0);
#[test]
fn first_click_is_single() {
assert_eq!(
next_click_count(None, (0.0, 0.0), 0, Instant::now(), POS),
1
);
}
#[test]
fn two_close_clicks_make_a_double_then_triple() {
let t0 = Instant::now();
let t1 = t0 + Duration::from_millis(100);
let t2 = t1 + Duration::from_millis(100);
assert_eq!(next_click_count(Some(t0), POS, 1, t1, POS), 2);
assert_eq!(next_click_count(Some(t1), POS, 2, t2, POS), 3);
}
#[test]
fn fourth_click_wraps_to_single() {
let t0 = Instant::now();
let t1 = t0 + Duration::from_millis(100);
assert_eq!(next_click_count(Some(t0), POS, 3, t1, POS), 1);
}
#[test]
fn slow_second_click_resets_to_single() {
let t0 = Instant::now();
let t_slow = t0 + Duration::from_millis(600); assert_eq!(next_click_count(Some(t0), POS, 1, t_slow, POS), 1);
}
#[test]
fn far_second_click_resets_to_single() {
let t0 = Instant::now();
let t1 = t0 + Duration::from_millis(100);
let far = (POS.0 + 10.0, POS.1); assert_eq!(next_click_count(Some(t0), POS, 1, t1, far), 1);
}
#[test]
fn small_jitter_within_tolerance_still_doubles() {
let t0 = Instant::now();
let t1 = t0 + Duration::from_millis(100);
let jittered = (POS.0 + 3.0, POS.1);
assert_eq!(next_click_count(Some(t0), POS, 1, t1, jittered), 2);
}
}