add overlay

This commit is contained in:
end-4
2025-11-06 10:29:59 +01:00
parent 56a7e8cbdd
commit 4f68e9e61a
19 changed files with 605 additions and 66 deletions
@@ -0,0 +1,69 @@
import qs
import qs.modules.common
import qs.modules.common.widgets
import qs.services
import QtQuick
import QtQuick.Controls
import QtQuick.Layouts
import Quickshell
import Quickshell.Io
import Quickshell.Wayland
import Quickshell.Hyprland
Scope {
id: root
property Component regionComponent: Component {
Region {}
}
Loader {
id: overlayLoader
active: GlobalStates.overlayOpen || OverlayContext.hasPinnedWidgets
sourceComponent: PanelWindow {
id: overlayWindow
exclusionMode: ExclusionMode.Ignore
WlrLayershell.namespace: "quickshell:overlay"
WlrLayershell.layer: WlrLayer.Overlay
WlrLayershell.keyboardFocus: WlrKeyboardFocus.OnDemand
visible: true
color: "transparent"
mask: Region {
item: GlobalStates.overlayOpen ? overlayContent : null
regions: OverlayContext.clickableWidgets.map((widget) => regionComponent.createObject(this, {
item: widget
}));
}
anchors {
top: true
bottom: true
left: true
right: true
}
OverlayContent {
id: overlayContent
anchors.fill: parent
}
}
}
IpcHandler {
target: "overlay"
function toggle(): void {
GlobalStates.overlayOpen = !GlobalStates.overlayOpen;
}
}
GlobalShortcut {
name: "overlayToggle"
description: "Toggles overlay on press"
onPressed: {
GlobalStates.overlayOpen = !GlobalStates.overlayOpen;
}
}
}
@@ -0,0 +1,66 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Widgets
import qs
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
import qs.modules.overlay.crosshair
Item {
id: root
readonly property bool usePasswordChars: !PolkitService.flow?.responseVisible ?? true
Keys.onPressed: (event) => { // Esc to close
if (event.key === Qt.Key_Escape) {
GlobalStates.overlayOpen = false;
}
}
property real initScale: 1.08
scale: initScale
Component.onCompleted: {
scale = 1
}
Behavior on scale {
animation: Appearance.animation.elementMoveEnter.numberAnimation.createObject(this)
}
Rectangle {
id: bg
anchors.fill: parent
color: Appearance.colors.colScrim
opacity: (GlobalStates.overlayOpen && root.scale !== initScale) ? 1 : 0
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
}
WidgetCanvas {
anchors.fill: parent
onClicked: GlobalStates.overlayOpen = false
OverlayTaskbar {
anchors {
horizontalCenter: parent.horizontalCenter
top: parent.top
topMargin: 50
}
}
Repeater {
model: ScriptModel {
values: Persistent.states.overlay.open.map(identifier => {
return OverlayContext.availableWidgets.find(w => w.identifier === identifier);
})
objectProp: "identifier"
}
delegate: OverlayWidgetDelegateChooser {
}
}
}
}
@@ -0,0 +1,37 @@
pragma Singleton
pragma ComponentBehavior: Bound
import Quickshell
Singleton {
id: root
readonly property list<var> availableWidgets: [
{ identifier: "crosshair", materialSymbol: "point_scan" },
{ identifier: "volumeMixer", materialSymbol: "volume_up" }
]
readonly property bool hasPinnedWidgets: root.pinnedWidgetIdentifiers.length > 0
property list<string> pinnedWidgetIdentifiers: []
property list<var> clickableWidgets: []
function pin(identifier: string, pin = true) {
if (pin) {
if (!root.pinnedWidgetIdentifiers.includes(identifier)) {
root.pinnedWidgetIdentifiers.push(identifier)
}
} else {
root.pinnedWidgetIdentifiers = root.pinnedWidgetIdentifiers.filter(id => id !== identifier)
}
}
function registerClickableWidget(widget: var, clickable = true) {
if (clickable) {
if (!root.clickableWidgets.includes(widget)) {
root.clickableWidgets.push(widget)
}
} else {
root.clickableWidgets = root.clickableWidgets.filter(w => w !== widget)
}
}
}
@@ -0,0 +1,113 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs
import qs.services
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
Rectangle {
id: root
property real padding: 8
opacity: GlobalStates.overlayOpen ? 1 : 0
implicitWidth: contentRow.implicitWidth + (padding * 2)
implicitHeight: contentRow.implicitHeight + (padding * 2)
color: Appearance.m3colors.m3surfaceContainer
radius: Appearance.rounding.large
border.color: Appearance.colors.colOutlineVariant
border.width: 1
Behavior on opacity {
animation: Appearance.animation.elementMoveFast.numberAnimation.createObject(this)
}
RowLayout {
id: contentRow
anchors {
fill: parent
margins: root.padding
}
spacing: 6
Row {
spacing: 4
Repeater {
model: ScriptModel {
values: OverlayContext.availableWidgets
}
delegate: WidgetButton {
required property var modelData
identifier: modelData.identifier
materialSymbol: modelData.materialSymbol
}
}
}
Separator {}
TimeWidget {}
}
component Separator: Rectangle {
implicitWidth: 1
color: Appearance.colors.colOutlineVariant
Layout.fillHeight: true
Layout.topMargin: 10
Layout.bottomMargin: 10
}
component TimeWidget: StyledText {
Layout.alignment: Qt.AlignVCenter
Layout.leftMargin: 8
Layout.rightMargin: 6
text: DateTime.time
font {
family: Appearance.font.family.numbers
variableAxes: Appearance.font.variableAxes.numbers
pixelSize: 22
}
}
component WidgetButton: RippleButton {
id: widgetButton
required property string identifier
required property string materialSymbol
Layout.alignment: Qt.AlignVCenter
toggled: Persistent.states.overlay.open.includes(identifier)
onClicked: {
if (widgetButton.toggled) {
Persistent.states.overlay.open = Persistent.states.overlay.open.filter(type => type !== identifier);
} else {
Persistent.states.overlay.open.push(identifier);
}
}
implicitWidth: implicitHeight
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
buttonRadius: root.radius - (root.height - height) / 2
contentItem: Item {
anchors.centerIn: parent
implicitWidth: 32
implicitHeight: 32
MaterialSymbol {
id: iconWidget
anchors.centerIn: parent
iconSize: 24
text: widgetButton.materialSymbol
color: widgetButton.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurfaceVariant
}
}
}
}
@@ -0,0 +1,18 @@
pragma ComponentBehavior: Bound
import qs.services
import qs.modules.common
import qs.modules.common.widgets
import QtQuick
import QtQuick.Layouts
import Quickshell
import Quickshell.Bluetooth
import qs.modules.overlay.crosshair
import qs.modules.overlay.volumeMixer
DelegateChooser {
id: root
role: "identifier"
DelegateChoice { roleValue: "crosshair"; Crosshair {} }
DelegateChoice { roleValue: "volumeMixer"; VolumeMixer {} }
}
@@ -0,0 +1,230 @@
pragma ComponentBehavior: Bound
import QtQuick
import QtQuick.Layouts
import Quickshell
import Qt5Compat.GraphicalEffects
import qs
import qs.modules.common
import qs.modules.common.functions
import qs.modules.common.widgets
import qs.modules.common.widgets.widgetCanvas
/*
* To make an overlay widget:
* 1. Create a modules/overlay/<yourWidget>/<YourWidget>.qml, using this as the base class
* 2. Add an entry to OverlayContext.availableWidgets with identifier=<yourWidgetIdentifier>
* 3. Add an entry in Persistent.states.overlay.<yourWidgetIdentifier> with x, y, pinned, clickthrough properties set to reasonable defaults
* 4. Add an entry in OverlayWidgetDelegateChooser with roleValue=<yourWidgetIdentifier> and Declare your widget in there
* Use existing entries as reference.
*/
AbstractOverlayWidget {
id: root
required property var modelData
required property Item contentItem
readonly property string identifier: modelData.identifier
readonly property string materialSymbol: modelData.materialSymbol ?? "widgets"
property string title: identifier.replace(/([A-Z])/g, " $1").replace(/^./, function(str){ return str.toUpperCase(); })
property var persistentStateEntry: Persistent.states.overlay[identifier]
property real radius: Appearance.rounding.windowRounding
property real minWidth: 250
draggable: GlobalStates.overlayOpen
x: Math.round(persistentStateEntry.x) // Round or it'll be blurry
y: Math.round(persistentStateEntry.y) // Round or it'll be blurry
pinned: persistentStateEntry.pinned
clickthrough: persistentStateEntry.clickthrough
drag {
minimumX: 0
minimumY: 0
maximumX: root.parent.width - root.width
maximumY: root.parent.height - root.height
}
// Guarded states & registration funcs
property bool open: Persistent.states.overlay.open
property bool actuallyPinned: pinned && open
property bool actuallyClickable: !clickthrough && actuallyPinned && open
onActuallyPinnedChanged: reportPinnedState();
onActuallyClickableChanged: reportClickableState();
function reportPinnedState() {
OverlayContext.pin(identifier, actuallyPinned);
}
function reportClickableState() {
OverlayContext.registerClickableWidget(contentItem, actuallyClickable);
}
// Self-registeration with OverlayContext
Component.onCompleted: {
reportPinnedState();
reportClickableState();
}
// Hooks
onReleased: savePosition();
function close() {
Persistent.states.overlay.open = Persistent.states.overlay.open.filter(type => type !== root.identifier);
}
function togglePinned() {
persistentStateEntry.pinned = !persistentStateEntry.pinned;
}
function toggleClickthrough() {
persistentStateEntry.clickthrough = !persistentStateEntry.clickthrough;
}
function savePosition(xPos = root.x, yPos = root.y) {
persistentStateEntry.x = xPos;
persistentStateEntry.y = yPos;
}
function center() {
const targetX = (root.parent.width - contentColumn.width) / 2
const targetY = (root.parent.height - contentItem.height) / 2 - titleBar.implicitHeight
root.x = targetX
root.y = targetY
root.savePosition(targetX, targetY)
}
visible: GlobalStates.overlayOpen || actuallyPinned
implicitWidth: Math.max(contentColumn.implicitWidth, minWidth)
implicitHeight: contentColumn.implicitHeight
Rectangle {
id: border
anchors.fill: parent
color: "transparent"
radius: root.radius
border.color: ColorUtils.transparentize(Appearance.colors.colOutlineVariant, GlobalStates.overlayOpen ? 0 : 1)
border.width: 1
layer.enabled: GlobalStates.overlayOpen
layer.effect: OpacityMask {
maskSource: Rectangle {
width: border.width
height: border.height
radius: root.radius
}
}
Column {
id: contentColumn
z: -1
anchors.fill: parent
// Title bar
Rectangle {
id: titleBar
opacity: GlobalStates.overlayOpen ? 1 : 0
anchors {
left: parent.left
right: parent.right
}
property real padding: 2
implicitWidth: titleBarRow.implicitWidth + padding * 2
implicitHeight: titleBarRow.implicitHeight + padding * 2
color: Appearance.m3colors.m3surfaceContainer
border.color: Appearance.colors.colOutlineVariant
border.width: 1
RowLayout {
id: titleBarRow
anchors {
fill: parent
margins: titleBar.padding
leftMargin: titleBar.padding + 8
}
spacing: 0
MaterialSymbol {
text: root.materialSymbol
iconSize: 20
Layout.alignment: Qt.AlignVCenter
Layout.rightMargin: 4
}
StyledText {
text: root.title
Layout.fillWidth: true
elide: Text.ElideRight
}
TitlebarButton {
materialSymbol: "recenter"
onClicked: root.center()
StyledToolTip {
text: "Center"
}
}
TitlebarButton {
materialSymbol: "mouse"
toggled: !root.clickthrough
onClicked: root.toggleClickthrough()
StyledToolTip {
text: "Clickable when pinned"
}
}
TitlebarButton {
materialSymbol: "keep"
toggled: root.pinned
onClicked: root.togglePinned()
StyledToolTip {
text: "Pin"
}
}
TitlebarButton {
materialSymbol: "close"
onClicked: root.close()
StyledToolTip {
text: "Close"
}
}
}
}
// Content
Item {
id: contentContainer
anchors.horizontalCenter: parent.horizontalCenter
implicitWidth: root.contentItem.implicitWidth
implicitHeight: root.contentItem.implicitHeight
children: [root.contentItem]
}
}
}
component TitlebarButton: RippleButton {
id: titlebarButton
required property string materialSymbol
buttonRadius: height / 2
implicitHeight: contentItem.implicitHeight
implicitWidth: implicitHeight
padding: 0
colBackgroundToggled: Appearance.colors.colSecondaryContainer
colBackgroundToggledHover: Appearance.colors.colSecondaryContainerHover
colRippleToggled: Appearance.colors.colSecondaryContainerActive
contentItem: Item {
anchors.centerIn: parent
implicitWidth: 30
implicitHeight: 30
MaterialSymbol {
id: iconWidget
anchors.centerIn: parent
iconSize: 20
text: titlebarButton.materialSymbol
fill: titlebarButton.toggled
color: titlebarButton.toggled ? Appearance.colors.colOnSecondaryContainer : Appearance.colors.colOnSurface
}
}
}
}
@@ -0,0 +1,9 @@
import QtQuick
import Quickshell
import qs.modules.common
import qs.modules.overlay
StyledOverlayWidget {
id: root
contentItem: CrosshairContent {}
}
@@ -0,0 +1,197 @@
pragma ComponentBehavior: Bound
import QtQuick
import qs.modules.common
import qs.modules.common.functions
Item {
id: root
// Keys to props
// f, 0f, 1f, m are irrelevant as they're firing error stuff
// 0 is irrelevant because it's some profile stuff
property var propertyMap: ({
"c": "color",
"u": "colorCode",
"h": "outline",
"o": "outlineOpacity",
"t": "outlineThickness",
"d": "centerDot",
"a": "centerDotOpacity",
"z": "centerDotSize",
"0a": "innerLineOpacity",
"0l": "innerLineLength",
"0v": "innerLineVerticalLength",
"0g": "innerLineUnbindAxesLengths",
"0t": "innerLineThickness",
"0o": "innerLineOffset",
"1b": "outerLines",
"1a": "outerLineOpacity",
"1l": "outerLineLength",
"1v": "outerLineVerticalLength",
"1g": "outerLineUnbindAxesLengths",
"1t": "outerLineThickness",
"1o": "outerLineOffset",
})
property var colorMap: ({
0: "#FFFFFF",
1: "#00FF00",
2: "#7FFF00",
3: "#DFFF00",
4: "#FFFF00",
5: "#00FFFF",
6: "#FF00FF",
7: "#FF0000"
})
// Raw props
property int color: 0
property string colorCode: "#FFFFFF"
property bool outline: true
property real outlineOpacity: 0.5
property int outlineThickness: 1
property bool centerDot: false
property real centerDotOpacity: 1
property int centerDotSize: 2
property bool innerLines: true
property real innerLineOpacity: 0.8
property int innerLineLength: 6
property int innerLineVerticalLength: innerLineLength
property bool innerLineUnbindAxesLengths: false
property int innerLineThickness: 2
property int innerLineOffset: 3
property bool outerLines: true
property real outerLineOpacity: 0.35
property int outerLineLength: 2
property int outerLineVerticalLength: outerLineLength
property bool outerLineUnbindAxesLengths: false
property int outerLineThickness: 2
property int outerLineOffset: 10
property string defaultCode: "c;0;u;FFFFFF;h;1;o;0.5;t;1;d;0;a;1;z;2;0a;0.8;0l;6;0v;6;0g;0;0t;2;0o;3;1b;1;1a;0.35;1l;2;1v;2;1g;0;1t;2;1o;10"
function loadFromCode(code: string): void {
let args = code.split(";");
for (let i = 0; i < args.length; i+= 2) {
let key = args[i];
let value = args[i+1];
let targetKey = root.propertyMap[key];
let targetType = typeof root[targetKey];
if (targetKey === undefined) continue;
if (targetType === "number") {
value = parseFloat(value);
} else if (targetType === "boolean") {
value = (value === "1");
}
if (targetKey === "colorCode") {
value = "#" + value.slice(0, 6);
}
root[targetKey] = value;
}
if (!root.innerLineUnbindAxesLengths) {
root.innerLineVerticalLength = root.innerLineLength;
}
if (!root.outerLineUnbindAxesLengths) {
root.outerLineVerticalLength = root.outerLineLength;
}
}
// Update values from code
property var code: Config.options.crosshair.code
Component.onCompleted: reloadFromCode();
onCodeChanged: reloadFromCode();
function reloadFromCode() {
root.loadFromCode(root.defaultCode);
root.loadFromCode(root.code);
}
// Aggregated props
property color crosshairColor: {
if (colorMap[color] !== undefined) return root.colorMap[color];
if (color === 8) return colorCode;
return "#FFFFFF";
}
property int borderWidth: outline ? outlineThickness : 0
property color borderColor: ColorUtils.transparentize("black", 1 - root.outlineOpacity)
property color innerLineColor: ColorUtils.transparentize(root.crosshairColor, 1 - root.innerLineOpacity)
property color outerLineColor: ColorUtils.transparentize(root.crosshairColor, 1 - root.outerLineOpacity)
property int innerLineTotalOffset: root.centerDotSize / 2 + 1 + root.innerLineOffset
property int outerLineTotalOffset: root.centerDotSize / 2 + 1 + root.outerLineOffset
property real centerDotTotalSize: root.centerDotSize + root.borderWidth * 2
property real innerLineTotalSize: (innerLineTotalOffset + root.innerLineLength + root.borderWidth) * 2
property real outerLineTotalSize: (outerLineTotalOffset + root.outerLineLength + root.borderWidth) * 2
implicitWidth: Math.max(centerDotTotalSize, innerLineTotalSize, outerLineTotalSize) + 2 // 2 for pixel correction
implicitHeight: implicitWidth
// width: implicitWidth
// height: implicitHeight
Rectangle {
id: centerDot
visible: root.centerDot
anchors.centerIn: parent
color: root.crosshairColor
opacity: root.centerDotOpacity
width: centerDotTotalSize
height: width
border.width: root.borderWidth
border.color: root.borderColor
}
Repeater {
id: innerLines
model: 4
Item {
id: innerHair
z: index % 2 // Vertical lines above horizontal lines
required property int index
property int pixelCorrection: (root.innerLineThickness % 2 === 1 && index > 1) ? 1 : 0
property int hairLength: (innerHair.index % 2 === 0 ? root.innerLineLength : root.innerLineVerticalLength)
visible: root.innerLines && hairLength > 0
anchors.fill: parent
rotation: index * 90
Rectangle {
x: parent.width / 2 + root.innerLineTotalOffset - root.borderWidth + innerHair.pixelCorrection
y: parent.height / 2 - height / 2
color: root.innerLineColor
width: innerHair.hairLength + root.borderWidth * 2
height: root.innerLineThickness + root.borderWidth * 2
border.width: root.borderWidth
border.color: root.borderColor
}
}
}
Repeater {
id: outerLines
model: 4
Item {
id: outerHair
z: index % 2 + 2 // Vertical lines above horizontal lines, above inner lines
required property int index
property int pixelCorrection: (root.outerLineThickness % 2 === 1 && index > 1) ? 1 : 0
property int hairLength: (outerHair.index % 2 === 0 ? root.outerLineLength : root.outerLineVerticalLength)
visible: root.outerLines && hairLength > 0
anchors.fill: parent
rotation: index * 90
Rectangle {
x: parent.width / 2 + root.outerLineTotalOffset - root.borderWidth + outerHair.pixelCorrection
y: parent.height / 2 - height / 2
color: root.outerLineColor
width: hairLength + root.borderWidth * 2
height: root.outerLineThickness + root.borderWidth * 2
border.width: root.borderWidth
border.color: root.borderColor
}
}
}
}
@@ -0,0 +1,24 @@
import QtQuick
import QtQuick.Layouts
import Quickshell
import qs.modules.common
import qs.modules.overlay
import qs.modules.sidebarRight.volumeMixer
StyledOverlayWidget {
id: root
contentItem: Rectangle {
anchors.centerIn: parent
color: Appearance.m3colors.m3surfaceContainer
property real padding: 16
implicitHeight: 700
implicitWidth: 400
VolumeDialogContent {
anchors.fill: parent
anchors.margins: parent.padding
isSink: true
}
}
}