diff --git a/app.tsx b/app.tsx index be3231c9..31e7ed16 100644 --- a/app.tsx +++ b/app.tsx @@ -1,11 +1,11 @@ import { execAsync, GLib, writeFileAsync } from "astal"; import { App } from "astal/gtk3"; -import Bar from "./modules/bar"; -import Launcher from "./modules/launcher"; -import NotifPopups from "./modules/notifpopups"; -import Osds from "./modules/osds"; -import Monitors from "./services/monitors"; -import Players from "./services/players"; +import Bar from "./src/modules/bar"; +import Launcher from "./src/modules/launcher"; +import NotifPopups from "./src/modules/notifpopups"; +import Osds from "./src/modules/osds"; +import Monitors from "./src/services/monitors"; +import Players from "./src/services/players"; const loadStyleAsync = async () => { if (!GLib.file_test(`${SRC}/scss/scheme/_index.scss`, GLib.FileTest.EXISTS)) diff --git a/scss/notifpopups.scss b/scss/notifpopups.scss index 325cb7bc..25e52e13 100644 --- a/scss/notifpopups.scss +++ b/scss/notifpopups.scss @@ -25,7 +25,7 @@ padding-top: lib.s(10); } - .popup { + .notification { @include lib.rounded(8, $tr: 0, $br: 0); @include lib.shadow; @include font.main; diff --git a/modules/bar.tsx b/src/modules/bar.tsx similarity index 99% rename from modules/bar.tsx rename to src/modules/bar.tsx index 5ed309e4..b56d94a7 100644 --- a/modules/bar.tsx +++ b/src/modules/bar.tsx @@ -7,7 +7,7 @@ import AstalNetwork from "gi://AstalNetwork"; import AstalNotifd from "gi://AstalNotifd"; import AstalTray from "gi://AstalTray"; import AstalWp01 from "gi://AstalWp"; -import { bar as config } from "../config"; +import { bar as config } from "../../config"; import type { Monitor } from "../services/monitors"; import Players from "../services/players"; import Updates from "../services/updates"; diff --git a/modules/launcher.tsx b/src/modules/launcher.tsx similarity index 98% rename from modules/launcher.tsx rename to src/modules/launcher.tsx index 966cdfdd..2fc6eefb 100644 --- a/modules/launcher.tsx +++ b/src/modules/launcher.tsx @@ -3,12 +3,13 @@ import { Astal, Gtk, Widget } from "astal/gtk3"; import fuzzysort from "fuzzysort"; import type AstalApps from "gi://AstalApps"; import AstalHyprland from "gi://AstalHyprland"; -import { launcher as config } from "../config"; +import { launcher as config } from "../../config"; import { Apps } from "../services/apps"; import Math, { type HistoryItem } from "../services/math"; import { getAppCategoryIcon } from "../utils/icons"; import { launch } from "../utils/system"; -import { PopupWindow, setupCustomTooltip } from "../utils/widgets"; +import { setupCustomTooltip } from "../utils/widgets"; +import PopupWindow from "../widgets/popupwindow"; type Mode = "apps" | "files" | "math"; diff --git a/src/modules/notifications.tsx b/src/modules/notifications.tsx new file mode 100644 index 00000000..66188a1d --- /dev/null +++ b/src/modules/notifications.tsx @@ -0,0 +1,57 @@ +import { Gtk } from "astal/gtk3"; +import AstalNotifd from "gi://AstalNotifd"; +import { PopupWindow, setupChildClickthrough } from "../utils/widgets"; + +const List = () => ( + { + const notifd = AstalNotifd.get_default(); + const map = new Map(); + self.hook(notifd, "notified", (self, id) => { + const notification = notifd.get_notification(id); + + const popup = () as NotifPopup; + popup.connect("destroy", () => map.get(notification.id) === popup && map.delete(notification.id)); + map.get(notification.id)?.destroyWithAnims(); + map.set(notification.id, popup); + + self.add( + event.button === Astal.MouseButton.MIDDLE && notification.dismiss()} + // Close on hover lost + onHoverLost={() => popup.destroyWithAnims()} + > + {popup} + + ); + + // 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()); + + // 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: ( + + + + ), + }); + + setupChildClickthrough(self); + } +} diff --git a/src/modules/notifpopups.tsx b/src/modules/notifpopups.tsx new file mode 100644 index 00000000..5da3092a --- /dev/null +++ b/src/modules/notifpopups.tsx @@ -0,0 +1,49 @@ +import { Astal, Gtk } from "astal/gtk3"; +import AstalNotifd from "gi://AstalNotifd"; +import { notifpopups as config } from "../../config"; +import { setupChildClickthrough } from "../utils/widgets"; +import Notification from "../widgets/notification"; + +export default () => ( + + { + const notifd = AstalNotifd.get_default(); + const map = new Map(); + self.hook(notifd, "notified", (self, id) => { + const notification = notifd.get_notification(id); + + const popup = () as Notification; + popup.connect("destroy", () => map.get(notification.id) === popup && map.delete(notification.id)); + map.get(notification.id)?.destroyWithAnims(); + map.set(notification.id, popup); + + self.add( + event.button === Astal.MouseButton.MIDDLE && notification.dismiss()} + // Close on hover lost + onHoverLost={() => popup.destroyWithAnims()} + > + {popup} + + ); + + // 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()); + + // Change input region to child region so can click through empty space + setupChildClickthrough(self); + }} + /> + +); diff --git a/modules/osds.tsx b/src/modules/osds.tsx similarity index 99% rename from modules/osds.tsx rename to src/modules/osds.tsx index b6e1333d..a87fcc31 100644 --- a/modules/osds.tsx +++ b/src/modules/osds.tsx @@ -5,9 +5,9 @@ import AstalWp from "gi://AstalWp"; import Cairo from "gi://cairo"; import Pango from "gi://Pango"; import PangoCairo from "gi://PangoCairo"; -import { osds as config } from "../config"; +import { osds as config } from "../../config"; import Monitors, { type Monitor } from "../services/monitors"; -import { PopupWindow } from "../utils/widgets"; +import PopupWindow from "../widgets/popupwindow"; const getStyle = (context: Gtk.StyleContext, prop: string) => context.get_property(prop, Gtk.StateFlags.NORMAL); const getNumStyle = (context: Gtk.StyleContext, prop: string) => getStyle(context, prop) as number; diff --git a/services/apps.ts b/src/services/apps.ts similarity index 100% rename from services/apps.ts rename to src/services/apps.ts diff --git a/services/math.ts b/src/services/math.ts similarity index 100% rename from services/math.ts rename to src/services/math.ts diff --git a/services/monitors.ts b/src/services/monitors.ts similarity index 100% rename from services/monitors.ts rename to src/services/monitors.ts diff --git a/services/players.ts b/src/services/players.ts similarity index 100% rename from services/players.ts rename to src/services/players.ts diff --git a/services/updates.ts b/src/services/updates.ts similarity index 98% rename from services/updates.ts rename to src/services/updates.ts index 0b04e854..5bb6bd13 100644 --- a/services/updates.ts +++ b/src/services/updates.ts @@ -1,5 +1,5 @@ import { execAsync, GLib, GObject, property, readFileAsync, register, writeFileAsync } from "astal"; -import { updates as config } from "../config"; +import { updates as config } from "../../config"; interface Update { name: string; diff --git a/utils/icons.ts b/src/utils/icons.ts similarity index 100% rename from utils/icons.ts rename to src/utils/icons.ts diff --git a/utils/mpris.ts b/src/utils/mpris.ts similarity index 100% rename from utils/mpris.ts rename to src/utils/mpris.ts diff --git a/utils/strings.ts b/src/utils/strings.ts similarity index 100% rename from utils/strings.ts rename to src/utils/strings.ts diff --git a/utils/system.ts b/src/utils/system.ts similarity index 100% rename from utils/system.ts rename to src/utils/system.ts diff --git a/src/utils/widgets.ts b/src/utils/widgets.ts new file mode 100644 index 00000000..08f97407 --- /dev/null +++ b/src/utils/widgets.ts @@ -0,0 +1,45 @@ +import { Binding } from "astal"; +import { Astal, Widget } from "astal/gtk3"; +import AstalHyprland from "gi://AstalHyprland"; + +export const setupCustomTooltip = (self: any, text: string | Binding) => { + if (!text) return null; + + const window = new Widget.Window({ + visible: false, + namespace: "caelestia-tooltip", + keymode: Astal.Keymode.NONE, + exclusivity: Astal.Exclusivity.IGNORE, + anchor: Astal.WindowAnchor.TOP | Astal.WindowAnchor.LEFT, + child: new Widget.Label({ className: "tooltip", label: text }), + }); + self.set_tooltip_window(window); + + let dirty = true; + let lastX = 0; + self.connect("size-allocate", () => (dirty = true)); + window.connect("size-allocate", () => { + window.marginLeft = lastX + (self.get_allocated_width() - window.get_preferred_width()[1]) / 2; + }); + if (text instanceof Binding) self.hook(text, (_: any, v: string) => !v && window.hide()); + + self.connect("query-tooltip", (_: any, x: number, y: number) => { + if (text instanceof Binding && !text.get()) return false; + if (dirty) { + const { width, height } = self.get_allocation(); + const { x: cx, y: cy } = AstalHyprland.get_default().get_cursor_position(); + window.marginLeft = cx + ((width - window.get_preferred_width()[1]) / 2 - x); + window.marginTop = cy + (height - y); + lastX = cx - x; + dirty = false; + } + return true; + }); + + self.connect("destroy", () => window.destroy()); + + return window; +}; + +export const setupChildClickthrough = (self: any) => + self.connect("size-allocate", () => self.get_window()?.set_child_input_shapes()); diff --git a/modules/notifpopups.tsx b/src/widgets/notification.tsx similarity index 70% rename from modules/notifpopups.tsx rename to src/widgets/notification.tsx index c3441a9b..0bef5ca6 100644 --- a/modules/notifpopups.tsx +++ b/src/widgets/notification.tsx @@ -1,9 +1,8 @@ import { GLib, register, timeout } from "astal"; import { Astal, Gtk, Widget } from "astal/gtk3"; import AstalNotifd from "gi://AstalNotifd"; -import { notifpopups as config } from "../config"; +import { notifpopups as config } from "../../config"; import { desktopEntrySubs } from "../utils/icons"; -import { setupChildClickthrough } from "../utils/widgets"; const urgencyToString = (urgency: AstalNotifd.Urgency) => { switch (urgency) { @@ -53,17 +52,17 @@ const Image = ({ icon }: { icon: string }) => { }; @register() -class NotifPopup extends Widget.Box { +export default class Notification extends Widget.Box { readonly #revealer; #destroyed = false; - constructor({ notification }: { notification: AstalNotifd.Notification }) { + constructor({ notification, popup }: { notification: AstalNotifd.Notification; popup?: boolean }) { super(); this.#revealer = ( - +