diff --git a/Commons/Settings.qml b/Commons/Settings.qml index faf0a87..76ddc9e 100644 --- a/Commons/Settings.qml +++ b/Commons/Settings.qml @@ -263,7 +263,7 @@ Singleton { // bar 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 list monitors: [] diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index e7abf78..a42abb0 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -34,14 +34,15 @@ Variants { 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 anchors { - top: Settings.data.bar.position === "top" - bottom: Settings.data.bar.position === "bottom" - left: true - right: true + top: Settings.data.bar.position === "top" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + bottom: Settings.data.bar.position === "bottom" || Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + left: Settings.data.bar.position === "left" || Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom" + 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 @@ -67,91 +68,197 @@ Variants { radius: Settings.data.bar.floating ? Settings.data.bar.rounding : 0 } - // ------------------------------ - // Left Section - Dynamic Widgets - Row { - id: leftSection - objectName: "leftSection" + // For vertical bars, use a single column layout + Loader { + id: verticalBarLayout + anchors.fill: parent + visible: Settings.data.bar.position === "left" || Settings.data.bar.position === "right" + sourceComponent: verticalBarComponent + } - height: parent.height - anchors.left: parent.left - anchors.leftMargin: Style.marginS * scaling - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginS * scaling + // For horizontal bars, use the original three-section layout + Loader { + id: horizontalBarLayout + anchors.fill: parent + visible: Settings.data.bar.position === "top" || Settings.data.bar.position === "bottom" + sourceComponent: horizontalBarComponent + } - 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": parent.objectName.replace("Section", "").toLowerCase(), - "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.bar.widgets.left.length + // Main layout components + Component { + id: verticalBarComponent + Item { + anchors.fill: parent + + // Top section (left widgets) + Column { + spacing: Style.marginS * root.scaling + anchors.horizontalCenter: parent.horizontalCenter + 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 + 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 + } + } } } } - // ------------------------------ - // Center Section - Dynamic Widgets - Row { - id: centerSection - objectName: "centerSection" - - height: parent.height - anchors.horizontalCenter: parent.horizontalCenter - anchors.verticalCenter: parent.verticalCenter - spacing: Style.marginS * 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": parent.objectName.replace("Section", "").toLowerCase(), - "sectionWidgetIndex": index, - "sectionWidgetsCount": Settings.data.bar.widgets.center.length - } + Component { + id: horizontalBarComponent + Row { + anchors.fill: parent + + // Left Section + Row { + id: leftSection + objectName: "leftSection" + height: parent.height + anchors.left: parent.left + anchors.leftMargin: Style.marginS * root.scaling 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 - } - } - } } } } diff --git a/Modules/Bar/Widgets/ActiveWindow.qml b/Modules/Bar/Widgets/ActiveWindow.qml index d3fb762..eb77fa0 100644 --- a/Modules/Bar/Widgets/ActiveWindow.qml +++ b/Modules/Bar/Widgets/ActiveWindow.qml @@ -8,7 +8,7 @@ import qs.Commons import qs.Services import qs.Widgets -RowLayout { +Item { id: root property ShellScreen screen property real scaling: 1.0 @@ -18,6 +18,7 @@ RowLayout { property string section: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 + property string barPosition: "top" property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { @@ -36,6 +37,9 @@ RowLayout { readonly property real minWidth: Math.max(1, screen.width * 0.06) 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() { try { return CompositorService.focusedWindowTitle !== "(No active window)" ? CompositorService.focusedWindowTitle : "" @@ -45,10 +49,25 @@ RowLayout { } } - Layout.alignment: Qt.AlignVCenter - spacing: Style.marginS * scaling 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() { try { // Try CompositorService first @@ -102,8 +121,9 @@ RowLayout { Rectangle { id: windowTitleRect visible: root.visible - Layout.preferredWidth: contentLayout.implicitWidth + Style.marginM * 2 * scaling - Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) + anchors.centerIn: parent + 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) color: Color.mSurfaceVariant @@ -114,10 +134,12 @@ RowLayout { anchors.rightMargin: Style.marginS * scaling clip: true + // Horizontal layout for top/bottom bars RowLayout { - id: contentLayout + id: horizontalLayout anchors.centerIn: parent spacing: Style.marginS * scaling + visible: barPosition === "top" || barPosition === "bottom" // Window icon 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 MouseArea { id: mouseArea anchors.fill: parent hoverEnabled: true 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() { try { windowIcon.source = Qt.binding(getAppIcon) + windowIconVertical.source = Qt.binding(getAppIcon) } catch (e) { Logger.warn("ActiveWindow", "Error in onActiveWindowChanged:", e) } @@ -198,6 +275,7 @@ RowLayout { function onWindowListChanged() { try { windowIcon.source = Qt.binding(getAppIcon) + windowIconVertical.source = Qt.binding(getAppIcon) } catch (e) { Logger.warn("ActiveWindow", "Error in onWindowListChanged:", e) } diff --git a/Modules/Bar/Widgets/SystemMonitor.qml b/Modules/Bar/Widgets/SystemMonitor.qml index e902e75..ef28e30 100644 --- a/Modules/Bar/Widgets/SystemMonitor.qml +++ b/Modules/Bar/Widgets/SystemMonitor.qml @@ -5,7 +5,7 @@ import qs.Commons import qs.Services import qs.Widgets -RowLayout { +Item { id: root property ShellScreen screen @@ -16,6 +16,7 @@ RowLayout { property string section: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 + property string barPosition: "top" property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { @@ -35,22 +36,60 @@ RowLayout { readonly property bool showNetworkStats: (widgetSettings.showNetworkStats !== undefined) ? widgetSettings.showNetworkStats : widgetMetadata.showNetworkStats readonly property bool showDiskUsage: (widgetSettings.showDiskUsage !== undefined) ? widgetSettings.showDiskUsage : widgetMetadata.showDiskUsage - Layout.alignment: Qt.AlignVCenter - spacing: Style.marginS * scaling + implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * 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 { - Layout.preferredHeight: Math.round(Style.capsuleHeight * scaling) - Layout.preferredWidth: mainLayout.implicitWidth + Style.marginM * scaling * 2 - Layout.alignment: Qt.AlignVCenter - + id: backgroundContainer + anchors.centerIn: parent + 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) color: Color.mSurfaceVariant + // Horizontal layout for top/bottom bars RowLayout { - id: mainLayout - anchors.centerIn: parent // Better centering than margins + id: horizontalLayout + anchors.centerIn: parent width: parent.width - Style.marginM * scaling * 2 + height: parent.height - Style.marginM * scaling * 2 spacing: Style.marginS * scaling + visible: false // Temporarily hide horizontal layout for debugging // CPU Usage Component 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 + } + } + } + } } } diff --git a/Modules/Bar/Widgets/Workspace.qml b/Modules/Bar/Widgets/Workspace.qml index 562994c..63f6c9c 100644 --- a/Modules/Bar/Widgets/Workspace.qml +++ b/Modules/Bar/Widgets/Workspace.qml @@ -19,6 +19,7 @@ Item { property string section: "" property int sectionWidgetIndex: -1 property int sectionWidgetsCount: 0 + property string barPosition: "top" property var widgetMetadata: BarWidgetRegistry.widgetMetadata[widgetId] property var widgetSettings: { @@ -47,17 +48,8 @@ Item { signal workspaceChanged(int workspaceId, color accentColor) - implicitHeight: Math.round(Style.barHeight * scaling) - implicitWidth: { - 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 - } + implicitHeight: (barPosition === "left" || barPosition === "right") ? calculatedVerticalHeight() : Math.round(Style.barHeight * scaling) + implicitWidth: (barPosition === "left" || barPosition === "right") ? Math.round(Style.barHeight * scaling) : calculatedHorizontalWidth() function calculatedWsWidth(ws) { if (ws.isFocused) @@ -68,6 +60,37 @@ Item { 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: { refreshWorkspaces() } @@ -99,7 +122,8 @@ Item { } } } - workspaceRepeater.model = localWorkspaces + workspaceRepeaterHorizontal.model = localWorkspaces + workspaceRepeaterVertical.model = localWorkspaces updateWorkspaceFocus() } @@ -148,9 +172,8 @@ Item { Rectangle { id: workspaceBackground - width: parent.width - - height: Math.round(Style.capsuleHeight * scaling) + 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) color: Color.mSurfaceVariant @@ -158,14 +181,17 @@ Item { anchors.verticalCenter: parent.verticalCenter } + // Horizontal layout for top/bottom bars Row { id: pillRow spacing: spacingBetweenPills anchors.verticalCenter: workspaceBackground.verticalCenter width: root.width - horizontalPadding * 2 x: horizontalPadding + visible: barPosition === "top" || barPosition === "bottom" + Repeater { - id: workspaceRepeater + id: workspaceRepeaterHorizontal model: localWorkspaces Item { 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 + } + } + } + } } diff --git a/Modules/SettingsPanel/Tabs/BarTab.qml b/Modules/SettingsPanel/Tabs/BarTab.qml index ae502ce..5c1d11e 100644 --- a/Modules/SettingsPanel/Tabs/BarTab.qml +++ b/Modules/SettingsPanel/Tabs/BarTab.qml @@ -42,6 +42,14 @@ ColumnLayout { key: "bottom" name: "Bottom" } + ListElement { + key: "left" + name: "Left" + } + ListElement { + key: "right" + name: "Right" + } } currentKey: Settings.data.bar.position onSelected: key => Settings.data.bar.position = key diff --git a/flake.nix b/flake.nix index b2daca6..cdb0de6 100644 --- a/flake.nix +++ b/flake.nix @@ -1,6 +1,5 @@ { - description = - "Noctalia shell - a Wayland desktop shell built with Quickshell"; + description = "Noctalia shell - a Wayland desktop shell built with Quickshell"; inputs = { nixpkgs.url = "github:nixos/nixpkgs/nixos-unstable"; @@ -12,13 +11,22 @@ }; }; - outputs = { self, nixpkgs, systems, quickshell, ... }: - let eachSystem = nixpkgs.lib.genAttrs (import systems); - in { - formatter = - eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra); + outputs = + { + self, + nixpkgs, + systems, + quickshell, + ... + }: + let + eachSystem = nixpkgs.lib.genAttrs (import systems); + in + { + formatter = eachSystem (system: nixpkgs.legacyPackages.${system}.alejandra); - packages = eachSystem (system: + packages = eachSystem ( + system: let pkgs = nixpkgs.legacyPackages.${system}; qs = quickshell.packages.${system}.default.override { @@ -26,7 +34,8 @@ withI3 = false; }; - runtimeDeps = with pkgs; + runtimeDeps = + with pkgs; [ bash bluez @@ -41,21 +50,34 @@ matugen networkmanager 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 { - fontDirectories = [ pkgs.roboto pkgs.inter-nerdfont ]; + fontDirectories = [ + pkgs.roboto + pkgs.inter-nerdfont + ]; }; - in { + in + { default = pkgs.stdenv.mkDerivation { pname = "noctalia-shell"; version = self.rev or self.dirtyRev or "dirty"; src = ./.; - nativeBuildInputs = - [ pkgs.gcc pkgs.makeWrapper pkgs.qt6.wrapQtAppsHook ]; - buildInputs = [ qs pkgs.xkeyboard_config pkgs.qt6.qtbase ]; + nativeBuildInputs = [ + pkgs.gcc + pkgs.makeWrapper + pkgs.qt6.wrapQtAppsHook + ]; + buildInputs = [ + qs + pkgs.xkeyboard-config + pkgs.qt6.qtbase + ]; propagatedBuildInputs = runtimeDeps; installPhase = '' @@ -69,14 +91,14 @@ ''; meta = { - description = - "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell."; + description = "A sleek and minimal desktop shell thoughtfully crafted for Wayland, built with Quickshell."; homepage = "https://github.com/noctalia-dev/noctalia-shell"; license = pkgs.lib.licenses.mit; mainProgram = "noctalia-shell"; }; }; - }); + } + ); defaultPackage = eachSystem (system: self.packages.${system}.default); };