Browse Source

Merge branch 'master' into feature/ui-automation

Steven Kirk 4 years ago
parent
commit
651fffdfa6
99 changed files with 2255 additions and 376 deletions
  1. 29 21
      azure-pipelines.yml
  2. 2 46
      build.sh
  3. 0 1
      build/SharedVersion.props
  4. 1 1
      global.json
  5. 7 4
      native/Avalonia.Native/src/OSX/window.mm
  6. 1 0
      packages/Avalonia/AvaloniaBuildTasks.targets
  7. 5 6
      readme.md
  8. 4 4
      samples/ControlCatalog/MainWindow.xaml
  9. 1 1
      samples/ControlCatalog/Pages/ButtonPage.xaml
  10. 3 3
      samples/ControlCatalog/Pages/CheckBoxPage.xaml
  11. 10 10
      samples/ControlCatalog/Pages/DialogsPage.xaml
  12. 20 4
      samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml
  13. 11 0
      samples/ControlCatalog/Pages/ListBoxPage.xaml
  14. 3 3
      samples/ControlCatalog/Pages/RadioButtonPage.xaml
  15. 3 3
      samples/ControlCatalog/Pages/ToggleSwitchPage.xaml
  16. 0 6
      src/Avalonia.Animation/Properties/AssemblyInfo.cs
  17. 1 9
      src/Avalonia.Base/Properties/AssemblyInfo.cs
  18. 11 19
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  19. 2 6
      src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs
  20. 3 0
      src/Avalonia.Controls/Button.cs
  21. 7 8
      src/Avalonia.Controls/ContextMenu.cs
  22. 42 1
      src/Avalonia.Controls/ItemsControl.cs
  23. 1 2
      src/Avalonia.Controls/NativeMenuItem.cs
  24. 23 1
      src/Avalonia.Controls/Panel.cs
  25. 5 1
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  26. 40 1
      src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs
  27. 1 1
      src/Avalonia.Controls/Primitives/AccessText.cs
  28. 2 6
      src/Avalonia.Controls/Properties/AssemblyInfo.cs
  29. 10 0
      src/Avalonia.Controls/RepeatButton.cs
  30. 29 2
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  31. 48 1
      src/Avalonia.Controls/TrayIcon.cs
  32. 20 7
      src/Avalonia.Controls/Utils/IEnumerableUtils.cs
  33. 1 1
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs
  34. 1 1
      src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs
  35. 3 1
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs
  36. 3 1
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs
  37. 116 25
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs
  38. 31 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs
  39. 2 1
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs
  40. 1 0
      src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs
  41. 12 5
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml
  42. 11 0
      src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs
  43. 3 3
      src/Avalonia.FreeDesktop/DBusHelper.cs
  44. 134 44
      src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs
  45. 1 1
      src/Avalonia.Headless/HeadlessWindowImpl.cs
  46. 1 1
      src/Avalonia.Input/InputElement.cs
  47. 4 5
      src/Avalonia.Layout/ElementManager.cs
  48. 2 4
      src/Avalonia.Layout/Properties/AssemblyInfo.cs
  49. 26 0
      src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs
  50. 32 0
      src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs
  51. 2 5
      src/Avalonia.Styling/Properties/AssemblyInfo.cs
  52. 56 0
      src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs
  53. 145 0
      src/Avalonia.Styling/Styling/NthChildSelector.cs
  54. 23 0
      src/Avalonia.Styling/Styling/NthLastChildSelector.cs
  55. 16 0
      src/Avalonia.Styling/Styling/Selectors.cs
  56. 2 1
      src/Avalonia.Themes.Default/AutoCompleteBox.xaml
  57. 1 0
      src/Avalonia.Themes.Default/Button.xaml
  58. 1 0
      src/Avalonia.Themes.Default/CheckBox.xaml
  59. 2 1
      src/Avalonia.Themes.Default/ComboBox.xaml
  60. 1 0
      src/Avalonia.Themes.Default/ContextMenu.xaml
  61. 1 1
      src/Avalonia.Themes.Default/FlyoutPresenter.xaml
  62. 1 1
      src/Avalonia.Themes.Default/MenuFlyoutPresenter.xaml
  63. 2 2
      src/Avalonia.Themes.Default/MenuItem.xaml
  64. 10 12
      src/Avalonia.Themes.Default/OverlayPopupHost.xaml
  65. 2 0
      src/Avalonia.Themes.Default/PopupRoot.xaml
  66. 1 0
      src/Avalonia.Themes.Default/RadioButton.xaml
  67. 1 0
      src/Avalonia.Themes.Default/ToggleButton.xaml
  68. 1 0
      src/Avalonia.Themes.Default/ToggleSwitch.xaml
  69. 1 0
      src/Avalonia.Themes.Fluent/Controls/Button.xaml
  70. 1 0
      src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml
  71. 4 0
      src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml
  72. 7 10
      src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml
  73. 1 0
      src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml
  74. 1 0
      src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml
  75. 1 0
      src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml
  76. 1 0
      src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml
  77. 1 8
      src/Avalonia.Visuals/Properties/AssemblyInfo.cs
  78. 17 2
      src/Avalonia.X11/X11Platform.cs
  79. 0 2
      src/Avalonia.X11/X11Window.Ime.cs
  80. 47 0
      src/Avalonia.X11/XEmbedTrayIconImpl.cs
  81. 35 0
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs
  82. 2 5
      src/Markup/Avalonia.Markup.Xaml/Properties/AssemblyInfo.cs
  83. 147 2
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs
  84. 6 0
      src/Markup/Avalonia.Markup/Markup/Parsers/SelectorParser.cs
  85. 2 5
      src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs
  86. 1 1
      src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs
  87. 2 5
      src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs
  88. 4 3
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  89. 0 6
      src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs
  90. 8 0
      src/tools/MicroComGenerator/Program.cs
  91. 14 0
      tests/Avalonia.Controls.UnitTests/ButtonTests.cs
  92. 17 0
      tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs
  93. 53 22
      tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs
  94. 159 0
      tests/Avalonia.Markup.UnitTests/Parsers/SelectorGrammarTests.cs
  95. 196 1
      tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs
  96. 14 8
      tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs
  97. 291 0
      tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs
  98. 220 0
      tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs
  99. 5 2
      tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs

+ 29 - 21
azure-pipelines.yml

@@ -3,19 +3,23 @@ jobs:
   pool:
     vmImage: 'ubuntu-20.04'
   steps:
-  - task: CmdLine@2
-    displayName: 'Install Nuke'
+  - task: UseDotNet@2
+    displayName: 'Use .NET Core SDK 3.1.414'
     inputs:
-      script: |
-         dotnet tool install --global Nuke.GlobalTool --version 0.24.0
+      version: 3.1.414
+
+  - task: UseDotNet@2
+    displayName: 'Use .NET Core SDK 5.0.402'
+    inputs:
+      version: 5.0.402
+
   - task: CmdLine@2
-    displayName: 'Run Nuke'
+    displayName: 'Run Build'
     inputs:
       script: |
-        export PATH="$PATH:$HOME/.dotnet/tools"
         dotnet --info
         printenv
-        nuke --target CiAzureLinux --configuration=Release
+        ./build.sh --target CiAzureLinux --configuration=Release
 
   - task: PublishTestResults@2
     inputs:
@@ -23,6 +27,7 @@ jobs:
       testResultsFiles: '$(Build.SourcesDirectory)/artifacts/test-results/*.trx'
     condition: not(canceled())
      
+
 - job: macOS
   variables:
     SolutionDir: '$(Build.SourcesDirectory)'
@@ -30,10 +35,15 @@ jobs:
     vmImage: 'macOS-10.15'
   steps:
   - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 3.1.401'
+    displayName: 'Use .NET Core SDK 3.1.414'
     inputs:
-      version: 3.1.401
+      version: 3.1.414
 
+  - task: UseDotNet@2
+    displayName: 'Use .NET Core SDK 5.0.402'
+    inputs:
+      version: 5.0.402
+      
   - task: CmdLine@2
     displayName: 'Install Mono 5.18'
     inputs:
@@ -45,6 +55,7 @@ jobs:
     displayName: 'Generate avalonia-native'
     inputs:
       script: |
+        export PATH="`pwd`/sdk:$PATH"
         cd src/tools/MicroComGenerator; dotnet run -i ../../Avalonia.Native/avn.idl --cpp ../../../native/Avalonia.Native/inc/avalonia-native.h
 
   - task: Xcode@5
@@ -58,13 +69,7 @@ jobs:
       args: '-derivedDataPath ./'
 
   - task: CmdLine@2
-    displayName: 'Install Nuke'
-    inputs:
-      script: |
-       dotnet tool install --global Nuke.GlobalTool --version 0.24.0
-
-  - task: CmdLine@2
-    displayName: 'Run Nuke'
+    displayName: 'Run Build'
     inputs:
       script: |
         export COREHOST_TRACE=0
@@ -72,10 +77,8 @@ jobs:
         export DOTNET_CLI_TELEMETRY_OPTOUT=1
         which dotnet
         dotnet --info
-        export PATH="$PATH:$HOME/.dotnet/tools"
-        dotnet --info
         printenv
-        nuke --target CiAzureOSX --configuration Release --skip-previewer
+        ./build.sh --target CiAzureOSX --configuration Release --skip-previewer
 
   - task: PublishTestResults@2
     inputs:
@@ -102,9 +105,14 @@ jobs:
     SolutionDir: '$(Build.SourcesDirectory)'
   steps:
   - task: UseDotNet@2
-    displayName: 'Use .NET Core SDK 3.1.401'
+    displayName: 'Use .NET Core SDK 3.1.414'
+    inputs:
+      version: 3.1.414
+
+  - task: UseDotNet@2
+    displayName: 'Use .NET Core SDK 5.0.402'
     inputs:
-      version: 3.1.401
+      version: 5.0.402
 
   - task: CmdLine@2
     displayName: 'Install Nuke'

+ 2 - 46
build.sh

@@ -20,55 +20,11 @@ SCRIPT_DIR=$(cd "$( dirname "${BASH_SOURCE[0]}" )" && pwd)
 ###########################################################################
 
 BUILD_PROJECT_FILE="$SCRIPT_DIR/nukebuild/_build.csproj"
-TEMP_DIRECTORY="$SCRIPT_DIR//.tmp"
-
-DOTNET_GLOBAL_FILE="$SCRIPT_DIR//global.json"
-DOTNET_INSTALL_URL="https://raw.githubusercontent.com/dotnet/cli/master/scripts/obtain/dotnet-install.sh"
-DOTNET_CHANNEL="Current"
 
 export DOTNET_CLI_TELEMETRY_OPTOUT=1
 export DOTNET_SKIP_FIRST_TIME_EXPERIENCE=1
 export NUGET_XMLDOC_MODE="skip"
 
-###########################################################################
-# EXECUTION
-###########################################################################
-
-function FirstJsonValue {
-    perl -nle 'print $1 if m{"'$1'": "([^"\-]+)",?}' <<< ${@:2}
-}
-
-# If global.json exists, load expected version
-if [ -f "$DOTNET_GLOBAL_FILE" ]; then
-    DOTNET_VERSION=$(FirstJsonValue "version" $(cat "$DOTNET_GLOBAL_FILE"))
-    if [ "$DOTNET_VERSION" == ""  ]; then
-        unset DOTNET_VERSION
-    fi
-fi
-
-# If dotnet is installed locally, and expected version is not set or installation matches the expected version
-if [[ -x "$(command -v dotnet)" && (-z ${DOTNET_VERSION+x} || $(dotnet --version) == "$DOTNET_VERSION") || "$SKIP_DOTNET_DOWNLOAD" == "1" ]]; then
-    export DOTNET_EXE="$(command -v dotnet)"
-else
-    DOTNET_DIRECTORY="$TEMP_DIRECTORY/dotnet-unix"
-    export DOTNET_EXE="$DOTNET_DIRECTORY/dotnet"
-    
-    # Download install script
-    DOTNET_INSTALL_FILE="$TEMP_DIRECTORY/dotnet-install.sh"
-    mkdir -p "$TEMP_DIRECTORY"
-    curl -Lsfo "$DOTNET_INSTALL_FILE" "$DOTNET_INSTALL_URL"
-    chmod +x "$DOTNET_INSTALL_FILE"
-    
-    # Install by channel or version
-    if [ -z ${DOTNET_VERSION+x} ]; then
-        "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --channel "$DOTNET_CHANNEL" --no-path
-    else
-        "$DOTNET_INSTALL_FILE" --install-dir "$DOTNET_DIRECTORY" --version "$DOTNET_VERSION" --no-path
-    fi
-fi
-
-export PATH=$DOTNET_DIRECTORY:$PATH
-
-echo "Microsoft (R) .NET Core SDK version $("$DOTNET_EXE" --version)"
+dotnet --info
 
-"$DOTNET_EXE" run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]}
+dotnet run --project "$BUILD_PROJECT_FILE" -- ${BUILD_ARGUMENTS[@]}

+ 0 - 1
build/SharedVersion.props

@@ -17,7 +17,6 @@
     <RepositoryType>git</RepositoryType>
     <AssemblyOriginatorKeyFile>$(MSBuildThisFileDirectory)\avalonia.snk</AssemblyOriginatorKeyFile>
     <SignAssembly>true</SignAssembly>
-    <DefineConstants Condition="$(SignAssembly) == true">$(DefineConstants);SIGNED_BUILD</DefineConstants>
   </PropertyGroup>
 
   <ItemGroup Label="PackageIcon">

+ 1 - 1
global.json

@@ -1,6 +1,6 @@
 {
 	"sdk": {
-		"version": "3.1.401"
+		"version": "5.0.402"
 	},
     "msbuild-sdks": {
         "Microsoft.Build.Traversal": "1.0.43",

+ 7 - 4
native/Avalonia.Native/src/OSX/window.mm

@@ -63,7 +63,6 @@ public:
         [Window setBackingType:NSBackingStoreBuffered];
         
         [Window setOpaque:false];
-        [Window setContentView: StandardContainer];
     }
     
     virtual HRESULT ObtainNSWindowHandle(void** ret) override
@@ -146,6 +145,8 @@ public:
             SetPosition(lastPositionSet);
             UpdateStyle();
             
+            [Window setContentView: StandardContainer];
+            
             [Window setTitle:_lastTitle];
             
             if(ShouldTakeFocusOnShow() && activate)
@@ -344,6 +345,7 @@ public:
                     BaseEvents->Resized(AvnSize{x,y}, reason);
                 }
                 
+                [StandardContainer setFrameSize:NSSize{x,y}];
                 [Window setContentSize:NSSize{x, y}];
             }
             @finally
@@ -2429,7 +2431,10 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
 
 - (void)sendEvent:(NSEvent *)event
 {
-    if(_parent != nullptr)
+    [super sendEvent:event];
+    
+    /// This is to detect non-client clicks. This can only be done on Windows... not popups, hence the dynamic_cast.
+    if(_parent != nullptr && dynamic_cast<WindowImpl*>(_parent.getRaw()) != nullptr)
     {
         switch(event.type)
         {
@@ -2459,8 +2464,6 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
                 break;
         }
     }
-    
-    [super sendEvent:event];
 }
 
 - (BOOL)isAccessibilityElement

+ 1 - 0
packages/Avalonia/AvaloniaBuildTasks.targets

@@ -54,6 +54,7 @@
       <Output TaskParameter="HashResult" PropertyName="AvaloniaResourcesDependencyHash" />
     </Hash>
 
+    <MakeDir Directories="$(IntermediateOutputPath)/Avalonia" />
     <WriteLinesToFile Overwrite="true" File="$(IntermediateOutputPath)/Avalonia/Resources.Inputs.cache" Lines="$(AvaloniaResourcesDependencyHash)" WriteOnlyWhenDifferent="True" />
   </Target>
   

+ 5 - 6
readme.md

@@ -60,6 +60,9 @@ See the [build instructions here](Documentation/build.md).
 
 ## Contributing
 
+This project exists thanks to all the people who contribute.
+<a href="https://github.com/AvaloniaUI/Avalonia/graphs/contributors"><img src="https://opencollective.com/Avalonia/contributors.svg?width=890&button=false" /></a>
+
 Please read the [contribution guidelines](CONTRIBUTING.md) before submitting a pull request.
 
 ## Code of Conduct
@@ -71,11 +74,6 @@ For more information see the [.NET Foundation Code of Conduct](https://dotnetfou
 
 Avalonia is licenced under the [MIT licence](licence.md).
 
-## Contributors
-
-This project exists thanks to all the people who contribute. [[Contribute](https://avaloniaui.net/contributing)].
-<a href="https://github.com/AvaloniaUI/Avalonia/graphs/contributors"><img src="https://opencollective.com/Avalonia/contributors.svg?width=890&button=false" /></a>
-
 ### Backers
 
 Thank you to all our backers! 🙏 [[Become a backer](https://opencollective.com/Avalonia#backer)]
@@ -95,7 +93,8 @@ Support this project by becoming a sponsor. Your logo will show up here with a l
 <a href="https://opencollective.com/Avalonia/sponsor/6/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/6/avatar.svg"></a>
 <a href="https://opencollective.com/Avalonia/sponsor/7/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/7/avatar.svg"></a>
 <a href="https://opencollective.com/Avalonia/sponsor/8/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/8/avatar.svg"></a>
-<a href="https://opencollective.com/Avalonia/sponsor/9/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/9/avatar.svg"></a> 
+<a href="https://opencollective.com/Avalonia/sponsor/9/website" target="_blank"><img src="https://opencollective.com/Avalonia/sponsor/9/avatar.svg"></a>
+<a href="https://baseheadinc.com/" target="_blank"><img height="50" src="https://baseheadinc.com/wp-content/uploads/2020/09/BH-Logo-for-Site-Header-New.png"></a>
 
 ## .NET Foundation
 

+ 4 - 4
samples/ControlCatalog/MainWindow.xaml

@@ -63,11 +63,11 @@
     <Panel Margin="{Binding #MainWindow.OffScreenMargin}">
       <DockPanel LastChildFill="True" Margin="{Binding #MainWindow.WindowDecorationMargin}">
         <Menu Name="MainMenu" DockPanel.Dock="Top">
-          <MenuItem Header="File">
-            <MenuItem Header="Exit" Command="{Binding ExitCommand}" />
+          <MenuItem Header="_File">
+            <MenuItem Header="E_xit" Command="{Binding ExitCommand}" />
           </MenuItem>
-          <MenuItem Header="Help">
-            <MenuItem Header="About" Command="{Binding AboutCommand}" />
+          <MenuItem Header="_Help">
+            <MenuItem Header="_About" Command="{Binding AboutCommand}" />
           </MenuItem>
         </Menu>
         <local:MainView />

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

@@ -10,7 +10,7 @@
                 HorizontalAlignment="Center"
                 Spacing="16">
       <StackPanel Orientation="Vertical" Spacing="8" Width="200">
-        <Button>Standard XAML Button</Button>
+        <Button>Standard _XAML Button</Button>
         <Button Foreground="White">Foreground</Button>
         <Button Background="{DynamicResource SystemAccentColor}">Background</Button>
         <Button IsEnabled="False">Disabled</Button>

+ 3 - 3
samples/ControlCatalog/Pages/CheckBoxPage.xaml

@@ -11,9 +11,9 @@
                 Spacing="16">
       <StackPanel Orientation="Vertical"
                   Spacing="16">
-        <CheckBox>Unchecked</CheckBox>
-        <CheckBox IsChecked="True">Checked</CheckBox>
-        <CheckBox IsChecked="{x:Null}">Indeterminate</CheckBox>
+        <CheckBox>_Unchecked</CheckBox>
+        <CheckBox IsChecked="True">_Checked</CheckBox>
+        <CheckBox IsChecked="{x:Null}">_Indeterminate</CheckBox>
         <CheckBox IsChecked="True" IsEnabled="False">Disabled</CheckBox>
       </StackPanel>
       <StackPanel Orientation="Vertical"

+ 10 - 10
samples/ControlCatalog/Pages/DialogsPage.xaml

@@ -3,15 +3,15 @@
              x:Class="ControlCatalog.Pages.DialogsPage">
   <StackPanel Orientation="Vertical" Spacing="4" Margin="4">
       <CheckBox Name="UseFilters">Use filters</CheckBox>
-      <Button Name="OpenFile">Open File</Button>
-      <Button Name="SaveFile">Save File</Button>
-      <Button Name="SelectFolder">Select Folder</Button>
-      <Button Name="OpenBoth">Select Both</Button>
-      <Button Name="DecoratedWindow">Decorated window</Button>
-      <Button Name="DecoratedWindowDialog">Decorated window (dialog)</Button>
-      <Button Name="Dialog">Dialog</Button>
-      <Button Name="DialogNoTaskbar">Dialog (No taskbar icon)</Button>
-      <Button Name="OwnedWindow">Owned window</Button>
-      <Button Name="OwnedWindowNoTaskbar">Owned window (No taskbar icon)</Button>
+      <Button Name="OpenFile">_Open File</Button>
+      <Button Name="SaveFile">_Save File</Button>
+      <Button Name="SelectFolder">Select Fo_lder</Button>
+      <Button Name="OpenBoth">Select _Both</Button>
+      <Button Name="DecoratedWindow">Decorated _window</Button>
+      <Button Name="DecoratedWindowDialog">Decorated w_indow (dialog)</Button>
+      <Button Name="Dialog">_Dialog</Button>
+      <Button Name="DialogNoTaskbar">Dialog (_No taskbar icon)</Button>
+      <Button Name="OwnedWindow">Own_ed window</Button>
+      <Button Name="OwnedWindowNoTaskbar">Owned window (No tas_kbar icon)</Button>
   </StackPanel>
 </UserControl>

+ 20 - 4
samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml

@@ -1,17 +1,33 @@
 <UserControl xmlns="https://github.com/avaloniaui"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="ControlCatalog.Pages.ItemsRepeaterPage">
+  <UserControl.Styles>
+    <Style Selector="ItemsRepeater TextBlock.oddTemplate">
+      <Setter Property="Background" Value="Yellow" />
+      <Setter Property="Foreground" Value="Black" />
+    </Style>
+    <Style Selector="ItemsRepeater TextBlock.evenTemplate">
+      <Setter Property="Background" Value="Wheat" />
+      <Setter Property="Foreground" Value="Black" />
+    </Style>
+    <Style Selector="ItemsRepeater TextBlock:nth-child(5n+3)">
+      <Setter Property="Foreground" Value="Red" />
+      <Setter Property="FontWeight" Value="Bold" />
+    </Style>
+    <Style Selector="ItemsRepeater TextBlock:nth-last-child(5n+4)">
+      <Setter Property="Foreground" Value="Blue" />
+      <Setter Property="FontWeight" Value="Bold" />
+    </Style>
+  </UserControl.Styles>
   <UserControl.Resources>
     <RecyclePool x:Key="RecyclePool" />
     <DataTemplate x:Key="odd">
-      <TextBlock Background="Yellow"
-                 Foreground="Black"
+      <TextBlock Classes="oddTemplate"
                  Height="{Binding Height}"
                  Text="{Binding Text}"/>
     </DataTemplate>
     <DataTemplate x:Key="even">
-      <TextBlock Background="Wheat"
-                 Foreground="Black"
+      <TextBlock Classes="evenTemplate"
                  Height="{Binding Height}"
                  Text="{Binding Text}"/>
     </DataTemplate>

+ 11 - 0
samples/ControlCatalog/Pages/ListBoxPage.xaml

@@ -2,9 +2,20 @@
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
              x:Class="ControlCatalog.Pages.ListBoxPage">
   <DockPanel>
+    <DockPanel.Styles>
+      <Style Selector="ListBox ListBoxItem:nth-child(5n+3)">
+        <Setter Property="Foreground" Value="Red" />
+        <Setter Property="FontWeight" Value="Bold" />
+      </Style>
+      <Style Selector="ListBox ListBoxItem:nth-last-child(5n+4)">
+        <Setter Property="Foreground" Value="Blue" />
+        <Setter Property="FontWeight" Value="Bold" />
+      </Style>
+    </DockPanel.Styles>
     <StackPanel DockPanel.Dock="Top" Margin="4">
       <TextBlock Classes="h1">ListBox</TextBlock>
       <TextBlock Classes="h2">Hosts a collection of ListBoxItem.</TextBlock>
+      <TextBlock Classes="h2">Each 5th item is highlighted with nth-child(5n+3) and nth-last-child(5n+4) rules.</TextBlock>
     </StackPanel>
     <StackPanel DockPanel.Dock="Right" Margin="4">
       <CheckBox IsChecked="{Binding Multiple}">Multiple</CheckBox>

+ 3 - 3
samples/ControlCatalog/Pages/RadioButtonPage.xaml

@@ -11,9 +11,9 @@
                 Spacing="16">
       <StackPanel Orientation="Vertical"
                   Spacing="16">
-        <RadioButton IsChecked="True">Option 1</RadioButton>
-        <RadioButton>Option 2</RadioButton>
-        <RadioButton IsChecked="{x:Null}">Option 3</RadioButton>
+        <RadioButton IsChecked="True">_Option 1</RadioButton>
+        <RadioButton>O_ption 2</RadioButton>
+        <RadioButton IsChecked="{x:Null}">Op_tion 3</RadioButton>
         <RadioButton IsEnabled="False">Disabled</RadioButton>
       </StackPanel>
       <StackPanel Orientation="Vertical"

+ 3 - 3
samples/ControlCatalog/Pages/ToggleSwitchPage.xaml

@@ -14,7 +14,7 @@
 
     <Border Classes="Thin">
       <StackPanel>
-        <ToggleSwitch Content="headered" IsChecked="true" Margin="10"/>
+        <ToggleSwitch Content="h_eadered" IsChecked="true" Margin="10"/>
         <TextBox Classes="CodeBox"
           Text="&lt;ToggleSwitch&gt;headered&lt;/ToggleSwitch&gt;"/>
       </StackPanel>
@@ -24,7 +24,7 @@
 
     <Border Classes="Thin">
       <StackPanel>
-        <ToggleSwitch Content="Custom"
+        <ToggleSwitch Content="_Custom"
           OnContent="On"
           OffContent="Off"
           Margin="10"/>
@@ -40,7 +40,7 @@ ContentOff=&quot;Off&quot; /&gt;"
 
     <Border Classes="Thin">
       <StackPanel>
-        <ToggleSwitch Content="Just Click!" Margin="10">
+        <ToggleSwitch Content="_Just Click!" Margin="10">
           <ToggleSwitch.OnContent>
             <Image Source="/Assets/hirsch-899118_640.jpg" Height="32"/>
           </ToggleSwitch.OnContent>

+ 0 - 6
src/Avalonia.Animation/Properties/AssemblyInfo.cs

@@ -1,15 +1,9 @@
 using Avalonia.Metadata;
-using System.Reflection;
 using System.Runtime.CompilerServices;
 
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Easings")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Animation.Animators")]
 
-#if SIGNED_BUILD
 [assembly: InternalsVisibleTo("Avalonia.LeakTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
 [assembly: InternalsVisibleTo("Avalonia.Animation.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
-#else
-[assembly: InternalsVisibleTo("Avalonia.LeakTests")]
-[assembly: InternalsVisibleTo("Avalonia.Animation.UnitTests")]
-#endif

+ 1 - 9
src/Avalonia.Base/Properties/AssemblyInfo.cs

@@ -5,18 +5,10 @@ using System.Runtime.CompilerServices;
 using Avalonia.Metadata;
 
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Data.Converters")]
-#if SIGNED_BUILD
 [assembly: InternalsVisibleTo("Avalonia.Base.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
 [assembly: InternalsVisibleTo("Avalonia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
 [assembly: InternalsVisibleTo("DynamicProxyGenAssembly2, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c547cac37abd99c8db225ef2f6c8a3602f3b3606cc9891605d02baa56104f4cfc0734aa39b93bf7852f7d9266654753cc297e7d2edfe0bac1cdcf9f717241550e0a7b191195b7667bb4f64bcb8e2121380fd1d9d46ad2d92d2d15605093924cceaf74c4861eff62abf69b9291ed0a340e113be11e6a7d3113e92484cf7045cc7")] 
 [assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
 [assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
 [assembly: InternalsVisibleTo("Avalonia.Visuals, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
-#else
-[assembly: InternalsVisibleTo("Avalonia.Base.UnitTests")]
-[assembly: InternalsVisibleTo("Avalonia.UnitTests")]
-[assembly: InternalsVisibleTo("DynamicProxyGenAssembly2")]
-[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid")]
-[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests")]
-[assembly: InternalsVisibleTo("Avalonia.Visuals")]
-#endif
+

+ 11 - 19
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@@ -35,6 +35,7 @@ namespace Avalonia.Controls
 
         private const int DATAGRIDCOLUMNHEADER_resizeRegionWidth = 5;
         private const double DATAGRIDCOLUMNHEADER_separatorThickness = 1;
+        private const int DATAGRIDCOLUMNHEADER_columnsDragTreshold = 5;
 
         private bool _areHandlersSuspended;
         private static DragMode _dragMode;
@@ -448,19 +449,6 @@ namespace Avalonia.Controls
 
             OnMouseMove_Reorder(ref handled, mousePosition, mousePositionHeaders, distanceFromLeft, distanceFromRight);
 
-            // if we still haven't done anything about moving the mouse while 
-            // the button is down, we remember that we're dragging, but we don't 
-            // claim to have actually handled the event
-            if (_dragMode == DragMode.MouseDown)
-            {
-                _dragMode = DragMode.Drag;
-            }
-
-            _lastMousePositionHeaders = mousePositionHeaders;
-            
-            if (args.Pointer.Captured != this && _dragMode == DragMode.Drag)
-                args.Pointer.Capture(this);
-            
             SetDragCursor(mousePosition);
         }
 
@@ -732,15 +720,19 @@ namespace Avalonia.Controls
             {
                 return;
             }
-
+            
             //handle entry into reorder mode
-            if (_dragMode == DragMode.MouseDown && _dragColumn == null && (distanceFromRight > DATAGRIDCOLUMNHEADER_resizeRegionWidth && distanceFromLeft > DATAGRIDCOLUMNHEADER_resizeRegionWidth))
+            if (_dragMode == DragMode.MouseDown && _dragColumn == null && _lastMousePositionHeaders != null && (distanceFromRight > DATAGRIDCOLUMNHEADER_resizeRegionWidth && distanceFromLeft > DATAGRIDCOLUMNHEADER_resizeRegionWidth))
             {
-                handled = CanReorderColumn(OwningColumn);
-
-                if (handled)
+                var distanceFromInitial = (Vector)(mousePositionHeaders - _lastMousePositionHeaders);
+                if (distanceFromInitial.Length > DATAGRIDCOLUMNHEADER_columnsDragTreshold)
                 {
-                    OnMouseMove_BeginReorder(mousePosition);
+                    handled = CanReorderColumn(OwningColumn);
+
+                    if (handled)
+                    {
+                        OnMouseMove_BeginReorder(mousePosition);
+                    }
                 }
             }
 

+ 2 - 6
src/Avalonia.Controls.DataGrid/Properties/AssemblyInfo.cs

@@ -1,13 +1,9 @@
-using System.Reflection;
 using System.Runtime.CompilerServices;
 using Avalonia.Metadata;
-#if SIGNED_BUILD
+
 [assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
 [assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
-#else
-[assembly: InternalsVisibleTo("Avalonia.Controls.DataGrid.UnitTests")]
-[assembly: InternalsVisibleTo("Avalonia.DesignerSupport")]
-#endif
+
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Collections")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls.Primitives")]

+ 3 - 0
src/Avalonia.Controls/Button.cs

@@ -100,6 +100,7 @@ namespace Avalonia.Controls
             CommandParameterProperty.Changed.Subscribe(CommandParameterChanged);
             IsDefaultProperty.Changed.Subscribe(IsDefaultChanged);
             IsCancelProperty.Changed.Subscribe(IsCancelChanged);
+            AccessKeyHandler.AccessKeyPressedEvent.AddClassHandler<Button>((lbl, args) => lbl.OnAccessKey(args));
         }
 
         public Button()
@@ -257,6 +258,8 @@ namespace Avalonia.Controls
             }
         }
 
+        protected virtual void OnAccessKey(RoutedEventArgs e) => OnClick();
+
         /// <inheritdoc/>
         protected override void OnKeyDown(KeyEventArgs e)
         {

+ 7 - 8
src/Avalonia.Controls/ContextMenu.cs

@@ -333,16 +333,8 @@ namespace Avalonia.Controls
             {
                 _popup = new Popup
                 {
-                    HorizontalOffset = HorizontalOffset,
-                    VerticalOffset = VerticalOffset,
-                    PlacementAnchor = PlacementAnchor,
-                    PlacementConstraintAdjustment = PlacementConstraintAdjustment,
-                    PlacementGravity = PlacementGravity,
-                    PlacementMode = PlacementMode,
-                    PlacementRect = PlacementRect,
                     IsLightDismissEnabled = true,
                     OverlayDismissEventPassThrough = true,
-                    WindowManagerAddShadowHint = WindowManagerAddShadowHint,
                 };
 
                 _popup.Opened += PopupOpened;
@@ -362,6 +354,13 @@ namespace Avalonia.Controls
                 : PlacementMode;
 
             _popup.PlacementTarget = placementTarget;
+            _popup.HorizontalOffset = HorizontalOffset;
+            _popup.VerticalOffset = VerticalOffset;
+            _popup.PlacementAnchor = PlacementAnchor;
+            _popup.PlacementConstraintAdjustment = PlacementConstraintAdjustment;
+            _popup.PlacementGravity = PlacementGravity;
+            _popup.PlacementRect = PlacementRect;
+            _popup.WindowManagerAddShadowHint = WindowManagerAddShadowHint;
             _popup.Child = this;
             IsOpen = true;
             _popup.IsOpen = true;

+ 42 - 1
src/Avalonia.Controls/ItemsControl.cs

@@ -22,7 +22,7 @@ namespace Avalonia.Controls
     /// Displays a collection of items.
     /// </summary>
     [PseudoClasses(":empty", ":singleitem")]
-    public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener
+    public class ItemsControl : TemplatedControl, IItemsPresenterHost, ICollectionChangedListener, IChildIndexProvider
     {
         /// <summary>
         /// The default value for the <see cref="ItemsPanel"/> property.
@@ -57,6 +57,7 @@ namespace Avalonia.Controls
         private IEnumerable _items = new AvaloniaList<object>();
         private int _itemCount;
         private IItemContainerGenerator _itemContainerGenerator;
+        private EventHandler<ChildIndexChangedEventArgs> _childIndexChanged;
 
         /// <summary>
         /// Initializes static members of the <see cref="ItemsControl"/> class.
@@ -146,11 +147,28 @@ namespace Avalonia.Controls
             protected set;
         }
 
+        event EventHandler<ChildIndexChangedEventArgs> IChildIndexProvider.ChildIndexChanged
+        {
+            add => _childIndexChanged += value;
+            remove => _childIndexChanged -= value;
+        }
+
         /// <inheritdoc/>
         void IItemsPresenterHost.RegisterItemsPresenter(IItemsPresenter presenter)
         {
+            if (Presenter is IChildIndexProvider oldInnerProvider)
+            {
+                oldInnerProvider.ChildIndexChanged -= PresenterChildIndexChanged;
+            }
+
             Presenter = presenter;
             ItemContainerGenerator.Clear();
+
+            if (Presenter is IChildIndexProvider innerProvider)
+            {
+                innerProvider.ChildIndexChanged += PresenterChildIndexChanged;
+                _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs());
+            }
         }
 
         void ICollectionChangedListener.PreChanged(INotifyCollectionChanged sender, NotifyCollectionChangedEventArgs e)
@@ -512,5 +530,28 @@ namespace Avalonia.Controls
 
             return null;
         }
+
+        private void PresenterChildIndexChanged(object sender, ChildIndexChangedEventArgs e)
+        {
+            _childIndexChanged?.Invoke(this, e);
+        }
+
+        int IChildIndexProvider.GetChildIndex(ILogical child)
+        {
+            return Presenter is IChildIndexProvider innerProvider
+                ? innerProvider.GetChildIndex(child) : -1;
+        }
+
+        bool IChildIndexProvider.TryGetTotalCount(out int count)
+        {
+            if (Presenter is IChildIndexProvider presenter
+                && presenter.TryGetTotalCount(out count))
+            {
+                return true;
+            }
+
+            count = ItemCount;
+            return true;
+        }
     }
 }

+ 1 - 2
src/Avalonia.Controls/NativeMenuItem.cs

@@ -16,6 +16,7 @@ namespace Avalonia.Controls
         private bool _isChecked = false;
         private NativeMenuItemToggleType _toggleType;
         private IBitmap _icon;
+        private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber;
 
         private NativeMenu _menu;
 
@@ -47,8 +48,6 @@ namespace Avalonia.Controls
             }
         }
 
-        private readonly CanExecuteChangedSubscriber _canExecuteChangedSubscriber;
-
 
         public NativeMenuItem()
         {

+ 23 - 1
src/Avalonia.Controls/Panel.cs

@@ -2,8 +2,10 @@ using System;
 using System.Collections.Generic;
 using System.Collections.Specialized;
 using System.Linq;
+using Avalonia.LogicalTree;
 using Avalonia.Media;
 using Avalonia.Metadata;
+using Avalonia.Styling;
 
 namespace Avalonia.Controls
 {
@@ -14,7 +16,7 @@ namespace Avalonia.Controls
     /// Controls can be added to a <see cref="Panel"/> by adding them to its <see cref="Children"/>
     /// collection. All children are layed out to fill the panel.
     /// </remarks>
-    public class Panel : Control, IPanel
+    public class Panel : Control, IPanel, IChildIndexProvider
     {
         /// <summary>
         /// Defines the <see cref="Background"/> property.
@@ -30,6 +32,8 @@ namespace Avalonia.Controls
             AffectsRender<Panel>(BackgroundProperty);
         }
 
+        private EventHandler<ChildIndexChangedEventArgs> _childIndexChanged;
+
         /// <summary>
         /// Initializes a new instance of the <see cref="Panel"/> class.
         /// </summary>
@@ -53,6 +57,12 @@ namespace Avalonia.Controls
             set { SetValue(BackgroundProperty, value); }
         }
 
+        event EventHandler<ChildIndexChangedEventArgs> IChildIndexProvider.ChildIndexChanged
+        {
+            add => _childIndexChanged += value;
+            remove => _childIndexChanged -= value;
+        }
+
         /// <summary>
         /// Renders the visual to a <see cref="DrawingContext"/>.
         /// </summary>
@@ -137,6 +147,7 @@ namespace Avalonia.Controls
                     throw new NotSupportedException();
             }
 
+            _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs());
             InvalidateMeasureOnChildrenChanged();
         }
 
@@ -160,5 +171,16 @@ namespace Avalonia.Controls
             var panel = control?.VisualParent as TPanel;
             panel?.InvalidateMeasure();
         }
+
+        int IChildIndexProvider.GetChildIndex(ILogical child)
+        {
+            return child is IControl control ? Children.IndexOf(control) : -1;
+        }
+
+        public bool TryGetTotalCount(out int count)
+        {
+            count = Children.Count;
+            return true;
+        }
     }
 }

+ 5 - 1
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

@@ -1,4 +1,5 @@
 using System;
+using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Input.Raw;
 using Avalonia.Interactivity;
@@ -376,7 +377,10 @@ namespace Avalonia.Controls.Platform
             {
                 if (item.IsSubMenuOpen)
                 {
-                    if (item.IsTopLevel)
+                    // PointerPressed events may bubble from disabled items in sub-menus. In this case,
+                    // keep the sub-menu open.
+                    var popup = (e.Source as ILogical)?.FindLogicalAncestorOfType<Popup>();
+                    if (item.IsTopLevel && popup == null)
                     {
                         CloseMenu(item);
                     }

+ 40 - 1
src/Avalonia.Controls/Presenters/ItemsPresenterBase.cs

@@ -5,6 +5,7 @@ using Avalonia.Collections;
 using Avalonia.Controls.Generators;
 using Avalonia.Controls.Templates;
 using Avalonia.Controls.Utils;
+using Avalonia.LogicalTree;
 using Avalonia.Styling;
 
 namespace Avalonia.Controls.Presenters
@@ -12,7 +13,7 @@ namespace Avalonia.Controls.Presenters
     /// <summary>
     /// Base class for controls that present items inside an <see cref="ItemsControl"/>.
     /// </summary>
-    public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl
+    public abstract class ItemsPresenterBase : Control, IItemsPresenter, ITemplatedControl, IChildIndexProvider
     {
         /// <summary>
         /// Defines the <see cref="Items"/> property.
@@ -36,6 +37,7 @@ namespace Avalonia.Controls.Presenters
         private IDisposable _itemsSubscription;
         private bool _createdPanel;
         private IItemContainerGenerator _generator;
+        private EventHandler<ChildIndexChangedEventArgs> _childIndexChanged;
 
         /// <summary>
         /// Initializes static members of the <see cref="ItemsPresenter"/> class.
@@ -129,6 +131,12 @@ namespace Avalonia.Controls.Presenters
 
         protected bool IsHosted => TemplatedParent is IItemsPresenterHost;
 
+        event EventHandler<ChildIndexChangedEventArgs> IChildIndexProvider.ChildIndexChanged
+        {
+            add => _childIndexChanged += value;
+            remove => _childIndexChanged -= value;
+        }
+
         /// <inheritdoc/>
         public override sealed void ApplyTemplate()
         {
@@ -149,6 +157,8 @@ namespace Avalonia.Controls.Presenters
             if (Panel != null)
             {
                 ItemsChanged(e);
+
+                _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs());
             }
         }
 
@@ -169,9 +179,21 @@ namespace Avalonia.Controls.Presenters
                 result.ItemTemplate = ItemTemplate;
             }
 
+            result.Materialized += ContainerActionHandler;
+            result.Dematerialized += ContainerActionHandler;
+            result.Recycled += ContainerActionHandler;
+
             return result;
         }
 
+        private void ContainerActionHandler(object sender, ItemContainerEventArgs e)
+        {
+            for (var i = 0; i < e.Containers.Count; i++)
+            {
+                _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(e.Containers[i].ContainerControl));
+            }
+        }
+
         /// <inheritdoc/>
         protected override Size MeasureOverride(Size availableSize)
         {
@@ -248,5 +270,22 @@ namespace Avalonia.Controls.Presenters
         {
             (e.NewValue as IItemsPresenterHost)?.RegisterItemsPresenter(this);
         }
+
+        int IChildIndexProvider.GetChildIndex(ILogical child)
+        {
+            if (child is IControl control && ItemContainerGenerator is { } generator)
+            {
+                var index = ItemContainerGenerator.IndexFromContainer(control);
+
+                return index;
+            }
+
+            return -1;
+        }
+
+        bool IChildIndexProvider.TryGetTotalCount(out int count)
+        {
+            return Items.TryGetCountFast(out count);
+        }
     }
 }

+ 1 - 1
src/Avalonia.Controls/Primitives/AccessText.cs

@@ -69,7 +69,7 @@ namespace Avalonia.Controls.Primitives
             if (underscore != -1 && ShowAccessKey)
             {
                 var rect = TextLayout.HitTestTextPosition(underscore);
-                var offset = new Vector(0, -0.5);
+                var offset = new Vector(0, -1.5);
                 context.DrawLine(
                     new Pen(Foreground, 1),
                     rect.BottomLeft + offset,

+ 2 - 6
src/Avalonia.Controls/Properties/AssemblyInfo.cs

@@ -1,13 +1,9 @@
-using System.Reflection;
 using System.Runtime.CompilerServices;
 using Avalonia.Metadata;
-#if SIGNED_BUILD
+
 [assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
 [assembly: InternalsVisibleTo("Avalonia.DesignerSupport, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
-#else
-[assembly: InternalsVisibleTo("Avalonia.Controls.UnitTests")]
-[assembly: InternalsVisibleTo("Avalonia.DesignerSupport")]
-#endif
+
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Automation")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")]

+ 10 - 0
src/Avalonia.Controls/RepeatButton.cs

@@ -70,6 +70,16 @@ namespace Avalonia.Controls
             _repeatTimer?.Stop();
         }
 
+        protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
+        {
+            base.OnPropertyChanged(change);
+
+            if (change.Property == IsPressedProperty && change.NewValue.GetValueOrDefault<bool>() == false)
+            {
+                StopTimer();
+            }
+        }
+
         protected override void OnKeyDown(KeyEventArgs e)
         {
             base.OnKeyDown(e);

+ 29 - 2
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@@ -10,6 +10,7 @@ using Avalonia.Controls.Templates;
 using Avalonia.Input;
 using Avalonia.Layout;
 using Avalonia.Logging;
+using Avalonia.LogicalTree;
 using Avalonia.Utilities;
 using Avalonia.VisualTree;
 
@@ -19,7 +20,7 @@ namespace Avalonia.Controls
     /// Represents a data-driven collection control that incorporates a flexible layout system,
     /// custom views, and virtualization.
     /// </summary>
-    public class ItemsRepeater : Panel
+    public class ItemsRepeater : Panel, IChildIndexProvider
     {
         /// <summary>
         /// Defines the <see cref="HorizontalCacheLength"/> property.
@@ -61,8 +62,9 @@ namespace Avalonia.Controls
         private readonly ViewportManager _viewportManager;
         private IEnumerable _items;
         private VirtualizingLayoutContext _layoutContext;
-        private NotifyCollectionChangedEventArgs _processingItemsSourceChange;
+        private EventHandler<ChildIndexChangedEventArgs> _childIndexChanged;
         private bool _isLayoutInProgress;
+        private NotifyCollectionChangedEventArgs _processingItemsSourceChange;
         private ItemsRepeaterElementPreparedEventArgs _elementPreparedArgs;
         private ItemsRepeaterElementClearingEventArgs _elementClearingArgs;
         private ItemsRepeaterElementIndexChangedEventArgs _elementIndexChangedArgs;
@@ -163,6 +165,25 @@ namespace Avalonia.Controls
             }
         }
 
+        event EventHandler<ChildIndexChangedEventArgs> IChildIndexProvider.ChildIndexChanged
+        {
+            add => _childIndexChanged += value;
+            remove => _childIndexChanged -= value;
+        }
+
+        int IChildIndexProvider.GetChildIndex(ILogical child)
+        {
+            return child is IControl control
+                ? GetElementIndex(control)
+                : -1;
+        }
+
+        bool IChildIndexProvider.TryGetTotalCount(out int count)
+        {
+            count = ItemsSourceView.Count;
+            return true;
+        }
+
         /// <summary>
         /// Occurs each time an element is cleared and made available to be re-used.
         /// </summary>
@@ -545,6 +566,8 @@ namespace Avalonia.Controls
 
                 ElementPrepared(this, _elementPreparedArgs);
             }
+
+            _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
         }
 
         internal void OnElementClearing(IControl element)
@@ -562,6 +585,8 @@ namespace Avalonia.Controls
 
                 ElementClearing(this, _elementClearingArgs);
             }
+
+            _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
         }
 
         internal void OnElementIndexChanged(IControl element, int oldIndex, int newIndex)
@@ -579,6 +604,8 @@ namespace Avalonia.Controls
 
                 ElementIndexChanged(this, _elementIndexChangedArgs);
             }
+
+            _childIndexChanged?.Invoke(this, new ChildIndexChangedEventArgs(element));
         }
 
         private void OnDataSourcePropertyChanged(ItemsSourceView oldValue, ItemsSourceView newValue)

+ 48 - 1
src/Avalonia.Controls/TrayIcon.cs

@@ -1,10 +1,12 @@
 using System;
 using System.Collections.Generic;
 using System.Linq;
+using System.Windows.Input;
 using Avalonia.Collections;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Controls.Platform;
 using Avalonia.Platform;
+using Avalonia.Utilities;
 
 #nullable enable
 
@@ -13,10 +15,13 @@ namespace Avalonia.Controls
     public sealed class TrayIcons : AvaloniaList<TrayIcon>
     {
     }
+    
+    
 
     public class TrayIcon : AvaloniaObject, INativeMenuExporterProvider, IDisposable
     {
         private readonly ITrayIconImpl? _impl;
+        private ICommand? _command;
 
         private TrayIcon(ITrayIconImpl? impl)
         {
@@ -26,7 +31,15 @@ namespace Avalonia.Controls
 
                 _impl.SetIsVisible(IsVisible);
 
-                _impl.OnClicked = () => Clicked?.Invoke(this, EventArgs.Empty);
+                _impl.OnClicked = () =>
+                {
+                    Clicked?.Invoke(this, EventArgs.Empty);
+                    
+                    if (Command?.CanExecute(CommandParameter) == true)
+                    {
+                        Command.Execute(CommandParameter);
+                    }
+                };
             }
         }
 
@@ -64,6 +77,21 @@ namespace Avalonia.Controls
         /// on OSX this event is not raised.
         /// </summary>
         public event EventHandler? Clicked;
+        
+        /// <summary>
+        /// Defines the <see cref="Command"/> property.
+        /// </summary>
+        public static readonly DirectProperty<TrayIcon, ICommand?> CommandProperty =
+            Button.CommandProperty.AddOwner<TrayIcon>(
+                trayIcon => trayIcon.Command,
+                (trayIcon, command) => trayIcon.Command = command,
+                enableDataValidation: true);
+
+        /// <summary>
+        /// Defines the <see cref="CommandParameter"/> property.
+        /// </summary>
+        public static readonly StyledProperty<object?> CommandParameterProperty =
+            Button.CommandParameterProperty.AddOwner<MenuItem>();
 
         /// <summary>
         /// Defines the <see cref="TrayIcons"/> attached property.
@@ -98,6 +126,25 @@ namespace Avalonia.Controls
         public static void SetIcons(AvaloniaObject o, TrayIcons trayIcons) => o.SetValue(IconsProperty, trayIcons);
 
         public static TrayIcons GetIcons(AvaloniaObject o) => o.GetValue(IconsProperty);
+        
+        /// <summary>
+        /// Gets or sets the <see cref="Command"/> property of a TrayIcon.
+        /// </summary>
+        public ICommand? Command
+        {
+            get => _command;
+            set => SetAndRaise(CommandProperty, ref _command, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the parameter to pass to the <see cref="Command"/> property of a
+        /// <see cref="TrayIcon"/>.
+        /// </summary>
+        public object CommandParameter
+        {
+            get { return GetValue(CommandParameterProperty); }
+            set { SetValue(CommandParameterProperty, value); }
+        }
 
         /// <summary>
         /// Gets or sets the Menu of the TrayIcon.

+ 20 - 7
src/Avalonia.Controls/Utils/IEnumerableUtils.cs

@@ -12,23 +12,36 @@ namespace Avalonia.Controls.Utils
             return items.IndexOf(item) != -1;
         }
 
-        public static int Count(this IEnumerable items)
+        public static bool TryGetCountFast(this IEnumerable items, out int count)
         {
             if (items != null)
             {
                 if (items is ICollection collection)
                 {
-                    return collection.Count;
+                    count = collection.Count;
+                    return true;
                 }
                 else if (items is IReadOnlyCollection<object> readOnly)
                 {
-                    return readOnly.Count;
-                }
-                else
-                {
-                    return Enumerable.Count(items.Cast<object>());
+                    count = readOnly.Count;
+                    return true;
                 }
             }
+
+            count = 0;
+            return false;
+        }
+
+        public static int Count(this IEnumerable items)
+        {
+            if (TryGetCountFast(items, out var count))
+            {
+                return count;
+            }
+            else if (items != null)
+            {
+                return Enumerable.Count(items.Cast<object>());
+            }
             else
             {
                 return 0;

+ 1 - 1
src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs

@@ -25,7 +25,6 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport
         private AutoResetEvent _wakeup = new AutoResetEvent(false);
         private FrameMessage _lastFrameMessage = null;
         private FrameMessage _lastSentFrameMessage = null;
-        private RequestViewportResizeMessage _lastViewportRequest;
         private Action<IAvaloniaRemoteTransportConnection, object> _onMessage;
         private Action<IAvaloniaRemoteTransportConnection, Exception> _onException;
         
@@ -177,6 +176,7 @@ namespace Avalonia.DesignerSupport.Remote.HtmlTransport
         
         public void Dispose()
         {
+            _disposed = true;
             _pendingSocket?.Dispose();
             _simpleServer.Dispose();
         }

+ 1 - 1
src/Avalonia.Diagnostics/Diagnostics/Models/EventChainLink.cs

@@ -29,7 +29,7 @@ namespace Avalonia.Diagnostics.Models
             }
         }
 
-        public bool Handled { get; }
+        public bool Handled { get; set; }
 
         public RoutingStrategies Route { get; }
     }

+ 3 - 1
src/Avalonia.Diagnostics/Diagnostics/ViewModels/AvaloniaPropertyViewModel.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Diagnostics.ViewModels
             Name = property.IsAttached ?
                 $"[{property.OwnerType.Name}.{property.Name}]" :
                 property.Name;
-
+            DeclaringType = property.OwnerType;
             Update();
         }
 
@@ -50,6 +50,8 @@ namespace Avalonia.Diagnostics.ViewModels
 
         public override string Group => _group;
 
+        public override System.Type? DeclaringType { get; }
+
         // [MemberNotNull(nameof(_type), nameof(_group), nameof(_priority))]
         public override void Update()
         {

+ 3 - 1
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ClrPropertyViewModel.cs

@@ -24,7 +24,7 @@ namespace Avalonia.Diagnostics.ViewModels
             {
                 Name = property.DeclaringType.Name + '.' + property.Name;
             }
-
+            DeclaringType = property.DeclaringType;
             Update();
         }
 
@@ -55,6 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels
         public override bool? IsAttached => 
             default;
 
+        public override System.Type? DeclaringType { get; }
+
         // [MemberNotNull(nameof(_type))]
         public override void Update()
         {

+ 116 - 25
src/Avalonia.Diagnostics/Diagnostics/ViewModels/ControlDetailsViewModel.cs

@@ -17,42 +17,26 @@ namespace Avalonia.Diagnostics.ViewModels
     internal class ControlDetailsViewModel : ViewModelBase, IDisposable
     {
         private readonly IVisual _control;
-        private readonly IDictionary<object, List<PropertyViewModel>> _propertyIndex;
+        private IDictionary<object, List<PropertyViewModel>> _propertyIndex;
         private PropertyViewModel? _selectedProperty;
+        private DataGridCollectionView _propertiesView;
         private bool _snapshotStyles;
         private bool _showInactiveStyles;
         private string? _styleStatus;
+        private object _selectedEntity;
+        private readonly Stack<(string Name,object Entry)> _selectedEntitiesStack = new();
+        private string _selectedEntityName;
+        private string _selectedEntityType;
 
         public ControlDetailsViewModel(TreePageViewModel treePage, IVisual control)
         {
             _control = control;
 
             TreePage = treePage;
-
-            var properties = GetAvaloniaProperties(control)
-                .Concat(GetClrProperties(control))
-                .OrderBy(x => x, PropertyComparer.Instance)
-                .ThenBy(x => x.Name)
-                .ToList();
-
-            _propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList());
-
-            var view = new DataGridCollectionView(properties);
-            view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group)));
-            view.Filter = FilterProperty;
-            PropertiesView = view;
-
+            
             Layout = new ControlLayoutViewModel(control);
 
-            if (control is INotifyPropertyChanged inpc)
-            {
-                inpc.PropertyChanged += ControlPropertyChanged;
-            }
-
-            if (control is AvaloniaObject ao)
-            {
-                ao.PropertyChanged += ControlPropertyChanged;
-            }
+            NavigateToProperty(control, (control as IControl)?.Name ?? control.ToString()); 
 
             AppliedStyles = new ObservableCollection<StyleViewModel>();
             PseudoClasses = new ObservableCollection<PseudoClassViewModel>();
@@ -133,12 +117,46 @@ namespace Avalonia.Diagnostics.ViewModels
 
         public TreePageViewModel TreePage { get; }
 
-        public DataGridCollectionView PropertiesView { get; }
+        public DataGridCollectionView PropertiesView
+        {
+            get => _propertiesView;
+            private set => RaiseAndSetIfChanged(ref _propertiesView, value);
+        }
 
         public ObservableCollection<StyleViewModel> AppliedStyles { get; }
 
         public ObservableCollection<PseudoClassViewModel> PseudoClasses { get; }
 
+        public object SelectedEntity
+        {
+            get => _selectedEntity;
+            set
+            {
+                RaiseAndSetIfChanged(ref _selectedEntity, value);
+               
+            }
+        }
+
+        public string SelectedEntityName
+        {
+            get => _selectedEntityName;
+            set
+            {
+                RaiseAndSetIfChanged(ref _selectedEntityName, value);
+               
+            }
+        }
+        
+        public string SelectedEntityType
+        {
+            get => _selectedEntityType;
+            set
+            {
+                RaiseAndSetIfChanged(ref _selectedEntityType, value);
+               
+            }
+        }
+        
         public PropertyViewModel? SelectedProperty
         {
             get => _selectedProperty;
@@ -378,5 +396,78 @@ namespace Avalonia.Diagnostics.ViewModels
                 }
             }
         }
+
+        public void ApplySelectedProperty()
+        {
+            var selectedProperty = SelectedProperty;
+            var selectedEntity = SelectedEntity;
+            var selectedEntityName = SelectedEntityName;
+            if (selectedProperty == null)
+                return;
+
+            object? property;
+            if (selectedProperty.Key is AvaloniaProperty avaloniaProperty)
+            {
+                property = (_selectedEntity as IControl)?.GetValue(avaloniaProperty);
+            }
+            else
+            {
+                property = selectedEntity.GetType().GetProperties()
+                     .FirstOrDefault(pi => pi.Name == selectedProperty.Name
+                           && pi.DeclaringType == selectedProperty.DeclaringType
+                           && pi.PropertyType.Name == selectedProperty.Type)
+                     ?.GetValue(selectedEntity);
+            }
+            if (property == null) return;
+            _selectedEntitiesStack.Push((Name:selectedEntityName,Entry:selectedEntity));
+            NavigateToProperty(property, selectedProperty.Name);
+        }
+
+        public void ApplyParentProperty()
+        {
+            if (_selectedEntitiesStack.Any())
+            {
+                var property = _selectedEntitiesStack.Pop();
+                NavigateToProperty(property.Entry, property.Name);
+            }
+        }
+        
+        protected  void NavigateToProperty(object o, string entityName)
+        {
+            var oldSelectedEntity = SelectedEntity;
+            if (oldSelectedEntity is IAvaloniaObject ao1)
+            {
+                ao1.PropertyChanged -= ControlPropertyChanged;
+            }
+            else if (oldSelectedEntity is INotifyPropertyChanged inpc1)
+            {
+                inpc1.PropertyChanged -= ControlPropertyChanged;
+            }
+      
+            SelectedEntity = o;
+            SelectedEntityName = entityName;
+            SelectedEntityType = o.ToString();
+            var properties = GetAvaloniaProperties(o)
+                .Concat(GetClrProperties(o))
+                .OrderBy(x => x, PropertyComparer.Instance)
+                .ThenBy(x => x.Name)
+                .ToList();
+
+            _propertyIndex = properties.GroupBy(x => x.Key).ToDictionary(x => x.Key, x => x.ToList());
+
+            var view = new DataGridCollectionView(properties);
+            view.GroupDescriptions.Add(new DataGridPathGroupDescription(nameof(AvaloniaPropertyViewModel.Group)));
+            view.Filter = FilterProperty;
+            PropertiesView = view;
+
+            if (o is IAvaloniaObject ao2)
+            {
+                ao2.PropertyChanged += ControlPropertyChanged;
+            }
+            else if (o is INotifyPropertyChanged inpc2)
+            {
+                inpc2.PropertyChanged += ControlPropertyChanged;
+            }
+        }
     }
 }

+ 31 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/EventTreeNode.cs

@@ -55,6 +55,8 @@ namespace Avalonia.Diagnostics.ViewModels
 
                 // FIXME: This leaks event handlers.
                 Event.AddClassHandler(typeof(object), HandleEvent, allRoutes, handledEventsToo: true);
+                Event.RouteFinished.Subscribe(HandleRouteFinished);
+                
                 _isRegistered = true;
             }
         }
@@ -92,6 +94,35 @@ namespace Avalonia.Diagnostics.ViewModels
             else
                 handler();
         }
+        
+        private void HandleRouteFinished(RoutedEventArgs e)
+        {
+            if (!_isRegistered || IsEnabled == false)
+                return;
+            if (e.Source is IVisual v && BelongsToDevTool(v))
+                return;
+
+            var s = e.Source;
+            var handled = e.Handled;
+            var route = e.Route;
+
+            void handler()
+            {
+                if (_currentEvent != null && handled)
+                {
+                    var linkIndex = _currentEvent.EventChain.Count - 1;
+                    var link = _currentEvent.EventChain[linkIndex];
+
+                    link.Handled = true;
+                    _currentEvent.HandledBy = link;
+                }
+            }
+
+            if (!Dispatcher.UIThread.CheckAccess())
+                Dispatcher.UIThread.Post(handler);
+            else
+                handler();
+        }
 
         private static bool BelongsToDevTool(IVisual v)
         {

+ 2 - 1
src/Avalonia.Diagnostics/Diagnostics/ViewModels/FiredEvent.cs

@@ -63,7 +63,7 @@ namespace Avalonia.Diagnostics.ViewModels
         {
             if (EventChain.Count > 0)
             {
-                var prevLink = EventChain[EventChain.Count-1];
+                var prevLink = EventChain[EventChain.Count - 1];
 
                 if (prevLink.Route != link.Route)
                 {
@@ -72,6 +72,7 @@ namespace Avalonia.Diagnostics.ViewModels
             }
 
             EventChain.Add(link);
+
             if (HandledBy == null && link.Handled)
                 HandledBy = link;
         }

+ 1 - 0
src/Avalonia.Diagnostics/Diagnostics/ViewModels/PropertyViewModel.cs

@@ -15,6 +15,7 @@ namespace Avalonia.Diagnostics.ViewModels
         public abstract string Name { get; }
         public abstract string Group { get; }
         public abstract string Type { get; }
+        public abstract Type? DeclaringType { get; }
         public abstract string Value { get; set; }
         public abstract string Priority { get; }
         public abstract bool? IsAttached { get;  }

+ 12 - 5
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml

@@ -13,23 +13,30 @@
 
   <Grid ColumnDefinitions="*,Auto,320">
 
-    <Grid Grid.Column="0" RowDefinitions="Auto,*">
+    <Grid Grid.Column="0" RowDefinitions="Auto,Auto,*">
 
-      <controls:FilterTextBox Grid.Row="0"
+      <Grid ColumnDefinitions="Auto, *" RowDefinitions="Auto, Auto">
+        <Button Grid.Column="0" Grid.RowSpan="2" Content="^" Command="{Binding ApplyParentProperty}" />
+        <TextBlock Grid.Column="1" Grid.Row="0" Text="{Binding SelectedEntityName}" FontWeight="Bold" />
+        <TextBlock Grid.Column="1" Grid.Row="1" Text="{Binding SelectedEntityType}" FontStyle="Italic" />
+      </Grid>
+      
+      <controls:FilterTextBox Grid.Row="1"
                               BorderThickness="0"
                               DataContext="{Binding TreePage.PropertiesFilter}"
                               Text="{Binding FilterString}"
                               Watermark="Filter properties"
                               UseCaseSensitiveFilter="{Binding UseCaseSensitiveFilter}"
                               UseWholeWordFilter="{Binding UseWholeWordFilter}"
-                              UseRegexFilter="{Binding UseRegexFilter}" />
+                              UseRegexFilter="{Binding UseRegexFilter}"/>
       
       <DataGrid Items="{Binding PropertiesView}"
-                Grid.Row="1"
+                Grid.Row="2"
                 BorderThickness="0"
                 RowBackground="Transparent"
                 SelectedItem="{Binding SelectedProperty, Mode=TwoWay}"
-                CanUserResizeColumns="true">
+                CanUserResizeColumns="true"
+                DoubleTapped="PropertiesGrid_OnDoubleTapped">
         <DataGrid.Columns>
           <DataGridTextColumn Header="Property" Binding="{Binding Name}" IsReadOnly="True" />
           <DataGridTextColumn Header="Value" Binding="{Binding Value}" />

+ 11 - 0
src/Avalonia.Diagnostics/Diagnostics/Views/ControlDetailsView.xaml.cs

@@ -1,4 +1,6 @@
 using Avalonia.Controls;
+using Avalonia.Diagnostics.ViewModels;
+using Avalonia.Input;
 using Avalonia.Markup.Xaml;
 
 namespace Avalonia.Diagnostics.Views
@@ -14,5 +16,14 @@ namespace Avalonia.Diagnostics.Views
         {
             AvaloniaXamlLoader.Load(this);
         }
+
+        private void PropertiesGrid_OnDoubleTapped(object sender, TappedEventArgs e)
+        {
+            if (sender is DataGrid grid && grid.DataContext is ControlDetailsViewModel controlDetails)
+            {
+                controlDetails.ApplySelectedProperty();
+            }
+            
+        }
     }
 }

+ 3 - 3
src/Avalonia.FreeDesktop/DBusHelper.cs

@@ -12,7 +12,7 @@ namespace Avalonia.FreeDesktop
         /// This class uses synchronous execution at DBus connection establishment stage
         /// then switches to using AvaloniaSynchronizationContext
         /// </summary>
-        class DBusSyncContext : SynchronizationContext
+        private class DBusSyncContext : SynchronizationContext
         {
             private SynchronizationContext _ctx;
             private object _lock = new object();
@@ -51,10 +51,10 @@ namespace Avalonia.FreeDesktop
 
         public static Connection TryInitialize(string dbusAddress = null)
         {
-            return Connection ?? TryGetConnection(dbusAddress);
+            return Connection ?? TryCreateNewConnection(dbusAddress);
         }
         
-        public static Connection TryGetConnection(string dbusAddress = null)
+        public static Connection TryCreateNewConnection(string dbusAddress = null)
         { 
             var oldContext = SynchronizationContext.Current;
             try

+ 134 - 44
src/Avalonia.X11/X11TrayIconImpl.cs → src/Avalonia.FreeDesktop/DBusTrayIconImpl.cs

@@ -6,55 +6,65 @@ using System.Reactive.Disposables;
 using System.Runtime.CompilerServices;
 using System.Threading.Tasks;
 using Avalonia.Controls.Platform;
-using Avalonia.FreeDesktop;
 using Avalonia.Logging;
 using Avalonia.Platform;
 using Tmds.DBus;
 
 [assembly: InternalsVisibleTo(Connection.DynamicAssemblyName)]
 
-namespace Avalonia.X11
+[assembly:
+    InternalsVisibleTo(
+        "Avalonia.X11, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
+
+namespace Avalonia.FreeDesktop
 {
-    internal class X11TrayIconImpl : ITrayIconImpl
+    internal class DBusTrayIconImpl : ITrayIconImpl
     {
         private static int s_trayIconInstanceId;
+
         private readonly ObjectPath _dbusMenuPath;
-        private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj;
         private readonly Connection? _connection;
-        private DbusPixmap _icon;
+        private IDisposable? _serviceWatchDisposable;
 
+        private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj;
         private IStatusNotifierWatcher? _statusNotifierWatcher;
+        private DbusPixmap _icon;
 
         private string? _sysTrayServiceName;
         private string? _tooltipText;
-        private bool _isActive;
         private bool _isDisposed;
-        private readonly bool _ctorFinished;
+        private bool _serviceConnected;
+        private bool _isVisible = true;
 
+        public bool IsActive { get; private set; }
         public INativeMenuExporter? MenuExporter { get; }
         public Action? OnClicked { get; set; }
+        public Func<IWindowIconImpl?, uint[]>? IconConverterDelegate { get; set; }
 
-        public X11TrayIconImpl()
+        public DBusTrayIconImpl()
         {
-            _connection = DBusHelper.TryGetConnection();
+            _connection = DBusHelper.TryCreateNewConnection();
 
             if (_connection is null)
             {
-                Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)
+                Logger.TryGet(LogEventLevel.Error, "DBUS")
                     ?.Log(this, "Unable to get a dbus connection for system tray icons.");
+
                 return;
             }
 
+            IsActive = true;
+
             _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath;
+
             MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection);
-            CreateTrayIcon();
-            _ctorFinished = true;
+
+            WatchAsync();
         }
 
-        public async void CreateTrayIcon()
+        private void InitializeSNWService()
         {
-            if (_connection is null)
-                return;
+            if (_connection is null || _isDisposed) return;
 
             try
             {
@@ -64,12 +74,58 @@ namespace Avalonia.X11
             }
             catch
             {
-                Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)
+                Logger.TryGet(LogEventLevel.Error, "DBUS")
+                    ?.Log(this,
+                        "org.kde.StatusNotifierWatcher service is not available on this system. Tray Icons will not work without it.");
+
+                return;
+            }
+
+            _serviceConnected = true;
+        }
+
+        private async void WatchAsync()
+        {
+            try
+            {
+                _serviceWatchDisposable =
+                    await _connection?.ResolveServiceOwnerAsync("org.kde.StatusNotifierWatcher", OnNameChange)!;
+            }
+            catch (Exception e)
+            {
+                Logger.TryGet(LogEventLevel.Error, "DBUS")
                     ?.Log(this,
-                        "DBUS: org.kde.StatusNotifierWatcher service is not available on this system. System Tray Icons will not work without it.");
+                        $"Unable to hook watcher method on org.kde.StatusNotifierWatcher: {e}");
             }
+        }
 
-            if (_statusNotifierWatcher is null)
+        private void OnNameChange(ServiceOwnerChangedEventArgs obj)
+        {
+            if (_isDisposed)
+                return;
+
+            if (!_serviceConnected & obj.NewOwner != null)
+            {
+                _serviceConnected = true;
+                InitializeSNWService();
+
+                DestroyTrayIcon();
+
+                if (_isVisible)
+                {
+                    CreateTrayIcon();
+                }
+            }
+            else if (_serviceConnected & obj.NewOwner is null)
+            {
+                DestroyTrayIcon();
+                _serviceConnected = false;
+            }
+        }
+
+        private void CreateTrayIcon()
+        {
+            if (_connection is null || !_serviceConnected || _isDisposed)
                 return;
 
             var pid = Process.GetCurrentProcess().Id;
@@ -78,45 +134,61 @@ namespace Avalonia.X11
             _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}";
             _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath);
 
-            await _connection.RegisterObjectAsync(_statusNotifierItemDbusObj);
-
-            await _connection.RegisterServiceAsync(_sysTrayServiceName);
+            try
+            {
+                _connection.RegisterObjectAsync(_statusNotifierItemDbusObj);
+                _connection.RegisterServiceAsync(_sysTrayServiceName);
+                _statusNotifierWatcher?.RegisterStatusNotifierItemAsync(_sysTrayServiceName);
+            }
+            catch (Exception e)
+            {
+                Logger.TryGet(LogEventLevel.Error, "DBUS")
+                    ?.Log(this, $"Error creating a DBus tray icon: {e}.");
 
-            await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName);
+                _serviceConnected = false;
+            }
 
             _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText);
             _statusNotifierItemDbusObj.SetIcon(_icon);
 
             _statusNotifierItemDbusObj.ActivationDelegate += OnClicked;
-
-            _isActive = true;
         }
 
-        public async void DestroyTrayIcon()
+        private void DestroyTrayIcon()
         {
-            if (_connection is null)
+            if (_connection is null || !_serviceConnected || _isDisposed || _statusNotifierItemDbusObj is null)
                 return;
+
             _connection.UnregisterObject(_statusNotifierItemDbusObj);
-            await _connection.UnregisterServiceAsync(_sysTrayServiceName);
-            _isActive = false;
+            _connection.UnregisterServiceAsync(_sysTrayServiceName);
         }
 
         public void Dispose()
         {
+            IsActive = false;
             _isDisposed = true;
             DestroyTrayIcon();
             _connection?.Dispose();
+            _serviceWatchDisposable?.Dispose();
         }
 
         public void SetIcon(IWindowIconImpl? icon)
         {
-            if (_isDisposed)
+            if (_isDisposed || IconConverterDelegate is null)
                 return;
-            if (!(icon is X11IconData x11icon))
+
+            if (icon is null)
+            {
+                _statusNotifierItemDbusObj?.SetIcon(DbusPixmap.EmptyPixmap);
                 return;
+            }
+
+            var x11iconData = IconConverterDelegate(icon);
+
+            if (x11iconData.Length == 0) return;
 
-            var w = (int)x11icon.Data[0];
-            var h = (int)x11icon.Data[1];
+            var w = (int)x11iconData[0];
+            var h = (int)x11iconData[1];
 
             var pixLength = w * h;
             var pixByteArrayCounter = 0;
@@ -124,7 +196,7 @@ namespace Avalonia.X11
 
             for (var i = 0; i < pixLength; i++)
             {
-                var rawPixel = x11icon.Data[i + 2].ToUInt32();
+                var rawPixel = x11iconData[i + 2];
                 pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF000000) >> 24);
                 pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF0000) >> 16);
                 pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF00) >> 8);
@@ -137,18 +209,21 @@ namespace Avalonia.X11
 
         public void SetIsVisible(bool visible)
         {
-            if (_isDisposed || !_ctorFinished)
+            if (_isDisposed)
                 return;
 
-            if (visible & !_isActive)
-            {
-                DestroyTrayIcon();
-                CreateTrayIcon();
-            }
-            else if (!visible & _isActive)
+            switch (visible)
             {
-                DestroyTrayIcon();
+                case true when !_isVisible:
+                    DestroyTrayIcon();
+                    CreateTrayIcon();
+                    break;
+                case false when _isVisible:
+                    DestroyTrayIcon();
+                    break;
             }
+
+            _isVisible = visible;
         }
 
         public void SetToolTipText(string? text)
@@ -248,7 +323,20 @@ namespace Avalonia.X11
             return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler));
         }
 
-        public Task<object> GetAsync(string prop) => Task.FromResult(new object());
+        public Task<object?> GetAsync(string prop)
+        {
+            return Task.FromResult<object?>(prop switch
+            {
+                nameof(_backingProperties.Category) => _backingProperties.Category,
+                nameof(_backingProperties.Id) => _backingProperties.Id,
+                nameof(_backingProperties.Menu) => _backingProperties.Menu,
+                nameof(_backingProperties.IconPixmap) => _backingProperties.IconPixmap,
+                nameof(_backingProperties.Status) => _backingProperties.Status,
+                nameof(_backingProperties.Title) => _backingProperties.Title,
+                nameof(_backingProperties.ToolTip) => _backingProperties.ToolTip,
+                _ => null
+            });
+        }
 
         public Task<StatusNotifierItemProperties> GetAllAsync() => Task.FromResult(_backingProperties);
 
@@ -298,17 +386,17 @@ namespace Avalonia.X11
         Task<IDisposable> WatchNewOverlayIconAsync(Action handler, Action<Exception> onError);
         Task<IDisposable> WatchNewToolTipAsync(Action handler, Action<Exception> onError);
         Task<IDisposable> WatchNewStatusAsync(Action<string> handler, Action<Exception> onError);
-        Task<object> GetAsync(string prop);
+        Task<object?> GetAsync(string prop);
         Task<StatusNotifierItemProperties> GetAllAsync();
         Task SetAsync(string prop, object val);
         Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler);
     }
 
-    [Dictionary]
     // This class is used by Tmds.Dbus to ferry properties
     // from the SNI spec.
     // Don't change this to actual C# properties since
     // Tmds.Dbus will get confused.
+    [Dictionary]
     internal class StatusNotifierItemProperties
     {
         public string? Category;
@@ -363,5 +451,7 @@ namespace Avalonia.X11
             Height = height;
             Data = data;
         }
+
+        public static DbusPixmap EmptyPixmap = new DbusPixmap(1, 1, new byte[] { 255, 0, 0, 0 });
     }
 }

+ 1 - 1
src/Avalonia.Headless/HeadlessWindowImpl.cs

@@ -200,7 +200,7 @@ namespace Avalonia.Headless
 
         public ILockedFramebuffer Lock()
         {
-            var bmp = new WriteableBitmap(PixelSize.FromSize(ClientSize, RenderScaling), new Vector(96, 96) * RenderScaling);
+            var bmp = new WriteableBitmap(PixelSize.FromSize(ClientSize, RenderScaling), new Vector(96, 96) * RenderScaling, PixelFormat.Rgba8888, AlphaFormat.Premul);
             var fb = bmp.Lock();
             return new FramebufferProxy(fb, () =>
             {

+ 1 - 1
src/Avalonia.Input/InputElement.cs

@@ -16,7 +16,7 @@ namespace Avalonia.Input
     /// <summary>
     /// Implements input-related functionality for a control.
     /// </summary>
-    [PseudoClasses(":disabled", ":focus", ":focus-visible", ":pointerover")]
+    [PseudoClasses(":disabled", ":focus", ":focus-visible", ":focus-within", ":pointerover")]
     public class InputElement : Interactive, IInputElement
     {
         /// <summary>

+ 4 - 5
src/Avalonia.Layout/ElementManager.cs

@@ -314,12 +314,11 @@ namespace Avalonia.Layout
                         }
                         break;
 
+                    // Remove clear all realized elements just to align the begavior
+                    // with ViewManager which resets realized item indices to defaults.
+                    // Freeing only removed items causes wrong indices to be stored
+                    // in virtualized info of items under some circumstances.
                     case NotifyCollectionChangedAction.Remove:
-                        {
-                            OnItemsRemoved(args.OldStartingIndex, args.OldItems.Count);
-                        }
-                        break;
-
                     case NotifyCollectionChangedAction.Reset:
                         ClearRealizedRange();
                         break;

+ 2 - 4
src/Avalonia.Layout/Properties/AssemblyInfo.cs

@@ -1,9 +1,7 @@
 using System.Runtime.CompilerServices;
 using Avalonia.Metadata;
-#if SIGNED_BUILD
+
 [assembly: InternalsVisibleTo("Avalonia.Layout.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
-#else
-[assembly: InternalsVisibleTo("Avalonia.Layout.UnitTests")]
-#endif
+
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Layout")]
 

+ 26 - 0
src/Avalonia.Styling/LogicalTree/ChildIndexChangedEventArgs.cs

@@ -0,0 +1,26 @@
+#nullable enable
+using System;
+
+namespace Avalonia.LogicalTree
+{
+    /// <summary>
+    /// Event args for <see cref="IChildIndexProvider.ChildIndexChanged"/> event.
+    /// </summary>
+    public class ChildIndexChangedEventArgs : EventArgs
+    {
+        public ChildIndexChangedEventArgs()
+        {
+        }
+
+        public ChildIndexChangedEventArgs(ILogical child)
+        {
+            Child = child;
+        }
+
+        /// <summary>
+        /// Logical child which index was changed.
+        /// If null, all children should be reset.
+        /// </summary>
+        public ILogical? Child { get; }
+    }
+}

+ 32 - 0
src/Avalonia.Styling/LogicalTree/IChildIndexProvider.cs

@@ -0,0 +1,32 @@
+#nullable enable
+using System;
+
+namespace Avalonia.LogicalTree
+{
+    /// <summary>
+    /// Child's index and total count information provider used by list-controls (ListBox, StackPanel, etc.)
+    /// </summary>
+    /// <remarks>
+    /// Used by nth-child and nth-last-child selectors. 
+    /// </remarks>
+    public interface IChildIndexProvider
+    {
+        /// <summary>
+        /// Gets child's actual index in order of the original source.
+        /// </summary>
+        /// <param name="child">Logical child.</param>
+        /// <returns>Index or -1 if child was not found.</returns>
+        int GetChildIndex(ILogical child);
+
+        /// <summary>
+        /// Total children count or null if source is infinite.
+        /// Some Avalonia features might not work if <see cref="TryGetTotalCount"/> returns false, for instance: nth-last-child selector.
+        /// </summary>
+        bool TryGetTotalCount(out int count);
+
+        /// <summary>
+        /// Notifies subscriber when child's index or total count was changed.
+        /// </summary>
+        event EventHandler<ChildIndexChangedEventArgs>? ChildIndexChanged;
+    }
+}

+ 2 - 5
src/Avalonia.Styling/Properties/AssemblyInfo.cs

@@ -1,12 +1,9 @@
-using System.Reflection;
 using System.Runtime.CompilerServices;
 using Avalonia.Metadata;
 
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Controls")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.LogicalTree")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Styling")]
-#if SIGNED_BUILD
+
 [assembly: InternalsVisibleTo("Avalonia.Styling.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
-#else
-[assembly: InternalsVisibleTo("Avalonia.Styling.UnitTests")]
-#endif
+

+ 56 - 0
src/Avalonia.Styling/Styling/Activators/NthChildActivator.cs

@@ -0,0 +1,56 @@
+#nullable enable
+using Avalonia.LogicalTree;
+
+namespace Avalonia.Styling.Activators
+{
+    /// <summary>
+    /// An <see cref="IStyleActivator"/> which is active when control's index was changed.
+    /// </summary>
+    internal sealed class NthChildActivator : StyleActivatorBase
+    {
+        private readonly ILogical _control;
+        private readonly IChildIndexProvider _provider;
+        private readonly int _step;
+        private readonly int _offset;
+        private readonly bool _reversed;
+
+        public NthChildActivator(
+            ILogical control,
+            IChildIndexProvider provider,
+            int step, int offset, bool reversed)
+        {
+            _control = control;
+            _provider = provider;
+            _step = step;
+            _offset = offset;
+            _reversed = reversed;
+        }
+
+        protected override void Initialize()
+        {
+            PublishNext(IsMatching());
+            _provider.ChildIndexChanged += ChildIndexChanged;
+        }
+
+        protected override void Deinitialize()
+        {
+            _provider.ChildIndexChanged -= ChildIndexChanged;
+        }
+
+        private void ChildIndexChanged(object sender, ChildIndexChangedEventArgs e)
+        {
+            // Run matching again if:
+            // 1. Selector is reversed, so other item insertion/deletion might affect total count without changing subscribed item index.
+            // 2. e.Child is null, when all children indeces were changed.
+            // 3. Subscribed child index was changed.
+            if (_reversed
+                || e.Child is null                
+                || e.Child == _control)
+            {
+                PublishNext(IsMatching());
+            }
+        }
+
+        private bool IsMatching() => NthChildSelector.Evaluate(_control, _provider, _step, _offset, _reversed).IsMatch;
+    }
+}

+ 145 - 0
src/Avalonia.Styling/Styling/NthChildSelector.cs

@@ -0,0 +1,145 @@
+#nullable enable
+using System;
+using System.Text;
+using Avalonia.LogicalTree;
+using Avalonia.Styling.Activators;
+
+namespace Avalonia.Styling
+{
+    /// <summary>
+    /// The :nth-child() pseudo-class matches elements based on their position in a group of siblings.
+    /// </summary>
+    /// <remarks>
+    /// Element indices are 1-based.
+    /// </remarks>
+    public class NthChildSelector : Selector
+    {
+        private const string NthChildSelectorName = "nth-child";
+        private const string NthLastChildSelectorName = "nth-last-child";
+        private readonly Selector? _previous;
+        private readonly bool _reversed;
+
+        internal protected NthChildSelector(Selector? previous, int step, int offset, bool reversed)
+        {
+            _previous = previous;
+            Step = step;
+            Offset = offset;
+            _reversed = reversed;
+        }
+
+        /// <summary>
+        /// Creates an instance of <see cref="NthChildSelector"/>
+        /// </summary>
+        /// <param name="previous">Previous selector.</param>
+        /// <param name="step">Position step.</param>
+        /// <param name="offset">Initial index offset.</param>
+        public NthChildSelector(Selector? previous, int step, int offset)
+            : this(previous, step, offset, false)
+        {
+
+        }
+
+        public override bool InTemplate => _previous?.InTemplate ?? false;
+
+        public override bool IsCombinator => false;
+
+        public override Type? TargetType => _previous?.TargetType;
+
+        public int Step { get; }
+        public int Offset { get; }
+
+        protected override SelectorMatch Evaluate(IStyleable control, bool subscribe)
+        {
+            if (!(control is ILogical logical))
+            {
+                return SelectorMatch.NeverThisType;
+            }
+
+            var controlParent = logical.LogicalParent;
+
+            if (controlParent is IChildIndexProvider childIndexProvider)
+            {
+                return subscribe
+                    ? new SelectorMatch(new NthChildActivator(logical, childIndexProvider, Step, Offset, _reversed))
+                    : Evaluate(logical, childIndexProvider, Step, Offset, _reversed);
+            }
+            else
+            {
+                return SelectorMatch.NeverThisInstance;
+            }
+        }
+
+        internal static SelectorMatch Evaluate(
+            ILogical logical, IChildIndexProvider childIndexProvider,
+            int step, int offset, bool reversed)
+        {
+            var index = childIndexProvider.GetChildIndex(logical);
+            if (index < 0)
+            {
+                return SelectorMatch.NeverThisInstance;
+            }
+
+            if (reversed)
+            {
+                if (childIndexProvider.TryGetTotalCount(out var totalCountValue))
+                {
+                    index = totalCountValue - index;
+                }
+                else
+                {
+                    return SelectorMatch.NeverThisInstance;
+                }
+            }
+            else
+            {
+                // nth child index is 1-based
+                index += 1;
+            }
+
+            var n = Math.Sign(step);
+
+            var diff = index - offset;
+            var match = diff == 0 || (Math.Sign(diff) == n && diff % step == 0);
+
+            return match ? SelectorMatch.AlwaysThisInstance : SelectorMatch.NeverThisInstance;
+        }
+
+        protected override Selector? MovePrevious() => _previous;
+
+        public override string ToString()
+        {
+            var expectedCapacity = NthLastChildSelectorName.Length + 8;
+            var stringBuilder = new StringBuilder(_previous?.ToString(), expectedCapacity);
+            
+            stringBuilder.Append(':');
+            stringBuilder.Append(_reversed ? NthLastChildSelectorName : NthChildSelectorName);
+            stringBuilder.Append('(');
+
+            var hasStep = false;
+            if (Step != 0)
+            {
+                hasStep = true;
+                stringBuilder.Append(Step);
+                stringBuilder.Append('n');
+            }
+
+            if (Offset > 0)
+            {
+                if (hasStep)
+                {
+                    stringBuilder.Append('+');
+                }
+                stringBuilder.Append(Offset);
+            }
+            else if (Offset < 0)
+            {
+                stringBuilder.Append('-');
+                stringBuilder.Append(-Offset);
+            }
+
+            stringBuilder.Append(')');
+
+            return stringBuilder.ToString();
+        }
+    }
+}

+ 23 - 0
src/Avalonia.Styling/Styling/NthLastChildSelector.cs

@@ -0,0 +1,23 @@
+#nullable enable
+
+namespace Avalonia.Styling
+{
+    /// <summary>
+    /// The :nth-child() pseudo-class matches elements based on their position among a group of siblings, counting from the end.
+    /// </summary>
+    /// <remarks>
+    /// Element indices are 1-based.
+    /// </remarks>
+    public class NthLastChildSelector : NthChildSelector
+    {
+        /// <summary>
+        /// Creates an instance of <see cref="NthLastChildSelector"/>
+        /// </summary>
+        /// <param name="previous">Previous selector.</param>
+        /// <param name="step">Position step.</param>
+        /// <param name="offset">Initial index offset, counting from the end.</param>
+        public NthLastChildSelector(Selector? previous, int step, int offset) : base(previous, step, offset, true)
+        {
+        }
+    }
+}

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

@@ -123,6 +123,22 @@ namespace Avalonia.Styling
             return new NotSelector(previous, argument);
         }
 
+        /// <inheritdoc cref="NthChildSelector"/>
+        /// <inheritdoc cref="NthChildSelector(Selector?, int, int)"/>
+        /// <returns>The selector.</returns>
+        public static Selector NthChild(this Selector previous, int step, int offset)
+        {
+            return new NthChildSelector(previous, step, offset);
+        }
+
+        /// <inheritdoc cref="NthLastChildSelector"/>
+        /// <inheritdoc cref="NthLastChildSelector(Selector?, int, int)"/>
+        /// <returns>The selector.</returns>
+        public static Selector NthLastChild(this Selector previous, int step, int offset)
+        {
+            return new NthLastChildSelector(previous, step, offset);
+        }
+
         /// <summary>
         /// Returns a selector which matches a type.
         /// </summary>

+ 2 - 1
src/Avalonia.Themes.Default/AutoCompleteBox.xaml

@@ -21,7 +21,8 @@
                  MaxHeight="{TemplateBinding MaxDropDownHeight}"
                  PlacementTarget="{TemplateBinding}"
                  IsLightDismissEnabled="True">
-            <Border BorderBrush="{DynamicResource ThemeBorderMidBrush}"
+            <Border Background="{DynamicResource ThemeBackgroundBrush}"
+                    BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                     BorderThickness="1">
               <ListBox Name="PART_SelectingItemsControl"
                        BorderThickness="0"

+ 1 - 0
src/Avalonia.Themes.Default/Button.xaml

@@ -17,6 +17,7 @@
                           ContentTemplate="{TemplateBinding ContentTemplate}"
                           Content="{TemplateBinding Content}"
                           Padding="{TemplateBinding Padding}"
+                          RecognizesAccessKey="True"
                           TextBlock.Foreground="{TemplateBinding Foreground}"
                           HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                           VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>

+ 1 - 0
src/Avalonia.Themes.Default/CheckBox.xaml

@@ -41,6 +41,7 @@
                             ContentTemplate="{TemplateBinding ContentTemplate}"
                             Content="{TemplateBinding Content}"
                             Margin="{TemplateBinding Padding}"
+                            RecognizesAccessKey="True"
                             VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                             HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                             IsVisible="{TemplateBinding Content, Converter={x:Static ObjectConverters.IsNotNull}}"

+ 2 - 1
src/Avalonia.Themes.Default/ComboBox.xaml

@@ -69,7 +69,8 @@
                    MaxHeight="{TemplateBinding MaxDropDownHeight}"
                    PlacementTarget="{TemplateBinding}"
                    IsLightDismissEnabled="True">
-              <Border BorderBrush="{DynamicResource ThemeBorderMidBrush}"
+              <Border Background="{DynamicResource ThemeBackgroundBrush}"
+                      BorderBrush="{DynamicResource ThemeBorderMidBrush}"                      
                       BorderThickness="1">
                 <ScrollViewer HorizontalScrollBarVisibility="{TemplateBinding ScrollViewer.HorizontalScrollBarVisibility}"
                               VerticalScrollBarVisibility="{TemplateBinding ScrollViewer.VerticalScrollBarVisibility}">

+ 1 - 0
src/Avalonia.Themes.Default/ContextMenu.xaml

@@ -1,4 +1,5 @@
 <Style xmlns="https://github.com/avaloniaui" Selector="ContextMenu">
+    <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
     <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}"/>
     <Setter Property="BorderThickness" Value="1"/>
     <Setter Property="Padding" Value="4,2"/>

+ 1 - 1
src/Avalonia.Themes.Default/FlyoutPresenter.xaml

@@ -2,7 +2,7 @@
   <Style Selector="FlyoutPresenter">
     <Setter Property="HorizontalContentAlignment" Value="Stretch" />
     <Setter Property="VerticalContentAlignment" Value="Stretch" />
-    <Setter Property="Background" Value="Transparent" />
+    <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
     <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}" />
     <Setter Property="BorderThickness" Value="1" />
     <Setter Property="Padding" Value="4" />

+ 1 - 1
src/Avalonia.Themes.Default/MenuFlyoutPresenter.xaml

@@ -1,6 +1,6 @@
 <Styles xmlns="https://github.com/avaloniaui">
   <Style Selector="MenuFlyoutPresenter">
-    <Setter Property="Background" Value="Transparent" />
+    <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
     <Setter Property="BorderBrush" Value="{DynamicResource ThemeBorderMidBrush}" />
     <Setter Property="BorderThickness" Value="1" />
     <Setter Property="ScrollViewer.HorizontalScrollBarVisibility" Value="Auto" />

+ 2 - 2
src/Avalonia.Themes.Default/MenuItem.xaml

@@ -62,7 +62,7 @@
                    PlacementMode="Right"
                    IsLightDismissEnabled="False"
                    IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
-              <Border Background="{TemplateBinding Background}"
+              <Border Background="{DynamicResource ThemeBackgroundBrush}"
                       BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                       BorderThickness="{TemplateBinding BorderThickness}">
                 <ScrollViewer Classes="menuscroller">                  
@@ -113,7 +113,7 @@
                    IsLightDismissEnabled="True"
                    OverlayInputPassThroughElement="{Binding $parent[Menu]}"
                    IsOpen="{TemplateBinding IsSubMenuOpen, Mode=TwoWay}">
-              <Border Background="{TemplateBinding Background}"
+              <Border Background="{DynamicResource ThemeBackgroundBrush}"
                       BorderBrush="{DynamicResource ThemeBorderMidBrush}"
                       BorderThickness="{TemplateBinding BorderThickness}">
                 <ScrollViewer Classes="menuscroller">

+ 10 - 12
src/Avalonia.Themes.Default/OverlayPopupHost.xaml

@@ -1,23 +1,21 @@
 <Style xmlns="https://github.com/avaloniaui" 
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Selector="OverlayPopupHost">
-  <Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
-  <Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}"/>
+  <Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}" />
+  <Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}" />
   <Setter Property="FontFamily" Value="{x:Static FontFamily.Default}" />
   <Setter Property="FontWeight" Value="400" />
   <Setter Property="FontStyle" Value="Normal" />
   <Setter Property="Template">
     <ControlTemplate>
-      <Panel>
-        <Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
-        <VisualLayerManager IsPopup="True">
-          <ContentPresenter Name="PART_ContentPresenter"
-                            Background="{TemplateBinding Background}"
-                            ContentTemplate="{TemplateBinding ContentTemplate}"
-                            Content="{TemplateBinding Content}"
-                            Padding="{TemplateBinding Padding}"/>
-        </VisualLayerManager>
-      </Panel>
+      <!--  Do not forget to update Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent test  -->
+      <VisualLayerManager IsPopup="True">
+        <ContentPresenter Name="PART_ContentPresenter"
+                          Background="{TemplateBinding Background}"
+                          ContentTemplate="{TemplateBinding ContentTemplate}"
+                          Content="{TemplateBinding Content}"
+                          Padding="{TemplateBinding Padding}"/>
+      </VisualLayerManager>
     </ControlTemplate>
   </Setter>
 </Style>

+ 2 - 0
src/Avalonia.Themes.Default/PopupRoot.xaml

@@ -1,6 +1,8 @@
 <Style xmlns="https://github.com/avaloniaui"
        xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
        Selector="PopupRoot">
+  <Setter Property="Background" Value="{x:Null}"/>
+  <Setter Property="TransparencyLevelHint" Value="Transparent" />
   <Setter Property="Foreground" Value="{DynamicResource ThemeForegroundBrush}"/>
   <Setter Property="FontSize" Value="{DynamicResource FontSizeNormal}"/>
   <Setter Property="FontFamily" Value="{x:Static FontFamily.Default}" />

+ 1 - 0
src/Avalonia.Themes.Default/RadioButton.xaml

@@ -31,6 +31,7 @@
                             ContentTemplate="{TemplateBinding ContentTemplate}"
                             Content="{TemplateBinding Content}"
                             Margin="4,0,0,0"
+                            RecognizesAccessKey="True"
                             VerticalAlignment="Center"
                             Grid.Column="1"/>
         </Grid>

+ 1 - 0
src/Avalonia.Themes.Default/ToggleButton.xaml

@@ -17,6 +17,7 @@
                           ContentTemplate="{TemplateBinding ContentTemplate}"
                           Content="{TemplateBinding Content}"
                           Padding="{TemplateBinding Padding}"
+                          RecognizesAccessKey="True"
                           TextBlock.Foreground="{TemplateBinding Foreground}"
                           HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                           VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"/>

+ 1 - 0
src/Avalonia.Themes.Default/ToggleSwitch.xaml

@@ -87,6 +87,7 @@
             Grid.Row="0"
             Content="{TemplateBinding Content}"
             ContentTemplate="{TemplateBinding ContentTemplate}"
+            RecognizesAccessKey="True"
             VerticalAlignment="Top"/>
 
           <Grid Grid.Row="1"

+ 1 - 0
src/Avalonia.Themes.Fluent/Controls/Button.xaml

@@ -34,6 +34,7 @@
                           Content="{TemplateBinding Content}"
                           ContentTemplate="{TemplateBinding ContentTemplate}"
                           Padding="{TemplateBinding Padding}"
+                          RecognizesAccessKey="True"
                           HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                           VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
       </ControlTemplate>

+ 1 - 0
src/Avalonia.Themes.Fluent/Controls/CheckBox.xaml

@@ -44,6 +44,7 @@
                          ContentTemplate="{TemplateBinding ContentTemplate}"
                          Content="{TemplateBinding Content}"
                          Margin="{TemplateBinding Padding}"
+                         RecognizesAccessKey="True"
                          HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                          VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                          Grid.Column="1" />

+ 4 - 0
src/Avalonia.Themes.Fluent/Controls/NumericUpDown.xaml

@@ -38,6 +38,7 @@
                        BorderBrush="{TemplateBinding BorderBrush}"
                        CornerRadius="{TemplateBinding CornerRadius}"
                        Padding="0"
+                       MinWidth="{TemplateBinding MinWidth}"
                        HorizontalContentAlignment="Stretch"
                        VerticalContentAlignment="Stretch"
                        AllowSpin="{TemplateBinding AllowSpin}"
@@ -49,6 +50,9 @@
                    BorderBrush="Transparent"
                    Margin="-1"
                    Padding="{TemplateBinding Padding}"
+                   MinWidth="{TemplateBinding MinWidth}"
+                   Foreground="{TemplateBinding Foreground}"
+                   FontSize="{TemplateBinding FontSize}"
                    Watermark="{TemplateBinding Watermark}"
                    IsReadOnly="{TemplateBinding IsReadOnly}"
                    VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}"

+ 7 - 10
src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml

@@ -6,16 +6,13 @@
   <Setter Property="FontStyle" Value="Normal" />
   <Setter Property="Template">
     <ControlTemplate>
-      <Panel>
-        <Border Name="PART_TransparencyFallback" IsHitTestVisible="False" />
-        <VisualLayerManager IsPopup="True">
-          <ContentPresenter Name="PART_ContentPresenter"
-                            Background="{TemplateBinding Background}"
-                            ContentTemplate="{TemplateBinding ContentTemplate}"
-                            Content="{TemplateBinding Content}"
-                            Padding="{TemplateBinding Padding}"/>
-        </VisualLayerManager>
-      </Panel>
+      <VisualLayerManager IsPopup="True">
+        <ContentPresenter Name="PART_ContentPresenter"
+                          Background="{TemplateBinding Background}"
+                          ContentTemplate="{TemplateBinding ContentTemplate}"
+                          Content="{TemplateBinding Content}"
+                          Padding="{TemplateBinding Padding}"/>
+      </VisualLayerManager>
     </ControlTemplate>
   </Setter>
 </Style>

+ 1 - 0
src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml

@@ -1,6 +1,7 @@
 <Styles xmlns="https://github.com/avaloniaui"
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
   <Style Selector="PopupRoot">
+    <Setter Property="Background" Value="{x:Null}"/>
     <Setter Property="TransparencyLevelHint" Value="Transparent" />
     <Setter Property="Foreground" Value="{DynamicResource SystemControlForegroundBaseHighBrush}"/>
     <Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}"/>

+ 1 - 0
src/Avalonia.Themes.Fluent/Controls/RadioButton.xaml

@@ -51,6 +51,7 @@
                               ContentTemplate="{TemplateBinding ContentTemplate}"
                               TextBlock.Foreground="{TemplateBinding Foreground}"
                               Margin="{TemplateBinding Padding}"
+                              RecognizesAccessKey="True"
                               HorizontalAlignment="{TemplateBinding HorizontalContentAlignment}"
                               VerticalAlignment="{TemplateBinding VerticalContentAlignment}"
                               Grid.Column="1" />

+ 1 - 0
src/Avalonia.Themes.Fluent/Controls/ToggleButton.xaml

@@ -34,6 +34,7 @@
                           Content="{TemplateBinding Content}"
                           ContentTemplate="{TemplateBinding ContentTemplate}"
                           Padding="{TemplateBinding Padding}"
+                          RecognizesAccessKey="True"
                           HorizontalContentAlignment="{TemplateBinding HorizontalContentAlignment}"
                           VerticalContentAlignment="{TemplateBinding VerticalContentAlignment}" />
       </ControlTemplate>

+ 1 - 0
src/Avalonia.Themes.Fluent/Controls/ToggleSwitch.xaml

@@ -53,6 +53,7 @@
             Grid.Row="0"
             Content="{TemplateBinding Content}"
             ContentTemplate="{TemplateBinding ContentTemplate}"
+            RecognizesAccessKey="True"
             VerticalAlignment="Top"/>
 
           <Grid Grid.Row="1"

+ 1 - 8
src/Avalonia.Visuals/Properties/AssemblyInfo.cs

@@ -1,4 +1,3 @@
-using System.Reflection;
 using System.Runtime.CompilerServices;
 using Avalonia.Metadata;
 
@@ -8,14 +7,8 @@ using Avalonia.Metadata;
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Media.Transformation")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia")]
 
-#if SIGNED_BUILD
 [assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
 [assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
 [assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
 [assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
-#else
-[assembly: InternalsVisibleTo("Avalonia.Visuals.UnitTests")]
-[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]
-[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")]
-[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests")]
-#endif
+

+ 17 - 2
src/Avalonia.X11/X11Platform.cs

@@ -1,5 +1,6 @@
 using System;
 using System.Collections.Generic;
+using System.Linq;
 using System.Reflection;
 using System.Runtime.InteropServices;
 using Avalonia.Controls;
@@ -104,11 +105,25 @@ namespace Avalonia.X11
         public IntPtr DeferredDisplay { get; set; }
         public IntPtr Display { get; set; }
 
-        public ITrayIconImpl CreateTrayIcon ()
+        private static uint[] X11IconConverter(IWindowIconImpl icon)
         {
-            return new X11TrayIconImpl();
+            if (!(icon is X11IconData x11icon))
+                return Array.Empty<uint>();
+
+            return x11icon.Data.Select(x => x.ToUInt32()).ToArray();
         }
 
+        public ITrayIconImpl CreateTrayIcon()
+        {
+            var dbusTrayIcon = new DBusTrayIconImpl();
+
+            if (!dbusTrayIcon.IsActive) return new XEmbedTrayIconImpl();
+
+            dbusTrayIcon.IconConverterDelegate = X11IconConverter;
+
+            return dbusTrayIcon;
+        }
+        
         public IWindowImpl CreateWindow()
         {
             return new X11Window(this, null);

+ 0 - 2
src/Avalonia.X11/X11Window.Ime.cs

@@ -33,8 +33,6 @@ namespace Avalonia.X11
                         && ((int)(style & XIMProperties.XIMStatusNothing) != 0))
                     {
                         XPoint spot = default;
-                        XRectangle area = default;
-
 
                         //using var areaS = new Utf8Buffer("area");
                         using var spotS = new Utf8Buffer("spotLocation");

+ 47 - 0
src/Avalonia.X11/XEmbedTrayIconImpl.cs

@@ -0,0 +1,47 @@
+using System;
+using Avalonia.Controls.Platform;
+using Avalonia.Logging;
+using Avalonia.Platform;
+
+namespace Avalonia.X11
+{
+    internal class XEmbedTrayIconImpl : ITrayIconImpl
+    {
+
+        private bool _isCalled;
+
+        private void NotImplemented()
+        {
+            if(_isCalled) return;
+            
+            Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)
+                ?.Log(this,
+                    "TODO: XEmbed System Tray Icons is not implemented yet. Tray icons won't be available on this system.");
+
+            _isCalled = true;
+        }
+        
+        public void Dispose()
+        {
+            NotImplemented();
+        }
+
+        public void SetIcon(IWindowIconImpl icon)
+        {
+             NotImplemented();
+        }
+
+        public void SetToolTipText(string text)
+        {
+            NotImplemented();
+        }
+
+        public void SetIsVisible(bool visible)
+        {
+            NotImplemented();
+        }
+
+        public INativeMenuExporter MenuExporter { get; }
+        public Action OnClicked { get; set; }
+    }
+}

+ 35 - 0
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs

@@ -97,6 +97,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
                         case SelectorGrammar.NotSyntax not:
                             result = new XamlIlNotSelector(result, Create(not.Argument, typeResolver));
                             break;
+                        case SelectorGrammar.NthChildSyntax nth:
+                            result = new XamlIlNthChildSelector(result, nth.Step, nth.Offset, XamlIlNthChildSelector.SelectorType.NthChild);
+                            break;
+                        case SelectorGrammar.NthLastChildSyntax nth:
+                            result = new XamlIlNthChildSelector(result, nth.Step, nth.Offset, XamlIlNthChildSelector.SelectorType.NthLastChild);
+                            break;
                         case SelectorGrammar.CommaSyntax comma:
                             if (results == null) 
                                 results = new XamlIlOrSelectorNode(node, selectorType);
@@ -273,6 +279,35 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers
         }
     }
 
+    class XamlIlNthChildSelector : XamlIlSelectorNode
+    {
+        private readonly int _step;
+        private readonly int _offset;
+        private readonly SelectorType _type;
+
+        public enum SelectorType
+        {
+            NthChild,
+            NthLastChild
+        }
+
+        public XamlIlNthChildSelector(XamlIlSelectorNode previous, int step, int offset, SelectorType type) : base(previous)
+        {
+            _step = step;
+            _offset = offset;
+            _type = type;
+        }
+
+        public override IXamlType TargetType => Previous?.TargetType;
+        protected override void DoEmit(XamlEmitContext<IXamlILEmitter, XamlILNodeEmitResult> context, IXamlILEmitter codeGen)
+        {
+            codeGen.Ldc_I4(_step);
+            codeGen.Ldc_I4(_offset);
+            EmitCall(context, codeGen,
+                m => m.Name == _type.ToString() && m.Parameters.Count == 3);
+        }
+    }
+
     class XamlIlPropertyEqualsSelector : XamlIlSelectorNode
     {
         public XamlIlPropertyEqualsSelector(XamlIlSelectorNode previous,

+ 2 - 5
src/Markup/Avalonia.Markup.Xaml/Properties/AssemblyInfo.cs

@@ -1,12 +1,9 @@
-using System.Reflection;
 using Avalonia.Metadata;
 using System.Runtime.CompilerServices;
 
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Markup.Xaml.MarkupExtensions")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Markup.Xaml.Styling")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Markup.Xaml.Templates")]
-#if SIGNED_BUILD
+
 [assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
-#else
-[assembly: InternalsVisibleTo("Avalonia.Markup.Xaml.UnitTests")]
-#endif
+

+ 147 - 2
src/Markup/Avalonia.Markup/Markup/Parsers/SelectorGrammar.cs

@@ -160,11 +160,13 @@ namespace Avalonia.Markup.Parsers
 
             if (identifier.IsEmpty)
             {
-                throw new ExpressionParseException(r.Position, "Expected class name or is selector after ':'.");
+                throw new ExpressionParseException(r.Position, "Expected class name, is, nth-child or nth-last-child selector after ':'.");
             }
 
             const string IsKeyword = "is";
             const string NotKeyword = "not";
+            const string NthChildKeyword = "nth-child";
+            const string NthLastChildKeyword = "nth-last-child";
 
             if (identifier.SequenceEqual(IsKeyword.AsSpan()) && r.TakeIf('('))
             {
@@ -181,6 +183,20 @@ namespace Avalonia.Markup.Parsers
                 var syntax = new NotSyntax { Argument = argument };
                 return (State.Middle, syntax);
             }
+            if (identifier.SequenceEqual(NthChildKeyword.AsSpan()) && r.TakeIf('('))
+            {
+                var (step, offset) = ParseNthChildArguments(ref r);
+
+                var syntax = new NthChildSyntax { Step = step, Offset = offset };
+                return (State.Middle, syntax);
+            }
+            if (identifier.SequenceEqual(NthLastChildKeyword.AsSpan()) && r.TakeIf('('))
+            {
+                var (step, offset) = ParseNthChildArguments(ref r);
+
+                var syntax = new NthLastChildSyntax { Step = step, Offset = offset };
+                return (State.Middle, syntax);
+            }
             else
             {
                 return (
@@ -191,7 +207,6 @@ namespace Avalonia.Markup.Parsers
                     });
             }
         }
-
         private static (State, ISyntax?) ParseTraversal(ref CharacterReader r)
         {
             r.SkipWhitespace();
@@ -302,6 +317,114 @@ namespace Avalonia.Markup.Parsers
             return syntax;
         }
 
+        private static (int step, int offset) ParseNthChildArguments(ref CharacterReader r)
+        {
+            int step = 0;
+            int offset = 0;
+
+            if (r.Peek == 'o')
+            {
+                var constArg = r.TakeUntil(')').ToString().Trim();
+                if (constArg.Equals("odd", StringComparison.Ordinal))
+                {
+                    step = 2;
+                    offset = 1;
+                }
+                else
+                {
+                    throw new ExpressionParseException(r.Position, $"Expected nth-child(odd). Actual '{constArg}'.");
+                }
+            }
+            else if (r.Peek == 'e')
+            {
+                var constArg = r.TakeUntil(')').ToString().Trim();
+                if (constArg.Equals("even", StringComparison.Ordinal))
+                {
+                    step = 2;
+                    offset = 0;
+                }
+                else
+                {
+                    throw new ExpressionParseException(r.Position, $"Expected nth-child(even). Actual '{constArg}'.");
+                }
+            }
+            else
+            {
+                r.SkipWhitespace();
+
+                var stepOrOffset = 0;
+                var stepOrOffsetStr = r.TakeWhile(c => char.IsDigit(c) || c == '-' || c == '+').ToString();
+                if (stepOrOffsetStr.Length == 0
+                    || (stepOrOffsetStr.Length == 1
+                    && stepOrOffsetStr[0] == '+'))
+                {
+                    stepOrOffset = 1;
+                }
+                else if (stepOrOffsetStr.Length == 1
+                    && stepOrOffsetStr[0] == '-')
+                {
+                    stepOrOffset = -1;
+                }
+                else if (!int.TryParse(stepOrOffsetStr.ToString(), out stepOrOffset))
+                {
+                    throw new ExpressionParseException(r.Position, "Couldn't parse nth-child step or offset value. Integer was expected.");
+                }
+
+                r.SkipWhitespace();
+
+                if (r.Peek == ')')
+                {
+                    step = 0;
+                    offset = stepOrOffset;
+                }
+                else
+                {
+                    step = stepOrOffset;
+
+                    if (r.Peek != 'n')
+                    {
+                        throw new ExpressionParseException(r.Position, "Couldn't parse nth-child step value, \"xn+y\" pattern was expected.");
+                    }
+
+                    r.Skip(1); // skip 'n'
+
+                    r.SkipWhitespace();
+
+                    if (r.Peek != ')')
+                    {
+                        int sign;
+                        var nextChar = r.Take();
+                        if (nextChar == '+')
+                        {
+                            sign = 1;
+                        }
+                        else if (nextChar == '-')
+                        {
+                            sign = -1;
+                        }
+                        else
+                        {
+                            throw new ExpressionParseException(r.Position, "Couldn't parse nth-child sign. '+' or '-' was expected.");
+                        }
+
+                        r.SkipWhitespace();
+
+                        if (sign != 0
+                            && !int.TryParse(r.TakeUntil(')').ToString(), out offset))
+                        {
+                            throw new ExpressionParseException(r.Position, "Couldn't parse nth-child offset value. Integer was expected.");
+                        }
+
+                        offset *= sign;
+                    }
+                }
+            }
+
+            Expect(ref r, ')');
+
+            return (step, offset);
+        }
+
         private static void Expect(ref CharacterReader r, char c)
         {
             if (r.End)
@@ -419,6 +542,28 @@ namespace Avalonia.Markup.Parsers
             }
         }
 
+        public class NthChildSyntax : ISyntax
+        {
+            public int Offset { get; set; }
+            public int Step { get; set; }
+
+            public override bool Equals(object? obj)
+            {
+                return (obj is NthChildSyntax nth) && nth.Offset == Offset && nth.Step == Step;
+            }
+        }
+
+        public class NthLastChildSyntax : ISyntax
+        {
+            public int Offset { get; set; }
+            public int Step { get; set; }
+
+            public override bool Equals(object? obj)
+            {
+                return (obj is NthLastChildSyntax nth) && nth.Offset == Offset && nth.Step == Step;
+            }
+        }
+
         public class CommaSyntax : ISyntax
         {
             public override bool Equals(object? obj)

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

@@ -104,6 +104,12 @@ namespace Avalonia.Markup.Parsers
                     case SelectorGrammar.NotSyntax not:
                         result = result.Not(x => Create(not.Argument));
                         break;
+                    case SelectorGrammar.NthChildSyntax nth:
+                        result = result.NthChild(nth.Step, nth.Offset);
+                        break;
+                    case SelectorGrammar.NthLastChildSyntax nth:
+                        result = result.NthLastChild(nth.Step, nth.Offset);
+                        break;
                     case SelectorGrammar.CommaSyntax comma:
                         if (results == null)
                         {

+ 2 - 5
src/Markup/Avalonia.Markup/Properties/AssemblyInfo.cs

@@ -1,11 +1,8 @@
-using System.Reflection;
 using Avalonia.Metadata;
 using System.Runtime.CompilerServices;
 
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Data")]
 [assembly: XmlnsDefinition("https://github.com/avaloniaui", "Avalonia.Markup.Data")]
-#if SIGNED_BUILD
+
 [assembly: InternalsVisibleTo("Avalonia.Markup.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
-#else
-[assembly: InternalsVisibleTo("Avalonia.Markup.UnitTests")]
-#endif
+

+ 1 - 1
src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs

@@ -66,7 +66,7 @@ namespace Avalonia.Skia
                 _canCreateSurfaces = true;
                 return surface;
             }
-            catch (Exception e)
+            catch (Exception)
             {
                 Logger.TryGet(LogEventLevel.Error, "OpenGL")
                     ?.Log(this, "Unable to create a Skia-compatible FBO manually");

+ 2 - 5
src/Skia/Avalonia.Skia/Properties/AssemblyInfo.cs

@@ -1,8 +1,5 @@
 using System.Runtime.CompilerServices;
-#if SIGNED_BUILD
+
 [assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
 [assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
-#else
-[assembly: InternalsVisibleTo("Avalonia.Skia.RenderTests")]
-[assembly: InternalsVisibleTo("Avalonia.Skia.UnitTests")]
-#endif
+

+ 4 - 3
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@@ -27,15 +27,16 @@ namespace Avalonia.Skia
             {
                 return typeface;
             }
+            
+            var initialWeight = (int)key.Weight;
 
             var weight = (int)key.Weight;
 
-            weight -= weight % 100; // make sure we start at a full weight
+            weight -= weight % 50; // make sure we start at a full weight
 
             for (var i = 0; i < 2; i++)
             {
-                // only try 2 font weights in each direction
-                for (var j = 0; j < 200; j += 100)
+                for (var j = 0; j < initialWeight; j += 50)
                 {
                     if (weight - j >= 100)
                     {

+ 0 - 6
src/Windows/Avalonia.Direct2D1/Properties/AssemblyInfo.cs

@@ -1,4 +1,3 @@
-using System.Reflection;
 using System.Runtime.CompilerServices;
 using Avalonia.Platform;
 using Avalonia.Direct2D1;
@@ -6,10 +5,5 @@ using Avalonia.Direct2D1;
 [assembly: ExportRenderingSubsystem(OperatingSystemType.WinNT, 1, "Direct2D1", typeof(Direct2D1Platform), nameof(Direct2D1Platform.Initialize),
     typeof(Direct2DChecker))]
 
-#if SIGNED_BUILD
 [assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
 [assembly: InternalsVisibleTo("Avalonia.Direct2D1.UnitTests, PublicKey=0024000004800000940000000602000000240000525341310004000001000100c1bba1142285fe0419326fb25866ba62c47e6c2b5c1ab0c95b46413fad375471232cb81706932e1cef38781b9ebd39d5100401bacb651c6c5bbf59e571e81b3bc08d2a622004e08b1a6ece82a7e0b9857525c86d2b95fab4bc3dce148558d7f3ae61aa3a234086902aeface87d9dfdd32b9d2fe3c6dd4055b5ab4b104998bd87")]
-#else
-[assembly: InternalsVisibleTo("Avalonia.Direct2D1.RenderTests")]
-[assembly: InternalsVisibleTo("Avalonia.Direct2D1.UnitTests")]
-#endif

+ 8 - 0
src/tools/MicroComGenerator/Program.cs

@@ -35,8 +35,16 @@ namespace MicroComGenerator
 
             if (opts.CppOutput != null)
                 File.WriteAllText(opts.CppOutput, CppGen.GenerateCpp(ast));
+            
             if (opts.CSharpOutput != null)
+            {
                 File.WriteAllText(opts.CSharpOutput, new CSharpGen(ast).Generate());
+                
+                // HACK: Can't work out how to get the VS project system's fast up-to-date checks
+                // to ignore the generated code, so as a workaround set the write time to that of
+                // the input.
+                File.SetLastWriteTime(opts.CSharpOutput, File.GetLastWriteTime(opts.Input));
+            }
             
             return 0;
         }

+ 14 - 0
tests/Avalonia.Controls.UnitTests/ButtonTests.cs

@@ -2,6 +2,7 @@
 using System.Windows.Input;
 using Avalonia.Data;
 using Avalonia.Input;
+using Avalonia.Interactivity;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Rendering;
@@ -271,6 +272,19 @@ namespace Avalonia.Controls.UnitTests
 
         [Fact]
         public void Button_Invokes_CanExecute_When_CommandParameter_Changed()
+        {
+            var target = new Button();
+            var raised = 0;
+
+            target.Click += (s, e) => ++raised;
+
+            target.RaiseEvent(new RoutedEventArgs(AccessKeyHandler.AccessKeyPressedEvent));
+
+            Assert.Equal(1, raised);
+        }
+
+        [Fact]
+        public void Raises_Click_When_AccessKey_Raised()
         {
             var command = new TestCommand(p => p is bool value && value);
             var target = new Button { Command = command };

+ 17 - 0
tests/Avalonia.Controls.UnitTests/Platform/DefaultMenuInteractionHandlerTests.cs

@@ -1,5 +1,6 @@
 using System;
 using Avalonia.Controls.Platform;
+using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.VisualTree;
@@ -540,6 +541,22 @@ namespace Avalonia.Controls.UnitTests.Platform
                 Mock.Get(item).Verify(x => x.MoveSelection(NavigationDirection.First, true), Times.Never);
                 Assert.True(e.Handled);
             }
+
+            [Fact]
+            public void PointerPressed_On_Disabled_Item_Doesnt_Close_SubMenu()
+            {
+                var target = new DefaultMenuInteractionHandler(false);
+                var menu = Mock.Of<IMenu>();
+                var parentItem = Mock.Of<IMenuItem>(x => x.IsTopLevel == true && x.HasSubMenu == true && x.IsSubMenuOpen == true && x.Parent == menu);
+                var popup = new Popup();
+                var e = CreatePressed(popup);
+                
+                ((ISetLogicalParent)popup).SetParent(parentItem);
+                target.PointerPressed(parentItem, e);
+
+                Mock.Get(parentItem).Verify(x => x.Close(), Times.Never);
+                Assert.True(e.Handled);
+            }
         }
 
         public class ContextMenu

+ 53 - 22
tests/Avalonia.Controls.UnitTests/Primitives/PopupTests.cs

@@ -294,6 +294,7 @@ namespace Avalonia.Controls.UnitTests.Primitives
         [Fact]
         public void Templated_Control_With_Popup_In_Template_Should_Set_TemplatedParent()
         {
+            // Test uses OverlayPopupHost default template
             using (CreateServices())
             {
                 PopupContentControl target;
@@ -316,33 +317,63 @@ namespace Avalonia.Controls.UnitTests.Primitives
                 var children = popupRoot.GetVisualDescendants().ToList();
                 var types = children.Select(x => x.GetType().Name).ToList();
 
-                Assert.Equal(
-                    new[]
-                    {
-                        "Panel",
-                        "Border",
-                        "VisualLayerManager",
-                        "ContentPresenter",
-                        "ContentPresenter",
-                        "Border",
-                    },
-                    types);
+                if (UsePopupHost)
+                {
+                    Assert.Equal(
+                        new[]
+                        {
+                            "VisualLayerManager",
+                            "ContentPresenter",
+                            "ContentPresenter",
+                            "Border",
+                        },
+                        types);
+                }
+                else
+                {
+                    Assert.Equal(
+                        new[]
+                        {
+                            "Panel",
+                            "Border",
+                            "VisualLayerManager",
+                            "ContentPresenter",
+                            "ContentPresenter",
+                            "Border",
+                        },
+                        types);
+                }
 
                 var templatedParents = children
                     .OfType<IControl>()
                     .Select(x => x.TemplatedParent).ToList();
 
-                Assert.Equal(
-                    new object[]
-                    {
-                        popupRoot,
-                        popupRoot,
-                        popupRoot,
-                        popupRoot,
-                        target,
-                        null,
-                    },
-                    templatedParents);
+                if (UsePopupHost)
+                {
+                    Assert.Equal(
+                        new object[]
+                        {
+                            popupRoot,
+                            popupRoot,
+                            target,
+                            null,
+                        },
+                        templatedParents);
+                }
+                else
+                {
+                    Assert.Equal(
+                        new object[]
+                        {
+                            popupRoot,
+                            popupRoot,
+                            popupRoot,
+                            popupRoot,
+                            target,
+                            null,
+                        },
+                        templatedParents);
+                }
             }
         }
 

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

@@ -236,6 +236,165 @@ namespace Avalonia.Markup.UnitTests.Parsers
                 result);
         }
 
+        [Theory]
+        [InlineData(":nth-child(xn+2)")]
+        [InlineData(":nth-child(2n+b)")]
+        [InlineData(":nth-child(2n+)")]
+        [InlineData(":nth-child(2na)")]
+        [InlineData(":nth-child(2x+1)")]
+        public void NthChild_Invalid_Inputs(string input)
+        {
+            Assert.Throws<ExpressionParseException>(() => SelectorGrammar.Parse(input));
+        }
+
+        [Theory]
+        [InlineData(":nth-child(+1)", 0, 1)]
+        [InlineData(":nth-child(1)", 0, 1)]
+        [InlineData(":nth-child(-1)", 0, -1)]
+        [InlineData(":nth-child(2n+1)", 2, 1)]
+        [InlineData(":nth-child(n)", 1, 0)]
+        [InlineData(":nth-child(+n)", 1, 0)]
+        [InlineData(":nth-child(-n)", -1, 0)]
+        [InlineData(":nth-child(-2n)", -2, 0)]
+        [InlineData(":nth-child(n+5)", 1, 5)]
+        [InlineData(":nth-child(n-5)", 1, -5)]
+        [InlineData(":nth-child( 2n + 1 )", 2, 1)]
+        [InlineData(":nth-child( 2n - 1 )", 2, -1)]
+        public void NthChild_Variations(string input, int step, int offset)
+        {
+            var result = SelectorGrammar.Parse(input);
+
+            Assert.Equal(
+                new SelectorGrammar.ISyntax[]
+                {
+                    new SelectorGrammar.NthChildSyntax()
+                    {
+                        Step = step,
+                        Offset = offset
+                    }
+                },
+                result);
+        }
+
+        [Theory]
+        [InlineData(":nth-last-child(+1)", 0, 1)]
+        [InlineData(":nth-last-child(1)", 0, 1)]
+        [InlineData(":nth-last-child(-1)", 0, -1)]
+        [InlineData(":nth-last-child(2n+1)", 2, 1)]
+        [InlineData(":nth-last-child(n)", 1, 0)]
+        [InlineData(":nth-last-child(+n)", 1, 0)]
+        [InlineData(":nth-last-child(-n)", -1, 0)]
+        [InlineData(":nth-last-child(-2n)", -2, 0)]
+        [InlineData(":nth-last-child(n+5)", 1, 5)]
+        [InlineData(":nth-last-child(n-5)", 1, -5)]
+        [InlineData(":nth-last-child( 2n + 1 )", 2, 1)]
+        [InlineData(":nth-last-child( 2n - 1 )", 2, -1)]
+        public void NthLastChild_Variations(string input, int step, int offset)
+        {
+            var result = SelectorGrammar.Parse(input);
+
+            Assert.Equal(
+                new SelectorGrammar.ISyntax[]
+                {
+                    new SelectorGrammar.NthLastChildSyntax()
+                    {
+                        Step = step,
+                        Offset = offset
+                    }
+                },
+                result);
+        }
+
+        [Fact]
+        public void OfType_NthChild()
+        {
+            var result = SelectorGrammar.Parse("Button:nth-child(2n+1)");
+
+            Assert.Equal(
+                new SelectorGrammar.ISyntax[]
+                {
+                    new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
+                    new SelectorGrammar.NthChildSyntax()
+                    {
+                        Step = 2,
+                        Offset = 1
+                    }
+                },
+                result);
+        }
+
+        [Fact]
+        public void OfType_NthChild_Without_Offset()
+        {
+            var result = SelectorGrammar.Parse("Button:nth-child(2147483647n)");
+
+            Assert.Equal(
+                new SelectorGrammar.ISyntax[]
+                {
+                    new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
+                    new SelectorGrammar.NthChildSyntax()
+                    {
+                        Step = int.MaxValue,
+                        Offset = 0
+                    }
+                },
+                result);
+        }
+
+        [Fact]
+        public void OfType_NthLastChild()
+        {
+            var result = SelectorGrammar.Parse("Button:nth-last-child(2n+1)");
+
+            Assert.Equal(
+                new SelectorGrammar.ISyntax[]
+                {
+                    new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
+                    new SelectorGrammar.NthLastChildSyntax()
+                    {
+                        Step = 2,
+                        Offset = 1
+                    }
+                },
+                result);
+        }
+
+        [Fact]
+        public void OfType_NthChild_Odd()
+        {
+            var result = SelectorGrammar.Parse("Button:nth-child(odd)");
+
+            Assert.Equal(
+                new SelectorGrammar.ISyntax[]
+                {
+                    new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
+                    new SelectorGrammar.NthChildSyntax()
+                    {
+                        Step = 2,
+                        Offset = 1
+                    }
+                },
+                result);
+        }
+
+        [Fact]
+        public void OfType_NthChild_Even()
+        {
+            var result = SelectorGrammar.Parse("Button:nth-child(even)");
+
+            Assert.Equal(
+                new SelectorGrammar.ISyntax[]
+                {
+                    new SelectorGrammar.OfTypeSyntax { TypeName = "Button" },
+                    new SelectorGrammar.NthChildSyntax()
+                    {
+                        Step = 2,
+                        Offset = 0
+                    }
+                },
+                result);
+        }
+
         [Fact]
         public void Is_Descendent_Not_OfType_Class()
         {

+ 196 - 1
tests/Avalonia.Markup.Xaml.UnitTests/Xaml/StyleTests.cs

@@ -1,6 +1,8 @@
+using System.Collections.Generic;
+using System.Collections.ObjectModel;
+using System.Linq;
 using System.Xml;
 using Avalonia.Controls;
-using Avalonia.Markup.Data;
 using Avalonia.Markup.Xaml.Styling;
 using Avalonia.Markup.Xaml.Templates;
 using Avalonia.Media;
@@ -267,6 +269,199 @@ namespace Avalonia.Markup.Xaml.UnitTests.Xaml
             }
         }
 
+        [Fact]
+        public void Style_Can_Use_NthChild_Selector()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Styles>
+        <Style Selector='Border.foo:nth-child(2n+1)'>
+            <Setter Property='Background' Value='Red'/>
+        </Style>
+    </Window.Styles>
+    <StackPanel>
+        <Border x:Name='b1' Classes='foo'/>
+        <Border x:Name='b2' />
+    </StackPanel>
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var b1 = window.FindControl<Border>("b1");
+                var b2 = window.FindControl<Border>("b2");
+
+                Assert.Equal(Brushes.Red, b1.Background);
+                Assert.Null(b2.Background);
+            }
+        }
+
+        [Fact]
+        public void Style_Can_Use_NthChild_Selector_After_Reoder()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Styles>
+        <Style Selector='Border:nth-child(2n)'>
+            <Setter Property='Background' Value='Red'/>
+        </Style>
+    </Window.Styles>
+    <StackPanel x:Name='parent'>
+        <Border x:Name='b1' />
+        <Border x:Name='b2' />
+    </StackPanel>
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+
+                var parent = window.FindControl<StackPanel>("parent");
+                var b1 = window.FindControl<Border>("b1");
+                var b2 = window.FindControl<Border>("b2");
+
+                Assert.Null(b1.Background);
+                Assert.Equal(Brushes.Red, b2.Background);
+
+                parent.Children.Remove(b1);
+
+                Assert.Null(b1.Background);
+                Assert.Null(b2.Background);
+
+                parent.Children.Add(b1);
+
+                Assert.Equal(Brushes.Red, b1.Background);
+                Assert.Null(b2.Background);
+            }
+        }
+
+        [Fact]
+        public void Style_Can_Use_NthLastChild_Selector_After_Reoder()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Styles>
+        <Style Selector='Border:nth-last-child(2n)'>
+            <Setter Property='Background' Value='Red'/>
+        </Style>
+    </Window.Styles>
+    <StackPanel x:Name='parent'>
+        <Border x:Name='b1' />
+        <Border x:Name='b2' />
+    </StackPanel>
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+
+                var parent = window.FindControl<StackPanel>("parent");
+                var b1 = window.FindControl<Border>("b1");
+                var b2 = window.FindControl<Border>("b2");
+
+                Assert.Equal(Brushes.Red, b1.Background);
+                Assert.Null(b2.Background);
+
+                parent.Children.Remove(b1);
+
+                Assert.Null(b1.Background);
+                Assert.Null(b2.Background);
+
+                parent.Children.Add(b1);
+
+                Assert.Null(b1.Background);
+                Assert.Equal(Brushes.Red, b2.Background);
+            }
+        }
+
+
+        [Fact]
+        public void Style_Can_Use_NthChild_Selector_With_ListBox()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Styles>
+        <Style Selector='ListBoxItem:nth-child(2n)'>
+            <Setter Property='Background' Value='{Binding}'/>
+        </Style>
+    </Window.Styles>
+    <ListBox x:Name='list' />
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var collection = new ObservableCollection<IBrush>()
+                {
+                    Brushes.Red, Brushes.Green, Brushes.Blue
+                };
+
+                var list = window.FindControl<ListBox>("list");
+                list.VirtualizationMode = ItemVirtualizationMode.Simple;
+                list.Items = collection;
+
+                window.Show();
+
+                IEnumerable<IBrush> GetColors() => list.Presenter.Panel.Children.Cast<ListBoxItem>().Select(t => t.Background);
+
+                Assert.Equal(new[] { Brushes.Transparent, Brushes.Green, Brushes.Transparent }, GetColors());
+
+                collection.Remove(Brushes.Green);
+
+                Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors());
+
+                collection.Add(Brushes.Violet);
+                collection.Add(Brushes.Black);
+
+                Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue, Brushes.Transparent, Brushes.Black }, GetColors());
+            }
+        }
+
+        [Fact]
+        public void Style_Can_Use_NthChild_Selector_With_ItemsRepeater()
+        {
+            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            {
+                var xaml = @"
+<Window xmlns='https://github.com/avaloniaui'
+             xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'>
+    <Window.Styles>
+        <Style Selector='TextBlock'>
+            <Setter Property='Foreground' Value='Transparent'/>
+        </Style>
+        <Style Selector='TextBlock:nth-child(2n)'>
+            <Setter Property='Foreground' Value='{Binding}'/>
+        </Style>
+    </Window.Styles>
+    <ItemsRepeater x:Name='list' />
+</Window>";
+                var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml);
+                var collection = new ObservableCollection<IBrush>()
+                {
+                    Brushes.Red, Brushes.Green, Brushes.Blue
+                };
+
+                var list = window.FindControl<ItemsRepeater>("list");
+                list.Items = collection;
+
+                window.Show();
+
+                IEnumerable<IBrush> GetColors() => Enumerable.Range(0, list.ItemsSourceView.Count)
+                    .Select(t => (list.GetOrCreateElement(t) as TextBlock)!.Foreground);
+
+                Assert.Equal(new[] { Brushes.Transparent, Brushes.Green, Brushes.Transparent }, GetColors());
+
+                collection.Remove(Brushes.Green);
+
+                Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue }, GetColors());
+
+                collection.Add(Brushes.Violet);
+                collection.Add(Brushes.Black);
+
+                Assert.Equal(new[] { Brushes.Transparent, Brushes.Blue, Brushes.Transparent, Brushes.Black }, GetColors());
+            }
+        }
+
         [Fact]
         public void Style_Can_Use_Or_Selector_1()
         {

+ 14 - 8
tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs

@@ -6,18 +6,24 @@ namespace Avalonia.Skia.UnitTests.Media
 {
     public class SKTypefaceCollectionCacheTests
     {
-        [Fact]
-        public void Should_Get_Near_Matching_Typeface()
+        private const string s_notoMono =
+            "resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono";
+        
+        [InlineData(s_notoMono, FontWeight.SemiLight, FontStyle.Normal)]
+        [InlineData(s_notoMono, FontWeight.Bold, FontStyle.Italic)]
+        [InlineData(s_notoMono, FontWeight.Heavy, FontStyle.Oblique)]
+        [Theory]
+        public void Should_Get_Near_Matching_Typeface(string familyName, FontWeight fontWeight, FontStyle fontStyle)
         {
             using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
             {
-                var notoMono =
-                    new FontFamily("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono");
-
-                var notoMonoCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(notoMono);
+                var fontFamily = new FontFamily(familyName);
+                
+                var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(fontFamily);
 
-                Assert.Equal("Noto Mono",
-                    notoMonoCollection.Get(new Typeface(notoMono, weight: FontWeight.Bold)).FamilyName);
+                var actual = typefaceCollection.Get(new Typeface(fontFamily, fontStyle, fontWeight))?.FamilyName;
+                
+                Assert.Equal("Noto Mono", actual);
             }
         }
         

+ 291 - 0
tests/Avalonia.Styling.UnitTests/SelectorTests_NthChild.cs

@@ -0,0 +1,291 @@
+using System.Linq;
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Xunit;
+
+namespace Avalonia.Styling.UnitTests
+{
+    public class SelectorTests_NthChild
+    {
+        [Theory]
+        [InlineData(2, 0, ":nth-child(2n)")]
+        [InlineData(2, 1, ":nth-child(2n+1)")]
+        [InlineData(1, 0, ":nth-child(1n)")]
+        [InlineData(4, -1, ":nth-child(4n-1)")]
+        [InlineData(0, 1, ":nth-child(1)")]
+        [InlineData(0, -1, ":nth-child(-1)")]
+        [InlineData(int.MaxValue, int.MinValue + 1, ":nth-child(2147483647n-2147483647)")]
+        public void Not_Selector_Should_Have_Correct_String_Representation(int step, int offset, string expected)
+        {
+            var target = default(Selector).NthChild(step, offset);
+
+            Assert.Equal(expected, target.ToString());
+        }
+
+        [Fact]
+        public async Task Nth_Child_Match_Control_In_Panel()
+        {
+            Border b1, b2, b3, b4;
+            var panel = new StackPanel();
+            panel.Children.AddRange(new[]
+            {
+                b1 = new Border(),
+                b2 = new Border(),
+                b3 = new Border(),
+                b4 = new Border()
+            });
+
+            var target = default(Selector).NthChild(2, 0);
+
+            Assert.False(await target.Match(b1).Activator!.Take(1));
+            Assert.True(await target.Match(b2).Activator!.Take(1));
+            Assert.False(await target.Match(b3).Activator!.Take(1));
+            Assert.True(await target.Match(b4).Activator!.Take(1));
+        }
+
+        [Fact]
+        public async Task Nth_Child_Match_Control_In_Panel_With_Offset()
+        {
+            Border b1, b2, b3, b4;
+            var panel = new StackPanel();
+            panel.Children.AddRange(new[]
+            {
+                b1 = new Border(),
+                b2 = new Border(),
+                b3 = new Border(),
+                b4 = new Border()
+            });
+
+            var target = default(Selector).NthChild(2, 1);
+
+            Assert.True(await target.Match(b1).Activator!.Take(1));
+            Assert.False(await target.Match(b2).Activator!.Take(1));
+            Assert.True(await target.Match(b3).Activator!.Take(1));
+            Assert.False(await target.Match(b4).Activator!.Take(1));
+        }
+
+        [Fact]
+        public async Task Nth_Child_Match_Control_In_Panel_With_Negative_Offset()
+        {
+            Border b1, b2, b3, b4;
+            var panel = new StackPanel();
+            panel.Children.AddRange(new[]
+            {
+                b1 = new Border(),
+                b2 = new Border(),
+                b3 = new Border(),
+                b4 = new Border()
+            });
+
+            var target = default(Selector).NthChild(4, -1);
+
+            Assert.False(await target.Match(b1).Activator!.Take(1));
+            Assert.False(await target.Match(b2).Activator!.Take(1));
+            Assert.True(await target.Match(b3).Activator!.Take(1));
+            Assert.False(await target.Match(b4).Activator!.Take(1));
+        }
+
+        [Fact]
+        public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step()
+        {
+            Border b1, b2, b3, b4;
+            var panel = new StackPanel();
+            panel.Children.AddRange(new[]
+            {
+                b1 = new Border(),
+                b2 = new Border(),
+                b3 = new Border(),
+                b4 = new Border()
+            });
+
+            var target = default(Selector).NthChild(1, 2);
+
+            Assert.False(await target.Match(b1).Activator!.Take(1));
+            Assert.True(await target.Match(b2).Activator!.Take(1));
+            Assert.True(await target.Match(b3).Activator!.Take(1));
+            Assert.True(await target.Match(b4).Activator!.Take(1));
+        }
+
+        [Fact]
+        public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset()
+        {
+            Border b1, b2, b3, b4;
+            var panel = new StackPanel();
+            panel.Children.AddRange(new[]
+            {
+                b1 = new Border(),
+                b2 = new Border(),
+                b3 = new Border(),
+                b4 = new Border()
+            });
+
+            var target = default(Selector).NthChild(1, -1);
+
+            Assert.True(await target.Match(b1).Activator!.Take(1));
+            Assert.True(await target.Match(b2).Activator!.Take(1));
+            Assert.True(await target.Match(b3).Activator!.Take(1));
+            Assert.True(await target.Match(b4).Activator!.Take(1));
+        }
+
+        [Fact]
+        public async Task Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset()
+        {
+            Border b1, b2, b3, b4;
+            var panel = new StackPanel();
+            panel.Children.AddRange(new[]
+            {
+                b1 = new Border(),
+                b2 = new Border(),
+                b3 = new Border(),
+                b4 = new Border()
+            });
+
+            var target = default(Selector).NthChild(0, 2);
+
+            Assert.False(await target.Match(b1).Activator!.Take(1));
+            Assert.True(await target.Match(b2).Activator!.Take(1));
+            Assert.False(await target.Match(b3).Activator!.Take(1));
+            Assert.False(await target.Match(b4).Activator!.Take(1));
+        }
+
+        [Fact]
+        public async Task Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset()
+        {
+            Border b1, b2, b3, b4;
+            var panel = new StackPanel();
+            panel.Children.AddRange(new[]
+            {
+                b1 = new Border(),
+                b2 = new Border(),
+                b3 = new Border(),
+                b4 = new Border()
+            });
+
+            var target = default(Selector).NthChild(0, -2);
+
+            Assert.False(await target.Match(b1).Activator!.Take(1));
+            Assert.False(await target.Match(b2).Activator!.Take(1));
+            Assert.False(await target.Match(b3).Activator!.Take(1));
+            Assert.False(await target.Match(b4).Activator!.Take(1));
+        }
+
+        [Fact]
+        public async Task Nth_Child_Match_Control_In_Panel_With_Previous_Selector()
+        {
+            Border b1, b2;
+            Button b3, b4;
+            var panel = new StackPanel();
+            panel.Children.AddRange(new Control[]
+            {
+                b1 = new Border(),
+                b2 = new Border(),
+                b3 = new Button(),
+                b4 = new Button()
+            });
+
+            var previous = default(Selector).OfType<Border>();
+            var target = previous.NthChild(2, 0);
+
+            Assert.False(await target.Match(b1).Activator!.Take(1));
+            Assert.True(await target.Match(b2).Activator!.Take(1));
+            Assert.Null(target.Match(b3).Activator);
+            Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b3).Result);
+            Assert.Null(target.Match(b4).Activator);
+            Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b4).Result);
+        }
+
+        [Fact]
+        public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent()
+        {
+            Border b1;
+            var contentControl = new ContentControl();
+            contentControl.Content = b1 = new Border();
+
+            var target = default(Selector).NthChild(1, 0);
+
+            Assert.Equal(SelectorMatch.NeverThisInstance, target.Match(b1));
+        }
+
+
+        [Theory] // http://nthmaster.com/
+        [InlineData(+0, 8, false, false, false, false, false, false, false, true , false, false, false)]
+        [InlineData(+1, 6, false, false, false, false, false, true , true , true , true , true , true )]
+        [InlineData(-1, 9, true , true , true , true , true , true , true , true , true , false, false)]
+        public async Task Nth_Child_Master_Com_Test_Sigle_Selector(
+            int step, int offset, params bool[] items)
+        {
+            var panel = new StackPanel();
+            panel.Children.AddRange(items.Select(_ => new Border()));
+
+            var previous = default(Selector).OfType<Border>();
+            var target = previous.NthChild(step, offset);
+
+            var results = new bool[items.Length];
+            for (int index = 0; index < items.Length; index++)
+            {
+                var border = panel.Children[index];
+                results[index] = await target.Match(border).Activator!.Take(1);
+            }
+
+            Assert.Equal(items, results);
+        }
+
+        [Theory] // http://nthmaster.com/
+        [InlineData(+1, 4, -1, 8, false, false, false, true , true , true , true , true , false, false, false)]
+        [InlineData(+3, 1, +2, 0, false, false, false, true , false, false, false, false, false, true , false)]
+        public async Task Nth_Child_Master_Com_Test_Double_Selector(
+            int step1, int offset1, int step2, int offset2, params bool[] items)
+        {
+            var panel = new StackPanel();
+            panel.Children.AddRange(items.Select(_ => new Border()));
+
+            var previous = default(Selector).OfType<Border>();
+            var middle = previous.NthChild(step1, offset1);
+            var target = middle.NthChild(step2, offset2);
+
+            var results = new bool[items.Length];
+            for (int index = 0; index < items.Length; index++)
+            {
+                var border = panel.Children[index];
+                results[index] = await target.Match(border).Activator!.Take(1);
+            }
+
+            Assert.Equal(items, results);
+        }
+
+        [Theory] // http://nthmaster.com/
+        [InlineData(+1, 2, 2, 1, -1, 9, false, false, true , false, true , false, true , false, true , false, false)]
+        public async Task Nth_Child_Master_Com_Test_Triple_Selector(
+            int step1, int offset1, int step2, int offset2, int step3, int offset3, params bool[] items)
+        {
+            var panel = new StackPanel();
+            panel.Children.AddRange(items.Select(_ => new Border()));
+
+            var previous = default(Selector).OfType<Border>();
+            var middle1 = previous.NthChild(step1, offset1);
+            var middle2 = middle1.NthChild(step2, offset2);
+            var target = middle2.NthChild(step3, offset3);
+
+            var results = new bool[items.Length];
+            for (int index = 0; index < items.Length; index++)
+            {
+                var border = panel.Children[index];
+                results[index] = await target.Match(border).Activator!.Take(1);
+            }
+
+            Assert.Equal(items, results);
+        }
+
+        [Fact]
+        public void Returns_Correct_TargetType()
+        {
+            var target = new NthChildSelector(default(Selector).OfType<Control1>(), 1, 0);
+
+            Assert.Equal(typeof(Control1), target.TargetType);
+        }
+
+        public class Control1 : Control
+        {
+        }
+    }
+}

+ 220 - 0
tests/Avalonia.Styling.UnitTests/SelectorTests_NthLastChild.cs

@@ -0,0 +1,220 @@
+using System.Threading.Tasks;
+using Avalonia.Controls;
+using Xunit;
+
+namespace Avalonia.Styling.UnitTests
+{
+    public class SelectorTests_NthLastChild
+    {
+        [Theory]
+        [InlineData(2, 0, ":nth-last-child(2n)")]
+        [InlineData(2, 1, ":nth-last-child(2n+1)")]
+        [InlineData(1, 0, ":nth-last-child(1n)")]
+        [InlineData(4, -1, ":nth-last-child(4n-1)")]
+        [InlineData(0, 1, ":nth-last-child(1)")]
+        [InlineData(0, -1, ":nth-last-child(-1)")]
+        [InlineData(int.MaxValue, int.MinValue + 1, ":nth-last-child(2147483647n-2147483647)")]
+        public void Not_Selector_Should_Have_Correct_String_Representation(int step, int offset, string expected)
+        {
+            var target = default(Selector).NthLastChild(step, offset);
+
+            Assert.Equal(expected, target.ToString());
+        }
+
+        [Fact]
+        public async Task Nth_Child_Match_Control_In_Panel()
+        {
+            Border b1, b2, b3, b4;
+            var panel = new StackPanel();
+            panel.Children.AddRange(new[]
+            {
+                b1 = new Border(),
+                b2 = new Border(),
+                b3 = new Border(),
+                b4 = new Border()
+            });
+
+            var target = default(Selector).NthLastChild(2, 0);
+
+            Assert.True(await target.Match(b1).Activator!.Take(1));
+            Assert.False(await target.Match(b2).Activator!.Take(1));
+            Assert.True(await target.Match(b3).Activator!.Take(1));
+            Assert.False(await target.Match(b4).Activator!.Take(1));
+        }
+
+        [Fact]
+        public async Task Nth_Child_Match_Control_In_Panel_With_Offset()
+        {
+            Border b1, b2, b3, b4;
+            var panel = new StackPanel();
+            panel.Children.AddRange(new[]
+            {
+                b1 = new Border(),
+                b2 = new Border(),
+                b3 = new Border(),
+                b4 = new Border()
+            });
+
+            var target = default(Selector).NthLastChild(2, 1);
+
+            Assert.False(await target.Match(b1).Activator!.Take(1));
+            Assert.True(await target.Match(b2).Activator!.Take(1));
+            Assert.False(await target.Match(b3).Activator!.Take(1));
+            Assert.True(await target.Match(b4).Activator!.Take(1));
+        }
+
+        [Fact]
+        public async Task Nth_Child_Match_Control_In_Panel_With_Negative_Offset()
+        {
+            Border b1, b2, b3, b4;
+            var panel = new StackPanel();
+            panel.Children.AddRange(new[]
+            {
+                b1 = new Border(),
+                b2 = new Border(),
+                b3 = new Border(),
+                b4 = new Border()
+            });
+
+            var target = default(Selector).NthLastChild(4, -1);
+
+            Assert.False(await target.Match(b1).Activator!.Take(1));
+            Assert.True(await target.Match(b2).Activator!.Take(1));
+            Assert.False(await target.Match(b3).Activator!.Take(1));
+            Assert.False(await target.Match(b4).Activator!.Take(1));
+        }
+
+        [Fact]
+        public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step()
+        {
+            Border b1, b2, b3, b4;
+            var panel = new StackPanel();
+            panel.Children.AddRange(new[]
+            {
+                b1 = new Border(),
+                b2 = new Border(),
+                b3 = new Border(),
+                b4 = new Border()
+            });
+
+            var target = default(Selector).NthLastChild(1, 2);
+
+            Assert.True(await target.Match(b1).Activator!.Take(1));
+            Assert.True(await target.Match(b2).Activator!.Take(1));
+            Assert.True(await target.Match(b3).Activator!.Take(1));
+            Assert.False(await target.Match(b4).Activator!.Take(1));
+        }
+
+        [Fact]
+        public async Task Nth_Child_Match_Control_In_Panel_With_Singular_Step_With_Negative_Offset()
+        {
+            Border b1, b2, b3, b4;
+            var panel = new StackPanel();
+            panel.Children.AddRange(new[]
+            {
+                b1 = new Border(),
+                b2 = new Border(),
+                b3 = new Border(),
+                b4 = new Border()
+            });
+
+            var target = default(Selector).NthLastChild(1, -2);
+
+            Assert.True(await target.Match(b1).Activator!.Take(1));
+            Assert.True(await target.Match(b2).Activator!.Take(1));
+            Assert.True(await target.Match(b3).Activator!.Take(1));
+            Assert.True(await target.Match(b4).Activator!.Take(1));
+        }
+
+        [Fact]
+        public async Task Nth_Child_Match_Control_In_Panel_With_Zero_Step_With_Offset()
+        {
+            Border b1, b2, b3, b4;
+            var panel = new StackPanel();
+            panel.Children.AddRange(new[]
+            {
+                b1 = new Border(),
+                b2 = new Border(),
+                b3 = new Border(),
+                b4 = new Border()
+            });
+
+            var target = default(Selector).NthLastChild(0, 2);
+
+            Assert.False(await target.Match(b1).Activator!.Take(1));
+            Assert.False(await target.Match(b2).Activator!.Take(1));
+            Assert.True(await target.Match(b3).Activator!.Take(1));
+            Assert.False(await target.Match(b4).Activator!.Take(1));
+        }
+
+        [Fact]
+        public async Task Nth_Child_Doesnt_Match_Control_In_Panel_With_Zero_Step_With_Negative_Offset()
+        {
+            Border b1, b2, b3, b4;
+            var panel = new StackPanel();
+            panel.Children.AddRange(new[]
+            {
+                b1 = new Border(),
+                b2 = new Border(),
+                b3 = new Border(),
+                b4 = new Border()
+            });
+
+            var target = default(Selector).NthLastChild(0, -2);
+
+            Assert.False(await target.Match(b1).Activator!.Take(1));
+            Assert.False(await target.Match(b2).Activator!.Take(1));
+            Assert.False(await target.Match(b3).Activator!.Take(1));
+            Assert.False(await target.Match(b4).Activator!.Take(1));
+        }
+
+        [Fact]
+        public async Task Nth_Child_Match_Control_In_Panel_With_Previous_Selector()
+        {
+            Border b1, b2;
+            Button b3, b4;
+            var panel = new StackPanel();
+            panel.Children.AddRange(new Control[]
+            {
+                b1 = new Border(),
+                b2 = new Border(),
+                b3 = new Button(),
+                b4 = new Button()
+            });
+
+            var previous = default(Selector).OfType<Border>();
+            var target = previous.NthLastChild(2, 0);
+
+            Assert.True(await target.Match(b1).Activator!.Take(1));
+            Assert.False(await target.Match(b2).Activator!.Take(1));
+            Assert.Null(target.Match(b3).Activator);
+            Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b3).Result);
+            Assert.Null(target.Match(b4).Activator);
+            Assert.Equal(SelectorMatchResult.NeverThisType, target.Match(b4).Result);
+        }
+
+        [Fact]
+        public void Nth_Child_Doesnt_Match_Control_Out_Of_Panel_Parent()
+        {
+            Border b1;
+            var contentControl = new ContentControl();
+            contentControl.Content = b1 = new Border();
+
+            var target = default(Selector).NthLastChild(1, 0);
+
+            Assert.Equal(SelectorMatch.NeverThisInstance, target.Match(b1));
+        }
+
+        [Fact]
+        public void Returns_Correct_TargetType()
+        {
+            var target = new NthLastChildSelector(default(Selector).OfType<Control1>(), 1, 0);
+
+            Assert.Equal(typeof(Control1), target.TargetType);
+        }
+
+        public class Control1 : Control
+        {
+        }
+    }
+}

+ 5 - 2
tests/Avalonia.Styling.UnitTests/StyleActivatorExtensions.cs

@@ -20,13 +20,17 @@ namespace Avalonia.Styling.UnitTests
 
         public static IObservable<bool> ToObservable(this IStyleActivator activator)
         {
+            if (activator == null)
+            {
+                throw new ArgumentNullException(nameof(activator));
+            }
+
             return new ObservableAdapter(activator);
         }
 
         private class ObservableAdapter : LightweightObservableBase<bool>, IStyleActivatorSink
         {
             private readonly IStyleActivator _source;
-            private bool _value;
             
             public ObservableAdapter(IStyleActivator source) => _source = source;
             protected override void Initialize() => _source.Subscribe(this);
@@ -34,7 +38,6 @@ namespace Avalonia.Styling.UnitTests
 
             void IStyleActivatorSink.OnNext(bool value, int tag)
             {
-                _value = value;
                 PublishNext(value);
             }
         }