From 039c5d15a88228037cf7a6a6a7e02b9df385dfbd Mon Sep 17 00:00:00 2001 From: Joshua Yuen Date: Sat, 12 Sep 2026 15:38:54 -0400 Subject: [PATCH] Add QuickSettings popup with power actions and volume/mic sliders --- modules/home/ags/app/style.scss | 67 ++++++------ modules/home/ags/app/widget/Bar.tsx | 12 +-- modules/home/ags/app/widget/Dropdown.tsx | 41 +++++-- modules/home/ags/app/widget/Power.tsx | 62 ----------- modules/home/ags/app/widget/QuickSettings.tsx | 102 ++++++++++++++++++ modules/home/ags/app/widget/Volume.tsx | 54 ---------- 6 files changed, 174 insertions(+), 164 deletions(-) delete mode 100644 modules/home/ags/app/widget/Power.tsx create mode 100644 modules/home/ags/app/widget/QuickSettings.tsx delete 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 2f2f027..fab841d 100644 --- a/modules/home/ags/app/style.scss +++ b/modules/home/ags/app/style.scss @@ -91,26 +91,16 @@ $module-bg: transparentize($base02, 0.4); color: $base00; } -.volume { - border-radius: 6px; - margin: 0 2px; - padding: 0 6px; -} - .ws:hover, -.volume:hover, .wifi:hover, -.battery:hover, -.power:hover { +.battery:hover { background: transparentize($base05, 0.88); } -.volume icon, -.mute icon, +.quicksettings icon, .wifi icon, .wifi-header icon, -.battery icon, -.power icon { +.battery icon { font-size: 16px; } @@ -121,10 +111,39 @@ $module-bg: transparentize($base02, 0.4); color: $base05; } -.mute { +.power-header { + margin-bottom: 10px; +} + +.power-action { + padding: 8px 0; + border-radius: 8px; + + icon { + font-size: 16px; + } + + &:hover { + background: transparentize($base05, 0.88); + } +} + +.qs-row { + margin: 4px 0; +} + +.qs-toggle { margin-right: 8px; - padding: 0 6px; + padding: 0 8px; border-radius: 6px; + + icon { + font-size: 16px; + } + + &:hover { + background: transparentize($base05, 0.88); + } } .vol-slider { @@ -231,24 +250,6 @@ $module-bg: transparentize($base02, 0.4); opacity: 0.7; } -.power-row { - padding: 4px 6px; - border-radius: 6px; - margin: 1px 0; - - icon { - font-size: 16px; - } - - label { - margin: 0 6px; - } -} - -.power-row:hover { - background: transparentize($base05, 0.88); -} - .notifications { .notif { background: $base01; diff --git a/modules/home/ags/app/widget/Bar.tsx b/modules/home/ags/app/widget/Bar.tsx index f2e081b..176bbd6 100644 --- a/modules/home/ags/app/widget/Bar.tsx +++ b/modules/home/ags/app/widget/Bar.tsx @@ -2,10 +2,9 @@ import { App, Astal, Gdk, Gtk } from "astal/gtk3" import Workspaces from "./Workspaces" import Logo from "./Logo" import Clock from "./Clock" -import { VolumeWidget } from "./Volume" import { WifiWidget } from "./Wifi" import { BatteryWidget } from "./Battery" -import { PowerWidget } from "./Power" +import QuickSettings from "./QuickSettings" const RADIUS = 12 @@ -56,22 +55,19 @@ function Corner({ side }: { side: "left" | "right" }) { export default function Bar(gdkmonitor: Gdk.Monitor) { const { TOP, LEFT, RIGHT } = Astal.WindowAnchor - const volume = VolumeWidget(gdkmonitor) const wifi = WifiWidget(gdkmonitor) const battery = BatteryWidget(gdkmonitor) - const power = PowerWidget(gdkmonitor) + const quicksettings = QuickSettings(gdkmonitor) const right: Array = [] if (wifi.button) right.push(wifi.button) - right.push(volume.button) if (battery.button) right.push(battery.button) - right.push(power.button) + right.push(quicksettings.button) const popups: Array = [] if (wifi.popup) popups.push(wifi.popup) - popups.push(volume.popup) if (battery.popup) popups.push(battery.popup) - popups.push(power.popup) + popups.push(quicksettings.popup) return [ ( diff --git a/modules/home/ags/app/widget/Dropdown.tsx b/modules/home/ags/app/widget/Dropdown.tsx index 933f229..ab786a9 100644 --- a/modules/home/ags/app/widget/Dropdown.tsx +++ b/modules/home/ags/app/widget/Dropdown.tsx @@ -1,6 +1,9 @@ import { App, Astal, Gdk, Gtk } from "astal/gtk3" +import GLib from "gi://GLib" import { Variable, bind } from "astal" +const DURATION = 180 + interface DropdownProps { gdkmonitor: Gdk.Monitor target: Gtk.Widget @@ -48,8 +51,26 @@ export default function Dropdown({ child.marginTop = Math.round(y) } + // the window outlives the animation: it hides once the slide-out + // revealer finished, and hiding the window resolves to open=false + const winVisible = Variable(false) + let closeTimer: number | undefined + open.subscribe((v) => { - if (v) reposition() + if (closeTimer !== undefined) { + GLib.source_remove(closeTimer) + closeTimer = undefined + } + if (v) { + reposition() + winVisible.set(true) + } else if (winVisible.get()) { + closeTimer = GLib.timeout_add(GLib.PRIORITY_DEFAULT, DURATION + 20, () => { + winVisible.set(false) + closeTimer = undefined + return GLib.SOURCE_REMOVE + }) + } }) return ( @@ -62,9 +83,9 @@ export default function Dropdown({ keymode={Astal.Keymode.EXCLUSIVE} anchor={TOP | BOTTOM | LEFT | RIGHT} exclusivity={Astal.Exclusivity.IGNORE} - visible={bind(open)} + visible={bind(winVisible)} onNotifyVisible={(self) => { - if (!self.visible) open.set(false) + if (!self.visible && open.get()) open.set(false) }} onButtonPressEvent={(self, e) => { const event = e as unknown as Gdk.Event @@ -73,14 +94,20 @@ export default function Dropdown({ const xOut = px < x || px > x + w const yOut = py < y || py > y + h - if (xOut || yOut) self.visible = false + if (xOut || yOut) open.set(false) }} - onKeyPressEvent={(self, e) => { + onKeyPressEvent={(_, e) => { const event = e as unknown as Gdk.Event - if (event.get_keyval()[1] === Gdk.KEY_Escape) self.visible = false + if (event.get_keyval()[1] === Gdk.KEY_Escape) open.set(false) }} > - {child} + + {child} + ) } diff --git a/modules/home/ags/app/widget/Power.tsx b/modules/home/ags/app/widget/Power.tsx deleted file mode 100644 index 6b1ba8e..0000000 --- a/modules/home/ags/app/widget/Power.tsx +++ /dev/null @@ -1,62 +0,0 @@ -import Gtk from "gi://Gtk?version=3.0" -import { execAsync, Variable } from "astal" -import Dropdown from "./Dropdown" - -const actions = [ - { label: "lock", icon: "system-lock-screen-symbolic", cmd: ["hyprlock"] }, - { - label: "suspend", - icon: "weather-clear-night-symbolic", - cmd: ["systemctl", "suspend"], - }, - { - label: "log out", - icon: "system-log-out-symbolic", - cmd: ["hyprctl", "dispatch", "exit"], - }, - { - label: "reboot", - icon: "system-reboot-symbolic", - cmd: ["systemctl", "reboot"], - }, - { - label: "shut down", - icon: "system-shutdown-symbolic", - cmd: ["systemctl", "poweroff"], - }, -] - -function PowerRow(action: (typeof actions)[number]) { - return ( - - ) -} - -export function PowerWidget(gdkmonitor: Gdk.Monitor) { - const open = Variable(false) - - const button = ( - - ) - - const popup = ( - - {actions.map((action) => PowerRow(action))} - - ) - - return { button, popup } -} \ No newline at end of file diff --git a/modules/home/ags/app/widget/QuickSettings.tsx b/modules/home/ags/app/widget/QuickSettings.tsx new file mode 100644 index 0000000..c06dc4c --- /dev/null +++ b/modules/home/ags/app/widget/QuickSettings.tsx @@ -0,0 +1,102 @@ +import AstalWp from "gi://AstalWp" +import { execAsync, Variable, bind } from "astal" +import Dropdown from "./Dropdown" + +const speaker = AstalWp.get_default().audio.default_speaker! +const mic = AstalWp.get_default().audio.default_microphone + +const speakerIcon = 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" + }, +) + +const micIcon = mic + ? Variable.derive([bind(mic, "volume"), bind(mic, "mute")], (volume, mute) => + mute || volume <= 0.01 + ? "microphone-sensitivity-muted-symbolic" + : "microphone-sensitivity-high-symbolic", + ) + : null + +const actions = [ + { icon: "system-lock-screen-symbolic", cmd: ["hyprlock"] }, + { icon: "weather-clear-night-symbolic", cmd: ["systemctl", "suspend"] }, + { icon: "system-log-out-symbolic", cmd: ["hyprctl", "dispatch", "exit"] }, + { icon: "system-reboot-symbolic", cmd: ["systemctl", "reboot"] }, + { icon: "system-shutdown-symbolic", cmd: ["systemctl", "poweroff"] }, +] + +export default function QuickSettings(gdkmonitor: Gdk.Monitor) { + const open = Variable(false) + + const button = ( + + ) + + const popup = ( + + + {actions.map((action) => ( + + ))} + + + + (speaker.volume = self.value)} + /> + + {mic && micIcon && ( + + + (mic.volume = self.value)} + /> + + )} + + ) + + return { button, popup } +} \ No newline at end of file diff --git a/modules/home/ags/app/widget/Volume.tsx b/modules/home/ags/app/widget/Volume.tsx deleted file mode 100644 index beb9a28..0000000 --- a/modules/home/ags/app/widget/Volume.tsx +++ /dev/null @@ -1,54 +0,0 @@ -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 } -}