mirror of
https://github.com/kovidgoyal/kitty.git
synced 2026-02-01 11:34:59 +01:00
This was needed to fix various corner cases when doing blending of colors in linear space. The new architecture has the same performance as the old in the common case of opaque rendering with no UI layers or images. In the case of only positive z-index images there is a performance decrease as the OS Window is now rendered to a offscreen texture and then blitted to screen. However, in the future when we move to Vulkan or I can figure out how to get Wayland to accept buffers with colors in linear space, this performance penalty can be removed. The performance penalty was not significant on my system but this is highly GPU dependent. Modern GPUs are supposedly optimised for rendering to offscreen buffers, so we will see. The awrit project might be a good test case. Now either we have 1-shot rendering for the case of opaque with only ext or all the various pieces are rendered in successive draw calls into an offscreen buffer that is blitted to the output buffer after all drawing is done. Fixes #8869
474 lines
19 KiB
Python
474 lines
19 KiB
Python
#!/usr/bin/env python
|
|
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
|
|
|
|
from collections.abc import Generator, Iterable, Iterator, Sequence
|
|
from functools import partial
|
|
from itertools import repeat
|
|
from typing import Any, Callable, NamedTuple
|
|
|
|
from kitty.borders import BorderColor
|
|
from kitty.constants import serialize_user_var_name
|
|
from kitty.fast_data_types import Region, set_active_window, viewport_for_window
|
|
from kitty.options.types import Options
|
|
from kitty.types import Edges, WindowGeometry, WindowMapper
|
|
from kitty.typing_compat import TypedDict, WindowType
|
|
from kitty.window_list import WindowGroup, WindowList
|
|
|
|
|
|
class BorderLine(NamedTuple):
|
|
edges: Edges = Edges()
|
|
color: BorderColor = BorderColor.inactive
|
|
|
|
|
|
class LayoutOpts:
|
|
|
|
def __init__(self, data: dict[str, str]):
|
|
pass
|
|
|
|
def serialized(self) -> dict[str, Any]:
|
|
return {}
|
|
|
|
|
|
class LayoutData(NamedTuple):
|
|
content_pos: int = 0
|
|
cells_per_window: int = 0
|
|
space_before: int = 0
|
|
space_after: int = 0
|
|
content_size: int = 0
|
|
|
|
|
|
DecorationPairs = Sequence[tuple[int, int]]
|
|
LayoutDimension = Generator[LayoutData, None, None]
|
|
ListOfWindows = list[WindowType]
|
|
|
|
|
|
class NeighborsMap(TypedDict):
|
|
left: list[int]
|
|
top: list[int]
|
|
right: list[int]
|
|
bottom: list[int]
|
|
|
|
|
|
class LayoutGlobalData:
|
|
draw_minimal_borders: bool = True
|
|
draw_active_borders: bool = True
|
|
alignment_x: int = 0
|
|
alignment_y: int = 0
|
|
|
|
central: Region = Region((0, 0, 199, 199, 200, 200))
|
|
cell_width: int = 20
|
|
cell_height: int = 20
|
|
|
|
|
|
lgd = LayoutGlobalData()
|
|
|
|
|
|
def idx_for_id(win_id: int, windows: Iterable[WindowType]) -> int | None:
|
|
for i, w in enumerate(windows):
|
|
if w.id == win_id:
|
|
return i
|
|
return None
|
|
|
|
|
|
def set_layout_options(opts: Options) -> None:
|
|
lgd.draw_minimal_borders = opts.draw_minimal_borders and sum(opts.window_margin_width) == 0
|
|
lgd.draw_active_borders = opts.active_border_color is not None
|
|
lgd.alignment_x = -1 if opts.placement_strategy.endswith('left') else 1 if opts.placement_strategy.endswith('right') else 0
|
|
lgd.alignment_y = -1 if opts.placement_strategy.startswith('top') else 1 if opts.placement_strategy.startswith('bottom') else 0
|
|
|
|
|
|
def convert_bias_map(bias: dict[int, float], number_of_windows: int, number_of_cells: int) -> Sequence[float]:
|
|
cells_per_window, extra = divmod(number_of_cells, number_of_windows)
|
|
cell_map = list(repeat(cells_per_window, number_of_windows))
|
|
cell_map[-1] += extra
|
|
base_bias = [x / number_of_cells for x in cell_map]
|
|
return distribute_indexed_bias(base_bias, bias)
|
|
|
|
|
|
def calculate_cells_map(
|
|
bias: None | Sequence[float] | dict[int, float],
|
|
number_of_windows: int, number_of_cells: int
|
|
) -> list[int]:
|
|
if isinstance(bias, dict):
|
|
bias = convert_bias_map(bias, number_of_windows, number_of_cells)
|
|
cells_per_window = number_of_cells // number_of_windows
|
|
if bias is not None and number_of_windows > 1 and number_of_windows == len(bias) and cells_per_window > 5:
|
|
cells_map = [int(b * number_of_cells) for b in bias]
|
|
while min(cells_map) < 5:
|
|
maxi, mini = map(cells_map.index, (max(cells_map), min(cells_map)))
|
|
if maxi == mini:
|
|
break
|
|
cells_map[mini] += 1
|
|
cells_map[maxi] -= 1
|
|
else:
|
|
cells_map = list(repeat(cells_per_window, number_of_windows))
|
|
extra = number_of_cells - sum(cells_map)
|
|
if extra > 0:
|
|
cells_map[-1] += extra
|
|
return cells_map
|
|
|
|
|
|
def layout_dimension(
|
|
start_at: int, length: int, cell_length: int,
|
|
decoration_pairs: DecorationPairs,
|
|
alignment: int = 0,
|
|
bias: None | Sequence[float] | dict[int, float] = None
|
|
) -> LayoutDimension:
|
|
number_of_windows = len(decoration_pairs)
|
|
number_of_cells = length // cell_length
|
|
dec_vals: Iterable[int] = map(sum, decoration_pairs)
|
|
space_needed_for_decorations = sum(dec_vals)
|
|
extra = length - number_of_cells * cell_length
|
|
while extra < space_needed_for_decorations:
|
|
number_of_cells -= 1
|
|
extra = length - number_of_cells * cell_length
|
|
cells_map = calculate_cells_map(bias, number_of_windows, number_of_cells)
|
|
assert sum(cells_map) == number_of_cells
|
|
|
|
extra = length - number_of_cells * cell_length - space_needed_for_decorations
|
|
pos = start_at # start
|
|
if alignment > 0: # end
|
|
pos += extra
|
|
elif alignment == 0: # center
|
|
pos += extra // 2
|
|
last_i = len(cells_map) - 1
|
|
|
|
for i, cells_per_window in enumerate(cells_map):
|
|
before_dec, after_dec = decoration_pairs[i]
|
|
pos += before_dec
|
|
if i == 0:
|
|
before_space = pos - start_at
|
|
else:
|
|
before_space = before_dec
|
|
content_size = cells_per_window * cell_length
|
|
if i == last_i:
|
|
after_space = (start_at + length) - (pos + content_size)
|
|
else:
|
|
after_space = after_dec
|
|
yield LayoutData(pos, cells_per_window, before_space, after_space, content_size)
|
|
pos += content_size + after_space
|
|
|
|
|
|
class Rect(NamedTuple):
|
|
left: int
|
|
top: int
|
|
right: int
|
|
bottom: int
|
|
|
|
|
|
def blank_rects_for_window(wg: WindowGeometry) -> Generator[Rect, None, None]:
|
|
left_width, right_width = wg.spaces.left, wg.spaces.right
|
|
top_height, bottom_height = wg.spaces.top, wg.spaces.bottom
|
|
if left_width > 0:
|
|
yield Rect(wg.left - left_width, wg.top - top_height, wg.left, wg.bottom + bottom_height)
|
|
if top_height > 0:
|
|
yield Rect(wg.left, wg.top - top_height, wg.right + right_width, wg.top)
|
|
if right_width > 0:
|
|
yield Rect(wg.right, wg.top, wg.right + right_width, wg.bottom + bottom_height)
|
|
if bottom_height > 0:
|
|
yield Rect(wg.left, wg.bottom, wg.right, wg.bottom + bottom_height)
|
|
|
|
|
|
def window_geometry(xstart: int, xnum: int, ystart: int, ynum: int, left: int, top: int, right: int, bottom: int) -> WindowGeometry:
|
|
return WindowGeometry(
|
|
left=xstart, top=ystart, xnum=max(0, xnum), ynum=max(0, ynum),
|
|
right=xstart + lgd.cell_width * xnum, bottom=ystart + lgd.cell_height * ynum,
|
|
spaces=Edges(left, top, right, bottom)
|
|
)
|
|
|
|
|
|
def window_geometry_from_layouts(x: LayoutData, y: LayoutData) -> WindowGeometry:
|
|
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)
|
|
|
|
|
|
def layout_single_window(
|
|
xdecoration_pairs: DecorationPairs,
|
|
ydecoration_pairs: DecorationPairs,
|
|
xalignment: int = 0,
|
|
yalignment: int = 0,
|
|
) -> WindowGeometry:
|
|
x = next(layout_dimension(lgd.central.left, lgd.central.width, lgd.cell_width, xdecoration_pairs, alignment=xalignment))
|
|
y = next(layout_dimension(lgd.central.top, lgd.central.height, lgd.cell_height, ydecoration_pairs, alignment=yalignment))
|
|
return window_geometry_from_layouts(x, y)
|
|
|
|
|
|
def safe_increment_bias(old_val: float, increment: float = 0) -> float:
|
|
return max(0.1, min(old_val + increment, 0.9))
|
|
|
|
|
|
def normalize_biases(biases: list[float]) -> list[float]:
|
|
s = sum(biases)
|
|
if s == 1.0:
|
|
return biases
|
|
return [x/s for x in biases]
|
|
|
|
|
|
def distribute_indexed_bias(base_bias: Sequence[float], index_bias_map: dict[int, float]) -> Sequence[float]:
|
|
if not index_bias_map:
|
|
return base_bias
|
|
ans = list(base_bias)
|
|
limit = len(ans)
|
|
for row, increment in index_bias_map.items():
|
|
if row >= limit or not increment:
|
|
continue
|
|
other_increment = -increment / (limit - 1)
|
|
ans = [safe_increment_bias(b, increment if i == row else other_increment) for i, b in enumerate(ans)]
|
|
return normalize_biases(ans)
|
|
|
|
|
|
def create_window_id_map_for_unserialize(all_windows: WindowList, serialize_user_var_name: str = serialize_user_var_name) -> dict[int, int]:
|
|
window_id_map = {}
|
|
for w in all_windows:
|
|
k = w.user_vars.pop(serialize_user_var_name, None)
|
|
if k is not None:
|
|
try:
|
|
window_id_map[int(k)] = w.id
|
|
except Exception:
|
|
pass
|
|
return window_id_map
|
|
|
|
|
|
class Layout:
|
|
|
|
name: str = ''
|
|
needs_window_borders = True
|
|
must_draw_borders = False # can be overridden to customize behavior from kittens
|
|
layout_opts = LayoutOpts({})
|
|
only_active_window_visible = False
|
|
|
|
def __init__(self, os_window_id: int, tab_id: int, layout_opts: str = '') -> None:
|
|
self.set_owner(os_window_id, tab_id)
|
|
# A set of rectangles corresponding to the blank spaces at the edges of
|
|
# this layout, i.e. spaces that are not covered by any window
|
|
self.blank_rects: list[Rect] = []
|
|
self.layout_opts = self.parse_layout_opts(layout_opts)
|
|
assert self.name is not None
|
|
self.full_name = f'{self.name}:{layout_opts}' if layout_opts else self.name
|
|
self.remove_all_biases()
|
|
|
|
def set_owner(self, os_window_id: int, tab_id: int) -> None:
|
|
# Useful when moving a layout from one tab to another typically a detached tab being re-attached
|
|
self.os_window_id = os_window_id
|
|
self.tab_id = tab_id
|
|
self.set_active_window_in_os_window = partial(set_active_window, os_window_id, tab_id)
|
|
|
|
def bias_increment_for_cell(self, all_windows: WindowList, is_horizontal: bool) -> float:
|
|
self._set_dimensions()
|
|
return self.calculate_bias_increment_for_a_single_cell(all_windows, is_horizontal)
|
|
|
|
def calculate_bias_increment_for_a_single_cell(self, all_windows: WindowList, is_horizontal: bool) -> float:
|
|
if is_horizontal:
|
|
return (lgd.cell_width + 1) / lgd.central.width
|
|
return (lgd.cell_height + 1) / lgd.central.height
|
|
|
|
def apply_bias(self, window_id: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool:
|
|
return False
|
|
|
|
def remove_all_biases(self) -> bool:
|
|
return False
|
|
|
|
def modify_size_of_window(self, all_windows: WindowList, window_id: int, increment: float, is_horizontal: bool = True) -> bool:
|
|
idx = all_windows.group_idx_for_window(window_id)
|
|
if idx is None or not increment:
|
|
return False
|
|
return self.apply_bias(idx, increment, all_windows, is_horizontal)
|
|
|
|
def parse_layout_opts(self, layout_opts: str | None = None) -> LayoutOpts:
|
|
data: dict[str, str] = {}
|
|
if layout_opts:
|
|
for x in layout_opts.split(';'):
|
|
k, v = x.partition('=')[::2]
|
|
if k and v:
|
|
data[k] = v
|
|
return type(self.layout_opts)(data)
|
|
|
|
def nth_window(self, all_windows: WindowList, num: int) -> WindowType | None:
|
|
return all_windows.active_window_in_nth_group(num, clamp=True)
|
|
|
|
def activate_nth_window(self, all_windows: WindowList, num: int) -> None:
|
|
all_windows.set_active_group_idx(num)
|
|
|
|
def next_window(self, all_windows: WindowList, delta: int = 1) -> None:
|
|
all_windows.activate_next_window_group(delta)
|
|
|
|
def neighbors(self, all_windows: WindowList) -> NeighborsMap:
|
|
w = all_windows.active_window
|
|
assert w is not None
|
|
return self.neighbors_for_window(w, all_windows)
|
|
|
|
def move_window(self, all_windows: WindowList, delta: int = 1) -> bool:
|
|
if all_windows.num_groups < 2 or not delta:
|
|
return False
|
|
|
|
return all_windows.move_window_group(by=delta)
|
|
|
|
def move_window_to_group(self, all_windows: WindowList, group: int) -> bool:
|
|
return all_windows.move_window_group(to_group=group)
|
|
|
|
def add_window(
|
|
self, all_windows: WindowList, window: WindowType, location: str | None = None,
|
|
overlay_for: int | None = None, put_overlay_behind: bool = False, bias: float | None = None,
|
|
next_to: WindowType | None = None,
|
|
) -> WindowType | None:
|
|
if overlay_for is not None:
|
|
underlay = all_windows.id_map.get(overlay_for)
|
|
if underlay is not None:
|
|
window.margin, window.padding = underlay.margin.copy(), underlay.padding.copy()
|
|
all_windows.add_window(window, group_of=overlay_for, head_of_group=put_overlay_behind)
|
|
return underlay
|
|
if location == 'neighbor':
|
|
location = 'after'
|
|
self.add_non_overlay_window(all_windows, window, location, bias, next_to)
|
|
return None
|
|
|
|
def add_non_overlay_window(
|
|
self, all_windows: WindowList, window: WindowType, location: str | None, bias: float | None = None, next_to: WindowType | None = None
|
|
) -> None:
|
|
before = False
|
|
next_to = next_to or all_windows.active_window
|
|
if location is not None:
|
|
if location in ('after', 'vsplit', 'hsplit'):
|
|
pass
|
|
elif location == 'before':
|
|
before = True
|
|
elif location == 'first':
|
|
before = True
|
|
next_to = None
|
|
elif location == 'last':
|
|
next_to = None
|
|
all_windows.add_window(window, next_to=next_to, before=before)
|
|
if bias is not None:
|
|
idx = all_windows.group_idx_for_window(window)
|
|
if idx is not None:
|
|
self._set_dimensions()
|
|
self._bias_slot(all_windows, idx, bias)
|
|
|
|
def _bias_slot(self, all_windows: WindowList, idx: int, bias: float) -> bool:
|
|
fractional_bias = max(10, min(abs(bias), 90)) / 100
|
|
h, v = self.calculate_bias_increment_for_a_single_cell(all_windows, True), self.calculate_bias_increment_for_a_single_cell(all_windows, False)
|
|
nh, nv = lgd.central.width / lgd.cell_width, lgd.central.height / lgd.cell_height
|
|
f = max(-90, min(bias, 90)) / 100.
|
|
return self.bias_slot(all_windows, idx, fractional_bias, h * nh *f, v * nv * f)
|
|
|
|
def bias_slot(self, all_windows: WindowList, idx: int, fractional_bias: float, cell_increment_bias_h: float, cell_increment_bias_v: float) -> bool:
|
|
return False
|
|
|
|
def update_visibility(self, all_windows: WindowList) -> None:
|
|
active_window = all_windows.active_window
|
|
for window, is_group_leader in all_windows.iter_windows_with_visibility():
|
|
is_visible = window is active_window or (is_group_leader and not self.only_active_window_visible)
|
|
window.set_visible_in_layout(is_visible)
|
|
|
|
def _set_dimensions(self) -> None:
|
|
lgd.central, tab_bar, vw, vh, lgd.cell_width, lgd.cell_height = viewport_for_window(self.os_window_id)
|
|
|
|
def __call__(self, all_windows: WindowList) -> None:
|
|
self._set_dimensions()
|
|
self.update_visibility(all_windows)
|
|
self.blank_rects = []
|
|
self.do_layout(all_windows)
|
|
|
|
def layout_single_window_group(self, wg: WindowGroup, add_blank_rects: bool = True) -> None:
|
|
bw = 1 if self.must_draw_borders else 0
|
|
xdecoration_pairs = ((
|
|
wg.decoration('left', border_mult=bw, is_single_window=True),
|
|
wg.decoration('right', border_mult=bw, is_single_window=True),
|
|
),)
|
|
ydecoration_pairs = ((
|
|
wg.decoration('top', border_mult=bw, is_single_window=True),
|
|
wg.decoration('bottom', border_mult=bw, is_single_window=True),
|
|
),)
|
|
geom = layout_single_window(xdecoration_pairs, ydecoration_pairs, xalignment=lgd.alignment_x, yalignment=lgd.alignment_y)
|
|
wg.set_geometry(geom)
|
|
if add_blank_rects:
|
|
self.blank_rects.extend(blank_rects_for_window(geom))
|
|
|
|
def xlayout(
|
|
self,
|
|
groups: Iterator[WindowGroup],
|
|
bias: None | Sequence[float] | dict[int, float] = None,
|
|
start: int | None = None,
|
|
size: int | None = None,
|
|
offset: int = 0,
|
|
border_mult: int = 1
|
|
) -> LayoutDimension:
|
|
decoration_pairs = tuple(
|
|
(g.decoration('left', border_mult=border_mult), g.decoration('right', border_mult=border_mult)) for i, g in
|
|
enumerate(groups) if i >= offset
|
|
)
|
|
if start is None:
|
|
start = lgd.central.left
|
|
if size is None:
|
|
size = lgd.central.width
|
|
return layout_dimension(start, size, lgd.cell_width, decoration_pairs, bias=bias, alignment=lgd.alignment_x)
|
|
|
|
def ylayout(
|
|
self,
|
|
groups: Iterator[WindowGroup],
|
|
bias: None | Sequence[float] | dict[int, float] = None,
|
|
start: int | None = None,
|
|
size: int | None = None,
|
|
offset: int = 0,
|
|
border_mult: int = 1
|
|
) -> LayoutDimension:
|
|
decoration_pairs = tuple(
|
|
(g.decoration('top', border_mult=border_mult), g.decoration('bottom', border_mult=border_mult)) for i, g in
|
|
enumerate(groups) if i >= offset
|
|
)
|
|
if start is None:
|
|
start = lgd.central.top
|
|
if size is None:
|
|
size = lgd.central.height
|
|
return layout_dimension(start, size, lgd.cell_height, decoration_pairs, bias=bias, alignment=lgd.alignment_y)
|
|
|
|
def set_window_group_geometry(self, wg: WindowGroup, xl: LayoutData, yl: LayoutData) -> WindowGeometry:
|
|
geom = window_geometry_from_layouts(xl, yl)
|
|
wg.set_geometry(geom)
|
|
self.blank_rects.extend(blank_rects_for_window(geom))
|
|
return geom
|
|
|
|
def do_layout(self, windows: WindowList) -> None:
|
|
raise NotImplementedError()
|
|
|
|
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> NeighborsMap:
|
|
return {'left': [], 'right': [], 'top': [], 'bottom': []}
|
|
|
|
def compute_needs_borders_map(self, all_windows: WindowList) -> dict[int, bool]:
|
|
return all_windows.compute_needs_borders_map(lgd.draw_active_borders)
|
|
|
|
def get_minimal_borders(self, windows: WindowList) -> Generator[BorderLine, None, None]:
|
|
self._set_dimensions()
|
|
yield from self.minimal_borders(windows)
|
|
|
|
def minimal_borders(self, windows: WindowList) -> Generator[BorderLine, None, None]:
|
|
return
|
|
yield BorderLine() # type: ignore
|
|
|
|
def layout_action(self, action_name: str, args: Sequence[str], all_windows: WindowList) -> bool | None:
|
|
pass
|
|
|
|
def layout_state(self) -> dict[str, Any]:
|
|
return {}
|
|
|
|
def set_layout_state(self, layout_state: dict[str, Any], map_group_id: WindowMapper) -> bool:
|
|
return True
|
|
|
|
def serialize(self, all_windows: WindowList) -> dict[str, Any]:
|
|
ans = self.layout_state()
|
|
ans['opts'] = self.layout_opts.serialized()
|
|
ans['class'] = self.__class__.__name__
|
|
ans['all_windows'] = all_windows.serialize_layout_state()
|
|
return ans
|
|
|
|
def unserialize(
|
|
self, s: dict[str, Any], all_windows: WindowList,
|
|
window_id_mapper: Callable[[WindowList], dict[int, int]] = create_window_id_map_for_unserialize,
|
|
) -> bool:
|
|
if s.get('class') != self.__class__.__name__:
|
|
return False
|
|
window_id_map = create_window_id_map_for_unserialize(all_windows)
|
|
m = all_windows.unserialize_layout_state(s['all_windows'], window_id_map)
|
|
if m is None:
|
|
return False
|
|
return self.set_layout_state(s, m.get)
|