diff --git a/Perspex.Controls/PopupRoot.cs b/Perspex.Controls/PopupRoot.cs index c7fff84549..ec2e3d0e57 100644 --- a/Perspex.Controls/PopupRoot.cs +++ b/Perspex.Controls/PopupRoot.cs @@ -41,7 +41,7 @@ namespace Perspex.Controls public void Show() { this.PlatformImpl.Show(); - this.ExecuteLayoutPass(); + this.LayoutManager.ExecuteLayoutPass(); this.IsVisible = true; } } diff --git a/Perspex.Controls/TopLevel.cs b/Perspex.Controls/TopLevel.cs index 8c24e18361..32d0021ca8 100644 --- a/Perspex.Controls/TopLevel.cs +++ b/Perspex.Controls/TopLevel.cs @@ -6,42 +6,74 @@ namespace Perspex.Controls { - using System; - using System.Reactive.Disposables; - using System.Reactive.Linq; using Perspex.Input; using Perspex.Input.Raw; using Perspex.Layout; - using Perspex.Media; using Perspex.Platform; using Perspex.Rendering; using Perspex.Styling; using Perspex.Threading; using Splat; + using System; + using System.Reactive.Disposables; + using System.Reactive.Linq; + /// + /// Base class for top-level windows. + /// + /// + /// This class acts as a base for top level windows such as and + /// . It handles scheduling layout, styling and rendering as well as + /// tracking the window and state. + /// public abstract class TopLevel : ContentControl, ILayoutRoot, IRenderRoot, ICloseable, IFocusScope { + /// + /// Defines the property. + /// public static readonly PerspexProperty ClientSizeProperty = PerspexProperty.Register("ClientSize"); + /// + /// Defines the property. + /// public static readonly PerspexProperty IsActiveProperty = - PerspexProperty.Register("IsActive"); + PerspexProperty.Register("IsActive"); + /// + /// The dispatcher for the window. + /// private Dispatcher dispatcher; + /// + /// The render manager for the window.s + /// private IRenderManager renderManager; + /// + /// The window renderer. + /// private IRenderer renderer; + /// + /// The input manager for the window. + /// private IInputManager inputManager; private bool autoSizing; + /// + /// Statically initializes the class. + /// static TopLevel() { TopLevel.AffectsMeasure(TopLevel.ClientSizeProperty); } + /// + /// Initializes a new instance of the class. + /// + /// The platform-specific window implementation. public TopLevel(ITopLevelImpl impl) { IPlatformRenderInterface renderInterface = Locator.Current.GetService(); @@ -104,51 +136,86 @@ namespace Perspex.Controls this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => this.PlatformImpl.ClientSize = x); } + /// + /// Fired when the window is activated. + /// public event EventHandler Activated; + /// + /// Fired when the window is closed. + /// public event EventHandler Closed; + /// + /// Fired when the window is deactivated. + /// public event EventHandler Deactivated; + /// + /// Gets or sets the client size of the window. + /// public Size ClientSize { get { return this.GetValue(ClientSizeProperty); } - set { this.SetValue(ClientSizeProperty, value); } + private set { this.SetValue(ClientSizeProperty, value); } } + /// + /// Gets a value that indicates whether the window is active. + /// public bool IsActive { get { return this.GetValue(IsActiveProperty); } private set { this.SetValue(IsActiveProperty, value); } } + /// + /// Gets the layout manager for the window. + /// public ILayoutManager LayoutManager { get; private set; } + /// + /// Gets the platform-specific window implementation. + /// public ITopLevelImpl PlatformImpl { get; private set; } + /// + /// Gets the window renderer. + /// IRenderer IRenderRoot.Renderer { get { return this.renderer; } } + /// + /// Gets the window render manager. + /// IRenderManager IRenderRoot.RenderManager { get { return this.renderManager; } } + /// + /// Translates a point from window coordinates into screen coordinates. + /// + /// The point. + /// The point in screen coordinates. Point IRenderRoot.TranslatePointToScreen(Point p) { return this.PlatformImpl.PointToScreen(p); } + /// + /// Activates the window. + /// public void Activate() { this.PlatformImpl.Activate(); @@ -160,26 +227,24 @@ namespace Perspex.Controls return Disposable.Create(() => this.autoSizing = false); } + /// + /// Carries out the arrange pass of the window. + /// + /// The final window size. + /// The parameter unchanged. protected override Size ArrangeOverride(Size finalSize) { - this.PlatformImpl.ClientSize = finalSize; - return base.ArrangeOverride(finalSize); - } - - protected void ExecuteLayoutPass() - { - this.LayoutManager.ExecuteLayoutPass(); - using (this.BeginAutoSizing()) { - this.ClientSize = new Size( - double.IsNaN(this.Width) ? this.DesiredSize.Width : this.ClientSize.Width, - double.IsNaN(this.Height) ? this.DesiredSize.Height : this.ClientSize.Height); + this.PlatformImpl.ClientSize = finalSize; } - this.PlatformImpl.Invalidate(new Rect(this.ClientSize)); + return base.ArrangeOverride(finalSize); } + /// + /// Handles an activated notification from . + /// private void HandleActivated() { if (this.Activated != null) @@ -191,6 +256,9 @@ namespace Perspex.Controls this.IsActive = true; } + /// + /// Handles a closed notification from . + /// private void HandleClosed() { if (this.Closed != null) @@ -199,6 +267,9 @@ namespace Perspex.Controls } } + /// + /// Handles a deactivated notification from . + /// private void HandleDeactivated() { this.IsActive = false; @@ -209,16 +280,25 @@ namespace Perspex.Controls } } + /// + /// Handles input from . + /// private void HandleInput(RawInputEventArgs e) { this.inputManager.Process(e); } + /// + /// Handles a layout request from . + /// private void HandleLayoutNeeded() { - this.dispatcher.InvokeAsync(this.ExecuteLayoutPass, DispatcherPriority.Render); + this.dispatcher.InvokeAsync(LayoutManager.ExecuteLayoutPass, DispatcherPriority.Render); } + /// + /// Handles a render request from . + /// private void HandleRenderNeeded() { this.dispatcher.InvokeAsync( @@ -226,12 +306,18 @@ namespace Perspex.Controls DispatcherPriority.Render); } + /// + /// Handles a paint request from . + /// private void HandlePaint(Rect rect, IPlatformHandle handle) { this.renderer.Render(this, handle); this.renderManager.RenderFinished(); } + /// + /// Handles a resize notification from . + /// private void HandleResized(Size clientSize) { if (!this.autoSizing) diff --git a/Perspex.Controls/Window.cs b/Perspex.Controls/Window.cs index 00605c7f36..07e0b2a74f 100644 --- a/Perspex.Controls/Window.cs +++ b/Perspex.Controls/Window.cs @@ -68,7 +68,7 @@ namespace Perspex.Controls public void Show() { - this.ExecuteLayoutPass(); + this.LayoutManager.ExecuteLayoutPass(); using (this.BeginAutoSizing()) { @@ -83,7 +83,7 @@ namespace Perspex.Controls public Task ShowDialog() { - this.ExecuteLayoutPass(); + this.LayoutManager.ExecuteLayoutPass(); using (this.BeginAutoSizing()) { diff --git a/Perspex.Input/Raw/RawKeyEventArgs.cs b/Perspex.Input/Raw/RawKeyEventArgs.cs index 9cf852cc92..7050375d6a 100644 --- a/Perspex.Input/Raw/RawKeyEventArgs.cs +++ b/Perspex.Input/Raw/RawKeyEventArgs.cs @@ -15,7 +15,7 @@ namespace Perspex.Input.Raw public class RawKeyEventArgs : RawInputEventArgs { public RawKeyEventArgs( - KeyboardDevice device, + IKeyboardDevice device, uint timestamp, RawKeyEventType type, Key key, diff --git a/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj b/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj index 9f33679406..27a2f7e59f 100644 --- a/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj +++ b/Tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj @@ -100,6 +100,7 @@ + diff --git a/Tests/Perspex.Controls.UnitTests/TopLevelTests.cs b/Tests/Perspex.Controls.UnitTests/TopLevelTests.cs new file mode 100644 index 0000000000..a360f415a0 --- /dev/null +++ b/Tests/Perspex.Controls.UnitTests/TopLevelTests.cs @@ -0,0 +1,319 @@ +// ----------------------------------------------------------------------- +// +// Copyright 2015 MIT Licence. See licence.md for more information. +// +// ----------------------------------------------------------------------- + +namespace Perspex.Controls.UnitTests +{ + using Moq; + using Perspex.Controls.Presenters; + using Perspex.Input; + using Perspex.Input.Raw; + using Perspex.Layout; + using Perspex.Platform; + using Perspex.Rendering; + using Perspex.Styling; + using Ploeh.AutoFixture; + using Ploeh.AutoFixture.AutoMoq; + using Splat; + using Xunit; + + public class TopLevelTests + { + [Fact] + public void ClientSize_Should_Be_Set_On_Construction() + { + using (Locator.CurrentMutable.WithResolver()) + { + this.RegisterServices(); + + var impl = new Mock(); + impl.Setup(x => x.ClientSize).Returns(new Size(123, 456)); + + var target = new TestTopLevel(impl.Object); + + Assert.Equal(new Size(123, 456), target.ClientSize); + } + } + + [Fact] + public void Width_Should_Not_Be_Set_On_Construction() + { + using (Locator.CurrentMutable.WithResolver()) + { + this.RegisterServices(); + + var impl = new Mock(); + impl.Setup(x => x.ClientSize).Returns(new Size(123, 456)); + + var target = new TestTopLevel(impl.Object); + + Assert.Equal(double.NaN, target.Width); + } + } + + [Fact] + public void Height_Should_Not_Be_Set_On_Construction() + { + using (Locator.CurrentMutable.WithResolver()) + { + this.RegisterServices(); + + var impl = new Mock(); + impl.Setup(x => x.ClientSize).Returns(new Size(123, 456)); + + var target = new TestTopLevel(impl.Object); + + Assert.Equal(double.NaN, target.Height); + } + } + + [Fact] + public void Layout_Pass_Should_Not_Be_Automatically_Scheduled() + { + using (Locator.CurrentMutable.WithResolver()) + { + this.RegisterServices(); + + var impl = new Mock(); + var target = new TestTopLevel(impl.Object); + + // The layout pass should be scheduled by the derived class. + var layoutManagerMock = Mock.Get(target.LayoutManager); + layoutManagerMock.Verify(x => x.ExecuteLayoutPass(), Times.Never); + } + } + + [Fact] + public void Bounds_Should_Be_Set_After_Layout_Pass() + { + using (Locator.CurrentMutable.WithResolver()) + { + this.RegisterServices(); + Locator.CurrentMutable.RegisterConstant(new LayoutManager(), typeof(ILayoutManager)); + + var impl = new Mock(); + + var target = new TestTopLevel(impl.Object) + { + Template = ControlTemplate.Create(x => + new ContentPresenter + { + [~ContentPresenter.ContentProperty] = x[~TestTopLevel.ContentProperty], + }), + Content = new TextBlock + { + Width = 321, + Height = 432, + } + }; + + target.LayoutManager.ExecuteLayoutPass(); + + Assert.Equal(new Rect(0, 0, 321, 432), target.Bounds); + } + } + + [Fact] + public void Impl_ClientSize_Should_Be_Set_After_Layout_Pass() + { + using (Locator.CurrentMutable.WithResolver()) + { + this.RegisterServices(); + Locator.CurrentMutable.RegisterConstant(new LayoutManager(), typeof(ILayoutManager)); + + var impl = new Mock(); + + var target = new TestTopLevel(impl.Object) + { + Template = ControlTemplate.Create(x => + new ContentPresenter + { + [~ContentPresenter.ContentProperty] = x[~TestTopLevel.ContentProperty], + }), + Content = new TextBlock + { + Width = 321, + Height = 432, + } + }; + + target.LayoutManager.ExecuteLayoutPass(); + + impl.VerifySet(x => x.ClientSize = new Size(321, 432)); + } + } + + [Fact] + public void Width_And_Height_Should_Not_Be_Set_After_Layout_Pass() + { + using (Locator.CurrentMutable.WithResolver()) + { + this.RegisterServices(); + + var impl = new Mock(); + impl.Setup(x => x.ClientSize).Returns(new Size(123, 456)); + + var target = new TestTopLevel(impl.Object); + target.LayoutManager.ExecuteLayoutPass(); + + Assert.Equal(double.NaN, target.Width); + Assert.Equal(double.NaN, target.Height); + } + } + + [Fact] + public void Width_And_Height_Should_Be_Set_After_Window_Resize_Notification() + { + using (Locator.CurrentMutable.WithResolver()) + { + this.RegisterServices(); + + var impl = new Mock(); + impl.SetupAllProperties(); + impl.Setup(x => x.ClientSize).Returns(new Size(123, 456)); + + // The user has resized the window, so we can no longer auto-size. + var target = new TestTopLevel(impl.Object); + impl.Object.Resized(new Size(100, 200)); + + Assert.Equal(100, target.Width); + Assert.Equal(200, target.Height); + } + } + + [Fact] + public void Activate_Should_Call_Impl_Activate() + { + using (Locator.CurrentMutable.WithResolver()) + { + this.RegisterServices(); + + var impl = new Mock(); + var target = new TestTopLevel(impl.Object); + + target.Activate(); + + impl.Verify(x => x.Activate()); + } + } + + [Fact] + public void Impl_Activate_Should_Call_Raise_Activated_Event() + { + using (Locator.CurrentMutable.WithResolver()) + { + this.RegisterServices(); + + var impl = new Mock(); + impl.SetupAllProperties(); + + bool raised = false; + var target = new TestTopLevel(impl.Object); + target.Activated += (s, e) => raised = true; + + impl.Object.Activated(); + + Assert.True(raised); + } + } + + [Fact] + public void Impl_Close_Should_Call_Raise_Closed_Event() + { + using (Locator.CurrentMutable.WithResolver()) + { + this.RegisterServices(); + + var impl = new Mock(); + impl.SetupAllProperties(); + + bool raised = false; + var target = new TestTopLevel(impl.Object); + target.Closed += (s, e) => raised = true; + + impl.Object.Closed(); + + Assert.True(raised); + } + } + + [Fact] + public void Impl_Deactivate_Should_Call_Raise_Activated_Event() + { + using (Locator.CurrentMutable.WithResolver()) + { + this.RegisterServices(); + + var impl = new Mock(); + impl.SetupAllProperties(); + + bool raised = false; + var target = new TestTopLevel(impl.Object); + target.Deactivated += (s, e) => raised = true; + + impl.Object.Deactivated(); + + Assert.True(raised); + } + } + + [Fact] + public void Impl_Input_Should_Pass_Input_To_InputManager() + { + using (Locator.CurrentMutable.WithResolver()) + { + this.RegisterServices(); + + var impl = new Mock(); + impl.SetupAllProperties(); + var target = new TestTopLevel(impl.Object); + + var input = new RawKeyEventArgs( + new Mock().Object, + 0, + RawKeyEventType.KeyDown, + Key.A, + "A"); + impl.Object.Input(input); + + var inputManagerMock = Mock.Get(InputManager.Instance); + inputManagerMock.Verify(x => x.Process(input)); + } + } + + private void RegisterServices() + { + var fixture = new Fixture().Customize(new AutoMoqCustomization()); + var l = Locator.CurrentMutable; + + var formattedText = fixture.Create(); + var globalStyles = new Mock(); + var layoutManager = fixture.Create(); + var renderInterface = fixture.Create(); + var renderManager = fixture.Create(); + var windowImpl = new Mock(); + var theme = new Styles(); + + globalStyles.Setup(x => x.Styles).Returns(theme); + + l.RegisterConstant(new Mock().Object, typeof(IInputManager)); + l.RegisterConstant(new Mock().Object, typeof(IFocusManager)); + l.RegisterConstant(globalStyles.Object, typeof(IGlobalStyles)); + l.RegisterConstant(layoutManager, typeof(ILayoutManager)); + l.RegisterConstant(new Mock().Object, typeof(IPlatformThreadingInterface)); + l.RegisterConstant(renderInterface, typeof(IPlatformRenderInterface)); + l.RegisterConstant(renderManager, typeof(IRenderManager)); + l.RegisterConstant(new Styler(), typeof(IStyler)); + } + + private class TestTopLevel : TopLevel + { + public TestTopLevel(ITopLevelImpl impl) + : base(impl) + { + } + } + } +} diff --git a/Windows/Perspex.Win32/WindowImpl.cs b/Windows/Perspex.Win32/WindowImpl.cs index 25ffd322b4..f7101a2e81 100644 --- a/Windows/Perspex.Win32/WindowImpl.cs +++ b/Windows/Perspex.Win32/WindowImpl.cs @@ -6,6 +6,11 @@ namespace Perspex.Win32 { + using Perspex.Controls; + using Perspex.Input.Raw; + using Perspex.Platform; + using Perspex.Win32.Input; + using Perspex.Win32.Interop; using System; using System.Collections.Generic; using System.ComponentModel; @@ -14,12 +19,6 @@ namespace Perspex.Win32 using System.Reactive.Disposables; using System.Reactive.Linq; using System.Runtime.InteropServices; - using System.Threading.Tasks; - using Perspex.Controls; - using Perspex.Input.Raw; - using Perspex.Platform; - using Perspex.Win32.Input; - using Perspex.Win32.Interop; public class WindowImpl : IWindowImpl {