mirror of
https://github.com/DeeJanuz/frametop.git
synced 2026-10-06 03:00:06 +02:00
Floating windows: fix the scale crash, the click offset, and placement
- KWin's nested backend makes an output the size it's configured to times its scale, so after Meta+scroll every size ft-floatd sent was multiplied again, and an odd result disconnected KWin (buffer not divisible by its scale). ft-floatd now asks for sizes in the output's scaled terms (kwin_size), and asks again after each scale change. - SteamVR reports mouse positions on a panel with texture bounds in the whole texture, not the crop, so clicks on a floating window landed up to ~200 px off. The mouse scale is now the buffer's size, as on a screen. - A floated window starts 30 cm in front of its screen (was 5 cm), so it's easy to point at apart from the screen behind it. - No 1 s wait before a spare turns on (a disabled output never commits), and the login splash on the spares isn't taken for floating windows. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
1 parent
2a9fbdebb4
commit
c8fc6351bb
3 files changed
+78
-43
No files matched your search
+64
-26
@@ -47,17 +47,32 @@ PATH = "/Float"
|
|||||||
SCRIPT = "frametop-float"
|
SCRIPT = "frametop-float"
|
||||||
POLL_SECONDS = 20 # NextCommand answers empty after this (KWin's D-Bus timeout is 25 s)
|
POLL_SECONDS = 20 # NextCommand answers empty after this (KWin's D-Bus timeout is 25 s)
|
||||||
SPARE_X, SPARE_CELL = 12000, 5000 # spares in KWin's layout: a grid from here, 4 across
|
SPARE_X, SPARE_CELL = 12000, 5000 # spares in KWin's layout: a grid from here, 4 across
|
||||||
PULL_OUT = 0.05 # a floated window starts this far in front of its screen (metres)
|
PULL_OUT = 0.3 # a floated window starts this far in front of its screen (metres), clear of it
|
||||||
DEFAULT_MPP = 1.6 / 1920 # metres per pixel when ft-screens can't say (no SteamVR)
|
DEFAULT_MPP = 1.6 / 1920 # metres per pixel when ft-screens can't say (no SteamVR)
|
||||||
DEBUG = os.environ.get("FT_FLOAT_DEBUG") == "1" # log every event from the script
|
DEBUG = os.environ.get("FT_FLOAT_DEBUG") == "1" # log every event from the script
|
||||||
|
|
||||||
|
|
||||||
def fit(px, scale):
|
def whole(*scales):
|
||||||
"""An output size KWin can draw at this scale: its nested backend gives the buffer a whole
|
"""The multiple a spare's size in pixels must be at these scales: KWin's nested backend gives
|
||||||
buffer scale (1.2 -> 2), and a buffer that isn't a multiple of it is a protocol error that
|
the buffer a whole buffer scale (1.2 -> 2), and a buffer that isn't a multiple of it is a
|
||||||
disconnects KWin. So round up to a multiple; the margin takes the extra pixels."""
|
protocol error that disconnects KWin."""
|
||||||
k = max(1, math.ceil(scale - 1e-6))
|
k = 1
|
||||||
return (px + k - 1) // k * k
|
for s in scales:
|
||||||
|
k = math.lcm(k, max(1, math.ceil(s - 1e-6)))
|
||||||
|
return k
|
||||||
|
|
||||||
|
|
||||||
|
def kwin_size(px, scale, k):
|
||||||
|
"""What to ask ft-screens for so a spare comes out at least px pixels, a multiple of k. KWin
|
||||||
|
makes a nested output the size it's configured to times its scale, rounded. Returns (the
|
||||||
|
size to ask for, the pixels it comes out); the margin takes the extra pixels."""
|
||||||
|
n = max(1, math.floor(px / scale))
|
||||||
|
while True:
|
||||||
|
exact = n * scale
|
||||||
|
p = math.floor(exact + 0.5)
|
||||||
|
if p >= px and p % k == 0 and abs(exact - math.floor(exact) - 0.5) > 1e-6:
|
||||||
|
return n, p
|
||||||
|
n += 1
|
||||||
|
|
||||||
|
|
||||||
def log(*args):
|
def log(*args):
|
||||||
@@ -126,6 +141,8 @@ class Slot:
|
|||||||
self.index = screens + k + 1 # ft-screens' number (1-based)
|
self.index = screens + k + 1 # ft-screens' number (1-based)
|
||||||
self.pos = (SPARE_X + (k % 4) * SPARE_CELL, (k // 4) * SPARE_CELL)
|
self.pos = (SPARE_X + (k % 4) * SPARE_CELL, (k // 4) * SPARE_CELL)
|
||||||
self.size = None # its output's size in pixels, as last set
|
self.size = None # its output's size in pixels, as last set
|
||||||
|
self.want = None # the size in pixels asked for (size is at least that)
|
||||||
|
self.kscale = 1.0 # its output's scale in KWin, as last set
|
||||||
self.window = None # Float
|
self.window = None # Float
|
||||||
|
|
||||||
|
|
||||||
@@ -257,12 +274,14 @@ class Daemon:
|
|||||||
if not ev.get("normal"):
|
if not ev.get("normal"):
|
||||||
return
|
return
|
||||||
if slot.window is None:
|
if slot.window is None:
|
||||||
|
if ev.get("cls") == "ksplashqml": # the login splash, on every output at first
|
||||||
|
return
|
||||||
# Floating when ft-floatd (re)started: take it over where it is.
|
# Floating when ft-floatd (re)started: take it over where it is.
|
||||||
f = Float(wid, slot, None)
|
f = Float(wid, slot, None)
|
||||||
slot.window = f
|
slot.window = f
|
||||||
self.floats[wid] = f
|
self.floats[wid] = f
|
||||||
f.scale = output_scales().get(slot.output, 1.0)
|
f.scale = slot.kscale = output_scales().get(slot.output, 1.0)
|
||||||
log(f"{wid[:9]} already floats on {slot.output}")
|
log(f"{wid[:9]} ({ev.get('cls')}) already floats on {slot.output}")
|
||||||
self.follow(f, ev)
|
self.follow(f, ev)
|
||||||
return
|
return
|
||||||
# A new window that opened on a floating window's output: windows of floating apps
|
# A new window that opened on a floating window's output: windows of floating apps
|
||||||
@@ -288,6 +307,15 @@ class Daemon:
|
|||||||
log(f"{slot.output} didn't take {size[0]}x{size[1]} in time")
|
log(f"{slot.output} didn't take {size[0]}x{size[1]} in time")
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
def set_size(self, slot, want, k=None):
|
||||||
|
"""Size a spare's output to at least want (pixels) at its current scale in KWin."""
|
||||||
|
if want == slot.want:
|
||||||
|
return
|
||||||
|
k = k or whole(slot.kscale)
|
||||||
|
(w, pw), (h, ph) = kwin_size(want[0], slot.kscale, k), kwin_size(want[1], slot.kscale, k)
|
||||||
|
slot.want, slot.size = want, (pw, ph)
|
||||||
|
self.screens.ask(f"size {slot.index} {w} {h}")
|
||||||
|
|
||||||
def disable_unused(self):
|
def disable_unused(self):
|
||||||
off = [f"output.{s.output}.disable" for s in self.slots if s.window is None]
|
off = [f"output.{s.output}.disable" for s in self.slots if s.window is None]
|
||||||
if off:
|
if off:
|
||||||
@@ -323,21 +351,24 @@ class Daemon:
|
|||||||
f = Float(wid, slot, {"output": ev["output"], "frame": ev["frame"], "onAllDesktops": ev.get("onAllDesktops")})
|
f = Float(wid, slot, {"output": ev["output"], "frame": ev["frame"], "onAllDesktops": ev.get("onAllDesktops")})
|
||||||
slot.window = f
|
slot.window = f
|
||||||
self.floats[wid] = f
|
self.floats[wid] = f
|
||||||
f.scale = output_scales().get(ev["output"], 1.0)
|
scales = output_scales()
|
||||||
|
f.scale = scales.get(ev["output"], 1.0)
|
||||||
|
slot.kscale = scales.get(slot.output, slot.kscale)
|
||||||
f.mpp = self.screen_mpp(ev["output"])
|
f.mpp = self.screen_mpp(ev["output"])
|
||||||
fr, s, m = ev["frame"], f.scale, self.margin
|
fr, s, m = ev["frame"], f.scale, self.margin
|
||||||
w, h = round(fr["w"] * s), round(fr["h"] * s)
|
w, h = round(fr["w"] * s), round(fr["h"] * s)
|
||||||
slot.size = (fit(w + 2 * m, s), fit(h + 2 * m, s))
|
|
||||||
log(f"{wid[:9]} ({ev.get('cls')}) floats on {slot.output}: {w}x{h} px, scale {s:g}")
|
log(f"{wid[:9]} ({ev.get('cls')}) floats on {slot.output}: {w}x{h} px, scale {s:g}")
|
||||||
# The spare's size first (while it's off, so its first frame is right), then its panel,
|
# The spare's size first (while it's off, so its first frame is right; it's sized at
|
||||||
# then turn it on, then the window.
|
# its old scale, for pixels that suit the new one), then its panel, then turn it on,
|
||||||
self.screens.ask(f"size {slot.index} {slot.size[0]} {slot.size[1]}")
|
# then the window.
|
||||||
self.sized(slot, slot.size)
|
slot.want = None
|
||||||
|
self.set_size(slot, (w + 2 * m, h + 2 * m), whole(slot.kscale, s))
|
||||||
self.screens.ask(f"scale {slot.index} {s:g}") # for pointer positions (KWin's units)
|
self.screens.ask(f"scale {slot.index} {s:g}") # for pointer positions (KWin's units)
|
||||||
self.set_panel(f, (m, m, w, h), title=round((ev["client"]["y"] - fr["y"]) * s))
|
self.set_panel(f, (m, m, w, h), title=round((ev["client"]["y"] - fr["y"]) * s))
|
||||||
self.place_panel(f, ev)
|
self.place_panel(f, ev)
|
||||||
kscreen(f"output.{slot.output}.enable", f"output.{slot.output}.scale.{s:g}",
|
kscreen(f"output.{slot.output}.enable", f"output.{slot.output}.scale.{s:g}",
|
||||||
f"output.{slot.output}.position.{slot.pos[0]},{slot.pos[1]}")
|
f"output.{slot.output}.position.{slot.pos[0]},{slot.pos[1]}")
|
||||||
|
self.rescaled(slot, s)
|
||||||
self.command(cmd="place", id=wid, output=slot.output, x=slot.pos[0] + m / s, y=slot.pos[1] + m / s,
|
self.command(cmd="place", id=wid, output=slot.output, x=slot.pos[0] + m / s, y=slot.pos[1] + m / s,
|
||||||
w=fr["w"], h=fr["h"], onAllDesktops=True)
|
w=fr["w"], h=fr["h"], onAllDesktops=True)
|
||||||
|
|
||||||
@@ -378,11 +409,7 @@ class Daemon:
|
|||||||
if not full:
|
if not full:
|
||||||
f.normal = (w, h)
|
f.normal = (w, h)
|
||||||
m = 0 if full else self.margin
|
m = 0 if full else self.margin
|
||||||
want = f.normal if full and f.normal else (w + 2 * m, h + 2 * m)
|
self.set_size(slot, f.normal if full and f.normal else (w + 2 * m, h + 2 * m))
|
||||||
want = (fit(want[0], s), fit(want[1], s))
|
|
||||||
if want != slot.size:
|
|
||||||
slot.size = want
|
|
||||||
self.screens.ask(f"size {slot.index} {want[0]} {want[1]}")
|
|
||||||
if not full:
|
if not full:
|
||||||
x0, y0 = slot.pos[0] + m / s, slot.pos[1] + m / s
|
x0, y0 = slot.pos[0] + m / s, slot.pos[1] + m / s
|
||||||
if abs(fr["x"] - x0) > 0.5 or abs(fr["y"] - y0) > 0.5:
|
if abs(fr["x"] - x0) > 0.5 or abs(fr["y"] - y0) > 0.5:
|
||||||
@@ -403,16 +430,26 @@ class Daemon:
|
|||||||
slot, m = f.slot, self.margin
|
slot, m = f.slot, self.margin
|
||||||
log(f"{f.id[:9]} scale {f.scale:g} -> {s:g}")
|
log(f"{f.id[:9]} scale {f.scale:g} -> {s:g}")
|
||||||
f.scale = s
|
f.scale = s
|
||||||
# The output's size must suit the new scale before KWin draws at it (see fit).
|
# The output's size in pixels must suit the new scale before KWin draws at it (see whole).
|
||||||
size = (fit(slot.size[0], s), fit(slot.size[1], s))
|
k = whole(slot.kscale, s)
|
||||||
if size != slot.size:
|
if slot.size[0] % k or slot.size[1] % k:
|
||||||
slot.size = size
|
slot.want = None
|
||||||
self.screens.ask(f"size {slot.index} {size[0]} {size[1]}")
|
self.set_size(slot, slot.size, k)
|
||||||
self.sized(slot, size)
|
self.sized(slot, slot.size)
|
||||||
kscreen(f"output.{slot.output}.scale.{s:g}")
|
kscreen(f"output.{slot.output}.scale.{s:g}")
|
||||||
self.screens.ask(f"scale {slot.index} {s:g}")
|
self.screens.ask(f"scale {slot.index} {s:g}")
|
||||||
|
self.rescaled(slot, s)
|
||||||
self.command(cmd="geometry", id=f.id, x=slot.pos[0] + m / s, y=slot.pos[1] + m / s, w=w / s, h=h / s)
|
self.command(cmd="geometry", id=f.id, x=slot.pos[0] + m / s, y=slot.pos[1] + m / s, w=w / s, h=h / s)
|
||||||
|
|
||||||
|
def rescaled(self, slot, s):
|
||||||
|
"""KWin has the spare at scale s now: ask for its size again in the new scale's terms,
|
||||||
|
or the next configure (any size, or KWin's own) would make it the old size times s."""
|
||||||
|
if s == slot.kscale:
|
||||||
|
return
|
||||||
|
slot.kscale = s
|
||||||
|
want, slot.want = slot.size, None
|
||||||
|
self.set_size(slot, want)
|
||||||
|
|
||||||
def dock(self, f, frame=None):
|
def dock(self, f, frame=None):
|
||||||
"""Back where it came from (or onto screen 1 if we don't know)."""
|
"""Back where it came from (or onto screen 1 if we don't know)."""
|
||||||
saved = f.saved or {"output": "WL-0", "frame": dict(f.frame or {"x": 100, "y": 100, "w": 800, "h": 600}),
|
saved = f.saved or {"output": "WL-0", "frame": dict(f.frame or {"x": 100, "y": 100, "w": 800, "h": 600}),
|
||||||
@@ -427,6 +464,7 @@ class Daemon:
|
|||||||
slot = f.slot
|
slot = f.slot
|
||||||
if slot.window is f:
|
if slot.window is f:
|
||||||
slot.window = None
|
slot.window = None
|
||||||
|
slot.want = None
|
||||||
for sub_id in list(f.subs):
|
for sub_id in list(f.subs):
|
||||||
self.drop_sub(sub_id)
|
self.drop_sub(sub_id)
|
||||||
self.screens.ask(f"unfloat {slot.index}", quiet=True)
|
self.screens.ask(f"unfloat {slot.index}", quiet=True)
|
||||||
|
|||||||
@@ -434,8 +434,10 @@ static int control_readable(int fd, uint32_t mask, void *data) {
|
|||||||
int value, index, w, h;
|
int value, index, w, h;
|
||||||
double scale;
|
double scale;
|
||||||
if (sscanf(buf, "size %d %d %d", &index, &w, &h) == 3) {
|
if (sscanf(buf, "size %d %d %d", &index, &w, &h) == 3) {
|
||||||
// A new resolution for a screen, live: KWin resizes the screen to match.
|
// A new resolution for a screen, live: KWin resizes the screen to match. (KWin makes
|
||||||
if (index < 1 || index > MAX_SCREENS || !s->screens[index - 1] || w < 320 || h < 200 || w > 16384 ||
|
// it this size times its scale; ft-floatd sends spares' sizes divided by theirs.)
|
||||||
|
const int min_w = index - 1 < s->n_config ? 320 : 64, min_h = index - 1 < s->n_config ? 200 : 64;
|
||||||
|
if (index < 1 || index > MAX_SCREENS || !s->screens[index - 1] || w < min_w || h < min_h || w > 16384 ||
|
||||||
h > 16384) {
|
h > 16384) {
|
||||||
snprintf(reply, sizeof reply, "error bad screen or size");
|
snprintf(reply, sizeof reply, "error bad screen or size");
|
||||||
} else {
|
} else {
|
||||||
|
|||||||
+10
-15
@@ -276,10 +276,8 @@ struct Screen {
|
|||||||
return width > 0 ? metres * height / width : metres * 9 / 16;
|
return width > 0 ? metres * height / width : metres * 9 / 16;
|
||||||
}
|
}
|
||||||
// Buffer pixels from OpenVR's mouse position on the panel (its origin is bottom left).
|
// 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 {
|
// A cropped panel too: SteamVR gives the position in the whole texture, not the crop.
|
||||||
if (floating && cropW > 0) *x = cropX + mx, *y = cropY + cropH - my;
|
void ToBuffer(double mx, double my, double *x, double *y) const { *x = mx, *y = height - my; }
|
||||||
else *x = mx, *y = height - my;
|
|
||||||
}
|
|
||||||
std::array<vr::VROverlayHandle_t, 6> Controls() const {
|
std::array<vr::VROverlayHandle_t, 6> Controls() const {
|
||||||
return {bar, curveButton, rollButton, handle, dockButton, closeButton};
|
return {bar, curveButton, rollButton, handle, dockButton, closeButton};
|
||||||
}
|
}
|
||||||
@@ -1150,14 +1148,15 @@ void Push(Screen &s, double notches) {
|
|||||||
|
|
||||||
// ---------------------------------------------------------------- floating windows
|
// ---------------------------------------------------------------- floating windows
|
||||||
|
|
||||||
// Show the window's rectangle of the buffer, and map the mouse to it (see ToBuffer).
|
// Show the window's rectangle of the buffer. Texture bounds are fractions of the buffer, v
|
||||||
// Texture bounds are fractions of the buffer, v from the top.
|
// from the top. SteamVR reports mouse positions in the whole texture (the bounds applied), so
|
||||||
|
// the mouse scale is the buffer's size, as on a screen (see ToBuffer).
|
||||||
void CropOverlay(vr::VROverlayHandle_t o, const Screen &s, int x, int y, int w, int h) {
|
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;
|
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,
|
vr::VRTextureBounds_t b = {float(x) / s.width, float(y) / s.height, float(x + w) / s.width,
|
||||||
float(y + h) / s.height};
|
float(y + h) / s.height};
|
||||||
vr::VROverlay()->SetOverlayTextureBounds(o, &b);
|
vr::VROverlay()->SetOverlayTextureBounds(o, &b);
|
||||||
vr::HmdVector2_t scale = {float(w), float(h)};
|
vr::HmdVector2_t scale = {float(s.width), float(s.height)};
|
||||||
vr::VROverlay()->SetOverlayMouseScale(o, &scale);
|
vr::VROverlay()->SetOverlayMouseScale(o, &scale);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1571,15 +1570,11 @@ void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
|
|||||||
RefreshPoses();
|
RefreshPoses();
|
||||||
for (auto &[index, s] : g_screens) {
|
for (auto &[index, s] : g_screens) {
|
||||||
vr::VREvent_t ev;
|
vr::VREvent_t ev;
|
||||||
// The screen itself: input for KWin.
|
|
||||||
// The screen itself (and a floating window's popups): input for KWin.
|
// The screen itself (and a floating window's popups): input for KWin.
|
||||||
auto panelEvent = [&](const vr::VREvent_t &ev, bool sub, const Sub *from) {
|
auto panelEvent = [&](const vr::VREvent_t &ev, bool sub) {
|
||||||
ft_event e{};
|
ft_event e{};
|
||||||
e.screen = index;
|
e.screen = index;
|
||||||
auto at = [&] {
|
auto at = [&] { s.ToBuffer(ev.data.mouse.x, ev.data.mouse.y, &e.x, &e.y); };
|
||||||
if (from) e.x = from->x + ev.data.mouse.x, e.y = from->y + from->h - ev.data.mouse.y;
|
|
||||||
else s.ToBuffer(ev.data.mouse.x, ev.data.mouse.y, &e.x, &e.y);
|
|
||||||
};
|
|
||||||
switch (ev.eventType) {
|
switch (ev.eventType) {
|
||||||
case vr::VREvent_MouseMove:
|
case vr::VREvent_MouseMove:
|
||||||
if (s.titleCarry) return; // KWin's pointer stays where the title bar was pressed
|
if (s.titleCarry) return; // KWin's pointer stays where the title bar was pressed
|
||||||
@@ -1624,9 +1619,9 @@ void ft_vr_poll(void (*handle)(const struct ft_event *, void *), void *data) {
|
|||||||
}
|
}
|
||||||
handle(&e, data);
|
handle(&e, data);
|
||||||
};
|
};
|
||||||
while (vr::VROverlay()->PollNextOverlayEvent(s.overlay, &ev, sizeof ev)) panelEvent(ev, false, nullptr);
|
while (vr::VROverlay()->PollNextOverlayEvent(s.overlay, &ev, sizeof ev)) panelEvent(ev, false);
|
||||||
for (const auto &[k, sub] : s.subs)
|
for (const auto &[k, sub] : s.subs)
|
||||||
while (vr::VROverlay()->PollNextOverlayEvent(sub.overlay, &ev, sizeof ev)) panelEvent(ev, true, &sub);
|
while (vr::VROverlay()->PollNextOverlayEvent(sub.overlay, &ev, sizeof ev)) panelEvent(ev, true);
|
||||||
// The controls light up under a laser.
|
// The controls light up under a laser.
|
||||||
auto hover = [&](int k) {
|
auto hover = [&](int k) {
|
||||||
const bool on = ev.eventType == vr::VREvent_MouseMove || ev.eventType == vr::VREvent_FocusEnter;
|
const bool on = ev.eventType == vr::VREvent_MouseMove || ev.eventType == vr::VREvent_FocusEnter;
|
||||||
|
|||||||
Reference in new issue
Block a user