mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 02:00:06 +02:00
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:
1 parent
87a402c68d
commit
c2e5e861c8
9 files changed
+480
-75
No files matched your search
@@ -305,6 +305,7 @@ class Daemon:
|
||||
# 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.
|
||||
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.place_panel(f, ev)
|
||||
kscreen(f"output.{slot.output}.enable", f"output.{slot.output}.scale.{s:g}",
|
||||
|
||||
+6
-1
@@ -684,13 +684,18 @@ def nested_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:
|
||||
data = json.loads(subprocess.run(["kscreen-doctor", "-j"], capture_output=True, text=True, env=env,
|
||||
timeout=10).stdout)
|
||||
except (OSError, ValueError, subprocess.TimeoutExpired):
|
||||
return []
|
||||
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", ""))])
|
||||
|
||||
|
||||
|
||||
@@ -371,6 +371,7 @@ private:
|
||||
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 ||
|
||||
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")
|
||||
continue;
|
||||
// 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);
|
||||
vr::VROverlayTransformType tt = vr::VROverlayTransform_Invalid;
|
||||
overlay->GetOverlayTransformType(h, &tt);
|
||||
// ft-screens' panels (frametop.screen.N) are 0x0 and absolute too (a shared
|
||||
// texture), but they're real panels of any size.
|
||||
// ft-screens' panels (frametop.screen.N, and floating windows with their
|
||||
// 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 &&
|
||||
key.rfind("frametop.screen.", 0) != 0;
|
||||
key.rfind("frametop.screen.", 0) != 0 && key.rfind("frametop.float.", 0) != 0;
|
||||
}
|
||||
ours = vr::k_unTrackedDeviceIndexInvalid;
|
||||
for (vr::TrackedDeviceIndex_t i = 0; i < vr::k_unMaxTrackedDeviceCount; ++i) {
|
||||
|
||||
+24
-10
@@ -13,13 +13,15 @@
|
||||
// scale first ("scale <screen> <s>", from ft-layout).
|
||||
//
|
||||
// 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)
|
||||
// --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
|
||||
// input, and nothing sent to the input relay. Commands still work, and
|
||||
// "toplevels" shows what KWin opened.
|
||||
// --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)
|
||||
// Runs in the dev container (wlroots 0.20); KWin connects from the host.
|
||||
#define _GNU_SOURCE
|
||||
@@ -57,7 +59,7 @@
|
||||
|
||||
#include "vr.h"
|
||||
|
||||
#define MAX_SCREENS 8
|
||||
#define MAX_SCREENS 24 // screens and spare outputs
|
||||
|
||||
struct config {
|
||||
int width, height;
|
||||
@@ -97,7 +99,8 @@ struct server {
|
||||
struct screen *screens[MAX_SCREENS];
|
||||
struct config config[MAX_SCREENS];
|
||||
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_event_source *tick;
|
||||
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;
|
||||
if (xdg->initial_commit) {
|
||||
// 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_activated(sc->toplevel, true);
|
||||
if (sc->decoration)
|
||||
@@ -205,7 +209,9 @@ static void screen_destroy(struct wl_listener *l, void *data) {
|
||||
// after it while that output is off.
|
||||
static void screen_title(struct wl_listener *l, void *data) {
|
||||
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) {
|
||||
@@ -222,9 +228,15 @@ static void new_toplevel(struct wl_listener *l, void *data) {
|
||||
sc->index = index;
|
||||
sc->toplevel = toplevel;
|
||||
s->screens[index] = sc;
|
||||
const struct config *c = &s->config[index < s->n_config ? index : 0];
|
||||
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 > index + 1 ? s->n_config : index + 1);
|
||||
if (index >= s->n_config) {
|
||||
wlr_log(WLR_INFO, "screen %d: KWin window, a spare output (floating window %d)", 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;
|
||||
wl_signal_add(&toplevel->base->surface->events.commit, &sc->commit);
|
||||
sc->destroy.notify = screen_destroy;
|
||||
@@ -537,6 +549,8 @@ int main(int argc, char **argv) {
|
||||
socket_name = argv[++i];
|
||||
} else if (strcmp(argv[i], "--control") == 0 && i + 1 < argc) {
|
||||
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) {
|
||||
s.vr = false;
|
||||
} 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 {
|
||||
fprintf(stderr,
|
||||
"usage: %s [--socket NAME] [--control NAME] [--no-vr] [--screen WxH@METRES]... "
|
||||
"[-- COMMAND ARGS...]\n",
|
||||
"[--spares N] [-- COMMAND ARGS...]\n",
|
||||
argv[0]);
|
||||
return 2;
|
||||
}
|
||||
@@ -599,7 +613,7 @@ int main(int argc, char **argv) {
|
||||
}
|
||||
char path[256];
|
||||
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);
|
||||
if (control < 0) return 1;
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
# or on the Frame. Build ft-screens first (screens/build.sh).
|
||||
#
|
||||
# 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 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
|
||||
@@ -89,7 +89,7 @@ case "${1:-}" in
|
||||
args=(--screen 1920x1080@1.6 --screen 1080x1920@0.9)
|
||||
for ((i = 2; i < screens; i++)); do args+=(--screen 1920x1080@1.6); done
|
||||
args=("${args[@]:0:$((screens * 2))}")
|
||||
for ((i = 0; i < spares; i++)); do args+=(--screen 800x600); done
|
||||
args+=(--spares "$spares")
|
||||
cd "$REPO_ROOT"
|
||||
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[@]}" \
|
||||
|
||||
+408
-55
@@ -42,6 +42,15 @@
|
||||
// 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
|
||||
// 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,
|
||||
// and handle are placed whenever their screen moves.
|
||||
#include "vr.h"
|
||||
@@ -49,6 +58,8 @@
|
||||
#include <openvr.h>
|
||||
|
||||
#include <fcntl.h>
|
||||
#include <sys/socket.h>
|
||||
#include <sys/un.h>
|
||||
#include <linux/input-event-codes.h>
|
||||
#include <limits.h>
|
||||
#include <spawn.h>
|
||||
@@ -59,6 +70,7 @@ extern char **environ; // for posix_spawn
|
||||
#include <chrono>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <cstdio>
|
||||
#include <cstdlib>
|
||||
#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)
|
||||
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 {
|
||||
vr::VROverlayHandle_t overlay = vr::k_ulOverlayHandleInvalid, bar = 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)
|
||||
double metres = 1;
|
||||
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
|
||||
Mat rollFrom = Identity(); // roll: the pose at the press (pinRel when pinned)
|
||||
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
|
||||
float controls = 0; // the controls' fade, 0 (hidden) .. 1
|
||||
bool controlsUp = false; // the controls' overlays are shown
|
||||
@@ -238,9 +257,35 @@ struct Screen {
|
||||
bool barLit = false;
|
||||
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 heightMetres() const { return width > 0 ? metres * height / width : metres * 9 / 16; }
|
||||
std::array<vr::VROverlayHandle_t, 4> Controls() const { return {bar, curveButton, rollButton, handle}; }
|
||||
std::array<vr::VROverlayHandle_t, 5> All() const { return {overlay, bar, curveButton, rollButton, handle}; }
|
||||
// A floating window's panel (see the top): the window's rectangle in the buffer, its
|
||||
// title bar's height there, and the density.
|
||||
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<const void *, vr::SharedTextureHandle_t> g_imports;
|
||||
@@ -348,6 +393,29 @@ std::vector<uint8_t> RollTexture(int n) {
|
||||
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 o = vr::k_ulOverlayHandleInvalid;
|
||||
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
|
||||
// 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.
|
||||
std::array<Mat, 4> ControlOffsets(const Screen &s) {
|
||||
// Where each control sits, relative to the screen: bar, curve, roll, resize tab, and a
|
||||
// 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;
|
||||
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),
|
||||
// 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) {
|
||||
@@ -517,12 +609,19 @@ void PlaceChrome(Screen &s) {
|
||||
vr::VROverlay()->SetOverlayWidthInMeters(s.curveButton, float(button));
|
||||
vr::VROverlay()->SetOverlayWidthInMeters(s.rollButton, float(button));
|
||||
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.
|
||||
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[] = {
|
||||
{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) {
|
||||
for (const auto &[o, off] : parts) {
|
||||
if (o == vr::k_ulOverlayHandleInvalid) continue;
|
||||
const Mat m = Mul(s.pinRel, off);
|
||||
vr::VROverlay()->SetOverlayTransformTrackedDeviceRelative(o, s.pinned, &m);
|
||||
}
|
||||
@@ -531,6 +630,7 @@ void PlaceChrome(Screen &s) {
|
||||
Mat p;
|
||||
if (!ScreenPose(s, &p)) return;
|
||||
for (const auto &[o, off] : parts) {
|
||||
if (o == vr::k_ulOverlayHandleInvalid) continue;
|
||||
const Mat m = Mul(p, off);
|
||||
vr::VROverlay()->SetOverlayTransformAbsolute(o, vr::TrackingUniverseStanding, &m);
|
||||
}
|
||||
@@ -637,11 +737,13 @@ bool ModeVisible() {
|
||||
// it's being dragged.
|
||||
void ApplyAlpha(const Screen &s) {
|
||||
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,
|
||||
s.hover[3] || s.drag == Drag::Resize};
|
||||
for (const auto &[k, sub] : s.subs) vr::VROverlay()->SetOverlayAlpha(sub.overlay, s.alpha);
|
||||
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();
|
||||
for (int k = 0; k < 4; ++k)
|
||||
vr::VROverlay()->SetOverlayAlpha(controls[k], s.alpha * s.controls * (active[k] ? 1.f : kChromeIdle));
|
||||
for (int k = 0; k < 6; ++k)
|
||||
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) {
|
||||
@@ -653,11 +755,14 @@ void SetVisible(Screen &s, bool visible, float alpha) {
|
||||
s.visible = visible;
|
||||
if (visible) {
|
||||
vr::VROverlay()->ShowOverlay(s.overlay);
|
||||
for (const auto &[k, sub] : s.subs) vr::VROverlay()->ShowOverlay(sub.overlay);
|
||||
return;
|
||||
}
|
||||
// Hidden: the controls go at once (UpdateControls brings them back).
|
||||
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;
|
||||
}
|
||||
|
||||
@@ -667,6 +772,9 @@ void UpdateVisibility() {
|
||||
const bool haveHead = DevicePose(vr::k_unTrackedDeviceIndex_Hmd, &head);
|
||||
for (auto &[i, s] : g_screens) {
|
||||
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;
|
||||
Mat p;
|
||||
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);
|
||||
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))));
|
||||
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);
|
||||
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]};
|
||||
@@ -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);
|
||||
// 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
|
||||
// any device's laser, whatever its shape.
|
||||
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;
|
||||
ApplyAlpha(s);
|
||||
}
|
||||
@@ -748,6 +859,19 @@ void UpdateControls() {
|
||||
|
||||
// ---------------------------------------------------------------- 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),
|
||||
// metres from its centre.
|
||||
bool RayOnPlane(const Mat &p, const Mat &d, double *x, double *y) {
|
||||
@@ -905,7 +1029,7 @@ void FinishDrag(Screen &s, int index) {
|
||||
EndDrag(s);
|
||||
Mat c, p;
|
||||
if (!moved) return;
|
||||
ArrangeDesktopSoon();
|
||||
if (!s.floating) ArrangeDesktopSoon();
|
||||
if (target != kNone && DevicePose(target, &c) && ScreenPose(s, &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);
|
||||
@@ -992,6 +1116,19 @@ void UpdateDrag(Screen &s, int index) {
|
||||
double hx, hy;
|
||||
Mat l;
|
||||
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 cx = hx - s.grabX, cy = hy - s.grabY; // where the corner should be
|
||||
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);
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 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
|
||||
|
||||
// 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 (auto &[i, s] : g_screens) {
|
||||
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;
|
||||
if (vr::VROverlay()->ComputeOverlayIntersection(o, ¶ms, &hit)) {
|
||||
if (o != vr::k_ulOverlayHandleInvalid && vr::VROverlay()->ComputeOverlayIntersection(o, ¶ms, &hit)) {
|
||||
g_press.distance = std::max(0.05, double(hit.fDistance));
|
||||
ShowCatcher(false);
|
||||
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(); }
|
||||
|
||||
void ft_vr_screen_create(int index, double metres, int count) {
|
||||
if (!g_vr) return;
|
||||
Screen &s = g_screens[index];
|
||||
s.metres = metres;
|
||||
} // extern "C"
|
||||
|
||||
namespace {
|
||||
|
||||
// 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];
|
||||
std::snprintf(key, sizeof key, "frametop.screen.%d", index + 1);
|
||||
std::snprintf(name, sizeof name, "Screen %d", index + 1);
|
||||
std::snprintf(key, sizeof key, "%s", prefix);
|
||||
std::snprintf(name, sizeof name, "%s", label);
|
||||
if (vr::VROverlay()->CreateOverlay(key, name, &s.overlay) != vr::VROverlayError_None) {
|
||||
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()->SetOverlayFlag(s.overlay, vr::VROverlayFlags_IgnoreTextureAlpha, 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 curve = CurveTexture(64);
|
||||
static const auto roll = RollTexture(64);
|
||||
std::snprintf(key, sizeof key, "frametop.screen.%d.bar", index + 1);
|
||||
std::snprintf(name, sizeof name, "Screen %d: move", index + 1);
|
||||
s.bar = MakeChrome(key, name, BarTexture(false), 256, 24);
|
||||
auto chrome = [&](const char *part, const char *what, const std::vector<uint8_t> &px, int w, int h) {
|
||||
std::snprintf(key, sizeof key, "%s.%s", prefix, part);
|
||||
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);
|
||||
std::snprintf(key, sizeof key, "frametop.screen.%d.curve", index + 1);
|
||||
std::snprintf(name, sizeof name, "Screen %d: curve", index + 1);
|
||||
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);
|
||||
s.curveButton = chrome("curve", "curve", curve, 64, 64);
|
||||
s.rollButton = chrome("roll", "roll", roll, 64, 64);
|
||||
vr::VROverlay()->SetOverlayFlag(s.rollButton, vr::VROverlayFlags_SendVRDiscreteScrollEvents, true);
|
||||
std::snprintf(key, sizeof key, "frametop.screen.%d.resize", index + 1);
|
||||
std::snprintf(name, sizeof name, "Screen %d: resize", index + 1);
|
||||
s.handle = MakeChrome(key, name, corner, 64, 64);
|
||||
s.handle = chrome("resize", "resize", corner, 64, 64);
|
||||
if (s.floating) {
|
||||
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);
|
||||
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.
|
||||
RefreshPoses();
|
||||
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));
|
||||
}
|
||||
|
||||
// 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) {
|
||||
auto it = g_screens.find(index);
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
s.width = b->width, s.height = b->height;
|
||||
vr::HmdVector2_t scale = {float(s.width), float(s.height)};
|
||||
vr::VROverlay()->SetOverlayMouseScale(s.overlay, &scale);
|
||||
if (s.floating) {
|
||||
ApplyCrop(s);
|
||||
} else {
|
||||
vr::HmdVector2_t scale = {float(s.width), float(s.height)};
|
||||
vr::VROverlay()->SetOverlayMouseScale(s.overlay, &scale);
|
||||
}
|
||||
PlaceChrome(s); // the height changed
|
||||
std::printf("screen %d: %dx%d\n", index + 1, s.width, s.height);
|
||||
}
|
||||
vr::SharedTextureHandle_t handle = it->second;
|
||||
vr::Texture_t tex = {&handle, vr::TextureType_SharedTextureHandle, vr::ColorSpace_Gamma};
|
||||
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
|
||||
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) {
|
||||
vr::VREvent_t ev;
|
||||
// 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{};
|
||||
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) {
|
||||
case vr::VREvent_MouseMove:
|
||||
if (s.titleCarry) return; // KWin's pointer stays where the title bar was pressed
|
||||
e.type = FT_MOTION;
|
||||
e.x = ev.data.mouse.x;
|
||||
e.y = s.height - ev.data.mouse.y; // OpenVR's mouse origin is bottom left
|
||||
at();
|
||||
if (g_press.buttons) g_press.screen = index, g_press.x = e.x, g_press.y = e.y;
|
||||
break;
|
||||
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.button = LinuxButton(ev.data.mouse.button);
|
||||
e.pressed = ev.eventType == vr::VREvent_MouseButtonDown;
|
||||
e.x = ev.data.mouse.x;
|
||||
e.y = s.height - ev.data.mouse.y;
|
||||
at();
|
||||
if (!e.pressed && s.titleCarry) e.x = s.carryX, e.y = s.carryY, s.titleCarry = false;
|
||||
if (e.pressed) {
|
||||
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 {
|
||||
g_press.buttons &= ~ButtonBit(e.button);
|
||||
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;
|
||||
break;
|
||||
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;
|
||||
break;
|
||||
default:
|
||||
continue;
|
||||
return;
|
||||
}
|
||||
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.
|
||||
auto hover = [&](int k) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
// 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.
|
||||
while (vr::VROverlay()->PollNextOverlayEvent(s.rollButton, &ev, sizeof ev)) {
|
||||
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
|
||||
// state -> "ok <mode> <manual 0|1> <wrist deg> <gesture hand> <gesture deg>
|
||||
// <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
|
||||
// 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) {
|
||||
if (!g_vr) return (void)std::snprintf(reply, size, "error no SteamVR (--no-vr)");
|
||||
RefreshPoses();
|
||||
@@ -1457,7 +1766,8 @@ void ft_vr_command(const char *cmd, char *reply, int size) {
|
||||
float r[12];
|
||||
auto each = [&](const char *which, auto fn) -> bool { // "all" or a screen number
|
||||
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;
|
||||
}
|
||||
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)
|
||||
len += std::snprintf(reply + len, size - len, " %.5f", s->pinRel.m[k / 4][k % 4]);
|
||||
} 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)
|
||||
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);
|
||||
} else if (std::strncmp(cmd, "head", 4) == 0) {
|
||||
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");
|
||||
UpdateLasers();
|
||||
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) {
|
||||
if ((g_press.buttons & ButtonBit(BTN_LEFT)) && g_press.device != kNone && !IsHandController(g_press.device))
|
||||
g_press.upAt = g_tick + 9;
|
||||
|
||||
@@ -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.
|
||||
void ft_vr_screen_create(int index, double metres, int count);
|
||||
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
|
||||
// can't import it.
|
||||
bool ft_vr_screen_present(int index, const void *key, const struct ft_dmabuf *buf);
|
||||
|
||||
@@ -30,7 +30,7 @@ for var in $(compgen -e); do
|
||||
done
|
||||
|
||||
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
|
||||
[ -f "$conf" ] && . "$conf"
|
||||
backend=${FT_BACKEND:-$BACKEND}
|
||||
@@ -39,6 +39,13 @@ width=${FT_WIDTH:-$WIDTH}
|
||||
height=${FT_HEIGHT:-$HEIGHT}
|
||||
phys_width=${FT_PHYS_WIDTH:-$PHYS_WIDTH}
|
||||
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
|
||||
# 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.
|
||||
@@ -80,10 +87,10 @@ if [ "${1:-}" != --inner ]; then
|
||||
# Plasma stay on the host and connect to its socket.
|
||||
socket=ft-screens-0
|
||||
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
|
||||
"$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; }
|
||||
trap stop_screens EXIT
|
||||
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.
|
||||
cat > "$runtime/bin/kwin_wayland_wrapper" <<EOF
|
||||
#!/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
|
||||
chmod +x "$runtime/bin/kwin_wayland_wrapper"
|
||||
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 &
|
||||
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
|
||||
@@ -7,6 +7,8 @@ WIDTH=1920 # gamescope: pixels per screen (at most 1920x1080 worth)
|
||||
HEIGHT=1080
|
||||
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)
|
||||
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_SENSITIVITY=0.03 # degrees per mouse count
|
||||
POINTER_IDLE=30 # seconds without mouse use before the controllers get their laser back
|
||||
|
||||
Reference in new issue
Block a user