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) diff --git a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs index a6598691..c6fac5e5 100644 --- a/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs +++ b/src/AvaloniaEdit/Rendering/SingleCharacterElementGenerator.cs @@ -223,13 +223,15 @@ public override TextRun CreateTextRun(int startVisualColumn, ITextRunConstructio } } - private sealed class SpecialCharacterTextRun : FormattedTextRun + internal sealed class SpecialCharacterTextRun : FormattedTextRun { private static readonly ISolidColorBrush DarkGrayBrush; + internal const double BoxMargin = 3; + 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 +239,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); } 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 e30038eb..eac67865 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; @@ -12,10 +14,11 @@ namespace AvaloniaEdit.Text internal sealed class TextLineRun { private const string NewlineString = "\r\n"; + private const string TabString = "\t"; private FormattedText _formattedText; private Size _formattedTextSize; - private GlyphWidths _glyphWidths; + private IReadOnlyList _glyphWidths; public StringRange StringRange { get; private set; } public int Length { get; set; } @@ -86,19 +89,19 @@ 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) ?? - CreateRunForText(stringRange, textRun, widthLeft, false, true); + return CreateRunForSpecialChars(textSource, stringRange, textRun, index, paragraphProperties) ?? + CreateRunForText(stringRange, textRun, widthLeft, false, true, paragraphProperties); } if (textRun is TextEndOfLine) @@ -112,10 +115,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 @@ -125,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]) { @@ -150,33 +150,29 @@ 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); default: return null; } } - private static TextLineRun CreateRunForTab(TextRun textRun) + private static TextLineRun CreateRunForTab(TextRun textRun, TextParagraphProperties paragraphProperties) { - var spaceRun = new TextCharacters(" ", 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, - // TODO: get from para props - Width = 40 + Width = paragraphProperties.DefaultIncrementalTab }; - run._glyphWidths = new GlyphWidths( - run.StringRange, - run.Typeface.GlyphTypeface, - run.FontSize); + run._glyphWidths = new double[] { run.Width }; 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 { @@ -204,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; } @@ -281,7 +278,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 +301,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 +320,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,25 +339,36 @@ 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; private GlyphTypeface _typeFace; private StringRange _range; private double _scale; + private double _tabSize; - internal GlyphWidths(StringRange range, GlyphTypeface typeFace, double fontSize) + public int Count => _glyphWidths.Length; + public double this[int index] => GetAt(index); + + internal GlyphWidths(StringRange range, GlyphTypeface typeFace, double fontSize, double tabSize) { _range = range; _typeFace = typeFace; _scale = fontSize / _typeFace.DesignEmHeight; + _tabSize = tabSize; InitGlyphWidths(); } - internal double GetAt(int index) + 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); @@ -390,6 +398,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 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 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/TextLineImplTests.cs b/test/AvaloniaEdit.Tests/Text/TextLineImplTests.cs new file mode 100644 index 00000000..a435ca43 --- /dev/null +++ b/test/AvaloniaEdit.Tests/Text/TextLineImplTests.cs @@ -0,0 +1,121 @@ +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.ToString()); + Assert.IsTrue(textLine.LineRuns[1].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].StringRange.ToString()); + 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() + { + 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 new file mode 100644 index 00000000..e4fd1aa1 --- /dev/null +++ b/test/AvaloniaEdit.Tests/Text/TextLineRunTests.cs @@ -0,0 +1,159 @@ +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, 4, CreateDefaultParagraphProperties()); + + 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()); + + var paragraphProperties = CreateDefaultParagraphProperties(); + + TextLineRun run = TextLineRun.Create(s, 0, 0, 1, paragraphProperties); + + 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() + { + 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, CreateDefaultParagraphProperties()); + + Assert.AreEqual( + runWidth + SpecialCharacterTextRun.BoxMargin, + run.GetDistanceFromCharacter(1)); + } + + TextRunProperties CreateDefaultTextProperties() + { + return new TextRunProperties() + { + Typeface = new Typeface("Default"), + FontSize = MockGlyphTypeface.DefaultFontSize, + }; + } + + TextParagraphProperties CreateDefaultParagraphProperties() + { + return new TextParagraphProperties() + { + DefaultTextRunProperties = CreateDefaultTextProperties(), + DefaultIncrementalTab = 70, + Indent = 4, + }; + } + } +}