Browse Source

Merge branch 'master' into net6-ios

pull/7565/head
Max Katz 4 years ago
committed by GitHub
parent
commit
a44340e02a
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. 2
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  14. 1
      src/Avalonia.Controls/Properties/AssemblyInfo.cs
  15. 69
      src/Avalonia.Controls/TextBlock.cs
  16. 2
      src/Avalonia.Controls/TopLevel.cs
  17. 344
      src/Avalonia.Controls/ValidatingToplevel.cs
  18. 11
      src/Avalonia.Controls/Window.cs
  19. 9
      src/Avalonia.Controls/WindowBase.cs
  20. 3
      src/Avalonia.Visuals/Avalonia.Visuals.csproj
  21. 2
      src/Avalonia.Visuals/Media/TextFormatting/FormattedTextSource.cs
  22. 20
      src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs
  23. 6
      src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs
  24. 8
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs
  25. 44
      tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
  26. 2
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  27. 4
      tests/Avalonia.LeakTests/ControlTests.cs
  28. 22
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/BasicTests.cs
  29. 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);
}
}
}

2
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.
/// </param>
public PopupRoot(TopLevel parent, IPopupImpl impl, IAvaloniaDependencyResolver? dependencyResolver)
: base(ValidatingPopupImpl.Wrap(impl), dependencyResolver)
: base(impl, dependencyResolver)
{
_parent = parent;
}

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();
}
}
}

2
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;

344
src/Avalonia.Controls/ValidatingToplevel.cs

@ -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<object> Surfaces => Inner.Surfaces;
public Action<RawInputEventArgs>? Input
{
get => Inner.Input;
set => Inner.Input = value;
}
public Action<Rect>? Paint
{
get => Inner.Paint;
set => Inner.Paint = value;
}
public Action<Size, PlatformResizeReason>? Resized
{
get => Inner.Resized;
set => Inner.Resized = value;
}
public Action<double>? ScalingChanged
{
get => Inner.ScalingChanged;
set => Inner.ScalingChanged = value;
}
public Action<WindowTransparencyLevel>? 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<PixelPoint>? 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<WindowState> 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<bool> Closing
{
get => Inner.Closing;
set => Inner.Closing = value;
}
public bool IsClientAreaExtendedToDecorations => Inner.IsClientAreaExtendedToDecorations;
public Action<bool> 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);
}

11
src/Avalonia.Controls/Window.cs

@ -237,14 +237,13 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="impl">The window implementation.</param>
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);

9
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;
}
/// <summary>

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);
}
}
}
}

2
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);
}

4
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<ContextMenu>()).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<ContextMenu>()).ObjectsCount));
dotMemory.Check(memory =>

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