notifications: make popup window
This commit is contained in:
parent
02fd2e97f2
commit
ed05e9af25
12 changed files with 302 additions and 155 deletions
2
app.tsx
2
app.tsx
|
|
@ -2,6 +2,7 @@ import { execAsync, GLib, writeFileAsync } from "astal";
|
|||
import { App } from "astal/gtk3";
|
||||
import Bar from "./src/modules/bar";
|
||||
import Launcher from "./src/modules/launcher";
|
||||
import Notifications from "./src/modules/notifications";
|
||||
import NotifPopups from "./src/modules/notifpopups";
|
||||
import Osds from "./src/modules/osds";
|
||||
import Monitors from "./src/services/monitors";
|
||||
|
|
@ -24,6 +25,7 @@ App.start({
|
|||
<NotifPopups />;
|
||||
<Osds />;
|
||||
Monitors.get_default().forEach(m => <Bar monitor={m} />);
|
||||
<Notifications />;
|
||||
|
||||
console.log("Caelestia started");
|
||||
},
|
||||
|
|
|
|||
|
|
@ -15,7 +15,7 @@ $scale: 0.068rem;
|
|||
border: s($width) $style color.change($colour, $alpha: $alpha);
|
||||
}
|
||||
|
||||
@mixin shadow($colour: scheme.$mantle, $alpha: 0.4, $x: 2, $y: 3, $blur: 8, $spread: 0) {
|
||||
@mixin shadow($colour: black, $alpha: 0.4, $x: 2, $y: 3, $blur: 8, $spread: 0) {
|
||||
box-shadow: s($x) s($y) s($blur) s($spread) color.change($colour, $alpha: $alpha);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -92,7 +92,7 @@
|
|||
color: scheme.$blue;
|
||||
}
|
||||
|
||||
.notifications {
|
||||
.unread {
|
||||
color: scheme.$mauve;
|
||||
}
|
||||
|
||||
|
|
|
|||
101
scss/notifications.scss
Normal file
101
scss/notifications.scss
Normal file
|
|
@ -0,0 +1,101 @@
|
|||
@use "sass:color";
|
||||
@use "scheme";
|
||||
@use "lib";
|
||||
@use "font";
|
||||
|
||||
@mixin popup($accent) {
|
||||
.separator {
|
||||
background-color: $accent;
|
||||
}
|
||||
|
||||
.image {
|
||||
@include lib.border($accent, 0.05);
|
||||
}
|
||||
}
|
||||
|
||||
.notifications {
|
||||
@include lib.rounded(8);
|
||||
@include lib.border(scheme.$mauve, 0.4, 2);
|
||||
@include lib.shadow;
|
||||
|
||||
min-width: lib.s(400);
|
||||
min-height: lib.s(600);
|
||||
background-color: scheme.$base;
|
||||
color: scheme.$mauve;
|
||||
padding: lib.s(10) lib.s(12);
|
||||
|
||||
.header {
|
||||
@include font.mono;
|
||||
@include lib.spacing(8);
|
||||
|
||||
padding: 0 lib.s(5);
|
||||
margin-bottom: lib.s(8);
|
||||
|
||||
button {
|
||||
@include lib.rounded(5);
|
||||
@include lib.element-decel;
|
||||
|
||||
padding: lib.s(3) lib.s(8);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: scheme.$surface0;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: scheme.$surface1;
|
||||
}
|
||||
|
||||
&.enabled {
|
||||
background-color: scheme.$mauve;
|
||||
color: scheme.$base;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: color.mix(scheme.$mauve, scheme.$base, 80%);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: color.mix(scheme.$mauve, scheme.$base, 70%);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.notification {
|
||||
.wrapper {
|
||||
padding-bottom: lib.s(10);
|
||||
}
|
||||
|
||||
.inner {
|
||||
background-color: color.mix(scheme.$surface0, scheme.$base, 70%);
|
||||
|
||||
&.low {
|
||||
@include popup(scheme.$overlay0);
|
||||
}
|
||||
|
||||
&.normal {
|
||||
@include lib.border(scheme.$lavender, 0.3);
|
||||
@include popup(scheme.$lavender);
|
||||
}
|
||||
|
||||
&.critical {
|
||||
@include lib.border(scheme.$red, 0.5);
|
||||
@include popup(scheme.$red);
|
||||
}
|
||||
}
|
||||
|
||||
.actions > * {
|
||||
background-color: scheme.$surface1;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: scheme.$surface2;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: scheme.$overlay0;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -21,86 +21,22 @@
|
|||
min-width: lib.s(410);
|
||||
padding-left: lib.s(10); // So notifications can overshoot for init animation
|
||||
|
||||
.wrapper {
|
||||
padding-top: lib.s(10);
|
||||
}
|
||||
|
||||
.notification {
|
||||
@include lib.rounded(8, $tr: 0, $br: 0);
|
||||
@include lib.shadow;
|
||||
@include font.main;
|
||||
@include popup(scheme.$lavender);
|
||||
|
||||
background-color: scheme.$base;
|
||||
color: scheme.$text;
|
||||
padding: lib.s(10) lib.s(12);
|
||||
|
||||
@include lib.spacing($vertical: true);
|
||||
|
||||
&.low {
|
||||
@include popup(scheme.$overlay0);
|
||||
.wrapper {
|
||||
padding-top: lib.s(10);
|
||||
}
|
||||
|
||||
&.critical {
|
||||
@include popup(scheme.$red);
|
||||
}
|
||||
}
|
||||
.inner {
|
||||
@include lib.rounded(8, $tr: 0, $br: 0);
|
||||
@include lib.shadow;
|
||||
@include popup(scheme.$lavender);
|
||||
|
||||
.header,
|
||||
.content {
|
||||
padding: 0 lib.s(5);
|
||||
}
|
||||
|
||||
.header {
|
||||
@include font.mono;
|
||||
@include lib.spacing(5);
|
||||
}
|
||||
|
||||
.content {
|
||||
@include lib.spacing(10);
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
font-size: lib.s(18);
|
||||
}
|
||||
|
||||
.image {
|
||||
@include lib.rounded(10);
|
||||
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
min-width: lib.s(64);
|
||||
min-height: lib.s(64);
|
||||
margin-top: lib.s(3);
|
||||
}
|
||||
|
||||
.summary {
|
||||
@include font.title;
|
||||
|
||||
font-size: lib.s(16);
|
||||
}
|
||||
|
||||
.body {
|
||||
font-size: lib.s(14);
|
||||
}
|
||||
|
||||
.actions {
|
||||
@include lib.spacing;
|
||||
|
||||
& > * {
|
||||
@include lib.rounded(5);
|
||||
@include lib.element-decel;
|
||||
|
||||
padding: lib.s(5) lib.s(10);
|
||||
background-color: scheme.$surface0;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: scheme.$surface1;
|
||||
&.low {
|
||||
@include popup(scheme.$overlay0);
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: scheme.$surface2;
|
||||
&.critical {
|
||||
@include popup(scheme.$red);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -17,6 +17,78 @@ label.icon {
|
|||
@include font.icon;
|
||||
}
|
||||
|
||||
.notification {
|
||||
.inner {
|
||||
@include lib.rounded(8);
|
||||
@include font.main;
|
||||
|
||||
background-color: scheme.$base;
|
||||
color: scheme.$text;
|
||||
padding: lib.s(10) lib.s(12);
|
||||
|
||||
@include lib.spacing($vertical: true);
|
||||
}
|
||||
|
||||
.header,
|
||||
.content {
|
||||
padding: 0 lib.s(5);
|
||||
}
|
||||
|
||||
.header {
|
||||
@include font.mono;
|
||||
@include lib.spacing(8);
|
||||
}
|
||||
|
||||
.content {
|
||||
@include lib.spacing(10);
|
||||
}
|
||||
|
||||
.app-icon {
|
||||
font-size: lib.s(18);
|
||||
}
|
||||
|
||||
.image {
|
||||
@include lib.rounded(10);
|
||||
|
||||
background-size: cover;
|
||||
background-position: center;
|
||||
min-width: lib.s(64);
|
||||
min-height: lib.s(64);
|
||||
margin-top: lib.s(3);
|
||||
}
|
||||
|
||||
.summary {
|
||||
@include font.title;
|
||||
|
||||
font-size: lib.s(16);
|
||||
}
|
||||
|
||||
.body {
|
||||
font-size: lib.s(14);
|
||||
}
|
||||
|
||||
.actions {
|
||||
@include lib.spacing;
|
||||
|
||||
& > * {
|
||||
@include lib.rounded(5);
|
||||
@include lib.element-decel;
|
||||
|
||||
padding: lib.s(5) lib.s(10);
|
||||
background-color: scheme.$surface0;
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
background-color: scheme.$surface1;
|
||||
}
|
||||
|
||||
&:active {
|
||||
background-color: scheme.$surface2;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
separator,
|
||||
.separator {
|
||||
@include lib.rounded(2);
|
||||
|
|
@ -112,9 +184,6 @@ tooltip {
|
|||
|
||||
scrollbar {
|
||||
trough {
|
||||
@include lib.rounded(1000);
|
||||
|
||||
min-width: lib.s(12);
|
||||
background-color: transparent;
|
||||
}
|
||||
|
||||
|
|
@ -122,12 +191,13 @@ scrollbar {
|
|||
@include lib.rounded(1000);
|
||||
@include lib.element-decel;
|
||||
|
||||
min-width: lib.s(6);
|
||||
min-width: lib.s(3);
|
||||
min-height: lib.s(30);
|
||||
background-color: color.change(scheme.$overlay0, $alpha: 0.3);
|
||||
|
||||
&:hover,
|
||||
&:focus {
|
||||
min-width: lib.s(6);
|
||||
background-color: color.change(scheme.$overlay0, $alpha: 0.4);
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -15,6 +15,7 @@ import { getAppCategoryIcon } from "../utils/icons";
|
|||
import { ellipsize } from "../utils/strings";
|
||||
import { osIcon } from "../utils/system";
|
||||
import { setupCustomTooltip } from "../utils/widgets";
|
||||
import type PopupWindow from "../widgets/popupwindow";
|
||||
|
||||
const hyprland = AstalHyprland.get_default();
|
||||
|
||||
|
|
@ -385,11 +386,19 @@ const PkgUpdates = () => (
|
|||
</box>
|
||||
);
|
||||
|
||||
const Notifications = () => {
|
||||
const Unread = () => {
|
||||
const unreadCount = Variable(0);
|
||||
return (
|
||||
<box
|
||||
className="module notifications"
|
||||
<button
|
||||
onClick={(self, event) => {
|
||||
if (event.button === Astal.MouseButton.PRIMARY) {
|
||||
const popup = App.get_window("notifications") as PopupWindow | null;
|
||||
if (popup) {
|
||||
if (popup.visible) popup.hide();
|
||||
else popup.popup_at_widget(self, event);
|
||||
}
|
||||
} else if (event.button === Astal.MouseButton.SECONDARY) unreadCount.set(0);
|
||||
}}
|
||||
setup={self =>
|
||||
setupCustomTooltip(
|
||||
self,
|
||||
|
|
@ -397,39 +406,37 @@ const Notifications = () => {
|
|||
)
|
||||
}
|
||||
>
|
||||
<label className="icon" label="info" />
|
||||
<label
|
||||
label="0"
|
||||
setup={self => {
|
||||
const notifd = AstalNotifd.get_default();
|
||||
let notifsOpen = false;
|
||||
let unread = new Set<number>();
|
||||
<box className="module unread">
|
||||
<label className="icon" label="info" />
|
||||
<label
|
||||
label={bind(unreadCount).as(String)}
|
||||
setup={self => {
|
||||
const notifd = AstalNotifd.get_default();
|
||||
let notifsOpen = false;
|
||||
let unread = new Set<number>();
|
||||
self.hook(unreadCount, (_, u) => u === 0 && unread.clear());
|
||||
|
||||
self.hook(notifd, "notified", (self, id, replaced) => {
|
||||
if (!notifsOpen && !replaced) {
|
||||
unread.add(id);
|
||||
unreadCount.set(unread.size);
|
||||
self.label = String(unread.size);
|
||||
}
|
||||
});
|
||||
self.hook(notifd, "resolved", (self, id) => {
|
||||
if (unread.delete(id)) {
|
||||
unreadCount.set(unread.size);
|
||||
self.label = String(unread.size);
|
||||
}
|
||||
});
|
||||
self.hook(App, "window-toggled", (_, window) => {
|
||||
if (window.name === "notifications") {
|
||||
notifsOpen = window.visible;
|
||||
if (notifsOpen) {
|
||||
unread.clear();
|
||||
unreadCount.set(0);
|
||||
self.hook(notifd, "notified", (_, id, replaced) => {
|
||||
if (!notifsOpen && !replaced) {
|
||||
unread.add(id);
|
||||
unreadCount.set(unread.size);
|
||||
}
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</box>
|
||||
});
|
||||
self.hook(notifd, "resolved", (_, id) => {
|
||||
if (unread.delete(id)) {
|
||||
unreadCount.set(unread.size);
|
||||
}
|
||||
});
|
||||
self.hook(App, "window-toggled", (_, window) => {
|
||||
if (window.name === "notifications") {
|
||||
notifsOpen = window.visible;
|
||||
if (notifsOpen) unreadCount.set(0);
|
||||
}
|
||||
});
|
||||
}}
|
||||
/>
|
||||
</box>
|
||||
</button>
|
||||
);
|
||||
};
|
||||
|
||||
|
|
@ -491,7 +498,7 @@ export default ({ monitor }: { monitor: Monitor }) => (
|
|||
<Tray />
|
||||
<StatusIcons />
|
||||
<PkgUpdates />
|
||||
<Notifications />
|
||||
<Unread />
|
||||
<DateTime />
|
||||
<Power />
|
||||
</box>
|
||||
|
|
|
|||
|
|
@ -1,6 +1,8 @@
|
|||
import { Gtk } from "astal/gtk3";
|
||||
import { bind } from "astal";
|
||||
import { Astal, Gtk } from "astal/gtk3";
|
||||
import AstalNotifd from "gi://AstalNotifd";
|
||||
import { PopupWindow, setupChildClickthrough } from "../utils/widgets";
|
||||
import Notification from "../widgets/notification";
|
||||
import PopupWindow from "../widgets/popupwindow";
|
||||
|
||||
const List = () => (
|
||||
<box
|
||||
|
|
@ -9,49 +11,63 @@ const List = () => (
|
|||
className="list"
|
||||
setup={self => {
|
||||
const notifd = AstalNotifd.get_default();
|
||||
const map = new Map<number, NotifPopup>();
|
||||
self.hook(notifd, "notified", (self, id) => {
|
||||
const notification = notifd.get_notification(id);
|
||||
const map = new Map<number, Notification>();
|
||||
|
||||
const popup = (<NotifPopup notification={notification} />) as NotifPopup;
|
||||
popup.connect("destroy", () => map.get(notification.id) === popup && map.delete(notification.id));
|
||||
const addNotification = (notification: AstalNotifd.Notification) => {
|
||||
const notif = (<Notification notification={notification} />) as Notification;
|
||||
notif.connect("destroy", () => map.get(notification.id) === notif && map.delete(notification.id));
|
||||
map.get(notification.id)?.destroyWithAnims();
|
||||
map.set(notification.id, popup);
|
||||
map.set(notification.id, notif);
|
||||
|
||||
self.add(
|
||||
self.pack_end(
|
||||
<eventbox
|
||||
// Dismiss on middle click
|
||||
onClick={(_, event) => event.button === Astal.MouseButton.MIDDLE && notification.dismiss()}
|
||||
// Close on hover lost
|
||||
onHoverLost={() => popup.destroyWithAnims()}
|
||||
>
|
||||
{popup}
|
||||
</eventbox>
|
||||
{notif}
|
||||
</eventbox>,
|
||||
false,
|
||||
false,
|
||||
0
|
||||
);
|
||||
};
|
||||
|
||||
// Limit number of popups
|
||||
if (config.maxPopups > 0 && self.children.length > config.maxPopups)
|
||||
map.values().next().value?.destroyWithAnims();
|
||||
});
|
||||
notifd
|
||||
.get_notifications()
|
||||
.sort((a, b) => a.time - b.time)
|
||||
.forEach(addNotification);
|
||||
|
||||
self.hook(notifd, "notified", (_, id) => addNotification(notifd.get_notification(id)));
|
||||
self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims());
|
||||
|
||||
// Change input region to child region so can click through empty space
|
||||
setupChildClickthrough(self);
|
||||
}}
|
||||
/>
|
||||
);
|
||||
|
||||
export default class Notifications extends PopupWindow {
|
||||
constructor() {
|
||||
super({
|
||||
name: "notifications",
|
||||
child: (
|
||||
<box>
|
||||
<List />
|
||||
</box>
|
||||
),
|
||||
});
|
||||
|
||||
setupChildClickthrough(self);
|
||||
}
|
||||
}
|
||||
export default () => (
|
||||
<PopupWindow name="notifications">
|
||||
<box vertical className="notifications">
|
||||
<box className="header">
|
||||
<label
|
||||
label={bind(AstalNotifd.get_default(), "notifications").as(
|
||||
n => `${n.length} notification${n.length === 1 ? "" : "s"}`
|
||||
)}
|
||||
/>
|
||||
<box hexpand />
|
||||
<button
|
||||
cursor="pointer"
|
||||
onClicked={() => (AstalNotifd.get_default().dontDisturb = !AstalNotifd.get_default().dontDisturb)}
|
||||
label="Silence"
|
||||
className={bind(AstalNotifd.get_default(), "dontDisturb").as(d => (d ? "enabled" : ""))}
|
||||
/>
|
||||
<button
|
||||
cursor="pointer"
|
||||
onClicked={() => AstalNotifd.get_default().notifications.forEach(n => n.dismiss())}
|
||||
label="Clear"
|
||||
/>
|
||||
</box>
|
||||
<scrollable expand hscroll={Gtk.PolicyType.NEVER}>
|
||||
<List />
|
||||
</scrollable>
|
||||
</box>
|
||||
</PopupWindow>
|
||||
);
|
||||
|
|
|
|||
|
|
@ -1,4 +1,4 @@
|
|||
import { Astal, Gtk } from "astal/gtk3";
|
||||
import { App, Astal, Gtk } from "astal/gtk3";
|
||||
import AstalNotifd from "gi://AstalNotifd";
|
||||
import { notifpopups as config } from "../../config";
|
||||
import { setupChildClickthrough } from "../utils/widgets";
|
||||
|
|
@ -16,7 +16,11 @@ export default () => (
|
|||
setup={self => {
|
||||
const notifd = AstalNotifd.get_default();
|
||||
const map = new Map<number, Notification>();
|
||||
let notifsOpen = false;
|
||||
|
||||
self.hook(notifd, "notified", (self, id) => {
|
||||
if (notifsOpen) return;
|
||||
|
||||
const notification = notifd.get_notification(id);
|
||||
|
||||
const popup = (<Notification popup notification={notification} />) as Notification;
|
||||
|
|
@ -41,6 +45,10 @@ export default () => (
|
|||
});
|
||||
self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims());
|
||||
|
||||
self.hook(App, "window-toggled", (_, window) => {
|
||||
if (window.name === "notifications") notifsOpen = window.visible;
|
||||
});
|
||||
|
||||
// Change input region to child region so can click through empty space
|
||||
setupChildClickthrough(self);
|
||||
}}
|
||||
|
|
|
|||
|
|
@ -57,12 +57,16 @@ export default class Notification extends Widget.Box {
|
|||
#destroyed = false;
|
||||
|
||||
constructor({ notification, popup }: { notification: AstalNotifd.Notification; popup?: boolean }) {
|
||||
super();
|
||||
super({ className: "notification" });
|
||||
|
||||
this.#revealer = (
|
||||
<revealer revealChild transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN} transitionDuration={150}>
|
||||
<revealer
|
||||
revealChild={popup}
|
||||
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
|
||||
transitionDuration={150}
|
||||
>
|
||||
<box className="wrapper">
|
||||
<box vertical className={`notification ${urgencyToString(notification.urgency)}`}>
|
||||
<box vertical className={`inner ${urgencyToString(notification.urgency)}`}>
|
||||
<box className="header">
|
||||
<AppIcon appIcon={notification.appIcon} desktopEntry={notification.appName} />
|
||||
<label className="app-name" label={notification.appName ?? "Unknown"} />
|
||||
|
|
@ -101,6 +105,7 @@ export default class Notification extends Widget.Box {
|
|||
const width = this.get_preferred_width()[1];
|
||||
this.css = `margin-left: ${width}px; margin-right: -${width}px;`;
|
||||
timeout(1, () => {
|
||||
this.#revealer.revealChild = true;
|
||||
this.css = `transition: 300ms cubic-bezier(0.05, 0.9, 0.1, 1.1); margin-left: 0; margin-right: 0;`;
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -28,10 +28,11 @@ export default class PopupWindow extends Widget.Window {
|
|||
});
|
||||
}
|
||||
|
||||
popup_at_widget(widget: JSX.Element, event: Gdk.Event) {
|
||||
popup_at_widget(widget: JSX.Element, event: Gdk.Event | Astal.ClickEvent) {
|
||||
const { width, height } = widget.get_allocation();
|
||||
const [_, x, y] = event.get_coords();
|
||||
const [_, x, y] = event instanceof Gdk.Event ? event.get_coords() : [null, event.x, event.y];
|
||||
const { x: cx, y: cy } = AstalHyprland.get_default().get_cursor_position();
|
||||
this.anchor = Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT;
|
||||
this.marginLeft = cx + ((width - this.get_preferred_width()[1]) / 2 - x);
|
||||
this.marginTop = cy + (height - y);
|
||||
this.show();
|
||||
|
|
|
|||
|
|
@ -6,6 +6,7 @@
|
|||
@use "scss/notifpopups";
|
||||
@use "scss/launcher";
|
||||
@use "scss/osds";
|
||||
@use "scss/notifications";
|
||||
|
||||
* {
|
||||
all: unset; // Remove GTK theme styles
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue