test(nix): share the columns of the scrolling layout, and two monitors

The checks of the scrolling suite were KDE's, but kfde and komorebi for
Windows lay the strip out with the same model. scrolling.columns is the
scenario of the five windows in two columns, now run by the scrolling
suite and by core-e2e; scrolling.two_monitors checks that a strip with a
display next to it leaves no window shown on that display.
This commit is contained in:
LGUG2Z committed 2026-10-30 16:18:32 -07:00
1 parent b4edd1c023
commit a8e79ca5d2
9 files changed
+438 -185

No files matched your search

+2
View File
@@ -60,6 +60,8 @@ runs, and goes stale when the code changes.
| `pausing` | a paused daemon leaves a new window alone, and an unpaused one takes the next | pass | pass |
| `tiling_off` | `toggle-tiling` leaves the windows where they are, a layout change moves none, and tiling again tiles them | pass | pass |
| `scrolling` | in the scrolling layout each window in turn has the focus and is inside the work area | pass | pass |
| `scrolling.columns` | five windows in two columns: the ones out of view rest at an edge with their width, the strip follows the focus and `pan`, and `consume`, `expel`, `toggle-column-stack` and `scrolling-layout-full-width` work | pass | not run yet |
| `scrolling.two_monitors` | a strip with a display next to it: scrolling with `focus` and `pan` shows no window on the second display | pass | not run yet |
| `monitors` | the monitor commands (`send-to-monitor`, `move-to-monitor`, their cycle, workspace and named forms, `focus-monitor`, `cycle-monitor`, `focus-monitor-workspace`, `cycle-move-workspace-to-monitor`, `focus-monitor-at-cursor` where there is a pointer) put each window on the monitor and workspace asked for, tiled in its display, and the focus where each leaves it | not run | not run |
| `layout_flip` | `layout_flip` in the config (`Horizontal`, `Vertical`, `HorizontalAndVertical`) mirrors a workspace's layout from the start, `flip-layout` turns it off and on, and it is the named workspace's own | not run | not run |
+4 -3
View File
@@ -55,8 +55,9 @@ is, whether it is hidden, how wide it really is) is a call to the backend in kfd
`WindowManager::process_command` against the fake desktop, a strip next to another monitor, and a
drag of the left edge of a column.
- `komorebic/tests/cli.rs`: each new subcommand sends its message.
- `nix/tests/vm/lib/komorebi_vm/platforms/kde/extras/scrolling.py`, run by `vm-scrolling`: where KWin
keeps a window at the edge and the commands of the columns, in a real session. Run on KDE: 24 passed,
0 failed. KWin leaves a window resting at the left edge with its right edge a few pixels inside the
- `nix/tests/vm/lib/komorebi_vm/core/scenarios/scrolling.py` (`columns`, shared with komorebi for
Windows), run by `vm-scrolling` and `core-e2e`: where the daemon keeps a window at the edge and the
commands of the columns, in a real session. Run on KDE: the `scrolling` suite passes
(32 checks, with `two_monitors`). KWin leaves a window resting at the left edge with its right edge a few pixels inside the
work area, or a cell short of them, because foot rounds its frame to whole cells. The shared
`scrolling` scenario passes in `core-e2e` too.
+9 -9
View File
@@ -181,15 +181,15 @@ per-output desktops.
## scrolling
The shared `scrolling` scenario (run by `core-e2e`) puts each window in view in turn. This suite is
about the windows that are not in view: where KWin keeps the columns that scroll off the screen
(at an edge, with a few pixels of them on it), and that the strip follows the focus. It also runs
the commands of the columns of several windows (`pan`, `consume`, `toggle-column-stack`, `expel`,
`scrolling-layout-full-width`), which are kfde's and not in the shared scenario because komorebi
for Windows has them only on its `next` branch. All of it is KWin's; it is the extra
`scrolling.run`. The check that a window keeps the distance of a column from the one in view,
which the layout does not keep any more (a window rests at the edge), was replaced by two that it
rests there and keeps its width.
The shared `scrolling` scenario (run by `core-e2e`) puts each window in view in turn. This suite
runs the shared `scrolling.columns`: five windows in two columns, where the daemon rests the ones
that scroll out of view (at an edge, a few pixels of them on the screen, their width kept), that the
strip follows the focus and `pan`, and the commands of the columns of several windows (`consume`,
`toggle-column-stack`, `expel`, `scrolling-layout-full-width`). kfde and komorebi for Windows lay
the strip out with the same model, so none of it is KDE's: the extra `scrolling.run` is gone, and
`core-e2e` runs `scrolling.columns` too, from a workspace with no window. The check that a window
keeps the distance of a column from the one in view, which the layout does not keep any more (a
window rests at the edge), was replaced by two that it rests there and keeps its width.
## stacks
@@ -1,10 +1,24 @@
"""In the scrolling layout the window that has the focus is in view, wherever the others are."""
"""In the scrolling layout the window that has the focus is in view, wherever the others are.
`run` checks that on the windows a suite has open. `columns` opens five windows of its own in two
columns, and checks where the strip puts the ones that are out of view, that it scrolls with the
focus and with `pan`, and the commands of the columns (`consume`, `expel`, `toggle-column-stack`,
`scrolling-layout-full-width`). Both ports lay the strip out with the same model.
"""
from komorebi_vm.core.checks import Checks
from komorebi_vm.core.geometry import Rect
from komorebi_vm.core.platform import Client, Platform
from komorebi_vm.core.scenarios.layouts import tiled
from komorebi_vm.core.scenarios.monitors import _plug
from komorebi_vm.core.scenarios.support import focus_agrees
STRIP = 5
# how many pixels of a window that rests at an edge of the work area are on the screen: 5 of them by
# design, and the paddings, what the compositor rounds and a terminal's grid of cells are the rest
PARKED_AT_MOST = 30
def run(platform: Platform, checks: Checks, clients: list[Client]) -> None:
"""Changes the layout of the first workspace, which has `clients` tiled in a container each,
@@ -31,3 +45,203 @@ def run(platform: Platform, checks: Checks, clients: list[Client]) -> None:
"back in bsp every window is where the layout says",
platform.wait_for(lambda: tiled(platform, clients), timeout=20),
)
def columns(platform: Platform, checks: Checks, clients: list[Client]) -> None:
"""Five windows in two columns on a workspace with none: the last two are in view and the
others are scrolled out, where the system keeps them; focusing to the far left and back, and
`pan`, scroll the strip; then the commands of a column of several windows. The layout is bsp
again and the windows are closed at the end."""
area = platform.work_area()
left, right = area.x, area.x + area.w
tol = platform.tolerance
titles = [f"strip{i}" for i in range(1, STRIP + 1)]
def frame(i: int) -> Rect:
window = platform.os_window(Client(titles[i - 1]))
return window.frame if window is not None else Rect(0, 0, 0, 0)
def in_view(i: int) -> bool:
r = frame(i)
return r.x >= left - tol and r.x + r.w <= right + tol
def out_of_view(i: int) -> bool:
r = frame(i)
return r.x >= right - PARKED_AT_MOST or r.x + r.w <= left + PARKED_AT_MOST
def agree(i: int) -> bool:
return focus_agrees(platform, [Client(t) for t in titles], titles[i - 1])
def wait(predicate, timeout: float = 10) -> bool: # noqa: ANN001
return platform.wait_for(predicate, timeout=timeout)
platform.komorebic("change-layout", "scrolling")
platform.komorebic("scrolling-layout-columns", "2")
opened = [platform.spawn(t) for t in titles]
checks.check(
"the daemon manages all five", wait(lambda: platform.state().managed() >= STRIP, 20)
)
checks.check(
"the focused (last) window is in view, and so is the one before it",
wait(lambda: in_view(5) and in_view(4), 20),
)
checks.check(
"the first three are scrolled out of view",
wait(lambda: all(out_of_view(i) for i in (1, 2, 3)), 20),
)
# the window rests with its right edge a few pixels inside the work area, as wide as the one
# in view: nothing pulled it further onto the screen, and nothing was resized. A terminal draws
# in whole cells, so its frame can be a cell narrower than the tile it was rested by
checks.check(
"the first window rests at the left edge, a few pixels of it on the screen or a cell short"
f" of them (right edge at {frame(1).x + frame(1).w}), not pulled back",
wait(lambda: left - tol < frame(1).x + frame(1).w <= left + PARKED_AT_MOST),
)
checks.check(
f"... and it kept its width ({frame(1).w} against {frame(4).w})",
wait(lambda: abs(frame(1).w - frame(4).w) <= tol),
)
checks.check("the system and the daemon agree on the focus", wait(lambda: agree(5)))
for _ in range(STRIP - 1):
platform.komorebic("focus", "left")
checks.check(
"focusing to the far left scrolls the strip back: the first is in view",
wait(lambda: in_view(1) and agree(1)),
)
checks.check("... and the last is past the right edge", wait(lambda: out_of_view(5)))
for _ in range(STRIP - 1):
platform.komorebic("focus", "right")
checks.check(
"walking right again reaches the last, in view", wait(lambda: agree(5) and in_view(5))
)
# the view moves a column without the focus moving first
platform.komorebic("pan", "left")
checks.check(
"panning left brings the third into view and puts the last out of it",
wait(lambda: in_view(3) and out_of_view(5)),
)
checks.check(
"... and the focus goes to the nearest column in view, the fourth", wait(lambda: agree(4))
)
platform.komorebic("pan", "right")
checks.check("panning right puts the last in view again", wait(lambda: in_view(5)))
# a column of two windows: the fourth is put at the top of the column of the fifth
platform.komorebic("focus", "right")
wait(lambda: agree(5))
platform.komorebic("consume", "left")
checks.check(
"consuming left puts the fourth above the fifth, in one column",
wait(lambda: abs(frame(4).x - frame(5).x) <= tol and frame(4).y < frame(5).y),
)
checks.check(
"... each with a share of the height of the column",
wait(lambda: frame(4).h < area.h * 0.6 and frame(5).h < area.h * 0.6),
)
checks.check("... and the focus stays on the fifth", wait(lambda: agree(5)))
pair = [Client(titles[3]), Client(titles[4])]
platform.komorebic("toggle-column-stack")
checks.check(
"stacking the column shows one window of the two",
wait(lambda: sum(platform.hidden(c) for c in pair) == 1),
)
platform.komorebic("toggle-column-stack")
checks.check(
"spreading it out shows both again, one above the other",
wait(lambda: not any(platform.hidden(c) for c in pair) and frame(4).y < frame(5).y),
)
platform.komorebic("expel")
checks.check(
"expelling puts the focused window in a column of its own on the right",
wait(lambda: frame(5).x > frame(4).x + tol and abs(frame(5).y - frame(4).y) <= tol),
)
platform.komorebic("scrolling-layout-full-width")
checks.check(
"the full width makes the focused column as wide as the work area",
wait(lambda: frame(5).w >= area.w - 60 and in_view(5)),
)
platform.komorebic("scrolling-layout-full-width")
checks.check(
"and again puts it back to the width it had",
wait(lambda: abs(frame(5).w - frame(4).w) <= tol),
)
platform.komorebic("change-layout", "bsp")
for client in opened:
platform.close(client)
def two_monitors(platform: Platform, checks: Checks, clients: list[Client]) -> None:
"""A strip with a display next to it leaks no window onto it.
Five windows in two columns on the first monitor, with a second display on its right. A window
shows on whichever display it overlaps, so one that rests past the right edge, or overhangs it,
would be seen on the neighbour: the daemon hides it, or makes it narrower, instead. Scrolling
with `focus` and `pan`, both ways, leaves no shown window on the second display, and no window
that is shown past the right edge of the first. The layout is bsp again, the windows are
closed, and the display is unplugged if the scenario plugged it, at the end."""
plugged = _plug(platform)
first, second = platform.displays()[0], platform.displays()[1]
tol = platform.tolerance
titles = [f"leak{i}" for i in range(1, STRIP + 1)]
mine = [Client(t) for t in titles]
def leaks() -> list[str]:
"""The windows that are shown and have a part on the second display, or reach past the
right edge of the first."""
out = []
for client in mine:
window = platform.os_window(client)
if window is None or platform.hidden(client):
continue
frame = window.frame
if frame.overlaps(second) or frame.x + frame.w > first.x + first.w + tol:
out.append(f"{client.title} {frame}")
return out
def settled(title: str) -> bool:
return focus_agrees(platform, mine, title)
def quiet(step: str, title: str) -> None:
platform.wait_for(lambda: settled(title), timeout=10)
platform.wait_for(lambda: not leaks(), timeout=5)
checks.check(f"{step}: no window is shown on the second display", not leaks())
if leaks():
print(" leaked: " + "; ".join(leaks()))
platform.komorebic("focus-monitor", "0")
platform.komorebic("change-layout", "scrolling")
platform.komorebic("scrolling-layout-columns", "2")
opened = [platform.spawn(t) for t in titles]
checks.check(
"the daemon manages all five",
platform.wait_for(lambda: platform.state().managed() >= STRIP, timeout=20),
)
quiet("with the strip at its right end", titles[-1])
for i in range(STRIP - 1, 0, -1):
platform.komorebic("focus", "left")
quiet(f"focusing left to {titles[i - 1]}", titles[i - 1])
for i in range(1, STRIP):
platform.komorebic("focus", "right")
quiet(f"focusing right to {titles[i]}", titles[i])
platform.komorebic("pan", "left")
platform.wait_for(lambda: not leaks(), timeout=10)
checks.check("panning left: no window is shown on the second display", not leaks())
platform.komorebic("pan", "right")
platform.wait_for(lambda: not leaks(), timeout=10)
checks.check("panning right: no window is shown on the second display", not leaks())
platform.komorebic("change-layout", "bsp")
for client in opened:
platform.close(client)
platform.komorebic("focus-workspace", "0")
if plugged:
platform.remove_display()
platform.wait_for(
lambda: len(platform.state().monitors) == len(platform.displays()), timeout=30
)
@@ -1,159 +0,0 @@
"""What only KDE can tell about the scrolling layout, beside the shared `scrolling` scenario.
The strip runs past the edge of the work area, and KWin is expected to leave the windows exactly
where the daemon puts them: a window that is out of view rests at the edge of the work area it left
by, with a few pixels of it on the screen and its size unchanged. KWin keeps part of a window it
resizes on an output, and the bridge asks again for the position of a window that was pulled back
(`settle` in `bridge.js`). The shared scenario checks that the focused window is in view; these
check where KWin keeps the ones that are not, that the strip scrolls with the focus `komorebic
focus` moves, and the commands of the columns that hold several windows (`consume`, `expel`,
`toggle-column-stack`, `scrolling-layout-full-width`, `pan`). Those commands are kfde's: the shared
scenario is also run against komorebi for Windows, which has them only on its `next` branch.
"""
from komorebi_vm.core.checks import Checks
from komorebi_vm.core.geometry import Rect
from komorebi_vm.core.platform import Client
from komorebi_vm.kde_platform import KdePlatform
WINDOWS = 5
# how many pixels of a window that rests at an edge of the work area are on the screen (5 of them
# by design, and the paddings and what the compositor rounds are the rest)
PARKED_AT_MOST = 30
def run(platform: KdePlatform, checks: Checks, clients: list[Client]) -> None:
"""Five windows in two columns: the last two are in view and the others are scrolled out, where
KWin keeps them; focusing to the far left and back scrolls the strip with the focus. The daemon
must be running with no window. The daemon is stopped at the end."""
kde = platform.kde
width = platform.size[0]
def n(i: int) -> Rect:
return kde.rect(kde.wid(f"win{i}"))
def in_view(r: Rect) -> bool:
return r.x >= 0 and r.x + r.w <= width
def out_of_view(r: Rect) -> bool:
return r.x >= width - PARKED_AT_MOST or r.x + r.w <= PARKED_AT_MOST
def focused_monitor() -> int:
return platform.state().focused_monitor
def agree(title: str) -> bool:
return kde.title(kde.active()) == title and kde.focused_title() == title
kde.komorebic("change-layout", "scrolling")
kde.komorebic("scrolling-layout-columns", "2")
for i in range(1, WINDOWS + 1):
platform.spawn(f"win{i}")
checks.check(
"the daemon manages all five", kde.wait_for(lambda: kde.managed() == WINDOWS, timeout=20)
)
checks.check(
"the focused (last) window is in view, and so is the one before it",
kde.wait_for(lambda: in_view(n(5)) and in_view(n(4)), timeout=20),
)
checks.check(
"the first three are scrolled out of view",
kde.wait_for(lambda: all(out_of_view(n(i)) for i in (1, 2, 3)), timeout=20),
)
for i in range(1, WINDOWS + 1):
print(f" win{i}: {n(i)}")
# the window rests with its right edge a few pixels inside the work area, and is as wide as
# the one in view: KWin did not pull it further onto the screen, and nothing was resized
# a terminal draws in whole cells, so its frame can be a cell narrower than the tile the daemon
# rests it by, and its right edge a cell short of the few pixels that were asked for
checks.check(
f"KWin left win1 at the left edge, a few pixels of it on the screen or a cell short of them"
f" (right edge at {n(1).x + n(1).w}), not pulled back",
kde.wait_for(lambda: -platform.tolerance < n(1).x + n(1).w <= PARKED_AT_MOST, timeout=10),
)
checks.check(
f"... and it kept its width ({n(1).w} against {n(4).w})",
kde.wait_for(lambda: abs(n(1).w - n(4).w) <= platform.tolerance, timeout=10),
)
checks.check("KWin and the daemon agree on the focus", kde.wait_for(lambda: agree("win5")))
for _ in range(4):
kde.komorebic("focus", "left")
kde.wait_for(lambda: kde.title(kde.active()) == kde.focused_title())
checks.check(
"focusing to the far left scrolls the strip back: win1 in view",
kde.wait_for(lambda: in_view(n(1)), timeout=10),
)
checks.check("... and win5 is past the right edge", kde.wait_for(lambda: out_of_view(n(5))))
checks.check("... and the focus is win1 on both sides", kde.wait_for(lambda: agree("win1")))
checks.check("... on the first monitor", focused_monitor() == 0)
for _ in range(4):
kde.komorebic("focus", "right")
kde.wait_for(lambda: kde.title(kde.active()) == kde.focused_title())
checks.check("walking right again reaches win5", kde.wait_for(lambda: agree("win5")))
checks.check("... without the focus having left the first monitor", focused_monitor() == 0)
checks.check("... and win5 is in view", kde.wait_for(lambda: in_view(n(5)), timeout=10))
# the view moves a column without the focus moving first
kde.komorebic("pan", "left")
checks.check(
"panning left brings win3 into view and puts win5 out of it",
kde.wait_for(lambda: in_view(n(3)) and out_of_view(n(5)), timeout=10),
)
checks.check(
"... and the focus goes to the nearest column in view, win4",
kde.wait_for(lambda: agree("win4")),
)
kde.komorebic("pan", "right")
checks.check("panning right puts win5 in view again", kde.wait_for(lambda: in_view(n(5))))
# a column of two windows: win4 is put at the top of the column of win5
kde.komorebic("focus", "right")
kde.wait_for(lambda: agree("win5"))
kde.komorebic("consume", "left")
checks.check(
"consuming left puts win4 above win5, in one column",
kde.wait_for(
lambda: abs(n(4).x - n(5).x) <= platform.tolerance and n(4).y < n(5).y, timeout=10
),
)
checks.check(
"... each with a share of the height of the column",
kde.wait_for(lambda: n(4).h < platform.size[1] * 0.6 and n(5).h < platform.size[1] * 0.6),
)
checks.check("... and the focus stays on win5", kde.wait_for(lambda: agree("win5")))
kde.komorebic("toggle-column-stack")
stacked = [Client("win4"), Client("win5")]
checks.check(
"stacking the column shows one window of the two",
kde.wait_for(lambda: sum(platform.hidden(c) for c in stacked) == 1, timeout=10),
)
kde.komorebic("toggle-column-stack")
checks.check(
"spreading it out shows both again, one above the other",
kde.wait_for(
lambda: not any(platform.hidden(c) for c in stacked) and n(4).y < n(5).y, timeout=10
),
)
kde.komorebic("expel")
checks.check(
"expelling puts the focused window in a column of its own on the right",
kde.wait_for(lambda: n(5).x > n(4).x + platform.tolerance and n(5).y == n(4).y, timeout=10),
)
kde.komorebic("scrolling-layout-full-width")
checks.check(
"the full width makes the focused column as wide as the work area",
kde.wait_for(lambda: n(5).w >= width - 60 and in_view(n(5)), timeout=10),
)
kde.komorebic("scrolling-layout-full-width")
checks.check(
"and again puts it back to the width it had",
kde.wait_for(lambda: abs(n(5).w - n(4).w) <= platform.tolerance, timeout=10),
)
kde.komorebic("stop")
kde.wait_for(lambda: not kde.komorebi_running())
+150
View File
@@ -0,0 +1,150 @@
"""A fake whose scrolling layout is a strip of columns, for the shared `scrolling.columns`.
Every window is a container of the workspace, as the daemon has it; the columns the strip lays
out are what this class adds: the windows of each, whether the column is stacked, its width, and
how far the strip is scrolled.
"""
from komorebi_vm.core.geometry import Rect
from komorebi_vm.core.platform import Client, OsWindow
from tests.fake_platform import FakePlatform
REST = 5
class FakeStrip(FakePlatform):
def __init__(self, *args, **kwargs) -> None: # noqa: ANN002, ANN003
super().__init__(*args, **kwargs)
self.cols: list[list[str]] = []
self.stacked: set[int] = set()
self.wide: set[int] = set()
self.per_view = 2
self.offset = 0
self.leaks = False
# the geometry of the strip
def _scrolls(self) -> bool:
return self.layout_name == "Scrolling"
def _width(self, i: int) -> int:
w = self.area.w
return w if i in self.wide else w // self.per_view
def _left(self, i: int) -> int:
return sum(self._width(j) for j in range(i))
def _column_of(self, title: str) -> int:
return next(i for i, col in enumerate(self.cols) if title in col)
def _title_in_focus(self) -> str:
return self.containers[self.focus][0]
def _keep_in_view(self) -> None:
i = self._column_of(self._title_in_focus())
left, right = self._left(i), self._left(i) + self._width(i)
if left < self.offset:
self.offset = left
elif right > self.offset + self.area.w:
self.offset = right - self.area.w
def _focus_column(self, i: int) -> None:
title = self.cols[i][0]
self.focus = next(k for k, c in enumerate(self.containers) if c[0] == title)
def _strip_frame(self, title: str) -> Rect:
i = self._column_of(title)
x, w = self._left(i) - self.offset, self._width(i)
if x + w <= 0:
x = REST - w
elif x >= self.area.w:
x = self.area.w - REST
if self._next_door() and x < self.area.w < x + w:
w = self.area.w - x
col = self.cols[i]
if len(col) == 1 or i in self.stacked:
return Rect(x, 0, w, self.area.h)
half = self.area.h // len(col)
return Rect(x, col.index(title) * half, w, half)
def _next_door(self) -> bool:
"""Another display is next to the right edge, and the daemon looks after it."""
return self.display_count > 1 and not self.leaks
# what the windows are and do
def spawn(self, title, *args, **kwargs) -> Client: # noqa: ANN001, ANN002, ANN003
client = super().spawn(title, *args, **kwargs)
if title in [c[0] for c in self.containers]:
self.cols.append([title])
self.focus = len(self.containers) - 1
self._keep_in_view()
return client
def close(self, client: Client) -> None:
super().close(client)
self.cols = [[t for t in col if t != client.title] for col in self.cols]
self.cols = [col for col in self.cols if col]
self.stacked = set()
self.wide = set()
def os_window(self, client: Client) -> OsWindow | None:
window = super().os_window(client)
if window is None or not self._scrolls() or client.title not in sum(self.cols, []):
return window
return OsWindow(window.title, self._strip_frame(client.title), window.focused)
def hidden(self, client: Client) -> bool:
if self._scrolls() and client.title in sum(self.cols, []):
i = self._column_of(client.title)
if self._next_door() and self._strip_frame(client.title).x >= self.area.w - REST:
return True
return i in self.stacked and client.title != self._title_in_focus()
return super().hidden(client)
def komorebic(self, *args: str) -> str:
if args[0] in ("focus-monitor", "focus-workspace"):
return ""
if not self._scrolls() and args[0] != "change-layout":
return super().komorebic(*args)
i = self._column_of(self._title_in_focus()) if self.cols and self._scrolls() else 0
match args:
case ("focus", "left" | "right" as way):
self._focus_column(
max(0, min(i + (1 if way == "right" else -1), len(self.cols) - 1))
)
self._keep_in_view()
case ("scrolling-layout-columns", n):
self.per_view = int(n)
case ("pan", way):
step = self._width(0) * (1 if way == "right" else -1)
limit = max(0, self._left(len(self.cols)) - self.area.w)
self.offset = max(0, min(self.offset + step, limit))
shown = [
j
for j in range(len(self.cols))
if self._left(j) >= self.offset
and self._left(j) + self._width(j) <= self.offset + self.area.w
]
if i not in shown:
self._focus_column(min(shown, key=lambda j: abs(j - i)))
case ("consume", "left") if i > 0:
self.cols[i - 1 : i + 1] = [self.cols[i - 1] + self.cols[i]]
self.stacked = set()
self.wide = set()
case ("expel",) if len(self.cols[i]) > 1:
title = self._title_in_focus()
self.cols[i].remove(title)
self.cols.insert(i + 1, [title])
self.stacked = set()
self.wide = set()
self._focus_column(i + 1)
self._keep_in_view()
case ("toggle-column-stack",):
self.stacked ^= {i}
case ("scrolling-layout-full-width",):
self.wide ^= {i}
self._keep_in_view()
case ("change-layout", name):
self.layout_name = "BSP" if name == "bsp" else name.title()
case _:
return super().komorebic(*args)
return ""
@@ -40,6 +40,7 @@ from komorebi_vm.core.scenarios import (
workspaces,
)
from tests.fake_platform import FakePlatform
from tests.fake_strip import FakeStrip
def opened(platform: FakePlatform, checks: Checks): # noqa: ANN201
@@ -918,3 +919,39 @@ def test_a_platform_that_cannot_move_a_window_while_locked_fails_locking() -> No
clients = opened(platform, checks)
locking.run(platform, checks, clients)
assert any("was moved while the screen was locked" in f for f in checks.failures)
def test_a_daemon_that_is_right_passes_the_scrolling_columns() -> None:
platform = FakeStrip()
checks = Checks()
scrolling.columns(platform, checks, [])
assert checks.failed == 0, checks.failures
assert checks.passed > 15
def test_a_daemon_that_pulls_the_windows_back_fails_the_scrolling_columns() -> None:
class Pulls(FakeStrip):
def _strip_frame(self, title): # noqa: ANN001, ANN202
r = super()._strip_frame(title)
return Rect(max(r.x, 0), r.y, r.w, r.h)
platform = Pulls()
checks = Checks()
scrolling.columns(platform, checks, [])
assert checks.failed > 0
def test_a_strip_with_a_monitor_next_to_it_leaks_nothing() -> None:
platform = FakeStrip()
checks = Checks()
scrolling.two_monitors(platform, checks, [])
assert checks.failed == 0, checks.failures
assert checks.passed > 10
def test_a_daemon_that_rests_windows_on_the_next_monitor_fails_two_monitors() -> None:
platform = FakeStrip()
platform.leaks = True
checks = Checks()
scrolling.two_monitors(platform, checks, [])
assert checks.failed > 0
+4
View File
@@ -150,6 +150,10 @@ run_suite(
# the option of a workspace in the config, on a daemon started with it
shared(layout_flip.run, platform, checks),
platform_only("kde", kde_core.no_window_is_left, *nothing),
shared(scrolling.columns, platform, checks, []),
platform_only("kde", kde_core.no_window_is_left, *nothing),
shared(scrolling.two_monitors, platform, checks, []),
platform_only("kde", kde_core.no_window_is_left, *nothing),
shared(config_reload.run, platform, checks),
platform_only(
"kde", kde_core.no_window_is_left, platform, checks, [Client("reload-stays")]
+17 -13
View File
@@ -1,16 +1,14 @@
# The scrolling layout in a real KWin. The strip runs past the edge of the work area, and KWin is
# expected to leave the windows exactly where komorebi puts them: a window that is out of view rests
# at the edge it left by, with a few pixels of it on the screen. What only a real KWin can tell is in
# `komorebi_vm.platforms.kde.extras.scrolling`: that KWin keeps them there, that the strip scrolls
# with the focus `komorebic focus` moves, and the commands of the columns that hold several windows
# (`consume`, `expel`, `toggle-column-stack`, `scrolling-layout-full-width`, `pan`). The shared scenario
# (`komorebi_vm.core.scenarios.scrolling`), which `core-e2e` runs and which runs on Windows too,
# checks the focused window is in view. `komorebi/tests/layout_options.rs` checks where komorebi
# puts the columns against the fake.
# The scrolling layout in a real KWin, with the shared scenario `scrolling.columns`
# (`komorebi_vm.core.scenarios.scrolling`), which runs on Windows too: the strip runs past the edge
# of the work area, a window that is out of view rests at the edge it left by with a few pixels of
# it on the screen, a strip with a monitor next to it leaks no window onto it (`two_monitors`), the strip scrolls with the focus and with `pan`, and the commands of the columns
# (`consume`, `expel`, `toggle-column-stack`, `scrolling-layout-full-width`) work. `core-e2e` runs it
# too, among the scenarios that start from a workspace with no window.
# `komorebi/tests/layout_options.rs` checks where komorebi puts the columns against the fake.
from komorebi_vm import Checks, Kde
from komorebi_vm.core.suite import platform_only, run_suite
from komorebi_vm.core.scenarios import scrolling
from komorebi_vm.core.suite import run_suite, shared
from komorebi_vm.kde_platform import KdePlatform
from komorebi_vm.platforms.kde.extras import scrolling as kde_scrolling
W, H = 1920, 1080
@@ -19,11 +17,17 @@ machine.wait_for_unit("multi-user.target")
machine.wait_for_unit("user@1000.service")
checks = Checks()
kde = Kde.headless(machine, "main", size=(W, H), scale=SCALE)
# two outputs, of which the second is unplugged before the daemon starts: `two_monitors` plugs it in
kde = Kde.headless(machine, "main", size=(W, H), outputs=2, scale=SCALE)
platform = KdePlatform(kde, (W, H))
platform.remove_display()
platform.start_komorebi()
run_suite(checks, platform_only("kde", kde_scrolling.run, platform, checks, []))
run_suite(
checks,
shared(scrolling.columns, platform, checks, []),
shared(scrolling.two_monitors, platform, checks, []),
)
kde.stop()
checks.finish()