aboutsummaryrefslogtreecommitdiff
path: root/src/app
diff options
context:
space:
mode:
Diffstat (limited to 'src/app')
-rw-r--r--src/app/input/mod.rs4
-rw-r--r--src/app/mod.rs39
-rw-r--r--src/app/runtime.rs22
-rw-r--r--src/app/state.rs4
4 files changed, 67 insertions, 2 deletions
diff --git a/src/app/input/mod.rs b/src/app/input/mod.rs
index a2e6597b..3d20adcd 100644
--- a/src/app/input/mod.rs
+++ b/src/app/input/mod.rs
@@ -464,7 +464,9 @@ impl App {
let Some(bytes) = bytes else {
return;
};
- rt.scroll_reset();
+ if !matches!(mouse.kind, MouseEventKind::Moved) {
+ rt.scroll_reset();
+ }
if let Err(err) = rt.try_send_bytes(Bytes::from(bytes)) {
warn!(err = %err, kind = ?mouse.kind, "failed to forward popup mouse event");
}
diff --git a/src/app/mod.rs b/src/app/mod.rs
index c2d479b3..e9aa24b6 100644
--- a/src/app/mod.rs
+++ b/src/app/mod.rs
@@ -5762,6 +5762,45 @@ last_pane = "prefix+tab"
}
#[tokio::test]
+ async fn popup_mouse_motion_preserves_scrollback() {
+ let mut app = test_app();
+ app.state.mode = Mode::Terminal;
+ app.state.view.terminal_area = ratatui::layout::Rect::new(0, 0, 80, 24);
+ let (popup_runtime, mut popup_rx) = TerminalRuntime::test_with_channel_and_scrollback_bytes(
+ 40,
+ 2,
+ 1024,
+ b"one\r\ntwo\r\nthree\r\n\x1b[?1003h\x1b[?1006h",
+ 4,
+ );
+ popup_runtime.scroll_up(1);
+ assert!(popup_runtime
+ .scroll_metrics()
+ .is_some_and(|metrics| metrics.offset_from_bottom > 0));
+ app.install_test_popup_runtime(popup_runtime);
+ let (_, inner) =
+ crate::ui::popup_pane_rects(&app.state, app.state.view.terminal_area).unwrap();
+
+ app.route_client_events(
+ vec![crate::raw_input::RawInputEvent::Mouse(
+ crossterm::event::MouseEvent {
+ kind: crossterm::event::MouseEventKind::Moved,
+ column: inner.x + 1,
+ row: inner.y,
+ modifiers: crossterm::event::KeyModifiers::NONE,
+ },
+ )],
+ true,
+ );
+
+ assert!(popup_rx.try_recv().is_ok());
+ assert!(app
+ .popup_runtime()
+ .and_then(TerminalRuntime::scroll_metrics)
+ .is_some_and(|metrics| metrics.offset_from_bottom > 0));
+ }
+
+ #[tokio::test]
async fn route_client_events_routes_popup_mouse_when_global_capture_is_disabled() {
let mut app = test_app();
let mut workspace = Workspace::test_new("tiled");
diff --git a/src/app/runtime.rs b/src/app/runtime.rs
index 180c3cef..bd7830b9 100644
--- a/src/app/runtime.rs
+++ b/src/app/runtime.rs
@@ -167,13 +167,15 @@ impl App {
true
}
crate::raw_input::RawInputEvent::Mouse(mouse) => {
+ let changes_view = !matches!(mouse.kind, crossterm::event::MouseEventKind::Moved)
+ || self.state.mode.mouse_motion_changes_view();
if self.state.popup_pane.is_some() || self.state.mouse_capture {
self.handle_mouse(mouse);
} else {
self.state
.handle_pane_mouse_only(&self.terminal_runtimes, mouse);
}
- true
+ changes_view
}
crate::raw_input::RawInputEvent::OuterFocusGained => {
self.send_outer_focus_event(crate::ghostty::FocusEvent::Gained);
@@ -809,6 +811,24 @@ mod tests {
}
#[tokio::test]
+ async fn passive_mouse_motion_does_not_request_monolithic_render() {
+ let (mut app, _) = test_app_with_pane();
+ app.state.mode = crate::app::Mode::Terminal;
+ let motion = || {
+ crate::raw_input::RawInputEvent::Mouse(crossterm::event::MouseEvent {
+ kind: crossterm::event::MouseEventKind::Moved,
+ column: 10,
+ row: 5,
+ modifiers: crossterm::event::KeyModifiers::empty(),
+ })
+ };
+
+ assert!(!app.handle_raw_input_event(motion()).await);
+ app.state.mode = crate::app::Mode::GlobalMenu;
+ assert!(app.handle_raw_input_event(motion()).await);
+ }
+
+ #[tokio::test]
async fn raw_input_batch_does_not_start_pending_agent_resume_before_render() {
let (mut app, pane_id) = test_app_with_pane();
app.state.ensure_test_terminals();
diff --git a/src/app/state.rs b/src/app/state.rs
index a3d15a8b..6069b8a6 100644
--- a/src/app/state.rs
+++ b/src/app/state.rs
@@ -814,6 +814,10 @@ pub enum Mode {
}
impl Mode {
+ pub(crate) fn mouse_motion_changes_view(self) -> bool {
+ matches!(self, Self::GlobalMenu | Self::ContextMenu | Self::Navigator)
+ }
+
/// Whether keys in this mode are commands/navigation (an ASCII input source is wanted) rather
/// than free text. This is an explicit **allowlist** of the prefix command/navigation realm:
/// any mode NOT listed defaults to leaving the user's IME alone (the safe default), so adding a