From c588794b20242b23ecdfb6281c68cc265db6802b Mon Sep 17 00:00:00 2001 From: Robin Seger Date: Sun, 29 Mar 2026 06:22:00 +0200 Subject: [PATCH] feat: fullscreen notification & toasts overlay (#1276) * reworked fullscreen mode as transforming shell * controlcenter configuring of toasts & notifs * fix animation on fs switch * border rounding & shadow animation * change controlcenter notifications layout * stay on WlrLayer.Overlay, use Anim * ci: update action versions * qmlformat * format take two * third time's the charm * fix: special workspace fullscreen * fix: bar width border.thickness on non-persistent behaviour * merge fix * stop layout shift on close * last few conventions sorted * linting * maximized state to fullscreen --------- Co-authored-by: 2 * r + 2 * t <61896496+soramanew@users.noreply.github.com> --- components/controls/Menu.qml | 1 + config/Config.qml | 5 +- config/NotifsConfig.qml | 1 + config/UtilitiesConfig.qml | 1 + modules/Shortcuts.qml | 2 +- modules/bar/Bar.qml | 6 + modules/bar/BarWrapper.qml | 9 +- .../components/workspaces/ActiveIndicator.qml | 1 + .../bar/components/workspaces/Workspaces.qml | 3 + modules/controlcenter/PaneRegistry.qml | 12 +- modules/controlcenter/Panes.qml | 1 + .../notifications/NotificationsPane.qml | 427 ++++++++++++++++++ modules/drawers/Backgrounds.qml | 8 +- modules/drawers/Border.qml | 7 +- modules/drawers/Drawers.qml | 57 ++- modules/drawers/Exclusions.qml | 4 +- modules/drawers/Interactions.qml | 9 +- modules/drawers/Panels.qml | 13 +- modules/notifications/Background.qml | 9 +- modules/osd/Background.qml | 3 +- modules/sidebar/Background.qml | 4 +- modules/utilities/Background.qml | 3 +- modules/utilities/toasts/Toasts.qml | 13 + services/Notifs.qml | 18 +- 24 files changed, 582 insertions(+), 35 deletions(-) create mode 100644 modules/controlcenter/notifications/NotificationsPane.qml diff --git a/components/controls/Menu.qml b/components/controls/Menu.qml index b60a3639..320f318c 100644 --- a/components/controls/Menu.qml +++ b/components/controls/Menu.qml @@ -55,6 +55,7 @@ Elevation { function onClicked(): void { root.itemSelected(item.modelData); root.active = item.modelData; + item.modelData.clicked(); root.expanded = false; } diff --git a/config/Config.qml b/config/Config.qml index 91454b59..bcdcff19 100644 --- a/config/Config.qml +++ b/config/Config.qml @@ -267,11 +267,13 @@ Singleton { function serializeNotifs(): var { return { expire: notifs.expire, + fullscreen: notifs.fullscreen, defaultExpireTimeout: notifs.defaultExpireTimeout, clearThreshold: notifs.clearThreshold, expandThreshold: notifs.expandThreshold, actionOnClick: notifs.actionOnClick, - groupPreviewNum: notifs.groupPreviewNum + groupPreviewNum: notifs.groupPreviewNum, + openExpanded: notifs.openExpanded }; } @@ -323,6 +325,7 @@ Singleton { maxToasts: utilities.maxToasts, toasts: { configLoaded: utilities.toasts.configLoaded, + fullscreen: utilities.toasts.fullscreen, chargingChanged: utilities.toasts.chargingChanged, gameModeChanged: utilities.toasts.gameModeChanged, dndChanged: utilities.toasts.dndChanged, diff --git a/config/NotifsConfig.qml b/config/NotifsConfig.qml index fa2db494..bd54b94b 100644 --- a/config/NotifsConfig.qml +++ b/config/NotifsConfig.qml @@ -2,6 +2,7 @@ import Quickshell.Io JsonObject { property bool expire: true + property string fullscreen: "on" property int defaultExpireTimeout: 5000 property real clearThreshold: 0.3 property int expandThreshold: 20 diff --git a/config/UtilitiesConfig.qml b/config/UtilitiesConfig.qml index 017ab4ad..c97e9f60 100644 --- a/config/UtilitiesConfig.qml +++ b/config/UtilitiesConfig.qml @@ -46,6 +46,7 @@ JsonObject { component Toasts: JsonObject { property bool configLoaded: true + property string fullscreen: "off" property bool chargingChanged: true property bool gameModeChanged: true property bool dndChanged: true diff --git a/modules/Shortcuts.qml b/modules/Shortcuts.qml index d73e6351..b4633e8f 100644 --- a/modules/Shortcuts.qml +++ b/modules/Shortcuts.qml @@ -9,7 +9,7 @@ Scope { id: root property bool launcherInterrupted - readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false + readonly property bool hasFullscreen: Hypr.focusedWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false // qmllint disable unresolved-type CustomShortcut { diff --git a/modules/bar/Bar.qml b/modules/bar/Bar.qml index 38ed564b..3b99a44b 100644 --- a/modules/bar/Bar.qml +++ b/modules/bar/Bar.qml @@ -16,6 +16,7 @@ ColumnLayout { required property ShellScreen screen required property DrawerVisibilities visibilities required property BarPopouts.Wrapper popouts + required property bool fullscreen readonly property int vPadding: Appearance.padding.large function closeTray(): void { @@ -128,6 +129,7 @@ ColumnLayout { delegate: WrappedLoader { sourceComponent: Workspaces { screen: root.screen + fullscreen: root.fullscreen } } } @@ -135,6 +137,7 @@ ColumnLayout { roleValue: "activeWindow" delegate: WrappedLoader { Layout.fillWidth: true + visible: !root.fullscreen sourceComponent: ActiveWindow { bar: root monitor: Brightness.getMonitorForScreen(root.screen) @@ -144,18 +147,21 @@ ColumnLayout { DelegateChoice { roleValue: "tray" delegate: WrappedLoader { + visible: !root.fullscreen sourceComponent: Tray {} } } DelegateChoice { roleValue: "clock" delegate: WrappedLoader { + visible: !root.fullscreen sourceComponent: Clock {} } } DelegateChoice { roleValue: "statusIcons" delegate: WrappedLoader { + visible: !root.fullscreen sourceComponent: StatusIcons {} } } diff --git a/modules/bar/BarWrapper.qml b/modules/bar/BarWrapper.qml index df29f0af..f29e23f9 100644 --- a/modules/bar/BarWrapper.qml +++ b/modules/bar/BarWrapper.qml @@ -13,12 +13,13 @@ Item { required property DrawerVisibilities visibilities required property BarPopouts.Wrapper popouts required property bool disabled + required property bool fullscreen readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth) readonly property int padding: Math.max(Appearance.padding.smaller, Config.border.thickness) readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2 readonly property int exclusiveZone: !disabled && (Config.bar.persistent || visibilities.bar) ? contentWidth : Config.border.thickness - readonly property bool shouldBeVisible: !disabled && (Config.bar.persistent || visibilities.bar || isHovered) + readonly property bool shouldBeVisible: !fullscreen && !disabled && (Config.bar.persistent || visibilities.bar || isHovered) property bool isHovered function closeTray(): void { @@ -33,8 +34,9 @@ Item { (content.item as Bar)?.handleWheel(y, angleDelta); } - visible: width > Config.border.thickness - implicitWidth: Config.border.thickness + clip: true + visible: width > 0 + implicitWidth: fullscreen ? 0 : Config.border.thickness states: State { name: "visible" @@ -83,6 +85,7 @@ Item { screen: root.screen visibilities: root.visibilities popouts: root.popouts // qmllint disable incompatible-type + fullscreen: root.fullscreen } } } diff --git a/modules/bar/components/workspaces/ActiveIndicator.qml b/modules/bar/components/workspaces/ActiveIndicator.qml index e8a52d4d..d10086fd 100644 --- a/modules/bar/components/workspaces/ActiveIndicator.qml +++ b/modules/bar/components/workspaces/ActiveIndicator.qml @@ -10,6 +10,7 @@ StyledRect { required property int activeWsId required property Repeater workspaces required property Item mask + required property bool fullscreen readonly property int currentWsIdx: { let i = activeWsId - 1; diff --git a/modules/bar/components/workspaces/Workspaces.qml b/modules/bar/components/workspaces/Workspaces.qml index f205dfac..bbbfdfbc 100644 --- a/modules/bar/components/workspaces/Workspaces.qml +++ b/modules/bar/components/workspaces/Workspaces.qml @@ -12,6 +12,7 @@ StyledClippingRect { id: root required property ShellScreen screen + required property bool fullscreen readonly property bool onSpecial: (Config.bar.workspaces.perMonitorWorkspaces ? Hypr.monitorFor(screen) : Hypr.focusedMonitor)?.lastIpcObject.specialWorkspace?.name !== "" readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId @@ -36,6 +37,7 @@ StyledClippingRect { anchors.fill: parent scale: root.onSpecial ? 0.8 : 1 opacity: root.onSpecial ? 0.5 : 1 + visible: !root.fullscreen layer.enabled: root.blur > 0 layer.effect: MultiEffect { @@ -86,6 +88,7 @@ StyledClippingRect { activeWsId: root.activeWsId workspaces: workspaces mask: layout + fullscreen: root.fullscreen } } diff --git a/modules/controlcenter/PaneRegistry.qml b/modules/controlcenter/PaneRegistry.qml index ca48551f..4d85969b 100644 --- a/modules/controlcenter/PaneRegistry.qml +++ b/modules/controlcenter/PaneRegistry.qml @@ -36,6 +36,12 @@ QtObject { readonly property string icon: "task_alt" readonly property string component: "taskbar/TaskbarPane.qml" }, + QtObject { + readonly property string id: "notifications" + readonly property string label: "notifications" + readonly property string icon: "notifications" + readonly property string component: "notifications/NotificationsPane.qml" + }, QtObject { readonly property string id: "launcher" readonly property string label: "launcher" @@ -60,7 +66,7 @@ QtObject { return result; } - function getByIndex(index: int): QtObject { + function getByIndex(index: int): var { if (index >= 0 && index < panes.length) { return panes[index]; } @@ -76,12 +82,12 @@ QtObject { return -1; } - function getByLabel(label: string): QtObject { + function getByLabel(label: string): var { const index = getIndexByLabel(label); return getByIndex(index); } - function getById(id: string): QtObject { + function getById(id: string): var { for (let i = 0; i < panes.length; i++) { if (panes[i].id === id) { return panes[i]; diff --git a/modules/controlcenter/Panes.qml b/modules/controlcenter/Panes.qml index 5660ea7d..6a536410 100644 --- a/modules/controlcenter/Panes.qml +++ b/modules/controlcenter/Panes.qml @@ -5,6 +5,7 @@ import "network" import "audio" import "appearance" import "taskbar" +import "notifications" import "launcher" import "dashboard" import QtQuick diff --git a/modules/controlcenter/notifications/NotificationsPane.qml b/modules/controlcenter/notifications/NotificationsPane.qml new file mode 100644 index 00000000..41920f69 --- /dev/null +++ b/modules/controlcenter/notifications/NotificationsPane.qml @@ -0,0 +1,427 @@ +pragma ComponentBehavior: Bound + +import QtQuick +import QtQuick.Layouts +import Quickshell +import Quickshell.Widgets +import ".." +import "../components" +import qs.components +import qs.components.controls +import qs.components.effects +import qs.components.containers +import qs.services +import qs.config + +Item { + id: root + + required property Session session + + property bool notificationsExpire: Config.notifs.expire ?? true + property string notificationsFullscreen: Config.notifs.fullscreen ?? "on" + property bool notificationsOpenExpanded: Config.notifs.openExpanded ?? false + property int notificationsDefaultExpireTimeout: Config.notifs.defaultExpireTimeout ?? 5000 + property int notificationsGroupPreviewNum: Config.notifs.groupPreviewNum ?? 3 + + property int maxToasts: Config.utilities.maxToasts ?? 4 + property string toastsFullscreen: Config.utilities.toasts.fullscreen ?? "off" + property bool chargingChanged: Config.utilities.toasts.chargingChanged ?? true + property bool gameModeChanged: Config.utilities.toasts.gameModeChanged ?? true + property bool dndChanged: Config.utilities.toasts.dndChanged ?? true + property bool audioOutputChanged: Config.utilities.toasts.audioOutputChanged ?? true + property bool audioInputChanged: Config.utilities.toasts.audioInputChanged ?? true + property bool capsLockChanged: Config.utilities.toasts.capsLockChanged ?? true + property bool numLockChanged: Config.utilities.toasts.numLockChanged ?? true + property bool kbLayoutChanged: Config.utilities.toasts.kbLayoutChanged ?? true + property bool vpnChanged: Config.utilities.toasts.vpnChanged ?? true + property bool nowPlaying: Config.utilities.toasts.nowPlaying ?? false + + function saveConfig(): void { + Config.notifs.expire = root.notificationsExpire; + Config.notifs.fullscreen = root.notificationsFullscreen; + Config.notifs.openExpanded = root.notificationsOpenExpanded; + Config.notifs.defaultExpireTimeout = root.notificationsDefaultExpireTimeout; + Config.notifs.groupPreviewNum = root.notificationsGroupPreviewNum; + + Config.utilities.maxToasts = root.maxToasts; + Config.utilities.toasts.fullscreen = root.toastsFullscreen; + Config.utilities.toasts.chargingChanged = root.chargingChanged; + Config.utilities.toasts.gameModeChanged = root.gameModeChanged; + Config.utilities.toasts.dndChanged = root.dndChanged; + Config.utilities.toasts.audioOutputChanged = root.audioOutputChanged; + Config.utilities.toasts.audioInputChanged = root.audioInputChanged; + Config.utilities.toasts.capsLockChanged = root.capsLockChanged; + Config.utilities.toasts.numLockChanged = root.numLockChanged; + Config.utilities.toasts.kbLayoutChanged = root.kbLayoutChanged; + Config.utilities.toasts.vpnChanged = root.vpnChanged; + Config.utilities.toasts.nowPlaying = root.nowPlaying; + + Config.save(); + } + + anchors.fill: parent + + ClippingRectangle { + id: notificationsClippingRect + + anchors.fill: parent + anchors.margins: Appearance.padding.normal + anchors.leftMargin: 0 + anchors.rightMargin: Appearance.padding.normal + + color: "transparent" + radius: notificationsBorder.innerRadius + + Loader { + id: notificationsLoader + + anchors.fill: parent + anchors.margins: Appearance.padding.large + Appearance.padding.normal + anchors.leftMargin: Appearance.padding.large + anchors.rightMargin: Appearance.padding.large + + sourceComponent: notificationsContentComponent + } + } + + InnerBorder { + id: notificationsBorder + + leftThickness: 0 + rightThickness: Appearance.padding.normal + } + + Component { + id: notificationsContentComponent + + StyledFlickable { + id: notificationsFlickable + + flickableDirection: Flickable.VerticalFlick + contentHeight: notificationsLayout.height + + StyledScrollBar.vertical: StyledScrollBar { + flickable: notificationsFlickable + } + + RowLayout { + id: notificationsLayout + + anchors.left: parent.left + anchors.right: parent.right + anchors.top: parent.top + spacing: Appearance.spacing.normal + + ColumnLayout { + Layout.fillWidth: true + Layout.maximumWidth: 500 + Layout.alignment: Qt.AlignTop + spacing: Appearance.spacing.normal + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Notifications") + font.pointSize: Appearance.font.size.normal + } + + SplitButtonRow { + id: notificationsFullscreenSelector + + function syncActiveItem(): void { + active = root.notificationsFullscreen === "off" ? notificationsFullscreenOffItem : notificationsFullscreenOnItem; + } + + label: qsTr("Show in fullscreen") + menuItems: [notificationsFullscreenOffItem, notificationsFullscreenOnItem] + + Component.onCompleted: syncActiveItem() + + Connections { + function onNotificationsFullscreenChanged(): void { + notificationsFullscreenSelector.syncActiveItem(); + } + + target: root + } + + MenuItem { + id: notificationsFullscreenOffItem + + text: qsTr("Off") + icon: "notifications_off" + activeText: qsTr("Off") + onClicked: { + root.notificationsFullscreen = "off"; + root.saveConfig(); + } + } + + MenuItem { + id: notificationsFullscreenOnItem + + text: qsTr("On") + icon: "notifications" + activeText: qsTr("On") + onClicked: { + root.notificationsFullscreen = "on"; + root.saveConfig(); + } + } + } + + SwitchRow { + label: qsTr("Expire automatically") + checked: root.notificationsExpire + onToggled: checked => { + root.notificationsExpire = checked; + root.saveConfig(); + } + } + + SwitchRow { + label: qsTr("Open expanded") + checked: root.notificationsOpenExpanded + onToggled: checked => { + root.notificationsOpenExpanded = checked; + root.saveConfig(); + } + } + + SpinBoxRow { + label: qsTr("Default timeout") + value: root.notificationsDefaultExpireTimeout + min: 1000 + max: 60000 + step: 500 + onValueModified: value => { + root.notificationsDefaultExpireTimeout = value; + root.saveConfig(); + } + } + + SpinBoxRow { + label: qsTr("Group preview count") + value: root.notificationsGroupPreviewNum + min: 1 + max: 10 + step: 1 + onValueModified: value => { + root.notificationsGroupPreviewNum = value; + root.saveConfig(); + } + } + } + } + + ColumnLayout { + Layout.fillWidth: true + Layout.alignment: Qt.AlignTop + spacing: Appearance.spacing.normal + + SectionContainer { + Layout.fillWidth: true + alignTop: true + + StyledText { + text: qsTr("Toast settings") + font.pointSize: Appearance.font.size.normal + } + + SplitButtonRow { + id: toastFullscreenSelector + + function syncActiveItem(): void { + if (root.toastsFullscreen === "all") { + active = toastFullscreenAllItem; + return; + } + + if (root.toastsFullscreen === "important") { + active = toastFullscreenImportantItem; + return; + } + + active = toastFullscreenOffItem; + } + + Layout.fillWidth: true + z: expanded ? 100 : 0 + label: qsTr("Show in fullscreen") + menuItems: [toastFullscreenOffItem, toastFullscreenImportantItem, toastFullscreenAllItem] + + Component.onCompleted: syncActiveItem() + + Connections { + function onToastsFullscreenChanged(): void { + toastFullscreenSelector.syncActiveItem(); + } + + target: root + } + + MenuItem { + id: toastFullscreenOffItem + + text: qsTr("Off") + icon: "notifications_off" + activeText: qsTr("Off") + onClicked: { + root.toastsFullscreen = "off"; + root.saveConfig(); + } + } + + MenuItem { + id: toastFullscreenImportantItem + + text: qsTr("Important") + icon: "priority_high" + activeText: qsTr("Important") + onClicked: { + root.toastsFullscreen = "important"; + root.saveConfig(); + } + } + + MenuItem { + id: toastFullscreenAllItem + + text: qsTr("On") + icon: "notifications" + activeText: qsTr("On") + onClicked: { + root.toastsFullscreen = "all"; + root.saveConfig(); + } + } + } + + SpinBoxRow { + Layout.fillWidth: true + label: qsTr("Visible toasts") + value: root.maxToasts + min: 1 + max: 10 + step: 1 + onValueModified: value => { + root.maxToasts = value; + root.saveConfig(); + } + } + + GridLayout { + Layout.fillWidth: true + columns: 2 + columnSpacing: Appearance.spacing.normal + rowSpacing: Appearance.spacing.normal + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Charging changes") + checked: root.chargingChanged + onToggled: checked => { + root.chargingChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Game mode changes") + checked: root.gameModeChanged + onToggled: checked => { + root.gameModeChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Do not disturb") + checked: root.dndChanged + onToggled: checked => { + root.dndChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Audio output changes") + checked: root.audioOutputChanged + onToggled: checked => { + root.audioOutputChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Audio input changes") + checked: root.audioInputChanged + onToggled: checked => { + root.audioInputChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Caps lock changes") + checked: root.capsLockChanged + onToggled: checked => { + root.capsLockChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Num lock changes") + checked: root.numLockChanged + onToggled: checked => { + root.numLockChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Keyboard layout changes") + checked: root.kbLayoutChanged + onToggled: checked => { + root.kbLayoutChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("VPN changes") + checked: root.vpnChanged + onToggled: checked => { + root.vpnChanged = checked; + root.saveConfig(); + } + } + + SwitchRow { + Layout.fillWidth: true + label: qsTr("Now playing") + checked: root.nowPlaying + onToggled: checked => { + root.nowPlaying = checked; + root.saveConfig(); + } + } + } + } + } + } + } + } +} diff --git a/modules/drawers/Backgrounds.qml b/modules/drawers/Backgrounds.qml index b79cd919..7592411c 100644 --- a/modules/drawers/Backgrounds.qml +++ b/modules/drawers/Backgrounds.qml @@ -15,14 +15,17 @@ Shape { required property Panels panels required property Item bar + required property real borderThickness + required property real borderRounding anchors.fill: parent - anchors.margins: Config.border.thickness + anchors.margins: root.borderThickness anchors.leftMargin: bar.implicitWidth preferredRendererType: Shape.CurveRenderer Osd.Background { wrapper: root.panels.osd // qmllint disable incompatible-type + rounding: Config.border.rounding startX: root.width - root.panels.session.width - root.panels.sidebar.width startY: (root.height - wrapper.height) / 2 - rounding @@ -31,6 +34,7 @@ Shape { Notifications.Background { wrapper: root.panels.notifications // qmllint disable incompatible-type sidebar: sidebar + rounding: Config.border.rounding startX: root.width startY: 0 @@ -68,6 +72,7 @@ Shape { Utilities.Background { wrapper: root.panels.utilities // qmllint disable incompatible-type sidebar: sidebar + rounding: root.borderRounding startX: root.width startY: root.height @@ -78,6 +83,7 @@ Shape { wrapper: root.panels.sidebar // qmllint disable incompatible-type panels: root.panels + rounding: root.borderRounding startX: root.width startY: root.panels.notifications.height diff --git a/modules/drawers/Border.qml b/modules/drawers/Border.qml index 13f92a4b..a638479d 100644 --- a/modules/drawers/Border.qml +++ b/modules/drawers/Border.qml @@ -4,12 +4,13 @@ import QtQuick import QtQuick.Effects import qs.components import qs.services -import qs.config Item { id: root required property Item bar + required property real borderThickness + required property real borderRounding anchors.fill: parent @@ -36,9 +37,9 @@ Item { Rectangle { anchors.fill: parent - anchors.margins: Config.border.thickness + anchors.margins: root.borderThickness anchors.leftMargin: root.bar.implicitWidth - radius: Config.border.rounding + radius: root.borderRounding } } } diff --git a/modules/drawers/Drawers.qml b/modules/drawers/Drawers.qml index 864ad5c4..9d3de776 100644 --- a/modules/drawers/Drawers.qml +++ b/modules/drawers/Drawers.qml @@ -25,18 +25,34 @@ Variants { Exclusions { screen: scope.modelData bar: bar + borderThickness: Config.border.thickness } StyledWindow { id: win - readonly property bool hasFullscreen: Hypr.monitorFor(screen)?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen === 2) ?? false + readonly property var monitor: Hypr.monitorFor(screen) + readonly property bool hasSpecialWorkspace: (monitor?.lastIpcObject?.specialWorkspace?.name.length ?? 0) > 0 + readonly property bool hasFullscreen: { + if (hasSpecialWorkspace) { + const specialName = monitor?.lastIpcObject?.specialWorkspace?.name; + if (!specialName) + return false; + const specialWs = Hypr.workspaces.values.find(ws => ws.name === specialName); + return specialWs?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; + } + return monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1) ?? false; + } + property real borderThickness: hasFullscreen ? 0 : Config.border.thickness + readonly property real borderLayoutThickness: hasFullscreen ? 0 : Config.border.thickness + property real borderRounding: hasFullscreen ? 0 : Config.border.rounding + property real shadowOpacity: hasFullscreen ? 0 : 0.7 readonly property int dragMaskPadding: { if (focusGrab.active || panels.popouts.isDetached) return 0; const mon = Hypr.monitorFor(screen); - if (mon?.lastIpcObject.specialWorkspace?.name || mon?.activeWorkspace?.lastIpcObject.windows > 0) + if (mon?.lastIpcObject.specialWorkspace?.name || mon?.activeWorkspace.lastIpcObject.windows > 0) return 0; const thresholds = []; @@ -55,6 +71,7 @@ Variants { screen: scope.modelData name: "drawers" WlrLayershell.exclusionMode: ExclusionMode.Ignore + WlrLayershell.layer: WlrLayer.Overlay WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None mask: Region { @@ -72,6 +89,30 @@ Variants { anchors.left: true anchors.right: true + Behavior on borderThickness { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on borderRounding { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + + Behavior on shadowOpacity { + Anim { + duration: Appearance.anim.durations.expressiveDefaultSpatial + easing.type: Easing.BezierSpline + easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial + } + } + Variants { id: regions @@ -81,7 +122,7 @@ Variants { required property Item modelData x: modelData.x + bar.implicitWidth - y: modelData.y + Config.border.thickness + y: modelData.y + win.borderLayoutThickness width: modelData.width height: modelData.height intersection: Intersection.Subtract @@ -120,16 +161,20 @@ Variants { layer.effect: MultiEffect { shadowEnabled: true blurMax: 15 - shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) + shadowColor: Qt.alpha(Colours.palette.m3shadow, Math.max(0, win.shadowOpacity)) } Border { bar: bar + borderThickness: win.borderThickness + borderRounding: win.borderRounding } Backgrounds { panels: panels bar: bar + borderThickness: win.borderThickness + borderRounding: win.borderRounding } } @@ -145,6 +190,8 @@ Variants { visibilities: visibilities panels: panels bar: bar + borderThickness: win.borderLayoutThickness + fullscreen: win.hasFullscreen Panels { id: panels @@ -152,6 +199,7 @@ Variants { screen: scope.modelData visibilities: visibilities bar: bar + borderThickness: win.borderLayoutThickness } BarWrapper { @@ -165,6 +213,7 @@ Variants { popouts: panels.popouts disabled: scope.barDisabled + fullscreen: win.hasFullscreen Component.onCompleted: Visibilities.bars.set(scope.modelData, this) } diff --git a/modules/drawers/Exclusions.qml b/modules/drawers/Exclusions.qml index f43afb9a..87610ee6 100644 --- a/modules/drawers/Exclusions.qml +++ b/modules/drawers/Exclusions.qml @@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound import QtQuick import Quickshell import qs.components.containers -import qs.config import qs.modules.bar as Bar Scope { @@ -11,6 +10,7 @@ Scope { required property ShellScreen screen required property Bar.BarWrapper bar + required property real borderThickness ExclusionZone { anchors.left: true @@ -32,7 +32,7 @@ Scope { component ExclusionZone: StyledWindow { screen: root.screen name: "border-exclusion" - exclusiveZone: Config.border.thickness + exclusiveZone: root.borderThickness mask: Region {} implicitWidth: 1 implicitHeight: 1 diff --git a/modules/drawers/Interactions.qml b/modules/drawers/Interactions.qml index fcb128a9..4b670fca 100644 --- a/modules/drawers/Interactions.qml +++ b/modules/drawers/Interactions.qml @@ -15,6 +15,8 @@ CustomMouseArea { required property DrawerVisibilities visibilities required property Panels panels required property Bar.BarWrapper bar + required property real borderThickness + required property bool fullscreen property point dragStart property bool dashboardShortcutActive @@ -22,7 +24,7 @@ CustomMouseArea { property bool utilitiesShortcutActive function withinPanelHeight(panel: Item, x: real, y: real): bool { - const panelY = Config.border.thickness + panel.y; + const panelY = root.borderThickness + panel.y; return y >= panelY - Config.border.rounding && y <= panelY + panel.height + Config.border.rounding; } @@ -48,13 +50,16 @@ CustomMouseArea { } function onWheel(event: WheelEvent): void { + if (fullscreen) + return; if (event.x < bar.implicitWidth) { bar.handleWheel(event.y, event.angleDelta); } } anchors.fill: parent - hoverEnabled: true + acceptedButtons: fullscreen ? Qt.NoButton : Qt.AllButtons + hoverEnabled: !fullscreen onPressed: event => dragStart = Qt.point(event.x, event.y) onContainsMouseChanged: { diff --git a/modules/drawers/Panels.qml b/modules/drawers/Panels.qml index f2531cfa..04b83c8d 100644 --- a/modules/drawers/Panels.qml +++ b/modules/drawers/Panels.qml @@ -19,6 +19,7 @@ Item { required property ShellScreen screen required property DrawerVisibilities visibilities required property Bar.BarWrapper bar + required property real borderThickness readonly property alias osd: osd readonly property alias notifications: notifications @@ -31,9 +32,17 @@ Item { readonly property alias sidebar: sidebar anchors.fill: parent - anchors.margins: Config.border.thickness + anchors.margins: root.borderThickness anchors.leftMargin: bar.implicitWidth + Behavior on anchors.margins { + Anim {} + } + + Behavior on anchors.leftMargin { + Anim {} + } + Osd.Wrapper { id: osd @@ -100,7 +109,7 @@ Item { if (isDetached) return (root.height - nonAnimHeight) / 2; - const off = currentCenter - Config.border.thickness - nonAnimHeight / 2; + const off = currentCenter - root.borderThickness - nonAnimHeight / 2; const diff = root.height - Math.floor(off + nonAnimHeight); if (diff < 0) return off + diff; diff --git a/modules/notifications/Background.qml b/modules/notifications/Background.qml index 4d7a5ff7..740cda10 100644 --- a/modules/notifications/Background.qml +++ b/modules/notifications/Background.qml @@ -2,14 +2,13 @@ import QtQuick import QtQuick.Shapes import qs.components import qs.services -import qs.config ShapePath { id: root required property Wrapper wrapper required property var sidebar - readonly property real rounding: Config.border.rounding + required property real rounding readonly property bool flatten: wrapper.height < rounding * 2 readonly property real roundingY: flatten ? wrapper.height / 2 : rounding @@ -31,14 +30,14 @@ ShapePath { relativeY: root.wrapper.height - root.roundingY * 2 } PathArc { - relativeX: root.sidebar.notifsRoundingX + relativeX: root.rounding relativeY: root.roundingY - radiusX: root.sidebar.notifsRoundingX + radiusX: root.rounding radiusY: Math.min(root.rounding, root.wrapper.height) direction: PathArc.Counterclockwise } PathLine { - relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding - root.sidebar.notifsRoundingX : root.wrapper.width + relativeX: root.wrapper.height > 0 ? root.wrapper.width - root.rounding * 2 : root.wrapper.width relativeY: 0 } PathArc { diff --git a/modules/osd/Background.qml b/modules/osd/Background.qml index a609f460..330c703e 100644 --- a/modules/osd/Background.qml +++ b/modules/osd/Background.qml @@ -2,13 +2,12 @@ import QtQuick import QtQuick.Shapes import qs.components import qs.services -import qs.config ShapePath { id: root required property Wrapper wrapper - readonly property real rounding: Config.border.rounding + required property real rounding readonly property bool flatten: wrapper.width < rounding * 2 readonly property real roundingX: flatten ? wrapper.width / 2 : rounding diff --git a/modules/sidebar/Background.qml b/modules/sidebar/Background.qml index 4cc14262..c7d42123 100644 --- a/modules/sidebar/Background.qml +++ b/modules/sidebar/Background.qml @@ -2,15 +2,13 @@ import QtQuick import QtQuick.Shapes import qs.components import qs.services -import qs.config ShapePath { id: root required property Wrapper wrapper required property var panels - - readonly property real rounding: Config.border.rounding + required property real rounding readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding diff --git a/modules/utilities/Background.qml b/modules/utilities/Background.qml index 975461a5..5b58b41e 100644 --- a/modules/utilities/Background.qml +++ b/modules/utilities/Background.qml @@ -2,14 +2,13 @@ import QtQuick import QtQuick.Shapes import qs.components import qs.services -import qs.config ShapePath { id: root required property Wrapper wrapper required property var sidebar - readonly property real rounding: Config.border.rounding + required property real rounding readonly property bool flatten: wrapper.height < rounding * 2 readonly property real roundingY: flatten ? wrapper.height / 2 : rounding diff --git a/modules/utilities/toasts/Toasts.qml b/modules/utilities/toasts/Toasts.qml index ac8772f5..1ef072e9 100644 --- a/modules/utilities/toasts/Toasts.qml +++ b/modules/utilities/toasts/Toasts.qml @@ -4,6 +4,7 @@ import QtQuick import Quickshell import Caelestia import qs.components +import qs.services import qs.config Item { @@ -12,6 +13,16 @@ Item { readonly property int spacing: Appearance.spacing.small property bool flag + function shouldShowToast(toast: Toast): bool { + if (!Notifs.hasFullscreen()) + return true; + if (Config.utilities.toasts.fullscreen === "all") + return true; + if (Config.utilities.toasts.fullscreen === "important") + return toast.type === Toast.Warning || toast.type === Toast.Error; + return false; + } + implicitWidth: Config.utilities.sizes.toastWidth - Appearance.padding.normal * 2 implicitHeight: { let h = -spacing; @@ -31,6 +42,8 @@ Item { const toasts = []; let count = 0; for (const toast of Toaster.toasts) { + if (!root.shouldShowToast(toast)) + continue; toasts.push(toast); if (!toast.closed) { count++; diff --git a/services/Notifs.qml b/services/Notifs.qml index 4e6c0dd6..92e609be 100644 --- a/services/Notifs.qml +++ b/services/Notifs.qml @@ -21,6 +21,22 @@ Singleton { property bool loaded + function hasFullscreen(): bool { + for (const monitor of Hypr.monitors.values) { + if (monitor?.activeWorkspace?.toplevels.values.some(t => t.lastIpcObject.fullscreen > 1)) + return true; + } + return false; + } + + function shouldShowPopup(): bool { + if (props.dnd || [...Visibilities.screens.values()].some(v => v.sidebar)) + return false; + if (Config.notifs.fullscreen === "off" && hasFullscreen()) + return false; + return true; + } + onDndChanged: { if (!Config.utilities.toasts.dndChanged) return; @@ -79,7 +95,7 @@ Singleton { notif.tracked = true; const comp = notifComp.createObject(root, { - popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar), + popup: root.shouldShowPopup(), notification: notif }); root.list = [comp, ...root.list];