Selaa lähdekoodia

Merge branch 'master' into refactor/use-selectionmodel

Steven Kirk 5 vuotta sitten
vanhempi
sitoutus
51c2cc8d5f
92 muutettua tiedostoa jossa 3174 lisäystä ja 1141 poistoa
  1. 2 2
      build/SkiaSharp.props
  2. 34 15
      native/Avalonia.Native/inc/avalonia-native.h
  3. 1 5
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme
  4. 3 0
      native/Avalonia.Native/src/OSX/SystemDialogs.mm
  5. 6 1
      native/Avalonia.Native/src/OSX/app.mm
  6. 5 5
      native/Avalonia.Native/src/OSX/common.h
  7. 6 19
      native/Avalonia.Native/src/OSX/main.mm
  8. 25 11
      native/Avalonia.Native/src/OSX/menu.h
  9. 224 62
      native/Avalonia.Native/src/OSX/menu.mm
  10. 3 1
      native/Avalonia.Native/src/OSX/platformthreading.mm
  11. 9 1
      native/Avalonia.Native/src/OSX/window.h
  12. 342 99
      native/Avalonia.Native/src/OSX/window.mm
  13. 3 2
      samples/ControlCatalog/MainView.xaml
  14. 21 3
      samples/ControlCatalog/MainWindow.xaml
  15. 1 0
      samples/ControlCatalog/MainWindow.xaml.cs
  16. 40 0
      samples/ControlCatalog/ViewModels/MainWindowViewModel.cs
  17. 2 2
      src/Avalonia.Animation/Animation.cs
  18. 9 0
      src/Avalonia.Animation/AnimatorKeyFrame.cs
  19. 3 0
      src/Avalonia.Animation/Animators/Animator`1.cs
  20. 20 0
      src/Avalonia.Animation/KeyFrame.cs
  21. 349 0
      src/Avalonia.Animation/KeySpline.cs
  22. 1 1
      src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs
  23. 3 0
      src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs
  24. 18 4
      src/Avalonia.Controls/ComboBox.cs
  25. 7 0
      src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs
  26. 7 0
      src/Avalonia.Controls/INativeMenuItemExporterEventsImplBridge.cs
  27. 14 6
      src/Avalonia.Controls/NativeMenu.cs
  28. 1 1
      src/Avalonia.Controls/NativeMenuBar.cs
  29. 80 38
      src/Avalonia.Controls/NativeMenuItem.cs
  30. 1 1
      src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs
  31. 2 3
      src/Avalonia.Controls/Platform/ISystemDialogImpl.cs
  32. 22 24
      src/Avalonia.Controls/Presenters/TextPresenter.cs
  33. 3 3
      src/Avalonia.Controls/SystemDialog.cs
  34. 23 18
      src/Avalonia.Controls/Window.cs
  35. 5 0
      src/Avalonia.Controls/WindowState.cs
  36. 2 2
      src/Avalonia.DesignerSupport/Remote/Stubs.cs
  37. 1 1
      src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs
  38. 4 8
      src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs
  39. 30 5
      src/Avalonia.FreeDesktop/DBusMenuExporter.cs
  40. 56 359
      src/Avalonia.Native/AvaloniaNativeMenuExporter.cs
  41. 176 0
      src/Avalonia.Native/IAvnMenu.cs
  42. 175 0
      src/Avalonia.Native/IAvnMenuItem.cs
  43. 2 0
      src/Avalonia.Native/Mappings.xml
  44. 20 0
      src/Avalonia.Native/MenuActionCallback.cs
  45. 147 0
      src/Avalonia.Native/OsxUnicodeKeys.cs
  46. 20 0
      src/Avalonia.Native/PredicateCallback.cs
  47. 14 6
      src/Avalonia.Native/SystemDialogs.cs
  48. 1 1
      src/Avalonia.Native/WindowImpl.cs
  49. 145 3
      src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs
  50. 22 4
      src/Avalonia.Visuals/Media/FontManager.cs
  51. 1 2
      src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs
  52. 8 8
      src/Avalonia.Visuals/Media/Fonts/FontKey.cs
  53. 6 0
      src/Avalonia.Visuals/Media/FormattedText.cs
  54. 1 1
      src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs
  55. 1 1
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs
  56. 0 44
      src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs
  57. 22 4
      src/Avalonia.Visuals/Rendering/DeferredRenderer.cs
  58. 7 2
      src/Avalonia.Visuals/Rendering/RendererBase.cs
  59. 7 6
      src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs
  60. 8 20
      src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs
  61. 13 1
      src/Avalonia.Visuals/VisualTree/VisualExtensions.cs
  62. 10 4
      src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs
  63. 1 0
      src/Avalonia.X11/X11Atoms.cs
  64. 21 11
      src/Avalonia.X11/X11Window.cs
  65. 10 7
      src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs
  66. 1 0
      src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs
  67. 45 22
      src/Skia/Avalonia.Skia/DrawingContextImpl.cs
  68. 37 9
      src/Skia/Avalonia.Skia/FontManagerImpl.cs
  69. 13 2
      src/Skia/Avalonia.Skia/FormattedTextImpl.cs
  70. 1 8
      src/Skia/Avalonia.Skia/GlyphRunImpl.cs
  71. 68 58
      src/Skia/Avalonia.Skia/PlatformRenderInterface.cs
  72. 2 2
      src/Skia/Avalonia.Skia/SKTypefaceCollection.cs
  73. 1 1
      src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs
  74. 9 0
      src/Skia/Avalonia.Skia/SkiaOptions.cs
  75. 1 1
      src/Skia/Avalonia.Skia/SkiaPlatform.cs
  76. 1 1
      src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs
  77. 55 0
      src/Windows/Avalonia.Win32/Interop/TaskBarList.cs
  78. 23 1
      src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs
  79. 4 7
      src/Windows/Avalonia.Win32/ScreenImpl.cs
  80. 7 8
      src/Windows/Avalonia.Win32/SystemDialogImpl.cs
  81. 13 0
      src/Windows/Avalonia.Win32/Win32TypeExtensions.cs
  82. 166 26
      src/Windows/Avalonia.Win32/WindowImpl.cs
  83. 49 0
      tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs
  84. 145 0
      tests/Avalonia.Animation.UnitTests/KeySplineTests.cs
  85. 169 152
      tests/Avalonia.Controls.UnitTests/WindowTests.cs
  86. 44 0
      tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs
  87. 1 1
      tests/Avalonia.RenderTests/TestBase.cs
  88. 38 10
      tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs
  89. 13 0
      tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs
  90. 22 2
      tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs
  91. 9 2
      tests/Avalonia.UnitTests/MockFontManagerImpl.cs
  92. 11 1
      tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs

+ 2 - 2
build/SkiaSharp.props

@@ -1,6 +1,6 @@
 <Project DefaultTargets="Build" xmlns="http://schemas.microsoft.com/developer/msbuild/2003">
   <ItemGroup>
-    <PackageReference Include="SkiaSharp" Version="1.68.1" />
-    <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.1" />
+    <PackageReference Include="SkiaSharp" Version="1.68.2" />
+    <PackageReference Condition="'$(IncludeLinuxSkia)' == 'true'" Include="SkiaSharp.NativeAssets.Linux" Version="1.68.2" />
   </ItemGroup>
 </Project>

+ 34 - 15
native/Avalonia.Native/inc/avalonia-native.h

@@ -1,5 +1,6 @@
 #include "com.h"
 #include "key.h"
+#include "stddef.h"
 
 #define AVNCOM(name, id) COMINTERFACE(name, 2e2cda0a, 9ae5, 4f1b, 8e, 20, 08, 1a, 04, 27, 9f, id)
 
@@ -19,8 +20,9 @@ struct IAvnGlContext;
 struct IAvnGlDisplay;
 struct IAvnGlSurfaceRenderTarget;
 struct IAvnGlSurfaceRenderingSession;
-struct IAvnAppMenu;
-struct IAvnAppMenuItem;
+struct IAvnMenu;
+struct IAvnMenuItem;
+struct IAvnMenuEvents;
 
 enum SystemDecorations {
     SystemDecorationsNone = 0,
@@ -133,6 +135,7 @@ enum AvnWindowState
     Normal,
     Minimized,
     Maximized,
+    FullScreen,
 };
 
 enum AvnStandardCursorType
@@ -175,6 +178,13 @@ enum AvnWindowEdge
     WindowEdgeSouthEast
 };
 
+enum AvnMenuItemToggleType
+{
+    None,
+    CheckMark,
+    Radio
+};
+
 AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown
 {
 public:
@@ -188,11 +198,10 @@ public:
     virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0;
     virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0;
     virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0;
-    virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) = 0;
-    virtual HRESULT SetAppMenu(IAvnAppMenu* menu) = 0;
-    virtual HRESULT CreateMenu (IAvnAppMenu** ppv) = 0;
-    virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) = 0;
-    virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) = 0;
+    virtual HRESULT SetAppMenu(IAvnMenu* menu) = 0;
+    virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) = 0;
+    virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) = 0;
+    virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) = 0;
 };
 
 AVNCOM(IAvnString, 17) : IUnknown
@@ -222,8 +231,7 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown
     virtual HRESULT SetTopMost (bool value) = 0;
     virtual HRESULT SetCursor(IAvnCursor* cursor) = 0;
     virtual HRESULT CreateGlRenderTarget(IAvnGlSurfaceRenderTarget** ret) = 0;
-    virtual HRESULT SetMainMenu(IAvnAppMenu* menu) = 0;
-    virtual HRESULT ObtainMainMenu(IAvnAppMenu** retOut) = 0;
+    virtual HRESULT SetMainMenu(IAvnMenu* menu) = 0;
     virtual HRESULT ObtainNSWindowHandle(void** retOut) = 0;
     virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0;
     virtual HRESULT ObtainNSViewHandle(void** retOut) = 0;
@@ -239,7 +247,7 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase
 {
     virtual HRESULT ShowDialog (IAvnWindow* parent) = 0;
     virtual HRESULT SetCanResize(bool value) = 0;
-    virtual HRESULT SetHasDecorations(SystemDecorations value) = 0;
+    virtual HRESULT SetDecorations(SystemDecorations value) = 0;
     virtual HRESULT SetTitle (void* utf8Title) = 0;
     virtual HRESULT SetTitleBarColor (AvnColor color) = 0;
     virtual HRESULT SetWindowState(AvnWindowState state) = 0;
@@ -388,10 +396,10 @@ AVNCOM(IAvnGlSurfaceRenderingSession, 16) : IUnknown
     virtual HRESULT GetScaling(double* ret) = 0;
 };
 
-AVNCOM(IAvnAppMenu, 17) : IUnknown
+AVNCOM(IAvnMenu, 17) : IUnknown
 {
-    virtual HRESULT AddItem (IAvnAppMenuItem* item) = 0;
-    virtual HRESULT RemoveItem (IAvnAppMenuItem* item) = 0;
+    virtual HRESULT InsertItem (int index, IAvnMenuItem* item) = 0;
+    virtual HRESULT RemoveItem (IAvnMenuItem* item) = 0;
     virtual HRESULT SetTitle (void* utf8String) = 0;
     virtual HRESULT Clear () = 0;
 };
@@ -401,12 +409,23 @@ AVNCOM(IAvnPredicateCallback, 18) : IUnknown
     virtual bool Evaluate() = 0;
 };
 
-AVNCOM(IAvnAppMenuItem, 19) : IUnknown
+AVNCOM(IAvnMenuItem, 19) : IUnknown
 {
-    virtual HRESULT SetSubMenu (IAvnAppMenu* menu) = 0;
+    virtual HRESULT SetSubMenu (IAvnMenu* menu) = 0;
     virtual HRESULT SetTitle (void* utf8String) = 0;
     virtual HRESULT SetGesture (void* utf8String, AvnInputModifiers modifiers) = 0;
     virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) = 0;
+    virtual HRESULT SetIsChecked (bool isChecked) = 0;
+    virtual HRESULT SetToggleType (AvnMenuItemToggleType toggleType) = 0;
+    virtual HRESULT SetIcon (void* data, size_t length) = 0;
+};
+
+AVNCOM(IAvnMenuEvents, 1A) : IUnknown
+{
+    /**
+     * NeedsUpdate
+     */
+    virtual void NeedsUpdate () = 0;
 };
 
 extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative();

+ 1 - 5
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme

@@ -29,8 +29,6 @@
       shouldUseLaunchSchemeArgsEnv = "YES">
       <Testables>
       </Testables>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </TestAction>
    <LaunchAction
       buildConfiguration = "Debug"
@@ -58,12 +56,10 @@
       </MacroExpansion>
       <CommandLineArguments>
          <CommandLineArgument
-            argument = "bin/Debug/netcoreapp2.0/ControlCatalog.NetCore.dll"
+            argument = "bin/Debug/netcoreapp3.1/ControlCatalog.NetCore.dll"
             isEnabled = "YES">
          </CommandLineArgument>
       </CommandLineArguments>
-      <AdditionalOptions>
-      </AdditionalOptions>
    </LaunchAction>
    <ProfileAction
       buildConfiguration = "Release"

+ 3 - 0
native/Avalonia.Native/src/OSX/SystemDialogs.mm

@@ -20,6 +20,7 @@ public:
             
             if(title != nullptr)
             {
+                panel.message = [NSString stringWithUTF8String:title];
                 panel.title = [NSString stringWithUTF8String:title];
             }
             
@@ -94,6 +95,7 @@ public:
             
             if(title != nullptr)
             {
+                panel.message = [NSString stringWithUTF8String:title];
                 panel.title = [NSString stringWithUTF8String:title];
             }
             
@@ -182,6 +184,7 @@ public:
             
             if(title != nullptr)
             {
+                panel.message = [NSString stringWithUTF8String:title];
                 panel.title = [NSString stringWithUTF8String:title];
             }
             

+ 6 - 1
native/Avalonia.Native/src/OSX/app.mm

@@ -2,7 +2,8 @@
 @interface AvnAppDelegate : NSObject<NSApplicationDelegate>
 @end
 
-extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular;
+NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationActivationPolicyRegular;
+
 @implementation AvnAppDelegate
 - (void)applicationWillFinishLaunching:(NSNotification *)notification
 {
@@ -14,6 +15,10 @@ extern NSApplicationActivationPolicy AvnDesiredActivationPolicy = NSApplicationA
         }
         
         [[NSApplication sharedApplication] setActivationPolicy: AvnDesiredActivationPolicy];
+        
+        [[NSUserDefaults standardUserDefaults] setBool:NO forKey:@"NSFullScreenMenuItemEverywhere"];
+        
+        [[NSApplication sharedApplication] setHelpMenu: [[NSMenu new] initWithTitle:@""]];
     }
 }
 

+ 5 - 5
native/Avalonia.Native/src/OSX/common.h

@@ -15,11 +15,11 @@ extern IAvnScreens* CreateScreens();
 extern IAvnClipboard* CreateClipboard();
 extern IAvnCursorFactory* CreateCursorFactory();
 extern IAvnGlDisplay* GetGlDisplay();
-extern IAvnAppMenu* CreateAppMenu();
-extern IAvnAppMenuItem* CreateAppMenuItem();
-extern IAvnAppMenuItem* CreateAppMenuItemSeperator();
-extern void SetAppMenu (NSString* appName, IAvnAppMenu* appMenu);
-extern IAvnAppMenu* GetAppMenu ();
+extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events);
+extern IAvnMenuItem* CreateAppMenuItem();
+extern IAvnMenuItem* CreateAppMenuItemSeperator();
+extern void SetAppMenu (NSString* appName, IAvnMenu* appMenu);
+extern IAvnMenu* GetAppMenu ();
 extern NSMenuItem* GetAppMenuItem ();
 
 extern void InitializeAvnApp();

+ 6 - 19
native/Avalonia.Native/src/OSX/main.mm

@@ -92,12 +92,11 @@ void SetProcessName(NSString* appTitle) {
     PrivateLSASN asn = ls_get_current_application_asn_func();
     // Constant used by WebKit; what exactly it means is unknown.
     const int magic_session_constant = -2;
-    OSErr err =
+    
     ls_set_application_information_item_func(magic_session_constant, asn,
                                              ls_display_name_key,
                                              process_name,
                                              NULL /* optional out param */);
-    //LOG_IF(ERROR, err) << "Call to set process name failed, err " << err;
 }
 
 class MacOptions : public ComSingleObject<IAvnMacOptions, &IID_IAvnMacOptions>
@@ -228,41 +227,29 @@ public:
         return S_OK;
     }
     
-    virtual HRESULT CreateMenu (IAvnAppMenu** ppv) override
+    virtual HRESULT CreateMenu (IAvnMenuEvents* cb, IAvnMenu** ppv) override
     {
-        *ppv = ::CreateAppMenu();
+        *ppv = ::CreateAppMenu(cb);
         return S_OK;
     }
     
-    virtual HRESULT CreateMenuItem (IAvnAppMenuItem** ppv) override
+    virtual HRESULT CreateMenuItem (IAvnMenuItem** ppv) override
     {
         *ppv = ::CreateAppMenuItem();
         return S_OK;
     }
     
-    virtual HRESULT CreateMenuItemSeperator (IAvnAppMenuItem** ppv) override
+    virtual HRESULT CreateMenuItemSeperator (IAvnMenuItem** ppv) override
     {
         *ppv = ::CreateAppMenuItemSeperator();
         return S_OK;
     }
     
-    virtual HRESULT SetAppMenu (IAvnAppMenu* appMenu) override
+    virtual HRESULT SetAppMenu (IAvnMenu* appMenu) override
     {
         ::SetAppMenu(s_appTitle, appMenu);
         return S_OK;
     }
-    
-    virtual HRESULT ObtainAppMenu(IAvnAppMenu** retOut) override
-    {
-        if(retOut == nullptr)
-        {
-            return E_POINTER;
-        }
-        
-        *retOut = ::GetAppMenu();
-        
-        return S_OK;
-    }
 };
 
 extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative()

+ 25 - 11
native/Avalonia.Native/src/OSX/menu.h

@@ -14,8 +14,10 @@
 class AvnAppMenuItem;
 class AvnAppMenu;
 
-@interface AvnMenu : NSMenu // for some reason it doesnt detect nsmenu here but compiler doesnt complain
-- (void)setMenu:(NSMenu*) menu;
+@interface AvnMenu : NSMenu
+- (id) initWithDelegate: (NSObject<NSMenuDelegate>*) del;
+- (void) setHasGlobalMenuItem: (bool) value;
+- (bool) hasGlobalMenuItem;
 @end
 
 @interface AvnMenuItem : NSMenuItem
@@ -23,13 +25,14 @@ class AvnAppMenu;
 - (void)didSelectItem:(id)sender;
 @end
 
-class AvnAppMenuItem : public ComSingleObject<IAvnAppMenuItem, &IID_IAvnAppMenuItem>
+class AvnAppMenuItem : public ComSingleObject<IAvnMenuItem, &IID_IAvnMenuItem>
 {
 private:
     NSMenuItem* _native; // here we hold a pointer to an AvnMenuItem
     IAvnActionCallback* _callback;
     IAvnPredicateCallback* _predicate;
     bool _isSeperator;
+    bool _isCheckable;
     
 public:
     FORWARD_IUNKNOWN()
@@ -38,7 +41,7 @@ public:
     
     NSMenuItem* GetNative();
     
-    virtual HRESULT SetSubMenu (IAvnAppMenu* menu) override;
+    virtual HRESULT SetSubMenu (IAvnMenu* menu) override;
     
     virtual HRESULT SetTitle (void* utf8String) override;
     
@@ -46,29 +49,36 @@ public:
     
     virtual HRESULT SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) override;
     
+    virtual HRESULT SetIsChecked (bool isChecked) override;
+    
+    virtual HRESULT SetToggleType (AvnMenuItemToggleType toggleType) override;
+    
+    virtual HRESULT SetIcon (void* data, size_t length) override;
+    
     bool EvaluateItemEnabled();
     
     void RaiseOnClicked();
 };
 
 
-class AvnAppMenu : public ComSingleObject<IAvnAppMenu, &IID_IAvnAppMenu>
+class AvnAppMenu : public ComSingleObject<IAvnMenu, &IID_IAvnMenu>
 {
 private:
     AvnMenu* _native;
+    ComPtr<IAvnMenuEvents> _baseEvents;
     
 public:
     FORWARD_IUNKNOWN()
     
-    AvnAppMenu();
-    
-    AvnAppMenu(AvnMenu* native);
-    
+    AvnAppMenu(IAvnMenuEvents* events);
+        
     AvnMenu* GetNative();
     
-    virtual HRESULT AddItem (IAvnAppMenuItem* item) override;
+    void RaiseNeedsUpdate ();
+    
+    virtual HRESULT InsertItem (int index, IAvnMenuItem* item) override;
     
-    virtual HRESULT RemoveItem (IAvnAppMenuItem* item) override;
+    virtual HRESULT RemoveItem (IAvnMenuItem* item) override;
     
     virtual HRESULT SetTitle (void* utf8String) override;
     
@@ -76,5 +86,9 @@ public:
 };
 
 
+@interface AvnMenuDelegate : NSObject<NSMenuDelegate>
+- (id) initWithParent: (AvnAppMenu*) parent;
+@end
+
 #endif
 

+ 224 - 62
native/Avalonia.Native/src/OSX/menu.mm

@@ -4,6 +4,30 @@
 #include "window.h"
 
 @implementation AvnMenu
+{
+    bool _isReparented;
+    NSObject<NSMenuDelegate>* _wtf;
+}
+
+- (id) initWithDelegate: (NSObject<NSMenuDelegate>*)del
+{
+    self = [super init];
+    self.delegate = del;
+    _wtf = del;
+    _isReparented = false;
+    return self;
+}
+
+- (bool)hasGlobalMenuItem
+{
+    return _isReparented;
+}
+
+- (void)setHasGlobalMenuItem:(bool)value
+{
+    _isReparented = value;
+}
+
 @end
 
 @implementation AvnMenuItem
@@ -46,6 +70,7 @@
 
 AvnAppMenuItem::AvnAppMenuItem(bool isSeperator)
 {
+    _isCheckable = false;
     _isSeperator = isSeperator;
     
     if(isSeperator)
@@ -65,49 +90,134 @@ NSMenuItem* AvnAppMenuItem::GetNative()
     return _native;
 }
 
-HRESULT AvnAppMenuItem::SetSubMenu (IAvnAppMenu* menu)
+HRESULT AvnAppMenuItem::SetSubMenu (IAvnMenu* menu)
 {
-    auto nsMenu = dynamic_cast<AvnAppMenu*>(menu)->GetNative();
-    
-    [_native setSubmenu: nsMenu];
-    
-    return S_OK;
+    @autoreleasepool
+    {
+        if(menu != nullptr)
+        {
+            auto nsMenu = dynamic_cast<AvnAppMenu*>(menu)->GetNative();
+            
+            [_native setSubmenu: nsMenu];
+        }
+        else
+        {
+            [_native setSubmenu: nullptr];
+        }
+        
+        return S_OK;
+    }
 }
 
 HRESULT AvnAppMenuItem::SetTitle (void* utf8String)
 {
-    if (utf8String != nullptr)
+    @autoreleasepool
     {
-        [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]];
+        if (utf8String != nullptr)
+        {
+            [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]];
+        }
+        
+        return S_OK;
     }
-    
-    return S_OK;
 }
 
 HRESULT AvnAppMenuItem::SetGesture (void* key, AvnInputModifiers modifiers)
 {
-    NSEventModifierFlags flags = 0;
-    
-    if (modifiers & Control)
-        flags |= NSEventModifierFlagControl;
-    if (modifiers & Shift)
-        flags |= NSEventModifierFlagShift;
-    if (modifiers & Alt)
-        flags |= NSEventModifierFlagOption;
-    if (modifiers & Windows)
-        flags |= NSEventModifierFlagCommand;
-    
-    [_native setKeyEquivalent:[NSString stringWithUTF8String:(const char*)key]];
-    [_native setKeyEquivalentModifierMask:flags];
-    
-    return S_OK;
+    @autoreleasepool
+    {
+        NSEventModifierFlags flags = 0;
+        
+        if (modifiers & Control)
+            flags |= NSEventModifierFlagControl;
+        if (modifiers & Shift)
+            flags |= NSEventModifierFlagShift;
+        if (modifiers & Alt)
+            flags |= NSEventModifierFlagOption;
+        if (modifiers & Windows)
+            flags |= NSEventModifierFlagCommand;
+        
+        [_native setKeyEquivalent:[NSString stringWithUTF8String:(const char*)key]];
+        [_native setKeyEquivalentModifierMask:flags];
+        
+        return S_OK;
+    }
 }
 
 HRESULT AvnAppMenuItem::SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback)
 {
-    _predicate = predicate;
-    _callback = callback;
-    return S_OK;
+    @autoreleasepool
+    {
+        _predicate = predicate;
+        _callback = callback;
+        return S_OK;
+    }
+}
+
+HRESULT AvnAppMenuItem::SetIsChecked (bool isChecked)
+{
+    @autoreleasepool
+    {
+        [_native setState:(isChecked && _isCheckable ? NSOnState : NSOffState)];
+        return S_OK;
+    }
+}
+
+HRESULT AvnAppMenuItem::SetToggleType(AvnMenuItemToggleType toggleType)
+{
+    @autoreleasepool
+    {
+        switch(toggleType)
+        {
+            case AvnMenuItemToggleType::None:
+                [_native setOnStateImage: [NSImage imageNamed:@"NSMenuCheckmark"]];
+                
+                _isCheckable = false;
+                break;
+                
+            case AvnMenuItemToggleType::CheckMark:
+                [_native setOnStateImage: [NSImage imageNamed:@"NSMenuCheckmark"]];
+                
+                _isCheckable = true;
+                break;
+                
+            case AvnMenuItemToggleType::Radio:
+                [_native setOnStateImage: [NSImage imageNamed:@"NSMenuItemBullet"]];
+                
+                _isCheckable = true;
+                break;
+        }
+        
+        return S_OK;
+    }
+}
+
+HRESULT AvnAppMenuItem::SetIcon(void *data, size_t length)
+{
+    @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;
+    }
 }
 
 bool AvnAppMenuItem::EvaluateItemEnabled()
@@ -130,71 +240,123 @@ void AvnAppMenuItem::RaiseOnClicked()
     }
 }
 
-AvnAppMenu::AvnAppMenu()
+AvnAppMenu::AvnAppMenu(IAvnMenuEvents* events)
 {
-    _native = [AvnMenu new];
+    _baseEvents = events;
+    id del = [[AvnMenuDelegate alloc] initWithParent: this];
+    _native = [[AvnMenu alloc] initWithDelegate: del];
 }
 
-AvnAppMenu::AvnAppMenu(AvnMenu* native)
-{
-    _native = native;
-}
 
 AvnMenu* AvnAppMenu::GetNative()
 {
     return _native;
 }
 
-HRESULT AvnAppMenu::AddItem (IAvnAppMenuItem* item)
+void AvnAppMenu::RaiseNeedsUpdate()
 {
-    auto avnMenuItem = dynamic_cast<AvnAppMenuItem*>(item);
-    
-    if(avnMenuItem != nullptr)
+    if(_baseEvents != nullptr)
     {
-        [_native addItem: avnMenuItem->GetNative()];
+        _baseEvents->NeedsUpdate();
     }
-    
-    return S_OK;
 }
 
-HRESULT AvnAppMenu::RemoveItem (IAvnAppMenuItem* item)
+HRESULT AvnAppMenu::InsertItem(int index, IAvnMenuItem *item)
 {
-    auto avnMenuItem = dynamic_cast<AvnAppMenuItem*>(item);
-    
-    if(avnMenuItem != nullptr)
+    @autoreleasepool
     {
-        [_native removeItem:avnMenuItem->GetNative()];
+        if([_native hasGlobalMenuItem])
+        {
+            index++;
+        }
+        
+        auto avnMenuItem = dynamic_cast<AvnAppMenuItem*>(item);
+        
+        if(avnMenuItem != nullptr)
+        {
+            [_native insertItem: avnMenuItem->GetNative() atIndex:index];
+        }
+        
+        return S_OK;
+    }
+}
+
+HRESULT AvnAppMenu::RemoveItem (IAvnMenuItem* item)
+{
+    @autoreleasepool
+    {
+        auto avnMenuItem = dynamic_cast<AvnAppMenuItem*>(item);
+        
+        if(avnMenuItem != nullptr)
+        {
+            [_native removeItem:avnMenuItem->GetNative()];
+        }
+        
+        return S_OK;
     }
-    
-    return S_OK;
 }
 
 HRESULT AvnAppMenu::SetTitle (void* utf8String)
 {
-    if (utf8String != nullptr)
+    @autoreleasepool
     {
-        [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]];
+        if (utf8String != nullptr)
+        {
+            [_native setTitle:[NSString stringWithUTF8String:(const char*)utf8String]];
+        }
+        
+        return S_OK;
     }
-    
-    return S_OK;
 }
 
 HRESULT AvnAppMenu::Clear()
 {
-    [_native removeAllItems];
-    return S_OK;
+    @autoreleasepool
+    {
+        [_native removeAllItems];
+        return S_OK;
+    }
+}
+
+@implementation AvnMenuDelegate
+{
+    ComPtr<AvnAppMenu> _parent;
 }
+- (id) initWithParent:(AvnAppMenu *)parent
+{
+    self = [super init];
+    _parent = parent;
+    return self;
+}
+- (BOOL)menu:(NSMenu *)menu updateItem:(NSMenuItem *)item atIndex:(NSInteger)index shouldCancel:(BOOL)shouldCancel
+{
+    if(shouldCancel)
+        return NO;
+    return YES;
+}
+
+- (NSInteger)numberOfItemsInMenu:(NSMenu *)menu
+{
+    return [menu numberOfItems];
+}
+
+- (void)menuNeedsUpdate:(NSMenu *)menu
+{
+    _parent->RaiseNeedsUpdate();
+}
+
+
+@end
 
-extern IAvnAppMenu* CreateAppMenu()
+extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* cb)
 {
     @autoreleasepool
     {
-        id menuBar = [NSMenu new];
-        return new AvnAppMenu(menuBar);
+        return new AvnAppMenu(cb);
     }
 }
 
-extern IAvnAppMenuItem* CreateAppMenuItem()
+extern IAvnMenuItem* CreateAppMenuItem()
 {
     @autoreleasepool
     {
@@ -202,7 +364,7 @@ extern IAvnAppMenuItem* CreateAppMenuItem()
     }
 }
 
-extern IAvnAppMenuItem* CreateAppMenuItemSeperator()
+extern IAvnMenuItem* CreateAppMenuItemSeperator()
 {
     @autoreleasepool
     {
@@ -210,10 +372,10 @@ extern IAvnAppMenuItem* CreateAppMenuItemSeperator()
     }
 }
 
-static IAvnAppMenu* s_appMenu = nullptr;
+static IAvnMenu* s_appMenu = nullptr;
 static NSMenuItem* s_appMenuItem = nullptr;
 
-extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu)
+extern void SetAppMenu (NSString* appName, IAvnMenu* menu)
 {
     s_appMenu = menu;
     
@@ -294,7 +456,7 @@ extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu)
     }
 }
 
-extern IAvnAppMenu* GetAppMenu ()
+extern IAvnMenu* GetAppMenu ()
 {
     return s_appMenu;
 }

+ 3 - 1
native/Avalonia.Native/src/OSX/platformthreading.mm

@@ -54,9 +54,11 @@ private:
     {
     public:
         FORWARD_IUNKNOWN()
+        
         bool Running = false;
         bool Cancelled = false;
-        virtual void Cancel()
+        
+        virtual void Cancel() override
         {
             Cancelled = true;
             if(Running)

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

@@ -19,7 +19,11 @@ class WindowBaseImpl;
 -(void) pollModalSession: (NSModalSession _Nonnull) session;
 -(void) restoreParentWindow;
 -(bool) shouldTryToHandleEvents;
--(void) applyMenu:(NSMenu *)menu;
+-(bool) isModal;
+-(void) setModal: (bool) isModal;
+-(void) showAppMenuOnly;
+-(void) showWindowMenuWithAppMenu;
+-(void) applyMenu:(NSMenu* _Nullable)menu;
 -(double) getScaling;
 @end
 
@@ -31,6 +35,10 @@ struct INSWindowHolder
 struct IWindowStateChanged
 {
     virtual void WindowStateChanged () = 0;
+    virtual void StartStateTransition () = 0;
+    virtual void EndStateTransition () = 0;
+    virtual SystemDecorations Decorations () = 0;
+    virtual AvnWindowState WindowState () = 0;
 };
 
 #endif /* window_h */

+ 342 - 99
native/Avalonia.Native/src/OSX/window.mm

@@ -27,7 +27,7 @@ public:
     NSObject<IRenderTarget>* renderTarget;
     AvnPoint lastPositionSet;
     NSString* _lastTitle;
-    IAvnAppMenu* _mainMenu;
+    IAvnMenu* _mainMenu;
     bool _shown;
     
     WindowBaseImpl(IAvnWindowBaseEvents* events, IAvnGlContext* gl)
@@ -234,7 +234,7 @@ public:
         }
     }
     
-    virtual HRESULT SetMainMenu(IAvnAppMenu* menu) override
+    virtual HRESULT SetMainMenu(IAvnMenu* menu) override
     {
         _mainMenu = menu;
         
@@ -244,18 +244,11 @@ public:
         
         [Window applyMenu:nsmenu];
         
-        return S_OK;
-    }
-    
-    virtual HRESULT ObtainMainMenu(IAvnAppMenu** ret) override
-    {
-        if(ret == nullptr)
+        if ([Window isKeyWindow])
         {
-            return E_POINTER;
+            [Window showWindowMenuWithAppMenu];
         }
         
-        *ret = _mainMenu;
-        
         return S_OK;
     }
     
@@ -398,7 +391,7 @@ protected:
     
     void UpdateStyle()
     {
-        [Window setStyleMask:GetStyle()];
+        [Window setStyleMask: GetStyle()];
     }
     
 public:
@@ -411,10 +404,13 @@ public:
 class WindowImpl : public virtual WindowBaseImpl, public virtual IAvnWindow, public IWindowStateChanged
 {
 private:
-    bool _canResize = true;
-    SystemDecorations _hasDecorations = SystemDecorationsFull;
-    CGRect _lastUndecoratedFrame;
+    bool _canResize;
+    bool _fullScreenActive;
+    SystemDecorations _decorations;
     AvnWindowState _lastWindowState;
+    bool _inSetWindowState;
+    NSRect _preZoomSize;
+    bool _transitioningWindowState;
     
     FORWARD_IUNKNOWN()
     BEGIN_INTERFACE_MAP()
@@ -428,10 +424,30 @@ private:
     ComPtr<IAvnWindowEvents> WindowEvents;
     WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl)
     {
+        _fullScreenActive = false;
+        _canResize = true;
+        _decorations = SystemDecorationsFull;
+        _transitioningWindowState = false;
+        _inSetWindowState = false;
         _lastWindowState = Normal;
         WindowEvents = events;
         [Window setCanBecomeKeyAndMain];
         [Window disableCursorRects];
+        [Window setTabbingMode:NSWindowTabbingModeDisallowed];
+    }
+    
+    void HideOrShowTrafficLights ()
+    {
+        for (id subview in Window.contentView.superview.subviews) {
+            if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) {
+                NSView *titlebarView = [subview subviews][0];
+                for (id button in titlebarView.subviews) {
+                    if ([button isKindOfClass:[NSButton class]]) {
+                        [button setHidden: (_decorations != SystemDecorationsFull)];
+                    }
+                }
+            }
+        }
     }
     
     virtual HRESULT Show () override
@@ -440,8 +456,13 @@ private:
         {
             if([Window parentWindow] != nil)
                 [[Window parentWindow] removeChildWindow:Window];
+            
+            [Window setModal:FALSE];
+            
             WindowBaseImpl::Show();
             
+            HideOrShowTrafficLights();
+            
             return SetWindowState(_lastWindowState);
         }
     }
@@ -457,44 +478,74 @@ private:
             if(cparent == nullptr)
                 return E_INVALIDARG;
             
+            [Window setModal:TRUE];
+            
             [cparent->Window addChildWindow:Window ordered:NSWindowAbove];
             WindowBaseImpl::Show();
             
+            HideOrShowTrafficLights();
+            
             return S_OK;
         }
     }
     
+    void StartStateTransition () override
+    {
+        _transitioningWindowState = true;
+    }
+    
+    void EndStateTransition () override
+    {
+        _transitioningWindowState = false;
+    }
+    
+    SystemDecorations Decorations () override
+    {
+        return _decorations;
+    }
+    
+    AvnWindowState WindowState () override
+    {
+        return _lastWindowState;
+    }
+    
     void WindowStateChanged () override
     {
-        AvnWindowState state;
-        GetWindowState(&state);
-        WindowEvents->WindowStateChanged(state);
+        if(!_inSetWindowState && !_transitioningWindowState)
+        {
+            AvnWindowState state;
+            GetWindowState(&state);
+            
+            if(_lastWindowState != state)
+            {
+                _lastWindowState = state;
+                WindowEvents->WindowStateChanged(state);
+            }
+        }
     }
     
     bool UndecoratedIsMaximized ()
     {
-        return CGRectEqualToRect([Window frame], [Window screen].visibleFrame);
+        auto windowSize = [Window frame];
+        auto available = [Window screen].visibleFrame;
+        return CGRectEqualToRect(windowSize, available);
     }
     
     bool IsZoomed ()
     {
-        return _hasDecorations != SystemDecorationsNone ? [Window isZoomed] : UndecoratedIsMaximized();
+        return _decorations == SystemDecorationsFull ? [Window isZoomed] : UndecoratedIsMaximized();
     }
     
     void DoZoom()
     {
-        switch (_hasDecorations)
+        switch (_decorations)
         {
             case SystemDecorationsNone:
-                if (!UndecoratedIsMaximized())
-                {
-                    _lastUndecoratedFrame = [Window frame];
-                }
-                
-                [Window zoom:Window];
+            case SystemDecorationsBorderOnly:
+                [Window setFrame:[Window screen].visibleFrame display:true];
                 break;
 
-            case SystemDecorationsBorderOnly:
+            
             case SystemDecorationsFull:
                 [Window performZoom:Window];
                 break;
@@ -511,25 +562,52 @@ private:
         }
     }
     
-    virtual HRESULT SetHasDecorations(SystemDecorations value) override
+    virtual HRESULT SetDecorations(SystemDecorations value) override
     {
         @autoreleasepool
         {
-            _hasDecorations = value;
+            auto currentWindowState = _lastWindowState;
+            _decorations = value;
+            
+            if(_fullScreenActive)
+            {
+                return S_OK;
+            }
+            
+            auto currentFrame = [Window frame];
+            
             UpdateStyle();
+            
+            HideOrShowTrafficLights();
 
-            switch (_hasDecorations)
+            switch (_decorations)
             {
                 case SystemDecorationsNone:
                     [Window setHasShadow:NO];
                     [Window setTitleVisibility:NSWindowTitleHidden];
                     [Window setTitlebarAppearsTransparent:YES];
+                    
+                    if(currentWindowState == Maximized)
+                    {
+                        if(!UndecoratedIsMaximized())
+                        {
+                            DoZoom();
+                        }
+                    }
                     break;
 
                 case SystemDecorationsBorderOnly:
                     [Window setHasShadow:YES];
                     [Window setTitleVisibility:NSWindowTitleHidden];
                     [Window setTitlebarAppearsTransparent:YES];
+                    
+                    if(currentWindowState == Maximized)
+                    {
+                        if(!UndecoratedIsMaximized())
+                        {
+                            DoZoom();
+                        }
+                    }
                     break;
 
                 case SystemDecorationsFull:
@@ -537,6 +615,13 @@ private:
                     [Window setTitleVisibility:NSWindowTitleVisible];
                     [Window setTitlebarAppearsTransparent:NO];
                     [Window setTitle:_lastTitle];
+                    
+                    if(currentWindowState == Maximized)
+                    {
+                        auto newFrame = [Window contentRectForFrameRect:[Window frame]].size;
+                        
+                        [View setFrameSize:newFrame];
+                    }
                     break;
             }
 
@@ -593,13 +678,19 @@ private:
                 return E_POINTER;
             }
             
+            if(([Window styleMask] & NSWindowStyleMaskFullScreen) == NSWindowStyleMaskFullScreen)
+            {
+                *ret = FullScreen;
+                return S_OK;
+            }
+            
             if([Window isMiniaturized])
             {
                 *ret = Minimized;
                 return S_OK;
             }
             
-            if([Window isZoomed])
+            if(IsZoomed())
             {
                 *ret = Maximized;
                 return S_OK;
@@ -611,16 +702,57 @@ private:
         }
     }
     
+    void EnterFullScreenMode ()
+    {
+        _fullScreenActive = true;
+        
+        [Window setHasShadow:YES];
+        [Window setTitleVisibility:NSWindowTitleVisible];
+        [Window setTitlebarAppearsTransparent:NO];
+        [Window setTitle:_lastTitle];
+        
+        [Window setStyleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskResizable];
+        
+        [Window toggleFullScreen:nullptr];
+    }
+    
+    void ExitFullScreenMode ()
+    {
+        [Window toggleFullScreen:nullptr];
+        
+        _fullScreenActive = false;
+        
+        SetDecorations(_decorations);
+    }
+    
     virtual HRESULT SetWindowState (AvnWindowState state) override
     {
         @autoreleasepool
         {
+            if(_lastWindowState == state)
+            {
+                return S_OK;
+            }
+            
+            _inSetWindowState = true;
+            
+            auto currentState = _lastWindowState;
             _lastWindowState = state;
             
+            if(currentState == Normal)
+            {
+                _preZoomSize = [Window frame];
+            }
+            
             if(_shown)
             {
                 switch (state) {
                     case Maximized:
+                        if(currentState == FullScreen)
+                        {
+                            ExitFullScreenMode();
+                        }
+                        
                         lastPositionSet.X = 0;
                         lastPositionSet.Y = 0;
                         
@@ -636,40 +768,66 @@ private:
                         break;
                         
                     case Minimized:
-                        [Window miniaturize:Window];
+                        if(currentState == FullScreen)
+                        {
+                            ExitFullScreenMode();
+                        }
+                        else
+                        {
+                            [Window miniaturize:Window];
+                        }
                         break;
                         
-                    default:
+                    case FullScreen:
                         if([Window isMiniaturized])
                         {
                             [Window deminiaturize:Window];
                         }
                         
+                        EnterFullScreenMode();
+                        break;
+                        
+                    case Normal:
+                        if([Window isMiniaturized])
+                        {
+                            [Window deminiaturize:Window];
+                        }
+                        
+                        if(currentState == FullScreen)
+                        {
+                            ExitFullScreenMode();
+                        }
+                        
                         if(IsZoomed())
                         {
-                            DoZoom();
+                            if(_decorations == SystemDecorationsFull)
+                            {
+                                DoZoom();
+                            }
+                            else
+                            {
+                                [Window setFrame:_preZoomSize display:true];
+                                auto newFrame = [Window contentRectForFrameRect:[Window frame]].size;
+                                
+                                [View setFrameSize:newFrame];
+                            }
+                            
                         }
                         break;
                 }
             }
             
+            _inSetWindowState = false;
+            
             return S_OK;
         }
     }
 
     virtual void OnResized () override
     {
-        if(_shown)
+        if(_shown && !_inSetWindowState && !_transitioningWindowState)
         {
-            auto windowState = [Window isMiniaturized] ? Minimized
-            : (IsZoomed() ? Maximized : Normal);
-            
-            if (windowState != _lastWindowState)
-            {
-                _lastWindowState = windowState;
-                
-                WindowEvents->WindowStateChanged(windowState);
-            }
+            WindowStateChanged();
         }
     }
     
@@ -678,22 +836,23 @@ protected:
     {
         unsigned long s = NSWindowStyleMaskBorderless;
 
-        switch (_hasDecorations)
+        switch (_decorations)
         {
             case SystemDecorationsNone:
+                s = s | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable;
                 break;
 
             case SystemDecorationsBorderOnly:
-                s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView;
+                s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskMiniaturizable;
                 break;
 
             case SystemDecorationsFull:
                 s = s | NSWindowStyleMaskTitled | NSWindowStyleMaskClosable | NSWindowStyleMaskMiniaturizable | NSWindowStyleMaskBorderless;
+                
                 if(_canResize)
                 {
                     s = s | NSWindowStyleMaskResizable;
                 }
-
                 break;
         }
 
@@ -1151,8 +1310,8 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     ComPtr<WindowBaseImpl> _parent;
     bool _canBecomeKeyAndMain;
     bool _closed;
-    NSMenu* _menu;
-    bool _isAppMenuApplied;
+    bool _isModal;
+    AvnMenu* _menu;
     double _lastScaling;
 }
 
@@ -1172,6 +1331,20 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     }
 }
 
+- (void)performClose:(id)sender
+{
+    if([[self delegate] respondsToSelector:@selector(windowShouldClose:)])
+    {
+        if(![[self delegate] windowShouldClose:self]) return;
+    }
+    else if([self respondsToSelector:@selector(windowShouldClose:)])
+    {
+        if(![self windowShouldClose:self]) return;
+    }
+    
+    [self close];
+}
+
 - (void)pollModalSession:(nonnull NSModalSession)session
 {
     auto response = [NSApp runModalSession:session];
@@ -1189,32 +1362,64 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
     }
 }
 
--(void) applyMenu:(NSMenu *)menu
+-(void) showWindowMenuWithAppMenu
 {
-    if(menu == nullptr)
+    if(_menu != nullptr)
     {
-        menu = [NSMenu new];
+        auto appMenuItem = ::GetAppMenuItem();
+        
+        if(appMenuItem != nullptr)
+        {
+            auto appMenu = [appMenuItem menu];
+            
+            [appMenu removeItem:appMenuItem];
+            
+            [_menu insertItem:appMenuItem atIndex:0];
+            
+            [_menu setHasGlobalMenuItem:true];
+        }
+        
+        [NSApp setMenu:_menu];
     }
+}
+
+-(void) showAppMenuOnly
+{
+    auto appMenuItem = ::GetAppMenuItem();
     
-    _menu = menu;
-    
-    if ([self isKeyWindow])
+    if(appMenuItem != nullptr)
     {
-        auto appMenu = ::GetAppMenuItem();
+        auto appMenu = ::GetAppMenu();
+        
+        auto nativeAppMenu = dynamic_cast<AvnAppMenu*>(appMenu);
         
-        if(appMenu != nullptr)
+        [[appMenuItem menu] removeItem:appMenuItem];
+        
+        if(_menu != nullptr)
         {
-            [[appMenu menu] removeItem:appMenu];
-            
-            [_menu insertItem:appMenu atIndex:0];
-            
-            _isAppMenuApplied = true;
+            [_menu setHasGlobalMenuItem:false];
         }
         
-        [NSApp setMenu:menu];
+        [nativeAppMenu->GetNative() addItem:appMenuItem];
+        
+        [NSApp setMenu:nativeAppMenu->GetNative()];
+    }
+    else
+    {
+        [NSApp setMenu:nullptr];
     }
 }
 
+-(void) applyMenu:(AvnMenu *)menu
+{
+    if(menu == nullptr)
+    {
+        menu = [AvnMenu new];
+    }
+    
+    _menu = menu;
+}
+
 -(void) setCanBecomeKeyAndMain
 {
     _canBecomeKeyAndMain = true;
@@ -1298,11 +1503,25 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
         auto ch = objc_cast<AvnWindow>(uch);
         if(ch == nil)
             continue;
+        
+        if(![ch isModal])
+            continue;
+        
         return FALSE;
     }
     return TRUE;
 }
 
+-(bool) isModal
+{
+    return _isModal;
+}
+
+-(void) setModal: (bool) isModal
+{
+    _isModal = isModal;
+}
+
 -(void)makeKeyWindow
 {
     if([self activateAppropriateChild: true])
@@ -1315,23 +1534,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
 {
     if([self activateAppropriateChild: true])
     {
-        if(_menu == nullptr)
-        {
-            _menu = [NSMenu new];
-        }
-        
-        auto appMenu = ::GetAppMenuItem();
-        
-        if(appMenu != nullptr)
-        {
-            [[appMenu menu] removeItem:appMenu];
-            
-            [_menu insertItem:appMenu atIndex:0];
-            
-            _isAppMenuApplied = true;
-        }
-        
-        [NSApp setMenu:_menu];
+        [self showWindowMenuWithAppMenu];
         
         _parent->BaseEvents->Activated();
         [super becomeKeyWindow];
@@ -1370,39 +1573,79 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
 
 - (void)windowDidResize:(NSNotification *)notification
 {
-    _parent->OnResized();
+    auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
+    
+    if(parent != nullptr)
+    {
+        parent->WindowStateChanged();
+    }
 }
 
-- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame
+- (void)windowWillExitFullScreen:(NSNotification *)notification
 {
-    return true;
+    auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
+    
+    if(parent != nullptr)
+    {
+        parent->StartStateTransition();
+    }
 }
 
--(void)resignKeyWindow
+- (void)windowDidExitFullScreen:(NSNotification *)notification
 {
-    if(_parent)
-        _parent->BaseEvents->Deactivated();
-    
-    auto appMenuItem = ::GetAppMenuItem();
+    auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
     
-    if(appMenuItem != nullptr)
+    if(parent != nullptr)
     {
-        auto appMenu = ::GetAppMenu();
+        parent->EndStateTransition();
         
-        auto nativeAppMenu = dynamic_cast<AvnAppMenu*>(appMenu);
-        
-        [[appMenuItem menu] removeItem:appMenuItem];
+        if(parent->Decorations() != SystemDecorationsFull && parent->WindowState() == Maximized)
+        {
+            NSRect screenRect = [[self screen] visibleFrame];
+            [self setFrame:screenRect display:YES];
+        }
         
-        [nativeAppMenu->GetNative() addItem:appMenuItem];
+        if(parent->WindowState() == Minimized)
+        {
+            [self miniaturize:nullptr];
+        }
         
-        [NSApp setMenu:nativeAppMenu->GetNative()];
+        parent->WindowStateChanged();
     }
-    else
+}
+
+- (void)windowWillEnterFullScreen:(NSNotification *)notification
+{
+    auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
+    
+    if(parent != nullptr)
     {
-        [NSApp setMenu:nullptr];
+        parent->StartStateTransition();
+    }
+}
+
+- (void)windowDidEnterFullScreen:(NSNotification *)notification
+{
+    auto parent = dynamic_cast<IWindowStateChanged*>(_parent.operator->());
+    
+    if(parent != nullptr)
+    {
+        parent->EndStateTransition();
+        parent->WindowStateChanged();
     }
+}
+
+- (BOOL)windowShouldZoom:(NSWindow *)window toFrame:(NSRect)newFrame
+{
+    return true;
+}
+
+-(void)resignKeyWindow
+{
+    if(_parent)
+        _parent->BaseEvents->Deactivated();
     
-    // remove window menu items from appmenu?
+    [self showAppMenuOnly];
     
     [super resignKeyWindow];
 }

+ 3 - 2
samples/ControlCatalog/MainView.xaml

@@ -59,8 +59,8 @@
       <TabItem Header="TreeView"><pages:TreeViewPage/></TabItem>
       <TabItem Header="Viewbox"><pages:ViewboxPage/></TabItem>
       <TabControl.Tag>
-        <StackPanel Width="115" Margin="8" HorizontalAlignment="Right" VerticalAlignment="Bottom">
-          <ComboBox x:Name="Decorations" SelectedIndex="0" Margin="0,0,0,8">
+        <StackPanel Width="115" Spacing="4" HorizontalAlignment="Right" VerticalAlignment="Bottom" Margin="8">
+          <ComboBox x:Name="Decorations" SelectedIndex="0">
             <ComboBoxItem>No Decorations</ComboBoxItem>
             <ComboBoxItem>Border Only</ComboBoxItem>
             <ComboBoxItem>Full Decorations</ComboBoxItem>
@@ -69,6 +69,7 @@
             <ComboBoxItem>Light</ComboBoxItem>
             <ComboBoxItem>Dark</ComboBoxItem>
           </ComboBox>
+          <ComboBox Items="{Binding WindowStates}" SelectedItem="{Binding WindowState}" />
         </StackPanel>
       </TabControl.Tag>
     </TabControl>

+ 21 - 3
samples/ControlCatalog/MainWindow.xaml

@@ -7,16 +7,16 @@
         xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
         xmlns:vm="clr-namespace:ControlCatalog.ViewModels"
         xmlns:v="clr-namespace:ControlCatalog.Views"
-        x:Class="ControlCatalog.MainWindow">
+        x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}">
 
   <NativeMenu.Menu>
     <NativeMenu>
       <NativeMenuItem Header="File">
         <NativeMenuItem.Menu>
           <NativeMenu>
-            <NativeMenuItem Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/>
+            <NativeMenuItem Icon="/Assets/test_icon.ico" Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/>
             <NativeMenuItemSeperator/>
-            <NativeMenuItem Header="Recent">
+            <NativeMenuItem Icon="/Assets/github_icon.png" Header="Recent">
               <NativeMenuItem.Menu>
                 <NativeMenu/>
               </NativeMenuItem.Menu>
@@ -36,6 +36,24 @@
           </NativeMenu>
         </NativeMenuItem.Menu>
       </NativeMenuItem>
+      <NativeMenuItem Header="Options">
+        <NativeMenuItem.Menu>
+          <NativeMenu>
+           <NativeMenuItem Header="Check Me (None)" 
+                            Command="{Binding ToggleMenuItemCheckedCommand}"
+                            ToggleType="None"
+                            IsChecked="{Binding IsMenuItemChecked}"  />
+            <NativeMenuItem Header="Check Me (CheckBox)" 
+                            Command="{Binding ToggleMenuItemCheckedCommand}"
+                            ToggleType="CheckBox"
+                            IsChecked="{Binding IsMenuItemChecked}"  />
+            <NativeMenuItem Header="Check Me (Radio)" 
+                            Command="{Binding ToggleMenuItemCheckedCommand}"
+                            ToggleType="Radio"
+                            IsChecked="{Binding IsMenuItemChecked}"  />
+          </NativeMenu>
+        </NativeMenuItem.Menu>
+      </NativeMenuItem>
     </NativeMenu>
   </NativeMenu.Menu>
 

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

@@ -29,6 +29,7 @@ namespace ControlCatalog
 
             DataContext = new MainWindowViewModel(_notificationArea);
             _recentMenu = ((NativeMenu.GetMenu(this).Items[0] as NativeMenuItem).Menu.Items[2] as NativeMenuItem).Menu;
+
             var mainMenu = this.FindControl<Menu>("MainMenu");
             mainMenu.AttachedToVisualTree += MenuAttached;
         }

+ 40 - 0
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

@@ -1,4 +1,5 @@
 using System.Reactive;
+using Avalonia.Controls;
 using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Controls.Notifications;
 using Avalonia.Dialogs;
@@ -10,6 +11,10 @@ namespace ControlCatalog.ViewModels
     {
         private IManagedNotificationManager _notificationManager;
 
+        private bool _isMenuItemChecked = true;
+        private WindowState _windowState;
+        private WindowState[] _windowStates;
+
         public MainWindowViewModel(IManagedNotificationManager notificationManager)
         {
             _notificationManager = notificationManager;
@@ -42,6 +47,33 @@ namespace ControlCatalog.ViewModels
             {
                 (App.Current.ApplicationLifetime as IClassicDesktopStyleApplicationLifetime).Shutdown();
             });
+
+            ToggleMenuItemCheckedCommand = ReactiveCommand.Create(() =>
+            {
+                IsMenuItemChecked = !IsMenuItemChecked;
+            });
+
+            WindowState = WindowState.Normal;
+
+            WindowStates = new WindowState[]
+            {
+                WindowState.Minimized,
+                WindowState.Normal,
+                WindowState.Maximized,
+                WindowState.FullScreen,
+            };
+        }
+
+        public WindowState WindowState
+        {
+            get { return _windowState; }
+            set { this.RaiseAndSetIfChanged(ref _windowState, value); }
+        }
+
+        public WindowState[] WindowStates
+        {
+            get { return _windowStates; }
+            set { this.RaiseAndSetIfChanged(ref _windowStates, value); }
         }
 
         public IManagedNotificationManager NotificationManager
@@ -50,6 +82,12 @@ namespace ControlCatalog.ViewModels
             set { this.RaiseAndSetIfChanged(ref _notificationManager, value); }
         }
 
+        public bool IsMenuItemChecked
+        {
+            get { return _isMenuItemChecked; }
+            set { this.RaiseAndSetIfChanged(ref _isMenuItemChecked, value); }
+        }
+
         public ReactiveCommand<Unit, Unit> ShowCustomManagedNotificationCommand { get; }
 
         public ReactiveCommand<Unit, Unit> ShowManagedNotificationCommand { get; }
@@ -59,5 +97,7 @@ namespace ControlCatalog.ViewModels
         public ReactiveCommand<Unit, Unit> AboutCommand { get; }
 
         public ReactiveCommand<Unit, Unit> ExitCommand { get; }
+
+        public ReactiveCommand<Unit, Unit> ToggleMenuItemCheckedCommand { get; }
     }
 }

+ 2 - 2
src/Avalonia.Animation/Animation.cs

@@ -251,10 +251,10 @@ namespace Avalonia.Animation
 
                     if (keyframe.TimingMode == KeyFrameTimingMode.TimeSpan)
                     {
-                        cue = new Cue(keyframe.KeyTime.Ticks / Duration.Ticks);
+                        cue = new Cue(keyframe.KeyTime.TotalSeconds / Duration.TotalSeconds);
                     }
 
-                    var newKF = new AnimatorKeyFrame(handler, cue);
+                    var newKF = new AnimatorKeyFrame(handler, cue, keyframe.KeySpline);
 
                     subscriptions.Add(newKF.BindSetter(setter, control));
 

+ 9 - 0
src/Avalonia.Animation/AnimatorKeyFrame.cs

@@ -24,11 +24,20 @@ namespace Avalonia.Animation
         {
             AnimatorType = animatorType;
             Cue = cue;
+            KeySpline = null;
+        }
+
+        public AnimatorKeyFrame(Type animatorType, Cue cue, KeySpline keySpline)
+        {
+            AnimatorType = animatorType;
+            Cue = cue;
+            KeySpline = keySpline;
         }
 
         internal bool isNeutral;
         public Type AnimatorType { get; }
         public Cue Cue { get; }
+        public KeySpline KeySpline { get; }
         public AvaloniaProperty Property { get; private set; }
 
         private object _value;

+ 3 - 0
src/Avalonia.Animation/Animators/Animator`1.cs

@@ -89,6 +89,9 @@ namespace Avalonia.Animation.Animators
             else
                 newValue = (T)lastKeyframe.Value;
 
+            if (lastKeyframe.KeySpline != null)
+                progress = lastKeyframe.KeySpline.GetSplineProgress(progress);
+
             return Interpolate(progress, oldValue, newValue);
         }
 

+ 20 - 0
src/Avalonia.Animation/KeyFrame.cs

@@ -19,6 +19,7 @@ namespace Avalonia.Animation
     {
         private TimeSpan _ktimeSpan;
         private Cue _kCue;
+        private KeySpline _kKeySpline;
 
         public KeyFrame()
         {
@@ -74,6 +75,25 @@ namespace Avalonia.Animation
             }
         }
 
+        /// <summary>
+        /// Gets or sets the KeySpline of this <see cref="KeyFrame"/>.
+        /// </summary>
+        /// <value>The key spline.</value>
+        public KeySpline KeySpline
+        {
+            get
+            {
+                return _kKeySpline;
+            }
+            set
+            {
+                _kKeySpline = value;
+                if (value != null && !value.IsValid())
+                {
+                    throw new ArgumentException($"{nameof(KeySpline)} must have X coordinates >= 0.0 and <= 1.0.");
+                }
+            }
+        }
 
     }
 

+ 349 - 0
src/Avalonia.Animation/KeySpline.cs

@@ -0,0 +1,349 @@
+using System;
+using System.Collections.Generic;
+using System.ComponentModel;
+using System.Globalization;
+using System.Text;
+using Avalonia;
+using Avalonia.Utilities;
+
+// Ported from WPF open-source code.
+// https://github.com/dotnet/wpf/blob/ae1790531c3b993b56eba8b1f0dd395a3ed7de75/src/Microsoft.DotNet.Wpf/src/PresentationCore/System/Windows/Media/Animation/KeySpline.cs
+
+namespace Avalonia.Animation
+{
+    /// <summary>
+    /// Determines how an animation is used based on a cubic bezier curve.
+    /// X1 and X2 must be between 0.0 and 1.0, inclusive.
+    /// See https://docs.microsoft.com/en-us/dotnet/api/system.windows.media.animation.keyspline
+    /// </summary>
+    [TypeConverter(typeof(KeySplineTypeConverter))]
+    public class KeySpline : AvaloniaObject
+    {
+        // Control points
+        private double _controlPointX1;
+        private double _controlPointY1;
+        private double _controlPointX2;
+        private double _controlPointY2;
+        private bool _isSpecified;
+        private bool _isDirty;
+
+        // The parameter that corresponds to the most recent time
+        private double _parameter;
+
+        // Cached coefficients
+        private double _Bx;        // 3*points[0].X
+        private double _Cx;        // 3*points[1].X
+        private double _Cx_Bx;     // 2*(Cx - Bx)
+        private double _three_Cx;  // 3 - Cx
+
+        private double _By;        // 3*points[0].Y
+        private double _Cy;        // 3*points[1].Y
+
+        // constants
+        private const double _accuracy = .001;   // 1/3 the desired accuracy in X
+        private const double _fuzz = .000001;    // computational zero
+
+        /// <summary>
+        /// Create a <see cref="KeySpline"/> with X1 = Y1 = 0 and X2 = Y2 = 1.
+        /// </summary>
+        public KeySpline()
+        {
+            _controlPointX1 = 0.0;
+            _controlPointY1 = 0.0;
+            _controlPointX2 = 1.0;
+            _controlPointY2 = 1.0;
+            _isDirty = true;
+        }
+
+        /// <summary>
+        /// Create a <see cref="KeySpline"/> with the given parameters
+        /// </summary>
+        /// <param name="x1">X coordinate for the first control point</param>
+        /// <param name="y1">Y coordinate for the first control point</param>
+        /// <param name="x2">X coordinate for the second control point</param>
+        /// <param name="y2">Y coordinate for the second control point</param>
+        public KeySpline(double x1, double y1, double x2, double y2)
+        {
+            _controlPointX1 = x1;
+            _controlPointY1 = y1;
+            _controlPointX2 = x2;
+            _controlPointY2 = y2;
+            _isDirty = true;
+        }
+
+        /// <summary>
+        /// Parse a <see cref="KeySpline"/> from a string. The string
+        /// needs to contain 4 values in it for the 2 control points.
+        /// </summary>
+        /// <param name="value">string with 4 values in it</param>
+        /// <param name="culture">culture of the string</param>
+        /// <exception cref="FormatException">Thrown if the string does not have 4 values</exception>
+        /// <returns>A <see cref="KeySpline"/> with the appropriate values set</returns>
+        public static KeySpline Parse(string value, CultureInfo culture)
+        {
+            using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid KeySpline."))
+            {
+                return new KeySpline(tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble(), tokenizer.ReadDouble());
+            }
+        }
+
+        /// <summary>
+        /// X coordinate of the first control point
+        /// </summary>
+        public double ControlPointX1
+        {
+            get => _controlPointX1;
+            set
+            {
+                if (IsValidXValue(value))
+                {
+                    _controlPointX1 = value;
+                }
+                else
+                {
+                    throw new ArgumentException("Invalid KeySpline X1 value. Must be >= 0.0 and <= 1.0.");
+                }
+            }
+        }
+
+        /// <summary>
+        /// Y coordinate of the first control point
+        /// </summary>
+        public double ControlPointY1
+        {
+            get => _controlPointY1;
+            set => _controlPointY1 = value;
+        }
+
+        /// <summary>
+        /// X coordinate of the second control point
+        /// </summary>
+        public double ControlPointX2
+        {
+            get => _controlPointX2;
+            set
+            {
+                if (IsValidXValue(value))
+                {
+                    _controlPointX2 = value;
+                }
+                else
+                {
+                    throw new ArgumentException("Invalid KeySpline X2 value. Must be >= 0.0 and <= 1.0.");
+                }
+            }
+        }
+
+        /// <summary>
+        /// Y coordinate of the second control point
+        /// </summary>
+        public double ControlPointY2
+        {
+            get => _controlPointY2;
+            set => _controlPointY2 = value;
+        }
+
+        /// <summary>
+        /// Calculates spline progress from a linear progress.
+        /// </summary>
+        /// <param name="linearProgress">the linear progress</param>
+        /// <returns>the spline progress</returns>
+        public double GetSplineProgress(double linearProgress)
+        {
+            if (_isDirty)
+            {
+                Build();
+            }
+
+            if (!_isSpecified)
+            {
+                return linearProgress;
+            }
+            else
+            {
+                SetParameterFromX(linearProgress);
+
+                return GetBezierValue(_By, _Cy, _parameter);
+            }
+        }
+
+        /// <summary>
+        /// Check to see whether the <see cref="KeySpline"/> is valid by looking
+        /// at its X values.
+        /// </summary>
+        /// <returns>true if the X values for this <see cref="KeySpline"/> fall in 
+        /// acceptable range; false otherwise.</returns>
+        public bool IsValid()
+        {
+            return IsValidXValue(_controlPointX1) && IsValidXValue(_controlPointX2);
+        }
+
+        /// <summary>
+        /// 
+        /// </summary>
+        /// <param name="value"></param>
+        /// <returns></returns>
+        private bool IsValidXValue(double value)
+        {
+            return value >= 0.0 && value <= 1.0;
+        }
+
+        /// <summary>
+        /// Compute cached coefficients.
+        /// </summary>
+        private void Build()
+        {
+            if (_controlPointX1 == 0 && _controlPointY1 == 0 && _controlPointX2 == 1 && _controlPointY2 == 1)
+            {
+                // This KeySpline would have no effect on the progress.
+                _isSpecified = false;
+            }
+            else
+            {
+                _isSpecified = true;
+
+                _parameter = 0;
+
+                // X coefficients
+                _Bx = 3 * _controlPointX1;
+                _Cx = 3 * _controlPointX2;
+                _Cx_Bx = 2 * (_Cx - _Bx);
+                _three_Cx = 3 - _Cx;
+
+                // Y coefficients
+                _By = 3 * _controlPointY1;
+                _Cy = 3 * _controlPointY2;
+            }
+
+            _isDirty = false;
+        }
+
+        /// <summary>
+        /// Get an X or Y value with the Bezier formula.
+        /// </summary>
+        /// <param name="b">the second Bezier coefficient</param>
+        /// <param name="c">the third Bezier coefficient</param>
+        /// <param name="t">the parameter value to evaluate at</param>
+        /// <returns>the value of the Bezier function at the given parameter</returns>
+        static private double GetBezierValue(double b, double c, double t)
+        {
+            double s = 1.0 - t;
+            double t2 = t * t;
+
+            return b * t * s * s + c * t2 * s + t2 * t;
+        }
+
+        /// <summary>
+        /// Get X and dX/dt at a given parameter
+        /// </summary>
+        /// <param name="t">the parameter value to evaluate at</param>
+        /// <param name="x">the value of x there</param>
+        /// <param name="dx">the value of dx/dt there</param>
+        private void GetXAndDx(double t, out double x, out double dx)
+        {
+            double s = 1.0 - t;
+            double t2 = t * t;
+            double s2 = s * s;
+
+            x = _Bx * t * s2 + _Cx * t2 * s + t2 * t;
+            dx = _Bx * s2 + _Cx_Bx * s * t + _three_Cx * t2;
+        }
+
+        /// <summary>
+        /// Compute the parameter value that corresponds to a given X value, using a modified
+        /// clamped Newton-Raphson algorithm to solve the equation X(t) - time = 0. We make 
+        /// use of some known properties of this particular function:
+        /// * We are only interested in solutions in the interval [0,1]
+        /// * X(t) is increasing, so we can assume that if X(t) > time t > solution.  We use
+        ///   that to clamp down the search interval with every probe.
+        /// * The derivative of X and Y are between 0 and 3.
+        /// </summary>
+        /// <param name="time">the time, scaled to fit in [0,1]</param>
+        private void SetParameterFromX(double time)
+        {
+            // Dynamic search interval to clamp with
+            double bottom = 0;
+            double top = 1;
+
+            if (time == 0)
+            {
+                _parameter = 0;
+            }
+            else if (time == 1)
+            {
+                _parameter = 1;
+            }
+            else
+            {
+                // Loop while improving the guess
+                while (top - bottom > _fuzz)
+                {
+                    double x, dx, absdx;
+
+                    // Get x and dx/dt at the current parameter
+                    GetXAndDx(_parameter, out x, out dx);
+                    absdx = Math.Abs(dx);
+
+                    // Clamp down the search interval, relying on the monotonicity of X(t)
+                    if (x > time)
+                    {
+                        top = _parameter;      // because parameter > solution
+                    }
+                    else
+                    {
+                        bottom = _parameter;  // because parameter < solution
+                    }
+
+                    // The desired accuracy is in ultimately in y, not in x, so the
+                    // accuracy needs to be multiplied by dx/dy = (dx/dt) / (dy/dt).
+                    // But dy/dt <=3, so we omit that
+                    if (Math.Abs(x - time) < _accuracy * absdx)
+                    {
+                        break; // We're there
+                    }
+
+                    if (absdx > _fuzz)
+                    {
+                        // Nonzero derivative, use Newton-Raphson to obtain the next guess
+                        double next = _parameter - (x - time) / dx;
+
+                        // If next guess is out of the search interval then clamp it in
+                        if (next >= top)
+                        {
+                            _parameter = (_parameter + top) / 2;
+                        }
+                        else if (next <= bottom)
+                        {
+                            _parameter = (_parameter + bottom) / 2;
+                        }
+                        else
+                        {
+                            // Next guess is inside the search interval, accept it
+                            _parameter = next;
+                        }
+                    }
+                    else    // Zero derivative, halve the search interval
+                    {
+                        _parameter = (bottom + top) / 2;
+                    }
+                }
+            }
+        }
+    }
+
+    /// <summary>
+    /// Converts string values to <see cref="KeySpline"/> values
+    /// </summary>
+    public class KeySplineTypeConverter : TypeConverter
+    {
+        public override bool CanConvertFrom(ITypeDescriptorContext context, Type sourceType)
+        {
+            return sourceType == typeof(string);
+        }
+
+        public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
+        {
+            return KeySpline.Parse((string)value, culture);
+        }
+    }
+}

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

@@ -55,7 +55,7 @@ namespace Avalonia.Controls
                                 binding.Mode = BindingMode.TwoWay;
                             } 
 
-                            if (binding.Converter == null)
+                            if (binding.Converter == null && string.IsNullOrEmpty(binding.StringFormat))
                             {
                                 binding.Converter = DataGridValueConverter.Instance;
                             }

+ 3 - 0
src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs

@@ -269,6 +269,9 @@ namespace Avalonia.Controls.Primitives
                 // Since we didn't know the final widths of the columns until we resized,
                 // we waited until now to measure each cell
                 double leftEdge = 0;
+                if (autoSizeHeight)
+                    DesiredHeight = 0;
+
                 foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetVisibleColumns())
                 {
                     DataGridCell cell = OwningRow.Cells[column.Index];

+ 18 - 4
src/Avalonia.Controls/ComboBox.cs

@@ -234,6 +234,23 @@ namespace Avalonia.Controls
             base.OnTemplateApplied(e);
         }
 
+        /// <summary>
+        /// Called when the ComboBox popup is closed, with the <see cref="PopupClosedEventArgs"/>
+        /// that caused the popup to close.
+        /// </summary>
+        /// <param name="e">The event args.</param>
+        /// <remarks>
+        /// This method can be overridden to control whether the event that caused the popup to close
+        /// is swallowed or passed through.
+        /// </remarks>
+        protected virtual void PopupClosedOverride(PopupClosedEventArgs e)
+        {
+            if (e.CloseEvent is PointerEventArgs pointerEvent)
+            {
+                pointerEvent.Handled = true;
+            }
+        }
+
         internal void ItemFocused(ComboBoxItem dropDownItem)
         {
             if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid)
@@ -247,10 +264,7 @@ namespace Avalonia.Controls
             _subscriptionsOnOpen?.Dispose();
             _subscriptionsOnOpen = null;
 
-            if (e.CloseEvent is PointerEventArgs pointerEvent)
-            {
-                pointerEvent.Handled = true;
-            }
+            PopupClosedOverride(e);
 
             if (CanFocus(this))
             {

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

@@ -0,0 +1,7 @@
+namespace Avalonia.Controls
+{
+    public interface INativeMenuExporterEventsImplBridge
+    {
+        void RaiseNeedsUpdate ();
+    }
+}

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

@@ -0,0 +1,7 @@
+namespace Avalonia.Controls
+{
+    public interface INativeMenuItemExporterEventsImplBridge
+    {
+        void RaiseClicked ();
+    }
+}

+ 14 - 6
src/Avalonia.Controls/NativeMenu.cs

@@ -3,13 +3,11 @@ using System.Collections;
 using System.Collections.Generic;
 using System.Collections.Specialized;
 using Avalonia.Collections;
-using Avalonia.Data;
-using Avalonia.LogicalTree;
 using Avalonia.Metadata;
 
 namespace Avalonia.Controls
 {
-    public partial class NativeMenu : AvaloniaObject, IEnumerable<NativeMenuItemBase>
+    public partial class NativeMenu : AvaloniaObject, IEnumerable<NativeMenuItemBase>, INativeMenuExporterEventsImplBridge
     {
         private readonly AvaloniaList<NativeMenuItemBase> _items =
             new AvaloniaList<NativeMenuItemBase> { ResetBehavior = ResetBehavior.Remove };
@@ -17,12 +15,22 @@ namespace Avalonia.Controls
         [Content]
         public IList<NativeMenuItemBase> Items => _items;
 
+        /// <summary>
+        /// Raised when the user clicks the menu and before its opened. Use this event to update the menu dynamically.
+        /// </summary>
+        public event EventHandler<EventArgs> Opening;
+
         public NativeMenu()
         {
             _items.Validate = Validator;
             _items.CollectionChanged += ItemsChanged;
         }
 
+        void INativeMenuExporterEventsImplBridge.RaiseNeedsUpdate()
+        {
+            Opening?.Invoke(this, EventArgs.Empty);
+        }
+
         private void Validator(NativeMenuItemBase obj)
         {
             if (obj.Parent != null)
@@ -31,10 +39,10 @@ namespace Avalonia.Controls
 
         private void ItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
         {
-            if(e.OldItems!=null)
+            if (e.OldItems != null)
                 foreach (NativeMenuItemBase i in e.OldItems)
                     i.Parent = null;
-            if(e.NewItems!=null)
+            if (e.NewItems != null)
                 foreach (NativeMenuItemBase i in e.NewItems)
                     i.Parent = this;
         }
@@ -49,7 +57,7 @@ namespace Avalonia.Controls
         }
 
         public void Add(NativeMenuItemBase item) => _items.Add(item);
-        
+
         public IEnumerator<NativeMenuItemBase> GetEnumerator() => _items.GetEnumerator();
 
         IEnumerator IEnumerable.GetEnumerator()

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

@@ -30,7 +30,7 @@ namespace Avalonia.Controls
 
         private static void OnMenuItemClick(object sender, RoutedEventArgs e)
         {
-            (((MenuItem)sender).DataContext as NativeMenuItem)?.RaiseClick();
+            (((MenuItem)sender).DataContext as INativeMenuItemExporterEventsImplBridge)?.RaiseClicked();
         }
     }
 }

+ 80 - 38
src/Avalonia.Controls/NativeMenuItem.cs

@@ -1,15 +1,20 @@
 using System;
 using System.Windows.Input;
 using Avalonia.Input;
+using Avalonia.Media.Imaging;
 using Avalonia.Utilities;
 
 namespace Avalonia.Controls
 {
-    public class NativeMenuItem : NativeMenuItemBase
+    public class NativeMenuItem : NativeMenuItemBase, INativeMenuItemExporterEventsImplBridge
     {
         private string _header;
         private KeyGesture _gesture;
-        private bool _enabled = true;
+        private bool _isEnabled = true;
+        private ICommand _command;
+        private bool _isChecked = false;
+        private NativeMenuItemToggleType _toggleType;
+        private IBitmap _icon;
 
         private NativeMenu _menu;
 
@@ -55,13 +60,7 @@ namespace Avalonia.Controls
         }
 
         public static readonly DirectProperty<NativeMenuItem, NativeMenu> MenuProperty =
-            AvaloniaProperty.RegisterDirect<NativeMenuItem, NativeMenu>(nameof(Menu), o => o._menu,
-                (o, v) =>
-                {
-                    if (v.Parent != null && v.Parent != o)
-                        throw new InvalidOperationException("NativeMenu already has a parent");
-                    o._menu = v;
-                });
+            AvaloniaProperty.RegisterDirect<NativeMenuItem, NativeMenu>(nameof(Menu), o => o.Menu, (o, v) => o.Menu = v);
 
         public NativeMenu Menu
         {
@@ -74,39 +73,63 @@ namespace Avalonia.Controls
             }
         }
 
+        public static readonly DirectProperty<NativeMenuItem, IBitmap> IconProperty =
+            AvaloniaProperty.RegisterDirect<NativeMenuItem, IBitmap>(nameof(Icon), o => o.Icon, (o, v) => o.Icon = v);
+
+
+        public IBitmap Icon
+        {
+            get => _icon;
+            set => SetAndRaise(IconProperty, ref _icon, value);
+        }  
+
         public static readonly DirectProperty<NativeMenuItem, string> HeaderProperty =
-            AvaloniaProperty.RegisterDirect<NativeMenuItem, string>(nameof(Header), o => o._header, (o, v) => o._header = v);
+            AvaloniaProperty.RegisterDirect<NativeMenuItem, string>(nameof(Header), o => o.Header, (o, v) => o.Header = v);
 
         public string Header
         {
-            get => GetValue(HeaderProperty);
-            set => SetValue(HeaderProperty, value);
+            get => _header;
+            set => SetAndRaise(HeaderProperty, ref _header, value);
         }
 
         public static readonly DirectProperty<NativeMenuItem, KeyGesture> GestureProperty =
-            AvaloniaProperty.RegisterDirect<NativeMenuItem, KeyGesture>(nameof(Gesture), o => o._gesture, (o, v) => o._gesture = v);
+            AvaloniaProperty.RegisterDirect<NativeMenuItem, KeyGesture>(nameof(Gesture), o => o.Gesture, (o, v) => o.Gesture = v);
 
         public KeyGesture Gesture
         {
-            get => GetValue(GestureProperty);
-            set => SetValue(GestureProperty, value);
+            get => _gesture;
+            set => SetAndRaise(GestureProperty, ref _gesture, value);
         }
 
-        private ICommand _command;
+        public static readonly DirectProperty<NativeMenuItem, bool> IsCheckedProperty =
+            AvaloniaProperty.RegisterDirect<NativeMenuItem, bool>(
+                nameof(IsChecked),
+                o => o.IsChecked,
+                (o, v) => o.IsChecked = v);
+
+        public bool IsChecked
+        {
+            get => _isChecked;
+            set => SetAndRaise(IsCheckedProperty, ref _isChecked, value);
+        }
+        
+        public static readonly DirectProperty<NativeMenuItem, NativeMenuItemToggleType> ToggleTypeProperty =
+            AvaloniaProperty.RegisterDirect<NativeMenuItem, NativeMenuItemToggleType>(
+                nameof(ToggleType),
+                o => o.ToggleType,
+                (o, v) => o.ToggleType = v);
+
+        public NativeMenuItemToggleType ToggleType
+        {
+            get => _toggleType;
+            set => SetAndRaise(ToggleTypeProperty, ref _toggleType, value);
+        }
 
         public static readonly DirectProperty<NativeMenuItem, ICommand> CommandProperty =
-           AvaloniaProperty.RegisterDirect<NativeMenuItem, ICommand>(nameof(Command),
-               o => o._command, (o, v) =>
-               {
-                   if (o._command != null)
-                       WeakSubscriptionManager.Unsubscribe(o._command,
-                           nameof(ICommand.CanExecuteChanged), o._canExecuteChangedSubscriber);
-                   o._command = v;
-                   if (o._command != null)
-                       WeakSubscriptionManager.Subscribe(o._command,
-                           nameof(ICommand.CanExecuteChanged), o._canExecuteChangedSubscriber);
-                   o.CanExecuteChanged();
-               });
+            Button.CommandProperty.AddOwner<NativeMenuItem>(
+                menuItem => menuItem.Command,
+                (menuItem, command) => menuItem.Command = command,
+                enableDataValidation: true);
 
         /// <summary>
         /// Defines the <see cref="CommandParameter"/> property.
@@ -114,27 +137,39 @@ namespace Avalonia.Controls
         public static readonly StyledProperty<object> CommandParameterProperty =
             Button.CommandParameterProperty.AddOwner<MenuItem>();
 
-        public static readonly DirectProperty<NativeMenuItem, bool> EnabledProperty =
-           AvaloniaProperty.RegisterDirect<NativeMenuItem, bool>(nameof(Enabled), o => o._enabled,
-               (o, v) => o._enabled = v, true);
+        public static readonly DirectProperty<NativeMenuItem, bool> IsEnabledProperty =
+           AvaloniaProperty.RegisterDirect<NativeMenuItem, bool>(nameof(IsEnabled), o => o.IsEnabled, (o, v) => o.IsEnabled = v, true);
 
-        public bool Enabled
+        public bool IsEnabled
         {
-            get => GetValue(EnabledProperty);
-            set => SetValue(EnabledProperty, value);
+            get => _isEnabled;
+            set => SetAndRaise(IsEnabledProperty, ref _isEnabled, value);
         }
 
         void CanExecuteChanged()
         {
-            Enabled = _command?.CanExecute(null) ?? true;
+            IsEnabled = _command?.CanExecute(null) ?? true;
         }
 
         public bool HasClickHandlers => Clicked != null;
 
         public ICommand Command
         {
-            get => GetValue(CommandProperty);
-            set => SetValue(CommandProperty, value);
+            get => _command;
+            set
+            {
+                if (_command != null)
+                    WeakSubscriptionManager.Unsubscribe(_command,
+                        nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber);
+
+                SetAndRaise(CommandProperty, ref _command, value);
+
+                if (_command != null)
+                    WeakSubscriptionManager.Subscribe(_command,
+                        nameof(ICommand.CanExecuteChanged), _canExecuteChangedSubscriber);
+
+                CanExecuteChanged();
+            }
         }
 
         /// <summary>
@@ -149,7 +184,7 @@ namespace Avalonia.Controls
 
         public event EventHandler Clicked;
 
-        public void RaiseClick()
+        void INativeMenuItemExporterEventsImplBridge.RaiseClicked()
         {
             Clicked?.Invoke(this, new EventArgs());
 
@@ -159,4 +194,11 @@ namespace Avalonia.Controls
             }
         }
     }
+    
+    public enum NativeMenuItemToggleType
+    {
+        None,
+        CheckBox,
+        Radio
+    }
 }

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

@@ -392,7 +392,7 @@ namespace Avalonia.Controls.Platform
             {
                 var control = e.Source as ILogical;
 
-                if (!Menu.IsLogicalParentOf(control))
+                if (!Menu.IsLogicalAncestorOf(control))
                 {
                     Menu.Close();
                 }

+ 2 - 3
src/Avalonia.Controls/Platform/ISystemDialogImpl.cs

@@ -1,5 +1,4 @@
 using System.Threading.Tasks;
-using Avalonia.Platform;
 
 namespace Avalonia.Controls.Platform
 {
@@ -14,8 +13,8 @@ namespace Avalonia.Controls.Platform
         /// <param name="dialog">The details of the file dialog to show.</param>
         /// <param name="parent">The parent window.</param>
         /// <returns>A task returning the selected filenames.</returns>
-        Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent);
+        Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent);
 
-        Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent);   
+        Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent);
     }
 }

+ 22 - 24
src/Avalonia.Controls/Presenters/TextPresenter.cs

@@ -74,16 +74,15 @@ namespace Avalonia.Controls.Presenters
 
         static TextPresenter()
         {
-            AffectsRender<TextPresenter>(PasswordCharProperty,
-                SelectionBrushProperty, SelectionForegroundBrushProperty,
-                SelectionStartProperty, SelectionEndProperty);
+            AffectsRender<TextPresenter>(SelectionBrushProperty);
 
-            Observable.Merge(
-                TextProperty.Changed,
-                SelectionStartProperty.Changed,
-                SelectionEndProperty.Changed,
-                PasswordCharProperty.Changed
-            ).AddClassHandler<TextPresenter>((x,_) => x.InvalidateFormattedText());
+            Observable.Merge(TextProperty.Changed, TextBlock.ForegroundProperty.Changed,
+                TextAlignmentProperty.Changed, TextWrappingProperty.Changed,
+                TextBlock.FontSizeProperty.Changed, TextBlock.FontStyleProperty.Changed, 
+                TextBlock.FontWeightProperty.Changed, TextBlock.FontFamilyProperty.Changed,
+                SelectionStartProperty.Changed, SelectionEndProperty.Changed,
+                SelectionForegroundBrushProperty.Changed, PasswordCharProperty.Changed
+            ).AddClassHandler<TextPresenter>((x, _) => x.InvalidateFormattedText());
 
             CaretIndexProperty.Changed.AddClassHandler<TextPresenter>((x, e) => x.CaretIndexChanged((int)e.NewValue));
         }
@@ -184,7 +183,7 @@ namespace Avalonia.Controls.Presenters
         {
             get
             {
-                return _formattedText ?? (_formattedText = CreateFormattedText(Bounds.Size, Text));
+                return _formattedText ?? (_formattedText = CreateFormattedText());
             }
         }
 
@@ -219,7 +218,7 @@ namespace Avalonia.Controls.Presenters
             get => GetValue(SelectionForegroundBrushProperty);
             set => SetValue(SelectionForegroundBrushProperty, value);
         }
-        
+
         public IBrush CaretBrush
         {
             get => GetValue(CaretBrushProperty);
@@ -284,13 +283,9 @@ namespace Avalonia.Controls.Presenters
         /// </summary>
         protected void InvalidateFormattedText()
         {
-            if (_formattedText != null)
-            {
-                _constraint = _formattedText.Constraint;
-                _formattedText = null;
-            }
+            _formattedText = null;
 
-            InvalidateVisual();
+            InvalidateMeasure();
         }
 
         /// <summary>
@@ -307,6 +302,7 @@ namespace Avalonia.Controls.Presenters
             }
 
             FormattedText.Constraint = Bounds.Size;
+
             context.DrawText(Foreground, new Point(), FormattedText);
         }
 
@@ -424,20 +420,20 @@ namespace Avalonia.Controls.Presenters
         /// <summary>
         /// Creates the <see cref="FormattedText"/> used to render the text.
         /// </summary>
-        /// <param name="constraint">The constraint of the text.</param>
-        /// <param name="text">The text to generated the <see cref="FormattedText"/> for.</param>
         /// <returns>A <see cref="FormattedText"/> object.</returns>
-        protected virtual FormattedText CreateFormattedText(Size constraint, string text)
+        protected virtual FormattedText CreateFormattedText()
         {
             FormattedText result = null;
 
+            var text = Text;
+
             if (PasswordChar != default(char))
             {
-                result = CreateFormattedTextInternal(constraint, new string(PasswordChar, text?.Length ?? 0));
+                result = CreateFormattedTextInternal(_constraint, new string(PasswordChar, text?.Length ?? 0));
             }
             else
             {
-                result = CreateFormattedTextInternal(constraint, text);
+                result = CreateFormattedTextInternal(_constraint, text);
             }
 
             var selectionStart = SelectionStart;
@@ -467,13 +463,15 @@ namespace Avalonia.Controls.Presenters
             {
                 if (TextWrapping == TextWrapping.Wrap)
                 {
-                    FormattedText.Constraint = new Size(availableSize.Width, double.PositiveInfinity);
+                    _constraint = new Size(availableSize.Width, double.PositiveInfinity);
                 }
                 else
                 {
-                    FormattedText.Constraint = Size.Infinity;
+                    _constraint = Size.Infinity;
                 }
 
+                _formattedText = null;
+
                 return FormattedText.Bounds.Size;
             }
 

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

@@ -32,7 +32,7 @@ namespace Avalonia.Controls
             if(parent == null)
                 throw new ArgumentNullException(nameof(parent));
             return ((await AvaloniaLocator.Current.GetService<ISystemDialogImpl>()
-                 .ShowFileDialogAsync(this, parent?.PlatformImpl)) ??
+                 .ShowFileDialogAsync(this, parent)) ??
              Array.Empty<string>()).FirstOrDefault();
         }
     }
@@ -45,7 +45,7 @@ namespace Avalonia.Controls
         {
             if(parent == null)
                 throw new ArgumentNullException(nameof(parent));
-            return AvaloniaLocator.Current.GetService<ISystemDialogImpl>().ShowFileDialogAsync(this, parent?.PlatformImpl);
+            return AvaloniaLocator.Current.GetService<ISystemDialogImpl>().ShowFileDialogAsync(this, parent);
         }
     }
 
@@ -61,7 +61,7 @@ namespace Avalonia.Controls
         {
             if(parent == null)
                 throw new ArgumentNullException(nameof(parent));
-            return AvaloniaLocator.Current.GetService<ISystemDialogImpl>().ShowFolderDialogAsync(this, parent?.PlatformImpl);
+            return AvaloniaLocator.Current.GetService<ISystemDialogImpl>().ShowFolderDialogAsync(this, parent);
         }
     }
 

+ 23 - 18
src/Avalonia.Controls/Window.cs

@@ -484,22 +484,12 @@ namespace Avalonia.Controls
         /// <returns>.
         /// A task that can be used to retrieve the result of the dialog when it closes.
         /// </returns>
-        public Task<TResult> ShowDialog<TResult>(Window owner) => ShowDialog<TResult>(owner.PlatformImpl);
-
-        /// <summary>
-        /// Shows the window as a dialog.
-        /// </summary>
-        /// <typeparam name="TResult">
-        /// The type of the result produced by the dialog.
-        /// </typeparam>
-        /// <param name="owner">The dialog's owner window.</param>
-        /// <returns>.
-        /// A task that can be used to retrieve the result of the dialog when it closes.
-        /// </returns>
-        public Task<TResult> ShowDialog<TResult>(IWindowImpl owner)
+        public Task<TResult> ShowDialog<TResult>(Window owner)
         {
             if (owner == null)
+            {
                 throw new ArgumentNullException(nameof(owner));
+            }
 
             if (IsVisible)
             {
@@ -510,29 +500,44 @@ namespace Avalonia.Controls
 
             EnsureInitialized();
             IsVisible = true;
+
+            var initialSize = new Size(
+                double.IsNaN(Width) ? ClientSize.Width : Width,
+                double.IsNaN(Height) ? ClientSize.Height : Height);
+
+            if (initialSize != ClientSize)
+            {
+                using (BeginAutoSizing())
+                {
+                    PlatformImpl?.Resize(initialSize);
+                }
+            }
+
             LayoutManager.ExecuteInitialLayoutPass(this);
 
             var result = new TaskCompletionSource<TResult>();
 
             using (BeginAutoSizing())
             {
-
-                PlatformImpl?.ShowDialog(owner);
+                PlatformImpl?.ShowDialog(owner.PlatformImpl);
 
                 Renderer?.Start();
+
                 Observable.FromEventPattern<EventHandler, EventArgs>(
-                    x => this.Closed += x,
-                    x => this.Closed -= x)
+                        x => Closed += x,
+                        x => Closed -= x)
                     .Take(1)
                     .Subscribe(_ =>
                     {
                         owner.Activate();
                         result.SetResult((TResult)(_dialogResult ?? default(TResult)));
                     });
+
                 OnOpened(EventArgs.Empty);
             }
 
-            SetWindowStartupLocation(owner);
+            SetWindowStartupLocation(owner.PlatformImpl);
+
             return result.Task;
         }
 

+ 5 - 0
src/Avalonia.Controls/WindowState.cs

@@ -19,5 +19,10 @@ namespace Avalonia.Controls
         /// The window is maximized.
         /// </summary>
         Maximized,
+
+        /// <summary>
+        /// The window is fullscreen.
+        /// </summary>
+        FullScreen,
     }
 }

+ 2 - 2
src/Avalonia.DesignerSupport/Remote/Stubs.cs

@@ -166,10 +166,10 @@ namespace Avalonia.DesignerSupport.Remote
 
     class SystemDialogsStub : ISystemDialogImpl
     {
-        public Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent) =>
+        public Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent) =>
             Task.FromResult((string[])null);
 
-        public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent) =>
+        public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent) =>
             Task.FromResult((string)null);
     }
 

+ 1 - 1
src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs

@@ -34,7 +34,7 @@ namespace Avalonia.Dialogs
                 return;
             }
 
-            var isQuickLink = _quickLinksRoot.IsLogicalParentOf(e.Source as Control);
+            var isQuickLink = _quickLinksRoot.IsLogicalAncestorOf(e.Source as Control);
             if (e.ClickCount == 2 || isQuickLink)
             {
                 if (model.ItemType == ManagedFileChooserItemType.File)

+ 4 - 8
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

@@ -1,19 +1,15 @@
-using System;
 using System.Linq;
 using System.Threading.Tasks;
-using Avalonia;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform;
-using Avalonia.Dialogs;
-using Avalonia.Platform;
 
 namespace Avalonia.Dialogs
 {
     public static class ManagedFileDialogExtensions
     {
-        class ManagedSystemDialogImpl<T> : ISystemDialogImpl where T : Window, new()
+        private class ManagedSystemDialogImpl<T> : ISystemDialogImpl where T : Window, new()
         {
-            async Task<string[]> Show(SystemDialog d, IWindowImpl parent)
+            async Task<string[]> Show(SystemDialog d, Window parent)
             {
                 var model = new ManagedFileChooserViewModel((FileSystemDialog)d);
 
@@ -39,12 +35,12 @@ namespace Avalonia.Dialogs
                 return result;
             }
 
-            public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
+            public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
             {
                 return await Show(dialog, parent);
             }
 
-            public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
+            public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
             {
                 return (await Show(dialog, parent))?.FirstOrDefault();
             }

+ 30 - 5
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

@@ -1,6 +1,7 @@
 using System;
 using System.Collections.Generic;
 using System.Collections.Specialized;
+using System.IO;
 using System.Reactive.Disposables;
 using System.Threading.Tasks;
 using Avalonia.Controls;
@@ -184,7 +185,7 @@ namespace Avalonia.FreeDesktop
 
             private static string[] AllProperties = new[]
             {
-                "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display"
+                "type", "label", "enabled", "visible", "shortcut", "toggle-type", "children-display", "toggle-state", "icon-data"
             };
             
             object GetProperty((NativeMenuItemBase item, NativeMenu menu) i, string name)
@@ -210,7 +211,7 @@ namespace Avalonia.FreeDesktop
                             return null;
                         if (item.Menu != null && item.Menu.Items.Count == 0)
                             return false;
-                        if (item.Enabled == false)
+                        if (item.IsEnabled == false)
                             return false;
                         return null;
                     }
@@ -234,6 +235,30 @@ namespace Avalonia.FreeDesktop
                         return new[] { lst.ToArray() };
                     }
 
+                    if (name == "toggle-type")
+                    {
+                        if (item.ToggleType == NativeMenuItemToggleType.CheckBox)
+                            return "checkmark";
+                        if (item.ToggleType == NativeMenuItemToggleType.Radio)
+                            return "radio";
+                    }
+
+                    if (name == "toggle-state")
+                    {
+                        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();
+                        }
+                    }
+
                     if (name == "children-display")
                         return menu != null ? "submenu" : null;
                 }
@@ -319,10 +344,10 @@ namespace Avalonia.FreeDesktop
                 {
                     var item = GetMenu(id).item;
 
-                    if (item is NativeMenuItem menuItem)
+                    if (item is NativeMenuItem menuItem && item is INativeMenuItemExporterEventsImplBridge bridge)
                     {
-                        if (menuItem?.Enabled == true)
-                            menuItem.RaiseClick();
+                        if (menuItem?.IsEnabled == true)
+                            bridge?.RaiseClicked();
                     }
                 }
             }

+ 56 - 359
src/Avalonia.Native/AvaloniaNativeMenuExporter.cs

@@ -1,185 +1,21 @@
 using System;
-using System.Collections.Generic;
-using System.Collections.Specialized;
-using System.Linq;
-using System.Text;
-using Avalonia.Collections;
 using Avalonia.Controls;
+using Avalonia.Controls.ApplicationLifetimes;
 using Avalonia.Controls.Platform;
-using Avalonia.Input;
+using Avalonia.Dialogs;
 using Avalonia.Native.Interop;
-using Avalonia.Platform.Interop;
 using Avalonia.Threading;
-using Avalonia.Dialogs;
-using Avalonia.Controls.ApplicationLifetimes;
 
 namespace Avalonia.Native
 {
-    enum OsxUnicodeSpecialKey
-    {
-        NSUpArrowFunctionKey = 0xF700,
-        NSDownArrowFunctionKey = 0xF701,
-        NSLeftArrowFunctionKey = 0xF702,
-        NSRightArrowFunctionKey = 0xF703,
-        NSF1FunctionKey = 0xF704,
-        NSF2FunctionKey = 0xF705,
-        NSF3FunctionKey = 0xF706,
-        NSF4FunctionKey = 0xF707,
-        NSF5FunctionKey = 0xF708,
-        NSF6FunctionKey = 0xF709,
-        NSF7FunctionKey = 0xF70A,
-        NSF8FunctionKey = 0xF70B,
-        NSF9FunctionKey = 0xF70C,
-        NSF10FunctionKey = 0xF70D,
-        NSF11FunctionKey = 0xF70E,
-        NSF12FunctionKey = 0xF70F,
-        NSF13FunctionKey = 0xF710,
-        NSF14FunctionKey = 0xF711,
-        NSF15FunctionKey = 0xF712,
-        NSF16FunctionKey = 0xF713,
-        NSF17FunctionKey = 0xF714,
-        NSF18FunctionKey = 0xF715,
-        NSF19FunctionKey = 0xF716,
-        NSF20FunctionKey = 0xF717,
-        NSF21FunctionKey = 0xF718,
-        NSF22FunctionKey = 0xF719,
-        NSF23FunctionKey = 0xF71A,
-        NSF24FunctionKey = 0xF71B,
-        NSF25FunctionKey = 0xF71C,
-        NSF26FunctionKey = 0xF71D,
-        NSF27FunctionKey = 0xF71E,
-        NSF28FunctionKey = 0xF71F,
-        NSF29FunctionKey = 0xF720,
-        NSF30FunctionKey = 0xF721,
-        NSF31FunctionKey = 0xF722,
-        NSF32FunctionKey = 0xF723,
-        NSF33FunctionKey = 0xF724,
-        NSF34FunctionKey = 0xF725,
-        NSF35FunctionKey = 0xF726,
-        NSInsertFunctionKey = 0xF727,
-        NSDeleteFunctionKey = 0xF728,
-        NSHomeFunctionKey = 0xF729,
-        NSBeginFunctionKey = 0xF72A,
-        NSEndFunctionKey = 0xF72B,
-        NSPageUpFunctionKey = 0xF72C,
-        NSPageDownFunctionKey = 0xF72D,
-        NSPrintScreenFunctionKey = 0xF72E,
-        NSScrollLockFunctionKey = 0xF72F,
-        NSPauseFunctionKey = 0xF730,
-        NSSysReqFunctionKey = 0xF731,
-        NSBreakFunctionKey = 0xF732,
-        NSResetFunctionKey = 0xF733,
-        NSStopFunctionKey = 0xF734,
-        NSMenuFunctionKey = 0xF735,
-        NSUserFunctionKey = 0xF736,
-        NSSystemFunctionKey = 0xF737,
-        NSPrintFunctionKey = 0xF738,
-        NSClearLineFunctionKey = 0xF739,
-        NSClearDisplayFunctionKey = 0xF73A,
-        NSInsertLineFunctionKey = 0xF73B,
-        NSDeleteLineFunctionKey = 0xF73C,
-        NSInsertCharFunctionKey = 0xF73D,
-        NSDeleteCharFunctionKey = 0xF73E,
-        NSPrevFunctionKey = 0xF73F,
-        NSNextFunctionKey = 0xF740,
-        NSSelectFunctionKey = 0xF741,
-        NSExecuteFunctionKey = 0xF742,
-        NSUndoFunctionKey = 0xF743,
-        NSRedoFunctionKey = 0xF744,
-        NSFindFunctionKey = 0xF745,
-        NSHelpFunctionKey = 0xF746,
-        NSModeSwitchFunctionKey = 0xF747
-    }
-
-    public class MenuActionCallback : CallbackBase, IAvnActionCallback
-    {
-        private Action _action;
-
-        public MenuActionCallback(Action action)
-        {
-            _action = action;
-        }
-
-        void IAvnActionCallback.Run()
-        {
-            _action?.Invoke();
-        }
-    }
-
-    public class PredicateCallback : CallbackBase, IAvnPredicateCallback
-    {
-        private Func<bool> _predicate;
-
-        public PredicateCallback(Func<bool> predicate)
-        {
-            _predicate = predicate;
-        }
-
-        bool IAvnPredicateCallback.Evaluate()
-        {
-            return _predicate();
-        }
-    }
-
     class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter
     {
         private IAvaloniaNativeFactory _factory;
-        private NativeMenu _menu;
         private bool _resetQueued;
         private bool _exported = false;
         private IAvnWindow _nativeWindow;
-        private List<NativeMenuItem> _menuItems = new List<NativeMenuItem>();
-
-        private static Dictionary<Key, OsxUnicodeSpecialKey> osxKeys = new Dictionary<Key, OsxUnicodeSpecialKey>
-        {
-            {Key.Up, OsxUnicodeSpecialKey.NSUpArrowFunctionKey },
-            {Key.Down, OsxUnicodeSpecialKey.NSDownArrowFunctionKey },
-            {Key.Left, OsxUnicodeSpecialKey.NSLeftArrowFunctionKey },
-            {Key.Right, OsxUnicodeSpecialKey.NSRightArrowFunctionKey },
-            { Key.F1, OsxUnicodeSpecialKey.NSF1FunctionKey },
-            { Key.F2, OsxUnicodeSpecialKey.NSF2FunctionKey },
-            { Key.F3, OsxUnicodeSpecialKey.NSF3FunctionKey },
-            { Key.F4, OsxUnicodeSpecialKey.NSF4FunctionKey },
-            { Key.F5, OsxUnicodeSpecialKey.NSF5FunctionKey },
-            { Key.F6, OsxUnicodeSpecialKey.NSF6FunctionKey },
-            { Key.F7, OsxUnicodeSpecialKey.NSF7FunctionKey },
-            { Key.F8, OsxUnicodeSpecialKey.NSF8FunctionKey },
-            { Key.F9, OsxUnicodeSpecialKey.NSF9FunctionKey },
-            { Key.F10, OsxUnicodeSpecialKey.NSF10FunctionKey },
-            { Key.F11, OsxUnicodeSpecialKey.NSF11FunctionKey },
-            { Key.F12, OsxUnicodeSpecialKey.NSF12FunctionKey },
-            { Key.F13, OsxUnicodeSpecialKey.NSF13FunctionKey },
-            { Key.F14, OsxUnicodeSpecialKey.NSF14FunctionKey },
-            { Key.F15, OsxUnicodeSpecialKey.NSF15FunctionKey },
-            { Key.F16, OsxUnicodeSpecialKey.NSF16FunctionKey },
-            { Key.F17, OsxUnicodeSpecialKey.NSF17FunctionKey },
-            { Key.F18, OsxUnicodeSpecialKey.NSF18FunctionKey },
-            { Key.F19, OsxUnicodeSpecialKey.NSF19FunctionKey },
-            { Key.F20, OsxUnicodeSpecialKey.NSF20FunctionKey },
-            { Key.F21, OsxUnicodeSpecialKey.NSF21FunctionKey },
-            { Key.F22, OsxUnicodeSpecialKey.NSF22FunctionKey },
-            { Key.F23, OsxUnicodeSpecialKey.NSF23FunctionKey },
-            { Key.F24, OsxUnicodeSpecialKey.NSF24FunctionKey },
-            { Key.Insert, OsxUnicodeSpecialKey.NSInsertFunctionKey },
-            { Key.Delete, OsxUnicodeSpecialKey.NSDeleteFunctionKey },
-            { Key.Home, OsxUnicodeSpecialKey.NSHomeFunctionKey },
-            //{ Key.Begin, OsxUnicodeSpecialKey.NSBeginFunctionKey },
-            { Key.End, OsxUnicodeSpecialKey.NSEndFunctionKey },
-            { Key.PageUp, OsxUnicodeSpecialKey.NSPageUpFunctionKey },
-            { Key.PageDown, OsxUnicodeSpecialKey.NSPageDownFunctionKey },
-            { Key.PrintScreen, OsxUnicodeSpecialKey.NSPrintScreenFunctionKey },
-            { Key.Scroll, OsxUnicodeSpecialKey.NSScrollLockFunctionKey },
-            //{ Key.SysReq, OsxUnicodeSpecialKey.NSSysReqFunctionKey },
-            //{ Key.Break, OsxUnicodeSpecialKey.NSBreakFunctionKey },
-            //{ Key.Reset, OsxUnicodeSpecialKey.NSResetFunctionKey },
-            //{ Key.Stop, OsxUnicodeSpecialKey.NSStopFunctionKey },
-            //{ Key.Menu, OsxUnicodeSpecialKey.NSMenuFunctionKey },
-            //{ Key.UserFunction, OsxUnicodeSpecialKey.NSUserFunctionKey },
-            //{ Key.SystemFunction, OsxUnicodeSpecialKey.NSSystemFunctionKey },
-            { Key.Print, OsxUnicodeSpecialKey.NSPrintFunctionKey },
-            //{ Key.ClearLine, OsxUnicodeSpecialKey.NSClearLineFunctionKey },
-            //{ Key.ClearDisplay, OsxUnicodeSpecialKey.NSClearDisplayFunctionKey },
-        };
+        private NativeMenu _menu;
+        private IAvnMenu _nativeMenu;
 
         public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory)
         {
@@ -193,7 +29,6 @@ namespace Avalonia.Native
         {
             _factory = factory;
 
-            _menu = NativeMenu.GetMenu(Application.Current);
             DoLayoutReset();
         }
 
@@ -203,17 +38,19 @@ namespace Avalonia.Native
 
         public void SetNativeMenu(NativeMenu menu)
         {
-            if (menu == null)
-                menu = new NativeMenu();
-
-            if (_menu != null)
-                ((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged;
-            _menu = menu;
-            ((INotifyCollectionChanged)_menu.Items).CollectionChanged += OnMenuItemsChanged;
+            _menu = menu == null ? new NativeMenu() : menu;
 
             DoLayoutReset();
         }
 
+        internal void UpdateIfNeeded()
+        {
+            if (_resetQueued)
+            {
+                DoLayoutReset();
+            }
+        }
+
         private static NativeMenu CreateDefaultAppMenu()
         {
             var result = new NativeMenu();
@@ -237,50 +74,34 @@ namespace Avalonia.Native
             return result;
         }
 
-        private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
-        {
-            QueueReset();
-        }
-
-        private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
-        {
-            QueueReset();
-        }
-
         void DoLayoutReset()
         {
             _resetQueued = false;
-            foreach (var i in _menuItems)
-            {
-                i.PropertyChanged -= OnItemPropertyChanged;
-                if (i.Menu != null)
-                    ((INotifyCollectionChanged)i.Menu.Items).CollectionChanged -= OnMenuItemsChanged;
-            }
-
-            _menuItems.Clear();
 
-            if(_nativeWindow is null)
+            if (_nativeWindow is null)
             {
-                _menu = NativeMenu.GetMenu(Application.Current);
+                var appMenu = NativeMenu.GetMenu(Application.Current);
 
-                if(_menu != null)
-                {
-                    SetMenu(_menu);
-                }
-                else
+                if (appMenu == null)
                 {
-                    SetMenu(CreateDefaultAppMenu());
+                    appMenu = CreateDefaultAppMenu();           
+                    NativeMenu.SetMenu(Application.Current, appMenu);         
                 }
+
+                SetMenu(appMenu);
             }
             else
             {
-                SetMenu(_nativeWindow, _menu?.Items);
+                if (_menu != null)
+                {
+                    SetMenu(_nativeWindow, _menu);
+                }
             }
 
             _exported = true;
         }
 
-        private void QueueReset()
+        internal void QueueReset()
         {
             if (_resetQueued)
                 return;
@@ -288,188 +109,64 @@ namespace Avalonia.Native
             Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background);
         }
 
-        private IAvnAppMenu CreateSubmenu(ICollection<NativeMenuItemBase> children)
+        private void SetMenu(NativeMenu menu)
         {
-            var menu = _factory.CreateMenu();
-
-            SetChildren(menu, children);
+            var menuItem = menu.Parent;
 
-            return menu;
-        }
+            var appMenuHolder = menuItem?.Parent;
 
-        private void AddMenuItem(NativeMenuItem item)
-        {
-            if (item.Menu?.Items != null)
+            if (menu.Parent is null)
             {
-                ((INotifyCollectionChanged)item.Menu.Items).CollectionChanged += OnMenuItemsChanged;
+                menuItem = new NativeMenuItem();
             }
-        }
 
-        private static string ConvertOSXSpecialKeyCodes(Key key)
-        {
-            if (osxKeys.ContainsKey(key))
-            {
-                return ((char)osxKeys[key]).ToString();
-            }
-            else
+            if (appMenuHolder is null)
             {
-                return key.ToString().ToLower();
-            }
-        }
+                appMenuHolder = new NativeMenu();
 
-        private void SetChildren(IAvnAppMenu menu, ICollection<NativeMenuItemBase> children)
-        {
-            foreach (var i in children)
-            {
-                if (i is NativeMenuItem item)
-                {
-                    AddMenuItem(item);
-
-                    var menuItem = _factory.CreateMenuItem();
-
-                    using (var buffer = new Utf8Buffer(item.Header))
-                    {
-                        menuItem.Title = buffer.DangerousGetHandle();
-                    }
-
-                    if (item.Gesture != null)
-                    {
-                        using (var buffer = new Utf8Buffer(ConvertOSXSpecialKeyCodes(item.Gesture.Key)))
-                        {
-                            menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers);
-                        }
-                    }
-
-                    menuItem.SetAction(new PredicateCallback(() =>
-                    {
-                        if (item.Command != null || item.HasClickHandlers)
-                        {
-                            return item.Enabled;
-                        }
-
-                        return false;
-                    }), new MenuActionCallback(() => { item.RaiseClick(); }));
-                    menu.AddItem(menuItem);
-
-                    if (item.Menu?.Items?.Count >= 0)
-                    {
-                        var submenu = _factory.CreateMenu();
-
-                        using (var buffer = new Utf8Buffer(item.Header))
-                        {
-                            submenu.Title = buffer.DangerousGetHandle();
-                        }
-
-                        menuItem.SetSubMenu(submenu);
-
-                        AddItemsToMenu(submenu, item.Menu?.Items);
-                    }
-                }
-                else if (i is NativeMenuItemSeperator seperator)
-                {
-                    menu.AddItem(_factory.CreateMenuItemSeperator());
-                }
+                appMenuHolder.Add(menuItem);
             }
-        }
 
-        private void AddItemsToMenu(IAvnAppMenu menu, ICollection<NativeMenuItemBase> items, bool isMainMenu = false)
-        {
-            foreach (var i in items)
-            {
-                if (i is NativeMenuItem item)
-                {
-                    var menuItem = _factory.CreateMenuItem();
-
-                    AddMenuItem(item);
-
-                    menuItem.SetAction(new PredicateCallback(() =>
-                    {
-                        if (item.Command != null || item.HasClickHandlers)
-                        {
-                            return item.Enabled;
-                        }
-
-                        return false;
-                    }), new MenuActionCallback(() => { item.RaiseClick(); }));
-
-                    if (item.Menu?.Items.Count >= 0 || isMainMenu)
-                    {
-                        var subMenu = CreateSubmenu(item.Menu?.Items);
-
-                        menuItem.SetSubMenu(subMenu);
-
-                        using (var buffer = new Utf8Buffer(item.Header))
-                        {
-                            subMenu.Title = buffer.DangerousGetHandle();
-                        }
-                    }
-                    else
-                    {
-                        using (var buffer = new Utf8Buffer(item.Header))
-                        {
-                            menuItem.Title = buffer.DangerousGetHandle();
-                        }
-
-                        if (item.Gesture != null)
-                        {
-                            using (var buffer = new Utf8Buffer(item.Gesture.Key.ToString().ToLower()))
-                            {
-                                menuItem.SetGesture(buffer.DangerousGetHandle(), (AvnInputModifiers)item.Gesture.KeyModifiers);
-                            }
-                        }
-                    }
-
-                    menu.AddItem(menuItem);
-                }
-                else if(i is NativeMenuItemSeperator seperator)
-                {
-                    menu.AddItem(_factory.CreateMenuItemSeperator());
-                }
-            }
-        }
+            menuItem.Menu = menu;
 
-        private void SetMenu(NativeMenu menu)
-        {
-            var appMenu = _factory.ObtainAppMenu();
+            var setMenu = false;
 
-            if (appMenu is null)
+            if (_nativeMenu is null)
             {
-                appMenu = _factory.CreateMenu();
-            }
+                _nativeMenu = IAvnMenu.Create(_factory);
 
-            var menuItem = menu.Parent;
+                _nativeMenu.Initialise(this, appMenuHolder, "");
 
-            if(menu.Parent is null)
-            {
-                menuItem = new NativeMenuItem();
+                setMenu = true;
             }
 
-            menuItem.Menu = menu;
-
-            appMenu.Clear();
-            AddItemsToMenu(appMenu, new List<NativeMenuItemBase> { menuItem });
+            _nativeMenu.Update(_factory, appMenuHolder);
 
-            _factory.SetAppMenu(appMenu);
+            if (setMenu)
+            {
+                _factory.SetAppMenu(_nativeMenu);
+            }
         }
 
-        private void SetMenu(IAvnWindow avnWindow, ICollection<NativeMenuItemBase> menuItems)
+        private void SetMenu(IAvnWindow avnWindow, NativeMenu menu)
         {
-            if (menuItems is null)
+            var setMenu = false;
+
+            if (_nativeMenu is null)
             {
-                menuItems = new List<NativeMenuItemBase>();
-            }
+                _nativeMenu = IAvnMenu.Create(_factory);
 
-            var appMenu = avnWindow.ObtainMainMenu();
+                _nativeMenu.Initialise(this, menu, "");     
 
-            if (appMenu is null)
-            {
-                appMenu = _factory.CreateMenu();
+                setMenu = true;           
             }
 
-            appMenu.Clear();
-            AddItemsToMenu(appMenu, menuItems);
+            _nativeMenu.Update(_factory, menu);
 
-            avnWindow.SetMainMenu(appMenu);
+            if(setMenu)
+            {
+                avnWindow.SetMainMenu(_nativeMenu);
+            }
         }
     }
 }

+ 176 - 0
src/Avalonia.Native/IAvnMenu.cs

@@ -0,0 +1,176 @@
+using System;
+using System.Collections.Generic;
+using System.Collections.Specialized;
+using System.Reactive.Disposables;
+using Avalonia.Controls;
+using Avalonia.Platform.Interop;
+
+namespace Avalonia.Native.Interop
+{
+    class MenuEvents : CallbackBase, IAvnMenuEvents
+    {
+        private IAvnMenu _parent;
+
+        public void Initialise(IAvnMenu parent)
+        {
+            _parent = parent;
+        }
+
+        public void NeedsUpdate()
+        {
+            _parent?.RaiseNeedsUpdate();
+        }
+    }
+
+    public partial class IAvnMenu
+    {
+        private MenuEvents _events;
+        private AvaloniaNativeMenuExporter _exporter;
+        private List<IAvnMenuItem> _menuItems = new List<IAvnMenuItem>();
+        private Dictionary<NativeMenuItemBase, IAvnMenuItem> _menuItemLookup = new Dictionary<NativeMenuItemBase, IAvnMenuItem>();
+        private CompositeDisposable _propertyDisposables = new CompositeDisposable();
+
+        internal void RaiseNeedsUpdate()
+        {
+            (ManagedMenu as INativeMenuExporterEventsImplBridge).RaiseNeedsUpdate();
+
+            _exporter.UpdateIfNeeded();
+        }
+
+        internal NativeMenu ManagedMenu { get; private set; }
+
+        public static IAvnMenu Create(IAvaloniaNativeFactory factory)
+        {
+            var events = new MenuEvents();
+
+            var menu = factory.CreateMenu(events);
+
+            events.Initialise(menu);
+
+            menu._events = events;
+
+            return menu;
+        }
+
+        protected override void Dispose(bool disposing)
+        {
+            if (disposing)
+            {
+                _events.Dispose();
+            }
+        }
+
+        private void RemoveAndDispose(IAvnMenuItem item)
+        {
+            _menuItemLookup.Remove(item.ManagedMenuItem);
+            _menuItems.Remove(item);
+            RemoveItem(item);
+
+            item.Deinitialise();
+            item.Dispose();
+        }
+
+        private void MoveExistingTo(int index, IAvnMenuItem item)
+        {
+            _menuItems.Remove(item);
+            _menuItems.Insert(index, item);
+
+            RemoveItem(item);
+            InsertItem(index, item);
+        }
+
+        private IAvnMenuItem CreateNewAt(IAvaloniaNativeFactory factory, int index, NativeMenuItemBase item)
+        {
+            var result = CreateNew(factory, item);
+
+            result.Initialise(item);
+
+            _menuItemLookup.Add(result.ManagedMenuItem, result);
+            _menuItems.Insert(index, result);
+
+            InsertItem(index, result);
+
+            return result;
+        }
+
+        private IAvnMenuItem CreateNew(IAvaloniaNativeFactory factory, NativeMenuItemBase item)
+        {
+            var nativeItem = item is NativeMenuItemSeperator ? factory.CreateMenuItemSeperator() : factory.CreateMenuItem();
+            nativeItem.ManagedMenuItem = item;
+
+            return nativeItem;
+        }
+
+        internal void Initialise(AvaloniaNativeMenuExporter exporter, NativeMenu managedMenu, string title)
+        {
+            _exporter = exporter;
+            ManagedMenu = managedMenu;
+
+            ((INotifyCollectionChanged)ManagedMenu.Items).CollectionChanged += OnMenuItemsChanged;
+
+            if (!string.IsNullOrWhiteSpace(title))
+            {
+                using (var buffer = new Utf8Buffer(title))
+                {
+                    Title = buffer.DangerousGetHandle();
+                }
+            }
+        }
+
+        internal void Deinitialise()
+        {
+            ((INotifyCollectionChanged)ManagedMenu.Items).CollectionChanged -= OnMenuItemsChanged;
+
+            foreach (var item in _menuItems)
+            {
+                item.Deinitialise();
+                item.Dispose();
+            }
+        }
+
+        internal void Update(IAvaloniaNativeFactory factory, NativeMenu menu)
+        {
+            if (menu != ManagedMenu)
+            {
+                throw new ArgumentException("The menu being updated does not match.", nameof(menu));
+            }
+
+            for (int i = 0; i < menu.Items.Count; i++)
+            {
+                IAvnMenuItem nativeItem;
+
+                if (i >= _menuItems.Count)
+                {
+                    nativeItem = CreateNewAt(factory, i, menu.Items[i]);
+                }
+                else if (menu.Items[i] == _menuItems[i].ManagedMenuItem)
+                {
+                    nativeItem = _menuItems[i];
+                }
+                else if (_menuItemLookup.TryGetValue(menu.Items[i], out nativeItem))
+                {
+                    MoveExistingTo(i, nativeItem);
+                }
+                else
+                {
+                    nativeItem = CreateNewAt(factory, i, menu.Items[i]);
+                }
+
+                if (menu.Items[i] is NativeMenuItem nmi)
+                {
+                    nativeItem.Update(_exporter, factory, nmi);
+                }
+            }
+
+            while (_menuItems.Count > menu.Items.Count)
+            {
+                RemoveAndDispose(_menuItems[_menuItems.Count - 1]);
+            }
+        }
+
+        private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
+        {
+            _exporter.QueueReset();
+        }
+    }
+}

+ 175 - 0
src/Avalonia.Native/IAvnMenuItem.cs

@@ -0,0 +1,175 @@
+using System;
+using System.IO;
+using System.Reactive.Disposables;
+using Avalonia.Controls;
+using Avalonia.Media.Imaging;
+using Avalonia.Platform.Interop;
+
+namespace Avalonia.Native.Interop
+{
+    public partial class IAvnMenuItem
+    {
+        private IAvnMenu _subMenu;
+        private CompositeDisposable _propertyDisposables = new CompositeDisposable();
+        private IDisposable _currentActionDisposable;
+
+        public NativeMenuItemBase ManagedMenuItem { get; set; }
+
+        private void UpdateTitle(string title)
+        {
+            using (var buffer = new Utf8Buffer(string.IsNullOrWhiteSpace(title) ? "" : title))
+            {
+                Title = buffer.DangerousGetHandle();
+            }
+        }
+
+        private void UpdateIsChecked(bool isChecked)
+        {
+            IsChecked = isChecked;
+        }
+
+        private void UpdateToggleType(NativeMenuItemToggleType toggleType)
+        {
+            ToggleType = (AvnMenuItemToggleType)toggleType;
+        }
+
+        private unsafe void UpdateIcon (IBitmap icon)
+        {
+            if(icon is null)
+            {
+                SetIcon(IntPtr.Zero, 0);
+            }
+            else
+            {
+                using(var ms = new MemoryStream())
+                {
+                    icon.Save(ms);
+
+                    var imageData = ms.ToArray();
+
+                    fixed(void* ptr = imageData)
+                    {
+                        SetIcon(new IntPtr(ptr), imageData.Length);
+                    }
+                }
+            }
+        }
+
+        private void UpdateGesture(Input.KeyGesture gesture)
+        {
+            // todo ensure backend can cope with setting null gesture.
+            using (var buffer = new Utf8Buffer(gesture == null ? "" : OsxUnicodeKeys.ConvertOSXSpecialKeyCodes(gesture.Key)))
+            {
+                var modifiers = gesture == null ? AvnInputModifiers.AvnInputModifiersNone : (AvnInputModifiers)gesture.KeyModifiers;
+                SetGesture(buffer.DangerousGetHandle(), modifiers);
+            }
+        }
+
+        private void UpdateAction(NativeMenuItem item)
+        {
+            _currentActionDisposable?.Dispose();
+
+            var action = new PredicateCallback(() =>
+            {
+                if (item.Command != null || item.HasClickHandlers)
+                {
+                    return item.IsEnabled;
+                }
+
+                return false;
+            });
+
+            var callback = new MenuActionCallback(() => { (item as INativeMenuItemExporterEventsImplBridge)?.RaiseClicked(); });
+
+            _currentActionDisposable = Disposable.Create(() =>
+            {
+                action.Dispose();
+                callback.Dispose();
+            });
+
+            SetAction(action, callback);
+        }
+
+        internal void Initialise(NativeMenuItemBase nativeMenuItem)
+        {
+            ManagedMenuItem = nativeMenuItem;
+
+            if (ManagedMenuItem is NativeMenuItem item)
+            {
+                UpdateTitle(item.Header);
+
+                UpdateGesture(item.Gesture);
+
+                UpdateAction(ManagedMenuItem as NativeMenuItem);
+
+                UpdateToggleType(item.ToggleType);
+
+                UpdateIcon(item.Icon);
+
+                UpdateIsChecked(item.IsChecked);
+
+                _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.HeaderProperty)
+                    .Subscribe(x => UpdateTitle(x)));
+
+                _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.GestureProperty)
+                    .Subscribe(x => UpdateGesture(x)));
+
+                _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.CommandProperty)
+                    .Subscribe(x => UpdateAction(ManagedMenuItem as NativeMenuItem)));
+
+                _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.ToggleTypeProperty)
+                    .Subscribe(x => UpdateToggleType(x)));
+
+                _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.IsCheckedProperty)
+                    .Subscribe(x => UpdateIsChecked(x)));
+
+                _propertyDisposables.Add(ManagedMenuItem.GetObservable(NativeMenuItem.IconProperty)
+                    .Subscribe(x => UpdateIcon(x)));
+            }
+        }
+
+        internal void Deinitialise()
+        {
+            if (_subMenu != null)
+            {
+                SetSubMenu(null);
+                _subMenu.Deinitialise();
+                _subMenu.Dispose();
+                _subMenu = null;
+            }
+
+            _propertyDisposables?.Dispose();
+            _currentActionDisposable?.Dispose();
+        }
+
+        internal void Update(AvaloniaNativeMenuExporter exporter, IAvaloniaNativeFactory factory, NativeMenuItem item)
+        {
+            if (item != ManagedMenuItem)
+            {
+                throw new ArgumentException("The item does not match the menuitem being updated.", nameof(item));
+            }
+
+            if (item.Menu != null)
+            {
+                if (_subMenu == null)
+                {
+                    _subMenu = IAvnMenu.Create(factory);
+
+                    _subMenu.Initialise(exporter, item.Menu, item.Header);
+
+                    SetSubMenu(_subMenu);
+                }
+
+                _subMenu.Update(factory, item.Menu);
+            }
+
+            if (item.Menu == null && _subMenu != null)
+            {
+                _subMenu.Deinitialise();
+                _subMenu.Dispose();
+
+                SetSubMenu(null);
+            }
+        }
+    }
+}

+ 2 - 0
src/Avalonia.Native/Mappings.xml

@@ -19,5 +19,7 @@
         <map param=".*::.*::ppv" return="true"/>
         <map param=".*::.*::ret" return="true"/>
         <map param=".*::.*::retOut" attribute="out" return="true"/>
+        <map method="IAvnAppMenu:.*" visibility="private" />
+        <map method="IAvnAppMenuItem:.*" visibility="private" />
     </mapping>
 </config>

+ 20 - 0
src/Avalonia.Native/MenuActionCallback.cs

@@ -0,0 +1,20 @@
+using System;
+using Avalonia.Native.Interop;
+
+namespace Avalonia.Native
+{
+    public class MenuActionCallback : CallbackBase, IAvnActionCallback
+    {
+        private Action _action;
+
+        public MenuActionCallback(Action action)
+        {
+            _action = action;
+        }
+
+        void IAvnActionCallback.Run()
+        {
+            _action?.Invoke();
+        }
+    }
+}

+ 147 - 0
src/Avalonia.Native/OsxUnicodeKeys.cs

@@ -0,0 +1,147 @@
+using System.Collections.Generic;
+using Avalonia.Input;
+
+namespace Avalonia.Native.Interop
+{
+    internal static class OsxUnicodeKeys
+    {
+        enum OsxUnicodeSpecialKey
+        {
+            NSUpArrowFunctionKey = 0xF700,
+            NSDownArrowFunctionKey = 0xF701,
+            NSLeftArrowFunctionKey = 0xF702,
+            NSRightArrowFunctionKey = 0xF703,
+            NSF1FunctionKey = 0xF704,
+            NSF2FunctionKey = 0xF705,
+            NSF3FunctionKey = 0xF706,
+            NSF4FunctionKey = 0xF707,
+            NSF5FunctionKey = 0xF708,
+            NSF6FunctionKey = 0xF709,
+            NSF7FunctionKey = 0xF70A,
+            NSF8FunctionKey = 0xF70B,
+            NSF9FunctionKey = 0xF70C,
+            NSF10FunctionKey = 0xF70D,
+            NSF11FunctionKey = 0xF70E,
+            NSF12FunctionKey = 0xF70F,
+            NSF13FunctionKey = 0xF710,
+            NSF14FunctionKey = 0xF711,
+            NSF15FunctionKey = 0xF712,
+            NSF16FunctionKey = 0xF713,
+            NSF17FunctionKey = 0xF714,
+            NSF18FunctionKey = 0xF715,
+            NSF19FunctionKey = 0xF716,
+            NSF20FunctionKey = 0xF717,
+            NSF21FunctionKey = 0xF718,
+            NSF22FunctionKey = 0xF719,
+            NSF23FunctionKey = 0xF71A,
+            NSF24FunctionKey = 0xF71B,
+            NSF25FunctionKey = 0xF71C,
+            NSF26FunctionKey = 0xF71D,
+            NSF27FunctionKey = 0xF71E,
+            NSF28FunctionKey = 0xF71F,
+            NSF29FunctionKey = 0xF720,
+            NSF30FunctionKey = 0xF721,
+            NSF31FunctionKey = 0xF722,
+            NSF32FunctionKey = 0xF723,
+            NSF33FunctionKey = 0xF724,
+            NSF34FunctionKey = 0xF725,
+            NSF35FunctionKey = 0xF726,
+            NSInsertFunctionKey = 0xF727,
+            NSDeleteFunctionKey = 0xF728,
+            NSHomeFunctionKey = 0xF729,
+            NSBeginFunctionKey = 0xF72A,
+            NSEndFunctionKey = 0xF72B,
+            NSPageUpFunctionKey = 0xF72C,
+            NSPageDownFunctionKey = 0xF72D,
+            NSPrintScreenFunctionKey = 0xF72E,
+            NSScrollLockFunctionKey = 0xF72F,
+            NSPauseFunctionKey = 0xF730,
+            NSSysReqFunctionKey = 0xF731,
+            NSBreakFunctionKey = 0xF732,
+            NSResetFunctionKey = 0xF733,
+            NSStopFunctionKey = 0xF734,
+            NSMenuFunctionKey = 0xF735,
+            NSUserFunctionKey = 0xF736,
+            NSSystemFunctionKey = 0xF737,
+            NSPrintFunctionKey = 0xF738,
+            NSClearLineFunctionKey = 0xF739,
+            NSClearDisplayFunctionKey = 0xF73A,
+            NSInsertLineFunctionKey = 0xF73B,
+            NSDeleteLineFunctionKey = 0xF73C,
+            NSInsertCharFunctionKey = 0xF73D,
+            NSDeleteCharFunctionKey = 0xF73E,
+            NSPrevFunctionKey = 0xF73F,
+            NSNextFunctionKey = 0xF740,
+            NSSelectFunctionKey = 0xF741,
+            NSExecuteFunctionKey = 0xF742,
+            NSUndoFunctionKey = 0xF743,
+            NSRedoFunctionKey = 0xF744,
+            NSFindFunctionKey = 0xF745,
+            NSHelpFunctionKey = 0xF746,
+            NSModeSwitchFunctionKey = 0xF747
+        }
+
+        private static Dictionary<Key, OsxUnicodeSpecialKey> s_osxKeys = new Dictionary<Key, OsxUnicodeSpecialKey>
+        {
+            {Key.Up, OsxUnicodeSpecialKey.NSUpArrowFunctionKey },
+            {Key.Down, OsxUnicodeSpecialKey.NSDownArrowFunctionKey },
+            {Key.Left, OsxUnicodeSpecialKey.NSLeftArrowFunctionKey },
+            {Key.Right, OsxUnicodeSpecialKey.NSRightArrowFunctionKey },
+            { Key.F1, OsxUnicodeSpecialKey.NSF1FunctionKey },
+            { Key.F2, OsxUnicodeSpecialKey.NSF2FunctionKey },
+            { Key.F3, OsxUnicodeSpecialKey.NSF3FunctionKey },
+            { Key.F4, OsxUnicodeSpecialKey.NSF4FunctionKey },
+            { Key.F5, OsxUnicodeSpecialKey.NSF5FunctionKey },
+            { Key.F6, OsxUnicodeSpecialKey.NSF6FunctionKey },
+            { Key.F7, OsxUnicodeSpecialKey.NSF7FunctionKey },
+            { Key.F8, OsxUnicodeSpecialKey.NSF8FunctionKey },
+            { Key.F9, OsxUnicodeSpecialKey.NSF9FunctionKey },
+            { Key.F10, OsxUnicodeSpecialKey.NSF10FunctionKey },
+            { Key.F11, OsxUnicodeSpecialKey.NSF11FunctionKey },
+            { Key.F12, OsxUnicodeSpecialKey.NSF12FunctionKey },
+            { Key.F13, OsxUnicodeSpecialKey.NSF13FunctionKey },
+            { Key.F14, OsxUnicodeSpecialKey.NSF14FunctionKey },
+            { Key.F15, OsxUnicodeSpecialKey.NSF15FunctionKey },
+            { Key.F16, OsxUnicodeSpecialKey.NSF16FunctionKey },
+            { Key.F17, OsxUnicodeSpecialKey.NSF17FunctionKey },
+            { Key.F18, OsxUnicodeSpecialKey.NSF18FunctionKey },
+            { Key.F19, OsxUnicodeSpecialKey.NSF19FunctionKey },
+            { Key.F20, OsxUnicodeSpecialKey.NSF20FunctionKey },
+            { Key.F21, OsxUnicodeSpecialKey.NSF21FunctionKey },
+            { Key.F22, OsxUnicodeSpecialKey.NSF22FunctionKey },
+            { Key.F23, OsxUnicodeSpecialKey.NSF23FunctionKey },
+            { Key.F24, OsxUnicodeSpecialKey.NSF24FunctionKey },
+            { Key.Insert, OsxUnicodeSpecialKey.NSInsertFunctionKey },
+            { Key.Delete, OsxUnicodeSpecialKey.NSDeleteFunctionKey },
+            { Key.Home, OsxUnicodeSpecialKey.NSHomeFunctionKey },
+            //{ Key.Begin, OsxUnicodeSpecialKey.NSBeginFunctionKey },
+            { Key.End, OsxUnicodeSpecialKey.NSEndFunctionKey },
+            { Key.PageUp, OsxUnicodeSpecialKey.NSPageUpFunctionKey },
+            { Key.PageDown, OsxUnicodeSpecialKey.NSPageDownFunctionKey },
+            { Key.PrintScreen, OsxUnicodeSpecialKey.NSPrintScreenFunctionKey },
+            { Key.Scroll, OsxUnicodeSpecialKey.NSScrollLockFunctionKey },
+            //{ Key.SysReq, OsxUnicodeSpecialKey.NSSysReqFunctionKey },
+            //{ Key.Break, OsxUnicodeSpecialKey.NSBreakFunctionKey },
+            //{ Key.Reset, OsxUnicodeSpecialKey.NSResetFunctionKey },
+            //{ Key.Stop, OsxUnicodeSpecialKey.NSStopFunctionKey },
+            //{ Key.Menu, OsxUnicodeSpecialKey.NSMenuFunctionKey },
+            //{ Key.UserFunction, OsxUnicodeSpecialKey.NSUserFunctionKey },
+            //{ Key.SystemFunction, OsxUnicodeSpecialKey.NSSystemFunctionKey },
+            { Key.Print, OsxUnicodeSpecialKey.NSPrintFunctionKey },
+            //{ Key.ClearLine, OsxUnicodeSpecialKey.NSClearLineFunctionKey },
+            //{ Key.ClearDisplay, OsxUnicodeSpecialKey.NSClearDisplayFunctionKey },
+        };
+
+        public static string ConvertOSXSpecialKeyCodes(Key key)
+        {
+            if (s_osxKeys.ContainsKey(key))
+            {
+                return ((char)s_osxKeys[key]).ToString();
+            }
+            else
+            {
+                return key.ToString().ToLower();
+            }
+        }
+    }
+}

+ 20 - 0
src/Avalonia.Native/PredicateCallback.cs

@@ -0,0 +1,20 @@
+using System;
+using Avalonia.Native.Interop;
+
+namespace Avalonia.Native
+{
+    public class PredicateCallback : CallbackBase, IAvnPredicateCallback
+    {
+        private Func<bool> _predicate;
+
+        public PredicateCallback(Func<bool> predicate)
+        {
+            _predicate = predicate;
+        }
+
+        bool IAvnPredicateCallback.Evaluate()
+        {
+            return _predicate();
+        }
+    }
+}

+ 14 - 6
src/Avalonia.Native/SystemDialogs.cs

@@ -5,7 +5,6 @@ using System.Threading.Tasks;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform;
 using Avalonia.Native.Interop;
-using Avalonia.Platform;
 
 namespace Avalonia.Native
 {
@@ -18,13 +17,15 @@ namespace Avalonia.Native
             _native = native;
         }
 
-        public Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
+        public Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
         {
             var events = new SystemDialogEvents();
 
+            var nativeParent = GetNativeWindow(parent);
+
             if (dialog is OpenFileDialog ofd)
             {
-                _native.OpenFileDialog((parent as WindowImpl)?.Native,
+                _native.OpenFileDialog(nativeParent,
                                         events, ofd.AllowMultiple,
                                         ofd.Title ?? "",
                                         ofd.InitialDirectory ?? "",
@@ -33,7 +34,7 @@ namespace Avalonia.Native
             }
             else
             {
-                _native.SaveFileDialog((parent as WindowImpl)?.Native,
+                _native.SaveFileDialog(nativeParent,
                                         events,
                                         dialog.Title ?? "",
                                         dialog.InitialDirectory ?? "",
@@ -44,14 +45,21 @@ namespace Avalonia.Native
             return events.Task.ContinueWith(t => { events.Dispose(); return t.Result; });
         }
 
-        public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
+        public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
         {
             var events = new SystemDialogEvents();
 
-            _native.SelectFolderDialog((parent as WindowImpl)?.Native, events, dialog.Title ?? "", dialog.InitialDirectory ?? "");
+            var nativeParent = GetNativeWindow(parent);
+
+            _native.SelectFolderDialog(nativeParent, events, dialog.Title ?? "", dialog.InitialDirectory ?? "");
 
             return events.Task.ContinueWith(t => { events.Dispose(); return t.Result.FirstOrDefault(); });
         }
+
+        private IAvnWindow GetNativeWindow(Window window)
+        {
+            return (window?.PlatformImpl as WindowImpl)?.Native;
+        }
     }
 
     public class SystemDialogEvents : CallbackBase, IAvnSystemDialogEvents

+ 1 - 1
src/Avalonia.Native/WindowImpl.cs

@@ -67,7 +67,7 @@ namespace Avalonia.Native
 
         public void SetSystemDecorations(Controls.SystemDecorations enabled)
         {
-            _native.HasDecorations = (Interop.SystemDecorations)enabled;
+            _native.Decorations = (Interop.SystemDecorations)enabled;
         }
 
         public void SetTitleBarColor (Avalonia.Media.Color color)

+ 145 - 3
src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs

@@ -1,11 +1,18 @@
 using System;
 using System.Collections.Generic;
-using System.Linq;
 
 namespace Avalonia.LogicalTree
 {
+    /// <summary>
+    /// Provides extension methods for working with the logical tree.
+    /// </summary>
     public static class LogicalExtensions
     {
+        /// <summary>
+        /// Enumerates the ancestors of an <see cref="ILogical"/> in the logical tree.
+        /// </summary>
+        /// <param name="logical">The logical.</param>
+        /// <returns>The logical's ancestors.</returns>
         public static IEnumerable<ILogical> GetLogicalAncestors(this ILogical logical)
         {
             Contract.Requires<ArgumentNullException>(logical != null);
@@ -19,6 +26,11 @@ namespace Avalonia.LogicalTree
             }
         }
 
+        /// <summary>
+        /// Enumerates an <see cref="ILogical"/> and its ancestors in the logical tree.
+        /// </summary>
+        /// <param name="logical">The logical.</param>
+        /// <returns>The logical and its ancestors.</returns>
         public static IEnumerable<ILogical> GetSelfAndLogicalAncestors(this ILogical logical)
         {
             yield return logical;
@@ -29,11 +41,50 @@ namespace Avalonia.LogicalTree
             }
         }
 
+        /// <summary>
+        /// Finds first ancestor of given type.
+        /// </summary>
+        /// <typeparam name="T">Ancestor type.</typeparam>
+        /// <param name="logical">The logical.</param>
+        /// <param name="includeSelf">If given logical should be included in search.</param>
+        /// <returns>First ancestor of given type.</returns>
+        public static T FindLogicalAncestorOfType<T>(this ILogical logical, bool includeSelf = false) where T : class
+        {
+            if (logical is null)
+            {
+                return null;
+            }
+
+            ILogical parent = includeSelf ? logical : logical.LogicalParent;
+
+            while (parent != null)
+            {
+                if (parent is T result)
+                {
+                    return result;
+                }
+
+                parent = parent.LogicalParent;
+            }
+
+            return null;
+        }
+
+        /// <summary>
+        /// Enumerates the children of an <see cref="ILogical"/> in the logical tree.
+        /// </summary>
+        /// <param name="logical">The logical.</param>
+        /// <returns>The logical children.</returns>
         public static IEnumerable<ILogical> GetLogicalChildren(this ILogical logical)
         {
             return logical.LogicalChildren;
         }
 
+        /// <summary>
+        /// Enumerates the descendants of an <see cref="ILogical"/> in the logical tree.
+        /// </summary>
+        /// <param name="logical">The logical.</param>
+        /// <returns>The logical's ancestors.</returns>
         public static IEnumerable<ILogical> GetLogicalDescendants(this ILogical logical)
         {
             foreach (ILogical child in logical.LogicalChildren)
@@ -47,6 +98,11 @@ namespace Avalonia.LogicalTree
             }
         }
 
+        /// <summary>
+        /// Enumerates an <see cref="ILogical"/> and its descendants in the logical tree.
+        /// </summary>
+        /// <param name="logical">The logical.</param>
+        /// <returns>The logical and its ancestors.</returns>
         public static IEnumerable<ILogical> GetSelfAndLogicalDescendants(this ILogical logical)
         {
             yield return logical;
@@ -57,16 +113,56 @@ namespace Avalonia.LogicalTree
             }
         }
 
+        /// <summary>
+        /// Finds first descendant of given type.
+        /// </summary>
+        /// <typeparam name="T">Descendant type.</typeparam>
+        /// <param name="logical">The logical.</param>
+        /// <param name="includeSelf">If given logical should be included in search.</param>
+        /// <returns>First descendant of given type.</returns>
+        public static T FindLogicalDescendantOfType<T>(this ILogical logical, bool includeSelf = false) where T : class
+        {
+            if (logical is null)
+            {
+                return null;
+            }
+
+            if (includeSelf && logical is T result)
+            {
+                return result;
+            }
+
+            return FindDescendantOfTypeCore<T>(logical);
+        }
+
+        /// <summary>
+        /// Gets the logical parent of an <see cref="ILogical"/>.
+        /// </summary>
+        /// <param name="logical">The logical.</param>
+        /// <returns>The parent, or null if the logical is unparented.</returns>
         public static ILogical GetLogicalParent(this ILogical logical)
         {
             return logical.LogicalParent;
         }
 
+        /// <summary>
+        /// Gets the logical parent of an <see cref="ILogical"/>.
+        /// </summary>
+        /// <typeparam name="T">The type of the logical parent.</typeparam>
+        /// <param name="logical">The logical.</param>
+        /// <returns>
+        /// The parent, or null if the logical is unparented or its parent is not of type <typeparamref name="T"/>.
+        /// </returns>
         public static T GetLogicalParent<T>(this ILogical logical) where T : class
         {
             return logical.LogicalParent as T;
         }
 
+        /// <summary>
+        /// Enumerates the siblings of an <see cref="ILogical"/> in the logical tree.
+        /// </summary>
+        /// <param name="logical">The logical.</param>
+        /// <returns>The logical siblings.</returns>
         public static IEnumerable<ILogical> GetLogicalSiblings(this ILogical logical)
         {
             ILogical parent = logical.LogicalParent;
@@ -80,9 +176,55 @@ namespace Avalonia.LogicalTree
             }
         }
 
-        public static bool IsLogicalParentOf(this ILogical logical, ILogical target)
+        /// <summary>
+        /// Tests whether an <see cref="ILogical"/> is an ancestor of another logical.
+        /// </summary>
+        /// <param name="logical">The logical.</param>
+        /// <param name="target">The potential descendant.</param>
+        /// <returns>
+        /// True if <paramref name="logical"/> is an ancestor of <paramref name="target"/>;
+        /// otherwise false.
+        /// </returns>
+        public static bool IsLogicalAncestorOf(this ILogical logical, ILogical target)
         {
-            return target.GetLogicalAncestors().Any(x => x == logical);
+            ILogical current = target?.LogicalParent;
+
+            while (current != null)
+            {
+                if (current == logical)
+                {
+                    return true;
+                }
+
+                current = current.LogicalParent;
+            }
+
+            return false;
+        }
+
+        private static T FindDescendantOfTypeCore<T>(ILogical logical) where T : class
+        {
+            var logicalChildren = logical.LogicalChildren;
+            var logicalChildrenCount = logicalChildren.Count;
+
+            for (var i = 0; i < logicalChildrenCount; i++)
+            {
+                ILogical child = logicalChildren[i];
+
+                if (child is T result)
+                {
+                    return result;
+                }
+
+                var childResult = FindDescendantOfTypeCore<T>(child);
+
+                if (!(childResult is null))
+                {
+                    return childResult;
+                }
+            }
+
+            return null;
         }
     }
 }

+ 22 - 4
src/Avalonia.Visuals/Media/FontManager.cs

@@ -23,6 +23,11 @@ namespace Avalonia.Media
 
             DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName();
 
+            if (string.IsNullOrEmpty(DefaultFontFamilyName))
+            {
+                throw new InvalidOperationException("Default font family name can't be null or empty.");
+            }
+
             _defaultFontFamily = new FontFamily(DefaultFontFamilyName);
         }
 
@@ -39,7 +44,8 @@ namespace Avalonia.Media
 
                 var fontManagerImpl = AvaloniaLocator.Current.GetService<IFontManagerImpl>();
 
-                if (fontManagerImpl == null) throw new InvalidOperationException("No font manager implementation was registered.");
+                if (fontManagerImpl == null)
+                    throw new InvalidOperationException("No font manager implementation was registered.");
 
                 current = new FontManager(fontManagerImpl);
 
@@ -87,7 +93,7 @@ namespace Avalonia.Media
                     fontFamily = _defaultFontFamily;
                 }
 
-                var key = new FontKey(fontFamily, fontWeight, fontStyle);
+                var key = new FontKey(fontFamily.Name, fontWeight, fontStyle);
 
                 if (_typefaceCache.TryGetValue(key, out var typeface))
                 {
@@ -126,9 +132,21 @@ namespace Avalonia.Media
             FontStyle fontStyle = FontStyle.Normal,
             FontFamily fontFamily = null, CultureInfo culture = null)
         {
-            return PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ?
-                _typefaceCache.GetOrAdd(key, new Typeface(key.FontFamily, key.Weight, key.Style)) :
+            foreach (var cachedTypeface in _typefaceCache.Values)
+            {
+                // First try to find a cached typeface by style and weight to avoid redundant glyph index lookup.
+                if (cachedTypeface.Style == fontStyle && cachedTypeface.Weight == fontWeight
+                                                      && cachedTypeface.GlyphTypeface.GetGlyph((uint)codepoint) != 0)
+                {
+                    return cachedTypeface;
+                }
+            }
+
+            var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ?
+                _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Weight, key.Style)) :
                 null;
+
+            return matchedTypeface;
         }
     }
 }

+ 1 - 2
src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs

@@ -1,7 +1,6 @@
 using System;
 using System.Collections;
 using System.Collections.Generic;
-using System.Linq;
 using System.Text;
 using Avalonia.Utilities;
 
@@ -21,7 +20,7 @@ namespace Avalonia.Media.Fonts
                 throw new ArgumentNullException(nameof(familyNames));
             }
 
-            Names = familyNames.Split(',').Select(x => x.Trim()).ToArray();
+            Names = Array.ConvertAll(familyNames.Split(','), p => p.Trim());
 
             PrimaryFamilyName = Names[0];
 

+ 8 - 8
src/Avalonia.Visuals/Media/Fonts/FontKey.cs

@@ -4,20 +4,20 @@ namespace Avalonia.Media.Fonts
 {
     public readonly struct FontKey : IEquatable<FontKey>
     {
-        public readonly FontFamily FontFamily;
-        public readonly FontStyle Style;
-        public readonly FontWeight Weight;
-
-        public FontKey(FontFamily fontFamily, FontWeight weight, FontStyle style)
+        public FontKey(string familyName, FontWeight weight, FontStyle style)
         {
-            FontFamily = fontFamily;
+            FamilyName = familyName;
             Style = style;
             Weight = weight;
         }
 
+        public string FamilyName { get; }
+        public FontStyle Style { get; }
+        public FontWeight Weight { get; }
+
         public override int GetHashCode()
         {
-            var hash = FontFamily.GetHashCode();
+            var hash = FamilyName.GetHashCode();
 
             hash = hash * 31 + (int)Style;
             hash = hash * 31 + (int)Weight;
@@ -32,7 +32,7 @@ namespace Avalonia.Media.Fonts
 
         public bool Equals(FontKey other)
         {
-            return FontFamily == other.FontFamily &&
+            return FamilyName == other.FamilyName &&
                 Style == other.Style &&
                    Weight == other.Weight;
         }

+ 6 - 0
src/Avalonia.Visuals/Media/FormattedText.cs

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

+ 1 - 1
src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs

@@ -66,7 +66,7 @@ namespace Avalonia.Media.TextFormatting
 
             //ToDo: Fix FontFamily fallback
             currentTypeface =
-                FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style);
+                FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style, defaultStyle.TextFormat.Typeface.FontFamily);
 
             if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count))
             {

+ 1 - 1
src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs

@@ -2,7 +2,7 @@
 
 namespace Avalonia.Media.TextFormatting.Unicode
 {
-    internal ref struct CodepointEnumerator
+    public ref struct CodepointEnumerator
     {
         private ReadOnlySlice<char> _text;
 

+ 0 - 44
src/Avalonia.Visuals/Media/TextFormatting/Unicode/UnicodeGeneralCategory.cs

@@ -1,44 +0,0 @@
-namespace Avalonia.Media.TextFormatting.Unicode
-{
-    public enum UnicodeGeneralCategory : byte
-    {
-        Other, //C# Cc | Cf | Cn | Co | Cs
-        Control, //Cc
-        Format, //Cf
-        Unassigned, //Cn
-        PrivateUse, //Co
-        Surrogate, //Cs
-        Letter, //L# Ll | Lm | Lo | Lt | Lu
-        CasedLetter, //LC# Ll | Lt | Lu
-        LowercaseLetter, //Ll
-        ModifierLetter, //Lm
-        OtherLetter, //Lo
-        TitlecaseLetter, //Lt
-        UppercaseLetter, //Lu
-        Mark, //M
-        SpacingMark, //Mc
-        EnclosingMark, //Me
-        NonspacingMark, //Mn
-        Number, //N# Nd | Nl | No
-        DecimalNumber, //Nd
-        LetterNumber, //Nl
-        OtherNumber, //No
-        Punctuation, //P
-        ConnectorPunctuation, //Pc
-        DashPunctuation, //Pd
-        ClosePunctuation, //Pe
-        FinalPunctuation, //Pf
-        InitialPunctuation, //Pi
-        OtherPunctuation, //Po
-        OpenPunctuation, //Ps
-        Symbol, //S# Sc | Sk | Sm | So
-        CurrencySymbol, //Sc
-        ModifierSymbol, //Sk
-        MathSymbol, //Sm
-        OtherSymbol, //So
-        Separator, //Z# Zl | Zp | Zs
-        LineSeparator, //Zl
-        ParagraphSeparator, //Zp
-        SpaceSeparator, //Zs
-    }
-}

+ 22 - 4
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

@@ -35,6 +35,7 @@ namespace Avalonia.Rendering
         private IRef<IDrawOperation> _currentDraw;
         private readonly IDeferredRendererLock _lock;
         private readonly object _sceneLock = new object();
+        private readonly Action _updateSceneIfNeededDelegate;
 
         /// <summary>
         /// Initializes a new instance of the <see cref="DeferredRenderer"/> class.
@@ -49,7 +50,7 @@ namespace Avalonia.Rendering
             IRenderLoop renderLoop,
             ISceneBuilder sceneBuilder = null,
             IDispatcher dispatcher = null,
-            IDeferredRendererLock rendererLock = null)
+            IDeferredRendererLock rendererLock = null) : base(true)
         {
             Contract.Requires<ArgumentNullException>(root != null);
 
@@ -59,6 +60,7 @@ namespace Avalonia.Rendering
             Layers = new RenderLayers();
             _renderLoop = renderLoop;
             _lock = rendererLock ?? new ManagedDeferredRendererLock();
+            _updateSceneIfNeededDelegate = UpdateSceneIfNeeded;
         }
 
         /// <summary>
@@ -73,7 +75,7 @@ namespace Avalonia.Rendering
         public DeferredRenderer(
             IVisual root,
             IRenderTarget renderTarget,
-            ISceneBuilder sceneBuilder = null)
+            ISceneBuilder sceneBuilder = null) : base(true)
         {
             Contract.Requires<ArgumentNullException>(root != null);
             Contract.Requires<ArgumentNullException>(renderTarget != null);
@@ -83,6 +85,7 @@ namespace Avalonia.Rendering
             _sceneBuilder = sceneBuilder ?? new SceneBuilder();
             Layers = new RenderLayers();
             _lock = new ManagedDeferredRendererLock();
+            _updateSceneIfNeededDelegate = UpdateSceneIfNeeded;
         }
 
         /// <inheritdoc/>
@@ -261,7 +264,8 @@ namespace Avalonia.Rendering
                     try
                     {
                         var (scene, updated) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context);
-
+                        if (updated)
+                            FpsTick();
                         using (scene)
                         {
                             if (scene?.Item != null)
@@ -318,17 +322,25 @@ namespace Avalonia.Rendering
                         _lastSceneId = scene.Generation;
 
 
+                    var isUiThread = Dispatcher.UIThread.CheckAccess();
                     // We have consumed the previously available scene, but there might be some dirty 
                     // rects since the last update. *If* we are on UI thread, we can force immediate scene
                     // rebuild before rendering anything on-screen
                     // We are calling the same method recursively here 
-                    if (!recursiveCall && Dispatcher.UIThread.CheckAccess() && NeedsUpdate)
+                    if (!recursiveCall && isUiThread && NeedsUpdate)
                     {
                         UpdateScene();
                         var (rs, _) = UpdateRenderLayersAndConsumeSceneIfNeeded(ref context, true);
                         return (rs, true);
                     }
 
+                    // We are rendering a new scene version, so it's highly likely
+                    // that there is already a pending update for animations
+                    // So we are scheduling an update call so UI thread could prepare a scene before
+                    // the next render timer tick
+                    if (!recursiveCall && !isUiThread)
+                        Dispatcher.UIThread.Post(_updateSceneIfNeededDelegate, DispatcherPriority.Render);
+
                     // Indicate that we have updated the layers
                     return (sceneRef.Clone(), true);
                 }
@@ -534,6 +546,12 @@ namespace Avalonia.Rendering
             context = RenderTarget.CreateDrawingContext(this);
         }
 
+        private void UpdateSceneIfNeeded()
+        {
+            if(NeedsUpdate)
+                UpdateScene();
+        }
+        
         private void UpdateScene()
         {
             Dispatcher.UIThread.VerifyAccess();

+ 7 - 2
src/Avalonia.Visuals/Rendering/RendererBase.cs

@@ -7,6 +7,7 @@ namespace Avalonia.Rendering
 {
     public class RendererBase
     {
+        private readonly bool _useManualFpsCounting;
         private static int s_fontSize = 18;
         private readonly Stopwatch _stopwatch = Stopwatch.StartNew();
         private int _framesThisSecond;
@@ -14,8 +15,9 @@ namespace Avalonia.Rendering
         private FormattedText _fpsText;
         private TimeSpan _lastFpsUpdate;
 
-        public RendererBase()
+        public RendererBase(bool useManualFpsCounting = false)
         {
+            _useManualFpsCounting = useManualFpsCounting;
             _fpsText = new FormattedText
             {
                 Typeface = FontManager.Current?.GetOrAddTypeface(FontFamily.Default),
@@ -23,12 +25,15 @@ namespace Avalonia.Rendering
             };
         }
 
+        protected void FpsTick() => _framesThisSecond++;
+
         protected void RenderFps(IDrawingContextImpl context, Rect clientRect, int? layerCount)
         {
             var now = _stopwatch.Elapsed;
             var elapsed = now - _lastFpsUpdate;
 
-            ++_framesThisSecond;
+            if (!_useManualFpsCounting)
+                ++_framesThisSecond;
 
             if (elapsed.TotalSeconds > 1)
             {

+ 7 - 6
src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs

@@ -162,17 +162,18 @@ namespace Avalonia.Rendering.SceneGraph
 
             index.Add(result.Visual, result);
 
-            int childCount = source.Children.Count;
+            var children = source.Children;
+            var childrenCount = children.Count;
 
-            if (childCount > 0)
+            if (childrenCount > 0)
             {
-                Span<IVisualNode> children = result.AddChildrenSpan(childCount);
+                result.TryPreallocateChildren(childrenCount);
 
-                for (var i = 0; i < childCount; i++)
+                for (var i = 0; i < childrenCount; i++)
                 {
-                    var child = source.Children[i];
+                    var child = children[i];
 
-                    children[i] = Clone((VisualNode)child, result, index);
+                    result.AddChild(Clone((VisualNode)child, result, index));
                 }
             }
 

+ 8 - 20
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

@@ -1,8 +1,7 @@
 using System;
 using System.Collections.Generic;
-using System.Linq;
 using System.Reactive.Disposables;
-using Avalonia.Collections.Pooled;
+using Avalonia.Collections;
 using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.Utilities;
@@ -20,8 +19,8 @@ namespace Avalonia.Rendering.SceneGraph
 
         private Rect? _bounds;
         private double _opacity;
-        private PooledList<IVisualNode> _children;
-        private PooledList<IRef<IDrawOperation>> _drawOperations;
+        private List<IVisualNode> _children;
+        private List<IRef<IDrawOperation>> _drawOperations;
         private IRef<IDisposable> _drawOperationsRefCounter;
         private bool _drawOperationsCloned;
         private Matrix transformRestore;
@@ -350,16 +349,9 @@ namespace Avalonia.Rendering.SceneGraph
             context.Transform = transformRestore;
         }
 
-        /// <summary>
-        /// Inserts default constructed children into collection and returns a span for the newly created range.
-        /// </summary>
-        /// <param name="count">Count of children that will be added.</param>
-        /// <returns></returns>
-        internal Span<IVisualNode> AddChildrenSpan(int count)
+        internal void TryPreallocateChildren(int count)
         {
             EnsureChildrenCreated(count);
-
-            return _children.AddSpan(count);
         }
 
         private Rect CalculateBounds()
@@ -379,7 +371,7 @@ namespace Avalonia.Rendering.SceneGraph
         {
             if (_children == null)
             {
-                _children = new PooledList<IVisualNode>(capacity);
+                _children = new List<IVisualNode>(capacity);
             }
         }
 
@@ -390,7 +382,7 @@ namespace Avalonia.Rendering.SceneGraph
         {
             if (_drawOperations == null)
             {
-                _drawOperations = new PooledList<IRef<IDrawOperation>>();
+                _drawOperations = new List<IRef<IDrawOperation>>();
                 _drawOperationsRefCounter = RefCountable.Create(CreateDisposeDrawOperations(_drawOperations));
                 _drawOperationsCloned = false;
             }
@@ -398,7 +390,7 @@ namespace Avalonia.Rendering.SceneGraph
             {
                 var oldDrawOperations = _drawOperations;
 
-                _drawOperations = new PooledList<IRef<IDrawOperation>>(oldDrawOperations.Count);
+                _drawOperations = new List<IRef<IDrawOperation>>(oldDrawOperations.Count);
 
                 foreach (var drawOperation in oldDrawOperations)
                 {
@@ -418,7 +410,7 @@ namespace Avalonia.Rendering.SceneGraph
         /// </summary>
         /// <param name="drawOperations">Draw operations that need to be disposed.</param>
         /// <returns>Disposable for given draw operations.</returns>
-        private static IDisposable CreateDisposeDrawOperations(PooledList<IRef<IDrawOperation>> drawOperations)
+        private static IDisposable CreateDisposeDrawOperations(List<IRef<IDrawOperation>> drawOperations)
         {
             return Disposable.Create(drawOperations, operations =>
             {
@@ -426,8 +418,6 @@ namespace Avalonia.Rendering.SceneGraph
                 {
                     operation.Dispose();
                 }
-
-                operations.Dispose();
             });
         }
 
@@ -437,8 +427,6 @@ namespace Avalonia.Rendering.SceneGraph
         {
             _drawOperationsRefCounter?.Dispose();
 
-            _children?.Dispose();
-
             Disposed = true;
         }
     }

+ 13 - 1
src/Avalonia.Visuals/VisualTree/VisualExtensions.cs

@@ -377,7 +377,19 @@ namespace Avalonia.VisualTree
         /// </returns>
         public static bool IsVisualAncestorOf(this IVisual visual, IVisual target)
         {
-            return target.GetVisualAncestors().Any(x => x == visual);
+            IVisual current = target?.VisualParent;
+
+            while (current != null)
+            {
+                if (current == visual)
+                {
+                    return true;
+                }
+
+                current = current.VisualParent;
+            }
+
+            return false;
         }
 
         public static IEnumerable<IVisual> SortByZIndex(this IEnumerable<IVisual> elements)

+ 10 - 4
src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs

@@ -102,23 +102,29 @@ namespace Avalonia.X11.NativeDialogs
             return tcs.Task;
         }
         
-        public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
+        public async Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
         {
             await EnsureInitialized();
+
+            var platformImpl = parent?.PlatformImpl;
+
             return await await RunOnGlibThread(
-                () => ShowDialog(dialog.Title, parent,
+                () => ShowDialog(dialog.Title, platformImpl,
                     dialog is OpenFileDialog ? GtkFileChooserAction.Open : GtkFileChooserAction.Save,
                     (dialog as OpenFileDialog)?.AllowMultiple ?? false,
                     Path.Combine(string.IsNullOrEmpty(dialog.InitialDirectory) ? "" : dialog.InitialDirectory,
                         string.IsNullOrEmpty(dialog.InitialFileName) ? "" : dialog.InitialFileName), dialog.Filters));
         }
 
-        public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
+        public async Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
         {
             await EnsureInitialized();
+
+            var platformImpl = parent?.PlatformImpl;
+
             return await await RunOnGlibThread(async () =>
             {
-                var res = await ShowDialog(dialog.Title, parent,
+                var res = await ShowDialog(dialog.Title, platformImpl,
                     GtkFileChooserAction.SelectFolder, false, dialog.InitialDirectory, null);
                 return res?.FirstOrDefault();
             });

+ 1 - 0
src/Avalonia.X11/X11Atoms.cs

@@ -156,6 +156,7 @@ namespace Avalonia.X11
         public readonly IntPtr _NET_SYSTEM_TRAY_OPCODE;
         public readonly IntPtr _NET_WM_STATE_MAXIMIZED_HORZ;
         public readonly IntPtr _NET_WM_STATE_MAXIMIZED_VERT;
+        public readonly IntPtr _NET_WM_STATE_FULLSCREEN;
         public readonly IntPtr _XEMBED;
         public readonly IntPtr _XEMBED_INFO;
         public readonly IntPtr _MOTIF_WM_HINTS;

+ 21 - 11
src/Avalonia.X11/X11Window.cs

@@ -220,16 +220,11 @@ namespace Avalonia.X11
             var decorations = MotifDecorations.Menu | MotifDecorations.Title | MotifDecorations.Border |
                               MotifDecorations.Maximize | MotifDecorations.Minimize | MotifDecorations.ResizeH;
 
-            if (_popup || _systemDecorations == SystemDecorations.None)
-            {
+            if (_popup 
+                || _systemDecorations == SystemDecorations.None) 
                 decorations = 0;
-            }
-            else if (_systemDecorations == SystemDecorations.BorderOnly)
-            {
-                decorations = MotifDecorations.Border;
-            }
 
-            if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly)
+            if (!_canResize)
             {
                 functions &= ~(MotifFunctions.Resize | MotifFunctions.Maximize);
                 decorations &= ~(MotifDecorations.Maximize | MotifDecorations.ResizeH);
@@ -252,7 +247,7 @@ namespace Avalonia.X11
             var min = _minMaxSize.minSize;
             var max = _minMaxSize.maxSize;
 
-            if (!_canResize || _systemDecorations == SystemDecorations.BorderOnly)
+            if (!_canResize)
                 max = min = _realSize;
             
             if (preResize.HasValue)
@@ -552,12 +547,21 @@ namespace Avalonia.X11
                 else if (value == WindowState.Maximized)
                 {
                     ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_HIDDEN);
+                    ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_FULLSCREEN);
                     ChangeWMAtoms(true, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT,
                         _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ);
                 }
+                else if (value == WindowState.FullScreen)
+                {
+                    ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_HIDDEN);
+                    ChangeWMAtoms(true, _x11.Atoms._NET_WM_STATE_FULLSCREEN);
+                    ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT,
+                        _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ);
+                }
                 else
                 {
                     ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_HIDDEN);
+                    ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_FULLSCREEN);
                     ChangeWMAtoms(false, _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT,
                         _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ);
                 }
@@ -585,6 +589,12 @@ namespace Avalonia.X11
                             break;
                         }
 
+                        if(pitems[c] == _x11.Atoms._NET_WM_STATE_FULLSCREEN)
+                        {
+                            state = WindowState.FullScreen;
+                            break;
+                        }
+
                         if (pitems[c] == _x11.Atoms._NET_WM_STATE_MAXIMIZED_HORZ ||
                             pitems[c] == _x11.Atoms._NET_WM_STATE_MAXIMIZED_VERT)
                         {
@@ -810,7 +820,7 @@ namespace Avalonia.X11
         
         public void SetSystemDecorations(SystemDecorations enabled)
         {
-            _systemDecorations = enabled;
+            _systemDecorations = enabled == SystemDecorations.Full ? SystemDecorations.Full : SystemDecorations.None;
             UpdateMotifHints();
             UpdateSizeHints(null);
         }
@@ -1052,7 +1062,7 @@ namespace Avalonia.X11
 
         void ChangeWMAtoms(bool enable, params IntPtr[] atoms)
         {
-            if (atoms.Length < 1 || atoms.Length > 4)
+            if (atoms.Length != 1 && atoms.Length != 2)
                 throw new ArgumentException();
 
             if (!_mapped)

+ 10 - 7
src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs

@@ -4,7 +4,8 @@ using System.Globalization;
 
 namespace Avalonia.Markup.Xaml.Converters
 {
-	using System.ComponentModel;
+    using System.ComponentModel;
+    using Avalonia.Utilities;
 
     public class PointsListTypeConverter : TypeConverter
     {
@@ -15,15 +16,17 @@ namespace Avalonia.Markup.Xaml.Converters
 
         public override object ConvertFrom(ITypeDescriptorContext context, CultureInfo culture, object value)
         {
-            string strValue = (string)value;
-            string[] pointStrs = strValue.Split(new[] { ' ', '\t', '\r', '\n' }, StringSplitOptions.RemoveEmptyEntries);
-            var result = new List<Point>(pointStrs.Length);
-            foreach (var pointStr in pointStrs)
+            var points = new List<Point>();
+
+            using (var tokenizer = new StringTokenizer((string)value, CultureInfo.InvariantCulture, exceptionMessage: "Invalid PointsList."))
             {
-                result.Add(Point.Parse(pointStr));
+                while (tokenizer.TryReadDouble(out double x))
+                {
+                    points.Add(new Point(x, tokenizer.ReadDouble()));
+                }
             }
 
-            return result;
+            return points;
         }
     }
 }

+ 1 - 0
src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs

@@ -100,6 +100,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions
                     => AddType(typeSystem.GetType(type), typeSystem.GetType(conv));
                 
                 Add("Avalonia.Media.IImage","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter");
+                Add("Avalonia.Media.Imaging.IBitmap","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter");
                 var ilist = typeSystem.GetType("System.Collections.Generic.IList`1");
                 AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")),
                     typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter"));

+ 45 - 22
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

@@ -30,6 +30,10 @@ namespace Avalonia.Skia
         private Matrix _currentTransform;
         private GRContext _grContext;
         private bool _disposed;
+
+        private readonly SKPaint _strokePaint = new SKPaint();
+        private readonly SKPaint _fillPaint = new SKPaint();
+
         /// <summary>
         /// Context create info.
         /// </summary>
@@ -153,7 +157,7 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public void DrawLine(IPen pen, Point p1, Point p2)
         {
-            using (var paint = CreatePaint(pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
+            using (var paint = CreatePaint(_strokePaint, pen, new Size(Math.Abs(p2.X - p1.X), Math.Abs(p2.Y - p1.Y))))
             {
                 Canvas.DrawLine((float) p1.X, (float) p1.Y, (float) p2.X, (float) p2.Y, paint.Paint);
             }
@@ -165,8 +169,8 @@ namespace Avalonia.Skia
             var impl = (GeometryImpl) geometry;
             var size = geometry.Bounds.Size;
 
-            using (var fill = brush != null ? CreatePaint(brush, size) : default(PaintWrapper))
-            using (var stroke = pen?.Brush != null ? CreatePaint(pen, size) : default(PaintWrapper))
+            using (var fill = brush != null ? CreatePaint(_fillPaint, brush, size) : default(PaintWrapper))
+            using (var stroke = pen?.Brush != null ? CreatePaint(_strokePaint, pen, size) : default(PaintWrapper))
             {
                 if (fill.Paint != null)
                 {
@@ -188,7 +192,7 @@ namespace Avalonia.Skia
 
             if (brush != null)
             {
-                using (var paint = CreatePaint(brush, rect.Size))
+                using (var paint = CreatePaint(_fillPaint, brush, rect.Size))
                 {
                     if (isRounded)
                     {
@@ -204,7 +208,7 @@ namespace Avalonia.Skia
 
             if (pen?.Brush != null)
             {
-                using (var paint = CreatePaint(pen, rect.Size))
+                using (var paint = CreatePaint(_strokePaint, pen, rect.Size))
                 {
                     if (isRounded)
                     {
@@ -222,7 +226,7 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public void DrawText(IBrush foreground, Point origin, IFormattedTextImpl text)
         {
-            using (var paint = CreatePaint(foreground, text.Bounds.Size))
+            using (var paint = CreatePaint(_fillPaint, foreground, text.Bounds.Size))
             {
                 var textImpl = (FormattedTextImpl) text;
                 textImpl.Draw(this, Canvas, origin.ToSKPoint(), paint, _canTextUseLcdRendering);
@@ -232,14 +236,14 @@ namespace Avalonia.Skia
         /// <inheritdoc />
         public void DrawGlyphRun(IBrush foreground, GlyphRun glyphRun, Point baselineOrigin)
         {
-            using (var paint = CreatePaint(foreground, glyphRun.Bounds.Size))
+            using (var paintWrapper = CreatePaint(_fillPaint, foreground, glyphRun.Bounds.Size))
             {
                 var glyphRunImpl = (GlyphRunImpl)glyphRun.GlyphRunImpl;
 
-                paint.ApplyTo(glyphRunImpl.Paint);
+                ConfigureTextRendering(paintWrapper);
 
                 Canvas.DrawText(glyphRunImpl.TextBlob, (float)baselineOrigin.X,
-                    (float)baselineOrigin.Y, glyphRunImpl.Paint);
+                    (float)baselineOrigin.Y, paintWrapper.Paint);
             }
         }
 
@@ -323,7 +327,7 @@ namespace Avalonia.Skia
             var paint = new SKPaint();
 
             Canvas.SaveLayer(paint);
-            _maskStack.Push(CreatePaint(mask, bounds.Size));
+            _maskStack.Push(CreatePaint(paint, mask, bounds.Size, true));
         }
 
         /// <inheritdoc />
@@ -364,6 +368,15 @@ namespace Avalonia.Skia
             }
         }
 
+        internal void ConfigureTextRendering(PaintWrapper wrapper)
+        {
+            var paint = wrapper.Paint;
+
+            paint.IsEmbeddedBitmapText = true;
+            paint.SubpixelText = true;
+            paint.LcdRenderText = _canTextUseLcdRendering;
+        }
+
         /// <summary>
         /// Configure paint wrapper for using gradient brush.
         /// </summary>
@@ -514,17 +527,16 @@ namespace Avalonia.Skia
         /// <summary>
         /// Creates paint wrapper for given brush.
         /// </summary>
+        /// <param name="paint">The paint to wrap.</param>
         /// <param name="brush">Source brush.</param>
         /// <param name="targetSize">Target size.</param>
+        /// <param name="disposePaint">Optional dispose of the supplied paint.</param>
         /// <returns>Paint wrapper for given brush.</returns>
-        internal PaintWrapper CreatePaint(IBrush brush, Size targetSize)
+        internal PaintWrapper CreatePaint(SKPaint paint, IBrush brush, Size targetSize, bool disposePaint = false)
         {
-            var paint = new SKPaint
-            {
-                IsAntialias = true
-            };
+            var paintWrapper = new PaintWrapper(paint, disposePaint);
 
-            var paintWrapper = new PaintWrapper(paint);
+            paint.IsAntialias = true;
 
             double opacity = brush.Opacity * _currentOpacity;
 
@@ -572,10 +584,12 @@ namespace Avalonia.Skia
         /// <summary>
         /// Creates paint wrapper for given pen.
         /// </summary>
+        /// <param name="paint">The paint to wrap.</param>
         /// <param name="pen">Source pen.</param>
         /// <param name="targetSize">Target size.</param>
+        /// <param name="disposePaint">Optional dispose of the supplied paint.</param>
         /// <returns></returns>
-        private PaintWrapper CreatePaint(IPen pen, Size targetSize)
+        private PaintWrapper CreatePaint(SKPaint paint, IPen pen, Size targetSize, bool disposePaint = false)
         {
             // In Skia 0 thickness means - use hairline rendering
             // and for us it means - there is nothing rendered.
@@ -584,8 +598,7 @@ namespace Avalonia.Skia
                 return default;
             }
 
-            var rv = CreatePaint(pen.Brush, targetSize);
-            var paint = rv.Paint;
+            var rv = CreatePaint(paint, pen.Brush, targetSize, disposePaint);
 
             paint.IsStroke = true;
             paint.StrokeWidth = (float) pen.Thickness;
@@ -668,7 +681,7 @@ namespace Avalonia.Skia
         /// <summary>
         /// Skia cached paint state.
         /// </summary>
-        private struct PaintState : IDisposable
+        private readonly struct PaintState : IDisposable
         {
             private readonly SKColor _color;
             private readonly SKShader _shader;
@@ -696,14 +709,16 @@ namespace Avalonia.Skia
         {
             //We are saving memory allocations there
             public readonly SKPaint Paint;
+            private readonly bool _disposePaint;
 
             private IDisposable _disposable1;
             private IDisposable _disposable2;
             private IDisposable _disposable3;
 
-            public PaintWrapper(SKPaint paint)
+            public PaintWrapper(SKPaint paint, bool disposePaint)
             {
                 Paint = paint;
+                _disposePaint = disposePaint;
 
                 _disposable1 = null;
                 _disposable2 = null;
@@ -751,7 +766,15 @@ namespace Avalonia.Skia
             /// <inheritdoc />
             public void Dispose()
             {
-                Paint?.Dispose();
+                if (_disposePaint)
+                {
+                    Paint?.Dispose();
+                }
+                else
+                {
+                    Paint?.Reset();
+                }
+
                 _disposable1?.Dispose();
                 _disposable2?.Dispose();
                 _disposable3?.Dispose();

+ 37 - 9
src/Skia/Avalonia.Skia/FontManagerImpl.cs

@@ -32,6 +32,27 @@ namespace Avalonia.Skia
         public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle,
             FontFamily fontFamily, CultureInfo culture, out FontKey fontKey)
         {
+            SKFontStyle skFontStyle;
+
+            switch (fontWeight)
+            {
+                case FontWeight.Normal when fontStyle == FontStyle.Normal:
+                    skFontStyle = SKFontStyle.Normal;
+                    break;
+                case FontWeight.Normal when fontStyle == FontStyle.Italic:
+                    skFontStyle = SKFontStyle.Italic;
+                    break;
+                case FontWeight.Bold when fontStyle == FontStyle.Normal:
+                    skFontStyle = SKFontStyle.Bold;
+                    break;
+                case FontWeight.Bold when fontStyle == FontStyle.Italic:
+                    skFontStyle = SKFontStyle.BoldItalic;
+                    break;
+                default:
+                    skFontStyle = new SKFontStyle((SKFontStyleWeight)fontWeight, SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle);
+                    break;
+            }
+
             if (culture == null)
             {
                 culture = CultureInfo.CurrentUICulture;
@@ -45,31 +66,32 @@ namespace Avalonia.Skia
             t_languageTagBuffer[0] = culture.TwoLetterISOLanguageName;
             t_languageTagBuffer[1] = culture.ThreeLetterISOLanguageName;
 
-            if (fontFamily != null)
+            if (fontFamily != null && fontFamily.FamilyNames.HasFallbacks)
             {
-                foreach (var familyName in fontFamily.FamilyNames)
+                var familyNames = fontFamily.FamilyNames;
+
+                for (var i = 1; i < familyNames.Count; i++)
                 {
-                    var skTypeface = _skFontManager.MatchCharacter(familyName, (SKFontStyleWeight)fontWeight,
-                        SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
+                    var skTypeface =
+                        _skFontManager.MatchCharacter(familyNames[i], skFontStyle, t_languageTagBuffer, codepoint);
 
                     if (skTypeface == null)
                     {
                         continue;
                     }
 
-                    fontKey = new FontKey(new FontFamily(familyName), fontWeight, fontStyle);
+                    fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle);
 
                     return true;
                 }
             }
             else
             {
-                var skTypeface = _skFontManager.MatchCharacter(null, (SKFontStyleWeight)fontWeight,
-                    SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, t_languageTagBuffer, codepoint);
+                var skTypeface = _skFontManager.MatchCharacter(null, skFontStyle, t_languageTagBuffer, codepoint);
 
                 if (skTypeface != null)
                 {
-                    fontKey = new FontKey(new FontFamily(skTypeface.FamilyName), fontWeight, fontStyle);
+                    fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle);
 
                     return true;
                 }
@@ -82,7 +104,7 @@ namespace Avalonia.Skia
 
         public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
         {
-            var skTypeface = SKTypeface.Default;
+            SKTypeface skTypeface = null;
 
             if (typeface.FontFamily.Key == null)
             {
@@ -109,6 +131,12 @@ namespace Avalonia.Skia
                 skTypeface = fontCollection.Get(typeface);
             }
 
+            if (skTypeface == null)
+            {
+                throw new InvalidOperationException(
+                    $"Could not create glyph typeface for: {typeface.FontFamily.Name}.");
+            }
+
             return new GlyphTypefaceImpl(skTypeface);
         }
     }

+ 13 - 2
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@@ -149,7 +149,17 @@ namespace Avalonia.Skia
             if (index >= Text.Length || index < 0)
             {
                 var r = rects.LastOrDefault();
-                return new Rect(r.X + r.Width, r.Y, 0, _lineHeight);
+
+                var c = Text[Text.Length - 1];
+
+                switch (c)
+                {
+                    case '\n':
+                    case '\r':
+                        return new Rect(r.X, r.Y, 0, _lineHeight);
+                    default:
+                        return new Rect(r.X + r.Width, r.Y, 0, _lineHeight);
+                }
             }
             return rects[index];
         }
@@ -266,7 +276,8 @@ namespace Avalonia.Skia
                                 if (fb != null)
                                 {
                                     //TODO: figure out how to get the brush size
-                                    currentWrapper = context.CreatePaint(fb, new Size());
+                                    currentWrapper = context.CreatePaint(new SKPaint { IsAntialias = true }, fb,
+                                        new Size());
                                 }
                                 else
                                 {

+ 1 - 8
src/Skia/Avalonia.Skia/GlyphRunImpl.cs

@@ -7,17 +7,11 @@ namespace Avalonia.Skia
     /// <inheritdoc />
     public class GlyphRunImpl : IGlyphRunImpl
     {
-        public GlyphRunImpl(SKPaint paint, SKTextBlob textBlob)
+        public GlyphRunImpl(SKTextBlob textBlob)
         {
-            Paint = paint;
             TextBlob = textBlob;
         }
 
-        /// <summary>
-        ///     Gets the paint to draw with.
-        /// </summary>
-        public SKPaint Paint { get; }
-
         /// <summary>
         ///     Gets the text blob to draw.
         /// </summary>
@@ -26,7 +20,6 @@ namespace Avalonia.Skia
         void IDisposable.Dispose()
         {
             TextBlob.Dispose();
-            Paint.Dispose();
         }
     }
 }

+ 68 - 58
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

@@ -18,7 +18,7 @@ namespace Avalonia.Skia
 
         private GRContext GrContext { get; }
 
-        public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu)
+        public PlatformRenderInterface(ICustomSkiaGpu customSkiaGpu, long maxResourceBytes = 100663296)
         {
             if (customSkiaGpu != null)
             {
@@ -26,6 +26,10 @@ namespace Avalonia.Skia
 
                 GrContext = _customSkiaGpu.GrContext;
 
+                GrContext.GetResourceCacheLimits(out var maxResources, out _);
+
+                GrContext.SetResourceCacheLimits(maxResources, maxResourceBytes);
+
                 return;
             }
 
@@ -39,6 +43,10 @@ namespace Avalonia.Skia
                     : GRGlInterface.AssembleGlesInterface((_, proc) => display.GlInterface.GetProcAddress(proc)))
                 {
                     GrContext = GRContext.Create(GRBackend.OpenGL, iface);
+
+                    GrContext.GetResourceCacheLimits(out var maxResources, out _);
+
+                    GrContext.SetResourceCacheLimits(maxResources, maxResourceBytes);
                 }
                 display.ClearContext();
             }
@@ -149,6 +157,16 @@ namespace Avalonia.Skia
             return new WriteableBitmapImpl(size, dpi, format);
         }
 
+        private static readonly SKPaint s_paint = new SKPaint
+        {
+            TextEncoding = SKTextEncoding.GlyphId,
+            IsAntialias = true,
+            IsStroke = false,
+            SubpixelText = true
+        };
+
+        private static readonly SKTextBlobBuilder s_textBlobBuilder = new SKTextBlobBuilder();
+
         /// <inheritdoc />
         public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width)
         {
@@ -158,92 +176,84 @@ namespace Avalonia.Skia
 
             var typeface = glyphTypeface.Typeface;
 
-            var paint = new SKPaint
-            {
-                TextSize = (float)glyphRun.FontRenderingEmSize,
-                Typeface = typeface,
-                TextEncoding = SKTextEncoding.GlyphId,
-                IsAntialias = true,
-                IsStroke = false,
-                SubpixelText = true
-            };
+            s_paint.TextSize = (float)glyphRun.FontRenderingEmSize;
+            s_paint.Typeface = typeface;
 
-            using (var textBlobBuilder = new SKTextBlobBuilder())
-            {
-                SKTextBlob textBlob;
 
-                width = 0;
+            SKTextBlob textBlob;
 
-                var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
+            width = 0;
 
-                if (glyphRun.GlyphOffsets.IsEmpty)
-                {
-                    if (glyphTypeface.IsFixedPitch)
-                    {
-                        textBlobBuilder.AddRun(paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span);
-
-                        textBlob = textBlobBuilder.Build();
-
-                        width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length;
-                    }
-                    else
-                    {
-                        var buffer = textBlobBuilder.AllocateHorizontalRun(paint, count, 0);
-
-                        var positions = buffer.GetPositionSpan();
+            var scale = (float)(glyphRun.FontRenderingEmSize / glyphTypeface.DesignEmHeight);
 
-                        for (var i = 0; i < count; i++)
-                        {
-                            positions[i] = (float)width;
-
-                            if (glyphRun.GlyphAdvances.IsEmpty)
-                            {
-                                width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
-                            }
-                            else
-                            {
-                                width += glyphRun.GlyphAdvances[i];
-                            }
-                        }
+            if (glyphRun.GlyphOffsets.IsEmpty)
+            {
+                if (glyphTypeface.IsFixedPitch)
+                {
+                    s_textBlobBuilder.AddRun(s_paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span);
 
-                        buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
+                    textBlob = s_textBlobBuilder.Build();
 
-                        textBlob = textBlobBuilder.Build();
-                    }
+                    width = glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[0]) * scale * glyphRun.GlyphIndices.Length;
                 }
                 else
                 {
-                    var buffer = textBlobBuilder.AllocatePositionedRun(paint, count);
-
-                    var glyphPositions = buffer.GetPositionSpan();
+                    var buffer = s_textBlobBuilder.AllocateHorizontalRun(s_paint, count, 0);
 
-                    var currentX = 0.0;
+                    var positions = buffer.GetPositionSpan();
 
                     for (var i = 0; i < count; i++)
                     {
-                        var glyphOffset = glyphRun.GlyphOffsets[i];
-
-                        glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
+                        positions[i] = (float)width;
 
                         if (glyphRun.GlyphAdvances.IsEmpty)
                         {
-                            currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
+                            width += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
                         }
                         else
                         {
-                            currentX += glyphRun.GlyphAdvances[i];
+                            width += glyphRun.GlyphAdvances[i];
                         }
                     }
 
                     buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
 
-                    width = currentX;
+                    textBlob = s_textBlobBuilder.Build();
+                }
+            }
+            else
+            {
+                var buffer = s_textBlobBuilder.AllocatePositionedRun(s_paint, count);
+
+                var glyphPositions = buffer.GetPositionSpan();
+
+                var currentX = 0.0;
+
+                for (var i = 0; i < count; i++)
+                {
+                    var glyphOffset = glyphRun.GlyphOffsets[i];
 
-                    textBlob = textBlobBuilder.Build();
+                    glyphPositions[i] = new SKPoint((float)(currentX + glyphOffset.X), (float)glyphOffset.Y);
+
+                    if (glyphRun.GlyphAdvances.IsEmpty)
+                    {
+                        currentX += glyphTypeface.GetGlyphAdvance(glyphRun.GlyphIndices[i]) * scale;
+                    }
+                    else
+                    {
+                        currentX += glyphRun.GlyphAdvances[i];
+                    }
                 }
 
-                return new GlyphRunImpl(paint, textBlob);
+                buffer.SetGlyphs(glyphRun.GlyphIndices.Buffer.Span);
+
+                width = currentX;
+
+                textBlob = s_textBlobBuilder.Build();
             }
+
+            return new GlyphRunImpl(textBlob);
+
         }
     }
 }

+ 2 - 2
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@@ -19,7 +19,7 @@ namespace Avalonia.Skia
 
         public SKTypeface Get(Typeface typeface)
         {
-            var key = new FontKey(typeface.FontFamily, typeface.Weight, typeface.Style);
+            var key = new FontKey(typeface.FontFamily.Name, typeface.Weight, typeface.Style);
 
             return GetNearestMatch(_typefaces, key);
         }
@@ -49,7 +49,7 @@ namespace Avalonia.Skia
 
             if (keys.Length == 0)
             {
-                return SKTypeface.Default;
+                return null;
             }
 
             key = keys[0];

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

@@ -54,7 +54,7 @@ namespace Avalonia.Skia
                     continue;
                 }
 
-                var key = new FontKey(fontFamily, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant);
+                var key = new FontKey(fontFamily.Name, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant);
 
                 typeFaceCollection.AddTypeface(key, typeface);
             }

+ 9 - 0
src/Skia/Avalonia.Skia/SkiaOptions.cs

@@ -8,9 +8,18 @@ namespace Avalonia
     /// </summary>
     public class SkiaOptions
     {
+        public SkiaOptions()
+        {
+            MaxGpuResourceSizeBytes = 100663296; // Value taken from skia.
+        }
         /// <summary>
         /// Custom gpu factory to use. Can be used to customize behavior of Skia renderer.
         /// </summary>
         public Func<ICustomSkiaGpu> CustomGpuFactory { get; set; }
+
+        /// <summary>
+        /// The maximum number of bytes for video memory to store textures and resources.
+        /// </summary>
+        public long MaxGpuResourceSizeBytes { get; set; }
     }
 }

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

@@ -18,7 +18,7 @@ namespace Avalonia.Skia
         public static void Initialize(SkiaOptions options)
         {
             var customGpu = options.CustomGpuFactory?.Invoke();
-            var renderInterface = new PlatformRenderInterface(customGpu);
+            var renderInterface = new PlatformRenderInterface(customGpu, options.MaxGpuResourceSizeBytes);
 
             AvaloniaLocator.CurrentMutable
                 .Bind<IPlatformRenderInterface>().ToConstant(renderInterface)

+ 1 - 1
src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs

@@ -50,7 +50,7 @@ namespace Avalonia.Direct2D1.Media
 
                 var fontFamilyName = font.FontFamily.FamilyNames.GetString(0);
 
-                fontKey = new FontKey(new FontFamily(fontFamilyName), fontWeight, fontStyle);
+                fontKey = new FontKey(fontFamilyName, fontWeight, fontStyle);
 
                 return true;
             }

+ 55 - 0
src/Windows/Avalonia.Win32/Interop/TaskBarList.cs

@@ -0,0 +1,55 @@
+using System;
+using System.Runtime.InteropServices;
+using static Avalonia.Win32.Interop.UnmanagedMethods;
+
+namespace Avalonia.Win32.Interop
+{
+    internal class TaskBarList
+    {
+        private static IntPtr s_taskBarList;
+        private static HrInit s_hrInitDelegate;
+        private static MarkFullscreenWindow s_markFullscreenWindowDelegate;
+
+        /// <summary>
+        /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc
+        /// </summary>
+        /// <param name="fullscreen">Fullscreen state.</param>
+        public static unsafe void MarkFullscreen(IntPtr hwnd, bool fullscreen)
+        {
+            if (s_taskBarList == IntPtr.Zero)
+            {
+                Guid clsid = ShellIds.TaskBarList;
+                Guid iid = ShellIds.ITaskBarList2;
+
+                int result = CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out s_taskBarList);
+
+                if (s_taskBarList != IntPtr.Zero)
+                {
+                    var ptr = (ITaskBarList2VTable**)s_taskBarList.ToPointer();
+
+                    if (s_hrInitDelegate is null)
+                    {
+                        s_hrInitDelegate = Marshal.GetDelegateForFunctionPointer<HrInit>((*ptr)->HrInit);
+                    }
+
+                    if (s_hrInitDelegate(s_taskBarList) != HRESULT.S_OK)
+                    {
+                        s_taskBarList = IntPtr.Zero;
+                    }
+                }
+            }
+
+            if (s_taskBarList != IntPtr.Zero)
+            {
+                var ptr = (ITaskBarList2VTable**)s_taskBarList.ToPointer();
+
+                if (s_markFullscreenWindowDelegate is null)
+                {
+                    s_markFullscreenWindowDelegate = Marshal.GetDelegateForFunctionPointer<MarkFullscreenWindow>((*ptr)->MarkFullscreenWindow);
+                }
+
+                s_markFullscreenWindowDelegate(s_taskBarList, hwnd, fullscreen);
+            }
+        }
+    }
+}

+ 23 - 1
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@@ -460,6 +460,7 @@ namespace Avalonia.Win32.Interop
             WS_SIZEFRAME = 0x40000,
             WS_SYSMENU = 0x80000,
             WS_TABSTOP = 0x10000,
+            WS_THICKFRAME = 0x40000,
             WS_VISIBLE = 0x10000000,
             WS_VSCROLL = 0x200000,
             WS_EX_DLGMODALFRAME = 0x00000001,
@@ -1146,7 +1147,10 @@ namespace Avalonia.Win32.Interop
         internal static extern int CoCreateInstance(ref Guid clsid,
             IntPtr ignore1, int ignore2, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown), Out] out object pUnkOuter);
 
-        
+        [DllImport("ole32.dll", PreserveSig = true)]
+        internal static extern int CoCreateInstance(ref Guid clsid,
+            IntPtr ignore1, int ignore2, ref Guid iid, [Out] out IntPtr pUnkOuter);
+
         [DllImport("shell32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
         internal static extern int SHCreateItemFromParsingName([MarshalAs(UnmanagedType.LPWStr)] string pszPath, IntPtr pbc, ref Guid riid, [MarshalAs(UnmanagedType.Interface)] out IShellItem ppv);
 
@@ -1642,6 +1646,8 @@ namespace Avalonia.Win32.Interop
             public static readonly Guid SaveFileDialog = Guid.Parse("C0B4E2F3-BA21-4773-8DBA-335EC946EB8B");
             public static readonly Guid IFileDialog = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8");
             public static readonly Guid IShellItem = Guid.Parse("43826D1E-E718-42EE-BC55-A1E261C37BFE");
+            public static readonly Guid TaskBarList = Guid.Parse("56FDF344-FD6D-11D0-958A-006097C9A090");
+            public static readonly Guid ITaskBarList2 = Guid.Parse("ea1afb91-9e28-4b86-90e9-9e9f8a5eefaf");
         }
 
         [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
@@ -1874,6 +1880,22 @@ namespace Avalonia.Win32.Interop
             [MarshalAs(UnmanagedType.LPWStr)]
             public string pszSpec;
         }
+
+        public delegate void MarkFullscreenWindow(IntPtr This, IntPtr hwnd, [MarshalAs(UnmanagedType.Bool)] bool fullscreen);
+        public delegate HRESULT HrInit(IntPtr This);
+
+        public struct ITaskBarList2VTable
+        {
+            public IntPtr IUnknown1;
+            public IntPtr IUnknown2;
+            public IntPtr IUnknown3;
+            public IntPtr HrInit;
+            public IntPtr AddTab;
+            public IntPtr DeleteTab;
+            public IntPtr ActivateTab;
+            public IntPtr SetActiveAlt;
+            public IntPtr MarkFullscreenWindow;
+        }
     }
 
     [Flags]

+ 4 - 7
src/Windows/Avalonia.Win32/ScreenImpl.cs

@@ -8,7 +8,7 @@ namespace Avalonia.Win32
 {
     public class ScreenImpl : IScreenImpl
     {
-        public  int ScreenCount
+        public int ScreenCount
         {
             get => GetSystemMetrics(SystemMetric.SM_CMONITORS);
         }
@@ -33,7 +33,7 @@ namespace Avalonia.Win32
                                 var shcore = LoadLibrary("shcore.dll");
                                 var method = GetProcAddress(shcore, nameof(GetDpiForMonitor));
                                 if (method != IntPtr.Zero)
-                                { 
+                                {
                                     GetDpiForMonitor(monitor, MONITOR_DPI_TYPE.MDT_EFFECTIVE_DPI, out var x, out _);
                                     dpi = (double)x;
                                 }
@@ -51,11 +51,8 @@ namespace Avalonia.Win32
 
                                 RECT bounds = monitorInfo.rcMonitor;
                                 RECT workingArea = monitorInfo.rcWork;
-                                PixelRect avaloniaBounds = new PixelRect(bounds.left, bounds.top, bounds.right - bounds.left,
-                                    bounds.bottom - bounds.top);
-                                PixelRect avaloniaWorkArea =
-                                    new PixelRect(workingArea.left, workingArea.top, workingArea.right - workingArea.left,
-                                        workingArea.bottom - workingArea.top);
+                                PixelRect avaloniaBounds = bounds.ToPixelRect();
+                                PixelRect avaloniaWorkArea = workingArea.ToPixelRect();
                                 screens[index] =
                                     new WinScreen(dpi / 96.0d, avaloniaBounds, avaloniaWorkArea, monitorInfo.dwFlags == 1,
                                         monitor);

+ 7 - 8
src/Windows/Avalonia.Win32/SystemDialogImpl.cs

@@ -5,7 +5,6 @@ using System.Runtime.InteropServices;
 using System.Threading.Tasks;
 using Avalonia.Controls;
 using Avalonia.Controls.Platform;
-using Avalonia.Platform;
 using Avalonia.Win32.Interop;
 
 namespace Avalonia.Win32
@@ -16,16 +15,16 @@ namespace Avalonia.Win32
         private const UnmanagedMethods.FOS DefaultDialogOptions = UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE |
             UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT;
 
-        public unsafe Task<string[]> ShowFileDialogAsync(FileDialog dialog, IWindowImpl parent)
+        public unsafe Task<string[]> ShowFileDialogAsync(FileDialog dialog, Window parent)
         {
-            var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero;
+            var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero;
             return Task.Factory.StartNew(() =>
             {
                 var result = Array.Empty<string>();
 
                 Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog;
                 Guid iid = UnmanagedMethods.ShellIds.IFileDialog;
-                UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk);
+                UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out object unk);
                 var frm = (UnmanagedMethods.IFileDialog)unk;
 
                 var openDialog = dialog as OpenFileDialog;
@@ -98,17 +97,17 @@ namespace Avalonia.Win32
             });
         }
 
-        public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, IWindowImpl parent)
+        public Task<string> ShowFolderDialogAsync(OpenFolderDialog dialog, Window parent)
         {
             return Task.Factory.StartNew(() =>
             {
                 string result = string.Empty;
 
-                var hWnd = parent?.Handle?.Handle ?? IntPtr.Zero;
+                var hWnd = parent?.PlatformImpl?.Handle?.Handle ?? IntPtr.Zero;
                 Guid clsid = UnmanagedMethods.ShellIds.OpenFileDialog;
-                Guid iid  = UnmanagedMethods.ShellIds.IFileDialog;
+                Guid iid = UnmanagedMethods.ShellIds.IFileDialog;
 
-                UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out var unk);
+                UnmanagedMethods.CoCreateInstance(ref clsid, IntPtr.Zero, 1, ref iid, out object unk);
                 var frm = (UnmanagedMethods.IFileDialog)unk;
                 uint options;
                 frm.GetOptions(out options);

+ 13 - 0
src/Windows/Avalonia.Win32/Win32TypeExtensions.cs

@@ -0,0 +1,13 @@
+using static Avalonia.Win32.Interop.UnmanagedMethods;
+
+namespace Avalonia.Win32
+{
+    internal static class Win32TypeExtensions
+    {
+        public static PixelRect ToPixelRect(this RECT rect)
+        {
+            return new PixelRect(rect.left, rect.top, rect.right - rect.left,
+                    rect.bottom - rect.top);
+        }
+    }
+}

+ 166 - 26
src/Windows/Avalonia.Win32/WindowImpl.cs

@@ -37,10 +37,14 @@ namespace Avalonia.Win32
                 { WindowEdge.West, HitTestValues.HTLEFT }
             };
 
+        private SavedWindowInfo _savedWindowInfo;
+        private bool _isFullScreenActive;
+
 #if USE_MANAGED_DRAG
         private readonly ManagedWindowResizeDragHelper _managedDrag;
 #endif
 
+        private const WindowStyles WindowStateMask = (WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE);
         private readonly List<WindowImpl> _disabledBy;
         private readonly TouchDevice _touchDevice;
         private readonly MouseDevice _mouseDevice;
@@ -82,7 +86,9 @@ namespace Avalonia.Win32
 
             _windowProperties = new WindowProperties
             {
-                ShowInTaskbar = false, IsResizable = true, Decorations = SystemDecorations.Full
+                ShowInTaskbar = false,
+                IsResizable = true,
+                Decorations = SystemDecorations.Full
             };
             _rendererLock = new ManagedDeferredRendererLock();
 
@@ -538,27 +544,98 @@ namespace Avalonia.Win32
             }
         }
 
+        /// <summary>
+        /// Ported from https://github.com/chromium/chromium/blob/master/ui/views/win/fullscreen_handler.cc
+        /// Method must only be called from inside UpdateWindowProperties.
+        /// </summary>
+        /// <param name="fullscreen"></param>
+        private void SetFullScreen(bool fullscreen)
+        {
+            if (fullscreen)
+            {
+                GetWindowRect(_hwnd, out var windowRect);
+                _savedWindowInfo.WindowRect = windowRect;
+
+                var current = GetStyle();
+                var currentEx = GetExtendedStyle();
+
+                _savedWindowInfo.Style = current;
+                _savedWindowInfo.ExStyle = currentEx;
+
+                // Set new window style and size.
+                SetStyle(current & ~(WindowStyles.WS_CAPTION | WindowStyles.WS_THICKFRAME), false);
+                SetExtendedStyle(currentEx & ~(WindowStyles.WS_EX_DLGMODALFRAME | WindowStyles.WS_EX_WINDOWEDGE | WindowStyles.WS_EX_CLIENTEDGE | WindowStyles.WS_EX_STATICEDGE), false);
+
+                // On expand, if we're given a window_rect, grow to it, otherwise do
+                // not resize.
+                MONITORINFO monitor_info = MONITORINFO.Create();
+                GetMonitorInfo(MonitorFromWindow(_hwnd, MONITOR.MONITOR_DEFAULTTONEAREST), ref monitor_info);
+
+                var window_rect = monitor_info.rcMonitor.ToPixelRect();
+
+                SetWindowPos(_hwnd, IntPtr.Zero, window_rect.X, window_rect.Y,
+                             window_rect.Width, window_rect.Height,
+                             SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED);
+
+                _isFullScreenActive = true;
+            }
+            else
+            {
+                // Reset original window style and size.  The multiple window size/moves
+                // here are ugly, but if SetWindowPos() doesn't redraw, the taskbar won't be
+                // repainted.  Better-looking methods welcome.
+                _isFullScreenActive = false;
+
+                var windowStates = GetWindowStateStyles();
+                SetStyle((_savedWindowInfo.Style & ~WindowStateMask) | windowStates, false);
+                SetExtendedStyle(_savedWindowInfo.ExStyle, false);
+
+                // On restore, resize to the previous saved rect size.
+                var new_rect = _savedWindowInfo.WindowRect.ToPixelRect();
+
+                SetWindowPos(_hwnd, IntPtr.Zero, new_rect.X, new_rect.Y, new_rect.Width,
+                             new_rect.Height,
+                            SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED);
+
+                UpdateWindowProperties(_windowProperties, true);
+            }
+
+            TaskBarList.MarkFullscreen(_hwnd, fullscreen);
+        }
+
         private void ShowWindow(WindowState state)
         {
             ShowWindowCommand command;
 
+            var newWindowProperties = _windowProperties;
+
             switch (state)
             {
                 case WindowState.Minimized:
+                    newWindowProperties.IsFullScreen = false;
                     command = ShowWindowCommand.Minimize;
                     break;
                 case WindowState.Maximized:
+                    newWindowProperties.IsFullScreen = false;
                     command = ShowWindowCommand.Maximize;
                     break;
 
                 case WindowState.Normal:
+                    newWindowProperties.IsFullScreen = false;
                     command = ShowWindowCommand.Restore;
                     break;
 
+                case WindowState.FullScreen:
+                    newWindowProperties.IsFullScreen = true;
+                    UpdateWindowProperties(newWindowProperties);
+                    return;
+
                 default:
                     throw new ArgumentException("Invalid WindowState.");
             }
 
+            UpdateWindowProperties(newWindowProperties);
+
             UnmanagedMethods.ShowWindow(_hwnd, command);
 
             if (state == WindowState.Maximized)
@@ -590,22 +667,69 @@ namespace Avalonia.Win32
                     SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW);
                 }
             }
+        }        
+
+        private WindowStyles GetWindowStateStyles ()
+        {
+            return GetStyle() & WindowStateMask;
         }
 
-        private WindowStyles GetStyle() => (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE);
+        private WindowStyles GetStyle()
+        {
+            if (_isFullScreenActive)
+            {
+                return _savedWindowInfo.Style;
+            }
+            else
+            {
+                return (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE);
+            }
+        }
 
-        private WindowStyles GetExtendedStyle() => (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE);
+        private WindowStyles GetExtendedStyle()
+        {
+            if (_isFullScreenActive)
+            {
+                return _savedWindowInfo.ExStyle;
+            }
+            else
+            {
+                return (WindowStyles)GetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE);
+            }
+        }
 
-        private void SetStyle(WindowStyles style) => SetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE, (uint)style);
+        private void SetStyle(WindowStyles style, bool save = true)
+        {
+            if (save)
+            {
+                _savedWindowInfo.Style = style;
+            }
 
-        private void SetExtendedStyle(WindowStyles style) => SetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE, (uint)style);
+            if (!_isFullScreenActive)
+            {
+                SetWindowLong(_hwnd, (int)WindowLongParam.GWL_STYLE, (uint)style);
+            }
+        }
+
+        private void SetExtendedStyle(WindowStyles style, bool save = true)
+        {
+            if (save)
+            {
+                _savedWindowInfo.ExStyle = style;
+            }
+
+            if (!_isFullScreenActive)
+            {
+                SetWindowLong(_hwnd, (int)WindowLongParam.GWL_EXSTYLE, (uint)style);
+            }
+        }
 
         private void UpdateEnabled()
         {
             EnableWindow(_hwnd, _disabledBy.Count == 0);
         }
 
-        private void UpdateWindowProperties(WindowProperties newProperties)
+        private void UpdateWindowProperties(WindowProperties newProperties, bool forceChanges = false)
         {
             var oldProperties = _windowProperties;
 
@@ -613,7 +737,7 @@ namespace Avalonia.Win32
             // according to the new values already.
             _windowProperties = newProperties;
 
-            if (oldProperties.ShowInTaskbar != newProperties.ShowInTaskbar)
+            if ((oldProperties.ShowInTaskbar != newProperties.ShowInTaskbar) || forceChanges)
             {
                 var exStyle = GetExtendedStyle();
 
@@ -632,7 +756,7 @@ namespace Avalonia.Win32
                 // Otherwise it will still show in the taskbar.
             }
 
-            if (oldProperties.IsResizable != newProperties.IsResizable)
+            if ((oldProperties.IsResizable != newProperties.IsResizable) || forceChanges)
             {
                 var style = GetStyle();
 
@@ -648,7 +772,12 @@ namespace Avalonia.Win32
                 SetStyle(style);
             }
 
-            if (oldProperties.Decorations != newProperties.Decorations)
+            if (oldProperties.IsFullScreen != newProperties.IsFullScreen)
+            {
+                SetFullScreen(newProperties.IsFullScreen);
+            }
+
+            if ((oldProperties.Decorations != newProperties.Decorations) || forceChanges)
             {
                 var style = GetStyle();
 
@@ -663,30 +792,33 @@ namespace Avalonia.Win32
                     style &= ~fullDecorationFlags;
                 }
 
-                var margins = new MARGINS
+                SetStyle(style);
+
+                if (!_isFullScreenActive)
                 {
-                    cyBottomHeight = newProperties.Decorations == SystemDecorations.BorderOnly ? 1 : 0
-                };
+                    var margins = new MARGINS
+                    {
+                        cyBottomHeight = newProperties.Decorations == SystemDecorations.BorderOnly ? 1 : 0
+                    };
 
-                DwmExtendFrameIntoClientArea(_hwnd, ref margins);
+                    DwmExtendFrameIntoClientArea(_hwnd, ref margins);
 
-                GetClientRect(_hwnd, out var oldClientRect);
-                var oldClientRectOrigin = new POINT();
-                ClientToScreen(_hwnd, ref oldClientRectOrigin);
-                oldClientRect.Offset(oldClientRectOrigin);
+                    GetClientRect(_hwnd, out var oldClientRect);
+                    var oldClientRectOrigin = new POINT();
+                    ClientToScreen(_hwnd, ref oldClientRectOrigin);
+                    oldClientRect.Offset(oldClientRectOrigin);
 
-                SetStyle(style);
+                    var newRect = oldClientRect;
 
-                var newRect = oldClientRect;
+                    if (newProperties.Decorations == SystemDecorations.Full)
+                    {
+                        AdjustWindowRectEx(ref newRect, (uint)style, false, (uint)GetExtendedStyle());
+                    }
 
-                if (newProperties.Decorations == SystemDecorations.Full)
-                {
-                    AdjustWindowRectEx(ref newRect, (uint)style, false, (uint)GetExtendedStyle());
+                    SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height,
+                        SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE |
+                        SetWindowPosFlags.SWP_FRAMECHANGED);
                 }
-
-                SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height,
-                    SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE |
-                    SetWindowPosFlags.SWP_FRAMECHANGED);
             }
         }
 
@@ -713,11 +845,19 @@ namespace Avalonia.Win32
 
         IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle;
 
+        private struct SavedWindowInfo
+        {
+            public WindowStyles Style { get; set; }
+            public WindowStyles ExStyle { get; set; }
+            public RECT WindowRect { get; set; }
+        };
+
         private struct WindowProperties
         {
             public bool ShowInTaskbar;
             public bool IsResizable;
             public SystemDecorations Decorations;
+            public bool IsFullScreen;
         }
     }
 }

+ 49 - 0
tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs

@@ -14,6 +14,55 @@ namespace Avalonia.Animation.UnitTests
 {
     public class AnimationIterationTests
     {
+        [Fact]
+        public void Check_KeyTime_Correctly_Converted_To_Cue()
+        {
+            var keyframe1 = new KeyFrame()
+            {
+                Setters =
+                {
+                    new Setter(Border.WidthProperty, 100d),
+                },
+                KeyTime = TimeSpan.FromSeconds(0.5)
+            };
+
+            var keyframe2 = new KeyFrame()
+            {
+                Setters =
+                {
+                    new Setter(Border.WidthProperty, 0d),
+                },
+                KeyTime = TimeSpan.FromSeconds(0)
+            };
+
+            var animation = new Animation()
+            {
+                Duration = TimeSpan.FromSeconds(1),
+                Children =
+                {
+                    keyframe2,
+                    keyframe1
+                }
+            };
+
+            var border = new Border()
+            {
+                Height = 100d,
+                Width = 100d
+            };
+
+            var clock = new TestClock();
+            var animationRun = animation.RunAsync(border, clock);
+
+            clock.Step(TimeSpan.Zero); 
+            Assert.Equal(border.Width, 0d);
+
+            clock.Step(TimeSpan.FromSeconds(1)); 
+            Assert.Equal(border.Width, 100d);
+ 
+        }
+
+
         [Fact]
         public void Check_Initial_Inter_and_Trailing_Delay_Values()
         {

+ 145 - 0
tests/Avalonia.Animation.UnitTests/KeySplineTests.cs

@@ -0,0 +1,145 @@
+using System;
+using Avalonia.Controls.Shapes;
+using Avalonia.Media;
+using Avalonia.Styling;
+using Xunit;
+
+namespace Avalonia.Animation.UnitTests
+{
+    public class KeySplineTests
+    {
+        [Theory]
+        [InlineData("1,2 3,4")]
+        [InlineData("1 2 3 4")]
+        [InlineData("1 2,3 4")]
+        [InlineData("1,2,3,4")]
+        public void Can_Parse_KeySpline_Via_TypeConverter(string input)
+        {
+            var conv = new KeySplineTypeConverter();
+
+            var keySpline = (KeySpline)conv.ConvertFrom(input);
+
+            Assert.Equal(1, keySpline.ControlPointX1);
+            Assert.Equal(2, keySpline.ControlPointY1);
+            Assert.Equal(3, keySpline.ControlPointX2);
+            Assert.Equal(4, keySpline.ControlPointY2);
+        }
+
+        [Theory]
+        [InlineData(0.00)]
+        [InlineData(0.50)]
+        [InlineData(1.00)]
+        public void KeySpline_X_Values_In_Range_Do_Not_Throw(double input)
+        {
+            var keySpline = new KeySpline();
+            keySpline.ControlPointX1 = input; // no exception will be thrown -- test will fail if exception thrown
+            keySpline.ControlPointX2 = input; // no exception will be thrown -- test will fail if exception thrown
+        }
+
+        [Theory]
+        [InlineData(-0.01)]
+        [InlineData(1.01)]
+        public void KeySpline_X_Values_Cannot_Be_Out_Of_Range(double input)
+        {
+            var keySpline = new KeySpline();
+            Assert.Throws<ArgumentException>(() => keySpline.ControlPointX1 = input);
+            Assert.Throws<ArgumentException>(() => keySpline.ControlPointX2 = input);
+        }
+
+        /*
+          To get the test values for the KeySpline test, you can:
+          1) Grab the WPF sample for KeySpline animations from https://github.com/microsoft/WPF-Samples/tree/master/Animation/KeySplineAnimations
+          2) Add the following xaml somewhere:
+            <Button Content="Capture"
+                    Click="Button_Click"/>
+            <ScrollViewer VerticalScrollBarVisibility="Visible">
+                <TextBlock Name="CaptureData"
+                           Text="---"
+                           TextWrapping="Wrap" />
+            </ScrollViewer>
+          3) Add the following code to the code behind:
+            private void Button_Click(object sender, RoutedEventArgs e)
+            {
+                CaptureData.Text += string.Format("\n{0} | {1}", myTranslateTransform3D.OffsetX, (TimeSpan)ExampleStoryboard.GetCurrentTime(this));
+                CaptureData.Text +=
+                    "\nKeySpline=\"" + mySplineKeyFrame.KeySpline.ControlPoint1.X.ToString() + "," +
+                    mySplineKeyFrame.KeySpline.ControlPoint1.Y.ToString() + " " +
+                    mySplineKeyFrame.KeySpline.ControlPoint2.X.ToString() + "," +
+                    mySplineKeyFrame.KeySpline.ControlPoint2.Y.ToString() + "\"";
+                CaptureData.Text += "\n-----";
+            }
+          4) Run the app, mess with the slider values, then click the button to capture output values
+         **/
+
+        [Fact]
+        public void Check_KeySpline_Handled_properly()
+        {
+            var keyframe1 = new KeyFrame()
+            {
+                Setters =
+                {
+                    new Setter(RotateTransform.AngleProperty, -2.5d),
+                },
+                KeyTime = TimeSpan.FromSeconds(0)
+            };
+
+            var keyframe2 = new KeyFrame()
+            {
+                Setters =
+                {
+                    new Setter(RotateTransform.AngleProperty, 2.5d),
+                },
+                KeyTime = TimeSpan.FromSeconds(5),
+                KeySpline = new KeySpline(0.1123555056179775,
+                                          0.657303370786517,
+                                          0.8370786516853934,
+                                          0.499999999999999999)
+            };
+
+            var animation = new Animation()
+            {
+                Duration = TimeSpan.FromSeconds(5),
+                Children =
+                {
+                    keyframe1,
+                    keyframe2
+                },
+                IterationCount = new IterationCount(5),
+                PlaybackDirection = PlaybackDirection.Alternate
+            };
+
+            var rotateTransform = new RotateTransform(-2.5);
+            var rect = new Rectangle()
+            {
+                RenderTransform = rotateTransform
+            };
+
+            var clock = new TestClock();
+            var animationRun = animation.RunAsync(rect, clock);
+
+            // position is what you'd expect at end and beginning
+            clock.Step(TimeSpan.Zero);
+            Assert.Equal(rotateTransform.Angle, -2.5);
+            clock.Step(TimeSpan.FromSeconds(5));
+            Assert.Equal(rotateTransform.Angle, 2.5);
+
+            // test some points in between end and beginning
+            var tolerance = 0.01;
+            clock.Step(TimeSpan.Parse("00:00:10.0153932"));
+            var expected = -2.4122350198982545;
+            Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
+
+            clock.Step(TimeSpan.Parse("00:00:11.2655407"));
+            expected = -0.37153223002125113;
+            Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
+
+            clock.Step(TimeSpan.Parse("00:00:12.6158773"));
+            expected = 0.3967885416786294;
+            Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
+
+            clock.Step(TimeSpan.Parse("00:00:14.6495256"));
+            expected = 1.8016358493761722;
+            Assert.True(Math.Abs(rotateTransform.Angle - expected) <= tolerance);
+        }
+    }
+}

+ 169 - 152
tests/Avalonia.Controls.UnitTests/WindowTests.cs

@@ -156,7 +156,7 @@ namespace Avalonia.Controls.UnitTests
         {
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
-                var parent = Mock.Of<IWindowImpl>();
+                var parent = Mock.Of<Window>();
                 var renderer = new Mock<IRenderer>();
                 var target = new Window(CreateImpl(renderer));
 
@@ -171,7 +171,7 @@ namespace Avalonia.Controls.UnitTests
         {
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
-                var parent = Mock.Of<IWindowImpl>();
+                var parent = Mock.Of<Window>();
                 var target = new Window();
                 var raised = false;
 
@@ -203,7 +203,7 @@ namespace Avalonia.Controls.UnitTests
         {
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
-                var parent = new Mock<IWindowImpl>();
+                var parent = new Mock<Window>();
                 var windowImpl = new Mock<IWindowImpl>();
                 windowImpl.SetupProperty(x => x.Closed);
                 windowImpl.Setup(x => x.Scaling).Returns(1);
@@ -242,7 +242,7 @@ namespace Avalonia.Controls.UnitTests
         {
             using (UnitTestApplication.Start(TestServices.StyledWindow))
             {
-                var parent = new Mock<IWindowImpl>();
+                var parent = new Mock<Window>();
                 var windowImpl = new Mock<IWindowImpl>();
                 windowImpl.SetupProperty(x => x.Closed);
                 windowImpl.Setup(x => x.Scaling).Returns(1);
@@ -336,210 +336,227 @@ namespace Avalonia.Controls.UnitTests
             }
         }
 
-        [Fact]
-        public void Child_Should_Be_Measured_With_Width_And_Height_If_SizeToContent_Is_Manual()
+        public class SizingTests
         {
-            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            [Fact]
+            public void Child_Should_Be_Measured_With_Width_And_Height_If_SizeToContent_Is_Manual()
             {
-                var child = new ChildControl();
-                var target = new Window 
-                { 
-                    Width = 100,
-                    Height = 50,
-                    SizeToContent = SizeToContent.Manual,
-                    Content = child 
-                };
+                using (UnitTestApplication.Start(TestServices.StyledWindow))
+                {
+                    var child = new ChildControl();
+                    var target = new Window
+                    {
+                        Width = 100,
+                        Height = 50,
+                        SizeToContent = SizeToContent.Manual,
+                        Content = child
+                    };
 
-                target.Show();
+                    Show(target);
 
-                Assert.Equal(1, child.MeasureSizes.Count);
-                Assert.Equal(new Size(100, 50), child.MeasureSizes[0]);
+                    Assert.Equal(1, child.MeasureSizes.Count);
+                    Assert.Equal(new Size(100, 50), child.MeasureSizes[0]);
+                }
             }
-        }
 
-        [Fact]
-        public void Child_Should_Be_Measured_With_ClientSize_If_SizeToContent_Is_Manual_And_No_Width_Height_Specified()
-        {
-            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            [Fact]
+            public void Child_Should_Be_Measured_With_ClientSize_If_SizeToContent_Is_Manual_And_No_Width_Height_Specified()
             {
-                var windowImpl = MockWindowingPlatform.CreateWindowMock();
-                windowImpl.Setup(x => x.ClientSize).Returns(new Size(550, 450));
-
-                var child = new ChildControl();
-                var target = new Window(windowImpl.Object)
+                using (UnitTestApplication.Start(TestServices.StyledWindow))
                 {
-                    SizeToContent = SizeToContent.Manual,
-                    Content = child
-                };
+                    var windowImpl = MockWindowingPlatform.CreateWindowMock();
+                    windowImpl.Setup(x => x.ClientSize).Returns(new Size(550, 450));
 
-                target.Show();
+                    var child = new ChildControl();
+                    var target = new Window(windowImpl.Object)
+                    {
+                        SizeToContent = SizeToContent.Manual,
+                        Content = child
+                    };
 
-                Assert.Equal(1, child.MeasureSizes.Count);
-                Assert.Equal(new Size(550, 450), child.MeasureSizes[0]);
+                    Show(target);
+
+                    Assert.Equal(1, child.MeasureSizes.Count);
+                    Assert.Equal(new Size(550, 450), child.MeasureSizes[0]);
+                }
             }
-        }
 
-        [Fact]
-        public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight()
-        {
-            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            [Fact]
+            public void Child_Should_Be_Measured_With_Infinity_If_SizeToContent_Is_WidthAndHeight()
             {
-                var child = new ChildControl();
-                var target = new Window
+                using (UnitTestApplication.Start(TestServices.StyledWindow))
                 {
-                    Width = 100,
-                    Height = 50,
-                    SizeToContent = SizeToContent.WidthAndHeight,
-                    Content = child
-                };
+                    var child = new ChildControl();
+                    var target = new Window
+                    {
+                        Width = 100,
+                        Height = 50,
+                        SizeToContent = SizeToContent.WidthAndHeight,
+                        Content = child
+                    };
 
-                target.Show();
+                    Show(target);
 
-                Assert.Equal(1, child.MeasureSizes.Count);
-                Assert.Equal(Size.Infinity, child.MeasureSizes[0]);
+                    Assert.Equal(1, child.MeasureSizes.Count);
+                    Assert.Equal(Size.Infinity, child.MeasureSizes[0]);
+                }
             }
-        }
 
-        [Fact]
-        public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size()
-        {
-            // Issue #3784.
-            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            [Fact]
+            public void Should_Not_Have_Offset_On_Bounds_When_Content_Larger_Than_Max_Window_Size()
             {
-                var windowImpl = MockWindowingPlatform.CreateWindowMock();
-                var clientSize = new Size(200, 200);
-                var maxClientSize = new Size(480, 480);
-
-                windowImpl.Setup(x => x.Resize(It.IsAny<Size>())).Callback<Size>(size =>
+                // Issue #3784.
+                using (UnitTestApplication.Start(TestServices.StyledWindow))
                 {
-                    clientSize = size.Constrain(maxClientSize);
-                    windowImpl.Object.Resized?.Invoke(clientSize);
-                });
+                    var windowImpl = MockWindowingPlatform.CreateWindowMock();
+                    var clientSize = new Size(200, 200);
+                    var maxClientSize = new Size(480, 480);
 
-                windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
+                    windowImpl.Setup(x => x.Resize(It.IsAny<Size>())).Callback<Size>(size =>
+                    {
+                        clientSize = size.Constrain(maxClientSize);
+                        windowImpl.Object.Resized?.Invoke(clientSize);
+                    });
 
-                var child = new Canvas
-                {
-                    Width = 400,
-                    Height = 800,
-                };
-                var target = new Window(windowImpl.Object)
-                {
-                    SizeToContent = SizeToContent.WidthAndHeight,
-                    Content = child
-                };
+                    windowImpl.Setup(x => x.ClientSize).Returns(() => clientSize);
 
-                target.Show();
+                    var child = new Canvas
+                    {
+                        Width = 400,
+                        Height = 800,
+                    };
+                    var target = new Window(windowImpl.Object)
+                    {
+                        SizeToContent = SizeToContent.WidthAndHeight,
+                        Content = child
+                    };
+
+                    Show(target);
 
-                Assert.Equal(new Size(400, 480), target.Bounds.Size);
+                    Assert.Equal(new Size(400, 480), target.Bounds.Size);
 
-                // Issue #3784 causes this to be (0, 160) which makes no sense as Window has no
-                // parent control to be offset against.
-                Assert.Equal(new Point(0, 0), target.Bounds.Position);
+                    // Issue #3784 causes this to be (0, 160) which makes no sense as Window has no
+                    // parent control to be offset against.
+                    Assert.Equal(new Point(0, 0), target.Bounds.Position);
+                }
             }
-        }
 
-        [Fact]
-        public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_WidthAndHeight()
-        {
-            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            [Fact]
+            public void Width_Height_Should_Not_Be_NaN_After_Show_With_SizeToContent_WidthAndHeight()
             {
-                var child = new Canvas
+                using (UnitTestApplication.Start(TestServices.StyledWindow))
                 {
-                    Width = 400,
-                    Height = 800,
-                };
+                    var child = new Canvas
+                    {
+                        Width = 400,
+                        Height = 800,
+                    };
 
-                var target = new Window()
-                {
-                    SizeToContent = SizeToContent.WidthAndHeight,
-                    Content = child
-                };
+                    var target = new Window()
+                    {
+                        SizeToContent = SizeToContent.WidthAndHeight,
+                        Content = child
+                    };
 
-                target.Show();
+                    Show(target);
 
-                Assert.Equal(400, target.Width);
-                Assert.Equal(800, target.Height);
+                    Assert.Equal(400, target.Width);
+                    Assert.Equal(800, target.Height);
+                }
             }
-        }
 
-        [Fact]
-        public void SizeToContent_Should_Not_Be_Lost_On_Show()
-        {
-            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            [Fact]
+            public void SizeToContent_Should_Not_Be_Lost_On_Show()
             {
-                var child = new Canvas
+                using (UnitTestApplication.Start(TestServices.StyledWindow))
                 {
-                    Width = 400,
-                    Height = 800,
-                };
+                    var child = new Canvas
+                    {
+                        Width = 400,
+                        Height = 800,
+                    };
 
-                var target = new Window()
-                {
-                    SizeToContent = SizeToContent.WidthAndHeight,
-                    Content = child
-                };
+                    var target = new Window()
+                    {
+                        SizeToContent = SizeToContent.WidthAndHeight,
+                        Content = child
+                    };
 
-                target.Show();
+                    Show(target);
 
-                Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
+                    Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
+                }
             }
-        }
 
-        [Fact]
-        public void Width_Height_Should_Be_Updated_When_SizeToContent_Is_WidthAndHeight()
-        {
-            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            [Fact]
+            public void Width_Height_Should_Be_Updated_When_SizeToContent_Is_WidthAndHeight()
             {
-                var child = new Canvas
+                using (UnitTestApplication.Start(TestServices.StyledWindow))
                 {
-                    Width = 400,
-                    Height = 800,
-                };
+                    var child = new Canvas
+                    {
+                        Width = 400,
+                        Height = 800,
+                    };
 
-                var target = new Window()
-                {
-                    SizeToContent = SizeToContent.WidthAndHeight,
-                    Content = child
-                };
+                    var target = new Window()
+                    {
+                        SizeToContent = SizeToContent.WidthAndHeight,
+                        Content = child
+                    };
 
-                target.Show();
+                    Show(target);
 
-                Assert.Equal(400, target.Width);
-                Assert.Equal(800, target.Height);
+                    Assert.Equal(400, target.Width);
+                    Assert.Equal(800, target.Height);
 
-                child.Width = 410;
-                target.LayoutManager.ExecuteLayoutPass();
+                    child.Width = 410;
+                    target.LayoutManager.ExecuteLayoutPass();
 
-                Assert.Equal(410, target.Width);
-                Assert.Equal(800, target.Height);
-                Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
+                    Assert.Equal(410, target.Width);
+                    Assert.Equal(800, target.Height);
+                    Assert.Equal(SizeToContent.WidthAndHeight, target.SizeToContent);
+                }
             }
-        }
 
-        [Fact]
-        public void Setting_Width_Should_Resize_WindowImpl()
-        {
-            // Issue #3796
-            using (UnitTestApplication.Start(TestServices.StyledWindow))
+            [Fact]
+            public void Setting_Width_Should_Resize_WindowImpl()
             {
-                var target = new Window()
+                // Issue #3796
+                using (UnitTestApplication.Start(TestServices.StyledWindow))
                 {
-                    Width = 400,
-                    Height = 800,
-                };
+                    var target = new Window()
+                    {
+                        Width = 400,
+                        Height = 800,
+                    };
 
-                target.Show();
+                    Show(target);
+
+                    Assert.Equal(400, target.Width);
+                    Assert.Equal(800, target.Height);
 
-                Assert.Equal(400, target.Width);
-                Assert.Equal(800, target.Height);
+                    target.Width = 410;
+                    target.LayoutManager.ExecuteLayoutPass();
 
-                target.Width = 410;
-                target.LayoutManager.ExecuteLayoutPass();
+                    var windowImpl = Mock.Get(target.PlatformImpl);
+                    windowImpl.Verify(x => x.Resize(new Size(410, 800)));
+                    Assert.Equal(410, target.Width);
+                }
+            }
 
-                var windowImpl = Mock.Get(target.PlatformImpl);
-                windowImpl.Verify(x => x.Resize(new Size(410, 800)));
-                Assert.Equal(410, target.Width);
+            protected virtual void Show(Window window)
+            {
+                window.Show();
+            }
+        }
+
+        public class DialogSizingTests : SizingTests
+        {
+            protected override void Show(Window window)
+            {
+                var owner = new Window();
+                window.ShowDialog(owner);
             }
         }
 

+ 44 - 0
tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs

@@ -0,0 +1,44 @@
+using System.Collections.Generic;
+using Avalonia.Controls.Shapes;
+using Avalonia.Markup.Xaml.Converters;
+using Xunit;
+
+namespace Avalonia.Markup.Xaml.UnitTests.Converters
+{
+    public class PointsListTypeConverterTests
+    {
+        [Theory]
+        [InlineData("1,2 3,4")]
+        [InlineData("1 2 3 4")]
+        [InlineData("1 2,3 4")]
+        [InlineData("1,2,3,4")]
+        public void TypeConverter_Should_Parse(string input)
+        {
+            var conv = new PointsListTypeConverter();
+
+            var points = (IList<Point>)conv.ConvertFrom(input);
+
+            Assert.Equal(2, points.Count);
+            Assert.Equal(new Point(1, 2), points[0]);
+            Assert.Equal(new Point(3, 4), points[1]);
+        }
+
+        [Theory]
+        [InlineData("1,2 3,4")]
+        [InlineData("1 2 3 4")]
+        [InlineData("1 2,3 4")]
+        [InlineData("1,2,3,4")]
+        public void Should_Parse_Points_in_Xaml(string input)
+        {
+            var xaml = $"<Polygon xmlns='https://github.com/avaloniaui' Points='{input}' />";
+            var loader = new AvaloniaXamlLoader();
+            var polygon = (Polygon)loader.Load(xaml);
+
+            var points = polygon.Points;
+
+            Assert.Equal(2, points.Count);
+            Assert.Equal(new Point(1, 2), points[0]);
+            Assert.Equal(new Point(3, 4), points[1]);
+        }
+    }
+}

+ 1 - 1
tests/Avalonia.RenderTests/TestBase.cs

@@ -184,7 +184,7 @@ namespace Avalonia.Direct2D1.RenderTests
 
             public void Signal(DispatcherPriority prio)
             {
-                throw new NotImplementedException();
+                // No-op
             }
 
             public IDisposable StartTimer(DispatcherPriority priority, TimeSpan interval, Action tick)

+ 38 - 10
tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs

@@ -11,10 +11,11 @@ namespace Avalonia.Skia.UnitTests
     public class CustomFontManagerImpl : IFontManagerImpl
     {
         private readonly Typeface[] _customTypefaces;
+        private readonly string _defaultFamilyName;
 
         private readonly Typeface _defaultTypeface =
             new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Mono");
-        private readonly Typeface _italicTypeface = 
+        private readonly Typeface _italicTypeface =
             new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Noto Sans");
         private readonly Typeface _emojiTypeface =
             new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Twitter Color Emoji");
@@ -22,11 +23,12 @@ namespace Avalonia.Skia.UnitTests
         public CustomFontManagerImpl()
         {
             _customTypefaces = new[] { _emojiTypeface, _italicTypeface, _defaultTypeface };
+            _defaultFamilyName = _defaultTypeface.FontFamily.FamilyNames.PrimaryFamilyName;
         }
 
         public string GetDefaultFontFamilyName()
         {
-            return _defaultTypeface.FontFamily.ToString();
+            return _defaultFamilyName;
         }
 
         public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
@@ -34,39 +36,65 @@ namespace Avalonia.Skia.UnitTests
             return _customTypefaces.Select(x => x.FontFamily.Name);
         }
 
+        private readonly string[] _bcp47 = { CultureInfo.CurrentCulture.ThreeLetterISOLanguageName, CultureInfo.CurrentCulture.TwoLetterISOLanguageName };
+
         public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily,
             CultureInfo culture, out FontKey fontKey)
         {
             foreach (var customTypeface in _customTypefaces)
             {
                 if (customTypeface.GlyphTypeface.GetGlyph((uint)codepoint) == 0)
+                {
                     continue;
-                fontKey = new FontKey(customTypeface.FontFamily, fontWeight, fontStyle);
+                }
+
+                fontKey = new FontKey(customTypeface.FontFamily.Name, fontWeight, fontStyle);
 
                 return true;
             }
 
-            var fallback = SKFontManager.Default.MatchCharacter(codepoint);
+            var fallback = SKFontManager.Default.MatchCharacter(fontFamily?.Name, (SKFontStyleWeight)fontWeight,
+                SKFontStyleWidth.Normal, (SKFontStyleSlant)fontStyle, _bcp47, codepoint);
 
-            fontKey = new FontKey(fallback?.FamilyName ?? SKTypeface.Default.FamilyName, fontWeight, fontStyle);
+            fontKey = new FontKey(fallback?.FamilyName ?? _defaultFamilyName, fontWeight, fontStyle);
 
             return true;
         }
 
         public IGlyphTypefaceImpl CreateGlyphTypeface(Typeface typeface)
         {
+            SKTypeface skTypeface;
+
             switch (typeface.FontFamily.Name)
             {
                 case "Twitter Color Emoji":
+                    {
+                        var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_emojiTypeface.FontFamily);
+                        skTypeface = typefaceCollection.Get(typeface);
+                        break;
+                    }
+
                 case "Noto Sans":
+                    {
+                        var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_italicTypeface.FontFamily);
+                        skTypeface = typefaceCollection.Get(typeface);
+                        break;
+                    }
                 case "Noto Mono":
-                    var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(typeface.FontFamily);
-                    var skTypeface = typefaceCollection.Get(typeface);
-                    return new GlyphTypefaceImpl(skTypeface);
+                    {
+                        var typefaceCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(_defaultTypeface.FontFamily);
+                        skTypeface = typefaceCollection.Get(typeface);
+                        break;
+                    }
                 default:
-                    return new GlyphTypefaceImpl(SKTypeface.FromFamilyName(typeface.FontFamily.Name,
-                        (SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style));
+                    {
+                        skTypeface = SKTypeface.FromFamilyName(typeface.FontFamily.Name,
+                            (SKFontStyleWeight)typeface.Weight, SKFontStyleWidth.Normal, (SKFontStyleSlant)typeface.Style);
+                        break;
+                    }
             }
+
+            return new GlyphTypefaceImpl(skTypeface);
         }
     }
 }

+ 13 - 0
tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs

@@ -95,5 +95,18 @@ namespace Avalonia.Skia.UnitTests
                 Assert.Equal("Noto Mono", skTypeface.FamilyName);
             }
         }
+
+        [Fact]
+        public void Should_Throw_For_Invalid_Custom_Font()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface))
+            {
+                var fontManager = new FontManagerImpl();
+
+                Assert.Throws<InvalidOperationException>(() =>
+                    fontManager.CreateGlyphTypeface(
+                        new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#Unknown")));
+            }
+        }
     }
 }

+ 22 - 2
tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs

@@ -332,7 +332,7 @@ namespace Avalonia.Skia.UnitTests
                     Typeface.Default,
                     12.0f,
                     Brushes.Black.ToImmutable(),
-                    maxWidth : 200, 
+                    maxWidth : 200,
                     maxHeight : 125,
                     textStyleOverrides: spans);
 
@@ -506,10 +506,30 @@ namespace Avalonia.Skia.UnitTests
             }
         }
 
+        private const string Text = "日本でTest一番読まれている英字新聞・ジャパンタイムズが発信する国内外ニュースと、様々なジャンルの特集記事。";
+
+        [Fact]
+        public void Should_Wrap()
+        {
+            using (Start())
+            {
+                for (var i = 0; i < 2000; i++)
+                {
+                    var layout = new TextLayout(
+                        Text,
+                        Typeface.Default,
+                        12,
+                        Brushes.Black,
+                        textWrapping: TextWrapping.Wrap,
+                        maxWidth: 50);
+                }
+            }
+        }
+
         public static IDisposable Start()
         {
             var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
-                .With(renderInterface: new PlatformRenderInterface(null), 
+                .With(renderInterface: new PlatformRenderInterface(null),
                     textShaperImpl: new TextShaperImpl(),
                     fontManagerImpl : new CustomFontManagerImpl()));
 

+ 9 - 2
tests/Avalonia.UnitTests/MockFontManagerImpl.cs

@@ -8,14 +8,21 @@ namespace Avalonia.UnitTests
 {
     public class MockFontManagerImpl : IFontManagerImpl
     {
+        private readonly string _defaultFamilyName;
+
+        public MockFontManagerImpl(string defaultFamilyName = "Default")
+        {
+            _defaultFamilyName = defaultFamilyName;
+        }
+
         public string GetDefaultFontFamilyName()
         {
-            return "Default";
+            return _defaultFamilyName;
         }
 
         public IEnumerable<string> GetInstalledFontFamilyNames(bool checkForUpdates = false)
         {
-            return new[] { "Default" };
+            return new[] { _defaultFamilyName };
         }
 
         public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, FontFamily fontFamily,

+ 11 - 1
tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs

@@ -1,4 +1,5 @@
-using Avalonia.Media;
+using System;
+using Avalonia.Media;
 using Avalonia.Platform;
 using Avalonia.UnitTests;
 using Xunit;
@@ -19,5 +20,14 @@ namespace Avalonia.Visuals.UnitTests.Media
                 Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily));
             }
         }
+
+        [Fact]
+        public void Should_Throw_When_Default_FamilyName_Is_Null()
+        {
+            using (UnitTestApplication.Start(TestServices.MockPlatformRenderInterface.With(fontManagerImpl: new MockFontManagerImpl(null))))
+            {
+                Assert.Throws<InvalidOperationException>(() => FontManager.Current);
+            }
+        }
     }
 }