Do drag resize by dragging window borders

This commit is contained in:
Kovid Goyal 2026-02-26 17:35:05 +05:30
parent 852db16fc9
commit ccc0ce5ceb
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
7 changed files with 76 additions and 55 deletions

View file

@ -24,16 +24,17 @@ class Border(NamedTuple):
right: int
bottom: int
color: BorderColor
is_actual_border: bool = False
def vertical_edge(rects: list[Border], color: BorderColor, width: int, top: int, bottom: int, left: int) -> None:
if width > 0:
rects.append(Border(left, top, left + width, bottom, color))
rects.append(Border(left, top, left + width, bottom, color, True))
def horizontal_edge(rects: list[Border], color: BorderColor, height: int, left: int, right: int, top: int) -> None:
if height > 0:
rects.append(Border(left, top, right, top + height, color))
rects.append(Border(left, top, right, top + height, color, True))
def add_borders(rects: list[Border], color: BorderColor, wg: WindowGroup) -> None:
@ -111,10 +112,10 @@ class Borders:
if opts.draw_window_borders_for_single_window and num_visible_groups == 1:
os_window_focused = current_focused_os_window_id() == self.os_window_id
for i, wg in enumerate(groups):
window_bg = color_as_int(wg.default_bg)
window_bg = (window_bg << 8) | BorderColor.window_bg
if draw_borders and not draw_minimal_borders:
if draw_borders and not draw_minimal_borders:
for i, wg in enumerate(groups):
window_bg = color_as_int(wg.default_bg)
window_bg = (window_bg << 8) | BorderColor.window_bg
# Draw the border rectangles
if wg is active_group and draw_active_borders and os_window_focused:
color = BorderColor.active
@ -124,5 +125,5 @@ class Borders:
if draw_minimal_borders:
for border_line in current_layout.get_minimal_borders(all_windows):
rects.append(Border(*border_line.edges, border_line.color))
rects.append(Border(*border_line.edges, border_line.color, is_actual_border=True))
set_borders_rects(self.os_window_id, self.tab_id, rects)

View file

@ -2419,13 +2419,15 @@ 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) -> bool:
if tab := self.active_tab:
horizontal, vertical = tab.current_layout.drag_resize_target_windows(x, y, tab.windows)
if horizontal is None or vertical is None:
return False
def drag_resize_start(
self, horizontal_allowed: bool, vertical_allowed: float, x: float, y: float,
window_id: int, cell_width: int, cell_height: int,
) -> bool:
if (w := self.window_id_map.get(window_id)) and (tab := w.tabref()):
horizontal, vertical = tab.current_layout.drag_resize_target_windows(w, x, y, tab.windows)
self.drag_resize_of_window = WindowResizeDrag(
is_active=True, horizontal_target_window_id=horizontal.id, vertical_target_window_id=vertical.id,
is_active=True, horizontal_target_window_id=horizontal.id if horizontal_allowed else 0,
vertical_target_window_id=vertical.id if vertical_allowed else 0,
cell_width=cell_width, cell_height=cell_height, initial_x=x, initial_y=y)
return True
return False

View file

@ -269,7 +269,7 @@ typedef struct {
} ColorProfile;
typedef struct {
unsigned int width, height;
unsigned width, height;
} CellPixelSize;
typedef struct {int x;} *SPRITE_MAP_HANDLE;

View file

@ -453,36 +453,26 @@ class Layout:
return True
def drag_resize_target_windows(
self, x: float, y: float, all_windows: WindowList
) -> tuple[WindowType | None, WindowType | None]:
# 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:
return None, None
self, click_window: WindowType, x: float, y: float, all_windows: WindowList
) -> tuple[WindowType, WindowType]:
g = click_window.geometry
left_half_clicked = x <= g.left + (g.right - g.left) / 2
top_half_clicked = y <= g.top + (g.bottom - g.top) / 2
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", ())
horizontal_target = vertical_target = click_window
# 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]]

View file

@ -253,6 +253,15 @@ contains_mouse(Window *w) {
return (w->visible && window_left(w) <= x && x < window_right(w) && window_top(w) <= y && y < window_bottom(w));
}
static void
border_contains_mouse(BorderRect *br, int 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 (br->px.right - br->px.left > br->px.bottom - br->px.top) *horizontal = true; else *vertical = true;
}
}
static double
distance_to_window(Window *w) {
double x = global_state.callback_os_window->mouse_x, y = global_state.callback_os_window->mouse_y;
@ -827,17 +836,6 @@ HANDLER(handle_button_event) {
Screen *screen = w->render_data.screen;
if (!screen) return;
if (!global_state.active_drag_resize && button == GLFW_MOUSE_BUTTON_LEFT && !is_release && modifiers == GLFW_MOD_CONTROL) {
RAII_PyObject(r, PyObject_CallMethod(
global_state.boss, "drag_resize_start", "ddII", osw->mouse_x, osw->mouse_y, screen->cell_size.width, screen->cell_size.height));
if (r == NULL) { PyErr_Print(); return; }
if (PyObject_IsTrue(r)) {
global_state.active_drag_resize = w->id;
mouse_cursor_shape = NESW_RESIZE_POINTER;
set_mouse_cursor(mouse_cursor_shape);
return;
}
}
bool a, b;
if (!set_mouse_position(w, &a, &b)) return;
@ -896,7 +894,7 @@ mouse_in_region(Region *r) {
}
static Window*
window_for_event(unsigned int *window_idx, bool *in_tab_bar) {
window_for_event(unsigned int *window_idx, bool *in_tab_bar, int *window_border) {
Region central, tab_bar;
os_window_regions(global_state.callback_os_window, &central, &tab_bar);
const bool in_central = mouse_in_region(&central);
@ -910,6 +908,16 @@ window_for_event(unsigned int *window_idx, bool *in_tab_bar) {
}
if (in_central && global_state.callback_os_window->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;
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);
}
*window_border = 0;
if (horizontal) *window_border |= 1;
if (vertical) *window_border |= 2;
}
for (unsigned int i = 0; i < t->num_windows; i++) {
if (contains_mouse(t->windows + i) && t->windows[i].render_data.screen) {
*window_idx = i;
@ -943,7 +951,7 @@ focus_in_event(void) {
bool in_tab_bar;
unsigned int window_idx = 0;
mouse_cursor_shape = TEXT_POINTER;
Window *w = window_for_event(&window_idx, &in_tab_bar);
Window *w = window_for_event(&window_idx, &in_tab_bar, NULL);
if (w && w->render_data.screen) {
screen_mark_url(w->render_data.screen, 0, 0, 0, 0);
set_mouse_cursor_for_screen(w->render_data.screen);
@ -956,7 +964,7 @@ update_mouse_pointer_shape(void) {
mouse_cursor_shape = TEXT_POINTER;
bool in_tab_bar;
unsigned int window_idx = 0;
Window *w = window_for_event(&window_idx, &in_tab_bar);
Window *w = window_for_event(&window_idx, &in_tab_bar, NULL);
if (in_tab_bar) {
mouse_cursor_shape = POINTER_POINTER;
} else if (w) {
@ -994,7 +1002,7 @@ enter_event(int modifiers) {
// since the last report.
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);
Window *w = window_for_event(&window_idx, &in_tab_bar, NULL);
set_currently_hovered_window(w ? w->id : 0, modifiers);
if (!w || in_tab_bar) return;
@ -1082,7 +1090,7 @@ mouse_event(const int button, int modifiers, int action) {
else { debug("%s mouse_button: %d %s", action == GLFW_RELEASE ? "\x1b[32mRelease\x1b[m" : "\x1b[31mPress\x1b[m", button, format_mods(modifiers)); }
}
if (global_state.redirect_mouse_handling) {
w = window_for_event(&window_idx, &in_tab_bar);
w = window_for_event(&window_idx, &in_tab_bar, NULL);
call_boss(mouse_event, "OK iiii dd",
(in_tab_bar ? Py_True : Py_False), (w ? w->id : 0),
action, modifiers, button, currently_pressed_button(),
@ -1157,7 +1165,8 @@ mouse_event(const int button, int modifiers, int action) {
}
return;
}
w = window_for_event(&window_idx, &in_tab_bar);
int window_border;
w = window_for_event(&window_idx, &in_tab_bar, &window_border);
set_currently_hovered_window(w ? w->id : 0, modifiers);
if (in_tab_bar || global_state.tab_being_dragged.id) {
@ -1167,6 +1176,21 @@ mouse_event(const int button, int modifiers, int action) {
} 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);
if (!w) w = closest_window_for_event(&window_idx);
if (window_border & 1) mouse_cursor_shape = window_border & 2 ? NESW_RESIZE_POINTER : NS_RESIZE_POINTER;
else if (window_border & 2) mouse_cursor_shape = EW_RESIZE_POINTER;
if (w && button == GLFW_MOUSE_BUTTON_LEFT && w->render_data.screen) {
RAII_PyObject(r, PyObject_CallMethod(
global_state.boss, "drag_resize_start", "OOddKII",
window_border & 2 ? Py_True : Py_False, window_border & 1 ? Py_True : Py_False,
osw->mouse_x, osw->mouse_y, w->id,
w->render_data.screen->cell_size.width, w->render_data.screen->cell_size.height));
if (r == NULL) { PyErr_Print(); return; }
if (PyObject_IsTrue(r)) global_state.active_drag_resize = w->id;
}
} 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);
@ -1273,7 +1297,7 @@ scroll_event(const GLFWScrollEvent *ev) {
osw->mouse_x = mouse_x * osw->viewport_x_ratio;
osw->mouse_y = mouse_y * osw->viewport_y_ratio;
}
Window *w = window_for_event(&window_idx, &in_tab_bar);
Window *w = window_for_event(&window_idx, &in_tab_bar, NULL);
if (!w && !in_tab_bar) {
// fallback to last active window
Tab *t = osw->tabs + osw->active_tab;

View file

@ -612,14 +612,16 @@ pyset_borders_rects(PyObject *self UNUSED, PyObject *args) {
ensure_space_for(br, rect_buf, BorderRect, br->num_border_rects + 1, capacity, 32, false);
for (unsigned i = 0; i < br->num_border_rects; i++) {
PyObject *pr = PyList_GET_ITEM(rects, i);
unsigned long left, top, right, bottom, color;
if (!PyArg_ParseTuple(pr, "kkkkk", &left, &top, &right, &bottom, &color)) return NULL;
unsigned long color; int is_actual_border;
BorderRect *r = br->rect_buf + i;
r->left = gl_pos_x(left, osw->viewport_width);
r->top = gl_pos_y(top, osw->viewport_height);
r->right = r->left + gl_size(right - left, osw->viewport_width);
r->bottom = r->top - gl_size(bottom - top, osw->viewport_height);
r->color = color;
if (!PyArg_ParseTuple(
pr, "IIIIkp", &r->px.left, &r->px.top, &r->px.right, &r->px.bottom, &color, &is_actual_border
)) return NULL;
r->left = gl_pos_x(r->px.left, osw->viewport_width);
r->top = gl_pos_y(r->px.top, osw->viewport_height);
r->right = r->left + gl_size(r->px.right - r->px.left, osw->viewport_width);
r->bottom = r->top - gl_size(r->px.bottom - r->px.top, osw->viewport_height);
r->color = color; r->is_actual_border = is_actual_border;
}
END_WITH_TAB
Py_RETURN_NONE;

View file

@ -237,7 +237,9 @@ typedef struct Window {
typedef struct BorderRect {
float left, top, right, bottom;
struct { unsigned left, top, right, bottom; } px;
uint32_t color;
bool is_actual_border;
} BorderRect;
typedef struct BorderRects {