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:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user