From 5416c6028801b23f5660f32a7d0f098e7b71d3c2 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 17 May 2023 16:01:42 +0200 Subject: [PATCH 01/20] update sample for simulate issue --- .../ViewModels/ListBoxPageViewModel.cs | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs index 7f32536b11..9c30992624 100644 --- a/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs @@ -21,7 +21,7 @@ namespace ControlCatalog.ViewModels public ListBoxPageViewModel() { Items = new ObservableCollection(Enumerable.Range(1, 10000).Select(i => GenerateItem())); - + Selection = new SelectionModel(); Selection.Select(1); @@ -34,7 +34,13 @@ namespace ControlCatalog.ViewModels (t ? Avalonia.Controls.SelectionMode.Toggle : 0) | (a ? Avalonia.Controls.SelectionMode.AlwaysSelected : 0)); - AddItemCommand = MiniCommand.Create(() => Items.Add(GenerateItem())); + AddItemCommand = MiniCommand.Create(() => + { + var item = GenerateItem(); + Items.Add(item); + Selection.Clear(); + Selection.Select(Items.Count - 1); + }); RemoveItemCommand = MiniCommand.Create(() => { @@ -96,7 +102,7 @@ namespace ControlCatalog.ViewModels public MiniCommand RemoveItemCommand { get; } public MiniCommand SelectRandomItemCommand { get; } - private ItemModel GenerateItem() => new ItemModel(_counter ++); + private ItemModel GenerateItem() => new ItemModel(_counter++); } /// From 74412481d8a8a5b74bf3354c632294c080c9a447 Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 17 May 2023 16:02:27 +0200 Subject: [PATCH 02/20] fix: Issue #6263 --- .../Primitives/SelectingItemsControl.cs | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index af82a89517..03c6eebf13 100644 --- a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs +++ b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs @@ -5,9 +5,7 @@ using System.Collections.Specialized; using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Linq; -using System.Xml.Linq; using Avalonia.Controls.Selection; -using Avalonia.Controls.Utils; using Avalonia.Data; using Avalonia.Input; using Avalonia.Input.Platform; @@ -171,7 +169,7 @@ namespace Avalonia.Controls.Primitives /// public event EventHandler? SelectionChanged { - add => AddHandler(SelectionChangedEvent, value); + add => AddHandler(SelectionChangedEvent, value); remove => RemoveHandler(SelectionChangedEvent, value); } @@ -369,7 +367,7 @@ namespace Avalonia.Controls.Primitives /// public bool WrapSelection { - get => GetValue(WrapSelectionProperty); + get => GetValue(WrapSelectionProperty); set => SetValue(WrapSelectionProperty, value); } @@ -382,7 +380,7 @@ namespace Avalonia.Controls.Primitives /// protected SelectionMode SelectionMode { - get => GetValue(SelectionModeProperty); + get => GetValue(SelectionModeProperty); set => SetValue(SelectionModeProperty, value); } @@ -465,7 +463,10 @@ namespace Avalonia.Controls.Primitives protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) { base.OnAttachedToVisualTree(e); - AutoScrollToSelectedItemIfNecessary(); + if (Selection?.AnchorIndex is int index) + { + AutoScrollToSelectedItemIfNecessary(index); + } } /// @@ -476,7 +477,10 @@ namespace Avalonia.Controls.Primitives void ExecuteScrollWhenLayoutUpdated(object? sender, EventArgs e) { LayoutUpdated -= ExecuteScrollWhenLayoutUpdated; - AutoScrollToSelectedItemIfNecessary(); + if (Selection?.AnchorIndex is int index) + { + AutoScrollToSelectedItemIfNecessary(index); + } } if (AutoScrollToSelectedItem) @@ -657,7 +661,10 @@ namespace Avalonia.Controls.Primitives if (change.Property == AutoScrollToSelectedItemProperty) { - AutoScrollToSelectedItemIfNecessary(); + if (Selection?.AnchorIndex is int index) + { + AutoScrollToSelectedItemIfNecessary(index); + } } else if (change.Property == SelectionModeProperty && _selection is object) { @@ -916,8 +923,11 @@ namespace Avalonia.Controls.Primitives if (e.PropertyName == nameof(ISelectionModel.AnchorIndex)) { _hasScrolledToSelectedItem = false; - KeyboardNavigation.SetTabOnceActiveElement(this, ContainerFromIndex(Selection.AnchorIndex)); - AutoScrollToSelectedItemIfNecessary(); + if (Selection?.AnchorIndex is int index) + { + KeyboardNavigation.SetTabOnceActiveElement(this, ContainerFromIndex(index)); + AutoScrollToSelectedItemIfNecessary(index); + } } else if (e.PropertyName == nameof(ISelectionModel.SelectedIndex) && _oldSelectedIndex != SelectedIndex) { @@ -1045,7 +1055,7 @@ namespace Avalonia.Controls.Primitives return value; } else - { + { return AvaloniaProperty.UnsetValue; } } @@ -1103,16 +1113,19 @@ namespace Avalonia.Controls.Primitives } } - private void AutoScrollToSelectedItemIfNecessary() + private void AutoScrollToSelectedItemIfNecessary(int anchorIndex) { if (AutoScrollToSelectedItem && !_hasScrolledToSelectedItem && Presenter is object && - Selection.AnchorIndex >= 0 && + anchorIndex >= 0 && IsAttachedToVisualTree) { - ScrollIntoView(Selection.AnchorIndex); - _hasScrolledToSelectedItem = true; + Dispatcher.UIThread.Post(state => + { + ScrollIntoView((int)state!); + _hasScrolledToSelectedItem = true; + }, anchorIndex); } } From 176a6a83c5e1d3b1be5013713653a78d5869661c Mon Sep 17 00:00:00 2001 From: Giuseppe Lippolis Date: Wed, 17 May 2023 17:26:32 +0200 Subject: [PATCH 03/20] fix(test): call Threading.Dispatcher.UIThread.RunJobs(); --- tests/Avalonia.Controls.UnitTests/ListBoxTests.cs | 12 ++++++++++++ .../Primitives/SelectingItemsControlTests.cs | 10 +++++----- 2 files changed, 17 insertions(+), 5 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 72f476a3b0..732f888e49 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -431,6 +431,8 @@ namespace Avalonia.Controls.UnitTests items.Remove("1"); lm.ExecuteLayoutPass(); + Threading.Dispatcher.UIThread.RunJobs(); + Assert.Equal("30", target.ContainerFromIndex(items.Count - 1).DataContext); Assert.Equal("29", target.ContainerFromIndex(items.Count - 2).DataContext); Assert.Equal("28", target.ContainerFromIndex(items.Count - 3).DataContext); @@ -456,8 +458,13 @@ namespace Avalonia.Controls.UnitTests Prepare(target); + Threading.Dispatcher.UIThread.RunJobs(); + // First an item that is not index 0 must be selected. _mouse.Click(target.Presenter.Panel.Children[1]); + + Threading.Dispatcher.UIThread.RunJobs(); + Assert.Equal(1, target.Selection.AnchorIndex); // We're going to be clicking on item 9. @@ -470,6 +477,7 @@ namespace Avalonia.Controls.UnitTests // into view due to SelectionMode.AlwaysSelected. target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => { + Assert.Same(item, e.TargetObject); ++raised; }); @@ -477,6 +485,8 @@ namespace Avalonia.Controls.UnitTests // Click item 9. _mouse.Click(item); + Threading.Dispatcher.UIThread.RunJobs(); + Assert.Equal(1, raised); } } @@ -743,6 +753,8 @@ namespace Avalonia.Controls.UnitTests items.Reverse(); Layout(target); + Threading.Dispatcher.UIThread.RunJobs(); + realized = target.GetRealizedContainers() .Cast() .Select(x => (string)x.DataContext) diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index 51745e1687..7ce9992313 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -1536,7 +1536,7 @@ namespace Avalonia.Controls.UnitTests.Primitives Prepare(target); target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true); target.SelectedIndex = 2; - + Threading.Dispatcher.UIThread.RunJobs(); Assert.True(raised); } @@ -1561,7 +1561,7 @@ namespace Avalonia.Controls.UnitTests.Primitives target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true); target.SelectedIndex = 2; Prepare(target); - + Threading.Dispatcher.UIThread.RunJobs(); Assert.True(raised); } @@ -1632,7 +1632,7 @@ namespace Avalonia.Controls.UnitTests.Primitives root.Child = null; target.SelectedIndex = 1; root.Child = target; - + Threading.Dispatcher.UIThread.RunJobs(); Assert.True(raised); } @@ -1689,11 +1689,11 @@ namespace Avalonia.Controls.UnitTests.Primitives var raised = false; target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => raised = true); target.SelectedIndex = 2; - + Threading.Dispatcher.UIThread.RunJobs(); Assert.False(raised); target.AutoScrollToSelectedItem = true; - + Threading.Dispatcher.UIThread.RunJobs(); Assert.True(raised); } From 5157ebccaeb91825946cced24af1594ee83317aa Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 9 Jun 2023 08:02:36 +0300 Subject: [PATCH 04/20] Added failing test for #11668 --- .../TransitioningContentControlTests.cs | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs index 77fc207554..be3c35e8e0 100644 --- a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs @@ -62,6 +62,25 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, transition.StartCount); } + [Fact] + public void Control_Should_Connect_To_VisualTree_Once() + { + using var app = Start(); + var (target, transition) = CreateTarget(new Control()); + + var control = new Control(); + int counter = 0; + + control.AttachedToVisualTree += (s,e) => counter++; + + target.Content = control; + Layout(target); + target.Content = new Control(); + Layout(target); + + Assert.Equal(1, counter); + } + [Fact] public void ContentPresenters_Should_Be_Setup_For_Transition() { From 849d4995393cb6ca5be4a501a5da7dadc216eb74 Mon Sep 17 00:00:00 2001 From: daniel Date: Fri, 9 Jun 2023 08:13:35 +0300 Subject: [PATCH 05/20] Use two presenter --- .../TransitioningContentControl.cs | 81 ++++++++++++++----- .../Controls/TransitioningContentControl.xaml | 3 +- 2 files changed, 60 insertions(+), 24 deletions(-) diff --git a/src/Avalonia.Controls/TransitioningContentControl.cs b/src/Avalonia.Controls/TransitioningContentControl.cs index 21b9c9b765..bf540698f1 100644 --- a/src/Avalonia.Controls/TransitioningContentControl.cs +++ b/src/Avalonia.Controls/TransitioningContentControl.cs @@ -15,8 +15,9 @@ namespace Avalonia.Controls; public class TransitioningContentControl : ContentControl { private CancellationTokenSource? _currentTransition; - private ContentPresenter? _transitionPresenter; - private Optional _transitionFrom; + private ContentPresenter? _presenter2; + private bool _isFirstFull; + private bool _shouldAnimate; /// /// Defines the property. @@ -39,46 +40,52 @@ public class TransitioningContentControl : ContentControl { var result = base.ArrangeOverride(finalSize); - if (_transitionFrom.HasValue) + if (_shouldAnimate) { _currentTransition?.Cancel(); - if (_transitionPresenter is not null && + if (_presenter2 is not null && Presenter is Visual presenter && - PageTransition is { } transition && - (_transitionFrom.Value is not Visual v || v.VisualParent is null)) - { - _transitionPresenter.Content = _transitionFrom.Value; - _transitionPresenter.IsVisible = true; - _transitionFrom = Optional.Empty; + PageTransition is { } transition) + { + _shouldAnimate = false; var cancel = new CancellationTokenSource(); _currentTransition = cancel; - transition.Start(_transitionPresenter, presenter, true, cancel.Token).ContinueWith(x => + var from = _isFirstFull ? _presenter2 : presenter; + var to = _isFirstFull ? presenter : _presenter2; + + transition.Start(from, to, true, cancel.Token).ContinueWith(x => { if (!cancel.IsCancellationRequested) { - _transitionPresenter.Content = null; - _transitionPresenter.IsVisible = false; + HideOldPresenter(); } }, TaskScheduler.FromCurrentSynchronizationContext()); } - _transitionFrom = Optional.Empty; + _shouldAnimate = false; } return result; } + protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) + { + base.OnAttachedToVisualTree(e); + UpdateContent(false); + } + protected override bool RegisterContentPresenter(ContentPresenter presenter) { if (!base.RegisterContentPresenter(presenter) && presenter is ContentPresenter p && - p.Name == "PART_TransitionContentPresenter") + p.Name == "PART_ContentPresenter2") { - _transitionPresenter = p; - _transitionPresenter.IsVisible = false; + _presenter2 = p; + _presenter2.IsVisible = false; + UpdateContent(false); return true; } @@ -89,14 +96,44 @@ public class TransitioningContentControl : ContentControl { base.OnPropertyChanged(change); - if (change.Property == ContentProperty && - _transitionPresenter is not null && - Presenter is Visual && - PageTransition is not null) + if (change.Property == ContentProperty) { - _transitionFrom = change.GetOldValue(); + UpdateContent(true); + } + } + + private void UpdateContent(bool withTransition) + { + if (VisualRoot is null || _presenter2 is null || Presenter is null) + { + return; + } + + var currentPresenter = _isFirstFull ? _presenter2 : Presenter; + currentPresenter.Content = Content; + currentPresenter.IsVisible = true; + + _isFirstFull = !_isFirstFull; + + if (PageTransition is not null && withTransition) + { + _shouldAnimate = true; InvalidateArrange(); } + else + { + HideOldPresenter(); + } + } + + private void HideOldPresenter() + { + var oldPresenter = _isFirstFull ? _presenter2 : Presenter; + if (oldPresenter is not null) + { + oldPresenter.Content = null; + oldPresenter.IsVisible = false; + } } private class ImmutableCrossFade : IPageTransition diff --git a/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml b/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml index 2078322318..03cb09e395 100644 --- a/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml +++ b/src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml @@ -11,11 +11,10 @@ BorderThickness="{TemplateBinding BorderThickness}" CornerRadius="{TemplateBinding CornerRadius}" ContentTemplate="{TemplateBinding ContentTemplate}" - Content="{TemplateBinding Content}" Padding="{TemplateBinding Padding}" VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}" /> - Date: Fri, 9 Jun 2023 08:15:50 +0300 Subject: [PATCH 06/20] Fix tests --- .../TransitioningContentControlTests.cs | 53 +++++++++++-------- 1 file changed, 32 insertions(+), 21 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs index be3c35e8e0..02dd4e6c03 100644 --- a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs @@ -27,13 +27,13 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void TransitionContentPresenter_Should_Initially_Be_Hidden() + public void ContentPresenters2_Should_Initially_Be_Hidden() { using var app = Start(); var (target, transition) = CreateTarget("foo"); - var transitionPresenter = GetTransitionContentPresenter(target); + var presenter2 = GetContentPresenters2(target); - Assert.False(transitionPresenter.IsVisible); + Assert.False(presenter2.IsVisible); } [Fact] @@ -82,36 +82,49 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void ContentPresenters_Should_Be_Setup_For_Transition() + public void ContentPresenters2_Should_Be_Setup() { using var app = Start(); var (target, transition) = CreateTarget("foo"); - var transitionPresenter = GetTransitionContentPresenter(target); + var presenter1 = target.Presenter!; + var presenter2 = GetContentPresenters2(target); target.Content = "bar"; Layout(target); - Assert.True(transitionPresenter.IsVisible); - Assert.Equal("bar", target.Presenter!.Content); - Assert.Equal("foo", transitionPresenter.Content); + Assert.True(presenter2.IsVisible); + Assert.Equal("foo", presenter1.Content); + Assert.Equal("bar", presenter2.Content); } [Fact] - public void TransitionContentPresenter_Should_Be_Hidden_When_Transition_Completes() + public void Old_Presenter_Should_Be_Hidden_When_Transition_Completes() { using var app = Start(); using var sync = UnitTestSynchronizationContext.Begin(); var (target, transition) = CreateTarget("foo"); - var transitionPresenter = GetTransitionContentPresenter(target); + var presenter1 = target.Presenter!; + var presenter2 = GetContentPresenters2(target); target.Content = "bar"; Layout(target); - Assert.True(transitionPresenter.IsVisible); + Assert.True(presenter1.IsVisible); + Assert.True(presenter2.IsVisible); transition.Complete(); sync.ExecutePostedCallbacks(); + Assert.True(presenter2.IsVisible); + Assert.False(presenter1.IsVisible); - Assert.False(transitionPresenter.IsVisible); + target.Content = "foo"; + Layout(target); + Assert.True(presenter1.IsVisible); + Assert.True(presenter2.IsVisible); + + transition.Complete(); + sync.ExecutePostedCallbacks(); + Assert.True(presenter1.IsVisible); + Assert.False(presenter2.IsVisible); } [Fact] @@ -120,7 +133,6 @@ namespace Avalonia.Controls.UnitTests using var app = Start(); using var sync = UnitTestSynchronizationContext.Begin(); var (target, transition) = CreateTarget("foo"); - var transitionPresenter = GetTransitionContentPresenter(target); target.Content = "bar"; Layout(target); @@ -139,7 +151,7 @@ namespace Avalonia.Controls.UnitTests using var app = Start(); using var sync = UnitTestSynchronizationContext.Begin(); var (target, transition) = CreateTarget("foo"); - var transitionPresenter = GetTransitionContentPresenter(target); + var presenter2 = GetContentPresenters2(target); target.Content = "bar"; Layout(target); @@ -153,7 +165,7 @@ namespace Avalonia.Controls.UnitTests var fromPresenter = Assert.IsType(from); var toPresenter = Assert.IsType(to); - Assert.Same(transitionPresenter, fromPresenter); + Assert.Same(presenter2, fromPresenter); Assert.Same(target.Presenter, toPresenter); Assert.Equal("bar", fromPresenter.Content); Assert.Equal("baz", toPresenter.Content); @@ -168,7 +180,7 @@ namespace Avalonia.Controls.UnitTests Assert.Equal(1, startedRaised); Assert.Equal("baz", target.Presenter!.Content); - Assert.Equal("bar", transitionPresenter.Content); + Assert.Equal("bar", presenter2.Content); } private static IDisposable Start() @@ -206,22 +218,21 @@ namespace Avalonia.Controls.UnitTests new ContentPresenter { Name = "PART_ContentPresenter", - [!ContentPresenter.ContentProperty] = x[!ContentControl.ContentProperty], }, new ContentPresenter { - Name = "PART_TransitionContentPresenter", + Name = "PART_ContentPresenter2", }, } }; }); } - private static ContentPresenter GetTransitionContentPresenter(TransitioningContentControl target) + private static ContentPresenter GetContentPresenters2(TransitioningContentControl target) { return Assert.IsType(target .GetTemplateChildren() - .First(x => x.Name == "PART_TransitionContentPresenter")); + .First(x => x.Name == "PART_ContentPresenter2")); } private void Layout(Control c) @@ -246,7 +257,7 @@ namespace Avalonia.Controls.UnitTests if (_tcs is not null) throw new InvalidOperationException("Transition already running"); _tcs = new TaskCompletionSource(); - cancellationToken.Register(() => _tcs.TrySetResult()); + cancellationToken.Register(() => _tcs?.TrySetResult()); await _tcs.Task; _tcs = null; From 85ed4ea1c99083a268270e95bdbffa0342f9cbd5 Mon Sep 17 00:00:00 2001 From: Glen Stone Date: Sun, 11 Jun 2023 13:27:59 -0400 Subject: [PATCH 07/20] Fix AvaloniaTestAttribute base class --- src/Headless/Avalonia.Headless.NUnit/AvaloniaTest.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Headless/Avalonia.Headless.NUnit/AvaloniaTest.cs b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTest.cs index 94b75cf849..3e115fe5c1 100644 --- a/src/Headless/Avalonia.Headless.NUnit/AvaloniaTest.cs +++ b/src/Headless/Avalonia.Headless.NUnit/AvaloniaTest.cs @@ -13,7 +13,7 @@ namespace Avalonia.Headless.NUnit; /// such that awaited expressions resume on the test's "main thread". /// [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AvaloniaTestAttribute : TestCaseAttribute, IWrapSetUpTearDown +public sealed class AvaloniaTestAttribute : TestAttribute, IWrapSetUpTearDown { public TestCommand Wrap(TestCommand command) { From 089b732b40c138f651acb114361c83331011831c Mon Sep 17 00:00:00 2001 From: Glen Stone Date: Sun, 11 Jun 2023 14:22:18 -0400 Subject: [PATCH 08/20] Fix ignoring of AvalonitTest --- tests/Avalonia.Headless.UnitTests/ThreadingTests.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs index 403ff84f2c..5a6026c1de 100644 --- a/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs +++ b/tests/Avalonia.Headless.UnitTests/ThreadingTests.cs @@ -18,7 +18,7 @@ public class ThreadingTests } #if NUNIT - [AvaloniaTest(Ignore = "This test should always fail, enable to test if it fails")] + [AvaloniaTest, Ignore("This test should always fail, enable to test if it fails")] #elif XUNIT [AvaloniaFact(Skip = "This test should always fail, enable to test if it fails")] #endif From 0a3349b957dea3b9ba16e2302a66e8df8bf813b6 Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Mon, 12 Jun 2023 10:13:19 +0200 Subject: [PATCH 09/20] Previewer: fix transparent frame on update --- .../Remote/Server/RemoteServerTopLevelImpl.cs | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs index 49af6a71a0..74f12280bb 100644 --- a/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs +++ b/src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs @@ -295,10 +295,7 @@ namespace Avalonia.Controls.Remote.Server lock (_lock) { - // Ideally we should only send a frame if its status is Rendered: since the renderer might not be - // initialized at the start, we're sending black frames in this case. However, this was the historical - // behavior and some external programs are depending on receiving a frame asap. - if (_lastReceivedFrame != _lastSentFrame || _framebuffer.GetStatus() == FrameStatus.CopiedToMessage) + if (_lastReceivedFrame != _lastSentFrame || _framebuffer.GetStatus() != FrameStatus.Rendered) return; framebuffer = _framebuffer; From b727ada00a8a944067c62b3c76d8102881f1b95d Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Mon, 12 Jun 2023 14:39:49 +0200 Subject: [PATCH 10/20] Previewer: reuse last render scaling --- src/Avalonia.DesignerSupport/DesignWindowLoader.cs | 7 +++++++ .../Remote/PreviewerWindowImpl.cs | 4 ++-- .../Remote/RemoteDesignerEntryPoint.cs | 9 +++++---- 3 files changed, 14 insertions(+), 6 deletions(-) diff --git a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs index eff190c39e..c248116614 100644 --- a/src/Avalonia.DesignerSupport/DesignWindowLoader.cs +++ b/src/Avalonia.DesignerSupport/DesignWindowLoader.cs @@ -4,6 +4,7 @@ using System.Linq; using System.Reflection; using System.Text; using Avalonia.Controls; +using Avalonia.Controls.Embedding.Offscreen; using Avalonia.Controls.Platform; using Avalonia.Markup.Xaml; using Avalonia.Styling; @@ -13,6 +14,9 @@ namespace Avalonia.DesignerSupport public class DesignWindowLoader { public static Window LoadDesignerWindow(string xaml, string assemblyPath, string xamlFileProjectPath) + => LoadDesignerWindow(xaml, assemblyPath, xamlFileProjectPath, 1.0); + + public static Window LoadDesignerWindow(string xaml, string assemblyPath, string xamlFileProjectPath, double renderScaling) { Window window; Control control; @@ -96,6 +100,9 @@ namespace Avalonia.DesignerSupport window = new Window() {Content = (Control)control}; } + if (window.PlatformImpl is OffscreenTopLevelImplBase offscreenImpl) + offscreenImpl.RenderScaling = renderScaling; + Design.ApplyDesignModeProperties(window, control); if (!window.IsSet(Window.SizeToContentProperty)) diff --git a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs index b6c0c3ae3d..9463224b99 100644 --- a/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs +++ b/src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs @@ -67,8 +67,8 @@ namespace Avalonia.DesignerSupport.Remote { _transport.Send(new RequestViewportResizeMessage { - Width = clientSize.Width, - Height = clientSize.Height + Width = Math.Ceiling(clientSize.Width * RenderScaling), + Height = Math.Ceiling(clientSize.Height * RenderScaling) }); ClientSize = clientSize; RenderAndSendFrameIfNeeded(); diff --git a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs index 313063269b..6a6bc8c746 100644 --- a/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs +++ b/src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs @@ -1,13 +1,10 @@ using System; using System.Collections.Generic; -using System.Diagnostics.CodeAnalysis; using System.Net; using System.Reflection; using System.Threading; -using System.Xml; using Avalonia.Controls; using Avalonia.DesignerSupport.Remote.HtmlTransport; -using Avalonia.Input; using Avalonia.Remote.Protocol; using Avalonia.Remote.Protocol.Designer; using Avalonia.Remote.Protocol.Viewport; @@ -20,6 +17,7 @@ namespace Avalonia.DesignerSupport.Remote private static ClientSupportedPixelFormatsMessage s_supportedPixelFormats; private static ClientViewportAllocatedMessage s_viewportAllocatedMessage; private static ClientRenderInfoMessage s_renderInfoMessage; + private static double s_lastRenderScaling = 1.0; private static IAvaloniaRemoteTransportConnection s_transport; class CommandLineArgs @@ -226,6 +224,9 @@ namespace Avalonia.DesignerSupport.Remote } if (obj is UpdateXamlMessage xaml) { + if (s_currentWindow is not null) + s_lastRenderScaling = s_currentWindow.RenderScaling; + try { s_currentWindow?.Close(); @@ -237,7 +238,7 @@ namespace Avalonia.DesignerSupport.Remote s_currentWindow = null; try { - s_currentWindow = DesignWindowLoader.LoadDesignerWindow(xaml.Xaml, xaml.AssemblyPath, xaml.XamlFileProjectPath); + s_currentWindow = DesignWindowLoader.LoadDesignerWindow(xaml.Xaml, xaml.AssemblyPath, xaml.XamlFileProjectPath, s_lastRenderScaling); s_transport.Send(new UpdateXamlResultMessage(){Handle = s_currentWindow.PlatformImpl?.Handle?.Handle.ToString()}); } catch (Exception e) From 5cc95534824af434676ce4012f4b3d95be1cf32e Mon Sep 17 00:00:00 2001 From: Julien Lebosquain Date: Mon, 12 Jun 2023 14:45:46 +0200 Subject: [PATCH 11/20] Compositor: fix initial dirty rect with scaling < 1 --- .../Composition/Server/ServerCompositionTarget.cs | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs index 5e76ee56cf..8f1aa1cb49 100644 --- a/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs +++ b/src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs @@ -159,14 +159,15 @@ namespace Avalonia.Rendering.Composition.Server _redrawRequested = false; using (var targetContext = _renderTarget.CreateDrawingContext()) { - var layerSize = Size * Scaling; + var size = Size; + var layerSize = size * Scaling; if (layerSize != _layerSize || _layer == null || _layer.IsCorrupted) { _layer?.Dispose(); _layer = null; - _layer = targetContext.CreateLayer(Size); + _layer = targetContext.CreateLayer(size); _layerSize = layerSize; - _dirtyRect = new Rect(0, 0, layerSize.Width, layerSize.Height); + _dirtyRect = new Rect(0, 0, size.Width, size.Height); } if (_dirtyRect.Width != 0 || _dirtyRect.Height != 0) @@ -187,7 +188,7 @@ namespace Avalonia.Rendering.Composition.Server else targetContext.DrawBitmap(_layer, 1, new Rect(_layerSize), - new Rect(Size)); + new Rect(size)); if (DebugOverlays != RendererDebugOverlays.None) { From 475f22f6f6858421e066acbf81b2a566d1ce1929 Mon Sep 17 00:00:00 2001 From: Benedikt Stebner Date: Tue, 13 Jun 2023 13:28:52 +0200 Subject: [PATCH 12/20] Fix visual tree handling when Inlines is reset --- src/Avalonia.Controls/TextBlock.cs | 12 +------ .../TextBlockTests.cs | 33 +++++++++++++++++++ 2 files changed, 34 insertions(+), 11 deletions(-) diff --git a/src/Avalonia.Controls/TextBlock.cs b/src/Avalonia.Controls/TextBlock.cs index 57d709ba94..ea420c7c45 100644 --- a/src/Avalonia.Controls/TextBlock.cs +++ b/src/Avalonia.Controls/TextBlock.cs @@ -668,17 +668,7 @@ namespace Avalonia.Controls if (HasComplexContent) { - if (_textRuns != null) - { - foreach (var textRun in _textRuns) - { - if (textRun is EmbeddedControlRun controlRun && - controlRun.Control is Control control) - { - VisualChildren.Remove(control); - } - } - } + VisualChildren.Clear(); var textRuns = new List(); diff --git a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs index e9250788c0..eb1d6f5ea4 100644 --- a/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TextBlockTests.cs @@ -115,6 +115,39 @@ namespace Avalonia.Controls.UnitTests } } + [Fact] + public void Changing_Inlines_Should_Reset_InlineUIContainer_VisualParent_On_Measure() + { + using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface)) + { + var target = new TextBlock(); + + var control = new Control(); + + var run = new InlineUIContainer(control); + + target.Inlines.Add(run); + + target.Measure(Size.Infinity); + + Assert.True(target.IsMeasureValid); + + Assert.Equal(target, control.VisualParent); + + target.Inlines = null; + + Assert.Null(run.Parent); + + target.Inlines = new InlineCollection { new Run("Hello World") }; + + Assert.Null(run.Parent); + + target.Measure(Size.Infinity); + + Assert.Null(control.VisualParent); + } + } + [Fact] public void InlineUIContainer_Child_Schould_Be_Arranged() { From d407764b84d4d71a2eb3e53c6cfe2ee0500e461c Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 13 Jun 2023 21:21:30 -0400 Subject: [PATCH 13/20] Remove some duplicate periods --- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 2 +- src/Avalonia.Base/Platform/IPlatformRenderInterface.cs | 2 +- src/Avalonia.Base/Utilities/TypeUtilities.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 0c22213d33..af64e5646f 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -334,7 +334,7 @@ namespace Avalonia /// . /// /// The type of the property change sender. - /// /// The type of the property.. + /// /// The type of the property. /// The property changed observable. /// /// The method to call. The parameters are the sender and the event args. diff --git a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs index 6f62c3be1d..57fedb3d69 100644 --- a/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs +++ b/src/Avalonia.Base/Platform/IPlatformRenderInterface.cs @@ -18,7 +18,7 @@ namespace Avalonia.Platform /// Creates an ellipse geometry implementation. /// /// The bounds of the ellipse. - /// An ellipse geometry.. + /// An ellipse geometry. IGeometryImpl CreateEllipseGeometry(Rect rect); /// diff --git a/src/Avalonia.Base/Utilities/TypeUtilities.cs b/src/Avalonia.Base/Utilities/TypeUtilities.cs index 3a82bf02e0..7dbb0872f5 100644 --- a/src/Avalonia.Base/Utilities/TypeUtilities.cs +++ b/src/Avalonia.Base/Utilities/TypeUtilities.cs @@ -306,7 +306,7 @@ namespace Avalonia.Utilities /// if the value could not be converted. /// /// The value to convert. - /// The type to convert to.. + /// The type to convert to. /// The culture to use. /// A value of . [RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)] From 7548238388fce993e66b27d3542a4130875484e4 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 13 Jun 2023 21:21:56 -0400 Subject: [PATCH 14/20] Fix EllipseGeometry clone method --- src/Avalonia.Base/Media/EllipseGeometry.cs | 41 +++++++++++++++++++++- 1 file changed, 40 insertions(+), 1 deletion(-) diff --git a/src/Avalonia.Base/Media/EllipseGeometry.cs b/src/Avalonia.Base/Media/EllipseGeometry.cs index 8211855324..84d74e888e 100644 --- a/src/Avalonia.Base/Media/EllipseGeometry.cs +++ b/src/Avalonia.Base/Media/EllipseGeometry.cs @@ -56,6 +56,10 @@ namespace Avalonia.Media /// /// Gets or sets a rect that defines the bounds of the ellipse. /// + /// + /// When set, this takes priority over the other properties that define an + /// ellipse using a center point and X/Y-axis radii. + /// public Rect Rect { get => GetValue(RectProperty); @@ -65,6 +69,10 @@ namespace Avalonia.Media /// /// Gets or sets a double that defines the radius in the X-axis of the ellipse. /// + /// + /// In order for this property to be used, must not be set + /// (equal to the default value). + /// public double RadiusX { get => GetValue(RadiusXProperty); @@ -74,6 +82,10 @@ namespace Avalonia.Media /// /// Gets or sets a double that defines the radius in the Y-axis of the ellipse. /// + /// + /// In order for this property to be used, must not be set + /// (equal to the default value). + /// public double RadiusY { get => GetValue(RadiusYProperty); @@ -83,6 +95,10 @@ namespace Avalonia.Media /// /// Gets or sets a point that defines the center of the ellipse. /// + /// + /// In order for this property to be used, must not be set + /// (equal to the default value). + /// public Point Center { get => GetValue(CenterProperty); @@ -92,7 +108,30 @@ namespace Avalonia.Media /// public override Geometry Clone() { - return new EllipseGeometry(Rect); + // Note that the ellipse properties are used in two modes: + // + // 1. Rect-only Mode: + // Directly set the rectangle bounds the ellipse will fill + // + // 2. Center + Radii Mode: + // Set a center-point and then X/Y-axis radii that are used to + // calculate the rectangle bounds the ellipse will fill. + // This is the only mode supported by WPF. + // + // Rendering the ellipse will only ever use one of these two modes + // based on if the Rect property is set (not equal to default). + // + // This means it would normally be fine to copy ONLY the Rect property + // when it is set. However, while it would render the same, it isn't + // a true clone. We want to include all the properties here regardless + // of the rendering mode that will eventually be used. + return new EllipseGeometry() + { + Rect = Rect, + RadiusX = RadiusX, + RadiusY = RadiusY, + Center = Center, + }; } /// From 36ea4a69c836a07b74df6970df656cdf67330385 Mon Sep 17 00:00:00 2001 From: robloo Date: Tue, 13 Jun 2023 21:43:02 -0400 Subject: [PATCH 15/20] Comment syntax fixes --- src/Avalonia.Base/AvaloniaObjectExtensions.cs | 2 +- src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs | 2 +- .../Primitives/PopupPositioning/IPopupPositioner.cs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index af64e5646f..b3f41eb420 100644 --- a/src/Avalonia.Base/AvaloniaObjectExtensions.cs +++ b/src/Avalonia.Base/AvaloniaObjectExtensions.cs @@ -334,7 +334,7 @@ namespace Avalonia /// . /// /// The type of the property change sender. - /// /// The type of the property. + /// The type of the property. /// The property changed observable. /// /// The method to call. The parameters are the sender and the event args. diff --git a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs index 3406432ce7..f418d4e14a 100644 --- a/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs +++ b/src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs @@ -687,7 +687,7 @@ namespace Avalonia.Media.TextFormatting.Unicode /// /// This method resolves the sos and eos values for the run /// and adds the run to the list - /// /// + /// /// The index of the start of the run (in x9 removed units) /// The length of the run (in x9 removed units) /// The level of the run diff --git a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs index 0c9bb89caa..4029782772 100644 --- a/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs +++ b/src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs @@ -216,7 +216,7 @@ namespace Avalonia.Controls.Primitives.PopupPositioning /// /// If the adjusted position also ends up being constrained, the resulting position of the /// FlipX adjustment will be the one before the adjustment. - /// /// + /// FlipX = 4, /// From c6ab46178c5360ae7acb0bbe9f427171baf9d811 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jun 2023 15:35:24 +0200 Subject: [PATCH 16/20] Update ncrunch configuration. --- .ncrunch/MobileSandbox.Browser.v3.ncrunchproject | 5 +++++ .ncrunch/WindowsInteropTest.net461.v3.ncrunchproject | 5 +++++ .ncrunch/WindowsInteropTest.net6.0-windows.v3.ncrunchproject | 5 +++++ 3 files changed, 15 insertions(+) create mode 100644 .ncrunch/MobileSandbox.Browser.v3.ncrunchproject create mode 100644 .ncrunch/WindowsInteropTest.net461.v3.ncrunchproject create mode 100644 .ncrunch/WindowsInteropTest.net6.0-windows.v3.ncrunchproject diff --git a/.ncrunch/MobileSandbox.Browser.v3.ncrunchproject b/.ncrunch/MobileSandbox.Browser.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/MobileSandbox.Browser.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/WindowsInteropTest.net461.v3.ncrunchproject b/.ncrunch/WindowsInteropTest.net461.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/WindowsInteropTest.net461.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file diff --git a/.ncrunch/WindowsInteropTest.net6.0-windows.v3.ncrunchproject b/.ncrunch/WindowsInteropTest.net6.0-windows.v3.ncrunchproject new file mode 100644 index 0000000000..319cd523ce --- /dev/null +++ b/.ncrunch/WindowsInteropTest.net6.0-windows.v3.ncrunchproject @@ -0,0 +1,5 @@ + + + True + + \ No newline at end of file From 5fb0cf7ddaf27efc805a997144b502a99e8569fd Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jun 2023 15:35:44 +0200 Subject: [PATCH 17/20] Added failing test for #11617. --- .../Selection/SelectionModelTests_Single.cs | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) diff --git a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs index c163298b40..4b652c68c5 100644 --- a/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs +++ b/tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs @@ -300,6 +300,27 @@ namespace Avalonia.Controls.UnitTests.Selection target.Source = new[] { 1, 2, 3 }; } + + [Fact] + public void Can_Change_Source_In_SelectedItem_Change_Handler() + { + // Issue #11617 + var target = CreateTarget(); + var raised = 0; + + target.PropertyChanged += (s, e) => + { + if (e.PropertyName == nameof(target.SelectedItem) && raised == 0) + { + ++raised; + target.Source = new[] { "foo", "baz", "bar" }; + } + }; + + target.SelectedIndex = 1; + + Assert.Equal(-1, target.SelectedIndex); + } } public class SelectedIndex From 1dc19f16278d22530aa2b82f6f70ef6d2fb0cef6 Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jun 2023 15:53:53 +0200 Subject: [PATCH 18/20] Checking _operation for null isn't enough. Instead check if `UpdateCount` is not 0 to allow recursive setting of `Source`. I have a vague feeling that this isn't actually the correct solution, and that more issues are going to arise around this scenario but for now I'm unable to create a failing test so let's go with the simple solution for now. Fixes #11617 --- src/Avalonia.Controls/Selection/SelectionModel.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Avalonia.Controls/Selection/SelectionModel.cs b/src/Avalonia.Controls/Selection/SelectionModel.cs index 68bad598d0..69bed2550e 100644 --- a/src/Avalonia.Controls/Selection/SelectionModel.cs +++ b/src/Avalonia.Controls/Selection/SelectionModel.cs @@ -277,7 +277,7 @@ namespace Avalonia.Controls.Selection { if (base.Source != value) { - if (_operation is not null) + if (_operation?.UpdateCount > 0) { throw new InvalidOperationException("Cannot change source while update is in progress."); } From 18e0512d81cb22989bc6adde3b3465ac22eae0ec Mon Sep 17 00:00:00 2001 From: Steven Kirk Date: Wed, 14 Jun 2023 16:43:26 +0200 Subject: [PATCH 19/20] Added failing test for #11679. --- .../VirtualizingStackPanelTests.cs | 61 ++++++++++++++++++- 1 file changed, 58 insertions(+), 3 deletions(-) diff --git a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs index 4be888f96d..ba4fb32067 100644 --- a/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs @@ -367,6 +367,38 @@ namespace Avalonia.Controls.UnitTests Assert.False(originalFocused.IsVisible); } + [Fact] + public void Focused_Element_Losing_Focus_Does_Not_Reset_Selection() + { + using var app = App(); + var (target, scroll, listBox) = CreateTarget( + styles: new[] + { + new Style(x => x.OfType()) + { + Setters = + { + new Setter(ListBoxItem.TemplateProperty, ListBoxItemTemplate()), + } + } + }); + + listBox.SelectedIndex = 0; + + var selectedContainer = target.GetRealizedElements().First()!; + selectedContainer.Focusable = true; + selectedContainer.Focus(); + + scroll.Offset = new Vector(0, 500); + Layout(target); + + var newFocused = target.GetRealizedElements().First()!; + newFocused.Focusable = true; + newFocused.Focus(); + + Assert.Equal(0, listBox.SelectedIndex); + } + [Fact] public void Removing_Range_When_Scrolled_To_End_Updates_Viewport() { @@ -776,7 +808,19 @@ namespace Avalonia.Controls.UnitTests Optional itemTemplate = default, IEnumerable