diff --git a/codex-rs/tui/src/snapshots/codex_tui__transcript_view__tests__horizontal_selection_at_edge_0.snap b/codex-rs/tui/src/snapshots/codex_tui__transcript_view__tests__horizontal_selection_at_edge_0.snap new file mode 100644 index 0000000000..cc03fe2d86 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__transcript_view__tests__horizontal_selection_at_edge_0.snap @@ -0,0 +1,10 @@ +--- +source: tui/src/transcript_view_tests.rs +expression: text(&selected) +--- +row 22 +row 23 +row 24 +row 25 +row 26 +row 27 diff --git a/codex-rs/tui/src/snapshots/codex_tui__transcript_view__tests__horizontal_selection_at_edge_5.snap b/codex-rs/tui/src/snapshots/codex_tui__transcript_view__tests__horizontal_selection_at_edge_5.snap new file mode 100644 index 0000000000..cc03fe2d86 --- /dev/null +++ b/codex-rs/tui/src/snapshots/codex_tui__transcript_view__tests__horizontal_selection_at_edge_5.snap @@ -0,0 +1,10 @@ +--- +source: tui/src/transcript_view_tests.rs +expression: text(&selected) +--- +row 22 +row 23 +row 24 +row 25 +row 26 +row 27 diff --git a/codex-rs/tui/src/snapshots/codex_tui__transcript_view__tests__wheel_stops_selection_autoscroll.snap b/codex-rs/tui/src/snapshots/codex_tui__transcript_view__tests__wheel_stops_selection_autoscroll.snap index bddf2511a9..e9d162d774 100644 --- a/codex-rs/tui/src/snapshots/codex_tui__transcript_view__tests__wheel_stops_selection_autoscroll.snap +++ b/codex-rs/tui/src/snapshots/codex_tui__transcript_view__tests__wheel_stops_selection_autoscroll.snap @@ -1,18 +1,17 @@ --- -source: tui/src/transcript_view/tests.rs -assertion_line: 265 +source: tui/src/transcript_view_tests.rs expression: "frames.join(\"\\n\\n\")" --- +row 15 +row 16 +row 17 row 18 row 19 row 20 -row 21 -row 22 -row 23 -row 26 -row 27 -row 28 row 29 row 30 row 31 +row 32 +row 33 +row 34 diff --git a/codex-rs/tui/src/transcript_view/selection.rs b/codex-rs/tui/src/transcript_view/selection.rs index 78cfd5f1fd..62cea72f45 100644 --- a/codex-rs/tui/src/transcript_view/selection.rs +++ b/codex-rs/tui/src/transcript_view/selection.rs @@ -14,6 +14,8 @@ pub(super) struct Selection { pub(super) end: Anchor, pub(super) dragging: bool, pub(super) moved: bool, + moved_vertically: bool, + pointer_origin_row: u16, pub(super) resume_on_empty: bool, pub(super) pointer: Option, pub(super) pressed_link: Option, @@ -56,6 +58,8 @@ impl TranscriptView { preferred_column: None, dragging: true, moved: false, + moved_vertically: false, + pointer_origin_row: row, resume_on_empty: was_following, pointer: Some(ScreenPosition::new(column, row)), pressed_link: None, @@ -123,6 +127,7 @@ impl TranscriptView { }; selection.snapshot.pinned.entry(end.key).or_insert(layout); selection.moved = true; + selection.moved_vertically |= selection.pointer_origin_row != row; selection.pointer = Some(ScreenPosition::new(column, row)); selection.preferred_column = None; self.pin_selection_range(&cells, &mut selection); @@ -272,7 +277,8 @@ impl TranscriptView { let Some(selection) = self .selection .as_ref() - .filter(|selection| selection.dragging && selection.moved) + // Horizontal selection on an edge row must not move the text under the pointer. + .filter(|selection| selection.dragging && selection.moved_vertically) else { return false; }; diff --git a/codex-rs/tui/src/transcript_view_tests.rs b/codex-rs/tui/src/transcript_view_tests.rs index 5b2252094c..c9b68b0dc5 100644 --- a/codex-rs/tui/src/transcript_view_tests.rs +++ b/codex-rs/tui/src/transcript_view_tests.rs @@ -340,7 +340,7 @@ fn single_row_scrolling_crosses_an_entry_separator_in_both_directions() { } #[test] -fn clicking_an_edge_does_not_start_selection_autoscroll() { +fn selecting_within_an_edge_row_does_not_start_selection_autoscroll() { let cells = vec![cell( (0..40) .map(|row| format!("row {row:02}\n")) @@ -367,6 +367,64 @@ fn clicking_an_edge_does_not_start_selection_autoscroll() { before ); } + view.handle_mouse( + MouseEvent { + kind: MouseEventKind::Drag(MouseButton::Left), + column: 3, + row: edge, + modifiers: KeyModifiers::NONE, + }, + &cells, + ); + let selected = render(&mut view, &cells, /*width*/ 20, /*height*/ 6); + assert_eq!(text(&selected), text(&before)); + assert_eq!(view.selected_text(&cells).as_deref(), Some("row")); + for _ in 0..10 { + assert!(!view.tick_selection(&cells)); + assert_eq!( + render(&mut view, &cells, /*width*/ 20, /*height*/ 6), + selected + ); + assert_eq!(view.selected_text(&cells).as_deref(), Some("row")); + } + insta::assert_snapshot!( + format!("horizontal_selection_at_edge_{edge}"), + text(&selected) + ); + let drag = MouseEvent { + kind: MouseEventKind::Drag(MouseButton::Left), + column: 3, + row: edge, + modifiers: KeyModifiers::NONE, + }; + view.handle_mouse( + MouseEvent { + kind: if edge == 0 { + MouseEventKind::ScrollUp + } else { + MouseEventKind::ScrollDown + }, + ..drag + }, + &cells, + ); + render(&mut view, &cells, /*width*/ 20, /*height*/ 6); + view.handle_mouse(drag, &cells); + assert!(!view.tick_selection(&cells)); + // A deliberate vertical drag can return to its starting edge and still autoscroll. + for row in [2, edge] { + view.handle_mouse( + MouseEvent { + kind: MouseEventKind::Drag(MouseButton::Left), + column: 3, + row, + modifiers: KeyModifiers::NONE, + }, + &cells, + ); + render(&mut view, &cells, /*width*/ 20, /*height*/ 6); + } + assert!(view.tick_selection(&cells)); } } @@ -393,6 +451,10 @@ fn wheel_scrolling_pauses_selection_autoscroll_until_the_next_drag() { row: edge, modifiers: KeyModifiers::NONE, }; + // Wheel input before the first drag must not lose the original physical row. + view.handle_mouse(MouseEvent { kind, ..drag }, &cells); + assert!(!view.tick_selection(&cells)); + render(&mut view, &cells, /*width*/ 20, /*height*/ 6); view.handle_mouse(drag, &cells); assert!(view.tick_selection(&cells)); render(&mut view, &cells, /*width*/ 20, /*height*/ 6);