Files
LookingGlass/client/src/core.c
Geoffrey McRae 01cae959d8 [client] core: workaround windows mouse information problem
Under windows there is no cursor enabled at all until it has been moved
for the fist time by the user. If our cursor information is invalid we
assume that this has occured and induce a wiggle to force windows to
create the cursor.
2025-09-07 20:24:17 +10:00

758 lines
20 KiB
C

/**
* Looking Glass
* Copyright © 2017-2025 The Looking Glass Authors
* https://looking-glass.io
*
* This program is free software; you can redistribute it and/or modify it
* under the terms of the GNU General Public License as published by the Free
* Software Foundation; either version 2 of the License, or (at your option)
* any later version.
*
* This program is distributed in the hope that it will be useful, but WITHOUT
* ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or
* FITNESS FOR A PARTICULAR PURPOSE. See the GNU General Public License for
* more details.
*
* You should have received a copy of the GNU General Public License along
* with this program; if not, write to the Free Software Foundation, Inc., 59
* Temple Place, Suite 330, Boston, MA 02111-1307 USA
*/
#include "core.h"
#include "main.h"
#include "app.h"
#include "util.h"
#include "kb.h"
#include "message.h"
#include "message.h"
#include "common/time.h"
#include "common/debug.h"
#include "common/array.h"
#include <math.h>
#define RESIZE_TIMEOUT (10 * 1000) // 10ms
static bool isInView(void)
{
return
g_cursor.pos.x >= g_state.dstRect.x &&
g_cursor.pos.x < g_state.dstRect.x + g_state.dstRect.w &&
g_cursor.pos.y >= g_state.dstRect.y &&
g_cursor.pos.y < g_state.dstRect.y + g_state.dstRect.h;
}
bool core_inputEnabled(void)
{
return g_params.useSpiceInput && !g_state.ignoreInput &&
((g_cursor.grab && g_params.captureInputOnly) || !g_params.captureInputOnly);
}
void core_invalidatePointer(bool detectInView)
{
/* if the display server does not support warp, then we can not operate in
* always relative mode and we should not grab the pointer */
enum LG_DSWarpSupport warpSupport = LG_DS_WARP_NONE;
app_getProp(LG_DS_WARP_SUPPORT, &warpSupport);
if (detectInView)
{
bool inView = isInView();
// do not allow the view to become active if any mouse buttons are being held,
// this fixes issues with meta window resizing.
if (inView && g_cursor.buttons)
return;
g_cursor.inView = inView;
}
g_cursor.draw = (g_params.alwaysShowCursor || g_params.captureInputOnly)
? true : g_cursor.inView;
g_cursor.redraw = true;
g_cursor.warpState = g_cursor.inView ? WARP_STATE_ON : WARP_STATE_OFF;
if (g_cursor.inView)
{
if (g_params.hideMouse)
g_state.ds->setPointer(LG_POINTER_NONE);
if (warpSupport != LG_DS_WARP_NONE && !g_params.captureInputOnly)
g_state.ds->grabPointer();
if (g_params.grabKeyboardOnFocus)
g_state.ds->grabKeyboard();
}
else
{
if (g_params.hideMouse)
g_state.ds->setPointer(LG_POINTER_SQUARE);
if (warpSupport != LG_DS_WARP_NONE)
g_state.ds->ungrabPointer();
g_state.ds->ungrabKeyboard();
}
g_cursor.warpState = WARP_STATE_ON;
}
void core_setCursorInView(bool enable)
{
// if the state has not changed, don't do anything else
if (g_cursor.inView == enable)
return;
if (enable && !g_state.focused)
return;
g_cursor.inView = enable;
core_invalidatePointer(false);
}
void core_setGrab(bool enable)
{
core_setGrabQuiet(enable);
app_alert(
g_cursor.grab ? LG_ALERT_SUCCESS : LG_ALERT_WARNING,
g_cursor.grab ? "Capture Enabled" : "Capture Disabled"
);
}
void core_setGrabQuiet(bool enable)
{
/* we always do this so that at init the cursor is in the right state */
if (g_params.captureInputOnly && g_params.hideMouse)
g_state.ds->setPointer(enable ? LG_POINTER_NONE : LG_POINTER_SQUARE);
if (g_cursor.grab == enable)
return;
g_cursor.grab = enable;
g_cursor.acc.x = 0.0;
g_cursor.acc.y = 0.0;
/* if the display server does not support warp we need to ungrab the pointer
* here instead of in the move handler */
enum LG_DSWarpSupport warpSupport = LG_DS_WARP_NONE;
app_getProp(LG_DS_WARP_SUPPORT, &warpSupport);
if (enable)
{
core_setCursorInView(true);
g_state.ignoreInput = false;
/* ensure the local mouse is inside the window before we capture, this fixes
* odd UI behaviour if the user is using focus follows mouse and the window
* was focused without the cursor being in window already */
struct DoublePoint local;
util_guestCurToLocal(&local);
core_warpPointer(local.x, local.y, true);
if (g_params.grabKeyboard)
g_state.ds->grabKeyboard();
g_state.ds->capturePointer();
}
else
{
if (g_params.grabKeyboard)
{
if (!g_params.grabKeyboardOnFocus ||
!g_state.focused || g_params.captureInputOnly)
g_state.ds->ungrabKeyboard();
}
g_state.ds->uncapturePointer();
/* if exiting capture when input on capture only we need to align the local
* cursor to the guest's location before it is shown. */
if (g_params.captureInputOnly || !g_params.hideMouse)
core_alignToGuest();
}
}
bool core_warpPointer(int x, int y, bool exiting)
{
if ((!g_cursor.inWindow && !exiting) ||
app_isOverlayMode() ||
g_cursor.warpState == WARP_STATE_OFF)
return false;
if (exiting)
g_cursor.warpState = WARP_STATE_OFF;
if (g_cursor.pos.x == x && g_cursor.pos.y == y)
return true;
g_state.ds->warpPointer(x, y, exiting);
return true;
}
void core_onWindowSizeChanged(unsigned width, unsigned height)
{
if (!g_state.pointerQueue)
return;
if (g_state.srcSize.x == width && g_state.srcSize.y == height)
return;
const KVMFRWindowSize msg =
{
.msg.type = KVMFR_MESSAGE_WINDOWSIZE,
.w = width,
.h = height
};
uint32_t serial;
LGMP_STATUS status;
if ((status = lgmpClientSendData(g_state.pointerQueue,
&msg, sizeof(msg), &serial)) != LGMP_OK)
DEBUG_WARN("Message send failed: %s", lgmpStatusString(status));
}
void core_updatePositionInfo(void)
{
if (g_params.setGuestRes && g_state.kvmfrFeatures & KVMFR_FEATURE_WINDOWSIZE)
{
LGMsg msg =
{
.type = LG_MSG_WINDOWSIZE,
.windowSize =
{
.width = g_state.windowW,
.height = g_state.windowH
}
};
lgMessage_post(&msg);
}
if (!g_state.haveSrcSize)
goto done;
float srcW;
float srcH;
switch(g_params.winRotate)
{
case LG_ROTATE_0:
case LG_ROTATE_180:
srcW = g_state.srcSize.x;
srcH = g_state.srcSize.y;
break;
case LG_ROTATE_90:
case LG_ROTATE_270:
srcW = g_state.srcSize.y;
srcH = g_state.srcSize.x;
break;
default:
DEBUG_UNREACHABLE();
}
if (g_params.keepAspect)
{
const float srcAspect = srcH / srcW;
const float wndAspect = (float)g_state.windowH / (float)g_state.windowW;
bool force = true;
if (g_params.dontUpscale &&
srcW <= g_state.windowW &&
srcH <= g_state.windowH)
{
force = false;
g_state.dstRect.w = srcW;
g_state.dstRect.h = srcH;
g_state.dstRect.x = g_state.windowCX - srcW / 2;
g_state.dstRect.y = g_state.windowCY - srcH / 2;
}
else
if (g_params.intUpscale &&
srcW <= g_state.windowW &&
srcH <= g_state.windowH)
{
force = false;
const int scale = min(
floor(g_state.windowW / srcW),
floor(g_state.windowH / srcH));
g_state.dstRect.w = srcW * scale;
g_state.dstRect.h = srcH * scale;
g_state.dstRect.x = g_state.windowCX - g_state.dstRect.w / 2;
g_state.dstRect.y = g_state.windowCY - g_state.dstRect.h / 2;
}
else
if ((int)(wndAspect * 1000) == (int)(srcAspect * 1000))
{
force = false;
g_state.dstRect.w = g_state.windowW;
g_state.dstRect.h = g_state.windowH;
g_state.dstRect.x = 0;
g_state.dstRect.y = 0;
}
else
if (wndAspect < srcAspect)
{
g_state.dstRect.w = (float)g_state.windowH / srcAspect;
g_state.dstRect.h = g_state.windowH;
g_state.dstRect.x = (g_state.windowW >> 1) - (g_state.dstRect.w >> 1);
g_state.dstRect.y = 0;
}
else
{
g_state.dstRect.w = g_state.windowW;
g_state.dstRect.h = (float)g_state.windowW * srcAspect;
g_state.dstRect.x = 0;
g_state.dstRect.y = (g_state.windowH >> 1) - (g_state.dstRect.h >> 1);
}
if (g_params.dontUpscale && g_params.shrinkOnUpscale)
{
if (g_state.windowW > srcW)
{
force = true;
g_state.dstRect.w = (int) (srcW + 0.5);
}
if (g_state.windowH > srcH)
{
force = true;
g_state.dstRect.h = (int) (srcH + 0.5);
}
}
if (force && g_params.forceAspect)
{
g_state.resizeTimeout = microtime() + RESIZE_TIMEOUT;
g_state.resizeDone = false;
}
}
else
{
g_state.dstRect.x = 0;
g_state.dstRect.y = 0;
g_state.dstRect.w = g_state.windowW;
g_state.dstRect.h = g_state.windowH;
}
g_state.dstRect.valid = true;
g_cursor.useScale = (
srcH != g_state.dstRect.h ||
srcW != g_state.dstRect.w);
g_cursor.scale.x = (float)srcW / (float)g_state.dstRect.w;
g_cursor.scale.y = (float)srcH / (float)g_state.dstRect.h;
if (!g_state.posInfoValid)
{
g_state.posInfoValid = true;
g_state.ds->realignPointer();
// g_cursor.guest.valid could have become true in the meantime.
if (g_cursor.guest.valid)
{
// Since posInfoValid was false, core_handleGuestMouseUpdate becomes a
// noop when called on the cursor thread, which means we need to call it
// again in order for the cursor to show up.
core_handleGuestMouseUpdate();
// Similarly, the position needs to be valid before the initial mouse
// move, otherwise we wouldn't know if the cursor is in the viewport.
app_handleMouseRelative(0.0, 0.0, 0.0, 0.0);
}
}
done:
atomic_fetch_add(&g_state.lgrResize, 1);
}
void core_alignToGuest(void)
{
if (!g_cursor.guest.valid || !g_state.focused)
return;
struct DoublePoint local;
if (util_guestCurToLocal(&local))
if (core_warpPointer(round(local.x), round(local.y), false))
core_setCursorInView(true);
}
bool core_isValidPointerPos(int x, int y)
{
return g_state.ds->isValidPointerPos(x, y);
}
bool core_startCursorThread(void)
{
if (g_state.cursorThread)
return true;
g_state.stopVideo = false;
if (!lgCreateThread("cursorThread", main_cursorThread, NULL,
&g_state.cursorThread))
{
DEBUG_ERROR("cursor create thread failed");
return false;
}
return true;
}
void core_stopCursorThread(void)
{
g_state.stopVideo = true;
if (g_state.cursorThread)
lgJoinThread(g_state.cursorThread, NULL);
g_state.cursorThread = NULL;
}
bool core_startFrameThread(void)
{
if (g_state.frameThread)
return true;
g_state.stopVideo = false;
if (!lgCreateThread("frameThread", main_frameThread, NULL,
&g_state.frameThread))
{
DEBUG_ERROR("frame create thread failed");
return false;
}
return true;
}
void core_stopFrameThread(void)
{
g_state.stopVideo = true;
if (g_state.frameThread)
lgJoinThread(g_state.frameThread, NULL);
g_state.frameThread = NULL;
}
void core_handleGuestMouseUpdate(void)
{
struct DoublePoint localPos;
if (!util_guestCurToLocal(&localPos))
return;
if (app_isOverlayMode() || !g_cursor.inView)
return;
g_state.ds->guestPointerUpdated(
g_cursor.guest.x, g_cursor.guest.y,
util_clamp(localPos.x, g_state.dstRect.x,
g_state.dstRect.x + g_state.dstRect.w),
util_clamp(localPos.y, g_state.dstRect.y,
g_state.dstRect.y + g_state.dstRect.h)
);
}
void core_handleMouseGrabbed(double ex, double ey)
{
if (!core_inputEnabled())
return;
int x, y;
if (g_params.rawMouse && !g_cursor.sens)
{
/* raw unscaled input are always round numbers */
x = floor(ex);
y = floor(ey);
}
else
{
/* apply sensitivity */
ex = (ex / 10.0) * (g_cursor.sens + 10);
ey = (ey / 10.0) * (g_cursor.sens + 10);
util_cursorToInt(ex, ey, &x, &y);
}
if (x == 0 && y == 0)
return;
if (!purespice_mouseMotion(x, y))
DEBUG_ERROR("failed to send mouse motion message");
}
void core_handleMouseNormal(double ex, double ey)
{
// prevent cursor handling outside of capture if the position is not known
if (!g_cursor.guest.valid)
{
if (app_guestIsWindows())
{
// wiggle the mouse when the guest has not provided any information, we need
// to do this because windows doesn't enable a cursor at all until it has
// been moved for the first time.
if (!purespice_mouseMotion(1, 1))
DEBUG_ERROR("failed to send mouse motion message");
if (!purespice_mouseMotion(-1, -1))
DEBUG_ERROR("failed to send mouse motion message");
}
return;
}
if (g_cursor.realigning)
return;
if (!core_inputEnabled())
return;
/* scale the movement to the guest */
if (g_cursor.useScale && g_params.scaleMouseInput)
{
ex *= g_cursor.scale.x;
ey *= g_cursor.scale.y;
}
bool testExit = true;
const bool inView = isInView();
if (!g_cursor.inView)
{
if (inView)
g_cursor.realign = true;
else /* nothing to do if we are outside the viewport */
return;
}
/*
* do not pass mouse events to the guest if we do not have focus, this must be
* done after the inView test has been performed so that when focus is gained
* we know if we should be drawing the cursor.
*/
if (!g_state.focused)
{
core_setCursorInView(inView);
return;
}
/* if we have been instructed to realign */
if (g_cursor.realign)
{
struct DoublePoint guest;
util_localCurToGuest(&guest);
if (!g_state.stopVideo &&
g_state.kvmfrFeatures & KVMFR_FEATURE_SETCURSORPOS)
{
const KVMFRSetCursorPos msg = {
.msg.type = KVMFR_MESSAGE_SETCURSORPOS,
.x = round(guest.x),
.y = round(guest.y)
};
uint32_t setPosSerial;
LGMP_STATUS status;
if ((status = lgmpClientSendData(g_state.pointerQueue,
&msg, sizeof(msg), &setPosSerial)) != LGMP_OK)
{
DEBUG_WARN("Message send failed: %s", lgmpStatusString(status));
goto fallback;
}
else
{
/* wait for the move request to be processed */
g_cursor.realigning = true;
do
{
LG_LOCK(g_state.pointerQueueLock);
if (!g_state.pointerQueue)
{
/* the queue is nolonger valid, assume complete */
g_cursor.realigning = false;
LG_UNLOCK(g_state.pointerQueueLock);
break;
}
uint32_t hostSerial;
if (lgmpClientGetSerial(g_state.pointerQueue, &hostSerial) != LGMP_OK)
{
g_cursor.realigning = false;
LG_UNLOCK(g_state.pointerQueueLock);
return;
}
LG_UNLOCK(g_state.pointerQueueLock);
if (hostSerial >= setPosSerial)
break;
g_state.ds->wait(1);
}
while(app_isRunning());
g_cursor.guest.x = msg.x;
g_cursor.guest.y = msg.y;
g_cursor.realign = false;
g_cursor.realigning = false;
g_cursor.redraw = true;
if (!g_cursor.inWindow)
return;
core_setCursorInView(true);
return;
}
}
else
{
fallback:
/* add the difference to the offset */
ex += guest.x - (g_cursor.guest.x + g_cursor.guest.hx);
ey += guest.y - (g_cursor.guest.y + g_cursor.guest.hy);
core_setCursorInView(true);
}
g_cursor.realign = false;
/* don't test for an exit as we just entered, we can get into a enter/exit
* loop otherwise */
testExit = false;
}
/* if we are in "autoCapture" and the delta was large don't test for exit */
if (g_params.autoCapture &&
(fabs(ex) > 20.0 / g_cursor.scale.x || fabs(ey) > 20.0 / g_cursor.scale.y))
testExit = false;
/* if any buttons are held we should not allow exit to happen */
if (g_cursor.buttons)
testExit = false;
if (testExit)
{
enum LG_DSWarpSupport warpSupport = LG_DS_WARP_NONE;
app_getProp(LG_DS_WARP_SUPPORT, &warpSupport);
/* translate the move to the guests orientation */
struct DoublePoint move = {.x = ex, .y = ey};
util_rotatePoint(&move);
/* translate the guests position to our coordinate space */
struct DoublePoint local;
util_guestCurToLocal(&local);
/* check if the move would push the cursor outside the guest's viewport */
if (
local.x + move.x < g_state.dstRect.x ||
local.y + move.y < g_state.dstRect.y ||
local.x + move.x >= g_state.dstRect.x + g_state.dstRect.w ||
local.y + move.y >= g_state.dstRect.y + g_state.dstRect.h)
{
local.x += move.x;
local.y += move.y;
const int tx = (local.x <= 0.0) ? floor(local.x) : ceil(local.x);
const int ty = (local.y <= 0.0) ? floor(local.y) : ceil(local.y);
switch (warpSupport)
{
case LG_DS_WARP_NONE:
break;
case LG_DS_WARP_SURFACE:
g_state.ds->ungrabPointer();
core_warpPointer(tx, ty, true);
if (!isInView() &&
tx >= 0 && tx < g_state.windowW &&
ty >= 0 && ty < g_state.windowH)
core_setCursorInView(false);
break;
case LG_DS_WARP_SCREEN:
if (core_isValidPointerPos(
g_state.windowPos.x + g_state.border.left + tx,
g_state.windowPos.y + g_state.border.top + ty))
{
core_setCursorInView(false);
/* preempt the window leave flag if the warp will leave our window */
if (tx < 0 || ty < 0 || tx > g_state.windowW || ty > g_state.windowH)
g_cursor.inWindow = false;
/* ungrab the pointer and move the local cursor to the exit point */
g_state.ds->ungrabPointer();
core_warpPointer(tx, ty, true);
return;
}
}
}
else if (warpSupport == LG_DS_WARP_SURFACE && isInView())
{
/* regrab the pointer in case the user did not move off the surface */
g_state.ds->grabPointer();
g_cursor.warpState = WARP_STATE_ON;
}
}
int x, y;
util_cursorToInt(ex, ey, &x, &y);
if (x == 0 && y == 0)
return;
if (g_params.autoCapture)
{
g_cursor.delta.x += x;
g_cursor.delta.y += y;
if (fabs(g_cursor.delta.x) > 50.0 || fabs(g_cursor.delta.y) > 50.0)
{
g_cursor.delta.x = 0;
g_cursor.delta.y = 0;
}
}
else
{
/* assume the mouse will move to the location we attempt to move it to so we
* avoid warp out of window issues. The cursorThread will correct this if
* wrong after the movement has ocurred on the guest */
g_cursor.guest.x += x;
g_cursor.guest.y += y;
}
if (!purespice_mouseMotion(x, y))
DEBUG_ERROR("failed to send mouse motion message");
}
void core_resetOverlayInputState(void)
{
g_state.io->MouseDown[ImGuiMouseButton_Left ] = false;
g_state.io->MouseDown[ImGuiMouseButton_Right ] = false;
g_state.io->MouseDown[ImGuiMouseButton_Middle] = false;
for(int key = ImGuiKey_NamedKey_BEGIN; key < ImGuiKey_NamedKey_END; key++)
if (linux_to_imgui[key])
ImGuiIO_AddKeyEvent(g_state.io, linux_to_imgui[key], false);
}
void core_updateOverlayState(void)
{
g_state.cursorLast = -2;
static bool wasGrabbed = false;
if (app_isOverlayMode())
{
wasGrabbed = g_cursor.grab;
g_state.io->ConfigFlags &= ~ImGuiConfigFlags_NoMouse;
g_state.io->MousePos = (ImVec2) { g_cursor.pos.x, g_cursor.pos.y };
core_setGrabQuiet(false);
core_setCursorInView(false);
}
else
{
g_state.io->ConfigFlags |= ImGuiConfigFlags_NoMouse;
core_resetOverlayInputState();
core_setGrabQuiet(wasGrabbed);
core_invalidatePointer(true);
app_invalidateWindow(false);
if (!g_cursor.grab)
{
g_cursor.realign = true;
core_handleMouseNormal(0, 0);
}
}
}