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, self, edges: int, x: float, y: float, window_id: int, cell_width: int, cell_height: int,
) -> bool: ) -> bool:
if (w := self.window_id_map.get(window_id)) and (tab := w.tabref()): if (w := self.window_id_map.get(window_id)) and (tab := w.tabref()):
horizontal, width_increases_rightwards, vertical, height_increases_downwards = \ data = tab.current_layout.drag_resize_target_windows(w, x, y, edges, tab.windows)
tab.current_layout.drag_resize_target_windows(w, x, y, edges, tab.windows) if not edges & (LEFT_EDGE | RIGHT_EDGE):
horizontal_allowed = bool(edges & (LEFT_EDGE | RIGHT_EDGE)) data = data._replace(horizontal_id=None)
vertical_allowed = bool(edges & (TOP_EDGE | BOTTOM_EDGE)) if not edges & (TOP_EDGE | BOTTOM_EDGE):
data = data._replace(vertical_id=None)
self.drag_resize_of_window = WindowResizeDrag( self.drag_resize_of_window = WindowResizeDrag(
is_active=True, horizontal_target_window_id=horizontal.id if horizontal_allowed else 0, is_active=True, tab_id=tab.id, data=data,
vertical_target_window_id=vertical.id if vertical_allowed else 0, tab_id=tab.id,
cell_width=cell_width, cell_height=cell_height, initial_x=x, initial_y=y, 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: for cw in tab:
cw.pause_resize_notifications_to_child() cw.pause_resize_notifications_to_child()
@@ -2444,22 +2442,21 @@ class Boss:
return False return False
def drag_resize_update(self, x: float, y: float) -> None: 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 return
if h := self.window_id_map.get(r.horizontal_target_window_id): if (h := r.data.horizontal_id) is not None:
mult = 1 if r.width_increases_rightwards else -1 mult = 1 if r.data.width_increases_rightwards else -1
step_x = floor((x - r.initial_x) / r.cell_width) * mult step_x = floor((x - r.initial_x) / r.cell_width) * mult
dx = step_x - r.last_step_x dx = step_x - r.last_step_x
if dx != 0: 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) self.drag_resize_of_window = r._replace(last_step_x=step_x)
if (v := r.data.vertical_id) is not None:
if v := self.window_id_map.get(r.vertical_target_window_id): mult = 1 if r.data.height_increases_downwards else -1
mult = 1 if r.height_increases_downwards else -1
step_y = floor((y - r.initial_y) / r.cell_height) * mult step_y = floor((y - r.initial_y) / r.cell_height) * mult
dy = step_y - r.last_step_y dy = step_y - r.last_step_y
if dy != 0: 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) self.drag_resize_of_window = r._replace(last_step_y=step_y)
def drag_resize_end(self) -> None: 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.borders import BorderColor
from kitty.fast_data_types import BOTTOM_EDGE, RIGHT_EDGE, Region, get_options, set_active_window, viewport_for_window 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.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.typing_compat import WindowType
from kitty.window_list import WindowGroup, WindowList from kitty.window_list import WindowGroup, WindowList
@@ -267,6 +267,7 @@ class Layout:
if idx is None or not increment: if idx is None or not increment:
return False return False
return self.apply_bias(idx, increment, all_windows, is_horizontal) 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: def parse_layout_opts(self, layout_opts: str | None = None) -> LayoutOpts:
data: dict[str, str] = {} data: dict[str, str] = {}
@@ -454,8 +455,8 @@ class Layout:
def drag_resize_target_windows( def drag_resize_target_windows(
self, click_window: WindowType, x: float, y: float, edges: int, all_windows: WindowList, self, click_window: WindowType, x: float, y: float, edges: int, all_windows: WindowList,
) -> tuple[WindowType, bool, WindowType, bool]: ) -> WindowResizeDragData:
return click_window, bool(edges & RIGHT_EDGE), click_window, bool(edges & BOTTOM_EDGE) 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]: def serialize(self, all_windows: WindowList) -> dict[str, Any]:
ans = self.layout_state() 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 typing import Any, NamedTuple, Optional, TypedDict, Union
from kitty.borders import BorderColor from kitty.borders import BorderColor
from kitty.fast_data_types import BOTTOM_EDGE, LEFT_EDGE, RIGHT_EDGE, TOP_EDGE from kitty.fast_data_types import BOTTOM_EDGE, RIGHT_EDGE
from kitty.types import Edges, NeighborsMap, WindowGeometry, WindowMapper from kitty.types import Edges, NeighborsMap, WindowGeometry, WindowMapper, WindowResizeDragData
from kitty.typing_compat import EdgeLiteral, WindowType from kitty.typing_compat import EdgeLiteral, WindowType
from kitty.window_list import WindowGroup, WindowList from kitty.window_list import WindowGroup, WindowList
@@ -460,6 +460,15 @@ class Pair:
else: else:
yield q 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): class SplitsLayoutOpts(LayoutOpts):
@@ -706,70 +715,39 @@ class Splits(Layout):
return None 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( def drag_resize_target_windows(
self, click_window: WindowType, x: float, y: float, edges: int, all_windows: WindowList, self, click_window: WindowType, x: float, y: float, edges: int, all_windows: WindowList,
) -> tuple[WindowType, bool, WindowType, bool]: ) -> WindowResizeDragData:
horizontal_target = click_window is_right, is_bottom = bool(edges & RIGHT_EDGE), bool(edges & BOTTOM_EDGE)
vertical_target = click_window ans = WindowResizeDragData(None, is_right, None, is_bottom)
width_increases_rightwards = bool(edges & RIGHT_EDGE) if (wg := all_windows.group_for_window(click_window)) is None or (pair := self.pairs_root.pair_for_window(wg.id)) is None:
height_increases_downwards = bool(edges & BOTTOM_EDGE) return ans
pair_parent_map = {}
id_group_map = {g.id: g for g in all_windows.iter_all_layoutable_groups()} for p in self.pairs_root.self_and_descendants():
if isinstance(p.one, Pair):
def window_for_id(wid: int) -> WindowType | None: pair_parent_map[p.one] = p
g = id_group_map.get(wid) if isinstance(p.two, Pair):
# Use the last (topmost) window in the group, matching the convention pair_parent_map[p.two] = p
# used elsewhere in the layout code (e.g. tall.py drag_resize_target_windows). p = pair
return g.windows[-1] if g and g.windows else None while ans.horizontal_id is None or ans.vertical_id is None:
if not p.is_redundant:
def select_resize_target(pair: Pair) -> tuple[WindowType | None, bool]: if ans.horizontal_id is None and p.horizontal and p.window_on_second(wg.id) != is_right:
# Prefer a window stored directly in pair (not inside a sub-Pair) so that ans = ans._replace(horizontal_id=id(p), width_increases_rightwards=not is_right)
# pair_for_window() returns this pair itself and modify_size_of_child() if ans.vertical_id is None and not p.horizontal and p.window_on_second(wg.id) != is_bottom:
# modifies the intended pair's bias without prematurely stopping at an inner ans = ans._replace(horizontal_id=id(p))
# pair of the same orientation. if (parent := pair_parent_map.get(p)) is None:
# pair.one (left/top) with direction=True: moving right/down grows pair.one. break
# pair.two (right/bottom) with direction=False: achieves the same net effect p = parent
# because modify_size_of_child negates the increment for which==2. return ans
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
def layout_state(self) -> dict[str, Any]: def layout_state(self) -> dict[str, Any]:
return {'pairs': self.pairs_root.serialize()} return {'pairs': self.pairs_root.serialize()}

View File

@@ -9,7 +9,7 @@ from typing import Any
from kitty.borders import BorderColor from kitty.borders import BorderColor
from kitty.conf.utils import to_bool from kitty.conf.utils import to_bool
from kitty.fast_data_types import BOTTOM_EDGE, RIGHT_EDGE 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.typing_compat import EdgeLiteral, WindowType
from kitty.window_list import WindowGroup, WindowList from kitty.window_list import WindowGroup, WindowList
@@ -33,12 +33,12 @@ def drag_resize_target_windows(
num_full_size_windows: int, num_full_size_windows: int,
all_windows: WindowList, all_windows: WindowList,
main_is_horizontal: bool = True main_is_horizontal: bool = True
) -> tuple[WindowType, bool, WindowType, bool]: ) -> WindowResizeDragData:
groups = tuple(all_windows.iter_all_layoutable_groups()) groups = tuple(all_windows.iter_all_layoutable_groups())
horizontal = vertical = click_window horizontal = vertical = click_window
min_dist = float(sys.maxsize) min_dist = float(sys.maxsize)
height_increases_downwards = bool(edges * BOTTOM_EDGE) height_increases_downwards = bool(edges & BOTTOM_EDGE)
width_increases_rightwards = bool(edges * RIGHT_EDGE) width_increases_rightwards = bool(edges & RIGHT_EDGE)
for gr in groups[num_full_size_windows:]: for gr in groups[num_full_size_windows:]:
if gr.windows: if gr.windows:
w = gr.windows[-1] w = gr.windows[-1]
@@ -53,7 +53,7 @@ def drag_resize_target_windows(
min_dist = dist min_dist = dist
horizontal = w horizontal = w
width_increases_rightwards = x > g.left + (g.right - g.left) / 2 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( def neighbors_for_tall_window(
@@ -391,7 +391,7 @@ class Tall(Layout):
def drag_resize_target_windows( def drag_resize_target_windows(
self, click_window: WindowType, x: float, y: float, edges: int, all_windows: WindowList, 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) 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; 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* static Window*
window_for_event(unsigned int *window_idx, bool *in_tab_bar, Edge *window_border) { window_for_event(unsigned int *window_idx, bool *in_tab_bar, Edge *window_border) {
Region central, tab_bar; 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) { if (in_central && w->num_tabs > 0) {
Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab; 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; *window_border = 0;
double dpi = (w->fonts_data->logical_dpi_x + w->fonts_data->logical_dpi_y) / 2.; 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)))); double tolerance = ((long)round((OPT(window_drag_tolerance) * (dpi / 72.0))));

View File

@@ -562,6 +562,12 @@ class Tab: # {{{
return None return None
return 'Could not resize' 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', ''' @ac('win', '''
Resize the active window by the specified amount Resize the active window by the specified amount

View File

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