AvaloniaUI / AvaloniaEdit

Avalonia-based text editor (port of AvalonEdit)
MIT License
746 stars 148 forks source link

Crash when Toggling TextEditor.ShowLineNumbers #434

Open gebodal opened 3 months ago

gebodal commented 3 months ago

Unless I'm missing something, it seems that changing the ShowLineNumbers property of a loaded TextEditor from false to true will cause an ArgumentOutOfRangeException that seems to be originating from LineNumberMargin.Render(). Specifically, this error is occurring in a TextEditor hosted inside a Window which has finished loading and is visible, with the ShowLineNumbers property being changed via user interaction.

https://github.com/AvaloniaUI/AvaloniaEdit/blob/f92e5527c0f6ef1a1a6f6d2dc904af5688315949/src/AvaloniaEdit/Editing/LineNumberMargin.cs#L76-L80

It seems that the EmSize (taken from the TextBlock.FontSizeProperty) is not being set somehow, possibly because it is set in MeasureOverride(), which presumably is not being called before rendering?

https://github.com/AvaloniaUI/AvaloniaEdit/blob/f92e5527c0f6ef1a1a6f6d2dc904af5688315949/src/AvaloniaEdit/Editing/LineNumberMargin.cs#L55

The "OnChanged" method for ShowLineNumbers in TextEditor creates a brand new LineNumberMargin Control when set to true from false, so even if altered in sequence true->false->true, the error occurs.

Stack trace of error:

System.ArgumentOutOfRangeException: The parameter value must be greater than zero. (Parameter 'emSize')
   at Avalonia.Media.FormattedText.ValidateFontSize(Double emSize)
   at Avalonia.Media.FormattedText..ctor(String textToFormat, CultureInfo culture, FlowDirection flowDirection, Typeface typeface, Double emSize, IBrush foreground)
   at AvaloniaEdit.Utils.TextFormatterFactory.CreateFormattedText(Control element, String text, Typeface typeface, Nullable`1 emSize, IBrush foreground)
   at AvaloniaEdit.Editing.LineNumberMargin.Render(DrawingContext drawingContext)
   at Avalonia.Rendering.Composition.CompositingRenderer.UpdateCore()
   at Avalonia.Rendering.Composition.CompositingRenderer.Update()
   at Avalonia.Rendering.Composition.Compositor.CommitCore()
   at Avalonia.Rendering.Composition.Compositor.Commit()
   at Avalonia.Media.MediaContext.CommitCompositor(Compositor compositor)
   at Avalonia.Media.MediaContext.SyncCommit(Compositor compositor, Boolean waitFullRender)
   at Avalonia.Media.MediaContext.SyncDisposeCompositionTarget(CompositionTarget compositionTarget)
   at Avalonia.Rendering.Composition.CompositingRenderer.Dispose()
   at Avalonia.Controls.TopLevel.HandleClosed()
   at Avalonia.Controls.WindowBase.HandleClosed()
   at Avalonia.Win32.WindowImpl.AppWndProc(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam)
   at Avalonia.Win32.WindowImpl.WndProc(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam)
   at Avalonia.Win32.PopupImpl.WndProc(IntPtr hWnd, UInt32 msg, IntPtr wParam, IntPtr lParam)

If there's a workaround, I'd love to hear about it! So far I have tried: InvalidateMeasure(), InvalidateArrange() and InvalidateVisual() after changing ShowLineNumbers on the TextEditor, the TextArea, and all Controls in textArea.LeftMargins; using Dispatcher.UIThread.Invoke() to change the value of ShowLineNumbers. None of these have solved the issue.

gebodal commented 3 months ago

OK, I realised there's a simple enough hack for this, where you just have to replace the TextEditor.ShowLineNumbers setter logic with your own. The only important point is that you have to wait until the control is loaded before disabling, otherwise the EmSize will not have been set correctly.

The below uses a modified version of the TextEditor code here: https://github.com/AvaloniaUI/AvaloniaEdit/blob/f92e5527c0f6ef1a1a6f6d2dc904af5688315949/src/AvaloniaEdit/TextEditor.cs#L549-L560

public MyControl() {
    // Other initialisation here

    //// Line numbers setup
    // Set to true initially so that internal values get initialised properly
    textEditor.ShowLineNumbers = true;

    // Collect list of line controls (following logic in TextEditor.cs#L549-L560)
    List<Control> lineNumberControlsList = new List<Control>();
    var leftMargins = textEditor.TextArea.LeftMargins;
    for (int i = 0; i < leftMargins.Count; i++) {
        if (leftMargins[i] is LineNumberMargin) {
            lineNumberControlsList.Add(leftMargins[i]);
            if (i + 1 < leftMargins.Count && DottedLineMargin.IsDottedLineMargin(leftMargins[i + 1])) {
                lineNumberControlsList.Add(leftMargins[i + 1]);
            }
            break;
        }
    }
    lineNumberControls = lineNumberControlsList.ToArray();

    // When finished loading, set visibility to desired value
    textEditor.Loaded += OnLoadedAdjustShowLineNumbers;
}

private readonly Control[] lineNumberControls;

// Adjust the visibility of the line number controls without actually removing them
public bool ShowLineNumbers {
    get {
        return lineNumberControls.Any(c => c.IsVisible); // Or something neater
    }
    set {
        foreach(Control c in lineNumberControls) {
            c.IsVisible = value;
        }
    }
}

private void OnLoadedAdjustShowLineNumbers(object? sender, RoutedEventArgs e) {
    ShowLineNumbers = MyDataManager.GetShowLineNumbers();
}

Or something like that, anyway. (Seems to work on my machine!)