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`) - 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 - 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 p = pair
def size_increases_forwards(p: Pair) -> bool: def size_increases_forwards(p: Pair) -> bool:
in_leading_half = not p.is_group_on_second(wg.id) in_leading_half = not p.is_group_on_second(wg.id)
if p is pair: return is_leading_edge != in_leading_half
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
def ancestor_with_neighboring_border_of_same_orientation(p: Pair) -> Pair | None: def ancestor_with_neighboring_border_of_same_orientation(p: Pair) -> Pair | None:
horizontal = bool(edges & (LEFT_EDGE | RIGHT_EDGE)) horizontal = bool(edges & (LEFT_EDGE | RIGHT_EDGE))
@@ -908,11 +903,21 @@ class Splits(Layout):
if p.is_redundant: if p.is_redundant:
continue continue
if ans.horizontal_id is None and p.horizontal: if ans.horizontal_id is None and p.horizontal:
p, fwd = pair_or_parent(p) new_p, fwd = pair_or_parent(p)
ans = ans._replace(horizontal_id=id(p), width_increases_rightwards=fwd) 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: if ans.vertical_id is None and not p.horizontal:
p, fwd = pair_or_parent(p) new_p, fwd = pair_or_parent(p)
ans = ans._replace(vertical_id=id(p), height_increases_downwards=fwd) 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: if (parent := pair_parent_map.get(p)) is None:
break break
p = parent p = parent

View File

@@ -2,7 +2,7 @@
# License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net> # License: GPL v3 Copyright: 2018, Kovid Goyal <kovid at kovidgoyal.net>
from kitty.config import defaults 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.base import lgd
from kitty.layout.interface import Grid, Horizontal, Splits, Stack, Tall from kitty.layout.interface import Grid, Horizontal, Splits, Stack, Tall
from kitty.layout.splits import Pair from kitty.layout.splits import Pair
@@ -324,3 +324,208 @@ class TestLayout(BaseTest):
result = q.layout_action('maximize', ('horizontal',), all_windows) result = q.layout_action('maximize', ('horizontal',), all_windows)
self.assertTrue(result) self.assertTrue(result)
self.ae(root.bias, root_bias_before) 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)