From f16d7f91f1e0483a55c23382171bb81a679e4d8c Mon Sep 17 00:00:00 2001
From: Caian Benedicto <caianbene@gmail.com>
Date: Wed, 10 Feb 2021 21:28:44 -0300
Subject: [PATCH] Improve inline keyboard compatibility (#1959)
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit

* Improve compatibility of the inline keyboard with some games

* Send an empty first text to avoid crashing some games

* Implement SetCustomizedDictionaries and fix SetCustomizeDic

* Expand Bg and Fg –abbreviations in the swkbd applet

* Fix variable names and add comments to software keyboard
---
 .../SoftwareKeyboard/InlineKeyboardRequest.cs |  48 ++-
 .../InlineKeyboardResponse.cs                 | 101 ++++-
 .../SoftwareKeyboard/InlineKeyboardState.cs   |  27 +-
 .../SoftwareKeyboard/InlineResponses.cs       |  70 +--
 .../SoftwareKeyboardAppear.cs                 |  34 +-
 .../SoftwareKeyboardApplet.cs                 | 406 ++++++++++++------
 .../SoftwareKeyboard/SoftwareKeyboardCalc.cs  |  79 +++-
 .../SoftwareKeyboardCustomizeDic.cs           |  14 +
 .../SoftwareKeyboardDictSet.cs                |  29 +-
 .../SoftwareKeyboardInitialize.cs             |  11 +-
 .../SoftwareKeyboardUserWord.cs               |  14 +
 11 files changed, 626 insertions(+), 207 deletions(-)
 create mode 100644 Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCustomizeDic.cs
 create mode 100644 Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUserWord.cs

diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs
index d0024001e2..b17debfc75 100644
--- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardRequest.cs
@@ -1,18 +1,48 @@
 namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
 {
     /// <summary>
-    /// Possible requests to the keyboard when running in inline mode.
+    /// Possible requests to the software keyboard when running in inline mode.
     /// </summary>
     enum InlineKeyboardRequest : uint
     {
-        Unknown0                    = 0x0,
-        Finalize                    = 0x4,
-        SetUserWordInfo             = 0x6,
-        SetCustomizeDic             = 0x7,
-        Calc                        = 0xA,
-        SetCustomizedDictionaries   = 0xB,
+        /// <summary>
+        /// Finalize the keyboard applet.
+        /// </summary>
+        Finalize = 0x4,
+
+        /// <summary>
+        /// Set user words for text prediction.
+        /// </summary>
+        SetUserWordInfo = 0x6,
+
+        /// <summary>
+        /// Sets the CustomizeDic data. Can't be used if CustomizedDictionaries is already set.
+        /// </summary>
+        SetCustomizeDic = 0x7,
+
+        /// <summary>
+        /// Configure the keyboard applet and put it in a state where it is processing input.
+        /// </summary>
+        Calc = 0xA,
+
+        /// <summary>
+        /// Set custom dictionaries for text prediction. Can't be used if SetCustomizeDic is already set.
+        /// </summary>
+        SetCustomizedDictionaries = 0xB,
+
+        /// <summary>
+        /// Release custom dictionaries data.
+        /// </summary>
         UnsetCustomizedDictionaries = 0xC,
-        UseChangedStringV2          = 0xD,
-        UseMovedCursorV2            = 0xE
+
+        /// <summary>
+        /// [8.0.0+] Request the keyboard applet to use the ChangedStringV2 response when notifying changes in text data.
+        /// </summary>
+        UseChangedStringV2 = 0xD,
+
+        /// <summary>
+        /// [8.0.0+] Request the keyboard applet to use the MovedCursorV2 response when notifying changes in cursor position.
+        /// </summary>
+        UseMovedCursorV2 = 0xE
     }
 }
diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs
index 61908b7bfb..b21db507cb 100644
--- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardResponse.cs
@@ -1,26 +1,93 @@
 namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
 {
     /// <summary>
-    /// Possible responses from the keyboard when running in inline mode.
+    /// Possible responses from the software keyboard when running in inline mode.
     /// </summary>
     enum InlineKeyboardResponse : uint
     {
-        FinishedInitialize          = 0x0,
-        Default                     = 0x1,
-        ChangedString               = 0x2,
-        MovedCursor                 = 0x3,
-        MovedTab                    = 0x4,
-        DecidedEnter                = 0x5,
-        DecidedCancel               = 0x6,
-        ChangedStringUtf8           = 0x7,
-        MovedCursorUtf8             = 0x8,
-        DecidedEnterUtf8            = 0x9,
-        UnsetCustomizeDic           = 0xA,
-        ReleasedUserWordInfo        = 0xB,
+        /// <summary>
+        /// The software keyboard received a Calc and it is fully initialized. Reply data is ignored by the user-process.
+        /// </summary>
+        FinishedInitialize = 0x0,
+
+        /// <summary>
+        /// Default response. Official sw has no handling for this besides just closing the storage.
+        /// </summary>
+        Default = 0x1,
+
+        /// <summary>
+        /// The text data in the software keyboard changed (UTF-16 encoding).
+        /// </summary>
+        ChangedString = 0x2,
+
+        /// <summary>
+        /// The cursor position in the software keyboard changed (UTF-16 encoding).
+        /// </summary>
+        MovedCursor = 0x3,
+
+        /// <summary>
+        /// A tab in the software keyboard changed.
+        /// </summary>
+        MovedTab = 0x4,
+
+        /// <summary>
+        /// The OK key was pressed in the software keyboard, confirming the input text (UTF-16 encoding).
+        /// </summary>
+        DecidedEnter = 0x5,
+
+        /// <summary>
+        /// The Cancel key was pressed in the software keyboard, cancelling the input.
+        /// </summary>
+        DecidedCancel = 0x6,
+
+        /// <summary>
+        /// Same as ChangedString, but with UTF-8 encoding.
+        /// </summary>
+        ChangedStringUtf8 = 0x7,
+
+        /// <summary>
+        /// Same as MovedCursor, but with UTF-8 encoding.
+        /// </summary>
+        MovedCursorUtf8 = 0x8,
+
+        /// <summary>
+        /// Same as DecidedEnter, but with UTF-8 encoding.
+        /// </summary>
+        DecidedEnterUtf8 = 0x9,
+
+        /// <summary>
+        /// They software keyboard is releasing the data previously set by a SetCustomizeDic request.
+        /// </summary>
+        UnsetCustomizeDic = 0xA,
+
+        /// <summary>
+        /// They software keyboard is releasing the data previously set by a SetUserWordInfo request.
+        /// </summary>
+        ReleasedUserWordInfo = 0xB,
+
+        /// <summary>
+        /// They software keyboard is releasing the data previously set by a SetCustomizedDictionaries request.
+        /// </summary>
         UnsetCustomizedDictionaries = 0xC,
-        ChangedStringV2             = 0xD,
-        MovedCursorV2               = 0xE,
-        ChangedStringUtf8V2         = 0xF,
-        MovedCursorUtf8V2           = 0x10
+
+        /// <summary>
+        /// Same as ChangedString, but with additional fields.
+        /// </summary>
+        ChangedStringV2 = 0xD,
+
+        /// <summary>
+        /// Same as MovedCursor, but with additional fields.
+        /// </summary>
+        MovedCursorV2 = 0xE,
+
+        /// <summary>
+        /// Same as ChangedStringUtf8, but with additional fields.
+        /// </summary>
+        ChangedStringUtf8V2 = 0xF,
+
+        /// <summary>
+        /// Same as MovedCursorUtf8, but with additional fields.
+        /// </summary>
+        MovedCursorUtf8V2 = 0x10
     }
 }
diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs
index 2940d1614d..024ff2cf4d 100644
--- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineKeyboardState.cs
@@ -1,14 +1,33 @@
 namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
 {
     /// <summary>
-    /// Possible states for the keyboard when running in inline mode.
+    /// Possible states for the software keyboard when running in inline mode.
     /// </summary>
     enum InlineKeyboardState : uint
     {
+        /// <summary>
+        /// The software keyboard has just been created or finalized and is uninitialized.
+        /// </summary>
         Uninitialized = 0x0,
-        Initializing  = 0x1,
-        Ready         = 0x2,
+
+        /// <summary>
+        /// A Calc was previously received and fulfilled, so the software keyboard is initialized, but is not processing input.
+        /// </summary>
+        Initialized = 0x1,
+
+        /// <summary>
+        /// A Calc was received and the software keyboard is processing input.
+        /// </summary>
+        Ready = 0x2,
+
+        /// <summary>
+        /// New text data or cursor position of the software keyboard are available.
+        /// </summary>
         DataAvailable = 0x3,
-        Completed     = 0x4
+
+        /// <summary>
+        /// The Calc request was fulfilled with either a text input or a cancel.
+        /// </summary>
+        Complete = 0x4
     }
 }
diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs
index 60cc528775..50e77b7421 100644
--- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/InlineResponses.cs
@@ -45,41 +45,41 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
             writer.Write(cursor); // Cursor position
         }
 
-        public static byte[] FinishedInitialize()
+        public static byte[] FinishedInitialize(InlineKeyboardState state)
         {
             uint resSize = 2 * sizeof(uint) + 0x1;
 
             using (MemoryStream stream = new MemoryStream(new byte[resSize]))
             using (BinaryWriter writer = new BinaryWriter(stream))
             {
-                BeginResponse(InlineKeyboardState.Ready, InlineKeyboardResponse.FinishedInitialize, writer);
+                BeginResponse(state, InlineKeyboardResponse.FinishedInitialize, writer);
                 writer.Write((byte)1); // Data (ignored by the program)
 
                 return stream.ToArray();
             }
         }
 
-        public static byte[] Default()
+        public static byte[] Default(InlineKeyboardState state)
         {
             uint resSize = 2 * sizeof(uint);
 
             using (MemoryStream stream = new MemoryStream(new byte[resSize]))
             using (BinaryWriter writer = new BinaryWriter(stream))
             {
-                BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.Default, writer);
+                BeginResponse(state, InlineKeyboardResponse.Default, writer);
 
                 return stream.ToArray();
             }
         }
 
-        public static byte[] ChangedString(string text)
+        public static byte[] ChangedString(string text, InlineKeyboardState state)
         {
-            uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16;
+            uint resSize = 6 * sizeof(uint) + MaxStrLenUTF16;
 
             using (MemoryStream stream = new MemoryStream(new byte[resSize]))
             using (BinaryWriter writer = new BinaryWriter(stream))
             {
-                BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.ChangedString, writer);
+                BeginResponse(state, InlineKeyboardResponse.ChangedString, writer);
                 WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode);
                 writer.Write((int)0); // ?
                 writer.Write((int)0); // ?
@@ -88,21 +88,21 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
             }
         }
 
-        public static byte[] MovedCursor(string text)
+        public static byte[] MovedCursor(string text, InlineKeyboardState state)
         {
             uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16;
 
             using (MemoryStream stream = new MemoryStream(new byte[resSize]))
             using (BinaryWriter writer = new BinaryWriter(stream))
             {
-                BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.MovedCursor, writer);
+                BeginResponse(state, InlineKeyboardResponse.MovedCursor, writer);
                 WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode);
 
                 return stream.ToArray();
             }
         }
 
-        public static byte[] MovedTab(string text)
+        public static byte[] MovedTab(string text, InlineKeyboardState state)
         {
             // Should be the same as MovedCursor.
 
@@ -111,48 +111,48 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
             using (MemoryStream stream = new MemoryStream(new byte[resSize]))
             using (BinaryWriter writer = new BinaryWriter(stream))
             {
-                BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.MovedTab, writer);
+                BeginResponse(state, InlineKeyboardResponse.MovedTab, writer);
                 WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode);
 
                 return stream.ToArray();
             }
         }
 
-        public static byte[] DecidedEnter(string text)
+        public static byte[] DecidedEnter(string text, InlineKeyboardState state)
         {
             uint resSize = 3 * sizeof(uint) + MaxStrLenUTF16;
 
             using (MemoryStream stream = new MemoryStream(new byte[resSize]))
             using (BinaryWriter writer = new BinaryWriter(stream))
             {
-                BeginResponse(InlineKeyboardState.Completed, InlineKeyboardResponse.DecidedEnter, writer);
+                BeginResponse(state, InlineKeyboardResponse.DecidedEnter, writer);
                 WriteString(text, writer, MaxStrLenUTF16, Encoding.Unicode);
 
                 return stream.ToArray();
             }
         }
 
-        public static byte[] DecidedCancel()
+        public static byte[] DecidedCancel(InlineKeyboardState state)
         {
             uint resSize = 2 * sizeof(uint);
 
             using (MemoryStream stream = new MemoryStream(new byte[resSize]))
             using (BinaryWriter writer = new BinaryWriter(stream))
             {
-                BeginResponse(InlineKeyboardState.Completed, InlineKeyboardResponse.DecidedCancel, writer);
+                BeginResponse(state, InlineKeyboardResponse.DecidedCancel, writer);
 
                 return stream.ToArray();
             }
         }
 
-        public static byte[] ChangedStringUtf8(string text)
+        public static byte[] ChangedStringUtf8(string text, InlineKeyboardState state)
         {
             uint resSize = 6 * sizeof(uint) + MaxStrLenUTF8;
 
             using (MemoryStream stream = new MemoryStream(new byte[resSize]))
             using (BinaryWriter writer = new BinaryWriter(stream))
             {
-                BeginResponse(InlineKeyboardState.DataAvailable, InlineKeyboardResponse.ChangedStringUtf8, writer);
+                BeginResponse(state, InlineKeyboardResponse.ChangedStringUtf8, writer);
                 WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8);
                 writer.Write((int)0); // ?
                 writer.Write((int)0); // ?
@@ -161,81 +161,81 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
             }
         }
 
-        public static byte[] MovedCursorUtf8(string text)
+        public static byte[] MovedCursorUtf8(string text, InlineKeyboardState state)
         {
             uint resSize = 4 * sizeof(uint) + MaxStrLenUTF8;
 
             using (MemoryStream stream = new MemoryStream(new byte[resSize]))
             using (BinaryWriter writer = new BinaryWriter(stream))
             {
-                BeginResponse(InlineKeyboardState.DataAvailable, InlineKeyboardResponse.MovedCursorUtf8, writer);
+                BeginResponse(state, InlineKeyboardResponse.MovedCursorUtf8, writer);
                 WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8);
 
                 return stream.ToArray();
             }
         }
 
-        public static byte[] DecidedEnterUtf8(string text)
+        public static byte[] DecidedEnterUtf8(string text, InlineKeyboardState state)
         {
             uint resSize = 3 * sizeof(uint) + MaxStrLenUTF8;
 
             using (MemoryStream stream = new MemoryStream(new byte[resSize]))
             using (BinaryWriter writer = new BinaryWriter(stream))
             {
-                BeginResponse(InlineKeyboardState.Completed, InlineKeyboardResponse.DecidedEnterUtf8, writer);
+                BeginResponse(state, InlineKeyboardResponse.DecidedEnterUtf8, writer);
                 WriteString(text, writer, MaxStrLenUTF8, Encoding.UTF8);
 
                 return stream.ToArray();
             }
         }
 
-        public static byte[] UnsetCustomizeDic()
+        public static byte[] UnsetCustomizeDic(InlineKeyboardState state)
         {
             uint resSize = 2 * sizeof(uint);
 
             using (MemoryStream stream = new MemoryStream(new byte[resSize]))
             using (BinaryWriter writer = new BinaryWriter(stream))
             {
-                BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.UnsetCustomizeDic, writer);
+                BeginResponse(state, InlineKeyboardResponse.UnsetCustomizeDic, writer);
 
                 return stream.ToArray();
             }
         }
 
-        public static byte[] ReleasedUserWordInfo()
+        public static byte[] ReleasedUserWordInfo(InlineKeyboardState state)
         {
             uint resSize = 2 * sizeof(uint);
 
             using (MemoryStream stream = new MemoryStream(new byte[resSize]))
             using (BinaryWriter writer = new BinaryWriter(stream))
             {
-                BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.ReleasedUserWordInfo, writer);
+                BeginResponse(state, InlineKeyboardResponse.ReleasedUserWordInfo, writer);
 
                 return stream.ToArray();
             }
         }
 
-        public static byte[] UnsetCustomizedDictionaries()
+        public static byte[] UnsetCustomizedDictionaries(InlineKeyboardState state)
         {
             uint resSize = 2 * sizeof(uint);
 
             using (MemoryStream stream = new MemoryStream(new byte[resSize]))
             using (BinaryWriter writer = new BinaryWriter(stream))
             {
-                BeginResponse(InlineKeyboardState.Initializing, InlineKeyboardResponse.UnsetCustomizedDictionaries, writer);
+                BeginResponse(state, InlineKeyboardResponse.UnsetCustomizedDictionaries, writer);
 
                 return stream.ToArray();
             }
         }
 
-        public static byte[] ChangedStringV2(string text)
+        public static byte[] ChangedStringV2(string text, InlineKeyboardState state)
         {
             uint resSize = 6 * sizeof(uint) + MaxStrLenUTF16 + 0x1;
 
             using (MemoryStream stream = new MemoryStream(new byte[resSize]))
             using (BinaryWriter writer = new BinaryWriter(stream))
             {
-                BeginResponse(InlineKeyboardState.DataAvailable, InlineKeyboardResponse.ChangedStringV2, writer);
+                BeginResponse(state, InlineKeyboardResponse.ChangedStringV2, writer);
                 WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode);
                 writer.Write((int)0); // ?
                 writer.Write((int)0); // ?
@@ -245,14 +245,14 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
             }
         }
 
-        public static byte[] MovedCursorV2(string text)
+        public static byte[] MovedCursorV2(string text, InlineKeyboardState state)
         {
             uint resSize = 4 * sizeof(uint) + MaxStrLenUTF16 + 0x1;
 
             using (MemoryStream stream = new MemoryStream(new byte[resSize]))
             using (BinaryWriter writer = new BinaryWriter(stream))
             {
-                BeginResponse(InlineKeyboardState.DataAvailable, InlineKeyboardResponse.MovedCursorV2, writer);
+                BeginResponse(state, InlineKeyboardResponse.MovedCursorV2, writer);
                 WriteStringWithCursor(text, writer, MaxStrLenUTF16, Encoding.Unicode);
                 writer.Write((byte)0); // Flag == 0
 
@@ -260,14 +260,14 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
             }
         }
 
-        public static byte[] ChangedStringUtf8V2(string text)
+        public static byte[] ChangedStringUtf8V2(string text, InlineKeyboardState state)
         {
             uint resSize = 6 * sizeof(uint) + MaxStrLenUTF8 + 0x1;
 
             using (MemoryStream stream = new MemoryStream(new byte[resSize]))
             using (BinaryWriter writer = new BinaryWriter(stream))
             {
-                BeginResponse(InlineKeyboardState.DataAvailable, InlineKeyboardResponse.ChangedStringUtf8V2, writer);
+                BeginResponse(state, InlineKeyboardResponse.ChangedStringUtf8V2, writer);
                 WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8);
                 writer.Write((int)0); // ?
                 writer.Write((int)0); // ?
@@ -277,14 +277,14 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
             }
         }
 
-        public static byte[] MovedCursorUtf8V2(string text)
+        public static byte[] MovedCursorUtf8V2(string text, InlineKeyboardState state)
         {
             uint resSize = 4 * sizeof(uint) + MaxStrLenUTF8 + 0x1;
 
             using (MemoryStream stream = new MemoryStream(new byte[resSize]))
             using (BinaryWriter writer = new BinaryWriter(stream))
             {
-                BeginResponse(InlineKeyboardState.DataAvailable, InlineKeyboardResponse.MovedCursorUtf8V2, writer);
+                BeginResponse(state, InlineKeyboardResponse.MovedCursorUtf8V2, writer);
                 WriteStringWithCursor(text, writer, MaxStrLenUTF8, Encoding.UTF8);
                 writer.Write((byte)0); // Flag == 0
 
diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs
index 23e8bd1fe5..262dd4df80 100644
--- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardAppear.cs
@@ -2,33 +2,38 @@
 
 namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
 {
+    /// <summary>
+    /// A structure with appearance configurations for the software keyboard when running in inline mode.
+    /// </summary>
     [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
     struct SoftwareKeyboardAppear
     {
         private const int OkTextLength = 8;
 
-        // Some games send a Calc without intention of showing the keyboard, a
-        // common trend observed is that this field will be != 0 in such cases.
+        /// <summary>
+        /// Some games send a Calc without intention of showing the keyboard, a
+        /// common trend observed is that this field will be != 0 in such cases.
+        /// </summary>
         public uint ShouldBeHidden;
 
+        /// <summary>
+        /// The string displayed in the Submit button.
+        /// </summary>
         [MarshalAs(UnmanagedType.ByValTStr, SizeConst = OkTextLength + 1)]
         public string OkText;
 
         /// <summary>
         /// The character displayed in the left button of the numeric keyboard.
-        /// This is ignored when Mode is not set to NumbersOnly.
         /// </summary>
         public char LeftOptionalSymbolKey;
 
         /// <summary>
         /// The character displayed in the right button of the numeric keyboard.
-        /// This is ignored when Mode is not set to NumbersOnly.
         /// </summary>
         public char RightOptionalSymbolKey;
 
         /// <summary>
-        /// When set, predictive typing is enabled making use of the system dictionary,
-        /// and any custom user dictionary.
+        /// When set, predictive typing is enabled making use of the system dictionary, and any custom user dictionary.
         /// </summary>
         [MarshalAs(UnmanagedType.I1)]
         public bool PredictionEnabled;
@@ -43,13 +48,24 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
         public int Padding1;
         public int Padding2;
 
-        public byte EnableReturnButton;
+        /// <summary>
+        /// Indicates the return button is enabled in the keyboard. This allows for input with multiple lines.
+        /// </summary>
+        [MarshalAs(UnmanagedType.I1)]
+        public bool UseNewLine;
+
+        /// <summary>
+        /// [10.0.0+] If value is 1 or 2, then keytopAsFloating=0 and footerScalable=1 in Calc.
+        /// </summary>
+        public byte Unknown1;
 
-        public byte Padding3;
         public byte Padding4;
         public byte Padding5;
 
-        public uint CalcArgFlags;
+        /// <summary>
+        /// Bitmask 0x1000 of the Calc and DirectionalButtonAssignEnabled in bitmask 0x10000000.
+        /// </summary>
+        public uint CalcFlags;
 
         public uint Padding6;
         public uint Padding7;
diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs
index 89ed559256..4f43472de4 100644
--- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs
@@ -1,4 +1,5 @@
-using Ryujinx.Common.Logging;
+using Ryujinx.Common;
+using Ryujinx.Common.Logging;
 using Ryujinx.HLE.HOS.Applets.SoftwareKeyboard;
 using Ryujinx.HLE.HOS.Services.Am.AppletAE;
 using System;
@@ -14,31 +15,41 @@ namespace Ryujinx.HLE.HOS.Applets
     {
         private const string DefaultText = "Ryujinx";
 
+        private const long DebounceTimeMillis = 200;
+        private const int ResetDelayMillis = 500;
+
         private readonly Switch _device;
 
         private const int StandardBufferSize    = 0x7D8;
         private const int InteractiveBufferSize = 0x7D4;
+        private const int MaxUserWords          = 0x1388;
 
-        private SoftwareKeyboardState _state = SoftwareKeyboardState.Uninitialized;
+        private SoftwareKeyboardState _foregroundState = SoftwareKeyboardState.Uninitialized;
+        private volatile InlineKeyboardState _backgroundState = InlineKeyboardState.Uninitialized;
 
         private bool _isBackground = false;
+        private bool _alreadyShown = false;
+        private volatile bool _useChangedStringV2 = false;
 
         private AppletSession _normalSession;
         private AppletSession _interactiveSession;
 
-        // Configuration for foreground mode
-        private SoftwareKeyboardConfig  _keyboardFgConfig;
-        private SoftwareKeyboardCalc    _keyboardCalc;
-        private SoftwareKeyboardDictSet _keyboardDict;
+        // Configuration for foreground mode.
+        private SoftwareKeyboardConfig _keyboardForegroundConfig;
 
-        // Configuration for background mode
-        private SoftwareKeyboardInitialize _keyboardBgInitialize;
+        // Configuration for background (inline) mode.
+        private SoftwareKeyboardInitialize   _keyboardBackgroundInitialize;
+        private SoftwareKeyboardCalc         _keyboardBackgroundCalc;
+        private SoftwareKeyboardCustomizeDic _keyboardBackgroundDic;
+        private SoftwareKeyboardDictSet      _keyboardBackgroundDictSet;
+        private SoftwareKeyboardUserWord[]   _keyboardBackgroundUserWords;
 
         private byte[] _transferMemory;
 
-        private string   _textValue = null;
+        private string   _textValue = "";
         private bool     _okPressed = false;
         private Encoding _encoding  = Encoding.Unicode;
+        private long     _lastTextSetMillis = 0;
 
         public event EventHandler AppletStateChanged;
 
@@ -55,22 +66,27 @@ namespace Ryujinx.HLE.HOS.Applets
 
             _interactiveSession.DataAvailable += OnInteractiveData;
 
+            _alreadyShown = false;
+            _useChangedStringV2 = false;
+
             var launchParams   = _normalSession.Pop();
             var keyboardConfig = _normalSession.Pop();
 
-            // TODO: A better way would be handling the background creation properly
-            // in LibraryAppleCreator / Acessor instead of guessing by size.
             if (keyboardConfig.Length == Marshal.SizeOf<SoftwareKeyboardInitialize>())
             {
+                // Initialize the keyboard applet in background mode.
+
                 _isBackground = true;
 
-                _keyboardBgInitialize = ReadStruct<SoftwareKeyboardInitialize>(keyboardConfig);
-                _state = SoftwareKeyboardState.Uninitialized;
+                _keyboardBackgroundInitialize = ReadStruct<SoftwareKeyboardInitialize>(keyboardConfig);
+                _backgroundState = InlineKeyboardState.Uninitialized;
 
                 return ResultCode.Success;
             }
             else
             {
+                // Initialize the keyboard applet in foreground mode.
+
                 _isBackground = false;
 
                 if (keyboardConfig.Length < Marshal.SizeOf<SoftwareKeyboardConfig>())
@@ -79,7 +95,7 @@ namespace Ryujinx.HLE.HOS.Applets
                 }
                 else
                 {
-                    _keyboardFgConfig = ReadStruct<SoftwareKeyboardConfig>(keyboardConfig);
+                    _keyboardForegroundConfig = ReadStruct<SoftwareKeyboardConfig>(keyboardConfig);
                 }
 
                 if (!_normalSession.TryPop(out _transferMemory))
@@ -87,12 +103,12 @@ namespace Ryujinx.HLE.HOS.Applets
                     Logger.Error?.Print(LogClass.ServiceAm, "SwKbd Transfer Memory is null");
                 }
 
-                if (_keyboardFgConfig.UseUtf8)
+                if (_keyboardForegroundConfig.UseUtf8)
                 {
                     _encoding = Encoding.UTF8;
                 }
 
-                _state = SoftwareKeyboardState.Ready;
+                _foregroundState = SoftwareKeyboardState.Ready;
 
                 ExecuteForegroundKeyboard();
 
@@ -105,32 +121,44 @@ namespace Ryujinx.HLE.HOS.Applets
             return ResultCode.Success;
         }
 
+        private InlineKeyboardState GetInlineState()
+        {
+            return _backgroundState;
+        }
+
+        private void SetInlineState(InlineKeyboardState state)
+        {
+            _backgroundState = state;
+        }
+
         private void ExecuteForegroundKeyboard()
         {
             string initialText = null;
 
             // Initial Text is always encoded as a UTF-16 string in the work buffer (passed as transfer memory)
             // InitialStringOffset points to the memory offset and InitialStringLength is the number of UTF-16 characters
-            if (_transferMemory != null && _keyboardFgConfig.InitialStringLength > 0)
+            if (_transferMemory != null && _keyboardForegroundConfig.InitialStringLength > 0)
             {
-                initialText = Encoding.Unicode.GetString(_transferMemory, _keyboardFgConfig.InitialStringOffset, 2 * _keyboardFgConfig.InitialStringLength);
+                initialText = Encoding.Unicode.GetString(_transferMemory, _keyboardForegroundConfig.InitialStringOffset,
+                    2 * _keyboardForegroundConfig.InitialStringLength);
             }
 
             // If the max string length is 0, we set it to a large default
             // length.
-            if (_keyboardFgConfig.StringLengthMax == 0)
+            if (_keyboardForegroundConfig.StringLengthMax == 0)
             {
-                _keyboardFgConfig.StringLengthMax = 100;
+                _keyboardForegroundConfig.StringLengthMax = 100;
             }
 
             var args = new SoftwareKeyboardUiArgs
             {
-                HeaderText = _keyboardFgConfig.HeaderText,
-                SubtitleText = _keyboardFgConfig.SubtitleText,
-                GuideText = _keyboardFgConfig.GuideText,
-                SubmitText = (!string.IsNullOrWhiteSpace(_keyboardFgConfig.SubmitText) ? _keyboardFgConfig.SubmitText : "OK"),
-                StringLengthMin = _keyboardFgConfig.StringLengthMin,
-                StringLengthMax = _keyboardFgConfig.StringLengthMax,
+                HeaderText = _keyboardForegroundConfig.HeaderText,
+                SubtitleText = _keyboardForegroundConfig.SubtitleText,
+                GuideText = _keyboardForegroundConfig.GuideText,
+                SubmitText = (!string.IsNullOrWhiteSpace(_keyboardForegroundConfig.SubmitText) ?
+                    _keyboardForegroundConfig.SubmitText : "OK"),
+                StringLengthMin = _keyboardForegroundConfig.StringLengthMin,
+                StringLengthMax = _keyboardForegroundConfig.StringLengthMax,
                 InitialText = initialText
             };
 
@@ -151,26 +179,26 @@ namespace Ryujinx.HLE.HOS.Applets
             // than our default text, repeat our default text until we meet
             // the minimum length requirement.
             // This should always be done before the text truncation step.
-            while (_textValue.Length < _keyboardFgConfig.StringLengthMin)
+            while (_textValue.Length < _keyboardForegroundConfig.StringLengthMin)
             {
                 _textValue = String.Join(" ", _textValue, _textValue);
             }
 
             // If our default text is longer than the allowed length,
             // we truncate it.
-            if (_textValue.Length > _keyboardFgConfig.StringLengthMax)
+            if (_textValue.Length > _keyboardForegroundConfig.StringLengthMax)
             {
-                _textValue = _textValue.Substring(0, (int)_keyboardFgConfig.StringLengthMax);
+                _textValue = _textValue.Substring(0, (int)_keyboardForegroundConfig.StringLengthMax);
             }
 
             // Does the application want to validate the text itself?
-            if (_keyboardFgConfig.CheckText)
+            if (_keyboardForegroundConfig.CheckText)
             {
                 // The application needs to validate the response, so we
                 // submit it to the interactive output buffer, and poll it
                 // for validation. Once validated, the application will submit
                 // back a validation status, which is handled in OnInteractiveDataPushIn.
-                _state = SoftwareKeyboardState.ValidationPending;
+                _foregroundState = SoftwareKeyboardState.ValidationPending;
 
                 _interactiveSession.Push(BuildResponse(_textValue, true));
             }
@@ -179,7 +207,7 @@ namespace Ryujinx.HLE.HOS.Applets
                 // If the application doesn't need to validate the response,
                 // we push the data to the non-interactive output buffer
                 // and poll it for completion.
-                _state = SoftwareKeyboardState.Complete;
+                _foregroundState = SoftwareKeyboardState.Complete;
 
                 _normalSession.Push(BuildResponse(_textValue, false));
 
@@ -204,7 +232,7 @@ namespace Ryujinx.HLE.HOS.Applets
 
         private void OnForegroundInteractiveData(byte[] data)
         {
-            if (_state == SoftwareKeyboardState.ValidationPending)
+            if (_foregroundState == SoftwareKeyboardState.ValidationPending)
             {
                 // TODO(jduncantor):
                 // If application rejects our "attempt", submit another attempt,
@@ -216,9 +244,9 @@ namespace Ryujinx.HLE.HOS.Applets
 
                 AppletStateChanged?.Invoke(this, null);
 
-                _state = SoftwareKeyboardState.Complete;
+                _foregroundState = SoftwareKeyboardState.Complete;
             }
-            else if(_state == SoftwareKeyboardState.Complete)
+            else if(_foregroundState == SoftwareKeyboardState.Complete)
             {
                 // If we have already completed, we push the result text
                 // back on the output buffer and poll the application.
@@ -242,142 +270,278 @@ namespace Ryujinx.HLE.HOS.Applets
             using (MemoryStream stream = new MemoryStream(data))
             using (BinaryReader reader = new BinaryReader(stream))
             {
-                var request = (InlineKeyboardRequest)reader.ReadUInt32();
-
+                InlineKeyboardRequest request = (InlineKeyboardRequest)reader.ReadUInt32();
+                InlineKeyboardState state = GetInlineState();
                 long remaining;
 
-                // Always show the keyboard if the state is 'Ready'.
-                bool showKeyboard = _state == SoftwareKeyboardState.Ready;
+                Logger.Debug?.Print(LogClass.ServiceAm, $"Keyboard received command {request} in state {state}");
 
                 switch (request)
                 {
-                    case InlineKeyboardRequest.Unknown0: // Unknown request sent by some games after calc
-                        _interactiveSession.Push(InlineResponses.Default());
-                        break;
                     case InlineKeyboardRequest.UseChangedStringV2:
-                        // Not used because we only send the entire string after confirmation.
-                        _interactiveSession.Push(InlineResponses.Default());
+                        _useChangedStringV2 = true;
                         break;
                     case InlineKeyboardRequest.UseMovedCursorV2:
-                        // Not used because we only send the entire string after confirmation.
-                        _interactiveSession.Push(InlineResponses.Default());
+                        // Not used because we only reply with the final string.
+                        break;
+                    case InlineKeyboardRequest.SetUserWordInfo:
+                        // Read the user word info data.
+                        remaining = stream.Length - stream.Position;
+                        if (remaining < sizeof(int))
+                        {
+                            Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info of {remaining} bytes");
+                        }
+                        else
+                        {
+                            int wordsCount = reader.ReadInt32();
+                            int wordSize = Marshal.SizeOf<SoftwareKeyboardUserWord>();
+                            remaining = stream.Length - stream.Position;
+
+                            if (wordsCount > MaxUserWords)
+                            {
+                                Logger.Warning?.Print(LogClass.ServiceAm, $"Received {wordsCount} User Words but the maximum is {MaxUserWords}");
+                            }
+                            else if (wordsCount * wordSize != remaining)
+                            {
+                                Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard User Word Info data of {remaining} bytes for {wordsCount} words");
+                            }
+                            else
+                            {
+                                _keyboardBackgroundUserWords = new SoftwareKeyboardUserWord[wordsCount];
+
+                                for (int word = 0; word < wordsCount; word++)
+                                {
+                                    byte[] wordData = reader.ReadBytes(wordSize);
+                                    _keyboardBackgroundUserWords[word] = ReadStruct<SoftwareKeyboardUserWord>(wordData);
+                                }
+                            }
+                        }
+                        _interactiveSession.Push(InlineResponses.ReleasedUserWordInfo(state));
                         break;
                     case InlineKeyboardRequest.SetCustomizeDic:
+                        // Read the custom dic data.
+                        remaining = stream.Length - stream.Position;
+                        if (remaining != Marshal.SizeOf<SoftwareKeyboardCustomizeDic>())
+                        {
+                            Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Customize Dic of {remaining} bytes");
+                        }
+                        else
+                        {
+                            var keyboardDicData = reader.ReadBytes((int)remaining);
+                            _keyboardBackgroundDic = ReadStruct<SoftwareKeyboardCustomizeDic>(keyboardDicData);
+                        }
+                        _interactiveSession.Push(InlineResponses.UnsetCustomizeDic(state));
+                        break;
+                    case InlineKeyboardRequest.SetCustomizedDictionaries:
+                        // Read the custom dictionaries data.
                         remaining = stream.Length - stream.Position;
                         if (remaining != Marshal.SizeOf<SoftwareKeyboardDictSet>())
                         {
-                            Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes!");
+                            Logger.Warning?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard DictSet of {remaining} bytes");
                         }
                         else
                         {
                             var keyboardDictData = reader.ReadBytes((int)remaining);
-                            _keyboardDict = ReadStruct<SoftwareKeyboardDictSet>(keyboardDictData);
+                            _keyboardBackgroundDictSet = ReadStruct<SoftwareKeyboardDictSet>(keyboardDictData);
                         }
-                        _interactiveSession.Push(InlineResponses.Default());
+                        _interactiveSession.Push(InlineResponses.UnsetCustomizedDictionaries(state));
                         break;
                     case InlineKeyboardRequest.Calc:
-                        // Put the keyboard in a Ready state, this will force showing
-                        _state = SoftwareKeyboardState.Ready;
+                        // The Calc request tells the Applet to enter the main input handling loop, which will end
+                        // with either a text being submitted or a cancel request from the user.
+
+                        // NOTE: Some Calc requests happen early in the process and are not meant to be shown. This possibly
+                        // happens because the game has complete control over when the inline keyboard is drawn, but here it
+                        // would cause a dialog to pop in the emulator, which is inconvenient. An algorithm is applied to
+                        // decide whether it is a dummy Calc or not, but regardless of the result, the dummy Calc appears to
+                        // never happen twice, so the keyboard will always show if it has already been shown before.
+                        bool forceShowKeyboard = _alreadyShown;
+                        _alreadyShown = true;
+
+                        // Read the Calc data.
                         remaining = stream.Length - stream.Position;
                         if (remaining != Marshal.SizeOf<SoftwareKeyboardCalc>())
                         {
-                            Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Calc of {remaining} bytes!");
+                            Logger.Error?.Print(LogClass.ServiceAm, $"Received invalid Software Keyboard Calc of {remaining} bytes");
                         }
                         else
                         {
                             var keyboardCalcData = reader.ReadBytes((int)remaining);
-                            _keyboardCalc = ReadStruct<SoftwareKeyboardCalc>(keyboardCalcData);
+                            _keyboardBackgroundCalc = ReadStruct<SoftwareKeyboardCalc>(keyboardCalcData);
 
-                            if (_keyboardCalc.Utf8Mode == 0x1)
+                            // Check if the application expects UTF8 encoding instead of UTF16.
+                            if (_keyboardBackgroundCalc.UseUtf8)
                             {
                                 _encoding = Encoding.UTF8;
                             }
 
                             // Force showing the keyboard regardless of the state, an unwanted
                             // input dialog may show, but it is better than a soft lock.
-                            if (_keyboardCalc.Appear.ShouldBeHidden == 0)
+                            if (_keyboardBackgroundCalc.Appear.ShouldBeHidden == 0)
                             {
-                                showKeyboard = true;
+                                forceShowKeyboard = true;
                             }
                         }
                         // Send an initialization finished signal.
-                        _interactiveSession.Push(InlineResponses.FinishedInitialize());
+                        state = InlineKeyboardState.Ready;
+                        SetInlineState(state);
+                        _interactiveSession.Push(InlineResponses.FinishedInitialize(state));
                         // Start a task with the GUI handler to get user's input.
-                        new Task(() =>
-                        {
-                            bool submit = true;
-                            string inputText = (!string.IsNullOrWhiteSpace(_keyboardCalc.InputText) ? _keyboardCalc.InputText : DefaultText);
-
-                            // Call the configured GUI handler to get user's input.
-                            if (!showKeyboard)
-                            {
-                                // Submit the default text to avoid soft locking if the keyboard was ignored by
-                                // accident. It's better to change the name than being locked out of the game.
-                                submit = true;
-                                inputText = DefaultText;
-
-                                Logger.Debug?.Print(LogClass.Application, "Received a dummy Calc, keyboard will not be shown");
-                            }
-                            else if (_device.UiHandler == null)
-                            {
-                                Logger.Warning?.Print(LogClass.Application, "GUI Handler is not set. Falling back to default");
-                            }
-                            else
-                            {
-                                var args = new SoftwareKeyboardUiArgs
-                                {
-                                    HeaderText = "", // The inline keyboard lacks these texts
-                                    SubtitleText = "",
-                                    GuideText = "",
-                                    SubmitText = (!string.IsNullOrWhiteSpace(_keyboardCalc.Appear.OkText) ? _keyboardCalc.Appear.OkText : "OK"),
-                                    StringLengthMin = 0,
-                                    StringLengthMax = 100,
-                                    InitialText = inputText
-                                };
-
-                                submit = _device.UiHandler.DisplayInputDialog(args, out inputText);
-                            }
-
-                            if (submit)
-                            {
-                                Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard OK...");
-
-                                if (_encoding == Encoding.UTF8)
-                                {
-                                    _interactiveSession.Push(InlineResponses.DecidedEnterUtf8(inputText));
-                                }
-                                else
-                                {
-                                    _interactiveSession.Push(InlineResponses.DecidedEnter(inputText));
-                                }
-                            }
-                            else
-                            {
-                                Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard Cancel...");
-                                _interactiveSession.Push(InlineResponses.DecidedCancel());
-                            }
-
-                            // TODO: Why is this necessary? Does the software expect a constant stream of responses?
-                            Thread.Sleep(500);
-
-                            Logger.Debug?.Print(LogClass.ServiceAm, "Resetting state of the keyboard...");
-                            _interactiveSession.Push(InlineResponses.Default());
-                        }).Start();
+                        new Task(() => { GetInputTextAndSend(forceShowKeyboard, state); }).Start();
                         break;
                     case InlineKeyboardRequest.Finalize:
-                        // The game wants to close the keyboard applet and will wait for a state change.
-                        _state = SoftwareKeyboardState.Uninitialized;
+                        // The calling process wants to close the keyboard applet and will wait for a state change.
+                        _backgroundState = InlineKeyboardState.Uninitialized;
                         AppletStateChanged?.Invoke(this, null);
                         break;
                     default:
                         // We shouldn't be able to get here through standard swkbd execution.
-                        Logger.Error?.Print(LogClass.ServiceAm, $"Invalid Software Keyboard request {request} during state {_state}!");
-                        _interactiveSession.Push(InlineResponses.Default());
+                        Logger.Warning?.Print(LogClass.ServiceAm, $"Invalid Software Keyboard request {request} during state {_backgroundState}");
+                        _interactiveSession.Push(InlineResponses.Default(state));
                         break;
                 }
             }
         }
 
+        private void GetInputTextAndSend(bool forceShowKeyboard, InlineKeyboardState oldState)
+        {
+            bool submit = true;
+
+            // Use the text specified by the Calc if it is available, otherwise use the default one.
+            string inputText = (!string.IsNullOrWhiteSpace(_keyboardBackgroundCalc.InputText) ?
+                _keyboardBackgroundCalc.InputText : DefaultText);
+
+            // Compute the elapsed time for the debouncing algorithm.
+            long currentMillis = PerformanceCounter.ElapsedMilliseconds;
+            long inputElapsedMillis = currentMillis - _lastTextSetMillis;
+
+            // Reset the input text before submitting the final result, that's because some games do not expect
+            // consecutive submissions to abruptly shrink and they will crash if it happens. Changing the string
+            // before the final submission prevents that.
+            InlineKeyboardState newState = InlineKeyboardState.DataAvailable;
+            SetInlineState(newState);
+            ChangedString("", newState);
+
+            if (inputElapsedMillis < DebounceTimeMillis)
+            {
+                // A repeated Calc request has been received without player interaction, after the input has been
+                // sent. This behavior happens in some games, so instead of showing another dialog, just apply a
+                // time-based debouncing algorithm and repeat the last submission, either a value or a cancel.
+                inputText = _textValue;
+                submit = _textValue != null;
+
+                Logger.Warning?.Print(LogClass.Application, "Debouncing repeated keyboard request");
+            }
+            else if (!forceShowKeyboard)
+            {
+                // Submit the default text to avoid soft locking if the keyboard was ignored by
+                // accident. It's better to change the name than being locked out of the game.
+                inputText = DefaultText;
+
+                Logger.Debug?.Print(LogClass.Application, "Received a dummy Calc, keyboard will not be shown");
+            }
+            else if (_device.UiHandler == null)
+            {
+                Logger.Warning?.Print(LogClass.Application, "GUI Handler is not set. Falling back to default");
+            }
+            else
+            {
+                // Call the configured GUI handler to get user's input.
+                var args = new SoftwareKeyboardUiArgs
+                {
+                    HeaderText = "", // The inline keyboard lacks these texts
+                    SubtitleText = "",
+                    GuideText = "",
+                    SubmitText = (!string.IsNullOrWhiteSpace(_keyboardBackgroundCalc.Appear.OkText) ?
+                        _keyboardBackgroundCalc.Appear.OkText : "OK"),
+                    StringLengthMin = 0,
+                    StringLengthMax = 100,
+                    InitialText = inputText
+                };
+
+                submit = _device.UiHandler.DisplayInputDialog(args, out inputText);
+                inputText = submit ? inputText : null;
+            }
+
+            // The 'Complete' state indicates the Calc request has been fulfilled by the applet.
+            newState = InlineKeyboardState.Complete;
+
+            if (submit)
+            {
+                Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard OK");
+                DecidedEnter(inputText, newState);
+            }
+            else
+            {
+                Logger.Debug?.Print(LogClass.ServiceAm, "Sending keyboard Cancel");
+                DecidedCancel(newState);
+            }
+
+            _interactiveSession.Push(InlineResponses.Default(newState));
+
+            // The constant calls to PopInteractiveData suggest that the keyboard applet continuously reports
+            // data back to the application and this can also be time-sensitive. Pushing a state reset right
+            // after the data has been sent does not work properly and the application will soft-lock. This
+            // delay gives time for the application to catch up with the data and properly process the state
+            // reset.
+            Thread.Sleep(ResetDelayMillis);
+
+            // 'Initialized' is the only known state so far that does not soft-lock the keyboard after use.
+            newState = InlineKeyboardState.Initialized;
+
+            Logger.Debug?.Print(LogClass.ServiceAm, $"Resetting state of the keyboard to {newState}");
+
+            SetInlineState(newState);
+            _interactiveSession.Push(InlineResponses.Default(newState));
+
+            // Keep the text and the timestamp of the input for the debouncing algorithm.
+            _textValue = inputText;
+            _lastTextSetMillis = PerformanceCounter.ElapsedMilliseconds;
+        }
+
+        private void ChangedString(string text, InlineKeyboardState state)
+        {
+            if (_encoding == Encoding.UTF8)
+            {
+                if (_useChangedStringV2)
+                {
+                    _interactiveSession.Push(InlineResponses.ChangedStringUtf8V2(text, state));
+                }
+                else
+                {
+                    _interactiveSession.Push(InlineResponses.ChangedStringUtf8(text, state));
+                }
+            }
+            else
+            {
+                if (_useChangedStringV2)
+                {
+                    _interactiveSession.Push(InlineResponses.ChangedStringV2(text, state));
+                }
+                else
+                {
+                    _interactiveSession.Push(InlineResponses.ChangedString(text, state));
+                }
+            }
+        }
+
+        private void DecidedEnter(string text, InlineKeyboardState state)
+        {
+            if (_encoding == Encoding.UTF8)
+            {
+                _interactiveSession.Push(InlineResponses.DecidedEnterUtf8(text, state));
+            }
+            else
+            {
+                _interactiveSession.Push(InlineResponses.DecidedEnter(text, state));
+            }
+        }
+
+        private void DecidedCancel(InlineKeyboardState state)
+        {
+            _interactiveSession.Push(InlineResponses.DecidedCancel(state));
+        }
+
         private byte[] BuildResponse(string text, bool interactive)
         {
             int bufferSize = interactive ? InteractiveBufferSize : StandardBufferSize;
diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs
index 6213c2e4f0..a80690c3d4 100644
--- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCalc.cs
@@ -3,7 +3,7 @@
 namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
 {
     /// <summary>
-    /// A structure that defines the configuration options of the software keyboard.
+    /// A structure with configuration options of the software keyboard when starting a new input request in inline mode.
     /// </summary>
     [StructLayout(LayoutKind.Sequential, Pack=1, CharSet = CharSet.Unicode)]
     struct SoftwareKeyboardCalc
@@ -12,28 +12,56 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
 
         public uint Unknown;
 
+        /// <summary>
+        /// The size of the Calc struct, as reported by the process communicating with the applet.
+        /// </summary>
         public ushort Size;
 
         public byte Unknown1;
         public byte Unknown2;
 
+        /// <summary>
+        /// Configuration flags. Their purpose is currently unknown.
+        /// </summary>
         public ulong Flags;
 
+        /// <summary>
+        /// The original parameters used when initializing the keyboard applet.
+        /// </summary>
         public SoftwareKeyboardInitialize Initialize;
 
+        /// <summary>
+        /// The audio volume used by the sound effects of the keyboard.
+        /// </summary>
         public float Volume;
 
+        /// <summary>
+        /// The initial position of the text cursor (caret) in the provided input text.
+        /// </summary>
         public int CursorPos;
 
+        /// <summary>
+        /// Appearance configurations for the on-screen keyboard.
+        /// </summary>
         public SoftwareKeyboardAppear Appear;
 
+        /// <summary>
+        /// The initial input text to be used by the software keyboard.
+        /// </summary>
         [MarshalAs(UnmanagedType.ByValTStr, SizeConst = InputTextLength + 1)]
         public string InputText;
 
-        public byte Utf8Mode;
+        /// <summary>
+        /// When set, the strings communicated by software keyboard will be encoded as UTF-8 instead of UTF-16.
+        /// </summary>
+        [MarshalAs(UnmanagedType.I1)]
+        public bool UseUtf8;
 
         public byte Unknown3;
 
+        /// <summary>
+        /// [5.0.0+] Enable the backspace key in the software keyboard.
+        /// </summary>
         [MarshalAs(UnmanagedType.I1)]
         public bool BackspaceEnabled;
 
@@ -41,32 +69,57 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
         public byte Unknown5;
 
         [MarshalAs(UnmanagedType.I1)]
-        public byte KeytopAsFloating;
+        public bool KeytopAsFloating;
 
         [MarshalAs(UnmanagedType.I1)]
-        public byte FooterScalable;
+        public bool FooterScalable;
 
         [MarshalAs(UnmanagedType.I1)]
-        public byte AlphaEnabledInInputMode;
+        public bool AlphaEnabledInInputMode;
 
-        [MarshalAs(UnmanagedType.I1)]
         public byte InputModeFadeType;
 
+        /// <summary>
+        /// When set, the software keyboard ignores touch input.
+        /// </summary>
         [MarshalAs(UnmanagedType.I1)]
-        public byte TouchDisabled;
+        public bool TouchDisabled;
 
+        /// <summary>
+        /// When set, the software keyboard ignores hardware keyboard commands.
+        /// </summary>
         [MarshalAs(UnmanagedType.I1)]
-        public byte HardwareKeyboardDisabled;
+        public bool HardwareKeyboardDisabled;
 
         public uint Unknown6;
         public uint Unknown7;
 
+        /// <summary>
+        /// Default value is 1.0.
+        /// </summary>
         public float KeytopScale0;
+
+        /// <summary>
+        /// Default value is 1.0.
+        /// </summary>
         public float KeytopScale1;
+
         public float KeytopTranslate0;
         public float KeytopTranslate1;
+
+        /// <summary>
+        /// Default value is 1.0.
+        /// </summary>
         public float KeytopBgAlpha;
+
+        /// <summary>
+        /// Default value is 1.0.
+        /// </summary>
         public float FooterBgAlpha;
+
+        /// <summary>
+        /// Default value is 1.0.
+        /// </summary>
         public float BalloonScale;
 
         public float Unknown8;
@@ -74,9 +127,19 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
         public uint Unknown10;
         public uint Unknown11;
 
+        /// <summary>
+        /// [5.0.0+] Enable sound effect.
+        /// </summary>
         public byte SeGroup;
 
+        /// <summary>
+        /// [6.0.0+] Enables the Trigger field when Trigger is non-zero.
+        /// </summary>
         public byte TriggerFlag;
+
+        /// <summary>
+        /// [6.0.0+] Always set to zero.
+        /// </summary>
         public byte Trigger;
 
         public byte Padding;
diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCustomizeDic.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCustomizeDic.cs
new file mode 100644
index 0000000000..538fb9274b
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardCustomizeDic.cs
@@ -0,0 +1,14 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
+{
+    /// <summary>
+    /// A structure used by SetCustomizeDic request to software keyboard.
+    /// </summary>
+    [StructLayout(LayoutKind.Sequential, Pack = 4)]
+    struct SoftwareKeyboardCustomizeDic
+    {
+        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 112)]
+        public byte[] Unknown;
+    }
+}
diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs
index 1abdc15b24..b4ffdb9082 100644
--- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardDictSet.cs
@@ -2,10 +2,33 @@
 
 namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
 {
-    [StructLayout(LayoutKind.Sequential, Pack = 4)]
+    /// <summary>
+    /// A structure with custom dictionary words for the software keyboard.
+    /// </summary>
+    [StructLayout(LayoutKind.Sequential, Pack = 2)]
     struct SoftwareKeyboardDictSet
     {
-        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 28)]
-        public uint[] Entries;
+        /// <summary>
+        /// A 0x1000-byte aligned buffer position.
+        /// </summary>
+        public ulong BufferPosition;
+
+        /// <summary>
+        /// A 0x1000-byte aligned buffer size.
+        /// </summary>
+        public uint BufferSize;
+
+        /// <summary>
+        /// Array of word entries in the buffer.
+        /// </summary>
+        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 24)]
+        public ulong[] Entries;
+
+        /// <summary>
+        /// Number of used entries in the Entries field.
+        /// </summary>
+        public ushort TotalEntries;
+
+        public ushort Padding1;
     }
 }
diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs
index 28e3df7f59..764d0e38fd 100644
--- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardInitialize.cs
@@ -3,14 +3,23 @@
 namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
 {
     /// <summary>
-    /// A structure that indicates the initialization the inline software keyboard.
+    /// A structure that mirrors the parameters used to initialize the keyboard applet.
     /// </summary>
     [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
     struct SoftwareKeyboardInitialize
     {
         public uint Unknown;
+
+        /// <summary>
+        /// The applet mode used when launching the swkb. The bits regarding the background vs foreground mode can be wrong.
+        /// </summary>
         public byte LibMode;
+
+        /// <summary>
+        /// [5.0.0+] Set to 0x1 to indicate a firmware version >= 5.0.0.
+        /// </summary>
         public byte FivePlus;
+
         public byte Padding1;
         public byte Padding2;
     }
diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUserWord.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUserWord.cs
new file mode 100644
index 0000000000..08f1c3d33a
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUserWord.cs
@@ -0,0 +1,14 @@
+using System.Runtime.InteropServices;
+
+namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
+{
+    /// <summary>
+    /// A structure used by SetUserWordInfo request to the software keyboard.
+    /// </summary>
+    [StructLayout(LayoutKind.Sequential, Pack = 4)]
+    struct SoftwareKeyboardUserWord
+    {
+        [MarshalAs(UnmanagedType.ByValArray, SizeConst = 100)]
+        public byte[] Unknown;
+    }
+}