Fix drag resize direction bugs and add comprehensive tests

Agent-Logs-Url: https://github.com/kovidgoyal/kitty/sessions/12198e55-3901-439b-9fba-9b5f5b470416

Co-authored-by: kovidgoyal <1308621+kovidgoyal@users.noreply.github.com>
This commit is contained in:
copilot-swe-agent[bot]
2026-04-01 03:48:49 +00:00
committed by GitHub
parent 82df574539
commit 96d10e51a0
3 changed files with 222 additions and 12 deletions

View File

@@ -213,7 +213,7 @@ Detailed list of changes
- macOS: Add Copy and Paste menu items to the Edit menu in the global menu bar (:iss:`9780`)
- Fix dragging of splits layout borders sometimes moving in the wrong direction (:pull:`9447`)
- Fix dragging of splits layout borders sometimes moving in the wrong direction or having no effect (:pull:`9447`)
- Password input in kittens: hide the cursor and display 🔒 (U+1F512) at the end of typed characters to make it visually clear the user is entering a password

View File

@@ -880,12 +880,7 @@ class Splits(Layout):
p = pair
def size_increases_forwards(p: Pair) -> bool:
in_leading_half = not p.is_group_on_second(wg.id)
if p is pair:
return is_leading_edge != in_leading_half
parent = pair_parent_map.get(p) or Pair()
if parent.horizontal != p.horizontal and is_leading_edge:
return True
return not in_leading_half
return is_leading_edge != in_leading_half
def ancestor_with_neighboring_border_of_same_orientation(p: Pair) -> Pair | None:
horizontal = bool(edges & (LEFT_EDGE | RIGHT_EDGE))
@@ -908,11 +903,21 @@ class Splits(Layout):
if p.is_redundant:
continue
if ans.horizontal_id is None and p.horizontal:
p, fwd = pair_or_parent(p)
ans = ans._replace(horizontal_id=id(p), width_increases_rightwards=fwd)
new_p, fwd = pair_or_parent(p)
p = new_p
if not p.horizontal and ans.vertical_id is None:
# pair_or_parent redirected to a vertical pair; use it for vertical resize
ans = ans._replace(vertical_id=id(p), height_increases_downwards=fwd)
else:
ans = ans._replace(horizontal_id=id(p), width_increases_rightwards=fwd)
if ans.vertical_id is None and not p.horizontal:
p, fwd = pair_or_parent(p)
ans = ans._replace(vertical_id=id(p), height_increases_downwards=fwd)
new_p, fwd = pair_or_parent(p)
p = new_p
if p.horizontal and ans.horizontal_id is None:
# pair_or_parent redirected to a horizontal pair; use it for horizontal resize
ans = ans._replace(horizontal_id=id(p), width_increases_rightwards=fwd)
else:
ans = ans._replace(vertical_id=id(p), height_increases_downwards=fwd)
if (parent := pair_parent_map.get(p)) is None:
break
p = parent

View File

@@ -2,7 +2,7 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from kitty.config import defaults
from kitty.fast_data_types import Region
from kitty.fast_data_types import BOTTOM_EDGE, LEFT_EDGE, RIGHT_EDGE, TOP_EDGE, Region
from kitty.layout.base import lgd
from kitty.layout.interface import Grid, Horizontal, Splits, Stack, Tall
from kitty.layout.splits import Pair
@@ -324,3 +324,208 @@ class TestLayout(BaseTest):
result = q.layout_action('maximize', ('horizontal',), all_windows)
self.assertTrue(result)
self.ae(root.bias, root_bias_before)
def test_drag_resize_target_windows(self):
# Helper: call drag_resize_target_windows with given window and edge flags.
def drtw(q, all_windows, window, edges):
return q.drag_resize_target_windows(window, 0, 0, edges, all_windows)
# --- 2-window horizontal split: A | B ---
q = create_layout(Splits)
all_windows = create_windows(q, num=0)
wA = Window(1)
q.add_window(all_windows, wA)
wB = Window(2)
q.add_window(all_windows, wB, location='vsplit')
q(all_windows)
root = q.pairs_root
self.ae(root.horizontal, True)
root_id = id(root)
# Right edge of A (left of divider): divider belongs to root, A is on one-side
d = drtw(q, all_windows, wA, RIGHT_EDGE)
self.ae(d.horizontal_id, root_id)
self.ae(d.width_increases_rightwards, True)
# Left edge of B (right of divider): same divider, same direction
d = drtw(q, all_windows, wB, LEFT_EDGE)
self.ae(d.horizontal_id, root_id)
self.ae(d.width_increases_rightwards, True)
# Right edge of B (outer border): direction reversed
d = drtw(q, all_windows, wB, RIGHT_EDGE)
self.ae(d.horizontal_id, root_id)
self.ae(d.width_increases_rightwards, False)
# --- 2-window vertical split: A / B ---
q = create_layout(Splits)
all_windows = create_windows(q, num=0)
wA = Window(1)
q.add_window(all_windows, wA)
wB = Window(2)
q.add_window(all_windows, wB, location='hsplit')
q(all_windows)
root = q.pairs_root
self.ae(root.horizontal, False)
root_id = id(root)
# Bottom edge of A: divider belongs to root, A is in one-side
d = drtw(q, all_windows, wA, BOTTOM_EDGE)
self.ae(d.vertical_id, root_id)
self.ae(d.height_increases_downwards, True)
# Top edge of B: same divider
d = drtw(q, all_windows, wB, TOP_EDGE)
self.ae(d.vertical_id, root_id)
self.ae(d.height_increases_downwards, True)
# Bottom edge of B (outer border): direction reversed
d = drtw(q, all_windows, wB, BOTTOM_EDGE)
self.ae(d.vertical_id, root_id)
self.ae(d.height_increases_downwards, False)
# --- 3-window layout: top_pair(A | B) / C ---
# root(vertical) -> one: top_pair(horizontal, one=A, two=B), two: C
q = create_layout(Splits)
all_windows = create_windows(q, num=0)
wA = Window(1)
q.add_window(all_windows, wA)
wC = Window(3)
q.add_window(all_windows, wC, location='hsplit') # C below A (vertical root)
all_windows.set_active_window_group_for(wA)
wB = Window(2)
q.add_window(all_windows, wB, location='vsplit') # B right of A (horizontal top_pair)
q(all_windows)
root = q.pairs_root
self.ae(root.horizontal, False)
top_pair = root.one
self.assertIsInstance(top_pair, Pair)
self.ae(top_pair.horizontal, True)
root_id = id(root)
top_pair_id = id(top_pair)
# Divider between A and B: belongs to top_pair
d = drtw(q, all_windows, wA, RIGHT_EDGE)
self.ae(d.horizontal_id, top_pair_id)
self.ae(d.width_increases_rightwards, True)
d = drtw(q, all_windows, wB, LEFT_EDGE)
self.ae(d.horizontal_id, top_pair_id)
self.ae(d.width_increases_rightwards, True)
# Divider between top_pair and C: belongs to root
d = drtw(q, all_windows, wA, BOTTOM_EDGE)
self.ae(d.vertical_id, root_id)
self.ae(d.height_increases_downwards, True)
d = drtw(q, all_windows, wB, BOTTOM_EDGE)
self.ae(d.vertical_id, root_id)
self.ae(d.height_increases_downwards, True)
d = drtw(q, all_windows, wC, TOP_EDGE)
self.ae(d.vertical_id, root_id)
self.ae(d.height_increases_downwards, True)
# --- 3-window layout: A | right_pair(B / C) ---
# root(horizontal) -> one: A, two: right_pair(vertical, one=B, two=C)
q = create_layout(Splits)
all_windows = create_windows(q, num=0)
wA = Window(1)
q.add_window(all_windows, wA)
wB = Window(2)
q.add_window(all_windows, wB, location='vsplit') # B right of A (horizontal root)
wC = Window(3)
q.add_window(all_windows, wC, location='hsplit') # C below B (vertical right_pair)
q(all_windows)
root = q.pairs_root
self.ae(root.horizontal, True)
right_pair = root.two
self.assertIsInstance(right_pair, Pair)
self.ae(right_pair.horizontal, False)
root_id = id(root)
right_pair_id = id(right_pair)
# Divider between A and right_pair: A at RIGHT_EDGE -> root
d = drtw(q, all_windows, wA, RIGHT_EDGE)
self.ae(d.horizontal_id, root_id)
self.ae(d.width_increases_rightwards, True)
# B at LEFT_EDGE: B is on the leading side of right_pair, border belongs to root
d = drtw(q, all_windows, wB, LEFT_EDGE)
self.ae(d.horizontal_id, root_id)
self.ae(d.width_increases_rightwards, True)
# Divider between B and C: belongs to right_pair
d = drtw(q, all_windows, wB, BOTTOM_EDGE)
self.ae(d.vertical_id, right_pair_id)
self.ae(d.height_increases_downwards, True)
d = drtw(q, all_windows, wC, TOP_EDGE)
self.ae(d.vertical_id, right_pair_id)
self.ae(d.height_increases_downwards, True)
# --- 4-window layout (bug scenario): left_pair(A/C) | right_pair(B/D) ---
# root(horizontal) -> one: left_pair(vertical, one=A, two=C),
# two: right_pair(vertical, one=B, two=D)
q = create_layout(Splits)
all_windows = create_windows(q, num=0)
wA = Window(1)
q.add_window(all_windows, wA)
wB = Window(2)
q.add_window(all_windows, wB, location='vsplit') # B right of A
all_windows.set_active_window_group_for(wA)
wC = Window(3)
q.add_window(all_windows, wC, location='hsplit') # C below A
all_windows.set_active_window_group_for(wB)
wD = Window(4)
q.add_window(all_windows, wD, location='hsplit') # D below B
q(all_windows)
root = q.pairs_root
self.ae(root.horizontal, True)
left_pair = root.one
right_pair = root.two
self.assertIsInstance(left_pair, Pair)
self.assertIsInstance(right_pair, Pair)
self.ae(left_pair.horizontal, False)
self.ae(right_pair.horizontal, False)
root_id = id(root)
left_pair_id = id(left_pair)
right_pair_id = id(right_pair)
# Bug #1: A at RIGHT_EDGE should give root with correct (rightward) direction
d = drtw(q, all_windows, wA, RIGHT_EDGE)
self.ae(d.horizontal_id, root_id)
self.ae(d.width_increases_rightwards, True)
# Bug #2: B at LEFT_EDGE should find root and give correct direction
d = drtw(q, all_windows, wB, LEFT_EDGE)
self.ae(d.horizontal_id, root_id)
self.ae(d.width_increases_rightwards, True)
# C at RIGHT_EDGE: same divider between left_pair and right_pair
d = drtw(q, all_windows, wC, RIGHT_EDGE)
self.ae(d.horizontal_id, root_id)
self.ae(d.width_increases_rightwards, True)
# D at LEFT_EDGE: same divider
d = drtw(q, all_windows, wD, LEFT_EDGE)
self.ae(d.horizontal_id, root_id)
self.ae(d.width_increases_rightwards, True)
# Vertical divider within left_pair (between A and C)
d = drtw(q, all_windows, wA, BOTTOM_EDGE)
self.ae(d.vertical_id, left_pair_id)
self.ae(d.height_increases_downwards, True)
d = drtw(q, all_windows, wC, TOP_EDGE)
self.ae(d.vertical_id, left_pair_id)
self.ae(d.height_increases_downwards, True)
# Vertical divider within right_pair (between B and D)
d = drtw(q, all_windows, wB, BOTTOM_EDGE)
self.ae(d.vertical_id, right_pair_id)
self.ae(d.height_increases_downwards, True)
d = drtw(q, all_windows, wD, TOP_EDGE)
self.ae(d.vertical_id, right_pair_id)
self.ae(d.height_increases_downwards, True)