Browse Source

Merge remote-tracking branch 'upstream/master'

Ivan Kochurkin 9 years ago
parent
commit
2072912281
38 changed files with 920 additions and 938 deletions
  1. 9 6
      docs/build.md
  2. 12 0
      samples/ControlCatalog/App.paml.cs
  3. 9 0
      samples/ControlCatalog/ControlCatalog.csproj
  4. 4 0
      samples/ControlCatalog/packages.config
  5. 10 7
      src/Perspex.Base/PriorityValue.cs
  6. 27 0
      src/Perspex.Base/Threading/DispatcherTimer.cs
  7. 122 391
      src/Perspex.Controls/DockPanel.cs
  8. 13 0
      src/Perspex.Controls/Generators/ItemContainerEventArgs.cs
  9. 19 0
      src/Perspex.Controls/Generators/TreeContainerIndex.cs
  10. 1 1
      src/Perspex.Controls/Primitives/HeaderedItemsControl.cs
  11. 56 1
      src/Perspex.Controls/TreeView.cs
  12. 1 21
      src/Perspex.Controls/TreeViewItem.cs
  13. 0 120
      src/Perspex.Diagnostics/DevTools.cs
  14. 18 0
      src/Perspex.Diagnostics/DevTools.paml
  15. 92 0
      src/Perspex.Diagnostics/DevTools.paml.cs
  16. 21 6
      src/Perspex.Diagnostics/Perspex.Diagnostics.csproj
  17. 1 1
      src/Perspex.Diagnostics/ViewLocator.cs
  18. 44 25
      src/Perspex.Diagnostics/ViewModels/DevToolsViewModel.cs
  19. 4 4
      src/Perspex.Diagnostics/ViewModels/LogicalTreeNode.cs
  20. 0 34
      src/Perspex.Diagnostics/ViewModels/LogicalTreeViewModel.cs
  21. 16 4
      src/Perspex.Diagnostics/ViewModels/TreeNode.cs
  22. 102 0
      src/Perspex.Diagnostics/ViewModels/TreePageViewModel.cs
  23. 5 5
      src/Perspex.Diagnostics/ViewModels/VisualTreeNode.cs
  24. 0 34
      src/Perspex.Diagnostics/ViewModels/VisualTreeViewModel.cs
  25. 0 99
      src/Perspex.Diagnostics/Views/LogicalTreeView.cs
  26. 0 43
      src/Perspex.Diagnostics/Views/TreePage.cs
  27. 70 0
      src/Perspex.Diagnostics/Views/TreePage.paml.cs
  28. 24 0
      src/Perspex.Diagnostics/Views/TreePageView.paml
  29. 0 101
      src/Perspex.Diagnostics/Views/VisualTreeView.cs
  30. 40 0
      src/Perspex.SceneGraph/Rect.cs
  31. 2 14
      src/Skia/Perspex.Skia/DrawingContextImpl.cs
  32. 32 3
      src/Skia/Perspex.Skia/MethodTable.cs
  33. 59 0
      src/Skia/Perspex.Skia/PerspexHandleHolder.cs
  34. 36 4
      src/Skia/Perspex.Skia/StreamGeometryImpl.cs
  35. 8 2
      src/Skia/getnatives.sh
  36. 62 0
      tests/Perspex.Controls.UnitTests/DockPanelTests.cs
  37. 1 0
      tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj
  38. 0 12
      tests/Perspex.RenderTests/Shapes/PathTests.cs

+ 9 - 6
docs/build.md

@@ -7,8 +7,11 @@ Perspex requires Visual Studio 2015 to build on Windows.
 ### Install GTK Sharp
 
 To compile the full project under windows, you must have [gtk-sharp](http://www.mono-project.com/download/#download-win) installed. However, if you're 
-not interested in building the cross-platform bits you can simply unload the Perspex.Cairo and 
-Perspex.Gtk project in Visual Studio.
+not interested in building the cross-platform bits you can simply unload these projects from Visual Studio:
+
+ - Perspex.Cairo
+ - Perspex.Cairo.RenderTests
+ - Perspex.Gtk
 
 ### Clone the Perspex repository
 
@@ -19,7 +22,7 @@ is linked as a submodule in the git repository, so run:
 
     git submodule update --init
     
-The next step is to download the Skia native libraries. Run ```getnatives.ps1``` PowerShell script which can be found under the folder ```Perspex\src\Skia\```.
+The next step is to download the Skia native libraries. Run ```getnatives.ps1``` PowerShell script which can be found under the folder ```src\Skia\```.
 
 ## Linux
 
@@ -39,14 +42,14 @@ Then install the needed packages:
 
 ### Clone the Perspex repository
 
-    git clone https://github.com/grokys/Perspex.git
+    git clone https://github.com/Perspex/Perspex.git
 
 We currently need to build our own private version of ReactiveUI as it doesn't work on mono. This
 is linked as a submodule in the git repository, so run:
 
     git submodule update --init
     
-The next step is to download the Skia native libraries. Run ```getnatives.sh``` script which can be found under the folder ```Perspex\src\Skia\```.
+The next step is to download the Skia native libraries. Run ```getnatives.sh``` script which can be found under the folder ```src\Skia\```.
    
 ### Load the Project in MonoDevelop
 
@@ -57,4 +60,4 @@ Set the TestApplication project as the startup project and click Run.
 
 There will be some compile errors in the tests, but ignore them for now. 
 
-You can track the Linux version's progress in the [Linux issue](https://github.com/grokys/Perspex/issues/78).
+You can track the Linux version's progress in the [Linux issue](https://github.com/Perspex/Perspex/issues/78).

+ 12 - 0
samples/ControlCatalog/App.paml.cs

@@ -5,6 +5,7 @@ using Perspex.Controls;
 using Perspex.Diagnostics;
 using Perspex.Markup.Xaml;
 using Perspex.Themes.Default;
+using Serilog;
 
 namespace ControlCatalog
 {
@@ -14,6 +15,7 @@ namespace ControlCatalog
         {
             RegisterServices();
             InitializeSubsystems(GetPlatformId());
+            InitializeLogging();
             Styles = new DefaultTheme();
             InitializeComponent();
         }
@@ -38,6 +40,16 @@ namespace ControlCatalog
             PerspexXamlLoader.Load(this);
         }
 
+        private void InitializeLogging()
+        {
+#if DEBUG
+            Log.Logger = new LoggerConfiguration()
+                .MinimumLevel.Error()
+                .WriteTo.Trace(outputTemplate: "{Message}")
+                .CreateLogger();
+#endif
+        }
+
         private int GetPlatformId()
         {
             var args = Environment.GetCommandLineArgs();

+ 9 - 0
samples/ControlCatalog/ControlCatalog.csproj

@@ -36,6 +36,14 @@
     <StartupObject />
   </PropertyGroup>
   <ItemGroup>
+    <Reference Include="Serilog, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
+      <HintPath>..\..\packages\Serilog.1.5.9\lib\net45\Serilog.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
+    <Reference Include="Serilog.FullNetFx, Version=1.5.0.0, Culture=neutral, PublicKeyToken=24c2f752a8e58a10, processorArchitecture=MSIL">
+      <HintPath>..\..\packages\Serilog.1.5.9\lib\net45\Serilog.FullNetFx.dll</HintPath>
+      <Private>True</Private>
+    </Reference>
     <Reference Include="System" />
     <Reference Include="System.Core" />
     <Reference Include="System.Xml.Linq" />
@@ -93,6 +101,7 @@
       <SubType>Designer</SubType>
     </EmbeddedResource>
     <EmbeddedResource Include="Pages\DropDownPage.paml" />
+    <None Include="packages.config" />
   </ItemGroup>
   <ItemGroup>
     <ProjectReference Include="..\..\src\Gtk\Perspex.Cairo\Perspex.Cairo.csproj">

+ 4 - 0
samples/ControlCatalog/packages.config

@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<packages>
+  <package id="Serilog" version="1.5.9" targetFramework="net46" />
+</packages>

+ 10 - 7
src/Perspex.Base/PriorityValue.cs

@@ -224,26 +224,29 @@ namespace Perspex
         /// <param name="priority">The priority level that the value came from.</param>
         private void UpdateValue(object value, int priority)
         {
-            if (TypeUtilities.TryCast(_valueType, value, out value))
+            object castValue;
+
+            if (TypeUtilities.TryCast(_valueType, value, out castValue))
             {
                 var old = _value;
 
-                if (_validate != null && value != PerspexProperty.UnsetValue)
+                if (_validate != null && castValue != PerspexProperty.UnsetValue)
                 {
-                    value = _validate(value);
+                    castValue = _validate(castValue);
                 }
 
                 ValuePriority = priority;
-                _value = value;
+                _value = castValue;
                 _changed.OnNext(Tuple.Create(old, _value));
             }
             else if (_logger != null)
             {
                 _logger.Error(
-                    "Binding produced invalid value for {$Type} {$Property}: {$Value}",
-                    _valueType,
+                    "Binding produced invalid value for {$Property} ({$PropertyType}): {$Value} ({$ValueType})",
                     _name,
-                    value);
+                    _valueType,
+                    value,
+                    value.GetType());
             }
         }
 

+ 27 - 0
src/Perspex.Base/Threading/DispatcherTimer.cs

@@ -146,6 +146,33 @@ namespace Perspex.Threading
             return Disposable.Create(() => timer.Stop());
         }
 
+        /// <summary>
+        /// Runs a method once, after the specified interval.
+        /// </summary>
+        /// <param name="action">
+        /// The method to call after the interval has elapsed.
+        /// </param>
+        /// <param name="interval">The interval after which to call the method.</param>
+        /// <param name="priority">The priority to use.</param>
+        /// <returns>An <see cref="IDisposable"/> used to cancel the timer.</returns>
+        public static IDisposable RunOnce(
+            Action action,
+            TimeSpan interval,
+            DispatcherPriority priority = DispatcherPriority.Normal)
+        {
+            var timer = new DispatcherTimer(priority) { Interval = interval };
+
+            timer.Tick += (s, e) =>
+            {
+                action();
+                timer.Stop();
+            };
+
+            timer.Start();
+
+            return Disposable.Create(() => timer.Stop());
+        }
+
         /// <summary>
         /// Starts the timer.
         /// </summary>

+ 122 - 391
src/Perspex.Controls/DockPanel.cs

@@ -1,445 +1,176 @@
 namespace Perspex.Controls
 {
     using System;
-    using System.Collections.Generic;
-    using System.Diagnostics.CodeAnalysis;
-    using System.Linq;
-    using Layout;
 
-    public class DockPanel : Panel
+    /// <summary>
+    /// Defines the available docking modes for a control in a <see cref="DockPanel"/>.
+    /// </summary>
+    public enum Dock
     {
-        public static readonly PerspexProperty<Dock> DockProperty = PerspexProperty.RegisterAttached<DockPanel, Control, Dock>("Dock");
+        Left = 0,
+        Bottom,
+        Right,
+        Top
+    }
 
+    /// <summary>
+    /// A panel which arranges its children at the top, bottom, left, right or center.
+    /// </summary>
+    public class DockPanel : Panel
+    {
+        /// <summary>
+        /// Defines the Dock attached property.
+        /// </summary>
+        public static readonly PerspexProperty<Dock> DockProperty =
+            PerspexProperty.RegisterAttached<DockPanel, Control, Dock>("Dock");
+
+        /// <summary>
+        /// Defines the <see cref="LastChildFill"/> property.
+        /// </summary>
+        public static readonly PerspexProperty<bool> LastChildFillProperty =
+            PerspexProperty.Register<DockPanel, bool>(
+                nameof(LastChildFillProperty),
+                defaultValue: true);
+
+        /// <summary>
+        /// Initializes static members of the <see cref="DockPanel"/> class.
+        /// </summary>
         static DockPanel()
         {
             AffectsArrange(DockProperty);
         }
 
-        // ReSharper disable once UnusedMember.Global
-        public static Dock GetDock(PerspexObject perspexObject)
+        /// <summary>
+        /// Gets the value of the Dock attached property on the specified control.
+        /// </summary>
+        /// <param name="control">The control.</param>
+        /// <returns>The Dock attached property.</returns>
+        public static Dock GetDock(Control control)
         {
-            return perspexObject.GetValue(DockProperty);
+            return control.GetValue(DockProperty);
         }
 
-        // ReSharper disable once UnusedMember.Global
-        public static void SetDock(PerspexObject element, Dock dock)
+        /// <summary>
+        /// Sets the value of the Dock attached property on the specified control.
+        /// </summary>
+        /// <param name="control">The control.</param>
+        /// <param name="value">The value of the Dock property.</param>
+        public static void SetDock(Control control, Dock value)
         {
-            element.SetValue(DockProperty, dock);
+            control.SetValue(DockProperty, value);
         }
 
-        public static readonly PerspexProperty<bool> LastChildFillProperty = PerspexProperty.Register<DockPanel, bool>(nameof(LastChildFillProperty), defaultValue: true);
-
+        /// <summary>
+        /// Gets or sets a value which indicates whether the last child of the 
+        /// <see cref="DockPanel"/> fills the remaining space in the panel.
+        /// </summary>
         public bool LastChildFill
         {
             get { return GetValue(LastChildFillProperty); }
             set { SetValue(LastChildFillProperty, value); }
         }
 
-        protected override Size MeasureOverride(Size availableSize)
-        {
-            if (!LastChildFill)
-            {
-                return MeasureItemsThatWillBeDocked(availableSize, Children);
-            }
-
-            var sizeRequiredByDockingItems = MeasureItemsThatWillBeDocked(availableSize, Children.WithoutLast());
-            var elementThatWillFill = Children.Last();
-            elementThatWillFill.Measure(availableSize - sizeRequiredByDockingItems);
-            var finalSize = sizeRequiredByDockingItems.Inflate(new Thickness(elementThatWillFill.DesiredSize.Width, elementThatWillFill.DesiredSize.Height));
-            return finalSize;
-        }
-
-        private static Size MeasureItemsThatWillBeDocked(Size availableSize, IEnumerable<IControl> children)
+        /// <inheritdoc/>
+        protected override Size MeasureOverride(Size constraint)
         {
-            var requiredHorizontalLength = 0D;
-            var requiredVerticalLength = 0D;
+            double usedWidth = 0.0;
+            double usedHeight = 0.0;
+            double maximumWidth = 0.0;
+            double maximumHeight = 0.0;
 
-            foreach (var control in children)
+            // Measure each of the Children
+            foreach (Control element in Children)
             {
-                control.Measure(availableSize);
-
-                var dock = control.GetValue(DockProperty);
-                if (IsHorizontal(dock))
-                {
-                    requiredHorizontalLength += control.DesiredSize.Width;
-                }
-                else
+                // Get the child's desired size
+                Size remainingSize = new Size(
+                    Math.Max(0.0, constraint.Width - usedWidth),
+                    Math.Max(0.0, constraint.Height - usedHeight));
+                element.Measure(remainingSize);
+                Size desiredSize = element.DesiredSize;
+
+                // Decrease the remaining space for the rest of the children
+                switch (GetDock(element))
                 {
-                    requiredVerticalLength += control.DesiredSize.Height;
+                    case Dock.Left:
+                    case Dock.Right:
+                        maximumHeight = Math.Max(maximumHeight, usedHeight + desiredSize.Height);
+                        usedWidth += desiredSize.Width;
+                        break;
+                    case Dock.Top:
+                    case Dock.Bottom:
+                        maximumWidth = Math.Max(maximumWidth, usedWidth + desiredSize.Width);
+                        usedHeight += desiredSize.Height;
+                        break;
                 }
             }
 
-            return new Size(requiredHorizontalLength, requiredVerticalLength);
-        }
-
-        private static bool IsHorizontal(Dock dock)
-        {
-            return dock == Dock.Left || dock == Dock.Right;
-        }
-
-        protected override Size ArrangeOverride(Size finalSize)
-        {
-            if (!LastChildFill)
-            {
-                return ArrangeAllChildren(finalSize);
-            }
-            else
-            {
-                return ArrangeChildrenAndFillLastChild(finalSize);
-            }
-        }
-
-        private Size ArrangeChildrenAndFillLastChild(Size finalSize)
-        {
-            var docker = new DockingArranger();
-            var requiredSize = docker.ArrangeAndGetUsedSize(finalSize, Children.WithoutLast());
-            ArrangeToFill(Children.Last(), finalSize, docker.UsedMargin);
-            return requiredSize;
-        }
-
-        private Size ArrangeAllChildren(Size finalSize)
-        {
-            return new DockingArranger().ArrangeAndGetUsedSize(finalSize, Children);
+            maximumWidth = Math.Max(maximumWidth, usedWidth);
+            maximumHeight = Math.Max(maximumHeight, usedHeight);
+            return new Size(maximumWidth, maximumHeight);
         }
 
-        private static void ArrangeToFill(ILayoutable layoutable, Size containerSize, Margin margin)
+        /// <inheritdoc/>
+        protected override Size ArrangeOverride(Size arrangeSize)
         {
-            var containerRect = new Rect(new Point(0, 0), containerSize);
-            var marginsCutout = margin.AsThickness();
-            var withoutMargins = containerRect.Deflate(marginsCutout);
-
-            layoutable.Arrange(withoutMargins);
-        }
+            double left = 0.0;
+            double top = 0.0;
+            double right = 0.0;
+            double bottom = 0.0;
 
-        private class DockingArranger
-        {
-            public Margin UsedMargin { get; private set; }
+            // Arrange each of the Children
+            var children = Children;
+            int dockedCount = children.Count - (LastChildFill ? 1 : 0);
+            int index = 0;
 
-            public Size ArrangeAndGetUsedSize(Size availableSize, IEnumerable<IControl> children)
+            foreach (Control element in children)
             {
-                var leftArranger = new LeftDocker(availableSize);
-                var rightArranger = new RightDocker(availableSize);
-                var topArranger = new LeftDocker(availableSize.Swap());
-                var bottomArranger = new RightDocker(availableSize.Swap());
-
-                UsedMargin = new Margin();
-
-                foreach (var control in children)
+                // Determine the remaining space left to arrange the element
+                Rect remainingRect = new Rect(
+                    left,
+                    top,
+                    Math.Max(0.0, arrangeSize.Width - left - right),
+                    Math.Max(0.0, arrangeSize.Height - top - bottom));
+
+                // Trim the remaining Rect to the docked size of the element
+                // (unless the element should fill the remaining space because
+                // of LastChildFill)
+                if (index < dockedCount)
                 {
-                    Rect dockedRect;
-                    var dock = control.GetValue(DockProperty);
-                    switch (dock)
+                    Size desiredSize = element.DesiredSize;
+                    switch (GetDock(element))
                     {
                         case Dock.Left:
-                            dockedRect = leftArranger.GetDockedRect(control.DesiredSize, UsedMargin, control.GetAlignments());
+                            left += desiredSize.Width;
+                            remainingRect = remainingRect.WithWidth(desiredSize.Width);
                             break;
-
                         case Dock.Top:
-                            UsedMargin.Swap();
-                            dockedRect = topArranger.GetDockedRect(control.DesiredSize.Swap(), UsedMargin, control.GetAlignments().Swap()).Swap();
-                            UsedMargin.Swap();
+                            top += desiredSize.Height;
+                            remainingRect = remainingRect.WithHeight(desiredSize.Height);
                             break;
-
                         case Dock.Right:
-                            dockedRect = rightArranger.GetDockedRect(control.DesiredSize, UsedMargin, control.GetAlignments());
+                            right += desiredSize.Width;
+                            remainingRect = new Rect(
+                                Math.Max(0.0, arrangeSize.Width - right),
+                                remainingRect.Y,
+                                desiredSize.Width,
+                                remainingRect.Height);
                             break;
-
                         case Dock.Bottom:
-                            UsedMargin.Swap();
-                            dockedRect = bottomArranger.GetDockedRect(control.DesiredSize.Swap(), UsedMargin, control.GetAlignments().Swap()).Swap();
-                            UsedMargin.Swap();
+                            bottom += desiredSize.Height;
+                            remainingRect = new Rect(
+                                remainingRect.X,
+                                Math.Max(0.0, arrangeSize.Height - bottom),
+                                remainingRect.Width,
+                                desiredSize.Height);
                             break;
-
-                        default:
-                            throw new InvalidOperationException($"Invalid dock value {dock}");
                     }
-
-                    control.Arrange(dockedRect);
                 }
 
-                return availableSize;
-            }
-        }
-
-        private class LeftDocker : Docker
-        {
-            public LeftDocker(Size availableSize) : base(availableSize)
-            {
-            }
-
-            public override Rect GetDockedRect(Size childSize, Margin margin, Alignments alignments)
-            {
-                var marginsCutout = margin.AsThickness();
-                var availableRect = OriginalRect.Deflate(marginsCutout);
-                var alignedRect = AlignToLeft(availableRect, childSize, alignments.Vertical);
-
-                AccumulatedOffset += childSize.Width;
-                margin.Horizontal = margin.Horizontal.Offset(childSize.Width, 0);
-
-                return alignedRect;
-            }
-
-            private static Rect AlignToLeft(Rect availableRect, Size childSize, Alignment verticalAlignment)
-            {
-                return availableRect.AlignChild(childSize, Alignment.Start, verticalAlignment);
-            }
-        }
-
-        private class RightDocker : Docker
-        {
-            public RightDocker(Size availableSize) : base(availableSize)
-            {
-            }
-
-            public override Rect GetDockedRect(Size childSize, Margin margin, Alignments alignments)
-            {
-                var marginsCutout = margin.AsThickness();
-                var withoutMargins = OriginalRect.Deflate(marginsCutout);
-                var finalRect = withoutMargins.AlignChild(childSize, Alignment.End, alignments.Vertical);
-
-                AccumulatedOffset += childSize.Width;
-                margin.Horizontal = margin.Horizontal.Offset(0, childSize.Width);
-
-                return finalRect;
-            }
-        }
-
-        private abstract class Docker
-        {
-            protected Docker(Size availableSize)
-            {
-                OriginalRect = new Rect(new Point(0, 0), availableSize);
+                element.Arrange(remainingRect);
+                index++;
             }
 
-            protected double AccumulatedOffset { get; set; }
-
-            protected Rect OriginalRect { get; }
-
-            public abstract Rect GetDockedRect(Size childSize, Margin margin, Alignments alignments);
-        }
-    }
-
-    public class Margin
-    {
-        public Segment Horizontal { get; set; }
-        public Segment Vertical { get; set; }
-    }
-
-    public enum Alignment
-    {
-        Stretch, Start, Middle, End,
-    }
-
-    public static class SegmentMixin
-    {
-        public static Segment AlignToStart(this Segment container, double length)
-        {
-            return new Segment(container.Start, container.Start + length);
-        }
-
-        public static Segment AlignToEnd(this Segment container, double length)
-        {
-            return new Segment(container.End - length, container.End);
-        }
-
-        public static Segment AlignToMiddle(this Segment container, double length)
-        {
-            var start = container.Start + (container.Length - length) / 2;
-            return new Segment(start, start + length);
-        }
-    }
-
-    public struct Alignments
-    {
-        public Alignments(Alignment horizontal, Alignment vertical)
-        {
-            Horizontal = horizontal;
-            Vertical = vertical;
-        }
-
-        public Alignment Horizontal { get; }
-
-        public Alignment Vertical { get; }
-    }
-
-    public static class CoordinateMixin
-    {
-        private static Point Swap(this Point p)
-        {
-            return new Point(p.Y, p.X);
-        }
-
-        public static Size Swap(this Size s)
-        {
-            return new Size(s.Height, s.Width);
-        }
-
-        public static Rect Swap(this Rect r)
-        {
-            return new Rect(r.Position.Swap(), r.Size.Swap());
-        }
-
-        public static Segment Offset(this Segment l, double startOffset, double endOffset)
-        {
-            return new Segment(l.Start + startOffset, l.End + endOffset);
-        }
-
-        public static void Swap(this Margin m)
-        {
-            var v = m.Vertical;
-            m.Vertical = m.Horizontal;
-            m.Horizontal = v;
-        }
-
-        public static Thickness AsThickness(this Margin margin)
-        {
-            return new Thickness(margin.Horizontal.Start, margin.Vertical.Start, margin.Horizontal.End, margin.Vertical.End);
-        }
-
-        private static Alignment AsAlignment(this HorizontalAlignment horz)
-        {
-            switch (horz)
-            {
-                case HorizontalAlignment.Stretch:
-                    return Alignment.Stretch;
-                case HorizontalAlignment.Left:
-                    return Alignment.Start;
-                case HorizontalAlignment.Center:
-                    return Alignment.Middle;
-                case HorizontalAlignment.Right:
-                    return Alignment.End;
-                default:
-                    throw new ArgumentOutOfRangeException(nameof(horz), horz, null);
-            }
-        }
-
-        private static Alignment AsAlignment(this VerticalAlignment vert)
-        {
-            switch (vert)
-            {
-                case VerticalAlignment.Stretch:
-                    return Alignment.Stretch;
-                case VerticalAlignment.Top:
-                    return Alignment.Start;
-                case VerticalAlignment.Center:
-                    return Alignment.Middle;
-                case VerticalAlignment.Bottom:
-                    return Alignment.End;
-                default:
-                    throw new ArgumentOutOfRangeException(nameof(vert), vert, null);
-            }
-        }
-
-        public static Alignments GetAlignments(this ILayoutable layoutable)
-        {
-            return new Alignments(layoutable.HorizontalAlignment.AsAlignment(), layoutable.VerticalAlignment.AsAlignment());
-        }
-
-        public static Alignments Swap(this Alignments alignments)
-        {
-            return new Alignments(alignments.Vertical, alignments.Horizontal);
-        }
-    }
-
-    public enum Dock
-    {
-        Left = 0,
-        Bottom,
-        Right,
-        Top
-    }
-
-    public static class RectMixin
-    {
-        public static Rect AlignChild(this Rect container, Size childSize, Alignment horizontalAlignment, Alignment verticalAlignment)
-        {
-            var horzSegment = container.GetHorizontalCoordinates();
-            var vertSegment = container.GetVerticalCoordinates();
-
-            var horzResult = GetAlignedSegment(childSize.Width, horizontalAlignment, horzSegment);
-            var vertResult = GetAlignedSegment(childSize.Height, verticalAlignment, vertSegment);
-
-            return FromSegments(horzResult, vertResult);
-        }
-
-        private static Rect FromSegments(Segment horzSegment, Segment vertSegment)
-        {
-            return new Rect(horzSegment.Start, vertSegment.Start, horzSegment.Length, vertSegment.Length);
-        }
-
-        private static Segment GetAlignedSegment(double width, Alignment alignment, Segment horzSegment)
-        {
-            switch (alignment)
-            {
-                case Alignment.Start:
-                    return horzSegment.AlignToStart(width);
-
-                case Alignment.Middle:
-                    return horzSegment.AlignToMiddle(width);
-
-                case Alignment.End:
-                    return horzSegment.AlignToEnd(width);
-
-                default:
-                    return new Segment(horzSegment.Start, horzSegment.End);
-            }
-        }
-
-        private static Segment GetHorizontalCoordinates(this Rect rect)
-        {
-            return new Segment(rect.X, rect.Right);
-        }
-
-        private static Segment GetVerticalCoordinates(this Rect rect)
-        {
-            return new Segment(rect.Y, rect.Bottom);
-        }
-    }
-
-    public struct Segment
-    {
-        public Segment(double start, double end)
-        {
-            Start = start;
-            End = end;
-        }
-
-        public double Start { get; }
-        public double End { get; }
-
-        public double Length => End - Start;
-
-        public override string ToString()
-        {
-            return $"Start: {Start}, End: {End}";
-        }
-    }
-
-    public static class EnumerableMixin
-    {
-        private static IEnumerable<T> Shrink<T>(this IEnumerable<T> source, int left, int right)
-        {
-            int i = 0;
-            var buffer = new Queue<T>(right + 1);
-
-            foreach (T x in source)
-            {
-                if (i >= left) // Read past left many elements at the start
-                {
-                    buffer.Enqueue(x);
-                    if (buffer.Count > right) // Build a buffer to drop right many elements at the end
-                        yield return buffer.Dequeue();
-                }
-                else i++;
-            }
-        }
-        public static IEnumerable<T> WithoutLast<T>(this IEnumerable<T> source, int n = 1)
-        {
-            return source.Shrink(0, n);
-        }
-        public static IEnumerable<T> WithoutFirst<T>(this IEnumerable<T> source, int n = 1)
-        {
-            return source.Shrink(n, 0);
+            return arrangeSize;
         }
     }
 }

+ 13 - 0
src/Perspex.Controls/Generators/ItemContainerEventArgs.cs

@@ -12,6 +12,19 @@ namespace Perspex.Controls.Generators
     /// </summary>
     public class ItemContainerEventArgs : EventArgs
     {
+        /// <summary>
+        /// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
+        /// </summary>
+        /// <param name="startingIndex">The index of the first container in the source items.</param>
+        /// <param name="container">The container.</param>
+        public ItemContainerEventArgs(
+            int startingIndex,
+            ItemContainer container)
+        {
+            StartingIndex = startingIndex;
+            Containers = new[] { container };
+        }
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ItemContainerEventArgs"/> class.
         /// </summary>

+ 19 - 0
src/Perspex.Controls/Generators/TreeContainerIndex.cs

@@ -1,6 +1,7 @@
 // Copyright (c) The Perspex Project. All rights reserved.
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
+using System;
 using System.Collections.Generic;
 
 namespace Perspex.Controls.Generators
@@ -19,6 +20,16 @@ namespace Perspex.Controls.Generators
         private readonly Dictionary<object, IControl> _itemToContainer = new Dictionary<object, IControl>();
         private readonly Dictionary<IControl, object> _containerToItem = new Dictionary<IControl, object>();
 
+        /// <summary>
+        /// Signalled whenever new containers are materialized.
+        /// </summary>
+        public event EventHandler<ItemContainerEventArgs> Materialized;
+
+        /// <summary>
+        /// Event raised whenever containers are dematerialized.
+        /// </summary>
+        public event EventHandler<ItemContainerEventArgs> Dematerialized;
+
         /// <summary>
         /// Gets the currently materialized containers.
         /// </summary>
@@ -33,6 +44,10 @@ namespace Perspex.Controls.Generators
         {
             _itemToContainer.Add(item, container);
             _containerToItem.Add(container, item);
+
+            Materialized?.Invoke(
+                this, 
+                new ItemContainerEventArgs(0, new ItemContainer(container, item, 0)));
         }
 
         /// <summary>
@@ -44,6 +59,10 @@ namespace Perspex.Controls.Generators
             var item = _containerToItem[container];
             _containerToItem.Remove(container);
             _itemToContainer.Remove(item);
+
+            Dematerialized?.Invoke(
+                this, 
+                new ItemContainerEventArgs(0, new ItemContainer(container, item, 0)));
         }
 
         /// <summary>

+ 1 - 1
src/Perspex.Controls/Primitives/HeaderedItemsControl.cs

@@ -51,8 +51,8 @@ namespace Perspex.Controls.Primitives
         /// <inheritdoc/>
         protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
         {
-            base.OnTemplateApplied(e);
             HeaderPresenter = e.NameScope.Find<ContentPresenter>("PART_HeaderPresenter");
+            base.OnTemplateApplied(e);
         }
     }
 }

+ 56 - 1
src/Perspex.Controls/TreeView.cs

@@ -8,6 +8,7 @@ using Perspex.Controls.Primitives;
 using Perspex.Input;
 using Perspex.Interactivity;
 using Perspex.Styling;
+using Perspex.Threading;
 using Perspex.VisualTree;
 
 namespace Perspex.Controls
@@ -17,6 +18,14 @@ namespace Perspex.Controls
     /// </summary>
     public class TreeView : ItemsControl
     {
+        /// <summary>
+        /// Defines the <see cref="AutoScrollToSelectedItem"/> property.
+        /// </summary>
+        public static readonly PerspexProperty<bool> AutoScrollToSelectedItemProperty =
+            PerspexProperty.Register<TreeView, bool>(
+                nameof(AutoScrollToSelectedItem),
+                defaultValue: true);
+
         /// <summary>
         /// Defines the <see cref="SelectedItem"/> property.
         /// </summary>
@@ -41,6 +50,15 @@ namespace Perspex.Controls
         public new ITreeItemContainerGenerator ItemContainerGenerator => 
             (ITreeItemContainerGenerator)base.ItemContainerGenerator;
 
+        /// <summary>
+        /// Gets or sets a value indicating whether to automatically scroll to newly selected items.
+        /// </summary>
+        public bool AutoScrollToSelectedItem
+        {
+            get { return GetValue(AutoScrollToSelectedItemProperty); }
+            set { SetValue(AutoScrollToSelectedItemProperty, value); }
+        }
+
         /// <summary>
         /// Gets or sets the selected item.
         /// </summary>
@@ -65,6 +83,11 @@ namespace Perspex.Controls
                 {
                     var container = ItemContainerGenerator.Index.ContainerFromItem(_selectedItem);
                     MarkContainerSelected(container, true);
+
+                    if (AutoScrollToSelectedItem && container != null)
+                    {
+                        container.BringIntoView();
+                    }
                 }
             }
         }
@@ -72,12 +95,14 @@ namespace Perspex.Controls
         /// <inheritdoc/>
         protected override IItemContainerGenerator CreateItemContainerGenerator()
         {
-            return new TreeItemContainerGenerator<TreeViewItem>(
+            var result = new TreeItemContainerGenerator<TreeViewItem>(
                 this,
                 TreeViewItem.HeaderProperty,
                 TreeViewItem.ItemsProperty,
                 TreeViewItem.IsExpandedProperty,
                 new TreeContainerIndex());
+            result.Index.Materialized += ContainerMaterialized;
+            return result;
         }
 
         /// <inheritdoc/>
@@ -190,6 +215,36 @@ namespace Perspex.Controls
             return null;
         }
 
+        /// <summary>
+        /// Called when a new item container is materialized, to set its selected state.
+        /// </summary>
+        /// <param name="sender">The event sender.</param>
+        /// <param name="e">The event args.</param>
+        private void ContainerMaterialized(object sender, ItemContainerEventArgs e)
+        {
+            var selectedItem = SelectedItem;
+
+            if (selectedItem != null)
+            {
+                foreach (var container in e.Containers)
+                {
+                    if (container.Item == selectedItem)
+                    {
+                        ((TreeViewItem)container.ContainerControl).IsSelected = true;
+
+                        if (AutoScrollToSelectedItem)
+                        {
+                            DispatcherTimer.RunOnce(
+                                container.ContainerControl.BringIntoView,
+                                TimeSpan.Zero);
+                        }
+
+                        break;
+                    }
+                }
+            }
+        }
+
         /// <summary>
         /// Sets a container's 'selected' class or <see cref="ISelectable.IsSelected"/>.
         /// </summary>

+ 1 - 21
src/Perspex.Controls/TreeViewItem.cs

@@ -73,16 +73,12 @@ namespace Perspex.Controls
         /// <inheritdoc/>
         protected override IItemContainerGenerator CreateItemContainerGenerator()
         {
-            var result =  new TreeItemContainerGenerator<TreeViewItem>(
+            return new TreeItemContainerGenerator<TreeViewItem>(
                 this,
                 TreeViewItem.HeaderProperty,
                 TreeViewItem.ItemsProperty,
                 TreeViewItem.IsExpandedProperty,
                 _treeView?.ItemContainerGenerator.Index ?? new TreeContainerIndex());
-
-            result.Materialized += ItemMaterialized;
-
-            return result;
         }
 
         /// <inheritdoc/>
@@ -123,21 +119,5 @@ namespace Perspex.Controls
 
             base.OnKeyDown(e);
         }
-
-        private void ItemMaterialized(object sender, ItemContainerEventArgs e)
-        {
-            var selectedItem = _treeView?.SelectedItem;
-
-            if (selectedItem != null)
-            {
-                foreach (var container in e.Containers)
-                {
-                    if (container.Item == selectedItem)
-                    {
-                        ((TreeViewItem)container.ContainerControl).IsSelected = true;
-                    }
-                }
-            }
-        }
     }
 }

+ 0 - 120
src/Perspex.Diagnostics/DevTools.cs

@@ -1,120 +0,0 @@
-// Copyright (c) The Perspex Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Reactive.Linq;
-using Perspex.Controls;
-using Perspex.Diagnostics.ViewModels;
-using Perspex.Input;
-using Perspex.Themes.Default;
-using ReactiveUI;
-
-namespace Perspex.Diagnostics
-{
-    public class DevTools : Decorator
-    {
-        public static readonly PerspexProperty<Control> RootProperty =
-            PerspexProperty.Register<DevTools, Control>("Root");
-
-        private readonly DevToolsViewModel _viewModel;
-
-        public DevTools()
-        {
-            _viewModel = new DevToolsViewModel();
-            this.GetObservable(RootProperty).Subscribe(x => _viewModel.Root = x);
-
-            InitializeComponent();
-        }
-
-        public Control Root
-        {
-            get { return GetValue(RootProperty); }
-            set { SetValue(RootProperty, value); }
-        }
-
-        public static IDisposable Attach(Window window)
-        {
-            return window.AddHandler(
-                KeyDownEvent,
-                WindowPreviewKeyDown,
-                Interactivity.RoutingStrategies.Tunnel);
-        }
-
-        private static void WindowPreviewKeyDown(object sender, KeyEventArgs e)
-        {
-            if (e.Key == Key.F12)
-            {
-                Window window = new Window
-                {
-                    Width = 1024,
-                    Height = 512,
-                    Content = new DevTools
-                    {
-                        Root = (Window)sender,
-                    },
-                };
-
-                window.Show();
-            }
-        }
-
-        private void InitializeComponent()
-        {
-            DataTemplates.Add(new ViewLocator<ReactiveObject>());
-            Styles.Add(new DefaultTheme());
-
-            Child = new Grid
-            {
-                RowDefinitions = new RowDefinitions("*,Auto"),
-                Children = new Controls.Controls
-                {
-                    new TabControl
-                    {
-                        Items = new[]
-                        {
-                            new TabItem
-                            {
-                                Header = "Logical Tree",
-                                [!ContentControl.ContentProperty] = _viewModel.WhenAnyValue(x => x.LogicalTree),
-                            },
-                            new TabItem
-                            {
-                                Header = "Visual Tree",
-                                [!ContentControl.ContentProperty] = _viewModel.WhenAnyValue(x => x.VisualTree),
-                            }
-                        },
-                    },
-                    new StackPanel
-                    {
-                        Orientation = Orientation.Horizontal,
-                        Gap = 4,
-                        [Grid.RowProperty] = 1,
-                        Children = new Controls.Controls
-                        {
-                            new TextBlock
-                            {
-                                Text = "Focused: "
-                            },
-                            new TextBlock
-                            {
-                                [!TextBlock.TextProperty] = _viewModel
-                                    .WhenAnyValue(x => x.FocusedControl)
-                                    .Select(x => x?.GetType().Name ?? "(null)")
-                            },
-                            new TextBlock
-                            {
-                                Text = "Pointer Over: "
-                            },
-                            new TextBlock
-                            {
-                                [!TextBlock.TextProperty] = _viewModel
-                                    .WhenAnyValue(x => x.PointerOverElement)
-                                    .Select(x => x?.GetType().Name ?? "(null)")
-                            }
-                        }
-                    }
-                }
-            };
-        }
-    }
-}

+ 18 - 0
src/Perspex.Diagnostics/DevTools.paml

@@ -0,0 +1,18 @@
+<UserControl xmlns="https://github.com/perspex">
+  <Grid RowDefinitions="Auto,*,Auto">
+    <TabStrip SelectedIndex="{Binding SelectedTab, Mode=TwoWay}">
+      <TabStripItem Content="Logical Tree"/>
+      <TabStripItem Content="Visual Tree"/>
+    </TabStrip>
+
+    <ContentControl Content="{Binding Content}" Grid.Row="1"/> 
+    
+    <StackPanel Gap="4" Orientation="Horizontal" Grid.Row="2">
+      <TextBlock>Focused:</TextBlock>
+      <TextBlock Text="{Binding FocusedControl}"/>
+      <Separator/>
+      <TextBlock>Pointer Over:</TextBlock>
+      <TextBlock Text="{Binding PointerOverElement}"/>
+    </StackPanel>
+  </Grid>
+</UserControl>

+ 92 - 0
src/Perspex.Diagnostics/DevTools.paml.cs

@@ -0,0 +1,92 @@
+using System;
+using System.Collections.Generic;
+using Perspex.Controls;
+using Perspex.Controls.Templates;
+using Perspex.Diagnostics.ViewModels;
+using Perspex.Input;
+using Perspex.Interactivity;
+using Perspex.Markup.Xaml;
+using ReactiveUI;
+
+namespace Perspex.Diagnostics
+{
+    public class DevTools : UserControl
+    {
+        private static Dictionary<Window, Window> s_open = new Dictionary<Window, Window>();
+
+        public DevTools(IControl root)
+        {
+            InitializeComponent();
+            Root = root;
+            DataContext = new DevToolsViewModel(root);
+            Root.PointerMoved += RootPointerMoved;
+        }
+
+        public IControl Root { get; }
+
+        public static IDisposable Attach(Window window)
+        {
+            return window.AddHandler(
+                KeyDownEvent,
+                WindowPreviewKeyDown,
+                RoutingStrategies.Tunnel);
+        }
+
+        private static void WindowPreviewKeyDown(object sender, KeyEventArgs e)
+        {
+            if (e.Key == Key.F12)
+            {
+                var window = (Window)sender;
+                var devToolsWindow = default(Window);
+
+                if (s_open.TryGetValue(window, out devToolsWindow))
+                {
+                    devToolsWindow.Activate();
+                }
+                else
+                {
+                    devToolsWindow = new Window
+                    {
+                        Width = 1024,
+                        Height = 512,
+                        Content = new DevTools(window),
+                        DataTemplates = new DataTemplates
+                        {
+                            new ViewLocator<ReactiveObject>(),
+                        }
+                    };
+
+                    devToolsWindow.Closed += DevToolsClosed;
+                    s_open.Add((Window)sender, devToolsWindow);
+                    devToolsWindow.Show();
+                }
+            }
+        }
+
+        private static void DevToolsClosed(object sender, EventArgs e)
+        {
+            var devToolsWindow = (Window)sender;
+            var devTools = (DevTools)devToolsWindow.Content;
+            var window = (Window)devTools.Root;
+
+            s_open.Remove(window);
+            devToolsWindow.Closed -= DevToolsClosed;
+        }
+
+        private void InitializeComponent()
+        {
+            PerspexXamlLoader.Load(this);
+        }
+
+        private void RootPointerMoved(object sender, PointerEventArgs e)
+        {
+            var modifiers = InputModifiers.Control | InputModifiers.Shift;
+
+            if ((e.InputModifiers & modifiers) == modifiers)
+            {
+                var vm = (DevToolsViewModel)DataContext;
+                vm.SelectControl((IControl)e.Source);
+            }
+        }
+    }
+}

+ 21 - 6
src/Perspex.Diagnostics/Perspex.Diagnostics.csproj

@@ -40,6 +40,14 @@
   </PropertyGroup>
   <ItemGroup>
     <!-- A reference to the entire .NET Framework is automatically included -->
+    <ProjectReference Include="..\Markup\Perspex.Markup.Xaml\Perspex.Markup.Xaml.csproj">
+      <Project>{3e53a01a-b331-47f3-b828-4a5717e77a24}</Project>
+      <Name>Perspex.Markup.Xaml</Name>
+    </ProjectReference>
+    <ProjectReference Include="..\Markup\Perspex.Markup\Perspex.Markup.csproj">
+      <Project>{6417e941-21bc-467b-a771-0de389353ce6}</Project>
+      <Name>Perspex.Markup</Name>
+    </ProjectReference>
     <ProjectReference Include="..\Perspex.Animation\Perspex.Animation.csproj">
       <Project>{D211E587-D8BC-45B9-95A4-F297C8FA5200}</Project>
       <Name>Perspex.Animation</Name>
@@ -86,23 +94,24 @@
       <Link>Properties\SharedAssemblyInfo.cs</Link>
     </Compile>
     <Compile Include="LogManager.cs" />
-    <Compile Include="DevTools.cs" />
     <Compile Include="Debug.cs" />
     <Compile Include="Properties\AssemblyInfo.cs" />
     <Compile Include="ViewLocator.cs" />
     <Compile Include="ViewModels\DevToolsViewModel.cs" />
-    <Compile Include="ViewModels\VisualTreeViewModel.cs" />
-    <Compile Include="ViewModels\LogicalTreeViewModel.cs" />
+    <Compile Include="ViewModels\TreePageViewModel.cs" />
     <Compile Include="ViewModels\PropertyDetails.cs" />
     <Compile Include="ViewModels\ControlDetailsViewModel.cs" />
     <Compile Include="ViewModels\LogicalTreeNode.cs" />
     <Compile Include="ViewModels\TreeNode.cs" />
     <Compile Include="ViewModels\VisualTreeNode.cs" />
+    <Compile Include="Views\TreePage.paml.cs">
+      <DependentUpon>TreePageView.paml</DependentUpon>
+    </Compile>
+    <Compile Include="DevTools.paml.cs">
+      <DependentUpon>DevTools.paml</DependentUpon>
+    </Compile>
     <Compile Include="Views\ControlDetailsView.cs" />
     <Compile Include="Views\GridRepeater.cs" />
-    <Compile Include="Views\VisualTreeView.cs" />
-    <Compile Include="Views\LogicalTreeView.cs" />
-    <Compile Include="Views\TreePage.cs" />
   </ItemGroup>
   <ItemGroup>
     <Reference Include="Splat">
@@ -124,6 +133,12 @@
   <ItemGroup>
     <None Include="app.config" />
     <None Include="packages.config" />
+    <EmbeddedResource Include="DevTools.paml">
+      <SubType>Designer</SubType>
+    </EmbeddedResource>
+    <EmbeddedResource Include="Views\TreePageView.paml">
+      <SubType>Designer</SubType>
+    </EmbeddedResource>
   </ItemGroup>
   <Import Project="$(MSBuildExtensionsPath32)\Microsoft\Portable\$(TargetFrameworkVersion)\Microsoft.Portable.CSharp.targets" />
   <!-- To modify your build process, add your task inside one of the targets below and uncomment it. 

+ 1 - 1
src/Perspex.Diagnostics/ViewLocator.cs

@@ -7,7 +7,7 @@ using Perspex.Controls.Templates;
 
 namespace Perspex.Diagnostics
 {
-    internal class ViewLocator<TViewModel> : IDataTemplate
+    public class ViewLocator<TViewModel> : IDataTemplate
     {
         public IControl Build(object data)
         {

+ 44 - 25
src/Perspex.Diagnostics/ViewModels/DevToolsViewModel.cs

@@ -11,54 +11,73 @@ namespace Perspex.Diagnostics.ViewModels
 {
     internal class DevToolsViewModel : ReactiveObject
     {
-        private Control _root;
+        private IControl _root;
 
-        private LogicalTreeViewModel _logicalTree;
+        private ReactiveObject _content;
 
-        private VisualTreeViewModel _visualTree;
+        private int _selectedTab;
 
-        private readonly ObservableAsPropertyHelper<IInputElement> _focusedControl;
+        private TreePageViewModel _logicalTree;
 
-        private readonly ObservableAsPropertyHelper<IInputElement> _pointerOverElement;
+        private TreePageViewModel _visualTree;
 
-        public DevToolsViewModel()
+        private readonly ObservableAsPropertyHelper<string> _focusedControl;
+
+        private readonly ObservableAsPropertyHelper<string> _pointerOverElement;
+
+        public DevToolsViewModel(IControl root)
         {
-            this.WhenAnyValue(x => x.Root).Subscribe(x =>
+            _root = root;
+            _logicalTree = new TreePageViewModel(LogicalTreeNode.Create(root));
+            _visualTree = new TreePageViewModel(VisualTreeNode.Create(root));
+
+            this.WhenAnyValue(x => x.SelectedTab).Subscribe(index =>
             {
-                LogicalTree = new LogicalTreeViewModel(_root);
-                VisualTree = new VisualTreeViewModel(_root);
+                switch (index)
+                {
+                    case 0:
+                        Content = _logicalTree;
+                        break;
+                    case 1:
+                        Content = _visualTree;
+                        break;
+                }
             });
 
             _focusedControl = KeyboardDevice.Instance
                 .WhenAnyValue(x => x.FocusedElement)
+                .Select(x => x?.GetType().Name)
                 .ToProperty(this, x => x.FocusedControl);
 
-            _pointerOverElement = this.WhenAnyValue(x => x.Root, x => x as TopLevel)
-                .Select(x => x?.GetObservable(TopLevel.PointerOverElementProperty) ?? Observable.Empty<IInputElement>())
-                .Switch()
+            _pointerOverElement = root.GetObservable(TopLevel.PointerOverElementProperty)
+                .Select(x => x?.GetType().Name)
                 .ToProperty(this, x => x.PointerOverElement);
         }
 
-        public Control Root
+        public ReactiveObject Content
         {
-            get { return _root; }
-            set { this.RaiseAndSetIfChanged(ref _root, value); }
+            get { return _content; }
+            private set { this.RaiseAndSetIfChanged(ref _content, value); }
         }
 
-        public LogicalTreeViewModel LogicalTree
+        public int SelectedTab
         {
-            get { return _logicalTree; }
-            private set { this.RaiseAndSetIfChanged(ref _logicalTree, value); }
+            get { return _selectedTab; }
+            set { this.RaiseAndSetIfChanged(ref _selectedTab, value); }
         }
 
-        public VisualTreeViewModel VisualTree
-        {
-            get { return _visualTree; }
-            private set { this.RaiseAndSetIfChanged(ref _visualTree, value); }
-        }
+        public string FocusedControl => _focusedControl.Value;
 
-        public IInputElement FocusedControl => _focusedControl.Value;
+        public string PointerOverElement => _pointerOverElement.Value;
 
-        public IInputElement PointerOverElement => _pointerOverElement.Value;
+        public void SelectControl(IControl control)
+        {
+            var tree = Content as TreePageViewModel;
+
+            if (tree != null)
+            {
+                tree.SelectControl(control);
+            }
+        }
     }
 }

+ 4 - 4
src/Perspex.Diagnostics/ViewModels/LogicalTreeNode.cs

@@ -9,16 +9,16 @@ namespace Perspex.Diagnostics.ViewModels
 {
     internal class LogicalTreeNode : TreeNode
     {
-        public LogicalTreeNode(ILogical logical)
-            : base((Control)logical)
+        public LogicalTreeNode(ILogical logical, TreeNode parent)
+            : base((Control)logical, parent)
         {
-            Children = logical.LogicalChildren.CreateDerivedCollection(x => new LogicalTreeNode(x));
+            Children = logical.LogicalChildren.CreateDerivedCollection(x => new LogicalTreeNode(x, this));
         }
 
         public static LogicalTreeNode[] Create(object control)
         {
             var logical = control as ILogical;
-            return logical != null ? new[] { new LogicalTreeNode(logical) } : null;
+            return logical != null ? new[] { new LogicalTreeNode(logical, null) } : null;
         }
     }
 }

+ 0 - 34
src/Perspex.Diagnostics/ViewModels/LogicalTreeViewModel.cs

@@ -1,34 +0,0 @@
-// Copyright (c) The Perspex Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System.Reactive.Linq;
-using Perspex.Controls;
-using ReactiveUI;
-
-namespace Perspex.Diagnostics.ViewModels
-{
-    internal class LogicalTreeViewModel : ReactiveObject
-    {
-        private LogicalTreeNode _selected;
-
-        private readonly ObservableAsPropertyHelper<ControlDetailsViewModel> _details;
-
-        public LogicalTreeViewModel(Control root)
-        {
-            Nodes = LogicalTreeNode.Create(root);
-            _details = this.WhenAnyValue(x => x.SelectedNode)
-                .Select(x => x != null ? new ControlDetailsViewModel(x.Control) : null)
-                .ToProperty(this, x => x.Details);
-        }
-
-        public LogicalTreeNode[] Nodes { get; }
-
-        public LogicalTreeNode SelectedNode
-        {
-            get { return _selected; }
-            set { this.RaiseAndSetIfChanged(ref _selected, value); }
-        }
-
-        public ControlDetailsViewModel Details => _details.Value;
-    }
-}

+ 16 - 4
src/Perspex.Diagnostics/ViewModels/TreeNode.cs

@@ -13,10 +13,12 @@ namespace Perspex.Diagnostics.ViewModels
     internal class TreeNode : ReactiveObject
     {
         private string _classes;
+        private bool _isExpanded;
 
-        public TreeNode(Control control)
+        public TreeNode(Control control, TreeNode parent)
         {
             Control = control;
+            Parent = parent;
             Type = control.GetType().Name;
 
             var classesChanged = Observable.FromEventPattern<
@@ -52,13 +54,23 @@ namespace Perspex.Diagnostics.ViewModels
             private set { this.RaiseAndSetIfChanged(ref _classes, value); }
         }
 
-        public string Type
+        public Control Control
         {
             get;
-            private set;
         }
 
-        public Control Control
+        public bool IsExpanded
+        {
+            get { return _isExpanded; }
+            set { this.RaiseAndSetIfChanged(ref _isExpanded, value); }
+        }
+
+        public TreeNode Parent
+        {
+            get;
+        }
+
+        public string Type
         {
             get;
             private set;

+ 102 - 0
src/Perspex.Diagnostics/ViewModels/TreePageViewModel.cs

@@ -0,0 +1,102 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using System.Reactive.Linq;
+using Perspex.Controls;
+using Perspex.VisualTree;
+using ReactiveUI;
+
+namespace Perspex.Diagnostics.ViewModels
+{
+    internal class TreePageViewModel : ReactiveObject
+    {
+        private TreeNode _selected;
+
+        private readonly ObservableAsPropertyHelper<ControlDetailsViewModel> _details;
+
+        public TreePageViewModel(TreeNode[] nodes)
+        {
+            Nodes = nodes;
+            _details = this.WhenAnyValue(x => x.SelectedNode)
+                .Select(x => x != null ? new ControlDetailsViewModel(x.Control) : null)
+                .ToProperty(this, x => x.Details);
+        }
+
+        public TreeNode[] Nodes { get; protected set; }
+
+        public TreeNode SelectedNode
+        {
+            get { return _selected; }
+            set { this.RaiseAndSetIfChanged(ref _selected, value); }
+        }
+
+        public ControlDetailsViewModel Details => _details.Value;
+
+        public TreeNode FindNode(IControl control)
+        {
+            foreach (var node in Nodes)
+            {
+                var result = FindNode(node, control);
+
+                if (result != null)
+                {
+                    return result;
+                }
+            }
+
+            return null;
+        }
+
+        public void SelectControl(IControl control)
+        {
+            var node = default(TreeNode);
+
+            while (node == null && control != null)
+            {
+                node = FindNode(control);
+
+                if (node == null)
+                {
+                    control = control.GetVisualParent<IControl>();
+                }
+            }            
+
+            if (node != null)
+            {
+                SelectedNode = node;
+                ExpandNode(node.Parent);
+            }
+        }
+
+        private void ExpandNode(TreeNode node)
+        {
+            if (node != null)
+            {
+                node.IsExpanded = true;
+                ExpandNode(node.Parent);
+            }
+        }
+
+        private TreeNode FindNode(TreeNode node, IControl control)
+        {
+            if (node.Control == control)
+            {
+                return node;
+            }
+            else
+            {
+                foreach (var child in node.Children)
+                {
+                    var result = FindNode(child, control);
+
+                    if (result != null)
+                    {
+                        return result;
+                    }
+                }
+            }
+
+            return null;
+        }
+    }
+}

+ 5 - 5
src/Perspex.Diagnostics/ViewModels/VisualTreeNode.cs

@@ -9,18 +9,18 @@ namespace Perspex.Diagnostics.ViewModels
 {
     internal class VisualTreeNode : TreeNode
     {
-        public VisualTreeNode(IVisual visual)
-            : base((Control)visual)
+        public VisualTreeNode(IVisual visual, TreeNode parent)
+            : base((Control)visual, parent)
         {
             var host = visual as IVisualTreeHost;
 
             if (host?.Root == null)
             {
-                Children = visual.VisualChildren.CreateDerivedCollection(x => new VisualTreeNode(x));
+                Children = visual.VisualChildren.CreateDerivedCollection(x => new VisualTreeNode(x, this));
             }
             else
             {
-                Children = new ReactiveList<VisualTreeNode>(new[] { new VisualTreeNode(host.Root) });
+                Children = new ReactiveList<VisualTreeNode>(new[] { new VisualTreeNode(host.Root, this) });
             }
 
             if (Control != null)
@@ -34,7 +34,7 @@ namespace Perspex.Diagnostics.ViewModels
         public static VisualTreeNode[] Create(object control)
         {
             var visual = control as IVisual;
-            return visual != null ? new[] { new VisualTreeNode(visual) } : null;
+            return visual != null ? new[] { new VisualTreeNode(visual, null) } : null;
         }
     }
 }

+ 0 - 34
src/Perspex.Diagnostics/ViewModels/VisualTreeViewModel.cs

@@ -1,34 +0,0 @@
-// Copyright (c) The Perspex Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System.Reactive.Linq;
-using Perspex.Controls;
-using ReactiveUI;
-
-namespace Perspex.Diagnostics.ViewModels
-{
-    internal class VisualTreeViewModel : ReactiveObject
-    {
-        private VisualTreeNode _selected;
-
-        private readonly ObservableAsPropertyHelper<ControlDetailsViewModel> _details;
-
-        public VisualTreeViewModel(Control root)
-        {
-            Nodes = VisualTreeNode.Create(root);
-            _details = this.WhenAnyValue(x => x.SelectedNode)
-                .Select(x => x != null ? new ControlDetailsViewModel(x.Control) : null)
-                .ToProperty(this, x => x.Details);
-        }
-
-        public VisualTreeNode[] Nodes { get; }
-
-        public VisualTreeNode SelectedNode
-        {
-            get { return _selected; }
-            set { this.RaiseAndSetIfChanged(ref _selected, value); }
-        }
-
-        public ControlDetailsViewModel Details => _details.Value;
-    }
-}

+ 0 - 99
src/Perspex.Diagnostics/Views/LogicalTreeView.cs

@@ -1,99 +0,0 @@
-// Copyright (c) The Perspex Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Reactive.Linq;
-using Perspex.Controls;
-using Perspex.Controls.Templates;
-using Perspex.Diagnostics.ViewModels;
-using ReactiveUI;
-
-namespace Perspex.Diagnostics.Views
-{
-    using Controls = Controls.Controls;
-
-    internal class LogicalTreeView : TreePage
-    {
-        private static readonly PerspexProperty<LogicalTreeViewModel> ViewModelProperty =
-            PerspexProperty.Register<LogicalTreeView, LogicalTreeViewModel>("ViewModel");
-
-        public LogicalTreeView()
-        {
-            InitializeComponent();
-            this.GetObservable(DataContextProperty)
-                .Subscribe(x => ViewModel = (LogicalTreeViewModel)x);
-        }
-
-        public LogicalTreeViewModel ViewModel
-        {
-            get { return GetValue(ViewModelProperty); }
-            private set { SetValue(ViewModelProperty, value); }
-        }
-
-        private void InitializeComponent()
-        {
-            TreeView tree;
-
-            Content = new Grid
-            {
-                ColumnDefinitions = new ColumnDefinitions
-                {
-                    new ColumnDefinition(1, GridUnitType.Star),
-                    new ColumnDefinition(4, GridUnitType.Pixel),
-                    new ColumnDefinition(3, GridUnitType.Star),
-                },
-                Children = new Controls
-                {
-                    (tree = new TreeView
-                    {
-                        DataTemplates = new DataTemplates
-                        {
-                            new FuncTreeDataTemplate<LogicalTreeNode>(GetHeader, x => x.Children),
-                        },
-                        [!ItemsControl.ItemsProperty] = this.WhenAnyValue(x => x.ViewModel.Nodes),
-                    }),
-                    new GridSplitter
-                    {
-                        Width = 4,
-                        Orientation = Orientation.Vertical,
-                        [Grid.ColumnProperty] = 1,
-                    },
-                    new ContentControl
-                    {
-                        [!ContentProperty] = this.WhenAnyValue(x => x.ViewModel.Details),
-                        [Grid.ColumnProperty] = 2,
-                    }
-                }
-            };
-
-            tree.GetObservable(TreeView.SelectedItemProperty)
-                .OfType<LogicalTreeNode>()
-                .Subscribe(x => ViewModel.SelectedNode = x);
-        }
-
-        private Control GetHeader(LogicalTreeNode node)
-        {
-            var result = new StackPanel
-            {
-                Orientation = Orientation.Horizontal,
-                Gap = 8,
-                Children = new Controls
-                {
-                    new TextBlock
-                    {
-                        Text = node.Type,
-                    },
-                    new TextBlock
-                    {
-                        [!TextBlock.TextProperty] = node.WhenAnyValue(x => x.Classes),
-                    }
-                }
-            };
-
-            result.PointerEnter += AddAdorner;
-            result.PointerLeave += RemoveAdorner;
-
-            return result;
-        }
-    }
-}

+ 0 - 43
src/Perspex.Diagnostics/Views/TreePage.cs

@@ -1,43 +0,0 @@
-// Copyright (c) The Perspex Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using Perspex.Controls;
-using Perspex.Controls.Primitives;
-using Perspex.Controls.Shapes;
-using Perspex.Diagnostics.ViewModels;
-using Perspex.Input;
-using Perspex.Media;
-
-namespace Perspex.Diagnostics.Views
-{
-    internal class TreePage : UserControl
-    {
-        private Control _adorner;
-
-        protected void AddAdorner(object sender, PointerEventArgs e)
-        {
-            var node = (TreeNode)((Control)sender).DataContext;
-            var layer = AdornerLayer.GetAdornerLayer(node.Control);
-
-            if (layer != null)
-            {
-                _adorner = new Rectangle
-                {
-                    Fill = new SolidColorBrush(0x80a0c5e8),
-                    [AdornerLayer.AdornedElementProperty] = node.Control,
-                };
-
-                layer.Children.Add(_adorner);
-            }
-        }
-
-        protected void RemoveAdorner(object sender, PointerEventArgs e)
-        {
-            if (_adorner != null)
-            {
-                ((Panel)_adorner.Parent).Children.Remove(_adorner);
-                _adorner = null;
-            }
-        }
-    }
-}

+ 70 - 0
src/Perspex.Diagnostics/Views/TreePage.paml.cs

@@ -0,0 +1,70 @@
+using Perspex.Controls;
+using Perspex.Controls.Generators;
+using Perspex.Controls.Primitives;
+using Perspex.Controls.Shapes;
+using Perspex.Diagnostics.ViewModels;
+using Perspex.Input;
+using Perspex.Markup.Xaml;
+using Perspex.Media;
+
+namespace Perspex.Diagnostics.Views
+{
+    public class TreePageView : UserControl
+    {
+        private Control _adorner;
+        private TreeView _tree;
+
+        public TreePageView()
+        {
+            this.InitializeComponent();
+            _tree.ItemContainerGenerator.Index.Materialized += TreeViewItemMaterialized;
+        }
+
+        protected void AddAdorner(object sender, PointerEventArgs e)
+        {
+            var node = (TreeNode)((Control)sender).DataContext;
+            var layer = AdornerLayer.GetAdornerLayer(node.Control);
+
+            if (layer != null)
+            {
+                _adorner = new Rectangle
+                {
+                    Fill = new SolidColorBrush(0x80a0c5e8),
+                    [AdornerLayer.AdornedElementProperty] = node.Control,
+                };
+
+                layer.Children.Add(_adorner);
+            }
+        }
+
+        protected void RemoveAdorner(object sender, PointerEventArgs e)
+        {
+            if (_adorner != null)
+            {
+                ((Panel)_adorner.Parent).Children.Remove(_adorner);
+                _adorner = null;
+            }
+        }
+
+        private void InitializeComponent()
+        {
+            PerspexXamlLoader.Load(this);
+            _tree = this.FindControl<TreeView>("tree");
+        }
+
+        private void TreeViewItemMaterialized(object sender, ItemContainerEventArgs e)
+        {
+            var item = (TreeViewItem)e.Containers[0].ContainerControl;
+            item.TemplateApplied += TreeViewItemTemplateApplied;
+        }
+
+        private void TreeViewItemTemplateApplied(object sender, TemplateAppliedEventArgs e)
+        {
+            var item = (TreeViewItem)sender;
+            var header = item.HeaderPresenter.Child;
+            header.PointerEnter += AddAdorner;
+            header.PointerLeave += RemoveAdorner;
+            item.TemplateApplied -= TreeViewItemTemplateApplied;
+        }
+    }
+}

+ 24 - 0
src/Perspex.Diagnostics/Views/TreePageView.paml

@@ -0,0 +1,24 @@
+<UserControl xmlns="https://github.com/perspex"
+             xmlns:vm="clr-namespace:Perspex.Diagnostics.ViewModels;assembly=Perspex.Diagnostics">
+  <Grid ColumnDefinitions="*,4,3*">    
+    <TreeView Name="tree" Items="{Binding Nodes}" SelectedItem="{Binding SelectedNode, Mode=TwoWay}">
+      <TreeView.DataTemplates>
+        <TreeDataTemplate DataType="vm:TreeNode"
+                          ItemsSource="{Binding Children}">
+          <StackPanel Orientation="Horizontal" Gap="8">
+            <TextBlock Text="{Binding Type}"/>
+            <TextBlock Text="{Binding Classes}"/>
+          </StackPanel>
+        </TreeDataTemplate>
+      </TreeView.DataTemplates>
+      <TreeView.Styles>
+        <Style Selector="TreeViewItem">
+          <Setter Property="IsExpanded" Value="{Binding IsExpanded, Mode=TwoWay}"/>
+        </Style>
+      </TreeView.Styles>
+    </TreeView>
+
+    <GridSplitter Width="4" Orientation="Vertical" Grid.Column="1"/>
+    <ContentControl Content="{Binding Details}" Grid.Column="2"/>
+  </Grid>
+</UserControl>

+ 0 - 101
src/Perspex.Diagnostics/Views/VisualTreeView.cs

@@ -1,101 +0,0 @@
-// Copyright (c) The Perspex Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
-
-using System;
-using System.Reactive.Linq;
-using Perspex.Controls;
-using Perspex.Controls.Templates;
-using Perspex.Diagnostics.ViewModels;
-using Perspex.Media;
-using ReactiveUI;
-
-namespace Perspex.Diagnostics.Views
-{
-    using Controls = Controls.Controls;
-
-    internal class VisualTreeView : TreePage
-    {
-        private static readonly PerspexProperty<VisualTreeViewModel> ViewModelProperty =
-            PerspexProperty.Register<VisualTreeView, VisualTreeViewModel>("ViewModel");
-
-        public VisualTreeView()
-        {
-            InitializeComponent();
-            this.GetObservable(DataContextProperty)
-                .Subscribe(x => ViewModel = (VisualTreeViewModel)x);
-        }
-
-        public VisualTreeViewModel ViewModel
-        {
-            get { return GetValue(ViewModelProperty); }
-            private set { SetValue(ViewModelProperty, value); }
-        }
-
-        private void InitializeComponent()
-        {
-            TreeView tree;
-
-            Content = new Grid
-            {
-                ColumnDefinitions = new ColumnDefinitions
-                {
-                    new ColumnDefinition(1, GridUnitType.Star),
-                    new ColumnDefinition(4, GridUnitType.Pixel),
-                    new ColumnDefinition(3, GridUnitType.Star),
-                },
-                Children = new Controls
-                {
-                    (tree = new TreeView
-                    {
-                        DataTemplates = new DataTemplates
-                        {
-                            new FuncTreeDataTemplate<VisualTreeNode>(GetHeader, x => x.Children),
-                        },
-                        [!ItemsControl.ItemsProperty] = this.WhenAnyValue(x => x.ViewModel.Nodes),
-                    }),
-                    new GridSplitter
-                    {
-                        Width = 4,
-                        Orientation = Orientation.Vertical,
-                        [Grid.ColumnProperty] = 1,
-                    },
-                    new ContentControl
-                    {
-                        [!ContentProperty] = this.WhenAnyValue(x => x.ViewModel.Details),
-                        [Grid.ColumnProperty] = 2,
-                    }
-                }
-            };
-
-            tree.GetObservable(TreeView.SelectedItemProperty)
-                .OfType<VisualTreeNode>()
-                .Subscribe(x => ViewModel.SelectedNode = x);
-        }
-
-        private Control GetHeader(VisualTreeNode node)
-        {
-            var result = new StackPanel
-            {
-                Orientation = Orientation.Horizontal,
-                Gap = 8,
-                Children = new Controls
-                {
-                    new TextBlock
-                    {
-                        FontStyle = node.IsInTemplate ? FontStyle.Italic : FontStyle.Normal,
-                        Text = node.Type,
-                    },
-                    new TextBlock
-                    {
-                        [!TextBlock.TextProperty] = node.WhenAnyValue(x => x.Classes),
-                    }
-                }
-            };
-
-            result.PointerEnter += AddAdorner;
-            result.PointerLeave += RemoveAdorner;
-
-            return result;
-        }
-    }
-}

+ 40 - 0
src/Perspex.SceneGraph/Rect.cs

@@ -402,6 +402,46 @@ namespace Perspex
             return new Rect(Position + offset, Size);
         }
 
+        /// <summary>
+        /// Returns a new <see cref="Rect"/> with the specified X position.
+        /// </summary>
+        /// <param name="x">The x position.</param>
+        /// <returns>The new <see cref="Rect"/>.</returns>
+        public Rect WithX(double x)
+        {
+            return new Rect(x, _y, _width, _height);
+        }
+
+        /// <summary>
+        /// Returns a new <see cref="Rect"/> with the specified Y position.
+        /// </summary>
+        /// <param name="y">The y position.</param>
+        /// <returns>The new <see cref="Rect"/>.</returns>
+        public Rect WithY(double y)
+        {
+            return new Rect(_x, y, _width, _height);
+        }
+
+        /// <summary>
+        /// Returns a new <see cref="Rect"/> with the specified width.
+        /// </summary>
+        /// <param name="width">The width.</param>
+        /// <returns>The new <see cref="Rect"/>.</returns>
+        public Rect WithWidth(double width)
+        {
+            return new Rect(_x, _y, width, _height);
+        }
+
+        /// <summary>
+        /// Returns a new <see cref="Rect"/> with the specified height.
+        /// </summary>
+        /// <param name="height">The height.</param>
+        /// <returns>The new <see cref="Rect"/>.</returns>
+        public Rect WithHeight(double height)
+        {
+            return new Rect(_x, _y, _width, height);
+        }
+
         /// <summary>
         /// Returns the string representation of the rectangle.
         /// </summary>

+ 2 - 14
src/Skia/Perspex.Skia/DrawingContextImpl.cs

@@ -37,18 +37,13 @@ namespace Perspex.Skia
         public void DrawGeometry(Brush brush, Pen pen, Geometry geometry)
         {
             var impl = ((StreamGeometryImpl) geometry.PlatformImpl);
-            var oldTransform = Transform;
-            if (!impl.Transform.IsIdentity)
-                Transform = impl.Transform*Transform;
-            
             var size = geometry.Bounds.Size;
             using(var fill = brush!=null?CreateBrush(brush, size):null)
             using (var stroke = pen?.Brush != null ? CreateBrush(pen, size) : null)
             {
-                MethodTable.Instance.DrawGeometry(Handle, impl.Path.Handle, fill != null ? fill.Brush : null,
+                MethodTable.Instance.DrawGeometry(Handle, impl.EffectivePath, fill != null ? fill.Brush : null,
                     stroke != null ? stroke.Brush : null, impl.FillRule == FillRule.EvenOdd);
             }
-            Transform = oldTransform;
         }
 
         unsafe NativeBrushContainer CreateBrush(Brush brush, Size targetSize)
@@ -194,14 +189,7 @@ namespace Perspex.Skia
                 if(_currentTransform == value)
                     return;
                 _currentTransform = value;
-                _fmatrix[0] = (float)value.M11;
-                _fmatrix[1] = (float)value.M21;
-                _fmatrix[2] = (float)value.M31;
-
-                _fmatrix[3] = (float)value.M12;
-                _fmatrix[4] = (float)value.M22;
-                _fmatrix[5] = (float)value.M32;
-                MethodTable.Instance.SetTransform(Handle, _fmatrix);
+                MethodTable.Instance.SetTransform(Handle, value);
             } 
         }
     }

+ 32 - 3
src/Skia/Perspex.Skia/MethodTable.cs

@@ -47,9 +47,9 @@ namespace Perspex.Skia
         public _PopClip PopClip;
 
         [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
-        public delegate void _SetTransform(IntPtr ctx, float[] matrix6);
+        public delegate void _SetTransform(IntPtr ctx, void* matrix6);
 
-        public _SetTransform SetTransform;
+        public _SetTransform SetTransformNative;
 
         [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
         public delegate void _DrawLine(IntPtr ctx, void* brush, float x1, float y1, float x2, float y2);
@@ -66,6 +66,11 @@ namespace Perspex.Skia
 
         public _DisposePath DisposePath;
 
+        [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
+        public delegate IntPtr _TransformPath(IntPtr path, void* matrix6);
+
+        public _TransformPath TransformPathNative;
+
         [UnmanagedFunctionPointer(CallingConvention.Cdecl)]
         public delegate void _DrawGeometry(IntPtr ctx, IntPtr path, void* fill, void* stroke, bool useEvenOdd);
 
@@ -185,10 +190,34 @@ namespace Perspex.Skia
             typeof (_RebuildFormattedText),
             typeof (_DestroyFormattedText),
             typeof (_DrawFormattedText),
-            typeof (_SetOption)
+            typeof (_SetOption),
+            typeof (_TransformPath)
         };
 
+        void ConvertMatrix(Matrix value, float* target)
+        {
+            target[0] = (float)value.M11;
+            target[1] = (float)value.M21;
+            target[2] = (float)value.M31;
+
+            target[3] = (float)value.M12;
+            target[4] = (float)value.M22;
+            target[5] = (float)value.M32;
+        }
 
+        public unsafe IntPtr TransformPath(IntPtr path, Matrix matrix)
+        {
+            var tmp = stackalloc float[6];
+            ConvertMatrix(matrix, tmp);
+            return TransformPathNative(path, tmp);
+        }
+
+        public unsafe void SetTransform(IntPtr ctx, Matrix matrix)
+        {
+            var tmp = stackalloc float[6];
+            ConvertMatrix(matrix, tmp);
+            SetTransformNative(ctx, tmp);
+        }
 
         protected MethodTable(IntPtr methodTable)
         {

+ 59 - 0
src/Skia/Perspex.Skia/PerspexHandleHolder.cs

@@ -44,4 +44,63 @@ namespace Perspex.Skia
             Dispose();
         }
     }
+
+    class RefCountable<T> : IDisposable where T : PerspexHandleHolder
+    {
+        class Shared
+        {
+            public readonly T Target;
+            private int _refCount = 1;
+
+            public Shared(T target)
+            {
+                Target = target;
+            }
+
+            public void AddRef() => _refCount++;
+            public void Release()
+            {
+                _refCount--;
+                if (_refCount <= 0)
+                    Target.Dispose();
+            }
+        }
+
+        public bool IsDisposed => _shared == null;
+        private Shared _shared;
+        public void CheckDisposed()
+        {
+            if (IsDisposed)
+                throw new ObjectDisposedException(GetType().FullName);
+        }
+
+        public IntPtr Handle
+        {
+            get
+            {
+                CheckDisposed();
+                return _shared.Target.Handle;
+            }
+        }
+
+        public RefCountable(T handle)
+        {
+            _shared = new Shared(handle);
+        }
+
+        public RefCountable(RefCountable<T> other)
+        {
+            other._shared.Target.CheckDisposed();
+            other._shared.AddRef();
+            _shared = other._shared;
+        }
+
+        public RefCountable<T> Clone() => new RefCountable<T>(this);
+
+        public void Dispose()
+        {
+            _shared?.Release();
+            _shared = null;
+        }
+    }
 }

+ 36 - 4
src/Skia/Perspex.Skia/StreamGeometryImpl.cs

@@ -41,7 +41,11 @@ namespace Perspex.Skia
 
     class StreamGeometryImpl : IStreamGeometryImpl
     {
-        public SkPath Path;
+        RefCountable<SkPath> _path;
+        RefCountable<SkPath> _transformedPath;
+        private Matrix _transform = Matrix.Identity;
+        
+        public IntPtr EffectivePath => (_transformedPath ?? _path).Handle;
 
         public Rect GetRenderBounds(double strokeThickness)
         {
@@ -51,11 +55,35 @@ namespace Perspex.Skia
 
         public Rect Bounds { get; private set; }
 
-        public Matrix Transform { get; set; } = Matrix.Identity;
+        public Matrix Transform
+        {
+            get { return _transform; }
+            set
+            {
+                if(_transform == value)
+                    return;
+                _transform = value;
+                ApplyTransform();
+            }
+        }
+
+        void ApplyTransform()
+        {
+            if(_path == null)
+                return;
+            if (_transformedPath != null)
+            {
+                _transformedPath.Dispose();
+                _transformedPath = null;
+            }
+            if (!_transform.IsIdentity)
+                _transformedPath =
+                    new RefCountable<SkPath>(new SkPath(MethodTable.Instance.TransformPath(_path.Handle, Transform)));
+        }
 
         public IStreamGeometryImpl Clone()
         {
-            return new StreamGeometryImpl() {Path = Path, Transform = Transform, Bounds = Bounds};
+            return new StreamGeometryImpl {_path = _path?.Clone(), _transformedPath = _transformedPath?.Clone(), _transform = Transform, Bounds = Bounds};
         }
 
         public IStreamGeometryContextImpl Open()
@@ -77,7 +105,11 @@ namespace Perspex.Skia
             {
                 var arr = _elements.ToArray();
                 SkRect rc;
-                _geometryImpl.Path = new SkPath(MethodTable.Instance.CreatePath(arr, arr.Length, out rc));
+                _geometryImpl._path?.Dispose();
+                _geometryImpl._path =
+                    new RefCountable<SkPath>(new SkPath(MethodTable.Instance.CreatePath(arr, arr.Length, out rc)));
+                _geometryImpl.ApplyTransform();
+
                 _geometryImpl.Bounds = rc.ToRect();
 
             }

+ 8 - 2
src/Skia/getnatives.sh

@@ -1,7 +1,13 @@
 #!/bin/sh
-rm -rf native
+rm -rf native native.zip
 mkdir -p native
 cd native
+if which curl
+then
+curl `cat ../native.url` -o native.zip
+else
 wget `cat ../native.url` -O native.zip
-unzip native.zip
+fi
 
+unzip native.zip
+chmod -R +x .

+ 62 - 0
tests/Perspex.Controls.UnitTests/DockPanelTests.cs

@@ -0,0 +1,62 @@
+// Copyright (c) The Perspex Project. All rights reserved.
+// Licensed under the MIT license. See licence.md file in the project root for full license information.
+
+using Xunit;
+
+namespace Perspex.Controls.UnitTests
+{
+    public class DockPanelTests
+    {
+        [Fact]
+        public void Should_Dock_Controls_Horizontal_First()
+        {
+            var target = new DockPanel
+            {
+                Children = new Controls
+                {
+                    new Border { Width = 500, Height = 50, [DockPanel.DockProperty] = Dock.Top },
+                    new Border { Width = 500, Height = 50, [DockPanel.DockProperty] = Dock.Bottom },
+                    new Border { Width = 50, Height = 400, [DockPanel.DockProperty] = Dock.Left },
+                    new Border { Width = 50, Height = 400, [DockPanel.DockProperty] = Dock.Right },
+                    new Border { },
+                }
+            };
+
+            target.Measure(Size.Infinity);
+            target.Arrange(new Rect(target.DesiredSize));
+
+            Assert.Equal(new Rect(0, 0, 500, 500), target.Bounds);
+            Assert.Equal(new Rect(0, 0, 500, 50), target.Children[0].Bounds);
+            Assert.Equal(new Rect(0, 450, 500, 50), target.Children[1].Bounds);
+            Assert.Equal(new Rect(0, 50, 50, 400), target.Children[2].Bounds);
+            Assert.Equal(new Rect(450, 50, 50, 400), target.Children[3].Bounds);
+            Assert.Equal(new Rect(50, 50, 400, 400), target.Children[4].Bounds);
+        }
+
+        [Fact]
+        public void Should_Dock_Controls_Vertical_First()
+        {
+            var target = new DockPanel
+            {
+                Children = new Controls
+                {
+                    new Border { Width = 50, Height = 400, [DockPanel.DockProperty] = Dock.Left },
+                    new Border { Width = 50, Height = 400, [DockPanel.DockProperty] = Dock.Right },
+                    new Border { Width = 500, Height = 50, [DockPanel.DockProperty] = Dock.Top },
+                    new Border { Width = 500, Height = 50, [DockPanel.DockProperty] = Dock.Bottom },
+                    new Border { },
+                }
+            };
+
+            target.Measure(Size.Infinity);
+            target.Arrange(new Rect(target.DesiredSize));
+
+            Assert.Equal(new Rect(0, 0, 600, 400), target.Bounds);
+            Assert.Equal(new Rect(0, 0, 50, 400), target.Children[0].Bounds);
+            Assert.Equal(new Rect(550, 0, 50, 400), target.Children[1].Bounds);
+            Assert.Equal(new Rect(50, 0, 500, 50), target.Children[2].Bounds);
+            Assert.Equal(new Rect(50, 350, 500, 50), target.Children[3].Bounds);
+            Assert.Equal(new Rect(50, 50, 500, 300), target.Children[4].Bounds);
+        }
+    }
+}

+ 1 - 0
tests/Perspex.Controls.UnitTests/Perspex.Controls.UnitTests.csproj

@@ -82,6 +82,7 @@
   </Choose>
   <ItemGroup>
     <Compile Include="ClassesTests.cs" />
+    <Compile Include="DockPanelTests.cs" />
     <Compile Include="EnumerableExtensions.cs" />
     <Compile Include="GridSplitterTests.cs" />
     <Compile Include="GridTests.cs" />

+ 0 - 12
tests/Perspex.RenderTests/Shapes/PathTests.cs

@@ -46,11 +46,7 @@ namespace Perspex.Direct2D1.RenderTests.Shapes
             CompareImages();
         }
 
-#if PERSPEX_SKIA
-        [Fact(Skip = "FIXME")]
-#else
         [Fact]
-#endif
         public void Path_Tick_Scaled()
         {
             Decorator target = new Decorator
@@ -73,11 +69,7 @@ namespace Perspex.Direct2D1.RenderTests.Shapes
             CompareImages();
         }
 
-#if PERSPEX_SKIA
-        [Fact(Skip = "FIXME")]
-#else
         [Fact]
-#endif
         public void Path_Tick_Scaled_Stroke_8px()
         {
             Decorator target = new Decorator
@@ -100,11 +92,7 @@ namespace Perspex.Direct2D1.RenderTests.Shapes
             CompareImages();
         }
 
-#if PERSPEX_SKIA
-        [Fact(Skip = "FIXME")]
-#else
         [Fact]
-#endif
         public void Path_Expander_With_Border()
         {
             Decorator target = new Decorator