Fix window chrome positioning on Sway

Improves focused-window geometry so chrome placement stays stable across workspaces and floating containers. WindowService now exposes focused workspace X/Y, resolves workspace by name, and publishes those offsets with focused geometry. The Sway backend now tracks the active floating frame through recursion and consistently derives frame/deco/content rects from that frame. QML chrome placement was updated to use workspace-relative coordinates and simpler direct margins (removing interpolation behaviors), with safer title-bar height handling for vertical centering.
This commit is contained in:
2026-08-26 23:10:22 +12:00
parent e4ea19cd09
commit ba7945a1d6
7 changed files with 75 additions and 66 deletions
@@ -66,38 +66,16 @@ bool nodeLooksLikeView(const QJsonObject &node)
return !properties.isEmpty();
}
void applyFrameGeometry(WindowInfo &window, const QJsonObject &node, const QJsonObject &parent)
void applyFrameGeometry(WindowInfo &window, const QJsonObject &node, const QJsonObject &frame)
{
QJsonObject rect = node.value(QStringLiteral("rect")).toObject();
QJsonObject deco = node.value(QStringLiteral("deco_rect")).toObject();
QJsonObject content = node.value(QStringLiteral("window_rect")).toObject();
const bool useFrame = !frame.isEmpty()
&& (frame.value(QStringLiteral("type")).toString() == QLatin1String("floating_con")
|| nodeIsFloating(frame));
const QJsonObject &source = useFrame ? frame : node;
int decoHeight = deco.value(QStringLiteral("height")).toInt();
int contentY = content.value(QStringLiteral("y")).toInt();
// Floating views live inside a floating_con whose rect includes Sway's
// server-side title bar. The child view rect is the client content rect.
// Always use the wrapper while it exists so refreshes cannot make chrome
// jump between frame and content coordinates.
if (nodeIsFloating(parent)) {
rect = parent.value(QStringLiteral("rect")).toObject();
deco = parent.value(QStringLiteral("deco_rect")).toObject();
content = parent.value(QStringLiteral("window_rect")).toObject();
decoHeight = deco.value(QStringLiteral("height")).toInt();
contentY = content.value(QStringLiteral("y")).toInt();
} else if (decoHeight <= 0 && contentY <= 0 && !parent.isEmpty()) {
const QJsonObject parentDeco = parent.value(QStringLiteral("deco_rect")).toObject();
const QJsonObject parentContent = parent.value(QStringLiteral("window_rect")).toObject();
const int parentDecoHeight = parentDeco.value(QStringLiteral("height")).toInt();
const int parentContentY = parentContent.value(QStringLiteral("y")).toInt();
if (parentDecoHeight > 0 || parentContentY > 0) {
rect = parent.value(QStringLiteral("rect")).toObject();
deco = parentDeco;
content = parentContent;
decoHeight = parentDecoHeight;
contentY = parentContentY;
}
}
const QJsonObject rect = source.value(QStringLiteral("rect")).toObject();
const QJsonObject deco = source.value(QStringLiteral("deco_rect")).toObject();
const QJsonObject content = source.value(QStringLiteral("window_rect")).toObject();
window.x = rect.value(QStringLiteral("x")).toInt();
window.y = rect.value(QStringLiteral("y")).toInt();
@@ -106,9 +84,9 @@ void applyFrameGeometry(WindowInfo &window, const QJsonObject &node, const QJson
window.decoX = deco.value(QStringLiteral("x")).toInt();
window.decoY = deco.value(QStringLiteral("y")).toInt();
window.decoWidth = deco.value(QStringLiteral("width")).toInt();
window.decoHeight = decoHeight;
window.decoHeight = deco.value(QStringLiteral("height")).toInt();
window.contentX = content.value(QStringLiteral("x")).toInt();
window.contentY = contentY;
window.contentY = content.value(QStringLiteral("y")).toInt();
}
} // namespace
@@ -530,7 +508,7 @@ bool SwayWindowBackend::isShellChrome(const QJsonObject &node)
}
void SwayWindowBackend::collect(const QJsonObject &node,
const QJsonObject &parent,
const QJsonObject &frame,
const QString &workspace,
const QString &output,
bool onScratchpad,
@@ -568,6 +546,10 @@ void SwayWindowBackend::collect(const QJsonObject &node,
}
}
QJsonObject nextFrame = frame;
if (type == QLatin1String("floating_con"))
nextFrame = node;
if (nodeLooksLikeView(node) && !isShellChrome(node)) {
WindowInfo window;
window.id = jsonId(node.value(QStringLiteral("id")));
@@ -580,18 +562,18 @@ void SwayWindowBackend::collect(const QJsonObject &node,
window.workspace = nextWorkspace;
window.output = nextOutput;
window.focused = node.value(QStringLiteral("focused")).toBool();
window.floating = nodeIsFloating(node) || nodeIsFloating(parent);
window.floating = nodeIsFloating(node) || nodeIsFloating(nextFrame);
window.fullscreen = node.value(QStringLiteral("fullscreen_mode")).toInt() > 0;
window.minimized = nextScratchpad;
applyFrameGeometry(window, node, parent);
applyFrameGeometry(window, node, nextFrame);
windows->append(window);
}
const QJsonArray nodes = node.value(QStringLiteral("nodes")).toArray();
for (const QJsonValue &child : nodes)
collect(child.toObject(), node, nextWorkspace, nextOutput, nextScratchpad, windows, workspaces);
collect(child.toObject(), nextFrame, nextWorkspace, nextOutput, nextScratchpad, windows, workspaces);
const QJsonArray floating = node.value(QStringLiteral("floating_nodes")).toArray();
for (const QJsonValue &child : floating)
collect(child.toObject(), node, nextWorkspace, nextOutput, nextScratchpad, windows, workspaces);
collect(child.toObject(), nextFrame, nextWorkspace, nextOutput, nextScratchpad, windows, workspaces);
}
@@ -44,7 +44,7 @@ private:
const WindowInfo *findWindow(qint64 windowId) const;
static bool isShellChrome(const QJsonObject &node);
static void collect(const QJsonObject &node,
const QJsonObject &parent,
const QJsonObject &frame,
const QString &workspace,
const QString &output,
bool onScratchpad,