aboutsummaryrefslogtreecommitdiff
path: root/src/server
diff options
context:
space:
mode:
authorCan Celik <ogulcancelik@gmail.com>2026-07-26 00:28:11 +0300
committerGitHub <noreply@github.com>2026-07-26 00:28:11 +0300
commit547aba8e0ab310623f1986d3026658f708d432bd (patch)
tree8b05ec9feec9a3c3c04f21aa308bf22faad3772a /src/server
parentb01fc37e66aac45c7fa9eccc826d793d68608a0a (diff)
fix: avoid renders for passive mouse motion (#1872)
refs #1865
Diffstat (limited to 'src/server')
-rw-r--r--src/server/headless.rs113
1 files changed, 111 insertions, 2 deletions
diff --git a/src/server/headless.rs b/src/server/headless.rs
index 94721c38..0b25dd97 100644
--- a/src/server/headless.rs
+++ b/src/server/headless.rs
@@ -2610,11 +2610,13 @@ impl HeadlessServer {
&events,
self.app.state.redraw_on_focus_gained,
);
+ let render_neutral_mouse_motion =
+ events_are_render_neutral_mouse_motion(&events, self.app.state.mode);
if let Some(client) = self.clients.get_mut(&client_id) {
if host_surface_redraw {
client.request_repaint();
client.defer_full_render();
- } else {
+ } else if !render_neutral_mouse_motion {
// Ensure semantic clients receive one post-input frame even if the
// semantic buffer compares equal. Terminal-ANSI clients must keep their
// server-side blit baseline; resetting it here forces a full redraw on
@@ -2669,7 +2671,7 @@ impl HeadlessServer {
false
} else {
- foreground_changed || theme_changed || interaction
+ foreground_changed || theme_changed || (interaction && !render_neutral_mouse_motion)
}
}
@@ -4203,6 +4205,25 @@ impl HeadlessServer {
}
}
+// Pane applications render their own motion responses through PTY output. Only Herdr modes with
+// hover selection mutate the current frame directly from a plain mouse-move event.
+fn events_are_render_neutral_mouse_motion(
+ events: &[crate::raw_input::RawInputEvent],
+ mode: crate::app::Mode,
+) -> bool {
+ !events.is_empty()
+ && !mode.mouse_motion_changes_view()
+ && events.iter().all(|event| {
+ matches!(
+ event,
+ crate::raw_input::RawInputEvent::Mouse(crossterm::event::MouseEvent {
+ kind: MouseEventKind::Moved,
+ ..
+ })
+ )
+ })
+}
+
fn events_for_app_routing(
events: Vec<crate::raw_input::RawInputEvent>,
mut source_is_foreground: bool,
@@ -7297,6 +7318,94 @@ next_tab = ""
assert_eq!(server.foreground_client_id, Some(3));
}
+ #[tokio::test]
+ async fn passive_mouse_motion_forwards_without_requesting_render() {
+ let mut server = test_headless_server();
+ let mut input_rx = install_focused_test_runtime(&mut server, b"\x1b[?1003h\x1b[?1006h");
+ server.clients.insert(1, test_app_client(Some(true), 1));
+ server.foreground_client_id = Some(1);
+ server.sync_foreground_client_state();
+ server.resize_shared_runtime_to_effective_size();
+ let baseline = FrameData {
+ cells: Vec::new(),
+ width: 0,
+ height: 0,
+ cursor: None,
+ hyperlinks: Vec::new(),
+ graphics: Vec::new(),
+ };
+ let client = server.clients.get_mut(&1).unwrap();
+ let prepared = client
+ .render_state
+ .prepare_frame(baseline.clone())
+ .expect("new semantic baseline");
+ client.render_state.commit_sent_frame(prepared);
+ let pane = server.app.state.view.pane_infos[0].clone();
+ let column = pane.inner_rect.x + 2;
+ let row = pane.inner_rect.y + 3;
+ let input = format!("\x1b[<35;{};{}M", column + 1, row + 1).into_bytes();
+
+ assert!(!server.handle_server_event(ServerEvent::ClientInput {
+ client_id: 1,
+ data: input,
+ }));
+ assert_eq!(
+ input_rx.try_recv().expect("forwarded mouse motion"),
+ Bytes::from_static(b"\x1b[<35;3;4M")
+ );
+ assert_eq!(
+ server.clients[&1].render_state.last_frame(),
+ Some(&baseline)
+ );
+ }
+
+ #[test]
+ fn background_mouse_motion_promotes_once_then_becomes_render_neutral() {
+ let mut server = test_headless_server();
+ server.app.state.mode = crate::app::Mode::Terminal;
+ server.clients.insert(1, test_app_client(Some(true), 1));
+ server.clients.insert(2, test_app_client(Some(true), 2));
+ server.foreground_client_id = Some(1);
+ server.sync_foreground_client_state();
+ let motion = || ServerEvent::ClientInputEvents {
+ client_id: 2,
+ events: vec![crate::protocol::ClientInputEvent::Mouse {
+ kind: crate::protocol::ClientMouseKind::Moved,
+ column: 10,
+ row: 5,
+ modifiers: 0,
+ }],
+ };
+
+ assert!(server.handle_server_event(motion()));
+ assert_eq!(server.foreground_client_id, Some(2));
+ assert!(!server.handle_server_event(motion()));
+ }
+
+ #[test]
+ fn mouse_motion_in_hover_modes_requires_render() {
+ let events = [crate::raw_input::RawInputEvent::Mouse(
+ crossterm::event::MouseEvent {
+ kind: MouseEventKind::Moved,
+ column: 10,
+ row: 5,
+ modifiers: KeyModifiers::empty(),
+ },
+ )];
+
+ assert!(events_are_render_neutral_mouse_motion(
+ &events,
+ crate::app::Mode::Terminal
+ ));
+ for mode in [
+ crate::app::Mode::GlobalMenu,
+ crate::app::Mode::ContextMenu,
+ crate::app::Mode::Navigator,
+ ] {
+ assert!(!events_are_render_neutral_mouse_motion(&events, mode));
+ }
+ }
+
fn install_focused_test_runtime(
server: &mut HeadlessServer,
terminal_bytes: &[u8],