diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml
new file mode 100644
index 0000000000..c3f6292703
--- /dev/null
+++ b/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'
diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml
index fe9455bd29..cb49ba96c6 100644
--- a/samples/ControlCatalog/Pages/TextBlockPage.xaml
+++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml
@@ -118,7 +118,7 @@
-
+
This is a
TextBlock
with several
@@ -126,7 +126,7 @@
using a variety of styles
.
-
+
diff --git a/src/Avalonia.Base/Controls/Classes.cs b/src/Avalonia.Base/Controls/Classes.cs
index 50605661fa..e64209c3cb 100644
--- a/src/Avalonia.Base/Controls/Classes.cs
+++ b/src/Avalonia.Base/Controls/Classes.cs
@@ -14,6 +14,8 @@ namespace Avalonia.Controls
///
public class Classes : AvaloniaList, IPseudoClasses
{
+ private List? _listeners;
+
///
/// Initializes a new instance of the class.
///
@@ -39,6 +41,11 @@ namespace Avalonia.Controls
{
}
+ ///
+ /// Gets the number of listeners subscribed to this collection for unit testing purposes.
+ ///
+ internal int ListenerCount => _listeners?.Count ?? 0;
+
///
/// Parses a classes string.
///
@@ -62,6 +69,7 @@ namespace Avalonia.Controls
if (!Contains(name))
{
base.Add(name);
+ NotifyChanged();
}
}
@@ -89,6 +97,7 @@ namespace Avalonia.Controls
}
base.AddRange(c);
+ NotifyChanged();
}
///
@@ -103,6 +112,8 @@ namespace Avalonia.Controls
RemoveAt(i);
}
}
+
+ NotifyChanged();
}
///
@@ -122,6 +133,7 @@ namespace Avalonia.Controls
if (!Contains(name))
{
base.Insert(index, name);
+ NotifyChanged();
}
}
@@ -154,6 +166,7 @@ namespace Avalonia.Controls
if (toInsert != null)
{
base.InsertRange(index, toInsert);
+ NotifyChanged();
}
}
@@ -169,7 +182,14 @@ namespace Avalonia.Controls
public override bool Remove(string name)
{
ThrowIfPseudoclass(name, "removed");
- return base.Remove(name);
+
+ if (base.Remove(name))
+ {
+ NotifyChanged();
+ return true;
+ }
+
+ return false;
}
///
@@ -197,6 +217,7 @@ namespace Avalonia.Controls
if (toRemove != null)
{
base.RemoveAll(toRemove);
+ NotifyChanged();
}
}
@@ -214,6 +235,7 @@ namespace Avalonia.Controls
var name = this[index];
ThrowIfPseudoclass(name, "removed");
base.RemoveAt(index);
+ NotifyChanged();
}
///
@@ -224,6 +246,7 @@ namespace Avalonia.Controls
public override void RemoveRange(int index, int count)
{
base.RemoveRange(index, count);
+ NotifyChanged();
}
///
@@ -255,6 +278,7 @@ namespace Avalonia.Controls
}
base.AddRange(source);
+ NotifyChanged();
}
///
@@ -263,13 +287,38 @@ namespace Avalonia.Controls
if (!Contains(name))
{
base.Add(name);
+ NotifyChanged();
}
}
///
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)
diff --git a/src/Avalonia.Base/Controls/IClassesChangedListener.cs b/src/Avalonia.Base/Controls/IClassesChangedListener.cs
new file mode 100644
index 0000000000..b4de893c97
--- /dev/null
+++ b/src/Avalonia.Base/Controls/IClassesChangedListener.cs
@@ -0,0 +1,14 @@
+namespace Avalonia.Controls
+{
+ ///
+ /// Internal interface for listening to changes in in a more
+ /// performant manner than subscribing to CollectionChanged.
+ ///
+ internal interface IClassesChangedListener
+ {
+ ///
+ /// Notifies the listener that the collection has changed.
+ ///
+ void Changed();
+ }
+}
diff --git a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
index b93bf87fdf..91d69b5d3d 100644
--- a/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
+++ b/src/Avalonia.Base/Data/Core/Plugins/InpcPropertyAccessorPlugin.cs
@@ -55,7 +55,7 @@ namespace Avalonia.Data.Core.Plugins
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);
var type = instance.GetType();
diff --git a/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs b/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs
index 98d3f16a0a..3f70ff50b3 100644
--- a/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs
+++ b/src/Avalonia.Base/Styling/Activators/StyleClassActivator.cs
@@ -1,6 +1,7 @@
using System.Collections.Generic;
using System.Collections.Specialized;
using Avalonia.Collections;
+using Avalonia.Controls;
#nullable enable
@@ -10,21 +11,17 @@ namespace Avalonia.Styling.Activators
/// An which is active when a set of classes match those on a
/// control.
///
- internal sealed class StyleClassActivator : StyleActivatorBase
+ internal sealed class StyleClassActivator : StyleActivatorBase, IClassesChangedListener
{
private readonly IList _match;
- private readonly IAvaloniaReadOnlyList _classes;
- private NotifyCollectionChangedEventHandler? _classesChangedHandler;
+ private readonly Classes _classes;
- public StyleClassActivator(IAvaloniaReadOnlyList classes, IList match)
+ public StyleClassActivator(Classes classes, IList match)
{
_classes = classes;
_match = match;
}
- private NotifyCollectionChangedEventHandler ClassesChangedHandler =>
- _classesChangedHandler ??= ClassesChanged;
-
public static bool AreClassesMatching(IReadOnlyList classes, IList toMatch)
{
int remainingMatches = toMatch.Count;
@@ -55,23 +52,20 @@ namespace Avalonia.Styling.Activators
return remainingMatches == 0;
}
- protected override void Initialize()
+ void IClassesChangedListener.Changed()
{
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)
- {
- PublishNext(IsMatching());
- }
+ _classes.RemoveListener(this);
}
private bool IsMatching() => AreClassesMatching(_classes, _match);
diff --git a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
index 24d5d6bbbf..d52c8c7d5c 100644
--- a/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
+++ b/src/Avalonia.Base/Styling/TypeNameAndClassSelector.cs
@@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Text;
+using Avalonia.Controls;
using Avalonia.Styling.Activators;
#nullable enable
@@ -125,7 +126,7 @@ namespace Avalonia.Styling
{
if (subscribe)
{
- var observable = new StyleClassActivator(control.Classes, _classes.Value);
+ var observable = new StyleClassActivator((Classes)control.Classes, _classes.Value);
return new SelectorMatch(observable);
}
diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs
index aaac3f8f9c..d42468f47e 100644
--- a/src/Avalonia.Controls.DataGrid/DataGrid.cs
+++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs
@@ -5990,15 +5990,14 @@ namespace Avalonia.Controls
/// The formatted string.
private string FormatClipboardContent(DataGridRowClipboardEventArgs e)
{
- StringBuilder text = new StringBuilder();
- for (int cellIndex = 0; cellIndex < e.ClipboardRowContent.Count; cellIndex++)
- {
- DataGridClipboardCellContent cellContent = e.ClipboardRowContent[cellIndex];
- if (cellContent != null)
- {
- text.Append(cellContent.Content);
- }
- if (cellIndex < e.ClipboardRowContent.Count - 1)
+ var text = new StringBuilder();
+ var clipboardRowContent = e.ClipboardRowContent;
+ var numberOfItem = clipboardRowContent.Count;
+ for (int cellIndex = 0; cellIndex < numberOfItem; cellIndex++)
+ {
+ var cellContent = clipboardRowContent[cellIndex];
+ text.Append(cellContent.Content);
+ if (cellIndex < numberOfItem - 1)
{
text.Append('\t');
}
diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs
index a76222385e..dc688fc359 100644
--- a/src/Avalonia.Controls/Documents/InlineCollection.cs
+++ b/src/Avalonia.Controls/Documents/InlineCollection.cs
@@ -12,39 +12,55 @@ namespace Avalonia.Controls.Documents
[WhitespaceSignificantCollection]
public class InlineCollection : AvaloniaList
{
- private readonly IInlineHost? _host;
+ private ILogical? _parent;
+ private IInlineHost? _inlineHost;
private string? _text = string.Empty;
///
/// Initializes a new instance of the class.
///
- public InlineCollection(ILogical parent) : this(parent, null) { }
-
- ///
- /// Initializes a new instance of the class.
- ///
- internal InlineCollection(ILogical parent, IInlineHost? host = null) : base(0)
+ public InlineCollection()
{
- _host = host;
-
ResetBehavior = ResetBehavior.Remove;
this.ForEachItem(
x =>
{
- ((ISetLogicalParent)x).SetParent(parent);
- x.InlineHost = host;
- host?.Invalidate();
+ ((ISetLogicalParent)x).SetParent(Parent);
+ x.InlineHost = InlineHost;
+ Invalidate();
},
x =>
{
((ISetLogicalParent)x).SetParent(null);
- x.InlineHost = host;
- host?.Invalidate();
+ x.InlineHost = InlineHost;
+ Invalidate();
},
() => 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;
///
@@ -61,10 +77,10 @@ namespace Avalonia.Controls.Documents
{
return _text;
}
-
+
var builder = new StringBuilder();
- foreach(var inline in this)
+ foreach (var inline in this)
{
inline.AppendText(builder);
}
@@ -100,7 +116,7 @@ namespace Avalonia.Controls.Documents
}
else
{
- _text += text;
+ _text = text;
}
}
@@ -120,7 +136,7 @@ namespace Avalonia.Controls.Documents
base.Add(new Run(_text));
}
- _text = string.Empty;
+ _text = null;
}
base.Add(item);
@@ -136,14 +152,28 @@ namespace Avalonia.Controls.Documents
///
protected void Invalidate()
{
- if(_host != null)
+ if(InlineHost != null)
{
- _host.Invalidate();
+ InlineHost.Invalidate();
}
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;
+ }
+ }
}
}
diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs
index bd1b4fc5e1..c7289dbc3f 100644
--- a/src/Avalonia.Controls/Documents/Span.cs
+++ b/src/Avalonia.Controls/Documents/Span.cs
@@ -14,25 +14,27 @@ namespace Avalonia.Controls.Documents
///
/// Defines the property.
///
- public static readonly DirectProperty InlinesProperty =
- AvaloniaProperty.RegisterDirect(
- nameof(Inlines),
- o => o.Inlines);
+ public static readonly StyledProperty InlinesProperty =
+ AvaloniaProperty.Register(
+ nameof(Inlines));
- ///
- /// Initializes a new instance of a Span element.
- ///
public Span()
{
- Inlines = new InlineCollection(this);
- Inlines.Invalidated += (s, e) => InlineHost?.Invalidate();
+ Inlines = new InlineCollection
+ {
+ Parent = this
+ };
}
///
/// Gets or sets the inlines.
///
[Content]
- public InlineCollection Inlines { get; }
+ public InlineCollection Inlines
+ {
+ get => GetValue(InlinesProperty);
+ set => SetValue(InlinesProperty, value);
+ }
internal override void BuildTextRun(IList textRuns)
{
@@ -52,7 +54,7 @@ namespace Avalonia.Controls.Documents
var textCharacters = new TextCharacters(text.AsMemory(), textRunProperties);
textRuns.Add(textCharacters);
- }
+ }
}
}
@@ -65,10 +67,52 @@ namespace Avalonia.Controls.Documents
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();
}
}
}
diff --git a/src/Avalonia.Controls/Documents/TextElement.cs b/src/Avalonia.Controls/Documents/TextElement.cs
index f228519e60..5bac3642ed 100644
--- a/src/Avalonia.Controls/Documents/TextElement.cs
+++ b/src/Avalonia.Controls/Documents/TextElement.cs
@@ -67,6 +67,8 @@ namespace Avalonia.Controls.Documents
Brushes.Black,
inherits: true);
+ private IInlineHost? _inlineHost;
+
///
/// Gets or sets a brush used to paint the control's background.
///
@@ -250,7 +252,21 @@ namespace Avalonia.Controls.Documents
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)
{
diff --git a/src/Avalonia.Controls/Presenters/TextPresenter.cs b/src/Avalonia.Controls/Presenters/TextPresenter.cs
index 3523cd5214..e463bc5731 100644
--- a/src/Avalonia.Controls/Presenters/TextPresenter.cs
+++ b/src/Avalonia.Controls/Presenters/TextPresenter.cs
@@ -543,9 +543,11 @@ namespace Avalonia.Controls.Presenters
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))
@@ -553,7 +555,7 @@ namespace Avalonia.Controls.Presenters
return finalSize;
}
- _constraint = new Size(finalSize.Width, double.PositiveInfinity);
+ _constraint = new Size(Math.Ceiling(finalSize.Width), double.PositiveInfinity);
_textLayout = null;
diff --git a/src/Avalonia.Controls/Primitives/AccessText.cs b/src/Avalonia.Controls/Primitives/AccessText.cs
index 87cf660cad..7e5b34acd9 100644
--- a/src/Avalonia.Controls/Primitives/AccessText.cs
+++ b/src/Avalonia.Controls/Primitives/AccessText.cs
@@ -79,9 +79,9 @@ namespace Avalonia.Controls.Primitives
}
///
- 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));
}
///
diff --git a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
index 3f42d95deb..ae663defd3 100644
--- a/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
+++ b/src/Avalonia.Controls/Repeater/ItemsRepeater.cs
@@ -421,6 +421,7 @@ namespace Avalonia.Controls
protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
{
+ base.OnAttachedToVisualTree(e);
InvalidateMeasure();
_viewportManager.ResetScrollers();
}
diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs
new file mode 100644
index 0000000000..2b84113497
--- /dev/null
+++ b/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
+{
+ ///
+ /// A control that displays a block of formatted text.
+ ///
+ public class RichTextBlock : TextBlock, IInlineHost
+ {
+ public static readonly StyledProperty IsTextSelectionEnabledProperty =
+ AvaloniaProperty.Register(nameof(IsTextSelectionEnabled), false);
+
+ public static readonly DirectProperty SelectionStartProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(SelectionStart),
+ o => o.SelectionStart,
+ (o, v) => o.SelectionStart = v);
+
+ public static readonly DirectProperty SelectionEndProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(SelectionEnd),
+ o => o.SelectionEnd,
+ (o, v) => o.SelectionEnd = v);
+
+ public static readonly DirectProperty SelectedTextProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(SelectedText),
+ o => o.SelectedText);
+
+ public static readonly StyledProperty SelectionBrushProperty =
+ AvaloniaProperty.Register(nameof(SelectionBrush), Brushes.Blue);
+
+ public static readonly StyledProperty SelectionForegroundBrushProperty =
+ AvaloniaProperty.Register(nameof(SelectionForegroundBrush));
+
+ ///
+ /// Defines the property.
+ ///
+ public static readonly StyledProperty InlinesProperty =
+ AvaloniaProperty.Register(
+ nameof(Inlines));
+
+ public static readonly DirectProperty CanCopyProperty =
+ AvaloniaProperty.RegisterDirect(
+ nameof(CanCopy),
+ o => o.CanCopy);
+
+ public static readonly RoutedEvent CopyingToClipboardEvent =
+ RoutedEvent.Register(
+ nameof(CopyingToClipboard), RoutingStrategies.Bubble);
+
+ private bool _canCopy;
+ private int _selectionStart;
+ private int _selectionEnd;
+
+ static RichTextBlock()
+ {
+ FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true);
+
+ AffectsRender(SelectionStartProperty, SelectionEndProperty, SelectionForegroundBrushProperty, SelectionBrushProperty);
+ }
+
+ public RichTextBlock()
+ {
+ Inlines = new InlineCollection
+ {
+ Parent = this,
+ InlineHost = this
+ };
+ }
+
+ ///
+ /// Gets or sets the brush that highlights selected text.
+ ///
+ public IBrush? SelectionBrush
+ {
+ get => GetValue(SelectionBrushProperty);
+ set => SetValue(SelectionBrushProperty, value);
+ }
+
+ ///
+ /// Gets or sets a value that defines the brush used for selected text.
+ ///
+ public IBrush? SelectionForegroundBrush
+ {
+ get => GetValue(SelectionForegroundBrushProperty);
+ set => SetValue(SelectionForegroundBrushProperty, value);
+ }
+
+ ///
+ /// Gets or sets a character index for the beginning of the current selection.
+ ///
+ public int SelectionStart
+ {
+ get => _selectionStart;
+ set
+ {
+ if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value))
+ {
+ RaisePropertyChanged(SelectedTextProperty, "", "");
+ }
+ }
+ }
+
+ ///
+ /// Gets or sets a character index for the end of the current selection.
+ ///
+ public int SelectionEnd
+ {
+ get => _selectionEnd;
+ set
+ {
+ if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value))
+ {
+ RaisePropertyChanged(SelectedTextProperty, "", "");
+ }
+ }
+ }
+
+ ///
+ /// Gets the content of the current selection.
+ ///
+ public string SelectedText
+ {
+ get => GetSelection();
+ }
+
+ ///
+ /// Gets or sets a value that indicates whether text selection is enabled, either through user action or calling selection-related API.
+ ///
+ public bool IsTextSelectionEnabled
+ {
+ get => GetValue(IsTextSelectionEnabledProperty);
+ set => SetValue(IsTextSelectionEnabledProperty, value);
+ }
+
+ ///
+ /// Gets or sets the inlines.
+ ///
+ [Content]
+ public InlineCollection Inlines
+ {
+ get => GetValue(InlinesProperty);
+ set => SetValue(InlinesProperty, value);
+ }
+
+ ///
+ /// Property for determining if the Copy command can be executed.
+ ///
+ public bool CanCopy
+ {
+ get => _canCopy;
+ private set => SetAndRaise(CanCopyProperty, ref _canCopy, value);
+ }
+
+ public event EventHandler? CopyingToClipboard
+ {
+ add => AddHandler(CopyingToClipboardEvent, value);
+ remove => RemoveHandler(CopyingToClipboardEvent, value);
+ }
+
+ ///
+ /// Copies the current selection to the Clipboard.
+ ///
+ 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);
+ }
+
+ ///
+ /// Select all text in the TextBox
+ ///
+ public void SelectAll()
+ {
+ if (!IsTextSelectionEnabled)
+ {
+ return;
+ }
+
+ var text = Text;
+
+ SelectionStart = 0;
+ SelectionEnd = text?.Length ?? 0;
+ }
+
+ ///
+ /// Clears the current selection/>
+ ///
+ 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);
+ }
+
+ ///
+ /// Creates the used to render the text.
+ ///
+ /// A object.
+ 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();
+
+ 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();
+
+ bool Match(List 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 _textRuns;
+
+ public InlinesTextSource(IReadOnlyList 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;
+ }
+ }
+ }
+}
diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs
index 532cb1d329..c344dd795d 100644
--- a/src/Avalonia.Controls/SplitView.cs
+++ b/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)
{
- var oldState = e.OldValue!.ToString()!.ToLower();
- var newState = e.NewValue!.ToString()!.ToLower();
+ var oldState = GetPseudoClass(e.GetOldValue());
+ var newState = GetPseudoClass(e.GetNewValue());
PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}");
}
private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs e)
{
- var oldState = e.OldValue!.ToString()!.ToLower();
- var newState = e.NewValue!.ToString()!.ToLower();
+ var oldState = GetPseudoClass(e.GetOldValue());
+ var newState = GetPseudoClass(e.GetNewValue());
PseudoClasses.Remove($":{oldState}");
PseudoClasses.Add($":{newState}");
- var (closedPaneWidth, paneColumnGridLength) = (SplitViewDisplayMode)e.NewValue switch
+ var (closedPaneWidth, paneColumnGridLength) = e.GetNewValue() switch
{
SplitViewDisplayMode.Overlay => (0, new GridLength(0, GridUnitType.Pixel)),
SplitViewDisplayMode.CompactOverlay => (CompactPaneLength, new GridLength(CompactPaneLength, GridUnitType.Pixel)),
diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs
index db315d3aaf..52261d1c76 100644
--- a/src/Avalonia.Controls/TextBlock.cs
+++ b/src/Avalonia.Controls/TextBlock.cs
@@ -1,12 +1,9 @@
using System;
-using System.Collections.Generic;
-using System.Text;
using Avalonia.Automation.Peers;
using Avalonia.Controls.Documents;
using Avalonia.Layout;
using Avalonia.Media;
using Avalonia.Media.TextFormatting;
-using Avalonia.Metadata;
using Avalonia.Utilities;
namespace Avalonia.Controls
@@ -14,7 +11,7 @@ namespace Avalonia.Controls
///
/// A control that displays a block of text.
///
- public class TextBlock : Control, IInlineHost
+ public class TextBlock : Control
{
///
/// Defines the property.
@@ -101,14 +98,6 @@ namespace Avalonia.Controls
o => o.Text,
(o, v) => o.Text = v);
- ///
- /// Defines the property.
- ///
- public static readonly DirectProperty InlinesProperty =
- AvaloniaProperty.RegisterDirect(
- nameof(Inlines),
- o => o.Inlines);
-
///
/// Defines the property.
///
@@ -139,8 +128,9 @@ namespace Avalonia.Controls
public static readonly StyledProperty TextDecorationsProperty =
AvaloniaProperty.Register(nameof(TextDecorations));
- private TextLayout? _textLayout;
- private Size _constraint;
+ protected string? _text;
+ protected TextLayout? _textLayout;
+ protected Size _constraint;
///
/// Initializes static members of the class.
@@ -152,14 +142,6 @@ namespace Avalonia.Controls
AffectsRender(BackgroundProperty, ForegroundProperty);
}
- ///
- /// Initializes a new instance of the class.
- ///
- public TextBlock()
- {
- Inlines = new InlineCollection(this, this);
- }
-
///
/// Gets the used to render the text.
///
@@ -167,7 +149,7 @@ namespace Avalonia.Controls
{
get
{
- return _textLayout ?? (_textLayout = CreateTextLayout(_constraint, Text));
+ return _textLayout ??= CreateTextLayout(_text);
}
}
@@ -194,28 +176,10 @@ namespace Avalonia.Controls
///
public string? Text
{
- get => Inlines.Text;
- set
- {
- var old = Text;
-
- if (value == old)
- {
- return;
- }
-
- Inlines.Text = value;
-
- RaisePropertyChanged(TextProperty, old, value);
- }
+ get => GetText();
+ set => SetText(value);
}
- ///
- /// Gets the inlines.
- ///
- [Content]
- public InlineCollection Inlines { get; }
-
///
/// Gets or sets the font family used to draw the control's text.
///
@@ -509,6 +473,10 @@ namespace Avalonia.Controls
control.SetValue(MaxLinesProperty, maxLines);
}
+ public void Add(string text)
+ {
+ _text = text;
+ }
///
/// Renders the to a drawing context.
@@ -544,13 +512,21 @@ namespace Avalonia.Controls
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);
+ }
+
///
/// Creates the used to render the text.
///
- /// The constraint of the text.
- /// The text to format.
/// A object.
- protected virtual TextLayout CreateTextLayout(Size constraint, string? text)
+ protected virtual TextLayout CreateTextLayout(string? text)
{
var defaultProperties = new GenericTextRunProperties(
new Typeface(FontFamily, FontStyle, FontWeight, FontStretch),
@@ -561,30 +537,12 @@ namespace Avalonia.Controls
var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false,
defaultProperties, TextWrapping, LineHeight, 0);
- ITextSource textSource;
-
- if (Inlines.HasComplexContent)
- {
- var textRuns = new List();
-
- foreach (var inline in Inlines)
- {
- inline.BuildTextRun(textRuns);
- }
-
- textSource = new InlinesTextSource(textRuns);
- }
- else
- {
- textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties);
- }
-
return new TextLayout(
- textSource,
+ new SimpleTextSource((text ?? "").AsMemory(), defaultProperties),
paragraphProperties,
TextTrimming,
- constraint.Width,
- constraint.Height,
+ _constraint.Width,
+ _constraint.Height,
maxLines: MaxLines,
lineHeight: LineHeight);
}
@@ -601,11 +559,6 @@ namespace Avalonia.Controls
protected override Size MeasureOverride(Size availableSize)
{
- if (!Inlines.HasComplexContent && string.IsNullOrEmpty(Text))
- {
- return new Size();
- }
-
var scale = LayoutHelper.GetLayoutScale(this);
var padding = LayoutHelper.RoundLayoutThickness(Padding, scale, scale);
@@ -623,9 +576,11 @@ namespace Avalonia.Controls
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))
@@ -637,7 +592,7 @@ namespace Avalonia.Controls
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;
@@ -685,57 +640,7 @@ namespace Avalonia.Controls
}
}
- private void InlinesChanged(object? sender, EventArgs 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 _textRuns;
-
- public InlinesTextSource(IReadOnlyList 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
+ protected readonly struct SimpleTextSource : ITextSource
{
private readonly ReadOnlySlice _text;
private readonly TextRunProperties _defaultProperties;
diff --git a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml
index b7995c38e3..a1a2ab34bf 100644
--- a/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml
+++ b/src/Avalonia.Diagnostics/Diagnostics/Controls/FilterTextBox.axaml
@@ -19,7 +19,7 @@
Classes="textBoxClearButton"
ToolTip.Tip="Clear"
Cursor="Hand"
- Command="{ReflectionBinding $parent[TextBox].Clear}"
+ Command="{Binding $parent[TextBox].Clear}"
Opacity="0.5" />
diff --git a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml
index a463334a76..482b219589 100644
--- a/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml
+++ b/src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml
@@ -21,7 +21,7 @@
-
+
@@ -32,7 +32,7 @@
-
+
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
index 04a61e5f10..e3a55feac9 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
@@ -45,10 +45,10 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
new AvaloniaXamlIlReorderClassesPropertiesTransformer()
);
- InsertBefore(
- new AvaloniaXamlIlBindingPathParser(),
+ InsertBefore(
new AvaloniaXamlIlSelectorTransformer(),
- new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(),
+ new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(),
+ new AvaloniaXamlIlBindingPathParser(),
new AvaloniaXamlIlPropertyPathTransformer(),
new AvaloniaXamlIlSetterTransformer(),
new AvaloniaXamlIlConstructorServiceProviderTransformer(),
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs
index 890cbb69bf..6150265cf8 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs
+++ b/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);
}
- var mode = relativeSourceObject.Children
+ var modeProperty = relativeSourceObject.Children
.OfType()
- .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Mode")
- ?.Values[0] is XamlAstTextNode modeAssignedValue ? modeAssignedValue.Text : null;
+ .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Mode")?
+ .Values.FirstOrDefault() as XamlAstTextNode
+ ?? relativeSourceObject.Arguments.OfType().FirstOrDefault();
+
+ var mode = modeProperty?.Text;
if (relativeSourceObject.Arguments.Count == 0 && mode == null)
{
mode = "FindAncestor";
@@ -212,16 +215,20 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
}
else if (mode == "TemplatedParent")
{
- var parentType = context.ParentNodes().OfType()
+ var contentTemplateNode = context.ParentNodes().OfType()
.FirstOrDefault(x =>
- x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate)
- ?.TargetType.GetClrType();
-
- if (parentType is null)
+ x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate);
+ if (contentTemplateNode is null)
{
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 };
}
else
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs
index 3cc3504e16..e509d47932 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs
+++ b/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;
}
+
+ if (dataTypeProperty?.Values.Count is 1 && dataTypeProperty.Values[0] is XamlTypeExtensionNode typeNode)
+ {
+ startType = typeNode.Value.GetClrType();
+ }
Func startTypeResolver = startType is not null ? () => startType : () =>
{
diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs
index c8de8f00f6..ae29dcf9cb 100644
--- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs
+++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs
@@ -47,7 +47,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
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(
context,
@@ -63,7 +67,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
}
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:
var templatedParentField = context.GetAvaloniaTypes().StyledElement.GetAllFields()
.FirstOrDefault(f => f.IsStatic && f.IsPublic && f.Name == "TemplatedParentProperty");
+ nodes.Add(new SelfPathElementNode(selfType));
nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(
templatedParentField,
templatedParent.Type));
@@ -374,6 +379,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
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);
namescopeRoot.Visit(finder);
return finder.TargetType;
@@ -399,6 +410,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
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 (registration.Name is XamlAstTextNode text && text.Text == Name)
diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs
index 73a14fd437..f78d4867ff 100644
--- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs
+++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs
@@ -74,6 +74,8 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings
return pathRoot ?? new EmptyExpressionNode();
}
+ internal IEnumerable Elements => _elements;
+
internal SourceMode SourceMode => _elements.Count > 0 && _elements[0] is IControlSourceBindingPathElement ? SourceMode.Control : SourceMode.Data;
internal object RawSource { get; }
diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs
index 82f1fa7a3e..259f5c996b 100644
--- a/src/Windows/Avalonia.Win32/WindowImpl.cs
+++ b/src/Windows/Avalonia.Win32/WindowImpl.cs
@@ -870,7 +870,14 @@ namespace Avalonia.Win32
if (fullscreen)
{
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 currentEx = GetExtendedStyle();
@@ -907,10 +914,10 @@ namespace Avalonia.Win32
SetExtendedStyle(_savedWindowInfo.ExStyle, false);
// 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,
- new_rect.Height,
+ SetWindowPos(_hwnd, IntPtr.Zero, newClientRect.X, newClientRect.Y, newClientRect.Width,
+ newClientRect.Height,
SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED);
UpdateWindowProperties(_windowProperties, true);
diff --git a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs b/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs
index 8b4c988037..176fa07f19 100644
--- a/tests/Avalonia.Base.UnitTests/Styling/SelectorTests_Template.cs
+++ b/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 selector = default(Selector).OfType(templatedControl.Object.GetType()).Class("foo").Template().OfType();
var activator = selector.Match(border).Activator;
- var inccDebug = (INotifyCollectionChangedDebug)styleable.Object.Classes;
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(Mock templatedControl) where T : class, IVisual
diff --git a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs b/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs
index c684466200..b57a024f41 100644
--- a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs
+++ b/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs
@@ -49,7 +49,7 @@ namespace Avalonia.Base.UnitTests.Styling
setter.Instance(control).Start(false);
- Assert.Equal("", control.Text);
+ Assert.Equal(null, control.Text);
}
[Fact]
diff --git a/tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs b/tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs
new file mode 100644
index 0000000000..f242e95966
--- /dev/null
+++ b/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().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 StringProperty =
+ AvaloniaProperty.Register("String");
+ public void DetachStyles() => InvalidateStyles();
+ }
+
+ private class TestClass2 : Control
+ {
+ }
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs
new file mode 100644
index 0000000000..eb4b88956d
--- /dev/null
+++ b/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);
+ }
+ }
+ }
+}
diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
index 0ed1f8d2d0..37dde9fbac 100644
--- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
+++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
@@ -20,13 +20,11 @@ namespace Avalonia.Controls.UnitTests
}
[Fact]
- public void Default_Text_Value_Should_Be_EmptyString()
+ public void Default_Text_Value_Should_Be_Null()
{
var textBlock = new TextBlock();
- Assert.Equal(
- "",
- textBlock.Text);
+ Assert.Equal(null, textBlock.Text);
}
[Fact]
@@ -62,47 +60,5 @@ namespace Avalonia.Controls.UnitTests
renderer.Verify(x => x.AddDirty(target), Times.Once);
}
-
- [Fact]
- public void Changing_InlinesCollection_Should_Invalidate_Measure()
- {
- using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
- {
- var target = new TextBlock();
-
- target.Measure(Size.Infinity);
-
- Assert.True(target.IsMeasureValid);
-
- target.Inlines.Add(new Run("Hello"));
-
- Assert.False(target.IsMeasureValid);
-
- target.Measure(Size.Infinity);
-
- Assert.True(target.IsMeasureValid);
- }
- }
-
- [Fact]
- public void Changing_Inlines_Properties_Should_Invalidate_Measure()
- {
- using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
- {
- var target = new TextBlock();
-
- var inline = new Run("Hello");
-
- target.Inlines.Add(inline);
-
- target.Measure(Size.Infinity);
-
- Assert.True(target.IsMeasureValid);
-
- inline.Text = "1337";
-
- Assert.False(target.IsMeasureValid);
- }
- }
}
}
diff --git a/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh
index 30a4a79f4a..635c593f37 100755
--- a/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh
+++ b/tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh
@@ -5,4 +5,5 @@ cd "$SCRIPT_DIR"/../.. || exit
git clean -xdf
./build.sh CompileNative
./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"
diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs
index 6fb7b1448c..a7474dc92e 100644
--- a/tests/Avalonia.LeakTests/ControlTests.cs
+++ b/tests/Avalonia.LeakTests/ControlTests.cs
@@ -314,7 +314,7 @@ namespace Avalonia.LeakTests
// The TextBox should have subscriptions to its Classes collection from the
// default theme.
- Assert.NotEmpty(((INotifyCollectionChangedDebug)textBox.Classes).GetCollectionChangedSubscribers());
+ Assert.NotEqual(0, textBox.Classes.ListenerCount);
// Clear the content and ensure the TextBox is removed.
window.Content = null;
diff --git a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
index 11a22f0dec..f4a4a2934f 100644
--- a/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
+++ b/tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs
@@ -637,6 +637,17 @@ namespace Avalonia.Markup.UnitTests.Data
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
{
public static readonly StyledProperty DoubleValueProperty =
diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs
index f3f2d2f1e4..215ae4d54f 100644
--- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs
+++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs
@@ -3,22 +3,23 @@ using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Globalization;
+using System.Linq;
using System.Reactive.Subjects;
-using System.Text;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Templates;
+using Avalonia.Data;
using Avalonia.Data.Converters;
using Avalonia.Data.Core;
using Avalonia.Input;
using Avalonia.Markup.Data;
+using Avalonia.Markup.Xaml.MarkupExtensions;
+using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings;
using Avalonia.Markup.Xaml.Templates;
using Avalonia.Media;
using Avalonia.Metadata;
using Avalonia.UnitTests;
-using JetBrains.Annotations;
-using XamlX;
using Xunit;
namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
@@ -102,6 +103,91 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
Assert.Equal(dataContext.StringProperty, textBlock.Text);
}
}
+
+ [Fact]
+ public void ResolvesPathPassedByPropertyWithInnerItemTemplate()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+
+
+
+
+
+
+";
+ var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+ var textBlock = window.FindControl("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 = @"
+
+
+";
+ var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+ var textBlock = window.FindControl("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 = @"
+
+
+";
+ var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+ var textBlock = window.FindControl("textBlock");
+
+ var dataContext = new TestDataContext
+ {
+ StringProperty = "foobar"
+ };
+
+ window.DataContext = dataContext;
+
+ Assert.Equal(dataContext.StringProperty, textBlock.Text);
+ }
+ }
[Fact]
public void ResolvesStreamTaskBindingCorrectly()
@@ -641,6 +727,64 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
}
}
+ [Fact]
+ public void ResolvesRelativeSourceBindingFromTemplate()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+
+
+";
+
+ var contentControl = AvaloniaRuntimeXamlLoader.Parse(xaml);
+ contentControl.Measure(new Size(10, 10));
+
+ var result = contentControl.GetTemplateChildren().OfType().First();
+ Assert.Equal("Hello", result.Content);
+ }
+ }
+
+ [Fact]
+ public void ResolvesElementNameInTemplate()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+
+
+";
+
+ var contentControl = AvaloniaRuntimeXamlLoader.Parse(xaml);
+ contentControl.Measure(new Size(10, 10));
+
+ var result = contentControl.GetTemplateChildren().OfType().First();
+
+ Assert.Equal("Hello", result.Content);
+ }
+ }
+
[Fact]
public void Binds_To_Source()
{
@@ -1145,6 +1289,28 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
Assert.Equal("bar-" + typeof(TestDataContext).FullName, textBlock.Text);
}
}
+
+ [Fact]
+ public void SupportCastToTypeInExpressionWithProperty_ExplicitPropertyCast()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+
+
+";
+ var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+ var contentControl = window.GetControl("contentControl");
+
+ var dataContext = new TestDataContext();
+
+ window.DataContext = dataContext;
+
+ Assert.Equal(((IHasExplicitProperty)dataContext).ExplicitProperty, contentControl.Content);
+ }
+ }
[Fact]
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 = @"
+";
+ var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(xaml);
+ var compiledPath = ((CompiledBindingExtension)control.X).Path;
+
+ var node = Assert.IsType(Assert.Single(compiledPath.Elements));
+ Assert.Equal(typeof(string), node.Property.PropertyType);
+ }
+ }
+
+ [Fact]
+ public void ResolvesDataTypeForAssignBinding_FromBindingProperty()
+ {
+ using (UnitTestApplication.Start(TestServices.StyledWindow))
+ {
+ var xaml = @"
+";
+ var control = (AssignBindingControl)AvaloniaRuntimeXamlLoader.Load(xaml);
+ var compiledPath = ((CompiledBindingExtension)control.X).Path;
+
+ var node = Assert.IsType(Assert.Single(compiledPath.Elements));
+ Assert.Equal(typeof(string), node.Property.PropertyType);
+ }
+ }
+
void Throws(string type, Action cb)
{
try
@@ -1410,6 +1613,11 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
public interface IHasPropertyDerived : IHasProperty
{ }
+ public interface IHasExplicitProperty
+ {
+ string ExplicitProperty { get; }
+ }
+
public class AppendConverter : IValueConverter
{
public static IValueConverter Instance { get; } = new AppendConverter();
@@ -1429,7 +1637,7 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
public class TestDataContextBaseClass {}
- public class TestDataContext : TestDataContextBaseClass, IHasPropertyDerived
+ public class TestDataContext : TestDataContextBaseClass, IHasPropertyDerived, IHasExplicitProperty
{
public string StringProperty { get; set; }
@@ -1449,6 +1657,10 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
public INonIntegerIndexerDerived NonIntegerIndexerInterfaceProperty => NonIntegerIndexerProperty;
+ string IHasExplicitProperty.ExplicitProperty => "Hello";
+
+ public string ExplicitProperty => "Bye";
+
public class NonIntegerIndexer : NotifyingBase, INonIntegerIndexerDerived
{
private readonly Dictionary _storage = new Dictionary();
@@ -1534,4 +1746,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions
}
public class CustomDataTemplateInherit : CustomDataTemplate { }
+
+ public class AssignBindingControl : Control
+ {
+ [AssignBinding] public IBinding X { get; set; }
+ }
}