Merge branch 'fix/nested-split-border-resize' of https://github.com/xiaobai050/kitty

This commit is contained in:
Kovid Goyal 2026-09-16 08:42:29 +05:30
commit a11839a2c7
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
3 changed files with 143 additions and 53 deletions

View file

@ -202,6 +202,8 @@ Detailed list of changes
0.49.0 [future]
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
- Splits layout: Fix dragging a border resizing the wrong split in nested layouts.
- Support for :doc:`/custom-shaders` for adding various graphical effects (:iss:`10344`)
- Various throughput performance improvements for a 15-35% real world improvement depending on workload

View file

@ -879,62 +879,36 @@ class Splits(Layout):
edges: int,
all_windows: WindowList,
) -> WindowResizeDragData:
is_right, is_bottom = bool(edges & RIGHT_EDGE), bool(edges & BOTTOM_EDGE)
is_leading_edge = not (is_right or is_bottom)
ans = WindowResizeDragData(None, is_right, None, is_bottom)
if (wg := all_windows.group_for_window(click_window)) is None or (pair := self.pairs_root.pair_for_window(wg.id)) is None:
right, bottom = bool(edges & RIGHT_EDGE), bool(edges & BOTTOM_EDGE)
ans = WindowResizeDragData(None, right, None, bottom)
group = all_windows.group_for_window(click_window)
if group is None:
return ans
path = self.pairs_root.find_window_in_tree(group.id)
if path is None:
return ans
pair_parent_map = {}
for p in self.pairs_root.self_and_descendants():
if isinstance(p.one, Pair):
pair_parent_map[p.one] = p
if isinstance(p.two, Pair):
pair_parent_map[p.two] = p
p = pair
def size_increases_forwards(p: Pair) -> bool:
in_leading_half = not p.is_group_on_second(wg.id)
return is_leading_edge != in_leading_half
def target(horizontal: bool, trailing: bool) -> tuple[int | None, bool]:
fallback = None
for pair, in_one in reversed(path):
if pair.is_redundant or pair.horizontal != horizontal:
continue
if fallback is None:
fallback = id(pair)
# The right/bottom edge of the first child, or left/top edge
# of the second child, belongs to this divider. Otherwise keep
# climbing, even across ancestors with the same split axis.
if in_one == trailing:
return id(pair), True
# Preserve native resizing from the outside edge of a layout.
return fallback, False
def ancestor_with_neighboring_border_of_same_orientation(p: Pair) -> Pair | None:
horizontal = bool(edges & (LEFT_EDGE | RIGHT_EDGE))
while q := pair_parent_map.get(p):
if q.horizontal == horizontal:
if q.between_borders:
return q
break
p = q
return None
def pair_or_parent(p: Pair) -> tuple[Pair, bool]:
in_leading_half = not p.is_group_on_second(wg.id)
if is_leading_edge == in_leading_half and p is pair and (parent := ancestor_with_neighboring_border_of_same_orientation(p)):
# special case for leading edge of one or trailing edge of two with parent being same orientation
return parent, True
return p, size_increases_forwards(p)
while ans.horizontal_id is None or ans.vertical_id is None:
if p.is_redundant:
continue
if ans.horizontal_id is None and p.horizontal:
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:
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
if edges & (LEFT_EDGE | RIGHT_EDGE):
horizontal_id, forwards = target(True, right)
ans = ans._replace(horizontal_id=horizontal_id, width_increases_rightwards=forwards)
if edges & (TOP_EDGE | BOTTOM_EDGE):
vertical_id, forwards = target(False, bottom)
ans = ans._replace(vertical_id=vertical_id, height_increases_downwards=forwards)
return ans
def layout_state(self) -> dict[str, Any]:

View file

@ -855,3 +855,117 @@ class TestLayout(BaseTest):
d = drtw(q, all_windows, wD, TOP_EDGE)
self.ae(d.vertical_id, right_pair_id)
self.ae(d.height_increases_downwards, True)
class TestSplitBorderResize(BaseTest):
def setUp(self):
super().setUp()
self.set_options({'tab_bar_style': 'hidden'})
def make_layout(self, shape):
layout = create_layout(Splits)
windows = create_windows(layout, num=0)
for i in range(1, 5):
layout.add_window(windows, Window(i))
layout.pairs_root.unserialize(shape, lambda x: x)
layout(windows)
return layout, windows
def test_left_center_stack_right_after_resize(self):
layout, windows = self.make_layout(
{
'bias': 1 / 3,
'one': 1,
'two': {'one': {'horizontal': False, 'one': 2, 'two': 3}, 'two': 4},
}
)
root = layout.pairs_root
right_pair = root.two
center = right_pair.one
ws = {w.id: w for w in windows}
# Resize right first, then alternate both sides of the left and right
# dividers and both halves of the central horizontal divider.
for _ in range(3):
for wid, edge, expected in (
(4, LEFT_EDGE, right_pair),
(2, LEFT_EDGE, root),
(3, LEFT_EDGE, root),
(1, RIGHT_EDGE, root),
(2, RIGHT_EDGE, right_pair),
(3, RIGHT_EDGE, right_pair),
(2, BOTTOM_EDGE, center),
(3, TOP_EDGE, center),
):
for increment in (0.04, -0.03):
with self.subTest(wid=wid, edge=edge, increment=increment):
data = layout.drag_resize_target_windows(ws[wid], 0, 0, edge, windows)
horizontal = bool(edge & (LEFT_EDGE | RIGHT_EDGE))
target = data.horizontal_id if horizontal else data.vertical_id
forwards = data.width_increases_rightwards if horizontal else data.height_increases_downwards
self.assertEqual(target, id(expected))
self.assertTrue(forwards)
before = {id(p): p.bias for p in root.self_and_descendants()}
self.assertTrue(layout.drag_resize_window(windows, target, increment, horizontal))
layout(windows)
for p in root.self_and_descendants():
self.assertAlmostEqual(
p.bias,
before[id(p)] + (increment if p is expected else 0),
)
def test_all_rendered_internal_borders(self):
# All 5 binary tree shapes with 4 leaves, with every combination of
# horizontal/vertical ancestors: 40 distinct arrangements.
def shapes(ids):
if len(ids) == 1:
yield ids[0]
return
for n in range(1, len(ids)):
for one in shapes(ids[:n]):
for two in shapes(ids[n:]):
for horizontal in (True, False):
yield {
'horizontal': horizontal,
'bias': 0.39,
'one': one,
'two': two,
}
count = 0
for shape in shapes((1, 2, 3, 4)):
layout, windows = self.make_layout(shape)
ws = {w.id: w for w in windows}
for pair in layout.pairs_root.self_and_descendants():
if pair.is_redundant:
continue
for half in pair.between_borders:
for border in half:
trailing = border.window_id > 0
edge = (RIGHT_EDGE if trailing else LEFT_EDGE) if pair.horizontal else (BOTTOM_EDGE if trailing else TOP_EDGE)
with self.subTest(shape=shape, wid=border.window_id, edge=edge):
data = layout.drag_resize_target_windows(ws[abs(border.window_id)], 0, 0, edge, windows)
target = data.horizontal_id if pair.horizontal else data.vertical_id
forwards = data.width_increases_rightwards if pair.horizontal else data.height_increases_downwards
self.assertEqual(target, id(pair))
self.assertTrue(forwards)
count += 1
self.assertEqual(count, 120)
def test_corner_axes_independent_and_single_pane(self):
layout, windows = self.make_layout(
{
'one': 1,
'two': {'one': {'horizontal': False, 'one': 2, 'two': 3}, 'two': 4},
}
)
ws = {w.id: w for w in windows}
data = layout.drag_resize_target_windows(ws[2], 0, 0, LEFT_EDGE | BOTTOM_EDGE, windows)
self.assertEqual(data.horizontal_id, id(layout.pairs_root))
self.assertEqual(data.vertical_id, id(layout.pairs_root.two.one))
self.assertTrue(data.width_increases_rightwards)
self.assertTrue(data.height_increases_downwards)
layout.pairs_root = Pair()
layout.pairs_root.one = 1
data = layout.drag_resize_target_windows(ws[1], 0, 0, LEFT_EDGE | TOP_EDGE, windows)
self.assertIsNone(data.horizontal_id)
self.assertIsNone(data.vertical_id)