Track window ids on border rects

More robust. Splits layout still needs work.
This commit is contained in:
Kovid Goyal 2026-03-02 08:31:36 +05:30
parent 9c0d7b83b1
commit 6b54c201e5
No known key found for this signature in database
GPG key ID: 06BC317B515ACE7C
11 changed files with 184 additions and 120 deletions

View file

@ -27,14 +27,14 @@ class Border(NamedTuple):
border_type: int = 0
def vertical_edge(rects: list[Border], color: BorderColor, width: int, top: int, bottom: int, left: int, is_left: bool) -> None:
def vertical_edge(rects: list[Border], color: BorderColor, width: int, top: int, bottom: int, left: int, border_type: int) -> None:
if width > 0:
rects.append(Border(left, top, left + width, bottom, color, -1 if is_left else 1))
rects.append(Border(left, top, left + width, bottom, color, border_type))
def horizontal_edge(rects: list[Border], color: BorderColor, height: int, left: int, right: int, top: int, is_top: bool) -> None:
def horizontal_edge(rects: list[Border], color: BorderColor, height: int, left: int, right: int, top: int, border_type: int) -> None:
if height > 0:
rects.append(Border(left, top, right, top + height, color, -1 if is_top else 1))
rects.append(Border(left, top, right, top + height, color, border_type))
def add_borders(rects: list[Border], color: BorderColor, wg: WindowGroup) -> None:
@ -59,10 +59,11 @@ def add_borders(rects: list[Border], color: BorderColor, wg: WindowGroup) -> Non
right += width
bottom += width
pl = pr = pb = pt = width
h(pt, left, right, top, True)
h(pb, left, right, bt, False)
v(pl, top, bottom, left, True)
v(pr, top, bottom, lr, False)
wid = wg.active_window_id
h(pt, left, right, top, -wid)
h(pb, left, right, bt, wid)
v(pl, top, bottom, left, -wid)
v(pr, top, bottom, lr, wid)
def load_borders_program() -> None:
@ -125,5 +126,5 @@ def __call__(
if draw_minimal_borders:
for border_line in current_layout.get_minimal_borders(all_windows):
rects.append(Border(*border_line.edges, border_line.color, True))
rects.append(Border(*border_line.edges, border_line.color, border_line.window_id))
set_borders_rects(self.os_window_id, self.tab_id, rects)

View file

@ -17,6 +17,7 @@
class BorderLine(NamedTuple):
edges: Edges = Edges()
color: BorderColor = BorderColor.inactive
window_id: int = 0
class LayoutOpts:
@ -441,8 +442,7 @@ def get_minimal_borders(self, windows: WindowList) -> Generator[BorderLine, None
yield from self.minimal_borders(windows)
def minimal_borders(self, windows: WindowList) -> Generator[BorderLine, None, None]:
return
yield BorderLine() # type: ignore
yield from ()
def layout_action(self, action_name: str, args: Sequence[str], all_windows: WindowList) -> bool | None:
pass

View file

@ -251,13 +251,12 @@ def borders_for_window(gid: int) -> Generator[Edges, None, None]:
yield Edges(right - bw, top, right, bottom)
for wg in all_groups_in_order:
for edges in borders_for_window(wg.id):
yield BorderLine(edges)
for wg in all_groups_in_order:
color = BorderColor.inactive
if needs_borders_map.get(wg.id):
color = BorderColor.active if wg is active_group else BorderColor.bell
for edges in borders_for_window(wg.id):
yield BorderLine(edges, color)
wid = wg.active_window_id
for edges in borders_for_window(wg.id):
yield BorderLine(edges, color, wid)
def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> NeighborsMap:
n = all_windows.num_groups

View file

@ -1,11 +1,11 @@
#!/usr/bin/env python
# License: GPLv3 Copyright: 2020, Kovid Goyal <kovid at kovidgoyal.net>
from collections.abc import Collection, Generator, Sequence
from typing import Any, NamedTuple, Optional, TypedDict, Union
from collections.abc import Collection, Generator, Iterator, Sequence
from typing import Any, Optional, TypedDict, Union
from kitty.borders import BorderColor
from kitty.fast_data_types import BOTTOM_EDGE, RIGHT_EDGE
from kitty.fast_data_types import BOTTOM_EDGE, LEFT_EDGE, RIGHT_EDGE, TOP_EDGE
from kitty.types import Edges, NeighborsMap, WindowGeometry, WindowMapper, WindowResizeDragData
from kitty.typing_compat import EdgeLiteral, WindowType
from kitty.window_list import WindowGroup, WindowList
@ -13,11 +13,6 @@
from .base import BorderLine, Layout, LayoutOpts, blank_rects_for_window, lgd, window_geometry_from_layouts
class Extent(NamedTuple):
start: int = 0
end: int = 0
class SerializedPair(TypedDict, total=False):
horizontal: bool # default to True if absent
bias: float # default to 0.5 if absent
@ -33,8 +28,9 @@ def __init__(self, horizontal: bool = True):
self.two: Pair | int | None = None
self.bias = 0.5
self.top = self.left = self.width = self.height = 0
self.between_borders: list[Edges] = []
self.first_extent = self.second_extent = Extent()
self.between_borders: tuple[Sequence[BorderLine], Sequence[BorderLine]] | None = None
self.first_extent = self.second_extent = Edges() # not including between_borders
self.border_width: int = 0
def serialize(self) -> SerializedPair:
ans: SerializedPair = {}
@ -242,12 +238,12 @@ def layout_pair(
id_window_map: dict[int, WindowGroup],
layout_object: Layout
) -> None:
self.between_borders = []
self.between_borders = None
self.left, self.top, self.width, self.height = left, top, width, height
bw = self.effective_border(id_window_map) if lgd.draw_minimal_borders else 0
self.first_extent = self.second_extent = Edges(left, top, left + width, top + height)
self.border_width = bw = self.effective_border(id_window_map) if lgd.draw_minimal_borders else 0
border_mult = 0 if lgd.draw_minimal_borders else 1
bw2 = bw * 2
self.first_extent = self.second_extent = Extent()
if self.one is None or self.two is None:
q = self.one or self.two
if isinstance(q, Pair):
@ -258,9 +254,11 @@ def layout_pair(
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=width, border_mult=border_mult))
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=height, border_mult=border_mult))
geom = window_geometry_from_layouts(xl, yl)
self.first_extent = Extent(left, left + width)
self.apply_window_geometry(q, geom, id_window_map, layout_object)
return
one: list[BorderLine] = []
two: list[BorderLine] = []
self.between_borders = one, two
if self.horizontal:
min_w1 = self.one.minimum_width(id_window_map) if isinstance(self.one, Pair) else lgd.cell_width
min_w2 = self.two.minimum_width(id_window_map) if isinstance(self.two, Pair) else lgd.cell_width
@ -269,26 +267,33 @@ def layout_pair(
if w2 < min_w2 and w1 >= min_w1 + bw2:
w2 = min_w2
w1 = width - w2
self.first_extent = Extent(max(0, left - bw), left + w1 + bw)
self.second_extent = Extent(left + w1 + bw, left + width + bw)
bleft = left + w1
self.first_extent = Edges(left, top, left + w1, top + height)
if isinstance(self.one, Pair):
self.one.layout_pair(left, top, w1, height, id_window_map, layout_object)
if bw:
for top, bottom, window_id in self.one.edge_border(RIGHT_EDGE, id_window_map):
one.append(BorderLine(Edges(bleft, top, bleft + bw, bottom), window_id=window_id))
else:
wg = id_window_map[self.one]
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=height, border_mult=border_mult))
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=w1, border_mult=border_mult))
geom = window_geometry_from_layouts(xl, yl)
self.apply_window_geometry(self.one, geom, id_window_map, layout_object)
self.between_borders = [
Edges(left + w1, top, left + w1 + bw, top + height),
Edges(left + w1 + bw, top, left + w1 + bw2, top + height),
]
left += bw2
if bw:
one.append(BorderLine(Edges(bleft, top, bleft + bw, top + height), window_id=wg.active_window_id))
left += w1 + bw2
self.second_extent = Edges(left, top, left + w2, top + height)
if isinstance(self.two, Pair):
self.two.layout_pair(left + w1, top, w2, height, id_window_map, layout_object)
self.two.layout_pair(left, top, w2, height, id_window_map, layout_object)
if bw:
for top, bottom, window_id in self.two.edge_border(LEFT_EDGE, id_window_map):
two.append(BorderLine(Edges(left - bw, top, left, bottom), window_id=window_id))
else:
wg = id_window_map[self.two]
xl = next(layout_object.xlayout(iter((wg,)), start=left + w1, size=w2, border_mult=border_mult))
if bw:
two.append(BorderLine(Edges(left - bw, top, left, top + height), window_id=-wg.active_window_id))
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=w2, border_mult=border_mult))
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=height, border_mult=border_mult))
geom = window_geometry_from_layouts(xl, yl)
self.apply_window_geometry(self.two, geom, id_window_map, layout_object)
@ -300,30 +305,118 @@ def layout_pair(
if h2 < min_h2 and h1 >= min_h1 + bw2:
h2 = min_h2
h1 = height - h2
self.first_extent = Extent(max(0, top - bw), top + h1 + bw)
self.second_extent = Extent(top + h1 + bw, top + height + bw)
btop = top + h1
self.first_extent = Edges(left, top, left + width, top + h1)
if isinstance(self.one, Pair):
self.one.layout_pair(left, top, width, h1, id_window_map, layout_object)
if bw:
for left, right, window_id in self.one.edge_border(BOTTOM_EDGE, id_window_map):
one.append(BorderLine(Edges(left, btop, right, btop + bw), window_id=window_id))
else:
wg = id_window_map[self.one]
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=width, border_mult=border_mult))
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=h1, border_mult=border_mult))
geom = window_geometry_from_layouts(xl, yl)
self.apply_window_geometry(self.one, geom, id_window_map, layout_object)
self.between_borders = [
Edges(left, top + h1, left + width, top + h1 + bw),
Edges(left, top + h1 + bw, left + width, top + h1 + bw2),
]
top += bw2
if bw:
one.append(BorderLine(Edges(left, btop, left + width, btop + bw), window_id=wg.active_window_id))
top += bw2 + h1
self.second_extent = Edges(left, top, left + width, top + h2)
if isinstance(self.two, Pair):
self.two.layout_pair(left, top + h1, width, h2, id_window_map, layout_object)
self.two.layout_pair(left, top, width, h2, id_window_map, layout_object)
if bw:
for left, right, window_id in self.two.edge_border(TOP_EDGE, id_window_map):
two.append(BorderLine(Edges(left, top - bw, right, top), window_id=window_id))
else:
wg = id_window_map[self.two]
if bw:
two.append(BorderLine(Edges(left, top - bw, left + width, top), window_id=-wg.active_window_id))
xl = next(layout_object.xlayout(iter((wg,)), start=left, size=width, border_mult=border_mult))
yl = next(layout_object.ylayout(iter((wg,)), start=top + h1, size=h2, border_mult=border_mult))
yl = next(layout_object.ylayout(iter((wg,)), start=top, size=h2, border_mult=border_mult))
geom = window_geometry_from_layouts(xl, yl)
self.apply_window_geometry(self.two, geom, id_window_map, layout_object)
def edge_border(self, which: int, id_group_map: dict[int, WindowGroup]) -> Iterator[tuple[int, int, int]]:
mult = 1 if which & (RIGHT_EDGE | BOTTOM_EDGE) else -1
def edge(x: int, p: Pair) -> tuple[int, int, int]:
wid = id_group_map[x].active_window_id * mult
if which & (LEFT_EDGE | RIGHT_EDGE):
return p.top, p.top + p.height, wid
return p.left, p.left + p.width, wid
def edges(x: int | Pair, parent: Pair) -> Iterator[tuple[int, int, int]]:
if isinstance(x, int):
yield edge(x, parent)
else:
yield from x.edge_border(which, id_group_map)
if self.two is None or self.one is None:
x = self.one or self.two
if x is not None:
yield from edges(x, self)
return
needs_vertical_edges = which in (LEFT_EDGE, RIGHT_EDGE)
if self.horizontal == needs_vertical_edges:
yield from edges(self.one if which in (LEFT_EDGE, TOP_EDGE) else self.two, self)
else:
g1, g2 = self.pair_geometry(id_group_map)
if g1 is not None and g2 is not None:
yield from edges(self.one, g1)
first_id = second_id = 0
if isinstance(self.one, int):
first_id = id_group_map[self.one].active_window_id
if isinstance(self.two, int):
second_id = id_group_map[self.two].active_window_id
if self.horizontal:
start = g1.left + g1.width
if isinstance(self.one, Pair):
first_id = id_group_map[self.one.corner_group_id(which | RIGHT_EDGE)].active_window_id
if isinstance(self.two, Pair):
second_id = id_group_map[self.two.corner_group_id(which | LEFT_EDGE)].active_window_id
else:
start = g1.top + g1.height
if isinstance(self.one, Pair):
first_id = id_group_map[self.one.corner_group_id(which | BOTTOM_EDGE)].active_window_id
if isinstance(self.two, Pair):
second_id = id_group_map[self.two.corner_group_id(which | TOP_EDGE)].active_window_id
yield start, start + self.border_width, first_id * mult
yield start + self.border_width, start + 2*self.border_width, second_id * mult
yield from edges(self.two, g2)
def corner_group_id(self, which: int) -> int:
if self.is_redundant:
q = self.one or self.two
elif self.horizontal:
q = self.one if which & LEFT_EDGE else self.two
else:
q = self.one if which & TOP_EDGE else self.two
if q is None:
return 0
return q if isinstance(q, int) else q.corner_group_id(which)
def pair_geometry(self, id_group_map: dict[int, WindowGroup]) -> 'tuple[Pair | None, Pair | None]':
g1: Pair | None = None
g2 = g1
if self.one is not None:
if isinstance(self.one, int):
g1 = Pair()
g1.one = self.one
e = self.first_extent
g1.left, g1.top, g1.width, g1.height = e.left, e.top, e.right - e.left, e.bottom - e.top
else:
g1 = self.one
if self.two is not None:
if isinstance(self.two, int):
g2 = Pair()
g2.one = self.two
g = self.second_extent
g2.left, g2.top, g2.width, g2.height = g.left, g.top, g.right - g.left, g.bottom - g.top
else:
g2 = self.two
return g1, g2
def set_bias(self, window_id: int, bias: int) -> None:
b = max(0, min(bias, 100)) / 100
self.bias = b if window_id == self.one else (1. - b)
@ -343,55 +436,6 @@ def modify_size_of_child(self, which: int, increment: float, is_horizontal: bool
return parent.modify_size_of_child(which, increment, is_horizontal, layout_object)
return False
def borders_for_window(self, layout_object: 'Splits', window_id: int) -> Generator[Edges, None, None]:
is_first = self.one == window_id
if self.between_borders:
yield self.between_borders[0 if is_first else 1]
q = self
found_same_direction = found_transverse1 = found_transverse2 = False
while not (found_same_direction and found_transverse1 and found_transverse2):
parent = q.parent(layout_object.pairs_root)
if parent is None:
break
q = parent
if not q.between_borders:
continue
if q.horizontal == self.horizontal:
if not found_same_direction:
if self.horizontal:
is_before = q.between_borders[0].left <= self.left
else:
is_before = q.between_borders[0].top <= self.top
if is_before == is_first:
found_same_direction = True
edges = q.between_borders[1 if is_before else 0]
if self.horizontal:
yield edges._replace(top=self.top, bottom=self.top + self.height)
else:
yield edges._replace(left=self.left, right=self.left + self.width)
else:
if self.horizontal:
is_before = q.between_borders[0].top <= self.top
else:
is_before = q.between_borders[0].left <= self.left
extent = self.first_extent if is_first else self.second_extent
if is_before:
if not found_transverse1:
found_transverse1 = True
edges = q.between_borders[1]
if self.horizontal:
yield edges._replace(left=extent.start, right=extent.end)
else:
yield edges._replace(top=extent.start, bottom=extent.end)
else:
if not found_transverse2:
found_transverse2 = True
edges = q.between_borders[0]
if self.horizontal:
yield edges._replace(left=extent.start, right=extent.end)
else:
yield edges._replace(top=extent.start, bottom=extent.end)
def neighbors_for_window(self, window_id: int, ans: NeighborsMap, layout_object: 'Splits', all_windows: WindowList) -> None:
def quadrant(is_horizontal: bool, is_first: bool) -> tuple[EdgeLiteral, EdgeLiteral]:
@ -602,19 +646,23 @@ def minimal_borders(self, all_windows: WindowList) -> Generator[BorderLine, None
window_count = len(groups)
if not lgd.draw_minimal_borders or window_count < 2:
return
for pair in self.pairs_root.self_and_descendants():
for edges in pair.between_borders:
yield BorderLine(edges)
needs_borders_map = all_windows.compute_needs_borders_map(lgd.draw_active_borders)
ag = all_windows.active_group
active_group_id = -1 if ag is None else ag.id
border_color_map = {}
for grp_id, needs_borders in needs_borders_map.items():
if needs_borders:
qpair = self.pairs_root.pair_for_window(grp_id)
if qpair is not None:
wid = g.active_window_id if (g := all_windows.group_for_id(grp_id)) else 0
if wid:
color = BorderColor.active if grp_id is active_group_id else BorderColor.bell
for edges in qpair.borders_for_window(self, grp_id):
yield BorderLine(edges, color)
border_color_map[wid] = color
for pair in self.pairs_root.self_and_descendants():
if pair.between_borders:
for which in pair.between_borders:
for bb in which:
yield bb._replace(color=border_color_map.get(abs(bb.window_id), BorderColor.inactive))
def neighbors_for_window(self, window: WindowType, all_windows: WindowList) -> NeighborsMap:
wg = all_windows.group_for_window(window)

View file

@ -366,9 +366,10 @@ def minimal_borders(self, all_windows: WindowList) -> Generator[BorderLine, None
xl.content_pos + xl.content_size + xl.space_after,
yl.content_pos + ((yl.content_size + yl.space_after) if mirrored else (bw - yl.space_before)),
)
perp_borders.append(BorderLine(e1, color))
perp_borders.append(BorderLine(e2, color))
perp_borders.append(BorderLine(e3, color))
wid = wg.active_window_id
perp_borders.append(BorderLine(e1, color, wid))
perp_borders.append(BorderLine(e2, color, wid))
perp_borders.append(BorderLine(e3, color, wid))
mirrored = self.layout_opts.mirrored
yield from borders(

View file

@ -50,8 +50,8 @@ def borders(
color = BorderColor.inactive
if needs_borders_map.get(wg.id):
color = BorderColor.active if wg is active_group else BorderColor.bell
borders.append(BorderLine(e1, color))
borders.append(BorderLine(e2, color))
borders.append(BorderLine(e1, color, wg.active_window_id))
borders.append(BorderLine(e2, color, wg.active_window_id))
last_idx = len(borders) - 1 - end_offset
for i, x in enumerate(borders):

View file

@ -260,8 +260,8 @@ border_contains_mouse(BorderRect *br, double tolerance, Edge *edges) {
if ((int)br->px.left - tolerance <= x && x < (int)br->px.right + tolerance && (int)br->px.top - tolerance <= y && y < (int)br->px.bottom + tolerance) {
ans = true;
if (br->px.right - br->px.left < br->px.bottom - br->px.top)
*edges |= br->border_type < 1 ? LEFT_EDGE : RIGHT_EDGE;
else *edges |= br->border_type < 1 ? TOP_EDGE : BOTTOM_EDGE;
*edges |= br->border_type < 0 ? LEFT_EDGE : RIGHT_EDGE;
else *edges |= br->border_type < 0 ? TOP_EDGE : BOTTOM_EDGE;
}
return ans;
}
@ -922,6 +922,7 @@ window_for_event(unsigned int *window_idx, bool *in_tab_bar, Edge *window_border
Tab *t = global_state.callback_os_window->tabs + global_state.callback_os_window->active_tab;
if (window_border && num_visible_windows(t) > 1) {
*window_border = 0;
id_type window_id = 0;
double dpi = (w->fonts_data->logical_dpi_x + w->fonts_data->logical_dpi_y) / 2.;
double tolerance = ((long)round((OPT(window_drag_tolerance) * (dpi / 72.0))));
BorderRect *closest_vert = NULL, *closest_horiz = NULL;
@ -934,6 +935,7 @@ window_for_event(unsigned int *window_idx, bool *in_tab_bar, Edge *window_border
*window_border |= edges;
if (edges & (LEFT_EDGE | RIGHT_EDGE)) { closest_vert_dist = -1; closest_vert = NULL; }
else { closest_horiz_dist = -1; closest_horiz = NULL; }
window_id = br->border_type < 0 ? -br->border_type : br->border_type;
} else if (border_contains_mouse(br, tolerance, &edges)) {
unsigned width = br->px.right - br->px.left, height = br->px.bottom - br->px.top;
if (width < height) {
@ -945,8 +947,16 @@ window_for_event(unsigned int *window_idx, bool *in_tab_bar, Edge *window_border
}
}
}
if (closest_vert) border_contains_mouse(closest_vert, tolerance, window_border);
if (closest_horiz) border_contains_mouse(closest_horiz, tolerance, window_border);
if (closest_vert && border_contains_mouse(closest_vert, tolerance, window_border) && !window_id)
window_id = closest_vert->border_type < 0 ? -closest_vert->border_type : closest_vert->border_type;
if (closest_horiz && border_contains_mouse(closest_horiz, tolerance, window_border) && !window_id)
window_id = closest_horiz->border_type < 0 ? -closest_horiz->border_type : closest_horiz->border_type;
if (*window_border) {
if (!window_id) return NULL;
for (unsigned int i = 0; i < t->num_windows; i++)
if (t->windows[i].id == window_id) return t->windows + i;
return NULL;
}
}
for (unsigned int i = 0; i < t->num_windows; i++) {
if (contains_mouse(t->windows + i) && t->windows[i].render_data.screen) {
@ -1204,9 +1214,7 @@ mouse_event(const int button, int modifiers, int action) {
handle_tab_bar_mouse(button, modifiers, action);
debug("handled by tab bar\n");
} else if (window_border) {
debug("window border: %d\n", window_border);
w = window_for_event(&window_idx, &in_tab_bar, NULL);
if (!w) w = closest_window_for_event(&window_idx);
debug("window border: %d window id: %llu\n", window_border, w ? w->id : 0);
if (window_border & LEFT_EDGE) {
if (window_border & TOP_EDGE) mouse_cursor_shape = NWSE_RESIZE_POINTER;
else if (window_border & BOTTOM_EDGE) mouse_cursor_shape = NESW_RESIZE_POINTER;

View file

@ -612,10 +612,10 @@ pyset_borders_rects(PyObject *self UNUSED, PyObject *args) {
ensure_space_for(br, rect_buf, BorderRect, br->num_border_rects + 1, capacity, 32, false);
for (unsigned i = 0; i < br->num_border_rects; i++) {
PyObject *pr = PyList_GET_ITEM(rects, i);
unsigned long color; int border_type;
unsigned long color; long long border_type;
BorderRect *r = br->rect_buf + i;
if (!PyArg_ParseTuple(
pr, "IIIIki", &r->px.left, &r->px.top, &r->px.right, &r->px.bottom, &color, &border_type
pr, "IIIIkL", &r->px.left, &r->px.top, &r->px.right, &r->px.bottom, &color, &border_type
)) return NULL;
r->left = gl_pos_x(r->px.left, osw->viewport_width);
r->top = gl_pos_y(r->px.top, osw->viewport_height);

View file

@ -240,7 +240,7 @@ typedef struct BorderRect {
float left, top, right, bottom;
struct { unsigned left, top, right, bottom; } px;
uint32_t color;
int border_type;
long long border_type;
} BorderRect;
typedef struct BorderRects {

View file

@ -948,7 +948,8 @@ def refresh(self, reload_all_gpu_data: bool = False) -> None:
def pause_resize_notifications_to_child(self, pause: bool = True) -> None:
if pause:
self._pause_resize_notifications_to_child = -1, -1, -1, -1
if self._pause_resize_notifications_to_child is None:
self._pause_resize_notifications_to_child = -1, -1, -1, -1
else:
p, self._pause_resize_notifications_to_child = self._pause_resize_notifications_to_child, None
if p and p[0] > 0:

View file

@ -350,6 +350,12 @@ def group_for_window(self, x: WindowOrId) -> WindowGroup | None:
return g
return None
def group_for_id(self, gid: int) -> WindowGroup | None:
for g in self.groups:
if g.id == gid:
return g
return None
def group_idx_for_window(self, x: WindowOrId) -> int | None:
q = self.id_map[x] if isinstance(x, int) else x
for i, g in enumerate(self.groups):