using Avalonia; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Rendering.SceneGraph; using Avalonia.Skia; using Mesen.Config; using Mesen.Utilities; using SkiaSharp; using System; using System.Collections.Generic; using System.Text; namespace Mesen.Debugger.Controls { public partial class HexEditor { class HexViewDrawOperation : ICustomDrawOperation { private HexEditor _he; private List _dataToDraw; private HashSet _fgColors; private Size _letterSize; private int _bytesPerRow; private bool _showStringView; private bool _inStringView; private double _rowHeight; private string _hexFormat; private string _fontFamily; private float _fontSize; private double _stringViewPosition; private IHexEditorDataProvider _dataProvider; private Dictionary _skFillPaints = new Dictionary(); private Dictionary _skBorderPaints = new Dictionary(); private Color _selectedColorOther = ColorHelper.GetColor(Colors.LightBlue); private Color _selectedColor = ColorHelper.GetColor(Colors.LightSkyBlue); private bool _highDensityMode; private SKFontEdging _skiaEdging; private bool _skiaSubpixelSmoothing; public HexViewDrawOperation(HexEditor he, List dataToDraw, HashSet fgColors, FontAntialiasing fontAntialiasing) { _he = he; Bounds = _he.Bounds; _fontFamily = _he.FontFamily.Name; _fontSize = (float)_he.FontSize; _bytesPerRow = _he.BytesPerRow; _hexFormat = _he.HexFormat; _rowHeight = _he.RowHeight; _dataProvider = _he.DataProvider; _dataToDraw = dataToDraw; _fgColors = fgColors; _letterSize = _he.LetterSize; _showStringView = _he.ShowStringView; _inStringView = _he._inStringView; _stringViewPosition = _he.RowWidth + _he.StringViewMargin; _highDensityMode = _he.HighDensityMode; _skiaEdging = fontAntialiasing switch { FontAntialiasing.Disabled => SKFontEdging.Alias, FontAntialiasing.Antialias => SKFontEdging.Antialias, FontAntialiasing.SubPixelAntialias or _ => SKFontEdging.SubpixelAntialias }; _skiaSubpixelSmoothing = fontAntialiasing == FontAntialiasing.SubPixelAntialias; foreach(ByteInfo byteInfo in dataToDraw) { if(!_skFillPaints.ContainsKey(byteInfo.BackColor)) { _skFillPaints[byteInfo.BackColor] = new SKPaint() { Color = new SKColor(ColorHelper.GetColor(byteInfo.BackColor).ToUInt32()) }; } if(!_skBorderPaints.ContainsKey(byteInfo.BorderColor)) { _skBorderPaints[byteInfo.BorderColor] = new SKPaint() { Style = SKPaintStyle.Stroke, Color = new SKColor(ColorHelper.GetColor(byteInfo.BorderColor).ToUInt32()) }; } } } private void SetFontProperties(SKFont font) { font.Edging = _skiaEdging; font.Subpixel = _skiaSubpixelSmoothing; //Fixes layout issues (on Linux) font.Hinting = SKFontHinting.Full; font.LinearMetrics = true; } public Rect Bounds { get; private set; } public void Dispose() { } public bool Equals(ICustomDrawOperation? other) => false; public bool HitTest(Point p) => false; public void Render(ImmediateDrawingContext context) { var leaseFeature = context.PlatformImpl.GetFeature(); using var lease = leaseFeature?.Lease(); var canvas = lease?.SkCanvas; if(canvas == null) { //context.DrawText(Brushes.Black, new Point(), _noSkia.PlatformImpl); } else { canvas.Save(); DrawBackground(canvas); foreach(Color color in _fgColors) { DrawHexView(canvas, color); } if(_showStringView) { canvas.Translate((float)_stringViewPosition, 0); PrepareStringView(); foreach(Color color in _fgColors) { DrawStringView(canvas, color); } } canvas.Restore(); } } private void DrawHexView(SKCanvas canvas, Color color) { SKPaint paint = new SKPaint(); paint.Color = new SKColor(ColorHelper.GetColor(color).ToUInt32()); SKTypeface typeface = SKTypeface.FromFamilyName(_fontFamily); SKFont font = new SKFont(typeface, _fontSize); SetFontProperties(font); using var builder = new SKTextBlobBuilder(); int pos = 0; double drawOffsetY = _highDensityMode ? -_rowHeight * 0.1 : -_rowHeight * 0.2; StringBuilder sb = new StringBuilder(); int row = 0; while(pos < _dataToDraw.Count) { for(int i = 0; i < _bytesPerRow; i++) { if(pos + i >= _dataToDraw.Count) { break; } ByteInfo byteInfo = _dataToDraw[pos + i]; if(byteInfo.ForeColor == color) { sb.Append(byteInfo.Value.ToString(_hexFormat)); } else { sb.Append(" "); } sb.Append(' '); } pos += _bytesPerRow; string rowText = sb.ToString(); int count = font.CountGlyphs(rowText); var buffer = builder.AllocateRun(font, count, 0, (float)(row * _rowHeight + drawOffsetY)); font.GetGlyphs(rowText, buffer.GetGlyphSpan()); row++; sb.Clear(); } SKTextBlob? textToDraw = builder.Build(); if(textToDraw != null) { canvas.DrawText(textToDraw, 0, (float)_rowHeight, paint); } } private void PrepareStringView() { SKFont altFont = new SKFont(SKFontManager.Default.MatchCharacter('あ'), _fontSize); SetFontProperties(altFont); int pos = 0; using var measureText = new SKTextBlobBuilder(); float[] startPositionByByte = new float[_dataToDraw.Count]; float[] endPositionByByte = new float[_dataToDraw.Count]; while(pos < _dataToDraw.Count) { double xPos = 0; int i; int gap = pos % _bytesPerRow; pos -= gap; for(i = gap; i < _bytesPerRow; i++) { if(pos + i >= _dataToDraw.Count) { break; } int index = pos + i; ByteInfo byteInfo = _dataToDraw[index]; UInt64 tblKeyValue = (UInt64)byteInfo.Value; for(int j = 1; j < 8; j++) { if(index + j < _dataToDraw.Count) { tblKeyValue += (UInt64)_dataToDraw[index + j].Value << (8 * j); } } string str = _dataProvider.ConvertValueToString(tblKeyValue, out int keyLength); byteInfo.StringValue = str; byteInfo.StringValueKeyLength = keyLength; int codepoint = Char.ConvertToUtf32(str, 0); if(codepoint > 0x024F) { SKRunBuffer measureBuffer = measureText.AllocateRun(altFont, altFont.CountGlyphs(str), 0, 0); byteInfo.UseAltFont = true; altFont.GetGlyphs(str, measureBuffer.GetGlyphSpan()); startPositionByByte[index] = (float)xPos; xPos += altFont.MeasureText(measureBuffer.GetGlyphSpan()); endPositionByByte[index] = (float)xPos; } else { startPositionByByte[index] = (float)xPos; xPos += _letterSize.Width * str.Length; endPositionByByte[index] = (float)xPos; } for(int j = 1; j < byteInfo.StringValueKeyLength && index + j < startPositionByByte.Length; j++) { startPositionByByte[index + j] = startPositionByByte[index]; endPositionByByte[index + j] = endPositionByByte[index]; } i += (byteInfo.StringValueKeyLength - 1); _dataToDraw[index] = byteInfo; } pos += i; } _he._startPositionByByte = startPositionByByte; _he._endPositionByByte = endPositionByByte; } private void DrawStringView(SKCanvas canvas, Color color) { SKPaint paint = new SKPaint(); paint.Color = new SKColor(ColorHelper.GetColor(color).ToUInt32()); SKTypeface typeface = SKTypeface.FromFamilyName(_fontFamily); SKFont monoFont = new SKFont(typeface, _fontSize); SetFontProperties(monoFont); SKFont altFont = new SKFont(SKFontManager.Default.MatchCharacter('あ'), _fontSize); SetFontProperties(altFont); using var builder = new SKTextBlobBuilder(); int pos = 0; int row = 0; double drawOffsetY = _highDensityMode ? -_rowHeight * 0.1 : -_rowHeight * 0.2; Color selectedColor = _inStringView ? _selectedColor : _selectedColorOther; SKPaint selectedPaint = new SKPaint() { Color = new SKColor(selectedColor.R, selectedColor.G, selectedColor.B, 255) }; SKRect GetRect(int i) => new SKRect( (float)_he._startPositionByByte[i], (float)(row * _rowHeight), (float)_he._endPositionByByte[i], (float)((row + 1) * _rowHeight) ); while(pos < _dataToDraw.Count) { int i; int gap = pos % _bytesPerRow; pos -= gap; for(i = gap; i < _bytesPerRow; i++) { if(pos + i >= _dataToDraw.Count) { break; } ByteInfo byteInfo = _dataToDraw[pos + i]; if(byteInfo.ForeColor == color) { SKFont currentFont = byteInfo.UseAltFont ? altFont : monoFont; SKRect rect = GetRect(pos + i); if(byteInfo.BackColor != Colors.Transparent) { canvas.DrawRect(rect, _skFillPaints[byteInfo.BackColor]); } if(byteInfo.Selected) { canvas.DrawRect(rect, selectedPaint); } if(byteInfo.BorderColor != Colors.Transparent) { rect.Inflate(0, -1); rect.Offset(0, -0.5f); canvas.DrawRect(rect, _skBorderPaints[byteInfo.BorderColor]); } int count = currentFont.CountGlyphs(byteInfo.StringValue); var buffer = builder.AllocateRun(currentFont, count, _he._startPositionByByte[pos + i], (float)(row * _rowHeight + drawOffsetY)); currentFont.GetGlyphs(byteInfo.StringValue, buffer.GetGlyphSpan()); } i += (byteInfo.StringValueKeyLength - 1); } pos += i; row++; } SKTextBlob? textToDraw = builder.Build(); if(textToDraw != null) { canvas.DrawText(textToDraw, 0, (float)_rowHeight, paint); } } private void DrawBackground(SKCanvas canvas) { int pos = 0; int row = 0; Color selectedColor = _inStringView ? _selectedColorOther : _selectedColor; SKPaint selectedPaint = new SKPaint() { Color = new SKColor(selectedColor.R, selectedColor.G, selectedColor.B, 255) }; SKRect GetRect(int start, int end) => new SKRect( (float)(start * 3 * _letterSize.Width), (float)(row * _rowHeight), (float)((end * 3 - 1) * _letterSize.Width), (float)((row + 1) * _rowHeight) ); while(pos < _dataToDraw.Count) { int bgStartPos = -1; int borderStartPos = -1; int selectedStartPos = -1; Color bgColor = Colors.Transparent; Color borderColor = Colors.Transparent; bool selected = false; for(int i = 0; i < _bytesPerRow; i++) { if(pos + i >= _dataToDraw.Count) { break; } ByteInfo byteInfo = _dataToDraw[pos + i]; if(byteInfo.BackColor != bgColor) { if(bgColor != Colors.Transparent && bgStartPos >= 0) { canvas.DrawRect(GetRect(bgStartPos, i), _skFillPaints[bgColor]); bgStartPos = -1; } if(byteInfo.BackColor != Colors.Transparent) { bgStartPos = i; } bgColor = byteInfo.BackColor; } } if(bgStartPos >= 0) { canvas.DrawRect(GetRect(bgStartPos, _bytesPerRow), _skFillPaints[bgColor]); } for(int i = 0; i < _bytesPerRow; i++) { if(pos + i >= _dataToDraw.Count) { break; } ByteInfo byteInfo = _dataToDraw[pos + i]; if(selected != byteInfo.Selected) { if(selectedStartPos >= 0 && selected) { canvas.DrawRect(GetRect(selectedStartPos, i), selectedPaint); selectedStartPos = -1; } if(byteInfo.Selected) { selectedStartPos = i; } selected = byteInfo.Selected; } } if(selectedStartPos >= 0) { canvas.DrawRect(GetRect(selectedStartPos, _bytesPerRow), selectedPaint); } for(int i = 0; i < _bytesPerRow; i++) { if(pos + i >= _dataToDraw.Count) { break; } ByteInfo byteInfo = _dataToDraw[pos + i]; if(byteInfo.BorderColor != borderColor) { if(borderColor != Colors.Transparent && borderStartPos >= 0) { SKRect rect = GetRect(borderStartPos, i); rect.Inflate(0, -1); rect.Offset(0, -0.5f); canvas.DrawRect(rect, _skBorderPaints[borderColor]); borderStartPos = -1; } if(byteInfo.BorderColor != Colors.Transparent) { borderStartPos = i; } borderColor = byteInfo.BorderColor; } } if(borderStartPos >= 0) { SKRect rect = GetRect(borderStartPos, _bytesPerRow); rect.Inflate(0, -1); rect.Offset(0, -0.5f); canvas.DrawRect(rect, _skBorderPaints[borderColor]); } pos += _bytesPerRow; row++; } } } class HexViewDrawRowHeaderOperation : ICustomDrawOperation { private HexEditor _he; private Size _letterSize; private int _bytesPerRow; private double _rowHeight; private string _fontFamily; private float _fontSize; private IHexEditorDataProvider _dataProvider; private double _headerCharLength; private double _rowHeaderWidth; private double _columnHeaderHeight; private Color _headerForeground; private int _firstByte; private bool _highDensityMode; private SKFontEdging _skiaEdging; private bool _skiaSubpixelSmoothing; public HexViewDrawRowHeaderOperation(HexEditor he, FontAntialiasing fontAntialiasing) { _he = he; Bounds = _he.Bounds; _fontFamily = _he.FontFamily.Name; _fontSize = (float)_he.FontSize; _bytesPerRow = _he.BytesPerRow; _rowHeight = _he.RowHeight; _rowHeaderWidth = _he.RowHeaderWidth; _columnHeaderHeight = _he.ColumnHeaderHeight; _headerCharLength = _he.HeaderCharLength; _headerForeground = _he.HeaderForeground.Color; _highDensityMode = _he.HighDensityMode; _dataProvider = _he.DataProvider; _letterSize = _he.LetterSize; _firstByte = _he.TopRow * _bytesPerRow; _skiaEdging = fontAntialiasing switch { FontAntialiasing.Disabled => SKFontEdging.Alias, FontAntialiasing.Antialias => SKFontEdging.Antialias, FontAntialiasing.SubPixelAntialias or _ => SKFontEdging.SubpixelAntialias }; _skiaSubpixelSmoothing = fontAntialiasing == FontAntialiasing.SubPixelAntialias; } public Rect Bounds { get; private set; } public void Dispose() { } public bool Equals(ICustomDrawOperation? other) => false; public bool HitTest(Point p) => false; public void Render(ImmediateDrawingContext context) { var leaseFeature = context.PlatformImpl.GetFeature(); using var lease = leaseFeature?.Lease(); var canvas = lease?.SkCanvas; if(canvas == null) { //context.DrawText(Brushes.Black, new Point(), _noSkia.PlatformImpl); } else { canvas.Save(); canvas.Translate(0, 0); SKPaint paint = new SKPaint(); paint.Color = new SKColor(ColorHelper.GetColor(_headerForeground).ToUInt32()); SKTypeface typeface = SKTypeface.FromFamilyName(_fontFamily); SKFont font = new SKFont(typeface, _fontSize); font.Edging = _skiaEdging; font.Subpixel = _skiaSubpixelSmoothing; //Fixes layout issues (on Linux) font.Hinting = SKFontHinting.Full; font.LinearMetrics = true; using var builder = new SKTextBlobBuilder(); int dataLength = _dataProvider.Length; int bytesPerRow = _bytesPerRow; double rowHeaderWidth = _rowHeaderWidth; double textWidth = _headerCharLength * _letterSize.Width; double xOffset = (rowHeaderWidth - textWidth) / 2; int headerByte = _firstByte; double y = 0; int row = 0; double drawOffsetY = _highDensityMode ? -_rowHeight * 0.1 : -_rowHeight * 0.2; //Draw row headers for each row while(y < Bounds.Height && headerByte < dataLength) { string rowText = headerByte.ToString("X" + _headerCharLength); int count = font.CountGlyphs(rowText); var buffer = builder.AllocateRun(font, count, (float)xOffset, (float)(row * _rowHeight + drawOffsetY)); font.GetGlyphs(rowText, buffer.GetGlyphSpan()); row++; y += _rowHeight; headerByte += bytesPerRow; } SKTextBlob? textToDraw = builder.Build(); if(textToDraw != null) { canvas.DrawText(textToDraw, 0, (float)_rowHeight, paint); } canvas.Restore(); } } } } }