Merge branch 'mouse-drag-resize' of https://github.com/mcrmck/kitty

This commit is contained in:
Kovid Goyal
2026-02-26 13:53:31 +05:30
4 changed files with 140 additions and 1 deletions

View File

@@ -13,6 +13,7 @@ from collections.abc import Callable, Container, Generator, Iterable, Iterator,
from contextlib import contextmanager, suppress
from functools import partial
from gettext import gettext as _
from math import floor
from gettext import ngettext
from time import sleep
from typing import (
@@ -381,6 +382,7 @@ class Boss:
global_shortcuts: dict[str, SingleKey],
talk_fd: int = -1,
):
self.drag_resize_active = False
self.atexit = Atexit()
set_layout_options(opts)
self.clipboard = Clipboard()
@@ -2415,6 +2417,52 @@ class Boss:
if tab:
tab.set_active_window(window_id)
def drag_resize_start(self, x: float, y: float, cell_width: int, cell_height: int) -> None:
tab = self.active_tab
if not tab:
return
(horizontal, vertical) = tab.current_layout.drag_resize_target_windows(x, y, tab.windows)
self.drag_resize_cell_width = cell_width
self.drag_resize_cell_height = cell_height
self.drag_resize_target_horizontal = horizontal
self.drag_resize_target_vertical = vertical
self.drag_resize_active = True
self.drag_resize_initial_x = x
self.drag_resize_initial_y = y
self.drag_resize_last_step_x = 0
self.drag_resize_last_step_y = 0
def drag_resize_update(self, x: float, y: float) -> None:
if not self.drag_resize_active:
return
if self.drag_resize_target_horizontal is not None:
step_x = floor((x - self.drag_resize_initial_x) / self.drag_resize_cell_width)
dx = step_x - self.drag_resize_last_step_x
if not dx == 0:
self.resize_layout_window(self.drag_resize_target_horizontal, float(dx), True, False)
self.drag_resize_last_step_x = step_x
if self.drag_resize_target_vertical is not None:
step_y = floor((y - self.drag_resize_initial_y) / self.drag_resize_cell_height)
dy = step_y - self.drag_resize_last_step_y
if not dy == 0:
self.resize_layout_window(self.drag_resize_target_vertical, float(dy), False, False)
self.drag_resize_last_step_y = step_y
def drag_resize_end(self) -> None:
self.drag_resize_cell_width = 0
self.drag_resize_cell_height = 0
self.drag_resize_target_horizontal = None
self.drag_resize_target_vertical = None
self.drag_resize_active = False
self.drag_resize_initial_x = 0.0
self.drag_resize_initial_y = 0.0
self.drag_resize_last_step_x = 0
self.drag_resize_last_step_y = 0
def open_kitty_website(self) -> None:
self.open_url(website_url())

View File

@@ -452,6 +452,43 @@ class Layout:
def set_layout_state(self, layout_state: dict[str, Any], map_group_id: WindowMapper) -> bool:
return True
def drag_resize_target_windows(self, x: float, y: float, all_windows: WindowList) -> tuple[WindowType, WindowType]:
# Identify the window where the click occurred and which horizontal and
# vertical half it was in
click_window, left_half_clicked, top_half_clicked = (None, False, False)
for w in all_windows.all_windows:
g = w.geometry
if x >= g.left and x <= g.right and y >= g.top and y <= g.bottom:
click_window = w
left_half_clicked = g.left <= x and x <= g.left + (float(g.right - g.left) / 2.0)
top_half_clicked = g.top <= y and y <= g.top + (float(g.bottom - g.top) / 2.0)
break
if click_window is None:
raise Exception("Failed to determine click window")
neighbors = self.neighbors_for_window(click_window, all_windows)
left = neighbors.get("left", [])
right = neighbors.get("right", [])
top = neighbors.get("top", [])
bottom = neighbors.get("bottom", [])
# Infer which window should be horizontally resized based on click
# position and layout state
horizontal_target = click_window
if ((left_half_clicked and len(left) > 0) or
(not left_half_clicked and len(left) > 0 and len(right) == 0)):
horizontal_target = all_windows.id_map[left[0]]
# Infer which window should be vertically resized based on click
# position and layout state
vertical_target = click_window
if ((top_half_clicked and len(top) > 0) or
(not top_half_clicked and len(top) > 0 and len(bottom) == 0)):
vertical_target = all_windows.id_map[top[0]]
return (horizontal_target, vertical_target)
def serialize(self, all_windows: WindowList) -> dict[str, Any]:
ans = self.layout_state()
ans['opts'] = self.layout_opts.serialized()

View File

@@ -923,6 +923,35 @@ closest_window_for_event(unsigned int *window_idx) {
return ans;
}
static void
drag_resize_start(double mouse_x, double mouse_y, unsigned int cell_width, unsigned int cell_height) {
call_boss(drag_resize_start, "ddII", mouse_x, mouse_y, cell_width, cell_height);
}
static void
drag_resize_update(double mouse_x, double mouse_y) {
call_boss(drag_resize_update, "dd", mouse_x, mouse_y);
}
static void
drag_resize_end(void) {
call_boss(drag_resize_end, "");
}
static bool
is_in_window(double mouse_x, double mouse_y) {
Tab *active_tab = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab;
for (unsigned int i = 0; i < active_tab->num_windows; ++i) {
WindowGeometry *g = &active_tab->windows[i].render_data.geometry;
if (g->left <= mouse_x && mouse_x <= g->right && g->top <= mouse_y && mouse_y <= g->bottom) {
return true;
}
}
return false;
}
void
focus_in_event(void) {
// Ensure that no URL is highlighted and the mouse cursor is in default shape
@@ -1060,6 +1089,31 @@ mouse_event(const int button, int modifiers, int action) {
bool in_tab_bar;
unsigned int window_idx = 0;
Window *w = NULL;
OSWindow *osw = global_state.callback_os_window;
// Handle mouse drag window resizing
if (!global_state.active_drag_resize && button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_PRESS && modifiers & GLFW_MOD_CONTROL) {
if (is_in_window(osw->mouse_x, osw->mouse_y)) {
drag_resize_start(osw->mouse_x, osw->mouse_y, osw->fonts_data->fcm.cell_width, osw->fonts_data->fcm.cell_height);
global_state.active_drag_resize = true;
mouse_cursor_shape = MOVE_POINTER;
set_mouse_cursor(mouse_cursor_shape);
return;
}
} else if (global_state.active_drag_resize) {
if (button == GLFW_MOUSE_BUTTON_LEFT && action == GLFW_RELEASE) {
drag_resize_end();
global_state.active_drag_resize = false;
mouse_cursor_shape = DEFAULT_POINTER;
set_mouse_cursor(mouse_cursor_shape);
return;
} else if (button < 0) {
drag_resize_update(osw->mouse_x, osw->mouse_y);
return;
}
}
if (OPT(debug_keyboard)) {
if (button < 0) { debug("%s x: %.1f y: %.1f ", "\x1b[36mMove\x1b[m", global_state.callback_os_window->mouse_x, global_state.callback_os_window->mouse_y); }
else { debug("%s mouse_button: %d %s", action == GLFW_RELEASE ? "\x1b[32mRelease\x1b[m" : "\x1b[31mPress\x1b[m", button, format_mods(modifiers)); }

View File

@@ -367,7 +367,7 @@ typedef struct GlobalState {
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, mouse_hover_in_window;
id_type active_drag_in_window, tracked_drag_in_window, mouse_hover_in_window, active_drag_resize;
int active_drag_button, tracked_drag_button;
CloseRequest quit_request;
bool redirect_mouse_handling;