mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-10-20 12:19:36 +00:00
Merge branch 'feature/tab-to-split' of https://github.com/xiaobai050/kitty
This commit is contained in:
commit
bde35f3dd0
6 changed files with 308 additions and 25 deletions
|
|
@ -202,6 +202,10 @@ Detailed list of changes
|
|||
0.49.0 [future]
|
||||
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
|
||||
|
||||
- Allow dropping a tab containing a single window into another tab's layout,
|
||||
using the existing window drop preview and preserving running programs.
|
||||
See :doc:`overview` for details.
|
||||
|
||||
- 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
|
||||
|
|
|
|||
|
|
@ -183,7 +183,13 @@ in your browser, etc. For details, see :doc:`here <open_actions>`.
|
|||
|
||||
Additionally, various bits of the kitty UI itself work with the mouse. You can
|
||||
drag and drop tabs in the tab bar to re-order them or move them from one OS
|
||||
Window to another, or even pop them out into a new OS Window.
|
||||
Window to another, or even pop them out into a new OS Window. A tab containing a
|
||||
single window can also be dropped into the content area of another tab to insert
|
||||
its window at the highlighted edge. In the Splits layout, all four edges are
|
||||
available. The window and any overlays move together, keeping running programs
|
||||
alive; the empty source tab is removed. This also works between OS Windows
|
||||
belonging to the same kitty process. Tabs containing multiple windows keep their
|
||||
existing tab-moving behavior.
|
||||
You can drag window borders to resize windows. You can double click on empty regions
|
||||
of the tab bar to create new tabs or double click on an existing tab to rename
|
||||
it.
|
||||
|
|
|
|||
|
|
@ -2149,8 +2149,19 @@ class Boss:
|
|||
in_tab_bar = tab_bar.left <= x < tab_bar.right and tab_bar.top <= y < tab_bar.bottom
|
||||
detach = not in_tab_bar or tab.os_window_id != tm.os_window_id or is_leave
|
||||
change_drag_thumbnail(tab.os_window_id, 1 if detach else 0)
|
||||
in_content = central.left <= x < central.right and central.top <= y < central.bottom
|
||||
window = tab.active_window
|
||||
merge_window = (
|
||||
window is not None
|
||||
and tab.windows.num_groups == 1
|
||||
and tm.active_tab is not None
|
||||
and tm.active_tab is not tab
|
||||
and in_content
|
||||
and not is_leave
|
||||
)
|
||||
for q in self.all_tab_managers:
|
||||
is_dest = q is tm and (in_tab_bar or os_window_id != tab.os_window_id) and not is_leave
|
||||
q.on_window_drop_move(window.id if merge_window and window is not None else 0, merge_window and q is tm, x, y)
|
||||
is_dest = not merge_window and q is tm and (in_tab_bar or os_window_id != tab.os_window_id) and not is_leave
|
||||
q.on_tab_drop_move(tab_id, is_dest, x, y)
|
||||
window_id, drag_started = get_window_being_dragged()[:2]
|
||||
if window_id and drag_started:
|
||||
|
|
@ -2180,12 +2191,17 @@ class Boss:
|
|||
if (tidb := drop.get(f'application/net.kovidgoyal.kitty-tab-{os.getpid()}')) and (tab := self.tab_for_id(int(tidb))):
|
||||
central, tab_bar = viewport_for_window(os_window_id)[:2]
|
||||
in_tab_bar = tab_bar.left <= x < tab_bar.right and tab_bar.top <= y < tab_bar.bottom
|
||||
if in_tab_bar or tab.os_window_id != tm.os_window_id:
|
||||
in_content = central.left <= x < central.right and central.top <= y < central.bottom
|
||||
window = tab.active_window
|
||||
if in_content and window is not None and tab.windows.num_groups == 1 and tm.active_tab is not None and tm.active_tab is not tab:
|
||||
tm.on_window_drop(x, y, window.id)
|
||||
elif in_tab_bar or tab.os_window_id != tm.os_window_id:
|
||||
tm.on_tab_drop(x, y)
|
||||
else:
|
||||
self._move_tab_to(tab)
|
||||
set_tab_being_dragged()
|
||||
for tm in self.all_tab_managers:
|
||||
tm.on_window_drop_move()
|
||||
tm.on_tab_drop_move()
|
||||
tm.layout_tab_bar() # ensure tab bar is fully updated
|
||||
return
|
||||
|
|
@ -2245,15 +2261,37 @@ class Boss:
|
|||
return
|
||||
if (tab_id := int((data or {}).get(f'application/net.kovidgoyal.kitty-tab-{os.getpid()}', b'0').decode())) and get_tab_being_dragged()[0] == tab_id:
|
||||
tab = self.tab_for_id(tab_id)
|
||||
if tab is not None and needs_toplevel_on_wayland:
|
||||
# The native callback reports was_dropped=False for internal drops.
|
||||
# Wayland can finish the source before delivering the destination's
|
||||
# data, so complete its pending UI target unless the drag was canceled.
|
||||
if not was_canceled and tab is not None and needs_toplevel_on_wayland:
|
||||
for tm in self.all_tab_managers:
|
||||
target = tm.window_being_dropped
|
||||
if (
|
||||
target is not None
|
||||
and tab.windows.num_groups == 1
|
||||
and (window := tab.active_window) is not None
|
||||
and (dest_window := self.window_id_map.get(target.window_id)) is not None
|
||||
):
|
||||
if target.quadrant == 5:
|
||||
self._move_window_to(window, target_tab_id=dest_window.tab_id)
|
||||
else:
|
||||
directions: dict[int, Literal['left', 'right', 'top', 'bottom']] = {1: 'left', 2: 'right', 3: 'top', 4: 'bottom'}
|
||||
self._insert_window_in_direction(window, dest_window, directions.get(target.quadrant, 'right'))
|
||||
set_tab_being_dragged()
|
||||
for manager in self.all_tab_managers:
|
||||
manager.on_window_drop_move()
|
||||
manager.on_tab_drop_move()
|
||||
manager.layout_tab_bar()
|
||||
return
|
||||
if tm.tab_being_dropped:
|
||||
tm.on_tab_drop(0, 0, bypass_move=True)
|
||||
return
|
||||
set_tab_being_dragged()
|
||||
for tm in self.all_tab_managers:
|
||||
tm.on_window_drop_move()
|
||||
tm.on_tab_drop_move()
|
||||
if was_dropped and tab is not None: # detach tab into new OS Window
|
||||
if was_dropped and not was_canceled and tab is not None: # detach tab into new OS Window
|
||||
self._move_tab_to(tab)
|
||||
|
||||
@ac(
|
||||
|
|
@ -3642,16 +3680,16 @@ class Boss:
|
|||
if src_tab is None or dest_tab is None:
|
||||
return
|
||||
with self.suppress_focus_change_events():
|
||||
if src_tab is not dest_tab:
|
||||
target_tab_id = dest_tab.id
|
||||
self._move_window_to(window, target_tab_id=target_tab_id)
|
||||
dest_tab_fresh = self.tab_for_id(dest_tab.id)
|
||||
if dest_tab_fresh is None:
|
||||
return
|
||||
src_tab = dest_tab_fresh
|
||||
layout = src_tab.current_layout
|
||||
horizontal = direction in ('left', 'right')
|
||||
after = direction in ('right', 'bottom')
|
||||
if src_tab is not dest_tab:
|
||||
# Insert directly at the drop target. Attaching at the default
|
||||
# location first would change existing split proportions.
|
||||
dest_tab.attach_windows(src_tab.detach_window(window), next_to=dest_window, horizontal=horizontal, after=after)
|
||||
self._cleanup_tab_after_window_removal(src_tab)
|
||||
dest_tab.make_active()
|
||||
return
|
||||
layout = src_tab.current_layout
|
||||
layout.insert_window_next_to(src_tab.windows, window, dest_window, horizontal, after)
|
||||
src_tab.relayout()
|
||||
|
||||
|
|
|
|||
|
|
@ -1162,7 +1162,9 @@ opt(
|
|||
long_text="""
|
||||
The threshold distance the mouse must move to start a drag and drop. Dragging
|
||||
works for tabs and windows. You can drag tabs to re-order them, detach
|
||||
them into new OS Windows or move them to another OS Window. Similarly,
|
||||
them into new OS Windows or move them to another OS Window. A tab with a single
|
||||
window can also be dropped into another tab's content area to insert that window
|
||||
at the highlighted edge, within the same kitty process. Similarly,
|
||||
by dragging the titlebar of a window (see :ac:`toggle_window_title_bars`)
|
||||
you can re-order it in its layout, detach it or move it to another tab.
|
||||
A value of zero disables all dragging.
|
||||
|
|
|
|||
|
|
@ -977,10 +977,18 @@ class Tab: # {{{
|
|||
attach_window(self.os_window_id, self.id, window.id)
|
||||
self._add_window(window, overlay_for=overlay_for)
|
||||
|
||||
def attach_windows(self, windows: Iterable[Window]) -> None:
|
||||
def attach_windows(self, windows: Iterable[Window], *, next_to: Window | None = None, horizontal: bool = True, after: bool = True) -> None:
|
||||
overlay_for: int | None = None
|
||||
for window in windows:
|
||||
self.attach_window(window, overlay_for)
|
||||
if overlay_for is None and next_to is not None:
|
||||
window.change_tab(self)
|
||||
attach_window(self.os_window_id, self.id, window.id)
|
||||
self.windows.add_window(window)
|
||||
self.current_layout.insert_window_next_to(self.windows, window, next_to, horizontal, after)
|
||||
self.mark_tab_bar_dirty()
|
||||
self.relayout()
|
||||
else:
|
||||
self.attach_window(window, overlay_for)
|
||||
overlay_for = window.id
|
||||
|
||||
def set_active_window(self, x: Window | int, for_keep_focus: Window | None = None) -> None:
|
||||
|
|
@ -2060,8 +2068,8 @@ class TabManager: # {{{
|
|||
central = viewport_for_window(self.os_window_id)[0]
|
||||
if not (central.left <= x < central.right and central.top <= y < central.bottom):
|
||||
return None
|
||||
rel_x = x - central.left
|
||||
rel_y = y - central.top
|
||||
# Window geometry is already relative to the OS window.
|
||||
rel_x, rel_y = x, y
|
||||
if (active_tab := self.active_tab) is None:
|
||||
return None
|
||||
for win in active_tab:
|
||||
|
|
@ -2119,10 +2127,7 @@ class TabManager: # {{{
|
|||
self._set_drag_target_tab(0)
|
||||
dest_window = self._find_window_at(x, y)
|
||||
if dest_window and dest_window.id != window_id:
|
||||
from .fast_data_types import viewport_for_window as _vfw
|
||||
|
||||
central = _vfw(self.os_window_id)[0]
|
||||
rel_y = y - central.top
|
||||
rel_y = y
|
||||
if dest_window.show_title_bar:
|
||||
from .fast_data_types import cell_size_for_window
|
||||
|
||||
|
|
@ -2136,7 +2141,7 @@ class TabManager: # {{{
|
|||
return
|
||||
active_tab = self.active_tab
|
||||
if active_tab is not None:
|
||||
rel_x = x - central.left
|
||||
rel_x = x
|
||||
g = dest_window.geometry
|
||||
dx = rel_x - (g.left + g.right) / 2
|
||||
dy = rel_y - (g.top + g.bottom) / 2
|
||||
|
|
@ -2189,8 +2194,8 @@ class TabManager: # {{{
|
|||
if not in_central:
|
||||
return
|
||||
|
||||
rel_x = x - central.left
|
||||
rel_y = y - central.top
|
||||
# Window geometry is already relative to the OS window.
|
||||
rel_x, rel_y = x, y
|
||||
if (active_tab := self.active_tab) is None:
|
||||
return
|
||||
|
||||
|
|
|
|||
228
kitty_tests/tab_drop.py
Normal file
228
kitty_tests/tab_drop.py
Normal file
|
|
@ -0,0 +1,228 @@
|
|||
#!/usr/bin/env python
|
||||
# License: GPL v3 Copyright: 2026, kitty contributors
|
||||
|
||||
import os
|
||||
from contextlib import nullcontext
|
||||
from types import SimpleNamespace
|
||||
from unittest.mock import MagicMock, Mock, patch
|
||||
|
||||
from kitty.boss import Boss
|
||||
from kitty.layout.splits import Splits
|
||||
from kitty.tabs import DragOverlayMode, Tab, TabManager
|
||||
|
||||
from .base import BaseTest
|
||||
from .layout import Window, create_layout, create_windows
|
||||
|
||||
|
||||
class TestTabDrop(BaseTest):
|
||||
def setUp(self):
|
||||
super().setUp()
|
||||
self.source = SimpleNamespace(id=1, os_window_id=1, windows=SimpleNamespace(num_groups=1), active_window=SimpleNamespace(id=10))
|
||||
self.destination = SimpleNamespace(id=2)
|
||||
self.tm = SimpleNamespace(
|
||||
os_window_id=1,
|
||||
active_tab=self.destination,
|
||||
on_tab_drop_move=Mock(),
|
||||
on_window_drop_move=Mock(),
|
||||
on_window_drop=Mock(),
|
||||
on_tab_drop=Mock(),
|
||||
layout_tab_bar=Mock(),
|
||||
tab_being_dropped=None,
|
||||
window_being_dropped=None,
|
||||
)
|
||||
self.boss = SimpleNamespace(
|
||||
os_window_map={1: self.tm},
|
||||
all_tab_managers=[self.tm],
|
||||
tab_for_id=lambda tid: self.source if tid == 1 else None,
|
||||
_move_tab_to=Mock(),
|
||||
_move_window_to=Mock(),
|
||||
_insert_window_in_direction=Mock(),
|
||||
window_id_map={},
|
||||
)
|
||||
self.data = {f'application/net.kovidgoyal.kitty-tab-{os.getpid()}': b'1'}
|
||||
self.region = SimpleNamespace(left=0, right=1000, top=30, bottom=1000)
|
||||
bar = SimpleNamespace(left=0, right=1000, top=0, bottom=30)
|
||||
for name, value in (
|
||||
('viewport_for_window', Mock(return_value=(self.region, bar))),
|
||||
('get_tab_being_dragged', Mock(return_value=(1, True))),
|
||||
('get_window_being_dragged', Mock(return_value=(0, False))),
|
||||
('set_tab_being_dragged', Mock()),
|
||||
('change_drag_thumbnail', Mock()),
|
||||
):
|
||||
patcher = patch('kitty.boss.' + name, value)
|
||||
patcher.start()
|
||||
self.addCleanup(patcher.stop)
|
||||
setattr(self, name, value)
|
||||
|
||||
def test_single_window_tab_preview_and_drop(self):
|
||||
Boss.on_drop_move(self.boss, 1, 600, 400, True, False)
|
||||
self.tm.on_window_drop_move.assert_called_with(10, True, 600, 400)
|
||||
self.tm.on_tab_drop_move.assert_called_with(1, False, 600, 400)
|
||||
Boss.on_drop(self.boss, 1, self.data, True, 600, 400)
|
||||
self.tm.on_window_drop.assert_called_once_with(600, 400, 10)
|
||||
self.boss._move_tab_to.assert_not_called()
|
||||
self.tm.on_window_drop_move.assert_called_with()
|
||||
self.set_tab_being_dragged.assert_called_once_with()
|
||||
|
||||
def test_single_window_tab_cross_os_window(self):
|
||||
self.source.os_window_id = 9
|
||||
Boss.on_drop_move(self.boss, 1, 600, 400, True, False)
|
||||
self.tm.on_window_drop_move.assert_called_with(10, True, 600, 400)
|
||||
Boss.on_drop(self.boss, 1, self.data, True, 600, 400)
|
||||
self.tm.on_window_drop.assert_called_once_with(600, 400, 10)
|
||||
self.tm.on_tab_drop.assert_not_called()
|
||||
|
||||
def test_tab_bar_drop_keeps_tab_behavior(self):
|
||||
Boss.on_drop_move(self.boss, 1, 600, 10, True, False)
|
||||
self.tm.on_window_drop_move.assert_called_with(0, False, 600, 10)
|
||||
self.tm.on_tab_drop_move.assert_called_with(1, True, 600, 10)
|
||||
Boss.on_drop(self.boss, 1, self.data, True, 600, 10)
|
||||
self.tm.on_tab_drop.assert_called_once_with(600, 10)
|
||||
self.tm.on_window_drop.assert_not_called()
|
||||
|
||||
def test_multi_window_tab_keeps_tab_behavior(self):
|
||||
self.source.windows.num_groups = 2
|
||||
Boss.on_drop_move(self.boss, 1, 600, 400, True, False)
|
||||
self.tm.on_window_drop_move.assert_called_with(0, False, 600, 400)
|
||||
Boss.on_drop(self.boss, 1, self.data, True, 600, 400)
|
||||
self.boss._move_tab_to.assert_called_once_with(self.source)
|
||||
self.tm.on_window_drop.assert_not_called()
|
||||
self.source.os_window_id = 9
|
||||
Boss.on_drop(self.boss, 1, self.data, True, 600, 400)
|
||||
self.tm.on_tab_drop.assert_called_once_with(600, 400)
|
||||
|
||||
def test_tab_drop_into_self_keeps_tab_behavior(self):
|
||||
self.tm.active_tab = self.source
|
||||
Boss.on_drop_move(self.boss, 1, 600, 400, True, False)
|
||||
self.tm.on_window_drop_move.assert_called_with(0, False, 600, 400)
|
||||
Boss.on_drop(self.boss, 1, self.data, True, 600, 400)
|
||||
self.boss._move_tab_to.assert_called_once_with(self.source)
|
||||
self.tm.on_window_drop.assert_not_called()
|
||||
|
||||
def test_leaving_clears_window_preview(self):
|
||||
Boss.on_drop_move(self.boss, 1, 600, 400, True, True)
|
||||
self.tm.on_window_drop_move.assert_called_with(0, False, 600, 400)
|
||||
self.tm.on_tab_drop_move.assert_called_with(1, False, 600, 400)
|
||||
|
||||
def test_cancel_clears_preview_without_detaching(self):
|
||||
for reported_drop in (True, False):
|
||||
Boss.on_drag_source_finished(self.boss, reported_drop, True, '', 0, self.data, True)
|
||||
self.tm.on_window_drop_move.assert_called_with()
|
||||
self.boss._move_tab_to.assert_not_called()
|
||||
self.boss._insert_window_in_direction.assert_not_called()
|
||||
|
||||
def test_wayland_finish_uses_pending_window_target(self):
|
||||
target = SimpleNamespace(id=20, tab_id=2)
|
||||
self.boss.window_id_map[20] = target
|
||||
for quadrant, direction in enumerate(('left', 'right', 'top', 'bottom'), 1):
|
||||
self.tm.window_being_dropped = SimpleNamespace(window_id=20, quadrant=quadrant)
|
||||
Boss.on_drag_source_finished(self.boss, True, False, '', 0, self.data, True)
|
||||
self.boss._insert_window_in_direction.assert_called_with(self.source.active_window, target, direction)
|
||||
self.tm.on_window_drop_move.assert_called_with()
|
||||
self.boss._move_tab_to.assert_not_called()
|
||||
self.tm.window_being_dropped.quadrant = 5
|
||||
Boss.on_drag_source_finished(self.boss, True, False, '', 0, self.data, True)
|
||||
self.boss._move_window_to.assert_called_once_with(self.source.active_window, target_tab_id=2)
|
||||
|
||||
def test_wayland_internal_drop_finishes_before_data_arrives(self):
|
||||
target = SimpleNamespace(id=20, tab_id=2)
|
||||
self.boss.window_id_map[20] = target
|
||||
self.tm.window_being_dropped = SimpleNamespace(window_id=20, quadrant=1)
|
||||
# glfw.c reports was_dropped=False while an internal drop destination
|
||||
# exists, even for a successful drop. Data can arrive after this callback.
|
||||
Boss.on_drag_source_finished(self.boss, False, False, '', 0, self.data, True)
|
||||
self.boss._insert_window_in_direction.assert_called_once_with(self.source.active_window, target, 'left')
|
||||
self.boss._move_tab_to.assert_not_called()
|
||||
|
||||
def test_wayland_internal_tab_reordering_is_preserved(self):
|
||||
self.tm.tab_being_dropped = SimpleNamespace()
|
||||
Boss.on_drag_source_finished(self.boss, False, False, '', 0, self.data, True)
|
||||
self.tm.on_tab_drop.assert_called_once_with(0, 0, bypass_move=True)
|
||||
self.boss._move_tab_to.assert_not_called()
|
||||
|
||||
def test_completed_drop_is_not_detached_again(self):
|
||||
self.get_tab_being_dragged.return_value = (0, False)
|
||||
Boss.on_drag_source_finished(self.boss, True, False, '', 0, self.data, True)
|
||||
self.boss._move_tab_to.assert_not_called()
|
||||
|
||||
def test_directional_attach_preserves_other_split_weights_and_overlays(self):
|
||||
for horizontal in (True, False):
|
||||
for after in (True, False):
|
||||
with self.subTest(horizontal=horizontal, after=after):
|
||||
layout = create_layout(Splits)
|
||||
windows = create_windows(layout, num=0)
|
||||
for wid in (1, 2, 3):
|
||||
layout.add_window(windows, Window(wid), location='vsplit')
|
||||
layout.pairs_root.unserialize({'bias': 0.2, 'one': 1, 'two': {'bias': 0.375, 'one': 2, 'two': 3}}, lambda x: x)
|
||||
tab = SimpleNamespace(id=2, os_window_id=1, current_layout=layout, windows=windows, mark_tab_bar_dirty=Mock(), relayout=Mock())
|
||||
pane, overlay = Window(4), Window(5)
|
||||
for window in (pane, overlay):
|
||||
window.change_tab = Mock()
|
||||
tab._add_window = lambda window, overlay_for: layout.add_window(windows, window, overlay_for=overlay_for)
|
||||
tab.attach_window = lambda window, overlay_for: Tab.attach_window(tab, window, overlay_for)
|
||||
with patch('kitty.tabs.attach_window') as native_attach:
|
||||
Tab.attach_windows(tab, (pane, overlay), next_to=windows.id_map[2], horizontal=horizontal, after=after)
|
||||
self.ae(native_attach.call_count, 2)
|
||||
root = layout.pairs_root
|
||||
self.assertAlmostEqual(root.bias, 0.2)
|
||||
self.assertAlmostEqual(root.two.bias, 0.375)
|
||||
split = root.two.one
|
||||
self.ae(split.horizontal, horizontal)
|
||||
self.ae((split.one, split.two), (2, 4) if after else (4, 2))
|
||||
self.assertAlmostEqual(split.bias, 0.5)
|
||||
self.assertIs(windows.group_for_window(pane), windows.group_for_window(overlay))
|
||||
|
||||
def test_cross_tab_directional_insert_uses_direct_attachment(self):
|
||||
source = SimpleNamespace(detach_window=Mock(return_value=('pane', 'overlay')))
|
||||
dest = SimpleNamespace(attach_windows=Mock(), make_active=Mock())
|
||||
pane = SimpleNamespace(tabref=lambda: source)
|
||||
target = SimpleNamespace(tabref=lambda: dest)
|
||||
boss = SimpleNamespace(suppress_focus_change_events=nullcontext, _cleanup_tab_after_window_removal=Mock())
|
||||
for direction, horizontal, after in (('left', True, False), ('right', True, True), ('top', False, False), ('bottom', False, True)):
|
||||
Boss._insert_window_in_direction(boss, pane, target, direction)
|
||||
dest.attach_windows.assert_called_with(('pane', 'overlay'), next_to=target, horizontal=horizontal, after=after)
|
||||
boss._cleanup_tab_after_window_removal.assert_called_with(source)
|
||||
dest.make_active.assert_called_with()
|
||||
|
||||
|
||||
class TestWindowDropCoordinates(BaseTest):
|
||||
def test_drop_with_offset_tab_bar(self):
|
||||
for left, top in ((0, 30), (80, 0), (80, 30)):
|
||||
for direction, quadrant in (('left', 1), ('right', 2), ('top', 3), ('bottom', 4)):
|
||||
with self.subTest(left=left, top=top, direction=direction):
|
||||
central = SimpleNamespace(left=left, top=top, right=1000, bottom=800)
|
||||
bar = SimpleNamespace(left=0, top=0, right=left or 1000, bottom=top or 800)
|
||||
g = SimpleNamespace(left=left + 5, top=top + 5, right=995, bottom=795)
|
||||
dest = SimpleNamespace(id=20, geometry=g, show_title_bar=False)
|
||||
src = SimpleNamespace(id=10, tabref=lambda: None)
|
||||
tab = MagicMock(current_layout=SimpleNamespace(drag_overlay_mode=DragOverlayMode.free))
|
||||
tab.__iter__.return_value = [dest]
|
||||
tm = SimpleNamespace(
|
||||
os_window_id=1,
|
||||
active_tab=tab,
|
||||
window_drag_over_me=True,
|
||||
_set_drag_target_tab=Mock(),
|
||||
_set_drag_target_window=Mock(),
|
||||
_clear_force_show_title_bars=Mock(),
|
||||
mark_tab_bar_dirty=Mock(),
|
||||
)
|
||||
tm._find_window_at = lambda x, y: TabManager._find_window_at(tm, x, y)
|
||||
boss = SimpleNamespace(window_id_map={10: src}, _insert_window_in_direction=Mock(), _move_window_to=Mock())
|
||||
x, y = {
|
||||
'left': (g.left + 2, (g.top + g.bottom) // 2),
|
||||
'right': (g.right - 2, (g.top + g.bottom) // 2),
|
||||
'top': ((g.left + g.right) // 2, g.top + 2),
|
||||
'bottom': ((g.left + g.right) // 2, g.bottom - 2),
|
||||
}[direction]
|
||||
with (
|
||||
patch('kitty.fast_data_types.viewport_for_window', return_value=(central, bar)),
|
||||
patch('kitty.fast_data_types.cell_size_for_window', return_value=(10, 20)),
|
||||
patch('kitty.tabs.get_options', return_value=SimpleNamespace(window_title_bar='top')),
|
||||
patch('kitty.tabs.get_boss', return_value=boss),
|
||||
patch('kitty.tabs.set_window_being_dragged'),
|
||||
):
|
||||
TabManager.on_window_drop_move(tm, 10, True, x, y)
|
||||
tm._set_drag_target_window.assert_called_with(20, quadrant)
|
||||
TabManager.on_window_drop(tm, x, y, 10)
|
||||
boss._insert_window_in_direction.assert_called_once_with(src, dest, direction)
|
||||
boss._move_window_to.assert_not_called()
|
||||
Loading…
Add table
Add a link
Reference in a new issue