From aa5c1a6ed487de0b08cf9d08294c0edf51a0c9cc Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 2 Apr 2020 19:03:13 +0200 Subject: [PATCH 01/25] Added ScrollViewer.ScrollChanged event. The API for `ScrollChangedEventArgs` is different to WPF's here, because: - Avalonia's `ScrollViewer` exposes `Extent`, `Offset` and `Viewport` as `Size`/`Vector` structs whereas WPF exposes separate `double` values for the X and Y components for each of these - The current values are not included in the event args: then can easily be read from the `sender` - UWP doesn't expose these values at all --- .../ScrollChangedEventArgs.cs | 45 ++++++++++++ src/Avalonia.Controls/ScrollViewer.cs | 49 ++++++++++++- .../ScrollViewerTests.cs | 73 ++++++++++++++++++- 3 files changed, 162 insertions(+), 5 deletions(-) create mode 100644 src/Avalonia.Controls/ScrollChangedEventArgs.cs diff --git a/src/Avalonia.Controls/ScrollChangedEventArgs.cs b/src/Avalonia.Controls/ScrollChangedEventArgs.cs new file mode 100644 index 0000000000..fed23964f5 --- /dev/null +++ b/src/Avalonia.Controls/ScrollChangedEventArgs.cs @@ -0,0 +1,45 @@ +using Avalonia.Interactivity; + +namespace Avalonia.Controls +{ + /// + /// Describes a change in scrolling state. + /// + public class ScrollChangedEventArgs : RoutedEventArgs + { + public ScrollChangedEventArgs( + Vector extentDelta, + Vector offsetDelta, + Vector viewportDelta) + : this(ScrollViewer.ScrollChangedEvent, extentDelta, offsetDelta, viewportDelta) + { + } + + public ScrollChangedEventArgs( + RoutedEvent routedEvent, + Vector extentDelta, + Vector offsetDelta, + Vector viewportDelta) + : base(routedEvent) + { + ExtentDelta = extentDelta; + OffsetDelta = offsetDelta; + ViewportDelta = viewportDelta; + } + + /// + /// Gets the change to the value of . + /// + public Vector ExtentDelta { get; } + + /// + /// Gets the change to the value of . + /// + public Vector OffsetDelta { get; } + + /// + /// Gets the change to the value of . + /// + public Vector ViewportDelta { get; } + } +} diff --git a/src/Avalonia.Controls/ScrollViewer.cs b/src/Avalonia.Controls/ScrollViewer.cs index f5881a8efe..fa95895fce 100644 --- a/src/Avalonia.Controls/ScrollViewer.cs +++ b/src/Avalonia.Controls/ScrollViewer.cs @@ -2,6 +2,7 @@ using System; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Input; +using Avalonia.Interactivity; namespace Avalonia.Controls { @@ -167,6 +168,14 @@ namespace Avalonia.Controls nameof(VerticalScrollBarVisibility), ScrollBarVisibility.Auto); + /// + /// Defines the event. + /// + public static readonly RoutedEvent ScrollChangedEvent = + RoutedEvent.Register( + nameof(ScrollChanged), + RoutingStrategies.Bubble); + private IDisposable _childSubscription; private ILogicalScrollable _logicalScrollable; private Size _extent; @@ -191,6 +200,15 @@ namespace Avalonia.Controls { } + /// + /// Occurs when changes are detected to the scroll position, extent, or viewport size. + /// + public event EventHandler ScrollChanged + { + add => AddHandler(ScrollChangedEvent, value); + remove => RemoveHandler(ScrollChangedEvent, value); + } + /// /// Gets the extent of the scrollable content. /// @@ -203,9 +221,11 @@ namespace Avalonia.Controls private set { + var old = _extent; + if (SetAndRaise(ExtentProperty, ref _extent, value)) { - CalculatedPropertiesChanged(); + CalculatedPropertiesChanged(extentDelta: value - old); } } } @@ -222,11 +242,13 @@ namespace Avalonia.Controls set { + var old = _offset; + value = ValidateOffset(this, value); if (SetAndRaise(OffsetProperty, ref _offset, value)) { - CalculatedPropertiesChanged(); + CalculatedPropertiesChanged(offsetDelta: value - old); } } } @@ -243,9 +265,11 @@ namespace Avalonia.Controls private set { + var old = _viewport; + if (SetAndRaise(ViewportProperty, ref _viewport, value)) { - CalculatedPropertiesChanged(); + CalculatedPropertiesChanged(viewportDelta: value - old); } } } @@ -525,7 +549,10 @@ namespace Avalonia.Controls } } - private void CalculatedPropertiesChanged() + private void CalculatedPropertiesChanged( + Size extentDelta = default, + Vector offsetDelta = default, + Size viewportDelta = default) { // Pass old values of 0 here because we don't have the old values at this point, // and it shouldn't matter as only the template uses these properies. @@ -546,6 +573,20 @@ namespace Avalonia.Controls SetAndRaise(SmallChangeProperty, ref _smallChange, s_defaultSmallChange); SetAndRaise(LargeChangeProperty, ref _largeChange, Viewport); } + + if (extentDelta != default || offsetDelta != default || viewportDelta != default) + { + using var route = BuildEventRoute(ScrollChangedEvent); + + if (route.HasHandlers) + { + var e = new ScrollChangedEventArgs( + new Vector(extentDelta.Width, extentDelta.Height), + offsetDelta, + new Vector(viewportDelta.Width, viewportDelta.Height)); + route.RaiseEvent(this, e); + } + } } protected override void OnKeyDown(KeyEventArgs e) diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index 5375a244c9..8da1e26f0d 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs @@ -4,7 +4,6 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Layout; -using Avalonia.LogicalTree; using Moq; using Xunit; @@ -147,6 +146,78 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(new Size(45, 67), target.LargeChange); } + [Fact] + public void Changing_Extent_Should_Raise_ScrollChanged() + { + var target = new ScrollViewer(); + var raised = 0; + + target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); + target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); + target.Offset = new Vector(10, 10); + + target.ScrollChanged += (s, e) => + { + Assert.Equal(new Vector(11, 12), e.ExtentDelta); + Assert.Equal(default, e.OffsetDelta); + Assert.Equal(default, e.ViewportDelta); + ++raised; + }; + + target.SetValue(ScrollViewer.ExtentProperty, new Size(111, 112)); + + Assert.Equal(1, raised); + + } + + [Fact] + public void Changing_Offset_Should_Raise_ScrollChanged() + { + var target = new ScrollViewer(); + var raised = 0; + + target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); + target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); + target.Offset = new Vector(10, 10); + + target.ScrollChanged += (s, e) => + { + Assert.Equal(default, e.ExtentDelta); + Assert.Equal(new Vector(12, 14), e.OffsetDelta); + Assert.Equal(default, e.ViewportDelta); + ++raised; + }; + + target.Offset = new Vector(22, 24); + + Assert.Equal(1, raised); + + } + + [Fact] + public void Changing_Viewport_Should_Raise_ScrollChanged() + { + var target = new ScrollViewer(); + var raised = 0; + + target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100)); + target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50)); + target.Offset = new Vector(10, 10); + + target.ScrollChanged += (s, e) => + { + Assert.Equal(default, e.ExtentDelta); + Assert.Equal(default, e.OffsetDelta); + Assert.Equal(new Vector(6, 8), e.ViewportDelta); + ++raised; + }; + + target.SetValue(ScrollViewer.ViewportProperty, new Size(56, 58)); + + Assert.Equal(1, raised); + + } + private Control CreateTemplate(ScrollViewer control, INameScope scope) { return new Grid From 99e5625ca63810a9a81710772136244b66b9eed7 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Mon, 6 Apr 2020 17:34:56 -0400 Subject: [PATCH 02/25] Add failing test for cut not working with undo/redo --- .../TextBoxTests.cs | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 6bb3d55799..557c3920cc 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -554,6 +554,28 @@ namespace Avalonia.Controls.UnitTests } } + + [Fact] + public void Cut_Allows_Undo() + { + using (UnitTestApplication.Start(Services)) + { + var target = new TextBox + { + Template = CreateTemplate(), + Text = "0123" + }; + target.SelectionStart = 1; + target.SelectionEnd = 3; + + RaiseKeyEvent(target, Key.X, KeyModifiers.Control); // cut + Assert.True(target.Text == "03"); + + RaiseKeyEvent(target, Key.Z, KeyModifiers.Control); // undo + Assert.True(target.Text == "0123"); + } + } + private static TestServices FocusServices => TestServices.MockThreadingInterface.With( focusManager: new FocusManager(), keyboardDevice: () => new KeyboardDevice(), From d8f74b7e41737f0fe703f5f2f8e3d9d861f48d6f Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Mon, 6 Apr 2020 17:48:12 -0400 Subject: [PATCH 03/25] Cut now triggers an undo --- src/Avalonia.Controls/TextBox.cs | 2 ++ tests/Avalonia.Controls.UnitTests/TextBoxTests.cs | 13 +++++++++++++ 2 files changed, 15 insertions(+) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 680d7e7d2b..02e34675fe 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -473,8 +473,10 @@ namespace Avalonia.Controls { if (!IsPasswordBox) { + _undoRedoHelper.Snapshot(); Copy(); DeleteSelection(); + _undoRedoHelper.Snapshot(); } handled = true; diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 557c3920cc..362ef53461 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -1,10 +1,12 @@ using System; using System.Reactive.Linq; +using System.Threading.Tasks; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.Data; using Avalonia.Input; +using Avalonia.Input.Platform; using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; @@ -567,6 +569,8 @@ namespace Avalonia.Controls.UnitTests }; target.SelectionStart = 1; target.SelectionEnd = 3; + AvaloniaLocator.CurrentMutable + .Bind().ToSingleton(); RaiseKeyEvent(target, Key.X, KeyModifiers.Control); // cut Assert.True(target.Text == "03"); @@ -638,5 +642,14 @@ namespace Avalonia.Controls.UnitTests set { _bar = value; RaisePropertyChanged(); } } } + + private class ClipboardStub : IClipboard // in order to get tests working that use the clipboard + { + public Task GetTextAsync() => Task.FromResult(""); + + public Task SetTextAsync(string text) => Task.CompletedTask; + + public Task ClearAsync() => Task.CompletedTask; + } } } From 3885a69ab156b42edaf409af4b1b9205cad9405d Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Mon, 6 Apr 2020 18:03:35 -0400 Subject: [PATCH 04/25] Add more failing tests for other keys --- .../TextBoxTests.cs | 18 +++++++++++------- 1 file changed, 11 insertions(+), 7 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs index 362ef53461..d2f62cde04 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBoxTests.cs @@ -556,25 +556,29 @@ namespace Avalonia.Controls.UnitTests } } - - [Fact] - public void Cut_Allows_Undo() + [Theory] + [InlineData(Key.X, KeyModifiers.Control)] + [InlineData(Key.Back, KeyModifiers.None)] + [InlineData(Key.Delete, KeyModifiers.None)] + [InlineData(Key.Tab, KeyModifiers.None)] + [InlineData(Key.Enter, KeyModifiers.None)] + public void Keys_Allow_Undo(Key key, KeyModifiers modifiers) { using (UnitTestApplication.Start(Services)) { var target = new TextBox { Template = CreateTemplate(), - Text = "0123" + Text = "0123", + AcceptsReturn = true, + AcceptsTab = true }; target.SelectionStart = 1; target.SelectionEnd = 3; AvaloniaLocator.CurrentMutable .Bind().ToSingleton(); - RaiseKeyEvent(target, Key.X, KeyModifiers.Control); // cut - Assert.True(target.Text == "03"); - + RaiseKeyEvent(target, key, modifiers); RaiseKeyEvent(target, Key.Z, KeyModifiers.Control); // undo Assert.True(target.Text == "0123"); } From 9463e278a545eeb9c213b086565c0da370dbbef1 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Mon, 6 Apr 2020 18:03:54 -0400 Subject: [PATCH 05/25] Fix undo not available for multiple key types --- src/Avalonia.Controls/TextBox.cs | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/Avalonia.Controls/TextBox.cs b/src/Avalonia.Controls/TextBox.cs index 02e34675fe..06624c555f 100644 --- a/src/Avalonia.Controls/TextBox.cs +++ b/src/Avalonia.Controls/TextBox.cs @@ -602,6 +602,7 @@ namespace Avalonia.Controls break; case Key.Back: + _undoRedoHelper.Snapshot(); if (hasWholeWordModifiers && SelectionStart == SelectionEnd) { SetSelectionForControlBackspace(); @@ -625,11 +626,13 @@ namespace Avalonia.Controls CaretIndex -= removedCharacters; SelectionStart = SelectionEnd = CaretIndex; } + _undoRedoHelper.Snapshot(); handled = true; break; case Key.Delete: + _undoRedoHelper.Snapshot(); if (hasWholeWordModifiers && SelectionStart == SelectionEnd) { SetSelectionForControlDelete(); @@ -651,6 +654,7 @@ namespace Avalonia.Controls SetTextInternal(text.Substring(0, caretIndex) + text.Substring(caretIndex + removedCharacters)); } + _undoRedoHelper.Snapshot(); handled = true; break; @@ -658,7 +662,9 @@ namespace Avalonia.Controls case Key.Enter: if (AcceptsReturn) { + _undoRedoHelper.Snapshot(); HandleTextInput(NewLine); + _undoRedoHelper.Snapshot(); handled = true; } @@ -667,7 +673,9 @@ namespace Avalonia.Controls case Key.Tab: if (AcceptsTab) { + _undoRedoHelper.Snapshot(); HandleTextInput("\t"); + _undoRedoHelper.Snapshot(); handled = true; } else From 94a5725a038c4b85572604900ee314b7472e836f Mon Sep 17 00:00:00 2001 From: FoggyFinder Date: Tue, 7 Apr 2020 12:21:02 +0300 Subject: [PATCH 06/25] apply transformation to x with alignment in case of empty text --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 15 +++++---------- 1 file changed, 5 insertions(+), 10 deletions(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 9612fa3d9b..d55af84b14 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -142,21 +142,16 @@ namespace Avalonia.Skia { var rects = GetRects(); - if (index < 0 || index >= rects.Count) - { - var r = rects.LastOrDefault(); - return new Rect(r.X + r.Width, r.Y, 0, _lineHeight); - } - if (rects.Count == 0) { - return new Rect(0, 0, 1, _lineHeight); + var x = TransformX(0, 0, _paint.TextAlign); + return new Rect(x, 0, 0, _lineHeight); } - if (index == rects.Count) + if (index < 0 || index >= rects.Count) { - var lr = rects[rects.Count - 1]; - return new Rect(new Point(lr.X + lr.Width, lr.Y), rects[index - 1].Size); + var r = rects.LastOrDefault(); + return new Rect(r.X + r.Width, r.Y, 0, _lineHeight); } return rects[index]; From 804d70e44868bc9de59cd61bf0403b482fd366ad Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Apr 2020 14:59:54 +0200 Subject: [PATCH 07/25] Add failing test for #2680. --- .../Shapes/PathTests.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs index 05224c2495..5a9ca410e4 100644 --- a/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Shapes/PathTests.cs @@ -1,4 +1,6 @@ using Avalonia.Controls.Shapes; +using Avalonia.Media; +using Avalonia.UnitTests; using Xunit; namespace Avalonia.Controls.UnitTests.Shapes @@ -12,5 +14,21 @@ namespace Avalonia.Controls.UnitTests.Shapes target.Measure(Size.Infinity); } + + [Fact] + public void Subscribes_To_Geometry_Changes() + { + using var app = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface); + + var geometry = new EllipseGeometry { Rect = new Rect(0, 0, 10, 10) }; + var target = new Path { Data = geometry }; + + target.Measure(Size.Infinity); + Assert.True(target.IsMeasureValid); + + geometry.Rect = new Rect(0, 0, 20, 20); + + Assert.False(target.IsMeasureValid); + } } } From 608efb11403be129f73ca0871aa348df1ad99c30 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Tue, 7 Apr 2020 15:06:28 +0200 Subject: [PATCH 08/25] Invalidate path on geometry change. Fixes #2680. --- src/Avalonia.Controls/Shapes/Path.cs | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/src/Avalonia.Controls/Shapes/Path.cs b/src/Avalonia.Controls/Shapes/Path.cs index 84c3ededa5..3fd84c0c7b 100644 --- a/src/Avalonia.Controls/Shapes/Path.cs +++ b/src/Avalonia.Controls/Shapes/Path.cs @@ -1,3 +1,5 @@ +using System; +using Avalonia.Data; using Avalonia.Media; namespace Avalonia.Controls.Shapes @@ -10,6 +12,7 @@ namespace Avalonia.Controls.Shapes static Path() { AffectsGeometry(DataProperty); + DataProperty.Changed.AddClassHandler((o, e) => o.DataChanged(e)); } public Geometry Data @@ -19,5 +22,26 @@ namespace Avalonia.Controls.Shapes } protected override Geometry CreateDefiningGeometry() => Data; + + private void DataChanged(AvaloniaPropertyChangedEventArgs e) + { + var oldGeometry = (Geometry)e.OldValue; + var newGeometry = (Geometry)e.NewValue; + + if (oldGeometry is object) + { + oldGeometry.Changed -= GeometryChanged; + } + + if (newGeometry is object) + { + newGeometry.Changed += GeometryChanged; + } + } + + private void GeometryChanged(object sender, EventArgs e) + { + InvalidateGeometry(); + } } } From 0aa30536613f78e48f57353d547608bbc7ec5a54 Mon Sep 17 00:00:00 2001 From: FoggyFinder Date: Tue, 7 Apr 2020 18:07:17 +0300 Subject: [PATCH 09/25] update --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index d55af84b14..53d55e3f99 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -140,20 +140,20 @@ namespace Avalonia.Skia public Rect HitTestTextPosition(int index) { - var rects = GetRects(); + var lineWidth = (float)_bounds.Width; + var alignmentOffset = TransformX(0, lineWidth, _paint.TextAlign); - if (rects.Count == 0) + if (string.IsNullOrEmpty(Text) || index < 0) { - var x = TransformX(0, 0, _paint.TextAlign); - return new Rect(x, 0, 0, _lineHeight); + return new Rect(alignmentOffset, 0, 0, _lineHeight); } - if (index < 0 || index >= rects.Count) + if (index >= Text.Length) { - var r = rects.LastOrDefault(); - return new Rect(r.X + r.Width, r.Y, 0, _lineHeight); + return new Rect(lineWidth + alignmentOffset, 0, 0, _lineHeight); } + var rects = GetRects(); return rects[index]; } From f1d54de97f48c5a5d4019f00c5429f18bb153040 Mon Sep 17 00:00:00 2001 From: FoggyFinder Date: Wed, 8 Apr 2020 00:13:42 +0300 Subject: [PATCH 10/25] revert last commit, use Text instead of rects in checks --- src/Skia/Avalonia.Skia/FormattedTextImpl.cs | 15 ++++++--------- 1 file changed, 6 insertions(+), 9 deletions(-) diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 53d55e3f99..d0157815a9 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -140,20 +140,17 @@ namespace Avalonia.Skia public Rect HitTestTextPosition(int index) { - var lineWidth = (float)_bounds.Width; - var alignmentOffset = TransformX(0, lineWidth, _paint.TextAlign); - - if (string.IsNullOrEmpty(Text) || index < 0) + if (string.IsNullOrEmpty(Text)) { + var alignmentOffset = TransformX(0, 0, _paint.TextAlign); return new Rect(alignmentOffset, 0, 0, _lineHeight); } - - if (index >= Text.Length) + var rects = GetRects(); + if (index >= Text.Length || index < 0) { - return new Rect(lineWidth + alignmentOffset, 0, 0, _lineHeight); + var r = rects.LastOrDefault(); + return new Rect(r.X + r.Width, r.Y, 0, _lineHeight); } - - var rects = GetRects(); return rects[index]; } From 252733e41f25eaf945aa739a2b426983c07f54bd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Apr 2020 16:04:38 +0200 Subject: [PATCH 11/25] Added failing test for #3760. --- .../Primitives/PopupTests.cs | 43 ++++++++++++++++--- 1 file changed, 36 insertions(+), 7 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index a4c0fa054b..4321d566df 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -298,13 +298,6 @@ namespace Avalonia.Controls.UnitTests.Primitives } } - Window PreparedWindow(object content = null) - { - var w = new Window {Content = content}; - w.ApplyTemplate(); - return w; - } - [Fact] public void DataContextBeginUpdate_Should_Not_Be_Called_For_Controls_That_Dont_Inherit() { @@ -351,6 +344,35 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void StaysOpen_False_Should_Not_Handle_Closing_Click() + { + using (CreateServices()) + { + var window = PreparedWindow(); + var target = new Popup() + { + PlacementTarget = window , + StaysOpen = false, + }; + + target.Open(); + + var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + var e = new PointerPressedEventArgs( + window, + pointer, + window, + default, + 0, + new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed), + KeyModifiers.None); + window.RaiseEvent(e); + + Assert.False(e.Handled); + } + } + private IDisposable CreateServices() { return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: @@ -363,6 +385,13 @@ namespace Avalonia.Controls.UnitTests.Primitives }))); } + private Window PreparedWindow(object content = null) + { + var w = new Window { Content = content }; + w.ApplyTemplate(); + return w; + } + private static IControl PopupContentControlTemplate(PopupContentControl control, INameScope scope) { return new Popup From d4977eaa46ac3bf64bbec197d45282739930bc92 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Apr 2020 16:14:10 +0200 Subject: [PATCH 12/25] Prevent handling closing click in Popup. --- src/Avalonia.Controls/Primitives/Popup.cs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index f069903e05..932800868c 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -404,7 +404,6 @@ namespace Avalonia.Controls.Primitives if (!StaysOpen && !IsChildOrThis((IVisual)e.Source)) { Close(); - e.Handled = true; } } From a98b22c40c671ca5763cea775ce2f64188a6de03 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 8 Apr 2020 18:32:24 +0200 Subject: [PATCH 13/25] Pass close event to Popup.Closed. So that the event can be marked as handled if necessary. Mark the event as handled in `AutoCompleteBox`, `ComboBox` and `DatePicker`, but _not_ `ContextMenu`. Fixes #3760. --- src/Avalonia.Controls/AutoCompleteBox.cs | 7 ++- src/Avalonia.Controls/Calendar/DatePicker.cs | 7 ++- src/Avalonia.Controls/ComboBox.cs | 7 ++- src/Avalonia.Controls/Primitives/Popup.cs | 56 ++++++++++--------- .../Primitives/PopupClosedEventArgs.cs | 33 +++++++++++ .../Primitives/PopupTests.cs | 52 ++++++++++++++--- 6 files changed, 123 insertions(+), 39 deletions(-) create mode 100644 src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs diff --git a/src/Avalonia.Controls/AutoCompleteBox.cs b/src/Avalonia.Controls/AutoCompleteBox.cs index bf177d64cd..9bc7ba9e2f 100644 --- a/src/Avalonia.Controls/AutoCompleteBox.cs +++ b/src/Avalonia.Controls/AutoCompleteBox.cs @@ -1630,7 +1630,7 @@ namespace Avalonia.Controls /// /// The source object. /// The event data. - private void DropDownPopup_Closed(object sender, EventArgs e) + private void DropDownPopup_Closed(object sender, PopupClosedEventArgs e) { // Force the drop down dependency property to be false. if (IsDropDownOpen) @@ -1638,6 +1638,11 @@ namespace Avalonia.Controls IsDropDownOpen = false; } + if (e.CloseEvent is PointerEventArgs pointerEvent) + { + pointerEvent.Handled = true; + } + // Fire the DropDownClosed event if (_popupHasOpened) { diff --git a/src/Avalonia.Controls/Calendar/DatePicker.cs b/src/Avalonia.Controls/Calendar/DatePicker.cs index 07e42c64e4..b4e4ad1452 100644 --- a/src/Avalonia.Controls/Calendar/DatePicker.cs +++ b/src/Avalonia.Controls/Calendar/DatePicker.cs @@ -895,12 +895,17 @@ namespace Avalonia.Controls _ignoreButtonClick = false; } } - private void PopUp_Closed(object sender, EventArgs e) + private void PopUp_Closed(object sender, PopupClosedEventArgs e) { IsDropDownOpen = false; if(!_isPopupClosing) { + if (e.CloseEvent is PointerEventArgs pointerEvent) + { + pointerEvent.Handled = true; + } + _isPopupClosing = true; Threading.Dispatcher.UIThread.InvokeAsync(() => _isPopupClosing = false); } diff --git a/src/Avalonia.Controls/ComboBox.cs b/src/Avalonia.Controls/ComboBox.cs index 4b7d931d80..1daa6a5630 100644 --- a/src/Avalonia.Controls/ComboBox.cs +++ b/src/Avalonia.Controls/ComboBox.cs @@ -242,11 +242,16 @@ namespace Avalonia.Controls } } - private void PopupClosed(object sender, EventArgs e) + private void PopupClosed(object sender, PopupClosedEventArgs e) { _subscriptionsOnOpen?.Dispose(); _subscriptionsOnOpen = null; + if (e.CloseEvent is PointerEventArgs pointerEvent) + { + pointerEvent.Handled = true; + } + if (CanFocus(this)) { Focus(); diff --git a/src/Avalonia.Controls/Primitives/Popup.cs b/src/Avalonia.Controls/Primitives/Popup.cs index 932800868c..66f2153b6c 100644 --- a/src/Avalonia.Controls/Primitives/Popup.cs +++ b/src/Avalonia.Controls/Primitives/Popup.cs @@ -95,7 +95,7 @@ namespace Avalonia.Controls.Primitives /// /// Raised when the popup closes. /// - public event EventHandler? Closed; + public event EventHandler? Closed; /// /// Raised when the popup opens. @@ -270,7 +270,7 @@ namespace Avalonia.Controls.Primitives if (parentPopupRoot?.Parent is Popup popup) { - DeferCleanup(SubscribeToEventHandler(popup, ParentClosed, + DeferCleanup(SubscribeToEventHandler>(popup, ParentClosed, (x, handler) => x.Closed += handler, (x, handler) => x.Closed -= handler)); } @@ -306,28 +306,7 @@ namespace Avalonia.Controls.Primitives /// /// Closes the popup. /// - public void Close() - { - if (_openState is null) - { - using (BeginIgnoringIsOpen()) - { - IsOpen = false; - } - - return; - } - - _openState.Dispose(); - _openState = null; - - using (BeginIgnoringIsOpen()) - { - IsOpen = false; - } - - Closed?.Invoke(this, EventArgs.Empty); - } + public void Close() => CloseCore(null); /// /// Measures the control. @@ -389,21 +368,44 @@ namespace Avalonia.Controls.Primitives } } + private void CloseCore(EventArgs? closeEvent) + { + if (_openState is null) + { + using (BeginIgnoringIsOpen()) + { + IsOpen = false; + } + + return; + } + + _openState.Dispose(); + _openState = null; + + using (BeginIgnoringIsOpen()) + { + IsOpen = false; + } + + Closed?.Invoke(this, new PopupClosedEventArgs(closeEvent)); + } + private void ListenForNonClientClick(RawInputEventArgs e) { var mouse = e as RawPointerEventArgs; if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown) { - Close(); + CloseCore(e); } } private void PointerPressedOutside(object sender, PointerPressedEventArgs e) { - if (!StaysOpen && !IsChildOrThis((IVisual)e.Source)) + if (!StaysOpen && e.Source is IVisual v && !IsChildOrThis(v)) { - Close(); + CloseCore(e); } } diff --git a/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs b/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs new file mode 100644 index 0000000000..c51543438c --- /dev/null +++ b/src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs @@ -0,0 +1,33 @@ +using System; +using Avalonia.Interactivity; + +#nullable enable + +namespace Avalonia.Controls.Primitives +{ + /// + /// Holds data for the event. + /// + public class PopupClosedEventArgs : EventArgs + { + /// + /// Initializes a new instance of the class. + /// + /// + public PopupClosedEventArgs(EventArgs? closeEvent) + { + CloseEvent = closeEvent; + } + + /// + /// Gets the event that closed the popup, if any. + /// + /// + /// If is false, then this property will hold details of the + /// interaction that caused the popup to close if the close was caused by e.g. a pointer press + /// outside the popup. It can be used to mark the event as handled if the event should not + /// be propagated. + /// + public EventArgs? CloseEvent { get; } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index 4321d566df..a479317f3d 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -358,21 +358,42 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Open(); - var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); - var e = new PointerPressedEventArgs( - window, - pointer, - window, - default, - 0, - new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed), - KeyModifiers.None); + var e = CreatePointerPressedEventArgs(window); window.RaiseEvent(e); Assert.False(e.Handled); } } + [Fact] + public void Should_Pass_Closing_Click_To_Closed_Event() + { + using (CreateServices()) + { + var window = PreparedWindow(); + var target = new Popup() + { + PlacementTarget = window, + StaysOpen = false, + }; + + target.Open(); + + var press = CreatePointerPressedEventArgs(window); + var raised = 0; + + target.Closed += (s, e) => + { + Assert.Same(press, e.CloseEvent); + ++raised; + }; + + window.RaiseEvent(press); + + Assert.Equal(1, raised); + } + } + private IDisposable CreateServices() { return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: @@ -385,6 +406,19 @@ namespace Avalonia.Controls.UnitTests.Primitives }))); } + private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source) + { + var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true); + return new PointerPressedEventArgs( + source, + pointer, + source, + default, + 0, + new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed), + KeyModifiers.None); + } + private Window PreparedWindow(object content = null) { var w = new Window { Content = content }; From faec3ae67f23e8f3d90cf1ac166c88b424b6215e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Apr 2020 09:03:32 +0200 Subject: [PATCH 14/25] Added NonVirtualizingStackLayout. Required fixing/implementing a bunch of infrastructure around non-virtualizing layouts. --- src/Avalonia.Layout/AttachedLayout.cs | 104 +++++- src/Avalonia.Layout/LayoutContextAdapter.cs | 45 +++ src/Avalonia.Layout/NonVirtualizingLayout.cs | 36 +- .../NonVirtualizingLayoutContext.cs | 17 + .../NonVirtualizingStackLayout.cs | 160 +++++++++ src/Avalonia.Layout/StackLayout.cs | 8 +- src/Avalonia.Layout/UniformGridLayout.cs | 8 +- .../VirtualLayoutContextAdapter.cs | 42 +++ src/Avalonia.Layout/VirtualizingLayout.cs | 36 +- .../VirtualizingLayoutContext.cs | 5 + .../NonVirtualizingStackLayoutTests.cs | 335 ++++++++++++++++++ 11 files changed, 728 insertions(+), 68 deletions(-) create mode 100644 src/Avalonia.Layout/LayoutContextAdapter.cs create mode 100644 src/Avalonia.Layout/NonVirtualizingStackLayout.cs create mode 100644 src/Avalonia.Layout/VirtualLayoutContextAdapter.cs create mode 100644 tests/Avalonia.Layout.UnitTests/NonVirtualizingStackLayoutTests.cs diff --git a/src/Avalonia.Layout/AttachedLayout.cs b/src/Avalonia.Layout/AttachedLayout.cs index 5622731a7c..d22566442a 100644 --- a/src/Avalonia.Layout/AttachedLayout.cs +++ b/src/Avalonia.Layout/AttachedLayout.cs @@ -46,7 +46,23 @@ namespace Avalonia.Layout /// to provide the behavior for /// this method in a derived class. /// - public abstract void InitializeForContext(LayoutContext context); + public void InitializeForContext(LayoutContext context) + { + if (this is VirtualizingLayout virtualizingLayout) + { + var virtualizingContext = GetVirtualizingLayoutContext(context); + virtualizingLayout.InitializeForContextCore(virtualizingContext); + } + else if (this is NonVirtualizingLayout nonVirtualizingLayout) + { + var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context); + nonVirtualizingLayout.InitializeForContextCore(nonVirtualizingContext); + } + else + { + throw new NotSupportedException(); + } + } /// /// Removes any state the layout previously stored on the ILayoutable container. @@ -55,7 +71,23 @@ namespace Avalonia.Layout /// The context object that facilitates communication between the layout and its host /// container. /// - public abstract void UninitializeForContext(LayoutContext context); + public void UninitializeForContext(LayoutContext context) + { + if (this is VirtualizingLayout virtualizingLayout) + { + var virtualizingContext = GetVirtualizingLayoutContext(context); + virtualizingLayout.UninitializeForContextCore(virtualizingContext); + } + else if (this is NonVirtualizingLayout nonVirtualizingLayout) + { + var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context); + nonVirtualizingLayout.UninitializeForContextCore(nonVirtualizingContext); + } + else + { + throw new NotSupportedException(); + } + } /// /// Suggests a DesiredSize for a container element. A container element that supports @@ -73,7 +105,23 @@ namespace Avalonia.Layout /// if scrolling or other resize behavior is possible in that particular container. /// /// - public abstract Size Measure(LayoutContext context, Size availableSize); + public Size Measure(LayoutContext context, Size availableSize) + { + if (this is VirtualizingLayout virtualizingLayout) + { + var virtualizingContext = GetVirtualizingLayoutContext(context); + return virtualizingLayout.MeasureOverride(virtualizingContext, availableSize); + } + else if (this is NonVirtualizingLayout nonVirtualizingLayout) + { + var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context); + return nonVirtualizingLayout.MeasureOverride(nonVirtualizingContext, availableSize); + } + else + { + throw new NotSupportedException(); + } + } /// /// Positions child elements and determines a size for a container UIElement. Container @@ -88,7 +136,23 @@ namespace Avalonia.Layout /// The final size that the container computes for the child in layout. /// /// The actual size that is used after the element is arranged in layout. - public abstract Size Arrange(LayoutContext context, Size finalSize); + public Size Arrange(LayoutContext context, Size finalSize) + { + if (this is VirtualizingLayout virtualizingLayout) + { + var virtualizingContext = GetVirtualizingLayoutContext(context); + return virtualizingLayout.ArrangeOverride(virtualizingContext, finalSize); + } + else if (this is NonVirtualizingLayout nonVirtualizingLayout) + { + var nonVirtualizingContext = GetNonVirtualizingLayoutContext(context); + return nonVirtualizingLayout.ArrangeOverride(nonVirtualizingContext, finalSize); + } + else + { + throw new NotSupportedException(); + } + } /// /// Invalidates the measurement state (layout) for all ILayoutable containers that reference @@ -102,5 +166,37 @@ namespace Avalonia.Layout /// occurs asynchronously. /// protected void InvalidateArrange() => ArrangeInvalidated?.Invoke(this, EventArgs.Empty); + + private VirtualizingLayoutContext GetVirtualizingLayoutContext(LayoutContext context) + { + if (context is VirtualizingLayoutContext virtualizingContext) + { + return virtualizingContext; + } + else if (context is NonVirtualizingLayoutContext nonVirtualizingContext) + { + return nonVirtualizingContext.GetVirtualizingContextAdapter(); + } + else + { + throw new NotSupportedException(); + } + } + + private NonVirtualizingLayoutContext GetNonVirtualizingLayoutContext(LayoutContext context) + { + if (context is NonVirtualizingLayoutContext nonVirtualizingContext) + { + return nonVirtualizingContext; + } + else if (context is VirtualizingLayoutContext virtualizingContext) + { + return virtualizingContext.GetNonVirtualizingContextAdapter(); + } + else + { + throw new NotSupportedException(); + } + } } } diff --git a/src/Avalonia.Layout/LayoutContextAdapter.cs b/src/Avalonia.Layout/LayoutContextAdapter.cs new file mode 100644 index 0000000000..695866df94 --- /dev/null +++ b/src/Avalonia.Layout/LayoutContextAdapter.cs @@ -0,0 +1,45 @@ +// This source file is adapted from the WinUI project. +// (https://github.com/microsoft/microsoft-ui-xaml) +// +// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. + +using System; + +namespace Avalonia.Layout +{ + internal class LayoutContextAdapter : VirtualizingLayoutContext + { + private readonly NonVirtualizingLayoutContext _nonVirtualizingContext; + + public LayoutContextAdapter(NonVirtualizingLayoutContext nonVirtualizingContext) + { + _nonVirtualizingContext = nonVirtualizingContext; + } + + protected override object LayoutStateCore + { + get => _nonVirtualizingContext.LayoutState; + set => _nonVirtualizingContext.LayoutState = value; + } + + protected override Point LayoutOriginCore + { + get => default; + set + { + if (value != default) + { + throw new InvalidOperationException("LayoutOrigin must be at (0,0) when RealizationRect is infinite sized."); + } + } + } + + protected override Rect RealizationRectCore() => new Rect(Size.Infinity); + + protected override int ItemCountCore() => _nonVirtualizingContext.Children.Count; + protected override object GetItemAtCore(int index) => _nonVirtualizingContext.Children[index]; + protected override ILayoutable GetOrCreateElementAtCore(int index, ElementRealizationOptions options) => + _nonVirtualizingContext.Children[index]; + protected override void RecycleElementCore(ILayoutable element) { } + } +} diff --git a/src/Avalonia.Layout/NonVirtualizingLayout.cs b/src/Avalonia.Layout/NonVirtualizingLayout.cs index 5d27ba9199..fb6b0dd4c9 100644 --- a/src/Avalonia.Layout/NonVirtualizingLayout.cs +++ b/src/Avalonia.Layout/NonVirtualizingLayout.cs @@ -17,30 +17,6 @@ namespace Avalonia.Layout /// public abstract class NonVirtualizingLayout : AttachedLayout { - /// - public sealed override void InitializeForContext(LayoutContext context) - { - InitializeForContextCore((NonVirtualizingLayoutContext)context); - } - - /// - public sealed override void UninitializeForContext(LayoutContext context) - { - UninitializeForContextCore((NonVirtualizingLayoutContext)context); - } - - /// - public sealed override Size Measure(LayoutContext context, Size availableSize) - { - return MeasureOverride((NonVirtualizingLayoutContext)context, availableSize); - } - - /// - public sealed override Size Arrange(LayoutContext context, Size finalSize) - { - return ArrangeOverride((NonVirtualizingLayoutContext)context, finalSize); - } - /// /// When overridden in a derived class, initializes any per-container state the layout /// requires when it is attached to an ILayoutable container. @@ -49,7 +25,7 @@ namespace Avalonia.Layout /// The context object that facilitates communication between the layout and its host /// container. /// - protected virtual void InitializeForContextCore(LayoutContext context) + protected internal virtual void InitializeForContextCore(LayoutContext context) { } @@ -61,7 +37,7 @@ namespace Avalonia.Layout /// The context object that facilitates communication between the layout and its host /// container. /// - protected virtual void UninitializeForContextCore(LayoutContext context) + protected internal virtual void UninitializeForContextCore(LayoutContext context) { } @@ -83,7 +59,9 @@ namespace Avalonia.Layout /// of the allocated sizes for child objects or based on other considerations such as a /// fixed container size. /// - protected abstract Size MeasureOverride(NonVirtualizingLayoutContext context, Size availableSize); + protected internal abstract Size MeasureOverride( + NonVirtualizingLayoutContext context, + Size availableSize); /// /// When implemented in a derived class, provides the behavior for the "Arrange" pass of @@ -98,6 +76,8 @@ namespace Avalonia.Layout /// its children. /// /// The actual size that is used after the element is arranged in layout. - protected virtual Size ArrangeOverride(NonVirtualizingLayoutContext context, Size finalSize) => finalSize; + protected internal virtual Size ArrangeOverride( + NonVirtualizingLayoutContext context, + Size finalSize) => finalSize; } } diff --git a/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs index d3dec83e9b..cef551f32e 100644 --- a/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs +++ b/src/Avalonia.Layout/NonVirtualizingLayoutContext.cs @@ -3,6 +3,8 @@ // // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. +using System.Collections.Generic; + namespace Avalonia.Layout { /// @@ -10,5 +12,20 @@ namespace Avalonia.Layout /// public abstract class NonVirtualizingLayoutContext : LayoutContext { + private VirtualizingLayoutContext _contextAdapter; + + /// + /// Gets the collection of child controls from the container that provides the context. + /// + public IReadOnlyList Children => ChildrenCore; + + /// + /// Implements the behavior for getting the return value of in a + /// derived or custom . + /// + protected abstract IReadOnlyList ChildrenCore { get; } + + internal VirtualizingLayoutContext GetVirtualizingContextAdapter() => + _contextAdapter ?? (_contextAdapter = new LayoutContextAdapter(this)); } } diff --git a/src/Avalonia.Layout/NonVirtualizingStackLayout.cs b/src/Avalonia.Layout/NonVirtualizingStackLayout.cs new file mode 100644 index 0000000000..0b730315e1 --- /dev/null +++ b/src/Avalonia.Layout/NonVirtualizingStackLayout.cs @@ -0,0 +1,160 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Data; + +namespace Avalonia.Layout +{ + public class NonVirtualizingStackLayout : NonVirtualizingLayout + { + /// + /// Defines the property. + /// + public static readonly StyledProperty OrientationProperty = + StackLayout.OrientationProperty.AddOwner(); + + /// + /// Defines the property. + /// + public static readonly StyledProperty SpacingProperty = + StackLayout.SpacingProperty.AddOwner(); + + /// + /// Gets or sets the axis along which items are laid out. + /// + /// + /// One of the enumeration values that specifies the axis along which items are laid out. + /// The default is Vertical. + /// + public Orientation Orientation + { + get => GetValue(OrientationProperty); + set => SetValue(OrientationProperty, value); + } + + /// + /// Gets or sets a uniform distance (in pixels) between stacked items. It is applied in the + /// direction of the StackLayout's Orientation. + /// + public double Spacing + { + get => GetValue(SpacingProperty); + set => SetValue(SpacingProperty, value); + } + + protected internal override Size MeasureOverride( + NonVirtualizingLayoutContext context, + Size availableSize) + { + var extentU = 0.0; + var extentV = 0.0; + var childCount = context.Children.Count; + var isVertical = Orientation == Orientation.Vertical; + var spacing = Spacing; + var constraint = isVertical ? + availableSize.WithHeight(double.PositiveInfinity) : + availableSize.WithWidth(double.PositiveInfinity); + + for (var i = 0; i < childCount; ++i) + { + var element = context.Children[i]; + + if (!element.IsVisible) + { + continue; + } + + element.Measure(constraint); + + if (isVertical) + { + extentU += element.DesiredSize.Height; + extentV = Math.Max(extentV, element.DesiredSize.Width); + } + else + { + extentU += element.DesiredSize.Width; + extentV = Math.Max(extentV, element.DesiredSize.Height); + } + + if (i < childCount - 1) + { + extentU += spacing; + } + } + + return isVertical ? new Size(extentV, extentU) : new Size(extentU, extentV); + } + + protected internal override Size ArrangeOverride( + NonVirtualizingLayoutContext context, + Size finalSize) + { + var u = 0.0; + var childCount = context.Children.Count; + var isVertical = Orientation == Orientation.Vertical; + var spacing = Spacing; + var bounds = new Rect(); + + for (var i = 0; i < childCount; ++i) + { + var element = context.Children[i]; + + if (!element.IsVisible) + { + continue; + } + + bounds = isVertical ? + LayoutVertical(element, u, finalSize) : + LayoutHorizontal(element, u, finalSize); + element.Arrange(bounds); + u = (isVertical ? bounds.Bottom : bounds.Right) + spacing; + } + + return new Size(bounds.Right, bounds.Bottom); + } + + private static Rect LayoutVertical(ILayoutable element, double y, Size constraint) + { + var x = 0.0; + var width = element.DesiredSize.Width; + + switch (element.HorizontalAlignment) + { + case HorizontalAlignment.Center: + x += (constraint.Width - element.DesiredSize.Width) / 2; + break; + case HorizontalAlignment.Right: + x += constraint.Width - element.DesiredSize.Width; + break; + case HorizontalAlignment.Stretch: + width = constraint.Width; + break; + } + + return new Rect(x, y, width, element.DesiredSize.Height); + } + + private static Rect LayoutHorizontal(ILayoutable element, double x, Size constraint) + { + var y = 0.0; + var height = element.DesiredSize.Height; + + switch (element.VerticalAlignment) + { + case VerticalAlignment.Center: + y += (constraint.Height - element.DesiredSize.Height) / 2; + break; + case VerticalAlignment.Bottom: + y += constraint.Height - element.DesiredSize.Height; + break; + case VerticalAlignment.Stretch: + height = constraint.Height; + break; + } + + return new Rect(x, y, element.DesiredSize.Width, height); + } + } +} diff --git a/src/Avalonia.Layout/StackLayout.cs b/src/Avalonia.Layout/StackLayout.cs index e8ad49e9b9..9b8eb4814e 100644 --- a/src/Avalonia.Layout/StackLayout.cs +++ b/src/Avalonia.Layout/StackLayout.cs @@ -234,7 +234,7 @@ namespace Avalonia.Layout return new FlowLayoutAnchorInfo { Index = anchorIndex, Offset = offset, }; } - protected override void InitializeForContextCore(VirtualizingLayoutContext context) + protected internal override void InitializeForContextCore(VirtualizingLayoutContext context) { var state = context.LayoutState; var stackState = state as StackLayoutState; @@ -254,13 +254,13 @@ namespace Avalonia.Layout stackState.InitializeForContext(context, this); } - protected override void UninitializeForContextCore(VirtualizingLayoutContext context) + protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context) { var stackState = (StackLayoutState)context.LayoutState; stackState.UninitializeForContext(context); } - protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) + protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) { var desiredSize = GetFlowAlgorithm(context).Measure( availableSize, @@ -275,7 +275,7 @@ namespace Avalonia.Layout return new Size(desiredSize.Width, desiredSize.Height); } - protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) + protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) { var value = GetFlowAlgorithm(context).Arrange( finalSize, diff --git a/src/Avalonia.Layout/UniformGridLayout.cs b/src/Avalonia.Layout/UniformGridLayout.cs index 54c3ccbb90..ee9cff4a01 100644 --- a/src/Avalonia.Layout/UniformGridLayout.cs +++ b/src/Avalonia.Layout/UniformGridLayout.cs @@ -392,7 +392,7 @@ namespace Avalonia.Layout { } - protected override void InitializeForContextCore(VirtualizingLayoutContext context) + protected internal override void InitializeForContextCore(VirtualizingLayoutContext context) { var state = context.LayoutState; var gridState = state as UniformGridLayoutState; @@ -412,13 +412,13 @@ namespace Avalonia.Layout gridState.InitializeForContext(context, this); } - protected override void UninitializeForContextCore(VirtualizingLayoutContext context) + protected internal override void UninitializeForContextCore(VirtualizingLayoutContext context) { var gridState = (UniformGridLayoutState)context.LayoutState; gridState.UninitializeForContext(context); } - protected override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) + protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize) { // Set the width and height on the grid state. If the user already set them then use the preset. // If not, we have to measure the first element and get back a size which we're going to be using for the rest of the items. @@ -442,7 +442,7 @@ namespace Avalonia.Layout return new Size(desiredSize.Width, desiredSize.Height); } - protected override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) + protected internal override Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) { var value = GetFlowAlgorithm(context).Arrange( finalSize, diff --git a/src/Avalonia.Layout/VirtualLayoutContextAdapter.cs b/src/Avalonia.Layout/VirtualLayoutContextAdapter.cs new file mode 100644 index 0000000000..80ccee2114 --- /dev/null +++ b/src/Avalonia.Layout/VirtualLayoutContextAdapter.cs @@ -0,0 +1,42 @@ +using System.Collections; +using System.Collections.Generic; + +namespace Avalonia.Layout +{ + public class VirtualLayoutContextAdapter : NonVirtualizingLayoutContext + { + private readonly VirtualizingLayoutContext _virtualizingContext; + private ChildrenCollection _children; + + public VirtualLayoutContextAdapter(VirtualizingLayoutContext virtualizingContext) + { + _virtualizingContext = virtualizingContext; + } + + protected override object LayoutStateCore + { + get => _virtualizingContext.LayoutState; + set => _virtualizingContext.LayoutState = value; + } + + protected override IReadOnlyList ChildrenCore => + _children ?? (_children = new ChildrenCollection(_virtualizingContext)); + + private class ChildrenCollection : IReadOnlyList + { + private readonly VirtualizingLayoutContext _context; + public ChildrenCollection(VirtualizingLayoutContext context) => _context = context; + public ILayoutable this[int index] => _context.GetOrCreateElementAt(index); + public int Count => _context.ItemCount; + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + + public IEnumerator GetEnumerator() + { + for (var i = 0; i < Count; ++i) + { + yield return this[i]; + } + } + } + } +} diff --git a/src/Avalonia.Layout/VirtualizingLayout.cs b/src/Avalonia.Layout/VirtualizingLayout.cs index 4c601175f3..15c7749dfe 100644 --- a/src/Avalonia.Layout/VirtualizingLayout.cs +++ b/src/Avalonia.Layout/VirtualizingLayout.cs @@ -19,30 +19,6 @@ namespace Avalonia.Layout /// public abstract class VirtualizingLayout : AttachedLayout { - /// - public sealed override void InitializeForContext(LayoutContext context) - { - InitializeForContextCore((VirtualizingLayoutContext)context); - } - - /// - public sealed override void UninitializeForContext(LayoutContext context) - { - UninitializeForContextCore((VirtualizingLayoutContext)context); - } - - /// - public sealed override Size Measure(LayoutContext context, Size availableSize) - { - return MeasureOverride((VirtualizingLayoutContext)context, availableSize); - } - - /// - public sealed override Size Arrange(LayoutContext context, Size finalSize) - { - return ArrangeOverride((VirtualizingLayoutContext)context, finalSize); - } - /// /// Notifies the layout when the data collection assigned to the container element (Items) /// has changed. @@ -70,7 +46,7 @@ namespace Avalonia.Layout /// The context object that facilitates communication between the layout and its host /// container. /// - protected virtual void InitializeForContextCore(VirtualizingLayoutContext context) + protected internal virtual void InitializeForContextCore(VirtualizingLayoutContext context) { } @@ -82,7 +58,7 @@ namespace Avalonia.Layout /// The context object that facilitates communication between the layout and its host /// container. /// - protected virtual void UninitializeForContextCore(VirtualizingLayoutContext context) + protected internal virtual void UninitializeForContextCore(VirtualizingLayoutContext context) { } @@ -104,7 +80,9 @@ namespace Avalonia.Layout /// of the allocated sizes for child objects or based on other considerations such as a /// fixed container size. /// - protected abstract Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize); + protected internal abstract Size MeasureOverride( + VirtualizingLayoutContext context, + Size availableSize); /// /// When implemented in a derived class, provides the behavior for the "Arrange" pass of @@ -119,7 +97,9 @@ namespace Avalonia.Layout /// its children. /// /// The actual size that is used after the element is arranged in layout. - protected virtual Size ArrangeOverride(VirtualizingLayoutContext context, Size finalSize) => finalSize; + protected internal virtual Size ArrangeOverride( + VirtualizingLayoutContext context, + Size finalSize) => finalSize; /// /// Notifies the layout when the data collection assigned to the container element (Items) diff --git a/src/Avalonia.Layout/VirtualizingLayoutContext.cs b/src/Avalonia.Layout/VirtualizingLayoutContext.cs index 980daec2eb..079b91a90f 100644 --- a/src/Avalonia.Layout/VirtualizingLayoutContext.cs +++ b/src/Avalonia.Layout/VirtualizingLayoutContext.cs @@ -43,6 +43,8 @@ namespace Avalonia.Layout /// public abstract class VirtualizingLayoutContext : LayoutContext { + private NonVirtualizingLayoutContext _contextAdapter; + /// /// Gets the number of items in the data. /// @@ -186,5 +188,8 @@ namespace Avalonia.Layout /// /// The element to clear. protected abstract void RecycleElementCore(ILayoutable element); + + internal NonVirtualizingLayoutContext GetNonVirtualizingContextAdapter() => + _contextAdapter ?? (_contextAdapter = new VirtualLayoutContextAdapter(this)); } } diff --git a/tests/Avalonia.Layout.UnitTests/NonVirtualizingStackLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/NonVirtualizingStackLayoutTests.cs new file mode 100644 index 0000000000..a7b378c322 --- /dev/null +++ b/tests/Avalonia.Layout.UnitTests/NonVirtualizingStackLayoutTests.cs @@ -0,0 +1,335 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Controls; +using Xunit; + +namespace Avalonia.Layout.UnitTests +{ + public class NonVirtualizingStackLayoutTests + { + [Fact] + public void Lays_Out_Children_Vertically() + { + var target = new NonVirtualizingStackLayout { Orientation = Orientation.Vertical }; + var context = CreateContext(new[] + { + new Border { Height = 20, Width = 120 }, + new Border { Height = 30 }, + new Border { Height = 50 }, + }); + + var desiredSize = target.Measure(context, Size.Infinity); + var arrangeSize = target.Arrange(context, desiredSize); + + Assert.Equal(new Size(120, 100), desiredSize); + Assert.Equal(new Size(120, 100), arrangeSize); + Assert.Equal(new Rect(0, 0, 120, 20), context.Children[0].Bounds); + Assert.Equal(new Rect(0, 20, 120, 30), context.Children[1].Bounds); + Assert.Equal(new Rect(0, 50, 120, 50), context.Children[2].Bounds); + } + + [Fact] + public void Lays_Out_Children_Horizontally() + { + var target = new NonVirtualizingStackLayout { Orientation = Orientation.Horizontal }; + var context = CreateContext(new[] + { + new Border { Width = 20, Height = 120 }, + new Border { Width = 30 }, + new Border { Width = 50 }, + }); + + var desiredSize = target.Measure(context, Size.Infinity); + var arrangeSize = target.Arrange(context, desiredSize); + + Assert.Equal(new Size(100, 120), desiredSize); + Assert.Equal(new Size(100, 120), arrangeSize); + Assert.Equal(new Rect(0, 0, 20, 120), context.Children[0].Bounds); + Assert.Equal(new Rect(20, 0, 30, 120), context.Children[1].Bounds); + Assert.Equal(new Rect(50, 0, 50, 120), context.Children[2].Bounds); + } + + [Fact] + public void Lays_Out_Children_Vertically_With_Spacing() + { + var target = new NonVirtualizingStackLayout + { + Orientation = Orientation.Vertical, + Spacing = 10, + }; + + var context = CreateContext(new[] + { + new Border { Height = 20, Width = 120 }, + new Border { Height = 30 }, + new Border { Height = 50 }, + }); + + var desiredSize = target.Measure(context, Size.Infinity); + var arrangeSize = target.Arrange(context, desiredSize); + + Assert.Equal(new Size(120, 120), desiredSize); + Assert.Equal(new Size(120, 120), arrangeSize); + Assert.Equal(new Rect(0, 0, 120, 20), context.Children[0].Bounds); + Assert.Equal(new Rect(0, 30, 120, 30), context.Children[1].Bounds); + Assert.Equal(new Rect(0, 70, 120, 50), context.Children[2].Bounds); + } + + [Fact] + public void Lays_Out_Children_Horizontally_With_Spacing() + { + var target = new NonVirtualizingStackLayout + { + Orientation = Orientation.Horizontal, + Spacing = 10, + }; + + var context = CreateContext(new[] + { + new Border { Width = 20, Height = 120 }, + new Border { Width = 30 }, + new Border { Width = 50 }, + }); + + var desiredSize = target.Measure(context, Size.Infinity); + var arrangeSize = target.Arrange(context, desiredSize); + + Assert.Equal(new Size(120, 120), desiredSize); + Assert.Equal(new Size(120, 120), arrangeSize); + Assert.Equal(new Rect(0, 0, 20, 120), context.Children[0].Bounds); + Assert.Equal(new Rect(30, 0, 30, 120), context.Children[1].Bounds); + Assert.Equal(new Rect(70, 0, 50, 120), context.Children[2].Bounds); + } + + [Fact] + public void Arranges_Vertical_Children_With_Correct_Bounds() + { + var target = new NonVirtualizingStackLayout + { + Orientation = Orientation.Vertical + }; + + var context = CreateContext(new[] + { + new TestControl + { + HorizontalAlignment = HorizontalAlignment.Left, + MeasureSize = new Size(50, 10), + }, + new TestControl + { + HorizontalAlignment = HorizontalAlignment.Left, + MeasureSize = new Size(150, 10), + }, + new TestControl + { + HorizontalAlignment = HorizontalAlignment.Center, + MeasureSize = new Size(50, 10), + }, + new TestControl + { + HorizontalAlignment = HorizontalAlignment.Center, + MeasureSize = new Size(150, 10), + }, + new TestControl + { + HorizontalAlignment = HorizontalAlignment.Right, + MeasureSize = new Size(50, 10), + }, + new TestControl + { + HorizontalAlignment = HorizontalAlignment.Right, + MeasureSize = new Size(150, 10), + }, + new TestControl + { + HorizontalAlignment = HorizontalAlignment.Stretch, + MeasureSize = new Size(50, 10), + }, + new TestControl + { + HorizontalAlignment = HorizontalAlignment.Stretch, + MeasureSize = new Size(150, 10), + }, + }); + + var desiredSize = target.Measure(context, new Size(100, 150)); + Assert.Equal(new Size(100, 80), desiredSize); + + target.Arrange(context, desiredSize); + + var bounds = context.Children.Select(x => x.Bounds).ToArray(); + + Assert.Equal( + new[] + { + new Rect(0, 0, 50, 10), + new Rect(0, 10, 100, 10), + new Rect(25, 20, 50, 10), + new Rect(0, 30, 100, 10), + new Rect(50, 40, 50, 10), + new Rect(0, 50, 100, 10), + new Rect(0, 60, 100, 10), + new Rect(0, 70, 100, 10), + + }, bounds); + } + + [Fact] + public void Arranges_Horizontal_Children_With_Correct_Bounds() + { + var target = new NonVirtualizingStackLayout + { + Orientation = Orientation.Horizontal + }; + + var context = CreateContext(new[] + { + new TestControl + { + VerticalAlignment = VerticalAlignment.Top, + MeasureSize = new Size(10, 50), + }, + new TestControl + { + VerticalAlignment = VerticalAlignment.Top, + MeasureSize = new Size(10, 150), + }, + new TestControl + { + VerticalAlignment = VerticalAlignment.Center, + MeasureSize = new Size(10, 50), + }, + new TestControl + { + VerticalAlignment = VerticalAlignment.Center, + MeasureSize = new Size(10, 150), + }, + new TestControl + { + VerticalAlignment = VerticalAlignment.Bottom, + MeasureSize = new Size(10, 50), + }, + new TestControl + { + VerticalAlignment = VerticalAlignment.Bottom, + MeasureSize = new Size(10, 150), + }, + new TestControl + { + VerticalAlignment = VerticalAlignment.Stretch, + MeasureSize = new Size(10, 50), + }, + new TestControl + { + VerticalAlignment = VerticalAlignment.Stretch, + MeasureSize = new Size(10, 150), + }, + }); + + var desiredSize = target.Measure(context, new Size(150, 100)); + Assert.Equal(new Size(80, 100), desiredSize); + + target.Arrange(context, desiredSize); + + var bounds = context.Children.Select(x => x.Bounds).ToArray(); + + Assert.Equal( + new[] + { + new Rect(0, 0, 10, 50), + new Rect(10, 0, 10, 100), + new Rect(20, 25, 10, 50), + new Rect(30, 0, 10, 100), + new Rect(40, 50, 10, 50), + new Rect(50, 0, 10, 100), + new Rect(60, 0, 10, 100), + new Rect(70, 0, 10, 100), + }, bounds); + } + + [Theory] + [InlineData(Orientation.Horizontal)] + [InlineData(Orientation.Vertical)] + public void Spacing_Not_Added_For_Invisible_Children(Orientation orientation) + { + var targetThreeChildrenOneInvisble = new NonVirtualizingStackLayout + { + Orientation = orientation, + Spacing = 40, + }; + + var contextThreeChildrenOneInvisble = CreateContext(new[] + { + new StackPanel { Width = 10, Height= 10, IsVisible = false }, + new StackPanel { Width = 10, Height= 10 }, + new StackPanel { Width = 10, Height= 10 }, + }); + + var targetTwoChildrenNoneInvisible = new NonVirtualizingStackLayout + { + Spacing = 40, + Orientation = orientation, + }; + + var contextTwoChildrenNoneInvisible = CreateContext(new[] + { + new StackPanel { Width = 10, Height = 10 }, + new StackPanel { Width = 10, Height = 10 } + }); + + var desiredSize1 = targetThreeChildrenOneInvisble.Measure(contextThreeChildrenOneInvisble, Size.Infinity); + var desiredSize2 = targetTwoChildrenNoneInvisible.Measure(contextTwoChildrenNoneInvisible, Size.Infinity); + + Assert.Equal(desiredSize2, desiredSize1); + } + + [Theory] + [InlineData(Orientation.Horizontal)] + [InlineData(Orientation.Vertical)] + public void Only_Arrange_Visible_Children(Orientation orientation) + { + var hiddenPanel = new Panel { Width = 10, Height = 10, IsVisible = false }; + var panel = new Panel { Width = 10, Height = 10 }; + + var target = new NonVirtualizingStackLayout + { + Spacing = 40, + Orientation = orientation, + }; + + var context = CreateContext(new[] + { + hiddenPanel, + panel + }); + + var desiredSize = target.Measure(context, Size.Infinity); + var arrangeSize = target.Arrange(context, desiredSize); + Assert.Equal(new Size(10, 10), arrangeSize); + } + + private NonVirtualizingLayoutContext CreateContext(Control[] children) + { + return new TestLayoutContext(children); + } + + private class TestLayoutContext : NonVirtualizingLayoutContext + { + public TestLayoutContext(Control[] children) => ChildrenCore = children; + protected override IReadOnlyList ChildrenCore { get; } + } + + private class TestControl : Control + { + public Size MeasureConstraint { get; private set; } + public Size MeasureSize { get; set; } + + protected override Size MeasureOverride(Size availableSize) + { + MeasureConstraint = availableSize; + return MeasureSize; + } + } + } +} From f8c8098ad57e57bcf7f518be41ffc752fce13d74 Mon Sep 17 00:00:00 2001 From: Dariusz Komosinski Date: Sat, 11 Apr 2020 18:36:19 +0200 Subject: [PATCH 15/25] Pool collections used by VisualNode. Ensure that when cloning we initialize collection sizes to avoid reallocations. --- .../Rendering/SceneGraph/Scene.cs | 17 ++++++-- .../Rendering/SceneGraph/SceneLayers.cs | 20 +++++++-- .../Rendering/SceneGraph/VisualNode.cs | 43 +++++++++++++++---- 3 files changed, 63 insertions(+), 17 deletions(-) diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index b4bf4c799a..b2e827fa26 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -12,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph /// public class Scene : IDisposable { - private Dictionary _index; + private readonly Dictionary _index; /// /// Initializes a new instance of the class. @@ -83,7 +83,7 @@ namespace Avalonia.Rendering.SceneGraph /// The cloned scene. public Scene CloneScene() { - var index = new Dictionary(); + var index = new Dictionary(_index.Count); var root = Clone((VisualNode)Root, null, index); var result = new Scene(root, index, Layers.Clone(), Generation + 1) @@ -162,9 +162,18 @@ namespace Avalonia.Rendering.SceneGraph index.Add(result.Visual, result); - foreach (var child in source.Children) + int childCount = source.Children.Count; + + if (childCount > 0) { - result.AddChild(Clone((VisualNode)child, result, index)); + Span children = result.AddChildrenSpan(childCount); + + for (var i = 0; i < childCount; i++) + { + var child = source.Children[i]; + + children[i] = Clone((VisualNode)child, result, index); + } } return result; diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs index 5960b4f560..25f7383a1a 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs @@ -11,16 +11,28 @@ namespace Avalonia.Rendering.SceneGraph public class SceneLayers : IEnumerable { private readonly IVisual _root; - private readonly List _inner = new List(); - private readonly Dictionary _index = new Dictionary(); + private readonly List _inner; + private readonly Dictionary _index; /// /// Initializes a new instance of the class. /// /// The scene's root visual. - public SceneLayers(IVisual root) + public SceneLayers(IVisual root) : this(root, 0) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The scene's root visual. + /// Initial layer capacity. + public SceneLayers(IVisual root, int capacity) { _root = root; + + _inner = new List(capacity); + _index = new Dictionary(capacity); } /// @@ -84,7 +96,7 @@ namespace Avalonia.Rendering.SceneGraph /// The cloned layers. public SceneLayers Clone() { - var result = new SceneLayers(_root); + var result = new SceneLayers(_root, Count); foreach (var src in _inner) { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 82444a0c29..8cd1a47795 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Linq; using System.Reactive.Disposables; +using Avalonia.Collections.Pooled; using Avalonia.Media; using Avalonia.Platform; using Avalonia.Utilities; @@ -19,8 +20,8 @@ namespace Avalonia.Rendering.SceneGraph private Rect? _bounds; private double _opacity; - private List _children; - private List> _drawOperations; + private PooledList _children; + private PooledList> _drawOperations; private IRef _drawOperationsRefCounter; private bool _drawOperationsCloned; private Matrix transformRestore; @@ -349,6 +350,18 @@ namespace Avalonia.Rendering.SceneGraph context.Transform = transformRestore; } + /// + /// Inserts default constructed children into collection and returns a span for the newly created range. + /// + /// Count of children that will be added. + /// + internal Span AddChildrenSpan(int count) + { + EnsureChildrenCreated(count); + + return _children.AddSpan(count); + } + private Rect CalculateBounds() { var result = new Rect(); @@ -362,11 +375,11 @@ namespace Avalonia.Rendering.SceneGraph return result; } - private void EnsureChildrenCreated() + private void EnsureChildrenCreated(int capacity = 0) { if (_children == null) { - _children = new List(); + _children = new PooledList(capacity); } } @@ -377,13 +390,21 @@ namespace Avalonia.Rendering.SceneGraph { if (_drawOperations == null) { - _drawOperations = new List>(); + _drawOperations = new PooledList>(); _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); _drawOperationsCloned = false; } else if (_drawOperationsCloned) { - _drawOperations = new List>(_drawOperations.Select(op => op.Clone())); + var oldDrawOperations = _drawOperations; + + _drawOperations = new PooledList>(oldDrawOperations.Count); + + foreach (var drawOperation in oldDrawOperations) + { + _drawOperations.Add(drawOperation.Clone()); + } + _drawOperationsRefCounter.Dispose(); _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); _drawOperationsCloned = false; @@ -397,14 +418,16 @@ namespace Avalonia.Rendering.SceneGraph /// /// Draw operations that need to be disposed. /// Disposable for given draw operations. - private static IDisposable CreateDisposeDrawOperations(List> drawOperations) + private static IDisposable CreateDisposeDrawOperations(PooledList> drawOperations) { - return Disposable.Create(() => + return Disposable.Create(drawOperations, operations => { - foreach (var operation in drawOperations) + foreach (var operation in operations) { operation.Dispose(); } + + operations.Dispose(); }); } @@ -414,6 +437,8 @@ namespace Avalonia.Rendering.SceneGraph { _drawOperationsRefCounter?.Dispose(); + _children?.Dispose(); + Disposed = true; } } From 48abbcdf9111d700a58ae52fb4930e4c7815fb27 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 14 Apr 2020 15:46:33 +0300 Subject: [PATCH 16/25] [X11] Special handling for XI2 events when window is blocked by child Dialog --- src/Avalonia.Input/Raw/RawPointerEventArgs.cs | 2 +- src/Avalonia.X11/X11Window.cs | 22 ++++++++++++++++++- src/Avalonia.X11/XI2Manager.cs | 10 ++++----- 3 files changed, 27 insertions(+), 7 deletions(-) diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index bbd5515da0..62a1dd5d84 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -63,7 +63,7 @@ namespace Avalonia.Input.Raw /// /// Gets the type of the event. /// - public RawPointerEventType Type { get; private set; } + public RawPointerEventType Type { get; set; } /// /// Gets the input modifiers. diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 60fd0346a3..478a908951 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -649,7 +649,27 @@ namespace Avalonia.X11 ScheduleInput(args); } - public void ScheduleInput(RawInputEventArgs args) + public void ScheduleXI2Input(RawInputEventArgs args) + { + if (args is RawPointerEventArgs pargs) + { + if ((pargs.Type == RawPointerEventType.TouchBegin + || pargs.Type == RawPointerEventType.TouchUpdate + || pargs.Type == RawPointerEventType.LeftButtonDown + || pargs.Type == RawPointerEventType.RightButtonDown + || pargs.Type == RawPointerEventType.MiddleButtonDown + || pargs.Type == RawPointerEventType.NonClientLeftButtonDown) + && ActivateTransientChildIfNeeded()) + return; + if (pargs.Type == RawPointerEventType.TouchEnd + && ActivateTransientChildIfNeeded()) + pargs.Type = RawPointerEventType.TouchCancel; + } + + ScheduleInput(args); + } + + private void ScheduleInput(RawInputEventArgs args) { if (args is RawPointerEventArgs mouse) mouse.Position = mouse.Position / Scaling; diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs index ac14efe133..0734532d92 100644 --- a/src/Avalonia.X11/XI2Manager.cs +++ b/src/Avalonia.X11/XI2Manager.cs @@ -196,7 +196,7 @@ namespace Avalonia.X11 (ev.Type == XiEventType.XI_TouchUpdate ? RawPointerEventType.TouchUpdate : RawPointerEventType.TouchEnd); - client.ScheduleInput(new RawTouchEventArgs(client.TouchDevice, + client.ScheduleXI2Input(new RawTouchEventArgs(client.TouchDevice, ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail)); return; } @@ -230,10 +230,10 @@ namespace Avalonia.X11 } if (scrollDelta != default) - client.ScheduleInput(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp, + client.ScheduleXI2Input(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, ev.Position, scrollDelta, ev.Modifiers)); if (_pointerDevice.HasMotion(ev)) - client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, + client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, RawPointerEventType.Move, ev.Position, ev.Modifiers)); } @@ -250,7 +250,7 @@ namespace Avalonia.X11 _ => (RawPointerEventType?)null }; if (type.HasValue) - client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, + client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, type.Value, ev.Position, ev.Modifiers)); } @@ -313,7 +313,7 @@ namespace Avalonia.X11 interface IXI2Client { IInputRoot InputRoot { get; } - void ScheduleInput(RawInputEventArgs args); + void ScheduleXI2Input(RawInputEventArgs args); IMouseDevice MouseDevice { get; } TouchDevice TouchDevice { get; } } From 7a3aebe3fb8193eeafd3456b675fbd9dd2440cc5 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 14 Apr 2020 16:37:16 +0300 Subject: [PATCH 17/25] Freeze OSX castxml version to 0.2.0 --- azure-pipelines.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b70e0bf77f..54645e461e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -68,7 +68,7 @@ jobs: inputs: script: | brew update - brew install castxml + brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb - task: CmdLine@2 displayName: 'Install Nuke' From af950096ccb5d295dce99911363ff85cce75be10 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 16 Apr 2020 12:07:12 +0200 Subject: [PATCH 18/25] Added failing test for #3784. Had to refactor the `MockWindowingPlatform` a bit. --- .../ContextMenuTests.cs | 7 +- .../Primitives/PopupRootTests.cs | 82 +++++++++++- .../Primitives/PopupTests.cs | 4 +- .../WindowTests.cs | 118 ++++++++++++++++++ .../MockWindowingPlatform.cs | 111 +++++++++++----- 5 files changed, 286 insertions(+), 36 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 5a47a86e51..28e87dd671 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -209,16 +209,17 @@ namespace Avalonia.Controls.UnitTests screenImpl.Setup(x => x.ScreenCount).Returns(1); screenImpl.Setup(X => X.AllScreens).Returns( new[] { new Screen(1, screen, screen, true) }); - popupImpl = MockWindowingPlatform.CreatePopupMock(); + var windowImpl = MockWindowingPlatform.CreateWindowMock(); + popupImpl = MockWindowingPlatform.CreatePopupMock(windowImpl.Object); popupImpl.SetupGet(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.CreatePopup()).Returns(popupImpl.Object); - var windowImpl = MockWindowingPlatform.CreateWindowMock(() => popupImpl.Object); windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object); var services = TestServices.StyledWindow.With( inputManager: new InputManager(), windowImpl: windowImpl.Object, - windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object, () => popupImpl.Object)); + windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object, x => popupImpl.Object)); return UnitTestApplication.Start(services); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index 501c0455d0..b03f8b8892 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -4,6 +4,7 @@ using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; using Avalonia.LogicalTree; +using Avalonia.Platform; using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; @@ -172,9 +173,75 @@ namespace Avalonia.Controls.UnitTests.Primitives } } - private PopupRoot CreateTarget(TopLevel popupParent) + [Fact] + public void Child_Should_Be_Measured_With_Infinity() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new ChildControl(); + var window = new Window(); + var target = CreateTarget(window); + + target.Content = child; + target.Show(); + + Assert.Equal(Size.Infinity, child.MeasureSize); + } + } + + [Fact] + public void Child_Should_Be_Measured_With_Width_Height_When_Set() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new ChildControl(); + var window = new Window(); + var target = CreateTarget(window); + + target.Width = 500; + target.Height = 600; + target.Content = child; + target.Show(); + + Assert.Equal(new Size(500, 600), child.MeasureSize); + } + } + + [Fact] + public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size() + { + // Issue #3784. + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl); + + popupImpl.Setup(x => x.ClientSize).Returns(new Size(400, 480)); + + var child = new Canvas + { + Width = 400, + Height = 800, + }; + + var target = CreateTarget(window, popupImpl.Object); + target.Content = child; + + target.Show(); + + Assert.Equal(new Size(400, 480), target.Bounds.Size); + + // Issue #3784 causes this to be (0, 160) which makes no sense as Window has no + // parent control to be offset against. + Assert.Equal(new Point(0, 0), target.Bounds.Position); + } + } + + private PopupRoot CreateTarget(TopLevel popupParent, IPopupImpl impl = null) { - var result = new PopupRoot(popupParent, popupParent.PlatformImpl.CreatePopup()) + impl ??= popupParent.PlatformImpl.CreatePopup(); + + var result = new PopupRoot(popupParent, impl) { Template = new FuncControlTemplate((parent, scope) => new ContentPresenter @@ -217,5 +284,16 @@ namespace Avalonia.Controls.UnitTests.Primitives Popup = (Popup)this.GetVisualChildren().Single(); } } + + private class ChildControl : Control + { + public Size MeasureSize { get; private set; } + + protected override Size MeasureOverride(Size availableSize) + { + MeasureSize = availableSize; + return base.MeasureOverride(availableSize); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index a4c0fa054b..1a2c93ac22 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs @@ -355,11 +355,11 @@ namespace Avalonia.Controls.UnitTests.Primitives { return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform: new MockWindowingPlatform(null, - () => + x => { if(UsePopupHost) return null; - return MockWindowingPlatform.CreatePopupMock().Object; + return MockWindowingPlatform.CreatePopupMock(x).Object; }))); } diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index fed63fc683..f9248b7382 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -1,6 +1,7 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; +using Avalonia.Layout; using Avalonia.Platform; using Avalonia.Rendering; using Avalonia.UnitTests; @@ -375,6 +376,123 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size() + { + // Issue #3784. + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var windowImpl = MockWindowingPlatform.CreateWindowMock(); + var clientSize = new Size(200, 200); + var maxClientSize = new Size(480, 480); + + windowImpl.Setup(x => x.Resize(It.IsAny())).Callback(size => + { + clientSize = size.Constrain(maxClientSize); + windowImpl.Object.Resized?.Invoke(clientSize); + }); + + windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); + + var child = new Canvas + { + Width = 400, + Height = 800, + }; + var target = new Window(windowImpl.Object) + { + SizeToContent = SizeToContent.WidthAndHeight, + Content = child + }; + + target.Show(); + + Assert.Equal(new Size(400, 480), target.Bounds.Size); + + // Issue #3784 causes this to be (0, 160) which makes no sense as Window has no + // parent control to be offset against. + Assert.Equal(new Point(0, 0), target.Bounds.Position); + } + } + + [Fact] + public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_WidthAndHeight() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new Canvas + { + Width = 400, + Height = 800, + }; + + var target = new Window() + { + SizeToContent = SizeToContent.WidthAndHeight, + Content = child + }; + + target.Show(); + + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); + } + } + + [Fact] + public void SizeToContent_Should_Not_Be_Lost_On_Show() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new Canvas + { + Width = 400, + Height = 800, + }; + + var target = new Window() + { + SizeToContent = SizeToContent.WidthAndHeight, + Content = child + }; + + target.Show(); + + Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); + } + } + + [Fact] + public void Width_Height_Should_Be_Updated_When_SizeToContent_Is_WidthAndHeight() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new Canvas + { + Width = 400, + Height = 800, + }; + + var target = new Window() + { + SizeToContent = SizeToContent.WidthAndHeight, + Content = child + }; + + target.Show(); + + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); + + child.Width = 410; + target.LayoutManager.ExecuteLayoutPass(); + + Assert.Equal(410, target.Width); + Assert.Equal(800, target.Height); + Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent); + } + } + private IWindowImpl CreateImpl(Mock renderer) { return Mock.Of(x => diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 782e4a0974..b8b7512c9e 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -8,65 +8,118 @@ namespace Avalonia.UnitTests { public class MockWindowingPlatform : IWindowingPlatform { + private static readonly Size s_screenSize = new Size(1280, 1024); private readonly Func _windowImpl; - private readonly Func _popupImpl; + private readonly Func _popupImpl; - public MockWindowingPlatform(Func windowImpl = null, Func popupImpl = null ) + public MockWindowingPlatform( + Func windowImpl = null, + Func popupImpl = null ) { _windowImpl = windowImpl; _popupImpl = popupImpl; } - public static Mock CreateWindowMock(Func popupImpl = null) + public static Mock CreateWindowMock() { - var win = Mock.Of(x => x.Scaling == 1); - var mock = Mock.Get(win); - mock.Setup(x => x.Show()).Callback(() => + var windowImpl = new Mock(); + var position = new PixelPoint(); + var clientSize = new Size(800, 600); + + windowImpl.SetupAllProperties(); + windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize); + windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.Screen).Returns(CreateScreenMock().Object); + windowImpl.Setup(x => x.Position).Returns(() => position); + SetupToplevel(windowImpl); + + windowImpl.Setup(x => x.CreatePopup()).Returns(() => { - mock.Object.Activated?.Invoke(); + return CreatePopupMock(windowImpl.Object).Object; }); - mock.Setup(x => x.CreatePopup()).Returns(() => + + windowImpl.Setup(x => x.Dispose()).Callback(() => { - if (popupImpl != null) - return popupImpl(); - return CreatePopupMock().Object; + windowImpl.Object.Closed?.Invoke(); + }); + + windowImpl.Setup(x => x.Move(It.IsAny())).Callback(x => + { + position = x; + windowImpl.Object.PositionChanged?.Invoke(x); + }); + windowImpl.Setup(x => x.Resize(It.IsAny())).Callback(x => + { + clientSize = x.Constrain(s_screenSize); + windowImpl.Object.Resized?.Invoke(clientSize); }); - mock.Setup(x => x.Dispose()).Callback(() => + + windowImpl.Setup(x => x.Show()).Callback(() => { - mock.Object.Closed?.Invoke(); + windowImpl.Object.Activated?.Invoke(); }); - PixelPoint pos = default; - mock.SetupGet(x => x.Position).Returns(() => pos); - mock.Setup(x => x.Move(It.IsAny())).Callback(new Action(np => pos = np)); - SetupToplevel(mock); - return mock; + + return windowImpl; } - static void SetupToplevel(Mock mock) where T : class, ITopLevelImpl + public static Mock CreatePopupMock(IWindowBaseImpl parent) { - mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice()); + var popupImpl = new Mock(); + + var positionerHelper = new ManagedPopupPositionerPopupImplHelper(parent, (pos, size, scale) => + { + popupImpl.Object.PositionChanged?.Invoke(pos); + popupImpl.Object.Resized?.Invoke(size); + }); + + var positioner = new ManagedPopupPositioner(positionerHelper); + + popupImpl.Setup(x => x.Scaling).Returns(1); + popupImpl.Setup(x => x.PopupPositioner).Returns(positioner); + + SetupToplevel(popupImpl); + + return popupImpl; } - public static Mock CreatePopupMock() + public static Mock CreateScreenMock() { - var positioner = Mock.Of(); - var popup = Mock.Of(x => x.Scaling == 1); - var mock = Mock.Get(popup); - mock.SetupGet(x => x.PopupPositioner).Returns(positioner); - SetupToplevel(mock); - - return mock; + var screenImpl = new Mock(); + var bounds = new PixelRect(0, 0, (int)s_screenSize.Width, (int)s_screenSize.Height); + var screen = new Screen(96, bounds, bounds, true); + screenImpl.Setup(x => x.AllScreens).Returns(new[] { screen }); + screenImpl.Setup(x => x.ScreenCount).Returns(1); + return screenImpl; } public IWindowImpl CreateWindow() { - return _windowImpl?.Invoke() ?? CreateWindowMock(_popupImpl).Object; + if (_windowImpl is object) + { + return _windowImpl(); + } + else + { + var mock = CreateWindowMock(); + + if (_popupImpl is object) + { + mock.Setup(x => x.CreatePopup()).Returns(() => _popupImpl(mock.Object)); + } + + return mock.Object; + } } public IEmbeddableWindowImpl CreateEmbeddableWindow() { throw new NotImplementedException(); } + + private static void SetupToplevel(Mock mock) where T : class, ITopLevelImpl + { + mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice()); + } } } From c6860ceefa74cb78338a645ab4ef81a5e6e0f0b8 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 16 Apr 2020 13:38:46 +0200 Subject: [PATCH 19/25] Bypass ArrangeCore logic in top-level controls. Top-level controls cannot have a `Bounds` offset, and their `(Min)/(Max)/Width` and `(Min)/(Max)/Height` reflects the client size of the actual window, so don't need to be applied at the layout level. Fixes #3784 --- src/Avalonia.Controls/Primitives/PopupRoot.cs | 12 +- src/Avalonia.Controls/Window.cs | 48 ++++---- src/Avalonia.Controls/WindowBase.cs | 60 +++++++++- .../WindowTests.cs | 21 ++++ .../FullLayoutTests.cs | 108 +----------------- 5 files changed, 109 insertions(+), 140 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 4c84d32637..4546a1aadb 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -117,20 +117,14 @@ namespace Avalonia.Controls.Primitives }); } - /// - /// Carries out the arrange pass of the window. - /// - /// The final window size. - /// The parameter unchanged. - protected override Size ArrangeOverride(Size finalSize) + protected override sealed Size ArrangeSetBounds(Size size) { using (BeginAutoSizing()) { - _positionerParameters.Size = finalSize; + _positionerParameters.Size = size; UpdatePosition(); + return ClientSize; } - - return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size)); } } } diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index 387bf0adb8..dcf4e98528 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -313,22 +313,7 @@ namespace Avalonia.Controls /// Should be called from left mouse button press event handler /// public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => PlatformImpl?.BeginResizeDrag(edge, e); - - /// - /// Carries out the arrange pass of the window. - /// - /// The final window size. - /// The parameter unchanged. - protected override Size ArrangeOverride(Size finalSize) - { - using (BeginAutoSizing()) - { - PlatformImpl?.Resize(finalSize); - } - return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size)); - } - /// Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize; @@ -450,6 +435,19 @@ namespace Avalonia.Controls EnsureInitialized(); IsVisible = true; + + var initialSize = new Size( + double.IsNaN(Width) ? ClientSize.Width : Width, + double.IsNaN(Height) ? ClientSize.Height : Height); + + if (initialSize != ClientSize) + { + using (BeginAutoSizing()) + { + PlatformImpl?.Resize(initialSize); + } + } + LayoutManager.ExecuteInitialLayoutPass(this); using (BeginAutoSizing()) @@ -569,31 +567,30 @@ namespace Avalonia.Controls } } - /// protected override Size MeasureOverride(Size availableSize) { var sizeToContent = SizeToContent; var clientSize = ClientSize; - var constraint = availableSize; + var constraint = clientSize; - if ((sizeToContent & SizeToContent.Width) != 0) + if (sizeToContent.HasFlagCustom(SizeToContent.Width)) { constraint = constraint.WithWidth(double.PositiveInfinity); } - if ((sizeToContent & SizeToContent.Height) != 0) + if (sizeToContent.HasFlagCustom(SizeToContent.Height)) { constraint = constraint.WithHeight(double.PositiveInfinity); } var result = base.MeasureOverride(constraint); - if ((sizeToContent & SizeToContent.Width) == 0) + if (!sizeToContent.HasFlagCustom(SizeToContent.Width)) { result = result.WithWidth(clientSize.Width); } - if ((sizeToContent & SizeToContent.Height) == 0) + if (!sizeToContent.HasFlagCustom(SizeToContent.Height)) { result = result.WithHeight(clientSize.Height); } @@ -601,6 +598,15 @@ namespace Avalonia.Controls return result; } + protected sealed override Size ArrangeSetBounds(Size size) + { + using (BeginAutoSizing()) + { + PlatformImpl?.Resize(size); + return ClientSize; + } + } + protected sealed override void HandleClosed() { RaiseEvent(new RoutedEventArgs(WindowClosedEvent)); diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 63eabb32f4..025dfde610 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -224,16 +224,66 @@ namespace Avalonia.Controls /// The new client size. protected override void HandleResized(Size clientSize) { - if (!AutoSizing) - { - Width = clientSize.Width; - Height = clientSize.Height; - } + Width = clientSize.Width; + Height = clientSize.Height; ClientSize = clientSize; LayoutManager.ExecuteLayoutPass(); Renderer?.Resized(clientSize); } + /// + /// Overrides the core measure logic for windows. + /// + /// The available size. + /// The measured size. + /// + /// The layout logic for top-level windows is different than for other controls because + /// they don't have a parent, meaning that many layout properties handled by the default + /// MeasureCore (such as margins and alignment) make no sense. + /// + protected override Size MeasureCore(Size availableSize) + { + ApplyStyling(); + ApplyTemplate(); + + var constraint = availableSize; + + if (!double.IsNaN(Width)) + { + constraint = constraint.WithWidth(Width); + } + + if (!double.IsNaN(Height)) + { + constraint = constraint.WithHeight(Height); + } + + return MeasureOverride(constraint); + } + + /// + /// Overrides the core arrange logic for windows. + /// + /// The final arrange rect. + /// + /// The layout logic for top-level windows is different than for other controls because + /// they don't have a parent, meaning that many layout properties handled by the default + /// ArrangeCore (such as margins and alignment) make no sense. + /// + protected override void ArrangeCore(Rect finalRect) + { + var constraint = ArrangeSetBounds(finalRect.Size); + var arrangeSize = ArrangeOverride(constraint); + Bounds = new Rect(arrangeSize); + } + + /// + /// Called durung the arrange pass to set the size of the window. + /// + /// The requested size of the window. + /// The actual size of the window. + protected virtual Size ArrangeSetBounds(Size size) => size; + /// /// Handles a window position change notification from /// . diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index f9248b7382..5382e6ea3e 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -356,6 +356,27 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Child_Should_Be_Measured_With_ClientSize_If_SizeToContent_Is_Manual_And_No_Width_Height_Specified() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var windowImpl = MockWindowingPlatform.CreateWindowMock(); + windowImpl.Setup(x => x.ClientSize).Returns(new Size(550, 450)); + + var child = new ChildControl(); + var target = new Window(windowImpl.Object) + { + SizeToContent = SizeToContent.Manual, + Content = child + }; + + target.Show(); + + Assert.Equal(new Size(550, 450), child.MeasureSize); + } + } + [Fact] public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight() { diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index 69eff0b65d..dcc29a9716 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs @@ -1,25 +1,12 @@ -using System.Diagnostics; -using System.IO; using System.Linq; -using Moq; using Avalonia.Controls; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; using Avalonia.Controls.Templates; -using Avalonia.Diagnostics; -using Avalonia.Input; -using Avalonia.Platform; -using Avalonia.Rendering; -using Avalonia.Shared.PlatformSupport; using Avalonia.Styling; -using Avalonia.Themes.Default; +using Avalonia.UnitTests; using Avalonia.VisualTree; using Xunit; -using Avalonia.Media; -using System; -using System.Collections.Generic; -using Avalonia.Controls.UnitTests; -using Avalonia.UnitTests; namespace Avalonia.Layout.UnitTests { @@ -28,10 +15,8 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Grandchild_Size_Changed() { - using (var context = AvaloniaLocator.EnterScope()) + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - RegisterServices(); - Border border; TextBlock textBlock; @@ -55,7 +40,6 @@ namespace Avalonia.Layout.UnitTests }; window.Show(); - window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.Equal(new Size(400, 400), border.Bounds.Size); textBlock.Width = 200; @@ -68,10 +52,8 @@ namespace Avalonia.Layout.UnitTests [Fact] public void Test_ScrollViewer_With_TextBlock() { - using (var context = AvaloniaLocator.EnterScope()) + using (UnitTestApplication.Start(TestServices.StyledWindow)) { - RegisterServices(); - ScrollViewer scrollViewer; TextBlock textBlock; @@ -79,7 +61,6 @@ namespace Avalonia.Layout.UnitTests { Width = 800, Height = 600, - SizeToContent = SizeToContent.WidthAndHeight, Content = scrollViewer = new ScrollViewer { Width = 200, @@ -99,7 +80,6 @@ namespace Avalonia.Layout.UnitTests window.Resources["ScrollBarThickness"] = 10.0; window.Show(); - window.LayoutManager.ExecuteInitialLayoutPass(window); Assert.Equal(new Size(800, 600), window.Bounds.Size); Assert.Equal(new Size(200, 200), scrollViewer.Bounds.Size); @@ -131,87 +111,5 @@ namespace Avalonia.Layout.UnitTests { return v.Bounds.Position; } - - class FormattedTextMock : IFormattedTextImpl - { - public FormattedTextMock(string text) - { - Text = text; - } - - public Size Constraint { get; set; } - - public string Text { get; } - - public Rect Bounds => Rect.Empty; - - public void Dispose() - { - } - - public IEnumerable GetLines() => new FormattedTextLine[0]; - - public TextHitTestResult HitTestPoint(Point point) => new TextHitTestResult(); - - public Rect HitTestTextPosition(int index) => new Rect(); - - public IEnumerable HitTestTextRange(int index, int length) => new Rect[0]; - - public Size Measure() => Constraint; - } - - private void RegisterServices() - { - var globalStyles = new Mock(); - var globalStylesResources = globalStyles.As(); - var outObj = (object)10; - globalStylesResources.Setup(x => x.TryGetResource("FontSizeNormal", out outObj)).Returns(true); - - var renderInterface = new Mock(); - renderInterface.Setup(x => - x.CreateFormattedText( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>())) - .Returns(new FormattedTextMock("TEST")); - - var streamGeometry = new Mock(); - streamGeometry.Setup(x => - x.Open()) - .Returns(new Mock().Object); - - renderInterface.Setup(x => - x.CreateStreamGeometry()) - .Returns(streamGeometry.Object); - - var windowImpl = new Mock(); - - Size clientSize = default(Size); - - windowImpl.SetupGet(x => x.ClientSize).Returns(() => clientSize); - windowImpl.Setup(x => x.Resize(It.IsAny())).Callback(s => clientSize = s); - windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1024, 1024)); - windowImpl.SetupGet(x => x.Scaling).Returns(1); - - AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new CursorFactoryMock()) - .Bind().ToConstant(new AssetLoader()) - .Bind().ToConstant(new Mock().Object) - .Bind().ToConstant(globalStyles.Object) - .Bind().ToConstant(new AppBuilder().RuntimePlatform) - .Bind().ToConstant(renderInterface.Object) - .Bind().ToConstant(new Styler()) - .Bind().ToConstant(new MockFontManagerImpl()) - .Bind().ToConstant(new MockTextShaperImpl()) - .Bind().ToConstant(new Avalonia.Controls.UnitTests.WindowingPlatformMock(() => windowImpl.Object)); - - var theme = new DefaultTheme(); - globalStyles.Setup(x => x.IsStylesInitialized).Returns(true); - globalStyles.Setup(x => x.Styles).Returns(theme); - } } } From 24458d05b12161661b0d1b0f16510b2c42b710bb Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Thu, 16 Apr 2020 12:05:57 -0400 Subject: [PATCH 20/25] Fix native dialogs throwing InvalidCastException --- src/Avalonia.Dialogs/ManagedFileChooser.xaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Dialogs/ManagedFileChooser.xaml b/src/Avalonia.Dialogs/ManagedFileChooser.xaml index af0c91e7bd..8a0451f982 100644 --- a/src/Avalonia.Dialogs/ManagedFileChooser.xaml +++ b/src/Avalonia.Dialogs/ManagedFileChooser.xaml @@ -58,7 +58,7 @@ From 908eef0b0dac71748e1b46350609ad9faf842c43 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 17 Apr 2020 18:02:29 +0200 Subject: [PATCH 21/25] Added failing tests for #3796. --- .../Primitives/PopupRootTests.cs | 63 +++++++++++++++++-- .../WindowTests.cs | 26 ++++++++ .../MockWindowingPlatform.cs | 6 +- 3 files changed, 90 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index b03f8b8892..5ffb6027c4 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -2,12 +2,14 @@ using System; using System.Linq; using Avalonia.Controls.Presenters; using Avalonia.Controls.Primitives; +using Avalonia.Controls.Primitives.PopupPositioning; using Avalonia.Controls.Templates; using Avalonia.LogicalTree; using Avalonia.Platform; using Avalonia.Styling; using Avalonia.UnitTests; using Avalonia.VisualTree; +using Moq; using Xunit; namespace Avalonia.Controls.UnitTests.Primitives @@ -216,12 +218,10 @@ namespace Avalonia.Controls.UnitTests.Primitives var window = new Window(); var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl); - popupImpl.Setup(x => x.ClientSize).Returns(new Size(400, 480)); - var child = new Canvas { Width = 400, - Height = 800, + Height = 1344, }; var target = CreateTarget(window, popupImpl.Object); @@ -229,7 +229,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.Show(); - Assert.Equal(new Size(400, 480), target.Bounds.Size); + Assert.Equal(new Size(400, 1024), target.Bounds.Size); // Issue #3784 causes this to be (0, 160) which makes no sense as Window has no // parent control to be offset against. @@ -237,6 +237,61 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void MinWidth_MinHeight_Should_Be_Respected() + { + // Issue #3796 + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl); + + var target = CreateTarget(window, popupImpl.Object); + target.MinWidth = 400; + target.MinHeight = 800; + target.Content = new Border + { + Width = 100, + Height = 100, + }; + + target.Show(); + + Assert.Equal(new Rect(0, 0, 400, 800), target.Bounds); + Assert.Equal(new Size(400, 800), target.ClientSize); + Assert.Equal(new Size(400, 800), target.PlatformImpl.ClientSize); + } + } + + [Fact] + public void Setting_Width_Should_Resize_WindowImpl() + { + // Issue #3796 + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl); + var positioner = new Mock(); + popupImpl.Setup(x => x.PopupPositioner).Returns(positioner.Object); + + var target = CreateTarget(window, popupImpl.Object); + target.Width = 400; + target.Height = 800; + + target.Show(); + + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); + + target.Width = 410; + target.LayoutManager.ExecuteLayoutPass(); + + positioner.Verify(x => + x.Update(It.Is(x => x.Size.Width == 410))); + Assert.Equal(410, target.Width); + } + } + private PopupRoot CreateTarget(TopLevel popupParent, IPopupImpl impl = null) { impl ??= popupParent.PlatformImpl.CreatePopup(); diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index 5382e6ea3e..1ac4d7a236 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -514,6 +514,32 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Setting_Width_Should_Resize_WindowImpl() + { + // Issue #3796 + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var target = new Window() + { + Width = 400, + Height = 800, + }; + + target.Show(); + + Assert.Equal(400, target.Width); + Assert.Equal(800, target.Height); + + target.Width = 410; + target.LayoutManager.ExecuteLayoutPass(); + + var windowImpl = Mock.Get(target.PlatformImpl); + windowImpl.Verify(x => x.Resize(new Size(410, 800))); + Assert.Equal(410, target.Width); + } + } + private IWindowImpl CreateImpl(Mock renderer) { return Mock.Of(x => diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index b8b7512c9e..b3e4b4edbc 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs @@ -66,15 +66,19 @@ namespace Avalonia.UnitTests public static Mock CreatePopupMock(IWindowBaseImpl parent) { var popupImpl = new Mock(); + var clientSize = new Size(); var positionerHelper = new ManagedPopupPositionerPopupImplHelper(parent, (pos, size, scale) => { + clientSize = size.Constrain(s_screenSize); popupImpl.Object.PositionChanged?.Invoke(pos); - popupImpl.Object.Resized?.Invoke(size); + popupImpl.Object.Resized?.Invoke(clientSize); }); var positioner = new ManagedPopupPositioner(positionerHelper); + popupImpl.SetupAllProperties(); + popupImpl.Setup(x => x.ClientSize).Returns(() => clientSize); popupImpl.Setup(x => x.Scaling).Returns(1); popupImpl.Setup(x => x.PopupPositioner).Returns(positioner); From 945054be250ca53dc2324abdd3b2c6f7c334e988 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 17 Apr 2020 18:05:41 +0200 Subject: [PATCH 22/25] Make Window and Popup respect layout properties. Had to remove syncing client size to `Width`/`Height` in `PopupRoot` bceause without the `SizeToContent` property it was impossible to tell whether its `Width`/`Height` came from a requested bound or from the actual window size. --- src/Avalonia.Controls/Primitives/PopupRoot.cs | 27 +++++++++++++++++++ src/Avalonia.Controls/Window.cs | 23 +++++++++++++--- src/Avalonia.Controls/WindowBase.cs | 2 -- 3 files changed, 47 insertions(+), 5 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/PopupRoot.cs b/src/Avalonia.Controls/Primitives/PopupRoot.cs index 4546a1aadb..788fe03162 100644 --- a/src/Avalonia.Controls/Primitives/PopupRoot.cs +++ b/src/Avalonia.Controls/Primitives/PopupRoot.cs @@ -117,6 +117,33 @@ namespace Avalonia.Controls.Primitives }); } + protected override Size MeasureOverride(Size availableSize) + { + var measured = base.MeasureOverride(availableSize); + var width = measured.Width; + var height = measured.Height; + var widthCache = Width; + var heightCache = Height; + + if (!double.IsNaN(widthCache)) + { + width = widthCache; + } + + width = Math.Min(width, MaxWidth); + width = Math.Max(width, MinWidth); + + if (!double.IsNaN(heightCache)) + { + height = heightCache; + } + + height = Math.Min(height, MaxHeight); + height = Math.Max(height, MinHeight); + + return new Size(width, height); + } + protected override sealed Size ArrangeSetBounds(Size size) { using (BeginAutoSizing()) diff --git a/src/Avalonia.Controls/Window.cs b/src/Avalonia.Controls/Window.cs index dcf4e98528..b21f18a59d 100644 --- a/src/Avalonia.Controls/Window.cs +++ b/src/Avalonia.Controls/Window.cs @@ -570,8 +570,8 @@ namespace Avalonia.Controls protected override Size MeasureOverride(Size availableSize) { var sizeToContent = SizeToContent; + var constraint = availableSize; var clientSize = ClientSize; - var constraint = clientSize; if (sizeToContent.HasFlagCustom(SizeToContent.Width)) { @@ -587,12 +587,26 @@ namespace Avalonia.Controls if (!sizeToContent.HasFlagCustom(SizeToContent.Width)) { - result = result.WithWidth(clientSize.Width); + if (!double.IsInfinity(availableSize.Width)) + { + result = result.WithWidth(availableSize.Width); + } + else + { + result = result.WithWidth(clientSize.Width); + } } if (!sizeToContent.HasFlagCustom(SizeToContent.Height)) { - result = result.WithHeight(clientSize.Height); + if (!double.IsInfinity(availableSize.Height)) + { + result = result.WithHeight(availableSize.Height); + } + else + { + result = result.WithHeight(clientSize.Height); + } } return result; @@ -622,6 +636,9 @@ namespace Avalonia.Controls SizeToContent = SizeToContent.Manual; } + Width = clientSize.Width; + Height = clientSize.Height; + base.HandleResized(clientSize); } diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index 025dfde610..ea722f2952 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -224,8 +224,6 @@ namespace Avalonia.Controls /// The new client size. protected override void HandleResized(Size clientSize) { - Width = clientSize.Width; - Height = clientSize.Height; ClientSize = clientSize; LayoutManager.ExecuteLayoutPass(); Renderer?.Resized(clientSize); From ec24030bac5101139a48258957e9728d5e32e9f5 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 18 Apr 2020 14:25:10 +0200 Subject: [PATCH 23/25] Added another failing PopupRoot test. --- .../Primitives/PopupRootTests.cs | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index 5ffb6027c4..a61d313c8c 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs @@ -209,6 +209,24 @@ namespace Avalonia.Controls.UnitTests.Primitives } } + [Fact] + public void Child_Should_Be_Measured_With_MaxWidth_MaxHeight_When_Set() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var child = new ChildControl(); + var window = new Window(); + var target = CreateTarget(window); + + target.MaxWidth = 500; + target.MaxHeight = 600; + target.Content = child; + target.Show(); + + Assert.Equal(new Size(500, 600), child.MeasureSize); + } + } + [Fact] public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size() { From 0d109ea996fe482ae8403b90657bc3def8fbcb74 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 18 Apr 2020 14:25:23 +0200 Subject: [PATCH 24/25] Apply all layout constraints in WindowBase. --- src/Avalonia.Controls/WindowBase.cs | 12 +----------- 1 file changed, 1 insertion(+), 11 deletions(-) diff --git a/src/Avalonia.Controls/WindowBase.cs b/src/Avalonia.Controls/WindowBase.cs index ea722f2952..bb63d1b353 100644 --- a/src/Avalonia.Controls/WindowBase.cs +++ b/src/Avalonia.Controls/WindowBase.cs @@ -244,17 +244,7 @@ namespace Avalonia.Controls ApplyStyling(); ApplyTemplate(); - var constraint = availableSize; - - if (!double.IsNaN(Width)) - { - constraint = constraint.WithWidth(Width); - } - - if (!double.IsNaN(Height)) - { - constraint = constraint.WithHeight(Height); - } + var constraint = LayoutHelper.ApplyLayoutConstraints(this, availableSize); return MeasureOverride(constraint); } From 19570abec07d20b42a2a90bd6511431fdd097376 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 18 Apr 2020 14:25:48 +0200 Subject: [PATCH 25/25] Fix calculating min/max layout size. Code ported from WPF. --- src/Avalonia.Layout/LayoutHelper.cs | 50 +++++++++++++++++++++++------ 1 file changed, 40 insertions(+), 10 deletions(-) diff --git a/src/Avalonia.Layout/LayoutHelper.cs b/src/Avalonia.Layout/LayoutHelper.cs index af7d8ee52e..2b61de00a7 100644 --- a/src/Avalonia.Layout/LayoutHelper.cs +++ b/src/Avalonia.Layout/LayoutHelper.cs @@ -1,4 +1,5 @@ using System; +using Avalonia.Utilities; using Avalonia.VisualTree; namespace Avalonia.Layout @@ -19,16 +20,11 @@ namespace Avalonia.Layout /// The control's size. public static Size ApplyLayoutConstraints(ILayoutable control, Size constraints) { - var controlWidth = control.Width; - var controlHeight = control.Height; - - double width = (controlWidth > 0) ? controlWidth : constraints.Width; - double height = (controlHeight > 0) ? controlHeight : constraints.Height; - width = Math.Min(width, control.MaxWidth); - width = Math.Max(width, control.MinWidth); - height = Math.Min(height, control.MaxHeight); - height = Math.Max(height, control.MinHeight); - return new Size(width, height); + var minmax = new MinMax(control); + + return new Size( + MathUtilities.Clamp(constraints.Width, minmax.MinWidth, minmax.MaxWidth), + MathUtilities.Clamp(constraints.Height, minmax.MinHeight, minmax.MaxHeight)); } public static Size MeasureChild(ILayoutable control, Size availableSize, Thickness padding, @@ -85,5 +81,39 @@ namespace Avalonia.Layout InnerInvalidateMeasure(control); } + + /// + /// Calculates the min and max height for a control. Ported from WPF. + /// + private readonly struct MinMax + { + public MinMax(ILayoutable e) + { + MaxHeight = e.MaxHeight; + MinHeight = e.MinHeight; + double l = e.Height; + + double height = (double.IsNaN(l) ? double.PositiveInfinity : l); + MaxHeight = Math.Max(Math.Min(height, MaxHeight), MinHeight); + + height = (double.IsNaN(l) ? 0 : l); + MinHeight = Math.Max(Math.Min(MaxHeight, height), MinHeight); + + MaxWidth = e.MaxWidth; + MinWidth = e.MinWidth; + l = e.Width; + + double width = (double.IsNaN(l) ? double.PositiveInfinity : l); + MaxWidth = Math.Max(Math.Min(width, MaxWidth), MinWidth); + + width = (double.IsNaN(l) ? 0 : l); + MinWidth = Math.Max(Math.Min(MaxWidth, width), MinWidth); + } + + public double MinWidth { get; } + public double MaxWidth { get; } + public double MinHeight { get; } + public double MaxHeight { get; } + } } }