using System; using System.Collections.Generic; using System.Drawing; using System.Drawing.Drawing2D; using System.Drawing.Imaging; using System.Drawing.Text; using System.IO; using System.Windows.Forms; using BizHawk.Client.Common; using BizHawk.Emulation.Common; namespace BizHawk.Client.EmuHawk { public sealed class GuiApi : IGui { [RequiredService] private IEmulator Emulator { get; set; } public GuiApi(Action logCallback) { LogCallback = logCallback; } public GuiApi() : this(Console.WriteLine) {} private readonly Action LogCallback; private readonly Dictionary _imageCache = new Dictionary(); private readonly Bitmap _nullGraphicsBitmap = new Bitmap(1, 1); private readonly Dictionary _pens = new Dictionary(); private readonly Dictionary _solidBrushes = new Dictionary(); private ImageAttributes _attributes = new ImageAttributes(); private CompositingMode _compositingMode = CompositingMode.SourceOver; private Color? _defaultBackground; private Color _defaultForeground = Color.White; private int _defaultPixelFont = 1; // = "gens" private Color? _defaultTextBackground = Color.FromArgb(128, 0, 0, 0); private DisplaySurface _GUISurface; private Padding _padding = new Padding(0); public bool HasGUISurface => _GUISurface != null; private SolidBrush GetBrush(Color color) => _solidBrushes.TryGetValue(color, out var b) ? b : (_solidBrushes[color] = new SolidBrush(color)); private Pen GetPen(Color color) => _pens.TryGetValue(color, out var p) ? p : (_pens[color] = new Pen(color)); private Graphics GetGraphics() { var g = _GUISurface?.GetGraphics() ?? Graphics.FromImage(_nullGraphicsBitmap); // we don't like CoreComm, right? Someone should find a different way to do this then. var tx = Emulator.CoreComm.ScreenLogicalOffsetX; var ty = Emulator.CoreComm.ScreenLogicalOffsetY; if (tx != 0 || ty != 0) { var transform = g.Transform; transform.Translate(-tx, -ty); g.Transform = transform; } return g; } public void ToggleCompositingMode() => _compositingMode = 1 - _compositingMode; public ImageAttributes GetAttributes() => _attributes; public void SetAttributes(ImageAttributes a) => _attributes = a; public void DrawNew(string name, bool clear) { try { DrawFinish(); _GUISurface = GlobalWin.DisplayManager.LockLuaSurface(name, clear); } catch (InvalidOperationException ex) { LogCallback(ex.ToString()); } } public void DrawFinish() { if (_GUISurface != null) GlobalWin.DisplayManager.UnlockLuaSurface(_GUISurface); _GUISurface = null; } public void SetPadding(int all) => _padding = new Padding(all); public void SetPadding(int x, int y) => _padding = new Padding(x / 2, y / 2, x / 2 + x & 1, y / 2 + y & 1); public void SetPadding(int l, int t, int r, int b) => _padding = new Padding(l, t, r, b); public (int Left, int Top, int Right, int Bottom) GetPadding() => (_padding.Left, _padding.Top, _padding.Right, _padding.Bottom); public void AddMessage(string message) => GlobalWin.OSD.AddMessage(message); public void ClearGraphics() { _GUISurface.Clear(); DrawFinish(); } public void ClearText() => GlobalWin.OSD.ClearGuiText(); public void SetDefaultForegroundColor(Color color) => _defaultForeground = color; public void SetDefaultBackgroundColor(Color color) => _defaultBackground = color; public Color? GetDefaultTextBackground() => _defaultTextBackground; public void SetDefaultTextBackground(Color color) => _defaultTextBackground = color; public void SetDefaultPixelFont(string fontfamily) { switch (fontfamily) { case "fceux": case "0": _defaultPixelFont = 0; break; case "gens": case "1": _defaultPixelFont = 1; break; default: LogCallback($"Unable to find font family: {fontfamily}"); return; } } public void DrawBezier(Point p1, Point p2, Point p3, Point p4, Color? color = null) { try { using var g = GetGraphics(); g.CompositingMode = _compositingMode; g.DrawBezier(GetPen(color ?? _defaultForeground), p1, p2, p3, p4); } catch (Exception) { // ignored } } public void DrawBeziers(Point[] points, Color? color = null) { try { using var g = GetGraphics(); g.CompositingMode = _compositingMode; g.DrawBeziers(GetPen(color ?? _defaultForeground), points); } catch (Exception) { // ignored } } public void DrawBox(int x, int y, int x2, int y2, Color? line = null, Color? background = null) { try { float w; if (x < x2) { w = x2 - x; } else { x2 = x - x2; x -= x2; w = Math.Max(x2, 0.1f); } float h; if (y < y2) { h = y2 - y; } else { y2 = y - y2; y -= y2; h = Math.Max(y2, 0.1f); } using var g = GetGraphics(); g.CompositingMode = _compositingMode; g.DrawRectangle(GetPen(line ?? _defaultForeground), x, y, w, h); var bg = background ?? _defaultBackground; if (bg != null) g.FillRectangle(GetBrush(bg.Value), x + 1, y + 1, Math.Max(w - 1, 0), Math.Max(h - 1, 0)); } catch (Exception) { // need to stop the script from here } } public void DrawEllipse(int x, int y, int width, int height, Color? line = null, Color? background = null) { try { using var g = GetGraphics(); var bg = background ?? _defaultBackground; if (bg != null) g.FillEllipse(GetBrush(bg.Value), x, y, width, height); g.CompositingMode = _compositingMode; g.DrawEllipse(GetPen(line ?? _defaultForeground), x, y, width, height); } catch (Exception) { // need to stop the script from here } } public void DrawIcon(string path, int x, int y, int? width = null, int? height = null) { try { if (!File.Exists(path)) { AddMessage($"File not found: {path}"); return; } using var g = GetGraphics(); g.CompositingMode = _compositingMode; g.DrawIcon( width != null && height != null ? new Icon(path, width.Value, height.Value) : new Icon(path), x, y ); } catch (Exception) { // ignored } } public void DrawImage(string path, int x, int y, int? width = null, int? height = null, bool cache = true) { if (!File.Exists(path)) { LogCallback($"File not found: {path}"); return; } using var g = GetGraphics(); var isCached = _imageCache.ContainsKey(path); var img = isCached ? _imageCache[path] : Image.FromFile(path); if (!isCached && cache) _imageCache[path] = img; g.CompositingMode = _compositingMode; g.DrawImage( img, new Rectangle(x, y, width ?? img.Width, height ?? img.Height), 0, 0, img.Width, img.Height, GraphicsUnit.Pixel, _attributes ); } public void ClearImageCache() { foreach (var image in _imageCache) image.Value.Dispose(); _imageCache.Clear(); } public void DrawImageRegion(string path, int source_x, int source_y, int source_width, int source_height, int dest_x, int dest_y, int? dest_width = null, int? dest_height = null) { if (!File.Exists(path)) { LogCallback($"File not found: {path}"); return; } using var g = GetGraphics(); g.CompositingMode = _compositingMode; g.DrawImage( _imageCache.TryGetValue(path, out var img) ? img : (_imageCache[path] = Image.FromFile(path)), new Rectangle(dest_x, dest_y, dest_width ?? source_width, dest_height ?? source_height), source_x, source_y, source_width, source_height, GraphicsUnit.Pixel, _attributes ); } public void DrawLine(int x1, int y1, int x2, int y2, Color? color = null) { using var g = GetGraphics(); g.CompositingMode = _compositingMode; g.DrawLine(GetPen(color ?? _defaultForeground), x1, y1, x2, y2); } public void DrawAxis(int x, int y, int size, Color? color = null) { DrawLine(x + size, y, x - size, y, color ?? _defaultForeground); DrawLine(x, y + size, x, y - size, color ?? _defaultForeground); } public void DrawPie(int x, int y, int width, int height, int startangle, int sweepangle, Color? line = null, Color? background = null) { using var g = GetGraphics(); g.CompositingMode = _compositingMode; var bg = background ?? _defaultBackground; if (bg != null) g.FillPie(GetBrush(bg.Value), x, y, width, height, startangle, sweepangle); g.DrawPie(GetPen(line ?? _defaultForeground), x + 1, y + 1, width - 1, height - 1, startangle, sweepangle); } public void DrawPixel(int x, int y, Color? color = null) { try { using var g = GetGraphics(); g.DrawLine(GetPen(color ?? _defaultForeground), x, y, x + 0.1F, y); } catch (Exception) { // ignored } } public void DrawPolygon(Point[] points, Color? line = null, Color? background = null) { try { using var g = GetGraphics(); g.DrawPolygon(GetPen(line ?? _defaultForeground), points); var bg = background ?? _defaultBackground; if (bg != null) g.FillPolygon(GetBrush(bg.Value), points); } catch (Exception) { // ignored } } public void DrawRectangle(int x, int y, int width, int height, Color? line = null, Color? background = null) { using var g = GetGraphics(); var w = Math.Max(width, 0.1F); var h = Math.Max(height, 0.1F); g.DrawRectangle(GetPen(line ?? _defaultForeground), x, y, w, h); var bg = background ?? _defaultBackground; if (bg != null) g.FillRectangle(GetBrush(bg.Value), x + 1, y + 1, Math.Max(w - 1, 0), Math.Max(h - 1, 0)); } public void DrawString(int x, int y, string message, Color? forecolor = null, Color? backcolor = null, int? fontsize = null, string fontfamily = null, string fontstyle = null, string horizalign = null, string vertalign = null) { try { var family = fontfamily != null ? new FontFamily(fontfamily) : FontFamily.GenericMonospace; var fstyle = fontstyle?.ToLower() switch { "bold" => FontStyle.Bold, "italic" => FontStyle.Italic, "strikethrough" => FontStyle.Strikeout, "underline" => FontStyle.Underline, _ => FontStyle.Regular }; using var g = GetGraphics(); // The text isn't written out using GenericTypographic, so measuring it using GenericTypographic seemed to make it worse. // And writing it out with GenericTypographic just made it uglier. :p var font = new Font(family, fontsize ?? 12, fstyle, GraphicsUnit.Pixel); var sizeOfText = g.MeasureString(message, font, 0, new StringFormat(StringFormat.GenericDefault)).ToSize(); switch (horizalign?.ToLower()) { default: case "left": break; case "center": case "middle": x -= sizeOfText.Width / 2; break; case "right": x -= sizeOfText.Width; break; } switch (vertalign?.ToLower()) { default: case "top": break; case "center": case "middle": y -= sizeOfText.Height / 2; break; case "bottom": y -= sizeOfText.Height; break; } var bg = backcolor ?? _defaultBackground; if (bg != null) { var brush = GetBrush(bg.Value); for (var xd = -1; xd <= 1; xd++) for (var yd = -1; yd <= 1; yd++) { g.DrawString(message, font, brush, x + xd, y + yd); } } g.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit; g.DrawString(message, font, GetBrush(forecolor ?? _defaultForeground), x, y); } catch (Exception) { // ignored } } public void DrawText(int x, int y, string message, Color? forecolor = null, Color? backcolor = null, string fontfamily = null) { try { int index; switch (fontfamily) { case "fceux": case "0": index = 0; break; case "gens": case "1": index = 1; break; default: if (!string.IsNullOrEmpty(fontfamily)) // not a typo { LogCallback($"Unable to find font family: {fontfamily}"); return; } index = _defaultPixelFont; break; } using var g = GetGraphics(); var font = new Font(GlobalWin.DisplayManager.CustomFonts.Families[index], 8, FontStyle.Regular, GraphicsUnit.Pixel); var sizeOfText = g.MeasureString( message, font, 0, new StringFormat(StringFormat.GenericTypographic) { FormatFlags = StringFormatFlags.MeasureTrailingSpaces } ).ToSize(); if (backcolor.HasValue) g.FillRectangle(GetBrush(backcolor.Value), new Rectangle(new Point(x, y), sizeOfText + new Size(1, 0))); g.TextRenderingHint = TextRenderingHint.SingleBitPerPixelGridFit; g.DrawString(message, font, GetBrush(forecolor ?? _defaultForeground), x, y); } catch (Exception) { // ignored } } public void Text(int x, int y, string message, Color? forecolor = null, string anchor = null) { int a = default; if (!string.IsNullOrEmpty(anchor)) { a = anchor switch { "0" => 0, "topleft" => 0, "1" => 1, "topright" => 1, "2" => 2, "bottomleft" => 2, "3" => 3, "bottomright" => 3, _ => default }; } else { x -= Emulator.CoreComm.ScreenLogicalOffsetX; y -= Emulator.CoreComm.ScreenLogicalOffsetY; } var pos = new MessagePosition{ X = x, Y = y, Anchor = (MessagePosition.AnchorType)a }; GlobalWin.OSD.AddGuiText(message, pos, Color.Black, forecolor ?? Color.White); } public void Dispose() { foreach (var brush in _solidBrushes.Values) brush.Dispose(); foreach (var brush in _pens.Values) brush.Dispose(); } } }