summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
author2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-03-25 12:59:51 +1100
committer2 * r + 2 * t <61896496+soramanew@users.noreply.github.com>2025-03-25 12:59:51 +1100
commitfe978092e8c13b337eb4e58b9b08b9ea5cc93413 (patch)
tree3138398d1a9386f858948d97eb33a69c0a9ea4c1
parentnotifpopups: destroy event wrapper (diff)
downloadcaelestia-shell-fe978092e8c13b337eb4e58b9b08b9ea5cc93413.tar.gz
caelestia-shell-fe978092e8c13b337eb4e58b9b08b9ea5cc93413.tar.bz2
caelestia-shell-fe978092e8c13b337eb4e58b9b08b9ea5cc93413.zip
sidebar: create dashboard
-rw-r--r--app.tsx2
-rw-r--r--scss/common.scss21
-rw-r--r--scss/notifpopups.scss8
-rw-r--r--scss/sidebar.scss221
-rw-r--r--src/modules/sidebar/dashboard.tsx123
-rw-r--r--src/modules/sidebar/index.tsx34
-rw-r--r--src/modules/sidebar/modules/hwresources.tsx67
-rw-r--r--src/modules/sidebar/modules/notifications.tsx70
-rw-r--r--src/services/calendar.ts0
-rw-r--r--src/widgets/notification.tsx26
-rw-r--r--src/widgets/slider.tsx53
-rw-r--r--style.scss2
12 files changed, 592 insertions, 35 deletions
diff --git a/app.tsx b/app.tsx
index ad9693e..5d862ca 100644
--- a/app.tsx
+++ b/app.tsx
@@ -4,6 +4,7 @@ import NotifPopups from "@/modules/notifpopups";
import Osds from "@/modules/osds";
import Popdowns from "@/modules/popdowns";
import Session from "@/modules/session";
+import SideBar from "@/modules/sidebar";
import Monitors from "@/services/monitors";
import Palette from "@/services/palette";
import Players from "@/services/players";
@@ -73,6 +74,7 @@ App.start({
<NotifPopups />;
<Osds />;
<Session />;
+ Monitors.get_default().forEach(m => <SideBar monitor={m} />);
Monitors.get_default().forEach(m => <Bar monitor={m} />);
<Popdowns />;
diff --git a/scss/common.scss b/scss/common.scss
index 88f3326..1cf7249 100644
--- a/scss/common.scss
+++ b/scss/common.scss
@@ -61,25 +61,4 @@ label.icon {
font-size: lib.s(14);
color: scheme.$subtext0;
}
-
- .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;
- }
- }
- }
}
diff --git a/scss/notifpopups.scss b/scss/notifpopups.scss
index c4760b7..89e5eea 100644
--- a/scss/notifpopups.scss
+++ b/scss/notifpopups.scss
@@ -3,7 +3,7 @@
@use "lib";
@use "font";
-@mixin popup($colour, $alpha) {
+@mixin popup($colour) {
.separator {
background-color: $colour;
}
@@ -29,16 +29,16 @@
@include lib.shadow;
&.low {
- @include popup(scheme.$overlay0, 0.3);
+ @include popup(scheme.$overlay0);
}
&.normal {
- @include popup(scheme.$primary, 0.3);
+ @include popup(scheme.$primary);
}
&.critical {
@include lib.border(scheme.$error, 0.5);
- @include popup(scheme.$error, 0.8);
+ @include popup(scheme.$error);
}
}
}
diff --git a/scss/sidebar.scss b/scss/sidebar.scss
new file mode 100644
index 0000000..84d1d61
--- /dev/null
+++ b/scss/sidebar.scss
@@ -0,0 +1,221 @@
+@use "sass:color";
+@use "scheme";
+@use "lib";
+@use "font";
+
+@mixin notification($accent) {
+ .separator {
+ background-color: $accent;
+ }
+
+ .image {
+ @include lib.border($accent, 0.05);
+ }
+}
+
+.sidebar {
+ @include font.mono;
+
+ background-color: scheme.$mantle;
+ color: scheme.$text;
+ padding: lib.s(18) lib.s(20);
+ min-width: lib.s(380);
+
+ .pane {
+ @include lib.spacing(20, true);
+ }
+
+ .separator {
+ background-color: if(scheme.$light, scheme.$surface1, scheme.$overlay0);
+ margin: 0 lib.s(10);
+ }
+
+ .user {
+ @include lib.spacing(15);
+
+ .face {
+ @include lib.rounded(10);
+
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: cover;
+ min-width: lib.s(96);
+ min-height: lib.s(96);
+ font-size: lib.s(48);
+ font-weight: bold;
+ background-color: scheme.$base;
+ }
+
+ .details {
+ font-size: lib.s(14);
+ color: scheme.$yellow;
+
+ @include lib.spacing(8, true);
+
+ .name {
+ font-size: lib.s(18);
+ color: scheme.$text;
+ margin-bottom: lib.s(10);
+ }
+
+ .uptime {
+ color: scheme.$blue;
+ }
+ }
+ }
+
+ .media {
+ @include lib.spacing(15);
+
+ .cover-art {
+ @include lib.rounded(10);
+ @include lib.element-decel;
+
+ background-position: center;
+ background-repeat: no-repeat;
+ background-size: cover;
+ min-width: lib.s(128);
+ min-height: lib.s(128);
+ font-size: lib.s(64);
+ font-weight: bold;
+ background-color: scheme.$base;
+ }
+
+ .details {
+ font-size: lib.s(14);
+
+ .title {
+ font-size: lib.s(16);
+ color: scheme.$text;
+ }
+
+ .artist {
+ color: scheme.$green;
+ }
+
+ .controls {
+ margin-top: lib.s(20);
+ margin-bottom: lib.s(5);
+ font-size: lib.s(24);
+
+ & > * {
+ @include lib.element-decel;
+
+ &:disabled {
+ color: scheme.$overlay0;
+ }
+
+ &:hover,
+ &:focus {
+ color: scheme.$subtext0;
+ }
+
+ &:active {
+ color: scheme.$overlay2;
+ }
+ }
+ }
+
+ .slider {
+ @include lib.rounded(5);
+ @include lib.fluent-decel(1000ms);
+
+ min-height: lib.s(8);
+ background-color: scheme.$surface0;
+ color: scheme.$overlay0;
+ }
+
+ .time {
+ margin-top: lib.s(5);
+ font-size: lib.s(13);
+ color: scheme.$subtext0;
+ }
+ }
+ }
+
+ .notifications {
+ .header-bar {
+ margin-bottom: lib.s(10);
+ margin-right: lib.s(-10);
+
+ @include lib.spacing;
+
+ & > button {
+ @include lib.element-decel;
+ @include lib.rounded(10);
+
+ padding: lib.s(3) lib.s(8);
+
+ &:hover,
+ &:focus {
+ color: scheme.$subtext0;
+ }
+
+ &:active {
+ color: scheme.$overlay2;
+ }
+
+ &.enabled {
+ background-color: scheme.$primary;
+ color: scheme.$base;
+
+ &:hover,
+ &:focus {
+ background-color: color.mix(scheme.$primary, scheme.$base, 80%);
+ }
+
+ &:active {
+ background-color: color.mix(scheme.$primary, scheme.$base, 70%);
+ }
+ }
+ }
+ }
+
+ .notification {
+ .wrapper {
+ padding-bottom: lib.s(10);
+ }
+
+ .inner {
+ @include lib.rounded(20);
+
+ background-color: color.change(scheme.$surface1, $alpha: 0.4);
+
+ &.low {
+ @include notification(if(scheme.$light, scheme.$surface1, scheme.$overlay0));
+ }
+
+ &.normal {
+ @include lib.border(scheme.$primary, if(scheme.$light, 0.5, 0.3));
+ @include notification(scheme.$primary);
+ }
+
+ &.critical {
+ @include lib.border(scheme.$error, 0.8);
+ @include notification(scheme.$error);
+ }
+ }
+
+ .actions {
+ @include lib.spacing;
+
+ & > button {
+ @include lib.rounded(10);
+ @include lib.element-decel;
+
+ padding: lib.s(5) lib.s(10);
+ background-color: color.change(scheme.$surface1, $alpha: 0.5);
+
+ &:hover,
+ &:focus {
+ background-color: color.change(scheme.$surface2, $alpha: 0.5);
+ }
+
+ &:active {
+ background-color: color.change(scheme.$overlay0, $alpha: 0.5);
+ }
+ }
+ }
+ }
+ }
+}
diff --git a/src/modules/sidebar/dashboard.tsx b/src/modules/sidebar/dashboard.tsx
new file mode 100644
index 0000000..b7d03d0
--- /dev/null
+++ b/src/modules/sidebar/dashboard.tsx
@@ -0,0 +1,123 @@
+import Players from "@/services/players";
+import { osIcon, osId } from "@/utils/system";
+import Slider from "@/widgets/slider";
+import { bind, GLib, monitorFile, Variable } from "astal";
+import { Gtk } from "astal/gtk3";
+import AstalMpris from "gi://AstalMpris";
+import Notifications from "./modules/notifications";
+
+const lengthStr = (length: number) =>
+ `${Math.floor(length / 60)}:${Math.floor(length % 60)
+ .toString()
+ .padStart(2, "0")}`;
+
+const FaceFallback = () => (
+ <label
+ setup={self => {
+ const name = GLib.get_real_name();
+ if (name !== "Unknown")
+ self.label = name
+ .split(" ")
+ .map(s => s[0].toUpperCase())
+ .join("");
+ else {
+ self.label = "";
+ self.xalign = 0.44;
+ }
+ }}
+ />
+);
+
+const User = () => {
+ const uptime = Variable("").poll(5000, "uptime -p");
+ const hasFace = Variable(GLib.file_test(HOME + "/.face", GLib.FileTest.EXISTS));
+ monitorFile(HOME + "/.face", () => hasFace.set(GLib.file_test(HOME + "/.face", GLib.FileTest.EXISTS)));
+
+ return (
+ <box className="user">
+ <box
+ homogeneous
+ className="face"
+ setup={self => {
+ self.css = `background-image: url("${HOME}/.face");`;
+ monitorFile(HOME + "/.face", () => (self.css = `background-image: url("${HOME}/.face");`));
+ }}
+ onDestroy={() => hasFace.drop()}
+ >
+ {bind(hasFace).as(h => (h ? <box visible={false} /> : <FaceFallback />))}
+ </box>
+ <box vertical hexpand valign={Gtk.Align.CENTER} className="details">
+ <label xalign={0} className="name" label={`${osIcon} ${GLib.get_user_name()}`} />
+ <label xalign={0} label={(GLib.getenv("XDG_CURRENT_DESKTOP") ?? osId).toUpperCase()} />
+ <label truncate xalign={0} className="uptime" label={bind(uptime)} onDestroy={() => uptime.drop()} />
+ </box>
+ </box>
+ );
+};
+
+const QuickToggles = () => <box></box>;
+
+const Media = ({ player }: { player: AstalMpris.Player }) => {
+ const position = Variable.derive([bind(player, "position"), bind(player, "length")], (p, l) => p / l);
+
+ return (
+ <box className="media" onDestroy={() => position.drop()}>
+ <box
+ homogeneous
+ className="cover-art"
+ css={bind(player, "coverArt").as(a => `background-image: url("${a}");`)}
+ >
+ {bind(player, "coverArt").as(a => (a ? <box visible={false} /> : <label xalign={0.31} label="" />))}
+ </box>
+ <box vertical className="details">
+ <label truncate className="title" label={bind(player, "title")} />
+ <label truncate className="artist" label={bind(player, "artist")} />
+ <box hexpand className="controls">
+ <button
+ hexpand
+ sensitive={bind(player, "canGoPrevious")}
+ cursor="pointer"
+ onClicked={() => player.next()}
+ label="󰒮"
+ />
+ <button
+ hexpand
+ sensitive={bind(player, "canControl")}
+ cursor="pointer"
+ onClicked={() => player.play_pause()}
+ label={bind(player, "playbackStatus").as(s =>
+ s === AstalMpris.PlaybackStatus.PLAYING ? "󰏤" : "󰐊"
+ )}
+ />
+ <button
+ hexpand
+ sensitive={bind(player, "canGoNext")}
+ cursor="pointer"
+ onClicked={() => player.next()}
+ label="󰒭"
+ />
+ </box>
+ <Slider value={bind(position)} />
+ <box className="time">
+ <label label={bind(player, "position").as(lengthStr)} />
+ <box hexpand />
+ <label label={bind(player, "length").as(lengthStr)} />
+ </box>
+ </box>
+ </box>
+ );
+};
+
+const Today = () => <box></box>;
+
+export default () => (
+ <box vertical className="pane dashboard" name="dashboard">
+ <User />
+ <box className="separator" />
+ {bind(Players.get_default(), "lastPlayer").as(p => (
+ <Media player={p} />
+ ))}
+ <box className="separator" />
+ <Notifications />
+ </box>
+);
diff --git a/src/modules/sidebar/index.tsx b/src/modules/sidebar/index.tsx
new file mode 100644
index 0000000..3b62d82
--- /dev/null
+++ b/src/modules/sidebar/index.tsx
@@ -0,0 +1,34 @@
+import type { Monitor } from "@/services/monitors";
+import { bind, register, Variable } from "astal";
+import { App, Astal, Gtk, Widget } from "astal/gtk3";
+import Dashboard from "./dashboard";
+
+@register()
+export default class SideBar extends Widget.Window {
+ readonly shown: Variable<string> = Variable("dashboard");
+
+ constructor({ monitor }: { monitor: Monitor }) {
+ super({
+ application: App,
+ name: "sidebar",
+ namespace: "caelestia-sidebar",
+ monitor: monitor.id,
+ anchor: Astal.WindowAnchor.LEFT | Astal.WindowAnchor.TOP | Astal.WindowAnchor.BOTTOM,
+ exclusivity: Astal.Exclusivity.EXCLUSIVE,
+ // visible: false,
+ });
+
+ this.add(
+ <box vertical className="sidebar">
+ <stack
+ vexpand
+ transitionType={Gtk.StackTransitionType.SLIDE_UP_DOWN}
+ transitionDuration={200}
+ shown={bind(this.shown)}
+ >
+ <Dashboard />
+ </stack>
+ </box>
+ );
+ }
+}
diff --git a/src/modules/sidebar/modules/hwresources.tsx b/src/modules/sidebar/modules/hwresources.tsx
new file mode 100644
index 0000000..768d8bd
--- /dev/null
+++ b/src/modules/sidebar/modules/hwresources.tsx
@@ -0,0 +1,67 @@
+import Cpu from "@/services/cpu";
+import Gpu from "@/services/gpu";
+import Memory from "@/services/memory";
+import Storage from "@/services/storage";
+import Slider from "@/widgets/slider";
+import { bind, type Binding } from "astal";
+import { Gtk, type Widget } from "astal/gtk3";
+
+const fmt = (bytes: number, pow: number) => +(bytes / 1024 ** pow).toFixed(2);
+const format = ({ total, used }: { total: number; used: number }) => {
+ if (total >= 1024 ** 4) return `${fmt(used, 4)}/${fmt(total, 4)} TiB`;
+ if (total >= 1024 ** 3) return `${fmt(used, 3)}/${fmt(total, 3)} GiB`;
+ if (total >= 1024 ** 2) return `${fmt(used, 2)}/${fmt(total, 2)} MiB`;
+ if (total >= 1024) return `${fmt(used, 1)}/${fmt(total, 1)} KiB`;
+ return `${used}/${total} B`;
+};
+
+const Resource = ({
+ icon,
+ name,
+ value,
+ labelSetup,
+}: {
+ icon: string;
+ name: string;
+ value: Binding<number>;
+ labelSetup?: (self: Widget.Label) => void;
+}) => (
+ <box vertical className={`resource ${name}`}>
+ <box className="inner">
+ <label label={icon} />
+ <Slider value={value.as(v => v / 100)} />
+ </box>
+ <label halign={Gtk.Align.END} label={labelSetup ? "" : value.as(v => `${+v.toFixed(2)}%`)} setup={labelSetup} />
+ </box>
+);
+
+export default () => (
+ <box vertical className="hw-resources">
+ {Gpu.get_default().available && <Resource icon="󰢮" name="gpu" value={bind(Gpu.get_default(), "usage")} />}
+ <Resource icon="" name="cpu" value={bind(Cpu.get_default(), "usage")} />
+ <Resource
+ icon=""
+ name="memory"
+ value={bind(Memory.get_default(), "usage")}
+ labelSetup={self => {
+ const mem = Memory.get_default();
+ const update = () => (self.label = format(mem));
+ self.hook(mem, "notify::used", update);
+ self.hook(mem, "notify::total", update);
+ update();
+ }}
+ />
+ <Resource
+ icon="󰋊"
+ name="storage"
+ value={bind(Storage.get_default(), "usage")}
+ labelSetup={self => {
+ const storage = Storage.get_default();
+ const update = () => (self.label = format(storage));
+ self.hook(storage, "notify::used", update);
+ self.hook(storage, "notify::total", update);
+ update();
+ }}
+ />
+ </box>
+);
diff --git a/src/modules/sidebar/modules/notifications.tsx b/src/modules/sidebar/modules/notifications.tsx
new file mode 100644
index 0000000..eb8f0aa
--- /dev/null
+++ b/src/modules/sidebar/modules/notifications.tsx
@@ -0,0 +1,70 @@
+import Notification from "@/widgets/notification";
+import { bind } from "astal";
+import { Astal, Gtk } from "astal/gtk3";
+import AstalNotifd from "gi://AstalNotifd";
+
+const List = () => (
+ <box
+ vertical
+ valign={Gtk.Align.START}
+ className="list"
+ setup={self => {
+ const notifd = AstalNotifd.get_default();
+ const map = new Map<number, Notification>();
+
+ const addNotification = (notification: AstalNotifd.Notification) => {
+ const notif = (<Notification notification={notification} compact />) as Notification;
+ notif.connect("destroy", () => map.get(notification.id) === notif && map.delete(notification.id));
+ map.get(notification.id)?.destroyWithAnims();
+ map.set(notification.id, notif);
+
+ const widget = (
+ <eventbox
+ // Dismiss on middle click
+ onClick={(_, event) => event.button === Astal.MouseButton.MIDDLE && notification.dismiss()}
+ setup={self => self.hook(notif, "destroy", () => self.destroy())}
+ >
+ {notif}
+ </eventbox>
+ );
+
+ self.pack_end(widget, false, false, 0);
+ };
+
+ 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());
+ }}
+ />
+);
+
+export default () => (
+ <box vertical className="notifications">
+ <box className="header-bar">
+ <label
+ label={bind(AstalNotifd.get_default(), "notifications").as(
+ n => `${n.length} notification${n.length === 1 ? "" : "s"}`
+ )}
+ />
+ <box hexpand />
+ <button
+ className={bind(AstalNotifd.get_default(), "dontDisturb").as(d => (d ? "enabled" : ""))}
+ cursor="pointer"
+ onClicked={() => (AstalNotifd.get_default().dontDisturb = !AstalNotifd.get_default().dontDisturb)}
+ label="󰂛 Silence"
+ />
+ <button
+ cursor="pointer"
+ onClicked={() => AstalNotifd.get_default().notifications.forEach(n => n.dismiss())}
+ label="󰎟 Clear"
+ />
+ </box>
+ <scrollable expand hscroll={Gtk.PolicyType.NEVER}>
+ <List />
+ </scrollable>
+ </box>
+);
diff --git a/src/services/calendar.ts b/src/services/calendar.ts
new file mode 100644
index 0000000..e69de29
--- /dev/null
+++ b/src/services/calendar.ts
diff --git a/src/widgets/notification.tsx b/src/widgets/notification.tsx
index 1048826..368b99b 100644
--- a/src/widgets/notification.tsx
+++ b/src/widgets/notification.tsx
@@ -51,19 +51,19 @@ const AppIcon = ({ appIcon, desktopEntry }: { appIcon: string; desktopEntry: str
return icon ? <icon className="app-icon" icon={icon} /> : null;
};
-const Image = ({ popup, icon }: { popup?: boolean; icon: string }) => {
+const Image = ({ compact, icon }: { compact?: boolean; icon: string }) => {
if (GLib.file_test(icon, GLib.FileTest.EXISTS))
return (
<box
valign={Gtk.Align.START}
- className={`image ${popup ? "small" : ""}`}
+ className={`image ${compact ? "small" : ""}`}
css={`
background-image: url("${icon}");
`}
/>
);
if (Astal.Icon.lookup_icon(icon))
- return <icon valign={Gtk.Align.START} className={`image ${popup ? "small" : ""}`} icon={icon} />;
+ return <icon valign={Gtk.Align.START} className={`image ${compact ? "small" : ""}`} icon={icon} />;
return null;
};
@@ -72,7 +72,15 @@ export default class Notification extends Widget.Box {
readonly #revealer;
#destroyed = false;
- constructor({ notification, popup }: { notification: AstalNotifd.Notification; popup?: boolean }) {
+ constructor({
+ notification,
+ popup,
+ compact = popup,
+ }: {
+ notification: AstalNotifd.Notification;
+ popup?: boolean;
+ compact?: boolean;
+ }) {
super({ className: "notification" });
const time = Variable(getTime(notification.time)).poll(60000, () => getTime(notification.time));
@@ -94,17 +102,17 @@ export default class Notification extends Widget.Box {
</box>
<box hexpand className="separator" />
<box className="content">
- {notification.image && <Image popup={popup} icon={notification.image} />}
+ {notification.image && <Image compact={compact} icon={notification.image} />}
<box vertical>
<label className="summary" xalign={0} label={notification.summary} truncate />
{notification.body && (
<label
className="body"
xalign={0}
- label={popup ? notification.body.split("\n")[0] : notification.body}
+ label={compact ? notification.body.split("\n")[0] : notification.body}
wrap
- lines={popup ? 1 : -1}
- truncate={popup}
+ lines={compact ? 1 : -1}
+ truncate={compact}
/>
)}
</box>
@@ -132,7 +140,7 @@ export default class Notification extends Widget.Box {
// Init animation
const width = this.get_preferred_width()[1];
- this.css = `margin-left: ${width}px; margin-right: -${width}px;`;
+ if (popup) 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/slider.tsx b/src/widgets/slider.tsx
new file mode 100644
index 0000000..fb219bd
--- /dev/null
+++ b/src/widgets/slider.tsx
@@ -0,0 +1,53 @@
+import { bind, type Binding } from "astal";
+import { Gtk } from "astal/gtk3";
+import type cairo from "cairo";
+
+export default ({ value }: { value: Binding<number> }) => (
+ <drawingarea
+ hexpand
+ valign={Gtk.Align.CENTER}
+ className="slider"
+ css={bind(value).as(v => `font-size: ${v}px;`)}
+ setup={self => {
+ const halfPi = Math.PI / 2;
+
+ const styleContext = self.get_style_context();
+ self.set_size_request(-1, styleContext.get_property("min-height", Gtk.StateFlags.NORMAL) as number);
+
+ self.connect("draw", (_, cr: cairo.Context) => {
+ const styleContext = self.get_style_context();
+
+ const width = self.get_allocated_width();
+ const height = styleContext.get_property("min-height", Gtk.StateFlags.NORMAL) as number;
+ self.set_size_request(-1, height);
+
+ const progressValue = styleContext.get_property("font-size", Gtk.StateFlags.NORMAL) as number;
+ let radius = styleContext.get_property("border-radius", Gtk.StateFlags.NORMAL) as number;
+
+ const bg = styleContext.get_background_color(Gtk.StateFlags.NORMAL);
+ cr.setSourceRGBA(bg.red, bg.green, bg.blue, bg.alpha);
+
+ // Background
+ cr.arc(radius, radius, radius, -Math.PI, -halfPi); // Top left
+ cr.arc(width - radius, radius, radius, -halfPi, 0); // Top right
+ cr.arc(width - radius, height - radius, radius, 0, halfPi); // Bottom right
+ cr.arc(radius, height - radius, radius, halfPi, Math.PI); // Bottom left
+ cr.fill();
+
+ // Flatten when near 0
+ radius = Math.min(radius, Math.min(width * progressValue, height) / 2);
+
+ const progressPosition = width * progressValue - radius;
+ const fg = styleContext.get_color(Gtk.StateFlags.NORMAL);
+ cr.setSourceRGBA(fg.red, fg.green, fg.blue, fg.alpha);
+
+ // Foreground
+ cr.arc(radius, radius, radius, -Math.PI, -halfPi); // Top left
+ cr.arc(progressPosition, radius, radius, -halfPi, 0); // Top right
+ cr.arc(progressPosition, height - radius, radius, 0, halfPi); // Bottom right
+ cr.arc(radius, height - radius, radius, halfPi, Math.PI); // Bottom left
+ cr.fill();
+ });
+ }}
+ />
+);
diff --git a/style.scss b/style.scss
index fa1d826..78068c1 100644
--- a/style.scss
+++ b/style.scss
@@ -10,8 +10,8 @@
@use "scss/notifpopups";
@use "scss/launcher";
@use "scss/osds";
-@use "scss/popdowns";
@use "scss/session";
+@use "scss/sidebar";
* {
all: unset; // Remove GTK theme styles