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,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();
|
||||
}
|
||||
Reference in New Issue
Block a user