mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-05-13 08:26:56 +00:00
Implement maximize layout action for splits layout
Fixes #9629 Fixes #9630
This commit is contained in:
parent
26cf36dd40
commit
551acca0e4
3 changed files with 181 additions and 2 deletions
|
|
@ -184,10 +184,24 @@ define a few extra key bindings in :file:`kitty.conf`::
|
|||
# window's size.
|
||||
map ctrl+. layout_action bias 80
|
||||
|
||||
# Maximize the active window along the horizontal axis (fill full width),
|
||||
# keeping other windows visible in their vertical positions. Press again to
|
||||
# restore the original layout.
|
||||
map ctrl+shift+right layout_action maximize horizontal
|
||||
|
||||
# Maximize the active window along the vertical axis (fill full height),
|
||||
# keeping other windows visible in their horizontal positions. Press again
|
||||
# to restore the original layout.
|
||||
map ctrl+shift+up layout_action maximize vertical
|
||||
|
||||
|
||||
Windows can be resized using :ref:`window_resizing`. You can swap the windows
|
||||
in a split using the ``rotate`` action with an argument of ``180`` and rotate
|
||||
and swap with an argument of ``270``.
|
||||
and swap with an argument of ``270``. The ``maximize`` action expands the active
|
||||
window to fill the maximum available space along a single axis while keeping
|
||||
the rest of the layout intact. Use ``maximize horizontal`` to fill the full
|
||||
width and ``maximize vertical`` to fill the full height. Calling it again
|
||||
restores the original split sizes.
|
||||
|
||||
This layout takes one option, ``split_axis`` that controls whether new windows
|
||||
are placed into vertical or horizontal splits when a :option:`--location
|
||||
|
|
|
|||
|
|
@ -502,6 +502,44 @@ class Pair:
|
|||
return False
|
||||
return self.two.is_group_on_second(gid)
|
||||
|
||||
def find_window_in_tree(self, window_id: int) -> 'list[tuple[Pair, bool]] | None':
|
||||
# Returns list of (pair, is_in_one) from self down to the pair containing window_id.
|
||||
if self.one == window_id:
|
||||
return [(self, True)]
|
||||
if self.two == window_id:
|
||||
return [(self, False)]
|
||||
if isinstance(self.one, Pair):
|
||||
path = self.one.find_window_in_tree(window_id)
|
||||
if path is not None:
|
||||
return [(self, True)] + path
|
||||
if isinstance(self.two, Pair):
|
||||
path = self.two.find_window_in_tree(window_id)
|
||||
if path is not None:
|
||||
return [(self, False)] + path
|
||||
return None
|
||||
|
||||
def path_from_root(self, target: 'Pair') -> 'list[str] | None':
|
||||
if self is target:
|
||||
return []
|
||||
if isinstance(self.one, Pair):
|
||||
sub = self.one.path_from_root(target)
|
||||
if sub is not None:
|
||||
return ['one'] + sub
|
||||
if isinstance(self.two, Pair):
|
||||
sub = self.two.path_from_root(target)
|
||||
if sub is not None:
|
||||
return ['two'] + sub
|
||||
return None
|
||||
|
||||
def pair_at_path(self, path: 'list[str]') -> 'Pair | None':
|
||||
current: Pair = self
|
||||
for step in path:
|
||||
child = current.one if step == 'one' else current.two
|
||||
if not isinstance(child, Pair):
|
||||
return None
|
||||
current = child
|
||||
return current
|
||||
|
||||
|
||||
class SplitsLayoutOpts(LayoutOpts):
|
||||
|
||||
|
|
@ -749,6 +787,46 @@ class Splits(Layout):
|
|||
if pair is not None:
|
||||
pair.set_bias(wg.id, bias)
|
||||
return True
|
||||
elif action_name == 'maximize':
|
||||
args = args or ('horizontal',)
|
||||
axis = args[0]
|
||||
is_horizontal = axis == 'horizontal'
|
||||
wg = all_windows.active_group
|
||||
if wg is not None:
|
||||
key = (wg.id, is_horizontal)
|
||||
maximized_biases: dict[tuple[int, bool], list[tuple[Pair, float]]] = getattr(self, '_maximized_biases', {})
|
||||
if key in maximized_biases:
|
||||
# Already maximized along this axis for this window — toggle back
|
||||
current_pair_ids = {id(p) for p in self.pairs_root.self_and_descendants()}
|
||||
for pair_ref, saved_bias in maximized_biases.pop(key):
|
||||
if id(pair_ref) in current_pair_ids:
|
||||
pair_ref.bias = saved_bias
|
||||
self._maximized_biases = maximized_biases
|
||||
return True
|
||||
else:
|
||||
# Undo any existing maximize along the same axis (different window)
|
||||
stale_keys = [k for k in maximized_biases if k[1] == is_horizontal]
|
||||
if stale_keys:
|
||||
current_pair_ids = {id(p) for p in self.pairs_root.self_and_descendants()}
|
||||
for k in stale_keys:
|
||||
for pair_ref, saved_bias in maximized_biases.pop(k):
|
||||
if id(pair_ref) in current_pair_ids:
|
||||
pair_ref.bias = saved_bias
|
||||
# Maximize: set biases along the path to give maximum space to active window
|
||||
# Only adjust pairs whose split axis matches the requested direction:
|
||||
# horizontal maximize expands width (affects horizontal/side-by-side splits),
|
||||
# vertical maximize expands height (affects vertical/top-bottom splits).
|
||||
tree_path = self.pairs_root.find_window_in_tree(wg.id)
|
||||
if tree_path is not None:
|
||||
saved_biases: list[tuple[Pair, float]] = []
|
||||
for pair, is_in_one in tree_path:
|
||||
if pair.horizontal == is_horizontal and not pair.is_redundant:
|
||||
saved_biases.append((pair, pair.bias))
|
||||
pair.bias = 1.0 if is_in_one else 0.0
|
||||
if saved_biases:
|
||||
maximized_biases[key] = saved_biases
|
||||
self._maximized_biases = maximized_biases
|
||||
return True
|
||||
|
||||
return None
|
||||
|
||||
|
|
@ -818,7 +896,25 @@ class Splits(Layout):
|
|||
return ans
|
||||
|
||||
def layout_state(self) -> dict[str, Any]:
|
||||
return {'pairs': self.pairs_root.serialize()}
|
||||
ans: dict[str, Any] = {'pairs': self.pairs_root.serialize()}
|
||||
maximized_biases: dict[tuple[int, bool], list[tuple[Pair, float]]] = getattr(self, '_maximized_biases', {})
|
||||
if maximized_biases:
|
||||
serialized_maximized = []
|
||||
for (window_id, is_horizontal), saved_biases_list in maximized_biases.items():
|
||||
entries = []
|
||||
for pair_ref, saved_bias in saved_biases_list:
|
||||
path = self.pairs_root.path_from_root(pair_ref)
|
||||
if path is not None:
|
||||
entries.append({'path': path, 'bias': saved_bias})
|
||||
if entries:
|
||||
serialized_maximized.append({
|
||||
'window_id': window_id,
|
||||
'is_horizontal': is_horizontal,
|
||||
'saved_biases': entries,
|
||||
})
|
||||
if serialized_maximized:
|
||||
ans['maximized'] = serialized_maximized
|
||||
return ans
|
||||
|
||||
def set_layout_state(self, layout_state: dict[str, Any], map_group_id: WindowMapper) -> bool:
|
||||
new_root = Pair()
|
||||
|
|
@ -827,5 +923,21 @@ class Splits(Layout):
|
|||
if before == frozenset(new_root.all_window_ids()):
|
||||
self.pairs_root = new_root
|
||||
self.layout_opts = SplitsLayoutOpts(layout_state['opts'])
|
||||
if 'maximized' in layout_state:
|
||||
maximized_biases: dict[tuple[int, bool], list[tuple[Pair, float]]] = {}
|
||||
for entry in layout_state['maximized']:
|
||||
new_window_id = map_group_id(entry['window_id'])
|
||||
if new_window_id is None:
|
||||
continue
|
||||
is_horizontal: bool = entry['is_horizontal']
|
||||
saved_biases_list: list[tuple[Pair, float]] = []
|
||||
for saved in entry['saved_biases']:
|
||||
pair = new_root.pair_at_path(saved['path'])
|
||||
if pair is not None:
|
||||
saved_biases_list.append((pair, saved['bias']))
|
||||
if saved_biases_list:
|
||||
maximized_biases[(new_window_id, is_horizontal)] = saved_biases_list
|
||||
if maximized_biases:
|
||||
self._maximized_biases = maximized_biases
|
||||
return True
|
||||
return False
|
||||
|
|
|
|||
|
|
@ -5,6 +5,7 @@ from kitty.config import defaults
|
|||
from kitty.fast_data_types import Region
|
||||
from kitty.layout.base import lgd
|
||||
from kitty.layout.interface import Grid, Horizontal, Splits, Stack, Tall
|
||||
from kitty.layout.splits import Pair
|
||||
from kitty.types import WindowGeometry
|
||||
from kitty.window import EdgeWidths
|
||||
from kitty.window_list import WindowList, reset_group_id_counter
|
||||
|
|
@ -271,3 +272,55 @@ class TestLayout(BaseTest):
|
|||
self.ae(q.neighbors_for_window(windows[1], all_windows), {'left': [1], 'bottom': [3, 4]})
|
||||
self.ae(q.neighbors_for_window(windows[2], all_windows), {'left': [1], 'right': [4], 'top': [2]})
|
||||
self.ae(q.neighbors_for_window(windows[3], all_windows), {'left': [3], 'top': [2]})
|
||||
|
||||
def test_splits_maximize(self):
|
||||
q = create_layout(Splits)
|
||||
all_windows = create_windows(q, num=0)
|
||||
w1 = Window(1)
|
||||
q.add_window(all_windows, w1)
|
||||
w2 = Window(2)
|
||||
q.add_window(all_windows, w2, location='vsplit')
|
||||
w3 = Window(3)
|
||||
q.add_window(all_windows, w3, location='hsplit')
|
||||
# Layout: w1 | (w2 above w3) — horizontal split at root, vertical split on right
|
||||
root = q.pairs_root
|
||||
# root is horizontal, containing w1 and [w2/w3 vertical pair]
|
||||
self.ae(root.horizontal, True)
|
||||
right_pair = root.two if isinstance(root.two, Pair) else root.one
|
||||
self.assertIsInstance(right_pair, Pair)
|
||||
|
||||
# Focus window 3 (bottom-right)
|
||||
all_windows.set_active_group_idx(all_windows.groups.index(all_windows.group_for_window(w3)))
|
||||
|
||||
# Save original biases
|
||||
root_bias_before = root.bias
|
||||
right_pair_bias_before = right_pair.bias
|
||||
|
||||
# maximize vertical (fill full height) — affects vertical (horizontal==False) pairs
|
||||
result = q.layout_action('maximize', ('vertical',), all_windows)
|
||||
self.assertTrue(result)
|
||||
# right_pair is vertical (horizontal==False) so its bias should be 0.0 (w3 is in 'two')
|
||||
self.ae(right_pair.bias, 0.0)
|
||||
# root is horizontal so its bias should be unchanged
|
||||
self.ae(root.bias, root_bias_before)
|
||||
# _maximized_biases should track w3's vertical maximize
|
||||
self.assertIn((all_windows.active_group.id, False), q._maximized_biases)
|
||||
|
||||
# Toggle back
|
||||
result = q.layout_action('maximize', ('vertical',), all_windows)
|
||||
self.assertTrue(result)
|
||||
self.ae(right_pair.bias, right_pair_bias_before)
|
||||
self.ae(getattr(q, '_maximized_biases', {}), {})
|
||||
|
||||
# maximize horizontal (fill full width) — affects horizontal pairs
|
||||
result = q.layout_action('maximize', ('horizontal',), all_windows)
|
||||
self.assertTrue(result)
|
||||
# root is horizontal, w3 is under root.two (right side), so bias should be 0.0
|
||||
self.ae(root.bias, 0.0)
|
||||
# right_pair is vertical, so unchanged
|
||||
self.ae(right_pair.bias, right_pair_bias_before)
|
||||
|
||||
# Toggle back
|
||||
result = q.layout_action('maximize', ('horizontal',), all_windows)
|
||||
self.assertTrue(result)
|
||||
self.ae(root.bias, root_bias_before)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue