quickshell: move OSD into its own overlay window
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
parent
52599fd24d
commit
d1c92394e5
2 changed files with 133 additions and 110 deletions
|
|
@ -261,6 +261,7 @@ in
|
||||||
|
|
||||||
-- Layer rules: blur behind bar and toasts
|
-- Layer rules: blur behind bar and toasts
|
||||||
hl.layer_rule({ match = { namespace = "quickshell-bar" }, blur = true, ignore_alpha = 0.3 })
|
hl.layer_rule({ match = { namespace = "quickshell-bar" }, blur = true, ignore_alpha = 0.3 })
|
||||||
|
hl.layer_rule({ match = { namespace = "quickshell-osd" }, blur = true, ignore_alpha = 0.3 })
|
||||||
|
|
||||||
-- Startup
|
-- Startup
|
||||||
hl.on("hyprland.start", function()
|
hl.on("hyprland.start", function()
|
||||||
|
|
|
||||||
|
|
@ -1790,10 +1790,6 @@ in
|
||||||
required property var shellRoot
|
required property var shellRoot
|
||||||
screen: modelData
|
screen: modelData
|
||||||
WlrLayershell.namespace: "quickshell-bar"
|
WlrLayershell.namespace: "quickshell-bar"
|
||||||
// Hyprland doesn't render the top layer under a fullscreen
|
|
||||||
// window, so the OSD would be invisible exactly when a video
|
|
||||||
// is playing. Ride the overlay layer while it's on screen.
|
|
||||||
WlrLayershell.layer: osdItem.shown ? WlrLayer.Overlay : WlrLayer.Top
|
|
||||||
// OnDemand + HyprlandFocusGrab is the working combination
|
// OnDemand + HyprlandFocusGrab is the working combination
|
||||||
// (caelestia's): the grab redirects focus to this window and
|
// (caelestia's): the grab redirects focus to this window and
|
||||||
// OnDemand lets the layer surface accept it. Exclusive fights
|
// OnDemand lets the layer surface accept it. Exclusive fights
|
||||||
|
|
@ -6948,119 +6944,145 @@ in
|
||||||
// is needed. Brightness arrives via shellRoot's signal instead,
|
// is needed. Brightness arrives via shellRoot's signal instead,
|
||||||
// because sysfs backlight notifies with POLLPRI, which inotify
|
// because sysfs backlight notifies with POLLPRI, which inotify
|
||||||
// never sees. Focused monitor only.
|
// never sees. Focused monitor only.
|
||||||
Item {
|
// Its own window, not part of the bar's surface: Hyprland
|
||||||
id: osdItem
|
// doesn't render the top layer under a fullscreen window, so an
|
||||||
readonly property bool isFocused: Hyprland.focusedMonitor
|
// OSD drawn into the bar is invisible exactly when a video is
|
||||||
&& Hyprland.focusedMonitor.name === bar.screen.name
|
// playing. Overlay layer, sized to the bar strip, click-through,
|
||||||
// Every binding evaluates once at load; arm on a delay so
|
// and only mapped while the OSD is on screen — so a fullscreen
|
||||||
// a shell restart doesn't flash the slider on screen.
|
// video gets the slider alone, no bar and no frame.
|
||||||
property bool armed: false
|
PanelWindow {
|
||||||
property bool shown: false
|
id: osdWin
|
||||||
property string icon: "volume-2"
|
screen: bar.screen
|
||||||
property real value: 0
|
visible: osdItem.shown || osdItem.opacity > 0.01
|
||||||
property color fill: Theme.base0D
|
WlrLayershell.namespace: "quickshell-osd"
|
||||||
|
WlrLayershell.layer: WlrLayer.Overlay
|
||||||
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
||||||
|
// Ignore the bar's exclusive zone so this surface starts at
|
||||||
|
// y=0 like the bar's does — osdItem then reuses the bar's
|
||||||
|
// own coordinates to line up with the server glance slot.
|
||||||
|
exclusionMode: ExclusionMode.Ignore
|
||||||
|
anchors { top: true; left: true; bottom: true }
|
||||||
|
implicitWidth: Theme.barWidth
|
||||||
|
color: "transparent"
|
||||||
|
// Empty region: no input, clicks reach the video underneath.
|
||||||
|
mask: Region {}
|
||||||
|
|
||||||
// Takes over the server glance's slot: fixed position, no
|
Item {
|
||||||
// dependence on how tall the workspace column has grown.
|
id: osdItem
|
||||||
width: Theme.barWidth
|
readonly property bool isFocused: Hyprland.focusedMonitor
|
||||||
height: 170
|
&& Hyprland.focusedMonitor.name === bar.screen.name
|
||||||
anchors.horizontalCenter: barBgRect.horizontalCenter
|
// Every binding evaluates once at load; arm on a delay so
|
||||||
anchors.verticalCenter: srvWidget.verticalCenter
|
// a shell restart doesn't flash the slider on screen.
|
||||||
opacity: shown ? 1 : 0
|
property bool armed: false
|
||||||
visible: opacity > 0.01
|
property bool shown: false
|
||||||
|
property string icon: "volume-2"
|
||||||
Behavior on opacity {
|
property real value: 0
|
||||||
NumberAnimation { duration: Theme.animContent; easing.type: Easing.OutCubic }
|
property color fill: Theme.base0D
|
||||||
}
|
|
||||||
|
// Takes over the server glance's slot: fixed position, no
|
||||||
function show(ic, v, f) {
|
// dependence on how tall the workspace column has grown.
|
||||||
if (!armed || !isFocused) return;
|
width: Theme.barWidth
|
||||||
icon = ic;
|
height: 170
|
||||||
value = v;
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
fill = f;
|
// Both surfaces span the full screen height from y=0, so
|
||||||
shown = true;
|
// the bar's coordinates carry over unchanged.
|
||||||
osdTimer.restart();
|
y: srvWidget.y + srvWidget.height / 2 - height / 2
|
||||||
}
|
opacity: shown ? 1 : 0
|
||||||
|
visible: opacity > 0.01
|
||||||
Timer { id: osdTimer; interval: 1600; onTriggered: osdItem.shown = false }
|
|
||||||
Timer { id: osdArm; interval: 1500; running: true; onTriggered: osdItem.armed = true }
|
Behavior on opacity {
|
||||||
|
NumberAnimation { duration: Theme.animContent; easing.type: Easing.OutCubic }
|
||||||
function showVolume() {
|
|
||||||
// The volume dropdown already shows the level live.
|
|
||||||
if (bar.activeDropdown === volDropdown) return;
|
|
||||||
show(volWidget.volIcon, volWidget.vol / 100,
|
|
||||||
volWidget.muted ? Theme.base03 : Theme.base0D);
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: volWidget.sink && volWidget.sink.audio ? volWidget.sink.audio : null
|
|
||||||
// `volume` notifies through volumesChanged, not
|
|
||||||
// volumeChanged — it is a view over the per-channel
|
|
||||||
// volumes vector.
|
|
||||||
function onVolumesChanged() { osdItem.showVolume(); }
|
|
||||||
function onMutedChanged() { osdItem.showVolume(); }
|
|
||||||
}
|
|
||||||
|
|
||||||
Connections {
|
|
||||||
target: bar.shellRoot
|
|
||||||
function onOsdBrightness(icon, value) {
|
|
||||||
osdItem.show(icon, value, Theme.base0D);
|
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
function show(ic, v, f) {
|
||||||
// Card body so the OSD reads as a surface, not floating
|
if (!armed || !isFocused) return;
|
||||||
// marks on the bar. Scales in from slightly small.
|
icon = ic;
|
||||||
Rectangle {
|
value = v;
|
||||||
anchors.centerIn: parent
|
fill = f;
|
||||||
width: 36
|
shown = true;
|
||||||
height: osdCol.height + 2 * Theme.cardPad
|
osdTimer.restart();
|
||||||
radius: Theme.radiusSmall
|
|
||||||
color: Theme.cardBg
|
|
||||||
scale: osdItem.shown ? 1 : 0.85
|
|
||||||
Behavior on scale {
|
|
||||||
NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo }
|
|
||||||
}
|
}
|
||||||
|
|
||||||
Column {
|
Timer { id: osdTimer; interval: 1600; onTriggered: osdItem.shown = false }
|
||||||
id: osdCol
|
Timer { id: osdArm; interval: 1500; running: true; onTriggered: osdItem.armed = true }
|
||||||
anchors.centerIn: parent
|
|
||||||
spacing: 6
|
function showVolume() {
|
||||||
|
// The volume dropdown already shows the level live.
|
||||||
SText {
|
if (bar.activeDropdown === volDropdown) return;
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
show(volWidget.volIcon, volWidget.vol / 100,
|
||||||
text: Math.round(Math.max(0, Math.min(1, osdItem.value)) * 100)
|
volWidget.muted ? Theme.base03 : Theme.base0D);
|
||||||
color: Theme.base04
|
}
|
||||||
font.pixelSize: 11
|
|
||||||
|
Connections {
|
||||||
|
target: volWidget.sink && volWidget.sink.audio ? volWidget.sink.audio : null
|
||||||
|
// `volume` notifies through volumesChanged, not
|
||||||
|
// volumeChanged — it is a view over the per-channel
|
||||||
|
// volumes vector.
|
||||||
|
function onVolumesChanged() { osdItem.showVolume(); }
|
||||||
|
function onMutedChanged() { osdItem.showVolume(); }
|
||||||
|
}
|
||||||
|
|
||||||
|
Connections {
|
||||||
|
target: bar.shellRoot
|
||||||
|
function onOsdBrightness(icon, value) {
|
||||||
|
osdItem.show(icon, value, Theme.base0D);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
// Vertical track — fill grows upward from the bottom.
|
|
||||||
// PillSlider is horizontal-only, so this is its own
|
// Card body so the OSD reads as a surface, not floating
|
||||||
// rather than a rotated instance.
|
// marks on the bar. Scales in from slightly small.
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
anchors.centerIn: parent
|
||||||
width: 12
|
width: 36
|
||||||
height: 96
|
height: osdCol.height + 2 * Theme.cardPad
|
||||||
radius: 6
|
radius: Theme.radiusSmall
|
||||||
color: Theme.base02
|
color: Theme.cardBg
|
||||||
|
scale: osdItem.shown ? 1 : 0.85
|
||||||
|
Behavior on scale {
|
||||||
|
NumberAnimation { duration: Theme.animMorph; easing.type: Easing.OutExpo }
|
||||||
|
}
|
||||||
|
|
||||||
|
Column {
|
||||||
|
id: osdCol
|
||||||
|
anchors.centerIn: parent
|
||||||
|
spacing: 6
|
||||||
|
|
||||||
|
SText {
|
||||||
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
|
text: Math.round(Math.max(0, Math.min(1, osdItem.value)) * 100)
|
||||||
|
color: Theme.base04
|
||||||
|
font.pixelSize: 11
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vertical track — fill grows upward from the bottom.
|
||||||
|
// PillSlider is horizontal-only, so this is its own
|
||||||
|
// rather than a rotated instance.
|
||||||
Rectangle {
|
Rectangle {
|
||||||
anchors.bottom: parent.bottom
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
width: parent.width
|
width: 12
|
||||||
height: Math.max(parent.width,
|
height: 96
|
||||||
parent.height * Math.max(0, Math.min(1, osdItem.value)))
|
radius: 6
|
||||||
radius: parent.radius
|
color: Theme.base02
|
||||||
color: osdItem.fill
|
|
||||||
Behavior on height {
|
Rectangle {
|
||||||
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
|
anchors.bottom: parent.bottom
|
||||||
|
width: parent.width
|
||||||
|
height: Math.max(parent.width,
|
||||||
|
parent.height * Math.max(0, Math.min(1, osdItem.value)))
|
||||||
|
radius: parent.radius
|
||||||
|
color: osdItem.fill
|
||||||
|
Behavior on height {
|
||||||
|
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
|
SIcon {
|
||||||
SIcon {
|
anchors.horizontalCenter: parent.horizontalCenter
|
||||||
anchors.horizontalCenter: parent.horizontalCenter
|
text: osdItem.icon
|
||||||
text: osdItem.icon
|
color: Theme.base05
|
||||||
color: Theme.base05
|
font.pixelSize: 16
|
||||||
font.pixelSize: 16
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue