From 4d3bbd82e07301bdd6eb1bae5d8c79e8d4fb8d73 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sat, 12 Jul 2025 11:59:23 +0530 Subject: [PATCH] Extend the SGR Pixel mouse reporting protocol to also report when the mouse leaves the window --- docs/changelog.rst | 2 ++ docs/misc-protocol.rst | 13 ++++++++++ kitty/data-types.h | 1 + kitty/glfw.c | 19 ++++++++------ kitty/mouse.c | 49 ++++++++++++++++++++++++++++++------ kitty/state.h | 2 +- tools/cmd/mouse_demo/main.go | 4 +++ tools/tui/loop/mouse.go | 21 ++++++++++------ 8 files changed, 87 insertions(+), 24 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 7a21e558e..5e808bd2f 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -109,6 +109,8 @@ Detailed list of changes 0.42.2 [future] ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ +- A new :ref:`protocol extension ` to notify terminal programs that have turned on SGR Pixel mouse reporting when the mouse leaves the window (:disc:`8808`) + - Fix :opt:`remember_window_position` not working because of a stupid typo (:iss:`8646`) - A new :option:`kitty --grab-keyboard` that can be used to grab the keyboard so that global shortcuts are sent to kitty instead diff --git a/docs/misc-protocol.rst b/docs/misc-protocol.rst index 3323909f3..dc67f8a41 100644 --- a/docs/misc-protocol.rst +++ b/docs/misc-protocol.rst @@ -29,6 +29,19 @@ there is only one number to reset these attributes, SGR 22, which resets both. There is no way to reset one and not the other. kitty uses 221 and 222 to reset bold and faint independently. +.. _mouse_leave_window: + +Reporting when the mouse leaves the window +---------------------------------------------- + +kitty extends the SGR Pixel mouse reporting protocol created by xterm to +also report when the mouse leaves the window. This is event is delivered +encoded as a normal SGR pixel event except that the eight bit is set on the +first number. bits 1-7 are used to encode button and modifier information. +When bit 8 is set it means the event is a mouse has left the window event, +and all other bits should be ignored. The pixel position values must also +be ignored as they may not be accurate. + kitty specific private escape codes --------------------------------------- diff --git a/kitty/data-types.h b/kitty/data-types.h index 423440ab0..0db25a30f 100644 --- a/kitty/data-types.h +++ b/kitty/data-types.h @@ -318,6 +318,7 @@ void colorprofile_report_stack(ColorProfile*, unsigned int*, unsigned int*); void set_mouse_cursor(MouseShape); void enter_event(int modifiers); +void leave_event(int modifiers); void mouse_event(const int, int, int); void focus_in_event(void); void scroll_event(double, double, int, int); diff --git a/kitty/glfw.c b/kitty/glfw.c index bd0d5cc50..6348608f9 100644 --- a/kitty/glfw.c +++ b/kitty/glfw.c @@ -530,18 +530,21 @@ key_callback(GLFWwindow *w, GLFWkeyevent *ev) { static void cursor_enter_callback(GLFWwindow *w, int entered) { if (!set_callback_window(w)) return; + double x, y; + glfwGetCursorPos(w, &x, &y); + monotonic_t now = monotonic(); + global_state.callback_os_window->last_mouse_activity_at = now; + global_state.callback_os_window->mouse_x = x * global_state.callback_os_window->viewport_x_ratio; + global_state.callback_os_window->mouse_y = y * global_state.callback_os_window->viewport_y_ratio; if (entered) { - double x, y; - glfwGetCursorPos(w, &x, &y); debug_input("Mouse cursor entered window: %llu at %fx%f\n", global_state.callback_os_window->id, x, y); - monotonic_t now = monotonic(); cursor_active_callback(w, now); - global_state.callback_os_window->last_mouse_activity_at = now; - global_state.callback_os_window->mouse_x = x * global_state.callback_os_window->viewport_x_ratio; - global_state.callback_os_window->mouse_y = y * global_state.callback_os_window->viewport_y_ratio; if (is_window_ready_for_callbacks()) enter_event(mods_at_last_key_or_button_event); - request_tick_callback(); - } else debug_input("Mouse cursor left window: %llu\n", global_state.callback_os_window->id); + } else { + debug_input("Mouse cursor left window: %llu\n", global_state.callback_os_window->id); + if (is_window_ready_for_callbacks()) leave_event(mods_at_last_key_or_button_event); + } + request_tick_callback(); global_state.callback_os_window = NULL; } diff --git a/kitty/mouse.c b/kitty/mouse.c index 2980be691..122fcd28c 100644 --- a/kitty/mouse.c +++ b/kitty/mouse.c @@ -15,7 +15,7 @@ extern PyTypeObject Screen_Type; static MouseShape mouse_cursor_shape = TEXT_POINTER; -typedef enum MouseActions { PRESS, RELEASE, DRAG, MOVE } MouseAction; +typedef enum MouseActions { PRESS, RELEASE, DRAG, MOVE, LEAVE } MouseAction; #define debug debug_input // Encoding of mouse events {{{ @@ -25,6 +25,7 @@ typedef enum MouseActions { PRESS, RELEASE, DRAG, MOVE } MouseAction; #define MOTION_INDICATOR (1 << 5) #define SCROLL_BUTTON_INDICATOR (1 << 6) #define EXTRA_BUTTON_INDICATOR (1 << 7) +#define LEAVE_INDICATOR (1 << 8) static unsigned int @@ -65,11 +66,18 @@ static char mouse_event_buf[64]; static int encode_mouse_event_impl(const MousePosition *mpos, int mouse_tracking_protocol, int button, MouseAction action, int mods) { unsigned int cb = encode_button(button); - if (action == MOVE) { - if (cb == UINT_MAX) cb = 3; - cb += 32; - } else { - if (cb == UINT_MAX) return 0; + switch (action) { + case MOVE: + if (cb == UINT_MAX) cb = 3; + cb += 32; + break; + case LEAVE: + if (mouse_tracking_protocol != SGR_PIXEL_PROTOCOL) return 0; + cb = LEAVE_INDICATOR; + break; + default: + if (cb == UINT_MAX) return 0; + break; } if (action == DRAG || action == MOVE) cb |= MOTION_INDICATOR; else if (action == RELEASE && mouse_tracking_protocol < SGR_PROTOCOL) cb = 3; @@ -150,6 +158,21 @@ window_for_id(id_type window_id) { return window_for_window_id(window_id); } +static void +send_mouse_leave_event_if_needed(id_type currently_over_window, int modifiers) { + if (global_state.mouse_hover_in_window != currently_over_window && global_state.mouse_hover_in_window) { + Window *left_window = window_for_id(global_state.mouse_hover_in_window); + global_state.mouse_hover_in_window = currently_over_window; + if (left_window) { + int sz = encode_mouse_event(left_window, 0, LEAVE, modifiers); + if (sz > 0) { + mouse_event_buf[sz] = 0; + write_escape_code_to_child(left_window->render_data.screen, ESC_CSI, mouse_event_buf); + debug("Sent mouse leave event to window: %llu\n", left_window->id); + } + } + } +} static bool dispatch_mouse_event(Window *w, int button, int count, int modifiers, bool grabbed) { @@ -601,6 +624,8 @@ currently_pressed_button(void) { HANDLER(handle_event) { modifiers &= ~GLFW_LOCK_MASK; set_mouse_cursor_for_screen(w->render_data.screen); + send_mouse_leave_event_if_needed(w->id, modifiers); + global_state.mouse_hover_in_window = w->id; if (button == -1) { button = currently_pressed_button(); handle_move_event(w, button, modifiers, window_idx); @@ -611,6 +636,7 @@ HANDLER(handle_event) { static void handle_tab_bar_mouse(int button, int modifiers, int action) { + send_mouse_leave_event_if_needed(0, modifiers); if (button > -1) { // dont report motion events, as they are expensive and useless call_boss(handle_click_on_tab, "Kdiii", global_state.callback_os_window->id, global_state.callback_os_window->mouse_x, button, modifiers, action); } @@ -694,6 +720,12 @@ update_mouse_pointer_shape(void) { set_mouse_cursor(mouse_cursor_shape); } +void +leave_event(int modifiers) { + if (global_state.redirect_mouse_handling || global_state.active_drag_in_window || global_state.tracked_drag_in_window || !global_state.mouse_hover_in_window) return; + send_mouse_leave_event_if_needed(0, modifiers); +} + void enter_event(int modifiers) { #ifdef __APPLE__ @@ -713,9 +745,10 @@ enter_event(int modifiers) { if (global_state.redirect_mouse_handling || global_state.active_drag_in_window || global_state.tracked_drag_in_window) return; unsigned window_idx; bool in_tab_bar; Window *w = window_for_event(&window_idx, &in_tab_bar); + send_mouse_leave_event_if_needed(w ? w->id : 0, modifiers); if (!w || in_tab_bar) return; - bool mouse_cell_changed = false; - bool cell_half_changed = false; + global_state.mouse_hover_in_window = w->id; + bool mouse_cell_changed = false, cell_half_changed = false; if (!set_mouse_position(w, &mouse_cell_changed, &cell_half_changed)) return; Screen *screen = w->render_data.screen; int button = currently_pressed_button(); diff --git a/kitty/state.h b/kitty/state.h index 2f7cd25a1..436cd536d 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -332,7 +332,7 @@ typedef struct { bool has_pending_resizes, has_pending_closes; bool check_for_active_animated_images; struct { double x, y; } default_dpi; - id_type active_drag_in_window, tracked_drag_in_window; + id_type active_drag_in_window, tracked_drag_in_window, mouse_hover_in_window; int active_drag_button, tracked_drag_button; CloseRequest quit_request; bool redirect_mouse_handling; diff --git a/tools/cmd/mouse_demo/main.go b/tools/cmd/mouse_demo/main.go index 3f4979855..3a34bf3a0 100644 --- a/tools/cmd/mouse_demo/main.go +++ b/tools/cmd/mouse_demo/main.go @@ -72,6 +72,10 @@ func Run(args []string) (rc int, err error) { return } lp.ClearScreen() + if current_mouse_event.Event_type == loop.MOUSE_LEAVE { + lp.Println("Mouse has left the window") + return + } lp.Printf("Position: %d, %d (pixels)\r\n", current_mouse_event.Pixel.X, current_mouse_event.Pixel.Y) lp.Printf("Cell : %d, %d\r\n", current_mouse_event.Cell.X, current_mouse_event.Cell.Y) lp.Printf("Type : %s\r\n", current_mouse_event.Event_type) diff --git a/tools/tui/loop/mouse.go b/tools/tui/loop/mouse.go index a14e8c96a..03f6adb8f 100644 --- a/tools/tui/loop/mouse.go +++ b/tools/tui/loop/mouse.go @@ -18,6 +18,7 @@ const ( MOUSE_RELEASE MOUSE_MOVE MOUSE_CLICK + MOUSE_LEAVE ) func (e MouseEventType) String() string { @@ -141,10 +142,13 @@ func (e PointerShape) String() string { } const ( - SHIFT_INDICATOR int = 1 << 2 - ALT_INDICATOR = 1 << 3 - CTRL_INDICATOR = 1 << 4 - MOTION_INDICATOR = 1 << 5 + SHIFT_INDICATOR int = 1 << 2 + ALT_INDICATOR = 1 << 3 + CTRL_INDICATOR = 1 << 4 + MOTION_INDICATOR = 1 << 5 + SCROLL_BUTTON_INDICATOR = 1 << 6 + EXTRA_BUTTON_INDICATOR = 1 << 7 + LEAVE_INDICATOR = 1 << 8 ) const ( @@ -245,11 +249,14 @@ func decode_sgr_mouse(text string, screen_size ScreenSize, last_letter byte) *Mo ans.Event_type = MOUSE_MOVE } cb3 := cb & 3 - if cb >= 128 { + switch { + case cb&LEAVE_INDICATOR != 0: + ans.Event_type = MOUSE_LEAVE + case cb&EXTRA_BUTTON_INDICATOR != 0: ans.Buttons |= ebmap[cb3] - } else if cb >= 64 { + case cb&SCROLL_BUTTON_INDICATOR != 0: ans.Buttons |= wbmap[cb3] - } else if cb3 < 3 { + case cb3 < 3: ans.Buttons |= bmap[cb3] } if cb&SHIFT_INDICATOR != 0 {