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.
This commit is contained in:
extra-salad 2026-05-13 01:19:18 +01:00 committed by Nick Bolton
parent be3cc0ff7a
commit 5e79e49966
2 changed files with 282 additions and 6 deletions

View file

@ -16,6 +16,9 @@
#include "common/Settings.h" #include "common/Settings.h"
#endif #endif
#include <algorithm>
#include <cmath>
#include <limits>
#include <sys/socket.h> // for EIS fd hack, remove #include <sys/socket.h> // for EIS fd hack, remove
#include <sys/un.h> // for EIS fd hack, remove #include <sys/un.h> // for EIS fd hack, remove
@ -39,6 +42,118 @@ const char *PortalInputCapture::barrierSideName(BarrierSide side)
return "unknown"; 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<double>(sourceMin), static_cast<double>(sourceMax));
const auto fraction = (clamped - sourceMin) / (sourceMax - sourceMin);
const auto mapped = static_cast<int>(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<gint>::max();
bounds.top = std::numeric_limits<gint>::max();
bounds.right = std::numeric_limits<gint>::min();
bounds.bottom = std::numeric_limits<gint>::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<gint>(info.width) - 1);
bounds.bottom = std::max(bounds.bottom, info.y + static_cast<gint>(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<double>::max();
const auto considerSide = [&side, &sideDistance](BarrierSide candidateSide, double candidateDistance) {
if (candidateDistance < sideDistance) {
side = candidateSide;
sideDistance = candidateDistance;
}
};
if (activeSides & static_cast<int>(LeftMask)) {
considerSide(BarrierSide::Left, std::abs(x - screenLeft));
}
if (activeSides & static_cast<int>(RightMask)) {
considerSide(BarrierSide::Right, std::abs(x - screenRight));
}
if (activeSides & static_cast<int>(TopMask)) {
considerSide(BarrierSide::Top, std::abs(y - screenTop));
}
if (activeSides & static_cast<int>(BottomMask)) {
considerSide(BarrierSide::Bottom, std::abs(y - screenBottom));
}
if (sideDistance == std::numeric_limits<double>::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<int>::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<gint>(info.width) - 1;
const auto bottom = info.y + static_cast<gint>(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<int>::max();
}
PortalInputCapture::PortalInputCapture(EiScreen *screen, IEventQueue *events) PortalInputCapture::PortalInputCapture(EiScreen *screen, IEventQueue *events)
: m_screen{screen}, : m_screen{screen},
m_events{events}, 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); LOG_WARN("failed to apply barrier %d (%d/%d-%d/%d)", id, x1, y1, x2, y2);
g_object_unref(*elem); g_object_unref(*elem);
m_barriers.erase(elem); m_barriers.erase(elem);
std::erase_if(m_barrierInfo, [id](const auto &info) { return info.id == id; });
break; break;
} }
} }
@ -200,6 +316,110 @@ void PortalInputCapture::handleSetPointerBarriers(const GObject *, GAsyncResult
enable(); enable();
} }
std::pair<int, int>
PortalInputCapture::mapPortalActivationToScreenPosition(guint barrierId, double rawX, double rawY) const
{
auto x = static_cast<int>(rawX);
auto y = static_cast<int>(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<gint>(it->width) - 1;
const auto zoneBottom = it->y + static_cast<gint>(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<double, double> 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<gint>(releaseBarrier.width) - 1,
releaseBarrier.y + static_cast<gint>(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( void PortalInputCapture::addBarrier(
guint id, BarrierSide side, gint zoneX, gint zoneY, guint zoneWidth, guint zoneHeight 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( 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) 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() gboolean PortalInputCapture::initSession()
@ -345,8 +566,11 @@ void PortalInputCapture::release()
void PortalInputCapture::release(double x, double y) void PortalInputCapture::release(double x, double y)
{ {
LOG_DEBUG("releasing input capture session, id=%d x=%.1f y=%.1f", m_activationId, x, y); const auto [mappedX, mappedY] = mapPortalReleasePosition(x, y);
xdp_input_capture_session_release_at(m_session, m_activationId, 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; m_isActive = false;
} }
@ -385,7 +609,27 @@ void PortalInputCapture::handleActivated(
gdouble x; gdouble x;
gdouble y; gdouble y;
if (g_variant_lookup(options, "cursor_position", "(dd)", &x, &y)) { if (g_variant_lookup(options, "cursor_position", "(dd)", &x, &y)) {
m_screen->warpCursor((int)x, (int)y); auto warpX = static_cast<int>(x);
auto warpY = static_cast<int>(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 { } else {
LOG_WARN("failed to get cursor position"); LOG_WARN("failed to get cursor position");
} }
@ -406,16 +650,17 @@ void PortalInputCapture::handleDeactivated(
void PortalInputCapture::handleZonesChanged(XdpInputCaptureSession *session, const GVariant *) void PortalInputCapture::handleZonesChanged(XdpInputCaptureSession *session, const GVariant *)
{ {
for (auto b : m_barriers) for (auto b : m_barriers)
g_object_unref(b); g_object_unref(b);
m_barriers.clear(); m_barriers.clear();
m_barrierInfo.clear();
const auto activeSides = m_screen->activeSides(); const auto activeSides = m_screen->activeSides();
using enum DirectionMask; using enum DirectionMask;
// May not correctly handle different sized screens // May not correctly handle different sized screens
auto zones = xdp_input_capture_session_get_zones(session); auto zones = xdp_input_capture_session_get_zones(session);
guint id = 0;
while (zones != nullptr) { while (zones != nullptr) {
guint w; guint w;
guint h; 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); LOG_DEBUG("input capture zone, %dx%d@%d,%d", w, h, x, y);
auto id = 0;
if (activeSides & static_cast<int>(LeftMask)) { if (activeSides & static_cast<int>(LeftMask)) {
addBarrier(++id, BarrierSide::Left, x, y, w, h); addBarrier(++id, BarrierSide::Left, x, y, w, h);
} }

View file

@ -17,6 +17,7 @@
#include <cstdint> #include <cstdint>
#include <map> #include <map>
#include <utility>
#include <vector> #include <vector>
namespace deskflow { namespace deskflow {
@ -95,7 +96,38 @@ private:
Bottom 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 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<int, int> mapPortalActivationToScreenPosition(guint barrierId, double rawX, double rawY) const;
std::pair<double, double> mapPortalReleasePosition(double x, double y) const;
void addBarrier(guint id, BarrierSide side, gint zoneX, gint zoneY, guint zoneWidth, guint zoneHeight); void addBarrier(guint id, BarrierSide side, gint zoneX, gint zoneY, guint zoneWidth, guint zoneHeight);
EiScreen *m_screen = nullptr; EiScreen *m_screen = nullptr;
@ -121,6 +153,7 @@ private:
std::uint32_t m_activationId = 0; std::uint32_t m_activationId = 0;
std::vector<XdpInputCapturePointerBarrier *> m_barriers; std::vector<XdpInputCapturePointerBarrier *> m_barriers;
std::vector<BarrierInfo> m_barrierInfo;
}; };
} // namespace deskflow } // namespace deskflow