Browse Source

Implement InlineUIContainer

pull/7946/head
Benedikt Stebner 4 years ago
parent
commit
768024a879
  1. 11
      src/Avalonia.Controls/Documents/Inline.cs
  2. 12
      src/Avalonia.Controls/Documents/InlineCollection.cs
  3. 121
      src/Avalonia.Controls/Documents/InlineUIContainer.cs
  4. 20
      src/Avalonia.Controls/Documents/LineBreak.cs
  5. 18
      src/Avalonia.Controls/Documents/Run.cs
  6. 43
      src/Avalonia.Controls/Documents/Span.cs
  7. 126
      src/Avalonia.Controls/TextBlock.cs
  8. 6
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs
  9. 23
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs
  10. 94
      src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

11
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<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex);
internal abstract void BuildTextRun(IList<TextRun> 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);
}
}

12
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)

121
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
{
/// <summary>
/// InlineUIContainer - a wrapper for embedded UIElements in text
/// flow content inline collections
/// </summary>
public class InlineUIContainer : Inline
{
/// <summary>
/// Defines the <see cref="Child"/> property.
/// </summary>
public static readonly StyledProperty<IControl> ChildProperty =
AvaloniaProperty.Register<InlineUIContainer, IControl>(nameof(Child));
static InlineUIContainer()
{
BaselineAlignmentProperty.OverrideDefaultValue<InlineUIContainer>(BaselineAlignment.Top);
}
/// <summary>
/// Initializes a new instance of InlineUIContainer element.
/// </summary>
/// <remarks>
/// The purpose of this element is to be a wrapper for UIElements
/// when they are embedded into text flow - as items of
/// InlineCollections.
/// </remarks>
public InlineUIContainer()
{
}
/// <summary>
/// Initializes an InlineBox specifying its child UIElement
/// </summary>
/// <param name="child">
/// UIElement set as a child of this inline item
/// </param>
public InlineUIContainer(IControl child)
{
Child = child;
}
/// <summary>
/// The content spanned by this TextElement.
/// </summary>
[Content]
public IControl Child
{
get => GetValue(ChildProperty);
set => SetValue(ChildProperty, value);
}
internal override void BuildTextRun(IList<TextRun> 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));
}
}
}
}

20
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<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex)
internal override void BuildTextRun(IList<TextRun> textRuns, IInlinesHost parent)
{
var length = AppendText(stringBuilder);
textStyleOverrides.Add(new ValueSpan<TextRunProperties>(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);
}
}
}

18
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<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex)
internal override void BuildTextRun(IList<TextRun> textRuns, IInlinesHost parent)
{
var length = AppendText(stringBuilder);
var text = (Text ?? "").AsMemory();
textStyleOverrides.Add(new ValueSpan<TextRunProperties>(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<T>(AvaloniaPropertyChangedEventArgs<T> change)

43
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<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex)
internal override void BuildTextRun(IList<TextRun> 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<TextRunProperties>(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;
}
}
}

126
src/Avalonia.Controls/TextBlock.cs

@ -14,7 +14,7 @@ namespace Avalonia.Controls
/// <summary>
/// A control that displays a block of text.
/// </summary>
public class TextBlock : Control
public class TextBlock : Control, IInlinesHost
{
/// <summary>
/// Defines the <see cref="Background"/> property.
@ -400,38 +400,41 @@ namespace Avalonia.Controls
/// <returns>A <see cref="TextLayout"/> object.</returns>
protected virtual TextLayout CreateTextLayout(Size constraint, string? text)
{
List<ValueSpan<TextRunProperties>>? 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<ValueSpan<TextRunProperties>>(Inlines.Count);
var textPosition = 0;
var stringBuilder = new StringBuilder();
var textRuns = new List<TextRun>();
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);
}
/// <summary>
@ -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<TextRun> _textRuns;
public InlinesTextSource(IReadOnlyList<TextRun> 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<char> _text;
private readonly TextRunProperties _defaultProperties;
public SimpleTextSource(ReadOnlySlice<char> 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);
}
}
}
}

6
src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs

@ -1,6 +1,4 @@
using Avalonia.Media.TextFormatting.Unicode;
namespace Avalonia.Media.TextFormatting
namespace Avalonia.Media.TextFormatting
{
/// <summary>
/// Represents a base class for text formatting.
@ -40,7 +38,7 @@ namespace Avalonia.Media.TextFormatting
/// <param name="previousLineBreak">A <see cref="TextLineBreak"/> value that specifies the text formatter state,
/// in terms of where the previous line in the paragraph was broken by the text formatting process.</param>
/// <returns>The formatted line.</returns>
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);
}
}

23
src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs

@ -9,7 +9,7 @@ namespace Avalonia.Media.TextFormatting
internal class TextFormatterImpl : TextFormatter
{
/// <inheritdoc cref="TextFormatter.FormatLine"/>
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<DrawableTextRun> textRuns, int firstTextSourceIndex, double paragraphWidth, out int measuredLength)
private static bool TryMeasureLength(IReadOnlyList<DrawableTextRun> 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;
}

94
src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs

@ -12,11 +12,12 @@ namespace Avalonia.Media.TextFormatting
{
private static readonly char[] s_empty = { ' ' };
private readonly ReadOnlySlice<char> _text;
private readonly ITextSource _textSource;
private readonly TextParagraphProperties _paragraphProperties;
private readonly IReadOnlyList<ValueSpan<TextRunProperties>>? _textStyleOverrides;
private readonly TextTrimming _textTrimming;
private int _textSourceLength;
/// <summary>
/// Initializes a new instance of the <see cref="TextLayout" /> class.
/// </summary>
@ -50,17 +51,49 @@ namespace Avalonia.Media.TextFormatting
int maxLines = 0,
IReadOnlyList<ValueSpan<TextRunProperties>>? textStyleOverrides = null)
{
_text = string.IsNullOrEmpty(text) ?
new ReadOnlySlice<char>() :
new ReadOnlySlice<char>(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();
}
/// <summary>
/// Initializes a new instance of the <see cref="TextLayout" /> class.
/// </summary>
/// <param name="textSource">The text source.</param>
/// <param name="paragraphProperties">The default text paragraph properties.</param>
/// <param name="textTrimming">The text trimming.</param>
/// <param name="maxWidth">The maximum width.</param>
/// <param name="maxHeight">The maximum height.</param>
/// <param name="lineHeight">The height of each line of text.</param>
/// <param name="maxLines">The maximum number of text lines.</param>
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<TextLine> 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);

Loading…
Cancel
Save