From 2cb3b5f9163b8232ccde9276b67a25517b24bba4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Jos=C3=A9=20Pedro?= Date: Sun, 1 Mar 2020 23:26:06 +0000 Subject: [PATCH 01/16] Updated obsolete API usages in interfaces. --- src/Avalonia.Input/FocusManager.cs | 14 +++++++------- src/Avalonia.Input/IFocusManager.cs | 6 +++--- src/Avalonia.Input/IKeyboardDevice.cs | 2 +- src/Avalonia.Input/IKeyboardNavigationHandler.cs | 6 +++--- src/Avalonia.Input/KeyboardDevice.cs | 4 ++-- src/Avalonia.Input/KeyboardNavigationHandler.cs | 8 ++++---- .../WinForms/WinFormsAvaloniaControlHost.cs | 2 +- .../Avalonia.Win32/Input/WindowsKeyboardDevice.cs | 2 +- .../KeyboardDeviceTests.cs | 4 ++-- 9 files changed, 24 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Input/FocusManager.cs b/src/Avalonia.Input/FocusManager.cs index a9ce8ee494..85f5ffefdc 100644 --- a/src/Avalonia.Input/FocusManager.cs +++ b/src/Avalonia.Input/FocusManager.cs @@ -56,11 +56,11 @@ namespace Avalonia.Input /// /// The control to focus. /// The method by which focus was changed. - /// Any input modifiers active at the time of focus. + /// Any key modifiers active at the time of focus. public void Focus( IInputElement control, NavigationMethod method = NavigationMethod.Unspecified, - InputModifiers modifiers = InputModifiers.None) + KeyModifiers keyModifiers = KeyModifiers.None) { if (control != null) { @@ -70,7 +70,7 @@ namespace Avalonia.Input if (scope != null) { Scope = scope; - SetFocusedElement(scope, control, method, modifiers); + SetFocusedElement(scope, control, method, keyModifiers); } } else if (Current != null) @@ -98,7 +98,7 @@ namespace Avalonia.Input /// The focus scope. /// The element to focus. May be null. /// The method by which focus was changed. - /// Any input modifiers active at the time of focus. + /// Any key modifiers active at the time of focus. /// /// If the specified scope is the current then the keyboard focus /// will change. @@ -107,7 +107,7 @@ namespace Avalonia.Input IFocusScope scope, IInputElement element, NavigationMethod method = NavigationMethod.Unspecified, - InputModifiers modifiers = InputModifiers.None) + KeyModifiers keyModifiers = KeyModifiers.None) { Contract.Requires(scope != null); @@ -126,7 +126,7 @@ namespace Avalonia.Input if (Scope == scope) { - KeyboardDevice.Instance?.SetFocusedElement(element, method, modifiers); + KeyboardDevice.Instance?.SetFocusedElement(element, method, keyModifiers); } } @@ -198,7 +198,7 @@ namespace Avalonia.Input { if (element is IInputElement inputElement && CanFocus(inputElement)) { - Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.InputModifiers); + Instance?.Focus(inputElement, NavigationMethod.Pointer, ev.KeyModifiers); break; } diff --git a/src/Avalonia.Input/IFocusManager.cs b/src/Avalonia.Input/IFocusManager.cs index 29a22b86b2..9d4ccab5b8 100644 --- a/src/Avalonia.Input/IFocusManager.cs +++ b/src/Avalonia.Input/IFocusManager.cs @@ -23,11 +23,11 @@ namespace Avalonia.Input /// /// The control to focus. /// The method by which focus was changed. - /// Any input modifiers active at the time of focus. + /// Any key modifiers active at the time of focus. void Focus( - IInputElement control, + IInputElement control, NavigationMethod method = NavigationMethod.Unspecified, - InputModifiers modifiers = InputModifiers.None); + KeyModifiers keyModifiers = KeyModifiers.None); /// /// Notifies the focus manager of a change in focus scope. diff --git a/src/Avalonia.Input/IKeyboardDevice.cs b/src/Avalonia.Input/IKeyboardDevice.cs index 144979523f..90e5541e22 100644 --- a/src/Avalonia.Input/IKeyboardDevice.cs +++ b/src/Avalonia.Input/IKeyboardDevice.cs @@ -66,6 +66,6 @@ namespace Avalonia.Input void SetFocusedElement( IInputElement element, NavigationMethod method, - InputModifiers modifiers); + KeyModifiers modifiers); } } diff --git a/src/Avalonia.Input/IKeyboardNavigationHandler.cs b/src/Avalonia.Input/IKeyboardNavigationHandler.cs index db3a3cf114..4c5dba09fb 100644 --- a/src/Avalonia.Input/IKeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/IKeyboardNavigationHandler.cs @@ -22,10 +22,10 @@ namespace Avalonia.Input /// /// The current element. /// The direction to move. - /// Any input modifiers active at the time of focus. + /// Any key modifiers active at the time of focus. void Move( IInputElement element, NavigationDirection direction, - InputModifiers modifiers = InputModifiers.None); + KeyModifiers keyModifiers = KeyModifiers.None); } -} \ No newline at end of file +} diff --git a/src/Avalonia.Input/KeyboardDevice.cs b/src/Avalonia.Input/KeyboardDevice.cs index 9db8525591..391ed58607 100644 --- a/src/Avalonia.Input/KeyboardDevice.cs +++ b/src/Avalonia.Input/KeyboardDevice.cs @@ -38,7 +38,7 @@ namespace Avalonia.Input public void SetFocusedElement( IInputElement element, NavigationMethod method, - InputModifiers modifiers) + KeyModifiers keyModifiers) { if (element != FocusedElement) { @@ -56,7 +56,7 @@ namespace Avalonia.Input { RoutedEvent = InputElement.GotFocusEvent, NavigationMethod = method, - InputModifiers = modifiers, + KeyModifiers = keyModifiers, }); } } diff --git a/src/Avalonia.Input/KeyboardNavigationHandler.cs b/src/Avalonia.Input/KeyboardNavigationHandler.cs index aef98ddff8..36c9032e2f 100644 --- a/src/Avalonia.Input/KeyboardNavigationHandler.cs +++ b/src/Avalonia.Input/KeyboardNavigationHandler.cs @@ -94,11 +94,11 @@ namespace Avalonia.Input /// /// The current element. /// The direction to move. - /// Any input modifiers active at the time of focus. + /// Any key modifiers active at the time of focus. public void Move( IInputElement element, NavigationDirection direction, - InputModifiers modifiers = InputModifiers.None) + KeyModifiers keyModifiers = KeyModifiers.None) { Contract.Requires(element != null); @@ -109,7 +109,7 @@ namespace Avalonia.Input var method = direction == NavigationDirection.Next || direction == NavigationDirection.Previous ? NavigationMethod.Tab : NavigationMethod.Directional; - FocusManager.Instance.Focus(next, method, modifiers); + FocusManager.Instance.Focus(next, method, keyModifiers); } } @@ -126,7 +126,7 @@ namespace Avalonia.Input { var direction = (e.KeyModifiers & KeyModifiers.Shift) == 0 ? NavigationDirection.Next : NavigationDirection.Previous; - Move(current, direction, e.Modifiers); + Move(current, direction, e.KeyModifiers); e.Handled = true; } } diff --git a/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs b/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs index fe626f4d38..abace92f08 100644 --- a/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs +++ b/src/Windows/Avalonia.Win32.Interop/WinForms/WinFormsAvaloniaControlHost.cs @@ -45,7 +45,7 @@ namespace Avalonia.Win32.Embedding focused = focused.VisualParent; if (focused == _root) - KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, InputModifiers.None); + KeyboardDevice.Instance.SetFocusedElement(null, NavigationMethod.Unspecified, KeyModifiers.None); } private void PlatformImpl_LostFocus() diff --git a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs index fda5483b00..6fce14dbe8 100644 --- a/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs +++ b/src/Windows/Avalonia.Win32/Input/WindowsKeyboardDevice.cs @@ -47,7 +47,7 @@ namespace Avalonia.Win32.Input public void WindowActivated(Window window) { - SetFocusedElement(window, NavigationMethod.Unspecified, InputModifiers.None); + SetFocusedElement(window, NavigationMethod.Unspecified, KeyModifiers.None); } public string StringFromVirtualKey(uint virtualKey) diff --git a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs index 3c8e800fca..df0a077c7f 100644 --- a/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs +++ b/tests/Avalonia.Input.UnitTests/KeyboardDeviceTests.cs @@ -35,7 +35,7 @@ namespace Avalonia.Input.UnitTests target.SetFocusedElement( focused.Object, NavigationMethod.Unspecified, - InputModifiers.None); + KeyModifiers.None); target.ProcessRawEvent( new RawKeyEventArgs( @@ -75,7 +75,7 @@ namespace Avalonia.Input.UnitTests target.SetFocusedElement( focused.Object, NavigationMethod.Unspecified, - InputModifiers.None); + KeyModifiers.None); target.ProcessRawEvent( new RawTextInputEventArgs( From 99e5625ca63810a9a81710772136244b66b9eed7 Mon Sep 17 00:00:00 2001 From: Deadpikle Date: Mon, 6 Apr 2020 17:34:56 -0400 Subject: [PATCH 02/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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/16] 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 b80c61892e80df8383efb16eb1ce91d7ca11bd7d Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Sat, 22 Feb 2020 12:49:58 +0100 Subject: [PATCH 11/16] Added failing test for #3551 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Dariusz Komosiński --- .../TreeViewTests.cs | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index 32b09e2c47..bd303a81cd 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1002,6 +1002,35 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, child2Node.Presenter.Panel.Children.Count); } + [Fact] + public void Clearing_TreeView_Items_Clears_Index() + { + // Issue #3551 + var tree = CreateTestTreeData(); + var target = new TreeView + { + Template = CreateTreeViewTemplate(), + Items = tree, + }; + + var root = new TestRoot(); + root.Child = target; + + CreateNodeDataTemplate(target); + ApplyTemplates(target); + + var rootNode = tree[0]; + var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(rootNode); + + Assert.NotNull(container); + + root.Child = null; + + tree.Clear(); + + Assert.Empty(target.ItemContainerGenerator.Index.Containers); + } + private void ApplyTemplates(TreeView tree) { tree.ApplyTemplate(); From 3f3fb1a3f897a803e19998b17f81d1c929afa2e6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Thu, 9 Apr 2020 10:41:16 +0200 Subject: [PATCH 12/16] Update TreeView index when not attached to logical tree. Fixes #3551. --- src/Avalonia.Controls/TreeViewItem.cs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/src/Avalonia.Controls/TreeViewItem.cs b/src/Avalonia.Controls/TreeViewItem.cs index a224ceaadd..d3bd45d13c 100644 --- a/src/Avalonia.Controls/TreeViewItem.cs +++ b/src/Avalonia.Controls/TreeViewItem.cs @@ -51,6 +51,7 @@ namespace Avalonia.Controls SelectableMixin.Attach(IsSelectedProperty); FocusableProperty.OverrideDefaultValue(true); ItemsPanelProperty.OverrideDefaultValue(DefaultPanel); + ParentProperty.Changed.AddClassHandler((o, e) => o.OnParentChanged(e)); RequestBringIntoViewEvent.AddClassHandler((x, e) => x.OnRequestBringIntoView(e)); } @@ -179,5 +180,16 @@ namespace Avalonia.Controls return logical != null ? result : @default; } + + private void OnParentChanged(AvaloniaPropertyChangedEventArgs e) + { + if (!((ILogical)this).IsAttachedToLogicalTree && e.NewValue is null) + { + // If we're not attached to the logical tree, then OnDetachedFromLogicalTree isn't going to be + // called when the item is removed. This results in the item not being removed from the index, + // causing #3551. In this case, update the index when Parent is changed to null. + ItemContainerGenerator.UpdateIndex(); + } + } } } From 073bbb42e4e9c687db2c9146369df054e1a203e4 Mon Sep 17 00:00:00 2001 From: "Luis v.d.Eltz" Date: Thu, 9 Apr 2020 11:00:14 +0200 Subject: [PATCH 13/16] Fixed typo --- src/Avalonia.Base/Threading/DispatcherPriority.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Threading/DispatcherPriority.cs b/src/Avalonia.Base/Threading/DispatcherPriority.cs index ceda1c397f..a2b4b86bac 100644 --- a/src/Avalonia.Base/Threading/DispatcherPriority.cs +++ b/src/Avalonia.Base/Threading/DispatcherPriority.cs @@ -17,7 +17,7 @@ namespace Avalonia.Threading SystemIdle = 1, /// - /// The job will be processed when the application sis idle. + /// The job will be processed when the application is idle. /// ApplicationIdle = 2, From faec3ae67f23e8f3d90cf1ac166c88b424b6215e Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Fri, 10 Apr 2020 09:03:32 +0200 Subject: [PATCH 14/16] 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 48abbcdf9111d700a58ae52fb4930e4c7815fb27 Mon Sep 17 00:00:00 2001 From: Nikita Tsukanov Date: Tue, 14 Apr 2020 15:46:33 +0300 Subject: [PATCH 15/16] [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 16/16] 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'