Browse Source

Port GridSplitter from WPF.

Dariusz Komosinski 6 years ago
parent
commit
00263f71e4

+ 755 - 117
src/Avalonia.Controls/GridSplitter.cs

@@ -1,210 +1,848 @@
-// Copyright (c) The Avalonia Project. All rights reserved.
-// Licensed under the MIT license. See licence.md file in the project root for full license information.
+// This source file is adapted from the Windows Presentation Foundation project. 
+// (https://github.com/dotnet/wpf/) 
+// 
+// Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation.
 
 using System;
-using System.Collections.Generic;
-using System.Linq;
+using System.Diagnostics;
+using Avalonia.Collections;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
+using Avalonia.Interactivity;
 using Avalonia.Layout;
-using Avalonia.VisualTree;
+using Avalonia.Media;
+using Avalonia.Utilities;
 
 namespace Avalonia.Controls
 {
     /// <summary>
-    ///     Represents the control that redistributes space between columns or rows of a Grid control.
+    /// Enum to indicate whether GridSplitter resizes Columns or Rows.
+    /// </summary>
+    public enum GridResizeDirection
+    {
+        /// <summary>
+        /// Determines whether to resize rows or columns based on its Alignment and 
+        /// width compared to height.
+        /// </summary>
+        Auto,
+
+        /// <summary>
+        /// Resize columns when dragging Splitter.
+        /// </summary>
+        Columns,
+
+        /// <summary>
+        /// Resize rows when dragging Splitter.
+        /// </summary>
+        Rows
+    }
+
+    /// <summary>
+    /// Enum to indicate what Columns or Rows the GridSplitter resizes.
+    /// </summary>
+    public enum GridResizeBehavior
+    {
+        /// <summary>
+        /// Determine which columns or rows to resize based on its Alignment.
+        /// </summary>
+        BasedOnAlignment,
+
+        /// <summary>
+        /// Resize the current and next Columns or Rows.
+        /// </summary>
+        CurrentAndNext,
+
+        /// <summary>
+        /// Resize the previous and current Columns or Rows.
+        /// </summary>
+        PreviousAndCurrent,
+
+        /// <summary>
+        /// Resize the previous and next Columns or Rows.
+        /// </summary>
+        PreviousAndNext
+    }
+
+    /// <summary>
+    /// Represents the control that redistributes space between columns or rows of a Grid control.
     /// </summary>
-    /// <remarks>
-    ///     Unlike WPF GridSplitter, Avalonia GridSplitter has only one Behavior, GridResizeBehavior.PreviousAndNext.
-    /// </remarks>
     public class GridSplitter : Thumb
     {
-        private List<DefinitionBase> _definitions;
+        /// <summary>
+        /// Defines the <see cref="ResizeDirection"/> property.
+        /// </summary>
+        public static readonly AvaloniaProperty<GridResizeDirection> ResizeDirectionProperty =
+            AvaloniaProperty.Register<GridSplitter, GridResizeDirection>(nameof(ResizeDirection));
 
-        private Grid _grid;
+        /// <summary>
+        /// Defines the <see cref="ResizeBehavior"/> property.
+        /// </summary>
+        public static readonly AvaloniaProperty<GridResizeBehavior> ResizeBehaviorProperty =
+            AvaloniaProperty.Register<GridSplitter, GridResizeBehavior>(nameof(ResizeBehavior));
 
-        private DefinitionBase _nextDefinition;
+        /// <summary>
+        /// Defines the <see cref="ShowsPreview"/> property.
+        /// </summary>
+        public static readonly AvaloniaProperty<bool> ShowsPreviewProperty =
+            AvaloniaProperty.Register<GridSplitter, bool>(nameof(ShowsPreview));
 
-        private Orientation _orientation;
+        /// <summary>
+        /// Defines the <see cref="KeyboardIncrement"/> property.
+        /// </summary>
+        public static readonly AvaloniaProperty<double> KeyboardIncrementProperty =
+            AvaloniaProperty.Register<GridSplitter, double>(nameof(KeyboardIncrement), 10d);
 
-        private DefinitionBase _prevDefinition;
+        /// <summary>
+        /// Defines the <see cref="DragIncrement"/> property.
+        /// </summary>
+        public static readonly AvaloniaProperty<double> DragIncrementProperty =
+            AvaloniaProperty.Register<GridSplitter, double>(nameof(DragIncrement), 1d);
 
-        private void GetDeltaConstraints(out double min, out double max)
+        /// <summary>
+        /// Defines the <see cref="PreviewContent"/> property.
+        /// </summary>
+        public static readonly AvaloniaProperty<ITemplate<IControl>> PreviewContentProperty =
+            AvaloniaProperty.Register<GridSplitter, ITemplate<IControl>>(nameof(PreviewContent));
+
+        private static readonly Cursor s_columnSplitterCursor = new Cursor(StandardCursorType.SizeWestEast);
+        private static readonly Cursor s_rowSplitterCursor = new Cursor(StandardCursorType.SizeNorthSouth);
+
+        private ResizeData _resizeData;
+
+        /// <summary>
+        /// Indicates whether the Splitter resizes the Columns, Rows, or Both.
+        /// </summary>
+        public GridResizeDirection ResizeDirection
         {
-            var prevDefinitionLen = GetActualLength(_prevDefinition);
-            var prevDefinitionMin = GetMinLength(_prevDefinition);
-            var prevDefinitionMax = GetMaxLength(_prevDefinition);
+            get => GetValue(ResizeDirectionProperty);
+            set => SetValue(ResizeDirectionProperty, value);
+        }
 
-            var nextDefinitionLen = GetActualLength(_nextDefinition);
-            var nextDefinitionMin = GetMinLength(_nextDefinition);
-            var nextDefinitionMax = GetMaxLength(_nextDefinition);
-            // Determine the minimum and maximum the columns can be resized
-            min = -Math.Min(prevDefinitionLen - prevDefinitionMin, nextDefinitionMax - nextDefinitionLen);
-            max = Math.Min(prevDefinitionMax - prevDefinitionLen, nextDefinitionLen - nextDefinitionMin);
+        /// <summary>
+        /// Indicates which Columns or Rows the Splitter resizes.
+        /// </summary>
+        public GridResizeBehavior ResizeBehavior
+        {
+            get => GetValue(ResizeBehaviorProperty);
+            set => SetValue(ResizeBehaviorProperty, value);
         }
 
-        protected override void OnDragDelta(VectorEventArgs e)
+        /// <summary>
+        /// Indicates whether to Preview the column resizing without updating layout.
+        /// </summary>
+        public bool ShowsPreview
+        {
+            get => GetValue(ShowsPreviewProperty);
+            set => SetValue(ShowsPreviewProperty, value);
+        }
+
+        /// <summary>
+        /// The Distance to move the splitter when pressing the keyboard arrow keys.
+        /// </summary>
+        public double KeyboardIncrement
+        {
+            get => GetValue(KeyboardIncrementProperty);
+            set => SetValue(KeyboardIncrementProperty, value);
+        }
+
+        /// <summary>
+        /// Restricts splitter to move a multiple of the specified units.
+        /// </summary>
+        public double DragIncrement
+        {
+            get => GetValue(DragIncrementProperty);
+            set => SetValue(DragIncrementProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets content that will be shown when <see cref="ShowsPreview"/> is enabled and user starts resize operation.
+        /// </summary>
+        public ITemplate<IControl> PreviewContent
         {
-            // WPF doesn't change anything when spliter is in the last row/column
-            // but resizes the splitter row/column when it's the first one.
-            // this is different, but more internally consistent.
-            if (_prevDefinition == null || _nextDefinition == null)
-                return;
+            get => GetValue(PreviewContentProperty);
+            set => SetValue(PreviewContentProperty, value);
+        }
 
-            var delta = _orientation == Orientation.Vertical ? e.Vector.X : e.Vector.Y;
-            double max;
-            double min;
-            GetDeltaConstraints(out min, out max);
-            delta = Math.Min(Math.Max(delta, min), max);
+        /// <summary>
+        /// Converts BasedOnAlignment direction to Rows, Columns, or Both depending on its width/height.
+        /// </summary>
+        internal GridResizeDirection GetEffectiveResizeDirection()
+        {
+            GridResizeDirection direction = ResizeDirection;
 
-            var prevIsStar = IsStar(_prevDefinition);
-            var nextIsStar = IsStar(_nextDefinition);
+            if (direction != GridResizeDirection.Auto)
+            {
+                return direction;
+            }
 
-            if (prevIsStar && nextIsStar)
+            // When HorizontalAlignment is Left, Right or Center, resize Columns.
+            if (HorizontalAlignment != HorizontalAlignment.Stretch)
+            {
+                direction = GridResizeDirection.Columns;
+            }
+            else if (VerticalAlignment != VerticalAlignment.Stretch)
             {
-                foreach (var definition in _definitions)
+                direction = GridResizeDirection.Rows;
+            }
+            else if (Bounds.Width <= Bounds.Height) // Fall back to Width vs Height.
+            {
+                direction = GridResizeDirection.Columns;
+            }
+            else
+            {
+                direction = GridResizeDirection.Rows;
+            }
+
+            return direction;
+        }
+
+        /// <summary>
+        /// Convert BasedOnAlignment to Next/Prev/Both depending on alignment and Direction.
+        /// </summary>
+        private GridResizeBehavior GetEffectiveResizeBehavior(GridResizeDirection direction)
+        {
+            GridResizeBehavior resizeBehavior = ResizeBehavior;
+
+            if (resizeBehavior == GridResizeBehavior.BasedOnAlignment)
+            {
+                if (direction == GridResizeDirection.Columns)
                 {
-                    if (definition == _prevDefinition)
+                    switch (HorizontalAlignment)
                     {
-                        SetLengthInStars(_prevDefinition, GetActualLength(_prevDefinition) + delta);
+                        case HorizontalAlignment.Left:
+                            resizeBehavior = GridResizeBehavior.PreviousAndCurrent;
+                            break;
+                        case HorizontalAlignment.Right:
+                            resizeBehavior = GridResizeBehavior.CurrentAndNext;
+                            break;
+                        default:
+                            resizeBehavior = GridResizeBehavior.PreviousAndNext;
+                            break;
                     }
-                    else if (definition == _nextDefinition)
+                }
+                else
+                {
+                    switch (VerticalAlignment)
                     {
-                        SetLengthInStars(_nextDefinition, GetActualLength(_nextDefinition) - delta);
+                        case VerticalAlignment.Top:
+                            resizeBehavior = GridResizeBehavior.PreviousAndCurrent;
+                            break;
+                        case VerticalAlignment.Bottom:
+                            resizeBehavior = GridResizeBehavior.CurrentAndNext;
+                            break;
+                        default:
+                            resizeBehavior = GridResizeBehavior.PreviousAndNext;
+                            break;
                     }
-                    else if (IsStar(definition))
+                }
+            }
+
+            return resizeBehavior;
+        }
+
+        /// <summary>
+        /// Removes preview adorner from the grid.
+        /// </summary>
+        private void RemovePreviewAdorner()
+        {
+            if (_resizeData.Adorner != null)
+            {
+                AdornerLayer layer = AdornerLayer.GetAdornerLayer(this);
+                layer.Children.Remove(_resizeData.Adorner);
+            }
+        }
+
+        /// <summary>
+        /// Initialize the data needed for resizing.
+        /// </summary>
+        private void InitializeData(bool showsPreview)
+        {
+            // If not in a grid or can't resize, do nothing.
+            if (Parent is Grid grid)
+            {
+                GridResizeDirection resizeDirection = GetEffectiveResizeDirection();
+
+                // Setup data used for resizing.
+                _resizeData = new ResizeData
+                {
+                    Grid = grid,
+                    ShowsPreview = showsPreview,
+                    ResizeDirection = resizeDirection,
+                    SplitterLength = Math.Min(Bounds.Width, Bounds.Height),
+                    ResizeBehavior = GetEffectiveResizeBehavior(resizeDirection)
+                };
+
+                // Store the rows and columns to resize on drag events.
+                if (!SetupDefinitionsToResize())
+                {
+                    // Unable to resize, clear data.
+                    _resizeData = null;
+                    return;
+                }
+
+                // Setup the preview in the adorner if ShowsPreview is true.
+                SetupPreviewAdorner();
+            }
+        }
+
+        /// <summary>
+        /// Returns true if GridSplitter can resize rows/columns.
+        /// </summary>
+        private bool SetupDefinitionsToResize()
+        {
+            int gridSpan = GetValue(_resizeData.ResizeDirection == GridResizeDirection.Columns ?
+                Grid.ColumnSpanProperty :
+                Grid.RowSpanProperty);
+
+            if (gridSpan == 1)
+            {
+                var splitterIndex = GetValue(_resizeData.ResizeDirection == GridResizeDirection.Columns ?
+                    Grid.ColumnProperty :
+                    Grid.RowProperty);
+
+                // Select the columns based on behavior.
+                int index1, index2;
+
+                switch (_resizeData.ResizeBehavior)
+                {
+                    case GridResizeBehavior.PreviousAndCurrent:
+                        // Get current and previous.
+                        index1 = splitterIndex - 1;
+                        index2 = splitterIndex;
+                        break;
+                    case GridResizeBehavior.CurrentAndNext:
+                        // Get current and next.
+                        index1 = splitterIndex;
+                        index2 = splitterIndex + 1;
+                        break;
+                    default: // GridResizeBehavior.PreviousAndNext.
+                        // Get previous and next.
+                        index1 = splitterIndex - 1;
+                        index2 = splitterIndex + 1;
+                        break;
+                }
+
+                // Get count of rows/columns in the resize direction.
+                int count = _resizeData.ResizeDirection == GridResizeDirection.Columns ?
+                    _resizeData.Grid.ColumnDefinitions.Count :
+                    _resizeData.Grid.RowDefinitions.Count;
+
+                if (index1 >= 0 && index2 < count)
+                {
+                    _resizeData.SplitterIndex = splitterIndex;
+
+                    _resizeData.Definition1Index = index1;
+                    _resizeData.Definition1 = GetGridDefinition(_resizeData.Grid, index1, _resizeData.ResizeDirection);
+                    _resizeData.OriginalDefinition1Length =
+                        _resizeData.Definition1.UserSizeValueCache; // Save Size if user cancels.
+                    _resizeData.OriginalDefinition1ActualLength = GetActualLength(_resizeData.Definition1);
+
+                    _resizeData.Definition2Index = index2;
+                    _resizeData.Definition2 = GetGridDefinition(_resizeData.Grid, index2, _resizeData.ResizeDirection);
+                    _resizeData.OriginalDefinition2Length =
+                        _resizeData.Definition2.UserSizeValueCache; // Save Size if user cancels.
+                    _resizeData.OriginalDefinition2ActualLength = GetActualLength(_resizeData.Definition2);
+
+                    // Determine how to resize the columns.
+                    bool isStar1 = IsStar(_resizeData.Definition1);
+                    bool isStar2 = IsStar(_resizeData.Definition2);
+
+                    if (isStar1 && isStar2)
                     {
-                        SetLengthInStars(definition, GetActualLength(definition)); // same size but in stars.
+                        // If they are both stars, resize both.
+                        _resizeData.SplitBehavior = SplitBehavior.Split;
                     }
+                    else
+                    {
+                        // One column is fixed width, resize the first one that is fixed.
+                        _resizeData.SplitBehavior = !isStar1 ? SplitBehavior.Resize1 : SplitBehavior.Resize2;
+                    }
+
+                    return true;
                 }
             }
-            else if (prevIsStar)
+
+            return false;
+        }
+
+        /// <summary>
+        /// Create the preview adorner and add it to the adorner layer.
+        /// </summary>
+        private void SetupPreviewAdorner()
+        {
+            if (_resizeData.ShowsPreview)
             {
-                SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta);
+                // Get the adorner layer and add an adorner to it.
+                var adornerLayer = AdornerLayer.GetAdornerLayer(_resizeData.Grid);
+
+                var previewContent = PreviewContent;
+
+                // Can't display preview.
+                if (adornerLayer == null)
+                {
+                    return;
+                }
+
+                IControl builtPreviewContent = previewContent?.Build();
+
+                _resizeData.Adorner = new PreviewAdorner(builtPreviewContent);
+
+                AdornerLayer.SetAdornedElement(_resizeData.Adorner, this);
+
+                adornerLayer.Children.Add(_resizeData.Adorner);
+
+                // Get constraints on preview's translation.
+                GetDeltaConstraints(out _resizeData.MinChange, out _resizeData.MaxChange);
             }
-            else if (nextIsStar)
+        }
+
+        protected override void OnPointerEnter(PointerEventArgs e)
+        {
+            base.OnPointerEnter(e);
+
+            GridResizeDirection direction = GetEffectiveResizeDirection();
+
+            switch (direction)
             {
-                SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta);
+                case GridResizeDirection.Columns:
+                    Cursor = s_columnSplitterCursor;
+                    break;
+                case GridResizeDirection.Rows:
+                    Cursor = s_rowSplitterCursor;
+                    break;
             }
-            else
+        }
+
+        protected override void OnLostFocus(RoutedEventArgs e)
+        {
+            base.OnLostFocus(e);
+
+            if (_resizeData != null)
             {
-                SetLength(_prevDefinition, GetActualLength(_prevDefinition) + delta);
-                SetLength(_nextDefinition, GetActualLength(_nextDefinition) - delta);
+                CancelResize();
             }
         }
 
-        private double GetActualLength(DefinitionBase definition)
+        protected override void OnDragStarted(VectorEventArgs e)
         {
-            if (definition == null)
-                return 0;
-            var columnDefinition = definition as ColumnDefinition;
-            return columnDefinition?.ActualWidth ?? ((RowDefinition)definition).ActualHeight;
+            base.OnDragStarted(e);
+
+            Debug.Assert(_resizeData == null, "_resizeData is not null, DragCompleted was not called");
+
+            InitializeData(ShowsPreview);
         }
 
-        private double GetMinLength(DefinitionBase definition)
+        protected override void OnDragDelta(VectorEventArgs e)
         {
-            if (definition == null)
-                return 0;
-            var columnDefinition = definition as ColumnDefinition;
-            return columnDefinition?.MinWidth ?? ((RowDefinition)definition).MinHeight;
+            base.OnDragDelta(e);
+
+            if (_resizeData != null)
+            {
+                double horizontalChange = e.Vector.X;
+                double verticalChange = e.Vector.Y;
+
+                // Round change to nearest multiple of DragIncrement.
+                double dragIncrement = DragIncrement;
+                horizontalChange = Math.Round(horizontalChange / dragIncrement) * dragIncrement;
+                verticalChange = Math.Round(verticalChange / dragIncrement) * dragIncrement;
+
+                if (_resizeData.ShowsPreview)
+                {
+                    // Set the Translation of the Adorner to the distance from the thumb.
+                    if (_resizeData.ResizeDirection == GridResizeDirection.Columns)
+                    {
+                        _resizeData.Adorner.OffsetX = Math.Min(
+                            Math.Max(horizontalChange, _resizeData.MinChange),
+                            _resizeData.MaxChange);
+                    }
+                    else
+                    {
+                        _resizeData.Adorner.OffsetY = Math.Min(
+                            Math.Max(verticalChange, _resizeData.MinChange),
+                            _resizeData.MaxChange);
+                    }
+                }
+                else
+                {
+                    // Directly update the grid.
+                    MoveSplitter(horizontalChange, verticalChange);
+                }
+            }
         }
 
-        private double GetMaxLength(DefinitionBase definition)
+        protected override void OnDragCompleted(VectorEventArgs e)
         {
-            if (definition == null)
-                return 0;
-            var columnDefinition = definition as ColumnDefinition;
-            return columnDefinition?.MaxWidth ?? ((RowDefinition)definition).MaxHeight;
+            base.OnDragCompleted(e);
+
+            if (_resizeData != null)
+            {
+                if (_resizeData.ShowsPreview)
+                {
+                    // Update the grid.
+                    MoveSplitter(_resizeData.Adorner.OffsetX, _resizeData.Adorner.OffsetY);
+                    RemovePreviewAdorner();
+                }
+
+                _resizeData = null;
+            }
         }
 
-        private bool IsStar(DefinitionBase definition)
+        protected override void OnKeyDown(KeyEventArgs e)
         {
-            var columnDefinition = definition as ColumnDefinition;
-            return columnDefinition?.Width.IsStar ?? ((RowDefinition)definition).Height.IsStar;
+            Key key = e.Key;
+
+            switch (key)
+            {
+                case Key.Escape:
+                    if (_resizeData != null)
+                    {
+                        CancelResize();
+                        e.Handled = true;
+                    }
+
+                    break;
+
+                case Key.Left:
+                    e.Handled = KeyboardMoveSplitter(-KeyboardIncrement, 0);
+                    break;
+                case Key.Right:
+                    e.Handled = KeyboardMoveSplitter(KeyboardIncrement, 0);
+                    break;
+                case Key.Up:
+                    e.Handled = KeyboardMoveSplitter(0, -KeyboardIncrement);
+                    break;
+                case Key.Down:
+                    e.Handled = KeyboardMoveSplitter(0, KeyboardIncrement);
+                    break;
+            }
         }
 
-        private void SetLengthInStars(DefinitionBase definition, double value)
+        /// <summary>
+        /// Cancels the resize operation.
+        /// </summary>
+        private void CancelResize()
         {
-            var columnDefinition = definition as ColumnDefinition;
-            if (columnDefinition != null)
+            // Restore original column/row lengths.
+            if (_resizeData.ShowsPreview)
             {
-                columnDefinition.Width = new GridLength(value, GridUnitType.Star);
+                RemovePreviewAdorner();
             }
-            else
+            else // Reset the columns/rows lengths to the saved values.
             {
-                ((RowDefinition)definition).Height = new GridLength(value, GridUnitType.Star);
+                SetDefinitionLength(_resizeData.Definition1, _resizeData.OriginalDefinition1Length);
+                SetDefinitionLength(_resizeData.Definition2, _resizeData.OriginalDefinition2Length);
             }
+
+            _resizeData = null;
         }
 
-        private void SetLength(DefinitionBase definition, double value)
+        /// <summary>
+        /// Returns true if the row/column has a star length.
+        /// </summary>
+        private static bool IsStar(DefinitionBase definition)
         {
-            var columnDefinition = definition as ColumnDefinition;
-            if (columnDefinition != null)
+            return definition.UserSizeValueCache.IsStar;
+        }
+
+        /// <summary>
+        /// Gets Column or Row definition at index from grid based on resize direction.
+        /// </summary>
+        private static DefinitionBase GetGridDefinition(Grid grid, int index, GridResizeDirection direction)
+        {
+            return direction == GridResizeDirection.Columns ?
+                (DefinitionBase)grid.ColumnDefinitions[index] :
+                (DefinitionBase)grid.RowDefinitions[index];
+        }
+
+        /// <summary>
+        /// Retrieves the ActualWidth or ActualHeight of the definition depending on its type Column or Row.
+        /// </summary>
+        private double GetActualLength(DefinitionBase definition)
+        {
+            var column = definition as ColumnDefinition;
+
+            return column?.ActualWidth ?? ((RowDefinition)definition).ActualHeight;
+        }
+
+        /// <summary>
+        /// Gets Column or Row definition at index from grid based on resize direction.
+        /// </summary>
+        private static void SetDefinitionLength(DefinitionBase definition, GridLength length)
+        {
+            definition.SetValue(
+                definition is ColumnDefinition ? ColumnDefinition.WidthProperty : RowDefinition.HeightProperty, length);
+        }
+
+        /// <summary>
+        /// Get the minimum and maximum Delta can be given definition constraints (MinWidth/MaxWidth).
+        /// </summary>
+        private void GetDeltaConstraints(out double minDelta, out double maxDelta)
+        {
+            double definition1Len = GetActualLength(_resizeData.Definition1);
+            double definition1Min = _resizeData.Definition1.UserMinSizeValueCache;
+            double definition1Max = _resizeData.Definition1.UserMaxSizeValueCache;
+
+            double definition2Len = GetActualLength(_resizeData.Definition2);
+            double definition2Min = _resizeData.Definition2.UserMinSizeValueCache;
+            double definition2Max = _resizeData.Definition2.UserMaxSizeValueCache;
+
+            // Set MinWidths to be greater than width of splitter.
+            if (_resizeData.SplitterIndex == _resizeData.Definition1Index)
+            {
+                definition1Min = Math.Max(definition1Min, _resizeData.SplitterLength);
+            }
+            else if (_resizeData.SplitterIndex == _resizeData.Definition2Index)
+            {
+                definition2Min = Math.Max(definition2Min, _resizeData.SplitterLength);
+            }
+
+            if (_resizeData.SplitBehavior == SplitBehavior.Split)
             {
-                columnDefinition.Width = new GridLength(value);
+                // Determine the minimum and maximum the columns can be resized.
+                minDelta = -Math.Min(definition1Len - definition1Min, definition2Max - definition2Len);
+                maxDelta = Math.Min(definition1Max - definition1Len, definition2Len - definition2Min);
+            }
+            else if (_resizeData.SplitBehavior == SplitBehavior.Resize1)
+            {
+                minDelta = definition1Min - definition1Len;
+                maxDelta = definition1Max - definition1Len;
             }
             else
             {
-                ((RowDefinition)definition).Height = new GridLength(value);
+                minDelta = definition2Len - definition2Max;
+                maxDelta = definition2Len - definition2Min;
             }
         }
 
-        protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e)
+        /// <summary>
+        /// Sets the length of definition1 and definition2.
+        /// </summary>
+        private void SetLengths(double definition1Pixels, double definition2Pixels)
         {
-            base.OnAttachedToVisualTree(e);
-            _grid = this.GetVisualParent<Grid>();
+            // For the case where both definition1 and 2 are stars, update all star values to match their current pixel values.
+            if (_resizeData.SplitBehavior == SplitBehavior.Split)
+            {
+                var definitions = _resizeData.ResizeDirection == GridResizeDirection.Columns ?
+                    (IAvaloniaReadOnlyList<DefinitionBase>)_resizeData.Grid.ColumnDefinitions :
+                    (IAvaloniaReadOnlyList<DefinitionBase>)_resizeData.Grid.RowDefinitions;
 
-            _orientation = DetectOrientation();
+                var definitionsCount = definitions.Count;
 
-            int definitionIndex; //row or col
-            if (_orientation == Orientation.Vertical)
+                for (var i = 0; i < definitionsCount; i++)
+                {
+                    DefinitionBase definition = definitions[i];
+
+                    // For each definition, if it is a star, set is value to ActualLength in stars
+                    // This makes 1 star == 1 pixel in length
+                    if (i == _resizeData.Definition1Index)
+                    {
+                        SetDefinitionLength(definition, new GridLength(definition1Pixels, GridUnitType.Star));
+                    }
+                    else if (i == _resizeData.Definition2Index)
+                    {
+                        SetDefinitionLength(definition, new GridLength(definition2Pixels, GridUnitType.Star));
+                    }
+                    else if (IsStar(definition))
+                    {
+                        SetDefinitionLength(definition, new GridLength(GetActualLength(definition), GridUnitType.Star));
+                    }
+                }
+            }
+            else if (_resizeData.SplitBehavior == SplitBehavior.Resize1)
             {
-                Cursor = new Cursor(StandardCursorType.SizeWestEast);
-                _definitions = _grid.ColumnDefinitions.Cast<DefinitionBase>().ToList();
-                definitionIndex = GetValue(Grid.ColumnProperty);
-                PseudoClasses.Add(":vertical");
+                SetDefinitionLength(_resizeData.Definition1, new GridLength(definition1Pixels));
             }
             else
             {
-                Cursor = new Cursor(StandardCursorType.SizeNorthSouth);
-                definitionIndex = GetValue(Grid.RowProperty);
-                _definitions = _grid.RowDefinitions.Cast<DefinitionBase>().ToList();
-                PseudoClasses.Add(":horizontal");
+                SetDefinitionLength(_resizeData.Definition2, new GridLength(definition2Pixels));
             }
+        }
 
-            if (definitionIndex > 0)
-                _prevDefinition = _definitions[definitionIndex - 1];
+        /// <summary>
+        /// Move the splitter by the given Delta's in the horizontal and vertical directions.
+        /// </summary>
+        private void MoveSplitter(double horizontalChange, double verticalChange)
+        {
+            Debug.Assert(_resizeData != null, "_resizeData should not be null when calling MoveSplitter");
+
+            // Calculate the offset to adjust the splitter.
+            var delta = _resizeData.ResizeDirection == GridResizeDirection.Columns ? horizontalChange : verticalChange;
+
+            DefinitionBase definition1 = _resizeData.Definition1;
+            DefinitionBase definition2 = _resizeData.Definition2;
+
+            if (definition1 != null && definition2 != null)
+            {
+                double actualLength1 = GetActualLength(definition1);
+                double actualLength2 = GetActualLength(definition2);
+
+                // When splitting, Check to see if the total pixels spanned by the definitions 
+                // is the same asbefore starting resize. If not cancel the drag
+                if (_resizeData.SplitBehavior == SplitBehavior.Split &&
+                    !MathUtilities.AreClose(
+                        actualLength1 + actualLength2,
+                        _resizeData.OriginalDefinition1ActualLength + _resizeData.OriginalDefinition2ActualLength))
+                {
+                    CancelResize();
 
-            if (definitionIndex < _definitions.Count - 1)
-                _nextDefinition = _definitions[definitionIndex + 1];
+                    return;
+                }
+
+                GetDeltaConstraints(out var min, out var max);
+
+                // Constrain Delta to Min/MaxWidth of columns
+                delta = Math.Min(Math.Max(delta, min), max);
+
+                double definition1LengthNew = actualLength1 + delta;
+                double definition2LengthNew = actualLength1 + actualLength2 - definition1LengthNew;
+
+                SetLengths(definition1LengthNew, definition2LengthNew);
+            }
         }
 
-        private Orientation DetectOrientation()
+        /// <summary>
+        /// Move the splitter using the Keyboard (Don't show preview).
+        /// </summary>
+        private bool KeyboardMoveSplitter(double horizontalChange, double verticalChange)
         {
-            if (!_grid.ColumnDefinitions.Any())
-                return Orientation.Horizontal;
-            if (!_grid.RowDefinitions.Any())
-                return Orientation.Vertical;
+            // If moving with the mouse, ignore keyboard motion.
+            if (_resizeData != null)
+            {
+                return false; // Don't handle the event.
+            }
 
-            var col = GetValue(Grid.ColumnProperty);
-            var row = GetValue(Grid.RowProperty);
-            var width = _grid.ColumnDefinitions[col].Width;
-            var height = _grid.RowDefinitions[row].Height;
-            if (width.IsAuto && !height.IsAuto)
+            // Don't show preview.
+            InitializeData(false); 
+                
+            // Check that we are actually able to resize.
+            if (_resizeData == null)
             {
-                return Orientation.Vertical;
+                return false; // Don't handle the event.
             }
-            if (!width.IsAuto && height.IsAuto)
+
+            MoveSplitter(horizontalChange, verticalChange);
+
+            _resizeData = null;
+
+            return true;
+        }
+
+        /// <summary>
+        /// This adorner draws the preview for the <see cref="GridSplitter"/>.
+        /// It also positions the adorner.
+        /// </summary>
+        private sealed class PreviewAdorner : Decorator
+        {
+            private readonly TranslateTransform _translation;
+            private readonly Decorator _decorator;
+            
+            public PreviewAdorner(IControl previewControl)
             {
-                return Orientation.Horizontal;
+                // Add a decorator to perform translations.
+                _translation = new TranslateTransform();
+
+                _decorator = new Decorator
+                {
+                    Child = previewControl, 
+                    RenderTransform = _translation
+                };
+
+                Child = _decorator;
+            }
+
+            /// <summary>
+            /// The Preview's Offset in the X direction from the GridSplitter.
+            /// </summary>
+            public double OffsetX
+            {
+                get => _translation.X;
+                set => _translation.X = value;
+            }
+
+            /// <summary>
+            /// The Preview's Offset in the Y direction from the GridSplitter.
+            /// </summary>
+            public double OffsetY
+            {
+                get => _translation.Y;
+                set => _translation.Y = value;
             }
-            if (_grid.Children.OfType<Control>() // Decision based on other controls in the same column
-                .Where(c => Grid.GetColumn(c) == col)
-                .Any(c => c.GetType() != typeof(GridSplitter)))
+
+            protected override Size ArrangeOverride(Size finalSize)
             {
-                return Orientation.Horizontal;
+                // Adorners always get clipped to the owner control. In this case we want
+                // to constrain size to the splitter size but draw on top of the parent grid.
+                Clip = null;
+
+                return base.ArrangeOverride(finalSize);
             }
-            return Orientation.Vertical;
+        }
+
+        /// <summary>
+        /// <see cref="GridSplitter"/> has special Behavior when columns are fixed.
+        /// If the left column is fixed, splitter will only resize that column.
+        /// Else if the right column is fixed, splitter will only resize the right column.
+        /// </summary>
+        private enum SplitBehavior
+        {
+            /// <summary>
+            /// Both columns/rows are star lengths.
+            /// </summary>
+            Split,
+
+            /// <summary>
+            /// Resize 1 only.
+            /// </summary>
+            Resize1,
+
+            /// <summary>
+            /// Resize 2 only.
+            /// </summary>
+            Resize2
+        }
+
+        /// <summary>
+        /// Stores data during the resizing operation.
+        /// </summary>
+        private class ResizeData
+        {
+            public bool ShowsPreview;
+            public PreviewAdorner Adorner;
+
+            // The constraints to keep the Preview within valid ranges.
+            public double MinChange;
+            public double MaxChange;
+
+            // The grid to Resize.
+            public Grid Grid;
+
+            // Cache of Resize Direction and Behavior.
+            public GridResizeDirection ResizeDirection;
+            public GridResizeBehavior ResizeBehavior;
+
+            // The columns/rows to resize.
+            public DefinitionBase Definition1;
+            public DefinitionBase Definition2;
+
+            // Are the columns/rows star lengths.
+            public SplitBehavior SplitBehavior;
+
+            // The index of the splitter.
+            public int SplitterIndex;
+
+            // The indices of the columns/rows.
+            public int Definition1Index;
+            public int Definition2Index;
+
+            // The original lengths of Definition1 and Definition2 (to restore lengths if user cancels resize).
+            public GridLength OriginalDefinition1Length;
+            public GridLength OriginalDefinition2Length;
+            public double OriginalDefinition1ActualLength;
+            public double OriginalDefinition2ActualLength;
+
+            // The minimum of Width/Height of Splitter.  Used to ensure splitter 
+            // isn't hidden by resizing a row/column smaller than the splitter.
+            public double SplitterLength;
         }
     }
 }

+ 13 - 43
src/Avalonia.Themes.Default/GridSplitter.xaml

@@ -1,51 +1,21 @@
 <Styles xmlns="https://github.com/avaloniaui">
-  <Style Selector="GridSplitter:vertical">
-    <Setter Property="Width" Value="6"/>
-    <Setter Property="Background" Value="{DynamicResource ThemeControlLowBrush}"/>
-    <Setter Property="Template">
-      <ControlTemplate>
-        <Border Background="{TemplateBinding Background}">
-          <StackPanel Orientation="Vertical" HorizontalAlignment="Center" VerticalAlignment="Center">
-            <StackPanel.Styles>
-              <Style Selector="Ellipse">
-                <Setter Property="HorizontalAlignment" Value="Center"/>
-                <Setter Property="Width" Value="4"/>
-                <Setter Property="Height" Value="4"/>
-                <Setter Property="Fill" Value="{DynamicResource ThemeControlMidBrush}"/>
-                <Setter Property="Margin" Value="1"/>
-              </Style>
-            </StackPanel.Styles>
-            <Ellipse/>
-            <Ellipse/>
-            <Ellipse/>
-          </StackPanel>
-        </Border>
-      </ControlTemplate>
+
+  <Style Selector="GridSplitter">
+    <Setter Property="Focusable" Value="True" />
+    <Setter Property="Background" Value="{DynamicResource ThemeBorderLowBrush}" />
+    <Setter Property="PreviewContent">
+      <Template>
+        <Rectangle Fill="{DynamicResource HighlightBrush}" />
+      </Template>
     </Setter>
-  </Style>
-  <Style Selector="GridSplitter:horizontal">
-    <Setter Property="Height" Value="6"/>
-    <Setter Property="Background" Value="{DynamicResource ThemeControlLowBrush}"/>
     <Setter Property="Template">
       <ControlTemplate>
-        <Border Background="{TemplateBinding Background}">
-          <StackPanel Orientation="Horizontal" HorizontalAlignment="Center" VerticalAlignment="Center">
-            <StackPanel.Styles>
-              <Style Selector="Ellipse">
-                <Setter Property="VerticalAlignment" Value="Center"/>
-                <Setter Property="Width" Value="4"/>
-                <Setter Property="Height" Value="4"/>
-                <Setter Property="Fill" Value="{DynamicResource ThemeControlMidBrush}"/>
-                <Setter Property="Margin" Value="1"/>
-              </Style>
-            </StackPanel.Styles>
-            <Ellipse/>
-            <Ellipse/>
-            <Ellipse/>
-          </StackPanel>
-        </Border>
+        <Border 
+          BorderBrush="{TemplateBinding BorderBrush}"
+          BorderThickness="{TemplateBinding BorderThickness}"
+          Background="{TemplateBinding Background}"/>
       </ControlTemplate>
     </Setter>
   </Style>
-</Styles>
 
+</Styles>

+ 187 - 126
tests/Avalonia.Controls.UnitTests/GridSplitterTests.cs

@@ -2,9 +2,7 @@ using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Platform;
 using Avalonia.UnitTests;
-
 using Moq;
-
 using Xunit;
 
 namespace Avalonia.Controls.UnitTests
@@ -21,183 +19,246 @@ namespace Avalonia.Controls.UnitTests
         public void Detects_Horizontal_Orientation()
         {
             GridSplitter splitter;
-            var grid = new Grid()
-                       {
-                            RowDefinitions = new RowDefinitions("*,Auto,*"),
-                            ColumnDefinitions = new ColumnDefinitions("*,*"),
-                            Children =
-                            {
-                                new Border { [Grid.RowProperty] = 0 },
-                                (splitter = new GridSplitter { [Grid.RowProperty] = 1 }),
-                                new Border { [Grid.RowProperty] = 2 }
-                            }
-                       };
+
+            var grid = new Grid
+            {
+                RowDefinitions = new RowDefinitions("*,Auto,*"),
+                ColumnDefinitions = new ColumnDefinitions("*,*"),
+                Children =
+                {
+                    new Border { [Grid.RowProperty] = 0 },
+                    (splitter = new GridSplitter { [Grid.RowProperty] = 1 }),
+                    new Border { [Grid.RowProperty] = 2 }
+                }
+            };
 
             var root = new TestRoot { Child = grid };
             root.Measure(new Size(100, 300));
             root.Arrange(new Rect(0, 0, 100, 300));
-            Assert.Contains(splitter.Classes, ":horizontal".Equals);
+            Assert.Equal(GridResizeDirection.Rows, splitter.GetEffectiveResizeDirection());
         }
 
         [Fact]
         public void Detects_Vertical_Orientation()
         {
             GridSplitter splitter;
-            var grid = new Grid()
-                       {
-                            ColumnDefinitions = new ColumnDefinitions("*,Auto,*"),
-                            RowDefinitions = new RowDefinitions("*,*"),
-                            Children =
-                            {
-                                new Border { [Grid.ColumnProperty] = 0 },
-                                (splitter = new GridSplitter { [Grid.ColumnProperty] = 1}),
-                                new Border { [Grid.ColumnProperty] = 2 },
-                            }
-                       };
+
+            var grid = new Grid
+            {
+                ColumnDefinitions = new ColumnDefinitions("*,Auto,*"),
+                RowDefinitions = new RowDefinitions("*,*"),
+                Children =
+                {
+                    new Border { [Grid.ColumnProperty] = 0 },
+                    (splitter = new GridSplitter { [Grid.ColumnProperty] = 1 }),
+                    new Border { [Grid.ColumnProperty] = 2 },
+                }
+            };
 
             var root = new TestRoot { Child = grid };
             root.Measure(new Size(100, 300));
             root.Arrange(new Rect(0, 0, 100, 300));
-            Assert.Contains(splitter.Classes, ":vertical".Equals);
+            Assert.Equal(GridResizeDirection.Columns, splitter.GetEffectiveResizeDirection());
         }
 
         [Fact]
         public void Detects_With_Both_Auto()
         {
             GridSplitter splitter;
-            var grid = new Grid()
-                       {
-                            ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"),
-                            RowDefinitions = new RowDefinitions("Auto,Auto"),
-                            Children =
-                            {
-                                new Border { [Grid.ColumnProperty] = 0 },
-                                (splitter = new GridSplitter { [Grid.ColumnProperty] = 1}),
-                                new Border { [Grid.ColumnProperty] = 2 },
-                            }
-                       };
+
+            var grid = new Grid
+            {
+                ColumnDefinitions = new ColumnDefinitions("Auto,Auto,Auto"),
+                RowDefinitions = new RowDefinitions("Auto,Auto"),
+                Children =
+                {
+                    new Border { [Grid.ColumnProperty] = 0 },
+                    (splitter = new GridSplitter { [Grid.ColumnProperty] = 1 }),
+                    new Border { [Grid.ColumnProperty] = 2 },
+                }
+            };
 
             var root = new TestRoot { Child = grid };
             root.Measure(new Size(100, 300));
             root.Arrange(new Rect(0, 0, 100, 300));
-            Assert.Contains(splitter.Classes, ":vertical".Equals);
+            Assert.Equal(GridResizeDirection.Columns, splitter.GetEffectiveResizeDirection());
         }
 
         [Fact]
-        public void Horizontal_Stays_Within_Constraints()
+        public void In_First_Position_Doesnt_Throw_Exception()
+        {
+            GridSplitter splitter;
+            var grid = new Grid
+            {
+                ColumnDefinitions = new ColumnDefinitions("Auto,*,*"),
+                RowDefinitions = new RowDefinitions("*,*"),
+                Children =
+                {
+                    (splitter = new GridSplitter { [Grid.ColumnProperty] = 0 }),
+                    new Border { [Grid.ColumnProperty] = 1 },
+                    new Border { [Grid.ColumnProperty] = 2 },
+                }
+            };
+
+            var root = new TestRoot { Child = grid };
+            root.Measure(new Size(100, 300));
+            root.Arrange(new Rect(0, 0, 100, 300));
+
+            splitter.RaiseEvent(
+                new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent });
+
+            splitter.RaiseEvent(new VectorEventArgs
+            {
+                RoutedEvent = Thumb.DragDeltaEvent, Vector = new Vector(100, 1000)
+            });
+        }
+
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public void Horizontal_Stays_Within_Constraints(bool showsPreview)
         {
             var control1 = new Border { [Grid.RowProperty] = 0 };
-            var splitter = new GridSplitter
-                           {
-                               [Grid.RowProperty] = 1,
-                           };
+            var splitter = new GridSplitter { [Grid.RowProperty] = 1, ShowsPreview = showsPreview};
             var control2 = new Border { [Grid.RowProperty] = 2 };
 
-            var rowDefinitions = new RowDefinitions()
-                                 {
-                                     new RowDefinition(1, GridUnitType.Star) { MinHeight = 70, MaxHeight = 110 },
-                                     new RowDefinition(GridLength.Auto),
-                                     new RowDefinition(1, GridUnitType.Star) { MinHeight = 10, MaxHeight = 140 },
-                                 };
-
-            var grid = new Grid()
-                       {
-                            RowDefinitions = rowDefinitions,
-                            Children =
-                            {
-                                control1, splitter, control2
-                            }
-                       };
+            var rowDefinitions = new RowDefinitions
+            {
+                new RowDefinition(1, GridUnitType.Star) { MinHeight = 70, MaxHeight = 110 },
+                new RowDefinition(GridLength.Auto),
+                new RowDefinition(1, GridUnitType.Star) { MinHeight = 10, MaxHeight = 140 },
+            };
+
+            var grid = new Grid { RowDefinitions = rowDefinitions, Children = { control1, splitter, control2 } };
+
+            var root = new TestRoot
+            {
+                Child = new VisualLayerManager
+                {
+                    Child = grid
+                }
+            };
 
-            var root = new TestRoot { Child = grid };
             root.Measure(new Size(100, 200));
             root.Arrange(new Rect(0, 0, 100, 200));
 
+            splitter.RaiseEvent(
+                new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent });
+
             splitter.RaiseEvent(new VectorEventArgs
-                                {
-                                    RoutedEvent = Thumb.DragDeltaEvent,
-                                    Vector = new Vector(0, -100)
-                                });
-            Assert.Equal(rowDefinitions[0].Height, new GridLength(70, GridUnitType.Star));
-            Assert.Equal(rowDefinitions[2].Height, new GridLength(130, GridUnitType.Star));
+            {
+                RoutedEvent = Thumb.DragDeltaEvent,
+                Vector = new Vector(0, -100)
+            });
+
+            if (showsPreview)
+            {
+                Assert.Equal(rowDefinitions[0].Height, new GridLength(1, GridUnitType.Star));
+                Assert.Equal(rowDefinitions[2].Height, new GridLength(1, GridUnitType.Star));
+            }
+            else
+            {
+                Assert.Equal(rowDefinitions[0].Height, new GridLength(70, GridUnitType.Star));
+                Assert.Equal(rowDefinitions[2].Height, new GridLength(130, GridUnitType.Star));
+            }
+
             splitter.RaiseEvent(new VectorEventArgs
-                                {
-                                    RoutedEvent = Thumb.DragDeltaEvent,
-                                    Vector = new Vector(0, 100)
-                                });
-            Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star));
-            Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star));
-        }
+            {
+                RoutedEvent = Thumb.DragDeltaEvent,
+                Vector = new Vector(0, 100)
+            });
 
-        [Fact]
-        public void In_First_Position_Doesnt_Throw_Exception()
-        {
-            GridSplitter splitter;
-            var grid = new Grid()
-                       {
-                            ColumnDefinitions = new ColumnDefinitions("Auto,*,*"),
-                            RowDefinitions = new RowDefinitions("*,*"),
-                            Children =
-                            {
-                                (splitter = new GridSplitter { [Grid.ColumnProperty] = 0} ),
-                                new Border { [Grid.ColumnProperty] = 1 },
-                                new Border { [Grid.ColumnProperty] = 2 },
-                            }
-                       };
+            if (showsPreview)
+            {
+                Assert.Equal(rowDefinitions[0].Height, new GridLength(1, GridUnitType.Star));
+                Assert.Equal(rowDefinitions[2].Height, new GridLength(1, GridUnitType.Star));
+            }
+            else
+            {
+                Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star));
+                Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star));
+            }
 
-            var root = new TestRoot { Child = grid };
-            root.Measure(new Size(100, 300));
-            root.Arrange(new Rect(0, 0, 100, 300));
             splitter.RaiseEvent(new VectorEventArgs
-                                {
-                                    RoutedEvent = Thumb.DragDeltaEvent,
-                                    Vector = new Vector(100, 1000)
-                                });
+            {
+                RoutedEvent = Thumb.DragCompletedEvent
+            });
+
+            Assert.Equal(rowDefinitions[0].Height, new GridLength(110, GridUnitType.Star));
+            Assert.Equal(rowDefinitions[2].Height, new GridLength(90, GridUnitType.Star));
         }
 
-        [Fact]
-        public void Vertical_Stays_Within_Constraints()
+        [Theory]
+        [InlineData(false)]
+        [InlineData(true)]
+        public void Vertical_Stays_Within_Constraints(bool showsPreview)
         {
             var control1 = new Border { [Grid.ColumnProperty] = 0 };
-            var splitter = new GridSplitter
-                           {
-                               [Grid.ColumnProperty] = 1,
-                           };
+            var splitter = new GridSplitter { [Grid.ColumnProperty] = 1, ShowsPreview = showsPreview};
             var control2 = new Border { [Grid.ColumnProperty] = 2 };
 
-            var columnDefinitions = new ColumnDefinitions()
-                                    {
-                                        new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 10, MaxWidth = 190 },
-                                        new ColumnDefinition(GridLength.Auto),
-                                        new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 80, MaxWidth = 120 },
-                                    };
-
-            var grid = new Grid()
-                       {
-                            ColumnDefinitions = columnDefinitions,
-                            Children =
-                            {
-                                control1, splitter, control2
-                            }
-                       };
+            var columnDefinitions = new ColumnDefinitions
+            {
+                new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 10, MaxWidth = 190 },
+                new ColumnDefinition(GridLength.Auto),
+                new ColumnDefinition(1, GridUnitType.Star) { MinWidth = 80, MaxWidth = 120 },
+            };
 
-            var root = new TestRoot { Child = grid };
+            var grid = new Grid { ColumnDefinitions = columnDefinitions, Children = { control1, splitter, control2 } };
+
+            var root = new TestRoot
+            {
+                Child = new VisualLayerManager
+                {
+                    Child = grid
+                }
+            };
 
             root.Measure(new Size(200, 100));
             root.Arrange(new Rect(0, 0, 200, 100));
 
+            splitter.RaiseEvent(
+                new VectorEventArgs { RoutedEvent = Thumb.DragStartedEvent });
+
             splitter.RaiseEvent(new VectorEventArgs
-                                {
-                                    RoutedEvent = Thumb.DragDeltaEvent,
-                                    Vector = new Vector(-100, 0)
-                                });
-            Assert.Equal(columnDefinitions[0].Width, new GridLength(80, GridUnitType.Star));
-            Assert.Equal(columnDefinitions[2].Width, new GridLength(120, GridUnitType.Star));
+            {
+                RoutedEvent = Thumb.DragDeltaEvent,
+                Vector = new Vector(-100, 0)
+            });
+
+            if (showsPreview)
+            {
+                Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star));
+                Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star));
+            }
+            else
+            {
+                Assert.Equal(columnDefinitions[0].Width, new GridLength(80, GridUnitType.Star));
+                Assert.Equal(columnDefinitions[2].Width, new GridLength(120, GridUnitType.Star));
+            }
+
             splitter.RaiseEvent(new VectorEventArgs
-                                {
-                                    RoutedEvent = Thumb.DragDeltaEvent,
-                                    Vector = new Vector(100, 0)
-                                });
+            {
+                RoutedEvent = Thumb.DragDeltaEvent,
+                Vector = new Vector(100, 0)
+            });
+
+            if (showsPreview)
+            {
+                Assert.Equal(columnDefinitions[0].Width, new GridLength(1, GridUnitType.Star));
+                Assert.Equal(columnDefinitions[2].Width, new GridLength(1, GridUnitType.Star));
+            }
+            else
+            {
+                Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star));
+                Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star));
+            }
+
+            splitter.RaiseEvent(new VectorEventArgs
+            {
+                RoutedEvent = Thumb.DragCompletedEvent
+            });
+
             Assert.Equal(columnDefinitions[0].Width, new GridLength(120, GridUnitType.Star));
             Assert.Equal(columnDefinitions[2].Width, new GridLength(80, GridUnitType.Star));
         }