diff --git a/kitty/boss.py b/kitty/boss.py index 6d6832194..103276bc1 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -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()) diff --git a/kitty/layout/base.py b/kitty/layout/base.py index 273e1b9bc..552523817 100644 --- a/kitty/layout/base.py +++ b/kitty/layout/base.py @@ -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() diff --git a/kitty/mouse.c b/kitty/mouse.c index 180b54856..839c7b82f 100644 --- a/kitty/mouse.c +++ b/kitty/mouse.c @@ -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)); } diff --git a/kitty/state.h b/kitty/state.h index b33f53694..da34cbbe6 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -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;