SoftwareKeyboardRendererBase.cs 23 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591
  1. using Ryujinx.HLE.Ui;
  2. using Ryujinx.Memory;
  3. using SixLabors.ImageSharp;
  4. using SixLabors.ImageSharp.Processing;
  5. using SixLabors.ImageSharp.Drawing.Processing;
  6. using SixLabors.Fonts;
  7. using System;
  8. using System.Diagnostics;
  9. using System.IO;
  10. using System.Numerics;
  11. using System.Reflection;
  12. using System.Runtime.InteropServices;
  13. using SixLabors.ImageSharp.PixelFormats;
  14. using Ryujinx.Common;
  15. namespace Ryujinx.HLE.HOS.Applets.SoftwareKeyboard
  16. {
  17. /// <summary>
  18. /// Base class that generates the graphics for the software keyboard applet during inline mode.
  19. /// </summary>
  20. internal class SoftwareKeyboardRendererBase
  21. {
  22. public const int TextBoxBlinkThreshold = 8;
  23. const string MessageText = "Please use the keyboard to input text";
  24. const string AcceptText = "Accept";
  25. const string CancelText = "Cancel";
  26. const string ControllerToggleText = "Toggle input";
  27. private readonly object _bufferLock = new object();
  28. private RenderingSurfaceInfo _surfaceInfo = null;
  29. private Image<Argb32> _surface = null;
  30. private byte[] _bufferData = null;
  31. private Image _ryujinxLogo = null;
  32. private Image _padAcceptIcon = null;
  33. private Image _padCancelIcon = null;
  34. private Image _keyModeIcon = null;
  35. private float _textBoxOutlineWidth;
  36. private float _padPressedPenWidth;
  37. private Color _textNormalColor;
  38. private Color _textSelectedColor;
  39. private Color _textOverCursorColor;
  40. private IBrush _panelBrush;
  41. private IBrush _disabledBrush;
  42. private IBrush _cursorBrush;
  43. private IBrush _selectionBoxBrush;
  44. private Pen _textBoxOutlinePen;
  45. private Pen _cursorPen;
  46. private Pen _selectionBoxPen;
  47. private Pen _padPressedPen;
  48. private int _inputTextFontSize;
  49. private Font _messageFont;
  50. private Font _inputTextFont;
  51. private Font _labelsTextFont;
  52. private RectangleF _panelRectangle;
  53. private Point _logoPosition;
  54. private float _messagePositionY;
  55. public SoftwareKeyboardRendererBase(IHostUiTheme uiTheme)
  56. {
  57. int ryujinxLogoSize = 32;
  58. Stream logoStream = EmbeddedResources.GetStream("Ryujinx.Ui.Common/Resources/Logo_Ryujinx.png");
  59. _ryujinxLogo = LoadResource(logoStream, ryujinxLogoSize, ryujinxLogoSize);
  60. string padAcceptIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnA.png";
  61. string padCancelIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_BtnB.png";
  62. string keyModeIconPath = "Ryujinx.HLE.HOS.Applets.SoftwareKeyboard.Resources.Icon_KeyF6.png";
  63. _padAcceptIcon = LoadResource(Assembly.GetExecutingAssembly(), padAcceptIconPath , 0, 0);
  64. _padCancelIcon = LoadResource(Assembly.GetExecutingAssembly(), padCancelIconPath , 0, 0);
  65. _keyModeIcon = LoadResource(Assembly.GetExecutingAssembly(), keyModeIconPath , 0, 0);
  66. Color panelColor = ToColor(uiTheme.DefaultBackgroundColor, 255);
  67. Color panelTransparentColor = ToColor(uiTheme.DefaultBackgroundColor, 150);
  68. Color borderColor = ToColor(uiTheme.DefaultBorderColor);
  69. Color selectionBackgroundColor = ToColor(uiTheme.SelectionBackgroundColor);
  70. _textNormalColor = ToColor(uiTheme.DefaultForegroundColor);
  71. _textSelectedColor = ToColor(uiTheme.SelectionForegroundColor);
  72. _textOverCursorColor = ToColor(uiTheme.DefaultForegroundColor, null, true);
  73. float cursorWidth = 2;
  74. _textBoxOutlineWidth = 2;
  75. _padPressedPenWidth = 2;
  76. _panelBrush = new SolidBrush(panelColor);
  77. _disabledBrush = new SolidBrush(panelTransparentColor);
  78. _cursorBrush = new SolidBrush(_textNormalColor);
  79. _selectionBoxBrush = new SolidBrush(selectionBackgroundColor);
  80. _textBoxOutlinePen = new Pen(borderColor, _textBoxOutlineWidth);
  81. _cursorPen = new Pen(_textNormalColor, cursorWidth);
  82. _selectionBoxPen = new Pen(selectionBackgroundColor, cursorWidth);
  83. _padPressedPen = new Pen(borderColor, _padPressedPenWidth);
  84. _inputTextFontSize = 20;
  85. CreateFonts(uiTheme.FontFamily);
  86. }
  87. private void CreateFonts(string uiThemeFontFamily)
  88. {
  89. // Try a list of fonts in case any of them is not available in the system.
  90. string[] availableFonts = new string[]
  91. {
  92. uiThemeFontFamily,
  93. "Liberation Sans",
  94. "FreeSans",
  95. "DejaVu Sans"
  96. };
  97. foreach (string fontFamily in availableFonts)
  98. {
  99. try
  100. {
  101. _messageFont = SystemFonts.CreateFont(fontFamily, 26, FontStyle.Regular);
  102. _inputTextFont = SystemFonts.CreateFont(fontFamily, _inputTextFontSize, FontStyle.Regular);
  103. _labelsTextFont = SystemFonts.CreateFont(fontFamily, 24, FontStyle.Regular);
  104. return;
  105. }
  106. catch
  107. {
  108. }
  109. }
  110. throw new Exception($"None of these fonts were found in the system: {String.Join(", ", availableFonts)}!");
  111. }
  112. private Color ToColor(ThemeColor color, byte? overrideAlpha = null, bool flipRgb = false)
  113. {
  114. var a = (byte)(color.A * 255);
  115. var r = (byte)(color.R * 255);
  116. var g = (byte)(color.G * 255);
  117. var b = (byte)(color.B * 255);
  118. if (flipRgb)
  119. {
  120. r = (byte)(255 - r);
  121. g = (byte)(255 - g);
  122. b = (byte)(255 - b);
  123. }
  124. return Color.FromRgba(r, g, b, overrideAlpha.GetValueOrDefault(a));
  125. }
  126. private Image LoadResource(Assembly assembly, string resourcePath, int newWidth, int newHeight)
  127. {
  128. Stream resourceStream = assembly.GetManifestResourceStream(resourcePath);
  129. return LoadResource(resourceStream, newWidth, newHeight);
  130. }
  131. private Image LoadResource(Stream resourceStream, int newWidth, int newHeight)
  132. {
  133. Debug.Assert(resourceStream != null);
  134. var image = Image.Load(resourceStream);
  135. if (newHeight != 0 && newWidth != 0)
  136. {
  137. image.Mutate(x => x.Resize(newWidth, newHeight, KnownResamplers.Lanczos3));
  138. }
  139. return image;
  140. }
  141. private void SetGraphicsOptions(IImageProcessingContext context)
  142. {
  143. context.GetGraphicsOptions().Antialias = true;
  144. context.GetShapeGraphicsOptions().GraphicsOptions.Antialias = true;
  145. }
  146. private void DrawImmutableElements()
  147. {
  148. if (_surface == null)
  149. {
  150. return;
  151. }
  152. _surface.Mutate(context =>
  153. {
  154. SetGraphicsOptions(context);
  155. context.Clear(Color.Transparent);
  156. context.Fill(_panelBrush, _panelRectangle);
  157. context.DrawImage(_ryujinxLogo, _logoPosition, 1);
  158. float halfWidth = _panelRectangle.Width / 2;
  159. float buttonsY = _panelRectangle.Y + 185;
  160. PointF disableButtonPosition = new PointF(halfWidth + 180, buttonsY);
  161. DrawControllerToggle(context, disableButtonPosition);
  162. });
  163. }
  164. public void DrawMutableElements(SoftwareKeyboardUiState state)
  165. {
  166. if (_surface == null)
  167. {
  168. return;
  169. }
  170. _surface.Mutate(context =>
  171. {
  172. var messageRectangle = MeasureString(MessageText, _messageFont);
  173. float messagePositionX = (_panelRectangle.Width - messageRectangle.Width) / 2 - messageRectangle.X;
  174. float messagePositionY = _messagePositionY - messageRectangle.Y;
  175. var messagePosition = new PointF(messagePositionX, messagePositionY);
  176. var messageBoundRectangle = new RectangleF(messagePositionX, messagePositionY, messageRectangle.Width, messageRectangle.Height);
  177. SetGraphicsOptions(context);
  178. context.Fill(_panelBrush, messageBoundRectangle);
  179. context.DrawText(MessageText, _messageFont, _textNormalColor, messagePosition);
  180. if (!state.TypingEnabled)
  181. {
  182. // Just draw a semi-transparent rectangle on top to fade the component with the background.
  183. // TODO (caian): This will not work if one decides to add make background semi-transparent as well.
  184. context.Fill(_disabledBrush, messageBoundRectangle);
  185. }
  186. DrawTextBox(context, state);
  187. float halfWidth = _panelRectangle.Width / 2;
  188. float buttonsY = _panelRectangle.Y + 185;
  189. PointF acceptButtonPosition = new PointF(halfWidth - 180, buttonsY);
  190. PointF cancelButtonPosition = new PointF(halfWidth , buttonsY);
  191. PointF disableButtonPosition = new PointF(halfWidth + 180, buttonsY);
  192. DrawPadButton(context, acceptButtonPosition, _padAcceptIcon, AcceptText, state.AcceptPressed, state.ControllerEnabled);
  193. DrawPadButton(context, cancelButtonPosition, _padCancelIcon, CancelText, state.CancelPressed, state.ControllerEnabled);
  194. });
  195. }
  196. public void CreateSurface(RenderingSurfaceInfo surfaceInfo)
  197. {
  198. if (_surfaceInfo != null)
  199. {
  200. return;
  201. }
  202. _surfaceInfo = surfaceInfo;
  203. Debug.Assert(_surfaceInfo.ColorFormat == Services.SurfaceFlinger.ColorFormat.A8B8G8R8);
  204. // Use the whole area of the image to draw, even the alignment, otherwise it may shear the final
  205. // image if the pitch is different.
  206. uint totalWidth = _surfaceInfo.Pitch / 4;
  207. uint totalHeight = _surfaceInfo.Size / _surfaceInfo.Pitch;
  208. Debug.Assert(_surfaceInfo.Width <= totalWidth);
  209. Debug.Assert(_surfaceInfo.Height <= totalHeight);
  210. Debug.Assert(_surfaceInfo.Pitch * _surfaceInfo.Height <= _surfaceInfo.Size);
  211. _surface = new Image<Argb32>((int)totalWidth, (int)totalHeight);
  212. ComputeConstants();
  213. DrawImmutableElements();
  214. }
  215. private void ComputeConstants()
  216. {
  217. int totalWidth = (int)_surfaceInfo.Width;
  218. int totalHeight = (int)_surfaceInfo.Height;
  219. int panelHeight = 240;
  220. int panelPositionY = totalHeight - panelHeight;
  221. _panelRectangle = new RectangleF(0, panelPositionY, totalWidth, panelHeight);
  222. _messagePositionY = panelPositionY + 60;
  223. int logoPositionX = (totalWidth - _ryujinxLogo.Width) / 2;
  224. int logoPositionY = panelPositionY + 18;
  225. _logoPosition = new Point(logoPositionX, logoPositionY);
  226. }
  227. private RectangleF MeasureString(string text, Font font)
  228. {
  229. RendererOptions options = new RendererOptions(font);
  230. FontRectangle rectangle = TextMeasurer.Measure(text == "" ? " " : text, options);
  231. if (text == "")
  232. {
  233. return new RectangleF(0, rectangle.Y, 0, rectangle.Height);
  234. }
  235. else
  236. {
  237. return new RectangleF(rectangle.X, rectangle.Y, rectangle.Width, rectangle.Height);
  238. }
  239. }
  240. private void DrawTextBox(IImageProcessingContext context, SoftwareKeyboardUiState state)
  241. {
  242. var inputTextRectangle = MeasureString(state.InputText, _inputTextFont);
  243. float boxWidth = (int)(Math.Max(300, inputTextRectangle.Width + inputTextRectangle.X + 8));
  244. float boxHeight = 32;
  245. float boxY = _panelRectangle.Y + 110;
  246. float boxX = (int)((_panelRectangle.Width - boxWidth) / 2);
  247. RectangleF boxRectangle = new RectangleF(boxX, boxY, boxWidth, boxHeight);
  248. RectangleF boundRectangle = new RectangleF(_panelRectangle.X, boxY - _textBoxOutlineWidth,
  249. _panelRectangle.Width, boxHeight + 2 * _textBoxOutlineWidth);
  250. context.Fill(_panelBrush, boundRectangle);
  251. context.Draw(_textBoxOutlinePen, boxRectangle);
  252. float inputTextX = (_panelRectangle.Width - inputTextRectangle.Width) / 2 - inputTextRectangle.X;
  253. float inputTextY = boxY + 5;
  254. var inputTextPosition = new PointF(inputTextX, inputTextY);
  255. context.DrawText(state.InputText, _inputTextFont, _textNormalColor, inputTextPosition);
  256. // Draw the cursor on top of the text and redraw the text with a different color if necessary.
  257. Color cursorTextColor;
  258. IBrush cursorBrush;
  259. Pen cursorPen;
  260. float cursorPositionYTop = inputTextY + 1;
  261. float cursorPositionYBottom = cursorPositionYTop + _inputTextFontSize + 1;
  262. float cursorPositionXLeft;
  263. float cursorPositionXRight;
  264. bool cursorVisible = false;
  265. if (state.CursorBegin != state.CursorEnd)
  266. {
  267. Debug.Assert(state.InputText.Length > 0);
  268. cursorTextColor = _textSelectedColor;
  269. cursorBrush = _selectionBoxBrush;
  270. cursorPen = _selectionBoxPen;
  271. string textUntilBegin = state.InputText.Substring(0, state.CursorBegin);
  272. string textUntilEnd = state.InputText.Substring(0, state.CursorEnd);
  273. var selectionBeginRectangle = MeasureString(textUntilBegin, _inputTextFont);
  274. var selectionEndRectangle = MeasureString(textUntilEnd , _inputTextFont);
  275. cursorVisible = true;
  276. cursorPositionXLeft = inputTextX + selectionBeginRectangle.Width + selectionBeginRectangle.X;
  277. cursorPositionXRight = inputTextX + selectionEndRectangle.Width + selectionEndRectangle.X;
  278. }
  279. else
  280. {
  281. cursorTextColor = _textOverCursorColor;
  282. cursorBrush = _cursorBrush;
  283. cursorPen = _cursorPen;
  284. if (state.TextBoxBlinkCounter < TextBoxBlinkThreshold)
  285. {
  286. // Show the blinking cursor.
  287. int cursorBegin = Math.Min(state.InputText.Length, state.CursorBegin);
  288. string textUntilCursor = state.InputText.Substring(0, cursorBegin);
  289. var cursorTextRectangle = MeasureString(textUntilCursor, _inputTextFont);
  290. cursorVisible = true;
  291. cursorPositionXLeft = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.X;
  292. if (state.OverwriteMode)
  293. {
  294. // The blinking cursor is in overwrite mode so it takes the size of a character.
  295. if (state.CursorBegin < state.InputText.Length)
  296. {
  297. textUntilCursor = state.InputText.Substring(0, cursorBegin + 1);
  298. cursorTextRectangle = MeasureString(textUntilCursor, _inputTextFont);
  299. cursorPositionXRight = inputTextX + cursorTextRectangle.Width + cursorTextRectangle.X;
  300. }
  301. else
  302. {
  303. cursorPositionXRight = cursorPositionXLeft + _inputTextFontSize / 2;
  304. }
  305. }
  306. else
  307. {
  308. // The blinking cursor is in insert mode so it is only a line.
  309. cursorPositionXRight = cursorPositionXLeft;
  310. }
  311. }
  312. else
  313. {
  314. cursorPositionXLeft = inputTextX;
  315. cursorPositionXRight = inputTextX;
  316. }
  317. }
  318. if (state.TypingEnabled && cursorVisible)
  319. {
  320. float cursorWidth = cursorPositionXRight - cursorPositionXLeft;
  321. float cursorHeight = cursorPositionYBottom - cursorPositionYTop;
  322. if (cursorWidth == 0)
  323. {
  324. PointF[] points = new PointF[]
  325. {
  326. new PointF(cursorPositionXLeft, cursorPositionYTop),
  327. new PointF(cursorPositionXLeft, cursorPositionYBottom),
  328. };
  329. context.DrawLines(cursorPen, points);
  330. }
  331. else
  332. {
  333. var cursorRectangle = new RectangleF(cursorPositionXLeft, cursorPositionYTop, cursorWidth, cursorHeight);
  334. context.Draw(cursorPen , cursorRectangle);
  335. context.Fill(cursorBrush, cursorRectangle);
  336. Image<Argb32> textOverCursor = new Image<Argb32>((int)cursorRectangle.Width, (int)cursorRectangle.Height);
  337. textOverCursor.Mutate(context =>
  338. {
  339. var textRelativePosition = new PointF(inputTextPosition.X - cursorRectangle.X, inputTextPosition.Y - cursorRectangle.Y);
  340. context.DrawText(state.InputText, _inputTextFont, cursorTextColor, textRelativePosition);
  341. });
  342. var cursorPosition = new Point((int)cursorRectangle.X, (int)cursorRectangle.Y);
  343. context.DrawImage(textOverCursor, cursorPosition, 1);
  344. }
  345. }
  346. else if (!state.TypingEnabled)
  347. {
  348. // Just draw a semi-transparent rectangle on top to fade the component with the background.
  349. // TODO (caian): This will not work if one decides to add make background semi-transparent as well.
  350. context.Fill(_disabledBrush, boundRectangle);
  351. }
  352. }
  353. private void DrawPadButton(IImageProcessingContext context, PointF point, Image icon, string label, bool pressed, bool enabled)
  354. {
  355. // Use relative positions so we can center the the entire drawing later.
  356. float iconX = 0;
  357. float iconY = 0;
  358. float iconWidth = icon.Width;
  359. float iconHeight = icon.Height;
  360. var labelRectangle = MeasureString(label, _labelsTextFont);
  361. float labelPositionX = iconWidth + 8 - labelRectangle.X;
  362. float labelPositionY = 3;
  363. float fullWidth = labelPositionX + labelRectangle.Width + labelRectangle.X;
  364. float fullHeight = iconHeight;
  365. // Convert all relative positions into absolute.
  366. float originX = (int)(point.X - fullWidth / 2);
  367. float originY = (int)(point.Y - fullHeight / 2);
  368. iconX += originX;
  369. iconY += originY;
  370. var iconPosition = new Point((int)iconX, (int)iconY);
  371. var labelPosition = new PointF(labelPositionX + originX, labelPositionY + originY);
  372. var selectedRectangle = new RectangleF(originX - 2 * _padPressedPenWidth, originY - 2 * _padPressedPenWidth,
  373. fullWidth + 4 * _padPressedPenWidth, fullHeight + 4 * _padPressedPenWidth);
  374. var boundRectangle = new RectangleF(originX, originY, fullWidth, fullHeight);
  375. boundRectangle.Inflate(4 * _padPressedPenWidth, 4 * _padPressedPenWidth);
  376. context.Fill(_panelBrush, boundRectangle);
  377. context.DrawImage(icon, iconPosition, 1);
  378. context.DrawText(label, _labelsTextFont, _textNormalColor, labelPosition);
  379. if (enabled)
  380. {
  381. if (pressed)
  382. {
  383. context.Draw(_padPressedPen, selectedRectangle);
  384. }
  385. }
  386. else
  387. {
  388. // Just draw a semi-transparent rectangle on top to fade the component with the background.
  389. // TODO (caian): This will not work if one decides to add make background semi-transparent as well.
  390. context.Fill(_disabledBrush, boundRectangle);
  391. }
  392. }
  393. private void DrawControllerToggle(IImageProcessingContext context, PointF point)
  394. {
  395. var labelRectangle = MeasureString(ControllerToggleText, _labelsTextFont);
  396. // Use relative positions so we can center the the entire drawing later.
  397. float keyWidth = _keyModeIcon.Width;
  398. float keyHeight = _keyModeIcon.Height;
  399. float labelPositionX = keyWidth + 8 - labelRectangle.X;
  400. float labelPositionY = -labelRectangle.Y - 1;
  401. float keyX = 0;
  402. float keyY = (int)((labelPositionY + labelRectangle.Height - keyHeight) / 2);
  403. float fullWidth = labelPositionX + labelRectangle.Width;
  404. float fullHeight = Math.Max(labelPositionY + labelRectangle.Height, keyHeight);
  405. // Convert all relative positions into absolute.
  406. float originX = (int)(point.X - fullWidth / 2);
  407. float originY = (int)(point.Y - fullHeight / 2);
  408. keyX += originX;
  409. keyY += originY;
  410. var labelPosition = new PointF(labelPositionX + originX, labelPositionY + originY);
  411. var overlayPosition = new Point((int)keyX, (int)keyY);
  412. context.DrawImage(_keyModeIcon, overlayPosition, 1);
  413. context.DrawText(ControllerToggleText, _labelsTextFont, _textNormalColor, labelPosition);
  414. }
  415. public void CopyImageToBuffer()
  416. {
  417. lock (_bufferLock)
  418. {
  419. if (_surface == null)
  420. {
  421. return;
  422. }
  423. // Convert the pixel format used in the image to the one used in the Switch surface.
  424. if (!_surface.DangerousTryGetSinglePixelMemory(out Memory<Argb32> pixels))
  425. {
  426. return;
  427. }
  428. _bufferData = MemoryMarshal.AsBytes(pixels.Span).ToArray();
  429. Span<uint> dataConvert = MemoryMarshal.Cast<byte, uint>(_bufferData);
  430. Debug.Assert(_bufferData.Length == _surfaceInfo.Size);
  431. for (int i = 0; i < dataConvert.Length; i++)
  432. {
  433. dataConvert[i] = BitOperations.RotateRight(dataConvert[i], 8);
  434. }
  435. }
  436. }
  437. public bool WriteBufferToMemory(IVirtualMemoryManager destination, ulong position)
  438. {
  439. lock (_bufferLock)
  440. {
  441. if (_bufferData == null)
  442. {
  443. return false;
  444. }
  445. try
  446. {
  447. destination.Write(position, _bufferData);
  448. }
  449. catch
  450. {
  451. return false;
  452. }
  453. return true;
  454. }
  455. }
  456. }
  457. }