From 95769ff007b25c606c93dc8e55435154506a4363 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 23 Feb 2022 15:01:13 +0100 Subject: [PATCH 1/2] Add TextBlock inlines support --- .../ControlCatalog/Pages/TextBlockPage.xaml | 11 ++ .../TrimSurroundingWhitespaceAttribute.cs | 10 ++ ...hitespaceSignificantCollectionAttribute.cs | 12 ++ src/Avalonia.Controls/Documents/Bold.cs | 17 +++ src/Avalonia.Controls/Documents/Inline.cs | 71 ++++++++++ .../Documents/InlineCollection.cs | 123 +++++++++++++++++ src/Avalonia.Controls/Documents/Italic.cs | 17 +++ src/Avalonia.Controls/Documents/LineBreak.cs | 44 ++++++ src/Avalonia.Controls/Documents/Run.cs | 86 ++++++++++++ src/Avalonia.Controls/Documents/Span.cs | 95 +++++++++++++ .../Documents/TextElement.cs | 129 ++++++++++++++++++ src/Avalonia.Controls/Documents/Underline.cs | 15 ++ .../Properties/AssemblyInfo.cs | 1 + src/Avalonia.Controls/TextBlock.cs | 69 +++++++++- src/Avalonia.Visuals/Avalonia.Visuals.csproj | 3 + .../TextFormatting/FormattedTextSource.cs | 2 +- .../Media/TextFormatting/TextCharacters.cs | 20 +-- .../Media/TextFormatting/TextRunProperties.cs | 6 + .../AvaloniaXamlIlLanguage.cs | 8 ++ .../TextBlockTests.cs | 44 ++++++ .../Xaml/BasicTests.cs | 22 +++ .../Avalonia.UnitTests/MockTextShaperImpl.cs | 2 +- 22 files changed, 786 insertions(+), 21 deletions(-) create mode 100644 src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs create mode 100644 src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs create mode 100644 src/Avalonia.Controls/Documents/Bold.cs create mode 100644 src/Avalonia.Controls/Documents/Inline.cs create mode 100644 src/Avalonia.Controls/Documents/InlineCollection.cs create mode 100644 src/Avalonia.Controls/Documents/Italic.cs create mode 100644 src/Avalonia.Controls/Documents/LineBreak.cs create mode 100644 src/Avalonia.Controls/Documents/Run.cs create mode 100644 src/Avalonia.Controls/Documents/Span.cs create mode 100644 src/Avalonia.Controls/Documents/TextElement.cs create mode 100644 src/Avalonia.Controls/Documents/Underline.cs diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml index 9998a20d42..fe9455bd29 100644 --- a/samples/ControlCatalog/Pages/TextBlockPage.xaml +++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml @@ -117,6 +117,17 @@ + + + This is a + TextBlock + with several + Span elements, + + using a variety of styles + . + + diff --git a/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs b/src/Avalonia.Base/Metadata/TrimSurroundingWhitespaceAttribute.cs new file mode 100644 index 0000000000..c46891b3ad --- /dev/null +++ b/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 + { + + } +} diff --git a/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs b/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs new file mode 100644 index 0000000000..aeaa38dad9 --- /dev/null +++ b/src/Avalonia.Base/Metadata/WhitespaceSignificantCollectionAttribute.cs @@ -0,0 +1,12 @@ +using System; + +namespace Avalonia.Metadata +{ + /// + /// Indicates that a collection type should be processed as being whitespace significant by a XAML processor. + /// + [AttributeUsage(AttributeTargets.Class, AllowMultiple = false, Inherited = true)] + public class WhitespaceSignificantCollectionAttribute : Attribute + { + } +} diff --git a/src/Avalonia.Controls/Documents/Bold.cs b/src/Avalonia.Controls/Documents/Bold.cs new file mode 100644 index 0000000000..7d0a9130ae --- /dev/null +++ b/src/Avalonia.Controls/Documents/Bold.cs @@ -0,0 +1,17 @@ +using Avalonia.Media; + +namespace Avalonia.Controls.Documents +{ + /// + /// Bold element - markup helper for indicating bolded content. + /// Equivalent to a Span with FontWeight property set to FontWeights.Bold. + /// Can contain other inline elements. + /// + public sealed class Bold : Span + { + static Bold() + { + FontWeightProperty.OverrideDefaultValue(FontWeight.Bold); + } + } +} diff --git a/src/Avalonia.Controls/Documents/Inline.cs b/src/Avalonia.Controls/Documents/Inline.cs new file mode 100644 index 0000000000..5b63f95432 --- /dev/null +++ b/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 +{ + /// + /// Inline element. + /// + public abstract class Inline : TextElement + { + /// + /// AvaloniaProperty for property. + /// + public static readonly StyledProperty TextDecorationsProperty = + AvaloniaProperty.Register( + nameof(TextDecorations)); + + /// + /// AvaloniaProperty for property. + /// + public static readonly StyledProperty BaselineAlignmentProperty = + AvaloniaProperty.Register( + nameof(BaselineAlignment), + BaselineAlignment.Baseline); + + /// + /// The TextDecorations property specifies decorations that are added to the text of an element. + /// + public TextDecorationCollection TextDecorations + { + get { return GetValue(TextDecorationsProperty); } + set { SetValue(TextDecorationsProperty, value); } + } + + /// + /// Describes how the baseline for a text-based element is positioned on the vertical axis, + /// relative to the established baseline for text. + /// + public BaselineAlignment BaselineAlignment + { + get { return GetValue(BaselineAlignmentProperty); } + set { SetValue(BaselineAlignmentProperty, value); } + } + + internal abstract int BuildRun(StringBuilder stringBuilder, IList> 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(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + switch (change.Property.Name) + { + case nameof(TextDecorations): + case nameof(BaselineAlignment): + Invalidate(); + break; + } + } + } +} diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs new file mode 100644 index 0000000000..45c715c13a --- /dev/null +++ b/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 +{ + /// + /// A collection of s. + /// + [WhitespaceSignificantCollection] + public class InlineCollection : AvaloniaList + { + private string? _text = string.Empty; + + /// + /// Initializes a new instance of the class. + /// + 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; + + /// + /// Gets or adds the text held by the inlines collection. + /// + /// Can be null for complex content. + /// + /// + 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; + } + } + } + + /// + /// Add a text segment to the collection. + /// + /// For non complex content this appends the text to the end of currently held text. + /// For complex content this adds a to the collection. + /// + /// + /// + 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); + } + + /// + /// Raised when an inline in the collection changes. + /// + public event EventHandler? Invalidated; + + /// + /// Raises the event. + /// + protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty); + + private void Invalidate(object? sender, EventArgs e) => Invalidate(); + } +} diff --git a/src/Avalonia.Controls/Documents/Italic.cs b/src/Avalonia.Controls/Documents/Italic.cs new file mode 100644 index 0000000000..e9f4698fc4 --- /dev/null +++ b/src/Avalonia.Controls/Documents/Italic.cs @@ -0,0 +1,17 @@ +using Avalonia.Media; + +namespace Avalonia.Controls.Documents +{ + /// + /// Italic element - markup helper for indicating italicized content. + /// Equivalent to a Span with FontStyle property set to FontStyles.Italic. + /// Can contain other inline elements. + /// + public sealed class Italic : Span + { + static Italic() + { + FontStyleProperty.OverrideDefaultValue(FontStyle.Italic); + } + } +} diff --git a/src/Avalonia.Controls/Documents/LineBreak.cs b/src/Avalonia.Controls/Documents/LineBreak.cs new file mode 100644 index 0000000000..5e0cd1d387 --- /dev/null +++ b/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 +{ + /// + /// LineBreak element that forces a line breaking. + /// + [TrimSurroundingWhitespace] + public class LineBreak : Inline + { + /// + /// Creates a new LineBreak instance. + /// + public LineBreak() + { + } + + internal override int BuildRun(StringBuilder stringBuilder, + IList> textStyleOverrides, int firstCharacterIndex) + { + var length = AppendText(stringBuilder); + + textStyleOverrides.Add(new ValueSpan(firstCharacterIndex, length, + CreateTextRunProperties())); + + return length; + } + + internal override int AppendText(StringBuilder stringBuilder) + { + var text = Environment.NewLine; + + stringBuilder.Append(text); + + return text.Length; + } + } +} + diff --git a/src/Avalonia.Controls/Documents/Run.cs b/src/Avalonia.Controls/Documents/Run.cs new file mode 100644 index 0000000000..a7dd5fd94f --- /dev/null +++ b/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 +{ + /// + /// A terminal element in text flow hierarchy - contains a uniformatted run of unicode characters + /// + public class Run : Inline + { + /// + /// Initializes an instance of Run class. + /// + public Run() + { + } + + /// + /// Initializes an instance of Run class specifying its text content. + /// + /// + /// Text content assigned to the Run. + /// + public Run(string? text) + { + Text = text; + } + + /// + /// Dependency property backing Text. + /// + /// + /// 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. + /// + public static readonly StyledProperty TextProperty = AvaloniaProperty.Register ( + nameof (Text), defaultBindingMode: BindingMode.TwoWay); + + /// + /// The content spanned by this TextElement. + /// + [Content] + public string? Text { + get { return GetValue (TextProperty); } + set { SetValue (TextProperty, value); } + } + + internal override int BuildRun(StringBuilder stringBuilder, + IList> textStyleOverrides, int firstCharacterIndex) + { + var length = AppendText(stringBuilder); + + textStyleOverrides.Add(new ValueSpan(firstCharacterIndex, length, + CreateTextRunProperties())); + + return length; + } + + internal override int AppendText(StringBuilder stringBuilder) + { + var text = Text ?? ""; + + stringBuilder.Append(text); + + return text.Length; + } + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) + { + base.OnPropertyChanged(change); + + switch (change.Property.Name) + { + case nameof(Text): + Invalidate(); + break; + } + } + } +} diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs new file mode 100644 index 0000000000..c086997b07 --- /dev/null +++ b/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 +{ + /// + /// Span element used for grouping other Inline elements. + /// + public class Span : Inline + { + /// + /// Defines the property. + /// + public static readonly DirectProperty InlinesProperty = + AvaloniaProperty.RegisterDirect( + nameof(Inlines), + o => o.Inlines); + + /// + /// Initializes a new instance of a Span element. + /// + public Span() + { + Inlines = new InlineCollection(this); + + Inlines.Invalidated += (s, e) => Invalidate(); + } + + /// + /// Gets or sets the inlines. + /// + [Content] + public InlineCollection Inlines { get; } + + internal override int BuildRun(StringBuilder stringBuilder, IList> 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(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; + } + } +} diff --git a/src/Avalonia.Controls/Documents/TextElement.cs b/src/Avalonia.Controls/Documents/TextElement.cs new file mode 100644 index 0000000000..4083524881 --- /dev/null +++ b/src/Avalonia.Controls/Documents/TextElement.cs @@ -0,0 +1,129 @@ +using System; +using Avalonia.Media; + +namespace Avalonia.Controls.Documents +{ + /// + /// TextElement is an base class for content in text based controls. + /// TextElements span other content, applying property values or providing structural information. + /// + public abstract class TextElement : StyledElement + { + /// + /// Defines the property. + /// + public static readonly StyledProperty BackgroundProperty = + AvaloniaProperty.Register(nameof(Background), inherits: true); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontFamilyProperty = + TextBlock.FontFamilyProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontSizeProperty = + TextBlock.FontSizeProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontStyleProperty = + TextBlock.FontStyleProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty FontWeightProperty = + TextBlock.FontWeightProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly AttachedProperty ForegroundProperty = + TextBlock.ForegroundProperty.AddOwner(); + + /// + /// Gets or sets a brush used to paint the control's background. + /// + public IBrush? Background + { + get { return GetValue(BackgroundProperty); } + set { SetValue(BackgroundProperty, value); } + } + + /// + /// Gets or sets the font family. + /// + public FontFamily FontFamily + { + get { return GetValue(FontFamilyProperty); } + set { SetValue(FontFamilyProperty, value); } + } + + /// + /// Gets or sets the font size. + /// + public double FontSize + { + get { return GetValue(FontSizeProperty); } + set { SetValue(FontSizeProperty, value); } + } + + /// + /// Gets or sets the font style. + /// + public FontStyle FontStyle + { + get { return GetValue(FontStyleProperty); } + set { SetValue(FontStyleProperty, value); } + } + + /// + /// Gets or sets the font weight. + /// + public FontWeight FontWeight + { + get { return GetValue(FontWeightProperty); } + set { SetValue(FontWeightProperty, value); } + } + + /// + /// Gets or sets a brush used to paint the text. + /// + public IBrush? Foreground + { + get { return GetValue(ForegroundProperty); } + set { SetValue(ForegroundProperty, value); } + } + + /// + /// Raised when the visual representation of the text element changes. + /// + public event EventHandler? Invalidated; + + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs 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; + } + } + + /// + /// Raises the event. + /// + protected void Invalidate() => Invalidated?.Invoke(this, EventArgs.Empty); + } +} diff --git a/src/Avalonia.Controls/Documents/Underline.cs b/src/Avalonia.Controls/Documents/Underline.cs new file mode 100644 index 0000000000..fcd46c8439 --- /dev/null +++ b/src/Avalonia.Controls/Documents/Underline.cs @@ -0,0 +1,15 @@ +namespace Avalonia.Controls.Documents +{ + /// + /// Underline element - markup helper for indicating superscript content. + /// Equivalent to a Span with TextDecorations property set to TextDecorations.Underlined. + /// Can contain other inline elements. + /// + public sealed class Underline : Span + { + static Underline() + { + TextDecorationsProperty.OverrideDefaultValue(Media.TextDecorations.Underline); + } + } +} diff --git a/src/Avalonia.Controls/Properties/AssemblyInfo.cs b/src/Avalonia.Controls/Properties/AssemblyInfo.cs index 05561a38ef..302a2bbc13 100644 --- a/src/Avalonia.Controls/Properties/AssemblyInfo.cs +++ b/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")] diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index e41328b79a..c11f8434f8 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/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; @@ -99,6 +102,14 @@ namespace Avalonia.Controls o => o.Text, (o, v) => o.Text = v); + /// + /// Defines the property. + /// + public static readonly DirectProperty InlinesProperty = + AvaloniaProperty.RegisterDirect( + nameof(Inlines), + o => o.Inlines); + /// /// Defines the property. /// @@ -123,7 +134,6 @@ namespace Avalonia.Controls public static readonly StyledProperty TextDecorationsProperty = AvaloniaProperty.Register(nameof(TextDecorations)); - private string? _text; private TextLayout? _textLayout; private Size _constraint; @@ -142,7 +152,9 @@ namespace Avalonia.Controls /// public TextBlock() { - _text = string.Empty; + Inlines = new InlineCollection(this); + + Inlines.Invalidated += InlinesChanged; } /// @@ -177,13 +189,30 @@ namespace Avalonia.Controls /// /// Gets or sets the text. /// - [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); + } } + /// + /// Gets or sets the inlines. + /// + [Content] + public InlineCollection Inlines { get; } + /// /// Gets or sets the font family. /// @@ -430,6 +459,23 @@ namespace Avalonia.Controls /// A object. protected virtual TextLayout CreateTextLayout(Size constraint, string? text) { + List>? textStyleOverrides = null; + + if (Inlines.HasComplexContent) + { + textStyleOverrides = new List>(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), @@ -443,7 +489,8 @@ namespace Avalonia.Controls constraint.Width, constraint.Height, maxLines: MaxLines, - lineHeight: LineHeight); + lineHeight: LineHeight, + textStyleOverrides: textStyleOverrides); } /// @@ -458,6 +505,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); @@ -519,6 +571,11 @@ namespace Avalonia.Controls break; } } + } + + private void InlinesChanged(object? sender, EventArgs e) + { + InvalidateTextLayout(); } } } diff --git a/src/Avalonia.Visuals/Avalonia.Visuals.csproj b/src/Avalonia.Visuals/Avalonia.Visuals.csproj index 6c978e970e..a0b1f99fa1 100644 --- a/src/Avalonia.Visuals/Avalonia.Visuals.csproj +++ b/src/Avalonia.Visuals/Avalonia.Visuals.csproj @@ -12,6 +12,9 @@ + + + diff --git a/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs b/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs index 98344141f1..1b0feaa718 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs +++ b/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; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs index faa73719c8..09d9d9fbcd 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs +++ b/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); } } @@ -123,9 +120,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 @@ -147,9 +143,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); } /// diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs index 513778b596..99fcbd805f 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs +++ b/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); + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs index 1db0208310..8ed94f6b20 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/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", diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index b180a536a5..0ed1f8d2d0 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs +++ b/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); + } + } } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs index f20faa2287..e51fff5416 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs +++ b/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 = @" + + + + + Foo + + + "; + + var textBlock = AvaloniaRuntimeXamlLoader.Parse(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; } diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs index 3018c07819..fc22791102 100644 --- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs +++ b/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; } From bdd576bfc066021f69944c0ae80bf9ce0c19a137 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 24 Feb 2022 22:22:33 +0100 Subject: [PATCH 2/2] Removed TopLevelImpl validating layer. The validating layer introduced in #7369 has done its job and caught a few bugs, but was causing difficulties in certain places so removing it now. Fixes #7573 --- src/Avalonia.Controls/Primitives/PopupRoot.cs | 2 +- src/Avalonia.Controls/TopLevel.cs | 2 - src/Avalonia.Controls/ValidatingToplevel.cs | 344 ------------------ src/Avalonia.Controls/Window.cs | 11 +- src/Avalonia.Controls/WindowBase.cs | 9 +- .../WindowTests.cs | 2 +- tests/Avalonia.LeakTests/ControlTests.cs | 4 +- 7 files changed, 13 insertions(+), 361 deletions(-) delete mode 100644 src/Avalonia.Controls/ValidatingToplevel.cs diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index be447ea512..a9a362a762 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -44,7 +44,7 @@ namespace Avalonia.Controls.Primitives /// The dependency resolver to use. If null the default dependency resolver will be used. /// public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver? dependencyResolver) - : base(ValidatingPopupImpl.Wrap(impl), dependencyResolver) + : base(impl, dependencyResolver) { _parent = parent; } diff --git a/src/Avalonia.Controls/TopLevel.cs b/src/Avalonia.Controls/TopLevel.cs index 6bba889748..a4fe154515 100644 --- a/src/Avalonia.Controls/TopLevel.cs +++ b/src/Avalonia.Controls/TopLevel.cs @@ -134,8 +134,6 @@ namespace Avalonia.Controls "Could not create window implementation: maybe no windowing subsystem was initialized?"); } - impl = ValidatingToplevelImpl.Wrap(impl); - PlatformImpl = impl; _actualTransparencyLevel = PlatformImpl.TransparencyLevel; diff --git a/src/Avalonia.Controls/ValidatingToplevel.cs b/src/Avalonia.Controls/ValidatingToplevel.cs deleted file mode 100644 index 7e15bf4879..0000000000 --- a/src/Avalonia.Controls/ValidatingToplevel.cs +++ /dev/null @@ -1,344 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Controls.Platform; -using Avalonia.Controls.Primitives.PopupPositioning; -using Avalonia.Input; -using Avalonia.Input.Raw; -using Avalonia.Input.TextInput; -using Avalonia.Platform; -using Avalonia.Rendering; - -namespace Avalonia.Controls; - -internal class ValidatingToplevelImpl : ITopLevelImpl, ITopLevelImplWithNativeControlHost, - ITopLevelImplWithNativeMenuExporter, ITopLevelImplWithTextInputMethod -{ - private readonly ITopLevelImpl _impl; - private bool _disposed; - - public ValidatingToplevelImpl(ITopLevelImpl impl) - { - _impl = impl ?? throw new InvalidOperationException( - "Could not create TopLevel implementation: maybe no windowing subsystem was initialized?"); - } - - public void Dispose() - { - _disposed = true; - _impl.Dispose(); - } - - protected void CheckDisposed() - { - if (_disposed) - throw new ObjectDisposedException(_impl.GetType().FullName); - } - - protected ITopLevelImpl Inner - { - get - { - CheckDisposed(); - return _impl; - } - } - - public static ITopLevelImpl Wrap(ITopLevelImpl impl) - { -#if DEBUG - if (impl is ValidatingToplevelImpl) - return impl; - return new ValidatingToplevelImpl(impl); -#else - return impl; -#endif - } - - public Size ClientSize => Inner.ClientSize; - public Size? FrameSize => Inner.FrameSize; - public double RenderScaling => Inner.RenderScaling; - public IEnumerable Surfaces => Inner.Surfaces; - - public Action? Input - { - get => Inner.Input; - set => Inner.Input = value; - } - - public Action? Paint - { - get => Inner.Paint; - set => Inner.Paint = value; - } - - public Action? Resized - { - get => Inner.Resized; - set => Inner.Resized = value; - } - - public Action? ScalingChanged - { - get => Inner.ScalingChanged; - set => Inner.ScalingChanged = value; - } - - public Action? TransparencyLevelChanged - { - get => Inner.TransparencyLevelChanged; - set => Inner.TransparencyLevelChanged = value; - } - - public IRenderer CreateRenderer(IRenderRoot root) => Inner.CreateRenderer(root); - - public void Invalidate(Rect rect) => Inner.Invalidate(rect); - - public void SetInputRoot(IInputRoot inputRoot) => Inner.SetInputRoot(inputRoot); - - public Point PointToClient(PixelPoint point) => Inner.PointToClient(point); - - public PixelPoint PointToScreen(Point point) => Inner.PointToScreen(point); - - public void SetCursor(ICursorImpl? cursor) => Inner.SetCursor(cursor); - - public Action? Closed - { - get => Inner.Closed; - set => Inner.Closed = value; - } - - public Action? LostFocus - { - get => Inner.LostFocus; - set => Inner.LostFocus = value; - } - - // Exception: for some reason we are notifying platform mouse device from TopLevel.cs - public IMouseDevice MouseDevice => _impl.MouseDevice; - public IPopupImpl? CreatePopup() => Inner.CreatePopup(); - - public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) => - Inner.SetTransparencyLevelHint(transparencyLevel); - - - public WindowTransparencyLevel TransparencyLevel => Inner.TransparencyLevel; - public AcrylicPlatformCompensationLevels AcrylicCompensationLevels => Inner.AcrylicCompensationLevels; - public INativeControlHostImpl? NativeControlHost => (Inner as ITopLevelImplWithNativeControlHost)?.NativeControlHost; - - public ITopLevelNativeMenuExporter? NativeMenuExporter => - (Inner as ITopLevelImplWithNativeMenuExporter)?.NativeMenuExporter; - - public ITextInputMethodImpl? TextInputMethod => (Inner as ITopLevelImplWithTextInputMethod)?.TextInputMethod; -} - -internal class ValidatingWindowBaseImpl : ValidatingToplevelImpl, IWindowBaseImpl -{ - private readonly IWindowBaseImpl _impl; - - public ValidatingWindowBaseImpl(IWindowBaseImpl impl) : base(impl) - { - _impl = impl; - } - - protected new IWindowBaseImpl Inner - { - get - { - CheckDisposed(); - return _impl; - } - } - - public static IWindowBaseImpl Wrap(IWindowBaseImpl impl) - { -#if DEBUG - if (impl is ValidatingToplevelImpl) - return impl; - return new ValidatingWindowBaseImpl(impl); -#else - return impl; -#endif - } - - public void Show(bool activate, bool isDialog) => Inner.Show(activate, isDialog); - - public void Hide() => Inner.Hide(); - - public double DesktopScaling => Inner.DesktopScaling; - public PixelPoint Position => Inner.Position; - - public Action? PositionChanged - { - get => Inner.PositionChanged; - set => Inner.PositionChanged = value; - } - - public void Activate() => Inner.Activate(); - - public Action? Deactivated - { - get => Inner.Deactivated; - set => Inner.Deactivated = value; - } - - public Action? Activated - { - get => Inner.Activated; - set => Inner.Activated = value; - } - - public IPlatformHandle Handle => Inner.Handle; - public Size MaxAutoSizeHint => Inner.MaxAutoSizeHint; - public void SetTopmost(bool value) => Inner.SetTopmost(value); - public IScreenImpl Screen => Inner.Screen; -} - -internal class ValidatingWindowImpl : ValidatingWindowBaseImpl, IWindowImpl -{ - private readonly IWindowImpl _impl; - - public ValidatingWindowImpl(IWindowImpl impl) : base(impl) - { - _impl = impl; - } - - protected new IWindowImpl Inner - { - get - { - CheckDisposed(); - return _impl; - } - } - - public static IWindowImpl Unwrap(IWindowImpl impl) - { - if (impl is ValidatingWindowImpl v) - return v.Inner; - return impl; - } - - public static IWindowImpl Wrap(IWindowImpl impl) - { -#if DEBUG - if (impl is ValidatingToplevelImpl) - return impl; - return new ValidatingWindowImpl(impl); -#else - return impl; -#endif - } - - public WindowState WindowState - { - get => Inner.WindowState; - set => Inner.WindowState = value; - } - - public Action WindowStateChanged - { - get => Inner.WindowStateChanged; - set => Inner.WindowStateChanged = value; - } - - public void SetTitle(string? title) => Inner.SetTitle(title); - - public void SetParent(IWindowImpl parent) - { - //Workaround. SetParent will cast IWindowImpl to WindowImpl but ValidatingWindowImpl isn't actual WindowImpl so it will fail with InvalidCastException. - if (parent is ValidatingWindowImpl validatingToplevelImpl) - { - Inner.SetParent(validatingToplevelImpl.Inner); - } - else - { - Inner.SetParent(parent); - } - } - - public void SetEnabled(bool enable) => Inner.SetEnabled(enable); - - public Action GotInputWhenDisabled - { - get => Inner.GotInputWhenDisabled; - set => Inner.GotInputWhenDisabled = value; - } - - public void SetSystemDecorations(SystemDecorations enabled) => Inner.SetSystemDecorations(enabled); - - public void SetIcon(IWindowIconImpl? icon) => Inner.SetIcon(icon); - - public void ShowTaskbarIcon(bool value) => Inner.ShowTaskbarIcon(value); - - public void CanResize(bool value) => Inner.CanResize(value); - - public Func Closing - { - get => Inner.Closing; - set => Inner.Closing = value; - } - - public bool IsClientAreaExtendedToDecorations => Inner.IsClientAreaExtendedToDecorations; - - public Action ExtendClientAreaToDecorationsChanged - { - get => Inner.ExtendClientAreaToDecorationsChanged; - set => Inner.ExtendClientAreaToDecorationsChanged = value; - } - - public bool NeedsManagedDecorations => Inner.NeedsManagedDecorations; - public Thickness ExtendedMargins => Inner.ExtendedMargins; - public Thickness OffScreenMargin => Inner.OffScreenMargin; - public void BeginMoveDrag(PointerPressedEventArgs e) => Inner.BeginMoveDrag(e); - - public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => Inner.BeginResizeDrag(edge, e); - - public void Resize(Size clientSize, PlatformResizeReason reason) => - Inner.Resize(clientSize, reason); - - public void Move(PixelPoint point) => Inner.Move(point); - - public void SetMinMaxSize(Size minSize, Size maxSize) => Inner.SetMinMaxSize(minSize, maxSize); - - public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) => - Inner.SetExtendClientAreaToDecorationsHint(extendIntoClientAreaHint); - - public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) => - Inner.SetExtendClientAreaChromeHints(hints); - - public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) => - Inner.SetExtendClientAreaTitleBarHeightHint(titleBarHeight); -} - -internal class ValidatingPopupImpl : ValidatingWindowBaseImpl, IPopupImpl -{ - private readonly IPopupImpl _impl; - - public ValidatingPopupImpl(IPopupImpl impl) : base(impl) - { - _impl = impl; - } - - protected new IPopupImpl Inner - { - get - { - CheckDisposed(); - return _impl; - } - } - - public static IPopupImpl Wrap(IPopupImpl impl) - { -#if DEBUG - if (impl is ValidatingToplevelImpl) - return impl; - return new ValidatingPopupImpl(impl); -#else - return impl; -#endif - } - - public IPopupPositioner PopupPositioner => Inner.PopupPositioner; - public void SetWindowManagerAddShadowHint(bool enabled) => Inner.SetWindowManagerAddShadowHint(enabled); -} diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 2e31e1095d..e138a05c7d 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -237,14 +237,13 @@ namespace Avalonia.Controls /// /// The window implementation. public Window(IWindowImpl impl) - : base(ValidatingWindowImpl.Wrap(impl)) + : base(impl) { - var wrapped = (IWindowImpl)base.PlatformImpl!; - wrapped.Closing = HandleClosing; - wrapped.GotInputWhenDisabled = OnGotInputWhenDisabled; - wrapped.WindowStateChanged = HandleWindowStateChanged; + impl.Closing = HandleClosing; + impl.GotInputWhenDisabled = OnGotInputWhenDisabled; + impl.WindowStateChanged = HandleWindowStateChanged; _maxPlatformClientSize = PlatformImpl?.MaxAutoSizeHint ?? default(Size); - wrapped.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; + impl.ExtendClientAreaToDecorationsChanged = ExtendClientAreaToDecorationsChanged; this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl?.Resize(x, PlatformResizeReason.Application)); PlatformImpl?.ShowTaskbarIcon(ShowInTaskbar); diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 4464491020..cebdd8d897 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -57,13 +57,12 @@ namespace Avalonia.Controls { } - public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver? dependencyResolver) : base(ValidatingWindowBaseImpl.Wrap(impl), dependencyResolver) + public WindowBase(IWindowBaseImpl impl, IAvaloniaDependencyResolver? dependencyResolver) : base(impl, dependencyResolver) { Screens = new Screens(PlatformImpl?.Screen); - var wrapped = PlatformImpl!; - wrapped.Activated = HandleActivated; - wrapped.Deactivated = HandleDeactivated; - wrapped.PositionChanged = HandlePositionChanged; + impl.Activated = HandleActivated; + impl.Deactivated = HandleDeactivated; + impl.PositionChanged = HandlePositionChanged; } /// diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 4166242455..eb128ef038 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -821,7 +821,7 @@ namespace Avalonia.Controls.UnitTests target.Width = 410; target.LayoutManager.ExecuteLayoutPass(); - var windowImpl = Mock.Get(ValidatingWindowImpl.Unwrap(target.PlatformImpl)); + var windowImpl = Mock.Get(target.PlatformImpl); windowImpl.Verify(x => x.Resize(new Size(410, 800), PlatformResizeReason.Application)); Assert.Equal(410, target.Width); } diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index eed767e771..087d42370e 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -496,7 +496,7 @@ namespace Avalonia.LeakTests AttachShowAndDetachContextMenu(window); - Mock.Get(ValidatingWindowImpl.Unwrap(window.PlatformImpl)).Invocations.Clear(); + Mock.Get(window.PlatformImpl).Invocations.Clear(); dotMemory.Check(memory => Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); dotMemory.Check(memory => @@ -541,7 +541,7 @@ namespace Avalonia.LeakTests BuildAndShowContextMenu(window); BuildAndShowContextMenu(window); - Mock.Get(ValidatingWindowImpl.Unwrap(window.PlatformImpl)).Invocations.Clear(); + Mock.Get(window.PlatformImpl).Invocations.Clear(); dotMemory.Check(memory => Assert.Equal(initialMenuCount, memory.GetObjects(where => where.Type.Is()).ObjectsCount)); dotMemory.Check(memory =>