1#!/usr/local/bin/python3.8
2# vim:fileencoding=utf-8
3# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
4
5from functools import partial
6from itertools import repeat
7from typing import (
8    Any, Dict, Generator, Iterable, Iterator, List, NamedTuple, Optional,
9    Sequence, Tuple
10)
11
12from kitty.borders import BorderColor
13from kitty.fast_data_types import (
14    Region, set_active_window, viewport_for_window
15)
16from kitty.options.types import Options
17from kitty.types import Edges, WindowGeometry
18from kitty.typing import TypedDict, WindowType
19from kitty.window_list import WindowGroup, WindowList
20
21
22class BorderLine(NamedTuple):
23    edges: Edges = Edges()
24    color: BorderColor = BorderColor.inactive
25
26
27class LayoutOpts:
28
29    def __init__(self, data: Dict[str, str]):
30        pass
31
32
33class LayoutData(NamedTuple):
34    content_pos: int = 0
35    cells_per_window: int = 0
36    space_before: int = 0
37    space_after: int = 0
38    content_size: int = 0
39
40
41DecorationPairs = Sequence[Tuple[int, int]]
42LayoutDimension = Generator[LayoutData, None, None]
43ListOfWindows = List[WindowType]
44
45
46class NeighborsMap(TypedDict):
47    left: List[int]
48    top: List[int]
49    right: List[int]
50    bottom: List[int]
51
52
53class LayoutGlobalData:
54    draw_minimal_borders: bool = True
55    draw_active_borders: bool = True
56    align_top_left: bool = False
57
58    central: Region = Region((0, 0, 199, 199, 200, 200))
59    cell_width: int = 20
60    cell_height: int = 20
61
62
63lgd = LayoutGlobalData()
64
65
66def idx_for_id(win_id: int, windows: Iterable[WindowType]) -> Optional[int]:
67    for i, w in enumerate(windows):
68        if w.id == win_id:
69            return i
70
71
72def set_layout_options(opts: Options) -> None:
73    lgd.draw_minimal_borders = opts.draw_minimal_borders and sum(opts.window_margin_width) == 0
74    lgd.draw_active_borders = opts.active_border_color is not None
75    lgd.align_top_left = opts.placement_strategy == 'top-left'
76
77
78def calculate_cells_map(bias: Optional[Sequence[float]], number_of_windows: int, number_of_cells: int) -> List[int]:
79    cells_per_window = number_of_cells // number_of_windows
80    if bias is not None and 1 < number_of_windows == len(bias) and cells_per_window > 5:
81        cells_map = [int(b * number_of_cells) for b in bias]
82        while min(cells_map) < 5:
83            maxi, mini = map(cells_map.index, (max(cells_map), min(cells_map)))
84            if maxi == mini:
85                break
86            cells_map[mini] += 1
87            cells_map[maxi] -= 1
88    else:
89        cells_map = list(repeat(cells_per_window, number_of_windows))
90    extra = number_of_cells - sum(cells_map)
91    if extra > 0:
92        cells_map[-1] += extra
93    return cells_map
94
95
96def layout_dimension(
97    start_at: int, length: int, cell_length: int,
98    decoration_pairs: DecorationPairs,
99    left_align: bool = False,
100    bias: Optional[Sequence[float]] = None
101) -> LayoutDimension:
102    number_of_windows = len(decoration_pairs)
103    number_of_cells = length // cell_length
104    space_needed_for_decorations: int = sum(map(sum, decoration_pairs))
105    extra = length - number_of_cells * cell_length
106    while extra < space_needed_for_decorations:
107        number_of_cells -= 1
108        extra = length - number_of_cells * cell_length
109    cells_map = calculate_cells_map(bias, number_of_windows, number_of_cells)
110    assert sum(cells_map) == number_of_cells
111
112    extra = length - number_of_cells * cell_length - space_needed_for_decorations
113    pos = start_at
114    if not left_align:
115        pos += extra // 2
116    last_i = len(cells_map) - 1
117
118    for i, cells_per_window in enumerate(cells_map):
119        before_dec, after_dec = decoration_pairs[i]
120        pos += before_dec
121        if i == 0:
122            before_space = pos - start_at
123        else:
124            before_space = before_dec
125        content_size = cells_per_window * cell_length
126        if i == last_i:
127            after_space = (start_at + length) - (pos + content_size)
128        else:
129            after_space = after_dec
130        yield LayoutData(pos, cells_per_window, before_space, after_space, content_size)
131        pos += content_size + after_space
132
133
134class Rect(NamedTuple):
135    left: int
136    top: int
137    right: int
138    bottom: int
139
140
141def blank_rects_for_window(wg: WindowGeometry) -> Generator[Rect, None, None]:
142    left_width, right_width = wg.spaces.left, wg.spaces.right
143    top_height, bottom_height = wg.spaces.top, wg.spaces.bottom
144    if left_width > 0:
145        yield Rect(wg.left - left_width, wg.top - top_height, wg.left, wg.bottom + bottom_height)
146    if top_height > 0:
147        yield Rect(wg.left, wg.top - top_height, wg.right + right_width, wg.top)
148    if right_width > 0:
149        yield Rect(wg.right, wg.top, wg.right + right_width, wg.bottom + bottom_height)
150    if bottom_height > 0:
151        yield Rect(wg.left, wg.bottom, wg.right, wg.bottom + bottom_height)
152
153
154def window_geometry(xstart: int, xnum: int, ystart: int, ynum: int, left: int, top: int, right: int, bottom: int) -> WindowGeometry:
155    return WindowGeometry(
156        left=xstart, top=ystart, xnum=xnum, ynum=ynum,
157        right=xstart + lgd.cell_width * xnum, bottom=ystart + lgd.cell_height * ynum,
158        spaces=Edges(left, top, right, bottom)
159    )
160
161
162def window_geometry_from_layouts(x: LayoutData, y: LayoutData) -> WindowGeometry:
163    return window_geometry(x.content_pos, x.cells_per_window, y.content_pos, y.cells_per_window, x.space_before, y.space_before, x.space_after, y.space_after)
164
165
166def layout_single_window(xdecoration_pairs: DecorationPairs, ydecoration_pairs: DecorationPairs, left_align: bool = False) -> WindowGeometry:
167    x = next(layout_dimension(lgd.central.left, lgd.central.width, lgd.cell_width, xdecoration_pairs, left_align=lgd.align_top_left))
168    y = next(layout_dimension(lgd.central.top, lgd.central.height, lgd.cell_height, ydecoration_pairs, left_align=lgd.align_top_left))
169    return window_geometry_from_layouts(x, y)
170
171
172def safe_increment_bias(old_val: float, increment: float) -> float:
173    return max(0.1, min(old_val + increment, 0.9))
174
175
176def normalize_biases(biases: List[float]) -> List[float]:
177    s = sum(biases)
178    if s == 1:
179        return biases
180    return [x/s for x in biases]
181
182
183def distribute_indexed_bias(base_bias: Sequence[float], index_bias_map: Dict[int, float]) -> Sequence[float]:
184    if not index_bias_map:
185        return base_bias
186    ans = list(base_bias)
187    limit = len(ans)
188    for row, increment in index_bias_map.items():
189        if row >= limit or not increment:
190            continue
191        other_increment = -increment / (limit - 1)
192        ans = [safe_increment_bias(b, increment if i == row else other_increment) for i, b in enumerate(ans)]
193    return normalize_biases(ans)
194
195
196def variable_bias(num_windows: int, candidate: Dict[int, float]) -> Sequence[float]:
197    return distribute_indexed_bias(list(repeat(1/(num_windows), num_windows)), candidate)
198
199
200class Layout:
201
202    name: Optional[str] = None
203    needs_window_borders = True
204    must_draw_borders = False  # can be overridden to customize behavior from kittens
205    layout_opts = LayoutOpts({})
206    only_active_window_visible = False
207
208    def __init__(self, os_window_id: int, tab_id: int, layout_opts: str = '') -> None:
209        self.os_window_id = os_window_id
210        self.tab_id = tab_id
211        self.set_active_window_in_os_window = partial(set_active_window, os_window_id, tab_id)
212        # A set of rectangles corresponding to the blank spaces at the edges of
213        # this layout, i.e. spaces that are not covered by any window
214        self.blank_rects: List[Rect] = []
215        self.layout_opts = self.parse_layout_opts(layout_opts)
216        assert self.name is not None
217        self.full_name = self.name + ((':' + layout_opts) if layout_opts else '')
218        self.remove_all_biases()
219
220    def bias_increment_for_cell(self, is_horizontal: bool) -> float:
221        self._set_dimensions()
222        if is_horizontal:
223            return (lgd.cell_width + 1) / lgd.central.width
224        return (lgd.cell_height + 1) / lgd.central.height
225
226    def apply_bias(self, window_id: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool:
227        return False
228
229    def remove_all_biases(self) -> bool:
230        return False
231
232    def modify_size_of_window(self, all_windows: WindowList, window_id: int, increment: float, is_horizontal: bool = True) -> bool:
233        idx = all_windows.group_idx_for_window(window_id)
234        if idx is None:
235            return False
236        return self.apply_bias(idx, increment, all_windows, is_horizontal)
237
238    def parse_layout_opts(self, layout_opts: Optional[str] = None) -> LayoutOpts:
239        data: Dict[str, str] = {}
240        if layout_opts:
241            for x in layout_opts.split(';'):
242                k, v = x.partition('=')[::2]
243                if k and v:
244                    data[k] = v
245        return type(self.layout_opts)(data)
246
247    def nth_window(self, all_windows: WindowList, num: int) -> Optional[WindowType]:
248        return all_windows.active_window_in_nth_group(num, clamp=True)
249
250    def activate_nth_window(self, all_windows: WindowList, num: int) -> None:
251        all_windows.set_active_group_idx(num)
252
253    def next_window(self, all_windows: WindowList, delta: int = 1) -> None:
254        all_windows.activate_next_window_group(delta)
255
256    def neighbors(self, all_windows: WindowList) -> NeighborsMap:
257        w = all_windows.active_window
258        assert w is not None
259        return self.neighbors_for_window(w, all_windows)
260
261    def move_window(self, all_windows: WindowList, delta: int = 1) -> bool:
262        if all_windows.num_groups < 2 or not delta:
263            return False
264
265        return all_windows.move_window_group(by=delta)
266
267    def move_window_to_group(self, all_windows: WindowList, group: int) -> bool:
268        return all_windows.move_window_group(to_group=group)
269
270    def add_window(self, all_windows: WindowList, window: WindowType, location: Optional[str] = None, overlay_for: Optional[int] = None) -> None:
271        if overlay_for is not None and overlay_for in all_windows:
272            all_windows.add_window(window, group_of=overlay_for)
273            return
274        if location == 'neighbor':
275            location = 'after'
276        self.add_non_overlay_window(all_windows, window, location)
277
278    def add_non_overlay_window(self, all_windows: WindowList, window: WindowType, location: Optional[str]) -> None:
279        next_to: Optional[WindowType] = None
280        before = False
281        next_to = all_windows.active_window
282        if location is not None:
283            if location in ('after', 'vsplit', 'hsplit'):
284                pass
285            elif location == 'before':
286                before = True
287            elif location == 'first':
288                before = True
289                next_to = None
290            elif location == 'last':
291                next_to = None
292        all_windows.add_window(window, next_to=next_to, before=before)
293
294    def update_visibility(self, all_windows: WindowList) -> None:
295        active_window = all_windows.active_window
296        for window, is_group_leader in all_windows.iter_windows_with_visibility():
297            is_visible = window is active_window or (is_group_leader and not self.only_active_window_visible)
298            window.set_visible_in_layout(is_visible)
299
300    def _set_dimensions(self) -> None:
301        lgd.central, tab_bar, vw, vh, lgd.cell_width, lgd.cell_height = viewport_for_window(self.os_window_id)
302
303    def __call__(self, all_windows: WindowList) -> None:
304        self._set_dimensions()
305        self.update_visibility(all_windows)
306        self.blank_rects = []
307        self.do_layout(all_windows)
308
309    def layout_single_window_group(self, wg: WindowGroup, add_blank_rects: bool = True) -> None:
310        bw = 1 if self.must_draw_borders else 0
311        xdecoration_pairs = ((
312            wg.decoration('left', border_mult=bw, is_single_window=True),
313            wg.decoration('right', border_mult=bw, is_single_window=True),
314        ),)
315        ydecoration_pairs = ((
316            wg.decoration('top', border_mult=bw, is_single_window=True),
317            wg.decoration('bottom', border_mult=bw, is_single_window=True),
318        ),)
319        geom = layout_single_window(xdecoration_pairs, ydecoration_pairs, left_align=lgd.align_top_left)
320        wg.set_geometry(geom)
321        if add_blank_rects and wg:
322            self.blank_rects.extend(blank_rects_for_window(geom))
323
324    def xlayout(
325        self,
326        groups: Iterator[WindowGroup],
327        bias: Optional[Sequence[float]] = None,
328        start: Optional[int] = None,
329        size: Optional[int] = None,
330        offset: int = 0,
331        border_mult: int = 1
332    ) -> LayoutDimension:
333        decoration_pairs = tuple(
334            (g.decoration('left', border_mult=border_mult), g.decoration('right', border_mult=border_mult)) for i, g in
335            enumerate(groups) if i >= offset
336        )
337        if start is None:
338            start = lgd.central.left
339        if size is None:
340            size = lgd.central.width
341        return layout_dimension(start, size, lgd.cell_width, decoration_pairs, bias=bias, left_align=lgd.align_top_left)
342
343    def ylayout(
344        self,
345        groups: Iterator[WindowGroup],
346        bias: Optional[Sequence[float]] = None,
347        start: Optional[int] = None,
348        size: Optional[int] = None,
349        offset: int = 0,
350        border_mult: int = 1
351    ) -> LayoutDimension:
352        decoration_pairs = tuple(
353            (g.decoration('top', border_mult=border_mult), g.decoration('bottom', border_mult=border_mult)) for i, g in
354            enumerate(groups) if i >= offset
355        )
356        if start is None:
357            start = lgd.central.top
358        if size is None:
359            size = lgd.central.height
360        return layout_dimension(start, size, lgd.cell_height, decoration_pairs, bias=bias, left_align=lgd.align_top_left)
361
362    def set_window_group_geometry(self, wg: WindowGroup, xl: LayoutData, yl: LayoutData) -> WindowGeometry:
363        geom = window_geometry_from_layouts(xl, yl)
364        wg.set_geometry(geom)
365        self.blank_rects.extend(blank_rects_for_window(geom))
366        return geom
367
368    def do_layout(self, windows: WindowList) -> None:
369        raise NotImplementedError()
370
371    def neighbors_for_window(self, window: WindowType, windows: WindowList) -> NeighborsMap:
372        return {'left': [], 'right': [], 'top': [], 'bottom': []}
373
374    def compute_needs_borders_map(self, all_windows: WindowList) -> Dict[int, bool]:
375        return all_windows.compute_needs_borders_map(lgd.draw_active_borders)
376
377    def get_minimal_borders(self, windows: WindowList) -> Generator[BorderLine, None, None]:
378        self._set_dimensions()
379        yield from self.minimal_borders(windows)
380
381    def minimal_borders(self, windows: WindowList) -> Generator[BorderLine, None, None]:
382        return
383        yield BorderLine()  # type: ignore
384
385    def layout_action(self, action_name: str, args: Sequence[str], all_windows: WindowList) -> Optional[bool]:
386        pass
387
388    def layout_state(self) -> Dict[str, Any]:
389        return {}
390