Browse Source

Merge branch 'master' into features/updatable-native-app-menu

pull/3762/head
Dan Walmsley 6 years ago
parent
commit
f00fe63010
  1. 2
      azure-pipelines.yml
  2. 6
      packages/Avalonia/Avalonia.csproj
  3. 2
      packages/Avalonia/Avalonia.props
  4. 7
      samples/ControlCatalog/Pages/DialogsPage.xaml.cs
  5. 8
      scripts/ReplaceNugetCache.sh
  6. 7
      src/Avalonia.Controls/AutoCompleteBox.cs
  7. 7
      src/Avalonia.Controls/Calendar/DatePicker.cs
  8. 7
      src/Avalonia.Controls/ComboBox.cs
  9. 57
      src/Avalonia.Controls/Primitives/Popup.cs
  10. 33
      src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs
  11. 39
      src/Avalonia.Controls/Primitives/PopupRoot.cs
  12. 45
      src/Avalonia.Controls/ScrollChangedEventArgs.cs
  13. 49
      src/Avalonia.Controls/ScrollViewer.cs
  14. 69
      src/Avalonia.Controls/Window.cs
  15. 48
      src/Avalonia.Controls/WindowBase.cs
  16. 2
      src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj
  17. 2
      src/Avalonia.Dialogs/ManagedFileChooser.xaml
  18. 2
      src/Avalonia.Input/Raw/RawPointerEventArgs.cs
  19. 50
      src/Avalonia.Layout/LayoutHelper.cs
  20. 2
      src/Avalonia.ReactiveUI/AppBuilderExtensions.cs
  21. 17
      src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
  22. 20
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs
  23. 43
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  24. 8
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
  25. 10
      src/Avalonia.X11/X11ImmediateRendererProxy.cs
  26. 24
      src/Avalonia.X11/X11Window.cs
  27. 10
      src/Avalonia.X11/XI2Manager.cs
  28. 20
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  29. 2
      src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj
  30. 7
      tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs
  31. 155
      tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs
  32. 81
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  33. 73
      tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs
  34. 176
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  35. 2
      tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs
  36. 108
      tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs
  37. 115
      tests/Avalonia.UnitTests/MockWindowingPlatform.cs

2
azure-pipelines.yml

@ -68,7 +68,7 @@ jobs:
inputs:
script: |
brew update
brew install castxml
brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb
- task: CmdLine@2
displayName: 'Install Nuke'

6
packages/Avalonia/Avalonia.csproj

@ -1,6 +1,6 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>netstandard2.0;net461;netcoreapp3.1</TargetFrameworks>
<TargetFrameworks>netstandard2.0;net461;netcoreapp2.0</TargetFrameworks>
<PackageId>Avalonia</PackageId>
</PropertyGroup>
@ -20,8 +20,8 @@
Platform=$(Platform)" />
<ItemGroup>
<_PackageFiles Include="$(DesignerHostAppPath)/Avalonia.Designer.HostApp/bin/$(Configuration)/netcoreapp3.1/Avalonia.Designer.HostApp.dll">
<PackagePath>tools/netcoreapp3.1/designer</PackagePath>
<_PackageFiles Include="$(DesignerHostAppPath)/Avalonia.Designer.HostApp/bin/$(Configuration)/netcoreapp2.0/Avalonia.Designer.HostApp.dll">
<PackagePath>tools/netcoreapp2.0/designer</PackagePath>
<Visible>false</Visible>
<BuildAction>None</BuildAction>
</_PackageFiles>

2
packages/Avalonia/Avalonia.props

@ -1,6 +1,6 @@
<Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
<PropertyGroup>
<AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)\..\tools\netcoreapp3.1\designer\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
<AvaloniaPreviewerNetCoreToolPath>$(MSBuildThisFileDirectory)\..\tools\netcoreapp2.0\designer\Avalonia.Designer.HostApp.dll</AvaloniaPreviewerNetCoreToolPath>
<AvaloniaPreviewerNetFullToolPath>$(MSBuildThisFileDirectory)\..\tools\net461\designer\Avalonia.Designer.HostApp.exe</AvaloniaPreviewerNetFullToolPath>
<AvaloniaBuildTasksLocation>$(MSBuildThisFileDirectory)\..\tools\netstandard2.0\Avalonia.Build.Tasks.dll</AvaloniaBuildTasksLocation>
<AvaloniaUseExternalMSBuild>false</AvaloniaUseExternalMSBuild>

7
samples/ControlCatalog/Pages/DialogsPage.xaml.cs

@ -1,4 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
#pragma warning disable 4014
@ -34,7 +37,9 @@ namespace ControlCatalog.Pages
new OpenFileDialog()
{
Title = "Open file",
Filters = GetFilters()
Filters = GetFilters(),
// Almost guaranteed to exist
InitialFileName = Assembly.GetEntryAssembly()?.GetModules().FirstOrDefault()?.FullyQualifiedName
}.ShowAsync(GetWindow());
};
this.FindControl<Button>("SaveFile").Click += delegate

8
scripts/ReplaceNugetCache.sh

@ -1,8 +1,8 @@
#!/usr/bin/env bash
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netcoreapp2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.skia/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp2.0/Avalonia**.dll ~/.nuget/packages/avalonia.native/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp3.1/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netcoreapp3.1/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp3.1/Avalonia**.dll ~/.nuget/packages/avalonia/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp3.1/Avalonia**.dll ~/.nuget/packages/avalonia.skia/$1/lib/netstandard2.0/
cp ../samples/ControlCatalog.NetCore/bin/Debug/netcoreapp3.1/Avalonia**.dll ~/.nuget/packages/avalonia.native/$1/lib/netstandard2.0/

7
src/Avalonia.Controls/AutoCompleteBox.cs

@ -1630,7 +1630,7 @@ namespace Avalonia.Controls
/// </summary>
/// <param name="sender">The source object.</param>
/// <param name="e">The event data.</param>
private void DropDownPopup_Closed(object sender, EventArgs e)
private void DropDownPopup_Closed(object sender, PopupClosedEventArgs e)
{
// Force the drop down dependency property to be false.
if (IsDropDownOpen)
@ -1638,6 +1638,11 @@ namespace Avalonia.Controls
IsDropDownOpen = false;
}
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
// Fire the DropDownClosed event
if (_popupHasOpened)
{

7
src/Avalonia.Controls/Calendar/DatePicker.cs

@ -895,12 +895,17 @@ namespace Avalonia.Controls
_ignoreButtonClick = false;
}
}
private void PopUp_Closed(object sender, EventArgs e)
private void PopUp_Closed(object sender, PopupClosedEventArgs e)
{
IsDropDownOpen = false;
if(!_isPopupClosing)
{
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
_isPopupClosing = true;
Threading.Dispatcher.UIThread.InvokeAsync(() => _isPopupClosing = false);
}

7
src/Avalonia.Controls/ComboBox.cs

@ -242,11 +242,16 @@ namespace Avalonia.Controls
}
}
private void PopupClosed(object sender, EventArgs e)
private void PopupClosed(object sender, PopupClosedEventArgs e)
{
_subscriptionsOnOpen?.Dispose();
_subscriptionsOnOpen = null;
if (e.CloseEvent is PointerEventArgs pointerEvent)
{
pointerEvent.Handled = true;
}
if (CanFocus(this))
{
Focus();

57
src/Avalonia.Controls/Primitives/Popup.cs

@ -95,7 +95,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Raised when the popup closes.
/// </summary>
public event EventHandler? Closed;
public event EventHandler<PopupClosedEventArgs>? Closed;
/// <summary>
/// Raised when the popup opens.
@ -270,7 +270,7 @@ namespace Avalonia.Controls.Primitives
if (parentPopupRoot?.Parent is Popup popup)
{
DeferCleanup(SubscribeToEventHandler<Popup, EventHandler>(popup, ParentClosed,
DeferCleanup(SubscribeToEventHandler<Popup, EventHandler<PopupClosedEventArgs>>(popup, ParentClosed,
(x, handler) => x.Closed += handler,
(x, handler) => x.Closed -= handler));
}
@ -306,28 +306,7 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Closes the popup.
/// </summary>
public void Close()
{
if (_openState is null)
{
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
return;
}
_openState.Dispose();
_openState = null;
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
Closed?.Invoke(this, EventArgs.Empty);
}
public void Close() => CloseCore(null);
/// <summary>
/// Measures the control.
@ -389,22 +368,44 @@ namespace Avalonia.Controls.Primitives
}
}
private void CloseCore(EventArgs? closeEvent)
{
if (_openState is null)
{
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
return;
}
_openState.Dispose();
_openState = null;
using (BeginIgnoringIsOpen())
{
IsOpen = false;
}
Closed?.Invoke(this, new PopupClosedEventArgs(closeEvent));
}
private void ListenForNonClientClick(RawInputEventArgs e)
{
var mouse = e as RawPointerEventArgs;
if (!StaysOpen && mouse?.Type == RawPointerEventType.NonClientLeftButtonDown)
{
Close();
CloseCore(e);
}
}
private void PointerPressedOutside(object sender, PointerPressedEventArgs e)
{
if (!StaysOpen && !IsChildOrThis((IVisual)e.Source))
if (!StaysOpen && e.Source is IVisual v && !IsChildOrThis(v))
{
Close();
e.Handled = true;
CloseCore(e);
}
}

33
src/Avalonia.Controls/Primitives/PopupClosedEventArgs.cs

@ -0,0 +1,33 @@
using System;
using Avalonia.Interactivity;
#nullable enable
namespace Avalonia.Controls.Primitives
{
/// <summary>
/// Holds data for the <see cref="Popup.Closed"/> event.
/// </summary>
public class PopupClosedEventArgs : EventArgs
{
/// <summary>
/// Initializes a new instance of the <see cref="PopupClosedEventArgs"/> class.
/// </summary>
/// <param name="closeEvent"></param>
public PopupClosedEventArgs(EventArgs? closeEvent)
{
CloseEvent = closeEvent;
}
/// <summary>
/// Gets the event that closed the popup, if any.
/// </summary>
/// <remarks>
/// If <see cref="Popup.StaysOpen"/> is false, then this property will hold details of the
/// interaction that caused the popup to close if the close was caused by e.g. a pointer press
/// outside the popup. It can be used to mark the event as handled if the event should not
/// be propagated.
/// </remarks>
public EventArgs? CloseEvent { get; }
}
}

39
src/Avalonia.Controls/Primitives/PopupRoot.cs

@ -117,20 +117,41 @@ namespace Avalonia.Controls.Primitives
});
}
/// <summary>
/// Carries out the arrange pass of the window.
/// </summary>
/// <param name="finalSize">The final window size.</param>
/// <returns>The <paramref name="finalSize"/> parameter unchanged.</returns>
protected override Size ArrangeOverride(Size finalSize)
protected override Size MeasureOverride(Size availableSize)
{
var measured = base.MeasureOverride(availableSize);
var width = measured.Width;
var height = measured.Height;
var widthCache = Width;
var heightCache = Height;
if (!double.IsNaN(widthCache))
{
width = widthCache;
}
width = Math.Min(width, MaxWidth);
width = Math.Max(width, MinWidth);
if (!double.IsNaN(heightCache))
{
height = heightCache;
}
height = Math.Min(height, MaxHeight);
height = Math.Max(height, MinHeight);
return new Size(width, height);
}
protected override sealed Size ArrangeSetBounds(Size size)
{
using (BeginAutoSizing())
{
_positionerParameters.Size = finalSize;
_positionerParameters.Size = size;
UpdatePosition();
return ClientSize;
}
return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size));
}
}
}

45
src/Avalonia.Controls/ScrollChangedEventArgs.cs

@ -0,0 +1,45 @@
using Avalonia.Interactivity;
namespace Avalonia.Controls
{
/// <summary>
/// Describes a change in scrolling state.
/// </summary>
public class ScrollChangedEventArgs : RoutedEventArgs
{
public ScrollChangedEventArgs(
Vector extentDelta,
Vector offsetDelta,
Vector viewportDelta)
: this(ScrollViewer.ScrollChangedEvent, extentDelta, offsetDelta, viewportDelta)
{
}
public ScrollChangedEventArgs(
RoutedEvent routedEvent,
Vector extentDelta,
Vector offsetDelta,
Vector viewportDelta)
: base(routedEvent)
{
ExtentDelta = extentDelta;
OffsetDelta = offsetDelta;
ViewportDelta = viewportDelta;
}
/// <summary>
/// Gets the change to the value of <see cref="ScrollViewer.Extent"/>.
/// </summary>
public Vector ExtentDelta { get; }
/// <summary>
/// Gets the change to the value of <see cref="ScrollViewer.Offset"/>.
/// </summary>
public Vector OffsetDelta { get; }
/// <summary>
/// Gets the change to the value of <see cref="ScrollViewer.Viewport"/>.
/// </summary>
public Vector ViewportDelta { get; }
}
}

49
src/Avalonia.Controls/ScrollViewer.cs

@ -2,6 +2,7 @@ using System;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Input;
using Avalonia.Interactivity;
namespace Avalonia.Controls
{
@ -165,6 +166,14 @@ namespace Avalonia.Controls
nameof(VerticalScrollBarVisibility),
ScrollBarVisibility.Auto);
/// <summary>
/// Defines the <see cref="ScrollChanged"/> event.
/// </summary>
public static readonly RoutedEvent<ScrollChangedEventArgs> ScrollChangedEvent =
RoutedEvent.Register<ScrollViewer, ScrollChangedEventArgs>(
nameof(ScrollChanged),
RoutingStrategies.Bubble);
internal const double DefaultSmallChange = 16;
private IDisposable _childSubscription;
@ -191,6 +200,15 @@ namespace Avalonia.Controls
{
}
/// <summary>
/// Occurs when changes are detected to the scroll position, extent, or viewport size.
/// </summary>
public event EventHandler<ScrollChangedEventArgs> ScrollChanged
{
add => AddHandler(ScrollChangedEvent, value);
remove => RemoveHandler(ScrollChangedEvent, value);
}
/// <summary>
/// Gets the extent of the scrollable content.
/// </summary>
@ -203,9 +221,11 @@ namespace Avalonia.Controls
private set
{
var old = _extent;
if (SetAndRaise(ExtentProperty, ref _extent, value))
{
CalculatedPropertiesChanged();
CalculatedPropertiesChanged(extentDelta: value - old);
}
}
}
@ -222,11 +242,13 @@ namespace Avalonia.Controls
set
{
var old = _offset;
value = ValidateOffset(this, value);
if (SetAndRaise(OffsetProperty, ref _offset, value))
{
CalculatedPropertiesChanged();
CalculatedPropertiesChanged(offsetDelta: value - old);
}
}
}
@ -243,9 +265,11 @@ namespace Avalonia.Controls
private set
{
var old = _viewport;
if (SetAndRaise(ViewportProperty, ref _viewport, value))
{
CalculatedPropertiesChanged();
CalculatedPropertiesChanged(viewportDelta: value - old);
}
}
}
@ -525,7 +549,10 @@ namespace Avalonia.Controls
}
}
private void CalculatedPropertiesChanged()
private void CalculatedPropertiesChanged(
Size extentDelta = default,
Vector offsetDelta = default,
Size viewportDelta = default)
{
// Pass old values of 0 here because we don't have the old values at this point,
// and it shouldn't matter as only the template uses these properies.
@ -546,6 +573,20 @@ namespace Avalonia.Controls
SetAndRaise(SmallChangeProperty, ref _smallChange, new Size(DefaultSmallChange, DefaultSmallChange));
SetAndRaise(LargeChangeProperty, ref _largeChange, Viewport);
}
if (extentDelta != default || offsetDelta != default || viewportDelta != default)
{
using var route = BuildEventRoute(ScrollChangedEvent);
if (route.HasHandlers)
{
var e = new ScrollChangedEventArgs(
new Vector(extentDelta.Width, extentDelta.Height),
offsetDelta,
new Vector(viewportDelta.Width, viewportDelta.Height));
route.RaiseEvent(this, e);
}
}
}
protected override void OnKeyDown(KeyEventArgs e)

69
src/Avalonia.Controls/Window.cs

@ -313,22 +313,7 @@ namespace Avalonia.Controls
/// Should be called from left mouse button press event handler
/// </summary>
public void BeginResizeDrag(WindowEdge edge, PointerPressedEventArgs e) => PlatformImpl?.BeginResizeDrag(edge, e);
/// <summary>
/// Carries out the arrange pass of the window.
/// </summary>
/// <param name="finalSize">The final window size.</param>
/// <returns>The <paramref name="finalSize"/> parameter unchanged.</returns>
protected override Size ArrangeOverride(Size finalSize)
{
using (BeginAutoSizing())
{
PlatformImpl?.Resize(finalSize);
}
return base.ArrangeOverride(PlatformImpl?.ClientSize ?? default(Size));
}
/// <inheritdoc/>
Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize;
@ -450,6 +435,19 @@ namespace Avalonia.Controls
EnsureInitialized();
IsVisible = true;
var initialSize = new Size(
double.IsNaN(Width) ? ClientSize.Width : Width,
double.IsNaN(Height) ? ClientSize.Height : Height);
if (initialSize != ClientSize)
{
using (BeginAutoSizing())
{
PlatformImpl?.Resize(initialSize);
}
}
LayoutManager.ExecuteInitialLayoutPass(this);
using (BeginAutoSizing())
@ -569,38 +567,60 @@ namespace Avalonia.Controls
}
}
/// <inheritdoc/>
protected override Size MeasureOverride(Size availableSize)
{
var sizeToContent = SizeToContent;
var clientSize = ClientSize;
var constraint = availableSize;
var constraint = clientSize;
if ((sizeToContent & SizeToContent.Width) != 0)
if (sizeToContent.HasFlagCustom(SizeToContent.Width))
{
constraint = constraint.WithWidth(double.PositiveInfinity);
}
if ((sizeToContent & SizeToContent.Height) != 0)
if (sizeToContent.HasFlagCustom(SizeToContent.Height))
{
constraint = constraint.WithHeight(double.PositiveInfinity);
}
var result = base.MeasureOverride(constraint);
if ((sizeToContent & SizeToContent.Width) == 0)
if (!sizeToContent.HasFlagCustom(SizeToContent.Width))
{
result = result.WithWidth(clientSize.Width);
if (!double.IsInfinity(availableSize.Width))
{
result = result.WithWidth(availableSize.Width);
}
else
{
result = result.WithWidth(clientSize.Width);
}
}
if ((sizeToContent & SizeToContent.Height) == 0)
if (!sizeToContent.HasFlagCustom(SizeToContent.Height))
{
result = result.WithHeight(clientSize.Height);
if (!double.IsInfinity(availableSize.Height))
{
result = result.WithHeight(availableSize.Height);
}
else
{
result = result.WithHeight(clientSize.Height);
}
}
return result;
}
protected sealed override Size ArrangeSetBounds(Size size)
{
using (BeginAutoSizing())
{
PlatformImpl?.Resize(size);
return ClientSize;
}
}
protected sealed override void HandleClosed()
{
RaiseEvent(new RoutedEventArgs(WindowClosedEvent));
@ -616,6 +636,9 @@ namespace Avalonia.Controls
SizeToContent = SizeToContent.Manual;
}
Width = clientSize.Width;
Height = clientSize.Height;
base.HandleResized(clientSize);
}

48
src/Avalonia.Controls/WindowBase.cs

@ -224,16 +224,54 @@ namespace Avalonia.Controls
/// <param name="clientSize">The new client size.</param>
protected override void HandleResized(Size clientSize)
{
if (!AutoSizing)
{
Width = clientSize.Width;
Height = clientSize.Height;
}
ClientSize = clientSize;
LayoutManager.ExecuteLayoutPass();
Renderer?.Resized(clientSize);
}
/// <summary>
/// Overrides the core measure logic for windows.
/// </summary>
/// <param name="availableSize">The available size.</param>
/// <returns>The measured size.</returns>
/// <remarks>
/// The layout logic for top-level windows is different than for other controls because
/// they don't have a parent, meaning that many layout properties handled by the default
/// MeasureCore (such as margins and alignment) make no sense.
/// </remarks>
protected override Size MeasureCore(Size availableSize)
{
ApplyStyling();
ApplyTemplate();
var constraint = LayoutHelper.ApplyLayoutConstraints(this, availableSize);
return MeasureOverride(constraint);
}
/// <summary>
/// Overrides the core arrange logic for windows.
/// </summary>
/// <param name="finalRect">The final arrange rect.</param>
/// <remarks>
/// The layout logic for top-level windows is different than for other controls because
/// they don't have a parent, meaning that many layout properties handled by the default
/// ArrangeCore (such as margins and alignment) make no sense.
/// </remarks>
protected override void ArrangeCore(Rect finalRect)
{
var constraint = ArrangeSetBounds(finalRect.Size);
var arrangeSize = ArrangeOverride(constraint);
Bounds = new Rect(arrangeSize);
}
/// <summary>
/// Called durung the arrange pass to set the size of the window.
/// </summary>
/// <param name="size">The requested size of the window.</param>
/// <returns>The actual size of the window.</returns>
protected virtual Size ArrangeSetBounds(Size size) => size;
/// <summary>
/// Handles a window position change notification from
/// <see cref="IWindowBaseImpl.PositionChanged"/>.

2
src/Avalonia.DesktopRuntime/Avalonia.DesktopRuntime.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFrameworks>net461;netcoreapp3.1</TargetFrameworks>
<TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>

2
src/Avalonia.Dialogs/ManagedFileChooser.xaml

@ -58,7 +58,7 @@
<StackPanel Orientation="Horizontal" HorizontalAlignment="Right" Spacing="10">
<StackPanel.Styles>
<Style Selector="Button">
<Setter Property="Margin">4</Setter>
<Setter Property="Margin" Value="4"/>
</Style>
</StackPanel.Styles>
<Button Command="{Binding Ok}">OK</Button>

2
src/Avalonia.Input/Raw/RawPointerEventArgs.cs

@ -63,7 +63,7 @@ namespace Avalonia.Input.Raw
/// <summary>
/// Gets the type of the event.
/// </summary>
public RawPointerEventType Type { get; private set; }
public RawPointerEventType Type { get; set; }
/// <summary>
/// Gets the input modifiers.

50
src/Avalonia.Layout/LayoutHelper.cs

@ -1,4 +1,5 @@
using System;
using Avalonia.Utilities;
using Avalonia.VisualTree;
namespace Avalonia.Layout
@ -19,16 +20,11 @@ namespace Avalonia.Layout
/// <returns>The control's size.</returns>
public static Size ApplyLayoutConstraints(ILayoutable control, Size constraints)
{
var controlWidth = control.Width;
var controlHeight = control.Height;
double width = (controlWidth > 0) ? controlWidth : constraints.Width;
double height = (controlHeight > 0) ? controlHeight : constraints.Height;
width = Math.Min(width, control.MaxWidth);
width = Math.Max(width, control.MinWidth);
height = Math.Min(height, control.MaxHeight);
height = Math.Max(height, control.MinHeight);
return new Size(width, height);
var minmax = new MinMax(control);
return new Size(
MathUtilities.Clamp(constraints.Width, minmax.MinWidth, minmax.MaxWidth),
MathUtilities.Clamp(constraints.Height, minmax.MinHeight, minmax.MaxHeight));
}
public static Size MeasureChild(ILayoutable control, Size availableSize, Thickness padding,
@ -85,5 +81,39 @@ namespace Avalonia.Layout
InnerInvalidateMeasure(control);
}
/// <summary>
/// Calculates the min and max height for a control. Ported from WPF.
/// </summary>
private readonly struct MinMax
{
public MinMax(ILayoutable e)
{
MaxHeight = e.MaxHeight;
MinHeight = e.MinHeight;
double l = e.Height;
double height = (double.IsNaN(l) ? double.PositiveInfinity : l);
MaxHeight = Math.Max(Math.Min(height, MaxHeight), MinHeight);
height = (double.IsNaN(l) ? 0 : l);
MinHeight = Math.Max(Math.Min(MaxHeight, height), MinHeight);
MaxWidth = e.MaxWidth;
MinWidth = e.MinWidth;
l = e.Width;
double width = (double.IsNaN(l) ? double.PositiveInfinity : l);
MaxWidth = Math.Max(Math.Min(width, MaxWidth), MinWidth);
width = (double.IsNaN(l) ? 0 : l);
MinWidth = Math.Max(Math.Min(MaxWidth, width), MinWidth);
}
public double MinWidth { get; }
public double MaxWidth { get; }
public double MinHeight { get; }
public double MaxHeight { get; }
}
}
}

2
src/Avalonia.ReactiveUI/AppBuilderExtensions.cs

@ -15,7 +15,7 @@ namespace Avalonia.ReactiveUI
public static TAppBuilder UseReactiveUI<TAppBuilder>(this TAppBuilder builder)
where TAppBuilder : AppBuilderBase<TAppBuilder>, new()
{
return builder.AfterSetup(_ =>
return builder.AfterPlatformServicesSetup(_ =>
{
RxApp.MainThreadScheduler = AvaloniaScheduler.Instance;
Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher));

17
src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs

@ -12,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
public class Scene : IDisposable
{
private Dictionary<IVisual, IVisualNode> _index;
private readonly Dictionary<IVisual, IVisualNode> _index;
/// <summary>
/// Initializes a new instance of the <see cref="Scene"/> class.
@ -83,7 +83,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <returns>The cloned scene.</returns>
public Scene CloneScene()
{
var index = new Dictionary<IVisual, IVisualNode>();
var index = new Dictionary<IVisual, IVisualNode>(_index.Count);
var root = Clone((VisualNode)Root, null, index);
var result = new Scene(root, index, Layers.Clone(), Generation + 1)
@ -162,9 +162,18 @@ namespace Avalonia.Rendering.SceneGraph
index.Add(result.Visual, result);
foreach (var child in source.Children)
int childCount = source.Children.Count;
if (childCount > 0)
{
result.AddChild(Clone((VisualNode)child, result, index));
Span<IVisualNode> children = result.AddChildrenSpan(childCount);
for (var i = 0; i < childCount; i++)
{
var child = source.Children[i];
children[i] = Clone((VisualNode)child, result, index);
}
}
return result;

20
src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs

@ -11,16 +11,28 @@ namespace Avalonia.Rendering.SceneGraph
public class SceneLayers : IEnumerable<SceneLayer>
{
private readonly IVisual _root;
private readonly List<SceneLayer> _inner = new List<SceneLayer>();
private readonly Dictionary<IVisual, SceneLayer> _index = new Dictionary<IVisual, SceneLayer>();
private readonly List<SceneLayer> _inner;
private readonly Dictionary<IVisual, SceneLayer> _index;
/// <summary>
/// Initializes a new instance of the <see cref="SceneLayers"/> class.
/// </summary>
/// <param name="root">The scene's root visual.</param>
public SceneLayers(IVisual root)
public SceneLayers(IVisual root) : this(root, 0)
{
}
/// <summary>
/// Initializes a new instance of the <see cref="SceneLayers"/> class.
/// </summary>
/// <param name="root">The scene's root visual.</param>
/// <param name="capacity">Initial layer capacity.</param>
public SceneLayers(IVisual root, int capacity)
{
_root = root;
_inner = new List<SceneLayer>(capacity);
_index = new Dictionary<IVisual, SceneLayer>(capacity);
}
/// <summary>
@ -84,7 +96,7 @@ namespace Avalonia.Rendering.SceneGraph
/// <returns>The cloned layers.</returns>
public SceneLayers Clone()
{
var result = new SceneLayers(_root);
var result = new SceneLayers(_root, Count);
foreach (var src in _inner)
{

43
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@ -2,6 +2,7 @@
using System.Collections.Generic;
using System.Linq;
using System.Reactive.Disposables;
using Avalonia.Collections.Pooled;
using Avalonia.Media;
using Avalonia.Platform;
using Avalonia.Utilities;
@ -19,8 +20,8 @@ namespace Avalonia.Rendering.SceneGraph
private Rect? _bounds;
private double _opacity;
private List<IVisualNode> _children;
private List<IRef<IDrawOperation>> _drawOperations;
private PooledList<IVisualNode> _children;
private PooledList<IRef<IDrawOperation>> _drawOperations;
private IRef<IDisposable> _drawOperationsRefCounter;
private bool _drawOperationsCloned;
private Matrix transformRestore;
@ -349,6 +350,18 @@ namespace Avalonia.Rendering.SceneGraph
context.Transform = transformRestore;
}
/// <summary>
/// Inserts default constructed children into collection and returns a span for the newly created range.
/// </summary>
/// <param name="count">Count of children that will be added.</param>
/// <returns></returns>
internal Span<IVisualNode> AddChildrenSpan(int count)
{
EnsureChildrenCreated(count);
return _children.AddSpan(count);
}
private Rect CalculateBounds()
{
var result = new Rect();
@ -362,11 +375,11 @@ namespace Avalonia.Rendering.SceneGraph
return result;
}
private void EnsureChildrenCreated()
private void EnsureChildrenCreated(int capacity = 0)
{
if (_children == null)
{
_children = new List<IVisualNode>();
_children = new PooledList<IVisualNode>(capacity);
}
}
@ -377,13 +390,21 @@ namespace Avalonia.Rendering.SceneGraph
{
if (_drawOperations == null)
{
_drawOperations = new List<IRef<IDrawOperation>>();
_drawOperations = new PooledList<IRef<IDrawOperation>>();
_drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations));
_drawOperationsCloned = false;
}
else if (_drawOperationsCloned)
{
_drawOperations = new List<IRef<IDrawOperation>>(_drawOperations.Select(op => op.Clone()));
var oldDrawOperations = _drawOperations;
_drawOperations = new PooledList<IRef<IDrawOperation>>(oldDrawOperations.Count);
foreach (var drawOperation in oldDrawOperations)
{
_drawOperations.Add(drawOperation.Clone());
}
_drawOperationsRefCounter.Dispose();
_drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations));
_drawOperationsCloned = false;
@ -397,14 +418,16 @@ namespace Avalonia.Rendering.SceneGraph
/// </summary>
/// <param name="drawOperations">Draw operations that need to be disposed.</param>
/// <returns>Disposable for given draw operations.</returns>
private static IDisposable CreateDisposeDrawOperations(List<IRef<IDrawOperation>> drawOperations)
private static IDisposable CreateDisposeDrawOperations(PooledList<IRef<IDrawOperation>> drawOperations)
{
return Disposable.Create(() =>
return Disposable.Create(drawOperations, operations =>
{
foreach (var operation in drawOperations)
foreach (var operation in operations)
{
operation.Dispose();
}
operations.Dispose();
});
}
@ -414,6 +437,8 @@ namespace Avalonia.Rendering.SceneGraph
{
_drawOperationsRefCounter?.Dispose();
_children?.Dispose();
Disposed = true;
}
}

8
src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs

@ -91,7 +91,13 @@ namespace Avalonia.X11.NativeDialogs
gtk_dialog_add_button(dlg, open, GtkResponseType.Cancel);
if (initialFileName != null)
using (var fn = new Utf8Buffer(initialFileName))
gtk_file_chooser_set_current_name(dlg, fn);
{
if (action == GtkFileChooserAction.Save)
gtk_file_chooser_set_current_name(dlg, fn);
else
gtk_file_chooser_set_filename(dlg, fn);
}
gtk_window_present(dlg);
return tcs.Task;
}

10
src/Avalonia.X11/X11ImmediateRendererProxy.cs

@ -11,6 +11,7 @@ namespace Avalonia.X11
private readonly IRenderLoop _loop;
private ImmediateRenderer _renderer;
private bool _invalidated;
private bool _running;
private object _lock = new object();
public X11ImmediateRendererProxy(IVisual root, IRenderLoop loop)
@ -22,6 +23,7 @@ namespace Avalonia.X11
public void Dispose()
{
_running = false;
_renderer.Dispose();
}
@ -78,12 +80,14 @@ namespace Avalonia.X11
public void Start()
{
_running = true;
_loop.Add(this);
_renderer.Start();
}
public void Stop()
{
_running = false;
_loop.Remove(this);
_renderer.Stop();
}
@ -100,7 +104,11 @@ namespace Avalonia.X11
{
lock (_lock)
_invalidated = false;
Dispatcher.UIThread.Post(() => Paint(new Rect(0, 0, 100000, 100000)));
Dispatcher.UIThread.Post(() =>
{
if (_running)
Paint(new Rect(0, 0, 100000, 100000));
});
}
}
}

24
src/Avalonia.X11/X11Window.cs

@ -442,7 +442,7 @@ namespace Avalonia.X11
updatedSizeViaScaling = UpdateScaling();
}
if (changedSize && !updatedSizeViaScaling)
if (changedSize && !updatedSizeViaScaling && !_popup)
Resized?.Invoke(ClientSize);
Dispatcher.UIThread.RunJobs(DispatcherPriority.Layout);
@ -649,7 +649,27 @@ namespace Avalonia.X11
ScheduleInput(args);
}
public void ScheduleInput(RawInputEventArgs args)
public void ScheduleXI2Input(RawInputEventArgs args)
{
if (args is RawPointerEventArgs pargs)
{
if ((pargs.Type == RawPointerEventType.TouchBegin
|| pargs.Type == RawPointerEventType.TouchUpdate
|| pargs.Type == RawPointerEventType.LeftButtonDown
|| pargs.Type == RawPointerEventType.RightButtonDown
|| pargs.Type == RawPointerEventType.MiddleButtonDown
|| pargs.Type == RawPointerEventType.NonClientLeftButtonDown)
&& ActivateTransientChildIfNeeded())
return;
if (pargs.Type == RawPointerEventType.TouchEnd
&& ActivateTransientChildIfNeeded())
pargs.Type = RawPointerEventType.TouchCancel;
}
ScheduleInput(args);
}
private void ScheduleInput(RawInputEventArgs args)
{
if (args is RawPointerEventArgs mouse)
mouse.Position = mouse.Position / Scaling;

10
src/Avalonia.X11/XI2Manager.cs

@ -196,7 +196,7 @@ namespace Avalonia.X11
(ev.Type == XiEventType.XI_TouchUpdate ?
RawPointerEventType.TouchUpdate :
RawPointerEventType.TouchEnd);
client.ScheduleInput(new RawTouchEventArgs(client.TouchDevice,
client.ScheduleXI2Input(new RawTouchEventArgs(client.TouchDevice,
ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail));
return;
}
@ -230,10 +230,10 @@ namespace Avalonia.X11
}
if (scrollDelta != default)
client.ScheduleInput(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp,
client.ScheduleXI2Input(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp,
client.InputRoot, ev.Position, scrollDelta, ev.Modifiers));
if (_pointerDevice.HasMotion(ev))
client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
RawPointerEventType.Move, ev.Position, ev.Modifiers));
}
@ -250,7 +250,7 @@ namespace Avalonia.X11
_ => (RawPointerEventType?)null
};
if (type.HasValue)
client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot,
type.Value, ev.Position, ev.Modifiers));
}
@ -313,7 +313,7 @@ namespace Avalonia.X11
interface IXI2Client
{
IInputRoot InputRoot { get; }
void ScheduleInput(RawInputEventArgs args);
void ScheduleXI2Input(RawInputEventArgs args);
IMouseDevice MouseDevice { get; }
TouchDevice TouchDevice { get; }
}

20
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -140,25 +140,17 @@ namespace Avalonia.Skia
public Rect HitTestTextPosition(int index)
{
if (string.IsNullOrEmpty(Text))
{
var alignmentOffset = TransformX(0, 0, _paint.TextAlign);
return new Rect(alignmentOffset, 0, 0, _lineHeight);
}
var rects = GetRects();
if (index < 0 || index >= rects.Count)
if (index >= Text.Length || index < 0)
{
var r = rects.LastOrDefault();
return new Rect(r.X + r.Width, r.Y, 0, _lineHeight);
}
if (rects.Count == 0)
{
return new Rect(0, 0, 1, _lineHeight);
}
if (index == rects.Count)
{
var lr = rects[rects.Count - 1];
return new Rect(new Point(lr.X + lr.Width, lr.Y), rects[index - 1].Size);
}
return rects[index];
}

2
src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj

@ -1,7 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<OutputType>Exe</OutputType>
<TargetFrameworks>net461;netcoreapp3.1</TargetFrameworks>
<TargetFrameworks>net461;netcoreapp2.0</TargetFrameworks>
</PropertyGroup>
<ItemGroup>
<ProjectReference Include="..\..\Avalonia.DesignerSupport\Avalonia.DesignerSupport.csproj" />

7
tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs

@ -209,16 +209,17 @@ namespace Avalonia.Controls.UnitTests
screenImpl.Setup(x => x.ScreenCount).Returns(1);
screenImpl.Setup(X => X.AllScreens).Returns( new[] { new Screen(1, screen, screen, true) });
popupImpl = MockWindowingPlatform.CreatePopupMock();
var windowImpl = MockWindowingPlatform.CreateWindowMock();
popupImpl = MockWindowingPlatform.CreatePopupMock(windowImpl.Object);
popupImpl.SetupGet(x => x.Scaling).Returns(1);
windowImpl.Setup(x => x.CreatePopup()).Returns(popupImpl.Object);
var windowImpl = MockWindowingPlatform.CreateWindowMock(() => popupImpl.Object);
windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object);
var services = TestServices.StyledWindow.With(
inputManager: new InputManager(),
windowImpl: windowImpl.Object,
windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object, () => popupImpl.Object));
windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object, x => popupImpl.Object));
return UnitTestApplication.Start(services);
}

155
tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs

@ -2,11 +2,14 @@ using System;
using System.Linq;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Primitives.PopupPositioning;
using Avalonia.Controls.Templates;
using Avalonia.LogicalTree;
using Avalonia.Platform;
using Avalonia.Styling;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Moq;
using Xunit;
namespace Avalonia.Controls.UnitTests.Primitives
@ -172,9 +175,146 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
}
private PopupRoot CreateTarget(TopLevel popupParent)
[Fact]
public void Child_Should_Be_Measured_With_Infinity()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new ChildControl();
var window = new Window();
var target = CreateTarget(window);
target.Content = child;
target.Show();
Assert.Equal(Size.Infinity, child.MeasureSize);
}
}
[Fact]
public void Child_Should_Be_Measured_With_Width_Height_When_Set()
{
var result = new PopupRoot(popupParent, popupParent.PlatformImpl.CreatePopup())
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new ChildControl();
var window = new Window();
var target = CreateTarget(window);
target.Width = 500;
target.Height = 600;
target.Content = child;
target.Show();
Assert.Equal(new Size(500, 600), child.MeasureSize);
}
}
[Fact]
public void Child_Should_Be_Measured_With_MaxWidth_MaxHeight_When_Set()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new ChildControl();
var window = new Window();
var target = CreateTarget(window);
target.MaxWidth = 500;
target.MaxHeight = 600;
target.Content = child;
target.Show();
Assert.Equal(new Size(500, 600), child.MeasureSize);
}
}
[Fact]
public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size()
{
// Issue #3784.
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window();
var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl);
var child = new Canvas
{
Width = 400,
Height = 1344,
};
var target = CreateTarget(window, popupImpl.Object);
target.Content = child;
target.Show();
Assert.Equal(new Size(400, 1024), target.Bounds.Size);
// Issue #3784 causes this to be (0, 160) which makes no sense as Window has no
// parent control to be offset against.
Assert.Equal(new Point(0, 0), target.Bounds.Position);
}
}
[Fact]
public void MinWidth_MinHeight_Should_Be_Respected()
{
// Issue #3796
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window();
var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl);
var target = CreateTarget(window, popupImpl.Object);
target.MinWidth = 400;
target.MinHeight = 800;
target.Content = new Border
{
Width = 100,
Height = 100,
};
target.Show();
Assert.Equal(new Rect(0, 0, 400, 800), target.Bounds);
Assert.Equal(new Size(400, 800), target.ClientSize);
Assert.Equal(new Size(400, 800), target.PlatformImpl.ClientSize);
}
}
[Fact]
public void Setting_Width_Should_Resize_WindowImpl()
{
// Issue #3796
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var window = new Window();
var popupImpl = MockWindowingPlatform.CreatePopupMock(window.PlatformImpl);
var positioner = new Mock<IPopupPositioner>();
popupImpl.Setup(x => x.PopupPositioner).Returns(positioner.Object);
var target = CreateTarget(window, popupImpl.Object);
target.Width = 400;
target.Height = 800;
target.Show();
Assert.Equal(400, target.Width);
Assert.Equal(800, target.Height);
target.Width = 410;
target.LayoutManager.ExecuteLayoutPass();
positioner.Verify(x =>
x.Update(It.Is<PopupPositionerParameters>(x => x.Size.Width == 410)));
Assert.Equal(410, target.Width);
}
}
private PopupRoot CreateTarget(TopLevel popupParent, IPopupImpl impl = null)
{
impl ??= popupParent.PlatformImpl.CreatePopup();
var result = new PopupRoot(popupParent, impl)
{
Template = new FuncControlTemplate<PopupRoot>((parent, scope) =>
new ContentPresenter
@ -217,5 +357,16 @@ namespace Avalonia.Controls.UnitTests.Primitives
Popup = (Popup)this.GetVisualChildren().Single();
}
}
private class ChildControl : Control
{
public Size MeasureSize { get; private set; }
protected override Size MeasureOverride(Size availableSize)
{
MeasureSize = availableSize;
return base.MeasureOverride(availableSize);
}
}
}
}

81
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@ -298,13 +298,6 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
}
Window PreparedWindow(object content = null)
{
var w = new Window {Content = content};
w.ApplyTemplate();
return w;
}
[Fact]
public void DataContextBeginUpdate_Should_Not_Be_Called_For_Controls_That_Dont_Inherit()
{
@ -351,18 +344,88 @@ namespace Avalonia.Controls.UnitTests.Primitives
}
}
[Fact]
public void StaysOpen_False_Should_Not_Handle_Closing_Click()
{
using (CreateServices())
{
var window = PreparedWindow();
var target = new Popup()
{
PlacementTarget = window ,
StaysOpen = false,
};
target.Open();
var e = CreatePointerPressedEventArgs(window);
window.RaiseEvent(e);
Assert.False(e.Handled);
}
}
[Fact]
public void Should_Pass_Closing_Click_To_Closed_Event()
{
using (CreateServices())
{
var window = PreparedWindow();
var target = new Popup()
{
PlacementTarget = window,
StaysOpen = false,
};
target.Open();
var press = CreatePointerPressedEventArgs(window);
var raised = 0;
target.Closed += (s, e) =>
{
Assert.Same(press, e.CloseEvent);
++raised;
};
window.RaiseEvent(press);
Assert.Equal(1, raised);
}
}
private IDisposable CreateServices()
{
return UnitTestApplication.Start(TestServices.StyledWindow.With(windowingPlatform:
new MockWindowingPlatform(null,
() =>
x =>
{
if(UsePopupHost)
return null;
return MockWindowingPlatform.CreatePopupMock().Object;
return MockWindowingPlatform.CreatePopupMock(x).Object;
})));
}
private PointerPressedEventArgs CreatePointerPressedEventArgs(Window source)
{
var pointer = new Pointer(Pointer.GetNextFreeId(), PointerType.Mouse, true);
return new PointerPressedEventArgs(
source,
pointer,
source,
default,
0,
new PointerPointProperties(RawInputModifiers.None, PointerUpdateKind.LeftButtonPressed),
KeyModifiers.None);
}
private Window PreparedWindow(object content = null)
{
var w = new Window { Content = content };
w.ApplyTemplate();
return w;
}
private static IControl PopupContentControlTemplate(PopupContentControl control, INameScope scope)
{
return new Popup

73
tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs

@ -4,7 +4,6 @@ using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Moq;
using Xunit;
@ -147,6 +146,78 @@ namespace Avalonia.Controls.UnitTests
Assert.Equal(new Size(45, 67), target.LargeChange);
}
[Fact]
public void Changing_Extent_Should_Raise_ScrollChanged()
{
var target = new ScrollViewer();
var raised = 0;
target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100));
target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50));
target.Offset = new Vector(10, 10);
target.ScrollChanged += (s, e) =>
{
Assert.Equal(new Vector(11, 12), e.ExtentDelta);
Assert.Equal(default, e.OffsetDelta);
Assert.Equal(default, e.ViewportDelta);
++raised;
};
target.SetValue(ScrollViewer.ExtentProperty, new Size(111, 112));
Assert.Equal(1, raised);
}
[Fact]
public void Changing_Offset_Should_Raise_ScrollChanged()
{
var target = new ScrollViewer();
var raised = 0;
target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100));
target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50));
target.Offset = new Vector(10, 10);
target.ScrollChanged += (s, e) =>
{
Assert.Equal(default, e.ExtentDelta);
Assert.Equal(new Vector(12, 14), e.OffsetDelta);
Assert.Equal(default, e.ViewportDelta);
++raised;
};
target.Offset = new Vector(22, 24);
Assert.Equal(1, raised);
}
[Fact]
public void Changing_Viewport_Should_Raise_ScrollChanged()
{
var target = new ScrollViewer();
var raised = 0;
target.SetValue(ScrollViewer.ExtentProperty, new Size(100, 100));
target.SetValue(ScrollViewer.ViewportProperty, new Size(50, 50));
target.Offset = new Vector(10, 10);
target.ScrollChanged += (s, e) =>
{
Assert.Equal(default, e.ExtentDelta);
Assert.Equal(default, e.OffsetDelta);
Assert.Equal(new Vector(6, 8), e.ViewportDelta);
++raised;
};
target.SetValue(ScrollViewer.ViewportProperty, new Size(56, 58));
Assert.Equal(1, raised);
}
private Control CreateTemplate(ScrollViewer control, INameScope scope)
{
return new Grid

176
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Threading.Tasks;
using Avalonia.Layout;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.UnitTests;
@ -351,7 +352,30 @@ namespace Avalonia.Controls.UnitTests
target.Show();
Assert.Equal(new Size(100, 50), child.MeasureSize);
Assert.Equal(1, child.MeasureSizes.Count);
Assert.Equal(new Size(100, 50), child.MeasureSizes[0]);
}
}
[Fact]
public void Child_Should_Be_Measured_With_ClientSize_If_SizeToContent_Is_Manual_And_No_Width_Height_Specified()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var windowImpl = MockWindowingPlatform.CreateWindowMock();
windowImpl.Setup(x => x.ClientSize).Returns(new Size(550, 450));
var child = new ChildControl();
var target = new Window(windowImpl.Object)
{
SizeToContent = SizeToContent.Manual,
Content = child
};
target.Show();
Assert.Equal(1, child.MeasureSizes.Count);
Assert.Equal(new Size(550, 450), child.MeasureSizes[0]);
}
}
@ -371,7 +395,151 @@ namespace Avalonia.Controls.UnitTests
target.Show();
Assert.Equal(Size.Infinity, child.MeasureSize);
Assert.Equal(1, child.MeasureSizes.Count);
Assert.Equal(Size.Infinity, child.MeasureSizes[0]);
}
}
[Fact]
public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size()
{
// Issue #3784.
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var windowImpl = MockWindowingPlatform.CreateWindowMock();
var clientSize = new Size(200, 200);
var maxClientSize = new Size(480, 480);
windowImpl.Setup(x => x.Resize(It.IsAny<Size>())).Callback<Size>(size =>
{
clientSize = size.Constrain(maxClientSize);
windowImpl.Object.Resized?.Invoke(clientSize);
});
windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
var child = new Canvas
{
Width = 400,
Height = 800,
};
var target = new Window(windowImpl.Object)
{
SizeToContent = SizeToContent.WidthAndHeight,
Content = child
};
target.Show();
Assert.Equal(new Size(400, 480), target.Bounds.Size);
// Issue #3784 causes this to be (0, 160) which makes no sense as Window has no
// parent control to be offset against.
Assert.Equal(new Point(0, 0), target.Bounds.Position);
}
}
[Fact]
public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_WidthAndHeight()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new Canvas
{
Width = 400,
Height = 800,
};
var target = new Window()
{
SizeToContent = SizeToContent.WidthAndHeight,
Content = child
};
target.Show();
Assert.Equal(400, target.Width);
Assert.Equal(800, target.Height);
}
}
[Fact]
public void SizeToContent_Should_Not_Be_Lost_On_Show()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new Canvas
{
Width = 400,
Height = 800,
};
var target = new Window()
{
SizeToContent = SizeToContent.WidthAndHeight,
Content = child
};
target.Show();
Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
}
}
[Fact]
public void Width_Height_Should_Be_Updated_When_SizeToContent_Is_WidthAndHeight()
{
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var child = new Canvas
{
Width = 400,
Height = 800,
};
var target = new Window()
{
SizeToContent = SizeToContent.WidthAndHeight,
Content = child
};
target.Show();
Assert.Equal(400, target.Width);
Assert.Equal(800, target.Height);
child.Width = 410;
target.LayoutManager.ExecuteLayoutPass();
Assert.Equal(410, target.Width);
Assert.Equal(800, target.Height);
Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
}
}
[Fact]
public void Setting_Width_Should_Resize_WindowImpl()
{
// Issue #3796
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
var target = new Window()
{
Width = 400,
Height = 800,
};
target.Show();
Assert.Equal(400, target.Width);
Assert.Equal(800, target.Height);
target.Width = 410;
target.LayoutManager.ExecuteLayoutPass();
var windowImpl = Mock.Get(target.PlatformImpl);
windowImpl.Verify(x => x.Resize(new Size(410, 800)));
Assert.Equal(410, target.Width);
}
}
@ -384,11 +552,11 @@ namespace Avalonia.Controls.UnitTests
private class ChildControl : Control
{
public Size MeasureSize { get; private set; }
public List<Size> MeasureSizes { get; } = new List<Size>();
protected override Size MeasureOverride(Size availableSize)
{
MeasureSize = availableSize;
MeasureSizes.Add(availableSize);
return base.MeasureOverride(availableSize);
}
}

2
tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs

@ -18,7 +18,7 @@ namespace Avalonia.DesignerSupport.Tests
{
public class DesignerSupportTests
{
private const string DesignerAppPath = "../../../../../src/tools/Avalonia.Designer.HostApp/bin/$BUILD/netcoreapp3.1/Avalonia.Designer.HostApp.dll";
private const string DesignerAppPath = "../../../../../src/tools/Avalonia.Designer.HostApp/bin/$BUILD/netcoreapp2.0/Avalonia.Designer.HostApp.dll";
private readonly Xunit.Abstractions.ITestOutputHelper outputHelper;
public DesignerSupportTests(Xunit.Abstractions.ITestOutputHelper outputHelper)

108
tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs

@ -1,25 +1,12 @@
using System.Diagnostics;
using System.IO;
using System.Linq;
using Moq;
using Avalonia.Controls;
using Avalonia.Controls.Presenters;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Templates;
using Avalonia.Diagnostics;
using Avalonia.Input;
using Avalonia.Platform;
using Avalonia.Rendering;
using Avalonia.Shared.PlatformSupport;
using Avalonia.Styling;
using Avalonia.Themes.Default;
using Avalonia.UnitTests;
using Avalonia.VisualTree;
using Xunit;
using Avalonia.Media;
using System;
using System.Collections.Generic;
using Avalonia.Controls.UnitTests;
using Avalonia.UnitTests;
namespace Avalonia.Layout.UnitTests
{
@ -28,10 +15,8 @@ namespace Avalonia.Layout.UnitTests
[Fact]
public void Grandchild_Size_Changed()
{
using (var context = AvaloniaLocator.EnterScope())
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
RegisterServices();
Border border;
TextBlock textBlock;
@ -55,7 +40,6 @@ namespace Avalonia.Layout.UnitTests
};
window.Show();
window.LayoutManager.ExecuteInitialLayoutPass(window);
Assert.Equal(new Size(400, 400), border.Bounds.Size);
textBlock.Width = 200;
@ -68,10 +52,8 @@ namespace Avalonia.Layout.UnitTests
[Fact]
public void Test_ScrollViewer_With_TextBlock()
{
using (var context = AvaloniaLocator.EnterScope())
using (UnitTestApplication.Start(TestServices.StyledWindow))
{
RegisterServices();
ScrollViewer scrollViewer;
TextBlock textBlock;
@ -79,7 +61,6 @@ namespace Avalonia.Layout.UnitTests
{
Width = 800,
Height = 600,
SizeToContent = SizeToContent.WidthAndHeight,
Content = scrollViewer = new ScrollViewer
{
Width = 200,
@ -99,7 +80,6 @@ namespace Avalonia.Layout.UnitTests
window.Resources["ScrollBarThickness"] = 10.0;
window.Show();
window.LayoutManager.ExecuteInitialLayoutPass(window);
Assert.Equal(new Size(800, 600), window.Bounds.Size);
Assert.Equal(new Size(200, 200), scrollViewer.Bounds.Size);
@ -131,87 +111,5 @@ namespace Avalonia.Layout.UnitTests
{
return v.Bounds.Position;
}
class FormattedTextMock : IFormattedTextImpl
{
public FormattedTextMock(string text)
{
Text = text;
}
public Size Constraint { get; set; }
public string Text { get; }
public Rect Bounds => Rect.Empty;
public void Dispose()
{
}
public IEnumerable<FormattedTextLine> GetLines() => new FormattedTextLine[0];
public TextHitTestResult HitTestPoint(Point point) => new TextHitTestResult();
public Rect HitTestTextPosition(int index) => new Rect();
public IEnumerable<Rect> HitTestTextRange(int index, int length) => new Rect[0];
public Size Measure() => Constraint;
}
private void RegisterServices()
{
var globalStyles = new Mock<IGlobalStyles>();
var globalStylesResources = globalStyles.As<IResourceNode>();
var outObj = (object)10;
globalStylesResources.Setup(x => x.TryGetResource("FontSizeNormal", out outObj)).Returns(true);
var renderInterface = new Mock<IPlatformRenderInterface>();
renderInterface.Setup(x =>
x.CreateFormattedText(
It.IsAny<string>(),
It.IsAny<Typeface>(),
It.IsAny<double>(),
It.IsAny<TextAlignment>(),
It.IsAny<TextWrapping>(),
It.IsAny<Size>(),
It.IsAny<IReadOnlyList<FormattedTextStyleSpan>>()))
.Returns(new FormattedTextMock("TEST"));
var streamGeometry = new Mock<IStreamGeometryImpl>();
streamGeometry.Setup(x =>
x.Open())
.Returns(new Mock<IStreamGeometryContextImpl>().Object);
renderInterface.Setup(x =>
x.CreateStreamGeometry())
.Returns(streamGeometry.Object);
var windowImpl = new Mock<IWindowImpl>();
Size clientSize = default(Size);
windowImpl.SetupGet(x => x.ClientSize).Returns(() => clientSize);
windowImpl.Setup(x => x.Resize(It.IsAny<Size>())).Callback<Size>(s => clientSize = s);
windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1024, 1024));
windowImpl.SetupGet(x => x.Scaling).Returns(1);
AvaloniaLocator.CurrentMutable
.Bind<IStandardCursorFactory>().ToConstant(new CursorFactoryMock())
.Bind<IAssetLoader>().ToConstant(new AssetLoader())
.Bind<IInputManager>().ToConstant(new Mock<IInputManager>().Object)
.Bind<IGlobalStyles>().ToConstant(globalStyles.Object)
.Bind<IRuntimePlatform>().ToConstant(new AppBuilder().RuntimePlatform)
.Bind<IPlatformRenderInterface>().ToConstant(renderInterface.Object)
.Bind<IStyler>().ToConstant(new Styler())
.Bind<IFontManagerImpl>().ToConstant(new MockFontManagerImpl())
.Bind<ITextShaperImpl>().ToConstant(new MockTextShaperImpl())
.Bind<IWindowingPlatform>().ToConstant(new Avalonia.Controls.UnitTests.WindowingPlatformMock(() => windowImpl.Object));
var theme = new DefaultTheme();
globalStyles.Setup(x => x.IsStylesInitialized).Returns(true);
globalStyles.Setup(x => x.Styles).Returns(theme);
}
}
}

115
tests/Avalonia.UnitTests/MockWindowingPlatform.cs

@ -8,65 +8,122 @@ namespace Avalonia.UnitTests
{
public class MockWindowingPlatform : IWindowingPlatform
{
private static readonly Size s_screenSize = new Size(1280, 1024);
private readonly Func<IWindowImpl> _windowImpl;
private readonly Func<IPopupImpl> _popupImpl;
private readonly Func<IWindowBaseImpl, IPopupImpl> _popupImpl;
public MockWindowingPlatform(Func<IWindowImpl> windowImpl = null, Func<IPopupImpl> popupImpl = null )
public MockWindowingPlatform(
Func<IWindowImpl> windowImpl = null,
Func<IWindowBaseImpl, IPopupImpl> popupImpl = null )
{
_windowImpl = windowImpl;
_popupImpl = popupImpl;
}
public static Mock<IWindowImpl> CreateWindowMock(Func<IPopupImpl> popupImpl = null)
public static Mock<IWindowImpl> CreateWindowMock()
{
var win = Mock.Of<IWindowImpl>(x => x.Scaling == 1);
var mock = Mock.Get(win);
mock.Setup(x => x.Show()).Callback(() =>
var windowImpl = new Mock<IWindowImpl>();
var position = new PixelPoint();
var clientSize = new Size(800, 600);
windowImpl.SetupAllProperties();
windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
windowImpl.Setup(x => x.Scaling).Returns(1);
windowImpl.Setup(x => x.Screen).Returns(CreateScreenMock().Object);
windowImpl.Setup(x => x.Position).Returns(() => position);
SetupToplevel(windowImpl);
windowImpl.Setup(x => x.CreatePopup()).Returns(() =>
{
mock.Object.Activated?.Invoke();
return CreatePopupMock(windowImpl.Object).Object;
});
mock.Setup(x => x.CreatePopup()).Returns(() =>
windowImpl.Setup(x => x.Dispose()).Callback(() =>
{
if (popupImpl != null)
return popupImpl();
return CreatePopupMock().Object;
windowImpl.Object.Closed?.Invoke();
});
windowImpl.Setup(x => x.Move(It.IsAny<PixelPoint>())).Callback<PixelPoint>(x =>
{
position = x;
windowImpl.Object.PositionChanged?.Invoke(x);
});
windowImpl.Setup(x => x.Resize(It.IsAny<Size>())).Callback<Size>(x =>
{
clientSize = x.Constrain(s_screenSize);
windowImpl.Object.Resized?.Invoke(clientSize);
});
mock.Setup(x => x.Dispose()).Callback(() =>
windowImpl.Setup(x => x.Show()).Callback(() =>
{
mock.Object.Closed?.Invoke();
windowImpl.Object.Activated?.Invoke();
});
PixelPoint pos = default;
mock.SetupGet(x => x.Position).Returns(() => pos);
mock.Setup(x => x.Move(It.IsAny<PixelPoint>())).Callback(new Action<PixelPoint>(np => pos = np));
SetupToplevel(mock);
return mock;
return windowImpl;
}
static void SetupToplevel<T>(Mock<T> mock) where T : class, ITopLevelImpl
public static Mock<IPopupImpl> CreatePopupMock(IWindowBaseImpl parent)
{
mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice());
var popupImpl = new Mock<IPopupImpl>();
var clientSize = new Size();
var positionerHelper = new ManagedPopupPositionerPopupImplHelper(parent, (pos, size, scale) =>
{
clientSize = size.Constrain(s_screenSize);
popupImpl.Object.PositionChanged?.Invoke(pos);
popupImpl.Object.Resized?.Invoke(clientSize);
});
var positioner = new ManagedPopupPositioner(positionerHelper);
popupImpl.SetupAllProperties();
popupImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
popupImpl.Setup(x => x.Scaling).Returns(1);
popupImpl.Setup(x => x.PopupPositioner).Returns(positioner);
SetupToplevel(popupImpl);
return popupImpl;
}
public static Mock<IPopupImpl> CreatePopupMock()
public static Mock<IScreenImpl> CreateScreenMock()
{
var positioner = Mock.Of<IPopupPositioner>();
var popup = Mock.Of<IPopupImpl>(x => x.Scaling == 1);
var mock = Mock.Get(popup);
mock.SetupGet(x => x.PopupPositioner).Returns(positioner);
SetupToplevel(mock);
return mock;
var screenImpl = new Mock<IScreenImpl>();
var bounds = new PixelRect(0, 0, (int)s_screenSize.Width, (int)s_screenSize.Height);
var screen = new Screen(96, bounds, bounds, true);
screenImpl.Setup(x => x.AllScreens).Returns(new[] { screen });
screenImpl.Setup(x => x.ScreenCount).Returns(1);
return screenImpl;
}
public IWindowImpl CreateWindow()
{
return _windowImpl?.Invoke() ?? CreateWindowMock(_popupImpl).Object;
if (_windowImpl is object)
{
return _windowImpl();
}
else
{
var mock = CreateWindowMock();
if (_popupImpl is object)
{
mock.Setup(x => x.CreatePopup()).Returns(() => _popupImpl(mock.Object));
}
return mock.Object;
}
}
public IEmbeddableWindowImpl CreateEmbeddableWindow()
{
throw new NotImplementedException();
}
private static void SetupToplevel<T>(Mock<T> mock) where T : class, ITopLevelImpl
{
mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice());
}
}
}

Loading…
Cancel
Save