From 97034b2ae62c900ae68547dfb1e6d870269cc587 Mon Sep 17 00:00:00 2001 From: Tryanks Date: Thu, 30 Jul 2026 21:22:34 +0800 Subject: [PATCH] fix(ui): keep titlebar presses from starting a text selection A press on a window drag strip propagated to the window-level Markdown selection controller, which proxy-anchored a selection to the nearest message. Once start_window_move swallowed the mouse-up on macOS, the selection's auto-scroll loop kept scrolling the chat up and extending the selection for as long as the titlebar was held. Prevent the default on the drag area's left mouse-down: the controller already skips default-prevented presses outside Markdown hitboxes, and drag strips contain no Markdown. Covers the chat header, sidebar top strip, settings header and diff tab strip alike. Co-Authored-By: Claude Fable 5 --- crates/ui/src/markdown/window_selection.rs | 109 +++++++++++++++++++++ crates/ui/src/shell.rs | 8 ++ 2 files changed, 117 insertions(+) diff --git a/crates/ui/src/markdown/window_selection.rs b/crates/ui/src/markdown/window_selection.rs index 7fb0df6d..ea07a605 100644 --- a/crates/ui/src/markdown/window_selection.rs +++ b/crates/ui/src/markdown/window_selection.rs @@ -645,3 +645,112 @@ impl Element for TextSelectionController { }); } } + +#[cfg(test)] +mod tests { + use gpui::{ + AppContext as _, Context, Entity, IntoElement, Modifiers, MouseButton, ParentElement as _, + Render, Styled as _, TestAppContext, VisualTestContext, Window, div, point, px, + }; + + use super::*; + use crate::markdown::{MarkdownView, state::MarkdownState}; + + /// A titlebar drag strip above a selectable Markdown view — the layout of + /// every window header that hosts `window_drag_area`. + struct DragAreaRoot { + markdown: Entity, + } + + impl DragAreaRoot { + fn new(cx: &mut Context) -> Self { + Self { + markdown: cx.new(|cx| MarkdownState::new("Hello world", cx)), + } + } + } + + impl Render for DragAreaRoot { + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { + div() + .size_full() + .child(TextSelectionController) + .child(crate::window_drag_area( + "test-titlebar", + div().w_full().h(px(52.)), + window, + cx, + )) + .child( + div() + .h(px(40.)) + .child(MarkdownView::new(&self.markdown).selectable(true)), + ) + .child(div().h(px(60.))) + } + } + + fn setup(cx: &mut TestAppContext) -> &mut VisualTestContext { + cx.update(gpui_component::init); + cx.update(crate::markdown::init); + let (_, cx) = cx.add_window_view(|_, cx| DragAreaRoot::new(cx)); + let cx: &mut VisualTestContext = cx; + cx.update(|window, cx| { + let _ = window.draw(cx); + }); + cx + } + + fn is_selecting(cx: &mut VisualTestContext) -> bool { + cx.update(|window, cx| { + cx.try_global::() + .and_then(|global| global.windows.get(&window_id(window))) + .is_some_and(|state| state.selection.is_selecting) + }) + } + + /// A press on a `window_drag_area` strip must not begin a window text + /// selection. Left unprevented, the press proxy-anchors a selection to the + /// nearest Markdown view; once `start_window_move` swallows the mouse-up + /// the auto-scroll loop keeps scrolling the chat up and extending the + /// selection for as long as the titlebar is held. + #[gpui::test] + fn press_on_titlebar_drag_area_does_not_start_selection(cx: &mut TestAppContext) { + let cx = setup(cx); + + // Sanity: a press outside the drag strip (blank space below the + // content) does arm the controller, so the assertion below is not + // vacuously true. + cx.simulate_mouse_down( + point(px(10.), px(130.)), + MouseButton::Left, + Modifiers::default(), + ); + cx.update(|window, cx| { + let _ = window.draw(cx); + }); + assert!(is_selecting(cx), "blank-space press should arm selection"); + cx.simulate_mouse_up( + point(px(10.), px(130.)), + MouseButton::Left, + Modifiers::default(), + ); + cx.update(|window, cx| { + let _ = window.draw(cx); + }); + + // The press under test: on the titlebar drag strip. + cx.simulate_mouse_down( + point(px(10.), px(20.)), + MouseButton::Left, + Modifiers::default(), + ); + cx.update(|window, cx| { + let _ = window.draw(cx); + }); + assert!( + !is_selecting(cx), + "a titlebar press must not begin a window text selection" + ); + } +} diff --git a/crates/ui/src/shell.rs b/crates/ui/src/shell.rs index 1022a221..93ab1571 100644 --- a/crates/ui/src/shell.rs +++ b/crates/ui/src/shell.rs @@ -63,6 +63,14 @@ pub(crate) fn window_drag_area( .on_mouse_down( MouseButton::Left, window.listener_for(&state, |state, event: &MouseDownEvent, window, _| { + // A titlebar press must never begin a window text selection. Left + // unprevented, the Markdown selection controller proxy-anchors a + // selection to the nearest message and its auto-scroll loop takes + // over once `start_window_move` swallows the mouse-up: the chat + // keeps scrolling up and selecting while the window is dragged. + // The controller skips presses outside Markdown hitboxes when the + // default is prevented, and drag strips contain no Markdown. + window.prevent_default(); // Double-click zooms/maximizes the window like a native titlebar. if event.click_count >= 2 { state.should_move = false;