Browse Source

Merge branch 'master' into fix-headless-layout

pull/11763/head
Jan Kristian Bjerke 3 years ago
committed by GitHub
parent
commit
380f1292ea
No known key found for this signature in database GPG Key ID: 4AEE18F83AFDEB23
  1. 5
      .ncrunch/MobileSandbox.Browser.v3.ncrunchproject
  2. 5
      .ncrunch/WindowsInteropTest.net461.v3.ncrunchproject
  3. 5
      .ncrunch/WindowsInteropTest.net6.0-windows.v3.ncrunchproject
  4. 12
      samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs
  5. 2
      src/Avalonia.Base/AvaloniaObjectExtensions.cs
  6. 41
      src/Avalonia.Base/Media/EllipseGeometry.cs
  7. 2
      src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs
  8. 2
      src/Avalonia.Base/Platform/IPlatformRenderInterface.cs
  9. 9
      src/Avalonia.Base/Rendering/Composition/Server/ServerCompositionTarget.cs
  10. 2
      src/Avalonia.Base/Utilities/TypeUtilities.cs
  11. 2
      src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
  12. 54
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  13. 5
      src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
  14. 2
      src/Avalonia.Controls/Selection/SelectionModel.cs
  15. 12
      src/Avalonia.Controls/TextBlock.cs
  16. 81
      src/Avalonia.Controls/TransitioningContentControl.cs
  17. 7
      src/Avalonia.DesignerSupport/DesignWindowLoader.cs
  18. 4
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowImpl.cs
  19. 9
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  20. 3
      src/Avalonia.Themes.Fluent/Controls/TransitioningContentControl.xaml
  21. 2
      src/Headless/Avalonia.Headless.NUnit/AvaloniaTest.cs
  22. 12
      tests/Avalonia.Controls.UnitTests/ListBoxTests.cs
  23. 10
      tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs
  24. 21
      tests/Avalonia.Controls.UnitTests/Selection/SelectionModelTests_Single.cs
  25. 33
      tests/Avalonia.Controls.UnitTests/TextBlockTests.cs
  26. 72
      tests/Avalonia.Controls.UnitTests/TransitioningContentControlTests.cs
  27. 61
      tests/Avalonia.Controls.UnitTests/VirtualizingStackPanelTests.cs
  28. 2
      tests/Avalonia.Headless.UnitTests/ThreadingTests.cs

5
.ncrunch/MobileSandbox.Browser.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/WindowsInteropTest.net461.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

5
.ncrunch/WindowsInteropTest.net6.0-windows.v3.ncrunchproject

@ -0,0 +1,5 @@
<ProjectConfiguration>
<Settings>
<IgnoreThisComponentCompletely>True</IgnoreThisComponentCompletely>
</Settings>
</ProjectConfiguration>

12
samples/ControlCatalog/ViewModels/ListBoxPageViewModel.cs

@ -21,7 +21,7 @@ namespace ControlCatalog.ViewModels
public ListBoxPageViewModel()
{
Items = new ObservableCollection<ItemModel>(Enumerable.Range(1, 10000).Select(i => GenerateItem()));
Selection = new SelectionModel<ItemModel>();
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++);
}
/// <summary>

2
src/Avalonia.Base/AvaloniaObjectExtensions.cs

@ -334,7 +334,7 @@ namespace Avalonia
/// <typeparamref name="TTarget"/>.
/// </summary>
/// <typeparam name="TTarget">The type of the property change sender.</typeparam>
/// /// <typeparam name="TValue">The type of the property..</typeparam>
/// <typeparam name="TValue">The type of the property.</typeparam>
/// <param name="observable">The property changed observable.</param>
/// <param name="action">
/// The method to call. The parameters are the sender and the event args.

41
src/Avalonia.Base/Media/EllipseGeometry.cs

@ -56,6 +56,10 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets a rect that defines the bounds of the ellipse.
/// </summary>
/// <remarks>
/// When set, this takes priority over the other properties that define an
/// ellipse using a center point and X/Y-axis radii.
/// </remarks>
public Rect Rect
{
get => GetValue(RectProperty);
@ -65,6 +69,10 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets a double that defines the radius in the X-axis of the ellipse.
/// </summary>
/// <remarks>
/// In order for this property to be used, <see cref="Rect"/> must not be set
/// (equal to the default <see cref="Avalonia.Rect"/> value).
/// </remarks>
public double RadiusX
{
get => GetValue(RadiusXProperty);
@ -74,6 +82,10 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets a double that defines the radius in the Y-axis of the ellipse.
/// </summary>
/// <remarks>
/// In order for this property to be used, <see cref="Rect"/> must not be set
/// (equal to the default <see cref="Avalonia.Rect"/> value).
/// </remarks>
public double RadiusY
{
get => GetValue(RadiusYProperty);
@ -83,6 +95,10 @@ namespace Avalonia.Media
/// <summary>
/// Gets or sets a point that defines the center of the ellipse.
/// </summary>
/// <remarks>
/// In order for this property to be used, <see cref="Rect"/> must not be set
/// (equal to the default <see cref="Avalonia.Rect"/> value).
/// </remarks>
public Point Center
{
get => GetValue(CenterProperty);
@ -92,7 +108,30 @@ namespace Avalonia.Media
/// <inheritdoc/>
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,
};
}
/// <inheritdoc/>

2
src/Avalonia.Base/Media/TextFormatting/Unicode/BiDiAlgorithm.cs

@ -687,7 +687,7 @@ namespace Avalonia.Media.TextFormatting.Unicode
/// <remarks>
/// This method resolves the sos and eos values for the run
/// and adds the run to the list
/// /// </remarks>
/// </remarks>
/// <param name="start">The index of the start of the run (in x9 removed units)</param>
/// <param name="length">The length of the run (in x9 removed units)</param>
/// <param name="level">The level of the run</param>

2
src/Avalonia.Base/Platform/IPlatformRenderInterface.cs

@ -18,7 +18,7 @@ namespace Avalonia.Platform
/// Creates an ellipse geometry implementation.
/// </summary>
/// <param name="rect">The bounds of the ellipse.</param>
/// <returns>An ellipse geometry..</returns>
/// <returns>An ellipse geometry.</returns>
IGeometryImpl CreateEllipseGeometry(Rect rect);
/// <summary>

9
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)
{

2
src/Avalonia.Base/Utilities/TypeUtilities.cs

@ -306,7 +306,7 @@ namespace Avalonia.Utilities
/// if the value could not be converted.
/// </summary>
/// <param name="value">The value to convert.</param>
/// <param name="type">The type to convert to..</param>
/// <param name="type">The type to convert to.</param>
/// <param name="culture">The culture to use.</param>
/// <returns>A value of <paramref name="type"/>.</returns>
[RequiresUnreferencedCode(TrimmingMessages.TypeConversionRequiresUnreferencedCodeMessage)]

2
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.
/// /// </remarks>
/// </remarks>
FlipX = 4,
/// <summary>

54
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
/// </summary>
public event EventHandler<SelectionChangedEventArgs>? SelectionChanged
{
add => AddHandler(SelectionChangedEvent, value);
add => AddHandler(SelectionChangedEvent, value);
remove => RemoveHandler(SelectionChangedEvent, value);
}
@ -369,7 +367,7 @@ namespace Avalonia.Controls.Primitives
/// </summary>
public bool WrapSelection
{
get => GetValue(WrapSelectionProperty);
get => GetValue(WrapSelectionProperty);
set => SetValue(WrapSelectionProperty, value);
}
@ -382,7 +380,7 @@ namespace Avalonia.Controls.Primitives
/// </remarks>
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);
}
}
/// <inheritdoc />
@ -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;
}
}
/// <inheritdoc/>
@ -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);
}
}

5
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;

2
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.");
}

12
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<TextRun>();

81
src/Avalonia.Controls/TransitioningContentControl.cs

@ -15,8 +15,9 @@ namespace Avalonia.Controls;
public class TransitioningContentControl : ContentControl
{
private CancellationTokenSource? _currentTransition;
private ContentPresenter? _transitionPresenter;
private Optional<object?> _transitionFrom;
private ContentPresenter? _presenter2;
private bool _isFirstFull;
private bool _shouldAnimate;
/// <summary>
/// Defines the <see cref="PageTransition"/> 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<object?>.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<object?>.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<object?>();
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

7
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))

4
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();

9
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)

3
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}" />
<ContentPresenter Name="PART_TransitionContentPresenter"
<ContentPresenter Name="PART_ContentPresenter2"
Background="{TemplateBinding Background}"
BorderBrush="{TemplateBinding BorderBrush}"
BorderThickness="{TemplateBinding BorderThickness}"

2
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".
/// </summary>
[AttributeUsage(AttributeTargets.Method, AllowMultiple = true, Inherited = false)]
public sealed class AvaloniaTestAttribute : TestCaseAttribute, IWrapSetUpTearDown
public sealed class AvaloniaTestAttribute : TestAttribute, IWrapSetUpTearDown
{
public TestCommand Wrap(TestCommand command)
{

12
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<ListBoxItem>()
.Select(x => (string)x.DataContext)

10
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);
}

21
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

33
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()
{

72
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<ContentPresenter>(from);
var toPresenter = Assert.IsType<ContentPresenter>(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<ContentPresenter>(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;

61
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<ListBox>(
styles: new[]
{
new Style(x => x.OfType<ListBoxItem>())
{
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<IDataTemplate?> itemTemplate = default,
IEnumerable<Style>? styles = null)
{
var (target, scroll, itemsControl) = CreateUnrootedTarget<ItemsControl>(items, itemTemplate);
return CreateTarget<ItemsControl>(
items: items,
itemTemplate: itemTemplate,
styles: styles);
}
private static (VirtualizingStackPanel, ScrollViewer, T) CreateTarget<T>(
IEnumerable<object>? items = null,
Optional<IDataTemplate?> itemTemplate = default,
IEnumerable<Style>? styles = null)
where T : ItemsControl, new()
{
var (target, scroll, itemsControl) = CreateUnrootedTarget<T>(items, itemTemplate);
var root = CreateRoot(itemsControl, styles);
root.LayoutManager.ExecuteInitialLayoutPass();
@ -808,7 +852,7 @@ namespace Avalonia.Controls.UnitTests
var itemsControl = new T
{
ItemsSource = items,
Template = new FuncControlTemplate<ItemsControl>((_, ns) => scroll.RegisterInNameScope(ns)),
Template = new FuncControlTemplate<T>((_, ns) => scroll.RegisterInNameScope(ns)),
ItemsPanel = new FuncTemplate<Panel?>(() => target),
ItemTemplate = itemTemplate.GetValueOrDefault(DefaultItemTemplate()),
};
@ -838,12 +882,23 @@ namespace Avalonia.Controls.UnitTests
root?.LayoutManager.ExecuteLayoutPass();
}
private static IControlTemplate ListBoxItemTemplate()
{
return new FuncControlTemplate<ListBoxItem>((x, ns) =>
new ContentPresenter
{
Name = "PART_ContentPresenter",
Width = 100,
Height = 10,
}.RegisterInNameScope(ns));
}
private static IControlTemplate ScrollViewerTemplate()
{
return new FuncControlTemplate<ScrollViewer>((x, ns) =>
new ScrollContentPresenter
{
Name = "PART_ContentPresenter",
Name = "PART_ScrollContentPresenter",
}.RegisterInNameScope(ns));
}

2
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

Loading…
Cancel
Save