Merge branch 'draw-borders-when-focused' of https://github.com/jackielii/kitty

This commit is contained in:
Kovid Goyal
2025-10-15 08:56:02 +05:30
8 changed files with 71 additions and 10 deletions

View File

@@ -6,7 +6,7 @@ from enum import IntFlag
from functools import partial
from typing import NamedTuple
from .fast_data_types import BORDERS_PROGRAM, get_options, init_borders_program, set_borders_rects
from .fast_data_types import BORDERS_PROGRAM, current_focused_os_window_id, get_options, init_borders_program, set_borders_rects
from .shaders import program_for
from .typing_compat import LayoutType
from .utils import color_as_int
@@ -84,7 +84,6 @@ class Borders:
) -> None:
opts = get_options()
draw_active_borders = opts.active_border_color is not None
draw_minimal_borders = opts.draw_minimal_borders and max(opts.window_margin_width) < 1
rects: list[Border] = []
for br in current_layout.blank_rects:
rects.append(Border(*br, BorderColor.default_bg))
@@ -96,12 +95,28 @@ class Borders:
draw_borders = bw > 0 and draw_window_borders
active_group = all_windows.active_group
# Count visible windows
num_visible_groups = len(groups)
# When draw_window_borders_for_single_window is set and there's only 1 window,
# behave like draw_minimal_borders is False (draw full borders around the window)
if opts.draw_window_borders_for_single_window and num_visible_groups == 1:
draw_minimal_borders = False
else:
draw_minimal_borders = opts.draw_minimal_borders and max(opts.window_margin_width) < 1
# For single window with the option enabled, check OS window focus state
# When unfocused, the border should appear inactive
os_window_focused = True
if opts.draw_window_borders_for_single_window and num_visible_groups == 1:
os_window_focused = current_focused_os_window_id() == self.os_window_id
for i, wg in enumerate(groups):
window_bg = color_as_int(wg.default_bg)
window_bg = (window_bg << 8) | BorderColor.window_bg
if draw_borders and not draw_minimal_borders:
# Draw the border rectangles
if wg is active_group and draw_active_borders:
if wg is active_group and draw_active_borders and os_window_focused:
color = BorderColor.active
else:
color = BorderColor.bell if wg.needs_attention else BorderColor.inactive

View File

@@ -1854,6 +1854,13 @@ class Boss:
if is_macos and focused:
cocoa_set_menubar_title(w.title or '')
tm.mark_tab_bar_dirty()
# Redraw borders when focus changes if draw_window_borders_for_single_window is enabled
# and there's only a single window (to show inactive border when OS window loses focus)
opts = get_options()
if opts.draw_window_borders_for_single_window and tm.active_tab is not None:
# Only redraw if there's a single visible window
if tm.active_tab.windows.num_visble_groups == 1:
tm.active_tab.relayout_borders()
def on_activity_since_last_focus(self, window: Window) -> None:
os_window_id = window.os_window_id

View File

@@ -7,7 +7,7 @@ from itertools import repeat
from typing import Any, Callable, NamedTuple
from kitty.borders import BorderColor
from kitty.fast_data_types import Region, set_active_window, viewport_for_window
from kitty.fast_data_types import Region, get_options, 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
@@ -248,7 +248,7 @@ class Layout:
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()
self._set_dimensions(all_windows)
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:
@@ -335,7 +335,7 @@ class Layout:
if bias is not None:
idx = all_windows.group_idx_for_window(window)
if idx is not None:
self._set_dimensions()
self._set_dimensions(all_windows)
self._bias_slot(all_windows, idx, bias)
def _bias_slot(self, all_windows: WindowList, idx: int, bias: float) -> bool:
@@ -354,11 +354,23 @@ class Layout:
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:
def _set_dimensions(self, all_windows: WindowList | None = None) -> None:
lgd.central, tab_bar, vw, vh, lgd.cell_width, lgd.cell_height = viewport_for_window(self.os_window_id)
# Update lgd.draw_minimal_borders based on the current number of visible windows
# and the draw_window_borders_for_single_window option
opts = get_options()
base_draw_minimal = opts.draw_minimal_borders and sum(opts.window_margin_width) == 0
if all_windows is not None and opts.draw_window_borders_for_single_window:
num_visible = all_windows.num_groups
if num_visible == 1:
lgd.draw_minimal_borders = False
else:
lgd.draw_minimal_borders = base_draw_minimal
else:
lgd.draw_minimal_borders = base_draw_minimal
def __call__(self, all_windows: WindowList) -> None:
self._set_dimensions()
self._set_dimensions(all_windows)
self.update_visibility(all_windows)
self.blank_rects = []
self.do_layout(all_windows)
@@ -432,7 +444,7 @@ class Layout:
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()
self._set_dimensions(windows)
yield from self.minimal_borders(windows)
def minimal_borders(self, windows: WindowList) -> Generator[BorderLine, None, None]:

View File

@@ -1224,6 +1224,19 @@ drawn.
'''
)
opt('draw_window_borders_for_single_window', 'no',
option_type='to_bool',
long_text='''
Draw borders around a window even when there is only a single window visible. When
enabled and there is only a single window, full borders are drawn around it (as if
:opt:`draw_minimal_borders` is false). The border will show in the active color when
the window is focused and the OS window has focus, and in the inactive color when the
OS window loses focus. This provides a clear visual indicator of whether the kitty
window is focused. When there are multiple windows visible, this option has no effect
and normal border drawing rules apply.
'''
)
opt('window_margin_width', '0',
option_type='edge_width',
long_text='''

View File

@@ -971,6 +971,9 @@ class Parser:
def draw_minimal_borders(self, val: str, ans: dict[str, typing.Any]) -> None:
ans['draw_minimal_borders'] = to_bool(val)
def draw_window_borders_for_single_window(self, val: str, ans: dict[str, typing.Any]) -> None:
ans['draw_window_borders_for_single_window'] = to_bool(val)
def dynamic_background_opacity(self, val: str, ans: dict[str, typing.Any]) -> None:
ans['dynamic_background_opacity'] = to_bool(val)

View File

@@ -346,6 +346,7 @@ option_names = (
'dim_opacity',
'disable_ligatures',
'draw_minimal_borders',
'draw_window_borders_for_single_window',
'dynamic_background_opacity',
'editor',
'enable_audio_bell',
@@ -544,6 +545,7 @@ class Options:
dim_opacity: float = 0.4
disable_ligatures: int = 0
draw_minimal_borders: bool = True
draw_window_borders_for_single_window: bool = False
dynamic_background_opacity: bool = False
editor: str = '.'
enable_audio_bell: bool = True

View File

@@ -418,10 +418,15 @@ class Tab: # {{{
tm = self.tab_manager_ref()
if tm is not None:
ly = self.current_layout
opts = get_options()
draw_borders = (
(ly.needs_window_borders and self.windows.num_visble_groups > 1) or ly.must_draw_borders
or opts.draw_window_borders_for_single_window
)
self.borders(
all_windows=self.windows,
current_layout=ly, tab_bar_rects=tm.tab_bar_rects,
draw_window_borders=(ly.needs_window_borders and self.windows.num_visble_groups > 1) or ly.must_draw_borders
draw_window_borders=draw_borders
)
def create_layout_object(self, name: str) -> Layout:

View File

@@ -88,6 +88,10 @@ def utils(self, q, windows):
class TestLayout(BaseTest):
def setUp(self):
super().setUp()
self.set_options()
def do_ops_test(self, q):
windows = create_windows(q)
ids, visible_ids, expect_ids, check_visible = utils(self, q, windows)