diff --git a/.gitmodules b/.gitmodules index 10c780c09f..9dbc50ef61 100644 --- a/.gitmodules +++ b/.gitmodules @@ -3,4 +3,4 @@ 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 + url = https://github.com/kekekeks/XamlX.git diff --git a/Avalonia.sln b/Avalonia.sln index f6dc039c2f..4ab647a25e 100644 --- a/Avalonia.sln +++ b/Avalonia.sln @@ -207,6 +207,10 @@ 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 Global GlobalSection(SharedMSBuildProjectFiles) = preSolution src\Shared\RenderHelpers\RenderHelpers.projitems*{3c4c0cb4-0c0f-4450-a37b-148c84ff905f}*SharedItemsImports = 13 @@ -1826,6 +1830,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 diff --git a/native/Avalonia.Native/inc/avalonia-native.h b/native/Avalonia.Native/inc/avalonia-native.h index 6800ff7d68..9ff6130e5f 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/window.h b/native/Avalonia.Native/src/OSX/window.h index bdf3007a28..b1f64bca88 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 7f8a6e1393..872269bb26 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 @@ public: View = NULL; Window = NULL; } - NSVisualEffectView* VisualEffect; + AutoFitContentView* StandardContainer; AvnView* View; AvnWindow* Window; ComPtr BaseEvents; @@ -39,6 +37,7 @@ public: _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 @@ public: [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 @@ public: 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 @@ private: bool _inSetWindowState; NSRect _preZoomSize; bool _transitioningWindowState; + bool _isClientAreaExtended; + AvnExtendClientAreaChromeHints _extendClientHints; FORWARD_IUNKNOWN() BEGIN_INTERFACE_MAP() @@ -505,6 +497,8 @@ private: ComPtr WindowEvents; WindowImpl(IAvnWindowEvents* events, IAvnGlContext* gl) : WindowBaseImpl(events, gl) { + _isClientAreaExtended = false; + _extendClientHints = AvnDefaultChrome; _fullScreenActive = false; _canResize = true; _decorations = SystemDecorationsFull; @@ -523,8 +517,20 @@ private: 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 @@ private: 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 @@ private: return S_OK; } - auto currentFrame = [Window frame]; - UpdateStyle(); HideOrShowTrafficLights(); @@ -790,6 +823,81 @@ private: 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 @@ private: [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 @@ protected: { 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 @@ -1523,15 +1733,43 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent 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 +1888,7 @@ NSArray* AllLoopModes = [NSArray arrayWithObjects: NSDefaultRunLoopMode, NSEvent [self setOpaque:NO]; [self setBackgroundColor: [NSColor clearColor]]; [self invalidateShadow]; + _isExtended = false; return self; } diff --git a/nukebuild/Build.cs b/nukebuild/Build.cs index 890967ae4f..fe877dc49c 100644 --- a/nukebuild/Build.cs +++ b/nukebuild/Build.cs @@ -268,6 +268,8 @@ partial class Build : NukeBuild .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 0000000000..44f01da669 --- /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 584c36d033..4c64d4ff93 100644 --- a/nukebuild/_build.csproj +++ b/nukebuild/_build.csproj @@ -14,6 +14,9 @@ + + + diff --git a/samples/BindingDemo/MainWindow.xaml b/samples/BindingDemo/MainWindow.xaml index 26a62ebca6..14c371efef 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"> + @@ -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 d6d4a71ad3..b0c205246e 100644 --- a/samples/ControlCatalog/MainView.xaml.cs +++ b/samples/ControlCatalog/MainView.xaml.cs @@ -58,13 +58,6 @@ namespace ControlCatalog 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 76422bc130..97bd88f5e4 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 0000000000..96cfcc5288 --- /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 0000000000..44e7c4b92b --- /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 8133d0e408..bfd14cc627 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 8b697b7948..a4427c70c5 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 486cc55d44..369f703718 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 8f147638b5..8ccd8e97f7 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 65a798e53c..c22cf68b68 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/LayoutTransformControlPage.xaml b/samples/ControlCatalog/Pages/LayoutTransformControlPage.xaml index 446dfd7ce1..8cf3610b47 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/TextBlockPage.xaml b/samples/ControlCatalog/Pages/TextBlockPage.xaml index f73ef9b4fb..4b8edcf98c 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 73d83e08f1..ec073d48a9 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 0000000000..b90f43c3b6 --- /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 0000000000..d8d4f3f371 --- /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 1ee6bf7a29..7c911e91e9 100644 --- a/samples/ControlCatalog/SideBar.xaml +++ b/samples/ControlCatalog/SideBar.xaml @@ -12,7 +12,7 @@ diff --git a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs index 0257b4ce66..4356a032fa 100644 --- a/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs +++ b/samples/ControlCatalog/ViewModels/MainWindowViewModel.cs @@ -3,6 +3,8 @@ using Avalonia.Controls; 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 @@ namespace ControlCatalog.ViewModels 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 @@ namespace ControlCatalog.ViewModels 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/Pages/ClippingPage.xaml b/samples/RenderDemo/Pages/ClippingPage.xaml index 8920b8a123..10225f7c49 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 d6bde0b146..07fdb91a16 100644 --- a/samples/RenderDemo/SideBar.xaml +++ b/samples/RenderDemo/SideBar.xaml @@ -3,7 +3,7 @@ diff --git a/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj index c623ae68b5..cc831ef8ae 100644 --- a/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj +++ b/samples/interop/NativeEmbedSample/NativeEmbedSample.csproj @@ -10,6 +10,7 @@ + diff --git a/src/Avalonia.Base/AvaloniaProperty.cs b/src/Avalonia.Base/AvaloniaProperty.cs index 09480f2701..a873d5fd42 100644 --- a/src/Avalonia.Base/AvaloniaProperty.cs +++ b/src/Avalonia.Base/AvaloniaProperty.cs @@ -2,6 +2,7 @@ using System; 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 @@ namespace Avalonia 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 0000000000..f66411c2c2 --- /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 3bacd38a20..7ecaa278d7 100644 --- a/src/Avalonia.Base/Data/Core/ExpressionObserver.cs +++ b/src/Avalonia.Base/Data/Core/ExpressionObserver.cs @@ -54,6 +54,7 @@ namespace Avalonia.Data.Core private object _root; private IDisposable _rootSubscription; private WeakReference _value; + private IReadOnlyList _transformNodes; /// /// Initializes a new instance of the class. @@ -188,6 +189,24 @@ namespace Avalonia.Data.Core 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 @@ namespace Avalonia.Data.Core { 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 0000000000..2417d0ffc4 --- /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 dad0832e12..debf050a97 100644 --- a/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs +++ b/src/Avalonia.Base/Data/Core/Plugins/TaskStreamPlugin.cs @@ -59,7 +59,7 @@ namespace Avalonia.Data.Core.Plugins 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 aa4fe55f79..d5e835cabd 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 @@ namespace Avalonia.Data.Core _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 @@ namespace Avalonia.Data.Core { 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 @@ namespace Avalonia.Data.Core 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 0000000000..665953c4a1 --- /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 e501c0a03d..023999f5c5 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/Utilities/CharacterReader.cs b/src/Avalonia.Base/Utilities/CharacterReader.cs index 0a05915802..2b98785d0d 100644 --- a/src/Avalonia.Base/Utilities/CharacterReader.cs +++ b/src/Avalonia.Base/Utilities/CharacterReader.cs @@ -79,5 +79,25 @@ namespace Avalonia.Utilities 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 0000000000..16ef95f5f4 --- /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 @@ -21,15 +21,18 @@ 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 f94f10f792..5a2c74e16f 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.Helpers.cs @@ -5,7 +5,7 @@ using Avalonia.Utilities; 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 30e8f120d7..c1cc0e7bf0 100644 --- a/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs +++ b/src/Avalonia.Build.Tasks/XamlCompilerTaskExecutor.cs @@ -7,17 +7,18 @@ using System.Text; 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 @@ namespace Avalonia.Build.Tasks 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 @@ namespace Avalonia.Build.Tasks // 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 @@ namespace Avalonia.Build.Tasks 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/Themes/Default.xaml b/src/Avalonia.Controls.DataGrid/Themes/Default.xaml index 17e7ecba43..738732f671 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 95bd9550a5..ae5b902ae8 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 88266ac979..893d64f505 100644 --- a/src/Avalonia.Themes.Default/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Default/ToggleSwitch.xaml @@ -43,8 +43,7 @@ - - + diff --git a/src/Avalonia.Themes.Default/Window.xaml b/src/Avalonia.Themes.Default/Window.xaml index 0e7ec42856..739887fb35 100644 --- a/src/Avalonia.Themes.Default/Window.xaml +++ b/src/Avalonia.Themes.Default/Window.xaml @@ -5,7 +5,9 @@ - + + + #FF0066CC #FFFFFFFF #FF000000 - Segoe UI - 14 + avares://Avalonia.Themes.Fluent/Assets#Open Sans + 13 True diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml index 68b33d104a..fb08b97b47 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseDark.xaml @@ -158,8 +158,6 @@ XamlAutoFontFamily Segoe WP Segoe WP SemiLight - XamlAutoFontFamily - XamlAutoFontFamily XamlAutoFontFamily Segoe MDL2 Assets XamlAutoFontFamily @@ -217,9 +215,6 @@ 96 8 16 - 24 - 40 - 14 0.6 4 2 @@ -262,7 +257,6 @@ 44 44 12 - -25 0,0,0,0 0,0,0,0 0,0,0,0 @@ -312,12 +306,6 @@ 19,19,19,0 19,37,19,0 0,0,0,32.5 - 12,0,12,0 - 12,0,12,0 - 12,14,0,13 - 0 - 0,6,0,0 - 12,14,0,13 0 2 1 @@ -346,8 +334,6 @@ Normal SemiLight Normal - SemiLight - Bold Normal Normal Normal diff --git a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml index d8a641e8ac..6c8d16ddfa 100644 --- a/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/BaseLight.xaml @@ -157,8 +157,6 @@ XamlAutoFontFamily Segoe WP Segoe WP SemiLight - XamlAutoFontFamily - XamlAutoFontFamily XamlAutoFontFamily Segoe MDL2 Assets XamlAutoFontFamily @@ -216,9 +214,6 @@ 96 8 16 - 24 - 40 - 14 0.6 4 2 @@ -261,7 +256,6 @@ 44 44 12 - -25 0,0,0,0 0,0,0,0 0,0,0,0 @@ -311,12 +305,6 @@ 19,19,19,0 19,37,19,0 0,0,0,32.5 - 12,0,12,0 - 12,0,12,0 - 12,14,0,13 - 0 - 0,6,0,0 - 12,14,0,13 0 0 1 @@ -345,8 +333,6 @@ Normal SemiLight Normal - SemiLight - Bold Normal Normal Normal diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml index 8647e7f0dc..1c65911593 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseDark.xaml @@ -11,8 +11,8 @@ - 2,2,2,2 - 4,4,4,4 + 3 + 5 1,1,1,1 @@ -373,74 +373,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 + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml index 2b960259d0..ad23955776 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentBaseLight.xaml @@ -11,8 +11,8 @@ - 2,2,2,2 - 4,4,4,4 + 3 + 5 1,1,1,1 @@ -376,74 +376,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 + + + + + + + + + + + + + + + + + + + diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml index 5ab50f5b73..653e4733cb 100644 --- a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml +++ b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesDark.xaml @@ -254,9 +254,7 @@ 1 32 0,0 - 12,0,0,0 - 12,4,12,4 @@ -266,31 +264,12 @@ - - - - @@ -300,51 +279,16 @@ 1 - - - - - - - 0.6 4 - 0 + 0 - + @@ -700,7 +644,7 @@ - + @@ -727,75 +671,34 @@ - - - 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 @@ -891,5 +794,11 @@ 32 + + + + + + 1 diff --git a/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml b/src/Avalonia.Themes.Fluent/Accents/FluentControlResourcesLight.xaml index 88040a69e7..6a2a04f732 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 d29db79e7b..9ef92a44d5 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 43867f6e97..8c92040122 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/OpenSans-Bold.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-Bold.ttf new file mode 100644 index 0000000000..efdd5e84a0 Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-Bold.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-BoldItalic.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-BoldItalic.ttf new file mode 100644 index 0000000000..9bf9b4e97b Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-BoldItalic.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-ExtraBold.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-ExtraBold.ttf new file mode 100644 index 0000000000..67fcf0fb2a Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-ExtraBold.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-ExtraBoldItalic.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-ExtraBoldItalic.ttf new file mode 100644 index 0000000000..086722809c Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-ExtraBoldItalic.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-Italic.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-Italic.ttf new file mode 100644 index 0000000000..117856707b Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-Italic.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-Light.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-Light.ttf new file mode 100644 index 0000000000..6580d3a169 Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-Light.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-LightItalic.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-LightItalic.ttf new file mode 100644 index 0000000000..1e0c331981 Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-LightItalic.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-Regular.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-Regular.ttf new file mode 100644 index 0000000000..29bfd35a2b Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-Regular.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-SemiBold.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-SemiBold.ttf new file mode 100644 index 0000000000..54e7059cf3 Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-SemiBold.ttf differ diff --git a/src/Avalonia.Themes.Fluent/Assets/OpenSans-SemiBoldItalic.ttf b/src/Avalonia.Themes.Fluent/Assets/OpenSans-SemiBoldItalic.ttf new file mode 100644 index 0000000000..aebcf14212 Binary files /dev/null and b/src/Avalonia.Themes.Fluent/Assets/OpenSans-SemiBoldItalic.ttf differ diff --git a/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml b/src/Avalonia.Themes.Fluent/AutoCompleteBox.xaml index 44e7962e17..0d5d733cd9 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 84bf799d8d..c2a1359e8a 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 345a74512c..f9a3601edd 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 c26dec3d34..66ccea99b5 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 44783a8dea..a6b6156944 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 47d5988e8c..5a0fa32657 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 08de50c6e3..d1610b343c 100644 --- a/src/Avalonia.Themes.Fluent/NumericUpDown.xaml +++ b/src/Avalonia.Themes.Fluent/NumericUpDown.xaml @@ -25,8 +25,7 @@ - - + diff --git a/src/Avalonia.Themes.Fluent/OverlayPopupHost.xaml b/src/Avalonia.Themes.Fluent/OverlayPopupHost.xaml index 36dbc1b761..3809e11251 100644 --- a/src/Avalonia.Themes.Fluent/OverlayPopupHost.xaml +++ b/src/Avalonia.Themes.Fluent/OverlayPopupHost.xaml @@ -1,5 +1,5 @@ diff --git a/src/Avalonia.Themes.Fluent/RadioButton.xaml b/src/Avalonia.Themes.Fluent/RadioButton.xaml index 23bcfc616a..acde4ea0be 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 12ba38d614..5afaf05e9f 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 d45f705a40..628ab8dddd 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 49fc4b59b0..c1b60d061c 100644 --- a/src/Avalonia.Themes.Fluent/TextBox.xaml +++ b/src/Avalonia.Themes.Fluent/TextBox.xaml @@ -8,8 +8,7 @@ - - + @@ -50,7 +49,7 @@ Margin="{TemplateBinding BorderThickness}"> @@ -103,7 +102,6 @@ diff --git a/src/Avalonia.Themes.Fluent/TimePicker.xaml b/src/Avalonia.Themes.Fluent/TimePicker.xaml index 4a411796d8..f82e194a73 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 a3c300f6f5..f2f07d3e2a 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 88266ac979..893d64f505 100644 --- a/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml +++ b/src/Avalonia.Themes.Fluent/ToggleSwitch.xaml @@ -43,8 +43,7 @@ - - + diff --git a/src/Avalonia.Themes.Fluent/ToolTip.xaml b/src/Avalonia.Themes.Fluent/ToolTip.xaml index 47ad494bbf..f7a1ebbc6b 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 aee15347eb..794f516f71 100644 --- a/src/Avalonia.Themes.Fluent/Window.xaml +++ b/src/Avalonia.Themes.Fluent/Window.xaml @@ -1,7 +1,9 @@ + - + - + - + - + diff --git a/src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs b/src/Avalonia.Visuals/Media/AcrylicBackgroundSource.cs new file mode 100644 index 0000000000..34cede4018 --- /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/ExperimentalAcrylicMaterial.cs b/src/Avalonia.Visuals/Media/ExperimentalAcrylicMaterial.cs new file mode 100644 index 0000000000..6b699acd2e --- /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 f9410afe6a..bc979c15ee 100644 --- a/src/Avalonia.Visuals/Media/FontManager.cs +++ b/src/Avalonia.Visuals/Media/FontManager.cs @@ -100,7 +100,7 @@ namespace Avalonia.Media return typeface; } - typeface = new Typeface(fontFamily, fontWeight, fontStyle); + typeface = new Typeface(fontFamily, fontStyle, fontWeight); if (_typefaceCache.TryAdd(key, typeface)) { @@ -143,7 +143,7 @@ namespace Avalonia.Media } var matchedTypeface = PlatformImpl.TryMatchCharacter(codepoint, fontWeight, fontStyle, fontFamily, culture, out var key) ? - _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Weight, key.Style)) : + _typefaceCache.GetOrAdd(key, new Typeface(key.FamilyName, key.Style, key.Weight)) : null; return matchedTypeface; diff --git a/src/Avalonia.Visuals/Media/GlyphRun.cs b/src/Avalonia.Visuals/Media/GlyphRun.cs index 29c9d93560..a32a3e1b6c 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 @@ namespace Avalonia.Media 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 @@ namespace Avalonia.Media } } - 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 @@ namespace Avalonia.Media /// 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 @@ namespace Avalonia.Media 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 @@ namespace Avalonia.Media } } - 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 @@ namespace Avalonia.Media 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 7756e94598..15b7681be4 100644 --- a/src/Avalonia.Visuals/Media/IBrush.cs +++ b/src/Avalonia.Visuals/Media/IBrush.cs @@ -13,4 +13,4 @@ namespace Avalonia.Media /// 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 0000000000..e71584258a --- /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 0000000000..fcfe4631a6 --- /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 0000000000..f46d76cf3f --- /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 0000000000..c0b445c357 --- /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 a83555946b..681fc5d499 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 @@ namespace Avalonia.Media 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 @@ namespace Avalonia.Media } /// - /// 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 21e2e2484c..2dced2252e 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 dde425bb94..a61983e8d5 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 4903342cea..56790cc0db 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 @@ namespace Avalonia.Media.TextFormatting /// /// 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 0000000000..c4302aecec --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/GenericTextParagraphProperties.cs @@ -0,0 +1,69 @@ +namespace Avalonia.Media.TextFormatting +{ + public class GenericTextParagraphProperties : TextParagraphProperties + { + private TextAlignment _textAlignment; + private TextWrapping _textWrapping; + private TextTrimming _textTrimming; + private double _lineHeight; + + public GenericTextParagraphProperties( + TextRunProperties defaultTextRunProperties, + TextAlignment textAlignment = TextAlignment.Left, + TextWrapping textWrapping = TextWrapping.WrapWithOverflow, + TextTrimming textTrimming = TextTrimming.None, + double lineHeight = 0) + { + DefaultTextRunProperties = defaultTextRunProperties; + + _textAlignment = textAlignment; + + _textWrapping = textWrapping; + + _textTrimming = textTrimming; + + _lineHeight = lineHeight; + } + + public override TextRunProperties DefaultTextRunProperties { get; } + + public override TextAlignment TextAlignment => _textAlignment; + + public override TextWrapping TextWrapping => _textWrapping; + + public override TextTrimming TextTrimming => _textTrimming; + + 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 text trimming + /// + internal void SetTextTrimming(TextTrimming textTrimming) + { + _textTrimming = textTrimming; + } + + /// + /// 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 0000000000..3db3589498 --- /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 0000000000..0c6c722941 --- /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 0000000000..2e7e7aceb1 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/ShapedTextCharacters.cs @@ -0,0 +1,164 @@ +using Avalonia.Media.TextFormatting.Unicode; +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 = 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( + 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 00f9b918cb..0000000000 --- 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 f84e45d4c6..0000000000 --- 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 11d241bc34..0000000000 --- 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 d9b27958ab..b35882fc0e 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.Weight, defaultTypeface.Style, 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/TextFormat.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextFormat.cs deleted file mode 100644 index 18dd6c7c10..0000000000 --- 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 7da39dc5dc..e4c898e2b8 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 @@ namespace Avalonia.Media.TextFormatting return current; } - current = new SimpleTextFormatter(); + current = new TextFormatterImpl(); AvaloniaLocator.CurrentMutable.Bind().ToConstant(current); @@ -38,149 +37,10 @@ namespace Avalonia.Media.TextFormatting /// 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 0000000000..793707d0b2 --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextFormatterImpl.cs @@ -0,0 +1,544 @@ +using System.Collections.Generic; +using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.Platform; +using Avalonia.Utilities; + +namespace Avalonia.Media.TextFormatting +{ + internal class TextFormatterImpl : TextFormatter + { + private static readonly ReadOnlySlice s_ellipsis = new ReadOnlySlice(new[] { '\u2026' }); + + /// + public override TextLine FormatLine(ITextSource textSource, int firstTextSourceIndex, double paragraphWidth, + TextParagraphProperties paragraphProperties, TextLineBreak previousLineBreak = null) + { + var textTrimming = paragraphProperties.TextTrimming; + var textWrapping = paragraphProperties.TextWrapping; + TextLine textLine = null; + + var textRuns = FetchTextRuns(textSource, firstTextSourceIndex, previousLineBreak, out var nextLineBreak); + + var textRange = GetTextRange(textRuns); + + if (textTrimming != TextTrimming.None) + { + textLine = PerformTextTrimming(textRuns, textRange, paragraphWidth, paragraphProperties); + } + else + { + 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; + } + } + } + + return textLine; + } + + /// + /// 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) + { + 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, textRun.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 trimming and returns a trimmed line. + /// + /// The text runs to perform the trimming on. + /// The text range that is covered by the text runs. + /// 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. + /// + private static TextLine PerformTextTrimming(IReadOnlyList textRuns, TextRange textRange, + 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.Properties); + + var measuredLength = MeasureText(currentRun, availableWidth - ellipsisRun.GlyphRun.Bounds.Width); + + if (textTrimming == TextTrimming.WordEllipsis) + { + if (measuredLength < textRange.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, textRange, paragraphWidth, paragraphProperties); + + return new TextLineImpl(trimmedRuns, textLineMetrics); + } + + availableWidth -= currentRun.GlyphRun.Bounds.Width; + + runIndex++; + } + + return new TextLineImpl(textRuns, + TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties)); + } + + /// + /// 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 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) + { + if (paragraphProperties.TextWrapping == TextWrapping.WrapWithOverflow) + { + var lineBreaker = new LineBreakEnumerator(currentRun.Text.Skip(measuredLength)); + + if (lineBreaker.MoveNext()) + { + measuredLength += lineBreaker.Current.PositionWrap; + } + else + { + measuredLength = currentRun.Text.Length; + } + } + else + { + 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, + new TextRange(textRange.Start, length), 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; + + length += currentRun.GlyphRun.Characters.Length; + + runIndex++; + } + + return new TextLineImpl(textRuns, + TextLineMetrics.Create(textRuns, textRange, paragraphWidth, paragraphProperties)); + } + + /// + /// Measures the number of characters that fits into available width. + /// + /// The text run. + /// The available width. + /// + private static int MeasureText(ShapedTextCharacters textCharacters, double availableWidth) + { + var glyphRun = textCharacters.GlyphRun; + + var characterHit = glyphRun.GetCharacterHitFromDistance(availableWidth, out _); + + return characterHit.FirstCharacterIndex + characterHit.TrailingLength - textCharacters.Text.Start; + } + + /// + /// Creates an ellipsis. + /// + /// The text run properties. + /// + private static ShapedTextCharacters CreateEllipsisRun(TextRunProperties properties) + { + var formatterImpl = AvaloniaLocator.Current.GetService(); + + var glyphRun = formatterImpl.ShapeText(s_ellipsis, properties.Typeface, properties.FontRenderingEmSize, + properties.CultureInfo); + + return new ShapedTextCharacters(glyphRun, properties); + } + + /// + /// 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); + } + + /// + /// Split a sequence of runs into two segments at specified length. + /// + /// The text run's. + /// The length to split at. + /// The split text runs. + 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 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); + } + + 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; } + } + + 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 0292398782..2e2e4a8c68 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLayout.cs @@ -1,11 +1,9 @@ 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; +using Avalonia.Platform; namespace Avalonia.Media.TextFormatting { @@ -14,11 +12,11 @@ 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; /// /// Initializes a new instance of the class. @@ -33,6 +31,7 @@ namespace Avalonia.Media.TextFormatting /// 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 +45,22 @@ namespace Avalonia.Media.TextFormatting 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, textTrimming, + textDecorations, lineHeight); _textStyleOverrides = textStyleOverrides; + LineHeight = lineHeight; + MaxWidth = maxWidth; MaxHeight = maxHeight; @@ -67,22 +70,29 @@ namespace Avalonia.Media.TextFormatting 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. @@ -105,7 +115,7 @@ namespace Avalonia.Media.TextFormatting /// /// The drawing context. /// The origin. - public void Draw(IDrawingContextImpl context, Point origin) + public void Draw(DrawingContext context, Point origin) { if (!TextLines.Any()) { @@ -132,14 +142,16 @@ namespace Avalonia.Media.TextFormatting /// 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) + 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, textTrimming, + lineHeight); } /// @@ -170,14 +182,15 @@ namespace Avalonia.Media.TextFormatting /// 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)); } /// @@ -199,77 +212,38 @@ namespace Avalonia.Media.TextFormatting double left = 0.0, right = 0.0, bottom = 0.0; - var lineBreaker = new LineBreakEnumerator(_text); - var currentPosition = 0; + var textSource = new FormattedTextSource(_text, + _paragraphProperties.DefaultTextRunProperties, _textStyleOverrides); + + TextLineBreak previousLineBreak = null; + while (currentPosition < _text.Length && (MaxLines == 0 || textLines.Count < MaxLines)) { - int length; + var textLine = TextFormatter.Current.FormatLine(textSource, currentPosition, MaxWidth, + _paragraphProperties, previousLineBreak); - if (lineBreaker.MoveNext()) - { - if (!lineBreaker.Current.Required) - { - continue; - } + previousLineBreak = textLine.LineBreak; - length = lineBreaker.Current.PositionWrap - currentPosition; + textLines.Add(textLine); - if (currentPosition + length < _text.Length) - { - //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') - { - length++; - } - } - } - else + UpdateBounds(textLine, ref left, ref right, ref bottom); + + if (!double.IsPositiveInfinity(MaxHeight) && bottom > MaxHeight) { - length = _text.Length - currentPosition; + break; } - var remainingLength = length; + currentPosition += textLine.TextRange.Length; - while (remainingLength > 0 && (MaxLines == 0 || textLines.Count < MaxLines)) + if (currentPosition != _text.Length || textLine.LineBreak == null) { - 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; - } - - if (_paragraphProperties.TextTrimming != TextTrimming.None) - { - currentPosition += remainingLength; - - break; - } - - remainingLength -= textLine.Text.Length; - - currentPosition += textLine.Text.Length; + continue; } - } - if (lineBreaker.Current.Required && currentPosition == _text.Length) - { var emptyTextLine = CreateEmptyTextLine(currentPosition); - UpdateBounds(emptyTextLine, ref left, ref right, ref bottom); - textLines.Add(emptyTextLine); } @@ -279,22 +253,27 @@ namespace Avalonia.Media.TextFormatting } } - private struct FormattedTextSource : ITextSource + 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 +281,29 @@ namespace Avalonia.Media.TextFormatting 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 +311,34 @@ namespace Avalonia.Media.TextFormatting 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 +347,12 @@ namespace Avalonia.Media.TextFormatting 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 +363,7 @@ namespace Avalonia.Media.TextFormatting 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 a0f7b44882..c3b7dfc77a 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,12 +31,20 @@ namespace Avalonia.Media.TextFormatting /// 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; } + /// /// 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 diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineBreak.cs new file mode 100644 index 0000000000..c24454cb76 --- /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 0000000000..cf00399b8a --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextLineImpl.cs @@ -0,0 +1,235 @@ +using System.Collections.Generic; + +namespace Avalonia.Media.TextFormatting +{ + internal class TextLineImpl : TextLine + { + private readonly IReadOnlyList _textRuns; + + public TextLineImpl(IReadOnlyList textRuns, TextLineMetrics lineMetrics, + TextLineBreak lineBreak = null) + { + _textRuns = textRuns; + LineMetrics = lineMetrics; + LineBreak = lineBreak; + } + + /// + public override TextRange TextRange => LineMetrics.TextRange; + + /// + public override IReadOnlyList TextRuns => _textRuns; + + /// + public override TextLineMetrics LineMetrics { get; } + + /// + public override TextLineBreak LineBreak { get; } + + /// + public override void Draw(DrawingContext 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(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 new CharacterHit(TextRange.End); // Can't move, we're after the last character + } + + /// + public override CharacterHit GetPreviousCaretCharacterHit(CharacterHit characterHit) + { + if (TryFindPreviousCharacterHit(characterHit, out var previousCharacterHit)) + { + return previousCharacterHit; + } + + return new CharacterHit(TextRange.Start); // 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]; + + nextCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex + characterHit.TrailingLength, out _); + + if (codepointIndex <= nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength) + { + 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]; + + previousCharacterHit = run.GlyphRun.FindNearestCharacterHit(characterHit.FirstCharacterIndex - 1, out _); + + if (previousCharacterHit.FirstCharacterIndex < codepointIndex) + { + 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; + } + } +} diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextLineMetrics.cs index 096305c09c..d47cc0c394 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,20 @@ namespace Avalonia.Media.TextFormatting /// public readonly struct TextLineMetrics { - public TextLineMetrics(double width, double xOrigin, double ascent, double descent, double lineGap) + public TextLineMetrics(Size size, Point baselineOrigin, TextRange textRange) { - Ascent = ascent; - Descent = descent; - LineGap = lineGap; - Size = new Size(width, descent - ascent + lineGap); - BaselineOrigin = new Point(xOrigin, -ascent); + Size = size; + BaselineOrigin = baselineOrigin; + TextRange = textRange; } /// - /// 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. @@ -61,10 +44,12 @@ namespace Avalonia.Media.TextFormatting /// 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 +58,39 @@ namespace Avalonia.Media.TextFormatting 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 xOrigin = TextLine.GetParagraphOffsetX(lineWidth, paragraphWidth, paragraphProperties.TextAlignment); + + var baselineOrigin = new Point(xOrigin, -ascent); + + 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, baselineOrigin, textRange); } } } diff --git a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs index 1368f1777a..39eb695404 100644 --- a/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextParagraphProperties.cs @@ -3,38 +3,37 @@ /// /// 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. /// - public TextTrimming TextTrimming { get; } + public abstract TextTrimming TextTrimming { get; } + + /// + /// Paragraph's line height + /// + 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 65d5c04b4c..1177c758f4 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 @@ namespace Avalonia.Media.TextFormatting /// 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 28b83333b9..c15a771755 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 @@ namespace Avalonia.Media.TextFormatting } } - 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 0000000000..bbcdfe2d8e --- /dev/null +++ b/src/Avalonia.Visuals/Media/TextFormatting/TextRunProperties.cs @@ -0,0 +1,90 @@ +using System; +using System.Globalization; + +namespace Avalonia.Media.TextFormatting +{ + /// + /// Properties that can change from one run to the next, such as typeface or foreground brush. + /// + /// + /// The client provides a concrete implementation of this abstract run properties class. This + /// allows client to implement their run properties the way that fits with their run 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 eb3a4129bc..a02ace408f 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 @@ namespace Avalonia.Media.TextFormatting } /// - 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 cf52c3ca17..0000000000 --- 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 55f8999182..0000000000 --- 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/Unicode/Codepoint.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/Codepoint.cs index 94171b7324..20fe345d93 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 { diff --git a/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs b/src/Avalonia.Visuals/Media/TextFormatting/Unicode/CodepointEnumerator.cs index 2ff4952cab..9e1f748ebb 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 a6791b4a53..f268340eb9 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 fd7831dfe6..1e4ac8fe0f 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 25a32bb1a3..26f7721128 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 { diff --git a/src/Avalonia.Visuals/Media/TextWrapping.cs b/src/Avalonia.Visuals/Media/TextWrapping.cs index 56df3670bd..d649bda23f 100644 --- a/src/Avalonia.Visuals/Media/TextWrapping.cs +++ b/src/Avalonia.Visuals/Media/TextWrapping.cs @@ -5,6 +5,13 @@ namespace Avalonia.Media /// public enum TextWrapping { + /// + /// 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, + /// /// Text should not wrap. /// @@ -15,4 +22,4 @@ namespace Avalonia.Media /// Wrap } -} \ No newline at end of file +} diff --git a/src/Avalonia.Visuals/Media/Typeface.cs b/src/Avalonia.Visuals/Media/Typeface.cs index 7618598a3f..677e930804 100644 --- a/src/Avalonia.Visuals/Media/Typeface.cs +++ b/src/Avalonia.Visuals/Media/Typeface.cs @@ -16,11 +16,11 @@ namespace Avalonia.Media /// 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 @@ namespace Avalonia.Media /// 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 0000000000..a3cf0298dd --- /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/ITextShaperImpl.cs b/src/Avalonia.Visuals/Platform/ITextShaperImpl.cs index 4d770a6c6e..d915da2603 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 @@ namespace Avalonia.Platform /// 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 dfb21a0289..4a364998fd 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 @@ namespace Avalonia.Rendering.SceneGraph } } + /// + 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 0000000000..336d11e3fd --- /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 5059a6d042..ec1a7753b1 100644 --- a/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs +++ b/src/Avalonia.Visuals/Rendering/SceneGraph/RectangleNode.cs @@ -19,7 +19,7 @@ namespace Avalonia.Rendering.SceneGraph /// 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 ff2b3b9363..5feaa88e26 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 @@ namespace Avalonia.Utility 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 0000000000..7a10d865ef --- /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/X11Window.cs b/src/Avalonia.X11/X11Window.cs index 499fe5a67a..2a13999e8d 100644 --- a/src/Avalonia.X11/X11Window.cs +++ b/src/Avalonia.X11/X11Window.cs @@ -312,7 +312,15 @@ namespace Avalonia.X11 { 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; } @@ -1039,6 +1047,18 @@ namespace Avalonia.X11 _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 +1127,9 @@ namespace Avalonia.X11 } 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 2153344363..b8ae2eb4d8 100644 --- a/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs +++ b/src/Linux/Avalonia.LinuxFramebuffer/FramebufferToplevelImpl.cs @@ -82,5 +82,7 @@ namespace Avalonia.LinuxFramebuffer 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/Avalonia.Markup.Xaml.csproj b/src/Markup/Avalonia.Markup.Xaml/Avalonia.Markup.Xaml.csproj index 8876024bd4..35e801e5ce 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 @@ -17,6 +17,14 @@ + + + + + + + + @@ -26,7 +34,7 @@ - + @@ -38,13 +46,21 @@ - + + + + + + + + + @@ -55,14 +71,18 @@ + + + - - + + + 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 0000000000..aab733cb43 --- /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 0000000000..7cbe4942e5 --- /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 0000000000..d627fe3cd3 --- /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 0000000000..5820f47fbb --- /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 0000000000..3e3174123a --- /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 0000000000..b3f78bfbe3 --- /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 0000000000..6e7463776b --- /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 0000000000..164d38e3ae --- /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 cc66efa5fd..10770365a3 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/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs index 5a5da518d0..d9b4cd70da 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/AvaloniaXamlIlRuntimeCompiler.cs @@ -10,12 +10,15 @@ using System.Runtime.InteropServices; 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 @@ namespace Avalonia.Markup.Xaml.XamlIl #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 @@ namespace Avalonia.Markup.Xaml.XamlIl } 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 @@ namespace Avalonia.Markup.Xaml.XamlIl 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 @@ namespace Avalonia.Markup.Xaml.XamlIl 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 @@ namespace Avalonia.Markup.Xaml.XamlIl 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 @@ namespace Avalonia.Markup.Xaml.XamlIl 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 @@ namespace Avalonia.Markup.Xaml.XamlIl 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 @@ namespace Avalonia.Markup.Xaml.XamlIl 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/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs index b84f50fa8d..f3b8559d45 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompiler.cs @@ -1,29 +1,33 @@ 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; +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 + class AvaloniaXamlIlCompiler : XamlILCompiler { - private readonly XamlIlTransformerConfiguration _configuration; - private readonly IXamlIlType _contextType; + private readonly TransformerConfiguration _configuration; + private readonly IXamlType _contextType; private readonly AvaloniaXamlIlDesignPropertiesTransformer _designTransformer; + private readonly AvaloniaBindingExtensionTransformer _bindingTransformer; - private AvaloniaXamlIlCompiler(XamlIlTransformerConfiguration configuration) : base(configuration, true) + private AvaloniaXamlIlCompiler(TransformerConfiguration configuration, XamlLanguageEmitMappings emitMappings) + : base(configuration, emitMappings, true) { _configuration = configuration; - void InsertAfter(params IXamlIlAstTransformer[] t) + void InsertAfter(params IXamlAstTransformer[] t) => Transformers.InsertRange(Transformers.FindIndex(x => x is T) + 1, t); - void InsertBefore(params IXamlIlAstTransformer[] t) + void InsertBefore(params IXamlAstTransformer[] t) => Transformers.InsertRange(Transformers.FindIndex(x => x is T), t); @@ -32,40 +36,54 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions Transformers.Insert(0, new XNameTransformer()); Transformers.Insert(1, new IgnoredDirectivesTransformer()); Transformers.Insert(2, _designTransformer = new AvaloniaXamlIlDesignPropertiesTransformer()); - Transformers.Insert(3, new AvaloniaBindingExtensionHackTransformer()); + Transformers.Insert(3, _bindingTransformer = new AvaloniaBindingExtensionTransformer()); // Targeted - InsertBefore(new AvaloniaXamlIlTransformInstanceAttachedProperties()); - InsertAfter(new AvaloniaXamlIlAvaloniaPropertyResolver()); + InsertBefore( + new AvaloniaXamlIlTransformInstanceAttachedProperties(), + new AvaloniaXamlIlTransformSyntheticCompiledBindingMembers()); + InsertAfter(new AvaloniaXamlIlAvaloniaPropertyResolver()); - InsertBefore( + InsertBefore( + new AvaloniaXamlIlBindingPathParser(), new AvaloniaXamlIlSelectorTransformer(), - new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer(), + new AvaloniaXamlIlPropertyPathTransformer(), + new AvaloniaXamlIlSetterTransformer(), new AvaloniaXamlIlConstructorServiceProviderTransformer(), new AvaloniaXamlIlTransitionsTypeMetadataTransformer() ); - + // After everything else - - Transformers.Add(new AddNameScopeRegistration()); + 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(XamlIlTransformerConfiguration configuration, - IXamlIlTypeBuilder contextTypeBuilder) : this(configuration) + public AvaloniaXamlIlCompiler(TransformerConfiguration configuration, + XamlLanguageEmitMappings emitMappings, + IXamlTypeBuilder contextTypeBuilder) + : this(configuration, emitMappings) { _contextType = CreateContextType(contextTypeBuilder); } - public AvaloniaXamlIlCompiler(XamlIlTransformerConfiguration configuration, - IXamlIlType contextType) : this(configuration) + public AvaloniaXamlIlCompiler(TransformerConfiguration configuration, + XamlLanguageEmitMappings emitMappings, + IXamlType contextType) : this(configuration, emitMappings) { _contextType = contextType; } @@ -79,37 +97,41 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions set => _designTransformer.IsDesignMode = value; } - public void ParseAndCompile(string xaml, string baseUri, IFileSource fileSource, IXamlIlTypeBuilder tb, IXamlIlType overrideRootType) + public bool DefaultCompileBindings { - var parsed = XDocumentXamlIlParser.Parse(xaml, new Dictionary + 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 = (XamlIlAstObjectNode)parsed.Root; + var rootObject = (XamlAstObjectNode)parsed.Root; var classDirective = rootObject.Children - .OfType().FirstOrDefault(x => + .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), + new XamlAstClrTypeReference(classDirective, + _configuration.TypeSystem.GetType(((XamlAstTextNode)classDirective.Values[0]).Text), false) : - XamlIlTypeReferenceResolver.ResolveType(CreateTransformationContext(parsed, true), - (XamlIlAstXmlTypeReference)rootObject.Type, true); + TypeReferenceResolver.ResolveType(CreateTransformationContext(parsed, true), + (XamlAstXmlTypeReference)rootObject.Type, true); if (overrideRootType != null) { - - if (!rootType.Type.IsAssignableFrom(overrideRootType)) - throw new XamlIlLoadException( + throw new XamlX.XamlLoadException( $"Unable to substitute {rootType.Type.GetFqn()} with {overrideRootType.GetFqn()}", rootObject); - rootType = new XamlIlAstClrTypeReference(rootObject, overrideRootType, false); + rootType = new XamlAstClrTypeReference(rootObject, overrideRootType, false); } OverrideRootType(parsed, rootType); @@ -119,17 +141,17 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } - public void OverrideRootType(XamlIlDocument doc, IXamlIlAstTypeReference newType) + public void OverrideRootType(XamlDocument doc, IXamlAstTypeReference newType) { - var root = (XamlIlAstObjectNode)doc.Root; + var root = (XamlAstObjectNode)doc.Root; var oldType = root.Type; if (oldType.Equals(newType)) return; root.Type = newType; - foreach (var child in root.Children.OfType()) + foreach (var child in root.Children.OfType()) { - if (child.Property is XamlIlAstNamePropertyReference prop) + if (child.Property is XamlAstNamePropertyReference prop) { if (prop.DeclaringType.Equals(oldType)) prop.DeclaringType = newType; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlCompilerConfiguration.cs new file mode 100644 index 0000000000..0c0dcb1634 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/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/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs index a1fe6976b7..99ec3744bf 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/AvaloniaXamlIlLanguage.cs @@ -2,10 +2,12 @@ using System.Collections.Generic; 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions }, 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } - 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions _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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions } } - public IXamlIlCustomAttribute GetCustomAttribute(IXamlIlType type, IXamlIlType attributeType) + public IXamlCustomAttribute GetCustomAttribute(IXamlType type, IXamlType attributeType) { if (attributeType.Equals(_typeConverterAttribute)) { @@ -151,25 +157,25 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions { 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/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs index 805b733feb..cb36851bab 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AddNameScopeRegistration.cs @@ -1,139 +1,120 @@ using System; using System.Linq; -using XamlIl.Ast; -using XamlIl.Transform; -using XamlIl.TypeSystem; +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 : IXamlIlAstTransformer + class AddNameScopeRegistration : IXamlAstTransformer { - public IXamlIlAstNode Transform(XamlIlAstTransformationContext context, IXamlIlAstNode node) + public IXamlAstNode Transform(AstTransformationContext context, IXamlAstNode node) { - if (node is XamlIlPropertyAssignmentNode pa - && pa.Property.Name == "Name" - && pa.Property.DeclaringType.FullName == "Avalonia.StyledElement") + if (node is XamlPropertyAssignmentNode pa) { - 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)) + 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)) { - var local = new XamlIlAstCompilerLocalNode(value); - // Wrap original in local initialization - pa.Values[c] = new XamlIlAstLocalInitializationNodeEmitter(value, value, local); - // Use local - value = local; - } + 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; - } + break; + } - if (value != null) - return new XamlIlManipulationGroupNode(pa) + if (value != null) { - Children = + var objectType = context.ParentNodes().OfType().FirstOrDefault()?.Type.GetClrType(); + return new XamlManipulationGroupNode(pa) { - pa, - new AvaloniaNameScopeRegistrationXamlIlNode(value, context.GetAvaloniaTypes()) - } - }; + 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]); + } } - 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 + class NestedScopeMetadataNode : XamlValueWithSideEffectNodeBase + { + public NestedScopeMetadataNode(IXamlAstValueNode value) : base(value, value) { - 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 + class AvaloniaNameScopeRegistrationXamlIlNode : XamlAstNode, IXamlAstManipulationNode { - private readonly AvaloniaXamlIlWellKnownTypes _types; - public IXamlIlAstValueNode Name { get; set; } + public IXamlAstValueNode Name { get; set; } + public IXamlType TargetType { get; } - public AvaloniaNameScopeRegistrationXamlIlNode(IXamlIlAstValueNode name, AvaloniaXamlIlWellKnownTypes types) : base(name) + public AvaloniaNameScopeRegistrationXamlIlNode(IXamlAstValueNode name, IXamlType targetType) : base(name) { - _types = types; + TargetType = targetType; Name = name; } - public override void VisitChildren(IXamlIlAstVisitor visitor) - => Name = (IXamlIlAstValueNode)Name.Visit(visitor); + public override void VisitChildren(IXamlAstVisitor visitor) + => Name = (IXamlAstValueNode)Name.Visit(visitor); + } - public XamlIlNodeEmitResult Emit(XamlIlEmitContext context, IXamlIlEmitter codeGen) + class AvaloniaNameScopeRegistrationXamlIlNodeEmitter : IXamlAstLocalsNodeEmitter + { + public XamlILNodeEmitResult Emit(IXamlAstNode node, XamlEmitContextWithLocals context, IXamlILEmitter codeGen) { - var scopeField = context.RuntimeContext.ContextType.Fields.First(f => - f.Name == AvaloniaXamlIlLanguage.ContextNameScopeFieldName); - - using (var targetLoc = context.GetLocal(context.Configuration.WellKnownTypes.Object)) + if (node is AvaloniaNameScopeRegistrationXamlIlNode registration) { - 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); - } + 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); - return XamlIlNodeEmitResult.Void(1); + 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/XamlIl/CompilerExtensions/Transformers/AvaloniaBindingExtensionHackTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaBindingExtensionHackTransformer.cs deleted file mode 100644 index c89106312f..0000000000 --- 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/AvaloniaBindingExtensionTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaBindingExtensionTransformer.cs new file mode 100644 index 0000000000..de2c0eab96 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/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/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlAvaloniaPropertyResolver.cs index 74f5c29f6a..9219a39ddc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlAvaloniaPropertyResolver.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/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/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathParser.cs new file mode 100644 index 0000000000..7944d8b569 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/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/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlBindingPathTransformer.cs new file mode 100644 index 0000000000..fc32084687 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/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/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlCompiledBindingsMetadataRemover.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlCompiledBindingsMetadataRemover.cs new file mode 100644 index 0000000000..70bd15ea12 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/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/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs index 7e8f296b44..35e2624ff9 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlConstructorServiceProviderTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers 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/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs index 40386924c3..f95d086bf6 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlControlTemplateTargetTypeMetadataTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers .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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } } - 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers 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/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs new file mode 100644 index 0000000000..5a0d6bac8d --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDataContextTypeTransformer.cs @@ -0,0 +1,192 @@ +using System; +using System.Collections.Generic; +using System.Diagnostics; +using System.Linq; +using System.Text; +using Avalonia.Markup.Parsers; +using Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers; +using Avalonia.Utilities; +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) + { + var elementType = itemsCollectionType + .GetAllInterfaces() + .FirstOrDefault(i => + i.GenericTypeDefinition?.Equals(context.Configuration.WellKnownTypes.IEnumerableT) == true) + .GenericArguments[0]; + return new AvaloniaXamlIlDataContextTypeMetadataNode(on, elementType); + } + // 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); + } + } + + [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/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDesignPropertiesTransformer.cs index 9cc4c5cf11..3096be522f 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlDesignPropertiesTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers }; 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/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlRootObjectScopeTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlRootObjectScopeTransformer.cs new file mode 100644 index 0000000000..0513c78a7a --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/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/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs index d5114244cf..b81d25d613 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSelectorTransformer.cs @@ -3,41 +3,45 @@ using System.Collections.Generic; 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers - 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } - 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers _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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers _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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } } - 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/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs index 629e2562d3..e816265422 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlSetterTransformer.cs @@ -1,64 +1,79 @@ using System; using System.Collections.Generic; using System.Linq; -using XamlIl; -using XamlIl.Ast; -using XamlIl.Transform; -using XamlIl.Transform.Transformers; -using XamlIl.TypeSystem; +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 { - class AvaloniaXamlIlSetterTransformer : IXamlIlAstTransformer + using XamlParseException = XamlX.XamlParseException; + using XamlLoadException = XamlX.XamlLoadException; + class AvaloniaXamlIlSetterTransformer : 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.Styling.Setter")) return node; - var parent = context.ParentNodes().OfType() + var parent = context.ParentNodes().OfType() .FirstOrDefault(p => p.Type.GetClrType().FullName == "Avalonia.Styling.Style"); if (parent == null) - throw new XamlIlParseException( + throw new XamlParseException( "Avalonia.Styling.Setter is only valid inside Avalonia.Styling.Style", node); - var selectorProperty = parent.Children.OfType() + var selectorProperty = parent.Children.OfType() .FirstOrDefault(p => p.Property.GetClrProperty().Name == "Selector"); if (selectorProperty == null) - throw new XamlIlParseException( + throw new XamlParseException( "Can not find parent Style Selector", node); var selector = selectorProperty.Values.FirstOrDefault() as XamlIlSelectorNode; if (selector?.TargetType == null) - throw new XamlIlParseException( + throw new XamlParseException( "Can not resolve parent Style Selector type", node); - - var property = @on.Children.OfType() + IXamlType propType = null; + 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); + if (property != null) + { - var propertyName = property.Values.OfType().FirstOrDefault()?.Text; - if (propertyName == null) - throw new XamlIlParseException("Setter.Property must be a string", node); + 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 XamlIlAstClrTypeReference(selector, selector.TargetType, false), property.Values[0]); - property.Values = new List + var avaloniaPropertyNode = XamlIlAvaloniaPropertyHelper.CreateNode(context, propertyName, + new XamlAstClrTypeReference(selector, selector.TargetType, false), property.Values[0]); + property.Values = new List {avaloniaPropertyNode}; + propType = avaloniaPropertyNode.AvaloniaPropertyType; + } + else { - avaloniaPropertyNode - }; + 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 XamlIlAstTextNode) + .OfType().FirstOrDefault(p => p.Property.GetClrProperty().Name == "Value"); + if (valueProperty?.Values?.Count == 1 && valueProperty.Values[0] is XamlAstTextNode) { - var propType = avaloniaPropertyNode.AvaloniaPropertyType; - if (!XamlIlTransformHelpers.TryGetCorrectlyTypedValue(context, valueProperty.Values[0], + if (!XamlTransformHelpers.TryGetCorrectlyTypedValue(context, valueProperty.Values[0], propType, out var converted)) - throw new XamlIlParseException( + throw new XamlParseException( $"Unable to convert property value to {propType.GetFqn()}", valueProperty.Values[0]); @@ -69,9 +84,9 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers return node; } - class SetterValueProperty : XamlIlAstClrProperty + class SetterValueProperty : XamlAstClrProperty { - public SetterValueProperty(IXamlIlLineInfo line, IXamlIlType setterType, IXamlIlType targetType, + public SetterValueProperty(IXamlLineInfo line, IXamlType setterType, IXamlType targetType, AvaloniaXamlIlWellKnownTypes types) : base(line, "Value", setterType, null) { @@ -82,21 +97,21 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers Setters.Add(new XamlIlDirectCallPropertySetter(method, targetType)); } - class XamlIlDirectCallPropertySetter : IXamlIlPropertySetter + class XamlIlDirectCallPropertySetter : IXamlPropertySetter, IXamlEmitablePropertySetter { - private readonly IXamlIlMethod _method; - private readonly IXamlIlType _type; - public IXamlIlType TargetType { get; } + 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) + 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) + public XamlIlDirectCallPropertySetter(IXamlMethod method, IXamlType type) { _method = method; _type = type; diff --git a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs index 548f0161d6..8e194e9385 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformInstanceAttachedProperties.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers && 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers } } - class GetterMethod : IXamlIlCustomEmitMethod + class GetterMethod : IXamlCustomEmitMethod { public GetterMethod(AvaloniaAttachedInstanceProperty parent) { @@ -163,16 +165,16 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers 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 void EmitCall(IXamlILEmitter emitter) { var method = Parent._avaloniaObject .FindMethod(m => m.IsPublic && !m.IsStatic && m.Name == "GetValue" @@ -180,7 +182,7 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions.Transformers 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/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransformSyntheticCompiledBindingMembers.cs new file mode 100644 index 0000000000..154c6a235c --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/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/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransitionsTypeMetadataTransformer.cs index c4136f4824..4be2fc6f60 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlTransitionsTypeMetadataTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/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/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs index 1efae902c6..bf1fb4a6fc 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/AvaloniaXamlIlWellKnownTypes.cs @@ -1,80 +1,119 @@ -using XamlIl.Transform; -using XamlIl.TypeSystem; +using XamlX.Emit; +using XamlX.IL; +using XamlX.Transform; +using XamlX.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) + 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 DataTemplate { get; } + public IXamlType IItemsPresenterHost { get; } + public IXamlType ReflectionBindingExtension { get; } + + public IXamlType RelativeSource { get; } + + public AvaloniaXamlIlWellKnownTypes(TransformerConfiguration cfg) { - 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"); + 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, 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"); + 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 + IsStatic = false, + DeclaringOnly = true, + IsExactMatch = true }); INameScopeComplete = INameScope.GetMethod( new FindMethodMethodSignature("Complete", XamlIlTypes.Void) { - IsStatic = false, DeclaringOnly = true, IsExactMatch = true + IsStatic = false, + DeclaringOnly = true, + IsExactMatch = true }); - NameScope = ctx.Configuration.TypeSystem.GetType("Avalonia.Controls.NameScope"); + NameScope = cfg.TypeSystem.GetType("Avalonia.Controls.NameScope"); NameScopeSetNameScope = NameScope.GetMethod(new FindMethodMethodSignature("SetNameScope", - XamlIlTypes.Void, StyledElement, INameScope) {IsStatic = true}); - + 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"); + 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 XamlIlAstTransformationContext ctx) + 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)); + 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/XamlIl/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs index a79a4977ef..1f2508715e 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/IgnoredDirectivesTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/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/XamlIl/CompilerExtensions/Transformers/XNameTransformer.cs index dd64ab542a..f4eb7cf5f9 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/Transformers/XNameTransformer.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/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/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs index 6fc83cb5a5..7f1b8caf0d 100644 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlAvaloniaPropertyHelper.cs @@ -5,17 +5,36 @@ using System.Xml; 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions 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 @@ namespace Avalonia.Markup.Xaml.XamlIl.CompilerExtensions 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/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs new file mode 100644 index 0000000000..1f527e9569 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlBindingPathHelper.cs @@ -0,0 +1,660 @@ +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 = targetType.GetAllProperties().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; + for (var currentType = targetType; currentType != null; currentType = currentType.BaseType) + { + var defaultMemberAttribute = currentType.CustomAttributes.FirstOrDefault(x => x.Type.Namespace == "System.Reflection" && x.Type.Name == "DefaultMemberAttribute"); + if (defaultMemberAttribute != null) + { + property = targetType.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(); + } + } + + 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/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlClrPropertyInfoHelper.cs new file mode 100644 index 0000000000..871a2a2045 --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/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/XamlIl/CompilerExtensions/XamlIlPropertyInfoAccessorFactoryEmitter.cs b/src/Markup/Avalonia.Markup.Xaml/XamlIl/CompilerExtensions/XamlIlPropertyInfoAccessorFactoryEmitter.cs new file mode 100644 index 0000000000..d24a43a84f --- /dev/null +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/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/XamlIl/xamlil.github b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github index 0681622454..0028377ce7 160000 --- a/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github +++ b/src/Markup/Avalonia.Markup.Xaml/XamlIl/xamlil.github @@ -1 +1 @@ -Subproject commit 068162245473ec39ee36da12150e928072b96403 +Subproject commit 0028377ce7c7dc21f9fe71b45f62a95991b1ab58 diff --git a/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj b/src/Markup/Avalonia.Markup/Avalonia.Markup.csproj index 673eb51901..aca39dd8b0 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 f1a62f9bdc..bf43730481 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 @@ namespace Avalonia.Data /// 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 @@ namespace Avalonia.Data 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 @@ namespace Avalonia.Data } 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 @@ namespace Avalonia.Data 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 @@ namespace Avalonia.Data { 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 0000000000..7c4e7b5efe --- /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 IStyledElement)) + { + target = anchor as IStyledElement; + + 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 c3651594e9..e858bacb74 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ArgumentListParser.cs @@ -4,7 +4,10 @@ using Avalonia.Utilities; 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 0000000000..8de64e56ff --- /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 ae9f338806..1048148c1f 100644 --- a/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs +++ b/src/Markup/Avalonia.Markup/Markup/Parsers/ExpressionParser.cs @@ -8,12 +8,6 @@ using Avalonia.Controls; namespace Avalonia.Markup.Parsers { - internal enum SourceMode - { - Data, - Control - } - internal class ExpressionParser { private readonly bool _enableValidation; @@ -29,332 +23,85 @@ namespace Avalonia.Markup.Parsers 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 7eec80fc00..97198145a8 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 @@ using Avalonia.LogicalTree; 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 321a85c1d7..f304d1e9a2 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 @@ using Avalonia.LogicalTree; 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 2cb87efa65..1cd233c68a 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 0000000000..c5953b514c --- /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/Skia/Avalonia.Skia/Assets/NoiseAsset_256X256_PNG.png b/src/Skia/Avalonia.Skia/Assets/NoiseAsset_256X256_PNG.png new file mode 100644 index 0000000000..41de173d90 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 68da513528..d5ad70b944 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 ae756f4eab..a510763f64 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 @@ namespace Avalonia.Skia 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 @@ namespace Avalonia.Skia 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) @@ -659,6 +695,86 @@ namespace Avalonia.Skia } } + 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 @@ namespace Avalonia.Skia }; return new SurfaceRenderTarget(createInfo); - } + } /// /// Skia cached paint state. diff --git a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs index 5f876464e2..ade659f5eb 100644 --- a/src/Skia/Avalonia.Skia/FormattedTextImpl.cs +++ b/src/Skia/Avalonia.Skia/FormattedTextImpl.cs @@ -569,7 +569,7 @@ namespace Avalonia.Skia float constraint = -1; - if (_wrapping == TextWrapping.Wrap) + if (_wrapping != TextWrapping.NoWrap) { constraint = widthConstraint <= 0 ? MAX_LINE_WIDTH : widthConstraint; if (constraint > MAX_LINE_WIDTH) diff --git a/src/Skia/Avalonia.Skia/TextShaperImpl.cs b/src/Skia/Avalonia.Skia/TextShaperImpl.cs index 7a0823a223..786af7726c 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,7 +11,7 @@ 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()) { @@ -61,9 +61,11 @@ namespace Avalonia.Skia 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 +73,7 @@ namespace Avalonia.Skia font.GetScale(out var scaleX, out _); - var textScale = textFormat.FontRenderingEmSize / scaleX; + var textScale = fontRenderingEmSize / scaleX; var bufferLength = buffer.Length; @@ -101,7 +103,7 @@ namespace Avalonia.Skia 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), diff --git a/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs b/src/Windows/Avalonia.Direct2D1/Media/TextShaperImpl.cs index 2d2865e2b9..254b5684a4 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 @@ namespace Avalonia.Direct2D1.Media 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 @@ namespace Avalonia.Direct2D1.Media 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 f8366abb81..f5d83611bb 100644 --- a/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs +++ b/src/Windows/Avalonia.Win32.Interop/Wpf/WpfTopLevelImpl.cs @@ -256,5 +256,7 @@ namespace Avalonia.Win32.Interop.Wpf 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/Interop/UnmanagedMethods.cs b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs index b3b38db1ab..1aec4f0016 100644 --- a/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs +++ b/src/Windows/Avalonia.Win32/Interop/UnmanagedMethods.cs @@ -989,6 +989,12 @@ namespace Avalonia.Win32.Interop } } + [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); @@ -1317,6 +1323,9 @@ namespace Avalonia.Win32.Interop [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); diff --git a/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs new file mode 100644 index 0000000000..0ba1d311bc --- /dev/null +++ b/src/Windows/Avalonia.Win32/WindowImpl.AppWndProc.cs @@ -0,0 +1,539 @@ +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 = 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); + + 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); + + 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; + } + + 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 0000000000..2badf99f7f --- /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 * Scaling); + } + + // 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 391abdfc73..07f2311be8 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 36398eb810..6e9cb81b11 100644 --- a/src/Windows/Avalonia.Win32/WindowImpl.cs +++ b/src/Windows/Avalonia.Win32/WindowImpl.cs @@ -42,6 +42,10 @@ namespace Avalonia.Win32 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 @@ namespace Avalonia.Win32 private Size _minSize; private Size _maxSize; private POINT _maxTrackSize; - private WindowImpl _parent; + private WindowImpl _parent; + private ExtendClientAreaChromeHints _extendChromeHints = ExtendClientAreaChromeHints.Default; public WindowImpl() { @@ -183,6 +188,11 @@ namespace Avalonia.Win32 { get { + if(_isFullScreenActive) + { + return WindowState.FullScreen; + } + var placement = default(WINDOWPLACEMENT); GetWindowPlacement(_hwnd, ref placement); @@ -267,7 +277,7 @@ namespace Avalonia.Win32 } accent.AccentFlags = 2; - accent.GradientColor = 0x00FFFFFF; + accent.GradientColor = 0x01000000; var accentPtr = Marshal.AllocHGlobal(accentStructSize); Marshal.StructureToPtr(accent, accentPtr, false); @@ -668,6 +678,98 @@ namespace Avalonia.Win32 } 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 * Scaling); + } + + 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) / Scaling, 0, 0); + _offScreenMargin = new Thickness(borderThickness.left / Scaling, borderThickness.top / Scaling, borderThickness.right / Scaling, borderThickness.bottom / Scaling); + } + else + { + _extendedMargins = new Thickness(0, (borderCaptionThickness.top) / Scaling, 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 +920,10 @@ namespace Avalonia.Win32 // 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 +944,7 @@ namespace Avalonia.Win32 if ((oldProperties.Decorations != newProperties.Decorations) || forceChanges) { - var style = GetStyle(); + style = GetStyle(); const WindowStyles fullDecorationFlags = WindowStyles.WS_CAPTION | WindowStyles.WS_SYSMENU; @@ -886,7 +989,26 @@ namespace Avalonia.Win32 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 @@ -912,6 +1034,46 @@ namespace Avalonia.Win32 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/tests/Avalonia.Base.UnitTests/Data/Core/PropertyPathGrammarTests.cs b/tests/Avalonia.Base.UnitTests/Data/Core/PropertyPathGrammarTests.cs new file mode 100644 index 0000000000..99144b7381 --- /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.Direct2D1.UnitTests/Media/FontManagerImplTests.cs b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs index 572749a58a..c6ecc0a7e5 100644 --- a/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs +++ b/tests/Avalonia.Direct2D1.UnitTests/Media/FontManagerImplTests.cs @@ -41,7 +41,7 @@ namespace Avalonia.Direct2D1.UnitTests.Media 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 @@ namespace Avalonia.Direct2D1.UnitTests.Media 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.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs new file mode 100644 index 0000000000..37aa123cfc --- /dev/null +++ b/tests/Avalonia.Markup.Xaml.UnitTests/MarkupExtensions/CompiledBindingExtensionTests.cs @@ -0,0 +1,621 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Reactive.Subjects; +using System.Text; +using System.Threading.Tasks; +using Avalonia.Controls; +using Avalonia.Controls.Presenters; +using Avalonia.Data.Core; +using Avalonia.Markup.Data; +using Avalonia.UnitTests; +using XamlX; +using Xunit; + +namespace Avalonia.Markup.Xaml.UnitTests.MarkupExtensions +{ + public class CompiledBindingExtensionTests + { + [Fact] + public void ResolvesClrPropertyBasedOnDataContextType() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + + [Fact] + public void ResolvesPathPassedByProperty() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + + [Fact] + public void ResolvesStreamTaskBindingCorrectly() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + TaskProperty = Task.FromResult("foobar") + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.TaskProperty.Result, textBlock.Text); + } + } + + [Fact] + public void ResolvesStreamObservableBindingCorrectly() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + DelayedBinding.ApplyBindings(textBlock); + + var subject = new Subject(); + var dataContext = new TestDataContext + { + ObservableProperty = subject + }; + + window.DataContext = dataContext; + + subject.OnNext("foobar"); + + Assert.Equal("foobar", textBlock.Text); + } + } + + [Fact] + public void ResolvesIndexerBindingCorrectly() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + ListProperty = { "A", "B", "C", "D", "E" } + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.ListProperty[3], textBlock.Text); + } + } + + [Fact] + public void ResolvesArrayIndexerBindingCorrectly() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + ArrayProperty = new[] { "A", "B", "C", "D", "E" } + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.ArrayProperty[3], textBlock.Text); + } + } + + [Fact] + public void ResolvesObservableIndexerBindingCorrectly() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + ObservableCollectionProperty = { "A", "B", "C", "D", "E" } + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.ObservableCollectionProperty[3], textBlock.Text); + + dataContext.ObservableCollectionProperty[3] = "New Value"; + + Assert.Equal(dataContext.ObservableCollectionProperty[3], textBlock.Text); + } + } + + [Fact] + public void InfersCompiledBindingDataContextFromDataContextBinding() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + + var dataContext = new TestDataContext + { + StringProperty = "A" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + + [Fact] + public void ResolvesNonIntegerIndexerBindingCorrectly() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext(); + + dataContext.NonIntegerIndexerProperty["Test"] = "Initial Value"; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.NonIntegerIndexerProperty["Test"], textBlock.Text); + + dataContext.NonIntegerIndexerProperty["Test"] = "New Value"; + + Assert.Equal(dataContext.NonIntegerIndexerProperty["Test"], textBlock.Text); + } + } + + [Fact] + public void InfersDataTemplateTypeFromDataTypeProperty() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var target = window.FindControl("target"); + + var dataContext = new TestDataContext(); + + dataContext.StringProperty = "Initial Value"; + + window.DataContext = dataContext; + + window.ApplyTemplate(); + target.ApplyTemplate(); + ((ContentPresenter)target.Presenter).UpdateChild(); + + Assert.Equal(dataContext.StringProperty, ((TextBlock)target.Presenter.Child).Text); + } + } + + + [Fact] + public void ThrowsOnUninferrableLooseDataTemplateNoDataTypeWithCompiledBindingPath() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + Assert.Throws(() => loader.Load(xaml)); + } + } + + [Fact] + public void ThrowsOnUninferrableDataTypeFromNonCompiledDataContextBindingWithCompiledBindingPath() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + +"; + var loader = new AvaloniaXamlLoader(); + Assert.Throws(() => loader.Load(xaml)); + } + } + + [Fact] + public void InfersDataTemplateTypeFromParentCollectionItemsType() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var target = window.FindControl("target"); + + var dataContext = new TestDataContext(); + + dataContext.ListProperty.Add("Test"); + + window.DataContext = dataContext; + + window.ApplyTemplate(); + target.ApplyTemplate(); + target.Presenter.ApplyTemplate(); + + Assert.Equal(dataContext.ListProperty[0], (string)((ContentPresenter)target.Presenter.Panel.Children[0]).Content); + } + } + + [Fact] + public void ThrowsOnUninferrableDataTemplateInItemsControlWithoutItemsBinding() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + + + + +"; + var loader = new AvaloniaXamlLoader(); + Assert.Throws(() => loader.Load(xaml)); + } + } + + [Fact] + public void ResolvesElementNameBinding() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("text2"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + + [Fact] + public void ResolvesElementNameBindingFromLongForm() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + + + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("text2"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + + [Fact] + public void ResolvesRelativeSourceBindingLongForm() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var target = window.FindControl("text"); + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + target.ApplyTemplate(); + + Assert.Equal("test", target.Text); + } + } + + [Fact] + public void ResolvesSourceBindingLongForm() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var target = window.FindControl("text"); + + window.ApplyTemplate(); + window.Presenter.ApplyTemplate(); + target.ApplyTemplate(); + + Assert.Equal("Test".Length.ToString(), target.Text); + } + } + + [Fact] + public void CompilesBindingWhenRequested() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + var window = (Window)loader.Load(xaml); + var textBlock = window.FindControl("textBlock"); + + var dataContext = new TestDataContext + { + StringProperty = "foobar" + }; + + window.DataContext = dataContext; + + Assert.Equal(dataContext.StringProperty, textBlock.Text); + } + } + + [Fact] + public void ThrowsOnInvalidBindingPathOnCompiledBindingEnabledViaDirective() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + + +"; + var loader = new AvaloniaXamlLoader(); + Assert.Throws(() => loader.Load(xaml)); + } + } + + [Fact] + public void ThrowsOnInvalidCompileBindingsDirective() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var xaml = @" + +"; + var loader = new AvaloniaXamlLoader(); + Assert.Throws(() => loader.Load(xaml)); + } + } + } + + public class TestDataContext + { + public string StringProperty { get; set; } + + public Task TaskProperty { get; set; } + + public IObservable ObservableProperty { get; set; } + + public ObservableCollection ObservableCollectionProperty { get; set; } = new ObservableCollection(); + + public string[] ArrayProperty { get; set; } + + public List ListProperty { get; set; } = new List(); + + public NonIntegerIndexer NonIntegerIndexerProperty { get; set; } = new NonIntegerIndexer(); + + public class NonIntegerIndexer : NotifyingBase + { + private readonly Dictionary _storage = new Dictionary(); + + public string this[string key] + { + get + { + return _storage[key]; + } + set + { + _storage[key] = value; + RaisePropertyChanged(CommonPropertyNames.IndexerName); + } + } + } + } +} diff --git a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs index a408069cb0..ddeb558754 100644 --- a/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs +++ b/tests/Avalonia.Markup.Xaml.UnitTests/Xaml/XamlIlTests.cs @@ -6,7 +6,11 @@ using System.Linq; using System.Runtime.CompilerServices; using Avalonia.Controls; using Avalonia.Data.Converters; +using Avalonia.Data.Core; +using Avalonia.Input; +using Avalonia.Interactivity; using Avalonia.Media; +using Avalonia.Styling; using Avalonia.Threading; using Avalonia.UnitTests; using Avalonia.VisualTree; @@ -234,6 +238,30 @@ namespace Avalonia.Markup.Xaml.UnitTests } } + [Fact] + public void Provide_Value_Target_Should_Provide_Clr_Property_Info() + { + var parsed = AvaloniaXamlLoader.Parse(@" +", typeof(XamlIlClassWithClrPropertyWithValue).Assembly); + Assert.Equal(6, parsed.Count); + } + + [Fact] + public void DataContextType_Resolution() + { + using (UnitTestApplication.Start(TestServices.StyledWindow)) + { + var parsed = AvaloniaXamlLoader.Parse(@" +"); + } + } + [Fact] public void DataTemplates_Should_Resolve_Named_Controls_From_Parent_Scope() { @@ -345,4 +373,22 @@ namespace Avalonia.Markup.Xaml.UnitTests return (int)control.GetValue(TestIntProperty); } } + + public class XamlIlCheckClrPropertyInfoExtension + { + public string ExpectedPropertyName { get; set; } + + public object ProvideValue(IServiceProvider prov) + { + var pvt = prov.GetService(); + var info = (ClrPropertyInfo)pvt.TargetProperty; + var v = (int)info.Get(pvt.TargetObject); + return v + 1; + } + } + + public class XamlIlClassWithClrPropertyWithValue + { + public int Count { get; set; }= 5; + } } diff --git a/tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs similarity index 98% rename from tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs rename to tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs index 8d64190ebd..feed1179ef 100644 --- a/tests/Avalonia.Skia.UnitTests/CustomFontManagerImpl.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/CustomFontManagerImpl.cs @@ -6,7 +6,7 @@ using Avalonia.Media.Fonts; using Avalonia.Platform; using SkiaSharp; -namespace Avalonia.Skia.UnitTests +namespace Avalonia.Skia.UnitTests.Media { public class CustomFontManagerImpl : IFontManagerImpl { diff --git a/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs similarity index 95% rename from tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs rename to tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs index 8f80d89ac6..df286d709e 100644 --- a/tests/Avalonia.Skia.UnitTests/FontManagerImplTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/FontManagerImplTests.cs @@ -1,13 +1,11 @@ using System; using System.Linq; -using System.Reflection; using Avalonia.Media; -using Avalonia.Platform; using Avalonia.UnitTests; using SkiaSharp; using Xunit; -namespace Avalonia.Skia.UnitTests +namespace Avalonia.Skia.UnitTests.Media { public class FontManagerImplTests { @@ -39,7 +37,7 @@ namespace Avalonia.Skia.UnitTests string fontName = fontManager.GetInstalledFontFamilyNames().First(); var glyphTypeface = (GlyphTypefaceImpl)fontManager.CreateGlyphTypeface( - new Typeface(new FontFamily($"A, B, {fontName}"), FontWeight.Bold)); + new Typeface(new FontFamily($"A, B, {fontName}"), weight: FontWeight.Bold)); var skTypeface = glyphTypeface.Typeface; @@ -88,7 +86,7 @@ namespace Avalonia.Skia.UnitTests 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 skTypeface = glyphTypeface.Typeface; diff --git a/tests/Avalonia.Skia.UnitTests/SKTypefaceCollectionCacheTests.cs b/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs similarity index 89% rename from tests/Avalonia.Skia.UnitTests/SKTypefaceCollectionCacheTests.cs rename to tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs index 726052351b..f9f924e782 100644 --- a/tests/Avalonia.Skia.UnitTests/SKTypefaceCollectionCacheTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/SKTypefaceCollectionCacheTests.cs @@ -2,7 +2,7 @@ using Avalonia.UnitTests; using Xunit; -namespace Avalonia.Skia.UnitTests +namespace Avalonia.Skia.UnitTests.Media { public class SKTypefaceCollectionCacheTests { @@ -19,7 +19,7 @@ namespace Avalonia.Skia.UnitTests var notoMonoCollection = SKTypefaceCollectionCache.GetOrAddTypefaceCollection(notoMono); - var typeface = new Typeface("ABC", FontWeight.Bold, FontStyle.Italic); + var typeface = new Typeface("ABC", FontStyle.Italic, FontWeight.Bold); Assert.Equal("Noto Mono", notoMonoCollection.Get(typeface).FamilyName); diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs new file mode 100644 index 0000000000..6a5065939e --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/FormattableTextSource.cs @@ -0,0 +1,38 @@ +using System; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; + +namespace Avalonia.Skia.UnitTests.Media.TextFormatting +{ + internal class FormattableTextSource : ITextSource + { + private readonly ReadOnlySlice _text; + private readonly TextRunProperties _defaultStyle; + private ReadOnlySlice> _styleSpans; + + public FormattableTextSource(string text, TextRunProperties defaultStyle, + ReadOnlySlice> styleSpans) + { + _text = text.AsMemory(); + + _defaultStyle = defaultStyle; + + _styleSpans = styleSpans; + } + + public TextRun GetTextRun(int textSourceIndex) + { + if (_styleSpans.IsEmpty) + { + return new TextEndOfParagraph(); + } + + var currentSpan = _styleSpans[0]; + + _styleSpans = _styleSpans.Skip(1); + + return new TextCharacters(_text.AsSlice(currentSpan.Start, currentSpan.Length), + _defaultStyle); + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs new file mode 100644 index 0000000000..40aa862906 --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/MultiBufferTextSource.cs @@ -0,0 +1,36 @@ +using System; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; + +namespace Avalonia.Skia.UnitTests.Media.TextFormatting +{ + internal class MultiBufferTextSource : ITextSource + { + private readonly string[] _runTexts; + private readonly GenericTextRunProperties _defaultStyle; + + public MultiBufferTextSource(GenericTextRunProperties defaultStyle) + { + _defaultStyle = defaultStyle; + + _runTexts = new[] { "A123456789", "B123456789", "C123456789", "D123456789", "E123456789" }; + } + + public static TextRange TextRange => new TextRange(0, 50); + + public TextRun GetTextRun(int textSourceIndex) + { + if (textSourceIndex == 50) + { + return new TextEndOfParagraph(); + } + + var index = textSourceIndex / 10; + + var runText = _runTexts[index]; + + return new TextCharacters( + new ReadOnlySlice(runText.AsMemory(), textSourceIndex, runText.Length), _defaultStyle); + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs new file mode 100644 index 0000000000..045deacd0b --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/SingleBufferTextSource.cs @@ -0,0 +1,30 @@ +using System; +using Avalonia.Media.TextFormatting; +using Avalonia.Utilities; + +namespace Avalonia.Skia.UnitTests.Media.TextFormatting +{ + internal class SingleBufferTextSource : ITextSource + { + private readonly ReadOnlySlice _text; + private readonly GenericTextRunProperties _defaultGenericPropertiesRunProperties; + + public SingleBufferTextSource(string text, GenericTextRunProperties defaultProperties) + { + _text = text.AsMemory(); + _defaultGenericPropertiesRunProperties = defaultProperties; + } + + public TextRun GetTextRun(int textSourceIndex) + { + var runText = _text.Skip(textSourceIndex); + + if (runText.IsEmpty) + { + return new TextEndOfParagraph(); + } + + return new TextCharacters(runText, _defaultGenericPropertiesRunProperties); + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs new file mode 100644 index 0000000000..697cc4fec7 --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextFormatterTests.cs @@ -0,0 +1,275 @@ +using System; +using System.Collections.Generic; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.Media.TextFormatting.Unicode; +using Avalonia.UnitTests; +using Avalonia.Utilities; +using Xunit; + +namespace Avalonia.Skia.UnitTests.Media.TextFormatting +{ + public class TextFormatterTests + { + [Fact] + public void Should_Format_TextRuns_With_Default_Style() + { + using (Start()) + { + const string text = "0123456789"; + + var defaultProperties = + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.Single(textLine.TextRuns); + + var textRun = textLine.TextRuns[0]; + + Assert.Equal(defaultProperties.Typeface, textRun.Properties.Typeface); + + Assert.Equal(defaultProperties.ForegroundBrush, textRun.Properties.ForegroundBrush); + + Assert.Equal(text.Length, textRun.Text.Length); + } + } + + [Fact] + public void Should_Format_TextRuns_With_Multiple_Buffers() + { + using (Start()) + { + var defaultProperties = + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black); + + var textSource = new MultiBufferTextSource(defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.Equal(5, textLine.TextRuns.Count); + + Assert.Equal(50, textLine.TextRange.Length); + } + } + + [Fact] + public void Should_Format_TextRuns_With_TextRunStyles() + { + using (Start()) + { + const string text = "0123456789"; + + var defaultProperties = + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: Brushes.Black); + + var GenericTextRunPropertiesRuns = new[] + { + new ValueSpan(0, 3, defaultProperties), + new ValueSpan(3, 3, + new GenericTextRunProperties(Typeface.Default, 13, foregroundBrush: Brushes.Black)), + new ValueSpan(6, 3, + new GenericTextRunProperties(Typeface.Default, 14, foregroundBrush: Brushes.Black)), + new ValueSpan(9, 1, defaultProperties) + }; + + var textSource = new FormattableTextSource(text, defaultProperties, GenericTextRunPropertiesRuns); + + var formatter = new TextFormatterImpl(); + + var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.Equal(text.Length, textLine.TextRange.Length); + + for (var i = 0; i < GenericTextRunPropertiesRuns.Length; i++) + { + var GenericTextRunPropertiesRun = GenericTextRunPropertiesRuns[i]; + + var textRun = textLine.TextRuns[i]; + + Assert.Equal(GenericTextRunPropertiesRun.Length, textRun.Text.Length); + } + } + } + + [Theory] + [InlineData("0123", 1)] + [InlineData("\r\n", 1)] + [InlineData("👍b", 2)] + [InlineData("a👍b", 3)] + [InlineData("a👍子b", 4)] + public void Should_Produce_Unique_Runs(string text, int numberOfRuns) + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.Equal(numberOfRuns, textLine.TextRuns.Count); + } + } + + [Fact] + public void Should_Split_Run_On_Script() + { + using (Start()) + { + const string text = "1234الدولي"; + + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + Assert.Equal(4, textLine.TextRuns[0].Text.Length); + } + } + + [InlineData("𐐷𐐷𐐷𐐷𐐷", 10, 1)] + [InlineData("01234 56789 01234 56789", 6, 4)] + [Theory] + public void Should_Wrap_With_Overflow(string text, int expectedCharactersPerLine, int expectedNumberOfLines) + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var numberOfLines = 0; + + var currentPosition = 0; + + while (currentPosition < text.Length) + { + var textLine = + formatter.FormatLine(textSource, currentPosition, 1, + new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.WrapWithOverflow)); + + if (text.Length - currentPosition > expectedCharactersPerLine) + { + Assert.Equal(expectedCharactersPerLine, textLine.TextRange.Length); + } + + currentPosition += textLine.TextRange.Length; + + numberOfLines++; + } + + Assert.Equal(expectedNumberOfLines, numberOfLines); + } + } + + [InlineData("Whether to turn off HTTPS. This option only applies if Individual, " + + "IndividualB2C, SingleOrg, or MultiOrg aren't used for ‑‑auth." + , "Noto Sans", 40)] + [InlineData("01234 56789 01234 56789", "Noto Mono", 7)] + [Theory] + public void Should_Wrap(string text, string familyName, int numberOfCharactersPerLine) + { + using (Start()) + { + var lineBreaker = new LineBreakEnumerator(text.AsMemory()); + + var expected = new List(); + + while (lineBreaker.MoveNext()) + { + expected.Add(lineBreaker.Current.PositionWrap - 1); + } + + var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#" + + familyName); + + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var glyph = typeface.GlyphTypeface.GetGlyph('a'); + + var advance = typeface.GlyphTypeface.GetGlyphAdvance(glyph) * + (12.0 / typeface.GlyphTypeface.DesignEmHeight); + + var paragraphWidth = advance * numberOfCharactersPerLine; + + var currentPosition = 0; + + while (currentPosition < text.Length) + { + var textLine = + formatter.FormatLine(textSource, currentPosition, paragraphWidth, + new GenericTextParagraphProperties(defaultProperties, textWrapping: TextWrapping.Wrap)); + + Assert.True(expected.Contains(textLine.TextRange.End)); + + var index = expected.IndexOf(textLine.TextRange.End); + + for (var i = 0; i <= index; i++) + { + expected.RemoveAt(0); + } + + currentPosition += textLine.TextRange.Length; + } + } + } + + [Fact] + public void Should_Produce_Fixed_Height_Lines() + { + using (Start()) + { + const string text = "012345"; + + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties, lineHeight: 50)); + + Assert.Equal(50, textLine.LineMetrics.Size.Height); + } + } + + public static IDisposable Start() + { + var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface + .With(renderInterface: new PlatformRenderInterface(null), + textShaperImpl: new TextShaperImpl())); + + AvaloniaLocator.CurrentMutable + .Bind().ToConstant(new FontManager(new CustomFontManagerImpl())); + + return disposable; + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs similarity index 77% rename from tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs rename to tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs index a2c9f8b8cd..5d9aa2cf97 100644 --- a/tests/Avalonia.Skia.UnitTests/TextLayoutTests.cs +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLayoutTests.cs @@ -4,15 +4,33 @@ using Avalonia.Media; using Avalonia.Media.TextFormatting; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.UnitTests; +using Avalonia.Utilities; using Xunit; -namespace Avalonia.Skia.UnitTests +namespace Avalonia.Skia.UnitTests.Media.TextFormatting { public class TextLayoutTests { private static readonly string s_singleLineText = "0123456789"; private static readonly string s_multiLineText = "012345678\r\r0123456789"; + [InlineData("01234\r01234\r", 3)] + [InlineData("01234\r01234", 2)] + [Theory] + public void Should_Break_Lines(string text, int numberOfLines) + { + using (Start()) + { + var layout = new TextLayout( + text, + Typeface.Default, + 12.0f, + Brushes.Black); + + Assert.Equal(numberOfLines, layout.TextLines.Count); + } + } + [Fact] public void Should_Apply_TextStyleSpan_To_Text_In_Between() { @@ -22,17 +40,16 @@ namespace Avalonia.Skia.UnitTests var spans = new[] { - new TextStyleRun( - new TextPointer(1, 2), - new TextStyle(Typeface.Default, 12, foreground)) + new ValueSpan(1, 2, + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)) }; var layout = new TextLayout( s_multiLineText, - Typeface.Default, + Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textStyleOverrides : spans); + textStyleOverrides: spans); var textLine = layout.TextLines[0]; @@ -46,7 +63,7 @@ namespace Avalonia.Skia.UnitTests Assert.Equal("12", actual); - Assert.Equal(foreground, textRun.Style.Foreground); + Assert.Equal(foreground, textRun.Properties.ForegroundBrush); } } @@ -61,9 +78,8 @@ namespace Avalonia.Skia.UnitTests { var spans = new[] { - new TextStyleRun( - new TextPointer(0, i), - new TextStyle(Typeface.Default, 12, foreground)) + new ValueSpan(0, i, + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)) }; var expected = new TextLayout( @@ -72,22 +88,22 @@ namespace Avalonia.Skia.UnitTests 12.0f, Brushes.Black.ToImmutable(), textWrapping: TextWrapping.Wrap, - maxWidth : 25); + maxWidth: 25); var actual = new TextLayout( s_multiLineText, Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textWrapping : TextWrapping.Wrap, - maxWidth : 25, - textStyleOverrides : spans); + textWrapping: TextWrapping.Wrap, + maxWidth: 25, + textStyleOverrides: spans); Assert.Equal(expected.TextLines.Count, actual.TextLines.Count); for (var j = 0; j < actual.TextLines.Count; j++) { - Assert.Equal(expected.TextLines[j].Text.Length, actual.TextLines[j].Text.Length); + Assert.Equal(expected.TextLines[j].TextRange.Length, actual.TextLines[j].TextRange.Length); Assert.Equal(expected.TextLines[j].TextRuns.Sum(x => x.Text.Length), actual.TextLines[j].TextRuns.Sum(x => x.Text.Length)); @@ -105,9 +121,8 @@ namespace Avalonia.Skia.UnitTests var spans = new[] { - new TextStyleRun( - new TextPointer(0, 2), - new TextStyle(Typeface.Default, 12, foreground)) + new ValueSpan(0, 2, + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)) }; var layout = new TextLayout( @@ -115,7 +130,7 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textStyleOverrides : spans); + textStyleOverrides: spans); var textLine = layout.TextLines[0]; @@ -130,7 +145,7 @@ namespace Avalonia.Skia.UnitTests Assert.Equal("01", actual); - Assert.Equal(foreground, textRun.Style.Foreground); + Assert.Equal(foreground, textRun.Properties.ForegroundBrush); } } @@ -143,9 +158,8 @@ namespace Avalonia.Skia.UnitTests var spans = new[] { - new TextStyleRun( - new TextPointer(8, 2), - new TextStyle(Typeface.Default, 12, foreground)) + new ValueSpan(8, 2, + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)), }; var layout = new TextLayout( @@ -153,7 +167,7 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textStyleOverrides : spans); + textStyleOverrides: spans); var textLine = layout.TextLines[0]; @@ -167,7 +181,7 @@ namespace Avalonia.Skia.UnitTests Assert.Equal("89", actual); - Assert.Equal(foreground, textRun.Style.Foreground); + Assert.Equal(foreground, textRun.Properties.ForegroundBrush); } } @@ -180,9 +194,8 @@ namespace Avalonia.Skia.UnitTests var spans = new[] { - new TextStyleRun( - new TextPointer(0, 1), - new TextStyle(Typeface.Default, 12, foreground)) + new ValueSpan(0, 1, + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)) }; var layout = new TextLayout( @@ -190,7 +203,7 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textStyleOverrides : spans); + textStyleOverrides: spans); var textLine = layout.TextLines[0]; @@ -200,7 +213,7 @@ namespace Avalonia.Skia.UnitTests Assert.Equal(1, textRun.Text.Length); - Assert.Equal(foreground, textRun.Style.Foreground); + Assert.Equal(foreground, textRun.Properties.ForegroundBrush); } } @@ -215,9 +228,8 @@ namespace Avalonia.Skia.UnitTests var spans = new[] { - new TextStyleRun( - new TextPointer(2, 2), - new TextStyle(Typeface.Default, 12, foreground)) + new ValueSpan(2, 2, + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)) }; var layout = new TextLayout( @@ -239,7 +251,7 @@ namespace Avalonia.Skia.UnitTests Assert.Equal("😄", actual); - Assert.Equal(foreground, textRun.Style.Foreground); + Assert.Equal(foreground, textRun.Properties.ForegroundBrush); } } @@ -254,7 +266,7 @@ namespace Avalonia.Skia.UnitTests 12.0f, Brushes.Black.ToImmutable()); - Assert.Equal(s_multiLineText.Length, layout.TextLines.Sum(x => x.Text.Length)); + Assert.Equal(s_multiLineText.Length, layout.TextLines.Sum(x => x.TextRange.Length)); } } @@ -291,9 +303,8 @@ namespace Avalonia.Skia.UnitTests var spans = new[] { - new TextStyleRun( - new TextPointer(0, 24), - new TextStyle(Typeface.Default, 12, foreground)) + new ValueSpan(0, 24, + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)) }; var layout = new TextLayout( @@ -301,8 +312,8 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - textWrapping : TextWrapping.Wrap, - maxWidth : 180, + textWrapping: TextWrapping.Wrap, + maxWidth: 180, textStyleOverrides: spans); Assert.Equal( @@ -322,9 +333,8 @@ namespace Avalonia.Skia.UnitTests var spans = new[] { - new TextStyleRun( - new TextPointer(5, 20), - new TextStyle(Typeface.Default, 12, foreground)) + new ValueSpan(5, 20, + new GenericTextRunProperties(Typeface.Default, 12, foregroundBrush: foreground)) }; var layout = new TextLayout( @@ -332,13 +342,13 @@ namespace Avalonia.Skia.UnitTests Typeface.Default, 12.0f, Brushes.Black.ToImmutable(), - maxWidth : 200, - maxHeight : 125, + maxWidth: 200, + maxHeight: 125, textStyleOverrides: spans); - Assert.Equal(foreground, layout.TextLines[0].TextRuns[1].Style.Foreground); - Assert.Equal(foreground, layout.TextLines[1].TextRuns[0].Style.Foreground); - Assert.Equal(foreground, layout.TextLines[2].TextRuns[0].Style.Foreground); + Assert.Equal(foreground, layout.TextLines[0].TextRuns[1].Properties.ForegroundBrush); + Assert.Equal(foreground, layout.TextLines[1].TextRuns[0].Properties.ForegroundBrush); + Assert.Equal(foreground, layout.TextLines[2].TextRuns[0].Properties.ForegroundBrush); } } @@ -355,7 +365,7 @@ namespace Avalonia.Skia.UnitTests 12.0f, Brushes.Black.ToImmutable()); - var shapedRun = (ShapedTextRun)layout.TextLines[0].TextRuns[0]; + var shapedRun = (ShapedTextCharacters)layout.TextLines[0].TextRuns[0]; var glyphRun = shapedRun.GlyphRun; @@ -390,7 +400,7 @@ namespace Avalonia.Skia.UnitTests foreach (var textRun in textLine.TextRuns) { - var shapedRun = (ShapedTextRun)textRun; + var shapedRun = (ShapedTextCharacters)textRun; var glyphRun = shapedRun.GlyphRun; @@ -426,13 +436,13 @@ namespace Avalonia.Skia.UnitTests Assert.Equal(1, layout.TextLines[0].TextRuns.Count); - Assert.Equal(expectedLength, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Length); + Assert.Equal(expectedLength, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters.Length); - Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[5]); + Assert.Equal(5, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[5]); - if(expectedLength == 7) + if (expectedLength == 7) { - Assert.Equal(5, ((ShapedTextRun)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[6]); + Assert.Equal(5, ((ShapedTextCharacters)layout.TextLines[0].TextRuns[0]).GlyphRun.GlyphClusters[6]); } } } @@ -467,7 +477,7 @@ namespace Avalonia.Skia.UnitTests var textLine = layout.TextLines[0]; - var textRun = (ShapedTextRun)textLine.TextRuns[0]; + var textRun = (ShapedTextCharacters)textLine.TextRuns[0]; Assert.Equal(7, textRun.Text.Length); @@ -526,9 +536,28 @@ namespace Avalonia.Skia.UnitTests } } + [Fact] + public void Should_Produce_Fixed_Height_Lines() + { + using (Start()) + { + var layout = new TextLayout( + s_multiLineText, + Typeface.Default, + 12, + Brushes.Black, + lineHeight: 50); + + foreach (var line in layout.TextLines) + { + Assert.Equal(50, line.LineMetrics.Size.Height); + } + } + } + private const string Text = "日本でTest一番読まれている英字新聞・ジャパンタイムズが発信する国内外ニュースと、様々なジャンルの特集記事。"; - [Fact(Skip= "Only used for profiling.")] + [Fact(Skip = "Only used for profiling.")] public void Should_Wrap() { using (Start()) @@ -546,12 +575,12 @@ namespace Avalonia.Skia.UnitTests } } - public static IDisposable Start() + private static IDisposable Start() { var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface .With(renderInterface: new PlatformRenderInterface(null), textShaperImpl: new TextShaperImpl(), - fontManagerImpl : new CustomFontManagerImpl())); + fontManagerImpl: new CustomFontManagerImpl())); return disposable; } diff --git a/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs new file mode 100644 index 0000000000..ed00d6aaed --- /dev/null +++ b/tests/Avalonia.Skia.UnitTests/Media/TextFormatting/TextLineTests.cs @@ -0,0 +1,175 @@ +using System; +using System.Linq; +using Avalonia.Media; +using Avalonia.Media.TextFormatting; +using Avalonia.UnitTests; +using Xunit; + +namespace Avalonia.Skia.UnitTests.Media.TextFormatting +{ + public class TextLineTests + { + [InlineData("𐐷𐐷𐐷𐐷𐐷")] + [InlineData("𐐷1234")] + [Theory] + public void Should_Get_Next_Caret_CharacterHit(string text) + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var clusters = textLine.TextRuns.Cast().SelectMany(x => x.GlyphRun.GlyphClusters) + .ToArray(); + + var nextCharacterHit = new CharacterHit(0); + + for (var i = 1; i < clusters.Length; i++) + { + nextCharacterHit = textLine.GetNextCaretCharacterHit(nextCharacterHit); + + Assert.Equal(clusters[i], nextCharacterHit.FirstCharacterIndex + nextCharacterHit.TrailingLength); + } + } + } + + [InlineData("𐐷𐐷𐐷𐐷𐐷")] + [InlineData("𐐷1234")] + [Theory] + public void Should_Get_Previous_Caret_CharacterHit(string text) + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new SingleBufferTextSource(text, defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var clusters = textLine.TextRuns.Cast().SelectMany(x => x.GlyphRun.GlyphClusters) + .ToArray(); + + var previousCharacterHit = new CharacterHit(clusters[^1]); + + for (var i = clusters.Length - 2; i > 0; i--) + { + previousCharacterHit = textLine.GetPreviousCaretCharacterHit(previousCharacterHit); + + Assert.Equal(clusters[i], previousCharacterHit.FirstCharacterIndex); + } + } + } + + [Fact] + public void Should_Get_Distance_From_CharacterHit() + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new MultiBufferTextSource(defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var currentDistance = 0.0; + + foreach (var run in textLine.TextRuns) + { + var textRun = (ShapedTextCharacters)run; + + var glyphRun = textRun.GlyphRun; + + for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) + { + var cluster = glyphRun.GlyphClusters[i]; + + var glyph = glyphRun.GlyphIndices[i]; + + var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; + + var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); + + Assert.Equal(currentDistance, distance); + + currentDistance += advance; + } + } + + Assert.Equal(currentDistance, + textLine.GetDistanceFromCharacterHit(new CharacterHit(MultiBufferTextSource.TextRange.Length))); + } + } + + [Fact] + public void Should_Get_CharacterHit_From_Distance() + { + using (Start()) + { + var defaultProperties = new GenericTextRunProperties(Typeface.Default); + + var textSource = new MultiBufferTextSource(defaultProperties); + + var formatter = new TextFormatterImpl(); + + var textLine = + formatter.FormatLine(textSource, 0, double.PositiveInfinity, + new GenericTextParagraphProperties(defaultProperties)); + + var currentDistance = 0.0; + + CharacterHit characterHit; + + foreach (var run in textLine.TextRuns) + { + var textRun = (ShapedTextCharacters)run; + + var glyphRun = textRun.GlyphRun; + + for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) + { + var cluster = glyphRun.GlyphClusters[i]; + + var glyph = glyphRun.GlyphIndices[i]; + + var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; + + characterHit = textLine.GetCharacterHitFromDistance(currentDistance); + + Assert.Equal(cluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength); + + currentDistance += advance; + } + } + + characterHit = textLine.GetCharacterHitFromDistance(textLine.LineMetrics.Size.Width); + + Assert.Equal(MultiBufferTextSource.TextRange.End, characterHit.FirstCharacterIndex); + } + } + + private static IDisposable Start() + { + var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface + .With(renderInterface: new PlatformRenderInterface(null), + textShaperImpl: new TextShaperImpl(), + fontManagerImpl: new CustomFontManagerImpl())); + + return disposable; + } + } +} diff --git a/tests/Avalonia.Skia.UnitTests/SimpleTextFormatterTests.cs b/tests/Avalonia.Skia.UnitTests/SimpleTextFormatterTests.cs deleted file mode 100644 index 8e695a11c8..0000000000 --- a/tests/Avalonia.Skia.UnitTests/SimpleTextFormatterTests.cs +++ /dev/null @@ -1,373 +0,0 @@ -using System; -using System.Collections.Generic; -using Avalonia.Media; -using Avalonia.Media.TextFormatting; -using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.UnitTests; -using Avalonia.Utility; -using Xunit; - -namespace Avalonia.Skia.UnitTests -{ - public class SimpleTextFormatterTests - { - [Fact] - public void Should_Format_TextRuns_With_Default_Style() - { - using (Start()) - { - const string text = "0123456789"; - - var defaultTextRunStyle = new TextStyle(Typeface.Default, 12, Brushes.Black); - - var textSource = new SimpleTextSource(text, defaultTextRunStyle); - - var formatter = new SimpleTextFormatter(); - - var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); - - Assert.Single(textLine.TextRuns); - - var textRun = textLine.TextRuns[0]; - - Assert.Equal(defaultTextRunStyle.TextFormat, textRun.Style.TextFormat); - - Assert.Equal(defaultTextRunStyle.Foreground, textRun.Style.Foreground); - - Assert.Equal(text.Length, textRun.Text.Length); - } - } - - [Fact] - public void Should_Format_TextRuns_With_Multiple_Buffers() - { - using (Start()) - { - var defaultTextRunStyle = new TextStyle(Typeface.Default, 12, Brushes.Black); - - var textSource = new MultiBufferTextSource(defaultTextRunStyle); - - var formatter = new SimpleTextFormatter(); - - var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, - new TextParagraphProperties(defaultTextRunStyle)); - - Assert.Equal(5, textLine.TextRuns.Count); - - Assert.Equal(50, textLine.Text.Length); - } - } - - private class MultiBufferTextSource : ITextSource - { - private readonly string[] _runTexts; - private readonly TextStyle _defaultStyle; - - public MultiBufferTextSource(TextStyle defaultStyle) - { - _defaultStyle = defaultStyle; - - _runTexts = new[] { "A123456789", "B123456789", "C123456789", "D123456789", "E123456789" }; - } - - public TextPointer TextPointer => new TextPointer(0, 50); - - public TextRun GetTextRun(int textSourceIndex) - { - if (textSourceIndex == 50) - { - return new TextEndOfParagraph(); - } - - var index = textSourceIndex / 10; - - var runText = _runTexts[index]; - - return new TextCharacters( - new ReadOnlySlice(runText.AsMemory(), textSourceIndex, runText.Length), _defaultStyle); - } - } - - [Fact] - public void Should_Format_TextRuns_With_TextRunStyles() - { - using (Start()) - { - const string text = "0123456789"; - - var defaultStyle = new TextStyle(Typeface.Default, 12, Brushes.Black); - - var textStyleRuns = new[] - { - new TextStyleRun(new TextPointer(0, 3), defaultStyle ), - new TextStyleRun(new TextPointer(3, 3), new TextStyle(Typeface.Default, 13, Brushes.Black) ), - new TextStyleRun(new TextPointer(6, 3), new TextStyle(Typeface.Default, 14, Brushes.Black) ), - new TextStyleRun(new TextPointer(9, 1), defaultStyle ) - }; - - var textSource = new FormattableTextSource(text, defaultStyle, textStyleRuns); - - var formatter = new SimpleTextFormatter(); - - var textLine = formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); - - Assert.Equal(text.Length, textLine.Text.Length); - - for (var i = 0; i < textStyleRuns.Length; i++) - { - var textStyleRun = textStyleRuns[i]; - - var textRun = textLine.TextRuns[i]; - - Assert.Equal(textStyleRun.TextPointer.Length, textRun.Text.Length); - } - } - } - - private class FormattableTextSource : ITextSource - { - private readonly ReadOnlySlice _text; - private readonly TextStyle _defaultStyle; - private ReadOnlySlice _textStyleRuns; - - public FormattableTextSource(string text, TextStyle defaultStyle, ReadOnlySlice textStyleRuns) - { - _text = text.AsMemory(); - - _defaultStyle = defaultStyle; - - _textStyleRuns = textStyleRuns; - } - - public TextRun GetTextRun(int textSourceIndex) - { - if (_textStyleRuns.IsEmpty) - { - return new TextEndOfParagraph(); - } - - var styleRun = _textStyleRuns[0]; - - _textStyleRuns = _textStyleRuns.Skip(1); - - return new TextCharacters(_text.AsSlice(styleRun.TextPointer.Start, styleRun.TextPointer.Length), - _defaultStyle); - } - } - - [Theory] - [InlineData("0123", 1)] - [InlineData("\r\n", 1)] - [InlineData("👍b", 2)] - [InlineData("a👍b", 3)] - [InlineData("a👍子b", 4)] - public void Should_Produce_Unique_Runs(string text, int numberOfRuns) - { - using (Start()) - { - var textSource = new SimpleTextSource(text, new TextStyle(Typeface.Default)); - - var formatter = new SimpleTextFormatter(); - - var textLine = - formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); - - Assert.Equal(numberOfRuns, textLine.TextRuns.Count); - } - } - - private class SimpleTextSource : ITextSource - { - private readonly ReadOnlySlice _text; - private readonly TextStyle _defaultTextStyle; - - public SimpleTextSource(string text, TextStyle defaultText) - { - _text = text.AsMemory(); - _defaultTextStyle = defaultText; - } - - public TextRun GetTextRun(int textSourceIndex) - { - var runText = _text.Skip(textSourceIndex); - - if (runText.IsEmpty) - { - return new TextEndOfParagraph(); - } - - return new TextCharacters(runText, _defaultTextStyle); - } - } - - [Fact] - public void Should_Split_Run_On_Script() - { - using (Start()) - { - const string text = "1234الدولي"; - - var textSource = new SimpleTextSource(text, new TextStyle(Typeface.Default)); - - var formatter = new SimpleTextFormatter(); - - var textLine = - formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); - - Assert.Equal(4, textLine.TextRuns[0].Text.Length); - } - } - - [Fact] - public void Should_Get_Distance_From_CharacterHit() - { - using (Start()) - { - var textSource = new MultiBufferTextSource(new TextStyle(Typeface.Default)); - - var formatter = new SimpleTextFormatter(); - - var textLine = - formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); - - var currentDistance = 0.0; - - foreach (var run in textLine.TextRuns) - { - var textRun = (ShapedTextRun)run; - - var glyphRun = textRun.GlyphRun; - - for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) - { - var cluster = glyphRun.GlyphClusters[i]; - - var glyph = glyphRun.GlyphIndices[i]; - - var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; - - var distance = textLine.GetDistanceFromCharacterHit(new CharacterHit(cluster)); - - Assert.Equal(currentDistance, distance); - - currentDistance += advance; - } - } - - Assert.Equal(currentDistance, textLine.GetDistanceFromCharacterHit(new CharacterHit(textSource.TextPointer.Length))); - } - } - - [Fact] - public void Should_Get_CharacterHit_From_Distance() - { - using (Start()) - { - var textSource = new MultiBufferTextSource(new TextStyle(Typeface.Default)); - - var formatter = new SimpleTextFormatter(); - - var textLine = - formatter.FormatLine(textSource, 0, double.PositiveInfinity, new TextParagraphProperties()); - - var currentDistance = 0.0; - - CharacterHit characterHit; - - foreach (var run in textLine.TextRuns) - { - var textRun = (ShapedTextRun)run; - - var glyphRun = textRun.GlyphRun; - - for (var i = 0; i < glyphRun.GlyphClusters.Length; i++) - { - var cluster = glyphRun.GlyphClusters[i]; - - var glyph = glyphRun.GlyphIndices[i]; - - var advance = glyphRun.GlyphTypeface.GetGlyphAdvance(glyph) * glyphRun.Scale; - - characterHit = textLine.GetCharacterHitFromDistance(currentDistance); - - Assert.Equal(cluster, characterHit.FirstCharacterIndex + characterHit.TrailingLength); - - currentDistance += advance; - } - } - - characterHit = textLine.GetCharacterHitFromDistance(textLine.LineMetrics.Size.Width); - - Assert.Equal(textSource.TextPointer.End, characterHit.FirstCharacterIndex); - } - } - - [InlineData("Whether to turn off HTTPS. This option only applies if Individual, " + - "IndividualB2C, SingleOrg, or MultiOrg aren't used for ‑‑auth." - , "Noto Sans", 40)] - [InlineData("01234 56789 01234 56789", "Noto Mono", 7)] - [Theory] - public void Should_Wrap_Text(string text, string familyName, int numberOfCharactersPerLine) - { - using (Start()) - { - var lineBreaker = new LineBreakEnumerator(text.AsMemory()); - - var expected = new List(); - - while (lineBreaker.MoveNext()) - { - expected.Add(lineBreaker.Current.PositionWrap - 1); - } - - var typeface = new Typeface("resm:Avalonia.Skia.UnitTests.Assets?assembly=Avalonia.Skia.UnitTests#" + - familyName); - - var defaultStyle = new TextStyle(typeface); - - var textSource = new SimpleTextSource(text, defaultStyle); - - var formatter = new SimpleTextFormatter(); - - var glyph = typeface.GlyphTypeface.GetGlyph('a'); - - var advance = typeface.GlyphTypeface.GetGlyphAdvance(glyph) * - (12.0 / typeface.GlyphTypeface.DesignEmHeight); - - var paragraphWidth = advance * numberOfCharactersPerLine; - - var currentPosition = 0; - - while (currentPosition < text.Length) - { - var textLine = - formatter.FormatLine(textSource, currentPosition, paragraphWidth, - new TextParagraphProperties(defaultStyle, textWrapping: TextWrapping.Wrap)); - - Assert.True(expected.Contains(textLine.Text.End)); - - var index = expected.IndexOf(textLine.Text.End); - - for (var i = 0; i <= index; i++) - { - expected.RemoveAt(0); - } - - currentPosition += textLine.Text.Length; - } - } - } - - public static IDisposable Start() - { - var disposable = UnitTestApplication.Start(TestServices.MockPlatformRenderInterface - .With(renderInterface: new PlatformRenderInterface(null), - textShaperImpl: new TextShaperImpl())); - - AvaloniaLocator.CurrentMutable - .Bind().ToConstant(new FontManager(new CustomFontManagerImpl())); - - return disposable; - } - } -} diff --git a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs index 0772e0e9bd..fe1c34385f 100644 --- a/tests/Avalonia.UnitTests/MockTextShaperImpl.cs +++ b/tests/Avalonia.UnitTests/MockTextShaperImpl.cs @@ -1,19 +1,19 @@ -using Avalonia.Media; -using Avalonia.Media.TextFormatting; +using System; +using System.Globalization; +using Avalonia.Media; using Avalonia.Media.TextFormatting.Unicode; using Avalonia.Platform; -using Avalonia.Utility; +using Avalonia.Utilities; namespace Avalonia.UnitTests { public class MockTextShaperImpl : ITextShaperImpl { - public GlyphRun ShapeText(ReadOnlySlice text, TextFormat textFormat) + public GlyphRun ShapeText(ReadOnlySlice text, Typeface typeface, double fontRenderingEmSize, CultureInfo culture) { - var glyphTypeface = textFormat.Typeface.GlyphTypeface; + var glyphTypeface = typeface.GlyphTypeface; var glyphIndices = new ushort[text.Length]; - var height = textFormat.FontMetrics.LineHeight; - var width = 0.0; + var glyphCount = 0; for (var i = 0; i < text.Length;) { @@ -27,10 +27,11 @@ namespace Avalonia.UnitTests glyphIndices[index] = glyph; - width += glyphTypeface.GetGlyphAdvance(glyph); + glyphCount++; } - return new GlyphRun(glyphTypeface, textFormat.FontRenderingEmSize, glyphIndices, characters: text); + return new GlyphRun(glyphTypeface, fontRenderingEmSize, + new ReadOnlySlice(glyphIndices.AsMemory(0, glyphCount)), characters: text); } } } diff --git a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs index 028caa35c6..219c7ece46 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/GlyphRunTests.cs @@ -1,7 +1,7 @@ using Avalonia.Media; using Avalonia.Platform; using Avalonia.UnitTests; -using Avalonia.Utility; +using Avalonia.Utilities; using Xunit; namespace Avalonia.Visuals.UnitTests.Media diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs index 3ed5cfb0b2..3d489af3a2 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TextFormatting/LineBreakerTests.cs @@ -1,6 +1,6 @@ using System; using Avalonia.Media.TextFormatting.Unicode; -using Avalonia.Utility; +using Avalonia.Utilities; using Xunit; namespace Avalonia.Visuals.UnitTests.Media.TextFormatting diff --git a/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs b/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs index 0e43c76da1..e526172622 100644 --- a/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs +++ b/tests/Avalonia.Visuals.UnitTests/Media/TypefaceTests.cs @@ -9,7 +9,7 @@ namespace Avalonia.Visuals.UnitTests.Media [Fact] public void Exception_Should_Be_Thrown_If_FontWeight_LessThanEqualTo_Zero() { - Assert.Throws(() => new Typeface("foo", 0, (FontStyle)12)); + Assert.Throws(() => new Typeface("foo", (FontStyle)12, 0)); } [Fact]