fix(tui): make transcript scrollbar inert
## Summary - remove app-owned transcript scrollbar click/drag state so the right gutter cannot capture mouse drags - keep wheel scrolling, jump-to-latest, and normal transcript text selection intact - align the behavior with opencode's session surface: scroll via wheel/key paths, no app-level scrollbar drag affordance ## Test plan - cargo test -p deepseek-tui transcript_scrollbar_gutter_is_not_draggable --locked - cargo test -p deepseek-tui left_down_inside_transcript_starts_selection --locked - cargo fmt --all -- --check - git diff --check - GitHub CI: lint, version drift, ubuntu/macos/windows tests, npm wrapper smoke, GitGuardian
This commit is contained in:
@@ -574,7 +574,6 @@ pub struct ViewportState {
|
||||
pub mouse_scroll: MouseScrollState,
|
||||
pub transcript_cache: TranscriptViewCache,
|
||||
pub transcript_selection: TranscriptSelection,
|
||||
pub transcript_scrollbar_dragging: bool,
|
||||
pub last_transcript_area: Option<Rect>,
|
||||
pub last_transcript_top: usize,
|
||||
pub last_transcript_visible: usize,
|
||||
@@ -591,7 +590,6 @@ impl Default for ViewportState {
|
||||
mouse_scroll: MouseScrollState::new(),
|
||||
transcript_cache: TranscriptViewCache::new(),
|
||||
transcript_selection: TranscriptSelection::default(),
|
||||
transcript_scrollbar_dragging: false,
|
||||
last_transcript_area: None,
|
||||
last_transcript_top: 0,
|
||||
last_transcript_visible: 0,
|
||||
@@ -2457,7 +2455,6 @@ impl App {
|
||||
|
||||
self.viewport.pending_scroll_delta = 0;
|
||||
self.viewport.transcript_selection.clear();
|
||||
self.viewport.transcript_scrollbar_dragging = false;
|
||||
|
||||
self.viewport.last_transcript_area = None;
|
||||
self.viewport.last_transcript_top = 0;
|
||||
|
||||
@@ -7483,20 +7483,11 @@ fn handle_mouse_event(app: &mut App, mouse: MouseEvent) -> Vec<ViewEvent> {
|
||||
}
|
||||
}
|
||||
MouseEventKind::Down(MouseButton::Left) => {
|
||||
app.viewport.transcript_scrollbar_dragging = false;
|
||||
|
||||
if mouse_hits_rect(mouse, app.viewport.jump_to_latest_button_area) {
|
||||
app.scroll_to_bottom();
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
if mouse_hits_transcript_scrollbar(app, mouse) {
|
||||
app.viewport.transcript_scrollbar_dragging = true;
|
||||
app.viewport.transcript_selection.clear();
|
||||
scroll_transcript_to_mouse_row(app, mouse.row);
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
if let Some(point) = selection_point_from_mouse(app, mouse) {
|
||||
app.viewport.transcript_selection.anchor = Some(point);
|
||||
app.viewport.transcript_selection.head = Some(point);
|
||||
@@ -7516,21 +7507,12 @@ fn handle_mouse_event(app: &mut App, mouse: MouseEvent) -> Vec<ViewEvent> {
|
||||
}
|
||||
}
|
||||
MouseEventKind::Drag(MouseButton::Left) => {
|
||||
if app.viewport.transcript_scrollbar_dragging {
|
||||
scroll_transcript_to_mouse_row(app, mouse.row);
|
||||
return Vec::new();
|
||||
}
|
||||
|
||||
if app.viewport.transcript_selection.dragging
|
||||
&& let Some(point) = selection_point_from_mouse(app, mouse)
|
||||
{
|
||||
app.viewport.transcript_selection.head = Some(point);
|
||||
}
|
||||
}
|
||||
MouseEventKind::Up(MouseButton::Left) if app.viewport.transcript_scrollbar_dragging => {
|
||||
app.viewport.transcript_scrollbar_dragging = false;
|
||||
app.needs_redraw = true;
|
||||
}
|
||||
MouseEventKind::Up(MouseButton::Left) if app.viewport.transcript_selection.dragging => {
|
||||
app.viewport.transcript_selection.dragging = false;
|
||||
if selection_has_content(app) {
|
||||
@@ -7546,57 +7528,6 @@ fn handle_mouse_event(app: &mut App, mouse: MouseEvent) -> Vec<ViewEvent> {
|
||||
Vec::new()
|
||||
}
|
||||
|
||||
fn mouse_hits_transcript_scrollbar(app: &App, mouse: MouseEvent) -> bool {
|
||||
let Some(area) = app.viewport.last_transcript_area else {
|
||||
return false;
|
||||
};
|
||||
if area.width <= 1 || app.viewport.last_transcript_total <= app.viewport.last_transcript_visible
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
let scrollbar_col = area.x.saturating_add(area.width.saturating_sub(1));
|
||||
mouse.column == scrollbar_col
|
||||
&& mouse.row >= area.y
|
||||
&& mouse.row < area.y.saturating_add(area.height)
|
||||
}
|
||||
|
||||
fn scroll_transcript_to_mouse_row(app: &mut App, row: u16) -> bool {
|
||||
let Some(area) = app.viewport.last_transcript_area else {
|
||||
return false;
|
||||
};
|
||||
let total = app.viewport.last_transcript_total;
|
||||
let visible = app.viewport.last_transcript_visible;
|
||||
if area.height == 0 || total <= visible {
|
||||
return false;
|
||||
}
|
||||
|
||||
let max_start = total.saturating_sub(visible);
|
||||
if max_start == 0 {
|
||||
app.scroll_to_bottom();
|
||||
return true;
|
||||
}
|
||||
|
||||
let max_row = usize::from(area.height.saturating_sub(1));
|
||||
let relative_row = usize::from(row.saturating_sub(area.y)).min(max_row);
|
||||
let numerator = relative_row
|
||||
.saturating_mul(max_start)
|
||||
.saturating_add(max_row / 2);
|
||||
// Round to the nearest transcript offset so short thumbs still feel
|
||||
// responsive on compact terminals.
|
||||
let top = numerator.checked_div(max_row).unwrap_or(0);
|
||||
|
||||
app.viewport.transcript_scroll = if top >= max_start {
|
||||
TranscriptScroll::to_bottom()
|
||||
} else {
|
||||
TranscriptScroll::at_line(top)
|
||||
};
|
||||
app.viewport.pending_scroll_delta = 0;
|
||||
app.user_scrolled_during_stream = !app.viewport.transcript_scroll.is_at_tail();
|
||||
app.needs_redraw = true;
|
||||
true
|
||||
}
|
||||
|
||||
fn mouse_hits_rect(mouse: MouseEvent, area: Option<Rect>) -> bool {
|
||||
let Some(area) = area else {
|
||||
return false;
|
||||
|
||||
@@ -338,8 +338,19 @@ fn jump_to_latest_button_click_scrolls_to_tail() {
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transcript_scrollbar_drag_maps_mouse_row_to_scroll_position() {
|
||||
fn transcript_scrollbar_gutter_is_not_draggable() {
|
||||
let mut app = create_test_app();
|
||||
app.history = vec![HistoryCell::Assistant {
|
||||
content: "alpha beta".to_string(),
|
||||
streaming: false,
|
||||
}];
|
||||
app.resync_history_revisions();
|
||||
app.viewport.transcript_cache.ensure(
|
||||
&app.history,
|
||||
&app.history_revisions,
|
||||
80,
|
||||
app.transcript_render_options(),
|
||||
);
|
||||
app.viewport.last_transcript_area = Some(Rect {
|
||||
x: 2,
|
||||
y: 5,
|
||||
@@ -349,6 +360,7 @@ fn transcript_scrollbar_drag_maps_mouse_row_to_scroll_position() {
|
||||
app.viewport.last_transcript_visible = 10;
|
||||
app.viewport.last_transcript_total = 110;
|
||||
app.viewport.transcript_scroll = TranscriptScroll::to_bottom();
|
||||
app.user_scrolled_during_stream = false;
|
||||
|
||||
let events = handle_mouse_event(
|
||||
&mut app,
|
||||
@@ -361,12 +373,9 @@ fn transcript_scrollbar_drag_maps_mouse_row_to_scroll_position() {
|
||||
);
|
||||
|
||||
assert!(events.is_empty());
|
||||
assert!(app.viewport.transcript_scrollbar_dragging);
|
||||
assert!(!app.viewport.transcript_selection.dragging);
|
||||
assert!(!app.viewport.transcript_scroll.is_at_tail());
|
||||
let (_, top) = app.viewport.transcript_scroll.resolve_top(&[], 100);
|
||||
assert_eq!(top, 0);
|
||||
assert!(app.user_scrolled_during_stream);
|
||||
assert!(app.viewport.transcript_selection.dragging);
|
||||
assert!(app.viewport.transcript_scroll.is_at_tail());
|
||||
assert!(!app.user_scrolled_during_stream);
|
||||
|
||||
handle_mouse_event(
|
||||
&mut app,
|
||||
@@ -380,6 +389,7 @@ fn transcript_scrollbar_drag_maps_mouse_row_to_scroll_position() {
|
||||
|
||||
assert!(app.viewport.transcript_scroll.is_at_tail());
|
||||
assert!(!app.user_scrolled_during_stream);
|
||||
assert!(app.viewport.transcript_selection.dragging);
|
||||
|
||||
handle_mouse_event(
|
||||
&mut app,
|
||||
@@ -391,11 +401,11 @@ fn transcript_scrollbar_drag_maps_mouse_row_to_scroll_position() {
|
||||
},
|
||||
);
|
||||
|
||||
assert!(!app.viewport.transcript_scrollbar_dragging);
|
||||
assert!(!app.viewport.transcript_selection.dragging);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_left_down_clears_stale_transcript_scrollbar_drag() {
|
||||
fn left_down_inside_transcript_starts_selection() {
|
||||
let mut app = create_test_app();
|
||||
app.history = vec![HistoryCell::Assistant {
|
||||
content: "alpha beta".to_string(),
|
||||
@@ -410,7 +420,6 @@ fn new_left_down_clears_stale_transcript_scrollbar_drag() {
|
||||
});
|
||||
app.viewport.last_transcript_visible = 10;
|
||||
app.viewport.last_transcript_total = 110;
|
||||
app.viewport.transcript_scrollbar_dragging = true;
|
||||
|
||||
let events = handle_mouse_event(
|
||||
&mut app,
|
||||
@@ -423,7 +432,7 @@ fn new_left_down_clears_stale_transcript_scrollbar_drag() {
|
||||
);
|
||||
|
||||
assert!(events.is_empty());
|
||||
assert!(!app.viewport.transcript_scrollbar_dragging);
|
||||
assert!(app.viewport.transcript_selection.dragging);
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
||||
Reference in New Issue
Block a user