From 5e79e49966a05d2344eddb14448bf423cbba7435 Mon Sep 17 00:00:00 2001 From: extra-salad <87635414+extra-salad@users.noreply.github.com> Date: Wed, 13 May 2026 01:19:18 +0100 Subject: [PATCH] fix(wayland): map portal activations by barrier id Track InputCapture pointer barrier metadata so activation callbacks can identify the fired edge. Portal activations may report cursor positions on zone boundaries, while Deskflow switches using aggregate screen coordinates. Clamp the activation position to the portal zone, then project the fired edge onto Deskflow's aggregate screen before dispatching primary motion. Keep barrier metadata in sync when zones are rebuilt or when the compositor rejects a requested barrier. --- src/lib/platform/PortalInputCapture.cpp | 255 +++++++++++++++++++++++- src/lib/platform/PortalInputCapture.h | 33 +++ 2 files changed, 282 insertions(+), 6 deletions(-) diff --git a/src/lib/platform/PortalInputCapture.cpp b/src/lib/platform/PortalInputCapture.cpp index 3981651ab..dd8bafc74 100644 --- a/src/lib/platform/PortalInputCapture.cpp +++ b/src/lib/platform/PortalInputCapture.cpp @@ -16,6 +16,9 @@ #include "common/Settings.h" #endif +#include +#include +#include #include // for EIS fd hack, remove #include // for EIS fd hack, remove @@ -39,6 +42,118 @@ const char *PortalInputCapture::barrierSideName(BarrierSide side) return "unknown"; } +int PortalInputCapture::scaleCoordinateBetweenRanges( + double value, int sourceMin, int sourceMax, int destinationMin, int destinationMax +) +{ + if (sourceMax <= sourceMin || destinationMax <= destinationMin) { + return destinationMin; + } + + const auto clamped = std::clamp(value, static_cast(sourceMin), static_cast(sourceMax)); + const auto fraction = (clamped - sourceMin) / (sourceMax - sourceMin); + const auto mapped = static_cast(std::lround(destinationMin + fraction * (destinationMax - destinationMin))); + return std::clamp(mapped, destinationMin, destinationMax); +} + +bool PortalInputCapture::getPortalBounds(Bounds &bounds) const +{ + if (m_barrierInfo.empty()) { + return false; + } + + bounds.left = std::numeric_limits::max(); + bounds.top = std::numeric_limits::max(); + bounds.right = std::numeric_limits::min(); + bounds.bottom = std::numeric_limits::min(); + + for (const auto &info : m_barrierInfo) { + bounds.left = std::min(bounds.left, info.x); + bounds.top = std::min(bounds.top, info.y); + bounds.right = std::max(bounds.right, info.x + static_cast(info.width) - 1); + bounds.bottom = std::max(bounds.bottom, info.y + static_cast(info.height) - 1); + } + + return true; +} + +bool PortalInputCapture::getClosestReleaseBarrier( + double x, double y, int screenLeft, int screenTop, int screenRight, int screenBottom, const Bounds &portalBounds, + BarrierInfo &barrier +) const +{ + const auto activeSides = m_screen->activeSides(); + using enum DirectionMask; + + auto side = BarrierSide::Left; + auto sideDistance = std::numeric_limits::max(); + const auto considerSide = [&side, &sideDistance](BarrierSide candidateSide, double candidateDistance) { + if (candidateDistance < sideDistance) { + side = candidateSide; + sideDistance = candidateDistance; + } + }; + + if (activeSides & static_cast(LeftMask)) { + considerSide(BarrierSide::Left, std::abs(x - screenLeft)); + } + if (activeSides & static_cast(RightMask)) { + considerSide(BarrierSide::Right, std::abs(x - screenRight)); + } + if (activeSides & static_cast(TopMask)) { + considerSide(BarrierSide::Top, std::abs(y - screenTop)); + } + if (activeSides & static_cast(BottomMask)) { + considerSide(BarrierSide::Bottom, std::abs(y - screenBottom)); + } + if (sideDistance == std::numeric_limits::max()) { + return false; + } + + const auto portalX = scaleCoordinateBetweenRanges(x, screenLeft, screenRight, portalBounds.left, portalBounds.right); + const auto portalY = scaleCoordinateBetweenRanges(y, screenTop, screenBottom, portalBounds.top, portalBounds.bottom); + + auto bestDistance = std::numeric_limits::max(); + for (const auto &info : m_barrierInfo) { + if (info.side != side) { + continue; + } + + const auto left = info.x; + const auto top = info.y; + const auto right = info.x + static_cast(info.width) - 1; + const auto bottom = info.y + static_cast(info.height) - 1; + auto distance = 0; + + switch (side) { + case BarrierSide::Left: + case BarrierSide::Right: + if (portalY < top) { + distance = top - portalY; + } else if (portalY > bottom) { + distance = portalY - bottom; + } + break; + + case BarrierSide::Top: + case BarrierSide::Bottom: + if (portalX < left) { + distance = left - portalX; + } else if (portalX > right) { + distance = portalX - right; + } + break; + } + + if (distance < bestDistance) { + bestDistance = distance; + barrier = info; + } + } + + return bestDistance != std::numeric_limits::max(); +} + PortalInputCapture::PortalInputCapture(EiScreen *screen, IEventQueue *events) : m_screen{screen}, m_events{events}, @@ -189,6 +304,7 @@ void PortalInputCapture::handleSetPointerBarriers(const GObject *, GAsyncResult LOG_WARN("failed to apply barrier %d (%d/%d-%d/%d)", id, x1, y1, x2, y2); g_object_unref(*elem); m_barriers.erase(elem); + std::erase_if(m_barrierInfo, [id](const auto &info) { return info.id == id; }); break; } } @@ -200,6 +316,110 @@ void PortalInputCapture::handleSetPointerBarriers(const GObject *, GAsyncResult enable(); } +std::pair +PortalInputCapture::mapPortalActivationToScreenPosition(guint barrierId, double rawX, double rawY) const +{ + auto x = static_cast(rawX); + auto y = static_cast(rawY); + + const auto it = std::ranges::find_if(m_barrierInfo, [barrierId](const auto &info) { return info.id == barrierId; }); + if (it == m_barrierInfo.end()) { + LOG_DEBUG("activated barrier %u is not in the current pointer barrier set", barrierId); + return {x, y}; + } + + const auto zoneLeft = it->x; + const auto zoneTop = it->y; + const auto zoneRight = it->x + static_cast(it->width) - 1; + const auto zoneBottom = it->y + static_cast(it->height) - 1; + + std::int32_t screenX; + std::int32_t screenY; + std::int32_t screenW; + std::int32_t screenH; + m_screen->getShape(screenX, screenY, screenW, screenH); + + Bounds portalBounds; + if (getPortalBounds(portalBounds)) { + x = scaleCoordinateBetweenRanges(rawX, portalBounds.left, portalBounds.right, screenX, screenX + screenW - 1); + y = scaleCoordinateBetweenRanges(rawY, portalBounds.top, portalBounds.bottom, screenY, screenY + screenH - 1); + } else { + x = std::clamp(x, zoneLeft, zoneRight); + y = std::clamp(y, zoneTop, zoneBottom); + } + + // The portal reports per-output zones, while Deskflow models the whole computer as one screen. + // Use the activated barrier to preserve the intended switch direction in Deskflow's aggregate coordinates. + using enum BarrierSide; + switch (it->side) { + case Left: + x = screenX; + break; + case Right: + x = screenX + screenW - 1; + break; + case Top: + y = screenY; + break; + case Bottom: + y = screenY + screenH - 1; + break; + } + + return {x, y}; +} + +std::pair PortalInputCapture::mapPortalReleasePosition(double x, double y) const +{ + std::int32_t screenX; + std::int32_t screenY; + std::int32_t screenW; + std::int32_t screenH; + m_screen->getShape(screenX, screenY, screenW, screenH); + + const auto screenLeft = screenX; + const auto screenTop = screenY; + const auto screenRight = screenX + screenW - 1; + const auto screenBottom = screenY + screenH - 1; + const auto jumpZoneSize = m_screen->getJumpZoneSize(); + Bounds portalBounds; + if (!getPortalBounds(portalBounds)) { + return {x, y}; + } + + auto mappedX = scaleCoordinateBetweenRanges(x, screenLeft, screenRight, portalBounds.left, portalBounds.right); + auto mappedY = scaleCoordinateBetweenRanges(y, screenTop, screenBottom, portalBounds.top, portalBounds.bottom); + BarrierInfo releaseBarrier; + if (getClosestReleaseBarrier(x, y, screenLeft, screenTop, screenRight, screenBottom, portalBounds, releaseBarrier)) { + const Bounds releaseBounds = { + releaseBarrier.x, releaseBarrier.y, releaseBarrier.x + static_cast(releaseBarrier.width) - 1, + releaseBarrier.y + static_cast(releaseBarrier.height) - 1 + }; + + using enum BarrierSide; + switch (releaseBarrier.side) { + case Left: + mappedX = std::min(releaseBounds.left + jumpZoneSize, releaseBounds.right); + mappedY = std::clamp(mappedY, releaseBounds.top, releaseBounds.bottom); + break; + case Right: + mappedX = std::max(releaseBounds.right - jumpZoneSize, releaseBounds.left); + mappedY = std::clamp(mappedY, releaseBounds.top, releaseBounds.bottom); + break; + case Top: + mappedX = std::clamp(mappedX, releaseBounds.left, releaseBounds.right); + mappedY = std::min(releaseBounds.top + jumpZoneSize, releaseBounds.bottom); + break; + case Bottom: + mappedX = std::clamp(mappedX, releaseBounds.left, releaseBounds.right); + mappedY = std::max(releaseBounds.bottom - jumpZoneSize, releaseBounds.top); + break; + } + } + + return {mappedX, mappedY}; +} + void PortalInputCapture::addBarrier( guint id, BarrierSide side, gint zoneX, gint zoneY, guint zoneWidth, guint zoneHeight ) @@ -241,6 +461,7 @@ void PortalInputCapture::addBarrier( m_barriers.push_back(XDP_INPUT_CAPTURE_POINTER_BARRIER( g_object_new(XDP_TYPE_INPUT_CAPTURE_POINTER_BARRIER, "id", id, "x1", x1, "y1", y1, "x2", x2, "y2", y2, nullptr) )); + m_barrierInfo.push_back({id, side, zoneX, zoneY, zoneWidth, zoneHeight, x1, y1, x2, y2}); } gboolean PortalInputCapture::initSession() @@ -345,8 +566,11 @@ void PortalInputCapture::release() void PortalInputCapture::release(double x, double y) { - LOG_DEBUG("releasing input capture session, id=%d x=%.1f y=%.1f", m_activationId, x, y); - xdp_input_capture_session_release_at(m_session, m_activationId, x, y); + const auto [mappedX, mappedY] = mapPortalReleasePosition(x, y); + LOG_DEBUG( + "releasing input capture session, id=%d x=%.1f y=%.1f mapped=%.1f,%.1f", m_activationId, x, y, mappedX, mappedY + ); + xdp_input_capture_session_release_at(m_session, m_activationId, mappedX, mappedY); m_isActive = false; } @@ -385,7 +609,27 @@ void PortalInputCapture::handleActivated( gdouble x; gdouble y; if (g_variant_lookup(options, "cursor_position", "(dd)", &x, &y)) { - m_screen->warpCursor((int)x, (int)y); + auto warpX = static_cast(x); + auto warpY = static_cast(y); + + guint barrierId = 0; + const bool hasBarrierId = g_variant_lookup(options, "barrier_id", "u", &barrierId); + + if (hasBarrierId && barrierId > 0) { + auto [mappedX, mappedY] = mapPortalActivationToScreenPosition(barrierId, x, y); + warpX = mappedX; + warpY = mappedY; + } else if (!hasBarrierId) { + LOG_DEBUG("portal activation has no barrier id, using raw cursor position"); + } else { + LOG_DEBUG("portal activation barrier id is zero, using raw cursor position"); + } + + m_screen->warpCursor(warpX, warpY); + m_events->addEvent(Event( + EventTypes::PrimaryScreenMotionOnPrimary, m_screen->getEventTarget(), + IPrimaryScreen::MotionInfo::alloc(warpX, warpY) + )); } else { LOG_WARN("failed to get cursor position"); } @@ -406,16 +650,17 @@ void PortalInputCapture::handleDeactivated( void PortalInputCapture::handleZonesChanged(XdpInputCaptureSession *session, const GVariant *) { - for (auto b : m_barriers) g_object_unref(b); m_barriers.clear(); + m_barrierInfo.clear(); const auto activeSides = m_screen->activeSides(); using enum DirectionMask; // May not correctly handle different sized screens auto zones = xdp_input_capture_session_get_zones(session); + guint id = 0; while (zones != nullptr) { guint w; guint h; @@ -425,8 +670,6 @@ void PortalInputCapture::handleZonesChanged(XdpInputCaptureSession *session, con LOG_DEBUG("input capture zone, %dx%d@%d,%d", w, h, x, y); - auto id = 0; - if (activeSides & static_cast(LeftMask)) { addBarrier(++id, BarrierSide::Left, x, y, w, h); } diff --git a/src/lib/platform/PortalInputCapture.h b/src/lib/platform/PortalInputCapture.h index e3572b304..eaebeaf99 100644 --- a/src/lib/platform/PortalInputCapture.h +++ b/src/lib/platform/PortalInputCapture.h @@ -17,6 +17,7 @@ #include #include +#include #include namespace deskflow { @@ -95,7 +96,38 @@ private: Bottom }; + struct BarrierInfo + { + guint id = 0; + BarrierSide side = BarrierSide::Left; + gint x = 0; + gint y = 0; + guint width = 0; + guint height = 0; + gint x1 = 0; + gint y1 = 0; + gint x2 = 0; + gint y2 = 0; + }; + + struct Bounds + { + gint left = 0; + gint top = 0; + gint right = 0; + gint bottom = 0; + }; + static const char *barrierSideName(BarrierSide side); + static int + scaleCoordinateBetweenRanges(double value, int sourceMin, int sourceMax, int destinationMin, int destinationMax); + bool getPortalBounds(Bounds &bounds) const; + bool getClosestReleaseBarrier( + double x, double y, int screenLeft, int screenTop, int screenRight, int screenBottom, const Bounds &portalBounds, + BarrierInfo &barrier + ) const; + std::pair mapPortalActivationToScreenPosition(guint barrierId, double rawX, double rawY) const; + std::pair mapPortalReleasePosition(double x, double y) const; void addBarrier(guint id, BarrierSide side, gint zoneX, gint zoneY, guint zoneWidth, guint zoneHeight); EiScreen *m_screen = nullptr; @@ -121,6 +153,7 @@ private: std::uint32_t m_activationId = 0; std::vector m_barriers; + std::vector m_barrierInfo; }; } // namespace deskflow