refactor(bar): modularize bar widgets and layout

This commit is contained in:
tux
2026-08-23 12:53:07 +05:30
parent 494420d241
commit 2f122a9487
12 changed files with 381 additions and 318 deletions

29
config/BarLayout.qml Normal file
View File

@@ -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")
]
}

View File

@@ -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
}

View File

@@ -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
}
}
}
}
}
}

View File

@@ -0,0 +1,10 @@
pragma ComponentBehavior: Bound
import qs.ui
import qs.services
BarButton {
hoverHighlight: false
pointerCursor: false
label: Time.time
}

View File

@@ -0,0 +1,9 @@
pragma ComponentBehavior: Bound
import Quickshell
import qs.ui
BarButton {
iconOnly: true
iconSource: `${Quickshell.shellPath("assets")}/icons/ghost.svg`
}

View File

@@ -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"])
}

View File

@@ -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
}
}
}
}

View File

@@ -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
}
}
}
}
}

46
ui/BarButton.qml Normal file
View File

@@ -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
}
}
}
}

12
ui/ExclusionZone.qml Normal file
View File

@@ -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`
}

66
ui/RoundedCorner.qml Normal file
View File

@@ -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
}
}
}
}

View File

@@ -1,16 +1,11 @@
pragma ComponentBehavior: Bound
import Quickshell import Quickshell
import Quickshell.Io
import QtQuick import QtQuick
import QtQuick.Layouts import QtQuick.Layouts
import QtQuick.Controls
import QtQuick.Effects
import QtQuick.Shapes
import Quickshell.Hyprland
import Quickshell.Services.SystemTray
import Quickshell.Widgets import Quickshell.Widgets
import Quickshell.Wayland
import qs.config import qs.config
import qs.services import qs.ui
PanelWindow { PanelWindow {
id: window id: window
@@ -31,91 +26,51 @@ PanelWindow {
bottom: true bottom: true
} }
// Inline Components component BarSection: Rectangle {
component Corner: WrapperItem { id: section
id: root
property int corner property var widgets: []
property real radius: Appearance.radius property int alignment: Qt.AlignLeft
property color color
Component.onCompleted: { color: "transparent"
switch (corner) { implicitHeight: parent.height
case 0: Layout.fillWidth: true
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 { Row {
preferredRendererType: Shape.CurveRenderer 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 { Repeater {
strokeWidth: 0 model: section.widgets
fillColor: root.color
startX: root.radius
PathArc { Loader {
relativeX: -root.radius required property var modelData
relativeY: root.radius
radiusX: root.radius
radiusY: radiusX
direction: PathArc.Counterclockwise
}
PathLine { source: modelData
relativeX: 0
relativeY: -root.radius
}
PathLine {
relativeX: root.radius
relativeY: 0
} }
} }
} }
} }
component Exclusion: PanelWindow {
property string name
implicitWidth: 0
implicitHeight: 0
WlrLayershell.namespace: `quickshell:${name}ExclusionZone`
}
// Exclusions // Exclusions
Scope { Scope {
Exclusion { ExclusionZone {
name: "left" name: "left"
exclusiveZone: leftBar.implicitWidth exclusiveZone: leftBar.implicitWidth
anchors.left: true anchors.left: true
} }
Exclusion { ExclusionZone {
name: "top" name: "top"
exclusiveZone: topBar.implicitHeight exclusiveZone: topBar.implicitHeight
anchors.top: true anchors.top: true
} }
Exclusion { ExclusionZone {
name: "right" name: "right"
exclusiveZone: rightBar.implicitWidth exclusiveZone: rightBar.implicitWidth
anchors.right: true anchors.right: true
} }
Exclusion { ExclusionZone {
name: "bottom" name: "bottom"
exclusiveZone: bottomBar.implicitHeight exclusiveZone: bottomBar.implicitHeight
anchors.bottom: true anchors.bottom: true
@@ -125,6 +80,7 @@ PanelWindow {
// Bars // Bars
Rectangle { Rectangle {
id: leftBar id: leftBar
implicitWidth: 10 implicitWidth: 10
implicitHeight: QsWindow.window?.height ?? 0 implicitHeight: QsWindow.window?.height ?? 0
color: window.barColor color: window.barColor
@@ -132,6 +88,7 @@ PanelWindow {
} }
Rectangle { Rectangle {
id: rightBar id: rightBar
implicitWidth: 10 implicitWidth: 10
implicitHeight: QsWindow.window?.height ?? 0 implicitHeight: QsWindow.window?.height ?? 0
color: window.barColor color: window.barColor
@@ -139,6 +96,7 @@ PanelWindow {
} }
Rectangle { Rectangle {
id: bottomBar id: bottomBar
implicitWidth: QsWindow.window?.width ?? 0 implicitWidth: QsWindow.window?.width ?? 0
implicitHeight: 10 implicitHeight: 10
color: window.barColor color: window.barColor
@@ -146,6 +104,7 @@ PanelWindow {
} }
Rectangle { Rectangle {
id: topBar id: topBar
implicitWidth: QsWindow.window?.width ?? 0 implicitWidth: QsWindow.window?.width ?? 0
implicitHeight: 50 implicitHeight: 50
color: window.barColor color: window.barColor
@@ -153,6 +112,7 @@ PanelWindow {
FlexboxLayout { FlexboxLayout {
id: flexLayout id: flexLayout
anchors.fill: parent anchors.fill: parent
anchors.margins: Appearance.margin anchors.margins: Appearance.margin
@@ -160,260 +120,24 @@ PanelWindow {
direction: FlexboxLayout.Row direction: FlexboxLayout.Row
justifyContent: FlexboxLayout.JustifySpaceBetween justifyContent: FlexboxLayout.JustifySpaceBetween
BarSection {
Rectangle { widgets: BarLayout.left
color: 'transparent' alignment: Qt.AlignLeft
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"]);
} }
BarSection {
hoverEnabled: true widgets: BarLayout.center
alignment: Qt.AlignHCenter
HoverHandler {
cursorShape: Qt.PointingHandCursor
} }
BarSection {
background: Rectangle { widgets: BarLayout.right
anchors.fill: parent alignment: Qt.AlignRight
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
}
}
}
}
}
}
}
}
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
}
}
}
}
}
}
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
}
}
}
}
}
}
}
}
Rectangle { Rectangle {
id: cornersArea id: cornersArea
implicitWidth: QsWindow.window?.width - (leftBar.implicitWidth + rightBar.implicitWidth) implicitWidth: QsWindow.window?.width - (leftBar.implicitWidth + rightBar.implicitWidth)
implicitHeight: QsWindow.window?.height - (topBar.implicitHeight + bottomBar.implicitHeight) implicitHeight: QsWindow.window?.height - (topBar.implicitHeight + bottomBar.implicitHeight)
color: "transparent" color: "transparent"
@@ -423,13 +147,12 @@ PanelWindow {
Repeater { Repeater {
model: 4 model: 4
Corner { RoundedCorner {
required property int modelData required property int modelData
corner: modelData corner: modelData
color: window.barColor color: window.barColor
} }
} }
} }
} }