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
+28 -6
View File
@@ -156,6 +156,16 @@ int WindowService::focusedWindowTitleBarHeight() const
return m_focusedWindowTitleBarHeight; return m_focusedWindowTitleBarHeight;
} }
int WindowService::focusedWorkspaceX() const
{
return m_focusedWorkspaceX;
}
int WindowService::focusedWorkspaceY() const
{
return m_focusedWorkspaceY;
}
bool WindowService::focusWindow(const QString &windowId) bool WindowService::focusWindow(const QString &windowId)
{ {
return m_backend->focusWindow(parseWindowId(windowId)); return m_backend->focusWindow(parseWindowId(windowId));
@@ -333,6 +343,8 @@ void WindowService::publishFocusedGeometry(const WindowInfo *focused)
int decoHeight = 0; int decoHeight = 0;
int titleBarY = 0; int titleBarY = 0;
int titleBarHeight = 0; int titleBarHeight = 0;
int workspaceX = 0;
int workspaceY = 0;
bool fullscreen = false; bool fullscreen = false;
if (focused && !focused->minimized) { if (focused && !focused->minimized) {
@@ -349,13 +361,19 @@ void WindowService::publishFocusedGeometry(const WindowInfo *focused)
titleBarHeight = focused->decoHeight; titleBarHeight = focused->decoHeight;
if (titleBarHeight <= 0) if (titleBarHeight <= 0)
titleBarHeight = focused->contentY; titleBarHeight = focused->contentY;
if (titleBarHeight <= 0) if (titleBarHeight < 16 || titleBarHeight > 48)
titleBarHeight = 32; titleBarHeight = 32;
if (focused->decoHeight > 0 || focused->contentY > 0) // Frame rect from the floating container starts at the title bar.
titleBarY = focused->y + focused->decoY; titleBarY = focused->y + focused->decoY;
else
titleBarY = focused->y - titleBarHeight; const WorkspaceInfo *workspace = m_workspaces->workspaceByName(focused->workspace);
if (!workspace)
workspace = m_workspaces->focusedWorkspace();
if (workspace) {
workspaceX = workspace->x;
workspaceY = workspace->y;
}
} }
if (m_focusedWindowFullscreen != fullscreen) { if (m_focusedWindowFullscreen != fullscreen) {
@@ -372,7 +390,9 @@ void WindowService::publishFocusedGeometry(const WindowInfo *focused)
|| m_focusedWindowDecoWidth != decoWidth || m_focusedWindowDecoWidth != decoWidth
|| m_focusedWindowDecoHeight != decoHeight || m_focusedWindowDecoHeight != decoHeight
|| m_focusedWindowTitleBarY != titleBarY || m_focusedWindowTitleBarY != titleBarY
|| m_focusedWindowTitleBarHeight != titleBarHeight) { || m_focusedWindowTitleBarHeight != titleBarHeight
|| m_focusedWorkspaceX != workspaceX
|| m_focusedWorkspaceY != workspaceY) {
m_focusedWindowX = x; m_focusedWindowX = x;
m_focusedWindowY = y; m_focusedWindowY = y;
m_focusedWindowWidth = width; m_focusedWindowWidth = width;
@@ -383,6 +403,8 @@ void WindowService::publishFocusedGeometry(const WindowInfo *focused)
m_focusedWindowDecoHeight = decoHeight; m_focusedWindowDecoHeight = decoHeight;
m_focusedWindowTitleBarY = titleBarY; m_focusedWindowTitleBarY = titleBarY;
m_focusedWindowTitleBarHeight = titleBarHeight; m_focusedWindowTitleBarHeight = titleBarHeight;
m_focusedWorkspaceX = workspaceX;
m_focusedWorkspaceY = workspaceY;
emit focusedWindowGeometryChanged(); emit focusedWindowGeometryChanged();
} }
} }
+6
View File
@@ -39,6 +39,8 @@ class WindowService : public QObject
Q_PROPERTY(int focusedWindowDecoHeight READ focusedWindowDecoHeight NOTIFY focusedWindowGeometryChanged) Q_PROPERTY(int focusedWindowDecoHeight READ focusedWindowDecoHeight NOTIFY focusedWindowGeometryChanged)
Q_PROPERTY(int focusedWindowTitleBarY READ focusedWindowTitleBarY NOTIFY focusedWindowGeometryChanged) Q_PROPERTY(int focusedWindowTitleBarY READ focusedWindowTitleBarY NOTIFY focusedWindowGeometryChanged)
Q_PROPERTY(int focusedWindowTitleBarHeight READ focusedWindowTitleBarHeight NOTIFY focusedWindowGeometryChanged) Q_PROPERTY(int focusedWindowTitleBarHeight READ focusedWindowTitleBarHeight NOTIFY focusedWindowGeometryChanged)
Q_PROPERTY(int focusedWorkspaceX READ focusedWorkspaceX NOTIFY focusedWindowGeometryChanged)
Q_PROPERTY(int focusedWorkspaceY READ focusedWorkspaceY NOTIFY focusedWindowGeometryChanged)
public: public:
enum SnapEdge { enum SnapEdge {
@@ -73,6 +75,8 @@ public:
int focusedWindowDecoHeight() const; int focusedWindowDecoHeight() const;
int focusedWindowTitleBarY() const; int focusedWindowTitleBarY() const;
int focusedWindowTitleBarHeight() const; int focusedWindowTitleBarHeight() const;
int focusedWorkspaceX() const;
int focusedWorkspaceY() const;
Q_INVOKABLE bool focusWindow(const QString &windowId); Q_INVOKABLE bool focusWindow(const QString &windowId);
Q_INVOKABLE bool closeWindow(const QString &windowId); Q_INVOKABLE bool closeWindow(const QString &windowId);
@@ -124,5 +128,7 @@ private:
int m_focusedWindowDecoHeight = 0; int m_focusedWindowDecoHeight = 0;
int m_focusedWindowTitleBarY = 0; int m_focusedWindowTitleBarY = 0;
int m_focusedWindowTitleBarHeight = 0; int m_focusedWindowTitleBarHeight = 0;
int m_focusedWorkspaceX = 0;
int m_focusedWorkspaceY = 0;
QTimer *m_geometryTimer = nullptr; QTimer *m_geometryTimer = nullptr;
}; };
+11
View File
@@ -82,3 +82,14 @@ const WorkspaceInfo *WorkspaceModel::focusedWorkspace() const
} }
return nullptr; return nullptr;
} }
const WorkspaceInfo *WorkspaceModel::workspaceByName(const QString &name) const
{
if (name.isEmpty())
return nullptr;
for (const WorkspaceInfo &workspace : m_workspaces) {
if (workspace.name == name)
return &workspace;
}
return nullptr;
}
+1
View File
@@ -35,6 +35,7 @@ public:
void setWorkspaces(QVector<WorkspaceInfo> workspaces); void setWorkspaces(QVector<WorkspaceInfo> workspaces);
const WorkspaceInfo *workspaceAt(int row) const; const WorkspaceInfo *workspaceAt(int row) const;
const WorkspaceInfo *focusedWorkspace() const; const WorkspaceInfo *focusedWorkspace() const;
const WorkspaceInfo *workspaceByName(const QString &name) const;
signals: signals:
void countChanged(); void countChanged();
@@ -66,38 +66,16 @@ bool nodeLooksLikeView(const QJsonObject &node)
return !properties.isEmpty(); 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(); const bool useFrame = !frame.isEmpty()
QJsonObject deco = node.value(QStringLiteral("deco_rect")).toObject(); && (frame.value(QStringLiteral("type")).toString() == QLatin1String("floating_con")
QJsonObject content = node.value(QStringLiteral("window_rect")).toObject(); || nodeIsFloating(frame));
const QJsonObject &source = useFrame ? frame : node;
int decoHeight = deco.value(QStringLiteral("height")).toInt(); const QJsonObject rect = source.value(QStringLiteral("rect")).toObject();
int contentY = content.value(QStringLiteral("y")).toInt(); const QJsonObject deco = source.value(QStringLiteral("deco_rect")).toObject();
const QJsonObject content = source.value(QStringLiteral("window_rect")).toObject();
// 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;
}
}
window.x = rect.value(QStringLiteral("x")).toInt(); window.x = rect.value(QStringLiteral("x")).toInt();
window.y = rect.value(QStringLiteral("y")).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.decoX = deco.value(QStringLiteral("x")).toInt();
window.decoY = deco.value(QStringLiteral("y")).toInt(); window.decoY = deco.value(QStringLiteral("y")).toInt();
window.decoWidth = deco.value(QStringLiteral("width")).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.contentX = content.value(QStringLiteral("x")).toInt();
window.contentY = contentY; window.contentY = content.value(QStringLiteral("y")).toInt();
} }
} // namespace } // namespace
@@ -530,7 +508,7 @@ bool SwayWindowBackend::isShellChrome(const QJsonObject &node)
} }
void SwayWindowBackend::collect(const QJsonObject &node, void SwayWindowBackend::collect(const QJsonObject &node,
const QJsonObject &parent, const QJsonObject &frame,
const QString &workspace, const QString &workspace,
const QString &output, const QString &output,
bool onScratchpad, 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)) { if (nodeLooksLikeView(node) && !isShellChrome(node)) {
WindowInfo window; WindowInfo window;
window.id = jsonId(node.value(QStringLiteral("id"))); window.id = jsonId(node.value(QStringLiteral("id")));
@@ -580,18 +562,18 @@ void SwayWindowBackend::collect(const QJsonObject &node,
window.workspace = nextWorkspace; window.workspace = nextWorkspace;
window.output = nextOutput; window.output = nextOutput;
window.focused = node.value(QStringLiteral("focused")).toBool(); 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.fullscreen = node.value(QStringLiteral("fullscreen_mode")).toInt() > 0;
window.minimized = nextScratchpad; window.minimized = nextScratchpad;
applyFrameGeometry(window, node, parent); applyFrameGeometry(window, node, nextFrame);
windows->append(window); windows->append(window);
} }
const QJsonArray nodes = node.value(QStringLiteral("nodes")).toArray(); const QJsonArray nodes = node.value(QStringLiteral("nodes")).toArray();
for (const QJsonValue &child : nodes) 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(); const QJsonArray floating = node.value(QStringLiteral("floating_nodes")).toArray();
for (const QJsonValue &child : floating) 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; const WindowInfo *findWindow(qint64 windowId) const;
static bool isShellChrome(const QJsonObject &node); static bool isShellChrome(const QJsonObject &node);
static void collect(const QJsonObject &node, static void collect(const QJsonObject &node,
const QJsonObject &parent, const QJsonObject &frame,
const QString &workspace, const QString &workspace,
const QString &output, const QString &output,
bool onScratchpad, bool onScratchpad,
@@ -10,14 +10,15 @@ Window {
readonly property bool active: WindowService.hasFocusedWindow readonly property bool active: WindowService.hasFocusedWindow
&& !WindowService.focusedWindowFullscreen && !WindowService.focusedWindowFullscreen
&& !ShellState.launcherOpen && !ShellState.launcherOpen
readonly property int chromeX: WindowService.focusedWindowX + Theme.windowChromeInset readonly property int chromeX: WindowService.focusedWindowX
+ Theme.windowChromeInset
- WindowService.focusedWorkspaceX
readonly property int chromeY: { readonly property int chromeY: {
const barY = WindowService.focusedWindowTitleBarY const barH = WindowService.focusedWindowTitleBarHeight
const barH = Math.max(WindowService.focusedWindowTitleBarHeight, height) const centered = WindowService.focusedWindowTitleBarY
return barY + Math.round((barH - height) / 2) + Math.max(0, Math.round((barH - height) / 2))
return centered - WindowService.focusedWorkspaceY
} }
property real renderedChromeX: chromeX
property real renderedChromeY: chromeY
title: qsTr("Nebula Window Chrome") title: qsTr("Nebula Window Chrome")
color: "transparent" color: "transparent"
@@ -29,25 +30,11 @@ Window {
LayerShell.Window.scope: "nebula-window-chrome" LayerShell.Window.scope: "nebula-window-chrome"
LayerShell.Window.layer: LayerShell.Window.LayerTop LayerShell.Window.layer: LayerShell.Window.LayerTop
LayerShell.Window.anchors: LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorTop LayerShell.Window.anchors: LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorTop
LayerShell.Window.margins.left: Math.max(0, Math.round(renderedChromeX)) LayerShell.Window.margins.left: Math.max(0, chromeX)
LayerShell.Window.margins.top: Math.max(0, Math.round(renderedChromeY)) LayerShell.Window.margins.top: Math.max(0, chromeY)
LayerShell.Window.exclusionZone: 0 LayerShell.Window.exclusionZone: 0
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
Behavior on renderedChromeX {
NumberAnimation {
duration: 34
easing.type: Easing.OutQuad
}
}
Behavior on renderedChromeY {
NumberAnimation {
duration: 34
easing.type: Easing.OutQuad
}
}
WindowControls { WindowControls {
id: controls id: controls
anchors.centerIn: parent anchors.centerIn: parent