vertical bar

This commit is contained in:
end-4 2025-08-15 22:17:27 +07:00
parent 25a0c88670
commit 9fc0d26eb5
17 changed files with 1334 additions and 80 deletions

View file

@ -4,19 +4,18 @@ import QtQuick
import QtQuick.Layouts
import Quickshell.Services.SystemTray
// TODO: More fancy animation
Item {
id: root
implicitWidth: gridLayout.implicitWidth
implicitHeight: gridLayout.implicitHeight
property bool vertical: false
height: parent.height
implicitWidth: rowLayout.implicitWidth
Layout.leftMargin: Appearance.rounding.screenRounding
RowLayout {
id: rowLayout
GridLayout {
id: gridLayout
columns: root.vertical ? 1 : -1
anchors.fill: parent
spacing: 15
rowSpacing: 10
columnSpacing: 15
Repeater {
model: SystemTray.items
@ -24,12 +23,14 @@ Item {
SysTrayItem {
required property SystemTrayItem modelData
item: modelData
Layout.fillHeight: !root.vertical
Layout.fillWidth: root.vertical
}
}
StyledText {
Layout.alignment: Qt.AlignVCenter
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
font.pixelSize: Appearance.font.pixelSize.larger
color: Appearance.colors.colSubtext
text: "•"

View file

@ -13,11 +13,10 @@ MouseArea {
property var bar: root.QsWindow.window
required property SystemTrayItem item
property bool targetMenuOpen: false
property int trayItemWidth: Appearance.font.pixelSize.larger
acceptedButtons: Qt.LeftButton | Qt.RightButton
Layout.fillHeight: true
implicitWidth: trayItemWidth
implicitWidth: 20
implicitHeight: 20
onClicked: (event) => {
switch (event.button) {
case Qt.LeftButton:
@ -35,10 +34,11 @@ MouseArea {
menu: root.item.menu
anchor.window: bar
anchor.rect.x: root.x + bar.width
anchor.rect.y: root.y
anchor.rect.x: root.x + (Config.options.bar.vertical ? 0 : bar.width)
anchor.rect.y: root.y + (Config.options.bar.vertical ? bar.height : 0)
anchor.rect.height: root.height
anchor.edges: Edges.Bottom
anchor.rect.width: root.width
anchor.edges: Config.options.bar.bottom ? (Edges.Top | Edges.Left) : (Edges.Bottom | Edges.Right)
}
IconImage {

View file

@ -329,19 +329,23 @@ Singleton {
property real barCenterSideModuleWidthHellaShortened: 190
property real barShortenScreenWidthThreshold: 1200 // Shorten if screen width is at most this value
property real barHellaShortenScreenWidthThreshold: 1000 // Shorten even more...
property real sidebarWidth: 460
property real sidebarWidthExtended: 750
property real osdWidth: 200
property real mediaControlsWidth: 440
property real mediaControlsHeight: 160
property real notificationPopupWidth: 410
property real searchWidthCollapsed: 260
property real searchWidth: 450
property real hyprlandGapsOut: 5
property real elevationMargin: 10
property real fabShadowRadius: 5
property real fabHoveredShadowRadius: 7
property real hyprlandGapsOut: 5
property real mediaControlsWidth: 440
property real mediaControlsHeight: 160
property real notificationPopupWidth: 410
property real osdWidth: 200
property real searchWidthCollapsed: 260
property real searchWidth: 450
property real sidebarWidth: 460
property real sidebarWidthExtended: 750
property real baseVerticalBarWidth: 46
property real verticalBarWidth: Config.options.bar.cornerStyle === 1 ?
(baseVerticalBarWidth + root.sizes.hyprlandGapsOut * 2) : baseVerticalBarWidth
property real verticalBarGroupWidth: 38
}
syntaxHighlightingTheme: Appearance.m3colors.darkmode ? "Monokai" : "ayu Light"
syntaxHighlightingTheme: root.m3colors.darkmode ? "Monokai" : "ayu Light"
}

View file

@ -143,6 +143,7 @@ Singleton {
property string topLeftIcon: "spark" // Options: "distro" or any icon name in ~/.config/quickshell/ii/assets/icons
property bool showBackground: true
property bool verbose: true
property bool vertical: false
property JsonObject resources: JsonObject {
property bool alwaysShowSwap: true
property bool alwaysShowCpu: false

View file

@ -10,6 +10,7 @@ import Qt5Compat.GraphicalEffects
*/
ProgressBar {
id: root
property bool vertical: false
property real valueBarWidth: 30
property real valueBarHeight: 18
property color highlightColor: Appearance?.colors.colOnSecondaryContainer ?? "#685496"
@ -45,13 +46,36 @@ ProgressBar {
visible: false
Rectangle {
id: progressFill
anchors {
left: parent.left
top: parent.top
bottom: parent.bottom
left: parent.left
right: undefined
}
radius: Appearance.rounding.unsharpen
width: parent.width * root.visualPosition
height: parent.height
states: State {
name: "vertical"
when: root.vertical
AnchorChanges {
target: progressFill
anchors {
top: undefined
bottom: parent.bottom
left: parent.left
right: parent.right
}
}
PropertyChanges {
target: progressFill
width: parent.width
height: parent.height * root.visualPosition
}
}
radius: Appearance.rounding.unsharpen
color: root.highlightColor
}
}

View file

@ -17,9 +17,10 @@ LazyLoader {
id: popupWindow
color: "transparent"
anchors.left: true
anchors.top: !Config.options.bar.bottom
anchors.bottom: Config.options.bar.bottom
anchors.left: !Config.options.bar.vertical || (Config.options.bar.vertical && !Config.options.bar.bottom)
anchors.right: Config.options.bar.vertical && Config.options.bar.bottom
anchors.top: Config.options.bar.vertical || (!Config.options.bar.vertical && !Config.options.bar.bottom)
anchors.bottom: !Config.options.bar.vertical && Config.options.bar.bottom
implicitWidth: popupBackground.implicitWidth + Appearance.sizes.hyprlandGapsOut * 2
implicitHeight: popupBackground.implicitHeight + Appearance.sizes.hyprlandGapsOut * 2
@ -27,12 +28,22 @@ LazyLoader {
exclusionMode: ExclusionMode.Ignore
exclusiveZone: 0
margins {
left: root.QsWindow?.mapFromItem(
root.hoverTarget,
(root.hoverTarget.width - popupBackground.implicitWidth) / 2, 0
).x
top: Config?.options.bar.bottom ? 0 : Appearance.sizes.barHeight
bottom: Config?.options.bar.bottom ? Appearance.sizes.barHeight : 0
left: {
if (!Config.options.bar.vertical) return root.QsWindow?.mapFromItem(
root.hoverTarget,
(root.hoverTarget.width - popupBackground.implicitWidth) / 2, 0
).x;
return Appearance.sizes.barHeight
}
top: {
if (!Config.options.bar.vertical) return Appearance.sizes.barHeight;
return root.QsWindow?.mapFromItem(
root.hoverTarget,
(root.hoverTarget.height - popupBackground.implicitHeight) / 2, 0
).y;
}
right: Appearance.sizes.barHeight
bottom: Appearance.sizes.barHeight
}
WlrLayershell.namespace: "quickshell:popup"
WlrLayershell.layer: WlrLayer.Overlay

View file

@ -72,26 +72,53 @@ ContentPage {
ContentSection {
title: Translation.tr("Bar")
ConfigSelectionArray {
currentValue: Config.options.bar.cornerStyle
configOptionName: "bar.cornerStyle"
onSelected: newValue => {
Config.options.bar.cornerStyle = newValue;
}
options: [
{
displayName: Translation.tr("Hug"),
value: 0
},
{
displayName: Translation.tr("Float"),
value: 1
},
{
displayName: Translation.tr("Plain rectangle"),
value: 2
ConfigRow {
ContentSubsection {
title: "Corner style"
ConfigSelectionArray {
currentValue: Config.options.bar.cornerStyle
configOptionName: "bar.cornerStyle"
onSelected: newValue => {
Config.options.bar.cornerStyle = newValue; // Update local copy
}
options: [
{
displayName: Translation.tr("Hug"),
value: 0
},
{
displayName: Translation.tr("Float"),
value: 1
},
{
displayName: Translation.tr("Plain rectangle"),
value: 2
}
]
}
]
}
ContentSubsection {
title: "Bar layout"
ConfigSelectionArray {
currentValue: Config.options.bar.vertical
configOptionName: "bar.vertical"
onSelected: newValue => {
Config.options.bar.vertical = newValue;
}
options: [
{
displayName: Translation.tr("Horizontal"),
value: false
},
{
displayName: Translation.tr("Vertical"),
value: true
},
]
}
}
}
ContentSubsection {
@ -106,7 +133,7 @@ ContentPage {
}
}
ConfigSwitch {
text: Translation.tr("Place at the bottom")
text: Translation.tr("Place at the bottom/right")
checked: Config.options.bar.bottom
onCheckedChanged: {
Config.options.bar.bottom = checked;

View file

@ -0,0 +1,66 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
import "./../bar" as Bar
MouseArea {
id: root
property bool borderless: Config.options.bar.borderless
readonly property var chargeState: Battery.chargeState
readonly property bool isCharging: Battery.isCharging
readonly property bool isPluggedIn: Battery.isPluggedIn
readonly property real percentage: Battery.percentage
readonly property bool isLow: percentage <= Config.options.battery.low / 100
implicitHeight: batteryProgress.implicitHeight
implicitWidth: Appearance.sizes.verticalBarGroupWidth
hoverEnabled: true
ClippedProgressBar {
id: batteryProgress
anchors.centerIn: parent
vertical: true
valueBarWidth: 21
valueBarHeight: 40
value: percentage
highlightColor: (isLow && !isCharging) ? Appearance.m3colors.m3error : Appearance.colors.colOnSecondaryContainer
font {
pixelSize: text.length > 2 ? 11 : 13
weight: text.length > 2 ? Font.Medium : Font.DemiBold
}
textMask: Item {
anchors.centerIn: parent
width: batteryProgress.valueBarWidth
height: batteryProgress.valueBarHeight
ColumnLayout {
anchors.centerIn: parent
spacing: 0
MaterialSymbol {
id: boltIcon
Layout.alignment: Qt.AlignHCenter
fill: 1
text: isCharging ? "bolt" : "battery_android_full"
iconSize: Appearance.font.pixelSize.normal
visible: percentage < 1
}
StyledText {
Layout.alignment: Qt.AlignHCenter
font: batteryProgress.font
text: batteryProgress.text
}
}
}
}
Bar.BatteryPopup {
id: batteryPopup
hoverTarget: root
}
}

View file

@ -0,0 +1,43 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
import Quickshell
Item {
id: root
required property string iconName
required property double percentage
implicitHeight: resourceProgress.implicitHeight
implicitWidth: Appearance.sizes.verticalBarWidth
// Helper function to format KB to GB
function formatKB(kb) {
return (kb / (1024 * 1024)).toFixed(1) + " GB";
}
ClippedFilledCircularProgress {
id: resourceProgress
anchors.centerIn: parent
value: percentage
MaterialSymbol {
font.weight: Font.Medium
fill: 1
text: root.iconName
iconSize: 13
color: Appearance.colors.colOnSecondaryContainer
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
enabled: root.visible
}
}

View file

@ -0,0 +1,41 @@
import qs.services
import QtQuick
import QtQuick.Layouts
import "../bar" as Bar
MouseArea {
id: root
property bool alwaysShowAllResources: false
implicitHeight: columnLayout.implicitHeight
implicitWidth: columnLayout.implicitWidth
hoverEnabled: true
ColumnLayout {
id: columnLayout
spacing: 10
anchors.fill: parent
Resource {
Layout.alignment: Qt.AlignHCenter
iconName: "memory"
percentage: ResourceUsage.memoryUsedPercentage
}
Resource {
Layout.alignment: Qt.AlignHCenter
iconName: "swap_horiz"
percentage: ResourceUsage.swapUsedPercentage
}
Resource {
Layout.alignment: Qt.AlignHCenter
iconName: "planner_review"
percentage: ResourceUsage.cpuUsage
}
}
Bar.ResourcesPopup {
hoverTarget: root
}
}

View file

@ -0,0 +1,248 @@
import "./weather"
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Services.UPower
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
Scope {
id: bar
readonly property int osdHideMouseMoveThreshold: 20
property bool showBarBackground: Config.options.bar.showBackground
Variants {
// For each monitor
model: {
const screens = Quickshell.screens;
const list = Config.options.bar.screenList;
if (!list || list.length === 0)
return screens;
return screens.filter(screen => list.includes(screen.name));
}
LazyLoader {
id: barLoader
active: GlobalStates.barOpen && !GlobalStates.screenLocked
required property ShellScreen modelData
component: PanelWindow { // Bar window
id: barRoot
screen: barLoader.modelData
property var brightnessMonitor: Brightness.getMonitorForScreen(barLoader.modelData)
Timer {
id: showBarTimer
interval: (Config?.options.bar.autoHide.showWhenPressingSuper.delay ?? 100)
repeat: false
onTriggered: {
barRoot.superShow = true
}
}
Connections {
target: GlobalStates
function onSuperDownChanged() {
if (!Config?.options.bar.autoHide.showWhenPressingSuper.enable) return;
if (GlobalStates.superDown) showBarTimer.restart();
else {
showBarTimer.stop();
barRoot.superShow = false;
}
}
}
property bool superShow: false
property bool mustShow: hoverRegion.containsMouse || superShow
exclusionMode: ExclusionMode.Ignore
exclusiveZone: (Config?.options.bar.autoHide.enable && (!mustShow || !Config?.options.bar.autoHide.pushWindows)) ? 0 :
Appearance.sizes.baseVerticalBarWidth + (Config.options.bar.cornerStyle === 1 ? Appearance.sizes.hyprlandGapsOut : 0)
WlrLayershell.namespace: "quickshell:verticalBar"
// WlrLayershell.layer: WlrLayer.Overlay // TODO enable this when bar can hide when fullscreen
implicitWidth: Appearance.sizes.verticalBarWidth + Appearance.rounding.screenRounding
mask: Region {
item: hoverMaskRegion
}
color: "transparent"
anchors {
left: !Config.options.bar.bottom
right: Config.options.bar.bottom
top: true
bottom: true
}
MouseArea {
id: hoverRegion
hoverEnabled: true
anchors.fill: parent
Item {
id: hoverMaskRegion
anchors {
fill: barContent
leftMargin: -1
rightMargin: -1
}
}
VerticalBarContent {
id: barContent
implicitWidth: Appearance.sizes.verticalBarWidth
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
right: undefined
leftMargin: (Config?.options.bar.autoHide.enable && !mustShow) ? -Appearance.sizes.verticalBarWidth : 0
rightMargin: 0
}
Behavior on anchors.leftMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on anchors.rightMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
states: State {
name: "right"
when: Config.options.bar.bottom
AnchorChanges {
target: barContent
anchors {
top: parent.top
bottom: parent.bottom
left: undefined
right: parent.right
}
}
PropertyChanges {
target: barContent
anchors.topMargin: 0
anchors.rightMargin: (Config?.options.bar.autoHide.enable && !mustShow) ? -Appearance.sizes.barHeight : 0
}
}
}
// Round decorators
Loader {
id: roundDecorators
anchors {
top: parent.top
bottom: parent.bottom
left: barContent.right
right: undefined
}
width: Appearance.rounding.screenRounding
active: showBarBackground && Config.options.bar.cornerStyle === 0 // Hug
states: State {
name: "right"
when: Config.options.bar.bottom
AnchorChanges {
target: roundDecorators
anchors {
top: parent.top
bottom: parent.bottom
left: undefined
right: barContent.left
}
}
}
sourceComponent: Item {
implicitHeight: Appearance.rounding.screenRounding
RoundCorner {
id: topCorner
anchors {
left: parent.left
right: parent.right
top: parent.top
}
implicitSize: Appearance.rounding.screenRounding
color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
corner: RoundCorner.CornerEnum.TopLeft
states: State {
name: "bottom"
when: Config.options.bar.bottom
PropertyChanges {
topCorner.corner: RoundCorner.CornerEnum.TopRight
}
}
}
RoundCorner {
id: bottomCorner
anchors {
bottom: parent.bottom
left: !Config.options.bar.bottom ? parent.left : undefined
right: Config.options.bar.bottom ? parent.right : undefined
}
implicitSize: Appearance.rounding.screenRounding
color: showBarBackground ? Appearance.colors.colLayer0 : "transparent"
corner: RoundCorner.CornerEnum.BottomLeft
states: State {
name: "bottom"
when: Config.options.bar.bottom
PropertyChanges {
bottomCorner.corner: RoundCorner.CornerEnum.BottomRight
}
}
}
}
}
}
}
}
}
IpcHandler {
target: "bar"
function toggle(): void {
GlobalStates.barOpen = !GlobalStates.barOpen
}
function close(): void {
GlobalStates.barOpen = false
}
function open(): void {
GlobalStates.barOpen = true
}
}
GlobalShortcut {
name: "barToggle"
description: "Toggles bar on press"
onPressed: {
GlobalStates.barOpen = !GlobalStates.barOpen;
}
}
GlobalShortcut {
name: "barOpen"
description: "Opens bar on press"
onPressed: {
GlobalStates.barOpen = true;
}
}
GlobalShortcut {
name: "barClose"
description: "Closes bar on press"
onPressed: {
GlobalStates.barOpen = false;
}
}
}

View file

@ -0,0 +1,369 @@
import "./weather"
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Services.UPower
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import "../bar" as Bar
Item { // Bar content region
id: root
property var screen: root.QsWindow.window?.screen
property var brightnessMonitor: Brightness.getMonitorForScreen(screen)
component HorizontalBarSeparator: Rectangle {
Layout.leftMargin: Appearance.sizes.baseBarHeight / 3
Layout.rightMargin: Appearance.sizes.baseBarHeight / 3
Layout.fillWidth: true
implicitHeight: 1
color: Appearance.colors.colOutlineVariant
}
// Background shadow
Loader {
active: Config.options.bar.showBackground && Config.options.bar.cornerStyle === 1
anchors.fill: barBackground
sourceComponent: StyledRectangularShadow {
anchors.fill: undefined // The loader's anchors act on this, and this should not have any anchor
target: barBackground
}
}
// Background
Rectangle {
id: barBackground
anchors {
fill: parent
margins: Config.options.bar.cornerStyle === 1 ? (Appearance.sizes.hyprlandGapsOut) : 0 // idk why but +1 is needed
}
color: Config.options.bar.showBackground ? Appearance.colors.colLayer0 : "transparent"
radius: Config.options.bar.cornerStyle === 1 ? Appearance.rounding.windowRounding : 0
border.width: Config.options.bar.cornerStyle === 1 ? 1 : 0
border.color: Appearance.colors.colLayer0Border
}
MouseArea { // Top section | scroll to change brightness
id: barTopSectionMouseArea
anchors.top: parent.top
implicitHeight: topSectionColumnLayout.implicitHeight
implicitWidth: Appearance.sizes.baseVerticalBarWidth
height: (root.height - middleSection.height) / 2
width: Appearance.sizes.verticalBarWidth
property bool hovered: false
property real lastScrollX: 0
property real lastScrollY: 0
property bool trackingScroll: false
acceptedButtons: Qt.LeftButton
hoverEnabled: true
propagateComposedEvents: true
onEntered: event => {
barTopSectionMouseArea.hovered = true;
}
onExited: event => {
barTopSectionMouseArea.hovered = false;
barTopSectionMouseArea.trackingScroll = false;
}
onPressed: event => {
if (event.button === Qt.LeftButton) {
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
}
}
// Scroll to change brightness
WheelHandler {
onWheel: event => {
if (event.angleDelta.y < 0)
root.brightnessMonitor.setBrightness(root.brightnessMonitor.brightness - 0.05);
else if (event.angleDelta.y > 0)
root.brightnessMonitor.setBrightness(root.brightnessMonitor.brightness + 0.05);
// Store the mouse position and start tracking
barTopSectionMouseArea.lastScrollX = event.x;
barTopSectionMouseArea.lastScrollY = event.y;
barTopSectionMouseArea.trackingScroll = true;
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
}
onPositionChanged: mouse => {
if (barTopSectionMouseArea.trackingScroll) {
const dx = mouse.x - barTopSectionMouseArea.lastScrollX;
const dy = mouse.y - barTopSectionMouseArea.lastScrollY;
if (Math.sqrt(dx * dx + dy * dy) > osdHideMouseMoveThreshold) {
GlobalStates.osdBrightnessOpen = false;
barTopSectionMouseArea.trackingScroll = false;
}
}
}
ColumnLayout { // Content
id: topSectionColumnLayout
anchors.fill: parent
spacing: 10
RippleButton { // Left sidebar button
Layout.alignment: Qt.AlignTop | Qt.AlignHCenter
Layout.topMargin: (Appearance.sizes.baseVerticalBarWidth - implicitWidth) / 2 + Appearance.sizes.hyprlandGapsOut
Layout.fillHeight: false
property real buttonPadding: 5
implicitWidth: distroIcon.width + buttonPadding * 2
implicitHeight: distroIcon.height + buttonPadding * 2
buttonRadius: Appearance.rounding.full
colBackground: barTopSectionMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
colBackgroundHover: Appearance.colors.colLayer1Hover
colRipple: Appearance.colors.colLayer1Active
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
toggled: GlobalStates.sidebarLeftOpen
property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0
onPressed: {
GlobalStates.sidebarLeftOpen = !GlobalStates.sidebarLeftOpen;
}
CustomIcon {
id: distroIcon
anchors.centerIn: parent
width: 19.5
height: 19.5
source: Config.options.bar.topLeftIcon == 'distro' ? SystemInfo.distroIcon : `${Config.options.bar.topLeftIcon}-symbolic`
colorize: true
color: Appearance.colors.colOnLayer0
}
}
Item {
Layout.fillHeight: true
}
}
}
ColumnLayout { // Middle section
id: middleSection
anchors.centerIn: parent
spacing: 4
VerticalBarGroup {
padding: 8
Resources {
Layout.fillWidth: true
Layout.fillHeight: false
}
}
HorizontalBarSeparator {
visible: Config.options?.bar.borderless
}
VerticalBarGroup {
id: middleCenterGroup
padding: 6
Layout.fillHeight: true
Workspaces {
id: workspacesWidget
MouseArea {
// Right-click to toggle overview
anchors.fill: parent
acceptedButtons: Qt.RightButton
onPressed: event => {
if (event.button === Qt.RightButton) {
GlobalStates.overviewOpen = !GlobalStates.overviewOpen;
}
}
}
}
}
HorizontalBarSeparator {
visible: Config.options?.bar.borderless
}
VerticalBarGroup {
padding: 8
VerticalClockWidget {
Layout.fillWidth: true
Layout.fillHeight: false
}
BatteryIndicator {
Layout.fillWidth: true
Layout.fillHeight: false
}
}
}
MouseArea { // Bottom section | scroll to change volume
id: barBottomSectionMouseArea
anchors {
left: parent.left
right: parent.right
bottom: parent.bottom
}
implicitWidth: Appearance.sizes.baseVerticalBarWidth
implicitHeight: bottomSectionColumnLayout.implicitHeight
width: Appearance.sizes.verticalBarWidth
property bool hovered: false
property real lastScrollX: 0
property real lastScrollY: 0
property bool trackingScroll: false
acceptedButtons: Qt.LeftButton
hoverEnabled: true
propagateComposedEvents: true
onEntered: event => {
barBottomSectionMouseArea.hovered = true;
}
onExited: event => {
barBottomSectionMouseArea.hovered = false;
barBottomSectionMouseArea.trackingScroll = false;
}
onPressed: event => {
if (event.button === Qt.LeftButton) {
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
} else if (event.button === Qt.RightButton) {
MprisController.activePlayer.next();
}
}
// Scroll to change volume
WheelHandler {
onWheel: event => {
const currentVolume = Audio.value;
const step = currentVolume < 0.1 ? 0.01 : 0.02 || 0.2;
if (event.angleDelta.y < 0)
Audio.sink.audio.volume -= step;
else if (event.angleDelta.y > 0)
Audio.sink.audio.volume = Math.min(1, Audio.sink.audio.volume + step);
// Store the mouse position and start tracking
barBottomSectionMouseArea.lastScrollX = event.x;
barBottomSectionMouseArea.lastScrollY = event.y;
barBottomSectionMouseArea.trackingScroll = true;
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
}
onPositionChanged: mouse => {
if (barBottomSectionMouseArea.trackingScroll) {
const dx = mouse.x - barBottomSectionMouseArea.lastScrollX;
const dy = mouse.y - barBottomSectionMouseArea.lastScrollY;
if (Math.sqrt(dx * dx + dy * dy) > osdHideMouseMoveThreshold) {
GlobalStates.osdVolumeOpen = false;
barBottomSectionMouseArea.trackingScroll = false;
}
}
}
ColumnLayout {
id: bottomSectionColumnLayout
anchors.fill: parent
spacing: 4
Item {
Layout.fillWidth: true
Layout.fillHeight: true
}
Bar.SysTray {
vertical: true
Layout.fillWidth: true
Layout.fillHeight: false
}
RippleButton { // Right sidebar button
id: rightSidebarButton
Layout.alignment: Qt.AlignBottom | Qt.AlignHCenter
Layout.bottomMargin: Appearance.rounding.screenRounding
Layout.fillHeight: false
implicitHeight: indicatorsColumnLayout.implicitHeight + 4 * 2
implicitWidth: indicatorsColumnLayout.implicitWidth + 6 * 2
buttonRadius: Appearance.rounding.full
colBackground: barBottomSectionMouseArea.hovered ? Appearance.colors.colLayer1Hover : ColorUtils.transparentize(Appearance.colors.colLayer1Hover, 1)
colBackgroundHover: Appearance.colors.colLayer1Hover
colRipple: Appearance.colors.colLayer1Active
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
toggled: GlobalStates.sidebarRightOpen
property color colText: toggled ? Appearance.m3colors.m3onSecondaryContainer : Appearance.colors.colOnLayer0
Behavior on colText {
animation: Appearance.animation.elementMoveFast.colorAnimation.createObject(this)
}
onPressed: {
GlobalStates.sidebarRightOpen = !GlobalStates.sidebarRightOpen;
}
ColumnLayout {
id: indicatorsColumnLayout
anchors.centerIn: parent
property real realSpacing: 6
spacing: 0
Revealer {
vertical: true
reveal: Audio.sink?.audio?.muted ?? false
Layout.fillWidth: true
Layout.bottomMargin: reveal ? indicatorsColumnLayout.realSpacing : 0
Behavior on Layout.bottomMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
MaterialSymbol {
text: "volume_off"
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
}
Revealer {
vertical: true
reveal: Audio.source?.audio?.muted ?? false
Layout.fillWidth: true
Layout.bottomMargin: reveal ? indicatorsColumnLayout.realSpacing : 0
Behavior on Layout.topMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
MaterialSymbol {
text: "mic_off"
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
}
Loader {
active: HyprlandXkb.layoutCodes.length > 1
visible: active
Layout.bottomMargin: indicatorsColumnLayout.realSpacing
sourceComponent: StyledText {
text: HyprlandXkb.currentLayoutCode
font.pixelSize: Appearance.font.pixelSize.small
color: rightSidebarButton.colText
}
}
MaterialSymbol {
Layout.bottomMargin: indicatorsColumnLayout.realSpacing
text: Network.materialSymbol
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
MaterialSymbol {
text: Bluetooth.bluetoothConnected ? "bluetooth_connected" : Bluetooth.bluetoothEnabled ? "bluetooth" : "bluetooth_disabled"
iconSize: Appearance.font.pixelSize.larger
color: rightSidebarButton.colText
}
}
}
}
}
}

View file

@ -0,0 +1,37 @@
import qs.modules.common
import QtQuick
import QtQuick.Layouts
Item {
id: root
property real padding: 5
implicitWidth: Appearance.sizes.baseVerticalBarWidth
width: Appearance.sizes.verticalBarWidth
implicitHeight: columnLayout.implicitHeight + padding * 2
default property alias items: columnLayout.children
Rectangle {
id: background
anchors {
fill: parent
leftMargin: 4
rightMargin: 4
}
color: Config.options?.bar.borderless ? "transparent" : Appearance.colors.colLayer1
radius: Appearance.rounding.small
}
ColumnLayout {
id: columnLayout
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
bottom: parent.bottom
topMargin: root.padding
bottomMargin: root.padding
}
spacing: 12
// Children defined by `items` prop
}
}

View file

@ -0,0 +1,43 @@
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Layouts
import "../bar" as Bar
Item {
id: root
property bool borderless: Config.options.bar.borderless
implicitHeight: clockColumn.implicitHeight
implicitWidth: Appearance.sizes.verticalBarWidth
ColumnLayout {
id: clockColumn
anchors.centerIn: parent
spacing: 0
Repeater {
model: DateTime.time.split(/[: ]/)
delegate: StyledText {
required property string modelData
Layout.alignment: Qt.AlignHCenter
font.pixelSize: modelData.match(/am|pm/i) ?
Appearance.font.pixelSize.smaller // Smaller "am"/"pm" text
: Appearance.font.pixelSize.large
color: Appearance.colors.colOnLayer1
text: modelData.padStart(2, "0")
}
}
}
MouseArea {
id: mouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
Bar.ClockWidgetTooltip {
hoverTarget: mouseArea
}
}
}

View file

@ -0,0 +1,313 @@
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.functions
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Widgets
import Qt5Compat.GraphicalEffects
Item {
id: root
property bool borderless: Config.options.bar.borderless
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.QsWindow.window?.screen)
readonly property Toplevel activeWindow: ToplevelManager.activeToplevel
readonly property int workspaceGroup: Math.floor((monitor?.activeWorkspace?.id - 1) / Config.options.bar.workspaces.shown)
property list<bool> workspaceOccupied: []
property int workspaceButtonWidth: 26
property real workspaceIconSize: workspaceButtonWidth * 0.69
property real workspaceIconSizeShrinked: workspaceButtonWidth * 0.55
property real workspaceIconOpacityShrinked: 1
property real workspaceIconMarginShrinked: -4
property int workspaceIndexInGroup: (monitor?.activeWorkspace?.id - 1) % Config.options.bar.workspaces.shown
implicitHeight: columnLayout.implicitHeight + columnLayout.spacing * 2
implicitWidth: Appearance.sizes.verticalBarWidth
property bool showNumbers: false
Timer {
id: showNumbersTimer
interval: (Config?.options.bar.autoHide.showWhenPressingSuper.delay ?? 100)
repeat: false
onTriggered: {
root.showNumbers = true
}
}
Connections {
target: GlobalStates
function onSuperDownChanged() {
if (!Config?.options.bar.autoHide.showWhenPressingSuper.enable) return;
if (GlobalStates.superDown) showNumbersTimer.restart();
else {
showNumbersTimer.stop();
root.showNumbers = false;
}
}
function onSuperReleaseMightTriggerChanged() {
showNumbersTimer.stop()
}
}
// Function to update workspaceOccupied
function updateWorkspaceOccupied() {
workspaceOccupied = Array.from({ length: Config.options.bar.workspaces.shown }, (_, i) => {
return Hyprland.workspaces.values.some(ws => ws.id === workspaceGroup * Config.options.bar.workspaces.shown + i + 1);
})
}
// Occupied workspace updates
Component.onCompleted: updateWorkspaceOccupied()
Connections {
target: Hyprland.workspaces
function onValuesChanged() {
updateWorkspaceOccupied();
}
}
Connections {
target: Hyprland
function onFocusedWorkspaceChanged() {
updateWorkspaceOccupied();
}
}
onWorkspaceGroupChanged: {
updateWorkspaceOccupied();
}
// Scroll to switch workspaces
WheelHandler {
onWheel: (event) => {
if (event.angleDelta.y < 0)
Hyprland.dispatch(`workspace r+1`);
else if (event.angleDelta.y > 0)
Hyprland.dispatch(`workspace r-1`);
}
acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
}
MouseArea {
anchors.fill: parent
acceptedButtons: Qt.BackButton
onPressed: (event) => {
if (event.button === Qt.BackButton) {
Hyprland.dispatch(`togglespecialworkspace`);
}
}
}
// Workspaces - background
ColumnLayout {
id: columnLayout
z: 1
spacing: 0
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
implicitWidth: Appearance.sizes.verticalBarWidth
Repeater {
model: Config.options.bar.workspaces.shown
Rectangle {
z: 1
implicitWidth: workspaceButtonWidth
implicitHeight: workspaceButtonWidth
radius: (width / 2)
property var previousOccupied: (workspaceOccupied[index-1] && !(!activeWindow?.activated && monitor?.activeWorkspace?.id === index))
property var nextOccupied: (workspaceOccupied[index+1] && !(!activeWindow?.activated && monitor?.activeWorkspace?.id === index+2))
property var radiusTop: previousOccupied ? 0 : (width / 2)
property var radiusBottom: nextOccupied ? 0 : (width / 2)
topLeftRadius: radiusTop
topRightRadius: radiusTop
bottomLeftRadius: radiusBottom
bottomRightRadius: radiusBottom
color: ColorUtils.transparentize(Appearance.m3colors.m3secondaryContainer, 0.4)
opacity: (workspaceOccupied[index] && !(!activeWindow?.activated && monitor?.activeWorkspace?.id === index+1)) ? 1 : 0
Behavior on opacity {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on radiusTop {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
Behavior on radiusBottom {
animation: Appearance.animation.elementMove.numberAnimation.createObject(this)
}
}
}
}
// Active workspace
Rectangle {
z: 2
// Make active ws indicator, which has a brighter color, smaller to look like it is of the same size as ws occupied highlight
property real activeWorkspaceMargin: 2
implicitWidth: workspaceButtonWidth - activeWorkspaceMargin * 2
radius: Appearance.rounding.full
color: Appearance.colors.colPrimary
anchors.horizontalCenter: parent.horizontalCenter
property real idx1: workspaceIndexInGroup
property real idx2: workspaceIndexInGroup
y: Math.min(idx1, idx2) * workspaceButtonWidth + activeWorkspaceMargin
implicitHeight: Math.abs(idx1 - idx2) * workspaceButtonWidth + workspaceButtonWidth - activeWorkspaceMargin * 2
Behavior on activeWorkspaceMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on idx1 { // Leading anim
NumberAnimation {
duration: 100
easing.type: Easing.OutSine
}
}
Behavior on idx2 { // Following anim
NumberAnimation {
duration: 300
easing.type: Easing.OutSine
}
}
}
// Workspaces - numbers
ColumnLayout {
id: columnLayoutNumbers
z: 3
spacing: 0
anchors.fill: parent
implicitWidth: Appearance.sizes.verticalBarWidth
Repeater {
model: Config.options.bar.workspaces.shown
Button {
id: button
property int workspaceValue: workspaceGroup * Config.options.bar.workspaces.shown + index + 1
Layout.fillWidth: true
onPressed: Hyprland.dispatch(`workspace ${workspaceValue}`)
height: workspaceButtonWidth
background: Item {
id: workspaceButtonBackground
implicitWidth: workspaceButtonWidth
implicitHeight: workspaceButtonWidth
property var biggestWindow: HyprlandData.biggestWindowForWorkspace(button.workspaceValue)
property var mainAppIconSource: Quickshell.iconPath(AppSearch.guessIcon(biggestWindow?.class), "image-missing")
StyledText { // Workspace number text
opacity: root.showNumbers
|| ((Config.options?.bar.workspaces.alwaysShowNumbers && (!Config.options?.bar.workspaces.showAppIcons || !workspaceButtonBackground.biggestWindow || root.showNumbers))
|| (root.showNumbers && !Config.options?.bar.workspaces.showAppIcons)
) ? 1 : 0
z: 3
anchors.centerIn: parent
horizontalAlignment: Text.AlignHCenter
verticalAlignment: Text.AlignVCenter
font.pixelSize: Appearance.font.pixelSize.small - ((text.length - 1) * (text !== "10") * 2)
text: `${button.workspaceValue}`
elide: Text.ElideRight
color: (monitor?.activeWorkspace?.id == button.workspaceValue) ?
Appearance.m3colors.m3onPrimary :
(workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer :
Appearance.colors.colOnLayer1Inactive)
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
Rectangle { // Dot instead of ws number
id: wsDot
opacity: (Config.options?.bar.workspaces.alwaysShowNumbers
|| root.showNumbers
|| (Config.options?.bar.workspaces.showAppIcons && workspaceButtonBackground.biggestWindow)
) ? 0 : 1
visible: opacity > 0
anchors.centerIn: parent
width: workspaceButtonWidth * 0.18
height: width
radius: width / 2
color: (monitor?.activeWorkspace?.id == button.workspaceValue) ?
Appearance.m3colors.m3onPrimary :
(workspaceOccupied[index] ? Appearance.m3colors.m3onSecondaryContainer :
Appearance.colors.colOnLayer1Inactive)
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
Item { // Main app icon
anchors.centerIn: parent
width: workspaceButtonWidth
height: workspaceButtonWidth
opacity: !Config.options?.bar.workspaces.showAppIcons ? 0 :
(workspaceButtonBackground.biggestWindow && !root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
1 : workspaceButtonBackground.biggestWindow ? workspaceIconOpacityShrinked : 0
visible: opacity > 0
IconImage {
id: mainAppIcon
anchors.bottom: parent.bottom
anchors.right: parent.right
anchors.bottomMargin: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
(workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked
anchors.rightMargin: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ?
(workspaceButtonWidth - workspaceIconSize) / 2 : workspaceIconMarginShrinked
source: workspaceButtonBackground.mainAppIconSource
implicitSize: (!root.showNumbers && Config.options?.bar.workspaces.showAppIcons) ? workspaceIconSize : workspaceIconSizeShrinked
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on anchors.bottomMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on anchors.rightMargin {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on implicitSize {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
Loader {
active: Config.options.bar.workspaces.monochromeIcons
anchors.fill: mainAppIcon
sourceComponent: Item {
Desaturate {
id: desaturatedIcon
visible: false // There's already color overlay
anchors.fill: parent
source: mainAppIcon
desaturation: 0.8
}
ColorOverlay {
anchors.fill: desaturatedIcon
source: desaturatedIcon
color: ColorUtils.transparentize(wsDot.color, 0.9)
}
}
}
}
}
}
}
}
}

View file

@ -22,6 +22,7 @@ import "./modules/screenCorners/"
import "./modules/session/"
import "./modules/sidebarLeft/"
import "./modules/sidebarRight/"
import "./modules/verticalBar/"
import QtQuick
import QtQuick.Window
@ -47,6 +48,7 @@ ShellRoot {
property bool enableSession: true
property bool enableSidebarLeft: true
property bool enableSidebarRight: true
property bool enableVerticalBar: true
// Force initialization of some singletons
Component.onCompleted: {
@ -56,7 +58,7 @@ ShellRoot {
MaterialThemeLoader.reapplyTheme()
}
LazyLoader { active: enableBar; component: Bar {} }
LazyLoader { active: enableBar && Config.ready && !Config.options.bar.vertical; component: Bar {} }
LazyLoader { active: enableBackground; component: Background {} }
LazyLoader { active: enableCheatsheet; component: Cheatsheet {} }
LazyLoader { active: enableDock && Config.options.dock.enable; component: Dock {} }
@ -72,5 +74,6 @@ ShellRoot {
LazyLoader { active: enableSession; component: Session {} }
LazyLoader { active: enableSidebarLeft; component: SidebarLeft {} }
LazyLoader { active: enableSidebarRight; component: SidebarRight {} }
LazyLoader { active: enableVerticalBar && Config.ready && Config.options.bar.vertical; component: VerticalBar {} }
}

View file

@ -134,29 +134,52 @@ ApplicationWindow {
ContentSection {
title: Translation.tr("Bar")
ContentSubsection {
title: "Corner style"
ConfigRow {
ContentSubsection {
title: "Corner style"
ConfigSelectionArray {
currentValue: Config.options.bar.cornerStyle
configOptionName: "bar.cornerStyle"
onSelected: newValue => {
Config.options.bar.cornerStyle = newValue; // Update local copy
}
options: [
{
displayName: Translation.tr("Hug"),
value: 0
},
{
displayName: Translation.tr("Float"),
value: 1
},
{
displayName: Translation.tr("Plain rectangle"),
value: 2
ConfigSelectionArray {
currentValue: Config.options.bar.cornerStyle
configOptionName: "bar.cornerStyle"
onSelected: newValue => {
Config.options.bar.cornerStyle = newValue; // Update local copy
}
]
options: [
{
displayName: Translation.tr("Hug"),
value: 0
},
{
displayName: Translation.tr("Float"),
value: 1
},
{
displayName: Translation.tr("Plain rectangle"),
value: 2
}
]
}
}
ContentSubsection {
title: "Bar layout"
ConfigSelectionArray {
currentValue: Config.options.bar.vertical
configOptionName: "bar.vertical"
onSelected: newValue => {
Config.options.bar.vertical = newValue;
}
options: [
{
displayName: Translation.tr("Horizontal"),
value: false
},
{
displayName: Translation.tr("Vertical"),
value: true
},
]
}
}
}
@ -169,7 +192,7 @@ ApplicationWindow {
}
}
ConfigSwitch {
text: Translation.tr("Place at the bottom")
text: Translation.tr("Place at the bottom/right")
checked: Config.options.bar.bottom
onCheckedChanged: {
Config.options.bar.bottom = checked;