hefty: bar: time: popout

This commit is contained in:
end-4
2026-02-15 17:40:23 +01:00
parent 3447198e13
commit 6f5ab232a6
14 changed files with 739 additions and 406 deletions
@@ -4,10 +4,10 @@ import Quickshell.Io
JsonObject {
property JsonObject bar: JsonObject {
property list<var> leftWidgets: [["HWindowInfo"]]
property list<var> centerLeftWidgets: [["HTime"]]
property list<var> centerWidgets: [["HWorkspaces"]]
property list<var> centerRightWidgets: [["HBattery"]]
property list<var> leftWidgets: ["HWindowInfo"]
property list<var> centerLeftWidgets: ["HTime"]
property list<var> centerWidgets: ["HWorkspaces"]
property list<var> centerRightWidgets: ["HBattery"]
property list<var> rightWidgets: []
property bool m3ExpressiveGrouping: true
}
@@ -4,7 +4,7 @@ import QtQuick
// The former animates faster than the latter, see the NumberAnimations below
QtObject {
id: root
required property int index
property int index
property real idx1: index
property real idx2: index
@@ -2,8 +2,6 @@ pragma ComponentBehavior: Bound
import QtQuick
StyledRectangle {
id: root
property bool vertical: false
property real startRadius
property real endRadius
@@ -0,0 +1,12 @@
import QtQuick
RectangularContainerShape {
property bool vertical: false
property real startRadius
property real endRadius
topLeftRadius: startRadius
topRightRadius: vertical ? startRadius : endRadius
bottomLeftRadius: vertical ? endRadius : startRadius
bottomRightRadius: endRadius
}
@@ -0,0 +1,91 @@
import QtQuick
import qs.modules.common.models as M
import "shapes/material-shapes.js" as MaterialShapes
import "shapes/shapes/corner-rounding.js" as CornerRounding
import "shapes/geometry/offset.js" as Offset
// For returning the points
M.NestableObject {
id: root
required property real width
required property real height
property real radius: 0
property real topLeftRadius: radius
property real topRightRadius: radius
property real bottomLeftRadius: radius
property real bottomRightRadius: radius
property real xOffset: 0
property real yOffset: 0
readonly property real radiusLimit: Math.min(width, height) / 2
readonly property real effectiveTopLeftRadius: Math.min(topLeftRadius, radiusLimit)
readonly property real effectiveTopRightRadius: Math.min(topRightRadius, radiusLimit)
readonly property real effectiveBottomLeftRadius: Math.min(bottomLeftRadius, radiusLimit)
readonly property real effectiveBottomRightRadius: Math.min(bottomRightRadius, radiusLimit)
// Clockwise starting from bottom
property list<var> bottomPoints: [
new MaterialShapes.PointNRound(new Offset.Offset(xOffset + width - effectiveBottomRightRadius, yOffset + height), new CornerRounding.CornerRounding(0)),
new MaterialShapes.PointNRound(new Offset.Offset(xOffset + width / 2, yOffset + height), new CornerRounding.CornerRounding(0)),
new MaterialShapes.PointNRound(new Offset.Offset(xOffset + effectiveBottomLeftRadius, yOffset + height), new CornerRounding.CornerRounding(0)),
]
property list<var> leftPoints: [
new MaterialShapes.PointNRound(new Offset.Offset(xOffset + 0, yOffset + height - effectiveBottomLeftRadius), new CornerRounding.CornerRounding(0)),
new MaterialShapes.PointNRound(new Offset.Offset(xOffset + 0, yOffset + height / 2), new CornerRounding.CornerRounding(0)),
new MaterialShapes.PointNRound(new Offset.Offset(xOffset + 0, yOffset + effectiveTopLeftRadius), new CornerRounding.CornerRounding(0)),
]
property list<var> topPoints: [
new MaterialShapes.PointNRound(new Offset.Offset(xOffset + effectiveTopLeftRadius, yOffset + 0), new CornerRounding.CornerRounding(0)),
new MaterialShapes.PointNRound(new Offset.Offset(xOffset + width / 2, yOffset + 0), new CornerRounding.CornerRounding(0)),
new MaterialShapes.PointNRound(new Offset.Offset(xOffset + width - effectiveTopRightRadius, yOffset + 0), new CornerRounding.CornerRounding(0)),
]
property list<var> rightPoints: [
new MaterialShapes.PointNRound(new Offset.Offset(xOffset + width, yOffset + effectiveTopRightRadius), new CornerRounding.CornerRounding(0)),
new MaterialShapes.PointNRound(new Offset.Offset(xOffset + width, yOffset + height / 2), new CornerRounding.CornerRounding(0)),
new MaterialShapes.PointNRound(new Offset.Offset(xOffset + width, yOffset + height - effectiveBottomRightRadius), new CornerRounding.CornerRounding(0)),
]
function getFirstBottomPoints() {
return bottomPoints.slice(Math.floor(bottomPoints.length / 2))
}
function getLastBottomPoints() {
return bottomPoints.slice(0, Math.floor(bottomPoints.length / 2))
}
function getBottomLeftPoint(extraXOffset = 0, extraYOffset = 0, radius = undefined) {
if (radius === undefined) radius = effectiveBottomLeftRadius;
return new MaterialShapes.PointNRound(new Offset.Offset(xOffset + extraXOffset + 0, yOffset + extraYOffset + height), new CornerRounding.CornerRounding(radius))
}
function getTopLeftPoint(extraXOffset = 0, extraYOffset = 0, radius = undefined) {
if (radius === undefined) radius = effectiveTopLeftRadius;
return new MaterialShapes.PointNRound(new Offset.Offset(xOffset + extraXOffset + 0, yOffset + extraYOffset + 0), new CornerRounding.CornerRounding(radius))
}
function getTopRightPoint(extraXOffset = 0, extraYOffset = 0, radius = undefined) {
if (radius === undefined) radius = effectiveTopRightRadius;
return new MaterialShapes.PointNRound(new Offset.Offset(xOffset + extraXOffset + width, yOffset + extraYOffset + 0), new CornerRounding.CornerRounding(radius))
}
function getBottomRightPoint(extraXOffset = 0, extraYOffset = 0, radius = undefined) {
if (radius === undefined) radius = effectiveBottomRightRadius;
return new MaterialShapes.PointNRound(new Offset.Offset(xOffset + extraXOffset + width, yOffset + extraYOffset + height), new CornerRounding.CornerRounding(radius))
}
function getFullShape() {
const points = [
...getFirstBottomPoints(),
getBottomLeftPoint(),
...leftPoints,
getTopLeftPoint(),
...topPoints,
getTopRightPoint(),
...rightPoints,
getBottomRightPoint(),
...getLastBottomPoints(),
]
return MaterialShapes.customPolygon(points);
}
}
@@ -6,6 +6,7 @@ import qs.modules.common as C
// - osk.sh
// - 3d
// i hope i actually get to this and not shrimply forget
// aaaaa i realized for this to work i would have to make this for shapes in general not just rects
Rectangle {
enum ContentLayer { Background, Pane, Group, Subgroup, Control }
property var contentLayer: StyledRectangle.ContentLayer.Pane // To appropriately add effects like shadows/3d-ization
@@ -1,5 +1,6 @@
import QtQuick
import Quickshell
import qs.services as S
/**
* Abstract morphed panel to be used in TopLayerPanel.
@@ -21,6 +22,7 @@ Item {
// Signals & loading
signal requestFocus()
signal dismissed()
signal focusGrabDismissed()
property bool load: true
property bool shown: true
@@ -32,7 +34,40 @@ Item {
// Main stuff
property var backgroundPolygon
property list<Item> baseMaskItems: [root]
property list<Item> attachedMaskItems: []
property list<Item> maskItems: [...baseMaskItems, ...attachedMaskItems]
property Region maskRegion: Region {
item: root
regions: root.maskItems.map(item => regionComp.createObject(this, { "item": item }))
}
function addAttachedMaskItem(item) {
if (root.attachedMaskItems.includes(item)) return;
root.attachedMaskItems.push(item);
}
function removeAttachedMaskItem(item) {
root.attachedMaskItems = root.attachedMaskItems.filter(i => i !== item);
}
onAttachedMaskItemsChanged: {
if (attachedMaskItems.length > 0) {
S.GlobalFocusGrab.addDismissable(root.QsWindow.window);
} else {
S.GlobalFocusGrab.removeDismissable(root.QsWindow.window);
}
}
Connections {
target: S.GlobalFocusGrab
function onDismissed() {
root.attachedMaskItems = [];
root.focusGrabDismissed();
}
}
Component {
id: regionComp
Region {}
}
}
@@ -29,9 +29,7 @@ PanelWindow {
bottom: true
}
mask: Region {
item: root.currentPanel
}
mask: root.currentPanel.maskRegion
// HyprlandWindow.visibleMask: mask // TODO: use this later to optimize hyprland's rendering
///////////////// Content //////////////////
@@ -28,30 +28,32 @@ Item {
property alias topRightRadius: bg.topRightRadius
property alias bottomLeftRadius: bg.bottomLeftRadius
property alias bottomRightRadius: bg.bottomRightRadius
property real backgroundWidth: root.vertical ? root.backgroundUndirectionalWidth : root.width
property real backgroundHeight: root.vertical ? root.height : root.backgroundUndirectionalWidth
property real fullBackgroundRadius: Math.min(backgroundWidth, backgroundHeight) / 2
function getBackgroundRadius(atSide) {
if (root.m3eRadius) {
if (atSide) return fullBackgroundRadius;
else return C.Appearance.rounding.unsharpenmore;
} else {
return 12;
}
}
W.AxisRectangle {
property Item background: W.AxisRectangle {
id: bg
anchors.centerIn: parent
contentLayer: W.StyledRectangle.ContentLayer.Group
width: root.vertical ? root.backgroundUndirectionalWidth : root.width
height: root.vertical ? root.height : root.backgroundUndirectionalWidth
width: root.backgroundWidth
height: root.backgroundHeight
property real fullRadius: Math.min(width, height) / 2
function getRadius(atSide) {
if (root.m3eRadius) {
if (atSide) return fullRadius;
else return C.Appearance.rounding.unsharpenmore;
} else {
return 12;
}
}
vertical: root.vertical
startRadius: getRadius(root.startSide)
endRadius: getRadius(root.endSide)
startRadius: root.getBackgroundRadius(root.startSide)
endRadius: root.getBackgroundRadius(root.endSide)
}
GridLayout {
property Item contentItem: GridLayout {
id: layout
columns: C.Config.options.bar.vertical ? 1 : -1
anchors.centerIn: parent
@@ -59,4 +61,9 @@ Item {
columnSpacing: spacing
rowSpacing: spacing
}
children: [
background,
contentItem
]
}
@@ -0,0 +1,42 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.modules.common as C
import qs.modules.common.widgets as W
W.StyledRectangle {
id: root
contentLayer: W.StyledRectangle.ContentLayer.Pane
color: C.Appearance.colors.colLayer2Base
transitions: Transition {
AnchorAnimation {
duration: C.Appearance.animation.elementMove.duration
easing.type: C.Appearance.animation.elementMove.type
easing.bezierCurve: C.Appearance.animation.elementMove.bezierCurve
}
ColorAnimation {
duration: C.Appearance.animation.elementMoveFast.duration
easing.type: C.Appearance.animation.elementMoveFast.type
easing.bezierCurve: C.Appearance.animation.elementMoveFast.bezierCurve
}
PropertyAnimation {
properties: "topLeftRadius,topRightRadius,bottomLeftRadius,bottomRightRadius,intendedWidth,intendedHeight"
duration: C.Appearance.animation.elementMove.duration
easing.type: C.Appearance.animation.elementMove.type
easing.bezierCurve: C.Appearance.animation.elementMove.bezierCurve
}
PropertyAnimation {
properties: "opacity"
duration: C.Appearance.animation.elementMoveFast.duration
easing.type: C.Appearance.animation.elementMoveFast.type
easing.bezierCurve: C.Appearance.animation.elementMoveFast.bezierCurve
}
}
W.StyledRectangularShadow {
target: root
z: -1
radius: root.topLeftRadius
}
}
@@ -23,9 +23,9 @@ Repeater {
});
for (var i = 0;i < m.length; i++) {
const item = m[i];
if (item.type === "container") {
item.startSide = (i === 0) || (m[i - 1].type === "component");
item.endSide = (i + 1 >= m.length) || (m[i + 1].type === "component");
if (item.type === "container" || item.type === "component") {
item.startSide = (i === 0) || (m[i - 1].type !== m[i].type);
item.endSide = (i + 1 >= m.length) || (m[i + 1].type !== m[i].type);
}
}
// print(JSON.stringify(m, null, 2));
@@ -43,8 +43,11 @@ Repeater {
roleValue: "component"
delegate: W.UserFallbackLoader {
required property var modelData
required property int index
componentName: modelData.value
context: root.context
property bool startSide: index === 0
property bool endSide: index === root.model.length - 1
}
}
@@ -57,11 +60,15 @@ Repeater {
endSide: modelData.endSide
Repeater {
id: containerRepeater
model: group.modelData.value
delegate: W.UserFallbackLoader {
required property var modelData
required property int index
componentName: modelData
context: root.context
property bool startSide: index === 0
property bool endSide: index === group.modelData.value.length - 1
}
}
}
@@ -0,0 +1,9 @@
import QtQuick
import QtQuick.Layouts
import qs.modules.common as C
import qs.modules.common.widgets as W
HBarGroupContainer {
startSide: parent.startSide ?? true
endSide: parent.endSide ?? true
}
@@ -1,91 +1,226 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.modules.common as C
import qs.modules.common.functions as F
import qs.services as S
import qs.modules.common.widgets as W
import qs.modules.common.widgets.shapes as Shapes
import ".."
import "../../../../common/widgets/shapes/material-shapes.js" as MaterialShapes
import "../../../../common/widgets/shapes/shapes/corner-rounding.js" as CornerRounding
import "../../../../common/widgets/shapes/geometry/offset.js" as Offset
W.ButtonMouseArea {
id: root
HBarWidgetContainer {
id: containerRoot
property bool vertical: C.Config.options.bar.vertical
property bool showPopup: false
// Interactions
property var morphedPanelParent: F.ObjectUtils.findParentWithProperty(root, "maskItems")
Connections {
target: root
function onShowPopupChanged() {
if (root.showPopup) {
morphedPanelParent.addAttachedMaskItem(bgShape);
} else {
morphedPanelParent.removeAttachedMaskItem(bgShape);
}
}
}
Connections {
target: morphedPanelParent
function onFocusGrabDismissed() {
root.showPopup = false;
}
}
property var layoutParent: F.ObjectUtils.findParentWithProperty(root, "startSide")
property real layoutParentTopLeftRadius: layoutParent.topLeftRadius
property real layoutParentTopRightRadius: layoutParent.topRightRadius
property real layoutParentBottomLeftRadius: layoutParent.bottomLeftRadius
property real layoutParentBottomRightRadius: layoutParent.bottomRightRadius
readonly property real barThickness: vertical ? C.Appearance.sizes.verticalBarWidth : C.Appearance.sizes.barHeight
property var activeContent: vertical ? verticalContent : horizontalContent
property real parentRadiusToPaddingRatio: 0.3
implicitWidth: vertical ? barThickness : (activeContent.implicitWidth + (layoutParentTopLeftRadius + layoutParentBottomRightRadius) * parentRadiusToPaddingRatio)
implicitHeight: !vertical ? barThickness : (activeContent.implicitHeight + (layoutParentTopLeftRadius + layoutParentBottomRightRadius) * parentRadiusToPaddingRatio)
Layout.alignment: vertical ? Qt.AlignHCenter : Qt.AlignVCenter
Layout.fillWidth: vertical
Layout.fillHeight: !vertical
onClicked: showPopup = !showPopup
W.StateOverlay {
id: hoverOverlay
anchors.fill: parent
property real parentMargins: 4
property real ownMargins: 2
property real edgeMargins: parentMargins + ownMargins
property real sideMargins: -2
// Background container shape
background: Shapes.ShapeCanvas {
id: bgShape
property real baseTopMargin: (parent.height - containerShape.height) / 2
anchors {
leftMargin: root.vertical ? edgeMargins : sideMargins
rightMargin: root.vertical ? edgeMargins : sideMargins
topMargin: root.vertical ? sideMargins : edgeMargins
bottomMargin: root.vertical ? sideMargins : edgeMargins
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: {
if (!root.atBottom || !root.showPopup) return baseTopMargin;
else return baseTopMargin - popupShape.height - bgShape.spacing;
}
}
topLeftRadius: root.layoutParentTopLeftRadius - ownMargins
topRightRadius: root.layoutParentTopRightRadius - ownMargins
bottomLeftRadius: root.layoutParentBottomLeftRadius - ownMargins
bottomRightRadius: root.layoutParentBottomRightRadius - ownMargins
width: root.showPopup ? Math.max(containerShape.width, popupShape.width) : containerShape.width
height: root.showPopup ? (containerShape.height + popupShape.height + bgShape.spacing) : containerShape.height
color: root.showPopup || progress < 1 ? C.Appearance.colors.colLayer3Base : C.Appearance.colors.colLayer1
// color: "green"
// debug: true
xOffset: (width - containerShape.width) / 2
yOffset: root.atBottom ? (height - containerShape.height) : 0
animation: Anim {}
hover: root.containsMouse
press: root.containsPress
Behavior on width {
Anim {}
}
Behavior on height {
Anim {}
}
Behavior on anchors.topMargin {
Anim {}
}
// Rectangle {
// anchors.fill: parent
// }
polygonIsNormalized: false
property real spacing: baseTopMargin * 2
W.AxisRectangularContainerShape {
id: containerShape
width: containerRoot.backgroundWidth
height: containerRoot.backgroundHeight
startRadius: containerRoot.getBackgroundRadius(containerRoot.startSide)
endRadius: containerRoot.getBackgroundRadius(containerRoot.endSide)
}
W.RectangularContainerShape {
id: popupShape
width: 400 // TODO
height: 500 // TODO
radius: C.Appearance.rounding.large
xOffset: -(width - containerShape.width) / 2
yOffset: root.atBottom ? -(popupShape.height + bgShape.spacing) : (containerShape.height + bgShape.spacing)
}
roundedPolygon: {
if (!root.showPopup) return containerShape.getFullShape()
// return popupShape.getFullShape(); // debug
const points = [
...(root.atBottom ? containerShape.getFirstBottomPoints() : [
...popupShape.getFirstBottomPoints(),
popupShape.getBottomLeftPoint(),
...popupShape.leftPoints,
popupShape.getTopLeftPoint(),
]),
containerShape.getBottomLeftPoint(0, bgShape.spacing * (!root.atBottom ? 1 : 0), containerShape.radiusLimit),
// ...containerShape.leftPoints,
containerShape.getTopLeftPoint(0, bgShape.spacing * (root.atBottom ? -1 : 0), containerShape.radiusLimit),
...(!root.atBottom ? containerShape.topPoints : [
popupShape.getBottomLeftPoint(),
...popupShape.leftPoints,
popupShape.getTopLeftPoint(),
...popupShape.topPoints,
popupShape.getTopRightPoint(),
...popupShape.rightPoints,
popupShape.getBottomRightPoint(),
]),
containerShape.getTopRightPoint(0, bgShape.spacing * (root.atBottom ? -1 : 0), containerShape.radiusLimit),
// ...containerShape.rightPoints,
containerShape.getBottomRightPoint(0, bgShape.spacing * (!root.atBottom ? 1 : 0), containerShape.radiusLimit),
...(root.atBottom ? containerShape.getLastBottomPoints() : [
popupShape.getTopRightPoint(),
...popupShape.rightPoints,
popupShape.getBottomRightPoint(),
...popupShape.getLastBottomPoints(),
]),
];
return MaterialShapes.customPolygon(points);
}
component Anim: SpringAnimation {
spring: 3.5
damping: 0.35
}
}
W.FadeLoader {
id: horizontalContent
anchors.fill: parent
shown: !root.vertical
sourceComponent: RowLayout {
// The button on the bar
W.ButtonMouseArea {
id: root
property bool vertical: C.Config.options.bar.vertical
property bool atBottom: C.Config.options.bar.bottom
property bool showPopup: false
property var layoutParent: F.ObjectUtils.findParentWithProperty(root, "startSide")
property real layoutParentTopLeftRadius: layoutParent.topLeftRadius
property real layoutParentTopRightRadius: layoutParent.topRightRadius
property real layoutParentBottomLeftRadius: layoutParent.bottomLeftRadius
property real layoutParentBottomRightRadius: layoutParent.bottomRightRadius
readonly property real barThickness: vertical ? C.Appearance.sizes.verticalBarWidth : C.Appearance.sizes.barHeight
property var activeContent: vertical ? verticalContent : horizontalContent
property real parentRadiusToPaddingRatio: 0.3
implicitWidth: vertical ? barThickness : (activeContent.implicitWidth + (layoutParentTopLeftRadius + layoutParentBottomRightRadius) * parentRadiusToPaddingRatio)
implicitHeight: !vertical ? barThickness : (activeContent.implicitHeight + (layoutParentTopLeftRadius + layoutParentBottomRightRadius) * parentRadiusToPaddingRatio)
Layout.alignment: vertical ? Qt.AlignHCenter : Qt.AlignVCenter
Layout.fillWidth: vertical
Layout.fillHeight: !vertical
onClicked: showPopup = !showPopup
W.StateOverlay {
id: hoverOverlay
anchors.fill: parent
W.VisuallyCenteredStyledText {
Layout.leftMargin: root.layoutParentTopLeftRadius * root.parentRadiusToPaddingRatio
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
Layout.fillHeight: true
font.pixelSize: C.Appearance.font.pixelSize.large
color: C.Appearance.colors.colOnLayer1
text: S.DateTime.time
property real parentMargins: 4
property real ownMargins: 2
property real edgeMargins: parentMargins + ownMargins
property real sideMargins: -2
anchors {
leftMargin: root.vertical ? edgeMargins : sideMargins
rightMargin: root.vertical ? edgeMargins : sideMargins
topMargin: root.vertical ? sideMargins : edgeMargins
bottomMargin: root.vertical ? sideMargins : edgeMargins
}
topLeftRadius: root.layoutParentTopLeftRadius - ownMargins
topRightRadius: root.layoutParentTopRightRadius - ownMargins
bottomLeftRadius: root.layoutParentBottomLeftRadius - ownMargins
bottomRightRadius: root.layoutParentBottomRightRadius - ownMargins
W.StyledText {
Layout.alignment: Qt.AlignVCenter
font.pixelSize: C.Appearance.font.pixelSize.small
color: C.Appearance.colors.colOnLayer1
text: "•"
}
hover: root.containsMouse
press: root.containsPress
}
W.FadeLoader {
id: horizontalContent
anchors.fill: parent
shown: !root.vertical
sourceComponent: Item {
anchors.fill: parent
implicitWidth: contentLayout.implicitWidth
implicitHeight: contentLayout.implicitHeight
RowLayout {
id: contentLayout
anchors.fill: parent
W.VisuallyCenteredStyledText {
Layout.leftMargin: root.layoutParentTopLeftRadius * root.parentRadiusToPaddingRatio
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
Layout.fillHeight: true
font.pixelSize: C.Appearance.font.pixelSize.large
color: C.Appearance.colors.colOnLayer1
text: S.DateTime.time
}
W.StyledText {
Layout.alignment: Qt.AlignVCenter
font.pixelSize: C.Appearance.font.pixelSize.small
color: C.Appearance.colors.colOnLayer1
text: "•"
}
W.StyledText {
Layout.alignment: Qt.AlignVCenter
font.pixelSize: C.Appearance.font.pixelSize.small
color: C.Appearance.colors.colOnLayer1
text: S.DateTime.longDate
}
}
W.StyledText {
Layout.alignment: Qt.AlignVCenter
font.pixelSize: C.Appearance.font.pixelSize.small
color: C.Appearance.colors.colOnLayer1
text: S.DateTime.longDate
}
}
}
W.FadeLoader {
id: verticalContent
anchors.fill: parent
W.FadeLoader {
id: verticalContent
anchors.fill: parent
}
}
}
@@ -10,297 +10,327 @@ import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Hyprland
import ".."
Item {
id: root
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.QsWindow.window?.screen)
WorkspaceModel {
id: wsModel
monitor: root.monitor
}
property bool vertical: Config.options.bar.vertical
property bool superPressAndHeld: false // Relevant modifications at bottom of file
property real workspaceButtonWidth: 26
property real activeWorkspaceMargin: 2
property real activeWorkspaceSize: workspaceButtonWidth - activeWorkspaceMargin * 2
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) % wsModel.shownCount
property real specialTextSize: workspaceButtonWidth * 0.5
Layout.alignment: vertical ? Qt.AlignHCenter : Qt.AlignVCenter
Layout.fillWidth: vertical
Layout.fillHeight: !vertical
readonly property real barThickness: vertical ? Appearance.sizes.verticalBarWidth : Appearance.sizes.barHeight
implicitWidth: vertical ? barThickness : occupiedIndicators.implicitWidth
implicitHeight: vertical ? occupiedIndicators.implicitHeight : barThickness
property real specialBlur: (wsModel.specialWorkspaceActive && !interactionMouseArea.containsMouse) ? 1 : 0
Behavior on specialBlur {
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
}
HBarWidgetContainer {
Item {
id: regularWorkspaces
anchors.fill: parent
id: root
scale: 1 - 0.08 * root.specialBlur
layer.smooth: true
layer.enabled: root.specialBlur > 0
layer.effect: MultiEffect {
brightness: -0.1 * root.specialBlur
blurEnabled: true
blur: root.specialBlur
blurMax: 32
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(root.QsWindow.window?.screen)
WorkspaceModel {
id: wsModel
monitor: root.monitor
}
/////////////////// Occupied indicators ///////////////////
StyledRectangle {
id: occupiedIndicatorsBg
property bool vertical: Config.options.bar.vertical
property bool superPressAndHeld: false // Relevant modifications at bottom of file
property real workspaceButtonWidth: 26
property real activeWorkspaceMargin: 2
property real activeWorkspaceSize: workspaceButtonWidth - activeWorkspaceMargin * 2
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) % wsModel.shownCount
property real specialTextSize: workspaceButtonWidth * 0.5
Layout.alignment: vertical ? Qt.AlignHCenter : Qt.AlignVCenter
Layout.fillWidth: vertical
Layout.fillHeight: !vertical
readonly property real barThickness: vertical ? Appearance.sizes.verticalBarWidth : Appearance.sizes.barHeight
implicitWidth: vertical ? barThickness : occupiedIndicators.implicitWidth
implicitHeight: vertical ? occupiedIndicators.implicitHeight : barThickness
property real specialBlur: (wsModel.specialWorkspaceActive && !interactionMouseArea.containsMouse) ? 1 : 0
Behavior on specialBlur {
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
}
Item {
id: regularWorkspaces
anchors.fill: parent
contentLayer: StyledRectangle.ContentLayer.Group
color: ColorUtils.transparentize(Appearance.m3colors.m3secondaryContainer, 0.4)
visible: false
}
WorkspaceLayout {
id: occupiedIndicators
anchors.centerIn: parent
layer.enabled: true
visible: false
Repeater {
model: wsModel.shownCount
delegate: Item {
id: wsBg
required property int index
readonly property int wsId: wsModel.getWorkspaceIdAt(index)
property bool currentOccupied: wsModel.occupied[index] && wsId != wsModel.fakeWorkspace
property bool previousOccupied: index > 0 && wsModel.occupied[index - 1] && (wsId - 1) != wsModel.fakeWorkspace
property bool nextOccupied: index < wsModel.shownCount - 1 && wsModel.occupied[index + 1] && (wsId + 1) != wsModel.fakeWorkspace
implicitWidth: root.workspaceButtonWidth
implicitHeight: root.workspaceButtonWidth
// The idea: over-stretch to occupied sides, animate this for a smooth transition.
// masking already prevents weird overlaps
Pill {
property real undirectionalWidth: root.workspaceButtonWidth * wsBg.currentOccupied
property real undirectionalLength: root.workspaceButtonWidth * (1 + 0.5 * wsBg.previousOccupied + 0.5 * wsBg.nextOccupied) * currentOccupied
property real undirectionalOffset: (!wsBg.currentOccupied ? 0.5 : -0.5 * wsBg.previousOccupied) * root.workspaceButtonWidth
anchors.verticalCenter: root.vertical ? undefined : parent.verticalCenter
anchors.horizontalCenter: root.vertical ? parent.horizontalCenter : undefined
x: root.vertical ? 0 : undirectionalOffset
y: root.vertical ? undirectionalOffset : 0
implicitWidth: root.vertical ? undirectionalWidth : undirectionalLength
implicitHeight: root.vertical ? undirectionalLength : undirectionalWidth
Behavior on undirectionalWidth {
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
}
Behavior on undirectionalLength {
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
}
Behavior on undirectionalOffset {
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
}
}
}
}
}
MaskMultiEffect {
id: occupiedIndicatorsMultiEffect
z: 1
anchors.centerIn: parent
implicitWidth: occupiedIndicators.implicitWidth
implicitHeight: occupiedIndicators.implicitHeight
source: occupiedIndicatorsBg
maskSource: occupiedIndicators
}
/////////////////// Active indicator ///////////////////
TrailingIndicator {
id: activeIndicator
anchors.fill: parent
z: 2
index: root.workspaceIndexInGroup
}
/////////////////// Hover ///////////////////
ButtonMouseArea {
id: interactionMouseArea
z: 3
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
property int hoverIndex: {
const position = root.vertical ? mouseY : mouseX;
return Math.floor(position / root.workspaceButtonWidth);
scale: 1 - 0.08 * root.specialBlur
layer.smooth: true
layer.enabled: root.specialBlur > 0
layer.effect: MultiEffect {
brightness: -0.1 * root.specialBlur
blurEnabled: true
blur: root.specialBlur
blurMax: 32
}
onPressed: Hyprland.dispatch(`workspace ${wsModel.getWorkspaceIdAt(hoverIndex)}`)
onWheel: (event) => {
if (event.angleDelta.y < 0)
Hyprland.dispatch(`workspace r+1`);
else if (event.angleDelta.y > 0)
Hyprland.dispatch(`workspace r-1`);
/////////////////// Occupied indicators ///////////////////
StyledRectangle {
id: occupiedIndicatorsBg
anchors.fill: parent
contentLayer: StyledRectangle.ContentLayer.Group
color: ColorUtils.transparentize(Appearance.m3colors.m3secondaryContainer, 0.4)
visible: false
}
TrailingIndicator {
id: interactionIndicator
index: interactionMouseArea.containsMouse ? interactionMouseArea.hoverIndex : root.workspaceIndexInGroup
color: "transparent"
StateOverlay {
id: hoverOverlay
anchors.fill: interactionIndicator.indicatorRectangle
radius: root.activeWorkspaceSize / 2
hover: interactionMouseArea.containsMouse
press: interactionMouseArea.containsPress
drag: true // There are too many layers so we need to force this to be a lil more opaque
contentColor: Appearance.colors.colPrimary
}
}
}
/////////////////// Numbers ///////////////////
WorkspaceLayout {
id: numbersGrid
z: 4
layer.enabled: true // For the masking
Repeater {
model: wsModel.shownCount
delegate: NumberWorkspaceItem {}
}
}
Colorizer {
z: 5
anchors.fill: numbersGrid
colorizationColor: Appearance.colors.colOnPrimary
sourceColor: Appearance.colors.colOnSecondaryContainer
source: activeIndicator
maskEnabled: true
maskSource: numbersGrid
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
/////////////////// App icons ///////////////////
WorkspaceLayout {
id: appsGrid
z: 6
Repeater {
model: wsModel.shownCount
delegate: WorkspaceItem {
id: wsApp
property var biggestWindow: wsModel.biggestWindow[index]
property var mainAppIconSource: Quickshell.iconPath(AppSearch.guessIcon(biggestWindow?.class), "image-missing")
AppIcon {
id: appIcon
property real cornerMargin: (!root.superPressAndHeld && Config.options?.bar.workspaces.showAppIcons && wsApp.biggestWindow) ?
(root.workspaceButtonWidth - root.workspaceIconSize) / 2 : root.workspaceIconMarginShrinked
anchors {
bottom: parent.bottom
right: parent.right
bottomMargin: (parent.implicitHeight - root.workspaceButtonWidth) / 2 + cornerMargin
rightMargin: (parent.implicitWidth - root.workspaceButtonWidth) / 2 + cornerMargin
}
animated: !wsApp.biggestWindow // Prevent the "image-missing" icon
visible: false // Prevent dupe: the colorizer already copies the icon
source: wsApp.mainAppIconSource
implicitSize: NumberUtils.roundToEven(root.workspaceIconSize)
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on cornerMargin {
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
}
}
Circle {
id: iconMask
visible: false
layer.enabled: true
diameter: appIcon.implicitSize
}
Colorizer {
anchors.fill: appIcon
implicitWidth: appIcon.implicitWidth
implicitHeight: appIcon.implicitHeight
colorizationColor: Appearance.m3colors.darkmode ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnPrimary
colorization: Config.options.bar.workspaces.monochromeIcons ? 0.8 : 0.5
brightness: 0
source: appIcon
opacity: !Config.options?.bar.workspaces.showAppIcons ? 0 :
(wsApp.biggestWindow && !root.superPressAndHeld && Config.options?.bar.workspaces.showAppIcons) ?
1 : wsApp.biggestWindow ? root.workspaceIconOpacityShrinked : 0
visible: opacity > 0
scale: ((!root.superPressAndHeld && Config.options?.bar.workspaces.showAppIcons) ? root.workspaceIconSize : root.workspaceIconSizeShrinked) / root.workspaceIconSize
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on scale {
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
}
maskEnabled: true
maskSource: iconMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
}
}
}
}
FadeLoader {
anchors.centerIn: parent
shown: wsModel.specialWorkspaceActive
scale: 0.8 + 0.2 * root.specialBlur
opacity: root.specialBlur
Behavior on opacity {} // Don't animate, as specialBlur is already animated
sourceComponent: Pill {
anchors.centerIn: parent
property real undirectionalWidth: root.activeWorkspaceSize
property real undirectionalLength: {
const base = root.workspaceButtonWidth * Math.min(1.35, wsModel.shownCount) // Who tf only configures only 2 workspaces shown anyway?
if (root.vertical) return base;
return specialWsText.implicitWidth + undirectionalWidth
}
color: Appearance.colors.colPrimary
implicitWidth: root.vertical ? undirectionalWidth : undirectionalLength
implicitHeight: root.vertical ? undirectionalLength : undirectionalWidth
StyledText {
id: specialWsText
WorkspaceLayout {
id: occupiedIndicators
anchors.centerIn: parent
text: (!root.vertical ? wsModel.specialWorkspaceName : "S")
color: Appearance.colors.colOnPrimary
font.pixelSize: root.specialTextSize
layer.enabled: true
visible: false
Repeater {
model: wsModel.shownCount
delegate: Item {
id: wsBg
required property int index
readonly property int wsId: wsModel.getWorkspaceIdAt(index)
property bool currentOccupied: wsModel.occupied[index] && wsId != wsModel.fakeWorkspace
property bool previousOccupied: index > 0 && wsModel.occupied[index - 1] && (wsId - 1) != wsModel.fakeWorkspace
property bool nextOccupied: index < wsModel.shownCount - 1 && wsModel.occupied[index + 1] && (wsId + 1) != wsModel.fakeWorkspace
implicitWidth: root.workspaceButtonWidth
implicitHeight: root.workspaceButtonWidth
// The idea: over-stretch to occupied sides, animate this for a smooth transition.
// masking already prevents weird overlaps
Pill {
property real undirectionalWidth: root.workspaceButtonWidth * wsBg.currentOccupied
property real undirectionalLength: root.workspaceButtonWidth * (1 + 0.5 * wsBg.previousOccupied + 0.5 * wsBg.nextOccupied) * currentOccupied
property real undirectionalOffset: (!wsBg.currentOccupied ? 0.5 : -0.5 * wsBg.previousOccupied) * root.workspaceButtonWidth
anchors.verticalCenter: root.vertical ? undefined : parent.verticalCenter
anchors.horizontalCenter: root.vertical ? parent.horizontalCenter : undefined
x: root.vertical ? 0 : undirectionalOffset
y: root.vertical ? undirectionalOffset : 0
implicitWidth: root.vertical ? undirectionalWidth : undirectionalLength
implicitHeight: root.vertical ? undirectionalLength : undirectionalWidth
Behavior on undirectionalWidth {
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
}
Behavior on undirectionalLength {
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
}
Behavior on undirectionalOffset {
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
}
}
}
}
}
Behavior on undirectionalLength {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
MaskMultiEffect {
id: occupiedIndicatorsMultiEffect
z: 1
anchors.centerIn: parent
implicitWidth: occupiedIndicators.implicitWidth
implicitHeight: occupiedIndicators.implicitHeight
source: occupiedIndicatorsBg
maskSource: occupiedIndicators
}
/////////////////// Active indicator ///////////////////
TrailingIndicator {
id: activeIndicator
anchors.fill: parent
z: 2
index: root.workspaceIndexInGroup
}
/////////////////// Hover ///////////////////
ButtonMouseArea {
id: interactionMouseArea
z: 3
anchors.fill: parent
cursorShape: Qt.PointingHandCursor
hoverEnabled: true
property int hoverIndex: {
const position = root.vertical ? mouseY : mouseX;
return Math.floor(position / root.workspaceButtonWidth);
}
function switchWorkspaceToHovered() {
Hyprland.dispatch(`workspace ${wsModel.getWorkspaceIdAt(hoverIndex)}`)
}
onPressed: switchWorkspaceToHovered()
onWheel: event => {
if (event.angleDelta.y < 0)
Hyprland.dispatch(`workspace r+1`);
else if (event.angleDelta.y > 0)
Hyprland.dispatch(`workspace r-1`);
}
TrailingIndicator {
id: interactionIndicator
index: interactionMouseArea.containsMouse ? interactionMouseArea.hoverIndex : root.workspaceIndexInGroup
color: "transparent"
StateOverlay {
id: hoverOverlay
anchors.fill: interactionIndicator.indicatorRectangle
radius: root.activeWorkspaceSize / 2
hover: interactionMouseArea.containsMouse
press: interactionMouseArea.containsPress
drag: true // There are too many layers so we need to force this to be a lil more opaque
contentColor: Appearance.colors.colPrimary
}
}
}
/////////////////// Numbers ///////////////////
WorkspaceLayout {
id: numbersGrid
z: 4
layer.enabled: true // For the masking
Repeater {
model: wsModel.shownCount
delegate: NumberWorkspaceItem {}
}
}
Colorizer {
z: 5
anchors.fill: numbersGrid
colorizationColor: Appearance.colors.colOnPrimary
sourceColor: Appearance.colors.colOnSecondaryContainer
source: activeIndicator
maskEnabled: true
maskSource: numbersGrid
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
/////////////////// App icons ///////////////////
WorkspaceLayout {
id: appsGrid
z: 6
Repeater {
model: wsModel.shownCount
delegate: WorkspaceItem {
id: wsApp
property var biggestWindow: wsModel.biggestWindow[index]
property var mainAppIconSource: Quickshell.iconPath(AppSearch.guessIcon(biggestWindow?.class), "image-missing")
AppIcon {
id: appIcon
property real cornerMargin: (!root.superPressAndHeld && Config.options?.bar.workspaces.showAppIcons && wsApp.biggestWindow) ? (root.workspaceButtonWidth - root.workspaceIconSize) / 2 : root.workspaceIconMarginShrinked
anchors {
bottom: parent.bottom
right: parent.right
bottomMargin: (parent.implicitHeight - root.workspaceButtonWidth) / 2 + cornerMargin
rightMargin: (parent.implicitWidth - root.workspaceButtonWidth) / 2 + cornerMargin
}
animated: !wsApp.biggestWindow // Prevent the "image-missing" icon
visible: false // Prevent dupe: the colorizer already copies the icon
source: wsApp.mainAppIconSource
implicitSize: NumberUtils.roundToEven(root.workspaceIconSize)
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on cornerMargin {
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
}
}
Circle {
id: iconMask
visible: false
layer.enabled: true
diameter: appIcon.implicitSize
}
Colorizer {
anchors.fill: appIcon
implicitWidth: appIcon.implicitWidth
implicitHeight: appIcon.implicitHeight
colorizationColor: Appearance.m3colors.darkmode ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnPrimary
colorization: Config.options.bar.workspaces.monochromeIcons ? 0.8 : 0.5
brightness: 0
source: appIcon
opacity: !Config.options?.bar.workspaces.showAppIcons ? 0 : (wsApp.biggestWindow && !root.superPressAndHeld && Config.options?.bar.workspaces.showAppIcons) ? 1 : wsApp.biggestWindow ? root.workspaceIconOpacityShrinked : 0
visible: opacity > 0
scale: ((!root.superPressAndHeld && Config.options?.bar.workspaces.showAppIcons) ? root.workspaceIconSize : root.workspaceIconSizeShrinked) / root.workspaceIconSize
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
Behavior on scale {
animation: Appearance.animation.elementMoveSmall.numberAnimation.createObject(this)
}
maskEnabled: true
maskSource: iconMask
maskThresholdMin: 0.5
maskSpreadAtMin: 1
}
}
}
}
}
FadeLoader {
anchors.centerIn: parent
shown: wsModel.specialWorkspaceActive
scale: 0.8 + 0.2 * root.specialBlur
opacity: root.specialBlur
Behavior on opacity {} // Don't animate, as specialBlur is already animated
sourceComponent: Pill {
anchors.centerIn: parent
property real undirectionalWidth: root.activeWorkspaceSize
property real undirectionalLength: {
const base = root.workspaceButtonWidth * Math.min(1.35, wsModel.shownCount); // Who tf only configures only 2 workspaces shown anyway?
if (root.vertical)
return base;
return specialWsText.implicitWidth + undirectionalWidth;
}
color: Appearance.colors.colPrimary
implicitWidth: root.vertical ? undirectionalWidth : undirectionalLength
implicitHeight: root.vertical ? undirectionalLength : undirectionalWidth
StyledText {
id: specialWsText
anchors.centerIn: parent
text: (!root.vertical ? wsModel.specialWorkspaceName : "S")
color: Appearance.colors.colOnPrimary
font.pixelSize: root.specialTextSize
}
Behavior on undirectionalLength {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
}
}
/////////////////// Super key press handling ///////////////////
Timer {
id: superPressAndHeldTimer
interval: (Config?.options.bar.autoHide.showWhenPressingSuper.delay ?? 100)
repeat: false
onTriggered: {
root.superPressAndHeld = true;
}
}
Connections {
target: GlobalStates
function onSuperDownChanged() {
if (!Config?.options.bar.autoHide.showWhenPressingSuper.enable)
return;
if (GlobalStates.superDown)
superPressAndHeldTimer.restart();
else {
superPressAndHeldTimer.stop();
root.superPressAndHeld = false;
}
}
function onSuperReleaseMightTriggerChanged() {
superPressAndHeldTimer.stop();
}
}
}
@@ -334,10 +364,7 @@ Item {
property color contentColor: (wsModel.occupied[wsNum.index] && wsId !== wsModel.fakeWorkspace) ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnLayer1Inactive
FadeLoader {
shown: !(Config.options?.bar.workspaces.alwaysShowNumbers
|| root.superPressAndHeld
|| (Config.options?.bar.workspaces.showAppIcons && wsNum.hasBiggestWindow)
)
shown: !(Config.options?.bar.workspaces.alwaysShowNumbers || root.superPressAndHeld || (Config.options?.bar.workspaces.showAppIcons && wsNum.hasBiggestWindow))
anchors.centerIn: parent
Circle {
anchors.centerIn: parent
@@ -346,10 +373,7 @@ Item {
}
}
FadeLoader {
shown: root.superPressAndHeld
|| ((Config.options?.bar.workspaces.alwaysShowNumbers && (!Config.options?.bar.workspaces.showAppIcons || !wsNum.hasBiggestWindow || root.showNumbers))
|| (root.superPressAndHeld && !Config.options?.bar.workspaces.showAppIcons)
)
shown: root.superPressAndHeld || ((Config.options?.bar.workspaces.alwaysShowNumbers && (!Config.options?.bar.workspaces.showAppIcons || !wsNum.hasBiggestWindow || root.showNumbers)) || (root.superPressAndHeld && !Config.options?.bar.workspaces.showAppIcons))
anchors.centerIn: parent
StyledText {
anchors.centerIn: parent
@@ -397,30 +421,4 @@ Item {
implicitHeight: root.vertical ? indicatorLength : indicatorThickness
}
}
/////////////////// Super key press handling ///////////////////
Timer {
id: superPressAndHeldTimer
interval: (Config?.options.bar.autoHide.showWhenPressingSuper.delay ?? 100)
repeat: false
onTriggered: {
root.superPressAndHeld = true;
}
}
Connections {
target: GlobalStates
function onSuperDownChanged() {
if (!Config?.options.bar.autoHide.showWhenPressingSuper.enable)
return;
if (GlobalStates.superDown)
superPressAndHeldTimer.restart();
else {
superPressAndHeldTimer.stop();
root.superPressAndHeld = false;
}
}
function onSuperReleaseMightTriggerChanged() {
superPressAndHeldTimer.stop();
}
}
}