summaryrefslogtreecommitdiff
path: root/raveos-hyprland-theme/theme-data/dms/Widgets
diff options
context:
space:
mode:
Diffstat (limited to 'raveos-hyprland-theme/theme-data/dms/Widgets')
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/AppIconRenderer.qml130
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/AppLauncherGridDelegate.qml126
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/AppLauncherListDelegate.qml140
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/CachingImage.qml102
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankActionButton.qml44
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankAlbumArt.qml161
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankBackdrop.qml64
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankButton.qml99
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankButtonGroup.qml236
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankCircularImage.qml171
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankCollapsibleSection.qml130
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankDropdown.qml456
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankFilterChips.qml108
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankFlickable.qml179
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankGridView.qml175
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankIcon.qml77
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankIconPicker.qml259
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankListView.qml213
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankLocationSearch.qml266
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankNFIcon.qml164
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankNumberStepper.qml67
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankOSD.qml334
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankPopout.qml620
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankRipple.qml104
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankSVGIcon.qml81
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankScrollbar.qml46
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankSeekbar.qml262
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankSlideout.qml209
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankSlider.qml294
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankTabBar.qml263
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankTextField.qml271
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankToggle.qml195
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankTooltip.qml92
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/DankTooltipV2.qml154
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/FloatingWindowControls.qml118
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/KeybindItem.qml1840
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/M3WaveProgress.qml274
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/NumericText.qml22
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/PluginGlobalVar.qml30
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/ScrollConstants.js14
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/StateLayer.qml74
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/StyledRect.qml29
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/StyledText.qml49
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/StyledTextMetrics.qml24
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/SystemLogo.qml84
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/VpnDetailContent.qml235
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/VpnProfileDelegate.qml287
-rw-r--r--raveos-hyprland-theme/theme-data/dms/Widgets/WindowBlur.qml65
48 files changed, 9437 insertions, 0 deletions
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/AppIconRenderer.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/AppIconRenderer.qml
new file mode 100644
index 0000000..9cdb71b
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/AppIconRenderer.qml
@@ -0,0 +1,130 @@
+import QtQuick
+import Quickshell
+import Quickshell.Widgets
+import qs.Common
+import qs.Services
+
+Item {
+ id: root
+
+ required property string iconValue
+ required property int iconSize
+ property string fallbackText: "A"
+ property color iconColor: Theme.surfaceText
+ property color colorOverride: "transparent"
+ property real brightnessOverride: 0.0
+ property real contrastOverride: 0.0
+ property real saturationOverride: 0.0
+ property color fallbackBackgroundColor: Theme.surfaceLight
+ property color fallbackTextColor: Theme.primary
+ property real materialIconSizeAdjustment: Theme.spacingM
+ property real unicodeIconScale: 0.7
+ property real fallbackTextScale: 0.4
+ property real iconMargins: 0
+ property real fallbackLeftMargin: 0
+ property real fallbackRightMargin: 0
+ property real fallbackTopMargin: 0
+ property real fallbackBottomMargin: 0
+
+ readonly property bool isMaterial: iconValue && iconValue.startsWith("material:")
+ readonly property bool isUnicode: iconValue && iconValue.startsWith("unicode:")
+ readonly property bool isSvgCorner: iconValue && iconValue.startsWith("svg+corner:")
+ readonly property bool isSvg: iconValue && !isSvgCorner && iconValue.startsWith("svg:")
+ readonly property bool isImage: iconValue && iconValue.startsWith("image:")
+ readonly property bool hasColorOverride: colorOverride.a > 0
+ readonly property string materialName: isMaterial ? iconValue.substring(9) : ""
+ readonly property string unicodeChar: isUnicode ? iconValue.substring(8) : ""
+ readonly property string imagePath: isImage ? iconValue.substring(6) : ""
+ readonly property string svgSource: {
+ if (isSvgCorner) {
+ const parts = iconValue.substring(11).split("|");
+ return parts[0] || "";
+ }
+ if (isSvg)
+ return iconValue.substring(4);
+ return "";
+ }
+ readonly property string svgCornerIcon: isSvgCorner ? (iconValue.substring(11).split("|")[1] || "") : ""
+ readonly property bool hasSpecialPrefix: isMaterial || isUnicode || isSvg || isSvgCorner || isImage
+ readonly property string iconPath: {
+ if (hasSpecialPrefix || !iconValue)
+ return "";
+ return Paths.resolveIconPath(iconValue);
+ }
+
+ visible: iconValue !== undefined && iconValue !== ""
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: root.materialName
+ size: root.iconSize - root.materialIconSizeAdjustment
+ color: root.hasColorOverride ? root.colorOverride : root.iconColor
+ visible: root.isMaterial
+ }
+
+ StyledText {
+ anchors.centerIn: parent
+ text: root.unicodeChar
+ font.pixelSize: root.iconSize * root.unicodeIconScale
+ color: root.hasColorOverride ? root.colorOverride : root.iconColor
+ visible: root.isUnicode
+ }
+
+ DankSVGIcon {
+ anchors.centerIn: parent
+ source: root.svgSource
+ size: root.iconSize
+ cornerIcon: root.svgCornerIcon
+ colorOverride: root.colorOverride
+ brightnessOverride: root.brightnessOverride
+ contrastOverride: root.contrastOverride
+ saturationOverride: root.saturationOverride
+ visible: root.isSvg || root.isSvgCorner
+ }
+
+ CachingImage {
+ id: cachingImg
+ anchors.fill: parent
+ imagePath: root.imagePath
+ maxCacheSize: root.iconSize * 2
+ visible: root.isImage && status === Image.Ready
+ }
+
+ Loader {
+ id: iconImgLoader
+ anchors.fill: parent
+ anchors.margins: root.iconMargins
+ active: !root.hasSpecialPrefix && root.iconPath !== ""
+ sourceComponent: IconImage {
+ anchors.fill: parent
+ source: root.iconPath
+ backer.sourceSize: Qt.size(root.iconSize * 2, root.iconSize * 2)
+ mipmap: true
+ asynchronous: true
+ visible: status === Image.Ready
+ }
+ }
+
+ Rectangle {
+ id: fallbackRect
+
+ anchors.fill: parent
+ anchors.leftMargin: root.fallbackLeftMargin
+ anchors.rightMargin: root.fallbackRightMargin
+ anchors.topMargin: root.fallbackTopMargin
+ anchors.bottomMargin: root.fallbackBottomMargin
+ visible: !root.hasSpecialPrefix && (root.iconPath === "" || !iconImgLoader.item || iconImgLoader.item.status !== Image.Ready)
+ color: root.fallbackBackgroundColor
+ radius: Theme.cornerRadius
+ border.width: 0
+ border.color: Theme.primarySelected
+
+ StyledText {
+ anchors.centerIn: parent
+ text: root.fallbackText
+ font.pixelSize: root.iconSize * root.fallbackTextScale
+ color: root.fallbackTextColor
+ font.weight: Font.Bold
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/AppLauncherGridDelegate.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/AppLauncherGridDelegate.qml
new file mode 100644
index 0000000..f18bd94
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/AppLauncherGridDelegate.qml
@@ -0,0 +1,126 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ required property var model
+ required property int index
+ required property var gridView
+ property int cellWidth: 120
+ property int cellHeight: 120
+ property int minIconSize: 32
+ property int maxIconSize: 64
+ property real iconSizeRatio: 0.5
+ property bool hoverUpdatesSelection: true
+ property bool keyboardNavigationActive: false
+ property int currentIndex: -1
+ property bool isPlugin: model?.isPlugin || false
+ property real mouseAreaLeftMargin: 0
+ property real mouseAreaRightMargin: 0
+ property real mouseAreaBottomMargin: 0
+ property real iconFallbackLeftMargin: 0
+ property real iconFallbackRightMargin: 0
+ property real iconFallbackBottomMargin: 0
+ property real iconMaterialSizeAdjustment: 0
+ property real iconUnicodeScale: 0.8
+
+ signal itemClicked(int index, var modelData)
+ signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
+ signal keyboardNavigationReset
+
+ width: cellWidth - 1
+ height: cellHeight - 1
+ radius: Theme.cornerRadius
+ color: currentIndex === index ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryPressed : "transparent"
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ Item {
+ width: iconRenderer.computedIconSize
+ height: iconRenderer.computedIconSize
+ anchors.horizontalCenter: parent.horizontalCenter
+
+ AppIconRenderer {
+ id: iconRenderer
+ property int computedIconSize: Math.min(root.maxIconSize, Math.max(root.minIconSize, root.cellWidth * root.iconSizeRatio))
+
+ width: computedIconSize
+ height: computedIconSize
+ iconValue: (model.icon && model.icon !== "") ? model.icon : ""
+ iconSize: computedIconSize
+ fallbackText: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
+ materialIconSizeAdjustment: root.iconMaterialSizeAdjustment
+ unicodeIconScale: root.iconUnicodeScale
+ fallbackTextScale: Math.min(28, computedIconSize * 0.5) / computedIconSize
+ iconMargins: 0
+ fallbackLeftMargin: root.iconFallbackLeftMargin
+ fallbackRightMargin: root.iconFallbackRightMargin
+ fallbackBottomMargin: root.iconFallbackBottomMargin
+ }
+
+ DankIcon {
+ visible: model.pinned === true
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.rightMargin: -4
+ anchors.bottomMargin: -4
+ name: "push_pin"
+ size: 14
+ color: Theme.primary
+ }
+ }
+
+ StyledText {
+ anchors.horizontalCenter: parent.horizontalCenter
+ width: root.cellWidth - 12
+ text: model.name || ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignHCenter
+ maximumLineCount: 1
+ wrapMode: Text.NoWrap
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+
+ anchors.fill: parent
+ anchors.leftMargin: root.mouseAreaLeftMargin
+ anchors.rightMargin: root.mouseAreaRightMargin
+ anchors.bottomMargin: root.mouseAreaBottomMargin
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ z: 10
+ onEntered: {
+ if (root.hoverUpdatesSelection && !root.keyboardNavigationActive)
+ root.gridView.currentIndex = root.index;
+ }
+ onPositionChanged: {
+ root.keyboardNavigationReset();
+ }
+ onClicked: mouse => {
+ if (mouse.button === Qt.LeftButton) {
+ root.itemClicked(root.index, root.model);
+ }
+ }
+ onPressAndHold: mouse => {
+ const globalPos = mapToItem(null, mouse.x, mouse.y);
+ root.itemRightClicked(root.index, root.model, globalPos.x, globalPos.y);
+ }
+ onPressed: mouse => {
+ if (mouse.button === Qt.RightButton) {
+ const globalPos = mapToItem(null, mouse.x, mouse.y);
+ root.itemRightClicked(root.index, root.model, globalPos.x, globalPos.y);
+ mouse.accepted = true;
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/AppLauncherListDelegate.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/AppLauncherListDelegate.qml
new file mode 100644
index 0000000..281ad60
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/AppLauncherListDelegate.qml
@@ -0,0 +1,140 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ required property var model
+ required property int index
+ required property var listView
+ property int itemHeight: 60
+ property int iconSize: 40
+ property bool showDescription: true
+ property bool hoverUpdatesSelection: true
+ property bool keyboardNavigationActive: false
+ property bool isCurrentItem: false
+ property bool isPlugin: model?.isPlugin || false
+ property real mouseAreaLeftMargin: 0
+ property real mouseAreaRightMargin: 0
+ property real mouseAreaBottomMargin: 0
+ property real iconMargins: 0
+ property real iconFallbackLeftMargin: 0
+ property real iconFallbackRightMargin: 0
+ property real iconFallbackBottomMargin: 0
+ property real iconMaterialSizeAdjustment: Theme.spacingM
+ property real iconUnicodeScale: 0.7
+
+ signal itemClicked(int index, var modelData)
+ signal itemRightClicked(int index, var modelData, real mouseX, real mouseY)
+ signal keyboardNavigationReset
+
+ width: listView.width
+ height: itemHeight
+ radius: Theme.cornerRadius
+ color: isCurrentItem ? Theme.primaryPressed : mouseArea.containsMouse ? Theme.primaryPressed : "transparent"
+
+ Row {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingL
+
+ AppIconRenderer {
+ width: root.iconSize
+ height: root.iconSize
+ anchors.verticalCenter: parent.verticalCenter
+ iconValue: (model.icon && model.icon !== "") ? model.icon : ""
+ iconSize: root.iconSize
+ fallbackText: (model.name && model.name.length > 0) ? model.name.charAt(0).toUpperCase() : "A"
+ iconMargins: root.iconMargins
+ fallbackLeftMargin: root.iconFallbackLeftMargin
+ fallbackRightMargin: root.iconFallbackRightMargin
+ fallbackBottomMargin: root.iconFallbackBottomMargin
+ materialIconSizeAdjustment: root.iconMaterialSizeAdjustment
+ unicodeIconScale: root.iconUnicodeScale
+ }
+
+ Column {
+ anchors.verticalCenter: parent.verticalCenter
+ width: (model.icon !== undefined && model.icon !== "") ? (parent.width - root.iconSize - Theme.spacingL) : parent.width
+ spacing: Theme.spacingXS
+
+ Row {
+ width: parent.width
+ spacing: Theme.spacingXS
+
+ StyledText {
+ width: parent.width - (pinIcon.visible ? pinIcon.width + Theme.spacingXS : 0)
+ text: model.name || ""
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignLeft
+ wrapMode: Text.NoWrap
+ maximumLineCount: 1
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ DankIcon {
+ id: pinIcon
+ visible: model.pinned === true
+ name: "push_pin"
+ size: Theme.fontSizeMedium
+ color: Theme.primary
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ StyledText {
+ width: parent.width
+ text: model.comment || "Application"
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignLeft
+ maximumLineCount: 1
+ visible: root.showDescription && model.comment && model.comment.length > 0
+ }
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+
+ anchors.fill: parent
+ anchors.leftMargin: root.mouseAreaLeftMargin
+ anchors.rightMargin: root.mouseAreaRightMargin
+ anchors.bottomMargin: root.mouseAreaBottomMargin
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton | Qt.RightButton
+ z: 10
+ onEntered: {
+ if (root.hoverUpdatesSelection && !root.keyboardNavigationActive)
+ root.listView.currentIndex = root.index;
+ }
+ onPositionChanged: {
+ root.keyboardNavigationReset();
+ }
+ onClicked: mouse => {
+ if (mouse.button === Qt.LeftButton) {
+ root.itemClicked(root.index, root.model);
+ }
+ }
+ onPressAndHold: mouse => {
+ const globalPos = mapToItem(null, mouse.x, mouse.y);
+ root.itemRightClicked(root.index, root.model, globalPos.x, globalPos.y);
+ }
+ onPressed: mouse => {
+ if (mouse.button === Qt.RightButton) {
+ const globalPos = mapToItem(null, mouse.x, mouse.y);
+ root.itemRightClicked(root.index, root.model, globalPos.x, globalPos.y);
+ mouse.accepted = true;
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/CachingImage.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/CachingImage.qml
new file mode 100644
index 0000000..5ffec09
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/CachingImage.qml
@@ -0,0 +1,102 @@
+import QtQuick
+import qs.Common
+
+Item {
+ id: root
+
+ property string imagePath: ""
+ property int maxCacheSize: 512
+ property int status: isAnimated ? animatedImg.status : staticImg.status
+ property int fillMode: Image.PreserveAspectCrop
+
+ readonly property bool isRemoteUrl: imagePath.startsWith("http://") || imagePath.startsWith("https://")
+ readonly property bool isAnimated: {
+ if (!imagePath)
+ return false;
+ const lower = imagePath.toLowerCase();
+ return lower.endsWith(".gif") || lower.endsWith(".webp");
+ }
+ readonly property string normalizedPath: {
+ if (!imagePath)
+ return "";
+ if (isRemoteUrl)
+ return imagePath;
+ if (imagePath.startsWith("file://"))
+ return imagePath.substring(7);
+ return imagePath;
+ }
+
+ function djb2Hash(str) {
+ if (!str)
+ return "";
+ let hash = 5381;
+ for (let i = 0; i < str.length; i++) {
+ hash = ((hash << 5) + hash) + str.charCodeAt(i);
+ hash = hash & 0x7FFFFFFF;
+ }
+ return hash.toString(16).padStart(8, '0');
+ }
+
+ readonly property string imageHash: normalizedPath ? djb2Hash(normalizedPath) : ""
+ readonly property string cachePath: imageHash && !isRemoteUrl && !isAnimated ? `${Paths.stringify(Paths.imagecache)}/${imageHash}@${maxCacheSize}x${maxCacheSize}.png` : ""
+ readonly property string encodedImagePath: {
+ if (!normalizedPath)
+ return "";
+ if (isRemoteUrl)
+ return normalizedPath;
+ return "file://" + normalizedPath.split('/').map(s => encodeURIComponent(s)).join('/');
+ }
+
+ AnimatedImage {
+ id: animatedImg
+ anchors.fill: parent
+ visible: root.isAnimated
+ asynchronous: true
+ fillMode: root.fillMode
+ source: root.isAnimated ? root.imagePath : ""
+ playing: visible && status === AnimatedImage.Ready
+ }
+
+ Image {
+ id: staticImg
+ anchors.fill: parent
+ visible: !root.isAnimated
+ asynchronous: true
+ fillMode: root.fillMode
+ sourceSize.width: root.maxCacheSize
+ sourceSize.height: root.maxCacheSize
+ smooth: true
+
+ onStatusChanged: {
+ if (source == root.cachePath && status === Image.Error) {
+ source = root.encodedImagePath;
+ return;
+ }
+ if (root.isRemoteUrl || source != root.encodedImagePath || status !== Image.Ready || !root.cachePath)
+ return;
+ Paths.mkdir(Paths.imagecache);
+ const grabPath = root.cachePath;
+ if (visible && width > 0 && height > 0 && Window.window?.visible) {
+ grabToImage(res => res.saveToFile(grabPath));
+ }
+ }
+ }
+
+ onImagePathChanged: {
+ if (!imagePath) {
+ staticImg.source = "";
+ return;
+ }
+ if (isAnimated)
+ return;
+ if (isRemoteUrl) {
+ staticImg.source = imagePath;
+ return;
+ }
+ Paths.mkdir(Paths.imagecache);
+ const hash = djb2Hash(normalizedPath);
+ const cPath = hash ? `${Paths.stringify(Paths.imagecache)}/${hash}@${maxCacheSize}x${maxCacheSize}.png` : "";
+ const encoded = "file://" + normalizedPath.split('/').map(s => encodeURIComponent(s)).join('/');
+ staticImg.source = cPath || encoded;
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankActionButton.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankActionButton.qml
new file mode 100644
index 0000000..c23e84b
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankActionButton.qml
@@ -0,0 +1,44 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+StyledRect {
+ id: root
+
+ property string iconName: ""
+ property int iconSize: Theme.iconSize - 4
+ property color iconColor: Theme.surfaceText
+ property color backgroundColor: "transparent"
+ property bool circular: true
+ property bool enabled: true
+ property int buttonSize: 32
+ property var tooltipText: null
+ property string tooltipSide: "bottom"
+
+ signal clicked
+ signal entered
+ signal exited
+
+ width: buttonSize
+ height: buttonSize
+ radius: Theme.cornerRadius
+ color: backgroundColor
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: root.iconName
+ size: root.iconSize
+ color: root.iconColor
+ }
+
+ StateLayer {
+ disabled: !root.enabled
+ stateColor: Theme.primary
+ cornerRadius: root.radius
+ onClicked: root.clicked()
+ onEntered: root.entered()
+ onExited: root.exited()
+ tooltipText: root.tooltipText
+ tooltipSide: root.tooltipSide
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankAlbumArt.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankAlbumArt.qml
new file mode 100644
index 0000000..d5f8d10
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankAlbumArt.qml
@@ -0,0 +1,161 @@
+import QtQuick
+import QtQuick.Shapes
+import Quickshell.Services.Mpris
+import qs.Common
+import qs.Services
+
+Item {
+ id: root
+
+ property MprisPlayer activePlayer
+ property string artUrl: (activePlayer?.trackArtUrl) || ""
+ property string lastValidArtUrl: ""
+ property alias albumArtStatus: albumArt.imageStatus
+ property real albumSize: Math.min(width, height) * 0.88
+ property bool showAnimation: true
+ property real animationScale: 1.0
+
+ onArtUrlChanged: {
+ if (artUrl && albumArt.status !== Image.Error) {
+ lastValidArtUrl = artUrl;
+ }
+ }
+
+ Loader {
+ active: activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
+ sourceComponent: Component {
+ Ref {
+ service: CavaService
+ }
+ }
+ }
+
+ Shape {
+ id: morphingBlob
+ width: parent.width * 1.1
+ height: parent.height * 1.1
+ anchors.centerIn: parent
+ visible: CavaService.cavaAvailable && activePlayer?.playbackState === MprisPlaybackState.Playing && showAnimation
+ asynchronous: false
+ antialiasing: true
+ preferredRendererType: Shape.CurveRenderer
+ z: 0
+ layer.enabled: false
+
+ readonly property real centerX: width / 2
+ readonly property real centerY: height / 2
+ readonly property real baseRadius: Math.min(width, height) * 0.41 * root.animationScale
+ readonly property int segments: 28
+
+ property var audioLevels: {
+ if (!CavaService.cavaAvailable || CavaService.values.length === 0) {
+ return [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6];
+ }
+ return CavaService.values;
+ }
+
+ property var smoothedLevels: [0.5, 0.3, 0.7, 0.4, 0.6, 0.5, 0.8, 0.2, 0.9, 0.6]
+ property var cubics: []
+
+ Connections {
+ target: CavaService
+ function onValuesChanged() {
+ if (morphingBlob.visible) {
+ morphingBlob.updatePath();
+ }
+ }
+ }
+
+ Component {
+ id: cubicSegment
+ PathCubic {}
+ }
+
+ Component.onCompleted: {
+ shapePath.pathElements.push(Qt.createQmlObject('import QtQuick; import QtQuick.Shapes; PathMove {}', shapePath));
+
+ for (let i = 0; i < segments; i++) {
+ const seg = cubicSegment.createObject(shapePath);
+ shapePath.pathElements.push(seg);
+ cubics.push(seg);
+ }
+
+ updatePath();
+ }
+
+ function updatePath() {
+ if (cubics.length === 0)
+ return;
+
+ const alpha = 0.35;
+ const minLen = Math.min(smoothedLevels.length, audioLevels.length);
+ for (let i = 0; i < minLen; i++) {
+ smoothedLevels[i] += alpha * (audioLevels[i] - smoothedLevels[i]);
+ }
+
+ const angleStep = 2 * Math.PI / segments;
+ const tension3 = 0.16666667;
+ const startMove = shapePath.pathElements[0];
+
+ const points = new Array(segments);
+ for (let i = 0; i < segments; i++) {
+ const angle = i * angleStep;
+ const audioIndex = i % 10;
+ const rawLevel = smoothedLevels[audioIndex] || 0;
+ const clampedLevel = rawLevel < 0 ? 0 : (rawLevel > 100 ? 100 : rawLevel);
+ const audioLevel = Math.max(0.15, Math.sqrt(clampedLevel * 0.01)) * 0.5;
+ const radius = baseRadius * (1.0 + audioLevel);
+ points[i] = {
+ x: centerX + Math.cos(angle) * radius,
+ y: centerY + Math.sin(angle) * radius
+ };
+ }
+
+ startMove.x = points[0].x;
+ startMove.y = points[0].y;
+
+ for (let i = 0; i < segments; i++) {
+ const p0 = points[(i + segments - 1) % segments];
+ const p1 = points[i];
+ const p2 = points[(i + 1) % segments];
+ const p3 = points[(i + 2) % segments];
+
+ const seg = cubics[i];
+ seg.control1X = p1.x + (p2.x - p0.x) * tension3;
+ seg.control1Y = p1.y + (p2.y - p0.y) * tension3;
+ seg.control2X = p2.x - (p3.x - p1.x) * tension3;
+ seg.control2Y = p2.y - (p3.y - p1.y) * tension3;
+ seg.x = p2.x;
+ seg.y = p2.y;
+ }
+ }
+
+ ShapePath {
+ id: shapePath
+ fillColor: Theme.primary
+ strokeColor: "transparent"
+ strokeWidth: 0
+ joinStyle: ShapePath.RoundJoin
+ fillRule: ShapePath.WindingFill
+ }
+ }
+
+ DankCircularImage {
+ id: albumArt
+ width: albumSize
+ height: albumSize
+ anchors.centerIn: parent
+ z: 1
+
+ imageSource: artUrl || lastValidArtUrl || ""
+ fallbackIcon: "album"
+ border.color: Theme.primary
+ border.width: 2
+
+ onImageSourceChanged: {
+ if (imageSource && imageStatus !== Image.Error) {
+ lastValidArtUrl = imageSource;
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankBackdrop.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankBackdrop.qml
new file mode 100644
index 0000000..3db7c0c
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankBackdrop.qml
@@ -0,0 +1,64 @@
+import QtQuick
+import QtQuick.Effects
+import qs.Common
+
+Item {
+ id: root
+
+ anchors.fill: parent
+
+ property string screenName: ""
+ property bool isColorWallpaper: {
+ var currentWallpaper = SessionData.getMonitorWallpaper(screenName)
+ return currentWallpaper && currentWallpaper.startsWith("#")
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: isColorWallpaper ? SessionData.getMonitorWallpaper(screenName) : Theme.background
+ }
+
+ Rectangle {
+ x: parent.width * 0.7
+ y: -parent.height * 0.3
+ width: parent.width * 0.8
+ height: parent.height * 1.5
+ color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.15)
+ rotation: 35
+ visible: !isColorWallpaper
+ }
+
+ Rectangle {
+ x: parent.width * 0.85
+ y: -parent.height * 0.2
+ width: parent.width * 0.4
+ height: parent.height * 1.2
+ color: Qt.rgba(Theme.secondary.r, Theme.secondary.g, Theme.secondary.b, 0.12)
+ rotation: 35
+ visible: !isColorWallpaper
+ }
+
+ Image {
+ anchors.left: parent.left
+ anchors.bottom: parent.bottom
+ anchors.leftMargin: Theme.spacingXL * 2
+ anchors.bottomMargin: Theme.spacingXL * 2
+ width: 200
+ height: width * (569.94629 / 506.50931)
+ fillMode: Image.PreserveAspectFit
+ smooth: true
+ mipmap: true
+ asynchronous: true
+ source: "file://" + Theme.shellDir + "/assets/danklogonormal.svg"
+ opacity: 0.25
+ visible: !isColorWallpaper
+ layer.enabled: true
+ layer.smooth: true
+ layer.mipmap: true
+ layer.effect: MultiEffect {
+ saturation: 0
+ colorization: 1
+ colorizationColor: Theme.primary
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankButton.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankButton.qml
new file mode 100644
index 0000000..ea8a202
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankButton.qml
@@ -0,0 +1,99 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property string text: ""
+ property string iconName: ""
+ property int iconSize: Theme.iconSizeSmall
+ property bool enabled: true
+ property bool hovered: mouseArea.containsMouse
+ property bool pressed: mouseArea.pressed
+ property color backgroundColor: Theme.buttonBg
+ property color textColor: Theme.buttonText
+ property int buttonHeight: 40
+ property int horizontalPadding: Theme.spacingL
+ property bool enableScaleAnimation: false
+ property bool enableRipple: typeof SettingsData !== "undefined" ? (SettingsData.enableRippleEffects ?? true) : true
+
+ signal clicked
+
+ width: Math.max(contentRow.implicitWidth + horizontalPadding * 2, 64)
+ height: buttonHeight
+ radius: Theme.cornerRadius
+ color: backgroundColor
+ opacity: enabled ? 1 : 0.4
+ scale: (enableScaleAnimation && pressed) ? 0.98 : 1.0
+
+ Behavior on scale {
+ enabled: enableScaleAnimation && Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
+ DankAnim {
+ duration: 100
+ easing.bezierCurve: Theme.expressiveCurves.standard
+ }
+ }
+
+ Rectangle {
+ id: stateLayer
+ anchors.fill: parent
+ radius: parent.radius
+ color: {
+ if (pressed)
+ return Theme.withAlpha(root.textColor, 0.20);
+ if (hovered)
+ return Theme.withAlpha(root.textColor, 0.12);
+ return "transparent";
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shorterDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ DankRipple {
+ id: rippleLayer
+ rippleColor: root.textColor
+ cornerRadius: root.radius
+ enableRipple: root.enableRipple
+ }
+
+ Row {
+ id: contentRow
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: root.iconName
+ size: root.iconSize
+ color: root.textColor
+ visible: root.iconName !== ""
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: root.text
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: Font.Medium
+ color: root.textColor
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
+ enabled: root.enabled
+ onPressed: mouse => {
+ if (root.enableRipple)
+ rippleLayer.trigger(mouse.x, mouse.y);
+ }
+ onClicked: root.clicked()
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankButtonGroup.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankButtonGroup.qml
new file mode 100644
index 0000000..02e145e
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankButtonGroup.qml
@@ -0,0 +1,236 @@
+import QtQuick
+import Quickshell
+import qs.Common
+import qs.Widgets
+
+Row {
+ id: root
+
+ property var model: []
+ property int currentIndex: -1
+ property string selectionMode: "single"
+ property bool multiSelect: selectionMode === "multi"
+ property var initialSelection: []
+ property var currentSelection: initialSelection
+ property bool checkEnabled: true
+ property int buttonHeight: 40
+ property int minButtonWidth: 64
+ property int buttonPadding: Theme.spacingL
+ property int checkIconSize: Theme.iconSizeSmall
+ property int textSize: Theme.fontSizeMedium
+ property bool userInteracted: false
+
+ signal selectionChanged(int index, bool selected)
+ signal animationCompleted
+
+ spacing: Theme.spacingXS
+
+ Timer {
+ id: animationTimer
+ interval: Theme.shortDuration
+ onTriggered: {
+ root.userInteracted = false;
+ root.animationCompleted();
+ }
+ }
+
+ function isSelected(index) {
+ if (multiSelect) {
+ return repeater.itemAt(index)?.selected || false;
+ }
+ return index === currentIndex;
+ }
+
+ function selectItem(index) {
+ userInteracted = true;
+ if (multiSelect) {
+ const modelValue = model[index];
+ let newSelection = [...currentSelection];
+ const isCurrentlySelected = newSelection.includes(modelValue);
+
+ if (isCurrentlySelected) {
+ newSelection = newSelection.filter(item => item !== modelValue);
+ } else {
+ newSelection.push(modelValue);
+ }
+
+ currentSelection = newSelection;
+ selectionChanged(index, !isCurrentlySelected);
+ animationTimer.restart();
+ } else {
+ const oldIndex = currentIndex;
+ selectionChanged(index, true);
+ if (oldIndex !== index && oldIndex >= 0) {
+ selectionChanged(oldIndex, false);
+ }
+ animationTimer.restart();
+ }
+ }
+
+ Repeater {
+ id: repeater
+ model: ScriptModel {
+ values: root.model
+ }
+
+ delegate: Rectangle {
+ id: segment
+
+ property bool selected: multiSelect ? root.currentSelection.includes(modelData) : (index === root.currentIndex)
+ property bool hovered: mouseArea.containsMouse
+ property bool pressed: mouseArea.pressed
+ property bool isFirst: index === 0
+ property bool isLast: index === repeater.count - 1
+ property bool visualFirst: I18n.isRtl ? isLast : isFirst
+ property bool visualLast: I18n.isRtl ? isFirst : isLast
+ property bool prevSelected: index > 0 ? root.isSelected(index - 1) : false
+ property bool nextSelected: index < repeater.count - 1 ? root.isSelected(index + 1) : false
+
+ width: Math.max(contentItem.implicitWidth + root.buttonPadding * 2, root.minButtonWidth) + (selected ? 4 : 0)
+ height: root.buttonHeight
+
+ color: selected ? Theme.buttonBg : Theme.withAlpha(Theme.surfaceVariant, Theme.popupTransparency)
+ border.color: "transparent"
+ border.width: 0
+
+ topLeftRadius: (visualFirst || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
+ bottomLeftRadius: (visualFirst || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
+ topRightRadius: (visualLast || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
+ bottomRightRadius: (visualLast || selected) ? Theme.cornerRadius : Math.min(4, Theme.cornerRadius)
+
+ Behavior on width {
+ enabled: root.userInteracted
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Behavior on topLeftRadius {
+ enabled: root.userInteracted
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Behavior on topRightRadius {
+ enabled: root.userInteracted
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Behavior on bottomLeftRadius {
+ enabled: root.userInteracted
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Behavior on bottomRightRadius {
+ enabled: root.userInteracted
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Behavior on color {
+ enabled: root.userInteracted
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Rectangle {
+ id: stateLayer
+ anchors.fill: parent
+ topLeftRadius: parent.topLeftRadius
+ bottomLeftRadius: parent.bottomLeftRadius
+ topRightRadius: parent.topRightRadius
+ bottomRightRadius: parent.bottomRightRadius
+ color: {
+ if (pressed)
+ return selected ? Theme.buttonPressed : Theme.surfaceTextHover;
+ if (hovered)
+ return selected ? Theme.buttonHover : Theme.surfaceTextHover;
+ return "transparent";
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shorterDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ DankRipple {
+ id: segmentRipple
+ cornerRadius: Theme.cornerRadius
+ rippleColor: segment.selected ? Theme.buttonText : Theme.surfaceVariantText
+ }
+
+ Item {
+ id: contentItem
+ anchors.centerIn: parent
+ implicitWidth: contentRow.implicitWidth
+ implicitHeight: contentRow.implicitHeight
+
+ Row {
+ id: contentRow
+ spacing: Theme.spacingS
+
+ DankIcon {
+ id: checkIcon
+ name: "check"
+ size: root.checkIconSize
+ color: segment.selected ? Theme.buttonText : Theme.surfaceVariantText
+ visible: root.checkEnabled && segment.selected
+ opacity: segment.selected ? 1 : 0
+ scale: segment.selected ? 1 : 0.6
+ anchors.verticalCenter: parent.verticalCenter
+
+ Behavior on opacity {
+ enabled: root.userInteracted
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Behavior on scale {
+ enabled: root.userInteracted
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.emphasizedEasing
+ }
+ }
+ }
+
+ StyledText {
+ id: buttonText
+ text: typeof modelData === "string" ? modelData : modelData.text || ""
+ font.pixelSize: root.textSize
+ font.weight: segment.selected ? Font.Medium : Font.Normal
+ color: segment.selected ? Theme.buttonText : Theme.surfaceVariantText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => segmentRipple.trigger(mouse.x, mouse.y)
+ onClicked: root.selectItem(index)
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankCircularImage.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankCircularImage.qml
new file mode 100644
index 0000000..36cbfe4
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankCircularImage.qml
@@ -0,0 +1,171 @@
+import QtQuick
+import QtQuick.Window
+import QtQuick.Effects
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property string imageSource: ""
+ property string fallbackIcon: "notifications"
+ property string fallbackText: ""
+ property bool hasImage: imageSource !== ""
+ readonly property bool shouldProbe: imageSource !== "" && !imageSource.startsWith("image://")
+ // Probe with AnimatedImage first; once loaded, check frameCount to decide.
+ readonly property bool isAnimated: shouldProbe && probe.status === Image.Ready && probe.frameCount > 1
+ readonly property var activeImage: isAnimated ? probe : staticImage
+ property int imageStatus: activeImage.status
+
+ signal imageSaved(string filePath)
+
+ property string _pendingSavePath: ""
+ property var _attachedWindow: root.Window.window
+
+ on_AttachedWindowChanged: {
+ if (_attachedWindow && _pendingSavePath !== "") {
+ Qt.callLater(function () {
+ if (root._pendingSavePath !== "") {
+ let path = root._pendingSavePath;
+ root._pendingSavePath = "";
+ root.saveImageToFile(path);
+ }
+ });
+ }
+ }
+
+ function saveImageToFile(filePath) {
+ if (activeImage.status !== Image.Ready)
+ return false;
+
+ if (!activeImage.Window.window) {
+ _pendingSavePath = filePath;
+ return true;
+ }
+
+ activeImage.grabToImage(function (result) {
+ if (result && result.saveToFile(filePath)) {
+ root.imageSaved(filePath);
+ }
+ });
+ return true;
+ }
+
+ radius: width / 2
+ color: Qt.rgba(Theme.primary.r, Theme.primary.g, Theme.primary.b, 0.1)
+ border.color: "transparent"
+ border.width: 0
+
+ // Probe: loads as AnimatedImage to detect frame count.
+ AnimatedImage {
+ id: probe
+ anchors.fill: parent
+ anchors.margins: 2
+ asynchronous: true
+ fillMode: Image.PreserveAspectCrop
+ smooth: true
+ mipmap: true
+ cache: true
+ visible: false
+ source: root.shouldProbe ? root.imageSource : ""
+ }
+
+ // Static fallback: used once probe confirms the image is not animated.
+ Image {
+ id: staticImage
+ anchors.fill: parent
+ anchors.margins: 2
+ asynchronous: true
+ fillMode: Image.PreserveAspectCrop
+ smooth: true
+ mipmap: true
+ cache: true
+ visible: false
+ sourceSize.width: Math.max(width * 2, 128)
+ sourceSize.height: Math.max(height * 2, 128)
+ source: !root.shouldProbe ? root.imageSource : ""
+ }
+
+ // Once the probe loads, if not animated, hand off to Image and unload probe.
+ Connections {
+ target: probe
+ function onStatusChanged() {
+ if (!root.shouldProbe)
+ return;
+ switch (probe.status) {
+ case Image.Ready:
+ if (probe.frameCount <= 1) {
+ staticImage.source = root.imageSource;
+ probe.source = "";
+ }
+ break;
+ case Image.Error:
+ staticImage.source = root.imageSource;
+ probe.source = "";
+ break;
+ }
+ }
+ }
+
+ // If imageSource changes, reset: re-probe with AnimatedImage.
+ onImageSourceChanged: {
+ if (root.shouldProbe) {
+ staticImage.source = "";
+ probe.source = root.imageSource;
+ } else {
+ probe.source = "";
+ staticImage.source = root.imageSource;
+ }
+ }
+
+ MultiEffect {
+ anchors.fill: parent
+ anchors.margins: 2
+ source: root.activeImage
+ maskEnabled: true
+ maskSource: circularMask
+ visible: root.activeImage.status === Image.Ready && root.imageSource !== ""
+ maskThresholdMin: 0.5
+ maskSpreadAtMin: 1
+ }
+
+ Item {
+ id: circularMask
+ anchors.centerIn: parent
+ width: parent.width - 4
+ height: parent.height - 4
+ layer.enabled: true
+ layer.smooth: true
+ visible: false
+
+ Rectangle {
+ anchors.fill: parent
+ radius: width / 2
+ color: "black"
+ antialiasing: true
+ }
+ }
+
+ AppIconRenderer {
+ anchors.centerIn: parent
+ width: Math.round(parent.width * 0.75)
+ height: width
+ visible: (root.activeImage.status !== Image.Ready || root.imageSource === "") && root.fallbackIcon !== ""
+ iconValue: root.fallbackIcon
+ iconSize: width
+ iconColor: Theme.surfaceVariantText
+ materialIconSizeAdjustment: 0
+ fallbackText: root.fallbackText
+ fallbackBackgroundColor: "transparent"
+ fallbackTextColor: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ anchors.centerIn: parent
+ visible: root.imageSource === "" && root.fallbackIcon === "" && root.fallbackText !== ""
+ text: root.fallbackText
+ font.pixelSize: Math.max(12, parent.width * 0.5)
+ font.weight: Font.Bold
+ color: Theme.surfaceVariantText
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankCollapsibleSection.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankCollapsibleSection.qml
new file mode 100644
index 0000000..0ed0e59
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankCollapsibleSection.qml
@@ -0,0 +1,130 @@
+import QtQuick
+import QtQuick.Layouts
+import qs.Common
+import qs.Widgets
+
+ColumnLayout {
+ id: root
+
+ required property string title
+ property string description: ""
+ property bool expanded: false
+ property bool showBackground: false
+ property alias headerColor: headerRect.color
+
+ signal toggleRequested
+
+ spacing: Theme.spacingS
+ Layout.fillWidth: true
+
+ Rectangle {
+ id: headerRect
+ Layout.fillWidth: true
+ Layout.preferredHeight: Math.max(titleRow.implicitHeight + Theme.paddingM * 2, 48)
+ radius: Theme.cornerRadius
+ color: "transparent"
+
+ RowLayout {
+ id: titleRow
+ anchors.fill: parent
+ anchors.leftMargin: Theme.paddingM
+ anchors.rightMargin: Theme.paddingM
+ spacing: Theme.spacingM
+
+ StyledText {
+ text: root.title
+ font.pixelSize: Theme.fontSizeLarge
+ font.weight: Font.Medium
+ Layout.fillWidth: true
+ }
+
+ DankIcon {
+ name: "expand_more"
+ size: Theme.iconSizeSmall
+ rotation: root.expanded ? 180 : 0
+
+ Behavior on rotation {
+ enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
+ DankAnim {
+ duration: Theme.shortDuration
+ easing.bezierCurve: Theme.expressiveCurves.standard
+ }
+ }
+ }
+ }
+
+ StateLayer {
+ anchors.fill: parent
+ onClicked: {
+ root.toggleRequested();
+ root.expanded = !root.expanded;
+ }
+ }
+ }
+
+ default property alias content: contentColumn.data
+
+ Item {
+ id: contentWrapper
+ Layout.fillWidth: true
+ Layout.preferredHeight: root.expanded ? (contentColumn.implicitHeight + Theme.spacingS * 2) : 0
+ clip: true
+
+ Behavior on Layout.preferredHeight {
+ enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
+ DankAnim {
+ duration: Theme.expressiveDurations.expressiveDefaultSpatial
+ easing.bezierCurve: Theme.expressiveCurves.standard
+ }
+ }
+
+ Rectangle {
+ id: backgroundRect
+ anchors.fill: parent
+ radius: Theme.cornerRadius
+ color: Theme.surfaceContainer
+ opacity: root.showBackground && root.expanded ? 1.0 : 0.0
+ visible: root.showBackground
+
+ Behavior on opacity {
+ enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
+ DankAnim {
+ duration: Theme.shortDuration
+ easing.bezierCurve: Theme.expressiveCurves.standard
+ }
+ }
+ }
+
+ ColumnLayout {
+ id: contentColumn
+ anchors.left: parent.left
+ anchors.right: parent.right
+ y: Theme.spacingS
+ anchors.leftMargin: Theme.paddingM
+ anchors.rightMargin: Theme.paddingM
+ anchors.bottomMargin: Theme.spacingS
+ spacing: Theme.spacingS
+ opacity: root.expanded ? 1.0 : 0.0
+
+ Behavior on opacity {
+ enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
+ DankAnim {
+ duration: Theme.shortDuration
+ easing.bezierCurve: Theme.expressiveCurves.standard
+ }
+ }
+
+ StyledText {
+ id: descriptionText
+ Layout.fillWidth: true
+ Layout.topMargin: root.description !== "" ? Theme.spacingXS : 0
+ Layout.bottomMargin: root.description !== "" ? Theme.spacingS : 0
+ visible: root.description !== ""
+ text: root.description
+ color: Theme.surfaceTextSecondary
+ font.pixelSize: Theme.fontSizeSmall
+ wrapMode: Text.Wrap
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankDropdown.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankDropdown.qml
new file mode 100644
index 0000000..05b962a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankDropdown.qml
@@ -0,0 +1,456 @@
+import "../Common/fzf.js" as Fzf
+import QtQuick
+import QtQuick.Controls
+import Quickshell
+import qs.Common
+import qs.Widgets
+
+Item {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ function checkParentDisablesTransparency() {
+ let p = parent;
+ while (p) {
+ if (p.disablePopupTransparency === true)
+ return true;
+ p = p.parent;
+ }
+ return false;
+ }
+
+ property string text: ""
+ property string description: ""
+ property string currentValue: ""
+ property var options: []
+ property var optionIcons: []
+ property bool enableFuzzySearch: false
+ property var optionIconMap: ({})
+
+ function rebuildIconMap() {
+ const map = {};
+ for (let i = 0; i < options.length; i++) {
+ if (optionIcons.length > i)
+ map[options[i]] = optionIcons[i];
+ }
+ optionIconMap = map;
+ }
+
+ onOptionsChanged: rebuildIconMap()
+ onOptionIconsChanged: rebuildIconMap()
+
+ property int popupWidthOffset: 0
+ property int maxPopupHeight: 400
+ property bool openUpwards: false
+ property int popupWidth: 0
+ property bool alignPopupRight: false
+ property int dropdownWidth: 200
+ property bool compactMode: text === "" && description === ""
+ property bool addHorizontalPadding: false
+ property string emptyText: ""
+ property bool usePopupTransparency: !checkParentDisablesTransparency()
+
+ signal valueChanged(string value)
+
+ function closeDropdownMenu() {
+ dropdownMenu.close();
+ }
+
+ function resetSearch() {
+ searchField.text = "";
+ dropdownMenu.fzfFinder = null;
+ dropdownMenu.searchQuery = "";
+ dropdownMenu.selectedIndex = -1;
+ }
+
+ width: compactMode ? dropdownWidth : parent.width
+ implicitHeight: compactMode ? 40 : Math.max(60, labelColumn.implicitHeight + Theme.spacingM)
+
+ Component.onDestruction: {
+ if (dropdownMenu.visible)
+ dropdownMenu.close();
+ }
+
+ Column {
+ id: labelColumn
+
+ anchors.left: parent.left
+ anchors.right: dropdown.left
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: root.addHorizontalPadding ? Theme.spacingM : 0
+ anchors.rightMargin: Theme.spacingL
+ spacing: Theme.spacingXS
+ visible: !root.compactMode
+
+ StyledText {
+ text: root.text
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ StyledText {
+ text: root.description
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ visible: description.length > 0
+ wrapMode: Text.WordWrap
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+
+ Rectangle {
+ id: dropdown
+
+ width: root.compactMode ? parent.width : (root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : root.dropdownWidth))
+ height: 40
+ anchors.right: parent.right
+ anchors.rightMargin: root.addHorizontalPadding && !root.compactMode ? Theme.spacingM : 0
+ anchors.verticalCenter: parent.verticalCenter
+ radius: Theme.cornerRadius
+ color: dropdownArea.containsMouse || dropdownMenu.visible ? Theme.surfaceContainerHigh : (root.usePopupTransparency ? Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency) : Theme.surfaceContainer)
+ border.color: dropdownMenu.visible ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.2)
+ border.width: dropdownMenu.visible ? 2 : 1
+
+ MouseArea {
+ id: dropdownArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (dropdownMenu.visible) {
+ dropdownMenu.close();
+ return;
+ }
+
+ dropdownMenu.open();
+
+ let currentIndex = root.options.indexOf(root.currentValue);
+ listView.positionViewAtIndex(currentIndex, ListView.Beginning);
+
+ const pos = dropdown.mapToItem(Overlay.overlay, 0, 0);
+ const popupW = dropdownMenu.width;
+ const popupH = dropdownMenu.height;
+ const overlayH = Overlay.overlay.height;
+ const goUp = root.openUpwards || pos.y + dropdown.height + popupH + 4 > overlayH;
+ dropdownMenu.x = root.alignPopupRight ? pos.x + dropdown.width - popupW : pos.x - (root.popupWidthOffset / 2);
+ dropdownMenu.y = goUp ? pos.y - popupH - 4 : pos.y + dropdown.height + 4;
+ if (root.enableFuzzySearch)
+ searchField.forceActiveFocus();
+ }
+ }
+
+ Row {
+ id: contentRow
+
+ anchors.left: parent.left
+ anchors.right: expandIcon.left
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingS
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: root.optionIconMap[root.currentValue] ?? ""
+ size: 18
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ visible: name !== ""
+ }
+
+ StyledText {
+ text: root.currentValue
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ width: contentRow.width - (contentRow.children[0].visible ? contentRow.children[0].width + contentRow.spacing : 0)
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+
+ DankIcon {
+ id: expandIcon
+
+ name: dropdownMenu.visible ? "expand_less" : "expand_more"
+ size: 20
+ color: Theme.surfaceText
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.rightMargin: Theme.spacingS
+
+ Behavior on rotation {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+
+ Popup {
+ id: dropdownMenu
+
+ property string searchQuery: ""
+ property var filteredOptions: {
+ if (!root.enableFuzzySearch || searchQuery.length === 0)
+ return root.options;
+ if (!fzfFinder)
+ return root.options;
+ return fzfFinder.find(searchQuery).map(r => r.item);
+ }
+ property int selectedIndex: -1
+ property var fzfFinder: null
+
+ function initFinder() {
+ fzfFinder = new Fzf.Finder(root.options, {
+ "selector": option => option,
+ "limit": 50,
+ "casing": "case-insensitive",
+ "sort": true,
+ "tiebreakers": [(a, b, selector) => selector(a.item).length - selector(b.item).length]
+ });
+ }
+
+ function selectNext() {
+ if (filteredOptions.length === 0)
+ return;
+ selectedIndex = (selectedIndex + 1) % filteredOptions.length;
+ listView.positionViewAtIndex(selectedIndex, ListView.Contain);
+ }
+
+ function selectPrevious() {
+ if (filteredOptions.length === 0)
+ return;
+ selectedIndex = selectedIndex <= 0 ? filteredOptions.length - 1 : selectedIndex - 1;
+ listView.positionViewAtIndex(selectedIndex, ListView.Contain);
+ }
+
+ function selectCurrent() {
+ if (selectedIndex < 0 || selectedIndex >= filteredOptions.length)
+ return;
+ root.currentValue = filteredOptions[selectedIndex];
+ root.valueChanged(filteredOptions[selectedIndex]);
+ close();
+ }
+
+ onOpened: {
+ selectedIndex = -1;
+ if (searchField.text.length > 0) {
+ initFinder();
+ searchQuery = searchField.text;
+ } else {
+ fzfFinder = null;
+ searchQuery = "";
+ }
+ }
+
+ parent: Overlay.overlay
+ width: root.popupWidth === -1 ? undefined : (root.popupWidth > 0 ? root.popupWidth : (dropdown.width + root.popupWidthOffset))
+ height: {
+ let h = root.enableFuzzySearch ? 54 : 0;
+ if (root.options.length === 0 && root.emptyText !== "")
+ h += 32;
+ else
+ h += Math.min(filteredOptions.length, 10) * 36;
+ return Math.min(root.maxPopupHeight, h + 16);
+ }
+ padding: 0
+ modal: true
+ dim: false
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+
+ background: Rectangle {
+ color: "transparent"
+ }
+
+ contentItem: Rectangle {
+ id: contentSurface
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+ color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, 1)
+ border.color: Theme.primary
+ border.width: 2
+ radius: Theme.cornerRadius
+
+ ElevationShadow {
+ id: shadowLayer
+ anchors.fill: parent
+ z: -1
+ level: Theme.elevationLevel2
+ fallbackOffset: 4
+ targetRadius: contentSurface.radius
+ targetColor: contentSurface.color
+ borderColor: contentSurface.border.color
+ borderWidth: contentSurface.border.width
+ shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled
+ }
+
+ Column {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+
+ Rectangle {
+ id: searchContainer
+
+ width: parent.width
+ height: 42
+ visible: root.enableFuzzySearch
+ radius: Theme.cornerRadius
+ color: root.usePopupTransparency ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : Theme.surfaceContainerHigh
+
+ DankTextField {
+ id: searchField
+
+ anchors.fill: parent
+ anchors.margins: 1
+ placeholderText: I18n.tr("Search...")
+ topPadding: Theme.spacingS
+ bottomPadding: Theme.spacingS
+ onTextChanged: searchDebounce.restart()
+ Keys.onDownPressed: dropdownMenu.selectNext()
+ Keys.onUpPressed: dropdownMenu.selectPrevious()
+ Keys.onReturnPressed: dropdownMenu.selectCurrent()
+ Keys.onEnterPressed: dropdownMenu.selectCurrent()
+ Keys.onPressed: event => {
+ if (!(event.modifiers & Qt.ControlModifier))
+ return;
+ switch (event.key) {
+ case Qt.Key_N:
+ case Qt.Key_J:
+ dropdownMenu.selectNext();
+ event.accepted = true;
+ break;
+ case Qt.Key_P:
+ case Qt.Key_K:
+ dropdownMenu.selectPrevious();
+ event.accepted = true;
+ break;
+ }
+ }
+
+ Timer {
+ id: searchDebounce
+ interval: 50
+ onTriggered: {
+ if (!dropdownMenu.fzfFinder)
+ dropdownMenu.initFinder();
+ dropdownMenu.searchQuery = searchField.text;
+ dropdownMenu.selectedIndex = -1;
+ }
+ }
+ }
+ }
+
+ Item {
+ width: 1
+ height: Theme.spacingXS
+ visible: root.enableFuzzySearch
+ }
+
+ Item {
+ width: parent.width
+ height: 32
+ visible: root.options.length === 0 && root.emptyText !== ""
+
+ StyledText {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.leftMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ text: root.emptyText
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+
+ DankListView {
+ id: listView
+
+ width: parent.width
+ height: parent.height - (root.enableFuzzySearch ? searchContainer.height + Theme.spacingXS : 0) - (root.options.length === 0 && root.emptyText !== "" ? 32 : 0)
+ clip: true
+ visible: root.options.length > 0
+ model: ScriptModel {
+ values: dropdownMenu.filteredOptions
+ }
+ spacing: 2
+
+ interactive: true
+ flickDeceleration: 1500
+ maximumFlickVelocity: 2000
+ boundsBehavior: Flickable.DragAndOvershootBounds
+ boundsMovement: Flickable.FollowBoundsBehavior
+ pressDelay: 0
+ flickableDirection: Flickable.VerticalFlick
+
+ delegate: Rectangle {
+ id: delegateRoot
+
+ required property var modelData
+ required property int index
+ property bool isSelected: dropdownMenu.selectedIndex === index
+ property bool isCurrentValue: root.currentValue === modelData
+ property string iconName: root.optionIconMap[modelData] ?? ""
+
+ width: ListView.view.width
+ height: 32
+ radius: Theme.cornerRadius
+ color: isSelected ? Theme.primaryHover : optionArea.containsMouse ? Theme.primaryHoverLight : "transparent"
+
+ Row {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.leftMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: delegateRoot.iconName
+ size: 18
+ color: delegateRoot.isCurrentValue ? Theme.primary : Theme.surfaceText
+ visible: name !== ""
+ }
+
+ StyledText {
+ anchors.verticalCenter: parent.verticalCenter
+ text: delegateRoot.modelData
+ font.pixelSize: Theme.fontSizeMedium
+ color: delegateRoot.isCurrentValue ? Theme.primary : Theme.surfaceText
+ font.weight: delegateRoot.isCurrentValue ? Font.Medium : Font.Normal
+ width: root.popupWidth > 0 ? undefined : (delegateRoot.width - parent.x - Theme.spacingS * 2)
+ elide: root.popupWidth > 0 ? Text.ElideNone : Text.ElideRight
+ wrapMode: Text.NoWrap
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+
+ MouseArea {
+ id: optionArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ root.currentValue = delegateRoot.modelData;
+ root.valueChanged(delegateRoot.modelData);
+ root.closeDropdownMenu();
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankFilterChips.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankFilterChips.qml
new file mode 100644
index 0000000..0a00831
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankFilterChips.qml
@@ -0,0 +1,108 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Flow {
+ id: root
+
+ property var model: []
+ property int currentIndex: 0
+ property int chipHeight: 32
+ property int chipPadding: Theme.spacingM
+ property bool showCheck: true
+ property bool showCounts: true
+
+ signal selectionChanged(int index)
+
+ spacing: Theme.spacingS
+ width: parent ? parent.width : 400
+
+ Repeater {
+ model: root.model
+
+ Rectangle {
+ id: chip
+ required property var modelData
+ required property int index
+
+ property bool selected: index === root.currentIndex
+ property bool hovered: mouseArea.containsMouse
+ property bool pressed: mouseArea.pressed
+ property string label: typeof modelData === "string" ? modelData : (modelData.label || "")
+ property int count: typeof modelData === "object" ? (modelData.count || 0) : 0
+ property bool showCount: root.showCounts && count > 0
+
+ width: contentRow.implicitWidth + root.chipPadding * 2
+ height: root.chipHeight
+ radius: height / 2
+
+ color: selected ? Theme.primary : Theme.surfaceVariant
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: {
+ if (pressed)
+ return chip.selected ? Theme.primaryPressed : Theme.surfaceTextHover;
+ if (hovered)
+ return chip.selected ? Theme.primaryHover : Theme.surfaceTextHover;
+ return "transparent";
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shorterDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ DankRipple {
+ id: chipRipple
+ cornerRadius: chip.radius
+ rippleColor: chip.selected ? Theme.primaryText : Theme.surfaceVariantText
+ }
+
+ Row {
+ id: contentRow
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: "check"
+ size: 16
+ anchors.verticalCenter: parent.verticalCenter
+ color: Theme.primaryText
+ visible: root.showCheck && chip.selected
+ }
+
+ StyledText {
+ text: chip.label + (chip.showCount ? " (" + chip.count + ")" : "")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: chip.selected ? Font.Medium : Font.Normal
+ color: chip.selected ? Theme.primaryText : Theme.surfaceVariantText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => chipRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ root.currentIndex = chip.index;
+ root.selectionChanged(chip.index);
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankFlickable.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankFlickable.qml
new file mode 100644
index 0000000..6c93f3c
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankFlickable.qml
@@ -0,0 +1,179 @@
+import QtQuick
+import QtQuick.Controls
+import qs.Widgets
+import "ScrollConstants.js" as Scroll
+
+Flickable {
+ id: flickable
+
+ property alias verticalScrollBar: vbar
+ property real mouseWheelSpeed: Scroll.mouseWheelSpeed
+ property real momentumVelocity: 0
+ property bool isMomentumActive: false
+ property real friction: Scroll.friction
+ property bool _scrollBarActive: false
+
+ flickDeceleration: Scroll.flickDeceleration
+ maximumFlickVelocity: Scroll.maximumFlickVelocity
+ boundsBehavior: Flickable.StopAtBounds
+ boundsMovement: Flickable.FollowBoundsBehavior
+ pressDelay: 0
+ flickableDirection: Flickable.VerticalFlick
+
+ WheelHandler {
+ id: wheelHandler
+
+ property real touchpadSpeed: Scroll.touchpadSpeed
+ property real momentumRetention: Scroll.momentumRetention
+ property real lastWheelTime: 0
+ property real momentum: 0
+ property var velocitySamples: []
+ property bool sessionUsedMouseWheel: false
+
+ function startMomentum() {
+ flickable.isMomentumActive = true;
+ momentumAnim.running = true;
+ }
+
+ acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
+
+ onWheel: event => {
+ vbar._scrollBarActive = true;
+ vbar.hideTimer.restart();
+
+ const currentTime = Date.now();
+ const timeDelta = currentTime - lastWheelTime;
+ lastWheelTime = currentTime;
+
+ const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
+ const deltaY = event.angleDelta.y;
+ const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
+ const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
+ const isTouchpad = hasPixel;
+
+ if (isTraditionalMouse) {
+ sessionUsedMouseWheel = true;
+ momentumAnim.running = false;
+ flickable.isMomentumActive = false;
+ velocitySamples = [];
+ momentum = 0;
+ flickable.momentumVelocity = 0;
+
+ const lines = Math.round(Math.abs(deltaY) / 120);
+ const scrollAmount = (deltaY > 0 ? -lines : lines) * flickable.mouseWheelSpeed;
+ let newY = flickable.contentY + scrollAmount;
+ newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
+
+ if (flickable.flicking) {
+ flickable.cancelFlick();
+ }
+
+ flickable.contentY = newY;
+ } else if (isHighDpiMouse) {
+ sessionUsedMouseWheel = true;
+ momentumAnim.running = false;
+ flickable.isMomentumActive = false;
+ velocitySamples = [];
+ momentum = 0;
+ flickable.momentumVelocity = 0;
+
+ let delta = deltaY / 8 * touchpadSpeed;
+ let newY = flickable.contentY - delta;
+ newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
+
+ if (flickable.flicking) {
+ flickable.cancelFlick();
+ }
+
+ flickable.contentY = newY;
+ } else if (isTouchpad) {
+ sessionUsedMouseWheel = false;
+ momentumAnim.running = false;
+ flickable.isMomentumActive = false;
+
+ let delta = event.pixelDelta.y * touchpadSpeed;
+
+ velocitySamples.push({
+ "delta": delta,
+ "time": currentTime
+ });
+ velocitySamples = velocitySamples.filter(s => currentTime - s.time < Scroll.velocitySampleWindowMs);
+
+ if (velocitySamples.length > 1) {
+ const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0);
+ const timeSpan = currentTime - velocitySamples[0].time;
+ if (timeSpan > 0) {
+ flickable.momentumVelocity = Math.max(-Scroll.maxMomentumVelocity, Math.min(Scroll.maxMomentumVelocity, totalDelta / timeSpan * 1000));
+ }
+ }
+
+ if (timeDelta < Scroll.momentumTimeThreshold) {
+ momentum = momentum * momentumRetention + delta * Scroll.momentumDeltaFactor;
+ delta += momentum;
+ } else {
+ momentum = 0;
+ }
+
+ let newY = flickable.contentY - delta;
+ newY = Math.max(0, Math.min(flickable.contentHeight - flickable.height, newY));
+
+ if (flickable.flicking) {
+ flickable.cancelFlick();
+ }
+
+ flickable.contentY = newY;
+ }
+
+ event.accepted = true;
+ }
+
+ onActiveChanged: {
+ if (!active) {
+ if (!sessionUsedMouseWheel && Math.abs(flickable.momentumVelocity) >= Scroll.minMomentumVelocity) {
+ startMomentum();
+ } else {
+ velocitySamples = [];
+ flickable.momentumVelocity = 0;
+ }
+ }
+ }
+ }
+
+ onMovementStarted: {
+ vbar._scrollBarActive = true;
+ vbar.hideTimer.stop();
+ }
+ onMovementEnded: vbar.hideTimer.restart()
+
+ FrameAnimation {
+ id: momentumAnim
+ running: false
+
+ onTriggered: {
+ const dt = frameTime;
+ const newY = flickable.contentY - flickable.momentumVelocity * dt;
+ const maxY = Math.max(0, flickable.contentHeight - flickable.height);
+
+ if (newY < 0 || newY > maxY) {
+ flickable.contentY = newY < 0 ? 0 : maxY;
+ running = false;
+ flickable.isMomentumActive = false;
+ flickable.momentumVelocity = 0;
+ return;
+ }
+
+ flickable.contentY = newY;
+ flickable.momentumVelocity *= Math.pow(flickable.friction, dt / 0.016);
+
+ if (Math.abs(flickable.momentumVelocity) < Scroll.momentumStopThreshold) {
+ running = false;
+ flickable.isMomentumActive = false;
+ flickable.momentumVelocity = 0;
+ }
+ }
+ }
+
+ ScrollBar.vertical: DankScrollbar {
+ id: vbar
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankGridView.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankGridView.qml
new file mode 100644
index 0000000..c670d0f
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankGridView.qml
@@ -0,0 +1,175 @@
+import QtQuick
+import QtQuick.Controls
+import qs.Widgets
+import "ScrollConstants.js" as Scroll
+
+GridView {
+ id: gridView
+
+ property real momentumVelocity: 0
+ property bool isMomentumActive: false
+ property real friction: Scroll.friction
+
+ flickDeceleration: Scroll.flickDeceleration
+ maximumFlickVelocity: Scroll.maximumFlickVelocity
+ boundsBehavior: Flickable.StopAtBounds
+ boundsMovement: Flickable.FollowBoundsBehavior
+ pressDelay: 0
+ flickableDirection: Flickable.VerticalFlick
+
+ onMovementStarted: {
+ vbar._scrollBarActive = true;
+ vbar.hideTimer.stop();
+ }
+ onMovementEnded: vbar.hideTimer.restart()
+
+ WheelHandler {
+ id: wheelHandler
+
+ property real mouseWheelSpeed: Scroll.mouseWheelSpeed
+ property real touchpadSpeed: Scroll.touchpadSpeed
+ property real momentumRetention: Scroll.momentumRetention
+ property real lastWheelTime: 0
+ property real momentum: 0
+ property var velocitySamples: []
+ property bool sessionUsedMouseWheel: false
+
+ function startMomentum() {
+ isMomentumActive = true;
+ momentumAnim.running = true;
+ }
+
+ acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
+ onWheel: event => {
+ vbar._scrollBarActive = true;
+ vbar.hideTimer.restart();
+
+ const currentTime = Date.now();
+ const timeDelta = currentTime - lastWheelTime;
+ lastWheelTime = currentTime;
+
+ const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
+ const deltaY = event.angleDelta.y;
+ const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
+ const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
+ const isTouchpad = hasPixel;
+
+ if (isTraditionalMouse) {
+ sessionUsedMouseWheel = true;
+ momentumAnim.running = false;
+ isMomentumActive = false;
+ velocitySamples = [];
+ momentum = 0;
+ momentumVelocity = 0;
+
+ const lines = Math.round(Math.abs(deltaY) / 120);
+ const scrollAmount = (deltaY > 0 ? -lines : lines) * cellHeight * 0.35;
+ let newY = contentY + scrollAmount;
+ newY = Math.max(0, Math.min(contentHeight - height, newY));
+
+ if (flicking) {
+ cancelFlick();
+ }
+
+ contentY = newY;
+ } else if (isHighDpiMouse) {
+ sessionUsedMouseWheel = true;
+ momentumAnim.running = false;
+ isMomentumActive = false;
+ velocitySamples = [];
+ momentum = 0;
+ momentumVelocity = 0;
+
+ let delta = deltaY / 120 * cellHeight * 1.2;
+ let newY = contentY - delta;
+ newY = Math.max(0, Math.min(contentHeight - height, newY));
+
+ if (flicking) {
+ cancelFlick();
+ }
+
+ contentY = newY;
+ } else if (isTouchpad) {
+ sessionUsedMouseWheel = false;
+ momentumAnim.running = false;
+ isMomentumActive = false;
+
+ let delta = event.pixelDelta.y * touchpadSpeed;
+
+ velocitySamples.push({
+ "delta": delta,
+ "time": currentTime
+ });
+ velocitySamples = velocitySamples.filter(s => currentTime - s.time < Scroll.velocitySampleWindowMs);
+
+ if (velocitySamples.length > 1) {
+ const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0);
+ const timeSpan = currentTime - velocitySamples[0].time;
+ if (timeSpan > 0) {
+ momentumVelocity = Math.max(-Scroll.maxMomentumVelocity, Math.min(Scroll.maxMomentumVelocity, totalDelta / timeSpan * 1000));
+ }
+ }
+
+ if (timeDelta < Scroll.momentumTimeThreshold) {
+ momentum = momentum * momentumRetention + delta * Scroll.momentumDeltaFactor;
+ delta += momentum;
+ } else {
+ momentum = 0;
+ }
+
+ let newY = contentY - delta;
+ newY = Math.max(0, Math.min(contentHeight - height, newY));
+
+ if (flicking) {
+ cancelFlick();
+ }
+
+ contentY = newY;
+ }
+
+ event.accepted = true;
+ }
+ onActiveChanged: {
+ if (!active) {
+ if (!sessionUsedMouseWheel && Math.abs(momentumVelocity) >= Scroll.minMomentumVelocity) {
+ startMomentum();
+ } else {
+ velocitySamples = [];
+ momentumVelocity = 0;
+ }
+ }
+ }
+ }
+
+ FrameAnimation {
+ id: momentumAnim
+ running: false
+
+ onTriggered: {
+ const dt = frameTime;
+ const newY = contentY - momentumVelocity * dt;
+ const maxY = Math.max(0, contentHeight - height);
+
+ if (newY < 0 || newY > maxY) {
+ contentY = newY < 0 ? 0 : maxY;
+ running = false;
+ isMomentumActive = false;
+ momentumVelocity = 0;
+ return;
+ }
+
+ contentY = newY;
+ momentumVelocity *= Math.pow(friction, dt / 0.016);
+
+ if (Math.abs(momentumVelocity) < Scroll.momentumStopThreshold) {
+ running = false;
+ isMomentumActive = false;
+ momentumVelocity = 0;
+ }
+ }
+ }
+
+ ScrollBar.vertical: DankScrollbar {
+ id: vbar
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankIcon.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankIcon.qml
new file mode 100644
index 0000000..e80b93a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankIcon.qml
@@ -0,0 +1,77 @@
+import QtQuick
+import qs.Common
+
+Item {
+ id: root
+
+ property alias name: icon.text
+ property alias size: icon.font.pixelSize
+ property alias color: icon.color
+ property bool filled: false
+ property real fill: filled ? 1.0 : 0.0
+ property int grade: Theme.isLightMode ? 0 : -25
+ property int weight: filled ? 500 : 400
+
+ implicitWidth: Math.round(size)
+ implicitHeight: Math.round(size)
+
+ signal rotationCompleted
+
+ FontLoader {
+ id: materialSymbolsFont
+ source: Qt.resolvedUrl("../assets/fonts/material-design-icons/variablefont/MaterialSymbolsRounded[FILL,GRAD,opsz,wght].ttf")
+ }
+
+ StyledText {
+ id: icon
+
+ anchors.fill: parent
+
+ font.family: materialSymbolsFont.name
+ font.pixelSize: Theme.fontSizeMedium
+ font.weight: root.weight
+ color: Theme.surfaceText
+ verticalAlignment: Text.AlignVCenter
+ horizontalAlignment: Text.AlignHCenter
+ antialiasing: true
+
+ Behavior on color {
+ enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
+ DankColorAnim {
+ duration: Theme.shorterDuration
+ easing.bezierCurve: Theme.expressiveCurves.standard
+ }
+ }
+ font.variableAxes: {
+ "FILL": root.fill.toFixed(1),
+ "GRAD": root.grade,
+ "opsz": 24,
+ "wght": root.weight
+ }
+
+ Behavior on font.weight {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ Behavior on fill {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Timer {
+ id: rotationTimer
+ interval: 16
+ repeat: false
+ onTriggered: root.rotationCompleted()
+ }
+
+ onRotationChanged: {
+ rotationTimer.restart();
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankIconPicker.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankIconPicker.qml
new file mode 100644
index 0000000..3738525
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankIconPicker.qml
@@ -0,0 +1,259 @@
+import QtQuick
+import QtQuick.Controls
+import qs.Common
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ property string currentIcon: ""
+ property string iconType: "icon"
+
+ signal iconSelected(string iconName, string iconType)
+
+ width: 240
+ height: 32
+ radius: Theme.cornerRadius
+ color: Theme.surfaceContainer
+ border.color: iconPopup.visible ? Theme.primary : Theme.outline
+ border.width: 1
+
+ property var iconCategories: [
+ {
+ "name": I18n.tr("Numbers"),
+ "icons": ["looks_one", "looks_two", "looks_3", "looks_4", "looks_5", "looks_6", "filter_1", "filter_2", "filter_3", "filter_4", "filter_5", "filter_6", "filter_7", "filter_8", "filter_9", "filter_9_plus", "plus_one", "exposure_plus_1", "exposure_plus_2"]
+ },
+ {
+ "name": I18n.tr("Workspace"),
+ "icons": ["work", "laptop", "desktop_windows", "folder", "view_module", "dashboard", "apps", "grid_view"]
+ },
+ {
+ "name": I18n.tr("Development"),
+ "icons": ["code", "terminal", "bug_report", "build", "engineering", "integration_instructions", "data_object", "schema", "api", "webhook"]
+ },
+ {
+ "name": I18n.tr("Communication"),
+ "icons": ["chat", "mail", "forum", "message", "video_call", "call", "contacts", "group", "notifications", "campaign"]
+ },
+ {
+ "name": I18n.tr("Media"),
+ "icons": ["music_note", "headphones", "mic", "videocam", "photo", "movie", "library_music", "album", "radio", "volume_up"]
+ },
+ {
+ "name": I18n.tr("System"),
+ "icons": ["memory", "storage", "developer_board", "monitor", "keyboard", "mouse", "battery_std", "wifi", "bluetooth", "security", "settings"]
+ },
+ {
+ "name": I18n.tr("Navigation"),
+ "icons": ["home", "arrow_forward", "arrow_back", "expand_more", "expand_less", "menu", "close", "search", "filter_list", "sort"]
+ },
+ {
+ "name": I18n.tr("Actions"),
+ "icons": ["add", "remove", "edit", "delete", "save", "download", "upload", "share", "content_copy", "content_paste", "content_cut", "undo", "redo"]
+ },
+ {
+ "name": I18n.tr("Status"),
+ "icons": ["check", "error", "warning", "info", "done", "pending", "schedule", "update", "sync", "offline_bolt"]
+ },
+ {
+ "name": I18n.tr("Fun"),
+ "icons": ["celebration", "cake", "star", "favorite", "pets", "sports_esports", "local_fire_department", "bolt", "auto_awesome", "diamond"]
+ }
+ ]
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ if (iconPopup.visible) {
+ iconPopup.close();
+ return;
+ }
+ const pos = root.mapToItem(Overlay.overlay, 0, 0);
+ const popupHeight = 500;
+ const overlayHeight = Overlay.overlay?.height ?? 800;
+ iconPopup.x = pos.x;
+ if (pos.y + root.height + popupHeight + 4 > overlayHeight) {
+ iconPopup.y = pos.y - popupHeight - 4;
+ } else {
+ iconPopup.y = pos.y + root.height + 4;
+ }
+ iconPopup.open();
+ }
+ }
+
+ Row {
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Theme.spacingS
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: (root.iconType === "icon" && root.currentIcon) ? root.currentIcon : (root.iconType === "text" ? "text_fields" : "add")
+ size: 16
+ color: root.currentIcon ? Theme.surfaceText : Theme.outline
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: root.currentIcon ? root.currentIcon : I18n.tr("Choose icon")
+ font.pixelSize: Theme.fontSizeSmall
+ color: root.currentIcon ? Theme.surfaceText : Theme.outline
+ anchors.verticalCenter: parent.verticalCenter
+ width: 160
+ elide: Text.ElideRight
+ }
+ }
+
+ DankIcon {
+ name: iconPopup.visible ? "expand_less" : "expand_more"
+ size: 16
+ color: Theme.outline
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Popup {
+ id: iconPopup
+
+ parent: Overlay.overlay
+ width: 320
+ height: Math.min(500, dropdownContent.implicitHeight + 32)
+ padding: 0
+ modal: true
+ dim: false
+ closePolicy: Popup.CloseOnEscape | Popup.CloseOnPressOutside
+
+ background: Rectangle {
+ color: "transparent"
+ }
+
+ contentItem: Rectangle {
+ id: contentSurface
+ color: Theme.surface
+ radius: Theme.cornerRadius
+
+ ElevationShadow {
+ id: shadowLayer
+ anchors.fill: parent
+ z: -1
+ level: Theme.elevationLevel2
+ fallbackOffset: 4
+ targetRadius: contentSurface.radius
+ targetColor: contentSurface.color
+ shadowOpacity: Theme.elevationLevel2 && Theme.elevationLevel2.alpha !== undefined ? Theme.elevationLevel2.alpha : 0.25
+ shadowEnabled: Theme.elevationEnabled
+ }
+
+ Rectangle {
+ width: 24
+ height: 24
+ radius: 12
+ color: closeMouseArea.containsMouse ? Theme.errorHover : "transparent"
+ anchors.top: parent.top
+ anchors.right: parent.right
+ anchors.topMargin: Theme.spacingS
+ anchors.rightMargin: Theme.spacingS
+ z: 1
+
+ DankIcon {
+ name: "close"
+ size: 16
+ color: closeMouseArea.containsMouse ? Theme.error : Theme.outline
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: closeMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: iconPopup.close()
+ }
+ }
+
+ DankFlickable {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+ contentHeight: dropdownContent.height
+ clip: true
+ pressDelay: 0
+
+ Column {
+ id: dropdownContent
+ width: parent.width
+ spacing: Theme.spacingM
+
+ Repeater {
+ model: root.iconCategories
+
+ Column {
+ required property var modelData
+ width: parent.width
+ spacing: Theme.spacingS
+
+ StyledText {
+ text: modelData.name
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceText
+ }
+
+ Flow {
+ width: parent.width
+ spacing: 4
+
+ Repeater {
+ model: modelData.icons
+
+ Rectangle {
+ required property string modelData
+ width: 36
+ height: 36
+ radius: Theme.cornerRadius
+ color: iconMouseArea.containsMouse ? Theme.primaryHover : Theme.withAlpha(Theme.primaryHover, 0)
+ border.color: root.currentIcon === modelData ? Theme.primary : Theme.withAlpha(Theme.primary, 0)
+ border.width: 2
+
+ DankIcon {
+ name: parent.modelData
+ size: 20
+ color: root.currentIcon === parent.modelData ? Theme.primary : Theme.surfaceText
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: iconMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ root.iconSelected(parent.modelData, "icon");
+ iconPopup.close();
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+ }
+
+ function setIcon(iconName, type) {
+ root.iconType = type;
+ root.iconType = "icon";
+ root.currentIcon = iconName;
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankListView.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankListView.qml
new file mode 100644
index 0000000..41bdc9b
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankListView.qml
@@ -0,0 +1,213 @@
+import QtQuick
+import QtQuick.Controls
+import qs.Common
+import qs.Widgets
+import "ScrollConstants.js" as Scroll
+
+ListView {
+ id: listView
+
+ property real scrollBarTopMargin: 0
+ property real mouseWheelSpeed: Scroll.mouseWheelSpeed
+ property real savedY: 0
+ property bool justChanged: false
+ property bool isUserScrolling: false
+ property real momentumVelocity: 0
+ property bool isMomentumActive: false
+ property real friction: Scroll.friction
+
+ flickDeceleration: Scroll.flickDeceleration
+ maximumFlickVelocity: Scroll.maximumFlickVelocity
+ boundsBehavior: Flickable.StopAtBounds
+ boundsMovement: Flickable.FollowBoundsBehavior
+ pressDelay: 0
+ flickableDirection: Flickable.VerticalFlick
+
+ add: ListViewTransitions.add
+ remove: ListViewTransitions.remove
+ displaced: ListViewTransitions.displaced
+ move: ListViewTransitions.move
+
+ onMovementStarted: {
+ isUserScrolling = true;
+ vbar._scrollBarActive = true;
+ vbar.hideTimer.stop();
+ }
+ onMovementEnded: {
+ isUserScrolling = false;
+ vbar.hideTimer.restart();
+ }
+
+ onContentYChanged: {
+ if (!justChanged && isUserScrolling) {
+ savedY = contentY;
+ }
+ justChanged = false;
+ }
+
+ onModelChanged: {
+ justChanged = true;
+ contentY = savedY;
+ }
+
+ WheelHandler {
+ id: wheelHandler
+ property real touchpadSpeed: Scroll.touchpadSpeed
+ property real lastWheelTime: 0
+ property real momentum: 0
+ property var velocitySamples: []
+ property bool sessionUsedMouseWheel: false
+
+ function startMomentum() {
+ isMomentumActive = true;
+ momentumAnim.running = true;
+ }
+
+ acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad
+
+ onWheel: event => {
+ isUserScrolling = true;
+ vbar._scrollBarActive = true;
+ vbar.hideTimer.restart();
+
+ const currentTime = Date.now();
+ const timeDelta = currentTime - lastWheelTime;
+ lastWheelTime = currentTime;
+
+ const hasPixel = event.pixelDelta && event.pixelDelta.y !== 0;
+ const deltaY = event.angleDelta.y;
+ const isTraditionalMouse = !hasPixel && Math.abs(deltaY) >= 120 && (Math.abs(deltaY) % 120) === 0;
+ const isHighDpiMouse = !hasPixel && !isTraditionalMouse && deltaY !== 0;
+ const isTouchpad = hasPixel;
+
+ if (isTraditionalMouse) {
+ sessionUsedMouseWheel = true;
+ momentumAnim.running = false;
+ isMomentumActive = false;
+ velocitySamples = [];
+ momentum = 0;
+ momentumVelocity = 0;
+
+ const lines = Math.round(Math.abs(deltaY) / 120);
+ const scrollAmount = (deltaY > 0 ? -lines : lines) * mouseWheelSpeed;
+ let newY = listView.contentY + scrollAmount;
+ const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
+ newY = Math.max(listView.originY, Math.min(maxY, newY));
+
+ if (listView.flicking) {
+ listView.cancelFlick();
+ }
+
+ listView.contentY = newY;
+ savedY = newY;
+ } else if (isHighDpiMouse) {
+ sessionUsedMouseWheel = true;
+ momentumAnim.running = false;
+ isMomentumActive = false;
+ velocitySamples = [];
+ momentum = 0;
+ momentumVelocity = 0;
+
+ let delta = deltaY / 8 * touchpadSpeed;
+ let newY = listView.contentY - delta;
+ const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
+ newY = Math.max(listView.originY, Math.min(maxY, newY));
+
+ if (listView.flicking) {
+ listView.cancelFlick();
+ }
+
+ listView.contentY = newY;
+ savedY = newY;
+ } else if (isTouchpad) {
+ sessionUsedMouseWheel = false;
+ momentumAnim.running = false;
+ isMomentumActive = false;
+
+ let delta = event.pixelDelta.y * touchpadSpeed;
+
+ velocitySamples.push({
+ "delta": delta,
+ "time": currentTime
+ });
+ velocitySamples = velocitySamples.filter(s => currentTime - s.time < Scroll.velocitySampleWindowMs);
+
+ if (velocitySamples.length > 1) {
+ const totalDelta = velocitySamples.reduce((sum, s) => sum + s.delta, 0);
+ const timeSpan = currentTime - velocitySamples[0].time;
+ if (timeSpan > 0) {
+ momentumVelocity = Math.max(-Scroll.maxMomentumVelocity, Math.min(Scroll.maxMomentumVelocity, totalDelta / timeSpan * 1000));
+ }
+ }
+
+ if (timeDelta < Scroll.momentumTimeThreshold) {
+ momentum = momentum * Scroll.momentumRetention + delta * Scroll.momentumDeltaFactor;
+ delta += momentum;
+ } else {
+ momentum = 0;
+ }
+
+ let newY = listView.contentY - delta;
+ const maxY = Math.max(0, listView.contentHeight - listView.height + listView.originY);
+ newY = Math.max(listView.originY, Math.min(maxY, newY));
+
+ if (listView.flicking) {
+ listView.cancelFlick();
+ }
+
+ listView.contentY = newY;
+ savedY = newY;
+ }
+
+ event.accepted = true;
+ }
+
+ onActiveChanged: {
+ if (!active) {
+ isUserScrolling = false;
+ if (!sessionUsedMouseWheel && Math.abs(momentumVelocity) >= Scroll.minMomentumVelocity) {
+ startMomentum();
+ } else {
+ velocitySamples = [];
+ momentumVelocity = 0;
+ }
+ }
+ }
+ }
+
+ FrameAnimation {
+ id: momentumAnim
+ running: false
+
+ onTriggered: {
+ const dt = frameTime;
+ const newY = contentY - momentumVelocity * dt;
+ const maxY = Math.max(0, contentHeight - height + originY);
+ const minY = originY;
+
+ if (newY < minY || newY > maxY) {
+ contentY = newY < minY ? minY : maxY;
+ savedY = contentY;
+ running = false;
+ isMomentumActive = false;
+ momentumVelocity = 0;
+ return;
+ }
+
+ contentY = newY;
+ savedY = newY;
+ momentumVelocity *= Math.pow(friction, dt / 0.016);
+
+ if (Math.abs(momentumVelocity) < Scroll.momentumStopThreshold) {
+ running = false;
+ isMomentumActive = false;
+ momentumVelocity = 0;
+ }
+ }
+ }
+
+ ScrollBar.vertical: DankScrollbar {
+ id: vbar
+ topPadding: listView.scrollBarTopMargin
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankLocationSearch.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankLocationSearch.qml
new file mode 100644
index 0000000..d58538a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankLocationSearch.qml
@@ -0,0 +1,266 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Item {
+ id: root
+
+ activeFocusOnTab: true
+
+ KeyNavigation.tab: keyNavigationTab
+ KeyNavigation.backtab: keyNavigationBacktab
+
+ onActiveFocusChanged: {
+ if (activeFocus) {
+ locationInput.forceActiveFocus();
+ }
+ }
+
+ property string currentLocation: ""
+ property string placeholderText: I18n.tr("Search for a location...")
+ property bool _internalChange: false
+ property bool isLoading: false
+ property string currentSearchText: ""
+ property Item keyNavigationTab: null
+ property Item keyNavigationBacktab: null
+
+ signal locationSelected(string displayName, string coordinates)
+
+ function resetSearchState() {
+ locationSearchTimer.stop();
+ dropdownHideTimer.stop();
+ isLoading = false;
+ searchResultsModel.clear();
+ }
+
+ width: parent.width
+ height: searchInputField.height + (searchDropdown.visible ? searchDropdown.height : 0)
+
+ ListModel {
+ id: searchResultsModel
+ }
+
+ Timer {
+ id: locationSearchTimer
+
+ interval: 500
+ running: false
+ repeat: false
+ onTriggered: {
+ if (locationInput.text.length > 2) {
+ searchResultsModel.clear();
+ root.isLoading = true;
+ const searchLocation = locationInput.text;
+ root.currentSearchText = searchLocation;
+ const encodedLocation = encodeURIComponent(searchLocation);
+ const searchUrl = "https://nominatim.openstreetmap.org/search?q=" + encodedLocation + "&format=json&limit=5&addressdetails=1";
+ Proc.runCommand("locationSearch", ["dms", "dl", "-4", "--timeout", "10", searchUrl], (output, exitCode) => {
+ root.isLoading = false;
+ if (exitCode !== 0) {
+ searchResultsModel.clear();
+ return;
+ }
+ if (root.currentSearchText !== locationInput.text)
+ return;
+ const raw = output.trim();
+ searchResultsModel.clear();
+ if (!raw || raw[0] !== "[") {
+ return;
+ }
+ try {
+ const data = JSON.parse(raw);
+ if (data.length === 0) {
+ return;
+ }
+ for (var i = 0; i < Math.min(data.length, 5); i++) {
+ const location = data[i];
+ if (location.display_name && location.lat && location.lon) {
+ const parts = location.display_name.split(', ');
+ let cleanName = parts[0];
+ if (parts.length > 1) {
+ const state = parts[parts.length - 2];
+ if (state && state !== cleanName)
+ cleanName += `, ${state}`;
+ }
+ const query = `${location.lat},${location.lon}`;
+ searchResultsModel.append({
+ "name": cleanName,
+ "query": query
+ });
+ }
+ }
+ } catch (e) {}
+ });
+ }
+ }
+ }
+
+ Timer {
+ id: dropdownHideTimer
+
+ interval: 200
+ running: false
+ repeat: false
+ onTriggered: {
+ if (!locationInput.getActiveFocus() && !searchDropdown.hovered)
+ root.resetSearchState();
+ }
+ }
+
+ Item {
+ id: searchInputField
+
+ width: parent.width
+ height: 48
+
+ DankTextField {
+ id: locationInput
+
+ width: parent.width
+ height: parent.height
+ leftIconName: "search"
+ placeholderText: root.placeholderText
+ text: ""
+ backgroundColor: Theme.surfaceVariant
+ normalBorderColor: Theme.primarySelected
+ focusedBorderColor: Theme.primary
+ keyNavigationTab: root.keyNavigationTab
+ keyNavigationBacktab: root.keyNavigationBacktab
+ onTextEdited: {
+ if (root._internalChange)
+ return;
+ if (getActiveFocus()) {
+ if (text.length > 2) {
+ root.isLoading = true;
+ locationSearchTimer.restart();
+ } else {
+ root.resetSearchState();
+ }
+ }
+ }
+ onFocusStateChanged: hasFocus => {
+ if (hasFocus) {
+ dropdownHideTimer.stop();
+ } else {
+ dropdownHideTimer.start();
+ }
+ }
+ }
+
+ DankIcon {
+ name: root.isLoading ? "hourglass_empty" : (searchResultsModel.count > 0 ? "check_circle" : "error")
+ size: Theme.iconSize - 4
+ color: root.isLoading ? Theme.surfaceVariantText : (searchResultsModel.count > 0 ? Theme.primary : Theme.error)
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ opacity: (locationInput.getActiveFocus() && locationInput.text.length > 2) ? 1 : 0
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+
+ StyledRect {
+ id: searchDropdown
+
+ property bool hovered: false
+
+ width: parent.width
+ height: Math.min(Math.max(searchResultsModel.count * 38 + Theme.spacingS * 2, 50), 200)
+ y: searchInputField.height
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ border.color: Theme.primarySelected
+ border.width: 1
+ visible: locationInput.getActiveFocus() && locationInput.text.length > 2 && (searchResultsModel.count > 0 || root.isLoading)
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ onEntered: {
+ parent.hovered = true;
+ dropdownHideTimer.stop();
+ }
+ onExited: {
+ parent.hovered = false;
+ if (!locationInput.getActiveFocus())
+ dropdownHideTimer.start();
+ }
+ acceptedButtons: Qt.NoButton
+ }
+
+ Item {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+
+ DankListView {
+ id: searchResultsList
+
+ anchors.fill: parent
+ clip: true
+ model: searchResultsModel
+ spacing: 2
+
+ delegate: StyledRect {
+ width: searchResultsList.width
+ height: 36
+ radius: Theme.cornerRadius
+ color: resultMouseArea.containsMouse ? Theme.surfaceLight : "transparent"
+
+ Row {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "place"
+ size: Theme.iconSize - 6
+ color: Theme.surfaceVariantText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: model.name || "Unknown"
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ elide: Text.ElideRight
+ width: parent.width - 30
+ }
+ }
+
+ MouseArea {
+ id: resultMouseArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ root._internalChange = true;
+ const selectedName = model.name;
+ const selectedQuery = model.query;
+ locationInput.text = selectedName;
+ root.locationSelected(selectedName, selectedQuery);
+ root.resetSearchState();
+ locationInput.setFocus(false);
+ root._internalChange = false;
+ }
+ }
+ }
+ }
+
+ StyledText {
+ anchors.centerIn: parent
+ text: root.isLoading ? "Searching..." : "No locations found"
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ visible: searchResultsList.count === 0 && locationInput.text.length > 2
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankNFIcon.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankNFIcon.qml
new file mode 100644
index 0000000..67d2f69
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankNFIcon.qml
@@ -0,0 +1,164 @@
+import QtQuick
+import qs.Common
+
+Item {
+ id: root
+
+ property string name: ""
+ property int size: Theme.fontSizeMedium
+ property alias color: icon.color
+
+ width: size
+ height: size
+ visible: text.length > 0
+
+ // This is for file browser, particularly - might want another map later for app IDs
+ readonly property var iconMap: ({
+ // --- Distribution logos ---
+ "debian": "\u{f08da}",
+ "arch": "\u{f08c7}",
+ "archcraft": "\u{f345}",
+ "guix": "\u{f325}",
+ "fedora": "\u{f08db}",
+ "nixos": "\u{f1105}",
+ "ubuntu": "\u{f0548}",
+ "gentoo": "\u{f08e8}",
+ "endeavouros": "\u{f322}",
+ "manjaro": "\u{f160a}",
+ "opensuse": "\u{f314}",
+ "artix": "\u{f31f}",
+ "void": "\u{f32e}",
+
+ // --- special types ---
+ "folder": "\u{F024B}",
+ "file": "\u{F0214}",
+
+ // --- special filenames (no extension) ---
+ "docker": "\u{F0868}",
+ "makefile": "\u{F09EE}",
+ "license": "\u{F09EE}",
+ "readme": "\u{F0354}",
+
+ // --- programming languages ---
+ "rs": "\u{F1617}",
+ "dart": "\u{e798}",
+ "go": "\u{F07D3}",
+ "py": "\u{F0320}",
+ "js": "\u{F031E}",
+ "jsx": "\u{F031E}",
+ "ts": "\u{F06E6}",
+ "tsx": "\u{F06E6}",
+ "java": "\u{F0B37}",
+ "c": "\u{F0671}",
+ "cpp": "\u{F0672}",
+ "cxx": "\u{F0672}",
+ "h": "\u{F0672}",
+ "hpp": "\u{F0672}",
+ "cs": "\u{F031B}",
+ "html": "\u{e60e}",
+ "htm": "\u{e60e}",
+ "css": "\u{E6b8}",
+ "scss": "\u{F031C}",
+ "less": "\u{F031C}",
+ "md": "\u{F0354}",
+ "markdown": "\u{F0354}",
+ "json": "\u{eb0f}",
+ "jsonc": "\u{eb0f}",
+ "yaml": "\u{e8eb}",
+ "yml": "\u{e8eb}",
+ "xml": "\u{F09EE}",
+ "sql": "\u{f1c0}",
+
+ // --- scripts / shells ---
+ "sh": "\u{f0bc1}",
+ "bash": "\u{f0bc1}",
+ "zsh": "\u{f0bc1}",
+ "fish": "\u{f0bc1}",
+ "ps1": "\u{f0bc1}",
+ "bat": "\u{f0bc1}",
+
+ // --- data / config ---
+ "toml": "\u{e6b2}",
+ "ini": "\u{F09EE}",
+ "conf": "\u{F09EE}",
+ "cfg": "\u{F09EE}",
+ "csv": "\u{eefc}",
+ "tsv": "\u{F021C}",
+
+ // --- docs / office ---
+ "pdf": "\u{F0226}",
+ "doc": "\u{F09EE}",
+ "docx": "\u{F09EE}",
+ "rtf": "\u{F09EE}",
+ "ppt": "\u{F09EE}",
+ "pptx": "\u{F09EE}",
+ "log": "\u{F09EE}",
+ "xls": "\u{F021C}",
+ "xlsx": "\u{F021C}",
+
+ // --- images ---
+ "ico": "\u{F021F}",
+
+ // --- audio / video ---
+ "mp3": "\u{e638}",
+ "wav": "\u{e638}",
+ "flac": "\u{e638}",
+ "ogg": "\u{e638}",
+ "mp4": "\u{f0567}",
+ "mkv": "\u{f0567}",
+ "webm": "\u{f0567}",
+ "mov": "\u{f0567}",
+
+ // --- archives / packages ---
+ "zip": "\u{e6aa}",
+ "tar": "\u{f003c}",
+ "gz": "\u{f003c}",
+ "bz2": "\u{f003c}",
+ "7z": "\u{f003c}",
+
+ // --- containers / infra / cloud ---
+ "dockerfile": "\u{F0868}",
+ "yml.k8s": "\u{F09EE}",
+ "yaml.k8s": "\u{F09EE}",
+ "tf": "\u{F09EE}",
+ "tfvars": "\u{F09EE}",
+
+ // --- moon phases
+ "moon_new": "\u{F0F64}",
+ "moon_waxing_crescent": "\u{F0F67}",
+ "moon_first_quarter": "\u{F0F61}",
+ "moon_waxing_gibbous": "\u{F0F68}",
+ "moon_full": "\u{F0F62}",
+ "moon_waning_gibbous": "\u{F0F66}",
+ "moon_last_quarter": "\u{F0F63}",
+ "moon_waning_crescent": "\u{F0F65}"
+ })
+
+ readonly property string text: iconMap[name] || iconMap["file"] || ""
+
+ function getIconForFile(fileName) {
+ const lowerName = fileName.toLowerCase();
+ if (lowerName.startsWith("dockerfile")) {
+ return "docker";
+ }
+ const ext = fileName.split('.').pop();
+ return ext || "";
+ }
+
+ FontLoader {
+ id: firaCodeFont
+ source: Qt.resolvedUrl("../assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf")
+ }
+
+ StyledText {
+ id: icon
+
+ anchors.centerIn: parent
+
+ font.family: firaCodeFont.name
+ font.pixelSize: root.size
+ color: Theme.surfaceText
+ text: root.text
+ antialiasing: true
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankNumberStepper.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankNumberStepper.qml
new file mode 100644
index 0000000..f4dcf5a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankNumberStepper.qml
@@ -0,0 +1,67 @@
+import QtCore
+import QtQuick
+import qs.Common
+
+Column {
+ id: root
+ property string text: ""
+ property var incrementTooltipText: ""
+ property var decrementTooltipText: ""
+ property var onIncrement: undefined
+ property var onDecrement: undefined
+ property string incrementIconName: "keyboard_arrow_up"
+ property string decrementIconName: "keyboard_arrow_down"
+
+ property bool incrementEnabled: true
+ property bool decrementEnabled: true
+
+ property color textColor: Theme.surfaceText
+ property color iconColor: Theme.withAlpha(Theme.surfaceText, 0.5)
+ property color backgroundColor: Theme.primary
+
+ property int textSize: Theme.fontSizeSmall
+ property var iconSize: 12
+ property int buttonSize: 20
+ property int horizontalPadding: Theme.spacingL
+
+ readonly property bool effectiveIncrementEnabled: root.onIncrement ? root.incrementEnabled : false
+ readonly property bool effectiveDecrementEnabled: root.onDecrement ? root.decrementEnabled : false
+
+
+ width: Math.max(buttonSize * 2, root.implicitWidth + horizontalPadding * 2)
+ spacing: 4
+
+ DankActionButton {
+ anchors.horizontalCenter: parent.horizontalCenter
+ enabled: root.effectiveIncrementEnabled
+ iconColor: root.effectiveIncrementEnabled ? root.iconColor : Theme.blendAlpha(root.iconColor, 0.5)
+ iconSize: root.iconSize
+ buttonSize: root.buttonSize
+ iconName: root.incrementIconName
+ onClicked: if (typeof root.onIncrement === 'function') root.onIncrement()
+ tooltipText: root.incrementTooltipText
+ }
+
+ Row {
+ anchors.horizontalCenter: parent.horizontalCenter
+ Item { width: 5; height: 1 }
+ StyledText {
+ isMonospace: true
+ text: root.text
+ font.pixelSize: root.textSize
+ color: root.textColor
+ }
+ Item { width: 5; height: 1 }
+ }
+
+ DankActionButton {
+ anchors.horizontalCenter: parent.horizontalCenter
+ enabled: root.effectiveDecrementEnabled
+ iconColor: root.effectiveDecrementEnabled ? root.iconColor : Theme.blendAlpha(root.iconColor, 0.5)
+ iconSize: root.iconSize
+ buttonSize: root.buttonSize
+ iconName: root.decrementIconName
+ onClicked: if (typeof root.onDecrement === 'function') root.onDecrement()
+ tooltipText: root.decrementTooltipText
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankOSD.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankOSD.qml
new file mode 100644
index 0000000..0eb0e5a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankOSD.qml
@@ -0,0 +1,334 @@
+import QtQuick
+import Quickshell
+import Quickshell.Wayland
+import qs.Common
+import qs.Services
+
+PanelWindow {
+ id: root
+
+ property string blurNamespace: "dms:osd"
+ WlrLayershell.namespace: blurNamespace
+
+ property alias content: contentLoader.sourceComponent
+ property alias contentLoader: contentLoader
+ property var modelData
+ property bool shouldBeVisible: false
+ property int autoHideInterval: 2000
+ property bool enableMouseInteraction: false
+ property real osdWidth: Theme.iconSize + Theme.spacingS * 2
+ property real osdHeight: Theme.iconSize + Theme.spacingS * 2
+ property int animationDuration: Theme.mediumDuration
+ property var animationEasing: Theme.emphasizedEasing
+
+ signal osdShown
+ signal osdHidden
+
+ function show() {
+ if (SessionData.suppressOSD)
+ return;
+ if (shouldBeVisible) {
+ hideTimer.restart();
+ return;
+ }
+ OSDManager.showOSD(root);
+ closeTimer.stop();
+ shouldBeVisible = true;
+ visible = true;
+ hideTimer.restart();
+ osdShown();
+ }
+
+ function hide() {
+ shouldBeVisible = false;
+ closeTimer.restart();
+ }
+
+ function resetHideTimer() {
+ if (shouldBeVisible) {
+ hideTimer.restart();
+ }
+ }
+
+ function updateHoverState() {
+ let isHovered = (enableMouseInteraction && mouseArea.containsMouse) || osdContainer.childHovered;
+ if (enableMouseInteraction) {
+ if (isHovered) {
+ hideTimer.stop();
+ } else if (shouldBeVisible) {
+ hideTimer.restart();
+ }
+ }
+ }
+
+ function setChildHovered(hovered) {
+ osdContainer.childHovered = hovered;
+ updateHoverState();
+ }
+
+ screen: modelData
+ visible: false
+
+ Connections {
+ target: Quickshell
+ function onScreensChanged() {
+ if (!root.visible && !root.shouldBeVisible)
+ return;
+ const currentScreenName = root.screen?.name;
+ if (!currentScreenName) {
+ root.hide();
+ return;
+ }
+ for (let i = 0; i < Quickshell.screens.length; i++) {
+ if (Quickshell.screens[i].name === currentScreenName)
+ return;
+ }
+ root.shouldBeVisible = false;
+ root.visible = false;
+ hideTimer.stop();
+ closeTimer.stop();
+ osdHidden();
+ }
+ }
+
+ WlrLayershell.layer: {
+ switch (Quickshell.env("DMS_OSD_LAYER")) {
+ case "bottom":
+ console.warn("DankOSD: 'bottom' layer is not valid for OSDs. Defaulting to 'overlay' layer.");
+ return WlrLayershell.Overlay;
+ case "background":
+ console.warn("DankOSD: 'background' layer is not valid for OSDs. Defaulting to 'overlay' layer.");
+ return WlrLayershell.Overlay;
+ case "top":
+ return WlrLayershell.Top;
+ default:
+ return WlrLayershell.Overlay;
+ }
+ }
+ WlrLayershell.exclusiveZone: -1
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
+
+ WindowBlur {
+ targetWindow: root
+ blurX: shadowBuffer
+ blurY: shadowBuffer
+ blurWidth: shouldBeVisible ? alignedWidth : 0
+ blurHeight: shouldBeVisible ? alignedHeight : 0
+ blurRadius: Theme.cornerRadius
+ }
+
+ color: "transparent"
+
+ readonly property real dpr: CompositorService.getScreenScale(screen)
+ readonly property real screenWidth: screen.width
+ readonly property real screenHeight: screen.height
+ readonly property real shadowBuffer: 15
+ readonly property real alignedWidth: Theme.px(osdWidth, dpr)
+ readonly property real alignedHeight: Theme.px(osdHeight, dpr)
+
+ readonly property bool isVerticalLayout: SettingsData.osdPosition === SettingsData.Position.LeftCenter || SettingsData.osdPosition === SettingsData.Position.RightCenter
+
+ readonly property real barThickness: {
+ const defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default");
+ if (!defaultBar || !(defaultBar.visible ?? true))
+ return 0;
+ const innerPadding = defaultBar.innerPadding ?? 4;
+ const widgetThickness = Math.max(20, 26 + innerPadding * 0.6);
+ return Math.max(widgetThickness + innerPadding + 4, Theme.barHeight - 4 - (8 - innerPadding));
+ }
+
+ readonly property real barOffset: {
+ const defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default");
+ if (!defaultBar || !(defaultBar.visible ?? true))
+ return 0;
+ const spacing = defaultBar.spacing ?? 4;
+ const bottomGap = defaultBar.bottomGap ?? 0;
+ return barThickness + spacing + bottomGap;
+ }
+
+ readonly property real dockThickness: {
+ if (!SettingsData.showDock)
+ return 0;
+ return SettingsData.dockIconSize + SettingsData.dockSpacing * 2 + 10;
+ }
+
+ readonly property real dockOffset: {
+ if (!SettingsData.showDock || SettingsData.dockAutoHide || SettingsData.dockSmartAutoHide)
+ return 0;
+ return dockThickness + SettingsData.dockSpacing + SettingsData.dockBottomGap + SettingsData.dockMargin;
+ }
+
+ readonly property real alignedX: {
+ const margin = Theme.spacingM;
+ const centerX = (screenWidth - alignedWidth) / 2;
+
+ const defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default");
+ const barPos = defaultBar?.position ?? SettingsData.Position.Top;
+
+ switch (SettingsData.osdPosition) {
+ case SettingsData.Position.Left:
+ case SettingsData.Position.Bottom:
+ const leftBarOffset = barPos === SettingsData.Position.Left ? barOffset : 0;
+ const leftDockOffset = SettingsData.dockPosition === SettingsData.Position.Left ? dockOffset : 0;
+ return Theme.snap(margin + Math.max(leftBarOffset, leftDockOffset), dpr);
+ case SettingsData.Position.Top:
+ case SettingsData.Position.Right:
+ const rightBarOffset = barPos === SettingsData.Position.Right ? barOffset : 0;
+ const rightDockOffset = SettingsData.dockPosition === SettingsData.Position.Right ? dockOffset : 0;
+ return Theme.snap(screenWidth - alignedWidth - margin - Math.max(rightBarOffset, rightDockOffset), dpr);
+ case SettingsData.Position.LeftCenter:
+ const leftCenterBarOffset = barPos === SettingsData.Position.Left ? barOffset : 0;
+ const leftCenterDockOffset = SettingsData.dockPosition === SettingsData.Position.Left ? dockOffset : 0;
+ return Theme.snap(margin + Math.max(leftCenterBarOffset, leftCenterDockOffset), dpr);
+ case SettingsData.Position.RightCenter:
+ const rightCenterBarOffset = barPos === SettingsData.Position.Right ? barOffset : 0;
+ const rightCenterDockOffset = SettingsData.dockPosition === SettingsData.Position.Right ? dockOffset : 0;
+ return Theme.snap(screenWidth - alignedWidth - margin - Math.max(rightCenterBarOffset, rightCenterDockOffset), dpr);
+ case SettingsData.Position.TopCenter:
+ case SettingsData.Position.BottomCenter:
+ default:
+ return Theme.snap(centerX, dpr);
+ }
+ }
+
+ readonly property real alignedY: {
+ const margin = Theme.spacingM;
+ const centerY = (screenHeight - alignedHeight) / 2;
+
+ const defaultBar = SettingsData.barConfigs[0] || SettingsData.getBarConfig("default");
+ const barPos = defaultBar?.position ?? SettingsData.Position.Top;
+
+ switch (SettingsData.osdPosition) {
+ case SettingsData.Position.Top:
+ case SettingsData.Position.Left:
+ case SettingsData.Position.TopCenter:
+ const topBarOffset = barPos === SettingsData.Position.Top ? barOffset : 0;
+ const topDockOffset = SettingsData.dockPosition === SettingsData.Position.Top ? dockOffset : 0;
+ return Theme.snap(margin + Math.max(topBarOffset, topDockOffset), dpr);
+ case SettingsData.Position.Right:
+ case SettingsData.Position.Bottom:
+ case SettingsData.Position.BottomCenter:
+ const bottomBarOffset = barPos === SettingsData.Position.Bottom ? barOffset : 0;
+ const bottomDockOffset = SettingsData.dockPosition === SettingsData.Position.Bottom ? dockOffset : 0;
+ return Theme.snap(screenHeight - alignedHeight - margin - Math.max(bottomBarOffset, bottomDockOffset), dpr);
+ case SettingsData.Position.LeftCenter:
+ case SettingsData.Position.RightCenter:
+ default:
+ return Theme.snap(centerY, dpr);
+ }
+ }
+
+ anchors {
+ top: true
+ left: true
+ }
+
+ WlrLayershell.margins {
+ left: Math.max(0, Theme.snap(alignedX - shadowBuffer, dpr))
+ top: Math.max(0, Theme.snap(alignedY - shadowBuffer, dpr))
+ }
+
+ implicitWidth: alignedWidth + (shadowBuffer * 2)
+ implicitHeight: alignedHeight + (shadowBuffer * 2)
+
+ Timer {
+ id: hideTimer
+
+ interval: autoHideInterval
+ repeat: false
+ onTriggered: {
+ if (!enableMouseInteraction || !mouseArea.containsMouse) {
+ hide();
+ } else {
+ hideTimer.restart();
+ }
+ }
+ }
+
+ Timer {
+ id: closeTimer
+ interval: animationDuration + 50
+ onTriggered: {
+ if (!shouldBeVisible) {
+ visible = false;
+ osdHidden();
+ }
+ }
+ }
+
+ Item {
+ id: osdContainer
+ x: shadowBuffer
+ y: shadowBuffer
+ width: alignedWidth
+ height: alignedHeight
+ opacity: shouldBeVisible ? 1 : 0
+ scale: shouldBeVisible ? 1 : 0.9
+
+ property bool childHovered: false
+ readonly property real popupSurfaceAlpha: Theme.popupTransparency
+
+ Rectangle {
+ id: background
+ anchors.fill: parent
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainer, osdContainer.popupSurfaceAlpha)
+ border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
+ border.width: BlurService.enabled ? BlurService.borderWidth : 1
+ z: -1
+ }
+
+ ElevationShadow {
+ id: bgShadowLayer
+ anchors.fill: parent
+ visible: osdContainer.popupSurfaceAlpha >= 0.95
+ z: -1
+ level: Theme.elevationLevel3
+ fallbackOffset: 6
+ targetRadius: Theme.cornerRadius
+ targetColor: Theme.withAlpha(Theme.surfaceContainer, osdContainer.popupSurfaceAlpha)
+ borderColor: Theme.outlineMedium
+ borderWidth: 1
+ shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
+ layer.textureSize: Qt.size(Math.round(width * root.dpr), Math.round(height * root.dpr))
+ layer.textureMirroring: ShaderEffectSource.MirrorVertically
+ }
+
+ MouseArea {
+ id: mouseArea
+ anchors.fill: parent
+ hoverEnabled: enableMouseInteraction
+ acceptedButtons: Qt.NoButton
+ propagateComposedEvents: true
+ z: -1
+ onContainsMouseChanged: updateHoverState()
+ }
+
+ onChildHoveredChanged: updateHoverState()
+
+ Loader {
+ id: contentLoader
+ anchors.fill: parent
+ active: root.visible
+ asynchronous: false
+ }
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: animationDuration
+ easing.type: animationEasing
+ }
+ }
+
+ Behavior on scale {
+ NumberAnimation {
+ duration: animationDuration
+ easing.type: animationEasing
+ }
+ }
+ }
+
+ mask: Region {
+ item: bgShadowLayer
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankPopout.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankPopout.qml
new file mode 100644
index 0000000..fb5f0b0
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankPopout.qml
@@ -0,0 +1,620 @@
+import QtQuick
+import Quickshell
+import Quickshell.Wayland
+import qs.Common
+import qs.Services
+
+Item {
+ id: root
+
+ property string layerNamespace: "dms:popout"
+ property alias content: contentLoader.sourceComponent
+ property alias contentLoader: contentLoader
+ property Component overlayContent: null
+ property alias overlayLoader: overlayLoader
+ property real popupWidth: 400
+ property real popupHeight: 300
+ property real triggerX: 0
+ property real triggerY: 0
+ property real triggerWidth: 40
+ property string triggerSection: ""
+ property string positioning: "center"
+ property int animationDuration: Theme.popoutAnimationDuration
+ property real animationScaleCollapsed: 0.96
+ property real animationOffset: Theme.spacingL
+ property list<real> animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial
+ property list<real> animationExitCurve: Theme.expressiveCurves.emphasized
+ property bool suspendShadowWhileResizing: false
+ property bool shouldBeVisible: false
+ property var customKeyboardFocus: null
+ property bool backgroundInteractive: true
+ property bool contentHandlesKeys: false
+ property bool fullHeightSurface: false
+ property bool _primeContent: false
+ property bool _resizeActive: false
+ property real _surfaceMarginLeft: 0
+ property real _surfaceW: 0
+
+ property real storedBarThickness: Theme.barHeight - 4
+ property real storedBarSpacing: 4
+ property var storedBarConfig: null
+ property var adjacentBarInfo: ({
+ "topBar": 0,
+ "bottomBar": 0,
+ "leftBar": 0,
+ "rightBar": 0
+ })
+ property var screen: null
+
+ readonly property real effectiveBarThickness: {
+ const padding = storedBarConfig ? (storedBarConfig.innerPadding !== undefined ? storedBarConfig.innerPadding : 4) : 4;
+ return Math.max(26 + padding * 0.6, Theme.barHeight - 4 - (8 - padding)) + storedBarSpacing;
+ }
+
+ readonly property var barBounds: {
+ if (!screen)
+ return {
+ "x": 0,
+ "y": 0,
+ "width": 0,
+ "height": 0,
+ "wingSize": 0
+ };
+ return SettingsData.getBarBounds(screen, effectiveBarThickness, effectiveBarPosition, storedBarConfig);
+ }
+
+ readonly property real barX: barBounds.x
+ readonly property real barY: barBounds.y
+ readonly property real barWidth: barBounds.width
+ readonly property real barHeight: barBounds.height
+ readonly property real barWingSize: barBounds.wingSize
+
+ signal opened
+ signal popoutClosed
+ signal backgroundClicked
+
+ property var _lastOpenedScreen: null
+
+ property int effectiveBarPosition: 0
+ property real effectiveBarBottomGap: 0
+ readonly property string autoBarShadowDirection: {
+ const section = triggerSection || "center";
+ switch (effectiveBarPosition) {
+ case SettingsData.Position.Top:
+ if (section === "left")
+ return "topLeft";
+ if (section === "right")
+ return "topRight";
+ return "top";
+ case SettingsData.Position.Bottom:
+ if (section === "left")
+ return "bottomLeft";
+ if (section === "right")
+ return "bottomRight";
+ return "bottom";
+ case SettingsData.Position.Left:
+ if (section === "left")
+ return "topLeft";
+ if (section === "right")
+ return "bottomLeft";
+ return "left";
+ case SettingsData.Position.Right:
+ if (section === "left")
+ return "topRight";
+ if (section === "right")
+ return "bottomRight";
+ return "right";
+ default:
+ return "top";
+ }
+ }
+ readonly property string effectiveShadowDirection: Theme.elevationLightDirection === "autoBar" ? autoBarShadowDirection : Theme.elevationLightDirection
+
+ // Snapshot mask geometry to prevent background damage on bar updates
+ property real _frozenMaskX: 0
+ property real _frozenMaskY: 0
+ property real _frozenMaskWidth: 0
+ property real _frozenMaskHeight: 0
+
+ function setBarContext(position, bottomGap) {
+ effectiveBarPosition = position !== undefined ? position : 0;
+ effectiveBarBottomGap = bottomGap !== undefined ? bottomGap : 0;
+ }
+
+ function primeContent() {
+ _primeContent = true;
+ }
+
+ function clearPrimedContent() {
+ _primeContent = false;
+ }
+
+ function setTriggerPosition(x, y, width, section, targetScreen, barPosition, barThickness, barSpacing, barConfig) {
+ triggerX = x;
+ triggerY = y;
+ triggerWidth = width;
+ triggerSection = section;
+ screen = targetScreen;
+
+ storedBarThickness = barThickness !== undefined ? barThickness : (Theme.barHeight - 4);
+ storedBarSpacing = barSpacing !== undefined ? barSpacing : 4;
+ storedBarConfig = barConfig;
+
+ const pos = barPosition !== undefined ? barPosition : 0;
+ const bottomGap = barConfig ? (barConfig.bottomGap !== undefined ? barConfig.bottomGap : 0) : 0;
+
+ adjacentBarInfo = SettingsData.getAdjacentBarInfo(targetScreen, pos, barConfig);
+ setBarContext(pos, bottomGap);
+ }
+
+ readonly property bool useBackgroundWindow: !CompositorService.isHyprland || CompositorService.useHyprlandFocusGrab
+
+ function updateSurfacePosition() {
+ if (useBackgroundWindow && shouldBeVisible) {
+ _surfaceMarginLeft = alignedX - shadowBuffer;
+ _surfaceW = alignedWidth + shadowBuffer * 2;
+ }
+ }
+
+ function open() {
+ if (!screen)
+ return;
+ closeTimer.stop();
+
+ // Snapshot mask geometry
+ _frozenMaskX = maskX;
+ _frozenMaskY = maskY;
+ _frozenMaskWidth = maskWidth;
+ _frozenMaskHeight = maskHeight;
+
+ if (_lastOpenedScreen !== null && _lastOpenedScreen !== screen) {
+ contentWindow.visible = false;
+ if (useBackgroundWindow)
+ backgroundWindow.visible = false;
+ }
+ _lastOpenedScreen = screen;
+
+ shouldBeVisible = true;
+ if (useBackgroundWindow) {
+ _surfaceMarginLeft = alignedX - shadowBuffer;
+ _surfaceW = alignedWidth + shadowBuffer * 2;
+ }
+ Qt.callLater(() => {
+ if (shouldBeVisible && screen) {
+ if (useBackgroundWindow)
+ backgroundWindow.visible = true;
+ contentWindow.visible = true;
+ PopoutManager.showPopout(root);
+ opened();
+ }
+ });
+ }
+
+ function close() {
+ shouldBeVisible = false;
+ _primeContent = false;
+ PopoutManager.popoutChanged();
+ closeTimer.restart();
+ }
+
+ function toggle() {
+ shouldBeVisible ? close() : open();
+ }
+
+ Connections {
+ target: Quickshell
+ function onScreensChanged() {
+ if (!shouldBeVisible || !screen)
+ return;
+ const currentScreenName = screen.name;
+ let screenStillExists = false;
+ for (let i = 0; i < Quickshell.screens.length; i++) {
+ if (Quickshell.screens[i].name === currentScreenName) {
+ screenStillExists = true;
+ break;
+ }
+ }
+ if (!screenStillExists) {
+ close();
+ }
+ }
+ }
+
+ Timer {
+ id: closeTimer
+ interval: animationDuration
+ onTriggered: {
+ if (!shouldBeVisible) {
+ contentWindow.visible = false;
+ if (useBackgroundWindow)
+ backgroundWindow.visible = false;
+ PopoutManager.hidePopout(root);
+ popoutClosed();
+ }
+ }
+ }
+
+ readonly property real screenWidth: screen ? screen.width : 0
+ readonly property real screenHeight: screen ? screen.height : 0
+ readonly property real dpr: screen ? screen.devicePixelRatio : 1
+
+ readonly property var shadowLevel: Theme.elevationLevel3
+ readonly property real shadowFallbackOffset: 6
+ readonly property real shadowRenderPadding: (Theme.elevationEnabled && SettingsData.popoutElevationEnabled) ? Theme.elevationRenderPadding(shadowLevel, effectiveShadowDirection, shadowFallbackOffset, 8, 16) : 0
+ readonly property real shadowMotionPadding: Math.max(0, animationOffset)
+ readonly property real shadowBuffer: Theme.snap(shadowRenderPadding + shadowMotionPadding, dpr)
+ readonly property real alignedWidth: Theme.px(popupWidth, dpr)
+ readonly property real alignedHeight: Theme.px(popupHeight, dpr)
+
+ onAlignedHeightChanged: {
+ if (!suspendShadowWhileResizing || !shouldBeVisible)
+ return;
+ _resizeActive = true;
+ resizeSettleTimer.restart();
+ }
+ onShouldBeVisibleChanged: {
+ if (!shouldBeVisible) {
+ _resizeActive = false;
+ resizeSettleTimer.stop();
+ }
+ }
+
+ Timer {
+ id: resizeSettleTimer
+ interval: 80
+ repeat: false
+ onTriggered: root._resizeActive = false
+ }
+
+ readonly property real alignedX: Theme.snap((() => {
+ const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
+ const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4;
+ const popupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
+
+ switch (effectiveBarPosition) {
+ case SettingsData.Position.Left:
+ return Math.max(popupGap, Math.min(screenWidth - popupWidth - popupGap, triggerX));
+ case SettingsData.Position.Right:
+ return Math.max(popupGap, Math.min(screenWidth - popupWidth - popupGap, triggerX - popupWidth));
+ default:
+ const rawX = triggerX + (triggerWidth / 2) - (popupWidth / 2);
+ const minX = adjacentBarInfo.leftBar > 0 ? adjacentBarInfo.leftBar : popupGap;
+ const maxX = screenWidth - popupWidth - (adjacentBarInfo.rightBar > 0 ? adjacentBarInfo.rightBar : popupGap);
+ return Math.max(minX, Math.min(maxX, rawX));
+ }
+ })(), dpr)
+
+ readonly property real alignedY: Theme.snap((() => {
+ const useAutoGaps = storedBarConfig?.popupGapsAuto !== undefined ? storedBarConfig.popupGapsAuto : true;
+ const manualGapValue = storedBarConfig?.popupGapsManual !== undefined ? storedBarConfig.popupGapsManual : 4;
+ const popupGap = useAutoGaps ? Math.max(4, storedBarSpacing) : manualGapValue;
+
+ switch (effectiveBarPosition) {
+ case SettingsData.Position.Bottom:
+ return Math.max(popupGap, Math.min(screenHeight - popupHeight - popupGap, triggerY - popupHeight));
+ case SettingsData.Position.Top:
+ return Math.max(popupGap, Math.min(screenHeight - popupHeight - popupGap, triggerY));
+ default:
+ const rawY = triggerY - (popupHeight / 2);
+ const minY = adjacentBarInfo.topBar > 0 ? adjacentBarInfo.topBar : popupGap;
+ const maxY = screenHeight - popupHeight - (adjacentBarInfo.bottomBar > 0 ? adjacentBarInfo.bottomBar : popupGap);
+ return Math.max(minY, Math.min(maxY, rawY));
+ }
+ })(), dpr)
+
+ readonly property real triggeringBarLeftExclusion: (effectiveBarPosition === SettingsData.Position.Left && barWidth > 0) ? Math.max(0, barX + barWidth) : 0
+ readonly property real triggeringBarTopExclusion: (effectiveBarPosition === SettingsData.Position.Top && barHeight > 0) ? Math.max(0, barY + barHeight) : 0
+ readonly property real triggeringBarRightExclusion: (effectiveBarPosition === SettingsData.Position.Right && barWidth > 0) ? Math.max(0, screenWidth - barX) : 0
+ readonly property real triggeringBarBottomExclusion: (effectiveBarPosition === SettingsData.Position.Bottom && barHeight > 0) ? Math.max(0, screenHeight - barY) : 0
+
+ readonly property real maskX: {
+ const adjacentLeftBar = adjacentBarInfo?.leftBar ?? 0;
+ return Math.max(triggeringBarLeftExclusion, adjacentLeftBar);
+ }
+
+ readonly property real maskY: {
+ const adjacentTopBar = adjacentBarInfo?.topBar ?? 0;
+ return Math.max(triggeringBarTopExclusion, adjacentTopBar);
+ }
+
+ readonly property real maskWidth: {
+ const adjacentRightBar = adjacentBarInfo?.rightBar ?? 0;
+ const rightExclusion = Math.max(triggeringBarRightExclusion, adjacentRightBar);
+ return Math.max(100, screenWidth - maskX - rightExclusion);
+ }
+
+ readonly property real maskHeight: {
+ const adjacentBottomBar = adjacentBarInfo?.bottomBar ?? 0;
+ const bottomExclusion = Math.max(triggeringBarBottomExclusion, adjacentBottomBar);
+ return Math.max(100, screenHeight - maskY - bottomExclusion);
+ }
+
+ PanelWindow {
+ id: backgroundWindow
+ screen: root.screen
+ visible: false
+ color: "transparent"
+ Component.onCompleted: {
+ if (typeof updatesEnabled !== "undefined" && !root.overlayContent)
+ updatesEnabled = false;
+ }
+
+ WlrLayershell.namespace: root.layerNamespace + ":background"
+ WlrLayershell.layer: WlrLayershell.Top
+ WlrLayershell.exclusiveZone: -1
+ WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
+
+ anchors {
+ top: true
+ left: true
+ right: true
+ bottom: true
+ }
+
+ mask: Region {
+ item: maskRect
+ }
+
+ Rectangle {
+ id: maskRect
+ visible: false
+ color: "transparent"
+ x: root._frozenMaskX
+ y: root._frozenMaskY
+ width: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskWidth : 0
+ height: (shouldBeVisible && backgroundInteractive) ? root._frozenMaskHeight : 0
+ }
+
+ MouseArea {
+ x: root._frozenMaskX
+ y: root._frozenMaskY
+ width: root._frozenMaskWidth
+ height: root._frozenMaskHeight
+ hoverEnabled: false
+ enabled: shouldBeVisible && backgroundInteractive
+ acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
+ onClicked: mouse => {
+ const clickX = mouse.x + root._frozenMaskX;
+ const clickY = mouse.y + root._frozenMaskY;
+ const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.alignedY || clickY > root.alignedY + root.alignedHeight;
+
+ if (!outsideContent)
+ return;
+ backgroundClicked();
+ }
+ }
+
+ Loader {
+ id: overlayLoader
+ anchors.fill: parent
+ active: root.overlayContent !== null && backgroundWindow.visible
+ sourceComponent: root.overlayContent
+ }
+ }
+
+ PanelWindow {
+ id: contentWindow
+ screen: root.screen
+ visible: false
+ color: "transparent"
+
+ WindowBlur {
+ id: popoutBlur
+ targetWindow: contentWindow
+ readonly property real s: Math.min(1, contentContainer.scaleValue)
+ blurX: contentContainer.x + contentContainer.width * (1 - s) * 0.5 + Theme.snap(contentContainer.animX, root.dpr)
+ blurY: contentContainer.y + contentContainer.height * (1 - s) * 0.5 + Theme.snap(contentContainer.animY, root.dpr)
+ blurWidth: (shouldBeVisible && contentWrapper.opacity > 0) ? contentContainer.width * s : 0
+ blurHeight: (shouldBeVisible && contentWrapper.opacity > 0) ? contentContainer.height * s : 0
+ blurRadius: Theme.cornerRadius
+ }
+
+ WlrLayershell.namespace: root.layerNamespace
+ WlrLayershell.layer: {
+ switch (Quickshell.env("DMS_POPOUT_LAYER")) {
+ case "bottom":
+ console.warn("DankPopout: 'bottom' layer is not valid for popouts. Defaulting to 'top' layer.");
+ return WlrLayershell.Top;
+ case "background":
+ console.warn("DankPopout: 'background' layer is not valid for popouts. Defaulting to 'top' layer.");
+ return WlrLayershell.Top;
+ case "overlay":
+ return WlrLayershell.Overlay;
+ default:
+ return WlrLayershell.Top;
+ }
+ }
+ WlrLayershell.exclusiveZone: -1
+ WlrLayershell.keyboardFocus: {
+ if (customKeyboardFocus !== null)
+ return customKeyboardFocus;
+ if (!shouldBeVisible)
+ return WlrKeyboardFocus.None;
+ if (CompositorService.useHyprlandFocusGrab)
+ return WlrKeyboardFocus.OnDemand;
+ return WlrKeyboardFocus.Exclusive;
+ }
+
+ readonly property bool _fullHeight: useBackgroundWindow && root.fullHeightSurface
+
+ anchors {
+ left: true
+ top: true
+ right: !useBackgroundWindow
+ bottom: _fullHeight || !useBackgroundWindow
+ }
+
+ WlrLayershell.margins {
+ left: useBackgroundWindow ? root._surfaceMarginLeft : 0
+ top: (useBackgroundWindow && !_fullHeight) ? (root.alignedY - shadowBuffer) : 0
+ }
+
+ implicitWidth: useBackgroundWindow ? root._surfaceW : 0
+ implicitHeight: (useBackgroundWindow && !_fullHeight) ? (root.alignedHeight + shadowBuffer * 2) : 0
+
+ mask: useBackgroundWindow ? contentInputMask : null
+
+ Region {
+ id: contentInputMask
+ item: contentMaskRect
+ }
+
+ Item {
+ id: contentMaskRect
+ visible: false
+ x: contentContainer.x
+ y: contentContainer.y
+ width: shouldBeVisible ? root.alignedWidth : 0
+ height: shouldBeVisible ? root.alignedHeight : 0
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ enabled: !useBackgroundWindow && shouldBeVisible && backgroundInteractive
+ acceptedButtons: Qt.LeftButton | Qt.RightButton | Qt.MiddleButton
+ z: -1
+ onClicked: mouse => {
+ const clickX = mouse.x;
+ const clickY = mouse.y;
+ const outsideContent = clickX < root.alignedX || clickX > root.alignedX + root.alignedWidth || clickY < root.alignedY || clickY > root.alignedY + root.alignedHeight;
+ if (!outsideContent)
+ return;
+ backgroundClicked();
+ }
+ }
+
+ Item {
+ id: contentContainer
+ x: useBackgroundWindow ? shadowBuffer : root.alignedX
+ y: (useBackgroundWindow && !contentWindow._fullHeight) ? shadowBuffer : root.alignedY
+ width: root.alignedWidth
+ height: root.alignedHeight
+
+ readonly property bool barTop: effectiveBarPosition === SettingsData.Position.Top
+ readonly property bool barBottom: effectiveBarPosition === SettingsData.Position.Bottom
+ readonly property bool barLeft: effectiveBarPosition === SettingsData.Position.Left
+ readonly property bool barRight: effectiveBarPosition === SettingsData.Position.Right
+ readonly property real offsetX: barLeft ? root.animationOffset : (barRight ? -root.animationOffset : 0)
+ readonly property real offsetY: barBottom ? -root.animationOffset : (barTop ? root.animationOffset : 0)
+
+ property real animX: 0
+ property real animY: 0
+ property real scaleValue: root.animationScaleCollapsed
+
+ onOffsetXChanged: animX = Theme.snap(root.shouldBeVisible ? 0 : offsetX, root.dpr)
+ onOffsetYChanged: animY = Theme.snap(root.shouldBeVisible ? 0 : offsetY, root.dpr)
+
+ Connections {
+ target: root
+ function onShouldBeVisibleChanged() {
+ contentContainer.animX = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetX, root.dpr);
+ contentContainer.animY = Theme.snap(root.shouldBeVisible ? 0 : contentContainer.offsetY, root.dpr);
+ contentContainer.scaleValue = root.shouldBeVisible ? 1.0 : root.animationScaleCollapsed;
+ }
+ }
+
+ Behavior on animX {
+ NumberAnimation {
+ duration: root.animationDuration
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
+ }
+ }
+
+ Behavior on animY {
+ NumberAnimation {
+ duration: root.animationDuration
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
+ }
+ }
+
+ Behavior on scaleValue {
+ NumberAnimation {
+ duration: root.animationDuration
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
+ }
+ }
+
+ ElevationShadow {
+ id: shadowSource
+ width: parent.width
+ height: parent.height
+ opacity: contentWrapper.opacity
+ scale: contentWrapper.scale
+ x: contentWrapper.x
+ y: contentWrapper.y
+ level: root.shadowLevel
+ direction: root.effectiveShadowDirection
+ fallbackOffset: root.shadowFallbackOffset
+ targetRadius: Theme.cornerRadius
+ targetColor: Theme.withAlpha(Theme.surfaceContainer, Theme.popupTransparency)
+ shadowEnabled: Theme.elevationEnabled && SettingsData.popoutElevationEnabled && Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1" && !(root.suspendShadowWhileResizing && root._resizeActive)
+ }
+
+ Item {
+ id: contentWrapper
+ anchors.centerIn: parent
+ width: parent.width
+ height: parent.height
+ opacity: shouldBeVisible ? 1 : 0
+ visible: opacity > 0
+ scale: contentContainer.scaleValue
+ x: Theme.snap(contentContainer.animX + (parent.width - width) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
+ y: Theme.snap(contentContainer.animY + (parent.height - height) * (1 - contentContainer.scaleValue) * 0.5, root.dpr)
+
+ layer.enabled: contentWrapper.opacity < 1
+ layer.smooth: false
+ layer.textureSize: root.dpr > 1 ? Qt.size(Math.ceil(width * root.dpr), Math.ceil(height * root.dpr)) : Qt.size(0, 0)
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: animationDuration
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: root.shouldBeVisible ? root.animationEnterCurve : root.animationExitCurve
+ }
+ }
+
+ Loader {
+ id: contentLoader
+ anchors.fill: parent
+ active: root._primeContent || shouldBeVisible || contentWindow.visible
+ asynchronous: false
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: parent.height
+ x: contentWrapper.x
+ y: contentWrapper.y
+ opacity: contentWrapper.opacity
+ scale: contentWrapper.scale
+ visible: contentWrapper.visible
+ radius: Theme.cornerRadius
+ color: "transparent"
+ border.color: BlurService.enabled ? BlurService.borderColor : Theme.outlineMedium
+ border.width: BlurService.borderWidth
+ z: 100
+ }
+ }
+
+ Item {
+ id: focusHelper
+ parent: contentContainer
+ anchors.fill: parent
+ visible: !root.contentHandlesKeys
+ enabled: !root.contentHandlesKeys
+ focus: !root.contentHandlesKeys
+ Keys.onPressed: event => {
+ if (root.contentHandlesKeys)
+ return;
+ if (event.key === Qt.Key_Escape) {
+ close();
+ event.accepted = true;
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankRipple.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankRipple.qml
new file mode 100644
index 0000000..94501d1
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankRipple.qml
@@ -0,0 +1,104 @@
+import QtQuick
+import qs.Common
+
+Item {
+ id: root
+
+ property color rippleColor: Theme.primary
+ property real cornerRadius: 0
+ property bool enableRipple: typeof SettingsData !== "undefined" ? (SettingsData.enableRippleEffects ?? true) : true
+
+ property real _rippleX: 0
+ property real _rippleY: 0
+ property real _rippleMaxRadius: 0
+ readonly property alias animating: rippleAnim.running
+
+ anchors.fill: parent
+
+ function trigger(x, y) {
+ if (!enableRipple || Theme.currentAnimationSpeed === SettingsData.AnimationSpeed.None)
+ return;
+
+ _rippleX = x;
+ _rippleY = y;
+
+ const dist = (ox, oy) => ox * ox + oy * oy;
+ _rippleMaxRadius = Math.sqrt(Math.max(dist(x, y), dist(x, height - y), dist(width - x, y), dist(width - x, height - y)));
+
+ rippleAnim.restart();
+ }
+
+ SequentialAnimation {
+ id: rippleAnim
+
+ PropertyAction {
+ target: rippleFx
+ property: "rippleCenterX"
+ value: root._rippleX
+ }
+ PropertyAction {
+ target: rippleFx
+ property: "rippleCenterY"
+ value: root._rippleY
+ }
+ PropertyAction {
+ target: rippleFx
+ property: "rippleRadius"
+ value: 0
+ }
+ PropertyAction {
+ target: rippleFx
+ property: "rippleOpacity"
+ value: 0.10
+ }
+
+ ParallelAnimation {
+ DankAnim {
+ target: rippleFx
+ property: "rippleRadius"
+ from: 0
+ to: root._rippleMaxRadius
+ duration: Theme.expressiveDurations.expressiveDefaultSpatial
+ easing.bezierCurve: Theme.expressiveCurves.standardDecel
+ }
+ SequentialAnimation {
+ PauseAnimation {
+ duration: Math.round(Theme.expressiveDurations.expressiveDefaultSpatial * 0.6)
+ }
+ DankAnim {
+ target: rippleFx
+ property: "rippleOpacity"
+ to: 0
+ duration: Theme.expressiveDurations.expressiveDefaultSpatial
+ easing.bezierCurve: Theme.expressiveCurves.standard
+ }
+ }
+ }
+ }
+
+ ShaderEffect {
+ id: rippleFx
+ visible: rippleAnim.running
+
+ property real rippleCenterX: 0
+ property real rippleCenterY: 0
+ property real rippleRadius: 0
+ property real rippleOpacity: 0
+
+ x: Math.max(0, rippleCenterX - rippleRadius)
+ y: Math.max(0, rippleCenterY - rippleRadius)
+ width: Math.max(0, Math.min(root.width, rippleCenterX + rippleRadius) - x)
+ height: Math.max(0, Math.min(root.height, rippleCenterY + rippleRadius) - y)
+
+ property real widthPx: width
+ property real heightPx: height
+ property real cornerRadiusPx: root.cornerRadius
+ property real offsetX: x
+ property real offsetY: y
+ property real parentWidth: root.width
+ property real parentHeight: root.height
+ property vector4d rippleCol: Qt.vector4d(root.rippleColor.r, root.rippleColor.g, root.rippleColor.b, root.rippleColor.a)
+
+ fragmentShader: Qt.resolvedUrl("../Shaders/qsb/ripple.frag.qsb")
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankSVGIcon.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankSVGIcon.qml
new file mode 100644
index 0000000..0cf11bc
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankSVGIcon.qml
@@ -0,0 +1,81 @@
+import QtQuick
+import QtQuick.Effects
+import Quickshell.Widgets
+import qs.Common
+
+Item {
+ id: root
+
+ required property string source
+ property int size: 24
+ property string cornerIcon: ""
+ property int cornerIconSize: Math.max(10, size * 0.4)
+ property color cornerIconColor: Theme.surfaceText
+ property color cornerIconBackground: Theme.surface
+ property color colorOverride: "transparent"
+ property real brightnessOverride: 0.0
+ property real contrastOverride: 0.0
+ property real saturationOverride: 0.0
+
+ readonly property bool hasCornerIcon: cornerIcon !== ""
+ readonly property bool hasColorOverride: colorOverride.a > 0
+ readonly property bool hasColorEffect: hasColorOverride || brightnessOverride !== 0.0 || contrastOverride !== 0.0 || saturationOverride !== 0.0
+ readonly property string resolvedSource: {
+ if (!source)
+ return "";
+ if (source.startsWith("file://"))
+ return source;
+ if (source.startsWith("/"))
+ return "file://" + source;
+ if (source.startsWith("qrc:"))
+ return source;
+ return source;
+ }
+
+ implicitWidth: size
+ implicitHeight: size
+
+ IconImage {
+ id: iconImage
+ anchors.fill: parent
+ source: root.resolvedSource
+ smooth: true
+ mipmap: true
+ asynchronous: true
+ implicitSize: root.size * 2
+ backer.sourceSize.width: root.size * 2
+ backer.sourceSize.height: root.size * 2
+ backer.cache: true
+ layer.enabled: root.hasColorEffect
+ layer.smooth: true
+ layer.mipmap: true
+ layer.effect: MultiEffect {
+ saturation: root.saturationOverride
+ colorization: root.hasColorOverride ? 1 : 0
+ colorizationColor: root.colorOverride
+ brightness: root.brightnessOverride
+ contrast: root.contrastOverride
+ }
+ }
+
+ Rectangle {
+ visible: root.hasCornerIcon
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.rightMargin: -2
+ anchors.bottomMargin: -2
+ width: root.cornerIconSize + 4
+ height: root.cornerIconSize + 4
+ radius: width / 2
+ color: root.cornerIconBackground
+ border.width: 1
+ border.color: Theme.surfaceLight
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: root.cornerIcon
+ size: root.cornerIconSize
+ color: root.cornerIconColor
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankScrollbar.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankScrollbar.qml
new file mode 100644
index 0000000..c5d2d70
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankScrollbar.qml
@@ -0,0 +1,46 @@
+import QtQuick
+import QtQuick.Controls
+import qs.Common
+
+ScrollBar {
+ id: scrollbar
+
+ property var targetFlickable: null
+ readonly property var _target: targetFlickable ?? parent
+
+ property bool _scrollBarActive: false
+ property alias hideTimer: hideScrollBarTimer
+ property bool _isParentMoving: _target && (_target.moving || _target.flicking || _target.isMomentumActive)
+ property bool _shouldShow: pressed || hovered || active || _isParentMoving || _scrollBarActive
+
+ policy: (_target && _target.contentHeight > _target.height) ? ScrollBar.AsNeeded : ScrollBar.AlwaysOff
+ minimumSize: 0.08
+ implicitWidth: 10
+ interactive: true
+ hoverEnabled: true
+ z: 1000
+ opacity: (policy !== ScrollBar.AlwaysOff && _shouldShow) ? 1.0 : 0.0
+ visible: policy !== ScrollBar.AlwaysOff
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: 160
+ easing.type: Easing.OutQuad
+ }
+ }
+
+ contentItem: Rectangle {
+ implicitWidth: 6
+ radius: width / 2
+ color: scrollbar.pressed ? Theme.primary : scrollbar._shouldShow ? Theme.outline : Theme.outlineMedium
+ opacity: scrollbar.pressed ? 1.0 : scrollbar._shouldShow ? 1.0 : 0.6
+ }
+
+ background: Item {}
+
+ Timer {
+ id: hideScrollBarTimer
+ interval: 1200
+ onTriggered: scrollbar._scrollBarActive = false
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankSeekbar.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankSeekbar.qml
new file mode 100644
index 0000000..85f13a5
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankSeekbar.qml
@@ -0,0 +1,262 @@
+import QtQuick
+import Quickshell.Services.Mpris
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Item {
+ id: root
+
+ property MprisPlayer activePlayer
+ property real seekPreviewRatio: -1
+ readonly property real playerValue: {
+ if (!activePlayer || activePlayer.length <= 0)
+ return 0;
+ const pos = (activePlayer.position || 0) % Math.max(1, activePlayer.length);
+ const calculatedRatio = pos / activePlayer.length;
+ return Math.max(0, Math.min(1, calculatedRatio));
+ }
+ property real value: seekPreviewRatio >= 0 ? seekPreviewRatio : playerValue
+ property bool isSeeking: false
+ property bool isDraggingSeek: false
+ property real committedSeekRatio: -1
+ property int previewSettleChecksRemaining: 0
+ property real dragThreshold: 4
+ property int holdIndicatorDelay: 180
+
+ function clampRatio(ratio) {
+ return Math.max(0, Math.min(1, ratio));
+ }
+
+ function ratioForPosition(position) {
+ if (!activePlayer || activePlayer.length <= 0)
+ return 0;
+ return clampRatio(position / activePlayer.length);
+ }
+
+ function positionForRatio(ratio) {
+ if (!activePlayer || activePlayer.length <= 0)
+ return 0;
+ const rawPosition = clampRatio(ratio) * activePlayer.length;
+ return Math.min(rawPosition, activePlayer.length * 0.99);
+ }
+
+ function updatePreviewFromMouse(mouseX, width) {
+ if (!activePlayer || activePlayer.length <= 0 || width <= 0)
+ return;
+ seekPreviewRatio = clampRatio(mouseX / width);
+ }
+
+ function clearCommittedSeekPreview() {
+ previewSettleTimer.stop();
+ committedSeekRatio = -1;
+ previewSettleChecksRemaining = 0;
+ if (!isSeeking)
+ seekPreviewRatio = -1;
+ }
+
+ function beginCommittedSeekPreview(position) {
+ seekPreviewRatio = ratioForPosition(position);
+ committedSeekRatio = seekPreviewRatio;
+ previewSettleChecksRemaining = 15;
+ previewSettleTimer.restart();
+ }
+
+ function handleSeekPressed(mouse, width, mouseArea, holdTimer) {
+ isSeeking = true;
+ isDraggingSeek = false;
+ mouseArea.pressX = mouse.x;
+ clearCommittedSeekPreview();
+ holdTimer.restart();
+ if (activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
+ updatePreviewFromMouse(mouse.x, width);
+ mouseArea.pendingSeekPosition = positionForRatio(seekPreviewRatio);
+ }
+ }
+
+ function handleSeekReleased(mouseArea, holdTimer) {
+ holdTimer.stop();
+ isSeeking = false;
+ isDraggingSeek = false;
+ if (mouseArea.pendingSeekPosition >= 0 && activePlayer && activePlayer.canSeek && activePlayer.length > 0) {
+ const clamped = Math.min(mouseArea.pendingSeekPosition, activePlayer.length * 0.99);
+ activePlayer.position = clamped;
+ mouseArea.pendingSeekPosition = -1;
+ beginCommittedSeekPreview(clamped);
+ } else {
+ seekPreviewRatio = -1;
+ }
+ }
+
+ function handleSeekPositionChanged(mouse, width, mouseArea) {
+ if (mouseArea.pressed && isSeeking && activePlayer && activePlayer.length > 0 && activePlayer.canSeek) {
+ if (!isDraggingSeek && Math.abs(mouse.x - mouseArea.pressX) >= dragThreshold)
+ isDraggingSeek = true;
+ updatePreviewFromMouse(mouse.x, width);
+ mouseArea.pendingSeekPosition = positionForRatio(seekPreviewRatio);
+ }
+ }
+
+ function handleSeekCanceled(mouseArea, holdTimer) {
+ holdTimer.stop();
+ isSeeking = false;
+ isDraggingSeek = false;
+ mouseArea.pendingSeekPosition = -1;
+ clearCommittedSeekPreview();
+ }
+
+ Timer {
+ id: previewSettleTimer
+ interval: 80
+ repeat: true
+ onTriggered: {
+ if (root.isSeeking || root.committedSeekRatio < 0) {
+ stop();
+ return;
+ }
+
+ const previewSettled = Math.abs(root.playerValue - root.committedSeekRatio) <= 0.0015;
+ if (previewSettled || root.previewSettleChecksRemaining <= 0) {
+ root.clearCommittedSeekPreview();
+ return;
+ }
+
+ root.previewSettleChecksRemaining -= 1;
+ }
+ }
+
+ implicitHeight: 20
+
+ Loader {
+ anchors.fill: parent
+ visible: activePlayer && activePlayer.length > 0
+ sourceComponent: SettingsData.waveProgressEnabled ? waveProgressComponent : flatProgressComponent
+ z: 1
+
+ Component {
+ id: waveProgressComponent
+
+ M3WaveProgress {
+ value: root.value
+ actualValue: root.playerValue
+ showActualPlaybackState: root.isSeeking
+ actualProgressColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.45)
+ isPlaying: activePlayer && activePlayer.playbackState === MprisPlaybackState.Playing
+
+ MouseArea {
+ id: waveMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
+
+ property real pendingSeekPosition: -1
+ property real pressX: 0
+
+ Timer {
+ id: waveHoldIndicatorTimer
+ interval: root.holdIndicatorDelay
+ repeat: false
+ onTriggered: {
+ if (parent.pressed && root.isSeeking)
+ root.isDraggingSeek = true;
+ }
+ }
+
+ onPressed: mouse => root.handleSeekPressed(mouse, parent.width, waveMouseArea, waveHoldIndicatorTimer)
+ onReleased: root.handleSeekReleased(waveMouseArea, waveHoldIndicatorTimer)
+ onPositionChanged: mouse => root.handleSeekPositionChanged(mouse, parent.width, waveMouseArea)
+ onCanceled: root.handleSeekCanceled(waveMouseArea, waveHoldIndicatorTimer)
+ }
+ }
+ }
+
+ Component {
+ id: flatProgressComponent
+
+ Item {
+ property real lineWidth: 3
+ property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
+ property color fillColor: Theme.primary
+ property color playheadColor: Theme.primary
+ property color actualProgressColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.45)
+ readonly property real midY: height / 2
+
+ Rectangle {
+ width: parent.width
+ height: parent.lineWidth
+ anchors.verticalCenter: parent.verticalCenter
+ color: parent.trackColor
+ radius: height / 2
+ }
+
+ Rectangle {
+ width: Math.max(0, Math.min(parent.width, parent.width * root.value))
+ height: parent.lineWidth
+ anchors.left: parent.left
+ anchors.verticalCenter: parent.verticalCenter
+ color: parent.fillColor
+ radius: height / 2
+ Behavior on width {
+ NumberAnimation {
+ duration: 80
+ }
+ }
+ }
+
+ Rectangle {
+ visible: root.isDraggingSeek
+ width: 2
+ height: Math.max(parent.lineWidth + 4, 10)
+ radius: width / 2
+ color: parent.actualProgressColor
+ x: Math.max(0, Math.min(parent.width, parent.width * root.playerValue)) - width / 2
+ y: parent.midY - height / 2
+ z: 2
+ }
+
+ Rectangle {
+ id: playhead
+ width: 3
+ height: Math.max(parent.lineWidth + 8, 14)
+ radius: width / 2
+ color: parent.playheadColor
+ x: Math.max(0, Math.min(parent.width, parent.width * root.value)) - width / 2
+ y: parent.midY - height / 2
+ z: 3
+ Behavior on x {
+ NumberAnimation {
+ duration: 80
+ }
+ }
+ }
+
+ MouseArea {
+ id: flatMouseArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ enabled: activePlayer && activePlayer.canSeek && activePlayer.length > 0
+
+ property real pendingSeekPosition: -1
+ property real pressX: 0
+
+ Timer {
+ id: flatHoldIndicatorTimer
+ interval: root.holdIndicatorDelay
+ repeat: false
+ onTriggered: {
+ if (parent.pressed && root.isSeeking)
+ root.isDraggingSeek = true;
+ }
+ }
+
+ onPressed: mouse => root.handleSeekPressed(mouse, parent.width, flatMouseArea, flatHoldIndicatorTimer)
+ onReleased: root.handleSeekReleased(flatMouseArea, flatHoldIndicatorTimer)
+ onPositionChanged: mouse => root.handleSeekPositionChanged(mouse, parent.width, flatMouseArea)
+ onCanceled: root.handleSeekCanceled(flatMouseArea, flatHoldIndicatorTimer)
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankSlideout.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankSlideout.qml
new file mode 100644
index 0000000..d346322
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankSlideout.qml
@@ -0,0 +1,209 @@
+import QtQuick
+import QtQuick.Controls
+import Quickshell
+import Quickshell.Wayland
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+pragma ComponentBehavior: Bound
+
+PanelWindow {
+ id: root
+
+ property string layerNamespace: "dms:slideout"
+ WlrLayershell.namespace: layerNamespace
+
+ property bool isVisible: false
+ property var targetScreen: null
+ property var modelData: null
+ property real slideoutWidth: 480
+ property bool expandable: false
+ property bool expandedWidth: false
+ property real expandedWidthValue: 960
+ property Component content: null
+ property string title: ""
+ property alias container: contentContainer
+ property real customTransparency: -1
+ signal aboutToHide
+
+ function show() {
+ visible = true
+ isVisible = true
+ }
+
+ function hide() {
+ aboutToHide()
+ isVisible = false
+ }
+
+ function toggle() {
+ if (isVisible) {
+ hide()
+ } else {
+ show()
+ }
+ }
+
+ visible: isVisible
+ screen: modelData
+
+ anchors.top: true
+ anchors.bottom: true
+ anchors.right: true
+
+ implicitWidth: expandable ? expandedWidthValue : slideoutWidth
+ implicitHeight: modelData ? modelData.height : 800
+
+ color: "transparent"
+
+ WlrLayershell.layer: WlrLayershell.Top
+ WlrLayershell.exclusiveZone: 0
+ WlrLayershell.keyboardFocus: isVisible ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
+
+ readonly property real dpr: CompositorService.getScreenScale(root.screen)
+ readonly property real alignedWidth: Theme.px(expandable && expandedWidth ? expandedWidthValue : slideoutWidth, dpr)
+ readonly property real alignedHeight: Theme.px(modelData ? modelData.height : 800, dpr)
+
+ mask: Region {
+ item: Rectangle {
+ x: root.width - alignedWidth
+ y: 0
+ width: alignedWidth
+ height: root.height
+ }
+ }
+
+ Item {
+ id: slideContainer
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.right: parent.right
+ width: alignedWidth
+ height: alignedHeight
+
+ property real slideOffset: alignedWidth
+
+ Connections {
+ target: root
+ function onIsVisibleChanged() {
+ slideContainer.slideOffset = root.isVisible ? 0 : slideContainer.width
+ }
+ }
+
+ Behavior on slideOffset {
+ NumberAnimation {
+ id: slideAnimation
+ duration: 450
+ easing.type: Easing.OutCubic
+
+ onRunningChanged: {
+ if (!running && !isVisible) {
+ root.visible = false
+ }
+ }
+ }
+ }
+
+ Behavior on width {
+ NumberAnimation {
+ duration: 250
+ easing.type: Easing.OutCubic
+ }
+ }
+
+ Item {
+ id: contentRect
+ layer.enabled: Quickshell.env("DMS_DISABLE_LAYER") !== "true" && Quickshell.env("DMS_DISABLE_LAYER") !== "1"
+ layer.smooth: false
+ layer.textureSize: Qt.size(width * root.dpr, height * root.dpr)
+ opacity: 1
+
+ readonly property real effectiveTransparency: customTransparency >= 0 ? customTransparency : SettingsData.popupTransparency
+
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ width: parent.width
+ x: Theme.snap(slideContainer.slideOffset, root.dpr)
+
+ Rectangle {
+ anchors.fill: parent
+ color: Qt.rgba(Theme.surfaceContainer.r, Theme.surfaceContainer.g, Theme.surfaceContainer.b, contentRect.effectiveTransparency)
+ radius: Theme.cornerRadius
+ }
+
+ Column {
+ id: headerColumn
+ anchors.top: parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.margins: Theme.spacingL
+ spacing: Theme.spacingM
+ visible: root.title !== ""
+
+ Row {
+ width: parent.width
+ height: 32
+
+ Column {
+ width: parent.width - buttonRow.width
+ spacing: Theme.spacingXS
+ anchors.verticalCenter: parent.verticalCenter
+
+ StyledText {
+ text: root.title
+ font.pixelSize: Theme.fontSizeLarge
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+ }
+
+ Row {
+ id: buttonRow
+ spacing: Theme.spacingXS
+
+ DankActionButton {
+ id: expandButton
+ iconName: root.expandedWidth ? "unfold_less" : "unfold_more"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ visible: root.expandable
+ onClicked: root.expandedWidth = !root.expandedWidth
+
+ transform: Rotation {
+ angle: 90
+ origin.x: expandButton.width / 2
+ origin.y: expandButton.height / 2
+ }
+ }
+
+ DankActionButton {
+ id: closeButton
+ iconName: "close"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.surfaceText
+ onClicked: root.hide()
+ }
+ }
+ }
+ }
+
+ Item {
+ id: contentContainer
+ anchors.top: root.title !== "" ? headerColumn.bottom : parent.top
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.topMargin: root.title !== "" ? 0 : Theme.spacingL
+ anchors.leftMargin: Theme.spacingL
+ anchors.rightMargin: Theme.spacingL
+ anchors.bottomMargin: Theme.spacingL
+
+ Loader {
+ anchors.fill: parent
+ sourceComponent: root.content
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankSlider.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankSlider.qml
new file mode 100644
index 0000000..f91a75e
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankSlider.qml
@@ -0,0 +1,294 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Item {
+ id: slider
+
+ property int value: 50
+ property int minimum: 0
+ property int maximum: 100
+ property int step: 1
+ property string leftIcon: ""
+ property string rightIcon: ""
+ property bool enabled: true
+ property string unit: "%"
+ property bool showValue: true
+ property bool isDragging: false
+ property bool wheelEnabled: true
+ property bool centerMinimum: false
+ property real valueOverride: -1
+ property bool alwaysShowValue: false
+ readonly property bool containsMouse: sliderMouseArea.containsMouse
+
+ property color thumbOutlineColor: Theme.surfaceContainer
+ property color trackColor: enabled ? Theme.outline : Theme.outline
+
+ signal sliderValueChanged(int newValue)
+ signal sliderDragFinished(int finalValue)
+
+ height: 48
+
+ function updateValueFromPosition(x) {
+ let ratio = Math.max(0, Math.min(1, (x - sliderHandle.width / 2) / (sliderTrack.width - sliderHandle.width)));
+ if (centerMinimum)
+ ratio = Math.max(0, (ratio - 0.5) * 2);
+ let rawValue = minimum + ratio * (maximum - minimum);
+ let newValue = step > 1 ? Math.round(rawValue / step) * step : Math.round(rawValue);
+ newValue = Math.max(minimum, Math.min(maximum, newValue));
+ if (newValue !== value) {
+ value = newValue;
+ sliderValueChanged(newValue);
+ }
+ }
+
+ Row {
+ anchors.centerIn: parent
+ width: parent.width
+ spacing: Theme.spacingM
+
+ DankIcon {
+ name: slider.leftIcon
+ size: Theme.iconSize
+ color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38
+ anchors.verticalCenter: parent.verticalCenter
+ visible: slider.leftIcon.length > 0
+ }
+
+ StyledRect {
+ id: sliderTrack
+
+ property int leftIconWidth: slider.leftIcon.length > 0 ? Theme.iconSize : 0
+ property int rightIconWidth: slider.rightIcon.length > 0 ? Theme.iconSize : 0
+
+ width: parent.width - (leftIconWidth + rightIconWidth + (slider.leftIcon.length > 0 ? Theme.spacingM : 0) + (slider.rightIcon.length > 0 ? Theme.spacingM : 0))
+ height: 12
+ radius: Theme.cornerRadius
+ color: slider.trackColor
+ anchors.verticalCenter: parent.verticalCenter
+ clip: false
+
+ StyledRect {
+ id: sliderFill
+ height: parent.height
+ radius: Theme.cornerRadius
+ width: {
+ const range = slider.maximum - slider.minimum;
+ const rawRatio = range === 0 ? 0 : (slider.value - slider.minimum) / range;
+ const ratio = slider.centerMinimum ? (0.5 + rawRatio * 0.5) : rawRatio;
+ const travel = sliderTrack.width - sliderHandle.width;
+ const center = (travel * ratio) + sliderHandle.width / 2;
+ return Math.max(0, Math.min(sliderTrack.width, center));
+ }
+ color: slider.enabled ? Theme.primary : Theme.withAlpha(Theme.onSurface, 0.12)
+ }
+
+ StyledRect {
+ id: sliderHandle
+
+ property bool active: sliderMouseArea.containsMouse || sliderMouseArea.pressed || slider.isDragging
+
+ width: 8
+ height: 24
+ radius: Theme.cornerRadius
+ x: {
+ const range = slider.maximum - slider.minimum;
+ const rawRatio = range === 0 ? 0 : (slider.value - slider.minimum) / range;
+ const ratio = slider.centerMinimum ? (0.5 + rawRatio * 0.5) : rawRatio;
+ const travel = sliderTrack.width - width;
+ return Math.max(0, Math.min(travel, travel * ratio));
+ }
+ anchors.verticalCenter: parent.verticalCenter
+ color: slider.enabled ? Theme.primary : Theme.withAlpha(Theme.onSurface, 0.12)
+ border.width: 3
+ border.color: slider.thumbOutlineColor
+
+ StyledRect {
+ anchors.fill: parent
+ radius: Theme.cornerRadius
+ color: Theme.onPrimary
+ opacity: slider.enabled ? (sliderMouseArea.pressed ? 0.16 : (sliderMouseArea.containsMouse ? 0.08 : 0)) : 0
+ visible: opacity > 0
+ }
+
+ StyledRect {
+ anchors.centerIn: parent
+ width: parent.width + 20
+ height: parent.height + 20
+ radius: width / 2
+ color: "transparent"
+ border.width: 2
+ border.color: Theme.primary
+ opacity: slider.enabled && slider.focus ? 0.3 : 0
+ visible: opacity > 0
+ }
+
+ Rectangle {
+ id: ripple
+ anchors.centerIn: parent
+ width: 0
+ height: 0
+ radius: width / 2
+ color: Theme.onPrimary
+ opacity: 0
+
+ function start() {
+ opacity = 0.16;
+ width = 0;
+ height = 0;
+ rippleAnimation.start();
+ }
+
+ SequentialAnimation {
+ id: rippleAnimation
+ NumberAnimation {
+ target: ripple
+ properties: "width,height"
+ to: 28
+ duration: 180
+ }
+ NumberAnimation {
+ target: ripple
+ property: "opacity"
+ to: 0
+ duration: 150
+ }
+ }
+ }
+
+ TapHandler {
+ acceptedButtons: Qt.LeftButton
+ onPressedChanged: {
+ if (pressed && slider.enabled) {
+ ripple.start();
+ }
+ }
+ }
+
+ scale: active ? 1.05 : 1.0
+
+ Behavior on scale {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ Item {
+ id: sliderContainer
+
+ anchors.fill: parent
+
+ MouseArea {
+ id: sliderMouseArea
+
+ property bool isDragging: false
+
+ anchors.fill: parent
+ anchors.topMargin: -10
+ anchors.bottomMargin: -10
+ hoverEnabled: true
+ cursorShape: enabled ? Qt.PointingHandCursor : Qt.ArrowCursor
+ enabled: slider.enabled
+ preventStealing: true
+ acceptedButtons: Qt.LeftButton
+ onWheel: wheelEvent => {
+ if (!slider.wheelEnabled) {
+ wheelEvent.accepted = false;
+ return;
+ }
+ let wheelStep = slider.step > 1 ? slider.step : Math.max(1, (maximum - minimum) / 100);
+ let newValue = wheelEvent.angleDelta.y > 0 ? Math.min(maximum, value + wheelStep) : Math.max(minimum, value - wheelStep);
+ if (slider.step > 1)
+ newValue = Math.round(newValue / slider.step) * slider.step;
+ newValue = Math.round(newValue);
+ if (newValue !== value) {
+ value = newValue;
+ sliderValueChanged(newValue);
+ }
+ wheelEvent.accepted = true;
+ }
+ onPressed: mouse => {
+ if (slider.enabled) {
+ slider.isDragging = true;
+ sliderMouseArea.isDragging = true;
+ updateValueFromPosition(mouse.x);
+ }
+ }
+ onReleased: {
+ if (slider.enabled) {
+ slider.isDragging = false;
+ sliderMouseArea.isDragging = false;
+ slider.sliderDragFinished(slider.value);
+ }
+ }
+ onPositionChanged: mouse => {
+ if (pressed && slider.isDragging && slider.enabled) {
+ updateValueFromPosition(mouse.x);
+ }
+ }
+ onClicked: mouse => {
+ if (slider.enabled && !slider.isDragging) {
+ updateValueFromPosition(mouse.x);
+ }
+ }
+ }
+ }
+
+ StyledRect {
+ id: valueTooltip
+
+ width: tooltipText.reservedWidth + Theme.spacingS * 2
+ height: tooltipText.contentHeight + Theme.spacingXS * 2
+ radius: Theme.cornerRadius
+ color: Theme.surfaceContainer
+ border.color: Theme.outline
+ border.width: 1
+ anchors.bottom: parent.top
+ anchors.bottomMargin: Theme.spacingM
+ x: Math.max(0, Math.min(parent.width - width, sliderHandle.x + sliderHandle.width / 2 - width / 2))
+ visible: slider.alwaysShowValue ? slider.showValue : ((sliderMouseArea.containsMouse && slider.showValue) || (slider.isDragging && slider.showValue))
+ opacity: visible ? 1 : 0
+
+ NumericText {
+ id: tooltipText
+
+ text: (slider.valueOverride >= 0 ? Math.round(slider.valueOverride) : slider.value) + slider.unit
+ reserveText: {
+ let widest = "";
+ const samples = [slider.minimum, slider.maximum];
+ if (slider.valueOverride >= 0)
+ samples.push(slider.valueOverride);
+ for (let i = 0; i < samples.length; i++) {
+ const candidate = Math.round(samples[i]) + slider.unit;
+ if (candidate.length > widest.length)
+ widest = candidate;
+ }
+ return widest;
+ }
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.centerIn: parent
+ font.hintingPreference: Font.PreferFullHinting
+ }
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+ }
+
+ DankIcon {
+ name: slider.rightIcon
+ size: Theme.iconSize
+ color: slider.enabled ? Theme.surfaceText : Theme.onSurface_38
+ anchors.verticalCenter: parent.verticalCenter
+ visible: slider.rightIcon.length > 0
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankTabBar.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankTabBar.qml
new file mode 100644
index 0000000..d3b72f1
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankTabBar.qml
@@ -0,0 +1,263 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+FocusScope {
+ id: tabBar
+
+ property alias model: tabRepeater.model
+ property int currentIndex: 0
+ property int spacing: Theme.spacingL
+ property int tabHeight: 56
+ property bool showIcons: true
+ property bool equalWidthTabs: true
+ property bool enableArrowNavigation: true
+ property Item nextFocusTarget: null
+ property Item previousFocusTarget: null
+
+ signal tabClicked(int index)
+ signal actionTriggered(int index)
+
+ focus: false
+ activeFocusOnTab: true
+ height: tabHeight
+
+ KeyNavigation.tab: nextFocusTarget
+ KeyNavigation.down: nextFocusTarget
+ KeyNavigation.backtab: previousFocusTarget
+ KeyNavigation.up: previousFocusTarget
+
+ Keys.onPressed: event => {
+ if (!tabBar.activeFocus || tabRepeater.count === 0)
+ return;
+ function findSelectableIndex(startIndex, step) {
+ let idx = startIndex;
+ for (let i = 0; i < tabRepeater.count; i++) {
+ idx = (idx + step + tabRepeater.count) % tabRepeater.count;
+ const item = tabRepeater.itemAt(idx);
+ if (item && !item.isAction)
+ return idx;
+ }
+ return -1;
+ }
+
+ const goToIndex = nextIndex => {
+ if (nextIndex >= 0 && nextIndex !== tabBar.currentIndex) {
+ tabBar.currentIndex = nextIndex;
+ tabBar.tabClicked(nextIndex);
+ }
+ };
+
+ const resolveTarget = item => {
+ if (!item)
+ return null;
+
+ if (item.focusTarget)
+ return resolveTarget(item.focusTarget);
+
+ return item;
+ };
+
+ const focusItem = item => {
+ const target = resolveTarget(item);
+ if (!target)
+ return false;
+
+ if (target.requestFocus) {
+ Qt.callLater(() => target.requestFocus());
+ return true;
+ }
+
+ if (target.forceActiveFocus) {
+ Qt.callLater(() => target.forceActiveFocus());
+ return true;
+ }
+
+ return false;
+ };
+
+ if (event.key === Qt.Key_Right && tabBar.enableArrowNavigation) {
+ const baseIndex = (tabBar.currentIndex >= 0 && tabBar.currentIndex < tabRepeater.count) ? tabBar.currentIndex : -1;
+ const nextIndex = findSelectableIndex(baseIndex, 1);
+ if (nextIndex >= 0) {
+ goToIndex(nextIndex);
+ event.accepted = true;
+ }
+ } else if (event.key === Qt.Key_Left && tabBar.enableArrowNavigation) {
+ const baseIndex = (tabBar.currentIndex >= 0 && tabBar.currentIndex < tabRepeater.count) ? tabBar.currentIndex : 0;
+ const nextIndex = findSelectableIndex(baseIndex, -1);
+ if (nextIndex >= 0) {
+ goToIndex(nextIndex);
+ event.accepted = true;
+ }
+ } else if (event.key === Qt.Key_Tab && (event.modifiers & Qt.ShiftModifier)) {
+ if (focusItem(tabBar.previousFocusTarget)) {
+ event.accepted = true;
+ }
+ } else if (event.key === Qt.Key_Tab || event.key === Qt.Key_Down) {
+ if (focusItem(tabBar.nextFocusTarget)) {
+ event.accepted = true;
+ }
+ } else if (event.key === Qt.Key_Up) {
+ if (focusItem(tabBar.previousFocusTarget)) {
+ event.accepted = true;
+ }
+ }
+ }
+
+ Row {
+ id: tabRow
+ anchors.fill: parent
+ spacing: tabBar.spacing
+
+ Repeater {
+ id: tabRepeater
+
+ Item {
+ id: tabItem
+ property bool isAction: modelData && modelData.isAction === true
+ property bool isActive: !isAction && tabBar.currentIndex === index
+ property bool hasIcon: tabBar.showIcons && modelData && modelData.icon && modelData.icon.length > 0
+ property bool hasText: modelData && modelData.text && modelData.text.length > 0
+
+ width: tabBar.equalWidthTabs ? (tabBar.width - tabBar.spacing * Math.max(0, tabRepeater.count - 1)) / Math.max(1, tabRepeater.count) : Math.max(contentCol.implicitWidth + Theme.spacingXL, 64)
+ height: tabBar.tabHeight
+
+ Column {
+ id: contentCol
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: modelData.icon || ""
+ anchors.horizontalCenter: parent.horizontalCenter
+ size: Theme.iconSize
+ color: tabItem.isActive ? Theme.primary : Theme.surfaceText
+ visible: hasIcon
+ }
+
+ StyledText {
+ text: modelData.text || ""
+ anchors.horizontalCenter: parent.horizontalCenter
+ font.pixelSize: Theme.fontSizeMedium
+ color: tabItem.isActive ? Theme.primary : Theme.surfaceText
+ font.weight: Font.Medium
+ visible: hasText
+ }
+ }
+
+ Rectangle {
+ id: stateLayer
+ anchors.fill: parent
+ color: Theme.surfaceTint
+ opacity: tabArea.pressed ? 0.12 : (tabArea.containsMouse ? 0.08 : 0)
+ visible: opacity > 0
+ radius: Theme.cornerRadius
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ DankRipple {
+ id: tabRipple
+ cornerRadius: Theme.cornerRadius
+ }
+
+ MouseArea {
+ id: tabArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onPressed: mouse => tabRipple.trigger(mouse.x, mouse.y)
+ onClicked: {
+ if (tabItem.isAction) {
+ tabBar.actionTriggered(index);
+ } else {
+ tabBar.tabClicked(index);
+ }
+ }
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: indicator
+ y: parent.height + 7
+ height: 3
+ width: 60
+ topLeftRadius: Theme.cornerRadius
+ topRightRadius: Theme.cornerRadius
+ bottomLeftRadius: 0
+ bottomRightRadius: 0
+ color: Theme.primary
+ visible: false
+
+ property bool animationEnabled: false
+ property bool initialSetupComplete: false
+
+ Behavior on x {
+ enabled: indicator.animationEnabled
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Behavior on width {
+ enabled: indicator.animationEnabled
+ NumberAnimation {
+ duration: Theme.mediumDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+
+ Rectangle {
+ width: parent.width
+ height: 1
+ y: parent.height + 10
+ color: Theme.outlineStrong
+ }
+
+ function updateIndicator() {
+ if (tabRepeater.count === 0 || currentIndex < 0 || currentIndex >= tabRepeater.count) {
+ return;
+ }
+
+ const item = tabRepeater.itemAt(currentIndex);
+ if (!item || item.isAction) {
+ return;
+ }
+
+ const tabPos = item.mapToItem(tabBar, 0, 0);
+ const tabCenterX = tabPos.x + item.width / 2;
+ const indicatorWidth = 60;
+
+ if (tabPos.x < 10 && currentIndex > 0) {
+ Qt.callLater(updateIndicator);
+ return;
+ }
+
+ if (!indicator.initialSetupComplete) {
+ indicator.animationEnabled = false;
+ indicator.width = indicatorWidth;
+ indicator.x = tabCenterX - indicatorWidth / 2;
+ indicator.visible = true;
+ indicator.initialSetupComplete = true;
+ indicator.animationEnabled = true;
+ } else {
+ indicator.width = indicatorWidth;
+ indicator.x = tabCenterX - indicatorWidth / 2;
+ indicator.visible = true;
+ }
+ }
+
+ onCurrentIndexChanged: {
+ Qt.callLater(updateIndicator);
+ }
+ onWidthChanged: Qt.callLater(updateIndicator)
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankTextField.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankTextField.qml
new file mode 100644
index 0000000..456ee2c
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankTextField.qml
@@ -0,0 +1,271 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+StyledRect {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ KeyNavigation.tab: keyNavigationTab
+ KeyNavigation.backtab: keyNavigationBacktab
+
+ function checkParentDisablesTransparency() {
+ let p = parent;
+ while (p) {
+ if (p.disablePopupTransparency === true)
+ return true;
+ p = p.parent;
+ }
+ return false;
+ }
+
+ property alias text: textInput.text
+ property string placeholderText: ""
+ property alias font: textInput.font
+ property alias textColor: textInput.color
+ property alias enabled: textInput.enabled
+ property alias echoMode: textInput.echoMode
+ property alias validator: textInput.validator
+ property alias maximumLength: textInput.maximumLength
+ property string leftIconName: ""
+ property int leftIconSize: Theme.iconSize
+ property color leftIconColor: Theme.surfaceVariantText
+ property color leftIconFocusedColor: Theme.primary
+ property bool showClearButton: false
+ property bool showPasswordToggle: false
+ property bool passwordVisible: false
+ property bool usePopupTransparency: !checkParentDisablesTransparency()
+ property color backgroundColor: usePopupTransparency ? Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency) : Theme.surfaceContainerHigh
+ property color focusedBorderColor: Theme.primary
+ property color normalBorderColor: Theme.outlineMedium
+ property color placeholderColor: Theme.outlineButton
+ property int borderWidth: 1
+ property int focusedBorderWidth: 2
+ property real cornerRadius: Theme.cornerRadius
+ readonly property real leftPadding: Theme.spacingM + (leftIconName ? leftIconSize + Theme.spacingM : 0)
+ readonly property real rightPadding: {
+ let p = Theme.spacingS;
+ if (showPasswordToggle)
+ p += 20 + Theme.spacingXS;
+ if (showClearButton && text.length > 0)
+ p += 20 + Theme.spacingXS;
+ return p;
+ }
+ property real topPadding: Theme.spacingS
+ property real bottomPadding: Theme.spacingS
+ property bool ignoreLeftRightKeys: false
+ property bool ignoreUpDownKeys: false
+ property bool ignoreTabKeys: false
+ property var keyForwardTargets: []
+ property Item keyNavigationTab: null
+ property Item keyNavigationBacktab: null
+
+ signal textEdited
+ signal editingFinished
+ signal accepted
+ signal focusStateChanged(bool hasFocus)
+
+ function getActiveFocus() {
+ return textInput.activeFocus;
+ }
+ function setFocus(value) {
+ textInput.focus = value;
+ }
+ function forceActiveFocus() {
+ textInput.forceActiveFocus();
+ }
+ function selectAll() {
+ textInput.selectAll();
+ }
+ function clear() {
+ textInput.clear();
+ }
+ function insertText(str) {
+ textInput.insert(textInput.cursorPosition, str);
+ }
+
+ width: 200
+ height: Math.round(Theme.fontSizeMedium * 3)
+ radius: cornerRadius
+ color: backgroundColor
+ border.color: textInput.activeFocus ? focusedBorderColor : normalBorderColor
+ border.width: textInput.activeFocus ? focusedBorderWidth : borderWidth
+
+ DankIcon {
+ id: leftIcon
+
+ anchors.left: parent.left
+ anchors.leftMargin: Theme.spacingM
+ anchors.verticalCenter: parent.verticalCenter
+ name: leftIconName
+ size: leftIconSize
+ color: textInput.activeFocus ? leftIconFocusedColor : leftIconColor
+ visible: leftIconName !== ""
+ }
+
+ TextInput {
+ id: textInput
+
+ anchors.left: leftIcon.visible ? leftIcon.right : parent.left
+ anchors.leftMargin: Theme.spacingM
+ anchors.right: rightButtonsRow.left
+ anchors.rightMargin: rightButtonsRow.visible ? Theme.spacingS : Theme.spacingM
+ anchors.top: parent.top
+ anchors.topMargin: root.topPadding
+ anchors.bottom: parent.bottom
+ anchors.bottomMargin: root.bottomPadding
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ selectionColor: Theme.primaryContainer
+ selectedTextColor: Theme.primary
+ horizontalAlignment: TextInput.AlignLeft
+ verticalAlignment: TextInput.AlignVCenter
+ selectByMouse: !root.ignoreLeftRightKeys
+ clip: true
+ activeFocusOnTab: true
+ KeyNavigation.tab: root.keyNavigationTab
+ KeyNavigation.backtab: root.keyNavigationBacktab
+ onTextChanged: root.textEdited()
+ onEditingFinished: root.editingFinished()
+ onAccepted: root.accepted()
+ onActiveFocusChanged: root.focusStateChanged(activeFocus)
+ Keys.forwardTo: root.keyForwardTargets
+ Keys.onLeftPressed: event => {
+ if (root.ignoreLeftRightKeys) {
+ event.accepted = true;
+ } else {
+ // Allow normal TextInput cursor movement
+ event.accepted = false;
+ }
+ }
+ Keys.onRightPressed: event => {
+ if (root.ignoreLeftRightKeys) {
+ event.accepted = true;
+ } else {
+ event.accepted = false;
+ }
+ }
+ Keys.onPressed: event => {
+ if (root.ignoreTabKeys && (event.key === Qt.Key_Tab || event.key === Qt.Key_Backtab)) {
+ event.accepted = false;
+ for (var i = 0; i < root.keyForwardTargets.length; i++) {
+ if (root.keyForwardTargets[i])
+ root.keyForwardTargets[i].Keys.pressed(event);
+ }
+ return;
+ }
+ if (root.ignoreUpDownKeys && (event.key === Qt.Key_Up || event.key === Qt.Key_Down)) {
+ event.accepted = false;
+ for (var i = 0; i < root.keyForwardTargets.length; i++) {
+ if (root.keyForwardTargets[i])
+ root.keyForwardTargets[i].Keys.pressed(event);
+ }
+ return;
+ }
+ if ((event.modifiers & (Qt.ControlModifier | Qt.AltModifier | Qt.MetaModifier)) && root.keyForwardTargets.length > 0) {
+ for (var i = 0; i < root.keyForwardTargets.length; i++) {
+ if (root.keyForwardTargets[i])
+ root.keyForwardTargets[i].Keys.pressed(event);
+ }
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.IBeamCursor
+ acceptedButtons: Qt.NoButton
+ }
+ }
+
+ Row {
+ id: rightButtonsRow
+
+ anchors.right: parent.right
+ anchors.rightMargin: Theme.spacingS
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingXS
+ visible: showPasswordToggle || (showClearButton && text.length > 0)
+
+ StyledRect {
+ id: passwordToggleButton
+
+ width: 20
+ height: 20
+ radius: 10
+ color: passwordToggleArea.containsMouse ? Theme.outlineStrong : "transparent"
+ visible: showPasswordToggle
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: passwordVisible ? "visibility_off" : "visibility"
+ size: 14
+ color: passwordToggleArea.containsMouse ? Theme.outline : Theme.surfaceVariantText
+ }
+
+ MouseArea {
+ id: passwordToggleArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: passwordVisible = !passwordVisible
+ }
+ }
+
+ StyledRect {
+ id: clearButton
+
+ width: 20
+ height: 20
+ radius: 10
+ color: clearArea.containsMouse ? Theme.outlineStrong : "transparent"
+ visible: showClearButton && text.length > 0
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "close"
+ size: 14
+ color: clearArea.containsMouse ? Theme.outline : Theme.surfaceVariantText
+ }
+
+ MouseArea {
+ id: clearArea
+
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: textInput.text = ""
+ }
+ }
+ }
+
+ StyledText {
+ id: placeholderLabel
+
+ anchors.fill: textInput
+ text: root.placeholderText
+ font: textInput.font
+ color: placeholderColor
+ horizontalAlignment: Text.AlignLeft
+ verticalAlignment: textInput.verticalAlignment
+ visible: textInput.text.length === 0 && !textInput.activeFocus
+ elide: I18n.isRtl ? Text.ElideLeft : Text.ElideRight
+ }
+
+ Behavior on border.color {
+ ColorAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ Behavior on border.width {
+ NumberAnimation {
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankToggle.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankToggle.qml
new file mode 100644
index 0000000..3b19a5a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankToggle.qml
@@ -0,0 +1,195 @@
+import QtQuick
+import qs.Common
+import qs.Widgets
+
+Item {
+ id: toggle
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ // API
+ property bool checked: false
+ property bool enabled: true
+ property bool toggling: false
+ property string text: ""
+ property string description: ""
+ property color descriptionColor: Theme.surfaceVariantText
+ property bool hideText: false
+
+ signal clicked
+ signal toggled(bool checked)
+ signal toggleCompleted(bool checked)
+
+ readonly property bool showText: text && !hideText
+
+ readonly property int trackWidth: 52
+ readonly property int trackHeight: 30
+ readonly property int insetCircle: 24
+
+ width: showText ? parent.width : trackWidth
+ height: showText ? Math.max(trackHeight, textColumn.implicitHeight + Theme.spacingM * 2) : trackHeight
+
+ function handleClick() {
+ if (!enabled)
+ return;
+ clicked();
+ toggled(!checked);
+ }
+
+ StyledRect {
+ id: background
+ anchors.fill: parent
+ radius: showText ? Theme.cornerRadius : 0
+ color: "transparent"
+ visible: showText
+
+ StateLayer {
+ visible: showText
+ disabled: !toggle.enabled
+ stateColor: Theme.primary
+ cornerRadius: parent.radius
+ onClicked: toggle.handleClick()
+ }
+ }
+
+ Row {
+ anchors.left: parent.left
+ anchors.right: toggleTrack.left
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ spacing: Theme.spacingXS
+ visible: showText
+
+ Column {
+ id: textColumn
+ width: parent.width
+ anchors.verticalCenter: parent.verticalCenter
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: toggle.text
+ font.pixelSize: Appearance.fontSize.normal
+ font.weight: Font.Medium
+ opacity: toggle.enabled ? 1 : 0.4
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ StyledText {
+ text: toggle.description
+ font.pixelSize: Appearance.fontSize.small
+ color: toggle.descriptionColor
+ wrapMode: Text.WordWrap
+ width: parent.width
+ visible: toggle.description.length > 0
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+ }
+
+ StyledRect {
+ id: toggleTrack
+
+ width: showText ? trackWidth : Math.max(parent.width, trackWidth)
+ height: showText ? trackHeight : Math.max(parent.height, trackHeight)
+ anchors.right: parent.right
+ anchors.rightMargin: showText ? Theme.spacingM : 0
+ anchors.verticalCenter: parent.verticalCenter
+ radius: Theme.cornerRadius
+
+ // Distinguish disabled checked vs unchecked so unchecked disabled switches don't look enabled
+ color: !toggle.enabled ? (toggle.checked ? Qt.alpha(Theme.surfaceText, 0.12) : "transparent") : (toggle.checked ? Theme.primary : Theme.surfaceVariantAlpha)
+ opacity: toggle.toggling ? 0.6 : 1
+
+ // M3 disabled unchecked border: on surface 12% opacity
+ border.color: toggle.checked ? "transparent" : (!toggle.enabled ? Qt.alpha(Theme.surfaceText, 0.12) : Theme.outline)
+
+ readonly property int pad: Math.round((height - thumb.width) / 2)
+ readonly property int edgeLeft: pad
+ readonly property int edgeRight: width - thumb.width - pad
+
+ StyledRect {
+ id: thumb
+
+ width: toggle.checked ? insetCircle : insetCircle - 4
+ height: toggle.checked ? insetCircle : insetCircle - 4
+ radius: Theme.cornerRadius
+ anchors.verticalCenter: parent.verticalCenter
+
+ // M3 disabled thumb:
+ // checked = solid surface | unchecked = outlined off-state thumb
+ color: !toggle.enabled ? (toggle.checked ? Theme.surface : "transparent") : (toggle.checked ? Theme.surface : Theme.outline)
+ border.color: !toggle.enabled ? (toggle.checked ? "transparent" : Qt.alpha(Theme.surfaceText, 0.38)) : Theme.outline
+ border.width: (toggle.checked && toggle.enabled) ? 1 : 2
+
+ x: toggle.checked ? toggleTrack.edgeRight : toggleTrack.edgeLeft
+
+ Behavior on x {
+ SequentialAnimation {
+ NumberAnimation {
+ duration: Appearance.anim.durations.normal
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Appearance.anim.curves.emphasizedDecel
+ }
+ ScriptAction {
+ script: {
+ toggle.toggleCompleted(toggle.checked);
+ }
+ }
+ }
+ }
+
+ Behavior on color {
+ ColorAnimation {
+ duration: Appearance.anim.durations.normal
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Appearance.anim.curves.emphasized
+ }
+ }
+
+ Behavior on border.width {
+ NumberAnimation {
+ duration: Appearance.anim.durations.normal
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Appearance.anim.curves.emphasized
+ }
+ }
+
+ DankIcon {
+ id: checkIcon
+ anchors.centerIn: parent
+ name: "check"
+ size: 20
+ // M3 disabled icon: on surface 38%
+ color: toggle.enabled ? Theme.surfaceText : Qt.alpha(Theme.surfaceText, 0.38)
+ filled: true
+ opacity: (toggle.checked && toggle.enabled) ? 1 : 0
+ scale: (toggle.checked && toggle.enabled) ? 1 : 0.6
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: Anims.durShort
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Anims.emphasized
+ }
+ }
+ Behavior on scale {
+ NumberAnimation {
+ duration: Anims.durShort
+ easing.type: Easing.BezierSpline
+ easing.bezierCurve: Anims.emphasized
+ }
+ }
+ }
+ }
+
+ StateLayer {
+ disabled: !toggle.enabled
+ stateColor: Theme.primary
+ cornerRadius: parent.radius
+ onClicked: toggle.handleClick()
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankTooltip.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankTooltip.qml
new file mode 100644
index 0000000..0e57de9
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankTooltip.qml
@@ -0,0 +1,92 @@
+import QtQuick
+import Quickshell
+import Quickshell.Wayland
+import qs.Common
+
+PanelWindow {
+ id: root
+
+ WlrLayershell.namespace: "dms:tooltip"
+
+ property string text: ""
+ property real targetX: 0
+ property real targetY: 0
+ property var targetScreen: null
+ property bool alignLeft: false
+ property bool alignRight: false
+
+ function show(text, x, y, screen, leftAlign, rightAlign) {
+ root.text = text;
+ if (screen) {
+ targetScreen = screen;
+ const screenX = screen.x || 0;
+ targetX = x - screenX;
+ } else {
+ targetScreen = null;
+ targetX = x;
+ }
+ targetY = y;
+ alignLeft = leftAlign ?? false;
+ alignRight = rightAlign ?? false;
+ visible = true;
+ }
+
+ function hide() {
+ visible = false;
+ }
+
+ screen: targetScreen
+ implicitWidth: Math.min(300, Math.max(120, textContent.implicitWidth + Theme.spacingM * 2))
+ implicitHeight: textContent.implicitHeight + Theme.spacingS * 2
+ color: "transparent"
+ visible: false
+ WlrLayershell.layer: WlrLayershell.Overlay
+ WlrLayershell.exclusiveZone: -1
+
+ anchors {
+ top: true
+ left: true
+ }
+
+ margins {
+ left: {
+ const screenWidth = targetScreen?.width ?? Screen.width
+ if (alignLeft) {
+ return Math.round(Math.max(Theme.spacingS, Math.min(screenWidth - implicitWidth - Theme.spacingS, targetX)))
+ } else if (alignRight) {
+ return Math.round(Math.max(Theme.spacingS, Math.min(screenWidth - implicitWidth - Theme.spacingS, targetX - implicitWidth)))
+ } else {
+ return Math.round(Math.max(Theme.spacingS, Math.min(screenWidth - implicitWidth - Theme.spacingS, targetX - implicitWidth / 2)))
+ }
+ }
+ top: {
+ const screenHeight = targetScreen?.height ?? Screen.height
+ if (alignLeft || alignRight) {
+ return Math.round(Math.max(Theme.spacingS, Math.min(screenHeight - implicitHeight - Theme.spacingS, targetY - implicitHeight / 2)))
+ } else {
+ return Math.round(Math.max(Theme.spacingS, Math.min(screenHeight - implicitHeight - Theme.spacingS, targetY)))
+ }
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ radius: Theme.cornerRadius
+ border.width: 1
+ border.color: Theme.outlineMedium
+
+ Text {
+ id: textContent
+
+ anchors.centerIn: parent
+ text: root.text
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ wrapMode: Text.NoWrap
+ maximumLineCount: 1
+ elide: Text.ElideRight
+ width: Math.min(implicitWidth, 300 - Theme.spacingM * 2)
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/DankTooltipV2.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/DankTooltipV2.qml
new file mode 100644
index 0000000..6cd53c8
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/DankTooltipV2.qml
@@ -0,0 +1,154 @@
+import QtQuick
+import QtQuick.Controls
+import qs.Common
+
+Item {
+ id: root
+
+ property string text: ""
+
+ function show(text, item, offsetX, offsetY, preferredSide) {
+ if (!item)
+ return;
+
+ let windowContentItem = item.Window?.window?.contentItem;
+ if (!windowContentItem) {
+ let current = item;
+ while (current) {
+ if (current.Window?.window?.contentItem) {
+ windowContentItem = current.Window.window.contentItem;
+ break;
+ }
+ current = current.parent;
+ }
+ }
+ if (!windowContentItem)
+ return;
+
+ tooltip.parent = windowContentItem;
+ tooltip.text = text;
+
+ const itemPos = item.mapToItem(windowContentItem, 0, 0);
+ const parentWidth = windowContentItem.width;
+ const parentHeight = windowContentItem.height;
+ const tooltipWidth = tooltip.implicitWidth;
+ const tooltipHeight = tooltip.implicitHeight;
+
+ const side = preferredSide || _determineBestSide(itemPos, item, parentWidth, parentHeight, tooltipWidth, tooltipHeight);
+
+ let targetX = 0;
+ let targetY = 0;
+
+ switch (side) {
+ case "left":
+ targetX = itemPos.x - tooltipWidth - 8;
+ targetY = itemPos.y + (item.height - tooltipHeight) / 2;
+ break;
+ case "right":
+ targetX = itemPos.x + item.width + 8;
+ targetY = itemPos.y + (item.height - tooltipHeight) / 2;
+ break;
+ case "top":
+ targetX = itemPos.x + (item.width - tooltipWidth) / 2;
+ targetY = itemPos.y - tooltipHeight - 8;
+ break;
+ case "bottom":
+ default:
+ targetX = itemPos.x + (item.width - tooltipWidth) / 2;
+ targetY = itemPos.y + item.height + 8;
+ break;
+ }
+
+ tooltip.x = Math.max(4, Math.min(parentWidth - tooltipWidth - 4, targetX + (offsetX || 0)));
+ tooltip.y = Math.max(4, Math.min(parentHeight - tooltipHeight - 4, targetY + (offsetY || 0)));
+
+ tooltip.open();
+ }
+
+ function _determineBestSide(itemPos, item, parentWidth, parentHeight, tooltipWidth, tooltipHeight) {
+ const itemCenterX = itemPos.x + item.width / 2;
+ const itemCenterY = itemPos.y + item.height / 2;
+
+ const spaceLeft = itemPos.x;
+ const spaceRight = parentWidth - (itemPos.x + item.width);
+ const spaceTop = itemPos.y;
+ const spaceBottom = parentHeight - (itemPos.y + item.height);
+
+ if (spaceRight >= tooltipWidth + 16) {
+ return "right";
+ }
+ if (spaceLeft >= tooltipWidth + 16) {
+ return "left";
+ }
+ if (spaceBottom >= tooltipHeight + 16) {
+ return "bottom";
+ }
+ if (spaceTop >= tooltipHeight + 16) {
+ return "top";
+ }
+
+ if (itemCenterX > parentWidth / 2) {
+ return "left";
+ }
+ return "right";
+ }
+
+ function hide() {
+ tooltip.close();
+ }
+
+ Popup {
+ id: tooltip
+
+ property string text: ""
+
+ leftPadding: Theme.spacingM
+ rightPadding: Theme.spacingM
+ topPadding: Theme.spacingS
+ bottomPadding: Theme.spacingS
+ closePolicy: Popup.NoAutoClose
+ modal: false
+ dim: false
+
+ background: Rectangle {
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+ radius: Theme.cornerRadius
+ border.width: 1
+ border.color: Theme.outlineMedium
+ }
+
+ contentItem: Text {
+ id: textContent
+
+ width: Math.min(implicitWidth, 500)
+ text: tooltip.text
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ wrapMode: Text.NoWrap
+ maximumLineCount: 1
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignHCenter
+ verticalAlignment: Text.AlignVCenter
+ }
+
+ enter: Transition {
+ NumberAnimation {
+ property: "opacity"
+ from: 0
+ to: 1
+ duration: Theme.shortDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+
+ exit: Transition {
+ NumberAnimation {
+ property: "opacity"
+ from: 1
+ to: 0
+ duration: Theme.shorterDuration
+ easing.type: Theme.standardEasing
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/FloatingWindowControls.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/FloatingWindowControls.qml
new file mode 100644
index 0000000..b56291e
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/FloatingWindowControls.qml
@@ -0,0 +1,118 @@
+import QtQuick
+
+Item {
+ id: root
+
+ readonly property real edgeSize: 8
+ required property var targetWindow
+ property bool supported: typeof targetWindow.startSystemMove === "function"
+ readonly property bool canMaximize: targetWindow.minimumSize.width !== targetWindow.maximumSize.width || targetWindow.minimumSize.height !== targetWindow.maximumSize.height
+
+ anchors.fill: parent
+
+ function tryStartMove() {
+ if (!supported)
+ return;
+ targetWindow.startSystemMove();
+ }
+
+ function tryStartResize(edges) {
+ if (!supported || !canMaximize)
+ return;
+ targetWindow.startSystemResize(edges);
+ }
+
+ function tryToggleMaximize() {
+ if (!supported || !canMaximize)
+ return;
+ targetWindow.maximized = !targetWindow.maximized;
+ }
+
+ MouseArea {
+ visible: root.supported && root.canMaximize
+ height: root.edgeSize
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.leftMargin: 6
+ anchors.rightMargin: 6
+ cursorShape: Qt.SizeVerCursor
+ onPressed: root.tryStartResize(Qt.TopEdge)
+ }
+
+ MouseArea {
+ visible: root.supported && root.canMaximize
+ width: root.edgeSize
+ anchors.left: parent.left
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.topMargin: 6
+ anchors.bottomMargin: 6
+ cursorShape: Qt.SizeHorCursor
+ onPressed: root.tryStartResize(Qt.LeftEdge)
+ }
+
+ MouseArea {
+ visible: root.supported && root.canMaximize
+ width: root.edgeSize
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ anchors.topMargin: 6
+ anchors.bottomMargin: 6
+ cursorShape: Qt.SizeHorCursor
+ onPressed: root.tryStartResize(Qt.RightEdge)
+ }
+
+ MouseArea {
+ visible: root.supported && root.canMaximize
+ width: root.edgeSize
+ height: root.edgeSize
+ anchors.left: parent.left
+ anchors.top: parent.top
+ cursorShape: Qt.SizeFDiagCursor
+ onPressed: root.tryStartResize(Qt.LeftEdge | Qt.TopEdge)
+ }
+
+ MouseArea {
+ visible: root.supported && root.canMaximize
+ width: root.edgeSize
+ height: root.edgeSize
+ anchors.right: parent.right
+ anchors.top: parent.top
+ cursorShape: Qt.SizeBDiagCursor
+ onPressed: root.tryStartResize(Qt.RightEdge | Qt.TopEdge)
+ }
+
+ MouseArea {
+ visible: root.supported && root.canMaximize
+ height: root.edgeSize
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ anchors.leftMargin: 6
+ anchors.rightMargin: 6
+ cursorShape: Qt.SizeVerCursor
+ onPressed: root.tryStartResize(Qt.BottomEdge)
+ }
+
+ MouseArea {
+ visible: root.supported && root.canMaximize
+ width: root.edgeSize
+ height: root.edgeSize
+ anchors.left: parent.left
+ anchors.bottom: parent.bottom
+ cursorShape: Qt.SizeBDiagCursor
+ onPressed: root.tryStartResize(Qt.LeftEdge | Qt.BottomEdge)
+ }
+
+ MouseArea {
+ visible: root.supported && root.canMaximize
+ width: root.edgeSize
+ height: root.edgeSize
+ anchors.right: parent.right
+ anchors.bottom: parent.bottom
+ cursorShape: Qt.SizeFDiagCursor
+ onPressed: root.tryStartResize(Qt.RightEdge | Qt.BottomEdge)
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/KeybindItem.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/KeybindItem.qml
new file mode 100644
index 0000000..a68cc4a
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/KeybindItem.qml
@@ -0,0 +1,1840 @@
+pragma ComponentBehavior: Bound
+
+import QtQuick
+import QtQuick.Layouts
+import Quickshell.Wayland
+import qs.Common
+import qs.Services
+import qs.Widgets
+import "../Common/KeyUtils.js" as KeyUtils
+import "../Common/KeybindActions.js" as Actions
+
+Item {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property var bindData: ({})
+ property bool isExpanded: false
+ property var panelWindow: null
+ property bool recording: false
+ property bool isNew: false
+ property string restoreKey: ""
+
+ property int editingKeyIndex: -1
+ property string editKey: ""
+ property string editAction: ""
+ property string editDesc: ""
+ property int editCooldownMs: 0
+ property string editFlags: ""
+ property bool editAllowWhenLocked: false
+ property var editRepeat: undefined
+ property var editAllowInhibiting: undefined
+ property int _savedCooldownMs: -1
+ property string _savedFlags: ""
+ property var _savedAllowWhenLocked: undefined
+ property var _savedRepeat: undefined
+ property var _savedAllowInhibiting: undefined
+ property bool hasChanges: false
+ property string _actionType: ""
+ property bool addingNewKey: false
+ property bool useCustomCompositor: false
+ property var _shortcutInhibitor: null
+ property bool _altShiftGhost: false
+
+ readonly property bool _shortcutInhibitorAvailable: {
+ try {
+ return typeof ShortcutInhibitor !== "undefined";
+ } catch (e) {
+ return false;
+ }
+ }
+
+ readonly property var keys: bindData.keys || []
+ readonly property bool hasOverride: {
+ for (var i = 0; i < keys.length; i++) {
+ if (keys[i].isOverride)
+ return true;
+ }
+ return false;
+ }
+ readonly property var configConflict: bindData.conflict || null
+ readonly property bool hasConfigConflict: configConflict !== null
+ readonly property string _originalKey: editingKeyIndex >= 0 && editingKeyIndex < keys.length ? keys[editingKeyIndex].key : ""
+ readonly property var _conflicts: editKey ? KeyUtils.getConflictingBinds(editKey, bindData.action, KeybindsService.getFlatBinds()) : []
+ readonly property bool hasConflict: _conflicts.length > 0
+
+ readonly property real _inputHeight: Math.round(Theme.fontSizeMedium * 3)
+ readonly property real _chipHeight: Math.round(Theme.fontSizeSmall * 2.3)
+ readonly property real _buttonHeight: Math.round(Theme.fontSizeMedium * 2.3)
+ readonly property real _keysColumnWidth: Math.round(Theme.fontSizeSmall * 12)
+ readonly property real _labelWidth: Math.round(Theme.fontSizeSmall * 5)
+
+ signal toggleExpand
+ signal saveBind(string originalKey, var newData)
+ signal removeBind(string key)
+ signal cancelEdit
+
+ implicitHeight: contentColumn.implicitHeight
+ height: implicitHeight
+
+ Component.onDestruction: _destroyShortcutInhibitor()
+
+ Component.onCompleted: {
+ if (isNew && isExpanded)
+ resetEdits();
+ }
+
+ onIsExpandedChanged: {
+ if (!isExpanded)
+ return;
+ if (restoreKey) {
+ restoreToKey(restoreKey);
+ } else {
+ resetEdits();
+ }
+ }
+
+ onRestoreKeyChanged: {
+ if (!isExpanded || !restoreKey)
+ return;
+ restoreToKey(restoreKey);
+ }
+
+ function restoreToKey(keyToFind) {
+ for (var i = 0; i < keys.length; i++) {
+ if (keys[i].key === keyToFind) {
+ editingKeyIndex = i;
+ editKey = keyToFind;
+ editAction = bindData.action || "";
+ editDesc = bindData.desc || "";
+ if (_savedCooldownMs >= 0) {
+ editCooldownMs = _savedCooldownMs;
+ _savedCooldownMs = -1;
+ } else {
+ editCooldownMs = keys[i].cooldownMs || 0;
+ }
+ if (_savedFlags) {
+ editFlags = _savedFlags;
+ _savedFlags = "";
+ } else {
+ editFlags = keys[i].flags || "";
+ }
+ if (_savedAllowWhenLocked !== undefined) {
+ editAllowWhenLocked = _savedAllowWhenLocked;
+ _savedAllowWhenLocked = undefined;
+ } else {
+ editAllowWhenLocked = keys[i].allowWhenLocked || false;
+ }
+ if (_savedRepeat !== undefined) {
+ editRepeat = _savedRepeat;
+ _savedRepeat = undefined;
+ } else {
+ editRepeat = keys[i].repeat;
+ }
+ if (_savedAllowInhibiting !== undefined) {
+ editAllowInhibiting = _savedAllowInhibiting;
+ _savedAllowInhibiting = undefined;
+ } else {
+ editAllowInhibiting = keys[i].allowInhibiting;
+ }
+ hasChanges = false;
+ _actionType = Actions.getActionType(editAction);
+ useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(KeybindsService.currentProvider, editAction);
+ return;
+ }
+ }
+ resetEdits();
+ }
+
+ onEditActionChanged: {
+ _actionType = Actions.getActionType(editAction);
+ }
+
+ function resetEdits() {
+ addingNewKey = false;
+ editingKeyIndex = keys.length > 0 ? 0 : -1;
+ editKey = editingKeyIndex >= 0 ? keys[editingKeyIndex].key : "";
+ editAction = bindData.action || "";
+ editDesc = bindData.desc || "";
+ editCooldownMs = editingKeyIndex >= 0 ? (keys[editingKeyIndex].cooldownMs || 0) : 0;
+ editFlags = editingKeyIndex >= 0 ? (keys[editingKeyIndex].flags || "") : "";
+ editAllowWhenLocked = editingKeyIndex >= 0 ? (keys[editingKeyIndex].allowWhenLocked || false) : false;
+ editRepeat = editingKeyIndex >= 0 ? keys[editingKeyIndex].repeat : undefined;
+ editAllowInhibiting = editingKeyIndex >= 0 ? keys[editingKeyIndex].allowInhibiting : undefined;
+ hasChanges = false;
+ _actionType = Actions.getActionType(editAction);
+ useCustomCompositor = _actionType === "compositor" && editAction && !Actions.isKnownCompositorAction(KeybindsService.currentProvider, editAction);
+ }
+
+ function startAddingNewKey() {
+ addingNewKey = true;
+ editingKeyIndex = -1;
+ editKey = "";
+ hasChanges = true;
+ }
+
+ function selectKeyForEdit(index) {
+ if (index < 0 || index >= keys.length)
+ return;
+ addingNewKey = false;
+ editingKeyIndex = index;
+ editKey = keys[index].key;
+ editCooldownMs = keys[index].cooldownMs || 0;
+ editFlags = keys[index].flags || "";
+ editAllowWhenLocked = keys[index].allowWhenLocked || false;
+ editRepeat = keys[index].repeat;
+ editAllowInhibiting = keys[index].allowInhibiting;
+ hasChanges = false;
+ }
+
+ function updateEdit(changes) {
+ if (changes.key !== undefined)
+ editKey = changes.key;
+ if (changes.action !== undefined)
+ editAction = changes.action;
+ if (changes.desc !== undefined)
+ editDesc = changes.desc;
+ if (changes.cooldownMs !== undefined)
+ editCooldownMs = changes.cooldownMs;
+ if (changes.flags !== undefined)
+ editFlags = changes.flags;
+ if (changes.allowWhenLocked !== undefined)
+ editAllowWhenLocked = changes.allowWhenLocked;
+ if (changes.repeat !== undefined)
+ editRepeat = changes.repeat;
+ if (changes.allowInhibiting !== undefined)
+ editAllowInhibiting = changes.allowInhibiting;
+ const hasKey = editingKeyIndex >= 0 && editingKeyIndex < keys.length;
+ const origKey = hasKey ? keys[editingKeyIndex].key : "";
+ const origCooldown = hasKey ? (keys[editingKeyIndex].cooldownMs || 0) : 0;
+ const origFlags = hasKey ? (keys[editingKeyIndex].flags || "") : "";
+ const origAllowWhenLocked = hasKey ? (keys[editingKeyIndex].allowWhenLocked || false) : false;
+ const origRepeat = hasKey ? keys[editingKeyIndex].repeat : undefined;
+ const origAllowInhibiting = hasKey ? keys[editingKeyIndex].allowInhibiting : undefined;
+ hasChanges = editKey !== origKey || editAction !== (bindData.action || "") || editDesc !== (bindData.desc || "") || editCooldownMs !== origCooldown || editFlags !== origFlags || editAllowWhenLocked !== origAllowWhenLocked || editRepeat !== origRepeat || editAllowInhibiting !== origAllowInhibiting;
+ }
+
+ function canSave() {
+ if (!editKey)
+ return false;
+ if (!Actions.isValidAction(editAction))
+ return false;
+ return true;
+ }
+
+ function doSave() {
+ if (!canSave())
+ return;
+ const origKey = addingNewKey ? "" : _originalKey;
+ let desc = editDesc;
+ if (expandedLoader.item?.currentTitle !== undefined)
+ desc = expandedLoader.item.currentTitle;
+ _savedCooldownMs = editCooldownMs;
+ _savedFlags = editFlags;
+ _savedAllowWhenLocked = editAllowWhenLocked;
+ _savedRepeat = editRepeat;
+ _savedAllowInhibiting = editAllowInhibiting;
+ saveBind(origKey, {
+ "key": editKey,
+ "action": editAction,
+ "desc": desc,
+ "cooldownMs": editCooldownMs,
+ "flags": editFlags,
+ "allowWhenLocked": editAllowWhenLocked,
+ "repeat": editRepeat,
+ "allowInhibiting": editAllowInhibiting
+ });
+ hasChanges = false;
+ addingNewKey = false;
+ }
+
+ function _createShortcutInhibitor() {
+ if (!_shortcutInhibitorAvailable || _shortcutInhibitor)
+ return;
+ const qmlString = `
+ import QtQuick
+ import Quickshell.Wayland
+
+ ShortcutInhibitor {
+ enabled: false
+ window: null
+ }
+ `;
+
+ _shortcutInhibitor = Qt.createQmlObject(qmlString, root, "KeybindItem.ShortcutInhibitor");
+ _shortcutInhibitor.enabled = Qt.binding(() => root.recording);
+ _shortcutInhibitor.window = Qt.binding(() => root.panelWindow);
+ }
+
+ function _destroyShortcutInhibitor() {
+ if (_shortcutInhibitor) {
+ _shortcutInhibitor.enabled = false;
+ _shortcutInhibitor.destroy();
+ _shortcutInhibitor = null;
+ }
+ }
+
+ function startRecording() {
+ _destroyShortcutInhibitor();
+ _createShortcutInhibitor();
+ recording = true;
+ }
+
+ function stopRecording() {
+ recording = false;
+ _destroyShortcutInhibitor();
+ }
+
+ Column {
+ id: contentColumn
+ width: parent.width
+ spacing: 0
+
+ Rectangle {
+ id: collapsedRect
+ width: parent.width
+ height: Math.max(root._inputHeight + Theme.spacingM, keysColumn.implicitHeight + Theme.spacingM * 2)
+ radius: root.isExpanded ? 0 : Theme.cornerRadius
+ topLeftRadius: Theme.cornerRadius
+ topRightRadius: Theme.cornerRadius
+ color: root.hasOverride ? Theme.surfaceContainer : Theme.surfaceContainerHighest
+ border.color: root.hasOverride ? Theme.outlineVariant : "transparent"
+ border.width: root.hasOverride ? 1 : 0
+
+ RowLayout {
+ id: collapsedContent
+ anchors.fill: parent
+ anchors.leftMargin: Theme.spacingM
+ anchors.rightMargin: Theme.spacingM
+ spacing: Theme.spacingM
+
+ Column {
+ id: keysColumn
+ Layout.preferredWidth: root._keysColumnWidth
+ Layout.alignment: Qt.AlignVCenter
+ spacing: Theme.spacingXS
+
+ Repeater {
+ model: root.keys
+
+ delegate: Rectangle {
+ required property var modelData
+ required property int index
+
+ property bool isSelected: root.isExpanded && root.editingKeyIndex === index && !root.addingNewKey
+
+ width: root._keysColumnWidth
+ height: root._chipHeight
+ radius: root._chipHeight / 4
+ color: isSelected ? Theme.primary : Theme.surfaceVariant
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: chipArea.pressed ? Theme.surfaceTextHover : (chipArea.containsMouse ? Theme.surfaceTextHover : "transparent")
+ }
+
+ StyledText {
+ text: modelData.key
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: parent.isSelected ? Font.Medium : Font.Normal
+ isMonospace: true
+ color: parent.isSelected ? Theme.primaryText : Theme.surfaceVariantText
+ anchors.centerIn: parent
+ width: parent.width - Theme.spacingS
+ horizontalAlignment: Text.AlignHCenter
+ elide: Text.ElideRight
+ }
+
+ MouseArea {
+ id: chipArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ root.selectKeyForEdit(index);
+ if (!root.isExpanded)
+ root.toggleExpand();
+ }
+ }
+ }
+ }
+ }
+
+ ColumnLayout {
+ Layout.fillWidth: true
+ Layout.alignment: Qt.AlignVCenter
+ spacing: 2
+
+ StyledText {
+ text: root.bindData.desc || root.bindData.action || I18n.tr("No action")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ elide: Text.ElideRight
+ Layout.fillWidth: true
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ RowLayout {
+ spacing: Theme.spacingS
+ Layout.fillWidth: true
+
+ StyledText {
+ text: root.bindData.category || ""
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ visible: text.length > 0
+ }
+
+ Rectangle {
+ width: 4
+ height: 4
+ radius: 2
+ color: Theme.surfaceVariantText
+ visible: root.hasOverride && (root.bindData.category ?? "")
+ }
+
+ StyledText {
+ text: I18n.tr("Override")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.primary
+ visible: root.hasOverride && !root.hasConfigConflict
+ }
+
+ DankIcon {
+ name: "warning"
+ size: Theme.iconSizeSmall
+ color: Theme.primary
+ visible: root.hasConfigConflict
+ }
+
+ StyledText {
+ text: I18n.tr("Overridden by config")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.primary
+ visible: root.hasConfigConflict
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+ }
+ }
+
+ DankIcon {
+ name: root.isExpanded ? "expand_less" : "expand_more"
+ size: Theme.iconSize - 4
+ color: Theme.surfaceVariantText
+ Layout.alignment: Qt.AlignVCenter
+ }
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ anchors.leftMargin: root._keysColumnWidth + Theme.spacingM * 2
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.toggleExpand()
+ }
+ }
+
+ Loader {
+ id: expandedLoader
+ width: parent.width
+ active: root.isExpanded
+ visible: status === Loader.Ready
+ asynchronous: true
+ sourceComponent: expandedComponent
+ }
+ }
+
+ Component {
+ id: expandedComponent
+
+ Rectangle {
+ id: expandedRect
+ width: parent ? parent.width : 0
+ height: expandedContent.implicitHeight + Theme.spacingL * 2
+ color: Theme.surfaceContainerHigh
+ border.color: root.hasOverride ? Theme.outlineVariant : "transparent"
+ border.width: root.hasOverride ? 1 : 0
+ bottomLeftRadius: Theme.cornerRadius
+ bottomRightRadius: Theme.cornerRadius
+
+ property alias currentTitle: titleField.text
+
+ ColumnLayout {
+ id: expandedContent
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.top: parent.top
+ anchors.margins: Theme.spacingL
+ spacing: Theme.spacingM
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: conflictColumn.implicitHeight + Theme.spacingM * 2
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.primary, 0.15)
+ border.color: Theme.withAlpha(Theme.primary, 0.3)
+ border.width: 1
+ visible: root.hasConfigConflict
+
+ Column {
+ id: conflictColumn
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingS
+
+ RowLayout {
+ width: parent.width
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "warning"
+ size: Theme.iconSizeSmall
+ color: Theme.primary
+ }
+
+ StyledText {
+ text: I18n.tr("This bind is overridden by config.kdl")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.primary
+ Layout.fillWidth: true
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Config action: %1").arg(root.configConflict?.action ?? "")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ wrapMode: Text.WordWrap
+ }
+
+ StyledText {
+ text: I18n.tr("To use this DMS bind, remove or change the keybind in your config.kdl")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ width: parent.width
+ wrapMode: Text.WordWrap
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: root.keys.length > 1 || root.addingNewKey
+
+ StyledText {
+ text: I18n.tr("Keys")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ Flow {
+ Layout.fillWidth: true
+ spacing: Theme.spacingXS
+
+ Repeater {
+ model: root.keys
+
+ delegate: Rectangle {
+ required property var modelData
+ required property int index
+
+ property bool isSelected: root.editingKeyIndex === index && !root.addingNewKey
+
+ width: editKeyChipText.implicitWidth + Theme.spacingM
+ height: root._chipHeight
+ radius: root._chipHeight / 4
+ color: isSelected ? Theme.primary : Theme.surfaceVariant
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: editKeyChipArea.pressed ? Theme.surfaceTextHover : (editKeyChipArea.containsMouse && !parent.isSelected ? Theme.surfaceTextHover : "transparent")
+ }
+
+ StyledText {
+ id: editKeyChipText
+ text: modelData.key
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: parent.isSelected ? Font.Medium : Font.Normal
+ isMonospace: true
+ color: parent.isSelected ? Theme.primaryText : Theme.surfaceVariantText
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: editKeyChipArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.selectKeyForEdit(index)
+ }
+ }
+ }
+
+ Rectangle {
+ width: root._chipHeight
+ height: root._chipHeight
+ radius: root._chipHeight / 4
+ color: root.addingNewKey ? Theme.primary : Theme.surfaceVariant
+ visible: !root.isNew
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: addKeyArea.pressed ? Theme.surfaceTextHover : (addKeyArea.containsMouse && !root.addingNewKey ? Theme.surfaceTextHover : "transparent")
+ }
+
+ DankIcon {
+ name: "add"
+ size: Theme.iconSizeSmall
+ color: root.addingNewKey ? Theme.primaryText : Theme.surfaceVariantText
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: addKeyArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.startAddingNewKey()
+ }
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+
+ StyledText {
+ text: root.addingNewKey ? I18n.tr("New Key") : I18n.tr("Key")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ FocusScope {
+ id: captureScope
+ Layout.fillWidth: true
+ Layout.preferredHeight: root._inputHeight
+ focus: root.recording
+
+ Component.onCompleted: {
+ if (root.recording)
+ forceActiveFocus();
+ }
+
+ Connections {
+ target: root
+ function onRecordingChanged() {
+ if (root.recording)
+ captureScope.forceActiveFocus();
+ }
+ }
+
+ Rectangle {
+ anchors.fill: parent
+ radius: Theme.cornerRadius
+ color: root.recording ? Theme.primaryContainer : Theme.surfaceContainer
+ border.color: root.recording ? Theme.primary : Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.3)
+ border.width: root.recording ? 2 : 1
+
+ Row {
+ anchors.left: parent.left
+ anchors.right: parent.right
+ anchors.verticalCenter: parent.verticalCenter
+ anchors.margins: Theme.spacingS
+ spacing: Theme.spacingS
+
+ StyledText {
+ text: root.editKey || (root.recording ? I18n.tr("Press key...") : I18n.tr("Click to capture"))
+ font.pixelSize: Theme.fontSizeMedium
+ isMonospace: root.editKey ? true : false
+ color: root.editKey ? Theme.surfaceText : Theme.surfaceVariantText
+ width: parent.width - recordBtn.width - parent.spacing
+ anchors.verticalCenter: parent.verticalCenter
+ elide: Text.ElideRight
+ }
+
+ DankActionButton {
+ id: recordBtn
+ width: root._chipHeight
+ height: root._chipHeight
+ anchors.verticalCenter: parent.verticalCenter
+ circular: false
+ iconName: root.recording ? "close" : "radio_button_checked"
+ iconSize: Theme.iconSizeSmall
+ iconColor: root.recording ? Theme.error : Theme.primary
+ onClicked: root.recording ? root.stopRecording() : root.startRecording()
+ }
+ }
+ }
+
+ Keys.onPressed: event => {
+ if (!root.recording)
+ return;
+ event.accepted = true;
+
+ switch (event.key) {
+ case Qt.Key_Control:
+ case Qt.Key_Shift:
+ case Qt.Key_Alt:
+ case Qt.Key_Meta:
+ return;
+ }
+
+ if (event.key === 0 && (event.modifiers & Qt.AltModifier)) {
+ root._altShiftGhost = true;
+ return;
+ }
+
+ let mods = KeyUtils.modsFromEvent(event.modifiers);
+ let qtKey = event.key;
+
+ if (root._altShiftGhost && (event.modifiers & Qt.AltModifier) && !mods.includes("Shift")) {
+ mods.push("Shift");
+ }
+ root._altShiftGhost = false;
+
+ if (qtKey === Qt.Key_Backtab) {
+ qtKey = Qt.Key_Tab;
+ if (!mods.includes("Shift"))
+ mods.push("Shift");
+ }
+
+ const key = KeyUtils.xkbKeyFromQtKey(qtKey);
+ if (!key) {
+ console.warn("[KeybindItem] Unknown key:", event.key, "mods:", event.modifiers);
+ return;
+ }
+
+ root.updateEdit({
+ "key": KeyUtils.formatToken(mods, key)
+ });
+ root.stopRecording();
+ }
+
+ MouseArea {
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: root.recording ? Qt.CrossCursor : Qt.PointingHandCursor
+ acceptedButtons: Qt.LeftButton
+
+ onClicked: {
+ if (!root.recording)
+ root.startRecording();
+ }
+
+ onWheel: wheel => {
+ if (!root.recording) {
+ wheel.accepted = false;
+ return;
+ }
+ wheel.accepted = true;
+
+ const mods = [];
+ if (wheel.modifiers & Qt.ControlModifier)
+ mods.push("Ctrl");
+ if (wheel.modifiers & Qt.ShiftModifier)
+ mods.push("Shift");
+ if (wheel.modifiers & Qt.AltModifier)
+ mods.push("Alt");
+ if (wheel.modifiers & Qt.MetaModifier)
+ mods.push("Super");
+
+ let wheelKey = "";
+ if (wheel.angleDelta.y > 0)
+ wheelKey = "WheelScrollUp";
+ else if (wheel.angleDelta.y < 0)
+ wheelKey = "WheelScrollDown";
+ else if (wheel.angleDelta.x > 0)
+ wheelKey = "WheelScrollRight";
+ else if (wheel.angleDelta.x < 0)
+ wheelKey = "WheelScrollLeft";
+
+ if (!wheelKey)
+ return;
+ root.updateEdit({
+ "key": KeyUtils.formatToken(mods, wheelKey)
+ });
+ root.stopRecording();
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.preferredWidth: root._inputHeight
+ Layout.preferredHeight: root._inputHeight
+ radius: Theme.cornerRadius
+ color: root.addingNewKey ? Theme.primary : Theme.surfaceVariant
+ visible: root.keys.length === 1 && !root.isNew
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: singleAddKeyArea.pressed ? Theme.surfaceTextHover : (singleAddKeyArea.containsMouse && !root.addingNewKey ? Theme.surfaceTextHover : "transparent")
+ }
+
+ DankIcon {
+ name: "add"
+ size: Theme.iconSizeSmall + 2
+ color: root.addingNewKey ? Theme.primaryText : Theme.surfaceVariantText
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: singleAddKeyArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.startAddingNewKey()
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingS
+ visible: root.hasConflict
+ Layout.leftMargin: root._labelWidth + Theme.spacingM
+
+ DankIcon {
+ name: "warning"
+ size: Theme.iconSizeSmall
+ color: Theme.primary
+ }
+
+ StyledText {
+ text: I18n.tr("Conflicts with: %1").arg(root._conflicts.map(c => c.desc).join(", "))
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.primary
+ Layout.fillWidth: true
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+
+ StyledText {
+ text: I18n.tr("Type")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingS
+
+ Repeater {
+ model: KeybindsService.actionTypes
+
+ delegate: Rectangle {
+ id: typeDelegate
+ required property var modelData
+ required property int index
+
+ readonly property var tooltipTexts: ({
+ "dms": I18n.tr("DMS shell actions (launcher, clipboard, etc.)"),
+ "compositor": I18n.tr("Niri compositor actions (focus, move, etc.)"),
+ "spawn": I18n.tr("Run a program (e.g., firefox, kitty)"),
+ "shell": I18n.tr("Run a shell command (e.g., notify-send)")
+ })
+
+ Layout.fillWidth: true
+ Layout.preferredHeight: root._buttonHeight
+ radius: Theme.cornerRadius
+ color: root._actionType === modelData.id ? Theme.surfaceContainerHighest : Theme.surfaceContainer
+ border.color: root._actionType === modelData.id ? Theme.outline : (typeArea.containsMouse ? Theme.outlineVariant : "transparent")
+ border.width: 1
+
+ RowLayout {
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: typeDelegate.modelData.icon
+ size: Theme.iconSizeSmall
+ color: root._actionType === typeDelegate.modelData.id ? Theme.surfaceText : Theme.surfaceVariantText
+ }
+
+ StyledText {
+ text: typeDelegate.modelData.label
+ font.pixelSize: Theme.fontSizeSmall
+ color: root._actionType === typeDelegate.modelData.id ? Theme.surfaceText : Theme.surfaceVariantText
+ visible: typeDelegate.width > 100
+ }
+ }
+
+ MouseArea {
+ id: typeArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ switch (typeDelegate.modelData.id) {
+ case "dms":
+ root.updateEdit({
+ "action": KeybindsService.dmsActions[0].id,
+ "desc": KeybindsService.dmsActions[0].label
+ });
+ break;
+ case "compositor":
+ root.updateEdit({
+ "action": "close-window",
+ "desc": "Close Window"
+ });
+ break;
+ case "spawn":
+ root.updateEdit({
+ "action": "spawn ",
+ "desc": ""
+ });
+ break;
+ case "shell":
+ root.updateEdit({
+ "action": "spawn sh -c \"\"",
+ "desc": ""
+ });
+ break;
+ }
+ }
+ onContainsMouseChanged: {
+ if (containsMouse) {
+ typeTooltip.show(typeDelegate.tooltipTexts[typeDelegate.modelData.id], typeDelegate, 0, 0, "bottom");
+ } else {
+ typeTooltip.hide();
+ }
+ }
+ }
+ }
+ }
+ }
+
+ DankTooltipV2 {
+ id: typeTooltip
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: root._actionType === "dms"
+
+ StyledText {
+ text: I18n.tr("Action")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ DankDropdown {
+ Layout.fillWidth: true
+ compactMode: true
+ currentValue: KeybindsService.getActionLabel(root.editAction) || I18n.tr("Select...")
+ options: KeybindsService.getDmsActions().map(a => a.label)
+ enableFuzzySearch: true
+ maxPopupHeight: 300
+ onValueChanged: value => {
+ const actions = KeybindsService.getDmsActions();
+ for (const act of actions) {
+ if (act.label === value) {
+ root.updateEdit({
+ "action": act.id,
+ "desc": act.label
+ });
+ return;
+ }
+ }
+ }
+ }
+ }
+
+ RowLayout {
+ id: dmsArgsRow
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+
+ readonly property var argConfig: Actions.getActionArgConfig(KeybindsService.currentProvider, root.editAction)
+ readonly property var parsedArgs: argConfig?.type === "dms" ? Actions.parseDmsActionArgs(root.editAction) : null
+ readonly property var dmsActionArgs: Actions.getDmsActionArgs()
+ readonly property bool hasAmountArg: parsedArgs?.base ? (dmsActionArgs[parsedArgs.base]?.args?.some(a => a.name === "amount") ?? false) : false
+ readonly property bool hasDeviceArg: parsedArgs?.base ? (dmsActionArgs[parsedArgs.base]?.args?.some(a => a.name === "device") ?? false) : false
+ readonly property bool hasTabArg: parsedArgs?.base ? (dmsActionArgs[parsedArgs.base]?.args?.some(a => a.name === "tab") ?? false) : false
+
+ visible: root._actionType === "dms" && argConfig?.type === "dms"
+
+ StyledText {
+ text: I18n.tr("Amount")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ visible: dmsArgsRow.hasAmountArg
+ }
+
+ DankTextField {
+ id: dmsAmountField
+ Layout.preferredWidth: Math.round(Theme.fontSizeMedium * 5.5)
+ Layout.preferredHeight: root._inputHeight
+ placeholderText: "5"
+ visible: dmsArgsRow.hasAmountArg
+
+ Connections {
+ target: dmsArgsRow
+ function onParsedArgsChanged() {
+ const newText = dmsArgsRow.parsedArgs?.args?.amount || "";
+ if (dmsAmountField.text !== newText)
+ dmsAmountField.text = newText;
+ }
+ }
+
+ Component.onCompleted: {
+ text = dmsArgsRow.parsedArgs?.args?.amount || "";
+ }
+
+ onEditingFinished: {
+ if (!dmsArgsRow.parsedArgs)
+ return;
+ const newArgs = Object.assign({}, dmsArgsRow.parsedArgs.args);
+ newArgs.amount = text || "5";
+ root.updateEdit({
+ "action": Actions.buildDmsAction(dmsArgsRow.parsedArgs.base, newArgs)
+ });
+ }
+ }
+
+ StyledText {
+ text: "%"
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ visible: dmsArgsRow.hasAmountArg
+ }
+
+ StyledText {
+ text: I18n.tr("Device")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.leftMargin: dmsArgsRow.hasAmountArg ? Theme.spacingM : 0
+ Layout.preferredWidth: dmsArgsRow.hasAmountArg ? -1 : root._labelWidth
+ visible: dmsArgsRow.hasDeviceArg
+ }
+
+ DankTextField {
+ id: dmsDeviceField
+ Layout.fillWidth: true
+ Layout.preferredHeight: root._inputHeight
+ placeholderText: I18n.tr("leave empty for default")
+ visible: dmsArgsRow.hasDeviceArg
+
+ Connections {
+ target: dmsArgsRow
+ function onParsedArgsChanged() {
+ const newText = dmsArgsRow.parsedArgs?.args?.device || "";
+ if (dmsDeviceField.text !== newText)
+ dmsDeviceField.text = newText;
+ }
+ }
+
+ Component.onCompleted: {
+ text = dmsArgsRow.parsedArgs?.args?.device || "";
+ }
+
+ onEditingFinished: {
+ if (!dmsArgsRow.parsedArgs)
+ return;
+ const newArgs = Object.assign({}, dmsArgsRow.parsedArgs.args);
+ newArgs.device = text;
+ root.updateEdit({
+ "action": Actions.buildDmsAction(dmsArgsRow.parsedArgs.base, newArgs)
+ });
+ }
+ }
+
+ Item {
+ Layout.fillWidth: true
+ visible: !dmsArgsRow.hasDeviceArg && !dmsArgsRow.hasTabArg
+ }
+
+ StyledText {
+ text: I18n.tr("Tab")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ visible: dmsArgsRow.hasTabArg
+ }
+
+ DankDropdown {
+ id: dmsTabDropdown
+ Layout.fillWidth: true
+ compactMode: true
+ visible: dmsArgsRow.hasTabArg
+ currentValue: {
+ const tab = dmsArgsRow.parsedArgs?.args?.tab || "";
+ switch (tab) {
+ case "media":
+ return I18n.tr("Media");
+ case "wallpaper":
+ return I18n.tr("Wallpaper");
+ case "weather":
+ return I18n.tr("Weather");
+ default:
+ return I18n.tr("Overview");
+ }
+ }
+ options: [I18n.tr("Overview"), I18n.tr("Media"), I18n.tr("Wallpaper"), I18n.tr("Weather")]
+ onValueChanged: value => {
+ if (!dmsArgsRow.parsedArgs)
+ return;
+ const newArgs = Object.assign({}, dmsArgsRow.parsedArgs.args);
+ switch (value) {
+ case I18n.tr("Media"):
+ newArgs.tab = "media";
+ break;
+ case I18n.tr("Wallpaper"):
+ newArgs.tab = "wallpaper";
+ break;
+ case I18n.tr("Weather"):
+ newArgs.tab = "weather";
+ break;
+ default:
+ newArgs.tab = "";
+ break;
+ }
+ root.updateEdit({
+ "action": Actions.buildDmsAction(dmsArgsRow.parsedArgs.base, newArgs)
+ });
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: root._actionType === "compositor" && !root.useCustomCompositor
+
+ StyledText {
+ text: I18n.tr("Action")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ DankDropdown {
+ id: compositorCatDropdown
+ Layout.preferredWidth: Math.round(Theme.fontSizeMedium * 8.5)
+ compactMode: true
+ currentValue: {
+ const base = root.editAction.split(" ")[0];
+ const cats = KeybindsService.getCompositorCategories();
+ for (const cat of cats) {
+ const actions = KeybindsService.getCompositorActions(cat);
+ for (const act of actions) {
+ if (act.id === base)
+ return cat;
+ }
+ }
+ return cats[0] || "Window";
+ }
+ options: KeybindsService.getCompositorCategories()
+ }
+
+ DankDropdown {
+ Layout.fillWidth: true
+ compactMode: true
+ currentValue: KeybindsService.getActionLabel(root.editAction) || I18n.tr("Select...")
+ options: KeybindsService.getCompositorActions(compositorCatDropdown.currentValue).map(a => a.label)
+ enableFuzzySearch: true
+ maxPopupHeight: 300
+ onValueChanged: value => {
+ const actions = KeybindsService.getCompositorActions(compositorCatDropdown.currentValue);
+ for (const act of actions) {
+ if (act.label === value) {
+ root.updateEdit({
+ "action": act.id,
+ "desc": act.label
+ });
+ return;
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.preferredWidth: root._inputHeight
+ Layout.preferredHeight: root._inputHeight
+ radius: Theme.cornerRadius
+ color: Theme.surfaceVariant
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: customToggleArea.pressed ? Theme.surfaceTextHover : (customToggleArea.containsMouse ? Theme.surfaceTextHover : "transparent")
+ }
+
+ DankIcon {
+ name: "edit"
+ size: Theme.iconSizeSmall + 2
+ color: Theme.surfaceVariantText
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: customToggleArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.useCustomCompositor = true
+ }
+ }
+ }
+
+ RowLayout {
+ id: optionsRow
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: root._actionType === "compositor" && !root.useCustomCompositor && Actions.getActionArgConfig(KeybindsService.currentProvider, root.editAction)
+
+ readonly property var argConfig: Actions.getActionArgConfig(KeybindsService.currentProvider, root.editAction)
+ readonly property var parsedArgs: Actions.parseCompositorActionArgs(KeybindsService.currentProvider, root.editAction)
+
+ StyledText {
+ text: I18n.tr("Options")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingS
+
+ DankTextField {
+ id: argValueField
+ Layout.fillWidth: true
+ Layout.preferredHeight: root._inputHeight
+
+ readonly property string _argName: optionsRow.argConfig?.config?.args[0]?.name || "value"
+
+ visible: {
+ const cfg = optionsRow.argConfig;
+ if (!cfg?.config?.args)
+ return false;
+ const firstArg = cfg.config.args[0];
+ return firstArg && (firstArg.type === "text" || firstArg.type === "number");
+ }
+ placeholderText: optionsRow.argConfig?.config?.args[0]?.placeholder || ""
+
+ Connections {
+ target: optionsRow
+ function onParsedArgsChanged() {
+ const argName = argValueField._argName;
+ const newText = optionsRow.parsedArgs?.args[argName] || "";
+ if (argValueField.text !== newText)
+ argValueField.text = newText;
+ }
+ }
+
+ Component.onCompleted: {
+ text = optionsRow.parsedArgs?.args[_argName] || "";
+ }
+
+ onEditingFinished: {
+ const cfg = optionsRow.argConfig;
+ if (!cfg)
+ return;
+ const parsed = optionsRow.parsedArgs;
+ const args = Object.assign({}, parsed?.args || {});
+ args[_argName] = text;
+ root.updateEdit({
+ "action": Actions.buildCompositorAction(KeybindsService.currentProvider, parsed?.base || cfg.base, args)
+ });
+ }
+ }
+
+ RowLayout {
+ visible: {
+ const cfg = optionsRow.argConfig;
+ if (!cfg)
+ return false;
+ switch (cfg.base) {
+ case "move-column-to-workspace":
+ case "move-column-to-workspace-down":
+ case "move-column-to-workspace-up":
+ return true;
+ }
+ return false;
+ }
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ id: focusToggle
+ checked: optionsRow.parsedArgs?.args?.focus !== false
+ onToggled: newChecked => {
+ const cfg = optionsRow.argConfig;
+ if (!cfg)
+ return;
+ const parsed = optionsRow.parsedArgs;
+ const args = {};
+ if (cfg.base === "move-column-to-workspace")
+ args.index = parsed?.args?.index || "";
+ if (!newChecked)
+ args.focus = false;
+ root.updateEdit({
+ "action": Actions.buildCompositorAction(KeybindsService.currentProvider, cfg.base, args)
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Follow focus")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ RowLayout {
+ visible: optionsRow.argConfig?.base?.startsWith("screenshot") ?? false
+ spacing: Theme.spacingM
+
+ RowLayout {
+ visible: optionsRow.argConfig?.base !== "screenshot-window"
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ id: showPointerToggle
+ checked: optionsRow.parsedArgs?.args["show-pointer"] === true
+ onToggled: newChecked => {
+ const parsed = optionsRow.parsedArgs;
+ const base = parsed?.base || "screenshot";
+ const args = Object.assign({}, parsed?.args || {});
+ args["show-pointer"] = newChecked;
+ root.updateEdit({
+ "action": Actions.buildCompositorAction(KeybindsService.currentProvider, base, args)
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Pointer")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ RowLayout {
+ visible: optionsRow.argConfig?.base !== "screenshot"
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ id: writeToDiskToggle
+ checked: optionsRow.parsedArgs?.args["write-to-disk"] === true
+ onToggled: newChecked => {
+ const parsed = optionsRow.parsedArgs;
+ const base = parsed?.base || "screenshot-screen";
+ const args = Object.assign({}, parsed?.args || {});
+ args["write-to-disk"] = newChecked;
+ root.updateEdit({
+ "action": Actions.buildCompositorAction(KeybindsService.currentProvider, base, args)
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Save")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+
+ RowLayout {
+ visible: optionsRow.argConfig?.base === "quit"
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ checked: optionsRow.parsedArgs?.args["skip-confirmation"] === true
+ onToggled: newChecked => {
+ const args = newChecked ? {
+ "skip-confirmation": true
+ } : {};
+ root.updateEdit({
+ "action": Actions.buildCompositorAction(KeybindsService.currentProvider, "quit", args)
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Skip confirmation")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: root._actionType === "compositor" && root.useCustomCompositor
+
+ StyledText {
+ text: I18n.tr("Custom")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ DankTextField {
+ id: customCompositorField
+ Layout.fillWidth: true
+ Layout.preferredHeight: root._inputHeight
+ placeholderText: I18n.tr("e.g., focus-workspace 3, resize-column -10")
+ text: root._actionType === "compositor" ? root.editAction : ""
+ onTextChanged: {
+ if (root._actionType !== "compositor")
+ return;
+ root.updateEdit({
+ "action": text
+ });
+ }
+ }
+
+ Rectangle {
+ Layout.preferredWidth: root._inputHeight
+ Layout.preferredHeight: root._inputHeight
+ radius: Theme.cornerRadius
+ color: Theme.surfaceVariant
+
+ Rectangle {
+ anchors.fill: parent
+ radius: parent.radius
+ color: presetToggleArea.pressed ? Theme.surfaceTextHover : (presetToggleArea.containsMouse ? Theme.surfaceTextHover : "transparent")
+ }
+
+ DankIcon {
+ name: "list"
+ size: Theme.iconSizeSmall + 2
+ color: Theme.surfaceVariantText
+ anchors.centerIn: parent
+ }
+
+ MouseArea {
+ id: presetToggleArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: {
+ root.useCustomCompositor = false;
+ root.updateEdit({
+ "action": "close-window",
+ "desc": "Close Window"
+ });
+ }
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: root._actionType === "spawn"
+
+ StyledText {
+ text: I18n.tr("Command")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ DankTextField {
+ id: spawnTextField
+ Layout.fillWidth: true
+ Layout.preferredHeight: root._inputHeight
+ placeholderText: I18n.tr("e.g., firefox, kitty --title foo")
+ readonly property var _parsed: root._actionType === "spawn" ? Actions.parseSpawnCommand(root.editAction) : null
+ text: _parsed ? (_parsed.command + " " + _parsed.args.join(" ")).trim() : ""
+ onTextChanged: {
+ if (root._actionType !== "spawn")
+ return;
+ const parts = text.trim().split(" ").filter(p => p);
+ const action = parts.length > 0 ? "spawn " + parts.join(" ") : "spawn ";
+ root.updateEdit({
+ "action": action
+ });
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: root._actionType === "shell"
+
+ StyledText {
+ text: I18n.tr("Shell")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ DankTextField {
+ id: shellTextField
+ Layout.fillWidth: true
+ Layout.preferredHeight: root._inputHeight
+ placeholderText: I18n.tr("e.g., notify-send 'Hello' && sleep 1")
+ text: root._actionType === "shell" ? Actions.parseShellCommand(root.editAction) : ""
+ onTextChanged: {
+ if (root._actionType !== "shell")
+ return;
+ var shell = Actions.getShellFromAction(root.editAction);
+ root.updateEdit({
+ "action": Actions.buildShellAction(KeybindsService.currentProvider, text, shell)
+ });
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+
+ StyledText {
+ text: I18n.tr("Title")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ DankTextField {
+ id: titleField
+ Layout.fillWidth: true
+ Layout.preferredHeight: root._inputHeight
+ placeholderText: I18n.tr("Hotkey overlay title (optional)")
+ text: root.editDesc
+ onTextChanged: root.updateEdit({
+ "desc": text
+ })
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: KeybindsService.currentProvider === "hyprland"
+
+ StyledText {
+ text: I18n.tr("Flags")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ Flow {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+
+ RowLayout {
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ checked: root.editFlags.indexOf("e") !== -1
+ onToggled: newChecked => {
+ let flags = root.editFlags.split("").filter(f => f !== "e");
+ if (newChecked)
+ flags.push("e");
+ root.updateEdit({
+ "flags": flags.join("")
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Repeat")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ RowLayout {
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ checked: root.editFlags.indexOf("l") !== -1
+ onToggled: newChecked => {
+ let flags = root.editFlags.split("").filter(f => f !== "l");
+ if (newChecked)
+ flags.push("l");
+ root.updateEdit({
+ "flags": flags.join("")
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Locked")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ RowLayout {
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ checked: root.editFlags.indexOf("r") !== -1
+ onToggled: newChecked => {
+ let flags = root.editFlags.split("").filter(f => f !== "r");
+ if (newChecked)
+ flags.push("r");
+ root.updateEdit({
+ "flags": flags.join("")
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Release")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ RowLayout {
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ checked: root.editFlags.indexOf("o") !== -1
+ onToggled: newChecked => {
+ let flags = root.editFlags.split("").filter(f => f !== "o");
+ if (newChecked)
+ flags.push("o");
+ root.updateEdit({
+ "flags": flags.join("")
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Long press")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: KeybindsService.currentProvider === "niri"
+
+ StyledText {
+ text: I18n.tr("Cooldown")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ DankTextField {
+ id: cooldownField
+ Layout.preferredWidth: Math.round(Theme.fontSizeMedium * 7)
+ Layout.preferredHeight: root._inputHeight
+ placeholderText: "0"
+
+ Connections {
+ target: root
+ function onEditCooldownMsChanged() {
+ const newText = root.editCooldownMs > 0 ? String(root.editCooldownMs) : "";
+ if (cooldownField.text !== newText)
+ cooldownField.text = newText;
+ }
+ }
+
+ Component.onCompleted: {
+ text = root.editCooldownMs > 0 ? String(root.editCooldownMs) : "";
+ }
+
+ onTextChanged: {
+ const val = parseInt(text) || 0;
+ if (val !== root.editCooldownMs)
+ root.updateEdit({
+ "cooldownMs": val
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("ms")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+ visible: KeybindsService.currentProvider === "niri"
+
+ StyledText {
+ text: I18n.tr("Options")
+ font.pixelSize: Theme.fontSizeSmall
+ font.weight: Font.Medium
+ color: Theme.surfaceVariantText
+ Layout.preferredWidth: root._labelWidth
+ }
+
+ Flow {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+
+ RowLayout {
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ checked: root.editRepeat !== false
+ onToggled: newChecked => {
+ root.updateEdit({
+ "repeat": newChecked ? undefined : false
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Repeat")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ RowLayout {
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ checked: root.editAllowWhenLocked
+ onToggled: newChecked => {
+ root.updateEdit({
+ "allowWhenLocked": newChecked
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("When locked")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+
+ RowLayout {
+ spacing: Theme.spacingXS
+
+ DankToggle {
+ checked: root.editAllowInhibiting !== false
+ onToggled: newChecked => {
+ root.updateEdit({
+ "allowInhibiting": newChecked ? undefined : false
+ });
+ }
+ }
+
+ StyledText {
+ text: I18n.tr("Inhibitable")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ Layout.fillWidth: true
+ Layout.preferredHeight: 1
+ color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.1)
+ }
+
+ RowLayout {
+ Layout.fillWidth: true
+ spacing: Theme.spacingM
+
+ DankActionButton {
+ Layout.preferredWidth: root._buttonHeight
+ Layout.preferredHeight: root._buttonHeight
+ circular: false
+ iconName: "delete"
+ iconSize: Theme.iconSize - 4
+ iconColor: Theme.error
+ visible: root.editingKeyIndex >= 0 && root.editingKeyIndex < root.keys.length && root.keys[root.editingKeyIndex].isOverride && !root.isNew
+ onClicked: root.removeBind(root._originalKey)
+ }
+
+ Item {
+ Layout.fillWidth: true
+ }
+
+ StyledText {
+ text: !root.canSave() ? I18n.tr("Set key and action to save") : (root.hasChanges ? I18n.tr("Unsaved changes") : I18n.tr("No changes"))
+ font.pixelSize: Theme.fontSizeSmall
+ color: root.hasChanges ? Theme.surfaceText : Theme.surfaceVariantText
+ visible: !root.isNew
+ }
+
+ DankButton {
+ text: I18n.tr("Cancel")
+ buttonHeight: root._buttonHeight
+ backgroundColor: Theme.surfaceContainer
+ textColor: Theme.surfaceText
+ visible: root.hasChanges || root.isNew
+ onClicked: {
+ if (root.isNew) {
+ root.cancelEdit();
+ } else {
+ root.resetEdits();
+ root.toggleExpand();
+ }
+ }
+ }
+
+ DankButton {
+ text: root.isNew ? I18n.tr("Add") : I18n.tr("Save")
+ buttonHeight: root._buttonHeight
+ enabled: root.canSave()
+ visible: root.hasChanges || root.isNew
+ onClicked: root.doSave()
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/M3WaveProgress.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/M3WaveProgress.qml
new file mode 100644
index 0000000..051e092
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/M3WaveProgress.qml
@@ -0,0 +1,274 @@
+import QtQuick
+import QtQuick.Shapes
+import qs.Common
+
+Item {
+ id: root
+
+ property real value: 0
+ property real actualValue: value
+ property bool showActualPlaybackState: false
+ property real lineWidth: 2
+ property real wavelength: 20
+ property real amp: 1.6
+ property real phase: 0.0
+ property bool isPlaying: false
+ property real currentAmp: 1.6
+ property color trackColor: Qt.rgba(Theme.surfaceVariant.r, Theme.surfaceVariant.g, Theme.surfaceVariant.b, 0.40)
+ property color fillColor: Theme.primary
+ property color playheadColor: Theme.primary
+ property color actualProgressColor: Qt.rgba(Theme.surfaceText.r, Theme.surfaceText.g, Theme.surfaceText.b, 0.45)
+
+ property real dpr: (root.window ? root.window.devicePixelRatio : 1)
+ function snap(v) {
+ return Math.round(v * dpr) / dpr;
+ }
+
+ readonly property real playX: snap(root.width * root.value)
+ readonly property real actualX: snap(root.width * root.actualValue)
+ readonly property real midY: snap(height / 2)
+ readonly property bool previewAhead: root.showActualPlaybackState && root.value > root.actualValue
+ readonly property bool previewBehind: root.showActualPlaybackState && root.value < root.actualValue
+ readonly property real previewGapStartX: Math.min(root.playX, root.actualX)
+ readonly property real previewGapEndX: Math.max(root.playX, root.actualX)
+
+ Behavior on currentAmp {
+ NumberAnimation {
+ duration: 300
+ easing.type: Easing.OutCubic
+ }
+ }
+ onIsPlayingChanged: currentAmp = isPlaying ? amp : 0
+
+ Shape {
+ id: flatTrack
+ anchors.fill: parent
+ antialiasing: true
+ preferredRendererType: Shape.CurveRenderer
+ layer.enabled: true
+
+ ShapePath {
+ strokeColor: root.trackColor
+ strokeWidth: snap(root.lineWidth)
+ capStyle: ShapePath.RoundCap
+ joinStyle: ShapePath.RoundJoin
+ fillColor: "transparent"
+ PathMove {
+ id: flatStart
+ x: Math.min(root.width, snap(root.playX + playhead.width / 2))
+ y: root.midY
+ }
+ PathLine {
+ id: flatEnd
+ x: root.width
+ y: root.midY
+ }
+ }
+ }
+
+ Item {
+ id: waveClip
+ anchors.fill: parent
+ clip: true
+
+ readonly property real startX: snap(root.lineWidth / 2)
+ readonly property real aaBias: (0.25 / root.dpr)
+ readonly property real endX: root.previewAhead ? Math.max(startX, Math.min(root.actualX - aaBias, width)) : Math.max(startX, Math.min(root.playX - startX - aaBias, width))
+ readonly property real gapStartX: root.previewAhead ? Math.max(startX, Math.min(root.actualX + aaBias, width)) : Math.max(startX, Math.min(root.playX + playhead.width / 2, width))
+ readonly property real gapEndX: root.previewAhead ? Math.max(gapStartX, Math.min(root.playX - playhead.width / 2 - aaBias, width)) : Math.max(gapStartX, Math.min(root.actualX - aaBias, width))
+
+ Rectangle {
+ id: mask
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ x: 0
+ width: waveClip.endX
+ color: "transparent"
+ clip: true
+
+ Shape {
+ id: waveShape
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ width: parent.width + 4 * root.wavelength
+ antialiasing: true
+ preferredRendererType: Shape.CurveRenderer
+ x: waveOffsetX
+
+ ShapePath {
+ id: wavePath
+ strokeColor: root.fillColor
+ strokeWidth: snap(root.lineWidth)
+ capStyle: ShapePath.RoundCap
+ joinStyle: ShapePath.RoundJoin
+ fillColor: "transparent"
+ PathSvg {
+ id: waveSvg
+ path: ""
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: actualMask
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ x: waveClip.gapStartX
+ width: Math.max(0, waveClip.gapEndX - waveClip.gapStartX)
+ color: "transparent"
+ clip: true
+ visible: (root.previewBehind || root.previewAhead) && width > 0
+
+ Shape {
+ anchors.top: parent.top
+ anchors.bottom: parent.bottom
+ width: root.width + 4 * root.wavelength
+ antialiasing: true
+ preferredRendererType: Shape.CurveRenderer
+ x: waveOffsetX
+
+ ShapePath {
+ strokeColor: root.actualProgressColor
+ strokeWidth: snap(root.lineWidth)
+ capStyle: ShapePath.RoundCap
+ joinStyle: ShapePath.RoundJoin
+ fillColor: "transparent"
+ PathSvg {
+ path: waveSvg.path
+ }
+ }
+ }
+ }
+
+ Rectangle {
+ id: startCap
+ width: snap(root.lineWidth)
+ height: snap(root.lineWidth)
+ radius: width / 2
+ color: root.fillColor
+ x: waveClip.startX - width / 2
+ y: waveY(waveClip.startX) - height / 2
+ visible: waveClip.endX > waveClip.startX
+ z: 2
+ }
+
+ Rectangle {
+ id: endCap
+ width: snap(root.lineWidth)
+ height: snap(root.lineWidth)
+ radius: width / 2
+ color: root.fillColor
+ x: waveClip.endX - width / 2
+ y: waveY(waveClip.endX) - height / 2
+ visible: waveClip.endX > waveClip.startX
+ z: 2
+ }
+
+ Rectangle {
+ id: actualEndCap
+ width: snap(root.lineWidth)
+ height: snap(root.lineWidth)
+ radius: width / 2
+ color: root.actualProgressColor
+ x: waveClip.gapEndX - width / 2
+ y: waveY(waveClip.gapEndX) - height / 2
+ visible: (root.previewBehind || root.previewAhead) && actualMask.width > 0
+ z: 2
+ }
+
+ Rectangle {
+ id: actualMarker
+ width: 2
+ height: Math.max(root.lineWidth + 4, 10)
+ radius: width / 2
+ color: root.actualProgressColor
+ x: root.actualX - width / 2
+ y: root.midY - height / 2
+ visible: root.showActualPlaybackState
+ z: 2
+ }
+ }
+
+ Rectangle {
+ id: playhead
+ width: 3.5
+ height: Math.max(root.lineWidth + 12, 16)
+ radius: width / 2
+ color: root.playheadColor
+ x: root.playX - width / 2
+ y: root.midY - height / 2
+ z: 3
+ }
+
+ property real k: (2 * Math.PI) / Math.max(1e-6, wavelength)
+ function wrapMod(a, m) {
+ let r = a % m;
+ return r < 0 ? r + m : r;
+ }
+ function waveY(x, amplitude = root.currentAmp, phaseOffset = root.phase) {
+ return root.midY + amplitude * Math.sin((x / root.wavelength) * 2 * Math.PI + phaseOffset);
+ }
+
+ readonly property real waveOffsetX: -wrapMod(phase / k, wavelength)
+
+ FrameAnimation {
+ running: root.visible && (root.isPlaying || root.currentAmp > 0)
+ onTriggered: {
+ if (root.isPlaying)
+ root.phase += 0.03 * frameTime * 60;
+ startCap.y = waveY(waveClip.startX) - startCap.height / 2;
+ endCap.y = waveY(waveClip.endX) - endCap.height / 2;
+ actualEndCap.y = waveY(waveClip.gapEndX) - actualEndCap.height / 2;
+ }
+ }
+
+ function buildStaticWave() {
+ const start = waveClip.startX - 2 * root.wavelength;
+ const end = width + 2 * root.wavelength;
+ if (end <= start) {
+ waveSvg.path = "";
+ return;
+ }
+
+ const kLocal = k;
+ const halfPeriod = root.wavelength / 2;
+ function y0(x) {
+ return root.midY + root.currentAmp * Math.sin(kLocal * x);
+ }
+ function dy0(x) {
+ return root.currentAmp * Math.cos(kLocal * x) * kLocal;
+ }
+
+ let x0 = start;
+ let d = `M ${x0} ${y0(x0)}`;
+ while (x0 < end) {
+ const x1 = Math.min(x0 + halfPeriod, end);
+ const dx = x1 - x0;
+ const yA = y0(x0), yB = y0(x1);
+ const dyA = dy0(x0), dyB = dy0(x1);
+ const c1x = x0 + dx / 3;
+ const c1y = yA + (dyA * dx) / 3;
+ const c2x = x1 - dx / 3;
+ const c2y = yB - (dyB * dx) / 3;
+ d += ` C ${c1x} ${c1y} ${c2x} ${c2y} ${x1} ${yB}`;
+ x0 = x1;
+ }
+ waveSvg.path = d;
+ }
+
+ Component.onCompleted: {
+ currentAmp = isPlaying ? amp : 0;
+ buildStaticWave();
+ }
+ onWidthChanged: {
+ flatEnd.x = width;
+ buildStaticWave();
+ }
+ onHeightChanged: buildStaticWave()
+ onCurrentAmpChanged: buildStaticWave()
+ onWavelengthChanged: {
+ k = (2 * Math.PI) / Math.max(1e-6, wavelength);
+ buildStaticWave();
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/NumericText.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/NumericText.qml
new file mode 100644
index 0000000..fc78c2d
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/NumericText.qml
@@ -0,0 +1,22 @@
+import QtQuick
+import qs.Common
+
+StyledText {
+ id: root
+
+ property string reserveText: ""
+ readonly property real reservedWidth: reserveText !== "" ? Math.max(contentWidth, reserveMetrics.width) : contentWidth
+
+ isMonospace: true
+ wrapMode: Text.NoWrap
+
+ StyledTextMetrics {
+ id: reserveMetrics
+ isMonospace: root.isMonospace
+ font.pixelSize: root.font.pixelSize
+ font.family: root.font.family
+ font.weight: root.font.weight
+ font.hintingPreference: root.font.hintingPreference
+ text: root.reserveText
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/PluginGlobalVar.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/PluginGlobalVar.qml
new file mode 100644
index 0000000..6a95a97
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/PluginGlobalVar.qml
@@ -0,0 +1,30 @@
+import QtQuick
+import qs.Services
+
+Item {
+ id: root
+
+ required property string varName
+ property var defaultValue: undefined
+
+ readonly property var value: {
+ const pid = parent?.pluginId ?? ""
+ if (!pid || !PluginService.globalVars[pid]) {
+ return defaultValue
+ }
+ return PluginService.globalVars[pid][varName] ?? defaultValue
+ }
+
+ function set(newValue) {
+ const pid = parent?.pluginId ?? ""
+ if (pid) {
+ PluginService.setGlobalVar(pid, varName, newValue)
+ } else {
+ console.warn("PluginGlobalVar: Cannot set", varName, "- no pluginId from parent")
+ }
+ }
+
+ visible: false
+ width: 0
+ height: 0
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/ScrollConstants.js b/raveos-hyprland-theme/theme-data/dms/Widgets/ScrollConstants.js
new file mode 100644
index 0000000..22d314f
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/ScrollConstants.js
@@ -0,0 +1,14 @@
+.pragma library
+
+const friction = 0.96;
+const touchpadSpeed = 3.5;
+const mouseWheelSpeed = 60;
+const momentumRetention = 0.92;
+const momentumDeltaFactor = 0.15;
+const maxMomentumVelocity = 2500;
+const minMomentumVelocity = 50;
+const momentumStopThreshold = 5;
+const velocitySampleWindowMs = 100;
+const momentumTimeThreshold = 50;
+const flickDeceleration = 1500;
+const maximumFlickVelocity = 2000;
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/StateLayer.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/StateLayer.qml
new file mode 100644
index 0000000..8969d93
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/StateLayer.qml
@@ -0,0 +1,74 @@
+import QtQuick
+import qs.Common
+
+MouseArea {
+ id: root
+
+ property bool disabled: false
+ property color stateColor: Theme.surfaceText
+ property real cornerRadius: parent && parent.radius !== undefined ? parent.radius : Theme.cornerRadius
+ property var tooltipText: null
+ property string tooltipSide: "bottom"
+ property bool enableRipple: typeof SettingsData !== "undefined" ? (SettingsData.enableRippleEffects ?? true) : true
+
+ readonly property real stateOpacity: disabled ? 0 : pressed ? 0.12 : containsMouse ? 0.08 : 0
+
+ anchors.fill: parent
+ cursorShape: disabled ? undefined : Qt.PointingHandCursor
+ hoverEnabled: true
+
+ onPressed: mouse => {
+ if (!disabled && enableRipple) {
+ rippleLayer.trigger(mouse.x, mouse.y);
+ }
+ }
+
+ Rectangle {
+ id: stateRect
+ anchors.fill: parent
+ radius: root.cornerRadius
+ color: Qt.rgba(stateColor.r, stateColor.g, stateColor.b, stateOpacity)
+
+ Behavior on color {
+ enabled: Theme.currentAnimationSpeed !== SettingsData.AnimationSpeed.None
+ DankColorAnim {
+ duration: Theme.shorterDuration
+ easing.bezierCurve: Theme.expressiveCurves.standardDecel
+ }
+ }
+ }
+
+ DankRipple {
+ id: rippleLayer
+ anchors.fill: parent
+ rippleColor: root.stateColor
+ cornerRadius: root.cornerRadius
+ enableRipple: root.enableRipple
+ }
+
+ Timer {
+ id: hoverDelay
+ interval: 400
+ repeat: false
+ onTriggered: {
+ tooltip.show(root.tooltipText, root, 0, 0, root.tooltipSide);
+ }
+ }
+
+ onEntered: {
+ if (!tooltipText)
+ return;
+ hoverDelay.restart();
+ }
+
+ onExited: {
+ if (!tooltipText)
+ return;
+ hoverDelay.stop();
+ tooltip.hide();
+ }
+
+ DankTooltipV2 {
+ id: tooltip
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/StyledRect.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/StyledRect.qml
new file mode 100644
index 0000000..03ab84b
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/StyledRect.qml
@@ -0,0 +1,29 @@
+import QtQuick
+import qs.Common
+
+Rectangle {
+ color: "transparent"
+ radius: Appearance.rounding.normal
+
+ readonly property var standardAnimation: {
+ "duration": Appearance.anim.durations.normal,
+ "easing.type": Easing.BezierSpline,
+ "easing.bezierCurve": Appearance.anim.curves.standard
+ }
+
+ Behavior on radius {
+ NumberAnimation {
+ duration: standardAnimation.duration
+ easing.type: standardAnimation["easing.type"]
+ easing.bezierCurve: standardAnimation["easing.bezierCurve"]
+ }
+ }
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: standardAnimation.duration
+ easing.type: standardAnimation["easing.type"]
+ easing.bezierCurve: standardAnimation["easing.bezierCurve"]
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/StyledText.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/StyledText.qml
new file mode 100644
index 0000000..3c07ab1
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/StyledText.qml
@@ -0,0 +1,49 @@
+import QtQuick
+import qs.Common
+
+Text {
+ property bool isMonospace: false
+
+ FontLoader {
+ id: interFont
+ source: Qt.resolvedUrl("../assets/fonts/inter/InterVariable.ttf")
+ }
+
+ FontLoader {
+ id: firaCodeFont
+ source: Qt.resolvedUrl("../assets/fonts/nerd-fonts/FiraCodeNerdFont-Regular.ttf")
+ }
+
+ readonly property string resolvedFontFamily: {
+ const requestedFont = isMonospace ? Theme.monoFontFamily : Theme.fontFamily;
+ const defaultFont = isMonospace ? Theme.defaultMonoFontFamily : Theme.defaultFontFamily;
+
+ if (requestedFont === defaultFont) {
+ return isMonospace ? firaCodeFont.name : interFont.name;
+ }
+ return requestedFont;
+ }
+
+ readonly property var standardAnimation: {
+ "duration": Appearance.anim.durations.normal,
+ "easing.type": Easing.BezierSpline,
+ "easing.bezierCurve": Appearance.anim.curves.standard
+ }
+
+ color: Theme.surfaceText
+ font.pixelSize: Appearance.fontSize.normal
+ font.family: resolvedFontFamily
+ font.weight: Theme.fontWeight
+ wrapMode: Text.WordWrap
+ elide: Text.ElideRight
+ verticalAlignment: Text.AlignVCenter
+ //renderType: Text.NativeRendering
+
+ Behavior on opacity {
+ NumberAnimation {
+ duration: standardAnimation.duration
+ easing.type: standardAnimation["easing.type"]
+ easing.bezierCurve: standardAnimation["easing.bezierCurve"]
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/StyledTextMetrics.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/StyledTextMetrics.qml
new file mode 100644
index 0000000..b297385
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/StyledTextMetrics.qml
@@ -0,0 +1,24 @@
+import QtQuick
+import qs.Common
+import qs.Services
+
+TextMetrics {
+ property bool isMonospace: false
+
+ readonly property string resolvedFontFamily: {
+ const requestedFont = isMonospace ? SettingsData.monoFontFamily : SettingsData.fontFamily;
+ const defaultFont = isMonospace ? Theme.defaultMonoFontFamily : Theme.defaultFontFamily;
+
+ if (requestedFont === defaultFont) {
+ const availableFonts = Qt.fontFamilies();
+ if (!availableFonts.includes(requestedFont)) {
+ return isMonospace ? "Monospace" : "DejaVu Sans";
+ }
+ }
+ return requestedFont;
+ }
+
+ font.pixelSize: Appearance.fontSize.normal
+ font.family: resolvedFontFamily
+ font.weight: SettingsData.fontWeight
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/SystemLogo.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/SystemLogo.qml
new file mode 100644
index 0000000..19922e5
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/SystemLogo.qml
@@ -0,0 +1,84 @@
+import QtQuick
+import QtQuick.Effects
+import Quickshell
+import Quickshell.Widgets
+import qs.Common
+import qs.Widgets
+
+Item {
+ id: root
+
+ property string colorOverride: ""
+ property real brightnessOverride: 0.5
+ property real contrastOverride: 1
+
+ readonly property bool hasColorOverride: colorOverride !== ""
+
+ property bool useNerdFont: false
+ property string nerdFontIcon: ""
+
+ IconImage {
+ id: iconImage
+ anchors.fill: parent
+ visible: !root.useNerdFont
+
+ smooth: true
+ asynchronous: true
+ layer.enabled: hasColorOverride
+
+ layer.effect: MultiEffect {
+ colorization: 1
+ colorizationColor: colorOverride
+ brightness: brightnessOverride
+ contrast: contrastOverride
+ }
+ }
+
+ DankNFIcon {
+ id: nfIcon
+ anchors.centerIn: parent
+ visible: root.useNerdFont
+ name: root.nerdFontIcon
+ size: Math.min(root.width, root.height)
+ color: hasColorOverride ? colorOverride : Theme.surfaceText
+ }
+
+ Component.onCompleted: {
+ Proc.runCommand(null, ["sh", "-c", ". /etc/os-release && echo $NAME"], (nameOutput, nameExitCode) => {
+ if (nameExitCode === 0 && nameOutput.trim() === "RaveOS") {
+ iconImage.source = "file://" + Theme.shellDir + "/assets/raveos-logo.svg"
+ return
+ }
+
+ Proc.runCommand(null, ["sh", "-c", ". /etc/os-release && echo $ID"], (output, exitCode) => {
+ if (!root || exitCode !== 0 || !output) return
+ const distroId = output.trim()
+ if (!distroId) return
+
+ const supportedDistroNFs = ["debian", "arch", "archcraft", "fedora", "nixos", "ubuntu", "guix", "gentoo", "endeavouros", "manjaro", "opensuse"]
+ if (supportedDistroNFs.includes(distroId)) {
+ if (!root) return
+ root.useNerdFont = true
+ root.nerdFontIcon = distroId
+ return
+ }
+
+ Proc.runCommand(null, ["sh", "-c", ". /etc/os-release && echo $LOGO"], (logoOutput, logoExitCode) => {
+ if (!root || !iconImage || logoExitCode !== 0 || !logoOutput) return
+ const logo = logoOutput.trim()
+ if (!logo) return
+
+ if (logo === "cachyos") {
+ iconImage.source = "file:///usr/share/icons/cachyos.svg"
+ return
+ }
+ if (logo === "guix-icon") {
+ iconImage.source = "file:///run/current-system/profile/share/icons/hicolor/scalable/apps/guix-icon.svg"
+ return
+ }
+ iconImage.source = Quickshell.iconPath(logo, true)
+ }, 0)
+ }, 0)
+ }, 0)
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/VpnDetailContent.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/VpnDetailContent.qml
new file mode 100644
index 0000000..0ca0e56
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/VpnDetailContent.qml
@@ -0,0 +1,235 @@
+import QtQuick
+import QtQuick.Layouts
+import Quickshell
+import qs.Common
+import qs.Modals.Common
+import qs.Modals.FileBrowser
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ property var parentPopout: null
+ property string expandedUuid: ""
+ property int listHeight: 180
+
+ implicitHeight: 32 + 1 + listHeight + Theme.spacingS * 4 + Theme.spacingM * 2
+ radius: Theme.cornerRadius
+ color: Theme.withAlpha(Theme.surfaceContainerHigh, Theme.popupTransparency)
+
+ FileBrowserSurfaceModal {
+ id: fileBrowser
+ browserTitle: I18n.tr("Import VPN")
+ browserIcon: "vpn_key"
+ browserType: "vpn"
+ fileExtensions: VPNService.getFileFilter()
+ parentPopout: root.parentPopout
+
+ onFileSelected: path => {
+ VPNService.importVpn(path.replace("file://", ""));
+ }
+ }
+
+ ConfirmModal {
+ id: deleteConfirm
+ }
+
+ Column {
+ id: contentColumn
+ anchors.fill: parent
+ anchors.margins: Theme.spacingM
+ spacing: Theme.spacingS
+
+ RowLayout {
+ spacing: Theme.spacingS
+ width: parent.width
+
+ StyledText {
+ text: {
+ if (!DMSNetworkService.connected)
+ return I18n.tr("Active: None");
+ const names = DMSNetworkService.activeNames || [];
+ if (names.length <= 1)
+ return I18n.tr("Active: %1").arg(names[0] || "VPN");
+ return I18n.tr("Active: %1 +%2").arg(names[0]).arg(names.length - 1);
+ }
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ Layout.fillWidth: true
+ }
+
+ Rectangle {
+ height: 28
+ radius: 14
+ color: importArea.containsMouse ? Theme.primaryHoverLight : Theme.surfaceLight
+ width: 90
+ Layout.alignment: Qt.AlignVCenter
+ opacity: VPNService.importing ? 0.5 : 1.0
+
+ Row {
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: VPNService.importing ? "sync" : "add"
+ size: Theme.fontSizeSmall
+ color: Theme.primary
+ }
+
+ StyledText {
+ text: I18n.tr("Import")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.primary
+ font.weight: Font.Medium
+ }
+ }
+
+ MouseArea {
+ id: importArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: VPNService.importing ? Qt.BusyCursor : Qt.PointingHandCursor
+ enabled: !VPNService.importing
+ onClicked: fileBrowser.open()
+ }
+ }
+
+ Rectangle {
+ height: 28
+ radius: 14
+ color: discAllArea.containsMouse ? Theme.errorHover : Theme.surfaceLight
+ visible: DMSNetworkService.connected
+ width: 100
+ Layout.alignment: Qt.AlignVCenter
+ opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
+
+ Row {
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ DankIcon {
+ name: "link_off"
+ size: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ }
+
+ StyledText {
+ text: I18n.tr("Disconnect")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ }
+ }
+
+ MouseArea {
+ id: discAllArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
+ enabled: !DMSNetworkService.isBusy
+ onClicked: DMSNetworkService.disconnectAllActive()
+ }
+ }
+
+ DankActionButton {
+ Layout.alignment: Qt.AlignVCenter
+ iconName: "settings"
+ buttonSize: 28
+ iconSize: 16
+ iconColor: Theme.surfaceVariantText
+ onClicked: {
+ PopoutService.closeControlCenter();
+ PopoutService.openSettingsWithTab("network");
+ }
+ }
+ }
+
+ Rectangle {
+ height: 1
+ width: parent.width
+ color: Qt.rgba(Theme.outline.r, Theme.outline.g, Theme.outline.b, 0.12)
+ }
+
+ Item {
+ width: parent.width
+ height: root.listHeight
+
+ Column {
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+ visible: DMSNetworkService.profiles.length === 0
+
+ DankIcon {
+ name: "vpn_key_off"
+ size: 36
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("No VPN profiles")
+ font.pixelSize: Theme.fontSizeMedium
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+
+ StyledText {
+ text: I18n.tr("Click Import to add a .ovpn or .conf")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ anchors.horizontalCenter: parent.horizontalCenter
+ }
+ }
+
+ DankListView {
+ id: vpnListView
+ anchors.fill: parent
+ visible: DMSNetworkService.profiles.length > 0
+ spacing: 4
+ cacheBuffer: 200
+ clip: true
+
+ model: ScriptModel {
+ values: DMSNetworkService.profiles
+ objectProp: "uuid"
+ }
+
+ delegate: VpnProfileDelegate {
+ required property var modelData
+ width: vpnListView.width
+ profile: modelData
+ isExpanded: root.expandedUuid === modelData.uuid
+ onToggleExpand: {
+ if (root.expandedUuid === modelData.uuid) {
+ root.expandedUuid = "";
+ return;
+ }
+ root.expandedUuid = modelData.uuid;
+ VPNService.getConfig(modelData.uuid);
+ }
+ onDeleteRequested: {
+ deleteConfirm.showWithOptions({
+ "title": I18n.tr("Delete VPN"),
+ "message": I18n.tr("Delete \"%1\"?").arg(modelData.name),
+ "confirmText": I18n.tr("Delete"),
+ "confirmColor": Theme.error,
+ "onConfirm": () => VPNService.deleteVpn(modelData.uuid)
+ });
+ }
+ }
+ }
+ }
+
+ Item {
+ width: 1
+ height: Theme.spacingS
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/VpnProfileDelegate.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/VpnProfileDelegate.qml
new file mode 100644
index 0000000..e859456
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/VpnProfileDelegate.qml
@@ -0,0 +1,287 @@
+import QtQuick
+import qs.Common
+import qs.Services
+import qs.Widgets
+
+Rectangle {
+ id: root
+
+ LayoutMirroring.enabled: I18n.isRtl
+ LayoutMirroring.childrenInherit: true
+
+ required property var profile
+ property bool isExpanded: false
+
+ signal toggleExpand
+ signal deleteRequested
+
+ readonly property bool isActive: DMSNetworkService.activeUuids?.includes(profile?.uuid) ?? false
+ readonly property bool isHovered: rowArea.containsMouse || expandBtn.containsMouse || deleteBtn.containsMouse
+ readonly property var configData: isExpanded ? VPNService.editConfig : null
+ readonly property var configFields: buildConfigFields()
+
+ height: isExpanded ? 46 + expandedContent.height : 46
+ radius: Theme.cornerRadius
+ color: isHovered ? Theme.primaryHoverLight : (isActive ? Theme.primaryPressed : Theme.surfaceLight)
+ border.width: isActive ? 2 : 1
+ border.color: isActive ? Theme.primary : Theme.outlineLight
+ opacity: DMSNetworkService.isBusy ? 0.5 : 1.0
+ clip: true
+
+ function buildConfigFields() {
+ if (!configData)
+ return [];
+ const fields = [];
+ const data = configData.data || {};
+ if (data.remote)
+ fields.push({
+ "key": "server",
+ "label": I18n.tr("Server"),
+ "value": data.remote
+ });
+ if (configData.username || data.username)
+ fields.push({
+ "key": "user",
+ "label": I18n.tr("Username"),
+ "value": configData.username || data.username
+ });
+ if (data.cipher)
+ fields.push({
+ "key": "cipher",
+ "label": I18n.tr("Cipher"),
+ "value": data.cipher
+ });
+ if (data.auth)
+ fields.push({
+ "key": "auth",
+ "label": I18n.tr("Auth"),
+ "value": data.auth
+ });
+ if (data["proto-tcp"] === "yes" || data["proto-tcp"] === "no")
+ fields.push({
+ "key": "proto",
+ "label": I18n.tr("Protocol"),
+ "value": data["proto-tcp"] === "yes" ? "TCP" : "UDP"
+ });
+ if (data["tunnel-mtu"])
+ fields.push({
+ "key": "mtu",
+ "label": I18n.tr("MTU"),
+ "value": data["tunnel-mtu"]
+ });
+ if (data["connection-type"])
+ fields.push({
+ "key": "conntype",
+ "label": I18n.tr("Auth Type"),
+ "value": data["connection-type"]
+ });
+ return fields;
+ }
+
+ Behavior on height {
+ NumberAnimation {
+ duration: 150
+ easing.type: Easing.OutQuad
+ }
+ }
+
+ MouseArea {
+ id: rowArea
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: DMSNetworkService.isBusy ? Qt.BusyCursor : Qt.PointingHandCursor
+ enabled: !DMSNetworkService.isBusy
+ onClicked: DMSNetworkService.toggle(profile.uuid)
+ }
+
+ Column {
+ anchors.fill: parent
+ anchors.margins: Theme.spacingS
+ spacing: Theme.spacingS
+
+ Row {
+ width: parent.width
+ height: 46 - Theme.spacingS * 2
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: isActive ? "vpn_lock" : "vpn_key_off"
+ size: 20
+ color: isActive ? Theme.primary : Theme.surfaceText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ Column {
+ spacing: 1
+ anchors.verticalCenter: parent.verticalCenter
+ width: parent.width - 20 - 28 - 28 - Theme.spacingS * 4
+
+ StyledText {
+ text: profile?.name ?? ""
+ font.pixelSize: Theme.fontSizeMedium
+ color: isActive ? Theme.primary : Theme.surfaceText
+ elide: Text.ElideRight
+ wrapMode: Text.NoWrap
+ width: parent.width
+ horizontalAlignment: Text.AlignLeft
+ }
+
+ StyledText {
+ text: VPNService.getVpnTypeFromProfile(profile)
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceTextMedium
+ wrapMode: Text.NoWrap
+ width: parent.width
+ elide: Text.ElideRight
+ horizontalAlignment: Text.AlignLeft
+ }
+ }
+
+ Item {
+ width: Theme.spacingXS
+ height: 1
+ }
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: 14
+ color: expandBtn.containsMouse ? Theme.surfacePressed : "transparent"
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: isExpanded ? "expand_less" : "expand_more"
+ size: 18
+ color: Theme.surfaceText
+ }
+
+ MouseArea {
+ id: expandBtn
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.toggleExpand()
+ }
+ }
+
+ Rectangle {
+ width: 28
+ height: 28
+ radius: 14
+ color: deleteBtn.containsMouse ? Theme.errorHover : "transparent"
+ anchors.verticalCenter: parent.verticalCenter
+
+ DankIcon {
+ anchors.centerIn: parent
+ name: "delete"
+ size: 18
+ color: deleteBtn.containsMouse ? Theme.error : Theme.surfaceVariantText
+ }
+
+ MouseArea {
+ id: deleteBtn
+ anchors.fill: parent
+ hoverEnabled: true
+ cursorShape: Qt.PointingHandCursor
+ onClicked: root.deleteRequested()
+ }
+ }
+ }
+
+ Column {
+ id: expandedContent
+ width: parent.width
+ spacing: Theme.spacingXS
+ visible: isExpanded
+
+ Rectangle {
+ width: parent.width
+ height: 1
+ color: Theme.outlineLight
+ }
+
+ Item {
+ width: parent.width
+ height: VPNService.configLoading ? 40 : 0
+ visible: VPNService.configLoading
+
+ Row {
+ anchors.centerIn: parent
+ spacing: Theme.spacingS
+
+ DankIcon {
+ name: "sync"
+ size: 16
+ color: Theme.surfaceVariantText
+ }
+
+ StyledText {
+ text: I18n.tr("Loading...")
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ }
+ }
+ }
+
+ Flow {
+ width: parent.width
+ spacing: Theme.spacingXS
+ visible: !VPNService.configLoading && configData
+
+ Repeater {
+ model: configFields
+
+ delegate: Rectangle {
+ required property var modelData
+
+ width: fieldContent.width + Theme.spacingM * 2
+ height: 32
+ radius: Theme.cornerRadius - 2
+ color: Theme.surfaceLight
+ border.width: 1
+ border.color: Theme.outlineLight
+
+ Row {
+ id: fieldContent
+ anchors.centerIn: parent
+ spacing: Theme.spacingXS
+
+ StyledText {
+ text: modelData.label + ":"
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceVariantText
+ anchors.verticalCenter: parent.verticalCenter
+ }
+
+ StyledText {
+ text: modelData.value
+ font.pixelSize: Theme.fontSizeSmall
+ color: Theme.surfaceText
+ font.weight: Font.Medium
+ anchors.verticalCenter: parent.verticalCenter
+ }
+ }
+ }
+ }
+ }
+
+ DankToggle {
+ width: parent.width
+ text: I18n.tr("Autoconnect")
+ checked: configData ? (configData.autoconnect || false) : false
+ visible: !VPNService.configLoading && configData !== null
+ onToggled: checked => {
+ VPNService.updateConfig(profile.uuid, {
+ autoconnect: checked
+ });
+ }
+ }
+
+ Item {
+ width: 1
+ height: Theme.spacingXS
+ }
+ }
+ }
+}
diff --git a/raveos-hyprland-theme/theme-data/dms/Widgets/WindowBlur.qml b/raveos-hyprland-theme/theme-data/dms/Widgets/WindowBlur.qml
new file mode 100644
index 0000000..3b45212
--- /dev/null
+++ b/raveos-hyprland-theme/theme-data/dms/Widgets/WindowBlur.qml
@@ -0,0 +1,65 @@
+import QtQuick
+import qs.Services
+
+Item {
+ id: root
+
+ visible: false
+
+ required property var targetWindow
+ property var blurItem: null
+ property real blurX: 0
+ property real blurY: 0
+ property real blurWidth: 0
+ property real blurHeight: 0
+ property real blurRadius: 0
+
+ property var _region: null
+
+ function _apply() {
+ if (!BlurService.enabled || !targetWindow) {
+ _cleanup();
+ return;
+ }
+
+ if (!_region)
+ _region = BlurService.createBlurRegion(targetWindow);
+
+ if (!_region)
+ return;
+
+ _region.item = Qt.binding(() => root.blurItem);
+ _region.x = Qt.binding(() => root.blurX);
+ _region.y = Qt.binding(() => root.blurY);
+ _region.width = Qt.binding(() => root.blurWidth);
+ _region.height = Qt.binding(() => root.blurHeight);
+ _region.radius = Qt.binding(() => root.blurRadius);
+ }
+
+ function _cleanup() {
+ if (!_region)
+ return;
+ BlurService.destroyBlurRegion(targetWindow, _region);
+ _region = null;
+ }
+
+ Connections {
+ target: BlurService
+ function onEnabledChanged() {
+ root._apply();
+ }
+ }
+
+ Connections {
+ target: root.targetWindow
+ function onVisibleChanged() {
+ if (root.targetWindow && root.targetWindow.visible) {
+ root._region = null;
+ root._apply();
+ }
+ }
+ }
+
+ Component.onCompleted: _apply()
+ Component.onDestruction: _cleanup()
+}