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
{