From 32d8ebfbb380964bd3584fe2875335a354b97421 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Thu, 26 Feb 2026 19:07:47 +0530 Subject: [PATCH] Add an option to control dragging to resize windows --- docs/changelog.rst | 3 +++ kitty/mouse.c | 21 +++++++++++++-------- kitty/options/definition.py | 14 +++++++++----- kitty/options/parse.py | 9 ++++++--- kitty/options/to-c-generated.h | 15 +++++++++++++++ kitty/options/types.py | 4 +++- kitty/state.h | 1 + 7 files changed, 50 insertions(+), 17 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index cbbe613b0..15ae2e902 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -164,6 +164,9 @@ Detailed list of changes - Allow dragging tabs in the tab bar to re-order, move to another OS Window or detach (:pull:`9296`) +- Allow dragging window borders to resize kitty windows in all the different + layouts, controlled by :opt:`window_drag_tolerance` (:pull:`9447`) + - choose-files kitten: Fix JXL image preview not working (:iss:`9323`) - Fix tab bar rendering glitches when using :opt:`tab_bar_filter` in some diff --git a/kitty/mouse.c b/kitty/mouse.c index e5a8419fc..a7d7c9ad6 100644 --- a/kitty/mouse.c +++ b/kitty/mouse.c @@ -254,9 +254,9 @@ contains_mouse(Window *w) { } static void -border_contains_mouse(BorderRect *br, int tolerance, bool *horizontal, bool *vertical) { +border_contains_mouse(BorderRect *br, double tolerance, bool *horizontal, bool *vertical) { double x = global_state.callback_os_window->mouse_x, y = global_state.callback_os_window->mouse_y; - if ((int)br->px.left - tolerance <= x && x < br->px.right + tolerance && (int)br->px.top - tolerance <= y && y < br->px.bottom + tolerance) { + if ((int)br->px.left - tolerance <= x && x < (int)br->px.right + tolerance && (int)br->px.top - tolerance <= y && y < (int)br->px.bottom + tolerance) { if (br->px.right - br->px.left > br->px.bottom - br->px.top) *horizontal = true; else *vertical = true; } } @@ -906,13 +906,15 @@ window_for_event(unsigned int *window_idx, bool *in_tab_bar, int *window_border) (tab_bar.bottom > central.bottom && w->mouse_y >= central.bottom) ) *in_tab_bar = true; } - if (in_central && global_state.callback_os_window->num_tabs > 0) { + if (in_central && w->num_tabs > 0) { Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab; if (window_border) { bool horizontal = false, vertical = false; + double dpi = (w->fonts_data->logical_dpi_x + w->fonts_data->logical_dpi_y) / 2.; + double tolerance = ((long)round((OPT(window_drag_tolerance) * (dpi / 72.0)))); for (unsigned i = 0; i < t->border_rects.num_border_rects && !(horizontal && vertical); i++) { BorderRect *br = t->border_rects.rect_buf + i; - if (br->is_actual_border) border_contains_mouse(br, 0, &horizontal, &vertical); + if (br->is_actual_border) border_contains_mouse(br, tolerance, &horizontal, &vertical); } *window_border = 0; if (horizontal) *window_border |= 1; @@ -1173,9 +1175,6 @@ mouse_event(const int button, int modifiers, int action) { mouse_cursor_shape = POINTER_POINTER; handle_tab_bar_mouse(button, modifiers, action); debug("handled by tab bar\n"); - } else if (w) { - debug("grabbed: %d\n", w->render_data.screen->modes.mouse_tracking_mode != 0); - handle_event(w, button, modifiers, window_idx); } else if (window_border) { debug("window border: %d\n", window_border); w = window_for_event(&window_idx, &in_tab_bar, NULL); @@ -1191,6 +1190,9 @@ mouse_event(const int button, int modifiers, int action) { if (r == NULL) { PyErr_Print(); return; } if (PyObject_IsTrue(r)) global_state.active_drag_resize = w->id; } + } else if (w) { + debug("grabbed: %d\n", w->render_data.screen->modes.mouse_tracking_mode != 0); + handle_event(w, button, modifiers, window_idx); } else if (button == GLFW_MOUSE_BUTTON_LEFT && global_state.callback_os_window->mouse_button_pressed[button]) { // initial click, clamp it to the closest window w = closest_window_for_event(&window_idx); @@ -1200,7 +1202,10 @@ mouse_event(const int button, int modifiers, int action) { handle_event(w, button, modifiers, window_idx); clamp_to_window = false; } else debug("no window for event\n"); - } else debug("\n"); + } else { + mouse_cursor_shape = DEFAULT_POINTER; + debug("\n"); + } if (mouse_cursor_shape != old_cursor) set_mouse_cursor(mouse_cursor_shape); } diff --git a/kitty/options/definition.py b/kitty/options/definition.py index e04ee5aff..abf9ed0a9 100644 --- a/kitty/options/definition.py +++ b/kitty/options/definition.py @@ -1457,6 +1457,13 @@ by adding :code:`count-background` to the setting, for example: :code:`-1 count- Note that if you want confirmation when closing individual windows, you can map the :ac:`close_window_with_confirmation` action. ''') + +opt('window_drag_tolerance', '2', option_type='float', ctype='double', long_text=''' +Control dragging window borders to resize kitty windows. This is the tolerance in pts +for the region around window borders where pressing the left mouse button +will start the dragging of window borders. Use a large negative value such as -200 to disable +dragging of borders. +''') egr() # }}} @@ -3847,9 +3854,7 @@ remove the default shortcuts. ''' ) -opt('map_timeout', '0.0', - option_type='positive_float', ctype='time', - long_text=''' +opt('map_timeout', '0.0', option_type='positive_float', long_text=''' The default timeout (in seconds) for multi-key mappings and modal keyboard modes. If you press the first key(s) of a multi-key mapping and don't press the next key within this timeout, the mapping is cancelled and the mode is exited. A value @@ -3862,8 +3867,7 @@ For example:: # This mode will have a 5 second timeout (overrides the global 2 second timeout) map --new-mode resize --timeout 5.0 kitty_mod+r -''' - ) +''') opt('+action_alias', 'launch_tab launch --type=tab --cwd=current', option_type='action_alias', diff --git a/kitty/options/parse.py b/kitty/options/parse.py index 055183fd9..ff7e3f0aa 100644 --- a/kitty/options/parse.py +++ b/kitty/options/parse.py @@ -1060,9 +1060,6 @@ class Parser: def kitty_mod(self, val: str, ans: dict[str, typing.Any]) -> None: ans['kitty_mod'] = to_modifiers(val) - def map_timeout(self, val: str, ans: dict[str, typing.Any]) -> None: - ans['map_timeout'] = positive_float(val) - def linux_bell_theme(self, val: str, ans: dict[str, typing.Any]) -> None: ans['linux_bell_theme'] = str(val) @@ -1123,6 +1120,9 @@ class Parser: def macos_window_resizable(self, val: str, ans: dict[str, typing.Any]) -> None: ans['macos_window_resizable'] = to_bool(val) + def map_timeout(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['map_timeout'] = positive_float(val) + def mark1_background(self, val: str, ans: dict[str, typing.Any]) -> None: ans['mark1_background'] = to_color(val) @@ -1472,6 +1472,9 @@ class Parser: def window_border_width(self, val: str, ans: dict[str, typing.Any]) -> None: ans['window_border_width'] = window_border_width(val) + def window_drag_tolerance(self, val: str, ans: dict[str, typing.Any]) -> None: + ans['window_drag_tolerance'] = float(val) + def window_logo_alpha(self, val: str, ans: dict[str, typing.Any]) -> None: ans['window_logo_alpha'] = unit_float(val) diff --git a/kitty/options/to-c-generated.h b/kitty/options/to-c-generated.h index 0a9d7b2b0..72d5637fa 100644 --- a/kitty/options/to-c-generated.h +++ b/kitty/options/to-c-generated.h @@ -980,6 +980,19 @@ convert_from_opts_resize_in_steps(PyObject *py_opts, Options *opts) { Py_DECREF(ret); } +static void +convert_from_python_window_drag_tolerance(PyObject *val, Options *opts) { + opts->window_drag_tolerance = PyFloat_AsDouble(val); +} + +static void +convert_from_opts_window_drag_tolerance(PyObject *py_opts, Options *opts) { + PyObject *ret = PyObject_GetAttrString(py_opts, "window_drag_tolerance"); + if (ret == NULL) return; + convert_from_python_window_drag_tolerance(ret, opts); + Py_DECREF(ret); +} + static void convert_from_python_tab_bar_edge(PyObject *val, Options *opts) { opts->tab_bar_edge = PyLong_AsLong(val); @@ -1535,6 +1548,8 @@ convert_opts_from_python_opts(PyObject *py_opts, Options *opts) { if (PyErr_Occurred()) return false; convert_from_opts_resize_in_steps(py_opts, opts); if (PyErr_Occurred()) return false; + convert_from_opts_window_drag_tolerance(py_opts, opts); + if (PyErr_Occurred()) return false; convert_from_opts_tab_bar_edge(py_opts, opts); if (PyErr_Occurred()) return false; convert_from_opts_tab_bar_margin_height(py_opts, opts); diff --git a/kitty/options/types.py b/kitty/options/types.py index e21c22c70..8272f0c70 100644 --- a/kitty/options/types.py +++ b/kitty/options/types.py @@ -488,6 +488,7 @@ option_names = ( 'wheel_scroll_multiplier', 'window_alert_on_bell', 'window_border_width', + 'window_drag_tolerance', 'window_logo_alpha', 'window_logo_path', 'window_logo_position', @@ -573,7 +574,6 @@ class Options: input_delay: int = 3 italic_font: FontSpec = FontSpec(family=None, style=None, postscript_name=None, full_name=None, system='auto', axes=(), variable_name=None, features=(), created_from_string='auto') kitty_mod: int = 5 - map_timeout: float = 0.0 linux_bell_theme: str = '__custom' linux_display_server: choices_for_linux_display_server = 'auto' listen_on: str = 'none' @@ -589,6 +589,7 @@ class Options: macos_titlebar_color: int = 0 macos_traditional_fullscreen: bool = False macos_window_resizable: bool = True + map_timeout: float = 0 mark1_background: Color = Color(152, 211, 203) mark1_foreground: Color = Color(0, 0, 0) mark2_background: Color = Color(242, 220, 211) @@ -679,6 +680,7 @@ class Options: wheel_scroll_multiplier: float = 5.0 window_alert_on_bell: bool = True window_border_width: tuple[float, str] = (0.5, 'pt') + window_drag_tolerance: float = 2.0 window_logo_alpha: float = 0.5 window_logo_path: str | None = None window_logo_position: choices_for_window_logo_position = 'bottom-right' diff --git a/kitty/state.h b/kitty/state.h index dfeecba43..bb59cce95 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -141,6 +141,7 @@ typedef struct Options { struct { float thickness; int unit; } underline_exclusion; float box_drawing_scale[4]; double momentum_scroll; + double window_drag_tolerance; } Options; typedef struct WindowLogoRenderData {