mirror of
https://github.com/kovidgoyal/kitty
synced 2026-07-07 09:38:16 +02:00
Add directional drag-and-drop inserts for Vertical, Horizontal, Tall, Fat, Grid
Previously, body drops in all non-Splits layouts showed a full-window overlay
and performed a positional swap. This adds proper top/bottom or left/right
half-window overlays and true before/after insertion for the five layouts
Kovid identified.
Architecture:
- New `drag_overlay_mode` ClassVar on Layout ('full'|'axis_y'|'axis_x'|'free')
controls both overlay display and valid direction axis. Layout subclasses set
one line; tabs.py and boss.py dispatch on this attribute instead of hasattr.
- New `insert_window_group_next_to(target_group_id, after)` on WindowList
performs a positional insert (not swap) by popping the active group and
inserting it before or after the target.
- New base `insert_window_next_to` on Layout uses insert_window_group_next_to
for axis_x/axis_y layouts and falls back to swap for 'full' (Stack).
Splits overrides this with its existing tree-based implementation.
- `_insert_window_in_direction` in boss.py collapses from a 7-line hasattr
branch to a single layout.insert_window_next_to() call.
Direction constraints:
Vertical, Tall, Grid -> top/bottom (axis_y)
Horizontal, Fat -> left/right (axis_x)
Splits -> 4-way free (unchanged)
Stack -> full-window swap (unchanged)
This commit is contained in:
@@ -3340,13 +3340,7 @@ class Boss:
|
||||
layout = src_tab.current_layout
|
||||
horizontal = direction in ('left', 'right')
|
||||
after = direction in ('right', 'bottom')
|
||||
if hasattr(layout, 'insert_window_next_to'):
|
||||
layout.insert_window_next_to(src_tab.windows, window, dest_window, horizontal, after)
|
||||
else:
|
||||
wg_dest = src_tab.windows.group_for_window(dest_window)
|
||||
if wg_dest:
|
||||
src_tab.windows.set_active_window_group_for(window)
|
||||
layout.move_window_to_group(src_tab.windows, wg_dest.id)
|
||||
layout.insert_window_next_to(src_tab.windows, window, dest_window, horizontal, after)
|
||||
src_tab.relayout()
|
||||
|
||||
def _move_tab_to(self, tab: Tab | None = None, target_os_window_id: int | None = None) -> Tab | None:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
from collections.abc import Generator, Iterable, Iterator, Sequence
|
||||
from functools import partial
|
||||
from itertools import repeat
|
||||
from typing import Any, Callable, NamedTuple
|
||||
from typing import Any, Callable, ClassVar, Literal, NamedTuple
|
||||
|
||||
from kitty.borders import BorderColor
|
||||
from kitty.fast_data_types import BOTTOM_EDGE, RIGHT_EDGE, Region, get_options, set_active_window, viewport_for_window
|
||||
@@ -232,6 +232,12 @@ class Layout:
|
||||
must_draw_borders = False # can be overridden to customize behavior from kittens
|
||||
layout_opts = LayoutOpts({})
|
||||
only_active_window_visible = False
|
||||
# Controls drag-and-drop overlay display and valid direction axis for body drops.
|
||||
# 'full' – full-window overlay, positional swap (Stack and any unrecognised layout)
|
||||
# 'axis_y' – top/bottom halves only (Vertical, Tall, Grid)
|
||||
# 'axis_x' – left/right halves only (Horizontal, Fat)
|
||||
# 'free' – 4-way free direction (Splits; handled by its own insert_window_next_to override)
|
||||
drag_overlay_mode: ClassVar[Literal['full', 'axis_y', 'axis_x', 'free']] = 'full'
|
||||
|
||||
def __init__(self, os_window_id: int, tab_id: int, layout_opts: str = '') -> None:
|
||||
self.set_owner(os_window_id, tab_id)
|
||||
@@ -303,6 +309,31 @@ class Layout:
|
||||
def move_window_to_group(self, all_windows: WindowList, group: int) -> bool:
|
||||
return all_windows.move_window_group(to_group=group)
|
||||
|
||||
def insert_window_next_to(
|
||||
self,
|
||||
all_windows: WindowList,
|
||||
window: WindowType,
|
||||
next_to: WindowType,
|
||||
horizontal: bool,
|
||||
after: bool,
|
||||
) -> None:
|
||||
"""Reposition window as a linear neighbour of next_to.
|
||||
|
||||
For axis_x/axis_y layouts this performs a positional insert that preserves
|
||||
the order of all other groups. For 'full' layouts it falls back to a swap.
|
||||
The Splits layout overrides this with tree-based logic.
|
||||
"""
|
||||
src_wg = all_windows.group_for_window(window)
|
||||
dest_wg = all_windows.group_for_window(next_to)
|
||||
if src_wg is None or dest_wg is None or src_wg.id == dest_wg.id:
|
||||
return
|
||||
all_windows.set_active_window_group_for(window)
|
||||
if self.drag_overlay_mode in ('axis_x', 'axis_y'):
|
||||
all_windows.insert_window_group_next_to(dest_wg.id, after)
|
||||
else:
|
||||
# 'full' fallback: swap (preserves existing behaviour for Stack etc.)
|
||||
self.move_window_to_group(all_windows, dest_wg.id)
|
||||
|
||||
def add_window(
|
||||
self, all_windows: WindowList, window: WindowType, location: str | None = None,
|
||||
overlay_for: int | None = None, put_overlay_behind: bool = False, bias: float | None = None,
|
||||
|
||||
@@ -5,7 +5,7 @@ from collections.abc import Callable, Generator, Iterator, Sequence
|
||||
from functools import lru_cache
|
||||
from itertools import repeat
|
||||
from math import ceil, floor
|
||||
from typing import Any
|
||||
from typing import Any, ClassVar, Literal
|
||||
|
||||
from kitty.borders import BorderColor
|
||||
from kitty.types import Edges, NeighborsMap, WindowMapper
|
||||
@@ -34,6 +34,7 @@ class Grid(Layout):
|
||||
|
||||
name: str = 'grid'
|
||||
no_minimal_window_borders = True
|
||||
drag_overlay_mode: ClassVar[Literal['axis_y']] = 'axis_y'
|
||||
|
||||
def remove_all_biases(self) -> bool:
|
||||
self.biased_rows: dict[int, float] = {}
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from collections.abc import Collection, Generator, Iterator, Sequence
|
||||
from typing import Any, Optional, TypedDict, Union
|
||||
from typing import Any, ClassVar, Literal, Optional, TypedDict, Union
|
||||
|
||||
from kitty.borders import BorderColor
|
||||
from kitty.fast_data_types import BOTTOM_EDGE, LEFT_EDGE, RIGHT_EDGE, TOP_EDGE
|
||||
@@ -561,6 +561,7 @@ class Splits(Layout):
|
||||
needs_all_windows = True
|
||||
layout_opts = SplitsLayoutOpts({})
|
||||
no_minimal_window_borders = True
|
||||
drag_overlay_mode: ClassVar[Literal['free']] = 'free'
|
||||
|
||||
@property
|
||||
def default_axis_is_horizontal(self) -> bool | None:
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
import sys
|
||||
from collections.abc import Generator, Iterator, Sequence
|
||||
from itertools import islice, repeat
|
||||
from typing import Any
|
||||
from typing import Any, ClassVar, Literal
|
||||
|
||||
from kitty.borders import BorderColor
|
||||
from kitty.conf.utils import to_bool
|
||||
@@ -136,6 +136,7 @@ class Tall(Layout):
|
||||
name = 'tall'
|
||||
main_is_horizontal = True
|
||||
no_minimal_window_borders = True
|
||||
drag_overlay_mode: ClassVar[Literal['axis_y']] = 'axis_y'
|
||||
layout_opts = TallLayoutOpts({})
|
||||
main_axis_layout = Layout.xlayout
|
||||
perp_axis_layout = Layout.ylayout
|
||||
@@ -381,5 +382,6 @@ class Fat(Tall):
|
||||
|
||||
name = 'fat'
|
||||
main_is_horizontal = False
|
||||
drag_overlay_mode: ClassVar[Literal['axis_x']] = 'axis_x'
|
||||
main_axis_layout = Layout.ylayout
|
||||
perp_axis_layout = Layout.xlayout
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
||||
|
||||
from collections.abc import Generator, Iterable
|
||||
from typing import Any
|
||||
from typing import Any, ClassVar, Literal
|
||||
|
||||
from kitty.borders import BorderColor
|
||||
from kitty.types import Edges, NeighborsMap, WindowMapper
|
||||
@@ -64,6 +64,7 @@ class Vertical(Layout):
|
||||
name = 'vertical'
|
||||
main_is_horizontal = False
|
||||
no_minimal_window_borders = True
|
||||
drag_overlay_mode: ClassVar[Literal['axis_y']] = 'axis_y'
|
||||
main_axis_layout = Layout.ylayout
|
||||
perp_axis_layout = Layout.xlayout
|
||||
|
||||
@@ -155,5 +156,6 @@ class Horizontal(Vertical):
|
||||
|
||||
name = 'horizontal'
|
||||
main_is_horizontal = True
|
||||
drag_overlay_mode: ClassVar[Literal['axis_x']] = 'axis_x'
|
||||
main_axis_layout = Layout.xlayout
|
||||
perp_axis_layout = Layout.ylayout
|
||||
|
||||
@@ -1950,18 +1950,25 @@ class TabManager: # {{{
|
||||
self._set_drag_target_window(dest_window.id, 5)
|
||||
return
|
||||
active_tab = self.active_tab
|
||||
if active_tab is not None and hasattr(active_tab.current_layout, 'insert_window_next_to'):
|
||||
# Splits layout body hover: directional half-window overlay
|
||||
if active_tab is not None:
|
||||
mode = active_tab.current_layout.drag_overlay_mode
|
||||
rel_x = x - central.left
|
||||
g = dest_window.geometry
|
||||
dx = rel_x - (g.left + g.right) / 2
|
||||
dy = rel_y - (g.top + g.bottom) / 2
|
||||
quad_map = {'left': 1, 'right': 2, 'top': 3, 'bottom': 4}
|
||||
direction = ('right' if dx > 0 else 'left') if abs(dx) >= abs(dy) else ('bottom' if dy > 0 else 'top')
|
||||
if mode == 'axis_y':
|
||||
direction = 'bottom' if dy > 0 else 'top'
|
||||
elif mode == 'axis_x':
|
||||
direction = 'right' if dx > 0 else 'left'
|
||||
elif mode == 'free':
|
||||
direction = ('right' if dx > 0 else 'left') if abs(dx) >= abs(dy) else ('bottom' if dy > 0 else 'top')
|
||||
else: # 'full' (Stack, etc.): full-window overlay, no directional highlight
|
||||
self._set_drag_target_window(dest_window.id, 6)
|
||||
return
|
||||
self._set_drag_target_window(dest_window.id, quad_map[direction])
|
||||
else:
|
||||
# All other body hover: full window overlay only (reorder/swap, no title bar flash)
|
||||
self._set_drag_target_window(dest_window.id, 6)
|
||||
self._set_drag_target_window(0)
|
||||
else:
|
||||
self._set_drag_target_window(0)
|
||||
|
||||
@@ -2024,13 +2031,16 @@ class TabManager: # {{{
|
||||
# Cross-tab title bar drop: move to the destination tab
|
||||
boss._move_window_to(w, target_tab_id=active_tab.id)
|
||||
else:
|
||||
# Quadrant-based directional insert
|
||||
g = dest_window.geometry
|
||||
win_cx = (g.left + g.right) / 2
|
||||
win_cy = (g.top + g.bottom) / 2
|
||||
dx = rel_x - win_cx
|
||||
dy = rel_y - win_cy
|
||||
direction: str = ('right' if dx > 0 else 'left') if abs(dx) >= abs(dy) else ('bottom' if dy > 0 else 'top')
|
||||
dx = rel_x - (g.left + g.right) / 2
|
||||
dy = rel_y - (g.top + g.bottom) / 2
|
||||
mode = active_tab.current_layout.drag_overlay_mode
|
||||
if mode == 'axis_y':
|
||||
direction: str = 'bottom' if dy > 0 else 'top'
|
||||
elif mode == 'axis_x':
|
||||
direction = 'right' if dx > 0 else 'left'
|
||||
else: # 'free' (Splits) or 'full' (swap fallback)
|
||||
direction = ('right' if dx > 0 else 'left') if abs(dx) >= abs(dy) else ('bottom' if dy > 0 else 'top')
|
||||
boss._insert_window_in_direction(w, dest_window, direction)
|
||||
|
||||
def update_progress(self) -> None:
|
||||
|
||||
@@ -519,6 +519,27 @@ class WindowList:
|
||||
return True
|
||||
return False
|
||||
|
||||
def insert_window_group_next_to(self, target_group_id: int, after: bool) -> bool:
|
||||
"""Move the active window group immediately before or after target_group_id.
|
||||
|
||||
Unlike move_window_group (which swaps), this is a positional insert that
|
||||
preserves the relative order of all other groups.
|
||||
"""
|
||||
src_idx = self.active_group_idx
|
||||
if src_idx < 0 or not self.groups:
|
||||
return False
|
||||
target_idx = next((i for i, g in enumerate(self.groups) if g.id == target_group_id), -1)
|
||||
if target_idx < 0 or src_idx == target_idx:
|
||||
return False
|
||||
group = self.groups.pop(src_idx)
|
||||
# All indices above src shift down by one after the pop
|
||||
if src_idx < target_idx:
|
||||
target_idx -= 1
|
||||
insert_pos = target_idx + (1 if after else 0)
|
||||
self.groups.insert(insert_pos, group)
|
||||
self.set_active_group_idx(insert_pos)
|
||||
return True
|
||||
|
||||
def compute_needs_borders_map(self, draw_active_borders: bool) -> dict[int, bool]:
|
||||
ag = self.active_group
|
||||
return {gr.id: ((gr is ag and draw_active_borders) or gr.needs_attention) for gr in self.groups}
|
||||
|
||||
Reference in New Issue
Block a user