diff --git a/kitty/fast_data_types.pyi b/kitty/fast_data_types.pyi index 4741f2207..1123cf91b 100644 --- a/kitty/fast_data_types.pyi +++ b/kitty/fast_data_types.pyi @@ -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 diff --git a/kitty/launch.py b/kitty/launch.py index a60f73388..aacc099c6 100644 --- a/kitty/launch.py +++ b/kitty/launch.py @@ -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: diff --git a/kitty/layout/base.py b/kitty/layout/base.py index 3493e7f1c..df177f694 100644 --- a/kitty/layout/base.py +++ b/kitty/layout/base.py @@ -341,14 +341,14 @@ class Layout: return False def update_visibility(self, all_windows: WindowList) -> None: - active_window = all_windows.active_window + 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_window or (is_group_leader and not self.only_active_window_visible) + 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): diff --git a/kitty/shaders.c b/kitty/shaders.c index f6c58b567..4ab037b8f 100644 --- a/kitty/shaders.c +++ b/kitty/shaders.c @@ -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 { diff --git a/kitty/state.c b/kitty/state.c index ece1296f3..5832db606 100644 --- a/kitty/state.c +++ b/kitty/state.c @@ -7,6 +7,7 @@ #include "cleanup.h" #include "options/to-c-generated.h" +#include "iqsort.h" #include #include @@ -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) diff --git a/kitty/state.h b/kitty/state.h index 2f7cd25a1..449c39285 100644 --- a/kitty/state.h +++ b/kitty/state.h @@ -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 { diff --git a/kitty/tabs.py b/kitty/tabs.py index 2cc95d797..767f605a2 100644 --- a/kitty/tabs.py +++ b/kitty/tabs.py @@ -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 @@ -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 diff --git a/kitty/window.py b/kitty/window.py index a4e7b0ff5..f3adad808 100644 --- a/kitty/window.py +++ b/kitty/window.py @@ -663,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() diff --git a/kitty/window_list.py b/kitty/window_list.py index c92793898..b7328e376 100644 --- a/kitty/window_list.py +++ b/kitty/window_list.py @@ -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] @@ -333,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):