Add focused window controls to top bar

Introduces a new `WindowControls` component in the desktop top bar with minimize, maximize/restore, and close actions for the focused window. `WindowService` now exposes focused-window presence/maximized state plus focused-window convenience actions to support this UI.

Also extends Nebula UI with a reusable `NebulaToolTip`, keyboard/pressed-state support in `NebulaIconButton`, new window-control glyphs in `NebulaIcon`, and theme tokens for pressed/danger hover states, with CMake and README updates to register/document the new components.
This commit is contained in:
2026-08-26 22:26:29 +12:00
parent e96a22eb9e
commit 08e67845a5
11 changed files with 329 additions and 4 deletions
+1
View File
@@ -16,6 +16,7 @@ set(NEBULA_UI_QML_FILES
NebulaCard.qml
NebulaIconButton.qml
NebulaTextField.qml
NebulaToolTip.qml
FocusFrame.qml
NebulaIcon.qml
)
+1 -1
View File
@@ -11,7 +11,7 @@ import Nebula.UI
This module contains reusable visual language only:
- colour, spacing, type, and motion tokens (`Theme`)
- generic controls (`NebulaButton`, `NebulaCard`, `NebulaIconButton`, `NebulaTextField`)
- generic controls (`NebulaButton`, `NebulaCard`, `NebulaIconButton`, `NebulaTextField`, `NebulaToolTip`)
- focus treatment (`FocusFrame`)
- compact vector icons (`NebulaIcon`)
@@ -82,6 +82,18 @@ Item {
case "application":
paintApplication(ctx)
break
case "minimize":
paintMinimize(ctx)
break
case "maximize":
paintMaximize(ctx)
break
case "restore":
paintRestore(ctx)
break
case "close":
paintClose(ctx)
break
default:
paintApplication(ctx)
break
@@ -192,6 +204,41 @@ Item {
ctx.stroke()
}
function paintMinimize(ctx) {
ctx.lineWidth = 1.7
ctx.beginPath()
ctx.moveTo(6.4, 16.2)
ctx.lineTo(17.6, 16.2)
ctx.stroke()
}
function paintMaximize(ctx) {
ctx.lineWidth = 1.5
ctx.beginPath()
roundedRect(ctx, 6.2, 6.2, 11.6, 11.6, 2)
ctx.stroke()
}
function paintRestore(ctx) {
ctx.lineWidth = 1.45
ctx.beginPath()
roundedRect(ctx, 8.6, 5.6, 9.4, 9.4, 1.7)
ctx.stroke()
ctx.beginPath()
roundedRect(ctx, 5.6, 8.6, 9.4, 9.4, 1.7)
ctx.stroke()
}
function paintClose(ctx) {
ctx.lineWidth = 1.7
ctx.beginPath()
ctx.moveTo(7.3, 7.3)
ctx.lineTo(16.7, 16.7)
ctx.moveTo(16.7, 7.3)
ctx.lineTo(7.3, 16.7)
ctx.stroke()
}
function paintApplication(ctx) {
ctx.lineWidth = 1.5
ctx.beginPath()
@@ -7,6 +7,7 @@ Item {
property bool active: false
property bool danger: false
readonly property bool hovered: hover.containsMouse
readonly property bool pressed: hover.pressed
readonly property color iconColor: {
if (danger && hovered)
return Theme.danger
@@ -23,16 +24,32 @@ Item {
implicitWidth: Theme.controlHeight
implicitHeight: Theme.controlHeight
activeFocusOnTab: true
Accessible.role: Accessible.Button
Accessible.name: root.label
Accessible.onPressAction: root.clicked()
Keys.onReturnPressed: {
event.accepted = true
root.clicked()
}
Keys.onSpacePressed: {
event.accepted = true
root.clicked()
}
Rectangle {
anchors.fill: parent
radius: Theme.radiusSmall
color: {
if (root.danger && hover.pressed)
return Theme.dangerSoftStrong
if (root.danger && hover.containsMouse)
return Theme.dangerSoft
if (root.active)
return Theme.accentSoft
if (hover.pressed)
return Theme.pressedFill
if (hover.containsMouse)
return Theme.hoverFill
return "transparent"
@@ -63,4 +80,9 @@ Item {
FocusFrame {
radius: Theme.radiusSmall
}
NebulaToolTip {
text: root.label
active: hover.containsMouse && !hover.pressed && root.label.length > 0
}
}
@@ -0,0 +1,98 @@
import QtQuick
import QtQuick.Window
Item {
id: root
property string text: ""
property bool active: false
property int delay: 550
property int timeout: 4000
width: 0
height: 0
enabled: false
Accessible.ignored: true
Timer {
id: showTimer
interval: root.delay
repeat: false
onTriggered: root.open()
}
Timer {
id: hideTimer
interval: root.timeout
repeat: false
onTriggered: tip.visible = false
}
Window {
id: tip
flags: Qt.Popup | Qt.ToolTip | Qt.FramelessWindowHint | Qt.WindowDoesNotAcceptFocus
color: "transparent"
visible: false
width: Math.ceil(tipText.implicitWidth) + 16
height: Math.ceil(tipText.implicitHeight) + 10
Rectangle {
anchors.fill: parent
radius: Theme.radiusSmall
color: Theme.surfaceRaised
border.color: Theme.borderStrong
border.width: 1
Text {
id: tipText
anchors.centerIn: parent
text: root.text
color: Theme.textPrimary
font.family: Theme.fontFamily
font.pixelSize: Theme.fontSizeSmall
}
}
}
onActiveChanged: root.sync()
onTextChanged: {
if (tip.visible)
root.place()
else
root.sync()
}
function sync() {
if (root.active && root.text.length > 0) {
showTimer.restart()
return
}
showTimer.stop()
hideTimer.stop()
tip.visible = false
}
function open() {
if (!root.active || root.text.length === 0)
return
const host = root.parent
if (!host)
return
const win = host.Window.window
if (win)
tip.transientParent = win
root.place()
tip.visible = true
hideTimer.restart()
}
function place() {
const host = root.parent
if (!host)
return
const pos = host.mapToGlobal(host.width / 2, host.height + 6)
const screenWidth = Screen.width
tip.x = Math.round(Math.max(8, Math.min(pos.x - tip.width / 2, screenWidth - tip.width - 8)))
tip.y = Math.round(pos.y)
}
}
@@ -26,6 +26,9 @@ QtObject {
readonly property color focus: Qt.rgba(126 / 255, 160 / 255, 196 / 255, 0.55)
readonly property color hoverFill: Qt.rgba(1, 1, 1, 0.06)
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 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)