From 284ed73a33c303eee7066249f4e92451674cb9b9 Mon Sep 17 00:00:00 2001 From: Joshua Yuen Date: Sat, 12 Sep 2026 01:59:57 -0400 Subject: [PATCH] Add AGS volume widget --- modules/home/ags/app/style.scss | 52 +++++++++++++- modules/home/ags/app/widget/Bar.tsx | 71 ++++++++++--------- modules/home/ags/app/widget/Dropdown.tsx | 86 ++++++++++++++++++++++++ modules/home/ags/app/widget/Volume.tsx | 54 +++++++++++++++ 4 files changed, 230 insertions(+), 33 deletions(-) create mode 100644 modules/home/ags/app/widget/Dropdown.tsx create mode 100644 modules/home/ags/app/widget/Volume.tsx diff --git a/modules/home/ags/app/style.scss b/modules/home/ags/app/style.scss index d589338..68a554c 100644 --- a/modules/home/ags/app/style.scss +++ b/modules/home/ags/app/style.scss @@ -6,7 +6,8 @@ font-size: $font-size; } -.Bar { +.Bar, +.popup-window { background: transparent; } @@ -34,3 +35,52 @@ font-weight: bold; margin: 0 8px; } + +.volume { + border-radius: 6px; + margin: 0 2px; + padding: 0 6px; +} + +.ws:hover, +.volume:hover { + background: transparentize($base05, 0.88); +} + +.volume icon, +.mute icon { + font-size: 16px; +} + +.dropdown { + background: $base01; + border-radius: 10px; + padding: 8px 12px; + color: $base05; +} + +.mute { + margin-right: 8px; + padding: 0 6px; + border-radius: 6px; +} + +.vol-slider { + trough { + background: $base03; + border-radius: 999px; + min-height: 12px; + + highlight { + background: $base0D; + border-radius: 999px; + } + + slider { + min-width: 16px; + min-height: 16px; + background: $base05; + border-radius: 999px; + } + } +} diff --git a/modules/home/ags/app/widget/Bar.tsx b/modules/home/ags/app/widget/Bar.tsx index 6a31361..2142d6d 100644 --- a/modules/home/ags/app/widget/Bar.tsx +++ b/modules/home/ags/app/widget/Bar.tsx @@ -1,40 +1,47 @@ import { App, Astal, Gdk, Gtk } from "astal/gtk3" import Workspaces from "./Workspaces" import Clock from "./Clock" +import { VolumeWidget } from "./Volume" export default function Bar(gdkmonitor: Gdk.Monitor) { const { TOP, LEFT, RIGHT } = Astal.WindowAnchor + const volume = VolumeWidget(gdkmonitor) - return ( - - - - - - - - - - - - ) + return [ + ( + + + + + + + + + + {volume.button} + + + + ), + volume.popup, + ] } diff --git a/modules/home/ags/app/widget/Dropdown.tsx b/modules/home/ags/app/widget/Dropdown.tsx new file mode 100644 index 0000000..933f229 --- /dev/null +++ b/modules/home/ags/app/widget/Dropdown.tsx @@ -0,0 +1,86 @@ +import { App, Astal, Gdk, Gtk } from "astal/gtk3" +import { Variable, bind } from "astal" + +interface DropdownProps { + gdkmonitor: Gdk.Monitor + target: Gtk.Widget + open: Variable + width?: number + vertical?: boolean + className?: string + children: Array +} + +export default function Dropdown({ + gdkmonitor, + target, + open, + width = 260, + vertical = true, + className = "", + children, +}: DropdownProps) { + const { TOP, BOTTOM, LEFT, RIGHT } = Astal.WindowAnchor + const GAP = 8 + + const child = ( + + {children} + + ) + + function reposition() { + const toplevel = target.get_toplevel() + const [, tx, ty] = target.translate_coordinates(toplevel, 0, 0) + const { width: bw, height: bh } = target.get_allocation() + const { width: sw } = gdkmonitor.get_geometry() + + const x = Math.min(Math.max(tx + bw / 2 - width / 2, GAP), sw - width - GAP) + const y = ty + bh + GAP + + child.marginStart = Math.round(x) + child.marginTop = Math.round(y) + } + + open.subscribe((v) => { + if (v) reposition() + }) + + return ( + { + if (!self.visible) open.set(false) + }} + onButtonPressEvent={(self, e) => { + const event = e as unknown as Gdk.Event + const [, px, py] = event.get_coords() + const { x, y, width: w, height: h } = child.get_allocation() + const xOut = px < x || px > x + w + const yOut = py < y || py > y + h + + if (xOut || yOut) self.visible = false + }} + onKeyPressEvent={(self, e) => { + const event = e as unknown as Gdk.Event + if (event.get_keyval()[1] === Gdk.KEY_Escape) self.visible = false + }} + > + {child} + + ) +} diff --git a/modules/home/ags/app/widget/Volume.tsx b/modules/home/ags/app/widget/Volume.tsx new file mode 100644 index 0000000..beb9a28 --- /dev/null +++ b/modules/home/ags/app/widget/Volume.tsx @@ -0,0 +1,54 @@ +import AstalWp from "gi://AstalWp" +import { Variable, bind } from "astal" +import Dropdown from "./Dropdown" + +const speaker = AstalWp.get_default().audio.default_speaker! + +const icon = Variable.derive( + [bind(speaker, "volume"), bind(speaker, "mute")], + (volume, mute) => { + if (mute || volume <= 0.01) return "audio-volume-muted-symbolic" + if (volume < 0.33) return "audio-volume-low-symbolic" + if (volume < 0.66) return "audio-volume-medium-symbolic" + return "audio-volume-high-symbolic" + } +) + +export function VolumeWidget(gdkmonitor: Gdk.Monitor) { + const open = Variable(false) + + const button = ( + + ) + + const popup = ( + + + (speaker.volume = self.value)} + /> + + ) + + return { button, popup } +}