libsdl-org / SDL

Simple Directmedia Layer
https://libsdl.org
zlib License
9.41k stars 1.75k forks source link

Bug: SDL_SetRelativeMouseMode() does not restore cursor position correctly #7840

Closed expikr closed 2 months ago

expikr commented 1 year ago

https://github.com/libsdl-org/SDL/assets/77922942/8d228139-6b8d-462b-9330-9ee159ec1a0f

Using Love2D 11.4 on Windows, tested both the default distribution as well as with SDL.dll replaced to latest release.

crosshairX, crosshairY = 0 , 0
posX, posY = 0 , 0
panning = false
function love.mousepressed( x, y, button, istouch, presses )
    if button==3 then 
       crosshairX , crosshairY = x , y
       panning = true
       love.mouse.setRelativeMode(true)
    end
end
function love.mousereleased( x, y, button, istouch, presses )
    if button==3 then 
       love.mouse.setRelativeMode(false)
       panning = false
       crosshairX , crosshairY = 0 , 0
    end
end
function love.mousemoved( x, y, dx, dy, istouch )
    if panning then 
       posX = posX + dx
       posY = posY + dy
    end
end
function love.draw()
    local w,h = love.graphics.getDimensions()
    local x,y = w/2 - posX , h/2 - posY
    love.graphics.rectangle('line',x-50,y-50,100,100)
    love.graphics.line(x,0,x,h)
    love.graphics.line(0,y,w,y)
    if panning then
       love.graphics.line(crosshairX,0,crosshairX,h)
       love.graphics.line(0,crosshairY,w,crosshairY)
    end
end
Susko3 commented 1 year ago

Hmm, not sure if toggling relative mouse mode should remember/set the mouse position. I think it's best if you manually set and restore the mouse position before enabling and after disabling relative mouse mode.

slouken commented 1 year ago

You're moving your mouse position based on relative motion, you're not querying what SDL thinks is the mouse position. The SDL mouse position should remain in the upper right corner in your video. You can confirm that by looking at the x and y coordinates in the mousemoved handler.

expikr commented 1 year ago

You're moving your mouse position based on relative motion, you're not querying what SDL thinks is the mouse position. The SDL mouse position should remain in the upper right corner in your video. You can confirm that by looking at the x and y coordinates in the mousemoved handler.

But I don't want what SDL thinks is the mouse position. since I'm using relative mouse movement to pan my canvas. The point here is that SDL is messing with my non-relative mode position where it shouldn't be.

Here's an example written in Autoit showing how Relative Mode should work:

Click to expand ```AutoIt Global $g_xPos=0, $g_yPos=0 Global $g_isPanning = False Global $hwnd = GUICreate('') Global $hlabel = GUICtrlCreateLabel('0,0',0,0,400,400) GUISetState() Local $struct = DllStructCreate('ushort;ushort;dword;hwnd') DllStructSetData($struct, 1, 1) DllStructSetData($struct, 2, 2) DllStructSetData($struct, 3, 0x100) DllStructSetData($struct, 4, $hwnd) DllCall('user32.dll','bool','RegisterRawInputDevices','struct*',$struct,'uint',1,'uint',DllStructGetSize($struct)) GUIRegisterMsg(0xFF,WM_INPUT) Do GUICtrlSetData($hlabel,$g_xPos & ',' & $g_yPos) Until -3=GUIGetMsg() Func AnchorCursor($dword) Local Static $rect = DllStructCreate('long left;long top;long right;long bottom;') Local Static $write = DllStructCreate('dword xy') Local Static $read = DllStructCreate('word x;word y',DllStructGetPtr($write)) $write.xy = $dword $rect.left = $read.x $rect.top = $read.y $rect.right = $read.x+1 $rect.bottom = $read.y+1 DllCall('user32.dll','bool','ClipCursor','struct*',$rect) EndFunc Func GetMsgPos() Return DllCall('user32.dll','dword','GetMessagePos')[0] EndFunc Func WM_INPUT($h,$m,$w,$l) Local Static $HEAD = 'struct;dword Type;dword Size;handle hDevice;wparam wParam;endstruct;' Local Static $HEADSIZE = DllStructGetSize(DllStructCreate($HEAD)) Local Static $MOU = $HEAD & 'ushort Flags;ushort Alignment;ushort ButtonFlags;short ButtonData;ulong RawButtons;long LastX;long LastY;ulong ExtraInformation;' Local Static $raw = DllStructCreate($MOU) DllCall ('user32.dll','uint','GetRawInputData','handle',$l,'uint',0x10000003,'struct*',$raw,'uint*',DllStructGetSize($raw),'uint',$HEADSIZE) If BitAnd(16,$raw.ButtonFlags) and not $w=1 Then $g_isPanning = True AnchorCursor(GetMsgPos()) ElseIf BitAnd(32,$raw.ButtonFlags) Then $g_isPanning = False DllCall('user32.dll','bool','ClipCursor','struct*',Null) EndIf If $g_isPanning Then $g_xPos += $raw.LastX $g_yPos += $raw.LastY EndIf Return 0 EndFunc ```

Zipped script. Drag and drop the txt file onto the autoit exe to run.

Hold mouse 3 to activate cursor-locked panning

https://github.com/libsdl-org/SDL/assets/77922942/8a05b2d2-e6c5-44e3-8d97-fc3b2abc9f42

Susko3 commented 1 year ago

I understand what you're trying to do. Unfortunately for you, SDL doesn't work this way out of the box so you'll need to have a bit of special handling. Change your program to the equivalent of this C code:

static SDL_Window * window;
static int saved_x, saved_y;

void setMyRelativeMouseMode(SDL_bool enable)
{
    if (enable) {
        SDL_GetMouseState(&saved_x, &saved_y);
        SDL_SetRelativeMouseMode(true);
    } else {
        // also try flipping the order of these calls.
        SDL_SetRelativeMouseMode(false);
        SDL_WarpMouseInWindow(window, saved_x, saved_y);
    }
}
expikr commented 1 year ago

Yes, that's what I ended up doing with my app, but I'm reporting that this behavior is poorly thought out, it's like SDL is going out of its way to mess with something it had no business of messing with. When I turn on relative mode, I want relative delta instead of the OS cursor. When I turn off relative mode, I want to use the OS cursor. Relative mode messing with the OS cursor is just bad design/poor ergonomics. The whole point of relative mode is that you get relative motion, it should have no concept of OS mouse position to begin with.

Here's the example with the grid drawn, implemented using WinAPI directly rather than SDL:

Click to expand code ```AutoIt #OnAutoitStartRegister SetProcessDPIAware Opt('GUIOnEventMode',1) Global $user32dll = DllOpen('user32.dll') Global $gdi32dll = DllOpen('gdi32.dll') Global $xPos=0, $yPos=0 Global $isPanning = False Global $quit = False Local $struct = DllStructCreate('ushort;ushort;dword;hwnd') DllStructSetData($struct, 1, 1) DllStructSetData($struct, 2, 2) DllStructSetData($struct, 3, 0x100) DllStructSetData($struct, 4, GUICreate('')) DllCall('user32.dll','bool','RegisterRawInputDevices','struct*',$struct,'uint',1,'uint',DllStructGetSize($struct)) GUIRegisterMsg(0xFF,WM_INPUT) GUIRegisterMsg(0x0200,WM_MOUSEMOVE) Main() Func SetProcessDPIAware() Local $h=GUICreate('') DllCall("user32.dll", "bool", "SetProcessDPIAware") GUIDelete($h) EndFunc Func Quit() $quit=True EndFunc Func AnchorCursor($dword) Local Static $rect = DllStructCreate('long left;long top;long right;long bottom;') Local Static $write = DllStructCreate('dword xy') Local Static $read = DllStructCreate('short x;short y',DllStructGetPtr($write)) $write.xy = $dword $rect.left = $read.x $rect.top = $read.y $rect.right = $read.x+1 $rect.bottom = $read.y+1 DllCall($user32dll,'bool','ClipCursor','struct*',$rect) EndFunc Func WM_INPUT($h,$m,$w,$l) Local Static $HEAD = 'struct;dword Type;dword Size;handle hDevice;wparam wParam;endstruct;' Local Static $HEADSIZE = DllStructGetSize(DllStructCreate($HEAD)) Local Static $MOU = $HEAD & 'ushort Flags;ushort Alignment;ushort ButtonFlags;short ButtonData;ulong RawButtons;long LastX;long LastY;ulong ExtraInformation;' Local Static $raw = DllStructCreate($MOU) DllCall ($user32dll,'uint','GetRawInputData','handle',$l,'uint',0x10000003,'struct*',$raw,'uint*',DllStructGetSize($raw),'uint',$HEADSIZE) If BitAnd(16,$raw.ButtonFlags) and not $w=1 Then $isPanning = True Local $fixPos = DllCall($user32dll,'dword','GetMessagePos')[0] AnchorCursor($fixPos) ElseIf BitAnd(32,$raw.ButtonFlags) Then $isPanning = False DllCall($user32dll,'bool','ClipCursor','struct*',Null) EndIf If $isPanning Then $xPos += $raw.LastX $yPos += $raw.LastY EndIf Return 0 EndFunc Func WM_MOUSEMOVE($h,$m,$w,$l) Local Static $write = DllStructCreate('dword xy') Local Static $read = DllStructCreate('short x;short y', DllStructGetPtr($write)) Local Static $last If BitAnd(1,$w) Then $write.xy = $last Local $oldX = $read.x , $oldY = $read.y $write.xy = $l Local $newX = $read.x , $newY = $read.y Local $dx = $newX - $oldX Local $dy = $newY - $oldY $xPos -= $dx $yPos -= $dy EndIf $last = $l Return 0 EndFunc Func Main() Local $winWidth = 800, $winHeight = 600 Local $winLeft = (@DesktopWidth - $winWidth)/2 , $winTop = (@DesktopHeight - $winHeight)/2 Local $hWnd = GUICreate('',$winWidth,$winHeight,$winLeft,$winTop,0x80000000) ; ,0x00080008) Local $hScreen = DllCall('user32.dll','handle','GetDC','handle',Null)[0] Local $hFront = DllCall('user32.dll','handle','GetDC','handle',$hWnd)[0] Local $hBack = DllCall('gdi32.dll','handle','CreateCompatibleDC','handle',$hScreen)[0] Local $hBitmap = DllCall('gdi32.dll','handle','CreateCompatibleBitmap','handle',$hScreen,'int',$winWidth,'int',$winHeight)[0] DllCall('gdi32.dll','handle','SelectObject','handle',$hBack,'handle',$hBitmap) DllCall('gdi32.dll','dword','SetDCPenColor','handle',$hBack,'dword',0x0000ff00) Local $curstate = DllStructCreate('long x;long y;long width;long height;long left;long top') $curstate.width = $winWidth $curstate.height = $winHeight $curstate.left = $winLeft $curstate.top = $winTop GUISetOnEvent(-3,Quit) GUISetState() HotKeySet('{esc}',Quit) While Sleep(10) $curstate.x = $xPos $curstate.y = $yPos DrawFrame($hFront,$hBack,$curstate) If $quit Then ExitLoop WEnd DllCall('user32.ll','int','ReleaseDC','handle',$hWnd,'handle',$hFront) DllCall('gdi32.dll','bool','DeleteObject','handle',$hBitmap) DllCall('gdi32.dll','bool','DeleteDC','handle',$hBack) EndFunc Func DrawFrame($hFront,$hBack,$state) Local Static $curPos = DllStructCreate('long x;long y') Local Static $horList = DllStructCreate('long x1;long y1;long x2;long y2;') Local Static $verList = DllStructCreate('long x1;long y1;long x2;long y2;') Local Static $dummyDC = DllCall($user32dll,'handle','GetDC','handle',Null)[0] Local Static $hPen = DllCall($gdi32dll,'handle','GetStockObject','int',19)[0] Local Static $hWhite = DllCall($gdi32dll,'handle','GetStockObject','int',6)[0] DllCall($gdi32dll,'bool','BitBlt','handle',$hBack,'int',0,'int',0,'int',$state.width,'int',$state.height,'handle',$dummyDC,'int',0,'int',0,'dword',0x42) DllCall($gdi32dll,'handle','SelectObject','handle',$hBack,'handle',$hWhite) Local $wrap = 160 Local $ceil = $wrap*Ceiling($state.height/$wrap/2) $horList.x1=0 $horList.x2=$state.width-1 For $i=-$ceil to $ceil Step $wrap Local $y = $state.height/2+$i - mod($state.y,$wrap) $horList.y1=$y $horList.y2=$y DllCall($gdi32dll,'bool','Polyline','handle',$hBack,'struct*',$horList,'int',2) Next Local $ceil = $wrap*Ceiling($state.width/$wrap/2) $verList.y1=0 $verList.y2=$state.height-1 For $i=-$ceil to $ceil Step $wrap Local $x = $state.width/2+$i - mod($state.x,$wrap) $verList.x1=$x $verList.x2=$x DllCall($gdi32dll,'bool','Polyline','handle',$hBack,'struct*',$verList,'int',2) Next DllCall($gdi32dll,'handle','SelectObject','handle',$hBack,'handle',$hPen) DllCall($user32dll,'dword','GetCursorPos','struct*',$curPos) $verList.y1=0 $verList.y2=$state.height-1 $verList.x1=$curPos.x - $state.left $verList.x2=$curPos.x - $state.left $horList.x1=0 $horList.x2=$state.width-1 $horList.y1=$curPos.y - $state.top $horList.y2=$curPos.y - $state.top DllCall($gdi32dll,'bool','Polyline','handle',$hBack,'struct*',$horList,'int',2) DllCall($gdi32dll,'bool','Polyline','handle',$hBack,'struct*',$verList,'int',2) DllCall($gdi32dll,'bool','BitBlt','handle',$hFront,'int',0,'int',0,'int',$state.width,'int',$state.height,'handle',$hBack,'int',0,'int',0,'dword',0xCC0020) EndFunc ```

https://github.com/libsdl-org/SDL/assets/77922942/3a21e37d-2ae1-4f45-b0fa-30f28e75e83d

slouken commented 1 year ago

I'm glad you have a solution that works for you. The SDL design came from systems where there is no absolute mouse position (it processed raw hardware deltas) and games that draw their own mouse cursor, but want the precision and unlimited motion of relative mode.

slouken commented 1 year ago

However, with SDL3, we have an opportunity to change that behavior. Let me mull that over for a bit.

expikr commented 1 year ago

Mainly I want an escape hatch to be able to access raw device deltas without dealing with all the baggage that RelativeMode forces upon you with respect to 1) force-hiding cursors (I had to manually access the WinAPI to unhide it), 2) messing with the cursor position on activation (I had to manually specify WinAPI ClipCursor to put it in a different spot), and 3) messing with the cursor position upon deactivation (needing the workaround as described by susko)

I'm glad you have a solution that works for you. The SDL design came from systems where there is no absolute mouse position (it processed raw hardware deltas) and games that draw their own mouse cursor, but want the precision and unlimited motion of relative mode.

I did not get a solution that works with SDL, the example posted was implemented in AutoIt using direct WinAPI calls

expikr commented 1 year ago

So I wrote this self-contained html page to test how browsers handle it, and Edge/Chrome works perfectly as I envisioned, you can test it out to get a grasp of what I'm describing.

(right click to pan, left click to drag)

<head>
<script>
window.addEventListener('load', (e) => {
    let x0 = 0;
    let y0 = 0;
    let sign = 1;
    let raw = false;
    let x=100;
    let y=100;
    let maindiv = document.getElementById('test');
    maindiv.innerHTML = 'hello world';
    let lockChangeAlert = () => {
      if (document.pointerLockElement === maindiv) {
        document.addEventListener("mousemove", updatePosition, false);
        raw = true;
      }
    }
    let updatePosition = (e) => {
        if ( e.buttons==0 && !raw ) {
            document.removeEventListener("mousemove", updatePosition, false);
            raw = false;
            return;
        }
        //  x = e.clientX;
        //  y = e.clientY;
        x += sign*e.movementX;
        y += sign*e.movementY;
        maindiv.innerHTML = x + ',' + y ;
        document.getElementById('object').style.left = x;
        document.getElementById('object').style.top = y;
    }
    document.addEventListener("pointerlockchange", lockChangeAlert, false);
    maindiv.addEventListener("mousedown", async (e) => {
        if (e.button==2) { 
            x0 = e.clientX;
            y0 = e.clientY;
            sign = -1;
            document.getElementById('anchor').style.left = x0;
            document.getElementById('anchor').style.top = y0;
            document.getElementById('anchor').style.visibility = 'visible';
            await maindiv.requestPointerLock({ unadjustedMovement: true,}); 
        } else if (e.button==0) {
            sign = 1;
            document.addEventListener("mousemove", updatePosition, false);
        }
    });
    maindiv.addEventListener("mouseup", () => {
        raw = false;
        document.exitPointerLock();
        document.removeEventListener("mousemove", updatePosition, false);
        document.getElementById('anchor').style.visibility = 'hidden';
    });
})
</script>
</head>
<div id='test' style='z-index:0;top:0;left:0;bottom:0;right:0;position:absolute;background-color:black;color:white'></div>
<div id='anchor' style='z-index:2;width:20px;height;20px;top:0;left:0;visibility:hidden;position:absolute;background-color:lime;color:lime'>.</div>
<div id='object' style='z-index:1;top:100px;left:100px;width:100%;position: absolute;color:red;'>Accusam et et nonummy ipsum feugiat dolore et eirmod sed. Magna dolores no sed rebum et possim diam diam diam labore ipsum amet id duo. Takimata nonumy consetetur amet magna et feugiat dolore velit consectetuer dolores amet consequat. Eos sanctus augue takimata rebum at lorem dolore takimata stet dolor et. Invidunt esse invidunt dolore et. Diam et adipiscing in nonumy odio magna. Dolore consequat amet et diam nobis duo est sea justo tempor nulla.</div>

(Sadly, my main browser Firefox inexplicably spazzes out when the pointer lock prompt hides itself, and the right click menu interferes with the panning)

Susko3 commented 1 year ago

Not sure how valid your test is, as it's running in quirks mode. If I add <!DOCTYPE html> to return to no-quirks mode, it behaves differently.

The mouse and the coordinates seem to be working the same in both modes, so that kinda proves your point.

expikr commented 1 year ago

Interesting, I've never written any html or javascript before so I didn't know about it.

Just tried it, it's still the same stuttery spazzling on Firefox with it added though, so it's definitely something wrong with Firefox's message handling.

slouken commented 1 year ago

Just thinking this through a little bit...

For an application to get the behavior you're asking for using the existing system is fairly easy:

I think the only thing that's tricky here is getting the same cursor that the OS uses, and at least on Windows you can write code to do that. An easier approach is to have a special panning cursor that you draw when you're in this mode.

expikr commented 1 year ago

Well, then why can't SDL just restore the cursor position to where it were before entering relative mode?

None of the four considerations you listed would affected by this fix, since the cursor is already being moved upon entering/exiting, you may as well just restore it to the correct place.

slouken commented 1 year ago

Well, SDL is processing relative motion and moving the cursor during relative mode (for the aforementioned scrolling map game), but we can certainly add a hint to freeze the mouse position while in relative mode.

expikr commented 1 year ago

No, that's not what I meant. I meant restore it to the right spot when you exit relative mode. This wouldn't affect any of the four points you raised, there really isn't any reason not to make this the exclusive behavior.

Susko3 commented 1 year ago

Reply to @slouken: The internal behaviour of the cursor probably should matter for this. Hiding the OS cursor while using relative mode is probably desired by most uses (eg. 3D camera) and required to hide the implementation details you mention. But what should be the visible behaviour. Where should the OS cursor be when relative mode is disabled (either gracefully by the application, or temporarily due to focus loss)?

(When looking into how this is currently handled, I discovered that it's not handled at all and leaks the internal implementation, so I'll be making new issues for that.)

Well, then why can't SDL just restore the cursor position to where it were before entering relative mode? [...] you may as well just restore it to the correct place.

What you consider the "correct place" might not be what another app/use case expects. And even if this was the desired behaviour, how should it work when relative mode was enabled when the cursor was outside the window (eg. alt-tabbing into a game while the cursor is on another monitor)? What about when the window is resized (eg. exiting full screen via hotkey)?

expikr commented 1 year ago

What you consider the "correct place" might not be what another app/use case expects. And even if this was the desired behaviour, how should it work when relative mode was enabled when the cursor was outside the window (eg. alt-tabbing into a game while the cursor is on another monitor)? What about when the window is resized (eg. exiting full screen via hotkey)?

Simple. You call ClipCursor first before saving the position.

Can you think of any situation in which having an arbitrary position restored upon exit of relativemode is desirable? At the very least, having it restored it to the center of the screen is still more predictable/logical than the current behavior, and not to mention that both Chromium and Firefox thinks that restoring to the pre-entry position makes the most sense.

slouken commented 1 year ago

No, that's not what I meant. I meant restore it to the right spot when you exit relative mode. This wouldn't affect any of the four points you raised, there really isn't any reason not to make this the exclusive behavior.

We currently move the mouse to SDL's internal mouse position when leaving relative mode, so freezing the mouse position has the effect you're asking for (I think?)

expikr commented 1 year ago

Ah I see, it's a bit ambiguous here whether we're talking about the OS logical cursor position or SDL's internal position when we say "mouse position". Indeed this will solve the usecase.

Perhaps can we also have a flag that keeps the cursor visible at where it's logically locked to (screen center), for those who really want the under-the-hood behavior to be visible?

Or maybe just make it a different API call altogether that has the more fine-grained control over what you want, rather than overloading the out-of-box experience of the RelativeMode API with flags? Something that lets you declare that you want to subscribe to a particular input source, rather than setting a global state. This architecture would also open the door for easier ManyMouse integration. These are just spurious suggestions so take it with a grain of salt, the main point is to conveying the existence of a desired usecase.

SDL has code internally to replicate the OS mouse acceleration curves, so for applications that do want normal mouse motion while also getting relative deltas (e.g. scrolling the map at the edge of the window), it is much easier for an application to draw a cursor at a fixed location than to try to replicate the mouse acceleration algorithm.

Out of curiosity, could you elaborate a bit more on what you meant with the map edge scrolling mechanic?

slouken commented 1 year ago

Let's say you have a real-time strategy game, and you're showing the cursor all the time (usually a custom cursor, based on the game state), and you're in the main map view where you can move the mouse to the edge of the screen to scroll the map. What you want to do is show the mouse at the edge of the screen, and not immediately start scrolling, but respond to additional movement beyond the edge of the screen for scrolling the map. A small movement will move the map a little bit, a large movement would move it a lot.

expikr commented 1 year ago

Interesting, do you know of any existing game that does this?

slouken commented 1 year ago

I've seen it in several games, but I don't remember which off the top of my head.

expikr commented 2 months ago

but we can certainly add a hint to freeze the mouse position while in relative mode.

Actually, there is already SDL_SetWindowMouseRect, but the documentation categorized it under the video section rather than the mouse section.

Perhaps this function would be more discoverable if listed under the mouse section too.

Love2D demo using SDL_SetWindowMouseRect with SDL_MOUSE_RELATIVE_MODE_CENTER and SDL_MOUSE_RELATIVE_CURSOR_VISIBLE (with modified SDL2):

Lua code ```lua local ffi = require("ffi") ffi.cdef[[ typedef struct { long x, y; } POINT; typedef struct SDL_Rect { int x, y; int w, h; } SDL_Rect; typedef enum {SDL_HINT_DEFAULT , SDL_HINT_NORMAL , SDL_HINT_OVERRIDE } SDL_HintPriority ; bool GetCursorPos(POINT *lpPoint); bool SDL_SetHintWithPriority(const char *name, const char *value, SDL_HintPriority priority); void *SDL_GL_GetCurrentWindow(void); int SDL_SetWindowMouseRect(void *window, const SDL_Rect * rect); ]] local CURPOS = ffi.new("POINT") local rect = ffi.new("SDL_Rect") local sdl = ffi.load('SDL2.dll') local sdl_wnd = sdl.SDL_GL_GetCurrentWindow() local offset_x = 0 local offset_y = 0 function love.mousemoved( x, y, dx, dy, istouch ) if not love.mouse.getRelativeMode() then return end offset_x = offset_x - dx offset_y = offset_y - dy end function love.mousepressed( x, y, button, istouch, presses ) if button==2 then rect.x,rect.y,rect.w,rect.h = x,y,1,1 sdl.SDL_SetWindowMouseRect(sdl_wnd, rect) love.mouse.setRelativeMode(true) end end function love.mousereleased( x, y, button, istouch, presses ) if button==2 then love.mouse.setRelativeMode(false) sdl.SDL_SetWindowMouseRect(sdl_wnd, nil) end end local toggle_force_center = false function love.keypressed( key, scancode, isrepeat ) if key == 'space' then toggle_force_center = not toggle_force_center if toggle_force_center then sdl.SDL_SetHintWithPriority('SDL_MOUSE_RELATIVE_MODE_CENTER','1',ffi.C.SDL_HINT_OVERRIDE) else sdl.SDL_SetHintWithPriority('SDL_MOUSE_RELATIVE_MODE_CENTER','0',ffi.C.SDL_HINT_OVERRIDE) end end end function love.draw() ffi.C.GetCursorPos(CURPOS) local x, y, displayindex = love.window.getPosition( ) local w, h = love.graphics.getDimensions( ) local mx, my = love.mouse.getPosition( ) local period = w / 5 love.graphics.setColor(1,1,1,0.25) for i=0,4 do love.graphics.line((offset_x + i*period)%w,1,(offset_x + i*period)%w,h) love.graphics.line(1,(offset_y + i*period)%w,w,(offset_y + i*period)%w) end love.graphics.setColor(1,0,1,1) love.graphics.line(1,1+my,w,1+my) love.graphics.line(1+mx,1,1+mx,h) love.graphics.setColor(0,1,0,1) love.graphics.print(CURPOS.x .. ',' .. CURPOS.y .. '\nSDL_MOUSE_RELATIVE_MODE_CENTER = ' .. tostring(toggle_force_center) .. " (space to goggle)") love.graphics.line(0,1+CURPOS.y-y,w,1+CURPOS.y-y) love.graphics.line(1+CURPOS.x-x,0,1+CURPOS.x-x,h) end function love.load() sdl.SDL_SetHintWithPriority('SDL_MOUSE_RELATIVE_MODE_CENTER','0',ffi.C.SDL_HINT_OVERRIDE) sdl.SDL_SetHintWithPriority('SDL_MOUSE_RELATIVE_CURSOR_VISIBLE','1',ffi.C.SDL_HINT_OVERRIDE) love.graphics.setLineStyle( 'rough' ) end ```
expikr commented 2 months ago

Let's say you have a real-time strategy game, and you're showing the cursor all the time (usually a custom cursor, based on the game state), and you're in the main map view where you can move the mouse to the edge of the screen to scroll the map. What you want to do is show the mouse at the edge of the screen, and not immediately start scrolling, but respond to additional movement beyond the edge of the screen for scrolling the map. A small movement will move the map a little bit, a large movement would move it a lot.

And here's the same demo modified to produce this behavior:

https://github.com/libsdl-org/SDL/assets/77922942/3a683ca8-2921-4cb3-8e28-fa6b51c3c50b

local ffi = require("ffi")
ffi.cdef[[
    typedef struct { long x, y; } POINT;
    typedef struct SDL_Rect { int x, y; int w, h; } SDL_Rect;
    typedef enum {SDL_HINT_DEFAULT , SDL_HINT_NORMAL , SDL_HINT_OVERRIDE } SDL_HintPriority ;
    bool GetCursorPos(POINT *lpPoint);
    bool SDL_SetHintWithPriority(const char *name,  const char *value, SDL_HintPriority priority);
    void *SDL_GL_GetCurrentWindow(void);
    int SDL_SetWindowMouseRect(void *window, const SDL_Rect * rect);
]]
local CURPOS = ffi.new("POINT")
local rect = ffi.new("SDL_Rect")
local sdl = ffi.load('SDL2.dll')
local sdl_wnd = sdl.SDL_GL_GetCurrentWindow()

local offset_x = 0
local offset_y = 0
function love.mousemoved( x, y, dx, dy, istouch )
    if love.mouse.isDown(2) then 
        offset_x = offset_x - dx
        offset_y = offset_y - dy
        return 
    end
    local w, h = love.graphics.getDimensions( )
    if not (0<x+dx and x+dx<w) then offset_x = offset_x - dx end
    if not (0<y+dy and y+dy<h) then offset_y = offset_y - dy end
end

function love.mousepressed( x, y, button, istouch, presses )
    if button==2 then
       rect.x,rect.y,rect.w,rect.h = x,y,1,1
       sdl.SDL_SetWindowMouseRect(sdl_wnd, rect)
    end
end

function love.mousereleased( x, y, button, istouch, presses )
    if button==2 then
       sdl.SDL_SetWindowMouseRect(sdl_wnd, nil)
    end
end

function love.update(dt)
    ffi.C.GetCursorPos(CURPOS)
    local wx, wy = love.window.getPosition( )
    local mx, my = love.mouse.getPosition( )
    if CURPOS.x ~= mx+wx or CURPOS.y ~= my+wy then love.mouse.setPosition(CURPOS.x-wx, CURPOS.y-wy) end
end

function love.draw()
    local x, y, displayindex = love.window.getPosition( )
    local w, h = love.graphics.getDimensions( )
    local mx, my = love.mouse.getPosition( )
    local period = w / 5
    love.graphics.setColor(1,1,1,0.25)
    for i=0,4 do
        love.graphics.line((offset_x + i*period)%w,1,(offset_x + i*period)%w,h)
        love.graphics.line(1,(offset_y + i*period)%w,w,(offset_y + i*period)%w)
    end
    love.graphics.setColor(1,0,1,1)
    love.graphics.line(1,1+my,w,1+my)
    love.graphics.line(1+mx,1,1+mx,h)
    love.graphics.setColor(0,1,0,1)
    love.graphics.print(CURPOS.x .. ',' .. CURPOS.y)
    love.graphics.line(0,1+CURPOS.y-y,w,1+CURPOS.y-y)
    love.graphics.line(1+CURPOS.x-x,0,1+CURPOS.x-x,h)
end

function love.load()
    sdl.SDL_SetHintWithPriority('SDL_MOUSE_RELATIVE_MODE_CENTER','0',ffi.C.SDL_HINT_OVERRIDE)
    sdl.SDL_SetHintWithPriority('SDL_MOUSE_RELATIVE_CURSOR_VISIBLE','1',ffi.C.SDL_HINT_OVERRIDE)
    love.graphics.setLineStyle( 'rough' )
    love.mouse.setRelativeMode(true)
end