diff --git a/float/ft_floatd.py b/float/ft_floatd.py index caf5c36..3137822 100644 --- a/float/ft_floatd.py +++ b/float/ft_floatd.py @@ -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}", diff --git a/layout/ft_layout.py b/layout/ft_layout.py index 601a817..50b42d1 100755 --- a/layout/ft_layout.py +++ b/layout/ft_layout.py @@ -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", ""))]) diff --git a/pointer/helper/ft-pointer.cpp b/pointer/helper/ft-pointer.cpp index bba6729..a41b5e7 100644 --- a/pointer/helper/ft-pointer.cpp +++ b/pointer/helper/ft-pointer.cpp @@ -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) { diff --git a/screens/compositor.c b/screens/compositor.c index ac48cfc..10affcc 100644 --- a/screens/compositor.c +++ b/screens/compositor.c @@ -13,13 +13,15 @@ // scale first ("scale ", 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; diff --git a/screens/test/headless.sh b/screens/test/headless.sh index d2f5bc1..7870fb5 100755 --- a/screens/test/headless.sh +++ b/screens/test/headless.sh @@ -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 '' e.g. toplevels, "input 1 down 400 20" # headless.sh kd 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[@]}" \ diff --git a/screens/vr.cpp b/screens/vr.cpp index 04c4af9..755f058 100644 --- a/screens/vr.cpp +++ b/screens/vr.cpp @@ -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 #include +#include +#include #include #include #include @@ -59,6 +70,7 @@ extern char **environ; // for posix_spawn #include #include #include +#include #include #include #include @@ -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 Controls() const { return {bar, curveButton, rollButton, handle}; } - std::array 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 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 Controls() const { + return {bar, curveButton, rollButton, handle, dockButton, closeButton}; + } + std::array All() const { + return {overlay, bar, curveButton, rollButton, handle, dockButton, closeButton}; + } }; std::map g_screens; std::map g_imports; @@ -348,6 +393,29 @@ std::vector RollTexture(int n) { DiscRim); } +std::vector 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 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 &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 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 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 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(&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 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 &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 // " +// Floating windows (from ft-floatd; is the spare output's number, after the screens): +// float 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; diff --git a/screens/vr.h b/screens/vr.h index 876fc1e..7b32186 100644 --- a/screens/vr.h +++ b/screens/vr.h @@ -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); diff --git a/session/frametop-session.sh b/session/frametop-session.sh index 068cbee..813771c 100755 --- a/session/frametop-session.sh +++ b/session/frametop-session.sh @@ -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 diff --git a/session/frametop.conf.example b/session/frametop.conf.example index 3ea46cb..194d4eb 100644 --- a/session/frametop.conf.example +++ b/session/frametop.conf.example @@ -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