SoftwareKeyboardRendererBase.cs
1 using Ryujinx.HLE.UI; 2 using Ryujinx.Memory; 3 using SkiaSharp; 4 using System; 5 using System.Diagnostics; 6 using System.IO; 7 using System.Reflection; 8 using System.Runtime.InteropServices; 9 10 namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard 11 { 12 /// <summary> 13 /// Base class that generates the graphics for the software keyboard applet during inline mode. 14 /// </summary> 15 internal class SoftwareKeyboardRendererBase 16 { 17 public const int TextBoxBlinkThreshold = 8; 18 19 const string MessageText = "Please use the keyboard to input text"; 20 const string AcceptText = "Accept"; 21 const string CancelText = "Cancel"; 22 const string ControllerToggleText = "Toggle input"; 23 24 private readonly object _bufferLock = new(); 25 26 private RenderingSurfaceInfo _surfaceInfo = null; 27 private SKImageInfo _imageInfo; 28 private SKSurface _surface = null; 29 private byte[] _bufferData = null; 30 31 private readonly SKBitmap _ryujinxLogo = null; 32 private readonly SKBitmap _padAcceptIcon = null; 33 private readonly SKBitmap _padCancelIcon = null; 34 private readonly SKBitmap _keyModeIcon = null; 35 36 private readonly float _textBoxOutlineWidth; 37 private readonly float _padPressedPenWidth; 38 39 private readonly SKColor _textNormalColor; 40 private readonly SKColor _textSelectedColor; 41 private readonly SKColor _textOverCursorColor; 42 43 private readonly SKPaint _panelBrush; 44 private readonly SKPaint _disabledBrush; 45 private readonly SKPaint _cursorBrush; 46 private readonly SKPaint _selectionBoxBrush; 47 48 private readonly SKPaint _textBoxOutlinePen; 49 private readonly SKPaint _cursorPen; 50 private readonly SKPaint _selectionBoxPen; 51 private readonly SKPaint _padPressedPen; 52 53 private readonly int _inputTextFontSize; 54 private SKFont _messageFont; 55 private SKFont _inputTextFont; 56 private SKFont _labelsTextFont; 57 58 private SKRect _panelRectangle; 59 private SKPoint _logoPosition; 60 private float _messagePositionY; 61 62 public SoftwareKeyboardRendererBase(IHostUITheme uiTheme) 63 { 64 int ryujinxLogoSize = 32; 65 66 string ryujinxIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Logo_Ryujinx.png"; 67 _ryujinxLogo = LoadResource(typeof(SoftwareKeyboardRendererBase).Assembly, ryujinxIconPath, ryujinxLogoSize, ryujinxLogoSize); 68 69 string padAcceptIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnA.png"; 70 string padCancelIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnB.png"; 71 string keyModeIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_KeyF6.png"; 72 73 _padAcceptIcon = LoadResource(typeof(SoftwareKeyboardRendererBase).Assembly, padAcceptIconPath, 0, 0); 74 _padCancelIcon = LoadResource(typeof(SoftwareKeyboardRendererBase).Assembly, padCancelIconPath, 0, 0); 75 _keyModeIcon = LoadResource(typeof(SoftwareKeyboardRendererBase).Assembly, keyModeIconPath, 0, 0); 76 77 var panelColor = ToColor(uiTheme.DefaultBackgroundColor, 255); 78 var panelTransparentColor = ToColor(uiTheme.DefaultBackgroundColor, 150); 79 var borderColor = ToColor(uiTheme.DefaultBorderColor); 80 var selectionBackgroundColor = ToColor(uiTheme.SelectionBackgroundColor); 81 82 _textNormalColor = ToColor(uiTheme.DefaultForegroundColor); 83 _textSelectedColor = ToColor(uiTheme.SelectionForegroundColor); 84 _textOverCursorColor = ToColor(uiTheme.DefaultForegroundColor, null, true); 85 86 float cursorWidth = 2; 87 88 _textBoxOutlineWidth = 2; 89 _padPressedPenWidth = 2; 90 91 _panelBrush = new SKPaint() 92 { 93 Color = panelColor, 94 IsAntialias = true 95 }; 96 _disabledBrush = new SKPaint() 97 { 98 Color = panelTransparentColor, 99 IsAntialias = true 100 }; 101 _cursorBrush = new SKPaint() { Color = _textNormalColor, IsAntialias = true }; 102 _selectionBoxBrush = new SKPaint() { Color = selectionBackgroundColor, IsAntialias = true }; 103 104 _textBoxOutlinePen = new SKPaint() 105 { 106 Color = borderColor, 107 StrokeWidth = _textBoxOutlineWidth, 108 IsStroke = true, 109 IsAntialias = true 110 }; 111 _cursorPen = new SKPaint() { Color = _textNormalColor, StrokeWidth = cursorWidth, IsStroke = true, IsAntialias = true }; 112 _selectionBoxPen = new SKPaint() { Color = selectionBackgroundColor, StrokeWidth = cursorWidth, IsStroke = true, IsAntialias = true }; 113 _padPressedPen = new SKPaint() { Color = borderColor, StrokeWidth = _padPressedPenWidth, IsStroke = true, IsAntialias = true }; 114 115 _inputTextFontSize = 20; 116 117 CreateFonts(uiTheme.FontFamily); 118 } 119 120 private void CreateFonts(string uiThemeFontFamily) 121 { 122 // Try a list of fonts in case any of them is not available in the system. 123 124 string[] availableFonts = { 125 uiThemeFontFamily, 126 "Liberation Sans", 127 "FreeSans", 128 "DejaVu Sans", 129 "Lucida Grande", 130 }; 131 132 foreach (string fontFamily in availableFonts) 133 { 134 try 135 { 136 using var typeface = SKTypeface.FromFamilyName(fontFamily, SKFontStyle.Normal); 137 _messageFont = new SKFont(typeface, 26); 138 _inputTextFont = new SKFont(typeface, _inputTextFontSize); 139 _labelsTextFont = new SKFont(typeface, 24); 140 141 return; 142 } 143 catch 144 { 145 } 146 } 147 148 throw new Exception($"None of these fonts were found in the system: {String.Join(", ", availableFonts)}!"); 149 } 150 151 private static SKColor ToColor(ThemeColor color, byte? overrideAlpha = null, bool flipRgb = false) 152 { 153 var a = (byte)(color.A * 255); 154 var r = (byte)(color.R * 255); 155 var g = (byte)(color.G * 255); 156 var b = (byte)(color.B * 255); 157 158 if (flipRgb) 159 { 160 r = (byte)(255 - r); 161 g = (byte)(255 - g); 162 b = (byte)(255 - b); 163 } 164 165 return new SKColor(r, g, b, overrideAlpha.GetValueOrDefault(a)); 166 } 167 168 private static SKBitmap LoadResource(Assembly assembly, string resourcePath, int newWidth, int newHeight) 169 { 170 Stream resourceStream = assembly.GetManifestResourceStream(resourcePath); 171 172 return LoadResource(resourceStream, newWidth, newHeight); 173 } 174 175 private static SKBitmap LoadResource(Stream resourceStream, int newWidth, int newHeight) 176 { 177 Debug.Assert(resourceStream != null); 178 179 var bitmap = SKBitmap.Decode(resourceStream); 180 181 if (newHeight != 0 && newWidth != 0) 182 { 183 var resized = bitmap.Resize(new SKImageInfo(newWidth, newHeight), SKFilterQuality.High); 184 if (resized != null) 185 { 186 bitmap.Dispose(); 187 bitmap = resized; 188 } 189 } 190 191 return bitmap; 192 } 193 194 private void DrawImmutableElements() 195 { 196 if (_surface == null) 197 { 198 return; 199 } 200 var canvas = _surface.Canvas; 201 202 canvas.Clear(SKColors.Transparent); 203 canvas.DrawRect(_panelRectangle, _panelBrush); 204 canvas.DrawBitmap(_ryujinxLogo, _logoPosition); 205 206 float halfWidth = _panelRectangle.Width / 2; 207 float buttonsY = _panelRectangle.Top + 185; 208 209 SKPoint disableButtonPosition = new(halfWidth + 180, buttonsY); 210 211 DrawControllerToggle(canvas, disableButtonPosition); 212 } 213 214 public void DrawMutableElements(SoftwareKeyboardUIState state) 215 { 216 if (_surface == null) 217 { 218 return; 219 } 220 221 using var paint = new SKPaint(_messageFont) 222 { 223 Color = _textNormalColor, 224 IsAntialias = true 225 }; 226 227 var canvas = _surface.Canvas; 228 var messageRectangle = MeasureString(MessageText, paint); 229 float messagePositionX = (_panelRectangle.Width - messageRectangle.Width) / 2 - messageRectangle.Left; 230 float messagePositionY = _messagePositionY - messageRectangle.Top; 231 var messagePosition = new SKPoint(messagePositionX, messagePositionY); 232 var messageBoundRectangle = SKRect.Create(messagePositionX, messagePositionY, messageRectangle.Width, messageRectangle.Height); 233 234 canvas.DrawRect(messageBoundRectangle, _panelBrush); 235 236 canvas.DrawText(MessageText, messagePosition.X, messagePosition.Y + _messageFont.Metrics.XHeight + _messageFont.Metrics.Descent, paint); 237 238 if (!state.TypingEnabled) 239 { 240 // Just draw a semi-transparent rectangle on top to fade the component with the background. 241 // TODO (caian): This will not work if one decides to add make background semi-transparent as well. 242 243 canvas.DrawRect(messageBoundRectangle, _disabledBrush); 244 } 245 246 DrawTextBox(canvas, state); 247 248 float halfWidth = _panelRectangle.Width / 2; 249 float buttonsY = _panelRectangle.Top + 185; 250 251 SKPoint acceptButtonPosition = new(halfWidth - 180, buttonsY); 252 SKPoint cancelButtonPosition = new(halfWidth, buttonsY); 253 SKPoint disableButtonPosition = new(halfWidth + 180, buttonsY); 254 255 DrawPadButton(canvas, acceptButtonPosition, _padAcceptIcon, AcceptText, state.AcceptPressed, state.ControllerEnabled); 256 DrawPadButton(canvas, cancelButtonPosition, _padCancelIcon, CancelText, state.CancelPressed, state.ControllerEnabled); 257 258 } 259 260 public void CreateSurface(RenderingSurfaceInfo surfaceInfo) 261 { 262 if (_surfaceInfo != null) 263 { 264 return; 265 } 266 267 _surfaceInfo = surfaceInfo; 268 269 Debug.Assert(_surfaceInfo.ColorFormat == Services.SurfaceFlinger.ColorFormat.A8B8G8R8); 270 271 // Use the whole area of the image to draw, even the alignment, otherwise it may shear the final 272 // image if the pitch is different. 273 uint totalWidth = _surfaceInfo.Pitch / 4; 274 uint totalHeight = _surfaceInfo.Size / _surfaceInfo.Pitch; 275 276 Debug.Assert(_surfaceInfo.Width <= totalWidth); 277 Debug.Assert(_surfaceInfo.Height <= totalHeight); 278 Debug.Assert(_surfaceInfo.Pitch * _surfaceInfo.Height <= _surfaceInfo.Size); 279 280 _imageInfo = new SKImageInfo((int)totalWidth, (int)totalHeight, SKColorType.Rgba8888); 281 _surface = SKSurface.Create(_imageInfo); 282 283 ComputeConstants(); 284 DrawImmutableElements(); 285 } 286 287 private void ComputeConstants() 288 { 289 int totalWidth = (int)_surfaceInfo.Width; 290 int totalHeight = (int)_surfaceInfo.Height; 291 292 int panelHeight = 240; 293 int panelPositionY = totalHeight - panelHeight; 294 295 _panelRectangle = SKRect.Create(0, panelPositionY, totalWidth, panelHeight); 296 297 _messagePositionY = panelPositionY + 60; 298 299 int logoPositionX = (totalWidth - _ryujinxLogo.Width) / 2; 300 int logoPositionY = panelPositionY + 18; 301 302 _logoPosition = new SKPoint(logoPositionX, logoPositionY); 303 } 304 private static SKRect MeasureString(string text, SKPaint paint) 305 { 306 SKRect bounds = SKRect.Empty; 307 308 if (text == "") 309 { 310 paint.MeasureText(" ", ref bounds); 311 } 312 else 313 { 314 paint.MeasureText(text, ref bounds); 315 } 316 317 return bounds; 318 } 319 320 private static SKRect MeasureString(ReadOnlySpan<char> text, SKPaint paint) 321 { 322 SKRect bounds = SKRect.Empty; 323 324 if (text == "") 325 { 326 paint.MeasureText(" ", ref bounds); 327 } 328 else 329 { 330 paint.MeasureText(text, ref bounds); 331 } 332 333 return bounds; 334 } 335 336 private void DrawTextBox(SKCanvas canvas, SoftwareKeyboardUIState state) 337 { 338 using var textPaint = new SKPaint(_labelsTextFont) 339 { 340 IsAntialias = true, 341 Color = _textNormalColor 342 }; 343 var inputTextRectangle = MeasureString(state.InputText, textPaint); 344 345 float boxWidth = (int)(Math.Max(300, inputTextRectangle.Width + inputTextRectangle.Left + 8)); 346 float boxHeight = 32; 347 float boxY = _panelRectangle.Top + 110; 348 float boxX = (int)((_panelRectangle.Width - boxWidth) / 2); 349 350 SKRect boxRectangle = SKRect.Create(boxX, boxY, boxWidth, boxHeight); 351 352 SKRect boundRectangle = SKRect.Create(_panelRectangle.Left, boxY - _textBoxOutlineWidth, 353 _panelRectangle.Width, boxHeight + 2 * _textBoxOutlineWidth); 354 355 canvas.DrawRect(boundRectangle, _panelBrush); 356 357 canvas.DrawRect(boxRectangle, _textBoxOutlinePen); 358 359 float inputTextX = (_panelRectangle.Width - inputTextRectangle.Width) / 2 - inputTextRectangle.Left; 360 float inputTextY = boxY + 5; 361 362 var inputTextPosition = new SKPoint(inputTextX, inputTextY); 363 canvas.DrawText(state.InputText, inputTextPosition.X, inputTextPosition.Y + (_labelsTextFont.Metrics.XHeight + _labelsTextFont.Metrics.Descent), textPaint); 364 365 // Draw the cursor on top of the text and redraw the text with a different color if necessary. 366 367 SKColor cursorTextColor; 368 SKPaint cursorBrush; 369 SKPaint cursorPen; 370 371 float cursorPositionYTop = inputTextY + 1; 372 float cursorPositionYBottom = cursorPositionYTop + _inputTextFontSize + 1; 373 float cursorPositionXLeft; 374 float cursorPositionXRight; 375 376 bool cursorVisible = false; 377 378 if (state.CursorBegin != state.CursorEnd) 379 { 380 Debug.Assert(state.InputText.Length > 0); 381 382 cursorTextColor = _textSelectedColor; 383 cursorBrush = _selectionBoxBrush; 384 cursorPen = _selectionBoxPen; 385 386 ReadOnlySpan<char> textUntilBegin = state.InputText.AsSpan(0, state.CursorBegin); 387 ReadOnlySpan<char> textUntilEnd = state.InputText.AsSpan(0, state.CursorEnd); 388 389 var selectionBeginRectangle = MeasureString(textUntilBegin, textPaint); 390 var selectionEndRectangle = MeasureString(textUntilEnd, textPaint); 391 392 cursorVisible = true; 393 cursorPositionXLeft = inputTextX + selectionBeginRectangle.Width + selectionBeginRectangle.Left; 394 cursorPositionXRight = inputTextX + selectionEndRectangle.Width + selectionEndRectangle.Left; 395 } 396 else 397 { 398 cursorTextColor = _textOverCursorColor; 399 cursorBrush = _cursorBrush; 400 cursorPen = _cursorPen; 401 402 if (state.TextBoxBlinkCounter < TextBoxBlinkThreshold) 403 { 404 // Show the blinking cursor. 405 406 int cursorBegin = Math.Min(state.InputText.Length, state.CursorBegin); 407 ReadOnlySpan<char> textUntilCursor = state.InputText.AsSpan(0, cursorBegin); 408 var cursorTextRectangle = MeasureString(textUntilCursor, textPaint); 409 410 cursorVisible = true; 411 cursorPositionXLeft = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.Left; 412 413 if (state.OverwriteMode) 414 { 415 // The blinking cursor is in overwrite mode so it takes the size of a character. 416 417 if (state.CursorBegin < state.InputText.Length) 418 { 419 textUntilCursor = state.InputText.AsSpan(0, cursorBegin + 1); 420 cursorTextRectangle = MeasureString(textUntilCursor, textPaint); 421 cursorPositionXRight = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.Left; 422 } 423 else 424 { 425 cursorPositionXRight = cursorPositionXLeft + _inputTextFontSize / 2; 426 } 427 } 428 else 429 { 430 // The blinking cursor is in insert mode so it is only a line. 431 cursorPositionXRight = cursorPositionXLeft; 432 } 433 } 434 else 435 { 436 cursorPositionXLeft = inputTextX; 437 cursorPositionXRight = inputTextX; 438 } 439 } 440 441 if (state.TypingEnabled && cursorVisible) 442 { 443 float cursorWidth = cursorPositionXRight - cursorPositionXLeft; 444 float cursorHeight = cursorPositionYBottom - cursorPositionYTop; 445 446 if (cursorWidth == 0) 447 { 448 canvas.DrawLine(new SKPoint(cursorPositionXLeft, cursorPositionYTop), 449 new SKPoint(cursorPositionXLeft, cursorPositionYBottom), 450 cursorPen); 451 } 452 else 453 { 454 var cursorRectangle = SKRect.Create(cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight); 455 456 canvas.DrawRect(cursorRectangle, cursorPen); 457 canvas.DrawRect(cursorRectangle, cursorBrush); 458 459 using var textOverCursor = SKSurface.Create(new SKImageInfo((int)cursorRectangle.Width, (int)cursorRectangle.Height, SKColorType.Rgba8888)); 460 var textOverCanvas = textOverCursor.Canvas; 461 var textRelativePosition = new SKPoint(inputTextPosition.X - cursorRectangle.Left, inputTextPosition.Y - cursorRectangle.Top); 462 463 using var cursorPaint = new SKPaint(_inputTextFont) 464 { 465 Color = cursorTextColor, 466 IsAntialias = true 467 }; 468 469 textOverCanvas.DrawText(state.InputText, textRelativePosition.X, textRelativePosition.Y + _inputTextFont.Metrics.XHeight + _inputTextFont.Metrics.Descent, cursorPaint); 470 471 var cursorPosition = new SKPoint((int)cursorRectangle.Left, (int)cursorRectangle.Top); 472 textOverCursor.Flush(); 473 canvas.DrawSurface(textOverCursor, cursorPosition); 474 } 475 } 476 else if (!state.TypingEnabled) 477 { 478 // Just draw a semi-transparent rectangle on top to fade the component with the background. 479 // TODO (caian): This will not work if one decides to add make background semi-transparent as well. 480 481 canvas.DrawRect(boundRectangle, _disabledBrush); 482 } 483 } 484 485 private void DrawPadButton(SKCanvas canvas, SKPoint point, SKBitmap icon, string label, bool pressed, bool enabled) 486 { 487 // Use relative positions so we can center the entire drawing later. 488 489 float iconX = 0; 490 float iconY = 0; 491 float iconWidth = icon.Width; 492 float iconHeight = icon.Height; 493 494 using var paint = new SKPaint(_labelsTextFont) 495 { 496 Color = _textNormalColor, 497 IsAntialias = true 498 }; 499 500 var labelRectangle = MeasureString(label, paint); 501 502 float labelPositionX = iconWidth + 8 - labelRectangle.Left; 503 float labelPositionY = 3; 504 505 float fullWidth = labelPositionX + labelRectangle.Width + labelRectangle.Left; 506 float fullHeight = iconHeight; 507 508 // Convert all relative positions into absolute. 509 510 float originX = (int)(point.X - fullWidth / 2); 511 float originY = (int)(point.Y - fullHeight / 2); 512 513 iconX += originX; 514 iconY += originY; 515 516 var iconPosition = new SKPoint((int)iconX, (int)iconY); 517 var labelPosition = new SKPoint(labelPositionX + originX, labelPositionY + originY); 518 519 var selectedRectangle = SKRect.Create(originX - 2 * _padPressedPenWidth, originY - 2 * _padPressedPenWidth, 520 fullWidth + 4 * _padPressedPenWidth, fullHeight + 4 * _padPressedPenWidth); 521 522 var boundRectangle = SKRect.Create(originX, originY, fullWidth, fullHeight); 523 boundRectangle.Inflate(4 * _padPressedPenWidth, 4 * _padPressedPenWidth); 524 525 canvas.DrawRect(boundRectangle, _panelBrush); 526 canvas.DrawBitmap(icon, iconPosition); 527 canvas.DrawText(label, labelPosition.X, labelPosition.Y + _labelsTextFont.Metrics.XHeight + _labelsTextFont.Metrics.Descent, paint); 528 529 if (enabled) 530 { 531 if (pressed) 532 { 533 canvas.DrawRect(selectedRectangle, _padPressedPen); 534 } 535 } 536 else 537 { 538 // Just draw a semi-transparent rectangle on top to fade the component with the background. 539 // TODO (caian): This will not work if one decides to add make background semi-transparent as well. 540 541 canvas.DrawRect(boundRectangle, _disabledBrush); 542 } 543 } 544 545 private void DrawControllerToggle(SKCanvas canvas, SKPoint point) 546 { 547 using var paint = new SKPaint(_labelsTextFont) 548 { 549 IsAntialias = true, 550 Color = _textNormalColor 551 }; 552 var labelRectangle = MeasureString(ControllerToggleText, paint); 553 554 // Use relative positions so we can center the entire drawing later. 555 556 float keyWidth = _keyModeIcon.Width; 557 float keyHeight = _keyModeIcon.Height; 558 559 float labelPositionX = keyWidth + 8 - labelRectangle.Left; 560 float labelPositionY = -labelRectangle.Top - 1; 561 562 float keyX = 0; 563 float keyY = (int)((labelPositionY + labelRectangle.Height - keyHeight) / 2); 564 565 float fullWidth = labelPositionX + labelRectangle.Width; 566 float fullHeight = Math.Max(labelPositionY + labelRectangle.Height, keyHeight); 567 568 // Convert all relative positions into absolute. 569 570 float originX = (int)(point.X - fullWidth / 2); 571 float originY = (int)(point.Y - fullHeight / 2); 572 573 keyX += originX; 574 keyY += originY; 575 576 var labelPosition = new SKPoint(labelPositionX + originX, labelPositionY + originY); 577 var overlayPosition = new SKPoint((int)keyX, (int)keyY); 578 579 canvas.DrawBitmap(_keyModeIcon, overlayPosition); 580 canvas.DrawText(ControllerToggleText, labelPosition.X, labelPosition.Y + _labelsTextFont.Metrics.XHeight, paint); 581 } 582 583 public unsafe void CopyImageToBuffer() 584 { 585 lock (_bufferLock) 586 { 587 if (_surface == null) 588 { 589 return; 590 } 591 592 // Convert the pixel format used in the image to the one used in the Switch surface. 593 _surface.Flush(); 594 595 var buffer = new byte[_imageInfo.BytesSize]; 596 fixed (byte* bufferPtr = buffer) 597 { 598 if (!_surface.ReadPixels(_imageInfo, (nint)bufferPtr, _imageInfo.RowBytes, 0, 0)) 599 { 600 return; 601 } 602 } 603 604 _bufferData = buffer; 605 606 Debug.Assert(buffer.Length == _surfaceInfo.Size); 607 } 608 } 609 610 public bool WriteBufferToMemory(IVirtualMemoryManager destination, ulong position) 611 { 612 lock (_bufferLock) 613 { 614 if (_bufferData == null) 615 { 616 return false; 617 } 618 619 try 620 { 621 destination.Write(position, _bufferData); 622 } 623 catch 624 { 625 return false; 626 } 627 628 return true; 629 } 630 } 631 } 632 }