dot
vizie's NixOS desktop: Hyprland, CachyOS kernel and gaming tweaks
| 1 | diff --color -ruN a/xembed-sni-proxy/sniproxy.cpp b/xembed-sni-proxy/sniproxy.cpp |
| 2 | |
| 3 | |
| 4 | |
| 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 | |
| 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 |