Add AGS volume widget

This commit is contained in:
Joshua Yuen 2026-09-12 01:59:57 -04:00
parent 1fc4d00bd1
commit 32251c2dfa
Signed by: josh
GPG Key ID: 530DA0331EFE99F5
4 changed files with 230 additions and 33 deletions

View File

@ -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;
}
}
}

View File

@ -1,11 +1,14 @@
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 [
(
<window
className="Bar"
namespace="ags-bar"
@ -33,8 +36,12 @@ export default function Bar(gdkmonitor: Gdk.Monitor) {
className="bar-right"
halign={Gtk.Align.END}
valign={Gtk.Align.CENTER}
/>
>
{volume.button}
</box>
</centerbox>
</window>
)
),
volume.popup,
]
}

View File

@ -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<boolean>
width?: number
vertical?: boolean
className?: string
children: Array<JSX.Element>
}
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 = (
<box
className={`dropdown ${className}`}
vertical={vertical}
widthRequest={width}
halign={Gtk.Align.START}
valign={Gtk.Align.START}
>
{children}
</box>
)
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 (
<window
className="popup-window"
application={App}
gdkmonitor={gdkmonitor}
namespace="ags-bar-popup"
layer={Astal.Layer.OVERLAY}
keymode={Astal.Keymode.EXCLUSIVE}
anchor={TOP | BOTTOM | LEFT | RIGHT}
exclusivity={Astal.Exclusivity.IGNORE}
visible={bind(open)}
onNotifyVisible={(self) => {
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}
</window>
)
}

View File

@ -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 = (
<button
className="volume"
onClicked={() => open.set(!open.get())}
onScroll={(_, e) => {
const volume = Math.min(1, Math.max(0, speaker.volume - e.delta_y * 0.02))
speaker.volume = volume
}}
>
<icon icon={icon()} />
</button>
)
const popup = (
<Dropdown
gdkmonitor={gdkmonitor}
target={button}
open={open}
width={240}
vertical={false}
>
<button className="mute" onClicked={() => (speaker.mute = !speaker.mute)}>
<icon icon={icon()} />
</button>
<slider
className="vol-slider"
hexpand
value={bind(speaker, "volume")}
onDragged={(self) => (speaker.volume = self.value)}
/>
</Dropdown>
)
return { button, popup }
}