Browse Source

Merge branch 'master' into feature/ui-automation

Steven Kirk 4 years ago
parent
commit
0388a7b920
100 changed files with 2120 additions and 211 deletions
  1. 1 0
      Documentation/build.md
  2. 6 0
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  3. 1 0
      native/Avalonia.Native/src/OSX/common.h
  4. 11 0
      native/Avalonia.Native/src/OSX/main.mm
  5. 33 0
      native/Avalonia.Native/src/OSX/trayicon.h
  6. 85 0
      native/Avalonia.Native/src/OSX/trayicon.mm
  7. 1 0
      native/Avalonia.Native/src/OSX/window.h
  8. 47 3
      native/Avalonia.Native/src/OSX/window.mm
  9. 24 1
      samples/ControlCatalog/App.xaml
  10. 9 0
      samples/ControlCatalog/App.xaml.cs
  11. 1 0
      samples/ControlCatalog/MainView.xaml
  12. 1 0
      samples/ControlCatalog/MainWindow.xaml
  13. 2 0
      samples/ControlCatalog/MainWindow.xaml.cs
  14. 9 1
      samples/ControlCatalog/Pages/TextBoxPage.xaml
  15. 1 0
      samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml
  16. 26 0
      samples/ControlCatalog/ViewModels/ApplicationViewModel.cs
  17. 5 0
      src/Avalonia.Base/Logging/LogArea.cs
  18. 1 0
      src/Avalonia.Base/Metadata/TemplateContent.cs
  19. 12 6
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  20. 1 1
      src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
  21. 29 0
      src/Avalonia.Controls.DataGrid/DataGridColumn.cs
  22. 20 6
      src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs
  23. 3 1
      src/Avalonia.Controls/ApiCompatBaseline.txt
  24. 2 0
      src/Avalonia.Controls/Border.cs
  25. 7 0
      src/Avalonia.Controls/Button.cs
  26. 61 37
      src/Avalonia.Controls/ItemsSourceView.cs
  27. 6 7
      src/Avalonia.Controls/NativeMenu.Export.cs
  28. 2 0
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  29. 1 1
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  30. 14 3
      src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs
  31. 35 0
      src/Avalonia.Controls/Platform/ITrayIconImpl.cs
  32. 5 0
      src/Avalonia.Controls/Platform/IWindowingPlatform.cs
  33. 15 1
      src/Avalonia.Controls/Platform/PlatformManager.cs
  34. 11 8
      src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs
  35. 3 1
      src/Avalonia.Controls/Primitives/Popup.cs
  36. 3 1
      src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs
  37. 3 0
      src/Avalonia.Controls/ProgressBar.cs
  38. 2 0
      src/Avalonia.Controls/Remote/RemoteWidget.cs
  39. 3 3
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  40. 4 5
      src/Avalonia.Controls/Templates/IControlTemplate.cs
  41. 20 0
      src/Avalonia.Controls/Templates/TemplateResult.cs
  42. 2 0
      src/Avalonia.Controls/TextBox.cs
  43. 187 0
      src/Avalonia.Controls/TrayIcon.cs
  44. 6 1
      src/Avalonia.Controls/WindowTransparencyLevel.cs
  45. 3 1
      src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs
  46. 9 3
      src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs
  47. 5 3
      src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs
  48. 2 2
      src/Avalonia.Dialogs/ManagedFileChooser.cs
  49. 1 1
      src/Avalonia.Dialogs/ManagedFileChooserSources.cs
  50. 5 2
      src/Avalonia.FreeDesktop/DBusHelper.cs
  51. 55 20
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  52. 2 0
      src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs
  53. 6 2
      src/Avalonia.Input/Gestures.cs
  54. 2 0
      src/Avalonia.Input/MouseDevice.cs
  55. 50 13
      src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
  56. 5 0
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  57. 63 0
      src/Avalonia.Native/TrayIconImpl.cs
  58. 9 0
      src/Avalonia.Native/avn.idl
  59. 8 2
      src/Avalonia.Themes.Default/OverlayPopupHost.xaml
  60. 8 2
      src/Avalonia.Themes.Default/PopupRoot.xaml
  61. 5 1
      src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml
  62. 5 1
      src/Avalonia.Themes.Fluent/Controls/PopupRoot.xaml
  63. 8 0
      src/Avalonia.Visuals/Media/DrawingImage.cs
  64. 1 1
      src/Avalonia.Visuals/Media/FormattedText.cs
  65. 2 2
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  66. 5 2
      src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs
  67. 16 4
      src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs
  68. 6 1
      src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs
  69. 6 6
      src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs
  70. 12 2
      src/Avalonia.Visuals/Rendering/SceneGraph/GeometryClipNode.cs
  71. 42 3
      src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs
  72. 3 1
      src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs
  73. 1 1
      src/Avalonia.Visuals/Vector.cs
  74. 3 3
      src/Avalonia.X11/X11CursorFactory.cs
  75. 2 0
      src/Avalonia.X11/X11IconLoader.cs
  76. 1 1
      src/Avalonia.X11/X11Info.cs
  77. 11 2
      src/Avalonia.X11/X11Platform.cs
  78. 367 0
      src/Avalonia.X11/X11TrayIconImpl.cs
  79. 1 1
      src/Avalonia.X11/X11Window.cs
  80. 0 3
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs
  81. 6 1
      src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs
  82. 1 1
      src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github
  83. 12 0
      src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs
  84. 11 1
      src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs
  85. 5 1
      src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs
  86. 1 1
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  87. 5 1
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  88. 9 1
      src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs
  89. 2 0
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  90. 63 0
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  91. 265 0
      src/Windows/Avalonia.Win32/TrayIconImpl.cs
  92. 1 1
      src/Windows/Avalonia.Win32/Win32GlManager.cs
  93. 54 0
      src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs
  94. 18 0
      src/Windows/Avalonia.Win32/Win32Platform.cs
  95. 12 3
      src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs
  96. 62 14
      src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs
  97. 5 5
      src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs
  98. 9 2
      src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs
  99. 119 4
      src/Windows/Avalonia.Win32/WinRT/winrt.idl
  100. 9 2
      src/Windows/Avalonia.Win32/WindowImpl.cs

+ 1 - 0
Documentation/build.md

@@ -6,6 +6,7 @@ Avalonia requires at least Visual Studio 2019 and .NET Core SDK 3.1 to build on
 
 ```
 git clone https://github.com/AvaloniaUI/Avalonia.git
+cd Avalonia
 git submodule update --init
 ```
 

+ 6 - 0
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj

@@ -22,6 +22,7 @@
 		37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37E2330E21583241000CB7E2 /* KeyTransform.mm */; };
 		520624B322973F4100C4DCEF /* menu.mm in Sources */ = {isa = PBXBuildFile; fileRef = 520624B222973F4100C4DCEF /* menu.mm */; };
 		522D5959258159C1006F7F7A /* Carbon.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 522D5958258159C1006F7F7A /* Carbon.framework */; };
+		523484CA26EA688F00EA0C2C /* trayicon.mm in Sources */ = {isa = PBXBuildFile; fileRef = 523484C926EA688F00EA0C2C /* trayicon.mm */; };
 		5B21A982216530F500CEE36E /* cursor.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B21A981216530F500CEE36E /* cursor.mm */; };
 		5B8BD94F215BFEA6005ED2A7 /* clipboard.mm in Sources */ = {isa = PBXBuildFile; fileRef = 5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */; };
 		AB00E4F72147CA920032A60A /* main.mm in Sources */ = {isa = PBXBuildFile; fileRef = AB00E4F62147CA920032A60A /* main.mm */; };
@@ -53,6 +54,8 @@
 		37E2330E21583241000CB7E2 /* KeyTransform.mm */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.cpp.objcpp; path = KeyTransform.mm; sourceTree = "<group>"; };
 		520624B222973F4100C4DCEF /* menu.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = menu.mm; sourceTree = "<group>"; };
 		522D5958258159C1006F7F7A /* Carbon.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = Carbon.framework; path = System/Library/Frameworks/Carbon.framework; sourceTree = SDKROOT; };
+		523484C926EA688F00EA0C2C /* trayicon.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = trayicon.mm; sourceTree = "<group>"; };
+		523484CB26EA68AA00EA0C2C /* trayicon.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = trayicon.h; sourceTree = "<group>"; };
 		5B21A981216530F500CEE36E /* cursor.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cursor.mm; sourceTree = "<group>"; };
 		5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = clipboard.mm; sourceTree = "<group>"; };
 		5BF943652167AD1D009CAE35 /* cursor.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = cursor.h; sourceTree = "<group>"; };
@@ -120,6 +123,8 @@
 				AB00E4F62147CA920032A60A /* main.mm */,
 				37155CE3233C00EB0034DCE9 /* menu.h */,
 				520624B222973F4100C4DCEF /* menu.mm */,
+				523484C926EA688F00EA0C2C /* trayicon.mm */,
+				523484CB26EA68AA00EA0C2C /* trayicon.h */,
 				1A3E5EA723E9E83B00EDE661 /* rendertarget.mm */,
 				37A517B22159597E00FBA241 /* Screens.mm */,
 				37C09D8721580FE4006A6758 /* SystemDialogs.mm */,
@@ -211,6 +216,7 @@
 				1A1852DC23E05814008F0DED /* deadlock.mm in Sources */,
 				5B21A982216530F500CEE36E /* cursor.mm in Sources */,
 				37DDA9B0219330F8002E132B /* AvnString.mm in Sources */,
+				523484CA26EA688F00EA0C2C /* trayicon.mm in Sources */,
 				AB8F7D6B21482D7F0057DBA5 /* platformthreading.mm in Sources */,
 				1A3E5EA823E9E83B00EDE661 /* rendertarget.mm in Sources */,
 				1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */,

+ 1 - 0
native/Avalonia.Native/src/OSX/common.h

@@ -22,6 +22,7 @@ extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop);
 extern IAvnCursorFactory* CreateCursorFactory();
 extern IAvnGlDisplay* GetGlDisplay();
 extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events);
+extern IAvnTrayIcon* CreateTrayIcon();
 extern IAvnMenuItem* CreateAppMenuItem();
 extern IAvnMenuItem* CreateAppMenuItemSeparator();
 extern IAvnNativeControlHost* CreateNativeControlHost(NSView* parent);

+ 11 - 0
native/Avalonia.Native/src/OSX/main.mm

@@ -304,6 +304,17 @@ public:
         }
     }
     
+    virtual HRESULT CreateTrayIcon (IAvnTrayIcon** ppv) override
+    {
+        START_COM_CALL;
+        
+        @autoreleasepool
+        {
+            *ppv = ::CreateTrayIcon();
+            return S_OK;
+        }
+    }
+    
     virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) override
     {
         START_COM_CALL;

+ 33 - 0
native/Avalonia.Native/src/OSX/trayicon.h

@@ -0,0 +1,33 @@
+//
+//  trayicon.h
+//  Avalonia.Native.OSX
+//
+//  Created by Dan Walmsley on 09/09/2021.
+//  Copyright © 2021 Avalonia. All rights reserved.
+//
+
+#ifndef trayicon_h
+#define trayicon_h
+
+#include "common.h"
+
+class AvnTrayIcon : public ComSingleObject<IAvnTrayIcon, &IID_IAvnTrayIcon>
+{
+private:
+    NSStatusItem* _native;
+    
+public:
+    FORWARD_IUNKNOWN()
+    
+    AvnTrayIcon();
+    
+    ~AvnTrayIcon ();
+    
+    virtual HRESULT SetIcon (void* data, size_t length) override;
+    
+    virtual HRESULT SetMenu (IAvnMenu* menu) override;
+    
+    virtual HRESULT SetIsVisible (bool isVisible) override;
+};
+
+#endif /* trayicon_h */

+ 85 - 0
native/Avalonia.Native/src/OSX/trayicon.mm

@@ -0,0 +1,85 @@
+#include "common.h"
+#include "trayicon.h"
+#include "menu.h"
+
+extern IAvnTrayIcon* CreateTrayIcon()
+{
+    @autoreleasepool
+    {
+        return new AvnTrayIcon();
+    }
+}
+
+AvnTrayIcon::AvnTrayIcon()
+{
+    _native = [[NSStatusBar systemStatusBar] statusItemWithLength: NSSquareStatusItemLength];
+    
+}
+
+AvnTrayIcon::~AvnTrayIcon()
+{
+    if(_native != nullptr)
+    {
+        [[_native statusBar] removeStatusItem:_native];
+        _native = nullptr;
+    }
+}
+
+HRESULT AvnTrayIcon::SetIcon (void* data, size_t length)
+{
+    START_COM_CALL;
+    
+    @autoreleasepool
+    {
+        if(data != nullptr)
+        {
+            NSData *imageData = [NSData dataWithBytes:data length:length];
+            NSImage *image = [[NSImage alloc] initWithData:imageData];
+            
+            NSSize originalSize = [image size];
+             
+            NSSize size;
+            size.height = [[NSFont menuFontOfSize:0] pointSize] * 1.333333;
+            
+            auto scaleFactor = size.height / originalSize.height;
+            size.width = originalSize.width * scaleFactor;
+            
+            [image setSize: size];
+            [_native setImage:image];
+        }
+        else
+        {
+            [_native setImage:nullptr];
+        }
+        return S_OK;
+    }
+}
+
+HRESULT AvnTrayIcon::SetMenu (IAvnMenu* menu)
+{
+    START_COM_CALL;
+    
+    @autoreleasepool
+    {
+        auto appMenu = dynamic_cast<AvnAppMenu*>(menu);
+        
+        if(appMenu != nullptr)
+        {
+            [_native setMenu:appMenu->GetNative()];	
+        }
+    }
+    
+    return  S_OK;
+}
+
+HRESULT AvnTrayIcon::SetIsVisible(bool isVisible)
+{
+    START_COM_CALL;
+    
+    @autoreleasepool
+    {
+        [_native setVisible:isVisible];
+    }
+    
+    return  S_OK;
+}

+ 1 - 0
native/Avalonia.Native/src/OSX/window.h

@@ -12,6 +12,7 @@ class WindowBaseImpl;
 -(AvnPixelSize) getPixelSize;
 -(AvnPlatformResizeReason) getResizeReason;
 -(void) setResizeReason:(AvnPlatformResizeReason)reason;
++ (AvnPoint)toAvnPoint:(CGPoint)p;
 @end
 
 @interface AutoFitContentView : NSView

+ 47 - 3
native/Avalonia.Native/src/OSX/window.mm

@@ -252,6 +252,8 @@ public:
     
     virtual HRESULT GetFrameSize(AvnSize* ret) override
     {
+        START_COM_CALL;
+        
         @autoreleasepool
         {
             if(ret == nullptr)
@@ -1581,7 +1583,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     return pt;
 }
 
-- (AvnPoint)toAvnPoint:(CGPoint)p
++ (AvnPoint)toAvnPoint:(CGPoint)p
 {
     AvnPoint result;
     
@@ -1638,7 +1640,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     }
     
     auto localPoint = [self convertPoint:[event locationInWindow] toView:self];
-    auto avnPoint = [self toAvnPoint:localPoint];
+    auto avnPoint = [AvnView toAvnPoint:localPoint];
     auto point = [self translateLocalPoint:avnPoint];
     AvnVector delta;
     
@@ -1983,7 +1985,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
 - (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id <NSDraggingInfo>)info
 {
     auto localPoint = [self convertPoint:[info draggingLocation] toView:self];
-    auto avnPoint = [self toAvnPoint:localPoint];
+    auto avnPoint = [AvnView toAvnPoint:localPoint];
     auto point = [self translateLocalPoint:avnPoint];
     auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]];
     NSDragOperation nsop = [info draggingSourceOperationMask];
@@ -2419,6 +2421,48 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     }
 }
 
+- (AvnPoint) translateLocalPoint:(AvnPoint)pt
+{
+    pt.Y = [self frame].size.height - pt.Y;
+    return pt;
+}
+
+- (void)sendEvent:(NSEvent *)event
+{
+    if(_parent != nullptr)
+    {
+        switch(event.type)
+        {
+            case NSEventTypeLeftMouseDown:
+            {
+                auto avnPoint = [AvnView toAvnPoint:[event locationInWindow]];
+                auto point = [self translateLocalPoint:avnPoint];
+                AvnVector delta;
+                
+                _parent->BaseEvents->RawMouseEvent(NonClientLeftButtonDown, [event timestamp] * 1000, AvnInputModifiersNone, point, delta);
+            }
+            break;
+                
+            case NSEventTypeMouseEntered:
+            {
+                _parent->UpdateCursor();
+            }
+            break;
+                
+            case NSEventTypeMouseExited:
+            {
+                [[NSCursor arrowCursor] set];
+            }
+            break;
+                
+            default:
+                break;
+        }
+    }
+    
+    [super sendEvent:event];
+}
+
 - (BOOL)isAccessibilityElement
 {
     [self getAutomationPeer];

+ 24 - 1
samples/ControlCatalog/App.xaml

@@ -1,5 +1,8 @@
 <Application xmlns="https://github.com/avaloniaui"
              xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+             xmlns:vm="using:ControlCatalog.ViewModels"
+             x:DataType="vm:ApplicationViewModel"
+             x:CompileBindings="True"
              x:Class="ControlCatalog.App">
   <Application.Styles>
     <Style Selector="TextBlock.h1">
@@ -22,6 +25,26 @@
     <Style Selector="Label.h3">
       <Setter Property="FontSize" Value="12" />
     </Style>
-    <StyleInclude Source="/SideBar.xaml"/>
+    <StyleInclude Source="/SideBar.xaml" />
   </Application.Styles>
+  <TrayIcon.Icons>
+    <TrayIcons>
+      <TrayIcon Icon="/Assets/test_icon.ico" ToolTipText="Avalonia Tray Icon ToolTip">
+        <TrayIcon.Menu>
+          <NativeMenu>
+            <NativeMenuItem Header="Settings">
+              <NativeMenu>
+                <NativeMenuItem Header="Option 1" ToggleType="Radio" IsChecked="True" Command="{Binding ToggleCommand}" />
+                <NativeMenuItem Header="Option 2" ToggleType="Radio" IsChecked="True" Command="{Binding ToggleCommand}" />
+                <NativeMenuItemSeparator />
+                <NativeMenuItem Header="Option 3" ToggleType="CheckBox" IsChecked="True" Command="{Binding ToggleCommand}" />
+                <NativeMenuItem Icon="/Assets/test_icon.ico" Header="Restore Defaults" Command="{Binding ToggleCommand}" />
+              </NativeMenu>
+            </NativeMenuItem>
+            <NativeMenuItem Header="Exit" Command="{Binding ExitCommand}" />
+          </NativeMenu>
+        </TrayIcon.Menu>
+      </TrayIcon>
+    </TrayIcons>
+  </TrayIcon.Icons>
 </Application>

+ 9 - 0
samples/ControlCatalog/App.xaml.cs

@@ -1,14 +1,21 @@
 using System;
 using Avalonia;
+using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Markup.Xaml;
 using Avalonia.Markup.Xaml.Styling;
 using Avalonia.Styling;
+using ControlCatalog.ViewModels;
 
 namespace ControlCatalog
 {
     public class App : Application
     {
+        public App()
+        {
+            DataContext = new ApplicationViewModel();
+        }
+
         private static readonly StyleInclude DataGridFluent = new StyleInclude(new Uri("avares://ControlCatalog/Styles"))
         {
             Source = new Uri("avares://Avalonia.Controls.DataGrid/Themes/Fluent.xaml")
@@ -97,7 +104,9 @@ namespace ControlCatalog
         public override void OnFrameworkInitializationCompleted()
         {
             if (ApplicationLifetime is IClassicDesktopStyleApplicationLifetime desktopLifetime)
+            {
                 desktopLifetime.MainWindow = new MainWindow();
+            }
             else if (ApplicationLifetime is ISingleViewApplicationLifetime singleViewLifetime)
                 singleViewLifetime.MainView = new MainView();
 

+ 1 - 0
samples/ControlCatalog/MainView.xaml

@@ -98,6 +98,7 @@
             <ComboBoxItem>Transparent</ComboBoxItem>
             <ComboBoxItem>Blur</ComboBoxItem>
             <ComboBoxItem>AcrylicBlur</ComboBoxItem>
+            <ComboBoxItem>Mica</ComboBoxItem>
           </ComboBox>
           <ComboBox Items="{Binding WindowStates}" SelectedItem="{Binding WindowState}" />
         </StackPanel>

+ 1 - 0
samples/ControlCatalog/MainWindow.xaml

@@ -12,6 +12,7 @@
         ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}"
         TransparencyLevelHint="{Binding TransparencyLevel}"        
         x:Name="MainWindow"
+        Background="Transparent"
         x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}">
   <NativeMenu.Menu>
     <NativeMenu>

+ 2 - 0
samples/ControlCatalog/MainWindow.xaml.cs

@@ -35,6 +35,8 @@ namespace ControlCatalog
 
             var mainMenu = this.FindControl<Menu>("MainMenu");
             mainMenu.AttachedToVisualTree += MenuAttached;
+
+            ExtendClientAreaChromeHints = Avalonia.Platform.ExtendClientAreaChromeHints.OSXThickTitleBar;
         }
 
         public static string MenuQuitHeader => RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? "Quit Avalonia" : "E_xit";

+ 9 - 1
samples/ControlCatalog/Pages/TextBoxPage.xaml

@@ -11,7 +11,15 @@
               HorizontalAlignment="Center"
               Spacing="16">
       <StackPanel Orientation="Vertical" Spacing="8">
-        <TextBox Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit." Width="200" />
+        <TextBox Text="Lorem ipsum dolor sit amet, consectetur adipiscing elit." Width="200"
+                 FontFamily="Comic Sans MS"
+                 Foreground="Blue">
+          <TextBox.ContextFlyout>
+            <Flyout>
+              <TextBlock>Custom context flyout</TextBlock>
+            </Flyout>
+          </TextBox.ContextFlyout>        
+        </TextBox>
         <TextBox Width="200" Watermark="ReadOnly" IsReadOnly="True" Text="This is read only"/>
         <TextBox Width="200" Watermark="Numeric Watermark" x:Name="numericWatermark"/>
         <TextBox Width="200"

+ 1 - 0
samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml

@@ -14,6 +14,7 @@
       <ComboBoxItem>Transparent</ComboBoxItem>
       <ComboBoxItem>Blur</ComboBoxItem>
       <ComboBoxItem>AcrylicBlur</ComboBoxItem>
+      <ComboBoxItem>Mica</ComboBoxItem>
     </ComboBox>
   </StackPanel>
 </UserControl>

+ 26 - 0
samples/ControlCatalog/ViewModels/ApplicationViewModel.cs

@@ -0,0 +1,26 @@
+using Avalonia;
+using Avalonia.Controls.ApplicationLifetimes;
+using MiniMvvm;
+
+namespace ControlCatalog.ViewModels
+{
+    public class ApplicationViewModel : ViewModelBase
+    {
+        public ApplicationViewModel()
+        {
+            ExitCommand = MiniCommand.Create(() =>
+            {
+                if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
+                {
+                    lifetime.Shutdown();
+                }
+            });
+
+            ToggleCommand = MiniCommand.Create(() => { });
+        }
+
+        public MiniCommand ExitCommand { get; }
+
+        public MiniCommand ToggleCommand { get; }
+    }
+}

+ 5 - 0
src/Avalonia.Base/Logging/LogArea.cs

@@ -39,5 +39,10 @@ namespace Avalonia.Logging
         /// The log event comes from Win32Platform.
         /// </summary>
         public const string Win32Platform = nameof(Win32Platform);
+        
+        /// <summary>
+        /// The log event comes from X11Platform.
+        /// </summary>
+        public const string X11Platform = nameof(X11Platform);
     }
 }

+ 1 - 0
src/Avalonia.Base/Metadata/TemplateContent.cs

@@ -8,5 +8,6 @@ namespace Avalonia.Metadata
     [AttributeUsage(AttributeTargets.Property)]
     public class TemplateContentAttribute : Attribute
     {
+        public Type TemplateResultType { get; set; }
     }
 }

+ 12 - 6
src/Avalonia.Controls.DataGrid/DataGrid.cs

@@ -2223,6 +2223,7 @@ namespace Avalonia.Controls
             if (IsEnabled && DisplayData.NumDisplayedScrollingElements > 0)
             {
                 var handled = false;
+                var ignoreInvalidate = false;
                 var scrollHeight = 0d;
 
                 // Vertical scroll handling
@@ -2252,8 +2253,7 @@ namespace Avalonia.Controls
                 // Horizontal scroll handling
                 if (delta.X != 0)
                 {
-                    var originalHorizontalOffset = HorizontalOffset;
-                    var horizontalOffset = originalHorizontalOffset - delta.X;
+                    var horizontalOffset = HorizontalOffset - delta.X;
                     var widthNotVisible = Math.Max(0, ColumnsInternal.VisibleEdgedColumnsWidth - CellsWidth);
 
                     if (horizontalOffset < 0)
@@ -2265,16 +2265,20 @@ namespace Avalonia.Controls
                         horizontalOffset = widthNotVisible;
                     }
 
-                    if (horizontalOffset != originalHorizontalOffset)
+                    if (UpdateHorizontalOffset(horizontalOffset))
                     {
-                        HorizontalOffset = horizontalOffset;
+                        // We don't need to invalidate once again after UpdateHorizontalOffset.
+                        ignoreInvalidate = true;
                         handled = true;
                     }
                 }
 
                 if (handled)
                 {
-                    InvalidateRowsMeasure(invalidateIndividualElements: false);
+                    if (!ignoreInvalidate)
+                    {
+                        InvalidateRowsMeasure(invalidateIndividualElements: false);
+                    }
                     return true;
                 }
             }
@@ -2932,7 +2936,7 @@ namespace Avalonia.Controls
             return SetCurrentCellCore(columnIndex, slot, commitEdit: true, endRowEdit: true);
         }
 
-        internal void UpdateHorizontalOffset(double newValue)
+        internal bool UpdateHorizontalOffset(double newValue)
         {
             if (HorizontalOffset != newValue)
             {
@@ -2940,7 +2944,9 @@ namespace Avalonia.Controls
 
                 InvalidateColumnHeadersMeasure();
                 InvalidateRowsMeasure(true);
+                return true;
             }
+            return false;
         }
 
         internal bool UpdateSelectionAndCurrency(int columnIndex, int slot, DataGridSelectionAction action, bool scrollIntoView)

+ 1 - 1
src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs

@@ -133,7 +133,7 @@ namespace Avalonia.Controls
 
         protected abstract IControl GenerateEditingElementDirect(DataGridCell cell, object dataItem); 
 
-        internal AvaloniaProperty BindingTarget { get; set; } 
+        protected AvaloniaProperty BindingTarget { get; set; } 
 
         internal void SetHeaderFromBinding()
         {

+ 29 - 0
src/Avalonia.Controls.DataGrid/DataGridColumn.cs

@@ -9,6 +9,7 @@ using Avalonia.VisualTree;
 using Avalonia.Collections;
 using Avalonia.Utilities;
 using System;
+using System.ComponentModel;
 using System.Linq;
 using System.Diagnostics;
 using Avalonia.Controls.Utils;
@@ -653,6 +654,34 @@ namespace Avalonia.Controls
             return null;
         }
 
+        /// <summary>
+        /// Clears the current sort direction
+        /// </summary>
+        public void ClearSort()
+        {
+            //InvokeProcessSort is already validating if sorting is possible
+            _headerCell?.InvokeProcessSort(Input.KeyModifiers.Control);
+        }
+
+        /// <summary>
+        /// Switches the current state of sort direction
+        /// </summary>
+        public void Sort()
+        {
+            //InvokeProcessSort is already validating if sorting is possible
+            _headerCell?.InvokeProcessSort(Input.KeyModifiers.None);
+        }
+
+        /// <summary>
+        /// Changes the sort direction of this column
+        /// </summary>
+        /// <param name="direction">New sort direction</param>
+        public void Sort(ListSortDirection direction)
+        {
+            //InvokeProcessSort is already validating if sorting is possible
+            _headerCell?.InvokeProcessSort(Input.KeyModifiers.None, direction);
+        }
+
         /// <summary>
         /// When overridden in a derived class, causes the column cell being edited to revert to the unedited value.
         /// </summary>

+ 20 - 6
src/Avalonia.Controls.DataGrid/DataGridColumnHeader.cs

@@ -201,21 +201,21 @@ namespace Avalonia.Controls
             handled = true;
         }
 
-        internal void InvokeProcessSort(KeyModifiers keyModifiers)
+        internal void InvokeProcessSort(KeyModifiers keyModifiers, ListSortDirection? forcedDirection = null)
         {
             Debug.Assert(OwningGrid != null);
-            if (OwningGrid.WaitForLostFocus(() => InvokeProcessSort(keyModifiers)))
+            if (OwningGrid.WaitForLostFocus(() => InvokeProcessSort(keyModifiers, forcedDirection)))
             {
                 return;
             }
             if (OwningGrid.CommitEdit(DataGridEditingUnit.Row, exitEditingMode: true))
             {
-                Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(keyModifiers));
+                Avalonia.Threading.Dispatcher.UIThread.Post(() => ProcessSort(keyModifiers, forcedDirection));
             }
         }
 
         //TODO GroupSorting
-        internal void ProcessSort(KeyModifiers keyModifiers)
+        internal void ProcessSort(KeyModifiers keyModifiers, ListSortDirection? forcedDirection = null)
         {
             // if we can sort:
             //  - AllowUserToSortColumns and CanSort are true, and
@@ -259,7 +259,14 @@ namespace Avalonia.Controls
                         {
                             if (sort != null)
                             {
-                                newSort = sort.SwitchSortDirection();
+                                if (forcedDirection == null || sort.Direction != forcedDirection)
+                                {
+                                    newSort = sort.SwitchSortDirection();
+                                }
+                                else
+                                {
+                                    newSort = sort;
+                                } 
 
                                 // changing direction should not affect sort order, so we replace this column's
                                 // sort description instead of just adding it to the end of the collection
@@ -276,7 +283,10 @@ namespace Avalonia.Controls
                             }
                             else if (OwningColumn.CustomSortComparer != null)
                             {
-                                newSort = DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer);
+                                newSort = forcedDirection != null ?
+                                    DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer, forcedDirection.Value) :
+                                    DataGridSortDescription.FromComparer(OwningColumn.CustomSortComparer);
+
 
                                 owningGrid.DataConnection.SortDescriptions.Add(newSort);
                             }
@@ -290,6 +300,10 @@ namespace Avalonia.Controls
                                 }
 
                                 newSort = DataGridSortDescription.FromPath(propertyName, culture: collectionView.Culture);
+                                if (forcedDirection != null && newSort.Direction != forcedDirection)
+                                {
+                                    newSort = newSort.SwitchSortDirection();
+                                }
 
                                 owningGrid.DataConnection.SortDescriptions.Add(newSort);
                             }

+ 3 - 1
src/Avalonia.Controls/ApiCompatBaseline.txt

@@ -37,6 +37,7 @@ MembersMustExist : Member 'public System.Action<Avalonia.Size> Avalonia.Controls
 MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.Resized.set(System.Action<Avalonia.Size>)' does not exist in the implementation but it does exist in the contract.
 MembersMustExist : Member 'public void Avalonia.Controls.Embedding.Offscreen.OffscreenTopLevelImplBase.SetCursor(Avalonia.Platform.IPlatformHandle)' does not exist in the implementation but it does exist in the contract.
 MembersMustExist : Member 'public Avalonia.AvaloniaProperty Avalonia.AvaloniaProperty Avalonia.Controls.Notifications.NotificationCard.CloseOnClickProperty' does not exist in the implementation but it does exist in the contract.
+InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Controls.Platform.ITopLevelNativeMenuExporter.SetNativeMenu(Avalonia.Controls.NativeMenu)' is present in the contract but not in the implementation.
 EnumValuesMustMatch : Enum value 'Avalonia.Platform.ExtendClientAreaChromeHints Avalonia.Platform.ExtendClientAreaChromeHints.Default' is (System.Int32)2 in the implementation but (System.Int32)1 in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize' is present in the implementation but not in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public System.Nullable<Avalonia.Size> Avalonia.Platform.ITopLevelImpl.FrameSize.get()' is present in the implementation but not in the contract.
@@ -58,4 +59,5 @@ InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platfor
 InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' is present in the contract but not in the implementation.
 MembersMustExist : Member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size)' does not exist in the implementation but it does exist in the contract.
 InterfacesShouldHaveSameMembers : Interface member 'public void Avalonia.Platform.IWindowImpl.Resize(Avalonia.Size, Avalonia.Platform.PlatformResizeReason)' is present in the implementation but not in the contract.
-Total Issues: 59
+InterfacesShouldHaveSameMembers : Interface member 'public Avalonia.Platform.ITrayIconImpl Avalonia.Platform.IWindowingPlatform.CreateTrayIcon()' is present in the implementation but not in the contract.
+Total Issues: 61

+ 2 - 0
src/Avalonia.Controls/Border.cs

@@ -8,7 +8,9 @@ namespace Avalonia.Controls
     /// <summary>
     /// A control which decorates a child with a border and background.
     /// </summary>
+#pragma warning disable CS0618 // Type or member is obsolete
     public partial class Border : Decorator, IVisualWithRoundRectClip
+#pragma warning restore CS0618 // Type or member is obsolete
     {
         /// <summary>
         /// Defines the <see cref="Background"/> property.

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

@@ -359,6 +359,13 @@ namespace Avalonia.Controls
             IsPressed = false;
         }
 
+        protected override void OnLostFocus(RoutedEventArgs e)
+        {
+            base.OnLostFocus(e);
+
+            IsPressed = false;
+        }
+
         protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
         {
             base.OnPropertyChanged(change);

+ 61 - 37
src/Avalonia.Controls/ItemsSourceView.cs

@@ -32,8 +32,8 @@ namespace Avalonia.Controls
         /// </summary>
         public static ItemsSourceView Empty { get; } = new ItemsSourceView(Array.Empty<object>());
 
-        private protected readonly IList _inner;
-        private INotifyCollectionChanged? _notifyCollectionChanged;
+        private IList? _inner;
+        private NotifyCollectionChangedEventHandler? _collectionChanged;
 
         /// <summary>
         /// Initializes a new instance of the ItemsSourceView class for the specified data source.
@@ -42,27 +42,22 @@ namespace Avalonia.Controls
         public ItemsSourceView(IEnumerable source)
         {
             source = source ?? throw new ArgumentNullException(nameof(source));
-
-            if (source is IList list)
-            {
-                _inner = list;
-            }
-            else if (source is IEnumerable<object> objectEnumerable)
+            _inner = source switch
             {
-                _inner = new List<object>(objectEnumerable);
-            }
-            else
-            {
-                _inner = new List<object>(source.Cast<object>());
-            }
-
-            ListenToCollectionChanges();
+                ItemsSourceView _ => throw new ArgumentException("Cannot wrap an existing ItemsSourceView.", nameof(source)),
+                IList list => list,
+                INotifyCollectionChanged _ => throw new ArgumentException(
+                    "Collection implements INotifyCollectionChanged by not IList.",
+                    nameof(source)),
+                IEnumerable<object> iObj => new List<object>(iObj),
+                _ => new List<object>(source.Cast<object>())
+            };
         }
 
         /// <summary>
         /// Gets the number of items in the collection.
         /// </summary>
-        public int Count => _inner.Count;
+        public int Count => Inner.Count;
 
         /// <summary>
         /// Gets a value that indicates whether the items source can provide a unique key for each item.
@@ -72,6 +67,19 @@ namespace Avalonia.Controls
         /// </remarks>
         public bool HasKeyIndexMapping => false;
 
+        /// <summary>
+        /// Gets the inner collection.
+        /// </summary>
+        public IList Inner
+        {
+            get
+            {
+                if (_inner is null)
+                    ThrowDisposed();
+                return _inner!;
+            }
+        }
+
         /// <summary>
         /// Retrieves the item at the specified index.
         /// </summary>
@@ -82,15 +90,38 @@ namespace Avalonia.Controls
         /// <summary>
         /// Occurs when the collection has changed to indicate the reason for the change and which items changed.
         /// </summary>
-        public event NotifyCollectionChangedEventHandler? CollectionChanged;
+        public event NotifyCollectionChangedEventHandler? CollectionChanged
+        {
+            add
+            {
+                if (_collectionChanged is null && Inner is INotifyCollectionChanged incc)
+                {
+                    incc.CollectionChanged += OnCollectionChanged;
+                }
+
+                _collectionChanged += value;
+            }
+
+            remove
+            {
+                _collectionChanged -= value;
+
+                if (_collectionChanged is null && Inner is INotifyCollectionChanged incc)
+                {
+                    incc.CollectionChanged -= OnCollectionChanged;
+                }
+            }
+        }
 
         /// <inheritdoc/>
         public void Dispose()
         {
-            if (_notifyCollectionChanged != null)
+            if (_inner is INotifyCollectionChanged incc)
             {
-                _notifyCollectionChanged.CollectionChanged -= OnCollectionChanged;
+                incc.CollectionChanged -= OnCollectionChanged;
             }
+
+            _inner = null;
         }
 
         /// <summary>
@@ -98,9 +129,9 @@ namespace Avalonia.Controls
         /// </summary>
         /// <param name="index">The index.</param>
         /// <returns>The item.</returns>
-        public object? GetAt(int index) => _inner[index];
+        public object? GetAt(int index) => Inner[index];
 
-        public int IndexOf(object? item) => _inner.IndexOf(item);
+        public int IndexOf(object? item) => Inner.IndexOf(item);
 
         public static ItemsSourceView GetOrCreate(IEnumerable? items)
         {
@@ -146,7 +177,7 @@ namespace Avalonia.Controls
 
         internal void AddListener(ICollectionChangedListener listener)
         {
-            if (_inner is INotifyCollectionChanged incc)
+            if (Inner is INotifyCollectionChanged incc)
             {
                 CollectionChangedEventManager.Instance.AddListener(incc, listener);
             }
@@ -154,7 +185,7 @@ namespace Avalonia.Controls
 
         internal void RemoveListener(ICollectionChangedListener listener)
         {
-            if (_inner is INotifyCollectionChanged incc)
+            if (Inner is INotifyCollectionChanged incc)
             {
                 CollectionChangedEventManager.Instance.RemoveListener(incc, listener);
             }
@@ -162,22 +193,15 @@ namespace Avalonia.Controls
 
         protected void OnItemsSourceChanged(NotifyCollectionChangedEventArgs args)
         {
-            CollectionChanged?.Invoke(this, args);
-        }
-
-        private void ListenToCollectionChanges()
-        {
-            if (_inner is INotifyCollectionChanged incc)
-            {
-                incc.CollectionChanged += OnCollectionChanged;
-                _notifyCollectionChanged = incc;
-            }
+            _collectionChanged?.Invoke(this, args);
         }
 
         private void OnCollectionChanged(object sender, NotifyCollectionChangedEventArgs e)
         {
             OnItemsSourceChanged(e);
         }
+
+        private void ThrowDisposed() => throw new ObjectDisposedException(nameof(ItemsSourceView));
     }
 
     public class ItemsSourceView<T> : ItemsSourceView, IReadOnlyList<T>
@@ -216,10 +240,10 @@ namespace Avalonia.Controls
         /// <param name="index">The index.</param>
         /// <returns>The item.</returns>
         [return: MaybeNull]
-        public new T GetAt(int index) => (T)_inner[index];
+        public new T GetAt(int index) => (T)Inner[index];
 
-        public IEnumerator<T> GetEnumerator() => _inner.Cast<T>().GetEnumerator();
-        IEnumerator IEnumerable.GetEnumerator() => _inner.GetEnumerator();
+        public IEnumerator<T> GetEnumerator() => Inner.Cast<T>().GetEnumerator();
+        IEnumerator IEnumerable.GetEnumerator() => Inner.GetEnumerator();
 
         public static new ItemsSourceView<T> GetOrCreate(IEnumerable? items)
         {

+ 6 - 7
src/Avalonia.Controls/NativeMenu.Export.cs

@@ -52,15 +52,10 @@ namespace Avalonia.Controls
         }
 
         public static readonly AttachedProperty<NativeMenu> MenuProperty
-            = AvaloniaProperty.RegisterAttached<NativeMenu, AvaloniaObject, NativeMenu>("Menu"/*, validate:
-                (o, v) =>
-                {
-                    if(!(o is Application || o is TopLevel))
-                        throw new InvalidOperationException("NativeMenu.Menu property isn't valid on "+o.GetType());
-                    return v;
-                }*/);
+            = AvaloniaProperty.RegisterAttached<NativeMenu, AvaloniaObject, NativeMenu>("Menu");
 
         public static void SetMenu(AvaloniaObject o, NativeMenu menu) => o.SetValue(MenuProperty, menu);
+
         public static NativeMenu GetMenu(AvaloniaObject o) => o.GetValue(MenuProperty);
         
         static NativeMenu()
@@ -79,6 +74,10 @@ namespace Avalonia.Controls
                 {
                     GetInfo(tl).Exporter?.SetNativeMenu(args.NewValue.GetValueOrDefault());
                 }
+                else if(args.Sender is INativeMenuExporterProvider provider)
+                {
+                    provider.NativeMenuExporter?.SetNativeMenu(args.NewValue.GetValueOrDefault());
+                }
             });
         }
     }

+ 2 - 0
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@@ -332,7 +332,9 @@ namespace Avalonia.Controls
         /// </summary>
         static NumericUpDown()
         {
+#pragma warning disable CS0612 // Type or member is obsolete
             CultureInfoProperty.Changed.Subscribe(OnCultureInfoChanged);
+#pragma warning restore CS0612 // Type or member is obsolete
             NumberFormatProperty.Changed.Subscribe(OnNumberFormatChanged);
             FormatStringProperty.Changed.Subscribe(FormatStringChanged);
             IncrementProperty.Changed.Subscribe(IncrementChanged);

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

@@ -275,7 +275,7 @@ namespace Avalonia.Controls.Platform
                 return;
             }
 
-            if (item.HasSubMenu)
+            if (item.HasSubMenu && item.IsEffectivelyEnabled)
             {
                 Open(item, true);
             }

+ 14 - 3
src/Avalonia.Controls/Platform/ITopLevelNativeMenuExporter.cs

@@ -1,14 +1,25 @@
 using System;
-using System.Collections.Generic;
 using Avalonia.Platform;
 
+#nullable enable
+
 namespace Avalonia.Controls.Platform
 {
-    public interface ITopLevelNativeMenuExporter
+    public interface INativeMenuExporter
+    {
+        void SetNativeMenu(NativeMenu? menu);
+    }
+
+    public interface ITopLevelNativeMenuExporter : INativeMenuExporter
     {
         bool IsNativeMenuExported { get; }
+
         event EventHandler OnIsNativeMenuExportedChanged;
-        void SetNativeMenu(NativeMenu menu);
+    }
+
+    public interface INativeMenuExporterProvider
+    {
+        INativeMenuExporter? NativeMenuExporter { get; }
     }
     
     public interface ITopLevelImplWithNativeMenuExporter : ITopLevelImpl

+ 35 - 0
src/Avalonia.Controls/Platform/ITrayIconImpl.cs

@@ -0,0 +1,35 @@
+using System;
+using Avalonia.Controls.Platform;
+
+#nullable enable
+
+namespace Avalonia.Platform
+{
+    public interface ITrayIconImpl : IDisposable
+    {
+        /// <summary>
+        /// Sets the icon of this tray icon.
+        /// </summary>
+        void SetIcon(IWindowIconImpl? icon);
+
+        /// <summary>
+        /// Sets the icon of this tray icon.
+        /// </summary>
+        void SetToolTipText(string? text);
+
+        /// <summary>
+        /// Sets if the tray icon is visible or not.
+        /// </summary>
+        void SetIsVisible(bool visible);
+
+        /// <summary>
+        /// Gets the MenuExporter to allow native menus to be exported to the TrayIcon.
+        /// </summary>
+        INativeMenuExporter? MenuExporter { get; }
+
+        /// <summary>
+        /// Gets or Sets the Action that is called when the TrayIcon is clicked.
+        /// </summary>
+        Action? OnClicked { get; set; }
+    }
+}

+ 5 - 0
src/Avalonia.Controls/Platform/IWindowingPlatform.cs

@@ -1,8 +1,13 @@
+#nullable enable
+
 namespace Avalonia.Platform
 {
     public interface IWindowingPlatform
     {
         IWindowImpl CreateWindow();
+
         IWindowImpl CreateEmbeddableWindow();
+
+        ITrayIconImpl? CreateTrayIcon();
     }
 }

+ 15 - 1
src/Avalonia.Controls/Platform/PlatformManager.cs

@@ -1,8 +1,9 @@
 using System;
 using System.Reactive.Disposables;
-using Avalonia.Media;
 using Avalonia.Platform;
 
+#nullable enable
+
 namespace Avalonia.Controls.Platform
 {
     public static partial class PlatformManager
@@ -22,6 +23,19 @@ namespace Avalonia.Controls.Platform
         {
         }
 
+        public static ITrayIconImpl? CreateTrayIcon()
+        {
+            var platform = AvaloniaLocator.Current.GetService<IWindowingPlatform>();
+
+            if (platform == null)
+            {
+                throw new Exception("Could not CreateWindow(): IWindowingPlatform is not registered.");
+            }
+
+            return s_designerMode ? null : platform.CreateTrayIcon();
+        }
+
+
         public static IWindowImpl CreateWindow()
         {
             var platform = AvaloniaLocator.Current.GetService<IWindowingPlatform>();

+ 11 - 8
src/Avalonia.Controls/Presenters/ScrollContentPresenter.cs

@@ -60,9 +60,6 @@ namespace Avalonia.Controls.Presenters
                 o => o.Viewport,
                 (o, v) => o.Viewport = v);
 
-        // Arbitrary chosen value, probably need to ask ILogicalScrollable
-        private const int LogicalScrollItemSize = 50;
-
         private bool _canHorizontallyScroll;
         private bool _canVerticallyScroll;
         private bool _arranging;
@@ -351,7 +348,8 @@ namespace Avalonia.Controls.Presenters
             if (Extent.Height > Viewport.Height || Extent.Width > Viewport.Width)
             {
                 var scrollable = Child as ILogicalScrollable;
-                bool isLogical = scrollable?.IsLogicalScrollEnabled == true;
+                var isLogical = scrollable?.IsLogicalScrollEnabled == true;
+                var logicalScrollItemSize = new Vector(1, 1);
 
                 double x = Offset.X;
                 double y = Offset.Y;
@@ -361,13 +359,18 @@ namespace Avalonia.Controls.Presenters
                     _activeLogicalGestureScrolls?.TryGetValue(e.Id, out delta);
                 delta += e.Delta;
 
+                if (isLogical && scrollable is object)
+                {
+                    logicalScrollItemSize = Bounds.Size / scrollable.Viewport;
+                }
+
                 if (Extent.Height > Viewport.Height)
                 {
                     double dy;
                     if (isLogical)
                     {
-                        var logicalUnits = delta.Y / LogicalScrollItemSize;
-                        delta = delta.WithY(delta.Y - logicalUnits * LogicalScrollItemSize);
+                        var logicalUnits = delta.Y / logicalScrollItemSize.Y;
+                        delta = delta.WithY(delta.Y - logicalUnits * logicalScrollItemSize.Y);
                         dy = logicalUnits * scrollable!.ScrollSize.Height;
                     }
                     else
@@ -384,8 +387,8 @@ namespace Avalonia.Controls.Presenters
                     double dx;
                     if (isLogical)
                     {
-                        var logicalUnits = delta.X / LogicalScrollItemSize;
-                        delta = delta.WithX(delta.X - logicalUnits * LogicalScrollItemSize);
+                        var logicalUnits = delta.X / logicalScrollItemSize.X;
+                        delta = delta.WithX(delta.X - logicalUnits * logicalScrollItemSize.X);
                         dx = logicalUnits * scrollable!.ScrollSize.Width;
                     }
                     else

+ 3 - 1
src/Avalonia.Controls/Primitives/Popup.cs

@@ -21,10 +21,12 @@ namespace Avalonia.Controls.Primitives
     /// <summary>
     /// Displays a popup window.
     /// </summary>
+#pragma warning disable CS0612 // Type or member is obsolete
     public class Popup : Control, IVisualTreeHost, IPopupHostProvider
+#pragma warning restore CS0612 // Type or member is obsolete
     {
         public static readonly StyledProperty<bool> WindowManagerAddShadowHintProperty =
-            AvaloniaProperty.Register<PopupRoot, bool>(nameof(WindowManagerAddShadowHint), true);
+            AvaloniaProperty.Register<PopupRoot, bool>(nameof(WindowManagerAddShadowHint), false);
 
         /// <summary>
         /// Defines the <see cref="Child"/> property.

+ 3 - 1
src/Avalonia.Controls/Primitives/PopupPositioning/IPopupPositioner.cs

@@ -447,8 +447,10 @@ namespace Avalonia.Controls.Primitives.PopupPositioning
             PopupPositionerConstraintAdjustment constraintAdjustment, Rect? rect)
         {
             // We need a better way for tracking the last pointer position
+#pragma warning disable CS0618 // Type or member is obsolete
             var pointer = topLevel.PointToClient(topLevel.PlatformImpl.MouseDevice.Position);
-            
+#pragma warning restore CS0618 // Type or member is obsolete
+
             positionerParameters.Offset = offset;
             positionerParameters.ConstraintAdjustment = constraintAdjustment;
             if (placement == PlacementMode.Pointer)

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

@@ -218,9 +218,12 @@ namespace Avalonia.Controls
                     TemplateProperties.Container2AnimationStartPosition = barIndicatorWidth2 * -1.5; // Position at -150%
                     TemplateProperties.Container2AnimationEndPosition = barIndicatorWidth2 * 1.66; // Position at 166%
 
+
+#pragma warning disable CS0618 // Type or member is obsolete
                     // Remove these properties when we switch to fluent as default and removed the old one.
                     IndeterminateStartingOffset = -dim;
                     IndeterminateEndingOffset = dim;
+#pragma warning restore CS0618 // Type or member is obsolete
 
                     var padding = Padding;
                     var rectangle = new RectangleGeometry(

+ 2 - 0
src/Avalonia.Controls/Remote/RemoteWidget.cs

@@ -77,8 +77,10 @@ namespace Avalonia.Controls.Remote
                     _bitmap.PixelSize.Height != _lastFrame.Height)
                 {
                     _bitmap?.Dispose();
+#pragma warning disable CS0618 // Type or member is obsolete
                     _bitmap = new WriteableBitmap(new PixelSize(_lastFrame.Width, _lastFrame.Height),
                         new Vector(96, 96), fmt);
+#pragma warning restore CS0618 // Type or member is obsolete
                 }
                 using (var l = _bitmap.Lock())
                 {

+ 3 - 3
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@@ -588,14 +588,14 @@ namespace Avalonia.Controls
                 throw new AvaloniaInternalException("Cannot set ItemsSourceView during layout.");
             }
 
-            ItemsSourceView?.Dispose();
-            ItemsSourceView = newValue;
-
             if (oldValue != null)
             {
                 oldValue.CollectionChanged -= OnItemsSourceViewChanged;
             }
 
+            ItemsSourceView?.Dispose();
+            ItemsSourceView = newValue;
+
             if (newValue != null)
             {
                 newValue.CollectionChanged += OnItemsSourceViewChanged;

+ 4 - 5
src/Avalonia.Controls/Templates/IControlTemplate.cs

@@ -1,3 +1,4 @@
+using System;
 using Avalonia.Controls.Primitives;
 using Avalonia.Styling;
 
@@ -10,18 +11,16 @@ namespace Avalonia.Controls.Templates
     {
     }
 
-    public class ControlTemplateResult
+    public class ControlTemplateResult : TemplateResult<IControl>
     {
         public IControl Control { get; }
-        public INameScope NameScope { get; }
 
-        public ControlTemplateResult(IControl control, INameScope nameScope)
+        public ControlTemplateResult(IControl control, INameScope nameScope) : base(control, nameScope)
         {
             Control = control;
-            NameScope = nameScope;
         }
 
-        public void Deconstruct(out IControl control, out INameScope scope)
+        public new void Deconstruct(out IControl control, out INameScope scope)
         {
             control = Control;
             scope = NameScope;

+ 20 - 0
src/Avalonia.Controls/Templates/TemplateResult.cs

@@ -0,0 +1,20 @@
+namespace Avalonia.Controls.Templates
+{
+    public class TemplateResult<T>
+    {
+        public T Result { get; }
+        public INameScope NameScope { get; }
+
+        public TemplateResult(T result, INameScope nameScope)
+        {
+            Result = result;
+            NameScope = nameScope;
+        }
+
+        public void Deconstruct(out T result, out INameScope scope)
+        {
+            result = Result;
+            scope = NameScope;
+        }
+    }
+}

+ 2 - 0
src/Avalonia.Controls/TextBox.cs

@@ -992,7 +992,9 @@ namespace Avalonia.Controls
             {
                 var point = e.GetPosition(_presenter);
                 var index = CaretIndex = _presenter.GetCaretIndex(point);
+#pragma warning disable CS0618 // Type or member is obsolete
                 switch (e.ClickCount)
+#pragma warning restore CS0618 // Type or member is obsolete
                 {
                     case 1:
                         SelectionStart = SelectionEnd = index;

+ 187 - 0
src/Avalonia.Controls/TrayIcon.cs

@@ -0,0 +1,187 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Collections;
+using Avalonia.Controls.ApplicationLifetimes;
+using Avalonia.Controls.Platform;
+using Avalonia.Platform;
+
+#nullable enable
+
+namespace Avalonia.Controls
+{
+    public sealed class TrayIcons : AvaloniaList<TrayIcon>
+    {
+    }
+
+    public class TrayIcon : AvaloniaObject, INativeMenuExporterProvider, IDisposable
+    {
+        private readonly ITrayIconImpl? _impl;
+
+        private TrayIcon(ITrayIconImpl? impl)
+        {
+            if (impl != null)
+            {
+                _impl = impl;
+
+                _impl.SetIsVisible(IsVisible);
+
+                _impl.OnClicked = () => Clicked?.Invoke(this, EventArgs.Empty);
+            }
+        }
+
+        public TrayIcon() : this(PlatformManager.CreateTrayIcon())
+        {
+        }
+
+        static TrayIcon()
+        {
+            IconsProperty.Changed.Subscribe(args =>
+            {
+                if (args.Sender is Application)
+                {
+                    if (args.OldValue.Value != null)
+                    {
+                        RemoveIcons(args.OldValue.Value);
+                    }
+
+                    if (args.NewValue.Value != null)
+                    {
+                        args.NewValue.Value.CollectionChanged += Icons_CollectionChanged;
+                    }
+                }
+            });
+
+            if (Application.Current.ApplicationLifetime is IClassicDesktopStyleApplicationLifetime lifetime)
+            {
+                lifetime.Exit += Lifetime_Exit;
+            }
+        }
+
+        /// <summary>
+        /// Raised when the TrayIcon is clicked.
+        /// Note, this is only supported on Win32 and some Linux DEs, 
+        /// on OSX this event is not raised.
+        /// </summary>
+        public event EventHandler? Clicked;
+
+        /// <summary>
+        /// Defines the <see cref="TrayIcons"/> attached property.
+        /// </summary>
+        public static readonly AttachedProperty<TrayIcons> IconsProperty
+            = AvaloniaProperty.RegisterAttached<TrayIcon, Application, TrayIcons>("Icons");
+
+        /// <summary>
+        /// Defines the <see cref="Menu"/> property.
+        /// </summary>
+        public static readonly StyledProperty<NativeMenu?> MenuProperty
+            = AvaloniaProperty.Register<TrayIcon, NativeMenu?>(nameof(Menu));
+
+        /// <summary>
+        /// Defines the <see cref="Icon"/> property.
+        /// </summary>
+        public static readonly StyledProperty<WindowIcon> IconProperty =
+            Window.IconProperty.AddOwner<TrayIcon>();
+
+        /// <summary>
+        /// Defines the <see cref="ToolTipText"/> property.
+        /// </summary>
+        public static readonly StyledProperty<string?> ToolTipTextProperty =
+            AvaloniaProperty.Register<TrayIcon, string?>(nameof(ToolTipText));
+
+        /// <summary>
+        /// Defines the <see cref="IsVisible"/> property.
+        /// </summary>
+        public static readonly StyledProperty<bool> IsVisibleProperty =
+            Visual.IsVisibleProperty.AddOwner<TrayIcon>();
+
+        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 Menu of the TrayIcon.
+        /// </summary>
+        public NativeMenu? Menu
+        {
+            get => GetValue(MenuProperty);
+            set => SetValue(MenuProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the icon of the TrayIcon.
+        /// </summary>
+        public WindowIcon Icon
+        {
+            get => GetValue(IconProperty);
+            set => SetValue(IconProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the tooltip text of the TrayIcon.
+        /// </summary>
+        public string? ToolTipText
+        {
+            get => GetValue(ToolTipTextProperty);
+            set => SetValue(ToolTipTextProperty, value);
+        }
+
+        /// <summary>
+        /// Gets or sets the visibility of the TrayIcon.
+        /// </summary>
+        public bool IsVisible
+        {
+            get => GetValue(IsVisibleProperty);
+            set => SetValue(IsVisibleProperty, value);
+        }
+
+        public INativeMenuExporter? NativeMenuExporter => _impl?.MenuExporter;
+
+        private static void Lifetime_Exit(object sender, ControlledApplicationLifetimeExitEventArgs e)
+        {
+            var trayIcons = GetIcons(Application.Current);
+
+            RemoveIcons(trayIcons);
+        }
+
+        private static void Icons_CollectionChanged(object sender, System.Collections.Specialized.NotifyCollectionChangedEventArgs e)
+        {
+            RemoveIcons(e.OldItems.Cast<TrayIcon>());
+        }
+
+        private static void RemoveIcons(IEnumerable<TrayIcon> icons)
+        {
+            foreach (var icon in icons)
+            {
+                icon.Dispose();
+            }
+        }
+
+        protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
+        {
+            base.OnPropertyChanged(change);
+
+            if (change.Property == IconProperty)
+            {
+                _impl?.SetIcon(Icon.PlatformImpl);
+            }
+            else if (change.Property == IsVisibleProperty)
+            {
+                _impl?.SetIsVisible(change.NewValue.GetValueOrDefault<bool>());
+            }
+            else if (change.Property == ToolTipTextProperty)
+            {
+                _impl?.SetToolTipText(change.NewValue.GetValueOrDefault<string?>());
+            }
+            else if (change.Property == MenuProperty)
+            {
+                _impl?.MenuExporter?.SetNativeMenu(change.NewValue.GetValueOrDefault<NativeMenu>());
+            }
+        }
+
+        /// <summary>
+        /// Disposes the tray icon (removing it from the tray area).
+        /// </summary>
+        public void Dispose() => _impl?.Dispose();
+    }
+}

+ 6 - 1
src/Avalonia.Controls/WindowTransparencyLevel.cs

@@ -20,6 +20,11 @@
         /// <summary>
         /// The window background is a blur-behind with a high blur radius. This level may fallback to Blur.
         /// </summary>
-        AcrylicBlur
+        AcrylicBlur,
+        
+        /// <summary>
+        /// The window background is based on desktop wallpaper tint with a blur. This will only work on Windows 11 
+        /// </summary>
+        Mica
     }
 }

+ 3 - 1
src/Avalonia.DesignerSupport/Remote/PreviewerWindowingPlatform.cs

@@ -16,7 +16,9 @@ namespace Avalonia.DesignerSupport.Remote
         private static DetachableTransportConnection s_lastWindowTransport;
         private static PreviewerWindowImpl s_lastWindow;
         public static List<object> PreFlightMessages = new List<object>();
-        
+
+        public ITrayIconImpl CreateTrayIcon() => null;
+
         public IWindowImpl CreateWindow() => new WindowStub();
 
         public IWindowImpl CreateEmbeddableWindow()

+ 9 - 3
src/Avalonia.Diagnostics/Diagnostics/Views/MainWindow.xaml.cs

@@ -160,13 +160,19 @@ namespace Avalonia.Diagnostics.Views
                 return;
             }
 
+            var root = Root;
+            if (root is null)
+            {
+                return;
+            }
+
             switch (e.Modifiers)
             {
                 case RawInputModifiers.Control | RawInputModifiers.Shift:
                 {
                     IControl? control = null;
 
-                    foreach (var popupRoot in GetPopupRoots(Root))
+                    foreach (var popupRoot in GetPopupRoots(root))
                     {
                         control = GetHoveredControl(popupRoot);
 
@@ -176,7 +182,7 @@ namespace Avalonia.Diagnostics.Views
                         }
                     }
 
-                    control ??= GetHoveredControl(Root);
+                    control ??= GetHoveredControl(root);
 
                     if (control != null)
                     {
@@ -190,7 +196,7 @@ namespace Avalonia.Diagnostics.Views
                 {
                     vm.FreezePopups = !vm.FreezePopups;
 
-                    foreach (var popupRoot in GetPopupRoots(Root))
+                    foreach (var popupRoot in GetPopupRoots(root))
                     {
                         if (popupRoot.Parent is Popup popup)
                         {

+ 5 - 3
src/Avalonia.Dialogs/AboutAvaloniaDialog.xaml.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Diagnostics;
 using System.Runtime.InteropServices;
+using System.Text.RegularExpressions;
 using Avalonia.Controls;
 using Avalonia.Markup.Xaml;
 
@@ -30,19 +31,20 @@ namespace Avalonia.Dialogs
             }
             else
             {
-                using (Process process = Process.Start(new ProcessStartInfo
+                using Process process = Process.Start(new ProcessStartInfo
                 {
                     FileName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows) ? url : "open",
                     Arguments = RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? $"{url}" : "",
                     CreateNoWindow = true,
                     UseShellExecute = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
-                }));
+                });
             }
         }
 
         private static void ShellExec(string cmd, bool waitForExit = true)
         {
-            var escapedArgs = cmd.Replace("\"", "\\\"");
+            var escapedArgs = Regex.Replace(cmd, "(?=[`~!#&*()|;'<>])", "\\")
+                .Replace("\"", "\\\\\\\"");
 
             using (var process = Process.Start(
                 new ProcessStartInfo

+ 2 - 2
src/Avalonia.Dialogs/ManagedFileChooser.cs

@@ -1,13 +1,11 @@
 using System;
 using System.Linq;
 using System.Threading.Tasks;
-using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.Primitives;
 using Avalonia.Input;
 using Avalonia.Interactivity;
 using Avalonia.LogicalTree;
-using Avalonia.Markup.Xaml;
 
 namespace Avalonia.Dialogs
 {
@@ -35,7 +33,9 @@ namespace Avalonia.Dialogs
             if (_quickLinksRoot != null)
             {
                 var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control);
+#pragma warning disable CS0618 // Type or member is obsolete
                 if (e.ClickCount == 2 || isQuickLink)
+#pragma warning restore CS0618 // Type or member is obsolete
                 {
                     if (model.ItemType == ManagedFileChooserItemType.File)
                     {

+ 1 - 1
src/Avalonia.Dialogs/ManagedFileChooserSources.cs

@@ -67,7 +67,7 @@ namespace Avalonia.Dialogs
                        {
                            Directory.GetFiles(x.VolumePath);
                        }
-                       catch (Exception _)
+                       catch (Exception)
                        {
                            return null;
                        }

+ 5 - 2
src/Avalonia.FreeDesktop/DBusHelper.cs

@@ -51,8 +51,11 @@ namespace Avalonia.FreeDesktop
 
         public static Connection TryInitialize(string dbusAddress = null)
         {
-            if (Connection != null)
-                return Connection;
+            return Connection ?? TryGetConnection(dbusAddress);
+        }
+        
+        public static Connection TryGetConnection(string dbusAddress = null)
+        { 
             var oldContext = SynchronizationContext.Current;
             try
             {

+ 55 - 20
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@@ -8,6 +8,7 @@ using Avalonia.Controls;
 using Avalonia.Controls.Platform;
 using Avalonia.FreeDesktop.DBusMenu;
 using Avalonia.Input;
+using Avalonia.Platform;
 using Avalonia.Threading;
 using Tmds.DBus;
 #pragma warning disable 1998
@@ -16,51 +17,78 @@ namespace Avalonia.FreeDesktop
 {
     public class DBusMenuExporter
     {
-        public static ITopLevelNativeMenuExporter TryCreate(IntPtr xid)
+        public static ITopLevelNativeMenuExporter TryCreateTopLevelNativeMenu(IntPtr xid)
         {
             if (DBusHelper.Connection == null)
                 return null;
 
             return new DBusMenuExporterImpl(DBusHelper.Connection, xid);
         }
+        
+        public static INativeMenuExporter TryCreateDetachedNativeMenu(ObjectPath path, Connection currentConection)
+        {
+            return new DBusMenuExporterImpl(currentConection, path);
+        }
+
+        public static ObjectPath GenerateDBusMenuObjPath => "/net/avaloniaui/dbusmenu/"
+                                                           + Guid.NewGuid().ToString("N");
 
-        class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable
+        private class DBusMenuExporterImpl : ITopLevelNativeMenuExporter, IDBusMenu, IDisposable
         {
             private readonly Connection _dbus;
             private readonly uint _xid;
-            private IRegistrar _registar;
+            private IRegistrar _registrar;
             private bool _disposed;
             private uint _revision = 1;
             private NativeMenu _menu;
-            private Dictionary<int, NativeMenuItemBase> _idsToItems = new Dictionary<int, NativeMenuItemBase>();
-            private Dictionary<NativeMenuItemBase, int> _itemsToIds = new Dictionary<NativeMenuItemBase, int>();
+            private readonly Dictionary<int, NativeMenuItemBase> _idsToItems = new Dictionary<int, NativeMenuItemBase>();
+            private readonly Dictionary<NativeMenuItemBase, int> _itemsToIds = new Dictionary<NativeMenuItemBase, int>();
             private readonly HashSet<NativeMenu> _menus = new HashSet<NativeMenu>();
             private bool _resetQueued;
             private int _nextId = 1;
+            private bool _appMenu = true;
+            
             public DBusMenuExporterImpl(Connection dbus, IntPtr xid)
             {
                 _dbus = dbus;
                 _xid = (uint)xid.ToInt32();
-                ObjectPath = new ObjectPath("/net/avaloniaui/dbusmenu/"
-                                            + Guid.NewGuid().ToString().Replace("-", ""));
+                ObjectPath = GenerateDBusMenuObjPath;
                 SetNativeMenu(new NativeMenu());
                 Init();
             }
 
+            public DBusMenuExporterImpl(Connection dbus, ObjectPath path)
+            {
+                _dbus = dbus;
+                _appMenu = false;
+                ObjectPath = path;
+                SetNativeMenu(new NativeMenu());
+                Init();
+            }
+            
             async void Init()
             {
                 try
                 {
-                    await _dbus.RegisterObjectAsync(this);
-                    _registar = DBusHelper.Connection.CreateProxy<IRegistrar>(
-                        "com.canonical.AppMenu.Registrar",
-                        "/com/canonical/AppMenu/Registrar");
-                    if (!_disposed)
-                        await _registar.RegisterWindowAsync(_xid, ObjectPath);
+                    if (_appMenu)
+                    {
+                        await _dbus.RegisterObjectAsync(this);
+                        _registrar = DBusHelper.Connection.CreateProxy<IRegistrar>(
+                            "com.canonical.AppMenu.Registrar",
+                            "/com/canonical/AppMenu/Registrar");
+                        if (!_disposed)
+                            await _registrar.RegisterWindowAsync(_xid, ObjectPath);
+                    }
+                    else
+                    {
+                        await _dbus.RegisterObjectAsync(this);
+                    }
                 }
                 catch (Exception e)
                 {
-                    Console.Error.WriteLine(e);
+                    Logging.Logger.TryGet(Logging.LogEventLevel.Error, Logging.LogArea.X11Platform)
+                        ?.Log(this, e.Message);
+
                     // It's not really important if this code succeeds,
                     // and it's not important to know if it succeeds
                     // since even if we register the window it's not guaranteed that
@@ -75,7 +103,7 @@ namespace Avalonia.FreeDesktop
                 _disposed = true;
                 _dbus.UnregisterObject(this);
                 // Fire and forget
-                _registar?.UnregisterWindowAsync(_xid);
+                _registrar?.UnregisterWindowAsync(_xid);
             }
 
 
@@ -248,17 +276,24 @@ namespace Avalonia.FreeDesktop
                         if (item.ToggleType != NativeMenuItemToggleType.None)
                             return item.IsChecked ? 1 : 0;
                     }
-
+                    
                     if (name == "icon-data")
                     {
                         if (item.Icon != null)
                         {
-                            var ms = new MemoryStream();
-                            item.Icon.Save(ms);
-                            return ms.ToArray();
+                            var loader = AvaloniaLocator.Current.GetService<IPlatformIconLoader>();
+
+                            if (loader != null)
+                            {
+                                var icon = loader.LoadIcon(item.Icon.PlatformImpl.Item);
+
+                                using var ms = new MemoryStream();
+                                icon.Save(ms);
+                                return ms.ToArray();
+                            }
                         }
                     }
-
+                    
                     if (name == "children-display")
                         return menu != null ? "submenu" : null;
                 }

+ 2 - 0
src/Avalonia.Headless/AvaloniaHeadlessPlatform.cs

@@ -51,6 +51,8 @@ namespace Avalonia.Headless
             public IWindowImpl CreateEmbeddableWindow() => throw new PlatformNotSupportedException();
 
             public IPopupImpl CreatePopup() => new HeadlessWindowImpl(true);
+
+            public ITrayIconImpl CreateTrayIcon() => null;
         }
         
         internal static void Initialize()

+ 6 - 2
src/Avalonia.Input/Gestures.cs

@@ -81,17 +81,21 @@ namespace Avalonia.Input
                 var e = (PointerPressedEventArgs)ev;
                 var visual = (IVisual)ev.Source;
 
-                if (e.ClickCount <= 1)
+#pragma warning disable CS0618 // Type or member is obsolete
+                var clickCount = e.ClickCount;
+#pragma warning restore CS0618 // Type or member is obsolete
+                if (clickCount <= 1)
                 {
                     s_lastPress = new WeakReference<IInteractive>(ev.Source);
                 }
-                else if (s_lastPress != null && e.ClickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
+                else if (s_lastPress != null && clickCount == 2 && e.GetCurrentPoint(visual).Properties.IsLeftButtonPressed)
                 {
                     if (s_lastPress.TryGetTarget(out var target) && target == e.Source)
                     {
                         e.Source.RaiseEvent(new TappedEventArgs(DoubleTappedEvent, e));
                     }
                 }
+
             }
         }
 

+ 2 - 0
src/Avalonia.Input/MouseDevice.cs

@@ -75,7 +75,9 @@ namespace Avalonia.Input
                 throw new InvalidOperationException("Control is not attached to visual tree.");
             }
 
+#pragma warning disable CS0618 // Type or member is obsolete
             var rootPoint = relativeTo.VisualRoot.PointToClient(Position);
+#pragma warning restore CS0618 // Type or member is obsolete
             var transform = relativeTo.VisualRoot.TransformToVisual(relativeTo);
             return rootPoint * transform!.Value;
         }

+ 50 - 13
src/Avalonia.Native/AvaloniaNativeMenuExporter.cs

@@ -9,14 +9,15 @@ using Avalonia.Threading;
 
 namespace Avalonia.Native
 {
-    class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter
+    internal class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter
     {
-        private IAvaloniaNativeFactory _factory;
+        private readonly IAvaloniaNativeFactory _factory;
         private bool _resetQueued = true;
-        private bool _exported = false;
-        private IAvnWindow _nativeWindow;
+        private bool _exported;
+        private readonly IAvnWindow _nativeWindow;
         private NativeMenu _menu;
         private __MicroComIAvnMenuProxy _nativeMenu;
+        private readonly IAvnTrayIcon _trayIcon;
 
         public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory)
         {
@@ -33,13 +34,21 @@ namespace Avalonia.Native
             DoLayoutReset();
         }
 
+        public AvaloniaNativeMenuExporter(IAvnTrayIcon trayIcon, IAvaloniaNativeFactory factory)
+        {
+            _factory = factory;
+            _trayIcon = trayIcon;
+            
+            DoLayoutReset();
+        }
+
         public bool IsNativeMenuExported => _exported;
 
         public event EventHandler OnIsNativeMenuExportedChanged;
 
         public void SetNativeMenu(NativeMenu menu)
         {
-            _menu = menu == null ? new NativeMenu() : menu;
+            _menu = menu ?? new NativeMenu();
             DoLayoutReset(true);
         }
 
@@ -82,15 +91,22 @@ namespace Avalonia.Native
 
                 if (_nativeWindow is null)
                 {
-                    var appMenu = NativeMenu.GetMenu(Application.Current);
+                    if (_trayIcon is null)
+                    {
+                        var appMenu = NativeMenu.GetMenu(Application.Current);
 
-                    if (appMenu == null)
+                        if (appMenu == null)
+                        {
+                            appMenu = CreateDefaultAppMenu();
+                            NativeMenu.SetMenu(Application.Current, appMenu);
+                        }
+
+                        SetMenu(appMenu);
+                    }
+                    else if (_menu != null)
                     {
-                        appMenu = CreateDefaultAppMenu();
-                        NativeMenu.SetMenu(Application.Current, appMenu);
+                        SetMenu(_trayIcon, _menu);
                     }
-
-                    SetMenu(appMenu);
                 }
                 else
                 {
@@ -118,7 +134,7 @@ namespace Avalonia.Native
 
             var appMenuHolder = menuItem?.Parent;
 
-            if (menu.Parent is null)
+            if (menuItem is null)
             {
                 menuItem = new NativeMenuItem();
             }
@@ -136,7 +152,7 @@ namespace Avalonia.Native
 
             if (_nativeMenu is null)
             {
-                _nativeMenu = (__MicroComIAvnMenuProxy)__MicroComIAvnMenuProxy.Create(_factory);
+                _nativeMenu = __MicroComIAvnMenuProxy.Create(_factory);
 
                 _nativeMenu.Initialize(this, appMenuHolder, "");
 
@@ -171,5 +187,26 @@ namespace Avalonia.Native
                 avnWindow.SetMainMenu(_nativeMenu);
             }
         }
+
+        private void SetMenu(IAvnTrayIcon trayIcon, NativeMenu menu)
+        {
+            var setMenu = false;
+
+            if (_nativeMenu is null)
+            {
+                _nativeMenu = __MicroComIAvnMenuProxy.Create(_factory);
+
+                _nativeMenu.Initialize(this, menu, "");     
+
+                setMenu = true;           
+            }
+
+            _nativeMenu.Update(_factory, menu);
+
+            if(setMenu)
+            {
+                trayIcon.SetMenu(_nativeMenu);
+            }
+        }
     }
 }

+ 5 - 0
src/Avalonia.Native/AvaloniaNativePlatform.cs

@@ -134,6 +134,11 @@ namespace Avalonia.Native
             }
         }
 
+        public ITrayIconImpl CreateTrayIcon ()
+        {
+            return new TrayIconImpl(_factory);
+        }
+
         public IWindowImpl CreateWindow()
         {
             return new WindowImpl(_factory, _options, _platformGl);

+ 63 - 0
src/Avalonia.Native/TrayIconImpl.cs

@@ -0,0 +1,63 @@
+using System;
+using System.IO;
+using Avalonia.Controls.Platform;
+using Avalonia.Native.Interop;
+using Avalonia.Platform;
+
+#nullable enable
+
+namespace Avalonia.Native
+{
+    internal class TrayIconImpl : ITrayIconImpl
+    {
+        private readonly IAvnTrayIcon _native;
+
+        public TrayIconImpl(IAvaloniaNativeFactory factory)
+        {
+            _native = factory.CreateTrayIcon();
+
+            MenuExporter = new AvaloniaNativeMenuExporter(_native, factory);
+        }
+
+        public Action? OnClicked { get; set; }
+
+        public void Dispose()
+        {
+            _native.Dispose();
+        }
+
+        public unsafe void SetIcon(IWindowIconImpl? icon)
+        {
+            if (icon is null)
+            {
+                _native.SetIcon(null, IntPtr.Zero);
+            }
+            else
+            {
+                using (var ms = new MemoryStream())
+                {
+                    icon.Save(ms);
+
+                    var imageData = ms.ToArray();
+
+                    fixed (void* ptr = imageData)
+                    {
+                        _native.SetIcon(ptr, new IntPtr(imageData.Length));
+                    }
+                }
+            }
+        }
+
+        public void SetToolTipText(string? text)
+        {
+            // NOP
+        }
+
+        public void SetIsVisible(bool visible)
+        {
+            _native.SetIsVisible(visible.AsComBool());
+        }
+
+        public INativeMenuExporter? MenuExporter { get; }
+    }
+}

+ 9 - 0
src/Avalonia.Native/avn.idl

@@ -479,6 +479,7 @@ interface IAvaloniaNativeFactory : IUnknown
      HRESULT CreateMenu(IAvnMenuEvents* cb, IAvnMenu** ppv);
      HRESULT CreateMenuItem(IAvnMenuItem** ppv);
      HRESULT CreateMenuItemSeparator(IAvnMenuItem** ppv);
+     HRESULT CreateTrayIcon(IAvnTrayIcon** ppv);
      HRESULT CreateAutomationNode(IAvnAutomationPeer* peer, IAvnAutomationNode** ppv);
 }
 
@@ -718,6 +719,14 @@ interface IAvnGlSurfaceRenderingSession : IUnknown
      HRESULT GetScaling(double* ret);
 }
 
+[uuid(60992d19-38f0-4141-a0a9-76ac303801f3)]
+interface IAvnTrayIcon : IUnknown
+{
+    HRESULT SetIcon(void* data, size_t length);
+    HRESULT SetMenu(IAvnMenu* menu);
+    HRESULT SetIsVisible(bool isVisible);
+}
+
 [uuid(a7724dc1-cf6b-4fa8-9d23-228bf2593edc)]
 interface IAvnMenu : IUnknown
 {

+ 8 - 2
src/Avalonia.Themes.Default/OverlayPopupHost.xaml

@@ -1,5 +1,11 @@
-<Style xmlns="https://github.com/avaloniaui" Selector="OverlayPopupHost">
-  <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
+<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="FontFamily" Value="{x:Static FontFamily.Default}" />
+  <Setter Property="FontWeight" Value="400" />
+  <Setter Property="FontStyle" Value="Normal" />
   <Setter Property="Template">
     <ControlTemplate>
       <Panel>

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

@@ -1,5 +1,11 @@
-<Style xmlns="https://github.com/avaloniaui" Selector="PopupRoot">
-  <Setter Property="Background" Value="{DynamicResource ThemeBackgroundBrush}"/>
+<Style xmlns="https://github.com/avaloniaui"
+       xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
+       Selector="PopupRoot">
+  <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>

+ 5 - 1
src/Avalonia.Themes.Fluent/Controls/OverlayPopupHost.xaml

@@ -1,5 +1,9 @@
 <Style xmlns="https://github.com/avaloniaui" Selector="OverlayPopupHost">
-  <Setter Property="Background" Value="{DynamicResource SystemControlBackgroundAltHighBrush}"/>
+  <Setter Property="Foreground" Value="{DynamicResource SystemControlForegroundBaseHighBrush}"/>
+  <Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}"/>
+  <Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" />
+  <Setter Property="FontWeight" Value="400" />
+  <Setter Property="FontStyle" Value="Normal" />
   <Setter Property="Template">
     <ControlTemplate>
       <Panel>

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

@@ -2,7 +2,11 @@
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml">
   <Style Selector="PopupRoot">
     <Setter Property="TransparencyLevelHint" Value="Transparent" />
-    <Setter Property="Background" Value="{x:Null}" />
+    <Setter Property="Foreground" Value="{DynamicResource SystemControlForegroundBaseHighBrush}"/>
+    <Setter Property="FontSize" Value="{DynamicResource ControlContentThemeFontSize}"/>
+    <Setter Property="FontFamily" Value="{DynamicResource ContentControlThemeFontFamily}" />
+    <Setter Property="FontWeight" Value="400" />
+    <Setter Property="FontStyle" Value="Normal" />
     <Setter Property="Template">
       <ControlTemplate>
         <Panel>

+ 8 - 0
src/Avalonia.Visuals/Media/DrawingImage.cs

@@ -11,6 +11,14 @@ namespace Avalonia.Media
     /// </summary>
     public class DrawingImage : AvaloniaObject, IImage, IAffectsRender
     {
+        public DrawingImage() 
+        { 
+        }
+
+        public DrawingImage(Drawing drawing)
+        {
+            Drawing = drawing;
+        }
         /// <summary>
         /// Defines the <see cref="Drawing"/> property.
         /// </summary>

+ 1 - 1
src/Avalonia.Visuals/Media/FormattedText.cs

@@ -200,7 +200,7 @@ namespace Avalonia.Media
 
         private void Set<T>(ref T field, T value)
         {
-            if (field != null && field.Equals(value))
+            if (EqualityComparer<T>.Default.Equals(field, value))
             {
                 return;
             }

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

@@ -279,13 +279,13 @@ namespace Avalonia.Rendering
         /// <inheritdoc/>
         Size IVisualBrushRenderer.GetRenderTargetSize(IVisualBrush brush)
         {
-            return (_currentDraw.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty;
+            return (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual]?.Size ?? Size.Empty;
         }
 
         /// <inheritdoc/>
         void IVisualBrushRenderer.RenderVisualBrush(IDrawingContextImpl context, IVisualBrush brush)
         {
-            var childScene = (_currentDraw.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual];
+            var childScene = (_currentDraw?.Item as BrushDrawOperation)?.ChildScenes?[brush.Visual];
 
             if (childScene != null)
             {

+ 5 - 2
src/Avalonia.Visuals/Rendering/ImmediateRenderer.cs

@@ -289,11 +289,14 @@ namespace Avalonia.Rendering
 
                 using (context.PushPostTransform(m))
                 using (context.PushOpacity(opacity))
-                using (clipToBounds 
-                    ? visual is IVisualWithRoundRectClip roundClipVisual 
+                using (clipToBounds
+#pragma warning disable CS0618 // Type or member is obsolete
+                    ? visual is IVisualWithRoundRectClip roundClipVisual
                         ? context.PushClip(new RoundedRect(bounds, roundClipVisual.ClipToBoundsRadius))
                         : context.PushClip(bounds) 
                     : default(DrawingContext.PushedState))
+#pragma warning restore CS0618 // Type or member is obsolete
+
                 using (visual.Clip != null ? context.PushGeometryClip(visual.Clip) : default(DrawingContext.PushedState))
                 using (visual.OpacityMask != null ? context.PushOpacityMask(visual.OpacityMask, bounds) : default(DrawingContext.PushedState))
                 using (context.PushTransformContainer())

+ 16 - 4
src/Avalonia.Visuals/Rendering/SceneGraph/ClipNode.cs

@@ -11,19 +11,23 @@ namespace Avalonia.Rendering.SceneGraph
         /// Initializes a new instance of the <see cref="ClipNode"/> class that represents a
         /// clip push.
         /// </summary>
+        /// <param name="transform">The current transform.</param>
         /// <param name="clip">The clip to push.</param>
-        public ClipNode(Rect clip)
+        public ClipNode(Matrix transform, Rect clip)
         {
+            Transform = transform;
             Clip = clip;
         }
-        
+
         /// <summary>
         /// Initializes a new instance of the <see cref="ClipNode"/> class that represents a
         /// clip push.
         /// </summary>
+        /// <param name="transform">The current transform.</param>
         /// <param name="clip">The clip to push.</param>
-        public ClipNode(RoundedRect clip)
+        public ClipNode(Matrix transform, RoundedRect clip)
         {
+            Transform = transform;
             Clip = clip;
         }
 
@@ -43,23 +47,31 @@ namespace Avalonia.Rendering.SceneGraph
         /// </summary>
         public RoundedRect? Clip { get; }
 
+        /// <summary>
+        /// Gets the transform with which the node will be drawn.
+        /// </summary>
+        public Matrix Transform { get; }
+
         /// <inheritdoc/>
         public bool HitTest(Point p) => false;
 
         /// <summary>
         /// Determines if this draw operation equals another.
         /// </summary>
+        /// <param name="transform">The transform of the other draw operation.</param>
         /// <param name="clip">The clip of the other draw operation.</param>
         /// <returns>True if the draw operations are the same, otherwise false.</returns>
         /// <remarks>
         /// The properties of the other draw operation are passed in as arguments to prevent
         /// allocation of a not-yet-constructed draw operation object.
         /// </remarks>
-        public bool Equals(RoundedRect? clip) => Clip == clip;
+        public bool Equals(Matrix transform, RoundedRect? clip) => Transform == transform && Clip == clip;
 
         /// <inheritdoc/>
         public void Render(IDrawingContextImpl context)
         {
+            context.Transform = Transform;
+
             if (Clip.HasValue)
             {
                 context.PushClip(Clip.Value);

+ 6 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/CustomDrawOperation.cs

@@ -17,7 +17,12 @@ namespace Avalonia.Rendering.SceneGraph
 
         public override bool HitTest(Point p)
         {
-            return Custom.HitTest(p * Transform);
+            if (Transform.HasInverse)
+            {
+                return Custom.HitTest(p * Transform.Invert());
+            }
+
+            return false;
         }
 
         public override void Render(IDrawingContextImpl context)

+ 6 - 6
src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs

@@ -303,9 +303,9 @@ namespace Avalonia.Rendering.SceneGraph
         {
             var next = NextDrawAs<ClipNode>();
 
-            if (next == null || !next.Item.Equals(clip))
+            if (next == null || !next.Item.Equals(Transform, clip))
             {
-                Add(new ClipNode(clip));
+                Add(new ClipNode(Transform, clip));
             }
             else
             {
@@ -318,9 +318,9 @@ namespace Avalonia.Rendering.SceneGraph
         {
             var next = NextDrawAs<ClipNode>();
 
-            if (next == null || !next.Item.Equals(clip))
+            if (next == null || !next.Item.Equals(Transform, clip))
             {
-                Add(new ClipNode(clip));
+                Add(new ClipNode(Transform, clip));
             }
             else
             {
@@ -333,9 +333,9 @@ namespace Avalonia.Rendering.SceneGraph
         {
             var next = NextDrawAs<GeometryClipNode>();
 
-            if (next == null || !next.Item.Equals(clip))
+            if (next == null || !next.Item.Equals(Transform, clip))
             {
-                Add(new GeometryClipNode(clip));
+                Add(new GeometryClipNode(Transform, clip));
             }
             else
             {

+ 12 - 2
src/Avalonia.Visuals/Rendering/SceneGraph/GeometryClipNode.cs

@@ -11,9 +11,11 @@ namespace Avalonia.Rendering.SceneGraph
         /// Initializes a new instance of the <see cref="GeometryClipNode"/> class that represents a
         /// geometry clip push.
         /// </summary>
+        /// <param name="transform">The current transform.</param>
         /// <param name="clip">The clip to push.</param>
-        public GeometryClipNode(IGeometryImpl clip)
+        public GeometryClipNode(Matrix transform, IGeometryImpl clip)
         {
+            Transform = transform;
             Clip = clip;
         }
 
@@ -33,23 +35,31 @@ namespace Avalonia.Rendering.SceneGraph
         /// </summary>
         public IGeometryImpl Clip { get; }
 
+        /// <summary>
+        /// Gets the transform with which the node will be drawn.
+        /// </summary>
+        public Matrix Transform { get; }
+
         /// <inheritdoc/>
         public bool HitTest(Point p) => false;
 
         /// <summary>
         /// Determines if this draw operation equals another.
         /// </summary>
+        /// <param name="transform">The transform of the other draw operation.</param>
         /// <param name="clip">The clip of the other draw operation.</param>
         /// <returns>True if the draw operations are the same, otherwise false.</returns>
         /// <remarks>
         /// The properties of the other draw operation are passed in as arguments to prevent
         /// allocation of a not-yet-constructed draw operation object.
         /// </remarks>
-        public bool Equals(IGeometryImpl clip) => Clip == clip;
+        public bool Equals(Matrix transform, IGeometryImpl clip) => Transform == transform && Clip == clip;
 
         /// <inheritdoc/>
         public void Render(IDrawingContextImpl context)
         {
+            context.Transform = Transform;
+
             if (Clip != null)
             {
                 context.PushGeometryClip(Clip);

+ 42 - 3
src/Avalonia.Visuals/Rendering/SceneGraph/LineNode.cs

@@ -1,4 +1,5 @@
-using System.Collections.Generic;
+using System;
+using System.Collections.Generic;
 using Avalonia.Media;
 using Avalonia.Media.Immutable;
 using Avalonia.Platform;
@@ -82,8 +83,46 @@ namespace Avalonia.Rendering.SceneGraph
 
         public override bool HitTest(Point p)
         {
-            // TODO: Implement line hit testing.
-            return false;
+            if (!Transform.HasInverse)
+                return false;
+
+            p *= Transform.Invert();
+
+            var halfThickness = Pen.Thickness / 2;
+            var minX = Math.Min(P1.X, P2.X) - halfThickness;
+            var maxX = Math.Max(P1.X, P2.X) + halfThickness;
+            var minY = Math.Min(P1.Y, P2.Y) - halfThickness;
+            var maxY = Math.Max(P1.Y, P2.Y) + halfThickness;
+
+            if (p.X < minX || p.X > maxX || p.Y < minY || p.Y > maxY)
+                return false;
+
+            var a = P1;
+            var b = P2;
+
+            //If dot1 or dot2 is negative, then the angle between the perpendicular and the segment is obtuse.
+            //The distance from a point to a straight line is defined as the
+            //length of the vector formed by the point and the closest point of the segment
+
+            Vector ap = p - a;
+            var dot1 = Vector.Dot(b - a, ap);
+
+            if (dot1 < 0)
+                return ap.Length <= Pen.Thickness / 2;
+
+            Vector bp = p - b;
+            var dot2 = Vector.Dot(a - b, bp);
+
+            if (dot2 < 0)
+                return bp.Length <= halfThickness;
+
+            var bXaX = b.X - a.X;
+            var bYaY = b.Y - a.Y;
+
+            var distance = (bXaX * (p.Y - a.Y) - bYaY * (p.X - a.X)) /
+                           (Math.Sqrt(bXaX * bXaX + bYaY * bYaY));
+
+            return Math.Abs(distance) <= halfThickness;
         }
     }
 }

+ 3 - 1
src/Avalonia.Visuals/Rendering/SceneGraph/SceneBuilder.cs

@@ -164,10 +164,12 @@ namespace Avalonia.Rendering.SceneGraph
             var visual = node.Visual;
             var opacity = visual.Opacity;
             var clipToBounds = visual.ClipToBounds;
+#pragma warning disable CS0618 // Type or member is obsolete
             var clipToBoundsRadius = visual is IVisualWithRoundRectClip roundRectClip ?
                 roundRectClip.ClipToBoundsRadius :
                 default;
-            
+#pragma warning restore CS0618 // Type or member is obsolete
+
             var bounds = new Rect(visual.Bounds.Size);
             var contextImpl = (DeferredDrawingContextImpl)context.PlatformImpl;
 

+ 1 - 1
src/Avalonia.Visuals/Vector.cs

@@ -175,7 +175,7 @@ namespace Avalonia
                    MathUtilities.AreClose(_y, other._y);
         }
 
-        public override bool Equals(object obj) => obj is Vector other && Equals(other);
+        public override bool Equals(object? obj) => obj is Vector other && Equals(other);
 
         public override int GetHashCode()
         {

+ 3 - 3
src/Avalonia.X11/X11CursorFactory.cs

@@ -23,9 +23,9 @@ namespace Avalonia.X11
         private static readonly Dictionary<StandardCursorType, CursorFontShape> s_mapping =
             new Dictionary<StandardCursorType, CursorFontShape>
             {
-                {StandardCursorType.Arrow, CursorFontShape.XC_top_left_arrow},
+                {StandardCursorType.Arrow, CursorFontShape.XC_left_ptr},
                 {StandardCursorType.Cross, CursorFontShape.XC_cross},
-                {StandardCursorType.Hand, CursorFontShape.XC_hand1},
+                {StandardCursorType.Hand, CursorFontShape.XC_hand2},
                 {StandardCursorType.Help, CursorFontShape.XC_question_arrow},
                 {StandardCursorType.Ibeam, CursorFontShape.XC_xterm},
                 {StandardCursorType.No, CursorFontShape.XC_X_cursor},
@@ -67,7 +67,7 @@ namespace Avalonia.X11
             {
                 handle = s_mapping.TryGetValue(cursorType, out var shape)
                 ? _cursors[shape]
-                : _cursors[CursorFontShape.XC_top_left_arrow];
+                : _cursors[CursorFontShape.XC_left_ptr];
             }
             return new CursorImpl(handle);
         }

+ 2 - 0
src/Avalonia.X11/X11IconLoader.cs

@@ -77,7 +77,9 @@ namespace Avalonia.X11
         public void Save(Stream outputStream)
         {
             using (var wr =
+#pragma warning disable CS0618 // Type or member is obsolete
                 new WriteableBitmap(new PixelSize(_width, _height), new Vector(96, 96), PixelFormat.Bgra8888))
+#pragma warning restore CS0618 // Type or member is obsolete
             {
                 using (var fb = wr.Lock())
                 {

+ 1 - 1
src/Avalonia.X11/X11Info.cs

@@ -42,7 +42,7 @@ namespace Avalonia.X11
             DefaultScreen = XDefaultScreen(display);
             BlackPixel = XBlackPixel(display, DefaultScreen);
             RootWindow = XRootWindow(display, DefaultScreen);
-            DefaultCursor = XCreateFontCursor(display, CursorFontShape.XC_top_left_arrow);
+            DefaultCursor = XCreateFontCursor(display, CursorFontShape.XC_left_ptr);
             DefaultRootWindow = XDefaultRootWindow(display);
             Atoms = new X11Atoms(display);
 

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

@@ -52,11 +52,14 @@ namespace Avalonia.X11
 
             XInitThreads();
             Display = XOpenDisplay(IntPtr.Zero);
+            if (Display == IntPtr.Zero)
+                throw new Exception("XOpenDisplay failed");
             DeferredDisplay = XOpenDisplay(IntPtr.Zero);
+            if (DeferredDisplay == IntPtr.Zero)
+                throw new Exception("XOpenDisplay failed");
+                
             OrphanedWindow = XCreateSimpleWindow(Display, XDefaultRootWindow(Display), 0, 0, 1, 1, 0, IntPtr.Zero,
                 IntPtr.Zero);
-            if (Display == IntPtr.Zero)
-                throw new Exception("XOpenDisplay failed");
             XError.Init();
             
             Info = new X11Info(Display, DeferredDisplay, useXim);
@@ -100,6 +103,12 @@ namespace Avalonia.X11
 
         public IntPtr DeferredDisplay { get; set; }
         public IntPtr Display { get; set; }
+
+        public ITrayIconImpl CreateTrayIcon ()
+        {
+            return new X11TrayIconImpl();
+        }
+
         public IWindowImpl CreateWindow()
         {
             return new X11Window(this, null);

+ 367 - 0
src/Avalonia.X11/X11TrayIconImpl.cs

@@ -0,0 +1,367 @@
+#nullable enable
+
+using System;
+using System.Diagnostics;
+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
+{
+    internal class X11TrayIconImpl : ITrayIconImpl
+    {
+        private static int s_trayIconInstanceId;
+        private readonly ObjectPath _dbusMenuPath;
+        private StatusNotifierItemDbusObj? _statusNotifierItemDbusObj;
+        private readonly Connection? _connection;
+        private DbusPixmap _icon;
+
+        private IStatusNotifierWatcher? _statusNotifierWatcher;
+
+        private string? _sysTrayServiceName;
+        private string? _tooltipText;
+        private bool _isActive;
+        private bool _isDisposed;
+        private readonly bool _ctorFinished;
+
+        public INativeMenuExporter? MenuExporter { get; }
+        public Action? OnClicked { get; set; }
+
+        public X11TrayIconImpl()
+        {
+            _connection = DBusHelper.TryGetConnection();
+
+            if (_connection is null)
+            {
+                Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)
+                    ?.Log(this, "Unable to get a dbus connection for system tray icons.");
+                return;
+            }
+
+            _dbusMenuPath = DBusMenuExporter.GenerateDBusMenuObjPath;
+            MenuExporter = DBusMenuExporter.TryCreateDetachedNativeMenu(_dbusMenuPath, _connection);
+            CreateTrayIcon();
+            _ctorFinished = true;
+        }
+
+        public async void CreateTrayIcon()
+        {
+            if (_connection is null)
+                return;
+
+            try
+            {
+                _statusNotifierWatcher = _connection.CreateProxy<IStatusNotifierWatcher>(
+                    "org.kde.StatusNotifierWatcher",
+                    "/StatusNotifierWatcher");
+            }
+            catch
+            {
+                Logger.TryGet(LogEventLevel.Error, LogArea.X11Platform)
+                    ?.Log(this,
+                        "DBUS: org.kde.StatusNotifierWatcher service is not available on this system. System Tray Icons will not work without it.");
+            }
+
+            if (_statusNotifierWatcher is null)
+                return;
+
+            var pid = Process.GetCurrentProcess().Id;
+            var tid = s_trayIconInstanceId++;
+
+            _sysTrayServiceName = $"org.kde.StatusNotifierItem-{pid}-{tid}";
+            _statusNotifierItemDbusObj = new StatusNotifierItemDbusObj(_dbusMenuPath);
+
+            await _connection.RegisterObjectAsync(_statusNotifierItemDbusObj);
+
+            await _connection.RegisterServiceAsync(_sysTrayServiceName);
+
+            await _statusNotifierWatcher.RegisterStatusNotifierItemAsync(_sysTrayServiceName);
+
+            _statusNotifierItemDbusObj.SetTitleAndTooltip(_tooltipText);
+            _statusNotifierItemDbusObj.SetIcon(_icon);
+
+            _statusNotifierItemDbusObj.ActivationDelegate += OnClicked;
+
+            _isActive = true;
+        }
+
+        public async void DestroyTrayIcon()
+        {
+            if (_connection is null)
+                return;
+            _connection.UnregisterObject(_statusNotifierItemDbusObj);
+            await _connection.UnregisterServiceAsync(_sysTrayServiceName);
+            _isActive = false;
+        }
+
+        public void Dispose()
+        {
+            _isDisposed = true;
+            DestroyTrayIcon();
+            _connection?.Dispose();
+        }
+
+        public void SetIcon(IWindowIconImpl? icon)
+        {
+            if (_isDisposed)
+                return;
+            if (!(icon is X11IconData x11icon))
+                return;
+
+            var w = (int)x11icon.Data[0];
+            var h = (int)x11icon.Data[1];
+
+            var pixLength = w * h;
+            var pixByteArrayCounter = 0;
+            var pixByteArray = new byte[w * h * 4];
+
+            for (var i = 0; i < pixLength; i++)
+            {
+                var rawPixel = x11icon.Data[i + 2].ToUInt32();
+                pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF000000) >> 24);
+                pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF0000) >> 16);
+                pixByteArray[pixByteArrayCounter++] = (byte)((rawPixel & 0xFF00) >> 8);
+                pixByteArray[pixByteArrayCounter++] = (byte)(rawPixel & 0xFF);
+            }
+
+            _icon = new DbusPixmap(w, h, pixByteArray);
+            _statusNotifierItemDbusObj?.SetIcon(_icon);
+        }
+
+        public void SetIsVisible(bool visible)
+        {
+            if (_isDisposed || !_ctorFinished)
+                return;
+
+            if (visible & !_isActive)
+            {
+                DestroyTrayIcon();
+                CreateTrayIcon();
+            }
+            else if (!visible & _isActive)
+            {
+                DestroyTrayIcon();
+            }
+        }
+
+        public void SetToolTipText(string? text)
+        {
+            if (_isDisposed || text is null)
+                return;
+            _tooltipText = text;
+            _statusNotifierItemDbusObj?.SetTitleAndTooltip(_tooltipText);
+        }
+    }
+
+    /// <summary>
+    /// DBus Object used for setting system tray icons.
+    /// </summary>
+    /// <remarks>
+    /// Useful guide: https://web.archive.org/web/20210818173850/https://www.notmart.org/misc/statusnotifieritem/statusnotifieritem.html
+    /// </remarks>
+    internal class StatusNotifierItemDbusObj : IStatusNotifierItem
+    {
+        private readonly StatusNotifierItemProperties _backingProperties;
+        public event Action? OnTitleChanged;
+        public event Action? OnIconChanged;
+        public event Action? OnAttentionIconChanged;
+        public event Action? OnOverlayIconChanged;
+        public event Action? OnTooltipChanged;
+        public Action<string>? NewStatusAsync { get; set; }
+        public Action? ActivationDelegate { get; set; }
+        public ObjectPath ObjectPath { get; }
+
+        public StatusNotifierItemDbusObj(ObjectPath dbusmenuPath)
+        {
+            ObjectPath = new ObjectPath($"/StatusNotifierItem");
+
+            _backingProperties = new StatusNotifierItemProperties
+            {
+                Menu = dbusmenuPath, // Needs a dbus menu somehow
+                ToolTip = new ToolTip("")
+            };
+
+            InvalidateAll();
+        }
+
+        public Task ContextMenuAsync(int x, int y) => Task.CompletedTask;
+
+        public Task ActivateAsync(int x, int y)
+        {
+            ActivationDelegate?.Invoke();
+            return Task.CompletedTask;
+        }
+
+        public Task SecondaryActivateAsync(int x, int y) => Task.CompletedTask;
+
+        public Task ScrollAsync(int delta, string orientation) => Task.CompletedTask;
+
+        public void InvalidateAll()
+        {
+            OnTitleChanged?.Invoke();
+            OnIconChanged?.Invoke();
+            OnOverlayIconChanged?.Invoke();
+            OnAttentionIconChanged?.Invoke();
+            OnTooltipChanged?.Invoke();
+        }
+
+        public Task<IDisposable> WatchNewTitleAsync(Action handler, Action<Exception> onError)
+        {
+            OnTitleChanged += handler;
+            return Task.FromResult(Disposable.Create(() => OnTitleChanged -= handler));
+        }
+
+        public Task<IDisposable> WatchNewIconAsync(Action handler, Action<Exception> onError)
+        {
+            OnIconChanged += handler;
+            return Task.FromResult(Disposable.Create(() => OnIconChanged -= handler));
+        }
+
+        public Task<IDisposable> WatchNewAttentionIconAsync(Action handler, Action<Exception> onError)
+        {
+            OnAttentionIconChanged += handler;
+            return Task.FromResult(Disposable.Create(() => OnAttentionIconChanged -= handler));
+        }
+
+        public Task<IDisposable> WatchNewOverlayIconAsync(Action handler, Action<Exception> onError)
+        {
+            OnOverlayIconChanged += handler;
+            return Task.FromResult(Disposable.Create(() => OnOverlayIconChanged -= handler));
+        }
+
+        public Task<IDisposable> WatchNewToolTipAsync(Action handler, Action<Exception> onError)
+        {
+            OnTooltipChanged += handler;
+            return Task.FromResult(Disposable.Create(() => OnTooltipChanged -= handler));
+        }
+
+        public Task<IDisposable> WatchNewStatusAsync(Action<string> handler, Action<Exception> onError)
+        {
+            NewStatusAsync += handler;
+            return Task.FromResult(Disposable.Create(() => NewStatusAsync -= handler));
+        }
+
+        public Task<object> GetAsync(string prop) => Task.FromResult(new object());
+
+        public Task<StatusNotifierItemProperties> GetAllAsync() => Task.FromResult(_backingProperties);
+
+        public Task SetAsync(string prop, object val) => Task.CompletedTask;
+
+        public Task<IDisposable> WatchPropertiesAsync(Action<PropertyChanges> handler) =>
+            Task.FromResult(Disposable.Empty);
+
+        public void SetIcon(DbusPixmap dbusPixmap)
+        {
+            _backingProperties.IconPixmap = new[] { dbusPixmap };
+            InvalidateAll();
+        }
+
+        public void SetTitleAndTooltip(string? text)
+        {
+            if (text is null)
+                return;
+
+            _backingProperties.Id = text;
+            _backingProperties.Category = "ApplicationStatus";
+            _backingProperties.Status = text;
+            _backingProperties.Title = text;
+            _backingProperties.ToolTip = new ToolTip(text);
+
+            InvalidateAll();
+        }
+    }
+
+    [DBusInterface("org.kde.StatusNotifierWatcher")]
+    internal interface IStatusNotifierWatcher : IDBusObject
+    {
+        Task RegisterStatusNotifierItemAsync(string Service);
+        Task RegisterStatusNotifierHostAsync(string Service);
+    }
+
+    [DBusInterface("org.kde.StatusNotifierItem")]
+    internal interface IStatusNotifierItem : IDBusObject
+    {
+        Task ContextMenuAsync(int x, int y);
+        Task ActivateAsync(int x, int y);
+        Task SecondaryActivateAsync(int x, int y);
+        Task ScrollAsync(int delta, string orientation);
+        Task<IDisposable> WatchNewTitleAsync(Action handler, Action<Exception> onError);
+        Task<IDisposable> WatchNewIconAsync(Action handler, Action<Exception> onError);
+        Task<IDisposable> WatchNewAttentionIconAsync(Action handler, Action<Exception> onError);
+        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<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.
+    internal class StatusNotifierItemProperties
+    {
+        public string? Category;
+
+        public string? Id;
+
+        public string? Title;
+
+        public string? Status;
+
+        public ObjectPath Menu;
+
+        public DbusPixmap[]? IconPixmap;
+
+        public ToolTip ToolTip;
+    }
+
+    internal struct ToolTip
+    {
+        public readonly string First;
+        public readonly DbusPixmap[] Second;
+        public readonly string Third;
+        public readonly string Fourth;
+
+        private static readonly DbusPixmap[] s_blank =
+        {
+            new DbusPixmap(0, 0, Array.Empty<byte>()), new DbusPixmap(0, 0, Array.Empty<byte>())
+        };
+
+        public ToolTip(string message) : this("", s_blank, message, "")
+        {
+        }
+
+        public ToolTip(string first, DbusPixmap[] second, string third, string fourth)
+        {
+            First = first;
+            Second = second;
+            Third = third;
+            Fourth = fourth;
+        }
+    }
+
+    internal readonly struct DbusPixmap
+    {
+        public readonly int Width;
+        public readonly int Height;
+        public readonly byte[] Data;
+
+        public DbusPixmap(int width, int height, byte[] data)
+        {
+            Width = width;
+            Height = height;
+            Data = data;
+        }
+    }
+}

+ 1 - 1
src/Avalonia.X11/X11Window.cs

@@ -191,7 +191,7 @@ namespace Avalonia.X11
             if(_popup)
                 PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(popupParent, MoveResize));
             if (platform.Options.UseDBusMenu)
-                NativeMenuExporter = DBusMenuExporter.TryCreate(_handle);
+                NativeMenuExporter = DBusMenuExporter.TryCreateTopLevelNativeMenu(_handle);
             NativeControlHost = new X11NativeControlHost(_platform, this);
             InitializeIme();
         }

+ 0 - 3
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs

@@ -14,7 +14,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
 {
     class AvaloniaXamlIlCompiler : XamlILCompiler
     {
-        private readonly TransformerConfiguration _configuration;
         private readonly IXamlType _contextType;
         private readonly AvaloniaXamlIlDesignPropertiesTransformer _designTransformer;
         private readonly AvaloniaBindingExtensionTransformer _bindingTransformer;
@@ -22,8 +21,6 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
         private AvaloniaXamlIlCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings<IXamlILEmitter, XamlILNodeEmitResult> emitMappings)
             : base(configuration, emitMappings, true)
         {
-            _configuration = configuration;
-
             void InsertAfter<T>(params IXamlAstTransformer[] t) 
                 => Transformers.InsertRange(Transformers.FindIndex(x => x is T) + 1, t);
 

+ 6 - 1
src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs

@@ -49,8 +49,13 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
                 XmlNamespaceInfoProvider =
                     typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.IAvaloniaXamlIlXmlNamespaceInfoProvider"),
                 DeferredContentPropertyAttributes = {typeSystem.GetType("Avalonia.Metadata.TemplateContentAttribute")},
+                DeferredContentExecutorCustomizationDefaultTypeParameter = typeSystem.GetType("Avalonia.Controls.IControl"),
+                DeferredContentExecutorCustomizationTypeParameterDeferredContentAttributePropertyNames = new List<string>
+                {
+                    "TemplateResultType"
+                },
                 DeferredContentExecutorCustomization =
-                    runtimeHelpers.FindMethod(m => m.Name == "DeferredTransformationFactoryV1"),
+                    runtimeHelpers.FindMethod(m => m.Name == "DeferredTransformationFactoryV2"),
                 UsableDuringInitializationAttributes =
                 {
                     typeSystem.GetType("Avalonia.Metadata.UsableDuringInitializationAttribute"),

+ 1 - 1
src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github

@@ -1 +1 @@
-Subproject commit f4ac681b91a9dc7a7a095d1050a683de23d86b72
+Subproject commit 8e20d65eb5f1efbae08e49b18f39bfdce32df7b3

+ 12 - 0
src/Markup/Avalonia.Markup.Xaml/Templates/TemplateContent.cs

@@ -7,6 +7,7 @@ namespace Avalonia.Markup.Xaml.Templates
     public static class TemplateContent
     {
         public static ControlTemplateResult Load(object templateContent)
+
         {
             if (templateContent is Func<IServiceProvider, object> direct)
             {
@@ -20,5 +21,16 @@ namespace Avalonia.Markup.Xaml.Templates
 
             throw new ArgumentException(nameof(templateContent));
         }
+
+        public static TemplateResult<T> Load<T>(object templateContent)
+        {
+            if (templateContent is Func<IServiceProvider, object> direct)
+                return (TemplateResult<T>)direct(null);
+
+            if (templateContent is null)
+                return null;
+
+            throw new ArgumentException(nameof(templateContent));
+        }
     }
 }

+ 11 - 1
src/Markup/Avalonia.Markup.Xaml/XamlIl/Runtime/XamlIlRuntimeHelpers.cs

@@ -15,6 +15,12 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
     {
         public static Func<IServiceProvider, object> DeferredTransformationFactoryV1(Func<IServiceProvider, object> builder,
             IServiceProvider provider)
+        {
+            return DeferredTransformationFactoryV2<IControl>(builder, provider);
+        }
+        
+        public static Func<IServiceProvider, object> DeferredTransformationFactoryV2<T>(Func<IServiceProvider, object> builder,
+            IServiceProvider provider)
         {
             var resourceNodes = provider.GetService<IAvaloniaXamlIlParentStackProvider>().Parents
                 .OfType<IResourceNode>().ToList();
@@ -25,7 +31,11 @@ namespace Avalonia.Markup.Xaml.XamlIl.Runtime
                 var scope = parentScope != null ? new ChildNameScope(parentScope) : (INameScope)new NameScope();
                 var obj = builder(new DeferredParentServiceProvider(sp, resourceNodes, rootObject, scope));
                 scope.Complete();
-                return new ControlTemplateResult((IControl)obj, scope);
+                
+                if(typeof(T) == typeof(IControl))
+                    return new ControlTemplateResult((IControl)obj, scope);
+
+                return new TemplateResult<T>((T)obj, scope);
             };
         }
 

+ 5 - 1
src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs

@@ -184,6 +184,9 @@ namespace Avalonia.Markup.Parsers
             
         }
 
+        // Don't need to override GetHashCode as the ISyntax objects will not be stored in a hash; the 
+        // only reason they have overridden Equals methods is for unit testing.
+#pragma warning disable CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode()
         public class PropertySyntax : ISyntax
         {
             public string Name { get; set; } = string.Empty;
@@ -205,7 +208,7 @@ namespace Avalonia.Markup.Parsers
                    && other.TypeName == TypeName
                    && other.TypeNamespace == TypeNamespace;
         }
-        
+
         public class ChildTraversalSyntax : ISyntax
         {
             public static ChildTraversalSyntax Instance { get;  } = new ChildTraversalSyntax();
@@ -231,5 +234,6 @@ namespace Avalonia.Markup.Parsers
                    && other.TypeName == TypeName
                    && other.TypeNamespace == TypeNamespace;
         }
+#pragma warning restore CS0659 // Type overrides Object.Equals(object o) but does not override Object.GetHashCode()
     }
 }

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

@@ -591,7 +591,7 @@ namespace Avalonia.Skia
         /// Configure paint wrapper for using gradient brush.
         /// </summary>
         /// <param name="paintWrapper">Paint wrapper.</param>
-        /// <param name="targetRect">Target bound rect.</param>
+        /// <param name="targetSize">Target size.</param>
         /// <param name="gradientBrush">Gradient brush.</param>
         private void ConfigureGradientBrush(ref PaintWrapper paintWrapper, Size targetSize, IGradientBrush gradientBrush)
         {

+ 5 - 1
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@@ -140,7 +140,11 @@ namespace Avalonia.Skia
                     $"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
             }
 
-            return new GlyphTypefaceImpl(skTypeface);
+            var isFakeBold = (int)typeface.Weight >= 600 && !skTypeface.IsBold;
+
+            var isFakeItalic = typeface.Style == FontStyle.Italic && !skTypeface.IsItalic;
+            
+            return new GlyphTypefaceImpl(skTypeface, isFakeBold, isFakeItalic);
         }
     }
 }

+ 9 - 1
src/Skia/Avalonia.Skia/GlyphTypefaceImpl.cs

@@ -10,7 +10,7 @@ namespace Avalonia.Skia
     {
         private bool _isDisposed;
 
-        public GlyphTypefaceImpl(SKTypeface typeface)
+        public GlyphTypefaceImpl(SKTypeface typeface, bool isFakeBold = false, bool isFakeItalic = false)
         {
             Typeface = typeface ?? throw new ArgumentNullException(nameof(typeface));
 
@@ -52,6 +52,10 @@ namespace Avalonia.Skia
                 0;
 
             IsFixedPitch = Typeface.IsFixedPitch;
+
+            IsFakeBold = isFakeBold;
+
+            IsFakeItalic = isFakeItalic;
         }
 
         public Face Face { get; }
@@ -86,6 +90,10 @@ namespace Avalonia.Skia
 
         /// <inheritdoc cref="IGlyphTypefaceImpl"/>
         public bool IsFixedPitch { get; }
+        
+        public bool IsFakeBold { get; }
+        
+        public bool IsFakeItalic { get; }
 
         /// <inheritdoc cref="IGlyphTypefaceImpl"/>
         public ushort GetGlyph(uint codepoint)

+ 2 - 0
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -217,6 +217,8 @@ namespace Avalonia.Skia
 
             s_font.Size = (float)glyphRun.FontRenderingEmSize;
             s_font.Typeface = typeface;
+            s_font.Embolden = glyphTypeface.IsFakeBold;
+            s_font.SkewX = glyphTypeface.IsFakeItalic ? -0.2f : 0;
 
             SKTextBlob textBlob;
 

+ 63 - 0
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@@ -1110,6 +1110,9 @@ namespace Avalonia.Win32.Interop
         [DllImport("user32.dll", SetLastError = true)]
         public static extern IntPtr SetActiveWindow(IntPtr hWnd);
 
+        [DllImport("user32.dll", SetLastError = true)]
+        public static extern bool SetForegroundWindow(IntPtr hWnd);
+
         [DllImport("user32.dll")]
         public static extern IntPtr SetCapture(IntPtr hWnd);
 
@@ -1197,6 +1200,9 @@ namespace Avalonia.Win32.Interop
             GCW_ATOM = -32
         }
 
+        [DllImport("shell32", CharSet = CharSet.Auto)]
+        public static extern int Shell_NotifyIcon(NIM dwMessage, NOTIFYICONDATA lpData);
+
         [DllImport("user32.dll", EntryPoint = "SetClassLongPtr")]
         private static extern IntPtr SetClassLong64(IntPtr hWnd, ClassLongIndex nIndex, IntPtr dwNewLong);
 
@@ -2296,4 +2302,61 @@ namespace Avalonia.Win32.Interop
         public uint VisibleMask;
         public uint DamageMask;
     }
+
+    internal enum NIM : uint
+    {
+        ADD = 0x00000000,
+        MODIFY = 0x00000001,
+        DELETE = 0x00000002,
+        SETFOCUS = 0x00000003,
+        SETVERSION = 0x00000004
+    }
+
+    [Flags]
+    internal enum NIF : uint
+    {
+        MESSAGE = 0x00000001,
+        ICON = 0x00000002,
+        TIP = 0x00000004,
+        STATE = 0x00000008,
+        INFO = 0x00000010,
+        GUID = 0x00000020,
+        REALTIME = 0x00000040,
+        SHOWTIP = 0x00000080
+    }
+
+    [Flags]
+    internal enum NIIF : uint
+    {
+        NONE = 0x00000000,
+        INFO = 0x00000001,
+        WARNING = 0x00000002,
+        ERROR = 0x00000003,
+        USER = 0x00000004,
+        ICON_MASK = 0x0000000F,
+        NOSOUND = 0x00000010,
+        LARGE_ICON = 0x00000020,
+        RESPECT_QUIET_TIME = 0x00000080
+    }
+
+    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
+    internal class NOTIFYICONDATA
+    {
+        public int cbSize = Marshal.SizeOf<NOTIFYICONDATA>();
+        public IntPtr hWnd;
+        public int uID;
+        public NIF uFlags;
+        public int uCallbackMessage;
+        public IntPtr hIcon;
+        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
+        public string szTip;
+        public int dwState = 0;
+        public int dwStateMask = 0;
+        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 256)]
+        public string szInfo;
+        public int uTimeoutOrVersion;
+        [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
+        public string szInfoTitle;
+        public NIIF dwInfoFlags;
+    }
 }

+ 265 - 0
src/Windows/Avalonia.Win32/TrayIconImpl.cs

@@ -0,0 +1,265 @@
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using Avalonia.Controls;
+using Avalonia.Controls.Platform;
+using Avalonia.Controls.Primitives.PopupPositioning;
+using Avalonia.LogicalTree;
+using Avalonia.Platform;
+using Avalonia.Styling;
+using Avalonia.Win32.Interop;
+using static Avalonia.Win32.Interop.UnmanagedMethods;
+
+#nullable enable
+
+namespace Avalonia.Win32
+{
+    public class TrayIconImpl : ITrayIconImpl
+    {
+        private readonly int _uniqueId;
+        private static int s_nextUniqueId;
+        private bool _iconAdded;
+        private IconImpl? _icon;
+        private string? _tooltipText;
+        private readonly Win32NativeToManagedMenuExporter _exporter;
+        private static readonly Dictionary<int, TrayIconImpl> s_trayIcons = new Dictionary<int, TrayIconImpl>();
+        private bool _disposedValue;
+
+        public TrayIconImpl()
+        {
+            _exporter = new Win32NativeToManagedMenuExporter();
+
+            _uniqueId = ++s_nextUniqueId;
+
+            s_trayIcons.Add(_uniqueId, this);
+        }
+
+        public Action? OnClicked { get; set; }
+
+        public INativeMenuExporter MenuExporter => _exporter;
+
+        internal static void ProcWnd(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
+        {
+            if (msg == (int)CustomWindowsMessage.WM_TRAYMOUSE && s_trayIcons.ContainsKey(wParam.ToInt32()))
+            {
+                s_trayIcons[wParam.ToInt32()].WndProc(hWnd, msg, wParam, lParam);
+            }
+        }
+
+        public void SetIcon(IWindowIconImpl? icon)
+        {
+            _icon = icon as IconImpl;
+            UpdateIcon();
+        }
+
+        public void SetIsVisible(bool visible)
+        {
+            UpdateIcon(!visible);
+        }
+
+        public void SetToolTipText(string? text)
+        {
+            _tooltipText = text;
+            UpdateIcon(!_iconAdded);
+        }
+
+        private void UpdateIcon(bool remove = false)
+        {
+            var iconData = new NOTIFYICONDATA()
+            {
+                hWnd = Win32Platform.Instance.Handle,
+                uID = _uniqueId,
+                uFlags = NIF.TIP | NIF.MESSAGE,
+                uCallbackMessage = (int)CustomWindowsMessage.WM_TRAYMOUSE,
+                hIcon = _icon?.HIcon ?? new IconImpl(new System.Drawing.Bitmap(32, 32)).HIcon,
+                szTip = _tooltipText ?? ""
+            };
+
+            if (!remove)
+            {
+                iconData.uFlags |= NIF.ICON;
+
+                if (!_iconAdded)
+                {
+                    Shell_NotifyIcon(NIM.ADD, iconData);
+                    _iconAdded = true;
+                }
+                else
+                {
+                    Shell_NotifyIcon(NIM.MODIFY, iconData);
+                }
+            }
+            else
+            {
+                Shell_NotifyIcon(NIM.DELETE, iconData);
+                _iconAdded = false;
+            }
+        }
+
+        private IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam)
+        {
+            if (msg == (uint)CustomWindowsMessage.WM_TRAYMOUSE)
+            {
+                // Determine the type of message and call the matching event handlers
+                switch (lParam.ToInt32())
+                {
+                    case (int)WindowsMessage.WM_LBUTTONUP:
+                        OnClicked?.Invoke();
+                        break;
+
+                    case (int)WindowsMessage.WM_RBUTTONUP:
+                        OnRightClicked();
+                        break;
+                }
+
+                return IntPtr.Zero;
+            }
+
+            return DefWindowProc(hWnd, msg, wParam, lParam);
+        }
+
+        private void OnRightClicked()
+        {
+            var _trayMenu = new TrayPopupRoot()
+            {
+                SystemDecorations = SystemDecorations.None,
+                SizeToContent = SizeToContent.WidthAndHeight,
+                Background = null,
+                TransparencyLevelHint = WindowTransparencyLevel.Transparent,
+                Content = new TrayIconMenuFlyoutPresenter()
+                {
+                    Items = _exporter.GetMenu()
+                }
+            };
+
+            GetCursorPos(out POINT pt);
+
+            _trayMenu.Position = new PixelPoint(pt.X, pt.Y);
+
+            _trayMenu.Show();
+        }
+
+        /// <summary>
+        /// Custom Win32 window messages for the NotifyIcon
+        /// </summary>
+        private enum CustomWindowsMessage : uint
+        {
+            WM_TRAYICON = WindowsMessage.WM_APP + 1024,
+            WM_TRAYMOUSE = WindowsMessage.WM_USER + 1024
+        }
+
+        private class TrayIconMenuFlyoutPresenter : MenuFlyoutPresenter, IStyleable
+        {
+            Type IStyleable.StyleKey => typeof(MenuFlyoutPresenter);
+
+            public override void Close()
+            {
+                // DefaultMenuInteractionHandler calls this
+                var host = this.FindLogicalAncestorOfType<TrayPopupRoot>();
+                if (host != null)
+                {
+                    SelectedIndex = -1;
+                    host.Close();
+                }
+            }
+        }
+
+        private class TrayPopupRoot : Window
+        {
+            private readonly ManagedPopupPositioner _positioner;
+
+            public TrayPopupRoot()
+            {
+                _positioner = new ManagedPopupPositioner(new TrayIconManagedPopupPositionerPopupImplHelper(MoveResize));
+                Topmost = true;
+
+                Deactivated += TrayPopupRoot_Deactivated;
+
+                ShowInTaskbar = false;
+
+                ShowActivated = true;
+            }
+
+            private void TrayPopupRoot_Deactivated(object sender, EventArgs e)
+            {
+                Close();
+            }
+
+            private void MoveResize(PixelPoint position, Size size, double scaling)
+            {
+                PlatformImpl!.Move(position);
+                PlatformImpl!.Resize(size, PlatformResizeReason.Layout);
+            }
+
+            protected override void ArrangeCore(Rect finalRect)
+            {
+                base.ArrangeCore(finalRect);
+
+                _positioner.Update(new PopupPositionerParameters
+                {
+                    Anchor = PopupAnchor.TopLeft,
+                    Gravity = PopupGravity.BottomRight,
+                    AnchorRectangle = new Rect(Position.ToPoint(1) / Screens.Primary.PixelDensity, new Size(1, 1)),
+                    Size = finalRect.Size,
+                    ConstraintAdjustment = PopupPositionerConstraintAdjustment.FlipX | PopupPositionerConstraintAdjustment.FlipY,
+                });
+            }
+
+            private class TrayIconManagedPopupPositionerPopupImplHelper : IManagedPopupPositionerPopup
+            {
+                private readonly Action<PixelPoint, Size, double> _moveResize;
+                private readonly Window _hiddenWindow;
+
+                public TrayIconManagedPopupPositionerPopupImplHelper(Action<PixelPoint, Size, double> moveResize)
+                {
+                    _moveResize = moveResize;
+                    _hiddenWindow = new Window();
+                }
+
+                public IReadOnlyList<ManagedPopupPositionerScreenInfo> Screens =>
+                _hiddenWindow.Screens.All.Select(s => new ManagedPopupPositionerScreenInfo(
+                    s.Bounds.ToRect(1), s.Bounds.ToRect(1))).ToList();
+
+                public Rect ParentClientAreaScreenGeometry
+                {
+                    get
+                    {
+                        var point = _hiddenWindow.Screens.Primary.Bounds.TopLeft;
+                        var size = _hiddenWindow.Screens.Primary.Bounds.Size;
+                        return new Rect(point.X, point.Y, size.Width * _hiddenWindow.Screens.Primary.PixelDensity, size.Height * _hiddenWindow.Screens.Primary.PixelDensity);
+                    }
+                }
+
+                public void MoveAndResize(Point devicePoint, Size virtualSize)
+                {
+                    _moveResize(new PixelPoint((int)devicePoint.X, (int)devicePoint.Y), virtualSize, _hiddenWindow.Screens.Primary.PixelDensity);
+                }
+
+                public double Scaling => _hiddenWindow.Screens.Primary.PixelDensity;
+            }
+        }
+
+        protected virtual void Dispose(bool disposing)
+        {
+            if (!_disposedValue)
+            {
+                UpdateIcon(true);
+
+                _disposedValue = true;
+            }
+        }
+
+        ~TrayIconImpl()
+        {
+            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+            Dispose(disposing: false);
+        }
+
+        public void Dispose()
+        {
+            // Do not change this code. Put cleanup code in 'Dispose(bool disposing)' method
+            Dispose(disposing: true);
+            GC.SuppressFinalize(this);
+        }
+    }
+}

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

@@ -27,7 +27,7 @@ namespace Avalonia.Win32
                     if (egl != null &&
                         opts?.UseWindowsUIComposition == true)
                     {
-                        WinUICompositorConnection.TryCreateAndRegister(egl);
+                        WinUICompositorConnection.TryCreateAndRegister(egl, opts.CompositionBackdropCornerRadius);
                     }
 
                     return egl;

+ 54 - 0
src/Windows/Avalonia.Win32/Win32NativeToManagedMenuExporter.cs

@@ -0,0 +1,54 @@
+using System.Collections.Generic;
+using Avalonia.Controls;
+using Avalonia.Controls.Platform;
+
+#nullable enable
+
+namespace Avalonia.Win32
+{
+    internal class Win32NativeToManagedMenuExporter : INativeMenuExporter
+    {
+        private NativeMenu? _nativeMenu;
+
+        public void SetNativeMenu(NativeMenu? nativeMenu)
+        {
+            _nativeMenu = nativeMenu;
+        }
+
+        private IEnumerable<MenuItem> Populate(NativeMenu nativeMenu)
+        {
+            foreach (var menuItem in nativeMenu.Items)
+            {
+                if (menuItem is NativeMenuItemSeparator)
+                {
+                    yield return new MenuItem { Header = "-" };
+                }
+                else if (menuItem is NativeMenuItem item)
+                {
+                    var newItem = new MenuItem { Header = item.Header, Icon = item.Icon, Command = item.Command, CommandParameter = item.CommandParameter };
+
+                    if (item.Menu != null)
+                    {
+                        newItem.Items = Populate(item.Menu);
+                    }
+                    else if (item.HasClickHandlers && item is INativeMenuItemExporterEventsImplBridge bridge)
+                    {
+                        newItem.Click += (_, __) => bridge.RaiseClicked();
+                    }
+
+                    yield return newItem;
+                }
+            }
+        }
+
+        public IEnumerable<MenuItem>? GetMenu()
+        {
+            if (_nativeMenu != null)
+            {
+                return Populate(_nativeMenu);
+            }
+
+            return null;
+        }
+    }
+}

+ 18 - 0
src/Windows/Avalonia.Win32/Win32Platform.cs

@@ -89,6 +89,13 @@ namespace Avalonia
         /// This is recommended if you need to use AcrylicBlur or acrylic in your applications.
         /// </remarks>
         public bool UseWindowsUIComposition { get; set; } = true;
+
+        /// <summary>
+        /// When <see cref="UseWindowsUIComposition"/> enabled, create rounded corner blur brushes
+        /// If set to null the brushes will be created using default settings (sharp corners)
+        /// This can be useful when you need a rounded-corner blurred Windows 10 app, or borderless Windows 11 app
+        /// </summary>
+        public float? CompositionBackdropCornerRadius { get; set; }
     }
 }
 
@@ -108,6 +115,10 @@ namespace Avalonia.Win32
             CreateMessageWindow();
         }
 
+        internal static Win32Platform Instance => s_instance;
+
+        internal IntPtr Handle => _hwnd;
+
         /// <summary>
         /// Gets the actual WindowsVersion. Same as the info returned from RtlGetVersion.
         /// </summary>
@@ -261,6 +272,8 @@ namespace Avalonia.Win32
                     }
                 }
             }
+            
+            TrayIconImpl.ProcWnd(hWnd, msg, wParam, lParam);
 
             return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam);
         }
@@ -293,6 +306,11 @@ namespace Avalonia.Win32
             }
         }
 
+        public ITrayIconImpl CreateTrayIcon ()
+        {
+            return new TrayIconImpl();
+        }
+
         public IWindowImpl CreateWindow()
         {
             return new WindowImpl();

+ 12 - 3
src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositedWindow.cs

@@ -13,6 +13,8 @@ namespace Avalonia.Win32.WinRT.Composition
     {
         private EglContext _syncContext;
         private readonly object _pumpLock;
+        private readonly IVisual _micaVisual;
+        private readonly ICompositionRoundedRectangleGeometry _roundedRectangleGeometry;
         private readonly IVisual _blurVisual;
         private ICompositionTarget _compositionTarget;
         private IVisual _contentVisual;
@@ -28,11 +30,14 @@ namespace Avalonia.Win32.WinRT.Composition
             object pumpLock,
             ICompositionTarget compositionTarget,
             ICompositionDrawingSurfaceInterop surfaceInterop,
-            IVisual contentVisual, IVisual blurVisual)
+            IVisual contentVisual, IVisual blurVisual, IVisual micaVisual,
+            ICompositionRoundedRectangleGeometry roundedRectangleGeometry)
         {
             _compositor = compositor.CloneReference();
             _syncContext = syncContext;
             _pumpLock = pumpLock;
+            _micaVisual = micaVisual;
+            _roundedRectangleGeometry = roundedRectangleGeometry;
             _blurVisual = blurVisual.CloneReference();
             _compositionTarget = compositionTarget.CloneReference();
             _contentVisual = contentVisual.CloneReference();
@@ -48,6 +53,7 @@ namespace Avalonia.Win32.WinRT.Composition
                 {
                     _surfaceInterop.Resize(new UnmanagedMethods.POINT { X = size.Width, Y = size.Height });
                     _contentVisual.SetSize(new Vector2(size.Width, size.Height));
+                    _roundedRectangleGeometry?.SetSize(new Vector2(size.Width, size.Height));
                     _size = size;
                 }
             }
@@ -72,10 +78,13 @@ namespace Avalonia.Win32.WinRT.Composition
             _surfaceInterop.EndDraw();
         }
 
-        public void SetBlur(bool enable)
+        public void SetBlur(BlurEffect blurEffect)
         {
             using (_syncContext.EnsureLocked())
-                _blurVisual.SetIsVisible(enable ? 1 : 0);
+            {
+                _blurVisual.SetIsVisible(blurEffect == BlurEffect.Acrylic ? 1 : 0);
+                _micaVisual?.SetIsVisible(blurEffect == BlurEffect.Mica ? 1 : 0);
+            }
         }
 
         public IDisposable BeginTransaction()

+ 62 - 14
src/Windows/Avalonia.Win32/WinRT/Composition/WinUICompositorConnection.cs

@@ -1,4 +1,5 @@
 using System;
+using System.Collections.Generic;
 using System.Diagnostics;
 using System.Numerics;
 using System.Runtime.InteropServices;
@@ -16,7 +17,9 @@ namespace Avalonia.Win32.WinRT.Composition
 {
     class WinUICompositorConnection : IRenderTimer
     {
+        private readonly float? _backdropCornerRadius;
         private readonly EglContext _syncContext;
+        private readonly ICompositionBrush _micaBrush;
         private ICompositor _compositor;
         private ICompositor2 _compositor2;
         private ICompositor5 _compositor5;
@@ -28,10 +31,11 @@ namespace Avalonia.Win32.WinRT.Composition
         private ICompositionBrush _blurBrush;
         private object _pumpLock = new object();
 
-        public WinUICompositorConnection(EglPlatformOpenGlInterface gl, object pumpLock)
+        public WinUICompositorConnection(EglPlatformOpenGlInterface gl, object pumpLock, float? backdropCornerRadius)
         {
             _gl = gl;
             _pumpLock = pumpLock;
+            _backdropCornerRadius = backdropCornerRadius;
             _syncContext = _gl.PrimaryEglContext;
             _angle = (AngleWin32EglDisplay)_gl.Display;
             _compositor = NativeWinRTMethods.CreateInstance<ICompositor>("Windows.UI.Composition.Compositor");
@@ -42,13 +46,13 @@ namespace Avalonia.Win32.WinRT.Composition
             using var device = MicroComRuntime.CreateProxyFor<IUnknown>(_angle.GetDirect3DDevice(), true);
             
             _device = _compositorInterop.CreateGraphicsDevice(device);
-            _blurBrush = CreateBlurBrush();
-            
+            _blurBrush = CreateAcrylicBlurBackdropBrush();
+            _micaBrush = CreateMicaBackdropBrush();
         }
 
         public EglPlatformOpenGlInterface Egl => _gl;
 
-        static bool TryCreateAndRegisterCore(EglPlatformOpenGlInterface angle)
+        static bool TryCreateAndRegisterCore(EglPlatformOpenGlInterface angle, float? backdropCornerRadius)
         {
             var tcs = new TaskCompletionSource<bool>();
             var pumpLock = new object();
@@ -63,7 +67,7 @@ namespace Avalonia.Win32.WinRT.Composition
                         dwSize = Marshal.SizeOf<NativeWinRTMethods.DispatcherQueueOptions>(),
                         threadType = NativeWinRTMethods.DISPATCHERQUEUE_THREAD_TYPE.DQTYPE_THREAD_CURRENT
                     });
-                    connect = new WinUICompositorConnection(angle, pumpLock);
+                    connect = new WinUICompositorConnection(angle, pumpLock, backdropCornerRadius);
                     AvaloniaLocator.CurrentMutable.BindToSelf(connect);
                     AvaloniaLocator.CurrentMutable.Bind<IRenderTimer>().ToConstant(connect);
                     tcs.SetResult(true);
@@ -130,7 +134,8 @@ namespace Avalonia.Win32.WinRT.Composition
             }
         }
 
-        public static void TryCreateAndRegister(EglPlatformOpenGlInterface angle)
+        public static void TryCreateAndRegister(EglPlatformOpenGlInterface angle,
+            float? backdropCornerRadius)
         {
             const int majorRequired = 10;
             const int buildRequired = 17134;
@@ -143,7 +148,7 @@ namespace Avalonia.Win32.WinRT.Composition
             {
                 try
                 {
-                    TryCreateAndRegisterCore(angle);
+                    TryCreateAndRegisterCore(angle, backdropCornerRadius);
                     return;
                 }
                 catch (Exception e)
@@ -188,16 +193,37 @@ namespace Avalonia.Win32.WinRT.Composition
             
             target.SetRoot(containerVisual);
 
-            using var blur = CreateBlurVisual(); 
+            using var blur = CreateBlurVisual(_blurBrush);
+            IVisual mica = null;
+            if (_micaBrush != null)
+            {
+                mica = CreateBlurVisual(_micaBrush);
+                containerChildren.InsertAtTop(mica);
+            }
+
+            var compositionRoundedRectangleGeometry = ClipVisual(blur, mica);
             
             containerChildren.InsertAtTop(blur);
             containerChildren.InsertAtTop(visual);
             
-            return new WinUICompositedWindow(_syncContext, _compositor, _pumpLock, target, surfaceInterop, visual, blur);
+            return new WinUICompositedWindow(_syncContext, _compositor, _pumpLock, target, surfaceInterop, visual,
+                blur, mica, compositionRoundedRectangleGeometry);
         }
 
+        private ICompositionBrush CreateMicaBackdropBrush()
+        {
+            if (Win32Platform.WindowsVersion.Build < 22000)
+                return null;
+
+            using var compositorWithBlurredWallpaperBackdropBrush =
+                _compositor.QueryInterface<ICompositorWithBlurredWallpaperBackdropBrush>();
+            using var blurredWallpaperBackdropBrush =
+                compositorWithBlurredWallpaperBackdropBrush?.TryCreateBlurredWallpaperBackdropBrush();
+            using var micaBackdropBrush = blurredWallpaperBackdropBrush?.QueryInterface<ICompositionBrush>();
+            return micaBackdropBrush.CloneReference();
+        }
 
-        private unsafe ICompositionBrush CreateBlurBrush()
+        private unsafe ICompositionBrush CreateAcrylicBlurBackdropBrush()
         {
             using var backDropParameterFactory = NativeWinRTMethods.CreateActivationFactory<ICompositionEffectSourceParameterFactory>(
                 "Windows.UI.Composition.CompositionEffectSourceParameter");
@@ -207,6 +233,7 @@ namespace Avalonia.Win32.WinRT.Composition
             using var backDropParameterAsSource = backDropParameter.QueryInterface<IGraphicsEffectSource>();
             var blurEffect = new WinUIGaussianBlurEffect(backDropParameterAsSource);
             using var blurEffectFactory = _compositor.CreateEffectFactory(blurEffect);
+            using var compositionEffectBrush = blurEffectFactory.CreateBrush();
             using var backdrop = _compositor2.CreateBackdropBrush();
             using var backdropBrush = backdrop.QueryInterface<ICompositionBrush>();
             
@@ -214,18 +241,39 @@ namespace Avalonia.Win32.WinRT.Composition
             var saturateEffect = new SaturationEffect(blurEffect);
             using var satEffectFactory = _compositor.CreateEffectFactory(saturateEffect);
             using var sat = satEffectFactory.CreateBrush();
-            sat.SetSourceParameter(backdropString.Handle, backdropBrush);
-            return sat.QueryInterface<ICompositionBrush>();
+            compositionEffectBrush.SetSourceParameter(backdropString.Handle, backdropBrush);
+            return compositionEffectBrush.QueryInterface<ICompositionBrush>();
+        }
+
+        private ICompositionRoundedRectangleGeometry ClipVisual(params IVisual[] containerVisuals)
+        {
+            if (!_backdropCornerRadius.HasValue)
+                return null;
+            using var roundedRectangleGeometry = _compositor5.CreateRoundedRectangleGeometry();
+            roundedRectangleGeometry.SetCornerRadius(new Vector2(_backdropCornerRadius.Value, _backdropCornerRadius.Value));
+
+            using var compositor6 = _compositor.QueryInterface<ICompositor6>();
+            using var compositionGeometry = roundedRectangleGeometry
+                .QueryInterface<ICompositionGeometry>();
+
+            using var geometricClipWithGeometry =
+                compositor6.CreateGeometricClipWithGeometry(compositionGeometry);
+            foreach (var visual in containerVisuals)
+            {
+                visual?.SetClip(geometricClipWithGeometry.QueryInterface<ICompositionClip>());
         }
         
-        private unsafe IVisual CreateBlurVisual()
+            return roundedRectangleGeometry.CloneReference();
+        }
+
+        private unsafe IVisual CreateBlurVisual(ICompositionBrush compositionBrush)
         {
             using var spriteVisual = _compositor.CreateSpriteVisual();
             using var visual = spriteVisual.QueryInterface<IVisual>();
             using var visual2 = spriteVisual.QueryInterface<IVisual2>();
            
             
-            spriteVisual.SetBrush(_blurBrush);
+            spriteVisual.SetBrush(compositionBrush);
             visual.SetIsVisible(0);
             visual2.SetRelativeSizeAdjustment(new Vector2(1.0f, 1.0f));
 

+ 5 - 5
src/Windows/Avalonia.Win32/WinRT/Composition/WinUiCompositedWindowSurface.cs

@@ -15,7 +15,7 @@ namespace Avalonia.Win32.WinRT.Composition
         private EglPlatformOpenGlInterface _egl;
         private readonly EglGlPlatformSurfaceBase.IEglWindowGlPlatformSurfaceInfo _info;
         private IRef<WinUICompositedWindow> _window;
-        private bool _enableBlur;
+        private BlurEffect _blurEffect;
 
         public WinUiCompositedWindowSurface(WinUICompositorConnection connection, IEglWindowGlPlatformSurfaceInfo info) : base()
         {
@@ -31,7 +31,7 @@ namespace Avalonia.Win32.WinRT.Composition
                 if (_window?.Item == null)
                 {
                     _window = RefCountable.Create(_connection.CreateWindow(_info.Handle));
-                    _window.Item.SetBlur(_enableBlur);
+                    _window.Item.SetBlur(_blurEffect);
                 }
 
                 return new CompositionRenderTarget(_egl, _window, _info);
@@ -100,10 +100,10 @@ namespace Avalonia.Win32.WinRT.Composition
             }
         }
 
-        public void SetBlur(bool enable)
+        public void SetBlur(BlurEffect blurEffect)
         {
-            _enableBlur = enable;
-            _window?.Item?.SetBlur(enable);
+            _blurEffect = blurEffect;
+            _window?.Item?.SetBlur(blurEffect);
         }
 
         public void Dispose()

+ 9 - 2
src/Windows/Avalonia.Win32/WinRT/IBlurHost.cs

@@ -1,7 +1,14 @@
 namespace Avalonia.Win32.WinRT
 {
-    public interface IBlurHost
+    public enum BlurEffect
     {
-        void SetBlur(bool enable);
+        None,
+        Acrylic,
+        Mica
+    }
+    
+    internal interface IBlurHost
+    {
+        void SetBlur(BlurEffect enable);
     }
 }

+ 119 - 4
src/Windows/Avalonia.Win32/WinRT/winrt.idl

@@ -358,6 +358,12 @@ interface ICompositor2 : IInspectable
     [overload("CreateStepEasingFunction")] HRESULT CreateStepEasingFunctionWithStepCount([in] INT32 stepCount, [out] [retval] void** result);
 }
 
+[uuid(0D8FB190-F122-5B8D-9FDD-543B0D8EB7F3)]
+interface ICompositorWithBlurredWallpaperBackdropBrush : IInspectable
+{
+    HRESULT TryCreateBlurredWallpaperBackdropBrush([out] [retval] ICompositionBackdropBrush** result);
+}
+
 [uuid(08E05581-1AD1-4F97-9757-402D76E4233B)]
 interface ISpriteVisual : IInspectable
 {
@@ -520,6 +526,13 @@ enum CompositionCompositeMode
     MinBlend,
 }
 
+[contract(Windows.Foundation.UniversalApiContract, 2.0)]
+[exclusiveto(Windows.UI.Composition.CompositionClip)]
+[uuid(1CCD2A52-CFC7-4ACE-9983-146BB8EB6A3C)]
+interface ICompositionClip : IInspectable
+{
+}
+
 [uuid(117E202D-A859-4C89-873B-C2AA566788E3)]
 interface IVisual : IInspectable
 {
@@ -531,8 +544,8 @@ interface IVisual : IInspectable
     [propput] HRESULT BorderMode([in] CompositionBorderMode value);
     [propget] HRESULT CenterPoint([out] [retval] Vector3* value);
     [propput] HRESULT CenterPoint([in] Vector3 value);
-    [propget] HRESULT Clip([out] [retval]void** value);
-    [propput] HRESULT Clip([in] void* value);
+    [propget] HRESULT Clip([out] [retval] ICompositionClip** value);
+    [propput] HRESULT Clip([in] ICompositionClip* value);
     [propget] HRESULT CompositeMode([out] [retval] CompositionCompositeMode* value);
     [propput] HRESULT CompositeMode([in] CompositionCompositeMode value);
     [propget] HRESULT IsVisible([out] [retval] boolean* value);
@@ -692,6 +705,99 @@ interface ICompositionScopedBatch : IInspectable
     [eventremove] HRESULT RemoveCompleted([in] int token);
 }
 
+[contract(Windows.Foundation.UniversalApiContract, 6.0)]
+[exclusiveto(Windows.UI.Composition.CompositionRoundedRectangleGeometry)]
+[uuid(8770C822-1D50-4B8B-B013-7C9A0E46935F)]
+interface ICompositionRoundedRectangleGeometry : IInspectable
+{
+    [propget] HRESULT CornerRadius([out] [retval] Vector2* value);
+    [propput] HRESULT CornerRadius([in] Vector2 value);
+    [propget] HRESULT Offset([out] [retval] Vector2* value);
+    [propput] HRESULT Offset([in] Vector2 value);
+    [propget] HRESULT Size([out] [retval] Vector2* value);
+    [propput] HRESULT Size([in] Vector2 value);
+}
+
+[contract(Windows.Foundation.UniversalApiContract, 6.0)]
+[exclusiveto(Windows.UI.Composition.CompositionGeometry)]
+[uuid(E985217C-6A17-4207-ABD8-5FD3DD612A9D)]
+interface ICompositionGeometry : IInspectable
+{
+    [propget] HRESULT TrimEnd([out] [retval] FLOAT* value);
+    [propput] HRESULT TrimEnd([in] FLOAT value);
+    [propget] HRESULT TrimOffset([out] [retval] FLOAT* value);
+    [propput] HRESULT TrimOffset([in] FLOAT value);
+    [propget] HRESULT TrimStart([out] [retval] FLOAT* value);
+    [propput] HRESULT TrimStart([in] FLOAT value);
+}
+
+[uuid(401B61BB-0007-4363-B1F3-6BCC003FB83E)]
+interface ICompositionSpriteShape : IInspectable
+{
+    [propget] HRESULT GetFillBrush([out] [retval] ICompositionBrush** value);
+    [propput] HRESULT SetFillBrush([in] ICompositionBrush* value);
+    [propget] HRESULT Geometry([out] [retval] ICompositionGeometry** value);
+    [propput] HRESULT Geometry([in] ICompositionGeometry* value);
+    [propget] HRESULT IsStrokeNonScaling([out] [retval] boolean* value);
+    [propput] HRESULT IsStrokeNonScaling([in] boolean value);
+    [propget] HRESULT StrokeBrush([out] [retval] ICompositionBrush** value);
+    [propput] HRESULT StrokeBrush([in] ICompositionBrush* value);
+    [propget] HRESULT StrokeDashArray();
+    [propget] HRESULT StrokeDashCap();
+    [propput] HRESULT StrokeDashCap();
+    [propget] HRESULT StrokeDashOffset();
+    [propput] HRESULT StrokeDashOffset();
+    [propget] HRESULT StrokeEndCap();
+    [propput] HRESULT StrokeEndCap();
+    [propget] HRESULT StrokeLineJoin();
+    [propput] HRESULT StrokeLineJoin();
+    [propget] HRESULT StrokeMiterLimit();
+    [propput] HRESULT StrokeMiterLimit();
+    [propget] HRESULT StrokeStartCap();
+    [propput] HRESULT StrokeStartCap();
+    [propget] HRESULT StrokeThickness();
+    [propput] HRESULT StrokeThickness();
+}
+
+[contract(Windows.Foundation.UniversalApiContract, 6.0)]
+[exclusiveto(Windows.UI.Composition.CompositionShape)]
+[uuid(B47CE2F7-9A88-42C4-9E87-2E500CA8688C)]
+interface ICompositionShape : IInspectable
+{
+    [propget] HRESULT CenterPoint([out] [retval] Vector2* value);
+    [propput] HRESULT CenterPoint([in] Vector2 value);
+}
+
+[uuid(42d4219a-be1b-5091-8f1e-90270840fc2d)]
+interface IVectorOfCompositionShape : IInspectable
+{
+    HRESULT GetAt();
+    [propget] HRESULT GetSize();
+    HRESULT GetView();
+    HRESULT IndexOf();
+    HRESULT SetAt();
+    HRESULT InsertAt(); 
+    HRESULT RemoveAt();
+    HRESULT Append([in] ICompositionShape* value);
+    HRESULT RemoveAtEnd();
+    HRESULT Clear();
+}
+
+[contract(Windows.Foundation.UniversalApiContract, 7.0)]
+[exclusiveto(Windows.UI.Composition.CompositionGeometricClip)]
+[uuid(C840B581-81C9-4444-A2C1-CCAECE3A50E5)]
+interface ICompositionGeometricClip : IInspectable
+{
+    [propget] HRESULT Geometry([out] [retval] ICompositionGeometry** value);
+    [propput] HRESULT Geometry([in] ICompositionGeometry* value);
+}
+
+[uuid(F2BD13C3-BA7E-4B0F-9126-FFB7536B8176)]
+interface IShapeVisual : IInspectable
+{
+    [propget] HRESULT Shapes([out] [retval] IUnknown** value);
+}
+
 [uuid(48EA31AD-7FCD-4076-A79C-90CC4B852C9B)]
 interface ICompositor5 : IInspectable
 {
@@ -709,10 +815,19 @@ interface ICompositor5 : IInspectable
     [overload("CreatePathGeometry")] HRESULT CreatePathGeometryWithPath([in] void* path, [out] [retval] void** result);
     HRESULT CreatePathKeyFrameAnimation([out] [retval] void** result);
     HRESULT CreateRectangleGeometry([out] [retval] void** result);
-    HRESULT CreateRoundedRectangleGeometry([out] [retval] void** result);
-    HRESULT CreateShapeVisual([out] [retval] void** result);
+    HRESULT CreateRoundedRectangleGeometry([out] [retval] ICompositionRoundedRectangleGeometry** result);
+    HRESULT CreateShapeVisual([out] [retval] IShapeVisual** result);
     [overload("CreateSpriteShape")] HRESULT CreateSpriteShape([out] [retval] void** result);
     [overload("CreateSpriteShape")] HRESULT CreateSpriteShapeWithGeometry([in] void* geometry, [out] [retval] void** result);
     HRESULT CreateViewBox([out] [retval] void** result);
     HRESULT RequestCommitAsync([out] [retval] IAsyncAction** operation);
 }
+
+[contract(Windows.Foundation.UniversalApiContract, 7.0)]
+[exclusiveto(Windows.UI.Composition.Compositor)]
+[uuid(7A38B2BD-CEC8-4EEB-830F-D8D07AEDEBC3)]
+interface ICompositor6 : IInspectable
+{
+    [overload("CreateGeometricClip")] HRESULT CreateGeometricClip([out] [retval] ICompositionGeometricClip** result);
+    [overload("CreateGeometricClip")] HRESULT CreateGeometricClipWithGeometry([in] ICompositionGeometry* geometry, [out] [retval] ICompositionGeometricClip** result); 
+}

+ 9 - 2
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -376,7 +376,13 @@ namespace Avalonia.Win32
         {
             if (_isUsingComposition)
             {
-                _blurHost?.SetBlur(transparencyLevel >= WindowTransparencyLevel.Blur);
+                _blurHost?.SetBlur(transparencyLevel switch
+                {
+                    WindowTransparencyLevel.Mica => BlurEffect.Mica,
+                    WindowTransparencyLevel.AcrylicBlur => BlurEffect.Acrylic,
+                    WindowTransparencyLevel.Blur => BlurEffect.Acrylic,
+                    _ => BlurEffect.None
+                });
 
                 return transparencyLevel;
             }
@@ -510,7 +516,7 @@ namespace Avalonia.Win32
 
         public void Activate()
         {
-            SetActiveWindow(_hwnd);
+            SetForegroundWindow(_hwnd);
         }
 
         public IPopupImpl CreatePopup() => Win32Platform.UseOverlayPopups ? null : new PopupImpl(this);
@@ -1003,6 +1009,7 @@ namespace Avalonia.Win32
             if (!Design.IsDesignMode && activate)
             {
                 SetFocus(_hwnd);
+                SetForegroundWindow(_hwnd);
             }
         }
         

Some files were not shown because too many files changed in this diff