From 0c8f54fe009761088f91bb394e136e4d9bb113d6 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 21 Jun 2022 15:34:05 +0200 Subject: [PATCH 01/38] Introduce RichTextBlock --- .../ControlCatalog/Pages/TextBlockPage.xaml | 4 +- .../Documents/InlineCollection.cs | 72 +++++-- src/Avalonia.Controls/Documents/Span.cs | 84 ++++++-- .../Documents/TextElement.cs | 18 +- src/Avalonia.Controls/RichTextBlock.cs | 199 ++++++++++++++++++ src/Avalonia.Controls/TextBlock.cs | 123 ++--------- 6 files changed, 353 insertions(+), 147 deletions(-) create mode 100644 src/Avalonia.Controls/RichTextBlock.cs 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.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index a76222385e..0cbf272297 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; /// @@ -57,14 +73,16 @@ namespace Avalonia.Controls.Documents { get { + return _text; + if (!HasComplexContent) { return _text; } - + var builder = new StringBuilder(); - foreach(var inline in this) + foreach (var inline in this) { inline.AppendText(builder); } @@ -100,7 +118,7 @@ namespace Avalonia.Controls.Documents } else { - _text += text; + _text = text; } } @@ -136,14 +154,30 @@ 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..c2576ec231 100644 --- a/src/Avalonia.Controls/Documents/Span.cs +++ b/src/Avalonia.Controls/Documents/Span.cs @@ -2,7 +2,6 @@ using System; using System.Collections.Generic; using System.Text; using Avalonia.Media.TextFormatting; -using Avalonia.Metadata; namespace Avalonia.Controls.Documents { @@ -14,25 +13,42 @@ 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); + } + + public void Add(Inline inline) + { + if (Inlines is not null) + { + Inlines.Add(inline); + } + } + + public void Add(string text) + { + if (Inlines is not null) + { + Inlines.Add(text); + } + } internal override void BuildTextRun(IList textRuns) { @@ -52,7 +68,7 @@ namespace Avalonia.Controls.Documents var textCharacters = new TextCharacters(text.AsMemory(), textRunProperties); textRuns.Add(textCharacters); - } + } } } @@ -71,5 +87,45 @@ namespace Avalonia.Controls.Documents 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 OnInlinesHostChanged(IInlineHost? oldValue, IInlineHost? newValue) + { + base.OnInlinesHostChanged(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 (newValue is not null) + { + 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..e75fd87615 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; + OnInlinesHostChanged(oldValue, value); + } + } + + internal virtual void OnInlinesHostChanged(IInlineHost? oldValue, IInlineHost? newValue) + { + + } protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs new file mode 100644 index 0000000000..16d0254f4a --- /dev/null +++ b/src/Avalonia.Controls/RichTextBlock.cs @@ -0,0 +1,199 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls.Documents; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; + +namespace Avalonia.Controls +{ + /// + /// A control that displays a block of text. + /// + public class RichTextBlock : TextBlock, IInlineHost + { + /// + /// Defines the property. + /// + public static readonly StyledProperty InlinesProperty = + AvaloniaProperty.Register( + nameof(Inlines)); + + public RichTextBlock() + { + Inlines = new InlineCollection + { + Parent = this, + InlineHost = this + }; + } + + /// + /// Gets or sets the inlines. + /// + public InlineCollection Inlines + { + get => GetValue(InlinesProperty); + set => SetValue(InlinesProperty, value); + } + + public void Add(Inline inline) + { + if (Inlines is not null) + { + Inlines.Add(inline); + } + } + + public new void Add(string text) + { + if (Inlines is not null) + { + Inlines.Add(text); + } + } + + /// + /// Creates the used to render the text. + /// + /// The constraint of the text. + /// The text to format. + /// A object. + protected override TextLayout CreateTextLayout(Size constraint, 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 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): + { + OnTextChanged(change.OldValue as string, change.NewValue as string); + break; + } + } + } + + private void OnTextChanged(string? oldValue, string? newValue) + { + if (oldValue == newValue) + { + return; + } + + if (Inlines is null) + { + return; + } + + Inlines.Text = newValue; + } + + 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/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 1a69d1218c..87966e9a6f 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -1,6 +1,5 @@ using System; using System.Collections.Generic; -using System.Text; using Avalonia.Automation.Peers; using Avalonia.Controls.Documents; using Avalonia.Layout; @@ -14,7 +13,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 +100,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. /// @@ -137,6 +128,7 @@ namespace Avalonia.Controls public static readonly StyledProperty TextDecorationsProperty = AvaloniaProperty.Register(nameof(TextDecorations)); + private string? _text; private TextLayout? _textLayout; private Size _constraint; @@ -150,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. /// @@ -165,7 +149,7 @@ namespace Avalonia.Controls { get { - return _textLayout ?? (_textLayout = CreateTextLayout(_constraint, Text)); + return _textLayout ??= CreateTextLayout(_constraint, Text); } } @@ -192,28 +176,13 @@ namespace Avalonia.Controls /// public string? Text { - get => Inlines.Text; + get => _text; set { - var old = Text; - - if (value == old) - { - return; - } - - Inlines.Text = value; - - RaisePropertyChanged(TextProperty, old, value); + SetAndRaise(TextProperty, ref _text, value); } } - /// - /// Gets the inlines. - /// - [Content] - public InlineCollection Inlines { get; } - /// /// Gets or sets the font family used to draw the control's text. /// @@ -333,6 +302,11 @@ namespace Avalonia.Controls set { SetValue(BaselineOffsetProperty, value); } } + public void Add(string text) + { + Text = text; + } + /// /// Reads the attached property from the given element /// @@ -559,26 +533,8 @@ 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, @@ -599,11 +555,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); @@ -683,57 +634,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; From a408ea10d79ac357d968a1ff96a3664506a6058b Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 22 Jun 2022 16:45:38 +0200 Subject: [PATCH 02/38] Some Progress --- samples/Sandbox/MainWindow.axaml | 14 + .../Media/TextFormatting/TextFormatterImpl.cs | 2 +- .../TextFormatting/Unicode/BiDiAlgorithm.cs | 6 - .../Documents/InlineCollection.cs | 2 - src/Avalonia.Controls/Documents/Span.cs | 20 +- .../Presenters/TextPresenter.cs | 8 +- src/Avalonia.Controls/RichTextBlock.cs | 326 +++++++++++++++++- src/Avalonia.Controls/TextBlock.cs | 12 +- .../TextFormatting/BiDiAlgorithmTests.cs | 2 +- .../RichTextBlockTests.cs | 52 +++ .../TextBlockTests.cs | 42 --- 11 files changed, 397 insertions(+), 89 deletions(-) create mode 100644 tests/Avalonia.Controls.UnitTests/RichTextBlockTests.cs diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index 6929f192c7..806f6d37da 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -1,4 +1,18 @@ + + + + This is a + TextBlock + with several + Span elements, + + using a variety of styles + . + + + + diff --git a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs index 4205268bc6..cd764be43f 100644 --- a/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs +++ b/src/Avalonia.Base/Media/TextFormatting/TextFormatterImpl.cs @@ -177,7 +177,7 @@ namespace Avalonia.Media.TextFormatting } - var biDi = BidiAlgorithm.Instance.Value!; + var biDi = new BidiAlgorithm(); biDi.Process(biDiData); diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 2511807d9c..3c510ff484 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -188,12 +188,6 @@ namespace Avalonia.Media.TextFormatting.Unicode { } - /// - /// Gets a per-thread instance that can be re-used as often - /// as necessary. - /// - public static ThreadLocal Instance { get; } = new ThreadLocal(() => new BidiAlgorithm()); - /// /// Gets the resolved levels. /// diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index 0cbf272297..2f27ca72d0 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -73,8 +73,6 @@ namespace Avalonia.Controls.Documents { get { - return _text; - if (!HasComplexContent) { return _text; diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs index c2576ec231..98851726da 100644 --- a/src/Avalonia.Controls/Documents/Span.cs +++ b/src/Avalonia.Controls/Documents/Span.cs @@ -2,6 +2,7 @@ using System; using System.Collections.Generic; using System.Text; using Avalonia.Media.TextFormatting; +using Avalonia.Metadata; namespace Avalonia.Controls.Documents { @@ -27,29 +28,14 @@ namespace Avalonia.Controls.Documents /// /// Gets or sets the inlines. - /// + /// + [Content] public InlineCollection Inlines { get => GetValue(InlinesProperty); set => SetValue(InlinesProperty, value); } - public void Add(Inline inline) - { - if (Inlines is not null) - { - Inlines.Add(inline); - } - } - - public void Add(string text) - { - if (Inlines is not null) - { - Inlines.Add(text); - } - } - internal override void BuildTextRun(IList textRuns) { if (Inlines.HasComplexContent) 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/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs index 16d0254f4a..859503e693 100644 --- a/src/Avalonia.Controls/RichTextBlock.cs +++ b/src/Avalonia.Controls/RichTextBlock.cs @@ -1,8 +1,14 @@ using System; using System.Collections.Generic; +using System.Diagnostics; using Avalonia.Controls.Documents; +using Avalonia.Controls.Utils; +using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Media; using Avalonia.Media.TextFormatting; +using Avalonia.Metadata; +using Avalonia.Utilities; namespace Avalonia.Controls { @@ -11,6 +17,38 @@ namespace Avalonia.Controls /// public class RichTextBlock : TextBlock, IInlineHost { + public static readonly StyledProperty IsTextSelectionEnabledProperty = + AvaloniaProperty.Register(nameof(IsTextSelectionEnabled), false); + + public static readonly DirectProperty CaretIndexProperty = + AvaloniaProperty.RegisterDirect( + nameof(CaretIndex), + o => o.CaretIndex, + (o, v) => o.CaretIndex = v); + + 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. /// @@ -18,6 +56,17 @@ namespace Avalonia.Controls AvaloniaProperty.Register( nameof(Inlines)); + private int _caretIndex; + private int _selectionStart; + private int _selectionEnd; + + static RichTextBlock() + { + FocusableProperty.OverrideDefaultValue(typeof(RichTextBlock), true); + + AffectsRender(SelectionStartProperty, SelectionEndProperty, SelectionForegroundBrushProperty, SelectionBrushProperty); + } + public RichTextBlock() { Inlines = new InlineCollection @@ -27,31 +76,85 @@ namespace Avalonia.Controls }; } - /// - /// Gets or sets the inlines. - /// - public InlineCollection Inlines + public IBrush? SelectionBrush { - get => GetValue(InlinesProperty); - set => SetValue(InlinesProperty, value); + get => GetValue(SelectionBrushProperty); + set => SetValue(SelectionBrushProperty, value); } - public void Add(Inline inline) + public IBrush? SelectionForegroundBrush { - if (Inlines is not null) + get => GetValue(SelectionForegroundBrushProperty); + set => SetValue(SelectionForegroundBrushProperty, value); + } + + public int CaretIndex + { + get => _caretIndex; + set { - Inlines.Add(inline); + if(SetAndRaise(CaretIndexProperty, ref _caretIndex, value)) + { + SelectionStart = SelectionEnd = value; + } } } - public new void Add(string text) + public int SelectionStart { - if (Inlines is not null) + get => _selectionStart; + set { - Inlines.Add(text); + if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value)) + { + RaisePropertyChanged(SelectedTextProperty, "", ""); + + if (SelectionEnd == value && CaretIndex != value) + { + CaretIndex = value; + } + } } } + public int SelectionEnd + { + get => _selectionEnd; + set + { + if(SetAndRaise(SelectionEndProperty, ref _selectionEnd, value)) + { + RaisePropertyChanged(SelectedTextProperty, "", ""); + + if (SelectionStart == value && CaretIndex != value) + { + CaretIndex = value; + } + } + } + } + + public string SelectedText + { + get => GetSelection(); + } + + 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); + } + /// /// Creates the used to render the text. /// @@ -99,6 +202,179 @@ namespace Avalonia.Controls lineHeight: LineHeight); } + 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 = Inlines.Text ?? Text; + + SelectionStart = 0; + SelectionEnd = text?.Length ?? 0; + } + + /// + /// Clears the current selection/> + /// + public void ClearSelection() + { + if (!IsTextSelectionEnabled) + { + return; + } + + SelectionEnd = SelectionStart; + } + + protected override void OnLostFocus(RoutedEventArgs e) + { + base.OnLostFocus(e); + + ClearSelection(); + } + + protected override void OnPointerPressed(PointerPressedEventArgs e) + { + if (!IsTextSelectionEnabled) + { + return; + } + + var text = Inlines.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 hit = TextLayout.HitTestPoint(point); + + var oldIndex = CaretIndex; + var index = hit.TextPosition; + CaretIndex = 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) + { + _caretIndex = SelectionEnd = SelectionStart = caretIndex; + } + } + + e.Pointer.Capture(null); + } + protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { base.OnPropertyChanged(change); @@ -134,6 +410,32 @@ namespace Avalonia.Controls Inlines.Text = newValue; } + private string GetSelection() + { + 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) diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 87966e9a6f..1f891b092f 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -128,8 +128,8 @@ namespace Avalonia.Controls public static readonly StyledProperty TextDecorationsProperty = AvaloniaProperty.Register(nameof(TextDecorations)); - private string? _text; - private TextLayout? _textLayout; + protected string? _text; + protected TextLayout? _textLayout; private Size _constraint; /// @@ -572,9 +572,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)) @@ -586,7 +588,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; diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiAlgorithmTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiAlgorithmTests.cs index f8a2abc716..5ff2c0e07b 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiAlgorithmTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiAlgorithmTests.cs @@ -27,7 +27,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting private bool Run(BiDiTestData testData) { - var bidi = BidiAlgorithm.Instance.Value; + var bidi = new BidiAlgorithm(); // Run the algorithm... ArraySlice resultLevels; 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..6da011f062 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs @@ -62,47 +62,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); - } - } } } From 90e0dcc9e3161cb9659ca7381c6ff98ee7e84f10 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Thu, 23 Jun 2022 15:18:36 +0200 Subject: [PATCH 03/38] Add Copy geasture --- samples/Sandbox/MainWindow.axaml | 1 + .../Documents/InlineCollection.cs | 4 +- src/Avalonia.Controls/Documents/Span.cs | 12 +- .../Documents/TextElement.cs | 4 +- .../Primitives/AccessText.cs | 4 +- src/Avalonia.Controls/RichTextBlock.cs | 194 +++++++++++++----- src/Avalonia.Controls/TextBlock.cs | 38 ++-- .../Media/TextFormatting/BiDiClassTests.cs | 2 +- .../Styling/SetterTests.cs | 2 +- 9 files changed, 174 insertions(+), 87 deletions(-) diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index 806f6d37da..a834e3fef3 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -13,6 +13,7 @@ . + diff --git a/src/Avalonia.Controls/Documents/InlineCollection.cs b/src/Avalonia.Controls/Documents/InlineCollection.cs index 2f27ca72d0..dc688fc359 100644 --- a/src/Avalonia.Controls/Documents/InlineCollection.cs +++ b/src/Avalonia.Controls/Documents/InlineCollection.cs @@ -136,7 +136,7 @@ namespace Avalonia.Controls.Documents base.Add(new Run(_text)); } - _text = string.Empty; + _text = null; } base.Add(item); @@ -160,8 +160,6 @@ namespace Avalonia.Controls.Documents Invalidated?.Invoke(this, EventArgs.Empty); } - private void Invalidate(object? sender, EventArgs e) => Invalidate(); - private void OnParentChanged(ILogical? parent) { foreach(var child in this) diff --git a/src/Avalonia.Controls/Documents/Span.cs b/src/Avalonia.Controls/Documents/Span.cs index 98851726da..c7289dbc3f 100644 --- a/src/Avalonia.Controls/Documents/Span.cs +++ b/src/Avalonia.Controls/Documents/Span.cs @@ -67,10 +67,12 @@ namespace Avalonia.Controls.Documents inline.AppendText(stringBuilder); } } - - if (Inlines.Text is string text) + else { - stringBuilder.Append(text); + if (Inlines.Text is string text) + { + stringBuilder.Append(text); + } } } @@ -87,9 +89,9 @@ namespace Avalonia.Controls.Documents } } - internal override void OnInlinesHostChanged(IInlineHost? oldValue, IInlineHost? newValue) + internal override void OnInlineHostChanged(IInlineHost? oldValue, IInlineHost? newValue) { - base.OnInlinesHostChanged(oldValue, newValue); + base.OnInlineHostChanged(oldValue, newValue); if(Inlines is not null) { diff --git a/src/Avalonia.Controls/Documents/TextElement.cs b/src/Avalonia.Controls/Documents/TextElement.cs index e75fd87615..5bac3642ed 100644 --- a/src/Avalonia.Controls/Documents/TextElement.cs +++ b/src/Avalonia.Controls/Documents/TextElement.cs @@ -259,11 +259,11 @@ namespace Avalonia.Controls.Documents { var oldValue = _inlineHost; _inlineHost = value; - OnInlinesHostChanged(oldValue, value); + OnInlineHostChanged(oldValue, value); } } - internal virtual void OnInlinesHostChanged(IInlineHost? oldValue, IInlineHost? newValue) + internal virtual void OnInlineHostChanged(IInlineHost? oldValue, IInlineHost? newValue) { } 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/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs index 859503e693..1411d715ec 100644 --- a/src/Avalonia.Controls/RichTextBlock.cs +++ b/src/Avalonia.Controls/RichTextBlock.cs @@ -1,9 +1,10 @@ using System; using System.Collections.Generic; -using System.Diagnostics; +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; @@ -56,6 +57,16 @@ namespace Avalonia.Controls 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 _caretIndex; private int _selectionStart; private int _selectionEnd; @@ -75,7 +86,7 @@ namespace Avalonia.Controls InlineHost = this }; } - + public IBrush? SelectionBrush { get => GetValue(SelectionBrushProperty); @@ -156,50 +167,43 @@ namespace Avalonia.Controls } /// - /// Creates the used to render the text. + /// Property for determining if the Copy command can be executed. /// - /// The constraint of the text. - /// The text to format. - /// A object. - protected override TextLayout CreateTextLayout(Size constraint, string? text) + public bool CanCopy { - var defaultProperties = new GenericTextRunProperties( - new Typeface(FontFamily, FontStyle, FontWeight, FontStretch), - FontSize, - TextDecorations, - Foreground); + get => _canCopy; + private set => SetAndRaise(CanCopyProperty, ref _canCopy, value); + } - var paragraphProperties = new GenericTextParagraphProperties(FlowDirection, TextAlignment, true, false, - defaultProperties, TextWrapping, LineHeight, 0); + public event EventHandler? CopyingToClipboard + { + add => AddHandler(CopyingToClipboardEvent, value); + remove => RemoveHandler(CopyingToClipboardEvent, value); + } - ITextSource textSource; + public async void Copy() + { + if (_canCopy || !IsTextSelectionEnabled) + { + return; + } - var inlines = Inlines; + var text = GetSelection(); - if (inlines is not null && inlines.HasComplexContent) + if (string.IsNullOrEmpty(text)) { - var textRuns = new List(); + return; + } - foreach (var inline in inlines) - { - inline.BuildTextRun(textRuns); - } + var eventArgs = new RoutedEventArgs(CopyingToClipboardEvent); - textSource = new InlinesTextSource(textRuns); - } - else + RaiseEvent(eventArgs); + + if (!eventArgs.Handled) { - textSource = new SimpleTextSource((text ?? "").AsMemory(), defaultProperties); + await ((IClipboard)AvaloniaLocator.Current.GetRequiredService(typeof(IClipboard))) + .SetTextAsync(text); } - - return new TextLayout( - textSource, - paragraphProperties, - TextTrimming, - constraint.Width, - constraint.Height, - maxLines: MaxLines, - lineHeight: LineHeight); } public override void Render(DrawingContext context) @@ -236,7 +240,7 @@ namespace Avalonia.Controls return; } - var text = Inlines.Text ?? Text; + var text = Text; SelectionStart = 0; SelectionEnd = text?.Length ?? 0; @@ -255,6 +259,75 @@ namespace Avalonia.Controls 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); @@ -262,6 +335,24 @@ namespace Avalonia.Controls 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) @@ -269,20 +360,21 @@ namespace Avalonia.Controls return; } - var text = Inlines.Text; + 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 hit = TextLayout.HitTestPoint(point); + var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift); var oldIndex = CaretIndex; + + var hit = TextLayout.HitTestPoint(point); var index = hit.TextPosition; - CaretIndex = index; + + SetAndRaise(CaretIndexProperty, ref _caretIndex, index); #pragma warning disable CS0618 // Type or member is obsolete switch (e.ClickCount) @@ -368,7 +460,7 @@ namespace Avalonia.Controls caretIndex >= firstSelection && caretIndex <= lastSelection; if (!didClickInSelection) { - _caretIndex = SelectionEnd = SelectionStart = caretIndex; + CaretIndex = SelectionEnd = SelectionStart = caretIndex; } } @@ -389,29 +481,19 @@ namespace Avalonia.Controls } case nameof(TextProperty): { - OnTextChanged(change.OldValue as string, change.NewValue as string); + InvalidateTextLayout(); break; } } } - private void OnTextChanged(string? oldValue, string? newValue) + private string GetSelection() { - if (oldValue == newValue) - { - return; - } - - if (Inlines is null) + if (!IsTextSelectionEnabled) { - return; + return ""; } - Inlines.Text = newValue; - } - - private string GetSelection() - { var text = Inlines.Text ?? Text; if (string.IsNullOrEmpty(text)) diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 1f891b092f..2f83ee1002 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -130,7 +130,7 @@ namespace Avalonia.Controls protected string? _text; protected TextLayout? _textLayout; - private Size _constraint; + protected Size _constraint; /// /// Initializes static members of the class. @@ -149,7 +149,7 @@ namespace Avalonia.Controls { get { - return _textLayout ??= CreateTextLayout(_constraint, Text); + return _textLayout ??= CreateTextLayout(_text); } } @@ -176,11 +176,8 @@ namespace Avalonia.Controls /// public string? Text { - get => _text; - set - { - SetAndRaise(TextProperty, ref _text, value); - } + get => GetText(); + set => SetText(value); } /// @@ -302,11 +299,6 @@ namespace Avalonia.Controls set { SetValue(BaselineOffsetProperty, value); } } - public void Add(string text) - { - Text = text; - } - /// /// Reads the attached property from the given element /// @@ -481,6 +473,10 @@ namespace Avalonia.Controls control.SetValue(MaxLinesProperty, maxLines); } + public void Add(string text) + { + _text = text; + } /// /// Renders the to a drawing context. @@ -516,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), @@ -537,8 +541,8 @@ namespace Avalonia.Controls new SimpleTextSource((text ?? "").AsMemory(), defaultProperties), paragraphProperties, TextTrimming, - constraint.Width, - constraint.Height, + _constraint.Width, + _constraint.Height, maxLines: MaxLines, lineHeight: LineHeight); } diff --git a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs index 1ed33e6132..f29420ff87 100644 --- a/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs +++ b/tests/Avalonia.Base.UnitTests/Media/TextFormatting/BiDiClassTests.cs @@ -30,7 +30,7 @@ namespace Avalonia.Visuals.UnitTests.Media.TextFormatting private bool Run(BiDiClassData t) { - var bidi = BidiAlgorithm.Instance.Value; + var bidi = new BidiAlgorithm(); var bidiData = new BidiData(t.ParagraphLevel); var text = Encoding.UTF32.GetString(MemoryMarshal.Cast(t.CodePoints).ToArray()); diff --git a/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs b/tests/Avalonia.Base.UnitTests/Styling/SetterTests.cs index ed4c78aa3e..99dfc93a68 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] From f4a38437314b00f99122c2b36d14a016c26a8c52 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 27 Jun 2022 09:49:21 +0200 Subject: [PATCH 04/38] Fix unit test --- tests/Avalonia.Controls.UnitTests/TextBlockTests.cs | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index 6da011f062..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] From aa5dc3e0f59e673bb089707008ad94c1da807112 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 2 Jul 2022 23:01:37 +0200 Subject: [PATCH 05/38] Added class selector style benchmarks. --- .../Styling/Style_ClassSelector.cs | 85 +++++++++++++++++++ 1 file changed, 85 insertions(+) create mode 100644 tests/Avalonia.Benchmarks/Styling/Style_ClassSelector.cs 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 + { + } + } +} From e63aa46458022a4a8974afe1eb1792ec7d1968c3 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sun, 3 Jul 2022 00:14:04 +0200 Subject: [PATCH 06/38] Add a more efficient way to listen to classes changes. Improves the situation in e.g. #8389 drastically. --- src/Avalonia.Base/Controls/Classes.cs | 53 ++++++++++++++++++- .../Controls/IClassesChangedListener.cs | 14 +++++ .../Styling/Activators/StyleClassActivator.cs | 26 ++++----- .../Styling/TypeNameAndClassSelector.cs | 3 +- .../Styling/SelectorTests_Template.cs | 5 +- tests/Avalonia.LeakTests/ControlTests.cs | 2 +- 6 files changed, 80 insertions(+), 23 deletions(-) create mode 100644 src/Avalonia.Base/Controls/IClassesChangedListener.cs 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/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/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.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 8c05f2a0a7..8651409af1 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -313,7 +313,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; From 5dc768aa13435f4f126dae6e2d778df9f66b0da1 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Mon, 4 Jul 2022 12:11:34 +0200 Subject: [PATCH 07/38] Add some xml comments --- src/Avalonia.Controls/RichTextBlock.cs | 71 +++++++++++--------------- src/Avalonia.Controls/TextBlock.cs | 2 - 2 files changed, 31 insertions(+), 42 deletions(-) diff --git a/src/Avalonia.Controls/RichTextBlock.cs b/src/Avalonia.Controls/RichTextBlock.cs index 1411d715ec..2b84113497 100644 --- a/src/Avalonia.Controls/RichTextBlock.cs +++ b/src/Avalonia.Controls/RichTextBlock.cs @@ -14,19 +14,13 @@ using Avalonia.Utilities; namespace Avalonia.Controls { /// - /// A control that displays a block of text. + /// 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 CaretIndexProperty = - AvaloniaProperty.RegisterDirect( - nameof(CaretIndex), - o => o.CaretIndex, - (o, v) => o.CaretIndex = v); - public static readonly DirectProperty SelectionStartProperty = AvaloniaProperty.RegisterDirect( nameof(SelectionStart), @@ -67,7 +61,6 @@ namespace Avalonia.Controls nameof(CopyingToClipboard), RoutingStrategies.Bubble); private bool _canCopy; - private int _caretIndex; private int _selectionStart; private int _selectionEnd; @@ -86,31 +79,28 @@ namespace Avalonia.Controls 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); } - public int CaretIndex - { - get => _caretIndex; - set - { - if(SetAndRaise(CaretIndexProperty, ref _caretIndex, value)) - { - SelectionStart = SelectionEnd = value; - } - } - } - + /// + /// Gets or sets a character index for the beginning of the current selection. + /// public int SelectionStart { get => _selectionStart; @@ -119,37 +109,36 @@ namespace Avalonia.Controls if (SetAndRaise(SelectionStartProperty, ref _selectionStart, value)) { RaisePropertyChanged(SelectedTextProperty, "", ""); - - if (SelectionEnd == value && CaretIndex != value) - { - CaretIndex = value; - } } } } + /// + /// 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)) + if (SetAndRaise(SelectionEndProperty, ref _selectionEnd, value)) { RaisePropertyChanged(SelectedTextProperty, "", ""); - - if (SelectionStart == value && CaretIndex != value) - { - CaretIndex = value; - } } } } + /// + /// 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); @@ -181,6 +170,9 @@ namespace Avalonia.Controls remove => RemoveHandler(CopyingToClipboardEvent, value); } + /// + /// Copies the current selection to the Clipboard. + /// public async void Copy() { if (_canCopy || !IsTextSelectionEnabled) @@ -259,7 +251,6 @@ namespace Avalonia.Controls SelectionEnd = SelectionStart; } - protected override string? GetText() { return _text ?? Inlines.Text; @@ -344,13 +335,13 @@ namespace Avalonia.Controls bool Match(List gestures) => gestures.Any(g => g.Matches(e)); if (Match(keymap.Copy)) - { + { Copy(); - + handled = true; } - e.Handled = handled; + e.Handled = handled; } protected override void OnPointerPressed(PointerPressedEventArgs e) @@ -367,14 +358,14 @@ namespace Avalonia.Controls { var point = e.GetPosition(this); - var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift); + var clickToSelect = e.KeyModifiers.HasFlag(KeyModifiers.Shift); - var oldIndex = CaretIndex; + var oldIndex = SelectionStart; var hit = TextLayout.HitTestPoint(point); var index = hit.TextPosition; - SetAndRaise(CaretIndexProperty, ref _caretIndex, index); + SelectionStart = SelectionEnd = index; #pragma warning disable CS0618 // Type or member is obsolete switch (e.ClickCount) @@ -460,7 +451,7 @@ namespace Avalonia.Controls caretIndex >= firstSelection && caretIndex <= lastSelection; if (!didClickInSelection) { - CaretIndex = SelectionEnd = SelectionStart = caretIndex; + SelectionStart = SelectionEnd = caretIndex; } } diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 27f7fc28b3..52261d1c76 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -1,11 +1,9 @@ using System; -using System.Collections.Generic; 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 From 9ed8648c91670cc1c851a2c47cc61cfc8b67e748 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Jul 2022 17:51:30 +0100 Subject: [PATCH 08/38] Set up CI with Azure Pipelines [skip ci] --- azure-pipelines-integrationtests.yml | 15 +++++++++++++++ 1 file changed, 15 insertions(+) create mode 100644 azure-pipelines-integrationtests.yml diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml new file mode 100644 index 0000000000..34ba412df0 --- /dev/null +++ b/azure-pipelines-integrationtests.yml @@ -0,0 +1,15 @@ +# 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 + +pool: + vmImage: osx-latest + +steps: +- script: start-appium + appium + displayName: 'start appium' From e4203da346ea2baa3d32800f1adc3d5447df370d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Jul 2022 17:53:55 +0100 Subject: [PATCH 09/38] Update azure-pipelines-integrationtests.yml for Azure Pipelines --- azure-pipelines-integrationtests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 34ba412df0..242a4e4463 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -7,7 +7,7 @@ trigger: - master pool: - vmImage: osx-latest + vmImage: macOS-10.15 steps: - script: start-appium From 9c8b7797479caf5280adfb156e27851f8cfa546b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Jul 2022 17:57:11 +0100 Subject: [PATCH 10/38] Update azure-pipelines-integrationtests.yml for Azure Pipelines --- azure-pipelines-integrationtests.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 242a4e4463..4c5026fa81 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -10,6 +10,5 @@ pool: vmImage: macOS-10.15 steps: -- script: start-appium - appium +- script: appium displayName: 'start appium' From d462d1147a31b1d5b4d4fb9397afcf9b85858173 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Jul 2022 18:00:20 +0100 Subject: [PATCH 11/38] Update azure-pipelines-integrationtests.yml for Azure Pipelines --- azure-pipelines-integrationtests.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 4c5026fa81..d6a6580981 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -10,5 +10,9 @@ pool: vmImage: macOS-10.15 steps: +- script: npm install -g appium + displayName: 'install appium' + - script: appium displayName: 'start appium' + From 0f40cedfb9f5fbaad51af2f22f5d9cb797cd4281 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Jul 2022 18:06:44 +0100 Subject: [PATCH 12/38] Update azure-pipelines-integrationtests.yml for Azure Pipelines --- azure-pipelines-integrationtests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index d6a6580981..f5938c85a3 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -13,6 +13,6 @@ steps: - script: npm install -g appium displayName: 'install appium' -- script: appium +- script: appium & displayName: 'start appium' From fb7998bf9d869ec16aff88a79e86101ce2cf8443 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Jul 2022 18:26:34 +0100 Subject: [PATCH 13/38] Update azure-pipelines-integrationtests.yml for Azure Pipelines --- azure-pipelines-integrationtests.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index f5938c85a3..476db51800 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -15,4 +15,7 @@ steps: - script: appium & displayName: 'start appium' + +- script: ./tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh + displayName: 'run integration tests' From d400353bfe2e30d76691cd5a85c21c261ec89f46 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Jul 2022 18:30:08 +0100 Subject: [PATCH 14/38] install sdks. --- azure-pipelines-integrationtests.yml | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 476db51800..981f6f673c 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -10,6 +10,16 @@ pool: vmImage: macOS-10.15 steps: +- task: UseDotNet@2 + displayName: 'Use .NET Core SDK 3.1.418' + inputs: + version: 3.1.418 + +- task: UseDotNet@2 + displayName: 'Use .NET Core SDK 6.0.202' + inputs: + version: 6.0.202 + - script: npm install -g appium displayName: 'install appium' From fc19f1e1a001539cfd77ca6210664603c051ef1b Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Jul 2022 18:31:12 +0100 Subject: [PATCH 15/38] fix yaml --- azure-pipelines-integrationtests.yml | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 981f6f673c..f8a38673a9 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -11,9 +11,9 @@ pool: steps: - task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.418' - inputs: - version: 3.1.418 + displayName: 'Use .NET Core SDK 3.1.418' + inputs: + version: 3.1.418 - task: UseDotNet@2 displayName: 'Use .NET Core SDK 6.0.202' From 92bdf1cce528df34df076bef95809a175452c011 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Jul 2022 18:50:34 +0100 Subject: [PATCH 16/38] install the integration sample app --- tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh | 1 + 1 file changed, 1 insertion(+) 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" From 219a12fb745cd067250f83bba2dbe7a8703cd6e1 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Mon, 4 Jul 2022 19:01:22 +0100 Subject: [PATCH 17/38] use macos-12 for integration tests --- azure-pipelines-integrationtests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index f8a38673a9..0598e205ec 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -7,7 +7,7 @@ trigger: - master pool: - vmImage: macOS-10.15 + vmImage: macOS-12 steps: - task: UseDotNet@2 From 9679b1562cda7c51c7b05ff77fd061fe275aa246 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 5 Jul 2022 11:42:44 +0100 Subject: [PATCH 18/38] use self hosted mac agent. --- azure-pipelines-integrationtests.yml | 15 +-------------- 1 file changed, 1 insertion(+), 14 deletions(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 0598e205ec..a8c78aa8d0 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -7,22 +7,9 @@ trigger: - master pool: - vmImage: macOS-12 + name: 'AvaloniaMacPool' steps: -- task: UseDotNet@2 - displayName: 'Use .NET Core SDK 3.1.418' - inputs: - version: 3.1.418 - -- task: UseDotNet@2 - displayName: 'Use .NET Core SDK 6.0.202' - inputs: - version: 6.0.202 - -- script: npm install -g appium - displayName: 'install appium' - - script: appium & displayName: 'start appium' From f215bfadf08421a2eebf60d175d9d5a4627bf942 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Tue, 5 Jul 2022 11:54:16 +0100 Subject: [PATCH 19/38] dont start appium --- azure-pipelines-integrationtests.yml | 3 --- 1 file changed, 3 deletions(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index a8c78aa8d0..94a80eef44 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -10,9 +10,6 @@ pool: name: 'AvaloniaMacPool' steps: -- script: appium & - displayName: 'start appium' - - script: ./tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh displayName: 'run integration tests' From b63b84860ca69e453b0c719873be381a1dba7099 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 6 Jul 2022 09:58:36 +0200 Subject: [PATCH 20/38] fix(DataGrid): CS8073 The result of the expression is always 'true' --- src/Avalonia.Controls.DataGrid/DataGrid.cs | 17 ++++++++--------- 1 file changed, 8 insertions(+), 9 deletions(-) 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'); } From 407f761201a287e22bc34f086f7491d7632b42c4 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 12:34:38 +0100 Subject: [PATCH 21/38] add windows task --- azure-pipelines-integrationtests.yml | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 94a80eef44..4f3ac57e73 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -6,10 +6,16 @@ trigger: - master -pool: - name: 'AvaloniaMacPool' -steps: -- script: ./tests/Avalonia.IntegrationTests.Appium/macos-clean-build-test.sh - displayName: 'run integration tests' +- 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' From 03b0fcb4549af35d142dcf9cb408fc49b6032e7d Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 12:40:42 +0100 Subject: [PATCH 22/38] add windows integration tests --- azure-pipelines-integrationtests.yml | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 4f3ac57e73..05872803b4 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -6,7 +6,7 @@ trigger: - master - +jobs: - job: Mac pool: name: 'AvaloniaMacPool' @@ -19,3 +19,20 @@ trigger: - job: Windows pool: vmImage: 'windows-2022' + + steps: + - task: Windows Application Driver@0 + inputs: + OperationType: 'Start' + AgentResolution: '1080p' + displayName: 'Start WinAppDriver' + + - task: DotNetCoreCLI@2 + inputs: + command: 'test' + projects: 'tests/Avalonia.IntegrationTests.Appium/' + + - task: Windows Application Driver@0 + inputs: + OperationType: 'Stop' + displayName: 'Stop WinAppDriver' From 7b12c76e1d79a8948bd1868018c1a829be1a5175 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 13:21:48 +0100 Subject: [PATCH 23/38] build test app first. --- azure-pipelines-integrationtests.yml | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 05872803b4..941dd01249 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -21,16 +21,26 @@ jobs: 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: '1080p' 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/' + projects: 'tests/Avalonia.IntegrationTests.Appium/Avalonia.IntegrationTests.Appium.csproj' - task: Windows Application Driver@0 inputs: From 0bb62fa8092421d6bf2f72479041f6bc93fffe34 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 13:39:03 +0100 Subject: [PATCH 24/38] use 4k res on windows tests --- azure-pipelines-integrationtests.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines-integrationtests.yml b/azure-pipelines-integrationtests.yml index 941dd01249..c3f6292703 100644 --- a/azure-pipelines-integrationtests.yml +++ b/azure-pipelines-integrationtests.yml @@ -29,7 +29,7 @@ jobs: - task: Windows Application Driver@0 inputs: OperationType: 'Start' - AgentResolution: '1080p' + AgentResolution: '4K' displayName: 'Start WinAppDriver' - task: DotNetCoreCLI@2 From c034525d29dd2d6e4c95aebf17751ccfc6332c0e Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 14:40:46 +0100 Subject: [PATCH 25/38] win32, keep window size at all scale settings when entering and exiting fs mode. --- src/Windows/Avalonia.Win32/WindowImpl.cs | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) 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); From 84a4de5c070f9dde1e0e752194f51c76daa978d3 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 16:43:50 +0100 Subject: [PATCH 26/38] make splitview not culture sensitive. --- src/Avalonia.Controls/SplitView.cs | 30 ++++++++++++++++++++++++++---- 1 file changed, 26 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index 532cb1d329..caefad4af7 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -431,18 +431,40 @@ namespace Avalonia.Controls } } + private string GetPsuedoClass(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 GetPsuedoClass(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 = GetPsuedoClass(e.GetOldValue()); + var newState = GetPsuedoClass(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 = GetPsuedoClass(e.GetOldValue()); + var newState = GetPsuedoClass(e.GetNewValue()); PseudoClasses.Remove($":{oldState}"); PseudoClasses.Add($":{newState}"); From b7a3bce6f99202fce9128c9824d3157e91c6d615 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 17:21:41 +0100 Subject: [PATCH 27/38] fix compiler warning. --- src/Avalonia.Controls/SplitView.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index caefad4af7..f560251e53 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -469,7 +469,7 @@ namespace Avalonia.Controls 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)), From cc2d791f3683ae77c21ba8d7b9ece37ac8852a7e Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Wed, 6 Jul 2022 19:45:01 +0200 Subject: [PATCH 28/38] Update samples/Sandbox/MainWindow.axaml Co-authored-by: Max Katz --- samples/Sandbox/MainWindow.axaml | 15 --------------- 1 file changed, 15 deletions(-) diff --git a/samples/Sandbox/MainWindow.axaml b/samples/Sandbox/MainWindow.axaml index a834e3fef3..6929f192c7 100644 --- a/samples/Sandbox/MainWindow.axaml +++ b/samples/Sandbox/MainWindow.axaml @@ -1,19 +1,4 @@ - - - - This is a - TextBlock - with several - Span elements, - - using a variety of styles - . - - - - - From 45ae4221dc8cf2f40a92c4d57dbdca7a3aacdd64 Mon Sep 17 00:00:00 2001 From: Dan Walmsley Date: Wed, 6 Jul 2022 20:58:36 +0100 Subject: [PATCH 29/38] correct spelling. --- src/Avalonia.Controls/SplitView.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.Controls/SplitView.cs b/src/Avalonia.Controls/SplitView.cs index f560251e53..c344dd795d 100644 --- a/src/Avalonia.Controls/SplitView.cs +++ b/src/Avalonia.Controls/SplitView.cs @@ -431,7 +431,7 @@ namespace Avalonia.Controls } } - private string GetPsuedoClass(SplitViewDisplayMode mode) + private string GetPseudoClass(SplitViewDisplayMode mode) { return mode switch { @@ -443,7 +443,7 @@ namespace Avalonia.Controls }; } - private string GetPsuedoClass(SplitViewPanePlacement placement) + private string GetPseudoClass(SplitViewPanePlacement placement) { return placement switch { @@ -455,16 +455,16 @@ namespace Avalonia.Controls private void OnPanePlacementChanged(AvaloniaPropertyChangedEventArgs e) { - var oldState = GetPsuedoClass(e.GetOldValue()); - var newState = GetPsuedoClass(e.GetNewValue()); + var oldState = GetPseudoClass(e.GetOldValue()); + var newState = GetPseudoClass(e.GetNewValue()); PseudoClasses.Remove($":{oldState}"); PseudoClasses.Add($":{newState}"); } private void OnDisplayModeChanged(AvaloniaPropertyChangedEventArgs e) { - var oldState = GetPsuedoClass(e.GetOldValue()); - var newState = GetPsuedoClass(e.GetNewValue()); + var oldState = GetPseudoClass(e.GetOldValue()); + var newState = GetPseudoClass(e.GetNewValue()); PseudoClasses.Remove($":{oldState}"); PseudoClasses.Add($":{newState}"); From f80cf2d355f813d9294523984c6323398b4ef65d Mon Sep 17 00:00:00 2001 From: daniel mayost Date: Thu, 7 Jul 2022 23:01:38 +0300 Subject: [PATCH 30/38] add base.OnAttachedToVisualTree(e) call to repeater --- src/Avalonia.Controls/Repeater/ItemsRepeater.cs | 1 + 1 file changed, 1 insertion(+) 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(); } From a1804e2e16611556e169243dff73e847935bf011 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:02:01 -0400 Subject: [PATCH 31/38] Add tests for explicitly implemented interface property binding --- .../CompiledBindingExtensionTests.cs | 33 ++++++++++++++++++- 1 file changed, 32 insertions(+), 1 deletion(-) diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index f3f2d2f1e4..898641f2ee 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -1145,6 +1145,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() @@ -1410,6 +1432,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 +1456,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 +1476,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(); From 042fd47c1e08fdd33023176520cf7c10f07b1ffd Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:04:00 -0400 Subject: [PATCH 32/38] Fix ElementName binding inside of control template --- .../XamlIlBindingPathHelper.cs | 7 ++++ .../CompiledBindingExtensionTests.cs | 32 +++++++++++++++++++ 2 files changed, 39 insertions(+) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index c8de8f00f6..dd809956cd 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -374,6 +374,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 +405,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/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 898641f2ee..e6df80f17e 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -641,6 +641,38 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } } + + [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() { From 142b878984db2f3be0977b4073a83ff3247ab2f0 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:05:25 -0400 Subject: [PATCH 33/38] Fix special case of compiled binding with Path child --- .../XamlIlBindingPathHelper.cs | 8 +++-- .../CompiledBindingExtensionTests.cs | 35 +++++++++++++++++++ 2 files changed, 41 insertions(+), 2 deletions(-) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index dd809956cd..8a115bf8fc 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"); } } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index e6df80f17e..754a819182 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -102,6 +102,41 @@ 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 ResolvesStreamTaskBindingCorrectly() From 2619ab17595b6317fe4afdf1bc0917cb325c5a1d Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:06:13 -0400 Subject: [PATCH 34/38] Fix compiled binding with TemplatedParent relative source --- .../AvaloniaXamlIlCompiler.cs | 6 ++-- .../AvaloniaXamlIlBindingPathParser.cs | 25 ++++++++++------ .../XamlIlBindingPathHelper.cs | 1 + .../CompiledBindingExtensionTests.cs | 30 +++++++++++++++++-- 4 files changed, 47 insertions(+), 15 deletions(-) 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..60c880c6aa 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs @@ -121,11 +121,14 @@ 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.", binding); + } + convertedNode = new TemplatedParentBindingExpressionNode { Type = parentType }; } else diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs index 8a115bf8fc..ae29dcf9cb 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -244,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)); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 754a819182..5ca25a176c 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -3,8 +3,8 @@ 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; @@ -17,8 +17,6 @@ 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 @@ -676,6 +674,32 @@ 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() From 5d234cd49c7e7da9c446d7450f06267053a71999 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:06:30 -0400 Subject: [PATCH 35/38] Remove reflection bindings from standard controls --- .../Diagnostics/Controls/FilterTextBox.axaml | 2 +- src/Avalonia.Themes.Fluent/Controls/MenuItem.xaml | 2 +- src/Avalonia.Themes.Fluent/Controls/ProgressBar.xaml | 4 ++-- 3 files changed, 4 insertions(+), 4 deletions(-) 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 @@ - + From ff3b23b2d6eb3e874ad2c71393181b7756ffb5ae Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:17:05 -0400 Subject: [PATCH 36/38] Formatting --- .../Transformers/AvaloniaXamlIlBindingPathParser.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) 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 60c880c6aa..6150265cf8 100644 --- a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs @@ -121,7 +121,7 @@ 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 modeProperty = relativeSourceObject.Children .OfType() .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Mode")? @@ -226,7 +226,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers var parentType = contentTemplateNode.TargetType.GetClrType(); if (parentType is null) { - throw new XamlParseException("TargetType has to be set on ControlTemplate.", binding); + 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 }; From 34f8f3d5564d3d7b1584746c8dcaa887a5e235f5 Mon Sep 17 00:00:00 2001 From: Max Katz Date: Fri, 8 Jul 2022 01:57:06 -0400 Subject: [PATCH 37/38] Fix DataType set on Binding extension + add tests --- .../AvaloniaXamlIlBindingPathTransformer.cs | 5 + .../CompiledBindings/CompiledBindingPath.cs | 2 + .../CompiledBindingExtensionTests.cs | 95 +++++++++++++++++++ 3 files changed, 102 insertions(+) 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/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/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs index 5ca25a176c..215ae4d54f 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -9,10 +9,13 @@ 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; @@ -135,6 +138,56 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions 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() @@ -1479,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 @@ -1656,4 +1746,9 @@ namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions } public class CustomDataTemplateInherit : CustomDataTemplate { } + + public class AssignBindingControl : Control + { + [AssignBinding] public IBinding X { get; set; } + } } From 8e57d0b7c6b52e6598570795e206a4a27e7e13b9 Mon Sep 17 00:00:00 2001 From: Takoooooo Date: Fri, 8 Jul 2022 11:52:55 +0300 Subject: [PATCH 38/38] Fix bindings to Types. --- .../Data/Core/Plugins/InpcPropertyAccessorPlugin.cs | 2 +- tests/Avalonia.Markup.UnitTests/Data/BindingTests.cs | 11 +++++++++++ 2 files changed, 12 insertions(+), 1 deletion(-) 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/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 =