aboutsummaryrefslogtreecommitdiff
path: root/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs
diff options
context:
space:
mode:
authorCaian Benedicto <caianbene@gmail.com>2021-10-12 16:54:21 -0300
committerGitHub <noreply@github.com>2021-10-12 21:54:21 +0200
commit380b95bc59e7dc419f89df951cdc086e792cb0ff (patch)
tree59a636b48db991d8e13132d7d3f41464d9b04b24 /Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs
parent69093cf2d69490862aff974f170cee63a0016fd0 (diff)
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
Diffstat (limited to 'Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs')
-rw-r--r--Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRenderer.cs717
1 files changed, 717 insertions, 0 deletions
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();
+ }
+ }
+}