diff options
Diffstat (limited to 'src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs')
-rw-r--r-- | src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs | 606 |
1 files changed, 606 insertions, 0 deletions
diff --git a/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs new file mode 100644 index 00000000..9a91fa32 --- /dev/null +++ b/src/Ryujinx.HLE/HOS/Applets/SoftwareKeyboard/SoftwareKeyboardRendererBase.cs @@ -0,0 +1,606 @@ +using Ryujinx.HLE.Ui; +using Ryujinx.Memory; +using SixLabors.Fonts; +using SixLabors.ImageSharp; +using SixLabors.ImageSharp.Drawing.Processing; +using SixLabors.ImageSharp.PixelFormats; +using SixLabors.ImageSharp.Processing; +using System; +using System.Diagnostics; +using System.IO; +using System.Numerics; +using System.Reflection; +using System.Runtime.InteropServices; + +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) + { + int ryujinxLogoSize = 32; + + string ryujinxIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Logo_Ryujinx.png"; + _ryujinxLogo = LoadResource(Assembly.GetExecutingAssembly(), ryujinxIconPath, 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", + "Lucida Grande" + }; + + 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); + + return LoadResource(resourceStream, newWidth, newHeight); + } + + private Image LoadResource(Stream resourceStream, int newWidth, int newHeight) + { + 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 static RectangleF MeasureString(string text, Font font) + { + RendererOptions options = new RendererOptions(font); + + if (text == "") + { + FontRectangle emptyRectangle = TextMeasurer.Measure(" ", options); + + return new RectangleF(0, emptyRectangle.Y, 0, emptyRectangle.Height); + } + + FontRectangle rectangle = TextMeasurer.Measure(text, options); + + return new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height); + } + + private static RectangleF MeasureString(ReadOnlySpan<char> text, Font font) + { + RendererOptions options = new RendererOptions(font); + + if (text == "") + { + FontRectangle emptyRectangle = TextMeasurer.Measure(" ", options); + return new RectangleF(0, emptyRectangle.Y, 0, emptyRectangle.Height); + } + + FontRectangle rectangle = TextMeasurer.Measure(text, options); + + 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; + + ReadOnlySpan<char> textUntilBegin = state.InputText.AsSpan(0, state.CursorBegin); + ReadOnlySpan<char> textUntilEnd = state.InputText.AsSpan(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); + ReadOnlySpan<char> textUntilCursor = state.InputText.AsSpan(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.AsSpan(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; + } + } + } +} |