From 768024a879345cd2b63ea1757f109d19b3cf142b Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 8 Apr 2022 09:05:08 +0200 Subject: [PATCH 01/10] Implement InlineUIContainer --- src/Avalonia.Controls/Documents/Inline.cs | 11 +- .../Documents/InlineCollection.cs | 12 ++ .../Documents/InlineUIContainer.cs | 121 +++++++++++++++++ src/Avalonia.Controls/Documents/LineBreak.cs | 20 +-- src/Avalonia.Controls/Documents/Run.cs | 18 ++- src/Avalonia.Controls/Documents/Span.cs | 43 ++---- src/Avalonia.Controls/TextBlock.cs | 126 ++++++++++++++---- .../Media/TextFormatting/TextFormatter.cs | 6 +- .../Media/TextFormatting/TextFormatterImpl.cs | 23 +++- .../Media/TextFormatting/TextLayout.cs | 94 +++++++++---- 10 files changed, 351 insertions(+), 123 deletions(-) create mode 100644 src/Avalonia.Controls/Documents/InlineUIContainer.cs diff --git a/src/Avalonia.Controls/Documents/Inline.cs b/src/Avalonia.Controls/Documents/Inline.cs index 5b63f95432..445a48ecf4 100644 --- a/src/Avalonia.Controls/Documents/Inline.cs +++ b/src/Avalonia.Controls/Documents/Inline.cs @@ -1,8 +1,8 @@ using System.Collections.Generic; using System.Text; +using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Media.TextFormatting; -using Avalonia.Utilities; namespace Avalonia.Controls.Documents { @@ -45,9 +45,9 @@ namespace Avalonia.Controls.Documents set { SetValue(BaselineAlignmentProperty, value); } } - internal abstract int BuildRun(StringBuilder stringBuilder, IList> textStyleOverrides, int firstCharacterIndex); + internal abstract void BuildTextRun(IList textRuns, IInlinesHost parent); - internal abstract int AppendText(StringBuilder stringBuilder); + internal abstract void AppendText(StringBuilder stringBuilder); protected TextRunProperties CreateTextRunProperties() { @@ -68,4 +68,9 @@ namespace Avalonia.Controls.Documents } } } + + public interface IInlinesHost : ILogical + { + void AddVisualChild(IControl child); + } } diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index 45c715c13a..abe8f2cd4d 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -96,6 +96,18 @@ namespace Avalonia.Controls.Documents } } + public void Add(IControl child) + { + if (!HasComplexContent && !string.IsNullOrEmpty(_text)) + { + base.Add(new Run(_text)); + + _text = string.Empty; + } + + base.Add(new InlineUIContainer(child)); + } + public override void Add(Inline item) { if (!HasComplexContent) diff --git a/src/Avalonia.Controls/Documents/InlineUIContainer.cs b/src/Avalonia.Controls/Documents/InlineUIContainer.cs new file mode 100644 index 0000000000..47851903dd --- /dev/null +++ b/src/Avalonia.Controls/Documents/InlineUIContainer.cs @@ -0,0 +1,121 @@ +using System.Collections.Generic; +using System.Text; +using Avalonia.LogicalTree; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Metadata; +using Avalonia.Utilities; + +namespace Avalonia.Controls.Documents +{ + /// + /// InlineUIContainer - a wrapper for embedded UIElements in text + /// flow content inline collections + /// + public class InlineUIContainer : Inline + { + /// + /// Defines the property. + /// + public static readonly StyledProperty ChildProperty = + AvaloniaProperty.Register(nameof(Child)); + + static InlineUIContainer() + { + BaselineAlignmentProperty.OverrideDefaultValue(BaselineAlignment.Top); + } + + /// + /// Initializes a new instance of InlineUIContainer element. + /// + /// + /// The purpose of this element is to be a wrapper for UIElements + /// when they are embedded into text flow - as items of + /// InlineCollections. + /// + public InlineUIContainer() + { + } + + /// + /// Initializes an InlineBox specifying its child UIElement + /// + /// + /// UIElement set as a child of this inline item + /// + public InlineUIContainer(IControl child) + { + Child = child; + } + + /// + /// The content spanned by this TextElement. + /// + [Content] + public IControl Child + { + get => GetValue(ChildProperty); + set => SetValue(ChildProperty, value); + } + + internal override void BuildTextRun(IList textRuns, IInlinesHost parent) + { + ((ISetLogicalParent)Child).SetParent(parent); + + parent.AddVisualChild(Child); + + textRuns.Add(new InlineRun(Child, CreateTextRunProperties())); + } + + internal override void AppendText(StringBuilder stringBuilder) + { + } + + private class InlineRun : DrawableTextRun + { + public InlineRun(IControl control, TextRunProperties properties) + { + Control = control; + Properties = properties; + } + + public IControl Control { get; } + + public override TextRunProperties? Properties { get; } + + public override Size Size + { + get + { + if (!Control.IsMeasureValid) + { + Control.Measure(Size.Infinity); + } + + return Control.DesiredSize; + } + } + + public override double Baseline + { + get + { + double baseline = Size.Height; + double baselineOffsetValue = (double)Control.GetValue(TextBlock.BaselineOffsetProperty); + + if (!MathUtilities.IsZero(baselineOffsetValue)) + { + baseline = baselineOffsetValue; + } + + return -baseline; + } + } + + public override void Draw(DrawingContext drawingContext, Point origin) + { + Control.Arrange(new Rect(origin, Size)); + } + } + } +} diff --git a/src/Avalonia.Controls/Documents/LineBreak.cs b/src/Avalonia.Controls/Documents/LineBreak.cs index 5e0cd1d387..00fad491d3 100644 --- a/src/Avalonia.Controls/Documents/LineBreak.cs +++ b/src/Avalonia.Controls/Documents/LineBreak.cs @@ -1,9 +1,9 @@ using System; using System.Collections.Generic; using System.Text; +using Avalonia.LogicalTree; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; -using Avalonia.Utilities; namespace Avalonia.Controls.Documents { @@ -20,24 +20,14 @@ namespace Avalonia.Controls.Documents { } - internal override int BuildRun(StringBuilder stringBuilder, - IList> textStyleOverrides, int firstCharacterIndex) + internal override void BuildTextRun(IList textRuns, IInlinesHost parent) { - var length = AppendText(stringBuilder); - - textStyleOverrides.Add(new ValueSpan(firstCharacterIndex, length, - CreateTextRunProperties())); - - return length; + textRuns.Add(new TextEndOfLine()); } - internal override int AppendText(StringBuilder stringBuilder) + internal override void AppendText(StringBuilder stringBuilder) { - var text = Environment.NewLine; - - stringBuilder.Append(text); - - return text.Length; + stringBuilder.Append(Environment.NewLine); } } } diff --git a/src/Avalonia.Controls/Documents/Run.cs b/src/Avalonia.Controls/Documents/Run.cs index a7dd5fd94f..884718c28b 100644 --- a/src/Avalonia.Controls/Documents/Run.cs +++ b/src/Avalonia.Controls/Documents/Run.cs @@ -2,9 +2,9 @@ using System; using System.Collections.Generic; using System.Text; using Avalonia.Data; +using Avalonia.LogicalTree; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; -using Avalonia.Utilities; namespace Avalonia.Controls.Documents { @@ -51,24 +51,22 @@ namespace Avalonia.Controls.Documents set { SetValue (TextProperty, value); } } - internal override int BuildRun(StringBuilder stringBuilder, - IList> textStyleOverrides, int firstCharacterIndex) + internal override void BuildTextRun(IList textRuns, IInlinesHost parent) { - var length = AppendText(stringBuilder); + var text = (Text ?? "").AsMemory(); - textStyleOverrides.Add(new ValueSpan(firstCharacterIndex, length, - CreateTextRunProperties())); + var textRunProperties = CreateTextRunProperties(); - return length; + var textCharacters = new TextCharacters(text, textRunProperties); + + textRuns.Add(textCharacters); } - internal override int AppendText(StringBuilder stringBuilder) + internal override void AppendText(StringBuilder stringBuilder) { var text = Text ?? ""; stringBuilder.Append(text); - - return text.Length; } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs index c086997b07..32e19d4153 100644 --- a/src/Avalonia.Controls/Documents/Span.cs +++ b/src/Avalonia.Controls/Documents/Span.cs @@ -1,5 +1,7 @@ +using System; using System.Collections.Generic; using System.Text; +using Avalonia.LogicalTree; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; using Avalonia.Utilities; @@ -35,61 +37,42 @@ namespace Avalonia.Controls.Documents [Content] public InlineCollection Inlines { get; } - internal override int BuildRun(StringBuilder stringBuilder, IList> textStyleOverrides, int firstCharacterIndex) + internal override void BuildTextRun(IList textRuns, IInlinesHost parent) { - var length = 0; - if (Inlines.HasComplexContent) { foreach (var inline in Inlines) { - var inlineLength = inline.BuildRun(stringBuilder, textStyleOverrides, firstCharacterIndex); - - firstCharacterIndex += inlineLength; - - length += inlineLength; + inline.BuildTextRun(textRuns, parent); } } else { - if (Inlines.Text == null) + if (Inlines.Text is string text) { - return length; - } - - stringBuilder.Append(Inlines.Text); + var textRunProperties = CreateTextRunProperties(); - length = Inlines.Text.Length; + var textCharacters = new TextCharacters(text.AsMemory(), textRunProperties); - textStyleOverrides.Add(new ValueSpan(firstCharacterIndex, length, - CreateTextRunProperties())); + textRuns.Add(textCharacters); + } } - - return length; } - internal override int AppendText(StringBuilder stringBuilder) + internal override void AppendText(StringBuilder stringBuilder) { if (Inlines.HasComplexContent) { - var length = 0; - foreach (var inline in Inlines) { - length += inline.AppendText(stringBuilder); + inline.AppendText(stringBuilder); } - - return length; } - if (Inlines.Text == null) + if (Inlines.Text is string text) { - return 0; + stringBuilder.Append(text); } - - stringBuilder.Append(Inlines.Text); - - return Inlines.Text.Length; } } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 703b851c79..a9d8f1b9c0 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls /// /// A control that displays a block of text. /// - public class TextBlock : Control + public class TextBlock : Control, IInlinesHost { /// /// Defines the property. @@ -400,38 +400,41 @@ namespace Avalonia.Controls /// A object. protected virtual TextLayout CreateTextLayout(Size constraint, string? text) { - List>? textStyleOverrides = null; + var defaultProperties = new GenericTextRunProperties( + new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), + FontSize, + TextDecorations, + Foreground); + + var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false, + defaultProperties, TextWrapping, LineHeight, 0); + + ITextSource textSource; if (Inlines.HasComplexContent) { - textStyleOverrides = new List>(Inlines.Count); - - var textPosition = 0; - var stringBuilder = new StringBuilder(); + var textRuns = new List(); foreach (var inline in Inlines) { - textPosition += inline.BuildRun(stringBuilder, textStyleOverrides, textPosition); + inline.BuildTextRun(textRuns, this); } - text = stringBuilder.ToString(); + textSource = new InlinesTextSource(textRuns); + } + else + { + textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties); } return new TextLayout( - text ?? string.Empty, - new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), - FontSize, - Foreground ?? Brushes.Transparent, - TextAlignment, - TextWrapping, + textSource, + paragraphProperties, TextTrimming, - TextDecorations, - FlowDirection, constraint.Width, constraint.Height, maxLines: MaxLines, - lineHeight: LineHeight, - textStyleOverrides: textStyleOverrides); + lineHeight: LineHeight); } /// @@ -440,7 +443,7 @@ namespace Avalonia.Controls protected void InvalidateTextLayout() { _textLayout = null; - + InvalidateMeasure(); } @@ -452,9 +455,9 @@ namespace Avalonia.Controls } var padding = Padding; - + _constraint = availableSize.Deflate(padding); - + _textLayout = null; InvalidateArrange(); @@ -470,9 +473,13 @@ namespace Avalonia.Controls { return finalSize; } - - _constraint = new Size(finalSize.Width, Math.Ceiling(finalSize.Height)); - + + var padding = Padding; + + var textSize = finalSize.Deflate(padding); + + _constraint = new Size(textSize.Width, Math.Ceiling(textSize.Height)); + _textLayout = null; return finalSize; @@ -521,9 +528,78 @@ namespace Avalonia.Controls } } - private void InlinesChanged(object? sender, EventArgs e) + private void InlinesChanged(object? sender, EventArgs e) { InvalidateTextLayout(); } + + void IInlinesHost.AddVisualChild(IControl child) + { + if (child.VisualParent == null) + { + VisualChildren.Add(child); + } + } + + private readonly struct InlinesTextSource : ITextSource + { + private readonly IReadOnlyList _textRuns; + + public InlinesTextSource(IReadOnlyList textRuns) + { + _textRuns = textRuns; + } + + public TextRun? GetTextRun(int textSourceIndex) + { + var currentPosition = 0; + + foreach (var textRun in _textRuns) + { + if(textRun.TextSourceLength == 0) + { + continue; + } + + if(currentPosition >= textSourceIndex) + { + return textRun; + } + + currentPosition += textRun.TextSourceLength; + } + + return null; + } + } + + private readonly struct SimpleTextSource : ITextSource + { + private readonly ReadOnlySlice _text; + private readonly TextRunProperties _defaultProperties; + + public SimpleTextSource(ReadOnlySlice text, TextRunProperties defaultProperties) + { + _text = text; + _defaultProperties = defaultProperties; + } + + public TextRun? GetTextRun(int textSourceIndex) + { + if (textSourceIndex > _text.Length) + { + return null; + } + + var runText = _text.Skip(textSourceIndex); + + if (runText.IsEmpty) + { + return null; + } + + return new TextCharacters(runText, _defaultProperties); + } + } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs index d521077a43..ff8c1c4860 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs @@ -1,6 +1,4 @@ -using Avalonia.Media.TextFormatting.Unicode; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// Represents a base class for text formatting. @@ -40,7 +38,7 @@ namespace Avalonia.Media.TextFormatting /// A value that specifies the text formatter state, /// in terms of where the previous line in the paragraph was broken by the text formatting process. /// The formatted line. - public abstract TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, + public abstract TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null); } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 7c60f73b8d..0ccff8ae3a 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -9,7 +9,7 @@ namespace Avalonia.Media.TextFormatting internal class TextFormatterImpl : TextFormatter { /// - public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, + public override TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null) { var textWrapping = paragraphProperties.TextWrapping; @@ -20,6 +20,11 @@ namespace Avalonia.Media.TextFormatting var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, out var textEndOfLine, out var textSourceLength); + if(textRuns.Count == 0) + { + return null; + } + if (previousLineBreak?.RemainingRuns != null) { flowDirection = previousLineBreak.FlowDirection; @@ -471,11 +476,10 @@ namespace Avalonia.Media.TextFormatting return false; } - private static bool TryMeasureLength(IReadOnlyList textRuns, int firstTextSourceIndex, double paragraphWidth, out int measuredLength) + private static bool TryMeasureLength(IReadOnlyList textRuns, double paragraphWidth, out int measuredLength) { measuredLength = 0; var currentWidth = 0.0; - var lastCluster = firstTextSourceIndex; foreach (var currentRun in textRuns) { @@ -483,12 +487,17 @@ namespace Avalonia.Media.TextFormatting { case ShapedTextCharacters shapedTextCharacters: { + var firstCluster = shapedTextCharacters.Text.Start; + var lastCluster = firstCluster; + for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++) { var glyphInfo = shapedTextCharacters.ShapedBuffer[i]; if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth) { + measuredLength += Math.Max(0, lastCluster - firstCluster + 1); + goto found; } @@ -496,6 +505,8 @@ namespace Avalonia.Media.TextFormatting currentWidth += glyphInfo.GlyphAdvance; } + measuredLength += currentRun.TextSourceLength; + break; } @@ -506,7 +517,7 @@ namespace Avalonia.Media.TextFormatting goto found; } - lastCluster += currentRun.TextSourceLength; + measuredLength += currentRun.TextSourceLength; currentWidth += currentRun.Size.Width; break; @@ -516,8 +527,6 @@ namespace Avalonia.Media.TextFormatting found: - measuredLength = Math.Max(0, lastCluster - firstTextSourceIndex + 1); - return measuredLength != 0; } @@ -535,7 +544,7 @@ namespace Avalonia.Media.TextFormatting double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection, TextLineBreak? currentLineBreak) { - if (!TryMeasureLength(textRuns, firstTextSourceIndex, paragraphWidth, out var measuredLength)) + if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) { measuredLength = 1; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index e3bcdee014..c6692b6203 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -12,11 +12,12 @@ namespace Avalonia.Media.TextFormatting { private static readonly char[] s_empty = { ' ' }; - private readonly ReadOnlySlice _text; + private readonly ITextSource _textSource; private readonly TextParagraphProperties _paragraphProperties; - private readonly IReadOnlyList>? _textStyleOverrides; private readonly TextTrimming _textTrimming; + private int _textSourceLength; + /// /// Initializes a new instance of the class. /// @@ -50,17 +51,49 @@ namespace Avalonia.Media.TextFormatting int maxLines = 0, IReadOnlyList>? textStyleOverrides = null) { - _text = string.IsNullOrEmpty(text) ? - new ReadOnlySlice() : - new ReadOnlySlice(text.AsMemory()); - _paragraphProperties = CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, textDecorations, flowDirection, lineHeight); + _textSource = new FormattedTextSource(text.AsMemory(), _paragraphProperties.DefaultTextRunProperties, textStyleOverrides); + _textTrimming = textTrimming ?? TextTrimming.None; - _textStyleOverrides = textStyleOverrides; + LineHeight = lineHeight; + + MaxWidth = maxWidth; + + MaxHeight = maxHeight; + + MaxLines = maxLines; + + TextLines = CreateTextLines(); + } + + /// + /// Initializes a new instance of the class. + /// + /// The text source. + /// The default text paragraph properties. + /// The text trimming. + /// The maximum width. + /// The maximum height. + /// The height of each line of text. + /// The maximum number of text lines. + public TextLayout( + ITextSource textSource, + TextParagraphProperties paragraphProperties, + TextTrimming? textTrimming = null, + double maxWidth = double.PositiveInfinity, + double maxHeight = double.PositiveInfinity, + double lineHeight = double.NaN, + int maxLines = 0) + { + _textSource = textSource; + + _paragraphProperties = paragraphProperties; + + _textTrimming = textTrimming ?? TextTrimming.None; LineHeight = lineHeight; @@ -147,7 +180,7 @@ namespace Avalonia.Media.TextFormatting return new Rect(); } - if (textPosition < 0 || textPosition >= _text.Length) + if (textPosition < 0 || textPosition >= _textSourceLength) { var lastLine = TextLines[TextLines.Count - 1]; @@ -273,7 +306,7 @@ namespace Avalonia.Media.TextFormatting return 0; } - if (charIndex > _text.Length) + if (charIndex > _textSourceLength) { return TextLines.Count - 1; } @@ -398,7 +431,7 @@ namespace Avalonia.Media.TextFormatting private IReadOnlyList CreateTextLines() { - if (_text.IsEmpty || MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) + if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { var textLine = CreateEmptyTextLine(0); @@ -411,26 +444,30 @@ namespace Avalonia.Media.TextFormatting double left = double.PositiveInfinity, width = 0.0, height = 0.0; - var currentPosition = 0; - - var textSource = new FormattedTextSource(_text, - _paragraphProperties.DefaultTextRunProperties, _textStyleOverrides); + _textSourceLength = 0; TextLine? previousLine = null; - while (currentPosition < _text.Length) + while (true) { - var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth, + var textLine = TextFormatter.Current.FormatLine(_textSource, _textSourceLength, MaxWidth, _paragraphProperties, previousLine?.TextLineBreak); -#if DEBUG - if (textLine.Length == 0) + if(textLine == null || textLine.Length == 0) { - throw new InvalidOperationException($"{nameof(textLine)} should not be empty."); + if(previousLine != null && previousLine.NewLineLength > 0) + { + var emptyTextLine = CreateEmptyTextLine(_textSourceLength); + + textLines.Add(emptyTextLine); + + UpdateBounds(emptyTextLine, ref left, ref width, ref height); + } + + break; } -#endif - currentPosition += textLine.Length; + _textSourceLength += textLine.Length; //Fulfill max height constraint if (textLines.Count > 0 && !double.IsPositiveInfinity(MaxHeight) && height + textLine.Height > MaxHeight) @@ -464,17 +501,16 @@ namespace Avalonia.Media.TextFormatting { break; } - - if (currentPosition != _text.Length || textLine.NewLineLength <= 0) - { - continue; - } + } - var emptyTextLine = CreateEmptyTextLine(currentPosition); + //Make sure the TextLayout always contains at least on empty line + if(textLines.Count == 0) + { + var textLine = CreateEmptyTextLine(0); - textLines.Add(emptyTextLine); + textLines.Add(textLine); - UpdateBounds(emptyTextLine,ref left, ref width, ref height); + UpdateBounds(textLine, ref left, ref width, ref height); } Bounds = new Rect(left, 0, width, height); From 6317a3ec181f107d6ce708ba288176e13d795403 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Fri, 8 Apr 2022 10:08:26 +0200 Subject: [PATCH 02/10] Always produce a TextLine --- .../Media/TextFormatting/TextFormatter.cs | 2 +- .../Media/TextFormatting/TextFormatterImpl.cs | 14 +++++++------- 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs index ff8c1c4860..0b5d7649d7 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs @@ -38,7 +38,7 @@ /// A value that specifies the text formatter state, /// in terms of where the previous line in the paragraph was broken by the text formatting process. /// The formatted line. - public abstract TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, + public abstract TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null); } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs index 0ccff8ae3a..7241c62472 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -9,7 +9,7 @@ namespace Avalonia.Media.TextFormatting internal class TextFormatterImpl : TextFormatter { /// - public override TextLine? FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, + public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null) { var textWrapping = paragraphProperties.TextWrapping; @@ -20,11 +20,6 @@ namespace Avalonia.Media.TextFormatting var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, out var textEndOfLine, out var textSourceLength); - if(textRuns.Count == 0) - { - return null; - } - if (previousLineBreak?.RemainingRuns != null) { flowDirection = previousLineBreak.FlowDirection; @@ -272,7 +267,6 @@ namespace Avalonia.Media.TextFormatting IReadOnlyList textRuns, ReadOnlySlice text, TextShaperOptions options) { var shapedRuns = new List(textRuns.Count); - var firstRun = textRuns[0]; var shapedBuffer = TextShaper.Current.ShapeText(text, options); @@ -544,6 +538,12 @@ namespace Avalonia.Media.TextFormatting double paragraphWidth, TextParagraphProperties paragraphProperties, FlowDirection flowDirection, TextLineBreak? currentLineBreak) { + if(textRuns.Count == 0) + { + return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection); + + } + if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) { measuredLength = 1; From 2902e3d24a9736484da225ec7ec15fa24b523437 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 19 Apr 2022 17:02:20 +0200 Subject: [PATCH 03/10] Rework Inlines invalidation --- .../TextFormatting/Unicode/BiDiAlgorithm.cs | 5 ++ .../Documents/IInlineHost.cs | 11 +++++ src/Avalonia.Controls/Documents/Inline.cs | 12 ++--- .../Documents/InlineCollection.cs | 48 ++++++++++++------- .../Documents/InlineUIContainer.cs | 12 +++-- src/Avalonia.Controls/Documents/LineBreak.cs | 2 +- src/Avalonia.Controls/Documents/Run.cs | 5 +- src/Avalonia.Controls/Documents/Span.cs | 9 ++-- .../Documents/TextElement.cs | 15 ++---- src/Avalonia.Controls/TextBlock.cs | 19 ++++---- 10 files changed, 77 insertions(+), 61 deletions(-) create mode 100644 src/Avalonia.Controls/Documents/IInlineHost.cs diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index d18a4b2a87..2511807d9c 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -238,6 +238,11 @@ namespace Avalonia.Media.TextFormatting.Unicode _levelRuns.Clear(); _resolvedLevelsBuffer.Clear(); + if (types.IsEmpty) + { + return; + } + // Setup original types and working types _originalClasses = types; _workingClasses = _workingClassesBuffer.Add(types); diff --git a/src/Avalonia.Controls/Documents/IInlineHost.cs b/src/Avalonia.Controls/Documents/IInlineHost.cs new file mode 100644 index 0000000000..da72c207be --- /dev/null +++ b/src/Avalonia.Controls/Documents/IInlineHost.cs @@ -0,0 +1,11 @@ +using Avalonia.LogicalTree; + +namespace Avalonia.Controls.Documents +{ + internal interface IInlineHost : ILogical + { + void AddVisualChild(IControl child); + + void Invalidate(); + } +} diff --git a/src/Avalonia.Controls/Documents/Inline.cs b/src/Avalonia.Controls/Documents/Inline.cs index 445a48ecf4..a657d754b3 100644 --- a/src/Avalonia.Controls/Documents/Inline.cs +++ b/src/Avalonia.Controls/Documents/Inline.cs @@ -1,10 +1,9 @@ using System.Collections.Generic; using System.Text; -using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Media.TextFormatting; -namespace Avalonia.Controls.Documents +namespace Avalonia.Controls.Documents { /// /// Inline element. @@ -45,7 +44,7 @@ namespace Avalonia.Controls.Documents set { SetValue(BaselineAlignmentProperty, value); } } - internal abstract void BuildTextRun(IList textRuns, IInlinesHost parent); + internal abstract void BuildTextRun(IList textRuns); internal abstract void AppendText(StringBuilder stringBuilder); @@ -63,14 +62,9 @@ namespace Avalonia.Controls.Documents { case nameof(TextDecorations): case nameof(BaselineAlignment): - Invalidate(); + InlineHost?.Invalidate(); break; } } } - - public interface IInlinesHost : ILogical - { - void AddVisualChild(IControl child); - } } diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index abe8f2cd4d..a76222385e 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -12,29 +12,37 @@ namespace Avalonia.Controls.Documents [WhitespaceSignificantCollection] public class InlineCollection : AvaloniaList { + private readonly IInlineHost? _host; private string? _text = string.Empty; /// /// Initializes a new instance of the class. /// - public InlineCollection(ILogical parent) : base(0) + public InlineCollection(ILogical parent) : this(parent, null) { } + + /// + /// Initializes a new instance of the class. + /// + internal InlineCollection(ILogical parent, IInlineHost? host = null) : base(0) { + _host = host; + ResetBehavior = ResetBehavior.Remove; this.ForEachItem( x => { ((ISetLogicalParent)x).SetParent(parent); - x.Invalidated += Invalidate; - Invalidate(); + x.InlineHost = host; + host?.Invalidate(); }, x => { ((ISetLogicalParent)x).SetParent(null); - x.Invalidated -= Invalidate; - Invalidate(); + x.InlineHost = host; + host?.Invalidate(); }, - () => throw new NotSupportedException()); + () => throw new NotSupportedException()); } public bool HasComplexContent => Count > 0; @@ -98,22 +106,20 @@ namespace Avalonia.Controls.Documents public void Add(IControl child) { - if (!HasComplexContent && !string.IsNullOrEmpty(_text)) - { - base.Add(new Run(_text)); - - _text = string.Empty; - } + var implicitRun = new InlineUIContainer(child); - base.Add(new InlineUIContainer(child)); + Add(implicitRun); } public override void Add(Inline item) { if (!HasComplexContent) { - base.Add(new Run(_text)); - + if (!string.IsNullOrEmpty(_text)) + { + base.Add(new Run(_text)); + } + _text = string.Empty; } @@ -124,11 +130,19 @@ namespace Avalonia.Controls.Documents /// Raised when an inline in the collection changes. /// public event EventHandler? Invalidated; - + /// /// Raises the event. /// - protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty); + protected void Invalidate() + { + if(_host != null) + { + _host.Invalidate(); + } + + Invalidated?.Invoke(this, EventArgs.Empty); + } private void Invalidate(object? sender, EventArgs e) => Invalidate(); } diff --git a/src/Avalonia.Controls/Documents/InlineUIContainer.cs b/src/Avalonia.Controls/Documents/InlineUIContainer.cs index 47851903dd..eb12092bb8 100644 --- a/src/Avalonia.Controls/Documents/InlineUIContainer.cs +++ b/src/Avalonia.Controls/Documents/InlineUIContainer.cs @@ -1,6 +1,5 @@ using System.Collections.Generic; using System.Text; -using Avalonia.LogicalTree; using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; @@ -58,11 +57,16 @@ namespace Avalonia.Controls.Documents set => SetValue(ChildProperty, value); } - internal override void BuildTextRun(IList textRuns, IInlinesHost parent) + internal override void BuildTextRun(IList textRuns) { - ((ISetLogicalParent)Child).SetParent(parent); + if(InlineHost == null) + { + return; + } + + ((ISetLogicalParent)Child).SetParent(InlineHost); - parent.AddVisualChild(Child); + InlineHost.AddVisualChild(Child); textRuns.Add(new InlineRun(Child, CreateTextRunProperties())); } diff --git a/src/Avalonia.Controls/Documents/LineBreak.cs b/src/Avalonia.Controls/Documents/LineBreak.cs index 00fad491d3..aeb81f7313 100644 --- a/src/Avalonia.Controls/Documents/LineBreak.cs +++ b/src/Avalonia.Controls/Documents/LineBreak.cs @@ -20,7 +20,7 @@ namespace Avalonia.Controls.Documents { } - internal override void BuildTextRun(IList textRuns, IInlinesHost parent) + internal override void BuildTextRun(IList textRuns) { textRuns.Add(new TextEndOfLine()); } diff --git a/src/Avalonia.Controls/Documents/Run.cs b/src/Avalonia.Controls/Documents/Run.cs index 884718c28b..2c6482b586 100644 --- a/src/Avalonia.Controls/Documents/Run.cs +++ b/src/Avalonia.Controls/Documents/Run.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Text; using Avalonia.Data; -using Avalonia.LogicalTree; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; @@ -51,7 +50,7 @@ namespace Avalonia.Controls.Documents set { SetValue (TextProperty, value); } } - internal override void BuildTextRun(IList textRuns, IInlinesHost parent) + internal override void BuildTextRun(IList textRuns) { var text = (Text ?? "").AsMemory(); @@ -76,7 +75,7 @@ namespace Avalonia.Controls.Documents switch (change.Property.Name) { case nameof(Text): - Invalidate(); + InlineHost?.Invalidate(); break; } } diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs index 32e19d4153..bd1b4fc5e1 100644 --- a/src/Avalonia.Controls/Documents/Span.cs +++ b/src/Avalonia.Controls/Documents/Span.cs @@ -1,10 +1,8 @@ using System; using System.Collections.Generic; using System.Text; -using Avalonia.LogicalTree; using Avalonia.Media.TextFormatting; using Avalonia.Metadata; -using Avalonia.Utilities; namespace Avalonia.Controls.Documents { @@ -27,8 +25,7 @@ namespace Avalonia.Controls.Documents public Span() { Inlines = new InlineCollection(this); - - Inlines.Invalidated += (s, e) => Invalidate(); + Inlines.Invalidated += (s, e) => InlineHost?.Invalidate(); } /// @@ -37,13 +34,13 @@ namespace Avalonia.Controls.Documents [Content] public InlineCollection Inlines { get; } - internal override void BuildTextRun(IList textRuns, IInlinesHost parent) + internal override void BuildTextRun(IList textRuns) { if (Inlines.HasComplexContent) { foreach (var inline in Inlines) { - inline.BuildTextRun(textRuns, parent); + inline.BuildTextRun(textRuns); } } else diff --git a/src/Avalonia.Controls/Documents/TextElement.cs b/src/Avalonia.Controls/Documents/TextElement.cs index d8e13554b5..faf869cce6 100644 --- a/src/Avalonia.Controls/Documents/TextElement.cs +++ b/src/Avalonia.Controls/Documents/TextElement.cs @@ -1,5 +1,4 @@ -using System; -using Avalonia.Media; +using Avalonia.Media; namespace Avalonia.Controls.Documents { @@ -251,10 +250,7 @@ namespace Avalonia.Controls.Documents control.SetValue(ForegroundProperty, value); } - /// - /// Raised when the visual representation of the text element changes. - /// - public event EventHandler? Invalidated; + internal IInlineHost? InlineHost { get; set; } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { @@ -269,14 +265,9 @@ namespace Avalonia.Controls.Documents case nameof(FontWeight): case nameof(FontStretch): case nameof(Foreground): - Invalidate(); + InlineHost?.Invalidate(); break; } } - - /// - /// Raises the event. - /// - protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty); } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index c698f0ff3b..3bcb74eee6 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -14,7 +14,7 @@ namespace Avalonia.Controls /// /// A control that displays a block of text. /// - public class TextBlock : Control, IInlinesHost + public class TextBlock : Control, IInlineHost { /// /// Defines the property. @@ -155,9 +155,7 @@ namespace Avalonia.Controls /// public TextBlock() { - Inlines = new InlineCollection(this); - - Inlines.Invalidated += InlinesChanged; + Inlines = new InlineCollection(this, this); } /// @@ -211,7 +209,7 @@ namespace Avalonia.Controls } /// - /// Gets or sets the inlines. + /// Gets the inlines. /// [Content] public InlineCollection Inlines { get; } @@ -569,7 +567,7 @@ namespace Avalonia.Controls foreach (var inline in Inlines) { - inline.BuildTextRun(textRuns, this); + inline.BuildTextRun(textRuns); } textSource = new InlinesTextSource(textRuns); @@ -667,8 +665,6 @@ namespace Avalonia.Controls case nameof (Padding): case nameof (LineHeight): case nameof (MaxLines): - - case nameof (InlinesProperty): case nameof (Text): case nameof (TextDecorations): @@ -685,7 +681,7 @@ namespace Avalonia.Controls InvalidateTextLayout(); } - void IInlinesHost.AddVisualChild(IControl child) + void IInlineHost.AddVisualChild(IControl child) { if (child.VisualParent == null) { @@ -693,6 +689,11 @@ namespace Avalonia.Controls } } + void IInlineHost.Invalidate() + { + InvalidateTextLayout(); + } + private readonly struct InlinesTextSource : ITextSource { private readonly IReadOnlyList _textRuns; From 050ac5fbba104fa7606cc5c3d3618f837f158731 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 20 Apr 2022 13:53:38 +0200 Subject: [PATCH 04/10] Fix line metrics for empty lines that are processed by TextWrapping --- .../Media/TextFormatting/TextFormatterImpl.cs | 26 +++++++++++++++-- .../Media/TextFormatting/TextLayout.cs | 29 ++----------------- 2 files changed, 27 insertions(+), 28 deletions(-) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 7241c62472..be07745d89 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -8,6 +8,8 @@ namespace Avalonia.Media.TextFormatting { internal class TextFormatterImpl : TextFormatter { + private static readonly char[] s_empty = { ' ' }; + /// public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, TextParagraphProperties paragraphProperties, TextLineBreak? previousLineBreak = null) @@ -524,6 +526,27 @@ namespace Avalonia.Media.TextFormatting return measuredLength != 0; } + /// + /// Creates an empty text line. + /// + /// The empty text line. + public static TextLineImpl CreateEmptyTextLine(int firstTextSourceIndex, TextParagraphProperties paragraphProperties) + { + var flowDirection = paragraphProperties.FlowDirection; + var properties = paragraphProperties.DefaultTextRunProperties; + var glyphTypeface = properties.Typeface.GlyphTypeface; + var text = new ReadOnlySlice(s_empty, firstTextSourceIndex, 1); + var glyph = glyphTypeface.GetGlyph(s_empty[0]); + var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) }; + + var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize, + (sbyte)flowDirection); + + var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) }; + + return new TextLineImpl(textRuns, firstTextSourceIndex, 1, double.PositiveInfinity, paragraphProperties, flowDirection).FinalizeLine(); + } + /// /// Performs text wrapping returns a list of text lines. /// @@ -540,8 +563,7 @@ namespace Avalonia.Media.TextFormatting { if(textRuns.Count == 0) { - return new TextLineImpl(textRuns, firstTextSourceIndex, 0, paragraphWidth, paragraphProperties, flowDirection); - + return CreateEmptyTextLine(firstTextSourceIndex, paragraphProperties); } if (!TryMeasureLength(textRuns, paragraphWidth, out var measuredLength)) diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs index c6692b6203..0df608cb34 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLayout.cs @@ -10,8 +10,6 @@ namespace Avalonia.Media.TextFormatting /// public class TextLayout { - private static readonly char[] s_empty = { ' ' }; - private readonly ITextSource _textSource; private readonly TextParagraphProperties _paragraphProperties; private readonly TextTrimming _textTrimming; @@ -408,32 +406,11 @@ namespace Avalonia.Media.TextFormatting height += textLine.Height; } - /// - /// Creates an empty text line. - /// - /// The empty text line. - private TextLine CreateEmptyTextLine(int firstTextSourceIndex) - { - var flowDirection = _paragraphProperties.FlowDirection; - var properties = _paragraphProperties.DefaultTextRunProperties; - var glyphTypeface = properties.Typeface.GlyphTypeface; - var text = new ReadOnlySlice(s_empty, firstTextSourceIndex, 1); - var glyph = glyphTypeface.GetGlyph(s_empty[0]); - var glyphInfos = new[] { new GlyphInfo(glyph, firstTextSourceIndex) }; - - var shapedBuffer = new ShapedBuffer(text, glyphInfos, glyphTypeface, properties.FontRenderingEmSize, - (sbyte)flowDirection); - - var textRuns = new List { new ShapedTextCharacters(shapedBuffer, properties) }; - - return new TextLineImpl(textRuns, firstTextSourceIndex, 1, MaxWidth, _paragraphProperties, flowDirection).FinalizeLine(); - } - private IReadOnlyList CreateTextLines() { if (MathUtilities.IsZero(MaxWidth) || MathUtilities.IsZero(MaxHeight)) { - var textLine = CreateEmptyTextLine(0); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, _paragraphProperties); Bounds = new Rect(0,0,0, textLine.Height); @@ -457,7 +434,7 @@ namespace Avalonia.Media.TextFormatting { if(previousLine != null && previousLine.NewLineLength > 0) { - var emptyTextLine = CreateEmptyTextLine(_textSourceLength); + var emptyTextLine = TextFormatterImpl.CreateEmptyTextLine(_textSourceLength, _paragraphProperties); textLines.Add(emptyTextLine); @@ -506,7 +483,7 @@ namespace Avalonia.Media.TextFormatting //Make sure the TextLayout always contains at least on empty line if(textLines.Count == 0) { - var textLine = CreateEmptyTextLine(0); + var textLine = TextFormatterImpl.CreateEmptyTextLine(0, _paragraphProperties); textLines.Add(textLine); From 3e6bc0b48d4ea1e32a7b552b5c3c54c093a6592a Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 21 Apr 2022 14:17:26 +0200 Subject: [PATCH 05/10] Some more hit testing fixes --- src/Avalonia.Base/Media/GlyphRun.cs | 44 +++++---- .../Media/TextFormatting/TextFormatterImpl.cs | 2 +- .../Media/TextFormatting/TextLineImpl.cs | 74 +++++++------- .../Media/TextFormatting/TextLineTests.cs | 97 +++++++++++++------ 4 files changed, 133 insertions(+), 84 deletions(-) diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index ec270d796a..9a2645f03d 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -49,7 +49,7 @@ namespace Avalonia.Media IReadOnlyList? glyphClusters = null, int biDiLevel = 0) { - _glyphTypeface = glyphTypeface; + _glyphTypeface = glyphTypeface; FontRenderingEmSize = fontRenderingEmSize; @@ -204,7 +204,7 @@ namespace Avalonia.Media public double GetDistanceFromCharacterHit(CharacterHit characterHit) { var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - + var distance = 0.0; if (IsLeftToRight) @@ -223,7 +223,7 @@ namespace Avalonia.Media } var glyphIndex = FindGlyphIndex(characterIndex); - + if (GlyphClusters != null) { var currentCluster = GlyphClusters[glyphIndex]; @@ -249,7 +249,7 @@ namespace Avalonia.Media { //RightToLeft var glyphIndex = FindGlyphIndex(characterIndex); - + if (GlyphClusters != null) { if (characterIndex > GlyphClusters[0]) @@ -284,13 +284,13 @@ namespace Avalonia.Media public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInside) { var characterIndex = 0; - + // Before if (distance <= 0) { isInside = false; - if(GlyphClusters != null) + if (GlyphClusters != null) { characterIndex = GlyphClusters[characterIndex]; } @@ -307,11 +307,11 @@ namespace Avalonia.Media characterIndex = GlyphIndices.Count - 1; - if(GlyphClusters != null) + if (GlyphClusters != null) { characterIndex = GlyphClusters[characterIndex]; } - + var lastCharacterHit = FindNearestCharacterHit(characterIndex, out _); return IsLeftToRight ? lastCharacterHit : new CharacterHit(lastCharacterHit.FirstCharacterIndex); @@ -327,7 +327,7 @@ namespace Avalonia.Media var advance = GetGlyphAdvance(index, out var cluster); characterIndex = cluster; - + if (distance > currentX && distance <= currentX + advance) { break; @@ -345,7 +345,7 @@ namespace Avalonia.Media var advance = GetGlyphAdvance(index, out var cluster); characterIndex = cluster; - + if (currentX - advance < distance) { break; @@ -554,6 +554,16 @@ namespace Avalonia.Media nextCluster = GlyphClusters[currentIndex]; } + if (nextCluster < Characters.Start) + { + nextCluster = Characters.Start; + } + + if (cluster < Characters.Start) + { + cluster = Characters.Start; + } + int trailingLength; if (nextCluster == cluster) @@ -577,7 +587,7 @@ namespace Avalonia.Media private double GetGlyphAdvance(int index, out int cluster) { cluster = GlyphClusters != null ? GlyphClusters[index] : index; - + if (GlyphAdvances != null) { return GlyphAdvances[index]; @@ -603,7 +613,7 @@ namespace Avalonia.Media var widthIncludingTrailingWhitespace = 0d; var trailingWhitespaceLength = GetTrailingWhitespaceLength(out var newLineLength, out var glyphCount); - + for (var index = 0; index < GlyphIndices.Count; index++) { var advance = GetGlyphAdvance(index, out _); @@ -615,7 +625,7 @@ namespace Avalonia.Media if (IsLeftToRight) { - for (var index = GlyphIndices.Count - glyphCount; index { new ShapedTextCharacters(shapedBuffer, properties) }; - return new TextLineImpl(textRuns, firstTextSourceIndex, 1, double.PositiveInfinity, paragraphProperties, flowDirection).FinalizeLine(); + return new TextLineImpl(textRuns, firstTextSourceIndex, 0, double.PositiveInfinity, paragraphProperties, flowDirection).FinalizeLine(); } /// diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index 30e3728d1f..b480774d1d 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -183,6 +183,7 @@ namespace Avalonia.Media.TextFormatting case ShapedTextCharacters shapedRun: { characterHit = shapedRun.GlyphRun.GetCharacterHitFromDistance(distance, out _); + break; } default: @@ -426,31 +427,42 @@ namespace Avalonia.Media.TextFormatting if (nextRun != null) { - if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End) + switch (nextRun) { - goto skip; - } + case ShapedTextCharacters when currentRun is ShapedTextCharacters: + { + if (nextRun.Text.Start < currentRun.Text.Start && firstTextSourceCharacterIndex + textLength < currentRun.Text.End) + { + goto skip; + } - if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength) - { - goto skip; - } + if (currentRun.Text.Start >= firstTextSourceCharacterIndex + textLength) + { + goto skip; + } - if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex) - { - goto skip; - } + if (currentRun.Text.Start > nextRun.Text.Start && currentRun.Text.Start < firstTextSourceCharacterIndex) + { + goto skip; + } - if (currentRun.Text.End < firstTextSourceCharacterIndex) - { - goto skip; - } + if (currentRun.Text.End < firstTextSourceCharacterIndex) + { + goto skip; + } - goto noop; + goto noop; + } + default: + { + goto noop; + } + } skip: { startX += currentRun.Size.Width; + currentPosition += currentRun.TextSourceLength; } continue; @@ -460,7 +472,6 @@ namespace Avalonia.Media.TextFormatting } } - var endX = startX; var endOffset = 0d; @@ -520,11 +531,13 @@ namespace Avalonia.Media.TextFormatting } default: { - if (firstTextSourceCharacterIndex + textLength >= currentRun.Text.Start + currentRun.Text.Length) + if (currentPosition + currentRun.TextSourceLength <= firstTextSourceCharacterIndex + textLength) { endX += currentRun.Size.Width; } + currentPosition += currentRun.TextSourceLength; + break; } } @@ -538,7 +551,9 @@ namespace Avalonia.Media.TextFormatting if (lastDirection == currentDirection && result.Count > 0 && MathUtilities.AreClose(currentRect.Right, startX)) { - var textBounds = new TextBounds(currentRect.WithWidth(currentRect.Width + width), currentDirection); + currentRect = currentRect.WithWidth(currentRect.Width + width); + + var textBounds = new TextBounds(currentRect, currentDirection); result[result.Count - 1] = textBounds; } @@ -551,21 +566,9 @@ namespace Avalonia.Media.TextFormatting if (currentDirection == FlowDirection.LeftToRight) { - if (nextRun != null) - { - if (nextRun.Text.Start > currentRun.Text.Start && nextRun.Text.Start >= firstTextSourceCharacterIndex + textLength) - { - break; - } - - currentPosition = nextRun.Text.End; - } - else + if (currentPosition >= firstTextSourceCharacterIndex + textLength) { - if (currentPosition >= firstTextSourceCharacterIndex + textLength) - { - break; - } + break; } } else @@ -575,10 +578,7 @@ namespace Avalonia.Media.TextFormatting break; } - if (currentPosition != currentRun.Text.Start) - { - endX += currentRun.Size.Width - endOffset; - } + endX += currentRun.Size.Width - endOffset; } lastDirection = currentDirection; diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index e9bc792be3..b58d9051f3 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -70,12 +70,12 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } } - + [Fact] public void Should_Get_Next_Caret_CharacterHit_Bidi() { const string text = "אבג 1 ABC"; - + using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); @@ -90,7 +90,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var clusters = new List(); - foreach (var textRun in textLine.TextRuns.OrderBy(x=> x.Text.Start)) + foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start)) { var shapedRun = (ShapedTextCharacters)textRun; @@ -98,7 +98,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting shapedRun.ShapedBuffer.GlyphClusters.Reverse() : shapedRun.ShapedBuffer.GlyphClusters); } - + var nextCharacterHit = new CharacterHit(0, clusters[1] - clusters[0]); foreach (var cluster in clusters) @@ -122,7 +122,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting public void Should_Get_Previous_Caret_CharacterHit_Bidi() { const string text = "אבג 1 ABC"; - + using (Start()) { var defaultProperties = new GenericTextRunProperties(Typeface.Default); @@ -137,7 +137,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var clusters = new List(); - foreach (var textRun in textLine.TextRuns.OrderBy(x=> x.Text.Start)) + foreach (var textRun in textLine.TextRuns.OrderBy(x => x.Text.Start)) { var shapedRun = (ShapedTextCharacters)textRun; @@ -147,13 +147,13 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } clusters.Reverse(); - + var nextCharacterHit = new CharacterHit(text.Length - 1); foreach (var cluster in clusters) { var currentCaretIndex = nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength; - + Assert.Equal(cluster, currentCaretIndex); nextCharacterHit = textLine.GetPreviousCaretCharacterHit(nextCharacterHit); @@ -168,7 +168,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(lastCharacterHit.TrailingLength, nextCharacterHit.TrailingLength); } } - + [InlineData("𐐷𐐷𐐷𐐷𐐷")] [InlineData("01234567🎉\n")] [InlineData("𐐷1234")] @@ -324,7 +324,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } - Assert.Equal(currentDistance,textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length))); + Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(s_multiLineText.Length))); } } @@ -371,7 +371,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting yield return CreateData("01234 01234", 58, TextTrimming.WordEllipsis, "01234\u2026"); yield return CreateData("01234", 9, TextTrimming.CharacterEllipsis, "\u2026"); yield return CreateData("01234", 2, TextTrimming.CharacterEllipsis, ""); - + object[] CreateData(string text, double width, TextTrimming mode, string expected) { return new object[] @@ -424,7 +424,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting { var defaultProperties = new GenericTextRunProperties(Typeface.Default); var textSource = new DrawableRunTextSource(); - + var formatter = new TextFormatterImpl(); var textLine = @@ -471,7 +471,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(4, textLine.TextRuns.Count); - var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3,1)); + var currentHit = textLine.GetPreviousCaretCharacterHit(new CharacterHit(3, 1)); Assert.Equal(3, currentHit.FirstCharacterIndex); Assert.Equal(0, currentHit.TrailingLength); @@ -552,11 +552,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting switch (textSourceIndex) { case 0: - return new CustomDrawableRun(); + return new CustomDrawableRun(); case 1: return new TextCharacters(new ReadOnlySlice(Text.AsMemory(), 1, 1, 1), new GenericTextRunProperties(Typeface.Default)); case 2: - return new CustomDrawableRun(); + return new CustomDrawableRun(); case 3: return new TextCharacters(new ReadOnlySlice(Text.AsMemory(), 3, 1, 3), new GenericTextRunProperties(Typeface.Default)); default: @@ -564,14 +564,14 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting } } } - + private class CustomDrawableRun : DrawableTextRun { public override Size Size => new(14, 14); public override double Baseline => 14; public override void Draw(DrawingContext drawingContext, Point origin) { - + } } @@ -587,29 +587,29 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var shapedTextRuns = textLine.TextRuns.Cast().ToList(); var lastCluster = -1; - + foreach (var textRun in shapedTextRuns) { var shapedBuffer = textRun.ShapedBuffer; var currentClusters = shapedBuffer.GlyphClusters.ToList(); - foreach (var currentCluster in currentClusters) + foreach (var currentCluster in currentClusters) { if (lastCluster == currentCluster) { continue; } - + glyphClusters.Add(currentCluster); lastCluster = currentCluster; } } - + return glyphClusters; } - + private static List BuildRects(TextLine textLine) { var rects = new List(); @@ -624,11 +624,11 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting foreach (var textRun in shapedTextRuns) { var shapedBuffer = textRun.ShapedBuffer; - + for (var index = 0; index < shapedBuffer.GlyphAdvances.Count; index++) { var currentCluster = shapedBuffer.GlyphClusters[index]; - + var advance = shapedBuffer.GlyphAdvances[index]; if (lastCluster != currentCluster) @@ -642,10 +642,10 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting rects.Remove(rect); rect = rect.WithWidth(rect.Width + advance); - + rects.Add(rect); } - + currentX += advance; lastCluster = currentCluster; @@ -655,8 +655,43 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting return rects; } + [Fact] - public void Should_Get_TextBounds() + public void Should_Get_TextBounds_Mixed() + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + var text = "0123".AsMemory(); + var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture); + + var textRuns = new List + { + new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text), shaperOption), defaultProperties), + new CustomDrawableRun(), + new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length + 1, text.Length), shaperOption), defaultProperties), + new CustomDrawableRun(), + new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 2 + 2, text.Length), shaperOption), defaultProperties), + new CustomDrawableRun(), + }; + + var textSource = new FixedRunsTextSource(textRuns); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var textBounds = textLine.GetTextBounds(0, text.Length * 3 + 3); + + Assert.Equal(1, textBounds.Count); + Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); + } + } + + [Fact] + public void Should_Get_TextBounds_BiDi() { using (Start()) { @@ -673,7 +708,7 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 3, text.Length), ltrOptions), defaultProperties) }; - + var textSource = new FixedRunsTextSource(textRuns); var formatter = new TextFormatterImpl(); @@ -700,12 +735,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting public TextRun? GetTextRun(int textSourceIndex) { + var currentPosition = 0; + foreach (var textRun in _textRuns) { - if(textRun.Text.Start == textSourceIndex) + if (currentPosition == textSourceIndex) { return textRun; } + + currentPosition += textRun.TextSourceLength; } return null; From 32d72930972ce5586caaf5259909c4b932945b2f Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 21 Apr 2022 15:33:01 +0200 Subject: [PATCH 06/10] Fix GetNextCharacterHit for trailing whitespace --- src/Avalonia.Controls/Presenters/TextPresenter.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs index 7f2dde7c1e..9ac4b71a12 100644 --- a/src/Avalonia.Controls/Presenters/TextPresenter.cs +++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs @@ -662,7 +662,7 @@ namespace Avalonia.Controls.Presenters caretIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; - if (textLine.NewLineLength > 0 && caretIndex == textLine.FirstTextSourceIndex + textLine.Length) + if (textLine.TrailingWhitespaceLength > 0 && caretIndex == textLine.FirstTextSourceIndex + textLine.Length) { characterHit = new CharacterHit(caretIndex); } From aaf04a38dab6bb8d1dfc263e6fc922a3ce111b10 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 21 Apr 2022 15:39:09 +0200 Subject: [PATCH 07/10] Fix property GetValue --- src/Avalonia.Controls/Documents/InlineUIContainer.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Documents/InlineUIContainer.cs b/src/Avalonia.Controls/Documents/InlineUIContainer.cs index eb12092bb8..5f08c23099 100644 --- a/src/Avalonia.Controls/Documents/InlineUIContainer.cs +++ b/src/Avalonia.Controls/Documents/InlineUIContainer.cs @@ -105,7 +105,7 @@ namespace Avalonia.Controls.Documents get { double baseline = Size.Height; - double baselineOffsetValue = (double)Control.GetValue(TextBlock.BaselineOffsetProperty); + double baselineOffsetValue = Control.GetValue(TextBlock.BaselineOffsetProperty); if (!MathUtilities.IsZero(baselineOffsetValue)) { From 0cdbd53bc312d2711461552c88a5250c1c1d3c1a Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 25 Apr 2022 15:21:26 +0200 Subject: [PATCH 08/10] Rewrite TextBounds test --- .../Media/TextFormatting/TextLineTests.cs | 19 ++++++++++++++++--- 1 file changed, 16 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index b58d9051f3..e3b9e5a8b1 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -665,14 +665,17 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var text = "0123".AsMemory(); var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture); + var shapedBuffer = TextShaper.Current.ShapeText(new ReadOnlySlice(text), shaperOption); + var firstRun = new ShapedTextCharacters(shapedBuffer, defaultProperties); + var textRuns = new List { - new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text), shaperOption), defaultProperties), new CustomDrawableRun(), - new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length + 1, text.Length), shaperOption), defaultProperties), + firstRun, new CustomDrawableRun(), - new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 2 + 2, text.Length), shaperOption), defaultProperties), + new ShapedTextCharacters(shapedBuffer, defaultProperties), new CustomDrawableRun(), + new ShapedTextCharacters(shapedBuffer, defaultProperties) }; var textSource = new FixedRunsTextSource(textRuns); @@ -687,6 +690,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); + + textBounds = textLine.GetTextBounds(0, firstRun.Text.Length); + + Assert.Equal(1, textBounds.Count); + Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); + + textBounds = textLine.GetTextBounds(0, firstRun.Text.Length + 1); + + Assert.Equal(1, textBounds.Count); + Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width); } } From 005907a93bf5f594694007de8b1fd91fda0cc466 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 27 Apr 2022 16:53:18 +0200 Subject: [PATCH 09/10] More hit testing fixes for embedded content runs --- src/Avalonia.Base/Media/GlyphRun.cs | 61 ++++++++++--------- .../Media/TextFormatting/TextFormatterImpl.cs | 24 ++++---- .../Media/TextFormatting/TextLineImpl.cs | 44 +++++++------ src/Skia/Avalonia.Skia/TextShaperImpl.cs | 3 +- .../Media/TextFormatting/TextLineTests.cs | 21 +++++-- 5 files changed, 81 insertions(+), 72 deletions(-) diff --git a/src/Avalonia.Base/Media/GlyphRun.cs b/src/Avalonia.Base/Media/GlyphRun.cs index 9a2645f03d..22be8d8865 100644 --- a/src/Avalonia.Base/Media/GlyphRun.cs +++ b/src/Avalonia.Base/Media/GlyphRun.cs @@ -28,6 +28,8 @@ namespace Avalonia.Media private IReadOnlyList? _glyphOffsets; private IReadOnlyList? _glyphClusters; + private int _offsetToFirstCharacter; + /// /// Initializes a new instance of the class by specifying properties of the class. /// @@ -203,7 +205,7 @@ namespace Avalonia.Media /// public double GetDistanceFromCharacterHit(CharacterHit characterHit) { - var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + var characterIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength - _offsetToFirstCharacter; var distance = 0.0; @@ -552,30 +554,20 @@ namespace Avalonia.Media } nextCluster = GlyphClusters[currentIndex]; - } - - if (nextCluster < Characters.Start) - { - nextCluster = Characters.Start; - } - - if (cluster < Characters.Start) - { - cluster = Characters.Start; - } + } int trailingLength; if (nextCluster == cluster) { - trailingLength = Characters.Start + Characters.Length - cluster; + trailingLength = Characters.Start + Characters.Length - _offsetToFirstCharacter - cluster; } else { trailingLength = nextCluster - cluster; } - return new CharacterHit(cluster, trailingLength); + return new CharacterHit(_offsetToFirstCharacter + cluster, trailingLength); } /// @@ -609,6 +601,13 @@ namespace Avalonia.Media private GlyphRunMetrics CreateGlyphRunMetrics() { + if (GlyphClusters != null && GlyphClusters.Count > 0) + { + var firstCluster = GlyphClusters[0]; + + _offsetToFirstCharacter = Math.Max(0, Characters.Start - firstCluster); + } + var height = (GlyphTypeface.Descent - GlyphTypeface.Ascent + GlyphTypeface.LineGap) * Scale; var widthIncludingTrailingWhitespace = 0d; @@ -680,34 +679,40 @@ namespace Avalonia.Media { for (var i = GlyphClusters.Count - 1; i >= 0; i--) { - var cluster = GlyphClusters[i]; - - var codepointIndex = IsLeftToRight ? cluster - _characters.Start : _characters.End - cluster; + var currentCluster = GlyphClusters[i]; + var characterIndex = Math.Max(0, currentCluster - _characters.BufferOffset); + var codepoint = Codepoint.ReadAt(_characters, characterIndex, out _); - if (codepointIndex < 0) + if (!codepoint.IsWhiteSpace) { - trailingWhitespaceLength = _characters.Length; - - glyphCount = GlyphClusters.Count; - break; } - var codepoint = Codepoint.ReadAt(_characters, codepointIndex, out _); + var clusterLength = 1; - if (!codepoint.IsWhiteSpace) + while(i - 1 >= 0) { + var nextCluster = GlyphClusters[i - 1]; + + if(currentCluster == nextCluster) + { + clusterLength++; + i--; + + continue; + } + break; } if (codepoint.IsBreakChar) { - newLineLength++; + newLineLength += clusterLength; } - trailingWhitespaceLength++; - - glyphCount++; + trailingWhitespaceLength += clusterLength; + + glyphCount++; } } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 67fba00ee8..7f0f204886 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -79,14 +79,14 @@ namespace Avalonia.Media.TextFormatting { var currentRun = textRuns[i]; - if (currentLength + currentRun.Text.Length < length) + if (currentLength + currentRun.TextSourceLength < length) { currentLength += currentRun.TextSourceLength; continue; } - var firstCount = currentRun.Text.Length >= 1 ? i + 1 : i; + var firstCount = currentRun.TextSourceLength >= 1 ? i + 1 : i; var first = new List(firstCount); @@ -100,13 +100,13 @@ namespace Avalonia.Media.TextFormatting var secondCount = textRuns.Count - firstCount; - if (currentLength + currentRun.Text.Length == length) + if (currentLength + currentRun.TextSourceLength == length) { var second = secondCount > 0 ? new List(secondCount) : null; if (second != null) { - var offset = currentRun.Text.Length >= 1 ? 1 : 0; + var offset = currentRun.TextSourceLength >= 1 ? 1 : 0; for (var j = 0; j < secondCount; j++) { @@ -124,16 +124,14 @@ namespace Avalonia.Media.TextFormatting var second = new List(secondCount); - if (currentRun is not ShapedTextCharacters shapedTextCharacters) + if (currentRun is ShapedTextCharacters shapedTextCharacters) { - throw new NotSupportedException("Only shaped runs can be split in between."); - } - - var split = shapedTextCharacters.Split(length - currentLength); + var split = shapedTextCharacters.Split(length - currentLength); - first.Add(split.First); + first.Add(split.First); - second.Add(split.Second!); + second.Add(split.Second!); + } for (var j = 1; j < secondCount; j++) { @@ -483,7 +481,7 @@ namespace Avalonia.Media.TextFormatting { case ShapedTextCharacters shapedTextCharacters: { - var firstCluster = shapedTextCharacters.Text.Start; + var firstCluster = shapedTextCharacters.ShapedBuffer.GlyphClusters[0]; var lastCluster = firstCluster; for (var i = 0; i < shapedTextCharacters.ShapedBuffer.Length; i++) @@ -492,7 +490,7 @@ namespace Avalonia.Media.TextFormatting if (currentWidth + glyphInfo.GlyphAdvance > paragraphWidth) { - measuredLength += Math.Max(0, lastCluster - firstCluster + 1); + measuredLength += Math.Max(0, lastCluster - firstCluster); goto found; } diff --git a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs index b480774d1d..6a704f6f3e 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextLineImpl.cs @@ -404,7 +404,7 @@ namespace Avalonia.Media.TextFormatting var result = new List(TextRuns.Count); var lastDirection = _flowDirection; var currentDirection = lastDirection; - var currentPosition = 0; + var currentPosition = FirstTextSourceIndex; var currentRect = Rect.Empty; var startX = Start; @@ -418,6 +418,11 @@ namespace Avalonia.Media.TextFormatting continue; } + if(currentPosition + currentRun.TextSourceLength <= firstTextSourceCharacterIndex) + { + continue; + } + TextRun? nextRun = null; if (index + 1 < TextRuns.Count) @@ -1018,31 +1023,21 @@ namespace Avalonia.Media.TextFormatting private TextLineMetrics CreateLineMetrics() { - var start = 0d; - var height = 0d; + var glyphTypeface = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface; + var fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize; + var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight; + var width = 0d; var widthIncludingWhitespace = 0d; var trailingWhitespaceLength = 0; var newLineLength = 0; - var ascent = 0d; - var descent = 0d; - var lineGap = 0d; - var fontRenderingEmSize = 0d; + var ascent = glyphTypeface.Ascent * scale; + var descent = glyphTypeface.Descent * scale; + var lineGap = glyphTypeface.LineGap * scale; - var lineHeight = _paragraphProperties.LineHeight; - - if (_textRuns.Count == 0) - { - var glyphTypeface = _paragraphProperties.DefaultTextRunProperties.Typeface.GlyphTypeface; - fontRenderingEmSize = _paragraphProperties.DefaultTextRunProperties.FontRenderingEmSize; - var scale = fontRenderingEmSize / glyphTypeface.DesignEmHeight; - ascent = glyphTypeface.Ascent * scale; - height = double.IsNaN(lineHeight) || MathUtilities.IsZero(lineHeight) ? - descent - ascent + lineGap : - lineHeight; - - return new TextLineMetrics(false, height, 0, start, -ascent, 0, 0, 0); - } + var height = descent - ascent + lineGap; + + var lineHeight = _paragraphProperties.LineHeight; for (var index = 0; index < _textRuns.Count; index++) { @@ -1166,12 +1161,15 @@ namespace Avalonia.Media.TextFormatting } } - start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth, + var start = GetParagraphOffsetX(width, widthIncludingWhitespace, _paragraphWidth, _paragraphProperties.TextAlignment, _paragraphProperties.FlowDirection); if (!double.IsNaN(lineHeight) && !MathUtilities.IsZero(lineHeight)) { - height = lineHeight; + if(lineHeight > height) + { + height = lineHeight; + } } return new TextLineMetrics(widthIncludingWhitespace > _paragraphWidth, height, newLineLength, start, diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index a0890262e7..ebaa247da8 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -1,6 +1,5 @@ using System; using System.Globalization; -using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; @@ -59,7 +58,7 @@ namespace Avalonia.Skia var glyphIndex = (ushort)sourceInfo.Codepoint; - var glyphCluster = (int)sourceInfo.Cluster; + var glyphCluster = (int)(sourceInfo.Cluster); var glyphAdvance = GetGlyphAdvance(glyphPositions, i, textScale); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs index e3b9e5a8b1..a47638d2ec 100644 --- a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -665,17 +665,16 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting var text = "0123".AsMemory(); var shaperOption = new TextShaperOptions(Typeface.Default.GlyphTypeface, 10, 0, CultureInfo.CurrentCulture); - var shapedBuffer = TextShaper.Current.ShapeText(new ReadOnlySlice(text), shaperOption); - var firstRun = new ShapedTextCharacters(shapedBuffer, defaultProperties); + var firstRun = new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, 1, text.Length), shaperOption), defaultProperties); var textRuns = new List { new CustomDrawableRun(), firstRun, new CustomDrawableRun(), - new ShapedTextCharacters(shapedBuffer, defaultProperties), + new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length + 2, text.Length), shaperOption), defaultProperties), new CustomDrawableRun(), - new ShapedTextCharacters(shapedBuffer, defaultProperties) + new ShapedTextCharacters(TextShaper.Current.ShapeText(new ReadOnlySlice(text, text.Length * 2 + 3, text.Length), shaperOption), defaultProperties) }; var textSource = new FixedRunsTextSource(textRuns); @@ -691,15 +690,25 @@ namespace Avalonia.Skia.UnitTests.Media.TextFormatting Assert.Equal(1, textBounds.Count); Assert.Equal(textLine.WidthIncludingTrailingWhitespace, textBounds.Sum(x => x.Rectangle.Width)); - textBounds = textLine.GetTextBounds(0, firstRun.Text.Length); + textBounds = textLine.GetTextBounds(0, 1); Assert.Equal(1, textBounds.Count); - Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); + Assert.Equal(14, textBounds[0].Rectangle.Width); textBounds = textLine.GetTextBounds(0, firstRun.Text.Length + 1); Assert.Equal(1, textBounds.Count); Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width); + + textBounds = textLine.GetTextBounds(1, firstRun.Text.Length); + + Assert.Equal(1, textBounds.Count); + Assert.Equal(firstRun.Size.Width, textBounds[0].Rectangle.Width); + + textBounds = textLine.GetTextBounds(1, firstRun.Text.Length + 1); + + Assert.Equal(1, textBounds.Count); + Assert.Equal(firstRun.Size.Width + 14, textBounds[0].Rectangle.Width); } } From a34e0f50e2367a45da815ea8dd36b0de4dca167c Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 27 Apr 2022 17:55:00 +0200 Subject: [PATCH 10/10] Bump --- .../Media/TextFormatting/TextLayoutTests.cs | 3 --- 1 file changed, 3 deletions(-) diff --git a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs index 6ed4ba0d4a..b668f4d39e 100644 --- a/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs +++ b/tests/Avalonia.RenderTests/Media/TextFormatting/TextLayoutTests.cs @@ -1,11 +1,8 @@ using Avalonia.Media; -using Avalonia.Platform; using System; -using System.Collections.Generic; using System.Globalization; using System.Linq; using System.Runtime.InteropServices; -using System.Text; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Media.TextFormatting;