From 8071c8c8c044ee56bc7578a4ba3178d2d03733db Mon Sep 17 00:00:00 2001
From: Ac_K <Acoustik666@gmail.com>
Date: Sun, 15 Jan 2023 01:05:44 +0100
Subject: [PATCH] Ava UI: Fixes "Hide Cursor on Idle" for Windows (#4266)

* Ava: Fixes "Hide Cursor on Idle" for Windows

* Add check in MouseDriver and reduce the time of idling

* Fix linux error

* Change idle time everywhere for consistencies
---
 Ryujinx.Ava/AppHost.cs                       | 128 +++++++++++--------
 Ryujinx.Ava/Input/AvaloniaMouseDriver.cs     |  32 ++++-
 Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs     |   7 +-
 Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs |  16 +++
 Ryujinx.Headless.SDL2/SDL2MouseDriver.cs     |   2 +-
 Ryujinx/Ui/RendererWidgetBase.cs             |   2 +-
 6 files changed, 125 insertions(+), 62 deletions(-)

diff --git a/Ryujinx.Ava/AppHost.cs b/Ryujinx.Ava/AppHost.cs
index 65f84c494c..5c4f5bd8b2 100644
--- a/Ryujinx.Ava/AppHost.cs
+++ b/Ryujinx.Ava/AppHost.cs
@@ -44,8 +44,10 @@ using System;
 using System.Collections.Generic;
 using System.Diagnostics;
 using System.IO;
+using System.Runtime.Versioning;
 using System.Threading;
 using System.Threading.Tasks;
+using static Ryujinx.Ava.UI.Helpers.Win32NativeInterop;
 using Image = SixLabors.ImageSharp.Image;
 using InputManager = Ryujinx.Input.HLE.InputManager;
 using Key = Ryujinx.Input.Key;
@@ -58,12 +60,14 @@ namespace Ryujinx.Ava
 {
     internal class AppHost
     {
-        private const int   CursorHideIdleTime = 8;    // Hide Cursor seconds.
+        private const int   CursorHideIdleTime = 5;    // Hide Cursor seconds.
         private const float MaxResolutionScale = 4.0f; // Max resolution hotkeys can scale to before wrapping.
         private const int   TargetFps          = 60;
         private const float VolumeDelta        = 0.05f;
 
         private static readonly Cursor InvisibleCursor = new(StandardCursorType.None);
+        private readonly IntPtr InvisibleCursorWin;
+        private readonly IntPtr DefaultCursorWin;
 
         private readonly long      _ticksPerFrame;
         private readonly Stopwatch _chrono;
@@ -81,7 +85,6 @@ namespace Ryujinx.Ava
         private float                       _newVolume;
         private KeyboardHotkeyState         _prevHotkeyState;
 
-        private bool _hideCursorOnIdle;
         private long _lastCursorMoveTime;
         private bool _isCursorInRenderer;
 
@@ -131,7 +134,6 @@ namespace Ryujinx.Ava
             _accountManager         = accountManager;
             _userChannelPersistence = userChannelPersistence;
             _renderingThread        = new Thread(RenderLoop, 1 * 1024 * 1024) { Name = "GUI.RenderThread" };
-            _hideCursorOnIdle       = ConfigurationState.Instance.HideCursorOnIdle;
             _lastCursorMoveTime     = Stopwatch.GetTimestamp();
             _glLogLevel             = ConfigurationState.Instance.Logger.GraphicsDebugLevel;
             _topLevel               = topLevel;
@@ -159,9 +161,14 @@ namespace Ryujinx.Ava
 
             ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorState_Changed;
 
-            _topLevel.PointerLeave += TopLevel_PointerLeave;
             _topLevel.PointerMoved += TopLevel_PointerMoved;
 
+            if (OperatingSystem.IsWindows())
+            {
+                InvisibleCursorWin = CreateEmptyCursor();
+                DefaultCursorWin   = CreateArrowCursor();
+            }
+
             ConfigurationState.Instance.System.IgnoreMissingServices.Event += UpdateIgnoreMissingServicesState;
             ConfigurationState.Instance.Graphics.AspectRatio.Event         += UpdateAspectRatioState;
             ConfigurationState.Instance.System.EnableDockedMode.Event      += UpdateDockedModeState;
@@ -172,20 +179,47 @@ namespace Ryujinx.Ava
 
         private void TopLevel_PointerMoved(object sender, PointerEventArgs e)
         {
-            if (sender is Control visual)
+            if (sender is MainWindow window)
             {
                 _lastCursorMoveTime = Stopwatch.GetTimestamp();
 
-                var point = e.GetCurrentPoint(visual).Position;
+                if ((Renderer.Content as EmbeddedWindow).TransformedBounds != null)
+                {
+                    var point  = e.GetCurrentPoint(window).Position;
+                    var bounds = (Renderer.Content as EmbeddedWindow).TransformedBounds.Value.Clip;
 
-                _isCursorInRenderer = Equals(visual.InputHitTest(point), Renderer);
+                    _isCursorInRenderer = point.X >= bounds.X &&
+                                          point.X <= bounds.Width + bounds.X &&
+                                          point.Y >= bounds.Y &&
+                                          point.Y <= bounds.Height + bounds.Y;
+                }
             }
         }
 
-        private void TopLevel_PointerLeave(object sender, PointerEventArgs e)
+        private void ShowCursor()
         {
-            _isCursorInRenderer = false;
-            _viewModel.Cursor   = Cursor.Default;
+            Dispatcher.UIThread.Post(() =>
+            {
+                _viewModel.Cursor = Cursor.Default;
+
+                if (OperatingSystem.IsWindows())
+                {
+                    SetCursor(DefaultCursorWin);
+                }
+            });
+        }
+
+        private void HideCursor()
+        {
+            Dispatcher.UIThread.Post(() =>
+            {
+                _viewModel.Cursor = InvisibleCursor;
+
+                if (OperatingSystem.IsWindows())
+                {
+                    SetCursor(InvisibleCursorWin);
+                }
+            });
         }
 
         private void SetRendererWindowSize(Size size)
@@ -380,7 +414,6 @@ namespace Ryujinx.Ava
             ConfigurationState.Instance.System.EnableDockedMode.Event      -= UpdateDockedModeState;
             ConfigurationState.Instance.System.AudioVolume.Event           -= UpdateAudioVolumeState;
 
-            _topLevel.PointerLeave -= TopLevel_PointerLeave;
             _topLevel.PointerMoved -= TopLevel_PointerMoved;
 
             _gpuCancellationTokenSource.Cancel();
@@ -406,19 +439,10 @@ namespace Ryujinx.Ava
 
         private void HideCursorState_Changed(object sender, ReactiveEventArgs<bool> state)
         {
-            Dispatcher.UIThread.InvokeAsync(delegate
+            if (state.NewValue)
             {
-                _hideCursorOnIdle = state.NewValue;
-
-                if (_hideCursorOnIdle)
-                {
-                    _lastCursorMoveTime = Stopwatch.GetTimestamp();
-                }
-                else
-                {
-                    _viewModel.Cursor = Cursor.Default;
-                }
-            });
+                _lastCursorMoveTime = Stopwatch.GetTimestamp();
+            }
         }
 
         public async Task<bool> LoadGuestApplication()
@@ -860,29 +884,6 @@ namespace Ryujinx.Ava
             }
         }
 
-        private void HandleScreenState()
-        {
-            if (ConfigurationState.Instance.Hid.EnableMouse)
-            {
-                Dispatcher.UIThread.Post(() =>
-                {
-                    _viewModel.Cursor = _isCursorInRenderer ? InvisibleCursor : Cursor.Default;
-                });
-            }
-            else
-            {
-                if (_hideCursorOnIdle)
-                {
-                    long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
-
-                    Dispatcher.UIThread.Post(() =>
-                    {
-                        _viewModel.Cursor = cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency ? InvisibleCursor : Cursor.Default;
-                    });
-                }
-            }
-        }
-
         private bool UpdateFrame()
         {
             if (!_isActive)
@@ -890,23 +891,44 @@ namespace Ryujinx.Ava
                 return false;
             }
 
+            NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
+
             if (_viewModel.IsActive)
             {
+                if (ConfigurationState.Instance.Hid.EnableMouse)
+                {
+                    if (_isCursorInRenderer)
+                    {
+                        HideCursor();
+                    }
+                    else
+                    {
+                        ShowCursor();
+                    }
+                }
+                else
+                {
+                    if (ConfigurationState.Instance.HideCursorOnIdle)
+                    {
+                        if (Stopwatch.GetTimestamp() - _lastCursorMoveTime >= CursorHideIdleTime * Stopwatch.Frequency)
+                        {
+                            HideCursor();
+                        }
+                        else
+                        {
+                            ShowCursor();
+                        }
+                    }
+                }
+
                 Dispatcher.UIThread.Post(() =>
                 {
-                    HandleScreenState();
-
                     if (_keyboardInterface.GetKeyboardStateSnapshot().IsPressed(Key.Delete) && _viewModel.WindowState != WindowState.FullScreen)
                     {
                         Device.Application.DiskCacheLoadState?.Cancel();
                     }
                 });
-            }
 
-            NpadManager.Update(ConfigurationState.Instance.Graphics.AspectRatio.Value.ToFloat());
-
-            if (_viewModel.IsActive)
-            {
                 KeyboardHotkeyState currentHotkeyState = GetHotkeyState();
 
                 if (currentHotkeyState != _prevHotkeyState)
diff --git a/Ryujinx.Ava/Input/AvaloniaMouseDriver.cs b/Ryujinx.Ava/Input/AvaloniaMouseDriver.cs
index b0b6cdf05a..b3e1a21a14 100644
--- a/Ryujinx.Ava/Input/AvaloniaMouseDriver.cs
+++ b/Ryujinx.Ava/Input/AvaloniaMouseDriver.cs
@@ -1,6 +1,7 @@
 using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Input;
+using FluentAvalonia.Core;
 using Ryujinx.Input;
 using System;
 using System.Numerics;
@@ -69,12 +70,22 @@ namespace Ryujinx.Ava.Input
 
         private void Parent_PointerReleaseEvent(object o, PointerReleasedEventArgs args)
         {
-            PressedButtons[(int)args.InitialPressMouseButton - 1] = false;
+            int button = (int)args.InitialPressMouseButton - 1;
+
+            if (PressedButtons.Count() >= button)
+            {
+                PressedButtons[button] = false;
+            }
         }
 
         private void Parent_PointerPressEvent(object o, PointerPressedEventArgs args)
         {
-            PressedButtons[(int)args.GetCurrentPoint(_widget).Properties.PointerUpdateKind] = true;
+            int button = (int)args.GetCurrentPoint(_widget).Properties.PointerUpdateKind;
+
+            if (PressedButtons.Count() >= button)
+            {
+                PressedButtons[button] = true;
+            }
         }
 
         private void Parent_PointerMovedEvent(object o, PointerEventArgs args)
@@ -86,12 +97,18 @@ namespace Ryujinx.Ava.Input
 
         public void SetMousePressed(MouseButton button)
         {
-            PressedButtons[(int)button] = true;
+            if (PressedButtons.Count() >= (int)button)
+            {
+                PressedButtons[(int)button] = true;
+            }
         }
 
         public void SetMouseReleased(MouseButton button)
         {
-            PressedButtons[(int)button] = false;
+            if (PressedButtons.Count() >= (int)button)
+            {
+                PressedButtons[(int)button] = false;
+            }
         }
 
         public void SetPosition(double x, double y)
@@ -101,7 +118,12 @@ namespace Ryujinx.Ava.Input
 
         public bool IsButtonPressed(MouseButton button)
         {
-            return PressedButtons[(int)button];
+            if (PressedButtons.Count() >= (int)button)
+            {
+                return PressedButtons[(int)button];
+            }
+
+            return false;
         }
 
         public Size GetClientSize()
diff --git a/Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs b/Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs
index 8247a89b59..67ab80aa70 100644
--- a/Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs
+++ b/Ryujinx.Ava/UI/Helpers/EmbeddedWindow.cs
@@ -34,6 +34,8 @@ namespace Ryujinx.Ava.UI.Helpers
         {
             WindowHandle = IntPtr.Zero;
             X11Display = IntPtr.Zero;
+            NsView = IntPtr.Zero;
+            MetalLayer = IntPtr.Zero;
         }
 
         public EmbeddedWindow()
@@ -42,7 +44,7 @@ namespace Ryujinx.Ava.UI.Helpers
 
             stateObserverable.Subscribe(StateChanged);
 
-            this.Initialized += NativeEmbeddedWindow_Initialized;
+            Initialized += NativeEmbeddedWindow_Initialized;
         }
 
         public virtual void OnWindowCreated() { }
@@ -127,7 +129,7 @@ namespace Ryujinx.Ava.UI.Helpers
                 lpfnWndProc = Marshal.GetFunctionPointerForDelegate(_wndProcDelegate),
                 style = ClassStyles.CS_OWNDC,
                 lpszClassName = Marshal.StringToHGlobalUni(_className),
-                hCursor = LoadCursor(IntPtr.Zero, (IntPtr)Cursors.IDC_ARROW)
+                hCursor = CreateArrowCursor()
             };
 
             var atom = RegisterClassEx(ref wndClassEx);
@@ -198,6 +200,7 @@ namespace Ryujinx.Ava.UI.Helpers
                         KeyModifiers.None));
                     break;
             }
+
             return DefWindowProc(hWnd, msg, wParam, lParam);
         }
 
diff --git a/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs b/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs
index 1e6e3c3bd3..03d3a49f3f 100644
--- a/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs
+++ b/Ryujinx.Ava/UI/Helpers/Win32NativeInterop.cs
@@ -70,6 +70,22 @@ namespace Ryujinx.Ava.UI.Helpers
             }
         }
 
+        public static IntPtr CreateEmptyCursor()
+        {
+            return CreateCursor(IntPtr.Zero, 0, 0, 1, 1, new byte[] { 0xFF }, new byte[] { 0x00 });
+        }
+
+        public static IntPtr CreateArrowCursor()
+        {
+            return LoadCursor(IntPtr.Zero, (IntPtr)Cursors.IDC_ARROW);
+        }
+
+        [LibraryImport("user32.dll")]
+        public static partial IntPtr SetCursor(IntPtr handle);
+
+        [LibraryImport("user32.dll")]
+        public static partial IntPtr CreateCursor(IntPtr hInst, int xHotSpot, int yHotSpot, int nWidth, int nHeight, byte[] pvANDPlane, byte[] pvXORPlane);
+
         [LibraryImport("user32.dll", SetLastError = true, EntryPoint = "RegisterClassExW")]
         public static partial ushort RegisterClassEx(ref WNDCLASSEX param);
 
diff --git a/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs b/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs
index bdf428cc4e..8c3412ff9a 100644
--- a/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs
+++ b/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs
@@ -10,7 +10,7 @@ namespace Ryujinx.Headless.SDL2
 {
     class SDL2MouseDriver : IGamepadDriver
     {
-        private const int CursorHideIdleTime = 8; // seconds
+        private const int CursorHideIdleTime = 5; // seconds
 
         private bool _isDisposed;
         private HideCursor _hideCursor;
diff --git a/Ryujinx/Ui/RendererWidgetBase.cs b/Ryujinx/Ui/RendererWidgetBase.cs
index 8db023bec5..4bf2a70ff3 100644
--- a/Ryujinx/Ui/RendererWidgetBase.cs
+++ b/Ryujinx/Ui/RendererWidgetBase.cs
@@ -68,7 +68,7 @@ namespace Ryujinx.Ui
         private readonly CancellationTokenSource _gpuCancellationTokenSource;
 
         // Hide Cursor
-        const int CursorHideIdleTime = 8; // seconds
+        const int CursorHideIdleTime = 5; // seconds
         private static readonly Cursor _invisibleCursor = new Cursor(Display.Default, CursorType.BlankCursor);
         private long _lastCursorMoveTime;
         private bool _hideCursorOnIdle;