Align window chrome with Sway title bars

Adds focused title-bar geometry to the window service and updates the Sway backend to derive frame data more reliably (including parent node fallback and content rect tracking for wrapped/floating views). Window chrome positioning now centers against the actual title bar instead of deco offsets, improving alignment. Also refreshes visual sizing/padding for traffic-light controls and Sway titlebar spacing to match the new chrome metrics.
This commit is contained in:
2026-08-26 22:45:50 +12:00
parent 4321209e92
commit 4d70014242
9 changed files with 100 additions and 33 deletions
+1 -1
View File
@@ -54,7 +54,7 @@ focus_follows_mouse yes
# Layer-shell surfaces (desktop, top bar, launcher, window chrome) are not # Layer-shell surfaces (desktop, top bar, launcher, window chrome) are not
# xdg_shell/XWayland views and are not decorated by these settings. # xdg_shell/XWayland views and are not decorated by these settings.
font pango:Ubuntu 10 font pango:Ubuntu 10
titlebar_padding 12 8 titlebar_padding 14 10
titlebar_border_thickness 1 titlebar_border_thickness 1
default_border normal 1 default_border normal 1
default_floating_border normal 1 default_floating_border normal 1
+28 -1
View File
@@ -146,6 +146,16 @@ int WindowService::focusedWindowDecoHeight() const
return m_focusedWindowDecoHeight; return m_focusedWindowDecoHeight;
} }
int WindowService::focusedWindowTitleBarY() const
{
return m_focusedWindowTitleBarY;
}
int WindowService::focusedWindowTitleBarHeight() const
{
return m_focusedWindowTitleBarHeight;
}
bool WindowService::focusWindow(const QString &windowId) bool WindowService::focusWindow(const QString &windowId)
{ {
return m_backend->focusWindow(parseWindowId(windowId)); return m_backend->focusWindow(parseWindowId(windowId));
@@ -321,6 +331,8 @@ void WindowService::publishFocusedGeometry(const WindowInfo *focused)
int decoY = 0; int decoY = 0;
int decoWidth = 0; int decoWidth = 0;
int decoHeight = 0; int decoHeight = 0;
int titleBarY = 0;
int titleBarHeight = 0;
bool fullscreen = false; bool fullscreen = false;
if (focused && !focused->minimized) { if (focused && !focused->minimized) {
@@ -333,6 +345,17 @@ void WindowService::publishFocusedGeometry(const WindowInfo *focused)
decoWidth = focused->decoWidth; decoWidth = focused->decoWidth;
decoHeight = focused->decoHeight; decoHeight = focused->decoHeight;
fullscreen = focused->fullscreen; fullscreen = focused->fullscreen;
titleBarHeight = focused->decoHeight;
if (titleBarHeight <= 0)
titleBarHeight = focused->contentY;
if (titleBarHeight <= 0)
titleBarHeight = 32;
if (focused->decoHeight > 0 || focused->contentY > 0)
titleBarY = focused->y + focused->decoY;
else
titleBarY = focused->y - titleBarHeight;
} }
if (m_focusedWindowFullscreen != fullscreen) { if (m_focusedWindowFullscreen != fullscreen) {
@@ -347,7 +370,9 @@ void WindowService::publishFocusedGeometry(const WindowInfo *focused)
|| m_focusedWindowDecoX != decoX || m_focusedWindowDecoX != decoX
|| m_focusedWindowDecoY != decoY || m_focusedWindowDecoY != decoY
|| m_focusedWindowDecoWidth != decoWidth || m_focusedWindowDecoWidth != decoWidth
|| m_focusedWindowDecoHeight != decoHeight) { || m_focusedWindowDecoHeight != decoHeight
|| m_focusedWindowTitleBarY != titleBarY
|| m_focusedWindowTitleBarHeight != titleBarHeight) {
m_focusedWindowX = x; m_focusedWindowX = x;
m_focusedWindowY = y; m_focusedWindowY = y;
m_focusedWindowWidth = width; m_focusedWindowWidth = width;
@@ -356,6 +381,8 @@ void WindowService::publishFocusedGeometry(const WindowInfo *focused)
m_focusedWindowDecoY = decoY; m_focusedWindowDecoY = decoY;
m_focusedWindowDecoWidth = decoWidth; m_focusedWindowDecoWidth = decoWidth;
m_focusedWindowDecoHeight = decoHeight; m_focusedWindowDecoHeight = decoHeight;
m_focusedWindowTitleBarY = titleBarY;
m_focusedWindowTitleBarHeight = titleBarHeight;
emit focusedWindowGeometryChanged(); emit focusedWindowGeometryChanged();
} }
} }
+6
View File
@@ -37,6 +37,8 @@ class WindowService : public QObject
Q_PROPERTY(int focusedWindowDecoY READ focusedWindowDecoY NOTIFY focusedWindowGeometryChanged) Q_PROPERTY(int focusedWindowDecoY READ focusedWindowDecoY NOTIFY focusedWindowGeometryChanged)
Q_PROPERTY(int focusedWindowDecoWidth READ focusedWindowDecoWidth NOTIFY focusedWindowGeometryChanged) Q_PROPERTY(int focusedWindowDecoWidth READ focusedWindowDecoWidth NOTIFY focusedWindowGeometryChanged)
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 focusedWindowTitleBarHeight READ focusedWindowTitleBarHeight NOTIFY focusedWindowGeometryChanged)
public: public:
enum SnapEdge { enum SnapEdge {
@@ -69,6 +71,8 @@ public:
int focusedWindowDecoY() const; int focusedWindowDecoY() const;
int focusedWindowDecoWidth() const; int focusedWindowDecoWidth() const;
int focusedWindowDecoHeight() const; int focusedWindowDecoHeight() const;
int focusedWindowTitleBarY() const;
int focusedWindowTitleBarHeight() 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);
@@ -118,5 +122,7 @@ private:
int m_focusedWindowDecoY = 0; int m_focusedWindowDecoY = 0;
int m_focusedWindowDecoWidth = 0; int m_focusedWindowDecoWidth = 0;
int m_focusedWindowDecoHeight = 0; int m_focusedWindowDecoHeight = 0;
int m_focusedWindowTitleBarY = 0;
int m_focusedWindowTitleBarHeight = 0;
QTimer *m_geometryTimer = nullptr; QTimer *m_geometryTimer = nullptr;
}; };
+2
View File
@@ -26,6 +26,8 @@ struct WindowInfo
int decoY = 0; int decoY = 0;
int decoWidth = 0; int decoWidth = 0;
int decoHeight = 0; int decoHeight = 0;
int contentX = 0;
int contentY = 0;
}; };
struct WorkspaceInfo struct WorkspaceInfo
@@ -66,6 +66,41 @@ bool nodeLooksLikeView(const QJsonObject &node)
return !properties.isEmpty(); return !properties.isEmpty();
} }
void applyFrameGeometry(WindowInfo &window, const QJsonObject &node, const QJsonObject &parent)
{
QJsonObject rect = node.value(QStringLiteral("rect")).toObject();
QJsonObject deco = node.value(QStringLiteral("deco_rect")).toObject();
QJsonObject content = node.value(QStringLiteral("window_rect")).toObject();
int decoHeight = deco.value(QStringLiteral("height")).toInt();
int contentY = content.value(QStringLiteral("y")).toInt();
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.y = rect.value(QStringLiteral("y")).toInt();
window.width = rect.value(QStringLiteral("width")).toInt();
window.height = rect.value(QStringLiteral("height")).toInt();
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.contentX = content.value(QStringLiteral("x")).toInt();
window.contentY = contentY;
}
} // namespace } // namespace
SwayWindowBackend::SwayWindowBackend(QObject *parent) SwayWindowBackend::SwayWindowBackend(QObject *parent)
@@ -247,7 +282,7 @@ bool SwayWindowBackend::updateFocusedGeometry()
QVector<WindowInfo> windows; QVector<WindowInfo> windows;
QVector<WorkspaceInfo> workspaces; QVector<WorkspaceInfo> workspaces;
collect(tree.object(), {}, {}, false, &windows, &workspaces); collect(tree.object(), {}, {}, {}, false, &windows, &workspaces);
for (WindowInfo &window : windows) { for (WindowInfo &window : windows) {
if (window.id != m_focusedWindowId) if (window.id != m_focusedWindowId)
@@ -263,6 +298,8 @@ bool SwayWindowBackend::updateFocusedGeometry()
&& cached.decoY == window.decoY && cached.decoY == window.decoY
&& cached.decoWidth == window.decoWidth && cached.decoWidth == window.decoWidth
&& cached.decoHeight == window.decoHeight && cached.decoHeight == window.decoHeight
&& cached.contentX == window.contentX
&& cached.contentY == window.contentY
&& cached.fullscreen == window.fullscreen) && cached.fullscreen == window.fullscreen)
return false; return false;
cached.x = window.x; cached.x = window.x;
@@ -273,6 +310,8 @@ bool SwayWindowBackend::updateFocusedGeometry()
cached.decoY = window.decoY; cached.decoY = window.decoY;
cached.decoWidth = window.decoWidth; cached.decoWidth = window.decoWidth;
cached.decoHeight = window.decoHeight; cached.decoHeight = window.decoHeight;
cached.contentX = window.contentX;
cached.contentY = window.contentY;
cached.fullscreen = window.fullscreen; cached.fullscreen = window.fullscreen;
return true; return true;
} }
@@ -328,7 +367,7 @@ void SwayWindowBackend::refresh()
QVector<WindowInfo> windows; QVector<WindowInfo> windows;
QVector<WorkspaceInfo> workspaces; QVector<WorkspaceInfo> workspaces;
collect(tree.object(), {}, {}, false, &windows, &workspaces); collect(tree.object(), {}, {}, {}, false, &windows, &workspaces);
QHash<QString, int> counts; QHash<QString, int> counts;
qint64 focused = 0; qint64 focused = 0;
@@ -481,6 +520,7 @@ bool SwayWindowBackend::isShellChrome(const QJsonObject &node)
} }
void SwayWindowBackend::collect(const QJsonObject &node, void SwayWindowBackend::collect(const QJsonObject &node,
const QJsonObject &parent,
const QString &workspace, const QString &workspace,
const QString &output, const QString &output,
bool onScratchpad, bool onScratchpad,
@@ -530,27 +570,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); window.floating = nodeIsFloating(node) || nodeIsFloating(parent);
window.fullscreen = node.value(QStringLiteral("fullscreen_mode")).toInt() > 0; window.fullscreen = node.value(QStringLiteral("fullscreen_mode")).toInt() > 0;
window.minimized = nextScratchpad; window.minimized = nextScratchpad;
const QJsonObject rect = node.value(QStringLiteral("rect")).toObject(); applyFrameGeometry(window, node, parent);
window.x = rect.value(QStringLiteral("x")).toInt();
window.y = rect.value(QStringLiteral("y")).toInt();
window.width = rect.value(QStringLiteral("width")).toInt();
window.height = rect.value(QStringLiteral("height")).toInt();
const QJsonObject deco = node.value(QStringLiteral("deco_rect")).toObject();
window.decoX = deco.value(QStringLiteral("x")).toInt();
window.decoY = deco.value(QStringLiteral("y")).toInt();
window.decoWidth = deco.value(QStringLiteral("width")).toInt();
window.decoHeight = deco.value(QStringLiteral("height")).toInt();
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(), nextWorkspace, nextOutput, nextScratchpad, windows, workspaces); collect(child.toObject(), node, 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(), nextWorkspace, nextOutput, nextScratchpad, windows, workspaces); collect(child.toObject(), node, nextWorkspace, nextOutput, nextScratchpad, windows, workspaces);
} }
@@ -44,6 +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 QString &workspace, const QString &workspace,
const QString &output, const QString &output,
bool onScratchpad, bool onScratchpad,
+2 -2
View File
@@ -52,9 +52,9 @@ QtObject {
readonly property int topBarHeight: 40 readonly property int topBarHeight: 40
readonly property int controlHeight: 28 readonly property int controlHeight: 28
readonly property int searchHeight: 36 readonly property int searchHeight: 36
readonly property int trafficLightSize: 12 readonly property int trafficLightSize: 18
readonly property int trafficLightGap: 8 readonly property int trafficLightGap: 8
readonly property int windowChromeInset: 10 readonly property int windowChromeInset: 12
readonly property int fontSize: 13 readonly property int fontSize: 13
readonly property int fontSizeApp: 12 readonly property int fontSizeApp: 12
@@ -13,8 +13,8 @@ Item {
signal clicked() signal clicked()
implicitWidth: Theme.trafficLightSize + 2 implicitWidth: Theme.trafficLightSize + 6
implicitHeight: Theme.trafficLightSize + 2 implicitHeight: Theme.trafficLightSize + 6
Accessible.role: Accessible.Button Accessible.role: Accessible.Button
Accessible.name: root.label Accessible.name: root.label
Accessible.onPressAction: root.clicked() Accessible.onPressAction: root.clicked()
@@ -50,17 +50,20 @@ Item {
Canvas { Canvas {
id: glyph id: glyph
anchors.centerIn: parent anchors.centerIn: parent
width: 8 width: Math.round(Theme.trafficLightSize * 0.58)
height: 8 height: width
antialiasing: true antialiasing: true
opacity: root.showGlyph ? 1 : 0 opacity: root.showGlyph ? 1 : 0
visible: opacity > 0.01 visible: opacity > 0.01
onWidthChanged: requestPaint()
onHeightChanged: requestPaint()
onPaint: { onPaint: {
const ctx = getContext("2d") const ctx = getContext("2d")
ctx.reset() ctx.reset()
ctx.scale(width / 8, height / 8)
ctx.strokeStyle = Qt.rgba(0.18, 0.12, 0.1, 0.78) ctx.strokeStyle = Qt.rgba(0.18, 0.12, 0.1, 0.78)
ctx.fillStyle = ctx.strokeStyle ctx.fillStyle = ctx.strokeStyle
ctx.lineWidth = 1.15 ctx.lineWidth = 1.35
ctx.lineCap = "round" ctx.lineCap = "round"
ctx.lineJoin = "round" ctx.lineJoin = "round"
@@ -10,15 +10,12 @@ 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 readonly property int chromeX: WindowService.focusedWindowX + Theme.windowChromeInset
+ WindowService.focusedWindowDecoX
+ Theme.windowChromeInset
readonly property int chromeY: { readonly property int chromeY: {
const top = WindowService.focusedWindowY + WindowService.focusedWindowDecoY const barY = WindowService.focusedWindowTitleBarY
const decoHeight = WindowService.focusedWindowDecoHeight const barH = Math.max(WindowService.focusedWindowTitleBarHeight, height)
if (decoHeight <= 0) const centered = barY + Math.round((barH - height) / 2)
return top + 6 return centered - Theme.topBarHeight
return top + Math.max(0, Math.round((decoHeight - height) / 2))
} }
title: qsTr("Nebula Window Chrome") title: qsTr("Nebula Window Chrome")