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:
2026-08-26 16:34:40 +12:00
parent 5504d3cfb8
commit befed8ff96
80 changed files with 2128 additions and 609 deletions
+30 -4
View File
@@ -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
View File
@@ -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
```
View File
+41
View File
@@ -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}
)
+20
View File
@@ -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"
}
+2 -1
View File
@@ -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
+178
View File
@@ -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.
-32
View File
@@ -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()
-146
View File
@@ -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_;
};
-115
View File
@@ -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_);
}
-40
View File
@@ -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);
};
-7
View File
@@ -1,7 +0,0 @@
#include "ShellApplication.hpp"
int main(int argc, char* argv[])
{
ShellApplication application;
return application.run(argc, argv);
}
+48
View File
@@ -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

@@ -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.
@@ -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.
@@ -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 = {
@@ -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();
+98
View File
@@ -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()
+16
View File
@@ -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()
}
}
+25
View File
@@ -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
}
}
+100
View File
@@ -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
}
}
+46
View File
@@ -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();
}
-30
View File
@@ -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.