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>
This commit is contained in:
Robin Seger 2026-03-29 06:22:00 +02:00 committed by GitHub
parent b5f761666d
commit c588794b20
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
24 changed files with 582 additions and 35 deletions

View file

@ -55,6 +55,7 @@ Elevation {
function onClicked(): void { function onClicked(): void {
root.itemSelected(item.modelData); root.itemSelected(item.modelData);
root.active = item.modelData; root.active = item.modelData;
item.modelData.clicked();
root.expanded = false; root.expanded = false;
} }

View file

@ -267,11 +267,13 @@ Singleton {
function serializeNotifs(): var { function serializeNotifs(): var {
return { return {
expire: notifs.expire, expire: notifs.expire,
fullscreen: notifs.fullscreen,
defaultExpireTimeout: notifs.defaultExpireTimeout, defaultExpireTimeout: notifs.defaultExpireTimeout,
clearThreshold: notifs.clearThreshold, clearThreshold: notifs.clearThreshold,
expandThreshold: notifs.expandThreshold, expandThreshold: notifs.expandThreshold,
actionOnClick: notifs.actionOnClick, actionOnClick: notifs.actionOnClick,
groupPreviewNum: notifs.groupPreviewNum groupPreviewNum: notifs.groupPreviewNum,
openExpanded: notifs.openExpanded
}; };
} }
@ -323,6 +325,7 @@ Singleton {
maxToasts: utilities.maxToasts, maxToasts: utilities.maxToasts,
toasts: { toasts: {
configLoaded: utilities.toasts.configLoaded, configLoaded: utilities.toasts.configLoaded,
fullscreen: utilities.toasts.fullscreen,
chargingChanged: utilities.toasts.chargingChanged, chargingChanged: utilities.toasts.chargingChanged,
gameModeChanged: utilities.toasts.gameModeChanged, gameModeChanged: utilities.toasts.gameModeChanged,
dndChanged: utilities.toasts.dndChanged, dndChanged: utilities.toasts.dndChanged,

View file

@ -2,6 +2,7 @@ import Quickshell.Io
JsonObject { JsonObject {
property bool expire: true property bool expire: true
property string fullscreen: "on"
property int defaultExpireTimeout: 5000 property int defaultExpireTimeout: 5000
property real clearThreshold: 0.3 property real clearThreshold: 0.3
property int expandThreshold: 20 property int expandThreshold: 20

View file

@ -46,6 +46,7 @@ JsonObject {
component Toasts: JsonObject { component Toasts: JsonObject {
property bool configLoaded: true property bool configLoaded: true
property string fullscreen: "off"
property bool chargingChanged: true property bool chargingChanged: true
property bool gameModeChanged: true property bool gameModeChanged: true
property bool dndChanged: true property bool dndChanged: true

View file

@ -9,7 +9,7 @@ Scope {
id: root id: root
property bool launcherInterrupted 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 // qmllint disable unresolved-type
CustomShortcut { CustomShortcut {

View file

@ -16,6 +16,7 @@ ColumnLayout {
required property ShellScreen screen required property ShellScreen screen
required property DrawerVisibilities visibilities required property DrawerVisibilities visibilities
required property BarPopouts.Wrapper popouts required property BarPopouts.Wrapper popouts
required property bool fullscreen
readonly property int vPadding: Appearance.padding.large readonly property int vPadding: Appearance.padding.large
function closeTray(): void { function closeTray(): void {
@ -128,6 +129,7 @@ ColumnLayout {
delegate: WrappedLoader { delegate: WrappedLoader {
sourceComponent: Workspaces { sourceComponent: Workspaces {
screen: root.screen screen: root.screen
fullscreen: root.fullscreen
} }
} }
} }
@ -135,6 +137,7 @@ ColumnLayout {
roleValue: "activeWindow" roleValue: "activeWindow"
delegate: WrappedLoader { delegate: WrappedLoader {
Layout.fillWidth: true Layout.fillWidth: true
visible: !root.fullscreen
sourceComponent: ActiveWindow { sourceComponent: ActiveWindow {
bar: root bar: root
monitor: Brightness.getMonitorForScreen(root.screen) monitor: Brightness.getMonitorForScreen(root.screen)
@ -144,18 +147,21 @@ ColumnLayout {
DelegateChoice { DelegateChoice {
roleValue: "tray" roleValue: "tray"
delegate: WrappedLoader { delegate: WrappedLoader {
visible: !root.fullscreen
sourceComponent: Tray {} sourceComponent: Tray {}
} }
} }
DelegateChoice { DelegateChoice {
roleValue: "clock" roleValue: "clock"
delegate: WrappedLoader { delegate: WrappedLoader {
visible: !root.fullscreen
sourceComponent: Clock {} sourceComponent: Clock {}
} }
} }
DelegateChoice { DelegateChoice {
roleValue: "statusIcons" roleValue: "statusIcons"
delegate: WrappedLoader { delegate: WrappedLoader {
visible: !root.fullscreen
sourceComponent: StatusIcons {} sourceComponent: StatusIcons {}
} }
} }

View file

@ -13,12 +13,13 @@ Item {
required property DrawerVisibilities visibilities required property DrawerVisibilities visibilities
required property BarPopouts.Wrapper popouts required property BarPopouts.Wrapper popouts
required property bool disabled required property bool disabled
required property bool fullscreen
readonly property int clampedWidth: Math.max(Config.border.minThickness, implicitWidth) 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 padding: Math.max(Appearance.padding.smaller, Config.border.thickness)
readonly property int contentWidth: Config.bar.sizes.innerWidth + padding * 2 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 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 property bool isHovered
function closeTray(): void { function closeTray(): void {
@ -33,8 +34,9 @@ Item {
(content.item as Bar)?.handleWheel(y, angleDelta); (content.item as Bar)?.handleWheel(y, angleDelta);
} }
visible: width > Config.border.thickness clip: true
implicitWidth: Config.border.thickness visible: width > 0
implicitWidth: fullscreen ? 0 : Config.border.thickness
states: State { states: State {
name: "visible" name: "visible"
@ -83,6 +85,7 @@ Item {
screen: root.screen screen: root.screen
visibilities: root.visibilities visibilities: root.visibilities
popouts: root.popouts // qmllint disable incompatible-type popouts: root.popouts // qmllint disable incompatible-type
fullscreen: root.fullscreen
} }
} }
} }

View file

@ -10,6 +10,7 @@ StyledRect {
required property int activeWsId required property int activeWsId
required property Repeater workspaces required property Repeater workspaces
required property Item mask required property Item mask
required property bool fullscreen
readonly property int currentWsIdx: { readonly property int currentWsIdx: {
let i = activeWsId - 1; let i = activeWsId - 1;

View file

@ -12,6 +12,7 @@ StyledClippingRect {
id: root id: root
required property ShellScreen screen 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 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 readonly property int activeWsId: Config.bar.workspaces.perMonitorWorkspaces ? (Hypr.monitorFor(screen).activeWorkspace?.id ?? 1) : Hypr.activeWsId
@ -36,6 +37,7 @@ StyledClippingRect {
anchors.fill: parent anchors.fill: parent
scale: root.onSpecial ? 0.8 : 1 scale: root.onSpecial ? 0.8 : 1
opacity: root.onSpecial ? 0.5 : 1 opacity: root.onSpecial ? 0.5 : 1
visible: !root.fullscreen
layer.enabled: root.blur > 0 layer.enabled: root.blur > 0
layer.effect: MultiEffect { layer.effect: MultiEffect {
@ -86,6 +88,7 @@ StyledClippingRect {
activeWsId: root.activeWsId activeWsId: root.activeWsId
workspaces: workspaces workspaces: workspaces
mask: layout mask: layout
fullscreen: root.fullscreen
} }
} }

View file

@ -36,6 +36,12 @@ QtObject {
readonly property string icon: "task_alt" readonly property string icon: "task_alt"
readonly property string component: "taskbar/TaskbarPane.qml" 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 { QtObject {
readonly property string id: "launcher" readonly property string id: "launcher"
readonly property string label: "launcher" readonly property string label: "launcher"
@ -60,7 +66,7 @@ QtObject {
return result; return result;
} }
function getByIndex(index: int): QtObject { function getByIndex(index: int): var {
if (index >= 0 && index < panes.length) { if (index >= 0 && index < panes.length) {
return panes[index]; return panes[index];
} }
@ -76,12 +82,12 @@ QtObject {
return -1; return -1;
} }
function getByLabel(label: string): QtObject { function getByLabel(label: string): var {
const index = getIndexByLabel(label); const index = getIndexByLabel(label);
return getByIndex(index); return getByIndex(index);
} }
function getById(id: string): QtObject { function getById(id: string): var {
for (let i = 0; i < panes.length; i++) { for (let i = 0; i < panes.length; i++) {
if (panes[i].id === id) { if (panes[i].id === id) {
return panes[i]; return panes[i];

View file

@ -5,6 +5,7 @@ import "network"
import "audio" import "audio"
import "appearance" import "appearance"
import "taskbar" import "taskbar"
import "notifications"
import "launcher" import "launcher"
import "dashboard" import "dashboard"
import QtQuick import QtQuick

View file

@ -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();
}
}
}
}
}
}
}
}
}

View file

@ -15,14 +15,17 @@ Shape {
required property Panels panels required property Panels panels
required property Item bar required property Item bar
required property real borderThickness
required property real borderRounding
anchors.fill: parent anchors.fill: parent
anchors.margins: Config.border.thickness anchors.margins: root.borderThickness
anchors.leftMargin: bar.implicitWidth anchors.leftMargin: bar.implicitWidth
preferredRendererType: Shape.CurveRenderer preferredRendererType: Shape.CurveRenderer
Osd.Background { Osd.Background {
wrapper: root.panels.osd // qmllint disable incompatible-type wrapper: root.panels.osd // qmllint disable incompatible-type
rounding: Config.border.rounding
startX: root.width - root.panels.session.width - root.panels.sidebar.width startX: root.width - root.panels.session.width - root.panels.sidebar.width
startY: (root.height - wrapper.height) / 2 - rounding startY: (root.height - wrapper.height) / 2 - rounding
@ -31,6 +34,7 @@ Shape {
Notifications.Background { Notifications.Background {
wrapper: root.panels.notifications // qmllint disable incompatible-type wrapper: root.panels.notifications // qmllint disable incompatible-type
sidebar: sidebar sidebar: sidebar
rounding: Config.border.rounding
startX: root.width startX: root.width
startY: 0 startY: 0
@ -68,6 +72,7 @@ Shape {
Utilities.Background { Utilities.Background {
wrapper: root.panels.utilities // qmllint disable incompatible-type wrapper: root.panels.utilities // qmllint disable incompatible-type
sidebar: sidebar sidebar: sidebar
rounding: root.borderRounding
startX: root.width startX: root.width
startY: root.height startY: root.height
@ -78,6 +83,7 @@ Shape {
wrapper: root.panels.sidebar // qmllint disable incompatible-type wrapper: root.panels.sidebar // qmllint disable incompatible-type
panels: root.panels panels: root.panels
rounding: root.borderRounding
startX: root.width startX: root.width
startY: root.panels.notifications.height startY: root.panels.notifications.height

View file

@ -4,12 +4,13 @@ import QtQuick
import QtQuick.Effects import QtQuick.Effects
import qs.components import qs.components
import qs.services import qs.services
import qs.config
Item { Item {
id: root id: root
required property Item bar required property Item bar
required property real borderThickness
required property real borderRounding
anchors.fill: parent anchors.fill: parent
@ -36,9 +37,9 @@ Item {
Rectangle { Rectangle {
anchors.fill: parent anchors.fill: parent
anchors.margins: Config.border.thickness anchors.margins: root.borderThickness
anchors.leftMargin: root.bar.implicitWidth anchors.leftMargin: root.bar.implicitWidth
radius: Config.border.rounding radius: root.borderRounding
} }
} }
} }

View file

@ -25,18 +25,34 @@ Variants {
Exclusions { Exclusions {
screen: scope.modelData screen: scope.modelData
bar: bar bar: bar
borderThickness: Config.border.thickness
} }
StyledWindow { StyledWindow {
id: win 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: { readonly property int dragMaskPadding: {
if (focusGrab.active || panels.popouts.isDetached) if (focusGrab.active || panels.popouts.isDetached)
return 0; return 0;
const mon = Hypr.monitorFor(screen); 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; return 0;
const thresholds = []; const thresholds = [];
@ -55,6 +71,7 @@ Variants {
screen: scope.modelData screen: scope.modelData
name: "drawers" name: "drawers"
WlrLayershell.exclusionMode: ExclusionMode.Ignore WlrLayershell.exclusionMode: ExclusionMode.Ignore
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None WlrLayershell.keyboardFocus: visibilities.launcher || visibilities.session || panels.dashboard.needsKeyboard ? WlrKeyboardFocus.OnDemand : WlrKeyboardFocus.None
mask: Region { mask: Region {
@ -72,6 +89,30 @@ Variants {
anchors.left: true anchors.left: true
anchors.right: 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 { Variants {
id: regions id: regions
@ -81,7 +122,7 @@ Variants {
required property Item modelData required property Item modelData
x: modelData.x + bar.implicitWidth x: modelData.x + bar.implicitWidth
y: modelData.y + Config.border.thickness y: modelData.y + win.borderLayoutThickness
width: modelData.width width: modelData.width
height: modelData.height height: modelData.height
intersection: Intersection.Subtract intersection: Intersection.Subtract
@ -120,16 +161,20 @@ Variants {
layer.effect: MultiEffect { layer.effect: MultiEffect {
shadowEnabled: true shadowEnabled: true
blurMax: 15 blurMax: 15
shadowColor: Qt.alpha(Colours.palette.m3shadow, 0.7) shadowColor: Qt.alpha(Colours.palette.m3shadow, Math.max(0, win.shadowOpacity))
} }
Border { Border {
bar: bar bar: bar
borderThickness: win.borderThickness
borderRounding: win.borderRounding
} }
Backgrounds { Backgrounds {
panels: panels panels: panels
bar: bar bar: bar
borderThickness: win.borderThickness
borderRounding: win.borderRounding
} }
} }
@ -145,6 +190,8 @@ Variants {
visibilities: visibilities visibilities: visibilities
panels: panels panels: panels
bar: bar bar: bar
borderThickness: win.borderLayoutThickness
fullscreen: win.hasFullscreen
Panels { Panels {
id: panels id: panels
@ -152,6 +199,7 @@ Variants {
screen: scope.modelData screen: scope.modelData
visibilities: visibilities visibilities: visibilities
bar: bar bar: bar
borderThickness: win.borderLayoutThickness
} }
BarWrapper { BarWrapper {
@ -165,6 +213,7 @@ Variants {
popouts: panels.popouts popouts: panels.popouts
disabled: scope.barDisabled disabled: scope.barDisabled
fullscreen: win.hasFullscreen
Component.onCompleted: Visibilities.bars.set(scope.modelData, this) Component.onCompleted: Visibilities.bars.set(scope.modelData, this)
} }

View file

@ -3,7 +3,6 @@ pragma ComponentBehavior: Bound
import QtQuick import QtQuick
import Quickshell import Quickshell
import qs.components.containers import qs.components.containers
import qs.config
import qs.modules.bar as Bar import qs.modules.bar as Bar
Scope { Scope {
@ -11,6 +10,7 @@ Scope {
required property ShellScreen screen required property ShellScreen screen
required property Bar.BarWrapper bar required property Bar.BarWrapper bar
required property real borderThickness
ExclusionZone { ExclusionZone {
anchors.left: true anchors.left: true
@ -32,7 +32,7 @@ Scope {
component ExclusionZone: StyledWindow { component ExclusionZone: StyledWindow {
screen: root.screen screen: root.screen
name: "border-exclusion" name: "border-exclusion"
exclusiveZone: Config.border.thickness exclusiveZone: root.borderThickness
mask: Region {} mask: Region {}
implicitWidth: 1 implicitWidth: 1
implicitHeight: 1 implicitHeight: 1

View file

@ -15,6 +15,8 @@ CustomMouseArea {
required property DrawerVisibilities visibilities required property DrawerVisibilities visibilities
required property Panels panels required property Panels panels
required property Bar.BarWrapper bar required property Bar.BarWrapper bar
required property real borderThickness
required property bool fullscreen
property point dragStart property point dragStart
property bool dashboardShortcutActive property bool dashboardShortcutActive
@ -22,7 +24,7 @@ CustomMouseArea {
property bool utilitiesShortcutActive property bool utilitiesShortcutActive
function withinPanelHeight(panel: Item, x: real, y: real): bool { 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; return y >= panelY - Config.border.rounding && y <= panelY + panel.height + Config.border.rounding;
} }
@ -48,13 +50,16 @@ CustomMouseArea {
} }
function onWheel(event: WheelEvent): void { function onWheel(event: WheelEvent): void {
if (fullscreen)
return;
if (event.x < bar.implicitWidth) { if (event.x < bar.implicitWidth) {
bar.handleWheel(event.y, event.angleDelta); bar.handleWheel(event.y, event.angleDelta);
} }
} }
anchors.fill: parent anchors.fill: parent
hoverEnabled: true acceptedButtons: fullscreen ? Qt.NoButton : Qt.AllButtons
hoverEnabled: !fullscreen
onPressed: event => dragStart = Qt.point(event.x, event.y) onPressed: event => dragStart = Qt.point(event.x, event.y)
onContainsMouseChanged: { onContainsMouseChanged: {

View file

@ -19,6 +19,7 @@ Item {
required property ShellScreen screen required property ShellScreen screen
required property DrawerVisibilities visibilities required property DrawerVisibilities visibilities
required property Bar.BarWrapper bar required property Bar.BarWrapper bar
required property real borderThickness
readonly property alias osd: osd readonly property alias osd: osd
readonly property alias notifications: notifications readonly property alias notifications: notifications
@ -31,9 +32,17 @@ Item {
readonly property alias sidebar: sidebar readonly property alias sidebar: sidebar
anchors.fill: parent anchors.fill: parent
anchors.margins: Config.border.thickness anchors.margins: root.borderThickness
anchors.leftMargin: bar.implicitWidth anchors.leftMargin: bar.implicitWidth
Behavior on anchors.margins {
Anim {}
}
Behavior on anchors.leftMargin {
Anim {}
}
Osd.Wrapper { Osd.Wrapper {
id: osd id: osd
@ -100,7 +109,7 @@ Item {
if (isDetached) if (isDetached)
return (root.height - nonAnimHeight) / 2; 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); const diff = root.height - Math.floor(off + nonAnimHeight);
if (diff < 0) if (diff < 0)
return off + diff; return off + diff;

View file

@ -2,14 +2,13 @@ import QtQuick
import QtQuick.Shapes import QtQuick.Shapes
import qs.components import qs.components
import qs.services import qs.services
import qs.config
ShapePath { ShapePath {
id: root id: root
required property Wrapper wrapper required property Wrapper wrapper
required property var sidebar 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 bool flatten: wrapper.height < rounding * 2
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding readonly property real roundingY: flatten ? wrapper.height / 2 : rounding
@ -31,14 +30,14 @@ ShapePath {
relativeY: root.wrapper.height - root.roundingY * 2 relativeY: root.wrapper.height - root.roundingY * 2
} }
PathArc { PathArc {
relativeX: root.sidebar.notifsRoundingX relativeX: root.rounding
relativeY: root.roundingY relativeY: root.roundingY
radiusX: root.sidebar.notifsRoundingX radiusX: root.rounding
radiusY: Math.min(root.rounding, root.wrapper.height) radiusY: Math.min(root.rounding, root.wrapper.height)
direction: PathArc.Counterclockwise direction: PathArc.Counterclockwise
} }
PathLine { 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 relativeY: 0
} }
PathArc { PathArc {

View file

@ -2,13 +2,12 @@ import QtQuick
import QtQuick.Shapes import QtQuick.Shapes
import qs.components import qs.components
import qs.services import qs.services
import qs.config
ShapePath { ShapePath {
id: root id: root
required property Wrapper wrapper 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 bool flatten: wrapper.width < rounding * 2
readonly property real roundingX: flatten ? wrapper.width / 2 : rounding readonly property real roundingX: flatten ? wrapper.width / 2 : rounding

View file

@ -2,15 +2,13 @@ import QtQuick
import QtQuick.Shapes import QtQuick.Shapes
import qs.components import qs.components
import qs.services import qs.services
import qs.config
ShapePath { ShapePath {
id: root id: root
required property Wrapper wrapper required property Wrapper wrapper
required property var panels required property var panels
required property real rounding
readonly property real rounding: Config.border.rounding
readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width readonly property real notifsWidthDiff: panels.notifications.width - wrapper.width
readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding readonly property real notifsRoundingX: panels.notifications.height > 0 && notifsWidthDiff < rounding * 2 ? notifsWidthDiff / 2 : rounding

View file

@ -2,14 +2,13 @@ import QtQuick
import QtQuick.Shapes import QtQuick.Shapes
import qs.components import qs.components
import qs.services import qs.services
import qs.config
ShapePath { ShapePath {
id: root id: root
required property Wrapper wrapper required property Wrapper wrapper
required property var sidebar 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 bool flatten: wrapper.height < rounding * 2
readonly property real roundingY: flatten ? wrapper.height / 2 : rounding readonly property real roundingY: flatten ? wrapper.height / 2 : rounding

View file

@ -4,6 +4,7 @@ import QtQuick
import Quickshell import Quickshell
import Caelestia import Caelestia
import qs.components import qs.components
import qs.services
import qs.config import qs.config
Item { Item {
@ -12,6 +13,16 @@ Item {
readonly property int spacing: Appearance.spacing.small readonly property int spacing: Appearance.spacing.small
property bool flag 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 implicitWidth: Config.utilities.sizes.toastWidth - Appearance.padding.normal * 2
implicitHeight: { implicitHeight: {
let h = -spacing; let h = -spacing;
@ -31,6 +42,8 @@ Item {
const toasts = []; const toasts = [];
let count = 0; let count = 0;
for (const toast of Toaster.toasts) { for (const toast of Toaster.toasts) {
if (!root.shouldShowToast(toast))
continue;
toasts.push(toast); toasts.push(toast);
if (!toast.closed) { if (!toast.closed) {
count++; count++;

View file

@ -21,6 +21,22 @@ Singleton {
property bool loaded 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: { onDndChanged: {
if (!Config.utilities.toasts.dndChanged) if (!Config.utilities.toasts.dndChanged)
return; return;
@ -79,7 +95,7 @@ Singleton {
notif.tracked = true; notif.tracked = true;
const comp = notifComp.createObject(root, { const comp = notifComp.createObject(root, {
popup: !props.dnd && ![...Visibilities.screens.values()].some(v => v.sidebar), popup: root.shouldShowPopup(),
notification: notif notification: notif
}); });
root.list = [comp, ...root.list]; root.list = [comp, ...root.list];