diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-07-26 00:28:11 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-07-26 00:28:11 +0300 |
| commit | 547aba8e0ab310623f1986d3026658f708d432bd (patch) | |
| tree | 8b05ec9feec9a3c3c04f21aa308bf22faad3772a /src/server | |
| parent | b01fc37e66aac45c7fa9eccc826d793d68608a0a (diff) | |
fix: avoid renders for passive mouse motion (#1872)
refs #1865
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/headless.rs | 113 |
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], |
