BizHawk/BizHawk.Client.EmuHawk/Api/Libraries/GuiApi.cs

505 lines
13 KiB
C#

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<string> logCallback)
{
LogCallback = logCallback;
}
public GuiApi() : this(Console.WriteLine) {}
private readonly Action<string> LogCallback;
private readonly Dictionary<string, Image> _imageCache = new Dictionary<string, Image>();
private readonly Bitmap _nullGraphicsBitmap = new Bitmap(1, 1);
private readonly Dictionary<Color, Pen> _pens = new Dictionary<Color, Pen>();
private readonly Dictionary<Color, SolidBrush> _solidBrushes = new Dictionary<Color, SolidBrush>();
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();
}
}
}