* Show OSD and sync touchpad steps when scrolling bar widgets Scrolling the volume or brightness icon in the bar changed the value directly without ever calling omarchy-osd, so only the keyboard media keys showed the popup. On top of that, a touchpad's stream of many small wheel events per finger-drag wasn't matched to a mouse's single ±120 notch per click, so touchpad scrolling felt uncoordinated and uneven next to the keyboard/mouse behavior. - shell/plugins/panels/audio/Panel.qml: accumulate raw wheel delta and only apply a step once it crosses a full mouse-notch's worth, so touchpad and mouse move the volume in identical 5% increments, and ping the OSD each time a step actually lands. - shell/plugins/panels/monitor/Panel.qml: same accumulator for brightness, with a 5% floor so scrolling down can never blank the screen. - shell/plugins/osd/Osd.qml: animate the progress bar's width instead of snapping, so successive steps glide smoothly. * Potential fix for pull request finding Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> * Fix bar wheel OSD behavior * Normalize scaled wheel events --------- Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com> Co-authored-by: David Heinemeier Hansson <david@hey.com>
205 lines
7.4 KiB
QML
205 lines
7.4 KiB
QML
import QtQuick
|
|
import Quickshell
|
|
import Quickshell.Io
|
|
import Quickshell.Wayland
|
|
import qs.Commons
|
|
import qs.Ui
|
|
import "OsdModel.js" as OsdModel
|
|
|
|
Item {
|
|
id: root
|
|
|
|
property bool opened: false
|
|
property string icon: ""
|
|
property string message: ""
|
|
property string iconKey: ""
|
|
property int value: 0
|
|
property int maxValue: 100
|
|
property bool hasProgress: true
|
|
property int duration: 1200
|
|
|
|
readonly property bool mediaOsd: iconKey.indexOf("media") === 0 || iconKey.indexOf("player") === 0
|
|
|
|
// The card is built out of measured columns instead of fixed widths, so it
|
|
// keeps exactly `pad` between border and content on every side whatever
|
|
// glyph or message it carries. Messages grow with their text up to
|
|
// `maxMessageWidth` and elide beyond it.
|
|
readonly property int pad: Style.space(16)
|
|
readonly property int gap: Style.space(16)
|
|
// A glyph next to a message reads airier than it measures: the icon outline
|
|
// and the letterforms both fall away from their ink extremes, so the space
|
|
// between them opens up well past the nominal gap. Text takes two thirds of
|
|
// it; the progress bar's hard edge keeps the full gap.
|
|
readonly property int messageGap: Math.round(root.gap * 2 / 3)
|
|
readonly property int barWidth: Style.space(142)
|
|
readonly property int maxMessageWidth: root.mediaOsd ? Style.space(325) : Style.space(190)
|
|
|
|
// Nerd Font glyphs draw well outside their monospace cell, so the icon
|
|
// column is measured by ink rather than by advance width. Progress OSDs pin
|
|
// it to the widest glyph the model can return, so the bar doesn't shift when
|
|
// volume crosses an icon threshold.
|
|
readonly property int iconInkWidth: Math.ceil(iconMetrics.tightBoundingRect.width)
|
|
readonly property int iconWidth: root.hasProgress
|
|
? Math.max(root.iconInkWidth, Math.ceil(widestIconMetrics.tightBoundingRect.width))
|
|
: root.iconInkWidth
|
|
// Same idea for the readout: it is as wide as the longest percentage so the
|
|
// digits don't jitter between 9% and 100%.
|
|
readonly property int valueWidth: Math.ceil(Math.max(valueMetrics.advanceWidth, messageMetrics.advanceWidth))
|
|
readonly property int messageWidth: Math.min(Math.ceil(messageMetrics.advanceWidth), root.maxMessageWidth)
|
|
readonly property int contentWidth: root.hasProgress
|
|
? root.iconWidth + root.gap + root.barWidth + root.gap + root.valueWidth
|
|
: (root.message === "" ? root.iconWidth : root.iconWidth + root.messageGap + root.messageWidth)
|
|
|
|
function iconFor(name, percent) {
|
|
return OsdModel.iconFor(name, percent)
|
|
}
|
|
|
|
function show(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration) {
|
|
var next = OsdModel.stateForShow(iconName, rawMessage, rawValue, rawMax, rawProgressText, rawDuration)
|
|
// Update before opening so a fresh OSD starts at its new value; only
|
|
// subsequent updates while it remains open animate the progress bar.
|
|
iconKey = next.iconKey
|
|
maxValue = next.maxValue
|
|
hasProgress = next.hasProgress
|
|
value = next.value
|
|
message = next.message
|
|
icon = next.icon
|
|
duration = next.duration
|
|
opened = true
|
|
if (duration > 0) hideTimer.restart()
|
|
else hideTimer.stop()
|
|
}
|
|
|
|
function open(payloadJson) {
|
|
try {
|
|
var p = JSON.parse(payloadJson || "{}")
|
|
show(p.icon || "", p.message || "", p.value === undefined ? "" : String(p.value), p.max === undefined ? "100" : String(p.max), p.progressText || "", p.duration === undefined ? "1200" : String(p.duration))
|
|
} catch (e) {}
|
|
}
|
|
|
|
function close() { opened = false }
|
|
|
|
Timer {
|
|
id: hideTimer
|
|
interval: root.duration
|
|
onTriggered: root.opened = false
|
|
}
|
|
|
|
TextMetrics {
|
|
id: messageMetrics
|
|
font.family: Style.font.family
|
|
font.bold: true
|
|
font.pixelSize: Style.font.title
|
|
text: root.message
|
|
}
|
|
|
|
TextMetrics {
|
|
id: valueMetrics
|
|
font: messageMetrics.font
|
|
text: "100%"
|
|
}
|
|
|
|
TextMetrics {
|
|
id: iconMetrics
|
|
font.family: Style.font.family
|
|
font.pixelSize: Style.font.displayLarge
|
|
text: root.icon
|
|
}
|
|
|
|
TextMetrics {
|
|
id: widestIconMetrics
|
|
font: iconMetrics.font
|
|
text: OsdModel.widestIcon
|
|
}
|
|
|
|
IpcHandler {
|
|
target: "osd"
|
|
function show(payloadJson: string): string {
|
|
root.open(payloadJson)
|
|
return "ok"
|
|
}
|
|
function close(): string { root.close(); return "ok" }
|
|
function state(): string { return root.opened ? "open" : "closed" }
|
|
function ping(): string { return "ok" }
|
|
}
|
|
|
|
PanelWindow {
|
|
id: panel
|
|
visible: root.opened
|
|
anchors { top: true; bottom: true; left: true; right: true }
|
|
color: "transparent"
|
|
WlrLayershell.namespace: "omarchy-osd"
|
|
WlrLayershell.layer: WlrLayer.Overlay
|
|
WlrLayershell.keyboardFocus: WlrKeyboardFocus.None
|
|
exclusionMode: ExclusionMode.Ignore
|
|
// Visual-only surface: keep the layer-shell input region empty so the OSD
|
|
// never blocks clicks to the desktop below it.
|
|
mask: Region {}
|
|
|
|
BorderSurface {
|
|
id: card
|
|
width: card.borderLeft + root.pad + root.contentWidth + root.pad + card.borderRight
|
|
height: card.borderTop + root.pad + Style.font.displayLarge + root.pad + card.borderBottom
|
|
anchors.horizontalCenter: parent.horizontalCenter
|
|
anchors.bottom: parent.bottom
|
|
anchors.bottomMargin: Style.space(67)
|
|
color: Util.alpha(Color.background, 0.97)
|
|
borderSpec: Border.surfaceSpec("popups", "border", Color.popups.border, Math.max(1, Style.space(2)))
|
|
radius: Style.cornerRadius
|
|
opacity: root.opened ? 1 : 0
|
|
|
|
Row {
|
|
anchors.fill: parent
|
|
anchors.topMargin: card.borderTop + root.pad
|
|
anchors.rightMargin: card.borderRight + root.pad
|
|
anchors.bottomMargin: card.borderBottom + root.pad
|
|
anchors.leftMargin: card.borderLeft + root.pad
|
|
spacing: root.hasProgress ? root.gap : root.messageGap
|
|
Item {
|
|
width: root.iconWidth
|
|
height: parent.height
|
|
Text {
|
|
// Sit the glyph's ink flush in the column, centered when the
|
|
// column is wider than this particular glyph.
|
|
x: Math.round((root.iconWidth - root.iconInkWidth) / 2 - iconMetrics.tightBoundingRect.x)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.icon
|
|
font: iconMetrics.font
|
|
color: Color.popups.text
|
|
}
|
|
}
|
|
Rectangle {
|
|
visible: root.hasProgress
|
|
width: root.barWidth
|
|
height: Math.max(Style.space(6), Style.spacing.sm)
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
color: Util.alpha(Color.popups.text, 0.45)
|
|
Rectangle {
|
|
height: parent.height
|
|
width: parent.width * (root.hasProgress ? root.value / root.maxValue : 0)
|
|
color: Color.accent
|
|
|
|
Behavior on width {
|
|
enabled: root.opened
|
|
NumberAnimation { duration: 140; easing.type: Easing.OutCubic }
|
|
}
|
|
}
|
|
}
|
|
Text {
|
|
visible: root.message !== ""
|
|
width: root.hasProgress ? root.valueWidth : root.messageWidth
|
|
// The readout hugs the card edge so a short percentage doesn't leave
|
|
// a hole in the padding; the slack lands in the gap after the bar.
|
|
horizontalAlignment: root.hasProgress ? Text.AlignRight : Text.AlignLeft
|
|
anchors.verticalCenter: parent.verticalCenter
|
|
text: root.message
|
|
font: messageMetrics.font
|
|
color: Color.popups.text
|
|
elide: Text.ElideRight
|
|
maximumLineCount: 1
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|