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 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++); } /// diff --git a/src/Avalonia.Base/AvaloniaObjectExtensions.cs b/src/Avalonia.Base/AvaloniaObjectExtensions.cs index 0c22213d33..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/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, + }; } /// 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.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/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) { 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)] 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, /// diff --git a/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs b/src/Avalonia.Controls/Primitives/SelectingItemsControl.cs index 0e627c2a37..e22d03273a 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) @@ -529,7 +533,16 @@ namespace Avalonia.Controls.Primitives protected internal override void ClearContainerForItemOverride(Control element) { base.ClearContainerForItemOverride(element); - element.ClearValue(IsSelectedProperty); + + try + { + _ignoreContainerSelectionChanged = true; + element.ClearValue(IsSelectedProperty); + } + finally + { + _ignoreContainerSelectionChanged = false; + } } /// @@ -625,7 +638,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) { @@ -909,8 +925,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) { @@ -1038,7 +1057,7 @@ namespace Avalonia.Controls.Primitives return value; } else - { + { return AvaloniaProperty.UnsetValue; } } @@ -1096,16 +1115,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); } } 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; 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."); } 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/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.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) 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}" /> - [AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)] -public sealed class AvaloniaTestAttribute : TestCaseAttribute, IWrapSetUpTearDown +public sealed class AvaloniaTestAttribute : TestAttribute, IWrapSetUpTearDown { public TestCommand Wrap(TestCommand command) { diff --git a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs index 687338ece0..aeebfabd41 100644 --- a/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ListBoxTests.cs @@ -432,6 +432,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); @@ -457,8 +459,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. @@ -471,6 +478,7 @@ namespace Avalonia.Controls.UnitTests // into view due to SelectionMode.AlwaysSelected. target.AddHandler(Control.RequestBringIntoViewEvent, (s, e) => { + Assert.Same(item, e.TargetObject); ++raised; }); @@ -478,6 +486,8 @@ namespace Avalonia.Controls.UnitTests // Click item 9. _mouse.Click(item); + Threading.Dispatcher.UIThread.RunJobs(); + Assert.Equal(1, raised); } } @@ -744,6 +754,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 6e49fb8ef7..5fba91fafb 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); } 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 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() { diff --git a/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs b/tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs index 77fc207554..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] @@ -63,36 +63,68 @@ namespace Avalonia.Controls.UnitTests } [Fact] - public void ContentPresenters_Should_Be_Setup_For_Transition() + 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 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] @@ -101,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); @@ -120,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); @@ -134,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); @@ -149,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() @@ -187,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) @@ -227,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; 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