diff options
| -rw-r--r-- | app.tsx | 2 | ||||
| -rw-r--r-- | scss/_lib.scss | 2 | ||||
| -rw-r--r-- | scss/bar.scss | 2 | ||||
| -rw-r--r-- | scss/notifications.scss | 101 | ||||
| -rw-r--r-- | scss/notifpopups.scss | 84 | ||||
| -rw-r--r-- | scss/widgets.scss | 78 | ||||
| -rw-r--r-- | src/modules/bar.tsx | 77 | ||||
| -rw-r--r-- | src/modules/notifications.tsx | 84 | ||||
| -rw-r--r-- | src/modules/notifpopups.tsx | 10 | ||||
| -rw-r--r-- | src/widgets/notification.tsx | 11 | ||||
| -rw-r--r-- | src/widgets/popupwindow.ts | 5 | ||||
| -rw-r--r-- | style.scss | 1 |
12 files changed, 302 insertions, 155 deletions
@@ -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"); }, diff --git a/scss/_lib.scss b/scss/_lib.scss index e12140f..d2ad3d0 100644 --- a/scss/_lib.scss +++ b/scss/_lib.scss @@ -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); } diff --git a/scss/bar.scss b/scss/bar.scss index ab75b00..1d73319 100644 --- a/scss/bar.scss +++ b/scss/bar.scss @@ -92,7 +92,7 @@ color: scheme.$blue; } - .notifications { + .unread { color: scheme.$mauve; } diff --git a/scss/notifications.scss b/scss/notifications.scss new file mode 100644 index 0000000..51f1a0b --- /dev/null +++ b/scss/notifications.scss @@ -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; + } + } + } +} diff --git a/scss/notifpopups.scss b/scss/notifpopups.scss index 25e52e1..cec24a4 100644 --- a/scss/notifpopups.scss +++ b/scss/notifpopups.scss @@ -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); - } - - &.critical { - @include popup(scheme.$red); + .wrapper { + padding-top: lib.s(10); } - } - - .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; + .inner { + @include lib.rounded(8, $tr: 0, $br: 0); + @include lib.shadow; + @include popup(scheme.$lavender); - &:hover, - &:focus { - background-color: scheme.$surface1; + &.low { + @include popup(scheme.$overlay0); } - &:active { - background-color: scheme.$surface2; + &.critical { + @include popup(scheme.$red); } } } diff --git a/scss/widgets.scss b/scss/widgets.scss index 39ab490..e54a117 100644 --- a/scss/widgets.scss +++ b/scss/widgets.scss @@ -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); } diff --git a/src/modules/bar.tsx b/src/modules/bar.tsx index b56d94a..aeb6e42 100644 --- a/src/modules/bar.tsx +++ b/src/modules/bar.tsx @@ -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> diff --git a/src/modules/notifications.tsx b/src/modules/notifications.tsx index 66188a1..ea98ada 100644 --- a/src/modules/notifications.tsx +++ b/src/modules/notifications.tsx @@ -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(); - }); - self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims()); + notifd + .get_notifications() + .sort((a, b) => a.time - b.time) + .forEach(addNotification); - // Change input region to child region so can click through empty space - setupChildClickthrough(self); + self.hook(notifd, "notified", (_, id) => addNotification(notifd.get_notification(id))); + self.hook(notifd, "resolved", (_, id) => map.get(id)?.destroyWithAnims()); }} /> ); -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> +); diff --git a/src/modules/notifpopups.tsx b/src/modules/notifpopups.tsx index 5da3092..9e34549 100644 --- a/src/modules/notifpopups.tsx +++ b/src/modules/notifpopups.tsx @@ -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); }} diff --git a/src/widgets/notification.tsx b/src/widgets/notification.tsx index 0bef5ca..adac831 100644 --- a/src/widgets/notification.tsx +++ b/src/widgets/notification.tsx @@ -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;`; }); diff --git a/src/widgets/popupwindow.ts b/src/widgets/popupwindow.ts index 67aa0ff..9f5192e 100644 --- a/src/widgets/popupwindow.ts +++ b/src/widgets/popupwindow.ts @@ -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 |