diff --git a/azure-pipelines.yml b/azure-pipelines.yml index b70e0bf77f..54645e461e 100644 --- a/azure-pipelines.yml +++ b/azure-pipelines.yml @@ -68,7 +68,7 @@ jobs: inputs: script: | brew update - brew install castxml + brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb - task: CmdLine@2 displayName: 'Install Nuke' diff --git a/packages/Avalonia/Avalonia.csproj b/packages/Avalonia/Avalonia.csproj index 9f7593b702..2c5a09bee7 100644 --- a/packages/Avalonia/Avalonia.csproj +++ b/packages/Avalonia/Avalonia.csproj @@ -1,6 +1,6 @@  - netstandard2.0;net461;netcoreapp3.1 + netstandard2.0;net461;netcoreapp2.0 Avalonia @@ -20,8 +20,8 @@ Platform=$(Platform)" /> - <_PackageFiles Include="$(DesignerHostAppPath)/Avalonia.Designer.HostApp/bin/$(Configuration)/netcoreapp3.1/Avalonia.Designer.HostApp.dll"> - tools/netcoreapp3.1/designer + <_PackageFiles Include="$(DesignerHostAppPath)/Avalonia.Designer.HostApp/bin/$(Configuration)/netcoreapp2.0/Avalonia.Designer.HostApp.dll"> + tools/netcoreapp2.0/designer false None diff --git a/packages/Avalonia/Avalonia.props b/packages/Avalonia/Avalonia.props index 99d53c1e03..6f21971d3d 100644 --- a/packages/Avalonia/Avalonia.props +++ b/packages/Avalonia/Avalonia.props @@ -1,6 +1,6 @@ - $(MSBuildThisFileDirectory)\..\tools\netcoreapp3.1\designer\Avalonia.Designer.HostApp.dll + $(MSBuildThisFileDirectory)\..\tools\netcoreapp2.0\designer\Avalonia.Designer.HostApp.dll $(MSBuildThisFileDirectory)\..\tools\net461\designer\Avalonia.Designer.HostApp.exe $(MSBuildThisFileDirectory)\..\tools\netstandard2.0\Avalonia.Build.Tasks.dll false diff --git a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs b/samples/ControlCatalog/Pages/DialogsPage.xaml.cs index ba68b113e3..ba1921a185 100644 --- a/samples/ControlCatalog/Pages/DialogsPage.xaml.cs +++ b/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 diff --git a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs index bbd5515da0..62a1dd5d84 100644 --- a/src/Avalonia.Input/Raw/RawPointerEventArgs.cs +++ b/src/Avalonia.Input/Raw/RawPointerEventArgs.cs @@ -63,7 +63,7 @@ namespace Avalonia.Input.Raw /// /// Gets the type of the event. /// - public RawPointerEventType Type { get; private set; } + public RawPointerEventType Type { get; set; } /// /// Gets the input modifiers. diff --git a/src/Avalonia.Layout/LayoutHelper.cs b/src/Avalonia.Layout/LayoutHelper.cs index af7d8ee52e..2b61de00a7 100644 --- a/src/Avalonia.Layout/LayoutHelper.cs +++ b/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 /// The control's size. 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); } + + /// + /// Calculates the min and max height for a control. Ported from WPF. + /// + 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; } + } } } diff --git a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs index 485aaf1c26..6771d3e179 100644 --- a/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs +++ b/src/Avalonia.ReactiveUI/AppBuilderExtensions.cs @@ -15,7 +15,7 @@ namespace Avalonia.ReactiveUI public static TAppBuilder UseReactiveUI(this TAppBuilder builder) where TAppBuilder : AppBuilderBase, new() { - return builder.AfterSetup(_ => + return builder.AfterPlatformServicesSetup(_ => { RxApp.MainThreadScheduler = AvaloniaScheduler.Instance; Locator.CurrentMutable.RegisterConstant(new AvaloniaActivationForViewFetcher(), typeof(IActivationForViewFetcher)); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs index b4bf4c799a..b2e827fa26 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs @@ -12,7 +12,7 @@ namespace Avalonia.Rendering.SceneGraph /// public class Scene : IDisposable { - private Dictionary _index; + private readonly Dictionary _index; /// /// Initializes a new instance of the class. @@ -83,7 +83,7 @@ namespace Avalonia.Rendering.SceneGraph /// The cloned scene. public Scene CloneScene() { - var index = new Dictionary(); + var index = new Dictionary(_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 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; diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs index 5960b4f560..25f7383a1a 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/SceneLayers.cs @@ -11,16 +11,28 @@ namespace Avalonia.Rendering.SceneGraph public class SceneLayers : IEnumerable { private readonly IVisual _root; - private readonly List _inner = new List(); - private readonly Dictionary _index = new Dictionary(); + private readonly List _inner; + private readonly Dictionary _index; /// /// Initializes a new instance of the class. /// /// The scene's root visual. - public SceneLayers(IVisual root) + public SceneLayers(IVisual root) : this(root, 0) + { + } + + /// + /// Initializes a new instance of the class. + /// + /// The scene's root visual. + /// Initial layer capacity. + public SceneLayers(IVisual root, int capacity) { _root = root; + + _inner = new List(capacity); + _index = new Dictionary(capacity); } /// @@ -84,7 +96,7 @@ namespace Avalonia.Rendering.SceneGraph /// The cloned layers. public SceneLayers Clone() { - var result = new SceneLayers(_root); + var result = new SceneLayers(_root, Count); foreach (var src in _inner) { diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs index 82444a0c29..8cd1a47795 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs +++ b/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 _children; - private List> _drawOperations; + private PooledList _children; + private PooledList> _drawOperations; private IRef _drawOperationsRefCounter; private bool _drawOperationsCloned; private Matrix transformRestore; @@ -349,6 +350,18 @@ namespace Avalonia.Rendering.SceneGraph context.Transform = transformRestore; } + /// + /// Inserts default constructed children into collection and returns a span for the newly created range. + /// + /// Count of children that will be added. + /// + internal Span 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(); + _children = new PooledList(capacity); } } @@ -377,13 +390,21 @@ namespace Avalonia.Rendering.SceneGraph { if (_drawOperations == null) { - _drawOperations = new List>(); + _drawOperations = new PooledList>(); _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations)); _drawOperationsCloned = false; } else if (_drawOperationsCloned) { - _drawOperations = new List>(_drawOperations.Select(op => op.Clone())); + var oldDrawOperations = _drawOperations; + + _drawOperations = new PooledList>(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 /// /// Draw operations that need to be disposed. /// Disposable for given draw operations. - private static IDisposable CreateDisposeDrawOperations(List> drawOperations) + private static IDisposable CreateDisposeDrawOperations(PooledList> 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; } } diff --git a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs b/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs index 492f5f824f..5c9a0c992a 100644 --- a/src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs +++ b/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; } diff --git a/src/Avalonia.X11/X11ImmediateRendererProxy.cs b/src/Avalonia.X11/X11ImmediateRendererProxy.cs index f7bc7f9711..7280567bb3 100644 --- a/src/Avalonia.X11/X11ImmediateRendererProxy.cs +++ b/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)); + }); } } } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 60fd0346a3..3a919e2bc4 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/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; diff --git a/src/Avalonia.X11/XI2Manager.cs b/src/Avalonia.X11/XI2Manager.cs index ac14efe133..0734532d92 100644 --- a/src/Avalonia.X11/XI2Manager.cs +++ b/src/Avalonia.X11/XI2Manager.cs @@ -196,7 +196,7 @@ namespace Avalonia.X11 (ev.Type == XiEventType.XI_TouchUpdate ? RawPointerEventType.TouchUpdate : RawPointerEventType.TouchEnd); - client.ScheduleInput(new RawTouchEventArgs(client.TouchDevice, + client.ScheduleXI2Input(new RawTouchEventArgs(client.TouchDevice, ev.Timestamp, client.InputRoot, type, ev.Position, ev.Modifiers, ev.Detail)); return; } @@ -230,10 +230,10 @@ namespace Avalonia.X11 } if (scrollDelta != default) - client.ScheduleInput(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp, + client.ScheduleXI2Input(new RawMouseWheelEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, ev.Position, scrollDelta, ev.Modifiers)); if (_pointerDevice.HasMotion(ev)) - client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, + client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, RawPointerEventType.Move, ev.Position, ev.Modifiers)); } @@ -250,7 +250,7 @@ namespace Avalonia.X11 _ => (RawPointerEventType?)null }; if (type.HasValue) - client.ScheduleInput(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, + client.ScheduleXI2Input(new RawPointerEventArgs(client.MouseDevice, ev.Timestamp, client.InputRoot, type.Value, ev.Position, ev.Modifiers)); } @@ -313,7 +313,7 @@ namespace Avalonia.X11 interface IXI2Client { IInputRoot InputRoot { get; } - void ScheduleInput(RawInputEventArgs args); + void ScheduleXI2Input(RawInputEventArgs args); IMouseDevice MouseDevice { get; } TouchDevice TouchDevice { get; } } diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 9612fa3d9b..d0157815a9 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/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]; } diff --git a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj index 372cb8476b..1c7077870a 100644 --- a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj +++ b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj @@ -1,7 +1,7 @@  Exe - net461;netcoreapp3.1 + net461;netcoreapp2.0 diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 5a47a86e51..28e87dd671 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/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); } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs index 501c0455d0..a61d313c8c 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupRootTests.cs +++ b/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(); + 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(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((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); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs index a4c0fa054b..0b9c94f850 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs +++ b/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 diff --git a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs b/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs index 5375a244c9..8da1e26f0d 100644 --- a/tests/Avalonia.Controls.UnitTests/ScrollViewerTests.cs +++ b/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 diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index fed63fc683..cf2920998a 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/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())).Callback(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 MeasureSizes { get; } = new List(); protected override Size MeasureOverride(Size availableSize) { - MeasureSize = availableSize; + MeasureSizes.Add(availableSize); return base.MeasureOverride(availableSize); } } diff --git a/tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs b/tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs index 319e91d2ba..dd704bc775 100644 --- a/tests/Avalonia.DesignerSupport.Tests/DesignerSupportTests.cs +++ b/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) diff --git a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs b/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs index 69eff0b65d..dcc29a9716 100644 --- a/tests/Avalonia.Layout.UnitTests/FullLayoutTests.cs +++ b/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 GetLines() => new FormattedTextLine[0]; - - public TextHitTestResult HitTestPoint(Point point) => new TextHitTestResult(); - - public Rect HitTestTextPosition(int index) => new Rect(); - - public IEnumerable HitTestTextRange(int index, int length) => new Rect[0]; - - public Size Measure() => Constraint; - } - - private void RegisterServices() - { - var globalStyles = new Mock(); - var globalStylesResources = globalStyles.As(); - var outObj = (object)10; - globalStylesResources.Setup(x => x.TryGetResource("FontSizeNormal", out outObj)).Returns(true); - - var renderInterface = new Mock(); - renderInterface.Setup(x => - x.CreateFormattedText( - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny(), - It.IsAny>())) - .Returns(new FormattedTextMock("TEST")); - - var streamGeometry = new Mock(); - streamGeometry.Setup(x => - x.Open()) - .Returns(new Mock().Object); - - renderInterface.Setup(x => - x.CreateStreamGeometry()) - .Returns(streamGeometry.Object); - - var windowImpl = new Mock(); - - Size clientSize = default(Size); - - windowImpl.SetupGet(x => x.ClientSize).Returns(() => clientSize); - windowImpl.Setup(x => x.Resize(It.IsAny())).Callback(s => clientSize = s); - windowImpl.Setup(x => x.MaxClientSize).Returns(new Size(1024, 1024)); - windowImpl.SetupGet(x => x.Scaling).Returns(1); - - AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new CursorFactoryMock()) - .Bind().ToConstant(new AssetLoader()) - .Bind().ToConstant(new Mock().Object) - .Bind().ToConstant(globalStyles.Object) - .Bind().ToConstant(new AppBuilder().RuntimePlatform) - .Bind().ToConstant(renderInterface.Object) - .Bind().ToConstant(new Styler()) - .Bind().ToConstant(new MockFontManagerImpl()) - .Bind().ToConstant(new MockTextShaperImpl()) - .Bind().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); - } } } diff --git a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs b/tests/Avalonia.UnitTests/MockWindowingPlatform.cs index 782e4a0974..b3e4b4edbc 100644 --- a/tests/Avalonia.UnitTests/MockWindowingPlatform.cs +++ b/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 _windowImpl; - private readonly Func _popupImpl; + private readonly Func _popupImpl; - public MockWindowingPlatform(Func windowImpl = null, Func popupImpl = null ) + public MockWindowingPlatform( + Func windowImpl = null, + Func popupImpl = null ) { _windowImpl = windowImpl; _popupImpl = popupImpl; } - public static Mock CreateWindowMock(Func popupImpl = null) + public static Mock CreateWindowMock() { - var win = Mock.Of(x => x.Scaling == 1); - var mock = Mock.Get(win); - mock.Setup(x => x.Show()).Callback(() => + var windowImpl = new Mock(); + 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())).Callback(x => + { + position = x; + windowImpl.Object.PositionChanged?.Invoke(x); + }); + windowImpl.Setup(x => x.Resize(It.IsAny())).Callback(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())).Callback(new Action(np => pos = np)); - SetupToplevel(mock); - return mock; + + return windowImpl; } - static void SetupToplevel(Mock mock) where T : class, ITopLevelImpl + public static Mock CreatePopupMock(IWindowBaseImpl parent) { - mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice()); + var popupImpl = new Mock(); + 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 CreatePopupMock() + public static Mock CreateScreenMock() { - var positioner = Mock.Of(); - var popup = Mock.Of(x => x.Scaling == 1); - var mock = Mock.Get(popup); - mock.SetupGet(x => x.PopupPositioner).Returns(positioner); - SetupToplevel(mock); - - return mock; + var screenImpl = new Mock(); + 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(Mock mock) where T : class, ITopLevelImpl + { + mock.SetupGet(x => x.MouseDevice).Returns(new MouseDevice()); + } } }