Browse Source

Make LayoutManager global.

Also made a few other changes along the way:

- Add MaxClientSize to ILayoutRoot
- Store IRenderRoot in Visual and use that to calculate
IsAttachedToVisualTree
- Make Affects* methods take multiple properties
pull/387/merge
Steven Kirk 11 years ago
parent
commit
5489487131
  1. 3
      src/Perspex.Controls/Border.cs
  2. 9
      src/Perspex.Controls/Button.cs
  3. 5
      src/Perspex.Controls/Canvas.cs
  4. 3
      src/Perspex.Controls/Primitives/PopupRoot.cs
  5. 5
      src/Perspex.Controls/Primitives/Track.cs
  6. 3
      src/Perspex.Controls/Shapes/Ellipse.cs
  7. 3
      src/Perspex.Controls/Shapes/Line.cs
  8. 3
      src/Perspex.Controls/Shapes/Rectangle.cs
  9. 16
      src/Perspex.Controls/Shapes/Shape.cs
  10. 46
      src/Perspex.Controls/TopLevel.cs
  11. 10
      src/Perspex.Controls/Window.cs
  12. 51
      src/Perspex.Layout/ILayoutManager.cs
  13. 6
      src/Perspex.Layout/ILayoutRoot.cs
  14. 97
      src/Perspex.Layout/LayoutManager.cs
  15. 57
      src/Perspex.Layout/Layoutable.cs
  16. 2
      src/Perspex.SceneGraph/Rendering/RenderQueueManager.cs
  17. 49
      src/Perspex.SceneGraph/Visual.cs
  18. 5
      tests/Perspex.Controls.UnitTests/ControlTests.cs
  19. 2
      tests/Perspex.Controls.UnitTests/TestRoot.cs
  20. 29
      tests/Perspex.Controls.UnitTests/TopLevelTests.cs
  21. 6
      tests/Perspex.Layout.UnitTests/FullLayoutTests.cs
  22. 33
      tests/Perspex.Layout.UnitTests/LayoutManagerTests.cs
  23. 6
      tests/Perspex.Layout.UnitTests/TestLayoutRoot.cs
  24. 33
      tests/Perspex.LeakTests/ControlTests.cs
  25. 2
      tests/Perspex.LeakTests/TestApp.cs
  26. 2
      tests/Perspex.Styling.UnitTests/TestRoot.cs

3
src/Perspex.Controls/Border.cs

@ -39,8 +39,7 @@ namespace Perspex.Controls
/// </summary>
static Border()
{
AffectsRender(BackgroundProperty);
AffectsRender(BorderBrushProperty);
AffectsRender(BackgroundProperty, BorderBrushProperty);
}
/// <summary>

9
src/Perspex.Controls/Button.cs

@ -275,18 +275,17 @@ namespace Perspex.Controls
{
var button = e.Sender as Button;
var isDefault = (bool)e.NewValue;
var root = button.GetSelfAndVisualAncestors().OfType<IRenderRoot>().FirstOrDefault();
var inputElement = root as IInputElement;
var inputRoot = button.VisualRoot as IInputElement;
if (inputElement != null)
if (inputRoot != null)
{
if (isDefault)
{
button.ListenForDefault(inputElement);
button.ListenForDefault(inputRoot);
}
else
{
button.StopListeningForDefault(inputElement);
button.StopListeningForDefault(inputRoot);
}
}
}

5
src/Perspex.Controls/Canvas.cs

@ -46,10 +46,7 @@ namespace Perspex.Controls
/// </summary>
static Canvas()
{
AffectsArrange(LeftProperty);
AffectsArrange(TopProperty);
AffectsArrange(RightProperty);
AffectsArrange(BottomProperty);
AffectsArrange(LeftProperty, TopProperty, RightProperty, BottomProperty);
}
/// <summary>

3
src/Perspex.Controls/Primitives/PopupRoot.cs

@ -5,6 +5,7 @@ using System;
using Perspex.Controls.Platform;
using Perspex.Controls.Presenters;
using Perspex.Interactivity;
using Perspex.Layout;
using Perspex.Media;
using Perspex.Platform;
using Perspex.VisualTree;
@ -78,7 +79,7 @@ namespace Perspex.Controls.Primitives
public void Show()
{
PlatformImpl.Show();
LayoutManager?.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteInitialLayoutPass(this);
IsVisible = true;
}

5
src/Perspex.Controls/Primitives/Track.cs

@ -34,10 +34,7 @@ namespace Perspex.Controls.Primitives
static Track()
{
AffectsArrange(MinimumProperty);
AffectsArrange(MaximumProperty);
AffectsArrange(ValueProperty);
AffectsMeasure(OrientationProperty);
AffectsArrange(MinimumProperty, MaximumProperty, ValueProperty, OrientationProperty);
}
public Track()

3
src/Perspex.Controls/Shapes/Ellipse.cs

@ -9,8 +9,7 @@ namespace Perspex.Controls.Shapes
{
static Ellipse()
{
AffectsGeometry(BoundsProperty);
AffectsGeometry(StrokeThicknessProperty);
AffectsGeometry(BoundsProperty, StrokeThicknessProperty);
}
protected override Geometry CreateDefiningGeometry()

3
src/Perspex.Controls/Shapes/Line.cs

@ -16,8 +16,7 @@ namespace Perspex.Controls.Shapes
static Line()
{
StrokeThicknessProperty.OverrideDefaultValue<Line>(1);
AffectsGeometry(StartPointProperty);
AffectsGeometry(EndPointProperty);
AffectsGeometry(StartPointProperty, EndPointProperty);
}
public Point StartPoint

3
src/Perspex.Controls/Shapes/Rectangle.cs

@ -9,8 +9,7 @@ namespace Perspex.Controls.Shapes
{
static Rectangle()
{
AffectsGeometry(BoundsProperty);
AffectsGeometry(StrokeThicknessProperty);
AffectsGeometry(BoundsProperty, StrokeThicknessProperty);
}
protected override Geometry CreateDefiningGeometry()

16
src/Perspex.Controls/Shapes/Shape.cs

@ -31,11 +31,8 @@ namespace Perspex.Controls.Shapes
static Shape()
{
AffectsRender(FillProperty);
AffectsMeasure(StretchProperty);
AffectsRender(StrokeProperty);
AffectsRender(StrokeDashArrayProperty);
AffectsMeasure(StrokeThicknessProperty);
AffectsMeasure(StretchProperty, StrokeThicknessProperty);
AffectsRender(FillProperty, StrokeProperty, StrokeDashArrayProperty);
}
public Geometry DefiningGeometry
@ -121,14 +118,17 @@ namespace Perspex.Controls.Shapes
/// <summary>
/// Marks a property as affecting the shape's geometry.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="properties">The properties.</param>
/// <remarks>
/// After a call to this method in a control's static constructor, any change to the
/// property will cause <see cref="InvalidateGeometry"/> to be called on the element.
/// </remarks>
protected static void AffectsGeometry(PerspexProperty property)
protected static void AffectsGeometry(params PerspexProperty[] properties)
{
property.Changed.Subscribe(AffectsGeometryInvalidate);
foreach (var property in properties)
{
property.Changed.Subscribe(AffectsGeometryInvalidate);
}
}
protected abstract Geometry CreateDefiningGeometry();

46
src/Perspex.Controls/TopLevel.cs

@ -12,7 +12,6 @@ using Perspex.Layout;
using Perspex.Platform;
using Perspex.Rendering;
using Perspex.Styling;
using Perspex.Threading;
namespace Perspex.Controls
{
@ -91,7 +90,6 @@ namespace Perspex.Controls
_accessKeyHandler = TryGetService<IAccessKeyHandler>(dependencyResolver);
_inputManager = TryGetService<IInputManager>(dependencyResolver);
_keyboardNavigationHandler = TryGetService<IKeyboardNavigationHandler>(dependencyResolver);
LayoutManager = TryGetService<ILayoutManager>(dependencyResolver);
_renderQueueManager = TryGetService<IRenderQueueManager>(dependencyResolver);
(TryGetService<ITopLevelRenderer>(dependencyResolver) ?? new DefaultTopLevelRenderer()).Attach(this);
@ -102,19 +100,11 @@ namespace Perspex.Controls
PlatformImpl.Input = HandleInput;
PlatformImpl.Resized = HandleResized;
var clientSize = ClientSize = PlatformImpl.ClientSize;
if (LayoutManager != null)
{
LayoutManager.Root = this;
LayoutManager.LayoutNeeded.Subscribe(_ => HandleLayoutNeeded());
LayoutManager.LayoutCompleted.Subscribe(_ => HandleLayoutCompleted());
}
_keyboardNavigationHandler?.SetOwner(this);
_accessKeyHandler?.SetOwner(this);
styler?.ApplyStyles(this);
ClientSize = PlatformImpl.ClientSize;
this.GetObservable(ClientSizeProperty).Skip(1).Subscribe(x => PlatformImpl.ClientSize = x);
this.GetObservable(PointerOverElementProperty)
.Select(
@ -156,11 +146,12 @@ namespace Perspex.Controls
}
/// <summary>
/// Gets the layout manager for the window.
/// Gets or sets the window position in screen coordinates.
/// </summary>
public ILayoutManager LayoutManager
public Point Position
{
get;
get { return PlatformImpl.Position; }
set { PlatformImpl.Position = value; }
}
/// <summary>
@ -204,6 +195,9 @@ namespace Perspex.Controls
set { SetValue(AccessText.ShowAccessKeyProperty, value); }
}
/// <inheritdoc/>
Size ILayoutRoot.MaxClientSize => Size.Infinity;
IStyleHost IStyleHost.StylingParent
{
get { return PerspexLocator.Current.GetService<IGlobalStyles>(); }
@ -279,7 +273,7 @@ namespace Perspex.Controls
}
ClientSize = clientSize;
LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteLayoutPass();
PlatformImpl.Invalidate(new Rect(clientSize));
}
@ -354,22 +348,6 @@ namespace Perspex.Controls
_inputManager.Process(e);
}
/// <summary>
/// Handles a layout request from <see cref="LayoutManager.LayoutNeeded"/>.
/// </summary>
private void HandleLayoutNeeded()
{
Dispatcher.UIThread.InvokeAsync(LayoutManager.ExecuteLayoutPass, DispatcherPriority.Render);
}
/// <summary>
/// Handles a layout completion request from <see cref="LayoutManager.LayoutCompleted"/>.
/// </summary>
private void HandleLayoutCompleted()
{
_renderQueueManager?.InvalidateRender(this);
}
/// <summary>
/// Starts moving a window with left button being held. Should be called from left mouse button press event handler
/// </summary>
@ -380,11 +358,5 @@ namespace Perspex.Controls
/// Should be called from left mouse button press event handler
/// </summary>
public void BeginResizeDrag(WindowEdge edge) => PlatformImpl.BeginResizeDrag(edge);
public Point Position
{
get { return PlatformImpl.Position; }
set { PlatformImpl.Position = value; }
}
}
}

10
src/Perspex.Controls/Window.cs

@ -6,6 +6,7 @@ using System.Reactive.Linq;
using System.Threading.Tasks;
using Perspex.Controls.Platform;
using Perspex.Input;
using Perspex.Layout;
using Perspex.Media;
using Perspex.Platform;
using Perspex.Styling;
@ -41,7 +42,7 @@ namespace Perspex.Controls
/// <summary>
/// A top-level window.
/// </summary>
public class Window : TopLevel, IStyleable, IFocusScope, INameScope
public class Window : TopLevel, IStyleable, IFocusScope, ILayoutRoot, INameScope
{
/// <summary>
/// Defines the <see cref="SizeToContent"/> property.
@ -132,6 +133,9 @@ namespace Perspex.Controls
set { SetValue(HasSystemDecorationsProperty, value); }
}
/// <inheritdoc/>
Size ILayoutRoot.MaxClientSize => _maxPlatformClientSize;
/// <inheritdoc/>
Type IStyleable.StyleKey => typeof(Window);
@ -174,7 +178,7 @@ namespace Perspex.Controls
/// </summary>
public void Show()
{
LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteInitialLayoutPass(this);
using (BeginAutoSizing())
{
@ -204,7 +208,7 @@ namespace Perspex.Controls
/// </returns>
public Task<TResult> ShowDialog<TResult>()
{
LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteInitialLayoutPass(this);
using (BeginAutoSizing())
{

51
src/Perspex.Layout/ILayoutManager.cs

@ -9,56 +9,37 @@ namespace Perspex.Layout
/// <summary>
/// Manages measuring and arranging of controls.
/// </summary>
/// <remarks>
/// Each layout root element such as a window has its own LayoutManager that is responsible
/// for laying out its child controls. When a layout is required the <see cref="LayoutNeeded"/>
/// observable will fire and the root element should respond by calling
/// <see cref="ExecuteLayoutPass"/> at the earliest opportunity to carry out the layout.
/// </remarks>
public interface ILayoutManager
{
/// <summary>
/// Gets or sets the root element that the manager is attached to.
/// </summary>
/// <remarks>
/// This must be set before the layout manager can be used.
/// </remarks>
ILayoutRoot Root { get; set; }
/// <summary>
/// Gets an observable that is fired when a layout pass is needed.
/// Notifies the layout manager that a control requires a measure.
/// </summary>
IObservable<Unit> LayoutNeeded { get; }
/// <param name="control">The control.</param>
void InvalidateMeasure(ILayoutable control);
/// <summary>
/// Gets an observable that is fired when a layout pass is completed.
/// Notifies the layout manager that a control requires an arrange.
/// </summary>
IObservable<Unit> LayoutCompleted { get; }
/// <param name="control">The control.</param>
void InvalidateArrange(ILayoutable control);
/// <summary>
/// Gets a value indicating whether a layout is queued.
/// Executes a layout pass.
/// </summary>
/// <remarks>
/// Returns true when <see cref="LayoutNeeded"/> has been fired, but
/// <see cref="ExecuteLayoutPass"/> has not yet been called.
/// You should not usually need to call this method explictly, the layout manager will
/// schedule layout passes itself.
/// </remarks>
bool LayoutQueued { get; }
/// <summary>
/// Executes a layout pass.
/// </summary>
void ExecuteLayoutPass();
/// <summary>
/// Notifies the layout manager that a control requires a measure.
/// Executes the initial layout pass on a layout root.
/// </summary>
/// <param name="control">The control.</param>
void InvalidateMeasure(ILayoutable control);
/// <summary>
/// Notifies the layout manager that a control requires an arrange.
/// </summary>
/// <param name="control">The control.</param>
void InvalidateArrange(ILayoutable control);
/// <param name="root">The control to lay out.</param>
/// <remarks>
/// You should not usually need to call this method explictly, the layout root will call
/// it to carry out the initial layout of the control.
/// </remarks>
void ExecuteInitialLayoutPass(ILayoutRoot root);
}
}

6
src/Perspex.Layout/ILayoutRoot.cs

@ -9,13 +9,13 @@ namespace Perspex.Layout
public interface ILayoutRoot : ILayoutable
{
/// <summary>
/// The size available to layout the controls.
/// The size available to lay out the controls.
/// </summary>
Size ClientSize { get; }
/// <summary>
/// The layout manager to use for laying out the tree.
/// The maximum client size available.
/// </summary>
ILayoutManager LayoutManager { get; }
Size MaxClientSize { get; }
}
}

97
src/Perspex.Layout/LayoutManager.cs

@ -3,9 +3,6 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Reactive;
using System.Reactive.Subjects;
using Perspex.Threading;
using Serilog;
using Serilog.Core.Enrichers;
@ -19,10 +16,8 @@ namespace Perspex.Layout
{
private readonly Queue<ILayoutable> _toMeasure = new Queue<ILayoutable>();
private readonly Queue<ILayoutable> _toArrange = new Queue<ILayoutable>();
private readonly Subject<Unit> _layoutNeeded = new Subject<Unit>();
private readonly Subject<Unit> _layoutCompleted = new Subject<Unit>();
private readonly ILogger _log;
private bool _first = true;
private bool _queued;
private bool _running;
/// <summary>
@ -39,44 +34,11 @@ namespace Perspex.Layout
}
/// <summary>
/// Gets or sets the root element that the manager is attached to.
/// Gets the layout manager.
/// </summary>
/// <remarks>
/// This must be set before the layout manager can be used.
/// </remarks>
public ILayoutRoot Root
{
get;
set;
}
/// <summary>
/// Gets an observable that is fired when a layout pass is needed.
/// </summary>
public IObservable<Unit> LayoutNeeded => _layoutNeeded;
/// <summary>
/// Gets an observable that is fired when a layout pass is completed.
/// </summary>
public IObservable<Unit> LayoutCompleted => _layoutCompleted;
public static ILayoutManager Instance => PerspexLocator.Current.GetService<ILayoutManager>();
/// <summary>
/// Gets a value indicating whether a layout is queued.
/// </summary>
/// <remarks>
/// Returns true when <see cref="LayoutNeeded"/> has been fired, but
/// <see cref="ExecuteLayoutPass"/> has not yet been called.
/// </remarks>
public bool LayoutQueued
{
get;
private set;
}
/// <summary>
/// Notifies the layout manager that a control requires a measure.
/// </summary>
/// <param name="control">The control.</param>
/// <inheritdoc/>
public void InvalidateMeasure(ILayoutable control)
{
Contract.Requires<ArgumentNullException>(control != null);
@ -84,36 +46,26 @@ namespace Perspex.Layout
_toMeasure.Enqueue(control);
_toArrange.Enqueue(control);
FireLayoutNeeded();
QueueLayoutPass();
}
/// <summary>
/// Notifies the layout manager that a control requires an arrange.
/// </summary>
/// <param name="control">The control.</param>
/// <inheritdoc/>
public void InvalidateArrange(ILayoutable control)
{
Contract.Requires<ArgumentNullException>(control != null);
Dispatcher.UIThread.VerifyAccess();
_toArrange.Enqueue(control);
FireLayoutNeeded();
QueueLayoutPass();
}
/// <summary>
/// Executes a layout pass.
/// </summary>
/// <inheritdoc/>
public void ExecuteLayoutPass()
{
const int MaxPasses = 3;
Dispatcher.UIThread.VerifyAccess();
if (Root == null)
{
throw new InvalidOperationException("Root must be set before executing layout pass.");
}
if (!_running)
{
_running = true;
@ -128,13 +80,6 @@ namespace Perspex.Layout
try
{
if (_first)
{
Measure(Root);
Arrange(Root);
_first = false;
}
for (var pass = 0; pass < MaxPasses; ++pass)
{
ExecuteMeasurePass();
@ -149,16 +94,26 @@ namespace Perspex.Layout
finally
{
_running = false;
LayoutQueued = false;
}
stopwatch.Stop();
_log.Information("Layout pass finised in {Time}", stopwatch.Elapsed);
_layoutCompleted.OnNext(Unit.Default);
}
}
/// <inheritdoc/>
public void ExecuteInitialLayoutPass(ILayoutRoot root)
{
Measure(root);
Arrange(root);
// Running the initial layout pass may have caused some control to be invalidated
// so run a full layout pass now (this usually due to scrollbars; its not known
// whether they will need to be shown until the layout pass has run and if the
// first guess was incorrect the layout will need to be updated).
ExecuteLayoutPass();
}
private void ExecuteMeasurePass()
{
while (_toMeasure.Count > 0)
@ -183,7 +138,7 @@ namespace Perspex.Layout
if (root != null)
{
root.Measure(Size.Infinity);
root.Measure(root.MaxClientSize);
}
else if (control.PreviousMeasure.HasValue)
{
@ -205,12 +160,12 @@ namespace Perspex.Layout
}
}
private void FireLayoutNeeded()
private void QueueLayoutPass()
{
if (!LayoutQueued)
if (!_queued)
{
_layoutNeeded.OnNext(Unit.Default);
LayoutQueued = true;
Dispatcher.UIThread.InvokeAsync(ExecuteLayoutPass, DispatcherPriority.Render);
_queued = true;
}
}
}

57
src/Perspex.Layout/Layoutable.cs

@ -142,16 +142,17 @@ namespace Perspex.Layout
/// </summary>
static Layoutable()
{
AffectsMeasure(IsVisibleProperty);
AffectsMeasure(WidthProperty);
AffectsMeasure(HeightProperty);
AffectsMeasure(MinWidthProperty);
AffectsMeasure(MaxWidthProperty);
AffectsMeasure(MinHeightProperty);
AffectsMeasure(MaxHeightProperty);
AffectsMeasure(MarginProperty);
AffectsMeasure(HorizontalAlignmentProperty);
AffectsMeasure(VerticalAlignmentProperty);
AffectsMeasure(
IsVisibleProperty,
WidthProperty,
HeightProperty,
MinWidthProperty,
MaxWidthProperty,
MinHeightProperty,
MaxHeightProperty,
MarginProperty,
HorizontalAlignmentProperty,
VerticalAlignmentProperty);
}
/// <summary>
@ -390,9 +391,7 @@ namespace Perspex.Layout
IsMeasureValid = false;
IsArrangeValid = false;
var root = GetLayoutRoot();
root?.LayoutManager?.InvalidateMeasure(this);
LayoutManager.Instance?.InvalidateMeasure(this);
}
}
@ -406,9 +405,7 @@ namespace Perspex.Layout
_layoutLog.Verbose("Arrange measure");
IsArrangeValid = false;
var root = GetLayoutRoot();
root?.LayoutManager?.InvalidateArrange(this);
LayoutManager.Instance?.InvalidateArrange(this);
}
}
@ -424,27 +421,33 @@ namespace Perspex.Layout
/// <summary>
/// Marks a property as affecting the control's measurement.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="properties">The properties.</param>
/// <remarks>
/// After a call to this method in a control's static constructor, any change to the
/// property will cause <see cref="InvalidateMeasure"/> to be called on the element.
/// </remarks>
protected static void AffectsMeasure(PerspexProperty property)
protected static void AffectsMeasure(params PerspexProperty[] properties)
{
property.Changed.Subscribe(AffectsMeasureInvalidate);
foreach (var property in properties)
{
property.Changed.Subscribe(AffectsMeasureInvalidate);
}
}
/// <summary>
/// Marks a property as affecting the control's arrangement.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="properties">The properties.</param>
/// <remarks>
/// After a call to this method in a control's static constructor, any change to the
/// property will cause <see cref="InvalidateArrange"/> to be called on the element.
/// </remarks>
protected static void AffectsArrange(PerspexProperty property)
protected static void AffectsArrange(params PerspexProperty[] properties)
{
property.Changed.Subscribe(AffectsArrangeInvalidate);
foreach (var property in properties)
{
property.Changed.Subscribe(AffectsArrangeInvalidate);
}
}
/// <summary>
@ -669,15 +672,5 @@ namespace Perspex.Layout
{
return new Size(Math.Max(size.Width, 0), Math.Max(size.Height, 0));
}
/// <summary>
/// Gets the layout root.
/// </summary>
private ILayoutRoot GetLayoutRoot()
{
return this.GetSelfAndVisualAncestors()
.OfType<ILayoutRoot>()
.FirstOrDefault();
}
}
}

2
src/Perspex.SceneGraph/Rendering/RenderQueueManager.cs

@ -22,7 +22,7 @@ namespace Perspex.Rendering
public IObservable<Unit> RenderNeeded => _renderNeeded;
/// <summary>
/// Gets a valuue indicating whether a render is queued.
/// Gets a value indicating whether a render is queued.
/// </summary>
public bool RenderQueued => _renderQueued;

49
src/Perspex.SceneGraph/Visual.cs

@ -92,11 +92,6 @@ namespace Perspex
/// </summary>
private IVisual _visualParent;
/// <summary>
/// Whether the element is attached to the visual tree.
/// </summary>
private bool _isAttachedToVisualTree;
/// <summary>
/// The logger for visual-level events.
/// </summary>
@ -107,8 +102,7 @@ namespace Perspex
/// </summary>
static Visual()
{
AffectsRender(IsVisibleProperty);
AffectsRender(OpacityProperty);
AffectsRender(BoundsProperty, IsVisibleProperty, OpacityProperty);
RenderTransformProperty.Changed.Subscribe(RenderTransformChanged);
}
@ -197,7 +191,7 @@ namespace Perspex
throw new InvalidOperationException("Cannot set Name to empty string.");
}
if (_isAttachedToVisualTree)
if (VisualRoot != null)
{
throw new InvalidOperationException("Cannot set Name : control already added to tree.");
}
@ -256,10 +250,19 @@ namespace Perspex
private set;
}
/// <summary>
/// Gets the root of the visual tree, if the control is attached to a visual tree.
/// </summary>
protected IRenderRoot VisualRoot
{
get;
private set;
}
/// <summary>
/// Gets a value indicating whether this scene graph node is attached to a visual root.
/// </summary>
bool IVisual.IsAttachedToVisualTree => _isAttachedToVisualTree;
bool IVisual.IsAttachedToVisualTree => VisualRoot != null;
/// <summary>
/// Gets the scene graph node's child nodes.
@ -276,10 +279,7 @@ namespace Perspex
/// </summary>
public void InvalidateVisual()
{
IRenderRoot root = this.GetSelfAndVisualAncestors()
.OfType<IRenderRoot>()
.FirstOrDefault();
root?.RenderQueueManager?.InvalidateRender(this);
VisualRoot?.RenderQueueManager?.InvalidateRender(this);
}
/// <summary>
@ -329,15 +329,18 @@ namespace Perspex
/// Indicates that a property change should cause <see cref="InvalidateVisual"/> to be
/// called.
/// </summary>
/// <param name="property">The property.</param>
/// <param name="properties">The properties.</param>
/// <remarks>
/// This method should be called in a control's static constructor for each property
/// This method should be called in a control's static constructor with each property
/// on the control which when changed should cause a redraw. This is similar to WPF's
/// FrameworkPropertyMetadata.AffectsRender flag.
/// </remarks>
protected static void AffectsRender(PerspexProperty property)
protected static void AffectsRender(params PerspexProperty[] properties)
{
property.Changed.Subscribe(AffectsRenderInvalidate);
foreach (var property in properties)
{
property.Changed.Subscribe(AffectsRenderInvalidate);
}
}
/// <summary>
@ -440,7 +443,7 @@ namespace Perspex
{
var sender = e.Sender as Visual;
if (sender?._isAttachedToVisualTree == true)
if (sender?.VisualRoot != null)
{
var oldValue = e.OldValue as Transform;
var newValue = e.NewValue as Transform;
@ -496,11 +499,9 @@ namespace Perspex
var old = _visualParent;
_visualParent = value;
if (_isAttachedToVisualTree)
if (VisualRoot != null)
{
var root = (this as IRenderRoot) ??
old.GetSelfAndVisualAncestors().OfType<IRenderRoot>().FirstOrDefault();
var e = new VisualTreeAttachmentEventArgs(root);
var e = new VisualTreeAttachmentEventArgs(VisualRoot);
NotifyDetachedFromVisualTree(e);
}
@ -550,7 +551,7 @@ namespace Perspex
{
_visualLogger.Verbose("Attached to visual tree");
_isAttachedToVisualTree = true;
VisualRoot = e.Root;
OnAttachedToVisualTree(e);
@ -572,7 +573,7 @@ namespace Perspex
{
_visualLogger.Verbose("Detached from visual tree");
_isAttachedToVisualTree = false;
VisualRoot = null;
OnDetachedFromVisualTree(e);
if (VisualChildren != null)

5
tests/Perspex.Controls.UnitTests/ControlTests.cs

@ -154,6 +154,11 @@ namespace Perspex.Controls.UnitTests
get { throw new NotImplementedException(); }
}
public Size MaxClientSize
{
get { throw new NotImplementedException(); }
}
public ILayoutManager LayoutManager
{
get { throw new NotImplementedException(); }

2
tests/Perspex.Controls.UnitTests/TestRoot.cs

@ -14,6 +14,8 @@ namespace Perspex.Controls.UnitTests
{
public Size ClientSize => new Size(100, 100);
public Size MaxClientSize => Size.Infinity;
public ILayoutManager LayoutManager => new Mock<ILayoutManager>().Object;
public IRenderTarget RenderTarget

29
tests/Perspex.Controls.UnitTests/TopLevelTests.cs

@ -80,7 +80,7 @@ namespace Perspex.Controls.UnitTests
var target = new TestTopLevel(impl.Object);
// The layout pass should be scheduled by the derived class.
var layoutManagerMock = Mock.Get(target.LayoutManager);
var layoutManagerMock = Mock.Get(LayoutManager.Instance);
layoutManagerMock.Verify(x => x.ExecuteLayoutPass(), Times.Never);
}
}
@ -107,7 +107,7 @@ namespace Perspex.Controls.UnitTests
}
};
target.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteInitialLayoutPass(target);
Assert.Equal(new Rect(0, 0, 321, 432), target.Bounds);
}
@ -133,7 +133,7 @@ namespace Perspex.Controls.UnitTests
}
};
target.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteInitialLayoutPass(target);
impl.VerifySet(x => x.ClientSize = new Size(321, 432));
}
@ -150,34 +150,13 @@ namespace Perspex.Controls.UnitTests
impl.Setup(x => x.ClientSize).Returns(new Size(123, 456));
var target = new TestTopLevel(impl.Object);
target.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteLayoutPass();
Assert.Equal(double.NaN, target.Width);
Assert.Equal(double.NaN, target.Height);
}
}
[Fact]
public void Render_Should_Be_Scheduled_After_Layout_Pass()
{
using (PerspexLocator.EnterScope())
{
RegisterServices();
var completed = new Subject<Unit>();
var layoutManagerMock = Mock.Get(PerspexLocator.Current.GetService<ILayoutManager>());
layoutManagerMock.Setup(x => x.LayoutCompleted).Returns(completed);
var impl = new Mock<ITopLevelImpl>();
impl.Setup(x => x.ClientSize).Returns(new Size(123, 456));
var target = new TestTopLevel(impl.Object);
completed.OnNext(Unit.Default);
var renderManagerMock = Mock.Get(PerspexLocator.Current.GetService<IRenderQueueManager>());
renderManagerMock.Verify(x => x.InvalidateRender(target));
}
}
[Fact]
public void Width_And_Height_Should_Be_Set_After_Window_Resize_Notification()
{

6
tests/Perspex.Layout.UnitTests/FullLayoutTests.cs

@ -55,11 +55,11 @@ namespace Perspex.Layout.UnitTests
}
};
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteInitialLayoutPass(window);
Assert.Equal(new Size(400, 400), border.Bounds.Size);
textBlock.Width = 200;
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteLayoutPass();
Assert.Equal(new Size(200, 400), border.Bounds.Size);
}
@ -96,7 +96,7 @@ namespace Perspex.Layout.UnitTests
}
};
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteInitialLayoutPass(window);
Assert.Equal(new Size(800, 600), window.Bounds.Size);
Assert.Equal(new Size(200, 200), scrollViewer.Bounds.Size);

33
tests/Perspex.Layout.UnitTests/LayoutManagerTests.cs

@ -11,28 +11,35 @@ namespace Perspex.Layout.UnitTests
[Fact]
public void Invalidating_Child_Should_Remeasure_Parent()
{
Border border;
StackPanel panel;
var layoutManager = new LayoutManager();
var root = new TestLayoutRoot
using (PerspexLocator.EnterScope())
{
Child = panel = new StackPanel
PerspexLocator.CurrentMutable.Bind<ILayoutManager>().ToConstant(layoutManager);
Border border;
StackPanel panel;
var root = new TestLayoutRoot
{
Children = new Controls.Controls
Child = panel = new StackPanel
{
Children = new Controls.Controls
{
(border = new Border())
}
}
};
}
};
root.LayoutManager.ExecuteLayoutPass();
Assert.Equal(new Size(0, 0), root.DesiredSize);
layoutManager.ExecuteInitialLayoutPass(root);
Assert.Equal(new Size(0, 0), root.DesiredSize);
border.Width = 100;
border.Height = 100;
border.Width = 100;
border.Height = 100;
root.LayoutManager.ExecuteLayoutPass();
Assert.Equal(new Size(100, 100), panel.DesiredSize);
layoutManager.ExecuteLayoutPass();
Assert.Equal(new Size(100, 100), panel.DesiredSize);
}
}
}
}

6
tests/Perspex.Layout.UnitTests/TestLayoutRoot.cs

@ -10,7 +10,6 @@ namespace Perspex.Layout.UnitTests
public TestLayoutRoot()
{
ClientSize = new Size(500, 500);
LayoutManager = new LayoutManager { Root = this };
}
public Size ClientSize
@ -19,9 +18,6 @@ namespace Perspex.Layout.UnitTests
set;
}
public ILayoutManager LayoutManager
{
get;
}
public Size MaxClientSize => Size.Infinity;
}
}

33
tests/Perspex.LeakTests/ControlTests.cs

@ -8,6 +8,7 @@ using JetBrains.dotMemoryUnit;
using Perspex.Controls;
using Perspex.Controls.Primitives;
using Perspex.Controls.Templates;
using Perspex.Layout;
using Perspex.VisualTree;
using Xunit;
using Xunit.Abstractions;
@ -34,12 +35,12 @@ namespace Perspex.LeakTests
};
// Do a layout and make sure that Canvas gets added to visual tree.
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteInitialLayoutPass(window);
Assert.IsType<Canvas>(window.Presenter.Child);
// Clear the content and ensure the Canvas is removed.
window.Content = null;
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteLayoutPass();
Assert.Null(window.Presenter.Child);
return window;
@ -65,13 +66,13 @@ namespace Perspex.LeakTests
};
// Do a layout and make sure that Canvas gets added to visual tree.
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteInitialLayoutPass(window);
Assert.IsType<Canvas>(window.Find<Canvas>("foo"));
Assert.IsType<Canvas>(window.Presenter.Child);
// Clear the content and ensure the Canvas is removed.
window.Content = null;
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteLayoutPass();
Assert.Null(window.Presenter.Child);
return window;
@ -95,13 +96,13 @@ namespace Perspex.LeakTests
// Do a layout and make sure that the control gets added to visual tree and its
// template applied.
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteInitialLayoutPass(window);
Assert.IsType<TestTemplatedControl>(window.Presenter.Child);
Assert.IsType<Canvas>(window.Presenter.Child.GetVisualChildren().SingleOrDefault());
// Clear the template and ensure the control template gets removed
((TestTemplatedControl)window.Content).Template = null;
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteLayoutPass();
Assert.Equal(0, window.Presenter.Child.GetVisualChildren().Count());
return window;
@ -128,13 +129,13 @@ namespace Perspex.LeakTests
// Do a layout and make sure that ScrollViewer gets added to visual tree and its
// template applied.
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteInitialLayoutPass(window);
Assert.IsType<ScrollViewer>(window.Presenter.Child);
Assert.IsType<Canvas>(((ScrollViewer)window.Presenter.Child).Presenter.Child);
// Clear the content and ensure the ScrollViewer is removed.
window.Content = null;
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteLayoutPass();
Assert.Null(window.Presenter.Child);
return window;
@ -160,13 +161,13 @@ namespace Perspex.LeakTests
// Do a layout and make sure that TextBox gets added to visual tree and its
// template applied.
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteInitialLayoutPass(window);
Assert.IsType<TextBox>(window.Presenter.Child);
Assert.NotEqual(0, window.Presenter.Child.GetVisualChildren().Count());
// Clear the content and ensure the TextBox is removed.
window.Content = null;
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteLayoutPass();
Assert.Null(window.Presenter.Child);
return window;
@ -199,14 +200,14 @@ namespace Perspex.LeakTests
// Do a layout and make sure that TextBox gets added to visual tree and its
// Text property set.
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteInitialLayoutPass(window);
Assert.IsType<TextBox>(window.Presenter.Child);
Assert.Equal("foo", ((TextBox)window.Presenter.Child).Text);
// Clear the content and DataContext and ensure the TextBox is removed.
window.Content = null;
window.DataContext = null;
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteLayoutPass();
Assert.Null(window.Presenter.Child);
return window;
@ -232,13 +233,13 @@ namespace Perspex.LeakTests
// Do a layout and make sure that TextBox gets added to visual tree and its
// template applied.
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteInitialLayoutPass(window);
Assert.IsType<TextBox>(window.Presenter.Child);
Assert.NotEqual(0, window.Presenter.Child.GetVisualChildren().Count());
// Clear the template and ensure the TextBox template gets removed
((TextBox)window.Content).Template = null;
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteLayoutPass();
Assert.Equal(0, window.Presenter.Child.GetVisualChildren().Count());
return window;
@ -280,12 +281,12 @@ namespace Perspex.LeakTests
};
// Do a layout and make sure that TreeViewItems get realized.
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteInitialLayoutPass(window);
Assert.Equal(1, target.ItemContainerGenerator.Containers.Count());
// Clear the content and ensure the TreeView is removed.
window.Content = null;
window.LayoutManager.ExecuteLayoutPass();
LayoutManager.Instance.ExecuteLayoutPass();
Assert.Null(window.Presenter.Child);
return window;

2
tests/Perspex.LeakTests/TestApp.cs

@ -3,6 +3,7 @@
using Moq;
using Perspex.Controls.UnitTests;
using Perspex.Layout;
using Perspex.Platform;
using Perspex.Shared.PlatformSupport;
using Perspex.Themes.Default;
@ -25,6 +26,7 @@ namespace Perspex.LeakTests
PerspexLocator.CurrentMutable
.Bind<IAssetLoader>().ToConstant(new AssetLoader())
.Bind<ILayoutManager>().ToConstant(new LayoutManager())
.Bind<IPclPlatformWrapper>().ToConstant(new PclPlatformWrapper())
.Bind<IPlatformRenderInterface>().ToConstant(renderInterface)
.Bind<IPlatformThreadingInterface>().ToConstant(threadingInterface)

2
tests/Perspex.Styling.UnitTests/TestRoot.cs

@ -14,7 +14,7 @@ namespace Perspex.Styling.UnitTests
{
public Size ClientSize => new Size(100, 100);
public ILayoutManager LayoutManager => new Mock<ILayoutManager>().Object;
public Size MaxClientSize => Size.Infinity;
public IRenderTarget RenderTarget
{

Loading…
Cancel
Save