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