diff --git a/.gitmodules b/.gitmodules index 10c780c09fd..2d11fdfa9e1 100644 --- a/.gitmodules +++ b/.gitmodules @@ -2,5 +2,5 @@ path = nukebuild/Numerge url = https://github.com/kekekeks/Numerge.git [submodule "src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github"] - path = src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github - url = https://github.com/kekekeks/XamlIl.git + path = src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github + url = https://github.com/kekekeks/XamlX.git diff --git a/Avalonia.sln b/Avalonia.sln index f6dc039c2f0..ccaa41f552b 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -123,10 +123,13 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "ControlCatalog.NetCore", "s EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1-27F5-4255-9AFC-04ABFD11683A}" ProjectSection(SolutionItems) = preProject + build\AndroidWorkarounds.props = build\AndroidWorkarounds.props build\Base.props = build\Base.props build\Binding.props = build\Binding.props - build\BuildTargets.targets = build\BuildTargets.targets + build\CoreLibraries.props = build\CoreLibraries.props + build\EmbedXaml.props = build\EmbedXaml.props build\HarfBuzzSharp.props = build\HarfBuzzSharp.props + build\iOSWorkarounds.props = build\iOSWorkarounds.props build\JetBrains.Annotations.props = build\JetBrains.Annotations.props build\JetBrains.dotMemoryUnit.props = build\JetBrains.dotMemoryUnit.props build\Magick.NET-Q16-AnyCPU.props = build\Magick.NET-Q16-AnyCPU.props @@ -136,17 +139,24 @@ Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Props", "Props", "{F3AC8BC1 build\NetCore.props = build\NetCore.props build\NetFX.props = build\NetFX.props build\ReactiveUI.props = build\ReactiveUI.props + build\ReferenceCoreLibraries.props = build\ReferenceCoreLibraries.props build\Rx.props = build\Rx.props build\SampleApp.props = build\SampleApp.props + build\SharedVersion.props = build\SharedVersion.props build\SharpDX.props = build\SharpDX.props build\SkiaSharp.props = build\SkiaSharp.props + build\SourceLink.props = build\SourceLink.props + build\System.Drawing.Common.props = build\System.Drawing.Common.props build\System.Memory.props = build\System.Memory.props + build\UnitTests.NetFX.props = build\UnitTests.NetFX.props build\XUnit.props = build\XUnit.props EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Targets", "Targets", "{4D6FAF79-58B4-482F-9122-0668C346364C}" ProjectSection(SolutionItems) = preProject build\UnitTests.NetCore.targets = build\UnitTests.NetCore.targets + build\BuildTargets.targets = build\BuildTargets.targets + build\LegacyProject.targets = build\LegacyProject.targets EndProjectSection EndProject Project("{2150E333-8FDC-42A3-9474-1A3956D46DE8}") = "Linux", "Linux", "{86C53C40-57AA-45B8-AD42-FAE0EFDF0F2B}" @@ -207,6 +217,12 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "NativeEmbedSample", "sample EndProject Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Avalonia.Themes.Fluent", "src\Avalonia.Themes.Fluent\Avalonia.Themes.Fluent.csproj", "{C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}" EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless", "src\Avalonia.Headless\Avalonia.Headless.csproj", "{8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Headless.Vnc", "src\Avalonia.Headless.Vnc\Avalonia.Headless.Vnc.csproj", "{B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}" +EndProject +Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Avalonia.Markup.Xaml.Loader", "src\Markup\Avalonia.Markup.Xaml.Loader\Avalonia.Markup.Xaml.Loader.csproj", "{909A8CBD-7D0E-42FD-B841-022AD8925820}" +EndProject Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -1826,6 +1842,54 @@ Global {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhone.Build.0 = Release|Any CPU {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {3278F3A9-9509-4A3F-A15B-BDC8B5BFF632}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhone.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhone.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|Any CPU.Build.0 = Release|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhone.ActiveCfg = Release|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhone.Build.0 = Release|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {8C89950F-F5D9-47FC-8066-CBC1EC3DF8FC}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhone.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhone.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|Any CPU.Build.0 = Release|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhone.ActiveCfg = Release|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhone.Build.0 = Release|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {B859AE7C-F34F-4A9E-88AE-E0E7229FDE1E}.Release|iPhoneSimulator.Build.0 = Release|Any CPU {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU {4D55985A-1EE2-4F25-AD39-6EA8BC04F8FB}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU @@ -1946,6 +2010,30 @@ Global {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhone.Build.0 = Release|Any CPU {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU {C42D2FC1-A531-4ED4-84B9-89AEC7C962FC}.Release|iPhoneSimulator.Build.0 = Release|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|Any CPU.ActiveCfg = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|Any CPU.Build.0 = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|iPhone.ActiveCfg = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|iPhone.Build.0 = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Ad-Hoc|iPhoneSimulator.Build.0 = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.AppStore|Any CPU.ActiveCfg = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.AppStore|Any CPU.Build.0 = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.AppStore|iPhone.ActiveCfg = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.AppStore|iPhone.Build.0 = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.AppStore|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.AppStore|iPhoneSimulator.Build.0 = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|Any CPU.Build.0 = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|iPhone.ActiveCfg = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|iPhone.Build.0 = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|iPhoneSimulator.ActiveCfg = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Debug|iPhoneSimulator.Build.0 = Debug|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|Any CPU.ActiveCfg = Release|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|Any CPU.Build.0 = Release|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhone.ActiveCfg = Release|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhone.Build.0 = Release|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.ActiveCfg = Release|Any CPU + {909A8CBD-7D0E-42FD-B841-022AD8925820}.Release|iPhoneSimulator.Build.0 = Release|Any CPU EndGlobalSection GlobalSection(SolutionProperties) = preSolution HideSolutionNode = FALSE @@ -2004,6 +2092,7 @@ Global {AF915D5C-AB00-4EA0-B5E6-001F4AE84E68} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {351337F5-D66F-461B-A957-4EF60BDB4BA6} = {C5A00AC3-B34C-4564-9BDD-2DA473EF4D8B} {3C84E04B-36CF-4D0D-B965-C26DD649D1F3} = {A0CC0258-D18C-4AB3-854F-7101680FC3F9} + {909A8CBD-7D0E-42FD-B841-022AD8925820} = {8B6A8209-894F-4BA1-B880-965FD453982C} EndGlobalSection GlobalSection(ExtensibilityGlobals) = postSolution SolutionGuid = {87366D66-1391-4D90-8999-95A620AD786A} diff --git a/Avalonia.v3.ncrunchsolution b/Avalonia.v3.ncrunchsolution index a2208a9a91d..bef7e45524d 100644 --- a/Avalonia.v3.ncrunchsolution +++ b/Avalonia.v3.ncrunchsolution @@ -3,6 +3,7 @@ tests\TestFiles\**.* src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Avalonia.Build.Tasks.dll + src\Avalonia.Build.Tasks\bin\Debug\netstandard2.0\Mono.Cecil.dll True .ncrunch diff --git a/Directory.Build.props b/Directory.Build.props index 1f26df9bbc1..b41f8c488e3 100644 --- a/Directory.Build.props +++ b/Directory.Build.props @@ -1,5 +1,6 @@ $(MSBuildThisFileDirectory)build-intermediate/nuget + $(MSBuildThisFileDirectory)\src\tools\Avalonia.Designer.HostApp\bin\$(Configuration)\netcoreapp2.0\Avalonia.Designer.HostApp.dll diff --git a/build/HarfBuzzSharp.props b/build/HarfBuzzSharp.props index 873048ef21d..88c4d362827 100644 --- a/build/HarfBuzzSharp.props +++ b/build/HarfBuzzSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/build/SkiaSharp.props b/build/SkiaSharp.props index 4def44cbd00..a8d9332c57b 100644 --- a/build/SkiaSharp.props +++ b/build/SkiaSharp.props @@ -1,6 +1,6 @@  - - + + diff --git a/dirs.proj b/dirs.proj index 4b3b1183f0c..26c8f54b23f 100644 --- a/dirs.proj +++ b/dirs.proj @@ -6,7 +6,7 @@ - + diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 6800ff7d689..9ff6130e5fa 100644 --- a/native/Avalonia.Native/inc/avalonia-native.h +++ b/native/Avalonia.Native/inc/avalonia-native.h @@ -205,6 +205,15 @@ enum AvnMenuItemToggleType Radio }; +enum AvnExtendClientAreaChromeHints +{ + AvnNoChrome = 0, + AvnSystemChrome = 0x01, + AvnPreferSystemChrome = 0x02, + AvnOSXThickTitleBar = 0x08, + AvnDefaultChrome = AvnSystemChrome, +}; + AVNCOM(IAvaloniaNativeFactory, 01) : IUnknown { public: @@ -279,6 +288,10 @@ AVNCOM(IAvnWindow, 04) : virtual IAvnWindowBase virtual HRESULT SetWindowState(AvnWindowState state) = 0; virtual HRESULT GetWindowState(AvnWindowState*ret) = 0; virtual HRESULT TakeFocusFromChildren() = 0; + virtual HRESULT SetExtendClientArea (bool enable) = 0; + virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) = 0; + virtual HRESULT GetExtendTitleBarHeight (double*ret) = 0; + virtual HRESULT SetExtendTitleBarHeight (double value) = 0; }; AVNCOM(IAvnWindowBaseEvents, 05) : IUnknown diff --git a/native/Avalonia.Native/src/OSX/Screens.mm b/native/Avalonia.Native/src/OSX/Screens.mm index 278daf9a189..455cfa2e415 100644 --- a/native/Avalonia.Native/src/OSX/Screens.mm +++ b/native/Avalonia.Native/src/OSX/Screens.mm @@ -4,6 +4,14 @@ { public: FORWARD_IUNKNOWN() + + private: + CGFloat PrimaryDisplayHeight() + { + return NSMaxY([[[NSScreen screens] firstObject] frame]); + } + +public: virtual HRESULT GetScreenCount (int* ret) override { @autoreleasepool @@ -25,15 +33,15 @@ virtual HRESULT GetScreen (int index, AvnScreen* ret) override auto screen = [[NSScreen screens] objectAtIndex:index]; - ret->Bounds.X = [screen frame].origin.x; - ret->Bounds.Y = [screen frame].origin.y; ret->Bounds.Height = [screen frame].size.height; ret->Bounds.Width = [screen frame].size.width; + ret->Bounds.X = [screen frame].origin.x; + ret->Bounds.Y = PrimaryDisplayHeight() - [screen frame].origin.y - ret->Bounds.Height; - ret->WorkingArea.X = [screen visibleFrame].origin.x; - ret->WorkingArea.Y = [screen visibleFrame].origin.y; ret->WorkingArea.Height = [screen visibleFrame].size.height; ret->WorkingArea.Width = [screen visibleFrame].size.width; + ret->WorkingArea.X = [screen visibleFrame].origin.x; + ret->WorkingArea.Y = ret->Bounds.Height - [screen visibleFrame].origin.y - ret->WorkingArea.Height; ret->PixelDensity = [screen backingScaleFactor]; diff --git a/native/Avalonia.Native/src/OSX/window.h b/native/Avalonia.Native/src/OSX/window.h index bdf3007a289..b1f64bca880 100644 --- a/native/Avalonia.Native/src/OSX/window.h +++ b/native/Avalonia.Native/src/OSX/window.h @@ -3,9 +3,6 @@ class WindowBaseImpl; -@interface AutoFitContentVisualEffectView : NSVisualEffectView -@end - @interface AvnView : NSView -(AvnView* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; -(NSEvent* _Nonnull) lastMouseDownEvent; @@ -15,6 +12,14 @@ class WindowBaseImpl; -(AvnPixelSize) getPixelSize; @end +@interface AutoFitContentView : NSView +-(AutoFitContentView* _Nonnull) initWithContent: (NSView* _Nonnull) content; +-(void) ShowTitleBar: (bool) show; +-(void) SetTitleBarHeightHint: (double) height; +-(void) SetContent: (NSView* _Nonnull) content; +-(void) ShowBlur: (bool) show; +@end + @interface AvnWindow : NSWindow +(void) closeAll; -(AvnWindow* _Nonnull) initWithParent: (WindowBaseImpl* _Nonnull) parent; @@ -27,6 +32,8 @@ class WindowBaseImpl; -(void) showWindowMenuWithAppMenu; -(void) applyMenu:(NSMenu* _Nullable)menu; -(double) getScaling; +-(double) getExtendedTitleBarHeight; +-(void) setIsExtended:(bool)value; @end struct INSWindowHolder diff --git a/native/Avalonia.Native/src/OSX/window.mm b/native/Avalonia.Native/src/OSX/window.mm index 7f8a6e13937..2d0ffbe4f0c 100644 --- a/native/Avalonia.Native/src/OSX/window.mm +++ b/native/Avalonia.Native/src/OSX/window.mm @@ -6,8 +6,6 @@ #include #include "rendertarget.h" - - class WindowBaseImpl : public virtual ComSingleObject, public INSWindowHolder { private: @@ -20,7 +18,7 @@ View = NULL; Window = NULL; } - NSVisualEffectView* VisualEffect; + AutoFitContentView* StandardContainer; AvnView* View; AvnWindow* Window; ComPtr BaseEvents; @@ -39,6 +37,7 @@ _glContext = gl; renderTarget = [[IOSurfaceRenderTarget alloc] initWithOpenGlContext: gl]; View = [[AvnView alloc] initWithParent:this]; + StandardContainer = [[AutoFitContentView new] initWithContent:View]; Window = [[AvnWindow alloc] initWithParent:this]; @@ -49,12 +48,8 @@ [Window setStyleMask:NSWindowStyleMaskBorderless]; [Window setBackingType:NSBackingStoreBuffered]; - VisualEffect = [AutoFitContentVisualEffectView new]; - [VisualEffect setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; - [VisualEffect setMaterial:NSVisualEffectMaterialLight]; - [VisualEffect setAutoresizesSubviews:true]; - - [Window setContentView: View]; + [Window setOpaque:false]; + [Window setContentView: StandardContainer]; } virtual HRESULT ObtainNSWindowHandle(void** ret) override @@ -410,12 +405,7 @@ virtual HRESULT CreateNativeControlHost(IAvnNativeControlHost** retOut) override virtual HRESULT SetBlurEnabled (bool enable) override { - [Window setContentView: enable ? VisualEffect : View]; - - if(enable) - { - [VisualEffect addSubview:View]; - } + [StandardContainer ShowBlur:enable]; return S_OK; } @@ -492,6 +482,8 @@ virtual void OnResized () bool _inSetWindowState; NSRect _preZoomSize; bool _transitioningWindowState; + bool _isClientAreaExtended; + AvnExtendClientAreaChromeHints _extendClientHints; FORWARD_IUNKNOWN() BEGIN_INTERFACE_MAP() @@ -505,6 +497,8 @@ virtual void OnResized () ComPtr WindowEvents; WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) { + _isClientAreaExtended = false; + _extendClientHints = AvnDefaultChrome; _fullScreenActive = false; _canResize = true; _decorations = SystemDecorationsFull; @@ -523,8 +517,20 @@ void HideOrShowTrafficLights () if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) { NSView *titlebarView = [subview subviews][0]; for (id button in titlebarView.subviews) { - if ([button isKindOfClass:[NSButton class]]) { - [button setHidden: (_decorations != SystemDecorationsFull)]; + if ([button isKindOfClass:[NSButton class]]) + { + if(_isClientAreaExtended) + { + auto wantsChrome = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + + [button setHidden: !wantsChrome]; + } + else + { + [button setHidden: (_decorations != SystemDecorationsFull)]; + } + + [button setWantsLayer:true]; } } } @@ -600,6 +606,35 @@ void WindowStateChanged () override if(_lastWindowState != state) { + if(_isClientAreaExtended) + { + if(_lastWindowState == FullScreen) + { + // we exited fs. + if(_extendClientHints & AvnOSXThickTitleBar) + { + Window.toolbar = [NSToolbar new]; + Window.toolbar.showsBaselineSeparator = false; + } + + [Window setTitlebarAppearsTransparent:true]; + + [StandardContainer setFrameSize: StandardContainer.frame.size]; + } + else if(state == FullScreen) + { + // we entered fs. + if(_extendClientHints & AvnOSXThickTitleBar) + { + Window.toolbar = nullptr; + } + + [Window setTitlebarAppearsTransparent:false]; + + [StandardContainer setFrameSize: StandardContainer.frame.size]; + } + } + _lastWindowState = state; WindowEvents->WindowStateChanged(state); } @@ -656,8 +691,6 @@ virtual HRESULT SetDecorations(SystemDecorations value) override return S_OK; } - auto currentFrame = [Window frame]; - UpdateStyle(); HideOrShowTrafficLights(); @@ -790,6 +823,81 @@ virtual HRESULT TakeFocusFromChildren () override return S_OK; if([Window isKeyWindow]) [Window makeFirstResponder: View]; + + return S_OK; + } + + virtual HRESULT SetExtendClientArea (bool enable) override + { + _isClientAreaExtended = enable; + + if(enable) + { + Window.titleVisibility = NSWindowTitleHidden; + + [Window setTitlebarAppearsTransparent:true]; + + auto wantsTitleBar = (_extendClientHints & AvnSystemChrome) || (_extendClientHints & AvnPreferSystemChrome); + + if (wantsTitleBar) + { + [StandardContainer ShowTitleBar:true]; + } + else + { + [StandardContainer ShowTitleBar:false]; + } + + if(_extendClientHints & AvnOSXThickTitleBar) + { + Window.toolbar = [NSToolbar new]; + Window.toolbar.showsBaselineSeparator = false; + } + else + { + Window.toolbar = nullptr; + } + } + else + { + Window.titleVisibility = NSWindowTitleVisible; + Window.toolbar = nullptr; + [Window setTitlebarAppearsTransparent:false]; + View.layer.zPosition = 0; + } + + [Window setIsExtended:enable]; + + HideOrShowTrafficLights(); + + UpdateStyle(); + + return S_OK; + } + + virtual HRESULT SetExtendClientAreaHints (AvnExtendClientAreaChromeHints hints) override + { + _extendClientHints = hints; + + SetExtendClientArea(_isClientAreaExtended); + return S_OK; + } + + virtual HRESULT GetExtendTitleBarHeight (double*ret) override + { + if(ret == nullptr) + { + return E_POINTER; + } + + *ret = [Window getExtendedTitleBarHeight]; + + return S_OK; + } + + virtual HRESULT SetExtendTitleBarHeight (double value) override + { + [StandardContainer SetTitleBarHeightHint:value]; return S_OK; } @@ -802,8 +910,9 @@ void EnterFullScreenMode () [Window setTitlebarAppearsTransparent:NO]; [Window setTitle:_lastTitle]; - [Window setStyleMask:NSWindowStyleMaskTitled | NSWindowStyleMaskResizable]; - + Window.styleMask = Window.styleMask | NSWindowStyleMaskTitled | NSWindowStyleMaskResizable; + Window.styleMask = Window.styleMask & ~NSWindowStyleMaskFullSizeContentView; + [Window toggleFullScreen:nullptr]; } @@ -951,19 +1060,120 @@ virtual NSWindowStyleMask GetStyle() override { s |= NSWindowStyleMaskMiniaturizable; } + + if(_isClientAreaExtended) + { + s |= NSWindowStyleMaskFullSizeContentView | NSWindowStyleMaskTexturedBackground; + } return s; } }; NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEventTrackingRunLoopMode, NSModalPanelRunLoopMode, NSRunLoopCommonModes, NSConnectionReplyMode, nil]; -@implementation AutoFitContentVisualEffectView +@implementation AutoFitContentView +{ + NSVisualEffectView* _titleBarMaterial; + NSBox* _titleBarUnderline; + NSView* _content; + NSVisualEffectView* _blurBehind; + double _titleBarHeightHint; + bool _settingSize; +} + +-(AutoFitContentView* _Nonnull) initWithContent:(NSView *)content +{ + _titleBarHeightHint = -1; + _content = content; + _settingSize = false; + + [self setAutoresizesSubviews:true]; + [self setWantsLayer:true]; + + _titleBarMaterial = [NSVisualEffectView new]; + [_titleBarMaterial setBlendingMode:NSVisualEffectBlendingModeWithinWindow]; + [_titleBarMaterial setMaterial:NSVisualEffectMaterialTitlebar]; + [_titleBarMaterial setWantsLayer:true]; + _titleBarMaterial.hidden = true; + + _titleBarUnderline = [NSBox new]; + _titleBarUnderline.boxType = NSBoxSeparator; + _titleBarUnderline.fillColor = [NSColor underPageBackgroundColor]; + _titleBarUnderline.hidden = true; + + [self addSubview:_titleBarMaterial]; + [self addSubview:_titleBarUnderline]; + + _blurBehind = [NSVisualEffectView new]; + [_blurBehind setBlendingMode:NSVisualEffectBlendingModeBehindWindow]; + [_blurBehind setMaterial:NSVisualEffectMaterialLight]; + [_blurBehind setWantsLayer:true]; + _blurBehind.hidden = true; + + [self addSubview:_blurBehind]; + [self addSubview:_content]; + + [self setWantsLayer:true]; + return self; +} + +-(void) ShowBlur:(bool)show +{ + _blurBehind.hidden = !show; +} + +-(void) ShowTitleBar: (bool) show +{ + _titleBarMaterial.hidden = !show; + _titleBarUnderline.hidden = !show; +} + +-(void) SetTitleBarHeightHint: (double) height +{ + _titleBarHeightHint = height; + + [self setFrameSize:self.frame.size]; +} + -(void)setFrameSize:(NSSize)newSize { - [super setFrameSize:newSize]; - if([[self subviews] count] == 0) + if(_settingSize) + { return; - [[self subviews][0] setFrameSize: newSize]; + } + + _settingSize = true; + [super setFrameSize:newSize]; + + [_blurBehind setFrameSize:newSize]; + [_content setFrameSize:newSize]; + + auto window = objc_cast([self window]); + + // TODO get actual titlebar size + + double height = _titleBarHeightHint == -1 ? [window getExtendedTitleBarHeight] : _titleBarHeightHint; + + NSRect tbar; + tbar.origin.x = 0; + tbar.origin.y = newSize.height - height; + tbar.size.width = newSize.width; + tbar.size.height = height; + + [_titleBarMaterial setFrame:tbar]; + tbar.size.height = height < 1 ? 0 : 1; + [_titleBarUnderline setFrame:tbar]; + _settingSize = false; +} + +-(void) SetContent: (NSView* _Nonnull) content +{ + if(content != nullptr) + { + [content removeFromSuperview]; + [self addSubview:content]; + _content = content; + } } @end @@ -1081,10 +1291,15 @@ -(void)setFrameSize:(NSSize)newSize _parent->UpdateCursor(); auto fsize = [self convertSizeToBacking: [self frame].size]; - _lastPixelSize.Width = (int)fsize.width; - _lastPixelSize.Height = (int)fsize.height; - [self updateRenderTarget]; - _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}); + + if(_lastPixelSize.Width != (int)fsize.width || _lastPixelSize.Height != (int)fsize.height) + { + _lastPixelSize.Width = (int)fsize.width; + _lastPixelSize.Height = (int)fsize.height; + [self updateRenderTarget]; + + _parent->BaseEvents->Resized(AvnSize{newSize.width, newSize.height}); + } } - (void)updateLayer @@ -1523,15 +1738,43 @@ @implementation AvnWindow bool _canBecomeKeyAndMain; bool _closed; bool _isEnabled; + bool _isExtended; AvnMenu* _menu; double _lastScaling; } +-(void) setIsExtended:(bool)value; +{ + _isExtended = value; +} + -(double) getScaling { return _lastScaling; } +-(double) getExtendedTitleBarHeight +{ + if(_isExtended) + { + for (id subview in self.contentView.superview.subviews) + { + if ([subview isKindOfClass:NSClassFromString(@"NSTitlebarContainerView")]) + { + NSView *titlebarView = [subview subviews][0]; + + return (double)titlebarView.frame.size.height; + } + } + + return -1; + } + else + { + return 0; + } +} + +(void)closeAll { NSArray* windows = [NSArray arrayWithArray:[NSApp windows]]; @@ -1650,6 +1893,7 @@ -(AvnWindow*) initWithParent: (WindowBaseImpl*) parent [self setOpaque:NO]; [self setBackgroundColor: [NSColor clearColor]]; [self invalidateShadow]; + _isExtended = false; return self; } diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 890967ae4ff..fe877dc49c3 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -268,6 +268,8 @@ void RunCoreTest(string projectName) .DependsOn(CreateIntermediateNugetPackages) .Executes(() => { + BuildTasksPatcher.PatchBuildTasksInPackage(Parameters.NugetIntermediateRoot / "Avalonia.Build.Tasks." + + Parameters.Version + ".nupkg"); var config = Numerge.MergeConfiguration.LoadFile(RootDirectory / "nukebuild" / "numerge.config"); EnsureCleanDirectory(Parameters.NugetRoot); if(!Numerge.NugetPackageMerger.Merge(Parameters.NugetIntermediateRoot, Parameters.NugetRoot, config, diff --git a/nukebuild/BuildTasksPatcher.cs b/nukebuild/BuildTasksPatcher.cs new file mode 100644 index 00000000000..44f01da6690 --- /dev/null +++ b/nukebuild/BuildTasksPatcher.cs @@ -0,0 +1,76 @@ +using System; +using System.IO; +using System.IO.Compression; +using System.Linq; +using ILRepacking; +using Mono.Cecil; + +public class BuildTasksPatcher +{ + public static void PatchBuildTasksInPackage(string packagePath) + { + using (var archive = new ZipArchive(File.Open(packagePath, FileMode.Open, FileAccess.ReadWrite), + ZipArchiveMode.Update)) + { + + foreach (var entry in archive.Entries.ToList()) + { + if (entry.Name == "Avalonia.Build.Tasks.dll") + { + var temp = Path.Combine(Path.GetTempPath(), Guid.NewGuid() + ".dll"); + var output = temp + ".output"; + var patched = new MemoryStream(); + try + { + entry.ExtractToFile(temp, true); + var repack = new ILRepacking.ILRepack(new RepackOptions() + { + Internalize = true, + InputAssemblies = new[] + { + temp, typeof(Mono.Cecil.AssemblyDefinition).Assembly.GetModules()[0] + .FullyQualifiedName + }, + SearchDirectories = new string[0], + OutputFile = output + }); + repack.Repack(); + + + // 'hurr-durr assembly with the same name is already loaded' prevention + using (var asm = AssemblyDefinition.ReadAssembly(output, + new ReaderParameters { ReadWrite = true, InMemory = true, })) + { + asm.Name = new AssemblyNameDefinition( + "Avalonia.Build.Tasks." + + Guid.NewGuid().ToString().Replace("-", ""), + new Version(0, 0, 0)); + asm.Write(patched); + patched.Position = 0; + } + } + finally + { + try + { + if (File.Exists(temp)) + File.Delete(temp); + if (File.Exists(output)) + File.Delete(output); + } + catch + { + //ignore + } + } + + var fn = entry.FullName; + entry.Delete(); + var newEntry = archive.CreateEntry(fn, CompressionLevel.Optimal); + using (var s = newEntry.Open()) + patched.CopyTo(s); + } + } + } + } +} \ No newline at end of file diff --git a/nukebuild/_build.csproj b/nukebuild/_build.csproj index 584c36d0337..4c64d4ff932 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -14,6 +14,9 @@ + + + diff --git a/readme.md b/readme.md index 6a04c7e31ec..19a9a8420dc 100644 --- a/readme.md +++ b/readme.md @@ -16,7 +16,7 @@ To see the status of some of our features, please see our [Roadmap](https://gith ## 🚀 Getting Started -The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starer guide see our [documentation](http://avaloniaui.net/docs/quickstart/create-new-project). +The Avalonia [Visual Studio Extension](https://marketplace.visualstudio.com/items?itemName=AvaloniaTeam.AvaloniaforVisualStudio) contains project and control templates that will help you get started, or you can use the .NET Core CLI. For a starter guide see our [documentation](http://avaloniaui.net/docs/quickstart/create-new-project). Avalonia is delivered via NuGet package manager. You can find the packages here: https://www.nuget.org/packages/Avalonia/ diff --git a/samples/BindingDemo/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml index 26a62ebca62..14c371efef9 100644 --- a/samples/BindingDemo/MainWindow.xaml +++ b/samples/BindingDemo/MainWindow.xaml @@ -5,7 +5,8 @@ xmlns:local="clr-namespace:BindingDemo" Title="AvaloniaUI Bindings Test" Width="800" - Height="600"> + Height="600" + x:DataType="vm:MainWindowViewModel"> + @@ -55,6 +55,7 @@ + @@ -66,6 +67,7 @@ + @@ -79,7 +81,7 @@ Simple - Light Simple - Dark - + None Transparent Blur diff --git a/samples/ControlCatalog/MainView.xaml.cs b/samples/ControlCatalog/MainView.xaml.cs index d6d4a71ad3c..b0c205246e2 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -58,13 +58,6 @@ public MainView() if (VisualRoot is Window window) window.SystemDecorations = (SystemDecorations)decorations.SelectedIndex; }; - - var transparencyLevels = this.Find("TransparencyLevels"); - transparencyLevels.SelectionChanged += (sender, e) => - { - if (VisualRoot is Window window) - window.TransparencyLevelHint = (WindowTransparencyLevel)transparencyLevels.SelectedIndex; - }; } protected override void OnAttachedToVisualTree(VisualTreeAttachmentEventArgs e) diff --git a/samples/ControlCatalog/MainWindow.xaml b/samples/ControlCatalog/MainWindow.xaml index 76422bc130e..97bd88f5e43 100644 --- a/samples/ControlCatalog/MainWindow.xaml +++ b/samples/ControlCatalog/MainWindow.xaml @@ -7,7 +7,12 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" xmlns:vm="clr-namespace:ControlCatalog.ViewModels" xmlns:v="clr-namespace:ControlCatalog.Views" - x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}" Background="{DynamicResource SystemControlPageBackgroundAltHighBrush}"> + ExtendClientAreaToDecorationsHint="{Binding ExtendClientAreaEnabled}" + ExtendClientAreaChromeHints="{Binding ChromeHints}" + ExtendClientAreaTitleBarHeightHint="{Binding TitleBarHeight}" + TransparencyLevelHint="{Binding TransparencyLevel}" + x:Name="MainWindow" + x:Class="ControlCatalog.MainWindow" WindowState="{Binding WindowState, Mode=TwoWay}"> @@ -56,20 +61,30 @@ - + - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml b/samples/ControlCatalog/Pages/AcrylicPage.xaml new file mode 100644 index 00000000000..96cfcc5288f --- /dev/null +++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml @@ -0,0 +1,139 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/AcrylicPage.xaml.cs b/samples/ControlCatalog/Pages/AcrylicPage.xaml.cs new file mode 100644 index 00000000000..44e7c4b92be --- /dev/null +++ b/samples/ControlCatalog/Pages/AcrylicPage.xaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class AcrylicPage : UserControl + { + public AcrylicPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/Pages/BorderPage.xaml b/samples/ControlCatalog/Pages/BorderPage.xaml index 8133d0e408c..bfd14cc627d 100644 --- a/samples/ControlCatalog/Pages/BorderPage.xaml +++ b/samples/ControlCatalog/Pages/BorderPage.xaml @@ -9,27 +9,27 @@ Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16"> - + Border - Border and Background - Rounded Corners - Rounded Corners - diff --git a/samples/ControlCatalog/Pages/ButtonPage.xaml b/samples/ControlCatalog/Pages/ButtonPage.xaml index 8b697b79481..a4427c70c51 100644 --- a/samples/ControlCatalog/Pages/ButtonPage.xaml +++ b/samples/ControlCatalog/Pages/ButtonPage.xaml @@ -9,10 +9,10 @@ Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16"> - - + + - + - - - - + + + + diff --git a/samples/ControlCatalog/Pages/ComboBoxPage.xaml b/samples/ControlCatalog/Pages/ComboBoxPage.xaml index 486cc55d445..369f7037186 100644 --- a/samples/ControlCatalog/Pages/ComboBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ComboBoxPage.xaml @@ -16,7 +16,7 @@ - + Control Items diff --git a/samples/ControlCatalog/Pages/ContextMenuPage.xaml b/samples/ControlCatalog/Pages/ContextMenuPage.xaml index 8f147638b51..8ccd8e97f73 100644 --- a/samples/ControlCatalog/Pages/ContextMenuPage.xaml +++ b/samples/ControlCatalog/Pages/ContextMenuPage.xaml @@ -9,7 +9,7 @@ Margin="0,16,0,0" HorizontalAlignment="Center" Spacing="16"> - @@ -34,7 +34,7 @@ - diff --git a/samples/ControlCatalog/Pages/DragAndDropPage.xaml b/samples/ControlCatalog/Pages/DragAndDropPage.xaml index 65a798e53c8..c22cf68b682 100644 --- a/samples/ControlCatalog/Pages/DragAndDropPage.xaml +++ b/samples/ControlCatalog/Pages/DragAndDropPage.xaml @@ -10,15 +10,15 @@ HorizontalAlignment="Center" Spacing="16"> - + Drag Me - + Drag Me (custom) - Drop some text or files here diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml index e600e644af9..304782dbf9d 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml @@ -1,6 +1,30 @@ + + + + + + + + + + + + + + + + ItemsRepeater @@ -23,16 +47,8 @@ - - - - - - - + diff --git a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs index cce80a2d3cd..9e898c45369 100644 --- a/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs +++ b/samples/ControlCatalog/Pages/ItemsRepeaterPage.xaml.cs @@ -38,6 +38,12 @@ private void InitializeComponent() AvaloniaXamlLoader.Load(this); } + public void OnSelectTemplateKey(object sender, SelectTemplateEventArgs e) + { + var item = (ItemsRepeaterPageViewModel.Item)e.DataContext; + e.TemplateKey = (item.Index % 2 == 0) ? "even" : "odd"; + } + private void LayoutChanged(object sender, SelectionChangedEventArgs e) { if (_repeater == null) @@ -79,6 +85,26 @@ private void LayoutChanged(object sender, SelectionChangedEventArgs e) MinItemHeight = 200, }; break; + case 4: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Auto; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Disabled; + _repeater.Layout = new WrapLayout + { + Orientation = Orientation.Vertical, + HorizontalSpacing = 20, + VerticalSpacing = 20 + }; + break; + case 5: + _scroller.HorizontalScrollBarVisibility = ScrollBarVisibility.Disabled; + _scroller.VerticalScrollBarVisibility = ScrollBarVisibility.Auto; + _repeater.Layout = new WrapLayout + { + Orientation = Orientation.Horizontal, + HorizontalSpacing = 20, + VerticalSpacing = 20 + }; + break; } } diff --git a/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml b/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml index 446dfd7ce17..8cf3610b472 100644 --- a/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml +++ b/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml @@ -11,10 +11,10 @@ RowDefinitions="24,Auto,24" HorizontalAlignment="Center" VerticalAlignment="Center"> - - - - + + + + diff --git a/samples/ControlCatalog/Pages/ListBoxPage.xaml b/samples/ControlCatalog/Pages/ListBoxPage.xaml index 47b4ce7151c..f4d81418ac8 100644 --- a/samples/ControlCatalog/Pages/ListBoxPage.xaml +++ b/samples/ControlCatalog/Pages/ListBoxPage.xaml @@ -10,7 +10,13 @@ HorizontalAlignment="Center" Spacing="16"> - + diff --git a/samples/ControlCatalog/Pages/RelativePanelPage.axaml b/samples/ControlCatalog/Pages/RelativePanelPage.axaml new file mode 100644 index 00000000000..3657d52bd9d --- /dev/null +++ b/samples/ControlCatalog/Pages/RelativePanelPage.axaml @@ -0,0 +1,60 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs b/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs new file mode 100644 index 00000000000..11d0a5152e0 --- /dev/null +++ b/samples/ControlCatalog/Pages/RelativePanelPage.axaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class RelativePanelPage : UserControl + { + public RelativePanelPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/Pages/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml index f73ef9b4fb0..4b8edcf98c3 100644 --- a/samples/ControlCatalog/Pages/TextBlockPage.xaml +++ b/samples/ControlCatalog/Pages/TextBlockPage.xaml @@ -64,51 +64,42 @@ - - - - - - - - - - - - + StrokeThicknessUnit="Pixel" + StrokeThickness="2"> + + + + + + + + - - - - - - - - - - - - + StrokeThicknessUnit="Pixel" + StrokeThickness="1"> + + + + + + + + - - - - - - - - - - - - + StrokeThicknessUnit="Pixel" + StrokeThickness="2"> + + + + + + + + diff --git a/samples/ControlCatalog/Pages/ToolTipPage.xaml b/samples/ControlCatalog/Pages/ToolTipPage.xaml index 73d83e08f1a..ec073d48a9b 100644 --- a/samples/ControlCatalog/Pages/ToolTipPage.xaml +++ b/samples/ControlCatalog/Pages/ToolTipPage.xaml @@ -12,7 +12,7 @@ HorizontalAlignment="Center"> @@ -26,7 +26,7 @@ diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml new file mode 100644 index 00000000000..b90f43c3b6c --- /dev/null +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml @@ -0,0 +1,19 @@ + + + + + + + + None + Transparent + Blur + AcrylicBlur + + + diff --git a/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs new file mode 100644 index 00000000000..d8d4f3f371e --- /dev/null +++ b/samples/ControlCatalog/Pages/WindowCustomizationsPage.xaml.cs @@ -0,0 +1,19 @@ +using Avalonia; +using Avalonia.Controls; +using Avalonia.Markup.Xaml; + +namespace ControlCatalog.Pages +{ + public class WindowCustomizationsPage : UserControl + { + public WindowCustomizationsPage() + { + this.InitializeComponent(); + } + + private void InitializeComponent() + { + AvaloniaXamlLoader.Load(this); + } + } +} diff --git a/samples/ControlCatalog/SideBar.xaml b/samples/ControlCatalog/SideBar.xaml index 1ee6bf7a293..7c911e91e9e 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -12,7 +12,7 @@ diff --git a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs index 73aaeff9944..f893a6e28e9 100644 --- a/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs +++ b/samples/ControlCatalog/ViewModels/ItemsRepeaterPageViewModel.cs @@ -62,13 +62,9 @@ private ObservableCollection CreateItems() public class Item : ReactiveObject { private double _height = double.NaN; - private int _index; - - public Item(int index) - { - _index = index; - } + public Item(int index) => Index = index; + public int Index { get; } public string Text { get; set; } public double Height @@ -76,8 +72,6 @@ public double Height get => _height; set => this.RaiseAndSetIfChanged(ref _height, value); } - - public IBrush Background => ((_index % 2) == 0) ? Brushes.Yellow : Brushes.Wheat; } } } diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 0257b4ce663..4356a032fad 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -3,6 +3,8 @@ using Avalonia.Controls.ApplicationLifetimes; using Avalonia.Controls.Notifications; using Avalonia.Dialogs; +using Avalonia.Platform; +using System; using ReactiveUI; namespace ControlCatalog.ViewModels @@ -14,6 +16,12 @@ class MainWindowViewModel : ReactiveObject private bool _isMenuItemChecked = true; private WindowState _windowState; private WindowState[] _windowStates; + private int _transparencyLevel; + private ExtendClientAreaChromeHints _chromeHints; + private bool _extendClientAreaEnabled; + private bool _systemTitleBarEnabled; + private bool _preferSystemChromeEnabled; + private double _titleBarHeight; public MainWindowViewModel(IManagedNotificationManager notificationManager) { @@ -62,6 +70,63 @@ public MainWindowViewModel(IManagedNotificationManager notificationManager) WindowState.Maximized, WindowState.FullScreen, }; + + this.WhenAnyValue(x => x.SystemTitleBarEnabled, x=>x.PreferSystemChromeEnabled) + .Subscribe(x => + { + var hints = ExtendClientAreaChromeHints.NoChrome | ExtendClientAreaChromeHints.OSXThickTitleBar; + + if(x.Item1) + { + hints |= ExtendClientAreaChromeHints.SystemChrome; + } + + if(x.Item2) + { + hints |= ExtendClientAreaChromeHints.PreferSystemChrome; + } + + ChromeHints = hints; + }); + + SystemTitleBarEnabled = true; + TitleBarHeight = -1; + } + + public int TransparencyLevel + { + get { return _transparencyLevel; } + set { this.RaiseAndSetIfChanged(ref _transparencyLevel, value); } + } + + public ExtendClientAreaChromeHints ChromeHints + { + get { return _chromeHints; } + set { this.RaiseAndSetIfChanged(ref _chromeHints, value); } + } + + public bool ExtendClientAreaEnabled + { + get { return _extendClientAreaEnabled; } + set { this.RaiseAndSetIfChanged(ref _extendClientAreaEnabled, value); } + } + + public bool SystemTitleBarEnabled + { + get { return _systemTitleBarEnabled; } + set { this.RaiseAndSetIfChanged(ref _systemTitleBarEnabled, value); } + } + + public bool PreferSystemChromeEnabled + { + get { return _preferSystemChromeEnabled; } + set { this.RaiseAndSetIfChanged(ref _preferSystemChromeEnabled, value); } + } + + public double TitleBarHeight + { + get { return _titleBarHeight; } + set { this.RaiseAndSetIfChanged(ref _titleBarHeight, value); } } public WindowState WindowState diff --git a/samples/RenderDemo/App.xaml b/samples/RenderDemo/App.xaml index ccc3f54cc0b..61e4d2385b1 100644 --- a/samples/RenderDemo/App.xaml +++ b/samples/RenderDemo/App.xaml @@ -3,8 +3,7 @@ xmlns:x="http://schemas.microsoft.com/winfx/2006/xaml" x:Class="RenderDemo.App"> - - + - \ No newline at end of file + diff --git a/samples/RenderDemo/Pages/ClippingPage.xaml b/samples/RenderDemo/Pages/ClippingPage.xaml index 8920b8a1235..10225f7c491 100644 --- a/samples/RenderDemo/Pages/ClippingPage.xaml +++ b/samples/RenderDemo/Pages/ClippingPage.xaml @@ -43,7 +43,7 @@ C 72.078834 28.113269 74.047517 25.960974 74.931641 23.777344 C 78.93827 14.586564 73.049722 2.8815081 63.248047 0.67382812 C 61.721916 0.22817968 60.165597 0.038541919 58.625 0.07421875 z "> - + @@ -53,4 +53,4 @@ Apply Geometry Clip - \ No newline at end of file + diff --git a/samples/RenderDemo/SideBar.xaml b/samples/RenderDemo/SideBar.xaml index d6bde0b1466..fd23067f611 100644 --- a/samples/RenderDemo/SideBar.xaml +++ b/samples/RenderDemo/SideBar.xaml @@ -1,65 +1,68 @@ - + - - - - - + + + + + + diff --git a/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj index c623ae68b5a..cc831ef8aeb 100644 --- a/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj +++ b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs index 69fd2a9f135..71dce93ce7a 100644 --- a/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs +++ b/src/Android/Avalonia.Android/Platform/SkiaPlatform/TopLevelImpl.cs @@ -126,7 +126,7 @@ public virtual void Show() _view.Visibility = ViewStates.Visible; } - public double Scaling => 1; + public double RenderScaling => 1; void Draw() { diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 09480f2701b..a873d5fd428 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -2,6 +2,7 @@ using System.Collections.Generic; using System.Reactive.Subjects; using Avalonia.Data; +using Avalonia.Data.Core; using Avalonia.Utilities; namespace Avalonia @@ -9,7 +10,7 @@ namespace Avalonia /// /// Base class for avalonia properties. /// - public abstract class AvaloniaProperty : IEquatable + public abstract class AvaloniaProperty : IEquatable, IPropertyInfo { /// /// Represents an unset property value. @@ -582,6 +583,11 @@ private PropertyMetadata GetMetadataWithOverrides(Type type) return _defaultMetadata; } + + bool IPropertyInfo.CanGet => true; + bool IPropertyInfo.CanSet => true; + object IPropertyInfo.Get(object target) => ((AvaloniaObject)target).GetValue(this); + void IPropertyInfo.Set(object target, object value) => ((AvaloniaObject)target).SetValue(this, value); } /// diff --git a/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs b/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs new file mode 100644 index 00000000000..f66411c2c2d --- /dev/null +++ b/src/Avalonia.Base/Data/Core/ClrPropertyInfo.cs @@ -0,0 +1,73 @@ +using System; +using System.Linq.Expressions; +using System.Reflection; + +namespace Avalonia.Data.Core +{ + public class ClrPropertyInfo : IPropertyInfo + { + private readonly Func _getter; + private readonly Action _setter; + + public ClrPropertyInfo(string name, Func getter, Action setter, Type propertyType) + { + _getter = getter; + _setter = setter; + PropertyType = propertyType; + Name = name; + } + + public string Name { get; } + public Type PropertyType { get; } + + public object Get(object target) + { + if (_getter == null) + throw new NotSupportedException("Property " + Name + " doesn't have a getter"); + return _getter(target); + } + + public void Set(object target, object value) + { + if (_setter == null) + throw new NotSupportedException("Property " + Name + " doesn't have a setter"); + _setter(target, value); + } + + public bool CanSet => _setter != null; + public bool CanGet => _getter != null; + } + + public class ReflectionClrPropertyInfo : ClrPropertyInfo + { + static Action CreateSetter(PropertyInfo info) + { + if (info.SetMethod == null) + return null; + var target = Expression.Parameter(typeof(object), "target"); + var value = Expression.Parameter(typeof(object), "value"); + return Expression.Lambda>( + Expression.Call(Expression.Convert(target, info.DeclaringType), info.SetMethod, + Expression.Convert(value, info.SetMethod.GetParameters()[0].ParameterType)), + target, value) + .Compile(); + } + + static Func CreateGetter(PropertyInfo info) + { + if (info.GetMethod == null) + return null; + var target = Expression.Parameter(typeof(object), "target"); + return Expression.Lambda>( + Expression.Convert(Expression.Call(Expression.Convert(target, info.DeclaringType), info.GetMethod), + typeof(object))) + .Compile(); + } + + public ReflectionClrPropertyInfo(PropertyInfo info) : base(info.Name, + CreateGetter(info), CreateSetter(info), info.PropertyType) + { + + } + } +} diff --git a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs index 3bacd38a200..7ecaa278d70 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -54,6 +54,7 @@ public class ExpressionObserver : LightweightObservableBase, IDescriptio private object _root; private IDisposable _rootSubscription; private WeakReference _value; + private IReadOnlyList _transformNodes; /// /// Initializes a new instance of the class. @@ -188,6 +189,24 @@ public static ExpressionObserver Create( description ?? expression.ToString()); } + private IReadOnlyList GetTransformNodesFromChain() + { + LinkedList transforms = new LinkedList(); + var node = _node; + while (node != null) + { + if (node is ITransformNode transform) + { + transforms.AddFirst(transform); + } + node = node.Next; + } + + return new List(transforms); + } + + private IReadOnlyList TransformNodes => (_transformNodes ?? (_transformNodes = GetTransformNodesFromChain())); + /// /// Attempts to set the value of a property expression. /// @@ -203,18 +222,13 @@ public bool SetValue(object value, BindingPriority priority = BindingPriority.Lo { if (Leaf is SettableNode settable) { - var node = _node; - while (node != null) + foreach (var transform in TransformNodes) { - if (node is ITransformNode transform) + value = transform.Transform(value); + if (value is BindingNotification) { - value = transform.Transform(value); - if (value is BindingNotification) - { - return false; - } + return false; } - node = node.Next; } return settable.SetTargetValue(value, priority); } diff --git a/src/Avalonia.Base/Data/Core/IPropertyInfo.cs b/src/Avalonia.Base/Data/Core/IPropertyInfo.cs new file mode 100644 index 00000000000..2417d0ffc47 --- /dev/null +++ b/src/Avalonia.Base/Data/Core/IPropertyInfo.cs @@ -0,0 +1,14 @@ +using System; + +namespace Avalonia.Data.Core +{ + public interface IPropertyInfo + { + string Name { get; } + object Get(object target); + void Set(object target, object value); + bool CanSet { get; } + bool CanGet { get; } + Type PropertyType { get; } + } +} diff --git a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs index dad0832e126..debf050a972 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs @@ -59,7 +59,7 @@ public virtual IObservable Start(WeakReference reference) return Observable.Empty(); } - protected IObservable HandleCompleted(Task task) + private IObservable HandleCompleted(Task task) { var resultProperty = task.GetType().GetRuntimeProperty("Result"); diff --git a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs index aa4fe55f79e..d5e835cabd3 100644 --- a/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs +++ b/src/Avalonia.Base/Data/Core/PropertyAccessorNode.cs @@ -7,6 +7,7 @@ namespace Avalonia.Data.Core public class PropertyAccessorNode : SettableNode { private readonly bool _enableValidation; + private IPropertyAccessorPlugin _customPlugin; private IPropertyAccessor _accessor; public PropertyAccessorNode(string propertyName, bool enableValidation) @@ -15,6 +16,13 @@ public PropertyAccessorNode(string propertyName, bool enableValidation) _enableValidation = enableValidation; } + public PropertyAccessorNode(string propertyName, bool enableValidation, IPropertyAccessorPlugin customPlugin) + { + PropertyName = propertyName; + _enableValidation = enableValidation; + _customPlugin = customPlugin; + } + public override string Description => PropertyName; public string PropertyName { get; } public override Type PropertyType => _accessor?.PropertyType; @@ -37,17 +45,7 @@ protected override void StartListeningCore(WeakReference reference) { reference.TryGetTarget(out object target); - IPropertyAccessorPlugin plugin = null; - - foreach (IPropertyAccessorPlugin x in ExpressionObserver.PropertyAccessors) - { - if (x.Match(target, PropertyName)) - { - plugin = x; - break; - } - } - + var plugin = _customPlugin ?? GetPropertyAccessorPluginForObject(target); var accessor = plugin?.Start(reference, PropertyName); // We need to handle accessor fallback before handling validation. Validators do not support null accessors. @@ -82,6 +80,18 @@ protected override void StartListeningCore(WeakReference reference) accessor.Subscribe(ValueChanged); } + private IPropertyAccessorPlugin GetPropertyAccessorPluginForObject(object target) + { + foreach (IPropertyAccessorPlugin x in ExpressionObserver.PropertyAccessors) + { + if (x.Match(target, PropertyName)) + { + return x; + } + } + return null; + } + protected override void StopListeningCore() { _accessor.Dispose(); diff --git a/src/Avalonia.Base/Data/Core/PropertyPath.cs b/src/Avalonia.Base/Data/Core/PropertyPath.cs new file mode 100644 index 00000000000..665953c4a1d --- /dev/null +++ b/src/Avalonia.Base/Data/Core/PropertyPath.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace Avalonia.Data.Core +{ + public class PropertyPath + { + public IReadOnlyList Elements { get; } + + public PropertyPath(IEnumerable elements) + { + Elements = elements.ToList(); + } + } + + public class PropertyPathBuilder + { + readonly List _elements = new List(); + + public PropertyPathBuilder Property(IPropertyInfo property) + { + _elements.Add(new PropertyPropertyPathElement(property)); + return this; + } + + + public PropertyPathBuilder ChildTraversal() + { + _elements.Add(new ChildTraversalPropertyPathElement()); + return this; + } + + public PropertyPathBuilder EnsureType(Type type) + { + _elements.Add(new EnsureTypePropertyPathElement(type)); + return this; + } + + public PropertyPathBuilder Cast(Type type) + { + _elements.Add(new CastTypePropertyPathElement(type)); + return this; + } + + public PropertyPath Build() + { + return new PropertyPath(_elements); + } + } + + public interface IPropertyPathElement + { + + } + + public class PropertyPropertyPathElement : IPropertyPathElement + { + public IPropertyInfo Property { get; } + + public PropertyPropertyPathElement(IPropertyInfo property) + { + Property = property; + } + } + + public class ChildTraversalPropertyPathElement : IPropertyPathElement + { + + } + + public class EnsureTypePropertyPathElement : IPropertyPathElement + { + public Type Type { get; } + + public EnsureTypePropertyPathElement(Type type) + { + Type = type; + } + } + + public class CastTypePropertyPathElement : IPropertyPathElement + { + public CastTypePropertyPathElement(Type type) + { + Type = type; + } + + public Type Type { get; } + } +} diff --git a/src/Avalonia.Base/Data/Core/StreamNode.cs b/src/Avalonia.Base/Data/Core/StreamNode.cs index e501c0a03d1..023999f5c51 100644 --- a/src/Avalonia.Base/Data/Core/StreamNode.cs +++ b/src/Avalonia.Base/Data/Core/StreamNode.cs @@ -1,35 +1,54 @@ using System; using System.Reactive.Linq; +using Avalonia.Data.Core.Plugins; namespace Avalonia.Data.Core { public class StreamNode : ExpressionNode { + private IStreamPlugin _customPlugin = null; private IDisposable _subscription; public override string Description => "^"; + public StreamNode() { } + + public StreamNode(IStreamPlugin customPlugin) + { + _customPlugin = customPlugin; + } + protected override void StartListeningCore(WeakReference reference) { + GetPlugin(reference)?.Start(reference).Subscribe(ValueChanged); + } + + protected override void StopListeningCore() + { + _subscription?.Dispose(); + _subscription = null; + } + + private IStreamPlugin GetPlugin(WeakReference reference) + { + if (_customPlugin != null) + { + return _customPlugin; + } + foreach (var plugin in ExpressionObserver.StreamHandlers) { if (plugin.Match(reference)) { - _subscription = plugin.Start(reference).Subscribe(ValueChanged); - return; + return plugin; } } - // TODO: Improve error. + // TODO: Improve error ValueChanged(new BindingNotification( new MarkupBindingChainException("Stream operator applied to unsupported type", Description), BindingErrorType.Error)); - } - - protected override void StopListeningCore() - { - _subscription?.Dispose(); - _subscription = null; + return null; } } } diff --git a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs index 38a23f918f7..40cf81358fe 100644 --- a/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs +++ b/src/Avalonia.Base/Threading/AvaloniaSynchronizationContext.cs @@ -1,3 +1,5 @@ +using System; +using System.Runtime.ConstrainedExecution; using System.Threading; namespace Avalonia.Threading @@ -7,6 +9,20 @@ namespace Avalonia.Threading /// public class AvaloniaSynchronizationContext : SynchronizationContext { + public interface INonPumpingPlatformWaitProvider + { + int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout); + } + + private readonly INonPumpingPlatformWaitProvider _waitProvider; + + public AvaloniaSynchronizationContext(INonPumpingPlatformWaitProvider waitProvider) + { + _waitProvider = waitProvider; + if (_waitProvider != null) + SetWaitNotificationRequired(); + } + /// /// Controls if SynchronizationContext should be installed in InstallIfNeeded. Used by Designer. /// @@ -22,7 +38,8 @@ public static void InstallIfNeeded() return; } - SetSynchronizationContext(new AvaloniaSynchronizationContext()); + SetSynchronizationContext(new AvaloniaSynchronizationContext(AvaloniaLocator.Current + .GetService())); } /// @@ -39,5 +56,13 @@ public override void Send(SendOrPostCallback d, object state) else Dispatcher.UIThread.InvokeAsync(() => d(state), DispatcherPriority.Send).Wait(); } + + [PrePrepareMethod] + public override int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) + { + if (_waitProvider != null) + return _waitProvider.Wait(waitHandles, waitAll, millisecondsTimeout); + return base.Wait(waitHandles, waitAll, millisecondsTimeout); + } } } diff --git a/src/Avalonia.Base/Utilities/CharacterReader.cs b/src/Avalonia.Base/Utilities/CharacterReader.cs index 0a05915802f..2b98785d0db 100644 --- a/src/Avalonia.Base/Utilities/CharacterReader.cs +++ b/src/Avalonia.Base/Utilities/CharacterReader.cs @@ -79,5 +79,25 @@ public ReadOnlySpan TakeWhile(Func condition) Position += len; return span; } + + public ReadOnlySpan TryPeek(int count) + { + if (_s.Length < count) + return ReadOnlySpan.Empty; + return _s.Slice(0, count); + } + + public ReadOnlySpan PeekWhitespace() + { + var trimmed = _s.TrimStart(); + return _s.Slice(0, _s.Length - trimmed.Length); + } + + public void Skip(int count) + { + if (_s.Length < count) + throw new IndexOutOfRangeException(); + _s = _s.Slice(count); + } } } diff --git a/src/Avalonia.Base/Utilities/KeywordParser.cs b/src/Avalonia.Base/Utilities/KeywordParser.cs new file mode 100644 index 00000000000..16ef95f5f49 --- /dev/null +++ b/src/Avalonia.Base/Utilities/KeywordParser.cs @@ -0,0 +1,46 @@ +using System; + +namespace Avalonia.Utilities +{ +#if !BUILDTASK + public +#endif + static class KeywordParser + { + public static bool CheckKeyword(this ref CharacterReader r, string keyword) + { + return (CheckKeywordInternal(ref r, keyword) >= 0); + } + + static int CheckKeywordInternal(this ref CharacterReader r, string keyword) + { + var ws = r.PeekWhitespace(); + + var chars = r.TryPeek(ws.Length + keyword.Length); + if (chars.IsEmpty) + return -1; + if (SpanEquals(chars.Slice(ws.Length), keyword.AsSpan())) + return chars.Length; + return -1; + } + + static bool SpanEquals(ReadOnlySpan left, ReadOnlySpan right) + { + if (left.Length != right.Length) + return false; + for(var c=0; cexe false tools - $(DefineConstants);BUILDTASK;XAMLIL_CECIL_INTERNAL;XAMLIL_INTERNAL + $(DefineConstants);BUILDTASK;XAMLX_CECIL_INTERNAL;XAMLX_INTERNAL true NU1605 @@ -17,19 +17,22 @@ Shared/AvaloniaResourceXamlInfo.cs - + XamlIlExtensions/%(RecursiveDir)%(FileName)%(Extension) - + XamlIl/%(RecursiveDir)%(FileName)%(Extension) - + XamlIl.Cecil/%(RecursiveDir)%(FileName)%(Extension) Markup/%(RecursiveDir)%(FileName)%(Extension) + + Markup/%(RecursiveDir)%(FileName)%(Extension) + Markup/%(RecursiveDir)%(FileName)%(Extension) @@ -38,36 +41,25 @@ Markup/%(RecursiveDir)%(FileName)%(Extension) - + Markup/%(RecursiveDir)%(FileName)%(Extension) + + Markup/%(RecursiveDir)%(FileName)%(Extension) + + + Markup/%(RecursiveDir)%(FileName)%(Extension) + + + Markup/%(RecursiveDir)%(FileName)%(Extension) + Markup/%(RecursiveDir)%(FileName)%(Extension) - - - + + - - - - - - $(MSBuildThisFileDirectory)bin\$(Configuration)\$(TargetFramework) - - - - - - - - - - - - - diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs index f94f10f7927..5a2c74e16f5 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs @@ -5,7 +5,7 @@ using Mono.Cecil; using Mono.Cecil.Cil; using Mono.Collections.Generic; -using XamlIl.TypeSystem; +using XamlX.TypeSystem; namespace Avalonia.Build.Tasks { diff --git a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs index 30e8f120d7c..c1cc0e7bf0b 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -7,17 +7,18 @@ using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions; using Microsoft.Build.Framework; using Mono.Cecil; -using XamlIl.TypeSystem; using Avalonia.Utilities; using Mono.Cecil.Cil; using Mono.Cecil.Rocks; -using XamlIl; -using XamlIl.Ast; -using XamlIl.Parsers; -using XamlIl.Transform; +using XamlX; +using XamlX.Ast; +using XamlX.Parsers; +using XamlX.Transform; +using XamlX.TypeSystem; using FieldAttributes = Mono.Cecil.FieldAttributes; using MethodAttributes = Mono.Cecil.MethodAttributes; using TypeAttributes = Mono.Cecil.TypeAttributes; +using XamlX.IL; namespace Avalonia.Build.Tasks { @@ -50,23 +51,32 @@ public static CompileResult Compile(IBuildEngine engine, string input, string[] if (avares.Resources.Count(CheckXamlName) == 0 && emres.Resources.Count(CheckXamlName) == 0) // Nothing to do return new CompileResult(true); - - var xamlLanguage = AvaloniaXamlIlLanguage.Configure(typeSystem); - var compilerConfig = new XamlIlTransformerConfiguration(typeSystem, + + var clrPropertiesDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlHelpers", + TypeAttributes.Class, asm.MainModule.TypeSystem.Object); + asm.MainModule.Types.Add(clrPropertiesDef); + var indexerAccessorClosure = new TypeDefinition("CompiledAvaloniaXaml", "!IndexerAccessorFactoryClosure", + TypeAttributes.Class, asm.MainModule.TypeSystem.Object); + asm.MainModule.Types.Add(indexerAccessorClosure); + + var (xamlLanguage , emitConfig) = AvaloniaXamlIlLanguage.Configure(typeSystem); + var compilerConfig = new AvaloniaXamlIlCompilerConfiguration(typeSystem, typeSystem.TargetAssembly, xamlLanguage, - XamlIlXmlnsMappings.Resolve(typeSystem, xamlLanguage), - AvaloniaXamlIlLanguage.CustomValueConverter); + XamlXmlnsMappings.Resolve(typeSystem, xamlLanguage), + AvaloniaXamlIlLanguage.CustomValueConverter, + new XamlIlClrPropertyInfoEmitter(typeSystem.CreateTypeBuilder(clrPropertiesDef)), + new XamlIlPropertyInfoAccessorFactoryEmitter(typeSystem.CreateTypeBuilder(indexerAccessorClosure))); var contextDef = new TypeDefinition("CompiledAvaloniaXaml", "XamlIlContext", TypeAttributes.Class, asm.MainModule.TypeSystem.Object); asm.MainModule.Types.Add(contextDef); - var contextClass = XamlIlContextDefinition.GenerateContextClass(typeSystem.CreateTypeBuilder(contextDef), typeSystem, - xamlLanguage); + var contextClass = XamlILContextDefinition.GenerateContextClass(typeSystem.CreateTypeBuilder(contextDef), typeSystem, + xamlLanguage, emitConfig); - var compiler = new AvaloniaXamlIlCompiler(compilerConfig, contextClass) { EnableIlVerification = verifyIl }; + var compiler = new AvaloniaXamlIlCompiler(compilerConfig, emitConfig, contextClass) { EnableIlVerification = verifyIl }; var editorBrowsableAttribute = typeSystem .GetTypeReference(typeSystem.FindType("System.ComponentModel.EditorBrowsableAttribute")) @@ -126,35 +136,35 @@ bool CompileGroup(IResourceGroup group) // StreamReader is needed here to handle BOM var xaml = new StreamReader(new MemoryStream(res.FileContents)).ReadToEnd(); - var parsed = XDocumentXamlIlParser.Parse(xaml); + var parsed = XDocumentXamlParser.Parse(xaml); - var initialRoot = (XamlIlAstObjectNode)parsed.Root; + var initialRoot = (XamlAstObjectNode)parsed.Root; - var precompileDirective = initialRoot.Children.OfType() + var precompileDirective = initialRoot.Children.OfType() .FirstOrDefault(d => d.Namespace == XamlNamespaces.Xaml2006 && d.Name == "Precompile"); if (precompileDirective != null) { - var precompileText = (precompileDirective.Values[0] as XamlIlAstTextNode)?.Text.Trim() + var precompileText = (precompileDirective.Values[0] as XamlAstTextNode)?.Text.Trim() .ToLowerInvariant(); if (precompileText == "false") continue; if (precompileText != "true") - throw new XamlIlParseException("Invalid value for x:Precompile", precompileDirective); + throw new XamlParseException("Invalid value for x:Precompile", precompileDirective); } - var classDirective = initialRoot.Children.OfType() + var classDirective = initialRoot.Children.OfType() .FirstOrDefault(d => d.Namespace == XamlNamespaces.Xaml2006 && d.Name == "Class"); - IXamlIlType classType = null; + IXamlType classType = null; if (classDirective != null) { - if (classDirective.Values.Count != 1 || !(classDirective.Values[0] is XamlIlAstTextNode tn)) - throw new XamlIlParseException("x:Class should have a string value", classDirective); + if (classDirective.Values.Count != 1 || !(classDirective.Values[0] is XamlAstTextNode tn)) + throw new XamlParseException("x:Class should have a string value", classDirective); classType = typeSystem.TargetAssembly.FindType(tn.Text); if (classType == null) - throw new XamlIlParseException($"Unable to find type `{tn.Text}`", classDirective); + throw new XamlParseException($"Unable to find type `{tn.Text}`", classDirective); compiler.OverrideRootType(parsed, - new XamlIlAstClrTypeReference(classDirective, classType, false)); + new XamlAstClrTypeReference(classDirective, classType, false)); initialRoot.Children.Remove(classDirective); } @@ -323,7 +333,7 @@ bool CompileGroup(IResourceGroup group) catch (Exception e) { int lineNumber = 0, linePosition = 0; - if (e is XamlIlParseException xe) + if (e is XamlParseException xe) { lineNumber = xe.LineNumber; linePosition = xe.LinePosition; diff --git a/src/Avalonia.Controls.DataGrid/DataGrid.cs b/src/Avalonia.Controls.DataGrid/DataGrid.cs index 7e893d131ec..7e921944eae 100644 --- a/src/Avalonia.Controls.DataGrid/DataGrid.cs +++ b/src/Avalonia.Controls.DataGrid/DataGrid.cs @@ -3920,6 +3920,14 @@ void SetValidationStatus(ICellEditBinding binding) dataGridColumn: CurrentColumn, dataGridRow: EditingRow, dataGridCell: editingCell); + + EditingRow.InvalidateDesiredHeight(); + var column = editingCell.OwningColumn; + if (column.Width.IsSizeToCells || column.Width.IsAuto) + {// Invalidate desired width and force recalculation + column.SetWidthDesiredValue(0); + EditingRow.OwningGrid.AutoSizeColumn(column, editingCell.DesiredSize.Width); + } } // We're done, so raise the CellEditEnded event diff --git a/src/Avalonia.Controls.DataGrid/DataGridRow.cs b/src/Avalonia.Controls.DataGrid/DataGridRow.cs index 830eff1102f..d5115c983ac 100644 --- a/src/Avalonia.Controls.DataGrid/DataGridRow.cs +++ b/src/Avalonia.Controls.DataGrid/DataGridRow.cs @@ -767,6 +767,11 @@ internal void InvalidateHorizontalArrange() } } + internal void InvalidateDesiredHeight() + { + _cellsElement?.InvalidateDesiredHeight(); + } + internal void ResetGridLine() { _bottomGridLine = null; diff --git a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs index 6e0703c90ff..c5fe9f0cb2a 100644 --- a/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs +++ b/src/Avalonia.Controls.DataGrid/Primitives/DataGridCellsPresenter.cs @@ -299,6 +299,11 @@ internal void Recycle() DesiredHeight = 0; } + internal void InvalidateDesiredHeight() + { + DesiredHeight = 0; + } + private bool ShouldDisplayCell(DataGridColumn column, double frozenLeftEdge, double scrollingLeftEdge) { if (!column.IsVisible) diff --git a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index 17e7ecba433..738732f6710 100644 --- a/src/Avalonia.Controls.DataGrid/Themes/Default.xaml +++ b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml @@ -188,7 +188,7 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/ComboBox.xaml b/src/Avalonia.Themes.Default/ComboBox.xaml index 95bd9550a50..ae5b902ae8c 100644 --- a/src/Avalonia.Themes.Default/ComboBox.xaml +++ b/src/Avalonia.Themes.Default/ComboBox.xaml @@ -1,10 +1,29 @@ + + + + + Item 1 + Item 2 + + + Item 1 + Item 2 + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Default/ToggleSwitch.xaml b/src/Avalonia.Themes.Default/ToggleSwitch.xaml index 88266ac9798..ded121f5f6b 100644 --- a/src/Avalonia.Themes.Default/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Default/ToggleSwitch.xaml @@ -44,7 +44,7 @@ - + diff --git a/src/Avalonia.Themes.Default/Window.xaml b/src/Avalonia.Themes.Default/Window.xaml index 0e7ec428563..1856eec2065 100644 --- a/src/Avalonia.Themes.Default/Window.xaml +++ b/src/Avalonia.Themes.Default/Window.xaml @@ -1,21 +1,25 @@ diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 88040a69e74..6a2a04f7326 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml @@ -261,9 +261,7 @@ 1 32 0,0 - 12,0,0,0 - 12,4,12,4 @@ -273,31 +271,12 @@ - - - @@ -307,40 +286,6 @@ 1 - - - - - - @@ -490,6 +435,15 @@ + + + + + + + + + 1 @@ -725,74 +679,33 @@ - - XamlAutoFontFamily - XamlAutoFontFamily - 24 - 40 - 14 - -25 - 12,0,12,0 - 12,0,12,0 - 12,14,0,13 - 0 - 0,6,0,0 - 12,14,0,13 - SemiLight - Bold - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - + + XamlAutoFontFamily + 24 + 40 + -25 + 12,0,12,0 + 12,0,12,0 + SemiLight + + + + + + + + + + + + + + + + + + + 0 @@ -888,5 +801,11 @@ 32 + + + + + + 1 diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml index d29db79e7bc..9ef92a44d5a 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentDark.xaml @@ -1,9 +1,9 @@ - - - - - + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml index 43867f6e973..8c920401224 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentLight.xaml @@ -1,9 +1,9 @@ - - - - - + + + + + diff --git a/src/Avalonia.Themes.Fluent/Assets/Roboto-Black.ttf b/src/Avalonia.Themes.Fluent/Assets/Roboto-Black.ttf new file mode 100644 index 00000000000..38f809991d8 Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/Roboto-Black.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/Roboto-Bold.ttf b/src/Avalonia.Themes.Fluent/Assets/Roboto-Bold.ttf new file mode 100644 index 00000000000..5fd9dd9eedf Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/Roboto-Bold.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/Roboto-Light.ttf b/src/Avalonia.Themes.Fluent/Assets/Roboto-Light.ttf new file mode 100644 index 00000000000..113d545eea2 Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/Roboto-Light.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/Roboto-Medium.ttf b/src/Avalonia.Themes.Fluent/Assets/Roboto-Medium.ttf new file mode 100644 index 00000000000..98c4538462d Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/Roboto-Medium.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/Roboto-Regular.ttf b/src/Avalonia.Themes.Fluent/Assets/Roboto-Regular.ttf new file mode 100644 index 00000000000..c1c3700fe55 Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/Roboto-Regular.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/Roboto-Thin.ttf b/src/Avalonia.Themes.Fluent/Assets/Roboto-Thin.ttf new file mode 100644 index 00000000000..04b15402d93 Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/Roboto-Thin.ttf differ diff --git a/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml b/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml index 44e7962e170..0d5d733cd9c 100644 --- a/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml +++ b/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml @@ -22,8 +22,7 @@ - - + diff --git a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj index 84bf799d8d0..c2a1359e8a2 100644 --- a/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj +++ b/src/Avalonia.Themes.Fluent/Avalonia.Themes.Fluent.csproj @@ -11,13 +11,10 @@ - - - - - - + + + - \ No newline at end of file + diff --git a/src/Avalonia.Themes.Fluent/Button.xaml b/src/Avalonia.Themes.Fluent/Button.xaml index 345a74512c0..34bcd0f0c14 100644 --- a/src/Avalonia.Themes.Fluent/Button.xaml +++ b/src/Avalonia.Themes.Fluent/Button.xaml @@ -19,8 +19,7 @@ - - + + + + - + - + - + - - - + - + - + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ComboBoxItem.xaml b/src/Avalonia.Themes.Fluent/ComboBoxItem.xaml index c26dec3d34b..66ccea99b52 100644 --- a/src/Avalonia.Themes.Fluent/ComboBoxItem.xaml +++ b/src/Avalonia.Themes.Fluent/ComboBoxItem.xaml @@ -1,17 +1,19 @@ - - + + Item 1 - Item 2 - - - - Item 1 - Item 2 - - + Item 2 long + Item 3 + Item 4 + + @@ -19,12 +21,11 @@ - - + Padding="{TemplateBinding Padding}" /> + + + + + + + + - + - diff --git a/src/Avalonia.Themes.Fluent/ContextMenu.xaml b/src/Avalonia.Themes.Fluent/ContextMenu.xaml index 44783a8dea0..a6b61569440 100644 --- a/src/Avalonia.Themes.Fluent/ContextMenu.xaml +++ b/src/Avalonia.Themes.Fluent/ContextMenu.xaml @@ -1,7 +1,7 @@ - - diff --git a/src/Avalonia.Themes.Fluent/NotificationCard.xaml b/src/Avalonia.Themes.Fluent/NotificationCard.xaml index 47d5988e8c3..5a0fa326577 100644 --- a/src/Avalonia.Themes.Fluent/NotificationCard.xaml +++ b/src/Avalonia.Themes.Fluent/NotificationCard.xaml @@ -1,92 +1,104 @@  - + + + + + + + + + + + + + + + + + + + + - + - + - + - - - - - + + + + + diff --git a/src/Avalonia.Themes.Fluent/NumericUpDown.xaml b/src/Avalonia.Themes.Fluent/NumericUpDown.xaml index 08de50c6e32..cc5e2234f8b 100644 --- a/src/Avalonia.Themes.Fluent/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Fluent/NumericUpDown.xaml @@ -25,8 +25,7 @@ - - + @@ -44,7 +43,7 @@ - + diff --git a/src/Avalonia.Themes.Fluent/ProgressBar.xaml b/src/Avalonia.Themes.Fluent/ProgressBar.xaml index ac5dd7c0edf..036595b61c1 100644 --- a/src/Avalonia.Themes.Fluent/ProgressBar.xaml +++ b/src/Avalonia.Themes.Fluent/ProgressBar.xaml @@ -142,35 +142,15 @@ diff --git a/src/Avalonia.Themes.Fluent/RadioButton.xaml b/src/Avalonia.Themes.Fluent/RadioButton.xaml index 23bcfc616a1..acde4ea0be4 100644 --- a/src/Avalonia.Themes.Fluent/RadioButton.xaml +++ b/src/Avalonia.Themes.Fluent/RadioButton.xaml @@ -17,8 +17,7 @@ - - + diff --git a/src/Avalonia.Themes.Fluent/RepeatButton.xaml b/src/Avalonia.Themes.Fluent/RepeatButton.xaml index 12ba38d614a..5afaf05e9f4 100644 --- a/src/Avalonia.Themes.Fluent/RepeatButton.xaml +++ b/src/Avalonia.Themes.Fluent/RepeatButton.xaml @@ -17,8 +17,7 @@ - - + diff --git a/src/Avalonia.Themes.Fluent/TabStripItem.xaml b/src/Avalonia.Themes.Fluent/TabStripItem.xaml index d45f705a400..628ab8ddddd 100644 --- a/src/Avalonia.Themes.Fluent/TabStripItem.xaml +++ b/src/Avalonia.Themes.Fluent/TabStripItem.xaml @@ -14,11 +14,11 @@ diff --git a/src/Avalonia.Themes.Fluent/TextBox.xaml b/src/Avalonia.Themes.Fluent/TextBox.xaml index 49fc4b59b07..0327e776e37 100644 --- a/src/Avalonia.Themes.Fluent/TextBox.xaml +++ b/src/Avalonia.Themes.Fluent/TextBox.xaml @@ -8,74 +8,67 @@ - - + - - - + - - - - - - - - - - - - - - - - - - - - + + + + + + + + + + + + + + + + + + + - - + - - - - - - + + + + + + + @@ -103,7 +97,6 @@ diff --git a/src/Avalonia.Themes.Fluent/TimePicker.xaml b/src/Avalonia.Themes.Fluent/TimePicker.xaml index 4a411796d8f..f82e194a73d 100644 --- a/src/Avalonia.Themes.Fluent/TimePicker.xaml +++ b/src/Avalonia.Themes.Fluent/TimePicker.xaml @@ -31,8 +31,7 @@ - + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/ToggleButton.xaml b/src/Avalonia.Themes.Fluent/ToggleButton.xaml index a3c300f6f58..f2f07d3e2ac 100644 --- a/src/Avalonia.Themes.Fluent/ToggleButton.xaml +++ b/src/Avalonia.Themes.Fluent/ToggleButton.xaml @@ -20,8 +20,7 @@ - - + diff --git a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml index 88266ac9798..4309edefe3f 100644 --- a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml @@ -44,7 +44,7 @@ - + @@ -156,8 +156,7 @@ - - - @@ -264,10 +259,6 @@ - - diff --git a/src/Avalonia.Themes.Fluent/ToolTip.xaml b/src/Avalonia.Themes.Fluent/ToolTip.xaml index 47ad494bbfd..f7a1ebbc6b9 100644 --- a/src/Avalonia.Themes.Fluent/ToolTip.xaml +++ b/src/Avalonia.Themes.Fluent/ToolTip.xaml @@ -45,8 +45,7 @@ - - + diff --git a/src/Avalonia.Themes.Fluent/Window.xaml b/src/Avalonia.Themes.Fluent/Window.xaml index aee15347eb4..1856eec2065 100644 --- a/src/Avalonia.Themes.Fluent/Window.xaml +++ b/src/Avalonia.Themes.Fluent/Window.xaml @@ -1,21 +1,25 @@ + - + - + - + - + diff --git a/src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs b/src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs new file mode 100644 index 00000000000..34cede40185 --- /dev/null +++ b/src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs @@ -0,0 +1,20 @@ +namespace Avalonia.Media +{ + /// + /// Background Sources for Acrylic. + /// + public enum AcrylicBackgroundSource + { + /// + /// The acrylic has no background. + /// + None, + + /// + /// Cuts through all render layers to reveal the window background. + /// This means if your window is transparent or blurred it + /// will be blended with the material. + /// + Digger + } +} diff --git a/src/Avalonia.Visuals/Media/Color.cs b/src/Avalonia.Visuals/Media/Color.cs index 052ee5e1b7e..16b4f90d579 100644 --- a/src/Avalonia.Visuals/Media/Color.cs +++ b/src/Avalonia.Visuals/Media/Color.cs @@ -89,6 +89,11 @@ public static Color FromUInt32(uint value) /// The . public static Color Parse(string s) { + if (s is null) + { + throw new ArgumentNullException(nameof(s)); + } + if (TryParse(s, out Color color)) { return color; @@ -120,14 +125,16 @@ public static Color Parse(ReadOnlySpan s) /// The status of the operation. public static bool TryParse(string s, out Color color) { - if (s == null) + color = default; + + if (s is null) { - throw new ArgumentNullException(nameof(s)); + return false; } if (s.Length == 0) { - throw new FormatException(); + return false; } if (s[0] == '#' && TryParseInternal(s.AsSpan(), out color)) @@ -144,8 +151,6 @@ public static bool TryParse(string s, out Color color) return true; } - color = default; - return false; } diff --git a/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs new file mode 100644 index 00000000000..6b699acd2e2 --- /dev/null +++ b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs @@ -0,0 +1,345 @@ +using System; + +namespace Avalonia.Media +{ + public class ExperimentalAcrylicMaterial : AvaloniaObject, IMutableExperimentalAcrylicMaterial + { + private Color _effectiveTintColor; + private Color _effectiveLuminosityColor; + + static ExperimentalAcrylicMaterial() + { + AffectsRender( + TintColorProperty, + BackgroundSourceProperty, + TintOpacityProperty, + MaterialOpacityProperty, + PlatformTransparencyCompensationLevelProperty); + + TintColorProperty.Changed.AddClassHandler((b, e) => + { + b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); + b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); + }); + + TintOpacityProperty.Changed.AddClassHandler((b, e) => + { + b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); + b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); + }); + + MaterialOpacityProperty.Changed.AddClassHandler((b, e) => + { + b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); + b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); + }); + + PlatformTransparencyCompensationLevelProperty.Changed.AddClassHandler((b, e) => + { + b._effectiveTintColor = GetEffectiveTintColor(b.TintColor, b.TintOpacity); + b._effectiveLuminosityColor = b.GetEffectiveLuminosityColor(); + }); + } + + /// + /// Defines the property. + /// + public static readonly StyledProperty TintColorProperty = + AvaloniaProperty.Register(nameof(TintColor)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty BackgroundSourceProperty = + AvaloniaProperty.Register(nameof(BackgroundSource)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty TintOpacityProperty = + AvaloniaProperty.Register(nameof(TintOpacity), 0.8); + + /// + /// Defines the property. + /// + public static readonly StyledProperty MaterialOpacityProperty = + AvaloniaProperty.Register(nameof(MaterialOpacity), 0.5); + + /// + /// Defines the property. + /// + public static readonly StyledProperty PlatformTransparencyCompensationLevelProperty = + AvaloniaProperty.Register(nameof(PlatformTransparencyCompensationLevel), 0.0); + + /// + /// Defines the property. + /// + public static readonly StyledProperty FallbackColorProperty = + AvaloniaProperty.Register(nameof(FallbackColor)); + + /// + public event EventHandler Invalidated; + + /// + /// Gets or Sets the BackgroundSource . + /// + public AcrylicBackgroundSource BackgroundSource + { + get => GetValue(BackgroundSourceProperty); + set => SetValue(BackgroundSourceProperty, value); + } + + /// + /// Gets or Sets the TintColor. + /// + public Color TintColor + { + get => GetValue(TintColorProperty); + set => SetValue(TintColorProperty, value); + } + + /// + /// Gets or Sets the Tint Opacity. + /// + public double TintOpacity + { + get => GetValue(TintOpacityProperty); + set => SetValue(TintOpacityProperty, value); + } + + /// + /// Gets or Sets the Fallback Color. + /// This is used on rendering plaforms that dont support acrylic. + /// + public Color FallbackColor + { + get => GetValue(FallbackColorProperty); + set => SetValue(FallbackColorProperty, value); + } + + /// + /// Gets or Sets the MaterialOpacity. + /// This makes the material more or less opaque. + /// + public double MaterialOpacity + { + get => GetValue(MaterialOpacityProperty); + set => SetValue(MaterialOpacityProperty, value); + } + + /// + /// Gets or Sets the PlatformTransparencyCompensationLevel. + /// This value defines the minimum that can be used. + /// It means material opacity is re-scaled from this value to 1. + /// + public double PlatformTransparencyCompensationLevel + { + get => GetValue(PlatformTransparencyCompensationLevelProperty); + set => SetValue(PlatformTransparencyCompensationLevelProperty, value); + } + + Color IExperimentalAcrylicMaterial.MaterialColor => _effectiveLuminosityColor; + + Color IExperimentalAcrylicMaterial.TintColor => _effectiveTintColor; + + private struct HsvColor + { + public float Hue { get; set; } + public float Saturation { get; set; } + public float Value { get; set; } + } + + private static HsvColor RgbToHsv(Color color) + { + var r = color.R / 255.0f; + var g = color.G / 255.0f; + var b = color.B / 255.0f; + var max = Math.Max(r, Math.Max(g, b)); + var min = Math.Min(r, Math.Min(g, b)); + + float h, s, v; + h = v = max; + + var d = max - min; + s = max == 0 ? 0 : d / max; + + if (max == min) + { + h = 0; // achromatic + } + else + { + if (max == r) + { + h = (g - b) / d + (g < b ? 6 : 0); + } + else if (max == g) + { + h = (b - r) / d + 2; + } + else if (max == b) + { + h = (r - g) / d + 4; + } + + h /= 6; + } + + return new HsvColor { Hue = h, Saturation = s, Value = v }; + } + + private static Color GetEffectiveTintColor(Color tintColor, double tintOpacity) + { + // Update tintColor's alpha with the combined opacity value + double tintOpacityModifier = GetTintOpacityModifier(tintColor); + + return new Color((byte)(255 * ((255.0 / tintColor.A) * tintOpacity) * tintOpacityModifier), tintColor.R, tintColor.G, tintColor.B); + } + + private static double GetTintOpacityModifier(Color tintColor) + { + // This method supresses the maximum allowable tint opacity depending on the luminosity and saturation of a color by + // compressing the range of allowable values - for example, a user-defined value of 100% will be mapped to 45% for pure + // white (100% luminosity), 85% for pure black (0% luminosity), and 90% for pure gray (50% luminosity). The intensity of + // the effect increases linearly as luminosity deviates from 50%. After this effect is calculated, we cancel it out + // linearly as saturation increases from zero. + + const double midPoint = 0.5; // Mid point of HsvV range that these calculations are based on. This is here for easy tuning. + + const double whiteMaxOpacity = 0.2; // 100% luminosity + const double midPointMaxOpacity = 0.45; // 50% luminosity + const double blackMaxOpacity = 0.45; // 0% luminosity + + var hsv = RgbToHsv(tintColor); + + double opacityModifier = midPointMaxOpacity; + + if (hsv.Value != midPoint) + { + // Determine maximum suppression amount + double lowestMaxOpacity = midPointMaxOpacity; + double maxDeviation = midPoint; + + if (hsv.Value > midPoint) + { + lowestMaxOpacity = whiteMaxOpacity; // At white (100% hsvV) + maxDeviation = 1 - maxDeviation; + } + else if (hsv.Value < midPoint) + { + lowestMaxOpacity = blackMaxOpacity; // At black (0% hsvV) + } + + double maxOpacitySuppression = midPointMaxOpacity - lowestMaxOpacity; + + // Determine normalized deviation from the midpoint + double deviation = Math.Abs(hsv.Value - midPoint); + double normalizedDeviation = deviation / maxDeviation; + + // If we have saturation, reduce opacity suppression to allow that color to come through more + if (hsv.Saturation > 0) + { + // Dampen opacity suppression based on how much saturation there is + maxOpacitySuppression *= Math.Max(1 - (hsv.Saturation * 2), 0.0); + } + + double opacitySuppression = maxOpacitySuppression * normalizedDeviation; + + opacityModifier = midPointMaxOpacity - opacitySuppression; + } + + return opacityModifier; + } + + private Color GetEffectiveLuminosityColor() + { + double? luminosityOpacity = MaterialOpacity; + + return GetLuminosityColor(luminosityOpacity); + } + + private static byte Trim(double value) + { + value = Math.Min(Math.Floor(value * 256), 255); + + if (value < 0) + { + return 0; + } + else if (value > 255) + { + return 255; + } + + return (byte)value; + } + + private static float RGBMax(Color color) + { + if (color.R > color.G) + return (color.R > color.B) ? color.R : color.B; + else + return (color.G > color.B) ? color.G : color.B; + } + + private static float RGBMin(Color color) + { + if (color.R < color.G) + return (color.R < color.B) ? color.R : color.B; + else + return (color.G < color.B) ? color.G : color.B; + } + + // The tintColor passed into this method should be the original, unmodified color created using user values for TintColor + TintOpacity + private Color GetLuminosityColor(double? luminosityOpacity) + { + // Calculate the HSL lightness value of the color. + var max = (float)RGBMax(TintColor) / 255.0f; + var min = (float)RGBMin(TintColor) / 255.0f; + + var lightness = (max + min) / 2.0; + + lightness = 1 - ((1 - lightness) * luminosityOpacity.Value); + + lightness = 0.13 + (lightness * 0.74); + + var luminosityColor = new Color(255, Trim(lightness), Trim(lightness), Trim(lightness)); + + var compensationMultiplier = 1 - PlatformTransparencyCompensationLevel; + return new Color((byte)(255 * Math.Max(Math.Min(PlatformTransparencyCompensationLevel + (luminosityOpacity.Value * compensationMultiplier), 1.0), 0.0)), luminosityColor.R, luminosityColor.G, luminosityColor.B); + } + + /// + /// Marks a property as affecting the brush's visual representation. + /// + /// The properties. + /// + /// After a call to this method in a brush's static constructor, any change to the + /// property will cause the event to be raised on the brush. + /// + protected static void AffectsRender(params AvaloniaProperty[] properties) + where T : ExperimentalAcrylicMaterial + { + static void Invalidate(AvaloniaPropertyChangedEventArgs e) + { + (e.Sender as T)?.RaiseInvalidated(EventArgs.Empty); + } + + foreach (var property in properties) + { + property.Changed.Subscribe(e => Invalidate(e)); + } + } + + /// + /// Raises the event. + /// + /// The event args. + protected void RaiseInvalidated(EventArgs e) => Invalidated?.Invoke(this, e); + + public IExperimentalAcrylicMaterial ToImmutable() + { + return new ImmutableExperimentalAcrylicMaterial(this); + } + } +} diff --git a/src/Avalonia.Visuals/Media/FontManager.cs b/src/Avalonia.Visuals/Media/FontManager.cs index f9410afe6a6..ad3fee7eb7f 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -79,12 +79,13 @@ public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = fa /// Returns a new typeface, or an existing one if a matching typeface exists. /// /// The font family. - /// The font weight. /// The font style. + /// The font weight. /// /// The typeface. /// - public Typeface GetOrAddTypeface(FontFamily fontFamily, FontWeight fontWeight = FontWeight.Normal, FontStyle fontStyle = FontStyle.Normal) + public Typeface GetOrAddTypeface(FontFamily fontFamily, FontStyle fontStyle = FontStyle.Normal, + FontWeight fontWeight = FontWeight.Normal) { while (true) { @@ -93,14 +94,14 @@ public Typeface GetOrAddTypeface(FontFamily fontFamily, FontWeight fontWeight = fontFamily = _defaultFontFamily; } - var key = new FontKey(fontFamily.Name, fontWeight, fontStyle); + var key = new FontKey(fontFamily.Name, fontStyle, fontWeight); if (_typefaceCache.TryGetValue(key, out var typeface)) { return typeface; } - typeface = new Typeface(fontFamily, fontWeight, fontStyle); + typeface = new Typeface(fontFamily, fontStyle, fontWeight); if (_typefaceCache.TryAdd(key, typeface)) { @@ -121,15 +122,16 @@ public Typeface GetOrAddTypeface(FontFamily fontFamily, FontWeight fontWeight = /// Returns null if no fallback was found. /// /// The codepoint to match against. - /// The font weight. /// The font style. + /// The font weight. /// The font family. This is optional and used for fallback lookup. /// The culture. /// /// The matched typeface. /// - public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = FontWeight.Normal, + public Typeface MatchCharacter(int codepoint, FontStyle fontStyle = FontStyle.Normal, + FontWeight fontWeight = FontWeight.Normal, FontFamily fontFamily = null, CultureInfo culture = null) { foreach (var cachedTypeface in _typefaceCache.Values) @@ -142,8 +144,8 @@ public Typeface MatchCharacter(int codepoint, FontWeight fontWeight = FontWeight } } - var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? - _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Weight, key.Style)) : + var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontStyle, fontWeight, fontFamily, culture, out var key) ? + _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Style, key.Weight)) : null; return matchedTypeface; diff --git a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs index a8d81648baf..b330db84628 100644 --- a/src/Avalonia.Visuals/Media/Fonts/FontKey.cs +++ b/src/Avalonia.Visuals/Media/Fonts/FontKey.cs @@ -4,7 +4,7 @@ namespace Avalonia.Media.Fonts { public readonly struct FontKey : IEquatable { - public FontKey(string familyName, FontWeight weight, FontStyle style) + public FontKey(string familyName, FontStyle style, FontWeight weight) { FamilyName = familyName; Style = style; diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index 29c9d93560e..a32a3e1b6c7 100644 --- a/src/Avalonia.Visuals/Media/GlyphRun.cs +++ b/src/Avalonia.Visuals/Media/GlyphRun.cs @@ -1,7 +1,7 @@ using System; using System.Collections.Generic; using Avalonia.Platform; -using Avalonia.Utility; +using Avalonia.Utilities; namespace Avalonia.Media { @@ -205,13 +205,16 @@ public double GetDistanceFromCharacterHit(CharacterHit characterHit) var glyphIndex = FindGlyphIndex(characterHit.FirstCharacterIndex); - var currentCluster = _glyphClusters[glyphIndex]; - - if (characterHit.TrailingLength > 0) + if (!GlyphClusters.IsEmpty) { - while (glyphIndex < _glyphClusters.Length && _glyphClusters[glyphIndex] == currentCluster) + var currentCluster = GlyphClusters[glyphIndex]; + + if (characterHit.TrailingLength > 0) { - glyphIndex++; + while (glyphIndex < GlyphClusters.Length && GlyphClusters[glyphIndex] == currentCluster) + { + glyphIndex++; + } } } @@ -302,7 +305,7 @@ public CharacterHit GetCharacterHitFromDistance(double distance, out bool isInsi } } - var characterHit = FindNearestCharacterHit(GlyphClusters[index], out var width); + var characterHit = FindNearestCharacterHit(GlyphClusters.IsEmpty ? index : GlyphClusters[index], out var width); var offset = GetDistanceFromCharacterHit(new CharacterHit(characterHit.FirstCharacterIndex)); @@ -370,26 +373,31 @@ public int Compare(T x, T y) /// public int FindGlyphIndex(int characterIndex) { + if (GlyphClusters.IsEmpty) + { + return characterIndex; + } + if (IsLeftToRight) { - if (characterIndex < _glyphClusters[0]) + if (characterIndex < GlyphClusters[0]) { return 0; } - if (characterIndex > _glyphClusters[_glyphClusters.Length - 1]) + if (characterIndex > GlyphClusters[GlyphClusters.Length - 1]) { return _glyphClusters.End; } } else { - if (characterIndex < _glyphClusters[_glyphClusters.Length - 1]) + if (characterIndex < GlyphClusters[GlyphClusters.Length - 1]) { return _glyphClusters.End; } - if (characterIndex > _glyphClusters[0]) + if (characterIndex > GlyphClusters[0]) { return 0; } @@ -397,7 +405,7 @@ public int FindGlyphIndex(int characterIndex) var comparer = IsLeftToRight ? s_ascendingComparer : s_descendingComparer; - var clusters = _glyphClusters.Buffer.Span; + var clusters = GlyphClusters.Buffer.Span; // Find the start of the cluster at the character index. var start = clusters.BinarySearch((ushort)characterIndex, comparer); @@ -418,9 +426,19 @@ public int FindGlyphIndex(int characterIndex) } } - while (start > 0 && clusters[start - 1] == clusters[start]) + if (IsLeftToRight) { - start--; + while (start > 0 && clusters[start - 1] == clusters[start]) + { + start--; + } + } + else + { + while (start + 1 < clusters.Length && clusters[start + 1] == clusters[start]) + { + start++; + } } return start; @@ -440,34 +458,74 @@ public CharacterHit FindNearestCharacterHit(int index, out double width) var start = FindGlyphIndex(index); - var currentCluster = _glyphClusters[start]; + if (GlyphClusters.IsEmpty) + { + width = GetGlyphWidth(index); + + return new CharacterHit(start, 1); + } - var trailingLength = 0; + var cluster = GlyphClusters[start]; - while (start < _glyphClusters.Length && _glyphClusters[start] == currentCluster) + var nextCluster = cluster; + + var currentIndex = start; + + while (nextCluster == cluster) { - if (GlyphAdvances.IsEmpty) + width += GetGlyphWidth(currentIndex); + + if (IsLeftToRight) { - var glyph = GlyphIndices[start]; + currentIndex++; - width += GlyphTypeface.GetGlyphAdvance(glyph) * Scale; + if (currentIndex == GlyphClusters.Length) + { + break; + } } else { - width += GlyphAdvances[start]; + currentIndex--; + + if (currentIndex < 0) + { + break; + } } - trailingLength++; - start++; + nextCluster = GlyphClusters[currentIndex]; + } + + int trailingLength; + + if (nextCluster == cluster) + { + trailingLength = Characters.Start + Characters.Length - cluster; + } + else + { + trailingLength = nextCluster - cluster; } - if (start == _glyphClusters.Length && - currentCluster + trailingLength != Characters.Start + Characters.Length) + return new CharacterHit(cluster, trailingLength); + } + + /// + /// Gets a glyph's width. + /// + /// The glyph index. + /// The glyph's width. + private double GetGlyphWidth(int index) + { + if (GlyphAdvances.IsEmpty) { - trailingLength = Characters.Start + Characters.Length - currentCluster; + var glyph = GlyphIndices[index]; + + return GlyphTypeface.GetGlyphAdvance(glyph) * Scale; } - return new CharacterHit(currentCluster, trailingLength); + return GlyphAdvances[index]; } /// diff --git a/src/Avalonia.Visuals/Media/IBrush.cs b/src/Avalonia.Visuals/Media/IBrush.cs index 7756e94598b..15b7681be4a 100644 --- a/src/Avalonia.Visuals/Media/IBrush.cs +++ b/src/Avalonia.Visuals/Media/IBrush.cs @@ -13,4 +13,4 @@ public interface IBrush /// double Opacity { get; } } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs new file mode 100644 index 00000000000..e71584258a7 --- /dev/null +++ b/src/Avalonia.Visuals/Media/IExperimentalAcrylicMaterial.cs @@ -0,0 +1,33 @@ +namespace Avalonia.Media +{ + /// + /// Experimental Interface for producing Acrylic-like materials. + /// + public interface IExperimentalAcrylicMaterial + { + /// + /// Gets the of the material. + /// + AcrylicBackgroundSource BackgroundSource { get; } + + /// + /// Gets the TintColor of the material. + /// + Color TintColor { get; } + + /// + /// Gets the TintOpacity of the material. + /// + double TintOpacity { get; } + + /// + /// Gets the effective material color. + /// + Color MaterialColor { get; } + + /// + /// Gets the fallback color. + /// + Color FallbackColor { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/IMutableExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/IMutableExperimentalAcrylicMaterial.cs new file mode 100644 index 00000000000..fcfe4631a60 --- /dev/null +++ b/src/Avalonia.Visuals/Media/IMutableExperimentalAcrylicMaterial.cs @@ -0,0 +1,14 @@ +namespace Avalonia.Media +{ + /// + /// Represents a mutable brush which can return an immutable clone of itself. + /// + public interface IMutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial, IAffectsRender + { + /// + /// Creates an immutable clone of the brush. + /// + /// The immutable clone. + IExperimentalAcrylicMaterial ToImmutable(); + } +} diff --git a/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs new file mode 100644 index 00000000000..f46d76cf3f5 --- /dev/null +++ b/src/Avalonia.Visuals/Media/ImmutableExperimentalAcrylicMaterial.cs @@ -0,0 +1,73 @@ +using System; + +namespace Avalonia.Media +{ + public readonly struct ImmutableExperimentalAcrylicMaterial : IExperimentalAcrylicMaterial, IEquatable + { + public ImmutableExperimentalAcrylicMaterial(IExperimentalAcrylicMaterial brush) + { + BackgroundSource = brush.BackgroundSource; + TintColor = brush.TintColor; + TintOpacity = brush.TintOpacity; + FallbackColor = brush.FallbackColor; + MaterialColor = brush.MaterialColor; + } + + public AcrylicBackgroundSource BackgroundSource { get; } + + public Color TintColor { get; } + + public Color MaterialColor { get; } + + public double TintOpacity { get; } + + public Color FallbackColor { get; } + + public bool Equals(ImmutableExperimentalAcrylicMaterial other) + { + // ReSharper disable once CompareOfFloatsByEqualityOperator + return + TintColor == other.TintColor && + TintOpacity == other.TintOpacity && + BackgroundSource == other.BackgroundSource && + FallbackColor == other.FallbackColor && MaterialColor == other.MaterialColor; + + } + + public override bool Equals(object obj) + { + return obj is ImmutableExperimentalAcrylicMaterial other && Equals(other); + } + + public Color GetEffectiveTintColor() + { + return TintColor; + } + + public override int GetHashCode() + { + unchecked + { + int hash = 17; + + hash = (hash * 23) + TintColor.GetHashCode(); + hash = (hash * 23) + TintOpacity.GetHashCode(); + hash = (hash * 23) + BackgroundSource.GetHashCode(); + hash = (hash * 23) + FallbackColor.GetHashCode(); + hash = (hash * 23) + MaterialColor.GetHashCode(); + + return hash; + } + } + + public static bool operator ==(ImmutableExperimentalAcrylicMaterial left, ImmutableExperimentalAcrylicMaterial right) + { + return left.Equals(right); + } + + public static bool operator !=(ImmutableExperimentalAcrylicMaterial left, ImmutableExperimentalAcrylicMaterial right) + { + return !left.Equals(right); + } + } +} diff --git a/src/Avalonia.Visuals/Media/MaterialExtensions.cs b/src/Avalonia.Visuals/Media/MaterialExtensions.cs new file mode 100644 index 00000000000..c0b445c357b --- /dev/null +++ b/src/Avalonia.Visuals/Media/MaterialExtensions.cs @@ -0,0 +1,22 @@ +using System; + +namespace Avalonia.Media +{ + public static class MaterialExtensions + { + /// + /// Converts a brush to an immutable brush. + /// + /// The brush. + /// + /// The result of calling if the brush is mutable, + /// otherwise . + /// + public static IExperimentalAcrylicMaterial ToImmutable(this IExperimentalAcrylicMaterial material) + { + Contract.Requires(material != null); + + return (material as IMutableExperimentalAcrylicMaterial)?.ToImmutable() ?? material; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextDecoration.cs b/src/Avalonia.Visuals/Media/TextDecoration.cs index a83555946ba..681fc5d4994 100644 --- a/src/Avalonia.Visuals/Media/TextDecoration.cs +++ b/src/Avalonia.Visuals/Media/TextDecoration.cs @@ -1,4 +1,5 @@ -using Avalonia.Media.Immutable; +using Avalonia.Collections; +using Avalonia.Media.TextFormatting; namespace Avalonia.Media { @@ -14,28 +15,52 @@ public class TextDecoration : AvaloniaObject AvaloniaProperty.Register(nameof(Location)); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty PenProperty = - AvaloniaProperty.Register(nameof(Pen)); + public static readonly StyledProperty StrokeProperty = + AvaloniaProperty.Register(nameof(Stroke)); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty PenThicknessUnitProperty = - AvaloniaProperty.Register(nameof(PenThicknessUnit)); + public static readonly StyledProperty StrokeThicknessUnitProperty = + AvaloniaProperty.Register(nameof(StrokeThicknessUnit)); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty PenOffsetProperty = - AvaloniaProperty.Register(nameof(PenOffset)); + public static readonly StyledProperty> StrokeDashArrayProperty = + AvaloniaProperty.Register>(nameof(StrokeDashArray)); /// - /// Defines the property. + /// Defines the property. /// - public static readonly StyledProperty PenOffsetUnitProperty = - AvaloniaProperty.Register(nameof(PenOffsetUnit)); + public static readonly StyledProperty StrokeDashOffsetProperty = + AvaloniaProperty.Register(nameof(StrokeDashOffset)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty StrokeThicknessProperty = + AvaloniaProperty.Register(nameof(StrokeThickness), 1); + + /// + /// Defines the property. + /// + public static readonly StyledProperty StrokeLineCapProperty = + AvaloniaProperty.Register(nameof(StrokeLineCap)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty StrokeOffsetProperty = + AvaloniaProperty.Register(nameof(StrokeOffset)); + + /// + /// Defines the property. + /// + public static readonly StyledProperty StrokeOffsetUnitProperty = + AvaloniaProperty.Register(nameof(StrokeOffsetUnit)); /// /// Gets or sets the location. @@ -50,54 +75,139 @@ public TextDecorationLocation Location } /// - /// Gets or sets the pen. + /// Gets or sets the that specifies how the is painted. /// - /// - /// The pen. - /// - public IPen Pen + public IBrush Stroke + { + get { return GetValue(StrokeProperty); } + set { SetValue(StrokeProperty, value); } + } + + /// + /// Gets the units in which the thickness of the is expressed. + /// + public TextDecorationUnit StrokeThicknessUnit + { + get => GetValue(StrokeThicknessUnitProperty); + set => SetValue(StrokeThicknessUnitProperty, value); + } + + /// + /// Gets or sets a collection of values that indicate the pattern of dashes and gaps + /// that is used to draw the . + /// + public AvaloniaList StrokeDashArray + { + get { return GetValue(StrokeDashArrayProperty); } + set { SetValue(StrokeDashArrayProperty, value); } + } + + /// + /// Gets or sets a value that specifies the distance within the dash pattern where a dash begins. + /// + public double StrokeDashOffset + { + get { return GetValue(StrokeDashOffsetProperty); } + set { SetValue(StrokeDashOffsetProperty, value); } + } + + /// + /// Gets or sets the thickness of the . + /// + public double StrokeThickness { - get => GetValue(PenProperty); - set => SetValue(PenProperty, value); + get { return GetValue(StrokeThicknessProperty); } + set { SetValue(StrokeThicknessProperty, value); } } /// - /// Gets the units in which the Thickness of the text decoration's is expressed. + /// Gets or sets a enumeration value that describes the shape at the ends of a line. /// - public TextDecorationUnit PenThicknessUnit + public PenLineCap StrokeLineCap { - get => GetValue(PenThicknessUnitProperty); - set => SetValue(PenThicknessUnitProperty, value); + get { return GetValue(StrokeLineCapProperty); } + set { SetValue(StrokeLineCapProperty, value); } } /// - /// Gets or sets the pen offset. + /// The stroke's offset. /// /// /// The pen offset. /// - public double PenOffset + public double StrokeOffset { - get => GetValue(PenOffsetProperty); - set => SetValue(PenOffsetProperty, value); + get => GetValue(StrokeOffsetProperty); + set => SetValue(StrokeOffsetProperty, value); } /// - /// Gets the units in which the value is expressed. + /// Gets the units in which the value is expressed. /// - public TextDecorationUnit PenOffsetUnit + public TextDecorationUnit StrokeOffsetUnit { - get => GetValue(PenOffsetUnitProperty); - set => SetValue(PenOffsetUnitProperty, value); + get => GetValue(StrokeOffsetUnitProperty); + set => SetValue(StrokeOffsetUnitProperty, value); } /// - /// Creates an immutable clone of the . + /// Draws the at given origin. /// - /// The immutable clone. - public ImmutableTextDecoration ToImmutable() + /// The drawing context. + /// The shaped characters that are decorated. + /// The origin. + internal void Draw(DrawingContext drawingContext, ShapedTextCharacters shapedTextCharacters, Point origin) { - return new ImmutableTextDecoration(Location, Pen?.ToImmutable(), PenThicknessUnit, PenOffset, PenOffsetUnit); + var fontRenderingEmSize = shapedTextCharacters.Properties.FontRenderingEmSize; + var fontMetrics = shapedTextCharacters.FontMetrics; + var thickness = StrokeThickness; + + switch (StrokeThicknessUnit) + { + case TextDecorationUnit.FontRecommended: + switch (Location) + { + case TextDecorationLocation.Underline: + thickness = fontMetrics.UnderlineThickness; + break; + case TextDecorationLocation.Strikethrough: + thickness = fontMetrics.StrikethroughThickness; + break; + } + + break; + case TextDecorationUnit.FontRenderingEmSize: + thickness = fontRenderingEmSize * thickness; + break; + } + + switch (Location) + { + case TextDecorationLocation.Overline: + origin += new Point(0, fontMetrics.Ascent); + break; + case TextDecorationLocation.Strikethrough: + origin += new Point(0, -fontMetrics.StrikethroughPosition); + break; + case TextDecorationLocation.Underline: + origin += new Point(0, -fontMetrics.UnderlinePosition); + break; + } + + switch (StrokeOffsetUnit) + { + case TextDecorationUnit.FontRenderingEmSize: + origin += new Point(0, StrokeOffset * fontRenderingEmSize); + break; + case TextDecorationUnit.Pixel: + origin += new Point(0, StrokeOffset); + break; + } + + var pen = new Pen(Stroke ?? shapedTextCharacters.Properties.ForegroundBrush, thickness, + new DashStyle(StrokeDashArray, StrokeDashOffset), StrokeLineCap); + + drawingContext.DrawLine(pen, origin, origin + new Point(shapedTextCharacters.Bounds.Width, 0)); } } } diff --git a/src/Avalonia.Visuals/Media/TextDecorationCollection.cs b/src/Avalonia.Visuals/Media/TextDecorationCollection.cs index 21e2e2484c5..2dced2252e1 100644 --- a/src/Avalonia.Visuals/Media/TextDecorationCollection.cs +++ b/src/Avalonia.Visuals/Media/TextDecorationCollection.cs @@ -1,7 +1,6 @@ using System; using System.Collections.Generic; using Avalonia.Collections; -using Avalonia.Media.Immutable; using Avalonia.Utilities; namespace Avalonia.Media @@ -11,22 +10,6 @@ namespace Avalonia.Media /// public class TextDecorationCollection : AvaloniaList { - /// - /// Creates an immutable clone of the . - /// - /// The immutable clone. - public ImmutableTextDecoration[] ToImmutable() - { - var immutable = new ImmutableTextDecoration[Count]; - - for (var i = 0; i < Count; i++) - { - immutable[i] = this[i].ToImmutable(); - } - - return immutable; - } - /// /// Parses a string. /// diff --git a/src/Avalonia.Visuals/Media/TextDecorationUnit.cs b/src/Avalonia.Visuals/Media/TextDecorationUnit.cs index dde425bb94c..a61983e8d59 100644 --- a/src/Avalonia.Visuals/Media/TextDecorationUnit.cs +++ b/src/Avalonia.Visuals/Media/TextDecorationUnit.cs @@ -1,7 +1,7 @@ namespace Avalonia.Media { /// - /// Specifies the unit type of either a or a thickness value. + /// Specifies the unit type of either a or a value. /// public enum TextDecorationUnit { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs index 4903342cea8..56790cc0db2 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/DrawableTextRun.cs @@ -1,6 +1,4 @@ -using Avalonia.Platform; - -namespace Avalonia.Media.TextFormatting +namespace Avalonia.Media.TextFormatting { /// /// A text run that supports drawing content. @@ -17,6 +15,6 @@ public abstract class DrawableTextRun : TextRun /// /// The drawing context. /// The origin. - public abstract void Draw(IDrawingContextImpl drawingContext, Point origin); + public abstract void Draw(DrawingContext drawingContext, Point origin); } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs new file mode 100644 index 00000000000..8e7d934bca4 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs @@ -0,0 +1,56 @@ +namespace Avalonia.Media.TextFormatting +{ + public class GenericTextParagraphProperties : TextParagraphProperties + { + private TextAlignment _textAlignment; + private TextWrapping _textWrapping; + private double _lineHeight; + + public GenericTextParagraphProperties( + TextRunProperties defaultTextRunProperties, + TextAlignment textAlignment = TextAlignment.Left, + TextWrapping textWrapping = TextWrapping.NoWrap, + double lineHeight = 0) + { + DefaultTextRunProperties = defaultTextRunProperties; + + _textAlignment = textAlignment; + + _textWrapping = textWrapping; + + _lineHeight = lineHeight; + } + + public override TextRunProperties DefaultTextRunProperties { get; } + + public override TextAlignment TextAlignment => _textAlignment; + + public override TextWrapping TextWrapping => _textWrapping; + + public override double LineHeight => _lineHeight; + + /// + /// Set text alignment + /// + internal void SetTextAlignment(TextAlignment textAlignment) + { + _textAlignment = textAlignment; + } + + /// + /// Set text wrap + /// + internal void SetTextWrapping(TextWrapping textWrapping) + { + _textWrapping = textWrapping; + } + + /// + /// Set line height + /// + internal void SetLineHeight(double lineHeight) + { + _lineHeight = lineHeight; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs new file mode 100644 index 00000000000..3db35894986 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextRunProperties.cs @@ -0,0 +1,40 @@ +using System.Globalization; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Generic implementation of TextRunProperties + /// + public class GenericTextRunProperties : TextRunProperties + { + public GenericTextRunProperties(Typeface typeface, double fontRenderingEmSize = 12, + TextDecorationCollection textDecorations = null, IBrush foregroundBrush = null, IBrush backgroundBrush = null, + CultureInfo cultureInfo = null) + { + Typeface = typeface; + FontRenderingEmSize = fontRenderingEmSize; + TextDecorations = textDecorations; + ForegroundBrush = foregroundBrush; + BackgroundBrush = backgroundBrush; + CultureInfo = cultureInfo; + } + + /// + public override Typeface Typeface { get; } + + /// + public override double FontRenderingEmSize { get; } + + /// + public override TextDecorationCollection TextDecorations { get; } + + /// + public override IBrush ForegroundBrush { get; } + + /// + public override IBrush BackgroundBrush { get; } + + /// + public override CultureInfo CultureInfo { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs new file mode 100644 index 00000000000..0c6c7229414 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapeableTextCharacters.cs @@ -0,0 +1,23 @@ +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// A group of characters that can be shaped. + /// + public sealed class ShapeableTextCharacters : TextRun + { + public ShapeableTextCharacters(ReadOnlySlice text, TextRunProperties properties) + { + TextSourceLength = text.Length; + Text = text; + Properties = properties; + } + + public override int TextSourceLength { get; } + + public override ReadOnlySlice Text { get; } + + public override TextRunProperties Properties { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs new file mode 100644 index 00000000000..9e67a03f452 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -0,0 +1,154 @@ +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// A text run that holds shaped characters. + /// + public sealed class ShapedTextCharacters : DrawableTextRun + { + public ShapedTextCharacters(GlyphRun glyphRun, TextRunProperties properties) + { + Text = glyphRun.Characters; + Properties = properties; + TextSourceLength = Text.Length; + FontMetrics = new FontMetrics(Properties.Typeface, Properties.FontRenderingEmSize); + GlyphRun = glyphRun; + } + + /// + public override ReadOnlySlice Text { get; } + + /// + public override TextRunProperties Properties { get; } + + /// + public override int TextSourceLength { get; } + + /// + public override Rect Bounds => GlyphRun.Bounds; + + /// + /// Gets the font metrics. + /// + /// + /// The font metrics. + /// + public FontMetrics FontMetrics { get; } + + /// + /// Gets the glyph run. + /// + /// + /// The glyphs. + /// + public GlyphRun GlyphRun { get; } + + /// + public override void Draw(DrawingContext drawingContext, Point origin) + { + if (GlyphRun.GlyphIndices.Length == 0) + { + return; + } + + if (Properties.Typeface == null) + { + return; + } + + if (Properties.ForegroundBrush == null) + { + return; + } + + if (Properties.BackgroundBrush != null) + { + drawingContext.DrawRectangle(Properties.BackgroundBrush, null, + new Rect(origin.X, origin.Y + FontMetrics.Ascent, Bounds.Width, Bounds.Height)); + } + + drawingContext.DrawGlyphRun(Properties.ForegroundBrush, GlyphRun, origin); + + if (Properties.TextDecorations == null) + { + return; + } + + foreach (var textDecoration in Properties.TextDecorations) + { + textDecoration.Draw(drawingContext, this, origin); + } + } + + /// + /// Splits the at specified length. + /// + /// The length. + /// The split result. + public SplitTextCharactersResult Split(int length) + { + var glyphCount = GlyphRun.FindGlyphIndex(GlyphRun.Characters.Start + length); + + if (GlyphRun.Characters.Length == length) + { + return new SplitTextCharactersResult(this, null); + } + + if (GlyphRun.GlyphIndices.Length == glyphCount) + { + return new SplitTextCharactersResult(this, null); + } + + var firstGlyphRun = new GlyphRun( + Properties.Typeface.GlyphTypeface, + Properties.FontRenderingEmSize, + GlyphRun.GlyphIndices.Take(glyphCount), + GlyphRun.GlyphAdvances.Take(glyphCount), + GlyphRun.GlyphOffsets.Take(glyphCount), + GlyphRun.Characters.Take(length), + GlyphRun.GlyphClusters.Take(glyphCount)); + + var firstTextRun = new ShapedTextCharacters(firstGlyphRun, Properties); + + var secondGlyphRun = new GlyphRun( + Properties.Typeface.GlyphTypeface, + Properties.FontRenderingEmSize, + GlyphRun.GlyphIndices.Skip(glyphCount), + GlyphRun.GlyphAdvances.Skip(glyphCount), + GlyphRun.GlyphOffsets.Skip(glyphCount), + GlyphRun.Characters.Skip(length), + GlyphRun.GlyphClusters.Skip(glyphCount)); + + var secondTextRun = new ShapedTextCharacters(secondGlyphRun, Properties); + + return new SplitTextCharactersResult(firstTextRun, secondTextRun); + } + + public readonly struct SplitTextCharactersResult + { + public SplitTextCharactersResult(ShapedTextCharacters first, ShapedTextCharacters second) + { + First = first; + + Second = second; + } + + /// + /// Gets the first text run. + /// + /// + /// The first text run. + /// + public ShapedTextCharacters First { get; } + + /// + /// Gets the second text run. + /// + /// + /// The second text run. + /// + public ShapedTextCharacters Second { get; } + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextRun.cs deleted file mode 100644 index 00f9b918cb1..00000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextRun.cs +++ /dev/null @@ -1,212 +0,0 @@ -using Avalonia.Media.Immutable; -using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Platform; -using Avalonia.Utility; - -namespace Avalonia.Media.TextFormatting -{ - /// - /// A text run that holds a shaped glyph run. - /// - public sealed class ShapedTextRun : DrawableTextRun - { - public ShapedTextRun(ReadOnlySlice text, TextStyle style) : this( - TextShaper.Current.ShapeText(text, style.TextFormat), style) - { - } - - public ShapedTextRun(GlyphRun glyphRun, TextStyle style) - { - Text = glyphRun.Characters; - Style = style; - GlyphRun = glyphRun; - } - - /// - public override Rect Bounds => GlyphRun.Bounds; - - /// - /// Gets the glyph run. - /// - /// - /// The glyphs. - /// - public GlyphRun GlyphRun { get; } - - /// - public override void Draw(IDrawingContextImpl drawingContext, Point origin) - { - if (GlyphRun.GlyphIndices.Length == 0) - { - return; - } - - if (Style.TextFormat.Typeface == null) - { - return; - } - - if (Style.Foreground == null) - { - return; - } - - drawingContext.DrawGlyphRun(Style.Foreground, GlyphRun, origin); - - if (Style.TextDecorations == null) - { - return; - } - - foreach (var textDecoration in Style.TextDecorations) - { - DrawTextDecoration(drawingContext, textDecoration, origin); - } - } - - /// - /// Draws the at given origin. - /// - /// The drawing context. - /// The text decoration. - /// The origin. - private void DrawTextDecoration(IDrawingContextImpl drawingContext, ImmutableTextDecoration textDecoration, Point origin) - { - var textFormat = Style.TextFormat; - - var fontMetrics = Style.TextFormat.FontMetrics; - - var thickness = textDecoration.Pen?.Thickness ?? 1.0; - - switch (textDecoration.PenThicknessUnit) - { - case TextDecorationUnit.FontRecommended: - switch (textDecoration.Location) - { - case TextDecorationLocation.Underline: - thickness = fontMetrics.UnderlineThickness; - break; - case TextDecorationLocation.Strikethrough: - thickness = fontMetrics.StrikethroughThickness; - break; - } - break; - case TextDecorationUnit.FontRenderingEmSize: - thickness = textFormat.FontRenderingEmSize * thickness; - break; - } - - switch (textDecoration.Location) - { - case TextDecorationLocation.Overline: - origin += new Point(0, textFormat.FontMetrics.Ascent); - break; - case TextDecorationLocation.Strikethrough: - origin += new Point(0, -textFormat.FontMetrics.StrikethroughPosition); - break; - case TextDecorationLocation.Underline: - origin += new Point(0, -textFormat.FontMetrics.UnderlinePosition); - break; - } - - switch (textDecoration.PenOffsetUnit) - { - case TextDecorationUnit.FontRenderingEmSize: - origin += new Point(0, textDecoration.PenOffset * textFormat.FontRenderingEmSize); - break; - case TextDecorationUnit.Pixel: - origin += new Point(0, textDecoration.PenOffset); - break; - } - - var pen = new ImmutablePen( - textDecoration.Pen?.Brush ?? Style.Foreground.ToImmutable(), - thickness, - textDecoration.Pen?.DashStyle?.ToImmutable(), - textDecoration.Pen?.LineCap ?? default, - textDecoration.Pen?.LineJoin ?? PenLineJoin.Miter, - textDecoration.Pen?.MiterLimit ?? 10.0); - - drawingContext.DrawLine(pen, origin, origin + new Point(GlyphRun.Bounds.Width, 0)); - } - - /// - /// Splits the at specified length. - /// - /// The length. - /// The split result. - public SplitTextCharactersResult Split(int length) - { - var glyphCount = 0; - - var firstCharacters = GlyphRun.Characters.Take(length); - - var codepointEnumerator = new CodepointEnumerator(firstCharacters); - - while (codepointEnumerator.MoveNext()) - { - glyphCount++; - } - - if (GlyphRun.Characters.Length == length) - { - return new SplitTextCharactersResult(this, null); - } - - if (GlyphRun.GlyphIndices.Length == glyphCount) - { - return new SplitTextCharactersResult(this, null); - } - - var firstGlyphRun = new GlyphRun( - Style.TextFormat.Typeface.GlyphTypeface, - Style.TextFormat.FontRenderingEmSize, - GlyphRun.GlyphIndices.Take(glyphCount), - GlyphRun.GlyphAdvances.Take(glyphCount), - GlyphRun.GlyphOffsets.Take(glyphCount), - GlyphRun.Characters.Take(length), - GlyphRun.GlyphClusters.Take(length)); - - var firstTextRun = new ShapedTextRun(firstGlyphRun, Style); - - var secondGlyphRun = new GlyphRun( - Style.TextFormat.Typeface.GlyphTypeface, - Style.TextFormat.FontRenderingEmSize, - GlyphRun.GlyphIndices.Skip(glyphCount), - GlyphRun.GlyphAdvances.Skip(glyphCount), - GlyphRun.GlyphOffsets.Skip(glyphCount), - GlyphRun.Characters.Skip(length), - GlyphRun.GlyphClusters.Skip(length)); - - var secondTextRun = new ShapedTextRun(secondGlyphRun, Style); - - return new SplitTextCharactersResult(firstTextRun, secondTextRun); - } - - public readonly struct SplitTextCharactersResult - { - public SplitTextCharactersResult(ShapedTextRun first, ShapedTextRun second) - { - First = first; - - Second = second; - } - - /// - /// Gets the first text run. - /// - /// - /// The first text run. - /// - public ShapedTextRun First { get; } - - /// - /// Gets the second text run. - /// - /// - /// The second text run. - /// - public ShapedTextRun Second { get; } - } - } -} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs b/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs deleted file mode 100644 index f84e45d4c6f..00000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextFormatter.cs +++ /dev/null @@ -1,395 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Platform; -using Avalonia.Utility; - -namespace Avalonia.Media.TextFormatting -{ - internal class SimpleTextFormatter : TextFormatter - { - private static readonly ReadOnlySlice s_ellipsis = new ReadOnlySlice(new[] { '\u2026' }); - - /// - public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, - TextParagraphProperties paragraphProperties) - { - var textTrimming = paragraphProperties.TextTrimming; - var textWrapping = paragraphProperties.TextWrapping; - TextLine textLine; - - var textRuns = FormatTextRuns(textSource, firstTextSourceIndex, out var textPointer); - - if (textTrimming != TextTrimming.None) - { - textLine = PerformTextTrimming(textPointer, textRuns, paragraphWidth, paragraphProperties); - } - else - { - if (textWrapping == TextWrapping.Wrap) - { - textLine = PerformTextWrapping(textPointer, textRuns, paragraphWidth, paragraphProperties); - } - else - { - var textLineMetrics = - TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment); - - textLine = new SimpleTextLine(textPointer, textRuns, textLineMetrics); - } - } - - return textLine; - } - - /// - /// Formats text runs with optional text style overrides. - /// - /// The text source. - /// The first text source index. - /// The text pointer that covers the formatted text runs. - /// - /// The formatted text runs. - /// - private List FormatTextRuns(ITextSource textSource, int firstTextSourceIndex, out TextPointer textPointer) - { - var start = -1; - var length = 0; - - var textRuns = new List(); - - while (true) - { - var textRun = textSource.GetTextRun(firstTextSourceIndex + length); - - if (start == -1) - { - start = textRun.Text.Start; - } - - if (textRun is TextEndOfLine) - { - break; - } - - switch (textRun) - { - case TextCharacters textCharacters: - - var runText = textCharacters.Text; - - while (!runText.IsEmpty) - { - var shapableTextStyleRun = CreateShapableTextStyleRun(runText, textRun.Style); - - var shapedRun = new ShapedTextRun(runText.Take(shapableTextStyleRun.TextPointer.Length), - shapableTextStyleRun.Style); - - textRuns.Add(shapedRun); - - runText = runText.Skip(shapedRun.Text.Length); - } - - break; - default: - throw new NotSupportedException("Run type not supported by the formatter."); - } - - length += textRun.Text.Length; - } - - textPointer = new TextPointer(start, length); - - return textRuns; - } - - /// - /// Performs text trimming and returns a trimmed line. - /// - /// A value that specifies the width of the paragraph that the line fills. - /// A value that represents paragraph properties, - /// such as TextWrapping, TextAlignment, or TextStyle. - /// The text runs to perform the trimming on. - /// The text that was used to construct the text runs. - /// - private static TextLine PerformTextTrimming(TextPointer text, IReadOnlyList textRuns, - double paragraphWidth, TextParagraphProperties paragraphProperties) - { - var textTrimming = paragraphProperties.TextTrimming; - var availableWidth = paragraphWidth; - var currentWidth = 0.0; - var runIndex = 0; - - while (runIndex < textRuns.Count) - { - var currentRun = textRuns[runIndex]; - - currentWidth += currentRun.GlyphRun.Bounds.Width; - - if (currentWidth > availableWidth) - { - var ellipsisRun = CreateEllipsisRun(currentRun.Style); - - var measuredLength = MeasureText(currentRun, availableWidth - ellipsisRun.GlyphRun.Bounds.Width); - - if (textTrimming == TextTrimming.WordEllipsis) - { - if (measuredLength < text.End) - { - var currentBreakPosition = 0; - - var lineBreaker = new LineBreakEnumerator(currentRun.Text); - - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) - { - var nextBreakPosition = lineBreaker.Current.PositionWrap; - - if (nextBreakPosition == 0) - { - break; - } - - if (nextBreakPosition > measuredLength) - { - break; - } - - currentBreakPosition = nextBreakPosition; - } - - measuredLength = currentBreakPosition; - } - } - - var splitResult = SplitTextRuns(textRuns, measuredLength); - - var trimmedRuns = new List(splitResult.First.Count + 1); - - trimmedRuns.AddRange(splitResult.First); - - trimmedRuns.Add(ellipsisRun); - - var textLineMetrics = - TextLineMetrics.Create(trimmedRuns, paragraphWidth, paragraphProperties.TextAlignment); - - return new SimpleTextLine(text.Take(measuredLength), trimmedRuns, textLineMetrics); - } - - availableWidth -= currentRun.GlyphRun.Bounds.Width; - - runIndex++; - } - - return new SimpleTextLine(text, textRuns, - TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment)); - } - - /// - /// Performs text wrapping returns a list of text lines. - /// - /// The text paragraph properties. - /// The text run'S. - /// The text to analyze for break opportunities. - /// - /// - private static TextLine PerformTextWrapping(TextPointer text, IReadOnlyList textRuns, - double paragraphWidth, TextParagraphProperties paragraphProperties) - { - var availableWidth = paragraphWidth; - var currentWidth = 0.0; - var runIndex = 0; - var length = 0; - - while (runIndex < textRuns.Count) - { - var currentRun = textRuns[runIndex]; - - if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth) - { - var measuredLength = MeasureText(currentRun, paragraphWidth - currentWidth); - - if (measuredLength < currentRun.Text.Length) - { - var currentBreakPosition = -1; - - var lineBreaker = new LineBreakEnumerator(currentRun.Text); - - while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) - { - var nextBreakPosition = lineBreaker.Current.PositionWrap; - - if (nextBreakPosition == 0) - { - break; - } - - if (nextBreakPosition > measuredLength) - { - break; - } - - currentBreakPosition = nextBreakPosition; - } - - if (currentBreakPosition != -1) - { - measuredLength = currentBreakPosition; - } - } - - length += measuredLength; - - var splitResult = SplitTextRuns(textRuns, length); - - var textLineMetrics = - TextLineMetrics.Create(splitResult.First, paragraphWidth, paragraphProperties.TextAlignment); - - return new SimpleTextLine(text.Take(length), splitResult.First, textLineMetrics); - } - - currentWidth += currentRun.GlyphRun.Bounds.Width; - - length += currentRun.GlyphRun.Characters.Length; - - runIndex++; - } - - return new SimpleTextLine(text, textRuns, - TextLineMetrics.Create(textRuns, paragraphWidth, paragraphProperties.TextAlignment)); - } - - /// - /// Measures the number of characters that fits into available width. - /// - /// The text run. - /// The available width. - /// - private static int MeasureText(ShapedTextRun textRun, double availableWidth) - { - var glyphRun = textRun.GlyphRun; - - var characterHit = glyphRun.GetCharacterHitFromDistance(availableWidth, out _); - - return characterHit.FirstCharacterIndex + characterHit.TrailingLength - textRun.Text.Start; - } - - /// - /// Creates an ellipsis. - /// - /// The text style. - /// - private static ShapedTextRun CreateEllipsisRun(TextStyle textStyle) - { - var formatterImpl = AvaloniaLocator.Current.GetService(); - - var glyphRun = formatterImpl.ShapeText(s_ellipsis, textStyle.TextFormat); - - return new ShapedTextRun(glyphRun, textStyle); - } - - private readonly struct SplitTextRunsResult - { - public SplitTextRunsResult(IReadOnlyList first, IReadOnlyList second) - { - First = first; - - Second = second; - } - - /// - /// Gets the first text runs. - /// - /// - /// The first text runs. - /// - public IReadOnlyList First { get; } - - /// - /// Gets the second text runs. - /// - /// - /// The second text runs. - /// - public IReadOnlyList Second { get; } - } - - /// - /// Split a sequence of runs into two segments at specified length. - /// - /// The text run's. - /// The length to split at. - /// - private static SplitTextRunsResult SplitTextRuns(IReadOnlyList textRuns, int length) - { - var currentLength = 0; - - for (var i = 0; i < textRuns.Count; i++) - { - var currentRun = textRuns[i]; - - if (currentLength + currentRun.GlyphRun.Characters.Length < length) - { - currentLength += currentRun.GlyphRun.Characters.Length; - continue; - } - - var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i; - - var first = new ShapedTextRun[firstCount]; - - if (firstCount > 1) - { - for (var j = 0; j < i; j++) - { - first[j] = textRuns[j]; - } - } - - var secondCount = textRuns.Count - firstCount; - - if (currentLength + currentRun.GlyphRun.Characters.Length == length) - { - var second = new ShapedTextRun[secondCount]; - - var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0; - - if (secondCount > 0) - { - for (var j = 0; j < secondCount; j++) - { - second[j] = textRuns[i + j + offset]; - } - } - - first[i] = currentRun; - - return new SplitTextRunsResult(first, second); - } - else - { - secondCount++; - - var second = new ShapedTextRun[secondCount]; - - if (secondCount > 0) - { - for (var j = 1; j < secondCount; j++) - { - second[j] = textRuns[i + j]; - } - } - - var split = currentRun.Split(length - currentLength); - - first[i] = split.First; - - second[0] = split.Second; - - return new SplitTextRunsResult(first, second); - } - } - - return new SplitTextRunsResult(textRuns, null); - } - } -} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextLine.cs deleted file mode 100644 index 11d241bc348..00000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/SimpleTextLine.cs +++ /dev/null @@ -1,259 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Platform; - -namespace Avalonia.Media.TextFormatting -{ - internal class SimpleTextLine : TextLine - { - private readonly IReadOnlyList _textRuns; - - public SimpleTextLine(TextPointer textPointer, IReadOnlyList textRuns, TextLineMetrics lineMetrics) - { - Text = textPointer; - _textRuns = textRuns; - LineMetrics = lineMetrics; - } - - /// - public override TextPointer Text { get; } - - /// - public override IReadOnlyList TextRuns => _textRuns; - - /// - public override TextLineMetrics LineMetrics { get; } - - /// - public override void Draw(IDrawingContextImpl drawingContext, Point origin) - { - var currentX = origin.X; - - foreach (var textRun in _textRuns) - { - var baselineOrigin = new Point(currentX + LineMetrics.BaselineOrigin.X, - origin.Y + LineMetrics.BaselineOrigin.Y); - - textRun.Draw(drawingContext, baselineOrigin); - - currentX += textRun.Bounds.Width; - } - } - - /// - public override CharacterHit GetCharacterHitFromDistance(double distance) - { - if (distance < 0) - { - // hit happens before the line, return the first position - return new CharacterHit(Text.Start); - } - - // process hit that happens within the line - var characterHit = new CharacterHit(); - - foreach (var run in _textRuns) - { - characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _); - - if (distance <= run.Bounds.Width) - { - break; - } - - distance -= run.Bounds.Width; - } - - return characterHit; - } - - /// - public override double GetDistanceFromCharacterHit(CharacterHit characterHit) - { - return DistanceFromCodepointIndex(characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0)); - } - - /// - public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) - { - int nextVisibleCp; - bool navigableCpFound; - - if (characterHit.TrailingLength == 0) - { - navigableCpFound = FindNextCodepointIndex(characterHit.FirstCharacterIndex, out nextVisibleCp); - - if (navigableCpFound) - { - // Move from leading to trailing edge - return new CharacterHit(nextVisibleCp, 1); - } - } - - navigableCpFound = FindNextCodepointIndex(characterHit.FirstCharacterIndex + 1, out nextVisibleCp); - - if (navigableCpFound) - { - // Move from trailing edge of current character to trailing edge of next - return new CharacterHit(nextVisibleCp, 1); - } - - // Can't move, we're after the last character - return characterHit; - } - - /// - public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) - { - int previousCodepointIndex; - bool codepointIndexFound; - - var cpHit = characterHit.FirstCharacterIndex; - var trailingHit = characterHit.TrailingLength != 0; - - // Input can be right after the end of the current line. Snap it to be at the end of the line. - if (cpHit >= Text.Start + Text.Length) - { - cpHit = Text.Start + Text.Length - 1; - - trailingHit = true; - } - - if (trailingHit) - { - codepointIndexFound = FindPreviousCodepointIndex(cpHit, out previousCodepointIndex); - - if (codepointIndexFound) - { - // Move from trailing to leading edge - return new CharacterHit(previousCodepointIndex, 0); - } - } - - codepointIndexFound = FindPreviousCodepointIndex(cpHit - 1, out previousCodepointIndex); - - if (codepointIndexFound) - { - // Move from leading edge of current character to leading edge of previous - return new CharacterHit(previousCodepointIndex, 0); - } - - // Can't move, we're before the first character - return characterHit; - } - - /// - public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit) - { - // same operation as move-to-previous - return GetPreviousCaretCharacterHit(characterHit); - } - - /// - /// Get distance from line start to the specified codepoint index - /// - private double DistanceFromCodepointIndex(int codepointIndex) - { - var currentDistance = 0.0; - - foreach (var textRun in _textRuns) - { - if (codepointIndex > textRun.Text.End) - { - currentDistance += textRun.Bounds.Width; - - continue; - } - - return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(codepointIndex)); - } - - return currentDistance; - } - - /// - /// Search forward from the given codepoint index (inclusive) to find the next navigable codepoint index. - /// Return true if one such codepoint index is found, false otherwise. - /// - private bool FindNextCodepointIndex(int codepointIndex, out int nextCodepointIndex) - { - nextCodepointIndex = codepointIndex; - - if (codepointIndex >= Text.Start + Text.Length) - { - return false; // Cannot go forward anymore - } - - GetRunIndexAtCodepointIndex(codepointIndex, out var runIndex, out var cpRunStart); - - while (runIndex < TextRuns.Count) - { - // When navigating forward, only the trailing edge of visible content is - // navigable. - if (runIndex < TextRuns.Count) - { - nextCodepointIndex = Math.Max(cpRunStart, codepointIndex); - return true; - } - - cpRunStart += TextRuns[runIndex++].Text.Length; - } - - return false; - } - - /// - /// Search backward from the given codepoint index (inclusive) to find the previous navigable codepoint index. - /// Return true if one such codepoint is found, false otherwise. - /// - private bool FindPreviousCodepointIndex(int codepointIndex, out int previousCodepointIndex) - { - previousCodepointIndex = codepointIndex; - - if (codepointIndex < Text.Start) - { - return false; // Cannot go backward anymore. - } - - // Position the cpRunEnd at the end of the span that contains the given cp - GetRunIndexAtCodepointIndex(codepointIndex, out var runIndex, out var codepointIndexAtRunEnd); - - codepointIndexAtRunEnd += TextRuns[runIndex].Text.End; - - while (runIndex >= 0) - { - // Visible content has caret stops at its leading edge. - if (runIndex + 1 < TextRuns.Count) - { - previousCodepointIndex = Math.Min(codepointIndexAtRunEnd, codepointIndex); - return true; - } - - // Newline sequence has caret stops at its leading edge. - if (runIndex == TextRuns.Count) - { - // Get the cp index at the beginning of the newline sequence. - previousCodepointIndex = codepointIndexAtRunEnd - TextRuns[runIndex].Text.Length + 1; - return true; - } - - codepointIndexAtRunEnd -= TextRuns[runIndex--].Text.Length; - } - - return false; - } - - private void GetRunIndexAtCodepointIndex(int codepointIndex, out int runIndex, out int codepointIndexAtRunStart) - { - codepointIndexAtRunStart = Text.Start; - runIndex = 0; - - // Find the span that contains the given cp - while (runIndex < TextRuns.Count && - codepointIndexAtRunStart + TextRuns[runIndex].Text.Length <= codepointIndex) - { - codepointIndexAtRunStart += TextRuns[runIndex++].Text.Length; - } - } - } -} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs index d9b27958ab9..47e716982c4 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCharacters.cs @@ -1,4 +1,6 @@ -using Avalonia.Utility; +using System.Collections.Generic; +using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -7,15 +9,182 @@ namespace Avalonia.Media.TextFormatting /// public class TextCharacters : TextRun { - protected TextCharacters() + public TextCharacters(ReadOnlySlice text, TextRunProperties properties) { - + TextSourceLength = text.Length; + Text = text; + Properties = properties; } - public TextCharacters(ReadOnlySlice text, TextStyle style) + /// + public override int TextSourceLength { get; } + + /// + public override ReadOnlySlice Text { get; } + + /// + public override TextRunProperties Properties { get; } + + /// + /// Gets a list of . + /// + /// The shapeable text characters. + internal IList GetShapeableCharacters() { - Text = text; - Style = style; + var shapeableCharacters = new List(2); + + var runText = Text; + + while (!runText.IsEmpty) + { + var shapeableRun = CreateShapeableRun(runText, Properties); + + shapeableCharacters.Add(shapeableRun); + + runText = runText.Skip(shapeableRun.Text.Length); + } + + return shapeableCharacters; + } + + /// + /// Creates a shapeable text run with unique properties. + /// + /// The text to create text runs from. + /// The default text run properties. + /// A list of shapeable text runs. + private ShapeableTextCharacters CreateShapeableRun(ReadOnlySlice text, TextRunProperties defaultProperties) + { + var defaultTypeface = defaultProperties.Typeface; + + var currentTypeface = defaultTypeface; + + if (TryGetRunProperties(text, currentTypeface, defaultTypeface, out var count)) + { + return new ShapeableTextCharacters(text.Take(count), + new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize, + defaultProperties.TextDecorations, defaultProperties.ForegroundBrush)); + + } + + var codepoint = Codepoint.ReadAt(text, count, out _); + + //ToDo: Fix FontFamily fallback + currentTypeface = + FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Style, defaultTypeface.Weight, defaultTypeface.FontFamily); + + if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) + { + //Fallback found + return new ShapeableTextCharacters(text.Take(count), + new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize, + defaultProperties.TextDecorations, defaultProperties.ForegroundBrush)); + } + + // no fallback found + currentTypeface = defaultTypeface; + + var glyphTypeface = currentTypeface.GlyphTypeface; + + var enumerator = new GraphemeEnumerator(text); + + while (enumerator.MoveNext()) + { + var grapheme = enumerator.Current; + + if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) + { + break; + } + + count += grapheme.Text.Length; + } + + return new ShapeableTextCharacters(text.Take(count), + new GenericTextRunProperties(currentTypeface, defaultProperties.FontRenderingEmSize, + defaultProperties.TextDecorations, defaultProperties.ForegroundBrush)); + } + + /// + /// Tries to get run properties. + /// + /// + /// + /// The typeface that is used to find matching characters. + /// + /// + protected bool TryGetRunProperties(ReadOnlySlice text, Typeface typeface, Typeface defaultTypeface, + out int count) + { + if (text.Length == 0) + { + count = 0; + return false; + } + + var isFallback = typeface != defaultTypeface; + + count = 0; + var script = Script.Common; + //var direction = BiDiClass.LeftToRight; + + var font = typeface.GlyphTypeface; + var defaultFont = defaultTypeface.GlyphTypeface; + + var enumerator = new GraphemeEnumerator(text); + + while (enumerator.MoveNext()) + { + var grapheme = enumerator.Current; + + var currentScript = grapheme.FirstCodepoint.Script; + + //var currentDirection = grapheme.FirstCodepoint.BiDiClass; + + //// ToDo: Implement BiDi algorithm + //if (currentScript.HorizontalDirection != direction) + //{ + // if (!UnicodeUtility.IsWhiteSpace(grapheme.FirstCodepoint)) + // { + // break; + // } + //} + + if (currentScript != script) + { + if (currentScript != Script.Inherited && currentScript != Script.Common) + { + if (script == Script.Inherited || script == Script.Common) + { + script = currentScript; + } + else + { + break; + } + } + } + + if (isFallback) + { + if (defaultFont.TryGetGlyph(grapheme.FirstCodepoint, out _)) + { + break; + } + } + + if (!font.TryGetGlyph(grapheme.FirstCodepoint, out _)) + { + if (!grapheme.FirstCodepoint.IsWhiteSpace) + { + break; + } + } + + count += grapheme.Text.Length; + } + + return count > 0; } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs new file mode 100644 index 00000000000..ffd65423a30 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingProperties.cs @@ -0,0 +1,23 @@ +namespace Avalonia.Media.TextFormatting +{ + /// + /// Properties of text collapsing + /// + public abstract class TextCollapsingProperties + { + /// + /// Gets the width in which the collapsible range is constrained to + /// + public abstract double Width { get; } + + /// + /// Gets the text run that is used as collapsing symbol + /// + public abstract TextRun Symbol { get; } + + /// + /// Gets the style of collapsing + /// + public abstract TextCollapsingStyle Style { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs new file mode 100644 index 00000000000..1523cc4d9aa --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextCollapsingStyle.cs @@ -0,0 +1,18 @@ +namespace Avalonia.Media.TextFormatting +{ + /// + /// Text collapsing style + /// + public enum TextCollapsingStyle + { + /// + /// Collapse trailing characters + /// + TrailingCharacter, + + /// + /// Collapse trailing words + /// + TrailingWord, + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormat.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormat.cs deleted file mode 100644 index 18dd6c7c109..00000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormat.cs +++ /dev/null @@ -1,71 +0,0 @@ -using System; - -namespace Avalonia.Media.TextFormatting -{ - /// - /// Unique text formatting properties that are used by the . - /// - public readonly struct TextFormat : IEquatable - { - public TextFormat(Typeface typeface, double fontRenderingEmSize) - { - Typeface = typeface; - FontRenderingEmSize = fontRenderingEmSize; - FontMetrics = new FontMetrics(typeface, fontRenderingEmSize); - } - - /// - /// Gets the typeface. - /// - /// - /// The typeface. - /// - public Typeface Typeface { get; } - - /// - /// Gets the font rendering em size. - /// - /// - /// The em rendering size of the font. - /// - public double FontRenderingEmSize { get; } - - /// - /// Gets the font metrics. - /// - /// - /// The metrics of the font. - /// - public FontMetrics FontMetrics { get; } - - public static bool operator ==(TextFormat self, TextFormat other) - { - return self.Equals(other); - } - - public static bool operator !=(TextFormat self, TextFormat other) - { - return !(self == other); - } - - public bool Equals(TextFormat other) - { - return Typeface.Equals(other.Typeface) && FontRenderingEmSize.Equals(other.FontRenderingEmSize); - } - - public override bool Equals(object obj) - { - return obj is TextFormat other && Equals(other); - } - - public override int GetHashCode() - { - unchecked - { - var hashCode = (Typeface != null ? Typeface.GetHashCode() : 0); - hashCode = (hashCode * 397) ^ FontRenderingEmSize.GetHashCode(); - return hashCode; - } - } - } -} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs index 7da39dc5dc9..e4c898e2b86 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatter.cs @@ -1,5 +1,4 @@ using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Utility; namespace Avalonia.Media.TextFormatting { @@ -22,7 +21,7 @@ public static TextFormatter Current return current; } - current = new SimpleTextFormatter(); + current = new TextFormatterImpl(); AvaloniaLocator.CurrentMutable.Bind().ToConstant(current); @@ -38,149 +37,10 @@ public static TextFormatter Current /// A value that specifies the width of the paragraph that the line fills. /// A value that represents paragraph properties, /// such as TextWrapping, TextAlignment, or TextStyle. + /// A value that specifies the text formatter state, + /// in terms of where the previous line in the paragraph was broken by the text formatting process. /// The formatted line. public abstract TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, - TextParagraphProperties paragraphProperties); - - /// - /// Creates a text style run with unique properties. - /// - /// The text to create text runs from. - /// - /// A list of text runs. - protected TextStyleRun CreateShapableTextStyleRun(ReadOnlySlice text, TextStyle defaultStyle) - { - var defaultTypeface = defaultStyle.TextFormat.Typeface; - - var currentTypeface = defaultTypeface; - - if (TryGetRunProperties(text, currentTypeface, defaultTypeface, out var count)) - { - return new TextStyleRun(new TextPointer(text.Start, count), new TextStyle(currentTypeface, - defaultStyle.TextFormat.FontRenderingEmSize, - defaultStyle.Foreground, defaultStyle.TextDecorations)); - - } - - var codepoint = Codepoint.ReadAt(text, count, out _); - - //ToDo: Fix FontFamily fallback - currentTypeface = - FontManager.Current.MatchCharacter(codepoint, defaultTypeface.Weight, defaultTypeface.Style, defaultStyle.TextFormat.Typeface.FontFamily); - - if (currentTypeface != null && TryGetRunProperties(text, currentTypeface, defaultTypeface, out count)) - { - //Fallback found - return new TextStyleRun(new TextPointer(text.Start, count), new TextStyle(currentTypeface, - defaultStyle.TextFormat.FontRenderingEmSize, - defaultStyle.Foreground, defaultStyle.TextDecorations)); - - } - - // no fallback found - currentTypeface = defaultTypeface; - - var glyphTypeface = currentTypeface.GlyphTypeface; - - var enumerator = new GraphemeEnumerator(text); - - while (enumerator.MoveNext()) - { - var grapheme = enumerator.Current; - - if (!grapheme.FirstCodepoint.IsWhiteSpace && glyphTypeface.TryGetGlyph(grapheme.FirstCodepoint, out _)) - { - break; - } - - count += grapheme.Text.Length; - } - - return new TextStyleRun(new TextPointer(text.Start, count), - new TextStyle(currentTypeface, defaultStyle.TextFormat.FontRenderingEmSize, - defaultStyle.Foreground, defaultStyle.TextDecorations)); - } - - /// - /// Tries to get run properties. - /// - /// - /// - /// The typeface that is used to find matching characters. - /// - /// - protected bool TryGetRunProperties(ReadOnlySlice text, Typeface typeface, Typeface defaultTypeface, - out int count) - { - if (text.Length == 0) - { - count = 0; - return false; - } - - var isFallback = typeface != defaultTypeface; - - count = 0; - var script = Script.Common; - //var direction = BiDiClass.LeftToRight; - - var font = typeface.GlyphTypeface; - var defaultFont = defaultTypeface.GlyphTypeface; - - var enumerator = new GraphemeEnumerator(text); - - while (enumerator.MoveNext()) - { - var grapheme = enumerator.Current; - - var currentScript = grapheme.FirstCodepoint.Script; - - //var currentDirection = grapheme.FirstCodepoint.BiDiClass; - - //// ToDo: Implement BiDi algorithm - //if (currentScript.HorizontalDirection != direction) - //{ - // if (!UnicodeUtility.IsWhiteSpace(grapheme.FirstCodepoint)) - // { - // break; - // } - //} - - if (currentScript != script) - { - if (currentScript != Script.Inherited && currentScript != Script.Common) - { - if (script == Script.Inherited || script == Script.Common) - { - script = currentScript; - } - else - { - break; - } - } - } - - if (isFallback) - { - if (defaultFont.TryGetGlyph(grapheme.FirstCodepoint, out _)) - { - break; - } - } - - if (!font.TryGetGlyph(grapheme.FirstCodepoint, out _)) - { - if (!grapheme.FirstCodepoint.IsWhiteSpace) - { - break; - } - } - - count += grapheme.Text.Length; - } - - return count > 0; - } + TextParagraphProperties paragraphProperties, TextLineBreak previousLineBreak = null); } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs new file mode 100644 index 00000000000..9318fcc68e5 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -0,0 +1,496 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media.TextFormatting.Unicode; + +namespace Avalonia.Media.TextFormatting +{ + internal class TextFormatterImpl : TextFormatter + { + /// + public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, + TextParagraphProperties paragraphProperties, TextLineBreak previousLineBreak = null) + { + var textWrapping = paragraphProperties.TextWrapping; + + var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, previousLineBreak, out var nextLineBreak); + + var textRange = GetTextRange(textRuns); + + TextLine textLine; + + switch (textWrapping) + { + case TextWrapping.NoWrap: + { + var textLineMetrics = + TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties); + + textLine = new TextLineImpl(textRuns, textLineMetrics, nextLineBreak); + break; + } + case TextWrapping.WrapWithOverflow: + case TextWrapping.Wrap: + { + textLine = PerformTextWrapping(textRuns, textRange, paragraphWidth, paragraphProperties); + break; + } + default: + throw new ArgumentOutOfRangeException(); + } + + return textLine; + } + + /// + /// Measures the number of characters that fits into available width. + /// + /// The text run. + /// The available width. + /// + internal static int MeasureCharacters(ShapedTextCharacters textCharacters, double availableWidth) + { + var glyphRun = textCharacters.GlyphRun; + + if (glyphRun.Bounds.Width < availableWidth) + { + return glyphRun.Characters.Length; + } + + var glyphCount = 0; + + var currentWidth = 0.0; + + if (glyphRun.GlyphAdvances.IsEmpty) + { + var glyphTypeface = glyphRun.GlyphTypeface; + + for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) + { + var glyph = glyphRun.GlyphIndices[i]; + + var advance = glyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; + + if (currentWidth + advance > availableWidth) + { + break; + } + + currentWidth += advance; + + glyphCount++; + } + } + else + { + foreach (var advance in glyphRun.GlyphAdvances) + { + if (currentWidth + advance > availableWidth) + { + break; + } + + currentWidth += advance; + + glyphCount++; + } + } + + if (glyphCount == glyphRun.GlyphIndices.Length) + { + return glyphRun.Characters.Length; + } + + if (glyphRun.GlyphClusters.IsEmpty) + { + return glyphCount; + } + + var firstCluster = glyphRun.GlyphClusters[0]; + + var lastCluster = glyphRun.GlyphClusters[glyphCount]; + + return lastCluster - firstCluster; + } + + /// + /// Split a sequence of runs into two segments at specified length. + /// + /// The text run's. + /// The length to split at. + /// The split text runs. + internal static SplitTextRunsResult SplitTextRuns(IReadOnlyList textRuns, int length) + { + var currentLength = 0; + + for (var i = 0; i < textRuns.Count; i++) + { + var currentRun = textRuns[i]; + + if (currentLength + currentRun.GlyphRun.Characters.Length < length) + { + currentLength += currentRun.GlyphRun.Characters.Length; + continue; + } + + var firstCount = currentRun.GlyphRun.Characters.Length >= 1 ? i + 1 : i; + + var first = new ShapedTextCharacters[firstCount]; + + if (firstCount > 1) + { + for (var j = 0; j < i; j++) + { + first[j] = textRuns[j]; + } + } + + var secondCount = textRuns.Count - firstCount; + + if (currentLength + currentRun.GlyphRun.Characters.Length == length) + { + var second = new ShapedTextCharacters[secondCount]; + + var offset = currentRun.GlyphRun.Characters.Length > 1 ? 1 : 0; + + if (secondCount > 0) + { + for (var j = 0; j < secondCount; j++) + { + second[j] = textRuns[i + j + offset]; + } + } + + first[i] = currentRun; + + return new SplitTextRunsResult(first, second); + } + else + { + secondCount++; + + var second = new ShapedTextCharacters[secondCount]; + + if (secondCount > 0) + { + for (var j = 1; j < secondCount; j++) + { + second[j] = textRuns[i + j]; + } + } + + var split = currentRun.Split(length - currentLength); + + first[i] = split.First; + + second[0] = split.Second; + + return new SplitTextRunsResult(first, second); + } + } + + return new SplitTextRunsResult(textRuns, null); + } + + /// + /// Fetches text runs. + /// + /// The text source. + /// The first text source index. + /// Previous line break. Can be null. + /// Next line break. Can be null. + /// + /// The formatted text runs. + /// + private static IReadOnlyList FetchTextRuns(ITextSource textSource, + int firstTextSourceIndex, TextLineBreak previousLineBreak, out TextLineBreak nextLineBreak) + { + nextLineBreak = default; + + var currentLength = 0; + + var textRuns = new List(); + + if (previousLineBreak != null) + { + foreach (var shapedCharacters in previousLineBreak.RemainingCharacters) + { + if (shapedCharacters == null) + { + continue; + } + + textRuns.Add(shapedCharacters); + + if (TryGetLineBreak(shapedCharacters, out var runLineBreak)) + { + var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap); + + nextLineBreak = new TextLineBreak(splitResult.Second); + + return splitResult.First; + } + + currentLength += shapedCharacters.Text.Length; + } + } + + firstTextSourceIndex += currentLength; + + var textRunEnumerator = new TextRunEnumerator(textSource, firstTextSourceIndex); + + while (textRunEnumerator.MoveNext()) + { + var textRun = textRunEnumerator.Current; + + switch (textRun) + { + case TextCharacters textCharacters: + { + var shapeableRuns = textCharacters.GetShapeableCharacters(); + + foreach (var run in shapeableRuns) + { + var glyphRun = TextShaper.Current.ShapeText(run.Text, run.Properties.Typeface, + run.Properties.FontRenderingEmSize, run.Properties.CultureInfo); + + var shapedCharacters = new ShapedTextCharacters(glyphRun, run.Properties); + + textRuns.Add(shapedCharacters); + } + + break; + } + } + + if (TryGetLineBreak(textRun, out var runLineBreak)) + { + var splitResult = SplitTextRuns(textRuns, currentLength + runLineBreak.PositionWrap); + + nextLineBreak = new TextLineBreak(splitResult.Second); + + return splitResult.First; + } + + currentLength += textRun.Text.Length; + } + + return textRuns; + } + + private static bool TryGetLineBreak(TextRun textRun, out LineBreak lineBreak) + { + lineBreak = default; + + if (textRun.Text.IsEmpty) + { + return false; + } + + var lineBreakEnumerator = new LineBreakEnumerator(textRun.Text); + + while (lineBreakEnumerator.MoveNext()) + { + if (!lineBreakEnumerator.Current.Required) + { + continue; + } + + lineBreak = lineBreakEnumerator.Current; + + if (lineBreak.PositionWrap >= textRun.Text.Length) + { + return true; + } + + //The line breaker isn't treating \n\r as a pair so we have to fix that here. + if (textRun.Text[lineBreak.PositionMeasure] == '\n' + && textRun.Text[lineBreak.PositionWrap] == '\r') + { + lineBreak = new LineBreak(lineBreak.PositionMeasure, lineBreak.PositionWrap + 1, + lineBreak.Required); + } + + return true; + } + + return false; + } + + /// + /// Performs text wrapping returns a list of text lines. + /// + /// The text run's. + /// The text range that is covered by the text runs. + /// The paragraph width. + /// The text paragraph properties. + /// The wrapped text line. + private static TextLine PerformTextWrapping(IReadOnlyList textRuns, TextRange textRange, + double paragraphWidth, TextParagraphProperties paragraphProperties) + { + var availableWidth = paragraphWidth; + var currentWidth = 0.0; + var runIndex = 0; + var currentLength = 0; + + while (runIndex < textRuns.Count) + { + var currentRun = textRuns[runIndex]; + + if (currentWidth + currentRun.GlyphRun.Bounds.Width > availableWidth) + { + var measuredLength = MeasureCharacters(currentRun, paragraphWidth - currentWidth); + + var breakFound = false; + + var currentBreakPosition = 0; + + if (measuredLength < currentRun.Text.Length) + { + var lineBreaker = new LineBreakEnumerator(currentRun.Text); + + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) + { + var nextBreakPosition = lineBreaker.Current.PositionWrap; + + if (nextBreakPosition == 0 || nextBreakPosition > measuredLength) + { + break; + } + + breakFound = lineBreaker.Current.Required || + lineBreaker.Current.PositionWrap != currentRun.Text.Length; + + currentBreakPosition = nextBreakPosition; + } + } + + if (breakFound) + { + measuredLength = currentBreakPosition; + } + else + { + if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) + { + var lineBreaker = new LineBreakEnumerator(currentRun.Text.Skip(currentBreakPosition)); + + if (lineBreaker.MoveNext()) + { + measuredLength = currentBreakPosition + lineBreaker.Current.PositionWrap; + } + } + } + + currentLength += measuredLength; + + var splitResult = SplitTextRuns(textRuns, currentLength); + + var textLineMetrics = TextLineMetrics.Create(splitResult.First, + new TextRange(textRange.Start, currentLength), paragraphWidth, paragraphProperties); + + var lineBreak = splitResult.Second != null && splitResult.Second.Count > 0 ? + new TextLineBreak(splitResult.Second) : + null; + + return new TextLineImpl(splitResult.First, textLineMetrics, lineBreak); + } + + currentWidth += currentRun.GlyphRun.Bounds.Width; + + currentLength += currentRun.GlyphRun.Characters.Length; + + runIndex++; + } + + return new TextLineImpl(textRuns, + TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties)); + } + + /// + /// Gets the text range that is covered by the text runs. + /// + /// The text runs. + /// The text range that is covered by the text runs. + private static TextRange GetTextRange(IReadOnlyList textRuns) + { + if (textRuns is null || textRuns.Count == 0) + { + return new TextRange(); + } + + var firstTextRun = textRuns[0]; + + if (textRuns.Count == 1) + { + return new TextRange(firstTextRun.Text.Start, firstTextRun.Text.Length); + } + + var start = firstTextRun.Text.Start; + + var end = textRuns[textRuns.Count - 1].Text.End + 1; + + return new TextRange(start, end - start); + } + + internal readonly struct SplitTextRunsResult + { + public SplitTextRunsResult(IReadOnlyList first, IReadOnlyList second) + { + First = first; + + Second = second; + } + + /// + /// Gets the first text runs. + /// + /// + /// The first text runs. + /// + public IReadOnlyList First { get; } + + /// + /// Gets the second text runs. + /// + /// + /// The second text runs. + /// + public IReadOnlyList Second { get; } + } + + private struct TextRunEnumerator + { + private readonly ITextSource _textSource; + private int _pos; + + public TextRunEnumerator(ITextSource textSource, int firstTextSourceIndex) + { + _textSource = textSource; + _pos = firstTextSourceIndex; + Current = null; + } + + // ReSharper disable once MemberHidesStaticFromOuterClass + public TextRun Current { get; private set; } + + public bool MoveNext() + { + Current = _textSource.GetTextRun(_pos); + + if (Current is null) + { + return false; + } + + if (Current.TextSourceLength == 0) + { + return false; + } + + _pos += Current.TextSourceLength; + + return !(Current is TextEndOfLine); + } + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs index 0292398782b..14602a25605 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -1,11 +1,7 @@ using System; using System.Collections.Generic; using System.Linq; -using Avalonia.Media.Immutable; -using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Platform; using Avalonia.Utilities; -using Avalonia.Utility; namespace Avalonia.Media.TextFormatting { @@ -14,11 +10,12 @@ namespace Avalonia.Media.TextFormatting /// public class TextLayout { - private static readonly ReadOnlySlice s_empty = new ReadOnlySlice(new[] { '\u200B' }); + private static readonly char[] s_empty = { '\u200B' }; private readonly ReadOnlySlice _text; private readonly TextParagraphProperties _paragraphProperties; - private readonly IReadOnlyList _textStyleOverrides; + private readonly IReadOnlyList> _textStyleOverrides; + private readonly TextTrimming _textTrimming; /// /// Initializes a new instance of the class. @@ -33,6 +30,7 @@ public class TextLayout /// The text decorations. /// The maximum width. /// The maximum height. + /// The height of each line of text. /// The maximum number of text lines. /// The text style overrides. public TextLayout( @@ -46,18 +44,24 @@ public TextLayout( TextDecorationCollection textDecorations = null, double maxWidth = double.PositiveInfinity, double maxHeight = double.PositiveInfinity, + double lineHeight = double.NaN, int maxLines = 0, - IReadOnlyList textStyleOverrides = null) + IReadOnlyList> textStyleOverrides = null) { _text = string.IsNullOrEmpty(text) ? new ReadOnlySlice() : new ReadOnlySlice(text.AsMemory()); _paragraphProperties = - CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, textTrimming, textDecorations?.ToImmutable()); + CreateTextParagraphProperties(typeface, fontSize, foreground, textAlignment, textWrapping, + textDecorations, lineHeight); + + _textTrimming = textTrimming; _textStyleOverrides = textStyleOverrides; + LineHeight = lineHeight; + MaxWidth = maxWidth; MaxHeight = maxHeight; @@ -67,22 +71,29 @@ public TextLayout( UpdateLayout(); } + /// + /// Gets or sets the height of each line of text. + /// + /// + /// A value of NaN (equivalent to an attribute value of "Auto") indicates that the line height + /// is determined automatically from the current font characteristics. The default is NaN. + /// + public double LineHeight { get; } + /// /// Gets the maximum width. /// public double MaxWidth { get; } - /// /// Gets the maximum height. /// public double MaxHeight { get; } - /// /// Gets the maximum number of text lines. /// - public double MaxLines { get; } + public int MaxLines { get; } /// /// Gets the text lines. @@ -93,19 +104,19 @@ public TextLayout( public IReadOnlyList TextLines { get; private set; } /// - /// Gets the bounds of the layout. + /// Gets the size of the layout. /// /// /// The bounds. /// - public Rect Bounds { get; private set; } + public Size Size { get; private set; } /// /// Draws the text layout. /// /// The drawing context. /// The origin. - public void Draw(IDrawingContextImpl context, Point origin) + public void Draw(DrawingContext context, Point origin) { if (!TextLines.Any()) { @@ -116,7 +127,10 @@ public void Draw(IDrawingContextImpl context, Point origin) foreach (var textLine in TextLines) { - textLine.Draw(context, new Point(origin.X, currentY)); + var offsetX = TextLine.GetParagraphOffsetX(textLine.LineMetrics.Size.Width, Size.Width, + _paragraphProperties.TextAlignment); + + textLine.Draw(context, new Point(origin.X + offsetX, currentY)); currentY += textLine.LineMetrics.Size.Height; } @@ -130,38 +144,32 @@ public void Draw(IDrawingContextImpl context, Point origin) /// The foreground. /// The text alignment. /// The text wrapping. - /// The text trimming. /// The text decorations. + /// The height of each line of text. /// private static TextParagraphProperties CreateTextParagraphProperties(Typeface typeface, double fontSize, - IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping, TextTrimming textTrimming, - ImmutableTextDecoration[] textDecorations) + IBrush foreground, TextAlignment textAlignment, TextWrapping textWrapping, + TextDecorationCollection textDecorations, double lineHeight) { - var textRunStyle = new TextStyle(typeface, fontSize, foreground, textDecorations); + var textRunStyle = new GenericTextRunProperties(typeface, fontSize, textDecorations, foreground); - return new TextParagraphProperties(textRunStyle, textAlignment, textWrapping, textTrimming); + return new GenericTextParagraphProperties(textRunStyle, textAlignment, textWrapping, lineHeight); } /// /// Updates the current bounds. /// /// The text line. - /// The left. - /// The right. - /// The bottom. - private static void UpdateBounds(TextLine textLine, ref double left, ref double right, ref double bottom) + /// The current width. + /// The current height. + private static void UpdateBounds(TextLine textLine, ref double width, ref double height) { - if (right < textLine.LineMetrics.BaselineOrigin.X + textLine.LineMetrics.Size.Width) - { - right = textLine.LineMetrics.BaselineOrigin.X + textLine.LineMetrics.Size.Width; - } - - if (left < textLine.LineMetrics.BaselineOrigin.X) + if (width < textLine.LineMetrics.Size.Width) { - left = textLine.LineMetrics.BaselineOrigin.X; + width = textLine.LineMetrics.Size.Width; } - bottom += textLine.LineMetrics.Size.Height; + height += textLine.LineMetrics.Size.Height; } /// @@ -170,14 +178,15 @@ private static void UpdateBounds(TextLine textLine, ref double left, ref double /// The empty text line. private TextLine CreateEmptyTextLine(int startingIndex) { - var textFormat = _paragraphProperties.DefaultTextStyle.TextFormat; + var properties = _paragraphProperties.DefaultTextRunProperties; - var glyphRun = TextShaper.Current.ShapeText(s_empty, textFormat); + var glyphRun = TextShaper.Current.ShapeText(new ReadOnlySlice(s_empty, startingIndex, 1), + properties.Typeface, properties.FontRenderingEmSize, properties.CultureInfo); - var textRuns = new[] { new ShapedTextRun(glyphRun, _paragraphProperties.DefaultTextStyle) }; + var textRuns = new[] { new ShapedTextCharacters(glyphRun, _paragraphProperties.DefaultTextRunProperties) }; - return new SimpleTextLine(new TextPointer(startingIndex, 0), textRuns, - TextLineMetrics.Create(textRuns, MaxWidth, _paragraphProperties.TextAlignment)); + return new TextLineImpl(textRuns, + TextLineMetrics.Create(textRuns, new TextRange(startingIndex, 1), MaxWidth, _paragraphProperties)); } /// @@ -191,110 +200,112 @@ private void UpdateLayout() TextLines = new List { textLine }; - Bounds = new Rect(textLine.LineMetrics.BaselineOrigin.X, 0, 0, textLine.LineMetrics.Size.Height); + Size = new Size(0, textLine.LineMetrics.Size.Height); } else { var textLines = new List(); - double left = 0.0, right = 0.0, bottom = 0.0; - - var lineBreaker = new LineBreakEnumerator(_text); + double width = 0.0, height = 0.0; var currentPosition = 0; - while (currentPosition < _text.Length && (MaxLines == 0 || textLines.Count < MaxLines)) - { - int length; + var textSource = new FormattedTextSource(_text, + _paragraphProperties.DefaultTextRunProperties, _textStyleOverrides); - if (lineBreaker.MoveNext()) - { - if (!lineBreaker.Current.Required) - { - continue; - } + TextLine previousLine = null; - length = lineBreaker.Current.PositionWrap - currentPosition; + while (currentPosition < _text.Length) + { + var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth, + _paragraphProperties, previousLine?.LineBreak); + + currentPosition += textLine.TextRange.Length; - if (currentPosition + length < _text.Length) + if (textLines.Count > 0) + { + if (textLines.Count == MaxLines || !double.IsPositiveInfinity(MaxHeight) && + height + textLine.LineMetrics.Size.Height > MaxHeight) { - //The line breaker isn't treating \n\r as a pair so we have to fix that here. - if (_text[lineBreaker.Current.PositionMeasure] == '\n' - && _text[lineBreaker.Current.PositionWrap] == '\r') + if (previousLine?.LineBreak != null && _textTrimming != TextTrimming.None) { - length++; + var collapsedLine = + previousLine.Collapse(GetCollapsingProperties(MaxWidth)); + + textLines[textLines.Count - 1] = collapsedLine; } + + break; } } - else - { - length = _text.Length - currentPosition; - } - var remainingLength = length; + var hasOverflowed = textLine.LineMetrics.HasOverflowed; - while (remainingLength > 0 && (MaxLines == 0 || textLines.Count < MaxLines)) + if (hasOverflowed && _textTrimming != TextTrimming.None) { - var textSlice = _text.AsSlice(currentPosition, remainingLength); - - var textSource = new FormattedTextSource(textSlice, _paragraphProperties.DefaultTextStyle, _textStyleOverrides); - - var textLine = TextFormatter.Current.FormatLine(textSource, 0, MaxWidth, _paragraphProperties); - - UpdateBounds(textLine, ref left, ref right, ref bottom); - - textLines.Add(textLine); - - if (!double.IsPositiveInfinity(MaxHeight) && bottom + textLine.LineMetrics.Size.Height > MaxHeight) - { - currentPosition = _text.Length; - break; - } + textLine = textLine.Collapse(GetCollapsingProperties(MaxWidth)); + } - if (_paragraphProperties.TextTrimming != TextTrimming.None) - { - currentPosition += remainingLength; + textLines.Add(textLine); - break; - } + UpdateBounds(textLine, ref width, ref height); - remainingLength -= textLine.Text.Length; + previousLine = textLine; - currentPosition += textLine.Text.Length; + if (currentPosition != _text.Length || textLine.LineBreak == null) + { + continue; } - } - if (lineBreaker.Current.Required && currentPosition == _text.Length) - { var emptyTextLine = CreateEmptyTextLine(currentPosition); - UpdateBounds(emptyTextLine, ref left, ref right, ref bottom); - textLines.Add(emptyTextLine); } - Bounds = new Rect(left, 0, right, bottom); + Size = new Size(width, height); TextLines = textLines; } } - private struct FormattedTextSource : ITextSource + /// + /// Gets the for current text trimming mode. + /// + /// The collapsing width. + /// The . + private TextCollapsingProperties GetCollapsingProperties(double width) + { + return _textTrimming switch + { + TextTrimming.CharacterEllipsis => new TextTrailingCharacterEllipsis(width, + _paragraphProperties.DefaultTextRunProperties), + TextTrimming.WordEllipsis => new TextTrailingWordEllipsis(width, + _paragraphProperties.DefaultTextRunProperties), + _ => throw new ArgumentOutOfRangeException(), + }; + } + + private readonly struct FormattedTextSource : ITextSource { private readonly ReadOnlySlice _text; - private readonly TextStyle _defaultStyle; - private readonly IReadOnlyList _textStyleOverrides; + private readonly TextRunProperties _defaultProperties; + private readonly IReadOnlyList> _textModifier; - public FormattedTextSource(ReadOnlySlice text, TextStyle defaultStyle, - IReadOnlyList textStyleOverrides) + public FormattedTextSource(ReadOnlySlice text, TextRunProperties defaultProperties, + IReadOnlyList> textModifier) { _text = text; - _defaultStyle = defaultStyle; - _textStyleOverrides = textStyleOverrides; + _defaultProperties = defaultProperties; + _textModifier = textModifier; } public TextRun GetTextRun(int textSourceIndex) { + if (textSourceIndex > _text.End) + { + return new TextEndOfLine(); + } + var runText = _text.Skip(textSourceIndex); if (runText.IsEmpty) @@ -302,30 +313,29 @@ public TextRun GetTextRun(int textSourceIndex) return new TextEndOfLine(); } - var textStyleRun = CreateTextStyleRunWithOverride(runText, _defaultStyle, _textStyleOverrides); + var textStyleRun = CreateTextStyleRun(runText, _defaultProperties, _textModifier); - return new TextCharacters(runText.Take(textStyleRun.TextPointer.Length), textStyleRun.Style); + return new TextCharacters(runText.Take(textStyleRun.Length), textStyleRun.Value); } /// - /// Creates a text style run that has overrides applied. Only overrides with equal TextStyle. - /// If optimizeForShaping is true Foreground is ignored. + /// Creates a span of text run properties that has modifier applied. /// - /// The text to create the run for. - /// The default text style for segments that don't have an override. - /// The text style overrides. + /// The text to create the properties for. + /// The default text properties. + /// The text properties modifier. /// /// The created text style run. /// - private static TextStyleRun CreateTextStyleRunWithOverride(ReadOnlySlice text, - TextStyle defaultTextStyle, IReadOnlyList textStyleOverrides) + private static ValueSpan CreateTextStyleRun(ReadOnlySlice text, + TextRunProperties defaultProperties, IReadOnlyList> textModifier) { - if(textStyleOverrides == null || textStyleOverrides.Count == 0) + if (textModifier == null || textModifier.Count == 0) { - return new TextStyleRun(new TextPointer(text.Start, text.Length), defaultTextStyle); + return new ValueSpan(text.Start, text.Length, defaultProperties); } - var currentTextStyle = defaultTextStyle; + var currentProperties = defaultProperties; var hasOverride = false; @@ -333,35 +343,34 @@ private static TextStyleRun CreateTextStyleRunWithOverride(ReadOnlySlice t var length = 0; - for (; i < textStyleOverrides.Count; i++) + for (; i < textModifier.Count; i++) { - var styleOverride = textStyleOverrides[i]; + var propertiesOverride = textModifier[i]; - var textPointer = styleOverride.TextPointer; + var textRange = new TextRange(propertiesOverride.Start, propertiesOverride.Length); - if (textPointer.End < text.Start) + if (textRange.End < text.Start) { continue; } - if (textPointer.Start > text.End) + if (textRange.Start > text.End) { length = text.Length; break; } - if (textPointer.Start > text.Start) + if (textRange.Start > text.Start) { - if (styleOverride.Style.TextFormat != currentTextStyle.TextFormat || - !currentTextStyle.Foreground.Equals(styleOverride.Style.Foreground)) + if (propertiesOverride.Value != currentProperties) { - length = Math.Min(Math.Abs(textPointer.Start - text.Start), text.Length); + length = Math.Min(Math.Abs(textRange.Start - text.Start), text.Length); break; } } - length += Math.Min(text.Length - length, textPointer.Length); + length += Math.Min(text.Length - length, textRange.Length); if (hasOverride) { @@ -370,13 +379,12 @@ private static TextStyleRun CreateTextStyleRunWithOverride(ReadOnlySlice t hasOverride = true; - currentTextStyle = styleOverride.Style; + currentProperties = propertiesOverride.Value; } - if (length < text.Length && i == textStyleOverrides.Count) + if (length < text.Length && i == textModifier.Count) { - if (currentTextStyle.Foreground.Equals(defaultTextStyle.Foreground) && - currentTextStyle.TextFormat == defaultTextStyle.TextFormat) + if (currentProperties == defaultProperties) { length = text.Length; } @@ -387,7 +395,7 @@ private static TextStyleRun CreateTextStyleRunWithOverride(ReadOnlySlice t text = text.Take(length); } - return new TextStyleRun(new TextPointer(text.Start, length), currentTextStyle); + return new ValueSpan(text.Start, length, currentProperties); } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs index a0f7b44882e..423ca9fb7f3 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLine.cs @@ -1,5 +1,4 @@ using System.Collections.Generic; -using Avalonia.Platform; namespace Avalonia.Media.TextFormatting { @@ -9,12 +8,12 @@ namespace Avalonia.Media.TextFormatting public abstract class TextLine { /// - /// Gets the text. + /// Gets the text range that is covered by the line. /// /// - /// The text pointer. + /// The text range that is covered by the line. /// - public abstract TextPointer Text { get; } + public abstract TextRange TextRange { get; } /// /// Gets the text runs. @@ -32,48 +31,73 @@ public abstract class TextLine /// public abstract TextLineMetrics LineMetrics { get; } + /// + /// Gets the state of the line when broken by line breaking process. + /// + /// + /// A value that represents the line break. + /// + public abstract TextLineBreak LineBreak { get; } + + /// + /// Gets a value that indicates whether the line is collapsed. + /// + /// + /// true, if the line is collapsed; otherwise, false. + /// + public abstract bool HasCollapsed { get; } + /// /// Draws the at the given origin. /// /// The drawing context. /// The origin. - public abstract void Draw(IDrawingContextImpl drawingContext, Point origin); + public abstract void Draw(DrawingContext drawingContext, Point origin); /// - /// Client to get the character hit corresponding to the specified - /// distance from the beginning of the line. + /// Create a collapsed line based on collapsed text properties. /// - /// distance in text flow direction from the beginning of the line - /// The + /// A list of + /// objects that represent the collapsed text properties. + /// + /// A value that represents a collapsed line that can be displayed. + /// + public abstract TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList); + + /// + /// Gets the character hit corresponding to the specified distance from the beginning of the line. + /// + /// A value that represents the distance from the beginning of the line. + /// The object at the specified distance from the beginning of the line. public abstract CharacterHit GetCharacterHitFromDistance(double distance); /// - /// Client to get the distance from the beginning of the line from the specified + /// Gets the distance from the beginning of the line to the specified character hit. /// . /// - /// of the character to query the distance. - /// Distance in text flow direction from the beginning of the line. + /// The object whose distance you want to query. + /// A that represents the distance from the beginning of the line. public abstract double GetDistanceFromCharacterHit(CharacterHit characterHit); /// - /// Client to get the next for caret navigation. + /// Gets the next character hit for caret navigation. /// /// The current . /// The next . public abstract CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit); /// - /// Client to get the previous character hit for caret navigation + /// Gets the previous character hit for caret navigation. /// - /// the current character hit - /// The previous + /// The current . + /// The previous . public abstract CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit); /// - /// Client to get the previous character hit after backspacing + /// Gets the previous character hit after backspacing. /// - /// the current character hit - /// The after backspacing + /// The current . + /// The after backspacing. public abstract CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit); /// diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs new file mode 100644 index 00000000000..c24454cb764 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs @@ -0,0 +1,17 @@ +using System.Collections.Generic; + +namespace Avalonia.Media.TextFormatting +{ + public class TextLineBreak + { + public TextLineBreak(IReadOnlyList remainingCharacters) + { + RemainingCharacters = remainingCharacters; + } + + /// + /// Get the remaining shaped characters that were split up by the during the formatting process. + /// + public IReadOnlyList RemainingCharacters { get; } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs new file mode 100644 index 00000000000..f73a7be759a --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -0,0 +1,377 @@ +using System.Collections.Generic; +using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Platform; + +namespace Avalonia.Media.TextFormatting +{ + internal class TextLineImpl : TextLine + { + private readonly IReadOnlyList _textRuns; + + public TextLineImpl(IReadOnlyList textRuns, TextLineMetrics lineMetrics, + TextLineBreak lineBreak = null, bool hasCollapsed = false) + { + _textRuns = textRuns; + LineMetrics = lineMetrics; + LineBreak = lineBreak; + HasCollapsed = hasCollapsed; + } + + /// + public override TextRange TextRange => LineMetrics.TextRange; + + /// + public override IReadOnlyList TextRuns => _textRuns; + + /// + public override TextLineMetrics LineMetrics { get; } + + /// + public override TextLineBreak LineBreak { get; } + + /// + public override bool HasCollapsed { get; } + + /// + public override void Draw(DrawingContext drawingContext, Point origin) + { + var currentX = origin.X; + + foreach (var textRun in _textRuns) + { + var baselineOrigin = new Point(currentX, origin.Y + LineMetrics.TextBaseline); + + textRun.Draw(drawingContext, baselineOrigin); + + currentX += textRun.Bounds.Width; + } + } + + /// + public override TextLine Collapse(params TextCollapsingProperties[] collapsingPropertiesList) + { + if (collapsingPropertiesList == null || collapsingPropertiesList.Length == 0) + { + return this; + } + + var collapsingProperties = collapsingPropertiesList[0]; + var runIndex = 0; + var currentWidth = 0.0; + var textRange = TextRange; + var collapsedLength = 0; + TextLineMetrics textLineMetrics; + + var shapedSymbol = CreateShapedSymbol(collapsingProperties.Symbol); + + var availableWidth = collapsingProperties.Width - shapedSymbol.Bounds.Width; + + while (runIndex < _textRuns.Count) + { + var currentRun = _textRuns[runIndex]; + + currentWidth += currentRun.GlyphRun.Bounds.Width; + + if (currentWidth > availableWidth) + { + var measuredLength = TextFormatterImpl.MeasureCharacters(currentRun, availableWidth); + + var currentBreakPosition = 0; + + if (measuredLength < textRange.End) + { + var lineBreaker = new LineBreakEnumerator(currentRun.Text); + + while (currentBreakPosition < measuredLength && lineBreaker.MoveNext()) + { + var nextBreakPosition = lineBreaker.Current.PositionWrap; + + if (nextBreakPosition == 0) + { + break; + } + + if (nextBreakPosition > measuredLength) + { + break; + } + + currentBreakPosition = nextBreakPosition; + } + } + + if (collapsingProperties.Style == TextCollapsingStyle.TrailingWord) + { + measuredLength = currentBreakPosition; + } + + collapsedLength += measuredLength; + + var splitResult = TextFormatterImpl.SplitTextRuns(_textRuns, collapsedLength); + + var shapedTextCharacters = new List(splitResult.First.Count + 1); + + shapedTextCharacters.AddRange(splitResult.First); + + shapedTextCharacters.Add(shapedSymbol); + + textRange = new TextRange(textRange.Start, collapsedLength); + + var shapedWidth = GetShapedWidth(shapedTextCharacters); + + textLineMetrics = new TextLineMetrics(new Size(shapedWidth, LineMetrics.Size.Height), + LineMetrics.TextBaseline, textRange, false); + + return new TextLineImpl(shapedTextCharacters, textLineMetrics, LineBreak, true); + } + + availableWidth -= currentRun.GlyphRun.Bounds.Width; + + collapsedLength += currentRun.GlyphRun.Characters.Length; + + runIndex++; + } + + textLineMetrics = + new TextLineMetrics(LineMetrics.Size.WithWidth(LineMetrics.Size.Width + shapedSymbol.Bounds.Width), + LineMetrics.TextBaseline, TextRange, LineMetrics.HasOverflowed); + + return new TextLineImpl(new List(_textRuns) { shapedSymbol }, textLineMetrics, null, + true); + } + + /// + public override CharacterHit GetCharacterHitFromDistance(double distance) + { + if (distance < 0) + { + // hit happens before the line, return the first position + return new CharacterHit(TextRange.Start); + } + + // process hit that happens within the line + var characterHit = new CharacterHit(); + + foreach (var run in _textRuns) + { + characterHit = run.GlyphRun.GetCharacterHitFromDistance(distance, out _); + + if (distance <= run.Bounds.Width) + { + break; + } + + distance -= run.Bounds.Width; + } + + return characterHit; + } + + /// + public override double GetDistanceFromCharacterHit(CharacterHit characterHit) + { + return DistanceFromCodepointIndex(characterHit.FirstCharacterIndex + (characterHit.TrailingLength != 0 ? 1 : 0)); + } + + /// + public override CharacterHit GetNextCaretCharacterHit(CharacterHit characterHit) + { + if (TryFindNextCharacterHit(characterHit, out var nextCharacterHit)) + { + return nextCharacterHit; + } + + return characterHit; // Can't move, we're after the last character + } + + /// + public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) + { + if (TryFindPreviousCharacterHit(characterHit, out var previousCharacterHit)) + { + return previousCharacterHit; + } + + return characterHit; // Can't move, we're before the first character + } + + /// + public override CharacterHit GetBackspaceCaretCharacterHit(CharacterHit characterHit) + { + // same operation as move-to-previous + return GetPreviousCaretCharacterHit(characterHit); + } + + /// + /// Get distance from line start to the specified codepoint index. + /// + private double DistanceFromCodepointIndex(int codepointIndex) + { + var currentDistance = 0.0; + + foreach (var textRun in _textRuns) + { + if (codepointIndex > textRun.Text.End) + { + currentDistance += textRun.Bounds.Width; + + continue; + } + + return currentDistance + textRun.GlyphRun.GetDistanceFromCharacterHit(new CharacterHit(codepointIndex)); + } + + return currentDistance; + } + + /// + /// Tries to find the next character hit. + /// + /// The current character hit. + /// The next character hit. + /// + private bool TryFindNextCharacterHit(CharacterHit characterHit, out CharacterHit nextCharacterHit) + { + nextCharacterHit = characterHit; + + var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + + if (codepointIndex >= TextRange.Start + TextRange.Length) + { + return false; // Cannot go forward anymore + } + + var runIndex = GetRunIndexAtCodepointIndex(codepointIndex); + + while (runIndex < TextRuns.Count) + { + var run = _textRuns[runIndex]; + + var foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); + + nextCharacterHit = characterHit.TrailingLength != 0 ? + foundCharacterHit : + new CharacterHit(foundCharacterHit.FirstCharacterIndex + foundCharacterHit.TrailingLength); + + if (nextCharacterHit.FirstCharacterIndex > characterHit.FirstCharacterIndex) + { + return true; + } + + runIndex++; + } + + return false; + } + + /// + /// Tries to find the previous character hit. + /// + /// The current character hit. + /// The previous character hit. + /// + private bool TryFindPreviousCharacterHit(CharacterHit characterHit, out CharacterHit previousCharacterHit) + { + previousCharacterHit = characterHit; + + var codepointIndex = characterHit.FirstCharacterIndex + characterHit.TrailingLength; + + if (codepointIndex < TextRange.Start) + { + return false; // Cannot go backward anymore. + } + + var runIndex = GetRunIndexAtCodepointIndex(codepointIndex); + + while (runIndex >= 0) + { + var run = _textRuns[runIndex]; + + var foundCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); + + previousCharacterHit = characterHit.TrailingLength != 0 ? + foundCharacterHit : + new CharacterHit(foundCharacterHit.FirstCharacterIndex); + + if (previousCharacterHit.FirstCharacterIndex < characterHit.FirstCharacterIndex) + { + return true; + } + + runIndex--; + } + + return false; + } + + /// + /// Gets the run index of the specified codepoint index. + /// + /// The codepoint index. + /// The text run index. + private int GetRunIndexAtCodepointIndex(int codepointIndex) + { + if (codepointIndex >= TextRange.End) + { + return _textRuns.Count - 1; + } + + if (codepointIndex <= 0) + { + return 0; + } + + var runIndex = 0; + + while (runIndex < _textRuns.Count) + { + var run = _textRuns[runIndex]; + + if (run.Text.End > codepointIndex) + { + return runIndex; + } + + runIndex++; + } + + return runIndex; + } + + /// + /// Creates a shaped symbol. + /// + /// The symbol run to shape. + /// + /// The shaped symbol. + /// + internal static ShapedTextCharacters CreateShapedSymbol(TextRun textRun) + { + var formatterImpl = AvaloniaLocator.Current.GetService(); + + var glyphRun = formatterImpl.ShapeText(textRun.Text, textRun.Properties.Typeface, textRun.Properties.FontRenderingEmSize, + textRun.Properties.CultureInfo); + + return new ShapedTextCharacters(glyphRun, textRun.Properties); + } + + /// + /// Gets the shaped width of specified shaped text characters. + /// + /// The shaped text characters. + /// + /// The shaped width. + /// + private static double GetShapedWidth(IReadOnlyList shapedTextCharacters) + { + var shapedWidth = 0.0; + + for (var i = 0; i < shapedTextCharacters.Count; i++) + { + shapedWidth += shapedTextCharacters[i].Bounds.Width; + } + + return shapedWidth; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs index 096305c09c0..6875cc1c04a 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs @@ -1,4 +1,5 @@ using System.Collections.Generic; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -8,38 +9,21 @@ namespace Avalonia.Media.TextFormatting /// public readonly struct TextLineMetrics { - public TextLineMetrics(double width, double xOrigin, double ascent, double descent, double lineGap) + public TextLineMetrics(Size size, double textBaseline, TextRange textRange, bool hasOverflowed) { - Ascent = ascent; - Descent = descent; - LineGap = lineGap; - Size = new Size(width, descent - ascent + lineGap); - BaselineOrigin = new Point(xOrigin, -ascent); + Size = size; + TextBaseline = textBaseline; + TextRange = textRange; + HasOverflowed = hasOverflowed; } /// - /// Gets the overall recommended distance above the baseline. + /// Gets the text range that is covered by the text line. /// /// - /// The ascent. + /// The text range that is covered by the text line. /// - public double Ascent { get; } - - /// - /// Gets the overall recommended distance under the baseline. - /// - /// - /// The descent. - /// - public double Descent { get; } - - /// - /// Gets the overall recommended additional space between two lines of text. - /// - /// - /// The leading. - /// - public double LineGap { get; } + public TextRange TextRange { get; } /// /// Gets the size of the text line. @@ -50,21 +34,26 @@ public TextLineMetrics(double width, double xOrigin, double ascent, double desce public Size Size { get; } /// - /// Gets the baseline origin. + /// Gets the distance from the top to the baseline of the line of text. /// - /// - /// The baseline origin. - /// - public Point BaselineOrigin { get; } + public double TextBaseline { get; } + + /// + /// Gets a boolean value that indicates whether content of the line overflows + /// the specified paragraph width. + /// + public bool HasOverflowed { get; } /// /// Creates the text line metrics. /// /// The text runs. + /// The text range that is covered by the text line. /// The paragraph width. - /// The text alignment. + /// The text alignment. /// - public static TextLineMetrics Create(IEnumerable textRuns, double paragraphWidth, TextAlignment textAlignment) + public static TextLineMetrics Create(IEnumerable textRuns, TextRange textRange, double paragraphWidth, + TextParagraphProperties paragraphProperties) { var lineWidth = 0.0; var ascent = 0.0; @@ -73,31 +62,35 @@ public static TextLineMetrics Create(IEnumerable textRuns, double parag foreach (var textRun in textRuns) { - var shapedRun = (ShapedTextRun)textRun; + var shapedRun = (ShapedTextCharacters)textRun; - lineWidth += shapedRun.Bounds.Width; + var fontMetrics = + new FontMetrics(shapedRun.Properties.Typeface, shapedRun.Properties.FontRenderingEmSize); - var textFormat = textRun.Style.TextFormat; + lineWidth += shapedRun.Bounds.Width; - if (ascent > textRun.Style.TextFormat.FontMetrics.Ascent) + if (ascent > fontMetrics.Ascent) { - ascent = textFormat.FontMetrics.Ascent; + ascent = fontMetrics.Ascent; } - if (descent < textFormat.FontMetrics.Descent) + if (descent < fontMetrics.Descent) { - descent = textFormat.FontMetrics.Descent; + descent = fontMetrics.Descent; } - if (lineGap < textFormat.FontMetrics.LineGap) + if (lineGap < fontMetrics.LineGap) { - lineGap = textFormat.FontMetrics.LineGap; + lineGap = fontMetrics.LineGap; } } - var xOrigin = TextLine.GetParagraphOffsetX(lineWidth, paragraphWidth, textAlignment); + var size = new Size(lineWidth, + double.IsNaN(paragraphProperties.LineHeight) || MathUtilities.IsZero(paragraphProperties.LineHeight) ? + descent - ascent + lineGap : + paragraphProperties.LineHeight); - return new TextLineMetrics(lineWidth, xOrigin, ascent, descent, lineGap); + return new TextLineMetrics(size, -ascent, textRange, size.Width > paragraphWidth); } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs index 1368f1777aa..3ecd1aafd93 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs @@ -3,38 +3,32 @@ /// /// Provides a set of properties that are used during the paragraph layout. /// - public readonly struct TextParagraphProperties + public abstract class TextParagraphProperties { - public TextParagraphProperties( - TextStyle defaultTextStyle, - TextAlignment textAlignment = TextAlignment.Left, - TextWrapping textWrapping = TextWrapping.NoWrap, - TextTrimming textTrimming = TextTrimming.None) - { - DefaultTextStyle = defaultTextStyle; - TextAlignment = textAlignment; - TextWrapping = textWrapping; - TextTrimming = textTrimming; - } + /// + /// Gets the text alignment. + /// + public abstract TextAlignment TextAlignment { get; } /// /// Gets the default text style. /// - public TextStyle DefaultTextStyle { get; } + public abstract TextRunProperties DefaultTextRunProperties { get; } /// - /// Gets the text alignment. + /// If not null, text decorations to apply to all runs in the line. This is in addition + /// to any text decorations specified by the TextRunProperties for individual text runs. /// - public TextAlignment TextAlignment { get; } + public virtual TextDecorationCollection TextDecorations => null; /// /// Gets the text wrapping. /// - public TextWrapping TextWrapping { get; } + public abstract TextWrapping TextWrapping { get; } /// - /// Gets the text trimming. + /// Paragraph's line height /// - public TextTrimming TextTrimming { get; } + public abstract double LineHeight { get; } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextPointer.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRange.cs similarity index 73% rename from src/Avalonia.Visuals/Media/TextFormatting/TextPointer.cs rename to src/Avalonia.Visuals/Media/TextFormatting/TextRange.cs index 65d5c04b4c3..1177c758f44 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextPointer.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRange.cs @@ -5,9 +5,9 @@ namespace Avalonia.Media.TextFormatting /// /// References a portion of a text buffer. /// - public readonly struct TextPointer + public readonly struct TextRange { - public TextPointer(int start, int length) + public TextRange(int start, int length) { Start = start; Length = length; @@ -41,30 +41,30 @@ public TextPointer(int start, int length) /// Returns a specified number of contiguous elements from the start of the slice. /// /// The number of elements to return. - /// A that contains the specified number of elements from the start of this slice. - public TextPointer Take(int length) + /// A that contains the specified number of elements from the start of this slice. + public TextRange Take(int length) { if (length > Length) { throw new ArgumentOutOfRangeException(nameof(length)); } - return new TextPointer(Start, length); + return new TextRange(Start, length); } /// /// Bypasses a specified number of elements in the slice and then returns the remaining elements. /// /// The number of elements to skip before returning the remaining elements. - /// A that contains the elements that occur after the specified index in this slice. - public TextPointer Skip(int length) + /// A that contains the elements that occur after the specified index in this slice. + public TextRange Skip(int length) { if (length > Length) { throw new ArgumentOutOfRangeException(nameof(length)); } - return new TextPointer(Start + length, Length - length); + return new TextRange(Start + length, Length - length); } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs index 28b83333b9d..c15a771755a 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRun.cs @@ -1,5 +1,5 @@ using System.Diagnostics; -using Avalonia.Utility; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -9,15 +9,22 @@ namespace Avalonia.Media.TextFormatting [DebuggerTypeProxy(typeof(TextRunDebuggerProxy))] public abstract class TextRun { + public static readonly int DefaultTextSourceLength = 1; + + /// + /// Gets the text source length. + /// + public virtual int TextSourceLength => DefaultTextSourceLength; + /// /// Gets the text run's text. /// - public ReadOnlySlice Text { get; protected set; } + public virtual ReadOnlySlice Text => default; /// - /// Gets the text run's style. + /// A set of properties shared by every characters in the run /// - public TextStyle Style { get; protected set; } + public virtual TextRunProperties Properties => null; private class TextRunDebuggerProxy { @@ -42,7 +49,7 @@ public string Text } } - public TextStyle Style => _textRun.Style; + public TextRunProperties Properties => _textRun.Properties; } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs new file mode 100644 index 00000000000..c4f9443c3d5 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs @@ -0,0 +1,89 @@ +using System; +using System.Globalization; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Provides a set of properties, such as typeface or foreground brush, that can be applied to a TextRun object. This is an abstract class. + /// + /// + /// The text layout client provides a concrete implementation of this abstract class. + /// This enables the client to implement text run properties in a way that corresponds with the associated formatting store. + /// + public abstract class TextRunProperties : IEquatable + { + /// + /// Run typeface + /// + public abstract Typeface Typeface { get; } + + /// + /// Em size of font used to format and display text + /// + public abstract double FontRenderingEmSize { get; } + + /// + /// Run TextDecorations. + /// + public abstract TextDecorationCollection TextDecorations { get; } + + /// + /// Brush used to fill text. + /// + public abstract IBrush ForegroundBrush { get; } + + /// + /// Brush used to paint background of run. + /// + public abstract IBrush BackgroundBrush { get; } + + /// + /// Run text culture. + /// + public abstract CultureInfo CultureInfo { get; } + + public bool Equals(TextRunProperties other) + { + if (ReferenceEquals(null, other)) + return false; + if (ReferenceEquals(this, other)) + return true; + + return Typeface.Equals(other.Typeface) && + FontRenderingEmSize.Equals(other.FontRenderingEmSize) + && Equals(TextDecorations, other.TextDecorations) && + Equals(ForegroundBrush, other.ForegroundBrush) && + Equals(BackgroundBrush, other.BackgroundBrush) && + Equals(CultureInfo, other.CultureInfo); + } + + public override bool Equals(object obj) + { + return ReferenceEquals(this, obj) || obj is TextRunProperties other && Equals(other); + } + + public override int GetHashCode() + { + unchecked + { + var hashCode = (Typeface != null ? Typeface.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ FontRenderingEmSize.GetHashCode(); + hashCode = (hashCode * 397) ^ (TextDecorations != null ? TextDecorations.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (ForegroundBrush != null ? ForegroundBrush.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (BackgroundBrush != null ? BackgroundBrush.GetHashCode() : 0); + hashCode = (hashCode * 397) ^ (CultureInfo != null ? CultureInfo.GetHashCode() : 0); + return hashCode; + } + } + + public static bool operator ==(TextRunProperties left, TextRunProperties right) + { + return Equals(left, right); + } + + public static bool operator !=(TextRunProperties left, TextRunProperties right) + { + return !Equals(left, right); + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs index eb3a4129bcc..a02ace408fc 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextShaper.cs @@ -1,6 +1,7 @@ using System; +using System.Globalization; using Avalonia.Platform; -using Avalonia.Utility; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting { @@ -44,9 +45,10 @@ public static TextShaper Current } /// - public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat) + public GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, + CultureInfo culture) { - return _platformImpl.ShapeText(text, textFormat); + return _platformImpl.ShapeText(text, typeface, fontRenderingEmSize, culture); } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextStyle.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextStyle.cs deleted file mode 100644 index cf52c3ca177..00000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextStyle.cs +++ /dev/null @@ -1,39 +0,0 @@ -using Avalonia.Media.Immutable; - -namespace Avalonia.Media.TextFormatting -{ - /// - /// Unique text formatting properties that effect the styling of a text. - /// - public readonly struct TextStyle - { - public TextStyle(Typeface typeface, double fontRenderingEmSize = 12, IBrush foreground = null, - ImmutableTextDecoration[] textDecorations = null) - : this(new TextFormat(typeface, fontRenderingEmSize), foreground, textDecorations) - { - } - - public TextStyle(TextFormat textFormat, IBrush foreground = null, - ImmutableTextDecoration[] textDecorations = null) - { - TextFormat = textFormat; - Foreground = foreground; - TextDecorations = textDecorations; - } - - /// - /// Gets the text format. - /// - public TextFormat TextFormat { get; } - - /// - /// Gets the foreground. - /// - public IBrush Foreground { get; } - - /// - /// Gets the text decorations. - /// - public ImmutableTextDecoration[] TextDecorations { get; } - } -} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextStyleRun.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextStyleRun.cs deleted file mode 100644 index 55f89991829..00000000000 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextStyleRun.cs +++ /dev/null @@ -1,24 +0,0 @@ -namespace Avalonia.Media.TextFormatting -{ - /// - /// Represents a text run's style and is used during the layout process of the . - /// - public readonly struct TextStyleRun - { - public TextStyleRun(TextPointer textPointer, TextStyle style) - { - TextPointer = textPointer; - Style = style; - } - - /// - /// Gets the text pointer. - /// - public TextPointer TextPointer { get; } - - /// - /// Gets the text style. - /// - public TextStyle Style { get; } - } -} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs new file mode 100644 index 00000000000..4bd46e8c751 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingCharacterEllipsis.cs @@ -0,0 +1,33 @@ +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// a collapsing properties to collapse whole line toward the end + /// at character granularity and with ellipsis being the collapsing symbol + /// + public class TextTrailingCharacterEllipsis : TextCollapsingProperties + { + private static readonly ReadOnlySlice s_ellipsis = new ReadOnlySlice(new[] { '\u2026' }); + + /// + /// Construct a text trailing character ellipsis collapsing properties + /// + /// width in which collapsing is constrained to + /// text run properties of ellispis symbol + public TextTrailingCharacterEllipsis(double width, TextRunProperties textRunProperties) + { + Width = width; + Symbol = new TextCharacters(s_ellipsis, textRunProperties); + } + + /// + public sealed override double Width { get; } + + /// + public sealed override TextRun Symbol { get; } + + /// + public sealed override TextCollapsingStyle Style { get; } = TextCollapsingStyle.TrailingCharacter; + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs new file mode 100644 index 00000000000..9dffddd207d --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextTrailingWordEllipsis.cs @@ -0,0 +1,37 @@ +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// a collapsing properties to collapse whole line toward the end + /// at word granularity and with ellipsis being the collapsing symbol + /// + public class TextTrailingWordEllipsis : TextCollapsingProperties + { + private static readonly ReadOnlySlice s_ellipsis = new ReadOnlySlice(new[] { '\u2026' }); + + /// + /// Construct a text trailing word ellipsis collapsing properties + /// + /// width in which collapsing is constrained to + /// text run properties of ellispis symbol + public TextTrailingWordEllipsis( + double width, + TextRunProperties textRunProperties + ) + { + Width = width; + Symbol = new TextCharacters(s_ellipsis, textRunProperties); + } + + + /// + public sealed override double Width { get; } + + /// + public sealed override TextRun Symbol { get; } + + /// + public sealed override TextCollapsingStyle Style { get; } = TextCollapsingStyle.TrailingWord; + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs index 94171b73244..2f46fdd9d05 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs @@ -1,4 +1,4 @@ -using Avalonia.Utility; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode { @@ -112,7 +112,7 @@ public static Codepoint ReadAt(ReadOnlySlice text, int index, out int coun { count = 1; - if (index > text.End) + if (index > text.Length) { return ReplacementCodepoint; } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs index 2ff4952cab1..9e1f748ebb5 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs @@ -1,4 +1,4 @@ -using Avalonia.Utility; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Grapheme.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Grapheme.cs index a6791b4a534..f268340eb98 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Grapheme.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Grapheme.cs @@ -1,4 +1,4 @@ -using Avalonia.Utility; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeEnumerator.cs index fd7831dfe60..1e4ac8fe0fd 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeEnumerator.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/GraphemeEnumerator.cs @@ -4,7 +4,7 @@ // Licensed to The Avalonia Project under MIT License, courtesy of The .NET Foundation. using System.Runtime.InteropServices; -using Avalonia.Utility; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs index 25a32bb1a39..76bb9ac44f4 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/LineBreakEnumerator.cs @@ -16,7 +16,7 @@ // Ported from: https://github.com/foliojs/linebreak // Copied from: https://github.com/toptensoftware/RichTextKit -using Avalonia.Utility; +using Avalonia.Utilities; namespace Avalonia.Media.TextFormatting.Unicode { @@ -109,7 +109,6 @@ public bool MoveNext() { case PairBreakType.DI: // Direct break shouldBreak = true; - _lastPos = _pos; break; case PairBreakType.IN: // possible indirect break diff --git a/src/Avalonia.Visuals/Media/TextWrapping.cs b/src/Avalonia.Visuals/Media/TextWrapping.cs index 56df3670bda..b7915e5612d 100644 --- a/src/Avalonia.Visuals/Media/TextWrapping.cs +++ b/src/Avalonia.Visuals/Media/TextWrapping.cs @@ -13,6 +13,13 @@ public enum TextWrapping /// /// Text can wrap. /// - Wrap + Wrap, + + /// + /// Line-breaking occurs if the line overflows the available block width. + /// However, a line may overflow the block width if the line breaking algorithm + /// cannot determine a break opportunity, as in the case of a very long word. + /// + WrapWithOverflow } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index 7618598a3fb..677e9308049 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -16,11 +16,11 @@ public class Typeface : IEquatable /// Initializes a new instance of the class. /// /// The font family. - /// The font weight. /// The font style. + /// The font weight. public Typeface([NotNull]FontFamily fontFamily, - FontWeight weight = FontWeight.Normal, - FontStyle style = FontStyle.Normal) + FontStyle style = FontStyle.Normal, + FontWeight weight = FontWeight.Normal) { if (weight <= 0) { @@ -39,9 +39,9 @@ public Typeface([NotNull]FontFamily fontFamily, /// The font style. /// The font weight. public Typeface(string fontFamilyName, - FontWeight weight = FontWeight.Normal, - FontStyle style = FontStyle.Normal) - : this(new FontFamily(fontFamilyName), weight, style) + FontStyle style = FontStyle.Normal, + FontWeight weight = FontWeight.Normal) + : this(new FontFamily(fontFamilyName), style, weight) { } diff --git a/src/Avalonia.Visuals/Platform/IDrawingContextWithAcrylicLikeSupport.cs b/src/Avalonia.Visuals/Platform/IDrawingContextWithAcrylicLikeSupport.cs new file mode 100644 index 00000000000..a3cf0298dd4 --- /dev/null +++ b/src/Avalonia.Visuals/Platform/IDrawingContextWithAcrylicLikeSupport.cs @@ -0,0 +1,9 @@ +using Avalonia.Media; + +namespace Avalonia.Platform +{ + public interface IDrawingContextWithAcrylicLikeSupport + { + void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect); + } +} diff --git a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs index 3d0bea8c802..59b08aae0a2 100644 --- a/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs +++ b/src/Avalonia.Visuals/Platform/IFontManagerImpl.cs @@ -22,15 +22,16 @@ public interface IFontManagerImpl /// Tries to match a specified character to a typeface that supports specified font properties. /// /// The codepoint to match against. - /// The font weight. /// The font style. + /// The font weight. /// The font family. This is optional and used for fallback lookup. /// The culture. /// The matching font key. /// /// True, if the could match the character to specified parameters, False otherwise. /// - bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + bool TryMatchCharacter(int codepoint, FontStyle fontStyle, + FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey); /// diff --git a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs index 4d770a6c6e6..d915da26034 100644 --- a/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs +++ b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs @@ -1,6 +1,6 @@ -using Avalonia.Media; -using Avalonia.Media.TextFormatting; -using Avalonia.Utility; +using System.Globalization; +using Avalonia.Media; +using Avalonia.Utilities; namespace Avalonia.Platform { @@ -13,8 +13,10 @@ public interface ITextShaperImpl /// Shapes the specified region within the text and returns a resulting glyph run. /// /// The text. - /// The text format. + /// The typeface. + /// The font rendering em size. + /// The culture. /// A shaped glyph run. - GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat); + GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture); } } diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs index dfb21a02891..4a364998fdb 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/DeferredDrawingContextImpl.cs @@ -11,7 +11,7 @@ namespace Avalonia.Rendering.SceneGraph /// /// A drawing context which builds a scene graph. /// - internal class DeferredDrawingContextImpl : IDrawingContextImpl + internal class DeferredDrawingContextImpl : IDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport { private readonly ISceneBuilder _sceneBuilder; private VisualNode _node; @@ -164,6 +164,21 @@ public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, } } + /// + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + var next = NextDrawAs(); + + if (next == null || !next.Item.Equals(Transform, material, rect)) + { + Add(new ExperimentalAcrylicNode(Transform, material, rect)); + } + else + { + ++_drawOperationindex; + } + } + public void Custom(ICustomDrawOperation custom) { var next = NextDrawAs(); diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs new file mode 100644 index 00000000000..336d11e3fd6 --- /dev/null +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/ExperimentalAcrylicNode.cs @@ -0,0 +1,94 @@ +using System; +using Avalonia.Media; +using Avalonia.Media.Immutable; +using Avalonia.Platform; + +namespace Avalonia.Rendering.SceneGraph +{ + /// + /// A node in the scene graph which represents a rectangle draw. + /// + internal class ExperimentalAcrylicNode : DrawOperation + { + /// + /// Initializes a new instance of the class. + /// + /// The transform. + /// The rectangle to draw. + /// The box shadow parameters + /// Child scenes for drawing visual brushes. + public ExperimentalAcrylicNode( + Matrix transform, + IExperimentalAcrylicMaterial material, + RoundedRect rect) + : base(rect.Rect, transform) + { + Transform = transform; + Material = material?.ToImmutable(); + Rect = rect; + } + + /// + /// Gets the transform with which the node will be drawn. + /// + public Matrix Transform { get; } + + public IExperimentalAcrylicMaterial Material { get; } + + /// + /// Gets the rectangle to draw. + /// + public RoundedRect Rect { get; } + + /// + /// Determines if this draw operation equals another. + /// + /// The transform of the other draw operation. + /// The fill of the other draw operation. + /// The rectangle of the other draw operation. + /// True if the draw operations are the same, otherwise false. + /// + /// The properties of the other draw operation are passed in as arguments to prevent + /// allocation of a not-yet-constructed draw operation object. + /// + public bool Equals(Matrix transform, IExperimentalAcrylicMaterial material, RoundedRect rect) + { + return transform == Transform && + Equals(material, Material) && + rect.Equals(Rect); + } + + /// + public override void Render(IDrawingContextImpl context) + { + context.Transform = Transform; + + if(context is IDrawingContextWithAcrylicLikeSupport idc) + { + idc.DrawRectangle(Material, Rect); + } + else + { + context.DrawRectangle(new ImmutableSolidColorBrush(Material.FallbackColor), null, Rect); + } + } + + /// + public override bool HitTest(Point p) + { + // TODO: This doesn't respect CornerRadius yet. + if (Transform.HasInverse) + { + p *= Transform.Invert(); + + if (Material != null) + { + var rect = Rect.Rect; + return rect.Contains(p); + } + } + + return false; + } + } +} diff --git a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs index 5059a6d0428..ec1a7753b1d 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -19,7 +19,7 @@ internal class RectangleNode : BrushDrawOperation /// The fill brush. /// The stroke pen. /// The rectangle to draw. - /// The box shadow parameters + /// The box shadow parameters /// Child scenes for drawing visual brushes. public RectangleNode( Matrix transform, diff --git a/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs b/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs similarity index 98% rename from src/Avalonia.Visuals/Utility/ReadOnlySlice.cs rename to src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs index ff2b3b93633..5feaa88e26c 100644 --- a/src/Avalonia.Visuals/Utility/ReadOnlySlice.cs +++ b/src/Avalonia.Visuals/Utilities/ReadOnlySlice.cs @@ -2,9 +2,8 @@ using System.Collections; using System.Collections.Generic; using System.Diagnostics; -using Avalonia.Utilities; -namespace Avalonia.Utility +namespace Avalonia.Utilities { /// /// ReadOnlySlice enables the ability to work with a sequence within a region of memory and retains the position in within that region. @@ -47,7 +46,7 @@ public ReadOnlySlice(ReadOnlyMemory buffer, int start, int length) public int Length { get; } /// - /// Gets a value that indicates whether this instance of is Empty. + /// Gets a value that indicates whether this instance of is Empty. /// public bool IsEmpty => Length == 0; diff --git a/src/Avalonia.Visuals/Utilities/ValueSpan.cs b/src/Avalonia.Visuals/Utilities/ValueSpan.cs new file mode 100644 index 00000000000..7a10d865ef4 --- /dev/null +++ b/src/Avalonia.Visuals/Utilities/ValueSpan.cs @@ -0,0 +1,30 @@ +namespace Avalonia.Utilities +{ + /// + /// Pairing of value and positions sharing that value. + /// + public readonly struct ValueSpan + { + public ValueSpan(int start, int length, T value) + { + Start = start; + Length = length; + Value = value; + } + + /// + /// Get's the start of the span. + /// + public int Start { get; } + + /// + /// Get's the length of the span. + /// + public int Length { get; } + + /// + /// Get's the value of the span. + /// + public T Value { get; } + } +} diff --git a/src/Avalonia.X11/X11NativeControlHost.cs b/src/Avalonia.X11/X11NativeControlHost.cs index 23fb27f72b1..6c4eb81c84e 100644 --- a/src/Avalonia.X11/X11NativeControlHost.cs +++ b/src/Avalonia.X11/X11NativeControlHost.cs @@ -167,7 +167,7 @@ public void HideWithSize(Size size) XUnmapWindow(_display, _holder.Handle); } - size *= _attachedTo.Window.Scaling; + size *= _attachedTo.Window.RenderScaling; XResizeWindow(_display, _child.Handle, Math.Max(1, (int)size.Width), Math.Max(1, (int)size.Height)); } @@ -179,7 +179,7 @@ public void ShowInBounds(Rect bounds) CheckDisposed(); if (_attachedTo == null) throw new InvalidOperationException("The control isn't currently attached to a toplevel"); - bounds *= _attachedTo.Window.Scaling; + bounds *= _attachedTo.Window.RenderScaling; var pixelRect = new PixelRect((int)bounds.X, (int)bounds.Y, Math.Max(1, (int)bounds.Width), Math.Max(1, (int)bounds.Height)); diff --git a/src/Avalonia.X11/X11Screens.cs b/src/Avalonia.X11/X11Screens.cs index e247a4241a0..af6d2674b64 100644 --- a/src/Avalonia.X11/X11Screens.cs +++ b/src/Avalonia.X11/X11Screens.cs @@ -48,9 +48,13 @@ static unsafe X11Screen[] UpdateWorkArea(X11Info info, X11Screen[] screens) var pwa = (IntPtr*)prop; var wa = new PixelRect(pwa[0].ToInt32(), pwa[1].ToInt32(), pwa[2].ToInt32(), pwa[3].ToInt32()); - - foreach (var s in screens) + + foreach (var s in screens) + { s.WorkingArea = s.Bounds.Intersect(wa); + if (s.WorkingArea.Width <= 0 || s.WorkingArea.Height <= 0) + s.WorkingArea = s.Bounds; + } XFree(prop); return screens; @@ -134,8 +138,14 @@ public FallbackScreensImpl(X11Info info, X11ScreensUserSettings settings) settings.GlobalScaleFactor) }); } - - Screens = new[] {new X11Screen(new PixelRect(0, 0, 1920, 1280), true, "Default", null, settings.GlobalScaleFactor)}; + else + { + Screens = new[] + { + new X11Screen(new PixelRect(0, 0, 1920, 1280), true, "Default", null, + settings.GlobalScaleFactor) + }; + } } public X11Screen[] Screens { get; } diff --git a/src/Avalonia.X11/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 499fe5a67ae..c24abcd2302 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -163,7 +163,7 @@ public X11Window(AvaloniaX11Platform platform, IWindowImpl popupParent) var surfaces = new List { new X11FramebufferSurface(_x11.DeferredDisplay, _renderHandle, - depth, () => Scaling) + depth, () => RenderScaling) }; if (egl != null) @@ -217,7 +217,7 @@ public PixelSize Size } } - public double Scaling => _window.Scaling; + public double Scaling => _window.RenderScaling; } void UpdateMotifHints() @@ -284,9 +284,9 @@ void UpdateSizeHints(PixelSize? preResize) XSetWMNormalHints(_x11.Display, _handle, ref hints); } - public Size ClientSize => new Size(_realSize.Width / Scaling, _realSize.Height / Scaling); + public Size ClientSize => new Size(_realSize.Width / RenderScaling, _realSize.Height / RenderScaling); - public double Scaling + public double RenderScaling { get { @@ -296,6 +296,8 @@ public double Scaling } private set => _scaling = value; } + + public double DesktopScaling => RenderScaling; public IEnumerable Surfaces { get; } public Action Input { get; set; } @@ -312,7 +314,15 @@ public Action TransparencyLevelChanged { get => _transparencyHelper.TransparencyLevelChanged; set => _transparencyHelper.TransparencyLevelChanged = value; - } + } + + public Action ExtendClientAreaToDecorationsChanged { get; set; } + + public Thickness ExtendedMargins { get; } = new Thickness(); + + public Thickness OffScreenMargin { get; } = new Thickness(); + + public bool IsClientAreaExtendedToDecorations { get; } public Action Closed { get; set; } public Action PositionChanged { get; set; } @@ -530,14 +540,14 @@ private bool UpdateScaling(bool skipResize = false) { var monitor = _platform.X11Screens.Screens.OrderBy(x => x.PixelDensity) .FirstOrDefault(m => m.Bounds.Contains(Position)); - newScaling = monitor?.PixelDensity ?? Scaling; + newScaling = monitor?.PixelDensity ?? RenderScaling; } - if (Scaling != newScaling) + if (RenderScaling != newScaling) { var oldScaledSize = ClientSize; - Scaling = newScaling; - ScalingChanged?.Invoke(Scaling); + RenderScaling = newScaling; + ScalingChanged?.Invoke(RenderScaling); SetMinMaxSize(_scaledMinMaxSize.minSize, _scaledMinMaxSize.maxSize); if(!skipResize) Resize(oldScaledSize, true); @@ -699,9 +709,9 @@ public void ScheduleXI2Input(RawInputEventArgs args) private void ScheduleInput(RawInputEventArgs args) { if (args is RawPointerEventArgs mouse) - mouse.Position = mouse.Position / Scaling; + mouse.Position = mouse.Position / RenderScaling; if (args is RawDragEvent drag) - drag.Location = drag.Location / Scaling; + drag.Location = drag.Location / RenderScaling; _lastEvent = new InputEventContainer() {Event = args}; _inputQueue.Enqueue(_lastEvent); @@ -752,11 +762,7 @@ public void SetInputRoot(IInputRoot inputRoot) public void Dispose() { - if (_handle != IntPtr.Zero) - { - XDestroyWindow(_x11.Display, _handle); - Cleanup(); - } + Cleanup(); } void Cleanup() @@ -779,8 +785,7 @@ void Cleanup() } if (_useRenderWindow && _renderHandle != IntPtr.Zero) - { - XDestroyWindow(_x11.Display, _renderHandle); + { _renderHandle = IntPtr.Zero; } } @@ -813,11 +818,11 @@ public void Show() public void Hide() => XUnmapWindow(_x11.Display, _handle); - public Point PointToClient(PixelPoint point) => new Point((point.X - Position.X) / Scaling, (point.Y - Position.Y) / Scaling); + public Point PointToClient(PixelPoint point) => new Point((point.X - Position.X) / RenderScaling, (point.Y - Position.Y) / RenderScaling); public PixelPoint PointToScreen(Point point) => new PixelPoint( - (int)(point.X * Scaling + Position.X), - (int)(point.Y * Scaling + Position.Y)); + (int)(point.X * RenderScaling + Position.X), + (int)(point.Y * RenderScaling + Position.Y)); public void SetSystemDecorations(SystemDecorations enabled) { @@ -837,7 +842,7 @@ private void MoveResize(PixelPoint position, Size size, double scaling) Resize(size, true); } - PixelSize ToPixelSize(Size size) => new PixelSize((int)(size.Width * Scaling), (int)(size.Height * Scaling)); + PixelSize ToPixelSize(Size size) => new PixelSize((int)(size.Width * RenderScaling), (int)(size.Height * RenderScaling)); void Resize(Size clientSize, bool force) { @@ -1017,13 +1022,13 @@ public void SetMinMaxSize(Size minSize, Size maxSize) { _scaledMinMaxSize = (minSize, maxSize); var min = new PixelSize( - (int)(minSize.Width < 1 ? 1 : minSize.Width * Scaling), - (int)(minSize.Height < 1 ? 1 : minSize.Height * Scaling)); + (int)(minSize.Width < 1 ? 1 : minSize.Width * RenderScaling), + (int)(minSize.Height < 1 ? 1 : minSize.Height * RenderScaling)); const int maxDim = MaxWindowDimension; var max = new PixelSize( - (int)(maxSize.Width > maxDim ? maxDim : Math.Max(min.Width, maxSize.Width * Scaling)), - (int)(maxSize.Height > maxDim ? maxDim : Math.Max(min.Height, maxSize.Height * Scaling))); + (int)(maxSize.Width > maxDim ? maxDim : Math.Max(min.Width, maxSize.Width * RenderScaling)), + (int)(maxSize.Height > maxDim ? maxDim : Math.Max(min.Height, maxSize.Height * RenderScaling))); _minMaxSize = (min, max); UpdateSizeHints(null); @@ -1039,6 +1044,18 @@ public void SetEnabled(bool enable) _disabled = !enable; } + public void SetExtendClientAreaToDecorationsHint(bool extendIntoClientAreaHint) + { + } + + public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) + { + } + + public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) + { + } + public Action GotInputWhenDisabled { get; set; } public void SetIcon(IWindowIconImpl icon) @@ -1107,5 +1124,9 @@ public void SetWindowManagerAddShadowHint(bool enabled) } public WindowTransparencyLevel TransparencyLevel => _transparencyHelper.CurrentLevel; + + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0.8); + + public bool NeedsManagedDecorations => false; } } diff --git a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs index 21533443633..0a101eec7a9 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -65,7 +65,7 @@ public void SetCursor(IPlatformHandle cursor) public IMouseDevice MouseDevice => new MouseDevice(); public IPopupImpl CreatePopup() => null; - public double Scaling => _outputBackend.Scaling; + public double RenderScaling => _outputBackend.Scaling; public IEnumerable Surfaces { get; } public Action Input { get; set; } public Action Paint { get; set; } @@ -77,10 +77,12 @@ public void SetCursor(IPlatformHandle cursor) public Action Closed { get; set; } public Action LostFocus { get; set; } - public Size ScaledSize => _outputBackend.PixelSize.ToSize(Scaling); + public Size ScaledSize => _outputBackend.PixelSize.ToSize(RenderScaling); public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { } public WindowTransparencyLevel TransparencyLevel { get; private set; } + + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); } } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj b/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj new file mode 100644 index 00000000000..768545eb7e7 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/Avalonia.Markup.Xaml.Loader.csproj @@ -0,0 +1,14 @@ + + + + netstandard2.0 + true + + + + + + + + + diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs new file mode 100644 index 00000000000..4569970d010 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaRuntimeXamlLoader.cs @@ -0,0 +1,42 @@ +using System; +using System.IO; +using System.Reflection; +using System.Text; +using Avalonia.Markup.Xaml.XamlIl; +// ReSharper disable CheckNamespace + +namespace Avalonia.Markup.Xaml +{ + public static class AvaloniaRuntimeXamlLoader + { + /// + /// Loads XAML from a string. + /// + /// The string containing the XAML. + /// Default assembly for clr-namespace: + /// + /// The optional instance into which the XAML should be loaded. + /// + /// The loaded object. + public static object Load(string xaml, Assembly localAssembly = null, object rootInstance = null, Uri uri = null, bool designMode = false) + { + Contract.Requires(xaml != null); + + using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(xaml))) + { + return Load(stream, localAssembly, rootInstance, uri, designMode); + } + } + + public static object Load(Stream stream, Assembly localAssembly, object rootInstance = null, Uri uri = null, + bool designMode = false) + => AvaloniaXamlIlRuntimeCompiler.Load(stream, localAssembly, rootInstance, uri, designMode); + + public static object Parse(string xaml, Assembly localAssembly = null) + => Load(xaml, localAssembly); + + public static T Parse(string xaml, Assembly localAssembly = null) + => (T)Parse(xaml, localAssembly); + + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs similarity index 85% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs index 5a5da518d0f..d9b4cd70daa 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/AvaloniaXamlIlRuntimeCompiler.cs @@ -10,12 +10,15 @@ using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions; using Avalonia.Markup.Xaml.XamlIl.Runtime; using Avalonia.Platform; -using XamlIl.Transform; -using XamlIl.TypeSystem; +using XamlX.Transform; +using XamlX.TypeSystem; +using XamlX.IL; +using XamlX.Emit; #if RUNTIME_XAML_CECIL using TypeAttributes = Mono.Cecil.TypeAttributes; using Mono.Cecil; -using XamlIl.Ast; +using XamlX.Ast; +using XamlX.IL.Cecil; #endif namespace Avalonia.Markup.Xaml.XamlIl { @@ -24,9 +27,10 @@ static class AvaloniaXamlIlRuntimeCompiler #if !RUNTIME_XAML_CECIL private static SreTypeSystem _sreTypeSystem; private static ModuleBuilder _sreBuilder; - private static IXamlIlType _sreContextType; - private static XamlIlLanguageTypeMappings _sreMappings; - private static XamlIlXmlnsMappings _sreXmlns; + private static IXamlType _sreContextType; + private static XamlLanguageTypeMappings _sreMappings; + private static XamlLanguageEmitMappings _sreEmitMappings; + private static XamlXmlnsMappings _sreXmlns; private static AssemblyBuilder _sreAsm; private static bool _sreCanSave; @@ -82,13 +86,14 @@ static void InitializeSre() } if (_sreMappings == null) - _sreMappings = AvaloniaXamlIlLanguage.Configure(_sreTypeSystem); + (_sreMappings, _sreEmitMappings) = AvaloniaXamlIlLanguage.Configure(_sreTypeSystem); if (_sreXmlns == null) - _sreXmlns = XamlIlXmlnsMappings.Resolve(_sreTypeSystem, _sreMappings); + _sreXmlns = XamlXmlnsMappings.Resolve(_sreTypeSystem, _sreMappings); if (_sreContextType == null) - _sreContextType = XamlIlContextDefinition.GenerateContextClass( + _sreContextType = XamlILContextDefinition.GenerateContextClass( _sreTypeSystem.CreateTypeBuilder( - _sreBuilder.DefineType("XamlIlContext")), _sreTypeSystem, _sreMappings); + _sreBuilder.DefineType("XamlIlContext")), _sreTypeSystem, _sreMappings, + _sreEmitMappings); } @@ -114,13 +119,19 @@ static object LoadSreCore(string xaml, Assembly localAssembly, object rootInstan InitializeSre(); var asm = localAssembly == null ? null : _sreTypeSystem.GetAssembly(localAssembly); + var tb = _sreBuilder.DefineType("Builder_" + Guid.NewGuid().ToString("N") + "_" + uri); + var clrPropertyBuilder = tb.DefineNestedType("ClrProperties_" + Guid.NewGuid().ToString("N")); + var indexerClosureType = _sreBuilder.DefineType("IndexerClosure_" + Guid.NewGuid().ToString("N")); - var compiler = new AvaloniaXamlIlCompiler(new XamlIlTransformerConfiguration(_sreTypeSystem, asm, - _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter), + var compiler = new AvaloniaXamlIlCompiler(new AvaloniaXamlIlCompilerConfiguration(_sreTypeSystem, asm, + _sreMappings, _sreXmlns, AvaloniaXamlIlLanguage.CustomValueConverter, + new XamlIlClrPropertyInfoEmitter(_sreTypeSystem.CreateTypeBuilder(clrPropertyBuilder)), + new XamlIlPropertyInfoAccessorFactoryEmitter(_sreTypeSystem.CreateTypeBuilder(indexerClosureType))), + _sreEmitMappings, _sreContextType) { EnableIlVerification = true }; - var tb = _sreBuilder.DefineType("Builder_" + Guid.NewGuid().ToString("N") + "_" + uri); + - IXamlIlType overrideType = null; + IXamlType overrideType = null; if (rootInstance != null) { overrideType = _sreTypeSystem.GetType(rootInstance.GetType()); @@ -129,6 +140,7 @@ static object LoadSreCore(string xaml, Assembly localAssembly, object rootInstan compiler.IsDesignMode = isDesignMode; compiler.ParseAndCompile(xaml, uri?.ToString(), null, _sreTypeSystem.CreateTypeBuilder(tb), overrideType); var created = tb.CreateTypeInfo(); + clrPropertyBuilder.CreateTypeInfo(); return LoadOrPopulate(created, rootInstance); } @@ -203,6 +215,7 @@ public static object Load(Stream stream, Assembly localAssembly, object rootInst private static string _cecilEmitDir; private static CecilTypeSystem _cecilTypeSystem; private static XamlIlLanguageTypeMappings _cecilMappings; + private static XamlLanguageEmitMappings _cecilEmitMappings; private static XamlIlXmlnsMappings _cecilXmlns; private static bool _cecilInitialized; @@ -215,7 +228,7 @@ static void InitializeCecil() Directory.CreateDirectory(_cecilEmitDir); var refs = new[] {path}.Concat(File.ReadAllLines(path + ".refs")); _cecilTypeSystem = new CecilTypeSystem(refs); - _cecilMappings = AvaloniaXamlIlLanguage.Configure(_cecilTypeSystem); + (_cecilMappings, _cecilEmitMappings) = AvaloniaXamlIlLanguage.Configure(_cecilTypeSystem); _cecilXmlns = XamlIlXmlnsMappings.Resolve(_cecilTypeSystem, _cecilMappings); _cecilInitialized = true; } @@ -226,7 +239,7 @@ static object LoadCecil(string xaml, Assembly localAssembly, object rootInstance if (uri == null) throw new InvalidOperationException("Please, go away"); InitializeCecil(); - IXamlIlType overrideType = null; + IXamlType overrideType = null; if (rootInstance != null) { overrideType = _cecilTypeSystem.GetType(rootInstance.GetType().FullName); @@ -261,6 +274,7 @@ static object LoadCecil(string xaml, Assembly localAssembly, object rootInstance localAssembly == null ? null : _cecilTypeSystem.FindAssembly(localAssembly.GetName().Name), _cecilMappings, XamlIlXmlnsMappings.Resolve(_cecilTypeSystem, _cecilMappings), AvaloniaXamlIlLanguage.CustomValueConverter), + _cecilEmitMappings, _cecilTypeSystem.CreateTypeBuilder(contextDef)); compiler.ParseAndCompile(xaml, uri.ToString(), tb, overrideType); var asmPath = Path.Combine(_cecilEmitDir, safeUri + ".dll"); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs new file mode 100644 index 00000000000..abff763bb14 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -0,0 +1,163 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Parsers; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions +{ + class AvaloniaXamlIlCompiler : XamlILCompiler + { + private readonly TransformerConfiguration _configuration; + private readonly IXamlType _contextType; + private readonly AvaloniaXamlIlDesignPropertiesTransformer _designTransformer; + private readonly AvaloniaBindingExtensionTransformer _bindingTransformer; + + private AvaloniaXamlIlCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings emitMappings) + : base(configuration, emitMappings, true) + { + _configuration = configuration; + + void InsertAfter(params IXamlAstTransformer[] t) + => Transformers.InsertRange(Transformers.FindIndex(x => x is T) + 1, t); + + void InsertBefore(params IXamlAstTransformer[] t) + => Transformers.InsertRange(Transformers.FindIndex(x => x is T), t); + + + // Before everything else + + Transformers.Insert(0, new XNameTransformer()); + Transformers.Insert(1, new IgnoredDirectivesTransformer()); + Transformers.Insert(2, _designTransformer = new AvaloniaXamlIlDesignPropertiesTransformer()); + Transformers.Insert(3, _bindingTransformer = new AvaloniaBindingExtensionTransformer()); + + + // Targeted + InsertBefore( + new AvaloniaXamlIlTransformInstanceAttachedProperties(), + new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers()); + InsertAfter( + new AvaloniaXamlIlAvaloniaPropertyResolver()); + + InsertBefore( + new AvaloniaXamlIlBindingPathParser(), + new AvaloniaXamlIlSelectorTransformer(), + new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), + new AvaloniaXamlIlPropertyPathTransformer(), + new AvaloniaXamlIlSetterTransformer(), + new AvaloniaXamlIlConstructorServiceProviderTransformer(), + new AvaloniaXamlIlTransitionsTypeMetadataTransformer(), + new AvaloniaXamlIlResolveByNameMarkupExtensionReplacer() + ); + + // After everything else + InsertBefore( + new AddNameScopeRegistration(), + new AvaloniaXamlIlDataContextTypeTransformer(), + new AvaloniaXamlIlBindingPathTransformer(), + new AvaloniaXamlIlCompiledBindingsMetadataRemover() + ); + + Transformers.Add(new AvaloniaXamlIlMetadataRemover()); + Transformers.Add(new AvaloniaXamlIlRootObjectScope()); + + Emitters.Add(new AvaloniaNameScopeRegistrationXamlIlNodeEmitter()); + Emitters.Add(new AvaloniaXamlIlRootObjectScope.Emitter()); + } + public AvaloniaXamlIlCompiler(TransformerConfiguration configuration, + XamlLanguageEmitMappings emitMappings, + IXamlTypeBuilder contextTypeBuilder) + : this(configuration, emitMappings) + { + _contextType = CreateContextType(contextTypeBuilder); + } + + + public AvaloniaXamlIlCompiler(TransformerConfiguration configuration, + XamlLanguageEmitMappings emitMappings, + IXamlType contextType) : this(configuration, emitMappings) + { + _contextType = contextType; + } + + public const string PopulateName = "__AvaloniaXamlIlPopulate"; + public const string BuildName = "__AvaloniaXamlIlBuild"; + + public bool IsDesignMode + { + get => _designTransformer.IsDesignMode; + set => _designTransformer.IsDesignMode = value; + } + + public bool DefaultCompileBindings + { + get => _bindingTransformer.CompileBindingsByDefault; + set => _bindingTransformer.CompileBindingsByDefault = value; + } + + public void ParseAndCompile(string xaml, string baseUri, IFileSource fileSource, IXamlTypeBuilder tb, IXamlType overrideRootType) + { + var parsed = XDocumentXamlParser.Parse(xaml, new Dictionary + { + {XamlNamespaces.Blend2008, XamlNamespaces.Blend2008} + }); + + var rootObject = (XamlAstObjectNode)parsed.Root; + + var classDirective = rootObject.Children + .OfType().FirstOrDefault(x => + x.Namespace == XamlNamespaces.Xaml2006 + && x.Name == "Class"); + + var rootType = + classDirective != null ? + new XamlAstClrTypeReference(classDirective, + _configuration.TypeSystem.GetType(((XamlAstTextNode)classDirective.Values[0]).Text), + false) : + TypeReferenceResolver.ResolveType(CreateTransformationContext(parsed, true), + (XamlAstXmlTypeReference)rootObject.Type, true); + + + if (overrideRootType != null) + { + if (!rootType.Type.IsAssignableFrom(overrideRootType)) + throw new XamlX.XamlLoadException( + $"Unable to substitute {rootType.Type.GetFqn()} with {overrideRootType.GetFqn()}", rootObject); + rootType = new XamlAstClrTypeReference(rootObject, overrideRootType, false); + } + + OverrideRootType(parsed, rootType); + + Transform(parsed); + Compile(parsed, tb, _contextType, PopulateName, BuildName, "__AvaloniaXamlIlNsInfo", baseUri, fileSource); + + } + + public void OverrideRootType(XamlDocument doc, IXamlAstTypeReference newType) + { + var root = (XamlAstObjectNode)doc.Root; + var oldType = root.Type; + if (oldType.Equals(newType)) + return; + + root.Type = newType; + foreach (var child in root.Children.OfType()) + { + if (child.Property is XamlAstNamePropertyReference prop) + { + if (prop.DeclaringType.Equals(oldType)) + prop.DeclaringType = newType; + if (prop.TargetType.Equals(oldType)) + prop.TargetType = newType; + } + } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs new file mode 100644 index 00000000000..0c0dcb16346 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs @@ -0,0 +1,26 @@ +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions +{ + class AvaloniaXamlIlCompilerConfiguration : TransformerConfiguration + { + public XamlIlClrPropertyInfoEmitter ClrPropertyEmitter { get; } + public XamlIlPropertyInfoAccessorFactoryEmitter AccessorFactoryEmitter { get; } + + public AvaloniaXamlIlCompilerConfiguration(IXamlTypeSystem typeSystem, + IXamlAssembly defaultAssembly, + XamlLanguageTypeMappings typeMappings, + XamlXmlnsMappings xmlnsMappings, + XamlValueConverter customValueConverter, + XamlIlClrPropertyInfoEmitter clrPropertyEmitter, + XamlIlPropertyInfoAccessorFactoryEmitter accessorFactoryEmitter) + : base(typeSystem, defaultAssembly, typeMappings, xmlnsMappings, customValueConverter) + { + ClrPropertyEmitter = clrPropertyEmitter; + AccessorFactoryEmitter = accessorFactoryEmitter; + AddExtra(ClrPropertyEmitter); + AddExtra(AccessorFactoryEmitter); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs similarity index 71% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs index a1fe6976b72..99ec3744bf3 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -2,10 +2,12 @@ using System.Globalization; using System.Linq; using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; -using XamlIl; -using XamlIl.Ast; -using XamlIl.Transform; -using XamlIl.TypeSystem; +using XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { @@ -17,12 +19,12 @@ ONLY use types from netstandard and XamlIl. NO dependencies on Avalonia are allo class AvaloniaXamlIlLanguage { - public static XamlIlLanguageTypeMappings Configure(IXamlIlTypeSystem typeSystem) + public static (XamlLanguageTypeMappings language, XamlLanguageEmitMappings emit) Configure(IXamlTypeSystem typeSystem) { var runtimeHelpers = typeSystem.GetType("Avalonia.Markup.Xaml.XamlIl.Runtime.XamlIlRuntimeHelpers"); var assignBindingAttribute = typeSystem.GetType("Avalonia.Data.AssignBindingAttribute"); var bindingType = typeSystem.GetType("Avalonia.Data.IBinding"); - var rv = new XamlIlLanguageTypeMappings(typeSystem) + var rv = new XamlLanguageTypeMappings(typeSystem) { SupportInitialize = typeSystem.GetType("System.ComponentModel.ISupportInitialize"), XmlnsAttributes = @@ -51,18 +53,22 @@ public static XamlIlLanguageTypeMappings Configure(IXamlIlTypeSystem typeSystem) }, InnerServiceProviderFactoryMethod = runtimeHelpers.FindMethod(m => m.Name == "CreateInnerServiceProviderV1"), - ProvideValueTargetPropertyEmitter = XamlIlAvaloniaPropertyHelper.Emit, }; rv.CustomAttributeResolver = new AttributeResolver(typeSystem, rv); - rv.ContextTypeBuilderCallback = (b, c) => EmitNameScopeField(rv, typeSystem, b, c); - return rv; + + var emit = new XamlLanguageEmitMappings + { + ProvideValueTargetPropertyEmitter = XamlIlAvaloniaPropertyHelper.EmitProvideValueTarget, + ContextTypeBuilderCallback = (b, c) => EmitNameScopeField(rv, typeSystem, b, c) + }; + return (rv, emit); } public const string ContextNameScopeFieldName = "AvaloniaNameScope"; - private static void EmitNameScopeField(XamlIlLanguageTypeMappings mappings, - IXamlIlTypeSystem typeSystem, - IXamlIlTypeBuilder typebuilder, IXamlIlEmitter constructor) + private static void EmitNameScopeField(XamlLanguageTypeMappings mappings, + IXamlTypeSystem typeSystem, + IXamlTypeBuilder typebuilder, IXamlILEmitter constructor) { var nameScopeType = typeSystem.FindType("Avalonia.Controls.INameScope"); @@ -78,23 +84,23 @@ private static void EmitNameScopeField(XamlIlLanguageTypeMappings mappings, } - class AttributeResolver : IXamlIlCustomAttributeResolver + class AttributeResolver : IXamlCustomAttributeResolver { - private readonly IXamlIlType _typeConverterAttribute; + private readonly IXamlType _typeConverterAttribute; - private readonly List> _converters = - new List>(); + private readonly List> _converters = + new List>(); - private readonly IXamlIlType _avaloniaList; - private readonly IXamlIlType _avaloniaListConverter; + private readonly IXamlType _avaloniaList; + private readonly IXamlType _avaloniaListConverter; - public AttributeResolver(IXamlIlTypeSystem typeSystem, XamlIlLanguageTypeMappings mappings) + public AttributeResolver(IXamlTypeSystem typeSystem, XamlLanguageTypeMappings mappings) { _typeConverterAttribute = mappings.TypeConverterAttributes.First(); - void AddType(IXamlIlType type, IXamlIlType conv) - => _converters.Add(new KeyValuePair(type, conv)); + void AddType(IXamlType type, IXamlType conv) + => _converters.Add(new KeyValuePair(type, conv)); void Add(string type, string conv) => AddType(typeSystem.GetType(type), typeSystem.GetType(conv)); @@ -113,7 +119,7 @@ void Add(string type, string conv) _avaloniaListConverter = typeSystem.GetType("Avalonia.Collections.AvaloniaListConverter`1"); } - IXamlIlType LookupConverter(IXamlIlType type) + IXamlType LookupConverter(IXamlType type) { foreach(var p in _converters) if (p.Key.Equals(type)) @@ -123,15 +129,15 @@ IXamlIlType LookupConverter(IXamlIlType type) return null; } - class ConstructedAttribute : IXamlIlCustomAttribute + class ConstructedAttribute : IXamlCustomAttribute { - public bool Equals(IXamlIlCustomAttribute other) => false; + public bool Equals(IXamlCustomAttribute other) => false; - public IXamlIlType Type { get; } + public IXamlType Type { get; } public List Parameters { get; } public Dictionary Properties { get; } - public ConstructedAttribute(IXamlIlType type, List parameters, Dictionary properties) + public ConstructedAttribute(IXamlType type, List parameters, Dictionary properties) { Type = type; Parameters = parameters ?? new List(); @@ -139,7 +145,7 @@ public ConstructedAttribute(IXamlIlType type, List parameters, Dictionar } } - public IXamlIlCustomAttribute GetCustomAttribute(IXamlIlType type, IXamlIlType attributeType) + public IXamlCustomAttribute GetCustomAttribute(IXamlType type, IXamlType attributeType) { if (attributeType.Equals(_typeConverterAttribute)) { @@ -151,25 +157,25 @@ public IXamlIlCustomAttribute GetCustomAttribute(IXamlIlType type, IXamlIlType a return null; } - public IXamlIlCustomAttribute GetCustomAttribute(IXamlIlProperty property, IXamlIlType attributeType) + public IXamlCustomAttribute GetCustomAttribute(IXamlProperty property, IXamlType attributeType) { return null; } } - public static bool CustomValueConverter(XamlIlAstTransformationContext context, - IXamlIlAstValueNode node, IXamlIlType type, out IXamlIlAstValueNode result) + public static bool CustomValueConverter(AstTransformationContext context, + IXamlAstValueNode node, IXamlType type, out IXamlAstValueNode result) { if (type.FullName == "System.TimeSpan" - && node is XamlIlAstTextNode tn + && node is XamlAstTextNode tn && !tn.Text.Contains(":")) { var seconds = double.Parse(tn.Text, CultureInfo.InvariantCulture); - result = new XamlIlStaticOrTargetedReturnMethodCallNode(tn, + result = new XamlStaticOrTargetedReturnMethodCallNode(tn, type.FindMethod("FromSeconds", type, false, context.Configuration.WellKnownTypes.Double), new[] { - new XamlIlConstantNode(tn, context.Configuration.WellKnownTypes.Double, seconds) + new XamlConstantNode(tn, context.Configuration.WellKnownTypes.Double, seconds) }); return true; } @@ -178,9 +184,9 @@ public static bool CustomValueConverter(XamlIlAstTransformationContext context, { var scope = context.ParentNodes().OfType().FirstOrDefault(); if (scope == null) - throw new XamlIlLoadException("Unable to find the parent scope for AvaloniaProperty lookup", node); - if (!(node is XamlIlAstTextNode text)) - throw new XamlIlLoadException("Property should be a text node", node); + throw new XamlX.XamlLoadException("Unable to find the parent scope for AvaloniaProperty lookup", node); + if (!(node is XamlAstTextNode text)) + throw new XamlX.XamlLoadException("Property should be a text node", node); result = XamlIlAvaloniaPropertyHelper.CreateNode(context, text.Text, scope.TargetType, text); return true; } diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AddNameScopeRegistration.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AddNameScopeRegistration.cs new file mode 100644 index 00000000000..cb36851bab8 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AddNameScopeRegistration.cs @@ -0,0 +1,120 @@ +using System; +using System.Linq; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; +using XamlX.Emit; +using XamlX.IL; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AddNameScopeRegistration : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is XamlPropertyAssignmentNode pa) + { + if (pa.Property.Name == "Name" + && pa.Property.DeclaringType.FullName == "Avalonia.StyledElement") + { + if (context.ParentNodes().FirstOrDefault() is XamlManipulationGroupNode mg + && mg.Children.OfType().Any()) + return node; + + IXamlAstValueNode value = null; + for (var c = 0; c < pa.Values.Count; c++) + if (pa.Values[c].Type.GetClrType().Equals(context.Configuration.WellKnownTypes.String)) + { + value = pa.Values[c]; + if (!(value is XamlAstTextNode)) + { + var local = new XamlAstCompilerLocalNode(value); + // Wrap original in local initialization + pa.Values[c] = new XamlAstLocalInitializationNodeEmitter(value, value, local); + // Use local + value = local; + } + + break; + } + + if (value != null) + { + var objectType = context.ParentNodes().OfType().FirstOrDefault()?.Type.GetClrType(); + return new XamlManipulationGroupNode(pa) + { + Children = + { + pa, + new AvaloniaNameScopeRegistrationXamlIlNode(value, objectType) + } + }; + } + } + else if (pa.Property.CustomAttributes.Select(attr => attr.Type).Intersect(context.Configuration.TypeMappings.DeferredContentPropertyAttributes).Any()) + { + pa.Values[pa.Values.Count - 1] = + new NestedScopeMetadataNode(pa.Values[pa.Values.Count - 1]); + } + } + + return node; + } + } + + class NestedScopeMetadataNode : XamlValueWithSideEffectNodeBase + { + public NestedScopeMetadataNode(IXamlAstValueNode value) : base(value, value) + { + } + } + + class AvaloniaNameScopeRegistrationXamlIlNode : XamlAstNode, IXamlAstManipulationNode + { + public IXamlAstValueNode Name { get; set; } + public IXamlType TargetType { get; } + + public AvaloniaNameScopeRegistrationXamlIlNode(IXamlAstValueNode name, IXamlType targetType) : base(name) + { + TargetType = targetType; + Name = name; + } + + public override void VisitChildren(IXamlAstVisitor visitor) + => Name = (IXamlAstValueNode)Name.Visit(visitor); + } + + class AvaloniaNameScopeRegistrationXamlIlNodeEmitter : IXamlAstLocalsNodeEmitter + { + public XamlILNodeEmitResult Emit(IXamlAstNode node, XamlEmitContextWithLocals context, IXamlILEmitter codeGen) + { + if (node is AvaloniaNameScopeRegistrationXamlIlNode registration) + { + + var scopeField = context.RuntimeContext.ContextType.Fields.First(f => + f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName); + + using (var targetLoc = context.GetLocalOfType(context.Configuration.WellKnownTypes.Object)) + { + + codeGen + // var target = {pop} + .Stloc(targetLoc.Local) + // _context.NameScope.Register(Name, target) + .Ldloc(context.ContextLocal) + .Ldfld(scopeField); + + context.Emit(registration.Name, codeGen, registration.Name.Type.GetClrType()); + + codeGen + .Ldloc(targetLoc.Local) + .EmitCall(context.GetAvaloniaTypes().INameScopeRegister, true); + } + + return XamlILNodeEmitResult.Void(1); + } + return default; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaBindingExtensionTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaBindingExtensionTransformer.cs new file mode 100644 index 00000000000..de2c0eab962 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaBindingExtensionTransformer.cs @@ -0,0 +1,75 @@ +using System.Linq; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; + + +using XamlParseException = XamlX.XamlParseException; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaBindingExtensionTransformer : IXamlAstTransformer + { + public bool CompileBindingsByDefault { get; set; } + + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (context.ParentNodes().FirstOrDefault() is AvaloniaXamlIlCompileBindingsNode) + { + return node; + } + + if (node is XamlAstObjectNode obj) + { + foreach (var item in obj.Children) + { + if (item is XamlAstXmlDirective directive) + { + if (directive.Namespace == XamlNamespaces.Xaml2006 + && directive.Name == "CompileBindings" + && directive.Values.Count == 1) + { + if (!(directive.Values[0] is XamlAstTextNode text + && bool.TryParse(text.Text, out var compileBindings))) + { + throw new XamlParseException("The value of x:CompileBindings must be a literal boolean value.", directive.Values[0]); + } + + obj.Children.Remove(directive); + + return new AvaloniaXamlIlCompileBindingsNode(obj, compileBindings); + } + } + } + } + + // Convert the tag to either a CompiledBinding or ReflectionBinding tag. + + if (node is XamlAstXmlTypeReference tref + && tref.Name == "Binding" + && tref.XmlNamespace == "https://github.com/avaloniaui") + { + tref.IsMarkupExtension = true; + + var compileBindings = context.ParentNodes() + .OfType() + .FirstOrDefault() + ?.CompileBindings ?? CompileBindingsByDefault; + + tref.Name = compileBindings ? "CompiledBinding" : "ReflectionBinding"; + } + return node; + } + } + + internal class AvaloniaXamlIlCompileBindingsNode : XamlValueWithSideEffectNodeBase + { + public AvaloniaXamlIlCompileBindingsNode(IXamlAstValueNode value, bool compileBindings) + : base(value, value) + { + CompileBindings = compileBindings; + } + + public bool CompileBindings { get; } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlAvaloniaPropertyResolver.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAvaloniaPropertyResolver.cs similarity index 64% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlAvaloniaPropertyResolver.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAvaloniaPropertyResolver.cs index 74f5c29f6a9..9219a39ddc4 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlAvaloniaPropertyResolver.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlAvaloniaPropertyResolver.cs @@ -1,14 +1,14 @@ using System.Linq; -using XamlIl.Ast; -using XamlIl.Transform; +using XamlX.Ast; +using XamlX.Transform; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class AvaloniaXamlIlAvaloniaPropertyResolver : IXamlIlAstTransformer + class AvaloniaXamlIlAvaloniaPropertyResolver : IXamlAstTransformer { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (node is XamlIlAstClrProperty prop) + if (node is XamlAstClrProperty prop) { var n = prop.Name + "Property"; var field = diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs new file mode 100644 index 00000000000..7944d8b5692 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs @@ -0,0 +1,332 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Avalonia.Markup.Parsers; +using Avalonia.Utilities; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; + +using XamlParseException = XamlX.XamlParseException; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlBindingPathParser : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is XamlAstObjectNode binding && binding.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)) + { + var convertedNode = ConvertLongFormPropertiesToBindingExpressionNode(context, binding); + + if (binding.Arguments.Count > 0 && binding.Arguments[0] is XamlAstTextNode bindingPathText) + { + var reader = new CharacterReader(bindingPathText.Text.AsSpan()); + var (nodes, _) = BindingExpressionGrammar.Parse(ref reader); + + if (convertedNode != null) + { + nodes.Insert(nodes.TakeWhile(x => x is BindingExpressionGrammar.ITransformNode).Count(), convertedNode); + } + + binding.Arguments[0] = new ParsedBindingPathNode(bindingPathText, context.GetAvaloniaTypes().CompiledBindingPath, nodes); + } + else + { + var bindingPathAssignment = binding.Children.OfType() + .FirstOrDefault(v => v.Property.GetClrProperty().Name == "Path"); + + if (bindingPathAssignment != null && bindingPathAssignment.Values[0] is XamlAstTextNode pathValue) + { + var reader = new CharacterReader(pathValue.Text.AsSpan()); + var (nodes, _) = BindingExpressionGrammar.Parse(ref reader); + + if (convertedNode != null) + { + nodes.Insert(nodes.TakeWhile(x => x is BindingExpressionGrammar.ITransformNode).Count(), convertedNode); + } + + bindingPathAssignment.Values[0] = new ParsedBindingPathNode(pathValue, context.GetAvaloniaTypes().CompiledBindingPath, nodes); + } + } + } + + return node; + } + + private static BindingExpressionGrammar.INode ConvertLongFormPropertiesToBindingExpressionNode( + AstTransformationContext context, + XamlAstObjectNode binding) + { + BindingExpressionGrammar.INode convertedNode = null; + + var syntheticCompiledBindingProperties = binding.Children.OfType() + .Where(v => v.Property is AvaloniaSyntheticCompiledBindingProperty) + .ToList(); + + var elementNameProperty = syntheticCompiledBindingProperties + .FirstOrDefault(v => + v.Property is AvaloniaSyntheticCompiledBindingProperty prop + && prop.Name == SyntheticCompiledBindingPropertyName.ElementName); + + var sourceProperty = syntheticCompiledBindingProperties + .FirstOrDefault(v => + v.Property is AvaloniaSyntheticCompiledBindingProperty prop + && prop.Name == SyntheticCompiledBindingPropertyName.Source); + + var relativeSourceProperty = syntheticCompiledBindingProperties + .FirstOrDefault(v => + v.Property is AvaloniaSyntheticCompiledBindingProperty prop + && prop.Name == SyntheticCompiledBindingPropertyName.RelativeSource); + + if (elementNameProperty?.Values[0] is XamlAstTextNode elementName) + { + convertedNode = new BindingExpressionGrammar.NameNode { Name = elementName.Text }; + } + else if (elementNameProperty != null) + { + throw new XamlParseException($"Invalid ElementName '{elementNameProperty.Values[0]}'.", elementNameProperty.Values[0]); + } + + if (sourceProperty?.Values[0] != null) + { + if (convertedNode != null) + { + throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding); + } + + convertedNode = new RawSourceBindingExpressionNode(sourceProperty?.Values[0]); + } + + if (GetRelativeSourceObjectFromAssignment( + context, + relativeSourceProperty, + out var relativeSourceObject)) + { + if (convertedNode != null) + { + throw new XamlParseException("Only one of ElementName, Source, or RelativeSource specified as a binding source. Only one property is allowed.", binding); + } + + var mode = relativeSourceObject.Children + .OfType() + .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Mode") + ?.Values[0] is XamlAstTextNode modeAssignedValue ? modeAssignedValue.Text : null; + if (relativeSourceObject.Arguments.Count == 0 && mode == null) + { + mode = "FindAncestor"; + } + + if (mode == "FindAncestor") + { + var ancestorLevel = relativeSourceObject.Children + .OfType() + .FirstOrDefault(x => x.Property.GetClrProperty().Name == "FindAncestor") + ?.Values[0] is XamlAstTextNode ancestorLevelText ? int.Parse(ancestorLevelText.Text) - 1 : 0; + + var treeType = relativeSourceObject.Children + .OfType() + .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Tree") + ?.Values[0] is XamlAstTextNode treeTypeValue ? treeTypeValue.Text : "Visual"; + + var ancestorTypeName = relativeSourceObject.Children + .OfType() + .FirstOrDefault(x => x.Property.GetClrProperty().Name == "AncestorType") + ?.Values[0] as XamlAstTextNode; + + IXamlType ancestorType = null; + if (ancestorTypeName is null) + { + if (treeType == "Visual") + { + throw new XamlParseException("AncestorType must be set for RelativeSourceMode.FindAncestor when searching the visual tree.", relativeSourceObject); + } + else if (treeType == "Logical") + { + var styledElementType = context.GetAvaloniaTypes().StyledElement; + ancestorType = context + .ParentNodes() + .OfType() + .Where(x => styledElementType.IsAssignableFrom(x.Type.GetClrType())) + .ElementAtOrDefault(ancestorLevel) + ?.Type.GetClrType(); + + if (ancestorType is null) + { + throw new XamlX.XamlParseException("Unable to resolve implicit ancestor type based on XAML tree.", relativeSourceObject); + } + } + } + else + { + ancestorType = TypeReferenceResolver.ResolveType( + context, + ancestorTypeName.Text, + false, + ancestorTypeName, + true).GetClrType(); + } + + if (treeType == "Visual") + { + convertedNode = new VisualAncestorBindingExpressionNode + { + Type = ancestorType, + Level = ancestorLevel + }; + } + else if (treeType == "Logical") + { + convertedNode = new LogicalAncestorBindingExpressionNode + { + Type = ancestorType, + Level = ancestorLevel + }; + } + else + { + throw new XamlParseException($"Unknown tree type '{treeType}'.", binding); + } + } + else if (mode == "DataContext") + { + convertedNode = null; + } + else if (mode == "Self") + { + convertedNode = new BindingExpressionGrammar.SelfNode(); + } + else if (mode == "TemplatedParent") + { + var parentType = context.ParentNodes().OfType() + .FirstOrDefault(x => + x.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.ControlTemplate) + ?.TargetType.GetClrType(); + + if (parentType is null) + { + throw new XamlParseException("A binding with a TemplatedParent RelativeSource has to be in a ControlTemplate.", binding); + } + + convertedNode = new TemplatedParentBindingExpressionNode { Type = parentType }; + } + else + { + throw new XamlParseException($"Unknown RelativeSource mode '{mode}'.", binding); + } + } + + if (elementNameProperty != null) + { + binding.Children.Remove(elementNameProperty); + } + if (sourceProperty != null) + { + binding.Children.Remove(sourceProperty); + } + if (relativeSourceProperty != null) + { + binding.Children.Remove(relativeSourceProperty); + } + + return convertedNode; + } + + private static bool GetRelativeSourceObjectFromAssignment( + AstTransformationContext context, + XamlAstXamlPropertyValueNode relativeSourceProperty, + out XamlAstObjectNode relativeSourceObject) + { + relativeSourceObject = null; + if (relativeSourceProperty is null) + { + return false; + } + + if (relativeSourceProperty.Values[0] is XamlMarkupExtensionNode me) + { + if (me.Type.GetClrType() != context.GetAvaloniaTypes().RelativeSource) + { + throw new XamlParseException($"Expected an object of type 'Avalonia.Data.RelativeSource'. Found a object of type '{me.Type.GetClrType().GetFqn()}'", me); + } + + relativeSourceObject = (XamlAstObjectNode)me.Value; + return true; + } + + if (relativeSourceProperty.Values[0] is XamlAstObjectNode on) + { + if (on.Type.GetClrType() != context.GetAvaloniaTypes().RelativeSource) + { + throw new XamlParseException($"Expected an object of type 'Avalonia.Data.RelativeSource'. Found a object of type '{on.Type.GetClrType().GetFqn()}'", on); + } + + relativeSourceObject = on; + return true; + } + + return false; + } + } + + class ParsedBindingPathNode : XamlAstNode, IXamlAstValueNode + { + public ParsedBindingPathNode(IXamlLineInfo lineInfo, IXamlType compiledBindingType, IList path) + : base(lineInfo) + { + Type = new XamlAstClrTypeReference(lineInfo, compiledBindingType, false); + Path = path; + } + + public IXamlAstTypeReference Type { get; } + + public IList Path { get; } + + public override void VisitChildren(IXamlAstVisitor visitor) + { + for (int i = 0; i < Path.Count; i++) + { + if (Path[i] is IXamlAstNode ast) + { + Path[i] = (BindingExpressionGrammar.INode)ast.Visit(visitor); + } + } + } + } + + class VisualAncestorBindingExpressionNode : BindingExpressionGrammar.INode + { + public IXamlType Type { get; set; } + public int Level { get; set; } + } + + class LogicalAncestorBindingExpressionNode : BindingExpressionGrammar.INode + { + public IXamlType Type { get; set; } + public int Level { get; set; } + } + + class TemplatedParentBindingExpressionNode : BindingExpressionGrammar.INode + { + public IXamlType Type { get; set; } + } + + class RawSourceBindingExpressionNode : XamlAstNode, BindingExpressionGrammar.INode + { + public RawSourceBindingExpressionNode(IXamlAstValueNode rawSource) + : base(rawSource) + { + RawSource = rawSource; + } + + public IXamlAstValueNode RawSource { get; private set; } + + public override void VisitChildren(IXamlAstVisitor visitor) + { + RawSource = (IXamlAstValueNode)RawSource.Visit(visitor); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs new file mode 100644 index 00000000000..fc320846877 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlBindingPathTransformer : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is XamlAstConstructableObjectNode binding && binding.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)) + { + IXamlType startType; + var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault(); + if (parentDataContextNode is null) + { + throw new XamlX.XamlParseException("Cannot parse a compiled binding without an explicit x:DataType directive to give a starting data type for bindings.", binding); + } + + startType = parentDataContextNode.DataContextType; + + XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, binding, startType); + } + + return node; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlCompiledBindingsMetadataRemover.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlCompiledBindingsMetadataRemover.cs new file mode 100644 index 00000000000..70bd15ea122 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlCompiledBindingsMetadataRemover.cs @@ -0,0 +1,24 @@ +using System.Linq; +using XamlX.Ast; +using XamlX.Transform; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlCompiledBindingsMetadataRemover : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + while (true) + { + if (node is NestedScopeMetadataNode nestedScope) + node = nestedScope.Value; + else if (node is AvaloniaXamlIlDataContextTypeMetadataNode dataContextType) + node = dataContextType.Value; + else if (node is AvaloniaXamlIlCompileBindingsNode compileBindings) + node = compileBindings.Value; + else + return node; + } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs similarity index 53% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs index 7e8f296b443..35e2624ff9b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs @@ -1,15 +1,17 @@ using System.Linq; -using XamlIl.Ast; -using XamlIl.Transform; -using XamlIl.TypeSystem; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class AvaloniaXamlIlConstructorServiceProviderTransformer : IXamlIlAstTransformer + class AvaloniaXamlIlConstructorServiceProviderTransformer : IXamlAstTransformer { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (node is XamlIlAstObjectNode on && on.Arguments.Count == 0) + if (node is XamlAstObjectNode on && on.Arguments.Count == 0) { var ctors = on.Type.GetClrType().Constructors; if (!ctors.Any(c => c.IsPublic && !c.IsStatic && c.Parameters.Count == 0)) @@ -27,20 +29,20 @@ public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlA return node; } - class InjectServiceProviderNode : XamlIlAstNode, IXamlIlAstValueNode,IXamlIlAstNodeNeedsParentStack, - IXamlIlAstEmitableNode + class InjectServiceProviderNode : XamlAstNode, IXamlAstValueNode,IXamlAstNodeNeedsParentStack, + IXamlAstEmitableNode { - public InjectServiceProviderNode(IXamlIlType type, IXamlIlLineInfo lineInfo) : base(lineInfo) + public InjectServiceProviderNode(IXamlType type, IXamlLineInfo lineInfo) : base(lineInfo) { - Type = new XamlIlAstClrTypeReference(lineInfo, type, false); + Type = new XamlAstClrTypeReference(lineInfo, type, false); } - public IXamlIlAstTypeReference Type { get; } + public IXamlAstTypeReference Type { get; } public bool NeedsParentStack => true; - public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) { codeGen.Ldloc(context.ContextLocal); - return XamlIlNodeEmitResult.Type(0, Type.GetClrType()); + return XamlILNodeEmitResult.Type(0, Type.GetClrType()); } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs similarity index 70% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs index 40386924c32..f95d086bf6c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs @@ -1,29 +1,29 @@ using System.Linq; -using XamlIl.Ast; -using XamlIl.Transform; -using XamlIl.TypeSystem; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer : IXamlIlAstTransformer + class AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer : IXamlAstTransformer { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (!(node is XamlIlAstObjectNode on + if (!(node is XamlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Markup.Xaml.Templates.ControlTemplate")) return node; - var tt = on.Children.OfType().FirstOrDefault(ch => + var tt = on.Children.OfType().FirstOrDefault(ch => ch.Property.GetClrProperty().Name == "TargetType"); if (context.ParentNodes().FirstOrDefault() is AvaloniaXamlIlTargetTypeMetadataNode) // Deja vu. I've just been in this place before return node; - IXamlIlAstTypeReference targetType; + IXamlAstTypeReference targetType; var templatableBaseType = context.Configuration.TypeSystem.GetType("Avalonia.Controls.Control"); - if ((tt?.Values.FirstOrDefault() is XamlIlTypeExtensionNode tn)) + if ((tt?.Values.FirstOrDefault() is XamlTypeExtensionNode tn)) { targetType = tn.Value; } @@ -33,11 +33,11 @@ public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlA .FirstOrDefault(); if (parentScope?.ScopeType == AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style) targetType = parentScope.TargetType; - else if (context.ParentNodes().Skip(1).FirstOrDefault() is XamlIlAstObjectNode directParentNode + else if (context.ParentNodes().Skip(1).FirstOrDefault() is XamlAstObjectNode directParentNode && templatableBaseType.IsAssignableFrom(directParentNode.Type.GetClrType())) targetType = directParentNode.Type; else - targetType = new XamlIlAstClrTypeReference(node, + targetType = new XamlAstClrTypeReference(node, templatableBaseType, false); } @@ -48,9 +48,9 @@ public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlA } } - class AvaloniaXamlIlTargetTypeMetadataNode : XamlIlValueWithSideEffectNodeBase + class AvaloniaXamlIlTargetTypeMetadataNode : XamlValueWithSideEffectNodeBase { - public IXamlIlAstTypeReference TargetType { get; set; } + public IXamlAstTypeReference TargetType { get; set; } public ScopeTypes ScopeType { get; } public enum ScopeTypes @@ -60,7 +60,7 @@ public enum ScopeTypes Transitions } - public AvaloniaXamlIlTargetTypeMetadataNode(IXamlIlAstValueNode value, IXamlIlAstTypeReference targetType, + public AvaloniaXamlIlTargetTypeMetadataNode(IXamlAstValueNode value, IXamlAstTypeReference targetType, ScopeTypes type) : base(value, value) { diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs new file mode 100644 index 00000000000..241976241f2 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs @@ -0,0 +1,197 @@ +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlDataContextTypeTransformer : IXamlAstTransformer + { + private const string AvaloniaNs = "https://github.com/avaloniaui"; + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (context.ParentNodes().FirstOrDefault() is AvaloniaXamlIlDataContextTypeMetadataNode) + { + // We've already resolved the data context type for this node. + return node; + } + + if (node is XamlAstConstructableObjectNode on) + { + AvaloniaXamlIlDataContextTypeMetadataNode inferredDataContextTypeNode = null; + AvaloniaXamlIlDataContextTypeMetadataNode directiveDataContextTypeNode = null; + bool isDataTemplate = on.Type.GetClrType().Equals(context.GetAvaloniaTypes().DataTemplate); + + for (int i = 0; i < on.Children.Count; ++i) + { + var child = on.Children[i]; + if (child is XamlAstXmlDirective directive) + { + if (directive.Namespace == XamlNamespaces.Xaml2006 + && directive.Name == "DataType" + && directive.Values.Count == 1) + { + on.Children.RemoveAt(i); + i--; + if (directive.Values[0] is XamlAstTextNode text) + { + directiveDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, + TypeReferenceResolver.ResolveType(context, text.Text, isMarkupExtension: false, text, strict: true).Type); + } + else + { + throw new XamlX.XamlParseException("x:DataType should be set to a type name.", directive.Values[0]); + } + } + } + else if (child is XamlPropertyAssignmentNode pa) + { + if (pa.Property.Name == "DataContext" + && pa.Property.DeclaringType.Equals(context.GetAvaloniaTypes().StyledElement) + && pa.Values[0] is XamlMarkupExtensionNode ext + && ext.Value is XamlAstConstructableObjectNode obj) + { + inferredDataContextTypeNode = ParseDataContext(context, on, obj); + } + else if(isDataTemplate + && pa.Property.Name == "DataType" + && pa.Values[0] is XamlTypeExtensionNode dataTypeNode) + { + inferredDataContextTypeNode = new AvaloniaXamlIlDataContextTypeMetadataNode(on, dataTypeNode.Value.GetClrType()); + } + } + } + + // If there is no x:DataType directive, + // do more specialized inference + if (directiveDataContextTypeNode is null) + { + if (isDataTemplate && inferredDataContextTypeNode is null) + { + // Infer data type from collection binding on a control that displays items. + var parentObject = context.ParentNodes().OfType().FirstOrDefault(); + if (parentObject != null && context.GetAvaloniaTypes().IItemsPresenterHost.IsDirectlyAssignableFrom(parentObject.Type.GetClrType())) + { + inferredDataContextTypeNode = InferDataContextOfPresentedItem(context, on, parentObject); + } + else + { + inferredDataContextTypeNode = new AvaloniaXamlIlUninferrableDataContextMetadataNode(on); + } + } + } + + return directiveDataContextTypeNode ?? inferredDataContextTypeNode ?? node; + } + + return node; + } + + private static AvaloniaXamlIlDataContextTypeMetadataNode InferDataContextOfPresentedItem(AstTransformationContext context, XamlAstConstructableObjectNode on, XamlAstConstructableObjectNode parentObject) + { + var parentItemsValue = parentObject + .Children.OfType() + .FirstOrDefault(pa => pa.Property.Name == "Items") + ?.Values[0]; + if (parentItemsValue is null) + { + // We can't infer the collection type and the currently calculated type is definitely wrong. + // Notify the user that we were unable to infer the data context type if they use a compiled binding. + return new AvaloniaXamlIlUninferrableDataContextMetadataNode(on); + } + + IXamlType itemsCollectionType = null; + if (context.GetAvaloniaTypes().IBinding.IsAssignableFrom(parentItemsValue.Type.GetClrType())) + { + if (parentItemsValue.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension) + && parentItemsValue is XamlMarkupExtensionNode ext && ext.Value is XamlAstConstructableObjectNode parentItemsBinding) + { + var parentItemsDataContext = context.ParentNodes().SkipWhile(n => n != parentObject).OfType().FirstOrDefault(); + if (parentItemsDataContext != null) + { + itemsCollectionType = XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, parentItemsBinding, parentItemsDataContext.DataContextType); + } + } + } + else + { + itemsCollectionType = parentItemsValue.Type.GetClrType(); + } + + if (itemsCollectionType != null) + { + foreach (var i in GetAllInterfacesIncludingSelf(itemsCollectionType)) + { + if (i.GenericTypeDefinition?.Equals(context.Configuration.WellKnownTypes.IEnumerableT) == true) + { + return new AvaloniaXamlIlDataContextTypeMetadataNode(on, i.GenericArguments[0]); + } + } + } + // We can't infer the collection type and the currently calculated type is definitely wrong. + // Notify the user that we were unable to infer the data context type if they use a compiled binding. + return new AvaloniaXamlIlUninferrableDataContextMetadataNode(on); + } + + private static AvaloniaXamlIlDataContextTypeMetadataNode ParseDataContext(AstTransformationContext context, XamlAstConstructableObjectNode on, XamlAstConstructableObjectNode obj) + { + var bindingType = context.GetAvaloniaTypes().IBinding; + if (!bindingType.IsAssignableFrom(obj.Type.GetClrType()) && !obj.Type.GetClrType().Equals(context.GetAvaloniaTypes().ReflectionBindingExtension)) + { + return new AvaloniaXamlIlDataContextTypeMetadataNode(on, obj.Type.GetClrType()); + } + else if (obj.Type.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)) + { + IXamlType startType; + var parentDataContextNode = context.ParentNodes().OfType().FirstOrDefault(); + if (parentDataContextNode is null) + { + throw new XamlX.XamlParseException("Cannot parse a compiled binding without an explicit x:DataType directive to give a starting data type for bindings.", obj); + } + + startType = parentDataContextNode.DataContextType; + + var bindingResultType = XamlIlBindingPathHelper.UpdateCompiledBindingExtension(context, obj, startType); + return new AvaloniaXamlIlDataContextTypeMetadataNode(on, bindingResultType); + } + + return new AvaloniaXamlIlUninferrableDataContextMetadataNode(on); + } + + private static IEnumerable GetAllInterfacesIncludingSelf(IXamlType type) + { + if (type.IsInterface) + yield return type; + + foreach (var i in type.GetAllInterfaces()) + yield return i; + } + } + + [DebuggerDisplay("DataType = {DataContextType}")] + class AvaloniaXamlIlDataContextTypeMetadataNode : XamlValueWithSideEffectNodeBase + { + public virtual IXamlType DataContextType { get; } + + public AvaloniaXamlIlDataContextTypeMetadataNode(IXamlAstValueNode value, IXamlType targetType) + : base(value, value) + { + DataContextType = targetType; + } + } + + [DebuggerDisplay("DataType = Unknown")] + class AvaloniaXamlIlUninferrableDataContextMetadataNode : AvaloniaXamlIlDataContextTypeMetadataNode + { + public AvaloniaXamlIlUninferrableDataContextMetadataNode(IXamlAstValueNode value) + : base(value, null) + { + } + + public override IXamlType DataContextType => throw new XamlTransformException("Unable to infer DataContext type for compiled bindings nested within this element.", Value); + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDesignPropertiesTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDesignPropertiesTransformer.cs similarity index 68% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDesignPropertiesTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDesignPropertiesTransformer.cs index 9cc4c5cf11f..3096be522fd 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDesignPropertiesTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlDesignPropertiesTransformer.cs @@ -1,12 +1,12 @@ using System.Collections.Generic; using System.Linq; -using XamlIl; -using XamlIl.Ast; -using XamlIl.Transform; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class AvaloniaXamlIlDesignPropertiesTransformer : IXamlIlAstTransformer + class AvaloniaXamlIlDesignPropertiesTransformer : IXamlAstTransformer { public bool IsDesignMode { get; set; } @@ -17,14 +17,14 @@ class AvaloniaXamlIlDesignPropertiesTransformer : IXamlIlAstTransformer }; private const string AvaloniaNs = "https://github.com/avaloniaui"; - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (node is XamlIlAstObjectNode on) + if (node is XamlAstObjectNode on) { for (var c=0; c() + .FirstOrDefault(); + if(parentScope == null) + throw new XamlX.XamlParseException("No target type scope found for property path", text); + if (parentScope.ScopeType != AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style) + throw new XamlX.XamlParseException("PropertyPath is currently only valid for styles", pv); + + + IEnumerable parsed; + try + { + parsed = PropertyPathGrammar.Parse(text.Text); + } + catch (Exception e) + { + throw new XamlX.XamlParseException("Unable to parse PropertyPath: " + e.Message, text); + } + + var elements = new List(); + IXamlType currentType = parentScope.TargetType.GetClrType(); + + + var expectProperty = true; + var expectCast = true; + var expectTraversal = false; + var types = context.GetAvaloniaTypes(); + + IXamlType GetType(string ns, string name) + { + return TypeReferenceResolver.ResolveType(context, $"{ns}:{name}", false, + text, true).GetClrType(); + } + + void HandleProperty(string name, string typeNamespace, string typeName) + { + if(!expectProperty || currentType == null) + throw new XamlX.XamlParseException("Unexpected property node", text); + + var propertySearchType = + typeName != null ? GetType(typeNamespace, typeName) : currentType; + + IXamlIlPropertyPathElementNode prop = null; + var avaloniaPropertyFieldName = name + "Property"; + var avaloniaPropertyField = propertySearchType.GetAllFields().FirstOrDefault(f => + f.IsStatic && f.IsPublic && f.Name == avaloniaPropertyFieldName); + if (avaloniaPropertyField != null) + { + prop = new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyField, + XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyField, types, text)); + } + else + { + var clrProperty = propertySearchType.GetAllProperties().FirstOrDefault(p => p.Name == name); + prop = new XamlIClrPropertyPathElementNode(clrProperty); + } + + if (prop == null) + throw new XamlX.XamlParseException( + $"Unable to resolve property {name} on type {propertySearchType.GetFqn()}", + text); + + currentType = prop.Type; + elements.Add(prop); + expectProperty = false; + expectTraversal = expectCast = true; + } + + foreach (var ge in parsed) + { + if (ge is PropertyPathGrammar.ChildTraversalSyntax) + { + if (!expectTraversal) + throw new XamlX.XamlParseException("Unexpected child traversal .", text); + elements.Add(new XamlIlChildTraversalPropertyPathElementNode()); + expectTraversal = expectCast = false; + expectProperty = true; + } + else if (ge is PropertyPathGrammar.EnsureTypeSyntax ets) + { + if(!expectCast) + throw new XamlX.XamlParseException("Unexpected cast node", text); + currentType = GetType(ets.TypeNamespace, ets.TypeName); + elements.Add(new XamlIlCastPropertyPathElementNode(currentType, true)); + expectProperty = false; + expectCast = expectTraversal = true; + } + else if (ge is PropertyPathGrammar.CastTypeSyntax cts) + { + if(!expectCast) + throw new XamlX.XamlParseException("Unexpected cast node", text); + //TODO: Check if cast can be done + currentType = GetType(cts.TypeNamespace, cts.TypeName); + elements.Add(new XamlIlCastPropertyPathElementNode(currentType, false)); + expectProperty = false; + expectCast = expectTraversal = true; + } + else if (ge is PropertyPathGrammar.PropertySyntax ps) + { + HandleProperty(ps.Name, null, null); + } + else if (ge is PropertyPathGrammar.TypeQualifiedPropertySyntax tqps) + { + HandleProperty(tqps.Name, tqps.TypeNamespace, tqps.TypeName); + } + else + throw new XamlX.XamlParseException("Unexpected node " + ge, text); + + } + var propertyPathNode = new XamlIlPropertyPathNode(text, elements, types); + if (propertyPathNode.Type == null) + throw new XamlX.XamlParseException("Unexpected end of the property path", text); + pv.Values[0] = propertyPathNode; + } + + return node; + } + + interface IXamlIlPropertyPathElementNode + { + void Emit(XamlEmitContext context, IXamlILEmitter codeGen); + IXamlType Type { get; } + } + + class XamlIlChildTraversalPropertyPathElementNode : IXamlIlPropertyPathElementNode + { + public void Emit(XamlEmitContext context, IXamlILEmitter codeGen) + => codeGen.EmitCall( + context.GetAvaloniaTypes() + .PropertyPathBuilder.FindMethod(m => m.Name == "ChildTraversal")); + + public IXamlType Type => null; + } + + class XamlIlAvaloniaPropertyPropertyPathElementNode : IXamlIlPropertyPathElementNode + { + private readonly IXamlField _field; + + public XamlIlAvaloniaPropertyPropertyPathElementNode(IXamlField field, IXamlType propertyType) + { + _field = field; + Type = propertyType; + } + + public void Emit(XamlEmitContext context, IXamlILEmitter codeGen) + => codeGen + .Ldsfld(_field) + .EmitCall(context.GetAvaloniaTypes() + .PropertyPathBuilder.FindMethod(m => m.Name == "Property")); + + public IXamlType Type { get; } + } + + class XamlIClrPropertyPathElementNode : IXamlIlPropertyPathElementNode + { + private readonly IXamlProperty _property; + + public XamlIClrPropertyPathElementNode(IXamlProperty property) + { + _property = property; + } + + public void Emit(XamlEmitContext context, IXamlILEmitter codeGen) + { + context.Configuration.GetExtra() + .Emit(context, codeGen, _property); + + codeGen.EmitCall(context.GetAvaloniaTypes() + .PropertyPathBuilder.FindMethod(m => m.Name == "Property")); + } + + public IXamlType Type => _property.Getter?.ReturnType ?? _property.Setter?.Parameters[0]; + } + + class XamlIlCastPropertyPathElementNode : IXamlIlPropertyPathElementNode + { + private readonly IXamlType _type; + private readonly bool _ensureType; + + public XamlIlCastPropertyPathElementNode(IXamlType type, bool ensureType) + { + _type = type; + _ensureType = ensureType; + } + + public void Emit(XamlEmitContext context, IXamlILEmitter codeGen) + { + codeGen + .Ldtype(_type) + .EmitCall(context.GetAvaloniaTypes() + .PropertyPathBuilder.FindMethod(m => m.Name == (_ensureType ? "EnsureType" : "Cast"))); + } + + public IXamlType Type => _type; + } + + class XamlIlPropertyPathNode : XamlAstNode, IXamlIlPropertyPathNode, IXamlAstEmitableNode + { + private readonly List _elements; + private readonly AvaloniaXamlIlWellKnownTypes _types; + + public XamlIlPropertyPathNode(IXamlLineInfo lineInfo, + List elements, + AvaloniaXamlIlWellKnownTypes types) : base(lineInfo) + { + _elements = elements; + _types = types; + Type = new XamlAstClrTypeReference(this, types.PropertyPath, false); + } + + public IXamlAstTypeReference Type { get; } + public IXamlType PropertyType => _elements.LastOrDefault()?.Type; + public XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) + { + codeGen + .Newobj(_types.PropertyPathBuilder.FindConstructor()); + foreach(var e in _elements) + e.Emit(context, codeGen); + codeGen.EmitCall(_types.PropertyPathBuilder.FindMethod(m => m.Name == "Build")); + return XamlILNodeEmitResult.Type(0, _types.PropertyPath); + } + } + } + + interface IXamlIlPropertyPathNode : IXamlAstValueNode + { + IXamlType PropertyType { get; } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResolveByNameMarkupExtensionReplacer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResolveByNameMarkupExtensionReplacer.cs new file mode 100644 index 00000000000..c0ac841b7f4 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlResolveByNameMarkupExtensionReplacer.cs @@ -0,0 +1,49 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; + +#nullable enable +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlResolveByNameMarkupExtensionReplacer : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (!(node is XamlAstXamlPropertyValueNode propertyValueNode)) return node; + + if (!(propertyValueNode.Property is XamlAstClrProperty clrProperty)) return node; + + IEnumerable attributes = propertyValueNode.Property.GetClrProperty().CustomAttributes; + + if (propertyValueNode.Property is XamlAstClrProperty referenceNode && + referenceNode.Getter != null) + { + attributes = attributes.Concat(referenceNode.Getter.CustomAttributes); + } + + if (attributes.All(attribute => attribute.Type.FullName != "Avalonia.Controls.ResolveByNameAttribute")) + return node; + + if (propertyValueNode.Values.Count != 1 || !(propertyValueNode.Values.First() is XamlAstTextNode)) + return node; + + var newNode = new XamlAstObjectNode( + propertyValueNode.Values[0], + new XamlAstClrTypeReference(propertyValueNode.Values[0], + context.GetAvaloniaTypes().ResolveByNameExtension, true)) + { + Arguments = new List { propertyValueNode.Values[0] } + }; + + if (XamlTransformHelpers.TryConvertMarkupExtension(context, newNode, out var extensionNode)) + { + propertyValueNode.Values[0] = extensionNode; + } + + return node; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlRootObjectScopeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlRootObjectScopeTransformer.cs new file mode 100644 index 00000000000..0513c78a7a1 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlRootObjectScopeTransformer.cs @@ -0,0 +1,72 @@ +using System; +using System.Linq; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; +using XamlX.IL; +using XamlX.Emit; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlRootObjectScope : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (!context.ParentNodes().Any() + && node is XamlValueWithManipulationNode mnode) + { + mnode.Manipulation = new XamlManipulationGroupNode(mnode, + new[] + { + mnode.Manipulation, + new HandleRootObjectScopeNode(mnode, context.GetAvaloniaTypes()) + }); + } + return node; + } + class HandleRootObjectScopeNode : XamlAstNode, IXamlAstManipulationNode + { + private readonly AvaloniaXamlIlWellKnownTypes _types; + + public HandleRootObjectScopeNode(IXamlLineInfo lineInfo, + AvaloniaXamlIlWellKnownTypes types) : base(lineInfo) + { + _types = types; + } + } + internal class Emitter : IXamlILAstNodeEmitter + { + public XamlILNodeEmitResult Emit(IXamlAstNode node, XamlEmitContext context, IXamlILEmitter codeGen) + { + if (!(node is HandleRootObjectScopeNode)) + { + return null; + } + var types = context.GetAvaloniaTypes(); + + var next = codeGen.DefineLabel(); + var scopeField = context.RuntimeContext.ContextType.Fields.First(f => + f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName); + using (var local = codeGen.LocalsPool.GetLocal(types.StyledElement)) + { + codeGen + .Isinst(types.StyledElement) + .Dup() + .Stloc(local.Local) + .Brfalse(next) + .Ldloc(local.Local) + .Ldloc(context.ContextLocal) + .Ldfld(scopeField) + .EmitCall(types.NameScopeSetNameScope, true) + .MarkLabel(next) + .Ldloc(context.ContextLocal) + .Ldfld(scopeField) + .EmitCall(types.INameScopeComplete, true); + } + + return XamlILNodeEmitResult.Void(1); + } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs similarity index 69% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs index d5114244cf9..b81d25d6132 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs @@ -3,41 +3,45 @@ using System.Globalization; using System.Linq; using Avalonia.Markup.Parsers; -using XamlIl; -using XamlIl.Ast; -using XamlIl.Transform; -using XamlIl.Transform.Transformers; -using XamlIl.TypeSystem; +using XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class AvaloniaXamlIlSelectorTransformer : IXamlIlAstTransformer + using XamlParseException = XamlX.XamlParseException; + using XamlLoadException = XamlX.XamlLoadException; + class AvaloniaXamlIlSelectorTransformer : IXamlAstTransformer { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (!(node is XamlIlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Styling.Style")) + if (!(node is XamlAstObjectNode on && on.Type.GetClrType().FullName == "Avalonia.Styling.Style")) return node; - var pn = on.Children.OfType() + var pn = on.Children.OfType() .FirstOrDefault(p => p.Property.GetClrProperty().Name == "Selector"); if (pn == null) return node; if (pn.Values.Count != 1) - throw new XamlIlParseException("Selector property should should have exactly one value", node); + throw new XamlParseException("Selector property should should have exactly one value", node); if (pn.Values[0] is XamlIlSelectorNode) //Deja vu. I've just been in this place before return node; - if (!(pn.Values[0] is XamlIlAstTextNode tn)) - throw new XamlIlParseException("Selector property should be a text node", node); + if (!(pn.Values[0] is XamlAstTextNode tn)) + throw new XamlParseException("Selector property should be a text node", node); var selectorType = pn.Property.GetClrProperty().Getter.ReturnType; var initialNode = new XamlIlSelectorInitialNode(node, selectorType); XamlIlSelectorNode Create(IEnumerable syntax, - Func typeResolver) + Func typeResolver) { XamlIlSelectorNode result = initialNode; XamlIlOrSelectorNode results = null; @@ -63,18 +67,18 @@ XamlIlSelectorNode Create(IEnumerable syntax, var type = result?.TargetType; if (type == null) - throw new XamlIlParseException("Property selectors must be applied to a type.", node); + throw new XamlParseException("Property selectors must be applied to a type.", node); var targetProperty = type.GetAllProperties().FirstOrDefault(p => p.Name == property.Property); if (targetProperty == null) - throw new XamlIlParseException($"Cannot find '{property.Property}' on '{type}", node); + throw new XamlParseException($"Cannot find '{property.Property}' on '{type}", node); - if (!XamlIlTransformHelpers.TryGetCorrectlyTypedValue(context, - new XamlIlAstTextNode(node, property.Value, context.Configuration.WellKnownTypes.String), + if (!XamlTransformHelpers.TryGetCorrectlyTypedValue(context, + new XamlAstTextNode(node, property.Value, context.Configuration.WellKnownTypes.String), targetProperty.PropertyType, out var typedValue)) - throw new XamlIlParseException( + throw new XamlParseException( $"Cannot convert '{property.Value}' to '{targetProperty.PropertyType.GetFqn()}", node); @@ -100,7 +104,7 @@ XamlIlSelectorNode Create(IEnumerable syntax, result = initialNode; break; default: - throw new XamlIlParseException($"Unsupported selector grammar '{i.GetType()}'.", node); + throw new XamlParseException($"Unsupported selector grammar '{i.GetType()}'.", node); } } @@ -119,15 +123,15 @@ XamlIlSelectorNode Create(IEnumerable syntax, } catch (Exception e) { - throw new XamlIlParseException("Unable to parse selector: " + e.Message, node); + throw new XamlParseException("Unable to parse selector: " + e.Message, node); } var selector = Create(parsed, (p, n) - => XamlIlTypeReferenceResolver.ResolveType(context, $"{p}:{n}", true, node, true)); + => TypeReferenceResolver.ResolveType(context, $"{p}:{n}", true, node, true)); pn.Values[0] = selector; return new AvaloniaXamlIlTargetTypeMetadataNode(on, - new XamlIlAstClrTypeReference(selector, selector.TargetType, false), + new XamlAstClrTypeReference(selector, selector.TargetType, false), AvaloniaXamlIlTargetTypeMetadataNode.ScopeTypes.Style); } @@ -135,32 +139,32 @@ XamlIlSelectorNode Create(IEnumerable syntax, - abstract class XamlIlSelectorNode : XamlIlAstNode, IXamlIlAstValueNode, IXamlIlAstEmitableNode + abstract class XamlIlSelectorNode : XamlAstNode, IXamlAstValueNode, IXamlAstEmitableNode { protected XamlIlSelectorNode Previous { get; } - public abstract IXamlIlType TargetType { get; } + public abstract IXamlType TargetType { get; } public XamlIlSelectorNode(XamlIlSelectorNode previous, - IXamlIlLineInfo info = null, - IXamlIlType selectorType = null) : base(info ?? previous) + IXamlLineInfo info = null, + IXamlType selectorType = null) : base(info ?? previous) { Previous = previous; - Type = selectorType == null ? previous.Type : new XamlIlAstClrTypeReference(this, selectorType, false); + Type = selectorType == null ? previous.Type : new XamlAstClrTypeReference(this, selectorType, false); } - public IXamlIlAstTypeReference Type { get; } + public IXamlAstTypeReference Type { get; } - public virtual XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public virtual XamlILNodeEmitResult Emit(XamlEmitContext context, IXamlILEmitter codeGen) { if (Previous != null) context.Emit(Previous, codeGen, Type.GetClrType()); DoEmit(context, codeGen); - return XamlIlNodeEmitResult.Type(0, Type.GetClrType()); + return XamlILNodeEmitResult.Type(0, Type.GetClrType()); } - protected abstract void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen); + protected abstract void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen); - protected void EmitCall(XamlIlEmitContext context, IXamlIlEmitter codeGen, Func method) + protected void EmitCall(XamlEmitContext context, IXamlILEmitter codeGen, Func method) { var selectors = context.Configuration.TypeSystem.GetType("Avalonia.Styling.Selectors"); var found = selectors.FindMethod(m => m.IsStatic && m.Parameters.Count > 0 && method(m)); @@ -170,27 +174,27 @@ protected void EmitCall(XamlIlEmitContext context, IXamlIlEmitter codeGen, Func< class XamlIlSelectorInitialNode : XamlIlSelectorNode { - public XamlIlSelectorInitialNode(IXamlIlLineInfo info, - IXamlIlType selectorType) : base(null, info, selectorType) + public XamlIlSelectorInitialNode(IXamlLineInfo info, + IXamlType selectorType) : base(null, info, selectorType) { } - public override IXamlIlType TargetType => null; - protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen) => codeGen.Ldnull(); + public override IXamlType TargetType => null; + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) => codeGen.Ldnull(); } class XamlIlTypeSelector : XamlIlSelectorNode { public bool Concrete { get; } - public XamlIlTypeSelector(XamlIlSelectorNode previous, IXamlIlType type, bool concrete) : base(previous) + public XamlIlTypeSelector(XamlIlSelectorNode previous, IXamlType type, bool concrete) : base(previous) { TargetType = type; Concrete = concrete; } - public override IXamlIlType TargetType { get; } - protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public override IXamlType TargetType { get; } + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) { var name = Concrete ? "OfType" : "Is"; codeGen.Ldtype(TargetType); @@ -217,8 +221,8 @@ public XamlIlStringSelector(XamlIlSelectorNode previous, SelectorType type, stri } - public override IXamlIlType TargetType => Previous?.TargetType; - protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public override IXamlType TargetType => Previous?.TargetType; + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) { codeGen.Ldstr(String); var name = _type.ToString(); @@ -242,8 +246,8 @@ public XamlIlCombinatorSelector(XamlIlSelectorNode previous, SelectorType type) _type = type; } - public override IXamlIlType TargetType => null; - protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public override IXamlType TargetType => null; + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) { var name = _type.ToString(); EmitCall(context, codeGen, @@ -260,8 +264,8 @@ public XamlIlNotSelector(XamlIlSelectorNode previous, XamlIlSelectorNode argumen Argument = argument; } - public override IXamlIlType TargetType => Previous?.TargetType; - protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public override IXamlType TargetType => Previous?.TargetType; + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) { context.Emit(Argument, codeGen, Type.GetClrType()); EmitCall(context, codeGen, @@ -272,22 +276,22 @@ protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen class XamlIlPropertyEqualsSelector : XamlIlSelectorNode { public XamlIlPropertyEqualsSelector(XamlIlSelectorNode previous, - IXamlIlProperty property, - IXamlIlAstValueNode value) + IXamlProperty property, + IXamlAstValueNode value) : base(previous) { Property = property; Value = value; } - public IXamlIlProperty Property { get; set; } - public IXamlIlAstValueNode Value { get; set; } + public IXamlProperty Property { get; set; } + public IXamlAstValueNode Value { get; set; } - public override IXamlIlType TargetType => Previous?.TargetType; - protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public override IXamlType TargetType => Previous?.TargetType; + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) { if (!XamlIlAvaloniaPropertyHelper.Emit(context, codeGen, Property)) - throw new XamlIlLoadException( + throw new XamlLoadException( $"{Property.Name} of {(Property.Setter ?? Property.Getter).DeclaringType.GetFqn()} doesn't seem to be an AvaloniaProperty", this); context.Emit(Value, codeGen, context.Configuration.WellKnownTypes.Object); @@ -302,7 +306,7 @@ protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen class XamlIlOrSelectorNode : XamlIlSelectorNode { List _selectors = new List(); - public XamlIlOrSelectorNode(IXamlIlLineInfo info, IXamlIlType selectorType) : base(null, info, selectorType) + public XamlIlOrSelectorNode(IXamlLineInfo info, IXamlType selectorType) : base(null, info, selectorType) { } @@ -311,11 +315,11 @@ public void Add(XamlIlSelectorNode node) _selectors.Add(node); } - public override IXamlIlType TargetType + public override IXamlType TargetType { get { - IXamlIlType result = null; + IXamlType result = null; foreach (var selector in _selectors) { @@ -340,10 +344,10 @@ public override IXamlIlType TargetType } } - protected override void DoEmit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + protected override void DoEmit(XamlEmitContext context, IXamlILEmitter codeGen) { if (_selectors.Count == 0) - throw new XamlIlLoadException("Invalid selector count", this); + throw new XamlLoadException("Invalid selector count", this); if (_selectors.Count == 1) { _selectors[0].Emit(context, codeGen); diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs new file mode 100644 index 00000000000..e816265422f --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Data.Core; +using XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + using XamlParseException = XamlX.XamlParseException; + using XamlLoadException = XamlX.XamlLoadException; + class AvaloniaXamlIlSetterTransformer : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (!(node is XamlAstObjectNode on + && on.Type.GetClrType().FullName == "Avalonia.Styling.Setter")) + return node; + + var parent = context.ParentNodes().OfType() + .FirstOrDefault(p => p.Type.GetClrType().FullName == "Avalonia.Styling.Style"); + + if (parent == null) + throw new XamlParseException( + "Avalonia.Styling.Setter is only valid inside Avalonia.Styling.Style", node); + var selectorProperty = parent.Children.OfType() + .FirstOrDefault(p => p.Property.GetClrProperty().Name == "Selector"); + if (selectorProperty == null) + throw new XamlParseException( + "Can not find parent Style Selector", node); + var selector = selectorProperty.Values.FirstOrDefault() as XamlIlSelectorNode; + if (selector?.TargetType == null) + throw new XamlParseException( + "Can not resolve parent Style Selector type", node); + + IXamlType propType = null; + var property = @on.Children.OfType() + .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Property"); + if (property != null) + { + + var propertyName = property.Values.OfType().FirstOrDefault()?.Text; + if (propertyName == null) + throw new XamlParseException("Setter.Property must be a string", node); + + + var avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName, + new XamlAstClrTypeReference(selector, selector.TargetType, false), property.Values[0]); + property.Values = new List {avaloniaPropertyNode}; + propType = avaloniaPropertyNode.AvaloniaPropertyType; + } + else + { + var propertyPath = on.Children.OfType() + .FirstOrDefault(x => x.Property.GetClrProperty().Name == "PropertyPath"); + if (propertyPath == null) + throw new XamlX.XamlParseException("Setter without a property or property path is not valid", node); + if (propertyPath.Values[0] is IXamlIlPropertyPathNode ppn + && ppn.PropertyType != null) + propType = ppn.PropertyType; + else + throw new XamlX.XamlParseException("Unable to get the property path property type", node); + } + + var valueProperty = on.Children + .OfType().FirstOrDefault(p => p.Property.GetClrProperty().Name == "Value"); + if (valueProperty?.Values?.Count == 1 && valueProperty.Values[0] is XamlAstTextNode) + { + if (!XamlTransformHelpers.TryGetCorrectlyTypedValue(context, valueProperty.Values[0], + propType, out var converted)) + throw new XamlParseException( + $"Unable to convert property value to {propType.GetFqn()}", + valueProperty.Values[0]); + + valueProperty.Property = new SetterValueProperty(valueProperty.Property, + on.Type.GetClrType(), propType, context.GetAvaloniaTypes()); + } + + return node; + } + + class SetterValueProperty : XamlAstClrProperty + { + public SetterValueProperty(IXamlLineInfo line, IXamlType setterType, IXamlType targetType, + AvaloniaXamlIlWellKnownTypes types) + : base(line, "Value", setterType, null) + { + Getter = setterType.Methods.First(m => m.Name == "get_Value"); + var method = setterType.Methods.First(m => m.Name == "set_Value"); + Setters.Add(new XamlIlDirectCallPropertySetter(method, types.IBinding)); + Setters.Add(new XamlIlDirectCallPropertySetter(method, types.UnsetValueType)); + Setters.Add(new XamlIlDirectCallPropertySetter(method, targetType)); + } + + class XamlIlDirectCallPropertySetter : IXamlPropertySetter, IXamlEmitablePropertySetter + { + private readonly IXamlMethod _method; + private readonly IXamlType _type; + public IXamlType TargetType { get; } + public PropertySetterBinderParameters BinderParameters { get; } = new PropertySetterBinderParameters(); + public IReadOnlyList Parameters { get; } + public void Emit(IXamlILEmitter codegen) + { + if (_type.IsValueType) + codegen.Box(_type); + codegen.EmitCall(_method, true); + } + + public XamlIlDirectCallPropertySetter(IXamlMethod method, IXamlType type) + { + _method = method; + _type = type; + Parameters = new[] {type}; + TargetType = method.ThisOrFirstParameter(); + } + } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs similarity index 75% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs index 548f0161d66..d78ceeb918c 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs @@ -1,20 +1,22 @@ using System.Collections.Generic; using System.Linq; -using XamlIl; -using XamlIl.Ast; -using XamlIl.Transform; -using XamlIl.TypeSystem; +using XamlX; +using XamlX.Ast; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class AvaloniaXamlIlTransformInstanceAttachedProperties : IXamlIlAstTransformer + class AvaloniaXamlIlTransformInstanceAttachedProperties : IXamlAstTransformer { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (node is XamlIlAstNamePropertyReference prop - && prop.TargetType is XamlIlAstClrTypeReference targetRef - && prop.DeclaringType is XamlIlAstClrTypeReference declaringRef) + if (node is XamlAstNamePropertyReference prop + && prop.TargetType is XamlAstClrTypeReference targetRef + && prop.DeclaringType is XamlAstClrTypeReference declaringRef) { // Target and declared type aren't assignable but both inherit from AvaloniaObject var avaloniaObject = context.Configuration.TypeSystem.FindType("Avalonia.AvaloniaObject"); @@ -70,21 +72,21 @@ public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlA return node; } - class AvaloniaAttachedInstanceProperty : XamlIlAstClrProperty, IXamlIlAvaloniaProperty + class AvaloniaAttachedInstanceProperty : XamlAstClrProperty, IXamlIlAvaloniaProperty { - private readonly XamlIlTransformerConfiguration _config; - private readonly IXamlIlType _declaringType; - private readonly IXamlIlType _avaloniaPropertyType; - private readonly IXamlIlType _avaloniaObject; - private readonly IXamlIlField _field; - - public AvaloniaAttachedInstanceProperty(XamlIlAstNamePropertyReference prop, - XamlIlTransformerConfiguration config, - IXamlIlType declaringType, - IXamlIlType type, - IXamlIlType avaloniaPropertyType, - IXamlIlType avaloniaObject, - IXamlIlField field) : base(prop, prop.Name, + private readonly TransformerConfiguration _config; + private readonly IXamlType _declaringType; + private readonly IXamlType _avaloniaPropertyType; + private readonly IXamlType _avaloniaObject; + private readonly IXamlField _field; + + public AvaloniaAttachedInstanceProperty(XamlAstNamePropertyReference prop, + TransformerConfiguration config, + IXamlType declaringType, + IXamlType type, + IXamlType avaloniaPropertyType, + IXamlType avaloniaObject, + IXamlField field) : base(prop, prop.Name, declaringType, null) @@ -104,11 +106,11 @@ public AvaloniaAttachedInstanceProperty(XamlIlAstNamePropertyReference prop, Getter = new GetterMethod(this); } - public IXamlIlType PropertyType { get; } + public IXamlType PropertyType { get; } - public IXamlIlField AvaloniaProperty => _field; + public IXamlField AvaloniaProperty => _field; - class SetterMethod : IXamlIlPropertySetter + class SetterMethod : IXamlPropertySetter, IXamlEmitablePropertySetter { private readonly AvaloniaAttachedInstanceProperty _parent; @@ -118,10 +120,10 @@ public SetterMethod(AvaloniaAttachedInstanceProperty parent) Parameters = new[] {_parent._avaloniaObject, _parent.PropertyType}; } - public IXamlIlType TargetType => _parent.DeclaringType; + public IXamlType TargetType => _parent.DeclaringType; public PropertySetterBinderParameters BinderParameters { get; } = new PropertySetterBinderParameters(); - public IReadOnlyList Parameters { get; } - public void Emit(IXamlIlEmitter emitter) + public IReadOnlyList Parameters { get; } + public void Emit(IXamlILEmitter emitter) { var so = _parent._config.WellKnownTypes.Object; var method = _parent._avaloniaObject @@ -133,7 +135,7 @@ public void Emit(IXamlIlEmitter emitter) && m.Parameters[2].IsEnum ); if (method == null) - throw new XamlIlTypeSystemException( + throw new XamlTypeSystemException( "Unable to find SetValue(AvaloniaProperty, object, BindingPriority) on AvaloniaObject"); using (var loc = emitter.LocalsPool.GetLocal(_parent.PropertyType)) emitter @@ -150,7 +152,7 @@ public void Emit(IXamlIlEmitter emitter) } } - class GetterMethod : IXamlIlCustomEmitMethod + class GetterMethod : IXamlCustomEmitMethod { public GetterMethod(AvaloniaAttachedInstanceProperty parent) { @@ -163,16 +165,19 @@ public GetterMethod(AvaloniaAttachedInstanceProperty parent) public bool IsPublic => true; public bool IsStatic => true; public string Name { get; protected set; } - public IXamlIlType DeclaringType { get; } - public IXamlIlMethod MakeGenericMethod(IReadOnlyList typeArguments) + public IXamlType DeclaringType { get; } + public IXamlMethod MakeGenericMethod(IReadOnlyList typeArguments) => throw new System.NotSupportedException(); - public bool Equals(IXamlIlMethod other) => + public bool Equals(IXamlMethod other) => other is GetterMethod m && m.Name == Name && m.DeclaringType.Equals(DeclaringType); - public IXamlIlType ReturnType => Parent.PropertyType; - public IReadOnlyList Parameters { get; } - public void EmitCall(IXamlIlEmitter emitter) + public IXamlType ReturnType => Parent.PropertyType; + public IReadOnlyList Parameters { get; } + + public IReadOnlyList CustomAttributes => DeclaringType.CustomAttributes; + + public void EmitCall(IXamlILEmitter emitter) { var method = Parent._avaloniaObject .FindMethod(m => m.IsPublic && !m.IsStatic && m.Name == "GetValue" @@ -180,7 +185,7 @@ public void EmitCall(IXamlIlEmitter emitter) m.Parameters.Count == 1 && m.Parameters[0].Equals(Parent._avaloniaPropertyType)); if (method == null) - throw new XamlIlTypeSystemException( + throw new XamlTypeSystemException( "Unable to find T GetValue(AvaloniaProperty) on AvaloniaObject"); emitter .Ldsfld(Parent._field) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs new file mode 100644 index 00000000000..154c6a235c2 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs @@ -0,0 +1,58 @@ +using System; +using System.Collections.Generic; +using System.Text; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlTransformSyntheticCompiledBindingMembers : IXamlAstTransformer + { + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) + { + if (node is XamlAstNamePropertyReference prop + && prop.TargetType is XamlAstClrTypeReference targetRef + && targetRef.GetClrType().Equals(context.GetAvaloniaTypes().CompiledBindingExtension)) + { + if (prop.Name == "ElementName") + { + return new AvaloniaSyntheticCompiledBindingProperty(node, + SyntheticCompiledBindingPropertyName.ElementName); + } + else if (prop.Name == "RelativeSource") + { + return new AvaloniaSyntheticCompiledBindingProperty(node, + SyntheticCompiledBindingPropertyName.RelativeSource); + } + else if (prop.Name == "Source") + { + return new AvaloniaSyntheticCompiledBindingProperty(node, + SyntheticCompiledBindingPropertyName.Source); + } + } + + return node; + } + } + + enum SyntheticCompiledBindingPropertyName + { + ElementName, + RelativeSource, + Source + } + + class AvaloniaSyntheticCompiledBindingProperty : XamlAstNode, IXamlAstPropertyReference + { + public SyntheticCompiledBindingPropertyName Name { get; } + + public AvaloniaSyntheticCompiledBindingProperty( + IXamlLineInfo lineInfo, + SyntheticCompiledBindingPropertyName name) + : base(lineInfo) + { + Name = name; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransitionsTypeMetadataTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransitionsTypeMetadataTransformer.cs similarity index 74% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransitionsTypeMetadataTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransitionsTypeMetadataTransformer.cs index c4136f48249..4be2fc6f609 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransitionsTypeMetadataTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlTransitionsTypeMetadataTransformer.cs @@ -1,17 +1,17 @@ -using XamlIl.Ast; -using XamlIl.Transform; +using XamlX.Ast; +using XamlX.Transform; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class AvaloniaXamlIlTransitionsTypeMetadataTransformer : IXamlIlAstTransformer + class AvaloniaXamlIlTransitionsTypeMetadataTransformer : IXamlAstTransformer { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (node is XamlIlAstObjectNode on) + if (node is XamlAstObjectNode on) { foreach (var ch in on.Children) { - if (ch is XamlIlAstXamlPropertyValueNode pn + if (ch is XamlAstXamlPropertyValueNode pn && pn.Property.GetClrProperty().Getter?.ReturnType.Equals(context.GetAvaloniaTypes().Transitions) == true) { for (var c = 0; c < pn.Values.Count; c++) diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs new file mode 100644 index 00000000000..58ea11aa8fb --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -0,0 +1,124 @@ +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.TypeSystem; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers +{ + class AvaloniaXamlIlWellKnownTypes + { + public IXamlType AvaloniaObject { get; } + public IXamlType IAvaloniaObject { get; } + public IXamlType BindingPriority { get; } + public IXamlType AvaloniaObjectExtensions { get; } + public IXamlType AvaloniaProperty { get; } + public IXamlType AvaloniaPropertyT { get; } + public IXamlType IBinding { get; } + public IXamlMethod AvaloniaObjectBindMethod { get; } + public IXamlMethod AvaloniaObjectSetValueMethod { get; } + public IXamlType IDisposable { get; } + public XamlTypeWellKnownTypes XamlIlTypes { get; } + public XamlLanguageTypeMappings XamlIlMappings { get; } + public IXamlType Transitions { get; } + public IXamlType AssignBindingAttribute { get; } + public IXamlType UnsetValueType { get; } + public IXamlType StyledElement { get; } + public IXamlType NameScope { get; } + public IXamlMethod NameScopeSetNameScope { get; } + public IXamlType INameScope { get; } + public IXamlMethod INameScopeRegister { get; } + public IXamlMethod INameScopeComplete { get; } + public IXamlType IPropertyInfo { get; } + public IXamlType ClrPropertyInfo { get; } + public IXamlType PropertyPath { get; } + public IXamlType PropertyPathBuilder { get; } + public IXamlType IPropertyAccessor { get; } + public IXamlType PropertyInfoAccessorFactory { get; } + public IXamlType CompiledBindingPathBuilder { get; } + public IXamlType CompiledBindingPath { get; } + public IXamlType CompiledBindingExtension { get; } + + public IXamlType ResolveByNameExtension { get; } + + public IXamlType DataTemplate { get; } + public IXamlType IItemsPresenterHost { get; } + public IXamlType ReflectionBindingExtension { get; } + + public IXamlType RelativeSource { get; } + + public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg) + { + XamlIlTypes = cfg.WellKnownTypes; + AvaloniaObject = cfg.TypeSystem.GetType("Avalonia.AvaloniaObject"); + IAvaloniaObject = cfg.TypeSystem.GetType("Avalonia.IAvaloniaObject"); + AvaloniaObjectExtensions = cfg.TypeSystem.GetType("Avalonia.AvaloniaObjectExtensions"); + AvaloniaProperty = cfg.TypeSystem.GetType("Avalonia.AvaloniaProperty"); + AvaloniaPropertyT = cfg.TypeSystem.GetType("Avalonia.AvaloniaProperty`1"); + BindingPriority = cfg.TypeSystem.GetType("Avalonia.Data.BindingPriority"); + IBinding = cfg.TypeSystem.GetType("Avalonia.Data.IBinding"); + IDisposable = cfg.TypeSystem.GetType("System.IDisposable"); + Transitions = cfg.TypeSystem.GetType("Avalonia.Animation.Transitions"); + AssignBindingAttribute = cfg.TypeSystem.GetType("Avalonia.Data.AssignBindingAttribute"); + AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, IAvaloniaObject, + AvaloniaProperty, + IBinding, cfg.WellKnownTypes.Object); + UnsetValueType = cfg.TypeSystem.GetType("Avalonia.UnsetValueType"); + StyledElement = cfg.TypeSystem.GetType("Avalonia.StyledElement"); + INameScope = cfg.TypeSystem.GetType("Avalonia.Controls.INameScope"); + INameScopeRegister = INameScope.GetMethod( + new FindMethodMethodSignature("Register", XamlIlTypes.Void, + XamlIlTypes.String, XamlIlTypes.Object) + { + IsStatic = false, + DeclaringOnly = true, + IsExactMatch = true + }); + INameScopeComplete = INameScope.GetMethod( + new FindMethodMethodSignature("Complete", XamlIlTypes.Void) + { + IsStatic = false, + DeclaringOnly = true, + IsExactMatch = true + }); + NameScope = cfg.TypeSystem.GetType("Avalonia.Controls.NameScope"); + NameScopeSetNameScope = NameScope.GetMethod(new FindMethodMethodSignature("SetNameScope", + XamlIlTypes.Void, StyledElement, INameScope) + { IsStatic = true }); + AvaloniaObjectSetValueMethod = AvaloniaObject.FindMethod("SetValue", XamlIlTypes.Void, + false, AvaloniaProperty, XamlIlTypes.Object, BindingPriority); + IPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.IPropertyInfo"); + ClrPropertyInfo = cfg.TypeSystem.GetType("Avalonia.Data.Core.ClrPropertyInfo"); + PropertyPath = cfg.TypeSystem.GetType("Avalonia.Data.Core.PropertyPath"); + PropertyPathBuilder = cfg.TypeSystem.GetType("Avalonia.Data.Core.PropertyPathBuilder"); + IPropertyAccessor = cfg.TypeSystem.GetType("Avalonia.Data.Core.Plugins.IPropertyAccessor"); + PropertyInfoAccessorFactory = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.PropertyInfoAccessorFactory"); + CompiledBindingPathBuilder = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPathBuilder"); + CompiledBindingPath = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings.CompiledBindingPath"); + CompiledBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindingExtension"); + ResolveByNameExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ResolveByNameExtension"); + DataTemplate = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.Templates.DataTemplate"); + IItemsPresenterHost = cfg.TypeSystem.GetType("Avalonia.Controls.Presenters.IItemsPresenterHost"); + ReflectionBindingExtension = cfg.TypeSystem.GetType("Avalonia.Markup.Xaml.MarkupExtensions.ReflectionBindingExtension"); + RelativeSource = cfg.TypeSystem.GetType("Avalonia.Data.RelativeSource"); + } + } + + static class AvaloniaXamlIlWellKnownTypesExtensions + { + public static AvaloniaXamlIlWellKnownTypes GetAvaloniaTypes(this AstTransformationContext ctx) + { + if (ctx.TryGetItem(out var rv)) + return rv; + ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration)); + return rv; + } + + public static AvaloniaXamlIlWellKnownTypes GetAvaloniaTypes(this XamlEmitContext ctx) + { + if (ctx.TryGetItem(out var rv)) + return rv; + ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx.Configuration)); + return rv; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs similarity index 55% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs index a79a4977efe..1f2508715e0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs @@ -1,17 +1,17 @@ using System.Linq; -using XamlIl; -using XamlIl.Ast; -using XamlIl.Transform; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class IgnoredDirectivesTransformer : IXamlIlAstTransformer + class IgnoredDirectivesTransformer : IXamlAstTransformer { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (node is XamlIlAstObjectNode no) + if (node is XamlAstObjectNode no) { - foreach (var d in no.Children.OfType().ToList()) + foreach (var d in no.Children.OfType().ToList()) { if (d.Namespace == XamlNamespaces.Xaml2006) { diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/XNameTransformer.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XNameTransformer.cs similarity index 55% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/XNameTransformer.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XNameTransformer.cs index dd64ab542af..f4eb7cf5f92 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/XNameTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/Transformers/XNameTransformer.cs @@ -1,30 +1,29 @@ -using XamlIl; -using XamlIl.Ast; -using XamlIl.Transform; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers { - class XNameTransformer : IXamlIlAstTransformer + class XNameTransformer : IXamlAstTransformer { - /// /// Converts x:Name directives to regular Name assignments /// /// - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (node is XamlIlAstObjectNode on) + if (node is XamlAstObjectNode on) { for (var c =0; c< on.Children.Count;c++) { var ch = on.Children[c]; - if (ch is XamlIlAstXmlDirective d + if (ch is XamlAstXmlDirective d && d.Namespace == XamlNamespaces.Xaml2006 && d.Name == "Name") - on.Children[c] = new XamlIlAstXamlPropertyValueNode(d, - new XamlIlAstNamePropertyReference(d, on.Type, "Name", on.Type), + on.Children[c] = new XamlAstXamlPropertyValueNode(d, + new XamlAstNamePropertyReference(d, on.Type, "Name", on.Type), d.Values); } } diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs similarity index 57% rename from src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs rename to src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs index 6fc83cb5a5f..7f1b8caf0df 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs @@ -5,17 +5,36 @@ using Avalonia.Markup.Xaml.Parsers; using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; using Avalonia.Utilities; -using XamlIl; -using XamlIl.Ast; -using XamlIl.Transform; -using XamlIl.Transform.Transformers; -using XamlIl.TypeSystem; +using XamlX; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; +using XamlX.Emit; +using XamlX.IL; + +using XamlIlEmitContext = XamlX.Emit.XamlEmitContext; +using IXamlIlAstEmitableNode = XamlX.Emit.IXamlAstEmitableNode; namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { class XamlIlAvaloniaPropertyHelper { - public static bool Emit(XamlIlEmitContext context, IXamlIlEmitter emitter, XamlIlAstClrProperty property) + public static bool EmitProvideValueTarget(XamlIlEmitContext context, IXamlILEmitter emitter, + XamlAstClrProperty property) + { + if (Emit(context, emitter, property)) + return true; + var foundClr = property.DeclaringType.Properties.FirstOrDefault(p => p.Name == property.Name); + if (foundClr == null) + return false; + context + .Configuration.GetExtra() + .Emit(context, emitter, foundClr); + return true; + } + + public static bool Emit(XamlIlEmitContext context, IXamlILEmitter emitter, XamlAstClrProperty property) { if (property is IXamlIlAvaloniaProperty ap) { @@ -32,7 +51,7 @@ public static bool Emit(XamlIlEmitContext context, IXamlIlEmitter emitter, XamlI return true; } - public static bool Emit(XamlIlEmitContext context, IXamlIlEmitter emitter, IXamlIlProperty property) + public static bool Emit(XamlIlEmitContext context, IXamlILEmitter emitter, IXamlProperty property) { var type = (property.Getter ?? property.Setter).DeclaringType; var name = property.Name + "Property"; @@ -44,16 +63,16 @@ public static bool Emit(XamlIlEmitContext context, IXamlIlEmitter emitter, IXaml return true; } - public static IXamlIlAvaloniaPropertyNode CreateNode(XamlIlAstTransformationContext context, - string propertyName, IXamlIlAstTypeReference selectorTypeReference, IXamlIlLineInfo lineInfo) + public static IXamlIlAvaloniaPropertyNode CreateNode(AstTransformationContext context, + string propertyName, IXamlAstTypeReference selectorTypeReference, IXamlLineInfo lineInfo) { - XamlIlAstNamePropertyReference forgedReference; + XamlAstNamePropertyReference forgedReference; var parser = new PropertyParser(); var parsedPropertyName = parser.Parse(new CharacterReader(propertyName.AsSpan())); if(parsedPropertyName.owner == null) - forgedReference = new XamlIlAstNamePropertyReference(lineInfo, selectorTypeReference, + forgedReference = new XamlAstNamePropertyReference(lineInfo, selectorTypeReference, propertyName, selectorTypeReference); else { @@ -62,101 +81,107 @@ public static IXamlIlAvaloniaPropertyNode CreateNode(XamlIlAstTransformationCont xmlOwner += ":"; xmlOwner += parsedPropertyName.owner; - var tref = XamlIlTypeReferenceResolver.ResolveType(context, xmlOwner, false, lineInfo, true); + var tref = TypeReferenceResolver.ResolveType(context, xmlOwner, false, lineInfo, true); var propertyFieldName = parsedPropertyName.name + "Property"; var found = tref.Type.GetAllFields() .FirstOrDefault(f => f.IsStatic && f.IsPublic && f.Name == propertyFieldName); if (found == null) - throw new XamlIlParseException( + throw new XamlX.XamlParseException( $"Unable to find {propertyFieldName} field on type {tref.Type.GetFullName()}", lineInfo); return new XamlIlAvaloniaPropertyFieldNode(context.GetAvaloniaTypes(), lineInfo, found); } var clrProperty = - ((XamlIlAstClrProperty)new XamlIlPropertyReferenceResolver().Transform(context, + ((XamlAstClrProperty)new PropertyReferenceResolver().Transform(context, forgedReference)); return new XamlIlAvaloniaPropertyNode(lineInfo, context.Configuration.TypeSystem.GetType("Avalonia.AvaloniaProperty"), clrProperty); } + + public static IXamlType GetAvaloniaPropertyType(IXamlField field, + AvaloniaXamlIlWellKnownTypes types, IXamlLineInfo lineInfo) + { + var avaloniaPropertyType = field.FieldType; + while (avaloniaPropertyType != null) + { + if (avaloniaPropertyType.GenericTypeDefinition?.Equals(types.AvaloniaPropertyT) == true) + { + return avaloniaPropertyType.GenericArguments[0]; + } + + avaloniaPropertyType = avaloniaPropertyType.BaseType; + } + + throw new XamlX.XamlParseException( + $"{field.Name}'s type {field.FieldType} doesn't inherit from AvaloniaProperty, make sure to use typed properties", + lineInfo); + + } } - interface IXamlIlAvaloniaPropertyNode : IXamlIlAstValueNode + interface IXamlIlAvaloniaPropertyNode : IXamlAstValueNode { - IXamlIlType AvaloniaPropertyType { get; } + IXamlType AvaloniaPropertyType { get; } } - class XamlIlAvaloniaPropertyNode : XamlIlAstNode, IXamlIlAstValueNode, IXamlIlAstEmitableNode, IXamlIlAvaloniaPropertyNode + class XamlIlAvaloniaPropertyNode : XamlAstNode, IXamlAstValueNode, IXamlIlAstEmitableNode, IXamlIlAvaloniaPropertyNode { - public XamlIlAvaloniaPropertyNode(IXamlIlLineInfo lineInfo, IXamlIlType type, XamlIlAstClrProperty property) : base(lineInfo) + public XamlIlAvaloniaPropertyNode(IXamlLineInfo lineInfo, IXamlType type, XamlAstClrProperty property) : base(lineInfo) { - Type = new XamlIlAstClrTypeReference(this, type, false); + Type = new XamlAstClrTypeReference(this, type, false); Property = property; AvaloniaPropertyType = Property.Getter?.ReturnType ?? Property.Setters.First().Parameters[0]; } - public XamlIlAstClrProperty Property { get; } + public XamlAstClrProperty Property { get; } - public IXamlIlAstTypeReference Type { get; } - public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public IXamlAstTypeReference Type { get; } + public XamlILNodeEmitResult Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) { if (!XamlIlAvaloniaPropertyHelper.Emit(context, codeGen, Property)) - throw new XamlIlLoadException(Property.Name + " is not an AvaloniaProperty", this); - return XamlIlNodeEmitResult.Type(0, Type.GetClrType()); + throw new XamlX.XamlLoadException(Property.Name + " is not an AvaloniaProperty", this); + return XamlILNodeEmitResult.Type(0, Type.GetClrType()); } - public IXamlIlType AvaloniaPropertyType { get; } + public IXamlType AvaloniaPropertyType { get; } } - class XamlIlAvaloniaPropertyFieldNode : XamlIlAstNode, IXamlIlAstValueNode, IXamlIlAstEmitableNode, IXamlIlAvaloniaPropertyNode + class XamlIlAvaloniaPropertyFieldNode : XamlAstNode, IXamlAstValueNode, IXamlIlAstEmitableNode, IXamlIlAvaloniaPropertyNode { - private readonly IXamlIlField _field; + private readonly IXamlField _field; public XamlIlAvaloniaPropertyFieldNode(AvaloniaXamlIlWellKnownTypes types, - IXamlIlLineInfo lineInfo, IXamlIlField field) : base(lineInfo) + IXamlLineInfo lineInfo, IXamlField field) : base(lineInfo) { _field = field; - var avaloniaPropertyType = field.FieldType; - while (avaloniaPropertyType != null) - { - if (avaloniaPropertyType.GenericTypeDefinition?.Equals(types.AvaloniaPropertyT) == true) - { - AvaloniaPropertyType = avaloniaPropertyType.GenericArguments[0]; - return; - } - - avaloniaPropertyType = avaloniaPropertyType.BaseType; - } - - throw new XamlIlParseException( - $"{field.Name}'s type {field.FieldType} doesn't inherit from AvaloniaProperty, make sure to use typed properties", - lineInfo); - + AvaloniaPropertyType = XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(field, + types, lineInfo); } - public IXamlIlAstTypeReference Type => new XamlIlAstClrTypeReference(this, _field.FieldType, false); - public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + public IXamlAstTypeReference Type => new XamlAstClrTypeReference(this, _field.FieldType, false); + public XamlILNodeEmitResult Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) { codeGen.Ldsfld(_field); - return XamlIlNodeEmitResult.Type(0, _field.FieldType); + return XamlILNodeEmitResult.Type(0, _field.FieldType); } - public IXamlIlType AvaloniaPropertyType { get; } + public IXamlType AvaloniaPropertyType { get; } } interface IXamlIlAvaloniaProperty { - IXamlIlField AvaloniaProperty { get; } + IXamlField AvaloniaProperty { get; } } - class XamlIlAvaloniaProperty : XamlIlAstClrProperty, IXamlIlAvaloniaProperty + class XamlIlAvaloniaProperty : XamlAstClrProperty, IXamlIlAvaloniaProperty { - public IXamlIlField AvaloniaProperty { get; } - public XamlIlAvaloniaProperty(XamlIlAstClrProperty original, IXamlIlField field, + public IXamlField AvaloniaProperty { get; } + public XamlIlAvaloniaProperty(XamlAstClrProperty original, IXamlField field, AvaloniaXamlIlWellKnownTypes types) :base(original, original.Name, original.DeclaringType, original.Getter, original.Setters) { @@ -168,41 +193,41 @@ public XamlIlAvaloniaProperty(XamlIlAstClrProperty original, IXamlIlField field, Setters.Insert(0, new UnsetValueSetter(types, original.DeclaringType, field)); } - abstract class AvaloniaPropertyCustomSetter : IXamlIlPropertySetter + abstract class AvaloniaPropertyCustomSetter : IXamlPropertySetter, IXamlEmitablePropertySetter { protected AvaloniaXamlIlWellKnownTypes Types; - protected IXamlIlField AvaloniaProperty; + protected IXamlField AvaloniaProperty; public AvaloniaPropertyCustomSetter(AvaloniaXamlIlWellKnownTypes types, - IXamlIlType declaringType, - IXamlIlField avaloniaProperty) + IXamlType declaringType, + IXamlField avaloniaProperty) { Types = types; AvaloniaProperty = avaloniaProperty; TargetType = declaringType; } - public IXamlIlType TargetType { get; } + public IXamlType TargetType { get; } public PropertySetterBinderParameters BinderParameters { get; } = new PropertySetterBinderParameters { AllowXNull = false }; - public IReadOnlyList Parameters { get; set; } - public abstract void Emit(IXamlIlEmitter codegen); + public IReadOnlyList Parameters { get; set; } + public abstract void Emit(IXamlILEmitter codegen); } class BindingSetter : AvaloniaPropertyCustomSetter { public BindingSetter(AvaloniaXamlIlWellKnownTypes types, - IXamlIlType declaringType, - IXamlIlField avaloniaProperty) : base(types, declaringType, avaloniaProperty) + IXamlType declaringType, + IXamlField avaloniaProperty) : base(types, declaringType, avaloniaProperty) { Parameters = new[] {types.IBinding}; } - public override void Emit(IXamlIlEmitter emitter) + public override void Emit(IXamlILEmitter emitter) { using (var bloc = emitter.LocalsPool.GetLocal(Types.IBinding)) emitter @@ -217,13 +242,13 @@ public override void Emit(IXamlIlEmitter emitter) class UnsetValueSetter : AvaloniaPropertyCustomSetter { - public UnsetValueSetter(AvaloniaXamlIlWellKnownTypes types, IXamlIlType declaringType, IXamlIlField avaloniaProperty) + public UnsetValueSetter(AvaloniaXamlIlWellKnownTypes types, IXamlType declaringType, IXamlField avaloniaProperty) : base(types, declaringType, avaloniaProperty) { Parameters = new[] {types.UnsetValueType}; } - public override void Emit(IXamlIlEmitter codegen) + public override void Emit(IXamlILEmitter codegen) { var unsetValue = Types.AvaloniaProperty.Fields.First(f => f.Name == "UnsetValue"); codegen diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs new file mode 100644 index 00000000000..03ec32b9cfb --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -0,0 +1,693 @@ +using System; +using System.Collections.Generic; +using System.Text; +using System.Linq; +using System.Reflection.Emit; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.Transform.Transformers; +using XamlX.TypeSystem; +using XamlX; +using XamlX.Emit; +using XamlX.IL; +using Avalonia.Utilities; + +using XamlIlEmitContext = XamlX.Emit.XamlEmitContext; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions +{ + static class XamlIlBindingPathHelper + { + public static IXamlType UpdateCompiledBindingExtension(AstTransformationContext context, XamlAstConstructableObjectNode binding, IXamlType startType) + { + IXamlType bindingResultType = null; + if (binding.Arguments.Count > 0 && binding.Arguments[0] is ParsedBindingPathNode bindingPath) + { + var transformed = TransformBindingPath( + context, + bindingPath, + startType, + bindingPath.Path); + + bindingResultType = transformed.BindingResultType; + binding.Arguments[0] = transformed; + } + else + { + var bindingPathAssignment = binding.Children.OfType() + .FirstOrDefault(v => v.Property.Name == "Path"); + + if (bindingPathAssignment is null) + { + return startType; + } + + if (bindingPathAssignment.Values[0] is ParsedBindingPathNode bindingPathNode) + { + var transformed = TransformBindingPath( + context, + bindingPathNode, + startType, + bindingPathNode.Path); + + bindingResultType = transformed.BindingResultType; + bindingPathAssignment.Values[0] = transformed; + } + else + { + throw new InvalidOperationException(); + } + } + + return bindingResultType; + } + + private static IXamlIlBindingPathNode TransformBindingPath(AstTransformationContext context, IXamlLineInfo lineInfo, IXamlType startType, IEnumerable bindingExpression) + { + List transformNodes = new List(); + List nodes = new List(); + foreach (var astNode in bindingExpression) + { + var targetType = nodes.Count == 0 ? startType : nodes[nodes.Count - 1].Type; + switch (astNode) + { + case BindingExpressionGrammar.EmptyExpressionNode _: + break; + case BindingExpressionGrammar.NotNode _: + transformNodes.Add(new XamlIlNotPathElementNode(context.Configuration.WellKnownTypes.Boolean)); + break; + case BindingExpressionGrammar.StreamNode _: + IXamlType observableType; + if (targetType.GenericTypeDefinition?.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1")) == true) + { + observableType = targetType; + } + else + { + observableType = targetType.GetAllInterfaces().FirstOrDefault(i => i.GenericTypeDefinition?.Equals(context.Configuration.TypeSystem.FindType("System.IObservable`1")) ?? false); + } + + if (observableType != null) + { + nodes.Add(new XamlIlStreamObservablePathElementNode(observableType.GenericArguments[0])); + break; + } + bool foundTask = false; + for (var currentType = targetType; currentType != null; currentType = currentType.BaseType) + { + if (currentType.GenericTypeDefinition.Equals(context.Configuration.TypeSystem.GetType("System.Threading.Tasks.Task`1"))) + { + foundTask = true; + nodes.Add(new XamlIlStreamTaskPathElementNode(currentType.GenericArguments[0])); + break; + } + } + if (foundTask) + { + break; + } + throw new XamlX.XamlParseException($"Compiled bindings do not support stream bindings for objects of type {targetType.FullName}.", lineInfo); + case BindingExpressionGrammar.PropertyNameNode propName: + var avaloniaPropertyFieldNameMaybe = propName.PropertyName + "Property"; + var avaloniaPropertyFieldMaybe = targetType.GetAllFields().FirstOrDefault(f => + f.IsStatic && f.IsPublic && f.Name == avaloniaPropertyFieldNameMaybe); + + if (avaloniaPropertyFieldMaybe != null) + { + nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyFieldMaybe, + XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyFieldMaybe, context.GetAvaloniaTypes(), lineInfo))); + } + else + { + var clrProperty = GetAllDefinedProperties(targetType).FirstOrDefault(p => p.Name == propName.PropertyName); + + if (clrProperty is null) + { + throw new XamlX.XamlParseException($"Unable to resolve property of name '{propName.PropertyName}' on type '{targetType}'.", lineInfo); + } + nodes.Add(new XamlIlClrPropertyPathElementNode(clrProperty)); + } + break; + case BindingExpressionGrammar.IndexerNode indexer: + { + if (targetType.IsArray) + { + nodes.Add(new XamlIlArrayIndexerPathElementNode(targetType, indexer.Arguments, lineInfo)); + break; + } + + IXamlProperty property = null; + foreach (var currentType in TraverseTypeHierarchy(targetType)) + { + var defaultMemberAttribute = currentType.CustomAttributes.FirstOrDefault(x => x.Type.Namespace == "System.Reflection" && x.Type.Name == "DefaultMemberAttribute"); + if (defaultMemberAttribute != null) + { + property = currentType.GetAllProperties().FirstOrDefault(x => x.Name == (string)defaultMemberAttribute.Parameters[0]); + break; + } + } + if (property is null) + { + throw new XamlX.XamlParseException($"The type '${targetType}' does not have an indexer.", lineInfo); + } + + IEnumerable parameters = property.IndexerParameters; + + List values = new List(); + int currentParamIndex = 0; + foreach (var param in parameters) + { + var textNode = new XamlAstTextNode(lineInfo, indexer.Arguments[currentParamIndex], type: context.Configuration.WellKnownTypes.String); + if (!XamlTransformHelpers.TryGetCorrectlyTypedValue(context, textNode, + param, out var converted)) + throw new XamlX.XamlParseException( + $"Unable to convert indexer parameter value of '{indexer.Arguments[currentParamIndex]}' to {param.GetFqn()}", + textNode); + + values.Add(converted); + currentParamIndex++; + } + + bool isNotifyingCollection = targetType.GetAllInterfaces().Any(i => i.FullName == "System.Collections.Specialized.INotifyCollectionChanged"); + + nodes.Add(new XamlIlClrIndexerPathElementNode(property, values, string.Join(",", indexer.Arguments), isNotifyingCollection)); + break; + } + case BindingExpressionGrammar.AttachedPropertyNameNode attachedProp: + var avaloniaPropertyFieldName = attachedProp.PropertyName + "Property"; + var avaloniaPropertyField = GetType(attachedProp.Namespace, attachedProp.TypeName).GetAllFields().FirstOrDefault(f => + f.IsStatic && f.IsPublic && f.Name == avaloniaPropertyFieldName); + nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode(avaloniaPropertyField, + XamlIlAvaloniaPropertyHelper.GetAvaloniaPropertyType(avaloniaPropertyField, context.GetAvaloniaTypes(), lineInfo))); + break; + case BindingExpressionGrammar.SelfNode _: + nodes.Add(new SelfPathElementNode(targetType)); + break; + case VisualAncestorBindingExpressionNode visualAncestor: + nodes.Add(new FindVisualAncestorPathElementNode(visualAncestor.Type, visualAncestor.Level)); + break; + case TemplatedParentBindingExpressionNode templatedParent: + var templatedParentField = context.GetAvaloniaTypes().StyledElement.GetAllFields() + .FirstOrDefault(f => f.IsStatic && f.IsPublic && f.Name == "TemplatedParentProperty"); + nodes.Add(new XamlIlAvaloniaPropertyPropertyPathElementNode( + templatedParentField, + templatedParent.Type)); + break; + case BindingExpressionGrammar.AncestorNode ancestor: + if (ancestor.Namespace is null && ancestor.TypeName is null) + { + var styledElementType = context.GetAvaloniaTypes().StyledElement; + var ancestorType = context + .ParentNodes() + .OfType() + .Where(x => styledElementType.IsAssignableFrom(x.Type.GetClrType())) + .ElementAtOrDefault(ancestor.Level) + ?.Type.GetClrType(); + + if (ancestorType is null) + { + throw new XamlX.XamlParseException("Unable to resolve implicit ancestor type based on XAML tree.", lineInfo); + } + + nodes.Add(new FindAncestorPathElementNode(ancestorType, ancestor.Level)); + } + else + { + nodes.Add(new FindAncestorPathElementNode(GetType(ancestor.Namespace, ancestor.TypeName), ancestor.Level)); + } + break; + case BindingExpressionGrammar.NameNode elementName: + IXamlType elementType = null; + foreach (var deferredContent in context.ParentNodes().OfType()) + { + elementType = ScopeRegistrationFinder.GetTargetType(deferredContent, elementName.Name); + if (!(elementType is null)) + { + break; + } + } + if (elementType is null) + { + elementType = ScopeRegistrationFinder.GetTargetType(context.ParentNodes().Last(), elementName.Name); + } + + if (elementType is null) + { + throw new XamlX.XamlParseException($"Unable to find element '{elementName.Name}' in the current namescope. Unable to use a compiled binding with a name binding if the name cannot be found at compile time.", lineInfo); + } + nodes.Add(new ElementNamePathElementNode(elementName.Name, elementType)); + break; + case RawSourceBindingExpressionNode rawSource: + nodes.Add(new RawSourcePathElementNode(rawSource.RawSource)); + break; + } + } + + return new XamlIlBindingPathNode(lineInfo, context.GetAvaloniaTypes().CompiledBindingPath, transformNodes, nodes); + + IXamlType GetType(string ns, string name) + { + return TypeReferenceResolver.ResolveType(context, $"{ns}:{name}", false, + lineInfo, true).GetClrType(); + } + + static IEnumerable GetAllDefinedProperties(IXamlType type) + { + foreach (var t in TraverseTypeHierarchy(type)) + { + foreach (var p in t.Properties) + { + yield return p; + } + } + } + + static IEnumerable TraverseTypeHierarchy(IXamlType type) + { + if (type.IsInterface) + { + yield return type; + foreach (var iface in type.Interfaces) + { + foreach (var h in TraverseTypeHierarchy(iface)) + { + yield return h; + } + } + } + else + { + for (var currentType = type; currentType != null; currentType = currentType.BaseType) + { + yield return currentType; + } + } + } + } + + class ScopeRegistrationFinder : IXamlAstVisitor + { + private Stack _stack = new Stack(); + private Stack _childScopesStack = new Stack(); + + private ScopeRegistrationFinder(string name) + { + Name = name; + } + + string Name { get; } + + IXamlType TargetType { get; set; } + + public static IXamlType GetTargetType(IXamlAstNode namescopeRoot, string name) + { + var finder = new ScopeRegistrationFinder(name); + namescopeRoot.Visit(finder); + return finder.TargetType; + } + + void IXamlAstVisitor.Pop() + { + var node = _stack.Pop(); + if (_childScopesStack.Count > 0 && node == _childScopesStack.Peek()) + { + _childScopesStack.Pop(); + } + } + + void IXamlAstVisitor.Push(IXamlAstNode node) + { + _stack.Push(node); + if (node is NestedScopeMetadataNode) + { + _childScopesStack.Push(node); + } + } + + IXamlAstNode IXamlAstVisitor.Visit(IXamlAstNode node) + { + if (_childScopesStack.Count == 0 && node is AvaloniaNameScopeRegistrationXamlIlNode registration) + { + if (registration.Name is XamlAstTextNode text && text.Text == Name) + { + TargetType = registration.TargetType; + } + } + return node; + } + } + + interface IXamlIlBindingPathElementNode + { + IXamlType Type { get; } + + void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen); + } + + class XamlIlNotPathElementNode : IXamlIlBindingPathElementNode + { + public XamlIlNotPathElementNode(IXamlType boolType) + { + Type = boolType; + } + + public IXamlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "Not")); + } + } + + class XamlIlStreamObservablePathElementNode : IXamlIlBindingPathElementNode + { + public XamlIlStreamObservablePathElementNode(IXamlType type) + { + Type = type; + } + + public IXamlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "StreamObservable").MakeGenericMethod(new[] { Type })); + } + } + + class XamlIlStreamTaskPathElementNode : IXamlIlBindingPathElementNode + { + public XamlIlStreamTaskPathElementNode(IXamlType type) + { + Type = type; + } + + public IXamlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "StreamTask").MakeGenericMethod(new[] { Type })); + } + } + + class SelfPathElementNode : IXamlIlBindingPathElementNode + { + public SelfPathElementNode(IXamlType type) + { + Type = type; + } + + public IXamlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "Self")); + } + } + + class FindAncestorPathElementNode : IXamlIlBindingPathElementNode + { + private readonly int _level; + + public FindAncestorPathElementNode(IXamlType ancestorType, int level) + { + Type = ancestorType; + _level = level; + } + + public IXamlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.Ldtype(Type) + .Ldc_I4(_level) + .EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "FindAncestor")); + } + } + + class FindVisualAncestorPathElementNode : IXamlIlBindingPathElementNode + { + private readonly int _level; + + public FindVisualAncestorPathElementNode(IXamlType ancestorType, int level) + { + Type = ancestorType; + _level = level; + } + + public IXamlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.Ldtype(Type) + .Ldc_I4(_level) + .EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "VisualAncestor")); + } + } + + class ElementNamePathElementNode : IXamlIlBindingPathElementNode + { + private readonly string _name; + + public ElementNamePathElementNode(string name, IXamlType elementType) + { + _name = name; + Type = elementType; + } + + public IXamlType Type { get; } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + var scopeField = context.RuntimeContext.ContextType.Fields.First(f => + f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName); + + codeGen + .Ldloc(context.ContextLocal) + .Ldfld(scopeField) + .Ldstr(_name) + .EmitCall(context.GetAvaloniaTypes().CompiledBindingPathBuilder.FindMethod(m => m.Name == "ElementName")); + } + } + + class XamlIlAvaloniaPropertyPropertyPathElementNode : IXamlIlBindingPathElementNode + { + private readonly IXamlField _field; + + public XamlIlAvaloniaPropertyPropertyPathElementNode(IXamlField field, IXamlType propertyType) + { + _field = field; + Type = propertyType; + } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.Ldsfld(_field); + context.Configuration.GetExtra() + .EmitLoadAvaloniaPropertyAccessorFactory(context, codeGen); + codeGen.EmitCall(context.GetAvaloniaTypes() + .CompiledBindingPathBuilder.FindMethod(m => m.Name == "Property")); + } + + public IXamlType Type { get; } + } + + class XamlIlClrPropertyPathElementNode : IXamlIlBindingPathElementNode + { + private readonly IXamlProperty _property; + + public XamlIlClrPropertyPathElementNode(IXamlProperty property) + { + _property = property; + } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + context.Configuration.GetExtra() + .Emit(context, codeGen, _property); + + context.Configuration.GetExtra() + .EmitLoadInpcPropertyAccessorFactory(context, codeGen); + + codeGen + .EmitCall(context.GetAvaloniaTypes() + .CompiledBindingPathBuilder.FindMethod(m => m.Name == "Property")); + } + + public IXamlType Type => _property.Getter?.ReturnType ?? _property.Setter?.Parameters[0]; + } + + class XamlIlClrIndexerPathElementNode : IXamlIlBindingPathElementNode + { + private readonly IXamlProperty _property; + private readonly List _values; + private readonly string _indexerKey; + private readonly bool _isNotifyingCollection; + + public XamlIlClrIndexerPathElementNode(IXamlProperty property, List values, string indexerKey, bool isNotifyingCollection) + { + _property = property; + _values = values; + _indexerKey = indexerKey; + _isNotifyingCollection = isNotifyingCollection; + } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + var intType = context.Configuration.TypeSystem.GetType("System.Int32"); + context.Configuration.GetExtra() + .Emit(context, codeGen, _property, _values, _indexerKey); + + if (_isNotifyingCollection + && + _values.Count == 1 + && _values[0].Type.GetClrType().Equals(intType)) + { + context.Configuration.GetExtra() + .EmitLoadIndexerAccessorFactory(context, codeGen, _values[0]); + } + else + { + context.Configuration.GetExtra() + .EmitLoadInpcPropertyAccessorFactory(context, codeGen); + } + + codeGen.EmitCall(context.GetAvaloniaTypes() + .CompiledBindingPathBuilder.FindMethod(m => m.Name == "Property")); + } + + public IXamlType Type => _property.Getter?.ReturnType ?? _property.Setter?.Parameters[0]; + } + + class XamlIlArrayIndexerPathElementNode : IXamlIlBindingPathElementNode + { + private readonly IXamlType _arrayType; + private readonly List _values; + + public XamlIlArrayIndexerPathElementNode(IXamlType arrayType, IList values, IXamlLineInfo lineInfo) + { + _arrayType = arrayType; + _values = new List(values.Count); + foreach (var item in values) + { + if (!int.TryParse(item, out var index)) + { + throw new XamlX.XamlParseException($"Unable to convert '{item}' to an integer.", lineInfo.Line, lineInfo.Position); + } + _values.Add(index); + } + } + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + var intType = context.Configuration.TypeSystem.GetType("System.Int32"); + var indices = codeGen.DefineLocal(intType.MakeArrayType(1)); + codeGen.Ldc_I4(_values.Count) + .Newarr(intType) + .Stloc(indices); + for (int i = 0; i < _values.Count; i++) + { + codeGen.Ldloc(indices) + .Ldc_I4(i) + .Ldc_I4(_values[i]) + .Emit(OpCodes.Stelem_I4); + } + + codeGen.Ldloc(indices) + .Ldtype(Type) + .EmitCall(context.GetAvaloniaTypes() + .CompiledBindingPathBuilder.FindMethod(m => m.Name == "ArrayElement")); + } + + public IXamlType Type => _arrayType.ArrayElementType; + } + + class RawSourcePathElementNode : XamlAstNode, IXamlIlBindingPathElementNode + { + private readonly IXamlAstValueNode _rawSource; + + public RawSourcePathElementNode(IXamlAstValueNode rawSource) + :base(rawSource) + { + _rawSource = rawSource; + + } + + public IXamlType Type => _rawSource.Type.GetClrType(); + + public void Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + context.Emit(_rawSource, codeGen, Type); + codeGen + .EmitCall(context.GetAvaloniaTypes() + .CompiledBindingPathBuilder.FindMethod(m => m.Name == "SetRawSource")); + } + } + + class XamlIlBindingPathNode : XamlAstNode, IXamlIlBindingPathNode, IXamlAstEmitableNode + { + private readonly List _transformElements; + private readonly List _elements; + + public XamlIlBindingPathNode(IXamlLineInfo lineInfo, + IXamlType bindingPathType, + List transformElements, + List elements) : base(lineInfo) + { + Type = new XamlAstClrTypeReference(lineInfo, bindingPathType, false); + _transformElements = transformElements; + _elements = elements; + } + + public IXamlType BindingResultType + => _transformElements.Count > 0 + ? _transformElements[0].Type + : _elements[_elements.Count - 1].Type; + + public IXamlAstTypeReference Type { get; } + + public XamlILNodeEmitResult Emit(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + var types = context.GetAvaloniaTypes(); + codeGen.Newobj(types.CompiledBindingPathBuilder.FindConstructor()); + + foreach (var transform in _transformElements) + { + transform.Emit(context, codeGen); + } + + foreach (var element in _elements) + { + element.Emit(context, codeGen); + } + + codeGen.EmitCall(types.CompiledBindingPathBuilder.FindMethod(m => m.Name == "Build")); + return XamlILNodeEmitResult.Type(0, types.CompiledBindingPath); + } + + public override void VisitChildren(IXamlAstVisitor visitor) + { + for (int i = 0; i < _transformElements.Count; i++) + { + if (_transformElements[i] is IXamlAstNode ast) + { + _transformElements[i] = (IXamlIlBindingPathElementNode)ast.Visit(visitor); + } + } + for (int i = 0; i < _elements.Count; i++) + { + if (_elements[i] is IXamlAstNode ast) + { + _elements[i] = (IXamlIlBindingPathElementNode)ast.Visit(visitor); + } + } + } + } + } + + interface IXamlIlBindingPathNode : IXamlAstValueNode + { + IXamlType BindingResultType { get; } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs new file mode 100644 index 00000000000..871a2a20458 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs @@ -0,0 +1,158 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; +using XamlX.IL; +using XamlX.Emit; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions +{ + class XamlIlClrPropertyInfoEmitter + { + private readonly IXamlTypeBuilder _builder; + + private Dictionary> _fields + = new Dictionary>(); + + public XamlIlClrPropertyInfoEmitter(IXamlTypeBuilder builder) + { + _builder = builder; + } + + static string GetKey(IXamlProperty property, string indexerArgumentsKey) + { + var baseKey = property.Getter.DeclaringType.GetFullName() + "." + property.Name; + + if (indexerArgumentsKey is null) + { + return baseKey; + } + + return baseKey + $"[{indexerArgumentsKey}]"; + } + + public IXamlType Emit(XamlEmitContext context, IXamlILEmitter codeGen, IXamlProperty property, IEnumerable indexerArguments = null, string indexerArgumentsKey = null) + { + indexerArguments = indexerArguments ?? Enumerable.Empty(); + var types = context.GetAvaloniaTypes(); + IXamlMethod Get() + { + var key = GetKey(property, indexerArgumentsKey); + if (!_fields.TryGetValue(key, out var lst)) + _fields[key] = lst = new List<(IXamlProperty prop, IXamlMethod get)>(); + + foreach (var cached in lst) + { + if ( + ((cached.prop.Getter == null && property.Getter == null) || + cached.prop.Getter?.Equals(property.Getter) == true) && + ((cached.prop.Setter == null && property.Setter == null) || + cached.prop.Setter?.Equals(property.Setter) == true) + ) + return cached.get; + } + + var name = lst.Count == 0 ? key : key + "_" + Guid.NewGuid().ToString("N"); + + var field = _builder.DefineField(types.IPropertyInfo, name + "!Field", false, true); + + void Load(IXamlMethod m, IXamlILEmitter cg) + { + cg + .Ldarg_0(); + if (m.DeclaringType.IsValueType) + cg.Unbox(m.DeclaringType); + else + cg.Castclass(m.DeclaringType); + + foreach (var indexerArg in indexerArguments) + { + context.Emit(indexerArg, cg, indexerArg.Type.GetClrType()); + } + } + + var getter = property.Getter == null ? + null : + _builder.DefineMethod(types.XamlIlTypes.Object, + new[] {types.XamlIlTypes.Object}, name + "!Getter", false, true, false); + if (getter != null) + { + Load(property.Getter, getter.Generator); + + getter.Generator.EmitCall(property.Getter); + if (property.Getter.ReturnType.IsValueType) + getter.Generator.Box(property.Getter.ReturnType); + getter.Generator.Ret(); + } + + var setter = property.Setter == null ? + null : + _builder.DefineMethod(types.XamlIlTypes.Void, + new[] {types.XamlIlTypes.Object, types.XamlIlTypes.Object}, + name + "!Setter", false, true, false); + if (setter != null) + { + Load(property.Setter, setter.Generator); + + setter.Generator.Ldarg(1); + if (property.Setter.Parameters[0].IsValueType) + setter.Generator.Unbox_Any(property.Setter.Parameters[0]); + else + setter.Generator.Castclass(property.Setter.Parameters[0]); + setter.Generator + .EmitCall(property.Setter, true) + .Ret(); + } + + var get = _builder.DefineMethod(types.IPropertyInfo, Array.Empty(), + name + "!Property", true, true, false); + + + var ctor = types.ClrPropertyInfo.Constructors.First(c => + c.Parameters.Count == 4 && c.IsStatic == false); + + var cacheMiss = get.Generator.DefineLabel(); + get.Generator + .Ldsfld(field) + .Brfalse(cacheMiss) + .Ldsfld(field) + .Ret() + .MarkLabel(cacheMiss) + .Ldstr(property.Name); + + void EmitFunc(IXamlILEmitter emitter, IXamlMethod method, IXamlType del) + { + if (method == null) + emitter.Ldnull(); + else + { + emitter + .Ldnull() + .Ldftn(method) + .Newobj(del.Constructors.First(c => + c.Parameters.Count == 2 && + c.Parameters[0].Equals(context.Configuration.WellKnownTypes.Object))); + } + } + + EmitFunc(get.Generator, getter, ctor.Parameters[1]); + EmitFunc(get.Generator, setter, ctor.Parameters[2]); + get.Generator + .Ldtype(property.PropertyType) + .Newobj(ctor) + .Stsfld(field) + .Ldsfld(field) + .Ret(); + + lst.Add((property, get)); + return get; + } + + codeGen.EmitCall(Get()); + return types.IPropertyInfo; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlPropertyInfoAccessorFactoryEmitter.cs b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlPropertyInfoAccessorFactoryEmitter.cs new file mode 100644 index 00000000000..d24a43a84fc --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/CompilerExtensions/XamlIlPropertyInfoAccessorFactoryEmitter.cs @@ -0,0 +1,118 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using XamlX.Ast; +using XamlX.Transform; +using XamlX.TypeSystem; +using XamlX.Emit; +using XamlX.IL; + +using XamlIlEmitContext = XamlX.Emit.XamlEmitContext; + +namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions +{ + class XamlIlPropertyInfoAccessorFactoryEmitter + { + private const string IndexerClosureFactoryMethodName = "CreateAccessor"; + private readonly IXamlTypeBuilder _indexerClosureTypeBuilder; + private IXamlType _indexerClosureType; + public XamlIlPropertyInfoAccessorFactoryEmitter(IXamlTypeBuilder indexerClosureType) + { + _indexerClosureTypeBuilder = indexerClosureType; + } + + public IXamlType EmitLoadInpcPropertyAccessorFactory(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.Ldnull(); + EmitLoadPropertyAccessorFactory(context, codeGen, context.GetAvaloniaTypes().PropertyInfoAccessorFactory, "CreateInpcPropertyAccessor"); + return EmitCreateAccessorFactoryDelegate(context, codeGen); + } + + public IXamlType EmitLoadAvaloniaPropertyAccessorFactory(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + codeGen.Ldnull(); + EmitLoadPropertyAccessorFactory(context, codeGen, context.GetAvaloniaTypes().PropertyInfoAccessorFactory, "CreateAvaloniaPropertyAccessor"); + return EmitCreateAccessorFactoryDelegate(context, codeGen); + } + + private void EmitLoadPropertyAccessorFactory(XamlIlEmitContext context, IXamlILEmitter codeGen, IXamlType type, string accessorFactoryName, bool isStatic = true) + { + var types = context.GetAvaloniaTypes(); + var weakReferenceType = context.Configuration.TypeSystem.GetType("System.WeakReference`1").MakeGenericType(context.Configuration.WellKnownTypes.Object); + FindMethodMethodSignature accessorFactorySignature = new FindMethodMethodSignature(accessorFactoryName, types.IPropertyAccessor, weakReferenceType, types.IPropertyInfo) + { + IsStatic = isStatic + }; + codeGen.Ldftn(type.GetMethod(accessorFactorySignature)); + } + + public IXamlType EmitLoadIndexerAccessorFactory(XamlIlEmitContext context, IXamlILEmitter codeGen, IXamlAstValueNode value) + { + var intType = context.Configuration.TypeSystem.GetType("System.Int32"); + if (_indexerClosureType is null) + { + _indexerClosureType = InitializeClosureType(context); + } + + context.Emit(value, codeGen, intType); + codeGen.Newobj(_indexerClosureType.FindConstructor(new List { intType })); + EmitLoadPropertyAccessorFactory(context, codeGen, _indexerClosureType, IndexerClosureFactoryMethodName, isStatic: false); + return EmitCreateAccessorFactoryDelegate(context, codeGen); + } + + private IXamlType InitializeClosureType(XamlIlEmitContext context) + { + var types = context.GetAvaloniaTypes(); + var intType = context.Configuration.TypeSystem.GetType("System.Int32"); + var weakReferenceType = context.Configuration.TypeSystem.GetType("System.WeakReference`1").MakeGenericType(context.Configuration.WellKnownTypes.Object); + var indexAccessorFactoryMethod = context.GetAvaloniaTypes().PropertyInfoAccessorFactory.GetMethod( + new FindMethodMethodSignature( + "CreateIndexerPropertyAccessor", + types.IPropertyAccessor, + weakReferenceType, + types.IPropertyInfo, + intType) + { + IsStatic = true + }); + var indexField = _indexerClosureTypeBuilder.DefineField(intType, "_index", false, false); + var ctor = _indexerClosureTypeBuilder.DefineConstructor(false, intType); + ctor.Generator + .Ldarg_0() + .Ldarg(1) + .Stfld(indexField) + .Ret(); + _indexerClosureTypeBuilder.DefineMethod( + types.IPropertyAccessor, + new[] { weakReferenceType, types.IPropertyInfo }, + IndexerClosureFactoryMethodName, + isPublic: true, + isStatic: false, + isInterfaceImpl: false) + .Generator + .Ldarg(1) + .Ldarg(2) + .LdThisFld(indexField) + .EmitCall(indexAccessorFactoryMethod) + .Ret(); + + return _indexerClosureTypeBuilder.CreateType(); + } + + private IXamlType EmitCreateAccessorFactoryDelegate(XamlIlEmitContext context, IXamlILEmitter codeGen) + { + var types = context.GetAvaloniaTypes(); + var weakReferenceType = context.Configuration.TypeSystem.GetType("System.WeakReference`1").MakeGenericType(context.Configuration.WellKnownTypes.Object); + var funcType = context.Configuration.TypeSystem.GetType("System.Func`3").MakeGenericType( + weakReferenceType, + types.IPropertyInfo, + types.IPropertyAccessor); + codeGen.Newobj(funcType.Constructors.First(c => + c.Parameters.Count == 2 && + c.Parameters[0].Equals(context.Configuration.WellKnownTypes.Object))); + return funcType; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/IncludeXamlIlSre.props b/src/Markup/Avalonia.Markup.Xaml.Loader/IncludeXamlIlSre.props new file mode 100644 index 00000000000..c902fa956a7 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/IncludeXamlIlSre.props @@ -0,0 +1,12 @@ + + + + + + + + + + + + diff --git a/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github new file mode 160000 index 00000000000..7b8b3013bd4 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml.Loader/xamlil.github @@ -0,0 +1 @@ +Subproject commit 7b8b3013bd42e1992838a525c991f44191da55be diff --git a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 985a6b9f5a2..24428253aa0 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj +++ b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj @@ -1,7 +1,7 @@  netstandard2.0 - PCL;NETSTANDARD;NETSTANDARD2_0;HAS_TYPE_CONVERTER;HAS_CUSTOM_ATTRIBUTE_PROVIDER;XAMLIL_INTERNAL + PCL;NETSTANDARD;NETSTANDARD2_0;HAS_TYPE_CONVERTER;HAS_CUSTOM_ATTRIBUTE_PROVIDER;XAMLX_INTERNAL false $(DefineConstants);RUNTIME_XAML_CECIL False @@ -16,7 +16,16 @@ + + + + + + + + + @@ -25,7 +34,7 @@ - + @@ -36,32 +45,11 @@ - - - - - - - - - - - - - - - - - - - - - - + @@ -74,8 +62,6 @@ - - diff --git a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs index 5c21037924c..e5c6b72d124 100644 --- a/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs +++ b/src/Markup/Avalonia.Markup.Xaml/AvaloniaXamlLoader.cs @@ -10,10 +10,13 @@ namespace Avalonia.Markup.Xaml /// /// Loads XAML for a avalonia application. /// - public class AvaloniaXamlLoader + public static class AvaloniaXamlLoader { - public bool IsDesignMode { get; set; } - + public interface IRuntimeXamlLoader + { + object Load(Stream stream, Assembly localAsm, object o, Uri baseUri, bool designMode); + } + /// /// Loads the XAML into a Avalonia component. /// @@ -32,7 +35,7 @@ public static void Load(object obj) /// A base URI to use if is relative. /// /// The loaded object. - public object Load(Uri uri, Uri baseUri = null) + public static object Load(Uri uri, Uri baseUri = null) { Contract.Requires(uri != null); @@ -55,52 +58,22 @@ public object Load(Uri uri, Uri baseUri = null) if (compiledResult != null) return compiledResult; } - - - var asset = assetLocator.OpenAndGetAssembly(uri, baseUri); - using (var stream = asset.stream) - { - var absoluteUri = uri.IsAbsoluteUri ? uri : new Uri(baseUri, uri); - return Load(stream, asset.assembly, null, absoluteUri); - } - } - - /// - /// Loads XAML from a string. - /// - /// The string containing the XAML. - /// Default assembly for clr-namespace: - /// - /// The optional instance into which the XAML should be loaded. - /// - /// The loaded object. - public object Load(string xaml, Assembly localAssembly = null, object rootInstance = null) - { - Contract.Requires(xaml != null); - using (var stream = new MemoryStream(Encoding.UTF8.GetBytes(xaml))) + // This is intended for unit-tests only + var runtimeLoader = AvaloniaLocator.Current.GetService(); + if (runtimeLoader != null) { - return Load(stream, localAssembly, rootInstance); + var asset = assetLocator.OpenAndGetAssembly(uri, baseUri); + using (var stream = asset.stream) + { + var absoluteUri = uri.IsAbsoluteUri ? uri : new Uri(baseUri, uri); + return runtimeLoader.Load(stream, asset.assembly, null, absoluteUri, false); + } } - } - /// - /// Loads XAML from a stream. - /// - /// The stream containing the XAML. - /// Default assembly for clr-namespace - /// - /// The optional instance into which the XAML should be loaded. - /// - /// The URI of the XAML - /// The loaded object. - public object Load(Stream stream, Assembly localAssembly, object rootInstance = null, Uri uri = null) - => AvaloniaXamlIlRuntimeCompiler.Load(stream, localAssembly, rootInstance, uri, IsDesignMode); - - public static object Parse(string xaml, Assembly localAssembly = null) - => new AvaloniaXamlLoader().Load(xaml, localAssembly); - - public static T Parse(string xaml, Assembly localAssembly = null) - => (T)Parse(xaml, localAssembly); + throw new XamlLoadException( + $"No precompiled XAML found for {uri} (baseUri: {baseUri}), make sure to specify x:Class and include your XAML file as AvaloniaResource"); + } + } } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs new file mode 100644 index 00000000000..aab733cb432 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindingExtension.cs @@ -0,0 +1,79 @@ +using System; +using Avalonia.Data; +using Avalonia.Controls; +using Avalonia.Styling; +using Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings; +using Avalonia.Data.Core; +using Avalonia.Markup.Parsers; + +namespace Avalonia.Markup.Xaml.MarkupExtensions +{ + public class CompiledBindingExtension : BindingBase + { + public CompiledBindingExtension() + { + Path = new CompiledBindingPath(); + } + + public CompiledBindingExtension(CompiledBindingPath path) + { + Path = path; + } + + public CompiledBindingExtension ProvideValue(IServiceProvider provider) + { + return new CompiledBindingExtension + { + Path = Path, + Converter = Converter, + FallbackValue = FallbackValue, + Mode = Mode, + Priority = Priority, + StringFormat = StringFormat, + DefaultAnchor = new WeakReference(GetDefaultAnchor(provider)) + }; + } + + private static object GetDefaultAnchor(IServiceProvider provider) + { + // If the target is not a control, so we need to find an anchor that will let us look + // up named controls and style resources. First look for the closest IControl in + // the context. + object anchor = provider.GetFirstParent(); + + // If a control was not found, then try to find the highest-level style as the XAML + // file could be a XAML file containing only styles. + return anchor ?? + provider.GetService()?.RootObject as IStyle ?? + provider.GetLastParent(); + } + + protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) + { + if (Path.RawSource != null) + { + return CreateSourceObserver( + Path.RawSource, + Path.BuildExpression(enableDataValidation)); + } + + if (Path.SourceMode == SourceMode.Data) + { + return CreateDataContextObserver( + target, + Path.BuildExpression(enableDataValidation), + targetProperty == StyledElement.DataContextProperty, + anchor); + } + else + { + return CreateSourceObserver( + (target as IStyledElement) ?? (anchor as IStyledElement), + Path.BuildExpression(enableDataValidation)); + } + } + + [ConstructorArgument("path")] + public CompiledBindingPath Path { get; set; } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ArrayElementPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ArrayElementPlugin.cs new file mode 100644 index 00000000000..7cbe4942e59 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ArrayElementPlugin.cs @@ -0,0 +1,71 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Avalonia.Data; +using Avalonia.Data.Core.Plugins; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + class ArrayElementPlugin : IPropertyAccessorPlugin + { + private readonly int[] _indices; + private readonly Type _elementType; + + public ArrayElementPlugin(int[] indices, Type elementType) + { + _indices = indices; + _elementType = elementType; + } + + public bool Match(object obj, string propertyName) + { + throw new InvalidOperationException("The ArrayElementPlugin does not support dynamic matching"); + } + + public IPropertyAccessor Start(WeakReference reference, string propertyName) + { + if (reference.TryGetTarget(out var target) && target is Array arr) + { + return new Accessor(new WeakReference(arr), _indices, _elementType); + } + return null; + } + + class Accessor : PropertyAccessorBase + { + private readonly int[] _indices; + private readonly WeakReference _reference; + + public Accessor(WeakReference reference, int[] indices, Type elementType) + { + _reference = reference; + _indices = indices; + PropertyType = elementType; + } + + public override Type PropertyType { get; } + + public override object Value => _reference.TryGetTarget(out var arr) ? arr.GetValue(_indices) : null; + + public override bool SetValue(object value, BindingPriority priority) + { + if (_reference.TryGetTarget(out var arr)) + { + arr.SetValue(value, _indices); + return true; + } + return false; + } + + protected override void SubscribeCore() + { + PublishValue(Value); + } + + protected override void UnsubscribeCore() + { + } + } + } + +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs new file mode 100644 index 00000000000..d627fe3cd32 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/CompiledBindingPath.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using Avalonia.Controls; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Parsers.Nodes; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + public class CompiledBindingPath + { + private readonly List _elements = new List(); + + public CompiledBindingPath() { } + + internal CompiledBindingPath(IEnumerable bindingPath, object rawSource) + { + _elements = new List(bindingPath); + RawSource = rawSource; + } + + public ExpressionNode BuildExpression(bool enableValidation) + { + ExpressionNode pathRoot = null; + ExpressionNode path = null; + foreach (var element in _elements) + { + ExpressionNode node = null; + switch (element) + { + case NotExpressionPathElement _: + node = new LogicalNotNode(); + break; + case PropertyElement prop: + node = new PropertyAccessorNode(prop.Property.Name, enableValidation, new PropertyInfoAccessorPlugin(prop.Property, prop.AccessorFactory)); + break; + case ArrayElementPathElement arr: + node = new PropertyAccessorNode(CommonPropertyNames.IndexerName, enableValidation, new ArrayElementPlugin(arr.Indices, arr.ElementType)); + break; + case VisualAncestorPathElement visualAncestor: + node = new FindVisualAncestorNode(visualAncestor.AncestorType, visualAncestor.Level); + break; + case AncestorPathElement ancestor: + node = new FindAncestorNode(ancestor.AncestorType, ancestor.Level); + break; + case SelfPathElement _: + node = new SelfNode(); + break; + case ElementNameElement name: + node = new ElementNameNode(name.NameScope, name.Name); + break; + case IStronglyTypedStreamElement stream: + node = new StreamNode(stream.CreatePlugin()); + break; + default: + throw new InvalidOperationException($"Unknown binding path element type {element.GetType().FullName}"); + } + + path = pathRoot is null ? (pathRoot = node) : path.Next = node; + } + + return pathRoot ?? new EmptyExpressionNode(); + } + + internal SourceMode SourceMode => _elements.Count > 0 && _elements[0] is IControlSourceBindingPathElement ? SourceMode.Control : SourceMode.Data; + + internal object RawSource { get; } + } + + public class CompiledBindingPathBuilder + { + private object _rawSource; + private List _elements = new List(); + + public CompiledBindingPathBuilder Not() + { + _elements.Add(new NotExpressionPathElement()); + return this; + } + + public CompiledBindingPathBuilder Property(IPropertyInfo info, Func, IPropertyInfo, IPropertyAccessor> accessorFactory) + { + _elements.Add(new PropertyElement(info, accessorFactory)); + return this; + } + + public CompiledBindingPathBuilder StreamTask() + { + _elements.Add(new TaskStreamPathElement()); + return this; + } + + public CompiledBindingPathBuilder StreamObservable() + { + _elements.Add(new ObservableStreamPathElement()); + return this; + } + + public CompiledBindingPathBuilder Self() + { + _elements.Add(new SelfPathElement()); + return this; + } + + public CompiledBindingPathBuilder Ancestor(Type ancestorType, int level) + { + _elements.Add(new AncestorPathElement(ancestorType, level)); + return this; + } + public CompiledBindingPathBuilder VisualAncestor(Type ancestorType, int level) + { + _elements.Add(new VisualAncestorPathElement(ancestorType, level)); + return this; + } + + public CompiledBindingPathBuilder ElementName(INameScope nameScope, string name) + { + _elements.Add(new ElementNameElement(nameScope, name)); + return this; + } + + public CompiledBindingPathBuilder ArrayElement(int[] indices, Type elementType) + { + _elements.Add(new ArrayElementPathElement(indices, elementType)); + return this; + } + + public CompiledBindingPathBuilder SetRawSource(object rawSource) + { + _rawSource = rawSource; + return this; + } + + public CompiledBindingPath Build() => new CompiledBindingPath(_elements, _rawSource); + } + + public interface ICompiledBindingPathElement + { + } + + internal interface IControlSourceBindingPathElement { } + + internal class NotExpressionPathElement : ICompiledBindingPathElement + { + public static readonly NotExpressionPathElement Instance = new NotExpressionPathElement(); + } + + internal class PropertyElement : ICompiledBindingPathElement + { + public PropertyElement(IPropertyInfo property, Func, IPropertyInfo, IPropertyAccessor> accessorFactory) + { + Property = property; + AccessorFactory = accessorFactory; + } + + public IPropertyInfo Property { get; } + + public Func, IPropertyInfo, IPropertyAccessor> AccessorFactory { get; } + } + + internal interface IStronglyTypedStreamElement : ICompiledBindingPathElement + { + IStreamPlugin CreatePlugin(); + } + + internal class TaskStreamPathElement : IStronglyTypedStreamElement + { + public static readonly TaskStreamPathElement Instance = new TaskStreamPathElement(); + + public IStreamPlugin CreatePlugin() => new TaskStreamPlugin(); + } + + internal class ObservableStreamPathElement : IStronglyTypedStreamElement + { + public static readonly ObservableStreamPathElement Instance = new ObservableStreamPathElement(); + + public IStreamPlugin CreatePlugin() => new ObservableStreamPlugin(); + } + + internal class SelfPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement + { + public static readonly SelfPathElement Instance = new SelfPathElement(); + } + + internal class AncestorPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement + { + public AncestorPathElement(Type ancestorType, int level) + { + AncestorType = ancestorType; + Level = level; + } + + public Type AncestorType { get; } + public int Level { get; } + } + + internal class VisualAncestorPathElement : ICompiledBindingPathElement, IControlSourceBindingPathElement + { + public VisualAncestorPathElement(Type ancestorType, int level) + { + AncestorType = ancestorType; + Level = level; + } + + public Type AncestorType { get; } + public int Level { get; } + } + + internal class ElementNameElement : ICompiledBindingPathElement, IControlSourceBindingPathElement + { + public ElementNameElement(INameScope nameScope, string name) + { + NameScope = nameScope; + Name = name; + } + + public INameScope NameScope { get; } + public string Name { get; } + } + + internal class ArrayElementPathElement : ICompiledBindingPathElement + { + public ArrayElementPathElement(int[] indices, Type elementType) + { + Indices = indices; + ElementType = elementType; + } + + public int[] Indices { get; } + public Type ElementType { get; } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/FindVisualAncestorNode.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/FindVisualAncestorNode.cs new file mode 100644 index 00000000000..5820f47fbb2 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/FindVisualAncestorNode.cs @@ -0,0 +1,52 @@ +using System; +using Avalonia.Data.Core; +using Avalonia.VisualTree; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + class FindVisualAncestorNode : ExpressionNode + { + private readonly int _level; + private readonly Type _ancestorType; + private IDisposable _subscription; + + public FindVisualAncestorNode(Type ancestorType, int level) + { + _level = level; + _ancestorType = ancestorType; + } + + public override string Description + { + get + { + if (_ancestorType == null) + { + return $"$visualparent[{_level}]"; + } + else + { + return $"$visualparent[{_ancestorType.Name}, {_level}]"; + } + } + } + + protected override void StartListeningCore(WeakReference reference) + { + if (reference.TryGetTarget(out object target) && target is IVisual visual) + { + _subscription = VisualLocator.Track(visual, _level, _ancestorType).Subscribe(ValueChanged); + } + else + { + _subscription = null; + } + } + + protected override void StopListeningCore() + { + _subscription?.Dispose(); + _subscription = null; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs new file mode 100644 index 00000000000..3e3174123a3 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/ObservableStreamPlugin.cs @@ -0,0 +1,30 @@ +using System; +using System.Collections.Generic; +using System.Reactive.Linq; +using System.Text; +using Avalonia.Data.Core.Plugins; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + class ObservableStreamPlugin : IStreamPlugin + { + public bool Match(WeakReference reference) + { + return reference.TryGetTarget(out var target) && target is IObservable; + } + + public IObservable Start(WeakReference reference) + { + if (!(reference.TryGetTarget(out var target) && target is IObservable obs)) + { + return Observable.Empty(); + } + else if (target is IObservable obj) + { + return obj; + } + + return obs.Select(x => (object)x); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs new file mode 100644 index 00000000000..b3f78bfbe3b --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorFactory.cs @@ -0,0 +1,225 @@ +using System; +using System.Collections.Generic; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Text; +using Avalonia.Data; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; +using Avalonia.Utilities; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + public static class PropertyInfoAccessorFactory + { + public static IPropertyAccessor CreateInpcPropertyAccessor(WeakReference target, IPropertyInfo property) + => new InpcPropertyAccessor(target, property); + + public static IPropertyAccessor CreateAvaloniaPropertyAccessor(WeakReference target, IPropertyInfo property) + => new AvaloniaPropertyAccessor(new WeakReference((AvaloniaObject)(target.TryGetTarget(out var o) ? o : null)), (AvaloniaProperty)property); + + public static IPropertyAccessor CreateIndexerPropertyAccessor(WeakReference target, IPropertyInfo property, int argument) + => new IndexerAccessor(target, property, argument); + } + + internal class AvaloniaPropertyAccessor : PropertyAccessorBase + { + private readonly WeakReference _reference; + private readonly AvaloniaProperty _property; + private IDisposable _subscription; + + public AvaloniaPropertyAccessor(WeakReference reference, AvaloniaProperty property) + { + Contract.Requires(reference != null); + Contract.Requires(property != null); + + _reference = reference; + _property = property; + } + + public AvaloniaObject Instance + { + get + { + _reference.TryGetTarget(out var result); + return result; + } + } + + public override Type PropertyType => _property.PropertyType; + public override object Value => Instance?.GetValue(_property); + + public override bool SetValue(object value, BindingPriority priority) + { + if (!_property.IsReadOnly) + { + Instance.SetValue(_property, value, priority); + return true; + } + + return false; + } + + protected override void SubscribeCore() + { + _subscription = Instance?.GetObservable(_property).Subscribe(PublishValue); + } + + protected override void UnsubscribeCore() + { + _subscription?.Dispose(); + _subscription = null; + } + } + + internal class InpcPropertyAccessor : PropertyAccessorBase + { + protected readonly WeakReference _reference; + private readonly IPropertyInfo _property; + + public InpcPropertyAccessor(WeakReference reference, IPropertyInfo property) + { + Contract.Requires(reference != null); + Contract.Requires(property != null); + + _reference = reference; + _property = property; + } + + public override Type PropertyType => _property.PropertyType; + + public override object Value + { + get + { + return _reference.TryGetTarget(out var o) ? _property.Get(o) : null; + } + } + + public override bool SetValue(object value, BindingPriority priority) + { + if (_property.CanSet && _reference.TryGetTarget(out var o)) + { + _property.Set(o, value); + + SendCurrentValue(); + + return true; + } + + return false; + } + + void OnNotifyPropertyChanged(object sender, PropertyChangedEventArgs e) + { + if (e.PropertyName == _property.Name || string.IsNullOrEmpty(e.PropertyName)) + { + SendCurrentValue(); + } + } + + protected override void SubscribeCore() + { + SendCurrentValue(); + SubscribeToChanges(); + } + + protected override void UnsubscribeCore() + { + if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) + { + WeakEventHandlerManager.Unsubscribe( + inpc, + nameof(INotifyPropertyChanged.PropertyChanged), + OnNotifyPropertyChanged); + } + } + + protected void SendCurrentValue() + { + try + { + var value = Value; + PublishValue(value); + } + catch { } + } + + private void SubscribeToChanges() + { + if (_reference.TryGetTarget(out var o) && o is INotifyPropertyChanged inpc) + { + WeakEventHandlerManager.Subscribe( + inpc, + nameof(INotifyPropertyChanged.PropertyChanged), + OnNotifyPropertyChanged); + } + } + } + + internal class IndexerAccessor : InpcPropertyAccessor + { + private int _index; + + public IndexerAccessor(WeakReference target, IPropertyInfo basePropertyInfo, int argument) + :base(target, basePropertyInfo) + { + _index = argument; + } + + + protected override void SubscribeCore() + { + base.SubscribeCore(); + if (_reference.TryGetTarget(out var o) && o is INotifyCollectionChanged incc) + { + WeakEventHandlerManager.Subscribe( + incc, + nameof(INotifyCollectionChanged.CollectionChanged), + OnNotifyCollectionChanged); + } + } + + protected override void UnsubscribeCore() + { + base.UnsubscribeCore(); + if (_reference.TryGetTarget(out var o) && o is INotifyCollectionChanged incc) + { + WeakEventHandlerManager.Unsubscribe( + incc, + nameof(INotifyCollectionChanged.CollectionChanged), + OnNotifyCollectionChanged); + } + } + + void OnNotifyCollectionChanged(object sender, NotifyCollectionChangedEventArgs args) + { + if (ShouldNotifyListeners(args)) + { + SendCurrentValue(); + } + } + + bool ShouldNotifyListeners(NotifyCollectionChangedEventArgs e) + { + switch (e.Action) + { + case NotifyCollectionChangedAction.Add: + return _index >= e.NewStartingIndex; + case NotifyCollectionChangedAction.Remove: + return _index >= e.OldStartingIndex; + case NotifyCollectionChangedAction.Replace: + return _index >= e.NewStartingIndex && + _index < e.NewStartingIndex + e.NewItems.Count; + case NotifyCollectionChangedAction.Move: + return (_index >= e.NewStartingIndex && + _index < e.NewStartingIndex + e.NewItems.Count) || + (_index >= e.OldStartingIndex && + _index < e.OldStartingIndex + e.OldItems.Count); + case NotifyCollectionChangedAction.Reset: + return true; + } + return false; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs new file mode 100644 index 00000000000..6e7463776b6 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/PropertyInfoAccessorPlugin.cs @@ -0,0 +1,33 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Text; +using Avalonia.Data; +using Avalonia.Data.Core; +using Avalonia.Data.Core.Plugins; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + class PropertyInfoAccessorPlugin : IPropertyAccessorPlugin + { + private readonly IPropertyInfo _propertyInfo; + private readonly Func, IPropertyInfo, IPropertyAccessor> _accessorFactory; + + public PropertyInfoAccessorPlugin(IPropertyInfo propertyInfo, Func, IPropertyInfo, IPropertyAccessor> accessorFactory) + { + _propertyInfo = propertyInfo; + _accessorFactory = accessorFactory; + } + + public bool Match(object obj, string propertyName) + { + throw new InvalidOperationException("The PropertyInfoAccessorPlugin does not support dynamic matching"); + } + + public IPropertyAccessor Start(WeakReference reference, string propertyName) + { + Debug.Assert(_propertyInfo.Name == propertyName); + return _accessorFactory(reference, _propertyInfo); + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs new file mode 100644 index 00000000000..164d38e3aec --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/CompiledBindings/TaskStreamPlugin.cs @@ -0,0 +1,53 @@ +using System; +using System.Reactive.Linq; +using System.Reactive.Subjects; +using System.Threading.Tasks; +using Avalonia.Data; +using Avalonia.Data.Core.Plugins; + +namespace Avalonia.Markup.Xaml.MarkupExtensions.CompiledBindings +{ + class TaskStreamPlugin : IStreamPlugin + { + public bool Match(WeakReference reference) + { + return reference.TryGetTarget(out var target) && target is Task; + } + + public IObservable Start(WeakReference reference) + { + if(!(reference.TryGetTarget(out var target) && target is Task task)) + { + return Observable.Empty(); + } + + switch (task.Status) + { + case TaskStatus.RanToCompletion: + case TaskStatus.Faulted: + return HandleCompleted(task); + default: + var subject = new Subject(); + task.ContinueWith( + x => HandleCompleted(task).Subscribe(subject), + TaskScheduler.FromCurrentSynchronizationContext()) + .ConfigureAwait(false); + return subject; + } + } + + + private static IObservable HandleCompleted(Task task) + { + switch (task.Status) + { + case TaskStatus.RanToCompletion: + return Observable.Return((object)task.Result); + case TaskStatus.Faulted: + return Observable.Return(new BindingNotification(task.Exception, BindingErrorType.Error)); + default: + throw new AvaloniaInternalException("HandleCompleted called for non-completed Task."); + } + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs similarity index 95% rename from src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs rename to src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs index cc66efa5fde..10770365a32 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/BindingExtension.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ReflectionBindingExtension.cs @@ -9,13 +9,13 @@ namespace Avalonia.Markup.Xaml.MarkupExtensions using Avalonia.Styling; using System.ComponentModel; - public class BindingExtension + public class ReflectionBindingExtension { - public BindingExtension() + public ReflectionBindingExtension() { } - public BindingExtension(string path) + public ReflectionBindingExtension(string path) { Path = path; } diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResolveByNameExtension.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResolveByNameExtension.cs new file mode 100644 index 00000000000..8561aa898b8 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResolveByNameExtension.cs @@ -0,0 +1,36 @@ +using System; +using Avalonia.Controls; +using Avalonia.Data.Core; + +#nullable enable + +namespace Avalonia.Markup.Xaml.MarkupExtensions +{ + public class ResolveByNameExtension + { + public ResolveByNameExtension(string name) + { + Name = name; + } + + public string Name { get; } + + public object? ProvideValue(IServiceProvider serviceProvider) + { + var nameScope = serviceProvider.GetService(); + + var value = nameScope.FindAsync(Name); + + if(value.IsCompleted) + return value.GetResult(); + + var provideValueTarget = serviceProvider.GetService(); + var target = provideValueTarget.TargetObject; + + if (provideValueTarget.TargetProperty is IPropertyInfo property) + value.OnCompleted(() => property.Set(target, value.GetResult())); + + return null; + } + } +} diff --git a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs index 0cedf4f3645..b6137aa89f7 100644 --- a/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/MarkupExtensions/ResourceInclude.cs @@ -25,8 +25,7 @@ public IResourceDictionary Loaded if (_loaded == null) { _isLoading = true; - var loader = new AvaloniaXamlLoader(); - _loaded = (IResourceDictionary)loader.Load(Source, _baseUri); + _loaded = (IResourceDictionary)AvaloniaXamlLoader.Load(Source, _baseUri); _isLoading = false; } diff --git a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs index ea9042f7790..607b552c281 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Styling/StyleInclude.cs @@ -51,8 +51,7 @@ public IStyle Loaded if (_loaded == null) { _isLoading = true; - var loader = new AvaloniaXamlLoader(); - var loaded = (IStyle)loader.Load(Source, _baseUri); + var loaded = (IStyle)AvaloniaXamlLoader.Load(Source, _baseUri); _loaded = new[] { loaded }; _isLoading = false; } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs index 5663d084124..07c5451135f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/DataTemplate.cs @@ -5,7 +5,7 @@ namespace Avalonia.Markup.Xaml.Templates { - public class DataTemplate : IDataTemplate + public class DataTemplate : IRecyclingDataTemplate { public Type DataType { get; set; } @@ -14,8 +14,6 @@ public class DataTemplate : IDataTemplate [TemplateContent] public object Content { get; set; } - public bool SupportsRecycling { get; set; } = true; - public bool Match(object data) { if (DataType == null) @@ -28,6 +26,11 @@ public bool Match(object data) } } - public IControl Build(object data) => TemplateContent.Load(Content).Control; + public IControl Build(object data) => Build(data, null); + + public IControl Build(object data, IControl existing) + { + return existing ?? TemplateContent.Load(Content).Control; + } } } diff --git a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs index b96486235ae..b8e1c2df80b 100644 --- a/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs +++ b/src/Markup/Avalonia.Markup.Xaml/Templates/TreeDataTemplate.cs @@ -18,8 +18,6 @@ public class TreeDataTemplate : ITreeDataTemplate [AssignBinding] public Binding ItemsSource { get; set; } - public bool SupportsRecycling { get; set; } = true; - public bool Match(object data) { if (DataType == null) diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs deleted file mode 100644 index b84f50fa8de..00000000000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ /dev/null @@ -1,142 +0,0 @@ -using System.Collections.Generic; -using System.Linq; -using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; -using XamlIl; -using XamlIl.Ast; -using XamlIl.Parsers; -using XamlIl.Transform; -using XamlIl.Transform.Transformers; -using XamlIl.TypeSystem; - -namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions -{ - class AvaloniaXamlIlCompiler : XamlIlCompiler - { - private readonly XamlIlTransformerConfiguration _configuration; - private readonly IXamlIlType _contextType; - private readonly AvaloniaXamlIlDesignPropertiesTransformer _designTransformer; - - private AvaloniaXamlIlCompiler(XamlIlTransformerConfiguration configuration) : base(configuration, true) - { - _configuration = configuration; - - void InsertAfter(params IXamlIlAstTransformer[] t) - => Transformers.InsertRange(Transformers.FindIndex(x => x is T) + 1, t); - - void InsertBefore(params IXamlIlAstTransformer[] t) - => Transformers.InsertRange(Transformers.FindIndex(x => x is T), t); - - - // Before everything else - - Transformers.Insert(0, new XNameTransformer()); - Transformers.Insert(1, new IgnoredDirectivesTransformer()); - Transformers.Insert(2, _designTransformer = new AvaloniaXamlIlDesignPropertiesTransformer()); - Transformers.Insert(3, new AvaloniaBindingExtensionHackTransformer()); - - - // Targeted - - InsertBefore(new AvaloniaXamlIlTransformInstanceAttachedProperties()); - InsertAfter(new AvaloniaXamlIlAvaloniaPropertyResolver()); - - - - InsertBefore( - new AvaloniaXamlIlSelectorTransformer(), - new AvaloniaXamlIlSetterTransformer(), - new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), - new AvaloniaXamlIlConstructorServiceProviderTransformer(), - new AvaloniaXamlIlTransitionsTypeMetadataTransformer() - ); - - // After everything else - - Transformers.Add(new AddNameScopeRegistration()); - Transformers.Add(new AvaloniaXamlIlMetadataRemover()); - - } - - public AvaloniaXamlIlCompiler(XamlIlTransformerConfiguration configuration, - IXamlIlTypeBuilder contextTypeBuilder) : this(configuration) - { - _contextType = CreateContextType(contextTypeBuilder); - } - - - public AvaloniaXamlIlCompiler(XamlIlTransformerConfiguration configuration, - IXamlIlType contextType) : this(configuration) - { - _contextType = contextType; - } - - public const string PopulateName = "__AvaloniaXamlIlPopulate"; - public const string BuildName = "__AvaloniaXamlIlBuild"; - - public bool IsDesignMode - { - get => _designTransformer.IsDesignMode; - set => _designTransformer.IsDesignMode = value; - } - - public void ParseAndCompile(string xaml, string baseUri, IFileSource fileSource, IXamlIlTypeBuilder tb, IXamlIlType overrideRootType) - { - var parsed = XDocumentXamlIlParser.Parse(xaml, new Dictionary - { - {XamlNamespaces.Blend2008, XamlNamespaces.Blend2008} - }); - - var rootObject = (XamlIlAstObjectNode)parsed.Root; - - var classDirective = rootObject.Children - .OfType().FirstOrDefault(x => - x.Namespace == XamlNamespaces.Xaml2006 - && x.Name == "Class"); - - var rootType = - classDirective != null ? - new XamlIlAstClrTypeReference(classDirective, - _configuration.TypeSystem.GetType(((XamlIlAstTextNode)classDirective.Values[0]).Text), - false) : - XamlIlTypeReferenceResolver.ResolveType(CreateTransformationContext(parsed, true), - (XamlIlAstXmlTypeReference)rootObject.Type, true); - - - if (overrideRootType != null) - { - - - if (!rootType.Type.IsAssignableFrom(overrideRootType)) - throw new XamlIlLoadException( - $"Unable to substitute {rootType.Type.GetFqn()} with {overrideRootType.GetFqn()}", rootObject); - rootType = new XamlIlAstClrTypeReference(rootObject, overrideRootType, false); - } - - OverrideRootType(parsed, rootType); - - Transform(parsed); - Compile(parsed, tb, _contextType, PopulateName, BuildName, "__AvaloniaXamlIlNsInfo", baseUri, fileSource); - - } - - public void OverrideRootType(XamlIlDocument doc, IXamlIlAstTypeReference newType) - { - var root = (XamlIlAstObjectNode)doc.Root; - var oldType = root.Type; - if (oldType.Equals(newType)) - return; - - root.Type = newType; - foreach (var child in root.Children.OfType()) - { - if (child.Property is XamlIlAstNamePropertyReference prop) - { - if (prop.DeclaringType.Equals(oldType)) - prop.DeclaringType = newType; - if (prop.TargetType.Equals(oldType)) - prop.TargetType = newType; - } - } - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs deleted file mode 100644 index 805b733feba..00000000000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs +++ /dev/null @@ -1,139 +0,0 @@ -using System; -using System.Linq; -using XamlIl.Ast; -using XamlIl.Transform; -using XamlIl.TypeSystem; - -namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers -{ - class AddNameScopeRegistration : IXamlIlAstTransformer - { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) - { - if (node is XamlIlPropertyAssignmentNode pa - && pa.Property.Name == "Name" - && pa.Property.DeclaringType.FullName == "Avalonia.StyledElement") - { - if (context.ParentNodes().FirstOrDefault() is XamlIlManipulationGroupNode mg - && mg.Children.OfType().Any()) - return node; - - IXamlIlAstValueNode value = null; - for (var c = 0; c < pa.Values.Count; c++) - if (pa.Values[c].Type.GetClrType().Equals(context.Configuration.WellKnownTypes.String)) - { - value = pa.Values[c]; - if (!(value is XamlIlAstTextNode)) - { - var local = new XamlIlAstCompilerLocalNode(value); - // Wrap original in local initialization - pa.Values[c] = new XamlIlAstLocalInitializationNodeEmitter(value, value, local); - // Use local - value = local; - } - - break; - } - - if (value != null) - return new XamlIlManipulationGroupNode(pa) - { - Children = - { - pa, - new AvaloniaNameScopeRegistrationXamlIlNode(value, context.GetAvaloniaTypes()) - } - }; - } - - if (!context.ParentNodes().Any() - && node is XamlIlValueWithManipulationNode mnode) - { - mnode.Manipulation = new XamlIlManipulationGroupNode(mnode, - new[] - { - mnode.Manipulation, - new HandleRootObjectScopeNode(mnode, context.GetAvaloniaTypes()) - }); - } - return node; - } - - class HandleRootObjectScopeNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode - { - private readonly AvaloniaXamlIlWellKnownTypes _types; - - public HandleRootObjectScopeNode(IXamlIlLineInfo lineInfo, - AvaloniaXamlIlWellKnownTypes types) : base(lineInfo) - { - _types = types; - } - - public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) - { - var next = codeGen.DefineLabel(); - var scopeField = context.RuntimeContext.ContextType.Fields.First(f => - f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName); - using (var local = codeGen.LocalsPool.GetLocal(_types.StyledElement)) - { - codeGen - .Isinst(_types.StyledElement) - .Dup() - .Stloc(local.Local) - .Brfalse(next) - .Ldloc(local.Local) - .Ldloc(context.ContextLocal) - .Ldfld(scopeField) - .EmitCall(_types.NameScopeSetNameScope, true) - .MarkLabel(next) - .Ldloc(context.ContextLocal) - .Ldfld(scopeField) - .EmitCall(_types.INameScopeComplete, true); - } - - return XamlIlNodeEmitResult.Void(1); - - } - } - } - - class AvaloniaNameScopeRegistrationXamlIlNode : XamlIlAstNode, IXamlIlAstManipulationNode, IXamlIlAstEmitableNode - { - private readonly AvaloniaXamlIlWellKnownTypes _types; - public IXamlIlAstValueNode Name { get; set; } - - public AvaloniaNameScopeRegistrationXamlIlNode(IXamlIlAstValueNode name, AvaloniaXamlIlWellKnownTypes types) : base(name) - { - _types = types; - Name = name; - } - - public override void VisitChildren(IXamlIlAstVisitor visitor) - => Name = (IXamlIlAstValueNode)Name.Visit(visitor); - - public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) - { - var scopeField = context.RuntimeContext.ContextType.Fields.First(f => - f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName); - - using (var targetLoc = context.GetLocal(context.Configuration.WellKnownTypes.Object)) - { - - codeGen - // var target = {pop} - .Stloc(targetLoc.Local) - // _context.NameScope.Register(Name, target) - .Ldloc(context.ContextLocal) - .Ldfld(scopeField); - - context.Emit(Name, codeGen, Name.Type.GetClrType()); - - codeGen - .Ldloc(targetLoc.Local) - .EmitCall(_types.INameScopeRegister, true); - } - - return XamlIlNodeEmitResult.Void(1); - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaBindingExtensionHackTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaBindingExtensionHackTransformer.cs deleted file mode 100644 index c89106312fc..00000000000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaBindingExtensionHackTransformer.cs +++ /dev/null @@ -1,20 +0,0 @@ -using XamlIl.Ast; -using XamlIl.Transform; - -namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers -{ - class AvaloniaBindingExtensionHackTransformer : IXamlIlAstTransformer - { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) - { - // Our code base expects XAML parser to prefer `FooExtension` to `Foo` even with `` syntax - // This is the legacy of Portable.Xaml, so we emulate that behavior here - - if (node is XamlIlAstXmlTypeReference tref - && tref.Name == "Binding" - && tref.XmlNamespace == "https://github.com/avaloniaui") - tref.IsMarkupExtension = true; - return node; - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs deleted file mode 100644 index 994928b7fe5..00000000000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlMetadataRemover.cs +++ /dev/null @@ -1,17 +0,0 @@ -using System.Linq; -using XamlIl.Ast; -using XamlIl.Transform; - -namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers -{ - class AvaloniaXamlIlMetadataRemover : IXamlIlAstTransformer - { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) - { - if (node is AvaloniaXamlIlTargetTypeMetadataNode md) - return md.Value; - - return node; - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs deleted file mode 100644 index 629e2562d38..00000000000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs +++ /dev/null @@ -1,109 +0,0 @@ -using System; -using System.Collections.Generic; -using System.Linq; -using XamlIl; -using XamlIl.Ast; -using XamlIl.Transform; -using XamlIl.Transform.Transformers; -using XamlIl.TypeSystem; - -namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers -{ - class AvaloniaXamlIlSetterTransformer : IXamlIlAstTransformer - { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) - { - if (!(node is XamlIlAstObjectNode on - && on.Type.GetClrType().FullName == "Avalonia.Styling.Setter")) - return node; - - var parent = context.ParentNodes().OfType() - .FirstOrDefault(p => p.Type.GetClrType().FullName == "Avalonia.Styling.Style"); - - if (parent == null) - throw new XamlIlParseException( - "Avalonia.Styling.Setter is only valid inside Avalonia.Styling.Style", node); - var selectorProperty = parent.Children.OfType() - .FirstOrDefault(p => p.Property.GetClrProperty().Name == "Selector"); - if (selectorProperty == null) - throw new XamlIlParseException( - "Can not find parent Style Selector", node); - var selector = selectorProperty.Values.FirstOrDefault() as XamlIlSelectorNode; - if (selector?.TargetType == null) - throw new XamlIlParseException( - "Can not resolve parent Style Selector type", node); - - - var property = @on.Children.OfType() - .FirstOrDefault(x => x.Property.GetClrProperty().Name == "Property"); - if (property == null) - throw new XamlIlParseException("Setter without a property is not valid", node); - - var propertyName = property.Values.OfType().FirstOrDefault()?.Text; - if (propertyName == null) - throw new XamlIlParseException("Setter.Property must be a string", node); - - - var avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName, - new XamlIlAstClrTypeReference(selector, selector.TargetType, false), property.Values[0]); - property.Values = new List - { - avaloniaPropertyNode - }; - - var valueProperty = on.Children - .OfType().FirstOrDefault(p => p.Property.GetClrProperty().Name == "Value"); - if (valueProperty?.Values?.Count == 1 && valueProperty.Values[0] is XamlIlAstTextNode) - { - var propType = avaloniaPropertyNode.AvaloniaPropertyType; - if (!XamlIlTransformHelpers.TryGetCorrectlyTypedValue(context, valueProperty.Values[0], - propType, out var converted)) - throw new XamlIlParseException( - $"Unable to convert property value to {propType.GetFqn()}", - valueProperty.Values[0]); - - valueProperty.Property = new SetterValueProperty(valueProperty.Property, - on.Type.GetClrType(), propType, context.GetAvaloniaTypes()); - } - - return node; - } - - class SetterValueProperty : XamlIlAstClrProperty - { - public SetterValueProperty(IXamlIlLineInfo line, IXamlIlType setterType, IXamlIlType targetType, - AvaloniaXamlIlWellKnownTypes types) - : base(line, "Value", setterType, null) - { - Getter = setterType.Methods.First(m => m.Name == "get_Value"); - var method = setterType.Methods.First(m => m.Name == "set_Value"); - Setters.Add(new XamlIlDirectCallPropertySetter(method, types.IBinding)); - Setters.Add(new XamlIlDirectCallPropertySetter(method, types.UnsetValueType)); - Setters.Add(new XamlIlDirectCallPropertySetter(method, targetType)); - } - - class XamlIlDirectCallPropertySetter : IXamlIlPropertySetter - { - private readonly IXamlIlMethod _method; - private readonly IXamlIlType _type; - public IXamlIlType TargetType { get; } - public PropertySetterBinderParameters BinderParameters { get; } = new PropertySetterBinderParameters(); - public IReadOnlyList Parameters { get; } - public void Emit(IXamlIlEmitter codegen) - { - if (_type.IsValueType) - codegen.Box(_type); - codegen.EmitCall(_method, true); - } - - public XamlIlDirectCallPropertySetter(IXamlIlMethod method, IXamlIlType type) - { - _method = method; - _type = type; - Parameters = new[] {type}; - TargetType = method.ThisOrFirstParameter(); - } - } - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs deleted file mode 100644 index 1efae902c6e..00000000000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ /dev/null @@ -1,81 +0,0 @@ -using XamlIl.Transform; -using XamlIl.TypeSystem; - -namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers -{ - class AvaloniaXamlIlWellKnownTypes - { - public IXamlIlType AvaloniaObject { get; } - public IXamlIlType IAvaloniaObject { get; } - public IXamlIlType BindingPriority { get; } - public IXamlIlType AvaloniaObjectExtensions { get; } - public IXamlIlType AvaloniaProperty { get; } - public IXamlIlType AvaloniaPropertyT { get; } - public IXamlIlType IBinding { get; } - public IXamlIlMethod AvaloniaObjectBindMethod { get; } - public IXamlIlMethod AvaloniaObjectSetValueMethod { get; } - public IXamlIlType IDisposable { get; } - public XamlIlTypeWellKnownTypes XamlIlTypes { get; } - public XamlIlLanguageTypeMappings XamlIlMappings { get; } - public IXamlIlType Transitions { get; } - public IXamlIlType AssignBindingAttribute { get; } - public IXamlIlType UnsetValueType { get; } - public IXamlIlType StyledElement { get; } - public IXamlIlType NameScope { get; } - public IXamlIlMethod NameScopeSetNameScope { get; } - public IXamlIlType INameScope { get; } - public IXamlIlMethod INameScopeRegister { get; } - public IXamlIlMethod INameScopeComplete { get; } - - public AvaloniaXamlIlWellKnownTypes(XamlIlAstTransformationContext ctx) - { - XamlIlTypes = ctx.Configuration.WellKnownTypes; - XamlIlMappings = ctx.Configuration.TypeMappings; - AvaloniaObject = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaObject"); - IAvaloniaObject = ctx.Configuration.TypeSystem.GetType("Avalonia.IAvaloniaObject"); - AvaloniaObjectExtensions = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaObjectExtensions"); - AvaloniaProperty = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaProperty"); - AvaloniaPropertyT = ctx.Configuration.TypeSystem.GetType("Avalonia.AvaloniaProperty`1"); - BindingPriority = ctx.Configuration.TypeSystem.GetType("Avalonia.Data.BindingPriority"); - IBinding = ctx.Configuration.TypeSystem.GetType("Avalonia.Data.IBinding"); - IDisposable = ctx.Configuration.TypeSystem.GetType("System.IDisposable"); - Transitions = ctx.Configuration.TypeSystem.GetType("Avalonia.Animation.Transitions"); - AssignBindingAttribute = ctx.Configuration.TypeSystem.GetType("Avalonia.Data.AssignBindingAttribute"); - AvaloniaObjectBindMethod = AvaloniaObjectExtensions.FindMethod("Bind", IDisposable, false, IAvaloniaObject, - AvaloniaProperty, - IBinding, ctx.Configuration.WellKnownTypes.Object); - UnsetValueType = ctx.Configuration.TypeSystem.GetType("Avalonia.UnsetValueType"); - StyledElement = ctx.Configuration.TypeSystem.GetType("Avalonia.StyledElement"); - INameScope = ctx.Configuration.TypeSystem.GetType("Avalonia.Controls.INameScope"); - INameScopeRegister = INameScope.GetMethod( - new FindMethodMethodSignature("Register", XamlIlTypes.Void, - XamlIlTypes.String, XamlIlTypes.Object) - { - IsStatic = false, DeclaringOnly = true, IsExactMatch = true - }); - INameScopeComplete = INameScope.GetMethod( - new FindMethodMethodSignature("Complete", XamlIlTypes.Void) - { - IsStatic = false, DeclaringOnly = true, IsExactMatch = true - }); - NameScope = ctx.Configuration.TypeSystem.GetType("Avalonia.Controls.NameScope"); - NameScopeSetNameScope = NameScope.GetMethod(new FindMethodMethodSignature("SetNameScope", - XamlIlTypes.Void, StyledElement, INameScope) {IsStatic = true}); - - AvaloniaObjectSetValueMethod = AvaloniaObject.FindMethod("SetValue", XamlIlTypes.Void, - false, AvaloniaProperty, XamlIlTypes.Object, BindingPriority); - - } - } - - static class AvaloniaXamlIlWellKnownTypesExtensions - { - public static AvaloniaXamlIlWellKnownTypes GetAvaloniaTypes(this XamlIlAstTransformationContext ctx) - { - if (ctx.TryGetItem(out var rv)) - return rv; - ctx.SetItem(rv = new AvaloniaXamlIlWellKnownTypes(ctx)); - return rv; - } - } -} diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github deleted file mode 160000 index 06816224547..00000000000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ /dev/null @@ -1 +0,0 @@ -Subproject commit 068162245473ec39ee36da12150e928072b96403 diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 673eb519016..aca39dd8b06 100644 --- a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj +++ b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj @@ -3,6 +3,12 @@ netstandard2.0 Avalonia + + + + + + diff --git a/src/Markup/Avalonia.Markup/Data/Binding.cs b/src/Markup/Avalonia.Markup/Data/Binding.cs index f1a62f9bdcd..bf43730481e 100644 --- a/src/Markup/Avalonia.Markup/Data/Binding.cs +++ b/src/Markup/Avalonia.Markup/Data/Binding.cs @@ -15,15 +15,14 @@ namespace Avalonia.Data /// /// A XAML binding. /// - public class Binding : IBinding + public class Binding : BindingBase { /// /// Initializes a new instance of the class. /// public Binding() + :base() { - FallbackValue = AvaloniaProperty.UnsetValue; - TargetNullValue = AvaloniaProperty.UnsetValue; } /// @@ -32,52 +31,16 @@ public Binding() /// The binding path. /// The binding mode. public Binding(string path, BindingMode mode = BindingMode.Default) - : this() + : base(mode) { Path = path; - Mode = mode; } - /// - /// Gets or sets the to use. - /// - public IValueConverter Converter { get; set; } - - /// - /// Gets or sets a parameter to pass to . - /// - public object ConverterParameter { get; set; } - /// /// Gets or sets the name of the element to use as the binding source. /// public string ElementName { get; set; } - /// - /// Gets or sets the value to use when the binding is unable to produce a value. - /// - public object FallbackValue { get; set; } - - /// - /// Gets or sets the value to use when the binding result is null. - /// - public object TargetNullValue { get; set; } - - /// - /// Gets or sets the binding mode. - /// - public BindingMode Mode { get; set; } - - /// - /// Gets or sets the binding path. - /// - public string Path { get; set; } = ""; - - /// - /// Gets or sets the binding priority. - /// - public BindingPriority Priority { get; set; } - /// /// Gets or sets the relative source for the binding. /// @@ -89,68 +52,58 @@ public Binding(string path, BindingMode mode = BindingMode.Default) public object Source { get; set; } /// - /// Gets or sets the string format. + /// Gets or sets the binding path. /// - public string StringFormat { get; set; } - - public WeakReference DefaultAnchor { get; set; } - - public WeakReference NameScope { get; set; } + public string Path { get; set; } = ""; /// /// Gets or sets a function used to resolve types from names in the binding path. /// public Func TypeResolver { get; set; } - /// - public InstancedBinding Initiate( - IAvaloniaObject target, - AvaloniaProperty targetProperty, - object anchor = null, - bool enableDataValidation = false) + protected override ExpressionObserver CreateExpressionObserver(IAvaloniaObject target, AvaloniaProperty targetProperty, object anchor, bool enableDataValidation) { Contract.Requires(target != null); anchor = anchor ?? DefaultAnchor?.Target; enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; - - ExpressionObserver observer; INameScope nameScope = null; NameScope?.TryGetTarget(out nameScope); + var (node, mode) = ExpressionObserverBuilder.Parse(Path, enableDataValidation, TypeResolver, nameScope); if (ElementName != null) { - observer = CreateElementObserver( + return CreateElementObserver( (target as IStyledElement) ?? (anchor as IStyledElement), ElementName, node); } else if (Source != null) { - observer = CreateSourceObserver(Source, node); + return CreateSourceObserver(Source, node); } else if (RelativeSource == null) { if (mode == SourceMode.Data) { - observer = CreateDataContextObserver( + return CreateDataContextObserver( target, node, targetProperty == StyledElement.DataContextProperty, - anchor); + anchor); } else { - observer = new ExpressionObserver( + return CreateSourceObserver( (target as IStyledElement) ?? (anchor as IStyledElement), node); } } else if (RelativeSource.Mode == RelativeSourceMode.DataContext) { - observer = CreateDataContextObserver( + return CreateDataContextObserver( target, node, targetProperty == StyledElement.DataContextProperty, @@ -158,13 +111,13 @@ public InstancedBinding Initiate( } else if (RelativeSource.Mode == RelativeSourceMode.Self) { - observer = CreateSourceObserver( + return CreateSourceObserver( (target as IStyledElement) ?? (anchor as IStyledElement), node); } else if (RelativeSource.Mode == RelativeSourceMode.TemplatedParent) { - observer = CreateTemplatedParentObserver( + return CreateTemplatedParentObserver( (target as IStyledElement) ?? (anchor as IStyledElement), node); } @@ -175,7 +128,7 @@ public InstancedBinding Initiate( throw new InvalidOperationException("AncestorType must be set for RelativeSourceMode.FindAncestor when searching the visual tree."); } - observer = CreateFindAncestorObserver( + return CreateFindAncestorObserver( (target as IStyledElement) ?? (anchor as IStyledElement), RelativeSource, node); @@ -184,197 +137,6 @@ public InstancedBinding Initiate( { throw new NotSupportedException(); } - - var fallback = FallbackValue; - - // If we're binding to DataContext and our fallback is UnsetValue then override - // the fallback value to null, as broken bindings to DataContext must reset the - // DataContext in order to not propagate incorrect DataContexts to child controls. - // See Avalonia.Markup.UnitTests.Data.DataContext_Binding_Should_Produce_Correct_Results. - if (targetProperty == StyledElement.DataContextProperty && fallback == AvaloniaProperty.UnsetValue) - { - fallback = null; - } - - var converter = Converter; - var targetType = targetProperty?.PropertyType ?? typeof(object); - - // We only respect `StringFormat` if the type of the property we're assigning to will - // accept a string. Note that this is slightly different to WPF in that WPF only applies - // `StringFormat` for target type `string` (not `object`). - if (!string.IsNullOrWhiteSpace(StringFormat) && - (targetType == typeof(string) || targetType == typeof(object))) - { - converter = new StringFormatValueConverter(StringFormat, converter); - } - - var subject = new BindingExpression( - observer, - targetType, - fallback, - TargetNullValue, - converter ?? DefaultValueConverter.Instance, - ConverterParameter, - Priority); - - return new InstancedBinding(subject, Mode, Priority); - } - - private ExpressionObserver CreateDataContextObserver( - IAvaloniaObject target, - ExpressionNode node, - bool targetIsDataContext, - object anchor) - { - Contract.Requires(target != null); - - if (!(target is IDataContextProvider)) - { - target = anchor as IDataContextProvider; - - if (target == null) - { - throw new InvalidOperationException("Cannot find a DataContext to bind to."); - } - } - - if (!targetIsDataContext) - { - var result = new ExpressionObserver( - () => target.GetValue(StyledElement.DataContextProperty), - node, - new UpdateSignal(target, StyledElement.DataContextProperty), - null); - - return result; - } - else - { - return new ExpressionObserver( - GetParentDataContext(target), - node, - null); - } - } - - private ExpressionObserver CreateElementObserver( - IStyledElement target, - string elementName, - ExpressionNode node) - { - Contract.Requires(target != null); - - NameScope.TryGetTarget(out var scope); - if (scope == null) - throw new InvalidOperationException("Name scope is null or was already collected"); - var result = new ExpressionObserver( - NameScopeLocator.Track(scope, elementName), - node, - null); - return result; - } - - private ExpressionObserver CreateFindAncestorObserver( - IStyledElement target, - RelativeSource relativeSource, - ExpressionNode node) - { - Contract.Requires(target != null); - - IObservable controlLocator; - - switch (relativeSource.Tree) - { - case TreeType.Logical: - controlLocator = ControlLocator.Track( - (ILogical)target, - relativeSource.AncestorLevel - 1, - relativeSource.AncestorType); - break; - case TreeType.Visual: - controlLocator = VisualLocator.Track( - (IVisual)target, - relativeSource.AncestorLevel - 1, - relativeSource.AncestorType); - break; - default: - throw new InvalidOperationException("Invalid tree to traverse."); - } - - return new ExpressionObserver( - controlLocator, - node, - null); - } - - private ExpressionObserver CreateSourceObserver( - object source, - ExpressionNode node) - { - Contract.Requires(source != null); - - return new ExpressionObserver(source, node); - } - - private ExpressionObserver CreateTemplatedParentObserver( - IAvaloniaObject target, - ExpressionNode node) - { - Contract.Requires(target != null); - - var result = new ExpressionObserver( - () => target.GetValue(StyledElement.TemplatedParentProperty), - node, - new UpdateSignal(target, StyledElement.TemplatedParentProperty), - null); - - return result; - } - - private IObservable GetParentDataContext(IAvaloniaObject target) - { - // The DataContext is based on the visual parent and not the logical parent: this may - // seem counter intuitive considering the fact that property inheritance works on the logical - // tree, but consider a ContentControl with a ContentPresenter. The ContentControl's - // Content property is bound to a value which becomes the ContentPresenter's - // DataContext - it is from this that the child hosted by the ContentPresenter needs to - // inherit its DataContext. - return target.GetObservable(Visual.VisualParentProperty) - .Select(x => - { - return (x as IAvaloniaObject)?.GetObservable(StyledElement.DataContextProperty) ?? - Observable.Return((object)null); - }).Switch(); - } - - private class UpdateSignal : SingleSubscriberObservableBase - { - private readonly IAvaloniaObject _target; - private readonly AvaloniaProperty _property; - - public UpdateSignal(IAvaloniaObject target, AvaloniaProperty property) - { - _target = target; - _property = property; - } - - protected override void Subscribed() - { - _target.PropertyChanged += PropertyChanged; - } - - protected override void Unsubscribed() - { - _target.PropertyChanged -= PropertyChanged; - } - - private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) - { - if (e.Property == _property) - { - PublishNext(Unit.Default); - } - } } } } diff --git a/src/Markup/Avalonia.Markup/Data/BindingBase.cs b/src/Markup/Avalonia.Markup/Data/BindingBase.cs new file mode 100644 index 00000000000..3dbc83a7dfe --- /dev/null +++ b/src/Markup/Avalonia.Markup/Data/BindingBase.cs @@ -0,0 +1,289 @@ + +using System; +using System.Linq; +using System.Reactive; +using System.Reactive.Linq; +using Avalonia.Controls; +using Avalonia.Data.Converters; +using Avalonia.Data.Core; +using Avalonia.LogicalTree; +using Avalonia.Markup.Parsers; +using Avalonia.Reactive; +using Avalonia.VisualTree; + + +namespace Avalonia.Data +{ + public abstract class BindingBase : IBinding + { + /// + /// Initializes a new instance of the class. + /// + public BindingBase() + { + FallbackValue = AvaloniaProperty.UnsetValue; + TargetNullValue = AvaloniaProperty.UnsetValue; + } + + /// + /// Initializes a new instance of the class. + /// + /// The binding mode. + public BindingBase(BindingMode mode = BindingMode.Default) + :this() + { + Mode = mode; + } + + /// + /// Gets or sets the to use. + /// + public IValueConverter Converter { get; set; } + + /// + /// Gets or sets a parameter to pass to . + /// + public object ConverterParameter { get; set; } + + /// + /// Gets or sets the value to use when the binding is unable to produce a value. + /// + public object FallbackValue { get; set; } + + /// + /// Gets or sets the value to use when the binding result is null. + /// + public object TargetNullValue { get; set; } + + /// + /// Gets or sets the binding mode. + /// + public BindingMode Mode { get; set; } + + /// + /// Gets or sets the binding priority. + /// + public BindingPriority Priority { get; set; } + + /// + /// Gets or sets the string format. + /// + public string StringFormat { get; set; } + + public WeakReference DefaultAnchor { get; set; } + + public WeakReference NameScope { get; set; } + + protected abstract ExpressionObserver CreateExpressionObserver( + IAvaloniaObject target, + AvaloniaProperty targetProperty, + object anchor, + bool enableDataValidation); + + /// + public InstancedBinding Initiate( + IAvaloniaObject target, + AvaloniaProperty targetProperty, + object anchor = null, + bool enableDataValidation = false) + { + Contract.Requires(target != null); + anchor = anchor ?? DefaultAnchor?.Target; + + enableDataValidation = enableDataValidation && Priority == BindingPriority.LocalValue; + + var observer = CreateExpressionObserver(target, targetProperty, anchor, enableDataValidation); + + var fallback = FallbackValue; + + // If we're binding to DataContext and our fallback is UnsetValue then override + // the fallback value to null, as broken bindings to DataContext must reset the + // DataContext in order to not propagate incorrect DataContexts to child controls. + // See Avalonia.Markup.UnitTests.Data.DataContext_Binding_Should_Produce_Correct_Results. + if (targetProperty == StyledElement.DataContextProperty && fallback == AvaloniaProperty.UnsetValue) + { + fallback = null; + } + + var converter = Converter; + var targetType = targetProperty?.PropertyType ?? typeof(object); + + // We only respect `StringFormat` if the type of the property we're assigning to will + // accept a string. Note that this is slightly different to WPF in that WPF only applies + // `StringFormat` for target type `string` (not `object`). + if (!string.IsNullOrWhiteSpace(StringFormat) && + (targetType == typeof(string) || targetType == typeof(object))) + { + converter = new StringFormatValueConverter(StringFormat, converter); + } + + var subject = new BindingExpression( + observer, + targetType, + fallback, + TargetNullValue, + converter ?? DefaultValueConverter.Instance, + ConverterParameter, + Priority); + + return new InstancedBinding(subject, Mode, Priority); + } + + protected ExpressionObserver CreateDataContextObserver( + IAvaloniaObject target, + ExpressionNode node, + bool targetIsDataContext, + object anchor) + { + Contract.Requires(target != null); + + if (!(target is IDataContextProvider)) + { + target = anchor as IDataContextProvider; + + if (target == null) + { + throw new InvalidOperationException("Cannot find a DataContext to bind to."); + } + } + + if (!targetIsDataContext) + { + var result = new ExpressionObserver( + () => target.GetValue(StyledElement.DataContextProperty), + node, + new UpdateSignal(target, StyledElement.DataContextProperty), + null); + + return result; + } + else + { + return new ExpressionObserver( + GetParentDataContext(target), + node, + null); + } + } + + protected ExpressionObserver CreateElementObserver( + IStyledElement target, + string elementName, + ExpressionNode node) + { + Contract.Requires(target != null); + + NameScope.TryGetTarget(out var scope); + if (scope == null) + throw new InvalidOperationException("Name scope is null or was already collected"); + var result = new ExpressionObserver( + NameScopeLocator.Track(scope, elementName), + node, + null); + return result; + } + + protected ExpressionObserver CreateFindAncestorObserver( + IStyledElement target, + RelativeSource relativeSource, + ExpressionNode node) + { + Contract.Requires(target != null); + + IObservable controlLocator; + + switch (relativeSource.Tree) + { + case TreeType.Logical: + controlLocator = ControlLocator.Track( + (ILogical)target, + relativeSource.AncestorLevel - 1, + relativeSource.AncestorType); + break; + case TreeType.Visual: + controlLocator = VisualLocator.Track( + (IVisual)target, + relativeSource.AncestorLevel - 1, + relativeSource.AncestorType); + break; + default: + throw new InvalidOperationException("Invalid tree to traverse."); + } + + return new ExpressionObserver( + controlLocator, + node, + null); + } + + protected ExpressionObserver CreateSourceObserver( + object source, + ExpressionNode node) + { + Contract.Requires(source != null); + + return new ExpressionObserver(source, node); + } + + protected ExpressionObserver CreateTemplatedParentObserver( + IAvaloniaObject target, + ExpressionNode node) + { + Contract.Requires(target != null); + + var result = new ExpressionObserver( + () => target.GetValue(StyledElement.TemplatedParentProperty), + node, + new UpdateSignal(target, StyledElement.TemplatedParentProperty), + null); + + return result; + } + + protected IObservable GetParentDataContext(IAvaloniaObject target) + { + // The DataContext is based on the visual parent and not the logical parent: this may + // seem counter intuitive considering the fact that property inheritance works on the logical + // tree, but consider a ContentControl with a ContentPresenter. The ContentControl's + // Content property is bound to a value which becomes the ContentPresenter's + // DataContext - it is from this that the child hosted by the ContentPresenter needs to + // inherit its DataContext. + return target.GetObservable(Visual.VisualParentProperty) + .Select(x => + { + return (x as IAvaloniaObject)?.GetObservable(StyledElement.DataContextProperty) ?? + Observable.Return((object)null); + }).Switch(); + } + + private class UpdateSignal : SingleSubscriberObservableBase + { + private readonly IAvaloniaObject _target; + private readonly AvaloniaProperty _property; + + public UpdateSignal(IAvaloniaObject target, AvaloniaProperty property) + { + _target = target; + _property = property; + } + + protected override void Subscribed() + { + _target.PropertyChanged += PropertyChanged; + } + + protected override void Unsubscribed() + { + _target.PropertyChanged -= PropertyChanged; + } + + private void PropertyChanged(object sender, AvaloniaPropertyChangedEventArgs e) + { + if (e.Property == _property) + { + PublishNext(Unit.Default); + } + } + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs index c3651594e93..e858bacb743 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs @@ -4,7 +4,10 @@ namespace Avalonia.Markup.Parsers { - internal static class ArgumentListParser +#if !BUILDTASK + public +#endif + static class ArgumentListParser { public static IList ParseArguments(this ref CharacterReader r, char open, char close, char delimiter = ',') { diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs new file mode 100644 index 00000000000..8de64e56ff5 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/BindingExpressionGrammar.cs @@ -0,0 +1,387 @@ +// Copyright (c) The Avalonia Project. All rights reserved. +// Licensed under the MIT license. See licence.md file in the project root for full license information. + +using Avalonia.Data.Core; +using Avalonia.Utilities; +using System; +using System.Collections.Generic; + +namespace Avalonia.Markup.Parsers +{ + internal enum SourceMode + { + Data, + Control + } + + internal static class BindingExpressionGrammar + { + public static (IList Nodes, SourceMode Mode) Parse(ref CharacterReader r) + { + var nodes = new List(); + var state = State.Start; + var mode = SourceMode.Data; + + while (!r.End && state != State.End) + { + switch (state) + { + case State.Start: + state = ParseStart(ref r, nodes); + break; + + case State.AfterMember: + state = ParseAfterMember(ref r, nodes); + break; + + case State.BeforeMember: + state = ParseBeforeMember(ref r, nodes); + break; + + case State.AttachedProperty: + state = ParseAttachedProperty(ref r, nodes); + break; + + case State.Indexer: + state = ParseIndexer(ref r, nodes); + break; + + case State.ElementName: + state = ParseElementName(ref r, nodes); + mode = SourceMode.Control; + break; + + case State.RelativeSource: + state = ParseRelativeSource(ref r, nodes); + mode = SourceMode.Control; + break; + } + } + + if (state == State.BeforeMember) + { + throw new ExpressionParseException(r.Position, "Unexpected end of expression."); + } + + return (nodes, mode); + } + + private static State ParseStart(ref CharacterReader r, IList nodes) + { + if (ParseNot(ref r)) + { + nodes.Add(new NotNode()); + return State.Start; + } + + else if (ParseSharp(ref r)) + { + return State.ElementName; + } + else if (ParseDollarSign(ref r)) + { + return State.RelativeSource; + } + else if (ParseOpenBrace(ref r)) + { + return State.AttachedProperty; + } + else if (PeekOpenBracket(ref r)) + { + return State.Indexer; + } + else if (ParseDot(ref r)) + { + nodes.Add(new EmptyExpressionNode()); + return State.End; + } + else + { + var identifier = r.ParseIdentifier(); + + if (!identifier.IsEmpty) + { + nodes.Add(new PropertyNameNode { PropertyName = identifier.ToString() }); + return State.AfterMember; + } + } + + return State.End; + } + + private static State ParseAfterMember(ref CharacterReader r, IList nodes) + { + if (ParseMemberAccessor(ref r)) + { + return State.BeforeMember; + } + else if (ParseStreamOperator(ref r)) + { + nodes.Add(new StreamNode()); + return State.AfterMember; + } + else if (PeekOpenBracket(ref r)) + { + return State.Indexer; + } + + return State.End; + } + + private static State ParseBeforeMember(ref CharacterReader r, IList nodes) + { + if (ParseOpenBrace(ref r)) + { + return State.AttachedProperty; + } + else + { + var identifier = r.ParseIdentifier(); + + if (!identifier.IsEmpty) + { + nodes.Add(new PropertyNameNode { PropertyName = identifier.ToString() }); + return State.AfterMember; + } + + return State.End; + } + } + + private static State ParseAttachedProperty(ref CharacterReader r, List nodes) + { + var (ns, owner) = ParseTypeName(ref r); + + if (r.End || !r.TakeIf('.')) + { + throw new ExpressionParseException(r.Position, "Invalid attached property name."); + } + + var name = r.ParseIdentifier(); + + if (r.End || !r.TakeIf(')')) + { + throw new ExpressionParseException(r.Position, "Expected ')'."); + } + + nodes.Add(new AttachedPropertyNameNode + { + Namespace = ns.ToString(), + TypeName = owner.ToString(), + PropertyName = name.ToString() + }); + return State.AfterMember; + } + + private static State ParseIndexer(ref CharacterReader r, List nodes) + { + var args = r.ParseArguments('[', ']'); + + if (args.Count == 0) + { + throw new ExpressionParseException(r.Position, "Indexer may not be empty."); + } + + nodes.Add(new IndexerNode { Arguments = args }); + return State.AfterMember; + } + + private static State ParseElementName(ref CharacterReader r, List nodes) + { + var name = r.ParseIdentifier(); + + if (name.IsEmpty) + { + throw new ExpressionParseException(r.Position, "Element name expected after '#'."); + } + + nodes.Add(new NameNode { Name = name.ToString() }); + return State.AfterMember; + } + + private static State ParseRelativeSource(ref CharacterReader r, List nodes) + { + var mode = r.ParseIdentifier(); + + if (mode.SequenceEqual("self".AsSpan())) + { + nodes.Add(new SelfNode()); + } + else if (mode.SequenceEqual("parent".AsSpan())) + { + string ancestorNamespace = null; + string ancestorType = null; + var ancestorLevel = 0; + if (PeekOpenBracket(ref r)) + { + var args = r.ParseArguments('[', ']', ';'); + if (args.Count > 2 || args.Count == 0) + { + throw new ExpressionParseException(r.Position, "Too many arguments in RelativeSource syntax sugar"); + } + else if (args.Count == 1) + { + if (int.TryParse(args[0], out int level)) + { + ancestorType = null; + ancestorLevel = level; + } + else + { + var reader = new CharacterReader(args[0].AsSpan()); + (ancestorNamespace, ancestorType) = ParseTypeName(ref reader); + } + } + else + { + var reader = new CharacterReader(args[0].AsSpan()); + (ancestorNamespace, ancestorType) = ParseTypeName(ref reader); + ancestorLevel = int.Parse(args[1]); + } + } + nodes.Add(new AncestorNode + { + Namespace = ancestorNamespace, + TypeName = ancestorType, + Level = ancestorLevel + }); + } + else + { + throw new ExpressionParseException(r.Position, "Unknown RelativeSource mode."); + } + + return State.AfterMember; + } + + private static TypeName ParseTypeName(ref CharacterReader r) + { + ReadOnlySpan ns, typeName; + ns = ReadOnlySpan.Empty; + var typeNameOrNamespace = r.ParseIdentifier(); + + if (!r.End && r.TakeIf(':')) + { + ns = typeNameOrNamespace; + typeName = r.ParseIdentifier(); + } + else + { + typeName = typeNameOrNamespace; + } + + return new TypeName(ns, typeName); + } + + private static bool ParseNot(ref CharacterReader r) + { + return !r.End && r.TakeIf('!'); + } + + private static bool ParseMemberAccessor(ref CharacterReader r) + { + return !r.End && r.TakeIf('.'); + } + + private static bool ParseOpenBrace(ref CharacterReader r) + { + return !r.End && r.TakeIf('('); + } + + private static bool PeekOpenBracket(ref CharacterReader r) + { + return !r.End && r.Peek == '['; + } + + private static bool ParseStreamOperator(ref CharacterReader r) + { + return !r.End && r.TakeIf('^'); + } + + private static bool ParseDollarSign(ref CharacterReader r) + { + return !r.End && r.TakeIf('$'); + } + + private static bool ParseSharp(ref CharacterReader r) + { + return !r.End && r.TakeIf('#'); + } + + private static bool ParseDot(ref CharacterReader r) + { + return !r.End && r.TakeIf('.'); + } + + private enum State + { + Start, + RelativeSource, + ElementName, + AfterMember, + BeforeMember, + AttachedProperty, + Indexer, + End, + } + + private readonly ref struct TypeName + { + public TypeName(ReadOnlySpan ns, ReadOnlySpan typeName) + { + Namespace = ns; + Type = typeName; + } + + public readonly ReadOnlySpan Namespace; + public readonly ReadOnlySpan Type; + + public void Deconstruct(out string ns, out string typeName) + { + ns = Namespace.ToString(); + typeName = Type.ToString(); + } + } + + public interface INode {} + + public interface ITransformNode {} + + public class EmptyExpressionNode : INode { } + + public class PropertyNameNode : INode + { + public string PropertyName { get; set; } + } + + public class AttachedPropertyNameNode : INode + { + public string Namespace { get; set; } + public string TypeName { get; set; } + public string PropertyName { get; set; } + } + + public class IndexerNode : INode + { + public IList Arguments { get; set; } + } + + public class NotNode : INode, ITransformNode {} + + public class StreamNode : INode {} + + public class SelfNode : INode {} + + public class NameNode : INode + { + public string Name { get; set; } + } + + public class AncestorNode : INode + { + public string Namespace { get; set; } + public string TypeName { get; set; } + public int Level { get; set; } + } + } +} diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs index ae9f3388064..1048148c1f1 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -8,12 +8,6 @@ namespace Avalonia.Markup.Parsers { - internal enum SourceMode - { - Data, - Control - } - internal class ExpressionParser { private readonly bool _enableValidation; @@ -29,332 +23,85 @@ public ExpressionParser(bool enableValidation, Func typeRe public (ExpressionNode Node, SourceMode Mode) Parse(ref CharacterReader r) { - var nodes = new List(); - var state = State.Start; - var mode = SourceMode.Data; + ExpressionNode rootNode = null; + ExpressionNode node = null; + var (astNodes, mode) = BindingExpressionGrammar.Parse(ref r); - while (!r.End && state != State.End) + foreach (var astNode in astNodes) { - switch (state) + ExpressionNode nextNode = null; + switch (astNode) { - case State.Start: - state = ParseStart(ref r, nodes); + case BindingExpressionGrammar.EmptyExpressionNode _: + nextNode = new EmptyExpressionNode(); break; - - case State.AfterMember: - state = ParseAfterMember(ref r, nodes); + case BindingExpressionGrammar.NotNode _: + nextNode = new LogicalNotNode(); break; - - case State.BeforeMember: - state = ParseBeforeMember(ref r, nodes); + case BindingExpressionGrammar.StreamNode _: + nextNode = new StreamNode(); break; - - case State.AttachedProperty: - state = ParseAttachedProperty(ref r, nodes); + case BindingExpressionGrammar.PropertyNameNode propName: + nextNode = new PropertyAccessorNode(propName.PropertyName, _enableValidation); break; - - case State.Indexer: - state = ParseIndexer(ref r, nodes); + case BindingExpressionGrammar.IndexerNode indexer: + nextNode = new StringIndexerNode(indexer.Arguments); break; - - case State.ElementName: - state = ParseElementName(ref r, nodes); - mode = SourceMode.Control; + case BindingExpressionGrammar.AttachedPropertyNameNode attachedProp: + nextNode = ParseAttachedProperty(attachedProp); break; - - case State.RelativeSource: - state = ParseRelativeSource(ref r, nodes); - mode = SourceMode.Control; + case BindingExpressionGrammar.SelfNode _: + nextNode = new SelfNode(); + break; + case BindingExpressionGrammar.AncestorNode ancestor: + nextNode = ParseFindAncestor(ancestor); + break; + case BindingExpressionGrammar.NameNode elementName: + nextNode = new ElementNameNode(_nameScope, elementName.Name); break; } - } - - if (state == State.BeforeMember) - { - throw new ExpressionParseException(r.Position, "Unexpected end of expression."); - } - - for (int n = 0; n < nodes.Count - 1; ++n) - { - nodes[n].Next = nodes[n + 1]; - } - - return (nodes.FirstOrDefault(), mode); - } - - private State ParseStart(ref CharacterReader r, IList nodes) - { - if (ParseNot(ref r)) - { - nodes.Add(new LogicalNotNode()); - return State.Start; - } - - else if (ParseSharp(ref r)) - { - return State.ElementName; - } - else if (ParseDollarSign(ref r)) - { - return State.RelativeSource; - } - else if (ParseOpenBrace(ref r)) - { - return State.AttachedProperty; - } - else if (PeekOpenBracket(ref r)) - { - return State.Indexer; - } - else if (ParseDot(ref r)) - { - nodes.Add(new EmptyExpressionNode()); - return State.End; - } - else - { - var identifier = r.ParseIdentifier(); - - if (!identifier.IsEmpty) + if (rootNode is null) { - nodes.Add(new PropertyAccessorNode(identifier.ToString(), _enableValidation)); - return State.AfterMember; + rootNode = node = nextNode; } - } - - return State.End; - } - - private static State ParseAfterMember(ref CharacterReader r, IList nodes) - { - if (ParseMemberAccessor(ref r)) - { - return State.BeforeMember; - } - else if (ParseStreamOperator(ref r)) - { - nodes.Add(new StreamNode()); - return State.AfterMember; - } - else if (PeekOpenBracket(ref r)) - { - return State.Indexer; - } - - return State.End; - } - - private State ParseBeforeMember(ref CharacterReader r, IList nodes) - { - if (ParseOpenBrace(ref r)) - { - return State.AttachedProperty; - } - else - { - var identifier = r.ParseIdentifier(); - - if (!identifier.IsEmpty) + else { - nodes.Add(new PropertyAccessorNode(identifier.ToString(), _enableValidation)); - return State.AfterMember; + node.Next = nextNode; + node = nextNode; } - - return State.End; - } - } - - private State ParseAttachedProperty(ref CharacterReader r, List nodes) - { - var (ns, owner) = ParseTypeName(ref r); - - if (r.End || !r.TakeIf('.')) - { - throw new ExpressionParseException(r.Position, "Invalid attached property name."); - } - - var name = r.ParseIdentifier(); - - if (r.End || !r.TakeIf(')')) - { - throw new ExpressionParseException(r.Position, "Expected ')'."); - } - - if (_typeResolver == null) - { - throw new InvalidOperationException("Cannot parse a binding path with an attached property without a type resolver. Maybe you can use a LINQ Expression binding path instead?"); - } - - var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(ns.ToString(), owner.ToString()), name.ToString()); - - nodes.Add(new AvaloniaPropertyAccessorNode(property, _enableValidation)); - return State.AfterMember; - } - - private State ParseIndexer(ref CharacterReader r, List nodes) - { - var args = r.ParseArguments('[', ']'); - - if (args.Count == 0) - { - throw new ExpressionParseException(r.Position, "Indexer may not be empty."); } - nodes.Add(new StringIndexerNode(args)); - return State.AfterMember; + return (rootNode, mode); } - private State ParseElementName(ref CharacterReader r, List nodes) + private FindAncestorNode ParseFindAncestor(BindingExpressionGrammar.AncestorNode node) { - var name = r.ParseIdentifier(); + Type ancestorType = null; + var ancestorLevel = node.Level; - if (name == null) + if (!(node.Namespace is null) && !(node.TypeName is null)) { - throw new ExpressionParseException(r.Position, "Element name expected after '#'."); - } - - nodes.Add(new ElementNameNode(_nameScope, name.ToString())); - return State.AfterMember; - } - - private State ParseRelativeSource(ref CharacterReader r, List nodes) - { - var mode = r.ParseIdentifier(); - - if (mode.Equals("self".AsSpan(), StringComparison.InvariantCulture)) - { - nodes.Add(new SelfNode()); - } - else if (mode.Equals("parent".AsSpan(), StringComparison.InvariantCulture)) - { - Type ancestorType = null; - var ancestorLevel = 0; - if (PeekOpenBracket(ref r)) + if (_typeResolver == null) { - var args = r.ParseArguments('[', ']', ';'); - if (args.Count > 2 || args.Count == 0) - { - throw new ExpressionParseException(r.Position, "Too many arguments in RelativeSource syntax sugar"); - } - else if (args.Count == 1) - { - if (int.TryParse(args[0], out int level)) - { - ancestorType = null; - ancestorLevel = level; - } - else - { - var reader = new CharacterReader(args[0].AsSpan()); - var typeName = ParseTypeName(ref reader); - ancestorType = _typeResolver(typeName.Namespace.ToString(), typeName.Type.ToString()); - } - } - else - { - var reader = new CharacterReader(args[0].AsSpan()); - var typeName = ParseTypeName(ref reader); - ancestorType = _typeResolver(typeName.Namespace.ToString(), typeName.Type.ToString()); - ancestorLevel = int.Parse(args[1]); - } + throw new InvalidOperationException("Cannot parse a binding path with a typed FindAncestor without a type resolver. Maybe you can use a LINQ Expression binding path instead?"); } - nodes.Add(new FindAncestorNode(ancestorType, ancestorLevel)); - } - else - { - throw new ExpressionParseException(r.Position, "Unknown RelativeSource mode."); - } - return State.AfterMember; - } - - private static TypeName ParseTypeName(ref CharacterReader r) - { - ReadOnlySpan ns, typeName; - ns = ReadOnlySpan.Empty; - var typeNameOrNamespace = r.ParseIdentifier(); - - if (!r.End && r.TakeIf(':')) - { - ns = typeNameOrNamespace; - typeName = r.ParseIdentifier(); + ancestorType = _typeResolver(node.Namespace, node.TypeName); } - else - { - typeName = typeNameOrNamespace; - } - - return new TypeName(ns, typeName); - } - - private static bool ParseNot(ref CharacterReader r) - { - return !r.End && r.TakeIf('!'); - } - - private static bool ParseMemberAccessor(ref CharacterReader r) - { - return !r.End && r.TakeIf('.'); - } - - private static bool ParseOpenBrace(ref CharacterReader r) - { - return !r.End && r.TakeIf('('); - } - - private static bool PeekOpenBracket(ref CharacterReader r) - { - return !r.End && r.Peek == '['; - } - - private static bool ParseStreamOperator(ref CharacterReader r) - { - return !r.End && r.TakeIf('^'); - } - - private static bool ParseDollarSign(ref CharacterReader r) - { - return !r.End && r.TakeIf('$'); - } - - private static bool ParseSharp(ref CharacterReader r) - { - return !r.End && r.TakeIf('#'); - } - private static bool ParseDot(ref CharacterReader r) - { - return !r.End && r.TakeIf('.'); - } - - private enum State - { - Start, - RelativeSource, - ElementName, - AfterMember, - BeforeMember, - AttachedProperty, - Indexer, - End, + return new FindAncestorNode(ancestorType, ancestorLevel); } - private readonly ref struct TypeName + private AvaloniaPropertyAccessorNode ParseAttachedProperty(BindingExpressionGrammar.AttachedPropertyNameNode node) { - public TypeName(ReadOnlySpan ns, ReadOnlySpan typeName) + if (_typeResolver == null) { - Namespace = ns; - Type = typeName; + throw new InvalidOperationException("Cannot parse a binding path with an attached property without a type resolver. Maybe you can use a LINQ Expression binding path instead?"); } - public readonly ReadOnlySpan Namespace; - public readonly ReadOnlySpan Type; + var property = AvaloniaPropertyRegistry.Instance.FindRegistered(_typeResolver(node.Namespace, node.TypeName), node.PropertyName); - public void Deconstruct(out ReadOnlySpan ns, out ReadOnlySpan typeName) - { - ns = Namespace; - typeName = Type; - } + return new AvaloniaPropertyAccessorNode(property, _enableValidation); } } } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs index 7eec80fc002..97198145a8e 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/ElementNameNode.cs @@ -5,7 +5,7 @@ namespace Avalonia.Markup.Parsers.Nodes { - internal class ElementNameNode : ExpressionNode + public class ElementNameNode : ExpressionNode { private readonly WeakReference _nameScope; private readonly string _name; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs index 321a85c1d73..f304d1e9a2b 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/FindAncestorNode.cs @@ -4,7 +4,7 @@ namespace Avalonia.Markup.Parsers.Nodes { - internal class FindAncestorNode : ExpressionNode + public class FindAncestorNode : ExpressionNode { private readonly int _level; private readonly Type _ancestorType; diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs index 2cb87efa65f..1cd233c68a6 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/Nodes/SelfNode.cs @@ -2,7 +2,7 @@ namespace Avalonia.Markup.Parsers.Nodes { - internal class SelfNode : ExpressionNode + public class SelfNode : ExpressionNode { public override string Description => "$self"; } diff --git a/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs new file mode 100644 index 00000000000..c5953b514c3 --- /dev/null +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/PropertyPathGrammar.cs @@ -0,0 +1,233 @@ +using System; +using System.Collections.Generic; +using Avalonia.Data.Core; +using Avalonia.Utilities; + +namespace Avalonia.Markup.Parsers +{ +#if !BUILDTASK + public +#endif + class PropertyPathGrammar + { + private enum State + { + Start, + Next, + AfterProperty, + End + } + + public static IEnumerable Parse(string s) + { + var r = new CharacterReader(s.AsSpan()); + return Parse(ref r); + } + + private static IEnumerable Parse(ref CharacterReader r) + { + var state = State.Start; + var parsed = new List(); + while (state != State.End) + { + ISyntax syntax = null; + if (state == State.Start) + (state, syntax) = ParseStart(ref r); + else if (state == State.Next) + (state, syntax) = ParseNext(ref r); + else if (state == State.AfterProperty) + (state, syntax) = ParseAfterProperty(ref r); + + + if (syntax != null) + { + parsed.Add(syntax); + } + } + + if (state != State.End && r.End) + { + throw new ExpressionParseException(r.Position, "Unexpected end of property path"); + } + + return parsed; + } + + private static (State, ISyntax) ParseNext(ref CharacterReader r) + { + r.SkipWhitespace(); + if (r.End) + return (State.End, null); + + return ParseStart(ref r); + } + + private static (State, ISyntax) ParseStart(ref CharacterReader r) + { + if (TryParseCasts(ref r, out var rv)) + return rv; + r.SkipWhitespace(); + + if (r.TakeIf('(')) + return ParseTypeQualifiedProperty(ref r); + + return ParseProperty(ref r); + } + + private static (State, ISyntax) ParseTypeQualifiedProperty(ref CharacterReader r) + { + r.SkipWhitespace(); + const string error = + "Unable to parse qualified property name, expected `(ns:TypeName.PropertyName)` or `(TypeName.PropertyName)` after `(`"; + + var typeName = ParseXamlIdentifier(ref r); + + + if (!r.TakeIf('.')) + throw new ExpressionParseException(r.Position, error); + + var propertyName = r.ParseIdentifier(); + if (propertyName.IsEmpty) + throw new ExpressionParseException(r.Position, error); + + r.SkipWhitespace(); + if (!r.TakeIf(')')) + throw new ExpressionParseException(r.Position, + "Expected ')' after qualified property name " + + typeName.ns + ':' + typeName.name + + "." + propertyName.ToString()); + + return (State.AfterProperty, + new TypeQualifiedPropertySyntax + { + Name = propertyName.ToString(), + TypeName = typeName.name, + TypeNamespace = typeName.ns + }); + } + + static (string ns, string name) ParseXamlIdentifier(ref CharacterReader r) + { + var ident = r.ParseIdentifier(); + if (ident.IsEmpty) + throw new ExpressionParseException(r.Position, "Expected identifier"); + if (r.TakeIf(':')) + { + var part2 = r.ParseIdentifier(); + if (part2.IsEmpty) + throw new ExpressionParseException(r.Position, + "Expected the rest of the identifier after " + ident.ToString() + ":"); + return (ident.ToString(), part2.ToString()); + } + + return (null, ident.ToString()); + } + + private static (State, ISyntax) ParseProperty(ref CharacterReader r) + { + r.SkipWhitespace(); + var prop = r.ParseIdentifier(); + if (prop.IsEmpty) + throw new ExpressionParseException(r.Position, "Unable to parse property name"); + return (State.AfterProperty, new PropertySyntax {Name = prop.ToString()}); + } + + private static bool TryParseCasts(ref CharacterReader r, out (State, ISyntax) rv) + { + if (r.TakeIfKeyword(":=")) + rv = ParseEnsureType(ref r); + else if (r.TakeIfKeyword(":>") || r.TakeIfKeyword("as ")) + rv = ParseCastType(ref r); + else + { + rv = default; + return false; + } + + return true; + } + + private static (State, ISyntax) ParseAfterProperty(ref CharacterReader r) + { + if (TryParseCasts(ref r, out var rv)) + return rv; + + r.SkipWhitespace(); + if (r.End) + return (State.End, null); + if (r.TakeIf('.')) + return (State.Next, ChildTraversalSyntax.Instance); + + + + throw new ExpressionParseException(r.Position, "Unexpected character " + r.Peek + " after property name"); + } + + private static (State, ISyntax) ParseEnsureType(ref CharacterReader r) + { + r.SkipWhitespace(); + var type = ParseXamlIdentifier(ref r); + return (State.AfterProperty, new EnsureTypeSyntax {TypeName = type.name, TypeNamespace = type.ns}); + } + + private static (State, ISyntax) ParseCastType(ref CharacterReader r) + { + r.SkipWhitespace(); + var type = ParseXamlIdentifier(ref r); + return (State.AfterProperty, new CastTypeSyntax {TypeName = type.name, TypeNamespace = type.ns}); + } + + public interface ISyntax + { + + } + + public class PropertySyntax : ISyntax + { + public string Name { get; set; } + + public override bool Equals(object obj) + => obj is PropertySyntax other + && other.Name == Name; + } + + public class TypeQualifiedPropertySyntax : ISyntax + { + public string Name { get; set; } + public string TypeName { get; set; } + public string TypeNamespace { get; set; } + + public override bool Equals(object obj) + => obj is TypeQualifiedPropertySyntax other + && other.Name == Name + && other.TypeName == TypeName + && other.TypeNamespace == TypeNamespace; + } + + public class ChildTraversalSyntax : ISyntax + { + public static ChildTraversalSyntax Instance { get; } = new ChildTraversalSyntax(); + public override bool Equals(object obj) => obj is ChildTraversalSyntax; + } + + public class EnsureTypeSyntax : ISyntax + { + public string TypeName { get; set; } + public string TypeNamespace { get; set; } + public override bool Equals(object obj) + => obj is EnsureTypeSyntax other + && other.TypeName == TypeName + && other.TypeNamespace == TypeNamespace; + } + + public class CastTypeSyntax : ISyntax + { + public string TypeName { get; set; } + public string TypeNamespace { get; set; } + public override bool Equals(object obj) + => obj is CastTypeSyntax other + && other.TypeName == TypeName + && other.TypeNamespace == TypeNamespace; + } + } +} diff --git a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs index 03832d3063b..d574732e3db 100644 --- a/src/Shared/PlatformSupport/StandardRuntimePlatform.cs +++ b/src/Shared/PlatformSupport/StandardRuntimePlatform.cs @@ -86,11 +86,15 @@ public void Dispose() #if DEBUG if (Thread.CurrentThread.ManagedThreadId == GCThread?.ManagedThreadId) { - lock(_lock) + lock (_lock) + { if (!IsDisposed) + { Console.Error.WriteLine("Native blob disposal from finalizer thread\nBacktrace: " - + Environment.StackTrace - + "\n\nBlob created by " + _backtrace); + + Environment.StackTrace + + "\n\nBlob created by " + _backtrace); + } + } } #endif DoDispose(); diff --git a/src/Skia/Avalonia.Skia/Assets/NoiseAsset_256X256_PNG.png b/src/Skia/Avalonia.Skia/Assets/NoiseAsset_256X256_PNG.png new file mode 100644 index 00000000000..41de173d901 Binary files /dev/null and b/src/Skia/Avalonia.Skia/Assets/NoiseAsset_256X256_PNG.png differ diff --git a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj index 68da513528a..d5ad70b9440 100644 --- a/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj +++ b/src/Skia/Avalonia.Skia/Avalonia.Skia.csproj @@ -7,6 +7,9 @@ true true + + + diff --git a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs index ae756f4eabc..d818e683c39 100644 --- a/src/Skia/Avalonia.Skia/DrawingContextImpl.cs +++ b/src/Skia/Avalonia.Skia/DrawingContextImpl.cs @@ -17,7 +17,7 @@ namespace Avalonia.Skia /// /// Skia based drawing context. /// - internal class DrawingContextImpl : IDrawingContextImpl, ISkiaDrawingContextImpl + internal class DrawingContextImpl : IDrawingContextImpl, ISkiaDrawingContextImpl, IDrawingContextWithAcrylicLikeSupport { private IDisposable[] _disposables; private readonly Vector _dpi; @@ -34,6 +34,7 @@ internal class DrawingContextImpl : IDrawingContextImpl, ISkiaDrawingContextImpl private readonly SKPaint _strokePaint = new SKPaint(); private readonly SKPaint _fillPaint = new SKPaint(); private readonly SKPaint _boxShadowPaint = new SKPaint(); + private static SKShader s_acrylicNoiseShader; /// /// Context create info. @@ -227,6 +228,41 @@ SKRect AreaCastingShadowInHole( return bounds; } + /// + public void DrawRectangle(IExperimentalAcrylicMaterial material, RoundedRect rect) + { + if (rect.Rect.Height <= 0 || rect.Rect.Width <= 0) + return; + + var rc = rect.Rect.ToSKRect(); + var isRounded = rect.IsRounded; + var needRoundRect = rect.IsRounded; + using var skRoundRect = needRoundRect ? new SKRoundRect() : null; + + if (needRoundRect) + skRoundRect.SetRectRadii(rc, + new[] + { + rect.RadiiTopLeft.ToSKPoint(), rect.RadiiTopRight.ToSKPoint(), + rect.RadiiBottomRight.ToSKPoint(), rect.RadiiBottomLeft.ToSKPoint(), + }); + + if (material != null) + { + using (var paint = CreateAcrylicPaint(_fillPaint, material)) + { + if (isRounded) + { + Canvas.DrawRoundRect(skRoundRect, paint.Paint); + } + else + { + Canvas.DrawRect(rc, paint.Paint); + } + + } + } + } /// public void DrawRectangle(IBrush brush, IPen pen, RoundedRect rect, BoxShadows boxShadows = default) @@ -593,8 +629,8 @@ private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Size targetSize, var tileTransform = tileBrush.TileMode != TileMode.None - ? SKMatrix.MakeTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y) - : SKMatrix.MakeIdentity(); + ? SKMatrix.CreateTranslation(-(float)calc.DestinationRect.X, -(float)calc.DestinationRect.Y) + : SKMatrix.CreateIdentity(); SKShaderTileMode tileX = tileBrush.TileMode == TileMode.None @@ -619,7 +655,7 @@ private void ConfigureTileBrush(ref PaintWrapper paintWrapper, Size targetSize, SKMatrix.Concat( ref paintTransform, tileTransform, - SKMatrix.MakeScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y))); + SKMatrix.CreateScale((float)(96.0 / _dpi.X), (float)(96.0 / _dpi.Y))); using (var shader = image.ToShader(tileX, tileY, paintTransform)) { @@ -659,6 +695,86 @@ private void ConfigureVisualBrush(ref PaintWrapper paintWrapper, IVisualBrush vi } } + static SKColorFilter CreateAlphaColorFilter(double opacity) + { + if (opacity > 1) + opacity = 1; + var c = new byte[256]; + var a = new byte[256]; + for (var i = 0; i < 256; i++) + { + c[i] = (byte)i; + a[i] = (byte)(i * opacity); + } + + return SKColorFilter.CreateTable(a, c, c, c); + } + + static byte Blend(byte leftColor, byte leftAlpha, byte rightColor, byte rightAlpha) + { + var ca = leftColor / 255d; + var aa = leftAlpha / 255d; + var cb = rightColor / 255d; + var ab = rightAlpha / 255d; + var r = (ca * aa + cb * ab * (1 - aa)) / (aa + ab * (1 - aa)); + return (byte)(r * 255); + } + + static Color Blend(Color left, Color right) + { + var aa = left.A / 255d; + var ab = right.A / 255d; + return new Color( + (byte)((aa + ab * (1 - aa)) * 255), + Blend(left.R, left.A, right.R, right.A), + Blend(left.G, left.A, right.G, right.A), + Blend(left.B, left.A, right.B, right.A) + ); + } + + internal PaintWrapper CreateAcrylicPaint (SKPaint paint, IExperimentalAcrylicMaterial material, bool disposePaint = false) + { + var paintWrapper = new PaintWrapper(paint, disposePaint); + + paint.IsAntialias = true; + + double opacity = _currentOpacity; + + var tintOpacity = + material.BackgroundSource == AcrylicBackgroundSource.Digger ? + material.TintOpacity : 1; + + const double noiseOpcity = 0.0225; + + var tintColor = material.TintColor; + var tint = new SKColor(tintColor.R, tintColor.G, tintColor.B, tintColor.A); + + if (s_acrylicNoiseShader == null) + { + using (var stream = typeof(DrawingContextImpl).Assembly.GetManifestResourceStream("Avalonia.Skia.Assets.NoiseAsset_256X256_PNG.png")) + using (var bitmap = SKBitmap.Decode(stream)) + { + s_acrylicNoiseShader = SKShader.CreateBitmap(bitmap, SKShaderTileMode.Repeat, SKShaderTileMode.Repeat) + .WithColorFilter(CreateAlphaColorFilter(noiseOpcity)); + } + } + + using (var backdrop = SKShader.CreateColor(new SKColor(material.MaterialColor.R, material.MaterialColor.G, material.MaterialColor.B, material.MaterialColor.A))) + using (var tintShader = SKShader.CreateColor(tint)) + using (var effectiveTint = SKShader.CreateCompose(backdrop, tintShader)) + using (var compose = SKShader.CreateCompose(effectiveTint, s_acrylicNoiseShader)) + { + paint.Shader = compose; + + if (material.BackgroundSource == AcrylicBackgroundSource.Digger) + { + paint.BlendMode = SKBlendMode.Src; + } + + return paintWrapper; + } + } + /// /// Creates paint wrapper for given brush. /// @@ -811,7 +927,7 @@ private SurfaceRenderTarget CreateRenderTarget(Size size, PixelFormat? format = }; return new SurfaceRenderTarget(createInfo); - } + } /// /// Skia cached paint state. diff --git a/src/Skia/Avalonia.Skia/FontManagerImpl.cs b/src/Skia/Avalonia.Skia/FontManagerImpl.cs index 415a89e1c10..91bc9374752 100644 --- a/src/Skia/Avalonia.Skia/FontManagerImpl.cs +++ b/src/Skia/Avalonia.Skia/FontManagerImpl.cs @@ -29,7 +29,8 @@ public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = fa [ThreadStatic] private static string[] t_languageTagBuffer; - public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, + FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { SKFontStyle skFontStyle; @@ -80,7 +81,7 @@ public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fo continue; } - fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle); + fontKey = new FontKey(skTypeface.FamilyName, fontStyle, fontWeight); return true; } @@ -91,7 +92,7 @@ public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fo if (skTypeface != null) { - fontKey = new FontKey(skTypeface.FamilyName, fontWeight, fontStyle); + fontKey = new FontKey(skTypeface.FamilyName, fontStyle, fontWeight); return true; } diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 5f876464e23..5e630e54a60 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -35,6 +35,7 @@ public FormattedTextImpl( IsAntialias = true, LcdRenderText = true, SubpixelText = true, + IsLinearText = true, Typeface = glyphTypeface.Typeface, TextSize = (float)fontSize, TextAlign = textAlignment.ToSKTextAlign() diff --git a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs index 0fdea5ed400..f59a0a32c27 100644 --- a/src/Skia/Avalonia.Skia/GlyphRunImpl.cs +++ b/src/Skia/Avalonia.Skia/GlyphRunImpl.cs @@ -1,5 +1,6 @@ using System; using Avalonia.Platform; +using JetBrains.Annotations; using SkiaSharp; namespace Avalonia.Skia @@ -7,9 +8,9 @@ namespace Avalonia.Skia /// public class GlyphRunImpl : IGlyphRunImpl { - public GlyphRunImpl(SKTextBlob textBlob) + public GlyphRunImpl([NotNull] SKTextBlob textBlob) { - TextBlob = textBlob; + TextBlob = textBlob ?? throw new ArgumentNullException (nameof (textBlob)); } /// diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs index 9b731740069..e0b7019672f 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlRenderTarget.cs @@ -82,10 +82,10 @@ public ISkiaGpuRenderSession BeginRenderingSession() var renderTarget = new GRBackendRenderTarget(size.Width, size.Height, disp.SampleCount, disp.StencilSize, - new GRGlFramebufferInfo((uint)fb, GRPixelConfig.Rgba8888.ToGlSizedFormat())); + new GRGlFramebufferInfo((uint)fb, SKColorType.Rgba8888.ToGlSizedFormat())); var surface = SKSurface.Create(_grContext, renderTarget, glSession.IsYFlipped ? GRSurfaceOrigin.TopLeft : GRSurfaceOrigin.BottomLeft, - GRPixelConfig.Rgba8888.ToColorType()); + SKColorType.Rgba8888); success = true; return new GlGpuSession(_grContext, renderTarget, surface, glSession); diff --git a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs index de188f42bd8..9278de21374 100644 --- a/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs +++ b/src/Skia/Avalonia.Skia/Gpu/OpenGl/GlSkiaGpu.cs @@ -15,19 +15,18 @@ public GlSkiaGpu(IWindowingPlatformGlFeature gl, long? maxResourceBytes) using (context.MakeCurrent()) { using (var iface = context.Version.Type == GlProfileType.OpenGL ? - GRGlInterface.AssembleGlInterface((_, proc) => context.GlInterface.GetProcAddress(proc)) : - GRGlInterface.AssembleGlesInterface((_, proc) => context.GlInterface.GetProcAddress(proc))) + GRGlInterface.CreateOpenGl(proc => context.GlInterface.GetProcAddress(proc)) : + GRGlInterface.CreateGles(proc => context.GlInterface.GetProcAddress(proc))) { - _grContext = GRContext.Create(GRBackend.OpenGL, iface); + _grContext = GRContext.CreateGl(iface); if (maxResourceBytes.HasValue) { - _grContext.GetResourceCacheLimits(out var maxResources, out _); - _grContext.SetResourceCacheLimits(maxResources, maxResourceBytes.Value); + _grContext.SetResourceCacheLimit(maxResourceBytes.Value); } } } } - + public ISkiaGpuRenderTarget TryCreateRenderTarget(IEnumerable surfaces) { foreach (var surface in surfaces) diff --git a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs index 0bc5dd56aca..66b7fdea138 100644 --- a/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs +++ b/src/Skia/Avalonia.Skia/PlatformRenderInterface.cs @@ -157,12 +157,12 @@ public IWriteableBitmapImpl CreateWriteableBitmap(PixelSize size, Vector dpi, Pi return new WriteableBitmapImpl(size, dpi, format); } - private static readonly SKPaint s_paint = new SKPaint + private static readonly SKFont s_font = new SKFont { - TextEncoding = SKTextEncoding.GlyphId, - IsAntialias = true, - IsStroke = false, - SubpixelText = true + Subpixel = true, + Edging = SKFontEdging.Antialias, + Hinting = SKFontHinting.Full, + LinearMetrics = true }; private static readonly SKTextBlobBuilder s_textBlobBuilder = new SKTextBlobBuilder(); @@ -176,8 +176,8 @@ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) var typeface = glyphTypeface.Typeface; - s_paint.TextSize = (float)glyphRun.FontRenderingEmSize; - s_paint.Typeface = typeface; + s_font.Size = (float)glyphRun.FontRenderingEmSize; + s_font.Typeface = typeface; SKTextBlob textBlob; @@ -190,7 +190,7 @@ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) { if (glyphTypeface.IsFixedPitch) { - s_textBlobBuilder.AddRun(s_paint, 0, 0, glyphRun.GlyphIndices.Buffer.Span); + s_textBlobBuilder.AddRun(glyphRun.GlyphIndices.Buffer.Span, s_font); textBlob = s_textBlobBuilder.Build(); @@ -198,7 +198,7 @@ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) } else { - var buffer = s_textBlobBuilder.AllocateHorizontalRun(s_paint, count, 0); + var buffer = s_textBlobBuilder.AllocateHorizontalRun(s_font, count, 0); var positions = buffer.GetPositionSpan(); @@ -223,7 +223,7 @@ public IGlyphRunImpl CreateGlyphRun(GlyphRun glyphRun, out double width) } else { - var buffer = s_textBlobBuilder.AllocatePositionedRun(s_paint, count); + var buffer = s_textBlobBuilder.AllocatePositionedRun(s_font, count); var glyphPositions = buffer.GetPositionSpan(); diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs index 7aea90e61e2..6c2ac17923c 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollection.cs @@ -19,42 +19,49 @@ public void AddTypeface(FontKey key, SKTypeface typeface) public SKTypeface Get(Typeface typeface) { - var key = new FontKey(typeface.FontFamily.Name, typeface.Weight, typeface.Style); + var key = new FontKey(typeface.FontFamily.Name, typeface.Style, typeface.Weight); return GetNearestMatch(_typefaces, key); } private static SKTypeface GetNearestMatch(IDictionary typefaces, FontKey key) { - if (typefaces.ContainsKey(key)) + if (typefaces.TryGetValue(new FontKey(key.FamilyName, key.Style, key.Weight), out var typeface)) { - return typefaces[key]; + return typeface; } - var keys = typefaces.Keys.Where( - x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && x.Style == key.Style).ToArray(); + var weight = (int)key.Weight; - if (!keys.Any()) - { - keys = typefaces.Keys.Where( - x => x.Weight == key.Weight && (x.Style >= key.Style || x.Style < key.Style)).ToArray(); + weight -= weight % 100; // make sure we start at a full weight - if (!keys.Any()) + for (var i = (int)key.Style; i < 2; i++) + { + // only try 2 font weights in each direction + for (var j = 0; j < 200; j += 100) { - keys = typefaces.Keys.Where( - x => ((int)x.Weight <= (int)key.Weight || (int)x.Weight > (int)key.Weight) && - (x.Style >= key.Style || x.Style < key.Style)).ToArray(); - } - } + if (weight - j >= 100) + { + if (typefaces.TryGetValue(new FontKey(key.FamilyName, (FontStyle)i, (FontWeight)(weight - j)), out typeface)) + { + return typeface; + } + } - if (keys.Length == 0) - { - return null; - } + if (weight + j > 900) + { + continue; + } - key = keys[0]; + if (typefaces.TryGetValue(new FontKey(key.FamilyName, (FontStyle)i, (FontWeight)(weight + j)), out typeface)) + { + return typeface; + } + } + } - return typefaces[key]; + //Nothing was found so we use the first typeface we can get. + return typefaces.Values.FirstOrDefault(); } } } diff --git a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs index a9aed80a042..d36baf331db 100644 --- a/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs +++ b/src/Skia/Avalonia.Skia/SKTypefaceCollectionCache.cs @@ -43,18 +43,20 @@ private static SKTypefaceCollection CreateCustomFontCollection(FontFamily fontFa { var assetStream = assetLoader.Open(asset); - if (assetStream == null) throw new InvalidOperationException("Asset could not be loaded."); + if (assetStream == null) + throw new InvalidOperationException("Asset could not be loaded."); var typeface = SKTypeface.FromStream(assetStream); - if(typeface == null) throw new InvalidOperationException("Typeface could not be loaded."); + if (typeface == null) + throw new InvalidOperationException("Typeface could not be loaded."); if (typeface.FamilyName != fontFamily.Name) { continue; } - var key = new FontKey(fontFamily.Name, (FontWeight)typeface.FontWeight, (FontStyle)typeface.FontSlant); + var key = new FontKey(fontFamily.Name, (FontStyle)typeface.FontSlant, (FontWeight)typeface.FontWeight); typeFaceCollection.AddTypeface(key, typeface); } diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index 7a0823a2230..b0384a1fdf3 100644 --- a/src/Skia/Avalonia.Skia/TextShaperImpl.cs +++ b/src/Skia/Avalonia.Skia/TextShaperImpl.cs @@ -1,9 +1,9 @@ using System; +using System.Globalization; using Avalonia.Media; -using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; -using Avalonia.Utility; +using Avalonia.Utilities; using HarfBuzzSharp; using Buffer = HarfBuzzSharp.Buffer; @@ -11,59 +11,17 @@ namespace Avalonia.Skia { internal class TextShaperImpl : ITextShaperImpl { - public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat) + public GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture) { using (var buffer = new Buffer()) { - buffer.ContentType = ContentType.Unicode; + FillBuffer(buffer, text); - var breakCharPosition = text.Length - 1; - - var codepoint = Codepoint.ReadAt(text, breakCharPosition, out var count); - - if (codepoint.IsBreakChar) - { - var breakCharCount = 1; - - if (text.Length > 1) - { - var previousCodepoint = Codepoint.ReadAt(text, breakCharPosition - count, out _); - - if (codepoint == '\r' && previousCodepoint == '\n' - || codepoint == '\n' && previousCodepoint == '\r') - { - breakCharCount = 2; - } - } - - if (breakCharPosition != text.Start) - { - buffer.AddUtf16(text.Buffer.Span.Slice(0, text.Length - breakCharCount)); - } - - var cluster = buffer.GlyphInfos.Length > 0 ? - buffer.GlyphInfos[buffer.Length - 1].Cluster + 1 : - (uint)text.Start; - - switch (breakCharCount) - { - case 1: - buffer.Add('\u200C', cluster); - break; - case 2: - buffer.Add('\u200C', cluster); - buffer.Add('\u200D', cluster); - break; - } - } - else - { - buffer.AddUtf16(text.Buffer.Span); - } + buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); buffer.GuessSegmentProperties(); - var glyphTypeface = textFormat.Typeface.GlyphTypeface; + var glyphTypeface = typeface.GlyphTypeface; var font = ((GlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font; @@ -71,7 +29,7 @@ public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat) font.GetScale(out var scaleX, out _); - var textScale = textFormat.FontRenderingEmSize / scaleX; + var textScale = fontRenderingEmSize / scaleX; var bufferLength = buffer.Length; @@ -91,7 +49,7 @@ public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat) { glyphIndices[i] = (ushort)glyphInfos[i].Codepoint; - clusters[i] = (ushort)(text.Start + glyphInfos[i].Cluster); + clusters[i] = (ushort)glyphInfos[i].Cluster; if (!glyphTypeface.IsFixedPitch) { @@ -101,7 +59,7 @@ public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat) SetOffset(glyphPositions, i, textScale, ref glyphOffsets); } - return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize, + return new GlyphRun(glyphTypeface, fontRenderingEmSize, new ReadOnlySlice(glyphIndices), new ReadOnlySlice(glyphAdvances), new ReadOnlySlice(glyphOffsets), @@ -110,6 +68,51 @@ public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat) } } + private static void FillBuffer(Buffer buffer, ReadOnlySlice text) + { + buffer.ContentType = ContentType.Unicode; + + var i = 0; + + while (i < text.Length) + { + var codepoint = Codepoint.ReadAt(text, i, out var count); + + var cluster = (uint)(text.Start + i); + + if (codepoint.IsBreakChar) + { + if (i + 1 < text.Length) + { + var nextCodepoint = Codepoint.ReadAt(text, i + 1, out _); + + if (nextCodepoint == '\r' && codepoint == '\n' || nextCodepoint == '\n' && codepoint == '\r') + { + count++; + + buffer.Add('\u200C', cluster); + + buffer.Add('\u200D', cluster); + } + else + { + buffer.Add('\u200C', cluster); + } + } + else + { + buffer.Add('\u200C', cluster); + } + } + else + { + buffer.Add(codepoint, cluster); + } + + i += count; + } + } + private static void SetOffset(ReadOnlySpan glyphPositions, int index, double textScale, ref Vector[] offsetBuffer) { diff --git a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs index 253a373106b..33af15076da 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/FontManagerImpl.cs @@ -32,7 +32,8 @@ public IEnumerable GetInstalledFontFamilyNames(bool checkForUpdates = fa return fontFamilies; } - public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fontStyle, + public bool TryMatchCharacter(int codepoint, FontStyle fontStyle, + FontWeight fontWeight, FontFamily fontFamily, CultureInfo culture, out FontKey fontKey) { var familyCount = Direct2D1FontCollectionCache.InstalledFontCollection.FontFamilyCount; @@ -50,7 +51,7 @@ public bool TryMatchCharacter(int codepoint, FontWeight fontWeight, FontStyle fo var fontFamilyName = font.FontFamily.FamilyNames.GetString(0); - fontKey = new FontKey(fontFamilyName, fontWeight, fontStyle); + fontKey = new FontKey(fontFamilyName, fontStyle, fontWeight); return true; } diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index 2d2865e2b9d..254b5684a45 100644 --- a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs +++ b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs @@ -1,8 +1,9 @@ -using Avalonia.Media; +using System.Globalization; +using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; -using Avalonia.Utility; +using Avalonia.Utilities; using HarfBuzzSharp; using Buffer = HarfBuzzSharp.Buffer; @@ -10,7 +11,7 @@ namespace Avalonia.Direct2D1.Media { internal class TextShaperImpl : ITextShaperImpl { - public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat) + public GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture) { using (var buffer = new Buffer()) { @@ -62,15 +63,17 @@ public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat) buffer.GuessSegmentProperties(); - var glyphTypeface = textFormat.Typeface.GlyphTypeface; + var glyphTypeface = typeface.GlyphTypeface; var font = ((GlyphTypefaceImpl)glyphTypeface.PlatformImpl).Font; + buffer.Language = new Language(culture ?? CultureInfo.CurrentCulture); + font.Shape(buffer); font.GetScale(out var scaleX, out _); - var textScale = textFormat.FontRenderingEmSize / scaleX; + var textScale = fontRenderingEmSize / scaleX; var len = buffer.Length; @@ -104,7 +107,7 @@ public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat) glyphOffsets[i] = new Vector(offsetX, offsetY); } - return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize, + return new GlyphRun(glyphTypeface, fontRenderingEmSize, new ReadOnlySlice(glyphIndices), new ReadOnlySlice(glyphAdvances), new ReadOnlySlice(glyphOffsets), diff --git a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs index f8366abb811..3467a33d161 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -75,7 +75,7 @@ public WpfTopLevelImpl() private IntPtr WndProc(IntPtr hwnd, int msg, IntPtr wparam, IntPtr lparam, ref bool handled) { if (msg == (int)UnmanagedMethods.WindowsMessage.WM_DPICHANGED) - _ttl.ScalingChanged?.Invoke(_ttl.Scaling); + _ttl.ScalingChanged?.Invoke(_ttl.RenderScaling); return IntPtr.Zero; } @@ -84,7 +84,7 @@ private void OnSourceChanged(object sender, SourceChangedEventArgs e) _currentHwndSource?.RemoveHook(_hook); _currentHwndSource = e.NewSource as HwndSource; _currentHwndSource?.AddHook(_hook); - _ttl.ScalingChanged?.Invoke(_ttl.Scaling); + _ttl.ScalingChanged?.Invoke(_ttl.RenderScaling); } public IRenderer CreateRenderer(IRenderRoot root) @@ -102,7 +102,7 @@ public void Dispose() Size ITopLevelImpl.ClientSize => _finalSize; IMouseDevice ITopLevelImpl.MouseDevice => _mouse; - double ITopLevelImpl.Scaling => PresentationSource.FromVisual(this)?.CompositionTarget?.TransformToDevice.M11 ?? 1; + double ITopLevelImpl.RenderScaling => PresentationSource.FromVisual(this)?.CompositionTarget?.TransformToDevice.M11 ?? 1; IEnumerable ITopLevelImpl.Surfaces => _surfaces; @@ -256,5 +256,7 @@ internal Vector GetScaling() public void SetTransparencyLevelHint(WindowTransparencyLevel transparencyLevel) { } public WindowTransparencyLevel TransparencyLevel { get; private set; } + + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 1, 1); } } diff --git a/src/Windows/Avalonia.Win32/FramebufferManager.cs b/src/Windows/Avalonia.Win32/FramebufferManager.cs index 87c5a1bb02d..6969a49dadf 100644 --- a/src/Windows/Avalonia.Win32/FramebufferManager.cs +++ b/src/Windows/Avalonia.Win32/FramebufferManager.cs @@ -5,7 +5,7 @@ namespace Avalonia.Win32 { - class FramebufferManager : IFramebufferPlatformSurface + class FramebufferManager : IFramebufferPlatformSurface, IDisposable { private readonly IntPtr _hwnd; private WindowFramebuffer _fb; @@ -17,17 +17,25 @@ public FramebufferManager(IntPtr hwnd) public ILockedFramebuffer Lock() { - UnmanagedMethods.RECT rc; - UnmanagedMethods.GetClientRect(_hwnd, out rc); - var width = rc.right - rc.left; - var height = rc.bottom - rc.top; - if ((_fb == null || _fb.Size.Width != width || _fb.Size.Height != height) && width > 0 && height > 0) + UnmanagedMethods.GetClientRect(_hwnd, out var rc); + + var width = Math.Max(1, rc.right - rc.left); + var height = Math.Max(1, rc.bottom - rc.top); + + if ((_fb == null || _fb.Size.Width != width || _fb.Size.Height != height)) { _fb?.Deallocate(); _fb = null; _fb = new WindowFramebuffer(_hwnd, new PixelSize(width, height)); } + return _fb; } + + public void Dispose() + { + _fb?.Deallocate(); + _fb = null; + } } } diff --git a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index b3b38db1abe..b7c68c4b951 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -1,4 +1,5 @@ using System; +using System.ComponentModel; using System.Diagnostics.CodeAnalysis; using System.Runtime.CompilerServices; using System.Runtime.InteropServices; @@ -989,6 +990,12 @@ public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr handle) } } + [DllImport("user32.dll")] + public static extern IntPtr GetSystemMenu(IntPtr hWnd, bool bRevert); + + [DllImport("user32.dll")] + public static extern bool EnableMenuItem(IntPtr hMenu, uint uIDEnableItem, uint uEnable); + [DllImport("user32.dll", SetLastError = true)] public static extern bool GetWindowPlacement(IntPtr hWnd, ref WINDOWPLACEMENT lpwndpl); @@ -1053,9 +1060,21 @@ public static IntPtr SetWindowLongPtr(IntPtr hWnd, int nIndex, IntPtr handle) [DllImport("user32.dll")] public static extern bool SetFocus(IntPtr hWnd); [DllImport("user32.dll")] + public static extern IntPtr GetFocus(); + [DllImport("user32.dll")] public static extern bool SetParent(IntPtr hWnd, IntPtr hWndNewParent); [DllImport("user32.dll")] public static extern IntPtr GetParent(IntPtr hWnd); + + public enum GetAncestorFlags + { + GA_PARENT = 1, + GA_ROOT = 2, + GA_ROOTOWNER = 3 + } + + [DllImport("user32.dll")] + public static extern IntPtr GetAncestor(IntPtr hwnd, GetAncestorFlags gaFlags); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr hWnd, ShowWindowCommand nCmdShow); @@ -1317,6 +1336,9 @@ public static extern IntPtr CreateFileMapping(IntPtr hFile, [DllImport("dwmapi.dll")] public static extern int DwmIsCompositionEnabled(out bool enabled); + [DllImport("dwmapi.dll")] + public static extern bool DwmDefWindowProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref IntPtr plResult); + [DllImport("dwmapi.dll")] public static extern void DwmEnableBlurBehindWindow(IntPtr hwnd, ref DWM_BLURBEHIND blurBehind); @@ -1373,6 +1395,22 @@ internal static Version RtlGetVersion() throw new Exception("RtlGetVersion failed!"); } } + + [DllImport("kernel32", EntryPoint="WaitForMultipleObjectsEx", SetLastError = true, CharSet = CharSet.Auto)] + private static extern int IntWaitForMultipleObjectsEx(int nCount, IntPtr[] pHandles, bool bWaitAll, int dwMilliseconds, bool bAlertable); + + public const int WAIT_FAILED = unchecked((int)0xFFFFFFFF); + + internal static int WaitForMultipleObjectsEx(int nCount, IntPtr[] pHandles, bool bWaitAll, int dwMilliseconds, bool bAlertable) + { + int result = IntWaitForMultipleObjectsEx(nCount, pHandles, bWaitAll, dwMilliseconds, bAlertable); + if(result == WAIT_FAILED) + { + throw new Win32Exception(); + } + + return result; + } [DllImport("user32.dll")] internal static extern int SetWindowCompositionAttribute(IntPtr hwnd, ref WindowCompositionAttributeData data); diff --git a/src/Windows/Avalonia.Win32/NonPumpingWaitProvider.cs b/src/Windows/Avalonia.Win32/NonPumpingWaitProvider.cs new file mode 100644 index 00000000000..a0160fcfbd6 --- /dev/null +++ b/src/Windows/Avalonia.Win32/NonPumpingWaitProvider.cs @@ -0,0 +1,15 @@ +using System; +using Avalonia.Threading; +using Avalonia.Win32.Interop; + +namespace Avalonia.Win32 +{ + internal class NonPumpingWaitProvider : AvaloniaSynchronizationContext.INonPumpingPlatformWaitProvider + { + public int Wait(IntPtr[] waitHandles, bool waitAll, int millisecondsTimeout) + { + return UnmanagedMethods.WaitForMultipleObjectsEx(waitHandles.Length, waitHandles, waitAll, + millisecondsTimeout, false); + } + } +} diff --git a/src/Windows/Avalonia.Win32/PopupImpl.cs b/src/Windows/Avalonia.Win32/PopupImpl.cs index cd25b32ed9b..57da1c4d660 100644 --- a/src/Windows/Avalonia.Win32/PopupImpl.cs +++ b/src/Windows/Avalonia.Win32/PopupImpl.cs @@ -7,6 +7,7 @@ namespace Avalonia.Win32 { class PopupImpl : WindowImpl, IPopupImpl { + private readonly IWindowBaseImpl _parent; private bool _dropShadowHint = true; private Size? _maxAutoSize; @@ -19,18 +20,25 @@ class PopupImpl : WindowImpl, IPopupImpl public override void Show() { UnmanagedMethods.ShowWindow(Handle.Handle, UnmanagedMethods.ShowWindowCommand.ShowNoActivate); - var parent = UnmanagedMethods.GetParent(Handle.Handle); - if (parent != IntPtr.Zero) - { - IntPtr nextParent = parent; - while (nextParent != IntPtr.Zero) - { - parent = nextParent; - nextParent = UnmanagedMethods.GetParent(parent); - } - UnmanagedMethods.SetFocus(parent); + // We need to steal focus if it's held by a child window of our toplevel window + var parent = _parent; + while(parent != null) + { + if(parent is PopupImpl pi) + parent = pi._parent; + else + break; } + + if(parent == null) + return; + + var focusOwner = UnmanagedMethods.GetFocus(); + if (focusOwner != IntPtr.Zero && + UnmanagedMethods.GetAncestor(focusOwner, UnmanagedMethods.GetAncestorFlags.GA_ROOT) + == parent.Handle.Handle) + UnmanagedMethods.SetFocus(parent.Handle.Handle); } protected override bool ShouldTakeFocusOnClick => false; @@ -49,7 +57,7 @@ public override Size MaxAutoSizeHint { var info = UnmanagedMethods.MONITORINFO.Create(); UnmanagedMethods.GetMonitorInfo(monitor, ref info); - _maxAutoSize = info.rcWork.ToPixelRect().ToRect(Scaling).Size; + _maxAutoSize = info.rcWork.ToPixelRect().ToRect(RenderScaling).Size; } } @@ -118,6 +126,7 @@ public PopupImpl(IWindowBaseImpl parent) : this(SaveParentHandle(parent), false) private PopupImpl(IWindowBaseImpl parent, bool dummy) : base() { + _parent = parent; PopupPositioner = new ManagedPopupPositioner(new ManagedPopupPositionerPopupImplHelper(parent, MoveResize)); } diff --git a/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs b/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs index d7bb2c037e4..8f62163d81c 100644 --- a/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs +++ b/src/Windows/Avalonia.Win32/Win32NativeControlHost.cs @@ -176,7 +176,7 @@ public void HideWithSize(Size size) UnmanagedMethods.SetWindowPosFlags.SWP_NOACTIVATE); if (_attachedTo == null || _child == null) return; - size *= _attachedTo.Window.Scaling; + size *= _attachedTo.Window.RenderScaling; UnmanagedMethods.MoveWindow(_child.Handle, 0, 0, Math.Max(1, (int)size.Width), Math.Max(1, (int)size.Height), false); } @@ -186,7 +186,7 @@ public unsafe void ShowInBounds(Rect bounds) CheckDisposed(); if (_attachedTo == null) throw new InvalidOperationException("The control isn't currently attached to a toplevel"); - bounds *= _attachedTo.Window.Scaling; + bounds *= _attachedTo.Window.RenderScaling; var pixelRect = new PixelRect((int)bounds.X, (int)bounds.Y, Math.Max(1, (int)bounds.Width), Math.Max(1, (int)bounds.Height)); diff --git a/src/Windows/Avalonia.Win32/Win32Platform.cs b/src/Windows/Avalonia.Win32/Win32Platform.cs index b7bb0e19bab..af6058d197d 100644 --- a/src/Windows/Avalonia.Win32/Win32Platform.cs +++ b/src/Windows/Avalonia.Win32/Win32Platform.cs @@ -93,6 +93,7 @@ public static void Initialize(Win32PlatformOptions options) .Bind().ToConstant(s_instance) .Bind().ToSingleton() .Bind().ToConstant(s_instance) + .Bind().ToConstant(new NonPumpingWaitProvider()) .Bind().ToConstant(new WindowsMountedVolumeInfoProvider()); if (options.AllowEglInitialization) diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs new file mode 100644 index 00000000000..25a34561fc4 --- /dev/null +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -0,0 +1,541 @@ +using System; +using System.Diagnostics.CodeAnalysis; +using System.Runtime.InteropServices; +using Avalonia.Controls; +using Avalonia.Controls.Platform; +using Avalonia.Input; +using Avalonia.Input.Raw; +using Avalonia.Win32.Input; +using static Avalonia.Win32.Interop.UnmanagedMethods; + +namespace Avalonia.Win32 +{ + public partial class WindowImpl + { + [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", + Justification = "Using Win32 naming for consistency.")] + protected virtual unsafe IntPtr AppWndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) + { + const double wheelDelta = 120.0; + uint timestamp = unchecked((uint)GetMessageTime()); + + RawInputEventArgs e = null; + var shouldTakeFocus = false; + + switch ((WindowsMessage)msg) + { + case WindowsMessage.WM_ACTIVATE: + { + var wa = (WindowActivate)(ToInt32(wParam) & 0xffff); + + switch (wa) + { + case WindowActivate.WA_ACTIVE: + case WindowActivate.WA_CLICKACTIVE: + { + Activated?.Invoke(); + break; + } + + case WindowActivate.WA_INACTIVE: + { + Deactivated?.Invoke(); + break; + } + } + + return IntPtr.Zero; + } + + case WindowsMessage.WM_NCCALCSIZE: + { + if (ToInt32(wParam) == 1 && !HasFullDecorations || _isClientAreaExtended) + { + return IntPtr.Zero; + } + + break; + } + + case WindowsMessage.WM_CLOSE: + { + bool? preventClosing = Closing?.Invoke(); + if (preventClosing == true) + { + return IntPtr.Zero; + } + + break; + } + + case WindowsMessage.WM_DESTROY: + { + //Window doesn't exist anymore + _hwnd = IntPtr.Zero; + //Remove root reference to this class, so unmanaged delegate can be collected + s_instances.Remove(this); + Closed?.Invoke(); + + _mouseDevice.Dispose(); + _touchDevice?.Dispose(); + //Free other resources + Dispose(); + return IntPtr.Zero; + } + + case WindowsMessage.WM_DPICHANGED: + { + var dpi = ToInt32(wParam) & 0xffff; + var newDisplayRect = Marshal.PtrToStructure(lParam); + _scaling = dpi / 96.0; + ScalingChanged?.Invoke(_scaling); + SetWindowPos(hWnd, + IntPtr.Zero, + newDisplayRect.left, + newDisplayRect.top, + newDisplayRect.right - newDisplayRect.left, + newDisplayRect.bottom - newDisplayRect.top, + SetWindowPosFlags.SWP_NOZORDER | + SetWindowPosFlags.SWP_NOACTIVATE); + return IntPtr.Zero; + } + + case WindowsMessage.WM_KEYDOWN: + case WindowsMessage.WM_SYSKEYDOWN: + { + e = new RawKeyEventArgs( + WindowsKeyboardDevice.Instance, + timestamp, + _owner, + RawKeyEventType.KeyDown, + KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), + WindowsKeyboardDevice.Instance.Modifiers); + break; + } + + case WindowsMessage.WM_MENUCHAR: + { + // mute the system beep + return (IntPtr)((int)MenuCharParam.MNC_CLOSE << 16); + } + + case WindowsMessage.WM_KEYUP: + case WindowsMessage.WM_SYSKEYUP: + { + e = new RawKeyEventArgs( + WindowsKeyboardDevice.Instance, + timestamp, + _owner, + RawKeyEventType.KeyUp, + KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), + WindowsKeyboardDevice.Instance.Modifiers); + break; + } + case WindowsMessage.WM_CHAR: + { + // Ignore control chars + if (ToInt32(wParam) >= 32) + { + e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, _owner, + new string((char)ToInt32(wParam), 1)); + } + + break; + } + + case WindowsMessage.WM_LBUTTONDOWN: + case WindowsMessage.WM_RBUTTONDOWN: + case WindowsMessage.WM_MBUTTONDOWN: + case WindowsMessage.WM_XBUTTONDOWN: + { + shouldTakeFocus = ShouldTakeFocusOnClick; + if (ShouldIgnoreTouchEmulatedMessage()) + { + break; + } + + e = new RawPointerEventArgs( + _mouseDevice, + timestamp, + _owner, + (WindowsMessage)msg switch + { + WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown, + WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown, + WindowsMessage.WM_MBUTTONDOWN => RawPointerEventType.MiddleButtonDown, + WindowsMessage.WM_XBUTTONDOWN => + HighWord(ToInt32(wParam)) == 1 ? + RawPointerEventType.XButton1Down : + RawPointerEventType.XButton2Down + }, + DipFromLParam(lParam), GetMouseModifiers(wParam)); + break; + } + + case WindowsMessage.WM_LBUTTONUP: + case WindowsMessage.WM_RBUTTONUP: + case WindowsMessage.WM_MBUTTONUP: + case WindowsMessage.WM_XBUTTONUP: + { + shouldTakeFocus = ShouldTakeFocusOnClick; + if (ShouldIgnoreTouchEmulatedMessage()) + { + break; + } + + e = new RawPointerEventArgs( + _mouseDevice, + timestamp, + _owner, + (WindowsMessage)msg switch + { + WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp, + WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp, + WindowsMessage.WM_MBUTTONUP => RawPointerEventType.MiddleButtonUp, + WindowsMessage.WM_XBUTTONUP => + HighWord(ToInt32(wParam)) == 1 ? + RawPointerEventType.XButton1Up : + RawPointerEventType.XButton2Up, + }, + DipFromLParam(lParam), GetMouseModifiers(wParam)); + break; + } + + case WindowsMessage.WM_MOUSEMOVE: + { + if (ShouldIgnoreTouchEmulatedMessage()) + { + break; + } + + if (!_trackingMouse) + { + var tm = new TRACKMOUSEEVENT + { + cbSize = Marshal.SizeOf(), + dwFlags = 2, + hwndTrack = _hwnd, + dwHoverTime = 0, + }; + + TrackMouseEvent(ref tm); + } + + e = new RawPointerEventArgs( + _mouseDevice, + timestamp, + _owner, + RawPointerEventType.Move, + DipFromLParam(lParam), GetMouseModifiers(wParam)); + + break; + } + + case WindowsMessage.WM_MOUSEWHEEL: + { + e = new RawMouseWheelEventArgs( + _mouseDevice, + timestamp, + _owner, + PointToClient(PointFromLParam(lParam)), + new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), GetMouseModifiers(wParam)); + break; + } + + case WindowsMessage.WM_MOUSEHWHEEL: + { + e = new RawMouseWheelEventArgs( + _mouseDevice, + timestamp, + _owner, + PointToClient(PointFromLParam(lParam)), + new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), GetMouseModifiers(wParam)); + break; + } + + case WindowsMessage.WM_MOUSELEAVE: + { + _trackingMouse = false; + e = new RawPointerEventArgs( + _mouseDevice, + timestamp, + _owner, + RawPointerEventType.LeaveWindow, + new Point(-1, -1), WindowsKeyboardDevice.Instance.Modifiers); + break; + } + + case WindowsMessage.WM_NCLBUTTONDOWN: + case WindowsMessage.WM_NCRBUTTONDOWN: + case WindowsMessage.WM_NCMBUTTONDOWN: + case WindowsMessage.WM_NCXBUTTONDOWN: + { + e = new RawPointerEventArgs( + _mouseDevice, + timestamp, + _owner, + (WindowsMessage)msg switch + { + WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType + .NonClientLeftButtonDown, + WindowsMessage.WM_NCRBUTTONDOWN => RawPointerEventType.RightButtonDown, + WindowsMessage.WM_NCMBUTTONDOWN => RawPointerEventType.MiddleButtonDown, + WindowsMessage.WM_NCXBUTTONDOWN => + HighWord(ToInt32(wParam)) == 1 ? + RawPointerEventType.XButton1Down : + RawPointerEventType.XButton2Down, + }, + PointToClient(PointFromLParam(lParam)), GetMouseModifiers(wParam)); + break; + } + case WindowsMessage.WM_TOUCH: + { + var touchInputCount = wParam.ToInt32(); + + var pTouchInputs = stackalloc TOUCHINPUT[touchInputCount]; + var touchInputs = new Span(pTouchInputs, touchInputCount); + + if (GetTouchInputInfo(lParam, (uint)touchInputCount, pTouchInputs, Marshal.SizeOf())) + { + foreach (var touchInput in touchInputs) + { + Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time, + _owner, + touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_UP) ? + RawPointerEventType.TouchEnd : + touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_DOWN) ? + RawPointerEventType.TouchBegin : + RawPointerEventType.TouchUpdate, + PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)), + WindowsKeyboardDevice.Instance.Modifiers, + touchInput.Id)); + } + + CloseTouchInputHandle(lParam); + return IntPtr.Zero; + } + + break; + } + case WindowsMessage.WM_NCPAINT: + { + if (!HasFullDecorations) + { + return IntPtr.Zero; + } + + break; + } + + case WindowsMessage.WM_NCACTIVATE: + { + if (!HasFullDecorations) + { + return new IntPtr(1); + } + + break; + } + + case WindowsMessage.WM_PAINT: + { + using (_rendererLock.Lock()) + { + if (BeginPaint(_hwnd, out PAINTSTRUCT ps) != IntPtr.Zero) + { + var f = RenderScaling; + var r = ps.rcPaint; + Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, + (r.bottom - r.top) / f)); + EndPaint(_hwnd, ref ps); + } + } + + return IntPtr.Zero; + } + + case WindowsMessage.WM_SIZE: + { + using (_rendererLock.Lock()) + { + // Do nothing here, just block until the pending frame render is completed on the render thread + } + + var size = (SizeCommand)wParam; + + if (Resized != null && + (size == SizeCommand.Restored || + size == SizeCommand.Maximized)) + { + var clientSize = new Size(ToInt32(lParam) & 0xffff, ToInt32(lParam) >> 16); + Resized(clientSize / RenderScaling); + } + + var windowState = size == SizeCommand.Maximized ? + WindowState.Maximized : + (size == SizeCommand.Minimized ? WindowState.Minimized : WindowState.Normal); + + if (windowState != _lastWindowState) + { + _lastWindowState = windowState; + + WindowStateChanged?.Invoke(windowState); + + if (_isClientAreaExtended) + { + UpdateExtendMargins(); + + ExtendClientAreaToDecorationsChanged?.Invoke(true); + } + } + + return IntPtr.Zero; + } + + case WindowsMessage.WM_MOVE: + { + PositionChanged?.Invoke(new PixelPoint((short)(ToInt32(lParam) & 0xffff), + (short)(ToInt32(lParam) >> 16))); + return IntPtr.Zero; + } + + case WindowsMessage.WM_GETMINMAXINFO: + { + MINMAXINFO mmi = Marshal.PtrToStructure(lParam); + + _maxTrackSize = mmi.ptMaxTrackSize; + + if (_minSize.Width > 0) + { + mmi.ptMinTrackSize.X = + (int)((_minSize.Width * RenderScaling) + BorderThickness.Left + BorderThickness.Right); + } + + if (_minSize.Height > 0) + { + mmi.ptMinTrackSize.Y = + (int)((_minSize.Height * RenderScaling) + BorderThickness.Top + BorderThickness.Bottom); + } + + if (!double.IsInfinity(_maxSize.Width) && _maxSize.Width > 0) + { + mmi.ptMaxTrackSize.X = + (int)((_maxSize.Width * RenderScaling) + BorderThickness.Left + BorderThickness.Right); + } + + if (!double.IsInfinity(_maxSize.Height) && _maxSize.Height > 0) + { + mmi.ptMaxTrackSize.Y = + (int)((_maxSize.Height * RenderScaling) + BorderThickness.Top + BorderThickness.Bottom); + } + + Marshal.StructureToPtr(mmi, lParam, true); + return IntPtr.Zero; + } + + case WindowsMessage.WM_DISPLAYCHANGE: + { + (Screen as ScreenImpl)?.InvalidateScreensCache(); + return IntPtr.Zero; + } + + case WindowsMessage.WM_KILLFOCUS: + LostFocus?.Invoke(); + break; + } + +#if USE_MANAGED_DRAG + if (_managedDrag.PreprocessInputEvent(ref e)) + return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); +#endif + + if(shouldTakeFocus) + { + SetFocus(_hwnd); + } + + if (e != null && Input != null) + { + Input(e); + + if (e.Handled) + { + return IntPtr.Zero; + } + } + + using (_rendererLock.Lock()) + { + return DefWindowProc(hWnd, msg, wParam, lParam); + } + } + + private static int ToInt32(IntPtr ptr) + { + if (IntPtr.Size == 4) + return ptr.ToInt32(); + + return (int)(ptr.ToInt64() & 0xffffffff); + } + + private static int HighWord(int param) => param >> 16; + + private Point DipFromLParam(IntPtr lParam) + { + return new Point((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)) / RenderScaling; + } + + private PixelPoint PointFromLParam(IntPtr lParam) + { + return new PixelPoint((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)); + } + + private bool ShouldIgnoreTouchEmulatedMessage() + { + if (!_multitouch) + { + return false; + } + + // MI_WP_SIGNATURE + // https://docs.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages + const long marker = 0xFF515700L; + + var info = GetMessageExtraInfo().ToInt64(); + return (info & marker) == marker; + } + + private static RawInputModifiers GetMouseModifiers(IntPtr wParam) + { + var keys = (ModifierKeys)ToInt32(wParam); + var modifiers = WindowsKeyboardDevice.Instance.Modifiers; + + if (keys.HasFlagCustom(ModifierKeys.MK_LBUTTON)) + { + modifiers |= RawInputModifiers.LeftMouseButton; + } + + if (keys.HasFlagCustom(ModifierKeys.MK_RBUTTON)) + { + modifiers |= RawInputModifiers.RightMouseButton; + } + + if (keys.HasFlagCustom(ModifierKeys.MK_MBUTTON)) + { + modifiers |= RawInputModifiers.MiddleMouseButton; + } + + if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON1)) + { + modifiers |= RawInputModifiers.XButton1MouseButton; + } + + if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON2)) + { + modifiers |= RawInputModifiers.XButton2MouseButton; + } + + return modifiers; + } + } +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs new file mode 100644 index 00000000000..a3b75743691 --- /dev/null +++ b/src/Windows/Avalonia.Win32/WindowImpl.CustomCaptionProc.cs @@ -0,0 +1,140 @@ +using System; +using System.Diagnostics; +using Avalonia.Controls; +using Avalonia.Input; +using static Avalonia.Win32.Interop.UnmanagedMethods; + +#nullable enable + +namespace Avalonia.Win32 +{ + public partial class WindowImpl + { + // Hit test the frame for resizing and moving. + HitTestValues HitTestNCA(IntPtr hWnd, IntPtr wParam, IntPtr lParam) + { + // Get the point coordinates for the hit test. + var ptMouse = PointFromLParam(lParam); + + // Get the window rectangle. + GetWindowRect(hWnd, out var rcWindow); + + // Get the frame rectangle, adjusted for the style without a caption. + RECT rcFrame = new RECT(); + AdjustWindowRectEx(ref rcFrame, (uint)(WindowStyles.WS_OVERLAPPEDWINDOW & ~WindowStyles.WS_CAPTION), false, 0); + + RECT border_thickness = new RECT(); + if (GetStyle().HasFlag(WindowStyles.WS_THICKFRAME)) + { + AdjustWindowRectEx(ref border_thickness, (uint)(GetStyle()), false, 0); + border_thickness.left *= -1; + border_thickness.top *= -1; + } + else if (GetStyle().HasFlag(WindowStyles.WS_BORDER)) + { + border_thickness = new RECT { bottom = 1, left = 1, right = 1, top = 1 }; + } + + if (_extendTitleBarHint >= 0) + { + border_thickness.top = (int)(_extendedMargins.Top * RenderScaling); + } + + // Determine if the hit test is for resizing. Default middle (1,1). + ushort uRow = 1; + ushort uCol = 1; + bool fOnResizeBorder = false; + + // Determine if the point is at the top or bottom of the window. + if (ptMouse.Y >= rcWindow.top && ptMouse.Y < rcWindow.top + border_thickness.top) + { + fOnResizeBorder = (ptMouse.Y < (rcWindow.top - rcFrame.top)); + uRow = 0; + } + else if (ptMouse.Y < rcWindow.bottom && ptMouse.Y >= rcWindow.bottom - border_thickness.bottom) + { + uRow = 2; + } + + // Determine if the point is at the left or right of the window. + if (ptMouse.X >= rcWindow.left && ptMouse.X < rcWindow.left + border_thickness.left) + { + uCol = 0; // left side + } + else if (ptMouse.X < rcWindow.right && ptMouse.X >= rcWindow.right - border_thickness.right) + { + uCol = 2; // right side + } + + // Hit test (HTTOPLEFT, ... HTBOTTOMRIGHT) + HitTestValues[][] hitTests = new[] + { + new []{ HitTestValues.HTTOPLEFT, fOnResizeBorder ? HitTestValues.HTTOP : HitTestValues.HTCAPTION, HitTestValues.HTTOPRIGHT }, + new []{ HitTestValues.HTLEFT, HitTestValues.HTNOWHERE, HitTestValues.HTRIGHT }, + new []{ HitTestValues.HTBOTTOMLEFT, HitTestValues.HTBOTTOM, HitTestValues.HTBOTTOMRIGHT }, + }; + + return hitTests[uRow][uCol]; + } + + protected virtual IntPtr CustomCaptionProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam, ref bool callDwp) + { + IntPtr lRet = IntPtr.Zero; + + callDwp = !DwmDefWindowProc(hWnd, msg, wParam, lParam, ref lRet); + + switch ((WindowsMessage)msg) + { + case WindowsMessage.WM_DWMCOMPOSITIONCHANGED: + // TODO handle composition changed. + break; + + case WindowsMessage.WM_NCHITTEST: + if (lRet == IntPtr.Zero) + { + if(WindowState == WindowState.FullScreen) + { + return (IntPtr)HitTestValues.HTCLIENT; + } + var hittestResult = HitTestNCA(hWnd, wParam, lParam); + + lRet = (IntPtr)hittestResult; + + uint timestamp = unchecked((uint)GetMessageTime()); + + if (hittestResult == HitTestValues.HTCAPTION) + { + var position = PointToClient(PointFromLParam(lParam)); + + if (_owner is Window window) + { + var visual = window.Renderer.HitTestFirst(position, _owner as Window, x => + { + if (x is IInputElement ie && !ie.IsHitTestVisible) + { + return false; + } + + return true; + }); + + if (visual != null) + { + hittestResult = HitTestValues.HTCLIENT; + lRet = (IntPtr)hittestResult; + } + } + } + + if (hittestResult != HitTestValues.HTNOWHERE) + { + callDwp = false; + } + } + break; + } + + return lRet; + } + } +} diff --git a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs index 391abdfc730..07f2311be8b 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.WndProc.cs @@ -15,519 +15,22 @@ namespace Avalonia.Win32 { public partial class WindowImpl { - [SuppressMessage("Microsoft.StyleCop.CSharp.NamingRules", "SA1305:FieldNamesMustNotUseHungarianNotation", - Justification = "Using Win32 naming for consistency.")] protected virtual unsafe IntPtr WndProc(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam) { - const double wheelDelta = 120.0; - uint timestamp = unchecked((uint)GetMessageTime()); + IntPtr lRet = IntPtr.Zero; + bool callDwp = true; - RawInputEventArgs e = null; - var shouldTakeFocus = false; - - switch ((WindowsMessage)msg) + if (_isClientAreaExtended) { - case WindowsMessage.WM_ACTIVATE: - { - var wa = (WindowActivate)(ToInt32(wParam) & 0xffff); - - switch (wa) - { - case WindowActivate.WA_ACTIVE: - case WindowActivate.WA_CLICKACTIVE: - { - Activated?.Invoke(); - break; - } - - case WindowActivate.WA_INACTIVE: - { - Deactivated?.Invoke(); - break; - } - } - - return IntPtr.Zero; - } - - case WindowsMessage.WM_NCCALCSIZE: - { - if (ToInt32(wParam) == 1 && !HasFullDecorations) - { - return IntPtr.Zero; - } - - break; - } - - case WindowsMessage.WM_CLOSE: - { - bool? preventClosing = Closing?.Invoke(); - if (preventClosing == true) - { - return IntPtr.Zero; - } - - break; - } - - case WindowsMessage.WM_DESTROY: - { - //Window doesn't exist anymore - _hwnd = IntPtr.Zero; - //Remove root reference to this class, so unmanaged delegate can be collected - s_instances.Remove(this); - Closed?.Invoke(); - - _mouseDevice.Dispose(); - _touchDevice?.Dispose(); - //Free other resources - Dispose(); - return IntPtr.Zero; - } - - case WindowsMessage.WM_DPICHANGED: - { - var dpi = ToInt32(wParam) & 0xffff; - var newDisplayRect = Marshal.PtrToStructure(lParam); - _scaling = dpi / 96.0; - ScalingChanged?.Invoke(_scaling); - SetWindowPos(hWnd, - IntPtr.Zero, - newDisplayRect.left, - newDisplayRect.top, - newDisplayRect.right - newDisplayRect.left, - newDisplayRect.bottom - newDisplayRect.top, - SetWindowPosFlags.SWP_NOZORDER | - SetWindowPosFlags.SWP_NOACTIVATE); - return IntPtr.Zero; - } - - case WindowsMessage.WM_KEYDOWN: - case WindowsMessage.WM_SYSKEYDOWN: - { - e = new RawKeyEventArgs( - WindowsKeyboardDevice.Instance, - timestamp, - _owner, - RawKeyEventType.KeyDown, - KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), - WindowsKeyboardDevice.Instance.Modifiers); - break; - } - - case WindowsMessage.WM_MENUCHAR: - { - // mute the system beep - return (IntPtr)((int)MenuCharParam.MNC_CLOSE << 16); - } - - case WindowsMessage.WM_KEYUP: - case WindowsMessage.WM_SYSKEYUP: - { - e = new RawKeyEventArgs( - WindowsKeyboardDevice.Instance, - timestamp, - _owner, - RawKeyEventType.KeyUp, - KeyInterop.KeyFromVirtualKey(ToInt32(wParam), ToInt32(lParam)), - WindowsKeyboardDevice.Instance.Modifiers); - break; - } - case WindowsMessage.WM_CHAR: - { - // Ignore control chars - if (ToInt32(wParam) >= 32) - { - e = new RawTextInputEventArgs(WindowsKeyboardDevice.Instance, timestamp, _owner, - new string((char)ToInt32(wParam), 1)); - } - - break; - } - - case WindowsMessage.WM_LBUTTONDOWN: - case WindowsMessage.WM_RBUTTONDOWN: - case WindowsMessage.WM_MBUTTONDOWN: - case WindowsMessage.WM_XBUTTONDOWN: - { - shouldTakeFocus = ShouldTakeFocusOnClick; - if (ShouldIgnoreTouchEmulatedMessage()) - { - break; - } - - e = new RawPointerEventArgs( - _mouseDevice, - timestamp, - _owner, - (WindowsMessage)msg switch - { - WindowsMessage.WM_LBUTTONDOWN => RawPointerEventType.LeftButtonDown, - WindowsMessage.WM_RBUTTONDOWN => RawPointerEventType.RightButtonDown, - WindowsMessage.WM_MBUTTONDOWN => RawPointerEventType.MiddleButtonDown, - WindowsMessage.WM_XBUTTONDOWN => - HighWord(ToInt32(wParam)) == 1 ? - RawPointerEventType.XButton1Down : - RawPointerEventType.XButton2Down - }, - DipFromLParam(lParam), GetMouseModifiers(wParam)); - break; - } - - case WindowsMessage.WM_LBUTTONUP: - case WindowsMessage.WM_RBUTTONUP: - case WindowsMessage.WM_MBUTTONUP: - case WindowsMessage.WM_XBUTTONUP: - { - shouldTakeFocus = ShouldTakeFocusOnClick; - if (ShouldIgnoreTouchEmulatedMessage()) - { - break; - } - - e = new RawPointerEventArgs( - _mouseDevice, - timestamp, - _owner, - (WindowsMessage)msg switch - { - WindowsMessage.WM_LBUTTONUP => RawPointerEventType.LeftButtonUp, - WindowsMessage.WM_RBUTTONUP => RawPointerEventType.RightButtonUp, - WindowsMessage.WM_MBUTTONUP => RawPointerEventType.MiddleButtonUp, - WindowsMessage.WM_XBUTTONUP => - HighWord(ToInt32(wParam)) == 1 ? - RawPointerEventType.XButton1Up : - RawPointerEventType.XButton2Up, - }, - DipFromLParam(lParam), GetMouseModifiers(wParam)); - break; - } - - case WindowsMessage.WM_MOUSEMOVE: - { - if (ShouldIgnoreTouchEmulatedMessage()) - { - break; - } - - if (!_trackingMouse) - { - var tm = new TRACKMOUSEEVENT - { - cbSize = Marshal.SizeOf(), - dwFlags = 2, - hwndTrack = _hwnd, - dwHoverTime = 0, - }; - - TrackMouseEvent(ref tm); - } - - e = new RawPointerEventArgs( - _mouseDevice, - timestamp, - _owner, - RawPointerEventType.Move, - DipFromLParam(lParam), GetMouseModifiers(wParam)); - - break; - } - - case WindowsMessage.WM_MOUSEWHEEL: - { - e = new RawMouseWheelEventArgs( - _mouseDevice, - timestamp, - _owner, - PointToClient(PointFromLParam(lParam)), - new Vector(0, (ToInt32(wParam) >> 16) / wheelDelta), GetMouseModifiers(wParam)); - break; - } - - case WindowsMessage.WM_MOUSEHWHEEL: - { - e = new RawMouseWheelEventArgs( - _mouseDevice, - timestamp, - _owner, - PointToClient(PointFromLParam(lParam)), - new Vector(-(ToInt32(wParam) >> 16) / wheelDelta, 0), GetMouseModifiers(wParam)); - break; - } - - case WindowsMessage.WM_MOUSELEAVE: - { - _trackingMouse = false; - e = new RawPointerEventArgs( - _mouseDevice, - timestamp, - _owner, - RawPointerEventType.LeaveWindow, - new Point(-1, -1), WindowsKeyboardDevice.Instance.Modifiers); - break; - } - - case WindowsMessage.WM_NCLBUTTONDOWN: - case WindowsMessage.WM_NCRBUTTONDOWN: - case WindowsMessage.WM_NCMBUTTONDOWN: - case WindowsMessage.WM_NCXBUTTONDOWN: - { - e = new RawPointerEventArgs( - _mouseDevice, - timestamp, - _owner, - (WindowsMessage)msg switch - { - WindowsMessage.WM_NCLBUTTONDOWN => RawPointerEventType - .NonClientLeftButtonDown, - WindowsMessage.WM_NCRBUTTONDOWN => RawPointerEventType.RightButtonDown, - WindowsMessage.WM_NCMBUTTONDOWN => RawPointerEventType.MiddleButtonDown, - WindowsMessage.WM_NCXBUTTONDOWN => - HighWord(ToInt32(wParam)) == 1 ? - RawPointerEventType.XButton1Down : - RawPointerEventType.XButton2Down, - }, - PointToClient(PointFromLParam(lParam)), GetMouseModifiers(wParam)); - break; - } - case WindowsMessage.WM_TOUCH: - { - var touchInputCount = wParam.ToInt32(); - - var pTouchInputs = stackalloc TOUCHINPUT[touchInputCount]; - var touchInputs = new Span(pTouchInputs, touchInputCount); - - if (GetTouchInputInfo(lParam, (uint)touchInputCount, pTouchInputs, Marshal.SizeOf())) - { - foreach (var touchInput in touchInputs) - { - Input?.Invoke(new RawTouchEventArgs(_touchDevice, touchInput.Time, - _owner, - touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_UP) ? - RawPointerEventType.TouchEnd : - touchInput.Flags.HasFlagCustom(TouchInputFlags.TOUCHEVENTF_DOWN) ? - RawPointerEventType.TouchBegin : - RawPointerEventType.TouchUpdate, - PointToClient(new PixelPoint(touchInput.X / 100, touchInput.Y / 100)), - WindowsKeyboardDevice.Instance.Modifiers, - touchInput.Id)); - } - - CloseTouchInputHandle(lParam); - return IntPtr.Zero; - } - - break; - } - case WindowsMessage.WM_NCPAINT: - { - if (!HasFullDecorations) - { - return IntPtr.Zero; - } - - break; - } - - case WindowsMessage.WM_NCACTIVATE: - { - if (!HasFullDecorations) - { - return new IntPtr(1); - } - - break; - } - - case WindowsMessage.WM_PAINT: - { - using (_rendererLock.Lock()) - { - if (BeginPaint(_hwnd, out PAINTSTRUCT ps) != IntPtr.Zero) - { - var f = Scaling; - var r = ps.rcPaint; - Paint?.Invoke(new Rect(r.left / f, r.top / f, (r.right - r.left) / f, - (r.bottom - r.top) / f)); - EndPaint(_hwnd, ref ps); - } - } - - return IntPtr.Zero; - } - - case WindowsMessage.WM_SIZE: - { - using (_rendererLock.Lock()) - { - // Do nothing here, just block until the pending frame render is completed on the render thread - } - - var size = (SizeCommand)wParam; - - if (Resized != null && - (size == SizeCommand.Restored || - size == SizeCommand.Maximized)) - { - var clientSize = new Size(ToInt32(lParam) & 0xffff, ToInt32(lParam) >> 16); - Resized(clientSize / Scaling); - } - - var windowState = size == SizeCommand.Maximized ? - WindowState.Maximized : - (size == SizeCommand.Minimized ? WindowState.Minimized : WindowState.Normal); - - if (windowState != _lastWindowState) - { - _lastWindowState = windowState; - WindowStateChanged?.Invoke(windowState); - } - - return IntPtr.Zero; - } - - case WindowsMessage.WM_MOVE: - { - PositionChanged?.Invoke(new PixelPoint((short)(ToInt32(lParam) & 0xffff), - (short)(ToInt32(lParam) >> 16))); - return IntPtr.Zero; - } - - case WindowsMessage.WM_GETMINMAXINFO: - { - MINMAXINFO mmi = Marshal.PtrToStructure(lParam); - - _maxTrackSize = mmi.ptMaxTrackSize; - - if (_minSize.Width > 0) - { - mmi.ptMinTrackSize.X = - (int)((_minSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right); - } - - if (_minSize.Height > 0) - { - mmi.ptMinTrackSize.Y = - (int)((_minSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom); - } - - if (!double.IsInfinity(_maxSize.Width) && _maxSize.Width > 0) - { - mmi.ptMaxTrackSize.X = - (int)((_maxSize.Width * Scaling) + BorderThickness.Left + BorderThickness.Right); - } - - if (!double.IsInfinity(_maxSize.Height) && _maxSize.Height > 0) - { - mmi.ptMaxTrackSize.Y = - (int)((_maxSize.Height * Scaling) + BorderThickness.Top + BorderThickness.Bottom); - } - - Marshal.StructureToPtr(mmi, lParam, true); - return IntPtr.Zero; - } - - case WindowsMessage.WM_DISPLAYCHANGE: - { - (Screen as ScreenImpl)?.InvalidateScreensCache(); - return IntPtr.Zero; - } - case WindowsMessage.WM_KILLFOCUS: - LostFocus?.Invoke(); - break; - } - -#if USE_MANAGED_DRAG - if (_managedDrag.PreprocessInputEvent(ref e)) - return UnmanagedMethods.DefWindowProc(hWnd, msg, wParam, lParam); -#endif - - if (shouldTakeFocus) - SetFocus(_hwnd); - - if (e != null && Input != null) - { - Input(e); - - if (e.Handled) - { - return IntPtr.Zero; - } - } - - using (_rendererLock.Lock()) - { - return DefWindowProc(hWnd, msg, wParam, lParam); - } - } - - private static int ToInt32(IntPtr ptr) - { - if (IntPtr.Size == 4) - return ptr.ToInt32(); - - return (int)(ptr.ToInt64() & 0xffffffff); - } - - private static int HighWord(int param) => param >> 16; - - private Point DipFromLParam(IntPtr lParam) - { - return new Point((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)) / Scaling; - } - - private PixelPoint PointFromLParam(IntPtr lParam) - { - return new PixelPoint((short)(ToInt32(lParam) & 0xffff), (short)(ToInt32(lParam) >> 16)); - } - - private bool ShouldIgnoreTouchEmulatedMessage() - { - if (!_multitouch) - { - return false; - } - - // MI_WP_SIGNATURE - // https://docs.microsoft.com/en-us/windows/win32/tablet/system-events-and-mouse-messages - const long marker = 0xFF515700L; - - var info = GetMessageExtraInfo().ToInt64(); - return (info & marker) == marker; - } - - private static RawInputModifiers GetMouseModifiers(IntPtr wParam) - { - var keys = (ModifierKeys)ToInt32(wParam); - var modifiers = WindowsKeyboardDevice.Instance.Modifiers; - - if (keys.HasFlagCustom(ModifierKeys.MK_LBUTTON)) - { - modifiers |= RawInputModifiers.LeftMouseButton; - } - - if (keys.HasFlagCustom(ModifierKeys.MK_RBUTTON)) - { - modifiers |= RawInputModifiers.RightMouseButton; - } - - if (keys.HasFlagCustom(ModifierKeys.MK_MBUTTON)) - { - modifiers |= RawInputModifiers.MiddleMouseButton; - } - - if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON1)) - { - modifiers |= RawInputModifiers.XButton1MouseButton; + lRet = CustomCaptionProc(hWnd, msg, wParam, lParam, ref callDwp); } - if (keys.HasFlagCustom(ModifierKeys.MK_XBUTTON2)) + if (callDwp) { - modifiers |= RawInputModifiers.XButton2MouseButton; + lRet = AppWndProc(hWnd, msg, wParam, lParam); } - return modifiers; + return lRet; } public INativeControlHostImpl NativeControlHost => _nativeControlHost; diff --git a/src/Windows/Avalonia.Win32/WindowImpl.cs b/src/Windows/Avalonia.Win32/WindowImpl.cs index 36398eb8109..6f22f940564 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -42,6 +42,10 @@ public partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGl private SavedWindowInfo _savedWindowInfo; private bool _isFullScreenActive; + private bool _isClientAreaExtended; + private Thickness _extendedMargins; + private Thickness _offScreenMargin; + private double _extendTitleBarHint = -1; #if USE_MANAGED_DRAG private readonly ManagedWindowResizeDragHelper _managedDrag; @@ -70,7 +74,8 @@ public partial class WindowImpl : IWindowImpl, EglGlPlatformSurface.IEglWindowGl private Size _minSize; private Size _maxSize; private POINT _maxTrackSize; - private WindowImpl _parent; + private WindowImpl _parent; + private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default; public WindowImpl() { @@ -159,7 +164,9 @@ public Thickness BorderThickness } } - public double Scaling => _scaling; + public double RenderScaling => _scaling; + + public double DesktopScaling => RenderScaling; public Size ClientSize { @@ -167,7 +174,7 @@ public Size ClientSize { GetClientRect(_hwnd, out var rect); - return new Size(rect.right, rect.bottom) / Scaling; + return new Size(rect.right, rect.bottom) / RenderScaling; } } @@ -175,7 +182,7 @@ public Size ClientSize public IPlatformHandle Handle { get; private set; } - public virtual Size MaxAutoSizeHint => new Size(_maxTrackSize.X / Scaling, _maxTrackSize.Y / Scaling); + public virtual Size MaxAutoSizeHint => new Size(_maxTrackSize.X / RenderScaling, _maxTrackSize.Y / RenderScaling); public IMouseDevice MouseDevice => _mouseDevice; @@ -183,6 +190,11 @@ public WindowState WindowState { get { + if(_isFullScreenActive) + { + return WindowState.FullScreen; + } + var placement = default(WINDOWPLACEMENT); GetWindowPlacement(_hwnd, ref placement); @@ -267,7 +279,7 @@ private WindowTransparencyLevel EnableBlur(WindowTransparencyLevel transparencyL } accent.AccentFlags = 2; - accent.GradientColor = 0x00FFFFFF; + accent.GradientColor = 0x01000000; var accentPtr = Marshal.AllocHGlobal(accentStructSize); Marshal.StructureToPtr(accent, accentPtr, false); @@ -332,8 +344,8 @@ public IRenderer CreateRenderer(IRenderRoot root) public void Resize(Size value) { - int requestedClientWidth = (int)(value.Width * Scaling); - int requestedClientHeight = (int)(value.Height * Scaling); + int requestedClientWidth = (int)(value.Width * RenderScaling); + int requestedClientHeight = (int)(value.Height * RenderScaling); GetClientRect(_hwnd, out var clientRect); @@ -379,11 +391,13 @@ public void Dispose() UnregisterClass(_className, GetModuleHandle(null)); _className = null; } + + _framebuffer.Dispose(); } public void Invalidate(Rect rect) { - var scaling = Scaling; + var scaling = RenderScaling; var r = new RECT { left = (int)Math.Floor(rect.X * scaling), @@ -399,12 +413,12 @@ public Point PointToClient(PixelPoint point) { var p = new POINT { X = point.X, Y = point.Y }; UnmanagedMethods.ScreenToClient(_hwnd, ref p); - return new Point(p.X, p.Y) / Scaling; + return new Point(p.X, p.Y) / RenderScaling; } public PixelPoint PointToScreen(Point point) { - point *= Scaling; + point *= RenderScaling; var p = new POINT { X = (int)point.X, Y = (int)point.Y }; ClientToScreen(_hwnd, ref p); return new PixelPoint(p.X, p.Y); @@ -668,6 +682,98 @@ private void SetFullScreen(bool fullscreen) } TaskBarList.MarkFullscreen(_hwnd, fullscreen); + + ExtendClientArea(); + } + + private MARGINS UpdateExtendMargins() + { + RECT borderThickness = new RECT(); + RECT borderCaptionThickness = new RECT(); + + AdjustWindowRectEx(ref borderCaptionThickness, (uint)(GetStyle()), false, 0); + AdjustWindowRectEx(ref borderThickness, (uint)(GetStyle() & ~WindowStyles.WS_CAPTION), false, 0); + borderThickness.left *= -1; + borderThickness.top *= -1; + borderCaptionThickness.left *= -1; + borderCaptionThickness.top *= -1; + + bool wantsTitleBar = _extendChromeHints.HasFlag(ExtendClientAreaChromeHints.SystemChrome) || _extendTitleBarHint == -1; + + if (!wantsTitleBar) + { + borderCaptionThickness.top = 1; + } + + MARGINS margins = new MARGINS(); + margins.cxLeftWidth = 1; + margins.cxRightWidth = 1; + margins.cyBottomHeight = 1; + + if (_extendTitleBarHint != -1) + { + borderCaptionThickness.top = (int)(_extendTitleBarHint * RenderScaling); + } + + margins.cyTopHeight = _extendChromeHints.HasFlag(ExtendClientAreaChromeHints.SystemChrome) && !_extendChromeHints.HasFlag(ExtendClientAreaChromeHints.PreferSystemChrome) ? borderCaptionThickness.top : 1; + + if (WindowState == WindowState.Maximized) + { + _extendedMargins = new Thickness(0, (borderCaptionThickness.top - borderThickness.top) / RenderScaling, 0, 0); + _offScreenMargin = new Thickness(borderThickness.left / RenderScaling, borderThickness.top / RenderScaling, borderThickness.right / RenderScaling, borderThickness.bottom / RenderScaling); + } + else + { + _extendedMargins = new Thickness(0, (borderCaptionThickness.top) / RenderScaling, 0, 0); + _offScreenMargin = new Thickness(); + } + + return margins; + } + + private void ExtendClientArea() + { + if (DwmIsCompositionEnabled(out bool compositionEnabled) < 0 || !compositionEnabled) + { + _isClientAreaExtended = false; + return; + } + + GetWindowRect(_hwnd, out var rcClient); + + // Inform the application of the frame change. + SetWindowPos(_hwnd, + IntPtr.Zero, + rcClient.left, rcClient.top, + rcClient.Width, rcClient.Height, + SetWindowPosFlags.SWP_FRAMECHANGED); + + if (_isClientAreaExtended && WindowState != WindowState.FullScreen) + { + var margins = UpdateExtendMargins(); + + DwmExtendFrameIntoClientArea(_hwnd, ref margins); + } + else + { + var margins = new MARGINS(); + DwmExtendFrameIntoClientArea(_hwnd, ref margins); + + _offScreenMargin = new Thickness(); + _extendedMargins = new Thickness(); + } + + if(!_isClientAreaExtended || (_extendChromeHints.HasFlag(ExtendClientAreaChromeHints.SystemChrome) && + !_extendChromeHints.HasFlag(ExtendClientAreaChromeHints.PreferSystemChrome))) + { + EnableCloseButton(_hwnd); + } + else + { + DisableCloseButton(_hwnd); + } + + ExtendClientAreaToDecorationsChanged?.Invoke(_isClientAreaExtended); } private void ShowWindow(WindowState state) @@ -818,9 +924,10 @@ private void UpdateWindowProperties(WindowProperties newProperties, bool forceCh // Otherwise it will still show in the taskbar. } + WindowStyles style; if ((oldProperties.IsResizable != newProperties.IsResizable) || forceChanges) { - var style = GetStyle(); + style = GetStyle(); if (newProperties.IsResizable) { @@ -841,7 +948,7 @@ private void UpdateWindowProperties(WindowProperties newProperties, bool forceCh if ((oldProperties.Decorations != newProperties.Decorations) || forceChanges) { - var style = GetStyle(); + style = GetStyle(); const WindowStyles fullDecorationFlags = WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU; @@ -886,7 +993,26 @@ private void UpdateWindowProperties(WindowProperties newProperties, bool forceCh SetWindowPosFlags.SWP_NOZORDER | SetWindowPosFlags.SWP_NOACTIVATE | SetWindowPosFlags.SWP_FRAMECHANGED); } - } + } + } + + private const int MF_BYCOMMAND = 0x0; + private const int MF_BYPOSITION = 0x400; + private const int MF_REMOVE = 0x1000; + private const int MF_ENABLED = 0x0; + private const int MF_GRAYED = 0x1; + private const int MF_DISABLED = 0x2; + private const int SC_CLOSE = 0xF060; + + void DisableCloseButton(IntPtr hwnd) + { + EnableMenuItem(GetSystemMenu(hwnd, false), SC_CLOSE, + MF_BYCOMMAND | MF_DISABLED | MF_GRAYED); + } + void EnableCloseButton(IntPtr hwnd) + { + EnableMenuItem(GetSystemMenu(hwnd, false), SC_CLOSE, + MF_BYCOMMAND | MF_ENABLED); } #if USE_MANAGED_DRAG @@ -910,8 +1036,50 @@ PixelSize EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Size } } + double EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Scaling => RenderScaling; + IntPtr EglGlPlatformSurface.IEglWindowGlPlatformSurfaceInfo.Handle => Handle.Handle; + public void SetExtendClientAreaToDecorationsHint(bool hint) + { + _isClientAreaExtended = hint; + + ExtendClientArea(); + } + + public void SetExtendClientAreaChromeHints(ExtendClientAreaChromeHints hints) + { + _extendChromeHints = hints; + + ExtendClientArea(); + } + + /// + public void SetExtendClientAreaTitleBarHeightHint(double titleBarHeight) + { + _extendTitleBarHint = titleBarHeight; + + ExtendClientArea(); + } + + /// + public bool IsClientAreaExtendedToDecorations => _isClientAreaExtended; + + /// + public Action ExtendClientAreaToDecorationsChanged { get; set; } + + /// + public bool NeedsManagedDecorations => _isClientAreaExtended && _extendChromeHints.HasFlag(ExtendClientAreaChromeHints.PreferSystemChrome); + + /// + public Thickness ExtendedMargins => _extendedMargins; + + /// + public Thickness OffScreenMargin => _offScreenMargin; + + /// + public AcrylicPlatformCompensationLevels AcrylicCompensationLevels { get; } = new AcrylicPlatformCompensationLevels(1, 0.8, 0); + private struct SavedWindowInfo { public WindowStyles Style { get; set; } diff --git a/src/iOS/Avalonia.iOS/TopLevelImpl.cs b/src/iOS/Avalonia.iOS/TopLevelImpl.cs index 83a68990d7e..5a85a5ea885 100644 --- a/src/iOS/Avalonia.iOS/TopLevelImpl.cs +++ b/src/iOS/Avalonia.iOS/TopLevelImpl.cs @@ -48,7 +48,7 @@ public TopLevelImpl() public new IPlatformHandle Handle => null; - public double Scaling => UIScreen.MainScreen.Scale; + public double RenderScaling => UIScreen.MainScreen.Scale; public override void LayoutSubviews() => Resized?.Invoke(ClientSize); diff --git a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj index 1c7077870a8..51d18e55d1a 100644 --- a/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj +++ b/src/tools/Avalonia.Designer.HostApp/Avalonia.Designer.HostApp.csproj @@ -18,6 +18,10 @@ - + + + + + diff --git a/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs new file mode 100644 index 00000000000..7af29a56a14 --- /dev/null +++ b/src/tools/Avalonia.Designer.HostApp/DesignXamlLoader.cs @@ -0,0 +1,16 @@ +using System; +using System.IO; +using System.Reflection; +using Avalonia.Markup.Xaml; +using Avalonia.Markup.Xaml.XamlIl; + +namespace Avalonia.Designer.HostApp +{ + class DesignXamlLoader : AvaloniaXamlLoader.IRuntimeXamlLoader + { + public object Load(Stream stream, Assembly localAsm, object o, Uri baseUri, bool designMode) + { + return AvaloniaXamlIlRuntimeCompiler.Load(stream, localAsm, o, baseUri, designMode); + } + } +} diff --git a/src/tools/Avalonia.Designer.HostApp/Program.cs b/src/tools/Avalonia.Designer.HostApp/Program.cs index 3163e1fbc38..4472dac4e35 100644 --- a/src/tools/Avalonia.Designer.HostApp/Program.cs +++ b/src/tools/Avalonia.Designer.HostApp/Program.cs @@ -1,6 +1,8 @@ using System; using System.IO; using System.Reflection; +using Avalonia.DesignerSupport; +using Avalonia.Markup.Xaml; namespace Avalonia.Designer.HostApp { @@ -40,8 +42,9 @@ static void Exec(string[] args) public static void Main(string[] args) #endif { + AvaloniaLocator.CurrentMutable.Bind() + .ToConstant(new DesignXamlLoader()); Avalonia.DesignerSupport.Remote.RemoteDesignerEntryPoint.Main(args); } - } } diff --git a/tests/Avalonia.Base.UnitTests/Data/Core/PropertyPathGrammarTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/PropertyPathGrammarTests.cs new file mode 100644 index 00000000000..99144b7381d --- /dev/null +++ b/tests/Avalonia.Base.UnitTests/Data/Core/PropertyPathGrammarTests.cs @@ -0,0 +1,103 @@ +using System.Collections.Generic; +using System.Linq; +using Avalonia.Markup.Parsers; +using Xunit; + +namespace Avalonia.Base.UnitTests.Data.Core +{ + public class PropertyPathGrammarTests + { + static void Check(string s, params PropertyPathGrammar.ISyntax[] expected) + { + var parsed = PropertyPathGrammar.Parse(s).ToList(); + Assert.Equal(expected.Length, parsed.Count); + for (var c = 0; c < parsed.Count; c++) + Assert.Equal(expected[c], parsed[c]); + } + + [Fact] + public void PropertyPath_Should_Support_Simple_Properties() + { + Check("SomeProperty", new PropertyPathGrammar.PropertySyntax {Name = "SomeProperty"}); + } + + [Fact] + public void PropertyPath_Should_Ignore_Trailing_Whitespace() + { + Check(" SomeProperty ", new PropertyPathGrammar.PropertySyntax {Name = "SomeProperty"}); + } + + [Fact] + public void PropertyPath_Should_Support_Qualified_Properties() + { + Check(" ( somens:SomeType.SomeProperty ) ", + new PropertyPathGrammar.TypeQualifiedPropertySyntax() + { + Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens" + }); + } + + [Fact] + public void PropertyPath_Should_Support_Property_Paths() + { + Check(" ( somens:SomeType.SomeProperty ).Child . SubChild ", + new PropertyPathGrammar.TypeQualifiedPropertySyntax() + { + Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "Child"}, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "SubChild"} + ); + } + + [Fact] + public void PropertyPath_Should_Support_Casts() + { + Check(" ( somens:SomeType.SomeProperty ) :> SomeType.Child as somens:SomeType . SubChild ", + new PropertyPathGrammar.TypeQualifiedPropertySyntax() + { + Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens" + }, + new PropertyPathGrammar.CastTypeSyntax + { + TypeName = "SomeType" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "Child"}, + new PropertyPathGrammar.CastTypeSyntax + { + TypeName = "SomeType", + TypeNamespace = "somens" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "SubChild"} + ); + } + + [Fact] + public void PropertyPath_Should_Support_Ensure_Type() + { + Check(" ( somens:SomeType.SomeProperty ) := SomeType.Child := somens:SomeType . SubChild ", + new PropertyPathGrammar.TypeQualifiedPropertySyntax() + { + Name = "SomeProperty", TypeName = "SomeType", TypeNamespace = "somens" + }, + new PropertyPathGrammar.EnsureTypeSyntax + { + TypeName = "SomeType" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "Child"}, + new PropertyPathGrammar.EnsureTypeSyntax + { + TypeName = "SomeType", + TypeNamespace = "somens" + }, + PropertyPathGrammar.ChildTraversalSyntax.Instance, + new PropertyPathGrammar.PropertySyntax {Name = "SubChild"} + ); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs index e533001242f..58ddc8ca606 100644 --- a/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ApplicationTests.cs @@ -1,6 +1,5 @@ using System; -using System.Collections.Generic; -using Avalonia.Threading; +using Avalonia.Data; using Avalonia.UnitTests; using Xunit; @@ -32,5 +31,20 @@ public void Raises_ResourcesChanged_When_Event_Handler_Added_After_Resources_Has Assert.True(raised); } } + + [Fact] + public void Can_Bind_To_DataContext() + { + using (UnitTestApplication.Start()) + { + var application = Application.Current; + + application.DataContext = "Test"; + + application.Bind(Application.NameProperty, new Binding(".")); + + Assert.Equal("Test", Application.Current.Name); + } + } } } diff --git a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj index 2ca93dcf56b..19c4454d3d1 100644 --- a/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj +++ b/tests/Avalonia.Controls.UnitTests/Avalonia.Controls.UnitTests.csproj @@ -13,6 +13,7 @@ + diff --git a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs index 9a81d19bb93..7a2109e5a77 100644 --- a/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ContextMenuTests.cs @@ -191,8 +191,7 @@ public void Context_Menu_In_Resources_Can_Be_Shared() "; - var loader = new AvaloniaXamlLoader(); - var window = (Window)loader.Load(xaml); + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var target1 = window.Find("target1"); var target2 = window.Find("target2"); var mouse = new MouseTestHelper(); @@ -235,8 +234,7 @@ public void Context_Menu_Can_Be_Set_In_Style() "; - var loader = new AvaloniaXamlLoader(); - var window = (Window)loader.Load(xaml); + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var target1 = window.Find("target1"); var target2 = window.Find("target2"); var mouse = new MouseTestHelper(); @@ -298,7 +296,7 @@ private IDisposable Application() var windowImpl = MockWindowingPlatform.CreateWindowMock(); popupImpl = MockWindowingPlatform.CreatePopupMock(windowImpl.Object); - popupImpl.SetupGet(x => x.Scaling).Returns(1); + popupImpl.SetupGet(x => x.RenderScaling).Returns(1); windowImpl.Setup(x => x.CreatePopup()).Returns(popupImpl.Object); windowImpl.Setup(x => x.Screen).Returns(screenImpl.Object); diff --git a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs index dee7a848129..84f02aeda5f 100644 --- a/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs +++ b/tests/Avalonia.Controls.UnitTests/DesktopStyleApplicationLifetimeTests.cs @@ -191,7 +191,8 @@ public void Impl_Closing_Should_Remove_Window_From_OpenWindows() { var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Closed); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); var services = TestServices.StyledWindow.With( windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object)); diff --git a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs index fddc02f19c5..fab57cec494 100644 --- a/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Presenters/ItemsPresenterTests.cs @@ -60,6 +60,25 @@ public void Should_Add_Containers_Of_Correct_Type() Assert.IsType(target.Panel.Children[1]); } + [Fact] + public void Should_Create_Containers_Only_Once() + { + var parent = new TestItemsControl(); + var target = new ItemsPresenter + { + Items = new[] { "foo", "bar" }, + [StyledElement.TemplatedParentProperty] = parent, + }; + var raised = 0; + + parent.ItemContainerGenerator.Materialized += (s, e) => ++raised; + + target.ApplyTemplate(); + + Assert.Equal(2, target.Panel.Children.Count); + Assert.Equal(2, raised); + } + [Fact] public void ItemContainerGenerator_Should_Be_Picked_Up_From_TemplatedControl() { diff --git a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs index fe9c7b1261c..9ef2750ff34 100644 --- a/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/Primitives/SelectingItemsControlTests.cs @@ -531,6 +531,7 @@ public void Removing_Selected_Item_Should_Clear_Selection() }; target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); target.SelectedIndex = 1; Assert.Equal(items[1], target.SelectedItem); @@ -549,6 +550,45 @@ public void Removing_Selected_Item_Should_Clear_Selection() Assert.NotNull(receivedArgs); Assert.Empty(receivedArgs.AddedItems); Assert.Equal(new[] { removed }, receivedArgs.RemovedItems); + Assert.False(items.Single().IsSelected); + } + + [Fact] + public void Removing_Selected_Item_Should_Clear_Selection_With_BeginInit() + { + var items = new AvaloniaList + { + new Item(), + new Item(), + }; + + var target = new SelectingItemsControl(); + target.BeginInit(); + target.Items = items; + target.Template = Template(); + target.EndInit(); + + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + target.SelectedIndex = 0; + + Assert.Equal(items[0], target.SelectedItem); + Assert.Equal(0, target.SelectedIndex); + + SelectionChangedEventArgs receivedArgs = null; + + target.SelectionChanged += (_, args) => receivedArgs = args; + + var removed = items[0]; + + items.RemoveAt(0); + + Assert.Null(target.SelectedItem); + Assert.Equal(-1, target.SelectedIndex); + Assert.NotNull(receivedArgs); + Assert.Empty(receivedArgs.AddedItems); + Assert.Equal(new[] { removed }, receivedArgs.RemovedItems); + Assert.False(items.Single().IsSelected); } [Fact] diff --git a/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs b/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs new file mode 100644 index 00000000000..6e171a58e7c --- /dev/null +++ b/tests/Avalonia.Controls.UnitTests/RelativePanelTests.cs @@ -0,0 +1,136 @@ +using Avalonia.Controls.Shapes; +using Xunit; + +namespace Avalonia.Controls.UnitTests +{ + public class RelativePanelTests + { + [Fact] + public void Lays_Out_1_Child_Next_the_other() + { + var rect1 = new Rectangle { Height = 20, Width = 20 }; + var rect2 = new Rectangle { Height = 20, Width = 20 }; + + var target = new RelativePanel + { + VerticalAlignment = Layout.VerticalAlignment.Top, + HorizontalAlignment = Layout.HorizontalAlignment.Left, + Children = + { + rect1, rect2 + } + }; + + RelativePanel.SetAlignLeftWithPanel(rect1 , true); + RelativePanel.SetRightOf(rect2, rect1); + target.Measure(new Size(400, 400)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(40, 20), target.Bounds.Size); + Assert.Equal(new Rect(0, 0, 20, 20), target.Children[0].Bounds); + Assert.Equal(new Rect(20, 0, 20, 20), target.Children[1].Bounds); + } + + [Fact] + public void Lays_Out_1_Child_Below_the_other() + { + var rect1 = new Rectangle { Height = 20, Width = 20 }; + var rect2 = new Rectangle { Height = 20, Width = 20 }; + + var target = new RelativePanel + { + VerticalAlignment = Layout.VerticalAlignment.Top, + HorizontalAlignment = Layout.HorizontalAlignment.Left, + Children = + { + rect1, rect2 + } + }; + + RelativePanel.SetAlignLeftWithPanel(rect1, true); + RelativePanel.SetBelow(rect2, rect1); + target.Measure(new Size(400, 400)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(20, 40), target.Bounds.Size); + Assert.Equal(new Rect(0, 0, 20, 20), target.Children[0].Bounds); + Assert.Equal(new Rect(0, 20, 20, 20), target.Children[1].Bounds); + } + + [Fact] + public void RelativePanel_Can_Center() + { + var rect1 = new Rectangle { Height = 20, Width = 20 }; + var rect2 = new Rectangle { Height = 20, Width = 20 }; + + var target = new RelativePanel + { + VerticalAlignment = Layout.VerticalAlignment.Center, + HorizontalAlignment = Layout.HorizontalAlignment.Center, + Children = + { + rect1, rect2 + } + }; + + RelativePanel.SetAlignLeftWithPanel(rect1, true); + RelativePanel.SetBelow(rect2, rect1); + target.Measure(new Size(400, 400)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(20, 40), target.Bounds.Size); + Assert.Equal(new Rect(0, 0, 20, 20), target.Children[0].Bounds); + Assert.Equal(new Rect(0, 20, 20, 20), target.Children[1].Bounds); + } + + [Fact] + public void LeftOf_Measures_Correctly() + { + var rect1 = new Rectangle { Height = 20, Width = 20 }; + var rect2 = new Rectangle { Height = 20, Width = 20 }; + + var target = new RelativePanel + { + VerticalAlignment = Layout.VerticalAlignment.Center, + HorizontalAlignment = Layout.HorizontalAlignment.Center, + Children = + { + rect1, rect2 + } + }; + + RelativePanel.SetLeftOf(rect2, rect1); + target.Measure(new Size(400, 400)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(20, 20), target.Bounds.Size); + Assert.Equal(new Rect(0, 0, 20, 20), target.Children[0].Bounds); + Assert.Equal(new Rect(-20, 0, 20, 20), target.Children[1].Bounds); + } + + [Fact] + public void Above_Measures_Correctly() + { + var rect1 = new Rectangle { Height = 20, Width = 20 }; + var rect2 = new Rectangle { Height = 20, Width = 20 }; + + var target = new RelativePanel + { + VerticalAlignment = Layout.VerticalAlignment.Center, + HorizontalAlignment = Layout.HorizontalAlignment.Center, + Children = + { + rect1, rect2 + } + }; + + RelativePanel.SetAbove(rect2, rect1); + target.Measure(new Size(400, 400)); + target.Arrange(new Rect(target.DesiredSize)); + + Assert.Equal(new Size(20, 20), target.Bounds.Size); + Assert.Equal(new Rect(0, 0, 20, 20), target.Children[0].Bounds); + Assert.Equal(new Rect(0, -20, 20, 20), target.Children[1].Bounds); + } + } +} diff --git a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs index db9211ac3c0..fd52aeb9af2 100644 --- a/tests/Avalonia.Controls.UnitTests/TabControlTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TabControlTests.cs @@ -8,6 +8,7 @@ using Avalonia.Controls.Templates; using Avalonia.Controls.Utils; using Avalonia.LogicalTree; +using Avalonia.Markup.Xaml; using Avalonia.Styling; using Avalonia.UnitTests; using Xunit; @@ -338,8 +339,7 @@ public void Can_Have_Empty_Tab_Control() xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'> "; - var loader = new Markup.Xaml.AvaloniaXamlLoader(); - var window = (Window)loader.Load(xaml); + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var tabControl = window.FindControl("tabs"); tabControl.DataContext = new { Tabs = new List() }; diff --git a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs index 34b37e76352..9d7bc6af74f 100644 --- a/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs +++ b/tests/Avalonia.Controls.UnitTests/ToolTipTests.cs @@ -30,6 +30,40 @@ public void Should_Not_Open_On_Detached_Control() Assert.False(ToolTip.GetIsOpen(control)); } + + [Fact] + public void Should_Close_When_Control_Detaches() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var window = new Window(); + + var panel = new Panel(); + + var target = new Decorator() + { + [ToolTip.TipProperty] = "Tip", + [ToolTip.ShowDelayProperty] = 0 + }; + + panel.Children.Add(target); + + window.Content = panel; + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + Assert.True((target as IVisual).IsAttachedToVisualTree); + + _mouseHelper.Enter(target); + + Assert.True(ToolTip.GetIsOpen(target)); + + panel.Children.Remove(target); + + Assert.False(ToolTip.GetIsOpen(target)); + } + } [Fact] public void Should_Open_On_Pointer_Enter() diff --git a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs index e49e273becc..6b30aed257b 100644 --- a/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TopLevelTests.cs @@ -93,7 +93,7 @@ public void Bounds_Should_Be_Set_After_Layout_Pass() { var impl = new Mock(); impl.SetupProperty(x => x.Resized); - impl.SetupGet(x => x.Scaling).Returns(1); + impl.SetupGet(x => x.RenderScaling).Returns(1); var target = new TestTopLevel(impl.Object) { @@ -290,7 +290,7 @@ public void Reacts_To_Changes_In_Global_Styles() using (UnitTestApplication.Start(TestServices.StyledWindow)) { var impl = new Mock(); - impl.SetupGet(x => x.Scaling).Returns(1); + impl.SetupGet(x => x.RenderScaling).Returns(1); var child = new Border { Classes = { "foo" } }; var target = new TestTopLevel(impl.Object) diff --git a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs index c1bd45bcad7..c25ad19027b 100644 --- a/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs +++ b/tests/Avalonia.Controls.UnitTests/TreeViewTests.cs @@ -1273,8 +1273,6 @@ public IControl Build(object param) return new TextBlock { Text = node.Value }; } - public bool SupportsRecycling => false; - public InstancedBinding ItemsSelector(object item) { var obs = ExpressionObserver.Create(item, o => (o as Node).Children); diff --git a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs index 697ea9cff8f..84f212d1b36 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowBaseTests.cs @@ -110,7 +110,8 @@ public void IsVisible_Should_Be_False_Atfer_Hide() public void IsVisible_Should_Be_False_Atfer_Impl_Signals_Close() { var windowImpl = new Mock(); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.SetupProperty(x => x.Closed); using (UnitTestApplication.Start(TestServices.StyledWindow)) @@ -128,7 +129,8 @@ public void IsVisible_Should_Be_False_Atfer_Impl_Signals_Close() public void Setting_IsVisible_True_Shows_Window() { var windowImpl = new Mock(); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -143,7 +145,8 @@ public void Setting_IsVisible_True_Shows_Window() public void Setting_IsVisible_False_Hides_Window() { var windowImpl = new Mock(); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); using (UnitTestApplication.Start(TestServices.StyledWindow)) { @@ -208,7 +211,8 @@ public void Renderer_Should_Be_Disposed_When_Impl_Signals_Close() { var renderer = new Mock(); var windowImpl = new Mock(); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.SetupProperty(x => x.Closed); windowImpl.Setup(x => x.CreateRenderer(It.IsAny())).Returns(renderer.Object); @@ -237,7 +241,7 @@ private class TestWindowBase : WindowBase public TestWindowBase(IRenderer renderer = null) : base(Mock.Of(x => - x.Scaling == 1 && + x.RenderScaling == 1 && x.CreateRenderer(It.IsAny()) == renderer)) { } diff --git a/tests/Avalonia.Controls.UnitTests/WindowTests.cs b/tests/Avalonia.Controls.UnitTests/WindowTests.cs index e2b0def00b1..ba29001cf38 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowTests.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowTests.cs @@ -100,7 +100,8 @@ public void IsVisible_Should_Be_False_After_Impl_Signals_Close() { var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Closed); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); var services = TestServices.StyledWindow.With( windowingPlatform: new MockWindowingPlatform(() => windowImpl.Object)); @@ -206,7 +207,8 @@ public async Task ShowDialog_With_ValueType_Returns_Default_When_Closed() var parent = new Mock(); var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Closed); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); var target = new Window(windowImpl.Object); var task = target.ShowDialog(parent.Object); @@ -245,7 +247,8 @@ public async Task Calling_ShowDialog_On_Closed_Window_Should_Throw() var parent = new Mock(); var windowImpl = new Mock(); windowImpl.SetupProperty(x => x.Closed); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); var target = new Window(windowImpl.Object); var task = target.ShowDialog(parent.Object); @@ -273,7 +276,8 @@ public void Window_Should_Be_Centered_When_WindowStartupLocation_Is_CenterScreen var windowImpl = MockWindowingPlatform.CreateWindowMock(); windowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); windowImpl.Setup(x => x.Screen).Returns(screens.Object); using (UnitTestApplication.Start(TestServices.StyledWindow)) @@ -298,12 +302,14 @@ public void Window_Should_Be_Centered_Relative_To_Owner_When_WindowStartupLocati var parentWindowImpl = MockWindowingPlatform.CreateWindowMock(); parentWindowImpl.Setup(x => x.ClientSize).Returns(new Size(800, 480)); parentWindowImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1920, 1080)); - parentWindowImpl.Setup(x => x.Scaling).Returns(1); + parentWindowImpl.Setup(x => x.DesktopScaling).Returns(1); + parentWindowImpl.Setup(x => x.RenderScaling).Returns(1); var windowImpl = MockWindowingPlatform.CreateWindowMock(); windowImpl.Setup(x => x.ClientSize).Returns(new Size(320, 200)); windowImpl.Setup(x => x.MaxAutoSizeHint).Returns(new Size(1920, 1080)); - windowImpl.Setup(x => x.Scaling).Returns(1); + windowImpl.Setup(x => x.DesktopScaling).Returns(1); + windowImpl.Setup(x => x.RenderScaling).Returns(1); var parentWindowServices = TestServices.StyledWindow.With( windowingPlatform: new MockWindowingPlatform(() => parentWindowImpl.Object)); @@ -565,7 +571,7 @@ protected override void Show(Window window) private IWindowImpl CreateImpl(Mock renderer) { return Mock.Of(x => - x.Scaling == 1 && + x.RenderScaling == 1 && x.CreateRenderer(It.IsAny()) == renderer.Object); } diff --git a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs index 25e8c82b1a2..bf1322afbc3 100644 --- a/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs +++ b/tests/Avalonia.Controls.UnitTests/WindowingPlatformMock.cs @@ -17,7 +17,7 @@ public WindowingPlatformMock(Func windowImpl = null, Func(x => x.Scaling == 1); + return _windowImpl?.Invoke() ?? Mock.Of(x => x.RenderScaling == 1); } public IWindowImpl CreateEmbeddableWindow() @@ -25,6 +25,6 @@ public IWindowImpl CreateEmbeddableWindow() throw new NotImplementedException(); } - public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of(x => x.Scaling == 1); + public IPopupImpl CreatePopup() => _popupImpl?.Invoke() ?? Mock.Of(x => x.RenderScaling == 1); } } diff --git a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs index 572749a58ab..c6ecc0a7e55 100644 --- a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs @@ -41,7 +41,7 @@ public void Should_Create_Typeface_From_Fallback_Bold() var fontManager = new FontManagerImpl(); var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily("A, B, Arial"), FontWeight.Bold)); + new Typeface(new FontFamily("A, B, Arial"), weight: FontWeight.Bold)); var font = glyphTypeface.DWFont; @@ -105,7 +105,7 @@ public void Should_Load_Nearest_Matching_Font() var fontManager = new FontManagerImpl(); var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(s_fontUri, FontWeight.Black, FontStyle.Italic)); + new Typeface(s_fontUri, FontStyle.Italic, FontWeight.Black)); var font = glyphTypeface.DWFont; diff --git a/tests/Avalonia.LeakTests/ControlTests.cs b/tests/Avalonia.LeakTests/ControlTests.cs index 00ef503b8de..530b8fa20c5 100644 --- a/tests/Avalonia.LeakTests/ControlTests.cs +++ b/tests/Avalonia.LeakTests/ControlTests.cs @@ -355,7 +355,7 @@ public void RendererIsDisposed() var renderer = new Mock(); renderer.Setup(x => x.Dispose()); var impl = new Mock(); - impl.SetupGet(x => x.Scaling).Returns(1); + impl.SetupGet(x => x.RenderScaling).Returns(1); impl.SetupProperty(x => x.Closed); impl.Setup(x => x.CreateRenderer(It.IsAny())).Returns(renderer.Object); impl.Setup(x => x.Dispose()).Callback(() => impl.Object.Closed()); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj index e8c4daa7bc0..ad3592294dd 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Avalonia.Markup.Xaml.UnitTests.csproj @@ -11,6 +11,7 @@ + diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ConverterTests.cs index b424003ed60..c9420f1696d 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ConverterTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ConverterTests.cs @@ -9,7 +9,7 @@ public class ConverterTests : XamlTestBase public void Bug_2228_Relative_Uris_Should_Be_Correctly_Parsed() { var testClass = typeof(TestClassWithUri); - var parsed = AvaloniaXamlLoader.Parse( + var parsed = AvaloniaRuntimeXamlLoader.Parse( $"<{testClass.Name} xmlns='clr-namespace:{testClass.Namespace}' Uri='/test'/>", testClass.Assembly); Assert.False(parsed.Uri.IsAbsoluteUri); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs index a77723afe1b..466ae1bf7cd 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/MultiValueConverterTests.cs @@ -29,8 +29,7 @@ public void MultiValueConverter_Special_Values_Work() "; - var loader = new AvaloniaXamlLoader(); - var window = (Window)loader.Load(xaml); + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var textBlock = window.FindControl("textBlock"); window.ApplyTemplate(); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs index cdd40ed80f2..eb8851c80b7 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/NullableConverterTests.cs @@ -22,8 +22,7 @@ public void Nullable_Types_Should_Still_Be_Converted_Properly() xmlns='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Converters' Thickness = '5' Orientation='Vertical' >"; - var loader = new AvaloniaXamlLoader(); - var data = (ClassWithNullableProperties)loader.Load(xaml, typeof(ClassWithNullableProperties).Assembly); + var data = (ClassWithNullableProperties)AvaloniaRuntimeXamlLoader.Load(xaml, typeof(ClassWithNullableProperties).Assembly); Assert.Equal(new Thickness(5), data.Thickness); Assert.Equal(Orientation.Vertical, data.Orientation); } diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs index b060905f38d..3b729e9cd80 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/PointsListTypeConverterTests.cs @@ -31,8 +31,7 @@ public void TypeConverter_Should_Parse(string input) public void Should_Parse_Points_in_Xaml(string input) { var xaml = $""; - var loader = new AvaloniaXamlLoader(); - var polygon = (Polygon)loader.Load(xaml); + var polygon = (Polygon)AvaloniaRuntimeXamlLoader.Load(xaml); var points = polygon.Points; diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs index 5e698117c36..4d5983e2763 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Converters/ValueConverterTests.cs @@ -21,8 +21,7 @@ public void ValueConverter_Special_Values_Work() xmlns:c='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Converters;assembly=Avalonia.Markup.Xaml.UnitTests'> "; - var loader = new AvaloniaXamlLoader(); - var window = (Window)loader.Load(xaml); + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var textBlock = window.FindControl("textBlock"); window.ApplyTemplate(); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs index 6730e3134d5..afc4a36feae 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests.cs @@ -24,8 +24,7 @@ public void Binding_With_Null_Path_Works() xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'> "; - var loader = new AvaloniaXamlLoader(); - var window = (Window)loader.Load(xaml); + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var textBlock = window.FindControl("textBlock"); window.DataContext = "foo"; @@ -45,8 +44,7 @@ public void Binding_To_DoNothing_Works() xmlns:x='http://schemas.microsoft.com/winfx/2006/xaml'> "; - var loader = new AvaloniaXamlLoader(); - var window = (Window)loader.Load(xaml); + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var textBlock = window.FindControl("textBlock"); window.ApplyTemplate(); @@ -86,8 +84,7 @@ public void MultiBinding_TemplatedParent_Works() "; - var loader = new AvaloniaXamlLoader(); - var window = (Window)loader.Load(xaml); + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var textBox = window.FindControl("textBox"); window.ApplyTemplate(); diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs index b2b4c4da1ab..a7a004bd49b 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Data/BindingTests_Method.cs @@ -20,8 +20,7 @@ public void Binding_Method_To_Command_Works() xmlns:local='clr-namespace:Avalonia.Markup.Xaml.UnitTests.Xaml;assembly=Avalonia.Markup.Xaml.UnitTests'> "; - var loader = new AvaloniaXamlLoader(); - var window = (Window)loader.Load(xaml); + var window = (Window)AvaloniaRuntimeXamlLoader.Load(xaml); var button = window.FindControl "; - var control = AvaloniaXamlLoader.Parse(xaml); + var control = AvaloniaRuntimeXamlLoader.Parse(xaml); var button = control.FindControl "; - var control = AvaloniaXamlLoader.Parse(xaml); + var control = AvaloniaRuntimeXamlLoader.Parse(xaml); var button = control.FindControl