Shift-click extends the selection (Q#M5)
dispatch_pointer consults the mods it has carried since v5: a Down with SHIFT keeps the existing anchor (or, with no selection, anchors at the pre-click cursor) and only moves the cursor — the universal extend convention. Zero wire change. Frontend-side, a Shift-click neither advances nor inherits the multi-click chain, so two Shift-clicks can't become a word select. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
parent
aea08a58ec
commit
e8e494e1c1
|
|
@ -635,7 +635,8 @@ impl ApplicationHandler<AppEvent> for App {
|
|||
let Some(byte) = state.hit_test_source_byte(x, y) else {
|
||||
return;
|
||||
};
|
||||
let kind = state.classify_pointer_down(byte);
|
||||
let kind =
|
||||
state.classify_pointer_down(byte, mods.contains(Modifiers::SHIFT));
|
||||
state.pointer_drag_active = true;
|
||||
state.last_pointer_sent_byte = Some(byte);
|
||||
state.note_pointer_round_trip();
|
||||
|
|
@ -2070,7 +2071,14 @@ impl State {
|
|||
|
||||
/// Frontend-side double-click detection: a second Down at the
|
||||
/// same hit byte within the interval upgrades to `DoubleDown`.
|
||||
fn classify_pointer_down(&mut self, byte: u64) -> PointerKind {
|
||||
fn classify_pointer_down(&mut self, byte: u64, shift: bool) -> PointerKind {
|
||||
if shift {
|
||||
// Shift-click extends the selection (Q#M5); it neither
|
||||
// advances nor inherits the multi-click chain — two
|
||||
// Shift-clicks must not become a word select.
|
||||
self.last_pointer_down = None;
|
||||
return PointerKind::Down;
|
||||
}
|
||||
let now = std::time::Instant::now();
|
||||
let is_double = self.last_pointer_down.take().is_some_and(|(at, prev)| {
|
||||
prev == byte && now.duration_since(at) <= DOUBLE_CLICK_WINDOW
|
||||
|
|
|
|||
|
|
@ -787,7 +787,11 @@ impl EditorState {
|
|||
/// inline adornments, scroll), so no cell geometry is consulted.
|
||||
///
|
||||
/// * `Down` places the cursor and anchors a selection there
|
||||
/// (a following drag grows it).
|
||||
/// (a following drag grows it). With SHIFT it *extends*
|
||||
/// instead (Q#M5): the existing anchor — or, with no
|
||||
/// selection, the pre-click cursor — is kept and only the
|
||||
/// cursor moves, matching the universal Shift-click
|
||||
/// convention.
|
||||
/// * `Drag` moves the cursor; the anchor stays.
|
||||
/// * `Up` collapses an empty selection (a click without drag).
|
||||
/// * `DoubleDown` selects the word at the hit (frontend-side
|
||||
|
|
@ -795,15 +799,13 @@ impl EditorState {
|
|||
///
|
||||
/// The hit byte is clamped into the buffer and snapped back to a
|
||||
/// UTF-8 boundary: the frontend's hit may race an in-flight edit.
|
||||
/// `mods` is carried for future Shift-click extension and ignored
|
||||
/// today, matching `dispatch_mouse`.
|
||||
pub fn dispatch_pointer(
|
||||
&mut self,
|
||||
frontend_id: FrontendId,
|
||||
buffer_id: crate::buffer::BufferId,
|
||||
byte: u64,
|
||||
kind: crate::protocol::PointerKind,
|
||||
_mods: crate::protocol::Modifiers,
|
||||
mods: crate::protocol::Modifiers,
|
||||
) {
|
||||
use crate::protocol::PointerKind;
|
||||
let mut core = self.core.borrow_mut();
|
||||
|
|
@ -828,10 +830,19 @@ impl EditorState {
|
|||
};
|
||||
match kind {
|
||||
PointerKind::Down => {
|
||||
let prev_cursor = core.active_window().cursor;
|
||||
let extending = mods.contains(crate::protocol::Modifiers::SHIFT);
|
||||
let keep_anchor = extending && core.active_window().selection.is_some();
|
||||
let aw = core.active_window_mut();
|
||||
aw.cursor = byte;
|
||||
aw.goal_col = None;
|
||||
core.begin_selection(byte);
|
||||
if extending {
|
||||
if !keep_anchor {
|
||||
core.begin_selection(prev_cursor);
|
||||
}
|
||||
} else {
|
||||
core.begin_selection(byte);
|
||||
}
|
||||
}
|
||||
PointerKind::Drag => {
|
||||
let aw = core.active_window_mut();
|
||||
|
|
@ -4441,6 +4452,45 @@ mod tests {
|
|||
assert_eq!(s.core.borrow().cursor(), 15, "mismatched buffer ignored");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn dispatch_pointer_shift_down_extends_instead_of_restarting() {
|
||||
use crate::protocol::{Modifiers as WireMods, PointerKind};
|
||||
let mut s = fresh_with(b"hello world\n");
|
||||
let bid = s.core.borrow().active_buffer_id();
|
||||
let none = WireMods::NONE;
|
||||
let shift = WireMods::SHIFT;
|
||||
|
||||
// No selection, cursor parked at 2: Shift-Down anchors at the
|
||||
// pre-click cursor and moves to the hit (Q#M5).
|
||||
s.dispatch_pointer(FrontendId::LOCAL, bid, 2, PointerKind::Down, none);
|
||||
s.dispatch_pointer(FrontendId::LOCAL, bid, 2, PointerKind::Up, none);
|
||||
assert!(s.core.borrow().active_region().is_none());
|
||||
s.dispatch_pointer(FrontendId::LOCAL, bid, 7, PointerKind::Down, shift);
|
||||
assert_eq!(s.core.borrow().active_region(), Some((2, 7)));
|
||||
// The Up after a Shift-click must not collapse the region
|
||||
// (anchor ≠ cursor).
|
||||
s.dispatch_pointer(FrontendId::LOCAL, bid, 7, PointerKind::Up, shift);
|
||||
assert_eq!(s.core.borrow().active_region(), Some((2, 7)));
|
||||
|
||||
// With a live selection, Shift-Down keeps the anchor — even
|
||||
// extending in the other direction.
|
||||
s.dispatch_pointer(FrontendId::LOCAL, bid, 0, PointerKind::Down, shift);
|
||||
assert_eq!(
|
||||
s.core.borrow().active_region(),
|
||||
Some((0, 2)),
|
||||
"anchor 2 kept; cursor crossed to the other side"
|
||||
);
|
||||
|
||||
// A drag after a Shift-Down grows from the inherited anchor.
|
||||
s.dispatch_pointer(FrontendId::LOCAL, bid, 9, PointerKind::Drag, shift);
|
||||
assert_eq!(s.core.borrow().active_region(), Some((2, 9)));
|
||||
|
||||
// A plain Down restarts the anchor as before.
|
||||
s.dispatch_pointer(FrontendId::LOCAL, bid, 4, PointerKind::Down, none);
|
||||
s.dispatch_pointer(FrontendId::LOCAL, bid, 4, PointerKind::Up, none);
|
||||
assert!(s.core.borrow().active_region().is_none());
|
||||
}
|
||||
|
||||
/// Acceptance bullet 3: mouse events are coalesced at frame
|
||||
/// boundaries — many drag events between renders all apply, and
|
||||
/// the cursor ends up at the last position.
|
||||
|
|
|
|||
Loading…
Reference in New Issue