254 lines
10 KiB
Python
254 lines
10 KiB
Python
#!/usr/bin/env python
|
|
# vim:fileencoding=utf-8
|
|
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
|
|
|
from functools import lru_cache
|
|
from itertools import repeat
|
|
from typing import Callable, Dict, Generator, List, Optional, Sequence, Tuple
|
|
|
|
from kitty.constants import Edges
|
|
from kitty.typing import WindowType
|
|
from kitty.window_list import WindowList
|
|
|
|
from .base import (
|
|
Borders, NeighborsMap, Layout, LayoutData, LayoutDimension,
|
|
ListOfWindows, all_borders, layout_dimension, lgd, no_borders,
|
|
variable_bias
|
|
)
|
|
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 = 'grid'
|
|
|
|
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=variable_bias(num_windows, biased_map) if num_windows > 1 else None)
|
|
|
|
def apply_bias(self, idx: int, increment: float, top_level_windows: ListOfWindows, is_horizontal: bool = True) -> bool:
|
|
b = self.biased_cols if is_horizontal else self.biased_rows
|
|
num_windows = len(top_level_windows)
|
|
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
|
|
|
|
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, windows: WindowList, active_window_idx: int) -> None:
|
|
n = len(windows)
|
|
if n == 1:
|
|
self.layout_single_window(windows[0])
|
|
return
|
|
ncols, nrows, special_rows, special_col = calc_grid_size(n)
|
|
|
|
win_col_map = []
|
|
|
|
def on_col_done(col_windows: List[int]) -> None:
|
|
col_windows_w = [windows[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:
|
|
w = windows[window_idx]
|
|
bw = w.effective_border()
|
|
edges = Edges(
|
|
w.effective_margin('left') + w.effective_padding('left') + bw,
|
|
w.effective_margin('right') + w.effective_padding('right') + bw,
|
|
w.effective_margin('top') + w.effective_padding('top') + bw,
|
|
w.effective_margin('bottom') + w.effective_padding('bottom') + bw,
|
|
)
|
|
xl = layout(xl, lgd.cell_width, edges.left, edges.right)
|
|
yl = layout(yl, lgd.cell_height, edges.top, edges.bottom)
|
|
self.set_window_geometry(w, window_idx, 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, windows: WindowList, active_window: Optional[WindowType], needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]:
|
|
for w in windows:
|
|
if needs_borders_map[w.id]:
|
|
yield all_borders
|
|
else:
|
|
yield no_borders
|
|
|
|
def window_independent_borders(self, windows: WindowList, active_window: Optional[WindowType] = None) -> Generator[Edges, None, None]:
|
|
n = len(windows)
|
|
if not lgd.draw_minimal_borders or n < 2:
|
|
return
|
|
ncols, nrows, special_rows, special_col = calc_grid_size(n)
|
|
row_borders: List[List[Edges]] = [[]]
|
|
col_borders: List[Edges] = []
|
|
bw = windows[0].effective_border()
|
|
|
|
def on_col_done(col_windows: List[int]) -> None:
|
|
left = xl.content_pos + xl.content_size + xl.space_after - bw // 2
|
|
col_borders.append(Edges(left, lgd.central.top, left + bw, lgd.central.bottom))
|
|
row_borders.append([])
|
|
|
|
for window_idx, xl, yl in self.layout_windows(n, nrows, ncols, special_rows, special_col, on_col_done):
|
|
top = yl.content_pos + yl.content_size + yl.space_after - bw // 2
|
|
right = xl.content_pos + xl.content_size + xl.space_after
|
|
row_borders[-1].append(Edges(xl.content_pos - xl.space_before, top, right, top + bw))
|
|
|
|
for border in col_borders[:-1]:
|
|
yield border
|
|
|
|
for rows in row_borders:
|
|
for border in rows[:-1]:
|
|
yield border
|
|
|
|
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> NeighborsMap:
|
|
n = len(windows)
|
|
if n < 4:
|
|
return neighbors_for_tall_window(1, window, windows)
|
|
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 = iter(windows)
|
|
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[window.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
|
|
if col_counts[neighbor_col] == col_counts[col]:
|
|
return neighbors(row, neighbor_col)
|
|
return neighbors(min(row, col_counts[neighbor_col] - 1), neighbor_col)
|
|
|
|
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 [],
|
|
}
|