├── test
└── AvaloniaHex.Tests
│ ├── GlobalUsings.cs
│ ├── Document
│ ├── BitLocationTest.cs
│ └── BitRangeTest.cs
│ └── AvaloniaHex.Tests.csproj
├── assets
└── demo.gif
├── examples
└── AvaloniaHex.Demo
│ ├── Resources
│ └── SourceCodePro-Regular.ttf
│ ├── App.axaml.cs
│ ├── App.axaml
│ ├── Program.cs
│ ├── InputDialog.axaml
│ ├── InputDialog.axaml.cs
│ ├── RealTimeChangingDocument.cs
│ ├── AvaloniaHex.Demo.csproj
│ ├── SegmentedDocument.cs
│ ├── MainWindow.axaml
│ └── .gitignore
├── src
└── AvaloniaHex
│ ├── Rendering
│ ├── ILineTransformer.cs
│ ├── InvalidRangesHighlighter.cs
│ ├── ZeroesHighlighter.cs
│ ├── RangesHighlighter.cs
│ ├── SimpleTextSource.cs
│ ├── ColumnBackgroundLayer.cs
│ ├── Layer.cs
│ ├── LayerRenderMoments.cs
│ ├── TextLayer.cs
│ ├── TextRunExtensions.cs
│ ├── VisualBytesLineSegment.cs
│ ├── CellGeometryBuilder.cs
│ ├── OffsetColumn.cs
│ ├── HeaderLayer.cs
│ ├── ByteHighlighter.cs
│ ├── CellGroupsLayer.cs
│ ├── VisualBytesLinesBuffer.cs
│ ├── AsciiColumn.cs
│ ├── VisualBytesLine.cs
│ ├── BinaryColumn.cs
│ ├── HexColumn.cs
│ └── Column.cs
│ ├── Editing
│ ├── EditingMode.cs
│ ├── Selection.cs
│ ├── CurrentLineLayer.cs
│ ├── SelectionLayer.cs
│ ├── CaretLayer.cs
│ └── Caret.cs
│ ├── Document
│ ├── DocumentChangedEventArgs.cs
│ ├── BinaryDocumentChange.cs
│ ├── ReadOnlyBitRangeUnion.cs
│ ├── IReadOnlyBitRangeUnion.cs
│ ├── IBinaryDocument.cs
│ ├── ByteArrayBinaryDocument.cs
│ ├── MemoryBinaryDocument.cs
│ ├── DynamicBinaryDocument.cs
│ ├── MemoryMappedBinaryDocument.cs
│ ├── BitRange.cs
│ ├── BitRangeUnion.cs
│ └── BitLocation.cs
│ ├── HexEditor.axaml
│ ├── AvaloniaHex.csproj
│ └── Themes
│ ├── Base.axaml
│ ├── Simple
│ └── AvaloniaHex.axaml
│ └── Fluent
│ └── AvaloniaHex.axaml
├── appveyor.yml
├── Directory.Build.props
├── LICENSE.md
├── README.md
├── AvaloniaHex.sln
└── .gitignore
/test/AvaloniaHex.Tests/GlobalUsings.cs:
--------------------------------------------------------------------------------
1 | global using Xunit;
--------------------------------------------------------------------------------
/assets/demo.gif:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Washi1337/AvaloniaHex/HEAD/assets/demo.gif
--------------------------------------------------------------------------------
/examples/AvaloniaHex.Demo/Resources/SourceCodePro-Regular.ttf:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/Washi1337/AvaloniaHex/HEAD/examples/AvaloniaHex.Demo/Resources/SourceCodePro-Regular.ttf
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/ILineTransformer.cs:
--------------------------------------------------------------------------------
1 | namespace AvaloniaHex.Rendering;
2 |
3 | ///
4 | /// Provides members for transforming a visual line.
5 | ///
6 | public interface ILineTransformer
7 | {
8 | ///
9 | /// Transforms a visual line.
10 | ///
11 | ///
12 | /// The line to transform.
13 | void Transform(HexView hexView, VisualBytesLine line);
14 | }
15 |
--------------------------------------------------------------------------------
/src/AvaloniaHex/Editing/EditingMode.cs:
--------------------------------------------------------------------------------
1 | namespace AvaloniaHex.Editing;
2 |
3 | ///
4 | /// Provides members describing all possible editing modes a caret in a hex editor can be.
5 | ///
6 | public enum EditingMode
7 | {
8 | ///
9 | /// Indicates the cursor is overwriting the existing bytes.
10 | ///
11 | Overwrite,
12 |
13 | ///
14 | /// Indicates the cursor is inserting new bytes.
15 | ///
16 | Insert
17 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/InvalidRangesHighlighter.cs:
--------------------------------------------------------------------------------
1 | using AvaloniaHex.Document;
2 |
3 | namespace AvaloniaHex.Rendering;
4 |
5 | ///
6 | /// Provides an implementation of a highlighter that highlights all invalid ranges in a document.
7 | ///
8 | public class InvalidRangesHighlighter : ByteHighlighter
9 | {
10 | ///
11 | protected override bool IsHighlighted(HexView hexView, VisualBytesLine line, BitLocation location)
12 | {
13 | return !hexView.Document?.ValidRanges.Contains(location) ?? false;
14 | }
15 | }
--------------------------------------------------------------------------------
/appveyor.yml:
--------------------------------------------------------------------------------
1 | -
2 | branches:
3 | only:
4 | - main
5 |
6 | image: Visual Studio 2022
7 | version: 0.1.10-master-build.{build}
8 | configuration: Release
9 |
10 | before_build:
11 | - dotnet restore
12 |
13 | build:
14 | verbosity: minimal
15 |
16 | artifacts:
17 | - path: 'src\AvaloniaHex\bin\Release\*.nupkg'
18 |
19 | deploy:
20 | provider: NuGet
21 | api_key:
22 | secure: HyapzsqHiM9VMD2qxG9cPHTu+j4o8A5/sEKY3duRML7uw1JtxcWQFHy1GLy3HMjr
23 | skip_symbols: false
24 | artifact: /.*\.nupkg/
25 |
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/ZeroesHighlighter.cs:
--------------------------------------------------------------------------------
1 | using AvaloniaHex.Document;
2 |
3 | namespace AvaloniaHex.Rendering;
4 |
5 | ///
6 | /// Provides an implementation of a highlighter that highlights all zero bytes in a visual line.
7 | ///
8 | public class ZeroesHighlighter : ByteHighlighter
9 | {
10 | ///
11 | protected override bool IsHighlighted(HexView hexView, VisualBytesLine line, BitLocation location)
12 | {
13 | return hexView.Document!.ValidRanges.Contains(location) && line.GetByteAtAbsolute(location.ByteIndex) == 0;
14 | }
15 | }
--------------------------------------------------------------------------------
/Directory.Build.props:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | Copyright © Washi 2024-2025
5 | https://github.com/Washi1337/AvaloniaHex
6 | https://github.com/Washi1337/AvaloniaHex/LICENSE.md
7 | https://github.com/Washi1337/AvaloniaHex
8 | git
9 | 12
10 | 0.1.10
11 | true
12 |
13 |
14 |
15 |
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/RangesHighlighter.cs:
--------------------------------------------------------------------------------
1 | using AvaloniaHex.Document;
2 |
3 | namespace AvaloniaHex.Rendering;
4 |
5 | ///
6 | /// Highlights ranges of bytes within a document of a hex view.
7 | ///
8 | public class RangesHighlighter : ByteHighlighter
9 | {
10 | ///
11 | /// Gets the bit ranges that should be highlighted in the document.
12 | ///
13 | public BitRangeUnion Ranges { get; } = new();
14 |
15 | ///
16 | protected override bool IsHighlighted(HexView hexView, VisualBytesLine line, BitLocation location)
17 | {
18 | return Ranges.Contains(location);
19 | }
20 | }
--------------------------------------------------------------------------------
/examples/AvaloniaHex.Demo/App.axaml.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Controls.ApplicationLifetimes;
3 | using Avalonia.Markup.Xaml;
4 |
5 | namespace AvaloniaHex.Demo
6 | {
7 | public partial class App : Application
8 | {
9 | public override void Initialize()
10 | {
11 | AvaloniaXamlLoader.Load(this);
12 | }
13 |
14 | public override void OnFrameworkInitializationCompleted()
15 | {
16 | if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktop)
17 | {
18 | desktop.MainWindow = new MainWindow();
19 | }
20 |
21 | base.OnFrameworkInitializationCompleted();
22 | }
23 | }
24 | }
--------------------------------------------------------------------------------
/examples/AvaloniaHex.Demo/App.axaml:
--------------------------------------------------------------------------------
1 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 | avares://AvaloniaHex.Demo/Resources#Source Code Pro
14 |
15 |
--------------------------------------------------------------------------------
/examples/AvaloniaHex.Demo/Program.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Avalonia;
3 |
4 | namespace AvaloniaHex.Demo
5 | {
6 | class Program
7 | {
8 | // Initialization code. Don't use any Avalonia, third-party APIs or any
9 | // SynchronizationContext-reliant code before AppMain is called: things aren't initialized
10 | // yet and stuff might break.
11 | [STAThread]
12 | public static void Main(string[] args) => BuildAvaloniaApp()
13 | .StartWithClassicDesktopLifetime(args);
14 |
15 | // Avalonia configuration, don't remove; also used by visual designer.
16 | public static AppBuilder BuildAvaloniaApp()
17 | => AppBuilder.Configure()
18 | .UsePlatformDetect()
19 | .LogToTrace();
20 | }
21 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/SimpleTextSource.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Media.TextFormatting;
2 |
3 | namespace AvaloniaHex.Rendering;
4 |
5 | ///
6 | /// Wraps a string into a instance.
7 | ///
8 | internal readonly struct SimpleTextSource : ITextSource
9 | {
10 | private readonly TextRunProperties _defaultProperties;
11 | private readonly string _text;
12 |
13 | public SimpleTextSource(string text, TextRunProperties defaultProperties)
14 | {
15 | _text = text;
16 | _defaultProperties = defaultProperties;
17 | }
18 |
19 | public TextRun GetTextRun(int textSourceIndex)
20 | {
21 | if (textSourceIndex >= _text.Length)
22 | return new TextEndOfParagraph();
23 |
24 | return new TextCharacters(_text, _defaultProperties);
25 | }
26 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/ColumnBackgroundLayer.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Media;
2 |
3 | namespace AvaloniaHex.Rendering;
4 |
5 | ///
6 | /// Represents the column background rendering layer in a hex view.
7 | ///
8 | public class ColumnBackgroundLayer : Layer
9 | {
10 | ///
11 | public override LayerRenderMoments UpdateMoments => LayerRenderMoments.Minimal;
12 |
13 | ///
14 | public override void Render(DrawingContext context)
15 | {
16 | base.Render(context);
17 |
18 | if (HexView is null)
19 | return;
20 |
21 | foreach (var column in HexView.Columns)
22 | {
23 | if (column.Background is not null || column.Border is not null)
24 | context.DrawRectangle(column.Background, column.Border, column.Bounds);
25 | }
26 | }
27 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Document/DocumentChangedEventArgs.cs:
--------------------------------------------------------------------------------
1 | namespace AvaloniaHex.Document;
2 |
3 | ///
4 | /// Describes a change of documents in a hex view or editor.
5 | ///
6 | public class DocumentChangedEventArgs : EventArgs
7 | {
8 | ///
9 | /// Constructs a new document change event.
10 | ///
11 | /// The old document.
12 | /// The new document.
13 | public DocumentChangedEventArgs(IBinaryDocument? old, IBinaryDocument? @new)
14 | {
15 | Old = old;
16 | New = @new;
17 | }
18 |
19 | ///
20 | /// Gets the original document.
21 | ///
22 | public IBinaryDocument? Old { get; }
23 |
24 | ///
25 | /// Gets the new document.
26 | ///
27 | public IBinaryDocument? New { get; }
28 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/Layer.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Controls;
2 | using Avalonia.Media;
3 | using AvaloniaHex.Editing;
4 |
5 | namespace AvaloniaHex.Rendering;
6 |
7 | ///
8 | /// Represents a single layer in the hex view rendering.
9 | ///
10 | public abstract class Layer : Control
11 | {
12 | ///
13 | /// Gets a value indicating when the layer should be rendered.
14 | ///
15 | public virtual LayerRenderMoments UpdateMoments => LayerRenderMoments.Always;
16 |
17 | ///
18 | /// Gets the parent hex view the layer is added to.
19 | ///
20 | public HexView? HexView
21 | {
22 | get;
23 | internal set;
24 | }
25 |
26 | ///
27 | public override void Render(DrawingContext context)
28 | {
29 | base.Render(context);
30 | }
31 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/LayerRenderMoments.cs:
--------------------------------------------------------------------------------
1 | namespace AvaloniaHex.Rendering;
2 |
3 | ///
4 | /// Provides members describing when a layer should be rendered.
5 | ///
6 | [Flags]
7 | public enum LayerRenderMoments
8 | {
9 | ///
10 | /// Indicates the layer should only be rendered minimally.
11 | ///
12 | Minimal = 0,
13 |
14 | ///
15 | /// Indicates the layer should be rendered when a rearrange of the text is queued.
16 | ///
17 | NoResizeRearrange = 1,
18 |
19 | ///
20 | /// Indicates the layer should be rendered when a single line was invalidated.
21 | ///
22 | LineInvalidate = 2,
23 |
24 | ///
25 | /// Indicates the layer should always be rendered on every update.
26 | ///
27 | Always = NoResizeRearrange | LineInvalidate,
28 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Document/BinaryDocumentChange.cs:
--------------------------------------------------------------------------------
1 | namespace AvaloniaHex.Document;
2 |
3 | ///
4 | /// Describes a change in a binary document.
5 | ///
6 | /// The action that was performed.
7 | /// The range within the document that was affected.
8 | public record struct BinaryDocumentChange(BinaryDocumentChangeType Type, BitRange AffectedRange);
9 |
10 | ///
11 | /// Provides members describing the possible actions that can be applied to a document.
12 | ///
13 | public enum BinaryDocumentChangeType
14 | {
15 | ///
16 | /// Indicates the document was modified in-place.
17 | ///
18 | Modify,
19 |
20 | ///
21 | /// Indicates bytes were inserted into the document.
22 | ///
23 | Insert,
24 |
25 | ///
26 | /// Indicates the bytes were removed from the document.
27 | ///
28 | Remove,
29 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/TextLayer.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Media;
3 |
4 | namespace AvaloniaHex.Rendering;
5 |
6 | ///
7 | /// Represents the layer that renders the text in a hex view.
8 | ///
9 | public class TextLayer : Layer
10 | {
11 | ///
12 | public override void Render(DrawingContext context)
13 | {
14 | base.Render(context);
15 |
16 | if (HexView is null)
17 | return;
18 |
19 | double currentY = HexView.EffectiveHeaderSize;
20 | for (int i = 0; i < HexView.VisualLines.Count; i++)
21 | {
22 | var line = HexView.VisualLines[i];
23 | foreach (var column in HexView.Columns)
24 | {
25 | if (column.IsVisible)
26 | line.ColumnTextLines[column.Index]?.Draw(context, new Point(column.Bounds.Left, currentY));
27 | }
28 |
29 | currentY += line.Bounds.Height;
30 | }
31 | }
32 | }
--------------------------------------------------------------------------------
/test/AvaloniaHex.Tests/Document/BitLocationTest.cs:
--------------------------------------------------------------------------------
1 | using AvaloniaHex.Document;
2 |
3 | namespace AvaloniaHex.Tests.Document;
4 |
5 | public class BitLocationTest
6 | {
7 | [Fact]
8 | public void InvalidBitIndex()
9 | {
10 | Assert.Throws(() => new BitLocation(0, 8));
11 | Assert.Throws(() => new BitLocation(0, -3));
12 | }
13 |
14 | [Theory]
15 | [InlineData(0, 0, 3, 0, 3)]
16 | [InlineData(0, 0, 8+8+8+3, 3, 3)]
17 | [InlineData(0, 2, 3, 0, 5)]
18 | [InlineData(0, 7, 1, 1, 0)]
19 | [InlineData(0, 7, 3, 1, 2)]
20 | [InlineData(0, 7, 8+8+8+3, 4, 2)]
21 | public void AddBits(ulong startByteIndex, int startBitIndex, ulong bitCount, ulong endByteIndex, int endBitIndex)
22 | {
23 | var location = new BitLocation(startByteIndex, startBitIndex);
24 | var newLocation = location.AddBits(bitCount);
25 | Assert.Equal(new BitLocation(endByteIndex, endBitIndex), newLocation);
26 | }
27 | }
--------------------------------------------------------------------------------
/LICENSE.md:
--------------------------------------------------------------------------------
1 | The MIT License (MIT)
2 | =====================
3 |
4 | Copyright © `2024-2025` `Washi`
5 |
6 | Permission is hereby granted, free of charge, to any person
7 | obtaining a copy of this software and associated documentation
8 | files (the “Software”), to deal in the Software without
9 | restriction, including without limitation the rights to use,
10 | copy, modify, merge, publish, distribute, sublicense, and/or sell
11 | copies of the Software, and to permit persons to whom the
12 | Software is furnished to do so, subject to the following
13 | conditions:
14 |
15 | The above copyright notice and this permission notice shall be
16 | included in all copies or substantial portions of the Software.
17 |
18 | THE SOFTWARE IS PROVIDED “AS IS”, WITHOUT WARRANTY OF ANY KIND,
19 | EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
20 | OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
21 | NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
22 | HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
23 | WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
24 | FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
25 | OTHER DEALINGS IN THE SOFTWARE.
26 |
27 |
--------------------------------------------------------------------------------
/test/AvaloniaHex.Tests/AvaloniaHex.Tests.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | net6.0
5 | enable
6 | enable
7 |
8 | false
9 | true
10 |
11 |
12 |
13 |
14 |
15 |
16 | runtime; build; native; contentfiles; analyzers; buildtransitive
17 | all
18 |
19 |
20 | runtime; build; native; contentfiles; analyzers; buildtransitive
21 | all
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/src/AvaloniaHex/HexEditor.axaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
14 |
22 |
23 |
24 |
25 |
26 |
--------------------------------------------------------------------------------
/src/AvaloniaHex/AvaloniaHex.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 |
4 | AvaloniaHex
5 | A hex editor control for the Avalonia UI framework.
6 | avalonia ui hex editor binary data visualizer
7 | net6.0;net8.0
8 | enable
9 | enable
10 | true
11 | true
12 | README.md
13 | MIT
14 |
15 |
16 |
17 | bin\Debug\net6.0\AvaloniaHex.xml
18 |
19 |
20 |
21 | bin\Release\net6.0\AvaloniaHex.xml
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
--------------------------------------------------------------------------------
/examples/AvaloniaHex.Demo/InputDialog.axaml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
--------------------------------------------------------------------------------
/examples/AvaloniaHex.Demo/InputDialog.axaml.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using Avalonia;
3 | using Avalonia.Controls;
4 | using Avalonia.Interactivity;
5 | using Avalonia.Markup.Xaml;
6 |
7 | namespace AvaloniaHex.Demo;
8 |
9 | public partial class InputDialog : Window
10 | {
11 | public InputDialog()
12 | {
13 | InitializeComponent();
14 | }
15 |
16 | public string? Prompt
17 | {
18 | get => PromptLabel.Content as string;
19 | set => PromptLabel.Content = value;
20 | }
21 |
22 | public string? Input
23 | {
24 | get => InputTextBox.Text;
25 | set => InputTextBox.Text = value;
26 | }
27 |
28 | public string? Watermark
29 | {
30 | get => InputTextBox.Watermark;
31 | set => InputTextBox.Watermark = value;
32 | }
33 |
34 | public Predicate IsValid
35 | {
36 | get;
37 | set;
38 | } = static _ => true;
39 |
40 | protected override void OnLoaded(RoutedEventArgs e)
41 | {
42 | base.OnLoaded(e);
43 | InputTextBox.Focus();
44 | InputTextBox.SelectAll();
45 | }
46 |
47 | private void OKButtonOnClick(object? sender, RoutedEventArgs e) => Close(InputTextBox.Text);
48 |
49 | private void CancelButtonOnClick(object? sender, RoutedEventArgs e) => Close(null);
50 |
51 | private void InputTextBoxOnTextChanged(object? sender, TextChangedEventArgs e) => OKButton.IsEnabled = IsValid(Input);
52 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Editing/Selection.cs:
--------------------------------------------------------------------------------
1 | using AvaloniaHex.Document;
2 | using AvaloniaHex.Rendering;
3 |
4 | namespace AvaloniaHex.Editing;
5 |
6 | ///
7 | /// Represents a selection within a hex editor.
8 | ///
9 | public class Selection
10 | {
11 | ///
12 | /// Fires when the selection range has changed.
13 | ///
14 | public event EventHandler? RangeChanged;
15 |
16 | private BitRange _range;
17 |
18 | internal Selection(HexView hexView)
19 | {
20 | HexView = hexView;
21 | }
22 |
23 | ///
24 | /// Gets the hex view the selection is rendered on.
25 | ///
26 | public HexView HexView { get; }
27 |
28 | ///
29 | /// Gets or sets the range the selection spans.
30 | ///
31 | public BitRange Range
32 | {
33 | get => _range;
34 | set
35 | {
36 | value = HexView.Document is { } document
37 | ? value.Clamp(document.ValidRanges.EnclosingRange)
38 | : BitRange.Empty;
39 |
40 | if (_range != value)
41 | {
42 | _range = value;
43 | OnRangeChanged();
44 | }
45 | }
46 | }
47 |
48 | private void OnRangeChanged()
49 | {
50 | RangeChanged?.Invoke(this, EventArgs.Empty);
51 | }
52 |
53 | ///
54 | /// Selects the entire document.
55 | ///
56 | public void SelectAll()
57 | {
58 | Range = HexView is { Document.ValidRanges.EnclosingRange: var enclosingRange }
59 | ? enclosingRange
60 | : default;
61 | }
62 | }
--------------------------------------------------------------------------------
/examples/AvaloniaHex.Demo/RealTimeChangingDocument.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using Avalonia.Threading;
5 | using AvaloniaHex.Document;
6 |
7 | namespace AvaloniaHex.Demo;
8 |
9 | ///
10 | /// Provides an example implementation of a binary document that demonstrates notifying document changes to a hex view.
11 | ///
12 | public class RealTimeChangingDocument : MemoryBinaryDocument
13 | {
14 | private readonly Random _random = new();
15 |
16 | public RealTimeChangingDocument(int size, TimeSpan refreshInterval)
17 | : base(new byte[size])
18 | {
19 | var timer = new DispatcherTimer(refreshInterval, DispatcherPriority.Background, RefreshTimerOnTick);
20 | timer.Start();
21 | }
22 |
23 | ///
24 | /// Gets the collection of bit ranges that are changing continuously.
25 | ///
26 | public IList DynamicRanges { get; } = new List();
27 |
28 | private void RefreshTimerOnTick(object? sender, EventArgs e)
29 | {
30 | int maxLength = (int) DynamicRanges.Max(x => x.ByteLength);
31 | Span buffer = stackalloc byte[maxLength];
32 |
33 | for (int i = 0; i < DynamicRanges.Count; i++)
34 | {
35 | var range = DynamicRanges[i];
36 |
37 | // Generate some new random memory for this range.
38 | var span = buffer[..(int) range.ByteLength];
39 | _random.NextBytes(span);
40 | span.CopyTo(Memory.Span[(int) range.Start.ByteIndex..]);
41 |
42 | // Notify changes.
43 | OnChanged(new BinaryDocumentChange(BinaryDocumentChangeType.Modify, range));
44 | }
45 | }
46 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/TextRunExtensions.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Media;
2 | using Avalonia.Media.TextFormatting;
3 |
4 | namespace AvaloniaHex.Rendering;
5 |
6 | internal static class TextRunExtensions
7 | {
8 | public static GenericTextRunProperties WithForeground(this GenericTextRunProperties self, IBrush? foreground)
9 | {
10 | if (Equals(self.ForegroundBrush, foreground))
11 | return self;
12 |
13 | return new GenericTextRunProperties(
14 | self.Typeface,
15 | self.FontRenderingEmSize,
16 | self.TextDecorations,
17 | foreground,
18 | self.BackgroundBrush,
19 | self.BaselineAlignment,
20 | self.CultureInfo
21 | );
22 | }
23 |
24 | public static GenericTextRunProperties WithBackground(this GenericTextRunProperties self, IBrush? background)
25 | {
26 | if (Equals(self.BackgroundBrush, background))
27 | return self;
28 |
29 | return new GenericTextRunProperties(
30 | self.Typeface,
31 | self.FontRenderingEmSize,
32 | self.TextDecorations,
33 | self.ForegroundBrush,
34 | background,
35 | self.BaselineAlignment,
36 | self.CultureInfo
37 | );
38 | }
39 |
40 | public static GenericTextRunProperties WithBrushes(this GenericTextRunProperties self, IBrush? foreground, IBrush? background)
41 | {
42 | if (Equals(self.ForegroundBrush, foreground) && Equals(self.BackgroundBrush, background))
43 | return self;
44 |
45 | return new GenericTextRunProperties(
46 | self.Typeface,
47 | self.FontRenderingEmSize,
48 | self.TextDecorations,
49 | foreground,
50 | background,
51 | self.BaselineAlignment,
52 | self.CultureInfo
53 | );
54 | }
55 | }
--------------------------------------------------------------------------------
/examples/AvaloniaHex.Demo/AvaloniaHex.Demo.csproj:
--------------------------------------------------------------------------------
1 |
2 |
3 | WinExe
4 | net6.0;net8.0
5 | enable
6 |
7 | copyused
8 | true
9 | 12
10 |
11 |
12 |
13 |
14 |
15 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/VisualBytesLineSegment.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Media;
2 | using AvaloniaHex.Document;
3 |
4 | namespace AvaloniaHex.Rendering;
5 |
6 | ///
7 | /// Represents a single segment in a visual line.
8 | ///
9 | public class VisualBytesLineSegment
10 | {
11 | ///
12 | /// Creates a new segment range.
13 | ///
14 | /// The bit range the segment spans.
15 | public VisualBytesLineSegment(BitRange range)
16 | {
17 | Range = range;
18 | }
19 |
20 | ///
21 | /// Gets the bit range the segment spans in the visual.
22 | ///
23 | public BitRange Range { get; }
24 |
25 | ///
26 | /// Gets the foreground brush used for rendering the text in the segment, or null if the default foreground
27 | /// brush should be used instead.
28 | ///
29 | public IBrush? ForegroundBrush { get; set; }
30 |
31 | ///
32 | /// Gets the background brush used for rendering the text in the segment, or null if the default background
33 | /// brush should be used instead.
34 | ///
35 | public IBrush? BackgroundBrush { get; set; }
36 |
37 | ///
38 | /// Splits the segment in two parts at the provided bit location.
39 | ///
40 | /// The location to split at.
41 | /// The two resulting segments.
42 | public (VisualBytesLineSegment, VisualBytesLineSegment) Split(BitLocation location)
43 | {
44 | var (left, right) = Range.Split(location);
45 |
46 | return (
47 | new VisualBytesLineSegment(left)
48 | {
49 | ForegroundBrush = ForegroundBrush,
50 | BackgroundBrush = BackgroundBrush
51 | },
52 | new VisualBytesLineSegment(right)
53 | {
54 | ForegroundBrush = ForegroundBrush,
55 | BackgroundBrush = BackgroundBrush
56 | }
57 | );
58 | }
59 | }
60 |
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/CellGeometryBuilder.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Media;
3 | using AvaloniaHex.Document;
4 |
5 | namespace AvaloniaHex.Rendering;
6 |
7 | ///
8 | /// Provides utilities for computing the geometry of ranges within a hex view.
9 | ///
10 | public static class CellGeometryBuilder
11 | {
12 | ///
13 | /// Computes the geometry that bounds the provided cells in a range of a column.
14 | ///
15 | /// The column.
16 | /// The range of the cells to bound.
17 | /// The geometry, or null if the range is not visible.
18 | public static Geometry? CreateBoundingGeometry(CellBasedColumn column, BitRange range)
19 | {
20 | if (column.HexView is null || range.IsEmpty)
21 | return null;
22 |
23 | var startLine = column.HexView.GetVisualLineByLocation(range.Start);
24 | var endLine = column.HexView.GetVisualLineByLocation(range.End.PreviousOrZero());
25 | if (startLine is null || endLine is null)
26 | return null;
27 |
28 | var startBounds = column.GetGroupBounds(startLine, range.Start);
29 | var endBounds = column.GetGroupBounds(endLine, range.End.PreviousOrZero());
30 |
31 | var geometry = new PolylineGeometry
32 | {
33 | IsFilled = true
34 | };
35 |
36 | geometry.Points.Add(startBounds.TopLeft);
37 |
38 | if (startLine == endLine)
39 | {
40 | geometry.Points.Add(endBounds.TopRight);
41 | geometry.Points.Add(endBounds.BottomRight);
42 | }
43 | else
44 | {
45 | geometry.Points.Add(new Point(column.Bounds.Right, startBounds.Top));
46 | geometry.Points.Add(new Point(column.Bounds.Right, endBounds.Top));
47 | geometry.Points.Add(endBounds.TopRight);
48 | geometry.Points.Add(endBounds.BottomRight);
49 | geometry.Points.Add(new Point(column.Bounds.Left, endBounds.Bottom));
50 | geometry.Points.Add(new Point(column.Bounds.Left, startBounds.Bottom));
51 | }
52 |
53 | geometry.Points.Add(startBounds.BottomLeft);
54 |
55 | return geometry;
56 | }
57 | }
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # AvaloniaHex
2 |
3 | This is a(n experimental) hex editor control for the [Avalonia](https://github.com/AvaloniaUI/Avalonia) UI framework.
4 |
5 | 
6 |
7 |
8 | ## Features
9 |
10 | - [x] Display binary data in hex, binary, and ASCII. Extensible with custom column rendering.
11 | - [x] Adjust the displayed bytes per line manually or automatically.
12 | - [x] Modify binary documents in-place or resize documents dynamically.
13 | - [x] Support for documents with non-contiguous backing buffers and/or non-zero base addresses. Useful for documents with "gaps" (e.g., memory views).
14 | - [x] Many style customization options available with default Light and Dark themes.
15 | - [x] Custom syntax (byte) highlighting.
16 | - [x] Support for memory mapped files.
17 |
18 | ## Binaries
19 |
20 | - Stable: [NuGet](https://www.nuget.org/packages/AvaloniaHex)
21 | - Nightly: [AppVeyor](https://ci.appveyor.com/project/Washi1337/avaloniahex/build/artifacts)
22 |
23 |
24 | ## Quick Start Guide
25 |
26 | After installing the `AvaloniaHex` dependency, add the default control styles to your `App.axaml`:
27 |
28 | ```xml
29 |
30 |
31 | ...
32 |
33 |
34 |
35 | ```
36 |
37 | Then, add the `HexEditor` control to your window:
38 |
39 | ```xml
40 |
44 |
45 |
46 |
47 |
48 |
49 |
50 |
51 |
52 |
53 |
54 |
55 | ```
56 |
57 | To display a file in the control, assign the `Document` property:
58 |
59 | ```csharp
60 | HexEditor editor = ...;
61 |
62 | editor.Document = new MemoryBinaryDocument(File.ReadAllBytes(@"C:\Path\To\File.bin"));
63 | ```
64 |
65 | See [examples](examples) for more details.
66 |
67 |
68 | ## License
69 |
70 | MIT
--------------------------------------------------------------------------------
/src/AvaloniaHex/Document/ReadOnlyBitRangeUnion.cs:
--------------------------------------------------------------------------------
1 | using System.Collections;
2 | using System.Collections.Specialized;
3 |
4 | namespace AvaloniaHex.Document;
5 |
6 | ///
7 | /// Represents a read-only disjoint union of binary ranges in a document.
8 | ///
9 | public class ReadOnlyBitRangeUnion : IReadOnlyBitRangeUnion
10 | {
11 | ///
12 | public event NotifyCollectionChangedEventHandler? CollectionChanged;
13 |
14 | ///
15 | /// The empty union.
16 | ///
17 | public static readonly ReadOnlyBitRangeUnion Empty = new(new BitRangeUnion());
18 |
19 | private readonly BitRangeUnion _union;
20 |
21 | ///
22 | /// Wraps an existing disjoint binary range union into a .
23 | ///
24 | /// The union to wrap.
25 | public ReadOnlyBitRangeUnion(BitRangeUnion union)
26 | {
27 | _union = union;
28 | _union.CollectionChanged += UnionOnCollectionChanged;
29 | }
30 |
31 | private void UnionOnCollectionChanged(object? sender, NotifyCollectionChangedEventArgs e)
32 | {
33 | CollectionChanged?.Invoke(this, e);
34 | }
35 |
36 | ///
37 | public int Count => _union.Count;
38 |
39 | ///
40 | public BitRange EnclosingRange => _union.EnclosingRange;
41 |
42 | ///
43 | public bool IsFragmented => _union.IsFragmented;
44 |
45 | ///
46 | public bool Contains(BitLocation location) => _union.Contains(location);
47 |
48 | ///
49 | public bool IsSuperSetOf(BitRange range) => _union.IsSuperSetOf(range);
50 |
51 | ///
52 | public bool IntersectsWith(BitRange range) => _union.IntersectsWith(range);
53 |
54 | ///
55 | public int GetOverlappingRanges(BitRange range, Span output) => _union.GetOverlappingRanges(range, output);
56 |
57 | ///
58 | public int GetIntersectingRanges(BitRange range, Span output) => _union.GetIntersectingRanges(range, output);
59 |
60 | ///
61 | public BitRangeUnion.Enumerator GetEnumerator() => _union.GetEnumerator();
62 |
63 | IEnumerator IEnumerable.GetEnumerator() => _union.GetEnumerator();
64 |
65 | IEnumerator IEnumerable.GetEnumerator() => ((IEnumerable) _union).GetEnumerator();
66 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Themes/Base.axaml:
--------------------------------------------------------------------------------
1 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
19 |
20 |
23 |
24 |
27 |
28 |
31 |
32 |
35 |
36 |
40 |
41 |
47 |
48 |
54 |
--------------------------------------------------------------------------------
/src/AvaloniaHex/Document/IReadOnlyBitRangeUnion.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.Specialized;
2 |
3 | namespace AvaloniaHex.Document;
4 |
5 | ///
6 | /// Provides read-only access to a disjoint union of binary ranges in a document.
7 | ///
8 | public interface IReadOnlyBitRangeUnion : IReadOnlyCollection, INotifyCollectionChanged
9 | {
10 | ///
11 | /// Gets the minimum range that encloses all sub ranges included in the union.
12 | ///
13 | BitRange EnclosingRange { get; }
14 |
15 | ///
16 | /// Gets a value indicating whether the union consists of multiple disjoint ranges.
17 | ///
18 | bool IsFragmented { get; }
19 |
20 | ///
21 | /// Determines whether the provided location is within the included ranges.
22 | ///
23 | /// The location.
24 | /// true if the location is included, false otherwise.
25 | bool Contains(BitLocation location);
26 |
27 | ///
28 | /// Determines whether the provided range is within the included ranges.
29 | ///
30 | /// The range to test.
31 | /// true if the union is a super-set of the provided range, false otherwise.
32 | bool IsSuperSetOf(BitRange range);
33 |
34 | ///
35 | /// Determines whether the provided range intersects with any of the ranges in the union.
36 | ///
37 | /// The range to test.
38 | /// true if the union intersects with the provided range, false otherwise.
39 | bool IntersectsWith(BitRange range);
40 |
41 | ///
42 | /// Collects the disjoint ranges in the union that overlap with the provided range.
43 | ///
44 | /// The range to overlap with.
45 | /// The output buffer to store the overlapping disjoint ranges in.
46 | /// The number of found disjoint ranges.
47 | int GetOverlappingRanges(BitRange range, Span output);
48 |
49 | ///
50 | /// Collects the intersection of all disjoint ranges that overlap with the provided range.
51 | ///
52 | /// The range to intersect with.
53 | /// The output buffer to store the intersecting disjoint ranges in.
54 | /// The number of found disjoint ranges.
55 | int GetIntersectingRanges(BitRange range, Span output);
56 |
57 | ///
58 | /// Gets an enumerator that enumerates all the ranges in the union.
59 | ///
60 | new BitRangeUnion.Enumerator GetEnumerator();
61 | }
--------------------------------------------------------------------------------
/AvaloniaHex.sln:
--------------------------------------------------------------------------------
1 |
2 | Microsoft Visual Studio Solution File, Format Version 12.00
3 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AvaloniaHex", "src\AvaloniaHex\AvaloniaHex.csproj", "{63822CC7-0AD9-49F8-9B39-51179C82B10D}"
4 | EndProject
5 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AvaloniaHex.Demo", "examples\AvaloniaHex.Demo\AvaloniaHex.Demo.csproj", "{786B774D-9400-4E36-BA71-4699727AD6BA}"
6 | EndProject
7 | Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "AvaloniaHex.Tests", "test\AvaloniaHex.Tests\AvaloniaHex.Tests.csproj", "{5019E7CF-B71D-45B6-8B2D-DED8FF6AE50D}"
8 | EndProject
9 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "src", "src", "{6DE9F884-B751-4065-9667-30AAC94DD926}"
10 | EndProject
11 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "examples", "examples", "{3B83E6BF-6EF1-4DBC-B73E-761BB0615360}"
12 | EndProject
13 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "test", "test", "{4A2CD31E-0968-4F4B-97E9-24B370A236C8}"
14 | EndProject
15 | Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Solution Items", "Solution Items", "{0A6B004C-0CFA-4CE1-BAA2-3368BE1643A2}"
16 | ProjectSection(SolutionItems) = preProject
17 | Directory.Build.props = Directory.Build.props
18 | LICENSE.md = LICENSE.md
19 | README.md = README.md
20 | appveyor.yml = appveyor.yml
21 | EndProjectSection
22 | EndProject
23 | Global
24 | GlobalSection(SolutionConfigurationPlatforms) = preSolution
25 | Debug|Any CPU = Debug|Any CPU
26 | Release|Any CPU = Release|Any CPU
27 | EndGlobalSection
28 | GlobalSection(ProjectConfigurationPlatforms) = postSolution
29 | {63822CC7-0AD9-49F8-9B39-51179C82B10D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
30 | {63822CC7-0AD9-49F8-9B39-51179C82B10D}.Debug|Any CPU.Build.0 = Debug|Any CPU
31 | {63822CC7-0AD9-49F8-9B39-51179C82B10D}.Release|Any CPU.ActiveCfg = Release|Any CPU
32 | {63822CC7-0AD9-49F8-9B39-51179C82B10D}.Release|Any CPU.Build.0 = Release|Any CPU
33 | {786B774D-9400-4E36-BA71-4699727AD6BA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
34 | {786B774D-9400-4E36-BA71-4699727AD6BA}.Debug|Any CPU.Build.0 = Debug|Any CPU
35 | {786B774D-9400-4E36-BA71-4699727AD6BA}.Release|Any CPU.ActiveCfg = Release|Any CPU
36 | {786B774D-9400-4E36-BA71-4699727AD6BA}.Release|Any CPU.Build.0 = Release|Any CPU
37 | {5019E7CF-B71D-45B6-8B2D-DED8FF6AE50D}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
38 | {5019E7CF-B71D-45B6-8B2D-DED8FF6AE50D}.Debug|Any CPU.Build.0 = Debug|Any CPU
39 | {5019E7CF-B71D-45B6-8B2D-DED8FF6AE50D}.Release|Any CPU.ActiveCfg = Release|Any CPU
40 | {5019E7CF-B71D-45B6-8B2D-DED8FF6AE50D}.Release|Any CPU.Build.0 = Release|Any CPU
41 | EndGlobalSection
42 | GlobalSection(NestedProjects) = preSolution
43 | {63822CC7-0AD9-49F8-9B39-51179C82B10D} = {6DE9F884-B751-4065-9667-30AAC94DD926}
44 | {786B774D-9400-4E36-BA71-4699727AD6BA} = {3B83E6BF-6EF1-4DBC-B73E-761BB0615360}
45 | {5019E7CF-B71D-45B6-8B2D-DED8FF6AE50D} = {4A2CD31E-0968-4F4B-97E9-24B370A236C8}
46 | EndGlobalSection
47 | EndGlobal
48 |
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/OffsetColumn.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Media.TextFormatting;
3 |
4 | namespace AvaloniaHex.Rendering;
5 |
6 | ///
7 | /// Represents the column rendering the line offsets.
8 | ///
9 | public class OffsetColumn : Column
10 | {
11 | private Size _minimumSize;
12 |
13 | static OffsetColumn()
14 | {
15 | IsUppercaseProperty.Changed.AddClassHandler(OnIsUpperCaseChanged);
16 | IsHeaderVisibleProperty.OverrideDefaultValue(false);
17 | HeaderProperty.OverrideDefaultValue("Offset");
18 | }
19 |
20 | ///
21 | public override Size MinimumSize => _minimumSize;
22 |
23 | ///
24 | /// Defines the property.
25 | ///
26 | public static readonly StyledProperty IsUppercaseProperty =
27 | AvaloniaProperty.Register(nameof(IsUppercase), true);
28 |
29 | ///
30 | /// Gets or sets a value indicating whether the hexadecimal digits should be rendered in uppercase or not.
31 | ///
32 | public bool IsUppercase
33 | {
34 | get => GetValue(IsUppercaseProperty);
35 | set => SetValue(IsUppercaseProperty, value);
36 | }
37 |
38 | private static void OnIsUpperCaseChanged(OffsetColumn arg1, AvaloniaPropertyChangedEventArgs arg2)
39 | {
40 | arg1.HexView?.InvalidateVisualLines();
41 | }
42 |
43 | ///
44 | public override void Measure()
45 | {
46 | if (HexView is null)
47 | {
48 | _minimumSize = default;
49 | }
50 | else
51 | {
52 | var dummy = CreateTextLine("00000000:")!;
53 | _minimumSize = new Size(dummy.Width, dummy.Height);
54 | }
55 | }
56 |
57 | ///
58 | public override TextLine? CreateTextLine(VisualBytesLine line)
59 | {
60 | if (HexView is null)
61 | throw new InvalidOperationException();
62 |
63 | return CreateTextLine(FormatOffset(line.Range.Start.ByteIndex));
64 | }
65 |
66 | ///
67 | /// Formats the provided offset to a string to be displayed in the column.
68 | ///
69 | /// The offset to format.
70 | /// The formatted offset.
71 | protected virtual string FormatOffset(ulong offset) => IsUppercase
72 | ? $"{offset:X8}:"
73 | : $"{offset:x8}:";
74 |
75 | private TextLine? CreateTextLine(string text)
76 | {
77 | if (HexView is null)
78 | return null;
79 |
80 | var properties = GetTextRunProperties();
81 | return TextFormatter.Current.FormatLine(
82 | new SimpleTextSource(text, properties),
83 | 0,
84 | double.MaxValue,
85 | new GenericTextParagraphProperties(properties)
86 | )!;
87 | }
88 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Document/IBinaryDocument.cs:
--------------------------------------------------------------------------------
1 | namespace AvaloniaHex.Document;
2 |
3 | ///
4 | /// Represents a binary document that can be displayed in a hex editor.
5 | ///
6 | public interface IBinaryDocument : IDisposable
7 | {
8 | ///
9 | /// Fires when the contents of the document has changed.
10 | ///
11 | event EventHandler Changed;
12 |
13 | ///
14 | /// Gets the total length of the document.
15 | ///
16 | ulong Length { get; }
17 |
18 | ///
19 | /// Gets a value indicating whether the document can be changed or not.
20 | ///
21 | bool IsReadOnly { get; }
22 |
23 | ///
24 | /// Gets a value indicating whether additional bytes can be inserted into the document.
25 | ///
26 | bool CanInsert { get; }
27 |
28 | ///
29 | /// Gets a value indicating whether bytes can be removed from the document.
30 | ///
31 | bool CanRemove { get; }
32 |
33 | ///
34 | /// Gets a collection of binary ranges in the document that are accessible/valid.
35 | ///
36 | IReadOnlyBitRangeUnion ValidRanges { get; }
37 |
38 | ///
39 | /// Reads bytes from the document at the provided offset.
40 | ///
41 | /// The offset to start reading at.
42 | /// The buffer to write the read data to.
43 | void ReadBytes(ulong offset, Span buffer);
44 |
45 | ///
46 | /// Writes bytes to the document at the provided offset.
47 | ///
48 | /// The offset to start writing at.
49 | /// The data to write.
50 | /// Occurs when the document is read-only.
51 | void WriteBytes(ulong offset, ReadOnlySpan buffer);
52 |
53 | ///
54 | /// Inserts bytes to the document at the provided offset, extending the document.
55 | ///
56 | /// The offset to start inserting at.
57 | /// The data to insert.
58 | ///
59 | /// Occurs when the document is read-only or cannot insert bytes.
60 | ///
61 | void InsertBytes(ulong offset, ReadOnlySpan buffer);
62 |
63 | ///
64 | /// Writes bytes to the document at the provided offset.
65 | ///
66 | /// The offset to start writing at.
67 | /// The number of bytes to remove.
68 | ///
69 | /// Occurs when the document is read-only or cannot remove bytes.
70 | ///
71 | void RemoveBytes(ulong offset, ulong length);
72 |
73 | ///
74 | /// Flushes all buffered changes to the underlying persistent backing storage of the document.
75 | ///
76 | void Flush();
77 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/HeaderLayer.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Media;
3 |
4 | namespace AvaloniaHex.Rendering;
5 |
6 | ///
7 | /// Represents the layer that renders the header in a hex view.
8 | ///
9 | public class HeaderLayer : Layer
10 | {
11 | ///
12 | /// Dependency property for
13 | ///
14 | public static readonly StyledProperty HeaderBackgroundProperty =
15 | AvaloniaProperty.Register(nameof(HeaderBackground));
16 |
17 | ///
18 | /// Gets or sets the base background brush that is used for rendering the header, or null if no background
19 | /// should be drawn.
20 | ///
21 | public IBrush? HeaderBackground
22 | {
23 | get => GetValue(HeaderBackgroundProperty);
24 | set => SetValue(HeaderBackgroundProperty, value);
25 | }
26 |
27 | ///
28 | /// Dependency property for
29 | ///
30 | public static readonly StyledProperty HeaderBorderProperty =
31 | AvaloniaProperty.Register(nameof(HeaderBorder));
32 |
33 | ///
34 | /// Gets or sets the base border pen that is used for rendering the border of the header, or null if no
35 | /// border should be drawn.
36 | ///
37 | public IPen? HeaderBorder
38 | {
39 | get => GetValue(HeaderBorderProperty);
40 | set => SetValue(HeaderBorderProperty, value);
41 | }
42 |
43 | ///
44 | public override void Render(DrawingContext context)
45 | {
46 | base.Render(context);
47 |
48 | if (HexView is not {IsHeaderVisible: true})
49 | return;
50 |
51 | // Do we even have a header?
52 | double headerSize = HexView.EffectiveHeaderSize;
53 | if (headerSize <= 0)
54 | return;
55 |
56 | // Render base background + border when necessary.
57 | if (HeaderBackground is not null || HeaderBorder is not null)
58 | context.DrawRectangle(HeaderBackground, HeaderBorder, new Rect(0, 0, Bounds.Width, headerSize));
59 |
60 | var padding = HexView.HeaderPadding;
61 | for (int i = 0; i < HexView.Columns.Count; i++)
62 | {
63 | var column = HexView.Columns[i];
64 |
65 | // Only draw headers that are visible.
66 | if (column is not {IsVisible: true, IsHeaderVisible: true})
67 | continue;
68 |
69 | // Draw background + border when necessary.
70 | if (column.HeaderBackground is not null || column.HeaderBorder is not null)
71 | {
72 | context.DrawRectangle(
73 | column.HeaderBackground,
74 | column.HeaderBorder,
75 | new Rect(column.Bounds.Left, 0, column.Bounds.Width, headerSize)
76 | );
77 | }
78 |
79 | // Draw header text.
80 | HexView.Headers[i]?.Draw(context, new Point(column.Bounds.Left, padding.Top));
81 | }
82 | }
83 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/ByteHighlighter.cs:
--------------------------------------------------------------------------------
1 | using Avalonia.Media;
2 | using AvaloniaHex.Document;
3 |
4 | namespace AvaloniaHex.Rendering;
5 |
6 | ///
7 | /// Provides a base for a byte-level highlighter in a hex view.
8 | ///
9 | public abstract class ByteHighlighter : ILineTransformer
10 | {
11 | ///
12 | /// Gets or sets the brush used for rendering the foreground of the highlighted bytes.
13 | ///
14 | public IBrush? Foreground { get; set; }
15 |
16 | ///
17 | /// Gets or sets the brush used for rendering the background of the highlighted bytes.
18 | ///
19 | public IBrush? Background { get; set; }
20 |
21 | ///
22 | /// Determines whether the provided location is highlighted or not.
23 | ///
24 | ///
25 | ///
26 | ///
27 | ///
28 | protected abstract bool IsHighlighted(HexView hexView, VisualBytesLine line, BitLocation location);
29 |
30 | ///
31 | public void Transform(HexView hexView, VisualBytesLine line)
32 | {
33 | for (int i = 0; i < line.Segments.Count; i++)
34 | ColorizeSegment(hexView, line, ref i);
35 | }
36 |
37 | private void ColorizeSegment(HexView hexView, VisualBytesLine line, ref int index)
38 | {
39 | var originalSegment = line.Segments[index];
40 |
41 | var currentSegment = originalSegment;
42 |
43 | bool isInModifiedRange = false;
44 | for (ulong j = 0; j < originalSegment.Range.ByteLength; j++)
45 | {
46 | var currentLocation = new BitLocation(originalSegment.Range.Start.ByteIndex + j);
47 |
48 | bool shouldSplit = IsHighlighted(hexView, line, currentLocation) ? !isInModifiedRange : isInModifiedRange;
49 | if (!shouldSplit)
50 | continue;
51 |
52 | isInModifiedRange = !isInModifiedRange;
53 |
54 | // Split the segment.
55 | var (left, right) = currentSegment.Split(currentLocation);
56 |
57 | if (isInModifiedRange)
58 | {
59 | // We entered a highlighted segment.
60 | right.ForegroundBrush = Foreground;
61 | right.BackgroundBrush = Background;
62 | }
63 | else
64 | {
65 | // We left a highlighted segment.
66 | right.ForegroundBrush = originalSegment.ForegroundBrush;
67 | right.BackgroundBrush = originalSegment.BackgroundBrush;
68 | }
69 |
70 | // Insert the ranges.
71 | if (left.Range.IsEmpty)
72 | {
73 | // Optimization. Just replace the left segment if it is empty.
74 | line.Segments[index] = right;
75 | }
76 | else
77 | {
78 | line.Segments[index] = left;
79 | line.Segments.Insert(index + 1, right);
80 | index++;
81 | }
82 |
83 | currentSegment = right;
84 | }
85 | }
86 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Themes/Simple/AvaloniaHex.axaml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/AvaloniaHex/Themes/Fluent/AvaloniaHex.axaml:
--------------------------------------------------------------------------------
1 |
3 |
4 |
5 |
6 |
7 |
8 |
9 |
10 |
11 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
27 |
28 |
29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 |
38 |
39 |
40 |
41 |
42 |
43 |
44 |
45 |
46 |
47 |
--------------------------------------------------------------------------------
/src/AvaloniaHex/Document/ByteArrayBinaryDocument.cs:
--------------------------------------------------------------------------------
1 | namespace AvaloniaHex.Document;
2 |
3 | ///
4 | /// Wraps a byte array into a binary document.
5 | ///
6 | [Obsolete("Use MemoryBinaryDocument instead.")]
7 | public class ByteArrayBinaryDocument : IBinaryDocument
8 | {
9 | ///
10 | public event EventHandler? Changed;
11 |
12 | private readonly byte[] _data;
13 |
14 | ///
15 | /// Creates a new byte array document.
16 | ///
17 | /// The backing buffer.
18 | public ByteArrayBinaryDocument(byte[] data)
19 | : this(data, false)
20 | {
21 | }
22 |
23 | ///
24 | /// Creates a new byte array document.
25 | ///
26 | /// The backing buffer.
27 | /// true if the document can be edited, false otherwise.
28 | public ByteArrayBinaryDocument(byte[] data, bool isReadOnly)
29 | {
30 | IsReadOnly = isReadOnly;
31 | _data = data;
32 | ValidRanges = new BitRangeUnion([new BitRange(0, Length)]).AsReadOnly();
33 | }
34 |
35 | ///
36 | /// Gets the data stored in the document.
37 | ///
38 | public byte[] Data => _data;
39 |
40 | ///
41 | public ulong Length => (ulong) _data.Length;
42 |
43 | ///
44 | public bool IsReadOnly { get; }
45 |
46 | ///
47 | public bool CanInsert => false;
48 |
49 | ///
50 | public bool CanRemove => false;
51 |
52 | ///
53 | public IReadOnlyBitRangeUnion ValidRanges { get; }
54 |
55 | ///
56 | public void ReadBytes(ulong offset, Span buffer)
57 | {
58 | _data.AsSpan((int) offset, buffer.Length).CopyTo(buffer);
59 | }
60 |
61 | ///
62 | public void WriteBytes(ulong offset, ReadOnlySpan buffer)
63 | {
64 | if (IsReadOnly)
65 | throw new InvalidOperationException("Document is read-only.");
66 |
67 | buffer.CopyTo(_data.AsSpan((int) offset, buffer.Length));
68 | OnChanged(new BinaryDocumentChange(BinaryDocumentChangeType.Modify, new BitRange(offset, offset + (ulong) buffer.Length)));
69 | }
70 |
71 | ///
72 | public void InsertBytes(ulong offset, ReadOnlySpan buffer)
73 | {
74 | if (IsReadOnly)
75 | throw new InvalidOperationException("Document is read-only.");
76 |
77 | throw new InvalidOperationException("Document cannot be resized.");
78 | }
79 |
80 | ///
81 | public void RemoveBytes(ulong offset, ulong length)
82 | {
83 | if (IsReadOnly)
84 | throw new InvalidOperationException("Document is read-only.");
85 |
86 | throw new InvalidOperationException("Document cannot be resized.");
87 | }
88 |
89 | void IBinaryDocument.Flush()
90 | {
91 | }
92 |
93 | ///
94 | /// Fires the event.
95 | ///
96 | /// The event arguments describing the change.
97 | protected virtual void OnChanged(BinaryDocumentChange e) => Changed?.Invoke(this, e);
98 |
99 | void IDisposable.Dispose()
100 | {
101 | }
102 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Editing/CurrentLineLayer.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Media;
3 | using AvaloniaHex.Rendering;
4 |
5 | namespace AvaloniaHex.Editing;
6 |
7 | ///
8 | /// Renders a highlight on the current active visual line.
9 | ///
10 | public class CurrentLineLayer : Layer
11 | {
12 | static CurrentLineLayer()
13 | {
14 | AffectsRender(
15 | CurrentLineBackgroundProperty,
16 | CurrentLineBorderProperty
17 | );
18 | }
19 |
20 | ///
21 | /// Creates a new current line highlighting layer.
22 | ///
23 | /// The cursor to follow.
24 | /// The selection to follow.
25 | public CurrentLineLayer(Caret caret, Selection selection)
26 | {
27 | Caret = caret;
28 | Selection = selection;
29 |
30 | Caret.LocationChanged += OnCursorChanged;
31 | Selection.RangeChanged += OnCursorChanged;
32 | }
33 |
34 | ///
35 | public override LayerRenderMoments UpdateMoments => LayerRenderMoments.NoResizeRearrange;
36 |
37 | ///
38 | /// Gets the cursor the highlighter is following.
39 | ///
40 | public Caret Caret { get; }
41 |
42 | ///
43 | /// Gets the selection the highlighter is following.
44 | ///
45 | public Selection Selection { get; }
46 |
47 | ///
48 | /// Defines the property.
49 | ///
50 | public static readonly StyledProperty CurrentLineBorderProperty =
51 | AvaloniaProperty.Register(
52 | nameof(CurrentLineBorder),
53 | new Pen(new SolidColorBrush(Colors.DimGray), 1.5)
54 | );
55 |
56 | ///
57 | /// Gets or sets the brush used to draw the background of the cursor in the secondary columns.
58 | ///
59 | public IPen? CurrentLineBorder
60 | {
61 | get => GetValue(CurrentLineBorderProperty);
62 | set => SetValue(CurrentLineBorderProperty, value);
63 | }
64 |
65 | ///
66 | /// Defines the property.
67 | ///
68 | public static readonly StyledProperty CurrentLineBackgroundProperty =
69 | AvaloniaProperty.Register(
70 | nameof(CurrentLineBackground),
71 | new SolidColorBrush(Colors.DimGray, 0.1)
72 | );
73 |
74 | ///
75 | /// Gets or sets the brush used to draw the background of the cursor in the secondary columns.
76 | ///
77 | public IBrush? CurrentLineBackground
78 | {
79 | get => GetValue(CurrentLineBackgroundProperty);
80 | set => SetValue(CurrentLineBackgroundProperty, value);
81 | }
82 |
83 | private void OnCursorChanged(object? sender, EventArgs e)
84 | {
85 | InvalidateVisual();
86 | }
87 |
88 | ///
89 | public override void Render(DrawingContext context)
90 | {
91 | base.Render(context);
92 |
93 | if (HexView is null || !HexView.IsFocused)
94 | return;
95 |
96 | var line = HexView.GetVisualLineByLocation(Caret.Location);
97 | if (line is null)
98 | return;
99 |
100 | if (Selection.Range.ByteLength <= 1)
101 | context.DrawRectangle(CurrentLineBackground, CurrentLineBorder, line.Bounds);
102 | }
103 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Document/MemoryBinaryDocument.cs:
--------------------------------------------------------------------------------
1 | namespace AvaloniaHex.Document;
2 |
3 | ///
4 | /// Represents a binary document that is backed by an instance of a fixed buffer.
5 | ///
6 | public class MemoryBinaryDocument : IBinaryDocument
7 | {
8 | ///
9 | public event EventHandler? Changed;
10 |
11 | private readonly Memory _memory;
12 |
13 | ///
14 | /// Creates a new memory binary document using the provided memory backing storage.
15 | ///
16 | /// The memory backing buffer.
17 | public MemoryBinaryDocument(Memory memory)
18 | : this(memory, false)
19 | {
20 | }
21 |
22 | ///
23 | /// Creates a new memory binary document using the provided memory backing storage.
24 | ///
25 | /// The memory backing buffer.
26 | /// true if the document can be edited, false otherwise.
27 | public MemoryBinaryDocument(Memory memory, bool isReadOnly)
28 | {
29 | _memory = memory;
30 | IsReadOnly = isReadOnly;
31 | ValidRanges = new BitRangeUnion([new BitRange(0, Length)]).AsReadOnly();
32 | }
33 |
34 | ///
35 | /// Gets the underlying memory backing buffer.
36 | ///
37 | public Memory Memory => _memory;
38 |
39 | ///
40 | public ulong Length => (ulong) _memory.Length;
41 |
42 | ///
43 | public bool IsReadOnly { get; }
44 |
45 | ///
46 | public bool CanInsert => false;
47 |
48 | ///
49 | public bool CanRemove => false;
50 |
51 | ///
52 | public IReadOnlyBitRangeUnion ValidRanges { get; }
53 |
54 | ///
55 | public void ReadBytes(ulong offset, Span buffer)
56 | {
57 | _memory.Span[(int) offset..((int)offset + buffer.Length)].CopyTo(buffer);
58 | }
59 |
60 | ///
61 | public void WriteBytes(ulong offset, ReadOnlySpan buffer)
62 | {
63 | if (IsReadOnly)
64 | throw new InvalidOperationException("Document is read-only.");
65 |
66 | buffer.CopyTo(_memory.Span[(int) offset..((int)offset + buffer.Length)]);
67 | OnChanged(new BinaryDocumentChange(BinaryDocumentChangeType.Modify, new BitRange(offset, offset + (ulong) buffer.Length)));
68 | }
69 |
70 | ///
71 | public void InsertBytes(ulong offset, ReadOnlySpan buffer)
72 | {
73 | if (IsReadOnly)
74 | throw new InvalidOperationException("Document is read-only.");
75 |
76 | throw new InvalidOperationException("Document cannot be resized.");
77 | }
78 |
79 | ///
80 | public void RemoveBytes(ulong offset, ulong length)
81 | {
82 | if (IsReadOnly)
83 | throw new InvalidOperationException("Document is read-only.");
84 |
85 | throw new InvalidOperationException("Document cannot be resized.");
86 | }
87 |
88 | void IBinaryDocument.Flush()
89 | {
90 | }
91 |
92 | ///
93 | /// Fires the event.
94 | ///
95 | /// The event arguments describing the change.
96 | protected virtual void OnChanged(BinaryDocumentChange e) => Changed?.Invoke(this, e);
97 |
98 | void IDisposable.Dispose()
99 | {
100 | }
101 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/CellGroupsLayer.cs:
--------------------------------------------------------------------------------
1 | using System.Collections.ObjectModel;
2 | using Avalonia;
3 | using Avalonia.Media;
4 | using AvaloniaHex.Document;
5 |
6 | namespace AvaloniaHex.Rendering;
7 |
8 | ///
9 | /// Provides a render layer for a hex view that visually separates groups of cells.
10 | ///
11 | public class CellGroupsLayer : Layer
12 | {
13 | static CellGroupsLayer()
14 | {
15 | AffectsRender(
16 | BytesPerGroupProperty,
17 | BorderProperty,
18 | BackgroundsProperty
19 | );
20 | }
21 |
22 | ///
23 | /// Defines the property.
24 | ///
25 | public static readonly StyledProperty BytesPerGroupProperty =
26 | AvaloniaProperty.Register(nameof(BytesPerGroup), 8);
27 |
28 | ///
29 | /// Gets or sets a value indicating the number of cells each group consists of.
30 | ///
31 | public int BytesPerGroup
32 | {
33 | get => GetValue(BytesPerGroupProperty);
34 | set => SetValue(BytesPerGroupProperty, value);
35 | }
36 |
37 | ///
38 | /// Defines the property.
39 | ///
40 | public static readonly StyledProperty BorderProperty =
41 | AvaloniaProperty.Register(
42 | nameof(Border));
43 |
44 | ///
45 | /// Gets or sets the pen used for rendering the separation lines between each group.
46 | ///
47 | public IPen? Border
48 | {
49 | get => GetValue(BorderProperty);
50 | set => SetValue(BorderProperty, value);
51 | }
52 |
53 | ///
54 | /// Defines the property.
55 | ///
56 | public static readonly DirectProperty> BackgroundsProperty =
57 | AvaloniaProperty.RegisterDirect>(
58 | nameof(Backgrounds),
59 | x => x.Backgrounds
60 | );
61 |
62 | ///
63 | /// Gets a collection of background brushes that each vertical cell group is rendered with.
64 | ///
65 | public ObservableCollection Backgrounds { get; } = new();
66 |
67 | ///
68 | public override void Render(DrawingContext context)
69 | {
70 | base.Render(context);
71 |
72 | if (HexView is null || Border is null || HexView.VisualLines.Count == 0)
73 | return;
74 |
75 | foreach (var c in HexView.Columns)
76 | {
77 | if (c is not CellBasedColumn { IsVisible: true } column)
78 | continue;
79 |
80 | DivideColumn(context, column);
81 | }
82 | }
83 |
84 | private void DivideColumn(DrawingContext context, CellBasedColumn column)
85 | {
86 | int groupIndex = 0;
87 |
88 | double left = column.Bounds.Left;
89 |
90 | var line = HexView!.VisualLines[0];
91 | for (uint offset = 0; offset < HexView.ActualBytesPerLine; offset += (uint)BytesPerGroup, groupIndex++)
92 | {
93 | var right1 = new BitLocation(line.Range.Start.ByteIndex + (uint)BytesPerGroup + offset - 1, 0);
94 | var right2 = new BitLocation(line.Range.Start.ByteIndex + (uint)BytesPerGroup + offset, 7);
95 | var rightCell1 = column.GetCellBounds(line, right1);
96 | var rightCell2 = column.GetCellBounds(line, right2);
97 |
98 | double right = Math.Min(column.Bounds.Right, 0.5 * (rightCell1.Right + rightCell2.Left));
99 |
100 | if (Backgrounds.Count > 0)
101 | {
102 | var background = Backgrounds[groupIndex % Backgrounds.Count];
103 | if (background is not null)
104 | context.FillRectangle(background, new Rect(left, 0, right - left, column.Bounds.Height));
105 | }
106 |
107 | if (groupIndex > 0)
108 | {
109 | context.DrawLine(
110 | Border!,
111 | new Point(left, 0),
112 | new Point(left, HexView.Bounds.Height)
113 | );
114 | }
115 |
116 | left = right;
117 | }
118 | }
119 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/VisualBytesLinesBuffer.cs:
--------------------------------------------------------------------------------
1 | using System.Collections;
2 | using AvaloniaHex.Document;
3 |
4 | namespace AvaloniaHex.Rendering;
5 |
6 | internal sealed class VisualBytesLinesBuffer : IReadOnlyList
7 | {
8 | private readonly HexView _owner;
9 | private readonly Stack _pool = new();
10 | private readonly List _activeLines = new();
11 |
12 | public VisualBytesLinesBuffer(HexView owner)
13 | {
14 | _owner = owner;
15 | }
16 |
17 | public int Count => _activeLines.Count;
18 |
19 | public VisualBytesLine this[int index] => _activeLines[index];
20 |
21 | public VisualBytesLine? GetVisualLineByLocation(BitLocation location)
22 | {
23 | for (int i = 0; i < _activeLines.Count; i++)
24 | {
25 | var line = _activeLines[i];
26 | if (line.VirtualRange.Contains(location))
27 | return line;
28 |
29 | if (line.Range.Start > location)
30 | return null;
31 | }
32 |
33 | return null;
34 | }
35 |
36 | public IEnumerable GetVisualLinesByRange(BitRange range)
37 | {
38 | for (int i = 0; i < _activeLines.Count; i++)
39 | {
40 | var line = _activeLines[i];
41 | if (line.VirtualRange.OverlapsWith(range))
42 | yield return line;
43 |
44 | if (line.Range.Start >= range.End)
45 | yield break;
46 | }
47 | }
48 |
49 | public VisualBytesLine GetOrCreateVisualLine(BitRange virtualRange)
50 | {
51 | VisualBytesLine? newLine = null;
52 |
53 | // Find existing line or create a new one, while keeping the list of visual lines ordered by range.
54 | for (int i = 0; i < _activeLines.Count; i++)
55 | {
56 | // Exact match on start?
57 | var currentLine = _activeLines[i];
58 | if (currentLine.VirtualRange.Start == virtualRange.Start)
59 | {
60 | // Edge-case: if our range is not exactly the same, the line's range is outdated (e.g., as a result of
61 | // inserting or removing a character at the end of the document).
62 | if (currentLine.SetRange(virtualRange))
63 | currentLine.Invalidate();
64 |
65 | return currentLine;
66 | }
67 |
68 | // If the next line is further than the requested start, the line does not exist.
69 | if (currentLine.Range.Start > virtualRange.Start)
70 | {
71 | newLine = Rent(virtualRange);
72 | _activeLines.Insert(i, newLine);
73 | break;
74 | }
75 | }
76 |
77 | // We didn't find any line for the location, add it to the end.
78 | if (newLine is null)
79 | {
80 | newLine = Rent(virtualRange);
81 | _activeLines.Add(newLine);
82 | }
83 |
84 | return newLine;
85 | }
86 |
87 | public void RemoveOutsideOfRange(BitRange range)
88 | {
89 | for (int i = 0; i < _activeLines.Count; i++)
90 | {
91 | var line = _activeLines[i];
92 | if (!range.Contains(line.VirtualRange.Start))
93 | {
94 | Return(line);
95 | _activeLines.RemoveAt(i--);
96 | }
97 | }
98 | }
99 |
100 | public void Clear()
101 | {
102 | foreach (var instance in _activeLines)
103 | Return(instance);
104 | _activeLines.Clear();
105 | }
106 |
107 | private VisualBytesLine Rent(BitRange virtualRange)
108 | {
109 | var line = GetPooledLine();
110 | line.SetRange(virtualRange);
111 | line.Invalidate();
112 | return line;
113 | }
114 |
115 | private VisualBytesLine GetPooledLine()
116 | {
117 | while (_pool.TryPop(out var line))
118 | {
119 | if (line.Data.Length == _owner.ActualBytesPerLine)
120 | return line;
121 | }
122 |
123 | return new VisualBytesLine(_owner);
124 | }
125 |
126 | private void Return(VisualBytesLine line)
127 | {
128 | _pool.Push(line);
129 | }
130 |
131 | public IEnumerator GetEnumerator() => _activeLines.GetEnumerator();
132 |
133 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
134 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Document/DynamicBinaryDocument.cs:
--------------------------------------------------------------------------------
1 | using System.Runtime.InteropServices;
2 |
3 | namespace AvaloniaHex.Document;
4 |
5 | ///
6 | /// Represents a binary document that can be dynamically resized.
7 | ///
8 | public class DynamicBinaryDocument : IBinaryDocument
9 | {
10 | // TODO: List should be replaced with something that is more efficient for insert/remove operations
11 | // such as a Rope or gap-buffer.
12 | private readonly List _data;
13 |
14 | private readonly BitRangeUnion _validRanges;
15 |
16 | ///
17 | public event EventHandler? Changed;
18 |
19 | ///
20 | /// Creates a new empty dynamic binary document.
21 | ///
22 | public DynamicBinaryDocument()
23 | {
24 | _data = new List();
25 | _validRanges = new BitRangeUnion();
26 | ValidRanges = _validRanges.AsReadOnly();
27 | }
28 |
29 | ///
30 | /// Creates a new dynamic binary document with the provided initial data.
31 | ///
32 | /// The data to initialize the document with.
33 | public DynamicBinaryDocument(byte[] initialData)
34 | {
35 | _data = new List(initialData);
36 | _validRanges = new BitRangeUnion([new BitRange(0ul, (ulong) initialData.Length)]);
37 | ValidRanges = _validRanges.AsReadOnly();
38 | }
39 |
40 | ///
41 | public ulong Length => (ulong) _data.Count;
42 |
43 | ///
44 | public bool IsReadOnly { get; set; }
45 |
46 | ///
47 | public bool CanInsert { get; set; } = true;
48 |
49 | ///
50 | public bool CanRemove { get; set; } = true;
51 |
52 | ///
53 | public IReadOnlyBitRangeUnion ValidRanges { get; }
54 |
55 | private void AssertIsWriteable()
56 | {
57 | if (IsReadOnly)
58 | throw new InvalidOperationException("Document is read-only.");
59 | }
60 |
61 | ///
62 | public void ReadBytes(ulong offset, Span buffer)
63 | {
64 | CollectionsMarshal.AsSpan(_data).Slice((int) offset, buffer.Length).CopyTo(buffer);
65 | }
66 |
67 | ///
68 | public void WriteBytes(ulong offset, ReadOnlySpan buffer)
69 | {
70 | AssertIsWriteable();
71 |
72 | buffer.CopyTo(CollectionsMarshal.AsSpan(_data).Slice((int) offset, buffer.Length));
73 | OnChanged(new BinaryDocumentChange(BinaryDocumentChangeType.Modify, new BitRange(offset, offset + (ulong) buffer.Length)));
74 | }
75 |
76 | ///
77 | public void InsertBytes(ulong offset, ReadOnlySpan buffer)
78 | {
79 | AssertIsWriteable();
80 |
81 | if (!CanInsert)
82 | throw new InvalidOperationException("Data cannot be inserted into the document.");
83 |
84 | _data.InsertRange((int) offset, buffer.ToArray());
85 | _validRanges.Add(new BitRange(_validRanges.EnclosingRange.End, _validRanges.EnclosingRange.End.AddBytes((ulong) buffer.Length)));
86 |
87 | OnChanged(new BinaryDocumentChange(BinaryDocumentChangeType.Insert, new BitRange(offset, offset + (ulong) buffer.Length)));
88 | }
89 |
90 | ///
91 | public void RemoveBytes(ulong offset, ulong length)
92 | {
93 | AssertIsWriteable();
94 |
95 | if (!CanRemove)
96 | throw new InvalidOperationException("Data cannot be removed from the document.");
97 |
98 | _data.RemoveRange((int) offset, (int) length);
99 | _validRanges.Remove(new BitRange(_validRanges.EnclosingRange.End.SubtractBytes(length), _validRanges.EnclosingRange.End));
100 |
101 | OnChanged(new BinaryDocumentChange(BinaryDocumentChangeType.Remove, new BitRange(offset, offset + length)));
102 | }
103 |
104 | ///
105 | /// Fires the event.
106 | ///
107 | /// The event arguments describing the change.
108 | protected virtual void OnChanged(BinaryDocumentChange e) => Changed?.Invoke(this, e);
109 |
110 | ///
111 | public void Flush()
112 | {
113 | }
114 |
115 | ///
116 | public void Dispose()
117 | {
118 | }
119 |
120 | ///
121 | /// Serializes the contents of the document into a byte array.
122 | ///
123 | /// The serialized contents.
124 | public byte[] ToArray() => _data.ToArray();
125 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Document/MemoryMappedBinaryDocument.cs:
--------------------------------------------------------------------------------
1 | using System.IO.MemoryMappedFiles;
2 |
3 | namespace AvaloniaHex.Document;
4 |
5 | ///
6 | /// Represents a binary document that is backed by a file that is mapped into memory.
7 | ///
8 | public class MemoryMappedBinaryDocument : IBinaryDocument
9 | {
10 | ///
11 | public event EventHandler? Changed;
12 |
13 | private readonly MemoryMappedViewAccessor _accessor;
14 | private readonly bool _leaveOpen;
15 |
16 | ///
17 | /// Opens a file as a memory mapped document.
18 | ///
19 | /// The file to memory map.
20 | public MemoryMappedBinaryDocument(string filePath)
21 | : this(MemoryMappedFile.CreateFromFile(filePath, FileMode.OpenOrCreate), false, false)
22 | {
23 | }
24 |
25 | ///
26 | /// Wraps a memory mapped file in a document.
27 | ///
28 | /// The file to use as a backing storage.
29 | /// true if should be kept open on disposing, false otherwise.
30 | public MemoryMappedBinaryDocument(MemoryMappedFile file, bool leaveOpen)
31 | : this(file, leaveOpen, false)
32 | {
33 | }
34 |
35 | ///
36 | /// Wraps a memory mapped file in a document.
37 | ///
38 | /// The file to use as a backing storage.
39 | /// true if should be kept open on disposing, false otherwise.
40 | /// true if the document can be edited, false otherwise.
41 | public MemoryMappedBinaryDocument(MemoryMappedFile file, bool leaveOpen, bool isReadOnly)
42 | {
43 | File = file;
44 | _leaveOpen = leaveOpen;
45 | _accessor = file.CreateViewAccessor();
46 |
47 | // Yuck! But this seems to be the only way to get the length from a MemoryMappedFile.
48 | using var stream = file.CreateViewStream();
49 | Length = (ulong) stream.Length;
50 |
51 | ValidRanges = new BitRangeUnion([new BitRange(0, Length)]).AsReadOnly();
52 | IsReadOnly = isReadOnly;
53 | }
54 |
55 | ///
56 | /// Gets the underlying memory mapped file that is used as a backing storage for this document.
57 | ///
58 | public MemoryMappedFile File { get; }
59 |
60 | ///
61 | public ulong Length { get; }
62 |
63 | ///
64 | public bool IsReadOnly { get; }
65 |
66 | ///
67 | public bool CanInsert => false;
68 |
69 | ///
70 | public bool CanRemove => false;
71 |
72 | ///
73 | public IReadOnlyBitRangeUnion ValidRanges { get; }
74 |
75 | ///
76 | public void ReadBytes(ulong offset, Span buffer)
77 | {
78 | _accessor.SafeMemoryMappedViewHandle.ReadSpan(offset, buffer);
79 | }
80 |
81 | ///
82 | public void WriteBytes(ulong offset, ReadOnlySpan buffer)
83 | {
84 | if (IsReadOnly)
85 | throw new InvalidOperationException("Document is read-only.");
86 |
87 | _accessor.SafeMemoryMappedViewHandle.WriteSpan(offset, buffer);
88 | OnChanged(new BinaryDocumentChange(BinaryDocumentChangeType.Modify, new BitRange(offset, offset + (ulong) buffer.Length)));
89 | }
90 |
91 | ///
92 | public void InsertBytes(ulong offset, ReadOnlySpan buffer)
93 | {
94 | if (IsReadOnly)
95 | throw new InvalidOperationException("Document is read-only.");
96 |
97 | throw new InvalidOperationException("Document cannot be resized.");
98 | }
99 |
100 | ///
101 | public void RemoveBytes(ulong offset, ulong length)
102 | {
103 | if (IsReadOnly)
104 | throw new InvalidOperationException("Document is read-only.");
105 |
106 | throw new InvalidOperationException("Document cannot be resized.");
107 | }
108 |
109 | ///
110 | public void Flush() => _accessor.Flush();
111 |
112 | ///
113 | /// Fires the event.
114 | ///
115 | /// The event arguments describing the change.
116 | protected virtual void OnChanged(BinaryDocumentChange e) => Changed?.Invoke(this, e);
117 |
118 | ///
119 | public void Dispose()
120 | {
121 | _accessor.Dispose();
122 |
123 | if (!_leaveOpen)
124 | File.Dispose();
125 | }
126 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/AsciiColumn.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Media.TextFormatting;
3 | using AvaloniaHex.Document;
4 |
5 | namespace AvaloniaHex.Rendering;
6 |
7 | ///
8 | /// Represents a column that renders binary data using the ASCII text encoding.
9 | ///
10 | public class AsciiColumn : CellBasedColumn
11 | {
12 | static AsciiColumn()
13 | {
14 | CursorProperty.OverrideDefaultValue(IBeamCursor);
15 | IsHeaderVisibleProperty.OverrideDefaultValue(false);
16 | HeaderProperty.OverrideDefaultValue("ASCII");
17 | }
18 |
19 | ///
20 | public override Size MinimumSize => default;
21 |
22 | ///
23 | public override int BitsPerCell => 8;
24 |
25 | ///
26 | public override int CellsPerWord => 1;
27 |
28 | ///
29 | public override double GroupPadding => 0;
30 |
31 | ///
32 | protected override bool TryWriteCell(Span buffer, BitLocation bufferStart, BitLocation writeLocation, char input)
33 | {
34 | buffer[(int) (writeLocation.ByteIndex - bufferStart.ByteIndex)] = (byte) input;
35 | return true;
36 | }
37 |
38 | ///
39 | public override string? GetText(BitRange range)
40 | {
41 | if (HexView?.Document is null)
42 | return null;
43 |
44 | byte[] data = new byte[range.ByteLength];
45 | HexView.Document.ReadBytes(range.Start.ByteIndex, data);
46 |
47 | char[] output = new char[data.Length];
48 | GetText(data, range, output);
49 |
50 | return new string(output);
51 | }
52 |
53 | ///
54 | public override TextLine? CreateTextLine(VisualBytesLine line)
55 | {
56 | if (HexView is null)
57 | return null;
58 |
59 | var properties = GetTextRunProperties();
60 | return TextFormatter.Current.FormatLine(
61 | new AsciiTextSource(this, line, properties),
62 | 0,
63 | double.MaxValue,
64 | new GenericTextParagraphProperties(properties)
65 | );
66 | }
67 |
68 | private static char MapToPrintableChar(byte b)
69 | {
70 | return b switch
71 | {
72 | >= 0x20 and < 0x7f => (char) b,
73 | _ => '.'
74 | };
75 | }
76 |
77 | private void GetText(ReadOnlySpan data, BitRange dataRange, Span buffer)
78 | {
79 | char invalidCellChar = InvalidCellChar;
80 |
81 | if (HexView?.Document?.ValidRanges is not { } valid)
82 | {
83 | buffer.Fill(invalidCellChar);
84 | return;
85 | }
86 |
87 | for (int i = 0; i < data.Length; i++)
88 | {
89 | var cellLocation = new BitLocation(dataRange.Start.ByteIndex + (ulong) i, 0);
90 | var cellRange = new BitRange(cellLocation, cellLocation.AddBits(8));
91 |
92 | buffer[i] = valid.IsSuperSetOf(cellRange)
93 | ? MapToPrintableChar(data[i])
94 | : invalidCellChar;
95 | }
96 | }
97 |
98 | private sealed class AsciiTextSource : ITextSource
99 | {
100 | private readonly AsciiColumn _column;
101 | private readonly GenericTextRunProperties _properties;
102 | private readonly VisualBytesLine _line;
103 |
104 | public AsciiTextSource(AsciiColumn column, VisualBytesLine line, GenericTextRunProperties properties)
105 | {
106 | _column = column;
107 | _line = line;
108 | _properties = properties;
109 | }
110 |
111 | ///
112 | public TextRun? GetTextRun(int textSourceIndex)
113 | {
114 | // Find current segment we're in.
115 | var currentLocation = new BitLocation(_line.Range.Start.ByteIndex + (ulong) textSourceIndex);
116 | var segment = _line.FindSegmentContaining(currentLocation);
117 | if (segment is null)
118 | return null;
119 |
120 | // Stringify the segment.
121 | var range = segment.Range;
122 | ReadOnlySpan data = _line.AsAbsoluteSpan(range);
123 | Span buffer = stackalloc char[(int) range.ByteLength];
124 | _column.GetText(data, range, buffer);
125 |
126 | // Render
127 | return new TextCharacters(
128 | new string(buffer),
129 | _properties.WithBrushes(
130 | segment.ForegroundBrush ?? _properties.ForegroundBrush,
131 | segment.BackgroundBrush ?? _properties.BackgroundBrush
132 | )
133 | );
134 | }
135 | }
136 |
137 | }
--------------------------------------------------------------------------------
/examples/AvaloniaHex.Demo/SegmentedDocument.cs:
--------------------------------------------------------------------------------
1 | using System;
2 | using System.Collections.Generic;
3 | using System.Linq;
4 | using AvaloniaHex.Document;
5 |
6 | namespace AvaloniaHex.Demo;
7 |
8 | ///
9 | /// Provides an example implementation of a custom binary document that consists of one or more disjoint segments.
10 | ///
11 | public class SegmentedDocument : IBinaryDocument
12 | {
13 | public event EventHandler? Changed;
14 |
15 | private readonly Mapping[] _mappings;
16 | private readonly BitRangeUnion _ranges = new();
17 |
18 | public SegmentedDocument(IEnumerable mappings)
19 | : this(mappings.ToArray())
20 | {
21 | }
22 |
23 | public SegmentedDocument(params Mapping[] mappings)
24 | {
25 | _mappings = mappings.ToArray();
26 |
27 | foreach (var mapping in _mappings)
28 | {
29 | ulong oldLength = _ranges.EnclosingRange.ByteLength;
30 | _ranges.Add(mapping.Range);
31 | if (_ranges.EnclosingRange.ByteLength < oldLength + mapping.Range.ByteLength)
32 | throw new ArgumentException("Mappings are overlapping.");
33 | }
34 |
35 | Array.Sort(_mappings, (a, b) => a.Location.CompareTo(b.Location));
36 |
37 | ValidRanges = _ranges.AsReadOnly();
38 | }
39 |
40 | ///
41 | public ulong Length => _mappings[^1].Range.End.ByteIndex - _mappings[0].Range.Start.ByteIndex;
42 |
43 | ///
44 | public bool IsReadOnly => false;
45 |
46 | ///
47 | public bool CanInsert => false;
48 |
49 | ///
50 | public bool CanRemove => false;
51 |
52 | ///
53 | public IReadOnlyBitRangeUnion ValidRanges { get; }
54 |
55 | private bool TryGetMappingIndex(ulong offset, out int index)
56 | {
57 | // Linear (slow) lookup of mapping.
58 |
59 | for (var i = 0; i < _mappings.Length; i++)
60 | {
61 | if (_mappings[i].Range.Contains(new BitLocation(offset)))
62 | {
63 | index = i;
64 | return true;
65 | }
66 |
67 | if (_mappings[i].Range.Start.ByteIndex > offset)
68 | {
69 | index = i;
70 | return false;
71 | }
72 | }
73 |
74 | index = _mappings.Length;
75 | return false;
76 | }
77 |
78 | ///
79 | public void ReadBytes(ulong offset, Span buffer)
80 | {
81 | int bufferIndex = 0;
82 | while (bufferIndex < buffer.Length && offset < ValidRanges.EnclosingRange.End.ByteIndex)
83 | {
84 | // Find mapped segment for this offset.
85 | if (!TryGetMappingIndex(offset, out int mappingIndex))
86 | {
87 | // It does not exist for this byte. Jump to next segment.
88 | if (mappingIndex >= _mappings.Length)
89 | return;
90 |
91 | ulong nextStart = _mappings[mappingIndex].Location;
92 | ulong delta = nextStart - offset;
93 | offset = nextStart;
94 | bufferIndex += (int) delta;
95 | continue;
96 | }
97 |
98 | // Get the current segment and compute boundaries.
99 | var mapping = _mappings[mappingIndex];
100 | int remainingBytesToRead = buffer.Length - bufferIndex;
101 | int relativeOffset = (int) (offset - mapping.Location);
102 | int remainingAvailableBytes = mapping.Data.Length - relativeOffset;
103 |
104 | // Read the data.
105 | int actualLength = Math.Min(remainingBytesToRead, remainingAvailableBytes);
106 | mapping.Data.AsSpan(relativeOffset, actualLength).CopyTo(buffer[bufferIndex..]);
107 |
108 | // Increment pointers.
109 | bufferIndex += actualLength;
110 | offset += (ulong) actualLength;
111 | }
112 | }
113 |
114 | ///
115 | public void WriteBytes(ulong offset, ReadOnlySpan buffer)
116 | {
117 | // Get the segment to write to.
118 | if (!TryGetMappingIndex(offset, out int mappingIndex))
119 | return;
120 |
121 | // Get mapping and compute boundaries.
122 | var mapping = _mappings[mappingIndex];
123 | int relativeOffset = (int) (offset - mapping.Location);
124 | int availableBytes = mapping.Data.Length - relativeOffset;
125 |
126 | // Write
127 | int actualLength = Math.Min(availableBytes, buffer.Length);
128 | buffer[..actualLength].CopyTo(mapping.Data.AsSpan(relativeOffset, actualLength));
129 |
130 | // Notify for changes.
131 | OnChanged(new BinaryDocumentChange(BinaryDocumentChangeType.Modify, new BitRange(offset, offset + (ulong) actualLength)));
132 | }
133 |
134 | ///
135 | public void InsertBytes(ulong offset, ReadOnlySpan buffer) => throw new NotSupportedException();
136 |
137 | ///
138 | public void RemoveBytes(ulong offset, ulong length) => throw new NotSupportedException();
139 |
140 | protected virtual void OnChanged(BinaryDocumentChange e) => Changed?.Invoke(this, e);
141 |
142 |
143 | void IBinaryDocument.Flush()
144 | {
145 | }
146 |
147 | void IDisposable.Dispose()
148 | {
149 | }
150 |
151 | ///
152 | /// A single mapped segment.
153 | ///
154 | /// The start address of the segment.
155 | /// The backing buffer.
156 | public readonly record struct Mapping(ulong Location, byte[] Data)
157 | {
158 | ///
159 | /// Gets the memory range the segment spans.
160 | ///
161 | public BitRange Range => new(Location, Location + (ulong) Data.Length);
162 | }
163 | }
--------------------------------------------------------------------------------
/test/AvaloniaHex.Tests/Document/BitRangeTest.cs:
--------------------------------------------------------------------------------
1 | using AvaloniaHex.Document;
2 |
3 | namespace AvaloniaHex.Tests.Document;
4 |
5 | public class BitRangeTest
6 | {
7 | [Fact]
8 | public void AllowEmptyRange()
9 | {
10 | Assert.True(new BitRange(0, 0).IsEmpty);
11 | Assert.True(new BitRange(10, 10).IsEmpty);
12 | Assert.True(new BitRange(new BitLocation(0, 5), new BitLocation(0, 5)).IsEmpty);
13 | }
14 |
15 | [Fact]
16 | public void DoNotAllowEndBeforeStart()
17 | {
18 | Assert.Throws(() => new BitRange(10, 9));
19 | Assert.Throws(() => new BitRange(new BitLocation(10, 5), new BitLocation(10, 4)));
20 | }
21 |
22 | [Theory]
23 | [InlineData(0, 0, 10, 0, 10 * 8)]
24 | [InlineData(10, 0, 10, 0, 0)]
25 | [InlineData(10, 5, 11, 0, 3)]
26 | [InlineData(10, 5, 12, 0, 8+3)]
27 | [InlineData(10, 0, 10, 5, 5)]
28 | [InlineData(10, 0, 11, 5, 8+5)]
29 | [InlineData(10, 3, 20, 3, 5+9*8+3)]
30 | public void BitLength(
31 | ulong startByte, int startBit,
32 | ulong endByte, int endBit,
33 | ulong length)
34 | {
35 | var range = new BitRange(
36 | new BitLocation(startByte, startBit),
37 | new BitLocation(endByte, endBit)
38 | );
39 |
40 | Assert.Equal(length, range.BitLength);
41 | }
42 |
43 | [Theory]
44 | [InlineData(0,0, 10, 0, 5, 0, true)] // middle
45 | [InlineData(10,5, 100, 0, 10, 5, true)] // start is inclusive
46 | [InlineData(10,5, 100, 0, 10, 4, false)] // before start
47 | [InlineData(10,5, 100, 5, 100, 5, false)] // end is exclusive
48 | [InlineData(10,5, 100, 5, 100, 4, true)] // at end.
49 | public void ContainsLocation(
50 | ulong startByte, int startBit,
51 | ulong endByte, int endBit,
52 | ulong needleByte, int needleBit,
53 | bool expected)
54 | {
55 | var range = new BitRange(
56 | new BitLocation(startByte, startBit),
57 | new BitLocation(endByte, endBit)
58 | );
59 |
60 | Assert.Equal(expected, range.Contains(new BitLocation(needleByte, needleBit)));
61 | }
62 |
63 | [Theory]
64 | [InlineData(
65 | 0, 0, 100, 0,
66 | 50, 0, 60, 0,
67 | true
68 | )]
69 | [InlineData(
70 | 0, 0, 100, 0,
71 | 50, 0, 150, 0,
72 | false
73 | )]
74 | [InlineData(
75 | 50, 0, 100, 0,
76 | 0, 0, 75, 0,
77 | false
78 | )]
79 | public void ContainsRange(
80 | ulong startByte1, int startBit1, ulong endByte1, int endBit1,
81 | ulong startByte2, int startBit2, ulong endByte2, int endBit2,
82 | bool expected)
83 | {
84 | var range1 = new BitRange(
85 | new BitLocation(startByte1, startBit1),
86 | new BitLocation(endByte1, endBit1)
87 | );
88 |
89 | var range2 = new BitRange(
90 | new BitLocation(startByte2, startBit2),
91 | new BitLocation(endByte2, endBit2)
92 | );
93 |
94 | Assert.Equal(expected, range1.Contains(range2));
95 | }
96 |
97 | [Theory]
98 | [InlineData(
99 | 0, 0, 10, 0,
100 | 0, 0, 10,0,
101 | true
102 | )]
103 | [InlineData(
104 | 0, 0, 10, 0,
105 | 5, 0, 15,0,
106 | true
107 | )]
108 | [InlineData(
109 | 0, 0, 10, 0,
110 | 9, 0, 15,0,
111 | true
112 | )]
113 | [InlineData(
114 | 0, 0, 10, 0,
115 | 10, 0, 15,0,
116 | false
117 | )]
118 | [InlineData(
119 | 5, 0, 15,0,
120 | 0, 0, 10, 0,
121 | true
122 | )]
123 | [InlineData(
124 | 9, 0, 15,0,
125 | 0, 0, 10, 0,
126 | true
127 | )]
128 | [InlineData(
129 | 10, 0, 15,0,
130 | 0, 0, 10, 0,
131 | false
132 | )]
133 | [InlineData(
134 | 5, 0, 15,0,
135 | 7, 0, 10, 0,
136 | true
137 | )]
138 | public void OverlapsWith(
139 | ulong startByte1, int startBit1, ulong endByte1, int endBit1,
140 | ulong startByte2, int startBit2, ulong endByte2, int endBit2,
141 | bool expected)
142 | {
143 | var range1 = new BitRange(
144 | new BitLocation(startByte1, startBit1),
145 | new BitLocation(endByte1, endBit1)
146 | );
147 |
148 | var range2 = new BitRange(
149 | new BitLocation(startByte2, startBit2),
150 | new BitLocation(endByte2, endBit2)
151 | );
152 |
153 | Assert.Equal(expected, range1.OverlapsWith(range2));
154 | Assert.Equal(expected, range2.OverlapsWith(range1));
155 | }
156 |
157 | [Theory]
158 | [InlineData(
159 | 10, 0, 100, 0,
160 | 40, 0, 50, 0,
161 | 40, 0, 50, 0
162 | )]
163 | [InlineData(
164 | 10, 0, 100, 0,
165 | 50, 0, 150, 0,
166 | 50, 0, 100, 0
167 | )]
168 | [InlineData(
169 | 10, 0, 100, 0,
170 | 0, 0, 50, 0,
171 | 10, 0, 50, 0
172 | )]
173 | public void ClampOverlapping(
174 | ulong startByte1, int startBit1, ulong endByte1, int endBit1,
175 | ulong startByte2, int startBit2, ulong endByte2, int endBit2,
176 | ulong startByte3, int startBit3, ulong endByte3, int endBit3)
177 | {
178 | var original = new BitRange(new BitLocation(startByte1, startBit1), new BitLocation(endByte1, endBit1));
179 | var restriction = new BitRange(new BitLocation(startByte2, startBit2), new BitLocation(endByte2, endBit2));
180 | var expected = new BitRange(new BitLocation(startByte3, startBit3), new BitLocation(endByte3, endBit3));
181 | Assert.Equal(expected, original.Clamp(restriction));
182 | }
183 |
184 | [Fact]
185 | public void ClampNonOverlapping()
186 | {
187 | var original = new BitRange(10, 100);
188 | var restriction = new BitRange(200, 210);
189 | Assert.True(original.Clamp(restriction).IsEmpty);
190 | }
191 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Editing/SelectionLayer.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Media;
3 | using AvaloniaHex.Document;
4 | using AvaloniaHex.Rendering;
5 |
6 | namespace AvaloniaHex.Editing;
7 |
8 | ///
9 | /// Represents the layer that renders the selection in a hex view.
10 | ///
11 | public class SelectionLayer : Layer
12 | {
13 | private readonly Selection _selection;
14 | private readonly Caret _caret;
15 |
16 | static SelectionLayer()
17 | {
18 | AffectsRender(
19 | PrimarySelectionBorderProperty,
20 | PrimarySelectionBackgroundProperty,
21 | SecondarySelectionBorderProperty,
22 | SecondarySelectionBackgroundProperty
23 | );
24 | }
25 |
26 | ///
27 | public override LayerRenderMoments UpdateMoments => LayerRenderMoments.NoResizeRearrange;
28 |
29 | ///
30 | /// Creates a new selection layer.
31 | ///
32 | /// The caret the selection is following.
33 | /// The selection to render.
34 | public SelectionLayer(Caret caret, Selection selection)
35 | {
36 | _selection = selection;
37 | _caret = caret;
38 | _selection.RangeChanged += SelectionOnRangeChanged;
39 | _caret.PrimaryColumnChanged += CaretOnPrimaryColumnChanged;
40 | }
41 |
42 | ///
43 | /// Defines the property.
44 | ///
45 | public static readonly StyledProperty PrimarySelectionBorderProperty =
46 | AvaloniaProperty.Register(
47 | nameof(PrimarySelectionBorder),
48 | new Pen(Brushes.Blue)
49 | );
50 |
51 | ///
52 | /// Gets or sets the pen used for drawing the border of the selection in the active column.
53 | ///
54 | public IPen? PrimarySelectionBorder
55 | {
56 | get => GetValue(PrimarySelectionBorderProperty);
57 | set => SetValue(PrimarySelectionBorderProperty, value);
58 | }
59 |
60 | ///
61 | /// Defines the property.
62 | ///
63 | public static readonly StyledProperty PrimarySelectionBackgroundProperty =
64 | AvaloniaProperty.Register(
65 | nameof(PrimarySelectionBackground),
66 | new SolidColorBrush(Colors.Blue, 0.5D)
67 | );
68 |
69 | ///
70 | /// Gets or sets the brush used for drawing the background of the selection in the active column.
71 | ///
72 | public IBrush? PrimarySelectionBackground
73 | {
74 | get => GetValue(PrimarySelectionBackgroundProperty);
75 | set => SetValue(PrimarySelectionBackgroundProperty, value);
76 | }
77 |
78 | ///
79 | /// Defines the property.
80 | ///
81 | public static readonly StyledProperty SecondarySelectionBorderProperty =
82 | AvaloniaProperty.Register(
83 | nameof(PrimarySelectionBorder),
84 | new Pen(Brushes.Blue)
85 | );
86 |
87 | ///
88 | /// Gets or sets the pen used for drawing the border of the selection in non-active columns.
89 | ///
90 | public IPen? SecondarySelectionBorder
91 | {
92 | get => GetValue(SecondarySelectionBorderProperty);
93 | set => SetValue(SecondarySelectionBorderProperty, value);
94 | }
95 |
96 | ///
97 | /// Defines the property.
98 | ///
99 | public static readonly StyledProperty SecondarySelectionBackgroundProperty =
100 | AvaloniaProperty.Register(
101 | nameof(SecondarySelectionBackgroundProperty),
102 | new SolidColorBrush(Colors.Blue, 0.25D)
103 | );
104 |
105 | ///
106 | /// Gets or sets the brush used for drawing the background of the selection in non-active columns.
107 | ///
108 | public IBrush? SecondarySelectionBackground
109 | {
110 | get => GetValue(SecondarySelectionBackgroundProperty);
111 | set => SetValue(SecondarySelectionBackgroundProperty, value);
112 | }
113 |
114 | private void SelectionOnRangeChanged(object? sender, EventArgs e)
115 | {
116 | InvalidateVisual();
117 | }
118 |
119 | private void CaretOnPrimaryColumnChanged(object? sender, EventArgs e)
120 | {
121 | InvalidateVisual();
122 | }
123 |
124 | ///
125 | public override void Render(DrawingContext context)
126 | {
127 | base.Render(context);
128 |
129 | if (HexView is null || GetVisibleSelectionRange() is not { } range)
130 | return;
131 |
132 | for (int i = 0; i < HexView.Columns.Count; i++)
133 | {
134 | if (HexView.Columns[i] is CellBasedColumn { IsVisible: true } column)
135 | DrawSelection(context, column, range);
136 | }
137 | }
138 |
139 | private BitRange? GetVisibleSelectionRange()
140 | {
141 | if (HexView is null || !_selection.Range.OverlapsWith(HexView.VisibleRange))
142 | return null;
143 |
144 | return new BitRange(
145 | _selection.Range.Start.Max(HexView.VisibleRange.Start),
146 | _selection.Range.End.Min(HexView.VisibleRange.End)
147 | );
148 | }
149 |
150 | private void DrawSelection(DrawingContext context, CellBasedColumn column, BitRange range)
151 | {
152 | var geometry = CellGeometryBuilder.CreateBoundingGeometry(column, range);
153 | if (geometry is null)
154 | return;
155 |
156 | if (_caret.PrimaryColumnIndex == column.Index)
157 | context.DrawGeometry(PrimarySelectionBackground, PrimarySelectionBorder, geometry);
158 | else
159 | context.DrawGeometry(SecondarySelectionBackground, SecondarySelectionBorder, geometry);
160 | }
161 |
162 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Document/BitRange.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 |
3 | namespace AvaloniaHex.Document;
4 |
5 | ///
6 | /// Represents a bit range within a binary document.
7 | ///
8 | [DebuggerDisplay("[{Start}, {End})")]
9 | public readonly struct BitRange : IEquatable
10 | {
11 | ///
12 | /// Represents the empty range.
13 | ///
14 | public static readonly BitRange Empty = new();
15 |
16 | ///
17 | /// Creates a new bit range.
18 | ///
19 | /// The start byte offset.
20 | /// The (exclusive) end byte offset.
21 | public BitRange(ulong start, ulong end)
22 | : this(new BitLocation(start), new BitLocation(end))
23 | {
24 | }
25 |
26 | ///
27 | /// Creates a new bit range.
28 | ///
29 | /// The start location.
30 | /// The (exclusive) end location.
31 | public BitRange(BitLocation start, BitLocation end)
32 | {
33 | if (end < start)
34 | throw new ArgumentException("End location is smaller than start location.");
35 |
36 | Start = start;
37 | End = end;
38 | }
39 |
40 | ///
41 | /// Gets the start location of the range.
42 | ///
43 | public BitLocation Start { get; }
44 |
45 | ///
46 | /// Gets the exclusive end location of the range.
47 | ///
48 | public BitLocation End { get; }
49 |
50 | ///
51 | /// Gets the total number of bytes that the range spans.
52 | ///
53 | public ulong ByteLength => End.ByteIndex - Start.ByteIndex;
54 |
55 | ///
56 | /// Gets the total number of bits that the range spans.
57 | ///
58 | public ulong BitLength
59 | {
60 | get
61 | {
62 | ulong result = ByteLength * 8;
63 | result -= (ulong)Start.BitIndex;
64 | result += (ulong)End.BitIndex;
65 | return result;
66 | }
67 | }
68 |
69 | ///
70 | /// Gets a value indicating whether the range is empty or not.
71 | ///
72 | public bool IsEmpty => BitLength == 0;
73 |
74 | ///
75 | /// Determines whether the provided location is within the range.
76 | ///
77 | /// The location.
78 | /// true if the location is within the range, false otherwise.
79 | public bool Contains(BitLocation location) => location >= Start && location < End;
80 |
81 | ///
82 | /// Determines whether the provided range falls completely within the current range.
83 | ///
84 | /// The other range.
85 | /// true if the provided range is completely enclosed, false otherwise.
86 | public bool Contains(BitRange other) => Contains(other.Start) && Contains(other.End.PreviousOrZero());
87 |
88 | ///
89 | /// Determines whether the current range overlaps with the provided range.
90 | ///
91 | /// The other range.
92 | /// true if the range overlaps, false otherwise.
93 | public bool OverlapsWith(BitRange other)
94 | => Contains(other.Start)
95 | || Contains(other.End.PreviousOrZero())
96 | || other.Contains(Start)
97 | || other.Contains(End.PreviousOrZero());
98 |
99 | ///
100 | /// Extends the range to the provided location.
101 | ///
102 | /// The location to extend to.
103 | /// The extended range.
104 | public BitRange ExtendTo(BitLocation location) => new(Start.Min(location), End.Max(location));
105 |
106 | ///
107 | /// Restricts the range to the provided range.
108 | ///
109 | /// The range to restrict to.
110 | /// The restricted range.
111 | public BitRange Clamp(BitRange range)
112 | {
113 | var start = Start.Max(range.Start);
114 | var end = End.Min(range.End);
115 | if (start > end)
116 | return Empty;
117 |
118 | return new BitRange(start, end);
119 | }
120 |
121 | ///
122 | /// Splits the range at the provided location.
123 | ///
124 | /// The location to split at.
125 | /// The two resulting ranges.
126 | ///
127 | /// Occurs when the provided location does not fall within the current range.
128 | ///
129 | public (BitRange, BitRange) Split(BitLocation location)
130 | {
131 | if (!Contains(location))
132 | throw new ArgumentOutOfRangeException(nameof(location));
133 |
134 | return (
135 | new BitRange(Start, location),
136 | new BitRange(location, End)
137 | );
138 | }
139 |
140 | ///
141 | public bool Equals(BitRange other) => Start.Equals(other.Start) && End.Equals(other.End);
142 |
143 | ///
144 | public override bool Equals(object? obj) => obj is BitRange other && Equals(other);
145 |
146 | ///
147 | public override int GetHashCode()
148 | {
149 | unchecked
150 | {
151 | return (Start.GetHashCode() * 397) ^ End.GetHashCode();
152 | }
153 | }
154 |
155 | ///
156 | public override string ToString() => $"[{Start}, {End})";
157 |
158 | ///
159 | /// Determines whether two ranges are equal.
160 | ///
161 | /// The first range.
162 | /// The second range.
163 | /// true if the ranges are equal, false otherwise.
164 | public static bool operator ==(BitRange a, BitRange b) => a.Equals(b);
165 |
166 | ///
167 | /// Determines whether two ranges are not equal.
168 | ///
169 | /// The first range.
170 | /// The second range.
171 | /// true if the ranges are not equal, false otherwise.
172 | public static bool operator !=(BitRange a, BitRange b) => !a.Equals(b);
173 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/VisualBytesLine.cs:
--------------------------------------------------------------------------------
1 | using System.Diagnostics;
2 | using Avalonia;
3 | using Avalonia.Media.TextFormatting;
4 | using AvaloniaHex.Document;
5 |
6 | namespace AvaloniaHex.Rendering;
7 |
8 | ///
9 | /// Represents a single visual line in a hex view.
10 | ///
11 | [DebuggerDisplay("{Range}")]
12 | public sealed class VisualBytesLine
13 | {
14 | internal VisualBytesLine(HexView hexView)
15 | {
16 | HexView = hexView;
17 |
18 | Data = new byte[hexView.ActualBytesPerLine];
19 | ColumnTextLines = new TextLine?[hexView.Columns.Count];
20 | Segments = new List();
21 | }
22 |
23 | ///
24 | /// Gets the parent ehx view the line is visible in.
25 | ///
26 | public HexView HexView { get; }
27 |
28 | ///
29 | /// Gets the bit range the visual line spans. If this line is the last visible line in the document, this may include
30 | /// the "virtual" cell to insert into.
31 | ///
32 | public BitRange VirtualRange { get; private set; }
33 |
34 | ///
35 | /// Gets the bit range the visual line spans.
36 | ///
37 | public BitRange Range { get; private set; }
38 |
39 | ///
40 | /// Gets the data that is displayed in the line.
41 | ///
42 | public byte[] Data { get; }
43 |
44 | ///
45 | /// Gets the bounding box in the hex view the line is rendered at.
46 | ///
47 | public Rect Bounds { get; internal set; }
48 |
49 | ///
50 | /// Gets the individual segments the line comprises.
51 | ///
52 | public List Segments { get; }
53 |
54 | ///
55 | /// Gets the individual text lines for every column.
56 | ///
57 | public TextLine?[] ColumnTextLines { get; }
58 |
59 | ///
60 | /// Gets a value indicating whether the data and line segments present in the visual line are up to date.
61 | ///
62 | public bool IsValid { get; private set; }
63 |
64 | ///
65 | /// Gets the byte in the visual line at the provided absolute byte offset.
66 | ///
67 | /// The byte offset.
68 | /// The byte.
69 | public byte GetByteAtAbsolute(ulong byteIndex)
70 | {
71 | return Data[byteIndex - Range.Start.ByteIndex];
72 | }
73 |
74 | ///
75 | /// Obtains the span that includes the provided range.
76 | ///
77 | /// The range.
78 | /// The span.
79 | public Span AsAbsoluteSpan(BitRange range)
80 | {
81 | if (!Range.Contains(range))
82 | throw new ArgumentException("Provided range is not within the current line");
83 |
84 | return Data.AsSpan(
85 | (int)(range.Start.ByteIndex - Range.Start.ByteIndex),
86 | (int)range.ByteLength
87 | );
88 | }
89 |
90 | ///
91 | /// Finds the segment that contains the provided location.
92 | ///
93 | /// The location.
94 | /// The segment, or null if no segment contains the provided location.
95 | public VisualBytesLineSegment? FindSegmentContaining(BitLocation location)
96 | {
97 | foreach (var segment in Segments)
98 | {
99 | if (segment.Range.Contains(location))
100 | return segment;
101 | }
102 |
103 | return null;
104 | }
105 |
106 | internal bool SetRange(BitRange virtualRange)
107 | {
108 | bool hasChanged = false;
109 |
110 | if (VirtualRange != virtualRange)
111 | {
112 | VirtualRange = virtualRange;
113 | hasChanged = true;
114 | }
115 |
116 | var range = HexView.Document is { ValidRanges.EnclosingRange: var enclosingRange }
117 | ? virtualRange.Clamp(enclosingRange)
118 | : BitRange.Empty;
119 |
120 | if (Range != range)
121 | {
122 | Range = range;
123 | hasChanged = true;
124 | }
125 |
126 | return hasChanged;
127 | }
128 |
129 | ///
130 | /// Ensures the visual line is populated with the latest binary data and line segments.
131 | ///
132 | public void EnsureIsValid()
133 | {
134 | if (!IsValid)
135 | Refresh();
136 | }
137 |
138 | ///
139 | /// Marks the visual line, its binary data and line segments as out of date.
140 | ///
141 | public void Invalidate() => IsValid = false;
142 |
143 | ///
144 | /// Updates the visual line with the latest data of the document and reconstructs all line segments.
145 | ///
146 | public void Refresh()
147 | {
148 | if (HexView.Document is null)
149 | return;
150 |
151 | ReadData();
152 | CreateLineSegments();
153 | CreateColumnTextLines();
154 |
155 | IsValid = true;
156 | }
157 |
158 | private void ReadData()
159 | {
160 | var document = HexView.Document!;
161 | var dataSpan = Data.AsSpan(0, (int) Range.ByteLength);
162 |
163 | // Fast path, just read entire range if possible.
164 | if (!document.ValidRanges.IsFragmented)
165 | {
166 | document.ReadBytes(Range.Start.ByteIndex, dataSpan);
167 | return;
168 | }
169 |
170 | // Only read valid segments in the line.
171 | Span ranges = stackalloc BitRange[dataSpan.Length];
172 | int count = document.ValidRanges.GetIntersectingRanges(Range, ranges);
173 | for (int i = 0; i < count; i++)
174 | {
175 | var range = ranges[i];
176 | int relativeOffset = (int) (range.Start.ByteIndex - Range.Start.ByteIndex);
177 |
178 | var chunk = dataSpan[relativeOffset..(relativeOffset + (int) range.ByteLength)];
179 | document.ReadBytes(range.Start.ByteIndex, chunk);
180 | }
181 | }
182 |
183 | private void CreateLineSegments()
184 | {
185 | Segments.Clear();
186 | Segments.Add(new VisualBytesLineSegment(Range));
187 |
188 | var transformers = HexView.LineTransformers;
189 | for (int i = 0; i < transformers.Count; i++)
190 | transformers[i].Transform(HexView, this);
191 | }
192 |
193 | private void CreateColumnTextLines()
194 | {
195 | for (int i = 0; i < HexView.Columns.Count; i++)
196 | {
197 | var column = HexView.Columns[i];
198 | if (column.IsVisible)
199 | {
200 | ColumnTextLines[i]?.Dispose();
201 | ColumnTextLines[i] = column.CreateTextLine(this);
202 | }
203 | }
204 | }
205 |
206 | ///
207 | /// Computes the required height required to the visual line occupies.
208 | ///
209 | /// The height.
210 | public double GetRequiredHeight()
211 | {
212 | double height = 0;
213 | foreach (var columns in ColumnTextLines)
214 | height = Math.Max(height, columns?.Height ?? 0);
215 | return height;
216 | }
217 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/BinaryColumn.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Media.TextFormatting;
3 | using AvaloniaHex.Document;
4 |
5 | namespace AvaloniaHex.Rendering;
6 |
7 | ///
8 | /// Represents a column that renders binary data using the binary number encoding.
9 | ///
10 | public class BinaryColumn : CellBasedColumn
11 | {
12 | static BinaryColumn()
13 | {
14 | CursorProperty.OverrideDefaultValue(IBeamCursor);
15 | UseDynamicHeaderProperty.Changed.AddClassHandler(OnUseDynamicHeaderChanged);
16 | HeaderProperty.OverrideDefaultValue("Binary");
17 | }
18 |
19 | ///
20 | /// Dependency property for
21 | ///
22 | public static readonly StyledProperty UseDynamicHeaderProperty =
23 | AvaloniaProperty.Register(nameof(UseDynamicHeader), true);
24 |
25 | ///
26 | /// Gets or sets a value indicating whether the header of this column should be dynamically
27 | ///
28 | public bool UseDynamicHeader
29 | {
30 | get => GetValue(IsHeaderVisibleProperty);
31 | set => SetValue(IsHeaderVisibleProperty, value);
32 | }
33 |
34 | ///
35 | public override Size MinimumSize => default;
36 |
37 | ///
38 | public override double GroupPadding => CellSize.Width;
39 |
40 | ///
41 | public override int BitsPerCell => 1;
42 |
43 | ///
44 | public override int CellsPerWord => 8;
45 |
46 | private static byte? ParseBit(char c) => c switch
47 | {
48 | '0' => 0,
49 | '1' => 1,
50 | _ => null
51 | };
52 |
53 | ///
54 | protected override string PrepareTextInput(string input) => input.Replace(" ", "");
55 |
56 | ///
57 | protected override bool TryWriteCell(Span buffer, BitLocation bufferStart, BitLocation writeLocation, char input)
58 | {
59 | if (ParseBit(input) is not { } bit)
60 | return false;
61 |
62 | int relativeByteIndex = (int) (writeLocation.ByteIndex - bufferStart.ByteIndex);
63 | buffer[relativeByteIndex] = (byte)(
64 | buffer[relativeByteIndex] & ~(1 << writeLocation.BitIndex) | (bit << writeLocation.BitIndex)
65 | );
66 |
67 | return true;
68 | }
69 |
70 | ///
71 | public override string? GetText(BitRange range)
72 | {
73 | if (HexView?.Document is null)
74 | return null;
75 |
76 | byte[] data = new byte[range.ByteLength];
77 | HexView.Document.ReadBytes(range.Start.ByteIndex, data);
78 |
79 | char[] output = new char[data.Length * 3 - 1];
80 | GetText(data, range, output);
81 |
82 | return new string(output);
83 | }
84 |
85 | ///
86 | public override TextLine? CreateHeaderLine()
87 | {
88 | if (!UseDynamicHeader)
89 | return base.CreateHeaderLine();
90 |
91 | if (HexView is null)
92 | return null;
93 |
94 | // Generate header text.
95 | int count = HexView.ActualBytesPerLine;
96 | char[] buffer = new char[count * 9 - 1];
97 |
98 | for (int i = 0; i < count; i++)
99 | {
100 | for (int j = 0; j < 8; j++)
101 | buffer[i * 9 + j] = (char) (((i >> (7 - j)) & 1) + '0');
102 |
103 | if (i < count - 1)
104 | buffer[i * 9 + 8] = ' ';
105 | }
106 |
107 | // Render.
108 | var properties = GetHeaderTextRunProperties();
109 | return TextFormatter.Current.FormatLine(
110 | new SimpleTextSource(new string(buffer), properties),
111 | 0,
112 | double.MaxValue,
113 | new GenericTextParagraphProperties(properties)
114 | );
115 | }
116 |
117 | ///
118 | public override TextLine? CreateTextLine(VisualBytesLine line)
119 | {
120 | if (HexView is null)
121 | return null;
122 |
123 | var properties = GetTextRunProperties();
124 | return TextFormatter.Current.FormatLine(
125 | new BinaryTextSource(this, line, properties),
126 | 0,
127 | double.MaxValue,
128 | new GenericTextParagraphProperties(properties)
129 | );
130 | }
131 |
132 | private void GetText(ReadOnlySpan data, BitRange dataRange, Span buffer)
133 | {
134 | char invalidCellChar = InvalidCellChar;
135 |
136 | if (HexView?.Document?.ValidRanges is not { } valid)
137 | {
138 | buffer.Fill(invalidCellChar);
139 | return;
140 | }
141 |
142 | int index = 0;
143 | for (int i = 0; i < data.Length; i++)
144 | {
145 | if (i > 0)
146 | buffer[index++] = ' ';
147 |
148 | byte value = data[i];
149 |
150 | for (int j = 0; j < 8; j++)
151 | {
152 | var location = new BitLocation(dataRange.Start.ByteIndex + (ulong) i, 7 - j);
153 | buffer[index + j] = valid.Contains(location)
154 | ? (char) (((value >> location.BitIndex) & 1) + '0')
155 | : invalidCellChar;
156 | }
157 |
158 | index += 8;
159 | }
160 | }
161 |
162 | private static void OnUseDynamicHeaderChanged(BinaryColumn arg1, AvaloniaPropertyChangedEventArgs arg2)
163 | {
164 | arg1.HexView?.InvalidateHeaders();
165 | }
166 |
167 | private sealed class BinaryTextSource : ITextSource
168 | {
169 | private readonly BinaryColumn _column;
170 | private readonly GenericTextRunProperties _properties;
171 | private readonly VisualBytesLine _line;
172 |
173 | public BinaryTextSource(BinaryColumn column, VisualBytesLine line, GenericTextRunProperties properties)
174 | {
175 | _column = column;
176 | _line = line;
177 | _properties = properties;
178 | }
179 |
180 | ///
181 | public TextRun? GetTextRun(int textSourceIndex)
182 | {
183 | // Calculate current byte location from text index.
184 | int byteIndex = Math.DivRem(textSourceIndex, 9, out int bitIndex);
185 | if (byteIndex < 0 || byteIndex >= _line.Data.Length)
186 | return null;
187 |
188 | // Special case nibble index 8 (space after byte).
189 | if (bitIndex == 8)
190 | {
191 | if (byteIndex >= _line.Data.Length - 1)
192 | return null;
193 |
194 | return new TextCharacters(" ", _properties);
195 | }
196 |
197 | // Find current segment we're in.
198 | var currentLocation = new BitLocation(_line.Range.Start.ByteIndex + (ulong) byteIndex, bitIndex);
199 | var segment = _line.FindSegmentContaining(currentLocation);
200 | if (segment is null)
201 | return null;
202 |
203 | // Stringify the segment.
204 | var range = segment.Range;
205 | ReadOnlySpan data = _line.AsAbsoluteSpan(range);
206 | Span buffer = stackalloc char[(int) segment.Range.ByteLength * 9 - 1];
207 | _column.GetText(data, range, buffer);
208 |
209 | // Render
210 | return new TextCharacters(
211 | new string(buffer),
212 | _properties.WithBrushes(
213 | segment.ForegroundBrush ?? _properties.ForegroundBrush,
214 | segment.BackgroundBrush ?? _properties.BackgroundBrush
215 | )
216 | );
217 | }
218 | }
219 |
220 | }
--------------------------------------------------------------------------------
/examples/AvaloniaHex.Demo/MainWindow.axaml:
--------------------------------------------------------------------------------
1 |
12 |
13 |
14 |
15 |
16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 |
26 |
101 |
102 |
105 |
106 |
107 |
108 |
109 |
110 |
115 |
116 |
117 |
118 |
119 |
120 |
121 |
122 |
123 |
124 |
125 |
126 |
127 |
128 |
129 |
--------------------------------------------------------------------------------
/src/AvaloniaHex/Editing/CaretLayer.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Interactivity;
3 | using Avalonia.Media;
4 | using Avalonia.Threading;
5 | using AvaloniaHex.Rendering;
6 |
7 | namespace AvaloniaHex.Editing;
8 |
9 | ///
10 | /// Represents the layer that renders the caret in a hex view.
11 | ///
12 | public class CaretLayer : Layer
13 | {
14 | private readonly DispatcherTimer _blinkTimer;
15 | private bool _caretVisible;
16 |
17 | static CaretLayer()
18 | {
19 | AffectsRender(
20 | InsertCaretWidthProperty,
21 | PrimaryColumnBorderProperty,
22 | PrimaryColumnBackgroundProperty,
23 | SecondaryColumnBorderProperty,
24 | SecondaryColumnBackgroundProperty
25 | );
26 | }
27 |
28 | ///
29 | /// Creates a new caret layer.
30 | ///
31 | /// The caret to render.
32 | public CaretLayer(Caret caret)
33 | {
34 | Caret = caret;
35 | Caret.LocationChanged += CaretOnChanged;
36 | Caret.ModeChanged += CaretOnChanged;
37 | Caret.PrimaryColumnChanged += CaretOnChanged;
38 | IsHitTestVisible = false;
39 |
40 | _blinkTimer = new DispatcherTimer
41 | {
42 | Interval = TimeSpan.FromSeconds(0.5),
43 | IsEnabled = true
44 | };
45 |
46 | _blinkTimer.Tick += BlinkTimerOnTick;
47 | }
48 |
49 | ///
50 | protected override void OnUnloaded(RoutedEventArgs e)
51 | {
52 | base.OnUnloaded(e);
53 |
54 | _blinkTimer.IsEnabled = false;
55 | _blinkTimer.Tick -= BlinkTimerOnTick;
56 | }
57 |
58 | ///
59 | public override LayerRenderMoments UpdateMoments => LayerRenderMoments.NoResizeRearrange;
60 |
61 | ///
62 | /// Gets the caret to render.
63 | ///
64 | public Caret Caret { get; }
65 |
66 | ///
67 | /// Gets or sets a value indicating whether the caret is visible.
68 | ///
69 | public bool CaretVisible
70 | {
71 | get => _caretVisible;
72 | set
73 | {
74 | if (_caretVisible != value)
75 | {
76 | _caretVisible = value;
77 | InvalidateVisual();
78 | }
79 | }
80 | }
81 |
82 | ///
83 | /// Defines the property.
84 | ///
85 | public static readonly DirectProperty BlinkingIntervalProperty =
86 | AvaloniaProperty.RegisterDirect(nameof(BlinkingInterval),
87 | x => x.BlinkingInterval,
88 | (x, v) => x.BlinkingInterval = v,
89 | unsetValue: TimeSpan.FromMilliseconds(500));
90 |
91 | ///
92 | /// Gets or sets the animation interval of the cursor blinker.
93 | ///
94 | public TimeSpan BlinkingInterval
95 | {
96 | get => _blinkTimer.Interval;
97 | set => _blinkTimer.Interval = value;
98 | }
99 |
100 | ///
101 | /// Defines the property.
102 | ///
103 | public static readonly StyledProperty InsertCaretWidthProperty =
104 | AvaloniaProperty.Register(nameof(InsertCaretWidth), 1D);
105 |
106 | ///
107 | /// Gets or sets the width of the caret when it is in insertion mode.
108 | ///
109 | public double InsertCaretWidth
110 | {
111 | get => GetValue(InsertCaretWidthProperty);
112 | set => SetValue(InsertCaretWidthProperty, value);
113 | }
114 |
115 | ///
116 | /// Gets or sets a value indicating whether the cursor of the caret is blinking.
117 | ///
118 | public bool IsBlinking
119 | {
120 | get => _blinkTimer.IsEnabled;
121 | set
122 | {
123 | if (_blinkTimer.IsEnabled != value)
124 | {
125 | _blinkTimer.IsEnabled = value;
126 | CaretVisible = true;
127 | }
128 | }
129 | }
130 |
131 | ///
132 | /// Defines the property.
133 | ///
134 | public static readonly StyledProperty PrimaryColumnBorderProperty =
135 | AvaloniaProperty.Register(nameof(PrimaryColumnBorder), new Pen(Brushes.Magenta));
136 |
137 | ///
138 | /// Gets or sets the pen used to draw the border of the cursor in the primary column.
139 | ///
140 | public IPen? PrimaryColumnBorder
141 | {
142 | get => GetValue(PrimaryColumnBorderProperty);
143 | set => SetValue(PrimaryColumnBorderProperty, value);
144 | }
145 |
146 | ///
147 | /// Defines the property.
148 | ///
149 | public static readonly StyledProperty PrimaryColumnBackgroundProperty =
150 | AvaloniaProperty.Register(
151 | nameof(PrimaryColumnBackground),
152 | new SolidColorBrush(Colors.Magenta, 0.3D)
153 | );
154 |
155 | ///
156 | /// Gets or sets the brush used to draw the background of the cursor in the primary column.
157 | ///
158 | public IBrush? PrimaryColumnBackground
159 | {
160 | get => GetValue(PrimaryColumnBackgroundProperty);
161 | set => SetValue(PrimaryColumnBackgroundProperty, value);
162 | }
163 |
164 | ///
165 | /// Defines the property.
166 | ///
167 | public static readonly StyledProperty SecondaryColumnBorderProperty =
168 | AvaloniaProperty.Register(nameof(SecondaryColumnBorder), new Pen(Brushes.DarkMagenta));
169 |
170 | ///
171 | /// Gets or sets the pen used to draw the border of the cursor in the secondary columns.
172 | ///
173 | public IPen? SecondaryColumnBorder
174 | {
175 | get => GetValue(SecondaryColumnBorderProperty);
176 | set => SetValue(SecondaryColumnBorderProperty, value);
177 | }
178 |
179 | ///
180 | /// Defines the property.
181 | ///
182 | public static readonly StyledProperty SecondaryColumnBackgroundProperty =
183 | AvaloniaProperty.Register(
184 | nameof(SecondaryColumnBackground),
185 | new SolidColorBrush(Colors.DarkMagenta, 0.5D)
186 | );
187 |
188 | ///
189 | /// Gets or sets the brush used to draw the background of the cursor in the secondary columns.
190 | ///
191 | public IBrush? SecondaryColumnBackground
192 | {
193 | get => GetValue(SecondaryColumnBackgroundProperty);
194 | set => SetValue(SecondaryColumnBackgroundProperty, value);
195 | }
196 |
197 | private void BlinkTimerOnTick(object? sender, EventArgs e)
198 | {
199 | CaretVisible = !CaretVisible;
200 | InvalidateVisual();
201 | }
202 |
203 | private void CaretOnChanged(object? sender, EventArgs e)
204 | {
205 | CaretVisible = true;
206 | InvalidateVisual();
207 | }
208 |
209 | ///
210 | public override void Render(DrawingContext context)
211 | {
212 | base.Render(context);
213 |
214 | if (HexView is null || !HexView.IsFocused)
215 | return;
216 |
217 | var line = HexView.GetVisualLineByLocation(Caret.Location);
218 | if (line is null)
219 | return;
220 |
221 | for (int i = 0; i < HexView.Columns.Count; i++)
222 | {
223 | var column = HexView.Columns[i];
224 | if (column is not CellBasedColumn { IsVisible: true } cellBasedColumn)
225 | continue;
226 |
227 | var bounds = cellBasedColumn.GetCellBounds(line, Caret.Location);
228 | if (Caret.Mode == EditingMode.Insert)
229 | bounds = new Rect(bounds.Left, bounds.Top, InsertCaretWidth, bounds.Height);
230 |
231 | if (i == Caret.PrimaryColumnIndex)
232 | {
233 | if (CaretVisible)
234 | context.DrawRectangle(PrimaryColumnBackground, PrimaryColumnBorder, bounds);
235 | }
236 | else
237 | {
238 | context.DrawRectangle(SecondaryColumnBackground, SecondaryColumnBorder, bounds);
239 | }
240 | }
241 | }
242 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Editing/Caret.cs:
--------------------------------------------------------------------------------
1 | using AvaloniaHex.Document;
2 | using AvaloniaHex.Rendering;
3 |
4 | namespace AvaloniaHex.Editing;
5 |
6 | ///
7 | /// Represents a caret in a hex editor.
8 | ///
9 | public sealed class Caret
10 | {
11 | ///
12 | /// Fires when the location of the caret has changed.
13 | ///
14 | public event EventHandler? LocationChanged;
15 |
16 | ///
17 | /// Fires when the caret's editing mode has changed.
18 | ///
19 | public event EventHandler? ModeChanged;
20 |
21 | ///
22 | /// Fires when the primary column of the caret has changed.
23 | ///
24 | public event EventHandler? PrimaryColumnChanged;
25 |
26 | private BitLocation _location;
27 | private int _primaryColumnIndex = 1;
28 | private EditingMode _mode;
29 |
30 | internal Caret(HexView view)
31 | {
32 | HexView = view;
33 | }
34 |
35 | ///
36 | /// Gets the hex view the caret is rendered on.
37 | ///
38 | public HexView HexView { get; }
39 |
40 | ///
41 | /// Gets or sets the editing.
42 | ///
43 | public EditingMode Mode
44 | {
45 | get => _mode;
46 | set
47 | {
48 | if (_mode != value)
49 | {
50 | _mode = value;
51 |
52 | // Force reclamp of caret location.
53 | Location = _location;
54 |
55 | OnModeChanged();
56 | }
57 | }
58 | }
59 |
60 | ///
61 | /// Gets or sets the current location of the caret.
62 | ///
63 | public BitLocation Location
64 | {
65 | get => _location;
66 | set
67 | {
68 | var primaryColumn = PrimaryColumn;
69 |
70 | if (primaryColumn is null || HexView.Document is not {ValidRanges.EnclosingRange: var enclosingRange})
71 | {
72 | // We have no column or document to select bytes in...
73 | value = default;
74 | }
75 | else if (!enclosingRange.Contains(value))
76 | {
77 | // Edge-case, we may not be in the enclosing document range
78 | // (e.g., virtual cell at the end of the document or trying to move before first valid range).
79 | value = value < enclosingRange.Start
80 | ? new BitLocation(enclosingRange.Start.ByteIndex, primaryColumn.FirstBitIndex)
81 | : new BitLocation(enclosingRange.End.ByteIndex, primaryColumn.FirstBitIndex);
82 | }
83 | else
84 | {
85 | // Otherwise, always make sure we are at a valid cell in the current column.
86 | value = primaryColumn.AlignToCell(value);
87 | }
88 |
89 | if (_location != value)
90 | {
91 | _location = value;
92 | OnLocationChanged();
93 | }
94 | }
95 | }
96 |
97 | ///
98 | /// Gets or sets the index of the primary column the caret is active in.
99 | ///
100 | public int PrimaryColumnIndex
101 | {
102 | get => _primaryColumnIndex;
103 | set
104 | {
105 | if (_primaryColumnIndex != value)
106 | {
107 | _primaryColumnIndex = value;
108 |
109 | // Force reclamp of caret location.
110 | Location = _location;
111 |
112 | OnPrimaryColumnChanged();
113 | }
114 | }
115 | }
116 |
117 | ///
118 | /// Gets the primary column the caret is active in..
119 | ///
120 | public CellBasedColumn? PrimaryColumn => HexView.Columns[PrimaryColumnIndex] as CellBasedColumn;
121 |
122 | private void OnLocationChanged()
123 | {
124 | HexView.BringIntoView(Location);
125 | LocationChanged?.Invoke(this, EventArgs.Empty);
126 | }
127 |
128 | private void OnModeChanged()
129 | {
130 | ModeChanged?.Invoke(this, EventArgs.Empty);
131 | }
132 |
133 | private void OnPrimaryColumnChanged()
134 | {
135 | PrimaryColumnChanged?.Invoke(this, EventArgs.Empty);
136 | }
137 |
138 | ///
139 | /// Moves the caret to the beginning of the document.
140 | ///
141 | public void GoToStartOfDocument()
142 | {
143 | if (PrimaryColumn is not { } primaryColumn)
144 | return;
145 |
146 | Location = primaryColumn.GetFirstLocation();
147 | }
148 |
149 | ///
150 | /// Moves the caret to the end of the document.
151 | ///
152 | public void GoToEndOfDocument()
153 | {
154 | if (PrimaryColumn is not { } primaryColumn)
155 | return;
156 |
157 | Location = primaryColumn.GetLastLocation(true);
158 | }
159 |
160 | ///
161 | /// Moves the caret to the beginning of the current line in the hex editor.
162 | ///
163 | public void GoToStartOfLine()
164 | {
165 | if (PrimaryColumn is not { } primaryColumn)
166 | return;
167 |
168 | if (HexView is not { Document.ValidRanges.EnclosingRange: var enclosingRange })
169 | return;
170 |
171 | ulong bytesPerLine = (ulong) HexView.ActualBytesPerLine;
172 | ulong lineIndex = (Location.ByteIndex - enclosingRange.Start.ByteIndex) / bytesPerLine;
173 |
174 | ulong byteIndex = enclosingRange.Start.ByteIndex + lineIndex * bytesPerLine;
175 | int bitIndex = primaryColumn.FirstBitIndex;
176 |
177 | Location = new BitLocation(byteIndex, bitIndex);
178 | }
179 |
180 | ///
181 | /// Moves the caret to the end of the current line in the hex editor.
182 | ///
183 | public void GoToEndOfLine()
184 | {
185 | if (HexView is not { Document.ValidRanges.EnclosingRange: var enclosingRange })
186 | return;
187 |
188 | ulong bytesPerLine = (ulong) HexView.ActualBytesPerLine;
189 | ulong lineIndex = (Location.ByteIndex - enclosingRange.Start.ByteIndex) / bytesPerLine;
190 |
191 | ulong byteIndex = Math.Min(
192 | enclosingRange.Start.ByteIndex + (lineIndex + 1) * bytesPerLine,
193 | enclosingRange.End.ByteIndex
194 | ) - 1;
195 |
196 | Location = new BitLocation(byteIndex, 0);
197 | }
198 |
199 | ///
200 | /// Moves the caret one cell to the left in the hex editor.
201 | ///
202 | public void GoLeft()
203 | {
204 | if (PrimaryColumn is { } column)
205 | Location = column.GetPreviousLocation(Location);
206 | }
207 |
208 | ///
209 | /// Moves the caret one cell up in the hex editor.
210 | ///
211 | public void GoUp() => GoBackward((ulong)HexView.ActualBytesPerLine);
212 |
213 | ///
214 | /// Moves the caret one page up in the hex editor.
215 | ///
216 | public void GoPageUp() => GoBackward((ulong)(HexView.ActualBytesPerLine * HexView.VisualLines.Count));
217 |
218 | ///
219 | /// Moves the caret the provided number of bytes backward in the hex editor.
220 | ///
221 | /// The number of bytes to move.
222 | public void GoBackward(ulong byteCount)
223 | {
224 | if (HexView is not { Document.ValidRanges.EnclosingRange: var enclosingRange } || PrimaryColumn is null)
225 | return;
226 |
227 | // Note: We cannot use BitLocation.Clamp due to unsigned overflow that may happen.
228 |
229 | Location = Location.ByteIndex - enclosingRange.Start.ByteIndex >= byteCount
230 | ? new BitLocation(Location.ByteIndex - byteCount, Location.BitIndex)
231 | : new BitLocation(enclosingRange.Start.ByteIndex, PrimaryColumn.FirstBitIndex);
232 | }
233 |
234 | ///
235 | /// Moves the caret one cell to the right in the hex editor.
236 | ///
237 | public void GoRight()
238 | {
239 | if (PrimaryColumn is { } column)
240 | Location = column.GetNextLocation(Location, true, true);
241 | }
242 |
243 | ///
244 | /// Moves the caret one cell down in the hex editor.
245 | ///
246 | public void GoDown() => GoForward((ulong)HexView.ActualBytesPerLine);
247 |
248 | ///
249 | /// Moves the caret one page down in the hex editor.
250 | ///
251 | public void GoPageDown() => GoForward((ulong)(HexView.ActualBytesPerLine * HexView.VisualLines.Count));
252 |
253 | ///
254 | /// Moves the caret the provided number of bytes forward in the hex editor.
255 | ///
256 | /// The number of bytes to move.
257 | public void GoForward(ulong byteCount)
258 | {
259 | if (HexView is not { Document.ValidRanges.EnclosingRange: var enclosingRange } || PrimaryColumn is null)
260 | return;
261 |
262 | // Note: We cannot use BitLocation.Clamp due to unsigned overflow that may happen.
263 |
264 | if (enclosingRange.End.ByteIndex < byteCount
265 | || Location.ByteIndex >= enclosingRange.End.ByteIndex - byteCount)
266 | {
267 | Location = new BitLocation(enclosingRange.End.ByteIndex, PrimaryColumn.FirstBitIndex);
268 | return;
269 | }
270 |
271 | Location = new BitLocation(Location.ByteIndex + byteCount, Location.BitIndex);
272 | }
273 |
274 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/HexColumn.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Media.TextFormatting;
3 | using AvaloniaHex.Document;
4 |
5 | namespace AvaloniaHex.Rendering;
6 |
7 | ///
8 | /// Represents a column that renders binary data using hexadecimal number encoding.
9 | ///
10 | public class HexColumn : CellBasedColumn
11 | {
12 | static HexColumn()
13 | {
14 | IsUppercaseProperty.Changed.AddClassHandler(OnIsUpperCaseChanged);
15 | UseDynamicHeaderProperty.Changed.AddClassHandler(OnUseDynamicHeaderChanged);
16 | CursorProperty.OverrideDefaultValue(IBeamCursor);
17 | HeaderProperty.OverrideDefaultValue("Hex");
18 | }
19 |
20 | ///
21 | /// Dependency property for
22 | ///
23 | public static readonly StyledProperty UseDynamicHeaderProperty =
24 | AvaloniaProperty.Register(nameof(UseDynamicHeader), true);
25 |
26 | ///
27 | /// Gets or sets a value indicating whether the header of this column should be dynamically
28 | ///
29 | public bool UseDynamicHeader
30 | {
31 | get => GetValue(IsHeaderVisibleProperty);
32 | set => SetValue(IsHeaderVisibleProperty, value);
33 | }
34 |
35 | ///
36 | public override Size MinimumSize => default;
37 |
38 | ///
39 | public override double GroupPadding => CellSize.Width;
40 |
41 | ///
42 | public override int BitsPerCell => 4;
43 |
44 | ///
45 | public override int CellsPerWord => 2;
46 |
47 | ///
48 | /// Defines the property.
49 | ///
50 | public static readonly StyledProperty IsUppercaseProperty =
51 | AvaloniaProperty.Register(nameof(IsUppercase), true);
52 |
53 | ///
54 | /// Gets or sets a value indicating whether the hexadecimal digits should be rendered in uppercase or not.
55 | ///
56 | public bool IsUppercase
57 | {
58 | get => GetValue(IsUppercaseProperty);
59 | set => SetValue(IsUppercaseProperty, value);
60 | }
61 |
62 | ///
63 | protected override string PrepareTextInput(string input) => input.Replace(" ", "");
64 |
65 | private static byte? ParseNibble(char c) => c switch
66 | {
67 | >= '0' and <= '9' => (byte?) (c - '0'),
68 | >= 'a' and <= 'f' => (byte?) (c - 'a' + 10),
69 | >= 'A' and <= 'F' => (byte?) (c - 'A' + 10),
70 | _ => null
71 | };
72 |
73 | ///
74 | protected override bool TryWriteCell(Span buffer, BitLocation bufferStart, BitLocation writeLocation, char input)
75 | {
76 | if (ParseNibble(input) is not { } nibble)
77 | return false;
78 |
79 | int relativeIndex = (int) (writeLocation.ByteIndex - bufferStart.ByteIndex);
80 | buffer[relativeIndex] = writeLocation.BitIndex == 4
81 | ? (byte) ((buffer[relativeIndex] & 0xF) | (nibble << 4))
82 | : (byte) ((buffer[relativeIndex] & 0xF0) | nibble);
83 | return true;
84 | }
85 |
86 | ///
87 | public override string? GetText(BitRange range)
88 | {
89 | if (HexView?.Document is null)
90 | return null;
91 |
92 | byte[] data = new byte[range.ByteLength];
93 | HexView.Document.ReadBytes(range.Start.ByteIndex, data);
94 |
95 | char[] output = new char[data.Length * 3 - 1];
96 | GetText(data, range, output);
97 |
98 | return new string(output);
99 | }
100 |
101 | ///
102 | public override TextLine? CreateHeaderLine()
103 | {
104 | if (!UseDynamicHeader)
105 | return base.CreateHeaderLine();
106 |
107 | if (HexView is null)
108 | return null;
109 |
110 | // Generate header text.
111 | int count = HexView.ActualBytesPerLine;
112 | char[] buffer = new char[count * 3 - 1];
113 | for (int i = 0; i < count; i++)
114 | {
115 | buffer[i * 3] = GetHexDigit((byte) ((i >> 4) & 0xF), IsUppercase);
116 | buffer[i * 3 + 1] = GetHexDigit((byte) (i & 0xF), IsUppercase);
117 | if (i < count - 1)
118 | buffer[i * 3 + 2] = ' ';
119 | }
120 |
121 | // Render.
122 | var properties = GetHeaderTextRunProperties();
123 | return TextFormatter.Current.FormatLine(
124 | new SimpleTextSource(new string(buffer), properties),
125 | 0,
126 | double.MaxValue,
127 | new GenericTextParagraphProperties(properties)
128 | );
129 | }
130 |
131 | ///
132 | public override TextLine? CreateTextLine(VisualBytesLine line)
133 | {
134 | if (HexView is null)
135 | return null;
136 |
137 | var properties = GetTextRunProperties();
138 | return TextFormatter.Current.FormatLine(
139 | new HexTextSource(this, line, properties),
140 | 0,
141 | double.MaxValue,
142 | new GenericTextParagraphProperties(properties)
143 | );
144 | }
145 |
146 | private static char GetHexDigit(byte nibble, bool uppercase) => nibble switch
147 | {
148 | < 10 => (char) (nibble + '0'),
149 | < 16 => (char) (nibble - 10 + (uppercase ? 'A' : 'a')),
150 | _ => throw new ArgumentOutOfRangeException(nameof(nibble))
151 | };
152 |
153 | private void GetText(ReadOnlySpan data, BitRange dataRange, Span buffer)
154 | {
155 | bool uppercase = IsUppercase;
156 | char invalidCellChar = InvalidCellChar;
157 |
158 | if (HexView?.Document?.ValidRanges is not { } valid)
159 | {
160 | buffer.Fill(invalidCellChar);
161 | return;
162 | }
163 |
164 | int index = 0;
165 | for (int i = 0; i < data.Length; i++)
166 | {
167 | if (i > 0)
168 | buffer[index++] = ' ';
169 |
170 | var location1 = new BitLocation(dataRange.Start.ByteIndex + (ulong) i, 0);
171 | var location2 = new BitLocation(dataRange.Start.ByteIndex + (ulong) i, 4);
172 | var location3 = new BitLocation(dataRange.Start.ByteIndex + (ulong) i + 1, 0);
173 |
174 | byte value = data[i];
175 |
176 | buffer[index] = valid.IsSuperSetOf(new BitRange(location2, location3))
177 | ? GetHexDigit((byte) ((value >> 4) & 0xF), uppercase)
178 | : invalidCellChar;
179 |
180 | buffer[index + 1] = valid.IsSuperSetOf(new BitRange(location1, location2))
181 | ? GetHexDigit((byte) (value & 0xF), uppercase)
182 | : invalidCellChar;
183 |
184 | index += 2;
185 | }
186 | }
187 |
188 | private static void OnIsUpperCaseChanged(HexColumn arg1, AvaloniaPropertyChangedEventArgs arg2)
189 | {
190 | if (arg1.HexView is null)
191 | return;
192 |
193 | arg1.HexView.InvalidateVisualLines();
194 | arg1.HexView.InvalidateHeaders();
195 | }
196 |
197 | private static void OnUseDynamicHeaderChanged(BinaryColumn arg1, AvaloniaPropertyChangedEventArgs arg2)
198 | {
199 | arg1.HexView?.InvalidateHeaders();
200 | }
201 |
202 | private sealed class HexTextSource : ITextSource
203 | {
204 | private readonly HexColumn _column;
205 | private readonly GenericTextRunProperties _properties;
206 | private readonly VisualBytesLine _line;
207 |
208 | public HexTextSource(HexColumn column, VisualBytesLine line, GenericTextRunProperties properties)
209 | {
210 | _column = column;
211 | _line = line;
212 | _properties = properties;
213 | }
214 |
215 | ///
216 | public TextRun? GetTextRun(int textSourceIndex)
217 | {
218 | // Calculate current byte location from text index.
219 | int byteIndex = Math.DivRem(textSourceIndex, 3, out int nibbleIndex);
220 | if (byteIndex < 0 || byteIndex >= _line.Data.Length)
221 | return null;
222 |
223 | // Special case nibble index 2 (space after byte).
224 | if (nibbleIndex == 2)
225 | {
226 | if (byteIndex >= _line.Data.Length - 1)
227 | return null;
228 |
229 | return new TextCharacters(" ", _properties);
230 | }
231 |
232 | // Find current segment we're in.
233 | var currentLocation = new BitLocation(_line.Range.Start.ByteIndex + (ulong) byteIndex, nibbleIndex * 4);
234 | var segment = _line.FindSegmentContaining(currentLocation);
235 | if (segment is null)
236 | return null;
237 |
238 | // Stringify the segment.
239 | var range = segment.Range;
240 | ReadOnlySpan data = _line.AsAbsoluteSpan(range);
241 | Span buffer = stackalloc char[(int) segment.Range.ByteLength * 3 - 1];
242 | _column.GetText(data, range, buffer);
243 |
244 | // Render
245 | return new TextCharacters(
246 | new string(buffer),
247 | _properties.WithBrushes(
248 | segment.ForegroundBrush ?? _properties.ForegroundBrush,
249 | segment.BackgroundBrush ?? _properties.BackgroundBrush
250 | )
251 | );
252 | }
253 | }
254 | }
--------------------------------------------------------------------------------
/examples/AvaloniaHex.Demo/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from https://github.com/github/gitignore/blob/master/VisualStudio.gitignore
5 |
6 | # User-specific files
7 | *.rsuser
8 | *.suo
9 | *.user
10 | *.userosscache
11 | *.sln.docstates
12 |
13 | # User-specific files (MonoDevelop/Xamarin Studio)
14 | *.userprefs
15 |
16 | # Mono auto generated files
17 | mono_crash.*
18 |
19 | # Build results
20 | [Dd]ebug/
21 | [Dd]ebugPublic/
22 | [Rr]elease/
23 | [Rr]eleases/
24 | x64/
25 | x86/
26 | [Ww][Ii][Nn]32/
27 | [Aa][Rr][Mm]/
28 | [Aa][Rr][Mm]64/
29 | bld/
30 | [Bb]in/
31 | [Oo]bj/
32 | [Ll]og/
33 | [Ll]ogs/
34 |
35 | # Visual Studio 2015/2017 cache/options directory
36 | .vs/
37 | # Uncomment if you have tasks that create the project's static files in wwwroot
38 | #wwwroot/
39 |
40 | # Visual Studio 2017 auto generated files
41 | Generated\ Files/
42 |
43 | # MSTest test Results
44 | [Tt]est[Rr]esult*/
45 | [Bb]uild[Ll]og.*
46 |
47 | # NUnit
48 | *.VisualState.xml
49 | TestResult.xml
50 | nunit-*.xml
51 |
52 | # Build Results of an ATL Project
53 | [Dd]ebugPS/
54 | [Rr]eleasePS/
55 | dlldata.c
56 |
57 | # Benchmark Results
58 | BenchmarkDotNet.Artifacts/
59 |
60 | # .NET Core
61 | project.lock.json
62 | project.fragment.lock.json
63 | artifacts/
64 |
65 | # Tye
66 | .tye/
67 |
68 | # ASP.NET Scaffolding
69 | ScaffoldingReadMe.txt
70 |
71 | # StyleCop
72 | StyleCopReport.xml
73 |
74 | # Files built by Visual Studio
75 | *_i.c
76 | *_p.c
77 | *_h.h
78 | *.ilk
79 | *.meta
80 | *.obj
81 | *.iobj
82 | *.pch
83 | *.pdb
84 | *.ipdb
85 | *.pgc
86 | *.pgd
87 | *.rsp
88 | *.sbr
89 | *.tlb
90 | *.tli
91 | *.tlh
92 | *.tmp
93 | *.tmp_proj
94 | *_wpftmp.csproj
95 | *.log
96 | *.vspscc
97 | *.vssscc
98 | .builds
99 | *.pidb
100 | *.svclog
101 | *.scc
102 |
103 | # Chutzpah Test files
104 | _Chutzpah*
105 |
106 | # Visual C++ cache files
107 | ipch/
108 | *.aps
109 | *.ncb
110 | *.opendb
111 | *.opensdf
112 | *.sdf
113 | *.cachefile
114 | *.VC.db
115 | *.VC.VC.opendb
116 |
117 | # Visual Studio profiler
118 | *.psess
119 | *.vsp
120 | *.vspx
121 | *.sap
122 |
123 | # Visual Studio Trace Files
124 | *.e2e
125 |
126 | # TFS 2012 Local Workspace
127 | $tf/
128 |
129 | # Guidance Automation Toolkit
130 | *.gpState
131 |
132 | # ReSharper is a .NET coding add-in
133 | _ReSharper*/
134 | *.[Rr]e[Ss]harper
135 | *.DotSettings.user
136 |
137 | # TeamCity is a build add-in
138 | _TeamCity*
139 |
140 | # DotCover is a Code Coverage Tool
141 | *.dotCover
142 |
143 | # AxoCover is a Code Coverage Tool
144 | .axoCover/*
145 | !.axoCover/settings.json
146 |
147 | # Coverlet is a free, cross platform Code Coverage Tool
148 | coverage*.json
149 | coverage*.xml
150 | coverage*.info
151 |
152 | # Visual Studio code coverage results
153 | *.coverage
154 | *.coveragexml
155 |
156 | # NCrunch
157 | _NCrunch_*
158 | .*crunch*.local.xml
159 | nCrunchTemp_*
160 |
161 | # MightyMoose
162 | *.mm.*
163 | AutoTest.Net/
164 |
165 | # Web workbench (sass)
166 | .sass-cache/
167 |
168 | # Installshield output folder
169 | [Ee]xpress/
170 |
171 | # DocProject is a documentation generator add-in
172 | DocProject/buildhelp/
173 | DocProject/Help/*.HxT
174 | DocProject/Help/*.HxC
175 | DocProject/Help/*.hhc
176 | DocProject/Help/*.hhk
177 | DocProject/Help/*.hhp
178 | DocProject/Help/Html2
179 | DocProject/Help/html
180 |
181 | # Click-Once directory
182 | publish/
183 |
184 | # Publish Web Output
185 | *.[Pp]ublish.xml
186 | *.azurePubxml
187 | # Note: Comment the next line if you want to checkin your web deploy settings,
188 | # but database connection strings (with potential passwords) will be unencrypted
189 | *.pubxml
190 | *.publishproj
191 |
192 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
193 | # checkin your Azure Web App publish settings, but sensitive information contained
194 | # in these scripts will be unencrypted
195 | PublishScripts/
196 |
197 | # NuGet Packages
198 | *.nupkg
199 | # NuGet Symbol Packages
200 | *.snupkg
201 | # The packages folder can be ignored because of Package Restore
202 | **/[Pp]ackages/*
203 | # except build/, which is used as an MSBuild target.
204 | !**/[Pp]ackages/build/
205 | # Uncomment if necessary however generally it will be regenerated when needed
206 | #!**/[Pp]ackages/repositories.config
207 | # NuGet v3's project.json files produces more ignorable files
208 | *.nuget.props
209 | *.nuget.targets
210 |
211 | # Microsoft Azure Build Output
212 | csx/
213 | *.build.csdef
214 |
215 | # Microsoft Azure Emulator
216 | ecf/
217 | rcf/
218 |
219 | # Windows Store app package directories and files
220 | AppPackages/
221 | BundleArtifacts/
222 | Package.StoreAssociation.xml
223 | _pkginfo.txt
224 | *.appx
225 | *.appxbundle
226 | *.appxupload
227 |
228 | # Visual Studio cache files
229 | # files ending in .cache can be ignored
230 | *.[Cc]ache
231 | # but keep track of directories ending in .cache
232 | !?*.[Cc]ache/
233 |
234 | # Others
235 | ClientBin/
236 | ~$*
237 | *~
238 | *.dbmdl
239 | *.dbproj.schemaview
240 | *.jfm
241 | *.pfx
242 | *.publishsettings
243 | orleans.codegen.cs
244 |
245 | # Including strong name files can present a security risk
246 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
247 | #*.snk
248 |
249 | # Since there are multiple workflows, uncomment next line to ignore bower_components
250 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
251 | #bower_components/
252 |
253 | # RIA/Silverlight projects
254 | Generated_Code/
255 |
256 | # Backup & report files from converting an old project file
257 | # to a newer Visual Studio version. Backup files are not needed,
258 | # because we have git ;-)
259 | _UpgradeReport_Files/
260 | Backup*/
261 | UpgradeLog*.XML
262 | UpgradeLog*.htm
263 | ServiceFabricBackup/
264 | *.rptproj.bak
265 |
266 | # SQL Server files
267 | *.mdf
268 | *.ldf
269 | *.ndf
270 |
271 | # Business Intelligence projects
272 | *.rdl.data
273 | *.bim.layout
274 | *.bim_*.settings
275 | *.rptproj.rsuser
276 | *- [Bb]ackup.rdl
277 | *- [Bb]ackup ([0-9]).rdl
278 | *- [Bb]ackup ([0-9][0-9]).rdl
279 |
280 | # Microsoft Fakes
281 | FakesAssemblies/
282 |
283 | # GhostDoc plugin setting file
284 | *.GhostDoc.xml
285 |
286 | # Node.js Tools for Visual Studio
287 | .ntvs_analysis.dat
288 | node_modules/
289 |
290 | # Visual Studio 6 build log
291 | *.plg
292 |
293 | # Visual Studio 6 workspace options file
294 | *.opt
295 |
296 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
297 | *.vbw
298 |
299 | # Visual Studio LightSwitch build output
300 | **/*.HTMLClient/GeneratedArtifacts
301 | **/*.DesktopClient/GeneratedArtifacts
302 | **/*.DesktopClient/ModelManifest.xml
303 | **/*.Server/GeneratedArtifacts
304 | **/*.Server/ModelManifest.xml
305 | _Pvt_Extensions
306 |
307 | # Paket dependency manager
308 | .paket/paket.exe
309 | paket-files/
310 |
311 | # FAKE - F# Make
312 | .fake/
313 |
314 | # CodeRush personal settings
315 | .cr/personal
316 |
317 | # Python Tools for Visual Studio (PTVS)
318 | __pycache__/
319 | *.pyc
320 |
321 | # Cake - Uncomment if you are using it
322 | # tools/**
323 | # !tools/packages.config
324 |
325 | # Tabs Studio
326 | *.tss
327 |
328 | # Telerik's JustMock configuration file
329 | *.jmconfig
330 |
331 | # BizTalk build output
332 | *.btp.cs
333 | *.btm.cs
334 | *.odx.cs
335 | *.xsd.cs
336 |
337 | # OpenCover UI analysis results
338 | OpenCover/
339 |
340 | # Azure Stream Analytics local run output
341 | ASALocalRun/
342 |
343 | # MSBuild Binary and Structured Log
344 | *.binlog
345 |
346 | # NVidia Nsight GPU debugger configuration file
347 | *.nvuser
348 |
349 | # MFractors (Xamarin productivity tool) working folder
350 | .mfractor/
351 |
352 | # Local History for Visual Studio
353 | .localhistory/
354 |
355 | # BeatPulse healthcheck temp database
356 | healthchecksdb
357 |
358 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
359 | MigrationBackup/
360 |
361 | # Ionide (cross platform F# VS Code tools) working folder
362 | .ionide/
363 |
364 | # Fody - auto-generated XML schema
365 | FodyWeavers.xsd
366 |
367 | ##
368 | ## Visual studio for Mac
369 | ##
370 |
371 |
372 | # globs
373 | Makefile.in
374 | *.userprefs
375 | *.usertasks
376 | config.make
377 | config.status
378 | aclocal.m4
379 | install-sh
380 | autom4te.cache/
381 | *.tar.gz
382 | tarballs/
383 | test-results/
384 |
385 | # Mac bundle stuff
386 | *.dmg
387 | *.app
388 |
389 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
390 | # General
391 | .DS_Store
392 | .AppleDouble
393 | .LSOverride
394 |
395 | # Icon must end with two \r
396 | Icon
397 |
398 |
399 | # Thumbnails
400 | ._*
401 |
402 | # Files that might appear in the root of a volume
403 | .DocumentRevisions-V100
404 | .fseventsd
405 | .Spotlight-V100
406 | .TemporaryItems
407 | .Trashes
408 | .VolumeIcon.icns
409 | .com.apple.timemachine.donotpresent
410 |
411 | # Directories potentially created on remote AFP share
412 | .AppleDB
413 | .AppleDesktop
414 | Network Trash Folder
415 | Temporary Items
416 | .apdisk
417 |
418 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
419 | # Windows thumbnail cache files
420 | Thumbs.db
421 | ehthumbs.db
422 | ehthumbs_vista.db
423 |
424 | # Dump file
425 | *.stackdump
426 |
427 | # Folder config file
428 | [Dd]esktop.ini
429 |
430 | # Recycle Bin used on file shares
431 | $RECYCLE.BIN/
432 |
433 | # Windows Installer files
434 | *.cab
435 | *.msi
436 | *.msix
437 | *.msm
438 | *.msp
439 |
440 | # Windows shortcuts
441 | *.lnk
442 |
443 | # JetBrains Rider
444 | .idea/
445 | *.sln.iml
446 |
447 | ##
448 | ## Visual Studio Code
449 | ##
450 | .vscode/*
451 | !.vscode/settings.json
452 | !.vscode/tasks.json
453 | !.vscode/launch.json
454 | !.vscode/extensions.json
455 |
--------------------------------------------------------------------------------
/src/AvaloniaHex/Document/BitRangeUnion.cs:
--------------------------------------------------------------------------------
1 | using System.Collections;
2 | using System.Collections.ObjectModel;
3 | using System.Collections.Specialized;
4 | using System.Diagnostics;
5 |
6 | namespace AvaloniaHex.Document;
7 |
8 | ///
9 | /// Represents a disjoint union of binary ranges.
10 | ///
11 | [DebuggerDisplay("Count = {Count}")]
12 | public class BitRangeUnion : IReadOnlyBitRangeUnion, ICollection
13 | {
14 | ///
15 | public event NotifyCollectionChangedEventHandler? CollectionChanged;
16 |
17 | private readonly ObservableCollection _ranges = new();
18 |
19 | ///
20 | /// Creates a new empty union.
21 | ///
22 | public BitRangeUnion()
23 | {
24 | _ranges.CollectionChanged += (sender, args) => CollectionChanged?.Invoke(this, args);
25 | }
26 |
27 | ///
28 | /// Initializes a new union of bit ranges.
29 | ///
30 | /// The ranges to unify.
31 | public BitRangeUnion(IEnumerable ranges)
32 | : this()
33 | {
34 | foreach (var range in ranges)
35 | Add(range);
36 | }
37 |
38 | ///
39 | public BitRange EnclosingRange => _ranges.Count == 0 ? BitRange.Empty : new(_ranges[0].Start, _ranges[^1].End);
40 |
41 | ///
42 | public bool IsFragmented => _ranges.Count > 1;
43 |
44 | ///
45 | public int Count => _ranges.Count;
46 |
47 | ///
48 | public bool IsReadOnly => false;
49 |
50 | private (SearchResult Result, int Index) FindFirstOverlappingRange(BitRange range)
51 | {
52 | // TODO: binary search
53 |
54 | range = new BitRange(range.Start, range.End.NextOrMax());
55 | for (int i = 0; i < _ranges.Count; i++)
56 | {
57 | var candidate = _ranges[i];
58 | if (candidate.ExtendTo(candidate.End.NextOrMax()).OverlapsWith(range))
59 | {
60 | if (candidate.Start >= range.Start)
61 | return (SearchResult.PresentAfterIndex, i);
62 | return (SearchResult.PresentBeforeIndex, i);
63 | }
64 |
65 | if (candidate.Start > range.End)
66 | {
67 | return (SearchResult.NotPresentAtIndex, i);
68 | }
69 | }
70 |
71 | return (SearchResult.NotPresentAtIndex, _ranges.Count);
72 | }
73 |
74 | private void MergeRanges(int startIndex)
75 | {
76 | for (int i = startIndex; i < _ranges.Count - 1; i++)
77 | {
78 | if (!_ranges[i].ExtendTo(_ranges[i].End.Next()).OverlapsWith(_ranges[i + 1]))
79 | return;
80 |
81 | _ranges[i] = _ranges[i]
82 | .ExtendTo(_ranges[i + 1].Start)
83 | .ExtendTo(_ranges[i + 1].End);
84 |
85 | _ranges.RemoveAt(i + 1);
86 | i--;
87 | }
88 | }
89 |
90 | ///
91 | public void Add(BitRange item)
92 | {
93 | (var result, int index) = FindFirstOverlappingRange(item);
94 |
95 | switch (result)
96 | {
97 | case SearchResult.PresentBeforeIndex:
98 | _ranges.Insert(index + 1, item);
99 | break;
100 |
101 | case SearchResult.PresentAfterIndex:
102 | case SearchResult.NotPresentAtIndex:
103 | _ranges.Insert(index, item);
104 | break;
105 |
106 | default:
107 | throw new ArgumentOutOfRangeException();
108 | }
109 |
110 | MergeRanges(index);
111 | }
112 |
113 | ///
114 | public void Clear() => _ranges.Clear();
115 |
116 | ///
117 | public bool Contains(BitRange item) => _ranges.Contains(item);
118 |
119 | ///
120 | public bool Contains(BitLocation location) => IsSuperSetOf(new BitRange(location, location.NextOrMax()));
121 |
122 | ///
123 | public bool IsSuperSetOf(BitRange range)
124 | {
125 | (var result, int index) = FindFirstOverlappingRange(range);
126 | if (result == SearchResult.NotPresentAtIndex)
127 | return false;
128 |
129 | return _ranges[index].Contains(range);
130 | }
131 |
132 | ///
133 | public bool IntersectsWith(BitRange range)
134 | {
135 | (var result, int index) = FindFirstOverlappingRange(range);
136 | if (result == SearchResult.NotPresentAtIndex)
137 | return false;
138 |
139 | return _ranges[index].OverlapsWith(range);
140 | }
141 |
142 | ///
143 | public int GetOverlappingRanges(BitRange range, Span output)
144 | {
145 | (var result, int index) = FindFirstOverlappingRange(range);
146 | if (result == SearchResult.NotPresentAtIndex)
147 | return 0;
148 |
149 | int count = 0;
150 | for (int i = index; i < _ranges.Count && count < output.Length; i++)
151 | {
152 | var current = _ranges[i];
153 | if (current.Start >= range.End)
154 | break;
155 |
156 | if (current.OverlapsWith(range))
157 | output[count++] = current;
158 | }
159 |
160 | return count;
161 | }
162 |
163 | ///
164 | public int GetIntersectingRanges(BitRange range, Span output)
165 | {
166 | // Get overlapping ranges.
167 | int count = GetOverlappingRanges(range, output);
168 |
169 | // Cut off first and last ranges.
170 | if (count > 0)
171 | {
172 | output[0] = output[0].Clamp(range);
173 | if (count > 1)
174 | output[count - 1] = output[count - 1].Clamp(range);
175 | }
176 |
177 | return count;
178 | }
179 |
180 | ///
181 | public void CopyTo(BitRange[] array, int arrayIndex) => _ranges.CopyTo(array, arrayIndex);
182 |
183 | ///
184 | public bool Remove(BitRange item)
185 | {
186 | (var result, int index) = FindFirstOverlappingRange(item);
187 |
188 | if (result == SearchResult.NotPresentAtIndex)
189 | return false;
190 |
191 | for (int i = index; i < _ranges.Count; i++)
192 | {
193 | // Is this an overlapping range?
194 | if (!_ranges[i].OverlapsWith(item))
195 | break;
196 |
197 | if (_ranges[i].Contains(new BitRange(item.Start, item.End.NextOrMax())))
198 | {
199 | // The range contains the entire range-to-remove, split up the range.
200 | var (a, rest) = _ranges[i].Split(item.Start);
201 | var (b, c) = rest.Split(item.End);
202 |
203 | if (a.IsEmpty)
204 | _ranges.RemoveAt(i--);
205 | else
206 | _ranges[i] = a;
207 |
208 | if (!c.IsEmpty)
209 | _ranges.Insert(i + 1, c);
210 | break;
211 | }
212 |
213 | if (item.Contains(_ranges[i]))
214 | {
215 | // The range-to-remove contains the entire current range.
216 | _ranges.RemoveAt(i--);
217 | }
218 | else if (item.Start < _ranges[i].Start)
219 | {
220 | // We are truncating the current range from the left.
221 | _ranges[i] = _ranges[i].Clamp(new BitRange(item.End, BitLocation.Maximum));
222 | }
223 | else if (item.End >= _ranges[i].End)
224 | {
225 | // We are truncating the current range from the right.
226 | _ranges[i] = _ranges[i].Clamp(new BitRange(BitLocation.Minimum, item.Start));
227 | }
228 | }
229 |
230 | return true;
231 | }
232 |
233 | ///
234 | public Enumerator GetEnumerator() => new(this);
235 |
236 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
237 |
238 | IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
239 |
240 | ///
241 | /// Wraps the union into a .
242 | ///
243 | /// The resulting read-only union.
244 | public ReadOnlyBitRangeUnion AsReadOnly() => new(this);
245 |
246 | private enum SearchResult
247 | {
248 | PresentBeforeIndex,
249 | PresentAfterIndex,
250 | NotPresentAtIndex,
251 | }
252 |
253 | ///
254 | /// An implementation of an enumerator that enumerates all disjoint ranges within a bit range union.
255 | ///
256 | public struct Enumerator : IEnumerator
257 | {
258 | private readonly BitRangeUnion _union;
259 | private int _index;
260 |
261 | ///
262 | /// Creates a new disjoint bit range union enumerator.
263 | ///
264 | /// The disjoint union to enumerate.
265 | public Enumerator(BitRangeUnion union) : this()
266 | {
267 | _union = union;
268 | _index = -1;
269 | }
270 |
271 | ///
272 | public BitRange Current => _index < _union._ranges.Count
273 | ? _union._ranges[_index]
274 | : default;
275 |
276 | ///
277 | object IEnumerator.Current => Current;
278 |
279 | ///
280 | public bool MoveNext()
281 | {
282 | _index++;
283 | return _index < _union._ranges.Count;
284 | }
285 |
286 | ///
287 | void IEnumerator.Reset()
288 | {
289 | }
290 |
291 | ///
292 | public void Dispose()
293 | {
294 | }
295 | }
296 | }
--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
1 | ## Ignore Visual Studio temporary files, build results, and
2 | ## files generated by popular Visual Studio add-ons.
3 | ##
4 | ## Get latest from `dotnet new gitignore`
5 |
6 | # dotenv files
7 | .env
8 |
9 | # User-specific files
10 | *.rsuser
11 | *.suo
12 | *.user
13 | *.userosscache
14 | *.sln.docstates
15 |
16 | # User-specific files (MonoDevelop/Xamarin Studio)
17 | *.userprefs
18 |
19 | # Mono auto generated files
20 | mono_crash.*
21 |
22 | # Build results
23 | [Dd]ebug/
24 | [Dd]ebugPublic/
25 | [Rr]elease/
26 | [Rr]eleases/
27 | x64/
28 | x86/
29 | [Ww][Ii][Nn]32/
30 | [Aa][Rr][Mm]/
31 | [Aa][Rr][Mm]64/
32 | bld/
33 | [Bb]in/
34 | [Oo]bj/
35 | [Ll]og/
36 | [Ll]ogs/
37 |
38 | # Visual Studio 2015/2017 cache/options directory
39 | .vs/
40 | # Uncomment if you have tasks that create the project's static files in wwwroot
41 | #wwwroot/
42 |
43 | # Visual Studio 2017 auto generated files
44 | Generated\ Files/
45 |
46 | # MSTest test Results
47 | [Tt]est[Rr]esult*/
48 | [Bb]uild[Ll]og.*
49 |
50 | # NUnit
51 | *.VisualState.xml
52 | TestResult.xml
53 | nunit-*.xml
54 |
55 | # Build Results of an ATL Project
56 | [Dd]ebugPS/
57 | [Rr]eleasePS/
58 | dlldata.c
59 |
60 | # Benchmark Results
61 | BenchmarkDotNet.Artifacts/
62 |
63 | # .NET
64 | project.lock.json
65 | project.fragment.lock.json
66 | artifacts/
67 |
68 | # Tye
69 | .tye/
70 |
71 | # ASP.NET Scaffolding
72 | ScaffoldingReadMe.txt
73 |
74 | # StyleCop
75 | StyleCopReport.xml
76 |
77 | # Files built by Visual Studio
78 | *_i.c
79 | *_p.c
80 | *_h.h
81 | *.ilk
82 | *.meta
83 | *.obj
84 | *.iobj
85 | *.pch
86 | *.pdb
87 | *.ipdb
88 | *.pgc
89 | *.pgd
90 | *.rsp
91 | *.sbr
92 | *.tlb
93 | *.tli
94 | *.tlh
95 | *.tmp
96 | *.tmp_proj
97 | *_wpftmp.csproj
98 | *.log
99 | *.tlog
100 | *.vspscc
101 | *.vssscc
102 | .builds
103 | *.pidb
104 | *.svclog
105 | *.scc
106 |
107 | # Chutzpah Test files
108 | _Chutzpah*
109 |
110 | # Visual C++ cache files
111 | ipch/
112 | *.aps
113 | *.ncb
114 | *.opendb
115 | *.opensdf
116 | *.sdf
117 | *.cachefile
118 | *.VC.db
119 | *.VC.VC.opendb
120 |
121 | # Visual Studio profiler
122 | *.psess
123 | *.vsp
124 | *.vspx
125 | *.sap
126 |
127 | # Visual Studio Trace Files
128 | *.e2e
129 |
130 | # TFS 2012 Local Workspace
131 | $tf/
132 |
133 | # Guidance Automation Toolkit
134 | *.gpState
135 |
136 | # ReSharper is a .NET coding add-in
137 | _ReSharper*/
138 | *.[Rr]e[Ss]harper
139 | *.DotSettings.user
140 |
141 | # TeamCity is a build add-in
142 | _TeamCity*
143 |
144 | # DotCover is a Code Coverage Tool
145 | *.dotCover
146 |
147 | # AxoCover is a Code Coverage Tool
148 | .axoCover/*
149 | !.axoCover/settings.json
150 |
151 | # Coverlet is a free, cross platform Code Coverage Tool
152 | coverage*.json
153 | coverage*.xml
154 | coverage*.info
155 |
156 | # Visual Studio code coverage results
157 | *.coverage
158 | *.coveragexml
159 |
160 | # NCrunch
161 | _NCrunch_*
162 | .*crunch*.local.xml
163 | nCrunchTemp_*
164 |
165 | # MightyMoose
166 | *.mm.*
167 | AutoTest.Net/
168 |
169 | # Web workbench (sass)
170 | .sass-cache/
171 |
172 | # Installshield output folder
173 | [Ee]xpress/
174 |
175 | # DocProject is a documentation generator add-in
176 | DocProject/buildhelp/
177 | DocProject/Help/*.HxT
178 | DocProject/Help/*.HxC
179 | DocProject/Help/*.hhc
180 | DocProject/Help/*.hhk
181 | DocProject/Help/*.hhp
182 | DocProject/Help/Html2
183 | DocProject/Help/html
184 |
185 | # Click-Once directory
186 | publish/
187 |
188 | # Publish Web Output
189 | *.[Pp]ublish.xml
190 | *.azurePubxml
191 | # Note: Comment the next line if you want to checkin your web deploy settings,
192 | # but database connection strings (with potential passwords) will be unencrypted
193 | *.pubxml
194 | *.publishproj
195 |
196 | # Microsoft Azure Web App publish settings. Comment the next line if you want to
197 | # checkin your Azure Web App publish settings, but sensitive information contained
198 | # in these scripts will be unencrypted
199 | PublishScripts/
200 |
201 | # NuGet Packages
202 | *.nupkg
203 | # NuGet Symbol Packages
204 | *.snupkg
205 | # The packages folder can be ignored because of Package Restore
206 | **/[Pp]ackages/*
207 | # except build/, which is used as an MSBuild target.
208 | !**/[Pp]ackages/build/
209 | # Uncomment if necessary however generally it will be regenerated when needed
210 | #!**/[Pp]ackages/repositories.config
211 | # NuGet v3's project.json files produces more ignorable files
212 | *.nuget.props
213 | *.nuget.targets
214 |
215 | # Microsoft Azure Build Output
216 | csx/
217 | *.build.csdef
218 |
219 | # Microsoft Azure Emulator
220 | ecf/
221 | rcf/
222 |
223 | # Windows Store app package directories and files
224 | AppPackages/
225 | BundleArtifacts/
226 | Package.StoreAssociation.xml
227 | _pkginfo.txt
228 | *.appx
229 | *.appxbundle
230 | *.appxupload
231 |
232 | # Visual Studio cache files
233 | # files ending in .cache can be ignored
234 | *.[Cc]ache
235 | # but keep track of directories ending in .cache
236 | !?*.[Cc]ache/
237 |
238 | # Others
239 | ClientBin/
240 | ~$*
241 | *~
242 | *.dbmdl
243 | *.dbproj.schemaview
244 | *.jfm
245 | *.pfx
246 | *.publishsettings
247 | orleans.codegen.cs
248 |
249 | # Including strong name files can present a security risk
250 | # (https://github.com/github/gitignore/pull/2483#issue-259490424)
251 | #*.snk
252 |
253 | # Since there are multiple workflows, uncomment next line to ignore bower_components
254 | # (https://github.com/github/gitignore/pull/1529#issuecomment-104372622)
255 | #bower_components/
256 |
257 | # RIA/Silverlight projects
258 | Generated_Code/
259 |
260 | # Backup & report files from converting an old project file
261 | # to a newer Visual Studio version. Backup files are not needed,
262 | # because we have git ;-)
263 | _UpgradeReport_Files/
264 | Backup*/
265 | UpgradeLog*.XML
266 | UpgradeLog*.htm
267 | ServiceFabricBackup/
268 | *.rptproj.bak
269 |
270 | # SQL Server files
271 | *.mdf
272 | *.ldf
273 | *.ndf
274 |
275 | # Business Intelligence projects
276 | *.rdl.data
277 | *.bim.layout
278 | *.bim_*.settings
279 | *.rptproj.rsuser
280 | *- [Bb]ackup.rdl
281 | *- [Bb]ackup ([0-9]).rdl
282 | *- [Bb]ackup ([0-9][0-9]).rdl
283 |
284 | # Microsoft Fakes
285 | FakesAssemblies/
286 |
287 | # GhostDoc plugin setting file
288 | *.GhostDoc.xml
289 |
290 | # Node.js Tools for Visual Studio
291 | .ntvs_analysis.dat
292 | node_modules/
293 |
294 | # Visual Studio 6 build log
295 | *.plg
296 |
297 | # Visual Studio 6 workspace options file
298 | *.opt
299 |
300 | # Visual Studio 6 auto-generated workspace file (contains which files were open etc.)
301 | *.vbw
302 |
303 | # Visual Studio 6 auto-generated project file (contains which files were open etc.)
304 | *.vbp
305 |
306 | # Visual Studio 6 workspace and project file (working project files containing files to include in project)
307 | *.dsw
308 | *.dsp
309 |
310 | # Visual Studio 6 technical files
311 | *.ncb
312 | *.aps
313 |
314 | # Visual Studio LightSwitch build output
315 | **/*.HTMLClient/GeneratedArtifacts
316 | **/*.DesktopClient/GeneratedArtifacts
317 | **/*.DesktopClient/ModelManifest.xml
318 | **/*.Server/GeneratedArtifacts
319 | **/*.Server/ModelManifest.xml
320 | _Pvt_Extensions
321 |
322 | # Paket dependency manager
323 | .paket/paket.exe
324 | paket-files/
325 |
326 | # FAKE - F# Make
327 | .fake/
328 |
329 | # CodeRush personal settings
330 | .cr/personal
331 |
332 | # Python Tools for Visual Studio (PTVS)
333 | __pycache__/
334 | *.pyc
335 |
336 | # Cake - Uncomment if you are using it
337 | # tools/**
338 | # !tools/packages.config
339 |
340 | # Tabs Studio
341 | *.tss
342 |
343 | # Telerik's JustMock configuration file
344 | *.jmconfig
345 |
346 | # BizTalk build output
347 | *.btp.cs
348 | *.btm.cs
349 | *.odx.cs
350 | *.xsd.cs
351 |
352 | # OpenCover UI analysis results
353 | OpenCover/
354 |
355 | # Azure Stream Analytics local run output
356 | ASALocalRun/
357 |
358 | # MSBuild Binary and Structured Log
359 | *.binlog
360 |
361 | # NVidia Nsight GPU debugger configuration file
362 | *.nvuser
363 |
364 | # MFractors (Xamarin productivity tool) working folder
365 | .mfractor/
366 |
367 | # Local History for Visual Studio
368 | .localhistory/
369 |
370 | # Visual Studio History (VSHistory) files
371 | .vshistory/
372 |
373 | # BeatPulse healthcheck temp database
374 | healthchecksdb
375 |
376 | # Backup folder for Package Reference Convert tool in Visual Studio 2017
377 | MigrationBackup/
378 |
379 | # Ionide (cross platform F# VS Code tools) working folder
380 | .ionide/
381 |
382 | # Fody - auto-generated XML schema
383 | FodyWeavers.xsd
384 |
385 | # VS Code files for those working on multiple tools
386 | .vscode/*
387 | !.vscode/settings.json
388 | !.vscode/tasks.json
389 | !.vscode/launch.json
390 | !.vscode/extensions.json
391 | *.code-workspace
392 |
393 | # Local History for Visual Studio Code
394 | .history/
395 |
396 | # Windows Installer files from build outputs
397 | *.cab
398 | *.msi
399 | *.msix
400 | *.msm
401 | *.msp
402 |
403 | # JetBrains Rider
404 | *.sln.iml
405 | .idea
406 |
407 | ##
408 | ## Visual studio for Mac
409 | ##
410 |
411 |
412 | # globs
413 | Makefile.in
414 | *.userprefs
415 | *.usertasks
416 | config.make
417 | config.status
418 | aclocal.m4
419 | install-sh
420 | autom4te.cache/
421 | *.tar.gz
422 | tarballs/
423 | test-results/
424 |
425 | # Mac bundle stuff
426 | *.dmg
427 | *.app
428 |
429 | # content below from: https://github.com/github/gitignore/blob/master/Global/macOS.gitignore
430 | # General
431 | .DS_Store
432 | .AppleDouble
433 | .LSOverride
434 |
435 | # Icon must end with two \r
436 | Icon
437 |
438 |
439 | # Thumbnails
440 | ._*
441 |
442 | # Files that might appear in the root of a volume
443 | .DocumentRevisions-V100
444 | .fseventsd
445 | .Spotlight-V100
446 | .TemporaryItems
447 | .Trashes
448 | .VolumeIcon.icns
449 | .com.apple.timemachine.donotpresent
450 |
451 | # Directories potentially created on remote AFP share
452 | .AppleDB
453 | .AppleDesktop
454 | Network Trash Folder
455 | Temporary Items
456 | .apdisk
457 |
458 | # content below from: https://github.com/github/gitignore/blob/master/Global/Windows.gitignore
459 | # Windows thumbnail cache files
460 | Thumbs.db
461 | ehthumbs.db
462 | ehthumbs_vista.db
463 |
464 | # Dump file
465 | *.stackdump
466 |
467 | # Folder config file
468 | [Dd]esktop.ini
469 |
470 | # Recycle Bin used on file shares
471 | $RECYCLE.BIN/
472 |
473 | # Windows Installer files
474 | *.cab
475 | *.msi
476 | *.msix
477 | *.msm
478 | *.msp
479 |
480 | # Windows shortcuts
481 | *.lnk
482 |
483 | # Vim temporary swap files
484 | *.swp
485 |
--------------------------------------------------------------------------------
/src/AvaloniaHex/Document/BitLocation.cs:
--------------------------------------------------------------------------------
1 | namespace AvaloniaHex.Document;
2 |
3 | ///
4 | /// Represents a bit offset within a binary document.
5 | ///
6 | public readonly struct BitLocation : IEquatable, IComparable
7 | {
8 | ///
9 | /// The minimum value for a bit location.
10 | ///
11 | public static readonly BitLocation Minimum = new(0, 0);
12 |
13 | ///
14 | /// The maximum value for a bit location.
15 | ///
16 | public static readonly BitLocation Maximum = new(ulong.MaxValue, 7);
17 |
18 | ///
19 | /// Creates a new bit location.
20 | ///
21 | /// The byte offset within the binary document.
22 | public BitLocation(ulong byteIndex)
23 | : this(byteIndex, 0)
24 | {
25 | }
26 |
27 | ///
28 | /// Creates a new bit location.
29 | ///
30 | /// The byte offset within the binary document.
31 | /// The bit index within the referenced byte.
32 | public BitLocation(ulong byteIndex, int bitIndex)
33 | {
34 | if (bitIndex is < 0 or >= 8)
35 | throw new ArgumentOutOfRangeException(nameof(bitIndex));
36 |
37 | ByteIndex = byteIndex;
38 | BitIndex = bitIndex;
39 | }
40 |
41 | ///
42 | /// Gets the byte offset within the binary document.
43 | ///
44 | public ulong ByteIndex
45 | {
46 | get;
47 | }
48 |
49 | ///
50 | /// Gets the bit index within the referenced byte.
51 | ///
52 | public int BitIndex
53 | {
54 | get;
55 | }
56 |
57 | ///
58 | /// Creates a single-byte range at the location.
59 | ///
60 | /// The range.
61 | public BitRange ToSingleByteRange() => new(ByteIndex, ByteIndex + 1);
62 |
63 | ///
64 | /// Adds a number of bytes to the location.
65 | ///
66 | /// The byte count.
67 | /// The new location.
68 | public BitLocation AddBytes(ulong bytes) => new(ByteIndex + bytes, BitIndex);
69 |
70 | ///
71 | /// Subtracts a number of bytes to the location.
72 | ///
73 | /// The byte count.
74 | /// The new location.
75 | public BitLocation SubtractBytes(ulong bytes) => new(ByteIndex - bytes, BitIndex);
76 |
77 | ///
78 | /// Adds a number of bits to the location.
79 | ///
80 | /// The bit count.
81 | /// The new location.
82 | public BitLocation AddBits(ulong bits)
83 | {
84 | ulong remaining = (ulong)(7 - BitIndex);
85 | if (remaining >= bits)
86 | return new BitLocation(ByteIndex, BitIndex + (int)bits);
87 |
88 | bits -= remaining + 1;
89 | (ulong byteCount, ulong bitCount) = Math.DivRem(bits, 8);
90 |
91 | return new BitLocation(ByteIndex + byteCount + 1, (int)bitCount);
92 | }
93 |
94 | ///
95 | /// Gets the location of the previous bit in the binary document.
96 | ///
97 | /// The previous location.
98 | ///
99 | /// Occurs when the bit location references the first bit in a binary document.
100 | ///
101 | public BitLocation Previous()
102 | {
103 | if (BitIndex == 0)
104 | {
105 | if (ByteIndex == 0)
106 | throw new ArgumentException("Cannot get the previous bit location before the zero offset.");
107 |
108 | return new BitLocation(ByteIndex - 1, 7);
109 | }
110 |
111 | return new BitLocation(ByteIndex, BitIndex - 1);
112 | }
113 |
114 | ///
115 | /// Gets the location of the previous bit in the binary document, or the first bit location in a binary document.
116 | ///
117 | /// The previous location.
118 | public BitLocation PreviousOrZero()
119 | {
120 | if (BitIndex == 0)
121 | return ByteIndex == 0 ? this : new BitLocation(ByteIndex - 1, 7);
122 |
123 | return new BitLocation(ByteIndex, BitIndex - 1);
124 | }
125 |
126 | ///
127 | /// Gets the location of the next bit in the binary document.
128 | ///
129 | /// The next location.
130 | ///
131 | /// Occurs when the bit location references the last possible bit in a binary document.
132 | ///
133 | public BitLocation Next()
134 | {
135 | if (BitIndex == 7)
136 | {
137 | if (ByteIndex == ulong.MaxValue)
138 | throw new ArgumentException("Cannot get the next bit location after the maximum offset.");
139 |
140 | return new BitLocation(ByteIndex + 1, 0);
141 | }
142 |
143 | return new BitLocation(ByteIndex, BitIndex + 1);
144 | }
145 |
146 | ///
147 | /// Gets the location of the next bit in the binary document, or the maximum bit location possible in a binary document.
148 | ///
149 | /// The next location.
150 | public BitLocation NextOrMax()
151 | {
152 | if (BitIndex == 7)
153 | return ByteIndex == ulong.MaxValue ? this : new BitLocation(ByteIndex + 1, 0);
154 |
155 | return new BitLocation(ByteIndex, BitIndex + 1);
156 | }
157 |
158 | ///
159 | /// Chooses the lowest bit location between the current and provided locations.
160 | ///
161 | /// The other location.
162 | /// The lowest bit location.
163 | public BitLocation Min(BitLocation other) => this > other ? other : this;
164 |
165 | ///
166 | /// Chooses the highest bit location between the current and provided locations.
167 | ///
168 | /// The other location.
169 | /// The highest bit location.
170 | public BitLocation Max(BitLocation other) => this < other ? other : this;
171 |
172 | ///
173 | /// Restricts the current location to the provided range.
174 | ///
175 | /// The range.
176 | /// The restricted location.
177 | public BitLocation Clamp(BitRange range) => Max(range.Start).Min(range.End.PreviousOrZero());
178 |
179 | ///
180 | /// Aligns the location down to the lower byte offset.
181 | ///
182 | /// The aligned location.
183 | public BitLocation AlignDown() => new(ByteIndex, 0);
184 |
185 | ///
186 | /// Aligns the location up to the next byte offset.
187 | ///
188 | /// The aligned location.
189 | public BitLocation AlignUp() => BitIndex > 0 ? new BitLocation(ByteIndex + 1, 0) : this;
190 |
191 | ///
192 | public int CompareTo(BitLocation other)
193 | {
194 | int byteIndexComparison = ByteIndex.CompareTo(other.ByteIndex);
195 | if (byteIndexComparison != 0)
196 | return byteIndexComparison;
197 |
198 | return BitIndex.CompareTo(other.BitIndex);
199 | }
200 |
201 | ///
202 | public bool Equals(BitLocation other)
203 | {
204 | return ByteIndex == other.ByteIndex && BitIndex == other.BitIndex;
205 | }
206 |
207 | ///
208 | public override bool Equals(object? obj)
209 | {
210 | return obj is BitLocation other && Equals(other);
211 | }
212 |
213 | ///
214 | public override int GetHashCode()
215 | {
216 | unchecked
217 | {
218 | return (ByteIndex.GetHashCode() * 397) ^ BitIndex;
219 | }
220 | }
221 |
222 | ///
223 | public override string ToString() => $"{ByteIndex:X}:{BitIndex}";
224 |
225 | ///
226 | /// Determines whether two locations are equal.
227 | ///
228 | /// The first location.
229 | /// The second location.
230 | /// true if the locations are equal, false otherwise.
231 | public static bool operator ==(BitLocation a, BitLocation b) => a.Equals(b);
232 |
233 | ///
234 | /// Determines whether two locations are not equal.
235 | ///
236 | /// The first location.
237 | /// The second location.
238 | /// true if the locations are not equal, false otherwise.
239 | public static bool operator !=(BitLocation a, BitLocation b) => !(a == b);
240 |
241 | ///
242 | /// Determines whether one location is lower than the other.
243 | ///
244 | /// The first location.
245 | /// The second location.
246 | /// true if the first location is lower, false otherwise.
247 | public static bool operator <(BitLocation a, BitLocation b) => a.CompareTo(b) < 0;
248 |
249 | ///
250 | /// Determines whether one location is lower than or equal to the other.
251 | ///
252 | /// The first location.
253 | /// The second location.
254 | /// true if the first location is lower or equal, false otherwise.
255 | public static bool operator <=(BitLocation a, BitLocation b) => a.CompareTo(b) <= 0;
256 |
257 | ///
258 | /// Determines whether one location is higher than the other.
259 | ///
260 | /// The first location.
261 | /// The second location.
262 | /// true if the first location is higher, false otherwise.
263 | public static bool operator >(BitLocation a, BitLocation b) => a.CompareTo(b) > 0;
264 |
265 | ///
266 | /// Determines whether one location is higher than or equal to the other.
267 | ///
268 | /// The first location.
269 | /// The second location.
270 | /// true if the first location is higher or equal, false otherwise.
271 | public static bool operator >=(BitLocation a, BitLocation b) => a.CompareTo(b) >= 0;
272 | }
--------------------------------------------------------------------------------
/src/AvaloniaHex/Rendering/Column.cs:
--------------------------------------------------------------------------------
1 | using Avalonia;
2 | using Avalonia.Input;
3 | using Avalonia.Media;
4 | using Avalonia.Media.TextFormatting;
5 |
6 | namespace AvaloniaHex.Rendering;
7 |
8 | ///
9 | /// Represents a single column in a hex view.
10 | ///
11 | public abstract class Column : Visual
12 | {
13 | internal static readonly Cursor IBeamCursor = new(StandardCursorType.Ibeam);
14 |
15 | private GenericTextRunProperties? _headerRunProperties;
16 | private GenericTextRunProperties? _textRunProperties;
17 |
18 | static Column()
19 | {
20 | ForegroundProperty.Changed.AddClassHandler(OnVisualPropertyChanged);
21 | BackgroundProperty.Changed.AddClassHandler(OnVisualPropertyChanged);
22 | BorderProperty.Changed.AddClassHandler(OnVisualPropertyChanged);
23 | IsVisibleProperty.Changed.AddClassHandler(OnVisibleChanged);
24 | IsHeaderVisibleProperty.Changed.AddClassHandler(OnHeaderChanged);
25 | }
26 |
27 | ///
28 | /// Gets the parent hex view the column was added to.
29 | ///
30 | public HexView? HexView
31 | {
32 | get;
33 | internal set;
34 | }
35 |
36 | ///
37 | /// Gets the index of the column in the hex view.
38 | ///
39 | public int Index => HexView?.Columns.IndexOf(this) ?? -1;
40 |
41 | ///
42 | /// Gets the minimum size of the column.
43 | ///
44 | public abstract Size MinimumSize { get; }
45 |
46 | ///
47 | /// Dependency property for
48 | ///
49 | public static readonly StyledProperty BorderProperty =
50 | AvaloniaProperty.Register(nameof(Border));
51 |
52 | ///
53 | /// Gets or sets the pen to draw border of the column with, or null if no border should be drawn.
54 | ///
55 | public IPen? Border
56 | {
57 | get => GetValue(BorderProperty);
58 | set => SetValue(BorderProperty, value);
59 | }
60 |
61 | ///
62 | /// Dependency property for
63 | ///
64 | public static readonly StyledProperty BackgroundProperty =
65 | AvaloniaProperty.Register(nameof(Background));
66 |
67 | ///
68 | /// Gets or sets the base background brush of the column, or null if no background should be drawn.
69 | ///
70 | public IBrush? Background
71 | {
72 | get => GetValue(BackgroundProperty);
73 | set => SetValue(BackgroundProperty, value);
74 | }
75 |
76 | ///
77 | /// Dependency property for
78 | ///
79 | public static readonly StyledProperty ForegroundProperty =
80 | AvaloniaProperty.Register(nameof(Foreground));
81 |
82 | ///
83 | /// Gets or sets the base foreground brush of the column, or null if the default foreground brush of the
84 | /// parent hex view should be used.
85 | ///
86 | public IBrush? Foreground
87 | {
88 | get => GetValue(ForegroundProperty);
89 | set => SetValue(ForegroundProperty, value);
90 | }
91 |
92 | ///
93 | /// Dependency property for
94 | ///
95 | public static readonly StyledProperty CursorProperty =
96 | AvaloniaProperty.Register(nameof(Cursor));
97 |
98 | ///
99 | /// Gets or sets the cursor to use in the column.
100 | ///
101 | public Cursor? Cursor
102 | {
103 | get => GetValue(CursorProperty);
104 | set => SetValue(CursorProperty, value);
105 | }
106 |
107 | ///
108 | /// Gets the column width.
109 | ///
110 | public virtual double Width => MinimumSize.Width;
111 |
112 | ///
113 | /// Dependency property for
114 | ///
115 | public static readonly StyledProperty IsHeaderVisibleProperty =
116 | AvaloniaProperty.Register(nameof(IsHeaderVisible), defaultValue: true);
117 |
118 | ///
119 | /// Gets or sets a value indicating whether the header of this column is visible.
120 | ///
121 | public bool IsHeaderVisible
122 | {
123 | get => GetValue(IsHeaderVisibleProperty);
124 | set => SetValue(IsHeaderVisibleProperty, value);
125 | }
126 |
127 | ///
128 | /// Dependency property for /
129 | ///
130 | public static readonly StyledProperty HeaderProperty =
131 | AvaloniaProperty.Register(nameof(Header));
132 |
133 | ///
134 | /// Gets or sets the header text of this column.
135 | ///
136 | public string? Header
137 | {
138 | get => GetValue(HeaderProperty);
139 | set => SetValue(HeaderProperty, value);
140 | }
141 |
142 | ///
143 | /// Dependency property for
144 | ///
145 | public static readonly StyledProperty HeaderBackgroundProperty =
146 | AvaloniaProperty.Register(nameof(HeaderBackground));
147 |
148 | ///
149 | /// Gets or sets the base background brush of the header of the column, or null if no background should be
150 | /// drawn.
151 | ///
152 | public IBrush? HeaderBackground
153 | {
154 | get => GetValue(HeaderBackgroundProperty);
155 | set => SetValue(HeaderBackgroundProperty, value);
156 | }
157 |
158 | ///
159 | /// Dependency property for
160 | ///
161 | public static readonly StyledProperty HeaderForegroundProperty =
162 | AvaloniaProperty.Register(nameof(HeaderForeground));
163 |
164 | ///
165 | /// Gets or sets the base foreground brush of the header of the column, or null if the default foreground
166 | /// brush of the parent hex view should be used.
167 | ///
168 | public IBrush? HeaderForeground
169 | {
170 | get => GetValue(HeaderForegroundProperty);
171 | set => SetValue(HeaderForegroundProperty, value);
172 | }
173 |
174 | ///
175 | /// Dependency property for
176 | ///
177 | public static readonly StyledProperty HeaderBorderProperty =
178 | AvaloniaProperty.Register(nameof(HeaderBorder));
179 |
180 | ///
181 | /// Gets or sets the pen to use for drawing the border around the header of the column.
182 | ///
183 | public IPen? HeaderBorder
184 | {
185 | get => GetValue(HeaderBorderProperty);
186 | set => SetValue(HeaderBorderProperty, value);
187 | }
188 |
189 | internal void SetBounds(Rect bounds) => Bounds = bounds;
190 |
191 | ///
192 | /// Gets the text run properties to use for rendering text in this column.
193 | ///
194 | /// The properties.
195 | /// Occurs when the column is not added to a hex view.
196 | protected GenericTextRunProperties GetTextRunProperties()
197 | {
198 | if (HexView is null)
199 | throw new InvalidOperationException("Cannot query text run properties on a column that is not attached to a hex view.");
200 |
201 | if (!HexView.TextRunProperties.Equals(_textRunProperties))
202 | _textRunProperties = HexView.TextRunProperties.WithBrushes(Foreground ?? HexView.Foreground, Background);
203 |
204 | return _textRunProperties;
205 | }
206 |
207 | ///
208 | /// Gets the text run properties to use for rendering text in this column.
209 | ///
210 | /// The properties.
211 | /// Occurs when the column is not added to a hex view.
212 | protected GenericTextRunProperties GetHeaderTextRunProperties()
213 | {
214 | if (HexView is null)
215 | throw new InvalidOperationException("Cannot query text run properties on a column that is not attached to a hex view.");
216 |
217 | if (!HexView.TextRunProperties.Equals(_headerRunProperties))
218 | _headerRunProperties = HexView.TextRunProperties.WithForeground(HeaderForeground ?? HexView.Foreground);
219 |
220 | return _headerRunProperties;
221 | }
222 |
223 | ///
224 | /// Refreshes the measurements required to calculate the dimensions of the column.
225 | ///
226 | public abstract void Measure();
227 |
228 | ///
229 | /// Constructs the text line of the header of the column.
230 | ///
231 | ///
232 | public virtual TextLine? CreateHeaderLine()
233 | {
234 | if (HexView is null || Header is not { } header)
235 | return null;
236 |
237 | var properties = GetHeaderTextRunProperties();
238 | return TextFormatter.Current.FormatLine(
239 | new SimpleTextSource(header, properties),
240 | 0,
241 | double.MaxValue,
242 | new GenericTextParagraphProperties(properties)
243 | )!;
244 | }
245 |
246 | ///
247 | /// Constructs the text line of the provided visual line for this column.
248 | ///
249 | /// The line to render.
250 | /// The rendered text.
251 | public abstract TextLine? CreateTextLine(VisualBytesLine line);
252 |
253 | private static void OnVisualPropertyChanged(Column arg1, AvaloniaPropertyChangedEventArgs arg2)
254 | {
255 | arg1.HexView?.InvalidateVisualLines();
256 | }
257 |
258 | private static void OnVisibleChanged(Column arg1, AvaloniaPropertyChangedEventArgs arg2)
259 | {
260 | if (arg1.HexView is null)
261 | return;
262 |
263 | arg1.HexView.InvalidateVisualLines();
264 | foreach (var layer in arg1.HexView.Layers)
265 | layer.InvalidateVisual();
266 | }
267 |
268 | private static void OnHeaderChanged(Column arg1, AvaloniaPropertyChangedEventArgs arg2)
269 | {
270 | if (arg1.HexView is null)
271 | return;
272 |
273 | arg1.HexView.InvalidateHeaders();
274 | foreach (var layer in arg1.HexView.Layers)
275 | layer.InvalidateVisual();
276 | }
277 | }
--------------------------------------------------------------------------------