From 3462bcc46ecf74f576ea4397f16492c16bd75b10 Mon Sep 17 00:00:00 2001 From: Nippy Date: Fri, 8 May 2026 19:50:49 +0200 Subject: raveos update --- .../theme-data/dms/Widgets/AppIconRenderer.qml | 130 ++ .../dms/Widgets/AppLauncherGridDelegate.qml | 126 ++ .../dms/Widgets/AppLauncherListDelegate.qml | 140 ++ .../theme-data/dms/Widgets/CachingImage.qml | 102 ++ .../theme-data/dms/Widgets/DankActionButton.qml | 44 + .../theme-data/dms/Widgets/DankAlbumArt.qml | 161 ++ .../theme-data/dms/Widgets/DankBackdrop.qml | 64 + .../theme-data/dms/Widgets/DankButton.qml | 99 ++ .../theme-data/dms/Widgets/DankButtonGroup.qml | 236 +++ .../theme-data/dms/Widgets/DankCircularImage.qml | 171 ++ .../dms/Widgets/DankCollapsibleSection.qml | 130 ++ .../theme-data/dms/Widgets/DankDropdown.qml | 456 +++++ .../theme-data/dms/Widgets/DankFilterChips.qml | 108 ++ .../theme-data/dms/Widgets/DankFlickable.qml | 179 ++ .../theme-data/dms/Widgets/DankGridView.qml | 175 ++ .../theme-data/dms/Widgets/DankIcon.qml | 77 + .../theme-data/dms/Widgets/DankIconPicker.qml | 259 +++ .../theme-data/dms/Widgets/DankListView.qml | 213 +++ .../theme-data/dms/Widgets/DankLocationSearch.qml | 266 +++ .../theme-data/dms/Widgets/DankNFIcon.qml | 164 ++ .../theme-data/dms/Widgets/DankNumberStepper.qml | 67 + .../theme-data/dms/Widgets/DankOSD.qml | 334 ++++ .../theme-data/dms/Widgets/DankPopout.qml | 620 +++++++ .../theme-data/dms/Widgets/DankRipple.qml | 104 ++ .../theme-data/dms/Widgets/DankSVGIcon.qml | 81 + .../theme-data/dms/Widgets/DankScrollbar.qml | 46 + .../theme-data/dms/Widgets/DankSeekbar.qml | 262 +++ .../theme-data/dms/Widgets/DankSlideout.qml | 209 +++ .../theme-data/dms/Widgets/DankSlider.qml | 294 ++++ .../theme-data/dms/Widgets/DankTabBar.qml | 263 +++ .../theme-data/dms/Widgets/DankTextField.qml | 271 +++ .../theme-data/dms/Widgets/DankToggle.qml | 195 +++ .../theme-data/dms/Widgets/DankTooltip.qml | 92 + .../theme-data/dms/Widgets/DankTooltipV2.qml | 154 ++ .../dms/Widgets/FloatingWindowControls.qml | 118 ++ .../theme-data/dms/Widgets/KeybindItem.qml | 1840 ++++++++++++++++++++ .../theme-data/dms/Widgets/M3WaveProgress.qml | 274 +++ .../theme-data/dms/Widgets/NumericText.qml | 22 + .../theme-data/dms/Widgets/PluginGlobalVar.qml | 30 + .../theme-data/dms/Widgets/ScrollConstants.js | 14 + .../theme-data/dms/Widgets/StateLayer.qml | 74 + .../theme-data/dms/Widgets/StyledRect.qml | 29 + .../theme-data/dms/Widgets/StyledText.qml | 49 + .../theme-data/dms/Widgets/StyledTextMetrics.qml | 24 + .../theme-data/dms/Widgets/SystemLogo.qml | 84 + .../theme-data/dms/Widgets/VpnDetailContent.qml | 235 +++ .../theme-data/dms/Widgets/VpnProfileDelegate.qml | 287 +++ .../theme-data/dms/Widgets/WindowBlur.qml | 65 + 48 files changed, 9437 insertions(+) create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/AppIconRenderer.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/AppLauncherGridDelegate.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/AppLauncherListDelegate.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/CachingImage.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankActionButton.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankAlbumArt.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankBackdrop.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankButton.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankButtonGroup.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankCircularImage.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankCollapsibleSection.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankDropdown.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankFilterChips.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankFlickable.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankGridView.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankIcon.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankIconPicker.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankListView.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankLocationSearch.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankNFIcon.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankNumberStepper.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankOSD.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankPopout.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankRipple.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankSVGIcon.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankScrollbar.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankSeekbar.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankSlideout.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankSlider.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankTabBar.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankTextField.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankToggle.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankTooltip.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/DankTooltipV2.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/FloatingWindowControls.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/KeybindItem.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/M3WaveProgress.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/NumericText.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/PluginGlobalVar.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/ScrollConstants.js create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/StateLayer.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/StyledRect.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/StyledText.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/StyledTextMetrics.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/SystemLogo.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/VpnDetailContent.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/VpnProfileDelegate.qml create mode 100644 raveos-hyprland-theme/theme-data/dms/Widgets/WindowBlur.qml (limited to 'raveos-hyprland-theme/theme-data/dms/Widgets') 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 animationEnterCurve: Theme.expressiveCurves.expressiveDefaultSpatial + property list 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() +} -- cgit v1.3