From 4321209e929dc5d9cac8169200829b265bbae801 Mon Sep 17 00:00:00 2001 From: Andrew Zambazos Date: Wed, 26 Aug 2026 22:38:33 +1200 Subject: [PATCH] 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. --- compositor/sway/nebula-sway.conf | 13 +- core/windows/WindowBackend.hpp | 1 + core/windows/WindowService.cpp | 152 +++++++++++++++++- core/windows/WindowService.hpp | 32 ++++ core/windows/WindowTypes.hpp | 4 + .../backends/sway/SwayWindowBackend.cpp | 54 ++++++- .../backends/sway/SwayWindowBackend.hpp | 1 + packages/nebula-ui/qml/Nebula/UI/Theme.qml | 6 + shells/desktop/shell/CMakeLists.txt | 2 + shells/desktop/shell/qml/Main.qml | 1 + .../desktop/shell/qml/components/TopBar.qml | 24 +-- .../qml/components/TrafficLightButton.qml | 123 ++++++++++++++ .../shell/qml/components/WindowControls.qml | 88 ++++------ .../qml/surfaces/WindowChromeSurface.qml | 43 +++++ 14 files changed, 452 insertions(+), 92 deletions(-) create mode 100644 shells/desktop/shell/qml/components/TrafficLightButton.qml create mode 100644 shells/desktop/shell/qml/surfaces/WindowChromeSurface.qml diff --git a/compositor/sway/nebula-sway.conf b/compositor/sway/nebula-sway.conf index 5d73305..a2f5674 100644 --- a/compositor/sway/nebula-sway.conf +++ b/compositor/sway/nebula-sway.conf @@ -46,17 +46,20 @@ focus_follows_mouse yes # Sway draws a restrained title bar and border on normal application windows # so they can be identified, dragged, and resized with the mouse. # -# This is temporary infrastructure. Nebula does not yet own client/server-side -# decorations. Do not treat these colours or metrics as the final design. +# Nebula overlays traffic-light controls on the focused window title bar. +# Horizontal padding keeps the Sway title text clear of those buttons. +# Menu-style chrome (application name, future File/Edit) stays in the top bar. # -# Layer-shell surfaces (desktop, top bar, launcher) are not xdg_shell/XWayland -# views and are not decorated by these settings. +# This is temporary infrastructure until Nebula owns server-side decorations. +# Layer-shell surfaces (desktop, top bar, launcher, window chrome) are not +# xdg_shell/XWayland views and are not decorated by these settings. font pango:Ubuntu 10 -titlebar_padding 7 3 +titlebar_padding 12 8 titlebar_border_thickness 1 default_border normal 1 default_floating_border normal 1 hide_edge_borders none +title_format " %title" # class border backgr. text indicator child_border client.focused #3d4d62 #1a222e #e8ecf2 #7ea0c4 #3d4d62 diff --git a/core/windows/WindowBackend.hpp b/core/windows/WindowBackend.hpp index a86a7dd..636690c 100644 --- a/core/windows/WindowBackend.hpp +++ b/core/windows/WindowBackend.hpp @@ -38,6 +38,7 @@ public: virtual bool resizeWindow(qint64 windowId, int width, int height) = 0; virtual bool snapWindow(qint64 windowId, SnapEdge edge) = 0; virtual bool switchWorkspace(const QString &workspaceId) = 0; + virtual bool updateFocusedGeometry() { return false; } signals: void connectedChanged(); diff --git a/core/windows/WindowService.cpp b/core/windows/WindowService.cpp index 4dce015..075e428 100644 --- a/core/windows/WindowService.cpp +++ b/core/windows/WindowService.cpp @@ -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 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 &windows) const diff --git a/core/windows/WindowService.hpp b/core/windows/WindowService.hpp index 7817538..43011fa 100644 --- a/core/windows/WindowService.hpp +++ b/core/windows/WindowService.hpp @@ -7,6 +7,7 @@ #include #include #include +#include #include #include @@ -27,6 +28,15 @@ class WindowService : public QObject Q_PROPERTY(QString focusedApplicationId READ focusedApplicationId NOTIFY focusChanged) Q_PROPERTY(QString focusedApplicationName READ focusedApplicationName NOTIFY focusChanged) Q_PROPERTY(bool focusedWindowMaximized READ focusedWindowMaximized NOTIFY focusChanged) + Q_PROPERTY(bool focusedWindowFullscreen READ focusedWindowFullscreen NOTIFY focusChanged) + Q_PROPERTY(int focusedWindowX READ focusedWindowX NOTIFY focusedWindowGeometryChanged) + Q_PROPERTY(int focusedWindowY READ focusedWindowY NOTIFY focusedWindowGeometryChanged) + Q_PROPERTY(int focusedWindowWidth READ focusedWindowWidth NOTIFY focusedWindowGeometryChanged) + Q_PROPERTY(int focusedWindowHeight READ focusedWindowHeight NOTIFY focusedWindowGeometryChanged) + Q_PROPERTY(int focusedWindowDecoX READ focusedWindowDecoX NOTIFY focusedWindowGeometryChanged) + Q_PROPERTY(int focusedWindowDecoY READ focusedWindowDecoY NOTIFY focusedWindowGeometryChanged) + Q_PROPERTY(int focusedWindowDecoWidth READ focusedWindowDecoWidth NOTIFY focusedWindowGeometryChanged) + Q_PROPERTY(int focusedWindowDecoHeight READ focusedWindowDecoHeight NOTIFY focusedWindowGeometryChanged) public: enum SnapEdge { @@ -50,6 +60,15 @@ public: QString focusedApplicationId() const; QString focusedApplicationName() const; bool focusedWindowMaximized() const; + bool focusedWindowFullscreen() const; + int focusedWindowX() const; + int focusedWindowY() const; + int focusedWindowWidth() const; + int focusedWindowHeight() const; + int focusedWindowDecoX() const; + int focusedWindowDecoY() const; + int focusedWindowDecoWidth() const; + int focusedWindowDecoHeight() const; Q_INVOKABLE bool focusWindow(const QString &windowId); Q_INVOKABLE bool closeWindow(const QString &windowId); @@ -69,9 +88,12 @@ signals: void connectedChanged(); void windowCountChanged(); void focusChanged(); + void focusedWindowGeometryChanged(); private: void syncFromBackend(); + void syncFocusedGeometry(); + void publishFocusedGeometry(const WindowInfo *focused); void resolveNames(QVector &windows) const; void bindApplicationService(); static qint64 parseWindowId(const QString &windowId); @@ -87,4 +109,14 @@ private: QString m_focusedApplicationId; QString m_focusedApplicationName; bool m_focusedWindowMaximized = false; + bool m_focusedWindowFullscreen = false; + int m_focusedWindowX = 0; + int m_focusedWindowY = 0; + int m_focusedWindowWidth = 0; + int m_focusedWindowHeight = 0; + int m_focusedWindowDecoX = 0; + int m_focusedWindowDecoY = 0; + int m_focusedWindowDecoWidth = 0; + int m_focusedWindowDecoHeight = 0; + QTimer *m_geometryTimer = nullptr; }; diff --git a/core/windows/WindowTypes.hpp b/core/windows/WindowTypes.hpp index 1f12485..d20e06c 100644 --- a/core/windows/WindowTypes.hpp +++ b/core/windows/WindowTypes.hpp @@ -22,6 +22,10 @@ struct WindowInfo int y = 0; int width = 0; int height = 0; + int decoX = 0; + int decoY = 0; + int decoWidth = 0; + int decoHeight = 0; }; struct WorkspaceInfo diff --git a/core/windows/backends/sway/SwayWindowBackend.cpp b/core/windows/backends/sway/SwayWindowBackend.cpp index ba2650e..298b8da 100644 --- a/core/windows/backends/sway/SwayWindowBackend.cpp +++ b/core/windows/backends/sway/SwayWindowBackend.cpp @@ -236,6 +236,52 @@ bool SwayWindowBackend::switchWorkspace(const QString &workspaceId) return run(QStringLiteral("workspace \"%1\"").arg(quoteCriteriaValue(workspaceId))); } +bool SwayWindowBackend::updateFocusedGeometry() +{ + if (!m_ipc->isConnected() || m_focusedWindowId == 0) + return false; + + const QJsonDocument tree = m_ipc->request(SwayIpcClient::GetTree); + if (!tree.isObject()) + return false; + + QVector windows; + QVector workspaces; + collect(tree.object(), {}, {}, false, &windows, &workspaces); + + for (WindowInfo &window : windows) { + if (window.id != m_focusedWindowId) + continue; + for (WindowInfo &cached : m_windows) { + if (cached.id != window.id) + continue; + if (cached.x == window.x + && cached.y == window.y + && cached.width == window.width + && cached.height == window.height + && cached.decoX == window.decoX + && cached.decoY == window.decoY + && cached.decoWidth == window.decoWidth + && cached.decoHeight == window.decoHeight + && cached.fullscreen == window.fullscreen) + return false; + cached.x = window.x; + cached.y = window.y; + cached.width = window.width; + cached.height = window.height; + cached.decoX = window.decoX; + cached.decoY = window.decoY; + cached.decoWidth = window.decoWidth; + cached.decoHeight = window.decoHeight; + cached.fullscreen = window.fullscreen; + return true; + } + break; + } + + return false; +} + void SwayWindowBackend::tryConnect() { if (m_ipc->isConnected()) @@ -422,7 +468,8 @@ bool SwayWindowBackend::isShellChrome(const QJsonObject &node) const QString name = node.value(QStringLiteral("name")).toString(); if (name == QLatin1String("Nebula Desktop") || name == QLatin1String("Nebula Top Bar") - || name == QLatin1String("Nebula Launcher")) { + || name == QLatin1String("Nebula Launcher") + || name == QLatin1String("Nebula Window Chrome")) { return true; } @@ -491,6 +538,11 @@ void SwayWindowBackend::collect(const QJsonObject &node, 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); } diff --git a/core/windows/backends/sway/SwayWindowBackend.hpp b/core/windows/backends/sway/SwayWindowBackend.hpp index ab3cd3b..32ec2b7 100644 --- a/core/windows/backends/sway/SwayWindowBackend.hpp +++ b/core/windows/backends/sway/SwayWindowBackend.hpp @@ -32,6 +32,7 @@ public: bool resizeWindow(qint64 windowId, int width, int height) override; bool snapWindow(qint64 windowId, SnapEdge edge) override; bool switchWorkspace(const QString &workspaceId) override; + bool updateFocusedGeometry() override; private: void tryConnect(); diff --git a/packages/nebula-ui/qml/Nebula/UI/Theme.qml b/packages/nebula-ui/qml/Nebula/UI/Theme.qml index f0f3bb0..1239256 100644 --- a/packages/nebula-ui/qml/Nebula/UI/Theme.qml +++ b/packages/nebula-ui/qml/Nebula/UI/Theme.qml @@ -29,6 +29,9 @@ QtObject { readonly property color pressedFill: Qt.rgba(1, 1, 1, 0.10) readonly property color dangerSoft: Qt.rgba(201 / 255, 136 / 255, 136 / 255, 0.18) readonly property color dangerSoftStrong: Qt.rgba(201 / 255, 136 / 255, 136 / 255, 0.28) + readonly property color trafficClose: "#ff5f57" + readonly property color trafficMinimise: "#febc2e" + readonly property color trafficZoom: "#28c840" readonly property color tileHover: Qt.rgba(1, 1, 1, 0.05) readonly property color iconWell: Qt.rgba(1, 1, 1, 0.05) readonly property color iconWellHighlight: Qt.rgba(1, 1, 1, 0.06) @@ -49,6 +52,9 @@ QtObject { readonly property int topBarHeight: 40 readonly property int controlHeight: 28 readonly property int searchHeight: 36 + readonly property int trafficLightSize: 12 + readonly property int trafficLightGap: 8 + readonly property int windowChromeInset: 10 readonly property int fontSize: 13 readonly property int fontSizeApp: 12 diff --git a/shells/desktop/shell/CMakeLists.txt b/shells/desktop/shell/CMakeLists.txt index 68c6cb9..3fb10d0 100644 --- a/shells/desktop/shell/CMakeLists.txt +++ b/shells/desktop/shell/CMakeLists.txt @@ -46,9 +46,11 @@ set(NEBULA_SHELL_QML_FILES qml/ShellState.qml qml/surfaces/DesktopSurface.qml qml/surfaces/TopBarSurface.qml + qml/surfaces/WindowChromeSurface.qml qml/surfaces/LauncherSurface.qml qml/components/TopBar.qml qml/components/WindowControls.qml + qml/components/TrafficLightButton.qml qml/components/NebulaButton.qml qml/components/Clock.qml qml/components/SystemArea.qml diff --git a/shells/desktop/shell/qml/Main.qml b/shells/desktop/shell/qml/Main.qml index 22b8221..08eaf38 100644 --- a/shells/desktop/shell/qml/Main.qml +++ b/shells/desktop/shell/qml/Main.qml @@ -5,6 +5,7 @@ QtObject { readonly property DesktopSurface desktop: DesktopSurface {} readonly property TopBarSurface topBar: TopBarSurface {} + readonly property WindowChromeSurface windowChrome: WindowChromeSurface {} readonly property LauncherSurface launcher: LauncherSurface {} readonly property Shortcut closeLauncherShortcut: Shortcut { diff --git a/shells/desktop/shell/qml/components/TopBar.qml b/shells/desktop/shell/qml/components/TopBar.qml index 844033d..d04d6d9 100644 --- a/shells/desktop/shell/qml/components/TopBar.qml +++ b/shells/desktop/shell/qml/components/TopBar.qml @@ -23,7 +23,7 @@ Item { id: left anchors.left: parent.left anchors.leftMargin: 6 - anchors.right: right.left + anchors.right: systemArea.left anchors.rightMargin: 12 anchors.verticalCenter: parent.verticalCenter spacing: 8 @@ -54,28 +54,10 @@ Item { } } - Row { - id: right + SystemArea { + id: systemArea anchors.right: parent.right anchors.rightMargin: 10 anchors.verticalCenter: parent.verticalCenter - spacing: windowControls.visible ? 8 : 0 - - WindowControls { - id: windowControls - anchors.verticalCenter: parent.verticalCenter - } - - Rectangle { - visible: windowControls.visible - width: visible ? 1 : 0 - height: 14 - color: Theme.borderStrong - anchors.verticalCenter: parent.verticalCenter - } - - SystemArea { - anchors.verticalCenter: parent.verticalCenter - } } } diff --git a/shells/desktop/shell/qml/components/TrafficLightButton.qml b/shells/desktop/shell/qml/components/TrafficLightButton.qml new file mode 100644 index 0000000..e384a42 --- /dev/null +++ b/shells/desktop/shell/qml/components/TrafficLightButton.qml @@ -0,0 +1,123 @@ +import QtQuick +import Nebula.UI + +Item { + id: root + + property string label: "" + property color fill: Theme.trafficClose + property string glyph: "close" + property bool showGlyph: false + readonly property bool hovered: hover.containsMouse + readonly property bool pressed: hover.pressed + + signal clicked() + + implicitWidth: Theme.trafficLightSize + 2 + implicitHeight: Theme.trafficLightSize + 2 + Accessible.role: Accessible.Button + Accessible.name: root.label + Accessible.onPressAction: root.clicked() + + Rectangle { + id: disc + anchors.centerIn: parent + width: Theme.trafficLightSize + height: Theme.trafficLightSize + radius: width / 2 + color: hover.pressed ? Qt.darker(root.fill, 1.18) : root.fill + border.width: 1 + border.color: Qt.rgba(0, 0, 0, 0.22) + + Behavior on color { + ColorAnimation { + duration: Theme.animationFast + } + } + + Rectangle { + anchors.horizontalCenter: parent.horizontalCenter + anchors.top: parent.top + anchors.topMargin: 1 + width: parent.width - 4 + height: Math.max(2, parent.height * 0.38) + radius: height / 2 + color: Qt.rgba(1, 1, 1, 0.28) + Accessible.ignored: true + } + } + + Canvas { + id: glyph + anchors.centerIn: parent + width: 8 + height: 8 + antialiasing: true + opacity: root.showGlyph ? 1 : 0 + visible: opacity > 0.01 + onPaint: { + const ctx = getContext("2d") + ctx.reset() + ctx.strokeStyle = Qt.rgba(0.18, 0.12, 0.1, 0.78) + ctx.fillStyle = ctx.strokeStyle + ctx.lineWidth = 1.15 + ctx.lineCap = "round" + ctx.lineJoin = "round" + + switch (root.glyph) { + case "minimize": + ctx.beginPath() + ctx.moveTo(1.2, 4) + ctx.lineTo(6.8, 4) + ctx.stroke() + break + case "zoom": + ctx.beginPath() + ctx.moveTo(1.4, 4) + ctx.lineTo(6.6, 4) + ctx.moveTo(4, 1.4) + ctx.lineTo(4, 6.6) + ctx.stroke() + break + case "restore": + ctx.beginPath() + ctx.rect(2.3, 0.7, 4.3, 4.3) + ctx.stroke() + ctx.beginPath() + ctx.rect(0.7, 2.3, 4.3, 4.3) + ctx.stroke() + break + default: + ctx.beginPath() + ctx.moveTo(1.6, 1.6) + ctx.lineTo(6.4, 6.4) + ctx.moveTo(6.4, 1.6) + ctx.lineTo(1.6, 6.4) + ctx.stroke() + break + } + } + + Behavior on opacity { + NumberAnimation { + duration: Theme.animationFast + } + } + } + + onGlyphChanged: glyph.requestPaint() + onShowGlyphChanged: if (showGlyph) glyph.requestPaint() + + MouseArea { + id: hover + anchors.fill: parent + hoverEnabled: true + cursorShape: Qt.PointingHandCursor + onClicked: root.clicked() + } + + NebulaToolTip { + text: root.label + active: hover.containsMouse && !hover.pressed && root.label.length > 0 + } +} diff --git a/shells/desktop/shell/qml/components/WindowControls.qml b/shells/desktop/shell/qml/components/WindowControls.qml index 8fd92a8..76d7d28 100644 --- a/shells/desktop/shell/qml/components/WindowControls.qml +++ b/shells/desktop/shell/qml/components/WindowControls.qml @@ -6,71 +6,43 @@ Item { id: root readonly property bool maximized: WindowService.focusedWindowMaximized + readonly property bool hovered: closeButton.hovered || minimizeButton.hovered || zoomButton.hovered - visible: WindowService.hasFocusedWindow - implicitWidth: well.implicitWidth - implicitHeight: Theme.controlHeight - width: visible ? implicitWidth : 0 + implicitWidth: row.implicitWidth + implicitHeight: row.implicitHeight Accessible.role: Accessible.Grouping Accessible.name: qsTr("Window controls") - Rectangle { - id: well - anchors.fill: parent - implicitWidth: row.implicitWidth + 2 - implicitHeight: Theme.controlHeight - radius: Theme.radiusSmall - color: Theme.iconWell - border.width: 1 - border.color: Theme.border + Row { + id: row + anchors.centerIn: parent + spacing: Theme.trafficLightGap - Row { - id: row - anchors.centerIn: parent - spacing: 0 + TrafficLightButton { + id: closeButton + fill: Theme.trafficClose + glyph: "close" + label: qsTr("Close") + showGlyph: root.hovered + onClicked: WindowService.closeFocusedWindow() + } - NebulaIconButton { - id: minimizeButton - width: 26 - height: 26 - label: qsTr("Minimise") - onClicked: WindowService.minimizeFocusedWindow() + TrafficLightButton { + id: minimizeButton + fill: Theme.trafficMinimise + glyph: "minimize" + label: qsTr("Minimise") + showGlyph: root.hovered + onClicked: WindowService.minimizeFocusedWindow() + } - NebulaIcon { - name: "minimize" - size: 14 - color: minimizeButton.iconColor - } - } - - NebulaIconButton { - id: maximizeButton - width: 26 - height: 26 - label: root.maximized ? qsTr("Restore") : qsTr("Maximise") - onClicked: WindowService.toggleMaximizeFocusedWindow() - - NebulaIcon { - name: root.maximized ? "restore" : "maximize" - size: 14 - color: maximizeButton.iconColor - } - } - - NebulaIconButton { - id: closeButton - width: 26 - height: 26 - label: qsTr("Close") - danger: true - onClicked: WindowService.closeFocusedWindow() - - NebulaIcon { - name: "close" - size: 14 - color: closeButton.iconColor - } - } + TrafficLightButton { + id: zoomButton + fill: Theme.trafficZoom + glyph: root.maximized ? "restore" : "zoom" + label: root.maximized ? qsTr("Restore") : qsTr("Maximise") + showGlyph: root.hovered + onClicked: WindowService.toggleMaximizeFocusedWindow() } } } diff --git a/shells/desktop/shell/qml/surfaces/WindowChromeSurface.qml b/shells/desktop/shell/qml/surfaces/WindowChromeSurface.qml new file mode 100644 index 0000000..0814946 --- /dev/null +++ b/shells/desktop/shell/qml/surfaces/WindowChromeSurface.qml @@ -0,0 +1,43 @@ +import QtQuick +import QtQuick.Window +import org.kde.layershell 1.0 as LayerShell +import Nebula.UI +import Nebula.Windows + +Window { + id: root + + readonly property bool active: WindowService.hasFocusedWindow + && !WindowService.focusedWindowFullscreen + && !ShellState.launcherOpen + readonly property int chromeX: WindowService.focusedWindowX + + WindowService.focusedWindowDecoX + + Theme.windowChromeInset + readonly property int chromeY: { + const top = WindowService.focusedWindowY + WindowService.focusedWindowDecoY + const decoHeight = WindowService.focusedWindowDecoHeight + if (decoHeight <= 0) + return top + 6 + return top + Math.max(0, Math.round((decoHeight - height) / 2)) + } + + title: qsTr("Nebula Window Chrome") + color: "transparent" + flags: Qt.FramelessWindowHint | Qt.WindowDoesNotAcceptFocus + visible: root.active + width: Math.max(1, controls.implicitWidth) + height: Math.max(1, controls.implicitHeight) + + LayerShell.Window.scope: "nebula-window-chrome" + LayerShell.Window.layer: LayerShell.Window.LayerTop + LayerShell.Window.anchors: LayerShell.Window.AnchorLeft | LayerShell.Window.AnchorTop + LayerShell.Window.margins.left: Math.max(0, chromeX) + LayerShell.Window.margins.top: Math.max(0, chromeY) + LayerShell.Window.exclusionZone: 0 + LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone + + WindowControls { + id: controls + anchors.centerIn: parent + } +}