Any desktop app can float in VR in a panel of its own, like SteamVR’s floating windows, while it stays part of the Frametop desktop. Drag and drop, the clipboard, and focus keep working between floating windows and the screens.
ft-float launch or ft-float run, or from a profile (profiles.md).dock_all action docks every floating window.How KWin behaves underneath all this, and what the KWin script does about it, is in design.md.
These were decided (see the table) but aren’t built:
FLOAT_SLOTS and FLOAT_MARGIN are set in ~/.config/frametop.conf.The numbers are cited in the code, so they stay as they are. Struck-out text was replaced by a later decision.
| # | Question | Decision |
|---|---|---|
| 1 | How many windows can float at once | 8 spare outputs by default, configurable with FLOAT_SLOTS (at most 16); a change needs a desktop restart. A Display Settings control isn’t built |
| 2 | Menus and dropdowns | Each floating output has a margin around the window. The panel shows only the window, and each open popup gets a small overlay of its own, cut from the same buffer |
| 3 | Tearing off | Not built. Drag the title bar past a screen’s edge and let go in the air, with a small dead zone past the edge |
| 4 | Docking by dragging | Not built. Push the window flush against a screen (within about 10 cm), with the landing spot highlighted, and let go |
| 5 | Visibility | Floating windows follow the same rules as the screens: the hide hotkey, the visibility modes, and the games rule |
| 6 | Windows a floating app opens | They float too |
| 7 | Launching floating from the headset | |
| 8 | Build order | Not kept here: it only set the order of the work |
| 9 | Show Desktop (Meta+D) | Floating windows stay. Not built |
| 10 | Frametop Apps and visibility | |
| 11 | Window frame | KWin’s title bar and border stay. Frametop’s bar, close, and “back to desktop” are extras |
| 12 | Resizing | The window’s own edges and Frametop’s corner tab both change the size in pixels at the same density; the output follows |
| 13 | Margin | 300 px on each side, configurable (FLOAT_MARGIN) |
| 14 | All spares in use | The window stays on the screens, with a notification |
| 15 | Where a floating app’s new windows go | Not built: where that app’s windows went last time, otherwise to the parent’s right, curving around you. For now, a new window that opens on a floating window’s output floats a little in front of it |
| 16 | Where the code is written | Not kept here: it was about the work, not about Frametop |
| 17 | Size when docked by dragging | Not built (see 4): the current floating size in pixels, shrunk to fit the screen |
| 18 | Bigger text | A scale for each window (KWin’s output scale): Meta+scroll over the window. Remembered for each app. +/- buttons on its bar aren’t built |
| 19 | Switching to a window you can’t see | It’s focused. Not built: a glow at the edge of your view that points to it, and a setting that moves it in front of you |
| 20 | Full screen | The window fills its own panel. The margin drops to zero while it’s full screen, and the panel keeps its size and place |
| 21 | Named layouts | |
| 22 | The float key | One toggle: it floats a window, or docks it if it already floats. The input relay owns it (float_toggle), Meta+Shift+F by default, rebindable in Frametop Input Settings and mappable to mouse and controller buttons. KWin has no shortcut of its own for it, so one press can’t fire twice |
| 23 | Which window the key acts on | The window under the desktop’s pointer; the active window if there’s none there (the wallpaper, the taskbar) |
| 24 | Docking everything | A dock_all action, with no default binding |
| 25 | A button on every window | A float button left of Close in the title bar, from Frametop’s own QML window decoration, made to look like Breeze. It shows a dock icon on floating windows. Apps that draw their own title bar (Chromium, Electron, GTK) use the key |
| 26 | Launching one app floating | “Launch as Standalone” in the right-click menu of every app in the Application Launcher and the taskbar, from copies of the apps’ desktop files that only the Frametop desktop reads. It replaces the Frametop Apps entry (7, 10) |
| 27 | Profiles | Named layouts become profiles: the screens’ places, which screens show, and the apps and their windows, floating or not. See docs/profiles.md |
Also: floating windows get the wrist pin, the head pin, and pass-through (pointer-ignore) like screens. Every gesture works with the controllers as well as the 3D mouse. A window launched floating uses the primary screen’s density. VNC shows only the primary screen.
Drag and drop and the clipboard only work between windows of the same compositor. A Wayland window can’t move from one compositor to another. So a floating window has to stay a KWin window.
ft-screens already shows each KWin output as a panel. It sets the output’s size with an xdg_toplevel configure, and KWin resizes the output to match. So a floating window gets an output of its own, sized to fit it, and ft-screens shows that output as a panel with its own controls. To KWin this is an ordinary desktop with more monitors. Dragging between two floating windows is the same as dragging between two monitors, which KWin already handles. ft-screens moves the pointer between panels in the middle of a drag: handle_vr_event moves pointer focus to another KWin window even while a button is held.
Alternatives considered:
SetOverlayTextureBounds). That needs only one extra output, with no copies. But an 8K canvas uses about 128 MB per buffer, with two or three buffers in KWin’s swapchain. It would also have to repack windows whenever one resized, full screen would fill the whole canvas, and every window would share one scale. It was the fallback in case per-window outputs didn’t work.zkde_screencast stream_window, over PipeWire). This adds copies and latency, and the window still needs a real place in KWin’s layout to receive input. Rejected.KWin’s nested backend opens its outputs at start (--output-count). The session starts KWin with the screen count plus FLOAT_SLOTS outputs (default 8, at most 16). ft-floatd turns off the spares nothing floats on with kscreen-doctor once it starts. Floating a window enables a spare, and docking the window disables it again. FLOAT_SLOTS limits how many windows can float at once, and changing it means restarting the desktop. How KWin’s nested backend treats disabled outputs, and why its virtual outputs can’t be used instead, is in design.md.
ft-screens creates a screen for each toplevel in the order they appear, and indexes its settings by that order. Spares come after the screens, so they get indices SCREENS and up. Their panels are hidden while their output is disabled.
KWin script "frametop-float" ft-floatd (host, Python) ft-screens
window events, moves, menus ── D-Bus ──▶ window ↔ output ↔ panel table ── @ft_screens ──▶ panels, controls,
runs commands ◀─ long poll ─ spare outputs (kscreen-doctor) ◀─ @frametop_float ─ lasers, catcher
frametop-float (float/frametop-float.js). ft-floatd loads it into the desktop’s KWin over D-Bus (org.kde.kwin.Scripting). A script keeps working across KWin updates. A C++ effect would have to match the host’s exact KWin build, and the build container is Fedora, not SteamOS. The script watches windows (windowAdded/windowRemoved, frameGeometryChanged, outputChanged, interactiveMoveResizeStarted/Finished, fullScreenChanged, maximizedChanged, minimizedChanged, keepBelowChanged, windowActivated) and the outputs (screensChanged). It runs commands: move a window to an output, set its geometry, put it on all virtual desktops, and restore it. It adds “Float in VR” (“Back to Desktop” on a floating window) to the window menu (registerUserActionsMenu). It registers no shortcut: the float key belongs to the input relay. KWin scripts can call D-Bus but can’t serve it, so commands come back through a long poll. The script calls ft-floatd’s NextCommand, which answers when a command is ready, and then the script calls it again, or after 30 seconds without an answer. It also keeps KWin’s placement memory from moving windows (see design.md).float/ft-floatd, Python). The host has dbus-python and PyGObject. It runs inside the desktop’s Plasma session, started from its autostart. It owns org.frametop.Float on the session’s private bus, and it keeps the table of which window is on which output and panel. It enables and disables spare outputs and sets their scale and position with kscreen-doctor, and their size through ft-screens. It tells ft-screens where each floating window goes and tells the script which window goes where. It launches apps floating, opens profiles’ apps, and remembers each app’s placement and scale, keyed by desktop file name. Commands come in on @frametop_float, from ft-float, the input relay, and ft-screens.MAX_SCREENS (screens and spares together) is 24. Commands arrive on @ft_screens. Events go out to @frametop_float from an unbound socket, the same way ft-screens talks to the input relay.FLOAT_SLOTS to KWin’s output count, starts ft-floatd from the desktop’s autostart, installs Frametop’s window decoration and chooses it in the session’s kwinrc, and writes the Launch as Standalone copies of the apps’ desktop files.WL-<SCREENS> and up) to ft-floatd, enabled or not.float_toggle and dock_all send float pointer and dock all to ft-floatd.Program names stay within 15 characters (ft-floatd). Overlay keys are frametop.float.N and frametop.float.N.bar, and so on; a floating window’s popups and dialogs are frametop.float.N.sub.K.
FLOAT_MARGIN, default 300 px). KWin keeps a Wayland popup inside its parent’s output, so the margin gives menus and dropdowns room past the window’s edges. X11 apps place their own menus within the monitor, so the same applies. Enabled spares sit apart from the screens and from each other in KWin’s layout, so nothing spills from one to the next. Memory: a 1600 × 1000 window with a 300 px margin is about 14 MB per buffer, 42 MB for three.SetOverlayTextureBounds and maps mouse positions through the crop. Each open popup or dialog gets a small overlay of its own, cut from the same buffer and placed a few millimetres in front of the window, so the main panel never changes size. KWin tells scripts about popups as windows of their own (windowAdded with popupWindow), so the script reports their rectangles.carry); ft-floatd puts back any few pixels the window slipped before that. Meanwhile ft-screens carries the panel with the pressing device, the same way the bar does: it follows rigidly, scroll pushes and pulls, and the 3D mouse’s right-drag tilts. When the button comes up, KWin gets the release at the press point. The bar under the panel works too.transientFor) show as small overlays over it. Another window of a floating app that opens on its output floats too, a little in front of it. Any other window that opens on a floating window’s output goes to the first screen that shows. When every spare is in use, the window stays on the screens and a notification says so.The design for decision 3:
move-start <output> <window> <rect>.SetOverlayTextureBounds, no copy). It’s at the screen’s pixel density and distance, on the laser, facing you, with the point you grabbed under the laser. The ghost takes mouse input, so SteamVR’s laser lands on it and the release comes to ft-screens.Decisions 22 to 25.
float_toggle, bound to Meta+Shift+F unless the rules file says otherwise (a rules file with no key_bindings gets that default; one with its own list, even an empty one, doesn’t). It can be rebound or removed in Frametop Input Settings, and mapped to a mouse button or a Frame controller button like any other action. The relay takes the combination before it reaches the desktop and sends float pointer to ft-floatd, which asks the script for the window under KWin’s pointer (workspace.cursorPos, top of workspace.stackingOrder, popups and dialogs counting as their parent). With none there, the wallpaper or the taskbar, it’s the active window. KWin’s pointer is where the 3D mouse or a laser last was on a Frametop panel. A window that floats docks; any other floats. The KWin script has no shortcut of its own (ft-floatd removes one that an older script registered), so one press can’t float a window and dock it again.dock_all (no default binding) sends dock all, which docks every floating window where it came from.decoration/, installed to ~/.local/share/kwin/decorations/kwin4_decoration_qml_frametop, chosen in the session’s kwinrc only, so Desktop Mode keeps Breeze; decoration/apply.sh switches the running desktop to it or back to Breeze). It’s drawn to look like Breeze, with a float button left of Close. The button calls requestToggleKeepBelow(), the one window request a decoration can make that has no visible effect here, and the KWin script reads the change: keep-below set on a window on the screens floats it, cleared on a floating window docks it. The script keeps keep-below set on every floating window, however it was floated, so the button shows its dock icon there. A window alone on its own output loses nothing by being kept below (only the wallpaper is under it). If the window can’t float (every spare is in use), the script clears the flag again. Keep Below Others in a window’s menu does the same as the button.float/ft_apps.py). They’re written to ~/.local/share/frametop/apps/applications from every desktop file in XDG_DATA_DIRS: by the session script before Plasma starts, and by ft-floatd whenever an app is installed, changed, or removed. The session puts ~/.local/share/frametop/apps first in XDG_DATA_DIRS. Plasma’s app cache is keyed by those directories, so Desktop Mode never sees the copies. Desktop files in ~/.local/share/applications come before every data dir, so an app you’ve customized there keeps your copy and has no Launch as Standalone. The action runs ft-float launch <desktop file name>.ft-float run <command> and ft-float launch <app.desktop> start an app and float its first window. ft-floatd records the process it started, and new windows are matched by PID, including child processes. Some single-instance apps (Firefox, D-Bus-activated apps) open the window from a process that was already running. Those are matched by desktop file name. Either way, the window has to show up within 30 seconds.docs/profiles.md). There’s no separate Frametop Apps entry or picker.~/.config/frametop-float.json. It’s kept whenever one of the app’s windows stops floating. With nothing remembered, the window opens in front of you, at the primary screen’s density, 0.8 to 2 m away. A profile’s own placement wins when the profile opens the app.KWin handles the protocols: Wayland, X11 through Xwayland, and the portal’s file transfer. Frametop has to get the pointer right between panels.
FT_LEAVE even with a button held. A release in empty space would never reach KWin, and the drag or move would stay stuck until the next click. So while a button is held on a Frametop panel and the laser leaves all of them, ft-screens puts an invisible catcher overlay on the laser. A release on the catcher releases in KWin wherever the pointer last was. Dropping in a gap cancels, just as dropping outside any window does. The pointer helper also tells ft-screens when the mouse’s left button comes up (“up”), in case the catcher misses it. This also covers window moves and drags on the screens that end off a panel.vrcmd loops.vrcmd --overlays in the background, so a new floating panel appears in its next listing.