Inline software keyboard without input pop up dialog (#2180)

* Initial implementation

* Refactor dynamic text input keys out to facilitate configuration via UI

* Fix code styling

* Add per applet indirect layer handles

* Remove static functions from SoftwareKeyboardRenderer

* Remove inline keyboard reset delay

* Remove inline keyboard V2 responses

* Add inline keyboard soft-lock recovering

* Add comments

* Forward accept and cancel key names to the keyboard and add soft-lock prevention line

* Add dummy window to handle paste events

* Rework inline keyboard state machine and graphics

* Implement IHostUiHandler interfaces on headless WindowBase class

* Add inline keyboard assets

* Fix coding style

* Fix coding style

* Change mode cycling shortcut to F6

* Fix invalid calc size error in games using extended calc

* Remove unnecessary namespaces
This commit is contained in:
Caian Benedicto 2021-10-12 16:54:21 -03:00 committed by GitHub
parent 69093cf2d6
commit 380b95bc59
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
47 changed files with 2853 additions and 344 deletions

View file

@ -0,0 +1,4 @@
namespace Ryujinx.HLE.Ui
{
public delegate void DynamicTextChangedHandler(string text, int cursorBegin, int cursorEnd, bool overwriteMode);
}

View file

@ -0,0 +1,16 @@
using System;
namespace Ryujinx.HLE.Ui
{
public interface IDynamicTextInputHandler : IDisposable
{
event DynamicTextChangedHandler TextChangedEvent;
event KeyPressedHandler KeyPressedEvent;
event KeyReleasedHandler KeyReleasedEvent;
bool TextProcessingEnabled { get; set; }
void SetText(string text, int cursorBegin);
void SetText(string text, int cursorBegin, int cursorEnd);
}
}

View file

@ -0,0 +1,51 @@
using Ryujinx.HLE.HOS.Applets;
using Ryujinx.HLE.HOS.Services.Am.AppletOE.ApplicationProxyService.ApplicationProxy.Types;
namespace Ryujinx.HLE.Ui
{
public interface IHostUiHandler
{
/// <summary>
/// Displays an Input Dialog box to the user and blocks until text is entered.
/// </summary>
/// <param name="userText">Text that the user entered. Set to `null` on internal errors</param>
/// <returns>True when OK is pressed, False otherwise. Also returns True on internal errors</returns>
bool DisplayInputDialog(SoftwareKeyboardUiArgs args, out string userText);
/// <summary>
/// Displays a Message Dialog box to the user and blocks until it is closed.
/// </summary>
/// <returns>True when OK is pressed, False otherwise.</returns>
bool DisplayMessageDialog(string title, string message);
/// <summary>
/// Displays a Message Dialog box specific to Controller Applet and blocks until it is closed.
/// </summary>
/// <returns>True when OK is pressed, False otherwise.</returns>
bool DisplayMessageDialog(ControllerAppletUiArgs args);
/// <summary>
/// Tell the UI that we need to transisition to another program.
/// </summary>
/// <param name="device">The device instance.</param>
/// <param name="kind">The program kind.</param>
/// <param name="value">The value associated to the <paramref name="kind"/>.</param>
void ExecuteProgram(Switch device, ProgramSpecifyKind kind, ulong value);
/// Displays a Message Dialog box specific to Error Applet and blocks until it is closed.
/// </summary>
/// <returns>False when OK is pressed, True when another button (Details) is pressed.</returns>
bool DisplayErrorAppletDialog(string title, string message, string[] buttonsText);
/// <summary>
/// Creates a handler to process keyboard inputs into text strings.
/// </summary>
/// <returns>An instance of the text handler.</returns>
IDynamicTextInputHandler CreateDynamicTextInputHandler();
/// <summary>
/// Gets fonts and colors used by the host.
/// </summary>
IHostUiTheme HostUiTheme { get; }
}
}

View file

@ -0,0 +1,13 @@
namespace Ryujinx.HLE.Ui
{
public interface IHostUiTheme
{
string FontFamily { get; }
ThemeColor DefaultBackgroundColor { get; }
ThemeColor DefaultForegroundColor { get; }
ThemeColor DefaultBorderColor { get; }
ThemeColor SelectionBackgroundColor { get; }
ThemeColor SelectionForegroundColor { get; }
}
}

View file

@ -0,0 +1,6 @@
using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Npad;
namespace Ryujinx.HLE.Ui.Input
{
delegate void NpadButtonHandler(int npadIndex, NpadButton button);
}

View file

@ -0,0 +1,137 @@
using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Common;
using Ryujinx.HLE.HOS.Services.Hid.Types.SharedMemory.Npad;
using System;
namespace Ryujinx.HLE.Ui.Input
{
/// <summary>
/// Class that converts Hid entries for the Npad into pressed / released events.
/// </summary>
class NpadReader
{
private readonly Switch _device;
private NpadCommonState[] _lastStates;
public event NpadButtonHandler NpadButtonUpEvent;
public event NpadButtonHandler NpadButtonDownEvent;
public NpadReader(Switch device)
{
_device = device;
_lastStates = new NpadCommonState[_device.Hid.SharedMemory.Npads.Length];
}
public NpadButton GetCurrentButtonsOfNpad(int npadIndex)
{
return _lastStates[npadIndex].Buttons;
}
public NpadButton GetCurrentButtonsOfAllNpads()
{
NpadButton buttons = 0;
foreach (var state in _lastStates)
{
buttons |= state.Buttons;
}
return buttons;
}
private ref RingLifo<NpadCommonState> GetCommonStateLifo(ref NpadInternalState npad)
{
switch (npad.StyleSet)
{
case NpadStyleTag.FullKey:
return ref npad.FullKey;
case NpadStyleTag.Handheld:
return ref npad.Handheld;
case NpadStyleTag.JoyDual:
return ref npad.JoyDual;
case NpadStyleTag.JoyLeft:
return ref npad.JoyLeft;
case NpadStyleTag.JoyRight:
return ref npad.JoyRight;
case NpadStyleTag.Palma:
return ref npad.Palma;
default:
return ref npad.SystemExt;
}
}
public void Update(bool supressEvents=false)
{
ref var npads = ref _device.Hid.SharedMemory.Npads;
// Process each input individually.
for (int npadIndex = 0; npadIndex < npads.Length; npadIndex++)
{
UpdateNpad(npadIndex, supressEvents);
}
}
private void UpdateNpad(int npadIndex, bool supressEvents)
{
const int MaxEntries = 1024;
ref var npadState = ref _device.Hid.SharedMemory.Npads[npadIndex];
ref var lastEntry = ref _lastStates[npadIndex];
var fullKeyEntries = GetCommonStateLifo(ref npadState.InternalState).ReadEntries(MaxEntries);
int firstEntryNum;
// Scan the LIFO for the first entry that is newer that what's already processed.
for (firstEntryNum = fullKeyEntries.Length - 1; firstEntryNum >= 0 && fullKeyEntries[firstEntryNum].Object.SamplingNumber <= lastEntry.SamplingNumber; firstEntryNum--) ;
if (firstEntryNum == -1)
{
return;
}
for (; firstEntryNum >= 0; firstEntryNum--)
{
var entry = fullKeyEntries[firstEntryNum];
// The interval of valid entries should be contiguous.
if (entry.SamplingNumber < lastEntry.SamplingNumber)
{
break;
}
if (!supressEvents)
{
ProcessNpadButtons(npadIndex, entry.Object.Buttons);
}
lastEntry = entry.Object;
}
}
private void ProcessNpadButtons(int npadIndex, NpadButton buttons)
{
NpadButton lastButtons = _lastStates[npadIndex].Buttons;
for (ulong buttonMask = 1; buttonMask != 0; buttonMask <<= 1)
{
NpadButton currentButton = (NpadButton)buttonMask & buttons;
NpadButton lastButton = (NpadButton)buttonMask & lastButtons;
if (lastButton != 0)
{
if (currentButton == 0)
{
NpadButtonUpEvent?.Invoke(npadIndex, lastButton);
}
}
else
{
if (currentButton != 0)
{
NpadButtonDownEvent?.Invoke(npadIndex, currentButton);
}
}
}
}
}
}

View file

@ -0,0 +1,6 @@
using Ryujinx.Common.Configuration.Hid;
namespace Ryujinx.HLE.Ui
{
public delegate bool KeyPressedHandler(Key key);
}

View file

@ -0,0 +1,6 @@
using Ryujinx.Common.Configuration.Hid;
namespace Ryujinx.HLE.Ui
{
public delegate bool KeyReleasedHandler(Key key);
}

View file

@ -0,0 +1,34 @@
using Ryujinx.HLE.HOS.Services.SurfaceFlinger;
namespace Ryujinx.HLE.Ui
{
/// <summary>
/// Information about the indirect layer that is being drawn to.
/// </summary>
class RenderingSurfaceInfo
{
public ColorFormat ColorFormat { get; }
public uint Width { get; }
public uint Height { get; }
public uint Pitch { get; }
public uint Size { get; }
public RenderingSurfaceInfo(ColorFormat colorFormat, uint width, uint height, uint pitch, uint size)
{
ColorFormat = colorFormat;
Width = width;
Height = height;
Pitch = pitch;
Size = size;
}
public bool Equals(RenderingSurfaceInfo other)
{
return ColorFormat == other.ColorFormat &&
Width == other.Width &&
Height == other.Height &&
Pitch == other.Pitch &&
Size == other.Size;
}
}
}

View file

@ -0,0 +1,18 @@
namespace Ryujinx.HLE.Ui
{
public struct ThemeColor
{
public float A { get; }
public float R { get; }
public float G { get; }
public float B { get; }
public ThemeColor(float a, float r, float g, float b)
{
A = a;
R = r;
G = g;
B = b;
}
}
}