Compare commits

...

3 Commits
master ... f2

Author SHA1 Message Date
Kovid Goyal
715645e69c More work on floats 2025-05-11 22:06:12 +05:30
Kovid Goyal
4e697abb34 more work on floats 2025-05-11 22:06:12 +05:30
Kovid Goyal
96b0c463e8 Start work on floating windows 2025-05-11 22:06:11 +05:30
15 changed files with 247 additions and 109 deletions

View File

@@ -91,7 +91,7 @@ class Borders:
for tbr in tab_bar_rects:
add_borders_rect(self.os_window_id, self.tab_id, *tbr)
bw = 0
groups = tuple(all_windows.iter_all_layoutable_groups(only_visible=True))
groups = tuple(all_windows.iter_all_layoutable_groups())
if groups:
bw = groups[0].effective_border()
draw_borders = bw > 0 and draw_window_borders

View File

@@ -526,7 +526,7 @@ def monitor_pid(pid: int) -> None:
pass
def add_window(os_window_id: int, tab_id: int, title: str) -> int:
def add_window(os_window_id: int, tab_id: int, title: str, is_floating: bool) -> int:
pass

View File

@@ -16,7 +16,7 @@ from .clipboard import set_clipboard_string, set_primary_selection
from .fast_data_types import add_timer, get_boss, get_options, get_os_window_title, patch_color_profiles
from .options.utils import env as parse_env
from .tabs import Tab, TabManager
from .types import LayerShellConfig, OverlayType, run_once
from .types import FloatType, LayerShellConfig, OverlayType, run_once
from .utils import get_editor, log_error, resolve_custom_file, which
from .window import CwdRequest, CwdRequestType, Watchers, Window
@@ -81,7 +81,7 @@ of the active window in the tab is used as the tab title. The special value
--type
type=choices
default=window
choices=window,tab,os-window,os-panel,overlay,overlay-main,background,clipboard,primary
choices=window,tab,os-window,os-panel,overlay,overlay-main,float-in-window,float-in-tab,background,clipboard,primary
Where to launch the child process:
:code:`window`
@@ -105,6 +105,14 @@ Where to launch the child process:
directory, the input text for kittens, launch commands, etc. Useful if this overlay is
intended to run for a long time as a primary window.
:code:`float-in-window`
A floating window that is drawn over a non-floating window, usually the currently
active window.
:code:`float-in-tab`
A floating window that is drawn over all windows in a tab, usually the currently
active tab.
:code:`background`
The process will be run in the :italic:`background`, without a kitty
window. Note that if :option:`kitten @ launch --allow-remote-control` is
@@ -753,8 +761,9 @@ def _launch(
tab = tab_for_window(boss, opts, target_tab, next_to)
watchers = load_watch_modules(opts.watcher)
with Window.set_ignore_focus_changes_for_new_windows(opts.keep_focus):
float_type = {'float-in-window': FloatType.window, 'float-in-tab': FloatType.tab}.get(opts.type, FloatType.none)
new_window: Window = tab.new_window(
env=env or None, watchers=watchers or None, is_clone_launch=is_clone_launch, next_to=next_to, **kw)
env=env or None, watchers=watchers or None, is_clone_launch=is_clone_launch, next_to=next_to, float_type=float_type, **kw)
if child_death_callback is not None:
boss.monitor_pid(new_window.child.pid or 0, child_death_callback)
if spacing:

View File

@@ -1,7 +1,7 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from collections.abc import Generator, Iterable, Iterator, Sequence
from collections.abc import Generator, Iterable, Sequence
from functools import partial
from itertools import repeat
from typing import Any, NamedTuple
@@ -9,7 +9,7 @@ from typing import Any, NamedTuple
from kitty.borders import BorderColor
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
from kitty.types import Edges, FloatType, WindowGeometry
from kitty.typing_compat import TypedDict, WindowType
from kitty.window_list import WindowGroup, WindowList
@@ -241,9 +241,9 @@ class Layout:
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)
return self.calculate_bias_increment_for_a_single_cell(is_horizontal)
def calculate_bias_increment_for_a_single_cell(self, all_windows: WindowList, is_horizontal: bool) -> float:
def calculate_bias_increment_for_a_single_cell(self, is_horizontal: bool) -> float:
if is_horizontal:
return (lgd.cell_width + 1) / lgd.central.width
return (lgd.cell_height + 1) / lgd.central.height
@@ -284,7 +284,7 @@ class Layout:
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:
if all_windows.num_of_non_floating_groups < 2 or not delta:
return False
return all_windows.move_window_group(by=delta)
@@ -332,7 +332,7 @@ class Layout:
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)
h, v = self.calculate_bias_increment_for_a_single_cell(True), self.calculate_bias_increment_for_a_single_cell(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)
@@ -341,10 +341,22 @@ class Layout:
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)
active_non_floating_window = all_windows.active_non_floating_window
floating_windows = []
vismap = {}
for window, is_group_leader, is_floating in all_windows.iter_windows_with_visibility():
if is_floating:
floating_windows.append((window, is_group_leader))
else:
is_visible = window is active_non_floating_window or (is_group_leader and not self.only_active_window_visible)
window.set_visible_in_layout(is_visible)
vismap[window.id] = is_visible
for window, is_group_leader in sorted(floating_windows, key=lambda x: x[0].id):
vis = is_group_leader
if vis and window.float_type is FloatType.window:
vis = vismap.get(window.floating_in_window, True) # if floating_in_window not found float in tab
window.set_visible_in_layout(vis)
vismap[window.id] = vis
def _set_dimensions(self) -> None:
lgd.central, tab_bar, vw, vh, lgd.cell_width, lgd.cell_height = viewport_for_window(self.os_window_id)
@@ -354,6 +366,7 @@ class Layout:
self.update_visibility(all_windows)
self.blank_rects = []
self.do_layout(all_windows)
self.layout_floats(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
@@ -372,7 +385,7 @@ class Layout:
def xlayout(
self,
groups: Iterator[WindowGroup],
groups: Iterable[WindowGroup],
bias: None | Sequence[float] | dict[int, float] = None,
start: int | None = None,
size: int | None = None,
@@ -391,7 +404,7 @@ class Layout:
def ylayout(
self,
groups: Iterator[WindowGroup],
groups: Iterable[WindowGroup],
bias: None | Sequence[float] | dict[int, float] = None,
start: int | None = None,
size: int | None = None,
@@ -417,6 +430,11 @@ class Layout:
def do_layout(self, windows: WindowList) -> None:
raise NotImplementedError()
def layout_floats(self, windows: WindowList) -> None:
for gr in windows.iter_all_floating_groups():
# TODO: actually position the float based on its properties
self.layout_single_window_group(gr, False)
def neighbors_for_window(self, window: WindowType, windows: WindowList) -> NeighborsMap:
return {'left': [], 'right': [], 'top': [], 'bottom': []}

View File

@@ -75,7 +75,7 @@ class Grid(Layout):
return 0, 0
def bias_slot(self, all_windows: WindowList, idx: int, fractional_bias: float, cell_increment_bias_h: float, cell_increment_bias_v: float) -> bool:
num_windows = all_windows.num_groups
num_windows = all_windows.num_layoutable_groups
ncols, nrows, special_rows, special_col = calc_grid_size(num_windows)
row_num, col_num = self.position_for_window_idx(idx, num_windows, ncols, nrows, special_rows, special_col)
if row_num == 0:
@@ -93,7 +93,7 @@ class Grid(Layout):
return tuple(self.variable_layout(layout_func, num_windows, b)) == before_layout
def apply_bias(self, idx: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool:
num_windows = all_windows.num_groups
num_windows = all_windows.num_layoutable_groups
ncols, nrows, special_rows, special_col = calc_grid_size(num_windows)
row_num, col_num = self.position_for_window_idx(idx, num_windows, ncols, nrows, special_rows, special_col)
@@ -151,7 +151,7 @@ class Grid(Layout):
on_col_done(col_windows)
def do_layout(self, all_windows: WindowList) -> None:
n = all_windows.num_groups
n = all_windows.num_layoutable_groups
if n == 1:
self.layout_single_window_group(next(all_windows.iter_all_layoutable_groups()))
return
@@ -193,7 +193,7 @@ class Grid(Layout):
position_window_in_grid_cell(window_idx, xl, yl)
def minimal_borders(self, all_windows: WindowList) -> Generator[BorderLine, None, None]:
n = all_windows.num_groups
n = all_windows.num_layoutable_groups
if not lgd.draw_minimal_borders or n < 2:
return
needs_borders_map = all_windows.compute_needs_borders_map(lgd.draw_active_borders)
@@ -260,7 +260,7 @@ class Grid(Layout):
yield BorderLine(edges, color)
def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> NeighborsMap:
n = all_windows.num_groups
n = all_windows.num_layoutable_groups
if n < 4:
return neighbors_for_tall_window(1, window, all_windows)

View File

@@ -118,9 +118,10 @@ class Tall(Layout):
return True
def variable_layout(self, all_windows: WindowList, biased_map: dict[int, float]) -> LayoutDimension:
num = all_windows.num_groups - self.num_full_size_windows
windows = tuple(all_windows.iter_all_layoutable_groups())
num = len(windows) - self.num_full_size_windows
bias = biased_map if num > 1 else None
return self.perp_axis_layout(all_windows.iter_all_layoutable_groups(), bias=bias, offset=self.num_full_size_windows)
return self.perp_axis_layout(windows, bias=bias, offset=self.num_full_size_windows)
def bias_slot(self, all_windows: WindowList, idx: int, fractional_bias: float, cell_increment_bias_h: float, cell_increment_bias_v: float) -> bool:
if idx < len(self.main_bias):
@@ -134,7 +135,7 @@ class Tall(Layout):
return before_layout == after_layout
def apply_bias(self, idx: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool:
num_windows = all_windows.num_groups
num_windows = all_windows.num_layoutable_groups
if self.main_is_horizontal == is_horizontal:
before_main_bias = self.main_bias
ncols = self.num_full_size_windows + 1
@@ -159,10 +160,10 @@ class Tall(Layout):
return before != after
def simple_layout(self, all_windows: WindowList) -> Generator[tuple[WindowGroup, LayoutData, LayoutData, bool], None, None]:
num = all_windows.num_groups
is_fat = not self.main_is_horizontal
mirrored = self.layout_opts.mirrored
groups = tuple(all_windows.iter_all_layoutable_groups())
num = len(groups)
main_bias = self.main_bias[::-1] if mirrored else self.main_bias
if mirrored:
groups = tuple(reversed(groups))
@@ -218,9 +219,10 @@ class Tall(Layout):
yield wg, xl, yl, False
def do_layout(self, all_windows: WindowList) -> None:
num = all_windows.num_groups
windows = tuple(all_windows.iter_all_layoutable_groups())
num = len(windows)
if num == 1:
self.layout_single_window_group(next(all_windows.iter_all_layoutable_groups()))
self.layout_single_window_group(windows[0])
return
layouts = (self.simple_layout if num <= self.num_full_size_windows + 1 else self.full_layout)(all_windows)
for wg, xl, yl, is_full_size in layouts:
@@ -270,7 +272,7 @@ class Tall(Layout):
return None
def minimal_borders(self, all_windows: WindowList) -> Generator[BorderLine, None, None]:
num = all_windows.num_groups
num = all_windows.num_layoutable_groups
if num < 2 or not lgd.draw_minimal_borders:
return
try:

View File

@@ -68,9 +68,10 @@ class Vertical(Layout):
perp_axis_layout = Layout.xlayout
def variable_layout(self, all_windows: WindowList, biased_map: dict[int, float]) -> LayoutDimension:
num_windows = all_windows.num_groups
windows = tuple(all_windows.iter_all_layoutable_groups())
num_windows = len(windows)
bias = biased_map if num_windows > 1 else None
return self.main_axis_layout(all_windows.iter_all_layoutable_groups(), bias=bias)
return self.main_axis_layout(windows, bias=bias)
def fixed_layout(self, wg: WindowGroup) -> LayoutDimension:
return self.perp_axis_layout(iter((wg,)), border_mult=0 if lgd.draw_minimal_borders else 1)
@@ -82,7 +83,7 @@ class Vertical(Layout):
def apply_bias(self, idx: int, increment: float, all_windows: WindowList, is_horizontal: bool = True) -> bool:
if self.main_is_horizontal != is_horizontal:
return False
num_windows = all_windows.num_groups
num_windows = all_windows.num_layoutable_groups
if num_windows < 2:
return False
before_layout = list(self.variable_layout(all_windows, self.biased_map))
@@ -109,7 +110,7 @@ class Vertical(Layout):
yield wg, xl, yl
def do_layout(self, all_windows: WindowList) -> None:
window_count = all_windows.num_groups
window_count = all_windows.num_layoutable_groups
if window_count == 1:
self.layout_single_window_group(next(all_windows.iter_all_layoutable_groups()))
return
@@ -117,7 +118,7 @@ class Vertical(Layout):
self.set_window_group_geometry(wg, xl, yl)
def minimal_borders(self, all_windows: WindowList) -> Generator[BorderLine, None, None]:
window_count = all_windows.num_groups
window_count = all_windows.num_layoutable_groups
if window_count < 2 or not lgd.draw_minimal_borders:
return
yield from borders(self.generate_layout_data(all_windows), self.main_is_horizontal, all_windows)

View File

@@ -1134,7 +1134,7 @@ draw_cells(ssize_t vao_idx, const WindowRenderData *srd, OSWindow *os_window, bo
}
bool use_premult = false;
has_underlying_image |= grd.num_of_below_refs > 0 || grd.num_of_negative_refs > 0;
if (os_window->is_semi_transparent) {
if (os_window->is_semi_transparent || srd->float_data.is_floating) {
if (has_underlying_image) { draw_cells_interleaved_premult(vao_idx, screen, os_window, &crd, grd, wl); use_premult = true; }
else draw_cells_simple(vao_idx, screen, &crd, grd, os_window->is_semi_transparent);
} else {

View File

@@ -7,6 +7,7 @@
#include "cleanup.h"
#include "options/to-c-generated.h"
#include "iqsort.h"
#include <math.h>
#include <sys/mman.h>
@@ -288,10 +289,11 @@ set_window_logo(Window *w, const char *path, const ImageAnchorPosition pos, floa
}
static void
initialize_window(Window *w, PyObject *title, bool init_gpu_resources) {
initialize_window(Window *w, PyObject *title, bool init_gpu_resources, bool is_floating) {
w->id = ++global_state.window_id_counter;
w->visible = true;
w->title = title;
w->render_data.float_data.is_floating = is_floating;
Py_XINCREF(title);
if (!set_window_logo(w, OPT(default_window_logo), OPT(window_logo_position), OPT(window_logo_alpha), true, NULL, 0)) {
log_error("Failed to load default window logo: %s", OPT(default_window_logo));
@@ -303,14 +305,23 @@ initialize_window(Window *w, PyObject *title, bool init_gpu_resources) {
}
}
static void
sort_windows_in_render_order(Window *windows, size_t count) {
#define lt(a, b) (!a->render_data.float_data.is_floating && b->render_data.float_data.is_floating) || (a->render_data.float_data.is_floating == b->render_data.float_data.is_floating && a->id < b->id)
QSORT(Window, windows, count, lt)
#undef lt
}
static id_type
add_window(id_type os_window_id, id_type tab_id, PyObject *title) {
add_window(id_type os_window_id, id_type tab_id, PyObject *title, bool is_floating) {
WITH_TAB(os_window_id, tab_id);
ensure_space_for(tab, windows, Window, tab->num_windows + 1, capacity, 1, true);
make_os_window_context_current(osw);
zero_at_i(tab->windows, tab->num_windows);
initialize_window(tab->windows + tab->num_windows, title, true);
return tab->windows[tab->num_windows++].id;
initialize_window(tab->windows + tab->num_windows, title, true, is_floating);
Window *ans = tab->windows + tab->num_windows++;
sort_windows_in_render_order(tab->windows, tab->num_windows);
return ans->id;
END_WITH_TAB;
return 0;
}
@@ -1290,13 +1301,14 @@ static PyObject*
pycreate_mock_window(PyObject *self UNUSED, PyObject *args) {
Screen *screen;
PyObject *title = NULL;
if (!PyArg_ParseTuple(args, "O|U", &screen, &title)) return NULL;
int is_floating = 0;
if (!PyArg_ParseTuple(args, "O|Up", &screen, &title, &is_floating)) return NULL;
Window *w = PyMem_Calloc(sizeof(Window), 1);
if (!w) return NULL;
Py_INCREF(screen);
PyObject *ans = PyCapsule_New(w, "Window", destroy_mock_window);
if (ans != NULL) {
initialize_window(w, title, false);
initialize_window(w, title, false, is_floating);
w->render_data.screen = screen;
}
return ans;
@@ -1404,7 +1416,11 @@ THREE_ID(remove_window)
THREE_ID(detach_window)
THREE_ID(attach_window)
PYWRAP1(add_tab) { return PyLong_FromUnsignedLongLong(add_tab(PyLong_AsUnsignedLongLong(args))); }
PYWRAP1(add_window) { PyObject *title; id_type a, b; PA("KKO", &a, &b, &title); return PyLong_FromUnsignedLongLong(add_window(a, b, title)); }
PYWRAP1(add_window) {
PyObject *title; id_type a, b; int is_floating;
PA("KKOp", &a, &b, &title, &is_floating);
return PyLong_FromUnsignedLongLong(add_window(a, b, title, is_floating));
}
PYWRAP0(current_os_window) { OSWindow *w = current_os_window(); if (!w) Py_RETURN_NONE; return PyLong_FromUnsignedLongLong(w->id); }
TWO_ID(remove_tab)
KI(set_active_tab)

View File

@@ -141,10 +141,15 @@ typedef struct WindowLogoRenderData {
bool using_default;
} WindowLogoRenderData;
typedef struct FloatRenderData {
bool is_floating;
} FloatRenderData;
typedef struct {
ssize_t vao_idx;
float xstart, ystart, dx, dy;
Screen *screen;
FloatRenderData float_data;
} WindowRenderData;
typedef struct {

View File

@@ -52,7 +52,7 @@ from .layout.base import Layout
from .layout.interface import create_layout_object_for, evict_cached_layouts
from .progress import ProgressState
from .tab_bar import TabBar, TabBarData
from .types import ac
from .types import FloatType, ac
from .typing_compat import EdgeLiteral, SessionTab, SessionType, TypedDict
from .utils import cmdline_for_hold, log_error, platform_window_id, resolved_shell, shlex_split, which
from .window import CwdRequest, Watchers, Window, WindowDict
@@ -192,7 +192,7 @@ class Tab: # {{{
def has_single_window_visible(self) -> bool:
if self.current_layout.only_active_window_visible:
return True
for i, g in enumerate(self.windows.iter_all_layoutable_groups(only_visible=True)):
for i, g in enumerate(self.windows.iter_all_layoutable_groups()):
if i > 0:
return False
return True
@@ -568,15 +568,21 @@ class Tab: # {{{
pass_fds: tuple[int, ...] = (),
remote_control_fd: int = -1,
next_to: Window | None = None,
float_type: FloatType = FloatType.none,
) -> Window:
child = self.launch_child(
use_shell=use_shell, cmd=cmd, stdin=stdin, cwd_from=cwd_from, cwd=cwd, env=env,
is_clone_launch=is_clone_launch, add_listen_on_env_var=False if allow_remote_control and remote_control_passwords else True,
hold=hold, pass_fds=pass_fds, remote_control_fd=remote_control_fd,
)
floating_in = 0
if float_type is FloatType.window:
w = next_to or self.active_window
if w:
floating_in = w.id
window = Window(
self, child, self.args, override_title=override_title,
copy_colors_from=copy_colors_from, watchers=watchers,
self, child, self.args, override_title=override_title, floating_in_window=floating_in,
copy_colors_from=copy_colors_from, watchers=watchers, float_type=float_type,
allow_remote_control=allow_remote_control, remote_control_passwords=remote_control_passwords
)
# Must add child before laying out so that resize_pty succeeds
@@ -673,8 +679,8 @@ class Tab: # {{{
if self.windows:
if num < 0:
self.windows.make_previous_group_active(-num)
elif self.windows.num_groups:
self.current_layout.activate_nth_window(self.windows, min(num, self.windows.num_groups - 1))
elif self.windows.num_of_floating_groups_and_non_floating_groups:
self.current_layout.activate_nth_window(self.windows, min(num, self.windows.num_of_floating_groups_and_non_floating_groups - 1))
self.relayout_borders()
@ac('win', 'Focus the first window')
@@ -893,7 +899,7 @@ class Tab: # {{{
@property
def num_window_groups(self) -> int:
return self.windows.num_groups
return self.windows.num_of_non_floating_groups
def __contains__(self, window: Window) -> bool:
return window in self.windows

View File

@@ -27,6 +27,13 @@ class OverlayType(Enum):
main = 'main'
class FloatType(Enum):
none = 'none'
window = 'window'
tab = 'tab'
os_window = 'os_window'
class ParsedShortcut(NamedTuple):
mods: int
key_name: str

View File

@@ -91,7 +91,7 @@ from .keys import keyboard_mode_name, mod_mask
from .progress import Progress
from .rgb import to_color
from .terminfo import get_capabilities
from .types import MouseEvent, OverlayType, WindowGeometry, ac, run_once
from .types import FloatType, MouseEvent, OverlayType, WindowGeometry, ac, run_once
from .typing_compat import BossType, ChildType, EdgeLiteral, TabType, TypedDict
from .utils import (
color_as_int,
@@ -623,12 +623,16 @@ class Window:
watchers: Watchers | None = None,
allow_remote_control: bool = False,
remote_control_passwords: dict[str, Sequence[str]] | None = None,
float_type: FloatType = FloatType.none,
floating_in_window: int = 0,
):
if watchers:
self.watchers = watchers
self.watchers.add(global_watchers())
else:
self.watchers = global_watchers().copy()
self.float_type = float_type
self.floating_in_window = floating_in_window
self.keys_redirected_till_ready_from: int = 0
self.last_focused_at = 0.
self.is_focused: bool = False
@@ -659,7 +663,7 @@ class Window:
self.child_title = self.default_title
self.title_stack: Deque[str] = deque(maxlen=10)
self.user_vars: dict[str, str] = {}
self.id: int = add_window(tab.os_window_id, tab.id, self.title)
self.id: int = add_window(tab.os_window_id, tab.id, self.title, self.is_floating)
self.clipboard_request_manager = ClipboardRequestManager(self.id)
self.margin = EdgeWidths()
self.padding = EdgeWidths()
@@ -682,6 +686,10 @@ class Window:
self.remote_control_passwords = remote_control_passwords
self.allow_remote_control = allow_remote_control
@property
def is_floating(self) -> bool:
return self.float_type is not FloatType.none
def remote_control_allowed(self, pcmd: dict[str, Any], extra_data: dict[str, Any]) -> bool:
if not self.allow_remote_control:
return False

View File

@@ -9,7 +9,7 @@ from itertools import count
from typing import Any, Deque, Union
from .fast_data_types import Color, get_options
from .types import OverlayType, WindowGeometry
from .types import FloatType, OverlayType, WindowGeometry
from .typing_compat import EdgeLiteral, TabType, WindowType
WindowOrId = Union[WindowType, int]
@@ -72,6 +72,13 @@ class WindowGroup:
else:
self.windows.append(window)
@property
def is_floating(self) -> bool:
for w in self.windows:
if w.is_floating:
return True
return False
def move_window_to_top_of_group(self, window: WindowType) -> bool:
try:
idx = self.windows.index(window)
@@ -100,22 +107,19 @@ class WindowGroup:
}
def decoration(self, which: EdgeLiteral, border_mult: int = 1, is_single_window: bool = False) -> int:
if not self.windows:
return 0
w = self.windows[0]
return w.effective_margin(which) + w.effective_border() * border_mult + w.effective_padding(which)
for w in self.windows:
return w.effective_margin(which) + w.effective_border() * border_mult + w.effective_padding(which)
return 0
def effective_padding(self, which: EdgeLiteral) -> int:
if not self.windows:
return 0
w = self.windows[0]
return w.effective_padding(which)
for w in self.windows:
return w.effective_padding(which)
return 0
def effective_border(self) -> int:
if not self.windows:
return 0
w = self.windows[0]
return w.effective_border()
for w in self.windows:
return w.effective_border()
return 0
def set_geometry(self, geom: WindowGeometry) -> None:
for w in self.windows:
@@ -123,22 +127,19 @@ class WindowGroup:
@property
def default_bg(self) -> Color:
if self.windows:
w = self.windows[-1]
for w in reversed(self.windows):
return w.screen.color_profile.default_bg or get_options().background
return get_options().background
@property
def geometry(self) -> WindowGeometry | None:
if self.windows:
w = self.windows[-1]
for w in reversed(self.windows):
return w.geometry
return None
@property
def is_visible_in_layout(self) -> bool:
if self.windows:
w = self.windows[-1]
for w in reversed(self.windows):
return w.is_visible_in_layout
return False
@@ -202,7 +203,7 @@ class WindowList:
def set_active_group_idx(self, i: int, notify: bool = True) -> bool:
changed = False
if i != self._active_group_idx and 0 <= i < len(self.groups):
if i != self._active_group_idx and 0 <= i < self.num_of_floating_groups_and_non_floating_groups:
old_active_window = self.active_window
g = self.active_group
if g is not None:
@@ -226,14 +227,42 @@ class WindowList:
def change_tab(self, tab: TabType) -> None:
self.tabref = weakref.ref(tab)
def iter_windows_with_visibility(self) -> Iterator[tuple[WindowType, bool]]:
def iter_windows_with_visibility(self) -> Iterator[tuple[WindowType, bool, bool]]:
for g in self.groups:
aw = g.active_window_id
is_floating = g.is_floating
for window in g:
yield window, window.id == aw
yield window, window.id == aw, is_floating
def iter_all_layoutable_groups(self, only_visible: bool = False) -> Iterator[WindowGroup]:
return iter(g for g in self.groups if g.is_visible_in_layout) if only_visible else iter(self.groups)
def iter_all_layoutable_groups(self) -> Iterator[WindowGroup]:
for g in self.groups:
if g.is_visible_in_layout and not g.is_floating:
yield g
def iter_all_floating_groups(self) -> Iterator[WindowGroup]:
for g in self.groups:
if g.is_floating:
yield g
@property
def num_layoutable_groups(self) -> int:
ans = 0
for g in self.groups:
if g.is_visible_in_layout and not g.is_floating:
ans += 1
return ans
@property
def num_of_non_floating_groups(self) -> int:
ans = 0
for g in self.groups:
if not g.is_floating:
ans += 1
return ans
@property
def num_of_floating_groups_and_non_floating_groups(self) -> int:
return len(self.groups)
def iter_windows_with_number(self, only_visible: bool = True) -> Iterator[tuple[int, WindowType]]:
for i, g in enumerate(self.groups):
@@ -259,10 +288,6 @@ class WindowList:
return
self.set_active_group_idx(len(self.groups) - 1, notify=notify)
@property
def num_groups(self) -> int:
return len(self.groups)
def group_for_window(self, x: WindowOrId) -> WindowGroup | None:
q = self.id_map[x] if isinstance(x, int) else x
for g in self.groups:
@@ -308,6 +333,31 @@ class WindowList:
return self.id_map[self.groups[self.active_group_idx].active_window_id]
return None
@property
def active_non_floating_window(self) -> WindowType | None:
w = self.active_window
if w is None:
return None
if not w.is_floating:
return w
if w.float_type is FloatType.window:
parent = self.id_map.get(w.floating_in_window)
if parent is not None:
g = self.group_for_window(parent)
if g is not None:
ans = self.id_map.get(g.active_window_id)
if ans is not None:
return ans
# tab or os window float or parent window closed
gid_map = {g.id: g for g in self.groups}
for gid in reversed(self.active_group_history):
g = gid_map.get(gid)
if g is not None:
w = self.id_map.get(g.active_window_id)
if w is not None and not w.is_floating:
return w
return None
@property
def active_group_main(self) -> WindowType | None:
with suppress(Exception):
@@ -343,22 +393,28 @@ class WindowList:
if group_of is not None:
target_group = self.group_for_window(group_of)
if target_group is None and next_to is not None:
q = self.id_map[next_to] if isinstance(next_to, int) else next_to
pos = -1
for i, g in enumerate(self.groups):
if q in g:
pos = i
break
if pos > -1:
if window.is_floating:
if target_group is None:
target_group = WindowGroup()
self.groups.insert(pos + (0 if before else 1), target_group)
if target_group is None:
target_group = WindowGroup()
if before:
self.groups.insert(0, target_group)
else:
self.groups.append(target_group)
else:
if target_group is None and next_to is not None:
q = self.id_map[next_to] if isinstance(next_to, int) else next_to
pos = -1
for i, g in enumerate(self.groups):
if q in g:
pos = i
break
if pos > -1:
target_group = WindowGroup()
self.groups.insert(pos + (0 if before else 1), target_group)
if target_group is None:
target_group = WindowGroup()
if before:
self.groups.insert(0, target_group)
else:
self.groups.append(target_group)
self.move_floating_groups_to_end()
old_active_window = self.active_window
target_group.add_window(window, head_of_group=head_of_group)
@@ -372,6 +428,14 @@ class WindowList:
self.notify_on_active_window_change(old_active_window, new_active_window)
return target_group
def move_floating_groups_to_end(self) -> None:
fg: list[WindowGroup] = []
ng: list[WindowGroup] = []
for g in self.groups:
(fg if g.is_floating else ng).append(g)
ng.extend(fg)
self.groups = ng
def remove_window(self, x: WindowOrId) -> None:
old_active_window = self.active_window
q = self.id_map[x] if isinstance(x, int) else x
@@ -398,8 +462,8 @@ class WindowList:
def active_window_in_nth_group(self, n: int, clamp: bool = False) -> WindowType | None:
if clamp:
n = max(0, min(n, self.num_groups - 1))
if 0 <= n < self.num_groups:
n = max(0, min(n, len(self.groups) - 1))
if 0 <= n < len(self.groups):
return self.id_map.get(self.groups[n].active_window_id)
return None
@@ -410,14 +474,14 @@ class WindowList:
return None
def activate_next_window_group(self, delta: int) -> None:
self.set_active_group_idx(wrap_increment(self.active_group_idx, self.num_groups, delta))
self.set_active_group_idx(wrap_increment(self.active_group_idx, self.num_of_floating_groups_and_non_floating_groups, delta))
def move_window_group(self, by: int | None = None, to_group: int | None = None) -> bool:
if self.active_group_idx < 0 or not self.groups:
return False
target = -1
if by is not None:
target = wrap_increment(self.active_group_idx, self.num_groups, by)
target = wrap_increment(self.active_group_idx, len(self.groups), by)
if to_group is not None:
for i, group in enumerate(self.groups):
if group.id == to_group:
@@ -427,13 +491,14 @@ class WindowList:
if target == self.active_group_idx:
return False
self.groups[self.active_group_idx], self.groups[target] = self.groups[target], self.groups[self.active_group_idx]
self.move_floating_groups_to_end()
self.set_active_group_idx(target)
return True
return False
def compute_needs_borders_map(self, draw_active_borders: bool) -> dict[int, bool]:
ag = self.active_group
return {gr.id: ((gr is ag and draw_active_borders) or gr.needs_attention) for gr in self.groups}
return {gr.id: ((gr is ag and draw_active_borders) or gr.needs_attention) for gr in self.groups if not gr.is_floating}
@property
def num_visble_groups(self) -> int:

View File

@@ -14,6 +14,7 @@ class Window:
def __init__(self, win_id, overlay_for=None, overlay_window_id=None):
self.id = win_id
self.is_floating = False
self.overlay_for = overlay_for
self.overlay_window_id = overlay_window_id
self.is_visible_in_layout = True
@@ -98,15 +99,15 @@ class TestLayout(BaseTest):
check_visible()
# Test nth_window
for i in range(windows.num_groups):
for i in range(windows.num_of_floating_groups_and_non_floating_groups):
q.activate_nth_window(windows, i)
self.ae(windows.active_group_idx, i)
expect_ids(*range(1, len(windows)+1))
check_visible()
# Test next_window
for i in range(2 * windows.num_groups):
expected = (windows.active_group_idx + 1) % windows.num_groups
for i in range(2 * windows.num_of_floating_groups_and_non_floating_groups):
expected = (windows.active_group_idx + 1) % windows.num_of_floating_groups_and_non_floating_groups
q.next_window(windows)
self.ae(windows.active_group_idx, expected)
expect_ids(*range(1, len(windows)+1))
@@ -127,9 +128,9 @@ class TestLayout(BaseTest):
# Test add_window
windows.set_active_group_idx(4)
q.add_window(windows, Window(6))
self.ae(windows.num_groups, 6)
self.ae(windows.num_of_floating_groups_and_non_floating_groups, 6)
self.ae(windows.active_group_idx, 5)
expect_ids(*range(1, windows.num_groups+1))
expect_ids(*range(1, windows.num_of_floating_groups_and_non_floating_groups+1))
check_visible()
# Test remove_window
@@ -150,7 +151,7 @@ class TestLayout(BaseTest):
expect_ids(2, 3, 5, 6)
# Test set_active_window
for i in range(windows.num_groups):
for i in range(windows.num_of_floating_groups_and_non_floating_groups):
windows.set_active_group_idx(i)
self.ae(i, windows.active_group_idx)
check_visible()
@@ -177,10 +178,10 @@ class TestLayout(BaseTest):
w = Window(len(windows) + 1)
windows.add_window(w)
expect_ids(1, 2, 3, 4, 5, 6)
self.ae(windows.active_group_idx, windows.num_groups - 1)
self.ae(windows.active_group_idx, windows.num_of_floating_groups_and_non_floating_groups - 1)
# Test nth_window
for i in range(windows.num_groups):
for i in range(windows.num_of_floating_groups_and_non_floating_groups):
q.activate_nth_window(windows, i)
self.ae(windows.active_group_idx, i)
if i == overlaid_group:
@@ -189,8 +190,8 @@ class TestLayout(BaseTest):
check_visible()
# Test next_window
for i in range(windows.num_groups):
expected = (windows.active_group_idx + 1) % windows.num_groups
for i in range(windows.num_of_floating_groups_and_non_floating_groups):
expected = (windows.active_group_idx + 1) % windows.num_of_floating_groups_and_non_floating_groups
q.next_window(windows)
self.ae(windows.active_group_idx, expected)
expect_ids(1, 2, 3, 4, 5, 6)
@@ -210,7 +211,7 @@ class TestLayout(BaseTest):
check_visible()
# Test set_active_window
for i in range(windows.num_groups):
for i in range(windows.num_of_floating_groups_and_non_floating_groups):
windows.set_active_group_idx(i)
self.ae(i, windows.active_group_idx)
if i == overlaid_group: