First iteration of vertical bar

This commit is contained in:
Ly-sec 2025-09-13 14:26:20 +02:00
parent 25ba27cbdd
commit 4f5acb7114
7 changed files with 744 additions and 128 deletions

View file

@ -263,7 +263,7 @@ Singleton {
// bar // bar
property JsonObject bar: JsonObject { property JsonObject bar: JsonObject {
property string position: "top" // "top" or "bottom" property string position: "top" // "top", "bottom", "left", or "right"
property real backgroundOpacity: 1.0 property real backgroundOpacity: 1.0
property list<string> monitors: [] property list<string> monitors: []

View file

@ -34,14 +34,15 @@ Variants {
WlrLayershell.namespace: "noctalia-bar" WlrLayershell.namespace: "noctalia-bar"
implicitHeight: Math.round(Style.barHeight * scaling) implicitHeight: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? screen.height : Math.round(Style.barHeight * scaling)
implicitWidth: (Settings.data.bar.position === "left" || Settings.data.bar.position === "right") ? Math.round(Style.barHeight * scaling) : screen.width
color: Color.transparent color: Color.transparent
anchors { anchors {
top: Settings.data.bar.position === "top" top: Settings.data.bar.position === "top" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
bottom: Settings.data.bar.position === "bottom" bottom: Settings.data.bar.position === "bottom" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
left: true left: Settings.data.bar.position === "left" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
right: true right: Settings.data.bar.position === "right" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
} }
// Floating bar margins - only apply when floating is enabled // Floating bar margins - only apply when floating is enabled
@ -67,91 +68,197 @@ Variants {
radius: Settings.data.bar.floating ? Settings.data.bar.rounding : 0 radius: Settings.data.bar.floating ? Settings.data.bar.rounding : 0
} }
// ------------------------------ // For vertical bars, use a single column layout
// Left Section - Dynamic Widgets Loader {
Row { id: verticalBarLayout
id: leftSection anchors.fill: parent
objectName: "leftSection" visible: Settings.data.bar.position === "left" || Settings.data.bar.position === "right"
sourceComponent: verticalBarComponent
}
height: parent.height // For horizontal bars, use the original three-section layout
anchors.left: parent.left Loader {
anchors.leftMargin: Style.marginS * scaling id: horizontalBarLayout
anchors.verticalCenter: parent.verticalCenter anchors.fill: parent
spacing: Style.marginS * scaling visible: Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom"
sourceComponent: horizontalBarComponent
}
Repeater { // Main layout components
model: Settings.data.bar.widgets.left Component {
delegate: NWidgetLoader { id: verticalBarComponent
widgetId: (modelData.id !== undefined ? modelData.id : "") Item {
widgetProps: { anchors.fill: parent
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen), // Top section (left widgets)
"widgetId": modelData.id, Column {
"section": parent.objectName.replace("Section", "").toLowerCase(), spacing: Style.marginS * root.scaling
"sectionWidgetIndex": index, anchors.horizontalCenter: parent.horizontalCenter
"sectionWidgetsCount": Settings.data.bar.widgets.left.length anchors.top: parent.top
anchors.topMargin: Style.marginM * root.scaling
width: parent.width
Repeater {
model: Settings.data.bar.widgets.left
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "left",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length,
"barPosition": Settings.data.bar.position
}
anchors.horizontalCenter: parent.horizontalCenter
}
} }
}
// Center section (center widgets)
Column {
spacing: Style.marginS * root.scaling
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
width: parent.width
Repeater {
model: Settings.data.bar.widgets.center
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "center",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length,
"barPosition": Settings.data.bar.position
}
anchors.horizontalCenter: parent.horizontalCenter
}
}
}
// Bottom section (right widgets)
Column {
spacing: Style.marginS * root.scaling
anchors.horizontalCenter: parent.horizontalCenter
anchors.bottom: parent.bottom
anchors.bottomMargin: Style.marginM * root.scaling
width: parent.width
Repeater {
model: Settings.data.bar.widgets.right
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "right",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length,
"barPosition": Settings.data.bar.position
}
anchors.horizontalCenter: parent.horizontalCenter
}
}
} }
} }
} }
// ------------------------------ Component {
// Center Section - Dynamic Widgets id: horizontalBarComponent
Row { Row {
id: centerSection anchors.fill: parent
objectName: "centerSection"
// Left Section
height: parent.height Row {
anchors.horizontalCenter: parent.horizontalCenter id: leftSection
anchors.verticalCenter: parent.verticalCenter objectName: "leftSection"
spacing: Style.marginS * scaling height: parent.height
anchors.left: parent.left
Repeater { anchors.leftMargin: Style.marginS * root.scaling
model: Settings.data.bar.widgets.center
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": parent.objectName.replace("Section", "").toLowerCase(),
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length
}
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * root.scaling
Repeater {
model: Settings.data.bar.widgets.left
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "left",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.left.length,
"barPosition": Settings.data.bar.position
}
anchors.verticalCenter: parent.verticalCenter
}
}
}
// Center Section
Row {
id: centerSection
objectName: "centerSection"
height: parent.height
anchors.horizontalCenter: parent.horizontalCenter
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * root.scaling
Repeater {
model: Settings.data.bar.widgets.center
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "center",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.center.length,
"barPosition": Settings.data.bar.position
}
anchors.verticalCenter: parent.verticalCenter
}
}
}
// Right Section
Row {
id: rightSection
objectName: "rightSection"
height: parent.height
anchors.right: parent.right
anchors.rightMargin: Style.marginS * root.scaling
anchors.verticalCenter: parent.verticalCenter
spacing: Style.marginS * root.scaling
Repeater {
model: Settings.data.bar.widgets.right
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": "right",
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length,
"barPosition": Settings.data.bar.position
}
anchors.verticalCenter: parent.verticalCenter
}
}
} }
} }
} }
// ------------------------------
// Right Section - Dynamic Widgets
Row {
id: rightSection
objectName: "rightSection"
height: parent.height
anchors.right: bar.right
anchors.rightMargin: Style.marginS * scaling
anchors.verticalCenter: bar.verticalCenter
spacing: Style.marginS * scaling
Repeater {
model: Settings.data.bar.widgets.right
delegate: NWidgetLoader {
widgetId: (modelData.id !== undefined ? modelData.id : "")
widgetProps: {
"screen": root.modelData || null,
"scaling": ScalingService.getScreenScale(screen),
"widgetId": modelData.id,
"section": parent.objectName.replace("Section", "").toLowerCase(),
"sectionWidgetIndex": index,
"sectionWidgetsCount": Settings.data.bar.widgets.right.length
}
anchors.verticalCenter: parent.verticalCenter
}
}
}
} }
} }
} }

View file

@ -8,7 +8,7 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
RowLayout { Item {
id: root id: root
property ShellScreen screen property ShellScreen screen
property real scaling: 1.0 property real scaling: 1.0
@ -18,6 +18,7 @@ RowLayout {
property string section: "" property string section: ""
property int sectionWidgetIndex: -1 property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0 property int sectionWidgetsCount: 0
property string barPosition: "top"
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: { property var widgetSettings: {
@ -36,6 +37,9 @@ RowLayout {
readonly property real minWidth: Math.max(1, screen.width * 0.06) readonly property real minWidth: Math.max(1, screen.width * 0.06)
readonly property real maxWidth: minWidth * 2 readonly property real maxWidth: minWidth * 2
implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : calculatedHorizontalWidth()
function getTitle() { function getTitle() {
try { try {
return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : "" return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : ""
@ -45,10 +49,25 @@ RowLayout {
} }
} }
Layout.alignment: Qt.AlignVCenter
spacing: Style.marginS * scaling
visible: getTitle() !== "" visible: getTitle() !== ""
function calculatedVerticalHeight() {
let total = Math.round(Style.capsuleHeight * scaling)
if (showIcon) {
total += Style.fontSizeL * scaling * 1.2 + Style.marginS * scaling
}
return total
}
function calculatedHorizontalWidth() {
let total = Style.marginM * 2 * scaling // padding
if (showIcon) {
total += Style.fontSizeL * scaling * 1.2 + Style.marginS * scaling
}
total += Math.min(fullTitleMetrics.contentWidth, minWidth * scaling)
return total
}
function getAppIcon() { function getAppIcon() {
try { try {
// Try CompositorService first // Try CompositorService first
@ -102,8 +121,9 @@ RowLayout {
Rectangle { Rectangle {
id: windowTitleRect id: windowTitleRect
visible: root.visible visible: root.visible
Layout.preferredWidth: contentLayout.implicitWidth + Style.marginM * 2 * scaling anchors.centerIn: parent
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) width: (barPosition === "left" || barPosition === "right") ? Math.round(60 * scaling) : parent.width
height: (barPosition === "left" || barPosition === "right") ? parent.height : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling) radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant color: Color.mSurfaceVariant
@ -114,10 +134,12 @@ RowLayout {
anchors.rightMargin: Style.marginS * scaling anchors.rightMargin: Style.marginS * scaling
clip: true clip: true
// Horizontal layout for top/bottom bars
RowLayout { RowLayout {
id: contentLayout id: horizontalLayout
anchors.centerIn: parent anchors.centerIn: parent
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
visible: barPosition === "top" || barPosition === "bottom"
// Window icon // Window icon
Item { Item {
@ -176,12 +198,66 @@ RowLayout {
} }
} }
// Vertical layout for left/right bars - icon only
Item {
id: verticalLayout
anchors.centerIn: parent
width: parent.width - Style.marginM * scaling * 2
height: parent.height - Style.marginM * scaling * 2
visible: barPosition === "left" || barPosition === "right"
// Window icon
Item {
width: Style.fontSizeL * scaling * 1.2
height: Style.fontSizeL * scaling * 1.2
anchors.centerIn: parent
visible: getTitle() !== "" && showIcon
IconImage {
id: windowIconVertical
anchors.fill: parent
source: getAppIcon()
asynchronous: true
smooth: true
visible: source !== ""
// Handle loading errors gracefully
onStatusChanged: {
if (status === Image.Error) {
Logger.warn("ActiveWindow", "Failed to load icon:", source)
}
}
}
}
}
// Mouse area for hover detection // Mouse area for hover detection
MouseArea { MouseArea {
id: mouseArea id: mouseArea
anchors.fill: parent anchors.fill: parent
hoverEnabled: true hoverEnabled: true
cursorShape: Qt.PointingHandCursor cursorShape: Qt.PointingHandCursor
onEntered: {
if (barPosition === "left" || barPosition === "right") {
tooltip.show()
}
}
onExited: {
if (barPosition === "left" || barPosition === "right") {
tooltip.hide()
}
}
}
// Hover tooltip with full title (only for vertical bars)
NTooltip {
id: tooltip
target: verticalLayout
text: getTitle()
positionLeft: barPosition === "right"
positionRight: barPosition === "left"
delay: 500
} }
} }
} }
@ -191,6 +267,7 @@ RowLayout {
function onActiveWindowChanged() { function onActiveWindowChanged() {
try { try {
windowIcon.source = Qt.binding(getAppIcon) windowIcon.source = Qt.binding(getAppIcon)
windowIconVertical.source = Qt.binding(getAppIcon)
} catch (e) { } catch (e) {
Logger.warn("ActiveWindow", "Error in onActiveWindowChanged:", e) Logger.warn("ActiveWindow", "Error in onActiveWindowChanged:", e)
} }
@ -198,6 +275,7 @@ RowLayout {
function onWindowListChanged() { function onWindowListChanged() {
try { try {
windowIcon.source = Qt.binding(getAppIcon) windowIcon.source = Qt.binding(getAppIcon)
windowIconVertical.source = Qt.binding(getAppIcon)
} catch (e) { } catch (e) {
Logger.warn("ActiveWindow", "Error in onWindowListChanged:", e) Logger.warn("ActiveWindow", "Error in onWindowListChanged:", e)
} }

View file

@ -5,7 +5,7 @@ import qs.Commons
import qs.Services import qs.Services
import qs.Widgets import qs.Widgets
RowLayout { Item {
id: root id: root
property ShellScreen screen property ShellScreen screen
@ -16,6 +16,7 @@ RowLayout {
property string section: "" property string section: ""
property int sectionWidgetIndex: -1 property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0 property int sectionWidgetsCount: 0
property string barPosition: "top"
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: { property var widgetSettings: {
@ -35,22 +36,60 @@ RowLayout {
readonly property bool showNetworkStats: (widgetSettings.showNetworkStats !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats readonly property bool showNetworkStats: (widgetSettings.showNetworkStats !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats
readonly property bool showDiskUsage: (widgetSettings.showDiskUsage !== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage readonly property bool showDiskUsage: (widgetSettings.showDiskUsage !== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage
Layout.alignment: Qt.AlignVCenter implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
spacing: Style.marginS * scaling implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : calculatedHorizontalWidth()
function calculatedVerticalHeight() {
let total = 0
let visibleCount = 0
if (showCpuUsage) visibleCount++
if (showCpuTemp) visibleCount++
if (showMemoryUsage) visibleCount++
if (showNetworkStats) visibleCount += 2 // download + upload
if (showDiskUsage) visibleCount++
total = visibleCount * Math.round(Style.capsuleHeight * scaling)
total += Math.max(visibleCount - 1, 0) * Style.marginS * scaling
total += Style.marginM * scaling * 2 // padding
return total
}
function calculatedHorizontalWidth() {
let total = 0
let visibleCount = 0
if (showCpuUsage) visibleCount++
if (showCpuTemp) visibleCount++
if (showMemoryUsage) visibleCount++
if (showNetworkStats) visibleCount += 2 // download + upload
if (showDiskUsage) visibleCount++
// Estimate width per component (icon + text + spacing)
total = visibleCount * Math.round(60 * scaling) // rough estimate
total += Math.max(visibleCount - 1, 0) * Style.marginS * scaling
total += Style.marginM * scaling * 2 // padding
return total
}
Rectangle { Rectangle {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) id: backgroundContainer
Layout.preferredWidth: mainLayout.implicitWidth + Style.marginM * scaling * 2 anchors.centerIn: parent
Layout.alignment: Qt.AlignVCenter width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : parent.width
height: (barPosition === "left" || barPosition === "right") ? parent.height : Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling) radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant color: Color.mSurfaceVariant
// Horizontal layout for top/bottom bars
RowLayout { RowLayout {
id: mainLayout id: horizontalLayout
anchors.centerIn: parent // Better centering than margins anchors.centerIn: parent
width: parent.width - Style.marginM * scaling * 2 width: parent.width - Style.marginM * scaling * 2
height: parent.height - Style.marginM * scaling * 2
spacing: Style.marginS * scaling spacing: Style.marginS * scaling
visible: false // Temporarily hide horizontal layout for debugging
// CPU Usage Component // CPU Usage Component
Item { Item {
@ -233,5 +272,196 @@ RowLayout {
} }
} }
} }
// Vertical layout for left/right bars
ColumnLayout {
id: verticalLayout
anchors.centerIn: parent
width: Math.round(32 * scaling)
height: parent.height - Style.marginM * scaling * 2
spacing: Style.marginS * scaling
visible: true // Temporarily show vertical layout for debugging
// CPU Usage Component
Item {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: Math.round(32 * scaling)
Layout.alignment: Qt.AlignHCenter
visible: showCpuUsage
Column {
id: cpuUsageRowVertical
anchors.centerIn: parent
spacing: Style.marginXXS * scaling
NIcon {
icon: "cpu-usage"
font.pointSize: Style.fontSizeS * scaling
anchors.horizontalCenter: parent.horizontalCenter
}
NText {
text: `${SystemStatService.cpuUsage}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling * 0.8
font.weight: Style.fontWeightMedium
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
color: Color.mPrimary
}
}
}
// CPU Temperature Component
Item {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: Math.round(32 * scaling)
Layout.alignment: Qt.AlignHCenter
visible: showCpuTemp
Column {
id: cpuTempRowVertical
anchors.centerIn: parent
spacing: Style.marginXXS * scaling
NIcon {
icon: "cpu-temperature"
// Fire is so tall, we need to make it smaller
font.pointSize: Style.fontSizeXS * scaling
anchors.horizontalCenter: parent.horizontalCenter
}
NText {
text: `${SystemStatService.cpuTemp}°C`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling * 0.8
font.weight: Style.fontWeightMedium
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
color: Color.mPrimary
}
}
}
// Memory Usage Component
Item {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: Math.round(32 * scaling)
Layout.alignment: Qt.AlignHCenter
visible: showMemoryUsage
Column {
id: memoryUsageRowVertical
anchors.centerIn: parent
spacing: Style.marginXXS * scaling
NIcon {
icon: "memory"
font.pointSize: Style.fontSizeS * scaling
anchors.horizontalCenter: parent.horizontalCenter
}
NText {
text: showMemoryAsPercent ? `${SystemStatService.memPercent}%` : `${SystemStatService.memGb}G`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling * 0.8
font.weight: Style.fontWeightMedium
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
color: Color.mPrimary
}
}
}
// Network Download Speed Component
Item {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: Math.round(32 * scaling)
Layout.alignment: Qt.AlignHCenter
visible: showNetworkStats
Column {
id: networkDownloadRowVertical
anchors.centerIn: parent
spacing: Style.marginXXS * scaling
NIcon {
icon: "download-speed"
font.pointSize: Style.fontSizeS * scaling
anchors.horizontalCenter: parent.horizontalCenter
}
NText {
text: SystemStatService.formatSpeed(SystemStatService.rxSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling * 0.8
font.weight: Style.fontWeightMedium
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
color: Color.mPrimary
}
}
}
// Network Upload Speed Component
Item {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: Math.round(32 * scaling)
Layout.alignment: Qt.AlignHCenter
visible: showNetworkStats
Column {
id: networkUploadRowVertical
anchors.centerIn: parent
spacing: Style.marginXXS * scaling
NIcon {
icon: "upload-speed"
font.pointSize: Style.fontSizeS * scaling
anchors.horizontalCenter: parent.horizontalCenter
}
NText {
text: SystemStatService.formatSpeed(SystemStatService.txSpeed)
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling * 0.8
font.weight: Style.fontWeightMedium
anchors.horizontalCenter: parent.horizontalCenter
horizontalAlignment: Text.AlignHCenter
color: Color.mPrimary
}
}
}
// Disk Usage Component (primary drive)
Item {
Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling)
Layout.preferredWidth: Math.round(32 * scaling)
Layout.alignment: Qt.AlignHCenter
visible: showDiskUsage
ColumnLayout {
id: diskUsageRowVertical
anchors.centerIn: parent
spacing: Style.marginXXS * scaling
NIcon {
icon: "storage"
font.pointSize: Style.fontSizeS * scaling
Layout.alignment: Qt.AlignHCenter
}
NText {
text: `${SystemStatService.diskPercent}%`
font.family: Settings.data.ui.fontFixed
font.pointSize: Style.fontSizeXXS * scaling * 0.8
font.weight: Style.fontWeightMedium
Layout.alignment: Qt.AlignHCenter
horizontalAlignment: Text.AlignHCenter
color: Color.mPrimary
}
}
}
}
} }
} }

View file

@ -19,6 +19,7 @@ Item {
property string section: "" property string section: ""
property int sectionWidgetIndex: -1 property int sectionWidgetIndex: -1
property int sectionWidgetsCount: 0 property int sectionWidgetsCount: 0
property string barPosition: "top"
property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId]
property var widgetSettings: { property var widgetSettings: {
@ -47,17 +48,8 @@ Item {
signal workspaceChanged(int workspaceId, color accentColor) signal workspaceChanged(int workspaceId, color accentColor)
implicitHeight: Math.round(Style.barHeight * scaling) implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling)
implicitWidth: { implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.barHeight * scaling) : calculatedHorizontalWidth()
let total = 0
for (var i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i)
total += calculatedWsWidth(ws)
}
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
total += horizontalPadding * 2
return total
}
function calculatedWsWidth(ws) { function calculatedWsWidth(ws) {
if (ws.isFocused) if (ws.isFocused)
@ -68,6 +60,37 @@ Item {
return Math.round(20 * scaling) return Math.round(20 * scaling)
} }
function calculatedWsHeight(ws) {
if (ws.isFocused)
return Math.round(44 * scaling)
else if (ws.isActive)
return Math.round(28 * scaling)
else
return Math.round(20 * scaling)
}
function calculatedVerticalHeight() {
let total = 0
for (var i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i)
total += calculatedWsHeight(ws)
}
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
total += horizontalPadding * 2
return total
}
function calculatedHorizontalWidth() {
let total = 0
for (var i = 0; i < localWorkspaces.count; i++) {
const ws = localWorkspaces.get(i)
total += calculatedWsWidth(ws)
}
total += Math.max(localWorkspaces.count - 1, 0) * spacingBetweenPills
total += horizontalPadding * 2
return total
}
Component.onCompleted: { Component.onCompleted: {
refreshWorkspaces() refreshWorkspaces()
} }
@ -99,7 +122,8 @@ Item {
} }
} }
} }
workspaceRepeater.model = localWorkspaces workspaceRepeaterHorizontal.model = localWorkspaces
workspaceRepeaterVertical.model = localWorkspaces
updateWorkspaceFocus() updateWorkspaceFocus()
} }
@ -148,9 +172,8 @@ Item {
Rectangle { Rectangle {
id: workspaceBackground id: workspaceBackground
width: parent.width width: (barPosition === "left" || barPosition === "right") ? Math.round(Style.capsuleHeight * scaling) : parent.width
height: (barPosition === "left" || barPosition === "right") ? parent.height : Math.round(Style.capsuleHeight * scaling)
height: Math.round(Style.capsuleHeight * scaling)
radius: Math.round(Style.radiusM * scaling) radius: Math.round(Style.radiusM * scaling)
color: Color.mSurfaceVariant color: Color.mSurfaceVariant
@ -158,14 +181,17 @@ Item {
anchors.verticalCenter: parent.verticalCenter anchors.verticalCenter: parent.verticalCenter
} }
// Horizontal layout for top/bottom bars
Row { Row {
id: pillRow id: pillRow
spacing: spacingBetweenPills spacing: spacingBetweenPills
anchors.verticalCenter: workspaceBackground.verticalCenter anchors.verticalCenter: workspaceBackground.verticalCenter
width: root.width - horizontalPadding * 2 width: root.width - horizontalPadding * 2
x: horizontalPadding x: horizontalPadding
visible: barPosition === "top" || barPosition === "bottom"
Repeater { Repeater {
id: workspaceRepeater id: workspaceRepeaterHorizontal
model: localWorkspaces model: localWorkspaces
Item { Item {
id: workspacePillContainer id: workspacePillContainer
@ -299,4 +325,149 @@ Item {
} }
} }
} }
// Vertical layout for left/right bars
Column {
id: pillColumn
spacing: spacingBetweenPills
anchors.horizontalCenter: workspaceBackground.horizontalCenter
height: root.height - horizontalPadding * 2
y: horizontalPadding
visible: barPosition === "left" || barPosition === "right"
Repeater {
id: workspaceRepeaterVertical
model: localWorkspaces
Item {
id: workspacePillContainerVertical
width: (labelMode !== "none") ? Math.round(18 * scaling) : Math.round(14 * scaling)
height: root.calculatedWsHeight(model)
Rectangle {
id: pillVertical
anchors.fill: parent
Loader {
active: (labelMode !== "none")
sourceComponent: Component {
Text {
x: (pillVertical.width - width) / 2
y: (pillVertical.height - height) / 2 + (height - contentHeight) / 2
text: {
if (labelMode === "name" && model.name && model.name.length > 0) {
return model.name.substring(0, 2)
} else {
return model.idx.toString()
}
}
font.pointSize: model.isFocused ? Style.fontSizeXS * scaling : Style.fontSizeXXS * scaling
font.capitalization: Font.AllUppercase
font.family: Settings.data.ui.fontFixed
font.weight: Style.fontWeightBold
wrapMode: Text.Wrap
color: {
if (model.isFocused)
return Color.mOnPrimary
if (model.isUrgent)
return Color.mOnError
if (model.isActive || model.isOccupied)
return Color.mOnSecondary
return Color.mOnSurface
}
}
}
}
radius: width * 0.5
color: {
if (model.isFocused)
return Color.mPrimary
if (model.isUrgent)
return Color.mError
if (model.isActive || model.isOccupied)
return Color.mSecondary
return Color.mOutline
}
scale: model.isFocused ? 1.0 : 0.9
z: 0
MouseArea {
id: pillMouseAreaVertical
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
onClicked: {
WorkspaceService.switchToWorkspace(model.idx)
}
hoverEnabled: true
}
// Material 3-inspired smooth animation for width, height, scale, color, opacity, and radius
Behavior on width {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on height {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on scale {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on color {
ColorAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
}
}
Behavior on opacity {
NumberAnimation {
duration: Style.animationFast
easing.type: Easing.InOutCubic
}
}
Behavior on radius {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
}
Behavior on width {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
Behavior on height {
NumberAnimation {
duration: Style.animationNormal
easing.type: Easing.OutBack
}
}
// Burst effect overlay for focused pill (smaller outline)
Rectangle {
id: pillBurstVertical
anchors.centerIn: workspacePillContainerVertical
width: workspacePillContainerVertical.width + 18 * root.masterProgress * scale
height: workspacePillContainerVertical.height + 18 * root.masterProgress * scale
radius: width / 2
color: Color.transparent
border.color: root.effectColor
border.width: Math.max(1, Math.round((2 + 6 * (1.0 - root.masterProgress)) * scaling))
opacity: root.effectsActive && model.isFocused ? (1.0 - root.masterProgress) * 0.7 : 0
visible: root.effectsActive && model.isFocused
z: 1
}
}
}
}
} }

View file

@ -42,6 +42,14 @@ ColumnLayout {
key: "bottom" key: "bottom"
name: "Bottom" name: "Bottom"
} }
ListElement {
key: "left"
name: "Left"
}
ListElement {
key: "right"
name: "Right"
}
} }
currentKey: Settings.data.bar.position currentKey: Settings.data.bar.position
onSelected: key => Settings.data.bar.position = key onSelected: key => Settings.data.bar.position = key

View file

@ -1,6 +1,5 @@
{ {
description = description = "Noctalia shell - a Wayland desktop shell built with Quickshell";
"Noctalia shell - a Wayland desktop shell built with Quickshell";
inputs = { inputs = {
nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable";
@ -12,13 +11,22 @@
}; };
}; };
outputs = { self, nixpkgs, systems, quickshell, ... }: outputs =
let eachSystem = nixpkgs.lib.genAttrs (import systems); {
in { self,
formatter = nixpkgs,
eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra); systems,
quickshell,
...
}:
let
eachSystem = nixpkgs.lib.genAttrs (import systems);
in
{
formatter = eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra);
packages = eachSystem (system: packages = eachSystem (
system:
let let
pkgs = nixpkgs.legacyPackages.${system}; pkgs = nixpkgs.legacyPackages.${system};
qs = quickshell.packages.${system}.default.override { qs = quickshell.packages.${system}.default.override {
@ -26,7 +34,8 @@
withI3 = false; withI3 = false;
}; };
runtimeDeps = with pkgs; runtimeDeps =
with pkgs;
[ [
bash bash
bluez bluez
@ -41,21 +50,34 @@
matugen matugen
networkmanager networkmanager
wl-clipboard wl-clipboard
] ++ lib.optionals (pkgs.stdenv.hostPlatform.isx86_64) ]
[ gpu-screen-recorder ]; ++ lib.optionals (pkgs.stdenv.hostPlatform.isx86_64) [
gpu-screen-recorder
];
fontconfig = pkgs.makeFontsConf { fontconfig = pkgs.makeFontsConf {
fontDirectories = [ pkgs.roboto pkgs.inter-nerdfont ]; fontDirectories = [
pkgs.roboto
pkgs.inter-nerdfont
];
}; };
in { in
{
default = pkgs.stdenv.mkDerivation { default = pkgs.stdenv.mkDerivation {
pname = "noctalia-shell"; pname = "noctalia-shell";
version = self.rev or self.dirtyRev or "dirty"; version = self.rev or self.dirtyRev or "dirty";
src = ./.; src = ./.;
nativeBuildInputs = nativeBuildInputs = [
[ pkgs.gcc pkgs.makeWrapper pkgs.qt6.wrapQtAppsHook ]; pkgs.gcc
buildInputs = [ qs pkgs.xkeyboard_config pkgs.qt6.qtbase ]; pkgs.makeWrapper
pkgs.qt6.wrapQtAppsHook
];
buildInputs = [
qs
pkgs.xkeyboard-config
pkgs.qt6.qtbase
];
propagatedBuildInputs = runtimeDeps; propagatedBuildInputs = runtimeDeps;
installPhase = '' installPhase = ''
@ -69,14 +91,14 @@
''; '';
meta = { meta = {
description = description = "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell.";
"A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell.";
homepage = "https://github.com/noctalia-dev/noctalia-shell"; homepage = "https://github.com/noctalia-dev/noctalia-shell";
license = pkgs.lib.licenses.mit; license = pkgs.lib.licenses.mit;
mainProgram = "noctalia-shell"; mainProgram = "noctalia-shell";
}; };
}; };
}); }
);
defaultPackage = eachSystem (system: self.packages.${system}.default); defaultPackage = eachSystem (system: self.packages.${system}.default);
}; };