diff --git a/Modules/Background/Corner.qml b/Modules/Background/Corner.qml deleted file mode 100644 index dfd5b38..0000000 --- a/Modules/Background/Corner.qml +++ /dev/null @@ -1,129 +0,0 @@ -import QtQuick -import QtQuick.Effects -import Quickshell -import Quickshell.Wayland -import qs.Services - -ShellRoot { - id: root - - // Visible ring color - property color ringColor: Colors.backgroundPrimary - // The amount subtracted from full size for the inner cutout - // Inner size = full size - borderWidth (per axis) - property int borderWidth: Style.borderMedium - // Rounded radius for the inner cutout - property int innerRadius: 20 - - Variants { - model: Quickshell.screens - - PanelWindow { - required property ShellScreen modelData - - anchors { - top: true - bottom: true - left: true - right: true - } - margins { - top: Math.round(Style.barHeight * Scaling.scale(screen)) - } - color: "transparent" - screen: modelData - WlrLayershell.exclusionMode: ExclusionMode.Ignore - WlrLayershell.namespace: "quickshell-corner" - // Do not take keyboard focus and make the surface click-through - WlrLayershell.keyboardFocus: WlrKeyboardFocus.None - mask: Region {} - - // Source we want to show only as a ring - Rectangle { - id: overlaySource - anchors.fill: parent - color: root.ringColor - } - - // Texture for overlaySource - ShaderEffectSource { - id: overlayTexture - anchors.fill: parent - sourceItem: overlaySource - hideSource: true - live: true - visible: false - } - - // Mask via Canvas: paint opaque white, then punch rounded inner hole - Canvas { - id: maskSource - anchors.fill: parent - antialiasing: true - renderTarget: Canvas.FramebufferObject - onPaint: function() { - const ctx = getContext("2d"); - ctx.reset(); - ctx.clearRect(0, 0, width, height); - // Solid white base (alpha=1) - ctx.globalCompositeOperation = "source-over"; - ctx.fillStyle = "#ffffffff"; - ctx.fillRect(0, 0, width, height); - - // Punch hole using destination-out with rounded rect path - const x = Math.round(root.borderWidth / 2); - const y = Math.round(root.borderWidth / 2); - const w = Math.max(0, width - root.borderWidth); - const h = Math.max(0, height - root.borderWidth); - const r = Math.max(0, Math.min(root.innerRadius, Math.min(w, h) / 2)); - - ctx.globalCompositeOperation = "destination-out"; - ctx.fillStyle = "#ffffffff"; - ctx.beginPath(); - // rounded rectangle path using arcTo - ctx.moveTo(x + r, y); - ctx.lineTo(x + w - r, y); - ctx.arcTo(x + w, y, x + w, y + r, r); - ctx.lineTo(x + w, y + h - r); - ctx.arcTo(x + w, y + h, x + w - r, y + h, r); - ctx.lineTo(x + r, y + h); - ctx.arcTo(x, y + h, x, y + h - r, r); - ctx.lineTo(x, y + r); - ctx.arcTo(x, y, x + r, y, r); - ctx.closePath(); - ctx.fill(); - } - onWidthChanged: requestPaint() - onHeightChanged: requestPaint() - } - - // Repaint mask when properties change - Connections { - target: root - function onBorderWidthChanged() { maskSource.requestPaint() } - function onRingColorChanged() { /* no-op for mask */ } - function onInnerRadiusChanged() { maskSource.requestPaint() } - } - - // Texture for maskSource; hides the original - ShaderEffectSource { - id: maskTexture - anchors.fill: parent - sourceItem: maskSource - hideSource: true - live: true - visible: false - } - - // Apply mask to show only the ring area - MultiEffect { - anchors.fill: parent - source: overlayTexture - maskEnabled: true - maskSource: maskTexture - maskInverted: false - } - } - } -} - diff --git a/Modules/Background/ScreenCorner.qml b/Modules/Background/ScreenCorner.qml new file mode 100644 index 0000000..46b9595 --- /dev/null +++ b/Modules/Background/ScreenCorner.qml @@ -0,0 +1,136 @@ +import QtQuick +import QtQuick.Effects +import Quickshell +import Quickshell.Wayland +import qs.Services +import qs.Widgets + +NLoader { + id: cornerLoader + isLoaded: Settings.data.general.showScreenCorners + panel: Component { + ShellRoot { + id: root + + // Visible ring color + property color ringColor: Colors.backgroundPrimary + // The amount subtracted from full size for the inner cutout + // Inner size = full size - borderWidth (per axis) + property int borderWidth: Style.borderMedium + // Rounded radius for the inner cutout + property int innerRadius: 20 + + Variants { + model: Quickshell.screens + + PanelWindow { + required property ShellScreen modelData + + anchors { + top: true + bottom: true + left: true + right: true + } + margins { + top: Math.round(Style.barHeight * Scaling.scale(screen)) + } + color: "transparent" + screen: modelData + WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.namespace: "quickshell-corner" + // Do not take keyboard focus and make the surface click-through + WlrLayershell.keyboardFocus: WlrKeyboardFocus.None + mask: Region {} + + // Source we want to show only as a ring + Rectangle { + id: overlaySource + anchors.fill: parent + color: root.ringColor + } + + // Texture for overlaySource + ShaderEffectSource { + id: overlayTexture + anchors.fill: parent + sourceItem: overlaySource + hideSource: true + live: true + visible: false + } + + // Mask via Canvas: paint opaque white, then punch rounded inner hole + Canvas { + id: maskSource + anchors.fill: parent + antialiasing: true + renderTarget: Canvas.FramebufferObject + onPaint: function() { + const ctx = getContext("2d"); + ctx.reset(); + ctx.clearRect(0, 0, width, height); + // Solid white base (alpha=1) + ctx.globalCompositeOperation = "source-over"; + ctx.fillStyle = "#ffffffff"; + ctx.fillRect(0, 0, width, height); + + // Punch hole using destination-out with rounded rect path + const x = Math.round(root.borderWidth / 2); + const y = Math.round(root.borderWidth / 2); + const w = Math.max(0, width - root.borderWidth); + const h = Math.max(0, height - root.borderWidth); + const r = Math.max(0, Math.min(root.innerRadius, Math.min(w, h) / 2)); + + ctx.globalCompositeOperation = "destination-out"; + ctx.fillStyle = "#ffffffff"; + ctx.beginPath(); + // rounded rectangle path using arcTo + ctx.moveTo(x + r, y); + ctx.lineTo(x + w - r, y); + ctx.arcTo(x + w, y, x + w, y + r, r); + ctx.lineTo(x + w, y + h - r); + ctx.arcTo(x + w, y + h, x + w - r, y + h, r); + ctx.lineTo(x + r, y + h); + ctx.arcTo(x, y + h, x, y + h - r, r); + ctx.lineTo(x, y + r); + ctx.arcTo(x, y, x + r, y, r); + ctx.closePath(); + ctx.fill(); + } + onWidthChanged: requestPaint() + onHeightChanged: requestPaint() + } + + // Repaint mask when properties change + Connections { + target: root + function onBorderWidthChanged() { maskSource.requestPaint() } + function onRingColorChanged() { /* no-op for mask */ } + function onInnerRadiusChanged() { maskSource.requestPaint() } + } + + // Texture for maskSource; hides the original + ShaderEffectSource { + id: maskTexture + anchors.fill: parent + sourceItem: maskSource + hideSource: true + live: true + visible: false + } + + // Apply mask to show only the ring area + MultiEffect { + anchors.fill: parent + source: overlayTexture + maskEnabled: true + maskSource: maskTexture + maskInverted: false + } + } + } + } + } +} + diff --git a/Modules/Bar/Bar.qml b/Modules/Bar/Bar.qml index 2161a54..5b91d8d 100644 --- a/Modules/Bar/Bar.qml +++ b/Modules/Bar/Bar.qml @@ -85,7 +85,18 @@ PanelWindow { icon: "widgets" anchors.verticalCenter: parent.verticalCenter onClicked: function () { - sidePanel.isLoaded = !sidePanel.isLoaded + // Map this button's center to the screen and open the side panel below it + const localCenterX = width / 2 + const localCenterY = height / 2 + const globalPoint = mapToItem(null, localCenterX, localCenterY) + if (sidePanel.isLoaded) { + sidePanel.isLoaded = false + } else if (sidePanel.openAt) { + sidePanel.openAt(globalPoint.x, screen) + } else { + // Fallback: toggle if API unavailable + sidePanel.isLoaded = true + } } } } diff --git a/Modules/SidePanel/SidePanel.qml b/Modules/SidePanel/SidePanel.qml index 675ebd6..8068a5c 100644 --- a/Modules/SidePanel/SidePanel.qml +++ b/Modules/SidePanel/SidePanel.qml @@ -6,30 +6,56 @@ import Quickshell.Wayland import qs.Services import qs.Widgets -/* - An experiment/demo panel to tweaks widgets -*/ - NLoader { id: root + // X coordinate on screen (in pixels) where the panel should align its center. + // Set via openAt(x) from the bar button. + property real anchorX: 0 + // Target screen to open on + property var targetScreen: null + + // Public API to open the panel aligned under a given x coordinate. + function openAt(x, screen) { + anchorX = x + targetScreen = screen + isLoaded = true + // If the panel is already instantiated, update immediately + if (item) { + if (item.anchorX !== undefined) + item.anchorX = anchorX + if (item.screen !== undefined) + item.screen = targetScreen + } + } + panel: Component { NPanel { id: sidePanel readonly property real scaling: Scaling.scale(screen) + // X coordinate from the bar to align this panel under + property real anchorX: root.anchorX + // Ensure this panel attaches to the intended screen + screen: root.targetScreen // Ensure panel shows itself once created Component.onCompleted: show() Rectangle { color: Colors.backgroundPrimary - radius: Style.radiusMedium * scaling + radius: Style.radiusLarge * scaling border.color: Colors.backgroundTertiary border.width: Math.min(1, Style.borderMedium * scaling) width: 500 * scaling height: 400 - anchors.centerIn: parent + // Place the panel just below the bar (overlay content starts below bar due to topMargin) + y: Style.marginSmall * scaling + // Center horizontally under the anchorX, clamped to the screen bounds + x: Math.max( + Style.marginSmall * scaling, + Math.min(parent.width - width - Style.marginSmall * scaling, + Math.round(anchorX - width / 2))) // Prevent closing when clicking in the panel bg MouseArea { anchors.fill: parent } diff --git a/Services/Settings.qml b/Services/Settings.qml index 3873d99..5b4869a 100644 --- a/Services/Settings.qml +++ b/Services/Settings.qml @@ -15,6 +15,9 @@ Singleton { property string colorsFile: Quickshell.env("NOCTALIA_COLORS_FILE") || (settingsDir + "colors.json") property var data: settingAdapter + + // Needed to only have one NPanel loaded at a time. + property var openPanel: null Item { Component.onCompleted: { diff --git a/Widgets/NLoader.qml b/Widgets/NLoader.qml index f0166c3..b0e5323 100644 --- a/Widgets/NLoader.qml +++ b/Widgets/NLoader.qml @@ -27,6 +27,7 @@ Loader { Connections { target: loader.item + ignoreUnknownSignals: true function onDismissed() { loader.isLoaded = false } diff --git a/Widgets/NPanel.qml b/Widgets/NPanel.qml index 7733f4d..c4b7a4b 100644 --- a/Widgets/NPanel.qml +++ b/Widgets/NPanel.qml @@ -13,11 +13,20 @@ PanelWindow { signal dismissed function hide() { - visible = false + //visible = false dismissed() } function show() { + // Ensure only one panel is visible at a time using Settings as ephemeral store + try { + if (Settings.openPanel && Settings.openPanel !== outerPanel && Settings.openPanel.hide) { + Settings.openPanel.hide() + } + Settings.openPanel = outerPanel + } catch (e) { + // ignore + } visible = true } @@ -44,4 +53,16 @@ PanelWindow { easing.type: Easing.InOutCubic } } + + Component.onDestruction: { + try { + if (visible && Settings.openPanel === outerPanel) Settings.openPanel = null + } catch (e) {} + } + + onVisibleChanged: function() { + try { + if (!visible && Settings.openPanel === outerPanel) Settings.openPanel = null + } catch (e) {} + } } diff --git a/shell.qml b/shell.qml index adac8ca..4f3eed4 100644 --- a/shell.qml +++ b/shell.qml @@ -5,6 +5,7 @@ import QtQuick import Quickshell import Quickshell.Io import Quickshell.Widgets +import qs.Widgets import qs.Modules.Bar import qs.Modules.DemoPanel import qs.Modules.Background @@ -26,7 +27,7 @@ ShellRoot { Background {} Overview {} - Corner{} + ScreenCorner {} DemoPanel { id: demoPanel