From dd574146fb5f05c1c0a469a4ad4a20c46bb37d74 Mon Sep 17 00:00:00 2001
From: TSRBerry <20988865+TSRBerry@users.noreply.github.com>
Date: Tue, 2 May 2023 03:29:47 +0200
Subject: [PATCH] Add hide-cursor command line argument & always hide cursor
 option (#4613)

* Add hide-cursor command line argument

* gtk: Adjust SettingsWindow for hide cursor options

* ava: Adjust SettingsWindow for hide cursor options

* ava: Add override check for HideCursor arg

* Remove copy&paste sins

* ava: Leave a little more room between the options

* gtk: Fix hide cursor issues

* ava: Only hide cursor if it's within the embedded window
---
 src/Ryujinx.Ava/AppHost.cs                    | 44 ++++++-----
 src/Ryujinx.Ava/Assets/Locales/en_US.json     |  5 +-
 src/Ryujinx.Ava/Program.cs                    | 14 +++-
 .../UI/ViewModels/SettingsViewModel.cs        |  6 +-
 .../UI/Views/Settings/SettingsUIView.axaml    | 31 ++++++--
 .../Configuration/HideCursorMode.cs           |  9 +++
 src/Ryujinx.Headless.SDL2/HideCursor.cs       |  9 ---
 .../OpenGL/OpenGLWindow.cs                    |  4 +-
 src/Ryujinx.Headless.SDL2/Options.cs          |  4 +-
 src/Ryujinx.Headless.SDL2/Program.cs          |  4 +-
 src/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs  | 13 ++--
 .../Vulkan/VulkanWindow.cs                    |  4 +-
 src/Ryujinx.Headless.SDL2/WindowBase.cs       |  4 +-
 .../Configuration/ConfigurationFileFormat.cs  |  6 +-
 .../Configuration/ConfigurationState.cs       | 12 +--
 .../Helper/CommandLineState.cs                | 11 +++
 src/Ryujinx/Program.cs                        | 16 +++-
 src/Ryujinx/Ui/RendererWidgetBase.cs          | 59 +++++++++-----
 src/Ryujinx/Ui/Windows/SettingsWindow.cs      | 32 ++++++--
 src/Ryujinx/Ui/Windows/SettingsWindow.glade   | 78 +++++++++++++++++--
 20 files changed, 266 insertions(+), 99 deletions(-)
 create mode 100644 src/Ryujinx.Common/Configuration/HideCursorMode.cs
 delete mode 100644 src/Ryujinx.Headless.SDL2/HideCursor.cs

diff --git a/src/Ryujinx.Ava/AppHost.cs b/src/Ryujinx.Ava/AppHost.cs
index 957a1c9d3..e11a954d8 100644
--- a/src/Ryujinx.Ava/AppHost.cs
+++ b/src/Ryujinx.Ava/AppHost.cs
@@ -157,7 +157,7 @@ namespace Ryujinx.Ava
                 _isFirmwareTitle = true;
             }
 
-            ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorState_Changed;
+            ConfigurationState.Instance.HideCursor.Event += HideCursorState_Changed;
 
             _topLevel.PointerMoved += TopLevel_PointerMoved;
 
@@ -468,9 +468,9 @@ namespace Ryujinx.Ava
             (_rendererHost.EmbeddedWindow as EmbeddedWindowOpenGL)?.MakeCurrent(null);
         }
 
-        private void HideCursorState_Changed(object sender, ReactiveEventArgs<bool> state)
+        private void HideCursorState_Changed(object sender, ReactiveEventArgs<HideCursorMode> state)
         {
-            if (state.NewValue)
+            if (state.NewValue == HideCursorMode.OnIdle)
             {
                 _lastCursorMoveTime = Stopwatch.GetTimestamp();
             }
@@ -965,30 +965,38 @@ namespace Ryujinx.Ava
 
             if (_viewModel.IsActive)
             {
-                if (ConfigurationState.Instance.Hid.EnableMouse)
+                if (_isCursorInRenderer)
                 {
-                    if (_isCursorInRenderer)
+                    if (ConfigurationState.Instance.Hid.EnableMouse)
                     {
                         HideCursor();
                     }
                     else
                     {
-                        ShowCursor();
+                        switch (ConfigurationState.Instance.HideCursor.Value)
+                        {
+                            case HideCursorMode.Never:
+                                ShowCursor();
+                                break;
+                            case HideCursorMode.OnIdle:
+                                if (Stopwatch.GetTimestamp() - _lastCursorMoveTime >= CursorHideIdleTime * Stopwatch.Frequency)
+                                {
+                                    HideCursor();
+                                }
+                                else
+                                {
+                                    ShowCursor();
+                                }
+                                break;
+                            case HideCursorMode.Always:
+                                HideCursor();
+                                break;
+                        }
                     }
                 }
                 else
                 {
-                    if (ConfigurationState.Instance.HideCursorOnIdle)
-                    {
-                        if (Stopwatch.GetTimestamp() - _lastCursorMoveTime >= CursorHideIdleTime * Stopwatch.Frequency)
-                        {
-                            HideCursor();
-                        }
-                        else
-                        {
-                            ShowCursor();
-                        }
-                    }
+                    ShowCursor();
                 }
 
                 Dispatcher.UIThread.Post(() =>
@@ -1133,4 +1141,4 @@ namespace Ryujinx.Ava
             return state;
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Ava/Assets/Locales/en_US.json b/src/Ryujinx.Ava/Assets/Locales/en_US.json
index 3a4bfc65e..617cad34f 100644
--- a/src/Ryujinx.Ava/Assets/Locales/en_US.json
+++ b/src/Ryujinx.Ava/Assets/Locales/en_US.json
@@ -80,7 +80,10 @@
   "SettingsTabGeneralEnableDiscordRichPresence": "Enable Discord Rich Presence",
   "SettingsTabGeneralCheckUpdatesOnLaunch": "Check for Updates on Launch",
   "SettingsTabGeneralShowConfirmExitDialog": "Show \"Confirm Exit\" Dialog",
-  "SettingsTabGeneralHideCursorOnIdle": "Hide Cursor on Idle",
+  "SettingsTabGeneralHideCursor": "Hide Cursor:",
+  "SettingsTabGeneralHideCursorNever": "Never",
+  "SettingsTabGeneralHideCursorOnIdle": "On Idle",
+  "SettingsTabGeneralHideCursorAlways": "Always",
   "SettingsTabGeneralGameDirectories": "Game Directories",
   "SettingsTabGeneralAdd": "Add",
   "SettingsTabGeneralRemove": "Remove",
diff --git a/src/Ryujinx.Ava/Program.cs b/src/Ryujinx.Ava/Program.cs
index 7f35c62a4..0629e6062 100644
--- a/src/Ryujinx.Ava/Program.cs
+++ b/src/Ryujinx.Ava/Program.cs
@@ -183,6 +183,18 @@ namespace Ryujinx.Ava
             {
                 ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value;
             }
+
+            // Check if HideCursor was overridden.
+            if (CommandLineState.OverrideHideCursor is not null)
+            {
+                ConfigurationState.Instance.HideCursor.Value = CommandLineState.OverrideHideCursor!.ToLower() switch
+                {
+                    "never" => HideCursorMode.Never,
+                    "onidle" => HideCursorMode.OnIdle,
+                    "always" => HideCursorMode.Always,
+                    _ => ConfigurationState.Instance.HideCursor.Value
+                };
+            }
         }
 
         private static void PrintSystemInfo()
@@ -226,4 +238,4 @@ namespace Ryujinx.Ava
             Logger.Shutdown();
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs b/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs
index 232c9d436..08612117a 100644
--- a/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs
+++ b/src/Ryujinx.Ava/UI/ViewModels/SettingsViewModel.cs
@@ -132,7 +132,7 @@ namespace Ryujinx.Ava.UI.ViewModels
         public bool EnableDiscordIntegration { get; set; }
         public bool CheckUpdatesOnStart { get; set; }
         public bool ShowConfirmExit { get; set; }
-        public bool HideCursorOnIdle { get; set; }
+        public int HideCursor { get; set; }
         public bool EnableDockedMode { get; set; }
         public bool EnableKeyboard { get; set; }
         public bool EnableMouse { get; set; }
@@ -375,7 +375,7 @@ namespace Ryujinx.Ava.UI.ViewModels
             EnableDiscordIntegration = config.EnableDiscordIntegration;
             CheckUpdatesOnStart = config.CheckUpdatesOnStart;
             ShowConfirmExit = config.ShowConfirmExit;
-            HideCursorOnIdle = config.HideCursorOnIdle;
+            HideCursor = (int)config.HideCursor.Value;
 
             GameDirectories.Clear();
             GameDirectories.AddRange(config.Ui.GameDirs.Value);
@@ -458,7 +458,7 @@ namespace Ryujinx.Ava.UI.ViewModels
             config.EnableDiscordIntegration.Value = EnableDiscordIntegration;
             config.CheckUpdatesOnStart.Value = CheckUpdatesOnStart;
             config.ShowConfirmExit.Value = ShowConfirmExit;
-            config.HideCursorOnIdle.Value = HideCursorOnIdle;
+            config.HideCursor.Value = (HideCursorMode)HideCursor;
 
             if (_directoryChanged)
             {
diff --git a/src/Ryujinx.Ava/UI/Views/Settings/SettingsUIView.axaml b/src/Ryujinx.Ava/UI/Views/Settings/SettingsUIView.axaml
index 61b6c4335..acc5e2b70 100644
--- a/src/Ryujinx.Ava/UI/Views/Settings/SettingsUIView.axaml
+++ b/src/Ryujinx.Ava/UI/Views/Settings/SettingsUIView.axaml
@@ -1,4 +1,4 @@
-<UserControl 
+<UserControl
     x:Class="Ryujinx.Ava.UI.Views.Settings.SettingsUIView"
     xmlns="https://github.com/avaloniaui"
     xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
@@ -12,7 +12,7 @@
     <Design.DataContext>
         <viewModels:SettingsViewModel />
     </Design.DataContext>
-    <ScrollViewer 
+    <ScrollViewer
         Name="UiPage"
         HorizontalAlignment="Stretch"
         VerticalAlignment="Stretch"
@@ -37,9 +37,24 @@
                     <CheckBox IsChecked="{Binding ShowConfirmExit}">
                         <TextBlock Text="{locale:Locale SettingsTabGeneralShowConfirmExitDialog}" />
                     </CheckBox>
-                    <CheckBox IsChecked="{Binding HideCursorOnIdle}">
-                        <TextBlock Text="{locale:Locale SettingsTabGeneralHideCursorOnIdle}" />
-                    </CheckBox>
+                    <StackPanel Margin="0, 15, 0, 10" Orientation="Horizontal">
+                        <TextBlock VerticalAlignment="Center"
+                                   Text="{locale:Locale SettingsTabGeneralHideCursor}"
+                                   Width="150" />
+                        <ComboBox SelectedIndex="{Binding HideCursor}"
+                                  HorizontalContentAlignment="Left"
+                                  MinWidth="100">
+                            <ComboBoxItem>
+                                <TextBlock Text="{locale:Locale SettingsTabGeneralHideCursorNever}" />
+                            </ComboBoxItem>
+                            <ComboBoxItem>
+                                <TextBlock Text="{locale:Locale SettingsTabGeneralHideCursorOnIdle}" />
+                            </ComboBoxItem>
+                            <ComboBoxItem>
+                                <TextBlock Text="{locale:Locale SettingsTabGeneralHideCursorAlways}" />
+                            </ComboBoxItem>
+                        </ComboBox>
+                    </StackPanel>
                 </StackPanel>
                 <Separator Height="1" />
                 <TextBlock Classes="h1" Text="{locale:Locale SettingsTabGeneralGameDirectories}" />
@@ -105,7 +120,7 @@
                         <RowDefinition />
                         <RowDefinition />
                     </Grid.RowDefinitions>
-                    <CheckBox 
+                    <CheckBox
                         IsChecked="{Binding EnableCustomTheme}"
                         ToolTip.Tip="{locale:Locale CustomThemeCheckTooltip}">
                         <TextBlock Text="{locale:Locale SettingsTabGeneralThemeEnableCustomTheme}" />
@@ -122,7 +137,7 @@
                         Grid.Column="1"
                         Margin="0,10,0,0"
                         Text="{Binding CustomThemePath}" />
-                    <Button 
+                    <Button
                         Grid.Row="1"
                         Grid.Column="2"
                         Margin="10,10,0,0"
@@ -153,4 +168,4 @@
             </StackPanel>
         </Border>
     </ScrollViewer>
-</UserControl>
\ No newline at end of file
+</UserControl>
diff --git a/src/Ryujinx.Common/Configuration/HideCursorMode.cs b/src/Ryujinx.Common/Configuration/HideCursorMode.cs
new file mode 100644
index 000000000..895fc1076
--- /dev/null
+++ b/src/Ryujinx.Common/Configuration/HideCursorMode.cs
@@ -0,0 +1,9 @@
+namespace Ryujinx.Common.Configuration
+{
+    public enum HideCursorMode
+    {
+        Never,
+        OnIdle,
+        Always
+    }
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Headless.SDL2/HideCursor.cs b/src/Ryujinx.Headless.SDL2/HideCursor.cs
deleted file mode 100644
index 2dc0bd6ab..000000000
--- a/src/Ryujinx.Headless.SDL2/HideCursor.cs
+++ /dev/null
@@ -1,9 +0,0 @@
-namespace Ryujinx.Headless.SDL2
-{
-    public enum HideCursor
-    {
-        Never,
-        OnIdle,
-        Always
-    }
-}
\ No newline at end of file
diff --git a/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs b/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs
index 69b0f42fb..dc7d811b9 100644
--- a/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs
+++ b/src/Ryujinx.Headless.SDL2/OpenGL/OpenGLWindow.cs
@@ -107,8 +107,8 @@ namespace Ryujinx.Headless.SDL2.OpenGL
             GraphicsDebugLevel glLogLevel,
             AspectRatio aspectRatio,
             bool enableMouse,
-            HideCursor hideCursor)
-            : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursor)
+            HideCursorMode hideCursorMode)
+            : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode)
         {
             _glLogLevel = glLogLevel;
         }
diff --git a/src/Ryujinx.Headless.SDL2/Options.cs b/src/Ryujinx.Headless.SDL2/Options.cs
index 982d09909..7dffa1b00 100644
--- a/src/Ryujinx.Headless.SDL2/Options.cs
+++ b/src/Ryujinx.Headless.SDL2/Options.cs
@@ -76,8 +76,8 @@ namespace Ryujinx.Headless.SDL2
         [Option("enable-mouse", Required = false, Default = false, HelpText = "Enable or disable mouse support.")]
         public bool EnableMouse { get; set; }
 
-        [Option("hide-cursor", Required = false, Default = HideCursor.OnIdle, HelpText = "Change when the cursor gets hidden.")]
-        public HideCursor HideCursor { get; set; }
+        [Option("hide-cursor", Required = false, Default = HideCursorMode.OnIdle, HelpText = "Change when the cursor gets hidden.")]
+        public HideCursorMode HideCursorMode { get; set; }
 
         [Option("list-input-profiles", Required = false, HelpText = "List inputs profiles.")]
         public bool ListInputProfiles { get; set; }
diff --git a/src/Ryujinx.Headless.SDL2/Program.cs b/src/Ryujinx.Headless.SDL2/Program.cs
index b0bdb97f1..453c470e7 100644
--- a/src/Ryujinx.Headless.SDL2/Program.cs
+++ b/src/Ryujinx.Headless.SDL2/Program.cs
@@ -478,8 +478,8 @@ namespace Ryujinx.Headless.SDL2
         private static WindowBase CreateWindow(Options options)
         {
             return options.GraphicsBackend == GraphicsBackend.Vulkan
-                ? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursor)
-                : new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursor);
+                ? new VulkanWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode)
+                : new OpenGLWindow(_inputManager, options.LoggingGraphicsDebugLevel, options.AspectRatio, options.EnableMouse, options.HideCursorMode);
         }
 
         private static IRenderer CreateRenderer(Options options, WindowBase window)
diff --git a/src/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs b/src/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs
index 8c3412ff9..7b88e2655 100644
--- a/src/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs
+++ b/src/Ryujinx.Headless.SDL2/SDL2MouseDriver.cs
@@ -1,4 +1,5 @@
-using Ryujinx.Input;
+using Ryujinx.Common.Configuration;
+using Ryujinx.Input;
 using System;
 using System.Diagnostics;
 using System.Drawing;
@@ -13,7 +14,7 @@ namespace Ryujinx.Headless.SDL2
         private const int CursorHideIdleTime = 5; // seconds
 
         private bool _isDisposed;
-        private HideCursor _hideCursor;
+        private HideCursorMode _hideCursorMode;
         private bool _isHidden;
         private long _lastCursorMoveTime;
 
@@ -23,12 +24,12 @@ namespace Ryujinx.Headless.SDL2
         public Vector2 Scroll { get; private set; }
         public Size _clientSize;
 
-        public SDL2MouseDriver(HideCursor hideCursor)
+        public SDL2MouseDriver(HideCursorMode hideCursorMode)
         {
             PressedButtons = new bool[(int)MouseButton.Count];
-            _hideCursor = hideCursor;
+            _hideCursorMode = hideCursorMode;
 
-            if (_hideCursor == HideCursor.Always)
+            if (_hideCursorMode == HideCursorMode.Always)
             {
                 SDL_ShowCursor(SDL_DISABLE);
                 _isHidden = true;
@@ -59,7 +60,7 @@ namespace Ryujinx.Headless.SDL2
 
         private void CheckIdle()
         {
-            if (_hideCursor != HideCursor.OnIdle)
+            if (_hideCursorMode != HideCursorMode.OnIdle)
             {
                 return;
             }
diff --git a/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs b/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs
index 172b7685a..5d048da1f 100644
--- a/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs
+++ b/src/Ryujinx.Headless.SDL2/Vulkan/VulkanWindow.cs
@@ -17,8 +17,8 @@ namespace Ryujinx.Headless.SDL2.Vulkan
             GraphicsDebugLevel glLogLevel,
             AspectRatio aspectRatio,
             bool enableMouse,
-            HideCursor hideCursor)
-            : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursor)
+            HideCursorMode hideCursorMode)
+            : base(inputManager, glLogLevel, aspectRatio, enableMouse, hideCursorMode)
         {
             _glLogLevel = glLogLevel;
         }
diff --git a/src/Ryujinx.Headless.SDL2/WindowBase.cs b/src/Ryujinx.Headless.SDL2/WindowBase.cs
index e33710421..7c3101535 100644
--- a/src/Ryujinx.Headless.SDL2/WindowBase.cs
+++ b/src/Ryujinx.Headless.SDL2/WindowBase.cs
@@ -78,9 +78,9 @@ namespace Ryujinx.Headless.SDL2
             GraphicsDebugLevel glLogLevel,
             AspectRatio aspectRatio,
             bool enableMouse,
-            HideCursor hideCursor)
+            HideCursorMode hideCursorMode)
         {
-            MouseDriver = new SDL2MouseDriver(hideCursor);
+            MouseDriver = new SDL2MouseDriver(hideCursorMode);
             _inputManager = inputManager;
             _inputManager.SetMouseDriver(MouseDriver);
             NpadManager = _inputManager.CreateNpadManager();
diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs
index 3168766b5..f0489915d 100644
--- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs
+++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationFileFormat.cs
@@ -162,9 +162,9 @@ namespace Ryujinx.Ui.Common.Configuration
         public bool ShowConfirmExit { get; set; }
 
         /// <summary>
-        /// Hide Cursor on Idle
+        /// Whether to hide cursor on idle, always or never
         /// </summary>
-        public bool HideCursorOnIdle { get; set; }
+        public HideCursorMode HideCursor { get; set; }
 
         /// <summary>
         /// Enables or disables Vertical Sync
@@ -395,4 +395,4 @@ namespace Ryujinx.Ui.Common.Configuration
             JsonHelper.SerializeToFile(path, this, ConfigurationFileFormatSettings.SerializerContext.ConfigurationFileFormat);
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs
index fc3693ae8..146a9b500 100644
--- a/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs
+++ b/src/Ryujinx.Ui.Common/Configuration/ConfigurationState.cs
@@ -613,7 +613,7 @@ namespace Ryujinx.Ui.Common.Configuration
         /// <summary>
         /// Hide Cursor on Idle
         /// </summary>
-        public ReactiveObject<bool> HideCursorOnIdle { get; private set; }
+        public ReactiveObject<HideCursorMode> HideCursor { get; private set; }
 
         private ConfigurationState()
         {
@@ -626,7 +626,7 @@ namespace Ryujinx.Ui.Common.Configuration
             EnableDiscordIntegration = new ReactiveObject<bool>();
             CheckUpdatesOnStart      = new ReactiveObject<bool>();
             ShowConfirmExit          = new ReactiveObject<bool>();
-            HideCursorOnIdle         = new ReactiveObject<bool>();
+            HideCursor               = new ReactiveObject<HideCursorMode>();
         }
 
         public ConfigurationFileFormat ToFileFormat()
@@ -662,7 +662,7 @@ namespace Ryujinx.Ui.Common.Configuration
                 EnableDiscordIntegration   = EnableDiscordIntegration,
                 CheckUpdatesOnStart        = CheckUpdatesOnStart,
                 ShowConfirmExit            = ShowConfirmExit,
-                HideCursorOnIdle           = HideCursorOnIdle,
+                HideCursor                 = HideCursor,
                 EnableVsync                = Graphics.EnableVsync,
                 EnableShaderCache          = Graphics.EnableShaderCache,
                 EnableTextureRecompression = Graphics.EnableTextureRecompression,
@@ -767,7 +767,7 @@ namespace Ryujinx.Ui.Common.Configuration
             EnableDiscordIntegration.Value            = true;
             CheckUpdatesOnStart.Value                 = true;
             ShowConfirmExit.Value                     = true;
-            HideCursorOnIdle.Value                    = false;
+            HideCursor.Value                          = Ryujinx.Common.Configuration.HideCursorMode.Never;
             Graphics.EnableVsync.Value                = true;
             Graphics.EnableShaderCache.Value          = true;
             Graphics.EnableTextureRecompression.Value = false;
@@ -1046,7 +1046,7 @@ namespace Ryujinx.Ui.Common.Configuration
             {
                 Ryujinx.Common.Logging.Logger.Warning?.Print(LogClass.Application, $"Outdated configuration version {configurationFileFormat.Version}, migrating to version 22.");
 
-                configurationFileFormat.HideCursorOnIdle = false;
+                configurationFileFormat.HideCursor = HideCursorMode.Never;
 
                 configurationFileUpdated = true;
             }
@@ -1427,7 +1427,7 @@ namespace Ryujinx.Ui.Common.Configuration
             EnableDiscordIntegration.Value            = configurationFileFormat.EnableDiscordIntegration;
             CheckUpdatesOnStart.Value                 = configurationFileFormat.CheckUpdatesOnStart;
             ShowConfirmExit.Value                     = configurationFileFormat.ShowConfirmExit;
-            HideCursorOnIdle.Value                    = configurationFileFormat.HideCursorOnIdle;
+            HideCursor.Value                          = configurationFileFormat.HideCursor;
             Graphics.EnableVsync.Value                = configurationFileFormat.EnableVsync;
             Graphics.EnableShaderCache.Value          = configurationFileFormat.EnableShaderCache;
             Graphics.EnableTextureRecompression.Value = configurationFileFormat.EnableTextureRecompression;
diff --git a/src/Ryujinx.Ui.Common/Helper/CommandLineState.cs b/src/Ryujinx.Ui.Common/Helper/CommandLineState.cs
index 8ca7fba18..660a4ce92 100644
--- a/src/Ryujinx.Ui.Common/Helper/CommandLineState.cs
+++ b/src/Ryujinx.Ui.Common/Helper/CommandLineState.cs
@@ -9,6 +9,7 @@ namespace Ryujinx.Ui.Common.Helper
 
         public static bool?  OverrideDockedMode      { get; private set; }
         public static string OverrideGraphicsBackend { get; private set; }
+        public static string OverrideHideCursor      { get; private set; }
         public static string BaseDirPathArg          { get; private set; }
         public static string Profile                 { get; private set; }
         public static string LaunchPathArg           { get; private set; }
@@ -76,6 +77,16 @@ namespace Ryujinx.Ui.Common.Helper
                     case "--handheld-mode":
                         OverrideDockedMode = false;
                         break;
+                    case "--hide-cursor":
+                        if (i + 1 >= args.Length)
+                        {
+                            Logger.Error?.Print(LogClass.Application, $"Invalid option '{arg}'");
+
+                            continue;
+                        }
+
+                        OverrideHideCursor = args[++i];
+                        break;
                     default:
                         LaunchPathArg = arg;
                         break;
diff --git a/src/Ryujinx/Program.cs b/src/Ryujinx/Program.cs
index 2e6ede444..836483d89 100644
--- a/src/Ryujinx/Program.cs
+++ b/src/Ryujinx/Program.cs
@@ -209,7 +209,7 @@ namespace Ryujinx
                 }
             }
 
-            // Check if graphics backend was overridden
+            // Check if graphics backend was overridden.
             if (CommandLineState.OverrideGraphicsBackend != null)
             {
                 if (CommandLineState.OverrideGraphicsBackend.ToLower() == "opengl")
@@ -224,7 +224,19 @@ namespace Ryujinx
                 }
             }
 
-            // Check if docked mode was overriden.
+            // Check if HideCursor was overridden.
+            if (CommandLineState.OverrideHideCursor is not null)
+            {
+                ConfigurationState.Instance.HideCursor.Value = CommandLineState.OverrideHideCursor!.ToLower() switch
+                {
+                    "never" => HideCursorMode.Never,
+                    "onidle" => HideCursorMode.OnIdle,
+                    "always" => HideCursorMode.Always,
+                    _ => ConfigurationState.Instance.HideCursor.Value
+                };
+            }
+
+            // Check if docked mode was overridden.
             if (CommandLineState.OverrideDockedMode.HasValue)
             {
                 ConfigurationState.Instance.System.EnableDockedMode.Value = CommandLineState.OverrideDockedMode.Value;
diff --git a/src/Ryujinx/Ui/RendererWidgetBase.cs b/src/Ryujinx/Ui/RendererWidgetBase.cs
index 65afa6e47..0fa7240b8 100644
--- a/src/Ryujinx/Ui/RendererWidgetBase.cs
+++ b/src/Ryujinx/Ui/RendererWidgetBase.cs
@@ -72,7 +72,7 @@ namespace Ryujinx.Ui
         const int CursorHideIdleTime = 5; // seconds
         private static readonly Cursor _invisibleCursor = new Cursor(Display.Default, CursorType.BlankCursor);
         private long _lastCursorMoveTime;
-        private bool _hideCursorOnIdle;
+        private HideCursorMode _hideCursorMode;
         private InputManager _inputManager;
         private IKeyboard _keyboardInterface;
         private GraphicsDebugLevel _glLogLevel;
@@ -113,10 +113,10 @@ namespace Ryujinx.Ui
 
             _gpuCancellationTokenSource = new CancellationTokenSource();
 
-            _hideCursorOnIdle = ConfigurationState.Instance.HideCursorOnIdle;
+            _hideCursorMode = ConfigurationState.Instance.HideCursor;
             _lastCursorMoveTime = Stopwatch.GetTimestamp();
 
-            ConfigurationState.Instance.HideCursorOnIdle.Event += HideCursorStateChanged;
+            ConfigurationState.Instance.HideCursor.Event += HideCursorStateChanged;
             ConfigurationState.Instance.Graphics.AntiAliasing.Event += UpdateAnriAliasing;
             ConfigurationState.Instance.Graphics.ScalingFilter.Event += UpdateScalingFilter;
             ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event += UpdateScalingFilterLevel;
@@ -145,26 +145,32 @@ namespace Ryujinx.Ui
             return Renderer.GetHardwareInfo().GpuVendor;
         }
 
-        private void HideCursorStateChanged(object sender, ReactiveEventArgs<bool> state)
+        private void HideCursorStateChanged(object sender, ReactiveEventArgs<HideCursorMode> state)
         {
             Application.Invoke(delegate
             {
-                _hideCursorOnIdle = state.NewValue;
+                _hideCursorMode = state.NewValue;
 
-                if (_hideCursorOnIdle)
+                switch (_hideCursorMode)
                 {
-                    _lastCursorMoveTime = Stopwatch.GetTimestamp();
-                }
-                else
-                {
-                    Window.Cursor = null;
+                    case HideCursorMode.Never:
+                        Window.Cursor = null;
+                        break;
+                    case HideCursorMode.OnIdle:
+                        _lastCursorMoveTime = Stopwatch.GetTimestamp();
+                        break;
+                    case HideCursorMode.Always:
+                        Window.Cursor = _invisibleCursor;
+                        break;
+                    default:
+                        throw new ArgumentOutOfRangeException();
                 }
             });
         }
 
         private void Renderer_Destroyed(object sender, EventArgs e)
         {
-            ConfigurationState.Instance.HideCursorOnIdle.Event -= HideCursorStateChanged;
+            ConfigurationState.Instance.HideCursor.Event -= HideCursorStateChanged;
             ConfigurationState.Instance.Graphics.AntiAliasing.Event -= UpdateAnriAliasing;
             ConfigurationState.Instance.Graphics.ScalingFilter.Event -= UpdateScalingFilter;
             ConfigurationState.Instance.Graphics.ScalingFilterLevel.Event -= UpdateScalingFilterLevel;
@@ -180,7 +186,7 @@ namespace Ryujinx.Ui
 
         protected override bool OnMotionNotifyEvent(EventMotion evnt)
         {
-            if (_hideCursorOnIdle)
+            if (_hideCursorMode == HideCursorMode.OnIdle)
             {
                 _lastCursorMoveTime = Stopwatch.GetTimestamp();
             }
@@ -315,15 +321,28 @@ namespace Ryujinx.Ui
 
             _toggleDockedMode = toggleDockedMode;
 
-            if (_hideCursorOnIdle && !ConfigurationState.Instance.Hid.EnableMouse)
+            if (ConfigurationState.Instance.Hid.EnableMouse.Value)
             {
-                long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
-                Window.Cursor = (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency) ? _invisibleCursor : null;
+                if (_isMouseInClient)
+                {
+                    Window.Cursor = _invisibleCursor;
+                }
             }
-
-            if (ConfigurationState.Instance.Hid.EnableMouse && _isMouseInClient)
+            else
             {
-                Window.Cursor = _invisibleCursor;
+                switch (_hideCursorMode)
+                {
+                    case HideCursorMode.OnIdle:
+                        long cursorMoveDelta = Stopwatch.GetTimestamp() - _lastCursorMoveTime;
+                        Window.Cursor = (cursorMoveDelta >= CursorHideIdleTime * Stopwatch.Frequency) ? _invisibleCursor : null;
+                        break;
+                    case HideCursorMode.Always:
+                        Window.Cursor = _invisibleCursor;
+                        break;
+                    case HideCursorMode.Never:
+                        Window.Cursor = null;
+                        break;
+                }
             }
         }
 
@@ -775,4 +794,4 @@ namespace Ryujinx.Ui
             return state;
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.cs b/src/Ryujinx/Ui/Windows/SettingsWindow.cs
index 27080bda3..3fb0447d3 100644
--- a/src/Ryujinx/Ui/Windows/SettingsWindow.cs
+++ b/src/Ryujinx/Ui/Windows/SettingsWindow.cs
@@ -52,7 +52,9 @@ namespace Ryujinx.Ui.Windows
         [GUI] CheckButton     _discordToggle;
         [GUI] CheckButton     _checkUpdatesToggle;
         [GUI] CheckButton     _showConfirmExitToggle;
-        [GUI] CheckButton     _hideCursorOnIdleToggle;
+        [GUI] RadioButton     _hideCursorNever;
+        [GUI] RadioButton     _hideCursorOnIdle;
+        [GUI] RadioButton     _hideCursorAlways;
         [GUI] CheckButton     _vSyncToggle;
         [GUI] CheckButton     _shaderCacheToggle;
         [GUI] CheckButton     _textureRecompressionToggle;
@@ -226,9 +228,17 @@ namespace Ryujinx.Ui.Windows
                 _showConfirmExitToggle.Click();
             }
 
-            if (ConfigurationState.Instance.HideCursorOnIdle)
+            switch (ConfigurationState.Instance.HideCursor.Value)
             {
-                _hideCursorOnIdleToggle.Click();
+                case HideCursorMode.Never:
+                    _hideCursorNever.Click();
+                    break;
+                case HideCursorMode.OnIdle:
+                    _hideCursorOnIdle.Click();
+                    break;
+                case HideCursorMode.Always:
+                    _hideCursorAlways.Click();
+                    break;
             }
 
             if (ConfigurationState.Instance.Graphics.EnableVsync)
@@ -560,6 +570,18 @@ namespace Ryujinx.Ui.Windows
                 _directoryChanged = false;
             }
 
+            HideCursorMode hideCursor = HideCursorMode.Never;
+
+            if (_hideCursorOnIdle.Active)
+            {
+                hideCursor = HideCursorMode.OnIdle;
+            }
+
+            if (_hideCursorAlways.Active)
+            {
+                hideCursor = HideCursorMode.Always;
+            }
+
             if (!float.TryParse(_resScaleText.Buffer.Text, out float resScaleCustom) || resScaleCustom <= 0.0f)
             {
                 resScaleCustom = 1.0f;
@@ -602,7 +624,7 @@ namespace Ryujinx.Ui.Windows
             ConfigurationState.Instance.EnableDiscordIntegration.Value            = _discordToggle.Active;
             ConfigurationState.Instance.CheckUpdatesOnStart.Value                 = _checkUpdatesToggle.Active;
             ConfigurationState.Instance.ShowConfirmExit.Value                     = _showConfirmExitToggle.Active;
-            ConfigurationState.Instance.HideCursorOnIdle.Value                    = _hideCursorOnIdleToggle.Active;
+            ConfigurationState.Instance.HideCursor.Value                          = hideCursor;
             ConfigurationState.Instance.Graphics.EnableVsync.Value                = _vSyncToggle.Active;
             ConfigurationState.Instance.Graphics.EnableShaderCache.Value          = _shaderCacheToggle.Active;
             ConfigurationState.Instance.Graphics.EnableTextureRecompression.Value = _textureRecompressionToggle.Active;
@@ -813,4 +835,4 @@ namespace Ryujinx.Ui.Windows
             Dispose();
         }
     }
-}
+}
\ No newline at end of file
diff --git a/src/Ryujinx/Ui/Windows/SettingsWindow.glade b/src/Ryujinx/Ui/Windows/SettingsWindow.glade
index 8ae6ea72f..0caa477bd 100644
--- a/src/Ryujinx/Ui/Windows/SettingsWindow.glade
+++ b/src/Ryujinx/Ui/Windows/SettingsWindow.glade
@@ -160,19 +160,83 @@
                                   </packing>
                                 </child>
                                 <child>
-                                  <object class="GtkCheckButton" id="_hideCursorOnIdleToggle">
-                                    <property name="label" translatable="yes">Hide Cursor On Idle</property>
+                                  <object class="GtkBox" id="_hideCursorBox">
                                     <property name="visible">True</property>
-                                    <property name="can-focus">True</property>
-                                    <property name="receives-default">False</property>
-                                    <property name="halign">start</property>
-                                    <property name="draw-indicator">True</property>
+                                    <property name="can-focus">False</property>
+                                    <child>
+                                      <object class="GtkLabel">
+                                        <property name="visible">True</property>
+                                        <property name="can-focus">False</property>
+                                        <property name="halign">end</property>
+                                        <property name="label" translatable="yes">Hide Cursor:</property>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">True</property>
+                                        <property name="padding">5</property>
+                                        <property name="position">2</property>
+                                      </packing>
+                                    </child>
+                                    <child>
+                                      <object class="GtkRadioButton" id="_hideCursorNever">
+                                        <property name="label" translatable="yes">Never</property>
+                                        <property name="visible">True</property>
+                                        <property name="can-focus">True</property>
+                                        <property name="receives-default">False</property>
+                                        <property name="halign">start</property>
+                                        <property name="margin-top">5</property>
+                                        <property name="margin-bottom">5</property>
+                                        <property name="active">True</property>
+                                        <property name="draw-indicator">True</property>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">True</property>
+                                        <property name="position">3</property>
+                                      </packing>
+                                    </child>
+                                    <child>
+                                      <object class="GtkRadioButton" id="_hideCursorOnIdle">
+                                        <property name="label" translatable="yes">On Idle</property>
+                                        <property name="visible">True</property>
+                                        <property name="can-focus">True</property>
+                                        <property name="receives-default">False</property>
+                                        <property name="halign">start</property>
+                                        <property name="margin-top">5</property>
+                                        <property name="margin-bottom">5</property>
+                                        <property name="draw-indicator">True</property>
+                                        <property name="group">_hideCursorNever</property>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">True</property>
+                                        <property name="position">4</property>
+                                      </packing>
+                                    </child>
+                                    <child>
+                                      <object class="GtkRadioButton" id="_hideCursorAlways">
+                                        <property name="label" translatable="yes">Always</property>
+                                        <property name="visible">True</property>
+                                        <property name="can-focus">True</property>
+                                        <property name="receives-default">False</property>
+                                        <property name="halign">start</property>
+                                        <property name="margin-top">5</property>
+                                        <property name="margin-bottom">5</property>
+                                        <property name="draw-indicator">True</property>
+                                        <property name="group">_hideCursorNever</property>
+                                      </object>
+                                      <packing>
+                                        <property name="expand">False</property>
+                                        <property name="fill">True</property>
+                                        <property name="position">5</property>
+                                      </packing>
+                                    </child>
                                   </object>
                                   <packing>
                                     <property name="expand">False</property>
                                     <property name="fill">True</property>
                                     <property name="padding">5</property>
-                                    <property name="position">3</property>
+                                    <property name="position">4</property>
                                   </packing>
                                 </child>
                               </object>