From c69b8870d29509332bcedff957a22377c642ee16 Mon Sep 17 00:00:00 2001 From: Kovid Goyal Date: Sun, 19 Apr 2020 09:36:26 +0530 Subject: [PATCH] Allow individually setting margins and padding for each edge (left, right, top, bottom) --- docs/changelog.rst | 3 + kitty/borders.py | 58 +++--- kitty/boss.py | 4 +- kitty/config.py | 17 +- kitty/config_data.py | 46 ++++- kitty/constants.py | 7 + kitty/fast_data_types.pyi | 4 + kitty/layout.py | 383 +++++++++++++++++++------------------- kitty/mouse.c | 40 ++-- kitty/session.py | 6 +- kitty/state.c | 28 ++- kitty/state.h | 4 +- kitty/tabs.py | 32 +--- kitty/window.py | 56 +++++- kitty_tests/layout.py | 13 +- 15 files changed, 411 insertions(+), 290 deletions(-) diff --git a/docs/changelog.rst b/docs/changelog.rst index 263fa3d35..c7dc20b3d 100644 --- a/docs/changelog.rst +++ b/docs/changelog.rst @@ -7,6 +7,9 @@ To update |kitty|, :doc:`follow the instructions `. 0.17.3 [future] ----------------- +- Allow individually setting margins and padding for each edge (left, right, + top, bottom) + - Fix reverse video not being rendered correctly when using transparency or a background image (:iss:`2419`) diff --git a/kitty/borders.py b/kitty/borders.py index 726400284..3f3c02afe 100644 --- a/kitty/borders.py +++ b/kitty/borders.py @@ -6,7 +6,6 @@ from enum import IntFlag from itertools import chain from typing import List, Optional, Sequence, Tuple -from .constants import WindowGeometry from .fast_data_types import ( BORDERS_PROGRAM, add_borders_rect, compile_program, init_borders_program, os_window_has_background_image @@ -22,22 +21,38 @@ class BorderColor(IntFlag): def vertical_edge(os_window_id: int, tab_id: int, color: int, width: int, top: int, bottom: int, left: int) -> None: - add_borders_rect(os_window_id, tab_id, left, top, left + width, bottom, color) + if width > 0: + add_borders_rect(os_window_id, tab_id, left, top, left + width, bottom, color) def horizontal_edge(os_window_id: int, tab_id: int, color: int, height: int, left: int, right: int, top: int) -> None: - add_borders_rect(os_window_id, tab_id, left, top, right, top + height, color) + if height > 0: + add_borders_rect(os_window_id, tab_id, left, top, right, top + height, color) -def draw_edges(os_window_id: int, tab_id: int, colors: Sequence[int], width: int, geometry: 'WindowGeometry', base_width: int = 0) -> None: - left = geometry.left - (width + base_width) - top = geometry.top - (width + base_width) - right = geometry.right + (width + base_width) - bottom = geometry.bottom + (width + base_width) - horizontal_edge(os_window_id, tab_id, colors[1], width, left, right, top) - horizontal_edge(os_window_id, tab_id, colors[3], width, left, right, geometry.bottom + base_width) - vertical_edge(os_window_id, tab_id, colors[0], width, top, bottom, left) - vertical_edge(os_window_id, tab_id, colors[2], width, top, bottom, geometry.right + base_width) +def draw_edges(os_window_id: int, tab_id: int, colors: Sequence[int], window: WindowType, borders: bool = False) -> None: + geometry = window.geometry + pl, pt = window.effective_padding('left'), window.effective_padding('top') + pr, pb = window.effective_padding('right'), window.effective_padding('bottom') + left = geometry.left - pl + top = geometry.top - pt + lr = geometry.right + right = lr + pr + bt = geometry.bottom + bottom = bt + pb + if borders: + width = window.effective_border() + bt = bottom + lr = right + left -= width + top -= width + right += width + bottom += width + pl = pr = pb = pt = width + horizontal_edge(os_window_id, tab_id, colors[1], pt, left, right, top) + horizontal_edge(os_window_id, tab_id, colors[3], pb, left, right, bt) + vertical_edge(os_window_id, tab_id, colors[0], pl, top, bottom, left) + vertical_edge(os_window_id, tab_id, colors[2], pr, top, bottom, lr) def load_borders_program() -> None: @@ -58,8 +73,6 @@ class Borders: active_window: Optional[WindowType], current_layout: LayoutType, extra_blank_rects: Sequence[Tuple[int, int, int, int]], - padding_width: int, - border_width: int, draw_window_borders: bool = True, ) -> None: add_borders_rect(self.os_window_id, self.tab_id, 0, 0, 0, 0, BorderColor.default_bg) @@ -68,15 +81,14 @@ class Borders: for br in chain(current_layout.blank_rects, extra_blank_rects): left, top, right, bottom = br add_borders_rect(self.os_window_id, self.tab_id, left, top, right, bottom, BorderColor.default_bg) - bw, pw = border_width, padding_width - if bw + pw <= 0: - return + bw = 0 + if windows: + bw = windows[0].effective_border() draw_borders = bw > 0 and draw_window_borders if draw_borders: border_data = current_layout.resolve_borders(windows, active_window) for i, w in enumerate(windows): - g = w.geometry window_bg = w.screen.color_profile.default_bg window_bg = (window_bg << 8) | BorderColor.window_bg if draw_borders: @@ -86,13 +98,11 @@ class Borders: else: color = BorderColor.bell if w.needs_attention else BorderColor.inactive colors = tuple(color if needed else window_bg for needed in next(border_data)) - draw_edges( - self.os_window_id, self.tab_id, colors, bw, g, base_width=pw) - if pw > 0 and not has_background_image: + draw_edges(self.os_window_id, self.tab_id, colors, w, borders=True) + if not has_background_image: # Draw the background rectangles over the padding region - colors = (window_bg, window_bg, window_bg, window_bg) - draw_edges( - self.os_window_id, self.tab_id, colors, pw, g) + colors = window_bg, window_bg, window_bg, window_bg + draw_edges(self.os_window_id, self.tab_id, colors, w) color = BorderColor.inactive for (left, top, right, bottom) in current_layout.window_independent_borders(windows, active_window): diff --git a/kitty/boss.py b/kitty/boss.py index 0a2afa905..214df48b0 100644 --- a/kitty/boss.py +++ b/kitty/boss.py @@ -534,7 +534,9 @@ class Boss: sz = os_window_font_size(os_window_id) if sz: os_window_font_size(os_window_id, sz, True) - tm.update_dpi_based_sizes() + for tab in tm: + for window in tab: + window.on_dpi_change(sz) tm.resize() def _set_os_window_background_opacity(self, os_window_id: int, opacity: float) -> None: diff --git a/kitty/config.py b/kitty/config.py index 63127009f..fc3dde9e9 100644 --- a/kitty/config.py +++ b/kitty/config.py @@ -23,7 +23,7 @@ from .config_data import all_options, parse_mods, type_convert from .constants import cache_dir, defconf, is_macos from .key_names import get_key_name_lookup, key_name_aliases from .options_stub import Options as OptionsStub -from .typing import TypedDict +from .typing import EdgeLiteral, TypedDict from .utils import log_error KeySpec = Tuple[int, bool, int] @@ -730,12 +730,23 @@ def initial_window_size_func(opts: OptionsStub, cached_values: Dict) -> Callable # scaling is not needed on Wayland, but is needed on macOS. Not # sure about X11. xscale = yscale = 1 + + def effective_margin(which: EdgeLiteral) -> float: + ans: float = getattr(opts.single_window_margin_width, which) + if ans < 0: + ans = getattr(opts.window_margin_width, which) + return ans + if w_unit == 'cells': - width = cell_width * w / xscale + (dpi_x / 72) * (opts.window_margin_width + opts.window_padding_width) + 1 + spacing = effective_margin('left') + effective_margin('right') + spacing += opts.window_padding_width.left + opts.window_padding_width.right + width = cell_width * w / xscale + (dpi_x / 72) * spacing + 1 else: width = w if h_unit == 'cells': - height = cell_height * h / yscale + (dpi_y / 72) * (opts.window_margin_width + opts.window_padding_width) + 1 + spacing = effective_margin('top') + effective_margin('bottom') + spacing += opts.window_padding_width.top + opts.window_padding_width.bottom + height = cell_height * h / yscale + (dpi_y / 72) * spacing + 1 else: height = h return int(width), int(height) diff --git a/kitty/config_data.py b/kitty/config_data.py index 3d05a41a3..317a48fb8 100644 --- a/kitty/config_data.py +++ b/kitty/config_data.py @@ -6,8 +6,8 @@ import os from gettext import gettext as _ from typing import ( - Any, Dict, FrozenSet, Iterable, List, Optional, Sequence, Set, Tuple, - TypeVar, Union + Any, Callable, Dict, FrozenSet, Iterable, List, Optional, Sequence, Set, + Tuple, TypeVar, Union ) from . import fast_data_types as defines @@ -16,7 +16,7 @@ from .conf.utils import ( choices, positive_float, positive_int, to_bool, to_cmdline, to_color, to_color_or_none, unit_float ) -from .constants import config_dir, is_macos +from .constants import FloatEdges, config_dir, is_macos from .fast_data_types import CURSOR_BEAM, CURSOR_BLOCK, CURSOR_UNDERLINE from .layout import all_layouts from .rgb import Color, color_as_int, color_as_sharp, color_from_int @@ -675,15 +675,42 @@ that separate the inactive window from a neighbor. Note that setting a non-zero window margin overrides this and causes all borders to be drawn. ''')) -o('window_margin_width', 0.0, option_type=positive_float, long_text=_(''' -The window margin (in pts) (blank area outside the border)''')) -o('single_window_margin_width', -1000.0, option_type=float, long_text=_(''' +def edge_width(x: str, converter: Callable[[str], float] = positive_float) -> FloatEdges: + parts = str(x).split() + num = len(parts) + if num == 1: + val = converter(parts[0]) + return FloatEdges(val, val, val, val) + if num == 2: + v = converter(parts[0]) + h = converter(parts[1]) + return FloatEdges(h, v, h, v) + if num == 3: + top, h, bottom = map(converter, parts) + return FloatEdges(h, top, h, bottom) + top, right, bottom, left = map(converter, parts) + return FloatEdges(left, top, right, bottom) + + +def optional_edge_width(x: str) -> FloatEdges: + return edge_width(x, float) + + +edge_desc = _( + 'A single value sets all four sides. Two values set the vertical and horizontal sides.' + ' Three values set top, horizontal and bottom. Four values set top, right, bottom and left.') + + +o('window_margin_width', '0', option_type=edge_width, long_text=_(''' +The window margin (in pts) (blank area outside the border). ''' + edge_desc)) + +o('single_window_margin_width', '-1', option_type=optional_edge_width, long_text=_(''' The window margin (in pts) to use when only a single window is visible. -Negative values will cause the value of :opt:`window_margin_width` to be used instead.''')) +Negative values will cause the value of :opt:`window_margin_width` to be used instead. ''' + edge_desc)) -o('window_padding_width', 0.0, option_type=positive_float, long_text=_(''' -The window padding (in pts) (blank area between the text and the window border)''')) +o('window_padding_width', '0', option_type=edge_width, long_text=_(''' +The window padding (in pts) (blank area between the text and the window border). ''' + edge_desc)) o('placement_strategy', 'center', option_type=choices('center', 'top-left'), long_text=_(''' When the window size is not an exact multiple of the cell size, the cell area of the terminal @@ -1025,7 +1052,6 @@ you also set :opt:`allow_remote_control` to enable remote control. See the help for :option:`kitty --listen-on` for more details. ''')) - o( '+env', '', add_to_default=False, diff --git a/kitty/constants.py b/kitty/constants.py index 81e4a28e5..a63a00d9b 100644 --- a/kitty/constants.py +++ b/kitty/constants.py @@ -34,6 +34,13 @@ class Edges(NamedTuple): bottom: int = 0 +class FloatEdges(NamedTuple): + left: float = 0 + top: float = 0 + right: float = 0 + bottom: float = 0 + + class ScreenGeometry(NamedTuple): xstart: float ystart: float diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 7bdd303a6..b8762c467 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -1132,3 +1132,7 @@ def spawn( def key_to_bytes(glfw_key: int, smkx: bool, extended: bool, mods: int, action: int) -> bytes: pass + + +def set_window_padding(os_window_id: int, tab_id: int, window_id: int, left: int, top: int, right: int, bottom: int) -> None: + pass diff --git a/kitty/layout.py b/kitty/layout.py index a10a76259..cb2d7a604 100644 --- a/kitty/layout.py +++ b/kitty/layout.py @@ -3,9 +3,9 @@ # License: GPL v3 Copyright: 2016, Kovid Goyal from functools import lru_cache, partial -from itertools import islice, repeat +from itertools import repeat from typing import ( - Callable, Collection, Deque, Dict, FrozenSet, Generator, Iterable, List, + Callable, Collection, Dict, FrozenSet, Generator, Iterable, List, NamedTuple, Optional, Sequence, Tuple, Union, cast ) @@ -35,6 +35,7 @@ class LayoutData(NamedTuple): cells_per_window: int space_before: int space_after: int + content_size: int # Utils {{{ @@ -46,7 +47,7 @@ draw_minimal_borders = False draw_active_borders = True align_top_left = False DecorationPairs = Sequence[Tuple[int, int]] -WindowList = Union[List[WindowType], Deque[WindowType]] +WindowList = List[WindowType] LayoutDimension = Generator[LayoutData, None, None] @@ -70,41 +71,15 @@ def idx_for_id(win_id: int, windows: Iterable[WindowType]) -> Optional[int]: return i -def effective_width(q: Optional[int], d: int) -> int: - return d if q is None else q - - def set_layout_options(opts: Options) -> None: global draw_minimal_borders, draw_active_borders, align_top_left - draw_minimal_borders = opts.draw_minimal_borders and opts.window_margin_width == 0 + draw_minimal_borders = opts.draw_minimal_borders and sum(opts.window_margin_width) == 0 draw_active_borders = opts.active_border_color is not None align_top_left = opts.placement_strategy == 'top-left' -def layout_dimension( - start_at: int, length: int, cell_length: int, - decoration_pairs: DecorationPairs, - left_align: bool = False, bias: Optional[Sequence[float]] = None -) -> LayoutDimension: - number_of_windows = len(decoration_pairs) - number_of_cells = length // cell_length - space_needed_for_decorations: int = sum(map(sum, decoration_pairs)) - extra = length - number_of_cells * cell_length - while extra < space_needed_for_decorations: - number_of_cells -= 1 - extra = length - number_of_cells * cell_length +def calculate_cells_map(bias: Optional[Sequence[float]], number_of_windows: int, number_of_cells: int) -> List[int]: cells_per_window = number_of_cells // number_of_windows - extra -= space_needed_for_decorations - pos = start_at - if not left_align: - pos += extra // 2 - - def calc_window_geom(i: int, cells_in_window: int) -> int: - nonlocal pos - pos += decoration_pairs[i][0] - inner_length = cells_in_window * cell_length - return inner_length + decoration_pairs[i][1] - if bias is not None and 1 < 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: @@ -115,23 +90,48 @@ def layout_dimension( 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, + left_align: bool = False, + bias: Optional[Sequence[float]] = None +) -> LayoutDimension: + number_of_windows = len(decoration_pairs) + number_of_cells = length // cell_length + space_needed_for_decorations: int = sum(map(sum, decoration_pairs)) + 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 + if not left_align: + pos += extra // 2 last_i = len(cells_map) - 1 + for i, cells_per_window in enumerate(cells_map): - window_length = calc_window_geom(i, cells_per_window) + before_dec, after_dec = decoration_pairs[i] + pos += before_dec if i == 0: before_space = pos - start_at else: - before_space = decoration_pairs[i][0] + before_space = before_dec + content_size = cells_per_window * cell_length if i == last_i: - after_space = (start_at + length) - pos + window_length + after_space = (start_at + length) - (pos + content_size) else: - after_space = decoration_pairs[i][1] - yield LayoutData(pos, cells_per_window, before_space, after_space) - pos += window_length + after_space = after_dec + yield LayoutData(pos, cells_per_window, before_space, after_space, content_size) + pos += content_size + after_space class Rect(NamedTuple): @@ -219,21 +219,11 @@ class Layout: # {{{ layout_opts = LayoutOpts({}) only_active_window_visible = False - def __init__( - self, - os_window_id: int, tab_id: int, - margin_width: int, single_window_margin_width: int, - padding_width: int, border_width: int, - layout_opts: str = '' - ) -> None: + def __init__(self, os_window_id: int, tab_id: int, layout_opts: str = '') -> None: 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) self.swap_windows_in_os_window = partial(swap_windows, os_window_id, tab_id) - self.border_width = border_width - self.margin_width = margin_width - self.single_window_margin_width = single_window_margin_width - self.padding_width = padding_width # 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] = [] @@ -242,19 +232,13 @@ class Layout: # {{{ self.full_name = self.name + ((':' + layout_opts) if layout_opts else '') self.remove_all_biases() - def update_sizes(self, margin_width: int, single_window_margin_width: int, padding_width: int, border_width: int) -> None: - self.border_width = border_width - self.margin_width = margin_width - self.single_window_margin_width = single_window_margin_width - self.padding_width = padding_width - def bias_increment_for_cell(self, is_horizontal: bool) -> float: self._set_dimensions() if is_horizontal: return (cell_width + 1) / central.width return (cell_height + 1) / central.height - def apply_bias(self, idx: int, increment: float, num_windows: int, is_horizontal: bool = True) -> bool: + def apply_bias(self, idx: int, increment: float, windows: WindowList, is_horizontal: bool = True) -> bool: return False def remove_all_biases(self) -> bool: @@ -270,7 +254,7 @@ class Layout: # {{{ if idx is None and w.overlay_window_id is not None: idx = idx_for_id(w.overlay_window_id, windows) if idx is not None: - return self.apply_bias(idx, increment, len(windows), is_horizontal) + return self.apply_bias(idx, increment, windows, is_horizontal) return False def parse_layout_opts(self, layout_opts: Optional[str] = None) -> LayoutOpts: @@ -486,16 +470,16 @@ class Layout: # {{{ return cast(int, idx_for_id(active_window.id, all_windows)) # Utils {{{ + def layout_single_window(self, w: WindowType, return_geometry: bool = False, left_align: bool = False) -> Optional[WindowGeometry]: - default_margin = self.margin_width if self.single_window_margin_width < 0 else self.single_window_margin_width - bw = self.border_width if self.must_draw_borders else 0 + bw = w.effective_border() if self.must_draw_borders else 0 xdecoration_pairs = (( - effective_width(w.padding.left, self.padding_width) + effective_width(w.margin.left, default_margin) + bw, - effective_width(w.padding.right, self.padding_width) + effective_width(w.margin.right, default_margin) + bw, + w.effective_padding('left') + w.effective_margin('left', is_single_window=True) + bw, + w.effective_padding('right') + w.effective_margin('right', is_single_window=True) + bw, ),) ydecoration_pairs = (( - effective_width(w.padding.top, self.padding_width) + effective_width(w.margin.top, default_margin) + bw, - effective_width(w.padding.bottom, self.padding_width) + effective_width(w.margin.bottom, default_margin) + bw, + w.effective_padding('top') + w.effective_margin('top', is_single_window=True) + bw, + w.effective_padding('bottom') + w.effective_margin('bottom', is_single_window=True) + bw, ),) wg = layout_single_window(xdecoration_pairs, ydecoration_pairs, left_align=left_align) if return_geometry: @@ -505,26 +489,44 @@ class Layout: # {{{ return None def xlayout( - self, num: int, bias: Optional[Sequence[float]] = None, left: Optional[int] = None, width: Optional[int] = None + self, + windows: WindowList, + bias: Optional[Sequence[float]] = None, + start: Optional[int] = None, + size: Optional[int] = None ) -> LayoutDimension: - decoration = self.margin_width + self.border_width + self.padding_width - decoration_pairs = tuple(repeat((decoration, decoration), num)) - if left is None: - left = central.left - if width is None: - width = central.width - return layout_dimension(left, width, cell_width, decoration_pairs, bias=bias, left_align=align_top_left) + decoration_pairs = tuple( + ( + w.effective_margin('left') + w.effective_border() + w.effective_padding('left'), + w.effective_margin('right') + w.effective_border() + w.effective_padding('right'), + ) for w in windows + ) + if start is None: + start = central.left + if size is None: + size = central.width + return layout_dimension(start, size, cell_width, decoration_pairs, bias=bias, left_align=align_top_left) def ylayout( - self, num: int, left_align: bool = True, bias: Optional[Sequence[float]] = None, top: Optional[int] = None, height: Optional[int] = None + self, windows: WindowList, bias: Optional[Sequence[float]] = None, start: Optional[int] = None, size: Optional[int] = None ) -> LayoutDimension: - decoration = self.margin_width + self.border_width + self.padding_width - decoration_pairs = tuple(repeat((decoration, decoration), num)) - if top is None: - top = central.top - if height is None: - height = central.height - return layout_dimension(top, height, cell_height, decoration_pairs, bias=bias, left_align=align_top_left) + decoration_pairs = tuple( + ( + w.effective_margin('top') + w.effective_border() + w.effective_padding('top'), + w.effective_margin('bottom') + w.effective_border() + w.effective_padding('bottom'), + ) for w in windows + ) + if start is None: + start = central.top + if size is None: + size = central.height + return layout_dimension(start, size, cell_height, decoration_pairs, bias=bias, left_align=align_top_left) + + def set_window_geometry(self, w: WindowType, idx: int, xl: LayoutData, yl: LayoutData) -> None: + wg = window_geometry_from_layouts(xl, yl) + w.set_geometry(idx, wg) + self.blank_rects.extend(blank_rects_for_window(wg)) + # }}} def do_layout(self, windows: WindowList, active_window_idx: int) -> None: @@ -626,6 +628,8 @@ class Tall(Layout): only_between_border = Borders(False, False, False, True) only_main_border = Borders(False, False, True, False) layout_opts = TallLayoutOpts({}) + main_axis_layout = Layout.xlayout + perp_axis_layout = Layout.ylayout @property def num_full_size_windows(self) -> int: @@ -636,14 +640,13 @@ class Tall(Layout): self.biased_map: Dict[int, float] = {} return True - def variable_layout(self, num_windows: int, biased_map: Dict[int, float]) -> LayoutDimension: - num_windows -= self.num_full_size_windows - bias = variable_bias(num_windows, biased_map) if num_windows > 1 else None - if self.main_is_horizontal: - return self.ylayout(num_windows, bias=bias) - return self.xlayout(num_windows, bias=bias) + def variable_layout(self, windows: WindowList, biased_map: Dict[int, float]) -> LayoutDimension: + windows = windows[self.num_full_size_windows:] + bias = variable_bias(len(windows), biased_map) if len(windows) > 1 else None + return self.perp_axis_layout(windows, bias=bias) - def apply_bias(self, idx: int, increment: float, num_windows: int, is_horizontal: bool = True) -> bool: + def apply_bias(self, idx: int, increment: float, windows: WindowList, is_horizontal: bool = True) -> bool: + num_windows = len(windows) if self.main_is_horizontal == is_horizontal: before_main_bias = self.main_bias ncols = self.num_full_size_windows + 1 @@ -658,11 +661,11 @@ class Tall(Layout): if idx < self.num_full_size_windows or num_of_short_windows < 2: return False idx -= self.num_full_size_windows - before_layout = list(self.variable_layout(num_windows, self.biased_map)) + before_layout = list(self.variable_layout(windows, self.biased_map)) before = self.biased_map.get(idx, 0.) candidate = self.biased_map.copy() candidate[idx] = after = before + increment - if before_layout == list(self.variable_layout(num_windows, candidate)): + if before_layout == list(self.variable_layout(windows, candidate)): return False self.biased_map = candidate return before != after @@ -671,29 +674,38 @@ class Tall(Layout): if len(windows) == 1: self.layout_single_window(windows[0]) return - yl = next(self.ylayout(1)) - if len(windows) <= self.num_full_size_windows: - bias = normalize_biases(self.main_bias[:-1]) - xlayout = self.xlayout(self.num_full_size_windows, bias=bias) + is_fat = not self.main_is_horizontal + if len(windows) <= self.num_full_size_windows + 1: + xlayout = self.main_axis_layout(windows, bias=self.main_bias) for i, (w, xl) in enumerate(zip(windows, xlayout)): - wg = window_geometry_from_layouts(xl, yl) - w.set_geometry(i, wg) - self.blank_rects.extend(blank_rects_for_window(wg)) + yl = next(self.perp_axis_layout([w])) + if is_fat: + xl, yl = yl, xl + self.set_window_geometry(w, i, xl, yl) return - xlayout = self.xlayout(self.num_full_size_windows + 1, bias=self.main_bias) - for i in range(self.num_full_size_windows): - w = windows[i] + xlayout = self.main_axis_layout(windows[:self.num_full_size_windows + 1], bias=self.main_bias) + attr: EdgeLiteral = 'bottom' if is_fat else 'right' + start = central.top if is_fat else central.left + for i, w in enumerate(windows): + if i >= self.num_full_size_windows: + break xl = next(xlayout) - wg = window_geometry_from_layouts(xl, yl) - w.set_geometry(i, wg) - self.blank_rects.extend(blank_rects_for_window(wg)) - xl = next(xlayout) - ylayout = self.variable_layout(len(windows), self.biased_map) - for i, (w, yl) in enumerate(zip(islice(windows, self.num_full_size_windows, None), ylayout)): - wg = window_geometry_from_layouts(xl, yl) - w.set_geometry(i + self.num_full_size_windows, wg) - self.blank_rects.extend(blank_rects_for_window(wg)) + yl = next(self.perp_axis_layout([w])) + if is_fat: + xl, yl = yl, xl + self.set_window_geometry(w, i, xl, yl) + start = getattr(w.geometry, attr) + w.effective_border() + w.effective_margin(attr) + w.effective_padding(attr) + ylayout = self.variable_layout(windows, self.biased_map) + size = (central.height if is_fat else central.width) - start + for i, w in enumerate(windows): + if i < self.num_full_size_windows: + continue + yl = next(ylayout) + xl = next(self.main_axis_layout([w], start=start, size=size)) + if is_fat: + xl, yl = yl, xl + self.set_window_geometry(w, i, xl, yl) def neighbors_for_window(self, window: WindowType, windows: WindowList) -> InternalNeighborsMap: return neighbors_for_tall_window(self.num_full_size_windows, window, windows) @@ -726,34 +738,8 @@ class Fat(Tall): # {{{ main_is_horizontal = False only_between_border = Borders(False, False, True, False) only_main_border = Borders(False, False, False, True) - - def do_layout(self, windows: WindowList, active_window_idx: int) -> None: - if len(windows) == 1: - self.layout_single_window(windows[0]) - return - xl = next(self.xlayout(1)) - if len(windows) <= self.num_full_size_windows: - bias = normalize_biases(self.main_bias[:-1]) - ylayout = self.ylayout(self.num_full_size_windows, bias=bias) - for i, (w, yl) in enumerate(zip(windows, ylayout)): - wg = window_geometry_from_layouts(xl, yl) - w.set_geometry(i, wg) - self.blank_rects.extend(blank_rects_for_window(wg)) - return - - ylayout = self.ylayout(self.num_full_size_windows + 1, bias=self.main_bias) - for i in range(self.num_full_size_windows): - w = windows[i] - yl = next(ylayout) - wg = window_geometry_from_layouts(xl, yl) - w.set_geometry(i, wg) - self.blank_rects.extend(blank_rects_for_window(wg)) - yl = next(ylayout) - xlayout = self.variable_layout(len(windows), self.biased_map) - for i, (w, xl) in enumerate(zip(islice(windows, self.num_full_size_windows, None), xlayout)): - wg = window_geometry_from_layouts(xl, yl) - w.set_geometry(i + self.num_full_size_windows, wg) - self.blank_rects.extend(blank_rects_for_window(wg)) + main_axis_layout = Layout.ylayout + perp_axis_layout = Layout.xlayout def neighbors_for_window(self, window: WindowType, windows: WindowList) -> InternalNeighborsMap: idx = windows.index(window) @@ -800,11 +786,28 @@ class Grid(Layout): 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(central.left, central.width, cell_width, decoration_pairs, bias=bias, left_align=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(central.top, central.height, cell_height, decoration_pairs, bias=bias, left_align=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, num_windows: int, is_horizontal: bool = True) -> bool: + def apply_bias(self, idx: int, increment: float, windows: WindowList, is_horizontal: bool = True) -> bool: b = self.biased_cols if is_horizontal else self.biased_rows + num_windows = len(windows) ncols, nrows, special_rows, special_col = calc_grid_size(num_windows) def position_for_window_idx(idx: int) -> Tuple[int, int]: @@ -830,8 +833,8 @@ class Grid(Layout): bias_idx = col_num attr = 'biased_cols' - def layout_func(num_windows: int, bias: Optional[Sequence[float]] = None) -> LayoutDimension: - return self.xlayout(num_windows, bias=bias) + def layout_func(windows: WindowList, bias: Optional[Sequence[float]] = None) -> LayoutDimension: + return self.column_layout(num_windows, bias=bias) else: b = self.biased_rows @@ -840,8 +843,8 @@ class Grid(Layout): bias_idx = row_num attr = 'biased_rows' - def layout_func(num_windows: int, bias: Optional[Sequence[float]] = None) -> LayoutDimension: - return self.xlayout(num_windows, bias=bias) + def layout_func(windows: WindowList, 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() @@ -860,9 +863,9 @@ class Grid(Layout): 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.xlayout, ncols, self.biased_cols) - yvals_normal = tuple(self.variable_layout(self.ylayout, nrows, self.biased_rows)) - yvals_special = yvals_normal if special_rows == nrows else tuple(self.variable_layout(self.ylayout, special_rows, self.biased_rows)) + 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 @@ -889,12 +892,19 @@ class Grid(Layout): 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 + for window_idx, xl, yl in self.layout_windows( len(windows), nrows, ncols, special_rows, special_col, on_col_done): w = windows[window_idx] - wg = window_geometry_from_layouts(xl, yl) - w.set_geometry(window_idx, wg) - self.blank_rects.extend(blank_rects_for_window(wg)) + start, size = extents(xl) + xl = next(self.xlayout([w], start=start, size=size)) + start, size = extents(yl) + yl = next(self.ylayout([w], start=start, size=size)) + self.set_window_geometry(w, window_idx, xl, yl) def minimal_borders(self, windows: WindowList, active_window: Optional[WindowType], needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]: n = len(windows) @@ -981,27 +991,29 @@ class Vertical(Layout): # {{{ name = 'vertical' main_is_horizontal = False only_between_border = Borders(False, False, False, True) + main_axis_layout = Layout.ylayout + perp_axis_layout = Layout.xlayout - def variable_layout(self, num_windows: int, biased_map: Dict[int, float]) -> LayoutDimension: + def variable_layout(self, windows: WindowList, biased_map: Dict[int, float]) -> LayoutDimension: + num_windows = len(windows) bias = variable_bias(num_windows, biased_map) if num_windows else None - if self.main_is_horizontal: - return self.xlayout(num_windows, bias=bias) - return self.ylayout(num_windows, bias=bias) + return self.main_axis_layout(windows, bias=bias) def remove_all_biases(self) -> bool: self.biased_map: Dict[int, float] = {} return True - def apply_bias(self, idx: int, increment: float, num_windows: int, is_horizontal: bool = True) -> bool: + def apply_bias(self, idx: int, increment: float, windows: WindowList, is_horizontal: bool = True) -> bool: if self.main_is_horizontal != is_horizontal: return False + num_windows = len(windows) if num_windows < 2: return False - before_layout = list(self.variable_layout(num_windows, self.biased_map)) + before_layout = list(self.variable_layout(windows, self.biased_map)) candidate = self.biased_map.copy() before = candidate.get(idx, 0) candidate[idx] = before + increment - if before_layout == list(self.variable_layout(num_windows, candidate)): + if before_layout == list(self.variable_layout(windows, candidate)): return False self.biased_map = candidate return True @@ -1012,13 +1024,12 @@ class Vertical(Layout): # {{{ self.layout_single_window(windows[0]) return - xlayout = self.xlayout(1) - xl = next(xlayout) - ylayout = self.variable_layout(window_count, self.biased_map) + ylayout = self.variable_layout(windows, self.biased_map) for i, (w, yl) in enumerate(zip(windows, ylayout)): - wg = window_geometry_from_layouts(xl, yl) - w.set_geometry(i, wg) - self.blank_rects.extend(blank_rects_for_window(wg)) + xl = next(self.perp_axis_layout([w])) + if self.main_is_horizontal: + xl, yl = yl, xl + self.set_window_geometry(w, i, xl, yl) def minimal_borders(self, windows: WindowList, active_window: Optional[WindowType], needs_borders_map: Dict[int, bool]) -> Generator[Borders, None, None]: last_i = len(windows) - 1 @@ -1050,20 +1061,8 @@ class Horizontal(Vertical): # {{{ name = 'horizontal' main_is_horizontal = True only_between_border = Borders(False, False, True, False) - - def do_layout(self, windows: WindowList, active_window_idx: int) -> None: - window_count = len(windows) - if window_count == 1: - self.layout_single_window(windows[0]) - return - - xlayout = self.variable_layout(window_count, self.biased_map) - ylayout = self.ylayout(1) - yl = next(ylayout) - for i, (w, xl) in enumerate(zip(windows, xlayout)): - wg = window_geometry_from_layouts(xl, yl) - w.set_geometry(i, wg) - self.blank_rects.extend(blank_rects_for_window(wg)) + main_axis_layout = Layout.xlayout + perp_axis_layout = Layout.ylayout # }}} @@ -1199,6 +1198,11 @@ class Pair: q.set_geometry(id_idx_map[q.id], window_geometry) layout_object.blank_rects.extend(blank_rects_for_window(window_geometry)) + def effective_border(self, id_window_map: Dict[int, WindowType]) -> int: + for wid in self.all_window_ids(): + return id_window_map[wid].effective_border() + return 0 + def layout_pair( self, left: int, top: int, width: int, height: int, @@ -1213,22 +1217,24 @@ class Pair: return q.layout_pair(left, top, width, height, id_window_map, id_idx_map, layout_object) if q is None: return - xl = next(layout_object.xlayout(1, left=left, width=width)) - yl = next(layout_object.ylayout(1, top=top, height=height)) + w = id_window_map[q] + xl = next(layout_object.xlayout([w], start=left, size=width)) + yl = next(layout_object.ylayout([w], start=top, size=height)) geom = window_geometry_from_layouts(xl, yl) self.apply_window_geometry(q, geom, id_window_map, id_idx_map, layout_object) return - bw = layout_object.border_width if draw_minimal_borders else 0 + bw = self.effective_border(id_window_map) if draw_minimal_borders else 0 b1 = bw // 2 b2 = bw - b1 if self.horizontal: - yl = next(layout_object.ylayout(1, top=top, height=height)) w1 = max(2*cell_width + 1, int(self.bias * width) - b1) w2 = max(2*cell_width + 1, width - w1 - b1 - b2) if isinstance(self.one, Pair): self.one.layout_pair(left, top, w1, height, id_window_map, id_idx_map, layout_object) else: - xl = next(layout_object.xlayout(1, left=left, width=w1)) + w = id_window_map[self.one] + yl = next(layout_object.ylayout([w], start=top, size=height)) + xl = next(layout_object.xlayout([w], start=left, size=w1)) geom = window_geometry_from_layouts(xl, yl) self.apply_window_geometry(self.one, geom, id_window_map, id_idx_map, layout_object) if b1 + b2: @@ -1237,17 +1243,20 @@ class Pair: if isinstance(self.two, Pair): self.two.layout_pair(left + w1, top, w2, height, id_window_map, id_idx_map, layout_object) else: - xl = next(layout_object.xlayout(1, left=left + w1, width=w2)) + w = id_window_map[self.two] + xl = next(layout_object.xlayout([w], start=left + w1, size=w2)) + yl = next(layout_object.ylayout([w], start=top, size=height)) geom = window_geometry_from_layouts(xl, yl) self.apply_window_geometry(self.two, geom, id_window_map, id_idx_map, layout_object) else: - xl = next(layout_object.xlayout(1, left=left, width=width)) h1 = max(2*cell_height + 1, int(self.bias * height) - b1) h2 = max(2*cell_height + 1, height - h1 - b1 - b2) if isinstance(self.one, Pair): self.one.layout_pair(left, top, width, h1, id_window_map, id_idx_map, layout_object) else: - yl = next(layout_object.ylayout(1, top=top, height=h1)) + w = id_window_map[self.one] + xl = next(layout_object.xlayout([w], start=left, size=width)) + yl = next(layout_object.ylayout([w], start=top, size=h1)) geom = window_geometry_from_layouts(xl, yl) self.apply_window_geometry(self.one, geom, id_window_map, id_idx_map, layout_object) if b1 + b2: @@ -1256,7 +1265,9 @@ class Pair: if isinstance(self.two, Pair): self.two.layout_pair(left, top + h1, width, h2, id_window_map, id_idx_map, layout_object) else: - yl = next(layout_object.ylayout(1, top=top + h1, height=h2)) + w = id_window_map[self.two] + xl = next(layout_object.xlayout([w], start=left, size=width)) + yl = next(layout_object.ylayout([w], start=top + h1, size=h2)) geom = window_geometry_from_layouts(xl, yl) self.apply_window_geometry(self.two, geom, id_window_map, id_idx_map, layout_object) @@ -1521,18 +1532,14 @@ class CreateLayoutObjectFor: name: str, os_window_id: int, tab_id: int, - margin_width: int, - single_window_margin_width: int, - padding_width: int, - border_width: int, layout_opts: str = '' ) -> Layout: - key = name, os_window_id, tab_id, margin_width, single_window_margin_width, padding_width, border_width, layout_opts + key = name, os_window_id, tab_id, layout_opts ans = create_layout_object_for.cache.get(key) if ans is None: name, layout_opts = name.partition(':')[::2] ans = create_layout_object_for.cache[key] = all_layouts[name]( - os_window_id, tab_id, margin_width, single_window_margin_width, padding_width, border_width, layout_opts) + os_window_id, tab_id, layout_opts) return ans diff --git a/kitty/mouse.c b/kitty/mouse.c index 19b7cbd06..8644c7d20 100644 --- a/kitty/mouse.c +++ b/kitty/mouse.c @@ -97,37 +97,37 @@ encode_mouse_event(Window *w, int button, MouseAction action, int mods) { // }}} -static inline double -window_left(Window *w, OSWindow *os_window) { - return w->geometry.left - OPT(window_padding_width) * (os_window->logical_dpi_x / 72.0); +static inline unsigned int +window_left(Window *w) { + return w->geometry.left - w->padding.left; } -static inline double -window_right(Window *w, OSWindow *os_window) { - return w->geometry.right + OPT(window_padding_width) * (os_window->logical_dpi_x / 72.0); +static inline unsigned int +window_right(Window *w) { + return w->geometry.right + w->padding.right; } -static inline double -window_top(Window *w, OSWindow *os_window) { - return w->geometry.top - OPT(window_padding_width) * (os_window->logical_dpi_y / 72.0); +static inline unsigned int +window_top(Window *w) { + return w->geometry.top - w->padding.top; } -static inline double -window_bottom(Window *w, OSWindow *os_window) { - return w->geometry.bottom + OPT(window_padding_width) * (os_window->logical_dpi_y / 72.0); +static inline unsigned int +window_bottom(Window *w) { + return w->geometry.bottom + w->padding.bottom; } static inline bool -contains_mouse(Window *w, OSWindow *os_window) { +contains_mouse(Window *w) { double x = global_state.callback_os_window->mouse_x, y = global_state.callback_os_window->mouse_y; - return (w->visible && window_left(w, os_window) <= x && x <= window_right(w, os_window) && window_top(w, os_window) <= y && y <= window_bottom(w, os_window)); + return (w->visible && window_left(w) <= x && x <= window_right(w) && window_top(w) <= y && y <= window_bottom(w)); } static inline double -distance_to_window(Window *w, OSWindow *os_window) { +distance_to_window(Window *w) { double x = global_state.callback_os_window->mouse_x, y = global_state.callback_os_window->mouse_y; - double cx = (window_left(w, os_window) + window_right(w, os_window)) / 2.0; - double cy = (window_top(w, os_window) + window_bottom(w, os_window)) / 2.0; + double cx = (window_left(w) + window_right(w)) / 2.0; + double cy = (window_top(w) + window_bottom(w)) / 2.0; return (x - cx) * (x - cx) + (y - cy) * (y - cy); } @@ -142,7 +142,7 @@ cell_for_pos(Window *w, unsigned int *x, unsigned int *y, bool *in_left_half_of_ bool in_left_half = true; double mouse_x = global_state.callback_os_window->mouse_x; double mouse_y = global_state.callback_os_window->mouse_y; - double left = window_left(w, os_window), top = window_top(w, os_window), right = window_right(w, os_window), bottom = window_bottom(w, os_window); + double left = window_left(w), top = window_top(w), right = window_right(w), bottom = window_bottom(w); if (clamp_to_window) { mouse_x = MIN(MAX(mouse_x, left), right); mouse_y = MIN(MAX(mouse_y, top), bottom); @@ -513,7 +513,7 @@ window_for_event(unsigned int *window_idx, bool *in_tab_bar) { if (!*in_tab_bar && global_state.callback_os_window->num_tabs > 0) { Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab; for (unsigned int i = 0; i < t->num_windows; i++) { - if (contains_mouse(t->windows + i, global_state.callback_os_window) && t->windows[i].render_data.screen) { + if (contains_mouse(t->windows + i) && t->windows[i].render_data.screen) { *window_idx = i; return t->windows + i; } } @@ -529,7 +529,7 @@ closest_window_for_event(unsigned int *window_idx) { Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab; for (unsigned int i = 0; i < t->num_windows; i++) { Window *w = t->windows + i; - double d = distance_to_window(w, global_state.callback_os_window); + double d = distance_to_window(w); if (d < closest_distance) { ans = w; closest_distance = d; *window_idx = i; } } } diff --git a/kitty/session.py b/kitty/session.py index 5f8f8b258..e101ea336 100644 --- a/kitty/session.py +++ b/kitty/session.py @@ -8,7 +8,7 @@ from typing import Generator, List, NamedTuple, Optional, Tuple, Union from .cli_stub import CLIOptions from .config_data import to_layout_names -from .constants import kitty_exe +from .constants import FloatEdges, kitty_exe from .layout import all_layouts from .options_stub import Options from .typing import SpecialWindowInstance @@ -19,8 +19,8 @@ class WindowSizeOpts(NamedTuple): initial_window_width: Tuple[int, str] initial_window_height: Tuple[int, str] - window_margin_width: float - window_padding_width: float + window_margin_width: FloatEdges + window_padding_width: FloatEdges remember_window_size: bool diff --git a/kitty/state.c b/kitty/state.c index e8432b032..6355e8f06 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -27,13 +27,25 @@ GlobalState global_state = {{0}}; #define END_WITH_OS_WINDOW break; }} #define WITH_TAB(os_window_id, tab_id) \ - for (size_t o = 0; o < global_state.num_os_windows; o++) { \ + for (size_t o = 0, tab_found = 0; o < global_state.num_os_windows && !tab_found; o++) { \ OSWindow *osw = global_state.os_windows + o; \ if (osw->id == os_window_id) { \ for (size_t t = 0; t < osw->num_tabs; t++) { \ if (osw->tabs[t].id == tab_id) { \ Tab *tab = osw->tabs + t; -#define END_WITH_TAB break; }}}} +#define END_WITH_TAB tab_found = 1; break; }}}} + +#define WITH_WINDOW(os_window_id, tab_id, window_id) \ + for (size_t o = 0, window_found = 0; o < global_state.num_os_windows && !window_found; o++) { \ + OSWindow *osw = global_state.os_windows + o; \ + if (osw->id == os_window_id) { \ + for (size_t t = 0; t < osw->num_tabs && !window_found; t++) { \ + if (osw->tabs[t].id == tab_id) { \ + Tab *tab = osw->tabs + t; \ + for (size_t w = 0; w < tab->num_windows; w++) { \ + Window *window = tab->windows + w; +#define END_WITH_WINDOW break; }}}}} + #define WITH_OS_WINDOW_REFS \ id_type cb_window_id = 0, focused_window_id = 0; \ @@ -603,7 +615,6 @@ PYWRAP1(set_options) { S(dim_opacity, PyFloat_AsFloat); S(dynamic_background_opacity, PyObject_IsTrue); S(inactive_text_alpha, PyFloat_AsFloat); - S(window_padding_width, PyFloat_AsFloat); S(scrollback_pager_history_size, PyLong_AsUnsignedLong); S(cursor_shape, PyLong_AsLong); S(cursor_beam_thickness, PyFloat_AsFloat); @@ -839,6 +850,16 @@ fix_window_idx(Tab *tab, id_type window_id, unsigned int *window_idx) { return false; } +PYWRAP1(set_window_padding) { + id_type os_window_id, tab_id, window_id; + unsigned int left, top, right, bottom; + PA("KKKIIII", &os_window_id, &tab_id, &window_id, &left, &top, &right, &bottom); + WITH_WINDOW(os_window_id, tab_id, window_id); + window->padding.left = left; window->padding.top = top; window->padding.right = right; window->padding.bottom = bottom; + END_WITH_WINDOW; + Py_RETURN_NONE; +} + PYWRAP1(set_window_render_data) { #define A(name) &(d.name) #define B(name) &(g.name) @@ -1080,6 +1101,7 @@ static PyMethodDef module_methods[] = { MW(add_borders_rect, METH_VARARGS), MW(set_tab_bar_render_data, METH_VARARGS), MW(set_window_render_data, METH_VARARGS), + MW(set_window_padding, METH_VARARGS), MW(viewport_for_window, METH_VARARGS), MW(cell_size_for_window, METH_VARARGS), MW(os_window_has_background_image, METH_VARARGS), diff --git a/kitty/state.h b/kitty/state.h index 0a55e0c94..33d0b9672 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -53,7 +53,6 @@ typedef struct { bool dynamic_background_opacity; float inactive_text_alpha; - float window_padding_width; Edge tab_bar_edge; unsigned long tab_bar_min_tabs; DisableLigature disable_ligatures; @@ -106,6 +105,9 @@ typedef struct { double x, y; bool in_left_half_of_cell; } mouse_pos; + struct { + unsigned int left, top, right, bottom; + } padding; WindowGeometry geometry; ClickQueue click_queue; monotonic_t last_drag_scroll_at; diff --git a/kitty/tabs.py b/kitty/tabs.py index 0406cc3f7..7d8207d4f 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -17,8 +17,8 @@ from .cli_stub import CLIOptions from .constants import appname, is_macos, is_wayland from .fast_data_types import ( add_tab, attach_window, detach_window, get_boss, mark_tab_bar_dirty, - next_window_id, pt_to_px, remove_tab, remove_window, ring_bell, - set_active_tab, swap_tabs, x11_window_id + next_window_id, remove_tab, remove_window, ring_bell, set_active_tab, + swap_tabs, x11_window_id ) from .layout import ( Layout, Rect, create_layout_object_for, evict_cached_layouts @@ -87,11 +87,10 @@ class Tab: # {{{ if not self.id: raise Exception('No OS window with id {} found, or tab counter has wrapped'.format(self.os_window_id)) self.opts, self.args = tab_manager.opts, tab_manager.args - self.recalculate_sizes(update_layout=False) self.name = getattr(session_tab, 'name', '') self.enabled_layouts = [x.lower() for x in getattr(session_tab, 'enabled_layouts', None) or self.opts.enabled_layouts] self.borders = Borders(self.os_window_id, self.id, self.opts) - self.windows: Deque[Window] = deque() + self.windows: List[Window] = [] for i, which in enumerate('first second third fourth fifth sixth seventh eighth ninth tenth'.split()): setattr(self, which + '_window', partial(self.nth_window, num=i)) self._last_used_layout: Optional[str] = None @@ -113,15 +112,6 @@ class Tab: # {{{ self._set_current_layout(l0) self.startup(session_tab) - def recalculate_sizes(self, update_layout: bool = True) -> None: - self.margin_width, self.padding_width, self.single_window_margin_width = map( - lambda x: pt_to_px(getattr(self.opts, x), self.os_window_id), ( - 'window_margin_width', 'window_padding_width', 'single_window_margin_width')) - self.border_width = pt_to_px(self.opts.window_border_width, self.os_window_id) - if update_layout and self.current_layout: - self.current_layout.update_sizes( - self.margin_width, self.single_window_margin_width, self.padding_width, self.border_width) - def take_over_from(self, other_tab: 'Tab') -> None: self.name, self.cwd = other_tab.name, other_tab.cwd self.enabled_layouts = list(other_tab.enabled_layouts) @@ -129,12 +119,12 @@ class Tab: # {{{ self._set_current_layout(other_tab._current_layout_name) self._last_used_layout = other_tab._last_used_layout - orig_windows = deque(other_tab.windows) + orig_windows = list(other_tab.windows) orig_history = deque(other_tab.active_window_history) orig_active = other_tab._active_window_idx for window in other_tab.windows: detach_window(other_tab.os_window_id, other_tab.id, window.id) - other_tab.windows = deque() + other_tab.windows = [] other_tab._active_window_idx = 0 self.active_window_history = orig_history self.windows = orig_windows @@ -243,17 +233,13 @@ class Tab: # {{{ self.borders( windows=visible_windows, active_window=w, current_layout=ly, extra_blank_rects=tm.blank_rects, - padding_width=self.padding_width, border_width=self.border_width, draw_window_borders=(ly.needs_window_borders and len(visible_windows) > 1) or ly.must_draw_borders ) if w is not None: w.change_titlebar_color() def create_layout_object(self, name: str) -> Layout: - return create_layout_object_for( - name, self.os_window_id, self.id, self.margin_width, - self.single_window_margin_width, self.padding_width, - self.border_width) + return create_layout_object_for(name, self.os_window_id, self.id) def next_layout(self) -> None: if len(self.enabled_layouts) > 1: @@ -559,7 +545,7 @@ class Tab: # {{{ evict_cached_layouts(self.id) for w in self.windows: w.destroy() - self.windows = deque() + self.windows = [] def __repr__(self) -> str: return 'Tab(title={}, id={})'.format(self.name or self.title, hex(id(self))) @@ -653,10 +639,6 @@ class TabManager: # {{{ def update_tab_bar_data(self) -> None: self.tab_bar.update(self.tab_bar_data) - def update_dpi_based_sizes(self) -> None: - for tab in self.tabs: - tab.recalculate_sizes() - def resize(self, only_tabs: bool = False) -> None: if not only_tabs: if not self.tab_bar_hidden: diff --git a/kitty/window.py b/kitty/window.py index cdcf9e22b..8ce6070c4 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -25,14 +25,15 @@ from .fast_data_types import ( MARK, MARK_MASK, OSC, REVERSE, SCROLL_FULL, SCROLL_LINE, SCROLL_PAGE, STRIKETHROUGH, TINT_PROGRAM, Screen, add_window, cell_size_for_window, compile_program, get_boss, get_clipboard_string, init_cell_program, - set_clipboard_string, set_titlebar_color, set_window_render_data, - update_window_title, update_window_visibility, viewport_for_window + pt_to_px, set_clipboard_string, set_titlebar_color, set_window_padding, + set_window_render_data, update_window_title, update_window_visibility, + viewport_for_window ) from .keys import defines, extended_key_event, keyboard_mode_name from .options_stub import Options from .rgb import to_color from .terminfo import get_capabilities -from .typing import BossType, ChildType, TabType, TypedDict +from .typing import BossType, ChildType, EdgeLiteral, TabType, TypedDict from .utils import ( color_as_int, get_primary_selection, load_shaders, open_cmd, open_url, parse_color_set, read_shell_environment, sanitize_title, @@ -186,12 +187,12 @@ def text_sanitizer(as_ansi: bool, add_wrap_markers: bool) -> Callable[[str], str class EdgeWidths: - left: Optional[int] - top: Optional[int] - right: Optional[int] - bottom: Optional[int] + left: Optional[float] + top: Optional[float] + right: Optional[float] + bottom: Optional[float] - def __init__(self, serialized: Optional[Dict[str, Optional[int]]] = None): + def __init__(self, serialized: Optional[Dict[str, Optional[float]]] = None): if serialized is not None: self.left = serialized['left'] self.right = serialized['right'] @@ -200,7 +201,7 @@ class EdgeWidths: else: self.left = self.top = self.right = self.bottom = None - def serialize(self) -> Dict[str, Optional[int]]: + def serialize(self) -> Dict[str, Optional[float]]: return {'left': self.left, 'right': self.right, 'top': self.top, 'bottom': self.bottom} @@ -250,11 +251,47 @@ class Window: else: setup_colors(self.screen, opts) + def on_dpi_change(self, font_sz: float) -> None: + self.update_effective_padding() + def change_tab(self, tab: TabType) -> None: self.tab_id = tab.id self.os_window_id = tab.os_window_id self.tabref = weakref.ref(tab) + def effective_margin(self, edge: EdgeLiteral, is_single_window: bool = False) -> int: + q = getattr(self.margin, edge) + if q is not None: + return pt_to_px(q, self.os_window_id) + if is_single_window: + q = getattr(self.opts.single_window_margin_width, edge) + if q > -0.1: + return pt_to_px(q, self.os_window_id) + q = getattr(self.opts.window_margin_width, edge) + return pt_to_px(q, self.os_window_id) + + def effective_padding(self, edge: EdgeLiteral) -> int: + q = getattr(self.padding, edge) + if q is not None: + return pt_to_px(q, self.os_window_id) + q = getattr(self.opts.window_padding_width, edge) + return pt_to_px(q, self.os_window_id) + + def update_effective_padding(self) -> None: + set_window_padding( + self.os_window_id, self.tab_id, self.id, + self.effective_padding('left'), self.effective_padding('top'), + self.effective_padding('right'), self.effective_padding('bottom')) + + def patch_edge_width(self, which: str, edge: EdgeLiteral, val: Optional[float]) -> None: + q = self.padding if which == 'padding' else self.margin + setattr(q, edge, val) + if q is self.padding: + self.update_effective_padding() + + def effective_border(self) -> int: + return pt_to_px(self.opts.window_border_width, self.os_window_id) + @property def title(self) -> str: return self.override_title or self.child_title @@ -362,6 +399,7 @@ class Window: sg = self.update_position(new_geometry) self.geometry = g = new_geometry set_window_render_data(self.os_window_id, self.tab_id, self.id, window_idx, sg.xstart, sg.ystart, sg.dx, sg.dy, self.screen, *g[:4]) + self.update_effective_padding() def contains(self, x: int, y: int) -> bool: g = self.geometry diff --git a/kitty_tests/layout.py b/kitty_tests/layout.py index 2b5b50584..a36ef9910 100644 --- a/kitty_tests/layout.py +++ b/kitty_tests/layout.py @@ -4,7 +4,6 @@ from kitty.config import defaults from kitty.constants import WindowGeometry -from kitty.fast_data_types import pt_to_px from kitty.layout import Grid, Horizontal, Splits, Stack, Tall, idx_for_id from kitty.window import EdgeWidths @@ -22,6 +21,15 @@ class Window: self.padding = EdgeWidths() self.margin = EdgeWidths() + def effective_border(self): + return 1 + + def effective_padding(self, edge): + return 1 + + def effective_margin(self, edge, is_single_window=False): + return 0 if is_single_window else 1 + def set_visible_in_layout(self, idx, val): self.is_visible_in_layout = bool(val) @@ -32,8 +40,7 @@ class Window: def create_layout(cls, opts=None, border_width=2): if opts is None: opts = defaults - mw, pw = map(pt_to_px, (opts.window_margin_width, opts.window_padding_width)) - ans = cls(1, 1, mw, mw, pw, border_width) + ans = cls(1, 1) ans.set_active_window_in_os_window = lambda idx: None ans.swap_windows_in_os_window = lambda a, b: None return ans