mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 04:00:06 +02:00
Arrange the desktop's outputs the way the screens are around you
KWin's output positions now follow where the Frametop screens are in the room instead of their numbers: a screen you see to the left of another is to its left in Plasma, so the pointer and dragged windows cross straight to it. Screens one above the other stack, and wrist-pinned screens go last. ft-layout scale works this out from ft-screens' head pose and screen poses, and runs after arranging, capturing, or pinning; ft-screens runs it half a second after a screen is let go. With no head pose (headset off) KWin's order is kept. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
c43418085d
commit
e84ac22313
3 files changed
+102
-8
No files matched your search
+3
-1
@@ -115,6 +115,8 @@ Device rules are saved in `~/.config/frametop-input.json`. `input-settings/insta
|
||||
|
||||
When the desktop starts, its screens arrange themselves around where you're facing. You can move them by hand at any time and put them back with Meta+Shift+R, the Reset Screen Layout menu entry, Arrange now in the app, or a mouse button mapped to Reset desktop screen layout.
|
||||
|
||||
The desktop's own screen arrangement follows where the screens are around you, whatever their numbers: a screen you see to the left of another is to its left in Plasma too, so the pointer and dragged windows cross straight to it. Screens one above the other stack, and screens pinned to a wrist come last. It's updated at startup, after arranging or saving the layout, and half a second after you let go of a screen you moved. With the headset off there's no head pose to go by, and the arrangement stays as it was.
|
||||
|
||||
Frametop Display Settings has three tabs:
|
||||
|
||||
- Screens: add and remove screens, and set each one's resolution (presets from 1080p to 4K, ultrawide, super ultrawide, portrait, or custom), its width in VR (0.5 to 6 m), its scale, whether it's curved, and whether it has the taskbar. Resolution, width, and curve apply at once. Adding or removing a screen takes a desktop restart, which the app offers.
|
||||
@@ -127,7 +129,7 @@ Frametop Display Settings has three tabs:
|
||||
layout/ft-layout apply # arrange every screen
|
||||
layout/ft-layout capture # save the current arrangement and sizes as the layout
|
||||
layout/ft-layout plan # print the arrangement as JSON (no VR needed)
|
||||
layout/ft-layout scale # per-screen scale, positions, and taskbar screen, to KWin
|
||||
layout/ft-layout scale # per-screen scale, positions (as the screens are around you), and taskbar screen, to KWin
|
||||
layout/ft-layout toggle # hide or show all screens
|
||||
display-settings/install.sh # menu entries and the Meta+Shift+R and Meta+Shift+H shortcuts
|
||||
```
|
||||
|
||||
+65
-7
@@ -39,7 +39,8 @@ Usage (on the Frame host; Frametop Display Settings calls it too):
|
||||
wait for the screens, skip if "auto" is off
|
||||
ft-layout capture save the current arrangement as the custom layout
|
||||
ft-layout plan print the arrangement as JSON (no VR needed)
|
||||
ft-layout scale per-screen scale, positions, and primary to KWin
|
||||
ft-layout scale per-screen scale, positions (as the screens are around
|
||||
you), and primary to KWin
|
||||
ft-layout screen-args ft-screens' --screen arguments for the session script
|
||||
ft-layout toggle hide or show all screens (ft-screens)
|
||||
ft-layout pin all|N left|right pin screens to a wrist as they are; unpin all|N
|
||||
@@ -611,6 +612,39 @@ def outputs(env):
|
||||
KSCREEN_ROTATION = {1: "normal", 2: "left", 4: "inverted", 8: "right"} # kscreen-doctor -j "rotation"
|
||||
|
||||
|
||||
def arrangement(count):
|
||||
"""The screens as you see them from where you are: columns left to right, each top to
|
||||
bottom (0-based screen indices), for KWin's output positions. Screens pinned to a
|
||||
wrist come last. None when there's nothing to go by (gamescope, no ft-screens)."""
|
||||
if backend() != "screens":
|
||||
return None
|
||||
try:
|
||||
sock = screens_socket()
|
||||
f = sock.ask("head").split()
|
||||
eye, heading = tuple(map(float, f[1:4])), float(f[4])
|
||||
gets = [parse_get(sock.ask(f"get {i + 1}")) for i in range(count)]
|
||||
except (RuntimeError, ValueError, IndexError):
|
||||
return None
|
||||
seen, pinned = [], []
|
||||
for i, g in enumerate(gets):
|
||||
if g["hand"] != "none":
|
||||
pinned.append(i)
|
||||
continue
|
||||
rel = turn_yaw(tuple(c - e for c, e in zip(g["center"], eye)), -heading)
|
||||
yaw, pitch = yaw_pitch(rel)
|
||||
half = math.degrees(math.atan2(g["metres"] / 2, max(0.1, math.sqrt(dot(rel, rel)))))
|
||||
seen.append({"i": i, "x": -yaw, "pitch": pitch, "half": half}) # x grows to the right
|
||||
seen.sort(key=lambda b: b["x"])
|
||||
columns = []
|
||||
for b in seen:
|
||||
# One above the other: centres closer sideways than half the narrower screen.
|
||||
if columns and abs(b["x"] - columns[-1][-1]["x"]) < min(b["half"], columns[-1][-1]["half"]):
|
||||
columns[-1].append(b)
|
||||
else:
|
||||
columns.append([b])
|
||||
return [[b["i"] for b in sorted(c, key=lambda b: -b["pitch"])] for c in columns] + [[i] for i in pinned]
|
||||
|
||||
|
||||
def apply_scales():
|
||||
"""Per-screen scale and rotation, positions side by side, and the primary screen (the
|
||||
taskbar goes there) to KWin, which keeps them in the session's config."""
|
||||
@@ -633,8 +667,8 @@ def apply_scales():
|
||||
args.append(f"output.{p['id']}.priority.1")
|
||||
if args:
|
||||
subprocess.run(["kscreen-doctor", *args], capture_output=True, env=env, timeout=20)
|
||||
# Side by side in screen order, centred vertically, so the pointer and dragged windows
|
||||
# cross between neighbours.
|
||||
# Laid out as you see the screens around you (arrangement), centred on one line, so
|
||||
# the pointer and dragged windows cross to the screen you see next to this one.
|
||||
outs = outputs(env)
|
||||
# kscreen's "size" is in pixels (already turned for a rotation); positions are in
|
||||
# logical units, the pixels divided by the scale (KWin rounds up).
|
||||
@@ -642,18 +676,38 @@ def apply_scales():
|
||||
math.ceil(o["size"]["height"] / float(o.get("scale", 1)) - 1e-6))
|
||||
for o in outs if o.get("size")]
|
||||
if len(sizes) == len(outs) and outs:
|
||||
tallest, x, moves = max(h for _, h in sizes), 0, []
|
||||
for o, (w, h) in zip(outs, sizes):
|
||||
want = (x, (tallest - h) // 2)
|
||||
columns = arrangement(len(outs))
|
||||
if not columns or sorted(i for c in columns for i in c) != list(range(len(outs))):
|
||||
# Nothing to go by (no head pose with the headset off, say): keep KWin's order.
|
||||
columns = [[i] for i in sorted(range(len(outs)), key=lambda i: (outs[i].get("pos", {}).get("x", 0),
|
||||
outs[i].get("pos", {}).get("y", 0)))]
|
||||
widths = [max(sizes[i][0] for i in c) for c in columns]
|
||||
heights = [sum(sizes[i][1] for i in c) for c in columns]
|
||||
tallest, x, moves = max(heights), 0, []
|
||||
for c, cw, ch in zip(columns, widths, heights):
|
||||
y = (tallest - ch) // 2
|
||||
for i in c:
|
||||
want = (x + (cw - sizes[i][0]) // 2, y)
|
||||
y += sizes[i][1]
|
||||
o = outs[i]
|
||||
if (o.get("pos", {}).get("x"), o.get("pos", {}).get("y")) != want:
|
||||
moves.append(f"output.{o['id']}.position.{want[0]},{want[1]}")
|
||||
x += w
|
||||
x += cw
|
||||
if moves:
|
||||
subprocess.run(["kscreen-doctor", *moves], capture_output=True, env=env, timeout=20)
|
||||
args += moves
|
||||
return args
|
||||
|
||||
|
||||
def kwin_follow():
|
||||
"""KWin's outputs after the screens moved, if the desktop is up."""
|
||||
try:
|
||||
changes = apply_scales()
|
||||
log("kwin: " + (" ".join(changes) if changes else "unchanged"))
|
||||
except RuntimeError as e:
|
||||
log(f"kwin: {e}")
|
||||
|
||||
|
||||
def main(argv):
|
||||
if len(argv) < 2 or argv[1] in ("-h", "--help"):
|
||||
print(__doc__.split("Usage")[1].split("\n", 1)[1])
|
||||
@@ -669,6 +723,7 @@ def main(argv):
|
||||
log(screens_socket().ask("toggle"))
|
||||
elif cmd in ("pin", "unpin") and len(argv) >= 3:
|
||||
log(screens_socket().ask(" ".join(argv[1:])))
|
||||
kwin_follow() # pinned screens go last
|
||||
elif cmd in ("apply", "capture", "scale"):
|
||||
with open(LOCK_PATH, "w") as lock:
|
||||
try:
|
||||
@@ -691,6 +746,8 @@ def main(argv):
|
||||
log(f"visibility: {e}")
|
||||
else:
|
||||
apply(wait)
|
||||
if not wait:
|
||||
kwin_follow()
|
||||
if wait:
|
||||
# KWin keeps these, but new screens or a changed layout need them once.
|
||||
for _ in range(30): # Plasma may still be starting
|
||||
@@ -705,6 +762,7 @@ def main(argv):
|
||||
elif cmd == "capture":
|
||||
for i, s in enumerate(capture()):
|
||||
log(f"screen {i + 1}: {s}")
|
||||
kwin_follow()
|
||||
else:
|
||||
changes = apply_scales()
|
||||
log("kwin: " + (" ".join(changes) if changes else "unchanged"))
|
||||
|
||||
@@ -41,7 +41,12 @@
|
||||
|
||||
#include <openvr.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <linux/input-event-codes.h>
|
||||
#include <limits.h>
|
||||
#include <spawn.h>
|
||||
|
||||
extern char **environ; // for posix_spawn
|
||||
|
||||
#include <algorithm>
|
||||
#include <chrono>
|
||||
@@ -854,6 +859,33 @@ void EndDrag(Screen &s) {
|
||||
ApplyAlpha(s);
|
||||
}
|
||||
|
||||
// KWin's outputs follow where the screens are, so the pointer and dragged windows cross
|
||||
// to the screen you see next to this one: `ft-layout scale` runs once a move has settled.
|
||||
long g_arrangeAt = -1; // g_tick to run it at, -1 = not pending
|
||||
|
||||
void ArrangeDesktopSoon() { g_arrangeAt = g_tick + 45; } // about half a second
|
||||
|
||||
void UpdateArrange() {
|
||||
if (g_arrangeAt < 0 || g_tick < g_arrangeAt) return;
|
||||
g_arrangeAt = -1;
|
||||
char exe[PATH_MAX];
|
||||
if (!realpath("/proc/self/exe", exe)) return;
|
||||
std::string layout(exe); // <repo>/screens/build/ft-screens -> <repo>/layout/ft-layout
|
||||
for (int up = 0; up < 3 && layout.rfind('/') != std::string::npos; ++up) layout.resize(layout.rfind('/'));
|
||||
layout += "/layout/ft-layout";
|
||||
posix_spawn_file_actions_t io;
|
||||
posix_spawn_file_actions_init(&io);
|
||||
posix_spawn_file_actions_addopen(&io, 0, "/dev/null", O_RDONLY, 0);
|
||||
posix_spawn_file_actions_addopen(&io, 1, "/tmp/frametop-layout.log", O_WRONLY | O_CREAT | O_APPEND, 0644);
|
||||
posix_spawn_file_actions_adddup2(&io, 1, 2);
|
||||
char scale[] = "scale";
|
||||
char *argv[] = {layout.data(), scale, nullptr};
|
||||
pid_t pid; // reaped by the compositor's SIGCHLD handler
|
||||
if (posix_spawn(&pid, layout.c_str(), &io, nullptr, argv, environ) != 0)
|
||||
std::printf("can't run %s\n", layout.c_str());
|
||||
posix_spawn_file_actions_destroy(&io);
|
||||
}
|
||||
|
||||
// Let go: pin to the armed wrist, as the screen is now.
|
||||
void FinishDrag(Screen &s, int index) {
|
||||
const bool moved = s.drag == Drag::Move;
|
||||
@@ -861,6 +893,7 @@ void FinishDrag(Screen &s, int index) {
|
||||
EndDrag(s);
|
||||
Mat c, p;
|
||||
if (!moved) return;
|
||||
ArrangeDesktopSoon();
|
||||
if (target != kNone && DevicePose(target, &c) && ScreenPose(s, &p)) {
|
||||
Pin(s, target, Mul(Inverse(c), p));
|
||||
std::printf("screen %d: pinned to the %s controller\n", index + 1, HandName(target));
|
||||
@@ -1228,6 +1261,7 @@ void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
|
||||
}
|
||||
++g_tick;
|
||||
UpdateGame();
|
||||
UpdateArrange();
|
||||
UpdateVisibility();
|
||||
UpdateLasers();
|
||||
UpdateControls();
|
||||
|
||||
Reference in new issue
Block a user