Work on drag resize for splits layout

This commit is contained in:
Kovid Goyal
2026-02-28 07:37:49 +05:30
parent c09f0c07a6
commit 2d1d340d41
7 changed files with 87 additions and 94 deletions

View File

@@ -2427,16 +2427,14 @@ class Boss:
self, edges: int, x: float, y: float, window_id: int, cell_width: int, cell_height: int,
) -> bool:
if (w := self.window_id_map.get(window_id)) and (tab := w.tabref()):
horizontal, width_increases_rightwards, vertical, height_increases_downwards = \
tab.current_layout.drag_resize_target_windows(w, x, y, edges, tab.windows)
horizontal_allowed = bool(edges & (LEFT_EDGE | RIGHT_EDGE))
vertical_allowed = bool(edges & (TOP_EDGE | BOTTOM_EDGE))
data = tab.current_layout.drag_resize_target_windows(w, x, y, edges, tab.windows)
if not edges & (LEFT_EDGE | RIGHT_EDGE):
data = data._replace(horizontal_id=None)
if not edges & (TOP_EDGE | BOTTOM_EDGE):
data = data._replace(vertical_id=None)
self.drag_resize_of_window = WindowResizeDrag(
is_active=True, horizontal_target_window_id=horizontal.id if horizontal_allowed else 0,
vertical_target_window_id=vertical.id if vertical_allowed else 0, tab_id=tab.id,
is_active=True, tab_id=tab.id, data=data,
cell_width=cell_width, cell_height=cell_height, initial_x=x, initial_y=y,
width_increases_rightwards=width_increases_rightwards,
height_increases_downwards=height_increases_downwards,
)
for cw in tab:
cw.pause_resize_notifications_to_child()
@@ -2444,22 +2442,21 @@ class Boss:
return False
def drag_resize_update(self, x: float, y: float) -> None:
if not (r := self.drag_resize_of_window):
if not (r := self.drag_resize_of_window) or not (tab := self.tab_for_id(r.tab_id)):
return
if h := self.window_id_map.get(r.horizontal_target_window_id):
mult = 1 if r.width_increases_rightwards else -1
if (h := r.data.horizontal_id) is not None:
mult = 1 if r.data.width_increases_rightwards else -1
step_x = floor((x - r.initial_x) / r.cell_width) * mult
dx = step_x - r.last_step_x
if dx != 0:
if self.resize_layout_window(h, float(dx), is_horizontal=True) is None:
if tab.drag_resize_window(h, float(dx), True):
self.drag_resize_of_window = r._replace(last_step_x=step_x)
if v := self.window_id_map.get(r.vertical_target_window_id):
mult = 1 if r.height_increases_downwards else -1
if (v := r.data.vertical_id) is not None:
mult = 1 if r.data.height_increases_downwards else -1
step_y = floor((y - r.initial_y) / r.cell_height) * mult
dy = step_y - r.last_step_y
if dy != 0:
if self.resize_layout_window(v, float(dy), is_horizontal=False) is None:
if tab.drag_resize_window(v, float(dy), False):
self.drag_resize_of_window = r._replace(last_step_y=step_y)
def drag_resize_end(self) -> None:

View File

@@ -9,7 +9,7 @@ from typing import Any, Callable, 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
from kitty.options.types import Options
from kitty.types import Edges, NeighborsMap, WindowGeometry, WindowMapper
from kitty.types import Edges, NeighborsMap, WindowGeometry, WindowMapper, WindowResizeDragData
from kitty.typing_compat import WindowType
from kitty.window_list import WindowGroup, WindowList
@@ -267,6 +267,7 @@ class Layout:
if idx is None or not increment:
return False
return self.apply_bias(idx, increment, all_windows, is_horizontal)
drag_resize_window = modify_size_of_window
def parse_layout_opts(self, layout_opts: str | None = None) -> LayoutOpts:
data: dict[str, str] = {}
@@ -454,8 +455,8 @@ class Layout:
def drag_resize_target_windows(
self, click_window: WindowType, x: float, y: float, edges: int, all_windows: WindowList,
) -> tuple[WindowType, bool, WindowType, bool]:
return click_window, bool(edges & RIGHT_EDGE), click_window, bool(edges & BOTTOM_EDGE)
) -> WindowResizeDragData:
return WindowResizeDragData(click_window.id, bool(edges & RIGHT_EDGE), click_window.id, bool(edges & BOTTOM_EDGE))
def serialize(self, all_windows: WindowList) -> dict[str, Any]:
ans = self.layout_state()

View File

@@ -5,8 +5,8 @@ from collections.abc import Collection, Generator, Sequence
from typing import Any, NamedTuple, Optional, TypedDict, Union
from kitty.borders import BorderColor
from kitty.fast_data_types import BOTTOM_EDGE, LEFT_EDGE, RIGHT_EDGE, TOP_EDGE
from kitty.types import Edges, NeighborsMap, WindowGeometry, WindowMapper
from kitty.fast_data_types import BOTTOM_EDGE, RIGHT_EDGE
from kitty.types import Edges, NeighborsMap, WindowGeometry, WindowMapper, WindowResizeDragData
from kitty.typing_compat import EdgeLiteral, WindowType
from kitty.window_list import WindowGroup, WindowList
@@ -460,6 +460,15 @@ class Pair:
else:
yield q
def window_on_second(self, wid: int) -> bool:
if self.one == wid:
return False
if self.two == wid:
return True
if not isinstance(self.two, Pair):
return False
return self.two.window_on_second(wid)
class SplitsLayoutOpts(LayoutOpts):
@@ -706,70 +715,39 @@ class Splits(Layout):
return None
def drag_resize_window(self, all_windows: WindowList, pair_id: int, increment: float, is_horizontal: bool = True) -> bool:
for pair in self.pairs_root.self_and_descendants():
if id(pair) == pair_id:
new_bias = max(0, min(pair.bias + increment, 1))
if new_bias != pair.bias:
pair.bias = new_bias
return True
return False
def drag_resize_target_windows(
self, click_window: WindowType, x: float, y: float, edges: int, all_windows: WindowList,
) -> tuple[WindowType, bool, WindowType, bool]:
horizontal_target = click_window
vertical_target = click_window
width_increases_rightwards = bool(edges & RIGHT_EDGE)
height_increases_downwards = bool(edges & BOTTOM_EDGE)
id_group_map = {g.id: g for g in all_windows.iter_all_layoutable_groups()}
def window_for_id(wid: int) -> WindowType | None:
g = id_group_map.get(wid)
# Use the last (topmost) window in the group, matching the convention
# used elsewhere in the layout code (e.g. tall.py drag_resize_target_windows).
return g.windows[-1] if g and g.windows else None
def select_resize_target(pair: Pair) -> tuple[WindowType | None, bool]:
# Prefer a window stored directly in pair (not inside a sub-Pair) so that
# pair_for_window() returns this pair itself and modify_size_of_child()
# modifies the intended pair's bias without prematurely stopping at an inner
# pair of the same orientation.
# pair.one (left/top) with direction=True: moving right/down grows pair.one.
# pair.two (right/bottom) with direction=False: achieves the same net effect
# because modify_size_of_child negates the increment for which==2.
if isinstance(pair.one, int):
w = window_for_id(pair.one)
if w is not None:
return w, True
if isinstance(pair.two, int):
w = window_for_id(pair.two)
if w is not None:
return w, False
# Both sides are sub-Pairs: best-effort fallback using any window from pair.one
if isinstance(pair.one, Pair):
for wid in pair.one.all_window_ids():
w = window_for_id(wid)
if w is not None:
return w, True
return None, True
for pair in self.pairs_root.self_and_descendants():
if not pair.between_borders:
continue
b0, b1 = pair.between_borders[0], pair.between_borders[1]
if pair.horizontal:
# Horizontal pairs have a vertical border (left/right edges).
# The border covers x in [b0.left, b1.right] and y in [b0.top, b0.bottom].
if edges & (LEFT_EDGE | RIGHT_EDGE):
if b0.left <= x <= b1.right and b0.top <= y <= b0.bottom:
target, direction = select_resize_target(pair)
if target is not None:
horizontal_target = target
width_increases_rightwards = direction
else:
# Vertical pairs have a horizontal border (top/bottom edges).
# The border covers x in [b0.left, b0.right] and y in [b0.top, b1.bottom].
if edges & (TOP_EDGE | BOTTOM_EDGE):
if b0.left <= x <= b0.right and b0.top <= y <= b1.bottom:
target, direction = select_resize_target(pair)
if target is not None:
vertical_target = target
height_increases_downwards = direction
return horizontal_target, width_increases_rightwards, vertical_target, height_increases_downwards
) -> WindowResizeDragData:
is_right, is_bottom = bool(edges & RIGHT_EDGE), bool(edges & BOTTOM_EDGE)
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:
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
while ans.horizontal_id is None or ans.vertical_id is None:
if not p.is_redundant:
if ans.horizontal_id is None and p.horizontal and p.window_on_second(wg.id) != is_right:
ans = ans._replace(horizontal_id=id(p), width_increases_rightwards=not is_right)
if ans.vertical_id is None and not p.horizontal and p.window_on_second(wg.id) != is_bottom:
ans = ans._replace(horizontal_id=id(p))
if (parent := pair_parent_map.get(p)) is None:
break
p = parent
return ans
def layout_state(self) -> dict[str, Any]:
return {'pairs': self.pairs_root.serialize()}

View File

@@ -9,7 +9,7 @@ from typing import Any
from kitty.borders import BorderColor
from kitty.conf.utils import to_bool
from kitty.fast_data_types import BOTTOM_EDGE, RIGHT_EDGE
from kitty.types import Edges, NeighborsMap, WindowMapper
from kitty.types import Edges, NeighborsMap, WindowMapper, WindowResizeDragData
from kitty.typing_compat import EdgeLiteral, WindowType
from kitty.window_list import WindowGroup, WindowList
@@ -33,12 +33,12 @@ def drag_resize_target_windows(
num_full_size_windows: int,
all_windows: WindowList,
main_is_horizontal: bool = True
) -> tuple[WindowType, bool, WindowType, bool]:
) -> WindowResizeDragData:
groups = tuple(all_windows.iter_all_layoutable_groups())
horizontal = vertical = click_window
min_dist = float(sys.maxsize)
height_increases_downwards = bool(edges * BOTTOM_EDGE)
width_increases_rightwards = bool(edges * RIGHT_EDGE)
height_increases_downwards = bool(edges & BOTTOM_EDGE)
width_increases_rightwards = bool(edges & RIGHT_EDGE)
for gr in groups[num_full_size_windows:]:
if gr.windows:
w = gr.windows[-1]
@@ -53,7 +53,7 @@ def drag_resize_target_windows(
min_dist = dist
horizontal = w
width_increases_rightwards = x > g.left + (g.right - g.left) / 2
return horizontal, width_increases_rightwards, vertical, height_increases_downwards
return WindowResizeDragData(horizontal.id, width_increases_rightwards, vertical.id, height_increases_downwards)
def neighbors_for_tall_window(
@@ -391,7 +391,7 @@ class Tall(Layout):
def drag_resize_target_windows(
self, click_window: WindowType, x: float, y: float, edges: int, all_windows: WindowList,
) -> tuple[WindowType, bool, WindowType, bool]:
) -> WindowResizeDragData:
return drag_resize_target_windows(click_window, edges, x, y, self.num_full_size_windows, all_windows, self.main_is_horizontal)

View File

@@ -898,6 +898,13 @@ mouse_in_region(Region *r) {
return true;
}
static unsigned
num_visible_windows(Tab *t) {
unsigned ans = t->num_windows;
for (unsigned i = 0; i < t->num_windows; i++) if (!t->windows[i].visible) ans--;
return ans;
}
static Window*
window_for_event(unsigned int *window_idx, bool *in_tab_bar, Edge *window_border) {
Region central, tab_bar;
@@ -913,7 +920,7 @@ window_for_event(unsigned int *window_idx, bool *in_tab_bar, Edge *window_border
}
if (in_central && w->num_tabs > 0) {
Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab;
if (window_border) {
if (window_border && num_visible_windows(t) > 1) {
*window_border = 0;
double dpi = (w->fonts_data->logical_dpi_x + w->fonts_data->logical_dpi_y) / 2.;
double tolerance = ((long)round((OPT(window_drag_tolerance) * (dpi / 72.0))));

View File

@@ -562,6 +562,12 @@ class Tab: # {{{
return None
return 'Could not resize'
def drag_resize_window(self, object_id: int, increment: float, is_horizontal: bool) -> bool:
increment_as_percent = self.current_layout.bias_increment_for_cell(self.windows, is_horizontal) * increment
if resized := self.current_layout.drag_resize_window(self.windows, object_id, increment_as_percent, is_horizontal):
self.relayout()
return resized
@ac('win', '''
Resize the active window by the specified amount

View File

@@ -243,19 +243,23 @@ class NeighborsMap(TypedDict, total=False):
bottom: list[int]
class WindowResizeDragData(NamedTuple):
horizontal_id: int | None = None
height_increases_downwards: bool = True
vertical_id: int | None = None
width_increases_rightwards: bool = True
class WindowResizeDrag(NamedTuple):
is_active: bool = False
cell_width: int = 0
cell_height: int = 0
horizontal_target_window_id: int = 0
vertical_target_window_id: int = 0
initial_x: float = 0
initial_y: float = 0
last_step_x: float = 0
last_step_y: float = 0
height_increases_downwards: bool = True
width_increases_rightwards: bool = True
tab_id: int = 0
data: WindowResizeDragData = WindowResizeDragData()
def __bool__(self) -> bool:
return self.is_active