Browse Source

Merge branch 'master' into colorpicker2

pull/8215/head
robloo 4 years ago
committed by GitHub
parent
commit
b481bea243
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 48
      azure-pipelines-integrationtests.yml
  2. 4
      samples/ControlCatalog/Pages/TextBlockPage.xaml
  3. 53
      src/Avalonia.Base/Controls/Classes.cs
  4. 14
      src/Avalonia.Base/Controls/IClassesChangedListener.cs
  5. 2
      src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
  6. 26
      src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs
  7. 3
      src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
  8. 17
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  9. 72
      src/Avalonia.Controls/Documents/InlineCollection.cs
  10. 70
      src/Avalonia.Controls/Documents/Span.cs
  11. 18
      src/Avalonia.Controls/Documents/TextElement.cs
  12. 8
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  13. 4
      src/Avalonia.Controls/Primitives/AccessText.cs
  14. 1
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  15. 574
      src/Avalonia.Controls/RichTextBlock.cs
  16. 32
      src/Avalonia.Controls/SplitView.cs
  17. 157
      src/Avalonia.Controls/TextBlock.cs
  18. 2
      src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml
  19. 2
      src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml
  20. 4
      src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml
  21. 6
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  22. 23
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs
  23. 5
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs
  24. 16
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs
  25. 2
      src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs
  26. 15
      src/Windows/Avalonia.Win32/WindowImpl.cs
  27. 5
      tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs
  28. 2
      tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs
  29. 85
      tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs
  30. 52
      tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs
  31. 48
      tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
  32. 1
      tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh
  33. 2
      tests/Avalonia.LeakTests/ControlTests.cs
  34. 11
      tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
  35. 225
      tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

48
azure-pipelines-integrationtests.yml

@ -0,0 +1,48 @@
# Starter pipeline
# Start with a minimal pipeline that you can customize to build and deploy your code.
# Add steps that build, run tests, deploy, and more:
# https://aka.ms/yaml
trigger:
- master
jobs:
- job: Mac
pool:
name: 'AvaloniaMacPool'
steps:
- script: ./tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh
displayName: 'run integration tests'
- job: Windows
pool:
vmImage: 'windows-2022'
steps:
- task: UseDotNet@2
displayName: 'Use .NET Core SDK 6.0.202'
inputs:
version: 6.0.202
- task: Windows Application Driver@0
inputs:
OperationType: 'Start'
AgentResolution: '4K'
displayName: 'Start WinAppDriver'
- task: DotNetCoreCLI@2
inputs:
command: 'build'
projects: 'samples/IntegrationTestApp/IntegrationTestApp.csproj'
- task: DotNetCoreCLI@2
inputs:
command: 'test'
projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj'
- task: Windows Application Driver@0
inputs:
OperationType: 'Stop'
displayName: 'Stop WinAppDriver'

4
samples/ControlCatalog/Pages/TextBlockPage.xaml

@ -118,7 +118,7 @@
</StackPanel> </StackPanel>
</Border> </Border>
<Border> <Border>
<TextBlock Margin="10" TextWrapping="Wrap"> <RichTextBlock Margin="10" TextWrapping="Wrap">
This <Span FontWeight="Bold">is</Span> a This <Span FontWeight="Bold">is</Span> a
<Span Background="Silver" Foreground="Maroon">TextBlock</Span> <Span Background="Silver" Foreground="Maroon">TextBlock</Span>
with <Span TextDecorations="Underline">several</Span> with <Span TextDecorations="Underline">several</Span>
@ -126,7 +126,7 @@
<Span Foreground="Blue"> <Span Foreground="Blue">
using a <Bold>variety</Bold> of <Italic>styles</Italic> using a <Bold>variety</Bold> of <Italic>styles</Italic>
</Span>. </Span>.
</TextBlock> </RichTextBlock>
</Border> </Border>
</WrapPanel> </WrapPanel>
</StackPanel> </StackPanel>

53
src/Avalonia.Base/Controls/Classes.cs

@ -14,6 +14,8 @@ namespace Avalonia.Controls
/// </remarks> /// </remarks>
public class Classes : AvaloniaList<string>, IPseudoClasses public class Classes : AvaloniaList<string>, IPseudoClasses
{ {
private List<IClassesChangedListener>? _listeners;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="Classes"/> class. /// Initializes a new instance of the <see cref="Classes"/> class.
/// </summary> /// </summary>
@ -39,6 +41,11 @@ namespace Avalonia.Controls
{ {
} }
/// <summary>
/// Gets the number of listeners subscribed to this collection for unit testing purposes.
/// </summary>
internal int ListenerCount => _listeners?.Count ?? 0;
/// <summary> /// <summary>
/// Parses a classes string. /// Parses a classes string.
/// </summary> /// </summary>
@ -62,6 +69,7 @@ namespace Avalonia.Controls
if (!Contains(name)) if (!Contains(name))
{ {
base.Add(name); base.Add(name);
NotifyChanged();
} }
} }
@ -89,6 +97,7 @@ namespace Avalonia.Controls
} }
base.AddRange(c); base.AddRange(c);
NotifyChanged();
} }
/// <summary> /// <summary>
@ -103,6 +112,8 @@ namespace Avalonia.Controls
RemoveAt(i); RemoveAt(i);
} }
} }
NotifyChanged();
} }
/// <summary> /// <summary>
@ -122,6 +133,7 @@ namespace Avalonia.Controls
if (!Contains(name)) if (!Contains(name))
{ {
base.Insert(index, name); base.Insert(index, name);
NotifyChanged();
} }
} }
@ -154,6 +166,7 @@ namespace Avalonia.Controls
if (toInsert != null) if (toInsert != null)
{ {
base.InsertRange(index, toInsert); base.InsertRange(index, toInsert);
NotifyChanged();
} }
} }
@ -169,7 +182,14 @@ namespace Avalonia.Controls
public override bool Remove(string name) public override bool Remove(string name)
{ {
ThrowIfPseudoclass(name, "removed"); ThrowIfPseudoclass(name, "removed");
return base.Remove(name);
if (base.Remove(name))
{
NotifyChanged();
return true;
}
return false;
} }
/// <summary> /// <summary>
@ -197,6 +217,7 @@ namespace Avalonia.Controls
if (toRemove != null) if (toRemove != null)
{ {
base.RemoveAll(toRemove); base.RemoveAll(toRemove);
NotifyChanged();
} }
} }
@ -214,6 +235,7 @@ namespace Avalonia.Controls
var name = this[index]; var name = this[index];
ThrowIfPseudoclass(name, "removed"); ThrowIfPseudoclass(name, "removed");
base.RemoveAt(index); base.RemoveAt(index);
NotifyChanged();
} }
/// <summary> /// <summary>
@ -224,6 +246,7 @@ namespace Avalonia.Controls
public override void RemoveRange(int index, int count) public override void RemoveRange(int index, int count)
{ {
base.RemoveRange(index, count); base.RemoveRange(index, count);
NotifyChanged();
} }
/// <summary> /// <summary>
@ -255,6 +278,7 @@ namespace Avalonia.Controls
} }
base.AddRange(source); base.AddRange(source);
NotifyChanged();
} }
/// <inheritdoc/> /// <inheritdoc/>
@ -263,13 +287,38 @@ namespace Avalonia.Controls
if (!Contains(name)) if (!Contains(name))
{ {
base.Add(name); base.Add(name);
NotifyChanged();
} }
} }
/// <inheritdoc/> /// <inheritdoc/>
bool IPseudoClasses.Remove(string name) bool IPseudoClasses.Remove(string name)
{ {
return base.Remove(name); if (base.Remove(name))
{
NotifyChanged();
return true;
}
return false;
}
internal void AddListener(IClassesChangedListener listener)
{
(_listeners ??= new()).Add(listener);
}
internal void RemoveListener(IClassesChangedListener listener)
{
_listeners?.Remove(listener);
}
private void NotifyChanged()
{
if (_listeners is null)
return;
foreach (var listener in _listeners)
listener.Changed();
} }
private void ThrowIfPseudoclass(string name, string operation) private void ThrowIfPseudoclass(string name, string operation)

14
src/Avalonia.Base/Controls/IClassesChangedListener.cs

@ -0,0 +1,14 @@
namespace Avalonia.Controls
{
/// <summary>
/// Internal interface for listening to changes in <see cref="Classes"/> in a more
/// performant manner than subscribing to CollectionChanged.
/// </summary>
internal interface IClassesChangedListener
{
/// <summary>
/// Notifies the listener that the <see cref="Classes"/> collection has changed.
/// </summary>
void Changed();
}
}

2
src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs

@ -55,7 +55,7 @@ namespace Avalonia.Data.Core.Plugins
private PropertyInfo? GetFirstPropertyWithName(object instance, string propertyName) private PropertyInfo? GetFirstPropertyWithName(object instance, string propertyName)
{ {
if (instance is IReflectableType reflectableType) if (instance is IReflectableType reflectableType && instance is not Type)
return reflectableType.GetTypeInfo().GetProperty(propertyName, PropertyBindingFlags); return reflectableType.GetTypeInfo().GetProperty(propertyName, PropertyBindingFlags);
var type = instance.GetType(); var type = instance.GetType();

26
src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs

@ -1,6 +1,7 @@
using System.Collections.Generic; using System.Collections.Generic;
using System.Collections.Specialized; using System.Collections.Specialized;
using Avalonia.Collections; using Avalonia.Collections;
using Avalonia.Controls;
#nullable enable #nullable enable
@ -10,21 +11,17 @@ namespace Avalonia.Styling.Activators
/// An <see cref="IStyleActivator"/> which is active when a set of classes match those on a /// An <see cref="IStyleActivator"/> which is active when a set of classes match those on a
/// control. /// control.
/// </summary> /// </summary>
internal sealed class StyleClassActivator : StyleActivatorBase internal sealed class StyleClassActivator : StyleActivatorBase, IClassesChangedListener
{ {
private readonly IList<string> _match; private readonly IList<string> _match;
private readonly IAvaloniaReadOnlyList<string> _classes; private readonly Classes _classes;
private NotifyCollectionChangedEventHandler? _classesChangedHandler;
public StyleClassActivator(IAvaloniaReadOnlyList<string> classes, IList<string> match) public StyleClassActivator(Classes classes, IList<string> match)
{ {
_classes = classes; _classes = classes;
_match = match; _match = match;
} }
private NotifyCollectionChangedEventHandler ClassesChangedHandler =>
_classesChangedHandler ??= ClassesChanged;
public static bool AreClassesMatching(IReadOnlyList<string> classes, IList<string> toMatch) public static bool AreClassesMatching(IReadOnlyList<string> classes, IList<string> toMatch)
{ {
int remainingMatches = toMatch.Count; int remainingMatches = toMatch.Count;
@ -55,23 +52,20 @@ namespace Avalonia.Styling.Activators
return remainingMatches == 0; return remainingMatches == 0;
} }
protected override void Initialize() void IClassesChangedListener.Changed()
{ {
PublishNext(IsMatching()); PublishNext(IsMatching());
_classes.CollectionChanged += ClassesChangedHandler;
} }
protected override void Deinitialize() protected override void Initialize()
{ {
_classes.CollectionChanged -= ClassesChangedHandler; PublishNext(IsMatching());
_classes.AddListener(this);
} }
private void ClassesChanged(object? sender, NotifyCollectionChangedEventArgs e) protected override void Deinitialize()
{ {
if (e.Action != NotifyCollectionChangedAction.Move) _classes.RemoveListener(this);
{
PublishNext(IsMatching());
}
} }
private bool IsMatching() => AreClassesMatching(_classes, _match); private bool IsMatching() => AreClassesMatching(_classes, _match);

3
src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs

@ -1,6 +1,7 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Text; using System.Text;
using Avalonia.Controls;
using Avalonia.Styling.Activators; using Avalonia.Styling.Activators;
#nullable enable #nullable enable
@ -125,7 +126,7 @@ namespace Avalonia.Styling
{ {
if (subscribe) if (subscribe)
{ {
var observable = new StyleClassActivator(control.Classes, _classes.Value); var observable = new StyleClassActivator((Classes)control.Classes, _classes.Value);
return new SelectorMatch(observable); return new SelectorMatch(observable);
} }

17
src/Avalonia.Controls.DataGrid/DataGrid.cs

@ -5990,15 +5990,14 @@ namespace Avalonia.Controls
/// <returns>The formatted string.</returns> /// <returns>The formatted string.</returns>
private string FormatClipboardContent(DataGridRowClipboardEventArgs e) private string FormatClipboardContent(DataGridRowClipboardEventArgs e)
{ {
StringBuilder text = new StringBuilder(); var text = new StringBuilder();
for (int cellIndex = 0; cellIndex < e.ClipboardRowContent.Count; cellIndex++) var clipboardRowContent = e.ClipboardRowContent;
{ var numberOfItem = clipboardRowContent.Count;
DataGridClipboardCellContent cellContent = e.ClipboardRowContent[cellIndex]; for (int cellIndex = 0; cellIndex < numberOfItem; cellIndex++)
if (cellContent != null) {
{ var cellContent = clipboardRowContent[cellIndex];
text.Append(cellContent.Content); text.Append(cellContent.Content);
} if (cellIndex < numberOfItem - 1)
if (cellIndex < e.ClipboardRowContent.Count - 1)
{ {
text.Append('\t'); text.Append('\t');
} }

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

@ -12,39 +12,55 @@ namespace Avalonia.Controls.Documents
[WhitespaceSignificantCollection] [WhitespaceSignificantCollection]
public class InlineCollection : AvaloniaList<Inline> public class InlineCollection : AvaloniaList<Inline>
{ {
private readonly IInlineHost? _host; private ILogical? _parent;
private IInlineHost? _inlineHost;
private string? _text = string.Empty; private string? _text = string.Empty;
/// <summary> /// <summary>
/// Initializes a new instance of the <see cref="InlineCollection"/> class. /// Initializes a new instance of the <see cref="InlineCollection"/> class.
/// </summary> /// </summary>
public InlineCollection(ILogical parent) : this(parent, null) { } public InlineCollection()
/// <summary>
/// Initializes a new instance of the <see cref="InlineCollection"/> class.
/// </summary>
internal InlineCollection(ILogical parent, IInlineHost? host = null) : base(0)
{ {
_host = host;
ResetBehavior = ResetBehavior.Remove; ResetBehavior = ResetBehavior.Remove;
this.ForEachItem( this.ForEachItem(
x => x =>
{ {
((ISetLogicalParent)x).SetParent(parent); ((ISetLogicalParent)x).SetParent(Parent);
x.InlineHost = host; x.InlineHost = InlineHost;
host?.Invalidate(); Invalidate();
}, },
x => x =>
{ {
((ISetLogicalParent)x).SetParent(null); ((ISetLogicalParent)x).SetParent(null);
x.InlineHost = host; x.InlineHost = InlineHost;
host?.Invalidate(); Invalidate();
}, },
() => throw new NotSupportedException()); () => throw new NotSupportedException());
} }
internal ILogical? Parent
{
get => _parent;
set
{
_parent = value;
OnParentChanged(value);
}
}
internal IInlineHost? InlineHost
{
get => _inlineHost;
set
{
_inlineHost = value;
OnInlineHostChanged(value);
}
}
public bool HasComplexContent => Count > 0; public bool HasComplexContent => Count > 0;
/// <summary> /// <summary>
@ -61,10 +77,10 @@ namespace Avalonia.Controls.Documents
{ {
return _text; return _text;
} }
var builder = new StringBuilder(); var builder = new StringBuilder();
foreach(var inline in this) foreach (var inline in this)
{ {
inline.AppendText(builder); inline.AppendText(builder);
} }
@ -100,7 +116,7 @@ namespace Avalonia.Controls.Documents
} }
else else
{ {
_text += text; _text = text;
} }
} }
@ -120,7 +136,7 @@ namespace Avalonia.Controls.Documents
base.Add(new Run(_text)); base.Add(new Run(_text));
} }
_text = string.Empty; _text = null;
} }
base.Add(item); base.Add(item);
@ -136,14 +152,28 @@ namespace Avalonia.Controls.Documents
/// </summary> /// </summary>
protected void Invalidate() protected void Invalidate()
{ {
if(_host != null) if(InlineHost != null)
{ {
_host.Invalidate(); InlineHost.Invalidate();
} }
Invalidated?.Invoke(this, EventArgs.Empty); Invalidated?.Invoke(this, EventArgs.Empty);
} }
private void Invalidate(object? sender, EventArgs e) => Invalidate(); private void OnParentChanged(ILogical? parent)
{
foreach(var child in this)
{
((ISetLogicalParent)child).SetParent(parent);
}
}
private void OnInlineHostChanged(IInlineHost? inlineHost)
{
foreach (var child in this)
{
child.InlineHost = inlineHost;
}
}
} }
} }

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

@ -14,25 +14,27 @@ namespace Avalonia.Controls.Documents
/// <summary> /// <summary>
/// Defines the <see cref="Inlines"/> property. /// Defines the <see cref="Inlines"/> property.
/// </summary> /// </summary>
public static readonly DirectProperty<Span, InlineCollection> InlinesProperty = public static readonly StyledProperty<InlineCollection> InlinesProperty =
AvaloniaProperty.RegisterDirect<Span, InlineCollection>( AvaloniaProperty.Register<Span, InlineCollection>(
nameof(Inlines), nameof(Inlines));
o => o.Inlines);
/// <summary>
/// Initializes a new instance of a Span element.
/// </summary>
public Span() public Span()
{ {
Inlines = new InlineCollection(this); Inlines = new InlineCollection
Inlines.Invalidated += (s, e) => InlineHost?.Invalidate(); {
Parent = this
};
} }
/// <summary> /// <summary>
/// Gets or sets the inlines. /// Gets or sets the inlines.
/// </summary> /// </summary>
[Content] [Content]
public InlineCollection Inlines { get; } public InlineCollection Inlines
{
get => GetValue(InlinesProperty);
set => SetValue(InlinesProperty, value);
}
internal override void BuildTextRun(IList<TextRun> textRuns) internal override void BuildTextRun(IList<TextRun> textRuns)
{ {
@ -52,7 +54,7 @@ namespace Avalonia.Controls.Documents
var textCharacters = new TextCharacters(text.AsMemory(), textRunProperties); var textCharacters = new TextCharacters(text.AsMemory(), textRunProperties);
textRuns.Add(textCharacters); textRuns.Add(textCharacters);
} }
} }
} }
@ -65,10 +67,52 @@ namespace Avalonia.Controls.Documents
inline.AppendText(stringBuilder); inline.AppendText(stringBuilder);
} }
} }
else
{
if (Inlines.Text is string text)
{
stringBuilder.Append(text);
}
}
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof(InlinesProperty):
OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
InlineHost?.Invalidate();
break;
}
}
internal override void OnInlineHostChanged(IInlineHost? oldValue, IInlineHost? newValue)
{
base.OnInlineHostChanged(oldValue, newValue);
if(Inlines is not null)
{
Inlines.InlineHost = newValue;
}
}
private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue)
{
if (oldValue is not null)
{
oldValue.Parent = null;
oldValue.InlineHost = null;
oldValue.Invalidated -= (s, e) => InlineHost?.Invalidate();
}
if (Inlines.Text is string text) if (newValue is not null)
{ {
stringBuilder.Append(text); newValue.Parent = this;
newValue.InlineHost = InlineHost;
newValue.Invalidated += (s, e) => InlineHost?.Invalidate();
} }
} }
} }

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

@ -67,6 +67,8 @@ namespace Avalonia.Controls.Documents
Brushes.Black, Brushes.Black,
inherits: true); inherits: true);
private IInlineHost? _inlineHost;
/// <summary> /// <summary>
/// Gets or sets a brush used to paint the control's background. /// Gets or sets a brush used to paint the control's background.
/// </summary> /// </summary>
@ -250,7 +252,21 @@ namespace Avalonia.Controls.Documents
control.SetValue(ForegroundProperty, value); control.SetValue(ForegroundProperty, value);
} }
internal IInlineHost? InlineHost { get; set; } internal IInlineHost? InlineHost
{
get => _inlineHost;
set
{
var oldValue = _inlineHost;
_inlineHost = value;
OnInlineHostChanged(oldValue, value);
}
}
internal virtual void OnInlineHostChanged(IInlineHost? oldValue, IInlineHost? newValue)
{
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{ {

8
src/Avalonia.Controls/Presenters/TextPresenter.cs

@ -543,9 +543,11 @@ namespace Avalonia.Controls.Presenters
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
if (finalSize.Width < TextLayout.Bounds.Width) var textWidth = Math.Ceiling(TextLayout.Bounds.Width);
if (finalSize.Width < textWidth)
{ {
finalSize = finalSize.WithWidth(TextLayout.Bounds.Width); finalSize = finalSize.WithWidth(textWidth);
} }
if (MathUtilities.AreClose(_constraint.Width, finalSize.Width)) if (MathUtilities.AreClose(_constraint.Width, finalSize.Width))
@ -553,7 +555,7 @@ namespace Avalonia.Controls.Presenters
return finalSize; return finalSize;
} }
_constraint = new Size(finalSize.Width, double.PositiveInfinity); _constraint = new Size(Math.Ceiling(finalSize.Width), double.PositiveInfinity);
_textLayout = null; _textLayout = null;

4
src/Avalonia.Controls/Primitives/AccessText.cs

@ -79,9 +79,9 @@ namespace Avalonia.Controls.Primitives
} }
/// <inheritdoc/> /// <inheritdoc/>
protected override TextLayout CreateTextLayout(Size constraint, string? text) protected override TextLayout CreateTextLayout(string? text)
{ {
return base.CreateTextLayout(constraint, RemoveAccessKeyMarker(text)); return base.CreateTextLayout(RemoveAccessKeyMarker(text));
} }
/// <inheritdoc/> /// <inheritdoc/>

1
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -421,6 +421,7 @@ namespace Avalonia.Controls
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{ {
base.OnAttachedToVisualTree(e);
InvalidateMeasure(); InvalidateMeasure();
_viewportManager.ResetScrollers(); _viewportManager.ResetScrollers();
} }

574
src/Avalonia.Controls/RichTextBlock.cs

@ -0,0 +1,574 @@
using System;
using System.Collections.Generic;
using System.Linq;
using Avalonia.Controls.Documents;
using Avalonia.Controls.Utils;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls
{
/// <summary>
/// A control that displays a block of formatted text.
/// </summary>
public class RichTextBlock : TextBlock, IInlineHost
{
public static readonly StyledProperty<bool> IsTextSelectionEnabledProperty =
AvaloniaProperty.Register<RichTextBlock, bool>(nameof(IsTextSelectionEnabled), false);
public static readonly DirectProperty<RichTextBlock, int> SelectionStartProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, int>(
nameof(SelectionStart),
o => o.SelectionStart,
(o, v) => o.SelectionStart = v);
public static readonly DirectProperty<RichTextBlock, int> SelectionEndProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, int>(
nameof(SelectionEnd),
o => o.SelectionEnd,
(o, v) => o.SelectionEnd = v);
public static readonly DirectProperty<RichTextBlock, string> SelectedTextProperty =
AvaloniaProperty.RegisterDirect<RichTextBlock, string>(
nameof(SelectedText),
o => o.SelectedText);
public static readonly StyledProperty<IBrush?> SelectionBrushProperty =
AvaloniaProperty.Register<RichTextBlock, IBrush?>(nameof(SelectionBrush), Brushes.Blue);
public static readonly StyledProperty<IBrush?> SelectionForegroundBrushProperty =
AvaloniaProperty.Register<RichTextBlock, IBrush?>(nameof(SelectionForegroundBrush));
/// <summary>
/// Defines the <see cref="Inlines"/> property.
/// </summary>
public static readonly StyledProperty<InlineCollection> InlinesProperty =
AvaloniaProperty.Register<RichTextBlock, InlineCollection>(
nameof(Inlines));
public static readonly DirectProperty<TextBox, bool> CanCopyProperty =
AvaloniaProperty.RegisterDirect<TextBox, bool>(
nameof(CanCopy),
o => o.CanCopy);
public static readonly RoutedEvent<RoutedEventArgs> CopyingToClipboardEvent =
RoutedEvent.Register<RichTextBlock, RoutedEventArgs>(
nameof(CopyingToClipboard), RoutingStrategies.Bubble);
private bool _canCopy;
private int _selectionStart;
private int _selectionEnd;
static RichTextBlock()
{
FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true);
AffectsRender<RichTextBlock>(SelectionStartProperty, SelectionEndProperty, SelectionForegroundBrushProperty, SelectionBrushProperty);
}
public RichTextBlock()
{
Inlines = new InlineCollection
{
Parent = this,
InlineHost = this
};
}
/// <summary>
/// Gets or sets the brush that highlights selected text.
/// </summary>
public IBrush? SelectionBrush
{
get => GetValue(SelectionBrushProperty);
set => SetValue(SelectionBrushProperty, value);
}
/// <summary>
/// Gets or sets a value that defines the brush used for selected text.
/// </summary>
public IBrush? SelectionForegroundBrush
{
get => GetValue(SelectionForegroundBrushProperty);
set => SetValue(SelectionForegroundBrushProperty, value);
}
/// <summary>
/// Gets or sets a character index for the beginning of the current selection.
/// </summary>
public int SelectionStart
{
get => _selectionStart;
set
{
if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value))
{
RaisePropertyChanged(SelectedTextProperty, "", "");
}
}
}
/// <summary>
/// Gets or sets a character index for the end of the current selection.
/// </summary>
public int SelectionEnd
{
get => _selectionEnd;
set
{
if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value))
{
RaisePropertyChanged(SelectedTextProperty, "", "");
}
}
}
/// <summary>
/// Gets the content of the current selection.
/// </summary>
public string SelectedText
{
get => GetSelection();
}
/// <summary>
/// Gets or sets a value that indicates whether text selection is enabled, either through user action or calling selection-related API.
/// </summary>
public bool IsTextSelectionEnabled
{
get => GetValue(IsTextSelectionEnabledProperty);
set => SetValue(IsTextSelectionEnabledProperty, value);
}
/// <summary>
/// Gets or sets the inlines.
/// </summary>
[Content]
public InlineCollection Inlines
{
get => GetValue(InlinesProperty);
set => SetValue(InlinesProperty, value);
}
/// <summary>
/// Property for determining if the Copy command can be executed.
/// </summary>
public bool CanCopy
{
get => _canCopy;
private set => SetAndRaise(CanCopyProperty, ref _canCopy, value);
}
public event EventHandler<RoutedEventArgs>? CopyingToClipboard
{
add => AddHandler(CopyingToClipboardEvent, value);
remove => RemoveHandler(CopyingToClipboardEvent, value);
}
/// <summary>
/// Copies the current selection to the Clipboard.
/// </summary>
public async void Copy()
{
if (_canCopy || !IsTextSelectionEnabled)
{
return;
}
var text = GetSelection();
if (string.IsNullOrEmpty(text))
{
return;
}
var eventArgs = new RoutedEventArgs(CopyingToClipboardEvent);
RaiseEvent(eventArgs);
if (!eventArgs.Handled)
{
await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard)))
.SetTextAsync(text);
}
}
public override void Render(DrawingContext context)
{
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var selectionBrush = SelectionBrush;
var selectionEnabled = IsTextSelectionEnabled;
if (selectionEnabled && selectionStart != selectionEnd && selectionBrush != null)
{
var start = Math.Min(selectionStart, selectionEnd);
var length = Math.Max(selectionStart, selectionEnd) - start;
var rects = TextLayout.HitTestTextRange(start, length);
foreach (var rect in rects)
{
context.FillRectangle(selectionBrush, PixelRect.FromRect(rect, 1).ToRect(1));
}
}
base.Render(context);
}
/// <summary>
/// Select all text in the TextBox
/// </summary>
public void SelectAll()
{
if (!IsTextSelectionEnabled)
{
return;
}
var text = Text;
SelectionStart = 0;
SelectionEnd = text?.Length ?? 0;
}
/// <summary>
/// Clears the current selection/>
/// </summary>
public void ClearSelection()
{
if (!IsTextSelectionEnabled)
{
return;
}
SelectionEnd = SelectionStart;
}
protected override string? GetText()
{
return _text ?? Inlines.Text;
}
protected override void SetText(string? text)
{
var oldValue = _text ?? Inlines?.Text;
if (Inlines is not null && Inlines.HasComplexContent)
{
Inlines.Text = text;
_text = null;
}
else
{
_text = text;
}
RaisePropertyChanged(TextProperty, oldValue, text);
}
/// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary>
/// <returns>A <see cref="TextLayout"/> object.</returns>
protected override TextLayout CreateTextLayout(string? text)
{
var defaultProperties = new GenericTextRunProperties(
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
FontSize,
TextDecorations,
Foreground);
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
defaultProperties, TextWrapping, LineHeight, 0);
ITextSource textSource;
var inlines = Inlines;
if (inlines is not null && inlines.HasComplexContent)
{
var textRuns = new List<TextRun>();
foreach (var inline in inlines)
{
inline.BuildTextRun(textRuns);
}
textSource = new InlinesTextSource(textRuns);
}
else
{
textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
}
return new TextLayout(
textSource,
paragraphProperties,
TextTrimming,
_constraint.Width,
_constraint.Height,
maxLines: MaxLines,
lineHeight: LineHeight);
}
protected override void OnLostFocus(RoutedEventArgs e)
{
base.OnLostFocus(e);
ClearSelection();
}
protected override void OnKeyDown(KeyEventArgs e)
{
var handled = false;
var modifiers = e.KeyModifiers;
var keymap = AvaloniaLocator.Current.GetRequiredService<PlatformHotkeyConfiguration>();
bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
if (Match(keymap.Copy))
{
Copy();
handled = true;
}
e.Handled = handled;
}
protected override void OnPointerPressed(PointerPressedEventArgs e)
{
if (!IsTextSelectionEnabled)
{
return;
}
var text = Text;
var clickInfo = e.GetCurrentPoint(this);
if (text != null && clickInfo.Properties.IsLeftButtonPressed)
{
var point = e.GetPosition(this);
var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift);
var oldIndex = SelectionStart;
var hit = TextLayout.HitTestPoint(point);
var index = hit.TextPosition;
SelectionStart = SelectionEnd = index;
#pragma warning disable CS0618 // Type or member is obsolete
switch (e.ClickCount)
#pragma warning restore CS0618 // Type or member is obsolete
{
case 1:
if (clickToSelect)
{
SelectionStart = Math.Min(oldIndex, index);
SelectionEnd = Math.Max(oldIndex, index);
}
else
{
SelectionStart = SelectionEnd = index;
}
break;
case 2:
if (!StringUtils.IsStartOfWord(text, index))
{
SelectionStart = StringUtils.PreviousWord(text, index);
}
SelectionEnd = StringUtils.NextWord(text, index);
break;
case 3:
SelectAll();
break;
}
}
e.Pointer.Capture(this);
e.Handled = true;
}
protected override void OnPointerMoved(PointerEventArgs e)
{
if (!IsTextSelectionEnabled)
{
return;
}
// selection should not change during pointer move if the user right clicks
if (e.Pointer.Captured == this && e.GetCurrentPoint(this).Properties.IsLeftButtonPressed)
{
var point = e.GetPosition(this);
point = new Point(
MathUtilities.Clamp(point.X, 0, Math.Max(Bounds.Width - 1, 0)),
MathUtilities.Clamp(point.Y, 0, Math.Max(Bounds.Height - 1, 0)));
var hit = TextLayout.HitTestPoint(point);
SelectionEnd = hit.TextPosition;
}
}
protected override void OnPointerReleased(PointerReleasedEventArgs e)
{
if (!IsTextSelectionEnabled)
{
return;
}
if (e.Pointer.Captured != this)
{
return;
}
if (e.InitialPressMouseButton == MouseButton.Right)
{
var point = e.GetPosition(this);
var hit = TextLayout.HitTestPoint(point);
var caretIndex = hit.TextPosition;
// see if mouse clicked inside current selection
// if it did not, we change the selection to where the user clicked
var firstSelection = Math.Min(SelectionStart, SelectionEnd);
var lastSelection = Math.Max(SelectionStart, SelectionEnd);
var didClickInSelection = SelectionStart != SelectionEnd &&
caretIndex >= firstSelection && caretIndex <= lastSelection;
if (!didClickInSelection)
{
SelectionStart = SelectionEnd = caretIndex;
}
}
e.Pointer.Capture(null);
}
protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change)
{
base.OnPropertyChanged(change);
switch (change.Property.Name)
{
case nameof(InlinesProperty):
{
OnInlinesChanged(change.OldValue as InlineCollection, change.NewValue as InlineCollection);
InvalidateTextLayout();
break;
}
case nameof(TextProperty):
{
InvalidateTextLayout();
break;
}
}
}
private string GetSelection()
{
if (!IsTextSelectionEnabled)
{
return "";
}
var text = Inlines.Text ?? Text;
if (string.IsNullOrEmpty(text))
{
return "";
}
var selectionStart = SelectionStart;
var selectionEnd = SelectionEnd;
var start = Math.Min(selectionStart, selectionEnd);
var end = Math.Max(selectionStart, selectionEnd);
if (start == end || text.Length < end)
{
return "";
}
var length = Math.Max(0, end - start);
var selectedText = text.Substring(start, length);
return selectedText;
}
private void OnInlinesChanged(InlineCollection? oldValue, InlineCollection? newValue)
{
if (oldValue is not null)
{
oldValue.Parent = null;
oldValue.InlineHost = null;
oldValue.Invalidated -= (s, e) => InvalidateTextLayout();
}
if (newValue is not null)
{
newValue.Parent = this;
newValue.InlineHost = this;
newValue.Invalidated += (s, e) => InvalidateTextLayout();
}
}
void IInlineHost.AddVisualChild(IControl child)
{
if (child.VisualParent == null)
{
VisualChildren.Add(child);
}
}
void IInlineHost.Invalidate()
{
InvalidateTextLayout();
}
private readonly struct InlinesTextSource : ITextSource
{
private readonly IReadOnlyList<TextRun> _textRuns;
public InlinesTextSource(IReadOnlyList<TextRun> textRuns)
{
_textRuns = textRuns;
}
public TextRun? GetTextRun(int textSourceIndex)
{
var currentPosition = 0;
foreach (var textRun in _textRuns)
{
if (textRun.TextSourceLength == 0)
{
continue;
}
if (currentPosition >= textSourceIndex)
{
return textRun;
}
currentPosition += textRun.TextSourceLength;
}
return null;
}
}
}
}

32
src/Avalonia.Controls/SplitView.cs

@ -431,23 +431,45 @@ namespace Avalonia.Controls
} }
} }
private string GetPseudoClass(SplitViewDisplayMode mode)
{
return mode switch
{
SplitViewDisplayMode.Inline => "inline",
SplitViewDisplayMode.CompactInline => "compactinline",
SplitViewDisplayMode.Overlay => "overlay",
SplitViewDisplayMode.CompactOverlay => "compactoverlay",
_ => throw new ArgumentOutOfRangeException(nameof(mode), mode, null)
};
}
private string GetPseudoClass(SplitViewPanePlacement placement)
{
return placement switch
{
SplitViewPanePlacement.Left => "left",
SplitViewPanePlacement.Right => "right",
_ => throw new ArgumentOutOfRangeException(nameof(placement), placement, null)
};
}
private void OnPanePlacementChanged(AvaloniaPropertyChangedEventArgs e) private void OnPanePlacementChanged(AvaloniaPropertyChangedEventArgs e)
{ {
var oldState = e.OldValue!.ToString()!.ToLower(); var oldState = GetPseudoClass(e.GetOldValue<SplitViewPanePlacement>());
var newState = e.NewValue!.ToString()!.ToLower(); var newState = GetPseudoClass(e.GetNewValue<SplitViewPanePlacement>());
PseudoClasses.Remove($":{oldState}"); PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}"); PseudoClasses.Add($":{newState}");
} }
private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs e) private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs e)
{ {
var oldState = e.OldValue!.ToString()!.ToLower(); var oldState = GetPseudoClass(e.GetOldValue<SplitViewDisplayMode>());
var newState = e.NewValue!.ToString()!.ToLower(); var newState = GetPseudoClass(e.GetNewValue<SplitViewDisplayMode>());
PseudoClasses.Remove($":{oldState}"); PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}"); PseudoClasses.Add($":{newState}");
var (closedPaneWidth, paneColumnGridLength) = (SplitViewDisplayMode)e.NewValue switch var (closedPaneWidth, paneColumnGridLength) = e.GetNewValue<SplitViewDisplayMode>() switch
{ {
SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)), SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)),
SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)), SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)),

157
src/Avalonia.Controls/TextBlock.cs

@ -1,12 +1,9 @@
using System; using System;
using System.Collections.Generic;
using System.Text;
using Avalonia.Automation.Peers; using Avalonia.Automation.Peers;
using Avalonia.Controls.Documents; using Avalonia.Controls.Documents;
using Avalonia.Layout; using Avalonia.Layout;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting;
using Avalonia.Metadata;
using Avalonia.Utilities; using Avalonia.Utilities;
namespace Avalonia.Controls namespace Avalonia.Controls
@ -14,7 +11,7 @@ namespace Avalonia.Controls
/// <summary> /// <summary>
/// A control that displays a block of text. /// A control that displays a block of text.
/// </summary> /// </summary>
public class TextBlock : Control, IInlineHost public class TextBlock : Control
{ {
/// <summary> /// <summary>
/// Defines the <see cref="Background"/> property. /// Defines the <see cref="Background"/> property.
@ -101,14 +98,6 @@ namespace Avalonia.Controls
o => o.Text, o => o.Text,
(o, v) => o.Text = v); (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> /// <summary>
/// Defines the <see cref="TextAlignment"/> property. /// Defines the <see cref="TextAlignment"/> property.
/// </summary> /// </summary>
@ -139,8 +128,9 @@ namespace Avalonia.Controls
public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty = public static readonly StyledProperty<TextDecorationCollection?> TextDecorationsProperty =
AvaloniaProperty.Register<TextBlock, TextDecorationCollection?>(nameof(TextDecorations)); AvaloniaProperty.Register<TextBlock, TextDecorationCollection?>(nameof(TextDecorations));
private TextLayout? _textLayout; protected string? _text;
private Size _constraint; protected TextLayout? _textLayout;
protected Size _constraint;
/// <summary> /// <summary>
/// Initializes static members of the <see cref="TextBlock"/> class. /// Initializes static members of the <see cref="TextBlock"/> class.
@ -152,14 +142,6 @@ namespace Avalonia.Controls
AffectsRender<TextBlock>(BackgroundProperty, ForegroundProperty); AffectsRender<TextBlock>(BackgroundProperty, ForegroundProperty);
} }
/// <summary>
/// Initializes a new instance of the <see cref="TextBlock"/> class.
/// </summary>
public TextBlock()
{
Inlines = new InlineCollection(this, this);
}
/// <summary> /// <summary>
/// Gets the <see cref="TextLayout"/> used to render the text. /// Gets the <see cref="TextLayout"/> used to render the text.
/// </summary> /// </summary>
@ -167,7 +149,7 @@ namespace Avalonia.Controls
{ {
get get
{ {
return _textLayout ?? (_textLayout = CreateTextLayout(_constraint, Text)); return _textLayout ??= CreateTextLayout(_text);
} }
} }
@ -194,28 +176,10 @@ namespace Avalonia.Controls
/// </summary> /// </summary>
public string? Text public string? Text
{ {
get => Inlines.Text; get => GetText();
set set => SetText(value);
{
var old = Text;
if (value == old)
{
return;
}
Inlines.Text = value;
RaisePropertyChanged(TextProperty, old, value);
}
} }
/// <summary>
/// Gets the inlines.
/// </summary>
[Content]
public InlineCollection Inlines { get; }
/// <summary> /// <summary>
/// Gets or sets the font family used to draw the control's text. /// Gets or sets the font family used to draw the control's text.
/// </summary> /// </summary>
@ -509,6 +473,10 @@ namespace Avalonia.Controls
control.SetValue(MaxLinesProperty, maxLines); control.SetValue(MaxLinesProperty, maxLines);
} }
public void Add(string text)
{
_text = text;
}
/// <summary> /// <summary>
/// Renders the <see cref="TextBlock"/> to a drawing context. /// Renders the <see cref="TextBlock"/> to a drawing context.
@ -544,13 +512,21 @@ namespace Avalonia.Controls
TextLayout.Draw(context, new Point(padding.Left, top)); TextLayout.Draw(context, new Point(padding.Left, top));
} }
protected virtual string? GetText()
{
return _text;
}
protected virtual void SetText(string? text)
{
SetAndRaise(TextProperty, ref _text, text);
}
/// <summary> /// <summary>
/// Creates the <see cref="TextLayout"/> used to render the text. /// Creates the <see cref="TextLayout"/> used to render the text.
/// </summary> /// </summary>
/// <param name="constraint">The constraint of the text.</param>
/// <param name="text">The text to format.</param>
/// <returns>A <see cref="TextLayout"/> object.</returns> /// <returns>A <see cref="TextLayout"/> object.</returns>
protected virtual TextLayout CreateTextLayout(Size constraint, string? text) protected virtual TextLayout CreateTextLayout(string? text)
{ {
var defaultProperties = new GenericTextRunProperties( var defaultProperties = new GenericTextRunProperties(
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
@ -561,30 +537,12 @@ namespace Avalonia.Controls
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false, var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
defaultProperties, TextWrapping, LineHeight, 0); defaultProperties, TextWrapping, LineHeight, 0);
ITextSource textSource;
if (Inlines.HasComplexContent)
{
var textRuns = new List<TextRun>();
foreach (var inline in Inlines)
{
inline.BuildTextRun(textRuns);
}
textSource = new InlinesTextSource(textRuns);
}
else
{
textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
}
return new TextLayout( return new TextLayout(
textSource, new SimpleTextSource((text ?? "").AsMemory(), defaultProperties),
paragraphProperties, paragraphProperties,
TextTrimming, TextTrimming,
constraint.Width, _constraint.Width,
constraint.Height, _constraint.Height,
maxLines: MaxLines, maxLines: MaxLines,
lineHeight: LineHeight); lineHeight: LineHeight);
} }
@ -601,11 +559,6 @@ namespace Avalonia.Controls
protected override Size MeasureOverride(Size availableSize) protected override Size MeasureOverride(Size availableSize)
{ {
if (!Inlines.HasComplexContent && string.IsNullOrEmpty(Text))
{
return new Size();
}
var scale = LayoutHelper.GetLayoutScale(this); var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
@ -623,9 +576,11 @@ namespace Avalonia.Controls
protected override Size ArrangeOverride(Size finalSize) protected override Size ArrangeOverride(Size finalSize)
{ {
if(finalSize.Width < TextLayout.Bounds.Width) var textWidth = Math.Ceiling(TextLayout.Bounds.Width);
if(finalSize.Width < textWidth)
{ {
finalSize = finalSize.WithWidth(TextLayout.Bounds.Width); finalSize = finalSize.WithWidth(textWidth);
} }
if (MathUtilities.AreClose(_constraint.Width, finalSize.Width)) if (MathUtilities.AreClose(_constraint.Width, finalSize.Width))
@ -637,7 +592,7 @@ namespace Avalonia.Controls
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale); var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
_constraint = new Size(finalSize.Deflate(padding).Width, double.PositiveInfinity); _constraint = new Size(Math.Ceiling(finalSize.Deflate(padding).Width), double.PositiveInfinity);
_textLayout = null; _textLayout = null;
@ -685,57 +640,7 @@ namespace Avalonia.Controls
} }
} }
private void InlinesChanged(object? sender, EventArgs e) protected readonly struct SimpleTextSource : ITextSource
{
InvalidateTextLayout();
}
void IInlineHost.AddVisualChild(IControl child)
{
if (child.VisualParent == null)
{
VisualChildren.Add(child);
}
}
void IInlineHost.Invalidate()
{
InvalidateTextLayout();
}
private readonly struct InlinesTextSource : ITextSource
{
private readonly IReadOnlyList<TextRun> _textRuns;
public InlinesTextSource(IReadOnlyList<TextRun> textRuns)
{
_textRuns = textRuns;
}
public TextRun? GetTextRun(int textSourceIndex)
{
var currentPosition = 0;
foreach (var textRun in _textRuns)
{
if(textRun.TextSourceLength == 0)
{
continue;
}
if(currentPosition >= textSourceIndex)
{
return textRun;
}
currentPosition += textRun.TextSourceLength;
}
return null;
}
}
private readonly struct SimpleTextSource : ITextSource
{ {
private readonly ReadOnlySlice<char> _text; private readonly ReadOnlySlice<char> _text;
private readonly TextRunProperties _defaultProperties; private readonly TextRunProperties _defaultProperties;

2
src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml

@ -19,7 +19,7 @@
Classes="textBoxClearButton" Classes="textBoxClearButton"
ToolTip.Tip="Clear" ToolTip.Tip="Clear"
Cursor="Hand" Cursor="Hand"
Command="{ReflectionBinding $parent[TextBox].Clear}" Command="{Binding $parent[TextBox].Clear}"
Opacity="0.5" /> Opacity="0.5" />
<ToggleButton Classes="filter-text-box-toggle" <ToggleButton Classes="filter-text-box-toggle"
ToolTip.Tip="Match Case" ToolTip.Tip="Match Case"

2
src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml

@ -160,7 +160,7 @@
</ContentPresenter> </ContentPresenter>
<Popup Name="PART_Popup" <Popup Name="PART_Popup"
WindowManagerAddShadowHint="False" WindowManagerAddShadowHint="False"
MinWidth="{ReflectionBinding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}" MinWidth="{Binding Bounds.Width, RelativeSource={RelativeSource TemplatedParent}}"
IsLightDismissEnabled="True" IsLightDismissEnabled="True"
IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}" IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}"
OverlayInputPassThroughElement="{Binding $parent[Menu]}"> OverlayInputPassThroughElement="{Binding $parent[Menu]}">

4
src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml

@ -21,7 +21,7 @@
<Setter Property="MinHeight" Value="{DynamicResource ProgressBarThemeMinHeight}" /> <Setter Property="MinHeight" Value="{DynamicResource ProgressBarThemeMinHeight}" />
<Setter Property="VerticalAlignment" Value="Center" /> <Setter Property="VerticalAlignment" Value="Center" />
<Setter Property="Template"> <Setter Property="Template">
<ControlTemplate> <ControlTemplate TargetType="ProgressBar">
<Border x:Name="ProgressBarRoot" ClipToBounds="True" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}"> <Border x:Name="ProgressBarRoot" ClipToBounds="True" Background="{TemplateBinding Background}" BorderBrush="{TemplateBinding BorderBrush}" BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}">
<Panel> <Panel>
<Panel x:Name="DeterminateRoot"> <Panel x:Name="DeterminateRoot">
@ -32,7 +32,7 @@
<Border x:Name="IndeterminateProgressBarIndicator2" CornerRadius="{TemplateBinding CornerRadius}" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" /> <Border x:Name="IndeterminateProgressBarIndicator2" CornerRadius="{TemplateBinding CornerRadius}" Margin="{TemplateBinding Padding}" Background="{TemplateBinding Foreground}" />
</Panel> </Panel>
<LayoutTransformControl x:Name="PART_LayoutTransformControl" HorizontalAlignment="Center" VerticalAlignment="Center" IsVisible="{TemplateBinding ShowProgressText}"> <LayoutTransformControl x:Name="PART_LayoutTransformControl" HorizontalAlignment="Center" VerticalAlignment="Center" IsVisible="{TemplateBinding ShowProgressText}">
<TextBlock Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" Text="{ReflectionBinding Value, RelativeSource={RelativeSource TemplatedParent}, StringFormat={}{0:0}%}" /> <TextBlock Foreground="{DynamicResource SystemControlForegroundBaseHighBrush}" Text="{Binding Value, RelativeSource={RelativeSource TemplatedParent}, StringFormat={}{0:0}%}" />
</LayoutTransformControl> </LayoutTransformControl>
</Panel> </Panel>
</Border> </Border>

6
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@ -45,10 +45,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AvaloniaXamlIlReorderClassesPropertiesTransformer() new AvaloniaXamlIlReorderClassesPropertiesTransformer()
); );
InsertBefore<ContentConvertTransformer>( InsertBefore<ContentConvertTransformer>(
new AvaloniaXamlIlBindingPathParser(),
new AvaloniaXamlIlSelectorTransformer(), new AvaloniaXamlIlSelectorTransformer(),
new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(),
new AvaloniaXamlIlBindingPathParser(),
new AvaloniaXamlIlPropertyPathTransformer(), new AvaloniaXamlIlPropertyPathTransformer(),
new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlSetterTransformer(),
new AvaloniaXamlIlConstructorServiceProviderTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(),

23
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs

@ -122,10 +122,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding); throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding);
} }
var mode = relativeSourceObject.Children var modeProperty = relativeSourceObject.Children
.OfType<XamlAstXamlPropertyValueNode>() .OfType<XamlAstXamlPropertyValueNode>()
.FirstOrDefault(x => x.Property.GetClrProperty().Name == "Mode") .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Mode")?
?.Values[0] is XamlAstTextNode modeAssignedValue ? modeAssignedValue.Text : null; .Values.FirstOrDefault() as XamlAstTextNode
?? relativeSourceObject.Arguments.OfType<XamlAstTextNode>().FirstOrDefault();
var mode = modeProperty?.Text;
if (relativeSourceObject.Arguments.Count == 0 && mode == null) if (relativeSourceObject.Arguments.Count == 0 && mode == null)
{ {
mode = "FindAncestor"; mode = "FindAncestor";
@ -212,16 +215,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
} }
else if (mode == "TemplatedParent") else if (mode == "TemplatedParent")
{ {
var parentType = context.ParentNodes().OfType<AvaloniaXamlIlTargetTypeMetadataNode>() var contentTemplateNode = context.ParentNodes().OfType<AvaloniaXamlIlTargetTypeMetadataNode>()
.FirstOrDefault(x => .FirstOrDefault(x =>
x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate) x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate);
?.TargetType.GetClrType(); if (contentTemplateNode is null)
if (parentType is null)
{ {
throw new XamlParseException("A binding with a TemplatedParent RelativeSource has to be in a ControlTemplate.", binding); throw new XamlParseException("A binding with a TemplatedParent RelativeSource has to be in a ControlTemplate.", binding);
} }
var parentType = contentTemplateNode.TargetType.GetClrType();
if (parentType is null)
{
throw new XamlParseException("TargetType has to be set on ControlTemplate or it should be defined inside of a Style.", binding);
}
convertedNode = new TemplatedParentBindingExpressionNode { Type = parentType }; convertedNode = new TemplatedParentBindingExpressionNode { Type = parentType };
} }
else else

5
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs

@ -105,6 +105,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
{ {
startType = TypeReferenceResolver.ResolveType(context, text.Text, isMarkupExtension: false, text, strict: true).Type; startType = TypeReferenceResolver.ResolveType(context, text.Text, isMarkupExtension: false, text, strict: true).Type;
} }
if (dataTypeProperty?.Values.Count is 1 && dataTypeProperty.Values[0] is XamlTypeExtensionNode typeNode)
{
startType = typeNode.Value.GetClrType();
}
Func<IXamlType> startTypeResolver = startType is not null ? () => startType : () => Func<IXamlType> startTypeResolver = startType is not null ? () => startType : () =>
{ {

16
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs

@ -47,7 +47,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
return startTypeResolver(); return startTypeResolver();
} }
if (bindingPathAssignment.Values[0] is ParsedBindingPathNode bindingPathNode) if (bindingPathAssignment.Values[0] is XamlIlBindingPathNode pathNode)
{
bindingResultType = pathNode.BindingResultType;
}
else if (bindingPathAssignment.Values[0] is ParsedBindingPathNode bindingPathNode)
{ {
var transformed = TransformBindingPath( var transformed = TransformBindingPath(
context, context,
@ -63,7 +67,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
} }
else else
{ {
throw new InvalidOperationException(); throw new InvalidOperationException("Invalid state of Path property");
} }
} }
@ -240,6 +244,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
case TemplatedParentBindingExpressionNode templatedParent: case TemplatedParentBindingExpressionNode templatedParent:
var templatedParentField = context.GetAvaloniaTypes().StyledElement.GetAllFields() var templatedParentField = context.GetAvaloniaTypes().StyledElement.GetAllFields()
.FirstOrDefault(f => f.IsStatic && f.IsPublic && f.Name == "TemplatedParentProperty"); .FirstOrDefault(f => f.IsStatic && f.IsPublic && f.Name == "TemplatedParentProperty");
nodes.Add(new SelfPathElementNode(selfType));
nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode( nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(
templatedParentField, templatedParentField,
templatedParent.Type)); templatedParent.Type));
@ -374,6 +379,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
public static IXamlType GetTargetType(IXamlAstNode namescopeRoot, string name) public static IXamlType GetTargetType(IXamlAstNode namescopeRoot, string name)
{ {
// If we start from the nested scope - skip it.
if (namescopeRoot is NestedScopeMetadataNode scope)
{
namescopeRoot = scope.Value;
}
var finder = new ScopeRegistrationFinder(name); var finder = new ScopeRegistrationFinder(name);
namescopeRoot.Visit(finder); namescopeRoot.Visit(finder);
return finder.TargetType; return finder.TargetType;
@ -399,6 +410,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node)
{ {
// Ignore name registrations, if we are inside of the nested namescope.
if (_childScopesStack.Count == 0 && node is AvaloniaNameScopeRegistrationXamlIlNode registration) if (_childScopesStack.Count == 0 && node is AvaloniaNameScopeRegistrationXamlIlNode registration)
{ {
if (registration.Name is XamlAstTextNode text && text.Text == Name) if (registration.Name is XamlAstTextNode text && text.Text == Name)

2
src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs

@ -74,6 +74,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
return pathRoot ?? new EmptyExpressionNode(); return pathRoot ?? new EmptyExpressionNode();
} }
internal IEnumerable<ICompiledBindingPathElement> Elements => _elements;
internal SourceMode SourceMode => _elements.Count > 0 && _elements[0] is IControlSourceBindingPathElement ? SourceMode.Control : SourceMode.Data; internal SourceMode SourceMode => _elements.Count > 0 && _elements[0] is IControlSourceBindingPathElement ? SourceMode.Control : SourceMode.Data;
internal object RawSource { get; } internal object RawSource { get; }

15
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -870,7 +870,14 @@ namespace Avalonia.Win32
if (fullscreen) if (fullscreen)
{ {
GetWindowRect(_hwnd, out var windowRect); GetWindowRect(_hwnd, out var windowRect);
_savedWindowInfo.WindowRect = windowRect; GetClientRect(_hwnd, out var clientRect);
clientRect.left += windowRect.left;
clientRect.right += windowRect.left;
clientRect.top += windowRect.top;
clientRect.bottom += windowRect.top;
_savedWindowInfo.WindowRect = clientRect;
var current = GetStyle(); var current = GetStyle();
var currentEx = GetExtendedStyle(); var currentEx = GetExtendedStyle();
@ -907,10 +914,10 @@ namespace Avalonia.Win32
SetExtendedStyle(_savedWindowInfo.ExStyle, false); SetExtendedStyle(_savedWindowInfo.ExStyle, false);
// On restore, resize to the previous saved rect size. // On restore, resize to the previous saved rect size.
var new_rect = _savedWindowInfo.WindowRect.ToPixelRect(); var newClientRect = _savedWindowInfo.WindowRect.ToPixelRect();
SetWindowPos(_hwnd, IntPtr.Zero, new_rect.X, new_rect.Y, new_rect.Width, SetWindowPos(_hwnd, IntPtr.Zero, newClientRect.X, newClientRect.Y, newClientRect.Width,
new_rect.Height, newClientRect.Height,
SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED);
UpdateWindowProperties(_windowProperties, true); UpdateWindowProperties(_windowProperties, true);

5
tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs

@ -142,14 +142,13 @@ namespace Avalonia.Base.UnitTests.Styling
var border = (Border)target.Object.VisualChildren.Single(); var border = (Border)target.Object.VisualChildren.Single();
var selector = default(Selector).OfType(templatedControl.Object.GetType()).Class("foo").Template().OfType<Border>(); var selector = default(Selector).OfType(templatedControl.Object.GetType()).Class("foo").Template().OfType<Border>();
var activator = selector.Match(border).Activator; var activator = selector.Match(border).Activator;
var inccDebug = (INotifyCollectionChangedDebug)styleable.Object.Classes;
using (activator.Subscribe(_ => { })) using (activator.Subscribe(_ => { }))
{ {
Assert.Single(inccDebug.GetCollectionChangedSubscribers()); Assert.Equal(1, ((Classes)styleable.Object.Classes).ListenerCount);
} }
Assert.Null(inccDebug.GetCollectionChangedSubscribers()); Assert.Equal(0, ((Classes)styleable.Object.Classes).ListenerCount);
} }
private void BuildVisualTree<T>(Mock<T> templatedControl) where T : class, IVisual private void BuildVisualTree<T>(Mock<T> templatedControl) where T : class, IVisual

2
tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs

@ -49,7 +49,7 @@ namespace Avalonia.Base.UnitTests.Styling
setter.Instance(control).Start(false); setter.Instance(control).Start(false);
Assert.Equal("", control.Text); Assert.Equal(null, control.Text);
} }
[Fact] [Fact]

85
tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs

@ -0,0 +1,85 @@
using System.Collections.Generic;
using System.Runtime.CompilerServices;
using Avalonia.Controls;
using Avalonia.Styling;
using BenchmarkDotNet.Attributes;
#nullable enable
namespace Avalonia.Benchmarks.Styling
{
[MemoryDiagnoser]
public class Style_ClassSelector
{
private Style _style = null!;
public Style_ClassSelector()
{
RuntimeHelpers.RunClassConstructor(typeof(TestClass).TypeHandle);
}
[GlobalSetup]
public void Setup()
{
_style = new Style(x => x.OfType<TestClass>().Class("foo"))
{
Setters = { new Setter(TestClass.StringProperty, "foo") }
};
}
[Benchmark(OperationsPerInvoke = 50)]
public void Apply()
{
var target = new TestClass();
target.BeginBatchUpdate();
for (var i = 0; i < 50; ++i)
_style.TryAttach(target, null);
target.EndBatchUpdate();
}
[Benchmark(OperationsPerInvoke = 50)]
public void Apply_Toggle()
{
var target = new TestClass();
target.BeginBatchUpdate();
for (var i = 0; i < 50; ++i)
_style.TryAttach(target, null);
target.EndBatchUpdate();
target.Classes.Add("foo");
target.Classes.Remove("foo");
}
[Benchmark(OperationsPerInvoke = 50)]
public void Apply_Detach()
{
var target = new TestClass();
target.BeginBatchUpdate();
for (var i = 0; i < 50; ++i)
_style.TryAttach(target, null);
target.EndBatchUpdate();
target.DetachStyles();
}
private class TestClass : Control
{
public static readonly StyledProperty<string?> StringProperty =
AvaloniaProperty.Register<TestClass, string?>("String");
public void DetachStyles() => InvalidateStyles();
}
private class TestClass2 : Control
{
}
}
}

52
tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs

@ -0,0 +1,52 @@
using Avalonia.Controls.Documents;
using Avalonia.Media;
using Avalonia.UnitTests;
using Xunit;
namespace Avalonia.Controls.UnitTests
{
public class RichTextBlockTests
{
[Fact]
public void Changing_InlinesCollection_Should_Invalidate_Measure()
{
using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
{
var target = new RichTextBlock();
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 RichTextBlock();
var inline = new Run("Hello");
target.Inlines.Add(inline);
target.Measure(Size.Infinity);
Assert.True(target.IsMeasureValid);
inline.Foreground = Brushes.Green;
Assert.False(target.IsMeasureValid);
}
}
}
}

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

@ -20,13 +20,11 @@ namespace Avalonia.Controls.UnitTests
} }
[Fact] [Fact]
public void Default_Text_Value_Should_Be_EmptyString() public void Default_Text_Value_Should_Be_Null()
{ {
var textBlock = new TextBlock(); var textBlock = new TextBlock();
Assert.Equal( Assert.Equal(null, textBlock.Text);
"",
textBlock.Text);
} }
[Fact] [Fact]
@ -62,47 +60,5 @@ namespace Avalonia.Controls.UnitTests
renderer.Verify(x => x.AddDirty(target), Times.Once); 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);
}
}
} }
} }

1
tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh

@ -5,4 +5,5 @@ cd "$SCRIPT_DIR"/../.. || exit
git clean -xdf git clean -xdf
./build.sh CompileNative ./build.sh CompileNative
./samples/IntegrationTestApp/bundle.sh ./samples/IntegrationTestApp/bundle.sh
open -n ./samples/IntegrationTestApp/bin/Debug/net6.0/osx-arm64/publish/IntegrationTestApp.app
dotnet test tests/Avalonia.IntegrationTests.Appium/ -l "console;verbosity=detailed" dotnet test tests/Avalonia.IntegrationTests.Appium/ -l "console;verbosity=detailed"

2
tests/Avalonia.LeakTests/ControlTests.cs

@ -314,7 +314,7 @@ namespace Avalonia.LeakTests
// The TextBox should have subscriptions to its Classes collection from the // The TextBox should have subscriptions to its Classes collection from the
// default theme. // default theme.
Assert.NotEmpty(((INotifyCollectionChangedDebug)textBox.Classes).GetCollectionChangedSubscribers()); Assert.NotEqual(0, textBox.Classes.ListenerCount);
// Clear the content and ensure the TextBox is removed. // Clear the content and ensure the TextBox is removed.
window.Content = null; window.Content = null;

11
tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs

@ -637,6 +637,17 @@ namespace Avalonia.Markup.UnitTests.Data
Assert.Equal("baz", source["Foo"]); Assert.Equal("baz", source["Foo"]);
} }
[Fact]
public void Binding_To_Types_Should_Work()
{
var type = typeof(string);
var textBlock = new TextBlock() { DataContext = type };
using (textBlock.Bind(TextBlock.TextProperty, new Binding("Name")))
{
Assert.Equal("String", textBlock.Text);
};
}
private class StyledPropertyClass : AvaloniaObject private class StyledPropertyClass : AvaloniaObject
{ {
public static readonly StyledProperty<double> DoubleValueProperty = public static readonly StyledProperty<double> DoubleValueProperty =

225
tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs

@ -3,22 +3,23 @@ using System.Collections.Generic;
using System.Collections.ObjectModel; using System.Collections.ObjectModel;
using System.ComponentModel; using System.ComponentModel;
using System.Globalization; using System.Globalization;
using System.Linq;
using System.Reactive.Subjects; using System.Reactive.Subjects;
using System.Text;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Presenters; using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates; using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Data.Converters; using Avalonia.Data.Converters;
using Avalonia.Data.Core; using Avalonia.Data.Core;
using Avalonia.Input; using Avalonia.Input;
using Avalonia.Markup.Data; using Avalonia.Markup.Data;
using Avalonia.Markup.Xaml.MarkupExtensions;
using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings;
using Avalonia.Markup.Xaml.Templates; using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media; using Avalonia.Media;
using Avalonia.Metadata; using Avalonia.Metadata;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using JetBrains.Annotations;
using XamlX;
using Xunit; using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
@ -102,6 +103,91 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
Assert.Equal(dataContext.StringProperty, textBlock.Text); Assert.Equal(dataContext.StringProperty, textBlock.Text);
} }
} }
[Fact]
public void ResolvesPathPassedByPropertyWithInnerItemTemplate()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:TestDataContext'>
<ItemsControl Name='itemsControl' Items='{CompiledBinding Path=ListProperty}'>
<ItemsControl.ItemTemplate>
<DataTemplate>
<TextBlock />
</DataTemplate>
</ItemsControl.ItemTemplate>
</ItemsControl>
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = window.FindControl<ItemsControl>("itemsControl");
var dataContext = new TestDataContext
{
ListProperty =
{
"Hello"
}
};
window.DataContext = dataContext;
Assert.Equal(dataContext.ListProperty, textBlock.Items);
}
}
[Fact]
public void ResolvesDataTypeFromBindingProperty()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'>
<TextBlock Text='{CompiledBinding StringProperty, DataType=local:TestDataContext}' Name='textBlock' />
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
var dataContext = new TestDataContext
{
StringProperty = "foobar"
};
window.DataContext = dataContext;
Assert.Equal(dataContext.StringProperty, textBlock.Text);
}
}
[Fact]
public void ResolvesDataTypeFromBindingProperty_TypeExtension()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'>
<TextBlock Text='{CompiledBinding StringProperty, DataType={x:Type local:TestDataContext}}' Name='textBlock' />
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var textBlock = window.FindControl<TextBlock>("textBlock");
var dataContext = new TestDataContext
{
StringProperty = "foobar"
};
window.DataContext = dataContext;
Assert.Equal(dataContext.StringProperty, textBlock.Text);
}
}
[Fact] [Fact]
public void ResolvesStreamTaskBindingCorrectly() public void ResolvesStreamTaskBindingCorrectly()
@ -641,6 +727,64 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
} }
} }
[Fact]
public void ResolvesRelativeSourceBindingFromTemplate()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<ContentControl xmlns='https://github.com/avaloniaui'
Content='Hello'>
<ContentControl.Styles>
<Style Selector='ContentControl'>
<Setter Property='Template'>
<ControlTemplate>
<ContentPresenter Content='{CompiledBinding Content, RelativeSource={RelativeSource TemplatedParent}}' />
</ControlTemplate>
</Setter>
</Style>
</ContentControl.Styles>
</ContentControl>";
var contentControl = AvaloniaRuntimeXamlLoader.Parse<ContentControl>(xaml);
contentControl.Measure(new Size(10, 10));
var result = contentControl.GetTemplateChildren().OfType<ContentPresenter>().First();
Assert.Equal("Hello", result.Content);
}
}
[Fact]
public void ResolvesElementNameInTemplate()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<ContentControl xmlns='https://github.com/avaloniaui'
Content='Hello'>
<ContentControl.Styles>
<Style Selector='ContentControl'>
<Setter Property='Template'>
<ControlTemplate>
<Panel>
<TextBox Name='InnerTextBox' Text='Hello' />
<ContentPresenter Content='{CompiledBinding Text, ElementName=InnerTextBox}' />
</Panel>
</ControlTemplate>
</Setter>
</Style>
</ContentControl.Styles>
</ContentControl>";
var contentControl = AvaloniaRuntimeXamlLoader.Parse<ContentControl>(xaml);
contentControl.Measure(new Size(10, 10));
var result = contentControl.GetTemplateChildren().OfType<ContentPresenter>().First();
Assert.Equal("Hello", result.Content);
}
}
[Fact] [Fact]
public void Binds_To_Source() public void Binds_To_Source()
{ {
@ -1145,6 +1289,28 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
Assert.Equal("bar-" + typeof(TestDataContext).FullName, textBlock.Text); Assert.Equal("bar-" + typeof(TestDataContext).FullName, textBlock.Text);
} }
} }
[Fact]
public void SupportCastToTypeInExpressionWithProperty_ExplicitPropertyCast()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<Window xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='using:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions'>
<ContentControl Content='{CompiledBinding $parent.((local:IHasExplicitProperty)DataContext).ExplicitProperty}' Name='contentControl' />
</Window>";
var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
var contentControl = window.GetControl<ContentControl>("contentControl");
var dataContext = new TestDataContext();
window.DataContext = dataContext;
Assert.Equal(((IHasExplicitProperty)dataContext).ExplicitProperty, contentControl.Content);
}
}
[Fact] [Fact]
public void Binds_To_Self_Without_DataType() public void Binds_To_Self_Without_DataType()
@ -1366,6 +1532,43 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
} }
} }
[Fact]
public void ResolvesDataTypeForAssignBinding()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<local:AssignBindingControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
x:DataType='local:TestDataContext'
X='{CompiledBinding StringProperty}' />";
var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var compiledPath = ((CompiledBindingExtension)control.X).Path;
var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath.Elements));
Assert.Equal(typeof(string), node.Property.PropertyType);
}
}
[Fact]
public void ResolvesDataTypeForAssignBinding_FromBindingProperty()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var xaml = @"
<local:AssignBindingControl xmlns='https://github.com/avaloniaui'
xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'
xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.MarkupExtensions;assembly=Avalonia.Markup.Xaml.UnitTests'
X='{CompiledBinding StringProperty, DataType=local:TestDataContext}' />";
var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(xaml);
var compiledPath = ((CompiledBindingExtension)control.X).Path;
var node = Assert.IsType<PropertyElement>(Assert.Single(compiledPath.Elements));
Assert.Equal(typeof(string), node.Property.PropertyType);
}
}
void Throws(string type, Action cb) void Throws(string type, Action cb)
{ {
try try
@ -1410,6 +1613,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
public interface IHasPropertyDerived : IHasProperty public interface IHasPropertyDerived : IHasProperty
{ } { }
public interface IHasExplicitProperty
{
string ExplicitProperty { get; }
}
public class AppendConverter : IValueConverter public class AppendConverter : IValueConverter
{ {
public static IValueConverter Instance { get; } = new AppendConverter(); public static IValueConverter Instance { get; } = new AppendConverter();
@ -1429,7 +1637,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
public class TestDataContextBaseClass {} public class TestDataContextBaseClass {}
public class TestDataContext : TestDataContextBaseClass, IHasPropertyDerived public class TestDataContext : TestDataContextBaseClass, IHasPropertyDerived, IHasExplicitProperty
{ {
public string StringProperty { get; set; } public string StringProperty { get; set; }
@ -1449,6 +1657,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
public INonIntegerIndexerDerived NonIntegerIndexerInterfaceProperty => NonIntegerIndexerProperty; public INonIntegerIndexerDerived NonIntegerIndexerInterfaceProperty => NonIntegerIndexerProperty;
string IHasExplicitProperty.ExplicitProperty => "Hello";
public string ExplicitProperty => "Bye";
public class NonIntegerIndexer : NotifyingBase, INonIntegerIndexerDerived public class NonIntegerIndexer : NotifyingBase, INonIntegerIndexerDerived
{ {
private readonly Dictionary<string, string> _storage = new Dictionary<string, string>(); private readonly Dictionary<string, string> _storage = new Dictionary<string, string>();
@ -1534,4 +1746,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
} }
public class CustomDataTemplateInherit : CustomDataTemplate { } public class CustomDataTemplateInherit : CustomDataTemplate { }
public class AssignBindingControl : Control
{
[AssignBinding] public IBinding X { get; set; }
}
} }

Loading…
Cancel
Save