From 02906a623485971475cd2095987c8f8d3caa9cdf Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Mon, 13 Feb 2023 18:04:22 +0200 Subject: [PATCH 1/7] Add SliderAutomationPeer --- .../Automation/Peers/SliderAutomationPeer.cs | 22 +++++++ src/Avalonia.Controls/Slider.cs | 6 ++ .../Automation/AutomationNode.cs | 65 ++++++------------- .../Automation/RootAutomationNode.cs | 35 +++++++++- 4 files changed, 81 insertions(+), 47 deletions(-) create mode 100644 src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs diff --git a/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs b/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs new file mode 100644 index 0000000000..42b15eec96 --- /dev/null +++ b/src/Avalonia.Controls/Automation/Peers/SliderAutomationPeer.cs @@ -0,0 +1,22 @@ +using Avalonia.Automation.Peers; + +namespace Avalonia.Controls.Automation.Peers +{ + public class SliderAutomationPeer : RangeBaseAutomationPeer + { + public SliderAutomationPeer(Slider owner) : base(owner) + { + } + + override protected string GetClassNameCore() + { + return "Slider"; + } + + override protected AutomationControlType GetAutomationControlTypeCore() + { + return AutomationControlType.Slider; + } + + } +} diff --git a/src/Avalonia.Controls/Slider.cs b/src/Avalonia.Controls/Slider.cs index 828bf2a1fb..7de726a932 100644 --- a/src/Avalonia.Controls/Slider.cs +++ b/src/Avalonia.Controls/Slider.cs @@ -10,6 +10,7 @@ using Avalonia.Interactivity; using Avalonia.Layout; using Avalonia.Utilities; using Avalonia.Automation; +using Avalonia.Controls.Automation.Peers; namespace Avalonia.Controls { @@ -380,6 +381,11 @@ namespace Avalonia.Controls } } + protected override AutomationPeer OnCreateAutomationPeer() + { + return new SliderAutomationPeer(this); + } + /// protected override void OnPropertyChanged(AvaloniaPropertyChangedEventArgs change) { diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index 29ab2cea3a..d35443b339 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -22,7 +22,6 @@ namespace Avalonia.Win32.Automation IRawElementProviderSimple, IRawElementProviderSimple2, IRawElementProviderFragment, - IRawElementProviderAdviseEvents, IInvokeProvider { private static Dictionary s_propertyMap = new Dictionary() @@ -50,14 +49,31 @@ namespace Avalonia.Win32.Automation new ConditionalWeakTable(); private readonly int[] _runtimeId; - private int _raiseFocusChanged; - private int _raisePropertyChanged; public AutomationNode(AutomationPeer peer) { _runtimeId = new int[] { 3, GetHashCode() }; Peer = peer; s_nodes.Add(peer, this); + peer.ChildrenChanged += Peer_ChildrenChanged; + peer.PropertyChanged += Peer_PropertyChanged; + } + + private void Peer_ChildrenChanged(object sender, EventArgs e) + { + ChildrenChanged(); + } + + private void Peer_PropertyChanged(object sender, AutomationPropertyChangedEventArgs e) + { + if (s_propertyMap.TryGetValue(e.Property, out var id)) + { + UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent( + this, + (int)id, + e.OldValue as IConvertible, + e.NewValue as IConvertible); + } } public AutomationPeer Peer { get; protected set; } @@ -89,14 +105,6 @@ namespace Avalonia.Win32.Automation 0); } - public void PropertyChanged(AutomationProperty property, object? oldValue, object? newValue) - { - if (_raisePropertyChanged > 0 && s_propertyMap.TryGetValue(property, out var id)) - { - UiaCoreProviderApi.UiaRaiseAutomationPropertyChangedEvent(this, (int)id, oldValue, newValue); - } - } - [return: MarshalAs(UnmanagedType.IUnknown)] public virtual object? GetPatternProvider(int patternId) { @@ -190,32 +198,6 @@ namespace Avalonia.Win32.Automation void IRawElementProviderSimple2.ShowContextMenu() => InvokeSync(() => Peer.ShowContextMenu()); void IInvokeProvider.Invoke() => InvokeSync((AAP.IInvokeProvider x) => x.Invoke()); - void IRawElementProviderAdviseEvents.AdviseEventAdded(int eventId, int[] properties) - { - switch ((UiaEventId)eventId) - { - case UiaEventId.AutomationPropertyChanged: - ++_raisePropertyChanged; - break; - case UiaEventId.AutomationFocusChanged: - ++_raiseFocusChanged; - break; - } - } - - void IRawElementProviderAdviseEvents.AdviseEventRemoved(int eventId, int[] properties) - { - switch ((UiaEventId)eventId) - { - case UiaEventId.AutomationPropertyChanged: - --_raisePropertyChanged; - break; - case UiaEventId.AutomationFocusChanged: - --_raiseFocusChanged; - break; - } - } - protected void InvokeSync(Action action) { if (Dispatcher.UIThread.CheckAccess()) @@ -268,15 +250,6 @@ namespace Avalonia.Win32.Automation throw new NotSupportedException(); } - protected void RaiseFocusChanged(AutomationNode? focused) - { - if (_raiseFocusChanged > 0) - { - UiaCoreProviderApi.UiaRaiseAutomationEvent( - focused, - (int)UiaEventId.AutomationFocusChanged); - } - } private AutomationNode? GetRoot() { diff --git a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs index b732c4169f..1c6784798e 100644 --- a/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/RootAutomationNode.cs @@ -12,8 +12,11 @@ namespace Avalonia.Win32.Automation { [RequiresUnreferencedCode("Requires .NET COM interop")] internal class RootAutomationNode : AutomationNode, - IRawElementProviderFragmentRoot + IRawElementProviderFragmentRoot, + IRawElementProviderAdviseEvents { + private int _raiseFocusChanged; + public RootAutomationNode(AutomationPeer peer) : base(peer) { @@ -44,6 +47,36 @@ namespace Avalonia.Win32.Automation return GetOrCreate(focus); } + void IRawElementProviderAdviseEvents.AdviseEventAdded(int eventId, int[] properties) + { + switch ((UiaEventId)eventId) + { + case UiaEventId.AutomationFocusChanged: + ++_raiseFocusChanged; + break; + } + } + + void IRawElementProviderAdviseEvents.AdviseEventRemoved(int eventId, int[] properties) + { + switch ((UiaEventId)eventId) + { + case UiaEventId.AutomationFocusChanged: + --_raiseFocusChanged; + break; + } + } + + protected void RaiseFocusChanged(AutomationNode? focused) + { + if (_raiseFocusChanged > 0) + { + UiaCoreProviderApi.UiaRaiseAutomationEvent( + focused, + (int)UiaEventId.AutomationFocusChanged); + } + } + public void FocusChanged(object? sender, EventArgs e) { RaiseFocusChanged(GetOrCreate(Peer.GetFocus())); From 37f5f50212cce6d4ee61d10f20cc6fcc309d1c21 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Wies=C5=82aw=20=C5=A0olt=C3=A9s?= Date: Mon, 13 Feb 2023 22:41:41 +0100 Subject: [PATCH 2/7] Update Layoutable.cs --- src/Avalonia.Base/Layout/Layoutable.cs | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index 775b8adddd..f14ad3058a 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -323,12 +323,15 @@ namespace Avalonia.Layout set { SetValue(UseLayoutRoundingProperty, value); } } - internal Size? PreviousMeasure => _previousMeasure; + /// + /// Gets the available size passed in the previous layout pass, if any. + /// + public Size? PreviousMeasure => _previousMeasure; /// /// Gets the layout rect passed in the previous layout pass, if any. /// - internal Rect? PreviousArrange => _previousArrange; + public Rect? PreviousArrange => _previousArrange; /// /// Creates the visual children of the control, if necessary From ac21ab2fe69539522296a6d043ebbb98dcaeaaaf Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Tue, 14 Feb 2023 15:06:13 +0200 Subject: [PATCH 3/7] Add Slider Automation Test --- samples/IntegrationTestApp/MainWindow.axaml | 3 ++ .../SliderTests.cs | 35 +++++++++++++++++++ 2 files changed, 38 insertions(+) create mode 100644 tests/Avalonia.IntegrationTests.Appium/SliderTests.cs diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index b116e4c789..8fd33061df 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -151,6 +151,9 @@ + + + diff --git a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs new file mode 100644 index 0000000000..f6c50af59d --- /dev/null +++ b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs @@ -0,0 +1,35 @@ +using System; +using OpenQA.Selenium.Appium; +using OpenQA.Selenium.Interactions; +using Xunit; + +namespace Avalonia.IntegrationTests.Appium +{ + [Collection("Default")] + public class SliderTests + { + private readonly AppiumDriver _session; + + public SliderTests(TestAppFixture fixture) + { + _session = fixture.Session; + + var tabs = _session.FindElementByAccessibilityId("MainTabs"); + var tab = tabs.FindElementByName("Slider"); + tab.Click(); + } + + [Fact] + public void Changes_Value_When_Moving_Slider() + { + var slider = _session.FindElementByAccessibilityId("Slider2"); + + // slider.Text gets the Slider value + Assert.True(double.Parse(slider.Text) == 30); + + new Actions(_session).Click(slider).MoveByOffset(100, 0).Perform(); + + Assert.Equal(50, Math.Round(double.Parse(slider.Text))); + } + } +} From 4ad731ea60bc11bba5dc35e9d3ebd65ce4bd04bd Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Tue, 14 Feb 2023 15:38:03 +0200 Subject: [PATCH 4/7] Fix nullability issues --- src/Windows/Avalonia.Win32/Automation/AutomationNode.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs index d35443b339..0642331b74 100644 --- a/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs +++ b/src/Windows/Avalonia.Win32/Automation/AutomationNode.cs @@ -59,12 +59,12 @@ namespace Avalonia.Win32.Automation peer.PropertyChanged += Peer_PropertyChanged; } - private void Peer_ChildrenChanged(object sender, EventArgs e) + private void Peer_ChildrenChanged(object? sender, EventArgs e) { ChildrenChanged(); } - private void Peer_PropertyChanged(object sender, AutomationPropertyChangedEventArgs e) + private void Peer_PropertyChanged(object? sender, AutomationPropertyChangedEventArgs e) { if (s_propertyMap.TryGetValue(e.Property, out var id)) { From 44b833a0af0645b3d99757e3d900834dfdeffd13 Mon Sep 17 00:00:00 2001 From: Daniil Pavliuchyk Date: Wed, 15 Feb 2023 15:20:05 +0200 Subject: [PATCH 5/7] Fix test --- samples/IntegrationTestApp/MainWindow.axaml | 4 ++-- tests/Avalonia.IntegrationTests.Appium/SliderTests.cs | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/samples/IntegrationTestApp/MainWindow.axaml b/samples/IntegrationTestApp/MainWindow.axaml index 8fd33061df..72470873cb 100644 --- a/samples/IntegrationTestApp/MainWindow.axaml +++ b/samples/IntegrationTestApp/MainWindow.axaml @@ -151,8 +151,8 @@ - - + + diff --git a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs index f6c50af59d..7fa5eb83ee 100644 --- a/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs +++ b/tests/Avalonia.IntegrationTests.Appium/SliderTests.cs @@ -15,19 +15,19 @@ namespace Avalonia.IntegrationTests.Appium _session = fixture.Session; var tabs = _session.FindElementByAccessibilityId("MainTabs"); - var tab = tabs.FindElementByName("Slider"); + var tab = tabs.FindElementByName("SliderTab"); tab.Click(); } [Fact] - public void Changes_Value_When_Moving_Slider() + public void Changes_Value_When_Clicking_Increase_Button() { - var slider = _session.FindElementByAccessibilityId("Slider2"); + var slider = _session.FindElementByAccessibilityId("Slider"); // slider.Text gets the Slider value Assert.True(double.Parse(slider.Text) == 30); - new Actions(_session).Click(slider).MoveByOffset(100, 0).Perform(); + new Actions(_session).Click(slider).Perform(); Assert.Equal(50, Math.Round(double.Parse(slider.Text))); } From 95b6343f8f217f281f59127dff4928113d5bc94b Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Thu, 16 Feb 2023 13:02:20 +0600 Subject: [PATCH 6/7] Respect AdornerLayer.IsClipEnabled --- .../Server/ServerCompositionVisual.cs | 5 +- src/Avalonia.Base/composition-schema.xml | 1 + .../Primitives/AdornerLayer.cs | 5 +- .../Controls/AdornerTests.cs | 81 +++++++++++------- ...s_Properly_Clipped_Clip_False.expected.png | Bin 0 -> 694 bytes ...s_Properly_Clipped_Clip_True.expected.png} | Bin ...s_Properly_Clipped_Clip_False.expected.png | Bin 0 -> 694 bytes ...s_Properly_Clipped_Clip_True.expected.png} | Bin 8 files changed, 56 insertions(+), 36 deletions(-) create mode 100644 tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_False.expected.png rename tests/TestFiles/Direct2D1/Controls/Adorner/{Focus_Adorner_Is_Properly_Clipped.expected.png => Focus_Adorner_Is_Properly_Clipped_Clip_True.expected.png} (100%) create mode 100644 tests/TestFiles/Skia/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_False.expected.png rename tests/TestFiles/Skia/Controls/Adorner/{Focus_Adorner_Is_Properly_Clipped.expected.png => Focus_Adorner_Is_Properly_Clipped_Clip_True.expected.png} (100%) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs index e33dc999dc..98be861afa 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionVisual.cs @@ -48,7 +48,8 @@ namespace Avalonia.Rendering.Composition.Server { canvas.PostTransform = Matrix.Identity; canvas.Transform = Matrix.Identity; - canvas.PushClip(AdornedVisual._combinedTransformedClipBounds); + if (AdornerIsClipped) + canvas.PushClip(AdornedVisual._combinedTransformedClipBounds); } var transform = GlobalTransformMatrix; canvas.PostTransform = MatrixUtils.ToMatrix(transform); @@ -74,7 +75,7 @@ namespace Avalonia.Rendering.Composition.Server canvas.PopGeometryClip(); if (ClipToBounds && !HandlesClipToBounds) canvas.PopClip(); - if (AdornedVisual != null) + if (AdornedVisual != null && AdornerIsClipped) canvas.PopClip(); if(Opacity != 1) canvas.PopOpacity(); diff --git a/src/Avalonia.Base/composition-schema.xml b/src/Avalonia.Base/composition-schema.xml index 36fd9fe709..31722974ee 100644 --- a/src/Avalonia.Base/composition-schema.xml +++ b/src/Avalonia.Base/composition-schema.xml @@ -26,6 +26,7 @@ + diff --git a/src/Avalonia.Controls/Primitives/AdornerLayer.cs b/src/Avalonia.Controls/Primitives/AdornerLayer.cs index 79719912ea..611d57a980 100644 --- a/src/Avalonia.Controls/Primitives/AdornerLayer.cs +++ b/src/Avalonia.Controls/Primitives/AdornerLayer.cs @@ -279,8 +279,11 @@ namespace Avalonia.Controls.Primitives private void UpdateAdornedElement(Visual adorner, Visual? adorned) { if (adorner.CompositionVisual != null) + { adorner.CompositionVisual.AdornedVisual = adorned?.CompositionVisual; - + adorner.CompositionVisual.AdornerIsClipped = GetIsClipEnabled(adorner); + } + var info = adorner.GetValue(s_adornedElementInfoProperty); if (info != null) diff --git a/tests/Avalonia.RenderTests/Controls/AdornerTests.cs b/tests/Avalonia.RenderTests/Controls/AdornerTests.cs index c0159aecff..b158bf798d 100644 --- a/tests/Avalonia.RenderTests/Controls/AdornerTests.cs +++ b/tests/Avalonia.RenderTests/Controls/AdornerTests.cs @@ -1,3 +1,4 @@ +using System.Runtime.CompilerServices; using System.Threading.Tasks; using Avalonia.Controls; using Avalonia.Controls.Primitives; @@ -18,56 +19,70 @@ public class AdornerTests : TestBase { } - [Fact] - public async Task Focus_Adorner_Is_Properly_Clipped() + async Task CheckAdornedContent(Control content, Control adorned, Control adorner, int width = 200, int height = 200, + [CallerMemberName] string testName = "") { - Border adorned; var tree = new Decorator { Child = new VisualLayerManager { - Child = new Border - { - Background = Brushes.Red, - Padding = new Thickness(10, 50, 10,10), - Child = new Border() - { - Background = Brushes.White, - ClipToBounds = true, - Padding = new Thickness(0, -30, 0, 0), - Child = adorned = new Border - { - Background = Brushes.Green, - VerticalAlignment = VerticalAlignment.Top, - Height = 100, - Width = 50 - } - } - } + Child = content }, - Width = 200, - Height = 200 - }; - var adorner = new Border - { - BorderThickness = new Thickness(2), - BorderBrush = Brushes.Black + Width = width, + Height = height }; - + var size = new Size(tree.Width, tree.Height); tree.Measure(size); tree.Arrange(new Rect(size)); - - + adorned.AttachedToVisualTree += delegate { AdornerLayer.SetAdornedElement(adorner, adorned); AdornerLayer.GetAdornerLayer(adorned)!.Children.Add(adorner); }; + tree.Measure(size); tree.Arrange(new Rect(size)); - await RenderToFile(tree); - CompareImages(skipImmediate: true); + await RenderToFile(tree, testName: testName); + CompareImages(skipImmediate: true, testName: testName); + } + + [Theory, + InlineData(true), + InlineData(false) + ] + public async Task Focus_Adorner_Is_Properly_Clipped(bool clip) + { + Border adorned; + var content = new Border + { + Background = Brushes.Red, + Padding = new Thickness(10, 50, 10, 10), + Child = new Border() + { + Background = Brushes.White, + ClipToBounds = true, + Padding = new Thickness(0, -30, 0, 0), + Child = adorned = new Border + { + Background = Brushes.Green, + VerticalAlignment = VerticalAlignment.Top, + Height = 100, + Width = 50 + } + } + }; + var adorner = new Border + { + BorderThickness = new Thickness(2), + BorderBrush = Brushes.Black + }; + if (!clip) + AdornerLayer.SetIsClipEnabled(adorner, false); + await CheckAdornedContent(content, adorned, adorner, + testName: "Focus_Adorner_Is_Properly_Clipped_Clip_" + clip); } + } \ No newline at end of file diff --git a/tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_False.expected.png b/tests/TestFiles/Direct2D1/Controls/Adorner/Focus_Adorner_Is_Properly_Clipped_Clip_False.expected.png new file mode 100644 index 0000000000000000000000000000000000000000..4821c22c397099cdae149b5ed9cc367ab9844a66 GIT binary patch literal 694 zcmeAS@N?(olHy`uVBq!ia0vp^CqS5k4M?tyST_$yu@pObhHwBu4M$1`0|Qg8r;B4q z#hkZyHf9|*5OD}x`Q-5D=dUhYT6t$y!XgXX^pB4oHtwjJ{Y)yE_d&S)ZuW+7H31H$ zMg<~?g4?zSrk=UWJm-iq%ki5E0yz`dW4^N0P3QgLW5&VK=paB5(egO|^SaghJgOJ{ z{JQJyp7*kk6TiP-zxj{l>HRkp1X!3H2R{J}SlM62vcKQXJ!@IsU(oeb=eR}1G&7Et z+wU0H9Xb2^|4{J=IJAMjc`L^hHHRkp1X!3H2R{J}SlM62vcKQXJ!@IsU(oeb=eR}1G&7Et z+wU0H9Xb2^|4{J=IJAMjc`L^hH Date: Thu, 16 Feb 2023 11:34:37 +0100 Subject: [PATCH 7/7] Implement LayoutInformation. --- src/Avalonia.Base/Layout/LayoutInformation.cs | 27 +++++++++++++++++++ src/Avalonia.Base/Layout/Layoutable.cs | 4 +-- 2 files changed, 29 insertions(+), 2 deletions(-) create mode 100644 src/Avalonia.Base/Layout/LayoutInformation.cs diff --git a/src/Avalonia.Base/Layout/LayoutInformation.cs b/src/Avalonia.Base/Layout/LayoutInformation.cs new file mode 100644 index 0000000000..9b821053a2 --- /dev/null +++ b/src/Avalonia.Base/Layout/LayoutInformation.cs @@ -0,0 +1,27 @@ +namespace Avalonia.Layout; + +/// +/// Provides access to layout information of a control. +/// +public static class LayoutInformation +{ + /// + /// Gets the available size constraint passed in the previous layout pass. + /// + /// The control. + /// Previous control measure constraint, if any. + public static Size? GetPreviousMeasureConstraint(Layoutable control) + { + return control.PreviousMeasure; + } + + /// + /// Gets the control bounds used in the previous layout arrange pass. + /// + /// The control. + /// Previous control arrange bounds, if any. + public static Rect? GetPreviousArrangeBounds(Layoutable control) + { + return control.PreviousArrange; + } +} diff --git a/src/Avalonia.Base/Layout/Layoutable.cs b/src/Avalonia.Base/Layout/Layoutable.cs index f14ad3058a..4a273b0291 100644 --- a/src/Avalonia.Base/Layout/Layoutable.cs +++ b/src/Avalonia.Base/Layout/Layoutable.cs @@ -326,12 +326,12 @@ namespace Avalonia.Layout /// /// Gets the available size passed in the previous layout pass, if any. /// - public Size? PreviousMeasure => _previousMeasure; + internal Size? PreviousMeasure => _previousMeasure; /// /// Gets the layout rect passed in the previous layout pass, if any. /// - public Rect? PreviousArrange => _previousArrange; + internal Rect? PreviousArrange => _previousArrange; /// /// Creates the visual children of the control, if necessary