Browse Source

AddUIElement / AddButton infrastructure for inline UI

Assisted-by: Claude:claude-opus-4-7:Claude Code

Assisted-by: Claude:claude-opus-4-7:Claude Code
pull/3755/head
Siegfried Pammer 2 months ago
parent
commit
16e64e4e8d
  1. 65
      ILSpy/SmartTextOutputExtensions.cs
  2. 18
      ILSpy/TextView/AvaloniaEditTextOutput.cs
  3. 10
      ILSpy/TextView/DecompilerTabPageModel.cs
  4. 5
      ILSpy/TextView/DecompilerTextView.axaml.cs
  5. 14
      ILSpy/TextView/ISmartTextOutput.cs
  6. 79
      ILSpy/TextView/UIElementGenerator.cs

65
ILSpy/SmartTextOutputExtensions.cs

@ -0,0 +1,65 @@ @@ -0,0 +1,65 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.Media;
using ILSpy.TextView;
namespace ILSpy
{
public static class SmartTextOutputExtensions
{
public static void AddButton(this ISmartTextOutput output, IImage? icon, string text, EventHandler<RoutedEventArgs> click)
{
ArgumentNullException.ThrowIfNull(output);
ArgumentNullException.ThrowIfNull(click);
output.AddUIElement(() => {
var button = new Button {
Cursor = new Cursor(StandardCursorType.Arrow),
Margin = new Thickness(2),
Padding = new Thickness(9, 1, 9, 1),
MinWidth = 73,
};
if (icon != null)
{
button.Content = new StackPanel {
Orientation = Orientation.Horizontal,
Children = {
new Image { Width = 16, Height = 16, Source = icon, Margin = new Thickness(0, 0, 4, 0) },
new TextBlock { Text = text },
},
};
}
else
{
button.Content = text;
}
button.Click += click;
return button;
});
}
}
}

18
ILSpy/TextView/AvaloniaEditTextOutput.cs

@ -16,10 +16,13 @@ @@ -16,10 +16,13 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using System.Reflection.Metadata;
using System.Text;
using Avalonia.Controls;
using AvaloniaEdit.Document;
using AvaloniaEdit.Folding;
using AvaloniaEdit.Highlighting;
@ -62,6 +65,12 @@ namespace ILSpy.TextView @@ -62,6 +65,12 @@ namespace ILSpy.TextView
/// <summary>Maps reference targets to their definition offsets in the rendered text.</summary>
public DefinitionLookup DefinitionLookup { get; } = new();
readonly List<KeyValuePair<int, Lazy<Control>>> uiElements = new();
/// <summary>Inline UI elements collected during writing, in offset order. Fed to
/// <see cref="UIElementGenerator"/> by the text view.</summary>
public IReadOnlyList<KeyValuePair<int, Lazy<Control>>> UIElements => uiElements;
public string Title { get; set; } = string.Empty;
public string IndentationString { get; set; } = "\t";
@ -172,6 +181,15 @@ namespace ILSpy.TextView @@ -172,6 +181,15 @@ namespace ILSpy.TextView
foldings.Add(folding);
}
public void AddUIElement(Func<Control> element)
{
if (element == null)
return;
if (uiElements.Count > 0 && uiElements[uiElements.Count - 1].Key == builder.Length)
throw new InvalidOperationException("Only one UIElement is allowed for each position in the document.");
uiElements.Add(new KeyValuePair<int, Lazy<Control>>(builder.Length, new Lazy<Control>(element)));
}
public void BeginSpan(HighlightingColor highlightingColor)
{
openSpans.Push((builder.Length, highlightingColor));

10
ILSpy/TextView/DecompilerTabPageModel.cs

@ -21,6 +21,7 @@ using System.Collections.Generic; @@ -21,6 +21,7 @@ using System.Collections.Generic;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Threading;
using AvaloniaEdit.Document;
@ -90,6 +91,13 @@ namespace ILSpy.TextView @@ -90,6 +91,13 @@ namespace ILSpy.TextView
[ObservableProperty]
private DefinitionLookup? definitionLookup;
/// <summary>
/// Inline UI elements (<see cref="ISmartTextOutput.AddUIElement"/>), in offset order.
/// Fed to <see cref="UIElementGenerator"/> by the text view.
/// </summary>
[ObservableProperty]
private IReadOnlyList<KeyValuePair<int, Lazy<Control>>>? uIElements;
/// <summary>
/// Fired when the user clicks a cross-document reference. The host (DockWorkspace)
/// resolves the target on the assembly tree side.
@ -172,6 +180,7 @@ namespace ILSpy.TextView @@ -172,6 +180,7 @@ namespace ILSpy.TextView
var collectedFoldings = output.Foldings;
var collectedReferences = output.References;
var collectedLookup = output.DefinitionLookup;
var collectedUIElements = output.UIElements;
await Dispatcher.UIThread.InvokeAsync(() => {
Title = nodeTitle;
SyntaxExtension = newSyntaxExtension;
@ -179,6 +188,7 @@ namespace ILSpy.TextView @@ -179,6 +188,7 @@ namespace ILSpy.TextView
Foldings = collectedFoldings;
References = collectedReferences;
DefinitionLookup = collectedLookup;
UIElements = collectedUIElements;
Text = rendered;
});
}

5
ILSpy/TextView/DecompilerTextView.axaml.cs

@ -62,6 +62,7 @@ namespace ILSpy.TextView @@ -62,6 +62,7 @@ namespace ILSpy.TextView
const double MaxMovementAwayFromPopup = 5;
readonly ReferenceElementGenerator referenceElementGenerator;
readonly UIElementGenerator uiElementGenerator;
readonly TextMarkerService textMarkerService;
readonly List<TextMarker> localReferenceMarks = new();
RichTextColorizer? activeColorizer;
@ -81,6 +82,9 @@ namespace ILSpy.TextView @@ -81,6 +82,9 @@ namespace ILSpy.TextView
referenceElementGenerator.ReferenceClicked += OnReferenceClicked;
Editor.TextArea.TextView.ElementGenerators.Add(referenceElementGenerator);
uiElementGenerator = new UIElementGenerator();
Editor.TextArea.TextView.ElementGenerators.Add(uiElementGenerator);
// Background renderer that paints local-reference highlights. Lives once for the
// lifetime of the view; the marks themselves are cleared and rebuilt per click.
textMarkerService = new TextMarkerService(Editor.TextArea.TextView);
@ -180,6 +184,7 @@ namespace ILSpy.TextView @@ -180,6 +184,7 @@ namespace ILSpy.TextView
}
referenceElementGenerator.References = model.References;
uiElementGenerator.UIElements = model.UIElements;
Editor.TextArea.TextView.Redraw();
Editor.ScrollToHome();

14
ILSpy/TextView/ISmartTextOutput.cs

@ -16,6 +16,10 @@ @@ -16,6 +16,10 @@
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using Avalonia.Controls;
using AvaloniaEdit.Highlighting;
using ICSharpCode.Decompiler;
@ -23,11 +27,17 @@ using ICSharpCode.Decompiler; @@ -23,11 +27,17 @@ using ICSharpCode.Decompiler;
namespace ILSpy.TextView
{
/// <summary>
/// Adds Avalonia-specific output features to <see cref="ITextOutput"/> — currently just
/// span-based highlighting. Inline UI elements are deferred to a later phase.
/// Adds Avalonia-specific output features to <see cref="ITextOutput"/>: span-based
/// highlighting + inline UI elements rendered by <see cref="UIElementGenerator"/>.
/// </summary>
public interface ISmartTextOutput : ITextOutput
{
/// <summary>
/// Inserts an interactive UI element at the current position. The factory runs lazily
/// on the UI thread when the element generator first realises the row.
/// </summary>
void AddUIElement(Func<Control> element);
void BeginSpan(HighlightingColor highlightingColor);
void EndSpan();

79
ILSpy/TextView/UIElementGenerator.cs

@ -0,0 +1,79 @@ @@ -0,0 +1,79 @@
// Copyright (c) 2026 AlphaSierraPapa for the SharpDevelop Team
//
// Permission is hereby granted, free of charge, to any person obtaining a copy of this
// software and associated documentation files (the "Software"), to deal in the Software
// without restriction, including without limitation the rights to use, copy, modify, merge,
// publish, distribute, sublicense, and/or sell copies of the Software, and to permit persons
// to whom the Software is furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all copies or
// substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR IMPLIED,
// INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS FOR A PARTICULAR
// PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE LIABLE
// FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR
// OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
// DEALINGS IN THE SOFTWARE.
using System;
using System.Collections.Generic;
using Avalonia.Controls;
using AvaloniaEdit.Rendering;
namespace ILSpy.TextView
{
using Pair = KeyValuePair<int, Lazy<Control>>;
/// <summary>
/// Embeds inline UI elements produced by <see cref="ISmartTextOutput.AddUIElement"/> in
/// the rendered text. Mirrors the WPF host's UIElementGenerator: the element factory is
/// stored as a <see cref="Lazy{Control}"/> so the actual control is constructed on the
/// UI thread when the row is first realised.
/// </summary>
sealed class UIElementGenerator : VisualLineElementGenerator, IComparer<Pair>
{
public IReadOnlyList<Pair>? UIElements;
public override int GetFirstInterestedOffset(int startOffset)
{
if (UIElements == null)
return -1;
int r = BinarySearch(UIElements, new Pair(startOffset, null!));
if (r < 0)
r = ~r;
return r < UIElements.Count ? UIElements[r].Key : -1;
}
public override VisualLineElement? ConstructElement(int offset)
{
if (UIElements == null)
return null;
int r = BinarySearch(UIElements, new Pair(offset, null!));
return r >= 0 ? new InlineObjectElement(0, UIElements[r].Value.Value) : null;
}
int IComparer<Pair>.Compare(Pair x, Pair y) => x.Key.CompareTo(y.Key);
// IReadOnlyList has no BinarySearch. Hand-rolled — list is offset-sorted by
// AvaloniaEditTextOutput.AddUIElement.
int BinarySearch(IReadOnlyList<Pair> list, Pair value)
{
int lo = 0, hi = list.Count - 1;
while (lo <= hi)
{
int mid = lo + ((hi - lo) >> 1);
int cmp = ((IComparer<Pair>)this).Compare(list[mid], value);
if (cmp == 0)
return mid;
if (cmp < 0)
lo = mid + 1;
else
hi = mid - 1;
}
return ~lo;
}
}
}
Loading…
Cancel
Save