mirror of
https://github.com/LGUG2Z/komorebi.git
synced 2026-11-14 21:04:04 +01:00
feat(nix): the drag-resize edge cases are a shared scenario
resize.edge_cases makes its drags from the daemon's layout and reads the frames from the operating system: both sides of a shared edge, a horizontal edge, a corner and an edge of the screen, with a neighbour or without. The KDE extra keeps pointer_is_exact.
This commit is contained in:
1 parent
4db3f97e2e
commit
595706d66f
6 files changed
+284
-218
No files matched your search
@@ -58,12 +58,15 @@ every count, with the extra's own check left to say only where KWin has the wind
|
||||
|
||||
## drag-resize
|
||||
|
||||
The suite's own cases are all KDE's: the pointer put to the pixel, and eight drags of edges and
|
||||
corners checked against frames, tiles and neighbors. None of them is removed; they moved to
|
||||
`kde_drag_resize` as `pointer_is_exact` and `edge_cases`, and the suite calls the shared `resize`
|
||||
and `focus_follows_mouse` after them, as before. The shared `resize` makes one drag of one edge,
|
||||
which the extra's `win1:right:...` cases also make: that is a repeat by design, because the extra
|
||||
reads the daemon's tiles and the frames together, and the shared one reads only the system.
|
||||
The suite's own cases were all KDE's: the pointer put to the pixel, and eight drags of edges and
|
||||
corners checked against frames, tiles and neighbors. The pointer check is the extra
|
||||
`pointer_is_exact`. The eight drags are the shared `resize.edge_cases`, which makes its cases from
|
||||
the daemon's layout (`latest_layout`) and reads the frames from the operating system, not from
|
||||
KWin: both sides of an edge two tiles share, a horizontal edge, a corner and an edge of the screen.
|
||||
With three windows in BSP the cases are the same eight as before. What changed: the wait for the
|
||||
end of a drag is the layout changing (three seconds at most) and the frames settling, not the
|
||||
daemon's log line `ResizeEnd`; and the strict checks use the platform's `tolerance` where the extra
|
||||
used the cell of the font. The shared `resize.run` makes one drag of one edge, as before.
|
||||
|
||||
One run of the suite failed `win2:left:-240,0` ("frame R moved -96 but was not dragged": the
|
||||
terminal's frame four cells short of an unchanged tile) and the next passed; not seen again.
|
||||
|
||||
@@ -93,3 +93,217 @@ def run(platform: Platform, checks: Checks, clients: list[Client], dx: int | Non
|
||||
"and do not overlap",
|
||||
not any(a.overlaps(b) for i, a in enumerate(frames) for b in frames[i + 1 :]),
|
||||
)
|
||||
|
||||
|
||||
GRABS = {
|
||||
"left": "L",
|
||||
"right": "R",
|
||||
"top": "T",
|
||||
"bottom": "B",
|
||||
"topleft": "LT",
|
||||
"topright": "RT",
|
||||
"bottomleft": "LB",
|
||||
"bottomright": "RB",
|
||||
}
|
||||
# how far apart two windows may be for one to be the neighbour of the other: twice the gap
|
||||
NEIGHBOUR = 80
|
||||
|
||||
|
||||
def _edges(r: Rect) -> dict[str, int]:
|
||||
return {"L": r.x, "T": r.y, "R": r.x + r.w, "B": r.y + r.h}
|
||||
|
||||
|
||||
def _tiles(platform: Platform) -> dict[str, Rect]:
|
||||
"""The daemon's layout: the tile it gave each container, by the title of its window in view."""
|
||||
workspace = platform.state().workspace()
|
||||
return {
|
||||
c[min(workspace.focused_windows[i], len(c) - 1)].title: workspace.latest_layout[i]
|
||||
for i, c in enumerate(workspace.containers)
|
||||
if i < len(workspace.latest_layout) and c
|
||||
}
|
||||
|
||||
|
||||
def _frames(platform: Platform, clients: list[Client]) -> dict[str, Rect]:
|
||||
return {c.title: w.frame for c in clients if (w := platform.os_window(c)) is not None}
|
||||
|
||||
|
||||
def _settled(platform: Platform, clients: list[Client]) -> dict[str, Rect]:
|
||||
"""The frames once they have stopped changing."""
|
||||
last = _frames(platform, clients)
|
||||
for _ in range(20):
|
||||
platform.wait_for(lambda: False, timeout=0.5)
|
||||
now = _frames(platform, clients)
|
||||
if now == last:
|
||||
return now
|
||||
last = now
|
||||
return last
|
||||
|
||||
|
||||
def _press_point(grab: str, r: Rect) -> tuple[int, int]:
|
||||
"""Where the pointer is pressed for a grab: in the gap outside the frame."""
|
||||
left, right, top, bottom = r.x - OFFSET, r.x + r.w + OFFSET, r.y - OFFSET, r.y + r.h + OFFSET
|
||||
cx, cy = r.x + r.w // 2, r.y + r.h // 2
|
||||
return {
|
||||
"left": (left, cy),
|
||||
"right": (right, cy),
|
||||
"top": (cx, top),
|
||||
"bottom": (cx, bottom),
|
||||
"topleft": (left, top),
|
||||
"topright": (right, top),
|
||||
"bottomleft": (left, bottom),
|
||||
"bottomright": (right, bottom),
|
||||
}[grab]
|
||||
|
||||
|
||||
def _has_neighbour(edge: str, name: str, frames: dict[str, Rect]) -> bool:
|
||||
"""Is there another window within a gap's reach on that side of this one, overlapping it."""
|
||||
me = frames[name]
|
||||
for other, o in frames.items():
|
||||
if other == name:
|
||||
continue
|
||||
vertical = o.y < me.y + me.h and me.y < o.y + o.h
|
||||
horizontal = o.x < me.x + me.w and me.x < o.x + o.w
|
||||
if edge == "L" and vertical and 0 <= me.x - (o.x + o.w) <= NEIGHBOUR:
|
||||
return True
|
||||
if edge == "R" and vertical and 0 <= o.x - (me.x + me.w) <= NEIGHBOUR:
|
||||
return True
|
||||
if edge == "T" and horizontal and 0 <= me.y - (o.y + o.h) <= NEIGHBOUR:
|
||||
return True
|
||||
if edge == "B" and horizontal and 0 <= o.y - (me.y + me.h) <= NEIGHBOUR:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def _stacked(layout: list[Rect]) -> tuple[int, int] | None:
|
||||
"""The first pair of tiles of which the second is below the first and overlaps it sideways."""
|
||||
for i, a in enumerate(layout):
|
||||
for j, b in enumerate(layout):
|
||||
if i != j and b.y >= a.y + a.h and a.x < b.x + b.w and b.x < a.x + a.w:
|
||||
return i, j
|
||||
return None
|
||||
|
||||
|
||||
def _cases(layout: list[Rect], titles: list[str], area: Rect) -> list[tuple[str, str, int, int]]:
|
||||
"""Drags to make, as (window, grab, dx, dy), from the layout: both sides of an edge that two
|
||||
tiles share, a horizontal one, a corner next to two neighbours, and an edge of the screen."""
|
||||
cases: list[tuple[str, str, int, int]] = []
|
||||
if (pair := _side_by_side(layout)) is not None:
|
||||
a, b = pair
|
||||
cases += [
|
||||
(titles[a], "right", 72, 0),
|
||||
(titles[a], "right", -72, 0),
|
||||
(titles[b], "left", -72, 0),
|
||||
(titles[b], "left", -240, 0),
|
||||
]
|
||||
if (stack := _stacked(layout)) is not None:
|
||||
a, b = stack
|
||||
cases += [(titles[a], "bottom", 0, 72), (titles[b], "top", 0, -72)]
|
||||
for k, tile in enumerate(layout):
|
||||
right_of = any(
|
||||
i != k and tile.x >= t.x + t.w and t.y < tile.y + tile.h and tile.y < t.y + t.h
|
||||
for i, t in enumerate(layout)
|
||||
)
|
||||
above = any(
|
||||
i != k and t.y >= tile.y + tile.h and tile.x < t.x + t.w and t.x < tile.x + tile.w
|
||||
for i, t in enumerate(layout)
|
||||
)
|
||||
if right_of and above:
|
||||
cases.append((titles[k], "bottomleft", -72, 72))
|
||||
break
|
||||
first = min(range(len(layout)), key=lambda i: layout[i].x)
|
||||
if _press_point("left", layout[first])[0] >= area.x:
|
||||
cases.append((titles[first], "left", -72, 0))
|
||||
return cases
|
||||
|
||||
|
||||
def _problems(
|
||||
platform: Platform,
|
||||
clients: list[Client],
|
||||
case: tuple[str, str, int, int],
|
||||
) -> list[str]:
|
||||
"""Makes a drag and says what is wrong with the outcome: the frame the user sees, the layout,
|
||||
the edges that were not dragged and the windows."""
|
||||
name, grab, dx, dy = case
|
||||
area = platform.work_area()
|
||||
platform.komorebic("retile")
|
||||
before = _settled(platform, clients)
|
||||
tiles_before = _tiles(platform)
|
||||
|
||||
px, py = _press_point(grab, before[name])
|
||||
end = (max(area.x, px + dx), max(area.y, py + dy))
|
||||
platform.pointer.drag((px, py), end)
|
||||
platform.wait_for(lambda: _tiles(platform) != tiles_before, timeout=3)
|
||||
after = _settled(platform, clients)
|
||||
tiles_after = _tiles(platform)
|
||||
|
||||
held = GRABS[grab]
|
||||
want = {"L": dx, "R": dx, "T": dy, "B": dy}
|
||||
problems = []
|
||||
moved = {e: _edges(after[name])[e] - _edges(before[name])[e] for e in "LTRB"}
|
||||
for e in "LTRB":
|
||||
if e in held and _has_neighbour(e, name, before):
|
||||
if abs(moved[e] - want[e]) > platform.tolerance:
|
||||
problems.append(f"frame {e} moved {moved[e]}, the pointer moved {want[e]}")
|
||||
elif abs(moved[e]) > platform.tolerance:
|
||||
why = "was not dragged" if e not in held else "has no neighbour"
|
||||
problems.append(f"frame {e} moved {moved[e]} but {why}")
|
||||
|
||||
tile_moved = {e: _edges(tiles_after[name])[e] - _edges(tiles_before[name])[e] for e in "LTRB"}
|
||||
for e in "LTRB":
|
||||
if e in held and _has_neighbour(e, name, before):
|
||||
if abs(tile_moved[e] - want[e]) > 2:
|
||||
problems.append(f"tile {e} moved {tile_moved[e]}, the pointer moved {want[e]}")
|
||||
elif abs(tile_moved[e]) > 1:
|
||||
why = "was not dragged" if e not in held else "has no neighbour"
|
||||
problems.append(f"tile {e} moved {tile_moved[e]} but {why}")
|
||||
|
||||
# nothing else moves, but with a dragged edge: they share it
|
||||
follows = [m for m in tile_moved.values() if abs(m) > 1]
|
||||
for other in tiles_after:
|
||||
if other == name:
|
||||
continue
|
||||
for e in "LTRB":
|
||||
m = _edges(tiles_after[other])[e] - _edges(tiles_before[other])[e]
|
||||
if abs(m) > 1 and not any(abs(m - f) <= 2 for f in follows):
|
||||
problems.append(f"{other} tile {e} moved {m}: not with the dragged edge")
|
||||
|
||||
names = list(after)
|
||||
for i, a in enumerate(names):
|
||||
if not after[a].inside(area):
|
||||
problems.append(f"{a} {after[a]} is outside the work area")
|
||||
problems += [f"{a} and {b} overlap" for b in names[i + 1 :] if after[a].overlaps(after[b])]
|
||||
print(f" {case}: frame {before[name]} -> {after[name]}, tile edges {tile_moved}")
|
||||
return problems
|
||||
|
||||
|
||||
def edge_cases(platform: Platform, checks: Checks, clients: list[Client]) -> None:
|
||||
"""Drags both sides of an edge two tiles share, a horizontal edge, a corner and an edge of the
|
||||
screen, and checks, against the layout the daemon computed (`latest_layout`) and the frames the
|
||||
operating system has:
|
||||
|
||||
- a dragged edge that has a neighbour moves by what the pointer moved, and the neighbour's
|
||||
edge moves with it
|
||||
- a dragged edge with no neighbour does not move, and the window is put back
|
||||
- edges that were not dragged do not move, and every edge of another tile stays or moves with
|
||||
a dragged one
|
||||
- the windows still tile: inside the work area, not on each other
|
||||
|
||||
`clients` are the tiled windows of the first workspace, each in a container of its own, in a
|
||||
layout that has them side by side or stacked. The cases are made from the layout, so that
|
||||
a layout with no stacked tiles has no horizontal case. The layout is retiled before each one.
|
||||
"""
|
||||
if not platform.has("pointer"):
|
||||
print(" skipped: this platform has no pointer")
|
||||
return
|
||||
platform.komorebic("retile")
|
||||
platform.wait_for(lambda: len(_tiles(platform)) == len(clients), timeout=20)
|
||||
tiles = _tiles(platform)
|
||||
titles = list(tiles)
|
||||
layout = [tiles[t] for t in titles]
|
||||
for case in _cases(layout, titles, platform.work_area()):
|
||||
problems = _problems(platform, clients, case)
|
||||
name, grab, dx, dy = case
|
||||
checks.check(
|
||||
f"{name}:{grab}:{dx},{dy}: " + ("as expected" if not problems else "; ".join(problems)),
|
||||
not problems,
|
||||
)
|
||||
@@ -1,211 +1,22 @@
|
||||
"""What only KDE can tell about dragging the edges of tiles with a real pointer.
|
||||
"""What only KDE can tell about dragging with a real pointer.
|
||||
|
||||
The pointer is a device made inside the guest, and KWin gives the daemon the end of each drag. Edges
|
||||
and corners of three tiles are grabbed and dragged, and what happened is checked against what a
|
||||
user expects (`komorebi/tests/drag_resize.rs` checks the window manager's side against the fake):
|
||||
The pointer is a device made inside the guest. The shared `resize` makes the drags of the edges and
|
||||
corners of the tiles, against the daemon's layout and the frames the operating system has
|
||||
(`komorebi/tests/drag_resize.rs` checks the window manager's side against the fake); what is here is
|
||||
that the device puts the pointer to the pixel, which a drag at a gap of four pixels depends on.
|
||||
|
||||
* a dragged edge that has a neighbor moves by what the pointer moved, and the neighbor's edge moves
|
||||
with it: they share it
|
||||
* a dragged edge with no neighbor (the edge of the screen) does not move, and the window is put back
|
||||
* edges that were not dragged do not move, and every edge of every other tile either stays or moves
|
||||
with a dragged one
|
||||
* the windows still tile: inside the screen, not overlapping
|
||||
|
||||
The strict checks are on the tiles, which are the daemon's own layout (`latest_layout` in
|
||||
`komorebic state`). A terminal's frame is checked loosely: it sizes itself to whole cells of its
|
||||
font, so it is a few pixels short of its tile. The shared `resize` makes one drag and reads the
|
||||
operating system; these are the cases it leaves out.
|
||||
|
||||
The suite runs `pointer_is_exact` and `edge_cases` on a session with a pointer and three tiled
|
||||
windows, `win1` to `win3`, then the shared scenarios.
|
||||
The suite runs `pointer_is_exact` on a session with a pointer and three tiled windows, `win1` to
|
||||
`win3`, then the shared `resize.edge_cases`, `resize.run` and `focus_follows_mouse`.
|
||||
"""
|
||||
|
||||
import re
|
||||
import time
|
||||
|
||||
from komorebi_vm import daemon
|
||||
from komorebi_vm.core.checks import Checks
|
||||
from komorebi_vm.core.platform import Client
|
||||
from komorebi_vm.kde_platform import CELL, KdePlatform
|
||||
from komorebi_vm.kde_platform import KdePlatform
|
||||
|
||||
# the module's unit, which the session starts when it logs in: the daemon the suite drags against
|
||||
UNIT = "komorebi.service"
|
||||
|
||||
# how far from its frame a window's resize border is pressed: in the gap between two tiles
|
||||
OFFSET = 4
|
||||
WINDOWS = 3
|
||||
|
||||
GRABS = {
|
||||
"left": "L",
|
||||
"right": "R",
|
||||
"top": "T",
|
||||
"bottom": "B",
|
||||
"topleft": "LT",
|
||||
"topright": "RT",
|
||||
"bottomleft": "LB",
|
||||
"bottomright": "RB",
|
||||
}
|
||||
|
||||
# BSP: win1 is the left half, win2 the top right and win3 the bottom right
|
||||
CASES = [
|
||||
"win1:right:72,0",
|
||||
"win1:right:-72,0",
|
||||
"win2:left:-72,0",
|
||||
"win2:left:-240,0",
|
||||
"win2:bottom:0,72",
|
||||
"win3:top:0,-72",
|
||||
"win2:bottomleft:-72,72",
|
||||
"win1:left:-72,0",
|
||||
]
|
||||
|
||||
Box = tuple[int, int, int, int]
|
||||
|
||||
|
||||
def frames(platform: KdePlatform) -> dict[str, Box]:
|
||||
kde = platform.kde
|
||||
out = {}
|
||||
for n in range(1, WINDOWS + 1):
|
||||
r = kde.rect(kde.wid(f"win{n}"))
|
||||
out[f"win{n}"] = (r.x, r.y, r.w, r.h)
|
||||
return out
|
||||
|
||||
|
||||
def tiles(platform: KdePlatform) -> dict[str, Box]:
|
||||
"""komorebi's layout: the tile it gave each window, by title"""
|
||||
workspace = platform.kde.state()["monitors"]["elements"][0]["workspaces"]["elements"][0]
|
||||
out = {}
|
||||
for i, container in enumerate(workspace["containers"]["elements"]):
|
||||
windows = container["windows"]
|
||||
title = windows["elements"][windows["focused"]]["details"]["title"]
|
||||
tile = workspace["latest_layout"][i]
|
||||
out[title] = (tile["left"], tile["top"], tile["right"], tile["bottom"])
|
||||
return out
|
||||
|
||||
|
||||
def edges(r: Box) -> dict[str, int]:
|
||||
x, y, w, h = r
|
||||
return {"L": x, "T": y, "R": x + w, "B": y + h}
|
||||
|
||||
|
||||
def settled(platform: KdePlatform) -> dict[str, Box]:
|
||||
"""The frames once they have stopped changing"""
|
||||
last = frames(platform)
|
||||
deadline = time.monotonic() + 10 * platform.kde.scale
|
||||
while time.monotonic() < deadline:
|
||||
time.sleep(0.5)
|
||||
now = frames(platform)
|
||||
if now == last:
|
||||
return now
|
||||
last = now
|
||||
return last
|
||||
|
||||
|
||||
def resize_ends(platform: KdePlatform) -> int:
|
||||
"""How many drags of an edge the daemon has handled the end of, by the unit's log"""
|
||||
log = platform.kde.run(f"journalctl --user -u {UNIT} --no-pager -o cat")[1]
|
||||
return sum(1 for e in daemon.events(log) if e.kind == "ResizeEnd")
|
||||
|
||||
|
||||
def press_point(grab: str, r: Box) -> tuple[int, int]:
|
||||
x, y, w, h = r
|
||||
left, right, top, bottom = x - OFFSET, x + w + OFFSET, y - OFFSET, y + h + OFFSET
|
||||
cx, cy = x + w // 2, y + h // 2
|
||||
return {
|
||||
"left": (left, cy),
|
||||
"right": (right, cy),
|
||||
"top": (cx, top),
|
||||
"bottom": (cx, bottom),
|
||||
"topleft": (left, top),
|
||||
"topright": (right, top),
|
||||
"bottomleft": (left, bottom),
|
||||
"bottomright": (right, bottom),
|
||||
}[grab]
|
||||
|
||||
|
||||
def has_neighbor(edge: str, name: str, before: dict[str, Box]) -> bool:
|
||||
"""Is there another window within a gap's reach on that side of this one, overlapping it"""
|
||||
mx, my, mw, mh = before[name]
|
||||
for other, (ox, oy, ow, oh) in before.items():
|
||||
if other == name:
|
||||
continue
|
||||
vertical_overlap = oy < my + mh and my < oy + oh
|
||||
horizontal_overlap = ox < mx + mw and mx < ox + ow
|
||||
if edge == "L" and vertical_overlap and 0 <= mx - (ox + ow) <= 80:
|
||||
return True
|
||||
if edge == "R" and vertical_overlap and 0 <= ox - (mx + mw) <= 80:
|
||||
return True
|
||||
if edge == "T" and horizontal_overlap and 0 <= my - (oy + oh) <= 80:
|
||||
return True
|
||||
if edge == "B" and horizontal_overlap and 0 <= oy - (my + mh) <= 80:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def run_case(platform: KdePlatform, spec: str) -> list[str]:
|
||||
kde = platform.kde
|
||||
W, H = platform.size
|
||||
name, grab, d = spec.split(":")
|
||||
dx, dy = (int(v) for v in d.split(","))
|
||||
|
||||
kde.komorebic("retile")
|
||||
before = settled(platform)
|
||||
tiles_before = tiles(platform)
|
||||
ends = resize_ends(platform)
|
||||
|
||||
px, py = press_point(grab, before[name])
|
||||
platform.pointer.drag((px, py), (px + dx, py + dy))
|
||||
kde.wait_for(lambda: resize_ends(platform) > ends, timeout=10)
|
||||
after = settled(platform)
|
||||
tiles_after = tiles(platform)
|
||||
|
||||
held = GRABS[grab]
|
||||
want = {"L": dx, "R": dx, "T": dy, "B": dy}
|
||||
problems = []
|
||||
|
||||
# the frame the user sees: it did what was asked, give or take the client's cells
|
||||
moved = {e: edges(after[name])[e] - edges(before[name])[e] for e in "LTRB"}
|
||||
for e in "LTRB":
|
||||
if e in held and has_neighbor(e, name, before):
|
||||
if abs(moved[e] - want[e]) > CELL:
|
||||
problems.append(f"frame {e} moved {moved[e]}, the pointer moved {want[e]}")
|
||||
elif abs(moved[e]) > CELL:
|
||||
why = "was not dragged" if e not in held else "has no neighbor"
|
||||
problems.append(f"frame {e} moved {moved[e]} but {why}")
|
||||
|
||||
# the layout, which is exact
|
||||
tile_moved = {e: edges(tiles_after[name])[e] - edges(tiles_before[name])[e] for e in "LTRB"}
|
||||
for e in "LTRB":
|
||||
if e in held and has_neighbor(e, name, before):
|
||||
if abs(tile_moved[e] - want[e]) > 2:
|
||||
problems.append(f"tile {e} moved {tile_moved[e]}, the pointer moved {want[e]}")
|
||||
elif abs(tile_moved[e]) > 1:
|
||||
why = "was not dragged" if e not in held else "has no neighbor"
|
||||
problems.append(f"tile {e} moved {tile_moved[e]} but {why}")
|
||||
|
||||
# nothing else moves, but with a dragged edge: they share it
|
||||
follows = [m for m in tile_moved.values() if abs(m) > 1]
|
||||
for other in tiles_after:
|
||||
if other == name:
|
||||
continue
|
||||
for e in "LTRB":
|
||||
m = edges(tiles_after[other])[e] - edges(tiles_before[other])[e]
|
||||
if abs(m) > 1 and not any(abs(m - f) <= 2 for f in follows):
|
||||
problems.append(f"{other} tile {e} moved {m}: not with the dragged edge")
|
||||
|
||||
# still a tiling
|
||||
names = list(after)
|
||||
for i, n1 in enumerate(names):
|
||||
x, y, w, h = after[n1]
|
||||
if x < 0 or y < 0 or x + w > W or y + h > H:
|
||||
problems.append(f"{n1} {after[n1]} is off the screen")
|
||||
for n2 in names[i + 1 :]:
|
||||
ox, oy, ow, oh = after[n2]
|
||||
if x < ox + ow and ox < x + w and y < oy + oh and oy < y + h:
|
||||
problems.append(f"{n1} and {n2} overlap")
|
||||
|
||||
print(f"{spec}: frame {before[name]} -> {after[name]}, tile edges {tile_moved}")
|
||||
return problems
|
||||
|
||||
|
||||
def _where(platform: KdePlatform) -> tuple[int, int]:
|
||||
found = re.search(r"x:(-?\d+) y:(-?\d+)", platform.kde.sh("kdotool getmouselocation"))
|
||||
@@ -225,13 +36,3 @@ def pointer_is_exact(platform: KdePlatform, checks: Checks, clients: list[Client
|
||||
gx, gy = _where(platform)
|
||||
worst = max(worst, abs(gx - x), abs(gy - y))
|
||||
checks.check(f"the pointer is put within a pixel of where it is asked ({worst})", worst <= 1)
|
||||
|
||||
|
||||
def edge_cases(platform: KdePlatform, checks: Checks, clients: list[Client]) -> None:
|
||||
"""Drags each edge and corner of `CASES` and checks the frames, the tiles and the neighbors."""
|
||||
for spec in CASES:
|
||||
problems = run_case(platform, spec)
|
||||
checks.check(
|
||||
f"{spec}: " + ("as expected" if not problems else "; ".join(problems)),
|
||||
not problems,
|
||||
)
|
||||
@@ -203,7 +203,9 @@ class FakePlatform:
|
||||
if not self.follows_pointer:
|
||||
return
|
||||
for i, tile in enumerate(self.tiles[:-1]):
|
||||
if abs(start[0] - (tile.x + tile.w + 4)) <= 1:
|
||||
right_of_it = abs(start[0] - (tile.x + tile.w + 4)) <= 1
|
||||
left_of_next = abs(start[0] - (self.tiles[i + 1].x - 4)) <= 1
|
||||
if right_of_it or left_of_next:
|
||||
a, b = tile, self.tiles[i + 1]
|
||||
self.tiles[i] = Rect(a.x, a.y, a.w + dx, a.h)
|
||||
self.tiles[i + 1] = Rect(b.x + dx, b.y, b.w - dx, b.h)
|
||||
@@ -659,6 +661,8 @@ class FakePlatform:
|
||||
self.saved[path] = list(self.tiles)
|
||||
case ("load-resize", path):
|
||||
self._load_resize(path)
|
||||
case ("retile",):
|
||||
self._relayout()
|
||||
case ("minimize",):
|
||||
self.minimize(Client(self.containers[self.focus][0]))
|
||||
case ("unmanage",):
|
||||
|
||||
@@ -11,6 +11,7 @@ from komorebi_vm.core.scenarios import (
|
||||
monocle,
|
||||
placement,
|
||||
queries,
|
||||
resize,
|
||||
rules,
|
||||
title_rule,
|
||||
)
|
||||
@@ -229,3 +230,45 @@ def test_a_daemon_that_does_not_follow_a_mode_change_fails_display_change() -> N
|
||||
clients = opened(platform, checks)
|
||||
display_change.run(platform, checks, clients)
|
||||
assert any("the daemon's monitor follows" in f for f in checks.failures)
|
||||
|
||||
|
||||
def test_a_daemon_that_follows_the_pointer_passes_the_edge_cases() -> None:
|
||||
platform = FakePlatform()
|
||||
checks = Checks()
|
||||
clients = opened(platform, checks)
|
||||
base = checks.passed
|
||||
resize.edge_cases(platform, checks, clients)
|
||||
assert checks.failed == 0, checks.failures
|
||||
assert checks.passed - base == 4
|
||||
|
||||
|
||||
def test_a_daemon_that_ignores_the_pointer_fails_the_edge_cases() -> None:
|
||||
platform = FakePlatform(follows_pointer=False)
|
||||
checks = Checks()
|
||||
clients = opened(platform, checks)
|
||||
resize.edge_cases(platform, checks, clients)
|
||||
assert checks.failed == 4, checks.failures
|
||||
|
||||
|
||||
def test_a_daemon_that_moves_the_other_edges_fails_the_edge_cases() -> None:
|
||||
class Sloppy(FakePlatform):
|
||||
def resize(self, start: tuple[int, int], dx: int) -> None:
|
||||
super().resize(start, dx)
|
||||
# the third tile is moved too, though no edge of it was dragged
|
||||
a = self.tiles[2]
|
||||
self.tiles[2] = Rect(a.x + dx, a.y, a.w, a.h) if dx else a
|
||||
|
||||
platform = Sloppy()
|
||||
checks = Checks()
|
||||
clients = opened(platform, checks)
|
||||
resize.edge_cases(platform, checks, clients)
|
||||
assert any("outside the work area" in f or "overlap" in f for f in checks.failures)
|
||||
|
||||
|
||||
def test_a_platform_with_no_pointer_skips_the_edge_cases() -> None:
|
||||
platform = FakePlatform(pointer=False)
|
||||
checks = Checks()
|
||||
clients = opened(platform, checks)
|
||||
base = checks.passed
|
||||
resize.edge_cases(platform, checks, clients)
|
||||
assert (checks.passed, checks.failed) == (base, 0)
|
||||
@@ -1,8 +1,9 @@
|
||||
# Resizing a tile by dragging its edge, in a Plasma session, with a pointer device the test drives
|
||||
# from inside the guest (`komorebi_vm.Pointer`). What only a real KWin and a real pointer can tell is in
|
||||
# `komorebi_vm.platforms.kde.extras.drag_resize`: edges and corners of three tiles are grabbed and
|
||||
# dragged, and what happened is checked against what a user expects. The shared scenarios
|
||||
# (`resize`, `focus_follows_mouse`) run on Windows too and make one drag and the focus changes.
|
||||
# `komorebi_vm.platforms.kde.extras.drag_resize`: that the pointer is put to the pixel. The shared
|
||||
# scenarios run on Windows too: `resize.edge_cases` grabs edges and corners of three tiles and drags
|
||||
# them, and checks what happened against what a user expects; `resize.run` makes one more drag, and
|
||||
# `focus_follows_mouse` the focus changes.
|
||||
from komorebi_vm import Checks, Kde, Pointer
|
||||
from komorebi_vm.core.platform import Client
|
||||
from komorebi_vm.core.scenarios import focus_follows_mouse, resize
|
||||
@@ -53,7 +54,7 @@ args = (platform, checks, clients)
|
||||
run_suite(
|
||||
checks,
|
||||
platform_only("kde", kde_drag_resize.pointer_is_exact, *args),
|
||||
platform_only("kde", kde_drag_resize.edge_cases, *args),
|
||||
shared(resize.edge_cases, *args),
|
||||
shared(resize.run, platform, checks, clients),
|
||||
shared(focus_follows_mouse.run, platform, checks, clients[0], clients[2]),
|
||||
)
|
||||
|
||||
Reference in new issue
Block a user