Add Qt/QML desktop shell and Nebula.UI
Introduce a new production desktop shell at `shells/desktop/shell` built with C++20, Qt Quick/QML, and LayerShellQt, including top bar, launcher, desktop surfaces, shell state, and mock app data. Add the shared `packages/nebula-ui` QML module (theme, controls, focus, icons) and wire it into the shell build. Retire the old GTK/WebKit native host by removing `shells/desktop/native`, move the React shell UI to `shells/desktop/prototype-react` as an archived design reference, and update root/compositor/session documentation to reflect the new Linux-only production architecture and temporary Sway role.
This commit is contained in:
@@ -7,14 +7,40 @@ A Linux desktop environment built around a custom compositor, shell, and service
|
|||||||
| Path | Purpose |
|
| Path | Purpose |
|
||||||
|------|---------|
|
|------|---------|
|
||||||
| `core/nebula-core` | Core runtime and platform APIs |
|
| `core/nebula-core` | Core runtime and platform APIs |
|
||||||
| `compositor/` | Compositor configuration (Sway) |
|
| `compositor/` | Compositor configuration (Sway is temporary) |
|
||||||
| `shells/` | Desktop and bigscreen shell implementations |
|
| `shells/` | Desktop and future Bigscreen shells |
|
||||||
| `packages/` | Shared UI libraries, icons, API, and design tokens |
|
| `packages/` | Shared UI (`Nebula.UI`), icons, API, and design tokens |
|
||||||
| `services/` | System services (audio, network, bluetooth, power, accounts) |
|
| `services/` | System services (audio, network, bluetooth, power, accounts) |
|
||||||
| `session/` | Session manager and desktop entry |
|
| `session/` | Session manager and desktop entry |
|
||||||
| `packaging/` | Distribution packaging (Ubuntu) |
|
| `packaging/` | Distribution packaging (Ubuntu) |
|
||||||
| `branding/` | Boot assets, wallpapers, and sounds |
|
| `branding/` | Boot assets, wallpapers, and sounds |
|
||||||
|
|
||||||
|
## Shells
|
||||||
|
|
||||||
|
**Nebula Desktop** (production) is a C++20 Qt Quick/QML Wayland shell using LayerShellQt. See `shells/desktop/`.
|
||||||
|
|
||||||
|
**Nebula Bigscreen** is planned as a separate Qt Quick/QML shell with a controller-first interaction model, sharing `Nebula.UI` and Nebula Core.
|
||||||
|
|
||||||
|
Standalone Nebula applications may still use React/Tauri where that is a better fit. The operating-system shell does not.
|
||||||
|
|
||||||
|
An archived React desktop prototype lives in `shells/desktop/prototype-react/` as a design reference only.
|
||||||
|
|
||||||
|
## Current stack
|
||||||
|
|
||||||
|
```text
|
||||||
|
Ubuntu
|
||||||
|
↓
|
||||||
|
Wayland
|
||||||
|
↓
|
||||||
|
Sway temporary compositor
|
||||||
|
↓
|
||||||
|
Nebula Shell Qt Quick/QML
|
||||||
|
```
|
||||||
|
|
||||||
|
Applications and games render on their own Wayland/XWayland paths. They are not composited inside QML.
|
||||||
|
|
||||||
## Getting started
|
## Getting started
|
||||||
|
|
||||||
Documentation and build instructions will be added as components land.
|
Desktop shell build instructions: `shells/desktop/README.md`.
|
||||||
|
|
||||||
|
The Qt shell must be compiled on Linux (the Ubuntu NebulaOS VM). It will not build on Windows.
|
||||||
|
|||||||
+25
-1
@@ -1,3 +1,27 @@
|
|||||||
# Compositor
|
# Compositor
|
||||||
|
|
||||||
NebulaOS uses Sway as its Wayland compositor. Configuration lives in `sway/nebula-sway.conf`.
|
Sway is the **temporary** Wayland compositor while Nebula Desktop is developed. Configuration lives in `sway/nebula-sway.conf`.
|
||||||
|
|
||||||
|
Sway currently provides:
|
||||||
|
|
||||||
|
- Wayland compositor
|
||||||
|
- application window management
|
||||||
|
- XWayland
|
||||||
|
- input
|
||||||
|
- output handling
|
||||||
|
|
||||||
|
Nebula provides the visible shell (Qt Quick/QML via LayerShellQt). The Sway config must not enable a Sway bar.
|
||||||
|
|
||||||
|
A custom Nebula compositor is a later milestone. Do not treat this Sway setup as the final architecture.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Ubuntu/Linux
|
||||||
|
↓
|
||||||
|
Wayland
|
||||||
|
↓
|
||||||
|
Sway now
|
||||||
|
Nebula compositor later
|
||||||
|
↓
|
||||||
|
Nebula Shell
|
||||||
|
Qt Quick/QML
|
||||||
|
```
|
||||||
|
|||||||
@@ -0,0 +1,41 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
if(NOT TARGET Qt6::Quick)
|
||||||
|
project(NebulaUI LANGUAGES CXX)
|
||||||
|
find_package(Qt6 6.4 REQUIRED COMPONENTS Quick)
|
||||||
|
qt_standard_project_setup(REQUIRES 6.4)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
qt_add_library(NebulaUI STATIC)
|
||||||
|
|
||||||
|
set(NEBULA_UI_QML_DIR "${CMAKE_CURRENT_SOURCE_DIR}/qml/Nebula/UI")
|
||||||
|
|
||||||
|
set(NEBULA_UI_QML_FILES
|
||||||
|
Theme.qml
|
||||||
|
NebulaButton.qml
|
||||||
|
NebulaCard.qml
|
||||||
|
NebulaIconButton.qml
|
||||||
|
NebulaTextField.qml
|
||||||
|
FocusFrame.qml
|
||||||
|
NebulaIcon.qml
|
||||||
|
)
|
||||||
|
|
||||||
|
set(NEBULA_UI_QML_PATHS)
|
||||||
|
foreach(qml_file IN LISTS NEBULA_UI_QML_FILES)
|
||||||
|
set(qml_path "${NEBULA_UI_QML_DIR}/${qml_file}")
|
||||||
|
set_source_files_properties("${qml_path}" PROPERTIES
|
||||||
|
QT_RESOURCE_ALIAS "${qml_file}"
|
||||||
|
)
|
||||||
|
list(APPEND NEBULA_UI_QML_PATHS "${qml_path}")
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
set_source_files_properties("${NEBULA_UI_QML_DIR}/Theme.qml" PROPERTIES
|
||||||
|
QT_QML_SINGLETON_TYPE TRUE
|
||||||
|
)
|
||||||
|
|
||||||
|
qt_add_qml_module(NebulaUI
|
||||||
|
URI Nebula.UI
|
||||||
|
VERSION 1.0
|
||||||
|
RESOURCE_PREFIX /qt/qml
|
||||||
|
QML_FILES ${NEBULA_UI_QML_PATHS}
|
||||||
|
)
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
# Nebula.UI
|
||||||
|
|
||||||
|
Shared Qt Quick/QML design system for NebulaOS shells.
|
||||||
|
|
||||||
|
Desktop and the future Bigscreen shell should both import:
|
||||||
|
|
||||||
|
```qml
|
||||||
|
import Nebula.UI
|
||||||
|
```
|
||||||
|
|
||||||
|
This module contains reusable visual language only:
|
||||||
|
|
||||||
|
- colour, spacing, type, and motion tokens (`Theme`)
|
||||||
|
- generic controls (`NebulaButton`, `NebulaCard`, `NebulaIconButton`, `NebulaTextField`)
|
||||||
|
- focus treatment (`FocusFrame`)
|
||||||
|
- compact vector icons (`NebulaIcon`)
|
||||||
|
|
||||||
|
Do not put Desktop- or Bigscreen-specific chrome here (top bar, launcher, session flow).
|
||||||
|
|
||||||
|
Built as part of `shells/desktop/shell`. It is not a standalone application.
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property Item target: parent
|
||||||
|
property real radius: Theme.radiusSmall
|
||||||
|
property bool active: target && target.activeFocus
|
||||||
|
|
||||||
|
anchors.fill: target
|
||||||
|
anchors.margins: -2
|
||||||
|
z: 20
|
||||||
|
enabled: false
|
||||||
|
visible: active
|
||||||
|
Accessible.ignored: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: "transparent"
|
||||||
|
border.width: 2
|
||||||
|
border.color: Theme.focus
|
||||||
|
radius: root.radius + 2
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,68 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string text: ""
|
||||||
|
property bool checked: false
|
||||||
|
property color textColor: checked ? Theme.accentStrong : Theme.textPrimary
|
||||||
|
|
||||||
|
default property alias icon: iconHolder.data
|
||||||
|
|
||||||
|
signal clicked()
|
||||||
|
|
||||||
|
implicitWidth: row.implicitWidth + 18
|
||||||
|
implicitHeight: Theme.controlHeight
|
||||||
|
Accessible.role: Accessible.Button
|
||||||
|
Accessible.name: root.text
|
||||||
|
Accessible.checkable: true
|
||||||
|
Accessible.checked: root.checked
|
||||||
|
Accessible.onPressAction: root.clicked()
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.radiusSmall
|
||||||
|
color: (hover.containsMouse || root.checked) ? Theme.accentSoft : "transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.animationFast
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: row
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: iconHolder
|
||||||
|
width: children.length > 0 ? 18 : 0
|
||||||
|
height: 18
|
||||||
|
visible: width > 0
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.text
|
||||||
|
color: root.textColor
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
visible: root.text.length > 0
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: hover
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.clicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusFrame {
|
||||||
|
radius: Theme.radiusSmall
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,21 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
color: Theme.surface
|
||||||
|
border.color: Theme.border
|
||||||
|
border.width: 1
|
||||||
|
radius: Theme.radiusLarge
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
id: shadow
|
||||||
|
z: -1
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: 10
|
||||||
|
anchors.bottomMargin: -10
|
||||||
|
radius: root.radius
|
||||||
|
color: Theme.shadow
|
||||||
|
opacity: 0.85
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,369 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string name: "nebula"
|
||||||
|
property color color: Theme.textPrimary
|
||||||
|
property int size: 16
|
||||||
|
property real value: 1
|
||||||
|
|
||||||
|
implicitWidth: size
|
||||||
|
implicitHeight: size
|
||||||
|
Accessible.ignored: true
|
||||||
|
|
||||||
|
Canvas {
|
||||||
|
id: canvas
|
||||||
|
anchors.fill: parent
|
||||||
|
antialiasing: true
|
||||||
|
onPaint: root.paint(getContext("2d"))
|
||||||
|
}
|
||||||
|
|
||||||
|
onNameChanged: canvas.requestPaint()
|
||||||
|
onColorChanged: canvas.requestPaint()
|
||||||
|
onValueChanged: canvas.requestPaint()
|
||||||
|
onWidthChanged: canvas.requestPaint()
|
||||||
|
onHeightChanged: canvas.requestPaint()
|
||||||
|
|
||||||
|
function paint(ctx) {
|
||||||
|
ctx.reset()
|
||||||
|
ctx.strokeStyle = root.color
|
||||||
|
ctx.fillStyle = root.color
|
||||||
|
ctx.lineCap = "round"
|
||||||
|
ctx.lineJoin = "round"
|
||||||
|
ctx.scale(width / 24, height / 24)
|
||||||
|
|
||||||
|
switch (root.name) {
|
||||||
|
case "nebula":
|
||||||
|
paintNebula(ctx)
|
||||||
|
break
|
||||||
|
case "search":
|
||||||
|
paintSearch(ctx)
|
||||||
|
break
|
||||||
|
case "wifi":
|
||||||
|
paintWifi(ctx)
|
||||||
|
break
|
||||||
|
case "speaker":
|
||||||
|
paintSpeaker(ctx)
|
||||||
|
break
|
||||||
|
case "battery":
|
||||||
|
paintBattery(ctx)
|
||||||
|
break
|
||||||
|
case "power":
|
||||||
|
paintPower(ctx)
|
||||||
|
break
|
||||||
|
case "restart":
|
||||||
|
paintRestart(ctx)
|
||||||
|
break
|
||||||
|
case "folder":
|
||||||
|
paintFolder(ctx)
|
||||||
|
break
|
||||||
|
case "globe":
|
||||||
|
paintGlobe(ctx)
|
||||||
|
break
|
||||||
|
case "terminal":
|
||||||
|
paintTerminal(ctx)
|
||||||
|
break
|
||||||
|
case "settings":
|
||||||
|
paintSettings(ctx)
|
||||||
|
break
|
||||||
|
case "game":
|
||||||
|
paintGame(ctx)
|
||||||
|
break
|
||||||
|
case "photo":
|
||||||
|
paintPhoto(ctx)
|
||||||
|
break
|
||||||
|
case "music":
|
||||||
|
paintMusic(ctx)
|
||||||
|
break
|
||||||
|
case "document":
|
||||||
|
paintDocument(ctx)
|
||||||
|
break
|
||||||
|
default:
|
||||||
|
paintFolder(ctx)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintNebula(ctx) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(12, 12, 4.2, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(12, 12)
|
||||||
|
ctx.rotate(-26 * Math.PI / 180)
|
||||||
|
ctx.scale(10 / 4.15, 1)
|
||||||
|
ctx.lineWidth = 1.35 * (4.15 / 10)
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(0, 0, 4.15, 0, Math.PI * 2)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.restore()
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintSearch(ctx) {
|
||||||
|
ctx.lineWidth = 1.6
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(11, 11, 6.25, 0, Math.PI * 2)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(15.8, 15.8)
|
||||||
|
ctx.lineTo(20, 20)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintWifi(ctx) {
|
||||||
|
const strength = root.value
|
||||||
|
ctx.lineWidth = 1.6
|
||||||
|
ctx.globalAlpha = strength > 0 ? 1 : 0.28
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(4.8, 15.2)
|
||||||
|
ctx.bezierCurveTo(8.8, 11.2, 15.2, 11.2, 19.2, 15.2)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.globalAlpha = strength > 1 ? 1 : 0.28
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(7.4, 17.4)
|
||||||
|
ctx.bezierCurveTo(10, 14.9, 14, 14.9, 16.6, 17.4)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(12, 20, 1.15, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintSpeaker(ctx) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(4.5, 9.4)
|
||||||
|
ctx.lineTo(7.6, 9.4)
|
||||||
|
ctx.lineTo(12, 6.2)
|
||||||
|
ctx.lineTo(12, 17.8)
|
||||||
|
ctx.lineTo(7.6, 14.6)
|
||||||
|
ctx.lineTo(4.5, 14.6)
|
||||||
|
ctx.closePath()
|
||||||
|
ctx.fill()
|
||||||
|
ctx.lineWidth = 1.5
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(12.5, 12, 4.2, -0.7, 0.7)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.globalAlpha = 0.7
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(12.5, 12, 7.2, -0.85, 0.85)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.globalAlpha = 1
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintBattery(ctx) {
|
||||||
|
ctx.lineWidth = 1.5
|
||||||
|
ctx.beginPath()
|
||||||
|
roundedRect(ctx, 2.5, 7.25, 16.5, 9.5, 2)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.beginPath()
|
||||||
|
roundedRect(ctx, 19.5, 10, 2, 4, 0.6)
|
||||||
|
ctx.fill()
|
||||||
|
const fillWidth = Math.max(1.5, (14 * Math.min(100, Math.max(0, root.value))) / 100)
|
||||||
|
ctx.beginPath()
|
||||||
|
roundedRect(ctx, 4.2, 9, fillWidth, 6, 1)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintPower(ctx) {
|
||||||
|
ctx.lineWidth = 1.7
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(12, 4.5)
|
||||||
|
ctx.lineTo(12, 11.7)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(12, 13, 6.2, Math.PI * 1.15, Math.PI * -0.15, false)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintRestart(ctx) {
|
||||||
|
ctx.lineWidth = 1.7
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(12, 12, 7, -0.2, Math.PI * 1.55, true)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(19, 5.2)
|
||||||
|
ctx.lineTo(19, 9.6)
|
||||||
|
ctx.lineTo(14.6, 9.6)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintFolder(ctx) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(3.5, 8.2)
|
||||||
|
ctx.quadraticCurveTo(3.5, 6.4, 5.3, 6.4)
|
||||||
|
ctx.lineTo(9.4, 6.4)
|
||||||
|
ctx.lineTo(11, 8.2)
|
||||||
|
ctx.lineTo(18.7, 8.2)
|
||||||
|
ctx.quadraticCurveTo(20.5, 8.2, 20.5, 10)
|
||||||
|
ctx.lineTo(20.5, 16.8)
|
||||||
|
ctx.quadraticCurveTo(20.5, 18.6, 18.7, 18.6)
|
||||||
|
ctx.lineTo(5.3, 18.6)
|
||||||
|
ctx.quadraticCurveTo(3.5, 18.6, 3.5, 16.8)
|
||||||
|
ctx.closePath()
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintGlobe(ctx) {
|
||||||
|
ctx.lineWidth = 1.6
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(12, 12, 7.25, 0, Math.PI * 2)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.lineWidth = 1.4
|
||||||
|
ctx.save()
|
||||||
|
ctx.translate(12, 12)
|
||||||
|
ctx.scale(3.1 / 7.25, 1)
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(0, 0, 7.25, 0, Math.PI * 2)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.restore()
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(5.2, 12)
|
||||||
|
ctx.lineTo(18.8, 12)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintTerminal(ctx) {
|
||||||
|
ctx.lineWidth = 1.7
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(7, 9.2)
|
||||||
|
ctx.lineTo(10.4, 12)
|
||||||
|
ctx.lineTo(7, 14.8)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(12.2, 15.2)
|
||||||
|
ctx.lineTo(17, 15.2)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintSettings(ctx) {
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(12, 12, 2.4, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.lineWidth = 1.6
|
||||||
|
const ticks = [
|
||||||
|
[12, 5.2, 12, 6.8],
|
||||||
|
[12, 17.2, 12, 18.8],
|
||||||
|
[5.2, 12, 6.8, 12],
|
||||||
|
[17.2, 12, 18.8, 12],
|
||||||
|
[7.2, 7.2, 8.3, 8.3],
|
||||||
|
[15.7, 15.7, 16.8, 16.8],
|
||||||
|
[16.8, 7.2, 15.7, 8.3],
|
||||||
|
[8.3, 15.7, 7.2, 16.8]
|
||||||
|
]
|
||||||
|
for (let i = 0; i < ticks.length; i++) {
|
||||||
|
const t = ticks[i]
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(t[0], t[1])
|
||||||
|
ctx.lineTo(t[2], t[3])
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintGame(ctx) {
|
||||||
|
ctx.lineWidth = 1.5
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(5.2, 10.4)
|
||||||
|
ctx.lineTo(18.8, 10.4)
|
||||||
|
ctx.quadraticCurveTo(20.4, 10.4, 20.3, 12.1)
|
||||||
|
ctx.lineTo(19.8, 15.3)
|
||||||
|
ctx.quadraticCurveTo(19.6, 16.9, 18.5, 16.9)
|
||||||
|
ctx.lineTo(16.3, 16.9)
|
||||||
|
ctx.lineTo(15, 15.1)
|
||||||
|
ctx.lineTo(10, 15.1)
|
||||||
|
ctx.lineTo(8.7, 16.9)
|
||||||
|
ctx.lineTo(6.5, 16.9)
|
||||||
|
ctx.quadraticCurveTo(5.4, 16.9, 5.2, 15.3)
|
||||||
|
ctx.lineTo(4.7, 12.1)
|
||||||
|
ctx.quadraticCurveTo(4.6, 10.4, 5.2, 10.4)
|
||||||
|
ctx.closePath()
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(8, 12.4)
|
||||||
|
ctx.lineTo(8, 15.6)
|
||||||
|
ctx.moveTo(6.4, 14)
|
||||||
|
ctx.lineTo(9.6, 14)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(15.4, 13.2, 0.9, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(17.2, 15, 0.9, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintPhoto(ctx) {
|
||||||
|
ctx.lineWidth = 1.5
|
||||||
|
ctx.beginPath()
|
||||||
|
roundedRect(ctx, 4, 6.2, 16, 11.6, 2)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(9, 10.2, 1.3, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(7.2, 15.8)
|
||||||
|
ctx.lineTo(10.6, 12.4)
|
||||||
|
ctx.lineTo(13.2, 15)
|
||||||
|
ctx.lineTo(15.4, 12.9)
|
||||||
|
ctx.lineTo(18.8, 15.8)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintMusic(ctx) {
|
||||||
|
ctx.lineWidth = 1.6
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(10, 16.6)
|
||||||
|
ctx.lineTo(10, 7.4)
|
||||||
|
ctx.lineTo(18, 5.8)
|
||||||
|
ctx.lineTo(18, 15.2)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(8.2, 16.6, 2.2, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.arc(16.2, 15.2, 2.2, 0, Math.PI * 2)
|
||||||
|
ctx.fill()
|
||||||
|
}
|
||||||
|
|
||||||
|
function paintDocument(ctx) {
|
||||||
|
ctx.lineWidth = 1.5
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(7, 5.5)
|
||||||
|
ctx.lineTo(13.2, 5.5)
|
||||||
|
ctx.lineTo(17.5, 10)
|
||||||
|
ctx.lineTo(17.5, 18.5)
|
||||||
|
ctx.quadraticCurveTo(17.5, 20, 16, 20)
|
||||||
|
ctx.lineTo(7, 20)
|
||||||
|
ctx.quadraticCurveTo(5.5, 20, 5.5, 18.5)
|
||||||
|
ctx.lineTo(5.5, 7)
|
||||||
|
ctx.quadraticCurveTo(5.5, 5.5, 7, 5.5)
|
||||||
|
ctx.closePath()
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(13.2, 5.5)
|
||||||
|
ctx.lineTo(13.2, 10)
|
||||||
|
ctx.lineTo(17.5, 10)
|
||||||
|
ctx.stroke()
|
||||||
|
ctx.beginPath()
|
||||||
|
ctx.moveTo(8.8, 13.4)
|
||||||
|
ctx.lineTo(15.2, 13.4)
|
||||||
|
ctx.moveTo(8.8, 16.2)
|
||||||
|
ctx.lineTo(13.2, 16.2)
|
||||||
|
ctx.stroke()
|
||||||
|
}
|
||||||
|
|
||||||
|
function roundedRect(ctx, x, y, w, h, r) {
|
||||||
|
const radius = Math.min(r, w / 2, h / 2)
|
||||||
|
ctx.moveTo(x + radius, y)
|
||||||
|
ctx.lineTo(x + w - radius, y)
|
||||||
|
ctx.quadraticCurveTo(x + w, y, x + w, y + radius)
|
||||||
|
ctx.lineTo(x + w, y + h - radius)
|
||||||
|
ctx.quadraticCurveTo(x + w, y + h, x + w - radius, y + h)
|
||||||
|
ctx.lineTo(x + radius, y + h)
|
||||||
|
ctx.quadraticCurveTo(x, y + h, x, y + h - radius)
|
||||||
|
ctx.lineTo(x, y + radius)
|
||||||
|
ctx.quadraticCurveTo(x, y, x + radius, y)
|
||||||
|
ctx.closePath()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,66 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string label: ""
|
||||||
|
property bool active: false
|
||||||
|
property bool danger: false
|
||||||
|
readonly property bool hovered: hover.containsMouse
|
||||||
|
readonly property color iconColor: {
|
||||||
|
if (danger && hovered)
|
||||||
|
return Theme.danger
|
||||||
|
if (active)
|
||||||
|
return Theme.accentStrong
|
||||||
|
if (hovered)
|
||||||
|
return Theme.textPrimary
|
||||||
|
return Theme.textSecondary
|
||||||
|
}
|
||||||
|
|
||||||
|
default property alias icon: iconHolder.data
|
||||||
|
|
||||||
|
signal clicked()
|
||||||
|
|
||||||
|
implicitWidth: Theme.controlHeight
|
||||||
|
implicitHeight: Theme.controlHeight
|
||||||
|
Accessible.role: Accessible.Button
|
||||||
|
Accessible.name: root.label
|
||||||
|
Accessible.onPressAction: root.clicked()
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.radiusSmall
|
||||||
|
color: {
|
||||||
|
if (root.active)
|
||||||
|
return Theme.accentSoft
|
||||||
|
if (hover.containsMouse)
|
||||||
|
return Theme.hoverFill
|
||||||
|
return "transparent"
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.animationFast
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: iconHolder
|
||||||
|
anchors.centerIn: parent
|
||||||
|
width: 16
|
||||||
|
height: 16
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: hover
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.clicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusFrame {
|
||||||
|
radius: Theme.radiusSmall
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,82 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property alias text: input.text
|
||||||
|
property alias placeholderText: placeholder.text
|
||||||
|
property alias input: input
|
||||||
|
|
||||||
|
signal accepted()
|
||||||
|
|
||||||
|
implicitHeight: Theme.searchHeight
|
||||||
|
implicitWidth: 200
|
||||||
|
Accessible.role: Accessible.EditableText
|
||||||
|
Accessible.name: placeholder.text
|
||||||
|
Accessible.editable: true
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.radiusMedium
|
||||||
|
color: Theme.searchFill
|
||||||
|
border.width: 1
|
||||||
|
border.color: input.activeFocus ? Theme.borderStrong : Theme.border
|
||||||
|
|
||||||
|
Behavior on border.color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.animationFast
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: row
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.leftMargin: 12
|
||||||
|
anchors.rightMargin: 12
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
NebulaIcon {
|
||||||
|
id: searchIcon
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
name: "search"
|
||||||
|
size: 16
|
||||||
|
color: input.activeFocus ? Theme.textPrimary : Theme.textSecondary
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: parent.width - searchIcon.width - row.spacing
|
||||||
|
height: parent.height
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: placeholder
|
||||||
|
anchors.fill: parent
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
text: qsTr("Search")
|
||||||
|
color: Theme.textTertiary
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
visible: input.text.length === 0 && !input.preeditText
|
||||||
|
}
|
||||||
|
|
||||||
|
TextInput {
|
||||||
|
id: input
|
||||||
|
anchors.fill: parent
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
color: Theme.textPrimary
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
clip: true
|
||||||
|
selectByMouse: true
|
||||||
|
activeFocusOnTab: true
|
||||||
|
onAccepted: root.accepted()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusFrame {
|
||||||
|
target: root
|
||||||
|
active: input.activeFocus
|
||||||
|
radius: Theme.radiusMedium
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,55 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
// Colours from the Nebula Desktop React prototype (tokens.css).
|
||||||
|
readonly property color background: "#0b0f16"
|
||||||
|
readonly property color backgroundDeep: "#080b11"
|
||||||
|
|
||||||
|
readonly property color surface: Qt.rgba(20 / 255, 26 / 255, 36 / 255, 0.78)
|
||||||
|
readonly property color surfaceRaised: Qt.rgba(32 / 255, 40 / 255, 54 / 255, 0.9)
|
||||||
|
readonly property color topBarBackground: Qt.rgba(10 / 255, 13 / 255, 18 / 255, 0.55)
|
||||||
|
|
||||||
|
readonly property color border: Qt.rgba(232 / 255, 236 / 255, 242 / 255, 0.08)
|
||||||
|
readonly property color borderStrong: Qt.rgba(232 / 255, 236 / 255, 242 / 255, 0.14)
|
||||||
|
|
||||||
|
readonly property color textPrimary: "#e8ecf2"
|
||||||
|
readonly property color textSecondary: "#8e99ab"
|
||||||
|
readonly property color textTertiary: "#6b7587"
|
||||||
|
|
||||||
|
readonly property color accent: "#7ea0c4"
|
||||||
|
readonly property color accentStrong: "#9bb6d4"
|
||||||
|
readonly property color accentSoft: Qt.rgba(126 / 255, 160 / 255, 196 / 255, 0.16)
|
||||||
|
|
||||||
|
readonly property color danger: "#c98888"
|
||||||
|
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 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)
|
||||||
|
readonly property color searchFill: Qt.rgba(8 / 255, 11 / 255, 17 / 255, 0.45)
|
||||||
|
readonly property color shadow: Qt.rgba(0, 0, 0, 0.42)
|
||||||
|
|
||||||
|
readonly property int radiusSmall: 6
|
||||||
|
readonly property int radiusMedium: 10
|
||||||
|
readonly property int radiusLarge: 14
|
||||||
|
|
||||||
|
readonly property int spacingSmall: 8
|
||||||
|
readonly property int spacingMedium: 16
|
||||||
|
readonly property int spacingLarge: 24
|
||||||
|
|
||||||
|
readonly property int animationFast: 120
|
||||||
|
readonly property int animationNormal: 200
|
||||||
|
|
||||||
|
readonly property int topBarHeight: 40
|
||||||
|
readonly property int controlHeight: 28
|
||||||
|
readonly property int searchHeight: 36
|
||||||
|
|
||||||
|
readonly property int fontSize: 13
|
||||||
|
readonly property int fontSizeApp: 12
|
||||||
|
readonly property int fontSizeSmall: 11
|
||||||
|
|
||||||
|
readonly property string fontFamily: "Ubuntu"
|
||||||
|
}
|
||||||
@@ -7,4 +7,5 @@ export XDG_CURRENT_DESKTOP=NebulaOS
|
|||||||
export XDG_SESSION_DESKTOP=nebula
|
export XDG_SESSION_DESKTOP=nebula
|
||||||
|
|
||||||
# Shell and services are wired here as they land
|
# Shell and services are wired here as they land
|
||||||
# exec nebula-shell-desktop
|
# Production desktop chrome: shells/desktop/shell (nebula-shell)
|
||||||
|
# exec nebula-shell
|
||||||
|
|||||||
@@ -0,0 +1,178 @@
|
|||||||
|
# Nebula Desktop
|
||||||
|
|
||||||
|
Production shell for the mouse-and-keyboard NebulaOS desktop.
|
||||||
|
|
||||||
|
## Architecture
|
||||||
|
|
||||||
|
Nebula Desktop is a **Qt Quick / QML** Wayland shell. It draws system chrome only. It does not host application or game windows.
|
||||||
|
|
||||||
|
```text
|
||||||
|
Ubuntu
|
||||||
|
↓
|
||||||
|
Wayland
|
||||||
|
↓
|
||||||
|
Sway
|
||||||
|
temporary compositor
|
||||||
|
↓
|
||||||
|
Nebula Shell
|
||||||
|
C++20 + Qt 6 + Qt Quick/QML + LayerShellQt
|
||||||
|
```
|
||||||
|
|
||||||
|
Eventual target:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Ubuntu/Linux
|
||||||
|
↓
|
||||||
|
Wayland
|
||||||
|
↓
|
||||||
|
Nebula compositor
|
||||||
|
↓
|
||||||
|
Nebula Shell
|
||||||
|
Qt Quick/QML
|
||||||
|
```
|
||||||
|
|
||||||
|
Normal applications remain ordinary Wayland / XWayland clients of the compositor:
|
||||||
|
|
||||||
|
```text
|
||||||
|
Normal Application
|
||||||
|
↓
|
||||||
|
Wayland / XWayland
|
||||||
|
↓
|
||||||
|
Compositor
|
||||||
|
↓
|
||||||
|
GPU
|
||||||
|
```
|
||||||
|
|
||||||
|
Nebula chrome:
|
||||||
|
|
||||||
|
```text
|
||||||
|
QML
|
||||||
|
↓
|
||||||
|
Qt Quick
|
||||||
|
↓
|
||||||
|
Wayland Layer Shell
|
||||||
|
↓
|
||||||
|
Compositor
|
||||||
|
```
|
||||||
|
|
||||||
|
Games, browsers, terminals, and other apps never pass through Qt Quick.
|
||||||
|
|
||||||
|
## Production stack
|
||||||
|
|
||||||
|
| Piece | Technology |
|
||||||
|
|-------|------------|
|
||||||
|
| Language | C++20 |
|
||||||
|
| UI | Qt 6, Qt Quick, QML |
|
||||||
|
| Layer surfaces | LayerShellQt |
|
||||||
|
| Build | CMake |
|
||||||
|
| Shared design system | `packages/nebula-ui` (`import Nebula.UI`) |
|
||||||
|
|
||||||
|
The executable is `nebula-shell`.
|
||||||
|
|
||||||
|
QML module URI: `Nebula.Shell`.
|
||||||
|
|
||||||
|
## Surfaces
|
||||||
|
|
||||||
|
Independent Wayland layer-shell windows:
|
||||||
|
|
||||||
|
| Surface | Layer | Role |
|
||||||
|
|---------|-------|------|
|
||||||
|
| Desktop | `BACKGROUND` | Wallpaper / desktop, no exclusive zone, no keyboard focus |
|
||||||
|
| Top Bar | `TOP` | 40px bar, exclusive zone, no keyboard focus by default |
|
||||||
|
| Launcher | `OVERLAY` | Hidden until the Nebula button opens it |
|
||||||
|
|
||||||
|
Stacking:
|
||||||
|
|
||||||
|
```text
|
||||||
|
OVERLAY
|
||||||
|
Nebula Launcher
|
||||||
|
|
||||||
|
TOP
|
||||||
|
Nebula Top Bar
|
||||||
|
|
||||||
|
NORMAL WAYLAND WINDOWS
|
||||||
|
Firefox, Steam, Blender, games, terminals, …
|
||||||
|
|
||||||
|
BACKGROUND
|
||||||
|
Nebula Desktop
|
||||||
|
```
|
||||||
|
|
||||||
|
## Layout
|
||||||
|
|
||||||
|
```text
|
||||||
|
shells/desktop/
|
||||||
|
├── README.md this file
|
||||||
|
├── shell/ production Qt/QML shell
|
||||||
|
│ ├── CMakeLists.txt
|
||||||
|
│ ├── src/main.cpp
|
||||||
|
│ └── qml/
|
||||||
|
└── prototype-react/ archived design reference only
|
||||||
|
```
|
||||||
|
|
||||||
|
Shared visual language lives in `packages/nebula-ui`, not in Desktop-specific chrome.
|
||||||
|
|
||||||
|
## Linux build
|
||||||
|
|
||||||
|
This project will not compile on Windows. LayerShellQt is a Wayland API and is not stubbed.
|
||||||
|
|
||||||
|
On the Ubuntu NebulaOS VM:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
sudo apt install \
|
||||||
|
qt6-base-dev \
|
||||||
|
qt6-declarative-dev \
|
||||||
|
qt6-wayland \
|
||||||
|
liblayershellqtinterface-dev \
|
||||||
|
qml6-module-org-kde-layershell \
|
||||||
|
qml6-module-qtquick \
|
||||||
|
qml6-module-qtquick-window \
|
||||||
|
cmake \
|
||||||
|
build-essential
|
||||||
|
```
|
||||||
|
|
||||||
|
Qt 6.4 or newer is required. `loadFromModule` is used on Qt 6.5+.
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd shells/desktop/shell
|
||||||
|
|
||||||
|
cmake -S . -B build
|
||||||
|
cmake --build build -j$(nproc)
|
||||||
|
./build/nebula-shell
|
||||||
|
```
|
||||||
|
|
||||||
|
Run inside a Wayland compositor such as Sway (`compositor/sway/nebula-sway.conf`). Sway must not show its own bar.
|
||||||
|
|
||||||
|
There is no npm server, Vite server, `NEBULA_SHELL_DEV_URL`, WebKitGTK, GTK4, or React involved in running the production shell.
|
||||||
|
|
||||||
|
### QML lint
|
||||||
|
|
||||||
|
If the installed Qt provides `qmllint`:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
qmllint \
|
||||||
|
shells/desktop/shell/qml/*.qml \
|
||||||
|
shells/desktop/shell/qml/surfaces/*.qml \
|
||||||
|
shells/desktop/shell/qml/components/*.qml \
|
||||||
|
shells/desktop/shell/qml/mock/*.qml \
|
||||||
|
packages/nebula-ui/qml/Nebula/UI/*.qml
|
||||||
|
```
|
||||||
|
|
||||||
|
Do not use ESLint or Oxlint on the production Qt shell.
|
||||||
|
|
||||||
|
## Design reference
|
||||||
|
|
||||||
|
`prototype-react/` is an archived React/Vite UI. It is **not** the production shell. Keep it until the Qt shell reaches visual and behavioural parity, then remove it in a later cleanup.
|
||||||
|
|
||||||
|
## Out of scope
|
||||||
|
|
||||||
|
Not implemented in this milestone:
|
||||||
|
|
||||||
|
- PipeWire, NetworkManager, BlueZ, UPower
|
||||||
|
- D-Bus services
|
||||||
|
- real `.desktop` discovery or app launching
|
||||||
|
- Sway IPC, workspaces, notifications
|
||||||
|
- authentication, greetd, Gamescope
|
||||||
|
- a custom Nebula compositor
|
||||||
|
- Nebula Bigscreen
|
||||||
|
|
||||||
|
Those come later. Bigscreen is expected to reuse `Nebula.UI` and shared C++ services with a different interaction model.
|
||||||
@@ -1,32 +0,0 @@
|
|||||||
cmake_minimum_required(VERSION 3.20)
|
|
||||||
|
|
||||||
# Linux-only. GTK4 Layer Shell and WebKitGTK are not built or stubbed on Windows.
|
|
||||||
project(nebula-shell LANGUAGES CXX)
|
|
||||||
|
|
||||||
set(CMAKE_CXX_STANDARD 20)
|
|
||||||
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
|
||||||
set(CMAKE_CXX_EXTENSIONS OFF)
|
|
||||||
|
|
||||||
find_package(PkgConfig REQUIRED)
|
|
||||||
pkg_check_modules(GTK4 REQUIRED IMPORTED_TARGET gtk4)
|
|
||||||
pkg_check_modules(GTK4_LAYER_SHELL REQUIRED IMPORTED_TARGET gtk4-layer-shell-0)
|
|
||||||
pkg_check_modules(WEBKITGTK REQUIRED IMPORTED_TARGET webkitgtk-6.0)
|
|
||||||
|
|
||||||
add_executable(nebula-shell
|
|
||||||
src/main.cpp
|
|
||||||
src/ShellApplication.cpp
|
|
||||||
src/Surface.cpp
|
|
||||||
src/DesktopSurface.cpp
|
|
||||||
src/TopBarSurface.cpp
|
|
||||||
src/LauncherSurface.cpp
|
|
||||||
)
|
|
||||||
|
|
||||||
target_link_libraries(nebula-shell PRIVATE
|
|
||||||
PkgConfig::GTK4
|
|
||||||
PkgConfig::GTK4_LAYER_SHELL
|
|
||||||
PkgConfig::WEBKITGTK
|
|
||||||
)
|
|
||||||
|
|
||||||
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
|
||||||
target_compile_options(nebula-shell PRIVATE -Wall -Wextra)
|
|
||||||
endif()
|
|
||||||
@@ -1,146 +0,0 @@
|
|||||||
# Nebula Desktop native host
|
|
||||||
|
|
||||||
`nebula-shell` hosts the React-based Nebula Desktop chrome as real Wayland Layer Shell surfaces.
|
|
||||||
|
|
||||||
It is a C++ process that creates **several** GTK4 windows, each backed by WebKitGTK 6.0 and placed with **gtk4-layer-shell**. It does not composite application windows. Those remain ordinary Wayland / XWayland clients of the compositor.
|
|
||||||
|
|
||||||
Stack:
|
|
||||||
|
|
||||||
- C++20
|
|
||||||
- GTK4
|
|
||||||
- gtk4-layer-shell
|
|
||||||
- WebKitGTK 6.0
|
|
||||||
- CMake
|
|
||||||
- Wayland
|
|
||||||
|
|
||||||
This host is Linux-only. It will not compile on Windows.
|
|
||||||
|
|
||||||
## Purpose
|
|
||||||
|
|
||||||
Nebula Desktop's React UI is shell chrome only: wallpaper/desktop, top bar, and launcher. `nebula-shell` maps each of those to a Wayland layer surface so they sit in the compositor like a real desktop environment, not like a browser window.
|
|
||||||
|
|
||||||
Normal applications and games never render inside WebKit.
|
|
||||||
|
|
||||||
## Architecture
|
|
||||||
|
|
||||||
```text
|
|
||||||
Nebula Shell
|
|
||||||
├── DesktopSurface
|
|
||||||
│ ├── WebKitGTK
|
|
||||||
│ ├── ?surface=desktop
|
|
||||||
│ └── BACKGROUND
|
|
||||||
│
|
|
||||||
├── TopBarSurface
|
|
||||||
│ ├── WebKitGTK
|
|
||||||
│ ├── ?surface=topbar
|
|
||||||
│ └── TOP
|
|
||||||
│
|
|
||||||
└── LauncherSurface
|
|
||||||
├── WebKitGTK
|
|
||||||
├── ?surface=launcher
|
|
||||||
└── OVERLAY
|
|
||||||
```
|
|
||||||
|
|
||||||
| Surface | Query | Layer Shell layer | Placement |
|
|
||||||
|-----------|-----------------------|-------------------|-----------|
|
|
||||||
| Desktop | `?surface=desktop` | `BACKGROUND` | Anchored to all monitor edges |
|
|
||||||
| Top Bar | `?surface=topbar` | `TOP` | Anchored top/left/right, 40px (matches the React top bar), exclusive zone enabled |
|
|
||||||
| Launcher | `?surface=launcher` | `OVERLAY` | Created at startup, hidden/unmapped until later toggle work |
|
|
||||||
|
|
||||||
```text
|
|
||||||
OVERLAY
|
|
||||||
Nebula Launcher ← created, currently unmapped
|
|
||||||
notifications
|
|
||||||
OSDs
|
|
||||||
|
|
||||||
TOP
|
|
||||||
Nebula Top Bar ← exclusive zone; apps do not go under it
|
|
||||||
|
|
||||||
NORMAL APPLICATION WINDOWS
|
|
||||||
Firefox, Steam, Blender, games, terminals, …
|
|
||||||
|
|
||||||
BACKGROUND
|
|
||||||
Nebula Desktop
|
|
||||||
wallpaper
|
|
||||||
```
|
|
||||||
|
|
||||||
React draws **shell chrome only**. Applications and games render through the compositor, not through WebKit:
|
|
||||||
|
|
||||||
```text
|
|
||||||
App
|
|
||||||
↓
|
|
||||||
Wayland / XWayland
|
|
||||||
↓
|
|
||||||
Compositor
|
|
||||||
↓
|
|
||||||
GPU
|
|
||||||
```
|
|
||||||
|
|
||||||
## Development
|
|
||||||
|
|
||||||
The host must not hardcode a Vite host or port. It reads a base origin from the environment and appends the surface query parameter.
|
|
||||||
|
|
||||||
Start the React UI (the port is an example, not part of the native architecture):
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd shells/desktop/ui
|
|
||||||
npm run dev -- --host 127.0.0.1 --port 5174 --strictPort
|
|
||||||
```
|
|
||||||
|
|
||||||
Point the shell at that origin:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
export NEBULA_SHELL_DEV_URL=http://127.0.0.1:5174
|
|
||||||
```
|
|
||||||
|
|
||||||
That loads:
|
|
||||||
|
|
||||||
```text
|
|
||||||
{NEBULA_SHELL_DEV_URL}/?surface=desktop
|
|
||||||
{NEBULA_SHELL_DEV_URL}/?surface=topbar
|
|
||||||
{NEBULA_SHELL_DEV_URL}/?surface=launcher
|
|
||||||
```
|
|
||||||
|
|
||||||
Build:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
cd shells/desktop/native
|
|
||||||
|
|
||||||
cmake -S . -B build
|
|
||||||
cmake --build build -j$(nproc)
|
|
||||||
```
|
|
||||||
|
|
||||||
Run inside a Wayland compositor such as Sway:
|
|
||||||
|
|
||||||
```bash
|
|
||||||
./build/nebula-shell
|
|
||||||
```
|
|
||||||
|
|
||||||
On startup the desktop and top bar are presented. The launcher is created but left hidden.
|
|
||||||
|
|
||||||
A combined browser preview (`/?surface=preview`, also the default) is for Windows UI work only. The native host loads the three surfaces separately.
|
|
||||||
|
|
||||||
### Linux packages
|
|
||||||
|
|
||||||
Build dependencies (install on the Ubuntu machine, not from this tree):
|
|
||||||
|
|
||||||
```text
|
|
||||||
libgtk-4-dev
|
|
||||||
libgtk4-layer-shell-dev
|
|
||||||
libwebkitgtk-6.0-dev
|
|
||||||
pkg-config
|
|
||||||
cmake
|
|
||||||
build-essential
|
|
||||||
```
|
|
||||||
|
|
||||||
## Production
|
|
||||||
|
|
||||||
Production loading from bundled `shells/desktop/ui/dist` files is not implemented yet. This milestone is development-URL loading only.
|
|
||||||
|
|
||||||
## Shell bridge
|
|
||||||
|
|
||||||
Not implemented in this milestone. Each WebKit view will later talk to the host through a JavaScript bridge (`shellBridge` in the React UI) so the top bar can map and unmap `LauncherSurface`.
|
|
||||||
|
|
||||||
## Current development compositor
|
|
||||||
|
|
||||||
Sway is the temporary compositor. See `compositor/sway/nebula-sway.conf`.
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#include "DesktopSurface.hpp"
|
|
||||||
|
|
||||||
DesktopSurface::DesktopSurface(GtkApplication* app, const std::string& base_url)
|
|
||||||
: Surface(app, base_url, Config{
|
|
||||||
.layer_namespace = "nebula-desktop",
|
|
||||||
.surface_name = "desktop",
|
|
||||||
.layer = GTK_LAYER_SHELL_LAYER_BACKGROUND,
|
|
||||||
.anchor_top = true,
|
|
||||||
.anchor_bottom = true,
|
|
||||||
.anchor_left = true,
|
|
||||||
.anchor_right = true,
|
|
||||||
.height_px = -1,
|
|
||||||
.keyboard_mode = GTK_LAYER_SHELL_KEYBOARD_MODE_NONE,
|
|
||||||
.auto_exclusive_zone = false,
|
|
||||||
})
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "Surface.hpp"
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
class DesktopSurface : public Surface {
|
|
||||||
public:
|
|
||||||
DesktopSurface(GtkApplication* app, const std::string& base_url);
|
|
||||||
};
|
|
||||||
@@ -1,21 +0,0 @@
|
|||||||
#include "LauncherSurface.hpp"
|
|
||||||
|
|
||||||
LauncherSurface::LauncherSurface(GtkApplication* app, const std::string& base_url)
|
|
||||||
: Surface(app, base_url, Config{
|
|
||||||
.layer_namespace = "nebula-launcher",
|
|
||||||
.surface_name = "launcher",
|
|
||||||
.layer = GTK_LAYER_SHELL_LAYER_OVERLAY,
|
|
||||||
.anchor_top = true,
|
|
||||||
.anchor_bottom = true,
|
|
||||||
.anchor_left = true,
|
|
||||||
.anchor_right = true,
|
|
||||||
.height_px = -1,
|
|
||||||
.keyboard_mode = GTK_LAYER_SHELL_KEYBOARD_MODE_ON_DEMAND,
|
|
||||||
.auto_exclusive_zone = false,
|
|
||||||
})
|
|
||||||
{
|
|
||||||
// Created and fully configured, but left unmapped on purpose.
|
|
||||||
// Later, when shellBridge is wired to WebKitGTK, call present() to show
|
|
||||||
// the launcher and gtk_widget_set_visible(GTK_WIDGET(window()), FALSE) to hide it.
|
|
||||||
gtk_widget_set_visible(GTK_WIDGET(window()), FALSE);
|
|
||||||
}
|
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "Surface.hpp"
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
class LauncherSurface : public Surface {
|
|
||||||
public:
|
|
||||||
LauncherSurface(GtkApplication* app, const std::string& base_url);
|
|
||||||
};
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
#include "ShellApplication.hpp"
|
|
||||||
|
|
||||||
#include <gtk4-layer-shell.h>
|
|
||||||
|
|
||||||
#include <algorithm>
|
|
||||||
#include <cctype>
|
|
||||||
#include <cstdlib>
|
|
||||||
#include <iostream>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
#ifndef G_APPLICATION_DEFAULT_FLAGS
|
|
||||||
#define G_APPLICATION_DEFAULT_FLAGS G_APPLICATION_FLAGS_NONE
|
|
||||||
#endif
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
std::string trim_copy(std::string value)
|
|
||||||
{
|
|
||||||
const auto is_not_space = [](unsigned char c) { return !std::isspace(c); };
|
|
||||||
value.erase(value.begin(), std::find_if(value.begin(), value.end(), is_not_space));
|
|
||||||
value.erase(std::find_if(value.rbegin(), value.rend(), is_not_space).base(), value.end());
|
|
||||||
return value;
|
|
||||||
}
|
|
||||||
|
|
||||||
std::string read_dev_url()
|
|
||||||
{
|
|
||||||
const char* value = std::getenv("NEBULA_SHELL_DEV_URL");
|
|
||||||
if (value == nullptr) {
|
|
||||||
return {};
|
|
||||||
}
|
|
||||||
return trim_copy(value);
|
|
||||||
}
|
|
||||||
|
|
||||||
void print_missing_dev_url_error()
|
|
||||||
{
|
|
||||||
std::cerr
|
|
||||||
<< "nebula-shell: NEBULA_SHELL_DEV_URL is not set or empty.\n"
|
|
||||||
<< "Set it to the origin of the Nebula Desktop UI, then run nebula-shell again.\n"
|
|
||||||
<< "Example:\n"
|
|
||||||
<< " export NEBULA_SHELL_DEV_URL=<scheme>://<host>:<port>\n";
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
void ShellApplication::on_activate(GtkApplication* app, gpointer user_data)
|
|
||||||
{
|
|
||||||
auto* self = static_cast<ShellApplication*>(user_data);
|
|
||||||
self->activate(app);
|
|
||||||
}
|
|
||||||
|
|
||||||
void ShellApplication::activate(GtkApplication* app)
|
|
||||||
{
|
|
||||||
if (desktop_ != nullptr) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (!gtk_layer_is_supported()) {
|
|
||||||
std::cerr
|
|
||||||
<< "nebula-shell: this session does not support wlr-layer-shell.\n"
|
|
||||||
<< "Run nebula-shell inside a Wayland compositor such as Sway.\n";
|
|
||||||
g_application_quit(G_APPLICATION(app));
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
desktop_ = std::make_unique<DesktopSurface>(app, base_url_);
|
|
||||||
topBar_ = std::make_unique<TopBarSurface>(app, base_url_);
|
|
||||||
launcher_ = std::make_unique<LauncherSurface>(app, base_url_);
|
|
||||||
|
|
||||||
desktop_->present();
|
|
||||||
topBar_->present();
|
|
||||||
// launcher_ is created above but not presented; it stays hidden/unmapped.
|
|
||||||
|
|
||||||
started_ = true;
|
|
||||||
}
|
|
||||||
|
|
||||||
int ShellApplication::run(int argc, char** argv)
|
|
||||||
{
|
|
||||||
base_url_ = read_dev_url();
|
|
||||||
if (base_url_.empty()) {
|
|
||||||
print_missing_dev_url_error();
|
|
||||||
return EXIT_FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (base_url_.find("://") == std::string::npos) {
|
|
||||||
std::cerr
|
|
||||||
<< "nebula-shell: NEBULA_SHELL_DEV_URL must include a URI scheme "
|
|
||||||
<< "(for example http or https).\n";
|
|
||||||
return EXIT_FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
GtkApplication* app = gtk_application_new("org.nebulaos.shell", G_APPLICATION_DEFAULT_FLAGS);
|
|
||||||
if (app == nullptr) {
|
|
||||||
std::cerr << "nebula-shell: failed to create the GTK application.\n";
|
|
||||||
return EXIT_FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
g_signal_connect(app, "activate", G_CALLBACK(on_activate), this);
|
|
||||||
|
|
||||||
const int status = g_application_run(G_APPLICATION(app), argc, argv);
|
|
||||||
g_object_unref(app);
|
|
||||||
|
|
||||||
if (!started_) {
|
|
||||||
return EXIT_FAILURE;
|
|
||||||
}
|
|
||||||
|
|
||||||
return status;
|
|
||||||
}
|
|
||||||
@@ -1,26 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "DesktopSurface.hpp"
|
|
||||||
#include "LauncherSurface.hpp"
|
|
||||||
#include "TopBarSurface.hpp"
|
|
||||||
|
|
||||||
#include <gtk/gtk.h>
|
|
||||||
|
|
||||||
#include <memory>
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
class ShellApplication {
|
|
||||||
public:
|
|
||||||
int run(int argc, char** argv);
|
|
||||||
|
|
||||||
private:
|
|
||||||
static void on_activate(GtkApplication* app, gpointer user_data);
|
|
||||||
void activate(GtkApplication* app);
|
|
||||||
|
|
||||||
std::string base_url_;
|
|
||||||
bool started_ = false;
|
|
||||||
|
|
||||||
std::unique_ptr<DesktopSurface> desktop_;
|
|
||||||
std::unique_ptr<TopBarSurface> topBar_;
|
|
||||||
std::unique_ptr<LauncherSurface> launcher_;
|
|
||||||
};
|
|
||||||
@@ -1,115 +0,0 @@
|
|||||||
#include "Surface.hpp"
|
|
||||||
|
|
||||||
#include <webkit/webkit.h>
|
|
||||||
|
|
||||||
#include <iostream>
|
|
||||||
|
|
||||||
namespace {
|
|
||||||
|
|
||||||
void apply_transparent_window_css()
|
|
||||||
{
|
|
||||||
static bool applied = false;
|
|
||||||
if (applied) {
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
applied = true;
|
|
||||||
|
|
||||||
GtkCssProvider* provider = gtk_css_provider_new();
|
|
||||||
const char* css = "window.nebula-shell-surface { background-color: transparent; }";
|
|
||||||
|
|
||||||
#if GTK_CHECK_VERSION(4, 12, 0)
|
|
||||||
gtk_css_provider_load_from_string(provider, css);
|
|
||||||
#else
|
|
||||||
gtk_css_provider_load_from_data(provider, css, -1);
|
|
||||||
#endif
|
|
||||||
|
|
||||||
GdkDisplay* display = gdk_display_get_default();
|
|
||||||
if (display != nullptr) {
|
|
||||||
gtk_style_context_add_provider_for_display(
|
|
||||||
display,
|
|
||||||
GTK_STYLE_PROVIDER(provider),
|
|
||||||
GTK_STYLE_PROVIDER_PRIORITY_APPLICATION);
|
|
||||||
}
|
|
||||||
|
|
||||||
g_object_unref(provider);
|
|
||||||
}
|
|
||||||
|
|
||||||
gboolean on_load_failed(
|
|
||||||
[[maybe_unused]] WebKitWebView* web_view,
|
|
||||||
[[maybe_unused]] WebKitLoadEvent load_event,
|
|
||||||
char* failing_uri,
|
|
||||||
GError* error,
|
|
||||||
[[maybe_unused]] gpointer user_data)
|
|
||||||
{
|
|
||||||
const char* uri = failing_uri != nullptr ? failing_uri : "(unknown URI)";
|
|
||||||
const char* message = (error != nullptr && error->message != nullptr)
|
|
||||||
? error->message
|
|
||||||
: "unknown error";
|
|
||||||
std::cerr << "nebula-shell: failed to load " << uri << ": " << message << '\n';
|
|
||||||
return FALSE;
|
|
||||||
}
|
|
||||||
|
|
||||||
} // namespace
|
|
||||||
|
|
||||||
std::string make_surface_url(std::string_view base_url, std::string_view surface_name)
|
|
||||||
{
|
|
||||||
std::string url{base_url};
|
|
||||||
while (!url.empty() && url.back() == '/') {
|
|
||||||
url.pop_back();
|
|
||||||
}
|
|
||||||
url += "/?surface=";
|
|
||||||
url += surface_name;
|
|
||||||
return url;
|
|
||||||
}
|
|
||||||
|
|
||||||
Surface::Surface(GtkApplication* app, const std::string& base_url, const Config& config)
|
|
||||||
{
|
|
||||||
apply_transparent_window_css();
|
|
||||||
|
|
||||||
const std::string url = make_surface_url(base_url, config.surface_name);
|
|
||||||
|
|
||||||
window_ = GTK_WINDOW(gtk_application_window_new(app));
|
|
||||||
gtk_window_set_decorated(window_, FALSE);
|
|
||||||
gtk_window_set_title(window_, config.layer_namespace);
|
|
||||||
gtk_widget_add_css_class(GTK_WIDGET(window_), "nebula-shell-surface");
|
|
||||||
|
|
||||||
// Layer Shell must be configured before the window is realized or presented.
|
|
||||||
gtk_layer_init_for_window(window_);
|
|
||||||
gtk_layer_set_namespace(window_, config.layer_namespace);
|
|
||||||
gtk_layer_set_layer(window_, config.layer);
|
|
||||||
gtk_layer_set_anchor(window_, GTK_LAYER_SHELL_EDGE_TOP, config.anchor_top);
|
|
||||||
gtk_layer_set_anchor(window_, GTK_LAYER_SHELL_EDGE_BOTTOM, config.anchor_bottom);
|
|
||||||
gtk_layer_set_anchor(window_, GTK_LAYER_SHELL_EDGE_LEFT, config.anchor_left);
|
|
||||||
gtk_layer_set_anchor(window_, GTK_LAYER_SHELL_EDGE_RIGHT, config.anchor_right);
|
|
||||||
gtk_layer_set_keyboard_mode(window_, config.keyboard_mode);
|
|
||||||
|
|
||||||
if (config.auto_exclusive_zone) {
|
|
||||||
gtk_layer_auto_exclusive_zone_enable(window_);
|
|
||||||
} else {
|
|
||||||
gtk_layer_set_exclusive_zone(window_, 0);
|
|
||||||
}
|
|
||||||
|
|
||||||
if (config.height_px > 0) {
|
|
||||||
gtk_window_set_default_size(window_, 1, config.height_px);
|
|
||||||
gtk_widget_set_size_request(GTK_WIDGET(window_), -1, config.height_px);
|
|
||||||
}
|
|
||||||
|
|
||||||
GtkWidget* webview_widget = webkit_web_view_new();
|
|
||||||
WebKitWebView* webview = WEBKIT_WEB_VIEW(webview_widget);
|
|
||||||
|
|
||||||
GdkRGBA transparent{0.0f, 0.0f, 0.0f, 0.0f};
|
|
||||||
webkit_web_view_set_background_color(webview, &transparent);
|
|
||||||
|
|
||||||
gtk_widget_set_hexpand(webview_widget, TRUE);
|
|
||||||
gtk_widget_set_vexpand(webview_widget, TRUE);
|
|
||||||
|
|
||||||
g_signal_connect(webview, "load-failed", G_CALLBACK(on_load_failed), nullptr);
|
|
||||||
|
|
||||||
gtk_window_set_child(window_, webview_widget);
|
|
||||||
webkit_web_view_load_uri(webview, url.c_str());
|
|
||||||
}
|
|
||||||
|
|
||||||
void Surface::present()
|
|
||||||
{
|
|
||||||
gtk_window_present(window_);
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <gtk/gtk.h>
|
|
||||||
#include <gtk4-layer-shell.h>
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <string_view>
|
|
||||||
|
|
||||||
// Shared Layer Shell + WebKitGTK surface. Subclasses only supply layout.
|
|
||||||
class Surface {
|
|
||||||
public:
|
|
||||||
struct Config {
|
|
||||||
const char* layer_namespace = nullptr;
|
|
||||||
const char* surface_name = nullptr;
|
|
||||||
GtkLayerShellLayer layer = GTK_LAYER_SHELL_LAYER_TOP;
|
|
||||||
bool anchor_top = false;
|
|
||||||
bool anchor_bottom = false;
|
|
||||||
bool anchor_left = false;
|
|
||||||
bool anchor_right = false;
|
|
||||||
int height_px = -1;
|
|
||||||
GtkLayerShellKeyboardMode keyboard_mode = GTK_LAYER_SHELL_KEYBOARD_MODE_NONE;
|
|
||||||
bool auto_exclusive_zone = false;
|
|
||||||
};
|
|
||||||
|
|
||||||
Surface(GtkApplication* app, const std::string& base_url, const Config& config);
|
|
||||||
~Surface() = default;
|
|
||||||
|
|
||||||
Surface(const Surface&) = delete;
|
|
||||||
Surface& operator=(const Surface&) = delete;
|
|
||||||
Surface(Surface&&) = delete;
|
|
||||||
Surface& operator=(Surface&&) = delete;
|
|
||||||
|
|
||||||
void present();
|
|
||||||
GtkWindow* window() const { return window_; }
|
|
||||||
|
|
||||||
private:
|
|
||||||
GtkWindow* window_ = nullptr;
|
|
||||||
};
|
|
||||||
|
|
||||||
std::string make_surface_url(std::string_view base_url, std::string_view surface_name);
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#include "TopBarSurface.hpp"
|
|
||||||
|
|
||||||
TopBarSurface::TopBarSurface(GtkApplication* app, const std::string& base_url)
|
|
||||||
: Surface(app, base_url, Config{
|
|
||||||
.layer_namespace = "nebula-topbar",
|
|
||||||
.surface_name = "topbar",
|
|
||||||
.layer = GTK_LAYER_SHELL_LAYER_TOP,
|
|
||||||
.anchor_top = true,
|
|
||||||
.anchor_bottom = false,
|
|
||||||
.anchor_left = true,
|
|
||||||
.anchor_right = true,
|
|
||||||
.height_px = kTopBarHeightPx,
|
|
||||||
.keyboard_mode = GTK_LAYER_SHELL_KEYBOARD_MODE_NONE,
|
|
||||||
.auto_exclusive_zone = true,
|
|
||||||
})
|
|
||||||
{
|
|
||||||
}
|
|
||||||
@@ -1,13 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "Surface.hpp"
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
|
|
||||||
// Matches --nebula-topbar-height in shells/desktop/ui/src/styles/tokens.css.
|
|
||||||
inline constexpr int kTopBarHeightPx = 40;
|
|
||||||
|
|
||||||
class TopBarSurface : public Surface {
|
|
||||||
public:
|
|
||||||
TopBarSurface(GtkApplication* app, const std::string& base_url);
|
|
||||||
};
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
#include "ShellApplication.hpp"
|
|
||||||
|
|
||||||
int main(int argc, char* argv[])
|
|
||||||
{
|
|
||||||
ShellApplication application;
|
|
||||||
return application.run(argc, argv);
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,48 @@
|
|||||||
|
# Nebula Desktop UI — prototype (archived)
|
||||||
|
|
||||||
|
```text
|
||||||
|
DESIGN REFERENCE ONLY
|
||||||
|
NOT THE PRODUCTION NEBULA SHELL
|
||||||
|
```
|
||||||
|
|
||||||
|
This directory is the original React + Vite desktop chrome. It remains only as a visual and behavioural reference while the Qt Quick/QML shell catches up.
|
||||||
|
|
||||||
|
**Do not add production functionality here.**
|
||||||
|
|
||||||
|
The production Nebula Desktop shell is:
|
||||||
|
|
||||||
|
```text
|
||||||
|
shells/desktop/shell
|
||||||
|
C++20 + Qt 6 + Qt Quick/QML + LayerShellQt
|
||||||
|
```
|
||||||
|
|
||||||
|
This prototype must not be launched as the OS shell. It does not talk to Wayland layer-shell, and it is not wired to Nebula Core.
|
||||||
|
|
||||||
|
## What this is for
|
||||||
|
|
||||||
|
- comparing layout, colour, spacing, and motion against `shells/desktop/shell`
|
||||||
|
- checking launcher search, mock launch, and top-bar treatment
|
||||||
|
- discarding this tree later, once Qt parity is accepted
|
||||||
|
|
||||||
|
## Surfaces (browser preview only)
|
||||||
|
|
||||||
|
| URL | Renders |
|
||||||
|
|-----|---------|
|
||||||
|
| `/?surface=preview` (default) | Combined mockup |
|
||||||
|
| `/?surface=desktop` | Background only |
|
||||||
|
| `/?surface=topbar` | Top bar only |
|
||||||
|
| `/?surface=launcher` | Launcher only |
|
||||||
|
|
||||||
|
## Preview on a development machine
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm install
|
||||||
|
npm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
```powershell
|
||||||
|
npm run lint
|
||||||
|
npm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
Oxlint applies to this JavaScript prototype only. It is not used for the Qt shell.
|
||||||
|
Before Width: | Height: | Size: 325 B After Width: | Height: | Size: 325 B |
Vendored
+5
-5
@@ -1,13 +1,13 @@
|
|||||||
/**
|
/**
|
||||||
* Nebula JavaScript API
|
* Nebula JavaScript API
|
||||||
*
|
*
|
||||||
* Boundary between the React shell UI and the future native Nebula Core:
|
* Placeholder APIs for the archived React prototype.
|
||||||
*
|
*
|
||||||
* React UI → nebula.* → native host / Nebula Core
|
* Archived React prototype → nebula.* placeholders
|
||||||
*
|
*
|
||||||
* Every method here is a placeholder. Implementations must not call Node
|
* Production system APIs will be C++ services exposed to QML, not this
|
||||||
* APIs, spawn processes, or talk to D-Bus, PipeWire, NetworkManager, or
|
* JavaScript module. Methods here must not call Node APIs, spawn processes,
|
||||||
* Wayland. The WebKitGTK host will later inject a real bridge.
|
* or talk to D-Bus, PipeWire, NetworkManager, or Wayland.
|
||||||
*
|
*
|
||||||
* The shell is chrome only. Applications and games render as compositor
|
* The shell is chrome only. Applications and games render as compositor
|
||||||
* surfaces, not inside this React tree.
|
* surfaces, not inside this React tree.
|
||||||
Vendored
+2
-3
@@ -1,9 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Shell coordination bridge.
|
* Shell coordination bridge.
|
||||||
*
|
*
|
||||||
* Top bar, launcher, and desktop will later live in separate WebKitGTK views.
|
* In-page mock for coordinating the archived browser preview.
|
||||||
* They must not share React component state. The native host will replace this
|
* Production shell state is the QML ShellState singleton, not this bridge.
|
||||||
* module with an injected C++ bridge.
|
|
||||||
*
|
*
|
||||||
* This development mock keeps in-page state and broadcasts it with window
|
* This development mock keeps in-page state and broadcasts it with window
|
||||||
* CustomEvents. No networking, WebSockets, or Node APIs.
|
* CustomEvents. No networking, WebSockets, or Node APIs.
|
||||||
+2
-3
@@ -1,9 +1,8 @@
|
|||||||
/**
|
/**
|
||||||
* Shell surface routing.
|
* Shell surface routing.
|
||||||
*
|
*
|
||||||
* The native host will load this UI in separate WebKitGTK views, each with a
|
* Browser preview routing for the archived React prototype.
|
||||||
* surface query parameter. Never hardcode a host or port — the loader decides
|
* Production surfaces are separate Qt Quick windows, not WebKit views.
|
||||||
* the URL; this module only reads the current location.
|
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export const SURFACES = {
|
export const SURFACES = {
|
||||||
+2
-1
@@ -9,7 +9,8 @@ import './surfaces.css';
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Browser-only composition of the three shell surfaces.
|
* Browser-only composition of the three shell surfaces.
|
||||||
* Production loads each surface in its own WebKitGTK view.
|
* Browser-only composition of the three shell surfaces.
|
||||||
|
* Archived design reference — production chrome is Qt Quick/QML.
|
||||||
*/
|
*/
|
||||||
export function PreviewSurface() {
|
export function PreviewSurface() {
|
||||||
const { launcherOpen } = useShellState();
|
const { launcherOpen } = useShellState();
|
||||||
@@ -0,0 +1,98 @@
|
|||||||
|
cmake_minimum_required(VERSION 3.20)
|
||||||
|
|
||||||
|
if(NOT CMAKE_SYSTEM_NAME STREQUAL "Linux")
|
||||||
|
message(FATAL_ERROR
|
||||||
|
"nebula-shell is a Linux Wayland shell using LayerShellQt.\n"
|
||||||
|
"Build and run it on the Ubuntu NebulaOS VM. Windows is not a supported target, "
|
||||||
|
"and LayerShellQt is not stubbed with other platform APIs.")
|
||||||
|
endif()
|
||||||
|
|
||||||
|
project(nebula-shell LANGUAGES CXX)
|
||||||
|
|
||||||
|
set(CMAKE_CXX_STANDARD 20)
|
||||||
|
set(CMAKE_CXX_STANDARD_REQUIRED ON)
|
||||||
|
set(CMAKE_CXX_EXTENSIONS OFF)
|
||||||
|
|
||||||
|
find_package(Qt6 6.4 REQUIRED COMPONENTS Core Gui Qml Quick)
|
||||||
|
find_package(LayerShellQt REQUIRED)
|
||||||
|
|
||||||
|
qt_standard_project_setup(REQUIRES 6.4)
|
||||||
|
|
||||||
|
set(QT_QML_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}/qml")
|
||||||
|
|
||||||
|
add_subdirectory(
|
||||||
|
"${CMAKE_CURRENT_SOURCE_DIR}/../../../packages/nebula-ui"
|
||||||
|
"${CMAKE_CURRENT_BINARY_DIR}/nebula-ui"
|
||||||
|
)
|
||||||
|
|
||||||
|
qt_add_executable(nebula-shell
|
||||||
|
src/main.cpp
|
||||||
|
)
|
||||||
|
|
||||||
|
set(NEBULA_SHELL_QML_FILES
|
||||||
|
qml/Main.qml
|
||||||
|
qml/ShellState.qml
|
||||||
|
qml/surfaces/DesktopSurface.qml
|
||||||
|
qml/surfaces/TopBarSurface.qml
|
||||||
|
qml/surfaces/LauncherSurface.qml
|
||||||
|
qml/components/TopBar.qml
|
||||||
|
qml/components/NebulaButton.qml
|
||||||
|
qml/components/Clock.qml
|
||||||
|
qml/components/SystemArea.qml
|
||||||
|
qml/components/Launcher.qml
|
||||||
|
qml/components/AppItem.qml
|
||||||
|
qml/mock/MockApps.qml
|
||||||
|
)
|
||||||
|
|
||||||
|
foreach(qml_file IN LISTS NEBULA_SHELL_QML_FILES)
|
||||||
|
get_filename_component(qml_name "${qml_file}" NAME)
|
||||||
|
set_source_files_properties("${qml_file}" PROPERTIES
|
||||||
|
QT_RESOURCE_ALIAS "${qml_name}"
|
||||||
|
)
|
||||||
|
endforeach()
|
||||||
|
|
||||||
|
set_source_files_properties(qml/ShellState.qml PROPERTIES
|
||||||
|
QT_QML_SINGLETON_TYPE TRUE
|
||||||
|
QT_RESOURCE_ALIAS ShellState.qml
|
||||||
|
)
|
||||||
|
set_source_files_properties(qml/mock/MockApps.qml PROPERTIES
|
||||||
|
QT_QML_SINGLETON_TYPE TRUE
|
||||||
|
QT_RESOURCE_ALIAS MockApps.qml
|
||||||
|
)
|
||||||
|
|
||||||
|
qt_add_qml_module(nebula-shell
|
||||||
|
URI Nebula.Shell
|
||||||
|
VERSION 1.0
|
||||||
|
RESOURCE_PREFIX /qt/qml
|
||||||
|
QML_FILES ${NEBULA_SHELL_QML_FILES}
|
||||||
|
DEPENDENCIES
|
||||||
|
QtQuick
|
||||||
|
Nebula.UI
|
||||||
|
)
|
||||||
|
|
||||||
|
target_link_libraries(nebula-shell
|
||||||
|
PRIVATE
|
||||||
|
Qt6::Core
|
||||||
|
Qt6::Gui
|
||||||
|
Qt6::Qml
|
||||||
|
Qt6::Quick
|
||||||
|
LayerShellQt::Interface
|
||||||
|
NebulaUI
|
||||||
|
)
|
||||||
|
|
||||||
|
if(TARGET NebulaUIplugin)
|
||||||
|
target_link_libraries(nebula-shell PRIVATE NebulaUIplugin)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
set_target_properties(nebula-shell PROPERTIES
|
||||||
|
OUTPUT_NAME nebula-shell
|
||||||
|
RUNTIME_OUTPUT_DIRECTORY "${CMAKE_BINARY_DIR}"
|
||||||
|
)
|
||||||
|
|
||||||
|
if(COMMAND qt_import_qml_plugins)
|
||||||
|
qt_import_qml_plugins(nebula-shell)
|
||||||
|
endif()
|
||||||
|
|
||||||
|
if(CMAKE_CXX_COMPILER_ID MATCHES "GNU|Clang")
|
||||||
|
target_compile_options(nebula-shell PRIVATE -Wall -Wextra)
|
||||||
|
endif()
|
||||||
@@ -0,0 +1,16 @@
|
|||||||
|
import QtQuick
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
id: shell
|
||||||
|
|
||||||
|
readonly property DesktopSurface desktop: DesktopSurface {}
|
||||||
|
readonly property TopBarSurface topBar: TopBarSurface {}
|
||||||
|
readonly property LauncherSurface launcher: LauncherSurface {}
|
||||||
|
|
||||||
|
Shortcut {
|
||||||
|
sequence: "Escape"
|
||||||
|
enabled: ShellState.launcherOpen
|
||||||
|
context: Qt.ApplicationShortcut
|
||||||
|
onActivated: ShellState.closeLauncher()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,25 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
property bool launcherOpen: false
|
||||||
|
property string activeApplication: "Desktop"
|
||||||
|
|
||||||
|
function openLauncher() {
|
||||||
|
launcherOpen = true
|
||||||
|
}
|
||||||
|
|
||||||
|
function closeLauncher() {
|
||||||
|
launcherOpen = false
|
||||||
|
}
|
||||||
|
|
||||||
|
function toggleLauncher() {
|
||||||
|
launcherOpen = !launcherOpen
|
||||||
|
}
|
||||||
|
|
||||||
|
function launch(name) {
|
||||||
|
activeApplication = name
|
||||||
|
closeLauncher()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,134 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Nebula.UI
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property string variant: "tile"
|
||||||
|
property var item
|
||||||
|
signal selected(var item)
|
||||||
|
|
||||||
|
readonly property bool rowVariant: variant === "row"
|
||||||
|
readonly property int iconSize: rowVariant ? 32 : (variant === "recent" ? 32 : 40)
|
||||||
|
readonly property int glyphSize: rowVariant ? 16 : 18
|
||||||
|
|
||||||
|
implicitWidth: rowVariant ? parent ? parent.width : 200 : 90
|
||||||
|
implicitHeight: rowVariant ? 48 : 78
|
||||||
|
Accessible.role: Accessible.Button
|
||||||
|
Accessible.name: item ? item.name : ""
|
||||||
|
Accessible.onPressAction: root.selected(item)
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.radiusMedium
|
||||||
|
color: hover.containsMouse ? Theme.tileHover : "transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.animationFast
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
visible: !root.rowVariant
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.topMargin: 10
|
||||||
|
anchors.bottomMargin: 8
|
||||||
|
anchors.leftMargin: 6
|
||||||
|
anchors.rightMargin: 6
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: root.iconSize
|
||||||
|
height: root.iconSize
|
||||||
|
radius: root.variant === "recent" ? 9 : 11
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
color: Theme.iconWell
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: parent.radius
|
||||||
|
color: "transparent"
|
||||||
|
border.color: Theme.iconWellHighlight
|
||||||
|
border.width: 1
|
||||||
|
}
|
||||||
|
|
||||||
|
NebulaIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: item && item.glyph ? item.glyph : "folder"
|
||||||
|
size: root.glyphSize
|
||||||
|
color: item && item.tint ? item.tint : Theme.accent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: item ? item.name : ""
|
||||||
|
color: Theme.textPrimary
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeApp
|
||||||
|
elide: Text.ElideRight
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
visible: root.rowVariant
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 8
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: root.iconSize
|
||||||
|
height: root.iconSize
|
||||||
|
radius: 9
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
color: Theme.iconWell
|
||||||
|
|
||||||
|
NebulaIcon {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
name: item && (item.glyph || item.kind) ? (item.glyph || item.kind) : "folder"
|
||||||
|
size: root.glyphSize
|
||||||
|
color: item && item.tint ? item.tint : Theme.accent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: parent.width - root.iconSize - 10
|
||||||
|
spacing: 1
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
text: item ? item.name : ""
|
||||||
|
color: Theme.textPrimary
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeApp
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
width: parent.width
|
||||||
|
visible: item && item.subtitle ? true : false
|
||||||
|
text: item && item.subtitle ? item.subtitle : ""
|
||||||
|
color: Theme.textTertiary
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
elide: Text.ElideRight
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: hover
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.selected(item)
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusFrame {
|
||||||
|
radius: Theme.radiusMedium
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQml
|
||||||
|
import Nebula.UI
|
||||||
|
|
||||||
|
Text {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property date now: new Date()
|
||||||
|
|
||||||
|
text: formatClock(now)
|
||||||
|
color: Theme.textPrimary
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
verticalAlignment: Text.AlignVCenter
|
||||||
|
horizontalAlignment: Text.AlignHCenter
|
||||||
|
leftPadding: 8
|
||||||
|
rightPadding: 8
|
||||||
|
height: Theme.controlHeight
|
||||||
|
Accessible.role: Accessible.StaticText
|
||||||
|
Accessible.name: formatFull(now)
|
||||||
|
|
||||||
|
Timer {
|
||||||
|
interval: 1000
|
||||||
|
running: true
|
||||||
|
repeat: true
|
||||||
|
onTriggered: root.now = new Date()
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatClock(date) {
|
||||||
|
const locale = Qt.locale()
|
||||||
|
const weekday = locale.toString(date, "ddd")
|
||||||
|
const time = locale.toString(date, locale.timeFormat(Locale.ShortFormat))
|
||||||
|
return weekday + " " + time
|
||||||
|
}
|
||||||
|
|
||||||
|
function formatFull(date) {
|
||||||
|
return Qt.locale().toString(date, Locale.LongFormat)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,295 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Nebula.UI
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
property real yOffset: 0
|
||||||
|
|
||||||
|
implicitWidth: 420
|
||||||
|
implicitHeight: 14 + searchField.implicitHeight + 14 + bodyColumn.implicitHeight + 14 + footer.height + 14
|
||||||
|
transform: Translate { y: root.yOffset }
|
||||||
|
transformOrigin: Item.TopLeft
|
||||||
|
Accessible.role: Accessible.Dialog
|
||||||
|
Accessible.name: qsTr("Launcher")
|
||||||
|
|
||||||
|
readonly property bool searching: searchField.text.trim().length > 0
|
||||||
|
readonly property var filteredApps: {
|
||||||
|
const query = searchField.text.trim().toLowerCase()
|
||||||
|
const apps = MockApps.apps
|
||||||
|
if (!query)
|
||||||
|
return apps
|
||||||
|
return apps.filter((app) => {
|
||||||
|
const haystack = (app.name + " " + (app.comment || "")).toLowerCase()
|
||||||
|
return haystack.indexOf(query) !== -1
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
Keys.onEscapePressed: {
|
||||||
|
event.accepted = true
|
||||||
|
ShellState.closeLauncher()
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: ShellState
|
||||||
|
function onLauncherOpenChanged() {
|
||||||
|
if (ShellState.launcherOpen) {
|
||||||
|
searchField.text = ""
|
||||||
|
searchField.input.forceActiveFocus()
|
||||||
|
} else {
|
||||||
|
searchField.text = ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NebulaCard {
|
||||||
|
id: card
|
||||||
|
anchors.fill: parent
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: column
|
||||||
|
anchors.fill: parent
|
||||||
|
anchors.margins: 14
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
NebulaTextField {
|
||||||
|
id: searchField
|
||||||
|
width: parent.width
|
||||||
|
placeholderText: qsTr("Search applications")
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: 14
|
||||||
|
}
|
||||||
|
|
||||||
|
Flickable {
|
||||||
|
id: body
|
||||||
|
width: parent.width
|
||||||
|
height: Math.max(0, column.height - searchField.height - footer.height - 29)
|
||||||
|
clip: true
|
||||||
|
contentWidth: width
|
||||||
|
contentHeight: bodyColumn.implicitHeight
|
||||||
|
boundsBehavior: Flickable.StopAtBounds
|
||||||
|
flickableDirection: Flickable.VerticalFlick
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: bodyColumn
|
||||||
|
width: body.width
|
||||||
|
spacing: 18
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 8
|
||||||
|
visible: !root.searching && MockApps.recents.length > 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("Recent")
|
||||||
|
color: Theme.textSecondary
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
font.letterSpacing: 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
Grid {
|
||||||
|
id: recentGrid
|
||||||
|
width: parent.width
|
||||||
|
columns: 4
|
||||||
|
columnSpacing: 6
|
||||||
|
rowSpacing: 6
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: MockApps.recents
|
||||||
|
delegate: AppItem {
|
||||||
|
required property var modelData
|
||||||
|
width: (recentGrid.width - 18) / 4
|
||||||
|
variant: "recent"
|
||||||
|
item: modelData
|
||||||
|
onSelected: root.launchApp(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: root.searching ? qsTr("Results") : qsTr("Applications")
|
||||||
|
color: Theme.textSecondary
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
font.letterSpacing: 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
Grid {
|
||||||
|
id: appGrid
|
||||||
|
visible: root.filteredApps.length > 0
|
||||||
|
width: parent.width
|
||||||
|
columns: 4
|
||||||
|
columnSpacing: 6
|
||||||
|
rowSpacing: 6
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: root.filteredApps
|
||||||
|
delegate: AppItem {
|
||||||
|
required property var modelData
|
||||||
|
width: (appGrid.width - 18) / 4
|
||||||
|
variant: "tile"
|
||||||
|
item: modelData
|
||||||
|
onSelected: root.launchApp(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
visible: root.filteredApps.length === 0
|
||||||
|
width: parent.width
|
||||||
|
topPadding: 8
|
||||||
|
text: qsTr("No matching applications")
|
||||||
|
color: Theme.textTertiary
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 8
|
||||||
|
visible: !root.searching && MockApps.projects.length > 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("Recent projects")
|
||||||
|
color: Theme.textSecondary
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
font.letterSpacing: 0.4
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
width: parent.width
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
Repeater {
|
||||||
|
model: MockApps.projects
|
||||||
|
delegate: AppItem {
|
||||||
|
required property var modelData
|
||||||
|
width: parent.width
|
||||||
|
variant: "row"
|
||||||
|
item: modelData
|
||||||
|
onSelected: root.openProject(item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
width: 1
|
||||||
|
height: 14
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: parent.width
|
||||||
|
height: 1
|
||||||
|
color: Theme.border
|
||||||
|
}
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: footer
|
||||||
|
width: parent.width
|
||||||
|
height: 38
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 10
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
width: 28
|
||||||
|
height: 28
|
||||||
|
radius: 14
|
||||||
|
color: Theme.accentSoft
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.centerIn: parent
|
||||||
|
text: MockApps.user.initials
|
||||||
|
color: Theme.accentStrong
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeApp
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 0
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: MockApps.user.name
|
||||||
|
color: Theme.textPrimary
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("Local session")
|
||||||
|
color: Theme.textTertiary
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSizeSmall
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
NebulaIconButton {
|
||||||
|
id: restartButton
|
||||||
|
label: qsTr("Restart")
|
||||||
|
onClicked: console.info("nebula: restart requested")
|
||||||
|
NebulaIcon {
|
||||||
|
name: "restart"
|
||||||
|
size: 16
|
||||||
|
color: restartButton.iconColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NebulaIconButton {
|
||||||
|
id: shutdownButton
|
||||||
|
label: qsTr("Shut down")
|
||||||
|
danger: true
|
||||||
|
onClicked: console.info("nebula: shutdown requested")
|
||||||
|
NebulaIcon {
|
||||||
|
name: "power"
|
||||||
|
size: 16
|
||||||
|
color: shutdownButton.iconColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
function launchApp(app) {
|
||||||
|
ShellState.launch(app.name)
|
||||||
|
}
|
||||||
|
|
||||||
|
function openProject(project) {
|
||||||
|
ShellState.launch(project.name)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Nebula.UI
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
implicitWidth: row.implicitWidth + 18
|
||||||
|
implicitHeight: Theme.controlHeight
|
||||||
|
property bool checked: false
|
||||||
|
|
||||||
|
signal clicked()
|
||||||
|
|
||||||
|
Accessible.role: Accessible.Button
|
||||||
|
Accessible.name: qsTr("Nebula")
|
||||||
|
Accessible.checkable: true
|
||||||
|
Accessible.checked: root.checked
|
||||||
|
Accessible.onPressAction: root.clicked()
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
radius: Theme.radiusSmall
|
||||||
|
color: (hover.containsMouse || root.checked) ? Theme.accentSoft : "transparent"
|
||||||
|
|
||||||
|
Behavior on color {
|
||||||
|
ColorAnimation {
|
||||||
|
duration: Theme.animationFast
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: row
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
NebulaIcon {
|
||||||
|
name: "nebula"
|
||||||
|
size: 18
|
||||||
|
color: root.checked ? Theme.accentStrong : Theme.textPrimary
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
text: qsTr("Nebula")
|
||||||
|
color: root.checked ? Theme.accentStrong : Theme.textPrimary
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
font.weight: Font.DemiBold
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
id: hover
|
||||||
|
anchors.fill: parent
|
||||||
|
hoverEnabled: true
|
||||||
|
cursorShape: Qt.PointingHandCursor
|
||||||
|
onClicked: root.clicked()
|
||||||
|
}
|
||||||
|
|
||||||
|
FocusFrame {
|
||||||
|
radius: Theme.radiusSmall
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,47 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Nebula.UI
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
spacing: 2
|
||||||
|
|
||||||
|
// Placeholder system status until native services exist.
|
||||||
|
readonly property int wifiStrength: 3
|
||||||
|
readonly property int volumePercent: 72
|
||||||
|
readonly property int batteryPercent: 84
|
||||||
|
|
||||||
|
NebulaIconButton {
|
||||||
|
id: networkButton
|
||||||
|
label: qsTr("Network connected, Nebula-Net")
|
||||||
|
NebulaIcon {
|
||||||
|
name: "wifi"
|
||||||
|
size: 16
|
||||||
|
color: networkButton.iconColor
|
||||||
|
value: root.wifiStrength
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NebulaIconButton {
|
||||||
|
id: volumeButton
|
||||||
|
label: qsTr("Volume %1%").arg(root.volumePercent)
|
||||||
|
NebulaIcon {
|
||||||
|
name: "speaker"
|
||||||
|
size: 16
|
||||||
|
color: volumeButton.iconColor
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
NebulaIconButton {
|
||||||
|
id: powerButton
|
||||||
|
label: qsTr("Battery %1%").arg(root.batteryPercent)
|
||||||
|
NebulaIcon {
|
||||||
|
name: "battery"
|
||||||
|
size: 16
|
||||||
|
color: powerButton.iconColor
|
||||||
|
value: root.batteryPercent
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Clock {}
|
||||||
|
}
|
||||||
@@ -0,0 +1,58 @@
|
|||||||
|
import QtQuick
|
||||||
|
import Nebula.UI
|
||||||
|
|
||||||
|
Item {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
implicitHeight: Theme.topBarHeight
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.fill: parent
|
||||||
|
color: Theme.topBarBackground
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.bottom: parent.bottom
|
||||||
|
height: 1
|
||||||
|
color: Theme.border
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Row {
|
||||||
|
id: left
|
||||||
|
anchors.left: parent.left
|
||||||
|
anchors.leftMargin: 6
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
spacing: 8
|
||||||
|
|
||||||
|
NebulaButton {
|
||||||
|
checked: ShellState.launcherOpen
|
||||||
|
onClicked: ShellState.toggleLauncher()
|
||||||
|
}
|
||||||
|
|
||||||
|
Rectangle {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: 1
|
||||||
|
height: 14
|
||||||
|
color: Theme.borderStrong
|
||||||
|
}
|
||||||
|
|
||||||
|
Text {
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
width: Math.min(implicitWidth, root.width * 0.4)
|
||||||
|
text: ShellState.activeApplication
|
||||||
|
color: Theme.textSecondary
|
||||||
|
font.family: Theme.fontFamily
|
||||||
|
font.pixelSize: Theme.fontSize
|
||||||
|
elide: Text.ElideRight
|
||||||
|
maximumLineCount: 1
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
SystemArea {
|
||||||
|
anchors.right: parent.right
|
||||||
|
anchors.rightMargin: 10
|
||||||
|
anchors.verticalCenter: parent.verticalCenter
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,100 @@
|
|||||||
|
pragma Singleton
|
||||||
|
|
||||||
|
import QtQuick
|
||||||
|
|
||||||
|
QtObject {
|
||||||
|
readonly property var apps: [
|
||||||
|
{
|
||||||
|
id: "files",
|
||||||
|
name: "Files",
|
||||||
|
comment: "Browse files and folders",
|
||||||
|
recent: true,
|
||||||
|
glyph: "folder",
|
||||||
|
tint: "#c4a574"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "firefox",
|
||||||
|
name: "Firefox",
|
||||||
|
comment: "Web browser",
|
||||||
|
recent: true,
|
||||||
|
glyph: "globe",
|
||||||
|
tint: "#d08a5a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "terminal",
|
||||||
|
name: "Terminal",
|
||||||
|
comment: "Command line",
|
||||||
|
recent: true,
|
||||||
|
glyph: "terminal",
|
||||||
|
tint: "#7eaa8e"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "settings",
|
||||||
|
name: "Settings",
|
||||||
|
comment: "System preferences",
|
||||||
|
recent: false,
|
||||||
|
glyph: "settings",
|
||||||
|
tint: "#8e99ab"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "steam",
|
||||||
|
name: "Steam",
|
||||||
|
comment: "Games",
|
||||||
|
recent: true,
|
||||||
|
glyph: "game",
|
||||||
|
tint: "#6f8fb8"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "photos",
|
||||||
|
name: "Photos",
|
||||||
|
comment: "Pictures and albums",
|
||||||
|
recent: false,
|
||||||
|
glyph: "photo",
|
||||||
|
tint: "#8aa8c4"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "music",
|
||||||
|
name: "Music",
|
||||||
|
comment: "Listen and organise",
|
||||||
|
recent: false,
|
||||||
|
glyph: "music",
|
||||||
|
tint: "#b48a9a"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "text-editor",
|
||||||
|
name: "Text Editor",
|
||||||
|
comment: "Edit documents",
|
||||||
|
recent: false,
|
||||||
|
glyph: "document",
|
||||||
|
tint: "#9aa7bc"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
readonly property var recents: apps.filter((app) => app.recent)
|
||||||
|
|
||||||
|
readonly property var projects: [
|
||||||
|
{
|
||||||
|
id: "nebulaos",
|
||||||
|
name: "NebulaOS",
|
||||||
|
subtitle: "Repository",
|
||||||
|
kind: "folder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "website",
|
||||||
|
name: "Website",
|
||||||
|
subtitle: "Project",
|
||||||
|
kind: "folder"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
id: "session-notes",
|
||||||
|
name: "Session notes",
|
||||||
|
subtitle: "Document",
|
||||||
|
kind: "document"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
|
||||||
|
readonly property var user: ({
|
||||||
|
name: "User",
|
||||||
|
initials: "N"
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Window
|
||||||
|
import org.kde.layershell 1.0 as LayerShell
|
||||||
|
import Nebula.UI
|
||||||
|
|
||||||
|
Window {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
title: qsTr("Nebula Desktop")
|
||||||
|
color: "transparent"
|
||||||
|
flags: Qt.FramelessWindowHint | Qt.WindowDoesNotAcceptFocus
|
||||||
|
visible: true
|
||||||
|
width: Screen.width
|
||||||
|
height: Screen.height
|
||||||
|
|
||||||
|
LayerShell.Window.scope: "nebula-desktop"
|
||||||
|
LayerShell.Window.layer: LayerShell.Window.LayerBackground
|
||||||
|
LayerShell.Window.anchors: LayerShell.Window.AnchorTop
|
||||||
|
| LayerShell.Window.AnchorBottom
|
||||||
|
| LayerShell.Window.AnchorLeft
|
||||||
|
| LayerShell.Window.AnchorRight
|
||||||
|
LayerShell.Window.exclusionZone: 0
|
||||||
|
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
|
||||||
|
|
||||||
|
Canvas {
|
||||||
|
id: backdrop
|
||||||
|
anchors.fill: parent
|
||||||
|
onPaint: {
|
||||||
|
const ctx = getContext("2d")
|
||||||
|
ctx.reset()
|
||||||
|
ctx.fillStyle = Theme.background
|
||||||
|
ctx.fillRect(0, 0, width, height)
|
||||||
|
|
||||||
|
let glow = ctx.createRadialGradient(width * 0.12, height * -0.08, 0, width * 0.12, height * -0.08, width * 0.7)
|
||||||
|
glow.addColorStop(0, "rgba(110, 140, 180, 0.16)")
|
||||||
|
glow.addColorStop(0.52, "rgba(110, 140, 180, 0)")
|
||||||
|
ctx.fillStyle = glow
|
||||||
|
ctx.fillRect(0, 0, width, height)
|
||||||
|
|
||||||
|
glow = ctx.createRadialGradient(width * 0.88, height * 1.08, 0, width * 0.88, height * 1.08, width * 0.45)
|
||||||
|
glow.addColorStop(0, "rgba(42, 68, 98, 0.28)")
|
||||||
|
glow.addColorStop(0.58, "rgba(42, 68, 98, 0)")
|
||||||
|
ctx.fillStyle = glow
|
||||||
|
ctx.fillRect(0, 0, width, height)
|
||||||
|
|
||||||
|
glow = ctx.createRadialGradient(width * 0.62, height * 0.38, 0, width * 0.62, height * 0.38, width * 0.28)
|
||||||
|
glow.addColorStop(0, "rgba(72, 92, 120, 0.08)")
|
||||||
|
glow.addColorStop(0.7, "rgba(72, 92, 120, 0)")
|
||||||
|
ctx.fillStyle = glow
|
||||||
|
ctx.fillRect(0, 0, width, height)
|
||||||
|
|
||||||
|
const veil = ctx.createLinearGradient(0, 0, 0, height)
|
||||||
|
veil.addColorStop(0, "rgba(8, 11, 17, 0.22)")
|
||||||
|
veil.addColorStop(0.16, "rgba(8, 11, 17, 0)")
|
||||||
|
veil.addColorStop(0.78, "rgba(8, 11, 17, 0)")
|
||||||
|
veil.addColorStop(1, "rgba(8, 11, 17, 0.38)")
|
||||||
|
ctx.fillStyle = veil
|
||||||
|
ctx.fillRect(0, 0, width, height)
|
||||||
|
}
|
||||||
|
|
||||||
|
onWidthChanged: requestPaint()
|
||||||
|
onHeightChanged: requestPaint()
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,75 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Window
|
||||||
|
import org.kde.layershell 1.0 as LayerShell
|
||||||
|
import Nebula.UI
|
||||||
|
|
||||||
|
Window {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
title: qsTr("Nebula Launcher")
|
||||||
|
color: "transparent"
|
||||||
|
flags: Qt.FramelessWindowHint
|
||||||
|
visible: ShellState.launcherOpen || launcher.opacity > 0.01
|
||||||
|
width: Screen.width
|
||||||
|
height: Screen.height - Theme.topBarHeight
|
||||||
|
|
||||||
|
LayerShell.Window.scope: "nebula-launcher"
|
||||||
|
LayerShell.Window.layer: LayerShell.Window.LayerOverlay
|
||||||
|
LayerShell.Window.anchors: LayerShell.Window.AnchorTop
|
||||||
|
| LayerShell.Window.AnchorBottom
|
||||||
|
| LayerShell.Window.AnchorLeft
|
||||||
|
| LayerShell.Window.AnchorRight
|
||||||
|
LayerShell.Window.margins.top: Theme.topBarHeight
|
||||||
|
LayerShell.Window.exclusionZone: 0
|
||||||
|
LayerShell.Window.keyboardInteractivity: ShellState.launcherOpen
|
||||||
|
? LayerShell.Window.KeyboardInteractivityExclusive
|
||||||
|
: LayerShell.Window.KeyboardInteractivityNone
|
||||||
|
|
||||||
|
Item {
|
||||||
|
anchors.fill: parent
|
||||||
|
focus: ShellState.launcherOpen
|
||||||
|
Keys.onEscapePressed: {
|
||||||
|
event.accepted = true
|
||||||
|
ShellState.closeLauncher()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
MouseArea {
|
||||||
|
anchors.fill: parent
|
||||||
|
acceptedButtons: Qt.LeftButton | Qt.RightButton
|
||||||
|
onClicked: ShellState.closeLauncher()
|
||||||
|
}
|
||||||
|
|
||||||
|
Launcher {
|
||||||
|
id: launcher
|
||||||
|
x: 10
|
||||||
|
y: 10
|
||||||
|
width: Math.min(420, parent.width - 20)
|
||||||
|
height: Math.min(implicitHeight, Math.min(640, parent.height - 24))
|
||||||
|
opacity: ShellState.launcherOpen ? 1 : 0
|
||||||
|
yOffset: ShellState.launcherOpen ? 0 : -6
|
||||||
|
scale: ShellState.launcherOpen ? 1 : 0.985
|
||||||
|
focus: ShellState.launcherOpen
|
||||||
|
|
||||||
|
Behavior on opacity {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.animationNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on yOffset {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.animationNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation {
|
||||||
|
duration: Theme.animationNormal
|
||||||
|
easing.type: Easing.OutCubic
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,27 @@
|
|||||||
|
import QtQuick
|
||||||
|
import QtQuick.Window
|
||||||
|
import org.kde.layershell 1.0 as LayerShell
|
||||||
|
import Nebula.UI
|
||||||
|
|
||||||
|
Window {
|
||||||
|
id: root
|
||||||
|
|
||||||
|
title: qsTr("Nebula Top Bar")
|
||||||
|
color: "transparent"
|
||||||
|
flags: Qt.FramelessWindowHint | Qt.WindowDoesNotAcceptFocus
|
||||||
|
visible: true
|
||||||
|
width: Screen.width
|
||||||
|
height: Theme.topBarHeight
|
||||||
|
|
||||||
|
LayerShell.Window.scope: "nebula-topbar"
|
||||||
|
LayerShell.Window.layer: LayerShell.Window.LayerTop
|
||||||
|
LayerShell.Window.anchors: LayerShell.Window.AnchorTop
|
||||||
|
| LayerShell.Window.AnchorLeft
|
||||||
|
| LayerShell.Window.AnchorRight
|
||||||
|
LayerShell.Window.exclusionZone: Theme.topBarHeight
|
||||||
|
LayerShell.Window.keyboardInteractivity: LayerShell.Window.KeyboardInteractivityNone
|
||||||
|
|
||||||
|
TopBar {
|
||||||
|
anchors.fill: parent
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#include <LayerShellQt/Shell>
|
||||||
|
|
||||||
|
#include <QGuiApplication>
|
||||||
|
#include <QQmlApplicationEngine>
|
||||||
|
#include <QQuickWindow>
|
||||||
|
#include <QUrl>
|
||||||
|
#include <QtGlobal>
|
||||||
|
|
||||||
|
int main(int argc, char *argv[])
|
||||||
|
{
|
||||||
|
LayerShellQt::Shell::useLayerShell();
|
||||||
|
|
||||||
|
QGuiApplication app(argc, argv);
|
||||||
|
app.setApplicationName(QStringLiteral("nebula-shell"));
|
||||||
|
app.setApplicationDisplayName(QStringLiteral("Nebula Desktop"));
|
||||||
|
app.setOrganizationName(QStringLiteral("NebulaOS"));
|
||||||
|
app.setDesktopFileName(QStringLiteral("org.nebulaos.shell"));
|
||||||
|
|
||||||
|
QQuickWindow::setDefaultAlphaBuffer(true);
|
||||||
|
|
||||||
|
QQmlApplicationEngine engine;
|
||||||
|
engine.addImportPath(QStringLiteral("qrc:/qt/qml"));
|
||||||
|
|
||||||
|
QObject::connect(
|
||||||
|
&engine,
|
||||||
|
&QQmlApplicationEngine::objectCreationFailed,
|
||||||
|
&app,
|
||||||
|
[]() {
|
||||||
|
qCritical("nebula-shell: failed to load the Nebula QML module.");
|
||||||
|
QCoreApplication::exit(EXIT_FAILURE);
|
||||||
|
},
|
||||||
|
Qt::QueuedConnection);
|
||||||
|
|
||||||
|
#if QT_VERSION >= QT_VERSION_CHECK(6, 5, 0)
|
||||||
|
engine.loadFromModule(QStringLiteral("Nebula.Shell"), QStringLiteral("Main"));
|
||||||
|
#else
|
||||||
|
engine.load(QUrl(QStringLiteral("qrc:/qt/qml/Nebula/Shell/Main.qml")));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
if (engine.rootObjects().isEmpty()) {
|
||||||
|
qCritical("nebula-shell: failed to load the Nebula QML module.");
|
||||||
|
return EXIT_FAILURE;
|
||||||
|
}
|
||||||
|
|
||||||
|
return app.exec();
|
||||||
|
}
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
# Nebula Desktop UI
|
|
||||||
|
|
||||||
React UI for Nebula Desktop shell chrome. It does not host application or game windows. Those render as Wayland / XWayland surfaces. The native host in `shells/desktop/native` will load this UI in WebKitGTK.
|
|
||||||
|
|
||||||
## Surfaces
|
|
||||||
|
|
||||||
The same codebase can render each Wayland layer on its own:
|
|
||||||
|
|
||||||
| URL | Renders |
|
|
||||||
|-----|---------|
|
|
||||||
| `/?surface=preview` (default) | Combined browser mockup |
|
|
||||||
| `/?surface=desktop` | Background layer only |
|
|
||||||
| `/?surface=topbar` | Top bar only |
|
|
||||||
| `/?surface=launcher` | Launcher only |
|
|
||||||
|
|
||||||
The native host chooses the origin. Surface selection is a relative query parameter, not a hardcoded port.
|
|
||||||
|
|
||||||
## Develop
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
npm install
|
|
||||||
npm run dev
|
|
||||||
```
|
|
||||||
|
|
||||||
```powershell
|
|
||||||
npm run lint
|
|
||||||
npm run build
|
|
||||||
```
|
|
||||||
|
|
||||||
Oxlint is the linter for this project.
|
|
||||||
Reference in New Issue
Block a user