Floating windows: keep KWin's placement memory out

KWin's PlacementTracker keeps each window's geometry, full screen and
maximized state per layout of the outputs, and puts windows back when a
layout it has seen comes back. A spare output resizes after its window,
so resizing a floating window back to an earlier size (or changing its
scale, or full screen) made the window and its output flip forever, and
floating or docking one window could move others onto or off a spare.

The KWin script now keeps where each window belongs, reports nothing
while KWin changes the outputs, and on screensChanged puts floating
windows back (and the screens' windows when only spares changed),
cancelling KWin's requests before the app sees them. A size asked for is
held for a second against late answers.

With that: a launched app and a profile get their remembered scale back,
scale steps keep the size in pixels, ft-floatd waits for the end of an
edge resize before resizing the output (KWin cancels the resize on any
output change), a window taken over after a restart keeps its app and
panel density, and `ft-float float ID` asks the script for the window's
current place.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
DeeJanuzandClaude Opus 5.5 committed 2026-10-01 08:50:38 -06:00
1 parent 81b64302aa
commit c3785bb46e
4 files changed
+242 -33

No files matched your search

+6 -6
View File
@@ -11,14 +11,13 @@ Built so far (2026-09-29; the KWin side tested on the headless test desktop, `sc
- The 3D mouse's drag lock crosses onto other Frametop panels (not while carrying one).
- The float key (2026-09-30): the input relay's `float_toggle` (Meta+Shift+F by default) and `dock_all`, through `float pointer` and `dock all` on ft-floatd. Tested on the headless desktop.
- The title bar button (2026-09-30, branch `float-titlebar`): `decoration/` (Frametop's QML window decoration, installed by the session script; `decoration/apply.sh` switches the running desktop to it or back to Breeze), and keep-below as the floating flag in the script. Tried on the live desktop: the button floats Dolphin and docks it again, the float key and `dock all` keep the flag in step, and maximized windows look right.
- Launching floating (2026-09-30, branch `float-launch`): `ft-float launch APP` and `ft-float run COMMAND`, matching the new window by process (or a child) or by desktop file name for 30 seconds, and each app's remembered place (pose relative to the primary screen, size in pixels, scale) in `~/.config/frametop-float.json`, kept whenever one of its 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. Launch as Standalone (`float/ft_apps.py`): the session writes the desktop file copies and puts them first in `XDG_DATA_DIRS`; ft-floatd rewrites them when apps change. Tried on the live desktop: Dolphin launched floating, came back at its remembered pose after closing, and Konsole floated from `ft-float run`; the right-click entry needs a desktop start to show. The remembered scale is kept but not applied yet (see Known problems).
- Launching floating (2026-09-30, branch `float-launch`): `ft-float launch APP` and `ft-float run COMMAND`, matching the new window by process (or a child) or by desktop file name for 30 seconds, and each app's remembered place (pose relative to the primary screen, size in pixels, scale) in `~/.config/frametop-float.json`, kept whenever one of its 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. Launch as Standalone (`float/ft_apps.py`): the session writes the desktop file copies and puts them first in `XDG_DATA_DIRS`; ft-floatd rewrites them when apps change. Tried on the live desktop: Dolphin launched floating, came back at its remembered pose after closing, and Konsole floated from `ft-float run`; the right-click entry needs a desktop start to show. The remembered scale is applied since 2026-10-01 (next item).
- Screens hidden one at a time (2026-09-30, branch `screen-hide`): ft-screens' `conceal`/`reveal`/`concealed`, `ft-layout hide|show N`, Screens shown in Display Settings, and ft-floatd floats a new window that opens on a hidden screen (and puts strays on a screen that shows).
- KWin's placement memory kept out (2026-10-01, branch `float-tracker`): the scale loop below turned out to be KWin's PlacementTracker, and resizing a floating window back to an earlier size set it off too. The script now puts windows back after KWin changes the outputs (see A floating window). With that, a launched app and a profile get their remembered scale back, a resize by the window's edge works (the output follows when the drag ends), and full screen on a floating window no longer flips. Tried on the live desktop with the headset off: resizing back and forth, scale steps up and down, launching at a remembered scale of 1.2, a profile putting a window's scale back, floating and docking one window while others stay put, full screen on and off, and an edge drag.
Not built yet: phase 2 (the ghost, tear-off by dragging, push-flush docking), the rest of phase 3 (new windows of a floating app placed where that app's last went), and phase 4.
Known problems:
- **Changing a floating window's scale can loop** (Meta+scroll, or the scale buttons). Seen on the live desktop on 2026-09-30 with Dolphin at 1.2: the window flipped between 1187 and 1424 logical pixels wide, and its output between 1687 and 1925, several times a second, until it was closed. The cause looks like this: when an output resizes, KWin resizes a window on it to keep its distance from both edges. `follow` then resizes the output from a window size that's already stale. The next output change resizes the window again, and the two stay one step out of phase. To fix: have ft-floatd own the window's size while it floats. After resizing the output, treat a window change by the same amount as KWin's echo and put the window back, instead of following it. Until then, a launched app doesn't get its remembered scale back.
Known problems: none open. The scale loop seen on 2026-09-30 (Dolphin flipping between 1187 and 1424 logical pixels wide, its output between 1687 and 1925) was KWin's placement memory, fixed on 2026-10-01.
Build order from 2026-09-30, each merged into `experimental` when done: (1) the float key and docking everything, (2) the title bar button, (3) phase 3, (4) hiding screens one at a time, (5) profiles (`docs/profiles.md`).
@@ -115,7 +114,8 @@ Program names stay within 15 characters (`ft-floatd`). Overlay keys are `frameto
- **Size and scale.** The panel's width is the window's pixel width times the source screen's metres per pixel, so text stays the same size in VR. A window launched floating uses the primary screen's density. Each window also has a scale (KWin's output scale), changed with Meta+scroll over the window or +/- on its bar and remembered for each app. A bigger scale makes the content bigger at the same panel size.
- **Window state.** An ordinary window, not maximized, placed inside its output with the margin around it, and set to show on all virtual desktops. It keeps its title bar and border. Apps that draw their own title bar (GTK, Chromium) keep theirs.
- **Moving.** Press the title bar. KWin starts an interactive move and the script reports it. ft-screens then stops forwarding pointer motion to KWin, so KWin's pointer stays at the press point and the window moves by nothing. Meanwhile ft-screens carries the panel with the pressing device, the same way the bar does today: 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.
- **Resizing.** The window's own edges (inside the margin, so KWin's resize works as on the desktop) and Frametop's corner tab both change the window's size in pixels at the same density, so the app lays itself out again. ft-floatd resizes the output to keep the margin, and the panel grows or shrinks around the window's top-left corner. A screen's tab only scales the panel. Resizing is throttled to about 20 updates a second, with a minimum of 320 × 200, like screens.
- **Resizing.** The window's own edges (inside the margin, so KWin's resize works as on the desktop) and Frametop's corner tab both change the window's size in pixels at the same density, so the app lays itself out again. ft-floatd resizes the output to keep the margin, and the panel grows or shrinks around the window's top-left corner. KWin ends a resize by the window's edge whenever an output changes, so during one the panel follows the window and the output only when the drag ends: the margin is the room to grow until then. A screen's tab only scales the panel. Resizing is throttled to about 20 updates a second, with a minimum of 320 × 200, like screens.
- **KWin's placement memory.** KWin keeps each window's geometry, full screen and maximized state for each layout of the outputs (every enabled output's name and geometry: its `PlacementTracker`), and when the outputs come back to a layout it has seen, it puts every window back as it was in it. That's for plugging monitors in and out. Here a spare output changes size after its window does, so what KWin keeps for a spare's size is the window's next size, and going back to an earlier size made the window and its output flip between two sizes for good. Full screen did the same, and floating or docking one window could move others, even onto a spare or off one. So the KWin script keeps where each window belongs (where ft-floatd put it, or where it went outside an output change), reports nothing while KWin changes the outputs, and once KWin is done (`screensChanged` comes after its restore) puts floating windows back, and the screens' windows too when only spares changed. KWin's resize request hasn't reached the app by then, so the app never sees it. A size asked for is held for a second, since an app can still answer an older request, and then the script takes the size the window has.
- **Full screen.** The window fills its own panel: the margin drops to zero while it's full screen, and the output is the panel's size in pixels. The panel keeps its size and place. On leaving full screen, the margin comes back.
- **Buttons.** The close button closes the window. "Back to desktop" docks it where it came from.
- **Minimize.** Minimizing, from the title bar or the taskbar, hides the panel, and restoring it shows the panel again. Floating windows stay in the desktop's taskbar and in Alt+Tab.
@@ -229,6 +229,6 @@ The drag proxy, the glow toward a window activated out of view (and the setting
## Risks
- A SteamOS update can change KWin's script API or its nested backend. The script and the output handling are the parts to recheck after one.
- A SteamOS update can change KWin's script API or its nested backend. The script and the output handling are the parts to recheck after one, and KWin's placement memory with them (the script undoes it, see A floating window).
- GPU memory: each floating output has its own swapchain of two or three buffers, including the margin. The Frame has 16 GB shared, with about 4 GB free in normal use (2026-09-29).
- Frame pacing with many panels hasn't been measured (already an open question in design.md). Each output is a separate render pass in KWin.
+1 -1
View File
@@ -41,7 +41,7 @@ So a "Work" profile can put three screens around you with a browser, two termina
```
- `screen` is 1-based, as everywhere in Frametop. `rect` is the window's frame in KWin's logical units, relative to its screen's output, so it survives the screens being arranged differently.
- A floating window's place (`rel`) is its panel's centre and axes in the frame of the primary screen's panel, the same way ft-floatd remembers each app's place. The screens go relative to your head when the profile is applied, and the floating windows follow them. `mpp` is its density in metres per pixel. Its scale is kept but not yet applied (see Known problems in `docs/floating-windows.md`).
- A floating window's place (`rel`) is its panel's centre and axes in the frame of the primary screen's panel, the same way ft-floatd remembers each app's place. The screens go relative to your head when the profile is applied, and the floating windows follow them. `mpp` is its density in metres per pixel, and `scale` its scale, put back with the rest.
- Renaming or deleting a layout renames or deletes its profile entry with it.
## How it works
+196 -8
View File
@@ -13,6 +13,10 @@ let screens = 0; // outputs WL-0 .. WL-<screens - 1> are screens; the rest
let polling = false;
const watched = {}; // id -> true once its signals are connected
let marking = false; // the script itself is setting keep-below (see mark)
const settled = {}; // id -> {output, frame, fullScreen, maximized}: where a window belongs (see putBack)
const held = {}; // id -> {w, h, until, asked}: a size asked for, for a second (see hold)
const moved = {}; // id -> true, or "back" once put back: it moved while KWin changed the outputs
let layout = "", layoutOutputs = {}, layoutSince = 0; // the outputs at the last screensChanged
function send(ev) {
callDBus(SERVICE, PATH, IFACE, "Event", JSON.stringify(ev));
@@ -67,20 +71,184 @@ function report(type, w) {
send(ev);
}
// KWin's placement memory (its PlacementTracker) keeps each window's geometry for each layout of
// the outputs (every enabled output's name and geometry), and when the outputs come back to a
// layout it has seen, it puts the windows back where they were in it. That's for plugging monitors
// in and out, and it does harm here. A spare output changes size after its window does, so what
// KWin keeps for a spare's size is the window's next size: resizing a floating window back to a
// size it had set off an endless flip between two sizes. And floating or docking one window could
// move others, even onto a spare or off one. So the script keeps where each window belongs
// (settled), tells ft-floatd nothing while KWin changes the outputs, and once KWin is done
// (screensChanged comes after its restore) puts the floating windows back, and the screens' windows
// too when only spares changed.
function outputsNow() {
const all = workspace.screens, out = {};
for (let i = 0; i < all.length; ++i) {
const g = all[i].geometry;
out[all[i].name] = g.x + "," + g.y + " " + g.width + "x" + g.height;
}
return out;
}
function keyOf(outputs) {
return Object.keys(outputs).sort().map(n => n + "=" + outputs[n]).join(" ");
}
function takeLayout() {
layoutOutputs = outputsNow();
layout = keyOf(layoutOutputs);
layoutSince = 0;
}
// KWin is changing the outputs: they differ from the last screensChanged.
function changingOutputs() {
if (keyOf(outputsNow()) === layout) {
layoutSince = 0;
return false;
}
if (!layoutSince) {
layoutSince = Date.now();
} else if (Date.now() - layoutSince > 2000) {
takeLayout(); // screensChanged should have come by now: don't stay quiet for good
return false;
}
return true;
}
function settle(w) {
if (w.output) {
settled[String(w.internalId)] = {output: w.output.name, frame: rect(w.frameGeometry), fullScreen: w.fullScreen};
}
}
// ft-floatd put the window here: it's where it belongs now.
function expect(w, output, c, fullScreen) {
settled[String(w.internalId)] = {output: output, frame: {x: c.x, y: c.y, w: c.w, h: c.h}, fullScreen: fullScreen};
hold(w, c.w, c.h);
}
// A size asked for (by ft-floatd, or by the script putting a window back) comes in when the app
// answers, and until then the app can still answer older requests: one KWin's restore made, or,
// just after it opened, its own. For a second, the script asks again instead of taking those;
// then it takes the size the window has (an app can refuse a size, below its minimum).
const holdTimer = new QTimer();
holdTimer.singleShot = true;
holdTimer.timeout.connect(() => {
const now = Date.now();
Object.keys(held).forEach(id => {
const h = held[id];
if (h.until > now) return;
delete held[id];
const w = byId(id);
if (!h.asked || !w || w.deleted) return; // (nothing held back: nothing to tell)
settle(w);
if (isSpare(w.output)) report("geometry", w);
});
if (Object.keys(held).length) holdTimer.start();
});
function hold(w, width, height) {
held[String(w.internalId)] = {w: width, h: height, until: Date.now() + 1000};
holdTimer.interval = 1100;
holdTimer.start();
}
// A size change while a size is held: true when it isn't that size (the script asked again).
// (ft-floatd's sizes can be fractional, the window's are whole: within a pixel is the same.)
function holding(w) {
const id = String(w.internalId), h = held[id], s = settled[id];
if (!h) return false;
const g = w.frameGeometry;
if (!s || h.until < Date.now() || w.move || w.resize || (Math.abs(g.width - h.w) < 1 && Math.abs(g.height - h.h) < 1)) {
delete held[id];
return false;
}
w.frameGeometry = {x: s.frame.x, y: s.frame.y, width: h.w, height: h.h};
h.asked = true;
return true;
}
// Runs while KWin's output change still counts as going on (nothing reported), so the steps on
// the way don't reach ft-floatd: told only where the window ends up (see reportMoved).
function putBack(w, screensChanged) {
const id = String(w.internalId), s = settled[id];
if (w.deleted || !s || screens === 0 || !marked(w)) return;
const o = outputByName(s.output);
// KWin's restore sets full screen (and maximized) as it was in that layout too: with a
// floating window that flipped forever, its output changing size with it. Ask for the
// state it had: KWin's request hasn't reached the app yet, so it never sees it.
w.fullScreen = s.fullScreen;
if (o && isSpare(o) && !s.fullScreen) w.setMaximize(false, false);
// Not where KWin had to move it: its output went, a screen changed, or it's full screen or
// maximized (KWin fits those to their output).
const back = o && !s.fullScreen && !w.fullScreen && !w.move && !w.resize && !s.maximized
&& (isSpare(o) || (!screensChanged && !isMaximized(w)));
if (!back) return;
if (!w.output || w.output.name !== s.output) workspace.sendClientToScreen(w, o);
w.frameGeometry = {x: s.frame.x, y: s.frame.y, width: s.frame.w, height: s.frame.h};
if (isSpare(o)) hold(w, s.frame.w, s.frame.h);
// Moved during the change (by KWin, by the lines above, or the size ft-floatd asked for came
// in): report where it is, and keep settled as it is.
if (moved[id]) moved[id] = "back";
}
// After an output change: tell ft-floatd where the windows that moved during it are now.
function reportMoved(w) {
const id = String(w.internalId), s = settled[id], how = moved[id];
if (!how) return;
delete moved[id];
if (w.deleted) return;
if (how !== "back") settle(w);
if (!w.output || !s || w.output.name !== s.output) {
report("output", w);
mark(w);
} else if (isSpare(w.output)) {
report("geometry", w);
}
}
workspace.screensChanged.connect(() => {
const before = layoutOutputs, now = outputsNow();
if (keyOf(now) === layout) return;
let screensChanged = screens === 0;
Object.keys(Object.assign({}, before, now)).forEach(name => {
const m = /^WL-(\d+)$/.exec(name);
if (before[name] !== now[name] && !(m && parseInt(m[1]) >= screens)) screensChanged = true;
});
const all = workspace.windowList();
all.forEach(w => putBack(w, screensChanged));
takeLayout();
all.forEach(reportMoved);
});
// Floating windows, and popups and dialogs on a spare output: tell ft-floatd about changes.
function watch(w) {
const id = String(w.internalId);
if (watched[id]) return;
watched[id] = true;
const onSpare = () => isSpare(w.output);
w.frameGeometryChanged.connect(() => { if (onSpare()) report("geometry", w); });
w.outputChanged.connect(() => { report("output", w); mark(w); });
w.frameGeometryChanged.connect(() => {
if (changingOutputs()) {
moved[id] = true;
return;
}
if (holding(w)) return;
settle(w);
if (onSpare()) report("geometry", w);
});
w.outputChanged.connect(() => {
if (changingOutputs()) {
moved[id] = true;
return;
}
if (!held[id]) settle(w); // (held: the place asked for is settled already)
report("output", w);
mark(w);
});
w.keepBelowChanged.connect(() => keepBelowChanged(w));
w.interactiveMoveResizeStarted.connect(() => {
if (onSpare()) send({ev: "move-start", id: id, move: w.move, resize: w.resize, frame: rect(w.frameGeometry)});
});
w.interactiveMoveResizeFinished.connect(() => { if (onSpare()) report("move-end", w); });
w.fullScreenChanged.connect(() => { if (onSpare()) report("fullscreen", w); });
w.fullScreenChanged.connect(() => {
if (changingOutputs()) {
moved[id] = true;
return;
}
if (settled[id]) settled[id].fullScreen = w.fullScreen;
if (onSpare()) report("fullscreen", w);
});
w.minimizedChanged.connect(() => { if (onSpare()) report("minimized", w); });
w.maximizedChanged.connect(() => {
// A floating window stays an ordinary window: its output is its size plus a margin.
@@ -90,16 +258,25 @@ function watch(w) {
workspace.windowAdded.connect(w => {
watch(w);
settle(w);
report("added", w);
});
workspace.windowRemoved.connect(w => {
send({ev: "removed", id: String(w.internalId)});
delete watched[String(w.internalId)];
const id = String(w.internalId);
send({ev: "removed", id: id});
delete watched[id];
delete settled[id];
delete held[id];
delete moved[id];
});
workspace.windowActivated.connect(w => {
if (w && isSpare(w.output)) send({ev: "activated", id: String(w.internalId)});
});
workspace.windowList().forEach(watch);
takeLayout();
workspace.windowList().forEach(w => {
watch(w);
settle(w);
});
// Keep-below means "floating" in the Frametop desktop. The title bar's float button (Frametop's
// window decoration, decoration/) is the Keep Below button, so setting the flag on a window on
@@ -178,14 +355,22 @@ function run(c) {
if (!o) break;
if (w.fullScreen && !c.keepFullScreen) w.fullScreen = false;
w.setMaximize(false, false);
expect(w, o.name, c, w.fullScreen && !!c.keepFullScreen);
workspace.sendClientToScreen(w, o);
w.frameGeometry = {x: c.x, y: c.y, width: c.w, height: c.h};
if (c.onAllDesktops !== undefined) w.onAllDesktops = c.onAllDesktops;
if (c.maximized) w.setMaximize(true, true);
if (c.maximized) {
// Maximized: KWin picks the size, and the place above is only where it goes.
delete held[c.id];
settled[c.id].maximized = true;
w.setMaximize(true, true);
}
break;
}
case "geometry":
if (w) w.frameGeometry = {x: c.x, y: c.y, width: c.w, height: c.h};
if (!w) break;
expect(w, settled[c.id] ? settled[c.id].output : (w.output ? w.output.name : ""), c, w.fullScreen);
w.frameGeometry = {x: c.x, y: c.y, width: c.w, height: c.h};
break;
case "close":
if (w) w.closeWindow();
@@ -203,6 +388,9 @@ function run(c) {
workspace.windowList().forEach(w => report("window", w));
send({ev: "reported", token: c.token});
break;
case "request-float": // ft-float float ID: float it, if it isn't floating
if (w && !isSpare(w.output)) requestFloat(w);
break;
case "request-active": // ft-float float|dock active
requestFloat(workspace.activeWindow);
break;
+39 -18
View File
@@ -257,6 +257,7 @@ class Float:
self.normal = None # its size in pixels when last not full screen
self.unfull_until = 0.0 # left full screen just now (see follow)
self.move_from = None # frame when a title-bar move started (put back after)
self.resizing = False # KWin's resize by the window's edge is going on (see follow)
self.subs = {} # popup or dialog id -> number on the panel
self.app = "" # its desktop file name (for its remembered place)
@@ -401,6 +402,11 @@ class Daemon:
elif kind == "move-start" and f and ev.get("move"):
f.move_from = ev["frame"]
self.screens.ask(f"carry {f.slot.index}", quiet=True)
elif kind == "move-start" and f and ev.get("resize"):
f.resizing = True
elif kind == "move-end" and f and f.resizing:
f.resizing = False
self.follow(f, ev)
elif kind == "move-end" and f and f.move_from:
# The panel carried the window; KWin may have slipped it a few pixels first.
m, f.move_from = f.move_from, None
@@ -443,9 +449,14 @@ class Daemon:
return
# Floating when ft-floatd (re)started: take it over where it is.
f = Float(wid, slot, None)
f.app = ev.get("app", "")
slot.window = f
self.floats[wid] = f
f.scale = slot.kscale = output_scales().get(slot.output, 1.0)
# Its panel's density as it is (follow below sets the panel again).
g, px = self.panel_get(slot.index), round(ev["frame"]["w"] * f.scale)
if g and g["metres"] > 0 and px > 0:
f.mpp = g["metres"] / px
log(f"{wid[:9]} ({ev.get('cls')}) already floats on {slot.output}")
self.follow(f, ev)
return
@@ -512,9 +523,10 @@ class Daemon:
return float(metres) / max(1, int(size.split("x")[0]))
return DEFAULT_MPP
def float_window(self, ev, place=None, mpp=None):
def float_window(self, ev, place=None, mpp=None, scale=None):
"""Float a window: its panel in front of where it was on its screen, or at place (centre
and axes in the world), at the density of its screen, or mpp. Returns its Float."""
and axes in the world), at the density of its screen, or mpp, and at its screen's scale,
or scale (then ev's frame is the window's size at that scale). Returns its Float."""
wid = ev["id"]
if wid in self.floats:
return self.floats[wid]
@@ -529,7 +541,7 @@ class Daemon:
slot.window = f
self.floats[wid] = f
scales = output_scales()
f.scale = scales.get(ev["output"], 1.0)
f.scale = scale or scales.get(ev["output"], 1.0)
slot.kscale = scales.get(slot.output, slot.kscale)
f.mpp = mpp or self.screen_mpp(ev["output"])
fr, s, m = ev["frame"], f.scale, self.margin
@@ -590,9 +602,17 @@ class Daemon:
full = bool(ev.get("fullScreen")) or (fills and time.monotonic() > f.unfull_until)
f.full = full
w, h = round(fr["w"] * s), round(fr["h"] * s)
if not full:
if not full and not (f.normal and abs(f.normal[0] - w) <= math.ceil(s) and abs(f.normal[1] - h) <= math.ceil(s)):
# (Within a logical pixel it's the same size: a size asked for in pixels comes out
# rounded to whole logical pixels. Keeping it stops scale changes from creeping.)
f.normal = (w, h)
m = 0 if full else self.margin
if f.resizing:
# KWin ends a resize by the window's edge whenever an output changes: the output
# follows when it's done (the margin is room to grow until then).
x, y = round((fr["x"] - out["x"]) * s), round((fr["y"] - out["y"]) * s)
self.set_panel(f, (x, y, w, h), title=round((cl["y"] - fr["y"]) * s))
return
self.set_size(slot, f.normal if full and f.normal else (w + 2 * m, h + 2 * m))
if not full:
x0, y0 = slot.pos[0] + m / s, slot.pos[1] + m / s
@@ -614,7 +634,7 @@ class Daemon:
s = min(3.0, max(0.5, s))
if not f.frame or f.full or s == f.scale or self.floats.get(f.id) is not f:
return
w, h = round(f.frame["w"] * f.scale), round(f.frame["h"] * f.scale)
w, h = f.normal or (round(f.frame["w"] * f.scale), round(f.frame["h"] * f.scale))
slot, m = f.slot, self.margin
log(f"{f.id[:9]} scale {f.scale:g} -> {s:g}")
f.scale = s
@@ -785,15 +805,11 @@ class Daemon:
ref = self.reference()
place = to_world(ref, known["rel"]) if known and ref and len(known.get("rel", [])) == 12 else self.in_front()
mpp = (known or {}).get("mpp") or self.screen_mpp(f"WL-{self.primary()}")
scale = None
if known and known.get("pixels"):
ev = dict(ev, frame=dict(ev["frame"]))
s = output_scales().get(ev["output"], 1.0)
ev["frame"]["w"], ev["frame"]["h"] = known["pixels"][0] / s, known["pixels"][1] / s
# The remembered scale isn't applied yet: changing a floating window's scale can set
# off a loop between its size and its output's (KWin keeps the window's margins when
# the output resizes, and follow resizes the output after the window), which needs
# fixing first (docs/floating-windows.md, Known problems).
self.float_window(dict(ev, app=app), place=place, mpp=mpp)
scale = known.get("scale") or output_scales().get(ev["output"], 1.0)
ev = dict(ev, frame=dict(ev["frame"], w=known["pixels"][0] / scale, h=known["pixels"][1] / scale))
self.float_window(dict(ev, app=app), place=place, mpp=mpp, scale=scale)
# ------------------------------------------------------------ profiles (docs/profiles.md)
@@ -920,18 +936,22 @@ class Daemon:
if not ref or len(fl.get("rel", [])) != 12:
return
place = to_world(ref, fl["rel"])
pixels = fl.get("pixels")
pixels, scale = fl.get("pixels"), fl.get("scale")
if f:
self.pose(f, *place)
if scale:
self.set_scale(f, scale)
if pixels and f.frame:
m = self.margin
self.command(cmd="geometry", id=f.id, x=f.slot.pos[0] + m / f.scale,
y=f.slot.pos[1] + m / f.scale, w=pixels[0] / f.scale, h=pixels[1] / f.scale)
return
if pixels:
s = output_scales().get(ev["output"], 1.0)
ev = dict(ev, frame=dict(ev["frame"], w=pixels[0] / s, h=pixels[1] / s))
self.float_window(ev, place=place, mpp=fl.get("mpp"))
scale = scale or output_scales().get(ev["output"], 1.0)
ev = dict(ev, frame=dict(ev["frame"], w=pixels[0] / scale, h=pixels[1] / scale))
else:
scale = None
self.float_window(ev, place=place, mpp=fl.get("mpp"), scale=scale)
return
n = int(e.get("screen", 1)) - 1
if not 0 <= n < self.screens_n:
@@ -1039,7 +1059,8 @@ class Daemon:
if not ev:
return f"error no window {rest[0]}"
if cmd == "float":
self.float_window(ev)
# The script's word for where it is now: a window on a screen isn't followed here.
self.command(cmd="request-float", id=rest[0])
elif cmd == "dock" and rest[0] in self.floats:
self.dock(self.floats[rest[0]])
elif cmd == "close":