瀏覽代碼

Merge branch 'master' into fixes/1625-textalignment-bounds

Steven Kirk 6 年之前
父節點
當前提交
0fc0ac51e7
共有 31 個文件被更改,包括 1202 次插入156 次删除
  1. 3 0
      .editorconfig
  2. 1 1
      samples/ControlCatalog/Pages/TreeViewPage.xaml
  3. 2 2
      src/Avalonia.Controls/AppBuilderBase.cs
  4. 2 2
      src/Avalonia.Controls/Button.cs
  5. 6 1
      src/Avalonia.Controls/Generators/TreeContainerIndex.cs
  6. 9 4
      src/Avalonia.Controls/MenuItem.cs
  7. 40 22
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  8. 1 1
      src/Avalonia.Controls/SystemDialog.cs
  9. 458 85
      src/Avalonia.Controls/TreeView.cs
  10. 2 0
      src/Avalonia.ReactiveUI/RoutedViewHost.cs
  11. 1 1
      src/Avalonia.Remote.Protocol/DefaultMessageTypeResolver.cs
  12. 2 2
      src/Avalonia.Remote.Protocol/MetsysBson.cs
  13. 131 0
      src/Avalonia.Styling/Styling/OrSelector.cs
  14. 22 0
      src/Avalonia.Styling/Styling/Selectors.cs
  15. 1 7
      src/Avalonia.Themes.Default/Separator.xaml
  16. 15 3
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  17. 2 2
      src/Avalonia.Visuals/Rendering/RenderLoop.cs
  18. 2 2
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  19. 36 0
      src/Avalonia.Visuals/VisualTree/TransformedBounds.cs
  20. 21 9
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
  21. 20 0
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
  22. 7 1
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  23. 1 1
      src/Windows/Avalonia.Win32/SystemDialogImpl.cs
  24. 257 7
      tests/Avalonia.Controls.UnitTests/TreeViewTests.cs
  25. 16 0
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
  26. 7 0
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs
  27. 11 3
      tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs
  28. 20 0
      tests/Avalonia.RenderTests/Shapes/RectangleTests.cs
  29. 106 0
      tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs
  30. 二進制
      tests/TestFiles/Direct2D1/Shapes/Rectangle/Rectangle_0px_Stroke.expected.png
  31. 二進制
      tests/TestFiles/Skia/Shapes/Rectangle/Rectangle_0px_Stroke.expected.png

+ 3 - 0
.editorconfig

@@ -132,6 +132,9 @@ csharp_space_between_method_declaration_parameter_list_parentheses = false
 csharp_space_between_parentheses = false
 csharp_space_between_square_brackets = false
 
+# Wrapping preferences
+csharp_wrap_before_ternary_opsigns = false
+
 # Xaml files
 [*.xaml]
 indent_size = 4

+ 1 - 1
samples/ControlCatalog/Pages/TreeViewPage.xaml

@@ -9,7 +9,7 @@
               Margin="0,16,0,0"
               HorizontalAlignment="Center"
               Spacing="16">
-      <TreeView Items="{Binding}" Width="250" Height="350">
+      <TreeView SelectionMode="Multiple" Items="{Binding}" Width="250" Height="350">
         <TreeView.ItemTemplate>
           <TreeDataTemplate ItemsSource="{Binding Children}">
             <TextBlock Text="{Binding Header}"/>

+ 2 - 2
src/Avalonia.Controls/AppBuilderBase.cs

@@ -210,7 +210,7 @@ namespace Avalonia.Controls
             var platformClassName = assemblyName.Replace("Avalonia.", string.Empty) + "Platform";
             var platformClassFullName = assemblyName + "." + platformClassName;
             var platformClass = assembly.GetType(platformClassFullName);
-            var init = platformClass.GetRuntimeMethod("Initialize", new Type[0]);
+            var init = platformClass.GetRuntimeMethod("Initialize", Type.EmptyTypes);
             init.Invoke(null, null);
         };
 
@@ -245,7 +245,7 @@ namespace Avalonia.Controls
                                      select (from constructor in moduleType.GetTypeInfo().DeclaredConstructors
                                              where constructor.GetParameters().Length == 0 && !constructor.IsStatic
                                              select constructor).Single() into constructor
-                                     select (Action)(() => constructor.Invoke(new object[0]));
+                                     select (Action)(() => constructor.Invoke(Array.Empty<object>()));
             Delegate.Combine(moduleInitializers.ToArray()).DynamicInvoke();
         }
 

+ 2 - 2
src/Avalonia.Controls/Button.cs

@@ -32,6 +32,8 @@ namespace Avalonia.Controls
     /// </summary>
     public class Button : ContentControl
     {
+        private ICommand _command;
+
         /// <summary>
         /// Defines the <see cref="ClickMode"/> property.
         /// </summary>
@@ -69,8 +71,6 @@ namespace Avalonia.Controls
         public static readonly RoutedEvent<RoutedEventArgs> ClickEvent =
             RoutedEvent.Register<Button, RoutedEventArgs>(nameof(Click), RoutingStrategies.Bubble);
 
-        private ICommand _command;
-
         public static readonly StyledProperty<bool> IsPressedProperty =
             AvaloniaProperty.Register<Button, bool>(nameof(IsPressed));
 

+ 6 - 1
src/Avalonia.Controls/Generators/TreeContainerIndex.cs

@@ -34,7 +34,12 @@ namespace Avalonia.Controls.Generators
         /// <summary>
         /// Gets the currently materialized containers.
         /// </summary>
-        public IEnumerable<IControl> Items => _containerToItem.Keys;
+        public IEnumerable<IControl> Containers => _containerToItem.Keys;
+
+        /// <summary>
+        /// Gets the items of currently materialized containers.
+        /// </summary>
+        public IEnumerable<object> Items => _containerToItem.Values;
 
         /// <summary>
         /// Adds an entry to the index.

+ 9 - 4
src/Avalonia.Controls/MenuItem.cs

@@ -20,11 +20,16 @@ namespace Avalonia.Controls
     /// </summary>
     public class MenuItem : HeaderedSelectingItemsControl, IMenuItem, ISelectable
     {
+        private ICommand _command;
+
         /// <summary>
         /// Defines the <see cref="Command"/> property.
         /// </summary>
-        public static readonly StyledProperty<ICommand> CommandProperty =
-            AvaloniaProperty.Register<MenuItem, ICommand>(nameof(Command));
+        public static readonly DirectProperty<MenuItem, ICommand> CommandProperty =
+            Button.CommandProperty.AddOwner<MenuItem>(
+                menuItem => menuItem.Command, 
+                (menuItem, command) => menuItem.Command = command, 
+                enableDataValidation: true);
 
         /// <summary>
         /// Defines the <see cref="HotKey"/> property.
@@ -159,8 +164,8 @@ namespace Avalonia.Controls
         /// </summary>
         public ICommand Command
         {
-            get { return GetValue(CommandProperty); }
-            set { SetValue(CommandProperty, value); }
+            get { return _command; }
+            set { SetAndRaise(CommandProperty, ref _command, value); }
         }
 
         /// <summary>

+ 40 - 22
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@@ -10,6 +10,7 @@ using Avalonia.Collections;
 using Avalonia.Controls.Generators;
 using Avalonia.Data;
 using Avalonia.Input;
+using Avalonia.Input.Platform;
 using Avalonia.Interactivity;
 using Avalonia.Styling;
 using Avalonia.VisualTree;
@@ -99,7 +100,7 @@ namespace Avalonia.Controls.Primitives
                 "SelectionChanged",
                 RoutingStrategies.Bubble);
 
-        private static readonly IList Empty = new object[0];
+        private static readonly IList Empty = Array.Empty<object>();
 
         private int _selectedIndex = -1;
         private object _selectedItem;
@@ -459,6 +460,23 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
+        protected override void OnKeyDown(KeyEventArgs e)
+        {
+            base.OnKeyDown(e);
+
+            if (!e.Handled)
+            {
+                var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
+                bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
+
+                if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll))
+                {
+                    SynchronizeItems(SelectedItems, Items?.Cast<object>());
+                    e.Handled = true;
+                }
+            }
+        }
+
         /// <summary>
         /// Moves the selection in the specified direction relative to the current selection.
         /// </summary>
@@ -614,32 +632,12 @@ namespace Avalonia.Controls.Primitives
             return false;
         }
 
-        /// <summary>
-        /// Gets a range of items from an IEnumerable.
-        /// </summary>
-        /// <param name="items">The items.</param>
-        /// <param name="first">The index of the first item.</param>
-        /// <param name="last">The index of the last item.</param>
-        /// <returns>The items.</returns>
-        private static IEnumerable<object> GetRange(IEnumerable items, int first, int last)
-        {
-            var list = (items as IList) ?? items.Cast<object>().ToList();
-            int step = first > last ? -1 : 1;
-
-            for (int i = first; i != last; i += step)
-            {
-                yield return list[i];
-            }
-
-            yield return list[last];
-        }
-
         /// <summary>
         /// Makes a list of objects equal another.
         /// </summary>
         /// <param name="items">The items collection.</param>
         /// <param name="desired">The desired items.</param>
-        private static void SynchronizeItems(IList items, IEnumerable<object> desired)
+        internal static void SynchronizeItems(IList items, IEnumerable<object> desired)
         {
             int index = 0;
 
@@ -666,6 +664,26 @@ namespace Avalonia.Controls.Primitives
             }
         }
 
+        /// <summary>
+        /// Gets a range of items from an IEnumerable.
+        /// </summary>
+        /// <param name="items">The items.</param>
+        /// <param name="first">The index of the first item.</param>
+        /// <param name="last">The index of the last item.</param>
+        /// <returns>The items.</returns>
+        private static IEnumerable<object> GetRange(IEnumerable items, int first, int last)
+        {
+            var list = (items as IList) ?? items.Cast<object>().ToList();
+            int step = first > last ? -1 : 1;
+
+            for (int i = first; i != last; i += step)
+            {
+                yield return list[i];
+            }
+
+            yield return list[last];
+        }
+
         /// <summary>
         /// Called when a container raises the <see cref="IsSelectedChangedEvent"/>.
         /// </summary>

+ 1 - 1
src/Avalonia.Controls/SystemDialog.cs

@@ -27,7 +27,7 @@ namespace Avalonia.Controls
                 throw new ArgumentNullException(nameof(parent));
             return ((await AvaloniaLocator.Current.GetService<ISystemDialogImpl>()
                  .ShowFileDialogAsync(this, parent?.PlatformImpl)) ??
-             new string[0]).FirstOrDefault();
+             Array.Empty<string>()).FirstOrDefault();
         }
     }
 

+ 458 - 85
src/Avalonia.Controls/TreeView.cs

@@ -2,13 +2,16 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Collections;
 using System.Collections.Generic;
+using System.Collections.Specialized;
 using System.Linq;
+using Avalonia.Collections;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
+using Avalonia.Input.Platform;
 using Avalonia.Interactivity;
-using Avalonia.Styling;
 using Avalonia.Threading;
 using Avalonia.VisualTree;
 
@@ -34,14 +37,24 @@ namespace Avalonia.Controls
                 (o, v) => o.SelectedItem = v);
 
         /// <summary>
-        /// Defines the <see cref="SelectedItemChanged"/> event.
+        /// Defines the <see cref="SelectedItems"/> property.
         /// </summary>
-        public static readonly RoutedEvent<SelectionChangedEventArgs> SelectedItemChangedEvent =
-            RoutedEvent.Register<TreeView, SelectionChangedEventArgs>(
-                "SelectedItemChanged",
-                RoutingStrategies.Bubble);
+        public static readonly DirectProperty<TreeView, IList> SelectedItemsProperty =
+            AvaloniaProperty.RegisterDirect<TreeView, IList>(
+                nameof(SelectedItems),
+                o => o.SelectedItems,
+                (o, v) => o.SelectedItems = v);
 
+        /// <summary>
+        /// Defines the <see cref="SelectionMode"/> property.
+        /// </summary>
+        protected static readonly StyledProperty<SelectionMode> SelectionModeProperty =
+            AvaloniaProperty.Register<SelectingItemsControl, SelectionMode>(
+                nameof(SelectionMode));
+
+        private static readonly IList Empty = new object[0];
         private object _selectedItem;
+        private IList _selectedItems;
 
         /// <summary>
         /// Initializes static members of the <see cref="TreeView"/> class.
@@ -54,16 +67,16 @@ namespace Avalonia.Controls
         /// <summary>
         /// Occurs when the control's selection changes.
         /// </summary>
-        public event EventHandler<SelectionChangedEventArgs> SelectedItemChanged
+        public event EventHandler<SelectionChangedEventArgs> SelectionChanged
         {
-            add { AddHandler(SelectedItemChangedEvent, value); }
-            remove { RemoveHandler(SelectedItemChangedEvent, value); }
+            add => AddHandler(SelectingItemsControl.SelectionChangedEvent, value);
+            remove => RemoveHandler(SelectingItemsControl.SelectionChangedEvent, value);
         }
 
         /// <summary>
         /// Gets the <see cref="ITreeItemContainerGenerator"/> for the tree view.
         /// </summary>
-        public new ITreeItemContainerGenerator ItemContainerGenerator => 
+        public new ITreeItemContainerGenerator ItemContainerGenerator =>
             (ITreeItemContainerGenerator)base.ItemContainerGenerator;
 
         /// <summary>
@@ -71,67 +84,258 @@ namespace Avalonia.Controls
         /// </summary>
         public bool AutoScrollToSelectedItem
         {
-            get { return GetValue(AutoScrollToSelectedItemProperty); }
-            set { SetValue(AutoScrollToSelectedItemProperty, value); }
+            get => GetValue(AutoScrollToSelectedItemProperty);
+            set => SetValue(AutoScrollToSelectedItemProperty, value);
+        }
+
+        private bool _syncingSelectedItems;
+
+        /// <summary>
+        /// Gets or sets the selection mode.
+        /// </summary>
+        public SelectionMode SelectionMode
+        {
+            get => GetValue(SelectionModeProperty);
+            set => SetValue(SelectionModeProperty, value);
         }
 
         /// <summary>
         /// Gets or sets the selected item.
         /// </summary>
         public object SelectedItem
+        {
+            get => _selectedItem;
+            set
+            {
+                SetAndRaise(SelectedItemProperty, ref _selectedItem,
+                    (object val, ref object backing, Action<Action> notifyWrapper) =>
+                    {
+                        var old = backing;
+                        backing = val;
+
+                        notifyWrapper(() =>
+                            RaisePropertyChanged(
+                                SelectedItemProperty,
+                                old,
+                                val));
+
+                        if (val != null)
+                        {
+                            if (SelectedItems.Count != 1 || SelectedItems[0] != val)
+                            {
+                                _syncingSelectedItems = true;
+                                SelectSingleItem(val);
+                                _syncingSelectedItems = false;
+                            }
+                        }
+                        else if (SelectedItems.Count > 0)
+                        {
+                            SelectedItems.Clear();
+                        }
+                    }, value);
+            }
+        }
+
+        /// <summary>
+        /// Gets the selected items.
+        /// </summary>
+        public IList SelectedItems
         {
             get
             {
-                return _selectedItem;
+                if (_selectedItems == null)
+                {
+                    _selectedItems = new AvaloniaList<object>();
+                    SubscribeToSelectedItems();
+                }
+
+                return _selectedItems;
             }
 
             set
             {
-                if (_selectedItem != null)
+                if (value?.IsFixedSize == true || value?.IsReadOnly == true)
                 {
-                    var container = ItemContainerGenerator.Index.ContainerFromItem(_selectedItem);
-                    MarkContainerSelected(container, false);
+                    throw new NotSupportedException(
+                        "Cannot use a fixed size or read-only collection as SelectedItems.");
                 }
 
-                var oldItem = _selectedItem;
-                SetAndRaise(SelectedItemProperty, ref _selectedItem, value);
+                UnsubscribeFromSelectedItems();
+                _selectedItems = value ?? new AvaloniaList<object>();
+                SubscribeToSelectedItems();
+            }
+        }
 
-                if (_selectedItem != null)
-                {
-                    var container = ItemContainerGenerator.Index.ContainerFromItem(_selectedItem);
-                    MarkContainerSelected(container, true);
+        /// <summary>
+        /// Subscribes to the <see cref="SelectedItems"/> CollectionChanged event, if any.
+        /// </summary>
+        private void SubscribeToSelectedItems()
+        {
+            if (_selectedItems is INotifyCollectionChanged incc)
+            {
+                incc.CollectionChanged += SelectedItemsCollectionChanged;
+            }
+
+            SelectedItemsCollectionChanged(
+                _selectedItems,
+                new NotifyCollectionChangedEventArgs(NotifyCollectionChangedAction.Reset));
+        }
+
+        private void SelectSingleItem(object item)
+        {
+            SelectedItems.Clear();
+            SelectedItems.Add(item);
+        }
+
+        /// <summary>
+        /// Called when the <see cref="SelectedItems"/> CollectionChanged event is raised.
+        /// </summary>
+        /// <param name="sender">The event sender.</param>
+        /// <param name="e">The event args.</param>
+        private void SelectedItemsCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            IList added = null;
+            IList removed = null;
 
-                    if (AutoScrollToSelectedItem && container != null)
+            switch (e.Action)
+            {
+                case NotifyCollectionChangedAction.Add:
+
+                    SelectedItemsAdded(e.NewItems.Cast<object>().ToArray());
+
+                    if (AutoScrollToSelectedItem)
                     {
-                        container.BringIntoView();
+                        var container = (TreeViewItem)ItemContainerGenerator.Index.ContainerFromItem(e.NewItems[0]);
+
+                        container?.BringIntoView();
                     }
-                }
 
-                if (oldItem != _selectedItem)
-                {
-                    // Fire the SelectionChanged event
-                    List<object> removed = new List<object>();
-                    if (oldItem != null)
+                    added = e.NewItems;
+
+                    break;
+                case NotifyCollectionChangedAction.Remove:
+
+                    if (!_syncingSelectedItems)
                     {
-                        removed.Add(oldItem);
+                        if (SelectedItems.Count == 0)
+                        {
+                            SelectedItem = null;
+                        }
+                        else
+                        {
+                            var selectedIndex = SelectedItems.IndexOf(_selectedItem);
+
+                            if (selectedIndex == -1)
+                            {
+                                var old = _selectedItem;
+                                _selectedItem = SelectedItems[0];
+
+                                RaisePropertyChanged(SelectedItemProperty, old, _selectedItem);
+                            }
+                        }
                     }
 
-                    List<object> added = new List<object>();
-                    if (_selectedItem != null)
+                    foreach (var item in e.OldItems)
                     {
-                        added.Add(_selectedItem);
+                        MarkItemSelected(item, false);
                     }
 
-                    var changed = new SelectionChangedEventArgs(
-                        SelectedItemChangedEvent,
-                        added,
-                        removed);
-                    RaiseEvent(changed);
-                }
+                    removed = e.OldItems;
+
+                    break;
+                case NotifyCollectionChangedAction.Reset:
+
+                    foreach (IControl container in ItemContainerGenerator.Index.Containers)
+                    {
+                        MarkContainerSelected(container, false);
+                    }
+
+                    if (SelectedItems.Count > 0)
+                    {
+                        SelectedItemsAdded(SelectedItems);
+
+                        added = SelectedItems;
+                    }
+                    else if (!_syncingSelectedItems)
+                    {
+                        SelectedItem = null;
+                    }
+
+                    break;
+                case NotifyCollectionChangedAction.Replace:
+
+                    foreach (var item in e.OldItems)
+                    {
+                        MarkItemSelected(item, false);
+                    }
+
+                    foreach (var item in e.NewItems)
+                    {
+                        MarkItemSelected(item, true);
+                    }
+
+                    if (SelectedItem != SelectedItems[0] && !_syncingSelectedItems)
+                    {
+                        var oldItem = SelectedItem;
+                        var item = SelectedItems[0];
+                        _selectedItem = item;
+                        RaisePropertyChanged(SelectedItemProperty, oldItem, item);
+                    }
+
+                    added = e.NewItems;
+                    removed = e.OldItems;
+
+                    break;
+            }
+
+            if (added?.Count > 0 || removed?.Count > 0)
+            {
+                var changed = new SelectionChangedEventArgs(
+                    SelectingItemsControl.SelectionChangedEvent,
+                    added ?? Empty,
+                    removed ?? Empty);
+                RaiseEvent(changed);
+            }
+        }
+
+        private void MarkItemSelected(object item, bool selected)
+        {
+            var container = ItemContainerGenerator.Index.ContainerFromItem(item);
+
+            MarkContainerSelected(container, selected);
+        }
+
+        private void SelectedItemsAdded(IList items)
+        {
+            if (items.Count == 0)
+            {
+                return;
+            }
+
+            foreach (object item in items)
+            {
+                MarkItemSelected(item, true);
+            }
+
+            if (SelectedItem == null && !_syncingSelectedItems)
+            {
+                SetAndRaise(SelectedItemProperty, ref _selectedItem, items[0]);
+            }
+        }
+
+        /// <summary>
+        /// Unsubscribes from the <see cref="SelectedItems"/> CollectionChanged event, if any.
+        /// </summary>
+        private void UnsubscribeFromSelectedItems()
+        {
+            if (_selectedItems is INotifyCollectionChanged incc)
+            {
+                incc.CollectionChanged -= SelectedItemsCollectionChanged;
             }
         }
 
-        (bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element, NavigationDirection direction)
+        (bool handled, IInputElement next) ICustomKeyboardNavigation.GetNext(IInputElement element,
+            NavigationDirection direction)
         {
             if (direction == NavigationDirection.Next || direction == NavigationDirection.Previous)
             {
@@ -142,10 +346,8 @@ namespace Avalonia.Controls
                         ItemContainerGenerator.ContainerFromIndex(0);
                     return (true, result);
                 }
-                else
-                {
-                    return (true, null);
-                }
+
+                return (true, null);
             }
 
             return (false, null);
@@ -186,7 +388,7 @@ namespace Avalonia.Controls
                 if (SelectedItem != null)
                 {
                     var next = GetContainerInDirection(
-                        GetContainerFromEventSource(e.Source) as TreeViewItem,
+                        GetContainerFromEventSource(e.Source),
                         direction.Value,
                         true);
 
@@ -201,6 +403,18 @@ namespace Avalonia.Controls
                     SelectedItem = ElementAt(Items, 0);
                 }
             }
+
+            if (!e.Handled)
+            {
+                var keymap = AvaloniaLocator.Current.GetService<PlatformHotkeyConfiguration>();
+                bool Match(List<KeyGesture> gestures) => gestures.Any(g => g.Matches(e));
+
+                if (this.SelectionMode == SelectionMode.Multiple && Match(keymap.SelectAll))
+                {
+                    SelectingItemsControl.SynchronizeItems(SelectedItems, ItemContainerGenerator.Index.Items);
+                    e.Handled = true;
+                }
+            }
         }
 
         private TreeViewItem GetContainerInDirection(
@@ -208,17 +422,9 @@ namespace Avalonia.Controls
             NavigationDirection direction,
             bool intoChildren)
         {
-            IItemContainerGenerator parentGenerator;
+            IItemContainerGenerator parentGenerator = GetParentContainerGenerator(from);
 
-            if (from?.Parent is TreeView treeView)
-            {
-                parentGenerator = treeView.ItemContainerGenerator;
-            }
-            else if (from?.Parent is TreeViewItem item)
-            {
-                parentGenerator = item.ItemContainerGenerator;
-            }
-            else
+            if (parentGenerator == null)
             {
                 return null;
             }
@@ -257,6 +463,7 @@ namespace Avalonia.Controls
                     {
                         return GetContainerInDirection(parentItem, direction, false);
                     }
+
                     break;
             }
 
@@ -293,18 +500,182 @@ namespace Avalonia.Controls
         {
             var item = ItemContainerGenerator.Index.ItemFromContainer(container);
 
-            if (item != null)
+            if (item == null)
             {
-                if (SelectedItem != null)
+                return;
+            }
+
+            IControl selectedContainer = null;
+
+            if (SelectedItem != null)
+            {
+                selectedContainer = ItemContainerGenerator.Index.ContainerFromItem(SelectedItem);
+            }
+
+            var mode = SelectionMode;
+            var toggle = toggleModifier || (mode & SelectionMode.Toggle) != 0;
+            var multi = (mode & SelectionMode.Multiple) != 0;
+            var range = multi && selectedContainer != null && rangeModifier;
+
+            if (!toggle && !range)
+            {
+                SelectSingleItem(item);
+            }
+            else if (multi && range)
+            {
+                SelectingItemsControl.SynchronizeItems(
+                    SelectedItems,
+                    GetItemsInRange(selectedContainer as TreeViewItem, container as TreeViewItem));
+            }
+            else
+            {
+                var i = SelectedItems.IndexOf(item);
+
+                if (i != -1)
+                {
+                    SelectedItems.Remove(item);
+                }
+                else
+                {
+                    if (multi)
+                    {
+                        SelectedItems.Add(item);
+                    }
+                    else
+                    {
+                        SelectedItem = item;
+                    }
+                }
+            }
+        }
+
+        private static IItemContainerGenerator GetParentContainerGenerator(TreeViewItem item)
+        {
+            if (item == null)
+            {
+                return null;
+            }
+
+            switch (item.Parent)
+            {
+                case TreeView treeView:
+                    return treeView.ItemContainerGenerator;
+                case TreeViewItem treeViewItem:
+                    return treeViewItem.ItemContainerGenerator;
+                default:
+                    return null;
+            }
+        }
+
+        /// <summary>
+        /// Find which node is first in hierarchy.
+        /// </summary>
+        /// <param name="treeView">Search root.</param>
+        /// <param name="nodeA">Nodes to find.</param>
+        /// <param name="nodeB">Node to find.</param>
+        /// <returns>Found first node.</returns>
+        private static TreeViewItem FindFirstNode(TreeView treeView, TreeViewItem nodeA, TreeViewItem nodeB)
+        {
+            return FindInContainers(treeView.ItemContainerGenerator, nodeA, nodeB);
+        }
+
+        private static TreeViewItem FindInContainers(ITreeItemContainerGenerator containerGenerator,
+            TreeViewItem nodeA,
+            TreeViewItem nodeB)
+        {
+            IEnumerable<ItemContainerInfo> containers = containerGenerator.Containers;
+
+            foreach (ItemContainerInfo container in containers)
+            {
+                TreeViewItem node = FindFirstNode(container.ContainerControl as TreeViewItem, nodeA, nodeB);
+
+                if (node != null)
+                {
+                    return node;
+                }
+            }
+
+            return null;
+        }
+
+        private static TreeViewItem FindFirstNode(TreeViewItem node, TreeViewItem nodeA, TreeViewItem nodeB)
+        {
+            if (node == null)
+            {
+                return null;
+            }
+
+            TreeViewItem match = node == nodeA ? nodeA : node == nodeB ? nodeB : null;
+
+            if (match != null)
+            {
+                return match;
+            }
+
+            return FindInContainers(node.ItemContainerGenerator, nodeA, nodeB);
+        }
+
+        /// <summary>
+        /// Returns all items that belong to containers between <paramref name="from"/> and <paramref name="to"/>.
+        /// The range is inclusive.
+        /// </summary>
+        /// <param name="from">From container.</param>
+        /// <param name="to">To container.</param>
+        private List<object> GetItemsInRange(TreeViewItem from, TreeViewItem to)
+        {
+            var items = new List<object>();
+
+            if (from == null || to == null)
+            {
+                return items;
+            }
+
+            TreeViewItem firstItem = FindFirstNode(this, from, to);
+
+            if (firstItem == null)
+            {
+                return items;
+            }
+
+            bool wasReversed = false;
+
+            if (firstItem == to)
+            {
+                var temp = from;
+
+                from = to;
+                to = temp;
+
+                wasReversed = true;
+            }
+
+            TreeViewItem node = from;
+
+            while (node != to)
+            {
+                var item = ItemContainerGenerator.Index.ItemFromContainer(node);
+
+                if (item != null)
                 {
-                    var old = ItemContainerGenerator.Index.ContainerFromItem(SelectedItem);
-                    MarkContainerSelected(old, false);
+                    items.Add(item);
                 }
 
-                SelectedItem = item;
+                node = GetContainerInDirection(node, NavigationDirection.Down, true);
+            }
+
+            var toItem = ItemContainerGenerator.Index.ItemFromContainer(to);
 
-                MarkContainerSelected(container, true);
+            if (toItem != null)
+            {
+                items.Add(toItem);
+            }
+
+            if (wasReversed)
+            {
+                items.Reverse();
             }
+
+            return items;
         }
 
         /// <summary>
@@ -341,7 +712,7 @@ namespace Avalonia.Controls
         /// </summary>
         /// <param name="eventSource">The control that raised the event.</param>
         /// <returns>The container or null if the event did not originate in a container.</returns>
-        protected IControl GetContainerFromEventSource(IInteractive eventSource)
+        protected TreeViewItem GetContainerFromEventSource(IInteractive eventSource)
         {
             var item = ((IVisual)eventSource).GetSelfAndVisualAncestors()
                 .OfType<TreeViewItem>()
@@ -349,7 +720,7 @@ namespace Avalonia.Controls
 
             if (item != null)
             {
-                if (item.ItemContainerGenerator.Index == this.ItemContainerGenerator.Index)
+                if (item.ItemContainerGenerator.Index == ItemContainerGenerator.Index)
                 {
                     return item;
                 }
@@ -367,21 +738,23 @@ namespace Avalonia.Controls
         {
             var selectedItem = SelectedItem;
 
-            if (selectedItem != null)
+            if (selectedItem == null)
             {
-                foreach (var container in e.Containers)
-                {
-                    if (container.Item == selectedItem)
-                    {
-                        ((TreeViewItem)container.ContainerControl).IsSelected = true;
+                return;
+            }
 
-                        if (AutoScrollToSelectedItem)
-                        {
-                            Dispatcher.UIThread.Post(container.ContainerControl.BringIntoView);
-                        }
+            foreach (var container in e.Containers)
+            {
+                if (container.Item == selectedItem)
+                {
+                    ((TreeViewItem)container.ContainerControl).IsSelected = true;
 
-                        break;
+                    if (AutoScrollToSelectedItem)
+                    {
+                        Dispatcher.UIThread.Post(container.ContainerControl.BringIntoView);
                     }
+
+                    break;
                 }
             }
         }
@@ -393,18 +766,18 @@ namespace Avalonia.Controls
         /// <param name="selected">Whether the control is selected</param>
         private void MarkContainerSelected(IControl container, bool selected)
         {
-            if (container != null)
+            if (container == null)
             {
-                var selectable = container as ISelectable;
+                return;
+            }
 
-                if (selectable != null)
-                {
-                    selectable.IsSelected = selected;
-                }
-                else
-                {
-                    ((IPseudoClasses)container.Classes).Set(":selected", selected);
-                }
+            if (container is ISelectable selectable)
+            {
+                selectable.IsSelected = selected;
+            }
+            else
+            {
+                container.Classes.Set(":selected", selected);
             }
         }
     }

+ 2 - 0
src/Avalonia.ReactiveUI/RoutedViewHost.cs

@@ -163,6 +163,8 @@ namespace Avalonia
     
             this.Log().Info($"Ready to show {view} with autowired {viewModel}.");
             view.ViewModel = viewModel;
+            if (view is IStyledElement styled)
+                styled.DataContext = viewModel;
             UpdateContent(view);
         }
     

+ 1 - 1
src/Avalonia.Remote.Protocol/DefaultMessageTypeResolver.cs

@@ -12,7 +12,7 @@ namespace Avalonia.Remote.Protocol
         public DefaultMessageTypeResolver(params Assembly[] assemblies)
         {
             foreach (var asm in
-                (assemblies ?? new Assembly[0]).Concat(new[]
+                (assemblies ?? Array.Empty<Assembly>()).Concat(new[]
                     {typeof(AvaloniaRemoteMessageGuidAttribute).GetTypeInfo().Assembly}))
             {
                 foreach (var t in asm.ExportedTypes)

+ 2 - 2
src/Avalonia.Remote.Protocol/MetsysBson.cs

@@ -806,7 +806,7 @@ namespace Metsys.Bson
             {
                 return Activator.CreateInstance(typeof(List<>).MakeGenericType(itemType));
             }
-            if (type.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, new Type[0], null) != null)
+            if (type.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, Type.EmptyTypes, null) != null)
             {
                 return Activator.CreateInstance(type);
             }
@@ -853,7 +853,7 @@ namespace Metsys.Bson
                 return (IDictionary)Activator.CreateInstance(typeof(Dictionary<,>).MakeGenericType(keyType, valueType));
             }
 
-            if (dictionaryType.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, new Type[0], null) != null)
+            if (dictionaryType.GetConstructor(BindingFlags.Instance | BindingFlags.Public, null, Type.EmptyTypes, null) != null)
             {
                 return (IDictionary)Activator.CreateInstance(dictionaryType);
             }

+ 131 - 0
src/Avalonia.Styling/Styling/OrSelector.cs

@@ -0,0 +1,131 @@
+// 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.
+
+using System;
+using System.Collections.Generic;
+
+namespace Avalonia.Styling
+{
+    /// <summary>
+    /// The OR style selector.
+    /// </summary>
+    internal class OrSelector : Selector
+    {
+        private readonly IReadOnlyList<Selector> _selectors;
+        private string _selectorString;
+        private Type _targetType;
+
+        /// <summary>
+        /// Initializes a new instance of the <see cref="OrSelector"/> class.
+        /// </summary>
+        /// <param name="selectors">The selectors to OR.</param>
+        public OrSelector(IReadOnlyList<Selector> selectors)
+        {
+            Contract.Requires<ArgumentNullException>(selectors != null);
+            Contract.Requires<ArgumentException>(selectors.Count > 1);
+
+            _selectors = selectors;
+        }
+
+        /// <inheritdoc/>
+        public override bool InTemplate => false;
+
+        /// <inheritdoc/>
+        public override bool IsCombinator => false;
+
+        /// <inheritdoc/>
+        public override Type TargetType
+        {
+            get
+            {
+                if (_targetType == null)
+                {
+                    _targetType = EvaluateTargetType();
+                }
+
+                return _targetType;
+            }
+        }
+
+        /// <inheritdoc/>
+        public override string ToString()
+        {
+            if (_selectorString == null)
+            {
+                _selectorString = string.Join(", ", _selectors);
+            }
+
+            return _selectorString;
+        }
+
+        protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
+        {
+            var activators = new List<IObservable<bool>>();
+            var neverThisInstance = false;
+
+            foreach (var selector in _selectors)
+            {
+                var match = selector.Match(control, subscribe);
+
+                switch (match.Result)
+                {
+                    case SelectorMatchResult.AlwaysThisType:
+                    case SelectorMatchResult.AlwaysThisInstance:
+                        return match;
+                    case SelectorMatchResult.NeverThisInstance:
+                        neverThisInstance = true;
+                        break;
+                    case SelectorMatchResult.Sometimes:
+                        activators.Add(match.Activator);
+                        break;
+                }
+            }
+
+            if (activators.Count > 1)
+            {
+                return new SelectorMatch(StyleActivator.Or(activators));
+            }
+            else if (activators.Count == 1)
+            {
+                return new SelectorMatch(activators[0]);
+            }
+            else if (neverThisInstance)
+            {
+                return SelectorMatch.NeverThisInstance;
+            }
+            else
+            {
+                return SelectorMatch.NeverThisType;
+            }
+        }
+
+        protected override Selector MovePrevious() => null;
+
+        private Type EvaluateTargetType()
+        {
+            var result = default(Type);
+
+            foreach (var selector in _selectors)
+            {
+                if (selector.TargetType == null)
+                {
+                    return null;
+                }
+                else if (result == null)
+                {
+                    result = selector.TargetType;
+                }
+                else
+                {
+                    while (!result.IsAssignableFrom(selector.TargetType))
+                    {
+                        result = result.BaseType;
+                    }
+                }
+            }
+
+            return result;
+        }
+    }
+}
+

+ 22 - 0
src/Avalonia.Styling/Styling/Selectors.cs

@@ -2,6 +2,8 @@
 // Licensed under the MIT license. See licence.md file in the project root for full license information.
 
 using System;
+using System.Collections.Generic;
+using System.Linq;
 
 namespace Avalonia.Styling
 {
@@ -137,6 +139,26 @@ namespace Avalonia.Styling
             return previous.OfType(typeof(T));
         }
 
+        /// <summary>
+        /// Returns a selector which ORs selectors.
+        /// </summary>
+        /// <param name="selectors">The selectors to be OR'd.</param>
+        /// <returns>The selector.</returns>
+        public static Selector Or(params Selector[] selectors)
+        {
+            return new OrSelector(selectors);
+        }
+
+        /// <summary>
+        /// Returns a selector which ORs selectors.
+        /// </summary>
+        /// <param name="selectors">The selectors to be OR'd.</param>
+        /// <returns>The selector.</returns>
+        public static Selector Or(IReadOnlyList<Selector> selectors)
+        {
+            return new OrSelector(selectors);
+        }
+
         /// <summary>
         /// Returns a selector which matches a control with the specified property value.
         /// </summary>

+ 1 - 7
src/Avalonia.Themes.Default/Separator.xaml

@@ -11,13 +11,7 @@
     </Setter>
   </Style>
 
-  <Style Selector="MenuItem > Separator">
-    <Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}"/>
-    <Setter Property="Margin" Value="29,1,0,1"/>
-    <Setter Property="Height" Value="1"/>
-  </Style>
-
-  <Style Selector="ContextMenu > Separator">
+  <Style Selector="MenuItem > Separator, ContextMenu > Separator">
     <Setter Property="Background" Value="{DynamicResource ThemeControlMidBrush}"/>
     <Setter Property="Margin" Value="29,1,0,1"/>
     <Setter Property="Height" Value="1"/>

+ 15 - 3
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@@ -393,24 +393,36 @@ namespace Avalonia.Rendering
                         }
                         else
                         {
+                            var scale = scene.Scaling;
+
                             foreach (var rect in layer.Dirty)
                             {
+                                var snappedRect = SnapToDevicePixels(rect, scale);
                                 context.Transform = Matrix.Identity;
-                                context.PushClip(rect);
+                                context.PushClip(snappedRect);
                                 context.Clear(Colors.Transparent);
-                                Render(context, node, layer.LayerRoot, rect);
+                                Render(context, node, layer.LayerRoot, snappedRect);
                                 context.PopClip();
 
                                 if (DrawDirtyRects)
                                 {
-                                    _dirtyRectsDisplay.Add(rect);
+                                    _dirtyRectsDisplay.Add(snappedRect);
                                 }
                             }
                         }
                     }
                 }
             }
+        }
 
+        private static Rect SnapToDevicePixels(Rect rect, double scale)
+        {
+            return new Rect(
+                Math.Floor(rect.X * scale) / scale,
+                Math.Floor(rect.Y * scale) / scale,
+                Math.Ceiling(rect.Width * scale) / scale,
+                Math.Ceiling(rect.Height * scale) / scale);
+                
         }
 
         private void RenderOverlay(Scene scene, IDrawingContextImpl parentContent)

+ 2 - 2
src/Avalonia.Visuals/Rendering/RenderLoop.cs

@@ -117,9 +117,9 @@ namespace Avalonia.Rendering
                         }, DispatcherPriority.Render);
                     }
 
-                    foreach (var i in _items)
+                    for(int i = 0; i < _items.Count; i++)
                     {
-                        i.Render();
+                        _items[i].Render();
                     }
                 }
                 catch (Exception ex)

+ 2 - 2
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@@ -17,8 +17,8 @@ namespace Avalonia.Rendering.SceneGraph
     /// </summary>
     internal class VisualNode : IVisualNode
     {
-        private static readonly IReadOnlyList<IVisualNode> EmptyChildren = new IVisualNode[0];
-        private static readonly IReadOnlyList<IRef<IDrawOperation>> EmptyDrawOperations = new IRef<IDrawOperation>[0];
+        private static readonly IReadOnlyList<IVisualNode> EmptyChildren = Array.Empty<IVisualNode>();
+        private static readonly IReadOnlyList<IRef<IDrawOperation>> EmptyDrawOperations = Array.Empty<IRef<IDrawOperation>>();
 
         private Rect? _bounds;
         private double _opacity;

+ 36 - 0
src/Avalonia.Visuals/VisualTree/TransformedBounds.cs

@@ -50,5 +50,41 @@ namespace Avalonia.VisualTree
                 return Bounds.Contains(point);
             }
         }
+
+        public bool Equals(TransformedBounds other)
+        {
+            return Bounds == other.Bounds && Clip == other.Clip && Transform == other.Transform;
+        }
+
+        public override bool Equals(object obj)
+        {
+            if (obj is null)
+            {
+                return false;
+            }
+
+            return obj is TransformedBounds other && Equals(other);
+        }
+
+        public override int GetHashCode()
+        {
+            unchecked
+            {
+                var hashCode = Bounds.GetHashCode();
+                hashCode = (hashCode * 397) ^ Clip.GetHashCode();
+                hashCode = (hashCode * 397) ^ Transform.GetHashCode();
+                return hashCode;
+            }
+        }
+
+        public static bool operator ==(TransformedBounds left, TransformedBounds right)
+        {
+            return left.Equals(right);
+        }
+
+        public static bool operator !=(TransformedBounds left, TransformedBounds right)
+        {
+            return !left.Equals(right);
+        }
     }
 }

+ 21 - 9
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs

@@ -49,7 +49,7 @@ namespace Avalonia.Markup.Parsers
                         state = ParseStart(ref r);
                         break;
                     case State.Middle:
-                        state = ParseMiddle(ref r, end);
+                        (state, syntax) = ParseMiddle(ref r, end);
                         break;
                     case State.CanHaveType:
                         state = ParseCanHaveType(ref r);
@@ -113,33 +113,37 @@ namespace Avalonia.Markup.Parsers
             return State.TypeName;
         }
 
-        private static State ParseMiddle(ref CharacterReader r, char? end)
+        private static (State, ISyntax) ParseMiddle(ref CharacterReader r, char? end)
         {
             if (r.TakeIf(':'))
             {
-                return State.Colon;
+                return (State.Colon, null);
             }
             else if (r.TakeIf('.'))
             {
-                return State.Class;
+                return (State.Class, null);
             }
             else if (r.TakeIf(char.IsWhiteSpace) || r.Peek == '>')
             {
-                return State.Traversal;
+                return (State.Traversal, null);
             }
             else if (r.TakeIf('/'))
             {
-                return State.Template;
+                return (State.Template, null);
             }
             else if (r.TakeIf('#'))
             {
-                return State.Name;
+                return (State.Name, null);
+            }
+            else if (r.TakeIf(','))
+            {
+                return (State.Start, new CommaSyntax());
             }
             else if (end.HasValue && !r.End && r.Peek == end.Value)
             {
-                return State.End;
+                return (State.End, null);
             }
-            return State.TypeName;
+            return (State.TypeName, null);
         }
 
         private static State ParseCanHaveType(ref CharacterReader r)
@@ -415,5 +419,13 @@ namespace Avalonia.Markup.Parsers
                 return (obj is NotSyntax not) && Argument.SequenceEqual(not.Argument);
             }
         }
+
+        public class CommaSyntax : ISyntax
+        {
+            public override bool Equals(object obj)
+            {
+                return obj is CommaSyntax or;
+            }
+        }
     }
 }

+ 20 - 0
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs

@@ -43,6 +43,7 @@ namespace Avalonia.Markup.Parsers
         private Selector Create(IEnumerable<SelectorGrammar.ISyntax> syntax)
         {
             var result = default(Selector);
+            var results = default(List<Selector>);
 
             foreach (var i in syntax)
             {
@@ -106,11 +107,30 @@ namespace Avalonia.Markup.Parsers
                     case SelectorGrammar.NotSyntax not:
                         result = result.Not(x => Create(not.Argument));
                         break;
+                    case SelectorGrammar.CommaSyntax comma:
+                        if (results == null)
+                        {
+                            results = new List<Selector>();
+                        }
+
+                        results.Add(result);
+                        result = null;
+                        break;
                     default:
                         throw new NotSupportedException($"Unsupported selector grammar '{i.GetType()}'.");
                 }
             }
 
+            if (results != null)
+            {
+                if (result != null)
+                {
+                    results.Add(result);
+                }
+
+                result = results.Count > 1 ? Selectors.Or(results) : results[0];
+            }
+
             return result;
         }
 

+ 7 - 1
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -501,7 +501,6 @@ namespace Avalonia.Skia
         {
             var paint = new SKPaint
             {
-                IsStroke = false,
                 IsAntialias = true
             };
 
@@ -558,6 +557,13 @@ namespace Avalonia.Skia
         /// <returns></returns>
         private PaintWrapper CreatePaint(Pen pen, Size targetSize)
         {
+            // In Skia 0 thickness means - use hairline rendering
+            // and for us it means - there is nothing rendered.
+            if (pen.Thickness == 0d)
+            {
+                return default;
+            }
+
             var rv = CreatePaint(pen.Brush, targetSize);
             var paint = rv.Paint;
 

+ 1 - 1
src/Windows/Avalonia.Win32/SystemDialogImpl.cs

@@ -21,7 +21,7 @@ namespace Avalonia.Win32
             var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero;
             return Task.Factory.StartNew(() =>
             {
-                var result = new string[0];
+                var result = Array.Empty<string>();
 
                 Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog;
                 Guid iid = UnmanagedMethods.ShellIds.IFileDialog;

+ 257 - 7
tests/Avalonia.Controls.UnitTests/TreeViewTests.cs

@@ -11,7 +11,6 @@ using Avalonia.Data;
 using Avalonia.Data.Core;
 using Avalonia.Input;
 using Avalonia.LogicalTree;
-using Avalonia.Markup.Data;
 using Avalonia.UnitTests;
 using Xunit;
 
@@ -55,7 +54,7 @@ namespace Avalonia.Controls.UnitTests
 
             ApplyTemplates(target);
 
-            var items = target.ItemContainerGenerator.Index.Items
+            var items = target.ItemContainerGenerator.Index.Containers
                 .OfType<TreeViewItem>()
                 .ToList();
 
@@ -140,6 +139,235 @@ namespace Avalonia.Controls.UnitTests
             Assert.True(container.IsSelected);
         }
 
+        [Fact]
+        public void Clicking_WithControlModifier_Selected_Item_Should_Deselect_It()
+        {
+            var tree = CreateTestTreeData();
+            var target = new TreeView
+            {
+                Template = CreateTreeViewTemplate(),
+                Items = tree
+            };
+
+            var visualRoot = new TestRoot();
+            visualRoot.Child = target;
+
+            CreateNodeDataTemplate(target);
+            ApplyTemplates(target);
+
+            var item = tree[0].Children[1].Children[0];
+            var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item);
+
+            Assert.NotNull(container);
+
+            target.SelectedItem = item;
+
+            Assert.True(container.IsSelected);
+
+            container.RaiseEvent(new PointerPressedEventArgs
+            {
+                RoutedEvent = InputElement.PointerPressedEvent,
+                MouseButton = MouseButton.Left,
+                InputModifiers = InputModifiers.Control
+            });
+
+            Assert.Null(target.SelectedItem);
+            Assert.False(container.IsSelected);
+        }
+
+        [Fact]
+        public void Clicking_WithControlModifier_Not_Selected_Item_Should_Select_It()
+        {
+            var tree = CreateTestTreeData();
+            var target = new TreeView
+            {
+                Template = CreateTreeViewTemplate(),
+                Items = tree
+            };
+
+            var visualRoot = new TestRoot();
+            visualRoot.Child = target;
+
+            CreateNodeDataTemplate(target);
+            ApplyTemplates(target);
+
+            var item1 = tree[0].Children[1].Children[0];
+            var container1 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1);
+
+            var item2 = tree[0].Children[1];
+            var container2 = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2);
+
+            Assert.NotNull(container1);
+            Assert.NotNull(container2);
+
+            target.SelectedItem = item1;
+
+            Assert.True(container1.IsSelected);
+
+            container2.RaiseEvent(new PointerPressedEventArgs
+            {
+                RoutedEvent = InputElement.PointerPressedEvent,
+                MouseButton = MouseButton.Left,
+                InputModifiers = InputModifiers.Control
+            });
+
+            Assert.Equal(item2, target.SelectedItem);
+            Assert.False(container1.IsSelected);
+            Assert.True(container2.IsSelected);
+        }
+
+        [Fact]
+        public void Clicking_WithControlModifier_Selected_Item_Should_Deselect_And_Remove_From_SelectedItems()
+        {
+            var tree = CreateTestTreeData();
+            var target = new TreeView
+            {
+                Template = CreateTreeViewTemplate(),
+                Items = tree,
+                SelectionMode = SelectionMode.Multiple
+            };
+
+            var visualRoot = new TestRoot();
+            visualRoot.Child = target;
+
+            CreateNodeDataTemplate(target);
+            ApplyTemplates(target);
+
+            var rootNode = tree[0];
+
+            var item1 = rootNode.Children[0];
+            var item2 = rootNode.Children.Last();
+
+            var item1Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item1);
+            var item2Container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(item2);
+
+            TreeTestHelper.ClickContainer(item1Container, InputModifiers.Control);
+            Assert.True(item1Container.IsSelected);
+
+            TreeTestHelper.ClickContainer(item2Container, InputModifiers.Control);
+            Assert.True(item2Container.IsSelected);
+
+            Assert.Equal(new[] {item1, item2}, target.SelectedItems.OfType<Node>());
+
+            TreeTestHelper.ClickContainer(item1Container, InputModifiers.Control);
+            Assert.False(item1Container.IsSelected);
+
+            Assert.DoesNotContain(item1, target.SelectedItems.OfType<Node>());
+        }
+
+        [Fact]
+        public void Clicking_WithShiftModifier_DownDirection_Should_Select_Range_Of_Items()
+        {
+            var tree = CreateTestTreeData();
+            var target = new TreeView
+            {
+                Template = CreateTreeViewTemplate(),
+                Items = tree,
+                SelectionMode = SelectionMode.Multiple
+            };
+
+            var visualRoot = new TestRoot();
+            visualRoot.Child = target;
+
+            CreateNodeDataTemplate(target);
+            ApplyTemplates(target);
+
+            var rootNode = tree[0];
+
+            var from = rootNode.Children[0];
+            var to = rootNode.Children.Last();
+
+            var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
+            var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
+
+            TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None);
+
+            Assert.True(fromContainer.IsSelected);
+
+            TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift);
+            TreeTestHelper.AssertChildrenSelected(target, rootNode);
+        }
+
+        [Fact]
+        public void Clicking_WithShiftModifier_UpDirection_Should_Select_Range_Of_Items()
+        {
+            var tree = CreateTestTreeData();
+            var target = new TreeView
+            {
+                Template = CreateTreeViewTemplate(),
+                Items = tree,
+                SelectionMode = SelectionMode.Multiple
+            };
+
+            var visualRoot = new TestRoot();
+            visualRoot.Child = target;
+
+            CreateNodeDataTemplate(target);
+            ApplyTemplates(target);
+
+            var rootNode = tree[0];
+
+            var from = rootNode.Children.Last();
+            var to = rootNode.Children[0];
+
+            var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
+            var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
+
+            TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None);
+
+            Assert.True(fromContainer.IsSelected);
+
+            TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift);
+            TreeTestHelper.AssertChildrenSelected(target, rootNode);
+        }
+
+        [Fact]
+        public void Clicking_First_Item_Of_SelectedItems_Should_Select_Only_It()
+        {
+            var tree = CreateTestTreeData();
+            var target = new TreeView
+            {
+                Template = CreateTreeViewTemplate(),
+                Items = tree,
+                SelectionMode = SelectionMode.Multiple
+            };
+
+            var visualRoot = new TestRoot();
+            visualRoot.Child = target;
+
+            CreateNodeDataTemplate(target);
+            ApplyTemplates(target);
+
+            var rootNode = tree[0];
+
+            var from = rootNode.Children.Last();
+            var to = rootNode.Children[0];
+
+            var fromContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(from);
+            var toContainer = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(to);
+
+            TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None);
+
+            TreeTestHelper.ClickContainer(toContainer, InputModifiers.Shift);
+            TreeTestHelper.AssertChildrenSelected(target, rootNode);
+
+            TreeTestHelper.ClickContainer(fromContainer, InputModifiers.None);
+
+            Assert.True(fromContainer.IsSelected);
+
+            foreach (var child in rootNode.Children)
+            {
+                if (child == from)
+                {
+                    continue;
+                }
+
+                var container = (TreeViewItem)target.ItemContainerGenerator.Index.ContainerFromItem(child);
+
+                Assert.False(container.IsSelected);
+            }
+        }
+
         [Fact]
         public void Setting_SelectedItem_Should_Set_Container_Selected()
         {
@@ -166,7 +394,6 @@ namespace Avalonia.Controls.UnitTests
             Assert.True(container.IsSelected);
         }
 
-
         [Fact]
         public void Setting_SelectedItem_Should_Raise_SelectedItemChanged_Event()
         {
@@ -186,7 +413,7 @@ namespace Avalonia.Controls.UnitTests
             var item = tree[0].Children[1].Children[0];
 
             var called = false;
-            target.SelectedItemChanged += (s, e) =>
+            target.SelectionChanged += (s, e) =>
             {
                 Assert.Empty(e.RemovedItems);
                 Assert.Equal(1, e.AddedItems.Count);
@@ -236,11 +463,11 @@ namespace Avalonia.Controls.UnitTests
             CreateNodeDataTemplate(target);
             ApplyTemplates(target);
 
-            Assert.Equal(5, target.ItemContainerGenerator.Index.Items.Count());
+            Assert.Equal(5, target.ItemContainerGenerator.Index.Containers.Count());
 
             tree[0].Children.RemoveAt(1);
 
-            Assert.Equal(3, target.ItemContainerGenerator.Index.Items.Count());
+            Assert.Equal(3, target.ItemContainerGenerator.Index.Containers.Count());
         }
 
         [Fact]
@@ -442,7 +669,7 @@ namespace Avalonia.Controls.UnitTests
                         new Node
                         {
                             Value = "Child3",
-                        },
+                        }
                     }
                 }
             };
@@ -515,6 +742,29 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
+        private static class TreeTestHelper
+        {
+            public static void ClickContainer(IControl container, InputModifiers modifiers)
+            {
+                container.RaiseEvent(new PointerPressedEventArgs
+                {
+                    RoutedEvent = InputElement.PointerPressedEvent,
+                    MouseButton = MouseButton.Left,
+                    InputModifiers = modifiers
+                });
+            }
+
+            public static void AssertChildrenSelected(TreeView treeView, Node rootNode)
+            {
+                foreach (var child in rootNode.Children)
+                {
+                    var container = (TreeViewItem)treeView.ItemContainerGenerator.Index.ContainerFromItem(child);
+
+                    Assert.True(container.IsSelected);
+                }
+            }
+        }
+
         private class Node :  NotifyingBase
         {
             private IAvaloniaList<Node> _children;

+ 16 - 0
tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs

@@ -261,6 +261,22 @@ namespace Avalonia.Markup.UnitTests.Parsers
                 result);
         }
 
+        [Fact]
+        public void OfType_Comma_Is_Class()
+        {
+            var result = SelectorGrammar.Parse("TextBlock, :is(Button).foo");
+
+            Assert.Equal(
+                new SelectorGrammar.ISyntax[]
+                {
+                    new SelectorGrammar.OfTypeSyntax { TypeName = "TextBlock" },
+                    new SelectorGrammar.CommaSyntax(),
+                    new SelectorGrammar.IsSyntax { TypeName = "Button" },
+                    new SelectorGrammar.ClassSyntax { Class = "foo" },
+                },
+                result);
+        }
+
         [Fact]
         public void Namespace_Alone_Fails()
         {

+ 7 - 0
tests/Avalonia.Markup.UnitTests/Parsers/SelectorParserTests.cs

@@ -14,6 +14,13 @@ namespace Avalonia.Markup.UnitTests.Parsers
             var result = target.Parse("TextBlock[IsPointerOver=True]");
         }
 
+        [Fact]
+        public void Parses_Comma_Separated_Selectors()
+        {
+            var target = new SelectorParser((ns, type) => typeof(TextBlock));
+            var result = target.Parse("TextBlock, TextBlock:foo");
+        }
+
         [Fact]
         public void Throws_If_OfType_Type_Not_Found()
         {

+ 11 - 3
tests/Avalonia.ReactiveUI.UnitTests/RoutedViewHostTest.cs

@@ -50,7 +50,7 @@ namespace Avalonia
         }
 
         [Fact]
-        public void RoutedViewHostShouldStayInSyncWithRoutingState() 
+        public void RoutedViewHost_Should_Stay_In_Sync_With_RoutingState() 
         {
             var screen = new ScreenViewModel();
             var defaultContent = new TextBlock();
@@ -71,19 +71,25 @@ namespace Avalonia
             Assert.Equal(typeof(TextBlock), host.Content.GetType());
             Assert.Equal(defaultContent, host.Content);
 
+            var first = new FirstRoutableViewModel();
             screen.Router.Navigate
-                .Execute(new FirstRoutableViewModel())
+                .Execute(first)
                 .Subscribe();
 
             Assert.NotNull(host.Content);
             Assert.Equal(typeof(FirstRoutableView), host.Content.GetType());
+            Assert.Equal(first, ((FirstRoutableView)host.Content).DataContext);
+            Assert.Equal(first, ((FirstRoutableView)host.Content).ViewModel);
 
+            var second = new SecondRoutableViewModel();
             screen.Router.Navigate
-                .Execute(new SecondRoutableViewModel())
+                .Execute(second)
                 .Subscribe();
 
             Assert.NotNull(host.Content);
             Assert.Equal(typeof(SecondRoutableView), host.Content.GetType());
+            Assert.Equal(second, ((SecondRoutableView)host.Content).DataContext);
+            Assert.Equal(second, ((SecondRoutableView)host.Content).ViewModel);
 
             screen.Router.NavigateBack
                 .Execute(Unit.Default)
@@ -91,6 +97,8 @@ namespace Avalonia
 
             Assert.NotNull(host.Content);
             Assert.Equal(typeof(FirstRoutableView), host.Content.GetType());
+            Assert.Equal(first, ((FirstRoutableView)host.Content).DataContext);
+            Assert.Equal(first, ((FirstRoutableView)host.Content).ViewModel);
 
             screen.Router.NavigateBack
                 .Execute(Unit.Default)

+ 20 - 0
tests/Avalonia.RenderTests/Shapes/RectangleTests.cs

@@ -20,6 +20,26 @@ namespace Avalonia.Direct2D1.RenderTests.Shapes
         {
         }
 
+        [Fact]
+        public async Task Rectangle_0px_Stroke()
+        {
+            Decorator target = new Decorator
+            {
+                Padding = new Thickness(8),
+                Width = 200,
+                Height = 200,
+                Child = new Rectangle
+                {
+                    Fill = Brushes.Transparent,
+                    Stroke = Brushes.Black,
+                    StrokeThickness = 0
+                }
+            };
+
+            await RenderToFile(target);
+            CompareImages();
+        }
+
         [Fact]
         public async Task Rectangle_1px_Stroke()
         {

+ 106 - 0
tests/Avalonia.Styling.UnitTests/SelectorTests_Or.cs

@@ -0,0 +1,106 @@
+// 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.
+
+using Xunit;
+
+namespace Avalonia.Styling.UnitTests
+{
+    public class SelectorTests_Or
+    {
+        [Fact]
+        public void Or_Selector_Should_Have_Correct_String_Representation()
+        {
+            var target = Selectors.Or(
+                default(Selector).OfType<Control1>().Class("foo"),
+                default(Selector).OfType<Control2>().Class("bar"));
+
+            Assert.Equal("Control1.foo, Control2.bar", target.ToString());
+        }
+
+        [Fact]
+        public void Or_Selector_Matches_Control_Of_Correct_Type()
+        {
+            var target = Selectors.Or(
+                default(Selector).OfType<Control1>(),
+                default(Selector).OfType<Control2>().Class("bar"));
+            var control = new Control1();
+
+            Assert.Equal(SelectorMatchResult.AlwaysThisType, target.Match(control).Result);
+        }
+
+        [Fact]
+        public void Or_Selector_Matches_Control_Of_Correct_Type_With_Class()
+        {
+            var target = Selectors.Or(
+                default(Selector).OfType<Control1>(),
+                default(Selector).OfType<Control2>().Class("bar"));
+            var control = new Control2();
+
+            Assert.Equal(SelectorMatchResult.Sometimes, target.Match(control).Result);
+        }
+
+        [Fact]
+        public void Or_Selector_Doesnt_Match_Control_Of_Incorrect_Type()
+        {
+            var target = Selectors.Or(
+                default(Selector).OfType<Control1>(),
+                default(Selector).OfType<Control2>().Class("bar"));
+            var control = new Control3();
+
+            Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(control).Result);
+        }
+
+        [Fact]
+        public void Or_Selector_Doesnt_Match_Control_With_Incorrect_Name()
+        {
+            var target = Selectors.Or(
+                default(Selector).OfType<Control1>().Name("foo"),
+                default(Selector).OfType<Control2>().Name("foo"));
+            var control = new Control1 { Name = "bar" };
+
+            Assert.Equal(SelectorMatchResult.NeverThisInstance, target.Match(control).Result);
+        }
+
+        [Fact]
+        public void Returns_Correct_TargetType_When_Types_Same()
+        {
+            var target = Selectors.Or(
+                default(Selector).OfType<Control1>().Class("foo"),
+                default(Selector).OfType<Control1>().Class("bar"));
+
+            Assert.Equal(typeof(Control1), target.TargetType);
+        }
+
+        [Fact]
+        public void Returns_Common_TargetType()
+        {
+            var target = Selectors.Or(
+                default(Selector).OfType<Control1>().Class("foo"),
+                default(Selector).OfType<Control2>().Class("bar"));
+
+            Assert.Equal(typeof(TestControlBase), target.TargetType);
+        }
+
+        [Fact]
+        public void Returns_Null_TargetType_When_A_Selector_Has_No_TargetType()
+        {
+            var target = Selectors.Or(
+                default(Selector).OfType<Control1>().Class("foo"),
+                default(Selector).Class("bar"));
+
+            Assert.Equal(null, target.TargetType);
+        }
+
+        public class Control1 : TestControlBase
+        {
+        }
+
+        public class Control2 : TestControlBase
+        {
+        }
+
+        public class Control3 : TestControlBase
+        {
+        }
+    }
+}

二進制
tests/TestFiles/Direct2D1/Shapes/Rectangle/Rectangle_0px_Stroke.expected.png


二進制
tests/TestFiles/Skia/Shapes/Rectangle/Rectangle_0px_Stroke.expected.png