diff options
| author | Mary <me@thog.eu> | 2021-11-28 21:24:17 +0100 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2021-11-28 21:24:17 +0100 |
| commit | 57d3296ba4e5c1fc7ca30376c7ca8eb3041ae2f6 (patch) | |
| tree | 02e17606a847ff11f68bc7bf123e882f87055b52 /Ryujinx.HLE/HOS/Applets | |
| parent | 7b040e51b078a16e979ad962ba1265f3be4bcb1d (diff) | |
infra: Migrate to .NET 6 (#2829)
* infra: Migrate to .NET 6
* Rollback version naming change
* Workaround .NET 6 ZipArchive API issues
* ci: Switch to VS 2022 for AppVeyor
CI is now ready for .NET 6
* Suppress WebClient warning in DoUpdateWithMultipleThreads
* Attempt to workaround System.Drawing.Common changes on 6.0.0
* Change keyboard rendering from System.Drawing to ImageSharp
* Make the software keyboard renderer multithreaded
* Bump ImageSharp version to 1.0.4 to fix a bug in Image.Load
* Add fallback fonts to the keyboard renderer
* Fix warnings
* Address caian's comment
* Clean up linux workaround as it's uneeded now
* Update readme
Co-authored-by: Caian Benedicto <caianbene@gmail.com>
Diffstat (limited to 'Ryujinx.HLE/HOS/Applets')
5 files changed, 712 insertions, 642 deletions
diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs index 523fa5de..3cfd192c 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardApplet.cs @@ -170,7 +170,9 @@ namespace Ryujinx.HLE.HOS.Applets { _npads?.Update(); - return _keyboardRenderer?.DrawTo(surfaceInfo, destination, position) ?? false; + _keyboardRenderer?.SetSurfaceInfo(surfaceInfo); + + return _keyboardRenderer?.DrawTo(destination, position) ?? false; } private void ExecuteForegroundKeyboard() diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs index dfd10925..c30ad11b 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs @@ -1,717 +1,164 @@ using Ryujinx.HLE.Ui; using Ryujinx.Memory; using System; -using System.Buffers.Binary; -using System.Diagnostics; -using System.Drawing; -using System.Drawing.Drawing2D; -using System.Drawing.Imaging; -using System.Drawing.Text; -using System.IO; -using System.Numerics; -using System.Reflection; -using System.Runtime.InteropServices; using System.Threading; namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard { /// <summary> - /// Class that generates the graphics for the software keyboard applet during inline mode. + /// Class that manages the renderer base class and its state in a multithreaded context. /// </summary> internal class SoftwareKeyboardRenderer : IDisposable { - const int TextBoxBlinkThreshold = 8; - const int TextBoxBlinkSleepMilliseconds = 100; - const int TextBoxBlinkJoinWaitMilliseconds = 1000; + private const int TextBoxBlinkSleepMilliseconds = 100; + private const int RendererWaitTimeoutMilliseconds = 100; - const string MessageText = "Please use the keyboard to input text"; - const string AcceptText = "Accept"; - const string CancelText = "Cancel"; - const string ControllerToggleText = "Toggle input"; + private readonly object _stateLock = new object(); - private RenderingSurfaceInfo _surfaceInfo; - private Bitmap _surface = null; - private object _renderLock = new object(); + private SoftwareKeyboardUiState _state = new SoftwareKeyboardUiState(); + private SoftwareKeyboardRendererBase _renderer; - private string _inputText = ""; - private int _cursorStart = 0; - private int _cursorEnd = 0; - private bool _acceptPressed = false; - private bool _cancelPressed = false; - private bool _overwriteMode = false; - private bool _typingEnabled = true; - private bool _controllerEnabled = true; - - private Image _ryujinxLogo = null; - private Image _padAcceptIcon = null; - private Image _padCancelIcon = null; - private Image _keyModeIcon = null; - - private float _textBoxOutlineWidth; - private float _padPressedPenWidth; - - private Brush _panelBrush; - private Brush _disabledBrush; - private Brush _textNormalBrush; - private Brush _textSelectedBrush; - private Brush _textOverCursorBrush; - private Brush _cursorBrush; - private Brush _selectionBoxBrush; - private Brush _keyCapBrush; - private Brush _keyProgressBrush; - - private Pen _gridSeparatorPen; - private Pen _textBoxOutlinePen; - private Pen _cursorPen; - private Pen _selectionBoxPen; - private Pen _padPressedPen; - - private int _inputTextFontSize; - private int _padButtonFontSize; - private Font _messageFont; - private Font _inputTextFont; - private Font _labelsTextFont; - private Font _padSymbolFont; - private Font _keyCapFont; - - private float _inputTextCalibrationHeight; - private float _panelPositionY; - private RectangleF _panelRectangle; - private PointF _logoPosition; - private float _messagePositionY; - - private TRef<int> _textBoxBlinkCounter = new TRef<int>(0); private TimedAction _textBoxBlinkTimedAction = new TimedAction(); + private TimedAction _renderAction = new TimedAction(); public SoftwareKeyboardRenderer(IHostUiTheme uiTheme) { - _surfaceInfo = new RenderingSurfaceInfo(0, 0, 0, 0, 0); - - string ryujinxLogoPath = "Ryujinx.Ui.Resources.Logo_Ryujinx.png"; - int ryujinxLogoSize = 32; - - _ryujinxLogo = LoadResource(Assembly.GetEntryAssembly(), ryujinxLogoPath, ryujinxLogoSize, ryujinxLogoSize); - - string padAcceptIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnA.png"; - string padCancelIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnB.png"; - string keyModeIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_KeyF6.png"; - - _padAcceptIcon = LoadResource(Assembly.GetExecutingAssembly(), padAcceptIconPath , 0, 0); - _padCancelIcon = LoadResource(Assembly.GetExecutingAssembly(), padCancelIconPath , 0, 0); - _keyModeIcon = LoadResource(Assembly.GetExecutingAssembly(), keyModeIconPath , 0, 0); - - Color panelColor = ToColor(uiTheme.DefaultBackgroundColor, 255); - Color panelTransparentColor = ToColor(uiTheme.DefaultBackgroundColor, 150); - Color normalTextColor = ToColor(uiTheme.DefaultForegroundColor); - Color invertedTextColor = ToColor(uiTheme.DefaultForegroundColor, null, true); - Color selectedTextColor = ToColor(uiTheme.SelectionForegroundColor); - Color borderColor = ToColor(uiTheme.DefaultBorderColor); - Color selectionBackgroundColor = ToColor(uiTheme.SelectionBackgroundColor); - Color gridSeparatorColor = Color.FromArgb(180, 255, 255, 255); - - float cursorWidth = 2; - - _textBoxOutlineWidth = 2; - _padPressedPenWidth = 2; - - _panelBrush = new SolidBrush(panelColor); - _disabledBrush = new SolidBrush(panelTransparentColor); - _textNormalBrush = new SolidBrush(normalTextColor); - _textSelectedBrush = new SolidBrush(selectedTextColor); - _textOverCursorBrush = new SolidBrush(invertedTextColor); - _cursorBrush = new SolidBrush(normalTextColor); - _selectionBoxBrush = new SolidBrush(selectionBackgroundColor); - _keyCapBrush = Brushes.White; - _keyProgressBrush = new SolidBrush(borderColor); + _renderer = new SoftwareKeyboardRendererBase(uiTheme); - _gridSeparatorPen = new Pen(gridSeparatorColor, 2); - _textBoxOutlinePen = new Pen(borderColor, _textBoxOutlineWidth); - _cursorPen = new Pen(normalTextColor, cursorWidth); - _selectionBoxPen = new Pen(selectionBackgroundColor, cursorWidth); - _padPressedPen = new Pen(borderColor, _padPressedPenWidth); - - _inputTextFontSize = 20; - _padButtonFontSize = 24; - - string font = uiTheme.FontFamily; - - _messageFont = new Font(font, 26, FontStyle.Regular, GraphicsUnit.Pixel); - _inputTextFont = new Font(font, _inputTextFontSize, FontStyle.Regular, GraphicsUnit.Pixel); - _labelsTextFont = new Font(font, 24, FontStyle.Regular, GraphicsUnit.Pixel); - _padSymbolFont = new Font(font, _padButtonFontSize, FontStyle.Regular, GraphicsUnit.Pixel); - _keyCapFont = new Font(font, 15, FontStyle.Regular, GraphicsUnit.Pixel); - - // System.Drawing has serious problems measuring strings, so it requires a per-pixel calibration - // to ensure we are rendering text inside the proper region - _inputTextCalibrationHeight = CalibrateTextHeight(_inputTextFont); - - StartTextBoxBlinker(_textBoxBlinkTimedAction, _textBoxBlinkCounter); + StartTextBoxBlinker(_textBoxBlinkTimedAction, _state, _stateLock); + StartRenderer(_renderAction, _renderer, _state, _stateLock); } - private static void StartTextBoxBlinker(TimedAction timedAction, TRef<int> blinkerCounter) + private static void StartTextBoxBlinker(TimedAction timedAction, SoftwareKeyboardUiState state, object stateLock) { timedAction.Reset(() => { - // The blinker is on falf of the time and events such as input - // changes can reset the blinker. - var value = Volatile.Read(ref blinkerCounter.Value); - value = (value + 1) % (2 * TextBoxBlinkThreshold); - Volatile.Write(ref blinkerCounter.Value, value); - - }, TextBoxBlinkSleepMilliseconds); - } - - private Color ToColor(ThemeColor color, byte? overrideAlpha = null, bool flipRgb = false) - { - var a = (byte)(color.A * 255); - var r = (byte)(color.R * 255); - var g = (byte)(color.G * 255); - var b = (byte)(color.B * 255); - - if (flipRgb) - { - r = (byte)(255 - r); - g = (byte)(255 - g); - b = (byte)(255 - b); - } - - return Color.FromArgb(overrideAlpha.GetValueOrDefault(a), r, g, b); - } - - private Image LoadResource(Assembly assembly, string resourcePath, int newWidth, int newHeight) - { - Stream resourceStream = assembly.GetManifestResourceStream(resourcePath); - - Debug.Assert(resourceStream != null); - - var originalImage = Image.FromStream(resourceStream); - - if (newHeight == 0 || newWidth == 0) - { - return originalImage; - } - - var newSize = new Rectangle(0, 0, newWidth, newHeight); - var newImage = new Bitmap(newWidth, newHeight); - - using (var graphics = System.Drawing.Graphics.FromImage(newImage)) - using (var wrapMode = new ImageAttributes()) - { - graphics.InterpolationMode = InterpolationMode.HighQualityBicubic; - graphics.CompositingQuality = CompositingQuality.HighQuality; - graphics.CompositingMode = CompositingMode.SourceCopy; - graphics.PixelOffsetMode = PixelOffsetMode.HighQuality; - graphics.SmoothingMode = SmoothingMode.HighQuality; - - wrapMode.SetWrapMode(WrapMode.TileFlipXY); - graphics.DrawImage(originalImage, newSize, 0, 0, originalImage.Width, originalImage.Height, GraphicsUnit.Pixel, wrapMode); - } - - return newImage; - } - -#pragma warning disable CS8632 - public void UpdateTextState(string? inputText, int? cursorStart, int? cursorEnd, bool? overwriteMode, bool? typingEnabled) -#pragma warning restore CS8632 - { - lock (_renderLock) - { - // Update the parameters that were provided. - _inputText = inputText != null ? inputText : _inputText; - _cursorStart = cursorStart.GetValueOrDefault(_cursorStart); - _cursorEnd = cursorEnd.GetValueOrDefault(_cursorEnd); - _overwriteMode = overwriteMode.GetValueOrDefault(_overwriteMode); - _typingEnabled = typingEnabled.GetValueOrDefault(_typingEnabled); - - // Reset the cursor blink. - Volatile.Write(ref _textBoxBlinkCounter.Value, 0); - } - } - - public void UpdateCommandState(bool? acceptPressed, bool? cancelPressed, bool? controllerEnabled) - { - lock (_renderLock) - { - // Update the parameters that were provided. - _acceptPressed = acceptPressed.GetValueOrDefault(_acceptPressed); - _cancelPressed = cancelPressed.GetValueOrDefault(_cancelPressed); - _controllerEnabled = controllerEnabled.GetValueOrDefault(_controllerEnabled); - } - } - - private void Redraw() - { - if (_surface == null) - { - return; - } - - using (var graphics = CreateGraphics()) - { - var messageRectangle = MeasureString(graphics, MessageText, _messageFont); - float messagePositionX = (_panelRectangle.Width - messageRectangle.Width) / 2 - messageRectangle.X; - float messagePositionY = _messagePositionY - messageRectangle.Y; - PointF messagePosition = new PointF(messagePositionX, messagePositionY); - - graphics.Clear(Color.Transparent); - graphics.TranslateTransform(0, _panelPositionY); - graphics.FillRectangle(_panelBrush, _panelRectangle); - graphics.DrawImage(_ryujinxLogo, _logoPosition); - - DrawString(graphics, MessageText, _messageFont, _textNormalBrush, messagePosition); - - if (!_typingEnabled) + lock (stateLock) { - // Just draw a semi-transparent rectangle on top to fade the component with the background. - // TODO (caian): This will not work if one decides to add make background semi-transparent as well. - graphics.FillRectangle(_disabledBrush, messagePositionX, messagePositionY, messageRectangle.Width, messageRectangle.Height); - } - - DrawTextBox(graphics); - - float halfWidth = _panelRectangle.Width / 2; - - PointF acceptButtonPosition = new PointF(halfWidth - 180, 185); - PointF cancelButtonPosition = new PointF(halfWidth , 185); - PointF disableButtonPosition = new PointF(halfWidth + 180, 185); - - DrawPadButton (graphics, acceptButtonPosition , _padAcceptIcon, AcceptText, _acceptPressed, _controllerEnabled); - DrawPadButton (graphics, cancelButtonPosition , _padCancelIcon, CancelText, _cancelPressed, _controllerEnabled); - DrawControllerToggle(graphics, disableButtonPosition, _controllerEnabled); - } - } - - private void RecreateSurface() - { - Debug.Assert(_surfaceInfo.ColorFormat == Services.SurfaceFlinger.ColorFormat.A8B8G8R8); - - // Use the whole area of the image to draw, even the alignment, otherwise it may shear the final - // image if the pitch is different. - uint totalWidth = _surfaceInfo.Pitch / 4; - uint totalHeight = _surfaceInfo.Size / _surfaceInfo.Pitch; - - Debug.Assert(_surfaceInfo.Width <= totalWidth); - Debug.Assert(_surfaceInfo.Height <= totalHeight); - Debug.Assert(_surfaceInfo.Pitch * _surfaceInfo.Height <= _surfaceInfo.Size); - - _surface = new Bitmap((int)totalWidth, (int)totalHeight, PixelFormat.Format32bppArgb); - } - - private void RecomputeConstants() - { - float totalWidth = _surfaceInfo.Width; - float totalHeight = _surfaceInfo.Height; - - float panelHeight = 240; + // The blinker is on half of the time and events such as input + // changes can reset the blinker. + state.TextBoxBlinkCounter = (state.TextBoxBlinkCounter + 1) % (2 * SoftwareKeyboardRendererBase.TextBoxBlinkThreshold); - _panelPositionY = totalHeight - panelHeight; - _panelRectangle = new RectangleF(0, 0, totalWidth, panelHeight); - - _messagePositionY = 60; - - float logoPositionX = (totalWidth - _ryujinxLogo.Width) / 2; - float logoPositionY = 18; - - _logoPosition = new PointF(logoPositionX, logoPositionY); - } - - private StringFormat CreateStringFormat(string text) - { - StringFormat format = new StringFormat(StringFormat.GenericTypographic); - format.FormatFlags |= StringFormatFlags.MeasureTrailingSpaces; - format.SetMeasurableCharacterRanges(new CharacterRange[] { new CharacterRange(0, text.Length) }); - - return format; - } - - private RectangleF MeasureString(System.Drawing.Graphics graphics, string text, System.Drawing.Font font) - { - bool isEmpty = false; - - if (string.IsNullOrEmpty(text)) - { - isEmpty = true; - text = " "; - } - - var format = CreateStringFormat(text); - var rectangle = new RectangleF(0, 0, float.PositiveInfinity, float.PositiveInfinity); - var regions = graphics.MeasureCharacterRanges(text, font, rectangle, format); - - Debug.Assert(regions.Length == 1); - - rectangle = regions[0].GetBounds(graphics); - - if (isEmpty) - { - rectangle.Width = 0; - } - else - { - rectangle.Width += 1.0f; - } - - return rectangle; + // Tell the render thread there is something new to render. + Monitor.PulseAll(stateLock); + } + }, TextBoxBlinkSleepMilliseconds); } - private float CalibrateTextHeight(Font font) + private static void StartRenderer(TimedAction timedAction, SoftwareKeyboardRendererBase renderer, SoftwareKeyboardUiState state, object stateLock) { - // This is a pixel-wise calibration that tests the offset of a reference character because Windows text measurement - // is horrible when compared to other frameworks like Cairo and diverge across systems and fonts. - - Debug.Assert(font.Unit == GraphicsUnit.Pixel); + SoftwareKeyboardUiState internalState = new SoftwareKeyboardUiState(); - var surfaceSize = (int)Math.Ceiling(2 * font.Size); + bool canCreateSurface = false; + bool needsUpdate = true; - string calibrationText = "|"; - - using (var surface = new Bitmap(surfaceSize, surfaceSize, PixelFormat.Format32bppArgb)) - using (var graphics = CreateGraphics(surface)) + timedAction.Reset(() => { - var measuredRectangle = MeasureString(graphics, calibrationText, font); - - Debug.Assert(measuredRectangle.Right <= surfaceSize); - Debug.Assert(measuredRectangle.Bottom <= surfaceSize); - - var textPosition = new PointF(0, 0); - - graphics.Clear(Color.Transparent); - DrawString(graphics, calibrationText, font, Brushes.White, textPosition); - - var lockRectangle = new Rectangle(0, 0, surface.Width, surface.Height); - var surfaceData = surface.LockBits(lockRectangle, ImageLockMode.ReadOnly, PixelFormat.Format32bppArgb); - var surfaceBytes = new byte[surfaceData.Stride * surfaceData.Height]; - - Marshal.Copy(surfaceData.Scan0, surfaceBytes, 0, surfaceBytes.Length); - - Point topLeft = new Point(); - Point bottomLeft = new Point(); - - bool foundTopLeft = false; - - for (int y = 0; y < surfaceData.Height; y++) + lock (stateLock) { - for (int x = 0; x < surfaceData.Stride; x += 4) + if (!Monitor.Wait(stateLock, RendererWaitTimeoutMilliseconds)) { - int position = y * surfaceData.Stride + x; - - if (surfaceBytes[position] != 0) - { - if (!foundTopLeft) - { - topLeft.X = x; - topLeft.Y = y; - foundTopLeft = true; - - break; - } - else - { - bottomLeft.X = x; - bottomLeft.Y = y; - - break; - } - } + return; } - } - - return bottomLeft.Y - topLeft.Y; - } - } - - private void DrawString(System.Drawing.Graphics graphics, string text, Font font, Brush brush, PointF point) - { - var format = CreateStringFormat(text); - graphics.DrawString(text, font, brush, point, format); - } - - private System.Drawing.Graphics CreateGraphics() - { - return CreateGraphics(_surface); - } - - private System.Drawing.Graphics CreateGraphics(Image surface) - { - var graphics = System.Drawing.Graphics.FromImage(surface); - - graphics.TextRenderingHint = TextRenderingHint.ClearTypeGridFit; - graphics.InterpolationMode = InterpolationMode.NearestNeighbor; - graphics.CompositingQuality = CompositingQuality.HighSpeed; - graphics.CompositingMode = CompositingMode.SourceOver; - graphics.PixelOffsetMode = PixelOffsetMode.HighSpeed; - graphics.SmoothingMode = SmoothingMode.HighSpeed; - - return graphics; - } - - private void DrawTextBox(System.Drawing.Graphics graphics) - { - var inputTextRectangle = MeasureString(graphics, _inputText, _inputTextFont); - - float boxWidth = (int)(Math.Max(300, inputTextRectangle.Width + inputTextRectangle.X + 8)); - float boxHeight = 32; - float boxY = 110; - float boxX = (int)((_panelRectangle.Width - boxWidth) / 2); - - graphics.DrawRectangle(_textBoxOutlinePen, boxX, boxY, boxWidth, boxHeight); - float inputTextX = (_panelRectangle.Width - inputTextRectangle.Width) / 2 - inputTextRectangle.X; - float inputTextY = boxY + boxHeight - inputTextRectangle.Bottom - 5; + needsUpdate = UpdateStateField(ref state.InputText, ref internalState.InputText); + needsUpdate |= UpdateStateField(ref state.CursorBegin, ref internalState.CursorBegin); + needsUpdate |= UpdateStateField(ref state.CursorEnd, ref internalState.CursorEnd); + needsUpdate |= UpdateStateField(ref state.AcceptPressed, ref internalState.AcceptPressed); + needsUpdate |= UpdateStateField(ref state.CancelPressed, ref internalState.CancelPressed); + needsUpdate |= UpdateStateField(ref state.OverwriteMode, ref internalState.OverwriteMode); + needsUpdate |= UpdateStateField(ref state.TypingEnabled, ref internalState.TypingEnabled); + needsUpdate |= UpdateStateField(ref state.ControllerEnabled, ref internalState.ControllerEnabled); + needsUpdate |= UpdateStateField(ref state.TextBoxBlinkCounter, ref internalState.TextBoxBlinkCounter); - var inputTextPosition = new PointF(inputTextX, inputTextY); + canCreateSurface = state.SurfaceInfo != null && internalState.SurfaceInfo == null; - DrawString(graphics, _inputText, _inputTextFont, _textNormalBrush, inputTextPosition); - - // Draw the cursor on top of the text and redraw the text with a different color if necessary. - - Brush cursorTextBrush; - Brush cursorBrush; - Pen cursorPen; - - float cursorPositionYBottom = inputTextY + inputTextRectangle.Bottom; - float cursorPositionYTop = cursorPositionYBottom - _inputTextCalibrationHeight - 2; - float cursorPositionXLeft; - float cursorPositionXRight; - - bool cursorVisible = false; - - if (_cursorStart != _cursorEnd) - { - cursorTextBrush = _textSelectedBrush; - cursorBrush = _selectionBoxBrush; - cursorPen = _selectionBoxPen; - - string textUntilBegin = _inputText.Substring(0, _cursorStart); - string textUntilEnd = _inputText.Substring(0, _cursorEnd); - - RectangleF selectionBeginRectangle = MeasureString(graphics, textUntilBegin, _inputTextFont); - RectangleF selectionEndRectangle = MeasureString(graphics, textUntilEnd , _inputTextFont); - - cursorVisible = true; - cursorPositionXLeft = inputTextX + selectionBeginRectangle.Width + selectionBeginRectangle.X; - cursorPositionXRight = inputTextX + selectionEndRectangle.Width + selectionEndRectangle.X; - } - else - { - cursorTextBrush = _textOverCursorBrush; - cursorBrush = _cursorBrush; - cursorPen = _cursorPen; - - if (Volatile.Read(ref _textBoxBlinkCounter.Value) < TextBoxBlinkThreshold) - { - // Show the blinking cursor. - - int cursorStart = Math.Min(_inputText.Length, _cursorStart); - string textUntilCursor = _inputText.Substring(0, cursorStart); - RectangleF cursorTextRectangle = MeasureString(graphics, textUntilCursor, _inputTextFont); - - cursorVisible = true; - cursorPositionXLeft = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.X; - - if (_overwriteMode) + if (canCreateSurface) { - // The blinking cursor is in overwrite mode so it takes the size of a character. - - if (_cursorStart < _inputText.Length) - { - textUntilCursor = _inputText.Substring(0, cursorStart + 1); - cursorTextRectangle = MeasureString(graphics, textUntilCursor, _inputTextFont); - cursorPositionXRight = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.X; - } - else - { - cursorPositionXRight = cursorPositionXLeft + _inputTextFontSize / 2; - } + internalState.SurfaceInfo = state.SurfaceInfo; } - else - { - // The blinking cursor is in insert mode so it is only a line. - cursorPositionXRight = cursorPositionXLeft; - } - } - else - { - cursorPositionXLeft = inputTextX; - cursorPositionXRight = inputTextX; } - } - - if (_typingEnabled && cursorVisible) - { - float cursorWidth = cursorPositionXRight - cursorPositionXLeft; - float cursorHeight = cursorPositionYBottom - cursorPositionYTop; - if (cursorWidth == 0) + if (canCreateSurface) { - graphics.DrawLine(cursorPen, cursorPositionXLeft, cursorPositionYTop, cursorPositionXLeft, cursorPositionYBottom); + renderer.CreateSurface(internalState.SurfaceInfo); } - else - { - graphics.DrawRectangle(cursorPen, cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight); - graphics.FillRectangle(cursorBrush, cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight); - - var cursorRectangle = new RectangleF(cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight); - - var oldClip = graphics.Clip; - graphics.Clip = new Region(cursorRectangle); - - DrawString(graphics, _inputText, _inputTextFont, cursorTextBrush, inputTextPosition); - graphics.Clip = oldClip; + if (needsUpdate) + { + renderer.DrawMutableElements(internalState); + renderer.CopyImageToBuffer(); + needsUpdate = false; } - } - else if (!_typingEnabled) - { - // Just draw a semi-transparent rectangle on top to fade the component with the background. - // TODO (caian): This will not work if one decides to add make background semi-transparent as well. - graphics.FillRectangle(_disabledBrush, boxX - _textBoxOutlineWidth, boxY - _textBoxOutlineWidth, - boxWidth + 2* _textBoxOutlineWidth, boxHeight + 2* _textBoxOutlineWidth); - } + }); } - private void DrawPadButton(System.Drawing.Graphics graphics, PointF point, Image icon, string label, bool pressed, bool enabled) + private static bool UpdateStateField<T>(ref T source, ref T destination) where T : IEquatable<T> { - // Use relative positions so we can center the the entire drawing later. - - float iconX = 0; - float iconY = 0; - float iconWidth = icon.Width; - float iconHeight = icon.Height; - - var labelRectangle = MeasureString(graphics, label, _labelsTextFont); - - float labelPositionX = iconWidth + 8 - labelRectangle.X; - float labelPositionY = (iconHeight - labelRectangle.Height) / 2 - labelRectangle.Y - 1; - - float fullWidth = labelPositionX + labelRectangle.Width + labelRectangle.X; - float fullHeight = iconHeight; - - // Convert all relative positions into absolute. - - float originX = (int)(point.X - fullWidth / 2); - float originY = (int)(point.Y - fullHeight / 2); - - iconX += originX; - iconY += originY; - - var labelPosition = new PointF(labelPositionX + originX, labelPositionY + originY); - - graphics.DrawImageUnscaled(icon, (int)iconX, (int)iconY); - - DrawString(graphics, label, _labelsTextFont, _textNormalBrush, labelPosition); - - GraphicsPath frame = new GraphicsPath(); - frame.AddRectangle(new RectangleF(originX - 2 * _padPressedPenWidth, originY - 2 * _padPressedPenWidth, - fullWidth + 4 * _padPressedPenWidth, fullHeight + 4 * _padPressedPenWidth)); - - if (enabled) + if (!source.Equals(destination)) { - if (pressed) - { - graphics.DrawPath(_padPressedPen, frame); - } + destination = source; + return true; } - else - { - // Just draw a semi-transparent rectangle on top to fade the component with the background. - // TODO (caian): This will not work if one decides to add make background semi-transparent as well. - graphics.FillPath(_disabledBrush, frame); - } - } - - private void DrawControllerToggle(System.Drawing.Graphics graphics, PointF point, bool enabled) - { - var labelRectangle = MeasureString(graphics, ControllerToggleText, _labelsTextFont); - - // Use relative positions so we can center the the entire drawing later. - - float keyWidth = _keyModeIcon.Width; - float keyHeight = _keyModeIcon.Height; - - float labelPositionX = keyWidth + 8 - labelRectangle.X; - float labelPositionY = -labelRectangle.Y - 1; - - float keyX = 0; - float keyY = (int)((labelPositionY + labelRectangle.Height - keyHeight) / 2); - - float fullWidth = labelPositionX + labelRectangle.Width; - float fullHeight = Math.Max(labelPositionY + labelRectangle.Height, keyHeight); - - // Convert all relative positions into absolute. - float originX = (int)(point.X - fullWidth / 2); - float originY = (int)(point.Y - fullHeight / 2); - - keyX += originX; - keyY += originY; - - var labelPosition = new PointF(labelPositionX + originX, labelPositionY + originY); - var overlayPosition = new Point((int)keyX, (int)keyY); - - graphics.DrawImageUnscaled(_keyModeIcon, overlayPosition); - - DrawString(graphics, ControllerToggleText, _labelsTextFont, _textNormalBrush, labelPosition); + return false; } - private bool TryCopyTo(IVirtualMemoryManager destination, ulong position) +#pragma warning disable CS8632 + public void UpdateTextState(string? inputText, int? cursorBegin, int? cursorEnd, bool? overwriteMode, bool? typingEnabled) +#pragma warning restore CS8632 { - if (_surface == null) + lock (_stateLock) { - return false; - } - - Rectangle lockRectangle = new Rectangle(0, 0, _surface.Width, _surface.Height); - BitmapData surfaceData = _surface.LockBits(lockRectangle, ImageLockMode.ReadWrite, PixelFormat.Format32bppArgb); - - Debug.Assert(surfaceData.Stride == _surfaceInfo.Pitch); - Debug.Assert(surfaceData.Stride * surfaceData.Height == _surfaceInfo.Size); - - // Convert the pixel format used in System.Drawing to the one required by a Switch Surface. - int dataLength = surfaceData.Stride * surfaceData.Height; - - byte[] data = new byte[dataLength]; - Span<uint> dataConvert = MemoryMarshal.Cast<byte, uint>(data); + // Update the parameters that were provided. + _state.InputText = inputText != null ? inputText : _state.InputText; + _state.CursorBegin = cursorBegin.GetValueOrDefault(_state.CursorBegin); + _state.CursorEnd = cursorEnd.GetValueOrDefault(_state.CursorEnd); + _state.OverwriteMode = overwriteMode.GetValueOrDefault(_state.OverwriteMode); + _state.TypingEnabled = typingEnabled.GetValueOrDefault(_state.TypingEnabled); - Marshal.Copy(surfaceData.Scan0, data, 0, dataLength); + // Reset the cursor blink. + _state.TextBoxBlinkCounter = 0; - for (int i = 0; i < dataConvert.Length; i++) - { - dataConvert[i] = BitOperations.RotateRight(BinaryPrimitives.ReverseEndianness(dataConvert[i]), 8); + // Tell the render thread there is something new to render. + Monitor.PulseAll(_stateLock); } + } - try - { - destination.Write(position, data); - } - finally + public void UpdateCommandState(bool? acceptPressed, bool? cancelPressed, bool? controllerEnabled) + { + lock (_stateLock) { - _surface.UnlockBits(surfaceData); - } + // Update the parameters that were provided. + _state.AcceptPressed = acceptPressed.GetValueOrDefault(_state.AcceptPressed); + _state.CancelPressed = cancelPressed.GetValueOrDefault(_state.CancelPressed); + _state.ControllerEnabled = controllerEnabled.GetValueOrDefault(_state.ControllerEnabled); - return true; + // Tell the render thread there is something new to render. + Monitor.PulseAll(_stateLock); + } } - internal bool DrawTo(RenderingSurfaceInfo surfaceInfo, IVirtualMemoryManager destination, ulong position) + public void SetSurfaceInfo(RenderingSurfaceInfo surfaceInfo) { - lock (_renderLock) + lock (_stateLock) { - if (!_surfaceInfo.Equals(surfaceInfo)) - { - _surfaceInfo = surfaceInfo; - RecreateSurface(); - RecomputeConstants(); - } - - Redraw(); + _state.SurfaceInfo = surfaceInfo; - return TryCopyTo(destination, position); + // Tell the render thread there is something new to render. + Monitor.PulseAll(_stateLock); } } + internal bool DrawTo(IVirtualMemoryManager destination, ulong position) + { + return _renderer.WriteBufferToMemory(destination, position); + } + public void Dispose() { _textBoxBlinkTimedAction.RequestCancel(); + _renderAction.RequestCancel(); } } } diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs new file mode 100644 index 00000000..b059200d --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs @@ -0,0 +1,585 @@ +using Ryujinx.HLE.Ui; +using Ryujinx.Memory; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Processing; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.Fonts; +using System; +using System.Diagnostics; +using System.IO; +using System.Numerics; +using System.Reflection; +using System.Runtime.InteropServices; +using SixLabors.ImageSharp.PixelFormats; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// Base class that generates the graphics for the software keyboard applet during inline mode. + /// </summary> + internal class SoftwareKeyboardRendererBase + { + public const int TextBoxBlinkThreshold = 8; + + const string MessageText = "Please use the keyboard to input text"; + const string AcceptText = "Accept"; + const string CancelText = "Cancel"; + const string ControllerToggleText = "Toggle input"; + + private readonly object _bufferLock = new object(); + + private RenderingSurfaceInfo _surfaceInfo = null; + private Image<Argb32> _surface = null; + private byte[] _bufferData = null; + + private Image _ryujinxLogo = null; + private Image _padAcceptIcon = null; + private Image _padCancelIcon = null; + private Image _keyModeIcon = null; + + private float _textBoxOutlineWidth; + private float _padPressedPenWidth; + + private Color _textNormalColor; + private Color _textSelectedColor; + private Color _textOverCursorColor; + + private IBrush _panelBrush; + private IBrush _disabledBrush; + private IBrush _cursorBrush; + private IBrush _selectionBoxBrush; + + private Pen _textBoxOutlinePen; + private Pen _cursorPen; + private Pen _selectionBoxPen; + private Pen _padPressedPen; + + private int _inputTextFontSize; + private Font _messageFont; + private Font _inputTextFont; + private Font _labelsTextFont; + + private RectangleF _panelRectangle; + private Point _logoPosition; + private float _messagePositionY; + + public SoftwareKeyboardRendererBase(IHostUiTheme uiTheme) + { + string ryujinxLogoPath = "Ryujinx.Ui.Resources.Logo_Ryujinx.png"; + int ryujinxLogoSize = 32; + + _ryujinxLogo = LoadResource(Assembly.GetEntryAssembly(), ryujinxLogoPath, ryujinxLogoSize, ryujinxLogoSize); + + string padAcceptIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnA.png"; + string padCancelIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnB.png"; + string keyModeIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_KeyF6.png"; + + _padAcceptIcon = LoadResource(Assembly.GetExecutingAssembly(), padAcceptIconPath , 0, 0); + _padCancelIcon = LoadResource(Assembly.GetExecutingAssembly(), padCancelIconPath , 0, 0); + _keyModeIcon = LoadResource(Assembly.GetExecutingAssembly(), keyModeIconPath , 0, 0); + + Color panelColor = ToColor(uiTheme.DefaultBackgroundColor, 255); + Color panelTransparentColor = ToColor(uiTheme.DefaultBackgroundColor, 150); + Color borderColor = ToColor(uiTheme.DefaultBorderColor); + Color selectionBackgroundColor = ToColor(uiTheme.SelectionBackgroundColor); + + _textNormalColor = ToColor(uiTheme.DefaultForegroundColor); + _textSelectedColor = ToColor(uiTheme.SelectionForegroundColor); + _textOverCursorColor = ToColor(uiTheme.DefaultForegroundColor, null, true); + + float cursorWidth = 2; + + _textBoxOutlineWidth = 2; + _padPressedPenWidth = 2; + + _panelBrush = new SolidBrush(panelColor); + _disabledBrush = new SolidBrush(panelTransparentColor); + _cursorBrush = new SolidBrush(_textNormalColor); + _selectionBoxBrush = new SolidBrush(selectionBackgroundColor); + + _textBoxOutlinePen = new Pen(borderColor, _textBoxOutlineWidth); + _cursorPen = new Pen(_textNormalColor, cursorWidth); + _selectionBoxPen = new Pen(selectionBackgroundColor, cursorWidth); + _padPressedPen = new Pen(borderColor, _padPressedPenWidth); + + _inputTextFontSize = 20; + + CreateFonts(uiTheme.FontFamily); + } + + private void CreateFonts(string uiThemeFontFamily) + { + // Try a list of fonts in case any of them is not available in the system. + + string[] availableFonts = new string[] + { + uiThemeFontFamily, + "Liberation Sans", + "FreeSans", + "DejaVu Sans" + }; + + foreach (string fontFamily in availableFonts) + { + try + { + _messageFont = SystemFonts.CreateFont(fontFamily, 26, FontStyle.Regular); + _inputTextFont = SystemFonts.CreateFont(fontFamily, _inputTextFontSize, FontStyle.Regular); + _labelsTextFont = SystemFonts.CreateFont(fontFamily, 24, FontStyle.Regular); + + return; + } + catch + { + } + } + + throw new Exception($"None of these fonts were found in the system: {String.Join(", ", availableFonts)}!"); + } + + private Color ToColor(ThemeColor color, byte? overrideAlpha = null, bool flipRgb = false) + { + var a = (byte)(color.A * 255); + var r = (byte)(color.R * 255); + var g = (byte)(color.G * 255); + var b = (byte)(color.B * 255); + + if (flipRgb) + { + r = (byte)(255 - r); + g = (byte)(255 - g); + b = (byte)(255 - b); + } + + return Color.FromRgba(r, g, b, overrideAlpha.GetValueOrDefault(a)); + } + + private Image LoadResource(Assembly assembly, string resourcePath, int newWidth, int newHeight) + { + Stream resourceStream = assembly.GetManifestResourceStream(resourcePath); + + Debug.Assert(resourceStream != null); + + var image = Image.Load(resourceStream); + + if (newHeight != 0 && newWidth != 0) + { + image.Mutate(x => x.Resize(newWidth, newHeight, KnownResamplers.Lanczos3)); + } + + return image; + } + + private void SetGraphicsOptions(IImageProcessingContext context) + { + context.GetGraphicsOptions().Antialias = true; + context.GetShapeGraphicsOptions().GraphicsOptions.Antialias = true; + } + + private void DrawImmutableElements() + { + if (_surface == null) + { + return; + } + + _surface.Mutate(context => + { + SetGraphicsOptions(context); + + context.Clear(Color.Transparent); + context.Fill(_panelBrush, _panelRectangle); + context.DrawImage(_ryujinxLogo, _logoPosition, 1); + + float halfWidth = _panelRectangle.Width / 2; + float buttonsY = _panelRectangle.Y + 185; + + PointF disableButtonPosition = new PointF(halfWidth + 180, buttonsY); + + DrawControllerToggle(context, disableButtonPosition); + }); + } + + public void DrawMutableElements(SoftwareKeyboardUiState state) + { + if (_surface == null) + { + return; + } + + _surface.Mutate(context => + { + var messageRectangle = MeasureString(MessageText, _messageFont); + float messagePositionX = (_panelRectangle.Width - messageRectangle.Width) / 2 - messageRectangle.X; + float messagePositionY = _messagePositionY - messageRectangle.Y; + var messagePosition = new PointF(messagePositionX, messagePositionY); + var messageBoundRectangle = new RectangleF(messagePositionX, messagePositionY, messageRectangle.Width, messageRectangle.Height); + + SetGraphicsOptions(context); + + context.Fill(_panelBrush, messageBoundRectangle); + + context.DrawText(MessageText, _messageFont, _textNormalColor, messagePosition); + + if (!state.TypingEnabled) + { + // Just draw a semi-transparent rectangle on top to fade the component with the background. + // TODO (caian): This will not work if one decides to add make background semi-transparent as well. + + context.Fill(_disabledBrush, messageBoundRectangle); + } + + DrawTextBox(context, state); + + float halfWidth = _panelRectangle.Width / 2; + float buttonsY = _panelRectangle.Y + 185; + + PointF acceptButtonPosition = new PointF(halfWidth - 180, buttonsY); + PointF cancelButtonPosition = new PointF(halfWidth , buttonsY); + PointF disableButtonPosition = new PointF(halfWidth + 180, buttonsY); + + DrawPadButton(context, acceptButtonPosition, _padAcceptIcon, AcceptText, state.AcceptPressed, state.ControllerEnabled); + DrawPadButton(context, cancelButtonPosition, _padCancelIcon, CancelText, state.CancelPressed, state.ControllerEnabled); + }); + } + + public void CreateSurface(RenderingSurfaceInfo surfaceInfo) + { + if (_surfaceInfo != null) + { + return; + } + + _surfaceInfo = surfaceInfo; + + Debug.Assert(_surfaceInfo.ColorFormat == Services.SurfaceFlinger.ColorFormat.A8B8G8R8); + + // Use the whole area of the image to draw, even the alignment, otherwise it may shear the final + // image if the pitch is different. + uint totalWidth = _surfaceInfo.Pitch / 4; + uint totalHeight = _surfaceInfo.Size / _surfaceInfo.Pitch; + + Debug.Assert(_surfaceInfo.Width <= totalWidth); + Debug.Assert(_surfaceInfo.Height <= totalHeight); + Debug.Assert(_surfaceInfo.Pitch * _surfaceInfo.Height <= _surfaceInfo.Size); + + _surface = new Image<Argb32>((int)totalWidth, (int)totalHeight); + + ComputeConstants(); + DrawImmutableElements(); + } + + private void ComputeConstants() + { + int totalWidth = (int)_surfaceInfo.Width; + int totalHeight = (int)_surfaceInfo.Height; + + int panelHeight = 240; + int panelPositionY = totalHeight - panelHeight; + + _panelRectangle = new RectangleF(0, panelPositionY, totalWidth, panelHeight); + + _messagePositionY = panelPositionY + 60; + + int logoPositionX = (totalWidth - _ryujinxLogo.Width) / 2; + int logoPositionY = panelPositionY + 18; + + _logoPosition = new Point(logoPositionX, logoPositionY); + } + + private RectangleF MeasureString(string text, Font font) + { + RendererOptions options = new RendererOptions(font); + FontRectangle rectangle = TextMeasurer.Measure(text == "" ? " " : text, options); + + if (text == "") + { + return new RectangleF(0, rectangle.Y, 0, rectangle.Height); + } + else + { + return new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + } + } + + private void DrawTextBox(IImageProcessingContext context, SoftwareKeyboardUiState state) + { + var inputTextRectangle = MeasureString(state.InputText, _inputTextFont); + + float boxWidth = (int)(Math.Max(300, inputTextRectangle.Width + inputTextRectangle.X + 8)); + float boxHeight = 32; + float boxY = _panelRectangle.Y + 110; + float boxX = (int)((_panelRectangle.Width - boxWidth) / 2); + + RectangleF boxRectangle = new RectangleF(boxX, boxY, boxWidth, boxHeight); + + RectangleF boundRectangle = new RectangleF(_panelRectangle.X, boxY - _textBoxOutlineWidth, + _panelRectangle.Width, boxHeight + 2 * _textBoxOutlineWidth); + + context.Fill(_panelBrush, boundRectangle); + + context.Draw(_textBoxOutlinePen, boxRectangle); + + float inputTextX = (_panelRectangle.Width - inputTextRectangle.Width) / 2 - inputTextRectangle.X; + float inputTextY = boxY + 5; + + var inputTextPosition = new PointF(inputTextX, inputTextY); + + context.DrawText(state.InputText, _inputTextFont, _textNormalColor, inputTextPosition); + + // Draw the cursor on top of the text and redraw the text with a different color if necessary. + + Color cursorTextColor; + IBrush cursorBrush; + Pen cursorPen; + + float cursorPositionYTop = inputTextY + 1; + float cursorPositionYBottom = cursorPositionYTop + _inputTextFontSize + 1; + float cursorPositionXLeft; + float cursorPositionXRight; + + bool cursorVisible = false; + + if (state.CursorBegin != state.CursorEnd) + { + Debug.Assert(state.InputText.Length > 0); + + cursorTextColor = _textSelectedColor; + cursorBrush = _selectionBoxBrush; + cursorPen = _selectionBoxPen; + + string textUntilBegin = state.InputText.Substring(0, state.CursorBegin); + string textUntilEnd = state.InputText.Substring(0, state.CursorEnd); + + var selectionBeginRectangle = MeasureString(textUntilBegin, _inputTextFont); + var selectionEndRectangle = MeasureString(textUntilEnd , _inputTextFont); + + cursorVisible = true; + cursorPositionXLeft = inputTextX + selectionBeginRectangle.Width + selectionBeginRectangle.X; + cursorPositionXRight = inputTextX + selectionEndRectangle.Width + selectionEndRectangle.X; + } + else + { + cursorTextColor = _textOverCursorColor; + cursorBrush = _cursorBrush; + cursorPen = _cursorPen; + + if (state.TextBoxBlinkCounter < TextBoxBlinkThreshold) + { + // Show the blinking cursor. + + int cursorBegin = Math.Min(state.InputText.Length, state.CursorBegin); + string textUntilCursor = state.InputText.Substring(0, cursorBegin); + var cursorTextRectangle = MeasureString(textUntilCursor, _inputTextFont); + + cursorVisible = true; + cursorPositionXLeft = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.X; + + if (state.OverwriteMode) + { + // The blinking cursor is in overwrite mode so it takes the size of a character. + + if (state.CursorBegin < state.InputText.Length) + { + textUntilCursor = state.InputText.Substring(0, cursorBegin + 1); + cursorTextRectangle = MeasureString(textUntilCursor, _inputTextFont); + cursorPositionXRight = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.X; + } + else + { + cursorPositionXRight = cursorPositionXLeft + _inputTextFontSize / 2; + } + } + else + { + // The blinking cursor is in insert mode so it is only a line. + cursorPositionXRight = cursorPositionXLeft; + } + } + else + { + cursorPositionXLeft = inputTextX; + cursorPositionXRight = inputTextX; + } + } + + if (state.TypingEnabled && cursorVisible) + { + float cursorWidth = cursorPositionXRight - cursorPositionXLeft; + float cursorHeight = cursorPositionYBottom - cursorPositionYTop; + + if (cursorWidth == 0) + { + PointF[] points = new PointF[] + { + new PointF(cursorPositionXLeft, cursorPositionYTop), + new PointF(cursorPositionXLeft, cursorPositionYBottom), + }; + + context.DrawLines(cursorPen, points); + } + else + { + var cursorRectangle = new RectangleF(cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight); + + context.Draw(cursorPen , cursorRectangle); + context.Fill(cursorBrush, cursorRectangle); + + Image<Argb32> textOverCursor = new Image<Argb32>((int)cursorRectangle.Width, (int)cursorRectangle.Height); + textOverCursor.Mutate(context => + { + var textRelativePosition = new PointF(inputTextPosition.X - cursorRectangle.X, inputTextPosition.Y - cursorRectangle.Y); + context.DrawText(state.InputText, _inputTextFont, cursorTextColor, textRelativePosition); + }); + + var cursorPosition = new Point((int)cursorRectangle.X, (int)cursorRectangle.Y); + context.DrawImage(textOverCursor, cursorPosition, 1); + } + } + else if (!state.TypingEnabled) + { + // Just draw a semi-transparent rectangle on top to fade the component with the background. + // TODO (caian): This will not work if one decides to add make background semi-transparent as well. + + context.Fill(_disabledBrush, boundRectangle); + } + } + + private void DrawPadButton(IImageProcessingContext context, PointF point, Image icon, string label, bool pressed, bool enabled) + { + // Use relative positions so we can center the the entire drawing later. + + float iconX = 0; + float iconY = 0; + float iconWidth = icon.Width; + float iconHeight = icon.Height; + + var labelRectangle = MeasureString(label, _labelsTextFont); + + float labelPositionX = iconWidth + 8 - labelRectangle.X; + float labelPositionY = 3; + + float fullWidth = labelPositionX + labelRectangle.Width + labelRectangle.X; + float fullHeight = iconHeight; + + // Convert all relative positions into absolute. + + float originX = (int)(point.X - fullWidth / 2); + float originY = (int)(point.Y - fullHeight / 2); + + iconX += originX; + iconY += originY; + + var iconPosition = new Point((int)iconX, (int)iconY); + var labelPosition = new PointF(labelPositionX + originX, labelPositionY + originY); + + var selectedRectangle = new RectangleF(originX - 2 * _padPressedPenWidth, originY - 2 * _padPressedPenWidth, + fullWidth + 4 * _padPressedPenWidth, fullHeight + 4 * _padPressedPenWidth); + + var boundRectangle = new RectangleF(originX, originY, fullWidth, fullHeight); + boundRectangle.Inflate(4 * _padPressedPenWidth, 4 * _padPressedPenWidth); + + context.Fill(_panelBrush, boundRectangle); + context.DrawImage(icon, iconPosition, 1); + context.DrawText(label, _labelsTextFont, _textNormalColor, labelPosition); + + if (enabled) + { + if (pressed) + { + context.Draw(_padPressedPen, selectedRectangle); + } + } + else + { + // Just draw a semi-transparent rectangle on top to fade the component with the background. + // TODO (caian): This will not work if one decides to add make background semi-transparent as well. + + context.Fill(_disabledBrush, boundRectangle); + } + } + + private void DrawControllerToggle(IImageProcessingContext context, PointF point) + { + var labelRectangle = MeasureString(ControllerToggleText, _labelsTextFont); + + // Use relative positions so we can center the the entire drawing later. + + float keyWidth = _keyModeIcon.Width; + float keyHeight = _keyModeIcon.Height; + + float labelPositionX = keyWidth + 8 - labelRectangle.X; + float labelPositionY = -labelRectangle.Y - 1; + + float keyX = 0; + float keyY = (int)((labelPositionY + labelRectangle.Height - keyHeight) / 2); + + float fullWidth = labelPositionX + labelRectangle.Width; + float fullHeight = Math.Max(labelPositionY + labelRectangle.Height, keyHeight); + + // Convert all relative positions into absolute. + + float originX = (int)(point.X - fullWidth / 2); + float originY = (int)(point.Y - fullHeight / 2); + + keyX += originX; + keyY += originY; + + var labelPosition = new PointF(labelPositionX + originX, labelPositionY + originY); + var overlayPosition = new Point((int)keyX, (int)keyY); + + context.DrawImage(_keyModeIcon, overlayPosition, 1); + context.DrawText(ControllerToggleText, _labelsTextFont, _textNormalColor, labelPosition); + } + + public void CopyImageToBuffer() + { + lock (_bufferLock) + { + if (_surface == null) + { + return; + } + + // Convert the pixel format used in the image to the one used in the Switch surface. + + if (!_surface.TryGetSinglePixelSpan(out Span<Argb32> pixels)) + { + return; + } + + _bufferData = MemoryMarshal.AsBytes(pixels).ToArray(); + Span<uint> dataConvert = MemoryMarshal.Cast<byte, uint>(_bufferData); + + Debug.Assert(_bufferData.Length == _surfaceInfo.Size); + + for (int i = 0; i < dataConvert.Length; i++) + { + dataConvert[i] = BitOperations.RotateRight(dataConvert[i], 8); + } + } + } + + public bool WriteBufferToMemory(IVirtualMemoryManager destination, ulong position) + { + lock (_bufferLock) + { + if (_bufferData == null) + { + return false; + } + + try + { + destination.Write(position, _bufferData); + } + catch + { + return false; + } + + return true; + } + } + } +} diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiState.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiState.cs new file mode 100644 index 00000000..e6131e62 --- /dev/null +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardUiState.cs @@ -0,0 +1,22 @@ +using Ryujinx.HLE.Ui; + +namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard +{ + /// <summary> + /// TODO + /// </summary> + internal class SoftwareKeyboardUiState + { + public string InputText = ""; + public int CursorBegin = 0; + public int CursorEnd = 0; + public bool AcceptPressed = false; + public bool CancelPressed = false; + public bool OverwriteMode = false; + public bool TypingEnabled = true; + public bool ControllerEnabled = true; + public int TextBoxBlinkCounter = 0; + + public RenderingSurfaceInfo SurfaceInfo = null; + } +} diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/TimedAction.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/TimedAction.cs index 8884bdcf..0de78a0e 100644 --- a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/TimedAction.cs +++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/TimedAction.cs @@ -144,6 +144,20 @@ namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard }), cancelled); } + public void Reset(Action action) + { + // Create a dedicated cancel token for each task. + var cancelled = new TRef<bool>(false); + + Reset(new Thread(() => + { + while (!Volatile.Read(ref cancelled.Value)) + { + action(); + } + }), cancelled); + } + private static bool SleepWithSubstep(SleepSubstepData substepData, TRef<bool> cancelled) { for (int i = 0; i < substepData.SleepCount; i++) |
