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
+1
View File
@@ -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 {
+3 -21
View File
@@ -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
}
}
}
@@ -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
}
}
@@ -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()
}
}
}
@@ -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
}
}