dot

vizie's NixOS desktop: Hyprland, CachyOS kernel and gaming tweaks

118 lines5573 bytesDiff
1diff --color -ruN a/xembed-sni-proxy/sniproxy.cpp b/xembed-sni-proxy/sniproxy.cpp
2--- a/xembed-sni-proxy/sniproxy.cpp 2026-10-04 00:53:25.516456922 +0200
3+++ b/xembed-sni-proxy/sniproxy.cpp 2026-10-04 00:53:25.529230701 +0200
4@@ -15,6 +15,11 @@
5 #include <xcb/xcb_icccm.h>
6
7 #include <QDBusPendingCallWatcher>
8+#include <QGuiApplication>
9+#include <QJsonArray>
10+#include <QJsonDocument>
11+#include <QJsonObject>
12+#include <QProcess>
13 #include <QScreen>
14 #include <QTimer>
15
16@@ -590,8 +595,102 @@
17 }
18 }
19
20+static QJsonDocument hyprctlJson(const QString &command)
21+{
22+ QProcess hyprctl;
23+ hyprctl.start(QStringLiteral("hyprctl"), {QStringLiteral("-j"), command});
24+ if (!hyprctl.waitForFinished(1000) || hyprctl.exitStatus() != QProcess::NormalExit || hyprctl.exitCode() != 0) {
25+ qCWarning(SNIPROXY) << "Could not query hyprctl for" << command;
26+ return {};
27+ }
28+ return QJsonDocument::fromJson(hyprctl.readAllStandardOutput());
29+}
30+
31+// A click on a tray icon lies inside the panel showing the tray, and clients open
32+// their menu at the click position, so partly over or under that panel. Move the
33+// point just outside the panel instead, on the side facing the middle of the monitor.
34+static QPoint movedOutOfPanel(const QPoint &point, const QRectF &monitor)
35+{
36+ static const int gap = 6;
37+
38+ const QJsonObject outputs = hyprctlJson(QStringLiteral("layers")).object();
39+ for (const QJsonValue &output : outputs) {
40+ const QJsonObject levels = output.toObject()[QLatin1String("levels")].toObject();
41+ for (const QString &level : {QStringLiteral("2"), QStringLiteral("3")}) { // top and overlay layers
42+ const QJsonArray layers = levels[level].toArray();
43+ for (const QJsonValue &value : layers) {
44+ const QJsonObject layer = value.toObject();
45+ const QRect panel(layer[QLatin1String("x")].toInt(),
46+ layer[QLatin1String("y")].toInt(),
47+ layer[QLatin1String("w")].toInt(),
48+ layer[QLatin1String("h")].toInt());
49+ if (!panel.contains(point)) {
50+ continue;
51+ }
52+ if (panel.width() >= panel.height()) { // horizontal panel
53+ if (panel.height() * 2 >= monitor.height()) {
54+ continue; // covers most of the monitor, not a panel
55+ }
56+ const bool atTop = panel.center().y() < monitor.center().y();
57+ return QPoint(point.x(), atTop ? panel.y() + panel.height() + gap : panel.y() - gap);
58+ }
59+ if (panel.width() * 2 >= monitor.width()) {
60+ continue;
61+ }
62+ const bool atLeft = panel.center().x() < monitor.center().x();
63+ return QPoint(atLeft ? panel.x() + panel.width() + gap : panel.x() - gap, point.y());
64+ }
65+ }
66+ }
67+ return point;
68+}
69+
70+// The tray host passes the click position in the compositor's global layout
71+// coordinates. Hyprland arranges its outputs differently for Xwayland (packed left to
72+// right from 0,0, as X11 has no negative coordinates), so map the point to X11 root
73+// coordinates through the output it lies on. Without this the client's menu opens
74+// offset by the difference between the two layouts.
75+static QPoint mapFromHyprlandLayout(QPoint point)
76+{
77+ if (qEnvironmentVariableIsEmpty("HYPRLAND_INSTANCE_SIGNATURE")) {
78+ return point;
79+ }
80+
81+ const QJsonArray monitors = hyprctlJson(QStringLiteral("monitors")).array();
82+ for (const QJsonValue &value : monitors) {
83+ const QJsonObject monitor = value.toObject();
84+ const qreal scale = monitor[QLatin1String("scale")].toDouble(1);
85+ QSizeF size(monitor[QLatin1String("width")].toDouble() / scale, monitor[QLatin1String("height")].toDouble() / scale);
86+ if (monitor[QLatin1String("transform")].toInt() % 2 == 1) { // rotated by 90 or 270 degrees
87+ size.transpose();
88+ }
89+ const QRectF layout(QPointF(monitor[QLatin1String("x")].toDouble(), monitor[QLatin1String("y")].toDouble()), size);
90+ if (size.isEmpty() || !layout.contains(point)) {
91+ continue;
92+ }
93+ point = movedOutOfPanel(point, layout);
94+
95+ const auto screens = QGuiApplication::screens();
96+ for (const QScreen *screen : screens) {
97+ if (screen->name() != monitor[QLatin1String("name")].toString()) {
98+ continue;
99+ }
100+ // position is in X11 pixels, size is divided by the device pixel ratio
101+ const QPoint origin = screen->geometry().topLeft();
102+ const QSizeF x11Size = QSizeF(screen->geometry().size()) * screen->devicePixelRatio();
103+ return QPoint(origin.x() + qRound((point.x() - layout.x()) * x11Size.width() / layout.width()),
104+ origin.y() + qRound((point.y() - layout.y()) * x11Size.height() / layout.height()));
105+ }
106+ }
107+ return point;
108+}
109+
110 void SNIProxy::sendClick(uint8_t mouseButton, int x, int y)
111 {
112+ const QPoint x11Point = mapFromHyprlandLayout(QPoint(x, y));
113+ x = x11Point.x();
114+ y = x11Point.y();
115+
116 // it's best not to look at this code
117 // GTK doesn't like send_events and double checks the mouse position matches where the window is and is top level
118 // in order to solve this we move the embed container over to where the mouse is then replay the event using send_event