Browse Source

Merge branch 'master' into refactor/use-selectionmodel

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

4
build/SkiaSharp.props

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

49
native/Avalonia.Native/inc/avalonia-native.h

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

6
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/xcshareddata/xcschemes/Avalonia.Native.OSX.xcscheme

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

3
native/Avalonia.Native/src/OSX/SystemDialogs.mm

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

7
native/Avalonia.Native/src/OSX/app.mm

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

10
native/Avalonia.Native/src/OSX/common.h

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

25
native/Avalonia.Native/src/OSX/main.mm

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

36
native/Avalonia.Native/src/OSX/menu.h

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

286
native/Avalonia.Native/src/OSX/menu.mm

@ -4,6 +4,30 @@
#include "window.h" #include "window.h"
@implementation AvnMenu @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 @end
@implementation AvnMenuItem @implementation AvnMenuItem
@ -46,6 +70,7 @@
AvnAppMenuItem::AvnAppMenuItem(bool isSeperator) AvnAppMenuItem::AvnAppMenuItem(bool isSeperator)
{ {
_isCheckable = false;
_isSeperator = isSeperator; _isSeperator = isSeperator;
if(isSeperator) if(isSeperator)
@ -65,49 +90,134 @@ NSMenuItem* AvnAppMenuItem::GetNative()
return _native; return _native;
} }
HRESULT AvnAppMenuItem::SetSubMenu (IAvnAppMenu* menu) HRESULT AvnAppMenuItem::SetSubMenu (IAvnMenu* menu)
{ {
auto nsMenu = dynamic_cast<AvnAppMenu*>(menu)->GetNative(); @autoreleasepool
{
[_native setSubmenu: nsMenu]; if(menu != nullptr)
{
return S_OK; auto nsMenu = dynamic_cast<AvnAppMenu*>(menu)->GetNative();
[_native setSubmenu: nsMenu];
}
else
{
[_native setSubmenu: nullptr];
}
return S_OK;
}
} }
HRESULT AvnAppMenuItem::SetTitle (void* utf8String) 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) HRESULT AvnAppMenuItem::SetGesture (void* key, AvnInputModifiers modifiers)
{ {
NSEventModifierFlags flags = 0; @autoreleasepool
{
if (modifiers & Control) NSEventModifierFlags flags = 0;
flags |= NSEventModifierFlagControl;
if (modifiers & Shift) if (modifiers & Control)
flags |= NSEventModifierFlagShift; flags |= NSEventModifierFlagControl;
if (modifiers & Alt) if (modifiers & Shift)
flags |= NSEventModifierFlagOption; flags |= NSEventModifierFlagShift;
if (modifiers & Windows) if (modifiers & Alt)
flags |= NSEventModifierFlagCommand; flags |= NSEventModifierFlagOption;
if (modifiers & Windows)
[_native setKeyEquivalent:[NSString stringWithUTF8String:(const char*)key]]; flags |= NSEventModifierFlagCommand;
[_native setKeyEquivalentModifierMask:flags];
[_native setKeyEquivalent:[NSString stringWithUTF8String:(const char*)key]];
return S_OK; [_native setKeyEquivalentModifierMask:flags];
return S_OK;
}
} }
HRESULT AvnAppMenuItem::SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback) HRESULT AvnAppMenuItem::SetAction (IAvnPredicateCallback* predicate, IAvnActionCallback* callback)
{ {
_predicate = predicate; @autoreleasepool
_callback = callback; {
return S_OK; _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() 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() AvnMenu* AvnAppMenu::GetNative()
{ {
return _native; return _native;
} }
HRESULT AvnAppMenu::AddItem (IAvnAppMenuItem* item) void AvnAppMenu::RaiseNeedsUpdate()
{ {
auto avnMenuItem = dynamic_cast<AvnAppMenuItem*>(item); if(_baseEvents != nullptr)
if(avnMenuItem != 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); @autoreleasepool
if(avnMenuItem != nullptr)
{ {
[_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) 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() HRESULT AvnAppMenu::Clear()
{ {
[_native removeAllItems]; @autoreleasepool
return S_OK; {
[_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 @autoreleasepool
{ {
id menuBar = [NSMenu new]; return new AvnAppMenu(cb);
return new AvnAppMenu(menuBar);
} }
} }
extern IAvnAppMenuItem* CreateAppMenuItem() extern IAvnMenuItem* CreateAppMenuItem()
{ {
@autoreleasepool @autoreleasepool
{ {
@ -202,7 +364,7 @@ extern IAvnAppMenuItem* CreateAppMenuItem()
} }
} }
extern IAvnAppMenuItem* CreateAppMenuItemSeperator() extern IAvnMenuItem* CreateAppMenuItemSeperator()
{ {
@autoreleasepool @autoreleasepool
{ {
@ -210,10 +372,10 @@ extern IAvnAppMenuItem* CreateAppMenuItemSeperator()
} }
} }
static IAvnAppMenu* s_appMenu = nullptr; static IAvnMenu* s_appMenu = nullptr;
static NSMenuItem* s_appMenuItem = nullptr; static NSMenuItem* s_appMenuItem = nullptr;
extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu) extern void SetAppMenu (NSString* appName, IAvnMenu* menu)
{ {
s_appMenu = menu; s_appMenu = menu;
@ -294,7 +456,7 @@ extern void SetAppMenu (NSString* appName, IAvnAppMenu* menu)
} }
} }
extern IAvnAppMenu* GetAppMenu () extern IAvnMenu* GetAppMenu ()
{ {
return s_appMenu; return s_appMenu;
} }

4
native/Avalonia.Native/src/OSX/platformthreading.mm

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

10
native/Avalonia.Native/src/OSX/window.h

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

441
native/Avalonia.Native/src/OSX/window.mm

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

5
samples/ControlCatalog/MainView.xaml

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

24
samples/ControlCatalog/MainWindow.xaml

@ -7,16 +7,16 @@
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:vm="clr-namespace:ControlCatalog.ViewModels"
xmlns:v="clr-namespace:ControlCatalog.Views" xmlns:v="clr-namespace:ControlCatalog.Views"
x:Class="ControlCatalog.MainWindow"> x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}">
<NativeMenu.Menu> <NativeMenu.Menu>
<NativeMenu> <NativeMenu>
<NativeMenuItem Header="File"> <NativeMenuItem Header="File">
<NativeMenuItem.Menu> <NativeMenuItem.Menu>
<NativeMenu> <NativeMenu>
<NativeMenuItem Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/> <NativeMenuItem Icon="/Assets/test_icon.ico" Header="Open" Clicked="OnOpenClicked" Gesture="Ctrl+O"/>
<NativeMenuItemSeperator/> <NativeMenuItemSeperator/>
<NativeMenuItem Header="Recent"> <NativeMenuItem Icon="/Assets/github_icon.png" Header="Recent">
<NativeMenuItem.Menu> <NativeMenuItem.Menu>
<NativeMenu/> <NativeMenu/>
</NativeMenuItem.Menu> </NativeMenuItem.Menu>
@ -36,6 +36,24 @@
</NativeMenu> </NativeMenu>
</NativeMenuItem.Menu> </NativeMenuItem.Menu>
</NativeMenuItem> </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>
</NativeMenu.Menu> </NativeMenu.Menu>

1
samples/ControlCatalog/MainWindow.xaml.cs

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

40
samples/ControlCatalog/ViewModels/MainWindowViewModel.cs

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

4
src/Avalonia.Animation/Animation.cs

@ -251,10 +251,10 @@ namespace Avalonia.Animation
if (keyframe.TimingMode == KeyFrameTimingMode.TimeSpan) 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)); subscriptions.Add(newKF.BindSetter(setter, control));

9
src/Avalonia.Animation/AnimatorKeyFrame.cs

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

3
src/Avalonia.Animation/Animators/Animator`1.cs

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

20
src/Avalonia.Animation/KeyFrame.cs

@ -19,6 +19,7 @@ namespace Avalonia.Animation
{ {
private TimeSpan _ktimeSpan; private TimeSpan _ktimeSpan;
private Cue _kCue; private Cue _kCue;
private KeySpline _kKeySpline;
public KeyFrame() 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
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);
}
}
}

2
src/Avalonia.Controls.DataGrid/DataGridBoundColumn.cs

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

3
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, // Since we didn't know the final widths of the columns until we resized,
// we waited until now to measure each cell // we waited until now to measure each cell
double leftEdge = 0; double leftEdge = 0;
if (autoSizeHeight)
DesiredHeight = 0;
foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetVisibleColumns()) foreach (DataGridColumn column in OwningGrid.ColumnsInternal.GetVisibleColumns())
{ {
DataGridCell cell = OwningRow.Cells[column.Index]; DataGridCell cell = OwningRow.Cells[column.Index];

22
src/Avalonia.Controls/ComboBox.cs

@ -234,6 +234,23 @@ namespace Avalonia.Controls
base.OnTemplateApplied(e); 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) internal void ItemFocused(ComboBoxItem dropDownItem)
{ {
if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid) if (IsDropDownOpen && dropDownItem.IsFocused && dropDownItem.IsArrangeValid)
@ -247,10 +264,7 @@ namespace Avalonia.Controls
_subscriptionsOnOpen?.Dispose(); _subscriptionsOnOpen?.Dispose();
_subscriptionsOnOpen = null; _subscriptionsOnOpen = null;
if (e.CloseEvent is PointerEventArgs pointerEvent) PopupClosedOverride(e);
{
pointerEvent.Handled = true;
}
if (CanFocus(this)) if (CanFocus(this))
{ {

7
src/Avalonia.Controls/INativeMenuExporterEventsImplBridge.cs

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

7
src/Avalonia.Controls/INativeMenuItemExporterEventsImplBridge.cs

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

20
src/Avalonia.Controls/NativeMenu.cs

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

2
src/Avalonia.Controls/NativeMenuBar.cs

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

118
src/Avalonia.Controls/NativeMenuItem.cs

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

2
src/Avalonia.Controls/Platform/DefaultMenuInteractionHandler.cs

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

5
src/Avalonia.Controls/Platform/ISystemDialogImpl.cs

@ -1,5 +1,4 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Platform;
namespace Avalonia.Controls.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="dialog">The details of the file dialog to show.</param>
/// <param name="parent">The parent window.</param> /// <param name="parent">The parent window.</param>
/// <returns>A task returning the selected filenames.</returns> /// <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);
} }
} }

46
src/Avalonia.Controls/Presenters/TextPresenter.cs

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

6
src/Avalonia.Controls/SystemDialog.cs

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

41
src/Avalonia.Controls/Window.cs

@ -484,22 +484,12 @@ namespace Avalonia.Controls
/// <returns>. /// <returns>.
/// A task that can be used to retrieve the result of the dialog when it closes. /// A task that can be used to retrieve the result of the dialog when it closes.
/// </returns> /// </returns>
public Task<TResult> ShowDialog<TResult>(Window owner) => ShowDialog<TResult>(owner.PlatformImpl); public Task<TResult> ShowDialog<TResult>(Window owner)
/// <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)
{ {
if (owner == null) if (owner == null)
{
throw new ArgumentNullException(nameof(owner)); throw new ArgumentNullException(nameof(owner));
}
if (IsVisible) if (IsVisible)
{ {
@ -510,29 +500,44 @@ namespace Avalonia.Controls
EnsureInitialized(); EnsureInitialized();
IsVisible = true; 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); LayoutManager.ExecuteInitialLayoutPass(this);
var result = new TaskCompletionSource<TResult>(); var result = new TaskCompletionSource<TResult>();
using (BeginAutoSizing()) using (BeginAutoSizing())
{ {
PlatformImpl?.ShowDialog(owner.PlatformImpl);
PlatformImpl?.ShowDialog(owner);
Renderer?.Start(); Renderer?.Start();
Observable.FromEventPattern<EventHandler, EventArgs>( Observable.FromEventPattern<EventHandler, EventArgs>(
x => this.Closed += x, x => Closed += x,
x => this.Closed -= x) x => Closed -= x)
.Take(1) .Take(1)
.Subscribe(_ => .Subscribe(_ =>
{ {
owner.Activate(); owner.Activate();
result.SetResult((TResult)(_dialogResult ?? default(TResult))); result.SetResult((TResult)(_dialogResult ?? default(TResult)));
}); });
OnOpened(EventArgs.Empty); OnOpened(EventArgs.Empty);
} }
SetWindowStartupLocation(owner); SetWindowStartupLocation(owner.PlatformImpl);
return result.Task; return result.Task;
} }

5
src/Avalonia.Controls/WindowState.cs

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

4
src/Avalonia.DesignerSupport/Remote/Stubs.cs

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

2
src/Avalonia.Dialogs/ManagedFileChooser.xaml.cs

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

12
src/Avalonia.Dialogs/ManagedFileDialogExtensions.cs

@ -1,19 +1,15 @@
using System;
using System.Linq; using System.Linq;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Dialogs;
using Avalonia.Platform;
namespace Avalonia.Dialogs namespace Avalonia.Dialogs
{ {
public static class ManagedFileDialogExtensions 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); var model = new ManagedFileChooserViewModel((FileSystemDialog)d);
@ -39,12 +35,12 @@ namespace Avalonia.Dialogs
return result; 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); 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(); return (await Show(dialog, parent))?.FirstOrDefault();
} }

35
src/Avalonia.FreeDesktop/DBusMenuExporter.cs

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

415
src/Avalonia.Native/AvaloniaNativeMenuExporter.cs

@ -1,185 +1,21 @@
using System; 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;
using Avalonia.Controls.ApplicationLifetimes;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Input; using Avalonia.Dialogs;
using Avalonia.Native.Interop; using Avalonia.Native.Interop;
using Avalonia.Platform.Interop;
using Avalonia.Threading; using Avalonia.Threading;
using Avalonia.Dialogs;
using Avalonia.Controls.ApplicationLifetimes;
namespace Avalonia.Native 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 class AvaloniaNativeMenuExporter : ITopLevelNativeMenuExporter
{ {
private IAvaloniaNativeFactory _factory; private IAvaloniaNativeFactory _factory;
private NativeMenu _menu;
private bool _resetQueued; private bool _resetQueued;
private bool _exported = false; private bool _exported = false;
private IAvnWindow _nativeWindow; private IAvnWindow _nativeWindow;
private List<NativeMenuItem> _menuItems = new List<NativeMenuItem>(); private NativeMenu _menu;
private IAvnMenu _nativeMenu;
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 },
};
public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory) public AvaloniaNativeMenuExporter(IAvnWindow nativeWindow, IAvaloniaNativeFactory factory)
{ {
@ -193,7 +29,6 @@ namespace Avalonia.Native
{ {
_factory = factory; _factory = factory;
_menu = NativeMenu.GetMenu(Application.Current);
DoLayoutReset(); DoLayoutReset();
} }
@ -203,17 +38,19 @@ namespace Avalonia.Native
public void SetNativeMenu(NativeMenu menu) public void SetNativeMenu(NativeMenu menu)
{ {
if (menu == null) _menu = menu == null ? new NativeMenu() : menu;
menu = new NativeMenu();
if (_menu != null)
((INotifyCollectionChanged)_menu.Items).CollectionChanged -= OnMenuItemsChanged;
_menu = menu;
((INotifyCollectionChanged)_menu.Items).CollectionChanged += OnMenuItemsChanged;
DoLayoutReset(); DoLayoutReset();
} }
internal void UpdateIfNeeded()
{
if (_resetQueued)
{
DoLayoutReset();
}
}
private static NativeMenu CreateDefaultAppMenu() private static NativeMenu CreateDefaultAppMenu()
{ {
var result = new NativeMenu(); var result = new NativeMenu();
@ -237,50 +74,34 @@ namespace Avalonia.Native
return result; return result;
} }
private void OnItemPropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e)
{
QueueReset();
}
private void OnMenuItemsChanged(object sender, NotifyCollectionChangedEventArgs e)
{
QueueReset();
}
void DoLayoutReset() void DoLayoutReset()
{ {
_resetQueued = false; _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) if (appMenu == null)
{
SetMenu(_menu);
}
else
{ {
SetMenu(CreateDefaultAppMenu()); appMenu = CreateDefaultAppMenu();
NativeMenu.SetMenu(Application.Current, appMenu);
} }
SetMenu(appMenu);
} }
else else
{ {
SetMenu(_nativeWindow, _menu?.Items); if (_menu != null)
{
SetMenu(_nativeWindow, _menu);
}
} }
_exported = true; _exported = true;
} }
private void QueueReset() internal void QueueReset()
{ {
if (_resetQueued) if (_resetQueued)
return; return;
@ -288,188 +109,64 @@ namespace Avalonia.Native
Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background); Dispatcher.UIThread.Post(DoLayoutReset, DispatcherPriority.Background);
} }
private IAvnAppMenu CreateSubmenu(ICollection<NativeMenuItemBase> children) private void SetMenu(NativeMenu menu)
{ {
var menu = _factory.CreateMenu(); var menuItem = menu.Parent;
SetChildren(menu, children);
return menu; var appMenuHolder = menuItem?.Parent;
}
private void AddMenuItem(NativeMenuItem item) if (menu.Parent is null)
{
if (item.Menu?.Items != null)
{ {
((INotifyCollectionChanged)item.Menu.Items).CollectionChanged += OnMenuItemsChanged; menuItem = new NativeMenuItem();
} }
}
private static string ConvertOSXSpecialKeyCodes(Key key) if (appMenuHolder is null)
{
if (osxKeys.ContainsKey(key))
{
return ((char)osxKeys[key]).ToString();
}
else
{ {
return key.ToString().ToLower(); appMenuHolder = new NativeMenu();
}
}
private void SetChildren(IAvnAppMenu menu, ICollection<NativeMenuItemBase> children) appMenuHolder.Add(menuItem);
{
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());
}
} }
}
private void AddItemsToMenu(IAvnAppMenu menu, ICollection<NativeMenuItemBase> items, bool isMainMenu = false) menuItem.Menu = menu;
{
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());
}
}
}
private void SetMenu(NativeMenu menu) var setMenu = false;
{
var appMenu = _factory.ObtainAppMenu();
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) setMenu = true;
{
menuItem = new NativeMenuItem();
} }
menuItem.Menu = menu; _nativeMenu.Update(_factory, appMenuHolder);
appMenu.Clear();
AddItemsToMenu(appMenu, new List<NativeMenuItemBase> { menuItem });
_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) setMenu = true;
{
appMenu = _factory.CreateMenu();
} }
appMenu.Clear(); _nativeMenu.Update(_factory, menu);
AddItemsToMenu(appMenu, menuItems);
avnWindow.SetMainMenu(appMenu); if(setMenu)
{
avnWindow.SetMainMenu(_nativeMenu);
}
} }
} }
} }

176
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
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
src/Avalonia.Native/Mappings.xml

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

20
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
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
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();
}
}
}

20
src/Avalonia.Native/SystemDialogs.cs

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

2
src/Avalonia.Native/WindowImpl.cs

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

148
src/Avalonia.Styling/LogicalTree/LogicalExtensions.cs

@ -1,11 +1,18 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Linq;
namespace Avalonia.LogicalTree namespace Avalonia.LogicalTree
{ {
/// <summary>
/// Provides extension methods for working with the logical tree.
/// </summary>
public static class LogicalExtensions 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) public static IEnumerable<ILogical> GetLogicalAncestors(this ILogical logical)
{ {
Contract.Requires<ArgumentNullException>(logical != null); 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) public static IEnumerable<ILogical> GetSelfAndLogicalAncestors(this ILogical logical)
{ {
yield return 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) public static IEnumerable<ILogical> GetLogicalChildren(this ILogical logical)
{ {
return logical.LogicalChildren; 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) public static IEnumerable<ILogical> GetLogicalDescendants(this ILogical logical)
{ {
foreach (ILogical child in logical.LogicalChildren) 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) public static IEnumerable<ILogical> GetSelfAndLogicalDescendants(this ILogical logical)
{ {
yield return 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) public static ILogical GetLogicalParent(this ILogical logical)
{ {
return logical.LogicalParent; 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 public static T GetLogicalParent<T>(this ILogical logical) where T : class
{ {
return logical.LogicalParent as T; 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) public static IEnumerable<ILogical> GetLogicalSiblings(this ILogical logical)
{ {
ILogical parent = logical.LogicalParent; 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;
} }
} }
} }

26
src/Avalonia.Visuals/Media/FontManager.cs

@ -23,6 +23,11 @@ namespace Avalonia.Media
DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName(); DefaultFontFamilyName = PlatformImpl.GetDefaultFontFamilyName();
if (string.IsNullOrEmpty(DefaultFontFamilyName))
{
throw new InvalidOperationException("Default font family name can't be null or empty.");
}
_defaultFontFamily = new FontFamily(DefaultFontFamilyName); _defaultFontFamily = new FontFamily(DefaultFontFamilyName);
} }
@ -39,7 +44,8 @@ namespace Avalonia.Media
var fontManagerImpl = AvaloniaLocator.Current.GetService<IFontManagerImpl>(); 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); current = new FontManager(fontManagerImpl);
@ -87,7 +93,7 @@ namespace Avalonia.Media
fontFamily = _defaultFontFamily; fontFamily = _defaultFontFamily;
} }
var key = new FontKey(fontFamily, fontWeight, fontStyle); var key = new FontKey(fontFamily.Name, fontWeight, fontStyle);
if (_typefaceCache.TryGetValue(key, out var typeface)) if (_typefaceCache.TryGetValue(key, out var typeface))
{ {
@ -126,9 +132,21 @@ namespace Avalonia.Media
FontStyle fontStyle = FontStyle.Normal, FontStyle fontStyle = FontStyle.Normal,
FontFamily fontFamily = null, CultureInfo culture = null) FontFamily fontFamily = null, CultureInfo culture = null)
{ {
return PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? foreach (var cachedTypeface in _typefaceCache.Values)
_typefaceCache.GetOrAdd(key, new Typeface(key.FontFamily, key.Weight, key.Style)) : {
// 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; null;
return matchedTypeface;
} }
} }
} }

3
src/Avalonia.Visuals/Media/Fonts/FamilyNameCollection.cs

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

16
src/Avalonia.Visuals/Media/Fonts/FontKey.cs

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

6
src/Avalonia.Visuals/Media/FormattedText.cs

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

2
src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs

@ -66,7 +66,7 @@ namespace Avalonia.Media.TextFormatting
//ToDo: Fix FontFamily fallback //ToDo: Fix FontFamily fallback
currentTypeface = 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)) if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count))
{ {

2
src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs

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

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
}
}

26
src/Avalonia.Visuals/Rendering/DeferredRenderer.cs

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

9
src/Avalonia.Visuals/Rendering/RendererBase.cs

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

13
src/Avalonia.Visuals/Rendering/SceneGraph/Scene.cs

@ -162,17 +162,18 @@ namespace Avalonia.Rendering.SceneGraph
index.Add(result.Visual, result); 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));
} }
} }

28
src/Avalonia.Visuals/Rendering/SceneGraph/VisualNode.cs

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

14
src/Avalonia.Visuals/VisualTree/VisualExtensions.cs

@ -377,7 +377,19 @@ namespace Avalonia.VisualTree
/// </returns> /// </returns>
public static bool IsVisualAncestorOf(this IVisual visual, IVisual target) 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) public static IEnumerable<IVisual> SortByZIndex(this IEnumerable<IVisual> elements)

14
src/Avalonia.X11/NativeDialogs/GtkNativeFileDialogs.cs

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

1
src/Avalonia.X11/X11Atoms.cs

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

32
src/Avalonia.X11/X11Window.cs

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

17
src/Markup/Avalonia.Markup.Xaml/Converters/PointsListTypeConverter.cs

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

1
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)); => AddType(typeSystem.GetType(type), typeSystem.GetType(conv));
Add("Avalonia.Media.IImage","Avalonia.Markup.Xaml.Converters.BitmapTypeConverter"); 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"); var ilist = typeSystem.GetType("System.Collections.Generic.IList`1");
AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")), AddType(ilist.MakeGenericType(typeSystem.GetType("Avalonia.Point")),
typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter")); typeSystem.GetType("Avalonia.Markup.Xaml.Converters.PointsListTypeConverter"));

67
src/Skia/Avalonia.Skia/DrawingContextImpl.cs

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

46
src/Skia/Avalonia.Skia/FontManagerImpl.cs

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

15
src/Skia/Avalonia.Skia/FormattedTextImpl.cs

@ -149,7 +149,17 @@ namespace Avalonia.Skia
if (index >= Text.Length || index < 0) if (index >= Text.Length || index < 0)
{ {
var r = rects.LastOrDefault(); 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]; return rects[index];
} }
@ -266,7 +276,8 @@ namespace Avalonia.Skia
if (fb != null) if (fb != null)
{ {
//TODO: figure out how to get the brush size //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 else
{ {

9
src/Skia/Avalonia.Skia/GlyphRunImpl.cs

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

126
src/Skia/Avalonia.Skia/PlatformRenderInterface.cs

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

4
src/Skia/Avalonia.Skia/SKTypefaceCollection.cs

@ -19,7 +19,7 @@ namespace Avalonia.Skia
public SKTypeface Get(Typeface typeface) 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); return GetNearestMatch(_typefaces, key);
} }
@ -49,7 +49,7 @@ namespace Avalonia.Skia
if (keys.Length == 0) if (keys.Length == 0)
{ {
return SKTypeface.Default; return null;
} }
key = keys[0]; key = keys[0];

2
src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs

@ -54,7 +54,7 @@ namespace Avalonia.Skia
continue; 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); typeFaceCollection.AddTypeface(key, typeface);
} }

9
src/Skia/Avalonia.Skia/SkiaOptions.cs

@ -8,9 +8,18 @@ namespace Avalonia
/// </summary> /// </summary>
public class SkiaOptions public class SkiaOptions
{ {
public SkiaOptions()
{
MaxGpuResourceSizeBytes = 100663296; // Value taken from skia.
}
/// <summary> /// <summary>
/// Custom gpu factory to use. Can be used to customize behavior of Skia renderer. /// Custom gpu factory to use. Can be used to customize behavior of Skia renderer.
/// </summary> /// </summary>
public Func<ICustomSkiaGpu> CustomGpuFactory { get; set; } 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; }
} }
} }

2
src/Skia/Avalonia.Skia/SkiaPlatform.cs

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

2
src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs

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

55
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);
}
}
}
}

24
src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs

@ -460,6 +460,7 @@ namespace Avalonia.Win32.Interop
WS_SIZEFRAME = 0x40000, WS_SIZEFRAME = 0x40000,
WS_SYSMENU = 0x80000, WS_SYSMENU = 0x80000,
WS_TABSTOP = 0x10000, WS_TABSTOP = 0x10000,
WS_THICKFRAME = 0x40000,
WS_VISIBLE = 0x10000000, WS_VISIBLE = 0x10000000,
WS_VSCROLL = 0x200000, WS_VSCROLL = 0x200000,
WS_EX_DLGMODALFRAME = 0x00000001, WS_EX_DLGMODALFRAME = 0x00000001,
@ -1146,7 +1147,10 @@ namespace Avalonia.Win32.Interop
internal static extern int CoCreateInstance(ref Guid clsid, internal static extern int CoCreateInstance(ref Guid clsid,
IntPtr ignore1, int ignore2, ref Guid iid, [MarshalAs(UnmanagedType.IUnknown), Out] out object pUnkOuter); 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)] [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); 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 SaveFileDialog = Guid.Parse("C0B4E2F3-BA21-4773-8DBA-335EC946EB8B");
public static readonly Guid IFileDialog = Guid.Parse("42F85136-DB7E-439C-85F1-E4075D135FC8"); 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 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)] [ComImport(), Guid("42F85136-DB7E-439C-85F1-E4075D135FC8"), InterfaceType(ComInterfaceType.InterfaceIsIUnknown)]
@ -1874,6 +1880,22 @@ namespace Avalonia.Win32.Interop
[MarshalAs(UnmanagedType.LPWStr)] [MarshalAs(UnmanagedType.LPWStr)]
public string pszSpec; 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] [Flags]

11
src/Windows/Avalonia.Win32/ScreenImpl.cs

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

15
src/Windows/Avalonia.Win32/SystemDialogImpl.cs

@ -5,7 +5,6 @@ using System.Runtime.InteropServices;
using System.Threading.Tasks; using System.Threading.Tasks;
using Avalonia.Controls; using Avalonia.Controls;
using Avalonia.Controls.Platform; using Avalonia.Controls.Platform;
using Avalonia.Platform;
using Avalonia.Win32.Interop; using Avalonia.Win32.Interop;
namespace Avalonia.Win32 namespace Avalonia.Win32
@ -16,16 +15,16 @@ namespace Avalonia.Win32
private const UnmanagedMethods.FOS DefaultDialogOptions = UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE | private const UnmanagedMethods.FOS DefaultDialogOptions = UnmanagedMethods.FOS.FOS_FORCEFILESYSTEM | UnmanagedMethods.FOS.FOS_NOVALIDATE |
UnmanagedMethods.FOS.FOS_NOTESTFILECREATE | UnmanagedMethods.FOS.FOS_DONTADDTORECENT; 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(() => return Task.Factory.StartNew(() =>
{ {
var result = Array.Empty<string>(); var result = Array.Empty<string>();
Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog; Guid clsid = dialog is OpenFileDialog ? UnmanagedMethods.ShellIds.OpenFileDialog : UnmanagedMethods.ShellIds.SaveFileDialog;
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; var frm = (UnmanagedMethods.IFileDialog)unk;
var openDialog = dialog as OpenFileDialog; 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(() => return Task.Factory.StartNew(() =>
{ {
string result = string.Empty; 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 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; var frm = (UnmanagedMethods.IFileDialog)unk;
uint options; uint options;
frm.GetOptions(out options); frm.GetOptions(out options);

13
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);
}
}
}

192
src/Windows/Avalonia.Win32/WindowImpl.cs

@ -37,10 +37,14 @@ namespace Avalonia.Win32
{ WindowEdge.West, HitTestValues.HTLEFT } { WindowEdge.West, HitTestValues.HTLEFT }
}; };
private SavedWindowInfo _savedWindowInfo;
private bool _isFullScreenActive;
#if USE_MANAGED_DRAG #if USE_MANAGED_DRAG
private readonly ManagedWindowResizeDragHelper _managedDrag; private readonly ManagedWindowResizeDragHelper _managedDrag;
#endif #endif
private const WindowStyles WindowStateMask = (WindowStyles.WS_MAXIMIZE | WindowStyles.WS_MINIMIZE);
private readonly List<WindowImpl> _disabledBy; private readonly List<WindowImpl> _disabledBy;
private readonly TouchDevice _touchDevice; private readonly TouchDevice _touchDevice;
private readonly MouseDevice _mouseDevice; private readonly MouseDevice _mouseDevice;
@ -82,7 +86,9 @@ namespace Avalonia.Win32
_windowProperties = new WindowProperties _windowProperties = new WindowProperties
{ {
ShowInTaskbar = false, IsResizable = true, Decorations = SystemDecorations.Full ShowInTaskbar = false,
IsResizable = true,
Decorations = SystemDecorations.Full
}; };
_rendererLock = new ManagedDeferredRendererLock(); _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) private void ShowWindow(WindowState state)
{ {
ShowWindowCommand command; ShowWindowCommand command;
var newWindowProperties = _windowProperties;
switch (state) switch (state)
{ {
case WindowState.Minimized: case WindowState.Minimized:
newWindowProperties.IsFullScreen = false;
command = ShowWindowCommand.Minimize; command = ShowWindowCommand.Minimize;
break; break;
case WindowState.Maximized: case WindowState.Maximized:
newWindowProperties.IsFullScreen = false;
command = ShowWindowCommand.Maximize; command = ShowWindowCommand.Maximize;
break; break;
case WindowState.Normal: case WindowState.Normal:
newWindowProperties.IsFullScreen = false;
command = ShowWindowCommand.Restore; command = ShowWindowCommand.Restore;
break; break;
case WindowState.FullScreen:
newWindowProperties.IsFullScreen = true;
UpdateWindowProperties(newWindowProperties);
return;
default: default:
throw new ArgumentException("Invalid WindowState."); throw new ArgumentException("Invalid WindowState.");
} }
UpdateWindowProperties(newWindowProperties);
UnmanagedMethods.ShowWindow(_hwnd, command); UnmanagedMethods.ShowWindow(_hwnd, command);
if (state == WindowState.Maximized) if (state == WindowState.Maximized)
@ -590,22 +667,69 @@ namespace Avalonia.Win32
SetWindowPos(_hwnd, WindowPosZOrder.HWND_NOTOPMOST, x, y, cx, cy, SetWindowPosFlags.SWP_SHOWWINDOW); 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() private void UpdateEnabled()
{ {
EnableWindow(_hwnd, _disabledBy.Count == 0); EnableWindow(_hwnd, _disabledBy.Count == 0);
} }
private void UpdateWindowProperties(WindowProperties newProperties) private void UpdateWindowProperties(WindowProperties newProperties, bool forceChanges = false)
{ {
var oldProperties = _windowProperties; var oldProperties = _windowProperties;
@ -613,7 +737,7 @@ namespace Avalonia.Win32
// according to the new values already. // according to the new values already.
_windowProperties = newProperties; _windowProperties = newProperties;
if (oldProperties.ShowInTaskbar != newProperties.ShowInTaskbar) if ((oldProperties.ShowInTaskbar != newProperties.ShowInTaskbar) || forceChanges)
{ {
var exStyle = GetExtendedStyle(); var exStyle = GetExtendedStyle();
@ -632,7 +756,7 @@ namespace Avalonia.Win32
// Otherwise it will still show in the taskbar. // Otherwise it will still show in the taskbar.
} }
if (oldProperties.IsResizable != newProperties.IsResizable) if ((oldProperties.IsResizable != newProperties.IsResizable) || forceChanges)
{ {
var style = GetStyle(); var style = GetStyle();
@ -648,7 +772,12 @@ namespace Avalonia.Win32
SetStyle(style); SetStyle(style);
} }
if (oldProperties.Decorations != newProperties.Decorations) if (oldProperties.IsFullScreen != newProperties.IsFullScreen)
{
SetFullScreen(newProperties.IsFullScreen);
}
if ((oldProperties.Decorations != newProperties.Decorations) || forceChanges)
{ {
var style = GetStyle(); var style = GetStyle();
@ -663,30 +792,33 @@ namespace Avalonia.Win32
style &= ~fullDecorationFlags; 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); GetClientRect(_hwnd, out var oldClientRect);
var oldClientRectOrigin = new POINT(); var oldClientRectOrigin = new POINT();
ClientToScreen(_hwnd, ref oldClientRectOrigin); ClientToScreen(_hwnd, ref oldClientRectOrigin);
oldClientRect.Offset(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) SetWindowPos(_hwnd, IntPtr.Zero, newRect.left, newRect.top, newRect.Width, newRect.Height,
{ SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE |
AdjustWindowRectEx(ref newRect, (uint)style, false, (uint)GetExtendedStyle()); 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; 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 private struct WindowProperties
{ {
public bool ShowInTaskbar; public bool ShowInTaskbar;
public bool IsResizable; public bool IsResizable;
public SystemDecorations Decorations; public SystemDecorations Decorations;
public bool IsFullScreen;
} }
} }
} }

49
tests/Avalonia.Animation.UnitTests/AnimationIterationTests.cs

@ -14,6 +14,55 @@ namespace Avalonia.Animation.UnitTests
{ {
public class AnimationIterationTests 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] [Fact]
public void Check_Initial_Inter_and_Trailing_Delay_Values() public void Check_Initial_Inter_and_Trailing_Delay_Values()
{ {

145
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);
}
}
}

321
tests/Avalonia.Controls.UnitTests/WindowTests.cs

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

44
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]);
}
}
}

2
tests/Avalonia.RenderTests/TestBase.cs

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

48
tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs

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

@ -95,5 +95,18 @@ namespace Avalonia.Skia.UnitTests
Assert.Equal("Noto Mono", skTypeface.FamilyName); 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")));
}
}
} }
} }

24
tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs

@ -332,7 +332,7 @@ namespace Avalonia.Skia.UnitTests
Typeface.Default, Typeface.Default,
12.0f, 12.0f,
Brushes.Black.ToImmutable(), Brushes.Black.ToImmutable(),
maxWidth : 200, maxWidth : 200,
maxHeight : 125, maxHeight : 125,
textStyleOverrides: spans); 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() public static IDisposable Start()
{ {
var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface
.With(renderInterface: new PlatformRenderInterface(null), .With(renderInterface: new PlatformRenderInterface(null),
textShaperImpl: new TextShaperImpl(), textShaperImpl: new TextShaperImpl(),
fontManagerImpl : new CustomFontManagerImpl())); fontManagerImpl : new CustomFontManagerImpl()));

11
tests/Avalonia.UnitTests/MockFontManagerImpl.cs

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

12
tests/Avalonia.Visuals.UnitTests/Media/FontManagerTests.cs

@ -1,4 +1,5 @@
using Avalonia.Media; using System;
using Avalonia.Media;
using Avalonia.Platform; using Avalonia.Platform;
using Avalonia.UnitTests; using Avalonia.UnitTests;
using Xunit; using Xunit;
@ -19,5 +20,14 @@ namespace Avalonia.Visuals.UnitTests.Media
Assert.Same(typeface, FontManager.Current.GetOrAddTypeface(fontFamily)); 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);
}
}
} }
} }

Loading…
Cancel
Save