From 380b95bc59e7dc419f89df951cdc086e792cb0ff Mon Sep 17 00:00:00 2001
From: Caian Benedicto <caianbene@gmail.com>
Date: Tue, 12 Oct 2021 16:54:21 -0300
Subject: 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
---
 .../SoftwareKeyboard/SoftwareKeyboardRenderer.cs   | 717 +++++++++++++++++++++
 1 file changed, 717 insertions(+)
 create mode 100644 Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs

(limited to 'Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs')

diff --git a/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs
new file mode 100644
index 00000000..c16b861e
--- /dev/null
+++ b/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs
@@ -0,0 +1,717 @@
+using Ryujinx.HLE.Ui;
+using Ryujinx.Memory;
+using System;
+using System.Diagnostics;
+using System.Drawing;
+using System.Drawing.Drawing2D;
+using System.Drawing.Imaging;
+using System.Drawing.Text;
+using System.IO;
+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.
+    /// </summary>
+    internal class SoftwareKeyboardRenderer : IDisposable
+    {
+        const int TextBoxBlinkThreshold            = 8;
+        const int TextBoxBlinkSleepMilliseconds    = 100;
+        const int TextBoxBlinkJoinWaitMilliseconds = 1000;
+
+        const string MessageText          = "Please use the keyboard to input text";
+        const string AcceptText           = "Accept";
+        const string CancelText           = "Cancel";
+        const string ControllerToggleText = "Toggle input";
+
+        private RenderingSurfaceInfo _surfaceInfo;
+        private Bitmap               _surface    = null;
+        private object               _renderLock = new object();
+
+        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();
+
+        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);
+
+            _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);
+        }
+
+        private static void StartTextBoxBlinker(TimedAction timedAction, TRef<int> blinkerCounter)
+        {
+            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)
+                {
+                    // 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;
+
+            _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;
+        }
+
+        private float CalibrateTextHeight(Font font)
+        {
+            // 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);
+
+            var surfaceSize = (int)Math.Ceiling(2 * font.Size);
+
+            string calibrationText = "|";
+
+            using (var surface = new Bitmap(surfaceSize, surfaceSize, PixelFormat.Format32bppArgb))
+            using (var graphics = CreateGraphics(surface))
+            {
+                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++)
+                {
+                    for (int x = 0; x < surfaceData.Stride; x += 4)
+                    {
+                        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 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;
+
+            var inputTextPosition = new PointF(inputTextX, inputTextY);
+
+            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)
+                    {
+                        // 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;
+                        }
+                    }
+                    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)
+                {
+                    graphics.DrawLine(cursorPen, cursorPositionXLeft, cursorPositionYTop, cursorPositionXLeft, cursorPositionYBottom);
+                }
+                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;
+                }
+            }
+            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)
+        {
+            // 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 (pressed)
+                {
+                    graphics.DrawPath(_padPressedPen, frame);
+                }
+            }
+            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);
+        }
+
+        private unsafe bool TryCopyTo(IVirtualMemoryManager destination, ulong position)
+        {
+            if (_surface == null)
+            {
+                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* dataPointer = (byte*)surfaceData.Scan0;
+            byte* dataEnd     = dataPointer + dataLength;
+
+            for (; dataPointer < dataEnd; dataPointer += 4)
+            {
+                *(uint*)dataPointer = (uint)(
+                     (*(dataPointer + 0) << 16) |
+                     (*(dataPointer + 1) << 8 ) |
+                     (*(dataPointer + 2) << 0 ) |
+                     (*(dataPointer + 3) << 24));
+            }
+
+            try
+            {
+                Span<byte> dataSpan = new Span<byte>((void*)surfaceData.Scan0, dataLength);
+                destination.Write(position, dataSpan);
+            }
+            finally
+            {
+                _surface.UnlockBits(surfaceData);
+            }
+
+            return true;
+        }
+
+        internal bool DrawTo(RenderingSurfaceInfo surfaceInfo, IVirtualMemoryManager destination, ulong position)
+        {
+            lock (_renderLock)
+            {
+                if (!_surfaceInfo.Equals(surfaceInfo))
+                {
+                    _surfaceInfo = surfaceInfo;
+                    RecreateSurface();
+                    RecomputeConstants();
+                }
+
+                Redraw();
+
+                return TryCopyTo(destination, position);
+            }
+        }
+
+        public void Dispose()
+        {
+            _textBoxBlinkTimedAction.RequestCancel();
+        }
+    }
+}
-- 
cgit v1.2.3-70-g09d2