diff --git a/kitty/layout/base.py b/kitty/layout/base.py index f97d87a8f..e7ef83f6e 100644 --- a/kitty/layout/base.py +++ b/kitty/layout/base.py @@ -6,7 +6,7 @@ from functools import partial from itertools import repeat from typing import ( Dict, Generator, Iterable, Iterator, List, NamedTuple, Optional, Sequence, - Tuple, Union, cast + Tuple ) from kitty.constants import Edges, WindowGeometry @@ -261,24 +261,14 @@ class Layout: assert w is not None return self.neighbors_for_window(w, all_windows) - def move_window(self, all_windows: WindowList, delta: Union[str, int] = 1) -> bool: - # delta can be either a number or a string such as 'left', 'top', etc - # for neighborhood moves + def move_window(self, all_windows: WindowList, delta: int = 1) -> bool: if all_windows.num_groups < 2 or not delta: return False - if isinstance(delta, int): - return all_windows.move_window_group(by=delta) - which = delta.lower() - which = {'up': 'top', 'down': 'bottom'}.get(which, which) - w = all_windows.active_window - if w is None: - return False - neighbors = self.neighbors_for_window(w, all_windows) - q: List[int] = cast(List[int], neighbors.get(which, [])) - if not q: - return False - return all_windows.move_window_group(to_group=q[0]) + return all_windows.move_window_group(by=delta) + + def move_window_to_group(self, all_windows: WindowList, group: int) -> bool: + return all_windows.move_window_group(to_group=group) def add_window(self, all_windows: WindowList, window: WindowType, location: Optional[str] = None, overlay_for: Optional[int] = None) -> None: if overlay_for is not None and overlay_for in all_windows: diff --git a/kitty/layout/grid.py b/kitty/layout/grid.py index e16abbf6a..4897bfdee 100644 --- a/kitty/layout/grid.py +++ b/kitty/layout/grid.py @@ -4,6 +4,7 @@ from functools import lru_cache from itertools import repeat +from math import ceil, floor from typing import Callable, Dict, Generator, List, Optional, Sequence, Tuple from kitty.constants import Edges @@ -235,9 +236,17 @@ class Grid(Layout): def side(row: int, col: int, delta: int) -> List[int]: neighbor_col = col + delta - if col_counts[neighbor_col] == col_counts[col]: + neighbor_nrows = col_counts[neighbor_col] + nrows = col_counts[col] + if neighbor_nrows == nrows: return neighbors(row, neighbor_col) - return neighbors(min(row, col_counts[neighbor_col] - 1), neighbor_col) + + start_row = floor(neighbor_nrows * row / nrows) + end_row = ceil(neighbor_nrows * (row + 1) / nrows) + xs = [] + for neighbor_row in range(start_row, end_row): + xs.extend(neighbors(neighbor_row, neighbor_col)) + return xs return { 'top': neighbors(row-1, col) if row else [], diff --git a/kitty/layout/splits.py b/kitty/layout/splits.py index 901dbf85e..293853882 100644 --- a/kitty/layout/splits.py +++ b/kitty/layout/splits.py @@ -244,7 +244,7 @@ class Pair: return parent.modify_size_of_child(which, increment, is_horizontal, layout_object) return False - def neighbors_for_window(self, window_id: int, ans: NeighborsMap, layout_object: 'Splits') -> None: + def neighbors_for_window(self, window_id: int, ans: NeighborsMap, layout_object: 'Splits', all_windows: WindowList) -> None: def quadrant(is_horizontal: bool, is_first: bool) -> Tuple[EdgeLiteral, EdgeLiteral]: if is_horizontal: @@ -255,13 +255,27 @@ class Pair: return 'top', 'bottom' return 'bottom', 'top' + geometries = dict((group.id, group.geometry) for group in all_windows.groups if group.geometry) + def extend(other: Union[int, 'Pair', None], edge: EdgeLiteral, which: EdgeLiteral) -> None: if not ans[which] and other: if isinstance(other, Pair): - ans[which].extend(other.edge_windows(edge)) + neighbors = ( + w for w in other.edge_windows(edge) + if is_neighbouring_geometry(geometries[w], geometries[window_id], which)) + ans[which].extend(neighbors) else: ans[which].append(other) + def is_neighbouring_geometry(a: WindowGeometry, b: WindowGeometry, direction: str) -> bool: + def edges(g: WindowGeometry) -> Tuple[int, int]: + return (g.top, g.bottom) if direction in ['left', 'right'] else (g.left, g.right) + + a1, a2 = edges(a) + b1, b2 = edges(b) + + return a1 < b2 and a2 > b1 + other = self.two if self.one == window_id else self.one extend(other, *quadrant(self.horizontal, self.one == window_id)) @@ -418,15 +432,15 @@ class Splits(Layout): pair = self.pairs_root.pair_for_window(wg.id) ans: NeighborsMap = {'left': [], 'right': [], 'top': [], 'bottom': []} if pair is not None: - pair.neighbors_for_window(wg.id, ans, self) + pair.neighbors_for_window(wg.id, ans, self, all_windows) return ans - def move_window(self, all_windows: WindowList, delta: Union[str, int] = 1) -> bool: + def move_window_to_group(self, all_windows: WindowList, group: int) -> bool: before = all_windows.active_group if before is None: return False before_idx = all_windows.active_group_idx - moved = super().move_window(all_windows, delta) + moved = super().move_window_to_group(all_windows, group) after = all_windows.groups[before_idx] if moved and before.id != after.id: self.pairs_root.swap_windows(before.id, after.id) diff --git a/kitty/tabs.py b/kitty/tabs.py index b582c42b9..e792f4048 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -410,15 +410,36 @@ class Tab: # {{{ prev_window = previous_window - def neighboring_window(self, which: str) -> None: + def most_recent_group(self, groups: List[int]) -> int: + groups_set = set(groups) + + for window_id in reversed(self.windows.active_window_history): + group = self.windows.group_for_window(window_id) + if group and group.id in groups_set: + return group.id + + return groups[0] + + def neighboring_group_id(self, which: str) -> Optional[int]: neighbors = self.current_layout.neighbors(self.windows) - candidates = cast(Optional[Tuple[int, ...]], neighbors.get(which)) + candidates = cast(Optional[List[int]], neighbors.get(which)) if candidates: - self.windows.set_active_group(candidates[0]) + return self.most_recent_group(candidates) + + def neighboring_window(self, which: str) -> None: + neighbor = self.neighboring_group_id(which) + if neighbor: + self.windows.set_active_group(neighbor) def move_window(self, delta: Union[str, int] = 1) -> None: - if self.current_layout.move_window(self.windows, delta): - self.relayout() + if isinstance(delta, int): + if self.current_layout.move_window(self.windows, delta): + self.relayout() + elif isinstance(delta, str): + neighbor = self.neighboring_group_id(delta) + if neighbor: + if self.current_layout.move_window_to_group(self.windows, neighbor): + self.relayout() def move_window_to_top(self) -> None: n = self.windows.num_groups diff --git a/kitty_tests/layout.py b/kitty_tests/layout.py index a9a2d8f14..7d82f372d 100644 --- a/kitty_tests/layout.py +++ b/kitty_tests/layout.py @@ -250,6 +250,10 @@ class TestLayout(BaseTest): self.ae(q.pairs_root.pair_for_window(2).horizontal, False) q.add_window(all_windows, Window(4), location='vsplit') windows = list(all_windows) + windows[0].set_geometry(WindowGeometry(0, 0, 10, 20, 0, 0)) + windows[1].set_geometry(WindowGeometry(11, 0, 20, 10, 0, 0)) + windows[2].set_geometry(WindowGeometry(11, 11, 15, 20, 0, 0)) + windows[3].set_geometry(WindowGeometry(16, 11, 20, 20, 0, 0)) self.ae(q.neighbors_for_window(windows[0], all_windows), {'left': [], 'right': [2, 3], 'top': [], 'bottom': []}) self.ae(q.neighbors_for_window(windows[1], all_windows), {'left': [1], 'right': [], 'top': [], 'bottom': [3, 4]}) self.ae(q.neighbors_for_window(windows[2], all_windows), {'left': [1], 'right': [4], 'top': [2], 'bottom': []})