diff --git a/Directory.Packages.props b/Directory.Packages.props index 1c49a52c..bf4bede5 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/DefaultAsciiFormatter.cs b/src/Spectron.Debugger/Controls/Hex/DefaultAsciiFormatter.cs new file mode 100644 index 00000000..ea275ef5 --- /dev/null +++ b/src/Spectron.Debugger/Controls/Hex/DefaultAsciiFormatter.cs @@ -0,0 +1,6 @@ +namespace OldBit.Spectron.Debugger.Controls.Hex; + +public sealed class DefaultAsciiFormatter : IAsciiFormatter +{ + public char Format(byte b) => b is >= 32 and <= 126 ? (char)b : '.'; +} \ No newline at end of file diff --git a/src/Spectron.Debugger/Controls/Hex/HexCellClickedEventArgs.cs b/src/Spectron.Debugger/Controls/Hex/HexCellClickedEventArgs.cs new file mode 100644 index 00000000..d4556a80 --- /dev/null +++ b/src/Spectron.Debugger/Controls/Hex/HexCellClickedEventArgs.cs @@ -0,0 +1,10 @@ +namespace OldBit.Spectron.Debugger.Controls.Hex; + +internal sealed class HexCellClickedEventArgs(int rowIndex, int position, bool isShiftPressed) : EventArgs +{ + internal int Position { get; } = position; + + internal int RowIndex { get; } = rowIndex; + + internal bool IsShiftPressed { get; } = isShiftPressed; +} 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..fb0be09c --- /dev/null +++ b/src/Spectron.Debugger/Controls/Hex/HexViewer.cs @@ -0,0 +1,513 @@ +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), defaultValue: 20); + + public static readonly StyledProperty BytesPerRowProperty = + AvaloniaProperty.Register(nameof(BytesPerRow), defaultValue: 16, inherits: true); + + public static readonly StyledProperty IsOffsetVisibleProperty = + AvaloniaProperty.Register(nameof(IsOffsetVisible), true, inherits: true); + + public static readonly StyledProperty IsHeaderVisibleProperty = + AvaloniaProperty.Register(nameof(IsHeaderVisible), defaultValue: true); + + public static readonly StyledProperty GroupSizeProperty = + AvaloniaProperty.Register(nameof(GroupSize), defaultValue: 8); + + public static readonly DirectProperty DataProperty = + AvaloniaProperty.RegisterDirect( + nameof(Data), + getter: o => o.Data, + setter: (o, v) => o.Data = v, + unsetValue: []); + + internal static readonly StyledProperty TypefaceProperty = + AvaloniaProperty.Register(nameof(Typeface), inherits: true); + + internal static readonly StyledProperty RowTextBuilderProperty = + AvaloniaProperty.Register(nameof(RowTextBuilder), inherits: true); + + public static readonly StyledProperty IsMultiSelectProperty = + AvaloniaProperty.Register(nameof(IsMultiSelect), defaultValue: true); + + public static readonly StyledProperty AsciiFormatterProperty = + AvaloniaProperty.Register(nameof(AsciiFormatter), new DefaultAsciiFormatter()); + + public static readonly StyledProperty SelectionProperty = + AvaloniaProperty.Register(nameof(Selection), Selection.Empty, 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 bool IsMultiSelect + { + get => GetValue(IsMultiSelectProperty); + set => SetValue(IsMultiSelectProperty, value); + } + + public IAsciiFormatter AsciiFormatter + { + get => GetValue(AsciiFormatterProperty); + set => SetValue(AsciiFormatterProperty, value); + } + + public Selection Selection + { + get => GetValue(SelectionProperty); + set => SetValue(SelectionProperty, 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; + } + + Selection = Selection.Empty; + _hexPanel.Clear(); + + UpdateView(); + } + } + } = []; + + internal Typeface Typeface + { + get => GetValue(TypefaceProperty); + private set => SetValue(TypefaceProperty, value); + } + + internal RowTextBuilder RowTextBuilder + { + get => GetValue(RowTextBuilderProperty); + set => SetValue(RowTextBuilderProperty, value); + } + + private readonly HexViewerHeader _header; + private readonly ScrollViewer _scrollViewer; + private readonly HexViewerPanel _hexPanel; + + private int _caretPosition = -1; + private int _selectionAnchor = -1; + + private int CurrentRowIndex => Selection.Start / BytesPerRow; + private int VisibleRowCount => (int)(_scrollViewer.Viewport.Height / RowHeight); + + private int PageHeight => VisibleRowCount * RowHeight; + private int PageSize => VisibleRowCount * BytesPerRow; + + 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(AsciiFormatterProperty).Subscribe(_ => Invalidate()); + this.GetObservable(IsHeaderVisibleProperty).Subscribe(_ => _header.IsVisible = IsHeaderVisible); + + _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); + } + } + + public void Select(int start, int length = 1) + { + Selection = new Selection(start, start + length - 1); + + _caretPosition = Selection.End; + _selectionAnchor = Selection.Start; + + var rowIndex = Selection.Start / BytesPerRow; + + if (!IsRowVisible(rowIndex)) + { + ScrollToRow(rowIndex); + } + } + + protected override void OnKeyDown(KeyEventArgs e) + { + switch (e.Key) + { + case Key.Left: + MoveLeft(e.KeyModifiers.HasFlag(KeyModifiers.Shift), offset: 1); + break; + + case Key.Right: + MoveRight(e.KeyModifiers.HasFlag(KeyModifiers.Shift), offset: 1); + break; + + case Key.Up: + MoveLeft(e.KeyModifiers.HasFlag(KeyModifiers.Shift), offset: BytesPerRow); + + if (!IsRowVisible(CurrentRowIndex)) + { + ScrollToRow(CurrentRowIndex); + } + break; + + case Key.Down: + MoveRight(e.KeyModifiers.HasFlag(KeyModifiers.Shift), offset: BytesPerRow); + + if (!IsRowVisible(CurrentRowIndex)) + { + var viewportRows = (int)(_scrollViewer.Viewport.Height / RowHeight); + var targetTopRow = CurrentRowIndex - Math.Max(0, viewportRows) + 1; + + ScrollToRow(targetTopRow); + } + break; + + case Key.Home: + _caretPosition = 0; + + if (!e.KeyModifiers.HasFlag(KeyModifiers.Shift)) + { + Selection = new Selection(_caretPosition, _caretPosition); + _selectionAnchor = _caretPosition; + } + else + { + Selection = new Selection(_caretPosition, Selection.End); + } + + _scrollViewer.ScrollToHome(); + break; + + case Key.End: + _caretPosition = Data.Length - 1; + + if (!e.KeyModifiers.HasFlag(KeyModifiers.Shift)) + { + Selection = new Selection(_caretPosition, _caretPosition); + _selectionAnchor = _caretPosition; + } + else + { + Selection = new Selection(Selection.Start, _caretPosition); + } + + _scrollViewer.ScrollToEnd(); + break; + + case Key.PageUp: + MoveLeft(e.KeyModifiers.HasFlag(KeyModifiers.Shift), offset: PageSize); + + _scrollViewer.Offset = new Vector(_scrollViewer.Offset.X, _scrollViewer.Offset.Y - PageHeight); + break; + + case Key.PageDown: + MoveRight(e.KeyModifiers.HasFlag(KeyModifiers.Shift), offset: PageSize); + + _scrollViewer.Offset = new Vector(_scrollViewer.Offset.X, _scrollViewer.Offset.Y + PageHeight); + break; + + default: + return; + } + } + + private void MoveLeft(bool isShiftPressed, int offset) + { + if (_caretPosition == 0) + { + return; + } + + _caretPosition = _caretPosition >= offset ? _caretPosition - offset : 0; + + if (!IsMultiSelect || !isShiftPressed) + { + Selection = new Selection(_caretPosition, _caretPosition); + _selectionAnchor = _caretPosition; + } + else + { + var start = Selection.Start; + var end = Selection.End; + + if (_caretPosition == Selection.End - offset && Selection.Length > 1) + { + end = _caretPosition; + } + + if (_caretPosition < Selection.Start) + { + start = _caretPosition; + } + + Selection = new Selection(start, end); + } + } + + private void MoveRight(bool isShiftPressed, int offset) + { + _caretPosition = _caretPosition < Data.Length - offset ? _caretPosition + offset : Data.Length - 1; + + if (!IsMultiSelect || !isShiftPressed) + { + Selection = new Selection(_caretPosition, _caretPosition); + _selectionAnchor = _caretPosition; + } + else + { + var start = Selection.Start; + var end = Selection.End; + + if (_caretPosition == Selection.Start + offset && Selection.Length > 1) + { + start = _caretPosition; + } + + if (_caretPosition > Selection.End) + { + end = _caretPosition; + } + + Selection = new Selection(start, end); + } + } + + + 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 + }; + + row.CellClicked += (_, e) => { HandleCellClicked(e); }; + + return row; + } + + private void HandleCellClicked(HexCellClickedEventArgs e) + { + var targetPosition = e.RowIndex * BytesPerRow + e.Position; + + if (!IsMultiSelect || !e.IsShiftPressed || _caretPosition < 0) + { + _caretPosition = targetPosition; + Selection = new Selection(_caretPosition, _caretPosition); + _selectionAnchor = _caretPosition; + } + else + { + int selectionAnchor; + + if (Selection.Length > 0) + { + if (targetPosition < Selection.Start) + { + selectionAnchor = Selection.End; + } + else if (targetPosition > Selection.End) + { + selectionAnchor = Selection.Start; + } + else + { + selectionAnchor = _selectionAnchor >= 0 ? _selectionAnchor : _caretPosition; + } + } + else + { + selectionAnchor = _selectionAnchor >= 0 ? _selectionAnchor : _caretPosition; + } + + _caretPosition = targetPosition; + Selection = new Selection( + Math.Min(selectionAnchor, targetPosition), + Math.Max(selectionAnchor, targetPosition)); + } + } + + 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 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(AsciiFormatter, IsOffsetVisible, GroupSize, BytesPerRow, formattedText.Width); + } + + private void Invalidate() + { + UpdateTypeface(); + + _hexPanel.InvalidateVisual(); + _header.InvalidateVisual(); + } + + private void ScrollToRow(int rowIndex) => _scrollViewer.Offset = + new Vector(_scrollViewer.Offset.X, rowIndex * RowHeight); +} diff --git a/src/Spectron.Debugger/Controls/Hex/HexViewerHeader.cs b/src/Spectron.Debugger/Controls/Hex/HexViewerHeader.cs new file mode 100644 index 00000000..a4236faf --- /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; + +public 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 = + HexViewer.TypefaceProperty.AddOwner(); + + private static readonly StyledProperty RowTextBuilderProperty = + HexViewer.RowTextBuilderProperty.AddOwner(); + + public 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..f7124c12 --- /dev/null +++ b/src/Spectron.Debugger/Controls/Hex/HexViewerPanel.cs @@ -0,0 +1,73 @@ +using Avalonia; +using Avalonia.Controls; + +namespace OldBit.Spectron.Debugger.Controls.Hex; + +internal class HexViewerPanel(HexViewer viewer) : 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 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 = (viewer.Data.Length + viewer.BytesPerRow - 1) / viewer.BytesPerRow; + double totalHeight = rowCount * viewer.RowHeight; + + return new Size(viewer.RowWidth, totalHeight); + } + + protected override Size ArrangeOverride(Size finalSize) + { + foreach (var (rowIndex, row) in _visibleRows) + { + var rect = new Rect(0, rowIndex * viewer.RowHeight, finalSize.Width, viewer.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..87c91355 --- /dev/null +++ b/src/Spectron.Debugger/Controls/Hex/HexViewerRow.cs @@ -0,0 +1,164 @@ +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)); + + private static readonly StyledProperty BytesPerRowProperty = + HexViewer.BytesPerRowProperty.AddOwner(); + + private static readonly StyledProperty FontSizeProperty = + TemplatedControl.FontSizeProperty.AddOwner(); + + private static readonly StyledProperty TypefaceProperty = + HexViewer.TypefaceProperty.AddOwner(); + + private static readonly StyledProperty RowTextBuilderProperty = + HexViewer.RowTextBuilderProperty.AddOwner(); + + public static readonly StyledProperty SelectionProperty = + HexViewer.SelectionProperty.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 Selection Selection => GetValue(SelectionProperty); + + private int BytesPerRow => GetValue(BytesPerRowProperty); + + private double FontSize => GetValue(FontSizeProperty); + + private Typeface Typeface => GetValue(TypefaceProperty); + + private RowTextBuilder RowTextBuilder => GetValue(RowTextBuilderProperty); + + private int RowStartIndex => RowIndex * BytesPerRow; + private int RowEndIndex => (RowIndex + 1) * BytesPerRow - 1; + + public required int RowIndex { get; init; } + public required int Offset { get; init; } + public required ArraySegment Data { get; init; } + + internal event EventHandler? CellClicked; + + internal HexViewerRow() + { + HorizontalAlignment = HorizontalAlignment.Left; + VerticalAlignment = VerticalAlignment.Center; + + this.GetObservable(TypefaceProperty).Subscribe(_ => InvalidateVisual()); + this.GetObservable(SelectionProperty).Subscribe(_ => InvalidateVisual()); + } + + 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); + + foreach (var selectedIndex in GetSelectedIndexes()) + { + 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) + { + if (!e.Properties.IsLeftButtonPressed) + { + return; + } + + var x = e.GetPosition(this).X; + var cellIndex = RowTextBuilder.GetIndexFromPosition(x); + + if (cellIndex == null) + { + return; + } + + var isShiftPressed = e.KeyModifiers.HasFlag(KeyModifiers.Shift); + CellClicked?.Invoke(this, new HexCellClickedEventArgs(RowIndex, cellIndex.Value, isShiftPressed)); + } + + 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); + } + + private IEnumerable GetSelectedIndexes() + { + for (var i = Selection.Start; i <= Selection.End; i++) + { + if (i >= RowStartIndex && i <= RowEndIndex) + { + yield return i - RowStartIndex; + } + } + } +} diff --git a/src/Spectron.Debugger/Controls/Hex/IAsciiFormatter.cs b/src/Spectron.Debugger/Controls/Hex/IAsciiFormatter.cs new file mode 100644 index 00000000..016c02cc --- /dev/null +++ b/src/Spectron.Debugger/Controls/Hex/IAsciiFormatter.cs @@ -0,0 +1,6 @@ +namespace OldBit.Spectron.Debugger.Controls.Hex; + +public interface IAsciiFormatter +{ + char Format(byte b); +} \ 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..028ec698 --- /dev/null +++ b/src/Spectron.Debugger/Controls/Hex/RowTextBuilder.cs @@ -0,0 +1,148 @@ +using System.Text; + +namespace OldBit.Spectron.Debugger.Controls.Hex; + +internal record RowTextLayout(int Position, int Width); + +internal sealed class RowTextBuilder +{ + private readonly IAsciiFormatter _asciiFormatter; + private readonly int _groupSize; + private readonly int _bytesPerRow; + private readonly RowTextLayout[] _layout; + + internal double CharWidth { get; } + internal bool IsOffsetVisible { get; } + + public RowTextBuilder( + IAsciiFormatter asciiFormatter, + bool isOffsetVisible, + int groupSize, + int bytesPerRow, + double charWidth) + { + IsOffsetVisible = isOffsetVisible; + _asciiFormatter = asciiFormatter; + _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.Format(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; +} \ No newline at end of file diff --git a/src/Spectron.Debugger/Controls/Hex/Selection.cs b/src/Spectron.Debugger/Controls/Hex/Selection.cs new file mode 100644 index 00000000..09f33298 --- /dev/null +++ b/src/Spectron.Debugger/Controls/Hex/Selection.cs @@ -0,0 +1,11 @@ +namespace OldBit.Spectron.Debugger.Controls.Hex; + +public sealed record Selection(int Start, int End) +{ + public static Selection Empty { get; } = new(-1, -1); + + public int Start { get; } = Start; + public int End { get; } = End; + + public int Length => End - Start + 1; +} \ No newline at end of file diff --git a/src/Spectron.Debugger/Controls/HexViewer.axaml b/src/Spectron.Debugger/Controls/HexViewer.axaml deleted file mode 100644 index 036a04e2..00000000 --- a/src/Spectron.Debugger/Controls/HexViewer.axaml +++ /dev/null @@ -1,113 +0,0 @@ - - - - - - - - - - - - - diff --git a/src/Spectron.Debugger/Controls/HexViewer.axaml.cs b/src/Spectron.Debugger/Controls/HexViewer.axaml.cs deleted file mode 100644 index 9eb1f93c..00000000 --- a/src/Spectron.Debugger/Controls/HexViewer.axaml.cs +++ /dev/null @@ -1,49 +0,0 @@ -using Avalonia; -using Avalonia.Controls.Primitives; - -namespace OldBit.Spectron.Debugger.Controls; - -public record HexViewerDataRow(int Address, HexViewerCell[] Cells); - -public class HexViewer : TemplatedControl -{ - private const int DefaultCellHeight = 25; - private const int DefaultCellWight = 25; - private const int DefaultBytesPerRow = 16; - - public static readonly StyledProperty CellHeightProperty = - AvaloniaProperty.Register(nameof(CellHeight), DefaultCellHeight); - - public static readonly StyledProperty CellWidthProperty = - AvaloniaProperty.Register(nameof(CellWidth), DefaultCellWight); - - public static readonly StyledProperty> DataProperty = - AvaloniaProperty.Register>(nameof(Data), []); - - private static readonly StyledProperty BytesPerRowProperty = - AvaloniaProperty.Register(nameof(BytesPerRow), DefaultBytesPerRow, validate: i => i > 0); - - public double CellHeight - { - get => GetValue(CellHeightProperty); - set => SetValue(CellHeightProperty, value); - } - - public double CellWidth - { - get => GetValue(CellWidthProperty); - set => SetValue(CellWidthProperty, value); - } - - public IEnumerable Data - { - get => GetValue(DataProperty); - set => SetValue(DataProperty, value); - } - - public int BytesPerRow - { - get => GetValue(BytesPerRowProperty); - set => SetValue(BytesPerRowProperty, value); - } -} \ No newline at end of file diff --git a/src/Spectron.Debugger/Controls/HexViewerCell.cs b/src/Spectron.Debugger/Controls/HexViewerCell.cs deleted file mode 100644 index 5110cf22..00000000 --- a/src/Spectron.Debugger/Controls/HexViewerCell.cs +++ /dev/null @@ -1,15 +0,0 @@ -using CommunityToolkit.Mvvm.ComponentModel; - -namespace OldBit.Spectron.Debugger.Controls; - -public partial class HexViewerCell : ObservableObject -{ - [ObservableProperty] - private byte _value; - - [ObservableProperty] - private bool _isSelected; - - public int RowIndex { get; init; } - public int ColumnIndex { get;init; } -} \ No newline at end of file diff --git a/src/Spectron.Debugger/Controls/Memory.axaml b/src/Spectron.Debugger/Controls/Memory.axaml index b85f94a1..9926b57b 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/Converters/Hex.cs b/src/Spectron.Debugger/Converters/Hex.cs new file mode 100644 index 00000000..3e717c5a --- /dev/null +++ b/src/Spectron.Debugger/Converters/Hex.cs @@ -0,0 +1,49 @@ +using System.Globalization; +using System.Numerics; + +namespace OldBit.Spectron.Debugger.Converters; + +internal static class Hex +{ + internal static bool TryParse(string input, out T value, bool preferDecimal = false) where T : IBinaryInteger + { + input = input.Trim(); + + if (preferDecimal && T.TryParse(input, NumberStyles.AllowLeadingSign, CultureInfo.InvariantCulture, out var result)) + { + value = result; + return true; + } + + var hex = TrimHexIndicator(input); + + if (T.TryParse(hex, NumberStyles.AllowHexSpecifier, CultureInfo.InvariantCulture, out result)) + { + value = result; + return true; + } + + value = T.Zero; + return false; + } + + private static string TrimHexIndicator(string input) + { + if (input.StartsWith("0x", StringComparison.OrdinalIgnoreCase)) + { + return input[2..]; + } + + if (input.StartsWith('$') || input.StartsWith('#')) + { + return input[1..]; + } + + if (input.EndsWith("h", StringComparison.OrdinalIgnoreCase)) + { + return input[..^1]; + } + + return input; + } +} \ No newline at end of file diff --git a/src/Spectron.Debugger/Converters/HexViewerDataConverter.cs b/src/Spectron.Debugger/Converters/HexViewerDataConverter.cs deleted file mode 100644 index 2cd662b8..00000000 --- a/src/Spectron.Debugger/Converters/HexViewerDataConverter.cs +++ /dev/null @@ -1,32 +0,0 @@ -using System.Globalization; -using Avalonia.Data.Converters; -using OldBit.Spectron.Debugger.Controls; -using OldBit.Spectron.Debugger.Extensions; - -namespace OldBit.Spectron.Debugger.Converters; - -public class HexViewerDataConverter : IMultiValueConverter -{ - public object? Convert(IList values, Type targetType, object? parameter, CultureInfo culture) - { - var hexRows = new List(); - - if (values.Count != 2 || values.Any(x => x == null) || values[0] is not byte[] byteArray || values[1] is not int bytesPerRow) - { - return hexRows; - } - - var chunks = byteArray.ToChunks(bytesPerRow); - hexRows.AddRange( - chunks.Select((chunk, i) => new HexViewerDataRow( - Address: bytesPerRow * i, - Cells: chunk.Select((byteValue, index) => new HexViewerCell - { - ColumnIndex = index, - RowIndex = i, - Value = byteValue - }).ToArray()))); - - return hexRows; - } -} \ No newline at end of file diff --git a/src/Spectron.Debugger/Converters/ZxAscii.cs b/src/Spectron.Debugger/Converters/ZxAscii.cs new file mode 100644 index 00000000..19ea198b --- /dev/null +++ b/src/Spectron.Debugger/Converters/ZxAscii.cs @@ -0,0 +1,92 @@ +using System.Text; + +namespace OldBit.Spectron.Debugger.Converters; + +/// +/// Converts ZX Spectrum ASCII codes to their Unicode equivalents. +/// +public static class ZxAscii +{ + public static char ToChar(byte value, char nonPrintChar = '.') + { + if (value >= 0x20 & value <= 0x8F) + { + return ToSpectrumChar(value); + } + + return nonPrintChar; + } + + public static string ToString(byte value, char nonPrintChar = '.') => ToChar(value, nonPrintChar).ToString(); + + public static string ToString(ReadOnlySpan values, char nonPrintChar = '.') + { + var s = new StringBuilder(values.Length); + + foreach (var value in values) + { + s.Append(ToChar(value, nonPrintChar)); + } + + return s.ToString(); + } + + public static byte[] FromString(ReadOnlySpan s) + { + var bytes = new byte[s.Length]; + + for (var i = 0; i < s.Length; i++) + { + bytes[i] = FromSpectrumChar(s[i]); + } + + return bytes; + } + + private static char ToSpectrumChar(byte code) => code switch + { + 0x5E => '\x2191', // "↑" + 0x60 => '\x00A3', // "£" + 0x7F => '\x00A9', // "©" + 0x80 => '\x0020', // " " + 0x81 => '\x259D', // "▝" + 0x82 => '\x2598', // "▘" + 0x83 => '\x2580', // "▀" + 0x84 => '\x2597', // "▗" + 0x85 => '\x2590', // "▐" + 0x86 => '\x259A', // "▚" + 0x87 => '\x259C', // "▜" + 0x88 => '\x2596', // "▖" + 0x89 => '\x259E', // "▞" + 0x8A => '\x258C', // "▌" + 0x8B => '\x259B', // "▛" + 0x8C => '\x2584', // "▄" + 0x8D => '\x259F', // "▟" + 0x8E => '\x2599', // "▙" + 0x8F => '\x2588', // "█" + _ => Convert.ToChar(code) + }; + + public static byte FromSpectrumChar(char value) => value switch + { + '\u2191' => 0x5E, // "↑" + '\u00A3' => 0x60, // "£" + '\u00A9' => 0x7F, // "©" + '\u259D' => 0x81, // "▝" + '\u2598' => 0x82, // "▘" + '\u2580' => 0x83, // "▀" + '\u2597' => 0x84, // "▗" + '\u2590' => 0x85, // "▐" + '\u259A' => 0x86, // "▚" + '\u259C' => 0x87, // "▜" + '\u2596' => 0x88, // "▖" + '\u259E' => 0x89, // "▞" + '\u258C' => 0x8A, // "▌" + '\u259B' => 0x8B, // "▛" + '\u2584' => 0x8C, // "▄" + '\u259F' => 0x8D, // "▟" + '\u2599' => 0x8E, // "▙" + '\u2588' => 0x8F, // "█" + _ => (byte)value // Only works for ASCII characters + }; +} \ No newline at end of file diff --git a/src/Spectron.Debugger/Converters/ZxAsciiConverter.cs b/src/Spectron.Debugger/Converters/ZxAsciiConverter.cs index f1dacd0b..f02839e4 100644 --- a/src/Spectron.Debugger/Converters/ZxAsciiConverter.cs +++ b/src/Spectron.Debugger/Converters/ZxAsciiConverter.cs @@ -1,18 +1,19 @@ using System.Globalization; using Avalonia.Data.Converters; +using OldBit.Spectron.Debugger.Controls.Hex; namespace OldBit.Spectron.Debugger.Converters; -public class ZxAsciiConverter : IValueConverter +public class ZxAsciiConverter : IValueConverter, IAsciiFormatter { public object? Convert(object? value, Type targetType, object? parameter, CultureInfo culture) { - return value switch + if (value is byte code) { - byte code - when code > 0x20 & code < 0x90 => ToSpectrumCharCode(code).ToString(), - _ => "." - }; + return ZxAscii.ToString(code); + } + + return "."; } public object? ConvertBack(object? value, Type targetType, object? parameter, CultureInfo culture) @@ -20,28 +21,5 @@ byte code throw new NotImplementedException(); } - private static char ToSpectrumCharCode(byte code) => - code switch - { - 0x5E => '\x2191', // "↑" - 0x60 => '\x00A3', // "£" - 0x7F => '\x00A9', // "©" - 0x80 => '\x0020', // " " - 0x81 => '\x259D', // "▝" - 0x82 => '\x2598', // "▘" - 0x83 => '\x2580', // "▀" - 0x84 => '\x2597', // "▗" - 0x85 => '\x2590', // "▐" - 0x86 => '\x259A', // "▚" - 0x87 => '\x259C', // "▜" - 0x88 => '\x2596', // "▖" - 0x89 => '\x259E', // "▞" - 0x8A => '\x258C', // "▌" - 0x8B => '\x259B', // "▛" - 0x8C => '\x2584', // "▄" - 0x8D => '\x259F', // "▟" - 0x8E => '\x2599', // "▙" - 0x8F => '\x2588', // "█" - _ => System.Convert.ToChar(code) - }; + public char Format(byte b) => ZxAscii.ToChar(b); } \ No newline at end of file diff --git a/src/Spectron.Debugger/Extensions/ByteArrayExtensions.cs b/src/Spectron.Debugger/Extensions/ByteArrayExtensions.cs index 95bf06f4..b093f6f1 100644 --- a/src/Spectron.Debugger/Extensions/ByteArrayExtensions.cs +++ b/src/Spectron.Debugger/Extensions/ByteArrayExtensions.cs @@ -2,23 +2,28 @@ namespace OldBit.Spectron.Debugger.Extensions; public static class ByteArrayExtensions { - public static List ToChunks(this byte[] byteArray, int chunkSize) + public static int IndexOfSequence(this ReadOnlySpan buffer, ReadOnlySpan pattern, int startIndex) { - var chunks = new List(); - var arrayLength = byteArray.Length; - var numChunks = (int)Math.Ceiling((double)arrayLength / chunkSize); + if (startIndex < 0 || startIndex > buffer.Length) + { + throw new ArgumentOutOfRangeException(nameof(startIndex)); + } - for (var i = 0; i < numChunks; i++) + if (pattern.IsEmpty || buffer.Length - startIndex < pattern.Length) { - var startIdx = i * chunkSize; - var endIdx = Math.Min(startIdx + chunkSize, arrayLength); - var chunkLength = endIdx - startIdx; + return -1; + } - var chunk = new byte[chunkLength]; - Array.Copy(byteArray, startIdx, chunk, 0, chunkLength); - chunks.Add(chunk); + var search = buffer[startIndex..]; + + for (var i = 0; i <= search.Length - pattern.Length; i++) + { + if (search.Slice(i, pattern.Length).SequenceEqual(pattern)) + { + return startIndex + i; + } } - return chunks; + return -1; } } \ No newline at end of file diff --git a/src/Spectron.Debugger/Extensions/Z80Extensions.cs b/src/Spectron.Debugger/Extensions/Z80Extensions.cs index c32a09bf..e18615d6 100644 --- a/src/Spectron.Debugger/Extensions/Z80Extensions.cs +++ b/src/Spectron.Debugger/Extensions/Z80Extensions.cs @@ -4,147 +4,150 @@ namespace OldBit.Spectron.Debugger.Extensions; public static class Z80Extensions { - public static int GetRegisterValue(this Z80Cpu.Z80 cpu, string register) => register.ToUpper() switch + extension(Z80Cpu.Z80 cpu) { - "A" => cpu.Registers.A, - "B" => cpu.Registers.B, - "C" => cpu.Registers.C, - "D" => cpu.Registers.D, - "E" => cpu.Registers.E, - "F" => (int)cpu.Registers.F, - "H" => cpu.Registers.H, - "L" => cpu.Registers.L, - "I" => cpu.Registers.I, - "R" => cpu.Registers.R, - "IXH" => cpu.Registers.IXH, - "IXL" => cpu.Registers.IXL, - "IYH" => cpu.Registers.IYH, - "IYL" => cpu.Registers.IYL, - "AF" => cpu.Registers.AF, - "AF'" => cpu.Registers.Prime.AF, - "BC" => cpu.Registers.BC, - "BC'" => cpu.Registers.Prime.BC, - "DE" => cpu.Registers.DE, - "DE'" => cpu.Registers.Prime.DE, - "HL" => cpu.Registers.HL, - "HL'" => cpu.Registers.Prime.HL, - "IX" => cpu.Registers.IX, - "IY" => cpu.Registers.IY, - "PC" => cpu.Registers.PC, - "SP" => cpu.Registers.SP, - _ => throw new ArgumentException($"Unknown register {register}") - }; - - public static void SetRegisterValue(this Z80Cpu.Z80 cpu, string register, int value) - { - switch (register.ToUpper()) + public int GetRegisterValue(string register) => register.ToUpper() switch { - case "A": - cpu.Registers.A = (byte)value; - break; - - case "B": - cpu.Registers.B = (byte)value; - break; - - case "C": - cpu.Registers.C = (byte)value; - break; - - case "D": - cpu.Registers.D = (byte)value; - break; - - case "E": - cpu.Registers.E = (byte)value; - break; - - case "F": - cpu.Registers.F = (Flags)value; - break; - - case "H": - cpu.Registers.H = (byte)value; - break; - - case "L": - cpu.Registers.L = (byte)value; - break; - - case "I": - cpu.Registers.I = (byte)value; - break; - - case "R": - cpu.Registers.R = (byte)value; - break; - - case "IXH": - cpu.Registers.IXH = (byte)value; - break; - - case "IXL": - cpu.Registers.IXL = (byte)value; - break; - - case "IYH": - cpu.Registers.IYH = (byte)value; - break; - - case "IYL": - cpu.Registers.IYL = (byte)value; - break; - - case "AF": - cpu.Registers.AF = (Word)value; - break; - - case "AF'": - cpu.Registers.Prime.AF = (Word)value; - break; - - case "BC": - cpu.Registers.BC = (Word)value; - break; - - case "BC'": - cpu.Registers.Prime.BC = (Word)value; - break; - - case "DE": - cpu.Registers.DE = (Word)value; - break; - - case "DE'": - cpu.Registers.Prime.DE = (Word)value; - break; - - case "HL": - cpu.Registers.HL = (Word)value; - break; - - case "HL'": - cpu.Registers.Prime.HL = (Word)value; - break; - - case "IX": - cpu.Registers.IX = (Word)value; - break; - - case "IY": - cpu.Registers.IY = (Word)value; - break; - - case "PC": - cpu.Registers.PC = (Word)value; - break; + "A" => cpu.Registers.A, + "B" => cpu.Registers.B, + "C" => cpu.Registers.C, + "D" => cpu.Registers.D, + "E" => cpu.Registers.E, + "F" => (int)cpu.Registers.F, + "H" => cpu.Registers.H, + "L" => cpu.Registers.L, + "I" => cpu.Registers.I, + "R" => cpu.Registers.R, + "IXH" => cpu.Registers.IXH, + "IXL" => cpu.Registers.IXL, + "IYH" => cpu.Registers.IYH, + "IYL" => cpu.Registers.IYL, + "AF" => cpu.Registers.AF, + "AF'" => cpu.Registers.Prime.AF, + "BC" => cpu.Registers.BC, + "BC'" => cpu.Registers.Prime.BC, + "DE" => cpu.Registers.DE, + "DE'" => cpu.Registers.Prime.DE, + "HL" => cpu.Registers.HL, + "HL'" => cpu.Registers.Prime.HL, + "IX" => cpu.Registers.IX, + "IY" => cpu.Registers.IY, + "PC" => cpu.Registers.PC, + "SP" => cpu.Registers.SP, + _ => throw new ArgumentException($"Unknown register {register}") + }; + + public void SetRegisterValue(string register, int value) + { + switch (register.ToUpper()) + { + case "A": + cpu.Registers.A = (byte)value; + break; - case "SP": - cpu.Registers.SP = (Word)value; - break; + case "B": + cpu.Registers.B = (byte)value; + break; + + case "C": + cpu.Registers.C = (byte)value; + break; + + case "D": + cpu.Registers.D = (byte)value; + break; + + case "E": + cpu.Registers.E = (byte)value; + break; + + case "F": + cpu.Registers.F = (Flags)value; + break; + + case "H": + cpu.Registers.H = (byte)value; + break; + + case "L": + cpu.Registers.L = (byte)value; + break; + + case "I": + cpu.Registers.I = (byte)value; + break; + + case "R": + cpu.Registers.R = (byte)value; + break; + + case "IXH": + cpu.Registers.IXH = (byte)value; + break; + + case "IXL": + cpu.Registers.IXL = (byte)value; + break; + + case "IYH": + cpu.Registers.IYH = (byte)value; + break; + + case "IYL": + cpu.Registers.IYL = (byte)value; + break; + + case "AF": + cpu.Registers.AF = (Word)value; + break; + + case "AF'": + cpu.Registers.Prime.AF = (Word)value; + break; + + case "BC": + cpu.Registers.BC = (Word)value; + break; + + case "BC'": + cpu.Registers.Prime.BC = (Word)value; + break; + + case "DE": + cpu.Registers.DE = (Word)value; + break; + + case "DE'": + cpu.Registers.Prime.DE = (Word)value; + break; + + case "HL": + cpu.Registers.HL = (Word)value; + break; + + case "HL'": + cpu.Registers.Prime.HL = (Word)value; + break; + + case "IX": + cpu.Registers.IX = (Word)value; + break; + + case "IY": + cpu.Registers.IY = (Word)value; + break; + + case "PC": + cpu.Registers.PC = (Word)value; + break; + + case "SP": + cpu.Registers.SP = (Word)value; + break; - default: - throw new ArgumentException($"Unknown register {register}"); + default: + throw new ArgumentException($"Unknown register {register}"); + } } } } \ No newline at end of file diff --git a/src/Spectron.Debugger/Spectron.Debugger.csproj b/src/Spectron.Debugger/Spectron.Debugger.csproj index b6dc8559..5cbbeb6e 100644 --- a/src/Spectron.Debugger/Spectron.Debugger.csproj +++ b/src/Spectron.Debugger/Spectron.Debugger.csproj @@ -22,12 +22,16 @@ + <_Parameter1>false + + <_Parameter1>OldBit.Spectron.Debugger.Tests + diff --git a/src/Spectron.Debugger/ViewModels/CodeListViewModel.cs b/src/Spectron.Debugger/ViewModels/CodeListViewModel.cs index 02915187..188dc660 100644 --- a/src/Spectron.Debugger/ViewModels/CodeListViewModel.cs +++ b/src/Spectron.Debugger/ViewModels/CodeListViewModel.cs @@ -19,7 +19,7 @@ public void Update(IMemory memory, Word address, Word pc, DebuggerSettings debug var startAddress = DetermineStartAddress(address, breakpointHitEventArgs); var disassembly = new Disassembler( - memory.GetBytes(), + memory.ToBytes(), startAddress, maxCount: 25, new DisassemblerOptions { NumberFormat = debuggerSettings.NumberFormat }); diff --git a/src/Spectron.Debugger/ViewModels/DebuggerViewModel.cs b/src/Spectron.Debugger/ViewModels/DebuggerViewModel.cs index 0b41f50e..4ddf204f 100644 --- a/src/Spectron.Debugger/ViewModels/DebuggerViewModel.cs +++ b/src/Spectron.Debugger/ViewModels/DebuggerViewModel.cs @@ -20,7 +20,7 @@ public partial class DebuggerViewModel : ObservableObject, IDisposable public StackViewModel StackViewModel { get; } = new(); public CpuViewModel CpuViewModel { get; } = new(); - public MemoryViewModel MemoryViewModel { get; } = new(); + public MemoryViewModel MemoryViewModel { get; private set; } = null!; [ObservableProperty] private CodeListViewModel _codeListViewModel = null!; @@ -58,6 +58,7 @@ public void ConfigureEmulator(Emulator emulator, BreakpointHitEventArgs? breakpo _breakpointHandler.BreakpointHit -= OnBreakpointHit; _breakpointHandler.BreakpointHit += OnBreakpointHit; + MemoryViewModel = new MemoryViewModel(Emulator); BreakpointListViewModel = new BreakpointListViewModel(_breakpointHandler.BreakpointManager, _debuggerSettings.NumberFormat); CodeListViewModel = new CodeListViewModel(_breakpointHandler.BreakpointManager); ImmediateViewModel = new ImmediateViewModel(_debuggerContext, _debuggerSettings.NumberFormat, emulator, @@ -178,6 +179,7 @@ private void DebuggerResume() private void Close() { LoggingViewModel.Dispose(); + MemoryViewModel.Dispose(); _breakpointHandler.BreakpointHit -= OnBreakpointHit; } diff --git a/src/Spectron.Debugger/ViewModels/ImmediateViewModel.cs b/src/Spectron.Debugger/ViewModels/ImmediateViewModel.cs index 4881dc19..af6e043a 100644 --- a/src/Spectron.Debugger/ViewModels/ImmediateViewModel.cs +++ b/src/Spectron.Debugger/ViewModels/ImmediateViewModel.cs @@ -173,7 +173,7 @@ private void ExecuteCommand() private void Save(SaveAction save) { - var memory = emulator.Memory.GetBytes(); + var memory = emulator.Memory.ToBytes(); var length = save.Length ?? memory.Length - save.Address; if (length > memory.Length) diff --git a/src/Spectron.Debugger/ViewModels/MemoryViewModel.cs b/src/Spectron.Debugger/ViewModels/MemoryViewModel.cs index 05ffe8b3..56a95d8a 100644 --- a/src/Spectron.Debugger/ViewModels/MemoryViewModel.cs +++ b/src/Spectron.Debugger/ViewModels/MemoryViewModel.cs @@ -1,13 +1,237 @@ +using System.ComponentModel.DataAnnotations; +using Avalonia.Input; +using Avalonia.Input.Platform; using CommunityToolkit.Mvvm.ComponentModel; +using CommunityToolkit.Mvvm.Input; +using OldBit.Spectron.Debugger.Controls.Hex; +using OldBit.Spectron.Debugger.Converters; +using OldBit.Spectron.Debugger.Extensions; +using OldBit.Spectron.Debugger.Parser; +using OldBit.Spectron.Emulation; using OldBit.Spectron.Emulation.Extensions; using OldBit.Z80Cpu; namespace OldBit.Spectron.Debugger.ViewModels; -public partial class MemoryViewModel : ObservableObject +public sealed partial class MemoryViewModel : ObservableValidator, IDisposable { + private interface ICommand; + private record WriteCommand (Word Address, byte Value) : ICommand; + private record GoToCommand (Word Address) : ICommand; + private record FindCommand (string Text, bool ForceText = false) : ICommand; + private record InvalidCommand (string Error) : ICommand; + + private readonly Emulator _emulator; + + public HexViewer? Viewer { get; set; } + public IClipboard? Clipboard { get; set; } + + public Action OnMemoryUpdated { get; set; } = (_, _) => { }; + public Action GoTo { get; set; } = _ => { }; + public Action Select { get; set; } = (_, _) => { }; + + private int _lastFindInex; + [ObservableProperty] private byte[] _memory = []; - public void Update(IMemory memory) => Memory = memory.GetBytes(); + [ObservableProperty] + [CustomValidation(typeof(MemoryViewModel), nameof(ValidateCommand))] + [NotifyDataErrorInfo] + private string _commandText = string.Empty; + + public MemoryViewModel(Emulator emulator) + { + _emulator = emulator; + + Memory = emulator.Memory.ToBytes(); + emulator.Memory.MemoryUpdated += MemoryUpdated; + } + + public void Update(IMemory memory) => Memory = memory.ToBytes(); + + [RelayCommand] + private async Task CopyHex() + { + if (Clipboard is null) + { + return; + } + + var bytes = GetSelectedBytes(); + var hex = BitConverter.ToString(bytes).Replace("-", " "); + + await Clipboard.SetTextAsync(hex); + } + + [RelayCommand] + private async Task CopyAscii() + { + if (Clipboard is null) + { + return; + } + + var bytes = GetSelectedBytes(); + var ascii = ZxAscii.ToString(bytes); + + await Clipboard.SetTextAsync(ascii); + } + + [RelayCommand] + private void Immediate(KeyEventArgs e) + { + if (e.Key != Key.Enter || string.IsNullOrWhiteSpace(CommandText)) + { + return; + } + + var command = ParseCommand(CommandText); + + switch (command) + { + case GoToCommand goToCommand: + GoTo(goToCommand.Address); + break; + + case WriteCommand memoryCommand: + _emulator.Memory.Write(memoryCommand.Address, memoryCommand.Value); + break; + + case FindCommand findCommand: + { + byte[] find = []; + + if (findCommand.ForceText) + { + find = ZxAscii.FromString(findCommand.Text); + } + else + { + var hexes = findCommand.Text.Split(' ', StringSplitOptions.RemoveEmptyEntries); + + foreach (var hex in hexes) + { + if (Hex.TryParse(hex, out var value, preferDecimal: false)) + { + find = find.Concat([value]).ToArray(); + } + else + { + find = ZxAscii.FromString(findCommand.Text); + break; + } + } + } + + var index = Memory.IndexOfSequence(find, _lastFindInex); + _lastFindInex = 0; + + if (index < 0) + { + return; + } + + Select((Word)index, find.Length); + + _lastFindInex = index + find.Length; + + if (_lastFindInex >= Memory.Length) + { + _lastFindInex = 0; + } + break; + } + } + } + + private byte[] GetSelectedBytes() + { + if (Viewer is null || Viewer.Selection.Length == 0) + { + return []; + } + + var selectedBytes = new byte[Viewer.Selection.Length]; + var index = 0; + + for (var selected = Viewer.Selection.Start; selected <= Viewer.Selection.End; selected++) + { + selectedBytes[index++] = Memory[selected]; + } + + return selectedBytes; + } + + private void MemoryUpdated(Word address, byte value) => OnMemoryUpdated.Invoke(address, value); + + public void Dispose() => _emulator.Memory.MemoryUpdated -= MemoryUpdated; + + public static ValidationResult? ValidateCommand(string s, ValidationContext context) + { + var command = ParseCommand(s); + + if (command is InvalidCommand invalidCommand) + { + return new ValidationResult(invalidCommand.Error); + } + + return ValidationResult.Success;; + } + + private static ICommand? ParseCommand(string command) + { + command = command.Trim(); + + if (string.IsNullOrWhiteSpace(command)) + { + return null; + } + + if (command.StartsWith("g", StringComparison.OrdinalIgnoreCase)) + { + if (Hex.TryParse(command[1..].Trim(), out var address)) + { + return new GoToCommand(address); + } + + return new InvalidCommand("Invalid address"); + } + + if (command.StartsWith("f", StringComparison.OrdinalIgnoreCase)) + { + var text = command[1..].Trim(); + + return text.Length switch + { + 0 => new InvalidCommand("Empty search text"), + > 2 when text[0] == '"' && text[^1] == '"' => new FindCommand(text[1..^1], ForceText: true), + _ => new FindCommand(text) + }; + } + + if (command.StartsWith("w", StringComparison.OrdinalIgnoreCase)) + { + var args = command[1..].Trim().Split(','); + + if (args.Length != 2) + { + return new InvalidCommand("Invalid arguments"); + } + + if (!Hex.TryParse(args[0].Trim(), out var address)) + { + return new InvalidCommand("Invalid address"); + } + + if (!Hex.TryParse(args[1].Trim(), out var value)) + { + return new InvalidCommand("Invalid value"); + } + + return new WriteCommand(address, value); + } + + return new InvalidCommand("Invalid command"); + } } \ No newline at end of file diff --git a/src/Spectron.Debugger/Views/MemoryView.axaml b/src/Spectron.Debugger/Views/MemoryView.axaml new file mode 100644 index 00000000..1fa9561d --- /dev/null +++ b/src/Spectron.Debugger/Views/MemoryView.axaml @@ -0,0 +1,72 @@ + + + + + + + Black + + + Chartreuse + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Spectron.Debugger/Views/MemoryView.axaml.cs b/src/Spectron.Debugger/Views/MemoryView.axaml.cs new file mode 100644 index 00000000..c73546fc --- /dev/null +++ b/src/Spectron.Debugger/Views/MemoryView.axaml.cs @@ -0,0 +1,35 @@ +using Avalonia.Controls; +using Avalonia.Threading; +using OldBit.Spectron.Debugger.Converters; +using OldBit.Spectron.Debugger.ViewModels; + +namespace OldBit.Spectron.Debugger.Views; + +public partial class MemoryView : Window +{ + public MemoryView() + { + InitializeComponent(); + MemoryViewer.AsciiFormatter = new ZxAsciiConverter(); + } + + protected override void OnDataContextChanged(EventArgs e) + { + if (DataContext is not MemoryViewModel viewModel) + { + return; + } + + viewModel.Clipboard = Clipboard; + viewModel.Viewer = MemoryViewer; + + viewModel.GoTo = address => + Dispatcher.UIThread.Post(() => MemoryViewer.Select(address)); + + viewModel.Select = (address, length) => + Dispatcher.UIThread.Post(() => MemoryViewer.Select(address, length)); + + viewModel.OnMemoryUpdated = (address, value) => + Dispatcher.UIThread.Post(() => MemoryViewer.UpdateValues(address, value)); + } +} \ No newline at end of file diff --git a/src/Spectron.Emulation/Devices/Memory/IEmulatorMemory.cs b/src/Spectron.Emulation/Devices/Memory/IEmulatorMemory.cs index d59c9d86..c85c3fe3 100644 --- a/src/Spectron.Emulation/Devices/Memory/IEmulatorMemory.cs +++ b/src/Spectron.Emulation/Devices/Memory/IEmulatorMemory.cs @@ -2,7 +2,7 @@ namespace OldBit.Spectron.Emulation.Devices.Memory; -public delegate void MemoryUpdatedEvent(Word address); +public delegate void MemoryUpdatedEvent(Word address, byte value); public interface IEmulatorMemory : IMemory, IDevice { diff --git a/src/Spectron.Emulation/Devices/Memory/Memory128K.cs b/src/Spectron.Emulation/Devices/Memory/Memory128K.cs index 8b975390..9edeefd6 100644 --- a/src/Spectron.Emulation/Devices/Memory/Memory128K.cs +++ b/src/Spectron.Emulation/Devices/Memory/Memory128K.cs @@ -78,7 +78,7 @@ public void Write(Word address, byte data) bank[relativeAddress] = data; - MemoryUpdated?.Invoke(address); + MemoryUpdated?.Invoke(address, data); } public void ShadowRom(IRomMemory? shadowRom) diff --git a/src/Spectron.Emulation/Devices/Memory/Memory16K.cs b/src/Spectron.Emulation/Devices/Memory/Memory16K.cs index bbf22cce..9d3af762 100644 --- a/src/Spectron.Emulation/Devices/Memory/Memory16K.cs +++ b/src/Spectron.Emulation/Devices/Memory/Memory16K.cs @@ -44,7 +44,7 @@ public void Write(Word address, byte data) _memory[address] = data; - MemoryUpdated?.Invoke(address); + MemoryUpdated?.Invoke(address, data); } public void ShadowRom(IRomMemory? shadowRom) => _activeRom = shadowRom ?? OriginalRom; diff --git a/src/Spectron.Emulation/Devices/Memory/Memory48K.cs b/src/Spectron.Emulation/Devices/Memory/Memory48K.cs index ade206a4..2980fba2 100644 --- a/src/Spectron.Emulation/Devices/Memory/Memory48K.cs +++ b/src/Spectron.Emulation/Devices/Memory/Memory48K.cs @@ -39,7 +39,7 @@ public void Write(Word address, byte data) _memory[address] = data; - MemoryUpdated?.Invoke(address); + MemoryUpdated?.Invoke(address, data); } public void ShadowRom(IRomMemory? shadowRom) => _activeRom = shadowRom ?? OriginalRom; diff --git a/src/Spectron.Emulation/Emulator.cs b/src/Spectron.Emulation/Emulator.cs index 95950e4b..283fc0e3 100644 --- a/src/Spectron.Emulation/Emulator.cs +++ b/src/Spectron.Emulation/Emulator.cs @@ -233,7 +233,7 @@ private void OnTimerElapsed(object? sender, EventArgs e) private void SetupEventHandlers() { - _memory.MemoryUpdated += address => + _memory.MemoryUpdated += (address, _) => { if (address < 0x5B00) { diff --git a/src/Spectron.Emulation/Extensions/MemoryExtensions.cs b/src/Spectron.Emulation/Extensions/MemoryExtensions.cs index 378736fc..d0949380 100644 --- a/src/Spectron.Emulation/Extensions/MemoryExtensions.cs +++ b/src/Spectron.Emulation/Extensions/MemoryExtensions.cs @@ -5,72 +5,75 @@ namespace OldBit.Spectron.Emulation.Extensions; public static class MemoryExtensions { - public static List ReadBytes(this IMemory memory, Word address, int count) + extension(IMemory memory) { - var bytes = new List(); - - for (var i = 0; i < count; i++) + public List ReadBytes(Word address, int count) { - bytes.Add(memory.Read((Word)(address + i))); - } + var bytes = new List(); - return bytes; - } + for (var i = 0; i < count; i++) + { + bytes.Add(memory.Read((Word)(address + i))); + } - public static byte? Read(this IMemory memory, Word address, int? bank = null) - { - if (bank is < 8 && memory is Memory128K memory128) - { - return memory128.Banks[bank.Value][address - 0xC000]; + return bytes; } - return memory.Read(address); - } - - public static void Write(this IMemory memory, Word address, byte value, int? bank = null) - { - if (bank is < 8 && memory is Memory128K memory128) + public byte? Read(Word address, int? bank = null) { - memory128.Banks[bank.Value][address - 0xC000] = value; + if (bank is < 8 && memory is Memory128K memory128) + { + return memory128.Banks[bank.Value][address - 0xC000]; + } + + return memory.Read(address); } - else + + public void Write(Word address, byte value, int? bank = null) { - memory.Write(address, value); + if (bank is < 8 && memory is Memory128K memory128) + { + memory128.Banks[bank.Value][address - 0xC000] = value; + } + else + { + memory.Write(address, value); + } } - } - - public static byte[] GetBytes(this IMemory memory) - { - var memory64 = new byte[65536]; - switch (memory) + public byte[] ToBytes() { - case Memory48K memory48: - memory48.Rom.CopyTo(memory64); - memory48.Ram.CopyTo(memory64.AsSpan(0x4000)); - break; + var memory64 = new byte[65536]; + + switch (memory) + { + case Memory48K memory48: + memory48.Rom.CopyTo(memory64); + memory48.Ram.CopyTo(memory64.AsSpan(0x4000)); + break; - case Memory16K memory16: - memory16.Rom.CopyTo(memory64); - memory16.Ram.CopyTo(memory64.AsSpan(0x4000)); - break; + case Memory16K memory16: + memory16.Rom.CopyTo(memory64); + memory16.Ram.CopyTo(memory64.AsSpan(0x4000)); + break; - case Memory128K memory128: - var banks = memory128.ActiveBanks; + case Memory128K memory128: + var banks = memory128.ActiveBanks; - Array.Copy(banks[0], 0, memory64, 0, 0x4000); - Array.Copy(banks[1], 0, memory64, 0x4000, 0x4000); - Array.Copy(banks[2], 0, memory64, 0x8000, 0x4000); - Array.Copy(banks[3], 0, memory64, 0xC000, 0x4000); - break; + Array.Copy(banks[0], 0, memory64, 0, 0x4000); + Array.Copy(banks[1], 0, memory64, 0x4000, 0x4000); + Array.Copy(banks[2], 0, memory64, 0x8000, 0x4000); + Array.Copy(banks[3], 0, memory64, 0xC000, 0x4000); + break; - default: - throw new NotSupportedException("Memory type not supported."); + default: + throw new NotSupportedException("Memory type not supported."); + } + + return memory64; } - return memory64; + public Word ReadWord(Word address) => + (Word)(memory.Read(address) | memory.Read((Word)(address + 1)) << 8); } - - public static Word ReadWord(this IMemory memory, Word address) => - (Word)(memory.Read(address) | memory.Read((Word)(address + 1)) << 8); } \ No newline at end of file diff --git a/src/Spectron/App.axaml b/src/Spectron/App.axaml index 7d342dbf..9ba73e66 100644 --- a/src/Spectron/App.axaml +++ b/src/Spectron/App.axaml @@ -21,7 +21,8 @@ - + + diff --git a/src/Spectron/Controls/MainMenu.axaml b/src/Spectron/Controls/MainMenu.axaml index 6590cdde..1ca95559 100644 --- a/src/Spectron/Controls/MainMenu.axaml +++ b/src/Spectron/Controls/MainMenu.axaml @@ -257,8 +257,11 @@ - + + + + diff --git a/src/Spectron/Controls/Validation/AdornerAnchorPanel.cs b/src/Spectron/Controls/Validation/AdornerAnchorPanel.cs new file mode 100644 index 00000000..852f88b5 --- /dev/null +++ b/src/Spectron/Controls/Validation/AdornerAnchorPanel.cs @@ -0,0 +1,224 @@ +using Avalonia; +using Avalonia.Controls; + +// Copyright (c) 2023 Jean-Paul Mikkers +// https://github.com/jpmikkers/Baksteen.Avalonia.Controls.AdornerAnchorPanel/blob/main/AdornerAnchorPanel/AdornerAnchorPanel.cs +namespace OldBit.Spectron.Controls.Validation; + +/// +/// A panel that displays child controls at arbitrary locations defined by fractional anchors. +/// +public class AdornerAnchorPanel : Panel +{ + /// + /// Defines the RootAnchorH attached property. + /// + public static readonly AttachedProperty RootAnchorHProperty = + AvaloniaProperty.RegisterAttached("RootAnchorH", 0.0); + + /// + /// Defines the RootAnchorV attached property. + /// + public static readonly AttachedProperty RootAnchorVProperty = + AvaloniaProperty.RegisterAttached("RootAnchorV", 0.0); + + /// + /// Defines the ChildAnchorH attached property. + /// + public static readonly AttachedProperty ChildAnchorHProperty = + AvaloniaProperty.RegisterAttached("ChildAnchorH", 0.0); + + /// + /// Defines the ChildAnchorV attached property. + /// + public static readonly AttachedProperty ChildAnchorVProperty = + AvaloniaProperty.RegisterAttached("ChildAnchorV", 0.0); + + /// + /// Defines the OffsetH attached property. + /// + public static readonly AttachedProperty OffsetHProperty = + AvaloniaProperty.RegisterAttached("OffsetH", 0.0); + + /// + /// Defines the OffsetV attached property. + /// + public static readonly AttachedProperty OffsetVProperty = + AvaloniaProperty.RegisterAttached("OffsetV", 0.0); + + /// + /// Initializes static members of the class. + /// + static AdornerAnchorPanel() + { + ClipToBoundsProperty.OverrideDefaultValue(false); + AffectsParentArrange( + RootAnchorHProperty, + RootAnchorVProperty, + ChildAnchorHProperty, + ChildAnchorVProperty, + OffsetHProperty, + OffsetVProperty); + } + + /// + /// Gets the value of the RootAnchorH attached property for a control. + /// + /// The control. + /// The control's horizontal root anchor. + public static double GetRootAnchorH(AvaloniaObject element) + { + return element.GetValue(RootAnchorHProperty); + } + + /// + /// Sets the value of the RootAnchorH attached property for a control. + /// + /// The control. + /// The horizontal root anchor. + public static void SetRootAnchorH(AvaloniaObject element, double value) + { + element.SetValue(RootAnchorHProperty, value); + } + + /// + /// Gets the value of the RootAnchorV attached property for a control. + /// + /// The control. + /// The control's vertical root anchor. + public static double GetRootAnchorV(AvaloniaObject element) + { + return element.GetValue(RootAnchorVProperty); + } + + /// + /// Sets the value of the RootAnchorV attached property for a control. + /// + /// The control. + /// The vertical root anchor. + public static void SetRootAnchorV(AvaloniaObject element, double value) + { + element.SetValue(RootAnchorVProperty, value); + } + + /// + /// Gets the value of the ChildAnchorH attached property for a control. + /// + /// The control. + /// The control's horizontal anchor. + public static double GetChildAnchorH(AvaloniaObject element) + { + return element.GetValue(ChildAnchorHProperty); + } + + /// + /// Sets the value of the ChildAnchorH attached property for a control. + /// + /// The control. + /// The horizontal anchor. + public static void SetChildAnchorH(AvaloniaObject element, double value) + { + element.SetValue(ChildAnchorHProperty, value); + } + + /// + /// Gets the value of the ChildAnchorV attached property for a control. + /// + /// The control. + /// The control's vertical anchor. + public static double GetChildAnchorV(AvaloniaObject element) + { + return element.GetValue(ChildAnchorVProperty); + } + + /// + /// Sets the value of the ChildAnchorV attached property for a control. + /// + /// The control. + /// The vertical anchor. + public static void SetChildAnchorV(AvaloniaObject element, double value) + { + element.SetValue(ChildAnchorVProperty, value); + } + + /// + /// Gets the value of the OffsetH attached property for a control. + /// + /// The control. + /// The horizontal pixel offset between the anchors. + public static double GetOffsetH(AvaloniaObject element) + { + return element.GetValue(OffsetHProperty); + } + + /// + /// Sets the value of the OffsetH attached property for a control. + /// + /// The control. + /// The horizontal pixel offset between the anchors. + public static void SetOffsetH(AvaloniaObject element, double value) + { + element.SetValue(OffsetHProperty, value); + } + + /// + /// Gets the value of the OffsetV attached property for a control. + /// + /// The control. + /// The vertical pixel offset between the anchors. + public static double GetOffsetV(AvaloniaObject element) + { + return element.GetValue(OffsetVProperty); + } + + /// + /// Sets the value of the OffsetV attached property for a control. + /// + /// The control. + /// The vertical pixel offset between the anchors. + public static void SetOffsetV(AvaloniaObject element, double value) + { + element.SetValue(OffsetVProperty, value); + } + + /// + /// Measures the control. + /// + /// The available size. + /// The desired size of the control. + protected override Size MeasureOverride(Size orgAvailableSize) + { + var availableSize = new Size(double.PositiveInfinity, double.PositiveInfinity); + + foreach(var child in Children) + { + child.Measure(availableSize); + } + + return new Size( + double.IsPositiveInfinity(orgAvailableSize.Width) ? 0.0 : orgAvailableSize.Width, + double.IsPositiveInfinity(orgAvailableSize.Height) ? 0.0 : orgAvailableSize.Height); + } + + private double Interpolate(double from, double to, double progress) => from + (to - from) * progress; + + /// + /// Arranges the control's children. + /// + /// The size allocated to the control. + /// The space taken. + protected override Size ArrangeOverride(Size finalSize) + { + foreach(Control child in Children) + { + var rootAnchorX = Interpolate(0, finalSize.Width, GetRootAnchorH(child)); + var rootAnchorY = Interpolate(0, finalSize.Height, GetRootAnchorV(child)); + var childAnchorX = Interpolate(0, child.DesiredSize.Width, GetChildAnchorH(child)); + var childAnchorY = Interpolate(0, child.DesiredSize.Height, GetChildAnchorV(child)); + + child.Arrange(new Rect(new Point(rootAnchorX - childAnchorX + GetOffsetH(child), rootAnchorY - childAnchorY + GetOffsetV(child)), child.DesiredSize)); + } + + return finalSize; + } +} \ No newline at end of file diff --git a/src/Spectron/Controls/Validation/DataValidationErrors.axaml b/src/Spectron/Controls/Validation/DataValidationErrors.axaml new file mode 100644 index 00000000..4f296c1a --- /dev/null +++ b/src/Spectron/Controls/Validation/DataValidationErrors.axaml @@ -0,0 +1,68 @@ + + + \ No newline at end of file diff --git a/src/Spectron/Messages/ShowMemoryViewMessage.cs b/src/Spectron/Messages/ShowMemoryViewMessage.cs new file mode 100644 index 00000000..b9ea9512 --- /dev/null +++ b/src/Spectron/Messages/ShowMemoryViewMessage.cs @@ -0,0 +1,5 @@ +using OldBit.Spectron.Debugger.ViewModels; + +namespace OldBit.Spectron.Messages; + +public record ShowMemoryViewMessage(MemoryViewModel ViewModel); \ No newline at end of file diff --git a/src/Spectron/ViewModels/MainWindowViewModel.Debugger.cs b/src/Spectron/ViewModels/MainWindowViewModel.Debugger.cs index ac674edf..8772db88 100644 --- a/src/Spectron/ViewModels/MainWindowViewModel.Debugger.cs +++ b/src/Spectron/ViewModels/MainWindowViewModel.Debugger.cs @@ -32,6 +32,17 @@ private void OpenDebuggerWindow(BreakpointHitEventArgs? breakpointHitEventArgs = WeakReferenceMessenger.Default.Send(new ShowDebuggerViewMessage(_debuggerViewModel)); } + private void OpenMemoryWindow() + { + if (Emulator == null) + { + return; + } + + var memoryViewModel = new MemoryViewModel(Emulator); + WeakReferenceMessenger.Default.Send(new ShowMemoryViewMessage(memoryViewModel)); + } + private void ConfigureDebugging(Emulator emulator) { if (_breakpointHandler == null) diff --git a/src/Spectron/ViewModels/MainWindowViewModel.cs b/src/Spectron/ViewModels/MainWindowViewModel.cs index 12f65250..fb8acbfc 100644 --- a/src/Spectron/ViewModels/MainWindowViewModel.cs +++ b/src/Spectron/ViewModels/MainWindowViewModel.cs @@ -275,6 +275,9 @@ private void TakeScreenshot() [RelayCommand] private void ToggleBreakpoints() => BreakpointsEnabled = !BreakpointsEnabled; + [RelayCommand] + private void ShowMemoryView() => OpenMemoryWindow(); + // Help [RelayCommand] private static void ShowAboutView() => OpenAboutWindow(); diff --git a/src/Spectron/Views/MainWindow.axaml.cs b/src/Spectron/Views/MainWindow.axaml.cs index ccdb811b..ce04d94b 100644 --- a/src/Spectron/Views/MainWindow.axaml.cs +++ b/src/Spectron/Views/MainWindow.axaml.cs @@ -39,6 +39,9 @@ public MainWindow() WeakReferenceMessenger.Default.Register(this, (_, m) => Show(null, m.ViewModel)); + WeakReferenceMessenger.Default.Register(this, (window, m) => + Show(window, m.ViewModel!)); + WeakReferenceMessenger.Default.Register(this, (window, message) => { var result = ShowDialog(window, new PreferencesViewModel(message.Preferences, message.GamepadManager)); @@ -124,6 +127,7 @@ public MainWindow() else { window.Open(owner); + window.Activate(); } return; diff --git a/tests/Spectron.Debugger.Tests/Converters/HexTests.cs b/tests/Spectron.Debugger.Tests/Converters/HexTests.cs new file mode 100644 index 00000000..95be43ed --- /dev/null +++ b/tests/Spectron.Debugger.Tests/Converters/HexTests.cs @@ -0,0 +1,101 @@ +using OldBit.Spectron.Debugger.Converters; + +namespace OldBit.Spectron.Debugger.Tests.Converters; + +public class HexTests +{ + [Theory] + [InlineData("12", 18)] + [InlineData("0x12", 18)] + [InlineData("0X12", 18)] + [InlineData("$12", 18)] + [InlineData("#12", 18)] + [InlineData("12h", 18)] + [InlineData("12H", 18)] + [InlineData("0012H", 18)] + [InlineData(" 0012H ", 18)] + public void HexString_ShouldConvertToByte(string input, byte expected) + { + var isSuccess = Hex.TryParse(input, out var result); + + isSuccess.ShouldBeTrue(); + result.ShouldBe(expected); + } + + [Theory] + [InlineData("F2", -14)] + [InlineData("0xF2", -14)] + [InlineData("0XF2", -14)] + [InlineData("$F2", -14)] + [InlineData("#F2", -14)] + [InlineData("F2h", -14)] + [InlineData("F2H", -14)] + [InlineData("00F2H", -14)] + [InlineData(" 00F2H ", -14)] + public void HexString_ShouldConvertToSignedByte(string input, sbyte expected) + { + var isSuccess = Hex.TryParse(input, out var result); + + isSuccess.ShouldBeTrue(); + result.ShouldBe(expected); + } + + [Theory] + [InlineData("1234", 4660)] + [InlineData("0x1234", 4660)] + [InlineData("0X1234", 4660)] + [InlineData("$1234", 4660)] + [InlineData("#1234", 4660)] + [InlineData("1234h", 4660)] + [InlineData("1234H", 4660)] + [InlineData("001234H", 4660)] + [InlineData(" 001234H ", 4660)] + public void HexString_ShouldConvertToWord(string input, Word expected) + { + var isSuccess = Hex.TryParse(input, out var result); + + isSuccess.ShouldBeTrue(); + result.ShouldBe(expected); + } + + [Theory] + [InlineData("F234", -3532)] + [InlineData("0xF234", -3532)] + [InlineData("0XF234", -3532)] + [InlineData("$F234", -3532)] + [InlineData("#F234", -3532)] + [InlineData("F234h", -3532)] + [InlineData("F234H", -3532)] + [InlineData("00F234H", -3532)] + [InlineData(" 00F234H ", -3532)] + public void HexString_ShouldConvertToSignedWord(string input, short expected) + { + var isSuccess = Hex.TryParse(input, out var result); + + isSuccess.ShouldBeTrue(); + result.ShouldBe(expected); + } + + [Theory] + [InlineData("12345", 12345)] + [InlineData("1234A", 74570)] + public void HexString_ShouldPreferDecimal(string input, int expected) + { + var isSuccess = Hex.TryParse(input, out var result, preferDecimal: true); + + isSuccess.ShouldBeTrue(); + result.ShouldBe(expected); + } + + [Theory] + [InlineData("120G")] + [InlineData("test")] + [InlineData("-123")] + [InlineData("0x100h")] + public void InvalidHexString_ShouldNotParse(string input) + { + var isSuccess = Hex.TryParse(input, out var _); + + isSuccess.ShouldBeFalse(); + } +} \ No newline at end of file