Skip to content

Commit 12054f5

Browse files
ylinwindclaude
andauthored
base: Stop drag auto scroll when the content mask collapses (longbridge#2902)
## Summary - Fix a crash in `WindowSelectionState::update_auto_scroll`: drag-selection auto scroll can scroll the anchor's clipping viewport away, and once the registered content mask collapses below 2px, clamping the drag position into it panics (`Ord::clamp`: `assertion failed: min <= max`). - Stop the anchor auto scroll when the mask is narrower than twice the hit-test inset (extracted as `HIT_TEST_INSET`) — there is nothing left to scroll. - Add a regression test: begin a drag on a normal registration, re-register with a collapsed content mask, then drive `update_in_window` past the viewport edge. ## Test plan - [x] `drag_auto_scroll_stops_when_the_content_mask_collapses` panics before the fix and passes after. - [x] `cargo test -p gpui-base --lib text_selection` — all 48 tests pass. - [x] `cargo run -p gpui-base --example components text-selection` — drag-selection auto scroll still behaves normally. 🤖 Generated with [Claude Code](https://claude.com/claude-code) --------- Co-authored-by: Claude Fable 5 <noreply@anthropic.com>
1 parent a3f7bb2 commit 12054f5

1 file changed

Lines changed: 52 additions & 4 deletions

File tree

‎crates/base/src/text_selection.rs‎

Lines changed: 52 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1472,6 +1472,15 @@ impl WindowSelectionState {
14721472
// moves, so selection keeps scrolling the same related region even
14731473
// after the anchor text has moved out of view.
14741474
let visible_bounds = registration.registration.hitbox.content_mask.bounds;
1475+
// Keeps the synthesized wheel event hit-testing inside the mask.
1476+
const HIT_TEST_INSET: Pixels = px(1.);
1477+
// A collapsed mask leaves an empty clamp range below — stop.
1478+
if visible_bounds.size.width < HIT_TEST_INSET * 2.
1479+
|| visible_bounds.size.height < HIT_TEST_INSET * 2.
1480+
{
1481+
self.stop_anchor_auto_scroll(cx);
1482+
return;
1483+
}
14751484
let delta = AutoScroll::compute_delta(position.y, visible_bounds);
14761485
let Some(window) = window else {
14771486
participant.update(cx, |state, cx| state.set_auto_scroll(delta, cx));
@@ -1480,12 +1489,12 @@ impl WindowSelectionState {
14801489

14811490
let event_position = point(
14821491
position.x.clamp(
1483-
visible_bounds.left() + px(1.),
1484-
visible_bounds.right() - px(1.),
1492+
visible_bounds.left() + HIT_TEST_INSET,
1493+
visible_bounds.right() - HIT_TEST_INSET,
14851494
),
14861495
position.y.clamp(
1487-
visible_bounds.top() + px(1.),
1488-
visible_bounds.bottom() - px(1.),
1496+
visible_bounds.top() + HIT_TEST_INSET,
1497+
visible_bounds.bottom() - HIT_TEST_INSET,
14891498
),
14901499
);
14911500
self.auto_scroll.last_drag_position = Some(event_position);
@@ -3095,6 +3104,45 @@ mod tests {
30953104
assert_eq!(commands.borrow().last(), Some(&None));
30963105
}
30973106

3107+
#[gpui::test]
3108+
fn drag_auto_scroll_stops_when_the_content_mask_collapses(cx: &mut TestAppContext) {
3109+
let window = cx.add_window(|_, cx| WindowSelectionView {
3110+
selection: TextSelectionHandle::new("unused", cx),
3111+
});
3112+
window
3113+
.update(cx, |_, window, cx| {
3114+
let state = cx.new(|_| WindowSelectionState::default());
3115+
let participant = FakeParticipant::new("participant", cx);
3116+
state.update(cx, |state, cx| {
3117+
participant.register(state, 0., TextSelectionScopeId::default(), 0, cx);
3118+
state.begin(point(px(1.), px(1.)), false, cx);
3119+
});
3120+
// The scrollable ancestor got clipped away mid-drag, so the
3121+
// refreshed registration carries a collapsed content mask.
3122+
let collapsed = Bounds::new(point(px(0.), px(0.)), size(px(100.), px(0.)));
3123+
state.update(cx, |state, cx| {
3124+
state.register_participant(
3125+
participant.selection.clone(),
3126+
TextSelectionRegistration::new(
3127+
Hitbox {
3128+
id: HitboxId::placeholder(),
3129+
bounds: collapsed,
3130+
content_mask: ContentMask { bounds: collapsed },
3131+
behavior: HitboxBehavior::Normal,
3132+
},
3133+
collapsed,
3134+
)
3135+
.with_text_bounds(vec![collapsed]),
3136+
cx,
3137+
);
3138+
state.update_in_window(point(px(1.), px(50.)), window, cx);
3139+
assert!(!state.auto_scroll.is_active());
3140+
assert!(state.auto_scroll.last_drag_position.is_none());
3141+
});
3142+
})
3143+
.unwrap();
3144+
}
3145+
30983146
#[gpui::test]
30993147
fn proxy_endpoints_break_equal_position_ties_by_document_order(cx: &mut TestAppContext) {
31003148
cx.update(|cx| {

0 commit comments

Comments
 (0)