utilities: add toasts

This commit is contained in:
2 * r + 2 * t 2025-09-21 16:43:45 +10:00
parent 9523fdf0eb
commit 0ca5d50546
9 changed files with 576 additions and 71 deletions

View file

@ -2,6 +2,7 @@ import Quickshell.Io
JsonObject { JsonObject {
property bool enabled: true property bool enabled: true
property int maxToasts: 4
property Sizes sizes: Sizes {} property Sizes sizes: Sizes {}

View file

@ -1,6 +1,7 @@
import qs.components.misc import qs.components.misc
import qs.modules.controlcenter import qs.modules.controlcenter
import qs.services import qs.services
import Caelestia
import Quickshell import Quickshell
import Quickshell.Io import Quickshell.Io
@ -95,4 +96,24 @@ Scope {
WindowFactory.create(); WindowFactory.create();
} }
} }
IpcHandler {
target: "toaster"
function info(title: string, message: string, icon: string): void {
Toaster.toast(title, message, icon, Toast.Info);
}
function success(title: string, message: string, icon: string): void {
Toaster.toast(title, message, icon, Toast.Success);
}
function warn(title: string, message: string, icon: string): void {
Toaster.toast(title, message, icon, Toast.Warning);
}
function error(title: string, message: string, icon: string): void {
Toaster.toast(title, message, icon, Toast.Error);
}
}
} }

View file

@ -6,6 +6,7 @@ import qs.modules.launcher as Launcher
import qs.modules.dashboard as Dashboard import qs.modules.dashboard as Dashboard
import qs.modules.bar.popouts as BarPopouts import qs.modules.bar.popouts as BarPopouts
import qs.modules.utilities as Utilities import qs.modules.utilities as Utilities
import qs.modules.utilities.toasts as Toasts
import qs.modules.sidebar as Sidebar import qs.modules.sidebar as Sidebar
import Quickshell import Quickshell
import QtQuick import QtQuick
@ -17,14 +18,15 @@ Item {
required property PersistentProperties visibilities required property PersistentProperties visibilities
required property Item bar required property Item bar
readonly property Osd.Wrapper osd: osd readonly property alias osd: osd
readonly property Notifications.Wrapper notifications: notifications readonly property alias notifications: notifications
readonly property Session.Wrapper session: session readonly property alias session: session
readonly property Launcher.Wrapper launcher: launcher readonly property alias launcher: launcher
readonly property Dashboard.Wrapper dashboard: dashboard readonly property alias dashboard: dashboard
readonly property BarPopouts.Wrapper popouts: popouts readonly property alias popouts: popouts
readonly property Utilities.Wrapper utilities: utilities readonly property alias utilities: utilities
readonly property Sidebar.Wrapper sidebar: sidebar readonly property alias toasts: toasts
readonly property alias sidebar: sidebar
anchors.fill: parent anchors.fill: parent
anchors.margins: Config.border.thickness anchors.margins: Config.border.thickness
@ -112,6 +114,14 @@ Item {
anchors.right: parent.right anchors.right: parent.right
} }
Toasts.Toasts {
id: toasts
anchors.bottom: sidebar.visible ? parent.bottom : utilities.top
anchors.right: sidebar.left
anchors.margins: Appearance.padding.normal
}
Sidebar.Wrapper { Sidebar.Wrapper {
id: sidebar id: sidebar

View file

@ -0,0 +1,132 @@
import qs.components
import qs.components.effects
import qs.services
import qs.config
import Caelestia
import QtQuick
import QtQuick.Layouts
StyledRect {
id: root
required property Toast modelData
anchors.left: parent.left
anchors.right: parent.right
implicitHeight: layout.implicitHeight + Appearance.padding.smaller * 2
radius: Appearance.rounding.normal
color: {
if (root.modelData.type === Toast.Success)
return Colours.palette.m3successContainer;
if (root.modelData.type === Toast.Warning)
return Colours.palette.m3secondary;
if (root.modelData.type === Toast.Error)
return Colours.palette.m3errorContainer;
return Colours.palette.m3surface;
}
border.width: 1
border.color: {
let colour = Colours.palette.m3outlineVariant;
if (root.modelData.type === Toast.Success)
colour = Colours.palette.m3success;
if (root.modelData.type === Toast.Warning)
colour = Colours.palette.m3secondaryContainer;
if (root.modelData.type === Toast.Error)
colour = Colours.palette.m3error;
return Qt.alpha(colour, 0.3);
}
Elevation {
anchors.fill: parent
radius: parent.radius
opacity: parent.opacity
z: -1
level: 3
}
RowLayout {
id: layout
anchors.fill: parent
anchors.margins: Appearance.padding.smaller
anchors.leftMargin: Appearance.padding.normal
anchors.rightMargin: Appearance.padding.normal
spacing: Appearance.spacing.normal
StyledRect {
radius: Appearance.rounding.normal
color: {
if (root.modelData.type === Toast.Success)
return Colours.palette.m3success;
if (root.modelData.type === Toast.Warning)
return Colours.palette.m3secondaryContainer;
if (root.modelData.type === Toast.Error)
return Colours.palette.m3error;
return Colours.palette.m3surfaceContainerHigh;
}
implicitWidth: implicitHeight
implicitHeight: icon.implicitHeight + Appearance.padding.normal * 2
MaterialIcon {
id: icon
anchors.centerIn: parent
text: root.modelData.icon
color: {
if (root.modelData.type === Toast.Success)
return Colours.palette.m3onSuccess;
if (root.modelData.type === Toast.Warning)
return Colours.palette.m3onSecondaryContainer;
if (root.modelData.type === Toast.Error)
return Colours.palette.m3onError;
return Colours.palette.m3onSurfaceVariant;
}
font.pointSize: Appearance.font.size.large
}
}
ColumnLayout {
Layout.fillWidth: true
spacing: 0
StyledText {
id: title
Layout.fillWidth: true
text: root.modelData.title
color: {
if (root.modelData.type === Toast.Success)
return Colours.palette.m3onSuccessContainer;
if (root.modelData.type === Toast.Warning)
return Colours.palette.m3onSecondary;
if (root.modelData.type === Toast.Error)
return Colours.palette.m3onErrorContainer;
return Colours.palette.m3onSurface;
}
font.pointSize: Appearance.font.size.normal
}
StyledText {
Layout.fillWidth: true
text: root.modelData.message
color: {
if (root.modelData.type === Toast.Success)
return Colours.palette.m3onSuccessContainer;
if (root.modelData.type === Toast.Warning)
return Colours.palette.m3onSecondary;
if (root.modelData.type === Toast.Error)
return Colours.palette.m3onErrorContainer;
return Colours.palette.m3onSurface;
}
opacity: 0.8
}
}
}
Behavior on border.color {
CAnim {}
}
}

View file

@ -0,0 +1,135 @@
pragma ComponentBehavior: Bound
import qs.components
import qs.config
import Caelestia
import Quickshell
import QtQuick
import QtQuick.Layouts
Item {
id: root
readonly property int spacing: Appearance.spacing.small
property bool flag
implicitWidth: Config.utilities.sizes.width - Appearance.padding.normal * 2
implicitHeight: {
let h = -spacing;
for (let i = 0; i < repeater.count; i++) {
const item = repeater.itemAt(i);
if (!item.modelData.closed && !item.previewHidden)
h += item.implicitHeight + spacing;
}
return h;
}
Repeater {
id: repeater
model: ScriptModel {
values: {
const toasts = [];
let count = 0;
for (const toast of Toaster.toasts) {
toasts.push(toast);
if (!toast.closed) {
count++;
if (count > Config.utilities.maxToasts)
break;
}
}
return toasts;
}
onValuesChanged: root.flagChanged()
}
MouseArea {
id: toast
required property int index
required property Toast modelData
readonly property bool previewHidden: {
let extraHidden = 0;
for (let i = 0; i < index; i++)
if (Toaster.toasts[i].closed)
extraHidden++;
return index >= Config.utilities.maxToasts + extraHidden;
}
opacity: modelData.closed || previewHidden ? 0 : 1
scale: modelData.closed || previewHidden ? 0.7 : 1
anchors.bottomMargin: {
root.flag; // Force update
let y = 0;
for (let i = 0; i < index; i++) {
const item = repeater.itemAt(i);
if (item && !item.modelData.closed && !item.previewHidden)
y += item.implicitHeight + root.spacing;
}
return y;
}
anchors.left: parent.left
anchors.right: parent.right
anchors.bottom: parent.bottom
implicitHeight: toastInner.implicitHeight
acceptedButtons: Qt.LeftButton | Qt.MiddleButton | Qt.RightButton
onClicked: modelData.close()
Component.onCompleted: modelData.lock(this)
Anim {
Component.onCompleted: running = !toast.previewHidden
target: toast
properties: "opacity,scale"
from: 0
to: 1
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
ParallelAnimation {
running: toast.modelData.closed
onStarted: toast.anchors.bottomMargin = toast.anchors.bottomMargin
onFinished: toast.modelData.unlock(toast)
Anim {
target: toast
property: "opacity"
to: 0
}
Anim {
target: toast
property: "scale"
to: 0.7
}
}
ToastItem {
id: toastInner
modelData: toast.modelData
}
Behavior on opacity {
Anim {}
}
Behavior on scale {
Anim {}
}
Behavior on anchors.bottomMargin {
Anim {
duration: Appearance.anim.durations.expressiveDefaultSpatial
easing.bezierCurve: Appearance.anim.curves.expressiveDefaultSpatial
}
}
}
}
}

View file

@ -44,6 +44,7 @@ qml_module(caelestia
qalculator.hpp qalculator.cpp qalculator.hpp qalculator.cpp
appdb.hpp appdb.cpp appdb.hpp appdb.cpp
requests.hpp requests.cpp requests.hpp requests.cpp
toaster.hpp toaster.cpp
LIBRARIES LIBRARIES
Qt::Gui Qt::Gui
Qt::Quick Qt::Quick

View file

@ -0,0 +1,116 @@
#include "toaster.hpp"
#include <qdebug.h>
#include <qlogging.h>
#include <qtimer.h>
namespace caelestia {
Toast::Toast(const QString& title, const QString& message, const QString& icon, Type type, int timeout, QObject* parent)
: QObject(parent)
, m_closed(false)
, m_title(title)
, m_message(message)
, m_icon(icon)
, m_type(type)
, m_timeout(timeout) {
QTimer::singleShot(timeout, this, &Toast::close);
if (m_icon.isEmpty()) {
switch (m_type) {
case Type::Success:
m_icon = "check_circle_unread";
break;
case Type::Warning:
m_icon = "warning";
break;
case Type::Error:
m_icon = "error";
break;
default:
m_icon = "info";
break;
}
}
if (timeout <= 0) {
switch (m_type) {
case Type::Warning:
m_timeout = 7000;
break;
case Type::Error:
m_timeout = 10000;
break;
default:
m_timeout = 5000;
break;
}
}
}
bool Toast::closed() const {
return m_closed;
}
QString Toast::title() const {
return m_title;
}
QString Toast::message() const {
return m_message;
}
QString Toast::icon() const {
return m_icon;
}
int Toast::timeout() const {
return m_timeout;
}
Toast::Type Toast::type() const {
return m_type;
}
void Toast::close() {
if (!m_closed) {
m_closed = true;
emit closedChanged();
}
if (m_locks.isEmpty()) {
emit finishedClose();
}
}
void Toast::lock(QObject* sender) {
m_locks << sender;
QObject::connect(sender, &QObject::destroyed, this, &Toast::unlock);
}
void Toast::unlock(QObject* sender) {
if (m_locks.remove(sender) && m_closed) {
close();
}
}
Toaster::Toaster(QObject* parent)
: QObject(parent) {}
QList<Toast*> Toaster::toasts() const {
return m_toasts;
}
void Toaster::toast(const QString& title, const QString& message, const QString& icon, Toast::Type type, int timeout) {
auto* toast = new Toast(title, message, icon, type, timeout, this);
QObject::connect(toast, &Toast::finishedClose, this, [toast, this]() {
if (m_toasts.removeOne(toast)) {
emit toastsChanged();
toast->deleteLater();
}
});
m_toasts.push_front(toast);
emit toastsChanged();
}
} // namespace caelestia

View file

@ -0,0 +1,81 @@
#pragma once
#include <qobject.h>
#include <qqmlintegration.h>
#include <qset.h>
namespace caelestia {
class Toast : public QObject {
Q_OBJECT
QML_ELEMENT
QML_UNCREATABLE("Toast instances can only be retrieved from a Toaster")
Q_PROPERTY(bool closed READ closed NOTIFY closedChanged)
Q_PROPERTY(QString title READ title CONSTANT)
Q_PROPERTY(QString message READ message CONSTANT)
Q_PROPERTY(QString icon READ icon CONSTANT)
Q_PROPERTY(int timeout READ timeout CONSTANT)
Q_PROPERTY(Type type READ type CONSTANT)
public:
enum class Type {
Info = 0,
Success,
Warning,
Error
};
Q_ENUM(Type)
explicit Toast(const QString& title, const QString& message, const QString& icon, Type type, int timeout,
QObject* parent = nullptr);
[[nodiscard]] bool closed() const;
[[nodiscard]] QString title() const;
[[nodiscard]] QString message() const;
[[nodiscard]] QString icon() const;
[[nodiscard]] int timeout() const;
[[nodiscard]] Type type() const;
Q_INVOKABLE void close();
Q_INVOKABLE void lock(QObject* sender);
Q_INVOKABLE void unlock(QObject* sender);
signals:
void closedChanged();
void finishedClose();
private:
QSet<QObject*> m_locks;
bool m_closed;
QString m_title;
QString m_message;
QString m_icon;
Type m_type;
int m_timeout;
};
class Toaster : public QObject {
Q_OBJECT
QML_ELEMENT
QML_SINGLETON
Q_PROPERTY(QList<Toast*> toasts READ toasts NOTIFY toastsChanged)
public:
explicit Toaster(QObject* parent = nullptr);
[[nodiscard]] QList<Toast*> toasts() const;
Q_INVOKABLE void toast(const QString& title, const QString& message, const QString& icon = QString(),
Toast::Type type = Toast::Type::Info, int timeout = 5000);
signals:
void toastsChanged();
private:
QList<Toast*> m_toasts;
};
} // namespace caelestia

View file

@ -146,6 +146,10 @@ Singleton {
readonly property color m3onError: root.layer(root.palette.m3onError) readonly property color m3onError: root.layer(root.palette.m3onError)
readonly property color m3errorContainer: root.layer(root.palette.m3errorContainer) readonly property color m3errorContainer: root.layer(root.palette.m3errorContainer)
readonly property color m3onErrorContainer: root.layer(root.palette.m3onErrorContainer) readonly property color m3onErrorContainer: root.layer(root.palette.m3onErrorContainer)
readonly property color m3success: root.layer(root.palette.m3success)
readonly property color m3onSuccess: root.layer(root.palette.m3onSuccess)
readonly property color m3successContainer: root.layer(root.palette.m3successContainer)
readonly property color m3onSuccessContainer: root.layer(root.palette.m3onSuccessContainer)
readonly property color m3primaryFixed: root.layer(root.palette.m3primaryFixed) readonly property color m3primaryFixed: root.layer(root.palette.m3primaryFixed)
readonly property color m3primaryFixedDim: root.layer(root.palette.m3primaryFixedDim) readonly property color m3primaryFixedDim: root.layer(root.palette.m3primaryFixedDim)
readonly property color m3onPrimaryFixed: root.layer(root.palette.m3onPrimaryFixed) readonly property color m3onPrimaryFixed: root.layer(root.palette.m3onPrimaryFixed)
@ -161,75 +165,79 @@ Singleton {
} }
component M3Palette: QtObject { component M3Palette: QtObject {
property color m3primary_paletteKeyColor: "#a26387" property color m3primary_paletteKeyColor: "#a8627b"
property color m3secondary_paletteKeyColor: "#8b6f7d" property color m3secondary_paletteKeyColor: "#8e6f78"
property color m3tertiary_paletteKeyColor: "#9c6c53" property color m3tertiary_paletteKeyColor: "#986e4c"
property color m3neutral_paletteKeyColor: "#7f7478" property color m3neutral_paletteKeyColor: "#807477"
property color m3neutral_variant_paletteKeyColor: "#827379" property color m3neutral_variant_paletteKeyColor: "#837377"
property color m3background: "#181115" property color m3background: "#191114"
property color m3onBackground: "#eddfe4" property color m3onBackground: "#efdfe2"
property color m3surface: "#181115" property color m3surface: "#191114"
property color m3surfaceDim: "#181115" property color m3surfaceDim: "#191114"
property color m3surfaceBright: "#40373b" property color m3surfaceBright: "#403739"
property color m3surfaceContainerLowest: "#130c10" property color m3surfaceContainerLowest: "#130c0e"
property color m3surfaceContainerLow: "#211a1d" property color m3surfaceContainerLow: "#22191c"
property color m3surfaceContainer: "#251e21" property color m3surfaceContainer: "#261d20"
property color m3surfaceContainerHigh: "#30282b" property color m3surfaceContainerHigh: "#31282a"
property color m3surfaceContainerHighest: "#3b3236" property color m3surfaceContainerHighest: "#3c3235"
property color m3onSurface: "#eddfe4" property color m3onSurface: "#efdfe2"
property color m3surfaceVariant: "#504349" property color m3surfaceVariant: "#514347"
property color m3onSurfaceVariant: "#d3c2c9" property color m3onSurfaceVariant: "#d5c2c6"
property color m3inverseSurface: "#eddfe4" property color m3inverseSurface: "#efdfe2"
property color m3inverseOnSurface: "#362e32" property color m3inverseOnSurface: "#372e30"
property color m3outline: "#9c8d93" property color m3outline: "#9e8c91"
property color m3outlineVariant: "#504349" property color m3outlineVariant: "#514347"
property color m3shadow: "#000000" property color m3shadow: "#000000"
property color m3scrim: "#000000" property color m3scrim: "#000000"
property color m3surfaceTint: "#fbb1d8" property color m3surfaceTint: "#ffb0ca"
property color m3primary: "#fbb1d8" property color m3primary: "#ffb0ca"
property color m3onPrimary: "#511d3e" property color m3onPrimary: "#541d34"
property color m3primaryContainer: "#6b3455" property color m3primaryContainer: "#6f334a"
property color m3onPrimaryContainer: "#ffd8ea" property color m3onPrimaryContainer: "#ffd9e3"
property color m3inversePrimary: "#864b6e" property color m3inversePrimary: "#8b4a62"
property color m3secondary: "#dfbecd" property color m3secondary: "#e2bdc7"
property color m3onSecondary: "#402a36" property color m3onSecondary: "#422932"
property color m3secondaryContainer: "#5a424f" property color m3secondaryContainer: "#5a3f48"
property color m3onSecondaryContainer: "#fcd9e9" property color m3onSecondaryContainer: "#ffd9e3"
property color m3tertiary: "#f3ba9c" property color m3tertiary: "#f0bc95"
property color m3onTertiary: "#4a2713" property color m3onTertiary: "#48290c"
property color m3tertiaryContainer: "#b8856a" property color m3tertiaryContainer: "#b58763"
property color m3onTertiaryContainer: "#000000" property color m3onTertiaryContainer: "#000000"
property color m3error: "#ffb4ab" property color m3error: "#ffb4ab"
property color m3onError: "#690005" property color m3onError: "#690005"
property color m3errorContainer: "#93000a" property color m3errorContainer: "#93000a"
property color m3onErrorContainer: "#ffdad6" property color m3onErrorContainer: "#ffdad6"
property color m3primaryFixed: "#ffd8ea" property color m3success: "#B5CCBA"
property color m3primaryFixedDim: "#fbb1d8" property color m3onSuccess: "#213528"
property color m3onPrimaryFixed: "#370728" property color m3successContainer: "#374B3E"
property color m3onPrimaryFixedVariant: "#6b3455" property color m3onSuccessContainer: "#D1E9D6"
property color m3secondaryFixed: "#fcd9e9" property color m3primaryFixed: "#ffd9e3"
property color m3secondaryFixedDim: "#dfbecd" property color m3primaryFixedDim: "#ffb0ca"
property color m3onSecondaryFixed: "#291520" property color m3onPrimaryFixed: "#39071f"
property color m3onSecondaryFixedVariant: "#58404c" property color m3onPrimaryFixedVariant: "#6f334a"
property color m3tertiaryFixed: "#ffdbca" property color m3secondaryFixed: "#ffd9e3"
property color m3tertiaryFixedDim: "#f3ba9c" property color m3secondaryFixedDim: "#e2bdc7"
property color m3onTertiaryFixed: "#311302" property color m3onSecondaryFixed: "#2b151d"
property color m3onTertiaryFixedVariant: "#653d27" property color m3onSecondaryFixedVariant: "#5a3f48"
property color term0: "#353434" property color m3tertiaryFixed: "#ffdcc3"
property color term1: "#fe45a7" property color m3tertiaryFixedDim: "#f0bc95"
property color term2: "#ffbac0" property color m3onTertiaryFixed: "#2f1500"
property color term3: "#ffdee3" property color m3onTertiaryFixedVariant: "#623f21"
property color term4: "#b3a2d5" property color m3term0: "#353434"
property color term5: "#e491bd" property color m3term1: "#ff4c8a"
property color term6: "#ffba93" property color m3term2: "#ffbbb7"
property color term7: "#edd2d5" property color m3term3: "#ffdedf"
property color term8: "#b29ea1" property color m3term4: "#b3a2d5"
property color term9: "#ff7db7" property color m3term5: "#e98fb0"
property color term10: "#ffd2d5" property color m3term6: "#ffba93"
property color term11: "#fff1f2" property color m3term7: "#eed1d2"
property color term12: "#babfdd" property color m3term8: "#b39e9e"
property color term13: "#f3a9cd" property color m3term9: "#ff80a3"
property color term14: "#ffd1c0" property color m3term10: "#ffd3d0"
property color term15: "#ffffff" property color m3term11: "#fff1f0"
property color m3term12: "#dcbc93"
property color m3term13: "#f9a8c2"
property color m3term14: "#ffd1c0"
property color m3term15: "#ffffff"
} }
} }