feat(tui): support transcript scrollbar dragging
This commit is contained in:
@@ -575,6 +575,7 @@ 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,6 +592,7 @@ 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,
|
||||
|
||||
@@ -7324,6 +7324,13 @@ fn handle_mouse_event(app: &mut App, mouse: MouseEvent) -> Vec<ViewEvent> {
|
||||
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);
|
||||
@@ -7343,12 +7350,21 @@ 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) {
|
||||
@@ -7364,6 +7380,61 @@ 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 top = if max_row == 0 {
|
||||
0
|
||||
} else {
|
||||
// Round to the nearest transcript offset so short thumbs still feel
|
||||
// responsive on compact terminals.
|
||||
(relative_row
|
||||
.saturating_mul(max_start)
|
||||
.saturating_add(max_row / 2))
|
||||
/ max_row
|
||||
};
|
||||
|
||||
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;
|
||||
|
||||
@@ -302,6 +302,63 @@ fn jump_to_latest_button_click_scrolls_to_tail() {
|
||||
assert!(!app.viewport.transcript_selection.dragging);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn transcript_scrollbar_drag_maps_mouse_row_to_scroll_position() {
|
||||
let mut app = create_test_app();
|
||||
app.viewport.last_transcript_area = Some(Rect {
|
||||
x: 2,
|
||||
y: 5,
|
||||
width: 20,
|
||||
height: 10,
|
||||
});
|
||||
app.viewport.last_transcript_visible = 10;
|
||||
app.viewport.last_transcript_total = 110;
|
||||
app.viewport.transcript_scroll = TranscriptScroll::to_bottom();
|
||||
|
||||
let events = handle_mouse_event(
|
||||
&mut app,
|
||||
MouseEvent {
|
||||
kind: MouseEventKind::Down(MouseButton::Left),
|
||||
column: 21,
|
||||
row: 5,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
},
|
||||
);
|
||||
|
||||
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);
|
||||
|
||||
handle_mouse_event(
|
||||
&mut app,
|
||||
MouseEvent {
|
||||
kind: MouseEventKind::Drag(MouseButton::Left),
|
||||
column: 21,
|
||||
row: 14,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
},
|
||||
);
|
||||
|
||||
assert!(app.viewport.transcript_scroll.is_at_tail());
|
||||
assert!(!app.user_scrolled_during_stream);
|
||||
|
||||
handle_mouse_event(
|
||||
&mut app,
|
||||
MouseEvent {
|
||||
kind: MouseEventKind::Up(MouseButton::Left),
|
||||
column: 21,
|
||||
row: 14,
|
||||
modifiers: KeyModifiers::NONE,
|
||||
},
|
||||
);
|
||||
|
||||
assert!(!app.viewport.transcript_scrollbar_dragging);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn right_click_opens_context_menu() {
|
||||
let mut app = create_test_app();
|
||||
|
||||
Reference in New Issue
Block a user