Extend the SGR Pixel mouse reporting protocol to also report when the mouse leaves the window

This commit is contained in:
Kovid Goyal 2025-07-12 11:59:23 +05:30
parent c3360cd76e
commit 4d3bbd82e0
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
8 changed files with 87 additions and 24 deletions

View file

@ -109,6 +109,8 @@ Detailed list of changes
0.42.2 [future]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- A new :ref:`protocol extension <mouse_leave_window>` 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

View file

@ -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
---------------------------------------

View file

@ -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);

View file

@ -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;
}

View file

@ -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();

View file

@ -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;

View file

@ -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)

View file

@ -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 {