ported some quickshell stuff from rivendell-hyprdots

This commit is contained in:
Tomas Rivera
2026-03-10 16:22:35 +01:00
parent 4b442dd121
commit b9e3f35abe
80 changed files with 4462 additions and 9 deletions
+59
View File
@@ -0,0 +1,59 @@
import Quickshell
import Quickshell.Io
import QtQuick
import QtQuick.Effects
import "./"
PanelWindow {
property var modelData
screen: modelData
exclusionMode: ExclusionMode.Ignore
aboveWindows: false
color: "transparent"
anchors {
top: true
left: true
bottom: true
right: true
}
Image {
id: bg
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
source: `./assets/${Globals.isDarkTheme ? "darkbg" : "bg"}.png`
smooth: false
}
MultiEffect {
source: bg
anchors.fill: bg
blurEnabled: false
blurMax: 24
blur: 1.0
}
Text {
id: timetxt
x: 900
y: 150
font.family: "BigBlueTermPlusNerdFont"
font.pointSize: 100
font.bold: true
color: Globals.isDarkTheme ? "#9292B6" : "#3B2640"
Process {
id: dateProc
command: ["date", "+%H %M"]
running: true
stdout: SplitParser {
onRead: data => timetxt.text = data
}
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: dateProc.running = true
}
}
}
+35
View File
@@ -0,0 +1,35 @@
pragma Singleton
import QtQuick
import Quickshell
import Quickshell.Io
Singleton {
id: root
readonly property var getThemeProc: Process {
running: true
command: ["gsettings", "get", "org.gnome.desktop.interface", "color-scheme"]
stdout: SplitParser {
onRead: data => root.isDarkTheme = data == "'prefer-dark'"
}
}
property var isDarkTheme: false
readonly property var kittenThemeProc: Process {
command: ["kitten", "theme", "--reload-in=all", `"Gruvbox ${root.isDarkTheme ? "Dark" : "Light"} Soft"`]
}
readonly property var gtk4ThemeProc: Process {
command: ["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", `"prefer-${root.isDarkTheme ? "dark" : "light"}"`]
}
readonly property var gtk3ThemeProc: Process {
command: ["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", `"Adwaita${root.isDarkTheme ? "-dark" : ""}"`]
}
onIsDarkThemeChanged: () => {
kittenThemeProc.command = ["kitten", "theme", "--reload-in=all", `Gruvbox ${root.isDarkTheme ? "Dark" : "Light"} Soft`];
kittenThemeProc.running = true;
gtk4ThemeProc.command = ["gsettings", "set", "org.gnome.desktop.interface", "color-scheme", `"prefer-${root.isDarkTheme ? "dark" : "light"}"`];
gtk4ThemeProc.running = true;
gtk3ThemeProc.command = ["gsettings", "set", "org.gnome.desktop.interface", "gtk-theme", `"Adwaita${root.isDarkTheme ? "-dark" : ""}"`];
gtk3ThemeProc.running = true;
}
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 157 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 176 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 136 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 326 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 842 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 160 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 62 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 383 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 143 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 285 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 11 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 148 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 124 B

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 114 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 141 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 206 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 172 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 312 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 188 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 164 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 220 B

Binary file not shown.
Binary file not shown.
Binary file not shown.

After

Width:  |  Height:  |  Size: 281 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 215 B

+166
View File
@@ -0,0 +1,166 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import QtQuick.Layouts
import Quickshell.Wayland
import "mpris" as Mpris
import "systray" as SysTray
import ".."
PanelWindow {
id: root
property var modelData
readonly property var tooltip: tooltip
readonly property real tooltipYOffset: root.height + 2
Tooltip {
id: tooltip
bar: root
}
function boundedY(targetY: real, height: real): real {
return Math.max(bar.anchors.topMargin + height, Math.min(bar.height + bar.anchors.topMargin - height, targetY));
}
function boundedX(targetX: real, width: real): real {
return Math.max(bar.anchors.leftMargin + width, Math.min(bar.width + bar.anchors.leftMargin - width, targetX));
}
WlrLayershell.namespace: "shell:bar"
anchors {
top: true
left: true
right: true
}
exclusiveZone: 48
implicitHeight: 72
color: "transparent"
mask: barRegion
Region {
id: barRegion
width: bar.width
height: bar.height
}
Item {
id: bar
anchors {
top: parent.top
left: parent.left
right: parent.right
}
implicitHeight: 48
Rectangle {
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
}
implicitWidth: left.width + left.anchors.leftMargin + left.anchors.rightMargin
implicitHeight: left.height
color: "#3B253F"
Rectangle {
z: -1
anchors.fill: parent
color: "#BE850E"
anchors.margins: -4
}
Image {
width: 16 * 6
height: 4 * 6
anchors.top: parent.bottom
anchors.left: parent.left
source: "../assets/bar-decor.png"
smooth: false
}
RowLayout {
id: left
anchors {
top: parent.top
bottom: parent.bottom
left: parent.left
leftMargin: 7
rightMargin: 7
}
spacing: 12
Launcher {}
Workspaces {
bar: root
}
SysTray.SysTray {
bar: root
}
}
}
Rectangle {
anchors {
top: parent.top
bottom: parent.bottom
right: parent.right
}
implicitWidth: right.width + right.anchors.leftMargin + right.anchors.rightMargin
implicitHeight: right.height
color: "#3B253F"
Rectangle {
z: -1
anchors.fill: parent
color: "#BE850E"
anchors.margins: -4
}
Image {
width: 16 * 6
height: 4 * 6
anchors.top: parent.bottom
anchors.right: parent.right
source: "../assets/bar-decor.png"
smooth: false
mirror: true
}
RowLayout {
id: right
anchors {
top: parent.top
bottom: parent.bottom
right: parent.right
leftMargin: 7
rightMargin: 7
}
spacing: 12
Time {}
Volume {
bar: root
}
Mpris.Players {
window: root
bar: root
}
Theme {}
Power {
id: power
bar: root
}
}
}
}
}
+48
View File
@@ -0,0 +1,48 @@
import QtQuick
import QtQuick.Effects
FullwidthMouseArea {
id: root
property bool showPressed: mouseArea.pressed
property real baseMargin: 0
property bool directScale: false
readonly property Item contentItem: mContentItem
default property alias contentItemData: mContentItem.data
property real targetBrightness: root.showPressed ? -25 : root.mouseArea.containsMouse && root.enabled ? 75 : 0
Behavior on targetBrightness {
SmoothedAnimation {
velocity: 600
}
}
property real targetMargins: root.showPressed ? 3 : 0
Behavior on targetMargins {
SmoothedAnimation {
velocity: 25
}
}
hoverEnabled: true
Item {
id: mContentItem
anchors.fill: parent
anchors.margins: root.baseMargin + (root.directScale ? 0 : root.targetMargins)
scale: root.directScale ? (width - root.targetMargins * 2) / width : 1.0
opacity: root.enabled ? 1.0 : 0.5
Behavior on opacity {
SmoothedAnimation {
velocity: 5
}
}
layer.enabled: root.targetBrightness != 0
layer.effect: MultiEffect {
brightness: root.targetBrightness / 1000
}
}
}
+20
View File
@@ -0,0 +1,20 @@
import QtQuick
BarButton {
id: root
property alias image: img.source
property alias mirror: img.mirror
implicitWidth: 24
implicitHeight: 24
Image {
id: img
anchors.fill: parent
smooth: false
scale: root.containsMouse ? 0.9 : 1
Behavior on scale {
NumberAnimation {
duration: 70
}
}
}
}
@@ -0,0 +1,53 @@
import QtQuick
Item {
id: root
property bool fillWindowWidth: false
property real extraVerticalMargin: 0
property alias mouseArea: mouseArea
property alias hoverEnabled: mouseArea.hoverEnabled
property alias acceptedButtons: mouseArea.acceptedButtons
property alias pressedButtons: mouseArea.pressedButtons
property alias containsMouse: mouseArea.containsMouse
property alias isPressed: mouseArea.pressed
signal clicked(event: MouseEvent)
signal entered
signal exited
signal pressed(event: MouseEvent)
signal released(event: MouseEvent)
signal wheel(event: WheelEvent)
MouseArea {
id: mouseArea
anchors {
fill: parent
// not much point in finding exact values
leftMargin: root.fillWindowWidth ? -50 : 0
rightMargin: root.fillWindowWidth ? -50 : 0
topMargin: -root.extraVerticalMargin
bottomMargin: -root.extraVerticalMargin
}
Component.onCompleted: {
this.clicked.connect(root.clicked);
//this.entered.connect(root.entered);
//this.exited.connect(root.exited);
//this.pressed.connect(root.pressed);
this.released.connect(root.released);
//this.wheel.connect(root.wheel);
}
// for some reason MouseArea.pressed is both a prop and signal so connect doesn't work
onPressed: event => root.pressed(event)
// connecting to onwheel seems to implicitly accept it. undo that.
onWheel: event => {
event.accepted = false;
root.wheel(event);
}
}
}
+39
View File
@@ -0,0 +1,39 @@
import QtQuick
import QtQuick.Effects
import Quickshell
import Quickshell.Io
import QtQuick.Layouts
import "../launcher" as Launcher
MouseArea {
id: root
implicitWidth: 26
implicitHeight: 26
acceptedButtons: Qt.LeftButton
Image {
id: img
anchors.fill: parent
source: "../assets/chi-ro.png"
smooth: false
scale: Launcher.Controller.isOpen ? 0.8 : 1
Behavior on scale {
NumberAnimation {
duration: 70
}
}
}
MultiEffect {
source: img
anchors.fill: img
blur: 1.0
brightness: 0.3
blurEnabled: true
}
onPressed: () => {
Launcher.Controller.isOpen = true;
}
}
+43
View File
@@ -0,0 +1,43 @@
import QtQuick
Item {
id: root
property alias mouseArea: mouseArea
property alias hoverEnabled: mouseArea.hoverEnabled
property alias acceptedButtons: mouseArea.acceptedButtons
property alias pressedButtons: mouseArea.pressedButtons
property alias containsMouse: mouseArea.containsMouse
property alias isPressed: mouseArea.pressed
signal clicked(event: MouseEvent)
signal entered
signal exited
signal pressed(event: MouseEvent)
signal released(event: MouseEvent)
signal wheel(event: WheelEvent)
MouseArea {
id: mouseArea
anchors.fill: parent
Component.onCompleted: {
this.clicked.connect(root.clicked);
//this.entered.connect(root.entered);
//this.exited.connect(root.exited);
//this.pressed.connect(root.pressed);
this.released.connect(root.released);
//this.wheel.connect(root.wheel);
}
// For some reason MouseArea.pressed is both a prop and signal so connect doesn't work
onPressed: event => root.pressed(event)
// Connecting to onwheel seems to implicitly accept it. undo that.
onWheel: event => {
event.accepted = false;
root.wheel(event);
}
}
}
+54
View File
@@ -0,0 +1,54 @@
import QtQuick
import QtQuick.Effects
import Quickshell.Services.UPower
import Qt5Compat.GraphicalEffects
Item {
id: root
required property var bar
readonly property var chargeState: UPower.displayDevice.state
readonly property bool isCharging: chargeState == UPowerDeviceState.Charging
readonly property bool isPluggedIn: isCharging || chargeState == UPowerDeviceState.PendingCharge
readonly property real percentage: UPower.displayDevice.percentage
readonly property bool isLow: percentage <= 0.20
readonly property bool isLaptop: UPowerDevice.isLaptopBattery
width: 32
height: 32
Image {
id: mask
source: "../assets/battery-mask.png"
smooth: false
anchors.fill: parent
visible: false
}
Rectangle {
id: progressBar
anchors.fill: parent
color: "black"
Rectangle {
anchors.bottom: parent.bottom
anchors.left: parent.left
anchors.right: parent.right
height: root.isLaptop ? root.percentage * parent.height : parent.height
color: root.isLaptop ? (root.isLow ? "red" : "#EEA939") : "#EEA939"
}
visible: false
}
OpacityMask {
id: pBarMask
anchors.fill: progressBar
source: progressBar
maskSource: mask
}
MultiEffect {
source: pBarMask
anchors.fill: pBarMask
brightness: root.isCharging ? 0.3 : 0
blur: 1.0
blurEnabled: root.isCharging
}
}
+112
View File
@@ -0,0 +1,112 @@
import QtQuick
// kind of like a lighter StackView which handles replacement better.
Item {
id: root
property Component enterTransition: XAnimator {
from: root.width
duration: 3000
}
property Component exitTransition: XAnimator {
to: target.x - target.width
duration: 3000
}
property bool animate: this.visible;
onAnimateChanged: {
if (!this.animate) this.finishAnimations();
}
property Component itemComponent: SlideViewItem {}
property SlideViewItem activeItem: null;
property Item pendingItem: null;
property bool pendingNoAnim: false;
property list<SlideViewItem> removingItems;
readonly property bool animating: activeItem?.activeAnimation != null
function replace(component: Component, defaults: var, noanim: bool) {
this.pendingNoAnim = noanim;
if (component) {
const props = defaults ?? {};
props.parent = null;
props.width = Qt.binding(() => this.width);
props.height = Qt.binding(() => this.height);
const item = component.createObject(this, props);
if (pendingItem) pendingItem.destroy();
pendingItem = item;
const ready = item?.svReady ?? true;
if (ready) finishPending();
} else {
finishPending(); // remove
}
}
Connections {
target: pendingItem
function onSvReadyChanged() {
if (pendingItem.svReady) {
root.finishPending();
}
}
}
function finishPending() {
const noanim = this.pendingNoAnim || !this.animate;
if (this.activeItem) {
if (noanim) {
this.activeItem.destroyAll();
this.activeItem = null;
} else {
removingItems.push(this.activeItem);
this.activeItem.animationCompleted.connect(item => root.removeItem(item));
this.activeItem.stopIfRunning();
this.activeItem.createAnimation(exitTransition);
this.activeItem = null;
}
}
if (!this.animate) finishAnimations();
if (this.pendingItem) {
pendingItem.parent = this;
this.activeItem = itemComponent.createObject(this, { item: this.pendingItem });
this.pendingItem = null;
if (!noanim) {
this.activeItem.createAnimation(enterTransition);
}
}
}
function removeItem(item: SlideViewItem) {
item.destroyAll();
for (const i = 0; i !== this.removingItems.length; i++) {
if (this.removingItems[i] === item) {
removingItems.splice(i, 1);
break;
}
}
}
function finishAnimations() {
this.removingItems.forEach(item => item.destroyAll())
this.removingItems = [];
if (this.activeItem) {
this.activeItem.finishIfRunning();
}
}
Component.onDestruction: {
this.removingItems.forEach(item => item.destroyAll());
this.activeItem?.destroyAll();
this.pendingItem?.destroy();
}
}
+49
View File
@@ -0,0 +1,49 @@
import Quickshell
import QtQuick
QtObject {
id: root
required property Item item
property Animation activeAnimation: null
signal animationCompleted(self: SlideViewItem)
property Connections __animConnection: Connections {
target: activeAnimation
function onStopped() {
root.activeAnimation.destroy();
root.animationCompleted(root);
}
}
function createAnimation(component: Component) {
this.stopIfRunning();
this.activeAnimation = component.createObject(this, {
target: this.item
});
this.activeAnimation.running = true;
}
function stopIfRunning() {
if (this.activeAnimation) {
this.activeAnimation.stop();
this.activeAnimation = null;
}
}
function finishIfRunning() {
if (this.activeAnimation) {
// animator types dont handle complete correctly.
this.activeAnimation.complete();
this.activeAnimation.stop();
this.item.x = 0;
this.item.y = 0;
this.activeAnimation = null;
}
}
function destroyAll() {
this.item.destroy();
this.destroy();
}
}
+31
View File
@@ -0,0 +1,31 @@
import QtQuick
import Quickshell
import QtQuick.Layouts
import "../"
MouseArea {
id: root
implicitWidth: 26
implicitHeight: 26
hoverEnabled: true
acceptedButtons: Qt.LeftButton
Image {
anchors.fill: parent
source: `../assets/${Globals.isDarkTheme ? "sun" : "moon"}.png`
smooth: false
scale: root.containsMouse ? 0.9 : 1
Behavior on scale {
NumberAnimation {
duration: 70
}
}
}
onPressed: () => {
Globals.isDarkTheme = !Globals.isDarkTheme;
}
}
+28
View File
@@ -0,0 +1,28 @@
import QtQuick
import QtQuick.Layouts
import Quickshell.Io
import ".."
Text {
id: timetxt
font.family: "BigBlueTermPlusNerdFont"
font.pointSize: 16
font.bold: true
color: "white"
opacity: 0.5
Process {
id: dateProc
command: ["date", "+%H⯁%M"]
running: true
stdout: SplitParser {
onRead: data => timetxt.text = data
}
}
Timer {
interval: 1000
running: true
repeat: true
onTriggered: dateProc.running = true
}
}
+239
View File
@@ -0,0 +1,239 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Hyprland
import "root:/"
Scope {
id: root
required property var bar
property TooltipItem activeTooltip: null
property TooltipItem activeMenu: null
readonly property TooltipItem activeItem: activeMenu ?? activeTooltip
property TooltipItem lastActiveItem: null
readonly property TooltipItem shownItem: activeItem ?? lastActiveItem
property real hangTime: lastActiveItem?.hangTime ?? 0
property Item tooltipItem: null
onActiveItemChanged: {
if (activeItem != null) {
hangTimer.stop();
activeItem.targetVisible = true;
if (tooltipItem) {
activeItem.parent = tooltipItem;
}
}
if (lastActiveItem != null && lastActiveItem != activeItem) {
if (activeItem != null)
lastActiveItem.targetVisible = false;
else if (root.hangTime == 0)
doLastHide();
else
hangTimer.start();
}
if (activeItem != null)
lastActiveItem = activeItem;
}
function setItem(item: TooltipItem) {
if (item.isMenu) {
activeMenu = item;
} else {
activeTooltip = item;
}
}
function removeItem(item: TooltipItem) {
if (item.isMenu && activeMenu == item) {
activeMenu = null;
} else if (!item.isMenu && activeTooltip == item) {
activeTooltip = null;
}
}
function doLastHide() {
lastActiveItem.targetVisible = false;
}
function onHidden(item: TooltipItem) {
if (item == lastActiveItem) {
lastActiveItem = null;
}
}
Timer {
id: hangTimer
interval: root.hangTime
onTriggered: root.doLastHide()
}
property real scaleMul: lastActiveItem && lastActiveItem.targetVisible ? 1 : 0
Behavior on scaleMul {
SmoothedAnimation {
velocity: 5
}
}
LazyLoader {
id: popupLoader
activeAsync: root.shownItem != null
PopupWindow {
id: popup
anchor {
window: root.bar
rect.y: root.bar.tooltipYOffset
adjustment: PopupAdjustment.None
}
HyprlandWindow.opacity: root.scaleMul
HyprlandWindow.visibleMask: Region {
id: visibleMask
item: tooltipItem
}
Connections {
target: root
function onScaleMulChanged() {
visibleMask.changed();
}
}
//height: bar.height
//implicitWidth: Math.max(700, tooltipItem.largestAnimWidth) // max due to qtwayland glitches
//implicitHeight: Math.max(700, tooltipItem.largestAnimHeight) // max due to qtwayland glitches
visible: true
color: "transparent"
//color: "#20ff0000"
mask: Region {
item: (shownItem?.hoverable ?? false) ? tooltipItem : null
}
HyprlandFocusGrab {
active: activeItem?.isMenu ?? false
windows: [popup, bar, ...(activeItem?.grabWindows ?? [])]
onActiveChanged: {
if (!active && activeItem?.isMenu) {
activeMenu.close();
}
}
}
Item {
id: tooltipItem
Component.onCompleted: {
root.tooltipItem = this;
if (root.shownItem) {
root.shownItem.parent = this;
}
//highestAnimY = targetY - targetHeight / 2;
//lowestAnimY = targetY + targetHeight / 2;
}
transform: Scale {
origin.x: 0
origin.y: tooltipItem.height / 2
xScale: 0.9 + scaleMul * 0.1
yScale: xScale
}
clip: width != targetWidth || height != targetHeight
// bkg
Rectangle {
anchors.fill: parent
color: "#3B253F"
}
readonly property var targetWidth: shownItem?.implicitWidth ?? 0
readonly property var targetHeight: shownItem?.implicitHeight ?? 0
readonly property real targetY: {
if (shownItem == null)
return 0;
const target = bar.contentItem.mapFromItem(shownItem.owner, 0, shownItem.targetRelativeY).y;
return bar.boundedY(target, shownItem.implicitHeight / 2);
}
readonly property real targetX: {
if (root.shownItem == null)
return 0;
const target = root.bar.contentItem.mapFromItem(root.shownItem.owner, 0, root.shownItem.targetRelativeX).x;
return root.bar.boundedX(target, root.shownItem.implicitWidth / 2);
}
property var w: -1
implicitWidth: Math.max(1, w)
property var y1: -1
property var y2: -1
y: y1 - popup.anchor.rect.y
height: y2 - y1
readonly property bool anyAnimsRunning: y1Anim.running || y2Anim.running || widthAnim.running
onAnyAnimsRunningChanged: {
if (!anyAnimsRunning) {
largestAnimWidth = targetWidth;
//highestAnimY = y1;
//lowestAnimY = y2;
}
}
SmoothedAnimation on y1 {
id: y1Anim
to: tooltipItem.targetY - tooltipItem.targetHeight / 2
onToChanged: {
if (tooltipItem.y1 == -1 || !(shownItem?.animateSize ?? true)) {
stop();
tooltipItem.y1 = to;
} else {
velocity = (Math.max(tooltipItem.y1, to) - Math.min(tooltipItem.y1, to)) * 5;
restart();
}
}
}
SmoothedAnimation on y2 {
id: y2Anim
to: tooltipItem.targetY + tooltipItem.targetHeight / 2
onToChanged: {
if (tooltipItem.y2 == -1 || !(shownItem?.animateSize ?? true)) {
stop();
tooltipItem.y2 = to;
} else {
velocity = (Math.max(tooltipItem.y2, to) - Math.min(tooltipItem.y2, to)) * 5;
restart();
}
}
}
SmoothedAnimation on w {
id: widthAnim
to: tooltipItem.targetWidth
onToChanged: {
if (tooltipItem.w == -1 || !(shownItem?.animateSize ?? true)) {
stop();
tooltipItem.w = to;
} else {
velocity = (Math.max(tooltipItem.width, to) - Math.min(tooltipItem.width, to)) * 5;
restart();
}
}
}
}
}
}
}
+274
View File
@@ -0,0 +1,274 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Hyprland
import "root:/"
Scope {
id: root
required property var bar
property TooltipItem activeTooltip: null
property TooltipItem activeMenu: null
readonly property TooltipItem activeItem: activeMenu ?? activeTooltip
property TooltipItem lastActiveItem: null
readonly property TooltipItem shownItem: activeItem ?? lastActiveItem
property real hangTime: lastActiveItem?.hangTime ?? 0
property Item tooltipItem: null
onActiveItemChanged: {
if (activeItem != null) {
hangTimer.stop();
activeItem.targetVisible = true;
if (tooltipItem) {
activeItem.parent = tooltipItem;
}
}
if (lastActiveItem != null && lastActiveItem != activeItem) {
if (activeItem != null)
lastActiveItem.targetVisible = false;
else if (root.hangTime == 0)
doLastHide();
else
hangTimer.start();
}
if (activeItem != null)
lastActiveItem = activeItem;
}
function setItem(item: TooltipItem) {
if (item.isMenu) {
activeMenu = item;
} else {
activeTooltip = item;
}
}
function removeItem(item: TooltipItem) {
if (item.isMenu && activeMenu == item) {
activeMenu = null;
} else if (!item.isMenu && activeTooltip == item) {
activeTooltip = null;
}
}
function doLastHide() {
lastActiveItem.targetVisible = false;
}
function onHidden(item: TooltipItem) {
if (item == lastActiveItem) {
lastActiveItem = null;
}
}
Timer {
id: hangTimer
interval: root.hangTime
onTriggered: doLastHide()
}
property real scaleMul: lastActiveItem && lastActiveItem.targetVisible ? 1 : 0
Behavior on scaleMul {
SmoothedAnimation {
velocity: 5
}
}
LazyLoader {
id: popupLoader
activeAsync: shownItem != null
PopupWindow {
id: popup
anchor {
window: bar
rect.x: tooltipItem.highestAnimX
rect.y: bar.tooltipYOffset
adjustment: PopupAdjustment.None
}
HyprlandWindow.opacity: root.scaleMul
HyprlandWindow.visibleMask: Region {
id: visibleMask
item: tooltipItem
}
Connections {
target: root
function onScaleMulChanged() {
visibleMask.changed();
}
}
//height: bar.height
width: Math.max(700, tooltipItem.largestAnimWidth) // max due to qtwayland glitches
height: {
const h = tooltipItem.lowestAnimY - tooltipItem.highestAnimY;
//console.log(`seth ${h} ${tooltipItem.highestAnimY} ${tooltipItem.lowestAnimY}; ${tooltipItem.y1} ${tooltipItem.y2}`)
return h;
}
visible: true
color: "transparent"
//color: "#20ff0000"
mask: Region {
item: (shownItem?.hoverable ?? false) ? tooltipItem : null
}
HyprlandFocusGrab {
active: root.activeItem?.isMenu ?? false
windows: [popup, root.bar, ...(root.activeItem?.grabWindows ?? [])]
onActiveChanged: {
if (!active && root.activeItem?.isMenu) {
activeMenu.close();
}
}
}
/*Rectangle {
color: "#10ff0000"
//y: tooltipItem.highestAnimY
height: tooltipItem.lowestAnimY - tooltipItem.highestAnimY
width: parent.width
}
Rectangle {
color: "#1000ff00"
//y: tooltipItem.highestAnimY
height: popup.height
width: parent.width
}*/
Item {
id: tooltipItem
Component.onCompleted: {
root.tooltipItem = this;
if (root.shownItem) {
root.shownItem.parent = this;
}
//highestAnimY = targetY - targetHeight / 2;
//lowestAnimY = targetY + targetHeight / 2;
}
transform: Scale {
origin.x: 0
origin.y: tooltipItem.height / 2
xScale: 0.9 + scaleMul * 0.1
yScale: xScale
}
clip: width != targetWidth || height != targetHeight
// bkg
Rectangle {
anchors.fill: parent
color: "red"
}
readonly property var targetWidth: shownItem?.implicitWidth ?? 0
readonly property var targetHeight: shownItem?.implicitHeight ?? 0
property var largestAnimWidth: 0
property var highestAnimY: 0 // unused due to reposition timing issues
property var lowestAnimY: bar.height
onTargetWidthChanged: {
if (targetWidth > largestAnimWidth) {
largestAnimWidth = targetWidth;
}
}
onTargetYChanged: updateYBounds()
onTargetHeightChanged: updateYBounds()
function updateYBounds() {
if (targetY - targetHeight / 2 < highestAnimY)
//highestAnimY = targetY - targetHeight / 2
{}
if (targetY + targetHeight / 2 > lowestAnimY)
//lowestAnimY = targetY + targetHeight / 2
{}
}
readonly property real targetY: {
if (shownItem == null)
return 0;
const target = bar.contentItem.mapFromItem(shownItem.owner, 0, shownItem.targetRelativeY).y;
return bar.boundedY(target, shownItem.implicitHeight / 2);
}
property var w: -1
width: Math.max(1, w)
property var y1: -1
property var y2: -1
y: y1 - popup.anchor.rect.y
height: y2 - y1
readonly property bool anyAnimsRunning: y1Anim.running || y2Anim.running || widthAnim.running
onAnyAnimsRunningChanged: {
if (!anyAnimsRunning) {
largestAnimWidth = targetWidth;
//highestAnimY = y1;
//lowestAnimY = y2;
}
}
SmoothedAnimation on y1 {
id: y1Anim
to: tooltipItem.targetY - tooltipItem.targetHeight / 2
onToChanged: {
if (tooltipItem.y1 == -1 || !(shownItem?.animateSize ?? true)) {
stop();
tooltipItem.y1 = to;
} else {
velocity = (Math.max(tooltipItem.y1, to) - Math.min(tooltipItem.y1, to)) * 5;
restart();
}
}
}
SmoothedAnimation on y2 {
id: y2Anim
to: tooltipItem.targetY + tooltipItem.targetHeight / 2
onToChanged: {
if (tooltipItem.y2 == -1 || !(shownItem?.animateSize ?? true)) {
stop();
tooltipItem.y2 = to;
} else {
velocity = (Math.max(tooltipItem.y2, to) - Math.min(tooltipItem.y2, to)) * 5;
restart();
}
}
}
SmoothedAnimation on w {
id: widthAnim
to: tooltipItem.targetWidth
onToChanged: {
if (tooltipItem.w == -1 || !(shownItem?.animateSize ?? true)) {
stop();
tooltipItem.w = to;
} else {
velocity = (Math.max(tooltipItem.width, to) - Math.min(tooltipItem.width, to)) * 5;
restart();
}
}
}
}
}
}
}
+91
View File
@@ -0,0 +1,91 @@
import QtQuick
import Quickshell
Item {
id: root
required property var tooltip
required property Item owner
property bool isMenu: false
property list<QtObject> grabWindows
property bool hoverable: isMenu
property bool animateSize: true
property bool show: false
property bool preloadBackground: root.visible
property real targetRelativeX: owner.width / 2
property real hangTime: isMenu ? 0 : 200
signal close
readonly property alias contentItem: contentItem
default property alias data: contentItem.data
property Component backgroundComponent: null
onShowChanged: {
if (show)
tooltip.setItem(this);
else
tooltip.removeItem(this);
}
property bool targetVisible: false
property real targetOpacity: 0
opacity: root.targetOpacity * (tooltip.scaleMul == 0 ? 0 : (1.0 / tooltip.scaleMul))
Behavior on targetOpacity {
id: opacityAnimation
SmoothedAnimation {
velocity: 5
}
}
function snapOpacity(opacity: real) {
opacityAnimation.enabled = false;
targetOpacity = opacity;
opacityAnimation.enabled = true;
}
onTargetVisibleChanged: {
if (targetVisible) {
visible = true;
targetOpacity = 1;
} else {
close();
targetOpacity = 0;
}
}
onTargetOpacityChanged: {
if (!targetVisible && targetOpacity == 0) {
visible = false;
this.parent = null;
if (tooltip)
tooltip.onHidden(this);
}
}
anchors.fill: parent
visible: false
//clip: true
implicitHeight: contentItem.implicitHeight + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
implicitWidth: contentItem.implicitWidth + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
readonly property Item item: contentItem
Loader {
anchors.fill: parent
active: root.backgroundComponent && (root.visible || root.preloadBackground)
asynchronous: !root.visible && root.preloadBackground
sourceComponent: backgroundComponent
}
Item {
id: contentItem
anchors.fill: parent
anchors.margins: 5
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
}
}
@@ -0,0 +1,92 @@
import QtQuick
import Quickshell
Item {
id: root
required property var tooltip
required property Item owner
property bool isMenu: false
property list<QtObject> grabWindows
property bool hoverable: isMenu
property bool animateSize: true
property bool show: false
property bool preloadBackground: root.visible
property real targetRelativeY: owner.height / 2
property real hangTime: isMenu ? 0 : 200
signal close
readonly property alias contentItem: contentItem
default property alias data: contentItem.data
property Component backgroundComponent: null
onShowChanged: {
console.log(this);
if (show)
tooltip.setItem(this);
else
tooltip.removeItem(this);
}
property bool targetVisible: false
property real targetOpacity: 0
opacity: root.targetOpacity * (tooltip.scaleMul == 0 ? 0 : (1.0 / tooltip.scaleMul))
Behavior on targetOpacity {
id: opacityAnimation
SmoothedAnimation {
velocity: 5
}
}
function snapOpacity(opacity: real) {
opacityAnimation.enabled = false;
targetOpacity = opacity;
opacityAnimation.enabled = true;
}
onTargetVisibleChanged: {
if (targetVisible) {
visible = true;
targetOpacity = 1;
} else {
close();
targetOpacity = 0;
}
}
onTargetOpacityChanged: {
if (!targetVisible && targetOpacity == 0) {
visible = false;
this.parent = null;
if (tooltip)
tooltip.onHidden(this);
}
}
anchors.fill: parent
visible: false
//clip: true
implicitHeight: contentItem.implicitHeight + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
implicitWidth: contentItem.implicitWidth + contentItem.anchors.leftMargin + contentItem.anchors.rightMargin
readonly property Item item: contentItem
Loader {
anchors.fill: parent
active: root.backgroundComponent && (root.visible || root.preloadBackground)
asynchronous: !root.visible && root.preloadBackground
sourceComponent: root.backgroundComponent
}
Item {
id: contentItem
anchors.fill: parent
anchors.margins: 5
implicitHeight: children[0].implicitHeight
implicitWidth: children[0].implicitWidth
}
}
+133
View File
@@ -0,0 +1,133 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Controls
import Quickshell
import Quickshell.Services.Pipewire
import Quickshell.Widgets
import "mpris"
MouseArea {
id: root
required property var bar
property var node: Pipewire.defaultAudioSink
PwObjectTracker {
objects: [root.node]
}
implicitWidth: 26
implicitHeight: 26
hoverEnabled: true
acceptedButtons: Qt.LeftButton
//enabled: MprisController.canChangeVolume
opacity: enabled ? 1 : 0.5
Image {
anchors.fill: parent
source: `../assets/${root.node.audio.muted ? "noaudio" : "audio"}.png`
smooth: false
scale: popupLoader.active ? 0.9 : 1
Behavior on scale {
NumberAnimation {
duration: 70
}
}
}
onClicked: node.audio.muted = !node.audio.muted
LazyLoader {
id: popupLoader
property bool popupContainsMouse
active: root.enabled && (root.containsMouse || popupContainsMouse)
Behavior on active {
NumberAnimation {
duration: 100
}
}
PopupWindow {
id: popup
visible: true
anchor.window: root.bar
width: 200
height: 32
color: "#9B5B36"
Connections {
target: root
function onXChanged() {
popup.updatePos();
}
}
Component.onCompleted: updatePos()
function updatePos() {
const pos = root.mapToGlobal(root.width / 2 - popup.width / 2, root.height);
popup.anchor.rect.x = pos.x;
popup.anchor.rect.y = pos.y + 5;
}
Slider {
id: slider
anchors {
left: parent.left
right: parent.right
verticalCenter: parent.verticalCenter
}
anchors.margins: 5
from: 0
to: 1
value: root.node.audio.volume
onValueChanged: root.node.audio.volume = value
background: Rectangle {
x: slider.leftPadding
y: slider.topPadding + slider.availableHeight / 2 - height / 2
implicitHeight: 16
width: slider.availableWidth
height: implicitHeight
color: "#824524"
opacity: node.audio.muted ? 0.5 : 1
Rectangle {
anchors.margins: 5
x: anchors.leftMargin
y: anchors.topMargin
width: slider.visualPosition * (parent.width - anchors.leftMargin - anchors.rightMargin)
height: parent.height - anchors.topMargin - anchors.bottomMargin
color: "#8D804B"
}
}
handle: Rectangle {
x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width)
y: slider.topPadding + slider.availableHeight / 2 - height / 2
implicitWidth: 12
implicitHeight: 12
rotation: 45
color: "#8D804B"
}
}
MouseArea {
id: popupMouseArea
anchors.fill: parent
hoverEnabled: true
acceptedButtons: Qt.NoButton
propagateComposedEvents: true
onEntered: popupLoader.popupContainsMouse = true
onExited: popupLoader.popupContainsMouse = false
}
}
}
}
+122
View File
@@ -0,0 +1,122 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell.Hyprland
MouseArea {
id: root
required property var bar
property int wsBaseIndex: 1
property int wsCount: 7
readonly property HyprlandMonitor monitor: Hyprland.monitorFor(bar.screen)
property int currentIndex: 0
property int existsCount: 0
signal workspaceAdded(workspace: HyprlandWorkspace)
implicitWidth: row.implicitWidth
implicitHeight: row.implicitHeight
acceptedButtons: Qt.NoButton
onWheel: e => {
e.accepted = true;
const step = -Math.sign(e.angleDelta.y);
const targetWs = currentIndex + step;
if (targetWs >= wsBaseIndex && targetWs < wsBaseIndex + wsCount) {
Hyprland.dispatch(`workspace ${targetWs}`);
}
}
RowLayout {
id: row
anchors.fill: parent
spacing: 8
Repeater {
model: root.wsCount
MouseArea {
id: wsItem
required property int index
property int wsIndex: root.wsBaseIndex + index
property HyprlandWorkspace workspace: null
property bool exists: workspace != null
property bool active: (root.monitor?.activeWorkspace ?? false) && root.monitor.activeWorkspace == workspace
implicitWidth: 26
implicitHeight: 26
acceptedButtons: Qt.LeftButton
onPressed: Hyprland.dispatch(`workspace ${wsIndex}`)
onExistsChanged: {
root.existsCount += exists ? 1 : -1;
}
onActiveChanged: {
if (active)
root.currentIndex = wsIndex;
}
Connections {
target: root
function onWorkspaceAdded(workspace: HyprlandWorkspace) {
if (workspace.id == wsItem.wsIndex) {
wsItem.workspace = workspace;
}
}
}
Rectangle {
id: fill
anchors.fill: parent
anchors.margins: 2
radius: width / 2
color: wsItem.exists ? "#C0DB4C" : "#59622D"
}
Image {
anchors.fill: parent
smooth: false
source: "../assets/workspace.png"
}
MultiEffect {
source: fill
anchors.fill: fill
blur: 1.0
brightness: 0.3
blurEnabled: true
visible: wsItem.active
opacity: wsItem.active ? 1.0 : 0
Behavior on opacity {
NumberAnimation {
duration: 100
}
}
}
}
}
}
Connections {
target: Hyprland.workspaces
function onObjectInsertedPost(workspace) {
root.workspaceAdded(workspace);
}
}
Component.onCompleted: {
Hyprland.workspaces.values.forEach(workspace => {
root.workspaceAdded(workspace);
});
}
}
@@ -0,0 +1,165 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQml.Models
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Services.Mpris
Singleton {
id: root
property MprisPlayer trackedPlayer: null
property MprisPlayer activePlayer: trackedPlayer ?? Mpris.players.values[0] ?? null
signal trackChanged(reverse: bool)
property bool __reverse: false
property var activeTrack
Instantiator {
model: Mpris.players
Connections {
required property MprisPlayer modelData
target: modelData
Component.onCompleted: {
if (root.trackedPlayer == null || modelData.isPlaying) {
root.trackedPlayer = modelData;
}
}
Component.onDestruction: {
if (root.trackedPlayer == null || !root.trackedPlayer.isPlaying) {
for (const player of Mpris.players.values) {
if (player.playbackState.isPlaying) {
root.trackedPlayer = player;
break;
}
}
if (trackedPlayer == null && Mpris.players.values.length != 0) {
trackedPlayer = Mpris.players.values[0];
}
}
}
function onPlaybackStateChanged() {
if (root.trackedPlayer !== modelData)
root.trackedPlayer = modelData;
}
}
}
Connections {
target: activePlayer
function onPostTrackChanged() {
root.updateTrack();
}
function onTrackArtUrlChanged() {
//root.updateTrack();
if (root.activePlayer.uniqueId == root.activeTrack.uniqueId && root.activePlayer.trackArtUrl != root.activeTrack.artUrl) {
// cantata likes to send cover updates *BEFORE* updating the track info.
// as such, art url changes shouldn't be able to break the reverse animation
const r = root.__reverse;
root.updateTrack();
root.__reverse = r;
}
}
}
onActivePlayerChanged: this.updateTrack()
function updateTrack() {
//console.log(`update: ${this.activePlayer?.trackTitle ?? ""} : ${this.activePlayer?.trackArtists}`)
this.activeTrack = {
uniqueId: this.activePlayer?.uniqueId ?? 0,
artUrl: this.activePlayer?.trackArtUrl ?? "",
title: this.activePlayer?.trackTitle || "Unknown Title",
artist: this.activePlayer?.trackArtist || "Unknown Artist",
album: this.activePlayer?.trackAlbum || "Unknown Album"
};
this.trackChanged(__reverse);
this.__reverse = false;
}
property bool isPlaying: this.activePlayer && this.activePlayer.isPlaying
property bool canTogglePlaying: this.activePlayer?.canTogglePlaying ?? false
function togglePlaying() {
if (this.canTogglePlaying)
this.activePlayer.togglePlaying();
}
property bool canGoPrevious: this.activePlayer?.canGoPrevious ?? false
function previous() {
if (this.canGoPrevious) {
this.__reverse = true;
this.activePlayer.previous();
}
}
property bool canGoNext: this.activePlayer?.canGoNext ?? false
function next() {
if (this.canGoNext) {
this.__reverse = false;
this.activePlayer.next();
}
}
property bool canChangeVolume: this.activePlayer && this.activePlayer.volumeSupported && this.activePlayer.canControl
property bool loopSupported: this.activePlayer && this.activePlayer.loopSupported && this.activePlayer.canControl
property var loopState: this.activePlayer?.loopState ?? MprisLoopState.None
function setLoopState(loopState: var) {
if (this.loopSupported) {
this.activePlayer.loopState = loopState;
}
}
property bool shuffleSupported: this.activePlayer && this.activePlayer.shuffleSupported && this.activePlayer.canControl
property bool hasShuffle: this.activePlayer?.shuffle ?? false
function setShuffle(shuffle: bool) {
if (this.shuffleSupported) {
this.activePlayer.shuffle = shuffle;
}
}
function setActivePlayer(player: MprisPlayer) {
const targetPlayer = player ?? Mpris.players[0];
console.log(`setactive: ${targetPlayer} from ${activePlayer}`);
if (targetPlayer && this.activePlayer) {
this.__reverse = Mpris.players.indexOf(targetPlayer) < Mpris.players.indexOf(this.activePlayer);
} else {
// always animate forward if going to null
this.__reverse = false;
}
this.trackedPlayer = targetPlayer;
}
IpcHandler {
target: "mpris"
function pauseAll(): void {
for (const player of Mpris.players.values) {
if (player.canPause)
player.pause();
}
}
function playPause(): void {
root.togglePlaying();
}
function previous(): void {
root.previous();
}
function next(): void {
root.next();
}
}
}
+151
View File
@@ -0,0 +1,151 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Services.Mpris
import "../.."
import ".."
MouseArea {
id: root
required property var window
required property var bar
readonly property var activePlayer: MprisController.activePlayer
property var isPopupOpen: false
implicitWidth: column.implicitWidth + 10
Layout.fillHeight: true
acceptedButtons: Qt.RightButton
onClicked: isPopupOpen = !isPopupOpen
Item {
id: widget
anchors.fill: parent
scale: root.isPopupOpen ? 0.92 : 1
Behavior on scale {
NumberAnimation {
duration: 70
}
}
implicitHeight: column.implicitHeight + 10
RowLayout {
id: column
anchors {
fill: parent
}
ClickableIcon {
acceptedButtons: Qt.LeftButton
implicitWidth: 20
implicitHeight: 20
image: "../../assets/fastforward.png"
mirror: true
hoverEnabled: true
enabled: MprisController.canGoPrevious
onClicked: MprisController.previous()
scale: containsMouse ? 0.9 : 1
Behavior on scale {
NumberAnimation {
duration: 70
}
}
}
ClickableIcon {
acceptedButtons: Qt.LeftButton
implicitWidth: 26
implicitHeight: 26
Layout.leftMargin: -4
Layout.rightMargin: -4
image: `../../assets/${MprisController.isPlaying ? "pause" : "play"}.png`
hoverEnabled: true
enabled: MprisController.canTogglePlaying
onClicked: MprisController.togglePlaying()
}
ClickableIcon {
acceptedButtons: Qt.LeftButton
implicitWidth: 20
implicitHeight: 20
image: "../../assets/fastforward.png"
hoverEnabled: true
enabled: MprisController.canGoNext
onClicked: MprisController.next()
scale: containsMouse ? 0.9 : 1
Behavior on scale {
NumberAnimation {
duration: 70
}
}
}
}
Connections {
target: root
function onIsPopupOpenChanged() {
root.window.focusable = root.isPopupOpen;
}
}
LazyLoader {
id: popupLoader
active: root.isPopupOpen
// Leave time for exit anim
Behavior on active {
enabled: popupLoader.active
NumberAnimation {
duration: 1000
}
}
PlayersPopup {
id: popup
visible: true
isOpen: root.isPopupOpen
anchor.window: root.window
anchor.rect.x: root.window.width
anchor.rect.y: 0
HyprlandFocusGrab {
active: root.isPopupOpen
windows: [popup]
}
contentItem {
focus: true
Keys.onEscapePressed: root.isPopupOpen = false
}
}
}
}
component CenteredText: Item {
Layout.fillWidth: true
property alias text: label.text
property alias font: label.font
Label {
id: label
visible: text != ""
anchors.centerIn: parent
elide: Text.ElideRight
width: Math.min(parent.width - 20, implicitWidth)
font.family: "BigBlueTermPlusNerdFont"
color: "white"
}
}
}
+529
View File
@@ -0,0 +1,529 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Qt5Compat.GraphicalEffects
import Quickshell
import Quickshell.Widgets
import Quickshell.Wayland
import Quickshell.Hyprland
import Quickshell.Services.Mpris
import "../.."
import "../../components"
import ".."
PopupWindow {
id: root
property bool isOpen: false
implicitWidth: 700
//implicitHeight: popupContent.implicitHeight + popupContent.anchors.margins * 2
implicitHeight: 800
mask: Region {
item: wrapper
}
color: "transparent"
property Scope positionInfo: Scope {
id: positionInfo
property int position: Math.floor(MprisController.activePlayer.position)
property int length: Math.floor(MprisController.activePlayer.length)
FrameAnimation {
id: posTracker
running: MprisController.activePlayer.isPlaying
onTriggered: MprisController.activePlayer.positionChanged()
}
function timeStr(time: int): string {
const seconds = time % 60;
const minutes = Math.floor(time / 60);
return `${minutes}:${seconds.toString().padStart(2, '0')}`;
}
}
Rope {
segmentCount: 4
segmentLen: 22
start: Qt.vector2d(250, -5)
end: Qt.vector2d(wrapper.x + 49, wrapper.y)
}
Rope {
segmentCount: 4
segmentLen: 22
start: Qt.vector2d(wrapper.width + 200 - 50, -5)
end: Qt.vector2d(wrapper.width + wrapper.x - 49, wrapper.y)
}
Rectangle {
id: wrapper
implicitWidth: 500
implicitHeight: popupContent.implicitHeight + popupContent.anchors.margins * 2
color: "#824524"
x: 200
y: -wrapper.height
readonly property var targetX: 200
readonly property var targetY: root.isOpen ? 70 : -wrapper.height - 50
property var velocityX: 0
property var velocityY: 0
FrameAnimation {
running: true
function dampingVelocity(currentVelocity, delta) {
const spring = 8.0;
const damping = 0.3;
const springForce = spring * delta;
const dampingForce = -damping * currentVelocity;
return currentVelocity + (springForce + dampingForce);
}
onTriggered: {
const deltaX = wrapper.targetX - wrapper.x;
const deltaY = wrapper.targetY - wrapper.y;
if (Math.abs(deltaX) > 0.1 || Math.abs(deltaY) > 0.1) {
wrapper.velocityX = dampingVelocity(wrapper.velocityX, deltaX);
wrapper.velocityY = dampingVelocity(wrapper.velocityY, deltaY);
wrapper.x += wrapper.velocityX * frameTime;
wrapper.y += wrapper.velocityY * frameTime;
}
}
}
MouseArea {
property var prevMouseX: 0
property var prevMouseY: 0
anchors.fill: parent
onPressed: e => {
prevMouseX = e.x;
prevMouseY = e.y;
}
onPositionChanged: e => {
wrapper.x += (e.x - prevMouseX) * 0.3;
wrapper.y += (e.y - prevMouseY) * 0.3;
prevMouseX = e.x;
prevMouseY = e.y;
}
}
Item {
anchors.fill: parent
anchors.margins: 8
clip: true
Image {
scale: 10
anchors.fill: parent
source: "../../assets/wood.png"
smooth: false
fillMode: Image.Tile
opacity: 0.4
}
}
ColumnLayout {
id: popupContent
anchors.fill: parent
anchors.margins: 8
Connections {
target: MprisController
function onTrackChanged(reverse: bool) {
trackStack.updateTrack(reverse, false);
}
}
Item {
id: playerSelectorContainment
Layout.fillWidth: true
implicitHeight: playerSelector.implicitHeight
implicitWidth: playerSelector.implicitWidth
Rectangle {
anchors.centerIn: parent
implicitWidth: 50
implicitHeight: 50
color: "#824524"
}
RowLayout {
id: playerSelector
property Item selectedPlayerDisplay: Mpris.players[0]
x: parent.width / 2 - (selectedPlayerDisplay ? selectedPlayerDisplay.x + selectedPlayerDisplay.width / 2 : 0)
anchors.verticalCenter: parent.verticalCenter
Behavior on x {
NumberAnimation {
duration: 400
easing.type: Easing.OutExpo
}
}
Repeater {
model: Mpris.players
MouseArea {
required property MprisPlayer modelData
readonly property bool selected: modelData == MprisController.activePlayer
onSelectedChanged: () => {
if (selected) {
playerSelector.selectedPlayerDisplay = this;
}
}
implicitWidth: childrenRect.width
implicitHeight: childrenRect.height
onClicked: MprisController.setActivePlayer(modelData)
Item {
width: 50
height: 50
Image {
anchors.fill: parent
anchors.margins: 0
source: {
// So janky xD
const identity = modelData.identity.toLowerCase();
if (identity.includes("chrome") || identity.includes("chromium")) {
return Quickshell.iconPath("google-chrome") || Quickshell.iconPath("chromium");
}
if (identity.includes("firefox")) {
return Quickshell.iconPath("firefox");
}
if (identity.includes("spotify")) {
return Quickshell.iconPath("spotify");
}
if (modelData.identity == "Brave") {
return "/opt/brave-bin/product_logo_64.png";
}
const entry = DesktopEntries.byId(modelData.desktopEntry);
if (entry && entry.icon) {
return Quickshell.iconPath(entry.icon);
}
return "../../assets/mpd.png";
}
smooth: false
sourceSize.width: 50
sourceSize.height: 50
cache: false
}
}
}
}
}
}
Item {
Layout.fillWidth: true
Layout.topMargin: 8
Layout.bottomMargin: 16
Label {
anchors.centerIn: parent
text: MprisController.activePlayer.identity
font.family: "BigBlueTermPlusNerdFont"
color: "white"
}
}
SlideView {
id: trackStack
Layout.fillWidth: true
implicitHeight: 100
clip: animating || (lastFlicked?.contentX ?? 0) != 0
property Flickable lastFlicked
property bool reverse: false
Component.onCompleted: updateTrack(false, true)
function updateTrack(reverse: bool, immediate: bool) {
this.reverse = reverse;
this.replace(trackComponent, {
track: MprisController.activeTrack
}, immediate);
}
property var trackComponent: Component {
Flickable {
id: flickable
required property var track
readonly property bool svReady: img.status === Image.Ready
contentWidth: width + 1
onDragStarted: trackStack.lastFlicked = this
onDragEnded: {
if (Math.abs(contentX) > 75) {
if (contentX < 0)
MprisController.previous();
else if (contentX > 0)
MprisController.next();
}
}
RowLayout {
anchors.fill: parent
spacing: 8
Rectangle {
Layout.alignment: Qt.AlignVCenter | Qt.AlignHCenter
implicitHeight: 128
implicitWidth: 128
color: "transparent"
Image {
id: img
anchors.fill: parent
fillMode: Image.PreserveAspectCrop
visible: flickable.track.artUrl != ""
source: flickable.track.artUrl
cache: false
asynchronous: true
}
}
ColumnLayout {
Layout.fillWidth: true
Layout.alignment: Qt.AlignVCenter
Label {
text: flickable.track.title
font.pointSize: albumLabel.font.pointSize + 1
font.family: "BigBlueTermPlusNerdFont"
color: "white"
elide: Text.ElideRight
Layout.maximumWidth: Math.min(300, implicitWidth)
}
Label {
id: albumLabel
text: flickable.track.album
opacity: 0.8
font.family: "BigBlueTermPlusNerdFont"
color: "white"
Layout.maximumWidth: Math.min(300, implicitWidth)
}
Label {
text: flickable.track.artist
opacity: 0.8
font.family: "BigBlueTermPlusNerdFont"
color: "white"
Layout.maximumWidth: Math.min(300, implicitWidth)
}
}
}
}
}
readonly property real fromPos: trackStack.width * (trackStack.reverse ? -1 : 1)
enterTransition: PropertyAnimation {
property: "x"
from: trackStack.fromPos
to: 0
duration: 350
easing.type: Easing.OutExpo
}
exitTransition: PropertyAnimation {
property: "x"
to: target.x - trackStack.fromPos
duration: 350
easing.type: Easing.OutExpo
}
}
Item {
Layout.fillWidth: true
implicitHeight: controlsRow.implicitHeight
RowLayout {
id: controlsRow
anchors.centerIn: parent
ClickableIcon {
Layout.leftMargin: 2
Layout.rightMargin: 2
implicitWidth: 24
implicitHeight: 24
enabled: MprisController.loopSupported
image: {
switch (MprisController.loopState) {
case MprisLoopState.None:
return "../../assets/repeat-off.png";
case MprisLoopState.Playlist:
return "../../assets/repeat.png";
case MprisLoopState.Track:
return "../../assets/repeat-once.png";
}
}
onClicked: {
let target = MprisLoopState.None;
switch (MprisController.loopState) {
case MprisLoopState.None:
target = MprisLoopState.Playlist;
break;
case MprisLoopState.Playlist:
target = MprisLoopState.Track;
break;
case MprisLoopState.Track:
target = MprisLoopState.None;
break;
}
MprisController.setLoopState(target);
}
}
ClickableIcon {
Layout.leftMargin: 2
Layout.rightMargin: 2
implicitWidth: 32
implicitHeight: 32
enabled: MprisController.canGoPrevious
image: "../../assets/fastforward.png"
mirror: true
onClicked: MprisController.previous()
}
ClickableIcon {
implicitWidth: 42
implicitHeight: 42
enabled: MprisController.canTogglePlaying
image: `../../assets/${MprisController.isPlaying ? "pause" : "play"}.png`
onClicked: MprisController.togglePlaying()
}
ClickableIcon {
Layout.leftMargin: 2
Layout.rightMargin: 2
implicitWidth: 32
implicitHeight: 32
enabled: MprisController.canGoNext
image: "../../assets/fastforward.png"
onClicked: MprisController.next()
}
ClickableIcon {
Layout.leftMargin: 2
Layout.rightMargin: 2
implicitWidth: 24
implicitHeight: 24
enabled: MprisController.shuffleSupported
image: `../../assets/${MprisController.hasShuffle ? "shuffle" : "noshuffle"}.png`
onClicked: MprisController.setShuffle(!MprisController.hasShuffle)
}
}
}
RowLayout {
Layout.margins: 5
Label {
Layout.preferredWidth: lengthLabel.implicitWidth
text: positionInfo.timeStr(positionInfo.position)
font.family: "BigBlueTermPlusNerdFont"
color: "white"
}
Slider {
id: slider
property bool bindSlider: true
property real boundAnimStart: 0
property real boundAnimFactor: 1
property real lastPosition: 0
property real lastLength: 0
property real boundPosition: {
const ppos = MprisController.activePlayer.position / MprisController.activePlayer.length;
const bpos = boundAnimStart;
return (ppos * boundAnimFactor) + (bpos * (1.0 - boundAnimFactor));
}
Layout.fillWidth: true
enabled: MprisController.activePlayer.canSeek
from: 0
to: 1
background: Rectangle {
x: slider.leftPadding
y: slider.topPadding + slider.availableHeight / 2 - height / 2
implicitHeight: 24
width: slider.availableWidth
height: implicitHeight
color: "#824524"
Rectangle {
anchors.margins: 8
x: anchors.leftMargin
y: anchors.topMargin
width: slider.visualPosition * (parent.width - anchors.leftMargin - anchors.rightMargin)
height: parent.height - anchors.topMargin - anchors.bottomMargin
color: "#8D804B"
}
}
handle: Rectangle {
x: slider.leftPadding + slider.visualPosition * (slider.availableWidth - width)
y: slider.topPadding + slider.availableHeight / 2 - height / 2
implicitWidth: 16
implicitHeight: 16
rotation: 45
color: "#8D804B"
}
Connections {
target: MprisController.activePlayer
function onPositionChanged() {
slider.lastPosition = MprisController.activePlayer.position;
slider.lastLength = MprisController.activePlayer.length;
}
}
onPressedChanged: () => {
if (!pressed)
MprisController.activePlayer.position = value * MprisController.activePlayer.length;
bindSlider = !pressed;
}
Binding {
when: slider.bindSlider
slider.value: slider.boundPosition
}
}
Label {
id: lengthLabel
text: positionInfo.timeStr(positionInfo.length)
font.family: "BigBlueTermPlusNerdFont"
color: "white"
}
}
}
}
Image {
source: "../../assets/rope-tie.png"
smooth: false
width: 32
height: 32
x: wrapper.x + 49 - width / 2
y: wrapper.y - height / 2
}
Image {
source: "../../assets/rope-tie.png"
smooth: false
width: 32
height: 32
x: wrapper.width + wrapper.x - 49 - width / 2
y: wrapper.y - height / 2
}
}
@@ -0,0 +1,44 @@
import QtQuick
import QtQuick.Shapes
import "../.."
Rectangle {
property var checkState: Qt.Unchecked
implicitHeight: 18
implicitWidth: 18
radius: 3
color: checkState == Qt.Checked ? "#C5E3EE" : "#90ACAD"
Shape {
visible: checkState == Qt.Checked
anchors.fill: parent
layer.enabled: true
layer.samples: 10
ShapePath {
strokeWidth: 2
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.RoundJoin
fillColor: "transparent"
startX: start.x
startY: start.y
PathLine {
id: start
x: width * 0.8
y: height * 0.2
}
PathLine {
x: width * 0.35
y: height * 0.8
}
PathLine {
x: width * 0.2
y: height * 0.6
}
}
}
}
@@ -0,0 +1,56 @@
import QtQuick
import QtQuick.Shapes
import Quickshell
Item {
property bool expanded: false;
readonly property bool open: progress != 0;
readonly property bool animating: internalProgress != (expanded ? 101 : -1);
implicitHeight: 16
implicitWidth: 16
property var xStart: Math.round(width * 0.3)
property var yStart: Math.round(height * 0.1)
property var xEnd: Math.round(width * 0.7)
property var yEnd: Math.round(height * 0.9)
property real internalProgress: expanded ? 101 : -1;
Behavior on internalProgress { SmoothedAnimation { velocity: 300 } }
EasingCurve {
id: curve
curve.type: Easing.InOutQuad
}
readonly property real progress: curve.valueAt(Math.min(100, Math.max(internalProgress, 0)) * 0.01)
rotation: progress * 90;
Shape {
anchors.fill: parent
layer.enabled: true
layer.samples: 3
ShapePath {
strokeWidth: 2
capStyle: ShapePath.RoundCap
joinStyle: ShapePath.MiterJoin
fillColor: "transparent"
startX: xStart
startY: yStart
PathLine {
x: xEnd
y: height / 2
}
PathLine {
y: yEnd
x: xStart
}
}
}
}
+118
View File
@@ -0,0 +1,118 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import Quickshell.DBusMenu
import "../.."
MouseArea {
id: root
required property QsMenuEntry entry
property alias expanded: childrenRevealer.expanded
property bool animating: childrenRevealer.animating || (childMenuLoader?.item?.animating ?? false)
// appears it won't actually create the handler when only used from MenuItemList.
onExpandedChanged: {}
onAnimatingChanged: {}
signal close
implicitWidth: row.implicitWidth + 4
implicitHeight: row.implicitHeight + 4
hoverEnabled: true
onClicked: {
if (entry.hasChildren)
childrenRevealer.expanded = !childrenRevealer.expanded;
else {
entry.triggered();
if (entry.toggleType == ToggleButtonType.None)
close();
}
}
Rectangle {
anchors.fill: parent
visible: root.containsMouse || childrenRevealer.expanded
color: "#824524"
}
ColumnLayout {
id: row
anchors.fill: parent
anchors.margins: 2
spacing: 0
RowLayout {
id: innerRow
Item {
implicitWidth: 22
implicitHeight: 22
MenuCheckBox {
anchors.centerIn: parent
visible: entry.buttonType == QsMenuButtonType.CheckBox
checkState: entry.checkState
}
MenuRadioButton {
anchors.centerIn: parent
visible: entry.buttonType == QsMenuButtonType.RadioButton
checkState: entry.checkState
}
MenuChildrenRevealer {
id: childrenRevealer
anchors.centerIn: parent
visible: entry.hasChildren
onOpenChanged: entry.showChildren = open
}
}
Text {
text: entry.text
color: entry.enabled ? "white" : "#bbbbbb"
}
Item {
Layout.fillWidth: true
implicitWidth: 22
implicitHeight: 22
IconImage {
anchors.right: parent.right
anchors.verticalCenter: parent.verticalCenter
source: entry.icon
visible: source != ""
implicitSize: parent.height
}
}
}
Loader {
id: childMenuLoader
Layout.fillWidth: true
Layout.preferredHeight: active ? item.implicitHeight * childrenRevealer.progress : 0
readonly property real widthDifference: {
Math.max(0, (item?.implicitWidth ?? 0) - innerRow.implicitWidth);
}
Layout.preferredWidth: active ? innerRow.implicitWidth + (widthDifference * childrenRevealer.progress) : 0
active: root.expanded || root.animating
clip: true
sourceComponent: MenuView {
id: childrenList
menu: entry
onClose: root.close()
anchors {
top: parent.top
left: parent.left
right: parent.right
}
}
}
}
}
@@ -0,0 +1,19 @@
import QtQuick
import "../.."
Rectangle {
property var checkState: Qt.Unchecked
implicitHeight: 18
implicitWidth: 18
radius: width / 2
color: checkState == Qt.Checked ? "#C5E3EE" : "#90ACAD"
Rectangle {
x: parent.width * 0.25
y: parent.height * 0.25
visible: checkState == Qt.Checked
width: parent.width * 0.5
height: width
radius: width / 2
}
}
+73
View File
@@ -0,0 +1,73 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
ColumnLayout {
id: root
property alias menu: menuView.menu
property Item animatingItem: null
property bool animating: animatingItem != null
signal close
signal submenuExpanded(item: var)
QsMenuOpener {
id: menuView
}
spacing: 0
Repeater {
model: menuView.children
Loader {
required property var modelData
property var item: Component {
BoundComponent {
id: itemComponent
source: "MenuItem.qml"
property var entry: modelData
function onClose() {
root.close();
}
function onExpandedChanged() {
if (item.expanded)
root.submenuExpanded(item);
}
function onAnimatingChanged() {
if (item.animating) {
root.animatingItem = this;
} else if (root.animatingItem == this) {
root.animatingItem = null;
}
}
Connections {
target: root
function onSubmenuExpanded(expandedItem) {
if (item != expandedItem)
item.expanded = false;
}
}
}
}
property var separator: Component {
Rectangle {
color: "#824524"
implicitHeight: 4
}
}
sourceComponent: modelData.isSeparator ? separator : item
Layout.fillHeight: true
Layout.fillWidth: true
}
}
}
+143
View File
@@ -0,0 +1,143 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import Quickshell
import Quickshell.Services.SystemTray
import ".."
import "../.."
import "../../components"
Rectangle {
id: root
required property var bar
implicitWidth: row.implicitWidth + 10
Layout.fillHeight: true
color: "transparent"
RowLayout {
id: row
implicitWidth: childrenRect.width
spacing: 5
anchors {
fill: parent
margins: 5
}
Repeater {
model: SystemTray.items
Item {
id: item
required property SystemTrayItem modelData
property bool targetMenuOpen: false
Layout.fillHeight: true
implicitWidth: height
Image {
anchors {
top: parent.top
bottom: parent.bottom
horizontalCenter: parent.horizontalCenter
}
width: height
source: item.modelData.icon
scale: item.targetMenuOpen ? 0.8 : 1
MouseArea {
id: mouseArea
anchors.fill: parent
acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
// showPressed: item.targetMenuOpen || (pressedButtons & ~Qt.RightButton)
// fillWindowWidth: true
// extraVerticalMargin: row.spacing / 2
onClicked: event => {
event.accepted = true;
if (event.button == Qt.LeftButton) {
item.modelData.activate();
} else if (event.button == Qt.MiddleButton) {
item.modelData.secondaryActivate();
}
}
onPressed: event => {
if (event.button == Qt.RightButton && item.modelData.hasMenu) {
item.targetMenuOpen = !item.targetMenuOpen;
}
}
onWheel: event => {
event.accepted = true;
const points = event.angleDelta.y / 120;
item.modelData.scroll(points, false);
}
// property var tooltip: TooltipItem {
// tooltip: root.bar.tooltip
// owner: mouseArea
//
// show: mouseArea.containsMouse
//
// Text {
// id: tooltipText
// text: item.modelData.tooltipTitle != "" ? item.modelData.tooltipTitle : item.modelData.id
// color: "white"
// }
// }
LazyLoader {
active: item.targetMenuOpen
PopupWindow {
id: rightclickMenu
anchor.window: root.bar
visible: true
Component.onCompleted: () => {
root.bar.focusable = true;
const pos = mouseArea.mapToGlobal(mouseArea.x, mouseArea.y);
anchor.rect.x = pos.x;
anchor.rect.y = pos.y + mouseArea.height;
}
Connections {
target: item
function onTargetMenuOpenChanged() {
root.bar.focusable = false;
}
}
contentItem {
focus: true
Keys.onEscapePressed: item.targetMenuOpen = false
}
implicitWidth: Math.max(1, menuView.implicitWidth)
implicitHeight: Math.max(1, menuView.implicitHeight)
color: "transparent"
Rectangle {
anchors.fill: parent
color: "#9B5B36"
MenuView {
id: menuView
anchors.fill: parent
menu: item.modelData.menu
onClose: item.targetMenuOpen = false
}
}
}
}
}
}
}
}
}
}
+98
View File
@@ -0,0 +1,98 @@
import QtQuick
import QtQuick.Shapes
Item {
id: root
required property vector2d start
required property vector2d end
property double segmentCount: 8
property double segmentLen: 50
property alias color: path.strokeColor
readonly property Component p: PathLine {
property vector2d pos
property vector2d prevPos: pos
property vector2d acc
x: pos.x
y: pos.y
}
readonly property double gravity: 5000
readonly property int constraintRunCount: 40
Shape {
id: shape
anchors.fill: parent
preferredRendererType: Shape.CurveRenderer
ShapePath {
id: path
capStyle: ShapePath.RoundCap
strokeColor: "#DAC99B"
strokeWidth: 10
fillColor: "transparent"
}
}
Component.onCompleted: () => {
// Create the points
const xInc = (end.x - start.x) / segmentCount;
const yInc = (end.y - start.y) / segmentCount;
let i = 0;
while (i < segmentCount) {
path.pathElements.push(p.createObject(root, {
pos: start.plus(Qt.vector2d(xInc * i, yInc * i)),
acc: Qt.vector2d(0, root.gravity)
}));
i++;
}
// Set up the first and last points
path.pathElements[0].acc.y = 0;
path.pathElements[segmentCount - 1].acc.y = 0;
path.startX = path.pathElements[0].x;
path.startY = path.pathElements[0].y;
}
FrameAnimation {
running: true
onTriggered: () => {
const dt = frameTime;
for (const p of path.pathElements.slice(1, root.segmentCount - 2)) {
// https://en.wikipedia.org/wiki/Verlet_integration
const newPos = p.pos.times(2.0).minus(p.prevPos).plus(p.acc.times(dt * dt));
p.prevPos = p.pos;
p.pos = newPos;
}
for (let i = 0; i < root.segmentCount - 1; i++) {
const cur = path.pathElements[i];
const next = path.pathElements[i + 1];
// Keep fixed distance between segments
for (let j = 0; j < root.constraintRunCount; j++) {
const toNext = next.pos.minus(cur.pos);
const distToNext = toNext.length();
const error = root.segmentLen - distToNext;
const pull = toNext.times(1.0 / distToNext).times(error).times(0.1);
if (i != 0) {
cur.pos = cur.pos.minus(pull.times(0.5));
next.pos = next.pos.plus(pull.times(0.5));
} else {
next.pos = next.pos.plus(pull);
}
}
}
path.pathElements[0].pos = root.start;
path.pathElements[root.segmentCount - 1].pos = root.end;
path.startX = path.pathElements[0].x;
path.startY = path.pathElements[0].y;
}
}
}
+16
View File
@@ -0,0 +1,16 @@
import Quickshell
import QtQuick
FloatingWindow {
visible: true
width: 200
height: 100
Text {
anchors.centerIn: parent
// centers it within parent
text: "Hello world!"
color: "#0db9d7"
font.pixelSize: 18
}
}
+40
View File
@@ -0,0 +1,40 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import "." as Launcher
Singleton {
id: root
property bool isOpen: false
property var lastUsedTimes: ({})
IpcHandler {
target: "launcher"
function open() {
root.isOpen = true;
}
function close() {
root.isOpen = false;
}
function toggle() {
root.isOpen = !root.isOpen;
}
}
LazyLoader {
active: root.isOpen
Launcher.Overlay {
controller: root
}
}
// Empty function to define first reference to singleton
function init() {
}
}
+302
View File
@@ -0,0 +1,302 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Effects
import QtQuick.Layouts
import Quickshell
import Quickshell.Wayland
import Quickshell.Widgets
import ".."
PanelWindow {
id: root
required property var controller
exclusionMode: ExclusionMode.Ignore
anchors {
left: true
right: true
top: true
bottom: true
}
color: "transparent"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
WlrLayershell.namespace: "shell:launcher"
Rectangle {
id: main
anchors.centerIn: parent
height: 7 + searchContainer.implicitHeight + list.topMargin + list.bottomMargin + Math.min(list.contentHeight, list.delegateHeight * 10)
Behavior on height {
NumberAnimation {
duration: 200
easing.type: Easing.OutCubic
}
}
width: 450
color: Globals.isDarkTheme ? "#393536" : "#f3eac7"
ColumnLayout {
anchors.fill: parent
anchors.margins: 7
anchors.bottomMargin: 0
spacing: 0
Rectangle {
id: searchContainer
Layout.fillWidth: true
implicitHeight: search.implicitHeight + 10
color: Globals.isDarkTheme ? "#44000000" : "#11000000"
TextInput {
id: search
anchors.verticalCenter: parent.verticalCenter
anchors.left: parent.left
anchors.right: parent.right
anchors.leftMargin: 4
color: Globals.isDarkTheme ? "#D6C8A6" : "#403D3B"
font.family: "BigBlueTermPlusNerdFont"
focus: true
Keys.forwardTo: [list]
Keys.onEscapePressed: root.controller.isOpen = false
Keys.onPressed: event => {
if (event.modifiers & Qt.ControlModifier) {
if (event.key == Qt.Key_J) {
list.currentIndex = list.currentIndex == list.count - 1 ? 0 : list.currentIndex + 1;
event.accepted = true;
} else if (event.key == Qt.Key_K) {
list.currentIndex = list.currentIndex == 0 ? list.count - 1 : list.currentIndex - 1;
event.accepted = true;
}
}
}
onAccepted: {
if (list.currentItem) {
list.currentItem.clicked(null);
}
}
onTextChanged: {
list.currentIndex = 0;
}
}
}
ListView {
id: list
Layout.fillWidth: true
Layout.fillHeight: true
clip: true
cacheBuffer: 0 // works around QTBUG-131106
//reuseItems: true
model: ScriptModel {
values: DesktopEntries.applications.values.map(object => {
const stxt = search.text.toLowerCase();
const ntxt = object.name.toLowerCase();
let si = 0;
let ni = 0;
let matches = [];
let startMatch = -1;
for (let si = 0; si != stxt.length; ++si) {
const sc = stxt[si];
while (true) {
// Drop any entries with letters that don't exist in order
if (ni == ntxt.length)
return null;
const nc = ntxt[ni++];
if (nc == sc) {
if (startMatch == -1)
startMatch = ni;
break;
} else {
if (startMatch != -1) {
matches.push({
index: startMatch,
length: ni - startMatch
});
startMatch = -1;
}
}
}
}
if (startMatch != -1) {
matches.push({
index: startMatch,
length: ni - startMatch + 1
});
}
return {
object: object,
matches: matches
};
}).filter(entry => entry !== null).sort((a, b) => {
if (!search.text) {
if (controller.lastUsedTimes[a.object.id])
print(controller.lastUsedTimes[a.object.id]);
let aDate = controller.lastUsedTimes[a.object.id] ?? new Date(0);
let bDate = controller.lastUsedTimes[b.object.id] ?? new Date(0);
return bDate - aDate;
}
let ai = 0;
let bi = 0;
let s = 0;
while (ai != a.matches.length && bi != b.matches.length) {
const am = a.matches[ai];
const bm = b.matches[bi];
s = bm.length - am.length;
if (s != 0)
return s;
s = am.index - bm.index;
if (s != 0)
return s;
++ai;
++bi;
}
s = a.matches.length - b.matches.length;
if (s != 0)
return s;
s = a.object.name.length - b.object.name.length;
if (s != 0)
return s;
return a.object.name.localeCompare(b.object.name);
}).map(entry => entry.object)
onValuesChanged: list.currentIndex = 0
}
topMargin: 7
bottomMargin: list.count == 0 ? 0 : 7
add: Transition {
NumberAnimation {
property: "opacity"
from: 0
to: 1
duration: 100
}
}
displaced: Transition {
NumberAnimation {
property: "y"
duration: 200
easing.type: Easing.OutCubic
}
NumberAnimation {
property: "opacity"
to: 1
duration: 100
}
}
move: Transition {
NumberAnimation {
property: "y"
duration: 200
easing.type: Easing.OutCubic
}
NumberAnimation {
property: "opacity"
to: 1
duration: 100
}
}
remove: Transition {
NumberAnimation {
property: "y"
duration: 200
easing.type: Easing.OutCubic
}
NumberAnimation {
property: "opacity"
to: 0
duration: 100
}
}
highlight: Rectangle {
color: Globals.isDarkTheme ? "#20e0ffff" : "#11000000"
}
keyNavigationEnabled: true
keyNavigationWraps: true
highlightMoveVelocity: -1
highlightMoveDuration: 100
preferredHighlightBegin: list.topMargin
preferredHighlightEnd: list.height - list.bottomMargin
highlightRangeMode: ListView.ApplyRange
snapMode: ListView.SnapToItem
readonly property real delegateHeight: 44
delegate: MouseArea {
required property DesktopEntry modelData
implicitHeight: list.delegateHeight
implicitWidth: ListView.view.width
onClicked: {
controller.lastUsedTimes[modelData.id] = new Date();
modelData.execute();
root.controller.isOpen = false;
}
RowLayout {
id: delegateLayout
anchors {
verticalCenter: parent.verticalCenter
left: parent.left
leftMargin: 5
}
IconImage {
Layout.alignment: Qt.AlignVCenter
asynchronous: true
implicitSize: 30
source: Quickshell.iconPath(modelData.icon)
}
Text {
text: modelData.name
font.family: "BigBlueTermPlusNerdFont"
color: Globals.isDarkTheme ? "#D6C8A6" : "#403D3B"
Layout.alignment: Qt.AlignVCenter
}
}
}
}
}
}
MultiEffect {
source: main
anchors.fill: main
shadowBlur: 0
shadowEnabled: true
shadowColor: "#33000000"
shadowVerticalOffset: 16
shadowHorizontalOffset: 12
}
}
+56
View File
@@ -0,0 +1,56 @@
import QtQuick
import Quickshell
import Quickshell.Services.Pam
Scope {
id: root
signal pamSuccess
signal unlocked
signal failed
// These properties are in the context and not individual lock surfaces
// so all surfaces can share the same state.
property string currentText: ""
property bool unlockInProgress: false
property bool showFailure: false
// Clear the failure text once the user starts typing.
onCurrentTextChanged: showFailure = false
function tryUnlock() {
if (currentText === "")
return;
root.unlockInProgress = true;
pam.start();
}
PamContext {
id: pam
// Its best to have a custom pam config for quickshell, as the system one
// might not be what your interface expects, and break in some way.
// This particular example only supports passwords.
configDirectory: "pam"
config: "password.conf"
// pam_unix will ask for a response for the password prompt
onPamMessage: {
if (this.responseRequired) {
this.respond(root.currentText);
}
}
// pam_unix won't send any important messages so all we need is the completion status.
onCompleted: result => {
if (result == PamResult.Success) {
root.pamSuccess();
} else {
root.currentText = "";
root.showFailure = true;
}
root.unlockInProgress = false;
}
}
}
+47
View File
@@ -0,0 +1,47 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import QtQuick
import "." as Lock
Singleton {
Process {
id: screencopyCmd
running: false
command: ["grim", "-s", "0.5", "-t", "jpeg", "/tmp/lock_screencopy.jpeg"]
onExited: lock.locked = true
}
IpcHandler {
target: "lockscreen"
function lock(): void {
screencopyCmd.running = true;
}
}
WlSessionLock {
id: lock
locked: false
WlSessionLockSurface {
id: lockSurface
color: "transparent"
Lock.Surface {
anchors.fill: parent
context: lockContext
}
}
}
Lock.Context {
id: lockContext
onUnlocked: lock.locked = false
}
// Empty function to define first reference to singleton
function init() {
}
}
+281
View File
@@ -0,0 +1,281 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import QtQuick.Effects
import QtQuick.Controls.Fusion
import Quickshell.Wayland
import Quickshell
import "." as Lock
Rectangle {
id: root
required property Lock.Context context
color: "black"
Image {
id: chest
anchors.centerIn: parent
source: "../assets/chest.png"
sourceClipRect: Qt.rect(32, 0, 32, 32)
smooth: false
height: width
// Rectangles
RowLayout {
visible: passwordBox.enabled
anchors.centerIn: parent
anchors.verticalCenterOffset: 31
spacing: 31
Repeater {
model: 6
delegate: Rectangle {
id: r
required property var modelData
color: root.context.showFailure ? "#D6568F" : "#6BCBF3"
implicitWidth: 31
implicitHeight: 31
MultiEffect {
id: glow
source: parent
anchors.fill: parent
brightness: passwordBox.length > r.modelData ? 0.7 : 0
blur: passwordBox.length > r.modelData ? 2.0 : 0
blurEnabled: true
}
}
}
}
Image {
id: screencopy
anchors.centerIn: parent
source: "/tmp/lock_screencopy.jpeg"
height: width * (9 / 16) // Width is animated below
}
Image {
id: chestInFront
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
height: parent.width / 2
source: "../assets/chest.png"
sourceClipRect: Qt.rect(32, 16, 32, 16)
smooth: false
visible: false
z: 9999
}
}
// Enter anim
// ------------
// Screencopy anim
NumberAnimation {
target: screencopy
property: "width"
from: 1920
to: 250
duration: 700
easing.type: Easing.OutBack
easing.overshoot: 0.9
running: true
}
SequentialAnimation {
id: screencopyAnim
running: true
NumberAnimation {
target: screencopy
property: "anchors.verticalCenterOffset"
to: -150
duration: 500
easing.type: Easing.OutBack
}
PauseAnimation {
duration: 200
}
PropertyAction {
target: chestInFront
property: "visible"
value: true
}
NumberAnimation {
target: screencopy
property: "anchors.verticalCenterOffset"
to: 100
duration: 300
easing.type: Easing.Linear
}
}
// Chest anim
SequentialAnimation {
id: chestAnim
running: true
NumberAnimation {
target: chest
property: "width"
from: 2000
to: 500
duration: 500
easing.type: Easing.OutBack
running: true
}
PauseAnimation {
duration: 500
}
PropertyAction {
target: chest
property: "sourceClipRect.x"
value: 0
}
PropertyAction {
target: chestInFront
property: "visible"
value: false
}
PropertyAction {
target: screencopy
property: "visible"
value: false
}
NumberAnimation {
onStarted: screencopy.visible = false
target: chest
property: "anchors.verticalCenterOffset"
to: 50
duration: 100
easing.type: Easing.OutBack
}
NumberAnimation {
target: chest
property: "anchors.verticalCenterOffset"
to: 0
duration: 100
easing.type: Easing.OutBack
}
}
// Exit anim
// ------------
// Screencopy anim
SequentialAnimation {
id: screencopyAnimExit
running: false
PropertyAction {
target: chestInFront
property: "visible"
value: true
}
NumberAnimation {
target: screencopy
property: "anchors.verticalCenterOffset"
to: -150
duration: 200
easing.type: Easing.OutBack
}
PropertyAction {
target: chestInFront
property: "visible"
value: false
}
NumberAnimation {
target: screencopy
property: "anchors.verticalCenterOffset"
to: 0
duration: 400
easing.type: Easing.OutBack
}
NumberAnimation {
target: screencopy
property: "width"
to: 1920
duration: 400
easing.type: Easing.OutBack
running: true
}
onFinished: () => root.context.unlocked()
}
// Chest anim
SequentialAnimation {
id: chestAnimExit
running: false
PropertyAction {
target: chest
property: "sourceClipRect.x"
value: 32
}
PropertyAction {
target: screencopy
property: "visible"
value: true
}
NumberAnimation {
target: chest
property: "anchors.verticalCenterOffset"
to: -50
duration: 100
easing.type: Easing.OutBack
}
NumberAnimation {
target: chest
property: "anchors.verticalCenterOffset"
to: 0
duration: 200
easing.type: Easing.OutBack
}
}
TextField {
id: passwordBox
implicitWidth: 400
font.pointSize: 50
color: "transparent"
cursorVisible: false
background: Rectangle {
color: "transparent"
}
focus: true
enabled: !screencopyAnim.running && !chestAnim.running && !screencopyAnimExit.running && !chestAnimExit.running && !root.context.unlockInProgress
opacity: 0
echoMode: TextInput.Password
inputMethodHints: Qt.ImhSensitiveData
onTextChanged: () => {
if (text != "") {
root.context.showFailure = false;
}
}
onAccepted: () => {
root.context.currentText = text;
root.context.tryUnlock();
}
Connections {
target: root.context
function onUnlockInProgressChanged() {
if (root.context.unlockInProgress == false) {
passwordBox.text = "";
}
}
function onPamSuccess() {
screencopyAnimExit.running = true;
chestAnimExit.running = true;
}
}
}
// Button {
// text: "Its not working, let me out"
// onClicked: root.context.unlocked()
// }
}
+2
View File
@@ -0,0 +1,2 @@
auth required pam_unix.so
+70
View File
@@ -0,0 +1,70 @@
// This is how the notification looks
import QtQuick
import QtQuick.Layouts
import Quickshell.Services.Notifications
Item {
id: root
required property Notification notif
implicitWidth: 240
implicitHeight: layout.height
ColumnLayout {
id: layout
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
spacing: 0
Image {
id: trumpetTop
Layout.fillWidth: true
Layout.preferredHeight: width * (sourceSize.height / sourceSize.width)
source: "../assets/trumpet-top.png"
smooth: false
}
Rectangle {
id: bannerRect
color: "#3B253F"
Layout.fillWidth: true
implicitHeight: textColumn.height
Layout.leftMargin: (width / trumpetTop.sourceSize.width) * 2 + 3
Layout.rightMargin: (width / trumpetTop.sourceSize.width) * 2 + 3
ColumnLayout {
id: textColumn
anchors.top: parent.top
anchors.left: parent.left
anchors.right: parent.right
Text {
Layout.maximumWidth: bannerRect.width
text: root.notif.summary + (root.notif.body ? "\n=======" : "")
font.family: "BigBlueTermPlusNerdFont"
wrapMode: Text.Wrap
font.pointSize: 18
font.bold: true
color: "#9292B6"
}
Text {
Layout.maximumWidth: bannerRect.width
text: root.notif.body
font.family: "BigBlueTermPlusNerdFont"
wrapMode: Text.Wrap
font.pointSize: 14
font.bold: false
color: "#9292B6"
}
}
}
}
}
+183
View File
@@ -0,0 +1,183 @@
// This is all the behaviour and animations for the notification
import QtQuick
import Quickshell.Io
import "." as Notifs
Item {
id: root
signal dismissed
required property var notif
property var screen: Qt.size(1920, 1080)
property var lifetime: 5000
enum AnimState {
Returning,
Inert,
Flinging,
Dismissing
}
property var state: Notif.Returning
property var isDragging: false
property var initialX: 200
property var initialY: 100
property var initialR: 45
property var targetX: 0
property var targetY: 0
property var targetR: 0
property var velocityX: 0
property var velocityY: 0
property var velocityR: 0
FrameAnimation {
function dampingVelocity(currentVelocity, delta) {
const spring = 1.0;
const damping = 0.1;
const springForce = spring * delta;
const dampingForce = -damping * currentVelocity;
return currentVelocity + (springForce + dampingForce);
}
running: root.state != Notif.Inert
onTriggered: {
if (root.state == Notif.Returning) {
const deltaX = root.targetX - display.x;
const deltaY = root.targetY - display.y;
const deltaR = root.targetR - display.rotation;
root.velocityX = dampingVelocity(root.velocityX, deltaX);
root.velocityY = dampingVelocity(root.velocityY, deltaY);
root.velocityR = dampingVelocity(root.velocityR, deltaR);
if (Math.abs(root.velocityX) < 0.1 && Math.abs(root.velocityY) < 0.1) {
console.log("inter");
root.state = Notif.Inert;
root.velocityX = 0;
root.velocityY = 0;
root.velocityR = 0;
display.x = root.targetX;
display.y = root.targetY;
display.rotation = root.targetR;
}
if (root.isDragging) {
if (Math.abs(root.velocityX) > 1200 || Math.abs(root.velocityY) > 1200) {
root.state = Notif.Flinging;
}
}
} else if (root.state == Notif.Flinging) {
root.velocityY += 3000 * frameTime;
display.rotation = -root.velocityY * frameTime;
knight.visible = true;
knight.x += root.velocityX * frameTime;
knight.y += root.velocityY * frameTime;
knight.rotation += root.velocityX * 0.2 * frameTime;
if (display.x > display.width || display.y > root.screen.height) {
root.dismissed();
}
} else if (root.state == Notif.Dismissing) {
root.velocityX += frameTime * 20000;
if (display.x > display.width) {
root.dismissed();
}
}
display.x += root.velocityX * frameTime;
display.y += root.velocityY * frameTime;
display.rotation += root.velocityR * frameTime;
}
}
implicitWidth: display.width
implicitHeight: display.height
anchors.fill: display
Notifs.Display {
id: display
notif: root.notif
x: root.initialX
y: root.initialY
rotation: root.initialR
transformOrigin: Item.Right
}
Image {
id: knight
source: "../assets/knight.png"
x: width
width: sourceSize.width * 10
height: sourceSize.height * 10
smooth: false
visible: false
Process {
id: screamCmd
command: ["play", "--no-show-progress", "assets/wilhelm-scream.ogg"]
}
onVisibleChanged: () => {
if (visible) {
screamCmd.startDetached();
}
}
}
MouseArea {
id: mouseArea
anchors.fill: display
acceptedButtons: Qt.LeftButton | Qt.RightButton
enabled: root.state != Notif.Flinging
property var prevMouseX: 0
property var prevMouseY: 0
onPressed: e => {
if (enabled && e.buttons & Qt.LeftButton) {
prevMouseX = e.x;
prevMouseY = e.y;
root.isDragging = true;
root.state = Notif.Returning;
}
}
onReleased: e => {
if (!(e.buttons & Qt.LeftButton)) {
root.isDragging = false;
}
}
onPositionChanged: e => {
if (enabled && root.isDragging) {
root.velocityX = (e.x - prevMouseX) * 200;
root.velocityY = (e.y - prevMouseY) * 200;
prevMouseX = e.x;
prevMouseY = e.y;
}
}
onClicked: e => {
if (enabled && e.button & Qt.RightButton) {
root.state = Notif.Dismissing;
}
}
}
Timer {
id: timer
interval: root.lifetime
repeat: false
running: !mouseArea.containsMouse && root.state == Notif.Inert
onTriggered: () => {
root.state = Notif.Dismissing;
}
}
Process {
id: playSoundCmd
command: ["play", "--no-show-progress", "assets/trumpet.wav"]
}
Component.onCompleted: playSoundCmd.startDetached()
}
+90
View File
@@ -0,0 +1,90 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Wayland
import Quickshell.Services.Notifications
import "."
PanelWindow {
id: root
required property var bar
property list<Notification> notifs
WlrLayershell.namespace: "shell:notifications"
exclusionMode: ExclusionMode.Ignore
color: "transparent"
anchors {
left: true
top: true
bottom: true
right: true
}
NotificationServer {
actionsSupported: true
onNotification: notif => {
notif.tracked = true;
root.notifs = [...root.notifs, notif];
}
}
visible: stack.children.length != 0
mask: Region {
item: stack
}
ListView {
id: stack
model: ScriptModel {
values: [...root.notifs]
}
anchors.right: parent.right
y: root.bar.height
implicitWidth: 240
implicitHeight: children.reduce((h, c) => h + c.height, 0)
interactive: false
spacing: 20
displaced: Transition {
NumberAnimation {
property: "y"
duration: 200
easing.type: Easing.OutCubic
}
}
move: Transition {
NumberAnimation {
property: "y"
duration: 200
easing.type: Easing.OutCubic
}
}
remove: Transition {
NumberAnimation {
property: "y"
duration: 200
easing.type: Easing.OutCubic
}
}
delegate: Notif {
required property Notification modelData
notif: modelData
onDismissed: () => {
modelData.dismiss();
// Remove from list
const index = root.notifs.indexOf(notif);
if (index > -1)
root.notifs.splice(index, 1);
}
}
}
}
@@ -0,0 +1,39 @@
pragma Singleton
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import "." as Screenshot
Singleton {
id: root
property bool isOpen: false
IpcHandler {
target: "screenshot"
function open() {
root.isOpen = true;
}
function close() {
root.isOpen = false;
}
function toggle() {
root.isOpen = !root.isOpen;
}
}
LazyLoader {
active: root.isOpen
Screenshot.Overlay {
controller: root
}
}
// Empty function to define first reference to singleton
function init() {
}
}
+198
View File
@@ -0,0 +1,198 @@
pragma ComponentBehavior: Bound
import QtQuick
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import "../components"
// TODO: make configurable: cursor visibility, screen freeze
PanelWindow {
id: root
required property var controller
exclusionMode: ExclusionMode.Ignore
anchors {
left: true
right: true
top: true
bottom: true
}
color: "transparent"
WlrLayershell.keyboardFocus: WlrKeyboardFocus.Exclusive
WlrLayershell.namespace: "shell:screenshot"
visible: screencopy.hasContent
ScreencopyView {
id: screencopy
captureSource: root.screen
anchors.fill: parent
paintCursor: false
}
Process {
id: cmd
running: false
onExited: notifyCmd.running = true
}
Process {
id: notifyCmd
command: ["notify-send", "\"Screenshot copied\"", "\"The screenshot was copied to your clipboard\""]
running: false
onExited: root.controller.isOpen = false
}
contentItem {
focus: true
Keys.onEscapePressed: root.controller.isOpen = false
Keys.onReturnPressed: () => {
const x = Math.ceil(root.left);
const y = Math.ceil(root.top);
const w = Math.floor(root.right - root.left);
const h = Math.floor(root.bottom - root.top);
cmd.command = ["sh", "-c", `grim -g "${x},${y} ${w}x${h}" - | wl-copy`];
cmd.running = true;
}
}
property var left: 0
property var top: 0
property var right: 0
property var bottom: 0
Canvas {
id: canvas
anchors.fill: parent
readonly property var borderExtendX: 56
readonly property var borderExtendY: 48
readonly property var borderWidthH: 12
readonly property var borderWidthV: 18
onPaint: {
var ctx = getContext("2d");
ctx.reset();
// Background
ctx.fillStyle = "#44000000";
ctx.fillRect(0, 0, root.width, root.height);
// Border
ctx.fillStyle = "#824524";
ctx.fillRect(root.left - borderExtendX, root.top - borderWidthH, root.right - root.left + borderExtendX * 2, borderWidthH);
ctx.fillRect(root.left - borderExtendX, root.bottom, root.right - root.left + borderExtendX * 2, borderWidthH);
ctx.fillRect(root.left - borderWidthV, root.top - borderExtendY, borderWidthV, root.bottom - root.top + borderExtendY * 2);
ctx.fillRect(root.right, root.top - borderExtendY, borderWidthV, root.bottom - root.top + borderExtendY * 2);
// Rect
ctx.clearRect(root.left, root.top, root.right - root.left, root.bottom - root.top);
}
}
Rope {
id: topLeftRope
anchors.fill: parent
start: Qt.vector2d(0, 0)
end: Qt.vector2d(0, 0)
}
Rope {
id: topRightRope
anchors.fill: parent
start: Qt.vector2d(0, 0)
end: Qt.vector2d(0, 0)
}
Rope {
id: bottomRightRope
anchors.fill: parent
start: Qt.vector2d(0, 0)
end: Qt.vector2d(0, 0)
}
Rope {
id: bottomLeftRope
anchors.fill: parent
start: Qt.vector2d(0, 0)
end: Qt.vector2d(0, 0)
}
RopeTie {
x: root.left - width / 2
y: root.top - height / 2
}
RopeTie {
x: root.right - width / 2
y: root.top - height / 2
}
RopeTie {
x: root.right - width / 2
y: root.bottom - height / 2
}
RopeTie {
x: root.left - width / 2
y: root.bottom - height / 2
}
MouseArea {
anchors.fill: parent
onPressed: e => {
root.left = e.x;
root.top = e.y;
root.right = e.x;
root.bottom = e.y;
}
onPositionChanged: e => {
if (e.x > root.left) {
root.right = e.x;
}
if (e.y > root.top) {
root.bottom = e.y;
}
}
}
onLeftChanged: () => {
canvas.requestPaint();
topLeftRope.end = Qt.vector2d(left, top);
bottomLeftRope.end = Qt.vector2d(left, bottom);
}
onTopChanged: () => {
canvas.requestPaint();
topLeftRope.end = Qt.vector2d(left, top);
topRightRope.end = Qt.vector2d(right, top);
}
onRightChanged: () => {
canvas.requestPaint();
topRightRope.end = Qt.vector2d(right, top);
bottomRightRope.end = Qt.vector2d(right, bottom);
}
onBottomChanged: () => {
canvas.requestPaint();
bottomRightRope.end = Qt.vector2d(right, bottom);
bottomLeftRope.end = Qt.vector2d(left, bottom);
}
onWidthChanged: () => {
left = width / 2;
right = width / 2;
topRightRope.start = Qt.vector2d(width, 0);
bottomRightRope.start = Qt.vector2d(width, height);
}
onHeightChanged: () => {
top = height / 2;
bottom = height / 2;
bottomRightRope.start = Qt.vector2d(width, height);
bottomLeftRope.start = Qt.vector2d(0, height);
}
component RopeTie: Image {
width: 48
height: 48
source: "../assets/rope-tie.png"
smooth: false
}
}
+25
View File
@@ -0,0 +1,25 @@
import Quickshell
import QtQuick
import "bar" as Bar
import "notifs" as Notifs
import "launcher" as Launcher
import "screenshot" as Screenshot
import "lock" as Lock
ShellRoot {
Bg {}
Bar.Bar {
id: bar
}
Notifs.Overlay {
bar: bar
}
Component.onCompleted: () => {
Launcher.Controller.init();
Screenshot.Controller.init();
Lock.Controller.init();
}
}