Files
kitty-mirror/kitty/layout/grid.py
2023-01-09 16:47:42 +05:30

297 lines
12 KiB
Python

#!/usr/bin/env python
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from functools import lru_cache
from itertools import repeat
from math import ceil, floor
from typing import Any, Callable, Dict, Generator, List, Optional, Sequence, Set, Tuple
from kitty.borders import BorderColor
from kitty.types import Edges
from kitty.typing import WindowType
from kitty.window_list import WindowGroup, WindowList
from .base import BorderLine, Layout, LayoutData, LayoutDimension, ListOfWindows, NeighborsMap, layout_dimension, lgd
from .tall import neighbors_for_tall_window
@lru_cache()
def calc_grid_size(n: int) -> Tuple[int, int, int, int]:
if n <= 5:
ncols = 1 if n == 1 else 2
else:
for ncols in range(3, (n // 2) + 1):
if ncols * ncols >= n:
break
nrows = n // ncols
special_rows = n - (nrows * (ncols - 1))
special_col = 0 if special_rows < nrows else ncols - 1
return ncols, nrows, special_rows, special_col
class Grid(Layout):
name: str = 'grid'
no_minimal_window_borders = True
def remove_all_biases(self) -> bool:
self.biased_rows: Dict[int, float] = {}
self.biased_cols: Dict[int, float] = {}
return True
def column_layout(
self,
num: int,
bias: Optional[Sequence[float]] = None,
) -> LayoutDimension:
decoration_pairs = tuple(repeat((0, 0), num))
return layout_dimension(lgd.central.left, lgd.central.width, lgd.cell_width, decoration_pairs, bias=bias, left_align=lgd.align_top_left)
def row_layout(
self,
num: int,
bias: Optional[Sequence[float]] = None,
) -> LayoutDimension:
decoration_pairs = tuple(repeat((0, 0), num))
return layout_dimension(lgd.central.top, lgd.central.height, lgd.cell_height, decoration_pairs, bias=bias, left_align=lgd.align_top_left)
def variable_layout(self, layout_func: Callable[..., LayoutDimension], num_windows: int, biased_map: Dict[int, float]) -> LayoutDimension:
return layout_func(num_windows, bias=biased_map if num_windows > 1 else None)
def apply_bias(self, idx: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool:
num_windows = all_windows.num_groups
ncols, nrows, special_rows, special_col = calc_grid_size(num_windows)
def position_for_window_idx(idx: int) -> Tuple[int, int]:
row_num = col_num = 0
def on_col_done(col_windows: List[int]) -> None:
nonlocal col_num, row_num
row_num = 0
col_num += 1
for window_idx, xl, yl in self.layout_windows(
num_windows, nrows, ncols, special_rows, special_col, on_col_done):
if idx == window_idx:
return row_num, col_num
row_num += 1
return 0, 0
row_num, col_num = position_for_window_idx(idx)
if is_horizontal:
b = self.biased_cols
if ncols < 2:
return False
bias_idx = col_num
attr = 'biased_cols'
def layout_func(windows: ListOfWindows, bias: Optional[Sequence[float]] = None) -> LayoutDimension:
return self.column_layout(num_windows, bias=bias)
else:
b = self.biased_rows
if max(nrows, special_rows) < 2:
return False
bias_idx = row_num
attr = 'biased_rows'
def layout_func(windows: ListOfWindows, bias: Optional[Sequence[float]] = None) -> LayoutDimension:
return self.row_layout(num_windows, bias=bias)
before_layout = list(self.variable_layout(layout_func, num_windows, b))
candidate = b.copy()
before = candidate.get(bias_idx, 0)
candidate[bias_idx] = before + increment
if before_layout == list(self.variable_layout(layout_func, num_windows, candidate)):
return False
setattr(self, attr, candidate)
return True
def layout_windows(
self,
num_windows: int,
nrows: int, ncols: int,
special_rows: int, special_col: int,
on_col_done: Callable[[List[int]], None] = lambda col_windows: None
) -> Generator[Tuple[int, LayoutData, LayoutData], None, None]:
# Distribute windows top-to-bottom, left-to-right (i.e. in columns)
xlayout = self.variable_layout(self.column_layout, ncols, self.biased_cols)
yvals_normal = tuple(self.variable_layout(self.row_layout, nrows, self.biased_rows))
yvals_special = yvals_normal if special_rows == nrows else tuple(self.variable_layout(self.row_layout, special_rows, self.biased_rows))
pos = 0
for col in range(ncols):
rows = special_rows if col == special_col else nrows
yls = yvals_special if col == special_col else yvals_normal
xl = next(xlayout)
col_windows = []
for i, yl in enumerate(yls):
window_idx = pos + i
yield window_idx, xl, yl
col_windows.append(window_idx)
pos += rows
on_col_done(col_windows)
def do_layout(self, all_windows: WindowList) -> None:
n = all_windows.num_groups
if n == 1:
self.layout_single_window_group(next(all_windows.iter_all_layoutable_groups()))
return
ncols, nrows, special_rows, special_col = calc_grid_size(n)
groups = tuple(all_windows.iter_all_layoutable_groups())
win_col_map: List[List[WindowGroup]] = []
def on_col_done(col_windows: List[int]) -> None:
col_windows_w = [groups[i] for i in col_windows]
win_col_map.append(col_windows_w)
def extents(ld: LayoutData) -> Tuple[int, int]:
start = ld.content_pos - ld.space_before
size = ld.space_before + ld.space_after + ld.content_size
return start, size
def layout(ld: LayoutData, cell_length: int, before_dec: int, after_dec: int) -> LayoutData:
start, size = extents(ld)
space_needed_for_decorations = before_dec + after_dec
content_size = size - space_needed_for_decorations
number_of_cells = content_size // cell_length
cell_area = number_of_cells * cell_length
extra = content_size - cell_area
if extra > 0 and not lgd.align_top_left:
before_dec += extra // 2
return LayoutData(start + before_dec, number_of_cells, before_dec, size - cell_area - before_dec, cell_area)
def position_window_in_grid_cell(window_idx: int, xl: LayoutData, yl: LayoutData) -> None:
wg = groups[window_idx]
edges = Edges(wg.decoration('left'), wg.decoration('top'), wg.decoration('right'), wg.decoration('bottom'))
xl = layout(xl, lgd.cell_width, edges.left, edges.right)
yl = layout(yl, lgd.cell_height, edges.top, edges.bottom)
self.set_window_group_geometry(wg, xl, yl)
for window_idx, xl, yl in self.layout_windows(
n, nrows, ncols, special_rows, special_col, on_col_done):
position_window_in_grid_cell(window_idx, xl, yl)
def minimal_borders(self, all_windows: WindowList) -> Generator[BorderLine, None, None]:
n = all_windows.num_groups
if not lgd.draw_minimal_borders or n < 2:
return
needs_borders_map = all_windows.compute_needs_borders_map(lgd.draw_active_borders)
ncols, nrows, special_rows, special_col = calc_grid_size(n)
is_first_row: Set[int] = set()
is_last_row: Set[int] = set()
is_first_column: Set[int] = set()
is_last_column: Set[int] = set()
groups = tuple(all_windows.iter_all_layoutable_groups())
bw = groups[0].effective_border()
if not bw:
return
xl: LayoutData = LayoutData()
yl: LayoutData = LayoutData()
prev_col_windows: List[int] = []
layout_data_map: Dict[int, Tuple[LayoutData, LayoutData]] = {}
def on_col_done(col_windows: List[int]) -> None:
nonlocal prev_col_windows, is_first_column
if col_windows:
is_first_row.add(groups[col_windows[0]].id)
is_last_row.add(groups[col_windows[-1]].id)
if not prev_col_windows:
is_first_column = {groups[x].id for x in col_windows}
prev_col_windows = col_windows
all_groups_in_order: List[WindowGroup] = []
for window_idx, xl, yl in self.layout_windows(n, nrows, ncols, special_rows, special_col, on_col_done):
wg = groups[window_idx]
all_groups_in_order.append(wg)
layout_data_map[wg.id] = xl, yl
is_last_column = {groups[x].id for x in prev_col_windows}
active_group = all_windows.active_group
def ends(yl: LayoutData) -> Tuple[int, int]:
return yl.content_pos - yl.space_before, yl.content_pos + yl.content_size + yl.space_after
def borders_for_window(gid: int) -> Generator[Edges, None, None]:
xl, yl = layout_data_map[gid]
left, right = ends(xl)
top, bottom = ends(yl)
first_row, last_row = gid in is_first_row, gid in is_last_row
first_column, last_column = gid in is_first_column, gid in is_last_column
# Horizontal
if not first_row:
yield Edges(left, top, right, top + bw)
if not last_row:
yield Edges(left, bottom - bw, right, bottom)
# Vertical
if not first_column:
yield Edges(left, top, left + bw, bottom)
if not last_column:
yield Edges(right - bw, top, right, bottom)
for wg in all_groups_in_order:
for edges in borders_for_window(wg.id):
yield BorderLine(edges)
for wg in all_groups_in_order:
if needs_borders_map.get(wg.id):
color = BorderColor.active if wg is active_group else BorderColor.bell
for edges in borders_for_window(wg.id):
yield BorderLine(edges, color)
def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> NeighborsMap:
n = all_windows.num_groups
if n < 4:
return neighbors_for_tall_window(1, window, all_windows)
wg = all_windows.group_for_window(window)
assert wg is not None
ncols, nrows, special_rows, special_col = calc_grid_size(n)
blank_row: List[Optional[int]] = [None for i in range(ncols)]
matrix = tuple(blank_row[:] for j in range(max(nrows, special_rows)))
wi = all_windows.iter_all_layoutable_groups()
pos_map: Dict[int, Tuple[int, int]] = {}
col_counts: List[int] = []
for col in range(ncols):
rows = special_rows if col == special_col else nrows
for row in range(rows):
w = next(wi)
matrix[row][col] = wid = w.id
pos_map[wid] = row, col
col_counts.append(rows)
row, col = pos_map[wg.id]
def neighbors(row: int, col: int) -> List[int]:
try:
ans = matrix[row][col]
except IndexError:
ans = None
return [] if ans is None else [ans]
def side(row: int, col: int, delta: int) -> List[int]:
neighbor_col = col + delta
neighbor_nrows = col_counts[neighbor_col]
nrows = col_counts[col]
if neighbor_nrows == nrows:
return neighbors(row, 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 [],
'bottom': neighbors(row + 1, col),
'left': side(row, col, -1) if col else [],
'right': side(row, col, 1) if col < ncols - 1 else [],
}
def layout_state(self) -> Dict[str, Any]:
return {
'biased_cols': self.biased_cols,
'biased_rows': self.biased_rows
}