Browse Source

Merge pull request #6953 from Gillibald/feature/textBlockInlines

Add TextBlock inlines support
pull/7701/head
Benedikt Stebner 5 years ago
committed by GitHub
parent
commit
237a9a41ea
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 11
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  2. 10
      src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs
  3. 12
      src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs
  4. 17
      src/Avalonia.Controls/Documents/Bold.cs
  5. 71
      src/Avalonia.Controls/Documents/Inline.cs
  6. 123
      src/Avalonia.Controls/Documents/InlineCollection.cs
  7. 17
      src/Avalonia.Controls/Documents/Italic.cs
  8. 44
      src/Avalonia.Controls/Documents/LineBreak.cs
  9. 86
      src/Avalonia.Controls/Documents/Run.cs
  10. 95
      src/Avalonia.Controls/Documents/Span.cs
  11. 129
      src/Avalonia.Controls/Documents/TextElement.cs
  12. 15
      src/Avalonia.Controls/Documents/Underline.cs
  13. 1
      src/Avalonia.Controls/Properties/AssemblyInfo.cs
  14. 69
      src/Avalonia.Controls/TextBlock.cs
  15. 3
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  16. 2
      src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
  17. 20
      src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
  18. 6
      src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
  19. 8
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs
  20. 44
      tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
  21. 22
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
  22. 2
      tests/Avalonia.UnitTests/MockTextShaperImpl.cs

11
samples/ControlCatalog/Pages/TextBlockPage.xaml

@ -117,6 +117,17 @@
<TextBlock Text="👪 👨‍👩‍👧 👨‍👩‍👧‍👦" />
</StackPanel>
</Border>
<Border>
<TextBlock Margin="10" TextWrapping="Wrap">
This <Span FontWeight="Bold">is</Span> a
<Span Background="Silver" Foreground="Maroon">TextBlock</Span>
with <Span TextDecorations="Underline">several</Span>
<Span FontStyle="Italic">Span</Span> elements,
<Span Foreground="Blue">
using a <Bold>variety</Bold> of <Italic>styles</Italic>
</Span>.
</TextBlock>
</Border>
</WrapPanel>
</StackPanel>
</UserControl>

10
src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs

@ -0,0 +1,10 @@
using System;
namespace Avalonia.Metadata
{
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class TrimSurroundingWhitespaceAttribute : Attribute
{
}
}

12
src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs

@ -0,0 +1,12 @@
using System;
namespace Avalonia.Metadata
{
/// <summary>
/// Indicates that a collection type should be processed as being whitespace significant by a XAML processor.
/// </summary>
[AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)]
public class WhitespaceSignificantCollectionAttribute : Attribute
{
}
}

17
src/Avalonia.Controls/Documents/Bold.cs

@ -0,0 +1,17 @@
using Avalonia.Media;
namespace Avalonia.Controls.Documents
{
/// <summary>
/// Bold element - markup helper for indicating bolded content.
/// Equivalent to a Span with FontWeight property set to FontWeights.Bold.
/// Can contain other inline elements.
/// </summary>
public sealed class Bold : Span
{
static Bold()
{
FontWeightProperty.OverrideDefaultValue<Bold>(FontWeight.Bold);
}
}
}

71
src/Avalonia.Controls/Documents/Inline.cs

@ -0,0 +1,71 @@
using System.Collections.Generic;
using System.Text;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Utilities;
namespace Avalonia.Controls.Documents
{
/// <summary>
/// Inline element.
/// </summary>
public abstract class Inline : TextElement
{
/// <summary>
/// AvaloniaProperty for <see cref="TextDecorations" /> property.
/// </summary>
public static readonly StyledProperty<TextDecorationCollection> TextDecorationsProperty =
AvaloniaProperty.Register<Inline, TextDecorationCollection>(
nameof(TextDecorations));
/// <summary>
/// AvaloniaProperty for <see cref="BaselineAlignment" /> property.
/// </summary>
public static readonly StyledProperty<BaselineAlignment> BaselineAlignmentProperty =
AvaloniaProperty.Register<Inline, BaselineAlignment>(
nameof(BaselineAlignment),
BaselineAlignment.Baseline);
/// <summary>
/// The TextDecorations property specifies decorations that are added to the text of an element.
/// </summary>
public TextDecorationCollection TextDecorations
{
get { return GetValue(TextDecorationsProperty); }
set { SetValue(TextDecorationsProperty, value); }
}
/// <summary>
/// Describes how the baseline for a text-based element is positioned on the vertical axis,
/// relative to the established baseline for text.
/// </summary>
public BaselineAlignment BaselineAlignment
{
get { return GetValue(BaselineAlignmentProperty); }
set { SetValue(BaselineAlignmentProperty, value); }
}
internal abstract int BuildRun(StringBuilder stringBuilder, IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex);
internal abstract int AppendText(StringBuilder stringBuilder);
protected TextRunProperties CreateTextRunProperties()
{
return new GenericTextRunProperties(new Typeface(FontFamily, FontStyle, FontWeight), FontSize,
TextDecorations, Foreground, Background, BaselineAlignment);
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof(TextDecorations):
case nameof(BaselineAlignment):
Invalidate();
break;
}
}
}
}

123
src/Avalonia.Controls/Documents/InlineCollection.cs

@ -0,0 +1,123 @@
using System;
using System.Text;
using Avalonia.Collections;
using Avalonia.LogicalTree;
using Avalonia.Metadata;
namespace Avalonia.Controls.Documents
{
/// <summary>
/// A collection of <see cref="Inline"/>s.
/// </summary>
[WhitespaceSignificantCollection]
public class InlineCollection : AvaloniaList<Inline>
{
private string? _text = string.Empty;
/// <summary>
/// Initializes a new instance of the <see cref="InlineCollection"/> class.
/// </summary>
public InlineCollection(ILogical parent) : base(0)
{
ResetBehavior = ResetBehavior.Remove;
this.ForEachItem(
x =>
{
((ISetLogicalParent)x).SetParent(parent);
x.Invalidated += Invalidate;
Invalidate();
},
x =>
{
((ISetLogicalParent)x).SetParent(null);
x.Invalidated -= Invalidate;
Invalidate();
},
() => throw new NotSupportedException());
}
public bool HasComplexContent => Count > 0;
/// <summary>
/// Gets or adds the text held by the inlines collection.
/// <remarks>
/// Can be null for complex content.
/// </remarks>
/// </summary>
public string? Text
{
get
{
if (!HasComplexContent)
{
return _text;
}
var builder = new StringBuilder();
foreach(var inline in this)
{
inline.AppendText(builder);
}
return builder.ToString();
}
set
{
if (HasComplexContent)
{
Add(new Run(value));
}
else
{
_text = value;
}
}
}
/// <summary>
/// Add a text segment to the collection.
/// <remarks>
/// For non complex content this appends the text to the end of currently held text.
/// For complex content this adds a <see cref="Run"/> to the collection.
/// </remarks>
/// </summary>
/// <param name="text"></param>
public void Add(string text)
{
if (HasComplexContent)
{
Add(new Run(text));
}
else
{
_text += text;
}
}
public override void Add(Inline item)
{
if (!HasComplexContent)
{
base.Add(new Run(_text));
_text = string.Empty;
}
base.Add(item);
}
/// <summary>
/// Raised when an inline in the collection changes.
/// </summary>
public event EventHandler? Invalidated;
/// <summary>
/// Raises the <see cref="Invalidated"/> event.
/// </summary>
protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty);
private void Invalidate(object? sender, EventArgs e) => Invalidate();
}
}

17
src/Avalonia.Controls/Documents/Italic.cs

@ -0,0 +1,17 @@
using Avalonia.Media;
namespace Avalonia.Controls.Documents
{
/// <summary>
/// Italic element - markup helper for indicating italicized content.
/// Equivalent to a Span with FontStyle property set to FontStyles.Italic.
/// Can contain other inline elements.
/// </summary>
public sealed class Italic : Span
{
static Italic()
{
FontStyleProperty.OverrideDefaultValue<Italic>(FontStyle.Italic);
}
}
}

44
src/Avalonia.Controls/Documents/LineBreak.cs

@ -0,0 +1,44 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls.Documents
{
/// <summary>
/// LineBreak element that forces a line breaking.
/// </summary>
[TrimSurroundingWhitespace]
public class LineBreak : Inline
{
/// <summary>
/// Creates a new LineBreak instance.
/// </summary>
public LineBreak()
{
}
internal override int BuildRun(StringBuilder stringBuilder,
IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex)
{
var length = AppendText(stringBuilder);
textStyleOverrides.Add(new ValueSpan<TextRunProperties>(firstCharacterIndex, length,
CreateTextRunProperties()));
return length;
}
internal override int AppendText(StringBuilder stringBuilder)
{
var text = Environment.NewLine;
stringBuilder.Append(text);
return text.Length;
}
}
}

86
src/Avalonia.Controls/Documents/Run.cs

@ -0,0 +1,86 @@
using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Data;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls.Documents
{
/// <summary>
/// A terminal element in text flow hierarchy - contains a uniformatted run of unicode characters
/// </summary>
public class Run : Inline
{
/// <summary>
/// Initializes an instance of Run class.
/// </summary>
public Run()
{
}
/// <summary>
/// Initializes an instance of Run class specifying its text content.
/// </summary>
/// <param name="text">
/// Text content assigned to the Run.
/// </param>
public Run(string? text)
{
Text = text;
}
/// <summary>
/// Dependency property backing Text.
/// </summary>
/// <remarks>
/// Note that when a TextRange that intersects with this Run gets modified (e.g. by editing
/// a selection in RichTextBox), we will get two changes to this property since we delete
/// and then insert when setting the content of a TextRange.
/// </remarks>
public static readonly StyledProperty<string?> TextProperty = AvaloniaProperty.Register<Run, string?> (
nameof (Text), defaultBindingMode: BindingMode.TwoWay);
/// <summary>
/// The content spanned by this TextElement.
/// </summary>
[Content]
public string? Text {
get { return GetValue (TextProperty); }
set { SetValue (TextProperty, value); }
}
internal override int BuildRun(StringBuilder stringBuilder,
IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex)
{
var length = AppendText(stringBuilder);
textStyleOverrides.Add(new ValueSpan<TextRunProperties>(firstCharacterIndex, length,
CreateTextRunProperties()));
return length;
}
internal override int AppendText(StringBuilder stringBuilder)
{
var text = Text ?? "";
stringBuilder.Append(text);
return text.Length;
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof(Text):
Invalidate();
break;
}
}
}
}

95
src/Avalonia.Controls/Documents/Span.cs

@ -0,0 +1,95 @@
using System.Collections.Generic;
using System.Text;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls.Documents
{
/// <summary>
/// Span element used for grouping other Inline elements.
/// </summary>
public class Span : Inline
{
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly DirectProperty<Span, InlineCollection> InlinesProperty =
AvaloniaProperty.RegisterDirect<Span, InlineCollection>(
nameof(Inlines),
o => o.Inlines);
/// <summary>
/// Initializes a new instance of a Span element.
/// </summary>
public Span()
{
Inlines = new InlineCollection(this);
Inlines.Invalidated += (s, e) => Invalidate();
}
/// <summary>
/// Gets or sets the inlines.
/// </summary>
[Content]
public InlineCollection Inlines { get; }
internal override int BuildRun(StringBuilder stringBuilder, IList<ValueSpan<TextRunProperties>> textStyleOverrides, int firstCharacterIndex)
{
var length = 0;
if (Inlines.HasComplexContent)
{
foreach (var inline in Inlines)
{
var inlineLength = inline.BuildRun(stringBuilder, textStyleOverrides, firstCharacterIndex);
firstCharacterIndex += inlineLength;
length += inlineLength;
}
}
else
{
if (Inlines.Text == null)
{
return length;
}
stringBuilder.Append(Inlines.Text);
length = Inlines.Text.Length;
textStyleOverrides.Add(new ValueSpan<TextRunProperties>(firstCharacterIndex, length,
CreateTextRunProperties()));
}
return length;
}
internal override int AppendText(StringBuilder stringBuilder)
{
if (Inlines.HasComplexContent)
{
var length = 0;
foreach (var inline in Inlines)
{
length += inline.AppendText(stringBuilder);
}
return length;
}
if (Inlines.Text == null)
{
return 0;
}
stringBuilder.Append(Inlines.Text);
return Inlines.Text.Length;
}
}
}

129
src/Avalonia.Controls/Documents/TextElement.cs

@ -0,0 +1,129 @@
using System;
using Avalonia.Media;
namespace Avalonia.Controls.Documents
{
/// <summary>
/// TextElement is an base class for content in text based controls.
/// TextElements span other content, applying property values or providing structural information.
/// </summary>
public abstract class TextElement : StyledElement
{
/// <summary>
/// Defines the <see cref="Background"/> property.
/// </summary>
public static readonly StyledProperty<IBrush?> BackgroundProperty =
AvaloniaProperty.Register<TextElement, IBrush?>(nameof(Background), inherits: true);
/// <summary>
/// Defines the <see cref="FontFamily"/> property.
/// </summary>
public static readonly AttachedProperty<FontFamily> FontFamilyProperty =
TextBlock.FontFamilyProperty.AddOwner<TextElement>();
/// <summary>
/// Defines the <see cref="FontSize"/> property.
/// </summary>
public static readonly AttachedProperty<double> FontSizeProperty =
TextBlock.FontSizeProperty.AddOwner<TextElement>();
/// <summary>
/// Defines the <see cref="FontStyle"/> property.
/// </summary>
public static readonly AttachedProperty<FontStyle> FontStyleProperty =
TextBlock.FontStyleProperty.AddOwner<TextElement>();
/// <summary>
/// Defines the <see cref="FontWeight"/> property.
/// </summary>
public static readonly AttachedProperty<FontWeight> FontWeightProperty =
TextBlock.FontWeightProperty.AddOwner<TextElement>();
/// <summary>
/// Defines the <see cref="Foreground"/> property.
/// </summary>
public static readonly AttachedProperty<IBrush?> ForegroundProperty =
TextBlock.ForegroundProperty.AddOwner<TextElement>();
/// <summary>
/// Gets or sets a brush used to paint the control's background.
/// </summary>
public IBrush? Background
{
get { return GetValue(BackgroundProperty); }
set { SetValue(BackgroundProperty, value); }
}
/// <summary>
/// Gets or sets the font family.
/// </summary>
public FontFamily FontFamily
{
get { return GetValue(FontFamilyProperty); }
set { SetValue(FontFamilyProperty, value); }
}
/// <summary>
/// Gets or sets the font size.
/// </summary>
public double FontSize
{
get { return GetValue(FontSizeProperty); }
set { SetValue(FontSizeProperty, value); }
}
/// <summary>
/// Gets or sets the font style.
/// </summary>
public FontStyle FontStyle
{
get { return GetValue(FontStyleProperty); }
set { SetValue(FontStyleProperty, value); }
}
/// <summary>
/// Gets or sets the font weight.
/// </summary>
public FontWeight FontWeight
{
get { return GetValue(FontWeightProperty); }
set { SetValue(FontWeightProperty, value); }
}
/// <summary>
/// Gets or sets a brush used to paint the text.
/// </summary>
public IBrush? Foreground
{
get { return GetValue(ForegroundProperty); }
set { SetValue(ForegroundProperty, value); }
}
/// <summary>
/// Raised when the visual representation of the text element changes.
/// </summary>
public event EventHandler? Invalidated;
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof(Background):
case nameof(FontFamily):
case nameof(FontSize):
case nameof(FontStyle):
case nameof(FontWeight):
case nameof(Foreground):
Invalidate();
break;
}
}
/// <summary>
/// Raises the <see cref="Invalidate"/> event.
/// </summary>
protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty);
}
}

15
src/Avalonia.Controls/Documents/Underline.cs

@ -0,0 +1,15 @@
namespace Avalonia.Controls.Documents
{
/// <summary>
/// Underline element - markup helper for indicating superscript content.
/// Equivalent to a Span with TextDecorations property set to TextDecorations.Underlined.
/// Can contain other inline elements.
/// </summary>
public sealed class Underline : Span
{
static Underline()
{
TextDecorationsProperty.OverrideDefaultValue<Underline>(Media.TextDecorations.Underline);
}
}
}

1
src/Avalonia.Controls/Properties/AssemblyInfo.cs

@ -14,3 +14,4 @@ using Avalonia.Metadata;
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Templates")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Notifications")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Chrome")]
[assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Documents")]

69
src/Avalonia.Controls/TextBlock.cs

@ -1,5 +1,8 @@
using System;
using System.Collections.Generic;
using System.Reactive.Linq;
using System.Text;
using Avalonia.Controls.Documents;
using Avalonia.LogicalTree;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
@ -108,6 +111,14 @@ namespace Avalonia.Controls
o => o.Text,
(o, v) => o.Text = v);
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly DirectProperty<TextBlock, InlineCollection> InlinesProperty =
AvaloniaProperty.RegisterDirect<TextBlock, InlineCollection>(
nameof(Inlines),
o => o.Inlines);
/// <summary>
/// Defines the <see cref="TextAlignment"/> property.
/// </summary>
@ -132,7 +143,6 @@ namespace Avalonia.Controls
public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty =
AvaloniaProperty.Register<TextBlock, TextDecorationCollection?>(nameof(TextDecorations));
private string? _text;
private TextLayout? _textLayout;
private Size _constraint;
@ -151,7 +161,9 @@ namespace Avalonia.Controls
/// </summary>
public TextBlock()
{
_text = string.Empty;
Inlines = new InlineCollection(this);
Inlines.Invalidated += InlinesChanged;
}
/// <summary>
@ -186,13 +198,30 @@ namespace Avalonia.Controls
/// <summary>
/// Gets or sets the text.
/// </summary>
[Content]
public string? Text
{
get { return _text; }
set { SetAndRaise(TextProperty, ref _text, value); }
get => Inlines.Text;
set
{
var old = Text;
if (value == old)
{
return;
}
Inlines.Text = value;
RaisePropertyChanged(TextProperty, old, value);
}
}
/// <summary>
/// Gets or sets the inlines.
/// </summary>
[Content]
public InlineCollection Inlines { get; }
/// <summary>
/// Gets or sets the font family.
/// </summary>
@ -463,6 +492,23 @@ namespace Avalonia.Controls
/// <returns>A <see cref="TextLayout"/> object.</returns>
protected virtual TextLayout CreateTextLayout(Size constraint, string? text)
{
List<ValueSpan<TextRunProperties>>? textStyleOverrides = null;
if (Inlines.HasComplexContent)
{
textStyleOverrides = new List<ValueSpan<TextRunProperties>>(Inlines.Count);
var textPosition = 0;
var stringBuilder = new StringBuilder();
foreach (var inline in Inlines)
{
textPosition += inline.BuildRun(stringBuilder, textStyleOverrides, textPosition);
}
text = stringBuilder.ToString();
}
return new TextLayout(
text ?? string.Empty,
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
@ -476,7 +522,8 @@ namespace Avalonia.Controls
constraint.Width,
constraint.Height,
maxLines: MaxLines,
lineHeight: LineHeight);
lineHeight: LineHeight,
textStyleOverrides: textStyleOverrides);
}
/// <summary>
@ -491,6 +538,11 @@ namespace Avalonia.Controls
protected override Size MeasureOverride(Size availableSize)
{
if (!Inlines.HasComplexContent && string.IsNullOrEmpty(Text))
{
return new Size();
}
var padding = Padding;
_constraint = availableSize.Deflate(padding);
@ -552,6 +604,11 @@ namespace Avalonia.Controls
break;
}
}
}
private void InlinesChanged(object? sender, EventArgs e)
{
InvalidateTextLayout();
}
}
}

3
src/Avalonia.Visuals/Avalonia.Visuals.csproj

@ -12,6 +12,9 @@
<ProjectReference Include="..\Avalonia.Base\Avalonia.Base.csproj" />
<ProjectReference Include="..\Avalonia.Styling\Avalonia.Styling.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Remove="Media\TextFormatting\TextRunCache.cs" />
</ItemGroup>
<Import Project="..\..\build\Rx.props" />
<Import Project="..\..\build\System.Memory.props" />
<Import Project="..\..\build\ApiDiff.props" />

2
src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs

@ -69,7 +69,7 @@ namespace Avalonia.Media.TextFormatting
var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length);
if (textRange.Start + textRange.Length < text.Start)
if (textRange.Start + textRange.Length <= text.Start)
{
continue;
}

20
src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs

@ -79,14 +79,12 @@ namespace Avalonia.Media.TextFormatting
if(TryGetShapeableLength(text, previousTypeface.Value, out var fallbackCount, out _))
{
return new ShapeableTextCharacters(text.Take(fallbackCount),
new GenericTextRunProperties(previousTypeface.Value, defaultProperties.FontRenderingEmSize,
defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
}
}
return new ShapeableTextCharacters(text.Take(count),
new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
biDiLevel);
}
if (previousTypeface is not null)
@ -94,8 +92,7 @@ namespace Avalonia.Media.TextFormatting
if(TryGetShapeableLength(text, previousTypeface.Value, out count, out _))
{
return new ShapeableTextCharacters(text.Take(count),
new GenericTextRunProperties(previousTypeface.Value, defaultProperties.FontRenderingEmSize,
defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
defaultProperties.WithTypeface(previousTypeface.Value), biDiLevel);
}
}
@ -124,9 +121,8 @@ namespace Avalonia.Media.TextFormatting
if (matchFound && TryGetShapeableLength(text, currentTypeface, out count, out _))
{
//Fallback found
return new ShapeableTextCharacters(text.Take(count),
new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
return new ShapeableTextCharacters(text.Take(count), defaultProperties.WithTypeface(currentTypeface),
biDiLevel);
}
// no fallback found
@ -148,9 +144,7 @@ namespace Avalonia.Media.TextFormatting
count += grapheme.Text.Length;
}
return new ShapeableTextCharacters(text.Take(count),
new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize,
defaultProperties.TextDecorations, defaultProperties.ForegroundBrush), biDiLevel);
return new ShapeableTextCharacters(text.Take(count), defaultProperties, biDiLevel);
}
/// <summary>

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

@ -90,5 +90,11 @@ namespace Avalonia.Media.TextFormatting
{
return !Equals(left, right);
}
internal TextRunProperties WithTypeface(Typeface typeface)
{
return new GenericTextRunProperties(typeface, FontRenderingEmSize,
TextDecorations, ForegroundBrush, BackgroundBrush, BaselineAlignment);
}
}
}

8
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs

@ -39,6 +39,14 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
{
typeSystem.GetType("Avalonia.Metadata.ContentAttribute")
},
WhitespaceSignificantCollectionAttributes =
{
typeSystem.GetType("Avalonia.Metadata.WhitespaceSignificantCollectionAttribute")
},
TrimSurroundingWhitespaceAttributes =
{
typeSystem.GetType("Avalonia.Metadata.TrimSurroundingWhitespaceAttribute")
},
ProvideValueTarget = typeSystem.GetType("Avalonia.Markup.Xaml.IProvideValueTarget"),
RootObjectProvider = typeSystem.GetType("Avalonia.Markup.Xaml.IRootObjectProvider"),
RootObjectProviderIntermediateRootPropertyName = "IntermediateRootObject",

44
tests/Avalonia.Controls.UnitTests/TextBlockTests.cs

@ -1,3 +1,5 @@
using System;
using Avalonia.Controls.Documents;
using Avalonia.Data;
using Avalonia.Media;
using Avalonia.Rendering;
@ -60,5 +62,47 @@ namespace Avalonia.Controls.UnitTests
renderer.Verify(x => x.AddDirty(target), Times.Once);
}
[Fact]
public void Changing_InlinesCollection_Should_Invalidate_Measure()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new TextBlock();
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
target.Inlines.Add(new Run("Hello"));
Assert.False(target.IsMeasureValid);
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
}
}
[Fact]
public void Changing_Inlines_Properties_Should_Invalidate_Measure()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new TextBlock();
var inline = new Run("Hello");
target.Inlines.Add(inline);
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
inline.Text = "1337";
Assert.False(target.IsMeasureValid);
}
}
}
}

22
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs

@ -935,6 +935,28 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
}
}
[Fact]
public void Should_Parse_Tip_With_Comment()
{
var xaml = @"
<TextBlock xmlns='https://github.com/avaloniaui' Text='TextBlock with tooltip'>
<ToolTip.Tip>
<!--Comment-->
<ToolTip>
Foo
</ToolTip>
</ToolTip.Tip>
</TextBlock>";
var textBlock = AvaloniaRuntimeXamlLoader.Parse<TextBlock>(xaml);
var toolTip = ToolTip.GetTip(textBlock) as ToolTip;
Assert.NotNull(toolTip);
Assert.Equal("Foo", toolTip.Content);
}
private class SelectedItemsViewModel : INotifyPropertyChanged
{
public string[] Items { get; set; }

2
tests/Avalonia.UnitTests/MockTextShaperImpl.cs

@ -21,7 +21,7 @@ namespace Avalonia.UnitTests
var glyphIndex = typeface.GetGlyph(codepoint);
shapedBuffer[i] = new GlyphInfo(glyphIndex, glyphCluster);
shapedBuffer[i] = new GlyphInfo(glyphIndex, glyphCluster, 10);
i += count;
}

Loading…
Cancel
Save