From 648ad2536cad05b537e0de7709f772e37ef0d12b Mon Sep 17 00:00:00 2001 From: voytas Date: Fri, 12 Dec 2025 21:56:54 +0000 Subject: [PATCH 01/21] Better HexViewer and memory view --- Directory.Packages.props | 1 + .../Breakpoints/BreakpointHandler.cs | 2 +- .../Controls/Hex/HexCellSelectedEventArgs.cs | 8 + .../Controls/Hex/HexViewer.axaml | 27 ++ .../Controls/Hex/HexViewer.cs | 366 ++++++++++++++++++ .../Controls/Hex/HexViewerHeader.cs | 52 +++ .../Controls/Hex/HexViewerPanel.cs | 88 +++++ .../Controls/Hex/HexViewerRow.cs | 149 +++++++ .../Controls/Hex/RowTextBuilder.cs | 143 +++++++ src/Spectron.Debugger/Controls/Memory.axaml | 20 +- .../Controls/Memory.axaml.cs | 15 + .../Overlays/GoToAddressOverlay.axaml | 45 +++ .../Overlays/GoToAddressOverlay.axaml.cs | 45 +++ .../Spectron.Debugger.csproj | 1 + .../ViewModels/CodeListViewModel.cs | 2 +- .../ViewModels/DebuggerViewModel.cs | 4 +- .../ViewModels/ImmediateViewModel.cs | 2 +- .../ViewModels/MemoryViewModel.cs | 35 +- .../Overlays/GoToAddressOverlayViewModel.cs | 32 ++ src/Spectron.Debugger/Views/MemoryView.axaml | 36 ++ .../Views/MemoryView.axaml.cs | 24 ++ .../Devices/Memory/IEmulatorMemory.cs | 2 +- .../Devices/Memory/Memory128K.cs | 2 +- .../Devices/Memory/Memory16K.cs | 2 +- .../Devices/Memory/Memory48K.cs | 2 +- src/Spectron.Emulation/Emulator.cs | 2 +- .../Extensions/MemoryExtensions.cs | 101 ++--- src/Spectron/App.axaml | 2 +- src/Spectron/Controls/MainMenu.axaml | 5 +- .../Messages/ShowMemoryViewMessage.cs | 5 + .../MainWindowViewModel.Debugger.cs | 11 + .../ViewModels/MainWindowViewModel.cs | 3 + src/Spectron/Views/MainWindow.axaml.cs | 4 + 33 files changed, 1173 insertions(+), 65 deletions(-) create mode 100644 src/Spectron.Debugger/Controls/Hex/HexCellSelectedEventArgs.cs create mode 100644 src/Spectron.Debugger/Controls/Hex/HexViewer.axaml create mode 100644 src/Spectron.Debugger/Controls/Hex/HexViewer.cs create mode 100644 src/Spectron.Debugger/Controls/Hex/HexViewerHeader.cs create mode 100644 src/Spectron.Debugger/Controls/Hex/HexViewerPanel.cs create mode 100644 src/Spectron.Debugger/Controls/Hex/HexViewerRow.cs create mode 100644 src/Spectron.Debugger/Controls/Hex/RowTextBuilder.cs create mode 100644 src/Spectron.Debugger/Controls/Overlays/GoToAddressOverlay.axaml create mode 100644 src/Spectron.Debugger/Controls/Overlays/GoToAddressOverlay.axaml.cs create mode 100644 src/Spectron.Debugger/ViewModels/Overlays/GoToAddressOverlayViewModel.cs create mode 100644 src/Spectron.Debugger/Views/MemoryView.axaml create mode 100644 src/Spectron.Debugger/Views/MemoryView.axaml.cs create mode 100644 src/Spectron/Messages/ShowMemoryViewMessage.cs diff --git a/Directory.Packages.props b/Directory.Packages.props index 80a01227..bdee193b 100644 --- a/Directory.Packages.props +++ b/Directory.Packages.props @@ -36,6 +36,7 @@ + diff --git a/src/Spectron.Debugger/Breakpoints/BreakpointHandler.cs b/src/Spectron.Debugger/Breakpoints/BreakpointHandler.cs index 24808abc..46d5b8c8 100644 --- a/src/Spectron.Debugger/Breakpoints/BreakpointHandler.cs +++ b/src/Spectron.Debugger/Breakpoints/BreakpointHandler.cs @@ -69,7 +69,7 @@ private void BeforeInstruction(Word pc) BreakpointHit?.Invoke(this, new BreakpointHitEventArgs(_previousAddress)); } - private void MemoryOnMemoryUpdated(Word address) + private void MemoryOnMemoryUpdated(Word address, byte value) { IsBreakpointHit = BreakpointManager.IsMemoryBreakpointHit(address, _emulator.Memory); diff --git a/src/Spectron.Debugger/Controls/Hex/HexCellSelectedEventArgs.cs b/src/Spectron.Debugger/Controls/Hex/HexCellSelectedEventArgs.cs new file mode 100644 index 00000000..0cb75f22 --- /dev/null +++ b/src/Spectron.Debugger/Controls/Hex/HexCellSelectedEventArgs.cs @@ -0,0 +1,8 @@ +namespace OldBit.Spectron.Debugger.Controls.Hex; + +internal sealed class HexCellSelectedEventArgs(int rowIndex, int position) : EventArgs +{ + internal int Position { get; } = position; + + internal int RowIndex { get; } = rowIndex; +} \ No newline at end of file diff --git a/src/Spectron.Debugger/Controls/Hex/HexViewer.axaml b/src/Spectron.Debugger/Controls/Hex/HexViewer.axaml new file mode 100644 index 00000000..b07af558 --- /dev/null +++ b/src/Spectron.Debugger/Controls/Hex/HexViewer.axaml @@ -0,0 +1,27 @@ + + + + + + + + + + + + + + + + \ No newline at end of file diff --git a/src/Spectron.Debugger/Controls/Hex/HexViewer.cs b/src/Spectron.Debugger/Controls/Hex/HexViewer.cs new file mode 100644 index 00000000..e6eebf0f --- /dev/null +++ b/src/Spectron.Debugger/Controls/Hex/HexViewer.cs @@ -0,0 +1,366 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.Media; + +namespace OldBit.Spectron.Debugger.Controls.Hex; + +public class HexViewer : ContentControl +{ + public static readonly StyledProperty RowHeightProperty = + AvaloniaProperty.Register(nameof(RowHeight), 20); + + public static readonly StyledProperty BytesPerRowProperty = + AvaloniaProperty.Register(nameof(BytesPerRow), 16, inherits: true); + + public static readonly StyledProperty IsOffsetVisibleProperty = + AvaloniaProperty.Register(nameof(IsOffsetVisible), true, inherits: true); + + public static readonly StyledProperty IsHeaderVisibleProperty = + AvaloniaProperty.Register(nameof(IsHeaderVisible), true); + + public static readonly StyledProperty GroupSizeProperty = + AvaloniaProperty.Register(nameof(GroupSize), 8); + + public static readonly DirectProperty DataProperty = + AvaloniaProperty.RegisterDirect( + nameof(Data), + getter: o => o.Data, + setter: (o, v) => o.Data = v, + unsetValue: []); + + public static readonly StyledProperty SelectedIndexProperty = + AvaloniaProperty.Register(nameof(SelectedIndex), -1); + + internal static readonly StyledProperty TypefaceProperty = + AvaloniaProperty.Register(nameof(Typeface), inherits: true); + + internal static readonly StyledProperty RowTextBuilderProperty = + AvaloniaProperty.Register(nameof(RowTextBuilder), inherits: true); + + public int RowHeight + { + get => GetValue(RowHeightProperty); + set => SetValue(RowHeightProperty, value); + } + + public int BytesPerRow + { + get => GetValue(BytesPerRowProperty); + set => SetValue(BytesPerRowProperty, value); + } + + public bool IsOffsetVisible + { + get => GetValue(IsOffsetVisibleProperty); + set => SetValue(IsOffsetVisibleProperty, value); + } + + public bool IsHeaderVisible + { + get => GetValue(IsHeaderVisibleProperty); + set => SetValue(IsHeaderVisibleProperty, value); + } + + public int GroupSize + { + get => GetValue(GroupSizeProperty); + set => SetValue(GroupSizeProperty, value); + } + + public byte[] Data + { + get; + set + { + if (value.Length == Data.Length) + { + Array.Copy(value, Data, value.Length); + _hexPanel.InvalidateVisual(); + } + else + { + if (!SetAndRaise(DataProperty, ref field, value)) + { + return; + } + + SelectedIndex = -1; + _hexPanel.Clear(); + UpdateView(); + } + } + } = []; + + internal Typeface Typeface + { + get => GetValue(TypefaceProperty); + private set => SetValue(TypefaceProperty, value); + } + + internal RowTextBuilder RowTextBuilder + { + get => GetValue(RowTextBuilderProperty); + set => SetValue(RowTextBuilderProperty, value); + } + + public int SelectedIndex + { + get => GetValue(SelectedIndexProperty); + set => SetValue(SelectedIndexProperty, value); + } + + private readonly HexViewerHeader _header; + private readonly ScrollViewer _scrollViewer; + private readonly HexViewerPanel _hexPanel; + + private int CurrentRowIndex => SelectedIndex / BytesPerRow; + private int VisibleRowCount => (int)(_scrollViewer.Viewport.Height / RowHeight); + private int PageHeight => VisibleRowCount * RowHeight; + + internal double RowWidth { get; private set; } + + public HexViewer() + { + Focusable = true; + + AffectsMeasure(RowHeightProperty); + AffectsMeasure(BytesPerRowProperty); + + _header = new HexViewerHeader() + { + Height = RowHeight, + }; + _hexPanel = new HexViewerPanel(this); + + _scrollViewer = new ScrollViewer() + { + HorizontalScrollBarVisibility = ScrollBarVisibility.Auto, + VerticalScrollBarVisibility = ScrollBarVisibility.Auto, + Content = _hexPanel, + }; + + HorizontalContentAlignment = HorizontalAlignment.Stretch; + VerticalContentAlignment = VerticalAlignment.Stretch; + + var headerHost = new Border + { + ClipToBounds = true, + Child = _header + }; + + var dockPanel = new DockPanel(); + dockPanel.Children.Add(headerHost); + dockPanel.Children.Add(_scrollViewer); + DockPanel.SetDock(headerHost, Dock.Top); + + this.GetObservable(FontFamilyProperty).Subscribe(_ => Invalidate()); + this.GetObservable(FontSizeProperty).Subscribe(_ => Invalidate()); + this.GetObservable(FontStyleProperty).Subscribe(_ => Invalidate()); + this.GetObservable(FontStretchProperty).Subscribe(_ => Invalidate()); + this.GetObservable(IsOffsetVisibleProperty).Subscribe(_ => Invalidate()); + this.GetObservable(BytesPerRowProperty).Subscribe(_ => Invalidate()); + this.GetObservable(IsHeaderVisibleProperty).Subscribe(_ => _header.IsVisible = IsHeaderVisible); + this.GetObservable(SelectedIndexProperty).Subscribe(_ => UpdateSelected()); + + _scrollViewer.GetObservable(ScrollViewer.OffsetProperty).Subscribe(offset => + { + _header.RenderTransform = new TranslateTransform(-offset.X, 0); + UpdateView(); + }); + _scrollViewer.GetObservable(ScrollViewer.ViewportProperty).Subscribe(_ => UpdateView()); + + Content = dockPanel; + } + + public void UpdateValues(int offset, params byte[] values) + { + var rowIndices = new HashSet(); + + for (var i = 0; i < values.Length; i++) + { + if (offset + i >= Data.Length) + { + break; + } + + Data[offset + i] = values[i]; + + var rowIndex = offset / BytesPerRow; + rowIndices.Add(rowIndex); + } + + foreach (var rowIndex in rowIndices) + { + _hexPanel.InvalidateRow(rowIndex); + } + } + + protected override void OnKeyDown(KeyEventArgs e) + { + switch (e.Key) + { + case Key.Left: + UpdateSelectedIndex(-1); + break; + + case Key.Right: + UpdateSelectedIndex(1); + break; + + case Key.Up: + UpdateSelectedIndex(-BytesPerRow); + + if (!IsRowVisible(CurrentRowIndex)) + { + _scrollViewer.Offset = new Vector(_scrollViewer.Offset.X, CurrentRowIndex * RowHeight); + } + break; + + case Key.Down: + UpdateSelectedIndex(BytesPerRow); + + if (!IsRowVisible(CurrentRowIndex)) + { + var viewportRows = (int)(_scrollViewer.Viewport.Height / RowHeight); + var targetTopRow = CurrentRowIndex - Math.Max(0, viewportRows) + 1; + + _scrollViewer.Offset = new Vector(_scrollViewer.Offset.X, targetTopRow * RowHeight); + } + break; + + case Key.Home: + SelectedIndex = 0; + + _scrollViewer.ScrollToHome(); + break; + + case Key.End: + SelectedIndex = Data.Length - 1; + + _scrollViewer.ScrollToEnd(); + break; + + case Key.PageUp: + UpdateSelectedIndex(-VisibleRowCount * BytesPerRow); + + _scrollViewer.Offset = new Vector(_scrollViewer.Offset.X, _scrollViewer.Offset.Y - PageHeight); + break; + + case Key.PageDown: + UpdateSelectedIndex(VisibleRowCount * BytesPerRow); + + _scrollViewer.Offset = new Vector(_scrollViewer.Offset.X, _scrollViewer.Offset.Y + PageHeight); + break; + } + + base.OnKeyDown(e); + } + + private HexViewerRow CreateRow(int rowIndex) + { + var startOffset = rowIndex * BytesPerRow; + var endOffset = startOffset + BytesPerRow; + + if (endOffset > Data.Length) + { + endOffset = Data.Length; + } + + var row = new HexViewerRow + { + RowIndex = rowIndex, + Offset = startOffset, + Data = new ArraySegment(Data, startOffset, endOffset - startOffset), + Height = RowHeight, + Width = RowWidth, + SelectedIndex = CurrentRowIndex == rowIndex ? SelectedIndex % BytesPerRow : -1 + }; + + row.CellSelected += (_, e) => SelectedIndex = e.RowIndex * BytesPerRow + e.Position; + + return row; + } + + private void UpdateView() + { + var offset = _scrollViewer.Offset.Y; + var viewportHeight = _scrollViewer.Viewport.Height; + + var startIndex = Math.Max(0, (int)(offset/ RowHeight)); + var endIndex = Math.Min( + (Data.Length + BytesPerRow - 1) / BytesPerRow, + (int)((offset + viewportHeight) / RowHeight) + 2); + + if (Data.Length == 0) + { + _hexPanel.Clear(); + return; + } + + _hexPanel.RemoveNotVisibleRows(startIndex, endIndex); + + for (var rowIndex = startIndex; rowIndex < endIndex; rowIndex++) + { + if (_hexPanel.ContainsRow(rowIndex)) + { + continue; + } + + var row = CreateRow(rowIndex); + _hexPanel.Add(row); + } + + _hexPanel.InvalidateArrange(); + } + + private void UpdateSelected() => _hexPanel.UpdateSelected( + SelectedIndex == -1 ? -1 : CurrentRowIndex, + SelectedIndex == -1 ? -1 : SelectedIndex % BytesPerRow); + + private bool IsRowVisible(int rowIndex) + { + var rowTop = rowIndex * RowHeight; + var rowBottom = rowTop + RowHeight; + + var viewportTop = _scrollViewer.Offset.Y; + var viewportBottom = viewportTop + _scrollViewer.Viewport.Height; + + return rowTop >= viewportTop && rowBottom <= viewportBottom; + } + + private void UpdateTypeface() + { + Typeface = new Typeface(FontFamily, FontStyle, FontWeight, FontStretch); + + var textLength = RowTextBuilder.CalculateTotalLength(IsOffsetVisible, GroupSize, BytesPerRow); + var formattedText = HexViewerRow.CreateFormattedText("X", Typeface, FontSize, Foreground); + + RowWidth = textLength * formattedText.Width; + RowTextBuilder = new RowTextBuilder(IsOffsetVisible, GroupSize, BytesPerRow, formattedText.Width); + } + + private void Invalidate() + { + UpdateTypeface(); + + _hexPanel.InvalidateVisual(); + _header.InvalidateVisual(); + } + + private void UpdateSelectedIndex(int amount) + { + SelectedIndex += amount; + + if (SelectedIndex < 0) + { + SelectedIndex = 0; + } + else if (SelectedIndex > Data.Length - 1) + { + SelectedIndex = Data.Length - 1; + } + } +} \ No newline at end of file diff --git a/src/Spectron.Debugger/Controls/Hex/HexViewerHeader.cs b/src/Spectron.Debugger/Controls/Hex/HexViewerHeader.cs new file mode 100644 index 00000000..7e71edd3 --- /dev/null +++ b/src/Spectron.Debugger/Controls/Hex/HexViewerHeader.cs @@ -0,0 +1,52 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Media; + +namespace OldBit.Spectron.Debugger.Controls.Hex; + +internal sealed class HexViewerHeader : Control +{ + internal static readonly StyledProperty ForegroundProperty = + TemplatedControl.ForegroundProperty.AddOwner(); + + private static readonly StyledProperty FontSizeProperty = + TemplatedControl.FontSizeProperty.AddOwner(); + + private static readonly StyledProperty TypefaceProperty = + Spectron.Debugger.Controls.Hex.HexViewer.TypefaceProperty.AddOwner(); + + private static readonly StyledProperty RowTextBuilderProperty = + Spectron.Debugger.Controls.Hex.HexViewer.RowTextBuilderProperty.AddOwner(); + + internal IBrush? Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + private double FontSize => GetValue(FontSizeProperty); + + private Typeface Typeface => GetValue(TypefaceProperty); + + private RowTextBuilder RowTextBuilder => GetValue(RowTextBuilderProperty); + + public override void Render(DrawingContext context) + { + var formattedText = GetFormattedText(); + var rect = new Rect(new Size(formattedText.Width, Bounds.Height)); + + context.FillRectangle(Brushes.Transparent, rect); + + var y = (Bounds.Height - formattedText.Height) / 2; + context.DrawText(formattedText, new Point(0, y)); + } + + private FormattedText GetFormattedText() + { + var text = RowTextBuilder.BuildHeader(); + var formattedText = HexViewerRow.CreateFormattedText(text, Typeface, FontSize, Foreground); + + return formattedText; + } +} \ No newline at end of file diff --git a/src/Spectron.Debugger/Controls/Hex/HexViewerPanel.cs b/src/Spectron.Debugger/Controls/Hex/HexViewerPanel.cs new file mode 100644 index 00000000..b4401190 --- /dev/null +++ b/src/Spectron.Debugger/Controls/Hex/HexViewerPanel.cs @@ -0,0 +1,88 @@ +using Avalonia; +using Avalonia.Controls; + +namespace OldBit.Spectron.Debugger.Controls.Hex; + +internal class HexViewerPanel(Spectron.Debugger.Controls.Hex.HexViewer parent) : Panel +{ + private readonly Dictionary _visibleRows = []; + + internal void Add(HexViewerRow row) + { + Children.Add(row); + _visibleRows.Add(row.RowIndex, row); + } + + internal bool ContainsRow(int rowIndex) => _visibleRows.ContainsKey(rowIndex); + + internal void Clear() + { + _visibleRows.Clear(); + Children.Clear(); + } + + internal void RemoveNotVisibleRows(int startIndex, int endIndex) + { + var removeIndices = _visibleRows.Where(row => row.Key > endIndex || row.Key < startIndex) + .Select(row => row.Key) + .ToList(); + + foreach (var index in removeIndices) + { + Children.Remove(_visibleRows[index]); + _visibleRows.Remove(index); + } + } + + internal void UpdateSelected(int rowIndex, int position) + { + foreach (var visibleRow in _visibleRows.Values) + { + visibleRow.SelectedIndex = -1; + } + + if (!_visibleRows.TryGetValue(rowIndex, out var row)) + { + return; + } + + row.SelectedIndex = position; + } + + internal new void InvalidateVisual() + { + foreach (var row in _visibleRows.Values) + { + row.InvalidateVisual(); + } + + base.InvalidateVisual(); + } + + internal void InvalidateRow(int rowIndex) + { + if (_visibleRows.TryGetValue(rowIndex, out var row)) + { + row.InvalidateVisual(); + } + } + + protected override Size MeasureOverride(Size availableSize) + { + var rowCount = (parent.Data.Length + parent.BytesPerRow - 1) / parent.BytesPerRow; + double totalHeight = rowCount * parent.RowHeight; + + return new Size(parent.RowWidth, totalHeight); + } + + protected override Size ArrangeOverride(Size finalSize) + { + foreach (var (rowIndex, row) in _visibleRows) + { + var rect = new Rect(0, rowIndex * parent.RowHeight, finalSize.Width, parent.RowHeight); + row.Arrange(rect); + } + + return finalSize; + } +} \ No newline at end of file diff --git a/src/Spectron.Debugger/Controls/Hex/HexViewerRow.cs b/src/Spectron.Debugger/Controls/Hex/HexViewerRow.cs new file mode 100644 index 00000000..d66a60cb --- /dev/null +++ b/src/Spectron.Debugger/Controls/Hex/HexViewerRow.cs @@ -0,0 +1,149 @@ +using System.Globalization; +using Avalonia; +using Avalonia.Controls; +using Avalonia.Controls.Primitives; +using Avalonia.Input; +using Avalonia.Layout; +using Avalonia.Media; + +namespace OldBit.Spectron.Debugger.Controls.Hex; + +public sealed class HexViewerRow : Control +{ + public static readonly StyledProperty AlternatingRowBackgroundProperty = + AvaloniaProperty.Register(nameof(AlternatingRowBackground)); + + public static readonly StyledProperty SelectedBackgroundProperty = + AvaloniaProperty.Register(nameof(SelectedBackground)); + + public static readonly StyledProperty ForegroundProperty = + TemplatedControl.ForegroundProperty.AddOwner(); + + public static readonly StyledProperty OffsetForegroundProperty = + AvaloniaProperty.Register(nameof(OffsetForeground)); + + public static readonly StyledProperty SelectedIndexProperty = + AvaloniaProperty.Register(nameof(SelectedIndex), -1); + + private static readonly StyledProperty BytesPerRowProperty = + Spectron.Debugger.Controls.Hex.HexViewer.BytesPerRowProperty.AddOwner(); + + private static readonly StyledProperty FontSizeProperty = + TemplatedControl.FontSizeProperty.AddOwner(); + + private static readonly StyledProperty TypefaceProperty = + Spectron.Debugger.Controls.Hex.HexViewer.TypefaceProperty.AddOwner(); + + private static readonly StyledProperty RowTextBuilderProperty = + Spectron.Debugger.Controls.Hex.HexViewer.RowTextBuilderProperty.AddOwner(); + + public IBrush? AlternatingRowBackground + { + get => GetValue(AlternatingRowBackgroundProperty); + set => SetValue(AlternatingRowBackgroundProperty, value); + } + + public IBrush? SelectedBackground + { + get => GetValue(SelectedBackgroundProperty); + set => SetValue(SelectedBackgroundProperty, value); + } + + public IBrush? Foreground + { + get => GetValue(ForegroundProperty); + set => SetValue(ForegroundProperty, value); + } + + public IBrush? OffsetForeground + { + get => GetValue(OffsetForegroundProperty); + set => SetValue(OffsetForegroundProperty, value); + } + + public int SelectedIndex + { + get => GetValue(SelectedIndexProperty); + set => SetValue(SelectedIndexProperty, value); + } + + private int BytesPerRow => GetValue(BytesPerRowProperty); + + private double FontSize => GetValue(FontSizeProperty); + + private Typeface Typeface => GetValue(TypefaceProperty); + + private RowTextBuilder RowTextBuilder => GetValue(RowTextBuilderProperty); + + public required int RowIndex { get; init; } + public required int Offset { get; init; } + public required ArraySegment Data { get; init; } + + internal event EventHandler? CellSelected; + + internal HexViewerRow() + { + HorizontalAlignment = HorizontalAlignment.Left; + + this.GetObservable(SelectedIndexProperty).Subscribe(_ => InvalidateVisual()); + this.GetObservable(TypefaceProperty).Subscribe(_ => InvalidateVisual()); + + VerticalAlignment = VerticalAlignment.Center; + } + + public override void Render(DrawingContext context) + { + var background = AlternatingRowBackground != null && RowIndex % 2 == 0 ? AlternatingRowBackground : null; + var formattedText = GetFormattedText(); + var rect = new Rect(new Size(formattedText.Width, Bounds.Height)); + + context.FillRectangle(background ?? Brushes.Transparent, rect); + + if (SelectedIndex != -1) + { + var layout = RowTextBuilder.GetLayout(SelectedIndex); + rect = new Rect(layout.Position * RowTextBuilder.CharWidth - 4, 1, layout.Width * RowTextBuilder.CharWidth + 8, Height - 2); + context.DrawRectangle(SelectedBackground, null, rect); + + layout = RowTextBuilder.GetLayout(SelectedIndex + BytesPerRow); + rect = new Rect(layout.Position * RowTextBuilder.CharWidth, 1, layout.Width * RowTextBuilder.CharWidth, Height - 2); + context.DrawRectangle(SelectedBackground, null, rect); + } + + if (RowTextBuilder.IsOffsetVisible) + { + formattedText.SetForegroundBrush(OffsetForeground, 0, 5); + } + + var y = (Bounds.Height - formattedText.Height) / 2; + context.DrawText(formattedText, new Point(0, y)); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + var x = e.GetPosition(this).X; + var cellIndex = RowTextBuilder?.GetIndexFromPosition(x); + + if (cellIndex != null) + { + CellSelected?.Invoke(this, new HexCellSelectedEventArgs(RowIndex, cellIndex.Value)); + } + + base.OnPointerPressed(e); + } + + internal static FormattedText CreateFormattedText(string text, Typeface typeface, double fontSize, IBrush? foreground) => new( + text, + CultureInfo.CurrentCulture, + FlowDirection.LeftToRight, + typeface, + fontSize, + foreground); + + private FormattedText GetFormattedText() + { + var text = RowTextBuilder.Build(Data, Offset); + + return CreateFormattedText(text, Typeface, FontSize, Foreground); + } +} \ No newline at end of file diff --git a/src/Spectron.Debugger/Controls/Hex/RowTextBuilder.cs b/src/Spectron.Debugger/Controls/Hex/RowTextBuilder.cs new file mode 100644 index 00000000..f8fdca64 --- /dev/null +++ b/src/Spectron.Debugger/Controls/Hex/RowTextBuilder.cs @@ -0,0 +1,143 @@ +using System.Text; + +namespace OldBit.Spectron.Debugger.Controls.Hex; + +internal record RowTextLayout(int Position, int Width); + +internal sealed class RowTextBuilder +{ + private readonly int _groupSize; + private readonly int _bytesPerRow; + private readonly RowTextLayout[] _layout; + + internal double CharWidth { get; } + internal bool IsOffsetVisible { get; } + + public RowTextBuilder(bool isOffsetVisible, int groupSize, int bytesPerRow, double charWidth) + { + IsOffsetVisible = isOffsetVisible; + _groupSize = groupSize; + _bytesPerRow = bytesPerRow; + CharWidth = charWidth; + + _layout = CreateLayout().ToArray(); + } + + internal string Build(ReadOnlySpan data, int offset) + { + var hex = new StringBuilder(); + var ascii = new StringBuilder(); + + var address = IsOffsetVisible ? $"{offset:X4}: " : string.Empty; + hex.Append(address); + + for (var i = 0; i < _bytesPerRow; i++) + { + if (i > 0) + { + hex.Append(' '); + } + + if (IsGroupSpacer(i)) + { + hex.Append(' '); + } + + hex.Append(i >= data.Length ? " " : data[i].ToString("X2")); + ascii.Append(i >= data.Length ? ' ' : AsciiFormatter(data[i])); + } + + hex.Append(" "); + hex.Append(ascii); + + return hex.ToString(); + } + + internal string BuildHeader() + { + var hex = new StringBuilder(); + + var address = IsOffsetVisible ? " " : string.Empty; + hex.Append(address); + + for (var i = 0; i < _bytesPerRow; i++) + { + if (i > 0) + { + hex.Append(' '); + } + + if (IsGroupSpacer(i)) + { + hex.Append(' '); + } + + hex.Append(i.ToString("X2")); + } + + return hex.ToString(); + } + + internal int? GetIndexFromPosition(double x) + { + var item = _layout + .Select((value, index) => new { Item = value, Index = index }) + .FirstOrDefault(p => + x > p.Item.Position * CharWidth && + x < (p.Item.Position + p.Item.Width) * CharWidth); + + return item?.Index % _bytesPerRow; + } + + internal RowTextLayout GetLayout(int index) => _layout[index]; + + internal static int CalculateTotalLength(bool isOffsetVisible, int groupSize, int bytesPerRow) + { + // Offset "F000: " + var length = isOffsetVisible ? 6 : 0; + + // Hex bytes + length += bytesPerRow * 3; + + // Group extra gap + if (groupSize > 1) + { + length += bytesPerRow / groupSize - 1; + } + + // Before ASCII gap + length += 2; + + // ASCII "ABCDEFGHIJKLMNOP" + length += bytesPerRow; + + return length; + } + + private IEnumerable CreateLayout() + { + var position = IsOffsetVisible ? 6 : 0; + + for (var i = 0; i < _bytesPerRow; i++) + { + if (IsGroupSpacer(i)) + { + position += 1; + } + + yield return new RowTextLayout (position, 2); + position += 3; + } + + position += 1; + + for (var i = 0; i < _bytesPerRow; i++) + { + yield return new RowTextLayout (position++, 1); + } + } + + private bool IsGroupSpacer(int index) => _groupSize > 0 & index > 0 && index % _groupSize == 0; + + private Func AsciiFormatter { get; set; } = b => b is >= 32 and <= 126 ? (char)b : '.'; +} \ No newline at end of file diff --git a/src/Spectron.Debugger/Controls/Memory.axaml b/src/Spectron.Debugger/Controls/Memory.axaml index b62d76ff..a9929d98 100644 --- a/src/Spectron.Debugger/Controls/Memory.axaml +++ b/src/Spectron.Debugger/Controls/Memory.axaml @@ -2,8 +2,8 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:d="http://schemas.microsoft.com/expression/blend/2008" xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006" - xmlns:controls="clr-namespace:OldBit.Spectron.Debugger.Controls" xmlns:viewModels="clr-namespace:OldBit.Spectron.Debugger.ViewModels" + xmlns:hex="clr-namespace:OldBit.Spectron.Debugger.Controls.Hex" x:DataType="viewModels:MemoryViewModel" mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450" x:Class="OldBit.Spectron.Debugger.Controls.Memory"> @@ -19,10 +19,24 @@ - + GroupSize="0" + IsOffsetVisible="True" + IsHeaderVisible="False" + BytesPerRow="8"> + + + + + diff --git a/src/Spectron.Debugger/Controls/Memory.axaml.cs b/src/Spectron.Debugger/Controls/Memory.axaml.cs index e110eebd..e4a30c74 100644 --- a/src/Spectron.Debugger/Controls/Memory.axaml.cs +++ b/src/Spectron.Debugger/Controls/Memory.axaml.cs @@ -1,8 +1,23 @@ using Avalonia.Controls; +using Avalonia.Threading; +using OldBit.Spectron.Debugger.ViewModels; namespace OldBit.Spectron.Debugger.Controls; public partial class Memory : UserControl { public Memory() => InitializeComponent(); + + protected override void OnDataContextChanged(EventArgs e) + { + if (DataContext is not MemoryViewModel viewModel) + { + return; + } + + viewModel.OnMemoryUpdated = (address, value) => + { + Dispatcher.UIThread.Post(() => MemoryView.UpdateValues(address, value)); + }; + } } \ No newline at end of file diff --git a/src/Spectron.Debugger/Controls/Overlays/GoToAddressOverlay.axaml b/src/Spectron.Debugger/Controls/Overlays/GoToAddressOverlay.axaml new file mode 100644 index 00000000..a5b7b8a9 --- /dev/null +++ b/src/Spectron.Debugger/Controls/Overlays/GoToAddressOverlay.axaml @@ -0,0 +1,45 @@ + + + + + + + + + + + + +