using Ryujinx.HLE.UI;
using Ryujinx.Memory;
using SkiaSharp;
using System;
using System.Diagnostics;
using System.IO;
using System.Reflection;
using System.Runtime.InteropServices;
namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
{
///
/// Base class that generates the graphics for the software keyboard applet during inline mode.
///
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();
private RenderingSurfaceInfo _surfaceInfo = null;
private SKImageInfo _imageInfo;
private SKSurface _surface = null;
private byte[] _bufferData = null;
private readonly SKBitmap _ryujinxLogo = null;
private readonly SKBitmap _padAcceptIcon = null;
private readonly SKBitmap _padCancelIcon = null;
private readonly SKBitmap _keyModeIcon = null;
private readonly float _textBoxOutlineWidth;
private readonly float _padPressedPenWidth;
private readonly SKColor _textNormalColor;
private readonly SKColor _textSelectedColor;
private readonly SKColor _textOverCursorColor;
private readonly SKPaint _panelBrush;
private readonly SKPaint _disabledBrush;
private readonly SKPaint _cursorBrush;
private readonly SKPaint _selectionBoxBrush;
private readonly SKPaint _textBoxOutlinePen;
private readonly SKPaint _cursorPen;
private readonly SKPaint _selectionBoxPen;
private readonly SKPaint _padPressedPen;
private readonly int _inputTextFontSize;
private SKFont _messageFont;
private SKFont _inputTextFont;
private SKFont _labelsTextFont;
private SKRect _panelRectangle;
private SKPoint _logoPosition;
private float _messagePositionY;
public SoftwareKeyboardRendererBase(IHostUITheme uiTheme)
{
int ryujinxLogoSize = 32;
string ryujinxIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Logo_Ryujinx.png";
_ryujinxLogo = LoadResource(typeof(SoftwareKeyboardRendererBase).Assembly, 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(typeof(SoftwareKeyboardRendererBase).Assembly, padAcceptIconPath, 0, 0);
_padCancelIcon = LoadResource(typeof(SoftwareKeyboardRendererBase).Assembly, padCancelIconPath, 0, 0);
_keyModeIcon = LoadResource(typeof(SoftwareKeyboardRendererBase).Assembly, keyModeIconPath, 0, 0);
var panelColor = ToColor(uiTheme.DefaultBackgroundColor, 255);
var panelTransparentColor = ToColor(uiTheme.DefaultBackgroundColor, 150);
var borderColor = ToColor(uiTheme.DefaultBorderColor);
var 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 SKPaint()
{
Color = panelColor,
IsAntialias = true
};
_disabledBrush = new SKPaint()
{
Color = panelTransparentColor,
IsAntialias = true
};
_cursorBrush = new SKPaint() { Color = _textNormalColor, IsAntialias = true };
_selectionBoxBrush = new SKPaint() { Color = selectionBackgroundColor, IsAntialias = true };
_textBoxOutlinePen = new SKPaint()
{
Color = borderColor,
StrokeWidth = _textBoxOutlineWidth,
IsStroke = true,
IsAntialias = true
};
_cursorPen = new SKPaint() { Color = _textNormalColor, StrokeWidth = cursorWidth, IsStroke = true, IsAntialias = true };
_selectionBoxPen = new SKPaint() { Color = selectionBackgroundColor, StrokeWidth = cursorWidth, IsStroke = true, IsAntialias = true };
_padPressedPen = new SKPaint() { Color = borderColor, StrokeWidth = _padPressedPenWidth, IsStroke = true, IsAntialias = true };
_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 = {
uiThemeFontFamily,
"Liberation Sans",
"FreeSans",
"DejaVu Sans",
"Lucida Grande",
};
foreach (string fontFamily in availableFonts)
{
try
{
using var typeface = SKTypeface.FromFamilyName(fontFamily, SKFontStyle.Normal);
_messageFont = new SKFont(typeface, 26);
_inputTextFont = new SKFont(typeface, _inputTextFontSize);
_labelsTextFont = new SKFont(typeface, 24);
return;
}
catch
{
}
}
throw new Exception($"None of these fonts were found in the system: {String.Join(", ", availableFonts)}!");
}
private static SKColor 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 new SKColor(r, g, b, overrideAlpha.GetValueOrDefault(a));
}
private static SKBitmap LoadResource(Assembly assembly, string resourcePath, int newWidth, int newHeight)
{
Stream resourceStream = assembly.GetManifestResourceStream(resourcePath);
return LoadResource(resourceStream, newWidth, newHeight);
}
private static SKBitmap LoadResource(Stream resourceStream, int newWidth, int newHeight)
{
Debug.Assert(resourceStream != null);
var bitmap = SKBitmap.Decode(resourceStream);
if (newHeight != 0 && newWidth != 0)
{
var resized = bitmap.Resize(new SKImageInfo(newWidth, newHeight), SKFilterQuality.High);
if (resized != null)
{
bitmap.Dispose();
bitmap = resized;
}
}
return bitmap;
}
private void DrawImmutableElements()
{
if (_surface == null)
{
return;
}
var canvas = _surface.Canvas;
canvas.Clear(SKColors.Transparent);
canvas.DrawRect(_panelRectangle, _panelBrush);
canvas.DrawBitmap(_ryujinxLogo, _logoPosition);
float halfWidth = _panelRectangle.Width / 2;
float buttonsY = _panelRectangle.Top + 185;
SKPoint disableButtonPosition = new(halfWidth + 180, buttonsY);
DrawControllerToggle(canvas, disableButtonPosition);
}
public void DrawMutableElements(SoftwareKeyboardUIState state)
{
if (_surface == null)
{
return;
}
using var paint = new SKPaint(_messageFont)
{
Color = _textNormalColor,
IsAntialias = true
};
var canvas = _surface.Canvas;
var messageRectangle = MeasureString(MessageText, paint);
float messagePositionX = (_panelRectangle.Width - messageRectangle.Width) / 2 - messageRectangle.Left;
float messagePositionY = _messagePositionY - messageRectangle.Top;
var messagePosition = new SKPoint(messagePositionX, messagePositionY);
var messageBoundRectangle = SKRect.Create(messagePositionX, messagePositionY, messageRectangle.Width, messageRectangle.Height);
canvas.DrawRect(messageBoundRectangle, _panelBrush);
canvas.DrawText(MessageText, messagePosition.X, messagePosition.Y + _messageFont.Metrics.XHeight + _messageFont.Metrics.Descent, paint);
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.
canvas.DrawRect(messageBoundRectangle, _disabledBrush);
}
DrawTextBox(canvas, state);
float halfWidth = _panelRectangle.Width / 2;
float buttonsY = _panelRectangle.Top + 185;
SKPoint acceptButtonPosition = new(halfWidth - 180, buttonsY);
SKPoint cancelButtonPosition = new(halfWidth, buttonsY);
SKPoint disableButtonPosition = new(halfWidth + 180, buttonsY);
DrawPadButton(canvas, acceptButtonPosition, _padAcceptIcon, AcceptText, state.AcceptPressed, state.ControllerEnabled);
DrawPadButton(canvas, 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);
_imageInfo = new SKImageInfo((int)totalWidth, (int)totalHeight, SKColorType.Rgba8888);
_surface = SKSurface.Create(_imageInfo);
ComputeConstants();
DrawImmutableElements();
}
private void ComputeConstants()
{
int totalWidth = (int)_surfaceInfo.Width;
int totalHeight = (int)_surfaceInfo.Height;
int panelHeight = 240;
int panelPositionY = totalHeight - panelHeight;
_panelRectangle = SKRect.Create(0, panelPositionY, totalWidth, panelHeight);
_messagePositionY = panelPositionY + 60;
int logoPositionX = (totalWidth - _ryujinxLogo.Width) / 2;
int logoPositionY = panelPositionY + 18;
_logoPosition = new SKPoint(logoPositionX, logoPositionY);
}
private static SKRect MeasureString(string text, SKPaint paint)
{
SKRect bounds = SKRect.Empty;
if (text == "")
{
paint.MeasureText(" ", ref bounds);
}
else
{
paint.MeasureText(text, ref bounds);
}
return bounds;
}
private static SKRect MeasureString(ReadOnlySpan text, SKPaint paint)
{
SKRect bounds = SKRect.Empty;
if (text == "")
{
paint.MeasureText(" ", ref bounds);
}
else
{
paint.MeasureText(text, ref bounds);
}
return bounds;
}
private void DrawTextBox(SKCanvas canvas, SoftwareKeyboardUIState state)
{
using var textPaint = new SKPaint(_labelsTextFont)
{
IsAntialias = true,
Color = _textNormalColor
};
var inputTextRectangle = MeasureString(state.InputText, textPaint);
float boxWidth = (int)(Math.Max(300, inputTextRectangle.Width + inputTextRectangle.Left + 8));
float boxHeight = 32;
float boxY = _panelRectangle.Top + 110;
float boxX = (int)((_panelRectangle.Width - boxWidth) / 2);
SKRect boxRectangle = SKRect.Create(boxX, boxY, boxWidth, boxHeight);
SKRect boundRectangle = SKRect.Create(_panelRectangle.Left, boxY - _textBoxOutlineWidth,
_panelRectangle.Width, boxHeight + 2 * _textBoxOutlineWidth);
canvas.DrawRect(boundRectangle, _panelBrush);
canvas.DrawRect(boxRectangle, _textBoxOutlinePen);
float inputTextX = (_panelRectangle.Width - inputTextRectangle.Width) / 2 - inputTextRectangle.Left;
float inputTextY = boxY + 5;
var inputTextPosition = new SKPoint(inputTextX, inputTextY);
canvas.DrawText(state.InputText, inputTextPosition.X, inputTextPosition.Y + (_labelsTextFont.Metrics.XHeight + _labelsTextFont.Metrics.Descent), textPaint);
// Draw the cursor on top of the text and redraw the text with a different color if necessary.
SKColor cursorTextColor;
SKPaint cursorBrush;
SKPaint 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 textUntilBegin = state.InputText.AsSpan(0, state.CursorBegin);
ReadOnlySpan textUntilEnd = state.InputText.AsSpan(0, state.CursorEnd);
var selectionBeginRectangle = MeasureString(textUntilBegin, textPaint);
var selectionEndRectangle = MeasureString(textUntilEnd, textPaint);
cursorVisible = true;
cursorPositionXLeft = inputTextX + selectionBeginRectangle.Width + selectionBeginRectangle.Left;
cursorPositionXRight = inputTextX + selectionEndRectangle.Width + selectionEndRectangle.Left;
}
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 textUntilCursor = state.InputText.AsSpan(0, cursorBegin);
var cursorTextRectangle = MeasureString(textUntilCursor, textPaint);
cursorVisible = true;
cursorPositionXLeft = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.Left;
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, textPaint);
cursorPositionXRight = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.Left;
}
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)
{
canvas.DrawLine(new SKPoint(cursorPositionXLeft, cursorPositionYTop),
new SKPoint(cursorPositionXLeft, cursorPositionYBottom),
cursorPen);
}
else
{
var cursorRectangle = SKRect.Create(cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight);
canvas.DrawRect(cursorRectangle, cursorPen);
canvas.DrawRect(cursorRectangle, cursorBrush);
using var textOverCursor = SKSurface.Create(new SKImageInfo((int)cursorRectangle.Width, (int)cursorRectangle.Height, SKColorType.Rgba8888));
var textOverCanvas = textOverCursor.Canvas;
var textRelativePosition = new SKPoint(inputTextPosition.X - cursorRectangle.Left, inputTextPosition.Y - cursorRectangle.Top);
using var cursorPaint = new SKPaint(_inputTextFont)
{
Color = cursorTextColor,
IsAntialias = true
};
textOverCanvas.DrawText(state.InputText, textRelativePosition.X, textRelativePosition.Y + _inputTextFont.Metrics.XHeight + _inputTextFont.Metrics.Descent, cursorPaint);
var cursorPosition = new SKPoint((int)cursorRectangle.Left, (int)cursorRectangle.Top);
textOverCursor.Flush();
canvas.DrawSurface(textOverCursor, cursorPosition);
}
}
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.
canvas.DrawRect(boundRectangle, _disabledBrush);
}
}
private void DrawPadButton(SKCanvas canvas, SKPoint point, SKBitmap icon, string label, bool pressed, bool enabled)
{
// Use relative positions so we can center the entire drawing later.
float iconX = 0;
float iconY = 0;
float iconWidth = icon.Width;
float iconHeight = icon.Height;
using var paint = new SKPaint(_labelsTextFont)
{
Color = _textNormalColor,
IsAntialias = true
};
var labelRectangle = MeasureString(label, paint);
float labelPositionX = iconWidth + 8 - labelRectangle.Left;
float labelPositionY = 3;
float fullWidth = labelPositionX + labelRectangle.Width + labelRectangle.Left;
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 SKPoint((int)iconX, (int)iconY);
var labelPosition = new SKPoint(labelPositionX + originX, labelPositionY + originY);
var selectedRectangle = SKRect.Create(originX - 2 * _padPressedPenWidth, originY - 2 * _padPressedPenWidth,
fullWidth + 4 * _padPressedPenWidth, fullHeight + 4 * _padPressedPenWidth);
var boundRectangle = SKRect.Create(originX, originY, fullWidth, fullHeight);
boundRectangle.Inflate(4 * _padPressedPenWidth, 4 * _padPressedPenWidth);
canvas.DrawRect(boundRectangle, _panelBrush);
canvas.DrawBitmap(icon, iconPosition);
canvas.DrawText(label, labelPosition.X, labelPosition.Y + _labelsTextFont.Metrics.XHeight + _labelsTextFont.Metrics.Descent, paint);
if (enabled)
{
if (pressed)
{
canvas.DrawRect(selectedRectangle, _padPressedPen);
}
}
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.
canvas.DrawRect(boundRectangle, _disabledBrush);
}
}
private void DrawControllerToggle(SKCanvas canvas, SKPoint point)
{
using var paint = new SKPaint(_labelsTextFont)
{
IsAntialias = true,
Color = _textNormalColor
};
var labelRectangle = MeasureString(ControllerToggleText, paint);
// Use relative positions so we can center the entire drawing later.
float keyWidth = _keyModeIcon.Width;
float keyHeight = _keyModeIcon.Height;
float labelPositionX = keyWidth + 8 - labelRectangle.Left;
float labelPositionY = -labelRectangle.Top - 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 SKPoint(labelPositionX + originX, labelPositionY + originY);
var overlayPosition = new SKPoint((int)keyX, (int)keyY);
canvas.DrawBitmap(_keyModeIcon, overlayPosition);
canvas.DrawText(ControllerToggleText, labelPosition.X, labelPosition.Y + _labelsTextFont.Metrics.XHeight, paint);
}
public unsafe void CopyImageToBuffer()
{
lock (_bufferLock)
{
if (_surface == null)
{
return;
}
// Convert the pixel format used in the image to the one used in the Switch surface.
_surface.Flush();
var buffer = new byte[_imageInfo.BytesSize];
fixed (byte* bufferPtr = buffer)
{
if (!_surface.ReadPixels(_imageInfo, (nint)bufferPtr, _imageInfo.RowBytes, 0, 0))
{
return;
}
}
_bufferData = buffer;
Debug.Assert(buffer.Length == _surfaceInfo.Size);
}
}
public bool WriteBufferToMemory(IVirtualMemoryManager destination, ulong position)
{
lock (_bufferLock)
{
if (_bufferData == null)
{
return false;
}
try
{
destination.Write(position, _bufferData);
}
catch
{
return false;
}
return true;
}
}
}
}