AvaloniaUI / AvaloniaEdit

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

Caret position breaks when an element longer than 1 text character is used in ElementGenerators #445

Open KillzXGaming opened 3 months ago

KillzXGaming commented 3 months ago

Hello. I made a custom VisualLineElementGenerator where I draw a box similar to how the control character boxes are done. The difference with mine, I use a longer length than 1 in FormattedTextElement constructor which seems to cause issues with the caret position as it advances much further out than it should be the more tags that are written before it.

The idea is I want to hide text longer than 1 character within [] brackets and create a custom visual for these.

image

A way to replicate this.


    public MessageEditor()
    {
        InitializeComponent();

        _textEditor.TextArea.TextView.ElementGenerators.Add(new ControlCodeElementGenerator(_textEditor.TextArea));
        _textEditor.Text = "Test [Color::Red][Text::Bold]aaa This text cannot be selected at the right pos[Text::Normal]Test";
    }

public class ControlCodeElementGenerator : VisualLineElementGenerator
{
    private readonly Regex _regex;
    private readonly TextArea _textArea;

    public ControlCodeElementGenerator(TextArea textArea)
    {
        _regex = new Regex(@"\[.*?\]"); //[] text are tags where we want to custom display
        _textArea = textArea;
    }

    public override int GetFirstInterestedOffset(int startOffset)
    {
        //get the placement of the a tag [] for constructing a visual element at the text place
        var text = CurrentContext.Document.Text;
        var match = _regex.Match(text, startOffset);

        return match.Success ? match.Index : -1;
    }

    public override VisualLineElement ConstructElement(int offset)
    {
        var text = CurrentContext.Document.Text;
        var match = _regex.Match(text, offset);

        if (match.Success && match.Index == offset) 
        {
            //constructs a box like the control character box
            var runProperties = new VisualLineElementTextRunProperties(CurrentContext.GlobalTextRunProperties);
            runProperties.SetForegroundBrush(Brushes.White);

            var controlCode = match.Value;

            var textLine = FormattedTextElement.PrepareText(
                TextFormatter.Current, "[Tag]", runProperties);

            return new SpecialCharacterBoxElement(textLine, controlCode.Length);  //only if length is 1, caret pos works correctly
        }

        return null;
    }
}

public class SpecialCharacterBoxElement : FormattedTextElement
{
    private int _length;

    public SpecialCharacterBoxElement(TextLine text, int length) : base(text, length) //todo only if length is 1, caret pos works correctly
    {
        _length = length;
    }

    public override int GetNextCaretPosition(int visualColumn, AvaloniaEdit.Document.LogicalDirection direction, CaretPositioningMode mode)
    {
        if (mode == CaretPositioningMode.Normal || mode == CaretPositioningMode.EveryCodepoint)
            return base.GetNextCaretPosition(visualColumn, direction, mode);
        else
            return -1;
    }

    public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructionContext context)
    {
        return new SpecialCharacterTextRun(this, TextRunProperties);
    }
}

public class SpecialCharacterTextRun : FormattedTextRun
{
    private static readonly ISolidColorBrush DarkGrayBrush;

    internal const double BoxMargin = 3;

    static SpecialCharacterTextRun()
    {
        DarkGrayBrush = new ImmutableSolidColorBrush(Color.FromArgb(200, 128, 128, 128));
    }

    public SpecialCharacterTextRun(FormattedTextElement element, TextRunProperties properties)
        : base(element, properties)
    {
    }

    public override Size Size
    {
        get
        {
            var s = base.Size;

            return s.WithWidth(s.Width + BoxMargin);
        }
    }

    public override void Draw(DrawingContext drawingContext, Point origin)
    {
        var (x, y) = origin;

        var newOrigin = new Point(x + (BoxMargin / 2), y);

        var (width, height) = Size;

        var r = new Rect(x, y, width, height);

        drawingContext.FillRectangle(DarkGrayBrush, r, 2.5f);

        base.Draw(drawingContext, newOrigin);
    }
}