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
@@ -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()
}
}