Add QuickSettings popup with power actions and volume/mic sliders
This commit is contained in:
parent
244e33cb8f
commit
039c5d15a8
|
|
@ -91,26 +91,16 @@ $module-bg: transparentize($base02, 0.4);
|
||||||
color: $base00;
|
color: $base00;
|
||||||
}
|
}
|
||||||
|
|
||||||
.volume {
|
|
||||||
border-radius: 6px;
|
|
||||||
margin: 0 2px;
|
|
||||||
padding: 0 6px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.ws:hover,
|
.ws:hover,
|
||||||
.volume:hover,
|
|
||||||
.wifi:hover,
|
.wifi:hover,
|
||||||
.battery:hover,
|
.battery:hover {
|
||||||
.power:hover {
|
|
||||||
background: transparentize($base05, 0.88);
|
background: transparentize($base05, 0.88);
|
||||||
}
|
}
|
||||||
|
|
||||||
.volume icon,
|
.quicksettings icon,
|
||||||
.mute icon,
|
|
||||||
.wifi icon,
|
.wifi icon,
|
||||||
.wifi-header icon,
|
.wifi-header icon,
|
||||||
.battery icon,
|
.battery icon {
|
||||||
.power icon {
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -121,10 +111,39 @@ $module-bg: transparentize($base02, 0.4);
|
||||||
color: $base05;
|
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;
|
margin-right: 8px;
|
||||||
padding: 0 6px;
|
padding: 0 8px;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
|
|
||||||
|
icon {
|
||||||
|
font-size: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
&:hover {
|
||||||
|
background: transparentize($base05, 0.88);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.vol-slider {
|
.vol-slider {
|
||||||
|
|
@ -231,24 +250,6 @@ $module-bg: transparentize($base02, 0.4);
|
||||||
opacity: 0.7;
|
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 {
|
.notifications {
|
||||||
.notif {
|
.notif {
|
||||||
background: $base01;
|
background: $base01;
|
||||||
|
|
|
||||||
|
|
@ -2,10 +2,9 @@ import { App, Astal, Gdk, Gtk } from "astal/gtk3"
|
||||||
import Workspaces from "./Workspaces"
|
import Workspaces from "./Workspaces"
|
||||||
import Logo from "./Logo"
|
import Logo from "./Logo"
|
||||||
import Clock from "./Clock"
|
import Clock from "./Clock"
|
||||||
import { VolumeWidget } from "./Volume"
|
|
||||||
import { WifiWidget } from "./Wifi"
|
import { WifiWidget } from "./Wifi"
|
||||||
import { BatteryWidget } from "./Battery"
|
import { BatteryWidget } from "./Battery"
|
||||||
import { PowerWidget } from "./Power"
|
import QuickSettings from "./QuickSettings"
|
||||||
|
|
||||||
const RADIUS = 12
|
const RADIUS = 12
|
||||||
|
|
||||||
|
|
@ -56,22 +55,19 @@ function Corner({ side }: { side: "left" | "right" }) {
|
||||||
|
|
||||||
export default function Bar(gdkmonitor: Gdk.Monitor) {
|
export default function Bar(gdkmonitor: Gdk.Monitor) {
|
||||||
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor
|
const { TOP, LEFT, RIGHT } = Astal.WindowAnchor
|
||||||
const volume = VolumeWidget(gdkmonitor)
|
|
||||||
const wifi = WifiWidget(gdkmonitor)
|
const wifi = WifiWidget(gdkmonitor)
|
||||||
const battery = BatteryWidget(gdkmonitor)
|
const battery = BatteryWidget(gdkmonitor)
|
||||||
const power = PowerWidget(gdkmonitor)
|
const quicksettings = QuickSettings(gdkmonitor)
|
||||||
|
|
||||||
const right: Array<JSX.Element> = []
|
const right: Array<JSX.Element> = []
|
||||||
if (wifi.button) right.push(wifi.button)
|
if (wifi.button) right.push(wifi.button)
|
||||||
right.push(volume.button)
|
|
||||||
if (battery.button) right.push(battery.button)
|
if (battery.button) right.push(battery.button)
|
||||||
right.push(power.button)
|
right.push(quicksettings.button)
|
||||||
|
|
||||||
const popups: Array<JSX.Element> = []
|
const popups: Array<JSX.Element> = []
|
||||||
if (wifi.popup) popups.push(wifi.popup)
|
if (wifi.popup) popups.push(wifi.popup)
|
||||||
popups.push(volume.popup)
|
|
||||||
if (battery.popup) popups.push(battery.popup)
|
if (battery.popup) popups.push(battery.popup)
|
||||||
popups.push(power.popup)
|
popups.push(quicksettings.popup)
|
||||||
|
|
||||||
return [
|
return [
|
||||||
(
|
(
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,9 @@
|
||||||
import { App, Astal, Gdk, Gtk } from "astal/gtk3"
|
import { App, Astal, Gdk, Gtk } from "astal/gtk3"
|
||||||
|
import GLib from "gi://GLib"
|
||||||
import { Variable, bind } from "astal"
|
import { Variable, bind } from "astal"
|
||||||
|
|
||||||
|
const DURATION = 180
|
||||||
|
|
||||||
interface DropdownProps {
|
interface DropdownProps {
|
||||||
gdkmonitor: Gdk.Monitor
|
gdkmonitor: Gdk.Monitor
|
||||||
target: Gtk.Widget
|
target: Gtk.Widget
|
||||||
|
|
@ -48,8 +51,26 @@ export default function Dropdown({
|
||||||
child.marginTop = Math.round(y)
|
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) => {
|
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 (
|
return (
|
||||||
|
|
@ -62,9 +83,9 @@ export default function Dropdown({
|
||||||
keymode={Astal.Keymode.EXCLUSIVE}
|
keymode={Astal.Keymode.EXCLUSIVE}
|
||||||
anchor={TOP | BOTTOM | LEFT | RIGHT}
|
anchor={TOP | BOTTOM | LEFT | RIGHT}
|
||||||
exclusivity={Astal.Exclusivity.IGNORE}
|
exclusivity={Astal.Exclusivity.IGNORE}
|
||||||
visible={bind(open)}
|
visible={bind(winVisible)}
|
||||||
onNotifyVisible={(self) => {
|
onNotifyVisible={(self) => {
|
||||||
if (!self.visible) open.set(false)
|
if (!self.visible && open.get()) open.set(false)
|
||||||
}}
|
}}
|
||||||
onButtonPressEvent={(self, e) => {
|
onButtonPressEvent={(self, e) => {
|
||||||
const event = e as unknown as Gdk.Event
|
const event = e as unknown as Gdk.Event
|
||||||
|
|
@ -73,14 +94,20 @@ export default function Dropdown({
|
||||||
const xOut = px < x || px > x + w
|
const xOut = px < x || px > x + w
|
||||||
const yOut = py < y || py > y + h
|
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
|
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}
|
<revealer
|
||||||
|
transitionType={Gtk.RevealerTransitionType.SLIDE_DOWN}
|
||||||
|
transitionDuration={DURATION}
|
||||||
|
revealChild={bind(open)}
|
||||||
|
>
|
||||||
|
{child}
|
||||||
|
</revealer>
|
||||||
</window>
|
</window>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -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 (
|
|
||||||
<button
|
|
||||||
className="power-row"
|
|
||||||
onClicked={() => execAsync(action.cmd)}
|
|
||||||
>
|
|
||||||
<box>
|
|
||||||
<icon icon={action.icon} />
|
|
||||||
<label label={action.label} halign={Gtk.Align.START} hexpand />
|
|
||||||
</box>
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
export function PowerWidget(gdkmonitor: Gdk.Monitor) {
|
|
||||||
const open = Variable(false)
|
|
||||||
|
|
||||||
const button = (
|
|
||||||
<button
|
|
||||||
className="power"
|
|
||||||
onClicked={() => open.set(!open.get())}
|
|
||||||
>
|
|
||||||
<icon icon="system-shutdown-symbolic" />
|
|
||||||
</button>
|
|
||||||
)
|
|
||||||
|
|
||||||
const popup = (
|
|
||||||
<Dropdown gdkmonitor={gdkmonitor} target={button} open={open} width={180}>
|
|
||||||
{actions.map((action) => PowerRow(action))}
|
|
||||||
</Dropdown>
|
|
||||||
)
|
|
||||||
|
|
||||||
return { button, popup }
|
|
||||||
}
|
|
||||||
|
|
@ -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 = (
|
||||||
|
<button
|
||||||
|
className="quicksettings end"
|
||||||
|
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
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<box spacing={6}>
|
||||||
|
<icon icon={speakerIcon()} />
|
||||||
|
{micIcon && <icon icon={micIcon()} />}
|
||||||
|
</box>
|
||||||
|
</button>
|
||||||
|
)
|
||||||
|
|
||||||
|
const popup = (
|
||||||
|
<Dropdown gdkmonitor={gdkmonitor} target={button} open={open} width={280}>
|
||||||
|
<box className="power-header" homogeneous>
|
||||||
|
{actions.map((action) => (
|
||||||
|
<button
|
||||||
|
className="power-action"
|
||||||
|
onClicked={() => execAsync(action.cmd)}
|
||||||
|
>
|
||||||
|
<icon icon={action.icon} />
|
||||||
|
</button>
|
||||||
|
))}
|
||||||
|
</box>
|
||||||
|
<box className="qs-row">
|
||||||
|
<button
|
||||||
|
className="qs-toggle"
|
||||||
|
onClicked={() => (speaker.mute = !speaker.mute)}
|
||||||
|
>
|
||||||
|
<icon icon={speakerIcon()} />
|
||||||
|
</button>
|
||||||
|
<slider
|
||||||
|
className="vol-slider"
|
||||||
|
hexpand
|
||||||
|
value={bind(speaker, "volume")}
|
||||||
|
onDragged={(self) => (speaker.volume = self.value)}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
{mic && micIcon && (
|
||||||
|
<box className="qs-row">
|
||||||
|
<button
|
||||||
|
className="qs-toggle"
|
||||||
|
onClicked={() => (mic.mute = !mic.mute)}
|
||||||
|
>
|
||||||
|
<icon icon={micIcon()} />
|
||||||
|
</button>
|
||||||
|
<slider
|
||||||
|
className="vol-slider"
|
||||||
|
hexpand
|
||||||
|
value={bind(mic, "volume")}
|
||||||
|
onDragged={(self) => (mic.volume = self.value)}
|
||||||
|
/>
|
||||||
|
</box>
|
||||||
|
)}
|
||||||
|
</Dropdown>
|
||||||
|
)
|
||||||
|
|
||||||
|
return { button, popup }
|
||||||
|
}
|
||||||
|
|
@ -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 = (
|
|
||||||
<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 }
|
|
||||||
}
|
|
||||||
Loading…
Reference in New Issue