Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve long lines support #172

Merged
merged 12 commits into from Dec 22, 2021
Merged
4 changes: 4 additions & 0 deletions src/AvaloniaEdit.Demo/MainWindow.xaml
Expand Up @@ -3,6 +3,7 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
MinWidth="500"
MinHeight="300"
Width="950"
Title="AvaloniaEdit Demo"
x:Class="AvaloniaEdit.Demo.MainWindow"
Background="#1E1E1E">
Expand All @@ -17,6 +18,9 @@
<ComboBox Name="syntaxModeCombo" />
<Button Name="changeThemeBtn" Content="Change theme"/>
</StackPanel>
<StackPanel Name="StatusBar" Background="Purple" Height="25" DockPanel.Dock="Bottom" Orientation="Horizontal">
<TextBlock Name="StatusText" Text="Ready" Margin="5 0 0 0" VerticalAlignment="Center" FontSize="12"/>
</StackPanel>
<AvalonEdit:TextEditor Name="Editor"
FontFamily="Consolas,Menlo,Monospace"
Margin="30"
Expand Down
10 changes: 10 additions & 0 deletions src/AvaloniaEdit.Demo/MainWindow.xaml.cs
Expand Up @@ -34,6 +34,7 @@ public class MainWindow : Window
private Button _clearControlBtn;
private Button _changeThemeBtn;
private ComboBox _syntaxModeCombo;
private TextBlock _statusTextBlock;
private ElementGenerator _generator = new ElementGenerator();
private int _currentTheme = (int)ThemeName.DarkPlus;

Expand All @@ -57,7 +58,9 @@ public MainWindow()
_textEditor.TextArea.TextEntered += textEditor_TextArea_TextEntered;
_textEditor.TextArea.TextEntering += textEditor_TextArea_TextEntering;
_textEditor.TextArea.IndentationStrategy = new Indentation.CSharp.CSharpIndentationStrategy();
_textEditor.TextArea.Caret.PositionChanged += Caret_PositionChanged;
_textEditor.TextArea.RightClickMovesCaret = true;

_addControlBtn = this.FindControl<Button>("addControlBtn");
_addControlBtn.Click += _addControlBtn_Click;

Expand Down Expand Up @@ -85,6 +88,8 @@ public MainWindow()
_textEditor.Document = new TextDocument(ResourceLoader.LoadSampleFile(scopeName));
_textMateInstallation.SetGrammarByLanguageId(csharpLanguage.Id);

_statusTextBlock = this.Find<TextBlock>("StatusText");

this.AddHandler(PointerWheelChangedEvent, (o, i) =>
{
if (i.KeyModifiers != KeyModifiers.Control) return;
Expand All @@ -93,6 +98,11 @@ public MainWindow()
}, RoutingStrategies.Bubble, true);
}

private void Caret_PositionChanged(object sender, EventArgs e)
{
_statusTextBlock.Text = String.Format("Line {0} Column {1}", _textEditor.TextArea.Caret.Line, _textEditor.TextArea.Caret.Column);
}

protected override void OnClosed(EventArgs e)
{
base.OnClosed(e);
Expand Down
2 changes: 1 addition & 1 deletion src/AvaloniaEdit.TextMate/AvaloniaEdit.TextMate.csproj
Expand Up @@ -19,7 +19,7 @@

<ItemGroup>
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="TextMateSharp" Version="1.0.13" />
<PackageReference Include="TextMateSharp" Version="1.0.14" />
</ItemGroup>

</Project>
21 changes: 19 additions & 2 deletions src/AvaloniaEdit/Rendering/VisualLine.cs
Expand Up @@ -37,6 +37,8 @@ namespace AvaloniaEdit.Rendering
/// </summary>
public sealed class VisualLine
{
public const int LENGTH_LIMIT = 3000;

private enum LifetimePhase : byte
{
Generating,
Expand Down Expand Up @@ -155,6 +157,7 @@ internal void ConstructVisualElements(ITextRunConstructionContext context, Visua
private void PerformVisualElementConstruction(VisualLineElementGenerator[] generators)
{
var document = Document;
var lineLength = FirstDocumentLine.Length;
var offset = FirstDocumentLine.Offset;
var currentLineEnd = offset + FirstDocumentLine.Length;
LastDocumentLine = FirstDocumentLine;
Expand All @@ -164,7 +167,7 @@ private void PerformVisualElementConstruction(VisualLineElementGenerator[] gener
var textPieceEndOffset = currentLineEnd;
foreach (var g in generators)
{
g.CachedInterest = g.GetFirstInterestedOffset(offset + askInterestOffset);
g.CachedInterest = (lineLength > LENGTH_LIMIT) ? -1: g.GetFirstInterestedOffset(offset + askInterestOffset);
if (g.CachedInterest != -1)
{
if (g.CachedInterest < offset)
Expand All @@ -179,7 +182,21 @@ private void PerformVisualElementConstruction(VisualLineElementGenerator[] gener
if (textPieceEndOffset > offset)
{
var textPieceLength = textPieceEndOffset - offset;
_elements.Add(new VisualLineText(this, textPieceLength));
int remaining = textPieceLength;
while (true)
{
if (remaining > LENGTH_LIMIT)
{
// split in chunks of LENGTH_LIMIT
_elements.Add(new VisualLineText(this, LENGTH_LIMIT));
remaining -= LENGTH_LIMIT;
}
else
{
_elements.Add(new VisualLineText(this, remaining));
break;
}
}
offset = textPieceEndOffset;
}
// If no elements constructed / only zero-length elements constructed:
Expand Down
7 changes: 0 additions & 7 deletions src/AvaloniaEdit/Text/TextLineImpl.cs
Expand Up @@ -9,8 +9,6 @@ namespace AvaloniaEdit.Text
{
internal sealed class TextLineImpl : TextLine
{
private const int MaxCharactersPerLine = 10000;

private readonly TextLineRun[] _runs;

public override int FirstIndex { get; }
Expand Down Expand Up @@ -49,11 +47,6 @@ internal static TextLineImpl Create(TextParagraphProperties paragraphProperties,
visibleLength += AddRunReturnVisibleLength(runs, prevRun);
}

if (visibleLength >= MaxCharactersPerLine)
{
throw new NotSupportedException("Too many characters per line");
}

while (true)
{
visibleLength += AddRunReturnVisibleLength(runs, run);
Expand Down
99 changes: 70 additions & 29 deletions src/AvaloniaEdit/Text/TextLineRun.cs
@@ -1,8 +1,12 @@
using System;
using System.Linq;

using Avalonia;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;

using AvaloniaEdit.Rendering;

namespace AvaloniaEdit.Text
{
internal sealed class TextLineRun
Expand All @@ -11,8 +15,7 @@ internal sealed class TextLineRun

private FormattedText _formattedText;
private Size _formattedTextSize;
private double[] _glyphWidths;

private GlyphWidths _glyphWidths;
public StringRange StringRange { get; private set; }

public int Length { get; set; }
Expand Down Expand Up @@ -109,7 +112,10 @@ private static TextLineRun Create(TextSource textSource, StringRange stringRange
return new TextLineRun(textRun.Length, textRun)
{
IsEmbedded = true,
_glyphWidths = new double[] { width },
_glyphWidths = new GlyphWidths(
stringRange,
textRun.Properties.Typeface.GlyphTypeface,
textRun.Properties.FontSize),
// Embedded objects must propagate their width to the container.
// Otherwise text runs after the embedded object are drawn at the same x position.
Width = width
Expand Down Expand Up @@ -162,7 +168,10 @@ private static TextLineRun CreateRunForTab(TextRun textRun)
Width = 40
};

run.SetGlyphWidths();
run._glyphWidths = new GlyphWidths(
run.StringRange,
run.Typeface.GlyphTypeface,
run.FontSize);

return run;
}
Expand Down Expand Up @@ -192,7 +201,10 @@ internal static TextLineRun CreateRunForText(StringRange stringRange, TextRun te

run.Width = size.Width;

run.SetGlyphWidths();
run._glyphWidths = new GlyphWidths(
run.StringRange,
run.Typeface.GlyphTypeface,
run.FontSize);

return run;
}
Expand All @@ -203,27 +215,6 @@ private TextLineRun(int length, TextRun textRun)
TextRun = textRun;
}

private void SetGlyphWidths()
{
var result = new double[StringRange.Length];

for (var i = 0; i < StringRange.Length; i++)
{
// TODO: is there a better way of getting glyph metrics?
var tf = Typeface;
var size = new FormattedText
{
Text = StringRange[i].ToString(),
Typeface = new Typeface(tf.FontFamily, tf.Style, tf.Weight),
FontSize = FontSize
}.Bounds.Size;

result[i] = size.Width;
}

_glyphWidths = result;
}

public void Draw(DrawingContext drawingContext, double x, double y)
{
if (IsEmbedded)
Expand Down Expand Up @@ -290,7 +281,7 @@ public bool UpdateTrailingInfo(TrailingInfo trailing)
{
while (index > 0 && IsSpace(StringRange[index - 1]))
{
trailing.SpaceWidth += _glyphWidths[index - 1];
trailing.SpaceWidth += _glyphWidths.GetAt(index - 1);
index--;
trailing.Count++;
}
Expand All @@ -313,7 +304,7 @@ public double GetDistanceFromCharacter(int index)
double distance = 0;
for (var i = 0; i < index; i++)
{
distance += _glyphWidths[i];
distance += _glyphWidths.GetAt(i);
}

return distance;
Expand All @@ -332,7 +323,7 @@ public double GetDistanceFromCharacter(int index)
double width = 0;
for (; index < Length; index++)
{
width = IsTab ? Width / Length : _glyphWidths[index];
width = IsTab ? Width / Length : _glyphWidths.GetAt(index);
if (distance < width)
{
break;
Expand All @@ -350,5 +341,55 @@ private static bool IsSpace(char ch)
{
return ch == ' ' || ch == '\u00a0';
}

class GlyphWidths
{
private const double NOT_CALCULATED_YET = -1;
private double[] _glyphWidths;
private GlyphTypeface _typeFace;
private StringRange _range;
private double _scale;

internal GlyphWidths(StringRange range, GlyphTypeface typeFace, double fontSize)
{
_range = range;
_typeFace = typeFace;
_scale = fontSize / _typeFace.DesignEmHeight;

InitGlyphWidths();
}

internal double GetAt(int index)
{
if (_glyphWidths[index] == NOT_CALCULATED_YET)
_glyphWidths[index] = MeasureGlyphAt(index);

return _glyphWidths[index];
}

double MeasureGlyphAt(int index)
{
return _typeFace.GetGlyphAdvance(
_typeFace.GetGlyph(_range[index])) * _scale;
}

void InitGlyphWidths()
{
int capacity = _range.Length;

bool useCheapGlyphMeasurement =
capacity >= VisualLine.LENGTH_LIMIT &&
_typeFace.IsFixedPitch;

if (useCheapGlyphMeasurement)
{
double size = MeasureGlyphAt(0);
_glyphWidths = Enumerable.Repeat<double>(size, capacity).ToArray();
return;
}

_glyphWidths = Enumerable.Repeat<double>(NOT_CALCULATED_YET, capacity).ToArray();
}
}
}
}