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
Steven Kirk 9 years ago
parent
commit
5489487131

+ 1 - 2
src/Perspex.Controls/Border.cs

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

+ 4 - 5
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);
                 }
             }
         }

+ 1 - 4
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>

+ 2 - 1
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;
         }
 

+ 1 - 4
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()

+ 1 - 2
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()

+ 1 - 2
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

+ 1 - 2
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()

+ 8 - 8
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();

+ 9 - 37
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; }
-        }
     }
 }

+ 7 - 3
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())
             {

+ 16 - 35
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);
     }
 }

+ 3 - 3
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; }
     }
 }

+ 26 - 71
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;
             }
         }
     }

+ 25 - 32
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();
-        }
     }
 }

+ 1 - 1
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;
 

+ 25 - 24
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 - 0
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 - 0
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

+ 4 - 25
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()
         {

+ 3 - 3
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);

+ 20 - 13
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);
+            }                
         }
     }
 }

+ 1 - 5
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;
     }
 }

+ 17 - 16
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 - 0
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)

+ 1 - 1
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
         {