Add floating traffic-light window chrome

Introduces a dedicated layer-shell `WindowChromeSurface` that renders macOS-style traffic-light controls over the focused Sway window titlebar, replacing the old top-bar window controls. Adds theme tokens and a reusable `TrafficLightButton` component, updates shell QML/CMake wiring, and adjusts Sway titlebar padding/title format so window titles do not overlap the overlay. Window tracking was extended with focused-window geometry/deco/fullscreen properties plus backend refresh support so the chrome follows window movement and state changes in near real time.
This commit is contained in:
2026-08-26 22:38:33 +12:00
parent 08e67845a5
commit 4321209e92
14 changed files with 452 additions and 92 deletions
+145 -7
View File
@@ -27,11 +27,15 @@ WindowService::WindowService(QObject *parent)
, m_backend(new SwayWindowBackend(this))
, m_model(new WindowModel(this))
, m_workspaces(new WorkspaceModel(this))
, m_geometryTimer(new QTimer(this))
{
connect(m_backend, &WindowBackend::connectedChanged, this, &WindowService::connectedChanged);
connect(m_backend, &WindowBackend::connectedChanged, this, &WindowService::syncFromBackend);
connect(m_backend, &WindowBackend::windowsChanged, this, &WindowService::syncFromBackend);
m_geometryTimer->setInterval(33);
connect(m_geometryTimer, &QTimer::timeout, this, &WindowService::syncFocusedGeometry);
QTimer::singleShot(0, this, [this]() {
bindApplicationService();
if (m_applications)
@@ -97,6 +101,51 @@ bool WindowService::focusedWindowMaximized() const
return m_focusedWindowMaximized;
}
bool WindowService::focusedWindowFullscreen() const
{
return m_focusedWindowFullscreen;
}
int WindowService::focusedWindowX() const
{
return m_focusedWindowX;
}
int WindowService::focusedWindowY() const
{
return m_focusedWindowY;
}
int WindowService::focusedWindowWidth() const
{
return m_focusedWindowWidth;
}
int WindowService::focusedWindowHeight() const
{
return m_focusedWindowHeight;
}
int WindowService::focusedWindowDecoX() const
{
return m_focusedWindowDecoX;
}
int WindowService::focusedWindowDecoY() const
{
return m_focusedWindowDecoY;
}
int WindowService::focusedWindowDecoWidth() const
{
return m_focusedWindowDecoWidth;
}
int WindowService::focusedWindowDecoHeight() const
{
return m_focusedWindowDecoHeight;
}
bool WindowService::focusWindow(const QString &windowId)
{
return m_backend->focusWindow(parseWindowId(windowId));
@@ -197,15 +246,24 @@ void WindowService::syncFromBackend()
QString appId;
QString appName;
bool maximized = false;
const WindowInfo *focusedInfo = nullptr;
if (const WindowInfo *focused = m_model->windowById(focusedId)) {
if (!focused->minimized) {
windowId = QString::number(focused->id);
title = focused->title;
appId = focused->appId.isEmpty() ? focused->windowClass : focused->appId;
appName = focused->displayName;
maximized = focused->maximized;
}
if (!focused->minimized)
focusedInfo = focused;
}
if (!focusedInfo && !m_focusedWindowId.isEmpty()) {
const WindowInfo *previous = m_model->windowById(parseWindowId(m_focusedWindowId));
if (previous && !previous->minimized)
focusedInfo = previous;
}
if (focusedInfo) {
windowId = QString::number(focusedInfo->id);
title = focusedInfo->title;
appId = focusedInfo->appId.isEmpty() ? focusedInfo->windowClass : focusedInfo->appId;
appName = focusedInfo->displayName;
maximized = focusedInfo->maximized;
}
if (m_focusedWindowId != windowId
@@ -220,6 +278,86 @@ void WindowService::syncFromBackend()
m_focusedWindowMaximized = maximized;
emit focusChanged();
}
publishFocusedGeometry(focusedInfo);
if (hasFocusedWindow()) {
if (!m_geometryTimer->isActive())
m_geometryTimer->start();
} else if (m_geometryTimer->isActive()) {
m_geometryTimer->stop();
}
}
void WindowService::syncFocusedGeometry()
{
if (m_focusedWindowId.isEmpty()) {
m_geometryTimer->stop();
publishFocusedGeometry(nullptr);
return;
}
m_backend->updateFocusedGeometry();
const QVector<WindowInfo> windows = m_backend->windows();
const qint64 id = parseWindowId(m_focusedWindowId);
const WindowInfo *focused = nullptr;
for (const WindowInfo &window : windows) {
if (window.id == id) {
focused = &window;
break;
}
}
publishFocusedGeometry(focused);
}
void WindowService::publishFocusedGeometry(const WindowInfo *focused)
{
int x = 0;
int y = 0;
int width = 0;
int height = 0;
int decoX = 0;
int decoY = 0;
int decoWidth = 0;
int decoHeight = 0;
bool fullscreen = false;
if (focused && !focused->minimized) {
x = focused->x;
y = focused->y;
width = focused->width;
height = focused->height;
decoX = focused->decoX;
decoY = focused->decoY;
decoWidth = focused->decoWidth;
decoHeight = focused->decoHeight;
fullscreen = focused->fullscreen;
}
if (m_focusedWindowFullscreen != fullscreen) {
m_focusedWindowFullscreen = fullscreen;
emit focusChanged();
}
if (m_focusedWindowX != x
|| m_focusedWindowY != y
|| m_focusedWindowWidth != width
|| m_focusedWindowHeight != height
|| m_focusedWindowDecoX != decoX
|| m_focusedWindowDecoY != decoY
|| m_focusedWindowDecoWidth != decoWidth
|| m_focusedWindowDecoHeight != decoHeight) {
m_focusedWindowX = x;
m_focusedWindowY = y;
m_focusedWindowWidth = width;
m_focusedWindowHeight = height;
m_focusedWindowDecoX = decoX;
m_focusedWindowDecoY = decoY;
m_focusedWindowDecoWidth = decoWidth;
m_focusedWindowDecoHeight = decoHeight;
emit focusedWindowGeometryChanged();
}
}
void WindowService::resolveNames(QVector<WindowInfo> &windows) const