diff --git a/config/BarLayout.qml b/config/BarLayout.qml new file mode 100644 index 0000000..a76b3f5 --- /dev/null +++ b/config/BarLayout.qml @@ -0,0 +1,29 @@ +pragma Singleton + +import Quickshell +import qs.ui +import qs.services + +Singleton { + id: root + + function widget(name: string): url { + return `${Quickshell.shellPath("modules/bar")}/${name}.qml`; + } + + property var left: [ + root.widget("LauncherButton"), + root.widget("BatteryIndicator"), + root.widget("CavaVisualizer") + ] + + property var center: [ + root.widget("Workspaces") + ] + + property var right: [ + root.widget("SystemTrayWidget"), + root.widget("ClockWidget"), + root.widget("GhostButton") + ] +} diff --git a/modules/bar/BatteryIndicator.qml b/modules/bar/BatteryIndicator.qml new file mode 100644 index 0000000..691a118 --- /dev/null +++ b/modules/bar/BatteryIndicator.qml @@ -0,0 +1,11 @@ +pragma ComponentBehavior: Bound + +import qs.ui +import qs.services + +BarButton { + hoverHighlight: false + pointerCursor: false + label: `${Battery.percentage}% ${Battery.energyRate}W` + iconSource: Battery.icon +} diff --git a/modules/bar/CavaVisualizer.qml b/modules/bar/CavaVisualizer.qml new file mode 100644 index 0000000..fdc75a4 --- /dev/null +++ b/modules/bar/CavaVisualizer.qml @@ -0,0 +1,51 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import qs.config +import qs.services + +Rectangle { + id: root + + property int barCount: 14 + property real maxValue: 100 + readonly property bool shouldVisualize: barCount > 0 && Cava.values.some(v => v >= 0.001) + + implicitWidth: 96 + implicitHeight: 32 + radius: Appearance.radius + color: Appearance.colors.inActive + visible: shouldVisualize + + Row { + id: barRow + + anchors.fill: parent + anchors.margins: 8 + spacing: 3 + + Repeater { + model: root.barCount + + Rectangle { + id: bar + + required property int index + readonly property real value: Cava.values[index] ?? 0 + + width: (barRow.width - barRow.spacing * (root.barCount - 1)) / root.barCount + height: Math.max(2, (bar.value / root.maxValue) * barRow.height) + anchors.bottom: parent.bottom + radius: bar.width / 2 + color: Appearance.colors.accent + antialiasing: true + + Behavior on height { + NumberAnimation { + duration: Appearance.duration + } + } + } + } + } +} diff --git a/modules/bar/ClockWidget.qml b/modules/bar/ClockWidget.qml new file mode 100644 index 0000000..25b4891 --- /dev/null +++ b/modules/bar/ClockWidget.qml @@ -0,0 +1,10 @@ +pragma ComponentBehavior: Bound + +import qs.ui +import qs.services + +BarButton { + hoverHighlight: false + pointerCursor: false + label: Time.time +} diff --git a/modules/bar/GhostButton.qml b/modules/bar/GhostButton.qml new file mode 100644 index 0000000..457cf36 --- /dev/null +++ b/modules/bar/GhostButton.qml @@ -0,0 +1,9 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import qs.ui + +BarButton { + iconOnly: true + iconSource: `${Quickshell.shellPath("assets")}/icons/ghost.svg` +} diff --git a/modules/bar/LauncherButton.qml b/modules/bar/LauncherButton.qml new file mode 100644 index 0000000..2e5595d --- /dev/null +++ b/modules/bar/LauncherButton.qml @@ -0,0 +1,10 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import qs.ui + +BarButton { + iconOnly: true + iconSource: `${Quickshell.shellPath("assets")}/icons/nix.svg` + onClicked: Quickshell.execDetached(["vicinae", "toggle"]) +} diff --git a/modules/bar/SystemTrayWidget.qml b/modules/bar/SystemTrayWidget.qml new file mode 100644 index 0000000..31f27c0 --- /dev/null +++ b/modules/bar/SystemTrayWidget.qml @@ -0,0 +1,36 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell.Services.SystemTray +import qs.config + +Rectangle { + id: root + + property int iconSize: 16 + + implicitWidth: trayRow.implicitWidth + 10 + implicitHeight: 34 + radius: Appearance.radius + color: Appearance.colors.inActive + visible: SystemTray.items.values.length > 0 + + Row { + id: trayRow + + anchors.centerIn: parent + spacing: Appearance.spacing + + Repeater { + model: SystemTray.items.values + + Image { + required property var modelData + + source: modelData.icon + width: root.iconSize + height: root.iconSize + } + } + } +} diff --git a/modules/bar/Workspaces.qml b/modules/bar/Workspaces.qml new file mode 100644 index 0000000..f73ce9a --- /dev/null +++ b/modules/bar/Workspaces.qml @@ -0,0 +1,60 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import Quickshell.Hyprland +import qs.config + +Row { + id: root + + property int count: 7 + + spacing: Appearance.spacing + + Repeater { + model: root.count + + Rectangle { + id: pill + + required property int index + property var ws: Hyprland.workspaces.values.find(w => w.id === pill.index + 1) + property bool isActive: Hyprland.focusedWorkspace?.id === (pill.index + 1) + + radius: 1000 + implicitHeight: 15 + implicitWidth: pill.isActive ? pill.implicitHeight * 2.3 : pill.implicitHeight + color: { + if (handler.hovered) + return Appearance.colors.accent; + if (pill.isActive || pill.ws) + return Appearance.colors.accent; + return Appearance.colors.inActive; + } + + MouseArea { + hoverEnabled: true + anchors.fill: parent + cursorShape: Qt.PointingHandCursor + onClicked: Hyprland.dispatch("workspace " + (pill.index + 1)) + } + + HoverHandler { + id: handler + } + + Behavior on color { + ColorAnimation { + duration: Appearance.duration + } + } + + Behavior on implicitWidth { + NumberAnimation { + duration: Appearance.duration + easing.type: Easing.OutQuad + } + } + } + } +} diff --git a/ui/BarButton.qml b/ui/BarButton.qml new file mode 100644 index 0000000..c00e53e --- /dev/null +++ b/ui/BarButton.qml @@ -0,0 +1,46 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Controls +import qs.config + +Button { + id: root + + property string label: "" + property url iconSource: "" + property bool iconOnly: false + property bool hoverHighlight: true + property bool pointerCursor: true + + padding: Appearance.padding + display: iconOnly ? AbstractButton.IconOnly : AbstractButton.TextBesideIcon + + text: label + font.family: Appearance.font.family + font.pointSize: Appearance.font.pointSize + palette.buttonText: Appearance.colors.foreground + icon.color: Appearance.colors.foreground + icon.source: iconSource + + HoverHandler { + cursorShape: Qt.PointingHandCursor + enabled: root.pointerCursor + } + + background: Rectangle { + anchors.fill: parent + radius: Appearance.radius + color: { + if (!root.hoverHighlight) + return Appearance.colors.inActive; + return root.hovered ? Appearance.colors.inActive : Appearance.colors.background; + } + + Behavior on color { + ColorAnimation { + duration: Appearance.duration + } + } + } +} diff --git a/ui/ExclusionZone.qml b/ui/ExclusionZone.qml new file mode 100644 index 0000000..016d2e4 --- /dev/null +++ b/ui/ExclusionZone.qml @@ -0,0 +1,12 @@ +pragma ComponentBehavior: Bound + +import Quickshell +import Quickshell.Wayland + +PanelWindow { + property string name + + implicitWidth: 0 + implicitHeight: 0 + WlrLayershell.namespace: `quickshell:${name}ExclusionZone` +} diff --git a/ui/RoundedCorner.qml b/ui/RoundedCorner.qml new file mode 100644 index 0000000..edc6df8 --- /dev/null +++ b/ui/RoundedCorner.qml @@ -0,0 +1,66 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Shapes +import Quickshell.Widgets +import qs.config + +WrapperItem { + id: root + + property int corner + property real radius: Appearance.radius + property color color + + Component.onCompleted: { + switch (corner) { + case 0: + anchors.left = parent.left; + anchors.top = parent.top; + break; + case 1: + anchors.top = parent.top; + anchors.right = parent.right; + rotation = 90; + break; + case 2: + anchors.right = parent.right; + anchors.bottom = parent.bottom; + rotation = 180; + break; + case 3: + anchors.left = parent.left; + anchors.bottom = parent.bottom; + rotation = -90; + break; + } + } + + Shape { + preferredRendererType: Shape.CurveRenderer + + ShapePath { + strokeWidth: 0 + fillColor: root.color + startX: root.radius + + PathArc { + relativeX: -root.radius + relativeY: root.radius + radiusX: root.radius + radiusY: radiusX + direction: PathArc.Counterclockwise + } + + PathLine { + relativeX: 0 + relativeY: -root.radius + } + + PathLine { + relativeX: root.radius + relativeY: 0 + } + } + } +} diff --git a/windows/Bar.qml b/windows/Bar.qml index d49e779..a20b795 100644 --- a/windows/Bar.qml +++ b/windows/Bar.qml @@ -1,16 +1,11 @@ +pragma ComponentBehavior: Bound + import Quickshell -import Quickshell.Io import QtQuick import QtQuick.Layouts -import QtQuick.Controls -import QtQuick.Effects -import QtQuick.Shapes -import Quickshell.Hyprland -import Quickshell.Services.SystemTray import Quickshell.Widgets -import Quickshell.Wayland import qs.config -import qs.services +import qs.ui PanelWindow { id: window @@ -31,91 +26,51 @@ PanelWindow { bottom: true } - // Inline Components - component Corner: WrapperItem { - id: root + component BarSection: Rectangle { + id: section - property int corner - property real radius: Appearance.radius - property color color + property var widgets: [] + property int alignment: Qt.AlignLeft - Component.onCompleted: { - switch (corner) { - case 0: - anchors.left = parent.left; - anchors.top = parent.top; - break; - case 1: - anchors.top = parent.top; - anchors.right = parent.right; - rotation = 90; - break; - case 2: - anchors.right = parent.right; - anchors.bottom = parent.bottom; - rotation = 180; - break; - case 3: - anchors.left = parent.left; - anchors.bottom = parent.bottom; - rotation = -90; - break; - } - } + color: "transparent" + implicitHeight: parent.height + Layout.fillWidth: true - Shape { - preferredRendererType: Shape.CurveRenderer + Row { + x: section.alignment === Qt.AlignLeft ? 0 : section.alignment === Qt.AlignRight ? parent.width - width : (parent.width - width) / 2 + y: (section.height - height) / 2 + spacing: Appearance.spacing - ShapePath { - strokeWidth: 0 - fillColor: root.color - startX: root.radius + Repeater { + model: section.widgets - PathArc { - relativeX: -root.radius - relativeY: root.radius - radiusX: root.radius - radiusY: radiusX - direction: PathArc.Counterclockwise - } + Loader { + required property var modelData - PathLine { - relativeX: 0 - relativeY: -root.radius - } - - PathLine { - relativeX: root.radius - relativeY: 0 + source: modelData } } } } - component Exclusion: PanelWindow { - property string name - implicitWidth: 0 - implicitHeight: 0 - WlrLayershell.namespace: `quickshell:${name}ExclusionZone` - } // Exclusions Scope { - Exclusion { + ExclusionZone { name: "left" exclusiveZone: leftBar.implicitWidth anchors.left: true } - Exclusion { + ExclusionZone { name: "top" exclusiveZone: topBar.implicitHeight anchors.top: true } - Exclusion { + ExclusionZone { name: "right" exclusiveZone: rightBar.implicitWidth anchors.right: true } - Exclusion { + ExclusionZone { name: "bottom" exclusiveZone: bottomBar.implicitHeight anchors.bottom: true @@ -125,6 +80,7 @@ PanelWindow { // Bars Rectangle { id: leftBar + implicitWidth: 10 implicitHeight: QsWindow.window?.height ?? 0 color: window.barColor @@ -132,6 +88,7 @@ PanelWindow { } Rectangle { id: rightBar + implicitWidth: 10 implicitHeight: QsWindow.window?.height ?? 0 color: window.barColor @@ -139,6 +96,7 @@ PanelWindow { } Rectangle { id: bottomBar + implicitWidth: QsWindow.window?.width ?? 0 implicitHeight: 10 color: window.barColor @@ -146,6 +104,7 @@ PanelWindow { } Rectangle { id: topBar + implicitWidth: QsWindow.window?.width ?? 0 implicitHeight: 50 color: window.barColor @@ -153,6 +112,7 @@ PanelWindow { FlexboxLayout { id: flexLayout + anchors.fill: parent anchors.margins: Appearance.margin @@ -160,260 +120,24 @@ PanelWindow { direction: FlexboxLayout.Row justifyContent: FlexboxLayout.JustifySpaceBetween - - Rectangle { - color: 'transparent' - implicitHeight: parent.height - Layout.fillWidth: true - - Row { - Layout.fillWidth: true - anchors.verticalCenter: parent.verticalCenter - anchors.left: parent.left - spacing: Appearance.spacing - - Button { - padding: Appearance.padding - display: AbstractButton.IconOnly - - icon.color: Appearance.colors.foreground - icon.source: Quickshell.shellPath("assets") + "/icons/nix.svg" - onClicked: { - Quickshell.execDetached(["vicinae", "toggle"]); - } - - hoverEnabled: true - - HoverHandler { - cursorShape: Qt.PointingHandCursor - } - - background: Rectangle { - anchors.fill: parent - radius: Appearance.radius - color: parent.hovered ? Appearance.colors.inActive : Appearance.colors.background - - Behavior on color { - ColorAnimation { - duration: Appearance.duration - } - } - } - } - - Button { - padding: Appearance.padding - - text: `${Battery.percentage}% ${Battery.energyRate}W` - palette.buttonText: Appearance.colors.foreground - font.family: Appearance.font.family - font.pointSize: Appearance.font.pointSize - - icon.color: Appearance.colors.foreground - icon.source: Battery.icon - - background: Rectangle { - anchors.fill: parent - radius: Appearance.radius - color: Appearance.colors.inActive - } - } - - Button { - id: cava - - padding: Appearance.padding - - readonly property int barCount: 14 - readonly property real maxValue: 100 - readonly property bool shouldVisualize: barCount > 0 && Cava.values.some(v => v >= 0.001) - - implicitWidth: 96 - implicitHeight: 32 - - background: Rectangle { - anchors.fill: parent - radius: Appearance.radius - color: Appearance.colors.inActive - } - - visible: shouldVisualize - - Row { - id: barRow - - anchors.fill: parent - anchors.margins: 8 - spacing: 3 - - Repeater { - model: cava.barCount - - Rectangle { - required property int index - readonly property real value: Cava.values[index] ?? 0 - - width: (barRow.width - barRow.spacing * (cava.barCount - 1)) / cava.barCount - height: Math.max(2, (value / cava.maxValue) * barRow.height) - anchors.bottom: parent.bottom - radius: width / 2 - color: Appearance.colors.accent - antialiasing: true - - Behavior on height { - NumberAnimation { - duration: Appearance.duration - } - } - } - } - } - } - } + BarSection { + widgets: BarLayout.left + alignment: Qt.AlignLeft } - - Rectangle { - color: 'transparent' - implicitHeight: parent.height - Layout.fillWidth: true - - Row { - Layout.fillWidth: true - anchors.verticalCenter: parent.verticalCenter - anchors.horizontalCenter: parent.horizontalCenter - spacing: Appearance.spacing - - Repeater { - model: 7 - - Rectangle { - property var ws: Hyprland.workspaces.values.find(w => w.id === index + 1) - property bool isActive: Hyprland.focusedWorkspace?.id === (index + 1) - Layout.alignment: Qt.AlignVCenter - radius: 1000 - implicitHeight: 15 - - implicitWidth: isActive ? this.implicitHeight * 2.3 : this.implicitHeight - - color: { - if (handler.hovered) { - return Appearance.colors.accent; - } else if (isActive || ws) { - return Appearance.colors.accent; - } else { - return Appearance.colors.inActive; - } - } - - MouseArea { - hoverEnabled: true - anchors.fill: parent - cursorShape: Qt.PointingHandCursor - onClicked: Hyprland.dispatch("workspace " + (index + 1)) - } - - Behavior on color { - ColorAnimation { - duration: Appearance.duration - } - } - HoverHandler { - id: handler - } - - Behavior on implicitWidth { - NumberAnimation { - duration: Appearance.duration - easing.type: Easing.OutQuad - } - } - } - } - } + BarSection { + widgets: BarLayout.center + alignment: Qt.AlignHCenter } - Rectangle { - color: 'transparent' - implicitHeight: parent.height - Layout.fillWidth: true - - Row { - Layout.fillWidth: true - anchors.verticalCenter: parent.verticalCenter - anchors.right: parent.right - spacing: Appearance.spacing - - Rectangle { - implicitWidth: sysTrayRow.implicitWidth + 10 - implicitHeight: parent.height - radius: Appearance.radius - color: Appearance.colors.inActive - visible: SystemTray.items.values.length > 0 - - Row { - id: sysTrayRow - - anchors.centerIn: parent - spacing: Appearance.spacing - - Repeater { - model: SystemTray.items.values.length - Image { - source: SystemTray.items.values[index].icon - height: parent.parent.height - 18 - width: parent.parent.height - 18 - } - } - } - } - - Button { - padding: Appearance.padding - text: Time.time - - palette.buttonText: Appearance.colors.foreground - font.family: Appearance.font.family - font.pointSize: Appearance.font.pointSize - - background: Rectangle { - anchors.fill: parent - radius: Appearance.radius - color: Appearance.colors.inActive - } - } - - Button { - padding: Appearance.padding - display: AbstractButton.IconOnly - - icon.color: Appearance.colors.foreground - icon.source: Quickshell.shellPath("assets") + "/icons/ghost.svg" - - hoverEnabled: true - - HoverHandler { - cursorShape: Qt.PointingHandCursor - } - - background: Rectangle { - anchors.fill: parent - radius: Appearance.radius - color: parent.hovered ? Appearance.colors.inActive : Appearance.colors.background - - Behavior on color { - ColorAnimation { - duration: Appearance.duration - } - } - } - } - } + BarSection { + widgets: BarLayout.right + alignment: Qt.AlignRight } } - } Rectangle { id: cornersArea + implicitWidth: QsWindow.window?.width - (leftBar.implicitWidth + rightBar.implicitWidth) implicitHeight: QsWindow.window?.height - (topBar.implicitHeight + bottomBar.implicitHeight) color: "transparent" @@ -421,15 +145,14 @@ PanelWindow { y: topBar.implicitHeight Repeater { - model: 4 + model: 4 - Corner { + RoundedCorner { required property int modelData + corner: modelData color: window.barColor } } } } - -