Show floating windows as panels of their own

ft-screens makes a panel for each spare output (frametop.float.N), hidden
until ft-floatd floats a window on it. The panel shows only the window's
rectangle of the buffer at the density of the screen it came from, its
popups and dialogs get small panels over it, pressing its title bar
carries it while KWin's pointer stays put, the corner tab resizes the
window in pixels, and two more buttons close it and put it back on the
desktop. The session adds FLOAT_SLOTS spare outputs to KWin and starts
ft-floatd; ft-layout arranges only the screens' outputs, and the pointer
helper treats the new panels like screens. Not yet tried in the headset.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-09-29 23:26:20 -06:00
1 parent 87a402c68d
commit c2e5e861c8
9 files changed
+480 -75

No files matched your search

+1
View File
@@ -305,6 +305,7 @@ class Daemon:
# The spare's size first (while it's off, so its first frame is right), then its panel, # The spare's size first (while it's off, so its first frame is right), then its panel,
# then turn it on, then the window. # then turn it on, then the window.
self.screens.ask(f"size {slot.index} {slot.size[0]} {slot.size[1]}") self.screens.ask(f"size {slot.index} {slot.size[0]} {slot.size[1]}")
self.screens.ask(f"scale {slot.index} {s:g}") # for pointer positions (KWin's units)
self.set_panel(f, (m, m, w, h), title=round((ev["client"]["y"] - fr["y"]) * s)) self.set_panel(f, (m, m, w, h), title=round((ev["client"]["y"] - fr["y"]) * s))
self.place_panel(f, ev) self.place_panel(f, ev)
kscreen(f"output.{slot.output}.enable", f"output.{slot.output}.scale.{s:g}", kscreen(f"output.{slot.output}.enable", f"output.{slot.output}.scale.{s:g}",
+6 -1
View File
@@ -684,13 +684,18 @@ def nested_env():
def outputs(env): def outputs(env):
"""KWin's outputs, in screen order (WL-0, WL-1, ...).""" """KWin's outputs for the screens, in screen order (WL-0, WL-1, ...)."""
try: try:
data = json.loads(subprocess.run(["kscreen-doctor", "-j"], capture_output=True, text=True, env=env, data = json.loads(subprocess.run(["kscreen-doctor", "-j"], capture_output=True, text=True, env=env,
timeout=10).stdout) timeout=10).stdout)
except (OSError, ValueError, subprocess.TimeoutExpired): except (OSError, ValueError, subprocess.TimeoutExpired):
return [] return []
outs = [o for o in data.get("outputs", []) if o.get("connected")] outs = [o for o in data.get("outputs", []) if o.get("connected")]
if backend() == "screens":
# Outputs after the screens are spares for floating windows (ft-floatd places them).
count = screen_count()
outs = [o for o in outs if not re.fullmatch(r"WL-(\d+)", o.get("name", "")) or
int(o["name"][3:]) < count]
return sorted(outs, key=lambda o: [int(t) if t.isdigit() else t for t in re.split(r"(\d+)", o.get("name", ""))]) return sorted(outs, key=lambda o: [int(t) if t.isdigit() else t for t in re.split(r"(\d+)", o.get("name", ""))])
+5 -3
View File
@@ -371,6 +371,7 @@ private:
if (rest.find("Thumbnail") != std::string::npos || rest.find("Subview") != std::string::npos) continue; if (rest.find("Thumbnail") != std::string::npos || rest.find("Subview") != std::string::npos) continue;
if (key.rfind("system.pointer", 0) == 0 || key.rfind("system.cursor", 0) == 0 || if (key.rfind("system.pointer", 0) == 0 || key.rfind("system.cursor", 0) == 0 ||
key.rfind("frametop.pointer", 0) == 0 || key.rfind("frametop.guide", 0) == 0 || key.rfind("frametop.pointer", 0) == 0 || key.rfind("frametop.guide", 0) == 0 ||
key == "frametop.catcher" || // ft-screens' release catcher: only on a laser mid-drag
key == "system.HeadsetView" || key == "system.toast") key == "system.HeadsetView" || key == "system.toast")
continue; continue;
// The name can hold quotes; it ends at the last "', " (the size and state follow). // The name can hold quotes; it ends at the last "', " (the size and state follow).
@@ -1255,10 +1256,11 @@ int main() {
overlay->GetOverlayTextureSize(h, &tw, &th); overlay->GetOverlayTextureSize(h, &tw, &th);
vr::VROverlayTransformType tt = vr::VROverlayTransform_Invalid; vr::VROverlayTransformType tt = vr::VROverlayTransform_Invalid;
overlay->GetOverlayTransformType(h, &tt); overlay->GetOverlayTransformType(h, &tt);
// ft-screens' panels (frametop.screen.N) are 0x0 and absolute too (a shared // ft-screens' panels (frametop.screen.N, and floating windows with their
// texture), but they're real panels of any size. // popups, frametop.float.N...) are 0x0 and absolute too (a shared texture),
// but they're real panels of any size.
sceneGraph[key] = (tw == 0 || th == 0) && tt == vr::VROverlayTransform_Absolute && sceneGraph[key] = (tw == 0 || th == 0) && tt == vr::VROverlayTransform_Absolute &&
key.rfind("frametop.screen.", 0) != 0; key.rfind("frametop.screen.", 0) != 0 && key.rfind("frametop.float.", 0) != 0;
} }
ours = vr::k_unTrackedDeviceIndexInvalid; ours = vr::k_unTrackedDeviceIndexInvalid;
for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) { for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) {
+24 -10
View File
@@ -13,13 +13,15 @@
// scale first ("scale <screen> <s>", from ft-layout). // scale first ("scale <screen> <s>", from ft-layout).
// //
// Usage: ft-screens [--socket NAME] [--control NAME] [--no-vr] [--screen WxH@METRES]... // Usage: ft-screens [--socket NAME] [--control NAME] [--no-vr] [--screen WxH@METRES]...
// [-- COMMAND ARGS...] // [--spares N] [-- COMMAND ARGS...]
// --socket Wayland socket name in $XDG_RUNTIME_DIR (default ft-screens-0) // --socket Wayland socket name in $XDG_RUNTIME_DIR (default ft-screens-0)
// --control the control socket's abstract name (default ft_screens) // --control the control socket's abstract name (default ft_screens)
// --no-vr run without SteamVR, for tests next to the running desktop: no panels, no // --no-vr run without SteamVR, for tests next to the running desktop: no panels, no
// input, and nothing sent to the input relay. Commands still work, and // input, and nothing sent to the input relay. Commands still work, and
// "toplevels" shows what KWin opened. // "toplevels" shows what KWin opened.
// --screen one per screen, in KWin's order (default: 3440x1440@2.4) // --screen one per screen, in KWin's order (default: 3440x1440@2.4)
// --spares KWin's outputs after the screens: spares for floating windows (ft-floatd
// turns them on and sizes them; see docs/floating-windows.md)
// COMMAND run with WAYLAND_DISPLAY set to our socket (e.g. the Frametop session) // COMMAND run with WAYLAND_DISPLAY set to our socket (e.g. the Frametop session)
// Runs in the dev container (wlroots 0.20); KWin connects from the host. // Runs in the dev container (wlroots 0.20); KWin connects from the host.
#define _GNU_SOURCE #define _GNU_SOURCE
@@ -57,7 +59,7 @@
#include "vr.h" #include "vr.h"
#define MAX_SCREENS 8 #define MAX_SCREENS 24 // screens and spare outputs
struct config { struct config {
int width, height; int width, height;
@@ -97,7 +99,8 @@ struct server {
struct screen *screens[MAX_SCREENS]; struct screen *screens[MAX_SCREENS];
struct config config[MAX_SCREENS]; struct config config[MAX_SCREENS];
double scale[MAX_SCREENS]; // KWin's scale for each screen (panel pixels per logical unit) double scale[MAX_SCREENS]; // KWin's scale for each screen (panel pixels per logical unit)
int n_config, n_screens; int n_config, n_screens; // n_config: the screens; the outputs after them are spares
int spares;
struct wl_list buffers; // tracked_buffer struct wl_list buffers; // tracked_buffer
struct wl_event_source *tick; struct wl_event_source *tick;
struct screen *pointer_focus; struct screen *pointer_focus;
@@ -145,7 +148,8 @@ static void screen_commit(struct wl_listener *l, void *data) {
struct wlr_xdg_surface *xdg = sc->toplevel->base; struct wlr_xdg_surface *xdg = sc->toplevel->base;
if (xdg->initial_commit) { if (xdg->initial_commit) {
// First commit: tell KWin the size of this screen. // First commit: tell KWin the size of this screen.
const struct config *c = &sc->server->config[sc->index < sc->server->n_config ? sc->index : 0]; const struct config spare = {640, 480, 0.5}; // until ft-floatd sizes it
const struct config *c = sc->index < sc->server->n_config ? &sc->server->config[sc->index] : &spare;
wlr_xdg_toplevel_set_size(sc->toplevel, c->width, c->height); wlr_xdg_toplevel_set_size(sc->toplevel, c->width, c->height);
wlr_xdg_toplevel_set_activated(sc->toplevel, true); wlr_xdg_toplevel_set_activated(sc->toplevel, true);
if (sc->decoration) if (sc->decoration)
@@ -205,7 +209,9 @@ static void screen_destroy(struct wl_listener *l, void *data) {
// after it while that output is off. // after it while that output is off.
static void screen_title(struct wl_listener *l, void *data) { static void screen_title(struct wl_listener *l, void *data) {
struct screen *sc = wl_container_of(l, sc, set_title); struct screen *sc = wl_container_of(l, sc, set_title);
wlr_log(WLR_INFO, "screen %d: \"%s\"", sc->index + 1, sc->toplevel->title ? sc->toplevel->title : ""); const char *title = sc->toplevel->title ? sc->toplevel->title : "";
wlr_log(WLR_INFO, "screen %d: \"%s\"", sc->index + 1, title);
if (sc->index >= sc->server->n_config) ft_vr_float_output(sc->index, !strstr(title, "Output disabled"));
} }
static void new_toplevel(struct wl_listener *l, void *data) { static void new_toplevel(struct wl_listener *l, void *data) {
@@ -222,9 +228,15 @@ static void new_toplevel(struct wl_listener *l, void *data) {
sc->index = index; sc->index = index;
sc->toplevel = toplevel; sc->toplevel = toplevel;
s->screens[index] = sc; s->screens[index] = sc;
const struct config *c = &s->config[index < s->n_config ? index : 0]; if (index >= s->n_config) {
wlr_log(WLR_INFO, "screen %d: KWin window, %dx%d, %.2f m wide", index + 1, c->width, c->height, c->metres); wlr_log(WLR_INFO, "screen %d: KWin window, a spare output (floating window %d)", index + 1,
ft_vr_screen_create(index, c->metres, s->n_config > index + 1 ? s->n_config : index + 1); index - s->n_config + 1);
ft_vr_float_create(index, index - s->n_config + 1);
} else {
const struct config *c = &s->config[index];
wlr_log(WLR_INFO, "screen %d: KWin window, %dx%d, %.2f m wide", index + 1, c->width, c->height, c->metres);
ft_vr_screen_create(index, c->metres, s->n_config);
}
sc->commit.notify = screen_commit; sc->commit.notify = screen_commit;
wl_signal_add(&toplevel->base->surface->events.commit, &sc->commit); wl_signal_add(&toplevel->base->surface->events.commit, &sc->commit);
sc->destroy.notify = screen_destroy; sc->destroy.notify = screen_destroy;
@@ -537,6 +549,8 @@ int main(int argc, char **argv) {
socket_name = argv[++i]; socket_name = argv[++i];
} else if (strcmp(argv[i], "--control") == 0 && i + 1 < argc) { } else if (strcmp(argv[i], "--control") == 0 && i + 1 < argc) {
control_name = argv[++i]; control_name = argv[++i];
} else if (strcmp(argv[i], "--spares") == 0 && i + 1 < argc) {
s.spares = atoi(argv[++i]);
} else if (strcmp(argv[i], "--no-vr") == 0) { } else if (strcmp(argv[i], "--no-vr") == 0) {
s.vr = false; s.vr = false;
} else if (strcmp(argv[i], "--screen") == 0 && i + 1 < argc && s.n_config < MAX_SCREENS) { } else if (strcmp(argv[i], "--screen") == 0 && i + 1 < argc && s.n_config < MAX_SCREENS) {
@@ -554,7 +568,7 @@ int main(int argc, char **argv) {
} else { } else {
fprintf(stderr, fprintf(stderr,
"usage: %s [--socket NAME] [--control NAME] [--no-vr] [--screen WxH@METRES]... " "usage: %s [--socket NAME] [--control NAME] [--no-vr] [--screen WxH@METRES]... "
"[-- COMMAND ARGS...]\n", "[--spares N] [-- COMMAND ARGS...]\n",
argv[0]); argv[0]);
return 2; return 2;
} }
@@ -599,7 +613,7 @@ int main(int argc, char **argv) {
} }
char path[256]; char path[256];
snprintf(path, sizeof path, "%s/%s", getenv("XDG_RUNTIME_DIR") ? getenv("XDG_RUNTIME_DIR") : "/tmp", socket_name); snprintf(path, sizeof path, "%s/%s", getenv("XDG_RUNTIME_DIR") ? getenv("XDG_RUNTIME_DIR") : "/tmp", socket_name);
wlr_log(WLR_INFO, "listening on %s; %d screen(s) configured", path, s.n_config); wlr_log(WLR_INFO, "listening on %s; %d screen(s) configured, %d spare(s)", path, s.n_config, s.spares);
const int control = open_control_socket(control_name); const int control = open_control_socket(control_name);
if (control < 0) return 1; if (control < 0) return 1;
+2 -2
View File
@@ -6,7 +6,7 @@
# or on the Frame. Build ft-screens first (screens/build.sh). # or on the Frame. Build ft-screens first (screens/build.sh).
# #
# headless.sh start [SCREENS] [SPARES] (default 2 screens, 1920x1080 and 1080x1920, and # headless.sh start [SCREENS] [SPARES] (default 2 screens, 1920x1080 and 1080x1920, and
# 3 spare 800x600 outputs, disabled once KWin is up) # 3 spare outputs, disabled once KWin is up)
# headless.sh stop # headless.sh stop
# headless.sh ask '<ft-screens command>' e.g. toplevels, "input 1 down 400 20" # headless.sh ask '<ft-screens command>' e.g. toplevels, "input 1 down 400 20"
# headless.sh kd <kscreen-doctor args> e.g. -o, output.WL-2.enable # headless.sh kd <kscreen-doctor args> e.g. -o, output.WL-2.enable
@@ -89,7 +89,7 @@ case "${1:-}" in
args=(--screen 1920x1080@1.6 --screen 1080x1920@0.9) args=(--screen 1920x1080@1.6 --screen 1080x1920@0.9)
for ((i = 2; i < screens; i++)); do args+=(--screen 1920x1080@1.6); done for ((i = 2; i < screens; i++)); do args+=(--screen 1920x1080@1.6); done
args=("${args[@]:0:$((screens * 2))}") args=("${args[@]:0:$((screens * 2))}")
for ((i = 0; i < spares; i++)); do args+=(--screen 800x600); done args+=(--spares "$spares")
cd "$REPO_ROOT" cd "$REPO_ROOT"
nohup "$HOME/.local/bin/distrobox" enter "$FRAME_BOX" -- env XDG_RUNTIME_DIR="$rt" \ nohup "$HOME/.local/bin/distrobox" enter "$FRAME_BOX" -- env XDG_RUNTIME_DIR="$rt" \
./screens/build/ft-screens --no-vr --socket ft-test-0 --control ft_screens_test "${args[@]}" \ ./screens/build/ft-screens --no-vr --socket ft-test-0 --control ft_screens_test "${args[@]}" \
+408 -55
View File
@@ -42,6 +42,15 @@
// or stay visible over it; the hotkey still shows them. // or stay visible over it; the hotkey still shows them.
// - the catcher: a button pressed on a screen is released in KWin even when the laser // - the catcher: a button pressed on a screen is released in KWin even when the laser
// lets go between panels (UpdateCatcher). // lets go between panels (UpdateCatcher).
// - floating windows (docs/floating-windows.md): KWin's spare outputs, after the screens,
// are panels too, for one window each. ft-floatd sizes the output to the window plus a
// margin and tells us the window's rectangle ("float"): the panel shows only that crop of
// the buffer (SetOverlayTextureBounds), at the density of the screen it came from, and
// each popup or dialog gets a small panel of its own over it, cut from the same buffer
// ("sub"). Pressing its title bar carries the panel like the bar does, while KWin's
// pointer stays put, so the window doesn't move on its output. The corner tab resizes the
// window (in pixels, at the same density) instead of scaling the panel, and two more
// buttons close it and put it back on the desktop (both through ft-floatd).
// OpenVR has no overlay-relative transforms here (openvr v2.15.6), so the bar, button, // OpenVR has no overlay-relative transforms here (openvr v2.15.6), so the bar, button,
// and handle are placed whenever their screen moves. // and handle are placed whenever their screen moves.
#include "vr.h" #include "vr.h"
@@ -49,6 +58,8 @@
#include <openvr.h> #include <openvr.h>
#include <fcntl.h> #include <fcntl.h>
#include <sys/socket.h>
#include <sys/un.h>
#include <linux/input-event-codes.h> #include <linux/input-event-codes.h>
#include <limits.h> #include <limits.h>
#include <spawn.h> #include <spawn.h>
@@ -59,6 +70,7 @@ extern char **environ; // for posix_spawn
#include <chrono> #include <chrono>
#include <array> #include <array>
#include <cmath> #include <cmath>
#include <cstddef>
#include <cstdio> #include <cstdio>
#include <cstdlib> #include <cstdlib>
#include <cstring> #include <cstring>
@@ -209,10 +221,17 @@ long g_tick = 0; // ft_vr_poll calls
bool g_vr = false; // connected to SteamVR (ft-screens --no-vr runs without it) bool g_vr = false; // connected to SteamVR (ft-screens --no-vr runs without it)
constexpr vr::TrackedDeviceIndex_t kNone = vr::k_unTrackedDeviceIndexInvalid; constexpr vr::TrackedDeviceIndex_t kNone = vr::k_unTrackedDeviceIndexInvalid;
// A popup or dialog of a floating window: a small panel over it, cut from the same buffer.
struct Sub {
vr::VROverlayHandle_t overlay = vr::k_ulOverlayHandleInvalid;
int x = 0, y = 0, w = 0, h = 0; // in the output's buffer, pixels
};
struct Screen { struct Screen {
vr::VROverlayHandle_t overlay = vr::k_ulOverlayHandleInvalid, bar = vr::k_ulOverlayHandleInvalid, vr::VROverlayHandle_t overlay = vr::k_ulOverlayHandleInvalid, bar = vr::k_ulOverlayHandleInvalid,
handle = vr::k_ulOverlayHandleInvalid, curveButton = vr::k_ulOverlayHandleInvalid, handle = vr::k_ulOverlayHandleInvalid, curveButton = vr::k_ulOverlayHandleInvalid,
rollButton = vr::k_ulOverlayHandleInvalid; rollButton = vr::k_ulOverlayHandleInvalid, dockButton = vr::k_ulOverlayHandleInvalid,
closeButton = vr::k_ulOverlayHandleInvalid; // the last two: floating windows
int width = 0, height = 0; // current buffer size (mouse scale) int width = 0, height = 0; // current buffer size (mouse scale)
double metres = 1; double metres = 1;
double curve = 0; // cylinder radius in metres; 0 = flat double curve = 0; // cylinder radius in metres; 0 = flat
@@ -228,7 +247,7 @@ struct Screen {
double grabX = 0, grabY = 0; // resize: the grab point relative to the corner double grabX = 0, grabY = 0; // resize: the grab point relative to the corner
Mat rollFrom = Identity(); // roll: the pose at the press (pinRel when pinned) Mat rollFrom = Identity(); // roll: the pose at the press (pinRel when pinned)
double rollAngle = 0; // roll: the laser's angle around the centre then double rollAngle = 0; // roll: the laser's angle around the centre then
bool hover[4] = {}; // a laser is on the bar, curve, roll, resize control bool hover[6] = {}; // a laser is on the bar, curve, roll, resize, dock, close control
bool lasers = true; // MakeOverlaysInteractiveIfVisible is set bool lasers = true; // MakeOverlaysInteractiveIfVisible is set
float controls = 0; // the controls' fade, 0 (hidden) .. 1 float controls = 0; // the controls' fade, 0 (hidden) .. 1
bool controlsUp = false; // the controls' overlays are shown bool controlsUp = false; // the controls' overlays are shown
@@ -238,9 +257,35 @@ struct Screen {
bool barLit = false; bool barLit = false;
double chrome = 0.3; // the bar's width; the other controls follow it (ChromeSize) double chrome = 0.3; // the bar's width; the other controls follow it (ChromeSize)
double grip = 0.04; // the corner tab's and the round buttons' size double grip = 0.04; // the corner tab's and the round buttons' size
double heightMetres() const { return width > 0 ? metres * height / width : metres * 9 / 16; } // A floating window's panel (see the top): the window's rectangle in the buffer, its
std::array<vr::VROverlayHandle_t, 4> Controls() const { return {bar, curveButton, rollButton, handle}; } // title bar's height there, and the density.
std::array<vr::VROverlayHandle_t, 5> All() const { return {overlay, bar, curveButton, rollButton, handle}; } bool floating = false; // a spare output's panel
bool floatOn = false; // ft-floatd has a window on it ("float" .. "unfloat")
bool outputOn = false; // KWin has the spare output turned on
bool minimized = false;
int cropX = 0, cropY = 0, cropW = 0, cropH = 0;
int titleH = 0;
double mpp = 0; // metres per buffer pixel
bool titleCarry = false; // carried by its title bar: KWin's pointer stays at carryX, carryY
double carryX = 0, carryY = 0;
long resizeSent = 0; // g_tick of the last resize request (they're throttled)
int resizeW = 0, resizeH = 0; // ...and its size
std::map<int, Sub> subs;
double heightMetres() const {
if (floating && cropW > 0) return metres * cropH / cropW;
return width > 0 ? metres * height / width : metres * 9 / 16;
}
// Buffer pixels from OpenVR's mouse position on the panel (its origin is bottom left).
void ToBuffer(double mx, double my, double *x, double *y) const {
if (floating && cropW > 0) *x = cropX + mx, *y = cropY + cropH - my;
else *x = mx, *y = height - my;
}
std::array<vr::VROverlayHandle_t, 6> Controls() const {
return {bar, curveButton, rollButton, handle, dockButton, closeButton};
}
std::array<vr::VROverlayHandle_t, 7> All() const {
return {overlay, bar, curveButton, rollButton, handle, dockButton, closeButton};
}
}; };
std::map<int, Screen> g_screens; std::map<int, Screen> g_screens;
std::map<const void *, vr::SharedTextureHandle_t> g_imports; std::map<const void *, vr::SharedTextureHandle_t> g_imports;
@@ -348,6 +393,29 @@ std::vector<uint8_t> RollTexture(int n) {
DiscRim); DiscRim);
} }
std::vector<uint8_t> CloseTexture(int n) {
// A cross: "close this window".
return ControlTexture(
n, InDisc,
[](double u, double v) {
return std::max(std::fabs(u), std::fabs(v)) < 0.42 &&
(std::fabs(u - v) < 0.12 || std::fabs(u + v) < 0.12);
},
DiscRim);
}
std::vector<uint8_t> DockTexture(int n) {
// An arrow down onto a line: "back to the desktop".
return ControlTexture(
n, InDisc,
[](double u, double v) {
if (std::fabs(u) < 0.5 && v > -0.52 && v < -0.38) return true; // the line
if (std::fabs(u) < 0.08 && v > -0.1 && v < 0.5) return true; // the shaft
return v >= -0.3 && v <= -0.05 && std::fabs(u) <= (v + 0.3) * 1.2; // the head, point down
},
DiscRim);
}
vr::VROverlayHandle_t MakeChrome(const char *key, const char *name, const std::vector<uint8_t> &px, int w, int h) { vr::VROverlayHandle_t MakeChrome(const char *key, const char *name, const std::vector<uint8_t> &px, int w, int h) {
vr::VROverlayHandle_t o = vr::k_ulOverlayHandleInvalid; vr::VROverlayHandle_t o = vr::k_ulOverlayHandleInvalid;
if (vr::VROverlay()->CreateOverlay(key, name, &o) != vr::VROverlayError_None) return o; if (vr::VROverlay()->CreateOverlay(key, name, &o) != vr::VROverlayError_None) return o;
@@ -500,13 +568,37 @@ Mat BarOffset(const Screen &s) { return OnSurface(s, 0, BarY(s), 0.003); }
// Put the bar, the curve button, and the corner tab under the screen (same parent: the // Put the bar, the curve button, and the corner tab under the screen (same parent: the
// room or the controller), sized for the screen and its distance, and on its surface. // room or the controller), sized for the screen and its distance, and on its surface.
// Where each control sits, relative to the screen: bar, curve, roll, resize tab. // Where each control sits, relative to the screen: bar, curve, roll, resize tab, and a
std::array<Mat, 4> ControlOffsets(const Screen &s) { // floating window's dock and close buttons (left of the bar).
std::array<Mat, 6> ControlOffsets(const Screen &s) {
const double h = s.heightMetres(), bar = s.chrome, button = s.grip, gap = bar * 0.06; const double h = s.heightMetres(), bar = s.chrome, button = s.grip, gap = bar * 0.06;
return {BarOffset(s), OnSurface(s, bar / 2 + gap + button / 2, BarY(s), 0.003), return {BarOffset(s), OnSurface(s, bar / 2 + gap + button / 2, BarY(s), 0.003),
OnSurface(s, bar / 2 + gap * 2 + button * 1.5, BarY(s), 0.003), OnSurface(s, bar / 2 + gap * 2 + button * 1.5, BarY(s), 0.003),
// The tab's top left corner is the screen's bottom right corner. // The tab's top left corner is the screen's bottom right corner.
OnSurface(s, s.metres / 2 + s.grip / 2, -(h / 2 + s.grip / 2), 0.003)}; OnSurface(s, s.metres / 2 + s.grip / 2, -(h / 2 + s.grip / 2), 0.003),
OnSurface(s, -(bar / 2 + gap + button / 2), BarY(s), 0.003),
OnSurface(s, -(bar / 2 + gap * 2 + button * 1.5), BarY(s), 0.003)};
}
// A floating window's popups and dialogs, a few millimetres in front of it, where they are
// in the buffer relative to the window.
void PlaceSubs(const Screen &s) {
if (s.subs.empty() || s.cropW <= 0) return;
Mat p;
if (s.pinned == kNone && !ScreenPose(s, &p)) return;
for (const auto &[k, sub] : s.subs) {
const double u = (sub.x + sub.w / 2.0 - (s.cropX + s.cropW / 2.0)) * s.mpp;
const double v = -(sub.y + sub.h / 2.0 - (s.cropY + s.cropH / 2.0)) * s.mpp;
const Mat off = OnSurface(s, u, v, 0.005);
vr::VROverlay()->SetOverlayWidthInMeters(sub.overlay, float(std::max(0.01, sub.w * s.mpp)));
if (s.pinned != kNone) {
const Mat m = Mul(s.pinRel, off);
vr::VROverlay()->SetOverlayTransformTrackedDeviceRelative(sub.overlay, s.pinned, &m);
} else {
const Mat m = Mul(p, off);
vr::VROverlay()->SetOverlayTransformAbsolute(sub.overlay, vr::TrackingUniverseStanding, &m);
}
}
} }
void PlaceChrome(Screen &s) { void PlaceChrome(Screen &s) {
@@ -517,12 +609,19 @@ void PlaceChrome(Screen &s) {
vr::VROverlay()->SetOverlayWidthInMeters(s.curveButton, float(button)); vr::VROverlay()->SetOverlayWidthInMeters(s.curveButton, float(button));
vr::VROverlay()->SetOverlayWidthInMeters(s.rollButton, float(button)); vr::VROverlay()->SetOverlayWidthInMeters(s.rollButton, float(button));
vr::VROverlay()->SetOverlayWidthInMeters(s.handle, float(s.grip)); vr::VROverlay()->SetOverlayWidthInMeters(s.handle, float(s.grip));
if (s.floating) {
vr::VROverlay()->SetOverlayWidthInMeters(s.dockButton, float(button));
vr::VROverlay()->SetOverlayWidthInMeters(s.closeButton, float(button));
}
// Curved, the bar bends with the screen's bottom edge. // Curved, the bar bends with the screen's bottom edge.
vr::VROverlay()->SetOverlayCurvature(s.bar, s.curve > 0 ? float(std::min(1.0, bar / (2 * M_PI * s.curve))) : 0.f); vr::VROverlay()->SetOverlayCurvature(s.bar, s.curve > 0 ? float(std::min(1.0, bar / (2 * M_PI * s.curve))) : 0.f);
const std::pair<vr::VROverlayHandle_t, Mat> parts[] = { const std::pair<vr::VROverlayHandle_t, Mat> parts[] = {
{s.bar, offsets[0]}, {s.curveButton, offsets[1]}, {s.rollButton, offsets[2]}, {s.handle, offsets[3]}}; {s.bar, offsets[0]}, {s.curveButton, offsets[1]}, {s.rollButton, offsets[2]},
{s.handle, offsets[3]}, {s.dockButton, offsets[4]}, {s.closeButton, offsets[5]}};
PlaceSubs(s);
if (s.pinned != kNone) { if (s.pinned != kNone) {
for (const auto &[o, off] : parts) { for (const auto &[o, off] : parts) {
if (o == vr::k_ulOverlayHandleInvalid) continue;
const Mat m = Mul(s.pinRel, off); const Mat m = Mul(s.pinRel, off);
vr::VROverlay()->SetOverlayTransformTrackedDeviceRelative(o, s.pinned, &m); vr::VROverlay()->SetOverlayTransformTrackedDeviceRelative(o, s.pinned, &m);
} }
@@ -531,6 +630,7 @@ void PlaceChrome(Screen &s) {
Mat p; Mat p;
if (!ScreenPose(s, &p)) return; if (!ScreenPose(s, &p)) return;
for (const auto &[o, off] : parts) { for (const auto &[o, off] : parts) {
if (o == vr::k_ulOverlayHandleInvalid) continue;
const Mat m = Mul(p, off); const Mat m = Mul(p, off);
vr::VROverlay()->SetOverlayTransformAbsolute(o, vr::TrackingUniverseStanding, &m); vr::VROverlay()->SetOverlayTransformAbsolute(o, vr::TrackingUniverseStanding, &m);
} }
@@ -637,11 +737,13 @@ bool ModeVisible() {
// it's being dragged. // it's being dragged.
void ApplyAlpha(const Screen &s) { void ApplyAlpha(const Screen &s) {
vr::VROverlay()->SetOverlayAlpha(s.overlay, s.alpha); vr::VROverlay()->SetOverlayAlpha(s.overlay, s.alpha);
const bool active[4] = {s.hover[0] || s.drag == Drag::Move, s.hover[1], s.hover[2] || s.drag == Drag::Roll, for (const auto &[k, sub] : s.subs) vr::VROverlay()->SetOverlayAlpha(sub.overlay, s.alpha);
s.hover[3] || s.drag == Drag::Resize}; const bool active[6] = {s.hover[0] || s.drag == Drag::Move, s.hover[1], s.hover[2] || s.drag == Drag::Roll,
s.hover[3] || s.drag == Drag::Resize, s.hover[4], s.hover[5]};
const auto controls = s.Controls(); const auto controls = s.Controls();
for (int k = 0; k < 4; ++k) for (int k = 0; k < 6; ++k)
vr::VROverlay()->SetOverlayAlpha(controls[k], s.alpha * s.controls * (active[k] ? 1.f : kChromeIdle)); if (controls[k] != vr::k_ulOverlayHandleInvalid)
vr::VROverlay()->SetOverlayAlpha(controls[k], s.alpha * s.controls * (active[k] ? 1.f : kChromeIdle));
} }
void SetVisible(Screen &s, bool visible, float alpha) { void SetVisible(Screen &s, bool visible, float alpha) {
@@ -653,11 +755,14 @@ void SetVisible(Screen &s, bool visible, float alpha) {
s.visible = visible; s.visible = visible;
if (visible) { if (visible) {
vr::VROverlay()->ShowOverlay(s.overlay); vr::VROverlay()->ShowOverlay(s.overlay);
for (const auto &[k, sub] : s.subs) vr::VROverlay()->ShowOverlay(sub.overlay);
return; return;
} }
// Hidden: the controls go at once (UpdateControls brings them back). // Hidden: the controls go at once (UpdateControls brings them back).
vr::VROverlay()->HideOverlay(s.overlay); vr::VROverlay()->HideOverlay(s.overlay);
for (auto o : s.Controls()) vr::VROverlay()->HideOverlay(o); for (const auto &[k, sub] : s.subs) vr::VROverlay()->HideOverlay(sub.overlay);
for (auto o : s.Controls())
if (o != vr::k_ulOverlayHandleInvalid) vr::VROverlay()->HideOverlay(o);
s.controls = 0, s.controlsUp = false; s.controls = 0, s.controlsUp = false;
} }
@@ -667,6 +772,9 @@ void UpdateVisibility() {
const bool haveHead = DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head); const bool haveHead = DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head);
for (auto &[i, s] : g_screens) { for (auto &[i, s] : g_screens) {
bool visible = s.shown && (shared || s.drag != Drag::None); bool visible = s.shown && (shared || s.drag != Drag::None);
// A floating window's panel: while a window floats on it, its output is on, and the
// window isn't minimized (and once it has a crop).
if (s.floating) visible = visible && s.floatOn && s.outputOn && !s.minimized && s.cropW > 0;
float alpha = 1; float alpha = 1;
Mat p; Mat p;
if (visible && s.pinned != kNone && s.pinned != vr::k_unTrackedDeviceIndex_Hmd && s.drag == Drag::None && if (visible && s.pinned != kNone && s.pinned != vr::k_unTrackedDeviceIndex_Hmd && s.drag == Drag::None &&
@@ -712,7 +820,9 @@ void UpdateControls() {
const auto offsets = ControlOffsets(s); const auto offsets = ControlOffsets(s);
for (double f : {-0.5, -0.25, 0.0, 0.25, 0.5}) for (double f : {-0.5, -0.25, 0.0, 0.25, 0.5})
spots.push_back(Mul(p, Mul(offsets[0], Translation(f * s.chrome, 0, 0)))); spots.push_back(Mul(p, Mul(offsets[0], Translation(f * s.chrome, 0, 0))));
for (int k = 1; k < 4; ++k) spots.push_back(Mul(p, offsets[k])); const auto controls = s.Controls();
for (int k = 1; k < 6; ++k)
if (controls[k] != vr::k_ulOverlayHandleInvalid) spots.push_back(Mul(p, offsets[k]));
const double reach = std::max(s.grip * 1.5, s.chrome * 0.12); const double reach = std::max(s.grip * 1.5, s.chrome * 0.12);
for (const Mat &d : lasers) { for (const Mat &d : lasers) {
const double o[3] = {d.m[0][3], d.m[1][3], d.m[2][3]}, dir[3] = {-d.m[0][2], -d.m[1][2], -d.m[2][2]}; const double o[3] = {d.m[0][3], d.m[1][3], d.m[2][3]}, dir[3] = {-d.m[0][2], -d.m[1][2], -d.m[2][2]};
@@ -730,13 +840,14 @@ void UpdateControls() {
} }
} }
} }
const bool inUse = s.drag != Drag::None || s.hover[0] || s.hover[1] || s.hover[2] || s.hover[3]; const bool inUse = s.drag != Drag::None || std::any_of(std::begin(s.hover), std::end(s.hover), [](bool h) { return h; });
const bool want = s.visible && (inUse || g_tick < s.nearUntil); const bool want = s.visible && (inUse || g_tick < s.nearUntil);
// The controls stay shown while their screen is, just fully transparent when not // The controls stay shown while their screen is, just fully transparent when not
// wanted: SteamVR's laser still hits them, and the hover event brings them in, for // wanted: SteamVR's laser still hits them, and the hover event brings them in, for
// any device's laser, whatever its shape. // any device's laser, whatever its shape.
if (s.visible && !s.controlsUp) { if (s.visible && !s.controlsUp) {
for (auto o : s.Controls()) vr::VROverlay()->ShowOverlay(o); for (auto o : s.Controls())
if (o != vr::k_ulOverlayHandleInvalid) vr::VROverlay()->ShowOverlay(o);
s.controlsUp = true; s.controlsUp = true;
ApplyAlpha(s); ApplyAlpha(s);
} }
@@ -748,6 +859,19 @@ void UpdateControls() {
// ---------------------------------------------------------------- moving, resizing, pinning // ---------------------------------------------------------------- moving, resizing, pinning
// To ft-floatd (@frametop_float), for floating windows: dock, close, resize. From an unbound
// socket, so its replies go nowhere.
void SendFloat(const std::string &msg) {
static const int fd = socket(AF_UNIX, SOCK_DGRAM | SOCK_CLOEXEC | SOCK_NONBLOCK, 0);
sockaddr_un addr{};
addr.sun_family = AF_UNIX;
const char name[] = "frametop_float";
std::memcpy(addr.sun_path + 1, name, sizeof name - 1);
sendto(fd, msg.data(), msg.size(), MSG_DONTWAIT, reinterpret_cast<sockaddr *>(&addr),
socklen_t(offsetof(sockaddr_un, sun_path) + 1 + sizeof name - 1));
std::printf("to ft-floatd: %s\n", msg.c_str());
}
// Where a device's ray meets the screen's plane, in the screen's x (right) and y (up), // Where a device's ray meets the screen's plane, in the screen's x (right) and y (up),
// metres from its centre. // metres from its centre.
bool RayOnPlane(const Mat &p, const Mat &d, double *x, double *y) { bool RayOnPlane(const Mat &p, const Mat &d, double *x, double *y) {
@@ -905,7 +1029,7 @@ void FinishDrag(Screen &s, int index) {
EndDrag(s); EndDrag(s);
Mat c, p; Mat c, p;
if (!moved) return; if (!moved) return;
ArrangeDesktopSoon(); if (!s.floating) ArrangeDesktopSoon();
if (target != kNone && DevicePose(target, &c) && ScreenPose(s, &p)) { if (target != kNone && DevicePose(target, &c) && ScreenPose(s, &p)) {
Pin(s, target, Mul(Inverse(c), p)); Pin(s, target, Mul(Inverse(c), p));
if (target == vr::k_unTrackedDeviceIndex_Hmd) std::printf("screen %d: pinned to the head\n", index + 1); if (target == vr::k_unTrackedDeviceIndex_Hmd) std::printf("screen %d: pinned to the head\n", index + 1);
@@ -992,6 +1116,19 @@ void UpdateDrag(Screen &s, int index) {
double hx, hy; double hx, hy;
Mat l; Mat l;
if (!LaserPose(s.dragDevice, &l) || !RayOnScreen(s, l, &hx, &hy)) return; if (!LaserPose(s.dragDevice, &l) || !RayOnScreen(s, l, &hx, &hy)) return;
if (s.floating) {
// A floating window: the corner goes where the laser is, in both directions, and the
// window gets that many pixels at the same density (ft-floatd resizes it, and the
// new crop comes back as "float", with the top left corner kept where it is).
if (s.mpp <= 0 || g_tick - s.resizeSent < 4) return; // about 20 a second
const double left = -s.metres / 2, top = s.heightMetres() / 2;
const int w = std::max(320, int(std::lround((hx - s.grabX - left) / s.mpp)));
const int h = std::max(200, int(std::lround((top - (hy - s.grabY)) / s.mpp)));
if (w == s.resizeW && h == s.resizeH) return;
s.resizeW = w, s.resizeH = h, s.resizeSent = g_tick;
SendFloat("resize " + std::to_string(index + 1) + " " + std::to_string(w) + " " + std::to_string(h));
return;
}
const double a = s.width > 0 ? double(s.height) / s.width : 9.0 / 16; const double a = s.width > 0 ? double(s.height) / s.width : 9.0 / 16;
const double cx = hx - s.grabX, cy = hy - s.grabY; // where the corner should be const double cx = hx - s.grabX, cy = hy - s.grabY; // where the corner should be
SetWidth(s, 2 * (cx - a * cy) / (1 + a * a)); SetWidth(s, 2 * (cx - a * cy) / (1 + a * a));
@@ -1011,6 +1148,92 @@ void Push(Screen &s, double notches) {
s.dragRel = Mul(Inverse(d), p); s.dragRel = Mul(Inverse(d), p);
} }
// ---------------------------------------------------------------- floating windows
// Show the window's rectangle of the buffer, and map the mouse to it (see ToBuffer).
// Texture bounds are fractions of the buffer, v from the top.
void CropOverlay(vr::VROverlayHandle_t o, const Screen &s, int x, int y, int w, int h) {
if (s.width <= 0 || s.height <= 0 || w <= 0 || h <= 0) return;
vr::VRTextureBounds_t b = {float(x) / s.width, float(y) / s.height, float(x + w) / s.width,
float(y + h) / s.height};
vr::VROverlay()->SetOverlayTextureBounds(o, &b);
vr::HmdVector2_t scale = {float(w), float(h)};
vr::VROverlay()->SetOverlayMouseScale(o, &scale);
}
void ApplyCrop(Screen &s) {
CropOverlay(s.overlay, s, s.cropX, s.cropY, s.cropW, s.cropH);
for (const auto &[k, sub] : s.subs) CropOverlay(sub.overlay, s, sub.x, sub.y, sub.w, sub.h);
}
// ft-floatd's "float": the window's rectangle, its title bar, and the density. The panel's
// top left corner stays where it is when the window changes size.
void SetFloat(Screen &s, double mpp, int x, int y, int w, int h, int title) {
const bool first = !s.floatOn || s.cropW <= 0;
const double oldW = s.metres, oldH = s.heightMetres();
s.floatOn = true;
s.mpp = mpp;
s.cropX = x, s.cropY = y, s.cropW = w, s.cropH = h, s.titleH = title;
s.metres = w * mpp;
vr::VROverlay()->SetOverlayWidthInMeters(s.overlay, float(s.metres));
ApplyCurve(s);
ApplyCrop(s);
const double dx = (s.metres - oldW) / 2, dy = -(s.heightMetres() - oldH) / 2;
if (!first && (std::fabs(dx) > 1e-6 || std::fabs(dy) > 1e-6)) {
if (s.pinned != kNone) Pin(s, s.pinned, Mul(s.pinRel, Translation(dx, dy, 0)));
else SetAbsolute(s, Mul(s.pose, Translation(dx, dy, 0)));
} else {
PlaceChrome(s);
}
}
void Unfloat(Screen &s) {
if (s.drag != Drag::None) EndDrag(s);
for (auto &[k, sub] : s.subs) vr::VROverlay()->DestroyOverlay(sub.overlay);
s.subs.clear();
s.floatOn = s.minimized = s.titleCarry = false;
s.cropW = s.cropH = 0;
s.resizeW = s.resizeH = 0;
}
// A popup or dialog (number k) at x, y, w, h in the buffer; w = 0 takes it away.
void SetSub(Screen &s, int index, int k, int x, int y, int w, int h) {
auto it = s.subs.find(k);
if (w <= 0 || h <= 0) {
if (it != s.subs.end()) {
vr::VROverlay()->DestroyOverlay(it->second.overlay);
s.subs.erase(it);
}
return;
}
if (it == s.subs.end()) {
Sub sub;
char key[80], name[64];
std::snprintf(key, sizeof key, "frametop.float.%d.sub.%d", index + 1, k);
std::snprintf(name, sizeof name, "Floating window menu %d", k);
if (vr::VROverlay()->CreateOverlay(key, name, &sub.overlay) != vr::VROverlayError_None) return;
vr::VROverlay()->SetOverlayInputMethod(sub.overlay, vr::VROverlayInputMethod_Mouse);
vr::VROverlay()->SetOverlayFlag(sub.overlay, vr::VROverlayFlags_IgnoreTextureAlpha, true);
vr::VROverlay()->SetOverlayFlag(sub.overlay, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true);
vr::VROverlay()->SetOverlayFlag(sub.overlay, vr::VROverlayFlags_MakeOverlaysInteractiveIfVisible, s.lasers);
vr::VROverlay()->SetOverlaySortOrder(sub.overlay, 5);
it = s.subs.emplace(k, sub).first;
if (s.shown) {
auto imp = g_imports.find(s.shown);
if (imp != g_imports.end()) {
vr::SharedTextureHandle_t handle = imp->second;
vr::Texture_t tex = {&handle, vr::TextureType_SharedTextureHandle, vr::ColorSpace_Gamma};
vr::VROverlay()->SetOverlayTexture(sub.overlay, &tex);
}
}
vr::VROverlay()->SetOverlayAlpha(sub.overlay, s.alpha);
if (s.visible) vr::VROverlay()->ShowOverlay(sub.overlay);
}
it->second.x = x, it->second.y = y, it->second.w = w, it->second.h = h;
CropOverlay(it->second.overlay, s, x, y, w, h);
PlaceSubs(s);
}
// ---------------------------------------------------------------- the catcher // ---------------------------------------------------------------- the catcher
// A button pressed on a screen belongs to KWin until it comes up, wherever the laser is by // A button pressed on a screen belongs to KWin until it comes up, wherever the laser is by
@@ -1083,9 +1306,11 @@ void UpdateCatcher() {
for (int k = 0; k < 3; ++k) params.vSource.v[k] = l.m[k][3], params.vDirection.v[k] = -l.m[k][2]; for (int k = 0; k < 3; ++k) params.vSource.v[k] = l.m[k][3], params.vDirection.v[k] = -l.m[k][2];
for (auto &[i, s] : g_screens) { for (auto &[i, s] : g_screens) {
if (!s.visible) continue; if (!s.visible) continue;
for (auto o : s.All()) { std::vector<vr::VROverlayHandle_t> parts(s.All().begin(), s.All().end());
for (const auto &[k, sub] : s.subs) parts.push_back(sub.overlay);
for (auto o : parts) {
vr::VROverlayIntersectionResults_t hit; vr::VROverlayIntersectionResults_t hit;
if (vr::VROverlay()->ComputeOverlayIntersection(o, &params, &hit)) { if (o != vr::k_ulOverlayHandleInvalid && vr::VROverlay()->ComputeOverlayIntersection(o, &params, &hit)) {
g_press.distance = std::max(0.05, double(hit.fDistance)); g_press.distance = std::max(0.05, double(hit.fDistance));
ShowCatcher(false); ShowCatcher(false);
return; return;
@@ -1194,18 +1419,21 @@ int ft_vr_modifiers(uint32_t format, uint64_t *out, int max) {
bool ft_vr_screens_shown(void) { return g_vr && ModeVisible(); } bool ft_vr_screens_shown(void) { return g_vr && ModeVisible(); }
void ft_vr_screen_create(int index, double metres, int count) { } // extern "C"
if (!g_vr) return;
Screen &s = g_screens[index]; namespace {
s.metres = metres;
// A panel and its controls. `prefix` names the overlays (frametop.screen.N,
// frametop.float.N), `label` is what SteamVR shows ("Screen 2", "Floating window 1").
bool MakePanel(Screen &s, const char *prefix, const char *label) {
char key[64], name[64]; char key[64], name[64];
std::snprintf(key, sizeof key, "frametop.screen.%d", index + 1); std::snprintf(key, sizeof key, "%s", prefix);
std::snprintf(name, sizeof name, "Screen %d", index + 1); std::snprintf(name, sizeof name, "%s", label);
if (vr::VROverlay()->CreateOverlay(key, name, &s.overlay) != vr::VROverlayError_None) { if (vr::VROverlay()->CreateOverlay(key, name, &s.overlay) != vr::VROverlayError_None) {
std::fprintf(stderr, "openvr: can't create overlay %s\n", key); std::fprintf(stderr, "openvr: can't create overlay %s\n", key);
return; return false;
} }
vr::VROverlay()->SetOverlayWidthInMeters(s.overlay, float(metres)); vr::VROverlay()->SetOverlayWidthInMeters(s.overlay, float(s.metres));
vr::VROverlay()->SetOverlayInputMethod(s.overlay, vr::VROverlayInputMethod_Mouse); vr::VROverlay()->SetOverlayInputMethod(s.overlay, vr::VROverlayInputMethod_Mouse);
vr::VROverlay()->SetOverlayFlag(s.overlay, vr::VROverlayFlags_IgnoreTextureAlpha, true); vr::VROverlay()->SetOverlayFlag(s.overlay, vr::VROverlayFlags_IgnoreTextureAlpha, true);
vr::VROverlay()->SetOverlayFlag(s.overlay, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true); vr::VROverlay()->SetOverlayFlag(s.overlay, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true);
@@ -1213,21 +1441,39 @@ void ft_vr_screen_create(int index, double metres, int count) {
static const auto corner = CornerTexture(64); static const auto corner = CornerTexture(64);
static const auto curve = CurveTexture(64); static const auto curve = CurveTexture(64);
static const auto roll = RollTexture(64); static const auto roll = RollTexture(64);
std::snprintf(key, sizeof key, "frametop.screen.%d.bar", index + 1); auto chrome = [&](const char *part, const char *what, const std::vector<uint8_t> &px, int w, int h) {
std::snprintf(name, sizeof name, "Screen %d: move", index + 1); std::snprintf(key, sizeof key, "%s.%s", prefix, part);
s.bar = MakeChrome(key, name, BarTexture(false), 256, 24); std::snprintf(name, sizeof name, "%s: %s", label, what);
return MakeChrome(key, name, px, w, h);
};
s.bar = chrome("bar", "move", BarTexture(false), 256, 24);
vr::VROverlay()->SetOverlayFlag(s.bar, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true); vr::VROverlay()->SetOverlayFlag(s.bar, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true);
std::snprintf(key, sizeof key, "frametop.screen.%d.curve", index + 1); s.curveButton = chrome("curve", "curve", curve, 64, 64);
std::snprintf(name, sizeof name, "Screen %d: curve", index + 1); s.rollButton = chrome("roll", "roll", roll, 64, 64);
s.curveButton = MakeChrome(key, name, curve, 64, 64);
std::snprintf(key, sizeof key, "frametop.screen.%d.roll", index + 1);
std::snprintf(name, sizeof name, "Screen %d: roll", index + 1);
s.rollButton = MakeChrome(key, name, roll, 64, 64);
vr::VROverlay()->SetOverlayFlag(s.rollButton, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true); vr::VROverlay()->SetOverlayFlag(s.rollButton, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true);
std::snprintf(key, sizeof key, "frametop.screen.%d.resize", index + 1); s.handle = chrome("resize", "resize", corner, 64, 64);
std::snprintf(name, sizeof name, "Screen %d: resize", index + 1); if (s.floating) {
s.handle = MakeChrome(key, name, corner, 64, 64); static const auto dock = DockTexture(64);
static const auto close = CloseTexture(64);
s.dockButton = chrome("dock", "back to the desktop", dock, 64, 64);
s.closeButton = chrome("close", "close", close, 64, 64);
}
ApplyAlpha(s); ApplyAlpha(s);
return true;
}
} // namespace
extern "C" {
void ft_vr_screen_create(int index, double metres, int count) {
if (!g_vr) return;
Screen &s = g_screens[index];
s.metres = metres;
char prefix[64], label[64];
std::snprintf(prefix, sizeof prefix, "frametop.screen.%d", index + 1);
std::snprintf(label, sizeof label, "Screen %d", index + 1);
if (!MakePanel(s, prefix, label)) return;
// Until the layout places it: 2 m ahead of the head, in a row, screen 1 on the left. // Until the layout places it: 2 m ahead of the head, in a row, screen 1 on the left.
RefreshPoses(); RefreshPoses();
Mat head; Mat head;
@@ -1238,10 +1484,28 @@ void ft_vr_screen_create(int index, double metres, int count) {
SetAbsolute(s, PanelPose(head.m[0][3] + dx * 2, head.m[1][3], head.m[2][3] + dz * 2, yaw, 0, 0)); SetAbsolute(s, PanelPose(head.m[0][3] + dx * 2, head.m[1][3], head.m[2][3] + dz * 2, yaw, 0, 0));
} }
// A spare output's panel (number `slot` from 1): hidden until a window floats on it.
void ft_vr_float_create(int index, int slot) {
if (!g_vr) return;
Screen &s = g_screens[index];
s.floating = true;
char prefix[64], label[64];
std::snprintf(prefix, sizeof prefix, "frametop.float.%d", slot);
std::snprintf(label, sizeof label, "Floating window %d", slot);
MakePanel(s, prefix, label);
}
void ft_vr_float_output(int index, bool on) {
auto it = g_screens.find(index);
if (it != g_screens.end()) it->second.outputOn = on;
}
void ft_vr_screen_destroy(int index) { void ft_vr_screen_destroy(int index) {
auto it = g_screens.find(index); auto it = g_screens.find(index);
if (it == g_screens.end()) return; if (it == g_screens.end()) return;
for (auto o : it->second.All()) vr::VROverlay()->DestroyOverlay(o); for (auto o : it->second.All())
if (o != vr::k_ulOverlayHandleInvalid) vr::VROverlay()->DestroyOverlay(o);
for (auto &[k, sub] : it->second.subs) vr::VROverlay()->DestroyOverlay(sub.overlay);
g_screens.erase(it); g_screens.erase(it);
} }
@@ -1274,14 +1538,19 @@ bool ft_vr_screen_present(int index, const void *key, const struct ft_dmabuf *b)
} }
if (b->width != s.width || b->height != s.height) { if (b->width != s.width || b->height != s.height) {
s.width = b->width, s.height = b->height; s.width = b->width, s.height = b->height;
vr::HmdVector2_t scale = {float(s.width), float(s.height)}; if (s.floating) {
vr::VROverlay()->SetOverlayMouseScale(s.overlay, &scale); ApplyCrop(s);
} else {
vr::HmdVector2_t scale = {float(s.width), float(s.height)};
vr::VROverlay()->SetOverlayMouseScale(s.overlay, &scale);
}
PlaceChrome(s); // the height changed PlaceChrome(s); // the height changed
std::printf("screen %d: %dx%d\n", index + 1, s.width, s.height); std::printf("screen %d: %dx%d\n", index + 1, s.width, s.height);
} }
vr::SharedTextureHandle_t handle = it->second; vr::SharedTextureHandle_t handle = it->second;
vr::Texture_t tex = {&handle, vr::TextureType_SharedTextureHandle, vr::ColorSpace_Gamma}; vr::Texture_t tex = {&handle, vr::TextureType_SharedTextureHandle, vr::ColorSpace_Gamma};
vr::VROverlay()->SetOverlayTexture(s.overlay, &tex); vr::VROverlay()->SetOverlayTexture(s.overlay, &tex);
for (const auto &[k, sub] : s.subs) vr::VROverlay()->SetOverlayTexture(sub.overlay, &tex);
s.shown = key; // UpdateVisibility shows it on the next tick s.shown = key; // UpdateVisibility shows it on the next tick
return true; return true;
} }
@@ -1299,14 +1568,19 @@ void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
for (auto &[index, s] : g_screens) { for (auto &[index, s] : g_screens) {
vr::VREvent_t ev; vr::VREvent_t ev;
// The screen itself: input for KWin. // The screen itself: input for KWin.
while (vr::VROverlay()->PollNextOverlayEvent(s.overlay, &ev, sizeof ev)) { // The screen itself (and a floating window's popups): input for KWin.
auto panelEvent = [&](const vr::VREvent_t &ev, bool sub, const Sub *from) {
ft_event e{}; ft_event e{};
e.screen = index; e.screen = index;
auto at = [&] {
if (from) e.x = from->x + ev.data.mouse.x, e.y = from->y + from->h - ev.data.mouse.y;
else s.ToBuffer(ev.data.mouse.x, ev.data.mouse.y, &e.x, &e.y);
};
switch (ev.eventType) { switch (ev.eventType) {
case vr::VREvent_MouseMove: case vr::VREvent_MouseMove:
if (s.titleCarry) return; // KWin's pointer stays where the title bar was pressed
e.type = FT_MOTION; e.type = FT_MOTION;
e.x = ev.data.mouse.x; at();
e.y = s.height - ev.data.mouse.y; // OpenVR's mouse origin is bottom left
if (g_press.buttons) g_press.screen = index, g_press.x = e.x, g_press.y = e.y; if (g_press.buttons) g_press.screen = index, g_press.x = e.x, g_press.y = e.y;
break; break;
case vr::VREvent_MouseButtonDown: case vr::VREvent_MouseButtonDown:
@@ -1315,10 +1589,17 @@ void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
e.type = FT_BUTTON; e.type = FT_BUTTON;
e.button = LinuxButton(ev.data.mouse.button); e.button = LinuxButton(ev.data.mouse.button);
e.pressed = ev.eventType == vr::VREvent_MouseButtonDown; e.pressed = ev.eventType == vr::VREvent_MouseButtonDown;
e.x = ev.data.mouse.x; at();
e.y = s.height - ev.data.mouse.y; if (!e.pressed && s.titleCarry) e.x = s.carryX, e.y = s.carryY, s.titleCarry = false;
if (e.pressed) { if (e.pressed) {
PressDown(ev.trackedDeviceIndex, e.button, index, e.x, e.y); PressDown(ev.trackedDeviceIndex, e.button, index, e.x, e.y);
// A floating window's title bar: carry the panel, and KWin (which starts
// moving the window on the press) sees no motion until the release.
if (!sub && s.floating && e.button == BTN_LEFT && s.titleH > 0 && e.y >= s.cropY &&
e.y < s.cropY + s.titleH && s.drag == Drag::None) {
s.titleCarry = true, s.carryX = e.x, s.carryY = e.y;
StartDrag(s, Drag::Move, ev.trackedDeviceIndex);
}
} else { } else {
g_press.buttons &= ~ButtonBit(e.button); g_press.buttons &= ~ButtonBit(e.button);
if (!g_press.buttons) g_press.upAt = -1; if (!g_press.buttons) g_press.upAt = -1;
@@ -1330,14 +1611,18 @@ void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
e.dy = -ev.data.scroll.ydelta; e.dy = -ev.data.scroll.ydelta;
break; break;
case vr::VREvent_FocusLeave: case vr::VREvent_FocusLeave:
if (g_press.buttons) continue; // KWin keeps the pointer while a button is held if (g_press.buttons) return; // KWin keeps the pointer while a button is held
if (sub) return; // off a popup is usually onto its window
e.type = FT_LEAVE; e.type = FT_LEAVE;
break; break;
default: default:
continue; return;
} }
handle(&e, data); handle(&e, data);
} };
while (vr::VROverlay()->PollNextOverlayEvent(s.overlay, &ev, sizeof ev)) panelEvent(ev, false, nullptr);
for (const auto &[k, sub] : s.subs)
while (vr::VROverlay()->PollNextOverlayEvent(sub.overlay, &ev, sizeof ev)) panelEvent(ev, true, &sub);
// The controls light up under a laser. // The controls light up under a laser.
auto hover = [&](int k) { auto hover = [&](int k) {
const bool on = ev.eventType == vr::VREvent_MouseMove || ev.eventType == vr::VREvent_FocusEnter; const bool on = ev.eventType == vr::VREvent_MouseMove || ev.eventType == vr::VREvent_FocusEnter;
@@ -1376,6 +1661,20 @@ void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
ReleaseAwayBy(ev.trackedDeviceIndex, ev.data.mouse.button, handle, data); ReleaseAwayBy(ev.trackedDeviceIndex, ev.data.mouse.button, handle, data);
} }
} }
// A floating window's buttons: back to the desktop, and close (ft-floatd does both).
for (int k : {4, 5}) {
const vr::VROverlayHandle_t o = s.Controls()[k];
if (o == vr::k_ulOverlayHandleInvalid) continue;
while (vr::VROverlay()->PollNextOverlayEvent(o, &ev, sizeof ev)) {
hover(k);
if (ev.eventType == vr::VREvent_MouseButtonDown && ev.data.mouse.button == vr::VRMouseButton_Left) {
SendFloat((k == 4 ? "dock " : "close ") + std::to_string(index + 1));
} else if (ev.eventType == vr::VREvent_MouseButtonUp) {
EndDragsBy(ev.trackedDeviceIndex);
ReleaseAwayBy(ev.trackedDeviceIndex, ev.data.mouse.button, handle, data);
}
}
}
// The roll button: drag around like a knob, or scroll. // The roll button: drag around like a knob, or scroll.
while (vr::VROverlay()->PollNextOverlayEvent(s.rollButton, &ev, sizeof ev)) { while (vr::VROverlay()->PollNextOverlayEvent(s.rollButton, &ev, sizeof ev)) {
hover(2); hover(2);
@@ -1446,8 +1745,18 @@ void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
// ~100 ms (it landed on something else), KWin gets it anyway // ~100 ms (it landed on something else), KWin gets it anyway
// state -> "ok <mode> <manual 0|1> <wrist deg> <gesture hand> <gesture deg> // state -> "ok <mode> <manual 0|1> <wrist deg> <gesture hand> <gesture deg>
// <controllers> <game running 0|1> <ingames>" // <controllers> <game running 0|1> <ingames>"
// Floating windows (from ft-floatd; <screen> is the spare output's number, after the screens):
// float <screen> <metres per pixel> <x> <y> <w> <h> <title> the window's rectangle in the
// buffer and its title bar's height (pixels); shows the panel
// unfloat <screen> hides it
// pose <screen> <12 numbers> its place in the room (rows of a 3x4, standing universe)
// sub <screen> <k> <x> <y> <w> <h> | sub <screen> <k> off popup or dialog k over it
// minimized <screen> 0|1
// carry <screen> the window's own title bar was pressed (an app that draws its
// own): carry the panel with the pressing laser until the release
// (size <screen> <w> <h> and key <code> <value> are handled in compositor.c.) Screens are // (size <screen> <w> <h> and key <code> <value> are handled in compositor.c.) Screens are
// numbered from 1 here, like everywhere the user sees them. // numbered from 1 here, like everywhere the user sees them. "screens" and "all" leave out
// floating windows.
void ft_vr_command(const char *cmd, char *reply, int size) { void ft_vr_command(const char *cmd, char *reply, int size) {
if (!g_vr) return (void)std::snprintf(reply, size, "error no SteamVR (--no-vr)"); if (!g_vr) return (void)std::snprintf(reply, size, "error no SteamVR (--no-vr)");
RefreshPoses(); RefreshPoses();
@@ -1457,7 +1766,8 @@ void ft_vr_command(const char *cmd, char *reply, int size) {
float r[12]; float r[12];
auto each = [&](const char *which, auto fn) -> bool { // "all" or a screen number auto each = [&](const char *which, auto fn) -> bool { // "all" or a screen number
if (std::strcmp(which, "all") == 0) { if (std::strcmp(which, "all") == 0) {
for (auto &[i, s] : g_screens) fn(s); for (auto &[i, s] : g_screens)
if (!s.floating) fn(s);
return true; return true;
} }
Screen *s = Find(std::atoi(which)); Screen *s = Find(std::atoi(which));
@@ -1528,9 +1838,10 @@ void ft_vr_command(const char *cmd, char *reply, int size) {
for (int k = 0; k < 12 && len < size; ++k) for (int k = 0; k < 12 && len < size; ++k)
len += std::snprintf(reply + len, size - len, " %.5f", s->pinRel.m[k / 4][k % 4]); len += std::snprintf(reply + len, size - len, " %.5f", s->pinRel.m[k / 4][k % 4]);
} else if (std::strncmp(cmd, "screens", 7) == 0) { } else if (std::strncmp(cmd, "screens", 7) == 0) {
int len = std::snprintf(reply, size, "ok %zu", g_screens.size()); const size_t count = std::count_if(g_screens.begin(), g_screens.end(), [](auto &e) { return !e.second.floating; });
int len = std::snprintf(reply, size, "ok %zu", count);
for (auto &[i, s] : g_screens) for (auto &[i, s] : g_screens)
if (len < size) if (len < size && !s.floating)
len += std::snprintf(reply + len, size - len, " %d:%dx%d:%.3f", i + 1, s.width, s.height, s.metres); len += std::snprintf(reply + len, size - len, " %d:%dx%d:%.3f", i + 1, s.width, s.height, s.metres);
} else if (std::strncmp(cmd, "head", 4) == 0) { } else if (std::strncmp(cmd, "head", 4) == 0) {
Mat m; Mat m;
@@ -1576,6 +1887,48 @@ void ft_vr_command(const char *cmd, char *reply, int size) {
else return (void)std::snprintf(reply, size, "error modes: always outside_games dashboard"); else return (void)std::snprintf(reply, size, "error modes: always outside_games dashboard");
UpdateLasers(); UpdateLasers();
std::snprintf(reply, size, "ok %s", LasersName()); std::snprintf(reply, size, "ok %s", LasersName());
} else if (int x0, y0, w0, h0, t0; std::sscanf(cmd, "float %d %lf %d %d %d %d %d", &n, &w, &x0, &y0, &w0, &h0, &t0) == 7) {
Screen *s = Find(n);
if (!s || !s->floating) return (void)std::snprintf(reply, size, "error no floating window panel %d", n);
if (!(w > 1e-5 && w < 0.01) || w0 < 1 || h0 < 1) return (void)std::snprintf(reply, size, "error bad float");
SetFloat(*s, w, x0, y0, w0, h0, std::max(0, t0));
std::snprintf(reply, size, "ok");
} else if (std::sscanf(cmd, "unfloat %d", &n) == 1) {
Screen *s = Find(n);
if (!s || !s->floating) return (void)std::snprintf(reply, size, "error no floating window panel %d", n);
Unfloat(*s);
UpdateVisibility();
std::snprintf(reply, size, "ok");
} else if (std::sscanf(cmd, "pose %d %f %f %f %f %f %f %f %f %f %f %f %f", &n, &r[0], &r[1], &r[2], &r[3], &r[4],
&r[5], &r[6], &r[7], &r[8], &r[9], &r[10], &r[11]) == 13) {
Screen *s = Find(n);
if (!s) return (void)std::snprintf(reply, size, "error no screen %d", n);
Mat m{};
for (int k = 0; k < 12; ++k) m.m[k / 4][k % 4] = r[k];
EndDrag(*s);
SetAbsolute(*s, m);
std::snprintf(reply, size, "ok");
} else if (int k0; std::sscanf(cmd, "sub %d %d %d %d %d %d", &n, &k0, &x0, &y0, &w0, &h0) == 6 ||
(std::sscanf(cmd, "sub %d %d %15s", &n, &k0, word) == 3 && !std::strcmp(word, "off"))) {
Screen *s = Find(n);
if (!s || !s->floating) return (void)std::snprintf(reply, size, "error no floating window panel %d", n);
if (std::strstr(cmd, " off")) w0 = h0 = 0;
SetSub(*s, n - 1, k0, x0, y0, w0, h0);
std::snprintf(reply, size, "ok");
} else if (int on; std::sscanf(cmd, "minimized %d %d", &n, &on) == 2) {
Screen *s = Find(n);
if (!s || !s->floating) return (void)std::snprintf(reply, size, "error no floating window panel %d", n);
s->minimized = on != 0;
UpdateVisibility();
std::snprintf(reply, size, "ok");
} else if (std::sscanf(cmd, "carry %d", &n) == 1) {
Screen *s = Find(n);
if (!s || !s->floating) return (void)std::snprintf(reply, size, "error no floating window panel %d", n);
if (!(g_press.buttons & ButtonBit(BTN_LEFT)) || g_press.screen != n - 1 || s->drag != Drag::None)
return (void)std::snprintf(reply, size, "error not pressed there");
s->titleCarry = true, s->carryX = g_press.x, s->carryY = g_press.y;
StartDrag(*s, Drag::Move, g_press.device);
std::snprintf(reply, size, "ok");
} else if (std::strcmp(cmd, "up") == 0) { } else if (std::strcmp(cmd, "up") == 0) {
if ((g_press.buttons & ButtonBit(BTN_LEFT)) && g_press.device != kNone && !IsHandController(g_press.device)) if ((g_press.buttons & ButtonBit(BTN_LEFT)) && g_press.device != kNone && !IsHandController(g_press.device))
g_press.upAt = g_tick + 9; g_press.upAt = g_tick + 9;
+4
View File
@@ -37,6 +37,10 @@ bool ft_vr_screens_shown(void);
// A panel for screen `index`, width in metres, placed in a row in front of the head. // A panel for screen `index`, width in metres, placed in a row in front of the head.
void ft_vr_screen_create(int index, double metres, int count); void ft_vr_screen_create(int index, double metres, int count);
void ft_vr_screen_destroy(int index); void ft_vr_screen_destroy(int index);
// A spare output's panel, for floating windows (slot numbers from 1): hidden until
// ft-floatd floats a window on it and KWin has the output turned on.
void ft_vr_float_create(int index, int slot);
void ft_vr_float_output(int index, bool on);
// Show a client buffer (identified by `key`) on the screen's panel. False if SteamVR // Show a client buffer (identified by `key`) on the screen's panel. False if SteamVR
// can't import it. // can't import it.
bool ft_vr_screen_present(int index, const void *key, const struct ft_dmabuf *buf); bool ft_vr_screen_present(int index, const void *key, const struct ft_dmabuf *buf);
+28 -4
View File
@@ -30,7 +30,7 @@ for var in $(compgen -e); do
done done
conf=$HOME/.config/frametop.conf conf=$HOME/.config/frametop.conf
BACKEND=screens SCREENS=2 WIDTH=1920 HEIGHT=1080 PHYS_WIDTH=1.6 REMOTE=0 BACKEND=screens SCREENS=2 WIDTH=1920 HEIGHT=1080 PHYS_WIDTH=1.6 REMOTE=0 FLOAT_SLOTS=8 FLOAT_MARGIN=300
# shellcheck disable=SC1090 # shellcheck disable=SC1090
[ -f "$conf" ] && . "$conf" [ -f "$conf" ] && . "$conf"
backend=${FT_BACKEND:-$BACKEND} backend=${FT_BACKEND:-$BACKEND}
@@ -39,6 +39,13 @@ width=${FT_WIDTH:-$WIDTH}
height=${FT_HEIGHT:-$HEIGHT} height=${FT_HEIGHT:-$HEIGHT}
phys_width=${FT_PHYS_WIDTH:-$PHYS_WIDTH} phys_width=${FT_PHYS_WIDTH:-$PHYS_WIDTH}
remote=${FT_REMOTE:-$REMOTE} remote=${FT_REMOTE:-$REMOTE}
# Floating windows (screens backend): KWin gets this many spare outputs after the screens,
# and ft-floatd floats a window on each (docs/floating-windows.md). Changing it takes a
# desktop restart.
float_slots=${FT_FLOAT_SLOTS:-$FLOAT_SLOTS}
[[ $float_slots =~ ^[0-9]+$ ]] || float_slots=8
[ "$float_slots" -le 16 ] || float_slots=16
[ "$backend" = screens ] || float_slots=0
if [ "$backend" = gamescope ] && [ $((width * height)) -gt $((1920 * 1080)) ]; then if [ "$backend" = gamescope ] && [ $((width * height)) -gt $((1920 * 1080)) ]; then
# gamescope's VR backend aborts above 1920x1080 worth of pixels (its upload buffer; # gamescope's VR backend aborts above 1920x1080 worth of pixels (its upload buffer;
# see docs/design.md). Shrink a bigger size to fit, keeping its shape. # see docs/design.md). Shrink a bigger size to fit, keeping its shape.
@@ -80,10 +87,10 @@ if [ "${1:-}" != --inner ]; then
# Plasma stay on the host and connect to its socket. # Plasma stay on the host and connect to its socket.
socket=ft-screens-0 socket=ft-screens-0
read -ra screen_args <<< "$("$here/../layout/ft-layout" screen-args)" read -ra screen_args <<< "$("$here/../layout/ft-layout" screen-args)"
export FT_SCREEN_COUNT=$(( ${#screen_args[@]} / 2 )) export FT_SCREEN_COUNT=$(( ${#screen_args[@]} / 2 )) FT_FLOAT_SLOTS=$float_slots
"$here/../scripts/container-up.sh" # not owned by this desktop, or stopping it would stop the container "$here/../scripts/container-up.sh" # not owned by this desktop, or stopping it would stop the container
"$HOME/.local/bin/distrobox" enter dev -- "$here/../screens/build/ft-screens" --socket "$socket" \ "$HOME/.local/bin/distrobox" enter dev -- "$here/../screens/build/ft-screens" --socket "$socket" \
"${screen_args[@]}" > /tmp/frametop-screens.log 2>&1 < /dev/null & "${screen_args[@]}" --spares "$float_slots" > /tmp/frametop-screens.log 2>&1 < /dev/null &
stop_screens() { pkill -x ft-screens 2>/dev/null || true; } stop_screens() { pkill -x ft-screens 2>/dev/null || true; }
trap stop_screens EXIT trap stop_screens EXIT
for _ in $(seq 100); do [ -S "$XDG_RUNTIME_DIR/$socket" ] && break; sleep 0.2; done for _ in $(seq 100); do [ -S "$XDG_RUNTIME_DIR/$socket" ] && break; sleep 0.2; done
@@ -135,7 +142,7 @@ fi
# With ft-screens the size is only the starting one: ft-screens sets each screen's own. # With ft-screens the size is only the starting one: ft-screens sets each screen's own.
cat > "$runtime/bin/kwin_wayland_wrapper" <<EOF cat > "$runtime/bin/kwin_wayland_wrapper" <<EOF
#!/bin/sh #!/bin/sh
exec /usr/bin/kwin_wayland_wrapper --width $width --height $height --output-count $screens --no-lockscreen "\$@" exec /usr/bin/kwin_wayland_wrapper --width $width --height $height --output-count $((screens + float_slots)) --no-lockscreen "\$@"
EOF EOF
chmod +x "$runtime/bin/kwin_wayland_wrapper" chmod +x "$runtime/bin/kwin_wayland_wrapper"
export PATH=$runtime/bin:$PATH export PATH=$runtime/bin:$PATH
@@ -163,4 +170,21 @@ if [ "$remote" = 1 ]; then
"$here/vnc-bridge.sh" "$width" "$height" > /tmp/frametop-vnc.log 2>&1 & "$here/vnc-bridge.sh" "$width" "$height" > /tmp/frametop-vnc.log 2>&1 &
fi fi
# ft-floatd (floating windows) runs inside the Plasma session, on its D-Bus: started from
# the session's autostart, which only this desktop reads (XDG_CONFIG_HOME above).
autostart=$XDG_CONFIG_HOME/autostart/frametop-floatd.desktop
if [ "$float_slots" -gt 0 ]; then
mkdir -p "$(dirname "$autostart")"
cat > "$autostart" <<EOF
[Desktop Entry]
Type=Application
Name=Frametop floating windows
Exec=sh -c 'exec "$here/../float/ft-floatd" --screens $screens --slots $float_slots > /tmp/frametop-floatd.log 2>&1'
X-KDE-autostart-phase=2
NoDisplay=true
EOF
else
rm -f "$autostart"
fi
dbus-run-session startplasma-wayland dbus-run-session startplasma-wayland
+2
View File
@@ -7,6 +7,8 @@ WIDTH=1920 # gamescope: pixels per screen (at most 1920x1080 worth)
HEIGHT=1080 HEIGHT=1080
PHYS_WIDTH=1.6 # gamescope: panel width in metres, docked PHYS_WIDTH=1.6 # gamescope: panel width in metres, docked
REMOTE=0 # 1 = serve the desktop over VNC on the tailnet (port 5900; see README) REMOTE=0 # 1 = serve the desktop over VNC on the tailnet (port 5900; see README)
FLOAT_SLOTS=8 # screens backend: how many windows can float in VR at once (spare outputs; 0 turns it off; restart the desktop after a change)
FLOAT_MARGIN=300 # floating windows: pixels around each window on its output, so menus have room past its edges
POINTER=0 # 1 = the mouse drives the universal 3D pointer (ft_pointer driver) instead of a plain mouse POINTER=0 # 1 = the mouse drives the universal 3D pointer (ft_pointer driver) instead of a plain mouse
POINTER_SENSITIVITY=0.03 # degrees per mouse count POINTER_SENSITIVITY=0.03 # degrees per mouse count
POINTER_IDLE=30 # seconds without mouse use before the controllers get their laser back POINTER_IDLE=30 # seconds without mouse use before the controllers get their laser back