Browse Source

Merge branch 'master' into feature/3109-nonanimated-property-changes

pull/3853/head
Steven Kirk 6 years ago
parent
commit
31d9e1d5e0
  1. 2
      Documentation/build.md
  2. 49
      native/Avalonia.Native/inc/avalonia-native.h
  3. 4
      native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj
  4. 3
      native/Avalonia.Native/src/OSX/AvnString.h
  5. 49
      native/Avalonia.Native/src/OSX/AvnString.mm
  6. 90
      native/Avalonia.Native/src/OSX/clipboard.mm
  7. 8
      native/Avalonia.Native/src/OSX/common.h
  8. 89
      native/Avalonia.Native/src/OSX/dnd.mm
  9. 19
      native/Avalonia.Native/src/OSX/main.mm
  10. 2
      native/Avalonia.Native/src/OSX/window.h
  11. 108
      native/Avalonia.Native/src/OSX/window.mm
  12. 14
      nukebuild/Build.cs
  13. 5
      packages/Avalonia/AvaloniaBuildTasks.targets
  14. 2
      samples/ControlCatalog.NetCore/Program.cs
  15. 4
      samples/ControlCatalog/ControlCatalog.csproj
  16. 1
      samples/ControlCatalog/MainView.xaml
  17. 12
      samples/ControlCatalog/Pages/DragAndDropPage.xaml
  18. 92
      samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs
  19. 29
      samples/ControlCatalog/Pages/OpenGlPage.xaml
  20. 401
      samples/ControlCatalog/Pages/OpenGlPage.xaml.cs
  21. 1
      samples/ControlCatalog/Pages/TreeViewPage.xaml
  22. 100
      samples/ControlCatalog/Pages/TreeViewPage.xaml.cs
  23. BIN
      samples/ControlCatalog/Pages/teapot.bin
  24. 115
      samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs
  25. 53
      samples/RenderDemo/Controls/LineBoundsDemoControl.cs
  26. 3
      samples/RenderDemo/MainWindow.xaml
  27. 28
      samples/RenderDemo/Pages/AnimationsPage.xaml
  28. 9
      samples/RenderDemo/Pages/LineBoundsPage.xaml
  29. 19
      samples/RenderDemo/Pages/LineBoundsPage.xaml.cs
  30. 3
      samples/RenderDemo/RenderDemo.csproj
  31. 10
      scripts/ReplaceNugetCache.ps1
  32. 10
      scripts/ReplaceNugetCacheRelease.ps1
  33. 50
      src/Avalonia.Base/Utilities/DisposableLock.cs
  34. 11
      src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs
  35. 9
      src/Avalonia.Build.Tasks/Extensions.cs
  36. 31
      src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs
  37. 4
      src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs
  38. 2
      src/Avalonia.Controls.DataGrid/DataGrid.cs
  39. 4
      src/Avalonia.Controls.DataGrid/DataGridCell.cs
  40. 4
      src/Avalonia.Controls.DataGrid/DataGridRow.cs
  41. 4
      src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs
  42. 4
      src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs
  43. 8
      src/Avalonia.Controls/Application.cs
  44. 6
      src/Avalonia.Controls/AutoCompleteBox.cs
  45. 25
      src/Avalonia.Controls/Border.cs
  46. 2
      src/Avalonia.Controls/ButtonSpinner.cs
  47. 4
      src/Avalonia.Controls/Calendar/Calendar.cs
  48. 3
      src/Avalonia.Controls/Calendar/CalendarButton.cs
  49. 4
      src/Avalonia.Controls/Calendar/CalendarDayButton.cs
  50. 4
      src/Avalonia.Controls/Calendar/CalendarItem.cs
  51. 4
      src/Avalonia.Controls/Calendar/DatePicker.cs
  52. 4
      src/Avalonia.Controls/ComboBox.cs
  53. 39
      src/Avalonia.Controls/ContextMenu.cs
  54. 3
      src/Avalonia.Controls/ListBox.cs
  55. 4
      src/Avalonia.Controls/MenuItem.cs
  56. 4
      src/Avalonia.Controls/Notifications/WindowNotificationManager.cs
  57. 2
      src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs
  58. 21
      src/Avalonia.Controls/Presenters/ContentPresenter.cs
  59. 4
      src/Avalonia.Controls/Primitives/ScrollBar.cs
  60. 8
      src/Avalonia.Controls/Primitives/SelectingItemsControl.cs
  61. 11
      src/Avalonia.Controls/Primitives/TemplatedControl.cs
  62. 2
      src/Avalonia.Controls/ProgressBar.cs
  63. 4
      src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs
  64. 28
      src/Avalonia.Controls/Repeater/ItemsRepeater.cs
  65. 36
      src/Avalonia.Controls/Repeater/ViewManager.cs
  66. 42
      src/Avalonia.Controls/SelectionModel.cs
  67. 134
      src/Avalonia.Controls/SelectionNode.cs
  68. 2
      src/Avalonia.Controls/Slider.cs
  69. 4
      src/Avalonia.Controls/TabControl.cs
  70. 2
      src/Avalonia.Controls/TextBox.cs
  71. 18
      src/Avalonia.Controls/TreeView.cs
  72. 3
      src/Avalonia.Controls/TreeViewItem.cs
  73. 46
      src/Avalonia.Controls/Utils/BorderRenderHelper.cs
  74. 3
      src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj
  75. 4
      src/Avalonia.DesignerSupport/Remote/DetachableTransportConnection.cs
  76. 90
      src/Avalonia.DesignerSupport/Remote/FileWatcherTransport.cs
  77. 266
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs
  78. 472
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/SimpleWebSocketHttpServer.cs
  79. 2
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/.gitignore
  80. 8878
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json
  81. 41
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package.json
  82. 57
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/FramePresenter.tsx
  83. 78
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/PreviewerServerConnection.ts
  84. 14
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.html
  85. 15
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.tsx
  86. 35
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/tsconfig.json
  87. 117
      src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/webpack.config.js
  88. 63
      src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs
  89. 10
      src/Avalonia.Layout/FlowLayoutAlgorithm.cs
  90. 20
      src/Avalonia.Layout/StackLayout.cs
  91. 2
      src/Avalonia.Layout/StackLayoutState.cs
  92. 1
      src/Avalonia.Layout/UniformGridLayout.cs
  93. 76
      src/Avalonia.Native/AvaloniaNativeDragSource.cs
  94. 16
      src/Avalonia.Native/AvaloniaNativePlatform.cs
  95. 39
      src/Avalonia.Native/AvnString.cs
  96. 98
      src/Avalonia.Native/ClipboardImpl.cs
  97. 86
      src/Avalonia.Native/GlPlatformFeature.cs
  98. 3
      src/Avalonia.Native/PopupImpl.cs
  99. 5
      src/Avalonia.Native/WindowImpl.cs
  100. 37
      src/Avalonia.Native/WindowImplBase.cs

2
Documentation/build.md

@ -36,7 +36,7 @@ Avalonia requires [CastXML](https://github.com/CastXML/CastXML) for XML processi
On macOS:
```
brew install castxml
brew install https://raw.githubusercontent.com/Homebrew/homebrew-core/8a004a91a7fcd3f6620d5b01b6541ff0a640ffba/Formula/castxml.rb
```
On Debian based Linux (Debian, Ubuntu, Mint, etc):

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

@ -22,6 +22,9 @@ struct IAvnGlSurfaceRenderTarget;
struct IAvnGlSurfaceRenderingSession;
struct IAvnMenu;
struct IAvnMenuItem;
struct IAvnStringArray;
struct IAvnDndResultCallback;
struct IAvnGCHandleDeallocatorCallback;
struct IAvnMenuEvents;
enum SystemDecorations {
@ -130,6 +133,22 @@ enum AvnInputModifiers
XButton2MouseButton = 256
};
enum class AvnDragDropEffects
{
None = 0,
Copy = 1,
Move = 2,
Link = 4,
};
enum class AvnDragEventType
{
Enter,
Over,
Leave,
Drop
};
enum AvnWindowState
{
Normal,
@ -188,7 +207,7 @@ enum AvnMenuItemToggleType
AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown
{
public:
virtual HRESULT Initialize() = 0;
virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) = 0;
virtual IAvnMacOptions* GetMacOptions() = 0;
virtual HRESULT CreateWindow(IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnWindow** ppv) = 0;
virtual HRESULT CreatePopup (IAvnWindowEvents* cb, IAvnGlContext* gl, IAvnPopup** ppv) = 0;
@ -196,6 +215,7 @@ public:
virtual HRESULT CreateSystemDialogs (IAvnSystemDialogs** ppv) = 0;
virtual HRESULT CreateScreens (IAvnScreens** ppv) = 0;
virtual HRESULT CreateClipboard(IAvnClipboard** ppv) = 0;
virtual HRESULT CreateDndClipboard(IAvnClipboard** ppv) = 0;
virtual HRESULT CreateCursorFactory(IAvnCursorFactory** ppv) = 0;
virtual HRESULT ObtainGlDisplay(IAvnGlDisplay** ppv) = 0;
virtual HRESULT SetAppMenu(IAvnMenu* menu) = 0;
@ -236,6 +256,8 @@ AVNCOM(IAvnWindowBase, 02) : IUnknown
virtual HRESULT ObtainNSWindowHandleRetained(void** retOut) = 0;
virtual HRESULT ObtainNSViewHandle(void** retOut) = 0;
virtual HRESULT ObtainNSViewHandleRetained(void** retOut) = 0;
virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point,
IAvnClipboard* clipboard, IAvnDndResultCallback* cb, void* sourceHandle) = 0;
};
AVNCOM(IAvnPopup, 03) : virtual IAvnWindowBase
@ -271,6 +293,9 @@ AVNCOM(IAvnWindowBaseEvents, 05) : IUnknown
virtual bool RawTextInputEvent (unsigned int timeStamp, const char* text) = 0;
virtual void ScalingChanged(double scaling) = 0;
virtual void RunRenderPriorityJobs() = 0;
virtual AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position,
AvnInputModifiers modifiers, AvnDragDropEffects effects,
IAvnClipboard* clipboard, void* dataObjectHandle) = 0;
};
@ -354,8 +379,10 @@ AVNCOM(IAvnScreens, 0e) : IUnknown
AVNCOM(IAvnClipboard, 0f) : IUnknown
{
virtual HRESULT GetText (IAvnString**ppv) = 0;
virtual HRESULT SetText (void* utf8Text) = 0;
virtual HRESULT GetText (char* type, IAvnString**ppv) = 0;
virtual HRESULT SetText (char* type, void* utf8Text) = 0;
virtual HRESULT ObtainFormats(IAvnStringArray**ppv) = 0;
virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) = 0;
virtual HRESULT Clear() = 0;
};
@ -428,4 +455,20 @@ AVNCOM(IAvnMenuEvents, 1A) : IUnknown
virtual void NeedsUpdate () = 0;
};
AVNCOM(IAvnStringArray, 20) : IUnknown
{
virtual unsigned int GetCount() = 0;
virtual HRESULT Get(unsigned int index, IAvnString**ppv) = 0;
};
AVNCOM(IAvnDndResultCallback, 21) : IUnknown
{
virtual void OnDragAndDropComplete(AvnDragDropEffects effecct) = 0;
};
AVNCOM(IAvnGCHandleDeallocatorCallback, 22) : IUnknown
{
virtual void FreeGCHandle(void* handle) = 0;
};
extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative();

4
native/Avalonia.Native/src/OSX/Avalonia.Native.OSX.xcodeproj/project.pbxproj

@ -12,6 +12,7 @@
1A3E5EAA23E9F26C00EDE661 /* IOSurface.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */; };
1A3E5EAE23E9FB1300EDE661 /* cgl.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A3E5EAD23E9FB1300EDE661 /* cgl.mm */; };
1A3E5EB023E9FE8300EDE661 /* QuartzCore.framework in Frameworks */ = {isa = PBXBuildFile; fileRef = 1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */; };
1A465D10246AB61600C5858B /* dnd.mm in Sources */ = {isa = PBXBuildFile; fileRef = 1A465D0F246AB61600C5858B /* dnd.mm */; };
37155CE4233C00EB0034DCE9 /* menu.h in Headers */ = {isa = PBXBuildFile; fileRef = 37155CE3233C00EB0034DCE9 /* menu.h */; };
37A517B32159597E00FBA241 /* Screens.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37A517B22159597E00FBA241 /* Screens.mm */; };
37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */ = {isa = PBXBuildFile; fileRef = 37C09D8721580FE4006A6758 /* SystemDialogs.mm */; };
@ -33,6 +34,7 @@
1A3E5EA923E9F26C00EDE661 /* IOSurface.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = IOSurface.framework; path = System/Library/Frameworks/IOSurface.framework; sourceTree = SDKROOT; };
1A3E5EAD23E9FB1300EDE661 /* cgl.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = cgl.mm; sourceTree = "<group>"; };
1A3E5EAF23E9FE8300EDE661 /* QuartzCore.framework */ = {isa = PBXFileReference; lastKnownFileType = wrapper.framework; name = QuartzCore.framework; path = System/Library/Frameworks/QuartzCore.framework; sourceTree = SDKROOT; };
1A465D0F246AB61600C5858B /* dnd.mm */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.cpp.objcpp; path = dnd.mm; sourceTree = "<group>"; };
37155CE3233C00EB0034DCE9 /* menu.h */ = {isa = PBXFileReference; fileEncoding = 4; lastKnownFileType = sourcecode.c.h; path = menu.h; sourceTree = "<group>"; };
379860FE214DA0C000CD0246 /* KeyTransform.h */ = {isa = PBXFileReference; lastKnownFileType = sourcecode.c.h; path = KeyTransform.h; sourceTree = "<group>"; };
37A4E71A2178846A00EACBCD /* headers */ = {isa = PBXFileReference; lastKnownFileType = folder; name = headers; path = ../../inc; sourceTree = "<group>"; };
@ -92,6 +94,7 @@
5BF943652167AD1D009CAE35 /* cursor.h */,
5B21A981216530F500CEE36E /* cursor.mm */,
5B8BD94E215BFEA6005ED2A7 /* clipboard.mm */,
1A465D0F246AB61600C5858B /* dnd.mm */,
AB8F7D6A21482D7F0057DBA5 /* platformthreading.mm */,
AB661C212148288600291242 /* common.h */,
379860FE214DA0C000CD0246 /* KeyTransform.h */,
@ -196,6 +199,7 @@
37E2330F21583241000CB7E2 /* KeyTransform.mm in Sources */,
520624B322973F4100C4DCEF /* menu.mm in Sources */,
37A517B32159597E00FBA241 /* Screens.mm in Sources */,
1A465D10246AB61600C5858B /* dnd.mm in Sources */,
AB00E4F72147CA920032A60A /* main.mm in Sources */,
37C09D8821580FE4006A6758 /* SystemDialogs.mm in Sources */,
AB661C202148286E00291242 /* window.mm in Sources */,

3
native/Avalonia.Native/src/OSX/AvnString.h

@ -10,5 +10,6 @@
#define AvnString_h
extern IAvnString* CreateAvnString(NSString* string);
extern IAvnStringArray* CreateAvnStringArray(NSArray<NSString*>* array);
extern IAvnStringArray* CreateAvnStringArray(NSString* string);
#endif /* AvnString_h */

49
native/Avalonia.Native/src/OSX/AvnString.mm

@ -7,6 +7,7 @@
//
#include "common.h"
#include <vector>
class AvnStringImpl : public virtual ComSingleObject<IAvnString, &IID_IAvnString>
{
@ -61,7 +62,55 @@ public:
}
};
class AvnStringArrayImpl : public virtual ComSingleObject<IAvnStringArray, &IID_IAvnStringArray>
{
private:
std::vector<ComPtr<IAvnString>> _list;
public:
FORWARD_IUNKNOWN()
AvnStringArrayImpl(NSArray<NSString*>* array)
{
for(int c = 0; c < [array count]; c++)
{
ComPtr<IAvnString> s;
*s.getPPV() = new AvnStringImpl([array objectAtIndex:c]);
_list.push_back(s);
}
}
AvnStringArrayImpl(NSString* string)
{
ComPtr<IAvnString> s;
*s.getPPV() = new AvnStringImpl(string);
_list.push_back(s);
}
virtual unsigned int GetCount() override
{
return (unsigned int)_list.size();
}
virtual HRESULT Get(unsigned int index, IAvnString**ppv) override
{
if(_list.size() <= index)
return E_INVALIDARG;
*ppv = _list[index].getRetainedReference();
return S_OK;
}
};
IAvnString* CreateAvnString(NSString* string)
{
return new AvnStringImpl(string);
}
IAvnStringArray* CreateAvnStringArray(NSArray<NSString*> * array)
{
return new AvnStringArrayImpl(array);
}
IAvnStringArray* CreateAvnStringArray(NSString* string)
{
return new AvnStringArrayImpl(string);
}

90
native/Avalonia.Native/src/OSX/clipboard.mm

@ -3,16 +3,27 @@
class Clipboard : public ComSingleObject<IAvnClipboard, &IID_IAvnClipboard>
{
private:
NSPasteboard* _pb;
NSPasteboardItem* _item;
public:
FORWARD_IUNKNOWN()
Clipboard()
Clipboard(NSPasteboard* pasteboard, NSPasteboardItem* item)
{
NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
[pasteBoard stringForType:NSPasteboardTypeString];
if(pasteboard == nil && item == nil)
pasteboard = [NSPasteboard generalPasteboard];
_pb = pasteboard;
_item = item;
}
virtual HRESULT GetText (IAvnString**ppv) override
NSPasteboardItem* TryGetItem()
{
return _item;
}
virtual HRESULT GetText (char* type, IAvnString**ppv) override
{
@autoreleasepool
{
@ -20,20 +31,53 @@ public:
{
return E_POINTER;
}
NSString* typeString = [NSString stringWithUTF8String:(const char*)type];
NSString* string = _item == nil ? [_pb stringForType:typeString] : [_item stringForType:typeString];
*ppv = CreateAvnString([[NSPasteboard generalPasteboard] stringForType:NSPasteboardTypeString]);
*ppv = CreateAvnString(string);
return S_OK;
}
}
virtual HRESULT SetText (void* utf8String) override
virtual HRESULT GetStrings(char* type, IAvnStringArray**ppv) override
{
@autoreleasepool
{
NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
[pasteBoard clearContents];
[pasteBoard setString:[NSString stringWithUTF8String:(const char*)utf8String] forType:NSPasteboardTypeString];
*ppv= nil;
NSString* typeString = [NSString stringWithUTF8String:(const char*)type];
NSObject* data = _item == nil ? [_pb propertyListForType: typeString] : [_item propertyListForType: typeString];
if(data == nil)
return S_OK;
if([data isKindOfClass: [NSString class]])
{
*ppv = CreateAvnStringArray((NSString*) data);
return S_OK;
}
NSArray* arr = (NSArray*)data;
for(int c = 0; c < [arr count]; c++)
if(![[arr objectAtIndex:c] isKindOfClass:[NSString class]])
return E_INVALIDARG;
*ppv = CreateAvnStringArray(arr);
return S_OK;
}
}
virtual HRESULT SetText (char* type, void* utf8String) override
{
Clear();
@autoreleasepool
{
auto string = [NSString stringWithUTF8String:(const char*)utf8String];
auto typeString = [NSString stringWithUTF8String:(const char*)type];
if(_item == nil)
[_pb setString: string forType: typeString];
else
[_item setString: string forType:typeString];
}
return S_OK;
@ -43,16 +87,34 @@ public:
{
@autoreleasepool
{
NSPasteboard *pasteBoard = [NSPasteboard generalPasteboard];
[pasteBoard clearContents];
[pasteBoard setString:@"" forType:NSPasteboardTypeString];
if(_item != nil)
_item = [NSPasteboardItem new];
else
{
[_pb clearContents];
[_pb setString:@"" forType:NSPasteboardTypeString];
}
}
return S_OK;
}
virtual HRESULT ObtainFormats(IAvnStringArray** ppv) override
{
*ppv = CreateAvnStringArray(_item == nil ? [_pb types] : [_item types]);
return S_OK;
}
};
extern IAvnClipboard* CreateClipboard()
extern IAvnClipboard* CreateClipboard(NSPasteboard* pb, NSPasteboardItem* item)
{
return new Clipboard(pb, item);
}
extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*cb)
{
return new Clipboard();
auto clipboard = dynamic_cast<Clipboard*>(cb);
if(clipboard == nil)
return nil;
return clipboard->TryGetItem();
}

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

@ -8,11 +8,17 @@
#include <pthread.h>
extern IAvnPlatformThreadingInterface* CreatePlatformThreading();
extern void FreeAvnGCHandle(void* handle);
extern IAvnWindow* CreateAvnWindow(IAvnWindowEvents*events, IAvnGlContext* gl);
extern IAvnPopup* CreateAvnPopup(IAvnWindowEvents*events, IAvnGlContext* gl);
extern IAvnSystemDialogs* CreateSystemDialogs();
extern IAvnScreens* CreateScreens();
extern IAvnClipboard* CreateClipboard();
extern IAvnClipboard* CreateClipboard(NSPasteboard*, NSPasteboardItem*);
extern NSPasteboardItem* TryGetPasteboardItem(IAvnClipboard*);
extern NSObject<NSDraggingSource>* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle);
extern void* GetAvnDataObjectHandleFromDraggingInfo(NSObject<NSDraggingInfo>* info);
extern NSString* GetAvnCustomDataType();
extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop);
extern IAvnCursorFactory* CreateCursorFactory();
extern IAvnGlDisplay* GetGlDisplay();
extern IAvnMenu* CreateAppMenu(IAvnMenuEvents* events);

89
native/Avalonia.Native/src/OSX/dnd.mm

@ -0,0 +1,89 @@
#include "common.h"
extern AvnDragDropEffects ConvertDragDropEffects(NSDragOperation nsop)
{
int effects = 0;
if((nsop & NSDragOperationCopy) != 0)
effects |= (int)AvnDragDropEffects::Copy;
if((nsop & NSDragOperationMove) != 0)
effects |= (int)AvnDragDropEffects::Move;
if((nsop & NSDragOperationLink) != 0)
effects |= (int)AvnDragDropEffects::Link;
return (AvnDragDropEffects)effects;
};
extern NSString* GetAvnCustomDataType()
{
char buffer[256];
sprintf(buffer, "net.avaloniaui.inproc.uti.n%in", getpid());
return [NSString stringWithUTF8String:buffer];
}
@interface AvnDndSource : NSObject<NSDraggingSource>
@end
@implementation AvnDndSource
{
NSDragOperation _operation;
ComPtr<IAvnDndResultCallback> _cb;
void* _sourceHandle;
};
- (NSDragOperation)draggingSession:(nonnull NSDraggingSession *)session sourceOperationMaskForDraggingContext:(NSDraggingContext)context
{
return NSDragOperationCopy;
}
- (AvnDndSource*) initWithOperation: (NSDragOperation)operation
andCallback: (IAvnDndResultCallback*) cb
andSourceHandle: (void*) handle
{
self = [super init];
_operation = operation;
_cb = cb;
_sourceHandle = handle;
return self;
}
- (void)draggingSession:(NSDraggingSession *)session
endedAtPoint:(NSPoint)screenPoint
operation:(NSDragOperation)operation
{
if(_cb != nil)
{
auto cb = _cb;
_cb = nil;
cb->OnDragAndDropComplete(ConvertDragDropEffects(operation));
}
if(_sourceHandle != nil)
{
FreeAvnGCHandle(_sourceHandle);
_sourceHandle = nil;
}
}
- (void*) gcHandle
{
return _sourceHandle;
}
@end
extern NSObject<NSDraggingSource>* CreateDraggingSource(NSDragOperation op, IAvnDndResultCallback* cb, void* handle)
{
return [[AvnDndSource alloc] initWithOperation:op andCallback:cb andSourceHandle:handle];
};
extern void* GetAvnDataObjectHandleFromDraggingInfo(NSObject<NSDraggingInfo>* info)
{
id obj = [info draggingSource];
if(obj == nil)
return nil;
if([obj isKindOfClass: [AvnDndSource class]])
{
auto src = (AvnDndSource*)obj;
return [src gcHandle];
}
return nil;
}

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

@ -150,14 +150,15 @@ public:
}
@end
static ComPtr<IAvnGCHandleDeallocatorCallback> _deallocator;
class AvaloniaNative : public ComSingleObject<IAvaloniaNativeFactory, &IID_IAvaloniaNativeFactory>
{
public:
FORWARD_IUNKNOWN()
virtual HRESULT Initialize() override
virtual HRESULT Initialize(IAvnGCHandleDeallocatorCallback* deallocator) override
{
_deallocator = deallocator;
@autoreleasepool{
[[ThreadingInitializer new] do];
}
@ -207,7 +208,13 @@ public:
virtual HRESULT CreateClipboard(IAvnClipboard** ppv) override
{
*ppv = ::CreateClipboard ();
*ppv = ::CreateClipboard (nil, nil);
return S_OK;
}
virtual HRESULT CreateDndClipboard(IAvnClipboard** ppv) override
{
*ppv = ::CreateClipboard (nil, [NSPasteboardItem new]);
return S_OK;
}
@ -257,6 +264,12 @@ extern "C" IAvaloniaNativeFactory* CreateAvaloniaNative()
return new AvaloniaNative();
};
extern void FreeAvnGCHandle(void* handle)
{
if(_deallocator != nil)
_deallocator->FreeGCHandle(handle);
}
NSSize ToNSSize (AvnSize s)
{
NSSize result;

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

@ -3,7 +3,7 @@
class WindowBaseImpl;
@interface AvnView : NSView<NSTextInputClient>
@interface AvnView : NSView<NSTextInputClient, NSDraggingDestination>
-(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent;
-(NSEvent* _Nonnull) lastMouseDownEvent;
-(AvnPoint) translateLocalPoint:(AvnPoint)pt;

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

@ -382,6 +382,50 @@ public:
*ppv = [renderTarget createSurfaceRenderTarget];
return *ppv == nil ? E_FAIL : S_OK;
}
virtual HRESULT BeginDragAndDropOperation(AvnDragDropEffects effects, AvnPoint point,
IAvnClipboard* clipboard, IAvnDndResultCallback* cb,
void* sourceHandle) override
{
auto item = TryGetPasteboardItem(clipboard);
[item setString:@"" forType:GetAvnCustomDataType()];
if(item == nil)
return E_INVALIDARG;
if(View == NULL)
return E_FAIL;
auto nsevent = [NSApp currentEvent];
auto nseventType = [nsevent type];
// If current event isn't a mouse one (probably due to malfunctioning user app)
// attempt to forge a new one
if(!((nseventType >= NSEventTypeLeftMouseDown && nseventType <= NSEventTypeMouseExited)
|| (nseventType >= NSEventTypeOtherMouseDown && nseventType <= NSEventTypeOtherMouseDragged)))
{
auto nspoint = [Window convertBaseToScreen: ToNSPoint(point)];
CGPoint cgpoint = NSPointToCGPoint(nspoint);
auto cgevent = CGEventCreateMouseEvent(NULL, kCGEventLeftMouseDown, cgpoint, kCGMouseButtonLeft);
nsevent = [NSEvent eventWithCGEvent: cgevent];
CFRelease(cgevent);
}
auto dragItem = [[NSDraggingItem alloc] initWithPasteboardWriter: item];
auto dragItemImage = [NSImage imageNamed:NSImageNameMultipleDocuments];
NSRect dragItemRect = {(float)point.X, (float)point.Y, [dragItemImage size].width, [dragItemImage size].height};
[dragItem setDraggingFrame: dragItemRect contents: dragItemImage];
int op = 0; int ieffects = (int)effects;
if((ieffects & (int)AvnDragDropEffects::Copy) != 0)
op |= NSDragOperationCopy;
if((ieffects & (int)AvnDragDropEffects::Link) != 0)
op |= NSDragOperationLink;
if((ieffects & (int)AvnDragDropEffects::Move) != 0)
op |= NSDragOperationMove;
[View beginDraggingSessionWithItems: @[dragItem] event: nsevent
source: CreateDraggingSource((NSDragOperation) op, cb, sourceHandle)];
return S_OK;
}
protected:
virtual NSWindowStyleMask GetStyle()
@ -911,7 +955,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
_area = nullptr;
_lastPixelSize.Height = 100;
_lastPixelSize.Width = 100;
[self registerForDraggedTypes: @[@"public.data", GetAvnCustomDataType()]];
return self;
}
@ -1302,6 +1346,68 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent
return result;
}
- (NSDragOperation)triggerAvnDragEvent: (AvnDragEventType) type info: (id <NSDraggingInfo>)info
{
auto localPoint = [self convertPoint:[info draggingLocation] toView:self];
auto avnPoint = [self toAvnPoint:localPoint];
auto point = [self translateLocalPoint:avnPoint];
auto modifiers = [self getModifiers:[[NSApp currentEvent] modifierFlags]];
NSDragOperation nsop = [info draggingSourceOperationMask];
auto effects = ConvertDragDropEffects(nsop);
int reffects = (int)_parent->BaseEvents
->DragEvent(type, point, modifiers, effects,
CreateClipboard([info draggingPasteboard], nil),
GetAvnDataObjectHandleFromDraggingInfo(info));
NSDragOperation ret = 0;
// Ensure that the managed part didn't add any new effects
reffects = (int)effects & (int)reffects;
// OSX requires exactly one operation
if((reffects & (int)AvnDragDropEffects::Copy) != 0)
ret = NSDragOperationCopy;
else if((reffects & (int)AvnDragDropEffects::Move) != 0)
ret = NSDragOperationMove;
else if((reffects & (int)AvnDragDropEffects::Link) != 0)
ret = NSDragOperationLink;
if(ret == 0)
ret = NSDragOperationNone;
return ret;
}
- (NSDragOperation)draggingEntered:(id <NSDraggingInfo>)sender
{
return [self triggerAvnDragEvent: AvnDragEventType::Enter info:sender];
}
- (NSDragOperation)draggingUpdated:(id <NSDraggingInfo>)sender
{
return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender];
}
- (void)draggingExited:(id <NSDraggingInfo>)sender
{
[self triggerAvnDragEvent: AvnDragEventType::Leave info:sender];
}
- (BOOL)prepareForDragOperation:(id <NSDraggingInfo>)sender
{
return [self triggerAvnDragEvent: AvnDragEventType::Over info:sender] != NSDragOperationNone;
}
- (BOOL)performDragOperation:(id <NSDraggingInfo>)sender
{
return [self triggerAvnDragEvent: AvnDragEventType::Drop info:sender] != NSDragOperationNone;
}
- (void)concludeDragOperation:(nullable id <NSDraggingInfo>)sender
{
}
@end

14
nukebuild/Build.cs

@ -12,6 +12,7 @@ using Nuke.Common.ProjectModel;
using Nuke.Common.Tooling;
using Nuke.Common.Tools.DotNet;
using Nuke.Common.Tools.MSBuild;
using Nuke.Common.Tools.Npm;
using Nuke.Common.Utilities;
using Nuke.Common.Utilities.Collections;
using static Nuke.Common.EnvironmentInfo;
@ -121,8 +122,21 @@ partial class Build : NukeBuild
EnsureCleanDirectory(Parameters.TestResultsRoot);
});
Target CompileHtmlPreviewer => _ => _
.DependsOn(Clean)
.Executes(() =>
{
var webappDir = RootDirectory / "src" / "Avalonia.DesignerSupport" / "Remote" / "HtmlTransport" / "webapp";
NpmTasks.NpmInstall(c => c.SetWorkingDirectory(webappDir));
NpmTasks.NpmRun(c => c
.SetWorkingDirectory(webappDir)
.SetCommand("dist"));
});
Target Compile => _ => _
.DependsOn(Clean)
.DependsOn(CompileHtmlPreviewer)
.Executes(() =>
{
if (Parameters.IsRunningOnWindows)

5
packages/Avalonia/AvaloniaBuildTasks.targets

@ -2,6 +2,7 @@
<PropertyGroup>
<_AvaloniaUseExternalMSBuild>$(AvaloniaUseExternalMSBuild)</_AvaloniaUseExternalMSBuild>
<_AvaloniaUseExternalMSBuild Condition="'$(_AvaloniaForceInternalMSBuild)' == 'true'">false</_AvaloniaUseExternalMSBuild>
<AvaloniaXamlReportImportance Condition="'$(AvaloniaXamlReportImportance)' == ''">low</AvaloniaXamlReportImportance>
</PropertyGroup>
<UsingTask TaskName="GenerateAvaloniaResourcesTask"
@ -38,7 +39,8 @@
Output="$(AvaloniaResourcesTemporaryFilePath)"
Root="$(MSBuildProjectDirectory)"
Resources="@(AvaloniaResource)"
EmbeddedResources="@(EmbeddedResources)"/>
EmbeddedResources="@(EmbeddedResources)"
ReportImportance="$(AvaloniaXamlReportImportance)"/>
<Exec
Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'"
Command="dotnet msbuild /nodereuse:false $(MSBuildProjectFile) /t:GenerateAvaloniaResources /p:_AvaloniaForceInternalMSBuild=true /p:Configuration=$(Configuration) /p:TargetFramework=$(TargetFramework) /p:BuildProjectReferences=false"/>
@ -67,6 +69,7 @@
OriginalCopyPath="$(AvaloniaXamlOriginalCopyFilePath)"
ProjectDirectory="$(MSBuildProjectDirectory)"
VerifyIl="$(AvaloniaXamlIlVerifyIl)"
ReportImportance="$(AvaloniaXamlReportImportance)"
/>
<Exec
Condition="'$(_AvaloniaUseExternalMSBuild)' == 'true'"

2
samples/ControlCatalog.NetCore/Program.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Globalization;
using System.Linq;
@ -6,6 +7,7 @@ using System.Threading;
using Avalonia;
using Avalonia.ReactiveUI;
using Avalonia.Dialogs;
using Avalonia.OpenGL;
namespace ControlCatalog.NetCore
{

4
samples/ControlCatalog/ControlCatalog.csproj

@ -1,6 +1,7 @@
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<TargetFramework>netstandard2.0</TargetFramework>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<Compile Update="**\*.xaml.cs">
@ -17,6 +18,7 @@
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-BoldItalic.ttf" />
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-Italic.ttf" />
<EmbeddedResource Include="Assets\Fonts\SourceSansPro-Regular.ttf" />
<EmbeddedResource Include="Pages\teapot.bin" />
</ItemGroup>
<ItemGroup>

1
samples/ControlCatalog/MainView.xaml

@ -47,6 +47,7 @@
<TabItem Header="Menu"><pages:MenuPage/></TabItem>
<TabItem Header="Notifications"><pages:NotificationsPage/></TabItem>
<TabItem Header="NumericUpDown"><pages:NumericUpDownPage/></TabItem>
<TabItem Header="OpenGL"><pages:OpenGlPage/></TabItem>
<TabItem Header="Pointers (Touch)"><pages:PointersPage/></TabItem>
<TabItem Header="ProgressBar"><pages:ProgressBarPage/></TabItem>
<TabItem Header="RadioButton"><pages:RadioButtonPage/></TabItem>

12
samples/ControlCatalog/Pages/DragAndDropPage.xaml

@ -9,9 +9,15 @@
Margin="0,16,0,0"
HorizontalAlignment="Center"
Spacing="16">
<Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16" Name="DragMe">
<TextBlock Name="DragState">Drag Me</TextBlock>
</Border>
<StackPanel>
<Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16" Name="DragMeText">
<TextBlock Name="DragStateText">Drag Me</TextBlock>
</Border>
<Border BorderBrush="{DynamicResource ThemeAccentBrush}" BorderThickness="2" Padding="16" Name="DragMeCustom">
<TextBlock Name="DragStateCustom">Drag Me (custom)</TextBlock>
</Border>
</StackPanel>
<Border Background="{DynamicResource ThemeAccentBrush2}" Padding="16"
DragDrop.AllowDrop="True">
<TextBlock Name="DropState">Drop some text or files here</TextBlock>

92
samples/ControlCatalog/Pages/DragAndDropPage.xaml.cs

@ -3,69 +3,85 @@ using Avalonia.Input;
using Avalonia.Markup.Xaml;
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Reflection;
using System.Text;
namespace ControlCatalog.Pages
{
public class DragAndDropPage : UserControl
{
private TextBlock _DropState;
private TextBlock _DragState;
private Border _DragMe;
private int DragCount = 0;
TextBlock _DropState;
private const string CustomFormat = "application/xxx-avalonia-controlcatalog-custom";
public DragAndDropPage()
{
this.InitializeComponent();
_DropState = this.Find<TextBlock>("DropState");
_DragMe.PointerPressed += DoDrag;
int textCount = 0;
SetupDnd("Text", d => d.Set(DataFormats.Text,
$"Text was dragged {++textCount} times"));
AddHandler(DragDrop.DropEvent, Drop);
AddHandler(DragDrop.DragOverEvent, DragOver);
SetupDnd("Custom", d => d.Set(CustomFormat, "Test123"));
}
private async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e)
void SetupDnd(string suffix, Action<DataObject> factory, DragDropEffects effects = DragDropEffects.Copy)
{
DataObject dragData = new DataObject();
dragData.Set(DataFormats.Text, $"You have dragged text {++DragCount} times");
var dragMe = this.Find<Border>("DragMe" + suffix);
var dragState = this.Find<TextBlock>("DragState"+suffix);
var result = await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy);
switch(result)
async void DoDrag(object sender, Avalonia.Input.PointerPressedEventArgs e)
{
case DragDropEffects.Copy:
_DragState.Text = "The text was copied"; break;
case DragDropEffects.Link:
_DragState.Text = "The text was linked"; break;
case DragDropEffects.None:
_DragState.Text = "The drag operation was canceled"; break;
var dragData = new DataObject();
factory(dragData);
var result = await DragDrop.DoDragDrop(e, dragData, DragDropEffects.Copy);
switch (result)
{
case DragDropEffects.Copy:
dragState.Text = "Data was copied";
break;
case DragDropEffects.Link:
dragState.Text = "Data was linked";
break;
case DragDropEffects.None:
dragState.Text = "The drag operation was canceled";
break;
}
}
}
private void DragOver(object sender, DragEventArgs e)
{
// Only allow Copy or Link as Drop Operations.
e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link);
void DragOver(object sender, DragEventArgs e)
{
// Only allow Copy or Link as Drop Operations.
e.DragEffects = e.DragEffects & (DragDropEffects.Copy | DragDropEffects.Link);
// Only allow if the dragged data contains text or filenames.
if (!e.Data.Contains(DataFormats.Text) && !e.Data.Contains(DataFormats.FileNames))
e.DragEffects = DragDropEffects.None;
}
// Only allow if the dragged data contains text or filenames.
if (!e.Data.Contains(DataFormats.Text)
&& !e.Data.Contains(DataFormats.FileNames)
&& !e.Data.Contains(CustomFormat))
e.DragEffects = DragDropEffects.None;
}
private void Drop(object sender, DragEventArgs e)
{
if (e.Data.Contains(DataFormats.Text))
_DropState.Text = e.Data.GetText();
else if (e.Data.Contains(DataFormats.FileNames))
_DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames());
void Drop(object sender, DragEventArgs e)
{
if (e.Data.Contains(DataFormats.Text))
_DropState.Text = e.Data.GetText();
else if (e.Data.Contains(DataFormats.FileNames))
_DropState.Text = string.Join(Environment.NewLine, e.Data.GetFileNames());
else if (e.Data.Contains(CustomFormat))
_DropState.Text = "Custom: " + e.Data.Get(CustomFormat);
}
dragMe.PointerPressed += DoDrag;
AddHandler(DragDrop.DropEvent, Drop);
AddHandler(DragDrop.DragOverEvent, DragOver);
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
_DropState = this.Find<TextBlock>("DropState");
_DragState = this.Find<TextBlock>("DragState");
_DragMe = this.Find<Border>("DragMe");
}
}
}

29
samples/ControlCatalog/Pages/OpenGlPage.xaml

@ -0,0 +1,29 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
x:Class="ControlCatalog.Pages.OpenGlPage"
xmlns:pages="clr-namespace:ControlCatalog.Pages">
<Grid>
<pages:OpenGlPageControl x:Name="GL"/>
<StackPanel>
<TextBlock Text="{Binding #GL.Info}"/>
</StackPanel>
<Grid ColumnDefinitions="*,Auto" Margin="20">
<StackPanel Grid.Column="1" MinWidth="300">
<TextBlock>Yaw</TextBlock>
<Slider Value="{Binding Yaw, Mode=TwoWay, ElementName=GL}" Maximum="10"/>
<TextBlock>Pitch</TextBlock>
<Slider Value="{Binding Pitch, Mode=TwoWay, ElementName=GL}" Maximum="10"/>
<TextBlock>Roll</TextBlock>
<Slider Value="{Binding Roll, Mode=TwoWay, ElementName=GL}" Maximum="10"/>
<StackPanel Orientation="Horizontal">
<TextBlock FontWeight="Bold" Foreground="#C000C0">D</TextBlock>
<TextBlock FontWeight="Bold" Foreground="#00C090">I</TextBlock>
<TextBlock FontWeight="Bold" Foreground="#90C000">S</TextBlock>
<TextBlock FontWeight="Bold" Foreground="#C09000">C</TextBlock>
<TextBlock FontWeight="Bold" Foreground="#00C090">O</TextBlock>
</StackPanel>
<Slider Value="{Binding Disco, Mode=TwoWay, ElementName=GL}" Maximum="1"/>
</StackPanel>
</Grid>
</Grid>
</UserControl>

401
samples/ControlCatalog/Pages/OpenGlPage.xaml.cs

@ -0,0 +1,401 @@
using System;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Numerics;
using System.Runtime.InteropServices;
using Avalonia;
using Avalonia.Controls;
using Avalonia.OpenGL;
using Avalonia.Platform.Interop;
using Avalonia.Threading;
using static Avalonia.OpenGL.GlConsts;
// ReSharper disable StringLiteralTypo
namespace ControlCatalog.Pages
{
public class OpenGlPage : UserControl
{
}
public class OpenGlPageControl : OpenGlControlBase
{
private float _yaw;
public static readonly DirectProperty<OpenGlPageControl, float> YawProperty =
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Yaw", o => o.Yaw, (o, v) => o.Yaw = v);
public float Yaw
{
get => _yaw;
set => SetAndRaise(YawProperty, ref _yaw, value);
}
private float _pitch;
public static readonly DirectProperty<OpenGlPageControl, float> PitchProperty =
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Pitch", o => o.Pitch, (o, v) => o.Pitch = v);
public float Pitch
{
get => _pitch;
set => SetAndRaise(PitchProperty, ref _pitch, value);
}
private float _roll;
public static readonly DirectProperty<OpenGlPageControl, float> RollProperty =
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Roll", o => o.Roll, (o, v) => o.Roll = v);
public float Roll
{
get => _roll;
set => SetAndRaise(RollProperty, ref _roll, value);
}
private float _disco;
public static readonly DirectProperty<OpenGlPageControl, float> DiscoProperty =
AvaloniaProperty.RegisterDirect<OpenGlPageControl, float>("Disco", o => o.Disco, (o, v) => o.Disco = v);
public float Disco
{
get => _disco;
set => SetAndRaise(DiscoProperty, ref _disco, value);
}
private string _info;
public static readonly DirectProperty<OpenGlPageControl, string> InfoProperty =
AvaloniaProperty.RegisterDirect<OpenGlPageControl, string>("Info", o => o.Info, (o, v) => o.Info = v);
public string Info
{
get => _info;
private set => SetAndRaise(InfoProperty, ref _info, value);
}
static OpenGlPageControl()
{
AffectsRender<OpenGlPageControl>(YawProperty, PitchProperty, RollProperty, DiscoProperty);
}
private int _vertexShader;
private int _fragmentShader;
private int _shaderProgram;
private int _vertexBufferObject;
private int _indexBufferObject;
private int _vertexArrayObject;
private GlExtrasInterface _glExt;
private string GetShader(bool fragment, string shader)
{
var version = (GlVersion.Type == GlProfileType.OpenGL ?
RuntimeInformation.IsOSPlatform(OSPlatform.OSX) ? 150 : 120 :
100);
var data = "#version " + version + "\n";
if (GlVersion.Type == GlProfileType.OpenGLES)
data += "precision mediump float;\n";
if (version >= 150)
{
shader = shader.Replace("attribute", "in");
if (fragment)
shader = shader
.Replace("varying", "in")
.Replace("//DECLAREGLFRAG", "out vec4 outFragColor;")
.Replace("gl_FragColor", "outFragColor");
else
shader = shader.Replace("varying", "out");
}
data += shader;
return data;
}
private string VertexShaderSource => GetShader(false, @"
attribute vec3 aPos;
attribute vec3 aNormal;
uniform mat4 uModel;
uniform mat4 uProjection;
uniform mat4 uView;
varying vec3 FragPos;
varying vec3 VecPos;
varying vec3 Normal;
uniform float uTime;
uniform float uDisco;
void main()
{
float discoScale = sin(uTime * 10.0) / 10.0;
float distortionX = 1.0 + uDisco * cos(uTime * 20.0) / 10.0;
float scale = 1.0 + uDisco * discoScale;
vec3 scaledPos = aPos;
scaledPos.x = scaledPos.x * distortionX;
scaledPos *= scale;
gl_Position = uProjection * uView * uModel * vec4(scaledPos, 1.0);
FragPos = vec3(uModel * vec4(aPos, 1.0));
VecPos = aPos;
Normal = normalize(vec3(uModel * vec4(aNormal, 1.0)));
}
");
private string FragmentShaderSource => GetShader(true, @"
varying vec3 FragPos;
varying vec3 VecPos;
varying vec3 Normal;
uniform float uMaxY;
uniform float uMinY;
uniform float uTime;
uniform float uDisco;
//DECLAREGLFRAG
void main()
{
float y = (VecPos.y - uMinY) / (uMaxY - uMinY);
float c = cos(atan(VecPos.x, VecPos.z) * 20.0 + uTime * 40.0 + y * 50.0);
float s = sin(-atan(VecPos.z, VecPos.x) * 20.0 - uTime * 20.0 - y * 30.0);
vec3 discoColor = vec3(
0.5 + abs(0.5 - y) * cos(uTime * 10.0),
0.25 + (smoothstep(0.3, 0.8, y) * (0.5 - c / 4.0)),
0.25 + abs((smoothstep(0.1, 0.4, y) * (0.5 - s / 4.0))));
vec3 objectColor = vec3((1.0 - y), 0.40 + y / 4.0, y * 0.75 + 0.25);
objectColor = objectColor * (1.0 - uDisco) + discoColor * uDisco;
float ambientStrength = 0.3;
vec3 lightColor = vec3(1.0, 1.0, 1.0);
vec3 lightPos = vec3(uMaxY * 2.0, uMaxY * 2.0, uMaxY * 2.0);
vec3 ambient = ambientStrength * lightColor;
vec3 norm = normalize(Normal);
vec3 lightDir = normalize(lightPos - FragPos);
float diff = max(dot(norm, lightDir), 0.0);
vec3 diffuse = diff * lightColor;
vec3 result = (ambient + diffuse) * objectColor;
gl_FragColor = vec4(result, 1.0);
}
");
[StructLayout(LayoutKind.Sequential, Pack = 4)]
private struct Vertex
{
public Vector3 Position;
public Vector3 Normal;
}
private readonly Vertex[] _points;
private readonly ushort[] _indices;
private readonly float _minY;
private readonly float _maxY;
public OpenGlPageControl()
{
var name = typeof(OpenGlPage).Assembly.GetManifestResourceNames().First(x => x.Contains("teapot.bin"));
using (var sr = new BinaryReader(typeof(OpenGlPage).Assembly.GetManifestResourceStream(name)))
{
var buf = new byte[sr.ReadInt32()];
sr.Read(buf, 0, buf.Length);
var points = new float[buf.Length / 4];
Buffer.BlockCopy(buf, 0, points, 0, buf.Length);
buf = new byte[sr.ReadInt32()];
sr.Read(buf, 0, buf.Length);
_indices = new ushort[buf.Length / 2];
Buffer.BlockCopy(buf, 0, _indices, 0, buf.Length);
_points = new Vertex[points.Length / 3];
for (var primitive = 0; primitive < points.Length / 3; primitive++)
{
var srci = primitive * 3;
_points[primitive] = new Vertex
{
Position = new Vector3(points[srci], points[srci + 1], points[srci + 2])
};
}
for (int i = 0; i < _indices.Length; i += 3)
{
Vector3 a = _points[_indices[i]].Position;
Vector3 b = _points[_indices[i + 1]].Position;
Vector3 c = _points[_indices[i + 2]].Position;
var normal = Vector3.Normalize(Vector3.Cross(c - b, a - b));
_points[_indices[i]].Normal += normal;
_points[_indices[i + 1]].Normal += normal;
_points[_indices[i + 2]].Normal += normal;
}
for (int i = 0; i < _points.Length; i++)
{
_points[i].Normal = Vector3.Normalize(_points[i].Normal);
_maxY = Math.Max(_maxY, _points[i].Position.Y);
_minY = Math.Min(_minY, _points[i].Position.Y);
}
}
}
private void CheckError(GlInterface gl)
{
int err;
while ((err = gl.GetError()) != GL_NO_ERROR)
Console.WriteLine(err);
}
protected unsafe override void OnOpenGlInit(GlInterface GL, int fb)
{
CheckError(GL);
_glExt = new GlExtrasInterface(GL);
Info = $"Renderer: {GL.GetString(GL_RENDERER)} Version: {GL.GetString(GL_VERSION)}";
// Load the source of the vertex shader and compile it.
_vertexShader = GL.CreateShader(GL_VERTEX_SHADER);
Console.WriteLine(GL.CompileShaderAndGetError(_vertexShader, VertexShaderSource));
// Load the source of the fragment shader and compile it.
_fragmentShader = GL.CreateShader(GL_FRAGMENT_SHADER);
Console.WriteLine(GL.CompileShaderAndGetError(_fragmentShader, FragmentShaderSource));
// Create the shader program, attach the vertex and fragment shaders and link the program.
_shaderProgram = GL.CreateProgram();
GL.AttachShader(_shaderProgram, _vertexShader);
GL.AttachShader(_shaderProgram, _fragmentShader);
const int positionLocation = 0;
const int normalLocation = 1;
GL.BindAttribLocationString(_shaderProgram, positionLocation, "aPos");
GL.BindAttribLocationString(_shaderProgram, normalLocation, "aNormal");
Console.WriteLine(GL.LinkProgramAndGetError(_shaderProgram));
CheckError(GL);
// Create the vertex buffer object (VBO) for the vertex data.
_vertexBufferObject = GL.GenBuffer();
// Bind the VBO and copy the vertex data into it.
GL.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject);
CheckError(GL);
var vertexSize = Marshal.SizeOf<Vertex>();
fixed (void* pdata = _points)
GL.BufferData(GL_ARRAY_BUFFER, new IntPtr(_points.Length * vertexSize),
new IntPtr(pdata), GL_STATIC_DRAW);
_indexBufferObject = GL.GenBuffer();
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBufferObject);
CheckError(GL);
fixed (void* pdata = _indices)
GL.BufferData(GL_ELEMENT_ARRAY_BUFFER, new IntPtr(_indices.Length * sizeof(ushort)), new IntPtr(pdata),
GL_STATIC_DRAW);
CheckError(GL);
_vertexArrayObject = _glExt.GenVertexArray();
_glExt.BindVertexArray(_vertexArrayObject);
CheckError(GL);
GL.VertexAttribPointer(positionLocation, 3, GL_FLOAT,
0, vertexSize, IntPtr.Zero);
GL.VertexAttribPointer(normalLocation, 3, GL_FLOAT,
0, vertexSize, new IntPtr(12));
GL.EnableVertexAttribArray(positionLocation);
GL.EnableVertexAttribArray(normalLocation);
CheckError(GL);
}
protected override void OnOpenGlDeinit(GlInterface GL, int fb)
{
// Unbind everything
GL.BindBuffer(GL_ARRAY_BUFFER, 0);
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, 0);
_glExt.BindVertexArray(0);
GL.UseProgram(0);
// Delete all resources.
GL.DeleteBuffers(2, new[] { _vertexBufferObject, _indexBufferObject });
_glExt.DeleteVertexArrays(1, new[] { _vertexArrayObject });
GL.DeleteProgram(_shaderProgram);
GL.DeleteShader(_fragmentShader);
GL.DeleteShader(_vertexShader);
}
static Stopwatch St = Stopwatch.StartNew();
protected override unsafe void OnOpenGlRender(GlInterface gl, int fb)
{
gl.ClearColor(0, 0, 0, 0);
gl.Clear(GL_COLOR_BUFFER_BIT | GL_DEPTH_BUFFER_BIT);
gl.Enable(GL_DEPTH_TEST);
gl.Viewport(0, 0, (int)Bounds.Width, (int)Bounds.Height);
var GL = gl;
GL.BindBuffer(GL_ARRAY_BUFFER, _vertexBufferObject);
GL.BindBuffer(GL_ELEMENT_ARRAY_BUFFER, _indexBufferObject);
_glExt.BindVertexArray(_vertexArrayObject);
GL.UseProgram(_shaderProgram);
CheckError(GL);
var projection =
Matrix4x4.CreatePerspectiveFieldOfView((float)(Math.PI / 4), (float)(Bounds.Width / Bounds.Height),
0.01f, 1000);
var view = Matrix4x4.CreateLookAt(new Vector3(25, 25, 25), new Vector3(), new Vector3(0, -1, 0));
var model = Matrix4x4.CreateFromYawPitchRoll(_yaw, _pitch, _roll);
var modelLoc = GL.GetUniformLocationString(_shaderProgram, "uModel");
var viewLoc = GL.GetUniformLocationString(_shaderProgram, "uView");
var projectionLoc = GL.GetUniformLocationString(_shaderProgram, "uProjection");
var maxYLoc = GL.GetUniformLocationString(_shaderProgram, "uMaxY");
var minYLoc = GL.GetUniformLocationString(_shaderProgram, "uMinY");
var timeLoc = GL.GetUniformLocationString(_shaderProgram, "uTime");
var discoLoc = GL.GetUniformLocationString(_shaderProgram, "uDisco");
GL.UniformMatrix4fv(modelLoc, 1, false, &model);
GL.UniformMatrix4fv(viewLoc, 1, false, &view);
GL.UniformMatrix4fv(projectionLoc, 1, false, &projection);
GL.Uniform1f(maxYLoc, _maxY);
GL.Uniform1f(minYLoc, _minY);
GL.Uniform1f(timeLoc, (float)St.Elapsed.TotalSeconds);
GL.Uniform1f(discoLoc, _disco);
CheckError(GL);
GL.DrawElements(GL_TRIANGLES, _indices.Length, GL_UNSIGNED_SHORT, IntPtr.Zero);
CheckError(GL);
if (_disco > 0.01)
Dispatcher.UIThread.Post(InvalidateVisual, DispatcherPriority.Background);
}
class GlExtrasInterface : GlInterfaceBase<GlInterface.GlContextInfo>
{
public GlExtrasInterface(GlInterface gl) : base(gl.GetProcAddress, gl.ContextInfo)
{
}
public delegate void GlDeleteVertexArrays(int count, int[] buffers);
[GlMinVersionEntryPoint("glDeleteVertexArrays", 3,0)]
[GlExtensionEntryPoint("glDeleteVertexArraysOES", "GL_OES_vertex_array_object")]
public GlDeleteVertexArrays DeleteVertexArrays { get; }
public delegate void GlBindVertexArray(int array);
[GlMinVersionEntryPoint("glBindVertexArray", 3,0)]
[GlExtensionEntryPoint("glBindVertexArrayOES", "GL_OES_vertex_array_object")]
public GlBindVertexArray BindVertexArray { get; }
public delegate void GlGenVertexArrays(int n, int[] rv);
[GlMinVersionEntryPoint("glGenVertexArrays",3,0)]
[GlExtensionEntryPoint("glGenVertexArraysOES", "GL_OES_vertex_array_object")]
public GlGenVertexArrays GenVertexArrays { get; }
public int GenVertexArray()
{
var rv = new int[1];
GenVertexArrays(1, rv);
return rv[0];
}
}
}
}

1
samples/ControlCatalog/Pages/TreeViewPage.xaml

@ -19,7 +19,6 @@
</TreeView>
<Button Command="{Binding AddItemCommand}">Add</Button>
<Button Command="{Binding RemoveItemCommand}">Remove</Button>
<ComboBox SelectedIndex="{Binding SelectionMode, Mode=TwoWay}">

100
samples/ControlCatalog/Pages/TreeViewPage.xaml.cs

@ -1,9 +1,6 @@
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
using ReactiveUI;
using ControlCatalog.ViewModels;
namespace ControlCatalog.Pages
{
@ -12,105 +9,12 @@ namespace ControlCatalog.Pages
public TreeViewPage()
{
InitializeComponent();
DataContext = new PageViewModel();
DataContext = new TreeViewPageViewModel();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
private class PageViewModel : ReactiveObject
{
private SelectionMode _selectionMode;
public PageViewModel()
{
Node root = new Node();
Items = root.Children;
Selection = new SelectionModel();
AddItemCommand = ReactiveCommand.Create(() =>
{
Node parentItem = Selection.SelectedItems.Count > 0 ?
(Node)Selection.SelectedItems[0] : root;
parentItem.AddNewItem();
});
RemoveItemCommand = ReactiveCommand.Create(() =>
{
while (Selection.SelectedItems.Count > 0)
{
Node lastItem = (Node)Selection.SelectedItems[0];
RecursiveRemove(Items, lastItem);
Selection.DeselectAt(Selection.SelectedIndices[0]);
}
bool RecursiveRemove(ObservableCollection<Node> items, Node selectedItem)
{
if (items.Remove(selectedItem))
{
return true;
}
foreach (Node item in items)
{
if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem))
{
return true;
}
}
return false;
}
});
}
public ObservableCollection<Node> Items { get; }
public SelectionModel Selection { get; }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
public SelectionMode SelectionMode
{
get => _selectionMode;
set
{
Selection.ClearSelection();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
}
}
private class Node
{
private int _counter;
private ObservableCollection<Node> _children;
public string Header { get; private set; }
public bool AreChildrenInitialized => _children != null;
public ObservableCollection<Node> Children
{
get
{
if (_children == null)
{
_children = new ObservableCollection<Node>(Enumerable.Range(1, 10).Select(i => CreateNewNode()));
}
return _children;
}
}
public void AddNewItem() => Children.Add(CreateNewNode());
public override string ToString() => Header;
private Node CreateNewNode() => new Node { Header = $"Item {_counter++}" };
}
}
}

BIN
samples/ControlCatalog/Pages/teapot.bin

Binary file not shown.

115
samples/ControlCatalog/ViewModels/TreeViewPageViewModel.cs

@ -0,0 +1,115 @@
using System;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.Linq;
using System.Reactive;
using Avalonia.Controls;
using ReactiveUI;
namespace ControlCatalog.ViewModels
{
public class TreeViewPageViewModel : ReactiveObject
{
private readonly Node _root;
private SelectionMode _selectionMode;
public TreeViewPageViewModel()
{
_root = new Node();
Items = _root.Children;
Selection = new SelectionModel();
Selection.SelectionChanged += SelectionChanged;
AddItemCommand = ReactiveCommand.Create(AddItem);
RemoveItemCommand = ReactiveCommand.Create(RemoveItem);
}
public ObservableCollection<Node> Items { get; }
public SelectionModel Selection { get; }
public ReactiveCommand<Unit, Unit> AddItemCommand { get; }
public ReactiveCommand<Unit, Unit> RemoveItemCommand { get; }
public SelectionMode SelectionMode
{
get => _selectionMode;
set
{
Selection.ClearSelection();
this.RaiseAndSetIfChanged(ref _selectionMode, value);
}
}
private void AddItem()
{
var parentItem = Selection.SelectedItems.Count > 0 ? (Node)Selection.SelectedItems[0] : _root;
parentItem.AddItem();
}
private void RemoveItem()
{
while (Selection.SelectedItems.Count > 0)
{
Node lastItem = (Node)Selection.SelectedItems[0];
RecursiveRemove(Items, lastItem);
Selection.DeselectAt(Selection.SelectedIndices[0]);
}
bool RecursiveRemove(ObservableCollection<Node> items, Node selectedItem)
{
if (items.Remove(selectedItem))
{
return true;
}
foreach (Node item in items)
{
if (item.AreChildrenInitialized && RecursiveRemove(item.Children, selectedItem))
{
return true;
}
}
return false;
}
}
private void SelectionChanged(object sender, SelectionModelSelectionChangedEventArgs e)
{
var selected = string.Join(",", e.SelectedIndices);
var deselected = string.Join(",", e.DeselectedIndices);
System.Diagnostics.Debug.WriteLine($"Selected '{selected}', Deselected '{deselected}'");
}
public class Node
{
private ObservableCollection<Node> _children;
private int _childIndex = 10;
public Node()
{
Header = "Item";
}
public Node(Node parent, int index)
{
Parent = parent;
Header = parent.Header + ' ' + index;
}
public Node Parent { get; }
public string Header { get; }
public bool AreChildrenInitialized => _children != null;
public ObservableCollection<Node> Children => _children ??= CreateChildren();
public void AddItem() => Children.Add(new Node(this, _childIndex++));
public void RemoveItem(Node child) => Children.Remove(child);
public override string ToString() => Header;
private ObservableCollection<Node> CreateChildren()
{
return new ObservableCollection<Node>(
Enumerable.Range(0, 10).Select(i => new Node(this, i)));
}
}
}
}

53
samples/RenderDemo/Controls/LineBoundsDemoControl.cs

@ -0,0 +1,53 @@
using System;
using Avalonia;
using Avalonia.Controls;
using Avalonia.Media;
using Avalonia.Rendering.SceneGraph;
using Avalonia.Threading;
namespace RenderDemo.Controls
{
public class LineBoundsDemoControl : Control
{
static LineBoundsDemoControl()
{
AffectsRender<LineBoundsDemoControl>(AngleProperty);
}
public LineBoundsDemoControl()
{
var timer = new DispatcherTimer();
timer.Interval = TimeSpan.FromSeconds(1 / 60.0);
timer.Tick += (sender, e) => Angle += Math.PI / 360;
timer.Start();
}
public static readonly StyledProperty<double> AngleProperty =
AvaloniaProperty.Register<LineBoundsDemoControl, double>(nameof(Angle));
public double Angle
{
get => GetValue(AngleProperty);
set => SetValue(AngleProperty, value);
}
public override void Render(DrawingContext drawingContext)
{
var lineLength = Math.Sqrt((100 * 100) + (100 * 100));
var diffX = LineBoundsHelper.CalculateAdjSide(Angle, lineLength);
var diffY = LineBoundsHelper.CalculateOppSide(Angle, lineLength);
var p1 = new Point(200, 200);
var p2 = new Point(p1.X + diffX, p1.Y + diffY);
var pen = new Pen(Brushes.Green, 20, lineCap: PenLineCap.Square);
var boundPen = new Pen(Brushes.Black);
drawingContext.DrawLine(pen, p1, p2);
drawingContext.DrawRectangle(boundPen, LineBoundsHelper.CalculateBounds(p1, p2, pen));
}
}
}

3
samples/RenderDemo/MainWindow.xaml

@ -44,6 +44,9 @@
<TabItem Header="GlyphRun">
<pages:GlyphRunPage/>
</TabItem>
<TabItem Header="LineBounds">
<pages:LineBoundsPage />
</TabItem>
</TabControl>
</DockPanel>
</Window>

28
samples/RenderDemo/Pages/AnimationsPage.xaml

@ -134,6 +134,32 @@
</Animation>
</Style.Animations>
</Style>
<Style Selector="Border.Shadow">
<Setter Property="BorderBrush" Value="Black"/>
<Setter Property="BorderThickness" Value="1"/>
<Style.Animations>
<Animation Duration="0:0:3"
IterationCount="Infinite"
PlaybackDirection="Alternate">
<KeyFrame Cue="0%">
<Setter Property="BoxShadow" Value="inset 0 0 0 2 Red, -15 -15 Green"/>
</KeyFrame>
<KeyFrame Cue="35%">
<Setter Property="BoxShadow" Value="inset 0 0 20 2 Blue, -15 20 0 0 Blue"/>
</KeyFrame>
<KeyFrame Cue="70%">
<Setter Property="BoxShadow" Value="inset 0 0 20 30 Green, 20 20 20 0 Red"/>
</KeyFrame>
<KeyFrame Cue="85%">
<Setter Property="BoxShadow" Value="inset 30 0 20 30 Green, 20 20 20 10 Red"/>
</KeyFrame>
<KeyFrame Cue="100%">
<Setter Property="BoxShadow" Value="inset 30 30 20 30 Green, 20 40 20 10 Red"/>
</KeyFrame>
</Animation>
</Style.Animations>
</Style>
</Styles>
</UserControl.Styles>
<Grid>
@ -152,6 +178,8 @@
<Border Classes="Test Rect4" Background="Navy"/>
<Border Classes="Test Rect5" Background="SeaGreen"/>
<Border Classes="Test Rect6" Background="Red"/>
<Border Classes="Test Shadow" CornerRadius="10" Child="{x:Null}" />
<Border Classes="Test Shadow" CornerRadius="0 30 60 0" Child="{x:Null}" />
</WrapPanel>
</StackPanel>
</Grid>

9
samples/RenderDemo/Pages/LineBoundsPage.xaml

@ -0,0 +1,9 @@
<UserControl xmlns="https://github.com/avaloniaui"
xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml"
xmlns:d="http://schemas.microsoft.com/expression/blend/2008"
xmlns:mc="http://schemas.openxmlformats.org/markup-compatibility/2006"
mc:Ignorable="d" d:DesignWidth="800" d:DesignHeight="450"
xmlns:controls="clr-namespace:RenderDemo.Controls"
x:Class="RenderDemo.Pages.LineBoundsPage">
<controls:LineBoundsDemoControl />
</UserControl>

19
samples/RenderDemo/Pages/LineBoundsPage.xaml.cs

@ -0,0 +1,19 @@
using Avalonia;
using Avalonia.Controls;
using Avalonia.Markup.Xaml;
namespace RenderDemo.Pages
{
public class LineBoundsPage : UserControl
{
public LineBoundsPage()
{
this.InitializeComponent();
}
private void InitializeComponent()
{
AvaloniaXamlLoader.Load(this);
}
}
}

3
samples/RenderDemo/RenderDemo.csproj

@ -3,6 +3,9 @@
<OutputType>Exe</OutputType>
<TargetFramework>netcoreapp3.1</TargetFramework>
</PropertyGroup>
<ItemGroup>
<Compile Include="..\..\src\Avalonia.Visuals\Rendering\SceneGraph\LineBoundsHelper.cs" Link="LineBoundsHelper.cs" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\..\src\Avalonia.Diagnostics\Avalonia.Diagnostics.csproj" />
<ProjectReference Include="..\..\src\Linux\Avalonia.LinuxFramebuffer\Avalonia.LinuxFramebuffer.csproj" />

10
scripts/ReplaceNugetCache.ps1

@ -1,5 +1,5 @@
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp2.0\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Debug\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\

10
scripts/ReplaceNugetCacheRelease.ps1

@ -1,5 +1,5 @@
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.gtk3\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore.\bin\Release\netcoreapp2.0\Avalonia**.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netcoreapp2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia**.dll ~\.nuget\packages\avalonia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia.Win32.dll ~\.nuget\packages\avalonia.win32\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.skia\$args\lib\netstandard2.0\
copy ..\samples\ControlCatalog.NetCore\bin\Release\netcoreapp3.1\Avalonia.Skia.dll ~\.nuget\packages\avalonia.direct2d1\$args\lib\netstandard2.0\

50
src/Avalonia.Base/Utilities/DisposableLock.cs

@ -0,0 +1,50 @@
using System;
using System.Threading;
namespace Avalonia.Utilities
{
public class DisposableLock
{
private readonly object _lock = new object();
/// <summary>
/// Tries to take a lock
/// </summary>
/// <returns>IDisposable if succeeded to obtain the lock</returns>
public IDisposable TryLock()
{
if (Monitor.TryEnter(_lock))
return new UnlockDisposable(_lock);
return null;
}
/// <summary>
/// Enters a waiting lock
/// </summary>
public IDisposable Lock()
{
Monitor.Enter(_lock);
return new UnlockDisposable(_lock);
}
private sealed class UnlockDisposable : IDisposable
{
private object _lock;
public UnlockDisposable(object @lock)
{
_lock = @lock;
}
public void Dispose()
{
object @lock = Interlocked.Exchange(ref _lock, null);
if (@lock != null)
{
Monitor.Exit(@lock);
}
}
}
}
}

11
src/Avalonia.Build.Tasks/CompileAvaloniaXamlTask.cs

@ -12,6 +12,8 @@ namespace Avalonia.Build.Tasks
{
public bool Execute()
{
Enum.TryParse(ReportImportance, true, out MessageImportance outputImportance);
OutputPath = OutputPath ?? AssemblyFile;
var outputPdb = GetPdbPath(OutputPath);
var input = AssemblyFile;
@ -32,9 +34,12 @@ namespace Avalonia.Build.Tasks
}
}
var msg = $"CompileAvaloniaXamlTask -> AssemblyFile:{AssemblyFile}, ProjectDirectory:{ProjectDirectory}, OutputPath:{OutputPath}";
BuildEngine.LogMessage(msg, outputImportance < MessageImportance.Low ? MessageImportance.High : outputImportance);
var res = XamlCompilerTaskExecutor.Compile(BuildEngine, input,
File.ReadAllLines(ReferencesFilePath).Where(l => !string.IsNullOrWhiteSpace(l)).ToArray(),
ProjectDirectory, OutputPath, VerifyIl);
ProjectDirectory, OutputPath, VerifyIl, outputImportance);
if (!res.Success)
return false;
if (!res.WrittenFile)
@ -68,7 +73,9 @@ namespace Avalonia.Build.Tasks
public string OutputPath { get; set; }
public bool VerifyIl { get; set; }
public string ReportImportance { get; set; }
public IBuildEngine BuildEngine { get; set; }
public ITaskHost HostObject { get; set; }
}

9
src/Avalonia.Build.Tasks/Extensions.cs

@ -9,14 +9,19 @@ namespace Avalonia.Build.Tasks
public static void LogError(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message)
{
engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message,
engine.LogErrorEvent(new BuildErrorEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message,
"", "Avalonia"));
}
public static void LogWarning(this IBuildEngine engine, BuildEngineErrorCode code, string file, string message)
{
engine.LogWarningEvent(new BuildWarningEventArgs("Avalonia", FormatErrorCode(code), file ?? "", 0, 0, 0, 0, message,
"", "Avalonia"));
}
public static void LogMessage(this IBuildEngine engine, string message, MessageImportance imp)
{
engine.LogMessageEvent(new BuildMessageEventArgs(message, "", "Avalonia", imp));
}
}
}

31
src/Avalonia.Build.Tasks/GenerateAvaloniaResourcesTask.cs

@ -22,6 +22,10 @@ namespace Avalonia.Build.Tasks
[Required]
public ITaskItem[] EmbeddedResources { get; set; }
public string ReportImportance { get; set; }
private MessageImportance _reportImportance;
class Source
{
public string Path { get; set; }
@ -29,15 +33,11 @@ namespace Avalonia.Build.Tasks
private byte[] _data;
private string _sourcePath;
public Source(string file, string root)
public Source(string relativePath, string root)
{
file = SPath.GetFullPath(file);
root = SPath.GetFullPath(root);
var fileUri = new Uri(file, UriKind.Absolute);
var rootUri = new Uri(root, UriKind.Absolute);
rootUri = new Uri(rootUri.ToString().TrimEnd('/') + '/');
Path = '/' + rootUri.MakeRelativeUri(fileUri).ToString().TrimStart('/');
_sourcePath = file;
Path = "/" + relativePath.Replace('\\', '/');
_sourcePath = SPath.Combine(root, relativePath);
Size = (int)new FileInfo(_sourcePath).Length;
}
@ -65,7 +65,14 @@ namespace Avalonia.Build.Tasks
}
}
List<Source> BuildResourceSources() => Resources.Select(r => new Source(r.ItemSpec, Root)).ToList();
List<Source> BuildResourceSources()
=> Resources.Select(r =>
{
var src = new Source(r.ItemSpec, Root);
BuildEngine.LogMessage($"avares -> name:{src.Path}, path: {src.SystemPath}, size:{src.Size}, ItemSpec:{r.ItemSpec}", _reportImportance);
return src;
}).ToList();
private void Pack(Stream output, List<Source> sources)
{
@ -136,10 +143,14 @@ namespace Avalonia.Build.Tasks
sources.Add(new Source("/!AvaloniaResourceXamlInfo", ms.ToArray()));
return true;
}
public bool Execute()
{
foreach(var r in EmbeddedResources.Where(r=>r.ItemSpec.EndsWith(".xaml")||r.ItemSpec.EndsWith(".paml")))
Enum.TryParse<MessageImportance>(ReportImportance, out _reportImportance);
BuildEngine.LogMessage($"GenerateAvaloniaResourcesTask -> Root: {Root}, {Resources?.Count()} resources, Output:{Output}", _reportImportance < MessageImportance.Low ? MessageImportance.High : _reportImportance);
foreach (var r in EmbeddedResources.Where(r => r.ItemSpec.EndsWith(".xaml") || r.ItemSpec.EndsWith(".paml")))
BuildEngine.LogWarning(BuildEngineErrorCode.LegacyResmScheme, r.ItemSpec,
"XAML file is packed using legacy EmbeddedResource/resm scheme, relative URIs won't work");
var resources = BuildResourceSources();

4
src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs

@ -40,7 +40,7 @@ namespace Avalonia.Build.Tasks
}
public static CompileResult Compile(IBuildEngine engine, string input, string[] references, string projectDirectory,
string output, bool verifyIl)
string output, bool verifyIl, MessageImportance logImportance)
{
var typeSystem = new CecilTypeSystem(references.Concat(new[] {input}), input);
var asm = typeSystem.TargetAssemblyDefinition;
@ -121,6 +121,8 @@ namespace Avalonia.Build.Tasks
{
try
{
engine.LogMessage($"XAMLIL: {res.Name} -> {res.Uri}", logImportance);
// StreamReader is needed here to handle BOM
var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd();
var parsed = XDocumentXamlIlParser.Parse(xaml);

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

@ -2245,7 +2245,7 @@ namespace Avalonia.Controls
/// Builds the visual tree for the column header when a new template is applied.
/// </summary>
//TODO Validation UI
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
// The template has changed, so we need to refresh the visuals
_measured = false;

4
src/Avalonia.Controls.DataGrid/DataGridCell.cs

@ -121,10 +121,8 @@ namespace Avalonia.Controls
/// <summary>
/// Builds the visual tree for the cell control when a new template is applied.
/// </summary>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
UpdatePseudoClasses();
_rightGridLine = e.NameScope.Find<Rectangle>(DATAGRIDCELL_elementRightGridLine);
if (_rightGridLine != null && OwningColumn == null)

4
src/Avalonia.Controls.DataGrid/DataGridRow.cs

@ -536,10 +536,8 @@ namespace Avalonia.Controls
/// <summary>
/// Builds the visual tree for the column header when a new template is applied.
/// </summary>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
RootElement = e.NameScope.Find<Panel>(DATAGRIDROW_elementRoot);
if (RootElement != null)
{

4
src/Avalonia.Controls.DataGrid/DataGridRowGroupHeader.cs

@ -168,7 +168,7 @@ namespace Avalonia.Controls
private IDisposable _expanderButtonSubscription;
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_rootElement = e.NameScope.Find<Panel>(DataGridRow.DATAGRIDROW_elementRoot);
@ -199,8 +199,6 @@ namespace Avalonia.Controls
_itemCountElement = e.NameScope.Find<TextBlock>(DATAGRIDROWGROUPHEADER_itemCountElement);
_propertyNameElement = e.NameScope.Find<TextBlock>(DATAGRIDROWGROUPHEADER_propertyNameElement);
UpdateTitleElements();
base.OnTemplateApplied(e);
}
internal void ApplyHeaderStatus()

4
src/Avalonia.Controls.DataGrid/DataGridRowHeader.cs

@ -94,10 +94,8 @@ namespace Avalonia.Controls.Primitives
/// <summary>
/// Builds the visual tree for the row header when a new template is applied.
/// </summary>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
_rootElement = e.NameScope.Find<Control>(DATAGRIDROWHEADER_elementRootName);
if (_rootElement != null)
{

8
src/Avalonia.Controls/Application.cs

@ -254,8 +254,12 @@ namespace Avalonia
.Bind<IKeyboardNavigationHandler>().ToTransient<KeyboardNavigationHandler>()
.Bind<IStyler>().ToConstant(_styler)
.Bind<IScheduler>().ToConstant(AvaloniaScheduler.Instance)
.Bind<IDragDropDevice>().ToConstant(DragDropDevice.Instance)
.Bind<IPlatformDragSource>().ToTransient<InProcessDragSource>();
.Bind<IDragDropDevice>().ToConstant(DragDropDevice.Instance);
// TODO: Fix this, for now we keep this behavior since someone might be relying on it in 0.9.x
if (AvaloniaLocator.Current.GetService<IPlatformDragSource>() == null)
AvaloniaLocator.CurrentMutable
.Bind<IPlatformDragSource>().ToTransient<InProcessDragSource>();
var clock = new RenderLoopClock();
AvaloniaLocator.CurrentMutable

6
src/Avalonia.Controls/AutoCompleteBox.cs

@ -10,6 +10,7 @@ using System.Collections.ObjectModel;
using System.Collections.Specialized;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Collections;
@ -1100,6 +1101,7 @@ namespace Avalonia.Controls
{
_textBoxSubscriptions =
_textBox.GetObservable(TextBox.TextProperty)
.Skip(1)
.Subscribe(_ => OnTextBoxTextChanged());
if (Text != null)
@ -1212,7 +1214,7 @@ namespace Avalonia.Controls
/// <see cref="T:Avalonia.Controls.AutoCompleteBox" /> control
/// when a new template is applied.
/// </summary>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (DropDownPopup != null)
@ -1240,7 +1242,7 @@ namespace Avalonia.Controls
OpeningDropDown(false);
}
base.OnTemplateApplied(e);
base.OnApplyTemplate(e);
}
/// <summary>

25
src/Avalonia.Controls/Border.cs

@ -33,6 +33,12 @@ namespace Avalonia.Controls
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
AvaloniaProperty.Register<Border, CornerRadius>(nameof(CornerRadius));
/// <summary>
/// Defines the <see cref="BoxShadow"/> property.
/// </summary>
public static readonly StyledProperty<BoxShadows> BoxShadowProperty =
AvaloniaProperty.Register<Border, BoxShadows>(nameof(BoxShadow));
private readonly BorderRenderHelper _borderRenderHelper = new BorderRenderHelper();
/// <summary>
@ -44,7 +50,8 @@ namespace Avalonia.Controls
BackgroundProperty,
BorderBrushProperty,
BorderThicknessProperty,
CornerRadiusProperty);
CornerRadiusProperty,
BoxShadowProperty);
AffectsMeasure<Border>(BorderThicknessProperty);
}
@ -83,14 +90,24 @@ namespace Avalonia.Controls
get { return GetValue(CornerRadiusProperty); }
set { SetValue(CornerRadiusProperty, value); }
}
/// <summary>
/// Gets or sets the box shadow effect parameters
/// </summary>
public BoxShadows BoxShadow
{
get => GetValue(BoxShadowProperty);
set => SetValue(BoxShadowProperty, value);
}
/// <summary>
/// Renders the control.
/// </summary>
/// <param name="context">The drawing context.</param>
public override void Render(DrawingContext context)
{
_borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush);
_borderRenderHelper.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush,
BoxShadow);
}
/// <summary>
@ -110,8 +127,6 @@ namespace Avalonia.Controls
/// <returns>The space taken.</returns>
protected override Size ArrangeOverride(Size finalSize)
{
_borderRenderHelper.Update(finalSize, BorderThickness, CornerRadius);
return LayoutHelper.ArrangeChild(Child, finalSize, Padding, BorderThickness);
}
}

2
src/Avalonia.Controls/ButtonSpinner.cs

@ -121,7 +121,7 @@ namespace Avalonia.Controls
}
/// <inheritdoc />
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
IncreaseButton = e.NameScope.Find<Button>("PART_IncreaseButton");
DecreaseButton = e.NameScope.Find<Button>("PART_DecreaseButton");

4
src/Avalonia.Controls/Calendar/Calendar.cs

@ -2079,10 +2079,8 @@ namespace Avalonia.Controls
/// <see cref="T:System.Windows.Controls.Calendar" /> when a new
/// template is applied.
/// </summary>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
Root = e.NameScope.Find<Panel>(PART_ElementRoot);
SelectedMonth = DisplayDate;

3
src/Avalonia.Controls/Calendar/CalendarButton.cs

@ -98,9 +98,8 @@ namespace Avalonia.Controls.Primitives
/// <see cref="T:System.Windows.Controls.Primitives.CalendarButton" />
/// when a new template is applied.
/// </summary>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
SetPseudoClasses();
}

4
src/Avalonia.Controls/Calendar/CalendarDayButton.cs

@ -150,11 +150,11 @@ namespace Avalonia.Controls.Primitives
}
}
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
SetPseudoClasses();
}
private void SetPseudoClasses()
{
if (_ignoringMouseOverState)

4
src/Avalonia.Controls/Calendar/CalendarItem.cs

@ -268,10 +268,8 @@ namespace Avalonia.Controls.Primitives
/// <see cref="T:System.Windows.Controls.Primitives.CalendarItem" />
/// when a new template is applied.
/// </summary>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
HeaderButton = e.NameScope.Find<Button>(PART_ElementHeaderButton);
PreviousButton = e.NameScope.Find<Button>(PART_ElementPreviousButton);
NextButton = e.NameScope.Find<Button>(PART_ElementNextButton);

4
src/Avalonia.Controls/Calendar/DatePicker.cs

@ -413,7 +413,7 @@ namespace Avalonia.Controls
DisplayDate = DateTime.Today;
}
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (_calendar != null)
{
@ -508,8 +508,6 @@ namespace Avalonia.Controls
SetSelectedDate();
}
}
base.OnTemplateApplied(e);
}
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)

4
src/Avalonia.Controls/ComboBox.cs

@ -219,7 +219,7 @@ namespace Avalonia.Controls
}
/// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (_popup != null)
{
@ -230,8 +230,6 @@ namespace Avalonia.Controls
_popup = e.NameScope.Get<Popup>("PART_Popup");
_popup.Opened += PopupOpened;
_popup.Closed += PopupClosed;
base.OnTemplateApplied(e);
}
/// <summary>

39
src/Avalonia.Controls/ContextMenu.cs

@ -1,4 +1,5 @@
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using Avalonia.Controls.Generators;
@ -9,18 +10,19 @@ using Avalonia.Input;
using Avalonia.Interactivity;
using Avalonia.Layout;
using Avalonia.LogicalTree;
using Avalonia.Styling;
namespace Avalonia.Controls
{
/// <summary>
/// A control context menu.
/// </summary>
public class ContextMenu : MenuBase
public class ContextMenu : MenuBase, ISetterValue
{
private static readonly ITemplate<IPanel> DefaultPanel =
new FuncTemplate<IPanel>(() => new StackPanel { Orientation = Orientation.Vertical });
private Popup _popup;
private Control _attachedControl;
private List<Control> _attachedControls;
private IInputElement _previousFocus;
/// <summary>
@ -74,13 +76,14 @@ namespace Avalonia.Controls
if (e.OldValue is ContextMenu oldMenu)
{
control.PointerReleased -= ControlPointerReleased;
oldMenu._attachedControl = null;
oldMenu._attachedControls?.Remove(control);
((ISetLogicalParent)oldMenu._popup)?.SetParent(null);
}
if (e.NewValue is ContextMenu newMenu)
{
newMenu._attachedControl = control;
newMenu._attachedControls ??= new List<Control>();
newMenu._attachedControls.Add(control);
control.PointerReleased += ControlPointerReleased;
}
}
@ -96,18 +99,22 @@ namespace Avalonia.Controls
/// <param name="control">The control.</param>
public void Open(Control control)
{
if (control is null && _attachedControl is null)
if (control is null && (_attachedControls is null || _attachedControls.Count == 0))
{
throw new ArgumentNullException(nameof(control));
}
if (control is object && _attachedControl is object && control != _attachedControl)
if (control is object &&
_attachedControls is object &&
!_attachedControls.Contains(control))
{
throw new ArgumentException(
"Cannot show ContentMenu on a different control to the one it is attached to.",
nameof(control));
}
control ??= _attachedControls[0];
if (IsOpen)
{
return;
@ -126,7 +133,12 @@ namespace Avalonia.Controls
_popup.Closed += PopupClosed;
}
((ISetLogicalParent)_popup).SetParent(control);
if (_popup.Parent != control)
{
((ISetLogicalParent)_popup).SetParent(null);
((ISetLogicalParent)_popup).SetParent(control);
}
_popup.Child = this;
_popup.IsOpen = true;
@ -155,6 +167,17 @@ namespace Avalonia.Controls
}
}
void ISetterValue.Initialize(ISetter setter)
{
// ContextMenu can be assigned to the ContextMenu property in a setter. This overrides
// the behavior defined in Control which requires controls to be wrapped in a <template>.
if (!(setter is Setter s && s.Property == ContextMenuProperty))
{
throw new InvalidOperationException(
"Cannot use a control as a Setter value. Wrap the control in a <Template>.");
}
}
protected override IItemContainerGenerator CreateItemContainerGenerator()
{
return new MenuItemContainerGenerator(this);
@ -179,7 +202,7 @@ namespace Avalonia.Controls
SelectedIndex = -1;
IsOpen = false;
if (_attachedControl is null)
if (_attachedControls is null || _attachedControls.Count == 0)
{
((ISetLogicalParent)_popup).SetParent(null);
}

3
src/Avalonia.Controls/ListBox.cs

@ -161,10 +161,9 @@ namespace Avalonia.Controls
}
}
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
Scroll = e.NameScope.Find<IScrollable>("PART_ScrollViewer");
base.OnTemplateApplied(e);
}
}
}

4
src/Avalonia.Controls/MenuItem.cs

@ -417,10 +417,8 @@ namespace Avalonia.Controls
}
/// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
if (_popup != null)
{
_popup.Opened -= PopupOpened;

4
src/Avalonia.Controls/Notifications/WindowNotificationManager.cs

@ -77,10 +77,8 @@ namespace Avalonia.Controls.Notifications
}
/// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
var itemsControl = e.NameScope.Find<Panel>("PART_Items");
_items = itemsControl?.Children;
}

2
src/Avalonia.Controls/NumericUpDown/NumericUpDown.cs

@ -295,7 +295,7 @@ namespace Avalonia.Controls
}
/// <inheritdoc />
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (TextBox != null)
{

21
src/Avalonia.Controls/Presenters/ContentPresenter.cs

@ -40,7 +40,12 @@ namespace Avalonia.Controls.Presenters
public static readonly StyledProperty<CornerRadius> CornerRadiusProperty =
Border.CornerRadiusProperty.AddOwner<ContentPresenter>();
/// <summary>
/// Defines the <see cref="BoxShadow"/> property.
/// </summary>
public static readonly StyledProperty<BoxShadows> BoxShadowProperty =
Border.BoxShadowProperty.AddOwner<ContentPresenter>();
/// <summary>
/// Defines the <see cref="Child"/> property.
/// </summary>
@ -132,6 +137,15 @@ namespace Avalonia.Controls.Presenters
set { SetValue(CornerRadiusProperty, value); }
}
/// <summary>
/// Gets or sets the box shadow effect parameters
/// </summary>
public BoxShadows BoxShadow
{
get => GetValue(BoxShadowProperty);
set => SetValue(BoxShadowProperty, value);
}
/// <summary>
/// Gets the control displayed by the presenter.
/// </summary>
@ -274,7 +288,8 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/>
public override void Render(DrawingContext context)
{
_borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush);
_borderRenderer.Render(context, Bounds.Size, BorderThickness, CornerRadius, Background, BorderBrush,
BoxShadow);
}
/// <summary>
@ -321,8 +336,6 @@ namespace Avalonia.Controls.Presenters
/// <inheritdoc/>
protected override Size ArrangeOverride(Size finalSize)
{
_borderRenderer.Update(finalSize, BorderThickness, CornerRadius);
return ArrangeOverrideImpl(finalSize, new Vector());
}

4
src/Avalonia.Controls/Primitives/ScrollBar.cs

@ -143,10 +143,8 @@ namespace Avalonia.Controls.Primitives
}
}
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
if (_lineUpButton != null)
{
_lineUpButton.Click -= LineUpClick;

8
src/Avalonia.Controls/Primitives/SelectingItemsControl.cs

@ -455,13 +455,13 @@ namespace Avalonia.Controls.Primitives
InternalEndInit();
}
protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
base.OnPropertyChanged(change);
if (property == SelectionModeProperty)
if (change.Property == SelectionModeProperty)
{
var mode = newValue.GetValueOrDefault<SelectionMode>();
var mode = change.NewValue.GetValueOrDefault<SelectionMode>();
Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple);
Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected);
}

11
src/Avalonia.Controls/Primitives/TemplatedControl.cs

@ -263,7 +263,10 @@ namespace Avalonia.Controls.Primitives
if (nameScope == null)
nameScope = new NameScope();
OnTemplateApplied(new TemplateAppliedEventArgs(nameScope));
var e = new TemplateAppliedEventArgs(nameScope);
OnApplyTemplate(e);
OnTemplateApplied(e);
RaiseEvent(e);
}
_appliedTemplate = template;
@ -306,13 +309,17 @@ namespace Avalonia.Controls.Primitives
base.OnDetachedFromLogicalTree(e);
}
protected virtual void OnApplyTemplate(TemplateAppliedEventArgs e)
{
}
/// <summary>
/// Called when the control's template is applied.
/// </summary>
/// <param name="e">The event args.</param>
[Obsolete("Use OnApplyTemplate")]
protected virtual void OnTemplateApplied(TemplateAppliedEventArgs e)
{
RaiseEvent(e);
}
/// <summary>

2
src/Avalonia.Controls/ProgressBar.cs

@ -98,7 +98,7 @@ namespace Avalonia.Controls
}
/// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_indicator = e.NameScope.Get<Border>("PART_Indicator");

4
src/Avalonia.Controls/Remote/Server/RemoteServerTopLevelImpl.cs

@ -115,7 +115,7 @@ namespace Avalonia.Controls.Remote.Server
{
lock (_lock)
{
_lastReceivedFrame = lastFrame.SequenceId;
_lastReceivedFrame = Math.Max(lastFrame.SequenceId, _lastReceivedFrame);
}
Dispatcher.UIThread.Post(RenderIfNeeded);
}
@ -298,6 +298,8 @@ namespace Avalonia.Controls.Remote.Server
Width = width,
Height = height,
Stride = width * bpp,
DpiX = _dpi.X,
DpiY = _dpi.Y
};
}

28
src/Avalonia.Controls/Repeater/ItemsRepeater.cs

@ -10,6 +10,7 @@ using Avalonia.Controls.Templates;
using Avalonia.Data;
using Avalonia.Input;
using Avalonia.Layout;
using Avalonia.VisualTree;
namespace Avalonia.Controls
{
@ -379,14 +380,19 @@ namespace Avalonia.Controls
{
if (change.Property == ItemsProperty)
{
var oldEnumerable = change.OldValue.GetValueOrDefault<IEnumerable>();
var newEnumerable = change.NewValue.GetValueOrDefault<IEnumerable>();
var newDataSource = newEnumerable as ItemsSourceView;
if (newEnumerable != null && newDataSource == null)
if (oldEnumerable != newEnumerable)
{
newDataSource = new ItemsSourceView(newEnumerable);
}
var newDataSource = newEnumerable as ItemsSourceView;
if (newEnumerable != null && newDataSource == null)
{
newDataSource = new ItemsSourceView(newEnumerable);
}
OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
OnDataSourcePropertyChanged(ItemsSourceView, newDataSource);
}
}
else if (change.Property == ItemTemplateProperty)
{
@ -435,8 +441,16 @@ namespace Avalonia.Controls
private int GetElementIndexImpl(IControl element)
{
var virtInfo = TryGetVirtualizationInfo(element);
return _viewManager.GetElementIndex(virtInfo);
// Verify that element is actually a child of this ItemsRepeater
var parent = element.GetVisualParent();
if (parent == this)
{
var virtInfo = TryGetVirtualizationInfo(element);
return _viewManager.GetElementIndex(virtInfo);
}
return -1;
}
private IControl GetElementFromIndexImpl(int index)

36
src/Avalonia.Controls/Repeater/ViewManager.cs

@ -388,19 +388,24 @@ namespace Avalonia.Controls
}
case NotifyCollectionChangedAction.Reset:
if (_owner.ItemsSourceView.HasKeyIndexMapping)
// If we get multiple resets back to back before
// running layout, we dont have to clear all the elements again.
if (!_isDataSourceStableResetPending)
{
_isDataSourceStableResetPending = true;
}
if (_owner.ItemsSourceView.HasKeyIndexMapping)
{
_isDataSourceStableResetPending = true;
}
// Walk through all the elements and make sure they are cleared, they will go into
// the stable id reset pool.
foreach (var element in _owner.Children)
{
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate)
// Walk through all the elements and make sure they are cleared, they will go into
// the stable id reset pool.
foreach (var element in _owner.Children)
{
_owner.ClearElementImpl(element);
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
if (virtInfo.IsRealized && virtInfo.AutoRecycleCandidate)
{
_owner.ClearElementImpl(element);
}
}
}
@ -441,6 +446,9 @@ namespace Avalonia.Controls
}
_resetPool.Clear();
// Flush the realized indices once the stable reset pool is cleared to start fresh.
InvalidateRealizedIndicesHeldByLayout();
}
}
@ -498,6 +506,10 @@ namespace Avalonia.Controls
var virtInfo = ItemsRepeater.GetVirtualizationInfo(element);
virtInfo.MoveOwnershipToLayoutFromUniqueIdResetPool();
UpdateElementIndex(element, virtInfo, index);
// Update realized indices
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
}
}
@ -519,6 +531,10 @@ namespace Avalonia.Controls
_pinnedPool.RemoveAt(i);
element = elementInfo.PinnedElement;
elementInfo.VirtualizationInfo.MoveOwnershipToLayoutFromPinnedPool();
// Update realized indices
_firstRealizedElementIndexHeldByLayout = Math.Min(_firstRealizedElementIndexHeldByLayout, index);
_lastRealizedElementIndexHeldByLayout = Math.Max(_lastRealizedElementIndexHeldByLayout, index);
break;
}
}

42
src/Avalonia.Controls/SelectionModel.cs

@ -20,6 +20,7 @@ namespace Avalonia.Controls
private bool _singleSelect;
private bool _autoSelect;
private int _operationCount;
private IndexPath _oldAnchorIndex;
private IReadOnlyList<IndexPath>? _selectedIndicesCached;
private IReadOnlyList<object?>? _selectedItemsCached;
private SelectionModelChildrenRequestedEventArgs? _childrenRequestedEventArgs;
@ -142,6 +143,8 @@ namespace Avalonia.Controls
}
set
{
var oldValue = AnchorIndex;
if (value != null)
{
SelectionTreeHelper.TraverseIndexPath(
@ -155,7 +158,10 @@ namespace Avalonia.Controls
_rootNode.AnchorIndex = -1;
}
RaisePropertyChanged("AnchorIndex");
if (_operationCount == 0 && oldValue != AnchorIndex)
{
RaisePropertyChanged("AnchorIndex");
}
}
}
@ -633,19 +639,18 @@ namespace Avalonia.Controls
_selectedIndicesCached = null;
_selectedItemsCached = null;
// Raise SelectionChanged event
if (e != null)
{
SelectionChanged?.Invoke(this, e);
}
RaisePropertyChanged(nameof(SelectedIndex));
RaisePropertyChanged(nameof(SelectedIndices));
RaisePropertyChanged(nameof(SelectedIndex));
RaisePropertyChanged(nameof(SelectedIndices));
if (_rootNode.Source != null)
{
RaisePropertyChanged(nameof(SelectedItem));
RaisePropertyChanged(nameof(SelectedItems));
if (_rootNode.Source != null)
{
RaisePropertyChanged(nameof(SelectedItem));
RaisePropertyChanged(nameof(SelectedItems));
}
}
}
@ -774,7 +779,10 @@ namespace Avalonia.Controls
winrtEnd,
info =>
{
info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select);
if (info.Path >= winrtStart && info.Path <= winrtEnd)
{
info.ParentNode!.Select(info.Path.GetAt(info.Path.GetSize() - 1), select);
}
});
}
@ -782,6 +790,7 @@ namespace Avalonia.Controls
{
if (_operationCount++ == 0)
{
_oldAnchorIndex = AnchorIndex;
_rootNode.BeginOperation();
}
}
@ -805,10 +814,17 @@ namespace Avalonia.Controls
var changeSet = new SelectionModelChangeSet(changes);
e = changeSet.CreateEventArgs();
}
}
OnSelectionChanged(e);
_rootNode.Cleanup();
OnSelectionChanged(e);
if (_oldAnchorIndex != AnchorIndex)
{
RaisePropertyChanged(nameof(AnchorIndex));
}
_rootNode.Cleanup();
_oldAnchorIndex = default;
}
}
private void ApplyAutoSelect()

134
src/Avalonia.Controls/SelectionNode.cs

@ -8,6 +8,7 @@ using System.Collections;
using System.Collections.Generic;
using System.Collections.Specialized;
using System.Linq;
using Avalonia.Controls.Utils;
#nullable enable
@ -214,7 +215,21 @@ namespace Avalonia.Controls
public void SetChildrenObservable(IObservable<object?> resolver)
{
_childrenSubscription = resolver.Subscribe(x => Source = x);
_childrenSubscription = resolver.Subscribe(x =>
{
if (Source != null)
{
using (_manager.Update())
{
SelectionTreeHelper.Traverse(
this,
realizeChildren: false,
info => info.Node.Clear());
}
}
Source = x;
});
}
public int SelectedCount { get; private set; }
@ -544,11 +559,14 @@ namespace Avalonia.Controls
private void ClearChildNodes()
{
foreach (var child in _childrenNodes)
for (int i = 0; i < _childrenNodes.Count; i++)
{
var child = _childrenNodes[i];
if (child != null && child != _manager.SharedLeafNode)
{
child.Dispose();
_childrenNodes[i] = null;
}
}
@ -713,101 +731,67 @@ namespace Avalonia.Controls
var selectionInvalidated = false;
var removed = new List<object?>();
var count = items.Count;
// Remove the items from the selection for leaf
if (ItemsSourceView!.Count > 0)
{
bool isSelected = false;
var isSelected = false;
for (int i = 0; i <= count - 1; i++)
for (int i = 0; i <= count - 1; i++)
{
if (IsSelected(index + i))
{
if (IsSelected(index + i))
{
isSelected = true;
removed.Add(items[i]);
}
isSelected = true;
removed.Add(items[i]);
}
}
if (isSelected)
{
var removeRange = new IndexRange(index, index + count - 1);
SelectedCount -= IndexRange.Remove(_selected, removeRange);
selectionInvalidated = true;
if (_selectedItems != null)
{
foreach (var i in items)
{
_selectedItems.Remove(i);
}
}
}
if (isSelected)
{
var removeRange = new IndexRange(index, index + count - 1);
SelectedCount -= IndexRange.Remove(_selected, removeRange);
selectionInvalidated = true;
for (int i = 0; i < _selected.Count; i++)
if (_selectedItems != null)
{
var range = _selected[i];
// The range is after the removed items, need to shift the range left
if (range.End > index)
foreach (var i in items)
{
// Shift the range to the left
_selected[i] = new IndexRange(range.Begin - count, range.End - count);
selectionInvalidated = true;
_selectedItems.Remove(i);
}
}
}
// Update for non-leaf if we are tracking non-leaf nodes
if (_childrenNodes.Count > 0)
{
selectionInvalidated = true;
for (int i = 0; i < count; i++)
{
if (_childrenNodes[index] != null)
{
removed.AddRange(_childrenNodes[index]!.SelectedItems);
RealizedChildrenNodeCount--;
_childrenNodes[index]!.Dispose();
}
_childrenNodes.RemoveAt(index);
}
}
for (int i = 0; i < _selected.Count; i++)
{
var range = _selected[i];
//Adjust the anchor
if (AnchorIndex >= index)
// The range is after the removed items, need to shift the range left
if (range.End > index)
{
AnchorIndex -= count;
// Shift the range to the left
_selected[i] = new IndexRange(range.Begin - count, range.End - count);
selectionInvalidated = true;
}
}
else
{
// No more items in the list, clear
ClearSelection();
RealizedChildrenNodeCount = 0;
selectionInvalidated = true;
}
// Check if removing a node invalidated an ancestors
// selection state. For example if parent was partially selected before
// removing an item, it could be selected now.
if (!selectionInvalidated)
// Update for non-leaf if we are tracking non-leaf nodes
if (_childrenNodes.Count > 0)
{
var parent = _parent;
while (parent != null)
selectionInvalidated = true;
for (int i = 0; i < count; i++)
{
var isSelected = parent.IsSelectedWithPartial();
// If a parent is partially selected, then it will become selected.
// If it is selected or not selected - there is no change.
if (!isSelected.HasValue)
if (_childrenNodes[index] != null)
{
selectionInvalidated = true;
break;
removed.AddRange(_childrenNodes[index]!.SelectedItems);
RealizedChildrenNodeCount--;
_childrenNodes[index]!.Dispose();
}
parent = parent._parent;
_childrenNodes.RemoveAt(index);
}
}
//Adjust the anchor
if (AnchorIndex >= index)
{
AnchorIndex -= count;
}
return (selectionInvalidated, removed);
}

2
src/Avalonia.Controls/Slider.cs

@ -82,7 +82,7 @@ namespace Avalonia.Controls
}
/// <inheritdoc/>
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
if (_decreaseButton != null)
{

4
src/Avalonia.Controls/TabControl.cs

@ -217,10 +217,8 @@ namespace Avalonia.Controls
return new TabItemContainerGenerator(this);
}
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
ItemsPresenterPart = e.NameScope.Get<ItemsPresenter>("PART_ItemsPresenter");
}

2
src/Avalonia.Controls/TextBox.cs

@ -341,7 +341,7 @@ namespace Avalonia.Controls
set { SetAndRaise(NewLineProperty, ref _newLine, value); }
}
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
_presenter = e.NameScope.Get<TextPresenter>("PART_TextPresenter");

18
src/Avalonia.Controls/TreeView.cs

@ -4,6 +4,7 @@ using System.Collections;
using System.Collections.Generic;
using System.ComponentModel;
using System.Linq;
using System.Reactive.Linq;
using Avalonia.Controls.Generators;
using Avalonia.Controls.Primitives;
using Avalonia.Controls.Utils;
@ -395,7 +396,14 @@ namespace Avalonia.Controls
private void OnSelectionModelChildrenRequested(object sender, SelectionModelChildrenRequestedEventArgs e)
{
var container = ItemContainerGenerator.Index.ContainerFromItem(e.Source) as ItemsControl;
e.Children = container.GetObservable(ItemsProperty);
if (container is object)
{
e.Children = Observable.CombineLatest(
container.GetObservable(TreeViewItem.IsExpandedProperty),
container.GetObservable(ItemsProperty),
(expanded, items) => expanded ? items : null);
}
}
private TreeViewItem GetContainerInDirection(
@ -478,13 +486,13 @@ namespace Avalonia.Controls
}
}
protected override void OnPropertyChanged<T>(AvaloniaProperty<T> property, Optional<T> oldValue, BindingValue<T> newValue, BindingPriority priority)
protected override void OnPropertyChanged<T>(AvaloniaPropertyChangedEventArgs<T> change)
{
base.OnPropertyChanged(property, oldValue, newValue, priority);
base.OnPropertyChanged(change);
if (property == SelectionModeProperty)
if (change.Property == SelectionModeProperty)
{
var mode = newValue.GetValueOrDefault<SelectionMode>();
var mode = change.NewValue.GetValueOrDefault<SelectionMode>();
Selection.SingleSelect = !mode.HasFlagCustom(SelectionMode.Multiple);
Selection.AutoSelect = mode.HasFlagCustom(SelectionMode.AlwaysSelected);
}

3
src/Avalonia.Controls/TreeViewItem.cs

@ -162,9 +162,8 @@ namespace Avalonia.Controls
// Don't call base.OnKeyDown - let events bubble up to containing TreeView.
}
protected override void OnTemplateApplied(TemplateAppliedEventArgs e)
protected override void OnApplyTemplate(TemplateAppliedEventArgs e)
{
base.OnTemplateApplied(e);
_header = e.NameScope.Find<IControl>("PART_Header");
}

46
src/Avalonia.Controls/Utils/BorderRenderHelper.cs

@ -1,17 +1,30 @@
using System;
using Avalonia.Media;
using Avalonia.Platform;
namespace Avalonia.Controls.Utils
{
internal class BorderRenderHelper
{
private bool _useComplexRendering;
private bool? _backendSupportsIndividualCorners;
private StreamGeometry _backgroundGeometryCache;
private StreamGeometry _borderGeometryCache;
private Size _size;
private Thickness _borderThickness;
private CornerRadius _cornerRadius;
private bool _initialized;
public void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius)
void Update(Size finalSize, Thickness borderThickness, CornerRadius cornerRadius)
{
if (borderThickness.IsUniform && cornerRadius.IsUniform)
_backendSupportsIndividualCorners ??= AvaloniaLocator.Current.GetService<IPlatformRenderInterface>()
.SupportsIndividualRoundRects;
_size = finalSize;
_borderThickness = borderThickness;
_cornerRadius = cornerRadius;
_initialized = true;
if (borderThickness.IsUniform && (cornerRadius.IsUniform || _backendSupportsIndividualCorners == true))
{
_backgroundGeometryCache = null;
_borderGeometryCache = null;
@ -67,7 +80,19 @@ namespace Avalonia.Controls.Utils
}
}
public void Render(DrawingContext context, Size size, Thickness borders, CornerRadius radii, IBrush background, IBrush borderBrush)
public void Render(DrawingContext context,
Size finalSize, Thickness borderThickness, CornerRadius cornerRadius,
IBrush background, IBrush borderBrush, BoxShadows boxShadows)
{
if (_size != finalSize
|| _borderThickness != borderThickness
|| _cornerRadius != cornerRadius
|| !_initialized)
Update(finalSize, borderThickness, cornerRadius);
RenderCore(context, background, borderBrush, boxShadows);
}
void RenderCore(DrawingContext context, IBrush background, IBrush borderBrush, BoxShadows boxShadows)
{
if (_useComplexRendering)
{
@ -85,9 +110,7 @@ namespace Avalonia.Controls.Utils
}
else
{
var borderThickness = borders.Top;
var top = borderThickness * 0.5;
var borderThickness = _borderThickness.Top;
IPen pen = null;
if (borderThickness > 0)
@ -95,11 +118,16 @@ namespace Avalonia.Controls.Utils
pen = new Pen(borderBrush, borderThickness);
}
var rect = new Rect(top, top, size.Width - borderThickness, size.Height - borderThickness);
var rrect = new RoundedRect(new Rect(_size), _cornerRadius.TopLeft, _cornerRadius.TopRight,
_cornerRadius.BottomRight, _cornerRadius.BottomLeft);
if (Math.Abs(borderThickness) > double.Epsilon)
{
rrect = rrect.Deflate(borderThickness * 0.5, borderThickness * 0.5);
}
context.DrawRectangle(background, pen, rect, radii.TopLeft, radii.TopLeft);
context.PlatformImpl.DrawRectangle(background, pen, rrect, boxShadows);
}
}
}
private static void CreateGeometry(StreamGeometryContext context, Rect boundRect, BorderGeometryKeypoints keypoints)
{

3
src/Avalonia.DesignerSupport/Avalonia.DesignerSupport.csproj

@ -8,8 +8,11 @@
-->
<Version>0.7.0</Version>
<NoWarn>CS1591</NoWarn>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
<ItemGroup>
<EmbeddedResource Include="Remote\HtmlTransport\webapp\build\**\*.gz" />
<EmbeddedResource Condition="'$(Configuration)' == 'Debug'" Remove="Remote\HtmlTransport\webapp\build\**\*.map.gz"/>
<ProjectReference Include="..\Markup\Avalonia.Markup.Xaml\Avalonia.Markup.Xaml.csproj" />
<ProjectReference Include="..\Markup\Avalonia.Markup\Avalonia.Markup.csproj" />
<ProjectReference Include="..\Avalonia.Animation\Avalonia.Animation.csproj" />

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

@ -35,5 +35,7 @@ namespace Avalonia.DesignerSupport.Remote
add {}
remove {}
}
public void Start() => _inner?.Start();
}
}
}

90
src/Avalonia.DesignerSupport/Remote/FileWatcherTransport.cs

@ -0,0 +1,90 @@
using System;
using System.IO;
using System.Threading.Tasks;
using Avalonia.Remote.Protocol;
using Avalonia.Remote.Protocol.Designer;
using Avalonia.Threading;
namespace Avalonia.DesignerSupport.Remote
{
class FileWatcherTransport : IAvaloniaRemoteTransportConnection, ITransportWithEnforcedMethod
{
private string _path;
private string _lastContents;
private bool _disposed;
public FileWatcherTransport(Uri file)
{
_path = file.LocalPath;
}
public void Dispose()
{
_disposed = true;
}
void Dump(object o, string pad)
{
foreach (var p in o.GetType().GetProperties())
{
Console.Write($"{pad}{p.Name}: ");
var v = p.GetValue(o);
if (v == null || v.GetType().IsPrimitive || v is string || v is Guid)
Console.WriteLine(v);
else
{
Console.WriteLine();
Dump(v, pad + " ");
}
}
}
public Task Send(object data)
{
Console.WriteLine(data.GetType().Name);
Dump(data, " ");
return Task.CompletedTask;
}
private Action<IAvaloniaRemoteTransportConnection, object> _onMessage;
public event Action<IAvaloniaRemoteTransportConnection, object> OnMessage
{
add
{
_onMessage+=value;
}
remove { _onMessage -= value; }
}
public event Action<IAvaloniaRemoteTransportConnection, Exception> OnException;
public void Start()
{
UpdaterThread();
}
// I couldn't get FileSystemWatcher working on Linux, so I came up with this abomination
async void UpdaterThread()
{
while (!_disposed)
{
var data = File.ReadAllText(_path);
if (data != _lastContents)
{
Console.WriteLine("Triggering XAML update");
_lastContents = data;
_onMessage?.Invoke(this, new UpdateXamlMessage { Xaml = data });
}
await Task.Delay(100);
}
}
public string PreviewerMethod => RemoteDesignerEntryPoint.Methods.Html;
}
interface ITransportWithEnforcedMethod
{
string PreviewerMethod { get; }
}
}

266
src/Avalonia.DesignerSupport/Remote/HtmlTransport/HtmlTransport.cs

@ -0,0 +1,266 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.IO.Compression;
using System.Linq;
using System.Net;
using System.Text;
using System.Threading;
using System.Threading.Tasks;
using Avalonia.Remote.Protocol;
using Avalonia.Remote.Protocol.Viewport;
namespace Avalonia.DesignerSupport.Remote.HtmlTransport
{
public class HtmlWebSocketTransport : IAvaloniaRemoteTransportConnection
{
private readonly IAvaloniaRemoteTransportConnection _signalTransport;
private readonly SimpleWebSocketHttpServer _simpleServer;
private readonly Dictionary<string, byte[]> _resources;
private SimpleWebSocket _pendingSocket;
private bool _disposed;
private object _lock = new object();
private AutoResetEvent _wakeup = new AutoResetEvent(false);
private FrameMessage _lastFrameMessage = null;
private FrameMessage _lastSentFrameMessage = null;
private RequestViewportResizeMessage _lastViewportRequest;
private Action<IAvaloniaRemoteTransportConnection, object> _onMessage;
private Action<IAvaloniaRemoteTransportConnection, Exception> _onException;
private static readonly Dictionary<string, string> Mime = new Dictionary<string, string>
{
["html"] = "text/html", ["htm"] = "text/html", ["js"] = "text/javascript", ["css"] = "text/css"
};
private static readonly byte[] NotFound = Encoding.UTF8.GetBytes("404 - Not Found");
public HtmlWebSocketTransport(IAvaloniaRemoteTransportConnection signalTransport, Uri listenUri)
{
if (listenUri.Scheme != "http")
throw new ArgumentException("listenUri");
var resourcePrefix = "Avalonia.DesignerSupport.Remote.HtmlTransport.webapp.build.";
_resources = typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceNames()
.Where(r => r.StartsWith(resourcePrefix) && r.EndsWith(".gz")).ToDictionary(
r => r.Substring(resourcePrefix.Length).Substring(0,r.Length-resourcePrefix.Length-3),
r =>
{
using (var s =
new GZipStream(typeof(HtmlWebSocketTransport).Assembly.GetManifestResourceStream(r),
CompressionMode.Decompress))
{
var ms = new MemoryStream();
s.CopyTo(ms);
return ms.ToArray();
}
});
_signalTransport = signalTransport;
var address = IPAddress.Parse(listenUri.Host);
_simpleServer = new SimpleWebSocketHttpServer(address, listenUri.Port);
_simpleServer.Listen();
Task.Run(AcceptWorker);
Task.Run(SocketWorker);
_signalTransport.Send(new HtmlTransportStartedMessage { Uri = "http://" + address + ":" + listenUri.Port + "/" });
}
async void AcceptWorker()
{
while (true)
{
using (var req = await _simpleServer.AcceptAsync())
{
if (!req.IsWebsocketRequest)
{
var key = req.Path == "/" ? "index.html" : req.Path.TrimStart('/').Replace('/', '.');
if (_resources.TryGetValue(key, out var data))
{
var ext = Path.GetExtension(key).Substring(1);
string mime = null;
if (ext == null || !Mime.TryGetValue(ext, out mime))
mime = "application/octet-stream";
await req.RespondAsync(200, data, mime);
}
else
{
await req.RespondAsync(404, NotFound, "text/plain");
}
}
else
{
var socket = await req.AcceptWebSocket();
SocketReceiveWorker(socket);
lock (_lock)
{
_pendingSocket?.Dispose();
_pendingSocket = socket;
}
}
}
}
}
async void SocketReceiveWorker(SimpleWebSocket socket)
{
try
{
while (true)
{
var msg = await socket.ReceiveMessage().ConfigureAwait(false);
if(msg == null)
return;
if (msg.IsText)
{
var s = Encoding.UTF8.GetString(msg.Data);
var parts = s.Split(':');
if (parts[0] == "frame-received")
_onMessage?.Invoke(this, new FrameReceivedMessage { SequenceId = long.Parse(parts[1]) });
}
}
}
catch(Exception e)
{
Console.Error.WriteLine(e.ToString());
}
}
async void SocketWorker()
{
try
{
SimpleWebSocket socket = null;
while (true)
{
if (_disposed)
{
socket?.Dispose();
return;
}
FrameMessage sendNow = null;
lock (_lock)
{
if (_pendingSocket != null)
{
socket?.Dispose();
socket = _pendingSocket;
_pendingSocket = null;
_lastSentFrameMessage = null;
}
if (_lastFrameMessage != _lastSentFrameMessage)
_lastSentFrameMessage = sendNow = _lastFrameMessage;
}
if (sendNow != null && socket != null)
{
await socket.SendMessage(
$"frame:{sendNow.SequenceId}:{sendNow.Width}:{sendNow.Height}:{sendNow.Stride}:{sendNow.DpiX}:{sendNow.DpiY}");
await socket.SendMessage(false, sendNow.Data);
}
_wakeup.WaitOne(TimeSpan.FromSeconds(1));
}
}
catch(Exception e)
{
Console.Error.WriteLine(e.ToString());
}
}
public void Dispose()
{
_pendingSocket?.Dispose();
_simpleServer.Dispose();
}
public Task Send(object data)
{
if (data is FrameMessage frame)
{
_lastFrameMessage = frame;
_wakeup.Set();
return Task.CompletedTask;
}
if (data is RequestViewportResizeMessage req)
{
return Task.CompletedTask;
}
return _signalTransport.Send(data);
}
public void Start()
{
_onMessage?.Invoke(this, new Avalonia.Remote.Protocol.Viewport.ClientSupportedPixelFormatsMessage
{
Formats = new []{PixelFormat.Rgba8888}
});
_signalTransport.Start();
}
#region Forward
public event Action<IAvaloniaRemoteTransportConnection, object> OnMessage
{
add
{
bool subscribeToInner;
lock (_lock)
{
subscribeToInner = _onMessage == null;
_onMessage += value;
}
if (subscribeToInner)
_signalTransport.OnMessage += OnSignalTransportMessage;
}
remove
{
lock (_lock)
{
_onMessage -= value;
if (_onMessage == null)
_signalTransport.OnMessage -= OnSignalTransportMessage;
}
}
}
private void OnSignalTransportMessage(IAvaloniaRemoteTransportConnection signal, object message) => _onMessage?.Invoke(this, message);
public event Action<IAvaloniaRemoteTransportConnection, Exception> OnException
{
add
{
lock (_lock)
{
var subscribeToInner = _onException == null;
_onException += value;
if (subscribeToInner)
_signalTransport.OnException += OnSignalTransportException;
}
}
remove
{
lock (_lock)
{
_onException -= value;
if(_onException==null)
_signalTransport.OnException -= OnSignalTransportException;
}
}
}
private void OnSignalTransportException(IAvaloniaRemoteTransportConnection arg1, Exception ex)
{
_onException?.Invoke(this, ex);
}
#endregion
}
}

472
src/Avalonia.DesignerSupport/Remote/HtmlTransport/SimpleWebSocketHttpServer.cs

@ -0,0 +1,472 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Linq;
using System.Net;
using System.Net.Sockets;
using System.Runtime.InteropServices;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
namespace Avalonia.DesignerSupport.Remote.HtmlTransport
{
public class SimpleWebSocketHttpServer : IDisposable
{
private readonly IPAddress _address;
private readonly int _port;
private TcpListener _listener;
public async Task<SimpleWebSocketHttpRequest> AcceptAsync()
{
while (true)
{
var res = await AcceptAsyncImpl();
if (res != null)
return res;
}
}
async Task<SimpleWebSocketHttpRequest> AcceptAsyncImpl()
{
if (_listener == null)
throw new InvalidOperationException("Currently not listening");
var socket = await _listener.AcceptSocketAsync();
var stream = new NetworkStream(socket);
bool error = true;
async Task<string> ReadLineAsync()
{
var readBuffer = new byte[1];
var lineBuffer = new byte[1024];
for (var c = 0; c < 1024; c++)
{
if (await stream.ReadAsync(readBuffer, 0, 1) == 0)
throw new EndOfStreamException();
if (readBuffer[0] == 10)
{
if (c == 0)
return "";
if (lineBuffer[c - 1] == 13)
c--;
if (c == 0)
return "";
return Encoding.UTF8.GetString(lineBuffer, 0, c);
}
lineBuffer[c] = readBuffer[0];
}
throw new InvalidDataException("Header is too large");
}
var headers = new Dictionary<string, string>();
string line = null;
try
{
line = await ReadLineAsync();
var sp = line.Split(' ');
if (sp.Length != 3 || !sp[2].StartsWith("HTTP") || sp[0] != "GET")
return null;
var path = sp[1];
while (true)
{
line = await ReadLineAsync();
if (line == "")
break;
sp = line.Split(new[] {':'}, 2);
headers[sp[0]] = sp[1].TrimStart();
}
error = false;
return new SimpleWebSocketHttpRequest(stream, path, headers);
}
catch
{
error = true;
return null;
}
finally
{
if (error)
stream.Dispose();
}
}
public void Listen()
{
var listener = new TcpListener(_address, _port);
listener.Start();
_listener = listener;
}
public SimpleWebSocketHttpServer(IPAddress address, int port)
{
_address = address;
_port = port;
}
public void Dispose()
{
_listener?.Stop();
_listener = null;
}
}
public class SimpleWebSocketHttpRequest : IDisposable
{
public Dictionary<string, string> Headers { get; }
public string Path { get; }
private NetworkStream _stream;
public bool IsWebsocketRequest { get; }
public IReadOnlyList<string> WebSocketProtocols { get; }
private string _websocketKey;
public SimpleWebSocketHttpRequest(NetworkStream stream, string path, Dictionary<string, string> headers)
{
Path = path;
Headers = headers;
_stream = stream;
if (headers.TryGetValue("Connection", out var h)
&& h.Contains("Upgrade")
&& headers.TryGetValue("Upgrade", out h) &&
h == "websocket"
&& headers.TryGetValue("Sec-WebSocket-Key", out _websocketKey))
{
IsWebsocketRequest = true;
if (headers.TryGetValue("Sec-WebSocket-Protocol", out h))
WebSocketProtocols = h.Split(',').Select(x => x.Trim()).ToArray();
else WebSocketProtocols = new string[0];
}
}
public async Task RespondAsync(int code, byte[] data, string contentType)
{
var headers = Encoding.UTF8.GetBytes($"HTTP/1.1 {code} {(HttpStatusCode)code}\r\nConnection: close\r\nContent-Type: {contentType}\r\nContent-Length: {data.Length}\r\n\r\n");
await _stream.WriteAsync(headers, 0, headers.Length);
await _stream.WriteAsync(data, 0, data.Length);
_stream.Dispose();
_stream = null;
}
public async Task<SimpleWebSocket> AcceptWebSocket(string protocol = null)
{
var handshakeSource = _websocketKey + "258EAFA5-E914-47DA-95CA-C5AB0DC85B11";
string handshake;
using (var sha1 = SHA1.Create())
handshake = Convert.ToBase64String(sha1.ComputeHash(Encoding.UTF8.GetBytes(handshakeSource)));
var headers =
"HTTP/1.1 101 Switching Protocols\r\nUpgrade: websocket\r\nConnection: Upgrade\r\nSec-WebSocket-Accept: "
+ handshake + "\r\n";
if (protocol != null)
headers += protocol + "\r\n";
headers += "\r\n";
var bheaders = Encoding.UTF8.GetBytes(headers);
await _stream.WriteAsync(bheaders, 0, bheaders.Length);
var s = _stream;
_stream = null;
return new SimpleWebSocket(s);
}
public void Dispose() => _stream?.Dispose();
}
public class SimpleWebSocket : IDisposable
{
class AsyncLock
{
private object _syncRoot = new object();
private Queue<TaskCompletionSource<IDisposable>> _queue = new Queue<TaskCompletionSource<IDisposable>>();
private bool _locked;
public Task<IDisposable> LockAsync()
{
lock (_syncRoot)
{
if (!_locked)
{
_locked = true;
return Task.FromResult<IDisposable>(new Lock(this));
}
else
{
var tcs = new TaskCompletionSource<IDisposable>();
_queue.Enqueue(tcs);
return tcs.Task;
}
}
}
private void Unlock()
{
lock (_syncRoot)
{
if (_queue.Count != 0)
_queue.Dequeue().SetResult(new Lock(this));
else
_locked = false;
}
}
class Lock : IDisposable
{
private AsyncLock _parent;
private object _syncRoot = new object();
public Lock(AsyncLock parent)
{
_parent = parent;
}
public void Dispose()
{
lock (_syncRoot)
{
if (_parent == null)
return;
var p = _parent;
_parent = null;
p.Unlock();
}
}
}
}
private Stream _stream;
private AsyncLock _sendLock = new AsyncLock();
private AsyncLock _recvLock = new AsyncLock();
private const int WebsocketInitialHeaderLength = 2;
private const int WebsocketLen16Length = 4;
private const int WebsocketLen64Length = 10;
private const int WebsocketLen16Code = 126;
private const int WebsocketLen64Code = 127;
[StructLayout(LayoutKind.Explicit)]
struct WebSocketHeader
{
[FieldOffset(0)] public byte Mask;
[FieldOffset(1)] public byte Length8;
[FieldOffset(2)] public ushort Length16;
[FieldOffset(2)] public ulong Length64;
}
readonly byte[] _sendHeaderBuffer = new byte[10];
readonly MemoryStream _receiveFrameStream = new MemoryStream();
readonly MemoryStream _receiveMessageStream = new MemoryStream();
private FrameType _currentMessageFrameType;
enum FrameType
{
Continue = 0x0,
Text = 0x1,
Binary = 0x2,
Close = 0x8,
Ping = 0x9,
Pong = 0xA
}
internal SimpleWebSocket(Stream stream)
{
_stream = stream;
}
public void Dispose()
{
_stream?.Dispose();
_stream = null;
}
public Task SendMessage(string text)
{
var data = Encoding.UTF8.GetBytes(text);
return SendMessage(true, data);
}
public Task SendMessage(bool isText, byte[] data) => SendMessage(isText, data, 0, data.Length);
public Task SendMessage(bool isText, byte[] data, int offset, int length)
=> SendFrame(isText ? FrameType.Text : FrameType.Binary, data, offset, length);
async Task SendFrame(FrameType type, byte[] data, int offset, int length)
{
using (var l = await _sendLock.LockAsync())
{
var header = new WebSocketHeader();
int headerLength;
if (data.Length <= 125)
{
headerLength = WebsocketInitialHeaderLength;
header.Length8 = (byte) length;
}
else if (length <= 0xffff)
{
headerLength = WebsocketLen16Length;
header.Length8 = WebsocketLen16Code;
header.Length16 = (ushort) IPAddress.HostToNetworkOrder((short) (ushort) length);
}
else
{
headerLength = WebsocketLen64Length;
header.Length8 = WebsocketLen64Code;
header.Length64 = (ulong) IPAddress.HostToNetworkOrder((long) length);
}
var endOfMessage = true;
header.Mask = (byte) (((endOfMessage ? 1u : 0u) << 7) | ((byte) (type) & 0xf));
unsafe
{
Marshal.Copy(new IntPtr(&header), _sendHeaderBuffer, 0, headerLength);
}
await _stream.WriteAsync(_sendHeaderBuffer, 0, headerLength);
await _stream.WriteAsync(data, offset, length);
}
}
struct Frame
{
public byte[] Data;
public bool EndOfMessage;
public FrameType FrameType;
}
byte[] _recvHeaderBuffer = new byte[8];
byte[] _maskBuffer = new byte[4];
async Task<Frame> ReadFrame()
{
_receiveFrameStream.Position = 0;
_receiveFrameStream.SetLength(0);
await ReadExact(_stream, _recvHeaderBuffer, 0, 2);
var masked = (_recvHeaderBuffer[1] & 0x80) != 0;
var len0 = (_recvHeaderBuffer[1] & 0x7F);
var endOfMessage = (_recvHeaderBuffer[0] & 0x80) != 0;
var frameType = (FrameType) (_recvHeaderBuffer[0] & 0xf);
int length;
if (len0 <= 125)
length = len0;
else if (len0 == WebsocketLen16Code)
{
await ReadExact(_stream, _recvHeaderBuffer, 0, 2);
length = (ushort) IPAddress.NetworkToHostOrder(BitConverter.ToInt16(_recvHeaderBuffer, 0));
}
else
{
await ReadExact(_stream, _recvHeaderBuffer, 0, 8);
length = (int) (ulong) IPAddress.NetworkToHostOrder((long) BitConverter.ToUInt64(_recvHeaderBuffer, 0));
}
if (masked)
await ReadExact(_stream, _maskBuffer, 0, 4);
await ReadExact(_stream, _receiveFrameStream, length);
var data = _receiveFrameStream.ToArray();
if(masked)
for (var c = 0; c < data.Length; c++)
data[c] = (byte) (data[c] ^ _maskBuffer[c % 4]);
return new Frame
{
Data = data,
EndOfMessage = endOfMessage,
FrameType = frameType
};
}
public async Task<SimpleWebSocketMessage> ReceiveMessage()
{
using (await _recvLock.LockAsync())
{
while (true)
{
var frame = await ReadFrame();
if (frame.FrameType == FrameType.Close)
return null;
if (frame.FrameType == FrameType.Ping)
await SendFrame(FrameType.Pong, frame.Data, 0, frame.Data.Length);
if (frame.FrameType == FrameType.Text || frame.FrameType == FrameType.Binary)
{
var isText = frame.FrameType == FrameType.Text;
if (_receiveMessageStream.Length == 0 && frame.EndOfMessage)
return new SimpleWebSocketMessage
{
IsText = isText,
Data = frame.Data
};
_receiveMessageStream.Write(frame.Data, 0, frame.Data.Length);
_currentMessageFrameType = frame.FrameType;
}
if (frame.FrameType == FrameType.Continue)
{
frame.FrameType = _currentMessageFrameType;
_receiveMessageStream.Write(frame.Data, 0, frame.Data.Length);
if (frame.EndOfMessage)
{
var isText = frame.FrameType == FrameType.Text;
var data = _receiveMessageStream.ToArray();
_receiveMessageStream.Position = 0;
_receiveMessageStream.SetLength(0);
return new SimpleWebSocketMessage
{
IsText = isText,
Data = data
};
}
}
}
}
}
byte[] _readExactBuffer = new byte[4096];
async Task ReadExact(Stream from, MemoryStream to, int length)
{
while (length>0)
{
var toRead = Math.Min(length, _readExactBuffer.Length);
var read = await from.ReadAsync(_readExactBuffer, 0, toRead);
to.Write(_readExactBuffer, 0, read);
if (read <= 0)
throw new EndOfStreamException();
length -= read;
}
}
async Task ReadExact(Stream from, byte[] to, int offset, int length)
{
while (length > 0)
{
var read = await from.ReadAsync(to, offset, length);
if (read <= 0)
throw new EndOfStreamException();
length -= read;
offset += read;
}
}
}
public class SimpleWebSocketMessage
{
public bool IsText { get; set; }
public byte[] Data { get; set; }
public string AsString()
{
return Encoding.UTF8.GetString(Data);
}
}
}

2
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/.gitignore

@ -0,0 +1,2 @@
build
node_modules

8878
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package-lock.json

File diff suppressed because it is too large

41
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/package.json

@ -0,0 +1,41 @@
{
"name": "simple",
"version": "1.0.0",
"description": "",
"main": "index.js",
"scripts": {
"webpack-ver": "cross-env NODE_ENV=production webpack --version",
"dist": "cross-env NODE_ENV=production webpack --display-error-details",
"watch": "cross-env NODE_ENV=development webpack --watch --display-error-details"
},
"author": "",
"license": "ISC",
"devDependencies": {
"awesome-typescript-loader": "^5.0.0",
"clean-webpack-plugin": "^0.1.19",
"compression-webpack-plugin": "^2.0.0",
"copy-webpack-plugin": "^4.6.0",
"cross-env": "^5.1.6",
"css-loader": "^1.0.0",
"file-loader": "^1.1.11",
"html-webpack-plugin": "^3.2.0",
"mini-css-extract-plugin": "^0.4.1",
"source-map-loader": "^0.2.3",
"style-loader": "^0.21.0",
"to-string-loader": "^1.1.5",
"tsconfig-paths-webpack-plugin": "^3.2.0",
"typescript": "^2.9.2",
"url-loader": "^1.0.1",
"webpack": "~4.16.3",
"webpack-cli": "~2.1.3",
"webpack-livereload-plugin": "~2.1.1"
},
"dependencies": {
"@types/react": "^16.3.14",
"@types/react-dom": "^16.0.5",
"mobx": "4.3.0",
"mobx-react": "^5.1.2",
"react": "^16.3.2",
"react-dom": "^16.3.2"
}
}

57
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/FramePresenter.tsx

@ -0,0 +1,57 @@
import {PreviewerFrame, PreviewerServerConnection} from "src/PreviewerServerConnection";
import * as React from "react";
interface PreviewerPresenterProps {
conn: PreviewerServerConnection;
}
export class PreviewerPresenter extends React.Component<PreviewerPresenterProps> {
private canvasRef: React.RefObject<HTMLCanvasElement>;
constructor(props: PreviewerPresenterProps) {
super(props);
this.state = {width: 1, height: 1};
this.canvasRef = React.createRef()
this.componentDidUpdate({
conn: null!
}, this.state);
}
componentDidMount(): void {
this.updateCanvas(this.canvasRef.current, this.props.conn.currentFrame);
}
componentDidUpdate(prevProps: Readonly<PreviewerPresenterProps>, prevState: Readonly<{}>, snapshot?: any): void {
if(prevProps.conn != this.props.conn)
{
if(prevProps.conn)
prevProps.conn.removeFrameListener(this.frameHandler);
if(this.props.conn)
this.props.conn.addFrameListener(this.frameHandler);
}
}
private frameHandler = (frame: PreviewerFrame)=>{
this.updateCanvas(this.canvasRef.current, frame);
};
updateCanvas(canvas: HTMLCanvasElement | null, frame: PreviewerFrame | null) {
if (!canvas)
return;
if (frame == null){
canvas.width = canvas.height = 1;
canvas.getContext('2d')!.clearRect(0,0,1,1);
}
else {
canvas.width = frame.data.width;
canvas.height = frame.data.height;
const ctx = canvas.getContext('2d')!;
ctx.putImageData(frame.data, 0,0);
}
}
render() {
return <canvas ref={this.canvasRef}/>
}
}

78
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/PreviewerServerConnection.ts

@ -0,0 +1,78 @@
export interface PreviewerFrame {
data: ImageData;
dpiX: number;
dpiY: number;
}
export class PreviewerServerConnection {
private nextFrame = {
width: 0,
height: 0,
stride: 0,
dpiX: 0,
dpiY: 0,
sequenceId: "0"
};
public currentFrame: PreviewerFrame | null;
private handlers = new Set<(frame: PreviewerFrame | null) => void>();
private conn: WebSocket;
public addFrameListener(listener: (frame: PreviewerFrame | null) => void) {
this.handlers.add(listener);
if (this.currentFrame)
listener(this.currentFrame);
}
public removeFrameListener(listener: (frame: PreviewerFrame | null) => void) {
this.handlers.delete(listener);
}
constructor(uri: string) {
this.currentFrame = null;
var conn = this.conn = new WebSocket(uri);
conn.binaryType = 'arraybuffer';
const onMessage = this.onMessage;
conn.onmessage = msg => onMessage(msg);
const onClose = () => this.setFrame(null);
conn.onclose = () => onClose();
conn.onerror = (err: Event) => {
onClose();
console.log("Connection error: " + err);
}
}
private onMessage = (msg: MessageEvent) => {
if (typeof msg.data == 'string' || msg.data instanceof String) {
const parts = msg.data.split(':');
if (parts[0] == 'frame') {
this.nextFrame = {
sequenceId: parts[1],
width: parseInt(parts[2]),
height: parseInt(parts[3]),
stride: parseInt(parts[4]),
dpiX: parseInt(parts[5]),
dpiY: parseInt(parts[6])
}
}
} else if (msg.data instanceof ArrayBuffer) {
const arr = new Uint8ClampedArray(msg.data, 0);
const imageData = new ImageData(arr, this.nextFrame.width, this.nextFrame.height);
this.conn.send('frame-received:' + this.nextFrame.sequenceId);
this.setFrame({
data: imageData,
dpiX: this.nextFrame.dpiX,
dpiY: this.nextFrame.dpiY
});
}
};
private setFrame(frame: PreviewerFrame | null) {
this.currentFrame = frame;
this.handlers.forEach(h => h(this.currentFrame));
}
}

14
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.html

@ -0,0 +1,14 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<title>Avalonia XAML previewer web edition</title>
</head>
<body>
<div id="app">
<center>Loading...</center>
</div>
<noscript>Javascript is required</noscript>
</body>
</html>

15
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/src/index.tsx

@ -0,0 +1,15 @@
import * as React from "react";
import {PreviewerPresenter} from './FramePresenter'
import {PreviewerServerConnection} from "src/PreviewerServerConnection";
import * as ReactDOM from "react-dom";
const loc = window.location;
const conn = new PreviewerServerConnection((loc.protocol === "https:" ? "wss" : "ws") + "://" + loc.host + "/ws");
const App = function(){
return <div style={{width: '100%'}}>
<PreviewerPresenter conn={conn}/>
</div>
};
ReactDOM.render(<App/>, document.getElementById("app"));

35
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/tsconfig.json

@ -0,0 +1,35 @@
{
"compilerOptions": {
"outDir": "build/dist",
"module": "esnext",
"target": "es5",
"lib": ["es6", "dom"],
"sourceMap": true,
"allowJs": true,
"jsx": "react",
"moduleResolution": "node",
"forceConsistentCasingInFileNames": true,
"noImplicitReturns": true,
"noImplicitThis": true,
"noImplicitAny": true,
"strictNullChecks": true,
"suppressImplicitAnyIndexErrors": true,
"noUnusedLocals": false,
"baseUrl": ".",
"experimentalDecorators": true,
"paths": {
"*": ["./node_modules/@types/*", "./node_modules/*"],
"src/*": ["./src/*"]
}
},
"include": ["./src/**/*"],
"exclude": [
"node_modules",
"build",
"scripts",
"acceptance-tests",
"webpack",
"jest",
"src/setupTests.ts"
]
}

117
src/Avalonia.DesignerSupport/Remote/HtmlTransport/webapp/webpack.config.js

@ -0,0 +1,117 @@
const webpack = require('webpack');
const path = require('path');
const LiveReloadPlugin = require('webpack-livereload-plugin');
const HtmlWebpackPlugin = require('html-webpack-plugin');
const CompressionPlugin = require('compression-webpack-plugin');
const CleanWebpackPlugin = require('clean-webpack-plugin');
const MiniCssExtractPlugin = require("mini-css-extract-plugin");
const TsconfigPathsPlugin = require('tsconfig-paths-webpack-plugin');
const CopyWebpackPlugin = require('copy-webpack-plugin');
const prod = process.env.NODE_ENV == 'production';
class Printer {
apply(compiler) {
compiler.hooks.afterEmit.tap("Printer", ()=> console.log("Build completed at " + new Date().toString()));
compiler.hooks.watchRun.tap("Printer", ()=> console.log("Watch triggered at " + new Date().toString()));
}
}
const config = {
entry: {
bundle: './src/index.tsx'
},
output: {
path: path.resolve(__dirname, 'build'),
publicPath: '/',
filename: '[name].[chunkhash].js'
},
performance: { hints: false },
mode: prod ? "production" : "development",
module: {
rules: [
{
enforce: "pre",
test: /\.js$/,
loader: "source-map-loader",
exclude: [
path.resolve(__dirname, 'node_modules/mobx-state-router')
]
},
{
"oneOf": [
{
test: /\.(ts|tsx)$/,
exclude: /node_modules/,
use: 'awesome-typescript-loader'
},
{
test: /\.css$/,
use: [
MiniCssExtractPlugin.loader,
'css-loader'
]
},
{
test: /\.(jpg|png)$/,
use: {
loader: "url-loader",
options: {
limit: 25000,
},
},
},
{
test: /.(ttf|otf|eot|svg|woff(2)?)(\?[a-z0-9]+)?$/,
use: [{
loader: 'file-loader',
options: {
name: '[name].[ext]',
outputPath: 'fonts/', // where the fonts will go
}
}]
},
{
loader: require.resolve('file-loader'),
exclude: [/\.(js|jsx|mjs|tsx|ts)$/, /\.html$/, /\.json$/],
options: {
name: 'assets/[name].[hash:8].[ext]',
},
}]
},
]
},
devtool: "source-map",
resolve: {
modules: [path.resolve(__dirname, 'node_modules')],
plugins: [new TsconfigPathsPlugin({ configFile: "./tsconfig.json", logLevel: 'info' })],
extensions: ['.ts', '.tsx', '.js', '.json'],
alias: {
'src': path.resolve(__dirname, 'src')
}
},
plugins:
[
new Printer(),
new CleanWebpackPlugin([path.resolve(__dirname, 'build')]),
new MiniCssExtractPlugin({
filename: "[name].[chunkhash]h" +
".css",
chunkFilename: "[id].[chunkhash].css"
}),
new LiveReloadPlugin({appendScriptTag: !prod}),
new HtmlWebpackPlugin({
template: path.resolve(__dirname, './src/index.html'),
filename: 'index.html' //relative to root of the application
}),
new CopyWebpackPlugin([
// relative path from src
//{ from: './src/favicon.ico' },
//{ from: './src/assets' }
]),
new CompressionPlugin({
test: /(\?.*)?$/i
})
]
};
module.exports = config;

63
src/Avalonia.DesignerSupport/Remote/RemoteDesignerEntryPoint.cs

@ -5,6 +5,7 @@ using System.Reflection;
using System.Threading;
using System.Xml;
using Avalonia.Controls;
using Avalonia.DesignerSupport.Remote.HtmlTransport;
using Avalonia.Input;
using Avalonia.Remote.Protocol;
using Avalonia.Remote.Protocol.Designer;
@ -24,15 +25,16 @@ namespace Avalonia.DesignerSupport.Remote
{
public string AppPath { get; set; }
public Uri Transport { get; set; }
public Uri HtmlMethodListenUri { get; set; }
public string Method { get; set; } = Methods.AvaloniaRemote;
public string SessionId { get; set; } = Guid.NewGuid().ToString();
}
static class Methods
internal static class Methods
{
public const string AvaloniaRemote = "avalonia-remote";
public const string Win32 = "win32";
public const string Html = "html";
}
static Exception Die(string error)
@ -52,6 +54,19 @@ namespace Avalonia.DesignerSupport.Remote
{
Console.Error.WriteLine("Usage: --transport transport_spec --session-id sid --method method app");
Console.Error.WriteLine();
Console.Error.WriteLine("--transport: transport used for communication with the IDE");
Console.Error.WriteLine(" 'tcp-bson' (e. g. 'tcp-bson://127.0.0.1:30243/') - TCP-based transport with BSON serialization of messages defined in Avalonia.Remote.Protocol");
Console.Error.WriteLine(" 'file' (e. g. 'file://C://my/file.xaml' - pseudo-transport that triggers XAML updates on file changes, useful as a standalone previewer tool, always uses http preview method");
Console.Error.WriteLine();
Console.Error.WriteLine("--session-id: session id to be sent to IDE process");
Console.Error.WriteLine();
Console.Error.WriteLine("--method: the way the XAML is displayed");
Console.Error.WriteLine(" 'avalonia-remote' - binary image is sent via transport connection in FrameMessage");
Console.Error.WriteLine(" 'win32' - XAML is displayed in win32 window (handle could be obtained from UpdateXamlResultMessage), IDE is responsible to use user32!SetParent");
Console.Error.WriteLine(" 'html' - Previewer starts an HTML server and displays XAML previewer as a web page");
Console.Error.WriteLine();
Console.Error.WriteLine("--html-url - endpoint for HTML method to listen on, e. g. http://127.0.0.1:8081");
Console.Error.WriteLine();
Console.Error.WriteLine("Example: --transport tcp-bson://127.0.0.1:30243/ --session-id 123 --method avalonia-remote MyApp.exe");
Console.Error.Flush();
return Die(null);
@ -74,6 +89,8 @@ namespace Avalonia.DesignerSupport.Remote
next = a => rv.Transport = new Uri(a, UriKind.Absolute);
else if (arg == "--method")
next = a => rv.Method = a;
else if (arg == "--html-url")
next = a => rv.HtmlMethodListenUri = new Uri(a, UriKind.Absolute);
else if (arg == "--session-id")
next = a => rv.SessionId = a;
else if (rv.AppPath == null)
@ -89,6 +106,9 @@ namespace Avalonia.DesignerSupport.Remote
{
PrintUsage();
}
if (next != null)
PrintUsage();
return rv;
}
@ -98,27 +118,40 @@ namespace Avalonia.DesignerSupport.Remote
{
return new BsonTcpTransport().Connect(IPAddress.Parse(transport.Host), transport.Port).Result;
}
if (transport.Scheme == "file")
{
return new FileWatcherTransport(transport);
}
PrintUsage();
return null;
}
interface IAppInitializer
{
Application GetConfiguredApp(IAvaloniaRemoteTransportConnection transport, CommandLineArgs args, object obj);
IAvaloniaRemoteTransportConnection ConfigureApp(IAvaloniaRemoteTransportConnection transport, CommandLineArgs args, object obj);
}
class AppInitializer<T> : IAppInitializer where T : AppBuilderBase<T>, new()
{
public Application GetConfiguredApp(IAvaloniaRemoteTransportConnection transport,
public IAvaloniaRemoteTransportConnection ConfigureApp(IAvaloniaRemoteTransportConnection transport,
CommandLineArgs args, object obj)
{
var builder = (AppBuilderBase<T>) obj;
var builder = (AppBuilderBase<T>)obj;
if (args.Method == Methods.AvaloniaRemote)
builder.UseWindowingSubsystem(() => PreviewerWindowingPlatform.Initialize(transport));
if (args.Method == Methods.Html)
{
transport = new HtmlWebSocketTransport(transport,
args.HtmlMethodListenUri ?? new Uri("http://localhost:5000"));
builder.UseWindowingSubsystem(() =>
PreviewerWindowingPlatform.Initialize(transport));
}
if (args.Method == Methods.Win32)
builder.UseWindowingSubsystem("Avalonia.Win32");
builder.SetupWithoutStarting();
return builder.Instance;
return transport;
}
}
@ -128,6 +161,8 @@ namespace Avalonia.DesignerSupport.Remote
{
var args = ParseCommandLineArgs(cmdline);
var transport = CreateTransport(args.Transport);
if (transport is ITransportWithEnforcedMethod enforcedMethod)
args.Method = enforcedMethod.PreviewerMethod;
var asm = Assembly.LoadFile(System.IO.Path.GetFullPath(args.AppPath));
var entryPoint = asm.EntryPoint;
if (entryPoint == null)
@ -141,12 +176,14 @@ namespace Avalonia.DesignerSupport.Remote
var appBuilder = builderMethod.Invoke(null, null);
Log($"Initializing application in design mode");
var initializer =(IAppInitializer)Activator.CreateInstance(typeof(AppInitializer<>).MakeGenericType(appBuilder.GetType()));
var app = initializer.GetConfiguredApp(transport, args, appBuilder);
transport = initializer.ConfigureApp(transport, args, appBuilder);
s_transport = transport;
transport.OnMessage += OnTransportMessage;
transport.OnException += (t, e) => Die(e.ToString());
transport.Start();
Log("Sending StartDesignerSessionMessage");
transport.Send(new StartDesignerSessionMessage {SessionId = args.SessionId});
Dispatcher.UIThread.MainLoop(CancellationToken.None);
}
@ -197,18 +234,10 @@ namespace Avalonia.DesignerSupport.Remote
}
catch (Exception e)
{
var xmlException = e as XmlException;
s_transport.Send(new UpdateXamlResultMessage
{
Error = e.ToString(),
Exception = new ExceptionDetails
{
ExceptionType = e.GetType().FullName,
Message = e.Message.ToString(),
LineNumber = xmlException?.LineNumber,
LinePosition = xmlException?.LinePosition,
}
Exception = new ExceptionDetails(e),
});
}
}

10
src/Avalonia.Layout/FlowLayoutAlgorithm.cs

@ -74,6 +74,7 @@ namespace Avalonia.Layout
double lineSpacing,
int maxItemsPerLine,
ScrollOrientation orientation,
bool disableVirtualization,
string layoutId)
{
_orientation.ScrollOrientation = orientation;
@ -95,14 +96,14 @@ namespace Avalonia.Layout
_elementManager.OnBeginMeasure(orientation);
int anchorIndex = GetAnchorIndex(availableSize, isWrapping, minItemSpacing, layoutId);
Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId);
Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId);
Generate(GenerateDirection.Forward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId);
Generate(GenerateDirection.Backward, anchorIndex, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId);
if (isWrapping && IsReflowRequired())
{
var firstElementBounds = _elementManager.GetLayoutBoundsForRealizedIndex(0);
_orientation.SetMinorStart(ref firstElementBounds, 0);
_elementManager.SetLayoutBoundsForRealizedIndex(0, firstElementBounds);
Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, layoutId);
Generate(GenerateDirection.Forward, 0 /*anchorIndex*/, availableSize, minItemSpacing, lineSpacing, maxItemsPerLine, disableVirtualization, layoutId);
}
RaiseLineArranged();
@ -273,6 +274,7 @@ namespace Avalonia.Layout
double minItemSpacing,
double lineSpacing,
int maxItemsPerLine,
bool disableVirtualization,
string layoutId)
{
if (anchorIndex != -1)
@ -288,7 +290,7 @@ namespace Avalonia.Layout
bool lineNeedsReposition = false;
while (_elementManager.IsIndexValidInData(currentIndex) &&
ShouldContinueFillingUpSpace(previousIndex, direction))
(disableVirtualization || ShouldContinueFillingUpSpace(previousIndex, direction)))
{
// Ensure layout element.
_elementManager.EnsureElementRealized(direction == GenerateDirection.Forward, currentIndex, layoutId);

20
src/Avalonia.Layout/StackLayout.cs

@ -14,6 +14,12 @@ namespace Avalonia.Layout
/// </summary>
public class StackLayout : VirtualizingLayout, IFlowLayoutAlgorithmDelegates
{
/// <summary>
/// Defines the <see cref="DisableVirtualization"/> property.
/// </summary>
public static readonly StyledProperty<bool> DisableVirtualizationProperty =
AvaloniaProperty.Register<StackLayout, bool>(nameof(DisableVirtualization));
/// <summary>
/// Defines the <see cref="Orientation"/> property.
/// </summary>
@ -36,6 +42,15 @@ namespace Avalonia.Layout
LayoutId = "StackLayout";
}
/// <summary>
/// Gets or sets a value indicating whether virtualization is disabled on the layout.
/// </summary>
public bool DisableVirtualization
{
get => GetValue(DisableVirtualizationProperty);
set => SetValue(DisableVirtualizationProperty, value);
}
/// <summary>
/// Gets or sets the axis along which items are laid out.
/// </summary>
@ -262,6 +277,8 @@ namespace Avalonia.Layout
protected internal override Size MeasureOverride(VirtualizingLayoutContext context, Size availableSize)
{
((StackLayoutState)context.LayoutState).OnMeasureStart();
var desiredSize = GetFlowAlgorithm(context).Measure(
availableSize,
context,
@ -270,6 +287,7 @@ namespace Avalonia.Layout
Spacing,
int.MaxValue,
_orientation.ScrollOrientation,
DisableVirtualization,
LayoutId);
return new Size(desiredSize.Width, desiredSize.Height);
@ -284,8 +302,6 @@ namespace Avalonia.Layout
FlowLayoutAlgorithm.LineAlignment.Start,
LayoutId);
((StackLayoutState)context.LayoutState).OnArrangeLayoutEnd();
return new Size(value.Width, value.Height);
}

2
src/Avalonia.Layout/StackLayoutState.cs

@ -56,6 +56,6 @@ namespace Avalonia.Layout
MaxArrangeBounds = Math.Max(MaxArrangeBounds, minorSize);
}
internal void OnArrangeLayoutEnd() => MaxArrangeBounds = 0;
internal void OnMeasureStart() => MaxArrangeBounds = 0;
}
}

1
src/Avalonia.Layout/UniformGridLayout.cs

@ -433,6 +433,7 @@ namespace Avalonia.Layout
LineSpacing,
_maximumRowsOrColumns,
_orientation.ScrollOrientation,
false,
LayoutId);
// If after Measure the first item is in the realization rect, then we revoke grid state's ownership,

76
src/Avalonia.Native/AvaloniaNativeDragSource.cs

@ -0,0 +1,76 @@
using System;
using System.Runtime.InteropServices;
using System.Threading.Tasks;
using Avalonia.Controls;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Interactivity;
using Avalonia.Native.Interop;
using Avalonia.Platform;
using Avalonia.VisualTree;
namespace Avalonia.Native
{
class AvaloniaNativeDragSource : IPlatformDragSource
{
private readonly IAvaloniaNativeFactory _factory;
public AvaloniaNativeDragSource(IAvaloniaNativeFactory factory)
{
_factory = factory;
}
TopLevel FindRoot(IInteractive interactive)
{
while (interactive != null && !(interactive is IVisual))
interactive = interactive.InteractiveParent;
if (interactive == null)
return null;
var visual = (IVisual)interactive;
return visual.VisualRoot as TopLevel;
}
class DndCallback : CallbackBase, IAvnDndResultCallback
{
private TaskCompletionSource<DragDropEffects> _tcs;
public DndCallback(TaskCompletionSource<DragDropEffects> tcs)
{
_tcs = tcs;
}
public void OnDragAndDropComplete(AvnDragDropEffects effect)
{
_tcs?.TrySetResult((DragDropEffects)effect);
_tcs = null;
}
}
public Task<DragDropEffects> DoDragDrop(PointerEventArgs triggerEvent, IDataObject data, DragDropEffects allowedEffects)
{
// Sanity check
var tl = FindRoot(triggerEvent.Source);
var view = tl?.PlatformImpl as WindowBaseImpl;
if (view == null)
throw new ArgumentException();
triggerEvent.Pointer.Capture(null);
var tcs = new TaskCompletionSource<DragDropEffects>();
var clipboardImpl = _factory.CreateDndClipboard();
using (var clipboard = new ClipboardImpl(clipboardImpl))
using (var cb = new DndCallback(tcs))
{
if (data.Contains(DataFormats.Text))
// API is synchronous, so it's OK
clipboard.SetTextAsync(data.GetText()).Wait();
view.BeginDraggingSession((AvnDragDropEffects)allowedEffects,
triggerEvent.GetPosition(tl).ToAvnPoint(), clipboardImpl, cb,
GCHandle.ToIntPtr(GCHandle.Alloc(data)));
}
return tcs.Task;
}
}
}

16
src/Avalonia.Native/AvaloniaNativePlatform.cs

@ -77,17 +77,25 @@ namespace Avalonia.Native
_factory = factory;
}
class GCHandleDeallocator : CallbackBase, IAvnGCHandleDeallocatorCallback
{
public void FreeGCHandle(IntPtr handle)
{
GCHandle.FromIntPtr(handle).Free();
}
}
void DoInitialize(AvaloniaNativePlatformOptions options)
{
_options = options;
_factory.Initialize();
_factory.Initialize(new GCHandleDeallocator());
if (_factory.MacOptions != null)
{
var macOpts = AvaloniaLocator.Current.GetService<MacOSPlatformOptions>();
_factory.MacOptions.ShowInDock = macOpts?.ShowInDock != false ? 1 : 0;
}
AvaloniaLocator.CurrentMutable
.Bind<IPlatformThreadingInterface>()
.ToConstant(new PlatformThreadingInterface(_factory.CreatePlatformThreadingInterface()))
@ -101,7 +109,9 @@ namespace Avalonia.Native
.Bind<IRenderTimer>().ToConstant(new DefaultRenderTimer(60))
.Bind<ISystemDialogImpl>().ToConstant(new SystemDialogs(_factory.CreateSystemDialogs()))
.Bind<PlatformHotkeyConfiguration>().ToConstant(new PlatformHotkeyConfiguration(KeyModifiers.Meta))
.Bind<IMountedVolumeInfoProvider>().ToConstant(new MacOSMountedVolumeInfoProvider());
.Bind<IMountedVolumeInfoProvider>().ToConstant(new MacOSMountedVolumeInfoProvider())
.Bind<IPlatformDragSource>().ToConstant(new AvaloniaNativeDragSource(_factory))
;
if (_options.UseGpu)
AvaloniaLocator.CurrentMutable.Bind<IWindowingPlatformGlFeature>()
.ToConstant(_glFeature = new GlPlatformFeature(_factory.ObtainGlDisplay()));

39
src/Avalonia.Native/AvnString.cs

@ -0,0 +1,39 @@
using System.Runtime.InteropServices;
namespace Avalonia.Native.Interop
{
unsafe partial class IAvnString
{
private string _managed;
public string String
{
get
{
if (_managed == null)
{
var ptr = Pointer();
if (ptr == null)
return null;
_managed = System.Text.Encoding.UTF8.GetString((byte*)ptr.ToPointer(), Length());
}
return _managed;
}
}
public override string ToString() => String;
}
partial class IAvnStringArray
{
public string[] ToStringArray()
{
var arr = new string[Count];
for(uint c = 0; c<arr.Length;c++)
using (var s = Get(c))
arr[c] = s.String;
return arr;
}
}
}

98
src/Avalonia.Native/ClipboardImpl.cs

@ -1,15 +1,22 @@
using System.Threading.Tasks;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using System.Runtime.InteropServices;
using Avalonia.Input;
using Avalonia.Input.Platform;
using Avalonia.Native.Interop;
using Avalonia.Platform.Interop;
namespace Avalonia.Native
{
class ClipboardImpl : IClipboard
class ClipboardImpl : IClipboard, IDisposable
{
private IAvnClipboard _native;
private const string NSPasteboardTypeString = "public.utf8-plain-text";
private const string NSFilenamesPboardType = "NSFilenamesPboardType";
private const string NSPasteboardTypeFileUrl = "public.file-url";
public ClipboardImpl(IAvnClipboard native)
{
_native = native;
@ -22,14 +29,10 @@ namespace Avalonia.Native
return Task.CompletedTask;
}
public unsafe Task<string> GetTextAsync()
public Task<string> GetTextAsync()
{
using (var text = _native.GetText())
{
var result = System.Text.Encoding.UTF8.GetString((byte*)text.Pointer(), text.Length());
return Task.FromResult(result);
}
using (var text = _native.GetText(NSPasteboardTypeString))
return Task.FromResult(text.String);
}
public Task SetTextAsync(string text)
@ -40,11 +43,84 @@ namespace Avalonia.Native
{
using (var buffer = new Utf8Buffer(text))
{
_native.SetText(buffer.DangerousGetHandle());
_native.SetText(NSPasteboardTypeString, buffer.DangerousGetHandle());
}
}
return Task.CompletedTask;
}
public IEnumerable<string> GetFormats()
{
var rv = new List<string>();
using (var formats = _native.ObtainFormats())
{
var cnt = formats.Count;
for (uint c = 0; c < cnt; c++)
{
using (var fmt = formats.Get(c))
{
if(fmt.String == NSPasteboardTypeString)
rv.Add(DataFormats.Text);
if(fmt.String == NSFilenamesPboardType)
rv.Add(DataFormats.FileNames);
}
}
}
return rv;
}
public void Dispose()
{
_native?.Dispose();
_native = null;
}
public IEnumerable<string> GetFileNames()
{
using (var strings = _native.GetStrings(NSFilenamesPboardType))
return strings.ToStringArray();
}
}
class ClipboardDataObject : IDataObject, IDisposable
{
private ClipboardImpl _clipboard;
private List<string> _formats;
public ClipboardDataObject(IAvnClipboard clipboard)
{
_clipboard = new ClipboardImpl(clipboard);
}
public void Dispose()
{
_clipboard?.Dispose();
_clipboard = null;
}
List<string> Formats => _formats ??= _clipboard.GetFormats().ToList();
public IEnumerable<string> GetDataFormats() => Formats;
public bool Contains(string dataFormat) => Formats.Contains(dataFormat);
public string GetText()
{
// bad idea in general, but API is synchronous anyway
return _clipboard.GetTextAsync().Result;
}
public IEnumerable<string> GetFileNames() => _clipboard.GetFileNames();
public object Get(string dataFormat)
{
if (dataFormat == DataFormats.Text)
return GetText();
if (dataFormat == DataFormats.FileNames)
return GetFileNames();
return null;
}
}
}

86
src/Avalonia.Native/GlPlatformFeature.cs

@ -8,42 +8,58 @@ namespace Avalonia.Native
{
class GlPlatformFeature : IWindowingPlatformGlFeature
{
private readonly IAvnGlDisplay _display;
public GlPlatformFeature(IAvnGlDisplay display)
{
_display = display;
var immediate = display.CreateContext(null);
var deferred = display.CreateContext(immediate);
GlDisplay = new GlDisplay(display, immediate.SampleCount, immediate.StencilSize);
ImmediateContext = new GlContext(Display, immediate);
DeferredContext = new GlContext(Display, deferred);
int major, minor;
GlInterface glInterface;
using (immediate.MakeCurrent())
{
var basic = new GlBasicInfoInterface(display.GetProcAddress);
basic.GetIntegerv(GlConsts.GL_MAJOR_VERSION, out major);
basic.GetIntegerv(GlConsts.GL_MINOR_VERSION, out minor);
_version = new GlVersion(GlProfileType.OpenGL, major, minor);
glInterface = new GlInterface(_version, (name) =>
{
var rv = _display.GetProcAddress(name);
return rv;
});
}
GlDisplay = new GlDisplay(display, glInterface, immediate.SampleCount, immediate.StencilSize);
ImmediateContext = new GlContext(GlDisplay, immediate, _version);
DeferredContext = new GlContext(GlDisplay, deferred, _version);
}
public IGlContext ImmediateContext { get; }
internal IGlContext ImmediateContext { get; }
public IGlContext MainContext => DeferredContext;
internal GlContext DeferredContext { get; }
internal GlDisplay GlDisplay;
public GlDisplay Display => GlDisplay;
private readonly GlVersion _version;
public IGlContext CreateContext() => new GlContext(GlDisplay,
_display.CreateContext(((GlContext)ImmediateContext).Context), _version);
}
class GlDisplay : IGlDisplay
class GlDisplay
{
private readonly IAvnGlDisplay _display;
public GlDisplay(IAvnGlDisplay display, int sampleCount, int stencilSize)
public GlDisplay(IAvnGlDisplay display, GlInterface glInterface, int sampleCount, int stencilSize)
{
_display = display;
SampleCount = sampleCount;
StencilSize = stencilSize;
GlInterface = new GlInterface((name, optional) =>
{
var rv = _display.GetProcAddress(name);
if (rv == IntPtr.Zero && !optional)
throw new OpenGlException($"{name} not found in system OpenGL");
return rv;
});
GlInterface = glInterface;
}
public GlDisplayType Type => GlDisplayType.OpenGL2;
public GlInterface GlInterface { get; }
public int SampleCount { get; }
@ -55,19 +71,26 @@ namespace Avalonia.Native
class GlContext : IGlContext
{
public IAvnGlContext Context { get; }
private readonly GlDisplay _display;
public IAvnGlContext Context { get; private set; }
public GlContext(GlDisplay display, IAvnGlContext context)
public GlContext(GlDisplay display, IAvnGlContext context, GlVersion version)
{
Display = display;
_display = display;
Context = context;
Version = version;
}
public IGlDisplay Display { get; }
public GlVersion Version { get; }
public GlInterface GlInterface => _display.GlInterface;
public int SampleCount => _display.SampleCount;
public int StencilSize => _display.StencilSize;
public IDisposable MakeCurrent() => Context.MakeCurrent();
public void MakeCurrent()
public void Dispose()
{
Context.LegacyMakeCurrent();
Context.Dispose();
Context = null;
}
}
@ -75,15 +98,18 @@ namespace Avalonia.Native
class GlPlatformSurfaceRenderTarget : IGlPlatformSurfaceRenderTarget
{
private IAvnGlSurfaceRenderTarget _target;
public GlPlatformSurfaceRenderTarget(IAvnGlSurfaceRenderTarget target)
private readonly IGlContext _context;
public GlPlatformSurfaceRenderTarget(IAvnGlSurfaceRenderTarget target, IGlContext context)
{
_target = target;
_context = context;
}
public IGlPlatformSurfaceRenderingSession BeginDraw()
{
var feature = (GlPlatformFeature)AvaloniaLocator.Current.GetService<IWindowingPlatformGlFeature>();
return new GlPlatformSurfaceRenderingSession(feature.Display, _target.BeginDrawing());
return new GlPlatformSurfaceRenderingSession(_context, _target.BeginDrawing());
}
public void Dispose()
@ -97,13 +123,13 @@ namespace Avalonia.Native
{
private IAvnGlSurfaceRenderingSession _session;
public GlPlatformSurfaceRenderingSession(GlDisplay display, IAvnGlSurfaceRenderingSession session)
public GlPlatformSurfaceRenderingSession(IGlContext context, IAvnGlSurfaceRenderingSession session)
{
Display = display;
Context = context;
_session = session;
}
public IGlDisplay Display { get; }
public IGlContext Context { get; }
public PixelSize Size
{
@ -129,14 +155,16 @@ namespace Avalonia.Native
class GlPlatformSurface : IGlPlatformSurface
{
private readonly IAvnWindowBase _window;
private readonly IGlContext _context;
public GlPlatformSurface(IAvnWindowBase window)
public GlPlatformSurface(IAvnWindowBase window, IGlContext context)
{
_window = window;
_context = context;
}
public IGlPlatformSurfaceRenderTarget CreateGlRenderTarget()
{
return new GlPlatformSurfaceRenderTarget(_window.CreateGlRenderTarget());
return new GlPlatformSurfaceRenderTarget(_window.CreateGlRenderTarget(), _context);
}
}

3
src/Avalonia.Native/PopupImpl.cs

@ -21,7 +21,8 @@ namespace Avalonia.Native
_glFeature = glFeature;
using (var e = new PopupEvents(this))
{
Init(factory.CreatePopup(e, _opts.UseGpu ? glFeature?.DeferredContext.Context : null), factory.CreateScreens());
var context = _opts.UseGpu ? glFeature?.DeferredContext : null;
Init(factory.CreatePopup(e, context?.Context), factory.CreateScreens(), context);
}
PopupPositioner = new ManagedPopupPositioner(new OsxManagedPopupPositionerPopupImplHelper(parent, MoveResize));
}

5
src/Avalonia.Native/WindowImpl.cs

@ -2,6 +2,7 @@
using Avalonia.Controls;
using Avalonia.Controls.Platform;
using Avalonia.Native.Interop;
using Avalonia.OpenGL;
using Avalonia.Platform;
using Avalonia.Platform.Interop;
@ -21,8 +22,8 @@ namespace Avalonia.Native
_glFeature = glFeature;
using (var e = new WindowEvents(this))
{
Init(_native = factory.CreateWindow(e,
_opts.UseGpu ? glFeature?.DeferredContext.Context : null), factory.CreateScreens());
var context = _opts.UseGpu ? glFeature?.DeferredContext : null;
Init(_native = factory.CreateWindow(e, context?.Context), factory.CreateScreens(), context);
}
NativeMenuExporter = new AvaloniaNativeMenuExporter(_native, factory);

37
src/Avalonia.Native/WindowImplBase.cs

@ -1,6 +1,7 @@
using System;
using System.Collections.Generic;
using System.Linq;
using System.Runtime.InteropServices;
using Avalonia.Controls;
using Avalonia.Controls.Platform.Surfaces;
using Avalonia.Input;
@ -56,6 +57,7 @@ namespace Avalonia.Native
private Size _lastRenderedLogicalSize;
private double _savedScaling;
private GlPlatformSurface _glSurface;
private IGlContext _glContext;
internal WindowBaseImpl(AvaloniaNativePlatformOptions opts, GlPlatformFeature glFeature)
{
@ -67,13 +69,14 @@ namespace Avalonia.Native
_cursorFactory = AvaloniaLocator.Current.GetService<IStandardCursorFactory>();
}
protected void Init(IAvnWindowBase window, IAvnScreens screens)
protected void Init(IAvnWindowBase window, IAvnScreens screens, IGlContext glContext)
{
_native = window;
_glContext = glContext;
Handle = new MacOSTopLevelWindowHandle(window);
if (_gpu)
_glSurface = new GlPlatformSurface(window);
_glSurface = new GlPlatformSurface(window, _glContext);
Screen = new ScreenImpl(screens);
_savedLogicalSize = ClientSize;
_savedScaling = Scaling;
@ -198,6 +201,30 @@ namespace Avalonia.Native
{
Dispatcher.UIThread.RunJobs(DispatcherPriority.Render);
}
public AvnDragDropEffects DragEvent(AvnDragEventType type, AvnPoint position,
AvnInputModifiers modifiers,
AvnDragDropEffects effects,
IAvnClipboard clipboard, IntPtr dataObjectHandle)
{
var device = AvaloniaLocator.Current.GetService<IDragDropDevice>();
IDataObject dataObject = null;
if (dataObjectHandle != IntPtr.Zero)
dataObject = GCHandle.FromIntPtr(dataObjectHandle).Target as IDataObject;
using(var clipboardDataObject = new ClipboardDataObject(clipboard))
{
if (dataObject == null)
dataObject = clipboardDataObject;
var args = new RawDragEvent(device, (RawDragEventType)type,
_parent._inputRoot, position.ToAvaloniaPoint(), dataObject, (DragDropEffects)effects,
(RawInputModifiers)modifiers);
_parent.Input(args);
return (AvnDragDropEffects)args.Effects;
}
}
}
public void Activate()
@ -356,6 +383,12 @@ namespace Avalonia.Native
}
internal void BeginDraggingSession(AvnDragDropEffects effects, AvnPoint point, IAvnClipboard clipboard,
IAvnDndResultCallback callback, IntPtr sourceHandle)
{
_native.BeginDragAndDropOperation(effects, point, clipboard, callback, sourceHandle);
}
public IPlatformHandle Handle { get; private set; }
}
}

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

Loading…
Cancel
Save