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 {
root.itemSelected(item.modelData);
root.active = item.modelData;
item.modelData.clicked();
root.expanded = false;
}

View file

@ -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,

View file

@ -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

View file

@ -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

View file

@ -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 {

View file

@ -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 {}
}
}

View file

@ -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
}
}
}

View file

@ -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;

View file

@ -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
}
}

View file

@ -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];

View file

@ -5,6 +5,7 @@ import "network"
import "audio"
import "appearance"
import "taskbar"
import "notifications"
import "launcher"
import "dashboard"
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 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

View file

@ -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
}
}
}

View file

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

View file

@ -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

View file

@ -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: {

View file

@ -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;

View file

@ -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 {

View file

@ -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

View file

@ -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

View file

@ -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

View file

@ -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++;

View file

@ -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];