diff --git a/src/components/PopupSlide.qml b/src/components/PopupSlide.qml index 2409da6..8080428 100644 --- a/src/components/PopupSlide.qml +++ b/src/components/PopupSlide.qml @@ -1,86 +1,43 @@ import QtQuick import "../" -// Slide-in/out animation container for all popups. -// -// Universal behavior lives here — animation, hover-to-open, -// self-hover tracking, and close delay. Each popup just wires -// its own Popups.* bool into `open` and optionally into -// `triggerHovered`, then listens for `closeRequested`. -// -// Usage (click-only popup — e.g. ArchMenu): -// PopupSlide { -// id: slide -// edge: "left" -// open: Popups.archMenuOpen -// // content -// } -// -// Usage (hover popup — e.g. AudioPopup): -// PopupSlide { -// id: slide -// edge: "right" -// open: Popups.audioOpen -// hoverEnabled: true -// triggerHovered: Popups.audioTriggerHovered -// onCloseRequested: Popups.audioOpen = false -// // content -// } -// -// Always bind PopupWindow.visible to slide.windowVisible. - Item { id: root - // ── Required ────────────────────────────────────────────────────────────── - property string edge: "left" // "left" | "right" | "top" | "bottom" - property bool open: false // the Popups.*Open bool for this popup - - // ── Hover-to-open (optional) ────────────────────────────────────────────── - property bool hoverEnabled: false - property bool triggerHovered: false // bind to Popups.*TriggerHovered - property bool pinned: false // bind to Popups.*Pinned + property string edge: "left" + property bool open: false + property bool hoverEnabled: false + property bool triggerHovered: false + property bool pinned: false - // ── Universal timing — sourced from Popups singleton ────────────────────── property int slideDuration: Popups.slideDuration property int closeDelay: Popups.hoverCloseDelay property int openDelay: Popups.hoverOpenDelay - // ── Output ──────────────────────────────────────────────────────────────── - // Bind PopupWindow.visible to this property bool windowVisible: false - readonly property bool sliding: xAnim.running || yAnim.running + readonly property bool sliding: shellXAnim.running || shellYAnim.running - // Emitted after closeDelay when hover leaves — popup sets its Popups.* bool - signal closeRequested() + readonly property real innerX: shell.x + readonly property real innerY: shell.y + readonly property real innerWidth: shell.width + readonly property real innerHeight: shell.height - // Emitted when clicked inside the popup to pin it open + signal closeRequested() signal pinRequested() - // ── Internal ────────────────────────────────────────────────────────────── - property bool _selfHovered: false - - // The popup should be visually open when: - readonly property bool _isHoverOpen: hoverEnabled && (triggerHovered || _selfHovered) - readonly property bool _effectiveOpen: open || _hoverOpenActive + default property alias content: contentHost.data + property bool _selfHovered: false property bool _hoverOpenActive: false + readonly property bool _effectiveOpen: open || _hoverOpenActive + readonly property bool _isHoverOpen: hoverEnabled && (triggerHovered || _selfHovered) on_IsHoverOpenChanged: { if (_isHoverOpen) { - if (hoverCloseTimer.running) { - // Recovering from a deadzone cross -> immediate open - hoverCloseTimer.stop() - _hoverOpenActive = true - } else if (!open && !_hoverOpenActive) { - // Initial hover -> delayed open - hoverOpenTimer.restart() - } + hoverCloseTimer.stop() + _hoverOpenActive = true } else { - hoverOpenTimer.stop() - if (_hoverOpenActive) { - hoverCloseTimer.restart() - } + if (_hoverOpenActive) hoverCloseTimer.restart() } } @@ -88,17 +45,12 @@ Item { id: hoverOpenTimer interval: root.openDelay onTriggered: { - if (root._isHoverOpen) { - root._hoverOpenActive = true - } + if (root._isHoverOpen) root._hoverOpenActive = true } } - default property alias content: inner.data - clip: true - // ── State machine ───────────────────────────────────────────────────────── on_EffectiveOpenChanged: { if (_effectiveOpen) { hoverCloseTimer.stop() @@ -109,51 +61,58 @@ Item { } } - // Wait for slide-out to finish before hiding window Timer { id: slideCloseTimer - interval: root.slideDuration + 20 + interval: (Anim.style === "none" ? 0 : root.slideDuration) + 20 onTriggered: root.windowVisible = false } - // Hover leave — wait closeDelay then emit closeRequested Timer { id: hoverCloseTimer interval: root.closeDelay onTriggered: { - // Double-check hover is still gone before requesting close if (!root.triggerHovered && !root._selfHovered) { root._hoverOpenActive = false - if (!root.pinned) { - root.closeRequested() - } + if (!root.pinned) root.closeRequested() } } } - // ── Sliding item ────────────────────────────────────────────────────────── + // Shell: the popup shape wrapper. Uses the global curve but clamps to prevent edge detachment. Item { - id: inner + id: shell width: parent.width height: parent.height - x: root._effectiveOpen ? 0 : (root.edge === "left" ? -width : - root.edge === "right" ? width : 0) - - y: root._effectiveOpen ? 0 : (root.edge === "top" ? -height : - root.edge === "bottom" ? height : 0) + property real targetX: root._effectiveOpen ? 0 : (root.edge === "left" ? -width : (root.edge === "right" ? width : 0)) + property real targetY: root._effectiveOpen ? 0 : (root.edge === "top" ? -height : (root.edge === "bottom" ? height : 0)) - Behavior on x { NumberAnimation { id: xAnim; duration: root.slideDuration; easing.type: Anim.outCubic} } - Behavior on y { NumberAnimation { id: yAnim; duration: root.slideDuration; easing.type: Anim.outCubic} } + property real animX: targetX + property real animY: targetY - // Self-hover tracking — automatically available to all popups - HoverHandler { - onHoveredChanged: root._selfHovered = hovered + Behavior on animX { + enabled: Anim.style !== "none" + NumberAnimation { id: shellXAnim; duration: root.slideDuration; easing.type: Anim.globalCurve } + } + Behavior on animY { + enabled: Anim.style !== "none" + NumberAnimation { id: shellYAnim; duration: root.slideDuration; easing.type: Anim.globalCurve } } - // Interaction pinning — passively detect clicks inside to pin - TapHandler { - onTapped: root.pinRequested() + x: root.edge === "left" ? Math.min(0, animX) + : root.edge === "right" ? Math.max(0, animX) + : animX + + y: root.edge === "top" ? Math.min(0, animY) + : root.edge === "bottom" ? Math.max(0, animY) + : animY + + HoverHandler { onHoveredChanged: root._selfHovered = hovered } + TapHandler { onTapped: root.pinRequested() } + + Item { + id: contentHost + anchors.fill: parent } } } diff --git a/src/popups/ArchMenu.qml b/src/popups/ArchMenu.qml index e95f0bd..733b298 100644 --- a/src/popups/ArchMenu.qml +++ b/src/popups/ArchMenu.qml @@ -7,124 +7,115 @@ import "../services" import "../components" import "../" -PopupWindow { - id: root - - required property var anchorWindow - - readonly property real localScale: Math.max(0.75, Math.min(1.5, (screen ? screen.height : 1080.0) / 1080.0)) - - readonly property int fw: Math.round(Theme.cornerRadius * root.localScale) - readonly property int fh: Math.round(Theme.cornerRadius * root.localScale) - - readonly property var pageHeights: ({ - "power": Math.round(270 * root.localScale), - "performance": Math.round(190 * root.localScale), - "stats": Math.round(250 * root.localScale) - }) - readonly property var pageWidths: ({ - "power": Math.round(220 * root.localScale), - "performance": Math.round(260 * root.localScale), - "stats": Math.round(390 * root.localScale) - }) - - readonly property int contentWidth: pageWidths[page] ?? Math.round(220 * root.localScale) - readonly property int contentHeight: pageHeights[page] ?? Math.round(220 * root.localScale) - - property string page: "power" - - color: "transparent" - visible: slide.windowVisible - mask: Region { item: maskProxy } - - Region { - id: archBlurReg - item: bg - } - - BackgroundEffect.blurRegion: PrefsService.bgBlur ? archBlurReg : null - - implicitWidth: (pageWidths["stats"] ?? Math.round(220 * root.localScale)) + fw - implicitHeight: (pageHeights["stats"] ?? Math.round(220 * root.localScale)) + fh * 2 - - anchor.window: anchorWindow - anchor.gravity: Edges.Right - anchor.rect: Qt.rect( - 0, - anchorWindow.height / 2, - anchorWindow.width, - anchorWindow.height - ) - - Item { - id: maskProxy - x: 0 - y: (root.implicitHeight - sizer.height) / 2-root.fh - width: sizer.width - height: sizer.height - } - - PopupSlide { - id: slide - anchors.fill: parent - edge: "left" - hoverEnabled: Popups.archMenuAllowHover - triggerHovered: Popups.archMenuTriggerHovered - pinned: Popups.archMenuPinned - open: Popups.archMenuOpen - onCloseRequested: Popups.archMenuOpen = false - onPinRequested: { - Popups.archMenuOpen = true - Popups.archMenuPinned = true - } - - Item { - id: sizer - anchors.left: parent.left - anchors.verticalCenter: parent.verticalCenter - clip: true - - width: root.contentWidth + root.fw - height: root.contentHeight + root.fh * 2 - - Behavior on width { NumberAnimation { duration: Anim.transition; easing.type: Anim.inOutCubic} } - Behavior on height { NumberAnimation { duration: Anim.transition; easing.type: Anim.inOutCubic} } - - PopupShape { - id: bg - anchors.fill: parent - attachedEdge: "left" - color: Theme.background - radius: Math.round(Theme.cornerRadius * root.localScale) - flareWidth: root.fw - flareHeight: root.fh - } - - Item { - anchors { - fill: parent - leftMargin: root.fw - Math.round(4 * root.localScale) - rightMargin: Math.round(8 * root.localScale) - topMargin: root.fh + Math.round(6 * root.localScale) - bottomMargin: root.fh + Math.round(6 * root.localScale) - } - //── Page content ────────────────────────────────────────── - Item { - width: parent.width - height: parent.height - clip: true - - PopupPage { - anchors.fill: parent - visible: root.page === "power" - - PowerMenu { - localScale: root.localScale - width: parent.width - } - } - } - } - } - } +PanelWindow { + id: root + + readonly property real localScale: Math.max(0.75, Math.min(1.5, (screen ? screen.height : 1080.0) / 1080.0)) + + readonly property int fw: Math.round(Theme.cornerRadius * root.localScale) + readonly property int fh: Math.round(Theme.cornerRadius * root.localScale) + + readonly property var pageHeights: ({ + "power": Math.round(270 * root.localScale), + "performance": Math.round(190 * root.localScale), + "stats": Math.round(250 * root.localScale) + }) + readonly property var pageWidths: ({ + "power": Math.round(220 * root.localScale), + "performance": Math.round(260 * root.localScale), + "stats": Math.round(390 * root.localScale) + }) + + readonly property int contentWidth: pageWidths[page] ?? Math.round(220 * root.localScale) + readonly property int contentHeight: pageHeights[page] ?? Math.round(220 * root.localScale) + + // Constant size for the panel window + readonly property int targetWidth: contentWidth + fw + readonly property int targetHeight: contentHeight + fh * 2 + + property string page: "power" + + anchors.left: true + anchors.top: true + anchors.bottom: true + margins.top: screen ? Math.round((screen.height - targetHeight) / 2) : 0 + margins.bottom: screen ? Math.round((screen.height - targetHeight) / 2) : 0 + margins.left: Math.round(Theme.borderWidth * root.localScale) + + implicitWidth: targetWidth + implicitHeight: targetHeight + + exclusionMode: ExclusionMode.Ignore + color: "transparent" + WlrLayershell.layer: WlrLayer.Overlay + + visible: slide.windowVisible + mask: Region { item: maskProxy } + + Region { + id: archBlurReg + item: slide + } + + BackgroundEffect.blurRegion: PrefsService.bgBlur ? archBlurReg : null + + Item { + id: maskProxy + x: slide.x + slide.innerX + y: slide.y + slide.innerY + width: slide.innerWidth + height: slide.innerHeight + } + + PopupSlide { + id: slide + anchors.fill: parent + edge: "left" + hoverEnabled: Popups.archMenuAllowHover + triggerHovered: Popups.archMenuTriggerHovered + pinned: Popups.archMenuPinned + open: Popups.archMenuOpen + onCloseRequested: Popups.archMenuOpen = false + onPinRequested: { + Popups.archMenuOpen = true + Popups.archMenuPinned = true + } + + PopupShape { + id: bg + anchors.fill: parent + attachedEdge: "left" + color: Theme.background + radius: Math.round(Theme.cornerRadius * root.localScale) + flareWidth: root.fw + flareHeight: root.fh + } + + Item { + anchors { + fill: parent + leftMargin: root.fw - Math.round(4 * root.localScale) + rightMargin: Math.round(8 * root.localScale) + topMargin: root.fh + Math.round(6 * root.localScale) + bottomMargin: root.fh + Math.round(6 * root.localScale) + } + + //── Page content ────────────────────────────────────────── + Item { + width: root.contentWidth - Math.round(12 * root.localScale) + height: root.contentHeight - Math.round(12 * root.localScale) + clip: true + + PopupPage { + anchors.fill: parent + visible: root.page === "power" + + PowerMenu { + localScale: root.localScale + width: parent.width + } + } + } + } + } } diff --git a/src/popups/AudioPopup.qml b/src/popups/AudioPopup.qml index ea0336b..4fb79b9 100644 --- a/src/popups/AudioPopup.qml +++ b/src/popups/AudioPopup.qml @@ -7,134 +7,128 @@ import "../components" import "../services" import "../" -PopupWindow { - id: root - - required property var anchorWindow - - readonly property real localScale: Math.max(0.75, Math.min(1.5, (screen ? screen.height : 1080.0) / 1080.0)) - - readonly property int fw: Math.round(Theme.cornerRadius * root.localScale) - readonly property int fh: Math.round(Theme.cornerRadius * root.localScale) - - readonly property var pageWidths: ({ - "output": Math.round(200 * root.localScale), - "input": Math.round(200 * root.localScale), - "mixer": Math.round(300 * root.localScale) - }) - - readonly property int popupHeight: Math.round(340 * root.localScale) - - readonly property int maxWidth: Math.round(300 * root.localScale) - - color: "transparent" - visible: slide.windowVisible - mask: Region { item: maskProxy } - - Region { - id: audioBlurReg - item: sizer - } - - BackgroundEffect.blurRegion: PrefsService.bgBlur ? audioBlurReg : null - - anchor.window: anchorWindow - anchor.rect: Qt.rect( - Math.round(Theme.cornerRadius * root.localScale), - anchorWindow ? anchorWindow.height/2 : 0, - 0, - popupHeight - ) - anchor.gravity: Edges.Left - - Item { - id: maskProxy - x: root.maxWidth - sizer.width - y: ((root.popupHeight - sizer.height) / 2) -root.fh - width: sizer.width - height: sizer.height - } - - implicitWidth: maxWidth - implicitHeight: popupHeight - - PopupSlide { - id: slide - anchors.fill: parent - edge: "right" - open: Popups.audioOpen - hoverEnabled: Popups.audioAllowHover - triggerHovered: Popups.audioTriggerHovered - pinned: Popups.audioPinned - onCloseRequested: Popups.audioOpen = false - onPinRequested: { - Popups.audioOpen = true - Popups.audioPinned = true - } - - Connections { - target: Popups - function onAudioOpenChanged() { - if (!Popups.audioOpen) audioResetTimer.restart() +PanelWindow { + id: root + + required property var anchorWindow + screen: anchorWindow ? anchorWindow.screen : undefined + + readonly property real localScale: Math.max(0.75, Math.min(1.5, (screen ? screen.height : 1080.0) / 1080.0)) + + readonly property int fw: Math.round(Theme.cornerRadius * root.localScale) + readonly property int fh: Math.round(Theme.cornerRadius * root.localScale) + + readonly property var pageWidths: ({ + "output": Math.round(200 * root.localScale), + "input": Math.round(200 * root.localScale), + "mixer": Math.round(300 * root.localScale) + }) + + readonly property int popupHeight: Math.round(340 * root.localScale) + readonly property int maxWidth: Math.round(300 * root.localScale) + + // Animate target width slightly for tab changes + property real targetWidth: (pageWidths[Popups.audioPage] ?? maxWidth) + Behavior on targetWidth { NumberAnimation { duration: Anim.transition; easing.type: Anim.globalCurve } } + + anchors.right: true + anchors.top: true + margins.top: screen ? Math.round((screen.height - popupHeight) / 2) : 0 + margins.right: Math.round(Theme.borderWidth * root.localScale) + + implicitWidth: root.maxWidth + implicitHeight: root.popupHeight + + exclusionMode: ExclusionMode.Ignore + color: "transparent" + WlrLayershell.layer: WlrLayer.Overlay + visible: slide.windowVisible + mask: Region { item: maskProxy } + + Region { + id: audioBlurReg + item: slide + } + + BackgroundEffect.blurRegion: PrefsService.bgBlur ? audioBlurReg : null + + Item { + id: maskProxy + x: slide.x + slide.innerX + y: slide.y + slide.innerY + width: slide.innerWidth + height: slide.innerHeight + } + + PopupSlide { + id: slide + anchors.right: parent.right + anchors.top: parent.top + anchors.bottom: parent.bottom + width: root.targetWidth + + edge: "right" + open: Popups.audioOpen + hoverEnabled: Popups.audioAllowHover + triggerHovered: Popups.audioTriggerHovered + pinned: Popups.audioPinned + onCloseRequested: Popups.audioOpen = false + onPinRequested: { + Popups.audioOpen = true + Popups.audioPinned = true + } + + Connections { + target: Popups + function onAudioOpenChanged() { + if (!Popups.audioOpen) audioResetTimer.restart() else audioControl.page = Popups.audioPage - } - + } function onAudioPageChanged() { audioControl.page = Popups.audioPage } - } - - Connections { - target: slide - function onWindowVisibleChanged() { - if (slide.windowVisible && !Popups.audioOpen) { - let opt = PrefsService.defaultAudioTab - if (opt === "Input") Popups.audioPage = "input" - else if (opt === "Mixers") Popups.audioPage = "mixer" - else Popups.audioPage = "output" - } - } - } - - Timer { - id: audioResetTimer - interval: Anim.transition + 20 - onTriggered: audioControl.reset() - } - - Item { - id: sizer - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - clip: true - - width: (root.pageWidths[audioControl.page] ?? root.maxWidth) - height: root.popupHeight - - Behavior on width { id: sizerWidthAnim; NumberAnimation { duration: Anim.transition; easing.type: Anim.inOutCubic} } - - PopupShape { - id: bg - anchors.fill: parent - attachedEdge: "right" - color: Theme.background - radius: Math.round(Theme.cornerRadius * root.localScale) - flareWidth: root.fw - flareHeight: root.fh - } - - AudioControl { - id: audioControl - localScale: root.localScale - fullyOpen: Popups.audioOpen && !slide.sliding && Math.round(sizer.width) === Math.round(root.pageWidths[audioControl.page] ?? root.maxWidth) - anchors { - fill: parent - topMargin: root.fh + Math.round(6 * root.localScale) - bottomMargin: root.fh + Math.round(6 * root.localScale) - leftMargin: Math.round(10 * root.localScale) - rightMargin: root.fw - Math.round(4 * root.localScale) - } - } - } - } + } + Connections { + target: slide + function onWindowVisibleChanged() { + if (slide.windowVisible && !Popups.audioOpen) { + let opt = PrefsService.defaultAudioTab + if (opt === "Input") Popups.audioPage = "input" + else if (opt === "Mixers") Popups.audioPage = "mixer" + else Popups.audioPage = "output" + } + } + } + + Timer { + id: audioResetTimer + interval: Anim.transition + 20 + onTriggered: audioControl.reset() + } + + PopupShape { + id: bg + anchors.fill: parent + attachedEdge: "right" + color: Theme.background + radius: Math.round(Theme.cornerRadius * root.localScale) + flareWidth: root.fw + flareHeight: root.fh + } + + AudioControl { + id: audioControl + localScale: root.localScale + fullyOpen: Popups.audioOpen && !slide.sliding + + width: root.targetWidth - Math.round(14 * root.localScale) - root.fw + height: root.popupHeight - root.fh * 2 - Math.round(12 * root.localScale) + + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + rightMargin: root.fw - Math.round(4 * root.localScale) + } + } + } } diff --git a/src/popups/PopupLayer.qml b/src/popups/PopupLayer.qml index 822ee69..d0e3d74 100644 --- a/src/popups/PopupLayer.qml +++ b/src/popups/PopupLayer.qml @@ -25,7 +25,7 @@ Item { // Left border → center ArchMenu { - anchorWindow: root.leftBorder + screen: root.leftBorder ? root.leftBorder.screen : undefined } // Bottom border → slides up diff --git a/src/popups/QuickControl.qml b/src/popups/QuickControl.qml index 9af09c4..8d32e40 100644 --- a/src/popups/QuickControl.qml +++ b/src/popups/QuickControl.qml @@ -8,59 +8,55 @@ import "../components" import "../services" import "../" -PopupWindow { +PanelWindow { id: root required property var anchorWindow + screen: anchorWindow ? anchorWindow.screen : undefined - // ── Context-Aware Scaling ───────────────────────────────────────────────── readonly property real localScale: Math.max(0.75, Math.min(1.5, (screen ? screen.height : 1080.0) / 1080.0)) - // ── Config ──────────────────────────────────────────────────────────────── readonly property int fw: Math.round(Theme.cornerRadius * root.localScale) readonly property int fh: Math.round(Theme.cornerRadius * root.localScale) readonly property int popupHeight: Math.round(340 * root.localScale) - readonly property int popupWidth: Math.round(180 * root.localScale) // Thinner than the 300px AudioPopup + readonly property int popupWidth: Math.round(180 * root.localScale) + anchors.right: true + anchors.top: true + anchors.bottom: true + margins.top: screen ? Math.round((screen.height - popupHeight) / 2) : 0 + margins.bottom: screen ? Math.round((screen.height - popupHeight) / 2) : 0 + margins.right: Math.round(Theme.borderWidth * root.localScale) + + implicitWidth: popupWidth + implicitHeight: popupHeight + + exclusionMode: ExclusionMode.Ignore color: "transparent" + WlrLayershell.layer: WlrLayer.Overlay visible: slide.windowVisible mask: Region { item: maskProxy } Region { id: qcBlurReg - item: sizer + item: slide } BackgroundEffect.blurRegion: PrefsService.bgBlur ? qcBlurReg : null - // ── Position: Right Center ──────────────────────────────────────────────── - anchor.window: anchorWindow - anchor.rect: Qt.rect( - anchorWindow ? anchorWindow.width - root.fw : 0, - anchorWindow ? anchorWindow.height/2 : 0, - 0, - 0 - ) - anchor.gravity: Edges.Right - Item { id: maskProxy - x: root.popupWidth - sizer.width - y: ((root.popupHeight - sizer.height) / 2) - root.fh - width: sizer.width - height: sizer.height + x: slide.x + slide.innerX + y: slide.y + slide.innerY + width: slide.innerWidth + height: slide.innerHeight } - implicitWidth: popupWidth - implicitHeight: popupHeight - - // ── Audio State ─────────────────────────────────────────────────────────── readonly property var sink: Pipewire.defaultAudioSink PwObjectTracker { objects: root.sink ? [root.sink] : [] } - // ── Brightness State ────────────────────────────────────────────────────── property real _bVal: 0.72 property int _bMax: 100 property bool _bBusy: false @@ -109,10 +105,10 @@ PopupWindow { bDebounce.restart() } - // ── Layout ──────────────────────────────────────────────────────────────── PopupSlide { id: slide - anchors.fill: parent + anchors.fill: parent + edge: "right" open: Popups.quickOpen hoverEnabled: !Popups.audioAllowHover && Popups.quickAllowHover @@ -124,79 +120,61 @@ PopupWindow { Popups.quickPinned = true } - Item { - id: sizer - anchors.right: parent.right - anchors.verticalCenter: parent.verticalCenter - clip: true - - width: root.popupWidth - height: root.popupHeight - - Behavior on width { NumberAnimation { duration: Anim.transition; easing.type: Anim.inOutCubic} } - - PopupShape { - id: bg - anchors.fill: parent - attachedEdge: "right" - color: Theme.background - radius: Theme.cornerRadius - flareWidth: root.fw - flareHeight: root.fh - } + PopupShape { + id: bg + anchors.fill: parent + attachedEdge: "right" + color: Theme.background + radius: Theme.cornerRadius + flareWidth: root.fw + flareHeight: root.fh + } - // ── Sliders Layout ──────────────────────────────────────────────── - Row { - anchors { - fill: parent - topMargin: root.fh + 30 - leftMargin: 8 - rightMargin: 8 + Row { + width: root.popupWidth - Math.round(16 * root.localScale) - root.fw + height: root.popupHeight - root.fh * 2 - 60 + + anchors { + right: parent.right + verticalCenter: parent.verticalCenter + rightMargin: root.fw - Math.round(4 * root.localScale) + } + spacing: 8 + + ChannelColumn { + icon: { + if (!root.sink?.ready) return "󰕾" + if (root.sink.audio.muted) return "󰖁" + if (root.sink.audio.volume > 0.6) return "󰕾" + if (root.sink.audio.volume > 0.2) return "󰖀" + return "󰕿" } - spacing: 8 - anchors.horizontalCenter: parent.horizontalCenter - - // Audio Slider - ChannelColumn { - icon: { - if (!root.sink?.ready) return "󰕾" - if (root.sink.audio.muted) return "󰖁" - if (root.sink.audio.volume > 0.6) return "󰕾" - if (root.sink.audio.volume > 0.2) return "󰖀" - return "󰕿" - } - value: root.sink?.ready ? root.sink.audio.volume : 0 - muted: root.sink?.audio.muted ?? false - active: root.sink?.ready ?? false - - onVolumeChanged: function(v) { - if (root.sink?.ready) root.sink.audio.volume = v - } - onMuteToggled: { - if (root.sink?.ready) root.sink.audio.muted = !root.sink.audio.muted - } + value: root.sink?.ready ? root.sink.audio.volume : 0 + muted: root.sink?.audio.muted ?? false + active: root.sink?.ready ?? false + onVolumeChanged: function(v) { + if (root.sink?.ready) root.sink.audio.volume = v } + onMuteToggled: { + if (root.sink?.ready) root.sink.audio.muted = !root.sink.audio.muted + } + } - // Brightness Slider - ChannelColumn { - localScale: root.localScale - icon: "󰃠" - value: root._bVal - muted: false - active: true - - onVolumeChanged: function(v) { - root.setBrightness(v) - } + ChannelColumn { + localScale: root.localScale + icon: "󰃠" + value: root._bVal + muted: false + active: true + onVolumeChanged: function(v) { + root.setBrightness(v) } } } } - // ── Reusable ChannelColumn Component ────────────────────────────────────── component ChannelColumn: Item { id: col - property real localScale: 1.0 property string label: "" property string icon: "" @@ -241,7 +219,6 @@ PopupWindow { radius: width / 2 color: Qt.rgba(Theme.text.r,Theme.text.g,Theme.text.b,0.08) - // Fill bar Rectangle { anchors { bottom: parent.bottom; left: parent.left; right: parent.right } height: Math.max(radius * 2, parent.height * col.value) @@ -251,7 +228,6 @@ PopupWindow { Behavior on height { NumberAnimation { duration: Anim.superFast; easing.type: Anim.outCubic} } } - // Thumb Rectangle { id: thumb anchors.horizontalCenter: parent.horizontalCenter @@ -266,7 +242,6 @@ PopupWindow { Behavior on color { ColorAnimation { duration: Anim.mediumFast} } } - // Drag to change value MouseArea { anchors.fill: parent cursorShape: Qt.SizeVerCursor @@ -278,7 +253,6 @@ PopupWindow { onPositionChanged: if (pressed) col.volumeChanged(calc(mouseY)) } - // Scroll wheel to change value WheelHandler { acceptedDevices: PointerDevice.Mouse | PointerDevice.TouchPad onWheel: function(event) { @@ -290,7 +264,6 @@ PopupWindow { } } - // Icon & Mute Toggle Rectangle { anchors.horizontalCenter: parent.horizontalCenter width: col.barW + Math.round(16 * root.localScale) @@ -318,7 +291,6 @@ PopupWindow { MouseArea { anchors.fill: parent; onClicked: col.muteToggled()} } - // Label Text { anchors.horizontalCenter: parent.horizontalCenter text: col.label diff --git a/src/popups/ScreenRecOptionsPopup.qml b/src/popups/ScreenRecOptionsPopup.qml index 4a5edb1..e3e6d21 100644 --- a/src/popups/ScreenRecOptionsPopup.qml +++ b/src/popups/ScreenRecOptionsPopup.qml @@ -4,21 +4,18 @@ import Quickshell.Wayland import "../services" import "../" -// ScreenRecOptionsPopup — minimal dropdown under the center notch. -// -// No checkboxes, no radio dots. Selection shown by background highlight only. -// Window sized exactly to content — no mask, no dead space. - PopupWindow { id: root required property var anchorWindow + readonly property real localScale: Math.max(0.75, Math.min(1.5, (screen ? screen.height : 1080.0) / 1080.0)) readonly property int _padH: Math.round(5 * root.localScale) readonly property int _padV: Math.round(5 * root.localScale) + readonly property int _optionWidth: Math.round(112 * root.localScale) - implicitWidth: optCol.implicitWidth + _padH * 2 + implicitWidth: _optionWidth + _padH * 2 implicitHeight: optCol.implicitHeight + _padV * 2 anchor.window: root.anchorWindow @@ -61,9 +58,9 @@ PopupWindow { id: optCol x: _padH y: _padV + width: _optionWidth spacing: Math.round(2 * root.localScale) - // Capture — radio (one active at a time) Repeater { model: ScreenRecService.openStrip === "capture" ? ["screen", "window", "region"] : [] @@ -77,7 +74,6 @@ PopupWindow { } } - // Audio — checkboxes (independent) Repeater { model: ScreenRecService.openStrip === "audio" ? ["mic", "system", "none"] : [] @@ -120,7 +116,7 @@ PopupWindow { signal clicked() - implicitWidth: Math.round((8 + 14 + 6 + 12) * root.localScale) + _lbl.implicitWidth + implicitWidth: root._optionWidth implicitHeight: Math.round(26 * root.localScale) Rectangle { @@ -133,7 +129,7 @@ PopupWindow { } Row { - anchors { left: parent.left; leftMargin: Math.round(8 * root.localScale); verticalCenter: parent.verticalCenter } + anchors.centerIn: parent spacing: Math.round(6 * root.localScale) Text { diff --git a/src/services/config_tab/VisualsBehaviorPage.qml b/src/services/config_tab/VisualsBehaviorPage.qml index 9f1c13d..e31d1e1 100644 --- a/src/services/config_tab/VisualsBehaviorPage.qml +++ b/src/services/config_tab/VisualsBehaviorPage.qml @@ -71,9 +71,6 @@ Item { id: sizingGroup localScale: root.localScale title: "Sizing & Borders" - opacity: 0.3 - enabled: false - SettingsSlider { localScale: root.localScale text: "Border Width" @@ -104,18 +101,6 @@ Item { valueSuffix: "px" } } - - Text { - anchors.centerIn: sizingGroup - anchors.verticalCenterOffset: Math.round(12 * root.localScale) - text: "WORK IN PROGRESS" - color: Theme.text - font.pixelSize: Math.round(20 * root.localScale) - font.weight: Font.Bold - font.letterSpacing: 2 - style: Text.Outline - styleColor: Theme.background - } } // Popup Behavior diff --git a/src/shapes/SeamlessBarShape.qml b/src/shapes/SeamlessBarShape.qml index c1725f7..b46a0c4 100644 --- a/src/shapes/SeamlessBarShape.qml +++ b/src/shapes/SeamlessBarShape.qml @@ -41,6 +41,15 @@ Canvas { var centerEnd = (w / 2) + (centerW / 2) var rightStart = w - rightW + var maxR = (h - b) / 2; + var rSafe = Math.min(r, maxR); + var gap1 = centerStart - leftW; + if (gap1 > 0) rSafe = Math.min(rSafe, gap1 / 2); + var gap2 = rightStart - centerEnd; + if (gap2 > 0) rSafe = Math.min(rSafe, gap2 / 2); + rSafe = Math.max(0, rSafe); + r = rSafe; + ctx.beginPath(); ctx.fillStyle = root.color; diff --git a/src/theme/Colors.qml b/src/theme/Colors.qml index 102882a..e804dad 100644 --- a/src/theme/Colors.qml +++ b/src/theme/Colors.qml @@ -8,8 +8,8 @@ QtObject { // ── Color loader — watches matugen output and updates live ──────────────── // Use a unique ID to avoid namespace collision with the 'Colors' singleton - property var _loader: ColorLoader { - id: internalLoader + property var _loader: ColorLoader { + id: internalLoader overrideMode: PrefsService.dynamicThemeOverride } @@ -28,5 +28,5 @@ QtObject { property color wsOccupied: Qt.rgba(text.r, text.g, text.b, 0.7) property color wsEmpty: Qt.rgba(text.r, text.g, text.b, 0.25) property color wsOverlay: Qt.rgba(background.r, background.g, background.b, 0.85) - property color wsUrgent: active //"#fa6b94" + property color wsUrgent: "#fa6b94" //active } diff --git a/src/windows/Border.qml b/src/windows/Border.qml index 447f19b..24ef0a7 100644 --- a/src/windows/Border.qml +++ b/src/windows/Border.qml @@ -22,8 +22,8 @@ PanelWindow { property int radius: Math.round(Theme.cornerRadius * root.localScale) property color fillColor: Theme.background - implicitWidth: (edge === "left" || edge === "right") ? radius : 0 - implicitHeight: (edge === "bottom") ? radius : 0 + implicitWidth: (edge === "left" || edge === "right") ? thickness + radius : 0 + implicitHeight: (edge === "bottom") ? thickness + radius : 0 color: "transparent" exclusionMode: ExclusionMode.Ignore @@ -54,6 +54,8 @@ PanelWindow { Connections { target: root function onFillColorChanged() { shape.requestPaint() } + function onThicknessChanged() { shape.requestPaint() } + function onRadiusChanged() { shape.requestPaint() } } onPaint: { @@ -125,7 +127,7 @@ PanelWindow { ctx.lineTo(t + r, h - t); // 4. Inner Left Corner (ROUNDED) - ctx.arcTo(t + 4, h - t, t, 0, r); + ctx.arcTo(t, h - t, t, 0, r); // 5. Close Loop ctx.lineTo(t, 0); diff --git a/src/windows/TopBar.qml b/src/windows/TopBar.qml index 8b928c1..e8b422a 100644 --- a/src/windows/TopBar.qml +++ b/src/windows/TopBar.qml @@ -64,7 +64,7 @@ PanelWindow { centerContent.implicitWidth + Math.round(Theme.notchPadding * 2 * root.localScale)) ) Behavior on cWidth { - NumberAnimation { duration: Anim.transition; easing.type: Anim.inOutCubic} + NumberAnimation { duration: Popups.slideDuration; easing.type: Anim.globalCurve} } // Width matches sizer open width: popupWidth + notchRadius (fw) in both popups @@ -115,7 +115,7 @@ PanelWindow { transitions: [ Transition { // This animation ONLY runs when switching between popups (and toasts) and the base state. - NumberAnimation { property: "rWidth"; duration: Anim.transition; easing.type: Anim.inOutCubic} + NumberAnimation { property: "rWidth"; duration: Popups.slideDuration; easing.type: Anim.globalCurve} } ] @@ -133,10 +133,9 @@ PanelWindow { Item { id: leftNotch width: root.lWidth - height: Math.round((Theme.notchHeight - Theme.borderWidth) * root.localScale) + height: Math.round(Theme.notchHeight * root.localScale) anchors.left: parent.left anchors.top: parent.top - anchors.topMargin: Math.round(Theme.borderWidth * root.localScale) LeftContent { id: leftContent @@ -148,10 +147,9 @@ PanelWindow { Item { id: centerNotch width: root.cWidth - height: Math.round((Theme.notchHeight - Theme.borderWidth) * root.localScale) + height: Math.round(Theme.notchHeight * root.localScale) anchors.centerIn: parent anchors.top: parent.top - anchors.topMargin: Math.round(Theme.borderWidth * root.localScale) CenterContent { id: centerContent @@ -163,10 +161,9 @@ PanelWindow { Item { id: rightNotch width: root.rWidth - height: Math.round((Theme.notchHeight - Theme.borderWidth) * root.localScale) + height: Math.round(Theme.notchHeight * root.localScale) anchors.right: parent.right anchors.top: parent.top - anchors.topMargin: Math.round(Theme.borderWidth * root.localScale) clip: true