From fcfdf72983a3f9c4f323451f03d47054b8b32810 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 22 Jan 2022 17:01:52 +0100 Subject: [PATCH 01/17] Fixed a regressing in TextLineRun TextEmbeddedObject don't have a defined StringRange, so we cannot calculate the glyphWidths for them. The previous implementation used the embedded object size as glyph widths. I modified current implementation to behave in the same way. Implemented the same for Tab TextRun. --- src/AvaloniaEdit/Text/TextLineRun.cs | 41 ++++++++++++++++++---------- 1 file changed, 27 insertions(+), 14 deletions(-) diff --git a/src/AvaloniaEdit/Text/TextLineRun.cs b/src/AvaloniaEdit/Text/TextLineRun.cs index e30038eb..49038c24 100644 --- a/src/AvaloniaEdit/Text/TextLineRun.cs +++ b/src/AvaloniaEdit/Text/TextLineRun.cs @@ -1,4 +1,6 @@ using System; +using System.Collections; +using System.Collections.Generic; using System.Linq; using Avalonia; @@ -15,7 +17,7 @@ internal sealed class TextLineRun private FormattedText _formattedText; private Size _formattedTextSize; - private GlyphWidths _glyphWidths; + private IReadOnlyList _glyphWidths; public StringRange StringRange { get; private set; } public int Length { get; set; } @@ -112,10 +114,7 @@ private static TextLineRun Create(TextSource textSource, StringRange stringRange return new TextLineRun(textRun.Length, textRun) { IsEmbedded = true, - _glyphWidths = new GlyphWidths( - stringRange, - textRun.Properties.Typeface.GlyphTypeface, - textRun.Properties.FontSize), + _glyphWidths = new double[] { width }, // 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 @@ -168,10 +167,7 @@ private static TextLineRun CreateRunForTab(TextRun textRun) Width = 40 }; - run._glyphWidths = new GlyphWidths( - run.StringRange, - run.Typeface.GlyphTypeface, - run.FontSize); + run._glyphWidths = new double[] { run.Width }; return run; } @@ -281,7 +277,7 @@ public bool UpdateTrailingInfo(TrailingInfo trailing) { while (index > 0 && IsSpace(StringRange[index - 1])) { - trailing.SpaceWidth += _glyphWidths.GetAt(index - 1); + trailing.SpaceWidth += _glyphWidths[index - 1]; index--; trailing.Count++; } @@ -304,7 +300,7 @@ public double GetDistanceFromCharacter(int index) double distance = 0; for (var i = 0; i < index; i++) { - distance += _glyphWidths.GetAt(i); + distance += _glyphWidths[i]; } return distance; @@ -323,7 +319,7 @@ public double GetDistanceFromCharacter(int index) double width = 0; for (; index < Length; index++) { - width = IsTab ? Width / Length : _glyphWidths.GetAt(index); + width = IsTab ? Width / Length : _glyphWidths[index]; if (distance < width) { break; @@ -342,7 +338,7 @@ private static bool IsSpace(char ch) return ch == ' ' || ch == '\u00a0'; } - class GlyphWidths + class GlyphWidths : IReadOnlyList { private const double NOT_CALCULATED_YET = -1; private double[] _glyphWidths; @@ -350,6 +346,9 @@ class GlyphWidths private StringRange _range; private double _scale; + public int Count => _glyphWidths.Length; + public double this[int index] => GetAt(index); + internal GlyphWidths(StringRange range, GlyphTypeface typeFace, double fontSize) { _range = range; @@ -359,8 +358,11 @@ internal GlyphWidths(StringRange range, GlyphTypeface typeFace, double fontSize) InitGlyphWidths(); } - internal double GetAt(int index) + double GetAt(int index) { + if (_glyphWidths.Length == 0) + return 0; + if (_glyphWidths[index] == NOT_CALCULATED_YET) _glyphWidths[index] = MeasureGlyphAt(index); @@ -390,6 +392,17 @@ void InitGlyphWidths() _glyphWidths = Enumerable.Repeat(NOT_CALCULATED_YET, capacity).ToArray(); } + + IEnumerator IEnumerable.GetEnumerator() + { + foreach (double value in _glyphWidths) + yield return value; + } + + IEnumerator IEnumerable.GetEnumerator() + { + return _glyphWidths.GetEnumerator(); + } } } } \ No newline at end of file From b9df612ad35b9c3d6addd619bb7f65808b7bb85e Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 22 Jan 2022 17:02:42 +0100 Subject: [PATCH 02/17] Fixed margins for the box displayed for ControlCharacters --- .../Rendering/SingleCharacterElementGenerator.cs | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs index a6598691..551d16d0 100644 --- a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs +++ b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs @@ -225,11 +225,12 @@ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructio private sealed class SpecialCharacterTextRun : FormattedTextRun { + private const double _boxMargin = 3; private static readonly ISolidColorBrush DarkGrayBrush; static SpecialCharacterTextRun() { - DarkGrayBrush = new ImmutableSolidColorBrush(Color.FromArgb(200, 128, 128, 128)); + DarkGrayBrush = new ImmutableSolidColorBrush(Color.FromArgb(200, 128, 128, 128)); } public SpecialCharacterTextRun(FormattedTextElement element, TextRunProperties properties) @@ -237,17 +238,17 @@ public SpecialCharacterTextRun(FormattedTextElement element, TextRunProperties p { } - public override Rect ComputeBoundingBox() + public override Size GetSize(double remainingParagraphWidth) { - var r = base.ComputeBoundingBox(); - return r.WithWidth(r.Width + 3); + var s = base.GetSize(remainingParagraphWidth); + return s.WithWidth(s.Width + _boxMargin); } public override void Draw(DrawingContext drawingContext, Point origin) { - var newOrigin = new Point(origin.X + 1.5, origin.Y); + var newOrigin = new Point(origin.X + (_boxMargin / 2), origin.Y); var metrics = GetSize(double.PositiveInfinity); - var r = new Rect(newOrigin.X - 0.5, newOrigin.Y, metrics.Width + 2, metrics.Height); + var r = new Rect(origin.X, origin.Y, metrics.Width, metrics.Height); drawingContext.FillRectangle(DarkGrayBrush, r, 2.5f); base.Draw(drawingContext, newOrigin); } From d46af8b7e8456708424170d85ffa4242c56f95ee Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 22 Jan 2022 18:40:44 +0100 Subject: [PATCH 03/17] Setup control characters and folding in the demo application for testing --- src/AvaloniaEdit.Demo/MainWindow.xaml.cs | 23 +++++++++++++++++++++-- 1 file changed, 21 insertions(+), 2 deletions(-) diff --git a/src/AvaloniaEdit.Demo/MainWindow.xaml.cs b/src/AvaloniaEdit.Demo/MainWindow.xaml.cs index 49b1809e..0e14d029 100644 --- a/src/AvaloniaEdit.Demo/MainWindow.xaml.cs +++ b/src/AvaloniaEdit.Demo/MainWindow.xaml.cs @@ -14,6 +14,7 @@ using AvaloniaEdit.Demo.Resources; using AvaloniaEdit.Document; using AvaloniaEdit.Editing; +using AvaloniaEdit.Folding; using AvaloniaEdit.Rendering; using AvaloniaEdit.TextMate; using AvaloniaEdit.TextMate.Grammars; @@ -25,6 +26,7 @@ namespace AvaloniaEdit.Demo public class MainWindow : Window { private readonly TextEditor _textEditor; + private FoldingManager _foldingManager; private readonly TextMate.TextMate.Installation _textMateInstallation; private CompletionWindow _completionWindow; private OverloadInsightWindow _insightWindow; @@ -56,7 +58,7 @@ public MainWindow() _textEditor.TextArea.Background = this.Background; _textEditor.TextArea.TextEntered += textEditor_TextArea_TextEntered; _textEditor.TextArea.TextEntering += textEditor_TextArea_TextEntering; - _textEditor.Options.ConvertTabsToSpaces = true; + _textEditor.Options.ShowBoxForControlCharacters = true; _textEditor.TextArea.IndentationStrategy = new Indentation.CSharp.CSharpIndentationStrategy(_textEditor.Options); _textEditor.TextArea.Caret.PositionChanged += Caret_PositionChanged; _textEditor.TextArea.RightClickMovesCaret = true; @@ -86,7 +88,9 @@ public MainWindow() string scopeName = _registryOptions.GetScopeByLanguageId(csharpLanguage.Id); - _textEditor.Document = new TextDocument(ResourceLoader.LoadSampleFile(scopeName)); + _textEditor.Document = new TextDocument( + "// AvaloniaEdit supports displaying control chars: \a or \b or \v" + Environment.NewLine + + ResourceLoader.LoadSampleFile(scopeName)); _textMateInstallation.SetGrammar(_registryOptions.GetScopeByLanguageId(csharpLanguage.Id)); _statusTextBlock = this.Find("StatusText"); @@ -117,10 +121,25 @@ private void SyntaxModeCombo_SelectionChanged(object sender, SelectionChangedEve { Language language = (Language)_syntaxModeCombo.SelectedItem; + if (_foldingManager != null) + { + _foldingManager.Clear(); + FoldingManager.Uninstall(_foldingManager); + } + string scopeName = _registryOptions.GetScopeByLanguageId(language.Id); _textEditor.Document = new TextDocument(ResourceLoader.LoadSampleFile(scopeName)); _textMateInstallation.SetGrammar(scopeName); + + if (language.Id == "xml") + { + _foldingManager = FoldingManager.Install(_textEditor.TextArea); + + var strategy = new XmlFoldingStrategy(); + strategy.UpdateFoldings(_foldingManager, _textEditor.Document); + return; + } } private void ChangeThemeButton_Click(object sender, RoutedEventArgs e) From 7bdde039d9d72db6a70b834b376459a10e54aff5 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 22 Jan 2022 18:41:05 +0100 Subject: [PATCH 04/17] Added an attribute to make internal types visible for unit testing --- test/AvaloniaEdit.Tests/AssemblyInfo.cs | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 test/AvaloniaEdit.Tests/AssemblyInfo.cs diff --git a/test/AvaloniaEdit.Tests/AssemblyInfo.cs b/test/AvaloniaEdit.Tests/AssemblyInfo.cs new file mode 100644 index 00000000..5558fe7d --- /dev/null +++ b/test/AvaloniaEdit.Tests/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("AvaloniaEdit.Tests")] \ No newline at end of file From 174f5e6d75b0b7ddcce912ebed1cfda661d34dce Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 22 Jan 2022 18:42:22 +0100 Subject: [PATCH 05/17] Added unit tests --- .../SingleCharacterElementGenerator.cs | 11 ++- .../AvaloniaMocks/MockFormattedTextImpl.cs | 37 ++++++++ .../AvaloniaMocks/MockGlyphTypeface.cs | 9 +- .../AvaloniaMocks/TestServices.cs | 11 ++- .../AvaloniaMocks/UnitTestApplication.cs | 3 +- .../Text/TextLineRunTests.cs | 91 +++++++++++++++++++ 6 files changed, 151 insertions(+), 11 deletions(-) create mode 100644 test/AvaloniaEdit.Tests/AvaloniaMocks/MockFormattedTextImpl.cs create mode 100644 test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs diff --git a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs index 551d16d0..9ce7d55c 100644 --- a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs +++ b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs @@ -18,6 +18,8 @@ using System; using System.Diagnostics.CodeAnalysis; +using System.Runtime.CompilerServices; + using Avalonia; using Avalonia.Media; using Avalonia.Media.Immutable; @@ -223,11 +225,12 @@ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructio } } - private sealed class SpecialCharacterTextRun : FormattedTextRun + internal sealed class SpecialCharacterTextRun : FormattedTextRun { - private const double _boxMargin = 3; private static readonly ISolidColorBrush DarkGrayBrush; + internal const double BoxMargin = 3; + static SpecialCharacterTextRun() { DarkGrayBrush = new ImmutableSolidColorBrush(Color.FromArgb(200, 128, 128, 128)); @@ -241,12 +244,12 @@ public SpecialCharacterTextRun(FormattedTextElement element, TextRunProperties p public override Size GetSize(double remainingParagraphWidth) { var s = base.GetSize(remainingParagraphWidth); - return s.WithWidth(s.Width + _boxMargin); + return s.WithWidth(s.Width + BoxMargin); } public override void Draw(DrawingContext drawingContext, Point origin) { - var newOrigin = new Point(origin.X + (_boxMargin / 2), origin.Y); + var newOrigin = new Point(origin.X + (BoxMargin / 2), origin.Y); var metrics = GetSize(double.PositiveInfinity); var r = new Rect(origin.X, origin.Y, metrics.Width, metrics.Height); drawingContext.FillRectangle(DarkGrayBrush, r, 2.5f); diff --git a/test/AvaloniaEdit.Tests/AvaloniaMocks/MockFormattedTextImpl.cs b/test/AvaloniaEdit.Tests/AvaloniaMocks/MockFormattedTextImpl.cs new file mode 100644 index 00000000..ff7e4692 --- /dev/null +++ b/test/AvaloniaEdit.Tests/AvaloniaMocks/MockFormattedTextImpl.cs @@ -0,0 +1,37 @@ +using Avalonia; +using Avalonia.Media; +using Avalonia.Platform; + +using System.Collections.Generic; + +namespace AvaloniaEdit.Tests.AvaloniaMocks +{ + internal class MockFormattedTextImpl : IFormattedTextImpl + { + Size IFormattedTextImpl.Constraint => new Size(0, 0); + + Rect IFormattedTextImpl.Bounds => new Rect(0, 0, 0, 0); + + string IFormattedTextImpl.Text => throw new System.NotImplementedException(); + + IEnumerable IFormattedTextImpl.GetLines() + { + return null; + } + + TextHitTestResult IFormattedTextImpl.HitTestPoint(Point point) + { + return null; + } + + Rect IFormattedTextImpl.HitTestTextPosition(int index) + { + return Rect.Empty; + } + + IEnumerable IFormattedTextImpl.HitTestTextRange(int index, int length) + { + return new Rect[] { }; + } + } +} diff --git a/test/AvaloniaEdit.Tests/AvaloniaMocks/MockGlyphTypeface.cs b/test/AvaloniaEdit.Tests/AvaloniaMocks/MockGlyphTypeface.cs index 262f0835..112506f5 100644 --- a/test/AvaloniaEdit.Tests/AvaloniaMocks/MockGlyphTypeface.cs +++ b/test/AvaloniaEdit.Tests/AvaloniaMocks/MockGlyphTypeface.cs @@ -6,7 +6,10 @@ namespace AvaloniaEdit.AvaloniaMocks { public class MockGlyphTypeface : IGlyphTypefaceImpl { - public short DesignEmHeight => 10; + public const int GlyphAdvance = 8; + public const short DefaultFontSize = 10; + + public short DesignEmHeight => DefaultFontSize; public int Ascent => 2; public int Descent => 10; public int LineGap { get; } @@ -28,7 +31,7 @@ public ushort[] GetGlyphs(ReadOnlySpan codepoints) public int GetGlyphAdvance(ushort glyph) { - return 8; + return GlyphAdvance; } public int[] GetGlyphAdvances(ReadOnlySpan glyphs) @@ -37,7 +40,7 @@ public int[] GetGlyphAdvances(ReadOnlySpan glyphs) for (var i = 0; i < advances.Length; i++) { - advances[i] = 8; + advances[i] = GlyphAdvance; } return advances; diff --git a/test/AvaloniaEdit.Tests/AvaloniaMocks/TestServices.cs b/test/AvaloniaEdit.Tests/AvaloniaMocks/TestServices.cs index 3fe1fac3..8d881dd2 100644 --- a/test/AvaloniaEdit.Tests/AvaloniaMocks/TestServices.cs +++ b/test/AvaloniaEdit.Tests/AvaloniaMocks/TestServices.cs @@ -75,7 +75,8 @@ public TestServices( IWindowImpl windowImpl = null, IWindowingPlatform windowingPlatform = null, PlatformHotkeyConfiguration platformHotkeyConfiguration = null, - IFontManagerImpl fontManagerImpl = null) + IFontManagerImpl fontManagerImpl = null, + IFormattedTextImpl formattedTextImpl = null) { AssetLoader = assetLoader; FocusManager = focusManager; @@ -95,6 +96,7 @@ public TestServices( WindowingPlatform = windowingPlatform; PlatformHotkeyConfiguration = platformHotkeyConfiguration; FontManagerImpl = fontManagerImpl; + FormattedTextImpl = formattedTextImpl; } public IAssetLoader AssetLoader { get; } @@ -115,6 +117,7 @@ public TestServices( public IWindowingPlatform WindowingPlatform { get; } public PlatformHotkeyConfiguration PlatformHotkeyConfiguration { get; } public IFontManagerImpl FontManagerImpl { get; } + public IFormattedTextImpl FormattedTextImpl { get; } public TestServices With( IAssetLoader assetLoader = null, @@ -135,7 +138,8 @@ public TestServices With( IWindowImpl windowImpl = null, IWindowingPlatform windowingPlatform = null, PlatformHotkeyConfiguration platformHotkeyConfiguration = null, - IFontManagerImpl fontManagerImpl = null) + IFontManagerImpl fontManagerImpl = null, + IFormattedTextImpl formattedTextImpl = null) { return new TestServices( assetLoader: assetLoader ?? AssetLoader, @@ -155,7 +159,8 @@ public TestServices With( windowingPlatform: windowingPlatform ?? WindowingPlatform, windowImpl: windowImpl ?? WindowImpl, platformHotkeyConfiguration: platformHotkeyConfiguration ?? PlatformHotkeyConfiguration, - fontManagerImpl: fontManagerImpl ?? FontManagerImpl); + fontManagerImpl: fontManagerImpl ?? FontManagerImpl, + formattedTextImpl : formattedTextImpl ?? FormattedTextImpl); } private static Styles CreateDefaultTheme() diff --git a/test/AvaloniaEdit.Tests/AvaloniaMocks/UnitTestApplication.cs b/test/AvaloniaEdit.Tests/AvaloniaMocks/UnitTestApplication.cs index e6642639..9c74326b 100644 --- a/test/AvaloniaEdit.Tests/AvaloniaMocks/UnitTestApplication.cs +++ b/test/AvaloniaEdit.Tests/AvaloniaMocks/UnitTestApplication.cs @@ -57,7 +57,8 @@ public override void RegisterServices() .Bind().ToConstant(Services.Styler) .Bind().ToConstant(Services.WindowingPlatform) .Bind().ToConstant(Services.PlatformHotkeyConfiguration) - .Bind().ToConstant(Services.FontManagerImpl); + .Bind().ToConstant(Services.FontManagerImpl) + .Bind().ToConstant(Services.FormattedTextImpl); var styles = Services.Theme?.Invoke(); if (styles != null) diff --git a/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs b/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs new file mode 100644 index 00000000..87335f18 --- /dev/null +++ b/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs @@ -0,0 +1,91 @@ +using Avalonia.Media; +using Avalonia.Platform; + +using AvaloniaEdit.AvaloniaMocks; +using AvaloniaEdit.Rendering; + +using Moq; + +using NUnit.Framework; + +using static AvaloniaEdit.Rendering.SingleCharacterElementGenerator; + +namespace AvaloniaEdit.Text +{ + [TestFixture] + internal class TextLineRunTests + { + [Test] + public void Text_Line_Run_Should_Have_Valid_Glyph_Widths() + { + using var app = UnitTestApplication.Start(new TestServices().With( + renderInterface: new MockPlatformRenderInterface(), + fontManagerImpl: new MockFontManagerImpl(), + formattedTextImpl: Mock.Of())); + + SimpleTextSource s = new SimpleTextSource( + "0123", + CreateDefaultTextProperties()); + + TextLineRun run = TextLineRun.Create(s, 0, 0, 2); + + Assert.AreEqual(MockGlyphTypeface.GlyphAdvance * 0, run.GetDistanceFromCharacter(0)); + Assert.AreEqual(MockGlyphTypeface.GlyphAdvance * 1, run.GetDistanceFromCharacter(1)); + Assert.AreEqual(MockGlyphTypeface.GlyphAdvance * 2, run.GetDistanceFromCharacter(2)); + Assert.AreEqual(MockGlyphTypeface.GlyphAdvance * 3, run.GetDistanceFromCharacter(3)); + } + + [Test] + public void Tab_Line_Run_Should_Have_Fixed_Glyph_Width() + { + using var app = UnitTestApplication.Start(new TestServices().With( + renderInterface: new MockPlatformRenderInterface(), + fontManagerImpl: new MockFontManagerImpl(), + formattedTextImpl: Mock.Of())); + + SimpleTextSource s = new SimpleTextSource( + "\t", + CreateDefaultTextProperties()); + + TextLineRun run = TextLineRun.Create(s, 0, 0, 1); + + Assert.AreEqual(40, run.GetDistanceFromCharacter(1)); + } + + [Test] + public void TextEmbeddedObject_Line_Run_Should_Have_Fixed_Glyph_Width() + { + using var app = UnitTestApplication.Start(new TestServices().With( + renderInterface: new MockPlatformRenderInterface(), + fontManagerImpl: new MockFontManagerImpl(), + formattedTextImpl: Mock.Of())); + + int runWidth = 50; + + TextLine textLine = Mock.Of( + t => t.WidthIncludingTrailingWhitespace == runWidth); + + SpecialCharacterTextRun f = new SpecialCharacterTextRun( + new FormattedTextElement("BEL", 1) { TextLine = textLine }, + CreateDefaultTextProperties()); + + Mock ts = new Mock(); + ts.Setup(s=> s.GetTextRun(It.IsAny())).Returns(f); + + TextLineRun run = TextLineRun.Create(ts.Object, 0, 0, 1); + + Assert.AreEqual( + runWidth + SpecialCharacterTextRun.BoxMargin, + run.GetDistanceFromCharacter(1)); + } + + TextRunProperties CreateDefaultTextProperties() + { + return new TextRunProperties() + { + Typeface = new Typeface("Default"), + FontSize = MockGlyphTypeface.DefaultFontSize, + }; + } + } +} From 23a8252c07fcd1903b2fd731d8294a865fc37327 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sat, 22 Jan 2022 18:48:17 +0100 Subject: [PATCH 06/17] Fixed TextRun length --- test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs b/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs index 87335f18..bb4f261f 100644 --- a/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs +++ b/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs @@ -27,7 +27,7 @@ public void Text_Line_Run_Should_Have_Valid_Glyph_Widths() "0123", CreateDefaultTextProperties()); - TextLineRun run = TextLineRun.Create(s, 0, 0, 2); + TextLineRun run = TextLineRun.Create(s, 0, 0, 4); Assert.AreEqual(MockGlyphTypeface.GlyphAdvance * 0, run.GetDistanceFromCharacter(0)); Assert.AreEqual(MockGlyphTypeface.GlyphAdvance * 1, run.GetDistanceFromCharacter(1)); From 350e7dcd35e61d3b3b58da9fa3c44ee156cb4bdb Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 23 Jan 2022 18:05:08 +0100 Subject: [PATCH 07/17] Fixed TextLineRuns with tab + spaces - Use the tab width from paragraph properties. - Split runs with mixed tabs and spaces in different runs. --- src/AvaloniaEdit/Rendering/TextView.cs | 1 + src/AvaloniaEdit/Text/TextLineImpl.cs | 8 ++-- src/AvaloniaEdit/Text/TextLineRun.cs | 43 ++++++++++++++----- .../Text/TextParagraphProperties.cs | 1 + 4 files changed, 40 insertions(+), 13 deletions(-) diff --git a/src/AvaloniaEdit/Rendering/TextView.cs b/src/AvaloniaEdit/Rendering/TextView.cs index 73a6591a..a9a3fc18 100644 --- a/src/AvaloniaEdit/Rendering/TextView.cs +++ b/src/AvaloniaEdit/Rendering/TextView.cs @@ -1076,6 +1076,7 @@ private TextParagraphProperties CreateParagraphProperties(TextRunProperties defa { DefaultTextRunProperties = defaultTextRunProperties, TextWrapping = _canHorizontallyScroll ? TextWrapping.NoWrap : TextWrapping.Wrap, + WideSpaceWidth = WideSpaceWidth, DefaultIncrementalTab = Options.IndentationSize * WideSpaceWidth }; } diff --git a/src/AvaloniaEdit/Text/TextLineImpl.cs b/src/AvaloniaEdit/Text/TextLineImpl.cs index cfde944b..9c5b7e77 100644 --- a/src/AvaloniaEdit/Text/TextLineImpl.cs +++ b/src/AvaloniaEdit/Text/TextLineImpl.cs @@ -11,6 +11,7 @@ internal sealed class TextLineImpl : TextLine { private readonly TextLineRun[] _runs; + public TextLineRun[] LineRuns { get { return _runs; } } public override int FirstIndex { get; } public override int Length { get; } @@ -31,13 +32,14 @@ internal static TextLineImpl Create(TextParagraphProperties paragraphProperties, var visibleLength = 0; var widthLeft = paragraphProperties.TextWrapping == TextWrapping.Wrap && paragraphLength > 0 ? paragraphLength : double.MaxValue; TextLineRun prevRun = null; - var run = TextLineRun.Create(textSource, index, firstIndex, widthLeft); + var run = TextLineRun.Create(textSource, index, firstIndex, widthLeft, paragraphProperties); + if (!run.IsEnd && run.Width <= widthLeft) { index += run.Length; widthLeft -= run.Width; prevRun = run; - run = TextLineRun.Create(textSource, index, firstIndex, widthLeft); + run = TextLineRun.Create(textSource, index, firstIndex, widthLeft, paragraphProperties); } var trailing = new TrailingInfo(); @@ -59,7 +61,7 @@ internal static TextLineImpl Create(TextParagraphProperties paragraphProperties, return new TextLineImpl(paragraphProperties, firstIndex, runs, trailing); } - run = TextLineRun.Create(textSource, index, firstIndex, widthLeft); + run = TextLineRun.Create(textSource, index, firstIndex, widthLeft, paragraphProperties); } } diff --git a/src/AvaloniaEdit/Text/TextLineRun.cs b/src/AvaloniaEdit/Text/TextLineRun.cs index 49038c24..b2e6c43c 100644 --- a/src/AvaloniaEdit/Text/TextLineRun.cs +++ b/src/AvaloniaEdit/Text/TextLineRun.cs @@ -14,6 +14,7 @@ namespace AvaloniaEdit.Text internal sealed class TextLineRun { private const string NewlineString = "\r\n"; + private const string TabString = "\t"; private FormattedText _formattedText; private Size _formattedTextSize; @@ -88,18 +89,18 @@ private TextLineRun() { } - public static TextLineRun Create(TextSource textSource, int index, int firstIndex, double lengthLeft) + public static TextLineRun Create(TextSource textSource, int index, int firstIndex, double lengthLeft, TextParagraphProperties paragraphProperties) { var textRun = textSource.GetTextRun(index); var stringRange = textRun.GetStringRange(); - return Create(textSource, stringRange, textRun, index, lengthLeft); + return Create(textSource, stringRange, textRun, index, lengthLeft, paragraphProperties); } - private static TextLineRun Create(TextSource textSource, StringRange stringRange, TextRun textRun, int index, double widthLeft) + private static TextLineRun Create(TextSource textSource, StringRange stringRange, TextRun textRun, int index, double widthLeft, TextParagraphProperties paragraphProperties) { if (textRun is TextCharacters) { - return CreateRunForEol(textSource, stringRange, textRun, index) ?? + return CreateRunForSpecialChars(textSource, stringRange, textRun, index, paragraphProperties) ?? CreateRunForText(stringRange, textRun, widthLeft, false, true); } @@ -124,7 +125,7 @@ private static TextLineRun Create(TextSource textSource, StringRange stringRange throw new NotSupportedException("Unsupported run type"); } - private static TextLineRun CreateRunForEol(TextSource textSource, StringRange stringRange, TextRun textRun, int index) + private static TextLineRun CreateRunForSpecialChars(TextSource textSource, StringRange stringRange, TextRun textRun, int index, TextParagraphProperties paragraphProperties) { switch (stringRange[0]) { @@ -149,22 +150,23 @@ private static TextLineRun CreateRunForEol(TextSource textSource, StringRange st case '\n': return new TextLineRun(1, textRun) { IsEnd = true }; case '\t': - return CreateRunForTab(textRun); + return CreateRunForTab(textRun, paragraphProperties); + case ' ': + return CreateRunForSpaceBlock(textRun, stringRange, paragraphProperties); default: return null; } } - private static TextLineRun CreateRunForTab(TextRun textRun) + private static TextLineRun CreateRunForTab(TextRun textRun, TextParagraphProperties paragraphProperties) { - var spaceRun = new TextCharacters(" ", textRun.Properties); + var spaceRun = new TextCharacters(TabString, textRun.Properties); var stringRange = spaceRun.StringRange; var run = new TextLineRun(1, spaceRun) { IsTab = true, StringRange = stringRange, - // TODO: get from para props - Width = 40 + Width = paragraphProperties.DefaultIncrementalTab }; run._glyphWidths = new double[] { run.Width }; @@ -172,6 +174,27 @@ private static TextLineRun CreateRunForTab(TextRun textRun) return run; } + private static TextLineRun CreateRunForSpaceBlock(TextRun textRun, StringRange stringRange, TextParagraphProperties paragraphProperties) + { + int blockLength = 0; + while (blockLength < stringRange.Length && stringRange[blockLength] == ' ') + blockLength++; + + var run = new TextLineRun(blockLength, textRun) + { + IsTab = false, + StringRange = stringRange, + Width = paragraphProperties.WideSpaceWidth * blockLength, + }; + + run._glyphWidths = Enumerable.Repeat( + paragraphProperties.WideSpaceWidth, + blockLength) + .ToList(); + + return run; + } + internal static TextLineRun CreateRunForText(StringRange stringRange, TextRun textRun, double widthLeft, bool emergencyWrap, bool breakOnTabs) { var run = new TextLineRun diff --git a/src/AvaloniaEdit/Text/TextParagraphProperties.cs b/src/AvaloniaEdit/Text/TextParagraphProperties.cs index fef4ebd1..6e5d3ecb 100644 --- a/src/AvaloniaEdit/Text/TextParagraphProperties.cs +++ b/src/AvaloniaEdit/Text/TextParagraphProperties.cs @@ -4,6 +4,7 @@ namespace AvaloniaEdit.Text { public sealed class TextParagraphProperties { + public double WideSpaceWidth { get; set; } public double DefaultIncrementalTab { get; set; } public bool FirstLineInParagraph { get; set; } From afeae9c66d357d5c1930e47256ade93a5a1d6ef8 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 23 Jan 2022 18:05:29 +0100 Subject: [PATCH 08/17] Added unit tests --- .../Text/TextLineImplTests.cs | 124 ++++++++++++++++++ .../Text/TextLineRunTests.cs | 20 ++- 2 files changed, 140 insertions(+), 4 deletions(-) create mode 100644 test/AvaloniaEdit.Tests/Text/TextLineImplTests.cs diff --git a/test/AvaloniaEdit.Tests/Text/TextLineImplTests.cs b/test/AvaloniaEdit.Tests/Text/TextLineImplTests.cs new file mode 100644 index 00000000..63ed3a26 --- /dev/null +++ b/test/AvaloniaEdit.Tests/Text/TextLineImplTests.cs @@ -0,0 +1,124 @@ +using Avalonia.Media; +using Avalonia.Platform; + +using AvaloniaEdit.AvaloniaMocks; +using AvaloniaEdit.Rendering; + +using Moq; + +using NUnit.Framework; + +namespace AvaloniaEdit.Text +{ + [TestFixture] + internal class TextLineImplTests + { + [Test] + public void Text_Line_Should_Generate_Text_Runs() + { + using var app = UnitTestApplication.Start(new TestServices().With( + renderInterface: new MockPlatformRenderInterface(), + fontManagerImpl: new MockFontManagerImpl(), + formattedTextImpl: Mock.Of())); + + SimpleTextSource textSource = new SimpleTextSource("hello", CreateDefaultTextProperties()); + + TextLineImpl textLine = TextLineImpl.Create( + CreateDefaultParagraphProperties(), 0, 5, textSource); + + Assert.AreEqual(2, textLine.LineRuns.Length); + Assert.AreEqual("hello", textLine.LineRuns[0].StringRange.String); + Assert.IsTrue(textLine.LineRuns[1].IsEnd); + } + + [Test] + public void Space_Block_With_Tab_Should_Split_Runs() + { + using var app = UnitTestApplication.Start(new TestServices().With( + renderInterface: new MockPlatformRenderInterface(), + fontManagerImpl: new MockFontManagerImpl(), + formattedTextImpl: Mock.Of())); + + SimpleTextSource s = new SimpleTextSource( + " \t ", + CreateDefaultTextProperties()); + + TextLineImpl textLine = TextLineImpl.Create( + CreateDefaultParagraphProperties(), 0, 5, s); + + Assert.AreEqual(4, textLine.LineRuns.Length); + + Assert.AreEqual(textLine.LineRuns[0].Length, 4); + Assert.IsTrue(textLine.LineRuns[1].IsTab); + Assert.AreEqual(textLine.LineRuns[2].Length, 4); + Assert.IsTrue(textLine.LineRuns[3].IsEnd); + } + + [Test] + public void Tab_Block_Should_Split_Runs() + { + using var app = UnitTestApplication.Start(new TestServices().With( + renderInterface: new MockPlatformRenderInterface(), + fontManagerImpl: new MockFontManagerImpl(), + formattedTextImpl: Mock.Of())); + + SimpleTextSource s = new SimpleTextSource( + "\t\t", + CreateDefaultTextProperties()); + + TextLineImpl textLine = TextLineImpl.Create( + CreateDefaultParagraphProperties(), 0, 2, s); + + var textRuns = textLine.GetTextRuns(); + + Assert.AreEqual(3, textRuns.Count); + Assert.IsTrue(textLine.LineRuns[0].IsTab); + Assert.IsTrue(textLine.LineRuns[1].IsTab); + Assert.IsTrue(textLine.LineRuns[2].IsEnd); + } + + [Test] + public void Tab_Block_With_Spaces_At_The_End_Should_Split_Runs() + { + using var app = UnitTestApplication.Start(new TestServices().With( + renderInterface: new MockPlatformRenderInterface(), + fontManagerImpl: new MockFontManagerImpl(), + formattedTextImpl: Mock.Of())); + + SimpleTextSource s = new SimpleTextSource( + "\t\t ", + CreateDefaultTextProperties()); + + TextLineImpl textLine = TextLineImpl.Create( + CreateDefaultParagraphProperties(), 0, 2, s); + + var textRuns = textLine.GetTextRuns(); + + Assert.AreEqual(4, textRuns.Count); + Assert.IsTrue(textLine.LineRuns[0].IsTab); + Assert.IsTrue(textLine.LineRuns[1].IsTab); + Assert.AreEqual(textLine.LineRuns[2].Length, 4); + Assert.IsTrue(textLine.LineRuns[3].IsEnd); + } + + + TextParagraphProperties CreateDefaultParagraphProperties() + { + return new TextParagraphProperties() + { + DefaultTextRunProperties = CreateDefaultTextProperties(), + DefaultIncrementalTab = 70, + Indent = 4, + }; + } + + TextRunProperties CreateDefaultTextProperties() + { + return new TextRunProperties() + { + Typeface = new Typeface("Default"), + FontSize = MockGlyphTypeface.DefaultFontSize, + }; + } + } +} diff --git a/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs b/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs index bb4f261f..435ed0aa 100644 --- a/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs +++ b/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs @@ -27,7 +27,7 @@ public void Text_Line_Run_Should_Have_Valid_Glyph_Widths() "0123", CreateDefaultTextProperties()); - TextLineRun run = TextLineRun.Create(s, 0, 0, 4); + TextLineRun run = TextLineRun.Create(s, 0, 0, 4, CreateDefaultParagraphProperties()); Assert.AreEqual(MockGlyphTypeface.GlyphAdvance * 0, run.GetDistanceFromCharacter(0)); Assert.AreEqual(MockGlyphTypeface.GlyphAdvance * 1, run.GetDistanceFromCharacter(1)); @@ -47,9 +47,11 @@ public void Tab_Line_Run_Should_Have_Fixed_Glyph_Width() "\t", CreateDefaultTextProperties()); - TextLineRun run = TextLineRun.Create(s, 0, 0, 1); + var paragraphProperties = CreateDefaultParagraphProperties(); - Assert.AreEqual(40, run.GetDistanceFromCharacter(1)); + TextLineRun run = TextLineRun.Create(s, 0, 0, 1, paragraphProperties); + + Assert.AreEqual(paragraphProperties.DefaultIncrementalTab, run.GetDistanceFromCharacter(1)); } [Test] @@ -72,7 +74,7 @@ public void TextEmbeddedObject_Line_Run_Should_Have_Fixed_Glyph_Width() Mock ts = new Mock(); ts.Setup(s=> s.GetTextRun(It.IsAny())).Returns(f); - TextLineRun run = TextLineRun.Create(ts.Object, 0, 0, 1); + TextLineRun run = TextLineRun.Create(ts.Object, 0, 0, 1, CreateDefaultParagraphProperties()); Assert.AreEqual( runWidth + SpecialCharacterTextRun.BoxMargin, @@ -87,5 +89,15 @@ TextRunProperties CreateDefaultTextProperties() FontSize = MockGlyphTypeface.DefaultFontSize, }; } + + TextParagraphProperties CreateDefaultParagraphProperties() + { + return new TextParagraphProperties() + { + DefaultTextRunProperties = CreateDefaultTextProperties(), + DefaultIncrementalTab = 70, + Indent = 4, + }; + } } } From ae5c53548cdedf9f124c05c33a55dda4bcea9e25 Mon Sep 17 00:00:00 2001 From: Daniel Date: Sun, 23 Jan 2022 18:25:39 +0100 Subject: [PATCH 09/17] Set the correct StringRange to the TextLineRun when splitting space blocks --- src/AvaloniaEdit/Text/StringRange.cs | 5 ++++ src/AvaloniaEdit/Text/TextLineRun.cs | 2 +- .../Text/TextLineImplTests.cs | 27 +++++++++++++++++-- 3 files changed, 31 insertions(+), 3 deletions(-) diff --git a/src/AvaloniaEdit/Text/StringRange.cs b/src/AvaloniaEdit/Text/StringRange.cs index a27cf081..7654ad4a 100644 --- a/src/AvaloniaEdit/Text/StringRange.cs +++ b/src/AvaloniaEdit/Text/StringRange.cs @@ -63,5 +63,10 @@ public override int GetHashCode() { return !left.Equals(right); } + + internal StringRange WithLength(int length) + { + return new StringRange(String, OffsetToFirstChar, length); + } } } \ No newline at end of file diff --git a/src/AvaloniaEdit/Text/TextLineRun.cs b/src/AvaloniaEdit/Text/TextLineRun.cs index b2e6c43c..7e878963 100644 --- a/src/AvaloniaEdit/Text/TextLineRun.cs +++ b/src/AvaloniaEdit/Text/TextLineRun.cs @@ -183,7 +183,7 @@ private static TextLineRun CreateRunForSpaceBlock(TextRun textRun, StringRange s var run = new TextLineRun(blockLength, textRun) { IsTab = false, - StringRange = stringRange, + StringRange = stringRange.WithLength(blockLength), Width = paragraphProperties.WideSpaceWidth * blockLength, }; diff --git a/test/AvaloniaEdit.Tests/Text/TextLineImplTests.cs b/test/AvaloniaEdit.Tests/Text/TextLineImplTests.cs index 63ed3a26..88022821 100644 --- a/test/AvaloniaEdit.Tests/Text/TextLineImplTests.cs +++ b/test/AvaloniaEdit.Tests/Text/TextLineImplTests.cs @@ -27,7 +27,7 @@ public void Text_Line_Should_Generate_Text_Runs() CreateDefaultParagraphProperties(), 0, 5, textSource); Assert.AreEqual(2, textLine.LineRuns.Length); - Assert.AreEqual("hello", textLine.LineRuns[0].StringRange.String); + Assert.AreEqual("hello", textLine.LineRuns[0].StringRange.ToString()); Assert.IsTrue(textLine.LineRuns[1].IsEnd); } @@ -54,6 +54,29 @@ public void Space_Block_With_Tab_Should_Split_Runs() Assert.IsTrue(textLine.LineRuns[3].IsEnd); } + [Test] + public void Space_Block_With_Tab_And_Text_Should_Split_Runs() + { + using var app = UnitTestApplication.Start(new TestServices().With( + renderInterface: new MockPlatformRenderInterface(), + fontManagerImpl: new MockFontManagerImpl(), + formattedTextImpl: Mock.Of())); + + SimpleTextSource s = new SimpleTextSource( + " \thello", + CreateDefaultTextProperties()); + + TextLineImpl textLine = TextLineImpl.Create( + CreateDefaultParagraphProperties(), 0, 5, s); + + Assert.AreEqual(4, textLine.LineRuns.Length); + + Assert.AreEqual(" ", textLine.LineRuns[0].StringRange.ToString()); + Assert.IsTrue(textLine.LineRuns[1].IsTab); + Assert.AreEqual("hello", textLine.LineRuns[2].StringRange.ToString()); + Assert.IsTrue(textLine.LineRuns[3].IsEnd); + } + [Test] public void Tab_Block_Should_Split_Runs() { @@ -97,7 +120,7 @@ public void Tab_Block_With_Spaces_At_The_End_Should_Split_Runs() Assert.AreEqual(4, textRuns.Count); Assert.IsTrue(textLine.LineRuns[0].IsTab); Assert.IsTrue(textLine.LineRuns[1].IsTab); - Assert.AreEqual(textLine.LineRuns[2].Length, 4); + Assert.AreEqual(" ", textLine.LineRuns[2].StringRange.ToString()); Assert.IsTrue(textLine.LineRuns[3].IsEnd); } From 4a1e572acd42026c6d5bf73d736d02277c8fa6d7 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 24 Jan 2022 09:39:40 +0100 Subject: [PATCH 10/17] Do not split text runs when not needed --- src/AvaloniaEdit/Text/TextLineRun.cs | 11 +++++++--- .../Text/TextLineImplTests.cs | 20 +++++++++++++++++++ 2 files changed, 28 insertions(+), 3 deletions(-) diff --git a/src/AvaloniaEdit/Text/TextLineRun.cs b/src/AvaloniaEdit/Text/TextLineRun.cs index 7e878963..d87e787c 100644 --- a/src/AvaloniaEdit/Text/TextLineRun.cs +++ b/src/AvaloniaEdit/Text/TextLineRun.cs @@ -152,7 +152,7 @@ private static TextLineRun CreateRunForSpecialChars(TextSource textSource, Strin case '\t': return CreateRunForTab(textRun, paragraphProperties); case ' ': - return CreateRunForSpaceBlock(textRun, stringRange, paragraphProperties); + return CreateRunForMixedSpaceAndTabs(textRun, stringRange, paragraphProperties); default: return null; } @@ -174,12 +174,17 @@ private static TextLineRun CreateRunForTab(TextRun textRun, TextParagraphPropert return run; } - private static TextLineRun CreateRunForSpaceBlock(TextRun textRun, StringRange stringRange, TextParagraphProperties paragraphProperties) + private static TextLineRun CreateRunForMixedSpaceAndTabs(TextRun textRun, StringRange stringRange, TextParagraphProperties paragraphProperties) { int blockLength = 0; - while (blockLength < stringRange.Length && stringRange[blockLength] == ' ') + while (blockLength < stringRange.Length && stringRange[blockLength] != '\t') blockLength++; + bool foundTab = blockLength < stringRange.Length; + + if (!foundTab) + return null; + var run = new TextLineRun(blockLength, textRun) { IsTab = false, diff --git a/test/AvaloniaEdit.Tests/Text/TextLineImplTests.cs b/test/AvaloniaEdit.Tests/Text/TextLineImplTests.cs index 88022821..1929731c 100644 --- a/test/AvaloniaEdit.Tests/Text/TextLineImplTests.cs +++ b/test/AvaloniaEdit.Tests/Text/TextLineImplTests.cs @@ -124,6 +124,26 @@ public void Tab_Block_With_Spaces_At_The_End_Should_Split_Runs() Assert.IsTrue(textLine.LineRuns[3].IsEnd); } + [Test] + public void Space_Block_Without_Tab_Should_Not_Split_Runs() + { + using var app = UnitTestApplication.Start(new TestServices().With( + renderInterface: new MockPlatformRenderInterface(), + fontManagerImpl: new MockFontManagerImpl(), + formattedTextImpl: Mock.Of())); + + SimpleTextSource s = new SimpleTextSource( + " hello", + CreateDefaultTextProperties()); + + TextLineImpl textLine = TextLineImpl.Create( + CreateDefaultParagraphProperties(), 0, 9, s); + + Assert.AreEqual(2, textLine.LineRuns.Length); + + Assert.AreEqual(" hello", textLine.LineRuns[0].StringRange.ToString()); + Assert.IsTrue(textLine.LineRuns[1].IsEnd); + } TextParagraphProperties CreateDefaultParagraphProperties() { From 61177db7ae1de6fc55aaf1848d39bd691f9b959f Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 24 Jan 2022 10:33:05 +0100 Subject: [PATCH 11/17] Fixed wrong condition --- src/AvaloniaEdit/Text/TextLineRun.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AvaloniaEdit/Text/TextLineRun.cs b/src/AvaloniaEdit/Text/TextLineRun.cs index d87e787c..18924595 100644 --- a/src/AvaloniaEdit/Text/TextLineRun.cs +++ b/src/AvaloniaEdit/Text/TextLineRun.cs @@ -177,10 +177,10 @@ private static TextLineRun CreateRunForTab(TextRun textRun, TextParagraphPropert private static TextLineRun CreateRunForMixedSpaceAndTabs(TextRun textRun, StringRange stringRange, TextParagraphProperties paragraphProperties) { int blockLength = 0; - while (blockLength < stringRange.Length && stringRange[blockLength] != '\t') + while (blockLength < stringRange.Length && stringRange[blockLength] == ' ') blockLength++; - bool foundTab = blockLength < stringRange.Length; + bool foundTab = blockLength < stringRange.Length && stringRange[blockLength] == '\t'; if (!foundTab) return null; From 3b4a5890cbd0a6697b8938be1b6717d55027b6cb Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 24 Jan 2022 10:59:05 +0100 Subject: [PATCH 12/17] Implement a simpler mechanism to manage tabs Just return the tab width when measuring it, so I avoid splitting runs, that is much more complex. --- src/AvaloniaEdit/Text/TextLineRun.cs | 42 ++++---------- .../Text/TextLineImplTests.cs | 46 --------------- .../Text/TextLineRunTests.cs | 56 +++++++++++++++++++ 3 files changed, 66 insertions(+), 78 deletions(-) diff --git a/src/AvaloniaEdit/Text/TextLineRun.cs b/src/AvaloniaEdit/Text/TextLineRun.cs index 18924595..44f54cac 100644 --- a/src/AvaloniaEdit/Text/TextLineRun.cs +++ b/src/AvaloniaEdit/Text/TextLineRun.cs @@ -101,7 +101,7 @@ private static TextLineRun Create(TextSource textSource, StringRange stringRange if (textRun is TextCharacters) { return CreateRunForSpecialChars(textSource, stringRange, textRun, index, paragraphProperties) ?? - CreateRunForText(stringRange, textRun, widthLeft, false, true); + CreateRunForText(stringRange, textRun, widthLeft, false, true, paragraphProperties); } if (textRun is TextEndOfLine) @@ -151,8 +151,6 @@ private static TextLineRun CreateRunForSpecialChars(TextSource textSource, Strin return new TextLineRun(1, textRun) { IsEnd = true }; case '\t': return CreateRunForTab(textRun, paragraphProperties); - case ' ': - return CreateRunForMixedSpaceAndTabs(textRun, stringRange, paragraphProperties); default: return null; } @@ -174,33 +172,7 @@ private static TextLineRun CreateRunForTab(TextRun textRun, TextParagraphPropert return run; } - private static TextLineRun CreateRunForMixedSpaceAndTabs(TextRun textRun, StringRange stringRange, TextParagraphProperties paragraphProperties) - { - int blockLength = 0; - while (blockLength < stringRange.Length && stringRange[blockLength] == ' ') - blockLength++; - - bool foundTab = blockLength < stringRange.Length && stringRange[blockLength] == '\t'; - - if (!foundTab) - return null; - - var run = new TextLineRun(blockLength, textRun) - { - IsTab = false, - StringRange = stringRange.WithLength(blockLength), - Width = paragraphProperties.WideSpaceWidth * blockLength, - }; - - run._glyphWidths = Enumerable.Repeat( - paragraphProperties.WideSpaceWidth, - blockLength) - .ToList(); - - return run; - } - - internal static TextLineRun CreateRunForText(StringRange stringRange, TextRun textRun, double widthLeft, bool emergencyWrap, bool breakOnTabs) + internal static TextLineRun CreateRunForText(StringRange stringRange, TextRun textRun, double widthLeft, bool emergencyWrap, bool breakOnTabs, TextParagraphProperties paragraphProperties) { var run = new TextLineRun { @@ -228,7 +200,8 @@ internal static TextLineRun CreateRunForText(StringRange stringRange, TextRun te run._glyphWidths = new GlyphWidths( run.StringRange, run.Typeface.GlyphTypeface, - run.FontSize); + run.FontSize, + paragraphProperties.DefaultIncrementalTab); return run; } @@ -373,15 +346,17 @@ class GlyphWidths : IReadOnlyList private GlyphTypeface _typeFace; private StringRange _range; private double _scale; + private double _tabSize; public int Count => _glyphWidths.Length; public double this[int index] => GetAt(index); - internal GlyphWidths(StringRange range, GlyphTypeface typeFace, double fontSize) + internal GlyphWidths(StringRange range, GlyphTypeface typeFace, double fontSize, double tabSize) { _range = range; _typeFace = typeFace; _scale = fontSize / _typeFace.DesignEmHeight; + _tabSize = tabSize; InitGlyphWidths(); } @@ -391,6 +366,9 @@ double GetAt(int index) if (_glyphWidths.Length == 0) return 0; + if (_range[index] == '\t') + return _tabSize; + if (_glyphWidths[index] == NOT_CALCULATED_YET) _glyphWidths[index] = MeasureGlyphAt(index); diff --git a/test/AvaloniaEdit.Tests/Text/TextLineImplTests.cs b/test/AvaloniaEdit.Tests/Text/TextLineImplTests.cs index 1929731c..a435ca43 100644 --- a/test/AvaloniaEdit.Tests/Text/TextLineImplTests.cs +++ b/test/AvaloniaEdit.Tests/Text/TextLineImplTests.cs @@ -31,52 +31,6 @@ public void Text_Line_Should_Generate_Text_Runs() Assert.IsTrue(textLine.LineRuns[1].IsEnd); } - [Test] - public void Space_Block_With_Tab_Should_Split_Runs() - { - using var app = UnitTestApplication.Start(new TestServices().With( - renderInterface: new MockPlatformRenderInterface(), - fontManagerImpl: new MockFontManagerImpl(), - formattedTextImpl: Mock.Of())); - - SimpleTextSource s = new SimpleTextSource( - " \t ", - CreateDefaultTextProperties()); - - TextLineImpl textLine = TextLineImpl.Create( - CreateDefaultParagraphProperties(), 0, 5, s); - - Assert.AreEqual(4, textLine.LineRuns.Length); - - Assert.AreEqual(textLine.LineRuns[0].Length, 4); - Assert.IsTrue(textLine.LineRuns[1].IsTab); - Assert.AreEqual(textLine.LineRuns[2].Length, 4); - Assert.IsTrue(textLine.LineRuns[3].IsEnd); - } - - [Test] - public void Space_Block_With_Tab_And_Text_Should_Split_Runs() - { - using var app = UnitTestApplication.Start(new TestServices().With( - renderInterface: new MockPlatformRenderInterface(), - fontManagerImpl: new MockFontManagerImpl(), - formattedTextImpl: Mock.Of())); - - SimpleTextSource s = new SimpleTextSource( - " \thello", - CreateDefaultTextProperties()); - - TextLineImpl textLine = TextLineImpl.Create( - CreateDefaultParagraphProperties(), 0, 5, s); - - Assert.AreEqual(4, textLine.LineRuns.Length); - - Assert.AreEqual(" ", textLine.LineRuns[0].StringRange.ToString()); - Assert.IsTrue(textLine.LineRuns[1].IsTab); - Assert.AreEqual("hello", textLine.LineRuns[2].StringRange.ToString()); - Assert.IsTrue(textLine.LineRuns[3].IsEnd); - } - [Test] public void Tab_Block_Should_Split_Runs() { diff --git a/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs b/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs index 435ed0aa..e4fd1aa1 100644 --- a/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs +++ b/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs @@ -54,6 +54,62 @@ public void Tab_Line_Run_Should_Have_Fixed_Glyph_Width() Assert.AreEqual(paragraphProperties.DefaultIncrementalTab, run.GetDistanceFromCharacter(1)); } + [Test] + public void Spaces_Plus_Tab_Line_Run_Should_Have_Correct_Glyph_Widths() + { + using var app = UnitTestApplication.Start(new TestServices().With( + renderInterface: new MockPlatformRenderInterface(), + fontManagerImpl: new MockFontManagerImpl(), + formattedTextImpl: Mock.Of())); + + SimpleTextSource s = new SimpleTextSource( + " \t ", + CreateDefaultTextProperties()); + + var paragraphProperties = CreateDefaultParagraphProperties(); + + TextLineRun run = TextLineRun.Create(s, 0, 0, 1, paragraphProperties); + + double[] expectedLengths = new double[] + { + 0, + MockGlyphTypeface.GlyphAdvance * 1, + MockGlyphTypeface.GlyphAdvance * 1 + paragraphProperties.DefaultIncrementalTab, + MockGlyphTypeface.GlyphAdvance * 2 + paragraphProperties.DefaultIncrementalTab + }; + + for (int i = 0; i < 4; i++) + Assert.AreEqual(expectedLengths[i], run.GetDistanceFromCharacter(i)); + } + + [Test] + public void Chars_Plus_Tab_Line_Run_Should_Have_Correct_Glyph_Widths() + { + using var app = UnitTestApplication.Start(new TestServices().With( + renderInterface: new MockPlatformRenderInterface(), + fontManagerImpl: new MockFontManagerImpl(), + formattedTextImpl: Mock.Of())); + + SimpleTextSource s = new SimpleTextSource( + "a\ta", + CreateDefaultTextProperties()); + + var paragraphProperties = CreateDefaultParagraphProperties(); + + TextLineRun run = TextLineRun.Create(s, 0, 0, 1, paragraphProperties); + + double[] expectedLengths = new double[] + { + 0, + MockGlyphTypeface.GlyphAdvance * 1, + MockGlyphTypeface.GlyphAdvance * 1 + paragraphProperties.DefaultIncrementalTab, + MockGlyphTypeface.GlyphAdvance * 2 + paragraphProperties.DefaultIncrementalTab + }; + + for (int i = 0; i < 4; i++) + Assert.AreEqual(expectedLengths[i], run.GetDistanceFromCharacter(i)); + } + [Test] public void TextEmbeddedObject_Line_Run_Should_Have_Fixed_Glyph_Width() { From b5c6078128dc11f7712e3158d388d0bca4a53cd0 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 24 Jan 2022 14:29:39 +0100 Subject: [PATCH 13/17] Remove unneeded property --- src/AvaloniaEdit/Rendering/TextView.cs | 1 - src/AvaloniaEdit/Text/TextParagraphProperties.cs | 1 - 2 files changed, 2 deletions(-) diff --git a/src/AvaloniaEdit/Rendering/TextView.cs b/src/AvaloniaEdit/Rendering/TextView.cs index a9a3fc18..73a6591a 100644 --- a/src/AvaloniaEdit/Rendering/TextView.cs +++ b/src/AvaloniaEdit/Rendering/TextView.cs @@ -1076,7 +1076,6 @@ private TextParagraphProperties CreateParagraphProperties(TextRunProperties defa { DefaultTextRunProperties = defaultTextRunProperties, TextWrapping = _canHorizontallyScroll ? TextWrapping.NoWrap : TextWrapping.Wrap, - WideSpaceWidth = WideSpaceWidth, DefaultIncrementalTab = Options.IndentationSize * WideSpaceWidth }; } diff --git a/src/AvaloniaEdit/Text/TextParagraphProperties.cs b/src/AvaloniaEdit/Text/TextParagraphProperties.cs index 6e5d3ecb..fef4ebd1 100644 --- a/src/AvaloniaEdit/Text/TextParagraphProperties.cs +++ b/src/AvaloniaEdit/Text/TextParagraphProperties.cs @@ -4,7 +4,6 @@ namespace AvaloniaEdit.Text { public sealed class TextParagraphProperties { - public double WideSpaceWidth { get; set; } public double DefaultIncrementalTab { get; set; } public bool FirstLineInParagraph { get; set; } From 3a4bf74586e74d6ea27ef7e46b0cd5149ba2ee24 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 24 Jan 2022 14:30:34 +0100 Subject: [PATCH 14/17] Removed unneeded method --- src/AvaloniaEdit/Text/StringRange.cs | 5 ----- 1 file changed, 5 deletions(-) diff --git a/src/AvaloniaEdit/Text/StringRange.cs b/src/AvaloniaEdit/Text/StringRange.cs index 7654ad4a..a27cf081 100644 --- a/src/AvaloniaEdit/Text/StringRange.cs +++ b/src/AvaloniaEdit/Text/StringRange.cs @@ -63,10 +63,5 @@ public override int GetHashCode() { return !left.Equals(right); } - - internal StringRange WithLength(int length) - { - return new StringRange(String, OffsetToFirstChar, length); - } } } \ No newline at end of file From ef6d3446a972356c124ae0f9941e085f0a8b3026 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 24 Jan 2022 14:33:39 +0100 Subject: [PATCH 15/17] Use a better variable name --- src/AvaloniaEdit/Text/TextLineRun.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AvaloniaEdit/Text/TextLineRun.cs b/src/AvaloniaEdit/Text/TextLineRun.cs index 44f54cac..eac67865 100644 --- a/src/AvaloniaEdit/Text/TextLineRun.cs +++ b/src/AvaloniaEdit/Text/TextLineRun.cs @@ -158,9 +158,9 @@ private static TextLineRun CreateRunForSpecialChars(TextSource textSource, Strin private static TextLineRun CreateRunForTab(TextRun textRun, TextParagraphProperties paragraphProperties) { - var spaceRun = new TextCharacters(TabString, textRun.Properties); - var stringRange = spaceRun.StringRange; - var run = new TextLineRun(1, spaceRun) + var tabRun = new TextCharacters(TabString, textRun.Properties); + var stringRange = tabRun.StringRange; + var run = new TextLineRun(1, tabRun) { IsTab = true, StringRange = stringRange, From aca289f4442bbbb7b557ed0b8993abc2194868a2 Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 24 Jan 2022 20:11:35 +0100 Subject: [PATCH 16/17] Removed unused using --- src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs index 9ce7d55c..05ec1ef6 100644 --- a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs +++ b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs @@ -18,7 +18,6 @@ using System; using System.Diagnostics.CodeAnalysis; -using System.Runtime.CompilerServices; using Avalonia; using Avalonia.Media; From 2a0df15223c44464fa675bdcbe727e09bc31e4eb Mon Sep 17 00:00:00 2001 From: Daniel Date: Mon, 24 Jan 2022 20:12:23 +0100 Subject: [PATCH 17/17] Removed extra line --- src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs index 05ec1ef6..c6fac5e5 100644 --- a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs +++ b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs @@ -18,7 +18,6 @@ using System; using System.Diagnostics.CodeAnalysis; - using Avalonia; using Avalonia.Media; using Avalonia.Media.Immutable;